@staix/agent-hub 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +4 -4
- package/docs/agent-notes/README.md +6 -0
- package/docs/agent-notes/adapters.md +13 -10
- package/docs/agent-notes/bus.md +1 -1
- package/docs/agent-notes/daemon.md +11 -4
- package/docs/agent-notes/local-worker.md +3 -1
- package/docs/agent-notes/tasks.md +6 -1
- package/docs/agent-notes/tests.md +8 -4
- package/docs/dogfood.md +13 -4
- package/docs/memory.md +1 -1
- package/docs/operations.md +98 -28
- package/docs/quickstart.md +13 -5
- package/docs/sdlc.md +12 -0
- package/docs/security.md +24 -15
- package/docs/smoke.md +3 -3
- package/docs/specs/2026-09-19-agent-hub-design.md +101 -8
- package/docs/verification/2026-10-25-remote-docker-transport.md +10 -0
- package/docs/verification/2026-10-26-agy-1.4.7.md +69 -0
- package/docs/verification/2026-10-26-claude-implementer-2.1.310.md +69 -0
- package/docs/verification/2026-10-26-headless-claude-shell-context.md +62 -0
- package/docs/verification/2026-10-26-jcode-0.99.1.md +49 -0
- package/docs/verification/2026-10-26-kimi-232-reviewer-compat.md +53 -0
- package/docs/verification/2026-10-26-pi-140-compat.md +41 -0
- package/docs/verification/2026-10-26-qwen-0261-acp.md +60 -0
- package/docs/verification/2026-10-27-0.19.0.md +86 -0
- package/docs/verification/2026-10-27-0.20.0.md +75 -0
- package/docs/verification/claude-permission-transport-native.py +187 -0
- package/docs/verification/claude-shell-wrapper-host.ts +20 -0
- package/docs/verification/claude-shell-wrapper-native.py +145 -0
- package/docs/verification/pi-140-model-free.ts +57 -0
- package/docs/verification/pi-140-native.mjs +21 -0
- package/docs/verified.json +25 -17
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +373 -72
- package/src/adapters/acp.ts +8 -0
- package/src/adapters/agy/entry.ts +1 -1
- package/src/adapters/channel-peer.ts +7 -5
- package/src/adapters/claude-channel.ts +7 -3
- package/src/adapters/claude-implementer.ts +6 -3
- package/src/adapters/codex-appserver.ts +11 -1
- package/src/adapters/compat.ts +32 -18
- package/src/adapters/conformance/enrollment.ts +4 -4
- package/src/adapters/conformance/factories.ts +2 -1
- package/src/adapters/conformance/fakes/pi-rpc.ts +1 -0
- package/src/adapters/conformance/kit.ts +10 -0
- package/src/adapters/definitions/builtin-metadata.ts +2 -2
- package/src/adapters/definitions/native-factories.ts +7 -2
- package/src/adapters/definitions/reviewer-variants.ts +2 -2
- package/src/adapters/local-worker.ts +2 -0
- package/src/adapters/pi.ts +46 -3
- package/src/adapters/qwen-dual.ts +25 -0
- package/src/adapters/registry-types.ts +6 -2
- package/src/adapters/registry.ts +1 -0
- package/src/adapters/reviewer-print.ts +1 -0
- package/src/cli/agents.ts +69 -7
- package/src/cli/claude-shell-wrapper.ts +54 -0
- package/src/cli/main.ts +30 -24
- package/src/cli/output.ts +4 -0
- package/src/hub/agent-drift.ts +61 -0
- package/src/hub/board.ts +14 -2
- package/src/hub/check-runner.ts +148 -18
- package/src/hub/ci-watch.ts +140 -0
- package/src/hub/claude-launch-proof.ts +67 -2
- package/src/hub/claude-shell-wrapper.ts +349 -0
- package/src/hub/config-apply.ts +2 -2
- package/src/hub/config-edit.ts +2 -2
- package/src/hub/config-tier.ts +2 -2
- package/src/hub/config-trust.ts +1 -0
- package/src/hub/daemon.ts +282 -113
- package/src/hub/host-executable.ts +20 -1
- package/src/hub/instances.ts +18 -2
- package/src/hub/lifecycle.ts +1 -1
- package/src/hub/operational-authority.ts +7 -0
- package/src/hub/operational-view.ts +6 -10
- package/src/hub/peers.ts +34 -0
- package/src/hub/read-only-shell-proof.ts +7 -6
- package/src/hub/review-scope.ts +13 -0
- package/src/hub/sdlc-metrics.ts +6 -4
- package/src/hub/settings.ts +1 -0
- package/src/hub/task-shell-command.ts +85 -0
- package/src/hub/task-shell-default.ts +79 -0
- package/src/hub/task-shell-proof.ts +171 -0
- package/src/hub/task-shell-sandbox.ts +61 -0
- package/src/hub/tasks.ts +274 -16
- package/src/local/deny.ts +112 -11
- package/src/local/sandbox.ts +70 -29
- package/src/local/shell-environment.ts +10 -0
- package/src/ui/index.html +1 -0
package/docs/security.md
CHANGED
|
@@ -6,14 +6,14 @@ agent-hub connects agents that can each run commands. This page says what the hu
|
|
|
6
6
|
|
|
7
7
|
- **Other agents' text is untrusted.** Every message that crosses from one peer to another is framed as untrusted input: a channel tag with `meta.source` for Claude started with `ahub claude`; otherwise a fixed header line (`frame()`, `[agent-hub message from ...]`) with an instruction that says it is untrusted, the plugin's server instructions for a tools-only Claude reading `hub_inbox` and a standing instruction for the others. A message body cannot forge the hub's own headers: such lines are quoted (`sanitize`). Replies inherit a hop count capped at 3, so agents cannot ping-pong forever; neither a digest nor a steer can reset it.
|
|
8
8
|
- **The control link is loopback plus a secret.** The daemon and the Codex proxy bind 127.0.0.1 only. The control WebSocket requires a per-run token (`.agenthub/state/control-token`, mode 600), and both servers refuse any request that carries an `Origin` header: any web page can open a WebSocket to localhost, and browsers always send `Origin`. The bearer header a Codex TUI sends to the proxy carries only the terminal launch id (#586): it binds terminal ownership and is not a credential. External clients cannot claim the console user's id or a hub-managed peer's id.
|
|
9
|
-
- **Permission prompts stay on unless an operator selects a permission mode.** `ask` leaves native decisions unchanged. `ask-when-needed` grants scoped Claude file tools or Pi/local writes and edits; Kimi and Codex use their own native policies. `never-ask` requires explicit runtime confirmation or a person's console `y` for a config default at hub start. Until that config confirmation the peer runs in `ask`. Kimi's and `local`'s permission requests are relayed to the console and cancelled after `approvals.timeout_s` (default 120 s) of silence; the macOS notification for a waiting request carries the peer and the tool name only. The one exception is the hub's own tools (`hub_send` and the task tools, matched by exact name): Kimi's requests for them are approved once without a prompt and logged by name, the same trust Codex gets through `approval_mode` in the hub's config. They invoke hub-owned operations rather than arbitrary file or shell tools, and every call passes the hub's own checks. A call the always-ask policy classes (below) is never approved this way, whatever its title. Identity comes from the exact permission title, or from the earlier tool-call title bound to the same call id and resolved against the configured MCP servers when the permission title contains argument JSON (Qwen). Payload text and unrelated display titles never establish identity. Configured ACP kinds (#451) never get this exception: a request whose title spells a hub tool exactly still goes to the person approval path, and only a mapped native mode changes what the agent itself asks. For ACP agents, a payload longer than the console shows is marked as cut and never offers a session-wide grant. Pi's write, edit, shell and git write requests are relayed the same way and can be answered `Always allow <tool> until Pi restarts`: later calls of that tool by the same Pi start skip the prompt but stay inside the path guard and sandbox. The grant covers every later call of that tool whatever its payload, is kept in memory only, cannot be given from the dashboard, and each call it allows is logged by tool name. A Pi or `local` request whose call was cancelled or whose turn ended is withdrawn at once: a later answer is refused and grants nothing, an `Always allow` included. One shell command runs without a hub approval: the `!command` a person types in the managed Pi TUI. It stays inside the same sandbox and denylist, and the hub takes it only from the verified owner process of a Pi in TUI mode, while Pi is idle, on a reservation that is used once; a model's own bash call never takes that path (issue #253). `--unattended` turns prompts off, says so loudly, and is never the default; a classed call from a hub-owned ACP peer, the local worker or Pi still waits for a person under it.
|
|
10
|
-
- **A committed config cannot choose launch commands, credential files, data endpoints, permission modes or a wider sandbox.** The machine-local fields (`kimi_cmd`, `permission_modes`, `codex_bin`, `pi.cmd`, `checks`, `mlx.bin`, `mlx.runtimeDir`, `mlx.modelPath`, `omniroute.urls`, `omniroute.access_hosts`, the `omniroute` key files, `memory.worker_url` (still accepted, ignored since 0.16.0), `local.read_allow`, `local.bash_network`, `local.network_allow`, `terminal.open`, `instances`, `action_policy`) apply only from a config file git confirms nobody committed (`instances` and `action_policy` are read only from an untracked `.agenthub/config.local.json`; in `config.json` they are ignored even when untracked): `.agenthub/config.json` or `.agenthub/config.local.json`, matched by file identity so no other spelling the file system accepts slips past, and `.agenthub` itself not a committed symlink or submodule. Without a repository, or when git fails, they keep their defaults; an empty value always means the default. "Untracked" is answered by the repository that contains the project: a checkout copied or extracted into an unrelated repository, or into an ignored directory of one, is trusted like your own files. So a cloned repository cannot choose a launch command, a completion check, a gateway to send a key file to, a memory endpoint, a permission mode, the command that opens a terminal, or a wider sandbox. A command in `checks` runs as you, outside the local worker's sandbox, like a git hook. So do the `setup` and `verify` commands of a benchmark suite (issue #251): `ahub bench run <suite.json>` is a person's command, refused from an agent session, and it resets the work tree for each attempt, so it runs only with `bench.enabled`, at the root of a clean repository that tracks nothing under `.agenthub/`. Nothing in `routing.toml` or in task text is ever run. `routing.toml` and the other shared fields still come from the checkout, and they matter: `routing.toml` picks the models the local worker and the hub's inference use at your gateway and can turn the PII constraint off, and roles and budget shape who does what. Review them in a repository you do not trust. The hub's own git calls outside the sandbox (snapshots, facts, config trust, project discovery, memory recall, the bench runner) all use the single hardened implementation in the exact-pinned first-party `@staix/agent-mem/platform` package, re-exported by `src/hub/git.ts`, which disables the programs a repository's configuration can name — the file-system monitor, hooks, external diff, the pager, and the credential and askpass helpers, with prompts failing closed — so a config that ever does get written cannot run code with the hub's rights. Of those, the monitor, hooks and external diff are reachable by commands the hub issues (the planted-program test exercises all three; a `diff --no-index` reads the repo's config, so that call also passes `--no-ext-diff` and `--no-textconv`); the pager is not (every call pipes stdout). Credential and askpass helpers are disabled for project repositories; memory sync alone may use credentials for its own memory repository, without prompts. The cost: `core.fsmonitor=false` also turns off a person's enabled built-in monitor for the hub's own calls, so the per-turn snapshot's `add -A` scans the whole tree (the snapshot Git calls of one burst share a 250 ms budget, each call at most 10 s; a boundary that runs out is recorded as unknown, never as a stale baseline) (#576). git has no global switch for filter drivers, and of the hub's commands only the snapshot's `add`/`restore` would run one (its driver command lives in the protected `.git/config`).
|
|
9
|
+
- **Permission prompts stay on unless an operator selects a permission mode.** `ask` leaves native decisions unchanged. `ask-when-needed` grants scoped Claude file tools or Pi/local writes and edits, and a headless Claude implementer's proven shell commands through the host wrapper (#845, below); Kimi and Codex use their own native policies. `never-ask` requires explicit runtime confirmation or a person's console `y` for a config default at hub start. Until that config confirmation the peer runs in `ask`. Kimi's and `local`'s permission requests are relayed to the console and cancelled after `approvals.timeout_s` (default 120 s) of silence; the macOS notification for a waiting request carries the peer and the tool name only. The one exception is the hub's own tools (`hub_send` and the task tools, matched by exact name): Kimi's requests for them are approved once without a prompt and logged by name, the same trust Codex gets through `approval_mode` in the hub's config. They invoke hub-owned operations rather than arbitrary file or shell tools, and every call passes the hub's own checks. A call the always-ask policy classes (below) is never approved this way, whatever its title. Identity comes from the exact permission title, or from the earlier tool-call title bound to the same call id and resolved against the configured MCP servers when the permission title contains argument JSON (Qwen). Payload text and unrelated display titles never establish identity. Configured ACP kinds (#451) never get this exception: a request whose title spells a hub tool exactly still goes to the person approval path, and only a mapped native mode changes what the agent itself asks. For ACP agents, a payload longer than the console shows is marked as cut and never offers a session-wide grant. Pi's write, edit, shell and git write requests are relayed the same way and can be answered `Always allow <tool> until Pi restarts`: later calls of that tool by the same Pi start skip the prompt but stay inside the path guard and sandbox. The grant covers every later call of that tool whatever its payload, is kept in memory only, cannot be given from the dashboard, and each call it allows is logged by tool name. A Pi or `local` request whose call was cancelled or whose turn ended is withdrawn at once: a later answer is refused and grants nothing, an `Always allow` included. One shell command runs without a hub approval: the `!command` a person types in the managed Pi TUI. It stays inside the same sandbox and denylist, and the hub takes it only from the verified owner process of a Pi in TUI mode, while Pi is idle, on a reservation that is used once; a model's own bash call never takes that path (issue #253). `--unattended` turns prompts off, says so loudly, and is never the default; a classed call from a hub-owned ACP peer, the local worker or Pi still waits for a person under it.
|
|
10
|
+
- **A committed config cannot choose launch commands, credential files, data endpoints, permission modes or a wider sandbox.** The machine-local fields (`kimi_cmd`, `permission_modes`, `codex_bin`, `pi.cmd`, `checks`, `mlx.bin`, `mlx.runtimeDir`, `mlx.modelPath`, `omniroute.urls`, `omniroute.access_hosts`, the `omniroute` key files, `memory.worker_url` (still accepted, ignored since 0.16.0), `local.read_allow`, `local.bash_network`, `local.network_allow`, `terminal.open`, `tasks.auto_pull`, `instances`, `action_policy`) apply only from a config file git confirms nobody committed (`instances` and `action_policy` are read only from an untracked `.agenthub/config.local.json`; in `config.json` they are ignored even when untracked): `.agenthub/config.json` or `.agenthub/config.local.json`, matched by file identity so no other spelling the file system accepts slips past, and `.agenthub` itself not a committed symlink or submodule. Without a repository, or when git fails, they keep their defaults; an empty value always means the default. "Untracked" is answered by the repository that contains the project: a checkout copied or extracted into an unrelated repository, or into an ignored directory of one, is trusted like your own files. So a cloned repository cannot choose a launch command, a completion check, a gateway to send a key file to, a memory endpoint, a permission mode, the command that opens a terminal, or a wider sandbox. A command in `checks` runs as you, outside the local worker's sandbox, like a git hook. So do the `setup` and `verify` commands of a benchmark suite (issue #251): `ahub bench run <suite.json>` is a person's command, refused from an agent session, and it resets the work tree for each attempt, so it runs only with `bench.enabled`, at the root of a clean repository that tracks nothing under `.agenthub/`. Nothing in `routing.toml` or in task text is ever run. `routing.toml` and the other shared fields still come from the checkout, and they matter: `routing.toml` picks the models the local worker and the hub's inference use at your gateway and can turn the PII constraint off, and roles and budget shape who does what. Review them in a repository you do not trust. The hub's own git calls outside the sandbox (snapshots, facts, config trust, project discovery, memory recall, the bench runner) all use the single hardened implementation in the exact-pinned first-party `@staix/agent-mem/platform` package, re-exported by `src/hub/git.ts`, which disables the programs a repository's configuration can name — the file-system monitor, hooks, external diff, the pager, and the credential and askpass helpers, with prompts failing closed — so a config that ever does get written cannot run code with the hub's rights. Of those, the monitor, hooks and external diff are reachable by commands the hub issues (the planted-program test exercises all three; a `diff --no-index` reads the repo's config, so that call also passes `--no-ext-diff` and `--no-textconv`); the pager is not (every call pipes stdout). Credential and askpass helpers are disabled for project repositories; memory sync alone may use credentials for its own memory repository, without prompts. The cost: `core.fsmonitor=false` also turns off a person's enabled built-in monitor for the hub's own calls, so the per-turn snapshot's `add -A` scans the whole tree (the snapshot Git calls of one burst share a 250 ms budget, each call at most 10 s; a boundary that runs out is recorded as unknown, never as a stale baseline) (#576). git has no global switch for filter drivers, and of the hub's commands only the snapshot's `add`/`restore` would run one (its driver command lives in the protected `.git/config`).
|
|
11
11
|
- **Telemetry holds no bodies.** `.agenthub/state/events.jsonl` (issue #40) records envelope ids, routing and sizes, task ids and states, overlapping paths and token counts, and the task id (with a PII flag) that each usage and token record is attributed to. It never records a message body, a task title or detail, and marks private (PII) envelopes and tasks as such. Budget pauses, resumes and quota resets carry the peer, window, times and the hub's own reason text, and a person's release authorization its commit, version and docs digest. The usage roll-up (#683) folds the usage, token and limit records into `hub.db` and joins issue, PR and release from the board when read; `ahub usage` and the console's Usage panel answer only the console and a configured planner, and a planner not during a private turn. Kimi's token records (#285) are counted from its own session wire log under its home: only the four token integers, the model label and the row time are copied, and the record id is an opaque hash, never a path, session id or text. The log stays on the machine; `ahub export` only prints it. Two opt-in stores copy numbers from it into your home directory, shared by every project on the machine (directories mode 700, files 600): research records (issue #247, `research.enabled`) in `~/.agenthub/research/` hold per approved task the ids, states, counts, tokens, times and PII flag the events hold, a person's label from a closed list, and a hash in place of the project id; benchmark runs (issue #251) in `~/.agenthub/bench/` hold suite name and hash, arm label, the attached peers' ids, models and permission modes, task ids, outcomes, exit codes, counts, tokens and times, never suite text or a command's output.
|
|
12
12
|
- **Snapshots stay in your repository.** Per-turn snapshots (issue #33) are git objects in the project's own object store, written through a temporary index; nothing is referenced, pushed or copied elsewhere, and `git gc` prunes them. They hold what the work tree held, including untracked files that are not ignored, so keep secrets in ignored files. They carry the repository's own permissions, and nothing caps their disk use but `git gc`. A turn of a peer holding an open PII task is not snapshotted; a PII file left in the project is snapshotted by later turns like any other file. `ahub undo` restores only files whose current content is exactly what the turn left.
|
|
13
13
|
- **The edit hook reads, never decides.** `ahub check-path --hook` (issue #32) reads hub.db and returns context for Claude and a line for you; it sets no permission decision, so your permission rules stay in charge. It names other owners' task ids, titles and states, which then reach Claude's model; PII tasks are left out.
|
|
14
14
|
- **The session record holds identities only.** `.agenthub/state/sessions.json` (issue #37, mode 600) keeps each attached peer's recovery metadata: launch options, session and thread ids, Pi's session file path. No message or task text; loss notices name deliveries by id, sender and public task title. The Pi continuity bookmark that recovery keeps across stop and archive (`.agenthub/state/recovery-continuity.json`, mode 600, at most 16 KiB) holds identities only too: operation and project ids, the project root, and Pi's session id, session file, backend and model name; a bookmark that is not a private regular file, is larger than that or names another project or operation reads as invalid, which is kept distinct from no bookmark.
|
|
15
15
|
- **Context checkpoints have a separate text record.** Native occupancy readings carry numeric metadata only. An optional context checkpoint persists its summary in `.agenthub/state/context-checkpoint-<peer>.json` with mode 0600 and, when memory is enabled, proposes it as a machine-local memory candidate that only a person can accept. Completion requires the outstanding request id, current peer/native session and transport generation; new connection claims invalidate old requests. Private turns, every open PII task associated with the peer as owner or reviewer (including pending review), and PII-pattern summaries are refused before persistence or memory. Its text is never broadcast, and context pressure does not pause, reassign or replace the session.
|
|
16
|
-
- **The local worker is boxed in.** Paths are resolved through symlinks and must stay inside the project; a secrets denylist (`.env*`, keys, credential files, the hub's own state and the state archives `ahub reset --all` leaves) applies to its file tools, its git arguments and its memory capture alike; `.git` and `.agenthub` are not writable. Writes, edits, shell commands and mutating git wait for approval unless the operator's permission mode grants them (writes and edits inside the grant filter in `ask-when-needed`, all four in `never-ask`; see below), and the approver sees what will be written or run, with control characters escaped. Everything it executes runs under the macOS sandbox, attended or not. Since 0.10 the profile starts from deny default (issue #39): commands run and read only the system, toolchain and project directories (and the project's git dir and the selected developer dir; with network on, the public CA bundles); no writes outside the project, its git dir and a temp dir of its own (`TMPDIR`, made for each command and removed when it ends; issue #63); no `.git` name at any depth, closing the creation, rename, symlink and gitfile routes to a planted repository, no `config` or `hooks` directly under one nor
|
|
16
|
+
- **The local worker is boxed in.** Paths are resolved through symlinks and must stay inside the project; a secrets denylist (`.env*`, keys, credential files, the hub's own state and the state archives `ahub reset --all` leaves) applies to its file tools, its git arguments and its memory capture alike; `.git` and `.agenthub` are not writable. Writes, edits, shell commands and mutating git wait for approval unless the operator's permission mode grants them (writes and edits inside the grant filter in `ask-when-needed`, all four in `never-ask`; see below), and the approver sees what will be written or run, with control characters escaped. Everything it executes runs under the macOS sandbox, attended or not. Since 0.10 the profile starts from deny default (issue #39): commands run and read only the system, toolchain and project directories (and the project's git dir and the selected developer dir; with network on, the public CA bundles); no writes outside the project, its git dir and a temp dir of its own (`TMPDIR`, made for each command and removed when it ends; issue #63); no `.git` name at any depth, closing the creation, rename, symlink and gitfile routes to a planted repository, no `config`, `config.worktree` or `hooks` directly under one, nor in any submodule git directory (every modules tree the project can reach is covered: the common dir's, each linked worktree's own under `<common>/worktrees/<wt>/modules/`, matched by pattern so a tree or worktree that does not exist yet is covered too, and each discovered submodule git directory's own `modules/`, nested submodules and a submodule's own linked worktrees included; under a modules tree every write that is not inside a discovered submodule git directory is refused, whatever its name, so a fake git directory cannot be planted for a later unsandboxed `git submodule update --init` to reuse; inside each discovered one its own `config`, `config.worktree` and `hooks` and its linked worktrees' `config.worktree` are refused, while refs, logs, objects, the index and `HEAD` stay writable, so commit, branch, tag, gc and rebase work; a linked worktree's git directory under `<common>/worktrees/` gets the same `config`, `config.worktree` and `hooks` refusal; every term is anchored at the discovered path, `hooks` by depth, so a directory named `config` or `hooks` above the project neither disables the rule nor trips it; the discovered git directories, the modules roots, the worktrees container, each worktree git directory and every component in between are also refused as targets, closing the rename, replace and symlink routes to the same files), no `commondir` at any depth below one, with every such name matched folded against the code points HFS+ ignores (the fold covers the security-relevant names; on a volume that folds names, folded spellings of the components between `.git` and a submodule's git directory are not matched — the 1024-byte SBPL literal cannot hold a folded path — the same residual the external git dir rules carry since #270), and no `.agenthub` anywhere in the project (in an external git dir the `config`/`config.worktree`/`hooks` rules cover only its own children and the hooks directory's contents, so refs named `config` or `hooks` still work); a nested repository in bare layout stays open — the directory is the git dir, with no `.git` name to anchor a rule on, and the hub never runs git there, whatever the project root is: its calls are all `-C <project root>` or `--no-index` on temp files, and none descends into a nested directory's git layout, so a planted hook fires only if a person or another tool runs git in that directory themselves; the rules are probed on HFS+, which folds the 16 code points the patterns tolerate, and on a case-sensitive APFS image, which folds nothing (a folded spelling is an inert new file there and the real names stay refused); the shared object store takes only new loose objects (#864): `objects/info` (alternates, http-alternates, commit-graph) and `objects/pack` are refused, and so are the `objects`, `objects/info` and `objects/pack` entries, moving a fan-out directory, and changing, removing or replacing an existing loose object, because git reads packs first and never rehashes what it reads, so a planted pack or alternate would change what every checkout and the implementer's host add, commit and push (#845) see; a hardlink to an existing object is refused as well; an object id that does not exist yet can still be planted before git writes it (its exact content must be predicted), which a local checkout then shows, while a push of it is rejected by the receiving side's rehash; the cost is that `git init`, `git clone` and `git worktree add` do not run inside the sandbox (add, commit, stash and checkout still do; `git gc`, `repack`, `fetch` and `maintenance` run outside it, #864), `git submodule update --init` does not either (its git directory's `config` write is refused), and neither do `git worktree remove`, `git worktree prune` of a stale record, `git clean -ffd` in a project with a nested repository, or `rm -rf` of a directory that holds a `.git` (refusing the unlink is what stops the rename-out route); no network, loopback included, unless `local.bash_network`; with it, only through the hub's egress proxy to the hosts in `local.network_allow` (issue #65), which refuses names that resolve to internal addresses (behind NAT64 with the well-known prefix, by the IPv4 address the answer carries; a network-specific prefix is not recognised), so claude-mem and the Codex app-server stay out of reach (`"direct"` opens everything, until 0.13.0). One channel stays open: `trustd`, which TLS clients need, can fetch a certificate's AIA or OCSP URL on a command's behalf, outside the proxy. The allow-default profile of 0.9 and earlier was removed in 0.12.0 (issue #83); a `local.sandbox` setting is ignored with a note. The shared temp dirs (the user's and `/private/tmp`) are closed. Without the sandbox there is no `bash` tool.
|
|
17
17
|
- **Capabilities are enforced, not suggested.** `capabilities` (issue #39) is checked by the daemon where task operations and messages arrive, so a peer cannot get round it by phrasing. A peer can never answer a permission request: the control link takes `permit` from the console role only, and a message that quotes a permit command is just text. Both bind the hub's own tool paths: a vendor agent with its own shell in the project (Codex, Claude, Kimi) can read `.agenthub/state/control-token` and connect as the console, which only the local worker's and Pi's sandbox prevents.
|
|
18
18
|
- **PII has an enforced path.** A task matching `signals.pii_patterns` goes to `local` or to nobody; its text is absent from other peers' envelopes, the console stream, the log and the board listing; the console user reviews it; `local` answers such a turn to the console only, keeps it out of its history, refuses it when the only gateway is off campus, and may not save notes or spin off tasks during it. Nothing about it enters hub-managed memory; local storage does not relax this privacy fence.
|
|
19
19
|
- **A model screen can add to the patterns (issue #198).** With `signals.pii_screen = "local"` in `routing.toml` (default `off`; it applies while `constraints.pii = "local_only"`), a newly proposed task is read by a model before the hub does anything else with it: until the verdict it is on no board, in no envelope, before no triage model, and the hub makes no brief or note call for it. The local worker's amem capture leaves its hub tool calls out (issue #230), so a proposal it makes in an ordinary turn cannot reach shared memory ahead of the verdict that way. The screen runs on the model on this machine (MLX or Ollama, loopback only) or, without one, on the gateway only while `onCampus()` holds, and the gateway call is refused once more right before transport if the gateway is behind Access; task text never goes off campus to be screened. When the on-device generation slot is taken (Pi generating, another screen), the screen goes to the campus gateway instead of waiting, and on campus a device that fails or is still loading after 60% of the deadline hands over to the gateway too; off campus it waits for the slot and the device within the deadline. With the legacy MLX provider the screen may start an `mlx_lm.server`, as Pi's relay does, and it stays running until `ahub models stop`. Its answer is `pii` or `clear` plus a category from a closed list (name, student id, phone, address, grade, health, other). Anything else, a timeout (8 s, one call and one generation slot per item), a missing or off-campus model, or text longer than 6000 UTF-8 bytes (about 2000 Hangul syllables) is `unknown`, and an unknown task is a PII task: this is the hub's one model call that fails closed, so with the screener down every new task waits for `local`. An unknown verdict has the costs of any PII task, even when a second look clears it later: opening it lifts every silent turn-free cohort for good and turns facts off while it is open, and off campus a proposal without a class is refused (triage is not asked for a PII task there), with an error that says so. Off campus with the on-device slot taken, a task stays `unknown` until it is screened again: while it is still proposed and no peer ever owned it, the hub's release timer (every 60 s, one task at a time, the least tried first, at most 10 times per task in a hub run; never text too long to read) asks again, and `clear` lifts its `pii` signal and routes it through the class peers (a named owner is not remembered; a reserved one is offered first, as always). A task `local` took keeps its PII path, also once it is declined back to nobody. A pattern match is PII without a call. The task's history records the source of its `pii` signal (`regex`, `screen` with the category, or `unknown` with the reason), an unknown verdict is a console line naming the task id only, and `events.jsonl` gets a `pii_screen` record with the item kind, label, source, category, reason and latency, never the text. A routing explanation of a draft does not call the screen and says so.
|
|
@@ -23,7 +23,7 @@ agent-hub connects agents that can each run commands. This page says what the hu
|
|
|
23
23
|
|
|
24
24
|
## Native memory authority and profile selection
|
|
25
25
|
|
|
26
|
-
The portable core is the exact-pinned first-party `@staix/agent-mem` 0.
|
|
26
|
+
The portable core is the exact-pinned first-party `@staix/agent-mem` 0.11.0 package; it does not replace the hub's caller identity, PII policy or task-write authority. Deny, hardened git, real paths and process identity are single-sourced in its Node-safe platform entry. Current project privacy is applied by the hub before promotion and again before model-facing reads. Standalone person reads are unscreened, and the standalone `--policy-free` acknowledgement grants no hub or peer authority.
|
|
27
27
|
|
|
28
28
|
`hub_memory_search`, `hub_memory_show` and `hub_memory_graph` take the project from the daemon, require `memory_read`, refuse private turns and validate UTF-8 input before logs or storage work. Complete records and graph metadata are screened before counts and truncation. Search is at most 20 lines/4 KiB (default 8); show accepts at most 8 ids and returns at most 8 KiB; graph is at most 120 nodes/240 edges/6 KiB (default 60 nodes), with no edge lacking an endpoint. Unknown, ambiguous and withheld ids share one neutral refusal. Arguments and results are excluded from hub-managed capture; this does not govern a vendor's independent transcript or memory hooks.
|
|
29
29
|
|
|
@@ -239,8 +239,11 @@ delivery holds. Through the `ahub` CLI under its own peer identity it may also
|
|
|
239
239
|
run the closed operational family of #812: `roster add|remove`, `stop <peer>`,
|
|
240
240
|
`queue show|resolve|discard`, `task cancel`, `doctor --orphans --kill`, `logs`,
|
|
241
241
|
`export` and `settings set` on twelve named live keys. Each is admitted by the
|
|
242
|
-
daemon after a recheck of the conductor role,
|
|
243
|
-
|
|
242
|
+
daemon after a recheck of the conductor role, and bounded; the effectful ones and
|
|
243
|
+
the history reads are audited as a closed label with no argument text (`queue show`
|
|
244
|
+
and a settings preview recheck the role without an audit record), a refusal of
|
|
245
|
+
that authority is audited as refused, and an ordinary operation failure keeps its
|
|
246
|
+
own cause rather than being reported as missing authority (#839): logs and export are public projections of the last
|
|
244
247
|
100 lines and 1000 events, orphan cleanup signals only a positively identified
|
|
245
248
|
hub-owned process of the selected project, and a stop keeps the existing
|
|
246
249
|
ownership and recovery guards (`peer_stop` from a non-console connection is
|
|
@@ -275,11 +278,11 @@ Please report vulnerabilities privately through GitHub's "Report a vulnerability
|
|
|
275
278
|
|
|
276
279
|
- Settings are a closed registry (`src/hub/settings.ts`). The dashboard and `ahub settings` can write only its keys, each checked against its own closed values, and only to `.agenthub/config.local.json` and `.agenthub/routing.local.toml`. A file git tracks is never written, so the shared `config.json` and `routing.toml` stay byte-identical; in a repository where git cannot answer, nothing is written either. Commands and binaries (`kimi_cmd`, `codex_bin`, `pi.cmd`, `checks`, `mlx`), credential files and gateway URLs (`omniroute`, `memory.worker_url`), the local worker's reach (`local.read_allow`, `local.bash_network`, `local.network_allow`), `bench.enabled`, `terminal` and `instances` are refused by name from every registry surface: they choose what the hub runs or where secrets and task text go. The structural editor below (#689) is the only other writer, and it keeps the executable and secret-locating keys console only.
|
|
277
280
|
- `routing.local.toml` holds `stay_switch` and, per class, `peers`, `escalate_to`, `route` (an id `routing.toml` defines) and `pi_backend`, and nothing else: targets, routes, signals and the PII constraint cannot be set through it. A write is read by the hub's own loader before it replaces the file (temp file and rename), and one previous version is kept for undo.
|
|
278
|
-
- An ordinary dashboard session (`ahub ui`) reads settings, lowers a mode to `ask`, turns `pi.auto_start`, `task_sweep.enabled` or `
|
|
281
|
+
- An ordinary dashboard session (`ahub ui`) reads settings, lowers a mode to `ask`, turns `pi.auto_start`, `task_sweep.enabled`, `task_sweep.auto_reassign` or `tasks.auto_pull` off, sets a start mode back to `tui` or a WIP limit to 0, and changes the safe switches (`research.enabled`, `approvals.notify`, `snapshots.enabled`, `coordination`). Whatever can widen what agents do without asking needs a settings session: a non-ask mode now or at hub start, routing, starting Pi with the hub, turning on automatic backlog pulls, removing a machine-local value, and undo. `ahub ui --settings` opens one: its ticket marks the session for 15 minutes, after which it is an ordinary session. `never-ask` also needs the peer's id typed as the confirmation, and a stored never-ask default still waits for the console's `y` at each hub start. An undo never puts never-ask back, and removing a machine-local value that uncovers a never-ask in the shared file needs the same confirmation. "Lowering to `ask`" means handing the decision back to the agent's own rules: for Codex that is the approval policy the person configured natively, which the hub does not judge.
|
|
279
282
|
- A settings session is as strong as the console for these settings and no stronger elsewhere. A mode change is the console's own request with the same refusals; tool approvals stay deny-only for Pi and local; there is still no shell, file, task-detail or launch-command API. `ahub ui --settings` and `ahub settings` pass the same identity gate as `ahub permission`, which is an operating policy, not authentication. The hostile-agent boundary above applies unchanged: an agent with an unrestricted shell as the same user can impersonate the console, and so can open a settings session.
|
|
280
283
|
- Every change writes a `settings` event (the key, the value before and after, the source `dashboard` or `terminal`) and a line in `hub.log` and open consoles; opening a settings session is logged too. The unified dashboard (`ahub ui --all`) forwards settings actions as an ordinary session and opens no settings session.
|
|
281
284
|
- Configuration reload (issue #439). The daemon watches `config.json`, `config.local.json`, `routing.toml` and `routing.local.toml` and re-reads the complete candidate after a change, as the dashboard's Reload configuration and `ahub settings reload` do; an invalid candidate applies nothing. Each changed key is then authorized on its own against its file's actual git provenance. A privacy tightening (local-only PII, the model screen on, added PII patterns, narrower roles or capability lists, unless it moves the planner or conductor) and a safe registered setting apply from any file. Every other change, machine-local fields included, keeps its running value when its file is tracked or git cannot answer. From an untracked file such a guarded change is adopted only by `ahub settings reload` in an interactive terminal, after it lists each guarded key with its full before and after values and the person types `y` within 30 seconds; the confirmation is bound to the file contents and accepted state it was shown for. File watches and dashboard reloads, a settings session's included, never confirm, and no reload changes the roster: membership moves only through the roster setting. Keys whose consumers are bound at start (memory, the gateway and MLX, checks, the privacy and sandbox policy of existing sessions, the terminal provider and others) stay pending until a restart.
|
|
282
|
-
- Structural configuration (issue #689). A second, wider list than the registry can be edited, in three tiers (`src/hub/config-tier.ts` is the one rule). Tier 2 (instance fields that choose no executable or home, the review, snapshots, approvals and `context.gate` keys, and `sdlc.toml` `mode` and `required_checks`) is editable from the console and from a settings session. Tier 3 is everything that chooses what the hub executes or where secrets go (the rest of `NEVER_EDITABLE`, and an instance's `jcode_home`, `provider`, `model` and `provider_profile`): console only, confirmed by typing the key's name, one key per write, never by `--yes`, and the dashboard shows it as text with the console command. A browser page must never be able to set what the hub runs, so the refusal is in the hub, not in the page: `config_edit_preview` and `config_edit_apply` from a dashboard session refuse a tier 3 path whatever else the request carries, and an ordinary session is refused before anything about the project is read. Reload authority admits a structural key from a settings session only while the daemon's own validated edit holds that key's scope (`structural` in `authorizeConfigLeaf`) and `structuralLeaf` says tier 2; a file edit made by hand still needs the terminal's confirmed reload. The candidate is read by the same loaders the daemon starts with before anything is written, the file revision is checked twice (planning and writing), the previous file is kept as one of the newest five backups, a symlinked, oversized or git-tracked file is never written, and unknown request fields and keys are refused. One write changes exactly one tier 3 key, counting the keys the plan adds from what the file holds (`plan.tier3Keys`), not only the ones the request names, and the audit record carries no hash of a tier 3 value. A whole instance that holds a tier 3 field is tier 3 to remove or replace (decided from what the file holds, not from the request), a preview or write reply masks every tier 3 value as `(set)`, and `.agenthub` itself must be a real directory. The `config_edit` event and the notice carry key paths and hashes, never a value, so a tier 3 key's home or model never reaches the event log, `hub.log` or the dashboard. Agent shells and hub tools cannot reach any of it: the CLI is `console` in `classifyPeerCommand` and the control requests answer the console role only.
|
|
285
|
+
- Structural configuration (issue #689). A second, wider list than the registry can be edited, in three tiers (`src/hub/config-tier.ts` is the one rule). Tier 2 (instance fields that choose no executable or home, the review, snapshots, approvals and `context.gate` keys, and `sdlc.toml` `mode` and `required_checks`) is editable from the console and from a settings session. Tier 3 is everything that chooses what the hub executes or where secrets go (the rest of `NEVER_EDITABLE`, and an instance's `jcode_home`, `provider`, `model` and `provider_profile`), plus a fresh reviewer's `review_scope`, which decides whether its approval covers tests and CI (#852): console only, confirmed by typing the key's name, one key per write, never by `--yes`, and the dashboard shows it as text with the console command. A browser page must never be able to set what the hub runs, so the refusal is in the hub, not in the page: `config_edit_preview` and `config_edit_apply` from a dashboard session refuse a tier 3 path whatever else the request carries, and an ordinary session is refused before anything about the project is read. Reload authority admits a structural key from a settings session only while the daemon's own validated edit holds that key's scope (`structural` in `authorizeConfigLeaf`) and `structuralLeaf` says tier 2; a file edit made by hand still needs the terminal's confirmed reload. The candidate is read by the same loaders the daemon starts with before anything is written, the file revision is checked twice (planning and writing), the previous file is kept as one of the newest five backups, a symlinked, oversized or git-tracked file is never written, and unknown request fields and keys are refused. One write changes exactly one tier 3 key, counting the keys the plan adds from what the file holds (`plan.tier3Keys`), not only the ones the request names, and the audit record carries no hash of a tier 3 value. A whole instance that holds a tier 3 field is tier 3 to remove or replace (decided from what the file holds, not from the request), a preview or write reply masks every tier 3 value as `(set)`, and `.agenthub` itself must be a real directory. The `config_edit` event and the notice carry key paths and hashes, never a value, so a tier 3 key's home or model never reaches the event log, `hub.log` or the dashboard. Agent shells and hub tools cannot reach any of it: the CLI is `console` in `classifyPeerCommand` and the control requests answer the console role only.
|
|
283
286
|
- Start modes. A start the hub makes itself (the dashboard's Start control, `peer_start` from a console child, the conductor's `hub_peer_start`, `pi.auto_start`) takes the peer's start mode. Through settings only a settings session or the terminal can set it to headless; a config file can also set `peers.<id>.start_mode`, the shared one included (it is not a machine-local field: headless was the only form for Pi, Kimi and the local worker before this change, and for Jcode before 0.12.39, a hub-made headless Codex start is the app-server the hub already ran for it, and a start mode grants nothing a permission mode or a capability controls). A declared extra instance accepts only `headless` from any surface; a Claude channel instance accepts no start mode (one that implements or verifies needs its own worktree, and a planner or reviewer one refuses a worktree, #525-1), and a headless Claude instance is either a reviewer (roles exactly `["reviewer"]`, no worktree) or an implementer with its own registered worktree and permission mode `ask` or `ask-when-needed` (#525-4). For a TUI the hub opens a terminal running `<runtime> <its own entry point> --project <this project> <peer>` (with `--mode tui` for Pi, Kimi and Jcode): every word is the hub's own, the command line is those words each quoted for a POSIX shell, and no request field, tool argument or agent message is part of it. In a `terminal.open` template `{command}` is that quoted line and `{title}` and `{cwd}` are raw text for argv elements of their own; each placeholder is substituted once, so a project path that itself contains a placeholder, a quote or `$` stays one word. A provider's own error text goes to `hub.log`, not to the caller. The terminal comes from `terminal.open`, a machine-local command template that config trust refuses from a committed file and that no settings surface can write, or from Orca. The opened terminal's environment carries no agent marker, so `ahub` there has the console's identity. Without a provider the start is refused with the command to run by hand; it is never downgraded to headless. After a successful headless start of a watch-capable peer (Kimi, Qwen) the hub also opens `<runtime> <its own entry point> --project <this project> watch <peer>` through the same provider under the same fixed-words rule, at most once per peer per 30 s and never for an already attached peer or a declared instance; the pane has the console's identity, and a missing or failing provider leaves the start standing and reports the command (#286). A conductor can start a peer but cannot choose its mode.
|
|
284
287
|
|
|
285
288
|
## Multi-project manager (protocol 7)
|
|
@@ -345,18 +348,18 @@ live after a settings write, undo or reload (turning it off from an edited file
|
|
|
345
348
|
be set through an agent, tools or conductor surface. Claude's daemon response
|
|
346
349
|
withholds the scoped mode on these writes so an older hook grants nothing too.
|
|
347
350
|
|
|
348
|
-
Agent CLI versions (#691). One policy gates the Codex and Kimi fresh reviewers, Jcode, agy and
|
|
351
|
+
Agent CLI versions (#691). One policy gates the Codex and Kimi fresh reviewers, the headless Claude implementer, Jcode, agy, Qwen (ACP and the dual-output TUI) and Pi (#843): a receipt-verified version passes, Jcode and agy also start an untested version inside their compatible line with a note, and any other version is refused unless the person trusts that exact version on this machine with `ahub agents trust <id> <version>`. The record is `agent-compat.json` in the hub home (`~/.agenthub`), written atomically with mode 600 and at most 64 entries; an unreadable, linked, oversized or invalid record trusts nothing. Agent shells and `--live-test` may read `ahub agents` but cannot trust or revoke. Trust lifts only the version pin: the fresh reviewers' runtime proofs still run before every child, and an untested version's native contract stays unverified.
|
|
349
352
|
|
|
350
|
-
Qwen Code's optional production ACP peer requires the qwen-code native identity, a compat-admitted version (receipt-verified 0.25.0 and 0.26.
|
|
353
|
+
Qwen Code's optional production ACP peer requires the qwen-code native identity, a compat-admitted version (receipt-verified 0.25.0, 0.26.0 and 0.26.1)
|
|
351
354
|
and advertised mode ids before setting a policy or starting work. Its own tools
|
|
352
355
|
have no hub sandbox; auto-edit approves file edits, yolo every tool. Configured
|
|
353
356
|
never-ask still requires the person's confirmation. qwen_cmd is machine-local and
|
|
354
357
|
never editable through settings. Legacy benchmark adapters remain unmanaged by
|
|
355
358
|
this production table. Startup diagnostics withhold native host/payload details.
|
|
356
359
|
|
|
357
|
-
Jcode is optional, source/version-fenced and runs native tools without a hub sandbox or hub permission-mode mapping. .jcode joins the scoped-grant exclusion, including lexical/resolved aliases. Only a person writes its unique project MCP entry; foreign/override/symlink entries refuse. The hub owns a private daemon/socket and ACP group, never the global daemon, and copies no credentials. It accepts a plain jcode release from 0.98.0 up to, not including, 1.0.0 (#529; only 0.98.1 and 0.99.
|
|
360
|
+
Jcode is optional, source/version-fenced and runs native tools without a hub sandbox or hub permission-mode mapping. .jcode joins the scoped-grant exclusion, including lexical/resolved aliases. Only a person writes its unique project MCP entry; foreign/override/symlink entries refuse. The hub owns a private daemon/socket and ACP group, never the global daemon, and copies no credentials. It accepts a plain jcode release from 0.98.0 up to, not including, 1.0.0 (#529; only 0.98.1, 0.99.0 and 0.99.1 are verified, and another version in that line starts with a note), or an exact version the person trusts (#691), and runs every owned jcode process (probe, serve, ACP and TUI) with `JCODE_NO_TELEMETRY=1`, because jcode's telemetry can report the provider account's email. Native diagnostics withhold payloads/hosts. Hub tools are bound to an owned launch (#368): the shared project entry names no peer, each owned Jcode process carries its own peer id and launch id, the daemon admits a hub-tools connection for a Jcode peer only with the id of that peer's current owned launch, and a tools-mode server started without a peer id refuses every tool. Extra Jcode instances (#685, at most 8) are declared only through `instances` in an untracked `.agenthub/config.local.json`, start headless, and each needs its own `jcode_home`, which may not be the base home or another instance's, nor nested in or around one, however it is spelled. Unproved conversation loads refuse; no fresh replacement is called a restore. A finished task's advisory about the project root checkout names the branch only after the hub's whole public-text screen (`Tasks.publicMetadataText`: a PII pattern, a secret shape and a project-private path) passes it and it is a plain ref name; otherwise a generic sentence carries no part of it (#572). Native TUI launch exposes its private socket/session tuple only to the authenticated console, bound to the same live owned socket and session after read-only native persistence proof. Tools/status do not receive it. The TUI has no single-writer guard: person input may interleave with ACP. Native policy remains unmanaged and closing the frontend does not stop ACP. An `important` message reaches a busy Jcode as a soft interrupt written to the hub's own private server socket for the owned session (#572); the hub never cancels a turn to deliver, and a steer that was sent but not acknowledged, or whose turn ended first, is held as `needs_review`, never replayed. Jcode has no switch to choose its MCP servers, so the owned serve and ACP processes load every server named in `<JCODE_HOME>/mcp.json` and the project's `.jcode/mcp.json`, `.mcp.json` and `.claude/mcp.json`, besides the hub's own; `ahub doctor` lists their names only. Point `jcode_home` at a separate home, or remove a server from those files, to keep it out.
|
|
358
361
|
|
|
359
|
-
Antigravity (`agy`, #485) is optional, cloud and off roster by default. The launcher accepts the Antigravity CLI 1.4.x line; only 1.4.2
|
|
362
|
+
Antigravity (`agy`, #485) is optional, cloud and off roster by default. The launcher accepts the Antigravity CLI 1.4.x line; only 1.4.2 and 1.4.7 have a native receipt ([1.4.7](verification/2026-10-26-agy-1.4.7.md)), so another 1.4.x patch launches with a warning that it is unverified, and every other version is refused before a hub connection or plugin write (#589) unless the person trusts that exact version (#691). It runs native tools under its own unmanaged permissions, without a hub sandbox or hub permission-mode mapping; headless, resume, unattended and native permission flags are refused. Its roles are capped at implementer and verifier, so it never plans, reviews or receives PII work. Only the person's `ahub agy` launcher writes its three plugin files in `.agents/plugins/agent-hub/` (`plugin.json`, `mcp_config.json`, `hooks.json`): linked or foreign entries refuse before any write, cleanup removes only that launch's matching files, and it never writes project-level `.agents/hooks.json` or the person's `~/.gemini` settings. Conductor starts are refused, and a declared agy extra instance is refused at config load (#625). Hook authority (`native_hook_launch`) comes only from that launcher, belongs to the console socket that began it and ends when that socket closes; hook input over 1 MiB is refused, and observations are bound to the daemon, launch, native session and project.
|
|
360
363
|
|
|
361
364
|
Configured ACP adapters (#451 A3) are declared only by the person, in a Git-confirmed untracked `.agenthub/config.local.json`. The whole declaration (command, argv, identity, roles and mode maps) is machine-local authority: a shared or tracked declaration is ignored with a notice, settings never edit it, and changes need a hub restart. The hub runs only the declared argv, never a module or callback. Argv that selects a leader, attach, connect or remote mode is refused, and a Grok declaration needs `--no-leader`. Each start requires the pinned native identity, an exact `session/load` echo on resume, and positive ownership of the spawned process and its original process group; unknown ownership refuses, and stop signals only that verified group. An agent that hands its work to a process outside that group is outside this boundary. Without a mode map native policy is unmanaged, and there is no hub sandbox either way. Configured kinds are cloud peers (never PII), headless only and off roster by default, and the hub makes no quota, context or controlled-recovery claims for them. A read-only instance needs the native read-only mode acknowledged before a prompt and is limited to read and review hub tools, both in the published MCP list and at the host; its native non-read tool requests are rejected, and no setting or runtime change can raise it. These agents' stderr and error text are withheld from logs, and previews redact the executable, argv and session id.
|
|
362
365
|
|
|
@@ -477,11 +480,17 @@ A declared instance whose only role is reviewer gets review requests, task messa
|
|
|
477
480
|
|
|
478
481
|
A terminal Claude implementer or verifier instance (#525-5) owns write-class tasks only after the daemon proves its execution root: the current launch id, a private single-link launch record naming the root, the cwd the channel's MCP server reports (verified model-free on Claude Code 2.1.309) and the daemon's own validation of the declared non-canonical branch worktree must all agree, and the proof belongs to the attached claim generation and socket. A detach, a replacement claim or a changed launch record revokes it. Missing, legacy or mismatched evidence still attaches the channel, but write-owner routing is refused and facts grants give nothing, never a fall-back to the canonical tree; terminal recovery revalidates the recorded root against the current declaration. This is same-user launch evidence, not filesystem isolation: Claude can still write wherever its own permissions allow.
|
|
479
482
|
|
|
480
|
-
The headless Claude implementer (#525-4) runs one owned `claude -p` child per turn in its declared worktree, with `--setting-sources ""`, `--strict-mcp-config`, native permission mode `default`, the hub's managed PreToolUse hook and `--permission-prompt-tool mcp__agent-hub__hub_permission_request`; never `--bare`, `acceptEdits`, `dontAsk` or `bypassPermissions`, and `never-ask` is refused. Each task keeps its own persisted session; unscoped chat reuses one only while exactly one task is in progress, and a `hub_inbox` read refuses a batch attributed to another task. The approval path fails closed: `hub_permission_request` is accepted only from the current child's launch and generation, goes to the console's existing approval queue and `approvals.timeout_s`, and offers allow-once and deny only, with no allow-always or session grant. A timeout, a prompt failure, a disconnect, a hub that is down or refusing, or a stale child binding answers deny; a failed hook decides nothing, so the call still waits for a person. The approval title is the tool name and a JSON rendering of the arguments passed through `sanitize()` and the public-text screen (any PII, secret or private-path match withholds the whole value as `[withheld]`, before any cut), never task text, cut at 600 characters with a `[cut, N chars]` marker; the original arguments stay with the child and are not logged. The MCP wire cannot tell a native permission dispatch from a call the model makes itself;
|
|
483
|
+
The headless Claude implementer (#525-4) runs one owned `claude -p` child per turn in its declared worktree, with `--setting-sources ""`, `--strict-mcp-config`, native permission mode `default`, the hub's managed PreToolUse hook and `--permission-prompt-tool mcp__agent-hub__hub_permission_request`; never `--bare`, `acceptEdits`, `dontAsk` or `bypassPermissions`, and `never-ask` is refused. Each task keeps its own persisted session; unscoped chat reuses one only while exactly one task is in progress, and a `hub_inbox` read refuses a batch attributed to another task. The approval path fails closed: `hub_permission_request` is accepted only from the current child's launch and generation, goes to the console's existing approval queue and `approvals.timeout_s`, and offers allow-once and deny only, with no allow-always or session grant. A timeout, a prompt failure, a disconnect, a hub that is down or refusing, or a stale child binding answers deny; a failed hook decides nothing, so the call still waits for a person. The approval title is the tool name and a JSON rendering of the arguments passed through `sanitize()` and the public-text screen (any PII, secret or private-path match withholds the whole value as `[withheld]`, before any cut), never task text, cut at 600 characters with a `[cut, N chars]` marker; the original arguments stay with the child and are not logged. The MCP wire cannot tell a native permission dispatch from a call the model makes itself; a person allow still refers only to its original arguments. Automatic Bash authorization uses the separate single-use host wrapper contract below, never a caller-supplied tool id as native provenance. A stop holds an uncertain delivery until the owned process group is confirmed gone. The [native permission receipt](verification/2026-10-23-headless-claude-permissions.md) is model-free on Claude Code 2.1.309, [repeated on 2.1.310](verification/2026-10-26-claude-implementer-2.1.310.md); it is not sandbox proof, and a real account, model and person interaction remains a live leg.
|
|
484
|
+
|
|
485
|
+
The #845 host shell wrapper is the only automatic Bash path for a headless implementer. In `ask-when-needed`, a complete public input must pass the existing #816 read-only proof or the closed task-command proof, with an active owned task and registered worktree. Enabled action-policy classes still ask, and `ask`, PII handling and the `never-ask` refusal are unchanged. The native request and authenticated host response both name wrapper protocol 1, so an older plugin cannot drop the rewrite and execute an original command automatically.
|
|
486
|
+
|
|
487
|
+
A grant uses a random, expiring single-use nonce and an in-memory record bound to task, child generation, root, branch, exact argv, executable and host environment. The fixed wrapper has no command-selection arguments. Redemption consumes the ticket before rechecking current task/child/root/branch and proof; unknown, expired, reused or mismatched tickets return a fixed nonzero refusal and execute nothing. Restart revokes tickets. The host service streams stdout/stderr and exit status from an owned command process, using the proven cwd, absolute executable and the #816 fixed environment plus proxy/peer identity and private command TMPDIR. Native `GIT_DIR`, `RIPGREP_CONFIG_PATH`, PATH and other inherited native values are not execution authority. Host audit logs include neither nonce nor command arguments. The requesting MCP call and wrapper argv necessarily see the nonce; a direct model-selected permission call receives no broader authority and can record the same branch binding after the same host proofs. Read-only and executable code grants (check/test/build) execute through the existing local-worker Seatbelt profile and own-temp rules. Code may write the worktree and owned temp, but all writes to the project's Git common/current metadata are denied, including loose and packed objects, refs, HEAD, index, logs, other worktrees and shared/private submodule metadata. It cannot poison objects that a later checkout would materialize; protected metadata directory entries cannot be renamed or replaced. Code cannot read credentials or the live hub state, even with a canonical-checkout read allowance; network follows local.bash_network, with the proxy for ordinary enabled mode and direct network access when the person explicitly selects "direct". The default branch is captured once at startup from a bounded remote symref probe against the captured GitHub repository, with private HOME/cwd and isolated config; local origin/HEAD is never authority. Unknown or unauthenticated private-remote evidence asks. Sandbox protections are rebuilt and compared at ticket redemption and immediately before spawn; a changed submodule Git-directory inventory invalidates the ticket. Without sandbox-exec these commands ask. Only closed host Git add/commit/push and gh operations run outside the profile. Filter/external-diff/textconv drivers (including global Git LFS) make only add/commit ask; push/gh do not execute those drivers and keep their grants. This deliberately preserves LFS conversion semantics on the person-approved path instead of neutralizing filters. Config includes and partial clones still refuse privileged Git/gh; host Git disables signing and automatic maintenance in addition to core hooks/fsmonitor/credential hardening. A deliberately setsid-detached target descendant can outlive owned-group cleanup and retain its confined worktree writes. This confines automatic task code, not the entire native Claude process.
|
|
488
|
+
|
|
489
|
+
Task branch authority uses recorded `refs.branch`, otherwise a host-observed non-default attached branch on the first eligible task-command request, excluding `main`, `master`, unknown/detached HEAD and another open task's branch. The binding is a normal `Tasks` write with history and an opaque canonical-root hash; a changed branch or worktree asks and never rebinds. Push grants accept only `git push origin <bound>` or `git push -u origin <bound>`; force, refspec, deletion and other remotes ask. Every origin push URL is checked against the captured project repository and PR argv pins that repository. Execution uses an explicit same-branch refspec; configured remote.origin.push/mirror refuses. Project/worktree PATH directories and aliases are removed, and gh child Git inherits core SAFE_CONFIG through fixed environment configuration. PR body-file content is bounded, screened and snapshotted into executed --body bytes. See [the design contract](specs/2026-09-19-agent-hub-design.md#headless-implementer-host-shell-execution-845) for the exact closed forms and [the native receipt](verification/2026-10-26-headless-claude-shell-context.md) for verification limits.
|
|
481
490
|
|
|
482
491
|
The fresh Codex reviewer (#519) needs `review_mode: "fresh"` and a compat-admitted Codex (receipt-verified 0.146.0, 0.163.0 and 0.164.0; `ahub agents` shows the current policy); an unverified/untrusted version is unavailable. Before each review it requires a model-free isolation receipt: binary and config unchanged, only the two hub tools in the configured MCP inventory (each approved by its own name, never by a server-wide default, #783), plugins, hooks and Apps disabled (Apps by a separate feature readback), and a named filesystem profile whose fixtures prove public reads, private-path denial (exact, glob and symlink), standard host credential denial and write denial. Credential and tool-home roots are denied even with an empty `local.deny`; that list does not cover every personal file. A dangling or unreadable credential root refuses the reviewer, and the reason names only its path class (`~/.docker`, `$DOCKER_CONFIG`), never the path or its contents. A failed per-review start names one class from a closed list (`version`, `binary`, `mcp_inventory`, `plugins_hooks_apps`, `sandbox_proof`, `credential_root`, `timeout`, `unknown`) with a fixed sentence in the unavailable reason, `hub.log` and the `reviewer` line of `ahub status`; it never carries a path, native CLI text or a token (#722). Its shell is sandboxed read-only, not Claude's Read/Grep/Glob whitelist. Network and web search are configured disabled but not observed: managed requirements can still select cached web search, and no run has shown the review selecting the profile through `default_permissions`. A failed receipt or start never falls back to a continuing native session, and a child that ends without a saved verdict is never approval. The agy reviewer slot is an unavailable placeholder and never runs; it binds nothing, so agy's own `hub_task_show` reads are not fenced as a review (#792).
|
|
483
492
|
|
|
484
|
-
The fresh Kimi reviewer (#533) needs `review_mode: "fresh"` and a compat-admitted Kimi Code (receipt-verified 2.2.0, 2.3.0 and 2.3.
|
|
493
|
+
The fresh Kimi reviewer (#533) needs `review_mode: "fresh"` and a compat-admitted Kimi Code (receipt-verified 2.2.0, 2.3.0, 2.3.1 and 2.3.2; `ahub agents` shows the current policy) with its native OAuth login; an unverified/untrusted version, or a failed preparation, is unavailable. The [2.3.0](verification/2026-10-22-kimi-qwen-compat.md) and [2.3.1](verification/2026-10-25-kimi-231-reviewer-compat.md) receipts are model-free and do not prove real login or a model review. It is a native tool-authority boundary, not a sandbox: no Seatbelt wraps it, and same-user native code is not isolated from the filesystem or network. Each review starts a new child in a private `KIMI_CODE_HOME` with managed-provider-only config, a clean temporary working directory and an empty skills directory. Its agent may use only `hub_review` and `hub_task_show`, served by a loopback MCP session bound to the task, head and review revision that never carries the hub's control token; it has no native read, file or shell tool and sees only the screened review brief and host facts. Its `credentials/` and `oauth/` directories and `device_id` link to the person's native ones, so refresh keeps Kimi's own lock and atomic rename; no token is copied and `KIMI_DISABLE_OAUTH_LOCK` is never set. A stop revokes its verdict authority before termination; killing it mid-refresh may require a native re-login. Private tasks are never given to it, and a child that ends without a saved verdict is never approval.
|
|
485
494
|
|
|
486
495
|
The review brief (#520) withholds private changes whole (both sides of a move) and counts them; a rule file that fails the private-data screen is named, not shown. Attributes and rules are read at the merge base, so a change cannot loosen its own review. Optional changed symbols (#684) come from ripwire 0.6.7 on the daemon's host PATH, run as an owned process on a disposable snapshot of the task's commit; each path, name and kind passes the hub's public metadata screen before the brief, the console's task detail or the dashboard shows it, a private task stores and shows none, and a failed or missing read is `unknown`, never a gate. Spec acceptance, plan review, plan conformance, premature-review, cost, test-integrity (#481 S4), issue and PR identity linking (#694) and dogfood scorecard (#687) reads fetch GitHub metadata only (edit times, check states, thread authorship and resolution, changed file names, open PR heads, workflow run conclusions), never issue bodies, comments or patches. Issue identity linking at propose and accept (#723) parses the task's title and refs locally, reads nothing from GitHub and skips PII tasks. The CI workflow keeps `contents: read` and `actions: read`.
|
|
487
496
|
|
|
@@ -505,7 +514,7 @@ Remote checks reuse the same request validation, exact Git snapshot, queue, auth
|
|
|
505
514
|
|
|
506
515
|
Docker checks use a non-root numeric uid, `--network none`, read-only root filesystem, home/temp/work tmpfs, all capabilities dropped, no-new-privileges and fixed pids, CPU (4) and memory (4 GiB) caps. No host path, socket, home or credential mount is allowed. A uniquely named/labeled run uses only a locally present image (`--pull never`). Removal is confirmed through project/name-filtered Docker inventory before the existing heavy lease is released. Inventory projects individual hub label fields instead of parsing Docker's comma-joined label display, and does not enumerate unrelated projects' containers. Readiness checks Docker reachability/version, the exact image digest and architecture, and a fixed hardened network-none probe that uses the same archive transfer and non-root readback as a run; any failed proof is `remote runner unavailable`, with no local fallback. Probe cleanup remains bounded and runs even after its admission signal is cancelled. Exact project/probe-UUID/name rows without a task run id are left to their fixed 600-second expiry and excluded from task-run orphan warnings. Malformed probe rows and unknown task-run owners still produce warnings; labels alone never authorize killing a probe.
|
|
507
516
|
|
|
508
|
-
|
|
517
|
+
Remote dependencies are prepared in a separate, admitted step inside the same run, never in the check (#793b). A named Docker volume holds `node_modules`; its name is `agenthub-check-deps-<project key>-<32 hex>`, the hash of the commit's `bun.lock` SHA-256, a digest of every other install input in the staged source (each `package.json`, `bunfig.toml`, `.npmrc` and `patches/` file, by path and content), the Bun version and architecture the readiness probe observed in the pinned image, and the image digest, so a different lockfile, Bun, architecture or image never shares a volume. Because a root or workspace lifecycle script (`preinstall`, `install`, `postinstall`, `prepublish`, `preprepare`, `prepare`, `postprepare`) can build `node_modules` from tracked source the key does not cover, a commit whose `package.json` declares one, or whose manifest is not valid JSON, is refused before any volume is created; the install stays exactly `bun install --frozen-lockfile` and is never weakened with `--ignore-scripts`. The volume carries `agenthub.deps.project` and `agenthub.deps.key` labels, and an existing volume is used only when both match exactly: a same-named volume with other labels is refused and never mounted or removed. A missing volume is prepared: a root, capability-dropped, network-less container only changes the new volume's ownership to uid 65532, then one container, identical to a check container apart from a writable volume mount and a `bridge` network, runs exactly `bun install --frozen-lockfile` on the commit's own source (a source-only snapshot with no Mac `node_modules`) and writes a marker holding the key. That container is the only one in the whole backend with a network, and the only one that mounts the volume writable. Every prepared volume, new or existing, is then proven by a separate network-less container that mounts it read-only, reads the marker and confirms a write fails; a volume that does not verify is removed and prepared again, never repaired in place, and a failed preparation removes its candidate volume. The check container mounts only that volume, read-only, with `--network none`, and never installs. The preparation, ownership and verification containers reuse the run's name and labels, are removed and confirmed absent through the existing inventory before the next step, and are covered by the same cancellation, deadline (the ten-minute budget includes preparation), cleanup and orphan-sweep paths and the heavy lease. A missing or unprovable volume refuses the run with `remote dependencies unavailable`, never a writable shared cache and never a local fallback. After a verified volume, the project's own volumes beyond the newest three (the current one always kept) are removed, but only those whose name matches the exact pattern, whose project label is this project and whose key label is a 64-hex key that reproduces the name; a volume with an absent, malformed or inconsistent key label, another project's volume and any other name are neither counted nor touched. Install output is not retained or shown. The backend's `root` stays reserved, validated metadata, and no Docker command uses a host path. Fake transport and argv tests establish the contract, and a local Docker run of the production transport showed one network-attached install on the first run and none on the second (see the [receipt](verification/2026-10-25-remote-docker-transport.md#dependency-volume-793b)). Actual machine opt-in, remote SSH and pi's live tools remain unverified (AC6).
|
|
509
518
|
|
|
510
519
|
Dependency copies detect observed inode, size and timestamp changes and refuse final-component symlinks using `O_NOFOLLOW`. A same-user writer can still exchange and restore a parent directory between observations; this is not an atomic immutable-dependency guarantee. Source commits remain immutable. Do not treat a dependency copy as protection against a hostile process running as the same host user.
|
|
511
520
|
|
package/docs/smoke.md
CHANGED
|
@@ -2001,13 +2001,13 @@ Complete these legs in a disposable project with authorized accounts after sourc
|
|
|
2001
2001
|
- Request a delta review and confirm the same eligible independent reviewing family receives a new session with prior findings. After an earlier same-family fallback, a newly available independent family still takes precedence. Exercise configured re-rotation separately. Stop or exhaust one reviewer and confirm the pure routing walk chooses the next eligible candidate; no failed start or child ending without a saved verdict becomes approval. Check the next candidate even when the missing verdict followed a vendor quota error; no text-based quota classifier is assumed.
|
|
2002
2002
|
- Make all pool candidates unavailable and confirm a blocked review awaiting planner/person action. Then test a different fresh session in same-family fallback only when no other family can run. Verify stale-child, changed-head and private-review verdict refusals.
|
|
2003
2003
|
- Verify compatibility separately: a Codex/Kimi headless reviewer-role declaration without `review_mode` keeps its native app-server/ACP behavior and existing worktree/permission options, and stays outside the fresh pool. An explicit fresh declaration must refuse an unsupported backend without downgrading.
|
|
2004
|
-
- The default logical pool is Claude, Codex, Kimi. Kimi is runnable for a compat-admitted version when runtime preparation passes (receipt-verified 2.2.0, 2.3.0 and 2.3.
|
|
2004
|
+
- The default logical pool is Claude, Codex, Kimi. Kimi is runnable for a compat-admitted version when runtime preparation passes (receipt-verified 2.2.0, 2.3.0, 2.3.1 and 2.3.2; inspect `ahub agents`); an unverified/untrusted version or failed preparation is unavailable with its reason. Seed modulo includes only kinds with a declared candidate, even when blocked; undeclared kinds remain trailing trace entries. Verify seeds 0..5 with only Claude/Codex declared alternate 3/3, and an offline declared kind still retains its slot. Agy stays unavailable as a reviewer with exact reason `agy is an implementer peer (#485)`, including explicit pool requests. #234 adds no agy reviewer launch or live leg.
|
|
2005
2005
|
|
|
2006
2006
|
Record source head, exact executable version/hash, isolation receipt, task/head identities, route walk, distinct native session evidence and persisted verdict separately. Do not record credentials or raw transport environments. Codex sandboxed shell must not be described as Claude's Read/Grep/Glob-only whitelist. Network policy is configured disabled; no network-denial probe or native LLM isolation result has been recorded by this investigation.
|
|
2007
2007
|
|
|
2008
2008
|
## Kimi reviewer and task-bound MCP (#533)
|
|
2009
2009
|
|
|
2010
|
-
Kimi receipt-verified 2.2.0, 2.3.0 and 2.3.
|
|
2010
|
+
Kimi receipt-verified 2.2.0, 2.3.0, 2.3.1 and 2.3.2 are available when runtime preparation passes; a failed or unsupported preparation remains unavailable. The [2.3.0](verification/2026-10-22-kimi-qwen-compat.md) and [2.3.1](verification/2026-10-25-kimi-231-reviewer-compat.md) and [2.3.2](verification/2026-10-26-kimi-232-reviewer-compat.md) receipts are model-free; the following authenticated evidence belongs to 2.2.0. Completed evidence includes account-free two-tool/native OAuth race checks, host/fake lifecycle refusals and authenticated fresh `changes_requested` verdicts. The complete-collector live run observed project parity/canary absence, authenticated exact-two hub inventory/calls, zero decoy requests, cleanup and unchanged real config hashes. Its original aggregate receipt remains failed because of an unattributed extra observer error; the later account-free GET reproduction/correction is not a new live pass. These separate outcomes are preserved in the R0/live report. The remaining actual interactive-TUI/coexistence and usage-observation checks are **UNVERIFIED**; they do not turn completed live/fake evidence into an unrun test. Person ruling 2026-10-19 (8281ec35) limits #234 to Kimi and task-bound MCP and supersedes all earlier agy reviewer implementation/admission/live-leg rulings. Agy is an implementer under #485; its retained reviewer slot remains unavailable with exact reason `agy is an implementer peer (#485)`. Earlier agy probes and unresolved meta-tool/resource findings remain historical in the [R0 report](verification/2026-10-19-kimi-agy-reviewer-isolation.md), not active smoke requirements here. Kimi's native-permission/shared-account ruling supersedes its earlier R0-only hold and Seatbelt requirement. The [spec amendment](specs/2026-09-19-agent-hub-design.md#kimi-reviewer-and-task-bound-mcp-amendment-533-hub-task-234) defines the active contract and residuals.
|
|
2011
2011
|
|
|
2012
2012
|
- For Kimi regression smoke, confirm a fresh child per review, no resume, source evidence supplied only by screened `hub_task_show`/review brief and host facts, and no native Read/file/shell tool. Confirm the in-process host MCP exposes only the task-bound `hub_review` and `hub_task_show` capability, without the full hub control token. Exercise wrong task/head/review revision, stale/replaced child, private review, malformed arguments, unknown tool and stopped-child refusals; verify no saved approval on any failure.
|
|
2013
2013
|
- For each compat-admitted Kimi version, confirm no Seatbelt wrapper, a private `KIMI_CODE_HOME` with managed-provider-only config, clean temporary cwd and explicit empty skills directory. Verify exactly the two `mcp__agent-hub__...` tools in model disclosure and invocation; reject native read/write/command and extra MCP calls. Confirm `credentials/` and `oauth/` are directory symlinks to the native locations, `device_id` links the native identity, no token copy/extraction/static provider is used, and `KIMI_DISABLE_OAUTH_LOCK` is absent. The concurrent account-free OAuth print stand-in proved two expired generations serialize refresh through shared directory links, but it was not an attached interactive TUI. Actual TUI refresh/atomic rename/lock coordination and shared-account quota/pause fanout remain separate live legs. Existing project MCP/config byte-parity observations remain recorded in the R0/live report. Record identities/metadata without token values.
|
|
@@ -2019,7 +2019,7 @@ Kimi usage ceiling: 2.2.0 print stream-json exposes no token counts. Keep unobse
|
|
|
2019
2019
|
|
|
2020
2020
|
## Antigravity P1 (#485)
|
|
2021
2021
|
|
|
2022
|
-
- Run agy on the 1.4.x line (1.4.2 verified; other patches warn, #589); verify its executable/version and keep the actual TUI/IDE leg separate from the account-free
|
|
2022
|
+
- Run agy on the 1.4.x line (1.4.2 and 1.4.7 verified; other patches warn, #589); verify its executable/version and keep the actual TUI/IDE leg separate from the account-free T0 transport receipts ([1.4.2](verification/2026-10-19-agy-p1-t0.md), [1.4.7](verification/2026-10-26-agy-1.4.7.md)).
|
|
2023
2023
|
- With a reviewed compatible daemon running and agy explicitly on roster, the person runs `ahub agy` in a terminal. Confirm peer `agy` is pull-only, native permissions unmanaged and no hub sandbox/budget source is claimed.
|
|
2024
2024
|
- In the native TUI call `hub_inbox`, `hub_send`, and a permitted board read/proposal within a non-PII task. Confirm the tool audience and actor remain `agy`, the native model consumes the tool results, and no unsolicited push or PII task text reaches it.
|
|
2025
2025
|
- End the TUI normally. Confirm its group/socket detaches and only its matching managed three-file plugin is cleaned up; unrelated project/global settings remain unchanged.
|
|
@@ -3285,7 +3285,7 @@ PII authority, complete-row host screening, capture exclusion, byte budgets and
|
|
|
3285
3285
|
|
|
3286
3286
|
### Bounded sandboxed-peer capabilities (#418)
|
|
3287
3287
|
|
|
3288
|
-
The frozen issue #418 contract supplies task-bound `hub_task_spec` and the finite `hub_check_start/result/cancel` tools; memory reads reuse #412 P2. `checks.runner = "container"` or `"remote"` remains an untracked, person-owned opt-in; the remote grant and destination require `config.local.json`. Specs include validated canonical GitHub intake identity as well as bounded textual references and remain complete-or-refused; checks run immutable Git source in a credential-free, isolated Linux VM/container with fixed commands and binding-scoped receipts. Default sandbox permissions remain unchanged. The remote backend (#793) streams a metadata-stripped, owner-normalized archive to non-root `docker exec -i ... tar` extraction in its work tmpfs, rather than Docker cp into a read-only-root container. Readiness proves that same transfer and readback path plus network isolation; project/name-filtered inventory reads individual hub labels and confirms cleanup.
|
|
3288
|
+
The frozen issue #418 contract supplies task-bound `hub_task_spec` and the finite `hub_check_start/result/cancel` tools; memory reads reuse #412 P2. `checks.runner = "container"` or `"remote"` remains an untracked, person-owned opt-in; the remote grant and destination require `config.local.json`. Specs include validated canonical GitHub intake identity as well as bounded textual references and remain complete-or-refused; checks run immutable Git source in a credential-free, isolated Linux VM/container with fixed commands and binding-scoped receipts. Default sandbox permissions remain unchanged. The remote backend (#793) streams a metadata-stripped, owner-normalized archive to non-root `docker exec -i ... tar` extraction in its work tmpfs, rather than Docker cp into a read-only-root container. Readiness proves that same transfer and readback path plus network isolation; project/name-filtered inventory reads individual hub labels and confirms cleanup. Each remote run proves or prepares a Docker volume of `node_modules` keyed by the commit's `bun.lock` hash, a digest of every manifest and install configuration file (`package.json`, `bunfig.toml`, `.npmrc`, `patches/`), and the Bun version, architecture and image digest the readiness probe observed (#793b). The one container with a network runs exactly `bun install --frozen-lockfile`; a separate offline container verifies the volume read-only; the check runs offline with the volume mounted read-only and never installs. A commit whose manifests declare an install lifecycle script is refused, because that output depends on source outside the key. Pruning keeps the newest three of the project's own volumes and touches only volumes whose name, project label and key label agree. Preparation is one `prepareDeps` seam on the transport (ownership, install, marker, verify and discard are internal steps, so none can be skipped), `dependencyKey` and `dependencyVolume(projectKey, key)` name the volume, and `dependencyManifest(source)` is asynchronous because the inputs are several files in the staged source.
|
|
3289
3289
|
|
|
3290
3290
|
Apple container 1.5.0 requires a running VM for `copy`. The implementation therefore uses `run --detach --rm` with the fixed bounded idle process, copies the validated source/dependencies, executes only the selected finite check argv and confirms cleanup. A create/copy/start probe failed on the stopped-container copy and left no container; the run/copy/exec probe passed. `--internal` is verified as network configuration `mode: hostOnly`, not an inferred network name. Inactive sandboxed peers establish the cached spec authentication probe at first start/use rather than making unrelated host-embedding fixtures contact real accounts. Source and dependency staging ceilings are 100,000 entries each, 512 MiB source and 1 GiB dependencies; exceeding one refuses the check.
|
|
3291
3291
|
|
|
@@ -4483,8 +4483,8 @@ Declaration: `"claude-impl": { "kind": "claude", "roles": ["implementer"], "star
|
|
|
4483
4483
|
- **Hub tools.** The implementer set (`hub_task_accept`, `hub_task_decline`, `hub_task_done`, `hub_checkpoint`, `hub_send`, `hub_task_show`, `hub_task_propose`, `hub_remember`, `hub_inbox`, `hub_permission_request`) through the ordinary tools-mode server with `AGENTHUB_PEER_ID=<id>` and a per-child `AGENTHUB_LAUNCH_ID`. The daemon accepts a `tools` hello for this id only with the current child's launch id; it is revoked when the child's group is confirmed gone. `hub_inbox` may pull only a batch whose host-resolved task attribution matches the current child, using the same unique-task rule for unscoped chat as delivery. A mixed-task batch is refused before any queue removal, so other tasks reach their own child/session.
|
|
4484
4484
|
- **Permissions.**
|
|
4485
4485
|
- `ask`: the hook decides nothing; every write, edit and shell call reaches the prompt tool.
|
|
4486
|
-
- Both modes run Claude in its `default` permission mode, never `acceptEdits`: `acceptEdits` auto-approves in-worktree edits before the hook's path gate or the prompt tool is consulted, so protected paths (`.agenthub`, agent configuration, unsafe aliases) could be written without a person.
|
|
4487
|
-
- `ask-when-needed`: the hook answers `allow` only for `projectFileTool` targets with `projectRoot` = the execution root (worktree), so `.git`, `.agenthub` and agent configuration stay protected (`src/cli/facts-hook.ts:20-54`, `:91-96`).
|
|
4486
|
+
- Both modes run Claude in its `default` permission mode, never `acceptEdits`: `acceptEdits` auto-approves in-worktree edits before the hook's path gate or the prompt tool is consulted, so protected paths (`.agenthub`, agent configuration, unsafe aliases) could be written without a person. File grants come from the hook's explicit `allow`; automatic shell grants use only the host wrapper described below (#845).
|
|
4487
|
+
- `ask-when-needed`: the hook answers `allow` only for `projectFileTool` targets with `projectRoot` = the execution root (worktree), so `.git`, `.agenthub` and agent configuration stay protected (`src/cli/facts-hook.ts:20-54`, `:91-96`). Bash reaches the prompt tool; a complete, public command may get the #845 wrapper grant below. Unproven calls and every enabled action-policy class still ask. `never-ask` remains refused (R1).
|
|
4488
4488
|
- The daemon puts `permission` in a `facts` reply for this instance's current child launch id, as it does for the recorded `ahub claude` launch (daemon.md facts-hook rule extended to this one case).
|
|
4489
4489
|
- **Person approvals.** `hub_permission_request` (tools-mode server, callable only by this variant's current child) puts the request on the existing approval path: console `permission` push, macOS notification (#5), `approvals.timeout_s`. Payload is the tool name plus a public rendering of the arguments made by a bounded generic sanitizer, not `Tasks.publicView` (that takes a `Task`): JSON-stringify, `sanitize()`, the PII screen used for public metadata text (a match replaces the value with `[withheld]`), path values through the same screen, then a cut at a character boundary with a `[cut, N chars]` marker inside the 600-character title bound (#72 rule). It never includes task text. The original arguments stay in the child's pending request and are what Claude's allow response refers to; they are not logged or shown. Options are allow-once and deny; no allow-always. While a request waits, the watchdog is kept alive (as ACP, #5).
|
|
4490
4490
|
- **No answer.** At `approvals.timeout_s` the tool returns deny with "no person answered"; Claude continues the turn with the denial; the delivery is not failed by the timeout itself; a console and log line is written.
|
|
@@ -4496,6 +4496,16 @@ Declaration: `"claude-impl": { "kind": "claude", "roles": ["implementer"], "star
|
|
|
4496
4496
|
- **Done.** `hub_task_done` from the child completes the task through `Tasks` with the ordinary owner check (`by === task.owner`), then the reviewer is notified (I-2 excludes this instance from reviewing it).
|
|
4497
4497
|
- **Recovery.** `session.freshOnRestore` is false; crash resume reuses the per-task map, and each task's session is checked by transcript as above. No terminal row. Controlled recovery requires target feature `role_stages` (below).
|
|
4498
4498
|
|
|
4499
|
+
### Headless implementer host shell execution (#845)
|
|
4500
|
+
|
|
4501
|
+
The 2026-10-26 amendment permits automatic Bash execution only in `ask-when-needed`, for a current non-private task owned by this child in its verified registered worktree. `ask`, PII handling, native `default` mode and enabled action-policy classes retain person approval. A client must explicitly support wrapper protocol 1; an older MCP client cannot receive an automatic allow that would run the original native command.
|
|
4502
|
+
|
|
4503
|
+
- The existing #816 `proveReadOnlyShell` supplies read-only authorization, including its parser, executable and path/config proof; there is no second read-only policy. Task commands use the same simple-shell parser and a closed argv grammar: `scripts/check.sh --changed`, `bun run build`, `bun test` with 1 to 16 explicit `test/**/*.test.ts` paths, `git add` with named paths, `git commit -m <message>`, `git push origin <branch>` or `git push -u origin <branch>`, and branch-selected `gh pr view`, `checks` or `create`. Creation requires the default base, bound head, title and exactly one body or body-file. Operators, expansions, extra flags, another remote/branch, background execution and model cwd fields ask.
|
|
4504
|
+
- The host captures the canonical repository default branch once at daemon startup, before task execution, from a bounded remote `ls-remote --symref` probe against the captured GitHub repository URL with private HOME/cwd and isolated Git configuration. Each request reads attached HEAD against that captured name; a later mutable local origin/HEAD never changes this authority. No credentials are supplied to this probe. Missing/private-remote startup evidence asks until a restart with available remote evidence. A recorded task branch must match. Otherwise the first eligible task-command request may bind a non-default branch, excluding `main`, `master`, detached/unknown HEAD and another open task's branch. `Tasks.bindShellBranch` records `refs.branch` plus a host history entry containing the branch and a hash of the canonical worktree. Restart retains the binding; a later branch or root mismatch cannot rebind it. Path guards and instruction/data policy still apply. Git effects use the core hardened argv/environment; origin fetch and every push URL must match the project's captured GitHub repository for push/PR grants, and `gh` gets an explicit host repository selector. Push executes the explicit same-branch refspec `refs/heads/<bound>:refs/heads/<bound>`; configured remote.origin.push or mirror refuses. A body-file is bounded, public-text screened and captured into the executed `--body` bytes, so the privileged CLI never reopens a mutable file.
|
|
4505
|
+
- An automatic allow returns fresh `updatedInput` containing a fixed absolute hub wrapper invocation and a random single-use ticket. Native shell cwd, PATH and environment select neither the command nor its authority. The grant record is held in bounded, expiring host memory; its opaque nonce is visible to the requesting MCP call and wrapper argv. A model-selected permission call can therefore receive a ticket and record a branch binding after the same host checks; ticket possession is not native-dispatch provenance. The record binds task, peer, current child generation, canonical root, branch, exact parsed/effective argv, executable, environment and proof recheck. It is consumed before validation or execution; restart revokes all tickets. Unknown, expired, reused or mismatched tickets run nothing and return the fixed refusal `host shell execution refused`.
|
|
4506
|
+
- The private Unix-socket execution service rechecks current child/task/root/branch and proof immediately before spawning the resolved absolute executable at the proven cwd. Read-only grants and check/test/build task grants retain the local-worker Seatbelt profile and own-temp rules. An additive deny-only profile forbids every write to project Git common/current metadata, including loose/packed objects, HEAD/index, refs/logs, other worktrees and shared/private submodule config/hooks/config.worktree. Directory entries cannot be renamed or replaced. Unknown layouts ask. Closed host Git add/commit run outside that profile, like push/gh: only parsed bounded paths/messages, with root/task/branch proof rechecked. Configured filter and external diff/textconv drivers refuse add/commit only; push/gh do not run clean/smudge and remain eligible with global filter.lfs. The chosen policy is person approval for add/commit, not neutralizing filters. Config includes and partial clones still refuse privileged Git/gh; fixed argv disables signing, automatic maintenance, hooks and fsmonitor. Credential denies remain for code, and canonical live hub state is denied even with a primary-checkout read allowance. Network follows local.bash_network: normal enabled mode uses the configured proxy, while the explicit "direct" setting gives gate code direct network access. Without macOS sandbox-exec these commands ask. Only closed host git-add/git-commit/git-push and gh operations remain outside that profile. Its environment is the #816 host baseline (`PATH`, `HOME`, `LANG`, `TERM`), fixed proxy and peer identity, plus an owned command TMPDIR; Project/worktree PATH entries and aliases are removed before proof and execution. Git receives only the core's fixed hardening variables; gh receives that same core configuration through GIT_CONFIG_COUNT/KEY/VALUE for its Git children. No inherited native environment or authentication value is copied into `updatedInput`. Output streams and numeric exit status pass through. Child abort, disconnect and shutdown terminate only the command's owned process group. A target that deliberately starts a new session with setsid can outlive that cleanup; such a descendant keeps its Seatbelt restrictions and may keep writing the permitted worktree paths. This is not an all-descendants termination guarantee.
|
|
4507
|
+
- The dated model-free native receipt must cover modified input, an earlier persistent-shell `cd`, spaces/apostrophes, poisoned native `RIPGREP_CONFIG_PATH` and `GIT_DIR`, and ticket replay refusal. Native transport evidence, host fixture tests, package gate and real-account behavior are distinct evidence classes. The executable task commands use the host profile above; this does not sandbox the entire native Claude process.
|
|
4508
|
+
|
|
4499
4509
|
### R5b. Terminal Claude implementer
|
|
4500
4510
|
|
|
4501
4511
|
Declaration: `"claude-impl-t": { "kind": "claude", "roles": ["implementer"], "worktree": ".worktrees/claude-impl-t" }`.
|
|
@@ -4629,7 +4639,7 @@ resumeFromRoster?(): Promise<void>;
|
|
|
4629
4639
|
/** Fixed builtins only: a person's frontend attaches to the native session through this hub proxy. */
|
|
4630
4640
|
readonly frontendProxy?: FrontendProxyPort;
|
|
4631
4641
|
/** Fixed builtins only: a fixed operator notice; never native ids or provider text. */
|
|
4632
|
-
readonly statusNotice?: { code: "unidentified-cancellation"; message: string };
|
|
4642
|
+
readonly statusNotice?: { code: "unidentified-cancellation" | "qwen-detached" | "qwen-stalled"; message: string };
|
|
4633
4643
|
/** Fixed builtins only, and only with session.nativeRevert: roll the native conversation back to before this native turn. */
|
|
4634
4644
|
revertTurn?(nativeTurn: string): Promise<void>;
|
|
4635
4645
|
```
|
|
@@ -4661,7 +4671,7 @@ Who sets what (parity by construction; each value is today's member, renamed at
|
|
|
4661
4671
|
| `detachForRoster` | `WsPeer` (ignores the mode, `channel-peer.ts:91`), `CodexPeer` (`codex-appserver.ts:473`), `PiPeer` (`pi.ts:471`) | `daemon.ts:4758-4760` |
|
|
4662
4672
|
| `resumeFromRoster` | `PiPeer` (`pi.ts:496`) | `daemon.ts:4773` |
|
|
4663
4673
|
| `frontendProxy` | `CodexPeer`: `url` = `proxyUrl` (`:137`), `attached` = `proxyAttached` (`:488`), the two methods as at `:494`, `:500` | `daemon.ts:3701`, `:5198-5280` |
|
|
4664
|
-
| `statusNotice` | `CodexPeer` (`
|
|
4674
|
+
| `statusNotice` | `CodexPeer` (`unidentified-cancellation`) and the existing `QwenDualPeer` (`qwen-detached`, `qwen-stalled`), unchanged | fixed-builtin-gated status projection |
|
|
4665
4675
|
| `revertTurn` | `CodexPeer.revert` (`codex-appserver.ts:610`) | `daemon.ts:2446`, `:2459` |
|
|
4666
4676
|
|
|
4667
4677
|
Per-port absent and undefined semantics:
|
|
@@ -4914,7 +4924,7 @@ Common acceptance criteria:
|
|
|
4914
4924
|
- `frontendProxy` undefined refuses permission changes and `already`.
|
|
4915
4925
|
- The `unidentified-cancellation` notice shows only from a fixed builtin.
|
|
4916
4926
|
- The Codex sticky-override, restoring-turn, detached-clear and attached-refusal tests stay unchanged.
|
|
4917
|
-
- `isCodexPeer` rows leave the table.
|
|
4927
|
+
- The proxy/startup/revert `isCodexPeer` rows leave the table. The later `actionEnforcement` branch remains intentional: it reads Codex `actionGated` to choose the existing enforceability refusal. It is outside 2d and waits for a separately specified action-policy port; no `actionGated` member is added to `FrontendProxyPort`.
|
|
4918
4928
|
5. **2e: Claude launch proof extraction.** Area note: `daemon.md` (Claude launch and hook rules). Size M.
|
|
4919
4929
|
- Paths: new `src/hub/claude-launch-proof.ts`, `src/hub/daemon.ts` (hello, `permissionDisplay`, `changePermission`, facts), tests.
|
|
4920
4930
|
- No change to `src/cli/launch.ts`, `src/cli/facts-hook.ts`, `src/adapters/claude-channel.ts` or the plugin bundle.
|
|
@@ -4965,8 +4975,9 @@ Native CLIs update often: Codex 0.146.0 → 0.163.0, and Kimi 2.2.0 → 2.3.0 by
|
|
|
4965
4975
|
- **Named reasons.** A version refusal starts with `VERSION_REFUSAL` and names only versions and the trust command. The daemon passes it through `sanitize()` for every reviewer backend, while other Codex prepare failures stay generic. The Jcode refusal is named before the owned daemon starts.
|
|
4966
4976
|
- **Seeing drift.**
|
|
4967
4977
|
- `ahub agents` (allowed for agents) and `ahub doctor` show each verdict.
|
|
4968
|
-
- A
|
|
4969
|
-
- Jcode
|
|
4978
|
+
- A daily workflow compares the latest published version of every public-source kind with `verified` and keeps one `agent-compat` issue current (#843 widened it from weekly to daily and from npm-only to every public source).
|
|
4979
|
+
- Sources: the npm-distributed kinds read npm `latest`; Jcode reads its GitHub releases' latest tag (leading `v` stripped, #843). agy has no public source and is reported as an informational note, never drift; pi has an entry since #847 and is watched like the rest.
|
|
4980
|
+
- Local drift (#843 AC3): at hub start and daily the daemon reads each installed CLI's version with the `ahub agents` probe and tells the effective planner once per (id, version) when it is not `verified`: id, version, state and the last receipt path only, deduplicated per planner in `hub.db` across restarts, so a planner that could not be queued is retried alone. A missing executable or an unreadable version is silent. `ahub status` shows one attention line per drifted id.
|
|
4970
4981
|
- **Moving a verified set.** A receipt in `docs/verification/` adds the version to `verified` in the same PR.
|
|
4971
4982
|
|
|
4972
4983
|
## Amendment: lifecycle identity linked by the hub (#694)
|
|
@@ -5163,3 +5174,85 @@ Ceiling: a scan lists the board once per status revision (at most one per flush)
|
|
|
5163
5174
|
- `hub_task_show` also admits the configured planner (a peer holding the `planner` role, `Tasks.isConfiguredPlanner`, `ConductorHooks.isPlanner`) for any task outside the backlog (a staged backlog row stays hidden from agent task tools, #263, and is read through the planner tools; a peer that is also the conductor takes the audited conductor path). It gets exactly the conductor's view (`publicConductorTask` over `Tasks.publicView(task, true)`), so a PII task is still a stub with no history. The read writes no conduct audit event and grants nothing else: the planner cannot assign, escalate, hold or start through the conductor tools, and every other peer is refused with the conductor-role error as before.
|
|
5164
5175
|
- `hub_task_cancel` by the planner keeps every check it had (its own proposal, `backlog` or `proposed` state, planner authority, no private turn on ordinary work, the flow revision, recovery and shutdown exclusion) and replaces only the acceptance test with the evidence test `hub_backlog_return_untouched` uses (`taskHasWorkEvidence`): a branch, commit, PR, recorded local check, done, verdict or reopen refuses with the unchanged text `planner may cancel only its own historically unaccepted backlog or proposed work`. An accepted-then-declined task with none of them (the observed case, #454) is cancellable; the history keeps the acceptance and the decline.
|
|
5165
5176
|
- Reason: the planner could not read a task it had proposed once an owner took it, and could not clear the board of a task its owner declined untouched. The person's cancellation is unchanged.
|
|
5177
|
+
|
|
5178
|
+
## Accepted contract: external module-kind instances (#754)
|
|
5179
|
+
|
|
5180
|
+
**Status: accepted on 2026-10-27.** The [person's decision on #754](https://github.com/STAIxBWLB/agent-hub/issues/754#issuecomment-6441781951) selects O1 explicit module opt-in, O2 implementer/verifier/ordinary continuing reviewer, O3 live startup catalog only and O4 retained live-module recovery refusal. This is board task #287's contract-only deliverable against main `7a599197`. Implementation is a separate board task after this amendment merges; its plan must freeze context and retained-record field names before source enables this boundary. The existing A4 refusal in `validateInstances` and the external-definition validator stays authoritative. This amendment neither advertises a daemon feature nor proves a module or native account can run instances.
|
|
5181
|
+
|
|
5182
|
+
### Existing boundary and accepted scope
|
|
5183
|
+
|
|
5184
|
+
- Keep person-owned, untracked machine-local bundle declarations, content pins, daemon-startup-only imports and immutable project catalogs. CLI reads, previews, reloads and watchers never evaluate a module. A tracked, shared, cloned or unknown-provenance declaration grants nothing.
|
|
5185
|
+
- Keep the A4 executable subset: headless ACP, the public `AcpPeer` transport, bound peer id, `deliveryOnly === true`, `hubOwned === true`, `form === "headless"`, verified owned-group admission and load-session echo. A module pin authorizes daemon-privileged code; it is not a sandbox or a proof that an approved module cannot be malicious. An import timeout is not cancellation or rollback of privileged initialization.
|
|
5186
|
+
- Keep builtin namespace reservations and every fixed-builtin capability exclusion. No native engine, native/fresh-reviewer variant, host proof port, native home/model/provider selector, native hook, budget callback or arbitrary configuration callback becomes an external-instance capability. `instances` policies reserved for builtin definitions do not become module-supplied callbacks.
|
|
5187
|
+
- The accepted first slice requires explicit module opt-in and permits implementer, verifier and ordinary continuing reviewer instances within an explicit module-role subset and the kind ceiling. Every instance has its own managed worktree. Existing reviewer independence and MCP restrictions apply; planner, conductor and fresh/isolated reviewer capabilities remain excluded, with no claim of fresh-reviewer isolation. Semantic validation uses the live startup catalog, and live external controlled recovery remains refused.
|
|
5188
|
+
|
|
5189
|
+
### Stable module identity and a current startup binding
|
|
5190
|
+
|
|
5191
|
+
- A kind name, display label, instance suffix, roster row or adapter-reported generation is not module identity. Startup retains the provenance of each validated definition alongside its captured bundle: canonical project root, normalized project-relative declaration path, full lowercase SHA-256, exported kind, `ADAPTER_API` and a digest of the validated data-only capability/permission ceiling. Duplicate kinds, builtin/configured namespace collisions, wrong hash/API/kind, unsupported fields and unknown provenance refuse independently before any affected native start or bus admission.
|
|
5192
|
+
- Resolve `instances.<id>.kind` only against that daemon's verified startup catalog. Its module binding is the exact retained tuple, not a later path lookup or an entry from the process-global builtin registry. No kind is inferred from an id prefix to repair a missing declaration, and no missing external kind falls back to a builtin, configured kind or another module with the same label.
|
|
5193
|
+
- Keep stable identity separate from liveness: a new daemon has a new catalog epoch and new owner generations. Recovery may compare an exact stable tuple, but it never reuses an old catalog epoch as current-owner proof. Pin, declaration-path, exported-policy or instance-policy changes invalidate continuity; no automatic repin, path relocation, native-session rebinding or fresh-session fallback is implied.
|
|
5194
|
+
- Module and instance declarations remain restart-bound. A reload can report pending changes and revoke affected authority when its evidence no longer matches, but cannot import, repin, create a new catalog or silently publish the desired replacement policy as running policy.
|
|
5195
|
+
|
|
5196
|
+
### Instance declaration and opt-in policy
|
|
5197
|
+
|
|
5198
|
+
- Instance ids retain the existing `<declared-kind>-<name>` grammar, nonreserved ASCII id bound of 32 characters and project-wide maximum of 64 instances; modules retain their 16-entry bound. A kind too long to admit a valid instance id refuses; ids are never truncated or rebound. Existing builtin/configured declarations keep their current rules.
|
|
5199
|
+
- Under accepted decision O1, a newly pinned module explicitly declares a bounded own-data headless-instance opt-in. Its proposed logical fields are headless form and an allowed role subset. This is a new reviewed external data subset, not permission to declare builtin `instances`, reviewer backends or executable policy callbacks. Field/API compatibility names must be frozen in the separate implementation task plan; older hosts must clearly refuse the new subset rather than treat it as singleton support.
|
|
5200
|
+
- An instance declares only its kind, role subset, headless form, managed worktree and the existing permission/read-only selection. Command, argv, provider, model, native data home, environment overrides, launch callbacks and fresh review mode are outside the first slice. The host must newly derive a private per-instance state namespace and pass it with the exact instance id, execution root and MCP binding. Today the ACP context carries the shared daemon `stateDir`; it does not already provide that isolation. Keep canonical daemon control/MCP state separate from private instance state, and freeze the context field carrying the new namespace before implementation; config cannot choose a callback or alternate SDK.
|
|
5201
|
+
- Roles are intersected with the captured kind ceiling, the accepted instance-policy ceiling and host role rules at config validation, effective-role derivation, routing and tool admission. A later id-keyed role list cannot restore a refused role. Conductor and builtin-only planner eligibility stay excluded. Accepted O2 includes ordinary continuing reviewer instances alongside implementers and verifiers, only under the explicit module-role subset and existing reviewer-pool independence and MCP restrictions. An external reviewer instance never reviews a task it owns or implemented; the current reviewer-pool family and same-instance rules apply unchanged, as for builtin reviewers. Fresh mode and isolated builtin reviewer capabilities remain refused; every reviewer instance has its own worktree, without any claim of fresh-reviewer isolation.
|
|
5202
|
+
|
|
5203
|
+
### Execution root and non-transferable current-owner proof
|
|
5204
|
+
|
|
5205
|
+
- Every external instance, including a verifier, names its own existing `.worktrees/<id>` directory. This is intentionally stricter than today's generic verifier fallback. The host checks real directory identity, exact registered worktree root, same Git common directory and non-prunable inventory; no creation, repair, another checkout or canonical-root fallback occurs. A potentially writing instance requires an attached branch. Detached read-only execution needs an explicitly accepted read-only policy and cannot be used as write-owner evidence.
|
|
5206
|
+
- Before native spawn, freeze the module/instance binding and the validated cwd. An affected instance joins the bus or gains `ownerIsolation` only after the host has current evidence tying that exact runtime object and owned-child launch generation to that cwd, module tuple, instance policy and daemon/catalog epoch. A native session or MCP environment string alone cannot establish it.
|
|
5207
|
+
- The implementation must obtain that receipt from the actual managed ACP spawn/owned-process path, through a host-retained non-virtual binding, not a module's `recoveryMetadata().launch.cwd`, arbitrary getter or returned "proven" flag. Bypassing the managed spawn, returning the wrong transport/id/options or failing owned-group verification leaves no admissible receipt and refuses instance admission. A receipt describes the managed runtime; it does not confine daemon-privileged module code or all native tools to a filesystem sandbox.
|
|
5208
|
+
- Recheck current bus owner, child generation, startup binding, policy and the registered worktree before write/task-owner admission, after intervening awaits and immediately before effects. Replacing a directory, moving a branch required for writing, changing the module/policy binding, losing the owned process or receiving unreadable evidence revokes authority. An unavailable check is unknown, never isolation; retain a failed-start owner with unconfirmed processes for explicit teardown rather than assigning its id to a replacement.
|
|
5209
|
+
- Give every instance its own host records, queue/task identity, native session binding and MCP identity. Child environments replace inherited agent markers with the exact instance id. MCP access requires a host-issued current launch binding, rechecked after awaits; an old child, a base-kind child or a client that echoes another instance's env cannot act for it. Native configuration/credential-home isolation is not implied by worktree isolation, and no automatic native-home copying or credential migration is introduced.
|
|
5210
|
+
|
|
5211
|
+
### Permission ceilings and lifecycle
|
|
5212
|
+
|
|
5213
|
+
- A per-instance choice is machine-local and no wider than the captured module kind's native mapping and accepted host ceiling. An explicit managed mode must be advertised and acknowledged by that native session before bus admission. Missing mapping, failed acknowledgement or unknown state refuses that selection; it never becomes an implicit allow or a falsely claimed native `ask`. An omitted choice may retain the kind's existing unmanaged behavior, visibly unmanaged.
|
|
5214
|
+
- `readOnly: true` requires the kind's supported native read-only mapping and observed acknowledgement. It also applies the existing host ACP read/search/think restriction and read-only MCP/tool ceiling; neither confirmation, an allow-always choice nor session budgets raise it. Read-only and an independent `permission_mode` selection stay mutually exclusive. Neither read-only acknowledgement nor a pin claims kernel isolation.
|
|
5215
|
+
- Roster removal first fences dispatch, current tool authority and approvals, then stops the instance through the verified headless owned-process path. It does not detach a person's TUI or widen an external kind to a terminal form. Offline is insufficient if an owned process is pending or its absence is unknown: attempt only verified owned teardown, retain the owner/queue/in-flight uncertainty on failure, and perform no permission reset, task handoff, automatic restart or replacement until teardown is confirmed.
|
|
5216
|
+
- A successful removal preserves queued work, independent pauses and recorded history. Return to the roster does not itself start or resume a module instance. A later explicit start obtains a new owner/launch proof under the same accepted identity rules; stale tools and replies never revive a retired generation.
|
|
5217
|
+
|
|
5218
|
+
### Bounded CLI metadata without code evaluation
|
|
5219
|
+
|
|
5220
|
+
- The current `{kind, displayName}` metadata is insufficient for external-instance semantic validation. A supporting daemon must project a separate data-only view of its verified catalog binding and instance policy. Do not manufacture `start.create`, `instances` callbacks or executable `AdapterDefinition` values from display metadata to make a validator pass.
|
|
5221
|
+
- Proposed public view fields are bounded instance id, kind, display name, role names, headless/read-only and permission-state enums, availability/refusal code and an observation epoch/fingerprint. Keep at most 64 instance rows, kind/id at 32 characters, display names at 256 UTF-8 bytes and fixed refusal codes; truncate display only with an explicit incomplete indication, never silently validate a truncated inventory. Full pins/declaration provenance belong in authenticated person diagnostics. Module bodies, callback source, argv, native ids, provider errors, secrets and arbitrary native text are excluded.
|
|
5222
|
+
- `status`, `help`, `route explain`, previews and upgrade planning consume declarations plus that bounded host projection without importing modules or invoking module callbacks. Metadata explains observed state; it cannot register code, grant permissions, prove current ownership or authorize a start. A mutating/start request revalidates the current authenticated daemon instance, feature set, startup tuple and declaration revision, not metadata echoed by its caller.
|
|
5223
|
+
- Accepted O3 requires live-catalog semantic validation and keeps module declarations as `{path, sha256}`. Without a matching daemon, raw declarations may be listed as declared/unknown but cannot be resolved to an executable instance, assigned a guessed ceiling or used to prepare recovery. A cached projection never becomes runtime authority merely because its hash or label matches.
|
|
5224
|
+
|
|
5225
|
+
### Recovery compatibility and refusal before effects
|
|
5226
|
+
|
|
5227
|
+
- Reserve proposed daemon feature `external_adapter_instances`, in addition to the existing `external_adapter_modules` and `peer_instances`. It is not advertised by this amendment. Retained receipts and restart snapshots must carry a versioned stable binding containing canonical project root, instance id/kind, normalized module declaration path, full pin, API version, capability and instance-policy digests, and validated execution root. Integrity validation covers every field; any recorded native session is bound to that same tuple. Catalog epoch, runtime object identity and launch generation remain live host evidence, never portable ownership proof. Freeze the serialized schema in the separate implementation task plan and add the feature to every source/target incompatibility preflight. Old metadata cannot be silently read as that new proof.
|
|
5228
|
+
- A supporting coordinator/source must refuse a target missing any required feature before recovery holds, session capture, native stop, terminal close, commit, target startup or module evaluation for that operation. This applies even when the affected instances are offline: a target that cannot interpret their declarations/bindings is not compatible. Apply the same rule to reused staged targets, lease renewal and the last pre-commit read; once committed, retain the verified target/package binding rather than rediscovering identity from mutable config.
|
|
5229
|
+
- A source lacking the combined feature, an unreadable source feature/inventory response, or a declaration with no verified kind-to-pin binding also refuses before lifecycle effects. An older source's unknown/all-offline rows cannot waive the new external-instance floor. The CLI may conservatively report a possible unresolved module instance to refuse; that is not permission to invent its identity. Existing builtin/configured offline exceptions remain unchanged for those declarations.
|
|
5230
|
+
- The staged target's host features/SDK and captured declaration pins are checked without importing project modules. A supporting target later validates the same captured module tuple and policy before admitting any affected instance; changed/missing pin, incompatible API/policy, wrong project/root or incomplete retained binding refuses without reusing a session under another identity. Module evaluation already authorized at daemon startup is not rolled back; a post-import exported-kind mismatch must still prevent native spawn, bus admission and owner proof.
|
|
5231
|
+
- Accepted O4 retains A4's live-module controlled-recovery refusal. Stop affected module instances explicitly before the supported host restart/upgrade path; keep their stopped records and restore builtin peers normally. External instances remain stopped until a person's explicit start with the exact verified module/instance binding. A supplied recorded native session id is bounded and must receive a matching load-session echo; absent proof refuses and never falls back fresh. Automatic external-instance resume would require separately accepted source/target native-session, process-group and restoration receipts, not inference from `resumeById`.
|
|
5232
|
+
- Feature-aware binaries (and any explicitly designated compatibility backport) must reject unsupported module-instance declarations before project-module evaluation or native start. Already released A4 binaries cannot provide this guarantee: at baseline `7a599197`, daemon startup imports pinned modules before validating instance declarations, so direct execution can evaluate a module and then refuse the instance. A supporting coordinator must refuse that target before starting it; direct legacy execution remains an explicit migration limitation, not a promised safe path. No client attaches an instance id without the combined feature. Removing a declaration to cross an initial unsupported-source upgrade is an explicit person action with stopped/history evidence, never an automatic workaround; a backport/version floor requires separate acceptance.
|
|
5233
|
+
|
|
5234
|
+
### Acceptance matrix for the later implementation
|
|
5235
|
+
|
|
5236
|
+
These are required future proofs, not tests run or support enabled by this docs-only PR. Use fake bundled modules, fake ACP agents, isolated homes and host-side effect counters first; real native concurrency/session restoration needs its own declared receipt.
|
|
5237
|
+
|
|
5238
|
+
| Claim | Required positive and negative proof |
|
|
5239
|
+
|---|---|
|
|
5240
|
+
| A1 mixed catalog/identity | A builtin extra instance and implementer, verifier and ordinary continuing reviewer instances of an opted-in external kind coexist with distinct roots, native sessions, records, queues and MCP identities. The reviewer is admitted only within the explicit module-role subset and existing reviewer independence/MCP restrictions, with its own worktree and fresh mode refused. Missing opt-in, duplicate/colliding kinds, kind/pin/API/policy changes, ambiguous namespace and malformed/over-limit declarations refuse without rebinding. |
|
|
5241
|
+
| A2 current execution owner | Registered exact worktrees succeed. Missing, symlink-replaced, prunable, foreign-repository, canonical-fallback and detached-writer roots refuse. Swap the root/pin/policy or owner across each await; no stale write/owner admission occurs. A factory that lies in metadata or bypasses managed spawn cannot mint owner proof. |
|
|
5242
|
+
| A3 policy ceiling | Implementer, verifier and ordinary continuing reviewer roles and id-keyed overrides cannot exceed the explicit module-role subset or accepted kind/instance ceiling. Continuing reviewers cannot review tasks they own or implemented; existing reviewer-pool family, same-instance and MCP restrictions apply unchanged, and fresh mode is refused and planner/conductor remain excluded. Missing or unacknowledged native modes, read-only escalation and mutating MCP operations refuse before effects. Builtin-only fields, fresh reviewers, TUI/native engines, native proof ports and config callbacks remain refused, including explicit undefined declarations where presence is forbidden. |
|
|
5243
|
+
| A4 no-import CLI | With import/callback tripwires, exercise online and offline `status`, `help`, `route explain`, preview and upgrade planning. No module evaluation occurs; unknown/stale/foreign-epoch/incomplete metadata cannot enable a factory, guess a ceiling or grant ownership. Outputs respect all row/byte bounds and contain no argv/provider/source data. |
|
|
5244
|
+
| A5 roster and process lifetime | Fence an active delivery and approval before stop. Repeat with offline-pending and unknown owned groups: no handoff/reset/replacement until confirmed gone; queued and uncertain work is retained. Return and stale MCP/reply attempts cannot revive an old launch. |
|
|
5245
|
+
| A6 source/target compatibility | Test missing features on source and target, unknown/all-offline old rows, retained/reused target mismatch, changed pins/API/root/policy and feature-aware/backported binaries reading unsupported config. Counter assertions show zero holds/captures/stops/closes/commits/new imports/native starts at a supporting preflight refusal. Separately cover the unpatched A4 startup order with a harmless import tripwire: it may evaluate the pin before refusing the instance, and must not be represented as satisfying pre-import refusal. Legacy builtin/configured recovery remains byte/behavior compatible. |
|
|
5246
|
+
| A7 supported recovery ceiling | Under accepted O4, live external peers refuse controlled recovery; explicitly stopped instances stay stopped while compatible builtin recovery completes. Manual same-binding session load requires exact echo; missing/foreign echo and stale native ids refuse with no fresh fallback. Automatic resume is outside the accepted first slice; any later extension needs separate acceptance and its additional receipt matrix. |
|
|
5247
|
+
| A8 delivery gate | Amend the relevant source/target feature checks and docs in the implementation PR; focused mixed regressions, conformance, affected seeded guards and the full `scripts/check.sh` pass at its head with `check: OK`, as required by issue #754 AC5. This docs-only amendment may use `scripts/check.sh --changed`; that does not satisfy the later implementation gate. Report native/account checks and unknown coverage separately from fake/local/hosted evidence. |
|
|
5248
|
+
|
|
5249
|
+
### Decisions
|
|
5250
|
+
|
|
5251
|
+
The [person accepted these choices on 2026-10-27](https://github.com/STAIxBWLB/agent-hub/issues/754#issuecomment-6441781951). Contract acceptance does not enable implementation: A4's refusal remains until a separate implementation task delivers the required proofs.
|
|
5252
|
+
|
|
5253
|
+
| Decision | Accepted first-slice choice |
|
|
5254
|
+
|---|---|
|
|
5255
|
+
| O1 opt-in authority | Require an explicit bounded headless-instance policy in a newly pinned module; an existing singleton module does not acquire multiple ids by config alone. |
|
|
5256
|
+
| O2 initial roles | Implementer, verifier and ordinary continuing reviewer within an explicit module-role subset and the kind ceiling, with existing independence and MCP restrictions. Every instance has its own worktree. Planner, conductor and fresh/isolated reviewer capabilities stay excluded; no fresh-reviewer isolation is claimed. |
|
|
5257
|
+
| O3 offline declaration binding | Keep module declarations as `{path, sha256}` and require the matching authenticated live startup catalog for semantic instance resolution; offline rows remain declared/unknown. |
|
|
5258
|
+
| O4 controlled recovery scope | Preserve the live-module refusal. Stop the instance explicitly, then a person starts it again with the same binding; there is no automatic resume. |
|
|
@@ -44,3 +44,13 @@ The shared lifecycle persisted a finished remote-shaped receipt, exit zero and l
|
|
|
44
44
|
## Remaining work
|
|
45
45
|
|
|
46
46
|
The production runner remains unavailable until the separate admitted, lockfile/Bun-keyed dependency preparation exists (AC3). A real remote SSH leg, the unchanged four-CPU launch there, person opt-in and pi's live hub_check_start receipt (AC6) remain separate work. The fake transport/config regressions and final packaging gate are recorded with the PR's exact head.
|
|
47
|
+
|
|
48
|
+
## Dependency volume (793b)
|
|
49
|
+
|
|
50
|
+
Date: 2026-10-26. Same local Docker daemon (29.5.2, Colima, two CPUs), same preinstalled pinned image, no pull, and the same disclosed change: the driver lowered only the CPU cap to two. The driver used the production `remoteRunnerTransport` and `ContainerRunner` lifecycle with a direct Docker command port, on a synthetic repository whose only dependency was `is-number@7.0.0` and whose test asserted both that the dependency imports and that a write into `/work/node_modules` fails.
|
|
51
|
+
|
|
52
|
+
- First run: one `volume create`, one networked container, one `bun install --frozen-lockfile`, then the check exited zero with both tests passing.
|
|
53
|
+
- Second run, same commit: no volume create, no install, no container with a network; the check exited zero against the same volume.
|
|
54
|
+
- Afterwards no labelled container remained and exactly one labelled volume existed; the driver removed it.
|
|
55
|
+
|
|
56
|
+
This does not verify the remote SSH leg, the four-CPU launch, a person opt-in or pi's live `hub_check_start` receipt (AC6).
|