From 4b5b8f30bd27796ff328deed789e6bb966775f01 Mon Sep 17 00:00:00 2001 From: William Wang Date: Sun, 5 Jul 2026 22:29:42 +0800 Subject: [PATCH] docs: sync auth model across changelog, security, and guides Update all docs that describe the initialize handshake or credentials flow to reflect the new agent-managed auth model: CHANGELOG entry, SECURITY.md wording fix (no longer 'no own auth'), an ACP Handshake section in ARCHITECTURE.md, a no-editor-key note in DEVELOPMENT.md, and a new Authentication/credentials troubleshooting entry. --- CHANGELOG.md | 14 ++++++ SECURITY.md | 20 ++++---- docs/ARCHITECTURE.md | 103 +++++++++++++++++++++++++--------------- docs/DEVELOPMENT.md | 5 ++ docs/TROUBLESHOOTING.md | 52 ++++++++++++++++---- 5 files changed, 139 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10fc58c..636440b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added + - Process watchdog (`backend/client.ts`): a detached child polls the bridge pid every 2s and SIGKILLs the zcode process group if the bridge disappears without running its signal handlers (Zed force-kill on reconnect, crash, @@ -17,8 +18,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `server.lastMode` map recording the mode value advertised to the client, used by the new mode reconciliation. - `.node-version` pinning node 22 for fnm. +- ACP Registry compatibility: `initialize` now advertises an agent-type + `authMethods` entry (`zcode-credentials`) declaring that the bridge reads + the GLM API key itself from `~/.zcode/v2/config.json` — no editor-side + credentials are required. The registry CI rejects empty `authMethods`, so + this is required for submission. Submission assets live under + `registry/zcode-acp-server/` (`agent.json` + `icon.svg`). ### Fixed + - `/mode` and `/thought` slash commands now emit `current_mode_update` and `config_option_update`. Previously they switched the backend mode but never notified the editor UI, because slash commands return `end_turn` and bypass @@ -33,10 +41,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 turn completion (`emitModeIfChanged`), since they bypass `session/setMode`. ### Changed + - `SLASH_COMMANDS` rewritten: command names dropped the `/` prefix, descriptions reworded. - `package.json` no longer declares `packageManager`; pnpm is managed via the local environment (corepack/fnm). +- Removed the empty `agentCapabilities.auth: {}` from the `initialize` + response; the auth story is now carried by the new `authMethods` entry. ## [0.1.0] - 2026-07-04 @@ -44,6 +55,7 @@ Initial release: a standalone TypeScript ACP server bridging the headless ZCode app-server to ACP-compatible editors (Zed, JetBrains). ### Added + - ZCode subprocess client with reader-loop multiplexing and process-group isolation. - Event-stream listener (`session/subscribe` + `session/event`). @@ -73,6 +85,7 @@ ZCode app-server to ACP-compatible editors (Zed, JetBrains). Development, Troubleshooting). ### Fixed + - `buildSnapshot` flattens `todoGroups` as a **list** (it was read as a single object); the plan list is no longer empty on `session/load` and `PlanUpdate` is correctly emitted at turn completion. @@ -89,6 +102,7 @@ ZCode app-server to ACP-compatible editors (Zed, JetBrains). - Stable plan signatures (sorted keys) prevent spurious `PlanUpdate`. ### Changed + - `engines.node` is `>=22.0.0` (the bridge requires `node:sqlite`). - `package.json` declares `files`, `repository`, `keywords`, `types`, `packageManager`. diff --git a/SECURITY.md b/SECURITY.md index 23b112c..08c001b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,16 +2,20 @@ ## What This Project Touches -This is a stdio ACP bridge — no network listener, no own auth. However, in -order to bridge the headless ZCode CLI to ACP-compatible editors, it does -read and write a small number of sensitive files owned by the ZCode app: - -| Path | Read/Write | What it touches | -|------|-----------|-----------------| -| `~/.zcode/v2/config.json` | read | Reads the active provider's `baseURL` and `apiKey` (in `backend/credentials.ts`) and the model list (in `config/options.ts`, `config/runtime-model.ts`). The `apiKey` is forwarded to the ZCode subprocess via an environment variable (`ANTHROPIC_API_KEY`) and is never written to logs, stdout, or any other file. | -| `~/.zcode/v2/tasks-index.sqlite` | **read/write** | Inserts/updates rows in the `tasks` table (`tasks-index.ts`) so that sessions created via ACP appear in the ZCode app's UI. Only the `tasks` table is touched, using `INSERT OR IGNORE` / bounded `UPDATE`. | +This is a stdio ACP bridge — no network listener. It does not implement its +own authentication logic or validate credentials; it advertises an agent-type +`authMethods` entry in its `initialize` response so editors know auth is +self-handled, and reads the GLM key from `~/.zcode/v2/config.json`. In order +to bridge the headless ZCode CLI to ACP-compatible editors, it does read and +write a small number of sensitive files owned by the ZCode app: + +| Path | Read/Write | What it touches | +| -------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `~/.zcode/v2/config.json` | read | Reads the active provider's `baseURL` and `apiKey` (in `backend/credentials.ts`) and the model list (in `config/options.ts`, `config/runtime-model.ts`). The `apiKey` is forwarded to the ZCode subprocess via an environment variable (`ANTHROPIC_API_KEY`) and is never written to logs, stdout, or any other file. | +| `~/.zcode/v2/tasks-index.sqlite` | **read/write** | Inserts/updates rows in the `tasks` table (`tasks-index.ts`) so that sessions created via ACP appear in the ZCode app's UI. Only the `tasks` table is touched, using `INSERT OR IGNORE` / bounded `UPDATE`. | The bridge does **not**: + - send credentials, tokens, or session data anywhere except the local ZCode subprocess; - modify the ZCode CLI binary, the app, or any file outside `tasks-index.sqlite`; - expose any network port. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 4e7d110..13e92db 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -25,6 +25,32 @@ zcode-acp-server (stdio JSON-RPC ACP) zcode app-server --stdio (line-delimited JSON) ``` +## ACP Handshake + +The `initialize` request (`server.ts`) negotiates the protocol version and +declares the agent's shape to the editor: + +- **`protocolVersion`** — pinned to `PROTOCOL_VERSION` (currently 1). +- **`agentInfo`** — name/title/version from `AGENT_INFO` in `utils.ts`. +- **`agentCapabilities`** — `loadSession`, plus `sessionCapabilities.list / +resume / fork`. The prompt capabilities (image/audio/embeddedContext) and + MCP capabilities are all off. +- **`authMethods`** — a single agent-type entry (`zcode-credentials`). The + bridge reads the GLM API key itself from `~/.zcode/v2/config.json` and + forwards it to the ZCode subprocess via `ANTHROPIC_API_KEY`; the editor + never supplies credentials. Omitting the `type` field defaults to `"agent"`, + which the ACP registry's auth-check accepts as "agent self-handles auth". + +`initialize` does **not** spawn the backend. The backend is lazily created on +the first `session/new` (via `ensureBackend()`), so the handshake succeeds +even in an environment without `~/.zcode/v2/config.json` (e.g. the registry +CI runs `initialize` with an isolated `HOME`). + +Client capabilities advertised at `initialize` are recorded on the server +(`clientCapabilities`) and drive later behaviour: `supportsElicitationForm()` +gates form-based elicitation, and `supportsTerminalOutput()` gates Zed's Bash +terminal UI. + ## Core Data Flow ### 1. Session lifecycle @@ -52,11 +78,13 @@ pollEvent() consumes → EventTranslator.translate() ### 3. Dual-path event handling #### Real-time path (EventTranslator) + - Listens to zcode `session/event` pushes - Translates each event to an ACP `session/update` in real time - Maintains `seenToolIds` to avoid duplicates #### Snapshot path (ProjectionDiffer) + - On turn completion, builds a snapshot from `session/messages` + `session/read` - Diffs two snapshots to produce new events (PlanUpdate / TextDelta / ToolCallNew, etc.) - Used for turn-completion triage and stall recovery @@ -82,6 +110,7 @@ Key: **`seenToolIds` synchronization** In `session.ts:629`, after the event path finishes processing, the state is synced to the differ: + ```typescript for (const seenId of translator.seenToolIds) { differ.markToolSeen(seenId); @@ -96,45 +125,45 @@ content-less ToolCallNew. ### `backend/` — ZCode process communication -| File | Responsibility | -|------|------| -| `client.ts` | Spawn/manage the zcode subprocess, reader-loop, request/response multiplexing, process watchdog | -| `listener.ts` | EventStreamListener (subscribe/consume the event stream) and TurnMonitor (snapshot polling) | -| `types.ts` | ZCode JSON-RPC message type definitions | +| File | Responsibility | +| ------------- | ----------------------------------------------------------------------------------------------- | +| `client.ts` | Spawn/manage the zcode subprocess, reader-loop, request/response multiplexing, process watchdog | +| `listener.ts` | EventStreamListener (subscribe/consume the event stream) and TurnMonitor (snapshot polling) | +| `types.ts` | ZCode JSON-RPC message type definitions | ### `translators/` — Event translation -| File | Responsibility | -|------|------| -| `event-translator.ts` | Translate zcode events to InternalEvent (real-time path) | -| `projection-differ.ts` | Diff two snapshots to produce InternalEvent (snapshot path) | -| `tool-helpers.ts` | Tool-related pure functions: title generation, output rendering, diff parsing, location extraction | -| `types.ts` | InternalEvent union type and plan entry builders | +| File | Responsibility | +| ---------------------- | -------------------------------------------------------------------------------------------------- | +| `event-translator.ts` | Translate zcode events to InternalEvent (real-time path) | +| `projection-differ.ts` | Diff two snapshots to produce InternalEvent (snapshot path) | +| `tool-helpers.ts` | Tool-related pure functions: title generation, output rendering, diff parsing, location extraction | +| `types.ts` | InternalEvent union type and plan entry builders | ### `handlers/` — ACP method handling -| File | Responsibility | -|------|------| -| `session.ts` | session/new/list/resume/load/prompt/set_config_option/cancel | -| `extensions.ts` | fork/rewind/rewindCascade/goal/compact/steer/cancelBackgroundTask/setModel/setMode/setThoughtLevel | -| `dispatch.ts` | dispatchEvent single exit point: InternalEvent → ACP session/update | +| File | Responsibility | +| -------------------- | ------------------------------------------------------------------------------------------------------------ | +| `session.ts` | session/new/list/resume/load/prompt/set_config_option/cancel | +| `extensions.ts` | fork/rewind/rewindCascade/goal/compact/steer/cancelBackgroundTask/setModel/setMode/setThoughtLevel | +| `dispatch.ts` | dispatchEvent single exit point: InternalEvent → ACP session/update | | `server-requests.ts` | Handle zcode interaction/* requests (tool auth, ExitPlanMode, AskUserQuestion), protocol negotiation routing | -| `io.ts` | ACP notification helpers (including `sendAvailableCommandsDeferred` deferred notification) | -| `slash.ts` | Interception of `/`-prefixed commands (/compact /goal /fork /rewind /steer /model /mode /thought) | +| `io.ts` | ACP notification helpers (including `sendAvailableCommandsDeferred` deferred notification) | +| `slash.ts` | Interception of `/`-prefixed commands (/compact /goal /fork /rewind /steer /model /mode /thought) | ### `interaction/` — Interaction bridging -| File | Responsibility | -|------|------| +| File | Responsibility | +| ------------ | ------------------------------------------------------------------------------------------------ | | `adapter.ts` | Conversion adapter from zcode interaction requests to ACP (requestPermission + elicitation form) | ### `config/` — Configuration management -| File | Responsibility | -|------|------| -| `options.ts` | configOptions / modes construction, set_config_option dispatch | -| `runtime-model.ts` | runtimeModel overlay construction and application | -| `model-cache.ts` | Model ID cache and usage initialization | +| File | Responsibility | +| ------------------ | -------------------------------------------------------------- | +| `options.ts` | configOptions / modes construction, set_config_option dispatch | +| `runtime-model.ts` | runtimeModel overlay construction and application | +| `model-cache.ts` | Model ID cache and usage initialization | ## Key State Machines @@ -222,12 +251,12 @@ mirroring the Python bridge's `_pending_post_notifs` queue + The session mode can change through four entry points, all of which must notify the editor UI: -| Trigger | Path | Notifies UI | -|------|------|:---:| -| `session/setMode` request | `extensions.ts:setMode` | yes | -| `session/set_config_option` (mode) | `session.ts:setConfigOptionHandler` → `emitConfigOptionUpdate` | yes | -| `/mode` slash command | `slash.ts` → `emitConfigOptionUpdate` | yes | -| In-turn `EnterPlanMode`/`ExitPlanMode` | reconciled at turn completion | yes | +| Trigger | Path | Notifies UI | +| -------------------------------------- | -------------------------------------------------------------- | :---------: | +| `session/setMode` request | `extensions.ts:setMode` | yes | +| `session/set_config_option` (mode) | `session.ts:setConfigOptionHandler` → `emitConfigOptionUpdate` | yes | +| `/mode` slash command | `slash.ts` → `emitConfigOptionUpdate` | yes | +| In-turn `EnterPlanMode`/`ExitPlanMode` | reconciled at turn completion | yes | The in-turn path bypasses the bridge entirely, so `prompt()` runs `emitModeIfChanged` (`session.ts`) at turn completion: it re-reads the @@ -277,12 +306,12 @@ zcode process exits, so a normal shutdown leaves no lingering watchdog. ### Why a dual path? -| Scenario | Real-time path | Snapshot path | -|------|---------|----------| -| Normal streaming | Low latency | Must wait for turn end | -| Lost events | Loses data | Recovers from snapshot | -| Deduplication | seenToolIds | seenMessageIds + markToolSeen() | -| Turn-completion triage | Not triggered | PlanUpdate / usage_update | +| Scenario | Real-time path | Snapshot path | +| ---------------------- | -------------- | ------------------------------- | +| Normal streaming | Low latency | Must wait for turn end | +| Lost events | Loses data | Recovers from snapshot | +| Deduplication | seenToolIds | seenMessageIds + markToolSeen() | +| Turn-completion triage | Not triggered | PlanUpdate / usage_update | ### Why no polling fallback? diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index ba7c108..bf00b96 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -117,6 +117,11 @@ cat ~/.zcode/v2/config.json # confirm a provider is enabled and has models ``` +The bridge reads the GLM API key from this file and forwards it to the ZCode +subprocess — no editor-side API key or environment variable is required. If +`config.json` is missing or no provider is enabled, install and log into the +ZCode desktop app so it writes a valid file. + ### Test session/subscribe ```bash diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index d01bc52..83f209f 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -5,6 +5,7 @@ ### Backend fails to start **Symptom:** + ``` [zcode-acp] backend: started zcode app-server (pid=12345) [zcode-acp] backend: reader exited (stdout closed) @@ -13,12 +14,14 @@ **Troubleshooting steps:** 1. Check the ZCode CLI version: + ```bash zcode --version ``` - Must be >= 0.14.8 2. Check whether `zcode` is on PATH: + ```bash which zcode ``` @@ -31,9 +34,30 @@ - Confirm a `provider` is enabled - Confirm `models` are defined +### Authentication / credential errors (401, provider auth failed) + +**Symptom:** turns fail with authentication errors (e.g. `401`, `provider auth failed`, `invalid api key`), or `~/.zcode/v2/config.json` is missing. + +**Why:** This bridge advertises agent-managed auth — it reads the GLM API key from `~/.zcode/v2/config.json` and forwards it to the ZCode subprocess. No editor-side API key or environment variable is involved. If the credentials file is absent, empty, or carries an expired/invalid key, every turn will fail at the first model call. + +**Troubleshooting steps:** + +1. Confirm the credentials file exists and has an enabled provider: + + ```bash + cat ~/.zcode/v2/config.json + ``` + - There must be a `provider` entry with `"enabled": true` + - Its `options.apiKey` must be present and non-empty + +2. If the file is missing or the key is stale, **install and log into the ZCode desktop app** — it writes a fresh `config.json` with a valid enabled provider. There is no manual API-key configuration in the editor. + +3. If you need to override the key/base URL without touching `config.json`, set `ZCODE_BASE_URL` and provide the key via the provider config (see `src/backend/credentials.ts` for the merge order). + ### session/subscribe fails **Symptom:** + ``` session/subscribe failed (ZCode CLI 0.14.8+ required) ``` @@ -41,12 +65,14 @@ session/subscribe failed (ZCode CLI 0.14.8+ required) **Troubleshooting steps:** 1. Confirm ZCode CLI >= 0.14.8: + ```bash zcode --version ``` 2. If the version is correct but it still fails, check whether the zcode app-server supports subscribe: + ```bash cd /path/to/project zcode app-server --stdio @@ -67,6 +93,7 @@ session/subscribe failed (ZCode CLI 0.14.8+ required) **Troubleshooting steps:** 1. Confirm the client declares `terminal_output`: + ```typescript // should be present in clientCapabilities: { "_meta": { "terminal_output": true } } @@ -87,9 +114,11 @@ session/subscribe failed (ZCode CLI 0.14.8+ required) **Troubleshooting steps:** 1. Check whether `EventStreamListener` subscribed successfully: + ``` [zcode-acp] backend: started zcode app-server (pid=12345) ``` + If this log is missing, the backend did not start. 2. Check whether `session/event` pushes arrive: @@ -106,6 +135,7 @@ session/subscribe failed (ZCode CLI 0.14.8+ required) **Troubleshooting steps:** 1. Check whether zcode sent an interaction request: + ``` [zcode-acp] -> interaction/requestPermission (zcode_id=100) ``` @@ -147,6 +177,7 @@ session/subscribe failed (ZCode CLI 0.14.8+ required) - backend reader death 2. Manually inspect zombie processes: + ```bash ps aux | grep zcode killall -9 zcode # cleanup @@ -197,6 +228,7 @@ session/subscribe failed (ZCode CLI 0.14.8+ required) 2. If the lock is still stuck on an older bridge, the zcode subprocess must be killed manually: + ```bash ps aux | grep zcode killall -9 zcode # caution: kills all zcode processes @@ -239,13 +271,13 @@ export function log(msg: string): void { ### Common log patterns -| Log | Meaning | -|------|------| -| `backend: started zcode app-server (pid=...)` | Backend started successfully | -| `backend: reader exited (...)` | Backend reader exited (backend may have crashed) | -| `session/new -> sess_xxx` | New session created successfully | -| `[event] turn.started` | Turn started | -| `[event] turn.completed (resultType=...)` | Turn completed | -| `-> interaction/... (zcode_id=...)` | Interaction request received | -| `<- replied to zcode (N request(s))` | Interaction request replied | -| `⚠ ...` | Warning / error | +| Log | Meaning | +| --------------------------------------------- | ------------------------------------------------ | +| `backend: started zcode app-server (pid=...)` | Backend started successfully | +| `backend: reader exited (...)` | Backend reader exited (backend may have crashed) | +| `session/new -> sess_xxx` | New session created successfully | +| `[event] turn.started` | Turn started | +| `[event] turn.completed (resultType=...)` | Turn completed | +| `-> interaction/... (zcode_id=...)` | Interaction request received | +| `<- replied to zcode (N request(s))` | Interaction request replied | +| `⚠ ...` | Warning / error |