@staix/agent-hub 0.23.1 → 0.25.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 +24 -0
- package/README.md +32 -5
- package/docs/agent-notes/adapters.md +5 -2
- package/docs/agent-notes/bus.md +1 -0
- package/docs/agent-notes/daemon.md +16 -2
- package/docs/agent-notes/tasks.md +9 -2
- package/docs/agent-notes/tests.md +3 -0
- package/docs/dogfood.md +16 -2
- package/docs/memory.md +1 -1
- package/docs/operations.md +198 -5
- package/docs/profiles.md +2 -0
- package/docs/quickstart.md +19 -4
- package/docs/security.md +225 -8
- package/docs/specs/2026-09-19-agent-hub-design.md +54 -1
- package/docs/specs/2026-09-19-multi-project-design.md +12 -1
- package/docs/specs/2026-11-01-research-campaigns-design.md +351 -0
- package/docs/verification/2026-10-31-0.24.0.md +63 -0
- package/docs/verification/2026-10-31-claude-implementer-2.1.316.md +65 -0
- package/docs/verification/2026-11-01-0.25.0.md +91 -0
- package/docs/verification/claude-shell-wrapper-native.py +1 -1
- package/docs/verified.json +31 -15
- package/package.json +4 -3
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +5120 -4710
- package/src/adapters/claude-channel.ts +29 -6
- package/src/adapters/compat.ts +3 -3
- package/src/adapters/definitions/native-cli-claude-setup.ts +4 -0
- package/src/adapters/definitions/native-cli-doctor.ts +30 -0
- package/src/adapters/jcode.ts +3 -0
- package/src/adapters/registry.ts +1 -1
- package/src/cli/acon-register.ts +29 -0
- package/src/cli/autonomy-homes.ts +33 -0
- package/src/cli/autonomy-offline.ts +75 -0
- package/src/cli/autonomy-projects.ts +103 -0
- package/src/cli/autonomy.ts +44 -0
- package/src/cli/claude-homes.ts +123 -0
- package/src/cli/claude-profile-hook.ts +130 -0
- package/src/cli/handoff-args.ts +27 -0
- package/src/cli/handoff-output.ts +25 -0
- package/src/cli/help.ts +8 -4
- package/src/cli/identity.ts +8 -1
- package/src/cli/main.ts +174 -33
- package/src/cli/output.ts +8 -5
- package/src/cli/schedule-output.ts +8 -4
- package/src/cli/setup-homes.ts +46 -0
- package/src/cli/setup.ts +1 -1
- package/src/cli/study-output.ts +71 -0
- package/src/cli/study.ts +58 -0
- package/src/cli/task-output.ts +1 -0
- package/src/hub/action-class.ts +203 -14
- package/src/hub/action-enforce.ts +8 -6
- package/src/hub/action-policy.ts +95 -9
- package/src/hub/autonomy-consent.ts +173 -0
- package/src/hub/autonomy-defaults.ts +58 -0
- package/src/hub/autonomy.ts +232 -0
- package/src/hub/board.ts +871 -2
- package/src/hub/channel-project.ts +61 -0
- package/src/hub/config-apply.ts +3 -2
- package/src/hub/confirmation-matrix.ts +202 -0
- package/src/hub/control-client.ts +10 -1
- package/src/hub/daemon.ts +546 -27
- package/src/hub/envelope.ts +2 -0
- package/src/hub/events.ts +4 -1
- package/src/hub/execution-budget.ts +12 -0
- package/src/hub/handoff.ts +126 -0
- package/src/hub/hub-tools.ts +12 -2
- package/src/hub/manager.ts +64 -57
- package/src/hub/no-writes.ts +1 -1
- package/src/hub/path-policy.ts +12 -8
- package/src/hub/project.ts +14 -5
- package/src/hub/provenance.ts +30 -0
- package/src/hub/research-campaign.ts +265 -0
- package/src/hub/research-experiments.ts +445 -0
- package/src/hub/restart.ts +26 -0
- package/src/hub/schedules.ts +210 -9
- package/src/hub/session-identity.ts +49 -0
- package/src/hub/settings.ts +105 -8
- package/src/hub/task-repository.ts +11 -6
- package/src/hub/tasks.ts +894 -22
- package/src/hub/ui.ts +15 -6
- package/src/memory/capture.ts +2 -2
- package/src/ui/index.html +5 -3
- package/src/ui/office-space.ts +118 -0
- package/src/ui/plugin.ts +293 -0
- package/templates/AGENTS.block.md +1 -0
- package/templates/presets/autonomous.json +161 -0
- package/templates/presets/confirmation-matrix.json +943 -0
- package/templates/profiles/general.json +1 -1
- package/templates/profiles/research.json +4 -4
- package/templates/profiles/writing.json +1 -1
- package/templates/roles/research/analyst.md +1 -0
- package/templates/roles/research/fact-checker.md +1 -0
- package/templates/roles/research/research-lead.md +1 -0
- package/templates/roles/research/researcher.md +1 -0
- package/templates/roles/research/statistician.md +1 -0
package/docs/security.md
CHANGED
|
@@ -6,8 +6,8 @@ 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, 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
|
|
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`, `local.doc_read`, `local.web_fetch`, `terminal.open`, `tasks.auto_pull`, `tasks.queue_backpressure`, `context.max_assignment_used`, `schedules`, `folder_triggers`, `corpus.paths`, `invocation`, `instances`, `action_policy`, `rooms`) apply only from a config file git confirms nobody committed (`instances`, `action_policy` and `rooms` 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, a folder the hub reads, work proposed on a timer, a host a model may fetch from, which of your agents' skills other peers may call, 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, a person's console `y` for a config default at hub start, or retained autonomy consent bound to that peer's exact never-ask value in the untracked `config.local.json` (#999, below). Without one of these 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`, `local.doc_read`, `local.web_fetch`, `terminal.open`, `tasks.auto_pull`, `tasks.queue_backpressure`, `context.max_assignment_used`, `schedules` (the `every`, `cron` and `tz` forms alike), `folder_triggers`, `corpus.paths`, `invocation`, `instances`, `action_policy`, `rooms`) apply only from a config file git confirms nobody committed (`instances`, `action_policy` and `rooms` 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, a folder the hub reads, work proposed on a timer, a host a model may fetch from, which of your agents' skills other peers may call, 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.
|
|
@@ -24,13 +24,13 @@ agent-hub connects agents that can each run commands. This page says what the hu
|
|
|
24
24
|
|
|
25
25
|
## Native memory authority and profile selection
|
|
26
26
|
|
|
27
|
-
The portable core is the exact-pinned first-party `@staix/agent-mem` 0.
|
|
27
|
+
The portable core is the exact-pinned first-party `@staix/agent-mem` 0.18.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.
|
|
28
28
|
|
|
29
29
|
`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.
|
|
30
30
|
|
|
31
31
|
Code neighbours (#684 phase 4). For a symbol start, `hub_memory_graph` and the dashboard Memory view's Code neighbours toggle add one hop of callers and callees, read at the current commit through the optional code-facts boundary and shown with that commit; these edges are never stored. They share the graph limits above, an ambiguous or name-only seed is withheld, attached notes are screened again when published, current HEAD, profile, policy and caller privacy are rechecked across awaits, and a PII task or private turn starts no code query. `ahub doctor --stale-anchors` (#684 phase 3) only reads: it never edits, deletes or repairs a note, and an anchor whose task record is now private, foreign or missing stays unknown.
|
|
32
32
|
|
|
33
|
-
`hub_memory_correct` requires `memory_correct` and proposes a machine-local candidate after the same project/privacy checks, including revalidation after asynchronous screening. It cannot accept or alter a note. Public review failures with unmet items may also propose candidates through Tasks, and `ahub ask --remember` answers and context checkpoints are saved only as candidates, never as curated notes (agent-mem#13 P4). Candidates and usage counters stay at `M/candidates.db`, are not synced/imported, and never enter recall, briefs, ask or graph evidence. A person accepts shared-v1 candidates through the package CLI's TTY/policy checks; the accepted result is an ordinary v1 note, while the sidecar remains local. Generation `amem review`
|
|
33
|
+
`hub_memory_correct` requires `memory_correct` and proposes a machine-local candidate after the same project/privacy checks, including revalidation after asynchronous screening. It cannot accept or alter a note. Public review failures with unmet items may also propose candidates through Tasks, and `ahub ask --remember` answers and context checkpoints are saved only as candidates, never as curated notes (agent-mem#13 P4). Candidates and usage counters stay at `M/candidates.db`, are not synced/imported, and never enter recall, briefs, ask or graph evidence. A person accepts shared-v1 candidates through the package CLI's TTY/policy checks; the accepted result is an ordinary v1 note, while the sidecar remains local. Generation `amem review` is supported since agent-mem 0.7.0, subject to the package CLI's applicable TTY and policy checks. Citation counters are observation proxies, not evidence that an agent understood or used a note.
|
|
34
34
|
|
|
35
35
|
The hub binds the selected root/profile at open. Invalid/missing generation metadata refuses native memory without a shared-v1 fallback; the bus and board continue. Explicit checks at hub operation entry and before sidecar writes stop later stale calls, but are not atomic against a selector change during an in-flight core operation. Migration therefore requires quiescing consumers and closing handles before changing selection, then reopening through the supported lifecycle. A public core pre-write hook is the upgrade path for atomic in-flight cutover.
|
|
36
36
|
|
|
@@ -216,7 +216,9 @@ same native session; a new session, a changed policy or a stopped, crashed or
|
|
|
216
216
|
restarted peer starts from zero, and an ordinary allow never spends it. The
|
|
217
217
|
limits are the point of the visibility rule: the hub enforces only a call it
|
|
218
218
|
sees before it runs. A Claude session without the hub hook or not yet proven to
|
|
219
|
-
have run it
|
|
219
|
+
have run it (the project-neutral profile hook that `ahub setup` may install, #999
|
|
220
|
+
T2, acts only for a session that inherited `AGENTHUB_STATE_DIR` and `AGENTHUB_PEER_ID`
|
|
221
|
+
without `AGENTHUB_LAUNCH_ID`, and its presence is configuration, not proof it ran), an `--unattended` Claude launch (it skips hooks), an ACP agent in a
|
|
220
222
|
native mode that sends no request for a call it allows (Kimi's `yolo` and
|
|
221
223
|
`auto`), a call whose payload the agent did not report, a Codex command its
|
|
222
224
|
native policy runs without sending a request (so status lists Codex as not
|
|
@@ -254,6 +256,66 @@ subcommand's argument). What the policy still cannot see is the content of a
|
|
|
254
256
|
variable used as the whole command, an alias or function, a script the command
|
|
255
257
|
runs, and text read from a file or stdin.
|
|
256
258
|
|
|
259
|
+
The floor policy (#999 T3). `action_policy.floor: true`, settable only in the
|
|
260
|
+
untracked `.agenthub/config.local.json` like the rest of `action_policy` and off by
|
|
261
|
+
default (the person's `ahub autonomy on` writes it and `off` removes it), stops the policy from asking about ordinary development work: an unforced
|
|
262
|
+
`git push` of a literal non-default branch to `origin` from inside the execution
|
|
263
|
+
root, and `gh pr create|comment|review|merge` in the project's own repository
|
|
264
|
+
(never `--admin`, never another repository by flag or URL). Everything else the
|
|
265
|
+
policy asked about still asks: force and delete pushes, the default branch and tags,
|
|
266
|
+
`gh release`, package publish, deletion outside the execution root, writes to
|
|
267
|
+
other repositories, mail and posting. It is a weakening, so every status policy row
|
|
268
|
+
carries the existing `attention` notice. The floor reads only host facts (the
|
|
269
|
+
default branch from the authoritative GitHub remote read, never a local
|
|
270
|
+
`origin/HEAD` or the agent's text; the project's own repository; whether a name is
|
|
271
|
+
also a local tag; the checked-out branch) and treats a missing or unproven fact as
|
|
272
|
+
the strict case: until the default branch is read, a failed read, a directory that
|
|
273
|
+
is not provably the project's own checkout (a nested clone has another `origin`), a
|
|
274
|
+
wrapper the classifier cannot read, `-c`, `--git-dir`, an expansion or an unplaceable
|
|
275
|
+
`cd` all keep the call asking. The flag is the only switch: facts alone never widen
|
|
276
|
+
a policy that did not set it, and a disabled policy is unchanged. With the floor on,
|
|
277
|
+
`publish`, `external-write` and `destroy` ask whatever `always_ask` says, so a coarse
|
|
278
|
+
selector cannot hand them back. It affects approval prompts only: the sandbox, deny
|
|
279
|
+
list, PII rule, path guard and trust checks do not change. The hub enforces the
|
|
280
|
+
floor only where it sees the call first. For Codex in `never-ask`, Kimi in `auto`,
|
|
281
|
+
Jcode and a Claude session without the hub hook it cannot, and the floor rests on
|
|
282
|
+
guards the hub does not verify (GitHub branch protection on the default branch,
|
|
283
|
+
release only through `ahub sdlc authorize-release` and the OIDC tag workflow, the
|
|
284
|
+
peer's native sandbox): `ahub status` names them on that peer's row and says they
|
|
285
|
+
are not verified, never that they hold. A missing guard, such as an unprotected
|
|
286
|
+
default branch, is the person's to find and report. The floor also reads, per call, the
|
|
287
|
+
checkout's effective push destinations (`remote.origin.pushurl`, `url.*.pushInsteadOf`, every URL),
|
|
288
|
+
the settings that add refs to a push (`push.followTags`, `push.pushOption`,
|
|
289
|
+
`push.recurseSubmodules`, `remote.origin.push`, `remote.origin.mirror`) and gh's default
|
|
290
|
+
repository (`gh repo set-default`, a second remote), and a call it cannot prove is
|
|
291
|
+
ordinary asks. A wrapper takes the floor away from a shell it wraps, and a command earlier
|
|
292
|
+
on the same line that can change those facts ends the floor for the rest of the line. That
|
|
293
|
+
includes `git fetch` (it creates tags and moves remotes, so a tag probe read before it is stale),
|
|
294
|
+
`bun test` and the other builds and test runs (they run the agent's own code), and `rg --pre`.
|
|
295
|
+
A `find` with any action that writes a file or runs a command (`-fprintf`, `-fprint`, `-fprint0`, `-fls`,
|
|
296
|
+
`-exec`, `-execdir`, `-ok`, `-okdir`, `-delete`) is not inert either: `-fprintf .git/config` can rewrite a
|
|
297
|
+
push destination. A glob anywhere in a `find` or `rg` call also ends the
|
|
298
|
+
floor, because the shell expands an unquoted one before the program runs and a file named `-fprintf` or
|
|
299
|
+
`--pre` becomes that option. `tree`, `file` and `git diff|log|show|blame --output` write named files and are
|
|
300
|
+
not inert. An option's value that holds an expansion or a glob character (`--body $(cat f)`,
|
|
301
|
+
`--body */*`) keeps the call asking, because it can become several words, one of them a real
|
|
302
|
+
`--repo`. That glob test does not see quoting, so a prose `--body` holding `*`, `?` or `[` also asks
|
|
303
|
+
(a `ponytail:` in `src/hub/action-class.ts` names the upgrade).
|
|
304
|
+
A `gh pr` call is read option by option: a value belongs to its option (`--body`, `--title`,
|
|
305
|
+
`--subject` and the rest), so a pull request URL inside a body is text and never selects a
|
|
306
|
+
repository, and an option the floor does not know keeps the call asking. Every authority read
|
|
307
|
+
shares one budget, the project's origin query included. A project whose `config.json` or
|
|
308
|
+
`config.local.json` names `action_policy.floor`, true or false, in the raw file refuses an older
|
|
309
|
+
recovery target, even where the key is ignored (a shared file may not set `action_policy`) or
|
|
310
|
+
normalized away (`false`), because the older parser rejects the key before that rule runs.
|
|
311
|
+
What it cannot see: the environment of a native agent's own process. The hub reads its own
|
|
312
|
+
`GH_REPO` and `GH_HOST`, but a `GH_REPO` exported in a Claude, Codex or Kimi shell never
|
|
313
|
+
reaches the hub. So for every peer whose environment the hub does not build (Claude,
|
|
314
|
+
Codex, Kimi, Qwen, Jcode, agy) an unqualified `gh pr` call is never treated as the
|
|
315
|
+
project's own and asks; only an explicit own `--repo`, `-R` or pull request URL runs.
|
|
316
|
+
The local worker and Pi run commands in an environment the hub builds, so for them the
|
|
317
|
+
unqualified call is judged on the facts the hub can read.
|
|
318
|
+
|
|
257
319
|
The `gh` classes (#636, #756, #762). `publish`: `gh release create|upload|edit|delete`.
|
|
258
320
|
`external-write`: `gh issue|pr|gist create|comment|edit|close|reopen|merge|review|delete`,
|
|
259
321
|
`gh run rerun|cancel|delete`, `gh workflow run|enable|disable`, and `gh api` with a
|
|
@@ -270,12 +332,15 @@ unclassed. `gh repo delete|archive|edit`, `gh secret` and `gh variable`
|
|
|
270
332
|
|
|
271
333
|
Config defaults apply automatically only up to ask-when-needed. A never-ask
|
|
272
334
|
request from an applied untracked config is held as ask until the person
|
|
273
|
-
confirms its source file in the console at hub start
|
|
335
|
+
confirms its source file in the console at hub start, unless retained autonomy
|
|
336
|
+
consent matches it (next paragraph). Git-tracked mode defaults
|
|
274
337
|
are filtered by config trust and never offered for confirmation. This confirmation reduces accidental startup
|
|
275
338
|
relaxation; an agent with unrestricted shell/token access can still impersonate
|
|
276
339
|
the console. Merged and generated Claude settings live in 0600 state files and
|
|
277
340
|
are passed by path, keeping caller settings out of process argv.
|
|
278
341
|
|
|
342
|
+
Project and machine autonomy (#999 T1, T2). `ahub autonomy on|off` is person-only: the daemon takes it from the console role only, `--live-test` refuses it, and no hub tool or conductor path reaches it; the hostile-agent boundary above still applies. After one confirmation bound to the preview it writes the shipped preset to the untracked `config.local.json` only: task flow switches, `action_policy.floor: true`, start defaults, never-ask for the kinds and forms the preset names, ask-when-needed for the headless Claude implementer, and no mode for an unmanaged kind or variant. It also writes the machine opt-in `~/.agenthub/defaults.json`, an owner-only 0600 regular file outside every project root; any other form is ignored with a notice and keeps ask. Retained consent is an HMAC record under the hub home, bound to the project id and each peer's exact never-ask value from `config.local.json`. Only the daemon creates it: on the console `on` path, and at startup when the trusted machine opt-in exists and the local value equals the preset plan for the environment. A `config.json` value, `_autonomy` metadata, a config marker, an offline `--all-projects` write and `ahub init` inheritance never create it. Under the machine opt-in, a preset-matching never-ask that any process writes to `config.local.json` is consented at the next hub start: the person's opt-in is that consent. A changed mode invalidates that peer's consent, and `off` and preset undo clear it. The sandbox, deny, PII, path-guard and trust rules and the floor's asking classes are unchanged.
|
|
343
|
+
|
|
279
344
|
Claude ask-when-needed grants only resolving project file targets outside
|
|
280
345
|
.agenthub, .git, .claude, .codex, .qwen, .kimi, .kimi-code, .jcode, .pi, .mcp.json, .agents, .agent, _agents, _agent, .gemini, .grok and .claude.json. Symlink escapes, protected aliases, traversal and
|
|
281
346
|
unresolved wildcard targets receive no decision. Names are compared folded (letter case, compatibility forms and
|
|
@@ -352,6 +417,33 @@ This is an honest default against accidental impersonation and injected commands
|
|
|
352
417
|
It does not make the token inaccessible to an agent with unrestricted project
|
|
353
418
|
shell access. The existing OS sandbox and loopback authentication boundaries apply.
|
|
354
419
|
|
|
420
|
+
Distinguishable sessions and sender provenance (#1036, #1037) make a mix-up
|
|
421
|
+
between two sessions of one person visible; they are not authentication. A tools
|
|
422
|
+
hello that names a Claude channel peer while presenting a launch id that is not
|
|
423
|
+
that peer's current launch record, or that asks to be a session (the channel
|
|
424
|
+
plugin does when its process has no project of its own and inherited another
|
|
425
|
+
project's state directory), is bound to an id the daemon mints, `<peer>~<origin>`.
|
|
426
|
+
Such a session may send, read status and run the read task ops in
|
|
427
|
+
`SESSION_READ_OPS`; every other message is refused with `this session is <id>,
|
|
428
|
+
not this project's <peer>`. It is never a bus peer: no delivery, no push, none of
|
|
429
|
+
the peer's roles, and it replaces nobody. Its envelopes carry `session: true` and
|
|
430
|
+
recipients see the minted id in the header; an origin label is kept only when it
|
|
431
|
+
already is a plain lowercase token, so no path or sentence reaches a sender name.
|
|
432
|
+
Every envelope event and one `attach` event per admitted peer or tools hello in
|
|
433
|
+
`events.jsonl` carry `origin`: the connection's role, peer and launch or instance
|
|
434
|
+
id, never a path, cwd, token or body. The channel plugin applies the CLI's
|
|
435
|
+
ownership proof to an inherited `AGENTHUB_STATE_DIR` instead of trusting it, and
|
|
436
|
+
does not join a hub whose `status.json` lacks `session_identity` as a session,
|
|
437
|
+
since that hub would bind the real peer id. What this does not guarantee: a hello
|
|
438
|
+
with no launch id still becomes the peer (the native facts hook, helpers and a
|
|
439
|
+
person's shell connect that way), a launch id is an environment value every child
|
|
440
|
+
of the session inherits, not a secret, and a same-user process that reads the
|
|
441
|
+
control token can present the right id, present none, or connect as the console.
|
|
442
|
+
Nothing that connected before is refused. A recipient weighs a `<peer>~<origin>`
|
|
443
|
+
sender as another session's message; a plain peer id proves no more than it did
|
|
444
|
+
before. A restart, upgrade or recovery onto a daemon without `session_identity`
|
|
445
|
+
is refused while an unsettled delivery holds a session's message.
|
|
446
|
+
|
|
355
447
|
Steering tools require an explicit `conductor` role, independent of default-allow
|
|
356
448
|
capabilities. Only one peer may hold that role. Two task-scoped exceptions need
|
|
357
449
|
no role: a task's current owner and reviewer may read its public view with
|
|
@@ -471,7 +563,7 @@ Please report vulnerabilities privately through GitHub's "Report a vulnerability
|
|
|
471
563
|
|
|
472
564
|
- 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.
|
|
473
565
|
- `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.
|
|
474
|
-
- 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.
|
|
566
|
+
- 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 unless retained autonomy consent matches it. 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.
|
|
475
567
|
- 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.
|
|
476
568
|
- 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.
|
|
477
569
|
- 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.
|
|
@@ -500,6 +592,42 @@ not inherit state, unattended or gateway URL/key overrides from the session that
|
|
|
500
592
|
opened the manager. Native memory is deliberately not a project-isolation boundary:
|
|
501
593
|
its existing basename/worktree aliases and provider accounts can remain shared.
|
|
502
594
|
|
|
595
|
+
## acon console plugin (#935, #1012)
|
|
596
|
+
|
|
597
|
+
`@staix/agent-hub/ui-plugin` runs inside the agent-console (`acon`) process as the
|
|
598
|
+
same user. It holds no credential of its own: it reaches project hubs only through
|
|
599
|
+
the multi-project manager's listing and forwarding above, which connect as the
|
|
600
|
+
console with each project's control token, so it has exactly what `ahub ui --all`
|
|
601
|
+
has. Every action must be on the manager's closed list and every view one of the
|
|
602
|
+
seven view names, and there is no generic control proxy, task-detail read, shell,
|
|
603
|
+
file access or caller-chosen command.
|
|
604
|
+
|
|
605
|
+
- **Registration.** `ahub setup` writes `console/plugins/ahub.json` under the hub
|
|
606
|
+
home (directory 0700, file 0600, atomic replace) with the module path and its
|
|
607
|
+
sha256; acon refuses to load a module whose hash no longer matches.
|
|
608
|
+
- **Confirmation.** Outward or destructive actions (`stop`, `permit`, `send`,
|
|
609
|
+
`setting`, `setting_undo`, `setting_reload`, `config_edit_apply`, `start_peer`,
|
|
610
|
+
`backlog_split` and the inbox's Allow once and Deny) first answer `needsConfirm`
|
|
611
|
+
with a preview and a fingerprint, a sha256 of the request's own fields; the call
|
|
612
|
+
runs only when it comes back with `confirmed` and that fingerprint. The
|
|
613
|
+
fingerprint binds a confirmation to one exact request, so a changed request needs
|
|
614
|
+
a new confirmation; it is not a secret, and the confirm step is acon's host UI
|
|
615
|
+
contract, not authentication. A process that can load the module already has the
|
|
616
|
+
console's reach, and the hostile-agent boundary above applies unchanged.
|
|
617
|
+
- **Inbox.** Items are the pending approvals and the in-review tasks whose reviewer
|
|
618
|
+
is the person, read from the same redacted dashboard snapshot: a local-worker or
|
|
619
|
+
Pi approval shows only the fixed private stub and offers Deny, and Allow once for
|
|
620
|
+
it is refused with "answer this approval with ahub permit". Allow once and Deny
|
|
621
|
+
resolve to the hub's own `permit` with an option the pending request offers,
|
|
622
|
+
looked up again at the time of the call; nothing else is reachable from an item.
|
|
623
|
+
Task titles are the public view's, so a PII task shows the `[pii]` stub.
|
|
624
|
+
- **Space.** The Space scene is built only from the office model the dashboard
|
|
625
|
+
already allowlists (#264): peer ids, conditions and counts, task ids, classes,
|
|
626
|
+
states, owners and public titles, approval counts, and overlap, conflict, cohort
|
|
627
|
+
and message link ids. No envelope body, task detail, approval title, reason, path
|
|
628
|
+
or symbol reaches it, a PII task never joins a link, and the scene is trimmed to
|
|
629
|
+
acon's caps. `test/acon-plugin.test.ts` pins this with a marker fixture.
|
|
630
|
+
|
|
503
631
|
## Peer roster authority
|
|
504
632
|
|
|
505
633
|
The running peer roster changes only through console-only settings requests.
|
|
@@ -675,7 +803,7 @@ A terminal Claude implementer or verifier instance (#525-5) owns write-class tas
|
|
|
675
803
|
|
|
676
804
|
A planner-only instance (#525-6: `planner` without `implementer` or `verifier`) plans on the project checkout, so a declared `worktree` is refused. A Codex planner's `thread/start` and `thread/resume` are requested read-only, and a thread is adopted only when Codex's reply positively confirms a read-only sandbox; an unconfirmed or malformed reply refuses that RPC and closes the link before the hub adopts the thread or delivers work, and the current roles are rechecked before each injection. This is an initial ceiling, not confinement of the person's attached TUI, which can change its native sandbox later. A declared planner-only Claude channel instance gets no automatic grant from its facts hook for any tool but Read, LS, Glob and Grep, whatever the permission mode.
|
|
677
805
|
|
|
678
|
-
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)
|
|
806
|
+
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), [on 2.1.314](verification/2026-10-31-claude-implementer-2.1.314.md) and [on 2.1.316](verification/2026-10-31-claude-implementer-2.1.316.md); it is not sandbox proof, and a real account, model and person interaction remains a live leg.
|
|
679
807
|
|
|
680
808
|
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.
|
|
681
809
|
|
|
@@ -695,6 +823,95 @@ Person release authorization (#481 S6). `ahub sdlc authorize-release <version>`
|
|
|
695
823
|
|
|
696
824
|
SDLC metrics and control bands (#481 S7). `ahub sdlc metrics` and `hub_sdlc_metrics` are planner or person only, read-only, and refused during a private or PII turn. A private task is counted as withheld and read for nothing else; the report calls no model and makes no GitHub read. Band breaches are written only by the daemon's evaluator through Tasks, never while it is stopping, recovering or has a storage error. A `diagnose` or `propose` card is a backlog task (`triage` or `plan`) whose text is fixed words, numbers and task ids, never task text; it passes the PII signal check and the candidate-text screen before it is written, and no GitHub issue is created. A `log` breach is one hub notice, remembered in the board's `band_notices` table as a marker (`<metric>:#<task>:log`, checked against a fixed pattern) and a time, nothing else.
|
|
697
825
|
|
|
826
|
+
## Research campaigns and study tools (#930, #932)
|
|
827
|
+
|
|
828
|
+
Campaign authority is the person's. `ahub study draft|revise|approve|approve-protocol|close`
|
|
829
|
+
are Console in `classifyPeerCommand`, and the daemon answers the `study_*` operations
|
|
830
|
+
to the console role only; `ahub study show` is the only form an agent shell may run.
|
|
831
|
+
Agents use hub tools instead: `hub_study_draft` and `hub_study_revise` take the
|
|
832
|
+
configured planner's planning authority and store drafts only, `hub_study_show` is
|
|
833
|
+
read-only for every connection, and `hub_study_handoff` takes only the current
|
|
834
|
+
in-progress owner of the bound unit task. No tool approves a campaign or a protocol,
|
|
835
|
+
closes a campaign or attests anything, and a unit's `purpose` or a research role
|
|
836
|
+
grants no authority: dispatch, ownership and review use the existing routing and
|
|
837
|
+
role admission.
|
|
838
|
+
|
|
839
|
+
- **Approval creates work, nothing else does.** Only the person's `ahub study approve`
|
|
840
|
+
creates unit tasks, as Backlog cards the planner offers through its ordinary pull.
|
|
841
|
+
The approval revalidates and screens the revision before a single transaction
|
|
842
|
+
that rechecks it is still the latest draft; a changed campaign, task or
|
|
843
|
+
dependency refuses whole. A campaign never assigns an owner or approves a task.
|
|
844
|
+
- **Dispatch is rechecked at the write.** Before a unit task is offered, assigned,
|
|
845
|
+
pulled or handed to another owner, after any asynchronous screen, the hub rereads
|
|
846
|
+
the stored campaign and holds the task when the revision is not the latest
|
|
847
|
+
approved one, the deadline has passed or the stored stop policy is exceeded. A
|
|
848
|
+
contract's `budget.hardCap` is checked against that exact budget id at approval
|
|
849
|
+
and again at every dispatch and owner change; another budget covering the task
|
|
850
|
+
proves nothing. A hold never stops a running native turn.
|
|
851
|
+
- **Provenance is never upgraded.** Experiment fields are `declared` unless the hub
|
|
852
|
+
measured them itself from a finished check receipt for the same task and commit,
|
|
853
|
+
or by hashing a deliverable through the #509 path policy; an agent-supplied hash
|
|
854
|
+
is never host evidence. `ahub study approve-protocol` binds approval to one exact
|
|
855
|
+
protocol hash and evaluator identity hash, and nothing copies it to a changed
|
|
856
|
+
protocol. Environment descriptors are refused when they contain assignment
|
|
857
|
+
forms, Bearer credentials or known secret shapes; that screen detects those forms
|
|
858
|
+
only and does not prove a secret is absent. This release exposes no hub tool for
|
|
859
|
+
recording experiments, and runner attestation (`research.runners`) is not
|
|
860
|
+
implemented yet.
|
|
861
|
+
- **Privacy.** A campaign whose parent task is PII, or that names a private source,
|
|
862
|
+
is private, and so is an attempt with a private task, input or output path.
|
|
863
|
+
Public views keep ids, counts, states and provenance and stub every question,
|
|
864
|
+
scope, text, path, excerpt and metric name as `[pii]`; source text is never stored
|
|
865
|
+
in a campaign or handoff, only source ids. Study tool arguments and results are
|
|
866
|
+
excluded from hub-managed memory capture, and raw contract, handoff and attempt
|
|
867
|
+
text never enters task history, notices or memory.
|
|
868
|
+
- **Recovery.** A restart, upgrade or recovery onto a daemon lacking the matching
|
|
869
|
+
`research_*` feature is refused while campaign, dispatch, re-offer, experiment or
|
|
870
|
+
comparison rows exist, or while `research.runners` is configured.
|
|
871
|
+
|
|
872
|
+
## Cross-project task handoff (#604, #1030)
|
|
873
|
+
|
|
874
|
+
A handoff crosses a project boundary, but not a security boundary: both projects
|
|
875
|
+
belong to the same machine user. The origin daemon reads the target's control
|
|
876
|
+
token file and connects to the target's daemon as the `console` role through
|
|
877
|
+
`ControlClient`, like every control message; the hello checks the target's project
|
|
878
|
+
root and instance. It sends that connection only the three handoff messages, and
|
|
879
|
+
it has no reach a same-user process lacks (see "What is not defended").
|
|
880
|
+
|
|
881
|
+
- **Who may hand off.** `ahub task handoff` is Console in `classifyPeerCommand`;
|
|
882
|
+
agents use `hub_task_handoff`, admitted for the person, a configured planner or
|
|
883
|
+
the conductor only, and never for a remote room peer. The target is chosen only
|
|
884
|
+
from the machine registry by canonical root or id, this project is refused, and
|
|
885
|
+
the target's own `status.json` must advertise `task_handoff` and name no other project. A target
|
|
886
|
+
that is not running is refused with the action; nothing is queued.
|
|
887
|
+
- **No authority in the target.** The target answers `task_handoff`,
|
|
888
|
+
`task_handoff_event` and `task_handoff_states` to a console connection only, and
|
|
889
|
+
refuses a new handoff while it recovers or shuts down. The task is an ownerless proposal
|
|
890
|
+
its own hub makes and routes. The origin project and peer are a record in
|
|
891
|
+
`task_handoff_origin` and a line at the end of the detail, never an actor, a
|
|
892
|
+
roster peer or a role: the origin cannot accept, review, approve, assign or
|
|
893
|
+
cancel there.
|
|
894
|
+
- **What crosses.** To the target: a title (300 characters), a class, a detail
|
|
895
|
+
(4,000), up to 16 repository-relative paths, a spec issue number, and the origin
|
|
896
|
+
project id, peer id and task id, at most 8 KiB in all. The request is parsed
|
|
897
|
+
closed on both sides: an unknown field, an accessor or a control character is
|
|
898
|
+
refused, never dropped, and the target sanitizes before it stores. No memory,
|
|
899
|
+
evidence, turn content or task history crosses. Back to the origin: an event
|
|
900
|
+
name, a PR number and a full 40-hex head, never the target's title, notes or
|
|
901
|
+
other text; the restart catch-up answers only for tasks whose recorded origin
|
|
902
|
+
is the asking project, with the same fields. The origin peer gets that as a
|
|
903
|
+
hub-written `kind: task` message.
|
|
904
|
+
- **Privacy.** The origin refuses during a private turn and for a `[pii]` task
|
|
905
|
+
named as the source, before anything leaves. The text itself is checked by the
|
|
906
|
+
target, against the target's PII patterns and, when the target's model screen is
|
|
907
|
+
on, that screen under the target's routing; a match or an uncleared verdict
|
|
908
|
+
refuses the whole handoff and nothing is stored on either board. The origin's
|
|
909
|
+
own patterns are not applied to the text, so keep private text out of a handoff.
|
|
910
|
+
`hub_task_handoff` and `hub_task_handoffs` arguments and results are excluded
|
|
911
|
+
from hub-managed memory capture.
|
|
912
|
+
- **Recovery.** A restart, upgrade or recovery onto a daemon lacking
|
|
913
|
+
`task_handoff` is refused while either handoff table holds a row.
|
|
914
|
+
|
|
698
915
|
## Bounded task specs and isolated checks (#418)
|
|
699
916
|
|
|
700
917
|
`hub_task_spec` reads only the GitHub issues named by a current owner's or reviewer's task in this project's origin repository, including the validated canonical GitHub intake source when its title/body has no issue token. Bare `#N` references count only in the title; detail references require `owner/repo#N` or `https://github.com/owner/repo/issues/N`. Bare detail numbers denote hub tasks, and arbitrary file paths are not parsed as issue references. A qualified reference cannot begin after a slash or a name character, so `src/hub/tasks.ts#12` is a file path. Routing uses this same parser. Without a GitHub project identity, bare title numbers do not require spec access, but explicit qualified/URL references, `refs.repo` and imported GitHub identity still fail closed. `refs.repo`, which `hub_task_propose` and `hub_task_accept` take since #476 to scope overlap per repository, is metadata only: it grants no file access or execution root, and a spec read for a task naming another repository refuses. The daemon resolves authenticated and container executables outside the project, pins their absolute paths and uses fixed arguments; repository-writable PATH shims are refused. Runner Git resolves its host executable through a restricted PATH. For specs, the model receives no credential or arbitrary authenticated URL capability. Foreign/transferred issues, more than three references, bodies over 64 KiB and aggregate results over 80 KiB fail explicitly. Spec fetches share a 25-second budget inside the control client's 30-second cap; each gh call remains bounded to 15 seconds. Current PII, secret and private-path policy withholds the complete spec rather than returning a partial contract. PII turns and PII tasks refuse these tools. The independent `spec_read` and `check_run` capabilities apply when a peer has a narrowed capability list. Arguments and results are excluded from capture. Proven deterministic pre-effect guards return PreEffectToolRefusal so Pi settles the exact refusal receipt; generic transport/storage failures and failures after durable receipt/output changes remain uncertain, never reclassified from error text.
|
|
@@ -1673,6 +1673,17 @@ This slice provides the pure classifier, machine-local policy, host-only per-ses
|
|
|
1673
1673
|
- **Remaining adapters (#636 slice 3).** Codex command approval RPCs (v2 command execution, excluding writeStdin, and legacy exec argv) consult the shared gate before native forwarding. A classed request is answered only once, bound to its link/thread/turn; cancellation, disconnect and stale answers deny. Unknown payloads and other approval schemas retain native handling. This proxy cannot intercept a native call that emits no request, so status never claims complete Codex enforcement. Local/Pi tools pass parsed arguments into their existing permit boundary and classify before mode or tool-wide grants. Pi's persisted call receipt encloses the gate, so replay spends no allowance twice. Only verified person-authored idle shell requests retain their existing bypass.
|
|
1674
1674
|
- **Rendered approvals.** Public/native command views are privacy-screened before rendering. A cut or withheld view offers deny only and cannot authorize even one invocation or a class budget. Local/Pi use their exact full host-generated tool title with a class line on the person's on-prem console, PII turns included; logs, public status, other peers and dashboard projections still withhold it. The same explicit class-budget choice, session/owner revocation and final-answer fence apply to received Codex, local and Pi approvals. Managed Claude forms keep asking through their native prompt path.
|
|
1675
1675
|
|
|
1676
|
+
## Amendment: the action floor (#999 T3)
|
|
1677
|
+
|
|
1678
|
+
A machine-local boolean `action_policy.floor` (default false, registered, applies at once, the same local-only trust rule as the other `action_policy` keys) lets ordinary development work run without a prompt. Absent or false, behaviour is exactly the always-ask policy above. The key is kept in the parsed configuration when present, true or false, so a controlled recovery can tell that the project's file names it: a target without the daemon feature `action_policy_floor` rejects the unknown key (the check reads the raw `config.json` and `config.local.json`, so an ignored shared-file key and `floor: false` count too), and `prepare`, `commit` and a target-checking `inspect` refuse it before any effect.
|
|
1679
|
+
|
|
1680
|
+
- **What runs.** With `enabled` and `floor` both true, an unforced `git push origin <branch>` of a non-default branch that is not also a tag, and `gh pr create|comment|review|merge` in the project's own repository (never `--admin`), do not ask. Nothing else changes: force and delete pushes, the default branch and tags, `gh release`, package publish, deletion outside the execution root, writes to another repository, mail and posting still ask, and a call the classifier cannot decide keeps the strictest class it might reach.
|
|
1681
|
+
- **Floor classes ask whatever `always_ask` says.** With the floor on, `publish`, `external-write` and `destroy` are selected whether or not `always_ask` lists them, so a coarse or empty selector cannot hand the floor's own classes back. `install` still follows `always_ask`.
|
|
1682
|
+
- **Facts come from the host.** The default branch (authoritative remote read, never a local `origin/HEAD`), the project's own repository, whether a name is also a tag, the checked-out branch and that the directory is the peer's own checkout are read by the daemon, never from the agent's text. The push side reads every effective push URL and the settings that add refs to a push (`push.followTags`, `push.pushOption`, `push.recurseSubmodules`, `remote.origin.push`, `remote.origin.mirror`); a qualified source (`refs/tags/x:dst`) is judged as the ref it names. An unqualified `gh pr` call is proven own only for a peer whose command environment the hub builds (`trust.hubSandboxed`: Pi, the local worker); for every other peer a `GH_REPO` in its shell is invisible to the hub, so only an explicit own `--repo`, `-R` or pull request URL runs. A command earlier on the same line that can change those facts ends the floor for the rest of the line.
|
|
1683
|
+
- **Bounded work.** One classification reads at most 16 distinct facts through one shared budget (every git read and the origin query), answers an identical read once, and answers none after 2.5 s; past the bound a fact is unknown and the call asks.
|
|
1684
|
+
- **Status.** Where the hub cannot see a call first (Codex in `never-ask`, Kimi in `auto`, Jcode, a session without the hub hook) the row says `floor on:` and names the structural guards the floor rests on (branch protection on the default branch, release only through `ahub sdlc authorize-release` and the OIDC tag workflow, the peer's native sandbox) as not verified by the hub, never as holding. Every policy row carries the existing `attention` notice, because the floor asks less than the default.
|
|
1685
|
+
- **Not changed.** The floor affects approval prompts only: no sandbox, deny list, PII rule, path guard or trust check changes.
|
|
1686
|
+
|
|
1676
1687
|
## Amendment: approval notifications and timeout (issue #5)
|
|
1677
1688
|
|
|
1678
1689
|
- A waiting permission request raises one macOS notification (`osascript`) with
|
|
@@ -3234,6 +3245,24 @@ acts on the stopped state directory.
|
|
|
3234
3245
|
- Control protocol: `settings_get`, `settings_set`, `settings_undo` and `settings_preview` are new console-role requests, and `ui` takes an optional `settings` flag and echoes it; an older hub answers the requests as unknown, and the CLI refuses a `ui` reply without the echo, so `PROTOCOL` stays 16. The `ui_snapshot` reply gains an optional `settings` object. Events schema stays 1.
|
|
3235
3246
|
- `ahub settings` talks to the running hub (ponytail: a file-only path if setting a start-time value on a stopped hub is ever wanted).
|
|
3236
3247
|
|
|
3248
|
+
## Amendment: project autonomy preset (issue #999 T1)
|
|
3249
|
+
|
|
3250
|
+
- `templates/presets/autonomous.json` defines per-kind permission/start defaults, environment rows and hub settings. The project command uses the T0 confirmation inventory to report unsupported forms and unknown enforcement. T1 wrote no action_policy keys; with T3 (#582) the preset writes `action_policy.floor: true` and `off` removes it, and no other action_policy key. Status reports each of the eight floor classes from `floorEnforcement(policy, actionEnforcement)` (`src/hub/action-policy.ts`, one class table shared with the preset's `floorClasses`): hub-enforced only where the hub sees the peer's calls first and the policy asks for the class, otherwise not enforced with a reason that names the guard the class rests on and says the hub has not verified it; a structural-guard row still needs supplied evidence.
|
|
3251
|
+
- `ahub autonomy on [--yes]` previews the concrete keys and apply timing and takes one person confirmation. `off` removes the fixed shipped preset keys, including prior equal values and later manual edits to those keys, and preserves unrelated keys. Reapplying on covers newly declared supported targets and environment defaults without replacing authenticated original restoration values used by precise undo.
|
|
3252
|
+
- Both CLI mutations and the daemon control request are person-only. Agent shells, delegated live-test calls, ordinary tools and conductor callers cannot toggle the preset. It has no hub tool. The shipped default remains ask, and T1 writes only this project's untracked `config.local.json`; machine defaults, `--all-projects`, init inheritance and profile-home setup belong to T2.
|
|
3253
|
+
- The existing settings store validates the whole candidate, writes the preset in one file replacement and retains one whole-file undo. Off removes the fixed shipped preset key list and retained consent regardless of forged, missing or stale ownership metadata; metadata cannot block the person's off switch. On over unverified metadata overwrites the requested preset keys and issues fresh metadata. Inert `_autonomy` metadata and private ownership/undo witnesses serve precise undo only and grant no approval. Preset undo requires the person's typed `autonomy` confirmation; a valid witness keeps the ordinary changed-file digest fence, while a bad witness falls back to off's key removal and consent clearing and reports that fallback.
|
|
3254
|
+
- The on confirmation is bound to the exact preview plan, source fingerprint and configuration revision. It retains consent under the machine hub home outside the project and execution roots, bound to the stable project id and the digest of each peer's exact confirmed permission-mode values only. Approval records are created only inside the daemon: by the console-role on path, or at startup under the trusted machine opt-in and exact local preset-match checks in T2 below. A config-file marker or unsigned state row proves nothing; missing, unsafe or mismatched records keep the existing per-peer never-ask confirmation. A changed permission value invalidates that peer only; independent action-policy or start-default edits do not invalidate mode consent. Off and preset undo remove retained consent. This is the opted-in exception to the per-start confirmation rule above; other projects and ordinary settings writes keep that rule.
|
|
3255
|
+
- Controlled recovery requires target feature `project_autonomy` while preset metadata or retained mode consent is active. The existing compatibility gate refuses an older target before holds or stops; an off project keeps the older-target path. The control protocol version stays unchanged.
|
|
3256
|
+
- Existing live config keys apply through the scoped reload path. Permission and start defaults apply at the next peer start; active native turns keep their existing policy. `ahub autonomy status [--json]` names each peer's effective mode, the preset's requested next-start mode, and each floor class as hub-enforced, structurally guarded with explicit evidence, or not enforced with a reason. Missing native, plugin, terminal or branch-protection evidence remains unknown. No sandbox, deny, PII or trust rule changes.
|
|
3257
|
+
|
|
3258
|
+
## Amendment: machine autonomy and used Claude homes (issue #999 T2)
|
|
3259
|
+
|
|
3260
|
+
- The person-only on/off CLI writes `defaults.json` under the machine hub home, containing only schema 1 and an autonomy boolean. Reads require an owner-only 0600 regular file owned by the current user, a safe parent, bounded valid data and a location outside project roots; refusal emits a notice and keeps ask. The shipped preset is still the sole settings definition.
|
|
3261
|
+
- `--all-projects` enumerates the registry, previews all on targets before one confirmation and uses existing console requests for live hubs. Stopped projects use `planAutonomy`, `writeAutonomyPreset` and current configuration validation under an exclusive registry claim. No daemon, consent or witness is created by offline apply or init. Offline off may call only the existing consent clear operation under that claim, never confirm or key/witness issue; if clearing fails it names the project and reports failure. Partial failures are explicit. Off uses the fixed removal scope, never ownership metadata, and disables machine inheritance even when some targets are unavailable.
|
|
3262
|
+
- New init inherits only a trusted opt-in and reports it; refresh of an existing project does not reapply it. At daemon startup the machine opt-in is a second host-owned consent trigger: a peer's local never-ask value must equal the environment's preset plan. Shared config values and project metadata cannot mint consent. Explicit Orca/CI evidence selects an environment; the detached daemon otherwise uses terminal defaults. The ordinary per-peer confirmation remains without matching trusted evidence.
|
|
3263
|
+
- Setup/status select an explicit `CLAUDE_CONFIG_DIR`, an existing default home (including the absent default on first use when no other home is selected), or a profile proven in use by live registered Claude launcher evidence. No profile-directory scan or unused-home write is allowed. Scoped plugin commands and a project-neutral managed PreToolUse hook preserve unrelated settings. The fallback accepts only inherited state/peer identity and no-ops for managed launches that already inject their own hook. Configuration presence never establishes live permission enforcement; missing or incomplete inventory remains explicit. Tests use isolated homes and fake native commands.
|
|
3264
|
+
- CLI mutation and delegated-live-test refusals remain in force. No hub tool writes machine defaults. Settings undo retains project scope. Product ask defaults, sandbox, deny, PII and trust rules do not change; T4 supplies the later native receipt.
|
|
3265
|
+
|
|
3237
3266
|
## Amendment: structural configuration from the console and the dashboard (issue #689)
|
|
3238
3267
|
|
|
3239
3268
|
- The registry (`src/hub/settings.ts`) stays closed and unchanged. A second path, `src/hub/config-edit.ts` (plan and write) with `src/hub/config-tier.ts` (the tier rule) and `src/hub/config-edit-host.ts` (authority, confirmation, audit, scoped publish), edits structural keys. Tier 2: `instances.<id>.{kind, roles, start_mode, review_mode, readOnly, worktree, permission_mode}`, `review.{adaptive, min_reviews, pool, rerotate}`, `snapshots.keep`, `approvals.timeout_s`, `context.gate` (all `config.local.json`) and `sdlc.toml` `mode` and `required_checks`. Tier 3: the rest of `NEVER_EDITABLE` and an instance's `jcode_home`, `provider`, `model` and `provider_profile`. `roster`, `tasks.wip_limits` and the other registry keys are refused here with the registry command.
|
|
@@ -4671,6 +4700,12 @@ ahub builds the review brief that the workspace gate used to take from `ocr dele
|
|
|
4671
4700
|
- **Coverage gate.** `hub_review` takes `coverage: { reviewed: [paths], skipped: [{ path, reason }] }`. An approval must account for every reviewable path. The gate follows the lifecycle mode (deviation from the issue, which refuses always): `enforce` refuses before any write, naming the paths, and also refuses when the brief is unavailable (an unknown inventory is never an empty one); `advise` records `Coverage gap (N): ...` or `Coverage unknown: ...` in the verdict note; `off` and a person's own verdict skip it. The commit under review is the task's commit, else the head the verdict names, else the head its lifecycle receipt records. Code work (a task with a commit, branch or PR) for which none of these is known, or whose brief cannot be built, has an unavailable brief. Work with no commit, branch or PR is exempt, with nothing to account for. So is a `Tasks` built without a brief provider (library use and tests; the daemon always provides one). One head throughout: the head the verdict names, or the `reviewedHead` it would inherit from an earlier review, must equal the brief's head (with a PR, the lifecycle approval gate already binds the reviewed head to the PR head). The task's commit and its lifecycle receipt (which holds the inherited `reviewedHead`) are pinned when coverage is checked and rechecked before every write after an await, so a commit or receipt that moves during the verdict is refused.
|
|
4672
4701
|
|
|
4673
4702
|
|
|
4703
|
+
### Owner re-file during review (#1000)
|
|
4704
|
+
|
|
4705
|
+
The owner may submit `hub_task_done` again while the task is `in_review` with a different full lowercase commit SHA. This replaces the delivered artifact, not a reviewer's approval. Same-head, malformed-head, foreign-owner and active-check submissions refuse without effects. Normal privacy, lifecycle, readiness and completion-check gates still apply; a failed admission leaves the old review intact. Concurrent submissions are fenced by their initial history revision, so a delayed submission cannot overwrite a newer delivery.
|
|
4706
|
+
|
|
4707
|
+
The new delivery clears stale observed/reviewed/merge evidence and retains local check evidence only when it already names the new head. Spec acceptance, open Important findings and rejection counts remain. A configured completion check atomically returns the task to `in_progress` with the new head; failure leaves it with the owner, and success publishes the new review. Other state transitions are unchanged. Every verdict, even without an explicit head in off mode, is fenced by the request revision it began reviewing. A refreshed delivery publishes a new request and replaces the old CI watcher; it never records a rejection or escalation.
|
|
4708
|
+
|
|
4674
4709
|
### Optional changed symbols (#682, #684 phase 1a)
|
|
4675
4710
|
|
|
4676
4711
|
`src/hub/code-facts.ts` is the sole runtime caller of optional ripwire. The initial verified CLI contract is 0.6.7: strict `ripwire.pr-context/v1` and `ripwire.at/v1` XML (these verbs do not support JSON), and validated callers/callees JSON keys. Unknown versions, schemas, malformed/capped output, tool failures and timeouts give `unknown`, never a passing gate. The rendered advice has a 16000-byte ceiling (oversize is unknown). Parse metadata values only; discard raw text, owners, author email and source fragments. Calls have a 5 s execution deadline and an owned-process cleanup bound; cache successful values by canonical repo, SHA and query (including the base for a diff). A busy reader gives new distinct queries unknown instead of making review wait in a queue. Name-only graph edges are marked ambiguous and hidden; graph count floors are retained.
|
|
@@ -5159,7 +5194,7 @@ Where the code differs from or fixes a detail the section above leaves open:
|
|
|
5159
5194
|
### Schedules and folder triggers (#512)
|
|
5160
5195
|
|
|
5161
5196
|
- `schedules` is a machine-local key: add it to `MACHINE_LOCAL` in `config-trust.ts`, so only `config.local.json` can set it. A committed config that creates tasks is refused, like the other machine-local keys.
|
|
5162
|
-
- Entry: `{id, every, class, title, detail?, sources?, owner?, queue?: "backlog" | "ready"}`. `every` is `<n>m|h|d` with a 15-minute minimum. `cron`
|
|
5197
|
+
- Entry: `{id, every, class, title, detail?, sources?, owner?, queue?: "backlog" | "ready"}`. `every` is `<n>m|h|d` with a 15-minute minimum. `cron` was refused here until #1017; see "Calendar-anchored schedules (#1017)" below for the calendar form.
|
|
5163
5198
|
- Due checks run on their own interval timer, started whenever `schedules` or folder triggers are configured, independent of `task_sweep.enabled` (the task sweep's timer exists only when that option is on, and it defaults to off). The timer runs no task sweep, and it is re-armed on config reload. `queue` defaults to `backlog`. Missed runs coalesce: a schedule proposes at most one task per tick, and none while its previous task is still open. A global pause or hold skips the tick and coalesces it. Last and next run times live in `hub.db`, and status and the dashboard show the next run.
|
|
5164
5199
|
- Folder trigger: `{id, folder, glob?, class, title, ...}`. `folder` is resolved with `realPath`, must lie inside the project or a declared data path, and is polled on the schedules timer, not watched. Symlinks that leave the folder are skipped. A new file (by `realPath`, size, mtime and SHA-256 recorded in `hub.db`) is registered as a source (#508) and proposes one task that names it. The trigger therefore lands after the source registry.
|
|
5165
5200
|
- `ahub schedule list|run <id>|pause <id>|resume <id>` is person-only for `list`; `run`, `pause` and `resume` are also the configured conductor's since #933 (implementation notes in #512b below).
|
|
@@ -5191,6 +5226,20 @@ Where the code states or fixes a detail the section above leaves open:
|
|
|
5191
5226
|
- **Dashboard.** The UI snapshot carries `schedules` (the same views, ids, enums and times only; absent when none is configured, so an older page and an unconfigured project see no change). The Agents view has a read-only Schedules table, built from the existing `usage-table` classes so no style changes, with a next-run column worded by `scheduleNextLabel`: the time for `scheduled`, `Paused (would be <time>)`, `Due now`, `Due, waiting for task #N to close` or `Due, held`, and `unknown` for anything else, never an invented time. The dashboard has no run, pause or resume control: those stay the person's CLI commands.
|
|
5192
5227
|
- **Not in this slice.** Folder triggers (#512c) and cron remain refused or absent.
|
|
5193
5228
|
|
|
5229
|
+
#### Calendar-anchored schedules (#1017)
|
|
5230
|
+
|
|
5231
|
+
Issue #1017 (the person's), AC1 to AC5. It replaces the "cron not supported yet" refusal and changes nothing about `every`.
|
|
5232
|
+
|
|
5233
|
+
- **Entry.** `{id, every | cron, tz?, class, title, ...}`. Exactly one of `every` and `cron`; both is refused ("give every or cron, not both"), neither is refused, and `tz` without `cron` is refused. The refusals name the entry like every other config error. `cron` is stored with its fields single-spaced; `tz` is stored as the runtime spells the zone (`asia/seoul` is `Asia/Seoul`).
|
|
5234
|
+
- **Expression.** The standard five fields with numbers, lists, ranges and steps (`*/n`, `a-b/n`; a bare `a/n` is refused); day of week 0 and 7 are Sunday. No seconds, no year, no `@` macros, no names, at most 100 printable characters. Both day fields restricted: a day matches if either does; a field that begins with `*`: both must match (the classic rule); the month always has to match.
|
|
5235
|
+
- **Zones.** `tz` is an IANA name validated with `Intl.DateTimeFormat`; without it the machine's own zone. No third-party dependency (AGENTS.md): the zone arithmetic is `Intl` and `Date` only, in `src/hub/schedules.ts` (`parseCron`, `cronNext`, `cronMinGapMinutes`, `scheduleNext`, `scheduleZone`), pure, each taking the time it works from.
|
|
5236
|
+
- **Daylight saving, the one rule.** Fires are found on the zone's wall clock and then placed on the timeline: a wall time the clock skips runs at the first valid minute of that day (`30 2 * * *` on the spring-forward day runs at 03:00), and a wall time the clock repeats runs once, at its first occurrence (`30 1 * * *` on the fall-back day runs at the first 01:30). A wall time never maps to two fires.
|
|
5237
|
+
- **Too frequent (AC3).** `schedulesConfig` refuses an expression whose smallest gap between fires, read on the calendar over one 400-year Gregorian cycle, is under 15 minutes, with the gap in the reason, and an expression that never fires. Daylight saving can move two fires closer on its one change day; a schedule proposes at most one task per pass and none while its previous task is open, so that costs no flood. ponytail: the gap is read on the wall clock, not per zone; upgrade is to walk `cronNext` over the change days of a zone.
|
|
5238
|
+
- **Next run and coalescing (AC4).** The period still starts at the anchor (first sight, then the last run) and the schedule is due once `scheduleNext(entry, anchor)` has passed, where `every` is anchor plus the period and `cron` is the next fire strictly after the anchor. A run restarts the period at `now`, so any number of missed fire times is one run: nothing a restart, a hold or a long outage missed is replayed. Holds, budget pauses, the open-task rule, the pause and resume commands, and the fences are `runSchedule`'s, unchanged. A cron with no matching day in a full 400-year Gregorian cycle (146097 days, after which every date and weekday pattern repeats; a Feb 29 that is a Sunday recurs 28 to 40 years apart, so a shorter scan would call it never) has a next run of `Number.MAX_SAFE_INTEGER` and never runs.
|
|
5239
|
+
- **Where it shows.** A view carries `cron` and `zone` instead of `every`; the task's detail says `Scheduled on the calendar "<cron>" (<zone>)`. `ahub schedule list` has a SCHEDULE column (`cron 30 7 * * 1-5 (Asia/Seoul)` or `every 1h`) and the next run in the schedule's zone with the relative time; `ahub status` and the dashboard read the same way (the dashboard shows the time in the schedule's zone, and says so when the browser does not know it). An `every` schedule reads as before.
|
|
5240
|
+
- **Older targets.** An older daemon refuses a `cron` or `tz` key at load ("cron not supported yet"), so a controlled recovery (`prepare`, `commit` and a target-checking `inspect`) refuses a target without the daemon feature `calendar_schedules` while a raw `schedules` entry in `config.json` or `config.local.json` has either key, before any effect. The raw files decide, so an entry the effective config drops still counts; an unreadable file is unknown, never absent. A project with only `every` entries is unchanged.
|
|
5241
|
+
- **Not here.** Holiday and exchange calendars.
|
|
5242
|
+
|
|
5194
5243
|
#### Folder triggers (#512c) implementation notes (hub task #336: `folder_triggers`, `src/hub/folder-trigger.ts`)
|
|
5195
5244
|
|
|
5196
5245
|
- **Config.** `folder_triggers` is a separate machine-local key (not inside `schedules`, whose entries are time-based), in `MACHINE_LOCAL` and `NEVER_EDITABLE`, validated by the pure `foldersConfig` in the loader and in `config-apply.ts`. Entry: `{id, folder, glob?, class, title, detail?, owner?, queue?}`, at most 16. `glob` is a file-name pattern (letters, digits, `. _ -`, space, `*`, `?`; never a path) that matches whole names; a dot file never matches.
|
|
@@ -6397,3 +6446,7 @@ C2 runs `read` invocations end to end. `write` and `external` stay refused (the
|
|
|
6397
6446
|
The writing domain pack is named `writing-pack` (base `writing`), preserving the existing rule that a pack cannot shadow a profile. `analysis` uses `research` and the existing analyst role; `ops` uses `general`. The catalog adds four original writing roles (editor, translator, style-checker, terminology-checker) and two original ops roles (project-manager, meeting-actions-owner). Their guidance grants no authority, and pack loading does not assign peers automatically.
|
|
6398
6447
|
|
|
6399
6448
|
Each pack supplies criterion ids in its rubric and exactly one advisory check entry. Standalone examples in `templates/checks/` validate writing JSON metadata/literal terminology/mechanical style, rerun an explicitly selected analysis program twice against declared input/expected bytes, or validate meeting-action owner/due/status records. They are shipped with synthetic fixtures, not executed by init or by parsing pack data. The existing person-owned `checks` policy is still the only way to select a task command. Inputs are bounded to 1 MiB, analysis runs to 5 seconds and 1 MiB stdout each, and errors never echo artifact contents. Checks emit criterion ids and a deterministic final result; semantic quality, meeting coverage and undeclared analysis dependencies remain reviewer judgments. `docs/profiles.md` records exact invocations, formats and limits. The role inventory lint compares every template group with the bundled registry rather than assuming the first eleven definitions. No live adoption or native model ingestion is claimed.
|
|
6449
|
+
|
|
6450
|
+
## Amendment: research campaigns, experiment receipts and runner attestation (#930, #932, #1018)
|
|
6451
|
+
|
|
6452
|
+
The implementation contract for #929 R1 and R3 and for #1018 is `docs/specs/2026-11-01-research-campaigns-design.md`: campaign, protocol, attempt and receipt schemas and bounds; the provenance levels `declared`, `host-observed`, `runner-attested` and `person-attested`; the Board tables; the person-only activation, attestation and runner registration; the `ahub study` and `hub_study_*` / `hub_experiment_*` wire names; the `research_records` recovery feature; and the delivery slices (#596 to #602). Slices that must differ amend that document in their own PR.
|
|
@@ -91,5 +91,16 @@ and verified PR. Preserve pre-existing instruction-file changes.
|
|
|
91
91
|
|
|
92
92
|
## Out of scope
|
|
93
93
|
|
|
94
|
-
Cross-
|
|
94
|
+
Cross-machine task routing (broker, #446), fully isolated native memory, new runtime dependencies,
|
|
95
95
|
visual redesign, automatic merging and npm publication.
|
|
96
|
+
|
|
97
|
+
## Cross-project task handoff (#604, agent-hub#1030)
|
|
98
|
+
|
|
99
|
+
Same-machine cross-project routing is no longer out of scope: work that belongs to another project that runs agent-hub is registered on that project's board.
|
|
100
|
+
|
|
101
|
+
- **Target.** `project` names a project in the machine registry (its canonical root or its id). An unregistered target, the caller's own project and a target whose daemon is not running are refused, the last with the person's action (`ahub up` in that project). Nothing is queued.
|
|
102
|
+
- **Who and what.** The person (`ahub task handoff --project <root|id> [<class>] <title...> [--path <p>]... [--issue <n>] [--task <n>] [--detail <text>]`), or a configured planner or the conductor (`hub_task_handoff`). A feature or behavior change first needs a spec issue in the target repository, passed as `issue`; a mechanical chore goes straight to a task. The request is closed (unknown fields refused), bounded (8 KiB whole) and repository-relative for paths.
|
|
103
|
+
- **Transport.** The origin's daemon reads the target's control descriptor (same machine user) and sends `task_handoff` as a console-role control message with origin `{ projectId, peer, taskId? }`. The target's `Tasks` creates an ownerless proposal made by the hub, with the origin recorded in `task_handoff_origin`; its own routing and planner take it from there. The origin peer has no identity or authority in the target: it is a record, never a roster peer.
|
|
104
|
+
- **Follow-back.** On accepted, in review, approved, declined and cancelled the target sends `task_handoff_event`; the origin records it in `task_handoff_outbound` and delivers one `kind: task` envelope to the requesting peer (`agent-hub task 123 approved: PR #N at <sha>`: an event name, a PR number and a full head, never target text). An origin that was down asks the target for the current state of its open handoffs when it starts (`task_handoff_states`). `ahub board --handoffs` and `hub_task_handoffs` list them.
|
|
105
|
+
- **Privacy.** Refused during a private (PII) turn and for a `[pii]` task named with `task`; the target refuses text that matches a PII pattern or that its screen cannot clear. No memory, evidence or turn content crosses beyond the handoff fields.
|
|
106
|
+
- **Compatibility.** The additive daemon feature `task_handoff` (no protocol bump: a new message type is answered as unknown by an older hub, and an optional field is ignored). An older target is refused with a named "upgrade the target hub" error, and a recovery target without the label is refused while either handoff table holds a row.
|