Metadata-Version: 2.4
Name: nexhelm
Version: 0.3.1
Summary: Nexhelm CLI (`nex`). Public client for Nexhelm AI — login, workspaces, workflows. No core IP; the backend is the boundary.
Author: Nexhelm
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27.0
Requires-Dist: mcp>=1.0.0
Requires-Dist: pydantic>=2.6.0
Requires-Dist: rich>=13.7.0
Requires-Dist: typer>=0.12.0
Requires-Dist: websockets>=12.0
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: respx>=0.21.0; extra == 'dev'
Requires-Dist: ruff>=0.4.0; extra == 'dev'
Description-Content-Type: text/markdown

# nexhelm-mcp

CLI + MCP server for Nexhelm. Lets Claude Code (and humans) design, verify,
and debug Workflow Lab runs end-to-end without leaving the terminal.

## Architecture

```
═══════════════ EXTERNAL ════════════════════════════════════════════
   human ──┐
           │ approves at gates, paste transcript
           ▼
   Claude Code (external) ─── reads/writes code in repo with approval
   │ stdio
   ▼
═══════════════ SURFACE TIER (this package) ═════════════════════════
   nexhelm-mcp server  +  nex CLI
     - agent broker, capability scopes, view_urls, exit codes
     - never inlines prompts; never returns LLM internals
     - persona-as-action surface:
         lab_workflow.{integration_engineer, designer, refine, run, verifier, trace, evaluator, show, list}
   │ HTTPS, public schemas only
   ▼
═══════════════ ENGINE TIER (Nexhelm FastAPI, blackbox) ═════════════
   Persona registry (proprietary):
     integration_engineer    services/personas/integration_engineer/
     workflow_designer       services/workflow_lab/designer.py
     verifier_orchestrator   services/workflow_lab/orchestrator.py
     workflow_evaluator      services/workflow_lab/evaluator.py
     (future) log_aggregator, firm_wide, code, infra, cfp, ...

   Each persona owns its prompts (private), model config (private), and
   exposes a stable public input/output schema. The HTTP boundary crosses
   ONLY public schemas. No prompt or chain-of-thought ever reaches the
   surface tier.
```

**Agent broker, not REST wrapper.** The surface tier is pure transport.
Each agent in the broker registers via `agents/__init__.py`; the server
and CLI loop over the registry. Adding a "code agent" or "CFP agent" is
one new file plus one import.

**Persona pattern (engine).** New blackbox personas live in
`fastapi_app/app/services/personas/<name>/{persona.py, prompts.py,
schemas.py}`. `prompts.py` is private; `schemas.py` is the public
contract; routers serialize only the schema.

**One-click URLs everywhere.** Every agent return value includes `view_url`
(and `verification_task_url` where applicable) so Claude prints a link and
the user clicks straight into the frontend.

## Install

**Recommended (any user, any machine):** one-shot via `install.sh`:

```bash
bash backend/tools/nexhelm-mcp/install.sh
# pipx-installs `nex` and `nexhelm-mcp-server` on PATH.
# Prints the exact Claude Code MCP config block to paste.
```

**Or manual:**

```bash
cd backend/tools/nexhelm-mcp
python3.12 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
pytest -q       # 117 passing
nex --help
```

Two entrypoints (`pyproject.toml`):

- `nex` — human CLI
- `nexhelm-mcp-server` — stdio MCP server for Claude Code

## Setup — `nex auth setup -e EMAIL` (recommended)

Email + password, no browser, no env files. Works on any machine.

```bash
nex auth setup -e you@nexhelm.ai --backend-url <BACKEND_URL>
# Password: ****
# bootstrap: supabase_url=https://<project>.supabase.co; required email domain @nexhelm.ai
# ok authenticated as you@nexhelm.ai
#
# Your clients (12)
# 1 c_001 Acme Wealth   ops@acme.com
# 2 c_002 Smith FO      ...
# Pick by # (Enter to skip): 2
# ok Saved to ~/.nex/config (mode 0600)

nex doctor
```

What's happening:
1. Backend's `GET /auth/cli-config` returns the public Supabase URL + anon key
2. CLI calls Supabase Auth `/token?grant_type=password` with email + password
3. CLI POSTs the resulting Supabase access token to backend `POST /auth/cli-token`
4. Backend gates `@nexhelm.ai`, mints a 24h HS256 service JWT, returns
   it + your clients
5. CLI saves the bundle to `~/.nex/config`

No `frontend/.env.local` needed. No shared HMAC secret on your machine.

### Optional: `nex auth login` (browser / PKCE)

Same end state as `setup -e`, but uses Supabase OAuth (Google by default)
instead of email + password. Useful if you'd rather not enter your
password into a CLI. Requires a one-time Supabase dashboard step:

- **Auth → Providers**: enable Google (or pass `--provider <name>`)
- **Auth → URL Configuration → Redirect URLs**: add `http://localhost:7866/callback`

If `redirect_to` isn't whitelisted, Supabase falls back to the project's
"Site URL" and the browser lands somewhere weird (usually the deployed
frontend behind Vercel SSO). One allowlist entry fixes it for everyone.

### Manual fallback: `nex auth setup` (legacy local-mint)

Backend devs only. Reads the shared HMAC secret from `frontend/.env.local`
(or `NEX_*` env vars) and mints JWTs locally per request. No backend
hop, no Supabase round-trip — but only works for users with that env
file on disk. Don't ship to the team; tell them to use `setup -e`.

```bash
nex auth setup
# Your user_id (Supabase UUID): <paste>
# Your client_id: <paste>
```

The CLI + the backend now live on the same branch (`marx/nex-cli-phase1`,
which is rebased on top of `marx/firm-workflow-context-design`). Point
`--backend-url` at any backend running this branch. Once both merge to
main/staging, point at staging/main as normal.

## Configure Claude Code

```jsonc
{
  "mcpServers": {
    "nexhelm": { "command": "nexhelm-mcp-server" }
  }
}
```

The MCP server inherits the same `~/.nex/config` and `frontend/.env.local`
that the CLI uses, so once `nex doctor` is green, Claude Code is good to go.

## Sandbox verification

Verification runs the agent **as a dedicated sandbox user**, not as you.
Each org has one sandbox user (a real `user_profiles` row + a real Google
or SharePoint account, distinct from any human operator). The verifier
agent's `user_id` is swapped to that sandbox user at `/agent/init` and
`/agent/chat` time, so every Drive / Gmail / Calendar / CRM call hits the
sandbox tenant — your own data is untouchable by construction.

Before each run, the orchestrator **wipes the sandbox account's Drive (or
SharePoint scope) and rebuilds it from a blueprint** — folders, docs,
sheets — recorded on the org's `sandbox_environments` row. After
`finalise_run` (or `cancel_run`) the same wipe runs again. So fixtures
are declarative; you don't preload your own Drive.

### One-time per org

1. Create a real Google / Microsoft account for the sandbox (e.g.
   `sandbox+<orgslug>@nexhelm.ai`); have it sign up through the normal
   user-onboarding flow so a `user_profiles` row exists with the right
   `organization_id`.
2. From the sandbox account, OAuth-connect Google Drive (or SharePoint)
   via the standard connect-apps flow so `oauth_credentials` is populated
   — `get_fresh_access_token(sandbox_user_id, "google_drive")` must
   succeed for setup/teardown to work.
3. `POST /workflow-lab/sandbox-client` — creates the dummy `clients` row
   the verifier targets as `target_client_id`.
4. `POST /workflow-lab/sandbox-environments` with `provider` + a
   `blueprint` JSON describing the directory layout the workflow
   expects, e.g.:
   ```jsonc
   {
     "provider": "google",
     "blueprint": {
       "container": { "type": "folder_root", "name_template": "WFL Sandbox — {{organization_name}}" },
       "folders":   [{ "path": "Tax Returns 2023" }],
       "docs":      [{ "path": "Tax Returns 2023", "title": "Doe_2023_1040", "text": "..." }],
       "sheets":    [{ "path": "Tax Returns 2023", "title": "Doe_2023_W2",   "rows": [...] }]
     }
   }
   ```
   DB triggers enforce **exactly one sandbox user per org**; supported
   providers are `google` and `sharepoint`.

### Per run

Pass `-s/--sandbox-user-id` at design time; discover candidates with
`GET /workflow-lab/sandbox-accounts`:

```bash
nex workflow designer -u <gdoc> -s <sandbox_user_uuid>      # bind run to sandbox user
nex workflow run <run_id>                                    # setup → drive wiped + rebuilt; agent runs as sandbox user
nex workflow evaluator <run_id> --verdict approved           # cleanup wipes drive again
```

### Concurrency + post-hoc gate

- Only **one active run per sandbox user** (enforced by
  `idx_workflow_lab_runs_active_sandbox_user`). Two SEs in the same org
  cannot verify concurrently; the second `start-verification` is
  rejected.
- Sandbox isolates blast radius; the evaluator still scans the trace for
  side-effect tools (`send_/create_/update_/delete_/post_/patch_`). If
  any fired and no reviewer verdict was supplied, terminal status is
  `awaiting_human_verify` — a human has to walk the trace before the
  design is stamped `verified`.

## CLI commands

### Auth

| Command | What it does |
|---|---|
| `nex auth setup [-e EMAIL] [-b BACKEND_URL] [-f FRONTEND_URL] [--user-id ID] [--client-id ID] [--skip-verify]` | Interactive setup → `~/.nex/config` (mode 0600). With `-e`, does Supabase email/password login + numbered client picker. |
| `nex auth status` | Show config + probe `/workflow-lab/runs`. |
| `nex auth clear` | Delete `~/.nex/config`. |

### Workflow

Persona-as-action grammar. Same names appear as MCP tools (`agents.lab_workflow.<persona>`) — CLI and MCP stay in sync by construction.

| Command | What it does |
|---|---|
| `nex workflow integration-engineer [-f FILE \| --transcript-text TEXT \| -u GDOC_URL] [--json]` | Run the **integration_engineer** persona: identify integration gaps. Returns capabilities the workflow needs and which are covered by existing Nexhelm tools vs missing. Run **before** `designer` when you suspect new integrations may be required. Powers the engineering loop (see below). |
| `nex workflow sandbox-accounts [--include-disabled] [--json]` | List sandbox user accounts available to the operator. Surfaces `sandbox_user_id` + email + organization + which providers each sandbox has `sandbox_environments` rows for (note: this is snapshot-scope provisioning, **not** OAuth/API-key connectivity — Wealthbox tokens etc. don't show up here). Use to discover the value for `designer --sandbox-user-id`. |
| `nex workflow designer [-f FILE \| --transcript-text TEXT \| -u GDOC_URL \| --transcript-doc-id ID] [-s SANDBOX_USER_ID] [--target-client-id ID] [--json]` | Run the **designer** persona on a transcript → returns `run_id`, `view_url`, proposal. The designer prompt favors fully-autonomous workflows (≥80% agent-only steps); HITL pauses are flagged `[HITL: <reason>]` only when there's a hard regulatory reason. **`-s/--sandbox-user-id`** binds the run to a sandbox account; the resulting task_definition lives in the operator's org but the sandbox user gets a direct edit permission so they can resolve it via Refine-with-AI URLs. Discover ids via `nex workflow sandbox-accounts`. |
| `nex workflow refine RUN_ID -m "FEEDBACK" \| -F FILE [--json]` | Re-run the designer with operator feedback. Produces a NEW run id. The source run is **auto-cancelled** to free the single active-sandbox slot (`idx_workflow_lab_runs_active_sandbox_user`) — no need to `cancel` manually before refining. For sandbox runs that already passed `start_verification`, the auto-cancel routes through full cleanup so adapter state stays symmetric. |
| `nex workflow run RUN_ID [-k PROMPT] [--as EMAIL] [--json]` | **Autonomous end-to-end.** Auto-fires the verifier (idempotent) then drains `/langchain-agent/chat`. Returns the agent transcript + a `stop_reason` so you see *where* it stopped: `complete` (call `evaluator`), `interaction` (HITL pause — refine to remove, or finish in dashboard), `error` (revise design), `cut_off` (network blip — re-run). Default kickoff prompt instructs the agent to be autonomous and fail loudly rather than ask. **Sandbox runs auto-switch identity:** if the run has `sandbox_user_id`, the backend transparently swaps the chat's `user_id` to the sandbox user (so its OAuth tokens / API keys drive tool calls) **without** forcing `read_only` — sandbox writes are allowed by design. No `--as` needed. **`--as EMAIL`** is still available for non-sandbox impersonation (e.g. debug against a real user's data); always READ-ONLY (write tools filtered). Operator must be in backend's `IMPERSONATION_ALLOWLIST`. |
| `nex workflow verifier RUN_ID [--json]` | Manual path: spawn a `client_task` only. Prefer `run` for autonomous verification. |
| `nex workflow trace RUN_ID [--json]` | Run state + verifier conversation rendered inline (role + tool calls). |
| `nex workflow evaluator RUN_ID [--verifier-thread-id ID] [--verifier-execution-id ID] [--verdict approved\|rejected] [--note TEXT] [--json]` | Run the **evaluator** persona: stamp run with trace ids + verdict. Verdict omitted ⇒ auto-classify. |
| `nex workflow show RUN_ID [--json]` | Fetch a single run by id. |
| `nex workflow list [-n LIMIT] [--sandbox-user-id ID] [--active] [--json]` | List runs for the configured user (1–200, default 50). `--sandbox-user-id` filters to runs bound to that sandbox; pair with `--active` to drop terminal-status rows — handy for finding which run is holding the active-sandbox slot. |
| `nex workflow cancel [RUN_ID] [--sandbox-user-id ID] [--all-active] [--json]` | Cancel a run. Three mutually-exclusive modes: positional `RUN_ID` cancels one specific run; `--sandbox-user-id` cancels every non-terminal run bound to that sandbox; `--all-active` cancels every non-terminal run owned by the operator. Useful as a manual rescue when refine/designer can't auto-resolve (e.g. a run is stuck in `setting_up`). |

### Diagnostics

| Command | What it does |
|---|---|
| `nex doctor` | End-to-end env checks (config, secret, JWT mint, backend reachability). Exit 0 = green. |

## MCP tools exposed to Claude Code

10 tools, each gated by a `PHASE_1_SCOPES` capability (`auth.py`):

| Tool | Args | Notes |
|---|---|---|
| `agents.list` | — | unscoped; tool registry browse |
| `agents.lab_workflow.integration_engineer` | `transcript_text? \| transcript_url?` | gap analysis pre-design |
| `agents.lab_workflow.designer` | `transcript_text? \| transcript_url? \| transcript_doc_id?`, `target_client_id?` | designer prompt biased toward HITL-free workflows |
| `agents.lab_workflow.refine` | `run_id`, `feedback` | re-runs designer with feedback; produces NEW run id |
| `agents.lab_workflow.run` | `run_id`, `kickoff_prompt?`, `as_email?` | **autonomous run.** drains chat stream; returns `events`, `stop_reason`, `thread_id`. long-running. `as_email` impersonates that user (READ-ONLY): operator's email must be in backend `IMPERSONATION_ALLOWLIST`; backend resolves email→user_id; write tools filtered. |
| `agents.lab_workflow.verifier` | `run_id` | manual path; spawns client_task only |
| `agents.lab_workflow.trace` | `run_id` | run + verifier conversation |
| `agents.lab_workflow.evaluator` | `run_id`, `verifier_thread_id?`, `verifier_execution_id?`, `reviewer_verdict?`, `reviewer_note?` | stamps verdict |
| `agents.lab_workflow.show` | `run_id` | run record |
| `agents.lab_workflow.list` | `limit=50` | recent runs |

## Impersonation (admin, read-only)

For verifying a workflow against a real (non-sandbox) user's data without
writing to their systems. Sandbox runs **do not need `--as`** — the
backend auto-switches identity via the `client_task → workflow_lab_run →
sandbox_user_id` lookup and allows writes (sandbox is throwaway by
design). Use `--as` for the genuinely different case: "run as Alice from
firmcorp so I can see what *her* Drive looks like".

`nex workflow run --as <email>` runs the agent against the target's
OAuth tokens (Drive/Gmail/Calendar/...) but with **write tools filtered
out of the agent's vocabulary** — `send_*`, `create_*`, `update_*`,
`delete_*`, `upload_*`, `post_*`, etc. The LLM literally cannot call
them, so accidental writes are impossible by construction (not by HITL
gate).

```bash
nex workflow run <run_id> --as alice@firmcorp.com
# magenta banner: "impersonating user_email=alice@firmcorp.com (READ-ONLY)"
# kickoff: <default kickoff>
# running... agent may take 30-300s on complex workflows
# [agent transcript with no writes possible]
```

**Allowlist gate (backend):** your email must be in
`IMPERSONATION_ALLOWLIST` (env var, comma-separated, case-insensitive):

```bash
# in your backend env (e.g. .env.local for local dev):
IMPERSONATION_ALLOWLIST=admin@nexhelm.ai,ops@nexhelm.ai
```

Without that, `chat` returns 403 and the CLI surfaces the error. The check
runs at `app/services/impersonation.py::resolve_impersonation` and is
re-validated on every chat call — there's no token-level grant. The target
email is resolved to a `user_id` via `user_profiles` (404 if no match).

The CLI looks up the operator's email in `~/.nex/config` (field
`user_email`, set by `nex auth setup -e <email>`). If missing, `--as`
fails fast with a clear error before any backend call.

**What's filtered:** the prefix list in `impersonation.py:_WRITE_TOOL_PREFIXES`
plus a hand-audited override list. Tools matching `set_thread_name` and a
small list of harmless mutations are kept. New tools that should be
considered writes can be added to `_WRITE_TOOL_NAMES`.

**Future (post-MVP):**
- Replace env-var allowlist with an `impersonation_grants` table
  (organization-scoped admin role + time-bound grants + audit log).
- Per-tool allow/deny lists so an admin can opt-in to a narrow write
  (e.g. send a single confirmation email, scoped + signed).

## Engineering loop — adding an integration

The full lifecycle from "user pastes a transcript" to "PR opened":

```
human pastes transcript
        │
        ▼
1. ANALYZE        nex workflow integration-engineer -u <gdoc>
                  → capabilities table, which exist, which are missing
        │
        ▼ (only if any are missing)
2. PLAN/IMPLEMENT external Claude Code (Claude Code) invokes the
                  add-integration skill at ~/.claude/skills/add-integration.md
                  Five phases with explicit approval gates:
                  CONSUME GAP REPORT  → wait `plan`
                  PROPOSE PLAN        → wait `approve`
                  IMPLEMENT           → wait `lab`
                  LAB VERIFY          → wait `pr`
                  PR
                  External Claude does code reads/writes locally; nex never
                  edits code.
        │
        ▼
3. DESIGN         nex workflow designer -u <gdoc>
                                       [--sandbox-user-id <id>]
                  → run_id, view_url, proposal
                  Designer prompt is biased toward fully-autonomous workflows
                  (≥80% agent-only steps); HITL pauses are flagged
                  `[HITL: <reason>]` only for hard regulatory gates.
                  Pass `--sandbox-user-id` to bind to a sandbox account
                  (discover with `nex workflow sandbox-accounts`); the run
                  then auto-switches identity in `run` (no `--as` needed).
        │
        ▼ (optional: review proposal, refine if needed)
3a. REFINE        nex workflow refine <run_id> -m "tighten X / drop Y"
                  → NEW run_id with refined proposal. Source run is
                  auto-cancelled to free the active-sandbox slot.
                  Loop here until the proposal is close enough to verify.
        │
        ▼
4. RUN            nex workflow run <run_id>
                  → autonomous: idempotently spawn verifier client_task,
                    POST kickoff to /langchain-agent/chat, drain SSE.
                  → prints conversation + stop_reason:
                      complete     → step 5
                      interaction  → revise (refine) or finish in dashboard
                      error        → revise the task definition; re-run
                      cut_off      → network/backend blip; re-run
                  This is the visibility primitive — operator sees where
                  the agent stopped and revises until it runs end-to-end
                  without intervention. (Manual fallback: `verifier` +
                  finish in dashboard + `trace`.)
        │
        ▼
5. FINALISE       nex workflow evaluator <run_id> --verdict approved|rejected
                  --verifier-thread-id <from `run` output>
        │
        ▼
6. PR             external Claude opens PR via `gh pr create` (linked from
                  the skill); engineering review takes over.
```

**Engineering vs verification — separation of concerns.** Nex runs the
analyze + design + verify orchestration (engine tier); external Claude
reads and writes code in the repo (with approval gates from the skill);
human approves at every gate. The CLI is the only thing the human ever
runs; the MCP server is the only thing external Claude calls. No double-
implementation between nex and Claude.

**Where the skill lives.** External, in `~/.claude/skills/add-integration.md`.
Deliberately outside the codebase — keeps your code engine and the external
AI/human persona separate. Update the skill independently of any repo PR.

## Code layout (`src/nexhelm_mcp/`)

| File | Role |
|---|---|
| `cli.py` | Typer app with auth/workflow/doctor command groups |
| `server.py` | FastMCP server, one wrapper per agent tool |
| `agents/lab_workflow.py` | Reference agent over `/workflow-lab/*` |
| `auth.py` | HS256 JWT minting (≤90s TTL); `PHASE_1_SCOPES` |
| `client.py` | `BackendClient` httpx wrapper |
| `config.py` | `~/.nex/config` (0600); reads secret from `frontend/.env.local` at runtime |
| `supabase_auth.py` | Email/password login + client list helper |
| `login.py` | Phase-2 PKCE browser flow placeholder |

## Adding a new agent

1. Create `src/nexhelm_mcp/agents/<name>.py` exposing `AGENT = Agent(...)`
   with one or more `AgentTool(...)` entries. Each `AgentTool.scope` must be
   added to `auth.PHASE_1_SCOPES` (or a new scope set for that agent).
2. Register in `agents/__init__.py`.
3. Add typed wrappers in `server.py` so Claude gets a proper schema.
4. Optionally add CLI subcommands in `cli.py`.
5. Add tests with `respx` mocking the backend.

The `lab_workflow` agent is the reference template.
