Skip to content

v0.10.0

Choose a tag to compare

@github-actions github-actions released this 26 Oct 22:11

0.10.0

Minor Changes

  • 5ea2f11: Control can start, prompt and cancel any agent that speaks the Agent Client Protocol (CONTROL.md §6, api mode; ADAPTERS.md "Agents driven through ACP"). The daemon runs the agent's own CLI as a child on the launch line the ACP registry publishes (Gemini CLI, OpenCode, goose, Copilot CLI, Cursor, Droid, Devin, Kilo, Qwen Code, Cline and twenty more, 2026-10-22), is its JSON-RPC client, keeps the process for the next message, cancels with session/cancel, reopens an idle session with session/load where the agent can, and answers its permission requests from the machine's mode (safe refuses and the reply says what; auto allows). Only an agent already installed is ever run (on PATH, or where SESSIONPIPE_ACP_<HARNESS> points); the hello advertises the ones found. The pairing request and every hello list at most 20 harnesses, the schema's limit (Claude Code, Codex and Antigravity first, then the agents in the registry's order), and their modes include api whenever an ACP agent is listed.

  • 7b65074: A Claude Code home that runs on another API now has an account. Such a home has no claude.ai login, and its settings.json sets env.ANTHROPIC_BASE_URL (Kimi K3 on a Modal endpoint, GLM on Z.ai). Its events carry account_id: api-<16 hex>, made from the endpoint's origin and the same on every machine that uses it. Control's pair and hello name it by the endpoint's host, in accounts[].name, a new optional field. Before this, its sessions carried no account, so a board filed them with the harnesses that report none (a person's Kimi sessions sat in "No account" beside 1,331 Codex ones). Only that one settings key is read, never the token or the headers beside it.

  • 27c4194: Control: device keys (CONTROL.md §2, §3.2). sessionpipe-core/control verifies a device key's signature ({cred, sig}, raw r‖s over sessionpipe.device.v1⏎<purpose>⏎<rp id>⏎<challenge>) for a grant, a confirm or a pairing proof, chosen by the machine's own record of the key; keyUsable closes key fields and checks a device key's rules (type: "device", alg -7, store: "hardware", uv: "each-use", id = SHA-256 of the SPKI); verifyCommand refuses a key.add whose key could never be used (bad_fields). verifyAssertion takes the purpose. New exports DEVICE_CONTEXT, deviceMessage, deviceKeyId, KEY_TYPES, DEVICE_SIG_FIELDS, DeviceSignature, DeviceGrant. The daemon advertises key_types: ["passkey","device"] when pairing and in every hello, stores a device key whole (from a pairing or a key.add), and control keys shows each key's kind. 26 new shared vectors (111–136).

  • abcba7f: Control: a third machine mode, inherit, so sessions you message run the way your own sessions on that machine run. sessionpipe control mode inherit (or --mode inherit on control pair and connect, or the whole word at pairing's question) passes no permission flag, so each harness starts every turn in its own default there: Claude Code in its settings' permissions.defaultMode (a resume too, and through the Agent SDK too), Codex as its config.toml's approval_policy and sandbox_mode say, Antigravity with no flag, as in safe. No mode passes a bypass flag. Before this, a machine whose Claude Code settings turn auto mode off (permissions.disableAutoMode) had no mode that fit: auto ran each messaged session in Claude Code's default mode, which refused every command that needed approval, and safe refuses them by design. Schemas: control-pair and control-hello mode gains inherit.

  • 531b391: Control: a machine runs in inherit mode only with receivers that know it. A receiver that predates inherit shows such a machine as safe, while its sessions run with no permission flag. A receiver now says it shows the mode as what it is with control.inherit: true in its well-known file (HTTP.md §1, CONTROL.md §2), and sessionpipe control pair --mode inherit (and connect --mode inherit) refuses, before anything is sent, a receiver that doesn't: "… doesn't know inherit mode yet; it would show this machine as safe while the sessions you message run with no permission flag". sessionpipe control mode inherit checks every paired receiver the same way and keeps the mode it had when one doesn't know it, or can't be read. Schema: well-known control.inherit (optional boolean). Conformance: delivery/23-control-inherit.

  • fc0fedd: Two Node-only subpaths in sessionpipe-core, so a receiver of the person's own (a board on their machine) runs as a service with the daemon's code rather than a copy of it. sessionpipe-core/service: the launchd agent and the systemd user unit for any process (serviceFiles, installService and uninstallService, each with a dry run that answers the files and commands and touches nothing, and ensureLinger). sessionpipe-core/version: compareVersions, isNewer, parseVersion, UPDATE_EXIT and supervise, the parent that restarts a process onto new code where no service manager will. The root export is unchanged and still runs in a Worker and a browser. The control daemon now uses both; its service is the same, except that the unit's ExecStart quotes every argument.

  • 2865e0d: For receivers. What a 0.10.0 machine sends and accepts, and how to adopt it while some machines still run 0.9.0 (CHANGELOG.md, "For receivers (0.10.0)", has the detail).

    • Closed command field set. Each command kind carries only its own fields; any other field inside the signed cmd is refused bad_fields, by the verifier and on the machine (0.9.0 ignored it). Keep your own data beside cmd, never in it.
    • mode: "inherit" in pairing requests and hellos: no permission flag, so a start that names no level runs as the person's own sessions there do, permission checks off included where their settings say so. Show it as inherit, never as safe or auto. A 0.10.0 machine pairs in inherit, or switches to it, only with a receiver whose well-known file says control.inherit: true; it tells everyone else's person "… doesn't know inherit mode yet; it would show this machine as safe". Set the flag once you show the mode truthfully and accept it in /pair and /hello (conformance delivery/23).
    • Refusal code mode is now permission, plus refused model and failed model. Map both codes; a 0.9.0 machine still says mode.
    • Hello: start (per harness: permissions among read, safe, edits, auto, capped by the mode; the default permission, which may be inherit; models, any_model), prompt_files: true, and no home: a start's cwd may be ~ or ~/… instead. harnesses lists at most 20, and modes include api when an ACP agent is listed.
    • Acks: a start's taken and delivered carry applied (permission may be inherit). Show applied, never what was asked.
    • Events: session.attended, turn.ended.background, turn.started.submitted_by and injected, session.launched_by, and account_id api-<16 hex> for a Claude Code home on another API, named in accounts[] by name with no email. All optional, tier 0.
    • ACP agents (gemini-cli, opencode, goose, cursor, copilot-cli, droid and more) appear in harnesses and in ack session refs, acked in mode api; they take no start options.
    • Device keys: an app that can't run WebAuthn signs with a hardware-held P-256 key, {id, alg: -7, spki, type: "device", store: "hardware", uv: "each-use"}, its signature {cred, sig} (plus str in a grant) where an assertion would stand; a 0.10.0 machine says key_types: ["passkey","device"] when pairing and in every hello. To accept them, store and carry the key whole (all six fields) in a pairing's and a key.add's key, pass a {cred, sig} grant, confirm or pairing proof through untouched, and say control.signing.key_types: ["passkey","device"] in your well-known file. They are added only as passkeys are: at the terminal's pairing code, or by a key.add a trusted key signed. A receiver that doesn't know them keeps working, passkeys only.
    • While machines run 0.9.0: send permission or model only where the last hello's start advertises them (0.9.0 ignores them silently); files on a prompt only with prompt_files: true (0.9.0 refuses them); a ~ folder only to a machine whose hello has start; never inherit as a command's level; a device key only for a machine whose last hello lists device in key_types (0.9.0 would store it as a passkey and refuse its signatures); read an absent attended, background, submitted_by, injected or launched_by as unknown, never as false, 0 or a person.
  • a6f8bb7: A control prompt can carry files, as a start can (CONTROL.md §3.1, §6 prompt), to a machine whose hello says prompt_files: true. The daemon fetches and checks them as for a start, writes them to <cwd>/.sessionpipe/files/<session>/<nonce>/ (the command's signed nonce, never the receiver's unsigned message id), and appends the same "Attached files" block before the message is handed over in any mode: a resume, a fork, a held SDK session, an ACP agent (api), a waiter or a Stop. A file that fails is acked failed with code file and the message is not delivered. A receiver no longer needs to put a link to an image into the message's text.

  • d51fd31: session.attended says whether a person attends the session: an optional tier-0 boolean in the harness's own word, absent when the harness does not say (unknown, never false). Claude Code fills it from CLAUDE_CODE_SESSION_ATTENDED, counting a control turn and a session with a link (its Remote Control bridge, the desktop app) as attended: every headless run says 0, a Remote Control session included, and a claude -p started inside one inherits CLAUDE_CODE_ENVIRONMENT_KIND=bridge, so the link is what tells them apart. The hook carries CLAUDE_CODE_SESSION_ATTENDED and SESSIONPIPE_CONTROL_JOB into the job, and the worker takes those and CLAUDE_CODE_HOST_SESSION_ID from the job alone, so a worker another session's hook spawned no longer lends a swept job its own. Codex, Gemini CLI, Antigravity and Devin leave it absent.

  • fae77a5: session.launched_by: the session that started this one, as <harness>:<id>, read from SESSIONPIPE_LAUNCHED_BY in the hook's environment (PROTOCOL.md §3). A script that runs claude -p for a session sets it, and a receiver can group the run under its launcher without reading it as a subagent. A subagent keeps its parent_id and gets no launched_by.

  • bd49ef7: A control start can say how much the new session may do and which model it runs (CONTROL.md §2, §3.1, §5 step 8, §6 Options; ADAPTERS.md "Start options"). permission is one of four levels, read, safe, edits and auto, with nothing above auto; model is the harness's own id, passed as one argument. The machine's mode is the ceiling, and a start above it, or at a level the harness has no flag for, is refused permission before anything is taken or run (the daemon's old mode code). On a machine in inherit mode a start that names no level runs with no permission flag, as the person's own sessions there do, and one that names a level may name only read or safe, since sessionpipe can't rank the person's own default; inherit is never a level a command names. The hello advertises what each harness takes here (start, capped by the mode: Claude Code's levels from its --help, Codex's models from its own cache; a harness's default may be inherit). A start's cwd may be ~ or ~/…, read under the daemon user's home, so a receiver can start a session in a folder an event reported without the machine ever telling it its home. A start's level and model are recorded with the session, and every later resume or fork runs at the narrower of that and the machine's mode then (at most safe on an inherit machine for a session that named a level). The taken and delivered acks of a start carry applied (its permission may be inherit); a model the harness rejects ends failed with code model. An ACP agent takes neither option: a start that names one is refused, never run without it. Each command kind now has a closed set of fields, so a field a machine doesn't know is refused rather than ignored. sessionpipe control status shows the options per harness. A start whose text begins with a dash no longer reaches claude -p as a flag.

  • d7269b4: turn.ended carries background: how much background work the harness reports still in flight as the turn ends, each piece of which wakes the session when it finishes. Claude Code's Stop hook lists it as background_tasks (in every Stop recorded since 2.1.258): a background shell command, a monitor, a subagent, a workflow. A session that ended its turn waiting on a two-hour background command read as finished on its receiver, under "Last 24 hours", until the command woke it. Tier 0, a count only: never what that work runs.

  • f8770b9: turn.started can now say who started the turn: two optional tier-0 fields, submitted_by (the harness's own word for who submitted the prompt, forwarded verbatim and never mapped onto a vocabulary of the spec — a different vocabulary from session.started.source) and injected (true when the prompt text is a block the harness wrote rather than words a person typed). Claude Code fills submitted_by from UserPromptSubmit.source when a payload carries that key — its hook schema documents user · sdk · system · loop_wakeup · schedule_wakeup · poll_event and says a payload may omit it while the field rolls out, and no build through 2.1.304 sends it — and sets injected from a closed list of its own prompt wrappers (<task-notification>, <cross-session-message …>, <system-reminder>, <local-command-stdout>, <command-…>), so a task notification, a peer message or a slash-command expansion is distinguishable from a person typing. injected: true says only that the prompt is not the person's words, never that no person caused the turn: a Talk or sessionpipe wait delivery arrives inside a <task-notification>. Both fields are absent when the harness does not say, and an absent field is unknown, never "the person". sessionpipe tail prints both. Codex, Gemini CLI, Antigravity and Devin leave both absent.