From 28379aeffb6de592fe9fbba6069b708524ec9215 Mon Sep 17 00:00:00 2001 From: Alex Lavaee Date: Wed, 14 Oct 2026 21:14:26 -0700 Subject: [PATCH 1/4] Design: run Chopin's harnesses on a connected machine Assistant-model: Claude Opus 5.5 (fast) Assistant-workflow: inline (fast subagents) Co-authored-by: Alex Lavaee --- docs/harness-on-machine-design.md | 606 ++++++++++++++++++++++++++++++ 1 file changed, 606 insertions(+) create mode 100644 docs/harness-on-machine-design.md diff --git a/docs/harness-on-machine-design.md b/docs/harness-on-machine-design.md new file mode 100644 index 000000000..3d4afffac --- /dev/null +++ b/docs/harness-on-machine-design.md @@ -0,0 +1,606 @@ +# Run Chopin's harnesses on a connected machine + +Status: draft for review. Design only; nothing here is implemented. + +## Summary + +Today a document's Planner runs in one of two places. Under `HARNESS=copilot-sdk` +or `HARNESS=atomic` it runs in the Chopin server. Under `HARNESS=remote` it runs +in a separate Planner command that `chopin connect --planner-command` starts once +per document, and that command dials the agent connection itself. + +This design replaces the second path. The machine's own `chopin connect` runs +the same `HarnessV1` adapters the server runs (Atomic first, then Copilot SDK), +with the machine's model logins, checkout and toolchain. The server keeps +everything it keeps today: the document, Chopin's Planner tools, prompts, +Decisions and permission checks. The two halves talk over the existing agent +connection, upgraded to `chopin.agent.v2`, opened once per machine and carrying +every document that machine serves. + +The decisions, in one place: + +| Topic | Decision | +| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Setting | `PLANNER_ON=server\|machine`. `HARNESS` keeps naming the adapter (`atomic`, `copilot-sdk`) wherever it runs. `HARNESS=remote` goes away. | +| Where the split is | At `HarnessV1`. The server keeps `HarnessAgent`, tools and prompts; the machine runs only the adapter. | +| Wire | `chopin.agent.v2` on `/api/v1/agent`. One socket per connector. Machine-level `hello`, per-document `assign`/`claimed`/`unassign`, v1 session and turn messages. | +| Bearer | The connector's connection token, not a per-document Planner credential. `chopin_p_` credentials are retired. | +| Ownership | Unchanged rules: one holder per document, the machine owner's delegated login, grace on a dropped socket. Moving a document starts fresh sessions. | +| Extensions and skills | Come from connect settings: `--agent atomic -- -e --skill ` applies to builds and the Planner alike. | +| Adapter code in the binary | Chopin's adapter glue is compiled in. The heavy runtimes (Atomic, the Copilot CLI) load from the machine, at a version Chopin pins and checks. | +| Planner products | Become Atomic extensions or packages loaded through connect settings. Their own Planner commands are retired. | +| ACP | An optional fallback Planner adapter for agents with no SDK, in a later phase. | + +## Goals + +- Run a document's Planner on a member's connected machine with the machine's + own model login, checkout and tools, using the adapters Chopin already ships + and tests (`apps/server/src/harness/atomic`, `apps/server/src/harness/copilot-sdk`). +- One agent connection per `chopin connect`, multiplexing every document whose + Planner that machine runs. +- One way to configure a machine's agent: the extensions and skills given to + `chopin connect --agent atomic -- ...` load for builds and for the Planner. +- Keep every guarantee the remote Planner gives today: one Planner per + document, push or admin access rechecked before every session and turn, + host tools run in the server under the owner, Decisions for every question, + run cards and Stop/Resume. +- Make a Planner product an extension, not a program that speaks Chopin's wire. +- Show what the machine's Planner is doing in `chopin status`. + +## Non-goals + +- Moving builds off ACP. Builds keep `chopin connect --agent` or `-- ` + and the MCP work loop in `apps/connector/src/connection.ts`. +- Running summary and research background jobs on machines. + `PLANNER_ON=machine` keeps the `BACKGROUND_JOBS=off` requirement that + `HARNESS=remote` has (`apps/server/src/config.ts:237-240`). +- Resuming a harness session across a dropped connection, a server restart or + a move to another machine. Sessions stay disposable, as + [Session lifecycle](hosted-agent.md#session-lifecycle) already says. +- Migrating live Atomic workflow runs between machines. +- New adapters beyond Atomic, Copilot SDK and the optional ACP fallback. +- Loosening what a Planner turn may call. The Atomic allowlist + (`FULL_PLANNER_TOOLS`, `apps/server/src/harness/atomic/adapter.ts:71-84`) + applies on a machine as it does on the server (see open question 6). + +## Where things are today + +### Two Planner paths + +```mermaid +flowchart LR + subgraph Server["Chopin server"] + HA["HarnessAgent + Planner tools
harness/session.ts"] + AD["HarnessV1 adapter
atomic | copilot-sdk"] + RA["remote adapter
harness/remote/adapter.ts"] + EP["/api/v1/agent
chopin.agent.v1"] + MCP["/api/connector MCP
wait_for_work"] + HA --> AD + HA --> RA --> EP + end + subgraph Machine["Member's machine"] + CC["chopin connect"] + PC1["Planner command
(document A)"] + PC2["Planner command
(document B)"] + ACP["ACP agent
(builds)"] + CC -->|spawns per document| PC1 & PC2 + CC -->|spawns per build| ACP + end + CC <-->|MCP over HTTP| MCP + PC1 <-->|one socket per document| EP + PC2 <-->|one socket per document| EP +``` + +| Piece | What it does now | +| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Harness map | `harnesses` maps `copilot-sdk`, `atomic`, `remote` (`apps/server/src/harness/harnesses.ts:33-37`). `harnessFor` refuses `HARNESS_AUTH` under `remote` (86-90) and allows `HARNESS_EXTENSIONS` only for `atomic` (64-73). | +| Contract suite | `harnessContract` checks host tools only, built-ins off, JSON output, abort and single destroy (`apps/server/src/harness/contract.ts`). `harness-v1.ts` re-exports `@ai-sdk/harness` 1.0.146 types. | +| Planner session | `openPlannerSession` builds a full Atomic workspace only when `harness === "atomic"` (`harness/session.ts:159-161`), registers a `RemotePlanner` for `remote` (184-205), and gives remote run control with `followsUpRuns: false` (83-103). | +| Atomic adapter | Auth `auto` or `ai-gateway` (`atomic/adapter.ts:49`); `auto` copies the host's `auth.json` (274-282); refuses resume (476); full sessions load operator extensions (623). Run state and HostInput come from the `FullPlanner` registry (`atomic/full.ts:19-39`). It never touches `sandboxSession`. | +| Copilot SDK adapter | Uses each owner's GitHub App token per session (`copilot-sdk/adapter.ts:309-310`), `useLoggedInUser: false` (88), and finds the native Copilot CLI in `node_modules` or `COPILOT_CLI_PATH` (`agent/cli.ts`). | +| Remote adapter | A `HarnessV1` that forwards `doStart` to the document's attached link (`remote/adapter.ts:30-60`). Links are keyed by document: `links: Map` (`remote/registry.ts:28-53`). | +| Agent connection | `chopin.agent.v1`, version 1 (`packages/agent-link/src/protocol.ts:7-16`). `attach` names exactly one document (207-212). `AgentLink` checks the version at attach (`remote/link.ts:692-699`). | +| Machine Planner credentials | `POST /api/connector/planner` issues a 15-minute `chopin_p_` credential for one document; the machine's owner holds the document through a delegated login (`remote/machine-planners.ts`). | +| Dispatch | A protocol 4 connector polls `wait_for_work` with `planners`; the server answers with the documents its owner chose the machine for, skipping one another account holds (`experiments/routes.ts:369-404`, 1329-1343). | +| Planner commands | `servePlanners` and `dispatchedPlanners` spawn the command per document with `CHOPIN_PLANNER_TOKEN` and `CHOPIN_GITHUB_TOKEN*` in its environment, restart it on backoff and stop its process group (`apps/connector/src/planner.ts:53-160`; [env table](agent-connection.md#environment)). | +| Connect settings | `--agent atomic` runs `atomic-acp`, and words after `--` pass through to Atomic (`packages/protocol/agents.ts:82-93`). `--planner-command` and `--planner` are separate options (`apps/connector/src/main.ts:369-373`, 411-424). | +| CLI binary | `bun build --compile` for five targets (`apps/connector/scripts/build-binaries.ts`). The connector depends only on the ACP SDK, the MCP SDK and two workspace packages (`apps/connector/package.json`). | + +### What hurts + +- **Two products in one setting.** `HARNESS` picks an adapter for the server + and, as `remote`, a place for the Planner. A machine cannot run the Atomic + adapter Chopin already tests; it has to bring a program that reimplements the + wire. +- **Every Planner product reimplements the same things.** Attaching, the + credential lifecycle, reconnects, worktree handling, Decisions, run reports + and token renewal live in each Planner command. Chopin's own Atomic adapter + already does the Atomic half of that, tested. +- **A socket, a process and a credential per document.** Ten documents on one + machine are ten processes, ten connections and ten credential refreshes. +- **Two sets of agent settings.** Extensions given to `--agent atomic -- -e ...` + reach builds only. The Planner gets whatever its command loads. + +## The proposal + +### Topology + +```mermaid +flowchart LR + subgraph Server["Chopin server (PLANNER_ON=machine)"] + HA["HarnessAgent + Planner tools
harness/session.ts (unchanged)"] + RA["machine adapter
(today's remote adapter, keyed by machine + document)"] + EP["/api/v1/agent
chopin.agent.v2"] + MCP["/api/connector MCP
builds"] + HA --> RA --> EP + end + subgraph Machine["Member's machine: chopin connect"] + HH["Harness host
one per connector"] + AA["Atomic adapter
(packages/harness)"] + WT["planners/<owner>/<repo>/<doc>
worktrees"] + ACP["ACP agent (builds)"] + HH --> AA --> WT + end + HH <-->|one socket, many documents| EP + Machine <-->|MCP over HTTP| MCP +``` + +The server half barely changes. Under `PLANNER_ON=machine` the server's +`HarnessV1` is today's remote adapter: it forwards `doStart` and +`doPromptTurn` over the socket and runs host tools when asked. What changes is +how it finds the socket: by the machine holding the document, not by a link that +attached to that one document. + +The machine half is new: a **harness host** inside `chopin connect`. It opens +the agent connection, takes the documents Chopin assigns, and turns `start`, +`turn`, `abort`, `stop`, `resume` and `destroy` into calls on a real +`HarnessV1` adapter. It is the inverse of the remote adapter. + +### Why split at `HarnessV1` + +`chopin.agent.v1` is already a `HarnessV1` transport. `start` is `doStart`, +`turn` is `doPromptTurn` with `tools`, `instructions` and `responseFormat`, +`part` is `emit`, `host-tool-call`/`host-tool-result` are a host tool call and +`submitToolResult`, `abort` is the turn's `abortSignal`, `destroy` is +`doDestroy`. Atomic's extras (HostInput as `input-request`, workflow runs as +`runs`, Stop/Resume as `stop`/`resume`) are in it too. So the boundary already +exists and is tested from both sides (`remote/link.test.ts`, +`remote.contract.test.ts`). The harness host is the piece nobody has written +yet, except inside each Planner command. + +### Settings + +| Variable | `PLANNER_ON=server` (default) | `PLANNER_ON=machine` | +| ----------------------------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| `HARNESS` | Adapter the server runs (`copilot-sdk`, `atomic`) | Adapter machines must run. A connector offering another is refused with `harness-unsupported`. Default `atomic`. | +| `HARNESS_AUTH` | As today | Refused at startup: model logins live on the machine. | +| `HARNESS_EXTENSIONS` | As today (Atomic only) | Refused at startup: extensions come from each machine's connect settings. | +| `MODEL` | As today | Optional. Passed as the turn's requested model, as under `remote` today. | +| `BACKGROUND_JOBS` | As today | Must be `off`. | +| `/api/v1/agent` | 404 | Speaks `chopin.agent.v2` (and v1 during the migration, see [Rollout](#rollout-as-stacked-prs)). | +| `PLANNER_ATTACH_GRACE_MS`, `AGENT_CONNECTION_*` | Unused | As today, applied per machine connection. | + +`HARNESS=remote` is refused at startup once the last PR lands, with a message +pointing at `PLANNER_ON=machine`. + +### Connect settings + +The Planner uses the same agent settings as builds. For Atomic, the connector +reads Atomic's own resource flags out of the words after `--`: + +```sh +chopin connect . --agent atomic -- -e /opt/planner-pkg --skill ~/skills/review --model openai/gpt-5.5 +``` + +| Words after `--` | Builds (ACP) | Planner (harness host) | +| --------------------------------- | -------------------------------- | -------------------------------------------------------- | +| `-e `, `--extension ` | Passed to `atomic-acp`, as today | `additionalExtensionPaths` of every full Planner session | +| `--skill ` | Passed to `atomic-acp`, as today | `additionalSkillPaths` | +| `--model ` | Passed to `atomic-acp`, as today | Default model when the server sends none | +| Anything else | Passed to `atomic-acp`, as today | Ignored, and `chopin status` lists what was ignored | + +Atomic 0.9.31's CLI documents `--extension, -e` and `--skill `, and its +resource loader takes `additionalExtensionPaths` and `additionalSkillPaths` +(checked in a sibling checkout's `node_modules`; this repository pins 0.9.37, so +PR 4 rechecks). Paths must be absolute and exist, the rule `checkedExtensions` +applies on the server (`harnesses.ts:64-73`). `--planner-command` and `--planner` +are retired; see [Rollout](#rollout-as-stacked-prs). + +A connector runs the Planner only when its agent has a harness adapter +(`--agent atomic` now, `--agent copilot` from PR 7). A connector given +`-- ` runs builds only, unless the ACP fallback adapter lands. + +### The door set + +Read the names alone; they say what the machine and server owe each other. + +| Door | Side | Promise | +| ------------------------ | ------- | ------------------------------------------------------------------------------------ | +| `hello` / `welcome` | both | The machine says which harness it runs; the server says whether it will take it. | +| `assign` / `claimed` | both | The machine now holds this document's Planner and is ready to start sessions for it. | +| `unassign` / `release` | both | The machine no longer holds the document. No grace period. | +| `start` / `started` | both | One harness session exists for one held document. | +| `turn` ... `turn-end` | both | One prompt turn runs to exactly one end. | +| `host-tool-call` ⚠ | machine | Run a Chopin tool in the server. The only way the machine changes the document. | +| `input-request` | machine | Ask the document's members through Decisions. | +| `runs`, `stop`, `resume` | both | Report and control the session's workflow runs. | + +The ⚠ door is the only one with a durable effect in Chopin. It is today's +`host-tool-call`, checked against the turn's tool list, run under the +document's Planner owner and the [permission checks](hosted-agent.md#permission-checks). +Pushing to GitHub happens on the machine with the owner's own token, and GitHub +enforces it, as for builds. + +## Wire: `chopin.agent.v2` + +The rules of [Agent connection](agent-connection.md) still hold unless +listed here: JSON text frames of at most 2 MiB, strict objects, pings, close +codes, the Decisions mapping, stream parts, structured tool errors. v2 changes +the handshake and adds a document layer. Session and turn messages stay as +they are, so the server's `RemoteSession` and its tests carry over. + +### Endpoint and bearer + +- Same path, `/api/v1/agent`. The upgrade offers `chopin.agent.v2`; a server + that speaks both answers 426 with `"supported": ["chopin.agent.v2", "chopin.agent.v1"]` + to an upgrade offering neither. +- `Authorization: Bearer `, the token + `POST /api/connector/connections` returns (`apps/connector/src/machine.ts:317-333`). + It names the machine, the checkout's repository and the connector's + protocol, and dies with that connection or a server restart. A GitHub token + or a `chopin_p_` credential offering v2 is refused with 401. +- A connector opens the socket only after its MCP connection registers, and + closes it when that connection ends. A connector restart, or a new connection + token after a server restart, opens a new socket. + +### Handshake + +```text +client { "type": "hello", "version": 2, "harness": { "name": "atomic", "version": "0.9.37" }, + "resources": { "extensions": ["planner-pkg"], "skills": ["review"] }, + "capacity": { "sessions": 4 }, + "checkout": { "origin": "git@github.com:octo-org/score.git", "branch": "main", "head": "3f2c…" } } + +server { "type": "welcome", "version": 2, "machine": { "id": "m_…", "label": "build-box" }, + "repository": "octo-org/score", "harness": "atomic", "model": "openai/gpt-5.5" } +server { "type": "refused", "code": "harness-unsupported", "message": "This Chopin runs Planners with copilot-sdk; this machine offers atomic." } +``` + +- `resources` carries names only (the last path segment), never paths, for the + Machines page and Chat's machine menu. +- `capacity.sessions` is how many harness sessions the machine runs at once. + The server never starts more on it. +- New refusal codes: `harness-unsupported` (close 4422) and + `machine-revoked` (4403). `planner-attached` and `sign-in-required` no longer + apply at this level. + +### Documents + +The server decides which documents a machine holds. It reuses today's rule +(`plannerDocuments`, `experiments/routes.ts:377-404`): documents in the +machine's repository that its owner chose this machine for, minus any whose +Planner another account holds. It takes the ownership through +`PlannerAttachments` with the machine's delegated login before it sends +`assign`, so `assign` means "you hold it now". + +```text +server { "type": "assign", "document": { "id": "5f0c…e21", "title": "Score export", "url": "https://chopin.example/octo-org/score/plan", "repository": "octo-org/score" }, "resumed": false } +client { "type": "claimed", "document": "5f0c…e21" } +client { "type": "claim-failed", "document": "5f0c…e21", "code": "worktree-failed", "message": "git worktree add failed: …" } + +server { "type": "unassign", "document": "5f0c…e21", "code": "chosen-elsewhere", "message": "The document chose another machine." } +client { "type": "release", "document": "5f0c…e21" } +``` + +- `claimed` comes once the connector has the document's worktree and the + adapter loaded. Until then Chat's machine button says **Starting…**, as now. +- `claim-failed` releases the ownership at once. `code` is one of + `worktree-failed`, `harness-unavailable`, `at-capacity`, `failed`. +- `unassign` codes: `chosen-elsewhere`, `access-revoked`, `document-archived`, + `document-deleted`, `planner-reset`, `machine-revoked`. Each matches a row of + today's [ownership table](agent-connection.md#ownership). Every open + session of the document ends as if `destroy` had arrived. +- `release` is the machine letting go, for example on `chopin` shutdown of one + document or a local failure. No grace period. +- `resumed: true` marks an `assign` re-sent after a reconnect within the grace + period. Ownership and its generation are the same; sessions are new. + +### Sessions and turns + +Only two messages change shape: + +```text +server { "type": "start", "session": "s_…", "document": "5f0c…e21", "mode": "planner" } +client { "type": "started", "session": "s_…", "workspace": { "kind": "worktree", "branch": "chopin/planner/5f0c…e21", "head": "3f2c…" } } +client { "type": "start-failed", "session": "s_…", "code": "at-capacity", "message": "This machine already runs 4 Planner sessions." } +``` + +- `start.document` is required and must be a document the connection holds, + else the client answers `start-failed` with `failed` and the server treats it + as a protocol error. +- `started.workspace` replaces the server's own `PlannerWorkspace` + (`atomic/workspace.ts`), so `channel.instructions(...)` can tell the Planner + it is in a worktree of the repository (`harness/session.ts:206-208`). +- `start-failed.code` is optional: `at-capacity`, `harness-unavailable`, + `failed`. + +Every other v1 message is reused verbatim: `turn`, `abort`, `tools`, `part`, +`host-tool-call`, `host-tool-result`, `turn-end`, `input-request`, +`input-cancel`, `input-result`, `runs`, `stop`, `resume`, `control-result`, +`destroy`, `detach`, `error`. Their ids are unique per connection, so routing by +session is enough. + +### One turn, end to end + +```mermaid +sequenceDiagram + participant W as Member (browser) + participant S as Chopin server + participant H as Harness host (chopin connect) + participant A as Atomic adapter + W->>S: @chopin "outline the export" + S->>H: start {session, document, mode: planner} + H->>A: doStart({sessionId, cwd: worktree, extensions, skills}) + H-->>S: started {workspace} + S->>H: turn {prompt, instructions, tools} + H->>A: doPromptTurn({tools, emit, abortSignal}) + H-->>S: tools {names: [read, search, workflow, ...]} + A-->>H: emit text-delta + H-->>S: part {text-delta} + A-->>H: emit tool-call (host tool "edit_block") + H-->>S: host-tool-call {edit_block, input} + S->>S: permission check, edit the room + S->>H: host-tool-result {output} + H->>A: submitToolResult + A-->>H: emit finish + H-->>S: part {finish}, turn-end {finished} +``` + +### Version rules + +- v2 is a new subprotocol, as [Versions](agent-connection.md#versions) + requires. `hello.version` is `2`. +- `@chopin/agent-link` exports both schemas. `AGENT_VERSION` becomes a list of + versions the package speaks. +- Additive fields within v2 follow the strict-object rule: a server rejects + fields it does not know, so a new field ships in the server before any client + sends it. + +## Ownership, grace, reconnects and moves + +```mermaid +stateDiagram-v2 + [*] --> Free + Free --> Assigned: owner chooses machine M\nserver takes ownership, sends assign + Assigned --> Held: claimed + Assigned --> Free: claim-failed + Held --> Grace: M's socket drops + Grace --> Held: M reconnects within grace\nassign (resumed) + Grace --> Free: grace ends + Held --> Free: unassign or release + Held --> Assigned: owner chooses machine N\nunassign to M, assign to N +``` + +- **One holder per document.** Unchanged. The holder is the machine owner's + delegated login (`machine-planners.ts`). Another account's choice of its own + machine is skipped while this one holds the document, as today. +- **Rechecks.** Before every `start` and `turn`, and every 5 minutes, the server + checks the machine (paired, authorized), the owner's admission, push or admin + access, and the document (`endpoint.ts:239-254`). A failed check now sends + `unassign` with `access-revoked` for that document instead of closing the + socket, unless the failure is the machine itself (`machine-revoked` closes + everything). +- **Grace.** A dropped socket puts every document it held into grace for + `PLANNER_ATTACH_GRACE_MS`. The same machine reconnecting gets `assign` + with `resumed: true` for each one still in grace. Its sessions, turns and + open questions ended when the socket closed, as in v1: no turn is replayed. +- **Moving a document.** When the owner chooses machine N, the server sends + `unassign` (`chosen-elsewhere`) to M, releases without grace, takes the + ownership again for N and sends `assign`. N's first session starts empty. Its + first turn bootstraps from the bounded transcript and the current document, + which is how every harness session starts today + ([Session lifecycle](hosted-agent.md#session-lifecycle)). Nothing on M + is copied. +- **Workflow runs on a move.** Runs M reported become **stopped** cards, as a + dropped v1 connection does today. Their state stays in M's Atomic workflow + store. They do not move (open question 4). +- **Server restart.** Unchanged: every ownership is released and connection + tokens die. Connectors register again, open a new socket, and get `assign` + for every document still chosen. + +## Builds and Planners on the same machine + +| Concern | Decision | +| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Working trees | Builds keep `runs/`. Each document's Planner works in `planners///`, the path Planner commands use today. The connector now creates it (`git worktree add --detach` at the checkout's `origin` default branch) and never resets one with local changes. | +| Running at once | A build and Planner sessions run in parallel; they share no worktree. Builds stay one at a time per checkout, as now. | +| Session cap | `capacity.sessions`, default 2, set with `--planner-sessions `. A kept Atomic session with live workflow runs counts. | +| Git locks | Worktree creation retries on `index.lock`/worktree lock contention with the checkout's own Git operations. | +| Shutdown | Ctrl-C reports running builds stopped (today), then sends `release` for each held document, destroys sessions, and closes the socket with `detach`. A second Ctrl-C exits at once. | +| `Build plan` | Unchanged. It goes to the chosen machine over MCP, and usually lands next to the Planner that wrote the plan. | + +## Security + +### What the harness gets + +| Credential | Where it lives | The machine's Planner harness | +| ---------------------------------------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Model login (`~/.atomic/agent/auth.json`, `copilot login`) | The machine | Read by the adapter in memory, as `HARNESS_AUTH=auto` does on a server today. Never sent to Chopin. | +| Owner's GitHub App token for host tools | The server | Never. Host tools run in the server with the delegated login's token. | +| Owner's GitHub App token for pushing | The connector, renewed 25 minutes before expiry | `CHOPIN_GITHUB_TOKEN_FILE` and `CHOPIN_GITHUB_TOKEN_EXPIRES_AT` in the environment of commands Atomic starts. The raw `CHOPIN_GITHUB_TOKEN` variable is dropped. | +| Connection token (v2 bearer) | Connector memory | Never. | +| Machine token `chopin_m_` | Base dir file | Never given. Readable by any process of the same user, as it is for build agents today (risk 3). | +| `chopin_p_` Planner credentials | Retired | n/a | +| `--env` values | `config.json` (0600) | In the environment, as for Planner commands today. | + +### Trust boundary + +- The server trusts the machine exactly as it trusts an attached v1 client: its + parts, its own tool reports and its run reports are the machine's word. Host + tool calls are checked against the turn's tool list, and a reported run never + starts a Planner turn (`followsUpRuns: false`, `harness/session.ts:101`). +- A machine runs Planners only for documents its owner chose it for. The server + never assigns another account's document to it, so a shared build box cannot + be steered into planning someone else's document. +- Extensions and skills run as the machine user, chosen by the machine owner. + They do not reach the server; the server sees only their names. +- Push checks stay where they are. The server checks push or admin access at + assign, before every `start` and `turn`, and every 5 minutes. A push itself is + between the machine and GitHub, with the owner's token, as for builds and + Planner commands now ([Which GitHub account pushes](chopin-cli.md#which-github-account-pushes)). + +## Bundling the adapters into `chopin` + +The CLI is one `bun build --compile` executable per platform. The adapters' +own code is small TypeScript; the runtimes behind them are not. + +| Part | Size and shape | Decision | +| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Chopin's adapter glue (`packages/harness`) | Plain TypeScript | Compiled into `chopin`. | +| `@ai-sdk/harness` | Plain TypeScript | Compiled in. | +| `@bastani/atomic` | 51 MB package; more than 250 MB with dependencies, glibc and musl native modules and an embedded PostgreSQL ([Atomic caveats](self-hosting.md#choose-and-trust-a-harness)) | Not compiled in. Loaded at runtime with `import()` from a version-pinned install the connector manages in `/harness/atomic/`, installed with the machine's `npm` on first use. | +| `@github/copilot-sdk` | Plain JavaScript | Compiled in. | +| Copilot CLI (`@github/copilot--`) | Native executable per platform (`agent/cli.ts`) | Not compiled in. The adapter drives the `copilot` the machine already installs for `--agent copilot`, found on `PATH` or through `COPILOT_CLI_PATH`, and checks its version. | + +Why not embed: each of the five targets would have to carry Atomic's native +modules (glibc and musl builds on Linux) and its embedded PostgreSQL, and +everyone who only runs builds would download a CLI several hundred megabytes +larger. Why a managed install rather than the global `@bastani/atomic`: the +adapter is tested against one Atomic version (`package.json` pins 0.9.37), and a +global install drifts. A machine set up for `--agent atomic` already has +Node.js 22.19 and `npm` (`packages/protocol/agents.ts:82-93`). + +PR 6 has to prove that a compiled Bun executable can `import()` an absolute +path outside its bundle and load that package's native modules on all five +targets. If it cannot, the fallback is a small helper script run by the +machine's Node.js that hosts the adapter over stdio (risk 1). + +## Observability + +`chopin status` gains a Planner block, and `--json` the same fields under +`planner`: + +```text +Connector: running for /home/me/src/score (pid 4242, chopin 0.1.0, the service) +Planner: atomic 0.9.37 (from /home/me/.local/state/chopin/connector/harness/atomic/0.9.37) + extensions: planner-pkg · skills: review · ignored: --thinking high + connection: up since 10:42 (chopin.agent.v2), 2 of 2 sessions in use + https://chopin.example/octo-org/score/plan turn running 34s, 1 workflow run live + https://chopin.example/octo-org/score/export idle, held since 09:15 +``` + +- A document that failed to claim, or whose sessions keep failing, shows its last + error, redacted with `redacted()` (`apps/connector/src/connection.ts:59-67`). +- The connector keeps this state in its base dir, as it keeps chosen Planners + and start failures today (`chosenPlanners`, `startFailures` in `workspace.ts`), + so `chopin status` can read it from another process. +- The server's Machines page shows "Runs Planners with Atomic 0.9.37" and the + resource names from `hello`. Chat's machine button keeps its states. +- Server logs name each assign, claim, unassign and grace expiry with machine id, + document id and code. No prompts, no tool inputs. + +## What a Planner package changes + +A Planner product that ships today as a `--planner-command` program becomes an +Atomic extension or package: + +| Today, in the Planner command | After | +| ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | +| Reads `CHOPIN_URL`, `CHOPIN_DOCUMENT`, `CHOPIN_PLANNER_TOKEN`; dials `/api/v1/agent` | Nothing. The connector holds the connection. | +| Reconnect, credential refresh, exit on 401 | Nothing. | +| Creates its worktree at `CHOPIN_WORKTREE` | Nothing. The session's working directory is the document's worktree. | +| Wraps Atomic itself, maps HostInput to `input-request`, reports `runs` | Nothing. Chopin's adapter does it. | +| Its prompts and procedures | Skills, prompt templates and workflows in the package. | +| Its own tools | Extension tools, usable inside its workflows. On Planner turns only if open question 6 says so. | +| Reads `CHOPIN_GITHUB_TOKEN` | Reads the file at `CHOPIN_GITHUB_TOKEN_FILE` before each use. | +| Started with `--planner-command "pkg-planner …"` | Loaded with `chopin connect . --agent atomic -- -e /path/to/pkg`. The same package also loads into builds. | + +## Alternatives considered + +| Option | For | Against | Verdict | +| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | +| A. Keep `HARNESS=remote` and Planner commands; ship a reference command built on Chopin's adapter | No protocol change | Still a process, socket and credential per document; settings still split; every product still owns the wire | Rejected | +| B. Split at `HarnessV1`, harness host in `chopin connect`, one socket per machine (this design) | Reuses the tested adapters and the v1 session messages; one place for agent settings | A new subprotocol; the connector grows a runtime dependency on Atomic | **Chosen** | +| C. Run the whole Planner (`HarnessAgent`, tools, prompts) on the machine; server exposes tools over MCP | Server becomes thinner | Prompts, permission checks and room writes would trust the machine; Chopin's tools would need a second transport; two copies of `session.ts` logic | Rejected | +| D. Keep v1, one socket per document, opened by the connector itself | No new handshake | N sockets and N credentials per machine; per-document `attach` cannot carry machine capabilities or capacity | Rejected | +| E. Run the adapter in a child process per machine instead of in the connector process | Crash and memory isolation from builds; its own environment | A second local protocol; more moving parts on day one | Deferred (open question 2) | +| F. ACP as the only Planner transport on the machine | One agent protocol for builds and Planner | ACP has no host tool results, structured output, HostInput or run control; loses what the adapters already do | Fallback only (PR 9) | + +## Rollout as stacked PRs + +Each PR is reviewable alone and leaves `main` working. v1 keeps working until +PR 8. + +| PR | Change | Acceptance tests | +| -- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | Move the Atomic adapter, `full.ts` run control, the Copilot SDK adapter and runtime into `packages/harness`, free of server imports. `human-input.ts`, `workspace.ts` and `checkout.ts` stay in the server. | `atomic.contract.test.ts`, `copilot-sdk/*.test.ts` and `harness/contract.test.ts` pass unchanged from their new home; `bun run types`; the server imports the package. | +| 2 | `chopin.agent.v2` schemas in `@chopin/agent-link`; v2 section in `docs/agent-connection.md`. | Protocol tests for every new message and limit; v1 tests untouched; the 426 body lists both subprotocols. | +| 3 | Server: `PLANNER_ON`, connection-token admission on v2, `assign`/`claimed`/`unassign` driven by `plannerDocuments` and `PlannerAttachments`, remote adapter keyed by machine and document. | With the agent-link test client speaking v2: assign on choice; claim-failed releases; reconnect within grace keeps the generation; choosing machine N unassigns M and assigns N; push loss unassigns one document; revoking the machine closes the socket; `PLANNER_ON=machine` with `HARNESS_AUTH` refuses to start. | +| 4 | Connector: harness host, Atomic from a managed install, document worktrees, Atomic flags read from connect settings, `--planner-sessions`. | E2E against a real server with Atomic's stub model (`atomic/model-stub.ts`): an `@chopin` turn edits the document; a questionnaire round-trips through Decisions; Stop/Resume a workflow run; Ctrl-C releases every document; a build runs during a Planner turn. | +| 5 | `chopin status` Planner block and `--json`; Machines page harness line. | Status snapshot tests; the Machines page shows the harness and resource names; nothing in status output matches the token patterns `redacted()` masks. | +| 6 | Compiled-binary support: runtime `import()` of the managed Atomic, `chopin harness check`. | CI builds each binary; on every target with a CI runner it installs the pinned Atomic, runs `chopin harness check`, and runs one stub-model turn. | +| 7 | Copilot SDK on the machine: `useLoggedInUser: true`, the machine's `copilot` CLI, version check. | The shared contract suite runs against the machine-hosted Copilot adapter; a turn with `--agent copilot` in CI with a stub. | +| 8 | Retire `HARNESS=remote`, `--planner-command`, `--planner`, `POST /api/connector/planner`, `chopin_p_` credentials, the protocol 4 Planner poll, and v1 (subject to open question 1). Update `chopin-cli.md`, `self-hosting.md`, `hosted-agent.md`, `architecture.md`. | Startup refuses `HARNESS=remote` with a pointer; `chopin connect --planner-command` exits 2 with a pointer; `bun run ci`; docs link check. | +| 9 | Optional: ACP fallback Planner adapter. Host tools reach the ACP agent through the connector's local MCP bridge (`apps/connector/src/implementation.ts`), structured output through a result tool. | The shared contract suite, minus the capabilities ACP lacks, which refuse with `HarnessCapabilityUnsupportedError`. | + +PRs 1 and 2 have no user-visible change. A deployment can switch to +`PLANNER_ON=machine` after PR 4; Planner commands keep working beside it until PR 8. + +## Risks + +1. **Bun's compiled executable may not load an external package with native + modules.** PR 6 proves it on all five targets before anything depends on it. + Fallback: a Node.js helper hosting the adapter over stdio. +2. **Atomic in the connector process.** A native crash or a runaway workflow + takes builds down with it, and memory grows with sessions. The session cap + bounds it; open question 2 decides whether to isolate. +3. **Same-user access.** Extensions and Atomic workflows can read the machine + token and `config.json`, as build agents can today. Nothing in this design + makes it worse; nothing makes it better either. +4. **Version skew.** The managed install pins Atomic, but a machine's Copilot + CLI is whatever the user installed. The connector checks versions at `hello` + and refuses with a message naming the version it needs. +5. **One socket for many documents.** A 2 MiB frame for one document delays the + others, and the shared 16 MiB queue fills faster. Watch it in PR 4's E2E with + several documents; split frames or add per-document flow control only if it + shows. +6. **Users lose the separate Planner command.** Anything a product did outside + Atomic's extension model has no home. Open question 6 covers the biggest gap, + tools on Planner turns. + +## Open questions + +1. **Keep v1 for interactive clients?** v1 also lets a member's own agent + session attach with a GitHub token, with no `chopin connect`. + (A) Keep v1 GitHub-bearer attach alongside v2 and retire only Planner commands + and `chopin_p_`. (B) Retire v1 entirely in PR 8. Recommendation: A for one + release, then decide from usage. +2. **In-process or child process?** (A) Adapter in the connector process + (this draft). (B) One harness child per connector, same binary, over stdio: + crash isolation and its own environment, at the cost of a local protocol. + Recommendation: A first, keeping the harness host behind an interface so B is + a contained change. +3. **Automatic and the Planner.** Today a machine runs a Planner only when its + owner chose it for the document. (A) Keep that: no choice, no Planner, and + `@chopin` says how to choose. (B) When nobody chose a machine, assign the + requester's Automatic machine (favorite, then last used, then first idle). + Recommendation: B, since it is what Automatic means for builds. +4. **Live workflow runs when a document moves.** (A) Move anyway and show the + runs stopped (this draft). (B) Ask the owner to confirm in the machine menu + when runs are live. (C) Refuse the move until runs end. Recommendation: B. +5. **Where Atomic comes from.** (A) A managed, pinned install in the base dir + (this draft). (B) The machine's global `@bastani/atomic`, version-checked. + Recommendation: A; B makes every Atomic release a support question. +6. **Extension tools on Planner turns.** `FULL_PLANNER_TOOLS` keeps an + extension's tools inside workflows. (A) Keep the allowlist. (B) On a machine, + also offer tools from connect-configured extensions on Planner turns, + declared with `tools` and shown in Work details. Recommendation: B behind an + explicit `--planner-tools` opt-in, since the machine owner chose the code. +7. **Default session cap.** 2 per machine in this draft. Is a kept session with + live workflow runs one slot, or should runs count separately? +8. **`HARNESS` under `PLANNER_ON=machine`.** One adapter per server (this draft), + or a list so machines with Copilot and machines with Atomic can serve the same + deployment? +9. **Grace for an explicit release.** `release` and `unassign` skip the grace + period in this draft. Should a connector restart (Ctrl-C, then connect) keep + ownership the way a dropped socket does? From 7651a63ef12fd390f0ed922ae8078f68b9111245 Mon Sep 17 00:00:00 2001 From: Alex Lavaee Date: Wed, 14 Oct 2026 21:22:55 -0700 Subject: [PATCH 2/4] Design: record the decisions on running harnesses on a machine Retire chopin.agent.v1 entirely, including GitHub-token attach: every Planner off the server, a person's own interactive one included, runs through a connected machine on v2. Automatic gives the Planner to the requester's machine, moves with live workflow runs need the owner's confirmation, Atomic comes from a managed install at the version the server asks for, and a connector restart keeps ownership through grace. The open questions become a Decisions section; two new questions remain. Assistant-model: Claude Opus 5.5 (fast) Assistant-workflow: inline (fast subagents) User-preference: Retire the v1 agent connection entirely; Planners always run on a connected machine Co-authored-by: Alex Lavaee --- docs/harness-on-machine-design.md | 340 ++++++++++++++++++------------ 1 file changed, 201 insertions(+), 139 deletions(-) diff --git a/docs/harness-on-machine-design.md b/docs/harness-on-machine-design.md index 3d4afffac..781e9b538 100644 --- a/docs/harness-on-machine-design.md +++ b/docs/harness-on-machine-design.md @@ -6,16 +6,19 @@ Status: draft for review. Design only; nothing here is implemented. Today a document's Planner runs in one of two places. Under `HARNESS=copilot-sdk` or `HARNESS=atomic` it runs in the Chopin server. Under `HARNESS=remote` it runs -in a separate Planner command that `chopin connect --planner-command` starts once -per document, and that command dials the agent connection itself. - -This design replaces the second path. The machine's own `chopin connect` runs -the same `HarnessV1` adapters the server runs (Atomic first, then Copilot SDK), -with the machine's model logins, checkout and toolchain. The server keeps +outside the server, attached over the agent connection (`chopin.agent.v1`): +either a Planner command that `chopin connect --planner-command` starts once per +document, or a member's own agent session attached with their GitHub token. + +This design replaces both kinds of attach and retires v1. The machine's own +`chopin connect` runs the same `HarnessV1` adapters the server runs (Atomic +first, then Copilot SDK), with the machine's model logins, checkout and +toolchain. Every Planner that does not run in the server runs through a +connected machine, including a person's own interactive one. The server keeps everything it keeps today: the document, Chopin's Planner tools, prompts, -Decisions and permission checks. The two halves talk over the existing agent -connection, upgraded to `chopin.agent.v2`, opened once per machine and carrying -every document that machine serves. +Decisions and permission checks. The two halves talk over `chopin.agent.v2` on +the same endpoint, opened once per machine and carrying every document that +machine serves. The decisions, in one place: @@ -24,10 +27,13 @@ The decisions, in one place: | Setting | `PLANNER_ON=server\|machine`. `HARNESS` keeps naming the adapter (`atomic`, `copilot-sdk`) wherever it runs. `HARNESS=remote` goes away. | | Where the split is | At `HarnessV1`. The server keeps `HarnessAgent`, tools and prompts; the machine runs only the adapter. | | Wire | `chopin.agent.v2` on `/api/v1/agent`. One socket per connector. Machine-level `hello`, per-document `assign`/`claimed`/`unassign`, v1 session and turn messages. | +| v1 | Retired completely, GitHub-token attach included. A person's own Planner runs through their connected machine. | | Bearer | The connector's connection token, not a per-document Planner credential. `chopin_p_` credentials are retired. | -| Ownership | Unchanged rules: one holder per document, the machine owner's delegated login, grace on a dropped socket. Moving a document starts fresh sessions. | +| Ownership | As today: one holder per document, the machine owner's delegated login. A dropped socket or a connector restart keeps it through the grace period. | +| Automatic | With no machine chosen, the Planner runs on the requester's machine: their favorite, else the one last used for the document, as for builds. | +| Moves | Moving a document starts fresh sessions. With live workflow runs, the owner confirms first. | | Extensions and skills | Come from connect settings: `--agent atomic -- -e --skill ` applies to builds and the Planner alike. | -| Adapter code in the binary | Chopin's adapter glue is compiled in. The heavy runtimes (Atomic, the Copilot CLI) load from the machine, at a version Chopin pins and checks. | +| Adapter code in the binary | Chopin's adapter glue is compiled in. Atomic loads from a managed install at the version the server asks for; the Copilot CLI from the machine, version-checked. | | Planner products | Become Atomic extensions or packages loaded through connect settings. Their own Planner commands are retired. | | ACP | An optional fallback Planner adapter for agents with no SDK, in a later phase. | @@ -59,9 +65,10 @@ The decisions, in one place: [Session lifecycle](hosted-agent.md#session-lifecycle) already says. - Migrating live Atomic workflow runs between machines. - New adapters beyond Atomic, Copilot SDK and the optional ACP fallback. -- Loosening what a Planner turn may call. The Atomic allowlist +- Loosening what a Planner turn may call by default. The Atomic allowlist (`FULL_PLANNER_TOOLS`, `apps/server/src/harness/atomic/adapter.ts:71-84`) - applies on a machine as it does on the server (see open question 6). + applies on a machine as it does on the server. Only the machine owner's + explicit `--planner-tools` opt-in adds their extensions' tools (decision 6). ## Where things are today @@ -170,17 +177,18 @@ yet, except inside each Planner command. ### Settings -| Variable | `PLANNER_ON=server` (default) | `PLANNER_ON=machine` | -| ----------------------------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | -| `HARNESS` | Adapter the server runs (`copilot-sdk`, `atomic`) | Adapter machines must run. A connector offering another is refused with `harness-unsupported`. Default `atomic`. | -| `HARNESS_AUTH` | As today | Refused at startup: model logins live on the machine. | -| `HARNESS_EXTENSIONS` | As today (Atomic only) | Refused at startup: extensions come from each machine's connect settings. | -| `MODEL` | As today | Optional. Passed as the turn's requested model, as under `remote` today. | -| `BACKGROUND_JOBS` | As today | Must be `off`. | -| `/api/v1/agent` | 404 | Speaks `chopin.agent.v2` (and v1 during the migration, see [Rollout](#rollout-as-stacked-prs)). | -| `PLANNER_ATTACH_GRACE_MS`, `AGENT_CONNECTION_*` | Unused | As today, applied per machine connection. | - -`HARNESS=remote` is refused at startup once the last PR lands, with a message +| Variable | `PLANNER_ON=server` (default) | `PLANNER_ON=machine` | +| ----------------------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `HARNESS` | Adapter the server runs (`copilot-sdk`, `atomic`) | The one adapter every machine must run. A connector offering another is refused with `harness-unsupported`. Default `atomic`. | +| `HARNESS_AUTH` | As today | Refused at startup: model logins live on the machine. | +| `HARNESS_EXTENSIONS` | As today (Atomic only) | Refused at startup: extensions come from each machine's connect settings. | +| `MODEL` | As today | Optional. Passed as the turn's requested model, as under `remote` today. | +| `BACKGROUND_JOBS` | As today | Must be `off`. | +| `/api/v1/agent` | 404 | Speaks `chopin.agent.v2` only. v1 is not offered. | +| `PLANNER_ATTACH_GRACE_MS`, `AGENT_CONNECTION_*` | Unused | As today, applied per machine connection. | + +Until PR 8, `HARNESS=remote` still serves v1 on its own; a server never speaks +both. PR 8 removes it, and startup then refuses `HARNESS=remote` with a message pointing at `PLANNER_ON=machine`. ### Connect settings @@ -206,10 +214,35 @@ PR 4 rechecks). Paths must be absolute and exist, the rule `checkedExtensions` applies on the server (`harnesses.ts:64-73`). `--planner-command` and `--planner` are retired; see [Rollout](#rollout-as-stacked-prs). +Two `chopin connect` options, outside the words after `--`, apply to the +Planner only: + +- `--planner-sessions ` sets how many Planner sessions the machine runs at + once (default 2). +- `--planner-tools` also offers the tools of connect-configured extensions on + Planner turns, beyond `FULL_PLANNER_TOOLS`. They are declared in `tools` and + shown in Work details. Without it, those tools work only inside workflows. + A connector runs the Planner only when its agent has a harness adapter (`--agent atomic` now, `--agent copilot` from PR 7). A connector given `-- ` runs builds only, unless the ACP fallback adapter lands. +### A person's own Planner + +Under v1 a member could make their own agent session in a checkout the +document's Planner by attaching it with their GitHub token. That goes away with +v1. Instead they run `chopin connect` in that checkout with the agent, extensions +and skills their session used, and either choose that machine for the document +or leave it on Automatic (their favorite machine first). The Planner then runs +through their connector with their model login, worktree and tools, and acts as +them. + +They talk to it the way everyone else does: `@chopin` in Chat. A local coding +agent can still hand it an instruction through +[local agent MCP](local-agent-mcp.md)'s `invoke_planner`. That reaches the +machine holding the document, or Automatic's pick for the caller when no machine +holds it. + ### The door set Read the names alone; they say what the machine and server owe each other. @@ -241,14 +274,14 @@ they are, so the server's `RemoteSession` and its tests carry over. ### Endpoint and bearer -- Same path, `/api/v1/agent`. The upgrade offers `chopin.agent.v2`; a server - that speaks both answers 426 with `"supported": ["chopin.agent.v2", "chopin.agent.v1"]` - to an upgrade offering neither. +- Same path, `/api/v1/agent`. The upgrade must offer `chopin.agent.v2`. Any + other upgrade, a v1 client's included, gets 426 with + `"supported": ["chopin.agent.v2"]`. - `Authorization: Bearer `, the token `POST /api/connector/connections` returns (`apps/connector/src/machine.ts:317-333`). It names the machine, the checkout's repository and the connector's protocol, and dies with that connection or a server restart. A GitHub token - or a `chopin_p_` credential offering v2 is refused with 401. + or a `chopin_p_` credential is refused with 401. - A connector opens the socket only after its MCP connection registers, and closes it when that connection ends. A connector restart, or a new connection token after a server restart, opens a new socket. @@ -262,14 +295,21 @@ client { "type": "hello", "version": 2, "harness": { "name": "atomic", "version "checkout": { "origin": "git@github.com:octo-org/score.git", "branch": "main", "head": "3f2c…" } } server { "type": "welcome", "version": 2, "machine": { "id": "m_…", "label": "build-box" }, - "repository": "octo-org/score", "harness": "atomic", "model": "openai/gpt-5.5" } + "repository": "octo-org/score", "harness": { "name": "atomic", "version": "0.9.37" }, "model": "openai/gpt-5.5" } server { "type": "refused", "code": "harness-unsupported", "message": "This Chopin runs Planners with copilot-sdk; this machine offers atomic." } ``` - `resources` carries names only (the last path segment), never paths, for the Machines page and Chat's machine menu. -- `capacity.sessions` is how many harness sessions the machine runs at once. - The server never starts more on it. +- `hello.harness.version` is the version the connector has loaded, if any. + `welcome.harness.version` is the one the server asks for. When the managed + install lacks it, the connector installs it (see + [Bundling](#bundling-the-adapters-into-chopin)) before it answers any + `assign`; a failed install answers `claim-failed` with `harness-unavailable`. +- `capacity.sessions` is how many harness sessions the machine runs at once + (`--planner-sessions`, default 2). The server never starts more on it. Live + workflow runs do not use session slots; they are counted on their own and + shown in `chopin status`. - New refusal codes: `harness-unsupported` (close 4422) and `machine-revoked` (4403). `planner-attached` and `sign-in-required` no longer apply at this level. @@ -279,9 +319,16 @@ server { "type": "refused", "code": "harness-unsupported", "message": "This Cho The server decides which documents a machine holds. It reuses today's rule (`plannerDocuments`, `experiments/routes.ts:377-404`): documents in the machine's repository that its owner chose this machine for, minus any whose -Planner another account holds. It takes the ownership through -`PlannerAttachments` with the machine's delegated login before it sends -`assign`, so `assign` means "you hold it now". +Planner another account holds. One case is new. When someone asks a document's +Planner for a turn (`@chopin` or `invoke_planner`) and nobody chose a machine for +it, Automatic picks the requester's machine with the rule builds use: their +favorite, else the one last used for the document, else any free one +(`experiments/connections.ts:266-267`). Automatic never moves a Planner a +machine already holds. A requester with no machine connected for the repository +is told to run `chopin connect` in a checkout of it. + +The server takes the ownership through `PlannerAttachments` with the machine's +delegated login before it sends `assign`, so `assign` means "you hold it now". ```text server { "type": "assign", "document": { "id": "5f0c…e21", "title": "Score export", "url": "https://chopin.example/octo-org/score/plan", "repository": "octo-org/score" }, "resumed": false } @@ -300,8 +347,9 @@ client { "type": "release", "document": "5f0c…e21" } `document-deleted`, `planner-reset`, `machine-revoked`. Each matches a row of today's [ownership table](agent-connection.md#ownership). Every open session of the document ends as if `destroy` had arrived. -- `release` is the machine letting go, for example on `chopin` shutdown of one - document or a local failure. No grace period. +- `release` is the machine letting go of one document, for example after a + local failure. No grace period. A connector that exits does not release; see + [grace](#ownership-grace-reconnects-and-moves). - `resumed: true` marks an `assign` re-sent after a reconnect within the grace period. Ownership and its generation are the same; sessions are new. @@ -360,8 +408,8 @@ sequenceDiagram - v2 is a new subprotocol, as [Versions](agent-connection.md#versions) requires. `hello.version` is `2`. -- `@chopin/agent-link` exports both schemas. `AGENT_VERSION` becomes a list of - versions the package speaks. +- `@chopin/agent-link` carries the v1 and v2 schemas side by side until PR 8 + deletes v1. A server speaks one of them, chosen by its settings, never both. - Additive fields within v2 follow the strict-object rule: a server rejects fields it does not know, so a new field ships in the server before any client sends it. @@ -371,14 +419,14 @@ sequenceDiagram ```mermaid stateDiagram-v2 [*] --> Free - Free --> Assigned: owner chooses machine M\nserver takes ownership, sends assign + Free --> Assigned: owner chooses machine M, or Automatic picks\nthe requester's machine; server takes ownership, sends assign Assigned --> Held: claimed Assigned --> Free: claim-failed - Held --> Grace: M's socket drops + Held --> Grace: M's socket drops or its connector exits Grace --> Held: M reconnects within grace\nassign (resumed) Grace --> Free: grace ends Held --> Free: unassign or release - Held --> Assigned: owner chooses machine N\nunassign to M, assign to N + Held --> Assigned: owner chooses machine N and confirms\nunassign to M, assign to N ``` - **One holder per document.** Unchanged. The holder is the machine owner's @@ -390,10 +438,11 @@ stateDiagram-v2 `unassign` with `access-revoked` for that document instead of closing the socket, unless the failure is the machine itself (`machine-revoked` closes everything). -- **Grace.** A dropped socket puts every document it held into grace for - `PLANNER_ATTACH_GRACE_MS`. The same machine reconnecting gets `assign` - with `resumed: true` for each one still in grace. Its sessions, turns and - open questions ended when the socket closed, as in v1: no turn is replayed. +- **Grace.** A dropped socket or a connector that exits (Ctrl-C, a restart, a + crash) puts every document it held into grace for `PLANNER_ATTACH_GRACE_MS`. + The same machine reconnecting gets `assign` with `resumed: true` for each one + still in grace. Its sessions, turns and open questions ended when the socket + closed, as in v1: no turn is replayed. - **Moving a document.** When the owner chooses machine N, the server sends `unassign` (`chosen-elsewhere`) to M, releases without grace, takes the ownership again for N and sends `assign`. N's first session starts empty. Its @@ -401,12 +450,15 @@ stateDiagram-v2 which is how every harness session starts today ([Session lifecycle](hosted-agent.md#session-lifecycle)). Nothing on M is copied. -- **Workflow runs on a move.** Runs M reported become **stopped** cards, as a - dropped v1 connection does today. Their state stays in M's Atomic workflow - store. They do not move (open question 4). +- **Workflow runs on a move.** If M reports live workflow runs, the machine menu + first lists them, says they will stop, and asks the owner to confirm. The move + happens only after they confirm; cancelling keeps the document on M. Once + moved, the runs become **stopped** cards and their state stays in M's Atomic + workflow store. Unassigns nobody chose (`access-revoked`, `machine-revoked`, + an archived or deleted document) do not ask. - **Server restart.** Unchanged: every ownership is released and connection tokens die. Connectors register again, open a new socket, and get `assign` - for every document still chosen. + for every document still chosen. Automatic picks again on the next request. ## Builds and Planners on the same machine @@ -414,9 +466,9 @@ stateDiagram-v2 | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Working trees | Builds keep `runs/`. Each document's Planner works in `planners///`, the path Planner commands use today. The connector now creates it (`git worktree add --detach` at the checkout's `origin` default branch) and never resets one with local changes. | | Running at once | A build and Planner sessions run in parallel; they share no worktree. Builds stay one at a time per checkout, as now. | -| Session cap | `capacity.sessions`, default 2, set with `--planner-sessions `. A kept Atomic session with live workflow runs counts. | +| Session cap | `capacity.sessions`, default 2, set with `--planner-sessions `. Live workflow runs do not take a slot; the connector counts them separately and `chopin status` shows both. | | Git locks | Worktree creation retries on `index.lock`/worktree lock contention with the checkout's own Git operations. | -| Shutdown | Ctrl-C reports running builds stopped (today), then sends `release` for each held document, destroys sessions, and closes the socket with `detach`. A second Ctrl-C exits at once. | +| Shutdown | Ctrl-C reports running builds stopped (today), destroys sessions and closes the socket with `detach`. Held documents go into grace, so connecting again within it keeps them. A second Ctrl-C exits at once. | | `Build plan` | Unchanged. It goes to the chosen machine over MCP, and usually lands next to the Planner that wrote the plan. | ## Security @@ -439,9 +491,10 @@ stateDiagram-v2 parts, its own tool reports and its run reports are the machine's word. Host tool calls are checked against the turn's tool list, and a reported run never starts a Planner turn (`followsUpRuns: false`, `harness/session.ts:101`). -- A machine runs Planners only for documents its owner chose it for. The server - never assigns another account's document to it, so a shared build box cannot - be steered into planning someone else's document. +- A machine runs Planners only for documents its owner chose it for, or that + Automatic gave it because its owner asked. Nobody else's choice or request + lands on it, so a shared build box cannot be steered into planning someone + else's document. - Extensions and skills run as the machine user, chosen by the machine owner. They do not reach the server; the server sees only their names. - Push checks stay where they are. The server checks push or admin access at @@ -454,21 +507,22 @@ stateDiagram-v2 The CLI is one `bun build --compile` executable per platform. The adapters' own code is small TypeScript; the runtimes behind them are not. -| Part | Size and shape | Decision | -| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Chopin's adapter glue (`packages/harness`) | Plain TypeScript | Compiled into `chopin`. | -| `@ai-sdk/harness` | Plain TypeScript | Compiled in. | -| `@bastani/atomic` | 51 MB package; more than 250 MB with dependencies, glibc and musl native modules and an embedded PostgreSQL ([Atomic caveats](self-hosting.md#choose-and-trust-a-harness)) | Not compiled in. Loaded at runtime with `import()` from a version-pinned install the connector manages in `/harness/atomic/`, installed with the machine's `npm` on first use. | -| `@github/copilot-sdk` | Plain JavaScript | Compiled in. | -| Copilot CLI (`@github/copilot--`) | Native executable per platform (`agent/cli.ts`) | Not compiled in. The adapter drives the `copilot` the machine already installs for `--agent copilot`, found on `PATH` or through `COPILOT_CLI_PATH`, and checks its version. | +| Part | Size and shape | Decision | +| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Chopin's adapter glue (`packages/harness`) | Plain TypeScript | Compiled into `chopin`. | +| `@ai-sdk/harness` | Plain TypeScript | Compiled in. | +| `@bastani/atomic` | 51 MB package; more than 250 MB with dependencies, glibc and musl native modules and an embedded PostgreSQL ([Atomic caveats](self-hosting.md#choose-and-trust-a-harness)) | Not compiled in. Loaded at runtime with `import()` from an install the connector manages in `/harness/atomic/`, at the version the server asks for in `welcome`, installed with the machine's `npm` on first use. Never the machine's global install. | +| `@github/copilot-sdk` | Plain JavaScript | Compiled in. | +| Copilot CLI (`@github/copilot--`) | Native executable per platform (`agent/cli.ts`) | Not compiled in. The adapter drives the `copilot` the machine already installs for `--agent copilot`, found on `PATH` or through `COPILOT_CLI_PATH`, and checks its version. | Why not embed: each of the five targets would have to carry Atomic's native modules (glibc and musl builds on Linux) and its embedded PostgreSQL, and everyone who only runs builds would download a CLI several hundred megabytes -larger. Why a managed install rather than the global `@bastani/atomic`: the -adapter is tested against one Atomic version (`package.json` pins 0.9.37), and a -global install drifts. A machine set up for `--agent atomic` already has -Node.js 22.19 and `npm` (`packages/protocol/agents.ts:82-93`). +larger. Why a managed install rather than the machine's global `@bastani/atomic`: +the adapter is tested against one Atomic version (`package.json` pins 0.9.37), +the server asks for that version, and a global install drifts. A machine set up +for `--agent atomic` already has Node.js 22.19 and `npm` +(`packages/protocol/agents.ts:82-93`). PR 6 has to prove that a compiled Bun executable can `import()` an absolute path outside its bundle and load that package's native modules on all five @@ -484,7 +538,7 @@ machine's Node.js that hosts the adapter over stdio (risk 1). Connector: running for /home/me/src/score (pid 4242, chopin 0.1.0, the service) Planner: atomic 0.9.37 (from /home/me/.local/state/chopin/connector/harness/atomic/0.9.37) extensions: planner-pkg · skills: review · ignored: --thinking high - connection: up since 10:42 (chopin.agent.v2), 2 of 2 sessions in use + connection: up since 10:42 (chopin.agent.v2), 2 of 2 sessions in use, 1 workflow run live https://chopin.example/octo-org/score/plan turn running 34s, 1 workflow run live https://chopin.example/octo-org/score/export idle, held since 09:15 ``` @@ -504,47 +558,50 @@ Planner: atomic 0.9.37 (from /home/me/.local/state/chopin/connector/harness/a A Planner product that ships today as a `--planner-command` program becomes an Atomic extension or package: -| Today, in the Planner command | After | -| ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | -| Reads `CHOPIN_URL`, `CHOPIN_DOCUMENT`, `CHOPIN_PLANNER_TOKEN`; dials `/api/v1/agent` | Nothing. The connector holds the connection. | -| Reconnect, credential refresh, exit on 401 | Nothing. | -| Creates its worktree at `CHOPIN_WORKTREE` | Nothing. The session's working directory is the document's worktree. | -| Wraps Atomic itself, maps HostInput to `input-request`, reports `runs` | Nothing. Chopin's adapter does it. | -| Its prompts and procedures | Skills, prompt templates and workflows in the package. | -| Its own tools | Extension tools, usable inside its workflows. On Planner turns only if open question 6 says so. | -| Reads `CHOPIN_GITHUB_TOKEN` | Reads the file at `CHOPIN_GITHUB_TOKEN_FILE` before each use. | -| Started with `--planner-command "pkg-planner …"` | Loaded with `chopin connect . --agent atomic -- -e /path/to/pkg`. The same package also loads into builds. | +| Today, in the Planner command | After | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | +| Reads `CHOPIN_URL`, `CHOPIN_DOCUMENT`, `CHOPIN_PLANNER_TOKEN`; dials `/api/v1/agent` | Nothing. The connector holds the connection. | +| Reconnect, credential refresh, exit on 401 | Nothing. | +| Creates its worktree at `CHOPIN_WORKTREE` | Nothing. The session's working directory is the document's worktree. | +| Wraps Atomic itself, maps HostInput to `input-request`, reports `runs` | Nothing. Chopin's adapter does it. | +| Its prompts and procedures | Skills, prompt templates and workflows in the package. | +| Its own tools | Extension tools, usable inside its workflows, and on Planner turns when the machine owner passes `--planner-tools`. | +| Reads `CHOPIN_GITHUB_TOKEN` | Reads the file at `CHOPIN_GITHUB_TOKEN_FILE` before each use. | +| Started with `--planner-command "pkg-planner …"` | Loaded with `chopin connect . --agent atomic -- -e /path/to/pkg`. The same package also loads into builds. | ## Alternatives considered -| Option | For | Against | Verdict | -| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | -| A. Keep `HARNESS=remote` and Planner commands; ship a reference command built on Chopin's adapter | No protocol change | Still a process, socket and credential per document; settings still split; every product still owns the wire | Rejected | -| B. Split at `HarnessV1`, harness host in `chopin connect`, one socket per machine (this design) | Reuses the tested adapters and the v1 session messages; one place for agent settings | A new subprotocol; the connector grows a runtime dependency on Atomic | **Chosen** | -| C. Run the whole Planner (`HarnessAgent`, tools, prompts) on the machine; server exposes tools over MCP | Server becomes thinner | Prompts, permission checks and room writes would trust the machine; Chopin's tools would need a second transport; two copies of `session.ts` logic | Rejected | -| D. Keep v1, one socket per document, opened by the connector itself | No new handshake | N sockets and N credentials per machine; per-document `attach` cannot carry machine capabilities or capacity | Rejected | -| E. Run the adapter in a child process per machine instead of in the connector process | Crash and memory isolation from builds; its own environment | A second local protocol; more moving parts on day one | Deferred (open question 2) | -| F. ACP as the only Planner transport on the machine | One agent protocol for builds and Planner | ACP has no host tool results, structured output, HostInput or run control; loses what the adapters already do | Fallback only (PR 9) | +| Option | For | Against | Verdict | +| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | +| A. Keep `HARNESS=remote` and Planner commands; ship a reference command built on Chopin's adapter | No protocol change | Still a process, socket and credential per document; settings still split; every product still owns the wire | Rejected | +| B. Split at `HarnessV1`, harness host in `chopin connect`, one socket per machine (this design) | Reuses the tested adapters and the v1 session messages; one place for agent settings | A new subprotocol; the connector grows a runtime dependency on Atomic | **Chosen** | +| C. Run the whole Planner (`HarnessAgent`, tools, prompts) on the machine; server exposes tools over MCP | Server becomes thinner | Prompts, permission checks and room writes would trust the machine; Chopin's tools would need a second transport; two copies of `session.ts` logic | Rejected | +| D. Keep v1, one socket per document, opened by the connector itself | No new handshake | N sockets and N credentials per machine; per-document `attach` cannot carry machine capabilities or capacity | Rejected | +| E. Run the adapter in a child process per machine instead of in the connector process | Crash and memory isolation from builds; its own environment | A second local protocol; more moving parts on day one | Deferred (decision 2) | +| F. ACP as the only Planner transport on the machine | One agent protocol for builds and Planner | ACP has no host tool results, structured output, HostInput or run control; loses what the adapters already do | Fallback only (PR 9) | +| G. Keep v1 GitHub-token attach for a person's own session beside v2 | People keep attaching their own session without `chopin connect` | Two ways to be a Planner, two admission paths, and a Planner that depends on a live browser sign-in | Rejected (decision 1) | ## Rollout as stacked PRs -Each PR is reviewable alone and leaves `main` working. v1 keeps working until -PR 8. - -| PR | Change | Acceptance tests | -| -- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 1 | Move the Atomic adapter, `full.ts` run control, the Copilot SDK adapter and runtime into `packages/harness`, free of server imports. `human-input.ts`, `workspace.ts` and `checkout.ts` stay in the server. | `atomic.contract.test.ts`, `copilot-sdk/*.test.ts` and `harness/contract.test.ts` pass unchanged from their new home; `bun run types`; the server imports the package. | -| 2 | `chopin.agent.v2` schemas in `@chopin/agent-link`; v2 section in `docs/agent-connection.md`. | Protocol tests for every new message and limit; v1 tests untouched; the 426 body lists both subprotocols. | -| 3 | Server: `PLANNER_ON`, connection-token admission on v2, `assign`/`claimed`/`unassign` driven by `plannerDocuments` and `PlannerAttachments`, remote adapter keyed by machine and document. | With the agent-link test client speaking v2: assign on choice; claim-failed releases; reconnect within grace keeps the generation; choosing machine N unassigns M and assigns N; push loss unassigns one document; revoking the machine closes the socket; `PLANNER_ON=machine` with `HARNESS_AUTH` refuses to start. | -| 4 | Connector: harness host, Atomic from a managed install, document worktrees, Atomic flags read from connect settings, `--planner-sessions`. | E2E against a real server with Atomic's stub model (`atomic/model-stub.ts`): an `@chopin` turn edits the document; a questionnaire round-trips through Decisions; Stop/Resume a workflow run; Ctrl-C releases every document; a build runs during a Planner turn. | -| 5 | `chopin status` Planner block and `--json`; Machines page harness line. | Status snapshot tests; the Machines page shows the harness and resource names; nothing in status output matches the token patterns `redacted()` masks. | -| 6 | Compiled-binary support: runtime `import()` of the managed Atomic, `chopin harness check`. | CI builds each binary; on every target with a CI runner it installs the pinned Atomic, runs `chopin harness check`, and runs one stub-model turn. | -| 7 | Copilot SDK on the machine: `useLoggedInUser: true`, the machine's `copilot` CLI, version check. | The shared contract suite runs against the machine-hosted Copilot adapter; a turn with `--agent copilot` in CI with a stub. | -| 8 | Retire `HARNESS=remote`, `--planner-command`, `--planner`, `POST /api/connector/planner`, `chopin_p_` credentials, the protocol 4 Planner poll, and v1 (subject to open question 1). Update `chopin-cli.md`, `self-hosting.md`, `hosted-agent.md`, `architecture.md`. | Startup refuses `HARNESS=remote` with a pointer; `chopin connect --planner-command` exits 2 with a pointer; `bun run ci`; docs link check. | -| 9 | Optional: ACP fallback Planner adapter. Host tools reach the ACP agent through the connector's local MCP bridge (`apps/connector/src/implementation.ts`), structured output through a result tool. | The shared contract suite, minus the capabilities ACP lacks, which refuse with `HarnessCapabilityUnsupportedError`. | +Each PR is reviewable alone and leaves `main` working. Until PR 8 a server runs +either the old path (`HARNESS=remote` on v1) or `PLANNER_ON=machine` on v2, +never both. PR 8 removes v1 completely. + +| PR | Change | Acceptance tests | +| -- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | Move the Atomic adapter, `full.ts` run control, the Copilot SDK adapter and runtime into `packages/harness`, free of server imports. `human-input.ts`, `workspace.ts` and `checkout.ts` stay in the server. | `atomic.contract.test.ts`, `copilot-sdk/*.test.ts` and `harness/contract.test.ts` pass unchanged from their new home; `bun run types`; the server imports the package. | +| 2 | `chopin.agent.v2` schemas in `@chopin/agent-link`, beside v1 until PR 8; v2 section in `docs/agent-connection.md`. | Protocol tests for every new message and limit; v1 tests untouched. | +| 3 | Server: `PLANNER_ON`, connection-token admission on v2, `assign`/`claimed`/`unassign` driven by `plannerDocuments`, Automatic and `PlannerAttachments`, remote adapter keyed by machine and document, grace on connector exit. The machine menu asks before moving a document with live workflow runs. | With the agent-link test client speaking v2: assign on choice; with nothing chosen, `@chopin` assigns the requester's favorite, else the machine last used for the document; claim-failed releases; reconnect within grace keeps the generation; choosing machine N unassigns M and assigns N; with live runs on M, nothing moves until the owner confirms; push loss unassigns one document; revoking the machine closes the socket; a v1 upgrade gets 426 listing only v2 and a GitHub token gets 401; `PLANNER_ON=machine` with `HARNESS_AUTH` refuses to start. | +| 4 | Connector: harness host behind an interface, Atomic from a managed install at the version `welcome` asks for, document worktrees, Atomic flags read from connect settings, `--planner-sessions`, `--planner-tools`. | E2E against a real server with Atomic's stub model (`atomic/model-stub.ts`): an `@chopin` turn edits the document; a questionnaire round-trips through Decisions; Stop/Resume a workflow run; live runs leave session slots free; Ctrl-C then connecting within grace keeps every document; an extension tool reaches Planner turns only with `--planner-tools`; a build runs during a Planner turn. | +| 5 | `chopin status` Planner block and `--json`; Machines page harness line. | Status snapshot tests; the Machines page shows the harness and resource names; nothing in status output matches the token patterns `redacted()` masks. | +| 6 | Compiled-binary support: runtime `import()` of the managed Atomic, `chopin harness check`. | CI builds each binary; on every target with a CI runner it installs the pinned Atomic, runs `chopin harness check`, and runs one stub-model turn. | +| 7 | Copilot SDK on the machine: `useLoggedInUser: true`, the machine's `copilot` CLI, version check. | The shared contract suite runs against the machine-hosted Copilot adapter; a turn with `--agent copilot` in CI with a stub. | +| 8 | Remove v1 completely: its schemas and client in `@chopin/agent-link`, GitHub-token attach and `sign-in-required`, `HARNESS=remote`, `--planner-command`, `--planner`, `POST /api/connector/planner`, `chopin_p_` credentials and the protocol 4 Planner poll. `invoke_planner`'s refusal points at `chopin connect`. Rewrite `agent-connection.md` for v2; update `chopin-cli.md`, `self-hosting.md`, `hosted-agent.md`, `local-agent-mcp.md`, `architecture.md`. | Startup refuses `HARNESS=remote` with a pointer; `chopin connect --planner-command` exits 2 with a pointer; no v1 schema is left in `@chopin/agent-link`; `bun run ci`; docs link check. | +| 9 | Optional: ACP fallback Planner adapter. Host tools reach the ACP agent through the connector's local MCP bridge (`apps/connector/src/implementation.ts`), structured output through a result tool. | The shared contract suite, minus the capabilities ACP lacks, which refuse with `HarnessCapabilityUnsupportedError`. | PRs 1 and 2 have no user-visible change. A deployment can switch to -`PLANNER_ON=machine` after PR 4; Planner commands keep working beside it until PR 8. +`PLANNER_ON=machine` after PR 4. One that stays on `HARNESS=remote` keeps v1, +Planner commands and GitHub-token attach until PR 8 removes them. ## Risks @@ -553,54 +610,59 @@ PRs 1 and 2 have no user-visible change. A deployment can switch to Fallback: a Node.js helper hosting the adapter over stdio. 2. **Atomic in the connector process.** A native crash or a runaway workflow takes builds down with it, and memory grows with sessions. The session cap - bounds it; open question 2 decides whether to isolate. + bounds sessions, but live workflow runs sit outside it. The harness host is + behind an interface, so moving it into a child process (alternative E) is a + contained change if PR 4's E2E shows crashes or memory growth. 3. **Same-user access.** Extensions and Atomic workflows can read the machine token and `config.json`, as build agents can today. Nothing in this design makes it worse; nothing makes it better either. -4. **Version skew.** The managed install pins Atomic, but a machine's Copilot - CLI is whatever the user installed. The connector checks versions at `hello` - and refuses with a message naming the version it needs. +4. **Version skew.** The server asks for one Atomic version, and the + connector's compiled adapter glue was built against one too. A connector + from another release may not handle the version asked for; `hello`, + `welcome` and `chopin harness check` catch it (open question 2). A machine's + Copilot CLI is whatever the user installed; the connector checks its version + at `hello` and refuses with a message naming the version it needs. 5. **One socket for many documents.** A 2 MiB frame for one document delays the others, and the shared 16 MiB queue fills faster. Watch it in PR 4's E2E with several documents; split frames or add per-document flow control only if it shows. 6. **Users lose the separate Planner command.** Anything a product did outside - Atomic's extension model has no home. Open question 6 covers the biggest gap, - tools on Planner turns. + Atomic's extension model has no home. `--planner-tools` (decision 6) covers + the biggest gap, tools on Planner turns. +7. **People who planned from their own session must run `chopin connect`.** + That means pairing a machine and using an agent with a harness adapter: + Atomic from PR 4, Copilot from PR 7. PR 8 comes after both. + +## Decisions + +1. **Retire v1 entirely.** Every Planner off the server, a person's own + interactive one included, runs through a connected machine on v2. One way to + be a Planner, one admission path. +2. **Adapter in the connector process first**, behind an interface. Fewer moving + parts now; a child process stays a contained change. +3. **Automatic picks the requester's machine** when nobody chose one: their + favorite, else the one last used for the document. It is what Automatic + already means for builds. +4. **Moving a document with live workflow runs asks the owner first**, showing + what is running and that it will stop. Runs cannot move, so stopping them + should be a choice. +5. **Atomic comes from a managed install** in the connector's base directory, at + the version the server asks for. A global install drifts from the version + the adapter is tested against. +6. **Extension tools reach Planner turns only with `--planner-tools`.** The + machine owner chose that code, so they opt in to it. +7. **Default cap of 2 Planner sessions per machine; live workflow runs counted + separately.** A run should not block a new conversation. +8. **One `HARNESS` adapter per server under `PLANNER_ON=machine`.** One adapter + to test and support per deployment. +9. **A connector restart keeps ownership through the grace period**, like a + dropped socket. A quick restart should not hand the document to someone else. ## Open questions -1. **Keep v1 for interactive clients?** v1 also lets a member's own agent - session attach with a GitHub token, with no `chopin connect`. - (A) Keep v1 GitHub-bearer attach alongside v2 and retire only Planner commands - and `chopin_p_`. (B) Retire v1 entirely in PR 8. Recommendation: A for one - release, then decide from usage. -2. **In-process or child process?** (A) Adapter in the connector process - (this draft). (B) One harness child per connector, same binary, over stdio: - crash isolation and its own environment, at the cost of a local protocol. - Recommendation: A first, keeping the harness host behind an interface so B is - a contained change. -3. **Automatic and the Planner.** Today a machine runs a Planner only when its - owner chose it for the document. (A) Keep that: no choice, no Planner, and - `@chopin` says how to choose. (B) When nobody chose a machine, assign the - requester's Automatic machine (favorite, then last used, then first idle). - Recommendation: B, since it is what Automatic means for builds. -4. **Live workflow runs when a document moves.** (A) Move anyway and show the - runs stopped (this draft). (B) Ask the owner to confirm in the machine menu - when runs are live. (C) Refuse the move until runs end. Recommendation: B. -5. **Where Atomic comes from.** (A) A managed, pinned install in the base dir - (this draft). (B) The machine's global `@bastani/atomic`, version-checked. - Recommendation: A; B makes every Atomic release a support question. -6. **Extension tools on Planner turns.** `FULL_PLANNER_TOOLS` keeps an - extension's tools inside workflows. (A) Keep the allowlist. (B) On a machine, - also offer tools from connect-configured extensions on Planner turns, - declared with `tools` and shown in Work details. Recommendation: B behind an - explicit `--planner-tools` opt-in, since the machine owner chose the code. -7. **Default session cap.** 2 per machine in this draft. Is a kept session with - live workflow runs one slot, or should runs count separately? -8. **`HARNESS` under `PLANNER_ON=machine`.** One adapter per server (this draft), - or a list so machines with Copilot and machines with Atomic can serve the same - deployment? -9. **Grace for an explicit release.** `release` and `unassign` skip the grace - period in this draft. Should a connector restart (Ctrl-C, then connect) keep - ownership the way a dropped socket does? +1. **A cap for live workflow runs?** They no longer use session slots, so + nothing bounds how many one machine runs at once. +2. **Connector and server from different releases.** When the server asks for + an Atomic version the connector's adapter glue does not support, should the + connector refuse with `harness-unsupported` naming the `chopin` release to + install, or should the glue support a range of Atomic versions? From c8f5b1c214b5c66420073c1d31700a7510d6d349 Mon Sep 17 00:00:00 2001 From: Alex Lavaee Date: Wed, 14 Oct 2026 21:25:13 -0700 Subject: [PATCH 3/4] Design: decide the workflow-run cap and Atomic version range Live workflow runs have no per-machine limit, and each chopin release declares the Atomic versions its adapter supports, refusing others with harness-unsupported and naming the chopin release to install. Assistant-model: Claude Opus 5.5 (fast) Assistant-workflow: inline Assistant-verification: bun run ci passed Co-authored-by: Alex Lavaee --- docs/harness-on-machine-design.md | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/docs/harness-on-machine-design.md b/docs/harness-on-machine-design.md index 781e9b538..ebb147375 100644 --- a/docs/harness-on-machine-design.md +++ b/docs/harness-on-machine-design.md @@ -619,7 +619,7 @@ Planner commands and GitHub-token attach until PR 8 removes them. 4. **Version skew.** The server asks for one Atomic version, and the connector's compiled adapter glue was built against one too. A connector from another release may not handle the version asked for; `hello`, - `welcome` and `chopin harness check` catch it (open question 2). A machine's + `welcome` and `chopin harness check` catch it (decision 11). A machine's Copilot CLI is whatever the user installed; the connector checks its version at `hello` and refuses with a message naming the version it needs. 5. **One socket for many documents.** A 2 MiB frame for one document delays the @@ -651,18 +651,15 @@ Planner commands and GitHub-token attach until PR 8 removes them. the adapter is tested against. 6. **Extension tools reach Planner turns only with `--planner-tools`.** The machine owner chose that code, so they opt in to it. -7. **Default cap of 2 Planner sessions per machine; live workflow runs counted - separately.** A run should not block a new conversation. +7. **Default cap of 2 Planner sessions per machine.** Live workflow runs don't + take a slot, so a run never blocks a new conversation. 8. **One `HARNESS` adapter per server under `PLANNER_ON=machine`.** One adapter to test and support per deployment. 9. **A connector restart keeps ownership through the grace period**, like a dropped socket. A quick restart should not hand the document to someone else. - -## Open questions - -1. **A cap for live workflow runs?** They no longer use session slots, so - nothing bounds how many one machine runs at once. -2. **Connector and server from different releases.** When the server asks for - an Atomic version the connector's adapter glue does not support, should the - connector refuse with `harness-unsupported` naming the `chopin` release to - install, or should the glue support a range of Atomic versions? +10. **No limit on live workflow runs per machine.** The machine's owner decides + how much work it takes on. +11. **Each `chopin` release declares the Atomic versions its adapter supports.** + When the server asks for one outside that range, the connector refuses with + `harness-unsupported`, and `chopin status` and the log name the `chopin` + release to install (`chopin update`). From be69323690766c3c589cdd54d0812b0f6979d318 Mon Sep 17 00:00:00 2001 From: Alex Lavaee Date: Fri, 16 Oct 2026 11:56:01 -0700 Subject: [PATCH 4/4] Design: update running harnesses on a machine for today's Chopin Rebased on main and rewritten for how Chopin works now: one connection per machine for every repository its owner can push to, Copilot CLI and Atomic as the built-in agents, checkouts or clones under the connector's base dir, Default machine running a Planner on its creator's machine, the machine's own GitHub token, and Decisions that never expire. The retired pieces it replaces are named once, in a "What this replaces" table. The doc leads with a plain summary, keeps all 11 decisions and lists the rollout as numbered PRs with one acceptance test each. Assistant-model: Claude Opus 5.5 (fast) Assistant-workflow: inline (fast subagents) Assistant-verification: claims checked against origin/main and bun run ci passed User-preference: Docs in PRs drop legacy commands and stay human-readable Co-authored-by: Alex Lavaee --- docs/harness-on-machine-design.md | 936 +++++++++++------------------- 1 file changed, 335 insertions(+), 601 deletions(-) diff --git a/docs/harness-on-machine-design.md b/docs/harness-on-machine-design.md index ebb147375..a0ec06136 100644 --- a/docs/harness-on-machine-design.md +++ b/docs/harness-on-machine-design.md @@ -1,665 +1,399 @@ # Run Chopin's harnesses on a connected machine -Status: draft for review. Design only; nothing here is implemented. +Status: draft for review. Design only; nothing here is built yet. ## Summary -Today a document's Planner runs in one of two places. Under `HARNESS=copilot-sdk` -or `HARNESS=atomic` it runs in the Chopin server. Under `HARNESS=remote` it runs -outside the server, attached over the agent connection (`chopin.agent.v1`): -either a Planner command that `chopin connect --planner-command` starts once per -document, or a member's own agent session attached with their GitHub token. - -This design replaces both kinds of attach and retires v1. The machine's own -`chopin connect` runs the same `HarnessV1` adapters the server runs (Atomic -first, then Copilot SDK), with the machine's model logins, checkout and -toolchain. Every Planner that does not run in the server runs through a -connected machine, including a person's own interactive one. The server keeps -everything it keeps today: the document, Chopin's Planner tools, prompts, -Decisions and permission checks. The two halves talk over `chopin.agent.v2` on -the same endpoint, opened once per machine and carrying every document that -machine serves. - -The decisions, in one place: - -| Topic | Decision | -| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Setting | `PLANNER_ON=server\|machine`. `HARNESS` keeps naming the adapter (`atomic`, `copilot-sdk`) wherever it runs. `HARNESS=remote` goes away. | -| Where the split is | At `HarnessV1`. The server keeps `HarnessAgent`, tools and prompts; the machine runs only the adapter. | -| Wire | `chopin.agent.v2` on `/api/v1/agent`. One socket per connector. Machine-level `hello`, per-document `assign`/`claimed`/`unassign`, v1 session and turn messages. | -| v1 | Retired completely, GitHub-token attach included. A person's own Planner runs through their connected machine. | -| Bearer | The connector's connection token, not a per-document Planner credential. `chopin_p_` credentials are retired. | -| Ownership | As today: one holder per document, the machine owner's delegated login. A dropped socket or a connector restart keeps it through the grace period. | -| Automatic | With no machine chosen, the Planner runs on the requester's machine: their favorite, else the one last used for the document, as for builds. | -| Moves | Moving a document starts fresh sessions. With live workflow runs, the owner confirms first. | -| Extensions and skills | Come from connect settings: `--agent atomic -- -e --skill ` applies to builds and the Planner alike. | -| Adapter code in the binary | Chopin's adapter glue is compiled in. Atomic loads from a managed install at the version the server asks for; the Copilot CLI from the machine, version-checked. | -| Planner products | Become Atomic extensions or packages loaded through connect settings. Their own Planner commands are retired. | -| ACP | An optional fallback Planner adapter for agents with no SDK, in a later phase. | - -## Goals - -- Run a document's Planner on a member's connected machine with the machine's - own model login, checkout and tools, using the adapters Chopin already ships - and tests (`apps/server/src/harness/atomic`, `apps/server/src/harness/copilot-sdk`). -- One agent connection per `chopin connect`, multiplexing every document whose - Planner that machine runs. -- One way to configure a machine's agent: the extensions and skills given to - `chopin connect --agent atomic -- ...` load for builds and for the Planner. -- Keep every guarantee the remote Planner gives today: one Planner per - document, push or admin access rechecked before every session and turn, - host tools run in the server under the owner, Decisions for every question, - run cards and Stop/Resume. -- Make a Planner product an extension, not a program that speaks Chopin's wire. -- Show what the machine's Planner is doing in `chopin status`. - -## Non-goals - -- Moving builds off ACP. Builds keep `chopin connect --agent` or `-- ` - and the MCP work loop in `apps/connector/src/connection.ts`. -- Running summary and research background jobs on machines. - `PLANNER_ON=machine` keeps the `BACKGROUND_JOBS=off` requirement that - `HARNESS=remote` has (`apps/server/src/config.ts:237-240`). -- Resuming a harness session across a dropped connection, a server restart or - a move to another machine. Sessions stay disposable, as - [Session lifecycle](hosted-agent.md#session-lifecycle) already says. -- Migrating live Atomic workflow runs between machines. -- New adapters beyond Atomic, Copilot SDK and the optional ACP fallback. -- Loosening what a Planner turn may call by default. The Atomic allowlist - (`FULL_PLANNER_TOOLS`, `apps/server/src/harness/atomic/adapter.ts:71-84`) - applies on a machine as it does on the server. Only the machine owner's - explicit `--planner-tools` opt-in adds their extensions' tools (decision 6). - -## Where things are today - -### Two Planner paths +- A document's Planner can run on your own machine, with your model login, your repositories and your tools. +- You set it up with the `chopin connect` you already run for builds. There is no separate Planner program. +- The extensions and skills you give your agent load for builds and for the Planner alike. +- One connection per machine carries every document whose Planner that machine runs, in any of your repositories. +- Chat, Decisions, run cards and Stop/Resume work exactly as when the Planner runs on the server. -```mermaid -flowchart LR - subgraph Server["Chopin server"] - HA["HarnessAgent + Planner tools
harness/session.ts"] - AD["HarnessV1 adapter
atomic | copilot-sdk"] - RA["remote adapter
harness/remote/adapter.ts"] - EP["/api/v1/agent
chopin.agent.v1"] - MCP["/api/connector MCP
wait_for_work"] - HA --> AD - HA --> RA --> EP - end - subgraph Machine["Member's machine"] - CC["chopin connect"] - PC1["Planner command
(document A)"] - PC2["Planner command
(document B)"] - ACP["ACP agent
(builds)"] - CC -->|spawns per document| PC1 & PC2 - CC -->|spawns per build| ACP - end - CC <-->|MCP over HTTP| MCP - PC1 <-->|one socket per document| EP - PC2 <-->|one socket per document| EP -``` +## What this replaces + +| Today | With this design | +| --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | +| `HARNESS=remote` | `PLANNER_ON=machine`; `HARNESS` names the adapter the machine runs | +| `chopin connect --planner-command ` and `--planner ` | `chopin connect --agent atomic` or `--agent copilot` | +| A backend command per document, attaching with a 15-minute `chopin_p_` credential from `POST /api/connector/planner` | One agent connection per machine, authorized by the connector's connection token | +| `chopin.agent.v1`: one socket per document, opened with `attach` | `chopin.agent.v2`: one socket per machine; documents arrive with `assign` | +| A person's own agent session attaching with their GitHub token | That person runs `chopin connect`, and the Planner runs through their machine | +| The Planner list in the connector's `wait_for_work` answers | `assign` and `unassign` on the agent connection | +| `CHOPIN_PLANNER_TOKEN`, `CHOPIN_DOCUMENT`, `CHOPIN_WORKTREE` and the `CHOPIN_GITHUB_TOKEN` variables a backend command gets | Nothing to read: the connector holds the connection, worktree and token | + +## Scope + +In scope: + +- Run a document's Planner on a member's machine with the adapters Chopin already ships and tests. +- Open one agent connection per `chopin connect`, for every document that machine serves. +- Keep every guarantee a Planner has today, and show the machine's Planner in `chopin status`. -| Piece | What it does now | -| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Harness map | `harnesses` maps `copilot-sdk`, `atomic`, `remote` (`apps/server/src/harness/harnesses.ts:33-37`). `harnessFor` refuses `HARNESS_AUTH` under `remote` (86-90) and allows `HARNESS_EXTENSIONS` only for `atomic` (64-73). | -| Contract suite | `harnessContract` checks host tools only, built-ins off, JSON output, abort and single destroy (`apps/server/src/harness/contract.ts`). `harness-v1.ts` re-exports `@ai-sdk/harness` 1.0.146 types. | -| Planner session | `openPlannerSession` builds a full Atomic workspace only when `harness === "atomic"` (`harness/session.ts:159-161`), registers a `RemotePlanner` for `remote` (184-205), and gives remote run control with `followsUpRuns: false` (83-103). | -| Atomic adapter | Auth `auto` or `ai-gateway` (`atomic/adapter.ts:49`); `auto` copies the host's `auth.json` (274-282); refuses resume (476); full sessions load operator extensions (623). Run state and HostInput come from the `FullPlanner` registry (`atomic/full.ts:19-39`). It never touches `sandboxSession`. | -| Copilot SDK adapter | Uses each owner's GitHub App token per session (`copilot-sdk/adapter.ts:309-310`), `useLoggedInUser: false` (88), and finds the native Copilot CLI in `node_modules` or `COPILOT_CLI_PATH` (`agent/cli.ts`). | -| Remote adapter | A `HarnessV1` that forwards `doStart` to the document's attached link (`remote/adapter.ts:30-60`). Links are keyed by document: `links: Map` (`remote/registry.ts:28-53`). | -| Agent connection | `chopin.agent.v1`, version 1 (`packages/agent-link/src/protocol.ts:7-16`). `attach` names exactly one document (207-212). `AgentLink` checks the version at attach (`remote/link.ts:692-699`). | -| Machine Planner credentials | `POST /api/connector/planner` issues a 15-minute `chopin_p_` credential for one document; the machine's owner holds the document through a delegated login (`remote/machine-planners.ts`). | -| Dispatch | A protocol 4 connector polls `wait_for_work` with `planners`; the server answers with the documents its owner chose the machine for, skipping one another account holds (`experiments/routes.ts:369-404`, 1329-1343). | -| Planner commands | `servePlanners` and `dispatchedPlanners` spawn the command per document with `CHOPIN_PLANNER_TOKEN` and `CHOPIN_GITHUB_TOKEN*` in its environment, restart it on backoff and stop its process group (`apps/connector/src/planner.ts:53-160`; [env table](agent-connection.md#environment)). | -| Connect settings | `--agent atomic` runs `atomic-acp`, and words after `--` pass through to Atomic (`packages/protocol/agents.ts:82-93`). `--planner-command` and `--planner` are separate options (`apps/connector/src/main.ts:369-373`, 411-424). | -| CLI binary | `bun build --compile` for five targets (`apps/connector/scripts/build-binaries.ts`). The connector depends only on the ACP SDK, the MCP SDK and two workspace packages (`apps/connector/package.json`). | - -### What hurts - -- **Two products in one setting.** `HARNESS` picks an adapter for the server - and, as `remote`, a place for the Planner. A machine cannot run the Atomic - adapter Chopin already tests; it has to bring a program that reimplements the - wire. -- **Every Planner product reimplements the same things.** Attaching, the - credential lifecycle, reconnects, worktree handling, Decisions, run reports - and token renewal live in each Planner command. Chopin's own Atomic adapter - already does the Atomic half of that, tested. -- **A socket, a process and a credential per document.** Ten documents on one - machine are ten processes, ten connections and ten credential refreshes. -- **Two sets of agent settings.** Extensions given to `--agent atomic -- -e ...` - reach builds only. The Planner gets whatever its command loads. - -## The proposal - -### Topology +Out of scope: + +- Builds stay on ACP and the connector's MCP work loop. +- Summary and research jobs stay off: `PLANNER_ON=machine` requires `BACKGROUND_JOBS=off`. +- Sessions stay disposable: none survives a dropped connection, a server restart or a move. +- Live Atomic workflow runs never move between machines. +- No adapters beyond Atomic, Copilot SDK and an optional ACP fallback. + +## Architecture ```mermaid flowchart LR - subgraph Server["Chopin server (PLANNER_ON=machine)"] - HA["HarnessAgent + Planner tools
harness/session.ts (unchanged)"] - RA["machine adapter
(today's remote adapter, keyed by machine + document)"] + subgraph Server["Chopin server, PLANNER_ON=machine"] + HA["HarnessAgent, prompts, Chopin's tools
harness/session.ts"] + MA["Machine adapter
forwards HarnessV1 calls"] EP["/api/v1/agent
chopin.agent.v2"] - MCP["/api/connector MCP
builds"] - HA --> RA --> EP + MCP["/api/connector MCP"] + HA --> MA --> EP end subgraph Machine["Member's machine: chopin connect"] - HH["Harness host
one per connector"] - AA["Atomic adapter
(packages/harness)"] - WT["planners/<owner>/<repo>/<doc>
worktrees"] - ACP["ACP agent (builds)"] - HH --> AA --> WT + HH["Harness host"] + AD["Atomic or Copilot SDK adapter"] + PW["planners/<owner>/<repo>/<document>
one worktree per document"] + BL["Build loop and ACP agent
runs/<run id>"] + HH --> AD --> PW end HH <-->|one socket, many documents| EP - Machine <-->|MCP over HTTP| MCP + BL <-->|builds over HTTP| MCP ``` -The server half barely changes. Under `PLANNER_ON=machine` the server's -`HarnessV1` is today's remote adapter: it forwards `doStart` and -`doPromptTurn` over the socket and runs host tools when asked. What changes is -how it finds the socket: by the machine holding the document, not by a link that -attached to that one document. - -The machine half is new: a **harness host** inside `chopin connect`. It opens -the agent connection, takes the documents Chopin assigns, and turns `start`, -`turn`, `abort`, `stop`, `resume` and `destroy` into calls on a real -`HarnessV1` adapter. It is the inverse of the remote adapter. - -### Why split at `HarnessV1` - -`chopin.agent.v1` is already a `HarnessV1` transport. `start` is `doStart`, -`turn` is `doPromptTurn` with `tools`, `instructions` and `responseFormat`, -`part` is `emit`, `host-tool-call`/`host-tool-result` are a host tool call and -`submitToolResult`, `abort` is the turn's `abortSignal`, `destroy` is -`doDestroy`. Atomic's extras (HostInput as `input-request`, workflow runs as -`runs`, Stop/Resume as `stop`/`resume`) are in it too. So the boundary already -exists and is tested from both sides (`remote/link.test.ts`, -`remote.contract.test.ts`). The harness host is the piece nobody has written -yet, except inside each Planner command. - -### Settings - -| Variable | `PLANNER_ON=server` (default) | `PLANNER_ON=machine` | -| ----------------------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -| `HARNESS` | Adapter the server runs (`copilot-sdk`, `atomic`) | The one adapter every machine must run. A connector offering another is refused with `harness-unsupported`. Default `atomic`. | -| `HARNESS_AUTH` | As today | Refused at startup: model logins live on the machine. | -| `HARNESS_EXTENSIONS` | As today (Atomic only) | Refused at startup: extensions come from each machine's connect settings. | -| `MODEL` | As today | Optional. Passed as the turn's requested model, as under `remote` today. | -| `BACKGROUND_JOBS` | As today | Must be `off`. | -| `/api/v1/agent` | 404 | Speaks `chopin.agent.v2` only. v1 is not offered. | -| `PLANNER_ATTACH_GRACE_MS`, `AGENT_CONNECTION_*` | Unused | As today, applied per machine connection. | - -Until PR 8, `HARNESS=remote` still serves v1 on its own; a server never speaks -both. PR 8 removes it, and startup then refuses `HARNESS=remote` with a message -pointing at `PLANNER_ON=machine`. - -### Connect settings - -The Planner uses the same agent settings as builds. For Atomic, the connector -reads Atomic's own resource flags out of the words after `--`: +The server keeps the document, Chopin's tools, prompts, Decisions and permission checks. Its +forwarding adapter already exists; it now finds the socket by machine and document. + +The new harness host inside `chopin connect` turns each message into a call on the same `HarnessV1` +adapter the server would run. The agent connection already carries those calls, with tests on both +sides, so the harness host is the only new piece. + +## Server settings + +| Variable | `PLANNER_ON=server` (default) | `PLANNER_ON=machine` | +| ----------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| `HARNESS` | Adapter the server runs | Adapter every machine must run, `atomic` (default) or `copilot-sdk`. A machine offering another is refused (decision 8). | +| `HARNESS_AUTH` | As today | Refused at startup: model logins live on the machines | +| `HARNESS_EXTENSIONS` | As today, Atomic only | Refused at startup: extensions come from each machine's connect settings | +| `MODEL` | As today | Optional; sent as each turn's requested model | +| `BACKGROUND_JOBS` | As today | Must be `off` | +| `/api/v1/agent` | 404 | Speaks `chopin.agent.v2` only | +| `PLANNER_ATTACH_GRACE_MS`, `AGENT_CONNECTION_*` | Unused | As today, applied per machine connection | + +## Machine settings + +The Planner uses the agent settings builds use. For Atomic, the connector reads Atomic's own +resource flags from the words after `--`: ```sh -chopin connect . --agent atomic -- -e /opt/planner-pkg --skill ~/skills/review --model openai/gpt-5.5 +chopin connect --agent atomic -- -e /opt/planner-pkg --skill /home/me/skills/review --model openai/gpt-5.5 ``` -| Words after `--` | Builds (ACP) | Planner (harness host) | -| --------------------------------- | -------------------------------- | -------------------------------------------------------- | -| `-e `, `--extension ` | Passed to `atomic-acp`, as today | `additionalExtensionPaths` of every full Planner session | -| `--skill ` | Passed to `atomic-acp`, as today | `additionalSkillPaths` | -| `--model ` | Passed to `atomic-acp`, as today | Default model when the server sends none | -| Anything else | Passed to `atomic-acp`, as today | Ignored, and `chopin status` lists what was ignored | - -Atomic 0.9.31's CLI documents `--extension, -e` and `--skill `, and its -resource loader takes `additionalExtensionPaths` and `additionalSkillPaths` -(checked in a sibling checkout's `node_modules`; this repository pins 0.9.37, so -PR 4 rechecks). Paths must be absolute and exist, the rule `checkedExtensions` -applies on the server (`harnesses.ts:64-73`). `--planner-command` and `--planner` -are retired; see [Rollout](#rollout-as-stacked-prs). - -Two `chopin connect` options, outside the words after `--`, apply to the -Planner only: - -- `--planner-sessions ` sets how many Planner sessions the machine runs at - once (default 2). -- `--planner-tools` also offers the tools of connect-configured extensions on - Planner turns, beyond `FULL_PLANNER_TOOLS`. They are declared in `tools` and - shown in Work details. Without it, those tools work only inside workflows. - -A connector runs the Planner only when its agent has a harness adapter -(`--agent atomic` now, `--agent copilot` from PR 7). A connector given -`-- ` runs builds only, unless the ACP fallback adapter lands. - -### A person's own Planner - -Under v1 a member could make their own agent session in a checkout the -document's Planner by attaching it with their GitHub token. That goes away with -v1. Instead they run `chopin connect` in that checkout with the agent, extensions -and skills their session used, and either choose that machine for the document -or leave it on Automatic (their favorite machine first). The Planner then runs -through their connector with their model login, worktree and tools, and acts as -them. - -They talk to it the way everyone else does: `@chopin` in Chat. A local coding -agent can still hand it an instruction through -[local agent MCP](local-agent-mcp.md)'s `invoke_planner`. That reaches the -machine holding the document, or Automatic's pick for the caller when no machine -holds it. - -### The door set - -Read the names alone; they say what the machine and server owe each other. - -| Door | Side | Promise | -| ------------------------ | ------- | ------------------------------------------------------------------------------------ | -| `hello` / `welcome` | both | The machine says which harness it runs; the server says whether it will take it. | -| `assign` / `claimed` | both | The machine now holds this document's Planner and is ready to start sessions for it. | -| `unassign` / `release` | both | The machine no longer holds the document. No grace period. | -| `start` / `started` | both | One harness session exists for one held document. | -| `turn` ... `turn-end` | both | One prompt turn runs to exactly one end. | -| `host-tool-call` ⚠ | machine | Run a Chopin tool in the server. The only way the machine changes the document. | -| `input-request` | machine | Ask the document's members through Decisions. | -| `runs`, `stop`, `resume` | both | Report and control the session's workflow runs. | - -The ⚠ door is the only one with a durable effect in Chopin. It is today's -`host-tool-call`, checked against the turn's tool list, run under the -document's Planner owner and the [permission checks](hosted-agent.md#permission-checks). -Pushing to GitHub happens on the machine with the owner's own token, and GitHub -enforces it, as for builds. - -## Wire: `chopin.agent.v2` - -The rules of [Agent connection](agent-connection.md) still hold unless -listed here: JSON text frames of at most 2 MiB, strict objects, pings, close -codes, the Decisions mapping, stream parts, structured tool errors. v2 changes -the handshake and adds a document layer. Session and turn messages stay as -they are, so the server's `RemoteSession` and its tests carry over. - -### Endpoint and bearer - -- Same path, `/api/v1/agent`. The upgrade must offer `chopin.agent.v2`. Any - other upgrade, a v1 client's included, gets 426 with - `"supported": ["chopin.agent.v2"]`. -- `Authorization: Bearer `, the token - `POST /api/connector/connections` returns (`apps/connector/src/machine.ts:317-333`). - It names the machine, the checkout's repository and the connector's - protocol, and dies with that connection or a server restart. A GitHub token - or a `chopin_p_` credential is refused with 401. -- A connector opens the socket only after its MCP connection registers, and - closes it when that connection ends. A connector restart, or a new connection - token after a server restart, opens a new socket. +| Words after `--` | Builds | Planner | +| --------------------------------- | -------------------------------- | -------------------------------------------------------------- | +| `-e `, `--extension ` | Passed to `atomic-acp`, as today | Loaded into every Planner session (`additionalExtensionPaths`) | +| `--skill ` | Passed to `atomic-acp`, as today | Loaded into every Planner session (`additionalSkillPaths`) | +| `--model ` | Passed to `atomic-acp`, as today | Model used when the server sends none | +| Anything else | Passed to `atomic-acp`, as today | Ignored; `chopin status` lists it | + +A package path loads its extensions, skills and workflows together, as `HARNESS_EXTENSIONS` does on +the server; paths must be absolute and exist. So a Planner product ships as an Atomic package, and +Chopin's adapter handles the connection, worktree, Decisions and run reports for it. + +Two options apply to the Planner only: + +| Option | Effect | +| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--planner-sessions ` | How many Planner sessions the machine runs at once. Default 2 (decision 7). | +| `--planner-tools` | Also offers the tools of connect-configured extensions on Planner turns, listed in Work details. Without it they work only inside workflows (decision 6). | + +A machine runs Planners only when its agent has a harness adapter: `--agent atomic` from PR 4 and +`--agent copilot` from PR 7. An agent given as an ACP command after `--` runs builds only. +`chopin connect` saves these settings with the agent's absolute path for `chopin service install`. + +## Which machine runs a document's Planner + +The server decides, with the rule it uses today (`plannerDocuments` in +`apps/server/src/experiments/routes.ts`): + +| The document's **Machine** setting | Its Planner runs on | +| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A machine someone chose | That machine, while it is connected | +| **Default machine** | The creator's machine, while the document is in use: their favorite, else the machine last used for the document, else any of theirs that runs Planners (decision 3) | + +A document is in use while it is open, while someone asks its Planner for something, or while its +Planner waits on a question or a workflow run. After 30 minutes without any of these, Default +machine stops its Planner. While its machine can run it, a Default machine Planner stays put. + +A document has one Planner at a time, and the server takes ownership for the machine owner before +it sends `assign`. To plan with your own agent, connect your machine and choose it, or leave +Default machine on a document you created. With no machine able to run it, `@chopin` and +`invoke_planner` are refused with a message saying how to connect one. + +## Messages + +`chopin.agent.v2` keeps every rule in [Agent connection](agent-connection.md) it does not change +here. That covers frame limits, strict objects, pings, close codes, stream parts, structured tool +errors, questions and run reports. + +### Endpoint + +- The path stays `/api/v1/agent`. The upgrade must offer `chopin.agent.v2`; anything else gets 426 + with `"supported": ["chopin.agent.v2"]`. +- The bearer is the connection token `POST /api/connector/connections` returns. It names the + machine and ends with that connection or a server restart. Any other bearer gets 401. +- The connector opens the socket after its MCP connection registers and closes it when that + connection ends. + +### Message list + +| Message | From | `HarnessV1` call | Promise | +| ----------------------------------------------- | -------------- | ----------------------------- | ------------------------------------------------------------------------------- | +| `hello`, `welcome`, `refused` | machine/server | none | The machine names its harness; the server takes it or refuses | +| `assign`, `claimed`, `claim-failed` | server/machine | none | The machine holds this document's Planner, with its worktree and adapter ready | +| `unassign`, `release` | either | none | The machine no longer holds the document; its sessions end; no grace period | +| `start`, `started`, `start-failed` | server/machine | `doStart` | One harness session exists for one held document | +| `turn`, `tools`, `part`, `turn-end` | both | `doPromptTurn`, `emit` | One prompt turn runs to exactly one end | +| `abort` | server | the turn's `abortSignal` | The turn stops | +| `host-tool-call`, `host-tool-result` | machine/server | tool call, `submitToolResult` | A Chopin tool runs in the server; the only way the machine changes the document | +| `input-request`, `input-cancel`, `input-result` | both | Atomic HostInput | A question becomes Decisions and waits until answered or withdrawn | +| `runs`, `stop`, `resume`, `control-result` | both | Atomic workflow runs | The session's workflow runs are reported and controlled | +| `destroy`, `detach`, `error` | both | `doDestroy` | The session or the connection ends | + +The `hello` and document messages are new, and `start` and `started` gain fields. Every other +message keeps its current shape, including run reports with the `stopped` status and stage +`parentIds`. Ids are unique per connection, so routing by session is enough. ### Handshake ```text client { "type": "hello", "version": 2, "harness": { "name": "atomic", "version": "0.9.37" }, - "resources": { "extensions": ["planner-pkg"], "skills": ["review"] }, - "capacity": { "sessions": 4 }, - "checkout": { "origin": "git@github.com:octo-org/score.git", "branch": "main", "head": "3f2c…" } } - + "resources": { "extensions": ["planner-pkg"], "skills": ["review"] }, "capacity": { "sessions": 2 } } server { "type": "welcome", "version": 2, "machine": { "id": "m_…", "label": "build-box" }, - "repository": "octo-org/score", "harness": { "name": "atomic", "version": "0.9.37" }, "model": "openai/gpt-5.5" } + "harness": { "name": "atomic", "version": "0.9.37" }, "model": "openai/gpt-5.5" } server { "type": "refused", "code": "harness-unsupported", "message": "This Chopin runs Planners with copilot-sdk; this machine offers atomic." } ``` -- `resources` carries names only (the last path segment), never paths, for the - Machines page and Chat's machine menu. -- `hello.harness.version` is the version the connector has loaded, if any. - `welcome.harness.version` is the one the server asks for. When the managed - install lacks it, the connector installs it (see - [Bundling](#bundling-the-adapters-into-chopin)) before it answers any - `assign`; a failed install answers `claim-failed` with `harness-unavailable`. -- `capacity.sessions` is how many harness sessions the machine runs at once - (`--planner-sessions`, default 2). The server never starts more on it. Live - workflow runs do not use session slots; they are counted on their own and - shown in `chopin status`. -- New refusal codes: `harness-unsupported` (close 4422) and - `machine-revoked` (4403). `planner-attached` and `sign-in-required` no longer - apply at this level. - -### Documents - -The server decides which documents a machine holds. It reuses today's rule -(`plannerDocuments`, `experiments/routes.ts:377-404`): documents in the -machine's repository that its owner chose this machine for, minus any whose -Planner another account holds. One case is new. When someone asks a document's -Planner for a turn (`@chopin` or `invoke_planner`) and nobody chose a machine for -it, Automatic picks the requester's machine with the rule builds use: their -favorite, else the one last used for the document, else any free one -(`experiments/connections.ts:266-267`). Automatic never moves a Planner a -machine already holds. A requester with no machine connected for the repository -is told to run `chopin connect` in a checkout of it. - -The server takes the ownership through `PlannerAttachments` with the machine's -delegated login before it sends `assign`, so `assign` means "you hold it now". - -```text -server { "type": "assign", "document": { "id": "5f0c…e21", "title": "Score export", "url": "https://chopin.example/octo-org/score/plan", "repository": "octo-org/score" }, "resumed": false } -client { "type": "claimed", "document": "5f0c…e21" } -client { "type": "claim-failed", "document": "5f0c…e21", "code": "worktree-failed", "message": "git worktree add failed: …" } - -server { "type": "unassign", "document": "5f0c…e21", "code": "chosen-elsewhere", "message": "The document chose another machine." } -client { "type": "release", "document": "5f0c…e21" } -``` - -- `claimed` comes once the connector has the document's worktree and the - adapter loaded. Until then Chat's machine button says **Starting…**, as now. -- `claim-failed` releases the ownership at once. `code` is one of - `worktree-failed`, `harness-unavailable`, `at-capacity`, `failed`. -- `unassign` codes: `chosen-elsewhere`, `access-revoked`, `document-archived`, - `document-deleted`, `planner-reset`, `machine-revoked`. Each matches a row of - today's [ownership table](agent-connection.md#ownership). Every open - session of the document ends as if `destroy` had arrived. -- `release` is the machine letting go of one document, for example after a - local failure. No grace period. A connector that exits does not release; see - [grace](#ownership-grace-reconnects-and-moves). -- `resumed: true` marks an `assign` re-sent after a reconnect within the grace - period. Ownership and its generation are the same; sessions are new. +- `resources` carries names only, never paths, for the Machines page and Chat's machine menu. +- `welcome.harness.version` is the version the server asks for. If the managed install lacks it, + the connector installs it before it answers any `assign` ([Bundling](#bundling)). +- `capacity.sessions` caps the sessions the server starts on this machine. Live workflow runs do + not count against it (decision 7). -### Sessions and turns +| New refusal code | Close code | Meaning | +| --------------------- | ---------- | --------------------------------------------------------------------------------------------------- | +| `harness-unsupported` | 4422 | The machine offers another harness, or its `chopin` release cannot run the requested Atomic version | +| `machine-revoked` | 4403 | The machine was disconnected in Chopin or must be paired again | -Only two messages change shape: +### Documents and sessions ```text +server { "type": "assign", "document": { "id": "5f0c…e21", "title": "Score export", "url": "https://chopin.example/octo-org/score/plan", "repository": "octo-org/score" }, "resumed": false } +client { "type": "claimed", "document": "5f0c…e21" } +client { "type": "claim-failed", "document": "5f0c…e21", "code": "no-checkout", "message": "This machine has no checkout of octo-org/score." } server { "type": "start", "session": "s_…", "document": "5f0c…e21", "mode": "planner" } client { "type": "started", "session": "s_…", "workspace": { "kind": "worktree", "branch": "chopin/planner/5f0c…e21", "head": "3f2c…" } } -client { "type": "start-failed", "session": "s_…", "code": "at-capacity", "message": "This machine already runs 4 Planner sessions." } -``` - -- `start.document` is required and must be a document the connection holds, - else the client answers `start-failed` with `failed` and the server treats it - as a protocol error. -- `started.workspace` replaces the server's own `PlannerWorkspace` - (`atomic/workspace.ts`), so `channel.instructions(...)` can tell the Planner - it is in a worktree of the repository (`harness/session.ts:206-208`). -- `start-failed.code` is optional: `at-capacity`, `harness-unavailable`, - `failed`. - -Every other v1 message is reused verbatim: `turn`, `abort`, `tools`, `part`, -`host-tool-call`, `host-tool-result`, `turn-end`, `input-request`, -`input-cancel`, `input-result`, `runs`, `stop`, `resume`, `control-result`, -`destroy`, `detach`, `error`. Their ids are unique per connection, so routing by -session is enough. - -### One turn, end to end - -```mermaid -sequenceDiagram - participant W as Member (browser) - participant S as Chopin server - participant H as Harness host (chopin connect) - participant A as Atomic adapter - W->>S: @chopin "outline the export" - S->>H: start {session, document, mode: planner} - H->>A: doStart({sessionId, cwd: worktree, extensions, skills}) - H-->>S: started {workspace} - S->>H: turn {prompt, instructions, tools} - H->>A: doPromptTurn({tools, emit, abortSignal}) - H-->>S: tools {names: [read, search, workflow, ...]} - A-->>H: emit text-delta - H-->>S: part {text-delta} - A-->>H: emit tool-call (host tool "edit_block") - H-->>S: host-tool-call {edit_block, input} - S->>S: permission check, edit the room - S->>H: host-tool-result {output} - H->>A: submitToolResult - A-->>H: emit finish - H-->>S: part {finish}, turn-end {finished} +client { "type": "start-failed", "session": "s_…", "code": "at-capacity", "message": "This machine already runs 2 Planner sessions." } +server { "type": "unassign", "document": "5f0c…e21", "code": "chosen-elsewhere", "message": "octocat chose studio for this document, so build-box stopped running its Planner." } +client { "type": "release", "document": "5f0c…e21" } ``` -### Version rules - -- v2 is a new subprotocol, as [Versions](agent-connection.md#versions) - requires. `hello.version` is `2`. -- `@chopin/agent-link` carries the v1 and v2 schemas side by side until PR 8 - deletes v1. A server speaks one of them, chosen by its settings, never both. -- Additive fields within v2 follow the strict-object rule: a server rejects - fields it does not know, so a new field ships in the server before any client - sends it. - -## Ownership, grace, reconnects and moves +The connector sends `claimed` once the worktree and adapter are ready; until then Chat says +**Planner: starting on build-box…**. `start.document` must be a document the connection holds, and +`started.workspace` lets the server tell the Planner it works in a worktree. The connector prints +each `unassign` message. + +| `claim-failed` or `start-failed` code | Meaning | +| ------------------------------------- | ------------------------------------------------------------------------ | +| `no-checkout` | No checkout or clone of the repository (machines without a GitHub token) | +| `worktree-failed` | `git worktree add` failed | +| `harness-unavailable` | The requested adapter version could not be installed or loaded | +| `at-capacity` | Every session slot is in use | +| `failed` | Anything else | + +| `unassign` code | Cause | +| --------------------------------------- | -------------------------------------------------------------------------------- | +| `chosen-elsewhere` | The owner chose another machine, or their favorite changed under Default machine | +| `unused` | Default machine stopped the Planner after 30 minutes unused | +| `access-revoked` | The owner lost push or admin access, or instance admission | +| `document-archived`, `document-deleted` | The document was archived or deleted | +| `planner-reset` | A repository writer reset the document's Planner | +| `machine-revoked` | The machine was disconnected in Chopin; the socket closes too | + +## Session lifecycle ```mermaid stateDiagram-v2 [*] --> Free - Free --> Assigned: owner chooses machine M, or Automatic picks\nthe requester's machine; server takes ownership, sends assign + Free --> Assigned: machine chosen, or Default machine picks it\nserver takes ownership, sends assign Assigned --> Held: claimed Assigned --> Free: claim-failed - Held --> Grace: M's socket drops or its connector exits - Grace --> Held: M reconnects within grace\nassign (resumed) - Grace --> Free: grace ends + Held --> Held: start, turn, destroy + Held --> Grace: socket drops or connector exits + Grace --> Held: same machine reconnects in time\nassign (resumed) + Grace --> Free: grace period ends Held --> Free: unassign or release - Held --> Assigned: owner chooses machine N and confirms\nunassign to M, assign to N + Held --> Assigned: owner moves it to machine N and confirms\nunassign to M, assign to N ``` -- **One holder per document.** Unchanged. The holder is the machine owner's - delegated login (`machine-planners.ts`). Another account's choice of its own - machine is skipped while this one holds the document, as today. -- **Rechecks.** Before every `start` and `turn`, and every 5 minutes, the server - checks the machine (paired, authorized), the owner's admission, push or admin - access, and the document (`endpoint.ts:239-254`). A failed check now sends - `unassign` with `access-revoked` for that document instead of closing the - socket, unless the failure is the machine itself (`machine-revoked` closes - everything). -- **Grace.** A dropped socket or a connector that exits (Ctrl-C, a restart, a - crash) puts every document it held into grace for `PLANNER_ATTACH_GRACE_MS`. - The same machine reconnecting gets `assign` with `resumed: true` for each one - still in grace. Its sessions, turns and open questions ended when the socket - closed, as in v1: no turn is replayed. -- **Moving a document.** When the owner chooses machine N, the server sends - `unassign` (`chosen-elsewhere`) to M, releases without grace, takes the - ownership again for N and sends `assign`. N's first session starts empty. Its - first turn bootstraps from the bounded transcript and the current document, - which is how every harness session starts today - ([Session lifecycle](hosted-agent.md#session-lifecycle)). Nothing on M - is copied. -- **Workflow runs on a move.** If M reports live workflow runs, the machine menu - first lists them, says they will stop, and asks the owner to confirm. The move - happens only after they confirm; cancelling keeps the document on M. Once - moved, the runs become **stopped** cards and their state stays in M's Atomic - workflow store. Unassigns nobody chose (`access-revoked`, `machine-revoked`, - an archived or deleted document) do not ask. -- **Server restart.** Unchanged: every ownership is released and connection - tokens die. Connectors register again, open a new socket, and get `assign` - for every document still chosen. Automatic picks again on the next request. - -## Builds and Planners on the same machine - -| Concern | Decision | -| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Working trees | Builds keep `runs/`. Each document's Planner works in `planners///`, the path Planner commands use today. The connector now creates it (`git worktree add --detach` at the checkout's `origin` default branch) and never resets one with local changes. | -| Running at once | A build and Planner sessions run in parallel; they share no worktree. Builds stay one at a time per checkout, as now. | -| Session cap | `capacity.sessions`, default 2, set with `--planner-sessions `. Live workflow runs do not take a slot; the connector counts them separately and `chopin status` shows both. | -| Git locks | Worktree creation retries on `index.lock`/worktree lock contention with the checkout's own Git operations. | -| Shutdown | Ctrl-C reports running builds stopped (today), destroys sessions and closes the socket with `detach`. Held documents go into grace, so connecting again within it keeps them. A second Ctrl-C exits at once. | -| `Build plan` | Unchanged. It goes to the chosen machine over MCP, and usually lands next to the Planner that wrote the plan. | +| State | Who holds the Planner | Sessions | +| -------- | ------------------------------------------------ | ---------------------------------------------- | +| Free | Nobody | None | +| Assigned | The machine owner; the machine is getting ready | None | +| Held | The machine owner | Up to `capacity.sessions`, started on demand | +| Grace | The machine owner, for `PLANNER_ATTACH_GRACE_MS` | None: turns ended and open questions withdrawn | + +- **Rechecks.** Before every `start` and `turn`, and every 5 minutes, the server checks the + machine, the owner's access and the document. A failure sends `unassign`, or closes the socket + when the machine itself fails. +- **Grace.** A dropped socket or an exiting connector puts every held document into grace + (decision 9). Reconnecting in time brings `assign` with `resumed: true`; no turn is replayed. +- **Moves.** A move starts fresh sessions from the bounded transcript and the current document. If + the old machine has live workflow runs, the menu lists them and moves only once the owner + confirms they will stop (decision 4). +- **Server restart.** Every ownership ends. Reconnecting machines get `assign` for documents still + chosen; Default machine assigns again on next use. + +## Builds and Planners on one machine + +| Concern | Behavior | +| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Worktrees | Builds keep `runs/`. Each Planner works in `planners///`, which the connector creates with `git worktree add --detach` at `origin`'s default branch and never resets while it has changes. | +| Source | The worktree comes from the repository's `--checkout`, else from the clone in `repos//`, fetched first. | +| Retention | `--keep-runs` applies to build runs only. Planner worktrees stay until removed by hand. | +| Running at once | Builds and Planner sessions run in parallel and share no worktree. Builds keep today's queue. | +| Git locks | Worktree creation retries when the repository's own Git operations hold a lock. | +| Shutdown | Ctrl-C reports running builds stopped, destroys sessions and closes the socket with `detach`. Held documents go into grace. A second Ctrl-C exits at once. | ## Security -### What the harness gets - -| Credential | Where it lives | The machine's Planner harness | -| ---------------------------------------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Model login (`~/.atomic/agent/auth.json`, `copilot login`) | The machine | Read by the adapter in memory, as `HARNESS_AUTH=auto` does on a server today. Never sent to Chopin. | -| Owner's GitHub App token for host tools | The server | Never. Host tools run in the server with the delegated login's token. | -| Owner's GitHub App token for pushing | The connector, renewed 25 minutes before expiry | `CHOPIN_GITHUB_TOKEN_FILE` and `CHOPIN_GITHUB_TOKEN_EXPIRES_AT` in the environment of commands Atomic starts. The raw `CHOPIN_GITHUB_TOKEN` variable is dropped. | -| Connection token (v2 bearer) | Connector memory | Never. | -| Machine token `chopin_m_` | Base dir file | Never given. Readable by any process of the same user, as it is for build agents today (risk 3). | -| `chopin_p_` Planner credentials | Retired | n/a | -| `--env` values | `config.json` (0600) | In the environment, as for Planner commands today. | - -### Trust boundary - -- The server trusts the machine exactly as it trusts an attached v1 client: its - parts, its own tool reports and its run reports are the machine's word. Host - tool calls are checked against the turn's tool list, and a reported run never - starts a Planner turn (`followsUpRuns: false`, `harness/session.ts:101`). -- A machine runs Planners only for documents its owner chose it for, or that - Automatic gave it because its owner asked. Nobody else's choice or request - lands on it, so a shared build box cannot be steered into planning someone - else's document. -- Extensions and skills run as the machine user, chosen by the machine owner. - They do not reach the server; the server sees only their names. -- Push checks stay where they are. The server checks push or admin access at - assign, before every `start` and `turn`, and every 5 minutes. A push itself is - between the machine and GitHub, with the owner's token, as for builds and - Planner commands now ([Which GitHub account pushes](chopin-cli.md#which-github-account-pushes)). - -## Bundling the adapters into `chopin` - -The CLI is one `bun build --compile` executable per platform. The adapters' -own code is small TypeScript; the runtimes behind them are not. - -| Part | Size and shape | Decision | -| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Chopin's adapter glue (`packages/harness`) | Plain TypeScript | Compiled into `chopin`. | -| `@ai-sdk/harness` | Plain TypeScript | Compiled in. | -| `@bastani/atomic` | 51 MB package; more than 250 MB with dependencies, glibc and musl native modules and an embedded PostgreSQL ([Atomic caveats](self-hosting.md#choose-and-trust-a-harness)) | Not compiled in. Loaded at runtime with `import()` from an install the connector manages in `/harness/atomic/`, at the version the server asks for in `welcome`, installed with the machine's `npm` on first use. Never the machine's global install. | -| `@github/copilot-sdk` | Plain JavaScript | Compiled in. | -| Copilot CLI (`@github/copilot--`) | Native executable per platform (`agent/cli.ts`) | Not compiled in. The adapter drives the `copilot` the machine already installs for `--agent copilot`, found on `PATH` or through `COPILOT_CLI_PATH`, and checks its version. | - -Why not embed: each of the five targets would have to carry Atomic's native -modules (glibc and musl builds on Linux) and its embedded PostgreSQL, and -everyone who only runs builds would download a CLI several hundred megabytes -larger. Why a managed install rather than the machine's global `@bastani/atomic`: -the adapter is tested against one Atomic version (`package.json` pins 0.9.37), -the server asks for that version, and a global install drifts. A machine set up -for `--agent atomic` already has Node.js 22.19 and `npm` -(`packages/protocol/agents.ts:82-93`). - -PR 6 has to prove that a compiled Bun executable can `import()` an absolute -path outside its bundle and load that package's native modules on all five -targets. If it cannot, the fallback is a small helper script run by the -machine's Node.js that hosts the adapter over stdio (risk 1). +| Credential | Where it lives | What the Planner's harness gets | +| ---------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Model login (`~/.atomic/agent/auth.json`, `copilot login`) | The machine | The adapter reads it in memory. It never reaches Chopin. | +| The machine's GitHub authorization, for Chopin's tools | The server | Nothing. Chopin's tools run in the server as the machine owner. | +| The owner's GitHub App token, for pushing | The connector, renewed before it expires | Per session, the way a build's agent gets it: `GH_TOKEN`, `GITHUB_TOKEN` and Git's credential helper for the document's repository, reading a `0600` token file. | +| Connection token | Connector memory | Nothing. | +| Machine token `chopin_m_` | The base dir | Not given, but readable by any process of the same user (risk 3). | +| `--env` values | `config.json`, mode `0600` | In the environment, as for builds. | + +- The server takes the machine's stream parts and run reports at its word. It checks every Chopin + tool call against the turn's tool list, and a reported run never starts a Planner turn. +- A machine plans only documents its owner chose it for, or its owner's own on Default machine. + Extensions and skills run as the machine user; the server sees only their names. +- Pushes go from the machine to GitHub with the owner's token, as for builds + ([Which GitHub account pushes](chopin-cli.md#which-github-account-pushes)). + +## Bundling + +`chopin` is one `bun build --compile` executable for each of five targets. + +| Part | Shape | Decision | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| Chopin's adapter code (`packages/harness`) | TypeScript | Compiled in | +| `@ai-sdk/harness`, `@github/copilot-sdk` | TypeScript and JavaScript | Compiled in | +| `@bastani/atomic` | 51 MB, over 250 MB with dependencies, native modules for glibc and musl, and an embedded PostgreSQL | Loaded with `import()` from `/harness/atomic/`, installed with the machine's `npm` on first use (decision 5) | +| Copilot CLI | A native executable per platform | The `copilot` whose absolute path `chopin connect` saved, version-checked at `hello` | + +Embedding Atomic would grow the CLI by hundreds of megabytes for everyone, builds-only users +included. A machine set up for `--agent atomic` already has Node.js 22.19 and `npm`. PR 6 proves +the compiled binary can `import()` that install on all five targets (risk 1). ## Observability -`chopin status` gains a Planner block, and `--json` the same fields under -`planner`: +`chopin status` gains Planner lines, and `--json` the same fields under `planner`: ```text -Connector: running for /home/me/src/score (pid 4242, chopin 0.1.0, the service) -Planner: atomic 0.9.37 (from /home/me/.local/state/chopin/connector/harness/atomic/0.9.37) +Connector: running (pid 4242, chopin 0.0.14, the service) +Planner: atomic 0.9.37 from /home/me/.local/state/chopin/connector/harness/atomic/0.9.37 extensions: planner-pkg · skills: review · ignored: --thinking high - connection: up since 10:42 (chopin.agent.v2), 2 of 2 sessions in use, 1 workflow run live - https://chopin.example/octo-org/score/plan turn running 34s, 1 workflow run live - https://chopin.example/octo-org/score/export idle, held since 09:15 + agent connection up since 10:42, 2 of 2 sessions in use, 1 workflow run live + https://chopin.example/octo-org/score/plan turn running 34s + https://chopin.example/octo-org/site/roadmap idle, held since 09:15 ``` -- A document that failed to claim, or whose sessions keep failing, shows its last - error, redacted with `redacted()` (`apps/connector/src/connection.ts:59-67`). -- The connector keeps this state in its base dir, as it keeps chosen Planners - and start failures today (`chosenPlanners`, `startFailures` in `workspace.ts`), - so `chopin status` can read it from another process. -- The server's Machines page shows "Runs Planners with Atomic 0.9.37" and the - resource names from `hello`. Chat's machine button keeps its states. -- Server logs name each assign, claim, unassign and grace expiry with machine id, - document id and code. No prompts, no tool inputs. - -## What a Planner package changes - -A Planner product that ships today as a `--planner-command` program becomes an -Atomic extension or package: - -| Today, in the Planner command | After | -| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | -| Reads `CHOPIN_URL`, `CHOPIN_DOCUMENT`, `CHOPIN_PLANNER_TOKEN`; dials `/api/v1/agent` | Nothing. The connector holds the connection. | -| Reconnect, credential refresh, exit on 401 | Nothing. | -| Creates its worktree at `CHOPIN_WORKTREE` | Nothing. The session's working directory is the document's worktree. | -| Wraps Atomic itself, maps HostInput to `input-request`, reports `runs` | Nothing. Chopin's adapter does it. | -| Its prompts and procedures | Skills, prompt templates and workflows in the package. | -| Its own tools | Extension tools, usable inside its workflows, and on Planner turns when the machine owner passes `--planner-tools`. | -| Reads `CHOPIN_GITHUB_TOKEN` | Reads the file at `CHOPIN_GITHUB_TOKEN_FILE` before each use. | -| Started with `--planner-command "pkg-planner …"` | Loaded with `chopin connect . --agent atomic -- -e /path/to/pkg`. The same package also loads into builds. | - -## Alternatives considered - -| Option | For | Against | Verdict | -| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | -| A. Keep `HARNESS=remote` and Planner commands; ship a reference command built on Chopin's adapter | No protocol change | Still a process, socket and credential per document; settings still split; every product still owns the wire | Rejected | -| B. Split at `HarnessV1`, harness host in `chopin connect`, one socket per machine (this design) | Reuses the tested adapters and the v1 session messages; one place for agent settings | A new subprotocol; the connector grows a runtime dependency on Atomic | **Chosen** | -| C. Run the whole Planner (`HarnessAgent`, tools, prompts) on the machine; server exposes tools over MCP | Server becomes thinner | Prompts, permission checks and room writes would trust the machine; Chopin's tools would need a second transport; two copies of `session.ts` logic | Rejected | -| D. Keep v1, one socket per document, opened by the connector itself | No new handshake | N sockets and N credentials per machine; per-document `attach` cannot carry machine capabilities or capacity | Rejected | -| E. Run the adapter in a child process per machine instead of in the connector process | Crash and memory isolation from builds; its own environment | A second local protocol; more moving parts on day one | Deferred (decision 2) | -| F. ACP as the only Planner transport on the machine | One agent protocol for builds and Planner | ACP has no host tool results, structured output, HostInput or run control; loses what the adapters already do | Fallback only (PR 9) | -| G. Keep v1 GitHub-token attach for a person's own session beside v2 | People keep attaching their own session without `chopin connect` | Two ways to be a Planner, two admission paths, and a Planner that depends on a live browser sign-in | Rejected (decision 1) | - -## Rollout as stacked PRs - -Each PR is reviewable alone and leaves `main` working. Until PR 8 a server runs -either the old path (`HARNESS=remote` on v1) or `PLANNER_ON=machine` on v2, -never both. PR 8 removes v1 completely. - -| PR | Change | Acceptance tests | -| -- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 1 | Move the Atomic adapter, `full.ts` run control, the Copilot SDK adapter and runtime into `packages/harness`, free of server imports. `human-input.ts`, `workspace.ts` and `checkout.ts` stay in the server. | `atomic.contract.test.ts`, `copilot-sdk/*.test.ts` and `harness/contract.test.ts` pass unchanged from their new home; `bun run types`; the server imports the package. | -| 2 | `chopin.agent.v2` schemas in `@chopin/agent-link`, beside v1 until PR 8; v2 section in `docs/agent-connection.md`. | Protocol tests for every new message and limit; v1 tests untouched. | -| 3 | Server: `PLANNER_ON`, connection-token admission on v2, `assign`/`claimed`/`unassign` driven by `plannerDocuments`, Automatic and `PlannerAttachments`, remote adapter keyed by machine and document, grace on connector exit. The machine menu asks before moving a document with live workflow runs. | With the agent-link test client speaking v2: assign on choice; with nothing chosen, `@chopin` assigns the requester's favorite, else the machine last used for the document; claim-failed releases; reconnect within grace keeps the generation; choosing machine N unassigns M and assigns N; with live runs on M, nothing moves until the owner confirms; push loss unassigns one document; revoking the machine closes the socket; a v1 upgrade gets 426 listing only v2 and a GitHub token gets 401; `PLANNER_ON=machine` with `HARNESS_AUTH` refuses to start. | -| 4 | Connector: harness host behind an interface, Atomic from a managed install at the version `welcome` asks for, document worktrees, Atomic flags read from connect settings, `--planner-sessions`, `--planner-tools`. | E2E against a real server with Atomic's stub model (`atomic/model-stub.ts`): an `@chopin` turn edits the document; a questionnaire round-trips through Decisions; Stop/Resume a workflow run; live runs leave session slots free; Ctrl-C then connecting within grace keeps every document; an extension tool reaches Planner turns only with `--planner-tools`; a build runs during a Planner turn. | -| 5 | `chopin status` Planner block and `--json`; Machines page harness line. | Status snapshot tests; the Machines page shows the harness and resource names; nothing in status output matches the token patterns `redacted()` masks. | -| 6 | Compiled-binary support: runtime `import()` of the managed Atomic, `chopin harness check`. | CI builds each binary; on every target with a CI runner it installs the pinned Atomic, runs `chopin harness check`, and runs one stub-model turn. | -| 7 | Copilot SDK on the machine: `useLoggedInUser: true`, the machine's `copilot` CLI, version check. | The shared contract suite runs against the machine-hosted Copilot adapter; a turn with `--agent copilot` in CI with a stub. | -| 8 | Remove v1 completely: its schemas and client in `@chopin/agent-link`, GitHub-token attach and `sign-in-required`, `HARNESS=remote`, `--planner-command`, `--planner`, `POST /api/connector/planner`, `chopin_p_` credentials and the protocol 4 Planner poll. `invoke_planner`'s refusal points at `chopin connect`. Rewrite `agent-connection.md` for v2; update `chopin-cli.md`, `self-hosting.md`, `hosted-agent.md`, `local-agent-mcp.md`, `architecture.md`. | Startup refuses `HARNESS=remote` with a pointer; `chopin connect --planner-command` exits 2 with a pointer; no v1 schema is left in `@chopin/agent-link`; `bun run ci`; docs link check. | -| 9 | Optional: ACP fallback Planner adapter. Host tools reach the ACP agent through the connector's local MCP bridge (`apps/connector/src/implementation.ts`), structured output through a result tool. | The shared contract suite, minus the capabilities ACP lacks, which refuse with `HarnessCapabilityUnsupportedError`. | - -PRs 1 and 2 have no user-visible change. A deployment can switch to -`PLANNER_ON=machine` after PR 4. One that stays on `HARNESS=remote` keeps v1, -Planner commands and GitHub-token attach until PR 8 removes them. +- A failing document shows its last error with tokens masked. The connector keeps this state in its + base dir, so `chopin status` reads it from another process. +- The Machines page shows "Runs Planners with Atomic 0.9.37" and the resource names from `hello`. +- Server logs name each assign, claim, unassign and grace expiry by machine, document and code, + never prompts or tool inputs. + +## Alternatives + +| Option | Verdict | +| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| A. Keep today's setup, the left column of [What this replaces](#what-this-replaces), and ship a reference backend | Rejected: still a process, socket and credential per document, and settings split between builds and Planner | +| B. Split at `HarnessV1`, harness host in `chopin connect`, one socket per machine | **Chosen**: reuses the tested adapters and session messages; one place for agent settings | +| C. Run the whole Planner, prompts and tools included, on the machine | Rejected: permission checks and document writes would trust the machine | +| D. One socket per document, opened by the connector | Rejected: N sockets per machine, and no place for machine-wide harness and capacity | +| E. Run the adapter in a child process | Deferred (decision 2): better isolation, but a second local protocol on day one | +| F. ACP as the only Planner transport | Fallback only (PR 9): ACP has no tool results from the host, structured output, HostInput or run control | +| G. Keep direct attach for a person's own session beside v2 | Rejected (decision 1): two admission paths, and a Planner tied to a browser sign-in | + +## Rollout + +Each PR stands alone and leaves `main` working. Until PR 8, a server runs either today's path or +`PLANNER_ON=machine`, never both. + +1. **Shared adapters.** Move the Atomic and Copilot SDK adapters into `packages/harness`. + - Acceptance: the Atomic, Copilot SDK and harness contract suites pass unchanged from there. +2. **v2 schemas.** Add `chopin.agent.v2` to `@chopin/agent-link` and to `agent-connection.md`. + - Acceptance: protocol tests cover every new message and limit. +3. **Server.** `PLANNER_ON`, connection-token admission, assign, grace and move confirmation. + - Acceptance: a v2 test client sees every transition in [Session lifecycle](#session-lifecycle). +4. **Connector.** The harness host, managed Atomic, worktrees, per-session tokens, both options. + - Acceptance: with Atomic's stub model, `@chopin` edits, Decisions, Stop/Resume and a build pass. +5. **Status.** `chopin status` Planner lines, `--json`, and the Machines page harness line. + - Acceptance: status snapshots pass and contain nothing `redacted()` would mask. +6. **Compiled binaries.** The binary loads the managed Atomic; `chopin harness check` reports it. + - Acceptance: each CI target installs the pinned Atomic and runs one stub-model turn. +7. **Copilot on the machine.** The Copilot SDK adapter as the machine's user, version-checked. + - Acceptance: the shared contract suite passes against the machine-hosted Copilot adapter. +8. **Removal.** Remove everything in [What this replaces](#what-this-replaces) and its docs. + - Acceptance: retired setting and options fail with a pointer, and `bun run ci` passes. +9. **Optional ACP fallback.** An ACP Planner adapter using the connector's local MCP bridge. + - Acceptance: the shared contract suite passes; capabilities ACP lacks refuse cleanly. ## Risks -1. **Bun's compiled executable may not load an external package with native - modules.** PR 6 proves it on all five targets before anything depends on it. - Fallback: a Node.js helper hosting the adapter over stdio. -2. **Atomic in the connector process.** A native crash or a runaway workflow - takes builds down with it, and memory grows with sessions. The session cap - bounds sessions, but live workflow runs sit outside it. The harness host is - behind an interface, so moving it into a child process (alternative E) is a - contained change if PR 4's E2E shows crashes or memory growth. -3. **Same-user access.** Extensions and Atomic workflows can read the machine - token and `config.json`, as build agents can today. Nothing in this design - makes it worse; nothing makes it better either. -4. **Version skew.** The server asks for one Atomic version, and the - connector's compiled adapter glue was built against one too. A connector - from another release may not handle the version asked for; `hello`, - `welcome` and `chopin harness check` catch it (decision 11). A machine's - Copilot CLI is whatever the user installed; the connector checks its version - at `hello` and refuses with a message naming the version it needs. -5. **One socket for many documents.** A 2 MiB frame for one document delays the - others, and the shared 16 MiB queue fills faster. Watch it in PR 4's E2E with - several documents; split frames or add per-document flow control only if it - shows. -6. **Users lose the separate Planner command.** Anything a product did outside - Atomic's extension model has no home. `--planner-tools` (decision 6) covers - the biggest gap, tools on Planner turns. -7. **People who planned from their own session must run `chopin connect`.** - That means pairing a machine and using an agent with a harness adapter: - Atomic from PR 4, Copilot from PR 7. PR 8 comes after both. +1. **Compiled Bun may not load Atomic's native modules.** PR 6 checks first; the fallback is a + Node.js helper hosting the adapter over stdio. +2. **Atomic shares the connector process.** A crash takes builds down too. A child process + (alternative E) is a contained change if PR 4 shows trouble. +3. **Same-user access.** Extensions can read the machine token and `config.json`, as build agents + can today. +4. **Version skew.** `hello`, `welcome` and `chopin harness check` catch an unsupported Atomic + version and name the release to install (decision 11). +5. **One socket, many documents.** A 2 MiB frame delays other documents; PR 4 measures it. +6. **One process, many repositories.** Each repository has its own GitHub token, so each Atomic + session needs its own environment. PR 4 proves it, or alternative E fixes it. +7. **Products outside Atomic's model.** `--planner-tools` covers the largest gap. ## Decisions -1. **Retire v1 entirely.** Every Planner off the server, a person's own - interactive one included, runs through a connected machine on v2. One way to - be a Planner, one admission path. -2. **Adapter in the connector process first**, behind an interface. Fewer moving - parts now; a child process stays a contained change. -3. **Automatic picks the requester's machine** when nobody chose one: their - favorite, else the one last used for the document. It is what Automatic - already means for builds. -4. **Moving a document with live workflow runs asks the owner first**, showing - what is running and that it will stop. Runs cannot move, so stopping them - should be a choice. -5. **Atomic comes from a managed install** in the connector's base directory, at - the version the server asks for. A global install drifts from the version - the adapter is tested against. -6. **Extension tools reach Planner turns only with `--planner-tools`.** The - machine owner chose that code, so they opt in to it. -7. **Default cap of 2 Planner sessions per machine.** Live workflow runs don't - take a slot, so a run never blocks a new conversation. -8. **One `HARNESS` adapter per server under `PLANNER_ON=machine`.** One adapter - to test and support per deployment. -9. **A connector restart keeps ownership through the grace period**, like a - dropped socket. A quick restart should not hand the document to someone else. -10. **No limit on live workflow runs per machine.** The machine's owner decides - how much work it takes on. -11. **Each `chopin` release declares the Atomic versions its adapter supports.** - When the server asks for one outside that range, the connector refuses with - `harness-unsupported`, and `chopin status` and the log name the `chopin` - release to install (`chopin update`). +| # | Decision | Why | +| -- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ | +| 1 | Every Planner off the server runs through a connected machine on `chopin.agent.v2`, including a person's own | One way to be a Planner, one admission path | +| 2 | The adapter runs in the connector process first, behind an interface | Fewer moving parts now; a child process stays a contained change | +| 3 | Default machine runs the Planner on the creator's machine, with today's rule | Same meaning of Default machine for builds and Planners | +| 4 | Moving a document with live workflow runs asks the owner first | Runs cannot move, so stopping them should be a choice | +| 5 | Atomic comes from a managed install at the version the server asks for | A global install drifts from the tested version | +| 6 | Extension tools reach Planner turns only with `--planner-tools` | The machine owner chose that code, so they opt in | +| 7 | Each machine runs at most 2 Planner sessions by default | Bounds load; live workflow runs take no slot | +| 8 | One `HARNESS` adapter per server under `PLANNER_ON=machine` | One adapter to test and support per deployment | +| 9 | A connector restart keeps ownership through the grace period, like a dropped socket | A quick restart should not hand the document to someone else | +| 10 | No limit on live workflow runs per machine | The machine owner decides how much work it takes on | +| 11 | Each `chopin` release declares the Atomic versions it supports and refuses others with `harness-unsupported` | Status and the log then name the release to install with `chopin update` | + +## Glossary + +| Term | Meaning | +| ---------------- | ------------------------------------------------------------------------------------------- | +| Planner | The agent that co-authors a document, asks questions through Decisions and starts workflows | +| Harness | The runtime that drives the model: Atomic or Copilot SDK | +| Adapter | Chopin's `HarnessV1` code for one harness | +| Harness host | The new part of `chopin connect` that runs an adapter for the documents its machine holds | +| Machine | A computer paired with Chopin; its running `chopin connect` is the connector | +| Connection token | What a connector gets when it connects; it authorizes the agent connection | +| Holder | The account whose machine runs a document's Planner; one at a time | +| Grace period | How long a holder keeps a document after its socket drops (`PLANNER_ATTACH_GRACE_MS`) | +| Default machine | The **Machine** setting that runs the Planner on the creator's machine | +| Chopin's tools | The tools that read and change the document; they always run in the server |