@staix/agent-hub 0.21.0 → 0.23.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 +19 -0
- package/README.md +2 -2
- package/docs/agent-notes/adapters.md +2 -1
- package/docs/agent-notes/budget.md +1 -1
- package/docs/agent-notes/bus.md +5 -2
- package/docs/agent-notes/daemon.md +5 -4
- package/docs/agent-notes/local-worker.md +1 -1
- package/docs/agent-notes/models.md +2 -1
- package/docs/agent-notes/tasks.md +2 -1
- package/docs/agent-notes/tests.md +2 -2
- package/docs/dogfood.md +10 -2
- package/docs/operations.md +192 -6
- package/docs/profiles.md +29 -4
- package/docs/quickstart.md +9 -1
- package/docs/security.md +7 -1
- package/docs/smoke.md +6 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +94 -1
- package/docs/verification/2026-10-30-0.22.0.md +49 -0
- package/docs/verification/2026-10-30-0.23.0.md +70 -0
- package/docs/verification/2026-10-30-445-capability-probes.md +172 -0
- package/docs/verification/2026-10-30-adapter-operational-settings.md +261 -0
- package/docs/verification/2026-10-30-bus-delivery-operational-settings.md +77 -0
- package/docs/verification/2026-10-30-jcode-ac4.md +24 -0
- package/docs/verification/2026-10-30-jcode-ac6.md +52 -0
- package/docs/verification/2026-10-30-model-operational-settings.md +211 -0
- package/docs/verification/2026-10-30-operational-settings-closure.md +1217 -0
- package/docs/verification/2026-10-30-recovery-wait-settings.md +141 -0
- package/docs/verification/2026-10-30-ui-console-operational-settings.md +1845 -0
- package/docs/verified.json +15 -15
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/ROLE-NOTICE.md +1 -1
- package/plugins/agent-hub/server.js +559 -115
- package/src/adapters/acp.ts +48 -2
- package/src/adapters/claude-channel.ts +6 -4
- package/src/adapters/codex-appserver.ts +19 -0
- package/src/adapters/jcode.ts +1 -1
- package/src/adapters/local-worker.ts +3 -3
- package/src/adapters/native-capabilities.ts +50 -0
- package/src/adapters/registry-types.ts +3 -10
- package/src/cli/capabilities-output.ts +30 -0
- package/src/cli/console-state.ts +8 -5
- package/src/cli/console.ts +27 -4
- package/src/cli/help.ts +3 -1
- package/src/cli/identity.ts +4 -1
- package/src/cli/learn-output.ts +38 -0
- package/src/cli/main.ts +28 -2
- package/src/cli/recovery-waits.ts +26 -0
- package/src/cli/smoke-pty-bridge.ts +67 -18
- package/src/cli/smoke-pty.ts +10 -2
- package/src/cli/upgrade-runtime.ts +16 -14
- package/src/cli/upgrade.ts +2 -1
- package/src/hub/board.ts +8 -2
- package/src/hub/bus.ts +117 -17
- package/src/hub/capabilities.ts +215 -0
- package/src/hub/config-apply.ts +8 -4
- package/src/hub/config-trust.ts +1 -0
- package/src/hub/control-client.ts +41 -8
- package/src/hub/daemon.ts +436 -33
- package/src/hub/deferred-hub.ts +2 -2
- package/src/hub/delivery-hints.ts +2 -2
- package/src/hub/delivery-journal.ts +80 -61
- package/src/hub/envelope.ts +12 -2
- package/src/hub/events.ts +2 -0
- package/src/hub/hub-tools.ts +16 -1
- package/src/hub/inference.ts +10 -5
- package/src/hub/invocations.ts +316 -0
- package/src/hub/learning.ts +33 -1
- package/src/hub/operational-settings.ts +180 -1
- package/src/hub/peers.ts +3 -0
- package/src/hub/restart.ts +2 -2
- package/src/hub/room-invocations.ts +145 -0
- package/src/hub/settings.ts +22 -2
- package/src/hub/shipped-packs.ts +5 -2
- package/src/hub/shipped-roles.ts +13 -0
- package/src/hub/tasks.ts +92 -1
- package/src/memory/capture.ts +2 -2
- package/src/models/relay.ts +20 -9
- package/src/rooms/application.ts +67 -2
- package/src/rooms/boundary.ts +43 -11
- package/src/rooms/service.ts +40 -2
- package/templates/checks/analysis.mjs +59 -0
- package/templates/checks/fixtures/analysis-expected.json +1 -0
- package/templates/checks/fixtures/analysis-input.json +1 -0
- package/templates/checks/fixtures/analysis.mjs +7 -0
- package/templates/checks/fixtures/ops.json +4 -0
- package/templates/checks/fixtures/writing-rules.json +4 -0
- package/templates/checks/fixtures/writing.json +5 -0
- package/templates/checks/ops.mjs +40 -0
- package/templates/checks/writing.mjs +40 -0
- package/templates/packs/analysis.pack.json +14 -0
- package/templates/packs/ops.pack.json +14 -0
- package/templates/packs/writing-pack.pack.json +14 -0
- package/templates/roles/NOTICE.md +1 -1
- package/templates/roles/ops/meeting-actions-owner.md +26 -0
- package/templates/roles/ops/project-manager.md +26 -0
- package/templates/roles/writing/editor.md +26 -0
- package/templates/roles/writing/style-checker.md +26 -0
- package/templates/roles/writing/terminology-checker.md +26 -0
- package/templates/roles/writing/translator.md +26 -0
package/docs/operations.md
CHANGED
|
@@ -581,6 +581,32 @@ authority. All newly admitted reads use the ordinary tools connection and public
|
|
|
581
581
|
views, never console identity. The person's full readback stays unchanged.
|
|
582
582
|
Conductor operations are listed in [the operational command table](#conductor-operations-812-t2-933-900).
|
|
583
583
|
|
|
584
|
+
### Capability discovery (#445 C1)
|
|
585
|
+
|
|
586
|
+
`ahub capabilities [--json]` is Console only: an agent shell is refused, and so is the configured conductor. It lists, for each attached peer, the skills and commands the hub can read from that peer's own session, whether you share each one, its digest, and the class a call would count as. It lists and never calls anything; agents call what you share with `hub_invoke` (see below).
|
|
587
|
+
|
|
588
|
+
What the hub can read is deliberately narrow. A listing is read only from a version whose listing was probed (the [C0 receipt](verification/2026-10-30-445-capability-probes.md)): Kimi 2.3.2, Qwen 0.26.1 and Codex 0.166.0. Any other version lists nothing until a new receipt covers it, and so does a peer that is offline or whose list did not arrive. Codex's pinned 0.154.0 is not probed, so a pinned install lists nothing for now. Only skills are listed: Qwen by its `_meta.source` (`bundled-skill`, `skill-dir-command`), Kimi by the `skill:` prefix of its name, Codex by an enabled entry of `skills/list`. A builtin such as Kimi's `compact` or Qwen's `clear` changes session or host state and is never listed. A session the hub cannot read natively publishes its own list with `hub_capabilities_publish`; that list is bound to the connection that published it, forced to `invoke: prompt`, and gone when the connection ends.
|
|
589
|
+
|
|
590
|
+
Sharing is a positive allowlist in `.agenthub/config.local.json` only (`invocation` is machine-local: a committed copy is ignored and logged, and it is never editable from settings):
|
|
591
|
+
|
|
592
|
+
```json
|
|
593
|
+
{ "invocation": { "share": { "kimi": { "command:skill:review": { "digest": "sha256:<64 hex>", "class": "read" } } } } }
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
The key is the peer id (an instance `codex-2` never inherits `codex`'s entries). The digest is the one `ahub capabilities` prints, and covers the kind, name, input schema, declared class, source and invoke form, not the description: a change to any of those unlists the entry until you pin the new digest, and rewording the description does not. `class` is `read`, `write`, `external` or `destructive`, and the effective class is the higher of what the source declared and what you wrote. No native listing declares a class, so an entry you share with no `class` counts as `destructive`, which a later invocation refuses.
|
|
597
|
+
|
|
598
|
+
Agents see only what you shared. `hub_capabilities { peer? }` returns the shared entries (id, flat input schema, effective class, digest, source, invoke form and the target's permission mode) and nothing else; it grants no right to call anything. Descriptions are written by the peer that offers them: they are sanitized, cut at 300 characters and shown as untrusted. Neither `hub_capabilities` nor `hub_capabilities_publish` runs in a PII turn or for a peer holding a PII task, and neither call reaches memory capture.
|
|
599
|
+
|
|
600
|
+
### Invoking a shared capability (#445 C2/C3)
|
|
601
|
+
|
|
602
|
+
`hub_invoke { peer, capability, args?, task? }` asks another peer to run one capability you shared from it (see `hub_capabilities`). A `read` may run without approval. A `write` or `external` requires an open non-PII task owned or reviewed by the caller and your allow-once approval on every call; unattended refuses before prompting. One with no class counts as `destructive` and is never invoked. The approval prompt shows the screened, sanitized arguments (200 characters per value, marked when cut), capability/class/task bindings and argument digest. Argument previews are person-only; audit metadata keeps the digest. Client requests cover the supported approval window; cancelling the tool or closing its control connection withdraws pending admission and approval. A late allow on a withdrawn request is refused. An already admitted native turn is not cancelled. After admission and any required approval, the call returns `{ invocation, state: "queued" }` and the result arrives later as one message from the hub, addressed to the caller only, so a long target turn never holds the caller's turn.
|
|
603
|
+
|
|
604
|
+
What the target receives is one hub-written request that names the caller and the capability and says the arguments are not instructions. It answers as an ordinary turn under its own permissions and sandbox, and may start its reply with `[REFUSED]` and a reason to decline. The result is the target's text, sanitized (a forged hub header is quoted), cut to 8 KiB and given a hub-written first line that marks it as untrusted agent text. Nothing else is promised: a result is another model's words. The answer reaches only that one result: it is taken over before the bus builds anything from it, so no other peer, digest, log line, event, journal row or memory entry ever carries it. The full answer, including a refusal, passes the private-turn, PII-pattern, secret and private-path checks and any enabled model screen using the full answer rather than only its shortened reason, before forwarding any reason; unknown or a screen failure does not clear it, and privacy is rechecked after that await. An unsafe `[REFUSED]` answer keeps outcome `refused` and returns only `<target> declined; the reason was withheld`. An ordinary unsafe answer keeps outcome `withheld` and the existing fixed withheld text. No unsafe callee text is kept; a clean refusal retains its screened reason.
|
|
605
|
+
|
|
606
|
+
Every admission refusal happens before any envelope exists and names its reason: the PII context, nesting (a peer answering an invocation cannot start one), the target being offline, paused, held, off the roster or pull-only, the capability not shared or changed since you pinned it, a destructive class, a missing effect task or person approval, a task that is not the caller's open work or is a PII task, an argument that does not fit the capability's schema, one that the PII or secret screen does not clear, or too many open invocations (4 per caller, 8 per target). A target that is paused (by you, the conductor or the budget) is refused too; a pause that starts while the request waits leaves it waiting until its deadline. Each call counts against the caller's ordinary message rate limits to that peer. An entry you did not share and one that does not exist read the same to an agent.
|
|
607
|
+
|
|
608
|
+
`invocation.timeout_s` in `.agenthub/config.local.json` (10 to 86400 seconds, default 600) bounds the wait. A request still queued at the deadline is withdrawn and nothing runs (`expired`). One the target received with no result by then is a `timeout` for read or `needs_review` for write/external: it may still run, and a late answer changes nothing. Read may automatically replay once on a proven `failed_safe` receipt; write/external never automatically replay. An uncertain effect holds its journal delivery for your inspection. Recipient retry does not replay invocation requests. There is no caller cancel; `ahub queue` resolves a delivery by hand. Task-scoped calls keep queued and terminal ids/digests/class/approval/outcome history through Tasks, without changing task state. The `invocations` table in `hub.db` and the `invocation` event hold ids, the capability, its class, the outcome and the result's size, never an argument or a result. A hub older than this feature refuses `hub_invoke` with a message, and a restart or upgrade refuses a target without the feature while an invocation is open.
|
|
609
|
+
|
|
584
610
|
## Conducting a team from Claude Code or Codex
|
|
585
611
|
|
|
586
612
|
Start the daemon with `ahub up --no-console`, set exactly one conductor in the
|
|
@@ -760,6 +786,22 @@ Private envelopes and PII tasks stay redacted. Local-worker and Pi tool details
|
|
|
760
786
|
allow decisions stay in `ahub tail` / `ahub permit`; the page can deny them.
|
|
761
787
|
No dashboard port is opened until requested. See [the session security model](security.md#local-dashboard-sessions-issue-6).
|
|
762
788
|
|
|
789
|
+
### Cross-hub read invocations (#446 X6)
|
|
790
|
+
|
|
791
|
+
Both persons consent by pinning. On the target hub, use the existing `ahub capabilities` to obtain the local capability id and digest and pin its effective class to `read`. Transfer that digest out of band. On the source hub, add an exact remote entry in its machine-local file:
|
|
792
|
+
|
|
793
|
+
```json
|
|
794
|
+
{ "invocation": { "share": { "kimi@beta": { "command:review": { "digest": "sha256:<64 hex>", "class": "read" } } } } }
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
The target independently retains its own local `kimi` entry for the same capability/digest/read class. Room peer exposure alone grants no invocation. There is no inheritance between labels/peers, and no remote discovery RPC. `hub_capabilities` lists local observed manifests; callers obtain remote capability ids/schema information manually.
|
|
798
|
+
|
|
799
|
+
`hub_invoke` can name the exact `peer@label`. The source validates bounded flat scalar arguments; the target applies its actual schema, current share/digest/effective-read class and privacy/availability gates before building a native request. Source task scope remains local and is not interpreted on the target. Task/approval refs and write/external/destructive requests never cross.
|
|
800
|
+
|
|
801
|
+
A signed room admission receipt is not a capability result. Native execution outcome and result transport state are recorded separately. Result correlation binds room/member/epoch/request/peer/digest, and duplicate or mismatched frames cannot trigger another native invocation. Nested invocation turns refuse depth 2, including before native acceptance is reported. Unknown handoff, receipt, result deadline or restart publication is `needs_review` under the existing journal hold and never silently replays. Inspect the retained invocation and room queue before explicit person disposition.
|
|
802
|
+
|
|
803
|
+
Remote-origin host envelopes retain the existing remote-turn memory/capture/history/telemetry exclusions even though the local native request is hub-authored. Live native account and two-Mac X7 receipts remain separate from the two-daemon broker/crypto fixtures.
|
|
804
|
+
|
|
763
805
|
### Dashboard design tokens (#469 R1)
|
|
764
806
|
|
|
765
807
|
The page's style block starts with one token block that owns every colour, the 4px spacing scale (4, 8, 12, 16, 24, 32), the
|
|
@@ -1964,6 +2006,31 @@ A guarded file edit that the daemon keeps at its accepted value remains visible
|
|
|
1964
2006
|
| Switches | `tasks.auto_pull` (default off: pull the top explicitly reserved ready card when a planner slot frees) | `config.local.json` | live; the next freed planner slot uses it |
|
|
1965
2007
|
| Routing | `tasks.queue_backpressure.max_queued` (0-10000), `.max_oldest_s` (0-604800); 0, the default, disables each ([backpressure](#queue-backpressure-on-automatic-task-offers-800)) | `config.local.json` | live |
|
|
1966
2008
|
| Roles | `planner` (one or up to eight planner holders) | `config.local.json` | at once |
|
|
2009
|
+
| Routing | `bus.max_attempts` (1-100 attempts, default 3) | `config.local.json` | live; next safe-failure retry evaluation, retaining existing counts and quota pauses |
|
|
2010
|
+
| Routing | `bus.digest_max` (1-10 originals, default 10) | `config.local.json` | live; next push/attached-channel take, with poison-alone and remote-singleton rules |
|
|
2011
|
+
| Routing | `bus.pull_budget_bytes` (1-65536 bytes, default 65536) | `config.local.json` | live; next pull-only read, oldest first; the first oversized item still goes |
|
|
2012
|
+
| Routing | `bus.notes_kept` (1-10 notes, default 10) | `config.local.json` | live; next shared-note or failed-preface merge; existing prefaces remain unchanged on reload |
|
|
2013
|
+
| Routing | `bus.terminal_history_limit` (1-2048 rows, default 2048) | `config.local.json` | live; next normal terminal settlement; unresolved rows and retry ancestry stay protected |
|
|
2014
|
+
| Routing | `recovery.reconnect_wait_s` (600-3600 seconds, default 600) | `config.local.json` | next use; the next managed-terminal attachment wait of an upgrade recovery reads it once, at its start |
|
|
2015
|
+
| Routing | `recovery.unmanaged_reconnect_wait_s` (90-900 seconds, default 90) | `config.local.json` | next use; the next unmanaged Claude reconnect wait reads it once, at its start |
|
|
2016
|
+
| Routing | `recovery.control_timeout_s` (30-300 seconds, default 30) | `config.local.json` | next use; the next control request of the recovery coordinator to a hub reads it |
|
|
2017
|
+
| Routing | `recovery.shutdown_wait_s` (30-300 seconds, default 30) | `config.local.json` | next use; the next wait for a committed source to stop reads it once, at its start |
|
|
2018
|
+
| Routing | `recovery.terminal_idle_wait_s` (30-600 seconds, default 120) | `config.local.json` | next use; the next wait for a terminal to go idle before it is closed reads it |
|
|
2019
|
+
| Start | `adapter_timing.acp_tool_ceiling_min` (1-30 minutes, default 30) | `config.local.json` | next use; a new ACP launch captures it; effective ceiling is at least the watchdog |
|
|
2020
|
+
| Start | `adapter_timing.acp_activity_batch_ms` (10-50 ms, default 50) | `config.local.json` | next use; a new ACP launch captures its activity batch delay; pending flushes are unchanged |
|
|
2021
|
+
| Start | `adapter_timing.jcode_steer_timeout_s` (5-60 seconds, default 5) | `config.local.json` | next use; a new Jcode launch captures one total native steer deadline |
|
|
2022
|
+
| Routing | `model_relay.cooldown_failures` (1-100, default 3) | `config.local.json` | restart; consecutive transport/startup failures before cooling an alias |
|
|
2023
|
+
| Routing | `model_relay.cooldown_s` (1-300 seconds, default 30) | `config.local.json` | restart; initial cooldown and recent-failure window |
|
|
2024
|
+
| Routing | `model_relay.cooldown_cap_s` (300-3600 seconds, default 300) | `config.local.json` | restart; cooldown cap, never below the initial cooldown |
|
|
2025
|
+
| Routing | `model_relay.terminal_history_limit` (1-1000 records, default 1000) | `config.local.json` | restart; only tightens retained closed-request history |
|
|
2026
|
+
| Routing | `inference.backoff_min` (1-60 minutes, default 5) | `config.local.json` | restart; failure backoff for optional inference, interactive calls exempt |
|
|
2027
|
+
| Routing | `inference.digest_min_items` (1-100000 items, default 5) | `config.local.json` | restart; existing condensation threshold |
|
|
2028
|
+
| Routing | `inference.digest_min_chars` (1-10000000 characters, default 8000) | `config.local.json` | restart; existing condensation threshold |
|
|
2029
|
+
| Start | `console.poll_ms` (2000-60000 ms, default 4000) | `config.local.json` | next use; next console launch captures its refresh interval |
|
|
2030
|
+
| Start | `console.notice_ms` (10000-60000 ms, default 10000) | `config.local.json` | next use; notice visibility may only lengthen |
|
|
2031
|
+
| Start | `console.history_limit` (1-100 commands, default 100) | `config.local.json` | next use; only tightens transient command history |
|
|
2032
|
+
| Start | `console.pending_stream_limit` (1-1000 events, default 1000) | `config.local.json` | next use; only tightens offscreen stream buffering, with omitted counts |
|
|
2033
|
+
| Start | `console.attention_limit` (1-1000 messages, default 1000) | `config.local.json` | next use; only tightens transient message attention entries |
|
|
1967
2034
|
|
|
1968
2035
|
### Operational numeric settings (#489 H1)
|
|
1969
2036
|
|
|
@@ -2021,6 +2088,123 @@ The H0 operational items of `tasks.ts`, `routing.ts` and `task-sweep.ts` that st
|
|
|
2021
2088
|
freshness check. They bound a whole call, a refusal of a stale host window protects the release, and none is
|
|
2022
2089
|
independently tunable without changing what a partial result means.
|
|
2023
2090
|
|
|
2091
|
+
#### Bus and delivery conversions (#489 H3)
|
|
2092
|
+
|
|
2093
|
+
The five `bus.*` rows above use the H1 descriptor, schema, settings writer and live
|
|
2094
|
+
reload path. Unset defaults equal `MAX_ATTEMPTS`, `DIGEST_MAX`, `PULL_BUDGET_BYTES`,
|
|
2095
|
+
`NOTES_KEPT` and `TERMINAL_CAP`. Only `bus.max_attempts` may rise; the other four
|
|
2096
|
+
can only tighten today's ceilings. A reload does not trim/reorder queues or
|
|
2097
|
+
prefaces, reset attempts, re-arm batch deadlines, prune history, or change a pause
|
|
2098
|
+
or in-flight delivery. Terminal history is a target: protected retry parents and the current mutation receipt may
|
|
2099
|
+
keep the count above it. The settings add no conductor authority.
|
|
2100
|
+
|
|
2101
|
+
The [H3 H0 ledger](verification/2026-10-30-bus-delivery-operational-settings.md)
|
|
2102
|
+
lists all 59 unique inventory identities (5 converted and 54 excluded). The
|
|
2103
|
+
persisted `SEEN_CAP`/dedupe contract and withdrawn cancellation evidence stay
|
|
2104
|
+
fixed; recovery fence polling goes to H4, expired-limit-record cleanup remains
|
|
2105
|
+
maintenance, existing limits/watchdog/batching knobs remain in their current
|
|
2106
|
+
configuration path, and wire/storage/safety/test-seam values remain constants.
|
|
2107
|
+
|
|
2108
|
+
#### Recovery and upgrade waits (#489 H4)
|
|
2109
|
+
|
|
2110
|
+
The five `recovery.*` rows extend the existing `recovery` block (`auto_resume_after_crash` is unchanged) and use
|
|
2111
|
+
the H1 descriptor, schema and settings writer. They are read by the upgrade coordinator, a CLI process and not the
|
|
2112
|
+
daemon, from the project it is acting on, at the start of each wait: the shared `config.json` first and the
|
|
2113
|
+
machine-local `config.local.json` over it (ignored when git tracks it). The apply class is `next use`: a wait that is
|
|
2114
|
+
already running keeps the deadline the receipt records in `waits`, and a resume reads the file again. Unset defaults
|
|
2115
|
+
equal `RECONNECT_WAIT_MS`, `UNMANAGED_RECONNECT_WAIT_MS`, `RECOVERY_CONTROL_TIMEOUT_MS`, the 30 s shutdown wait and
|
|
2116
|
+
the 120 s terminal idle wait. A value outside a row's bounds, or a file the coordinator cannot read, is the default:
|
|
2117
|
+
a recovery never fails on its configuration.
|
|
2118
|
+
|
|
2119
|
+
Every minimum is today's constant except `recovery.terminal_idle_wait_s`: a person-visible wait can be raised and
|
|
2120
|
+
never shortened below what ships, and a control request that times out may already have applied. A shorter terminal
|
|
2121
|
+
idle wait only ends in manual-required, which retains the terminal. The 600 s wait for the source to become quiet is
|
|
2122
|
+
not a setting: the source's preparation lease is a fixed ten minutes, so no other value is valid. The 180 s global
|
|
2123
|
+
install timeout has no project to own it, and the 610 s Orca command timeout follows terminal recovery's own ten-minute
|
|
2124
|
+
ceiling. See the [H4 ledger](verification/2026-10-30-recovery-wait-settings.md) for every item in the group.
|
|
2125
|
+
|
|
2126
|
+
#### Adapters and handshakes (#489 H5)
|
|
2127
|
+
|
|
2128
|
+
Three `adapter_timing.*` rows use the same H1 metadata, closed schema, loader and
|
|
2129
|
+
settings writer. `next use` means a new native peer launch: the host captures
|
|
2130
|
+
primitive option values for Kimi/generic ACP and Jcode, including Jcode's inherited
|
|
2131
|
+
ACP options. An accepted edit leaves active tool ceilings, pending activity
|
|
2132
|
+
flushes and native steer deadlines unchanged until a new peer starts.
|
|
2133
|
+
|
|
2134
|
+
The open-tool ceiling defaults to the legacy 30 minutes, can only tighten, and
|
|
2135
|
+
remains `max(watchdogMs, configured ceiling)` with remaining-only rearming. The
|
|
2136
|
+
activity delay defaults to the legacy 50 ms and never exceeds that buffer window;
|
|
2137
|
+
its 4096-character threshold and privacy handling stay fixed. Jcode's default
|
|
2138
|
+
5-second steer deadline can only rise, and one absolute timer covers all native
|
|
2139
|
+
phases; pre-send refusals and post-send uncertainty retain their existing fences.
|
|
2140
|
+
No new conductor authority or native permission mapping is added.
|
|
2141
|
+
|
|
2142
|
+
The [H5 ledger](verification/2026-10-30-adapter-operational-settings.md) accounts for
|
|
2143
|
+
all 232 unique H0 identities: 3 converted and 229 excluded with reasons. ACP
|
|
2144
|
+
startup/mode/cancel and Jcode readiness retain their 30-second constants: raising
|
|
2145
|
+
startup deadlines does not fit the existing 65/70-second callers, and shortening
|
|
2146
|
+
would fail slow healthy launches. The shared MCP server's reconnect cap stays
|
|
2147
|
+
fixed at 30 seconds; no daemon-to-plugin projection, raw-config reader, RPC or
|
|
2148
|
+
environment knob is introduced. Cleanup/identity proofs, wire/storage/safety caps,
|
|
2149
|
+
existing settings and test seams remain at their current owners.
|
|
2150
|
+
|
|
2151
|
+
#### Model relay and inference (#489 H6)
|
|
2152
|
+
|
|
2153
|
+
The seven rows above use the same H1 descriptors, validation, settings writer and
|
|
2154
|
+
undo path. All require a daemon restart: an inference consumer captures its
|
|
2155
|
+
configuration at daemon start, and each relay captures its policy at creation.
|
|
2156
|
+
A relay remains cached across a native peer stop/start, so restarting that peer
|
|
2157
|
+
alone does not adopt these values. Pending edits never retime current cooldowns,
|
|
2158
|
+
backoff deadlines or requests, or trim the running relay's journal.
|
|
2159
|
+
|
|
2160
|
+
The initial cooldown and its recent-failure window share one setting. Cooldowns
|
|
2161
|
+
still start only after consecutive transport/startup failures, double by the
|
|
2162
|
+
same fixed factor and end on expiry or an HTTP answer. The cap's minimum equals
|
|
2163
|
+
the initial cooldown's maximum, so it can never fall below that delay. The
|
|
2164
|
+
history setting can only tighten the legacy 1000 closed records; it does not
|
|
2165
|
+
change active-request evidence or suppress terminal observers.
|
|
2166
|
+
|
|
2167
|
+
`inference.backoff_min` preserves fail-open delivery and interactive-call
|
|
2168
|
+
exemption. The two digest thresholds were already configurable; their H1 rows
|
|
2169
|
+
retain those keys, defaults and existing schema bounds. No new model selection,
|
|
2170
|
+
PII screening, conductor authority or cross-process configuration path is added.
|
|
2171
|
+
The 180-second request deadline, 15-second moved-attempt first-byte deadline and
|
|
2172
|
+
8-second inference timeout stay fixed under their current caller budgets.
|
|
2173
|
+
|
|
2174
|
+
The [H6 ledger](verification/2026-10-30-model-operational-settings.md) accounts for
|
|
2175
|
+
all 169 H0 identities: seven registered and 162 left out with individual reasons.
|
|
2176
|
+
PII policy, route scoring, protocol contracts, existing model/routing settings
|
|
2177
|
+
and test seams remain with their current owners.
|
|
2178
|
+
|
|
2179
|
+
#### UI and console (#489 H7)
|
|
2180
|
+
|
|
2181
|
+
Five `console.*` rows use H1 and apply at the next console launch. The console
|
|
2182
|
+
reads accepted `settings_get` row values once through its existing authenticated
|
|
2183
|
+
control connection, under the existing 3-second read bound. It uses fixed H1
|
|
2184
|
+
bounds, ignores pending/file values, and retains legacy defaults for an older,
|
|
2185
|
+
refusing or unreachable hub. A running console keeps its captured preferences.
|
|
2186
|
+
The existing `options.pollMs` test seam still wins, with the fixed 2000 ms floor;
|
|
2187
|
+
refresh serialization and its pending-refresh coalescing remain unchanged.
|
|
2188
|
+
|
|
2189
|
+
Notice visibility may only lengthen. The other retention settings only tighten
|
|
2190
|
+
local presentation memory: command history, offscreen stream events and message
|
|
2191
|
+
attention references. They never trim bus queues, delivery records or approvals.
|
|
2192
|
+
Close/signal and permission handlers remain active while the startup read waits.
|
|
2193
|
+
If pushes arrived before capture, only cold-start presentation buffers are
|
|
2194
|
+
trimmed, preserving accurate omitted counts and every approval/control message.
|
|
2195
|
+
|
|
2196
|
+
The 1-second redraw/approval countdown interval, `appendEvent` cap test seam,
|
|
2197
|
+
Usage history query bound, browser toast/poll/office timings, authentication
|
|
2198
|
+
lifetimes and action budgets remain fixed. The multi-project manager owns the
|
|
2199
|
+
browser page; this change adds no per-project configuration projection or RPC.
|
|
2200
|
+
|
|
2201
|
+
The [H7 ledger](verification/2026-10-30-ui-console-operational-settings.md) covers
|
|
2202
|
+
all 1007 H0 UI/console fallback identities, with 81 in these UI/console paths and
|
|
2203
|
+
926 outside them. The additional `task-progress.ts` initializer is classified
|
|
2204
|
+
under tasks/routing by the inventory's precedence and is recorded separately.
|
|
2205
|
+
Five rows cover six H0 identities (the two command-history paths share one key).
|
|
2206
|
+
The deferred support, CLI and memory items stay constant at their current owners and bounds ([closure record](verification/2026-10-30-operational-settings-closure.md), #989); #489 is complete.
|
|
2207
|
+
|
|
2024
2208
|
- Writes go only to `.agenthub/config.local.json` and `.agenthub/routing.local.toml`,
|
|
2025
2209
|
the machine-local files (`ahub init` ignores both in git). The shared
|
|
2026
2210
|
`config.json` and the hand-written `routing.toml` are never rewritten, and a
|
|
@@ -3431,10 +3615,12 @@ after them is not retried on MLX, and a failed one marks `dgx/fast` failing (a c
|
|
|
3431
3615
|
disconnects does not). Each backend is tried once per request, so with moves on, a moved attempt
|
|
3432
3616
|
cut at 15 s followed by an MLX slot that stays busy past 120 s fails the request (502), where
|
|
3433
3617
|
without moves the request would have waited for MLX and then had `dgx/fast` with its full
|
|
3434
|
-
deadline; this is part of what the opt-in accepts until #199 AC5 measures it. After
|
|
3435
|
-
consecutive transport or startup failures
|
|
3436
|
-
|
|
3437
|
-
|
|
3618
|
+
deadline; this is part of what the opt-in accepts until #199 AC5 measures it. After
|
|
3619
|
+
`model_relay.cooldown_failures` (default three) consecutive transport or startup failures
|
|
3620
|
+
outside a cooldown (a failure more than twice the cap, 10 min by default, after the last counted
|
|
3621
|
+
one starts the count over), a relay alias cools down for `model_relay.cooldown_s` (default
|
|
3622
|
+
30 s), doubling up to `model_relay.cooldown_cap_s` (default 5 min); all three apply at the next
|
|
3623
|
+
daemon restart. A busy MLX slot and timeouts cut short by an execution budget never count; any HTTP
|
|
3438
3624
|
answer, a success or an error status, proves the transport works and ends the streak and the
|
|
3439
3625
|
cooldown. `ahub status` shows a `cooling` detail with a relative time and failure count and `events.jsonl`
|
|
3440
3626
|
records `cooldown` events.
|
|
@@ -4033,13 +4219,13 @@ A bundled adapter module is authorized by the person with `adapter_modules` (at
|
|
|
4033
4219
|
|
|
4034
4220
|
# Catalog session guidance (#295)
|
|
4035
4221
|
|
|
4036
|
-
- Read available guidance with `ahub role list [--group dev|research]` and `ahub role show <id>`. Each definition identifies its layer and content hash. Doctor reports invalid files/selections without printing their contents.
|
|
4222
|
+
- Read available guidance with `ahub role list [--group dev|research|writing|ops]` and `ahub role show <id>`. Each definition identifies its layer and content hash. Doctor reports invalid files/selections without printing their contents.
|
|
4037
4223
|
- Select up to two ids per peer in `catalog_roles` in the shared/local config, with at least one flow base already held by that peer. Empty local lists clear shared selections. This initial slice applies selections at a new daemon/MCP/adapter start; live assignment/settings and task roles follow separately.
|
|
4038
4224
|
- Defaults preserve the existing flow. Catalog guidance adds no authority or routing preference. Native ingestion/retention and measured benefit remain unverified. Schema, bounds and source notice: [roles](roles.md).
|
|
4039
4225
|
|
|
4040
4226
|
# Project profiles (#259)
|
|
4041
4227
|
|
|
4042
|
-
- Initialize new projects with `ahub init --profile research|writing|general`; default dev retains its existing templates and contracts. Profile aliases map to the existing base flow and are displayed beside it. `--profile <pack>` applies a domain pack (`.agenthub/roles/<name>.pack.json` or a shipped one) as review-checklist and example-check overrides; `--dry-run` previews ids only, and a pack is data, never a command ([domain packs](profiles.md#domain-packs-513a), #513a).
|
|
4228
|
+
- Initialize new projects with `ahub init --profile research|writing|general`; default dev retains its existing templates and contracts. Profile aliases map to the existing base flow and are displayed beside it. `--profile <pack>` applies a domain pack (`.agenthub/roles/<name>.pack.json` or a shipped one: `writing-pack`, `analysis`, `ops`) as review-checklist and example-check overrides; `--dry-run` previews ids only, and a pack is data, never a command ([domain packs](profiles.md#domain-packs-513a), #513a).
|
|
4043
4229
|
- Non-dev plans declare paths, section anchors, source ids and claims. Configure completion checks explicitly in machine-local config; selecting a profile never starts a check or study. Schema, examples and pending converter/policy slices: [profiles](profiles.md).
|
|
4044
4230
|
|
|
4045
4231
|
# Private and read-only data paths (#259)
|
package/docs/profiles.md
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
# Current scope and remaining slices
|
|
32
32
|
|
|
33
|
-
- Peer guidance and profile-specific task schemas use existing MCP/native/local paths. Research defaults use the #295 dev/research catalog, with bases already held. Writing
|
|
33
|
+
- Peer guidance and profile-specific task schemas use existing MCP/native/local paths. Research defaults use the #295 dev/research catalog, with bases already held. Writing and ops catalog groups are available through the domain packs below; profile defaults do not automatically assign them to peers.
|
|
34
34
|
- Board/public projections, route explain, terminal/dashboard cards and #247 events/records carry the profile alias and base. Native model ingestion and context retention remain unverified.
|
|
35
35
|
- Bounded rich-document facts/review/undo projections and declared pii.paths/data.readonly policy are supported. Opt-in shadow git supports non-git folders; project profile overrides are supported; bounded glob policy and deeper lifecycle integration are tracked in #259. Native ingestion and research measurement remain unverified.
|
|
36
36
|
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
# Project profile overrides
|
|
52
52
|
|
|
53
53
|
- Acceptance criteria are profile-independent (#507b): a task's recorded criteria checklist (docs/sdlc.md, Acceptance-criteria checklist) is listed in every profile's review request, ahead of the profile's `review_checklist` lines, and an unmet item that starts with a criterion id names it; a profile only changes the wording of its checklist, never the criteria or their ids.
|
|
54
|
-
- Recurring unmet criteria become a candidate, never a change (#514a): when reviews name the same criterion id unmet on at least 3 tasks of one profile and class within 30 days, and those tasks make up at least 2 separate pieces of work (tasks sharing a PR or a branch count once), the hub files one backlog card with the `profile_overrides.review_checklist` line to consider. The line is text
|
|
54
|
+
- Recurring unmet criteria become a candidate, never a change (#514a): when reviews name the same criterion id unmet on at least 3 tasks of one profile and class within 30 days, and those tasks make up at least 2 separate pieces of work (tasks sharing a PR or a branch count once), the hub files one backlog card with the `profile_overrides.review_checklist` line to consider. The line is text, and nothing applies it on its own. By hand: `profile_overrides` is not machine-local, so you choose the shared `.agenthub/config.json` (every checkout) or `config.local.json` (this machine), and add the line to your current checklist because `review_checklist` replaces as a unit. With the command (#514b): `ahub learn list` shows the open cards, `ahub learn accept <id>` adds that one line to your current checklist in `.agenthub/config.local.json` (this machine only, never the shared or any tracked file) through the settings writer, closes the card, and says it applies after a restart; `ahub settings undo` puts the previous file back; `ahub learn reject <id>` only closes the card. The line is rebuilt from the key line of the card and the board's own evidence, never parsed from the card text. Person only: an agent shell and the configured conductor are refused. PII tasks and evidence-only verdicts never count.
|
|
55
55
|
- `profile_overrides` in shared/local config selects project data over the chosen shipped profile. `classes`, `role_text` and `catalog_roles` maps merge by key across layers; `plan_keys`, `agents`, `example_checks` and `review_checklist` replace as units. A local `profile` selects the active base for both init and native contracts.
|
|
56
56
|
- Classes may add or remap profile aliases to existing base classes. Existing base names keep their meanings; class maps are bounded to 32 override entries. Task storage and route explanations preserve the alias, including a dev custom alias, while routing/budgets/checks use its base. Other tasks under custom dev data retain the selected profile semantics too, so document sections do not become whole-path overlaps. Unmodified dev rows retain their old shape.
|
|
57
57
|
- `plan_keys` selects distinct existing plan fields, not new board fields. Task proposals and their schemas use the selection. Accept schemas retain legacy fields for already existing tasks/profile changes.
|
|
@@ -61,14 +61,39 @@
|
|
|
61
61
|
|
|
62
62
|
# Domain packs (#513a)
|
|
63
63
|
|
|
64
|
-
A pack bundles a profile with the roles it expects, a review rubric and example checks in one file, so a project can adopt a way of working with one command. The
|
|
64
|
+
A pack bundles a profile with the roles it expects, a review rubric and example checks in one file, so a project can adopt a way of working with one command. The shipped writing, analysis and ops packs use this format (#513b).
|
|
65
65
|
|
|
66
|
-
- A pack comes from two layers: the shipped one (`templates/packs/`, bundled;
|
|
66
|
+
- A pack comes from two layers: the shipped one (`templates/packs/`, bundled; `example`, `writing-pack`, `analysis` and `ops`), and the project one, `.agenthub/roles/<name>.pack.json`, which replaces a shipped pack of the same name. The project layer is in the directory the role catalog already reads (the catalog takes only `.md`, so the two never collide). At most 16 packs of 8192 bytes each, plain regular files that are not links, in a directory that is not a link. A pack that is oversize, malformed or names a role that is not in the catalog (shipped or project) is refused with the reason, never loaded half usable.
|
|
67
67
|
- The file holds exactly `name` (equal to the file name, and never a profile's name), `profile` (one of dev, research, writing, general), `summary` (160 characters), `roles` (at most 8 distinct role ids), `rubric` (1 to 12 criteria, each `id`, `text` of at most 300 characters and `required`) and `checks` (at most 8, each `id` and a `note` of at most 500 characters). Any other field is refused. Text is plain ASCII punctuation with no control characters.
|
|
68
68
|
- Rubric ids are #507's criterion ids (a letter, then letters, digits or `-`, at most 48 characters), the same shape a task's recorded criteria use, so a reviewer names an unmet item with the id at the start of the line. The rubric only changes the wording of the review checklist; it never adds or removes a task's recorded criteria.
|
|
69
69
|
- A pack is data, never authority. A check is display text exactly like `example_checks`: nothing selects, runs or trusts a command from a pack, and there is no field that could. Roles are named, not created: the pack asks for catalog roles by id and the project assigns them with `catalog_roles` as before.
|
|
70
70
|
- `ahub init --profile <profile|pack>` takes a shipped profile or a project pack. `--dry-run` (add `--json` for rows) previews the config row with the pack's id, base profile, rubric criterion ids, check ids and expected role ids, and never their text. The apply writes the pack's rubric as `profile_overrides.review_checklist` (each line `<id>: <text>`, optional criteria marked, then the `{check}` line) and its checks as `profile_overrides.example_checks`, into a new `.agenthub/config.json` or the existing one, keeping every other key. Applying the same pack again changes nothing. A project whose `profile` differs from the pack's, or whose `profile_overrides` already hold different data (compared with sorted keys, so key order never matters), is refused with the reason rather than overwritten. Init does not copy role files and a check is a note, not a command: the spec amendment "domain packs (#513a)" records why.
|
|
71
71
|
|
|
72
|
+
# Shipped domain packs and checks (#513b)
|
|
73
|
+
|
|
74
|
+
| Pack | Base profile | Catalog roles | Deterministic check |
|
|
75
|
+
| --- | --- | --- | --- |
|
|
76
|
+
| `writing-pack` | writing | editor, translator, style-checker, terminology-checker | writing-metadata, writing-terms, writing-style |
|
|
77
|
+
| `analysis` | research | analyst | analysis-reproduce |
|
|
78
|
+
| `ops` | general | project-manager, meeting-actions-owner | ops-records |
|
|
79
|
+
|
|
80
|
+
Use `ahub init --profile writing-pack --dry-run` (or `analysis`/`ops`) to preview, then omit `--dry-run` to apply. `writing` remains the existing plain profile; the pack uses `writing-pack` because pack names cannot shadow profiles. Expected roles are already in the shipped catalog; assign them deliberately through the existing role flow. Neither init nor pack loading copies or executes a checker, adds trusted `checks` commands, or grants permissions.
|
|
81
|
+
|
|
82
|
+
From an agent-hub source checkout, these commands run the shipped synthetic examples:
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
bun templates/checks/writing.mjs templates/checks/fixtures/writing.json templates/checks/fixtures/writing-rules.json
|
|
86
|
+
bun templates/checks/analysis.mjs templates/checks/fixtures/analysis.mjs templates/checks/fixtures/analysis-input.json templates/checks/fixtures/analysis-expected.json
|
|
87
|
+
bun templates/checks/ops.mjs templates/checks/fixtures/ops.json
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The npm package includes these files under `templates/checks/`; when installed, use their absolute installed-package paths or copy the reviewed examples into the project. Only an explicitly selected, trusted analysis program is executed. A project that wants an automatic task check must separately review and configure a command through the existing `checks` policy. A pack note remains advisory.
|
|
91
|
+
|
|
92
|
+
- Writing takes a JSON document with nonempty `title`, `language` and `text`, and JSON rules with literal `requiredTerms` and `bannedPhrases` arrays. Required terms must occur in text; banned phrases are absent from title/text. The mechanical style check refuses TODO/TBD placeholders and trailing whitespace. These checks do not assess translation fidelity, source support, readability or audience fit.
|
|
93
|
+
- Analysis takes a program, JSON input and an exact expected stdout file. It invokes the selected program twice with the input path as its first argument, without a shell, compares stdout bytes including the final newline, and refuses changes to any declared file after either run. Each run has a 5-second/1-MiB stdout bound; its stderr is withheld. Equality does not prove method suitability, input provenance or reproducibility of undeclared environment/dependencies.
|
|
94
|
+
- Ops takes an array of exact `{id, owner, due, status}` records. IDs must be unique and nonempty, owners nonempty, due dates real `YYYY-MM-DD` values, and status one of `open`, `in_progress`, `blocked`, `done`. It does not compare to the current clock. An empty array passes structure only; it does not establish that a meeting had no actions or that all actions were captured.
|
|
95
|
+
- All input files are bounded to 1 MiB. JSON inputs must be valid UTF-8/JSON; symbolic links and missing files refuse. Analysis expected output is compared as raw bytes rather than parsed JSON. Results contain stable criterion ids and a final `writing: OK`, `analysis: OK` or `ops: OK` (or FAIL), never artifact text. Exit codes are 0 for pass, 1 for refusal and 2 for incorrect arguments. Human rubric items remain separate from these mechanical results. Fixture success is not live workflow or publication approval.
|
|
96
|
+
|
|
72
97
|
# Profile scorecard (#799)
|
|
73
98
|
|
|
74
99
|
The existing ahub report --dogfood command adds a profile section when the selected profile is non-dev, or bound non-dev tasks have evidence in the requested window. Dev-only JSON and readable output retain their prior bytes. Existing flow, quality, cost and other fields keep their meanings; the profile section does not require merged PRs.
|
package/docs/quickstart.md
CHANGED
|
@@ -17,7 +17,7 @@ ahub setup # installs the Claude Code channel plu
|
|
|
17
17
|
Or use the matching GitHub release:
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
bun add -g github:STAIxBWLB/agent-hub#v0.
|
|
20
|
+
bun add -g github:STAIxBWLB/agent-hub#v0.23.0
|
|
21
21
|
ahub setup
|
|
22
22
|
```
|
|
23
23
|
|
|
@@ -30,6 +30,14 @@ and upgrade or crash recovery, see the [operations guide](operations.md). To
|
|
|
30
30
|
move running projects to a newer release later, run `ahub upgrade` in a
|
|
31
31
|
terminal: it reviews the plan with you, applies it and follows it.
|
|
32
32
|
|
|
33
|
+
## Upgrading to 0.23.0
|
|
34
|
+
|
|
35
|
+
Nothing changes until you set something. Thirteen more operational numbers became settings in `config.local.json`, each keeping its old value when unset: three `adapter_timing.*` (at the next Kimi, ACP or Jcode launch, #982), four `model_relay.*` and `inference.backoff_min` (at the next daemon restart, #984) and five `console.*` (at the next console launch, #987). Capability sharing is new and opt-in. `ahub capabilities` (person only) lists the skills an attached Kimi 2.3.2, Qwen 0.26.1 or Codex 0.166.0 offers; nothing is offered to other agents until you pin an entry's digest under `invocation.share` in `.agenthub/config.local.json` (#985). Agents then call a shared `read` capability with `hub_invoke`. A `write` or `external` one needs an open task of the caller and your allow-once approval on every call, and one with no class is never invoked (#986, #991; see [invoking a shared capability](operations.md#invoking-a-shared-capability-445-c2c3)). Across a room, only a read capability both persons pinned crosses (#992). `ahub init --profile writing-pack|analysis|ops` applies the new shipped domain packs (#993). The `@staix/agent-mem` pin stays 0.13.0. Upgrade running hubs with `ahub upgrade` (an older hub refuses `hub_invoke`) and restart Claude sessions so they load the 0.23.0 plugin.
|
|
36
|
+
|
|
37
|
+
## Upgrading to 0.22.0
|
|
38
|
+
|
|
39
|
+
Nothing changes until you set something. Ten operational numbers became settings: five bus and delivery limits (`bus.*`, #975) and five upgrade-recovery waits (`recovery.*`, #979). Unset, each keeps the value it had. `ahub learn list|accept|reject` (#514b, person only) is your verdict on the recurring-unmet-criterion cards. The macOS PTY bridge no longer hangs under load or outlives its caller (#560). The `@staix/agent-mem` pin stays 0.13.0. Upgrade running hubs with `ahub upgrade` and restart Claude sessions so they load the 0.22.0 plugin.
|
|
40
|
+
|
|
33
41
|
## Upgrading to 0.21.0
|
|
34
42
|
|
|
35
43
|
The configured conductor may now run more operational commands from its own shell (#933): `board import`, `intake approve`, `schedule run|pause|resume`, `research backfill`, `bench run`, `source add`, `goal link`, `task unassign`, starting Jcode, and `settings set tasks.escalate_after`; the hub rechecks the role and records each run. Trust, money, attribution and interactive terminals stay the person's: `permission`, `agents trust`, `role assign`, budget changes, `--as-user`, `pet --window`, `watch`, `recovery reattach`, memory changes and every goal change except `link`. Any agent may `task cancel`, `task label` and `task role` a task it owns and list `ahub deliverables` for the tasks it can see. A conductor's `restart --yes`, `upgrade --yes --to <exact-version>` and `recovery resume|abort` need a source-issued continuation bound to that project, and a handoff to a newer release's coordinator stays the person's (#900; see [conductor operations](operations.md#conductor-operations-812-t2-933-900)). `ahub sdlc merge` and the conductor's `ahub doctor --orphans --kill` now print readable output: a script that parses them passes `--json`, which prints what they printed before (#899). `ahub down` stops the hub like `ahub kill`. New and opt-in: `ahub source add|list` (#508), machine-local `schedules` and `folder_triggers` with `ahub schedule` (#512), `ahub intake` draft backlog cards (#511), domain packs with `ahub init --profile <pack>` (#513a), `local.web_fetch` and `local.doc_read` for the on-prem worker (#515), queue backpressure and context headroom admission for new work (#800, #802), and person-only rooms between machines through `ahub broker` and `ahub room` (#446; see [outbound room membership](operations.md#outbound-room-membership-446-x3)). `ahub turns` and `ahub undo` name a dirty git submodule instead of hiding it (#798). A `hub_send` whose `to` is one peer id given as a string now reaches only that peer instead of every peer, and any other malformed `to` is refused before anything is sent (#963). The hub now exact-pins `@staix/agent-mem` 0.13.0, with no storage change for the hub. Upgrade running hubs with `ahub upgrade` and restart Claude sessions so they load the 0.21.0 plugin. Coming from 0.19.0 or older, the notes below apply too.
|
package/docs/security.md
CHANGED
|
@@ -7,7 +7,7 @@ agent-hub connects agents that can each run commands. This page says what the hu
|
|
|
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
9
|
- **Permission prompts stay on unless an operator selects a permission mode.** `ask` leaves native decisions unchanged. `ask-when-needed` grants scoped Claude file tools or Pi/local writes and edits, and a headless Claude implementer's proven shell commands through the host wrapper (#845, below); Kimi and Codex use their own native policies. `never-ask` requires explicit runtime confirmation or a person's console `y` for a config default at hub start. Until that config confirmation the peer runs in `ask`. Kimi's and `local`'s permission requests are relayed to the console and cancelled after `approvals.timeout_s` (default 120 s) of silence; the macOS notification for a waiting request carries the peer and the tool name only. The one exception is the hub's own tools (`hub_send` and the task tools, matched by exact name): Kimi's requests for them are approved once without a prompt and logged by name, the same trust Codex gets through `approval_mode` in the hub's config. They invoke hub-owned operations rather than arbitrary file or shell tools, and every call passes the hub's own checks. A call the always-ask policy classes (below) is never approved this way, whatever its title. Identity comes from the exact permission title, or from the earlier tool-call title bound to the same call id and resolved against the configured MCP servers when the permission title contains argument JSON (Qwen). Payload text and unrelated display titles never establish identity. Configured ACP kinds (#451) never get this exception: a request whose title spells a hub tool exactly still goes to the person approval path, and only a mapped native mode changes what the agent itself asks. For ACP agents, a payload longer than the console shows is marked as cut and never offers a session-wide grant. Pi's write, edit, shell and git write requests are relayed the same way and can be answered `Always allow <tool> until Pi restarts`: later calls of that tool by the same Pi start skip the prompt but stay inside the path guard and sandbox. The grant covers every later call of that tool whatever its payload, is kept in memory only, cannot be given from the dashboard, and each call it allows is logged by tool name. A Pi or `local` request whose call was cancelled or whose turn ended is withdrawn at once: a later answer is refused and grants nothing, an `Always allow` included. One shell command runs without a hub approval: the `!command` a person types in the managed Pi TUI. It stays inside the same sandbox and denylist, and the hub takes it only from the verified owner process of a Pi in TUI mode, while Pi is idle, on a reservation that is used once; a model's own bash call never takes that path (issue #253). `--unattended` turns prompts off, says so loudly, and is never the default; a classed call from a hub-owned ACP peer, the local worker or Pi still waits for a person under it.
|
|
10
|
-
- **A committed config cannot choose launch commands, credential files, data endpoints, permission modes or a wider sandbox.** The machine-local fields (`kimi_cmd`, `permission_modes`, `codex_bin`, `pi.cmd`, `checks`, `mlx.bin`, `mlx.runtimeDir`, `mlx.modelPath`, `omniroute.urls`, `omniroute.access_hosts`, the `omniroute` key files, `memory.worker_url` (still accepted, ignored since 0.16.0), `local.read_allow`, `local.bash_network`, `local.network_allow`, `local.doc_read`, `local.web_fetch`, `terminal.open`, `tasks.auto_pull`, `tasks.queue_backpressure`, `context.max_assignment_used`, `schedules`, `folder_triggers`, `corpus.paths`, `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, 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`).
|
|
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`).
|
|
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.
|
|
@@ -765,6 +765,12 @@ Room encryption authenticates a hub, not local role authority. Remote ids contai
|
|
|
765
765
|
`@` and cannot connect to local control, own tasks, review, plan, conduct, start a
|
|
766
766
|
native runtime or become a recovery session. Incoming chat is explicitly directed
|
|
767
767
|
to a current exposed local native peer, framed as untrusted, and capped at status.
|
|
768
|
+
Since X6 (#992), a remote peer may also ask an exposed local peer to run one
|
|
769
|
+
capability that both persons pinned as `read`: the target's own `invocation.share`
|
|
770
|
+
entry and the source's exact `peer@label` entry. Task and approval refs, and write,
|
|
771
|
+
external or destructive requests, never cross. The target re-applies its schema,
|
|
772
|
+
digest, class and privacy gates before any native request, and an unknown handoff or
|
|
773
|
+
result is `needs_review`, never replayed.
|
|
768
774
|
The same live sender-exposure check applies to restored inboxes. A removed
|
|
769
775
|
sender's registered pending application is quarantined before its plaintext is
|
|
770
776
|
retired, so it remains available for person disposition.
|
package/docs/smoke.md
CHANGED
|
@@ -1763,6 +1763,8 @@ Qwen recovery source/fake proof: saved ids pass through the driver and CLI to AC
|
|
|
1763
1763
|
|
|
1764
1764
|
Source/fixture proof is recorded in verification/2026-10-11-jcode-peer-contract.md; the live owned-daemon and ACP handshake on 0.94.0 and 0.98.1 in verification/2026-10-19-jcode-0.98.1.md. Unverified: real provider/model, native project MCP discovery and hub-tool receipt, native tool policy and stop/child teardown. Native automatic restoration is withheld because the pinned ACP load does not prove the id. No benchmark comparison or default replacement ran.
|
|
1765
1765
|
|
|
1766
|
+
Historical native-owned hub-tool effects are now recorded in the [ids-only AC4 receipt](verification/2026-10-30-jcode-ac4.md). The [fresh isolated 0.99.1 AC6 re-probe](verification/2026-10-30-jcode-ac6.md) still finds no `session/load` identity echo, so automatic recovery remains withheld; no fresh model turn or persisted-conversation restoration is claimed.
|
|
1767
|
+
|
|
1766
1768
|
### Extra Jcode instances (#685, #368)
|
|
1767
1769
|
|
|
1768
1770
|
Source and fixture coverage: `test/jcode-instances.test.ts` (declaration refusals, the bound of 8, home uniqueness, no relay or credential fields), `test/jcode-instance-peer.test.ts` (separate groups, sockets and homes, stopping one leaves the other, argv is the base argv plus the instance's `-p`, `--model` and `--provider-profile`, launch id per owned process, launcher, doctor) and `test/jcode-instance-daemon.test.ts` (home preflight, no file written under the home, status, hub-tools admission only for an owned launch, a forged `AGENTHUB_PEER_ID` refused, the MCP server with no peer id refusing every tool).
|
|
@@ -2100,6 +2102,10 @@ The new receipt must be a previously absent file directly under the fixture root
|
|
|
2100
2102
|
|
|
2101
2103
|
Real-account Q2 receipt: **UNVERIFIED**. The conductor runs this leg with the person after merge. Fake PTY/protocol tests are implementation evidence only. Q3 approvals, Q5 FIFO and Q6 resume are later executable legs. Q4 remains the known native Qwen turn-stop limitation documented in the 2026-10-25 T0 receipt; the runner's owned-group timeout cleanup does not claim a supported in-session turn interruption.
|
|
2102
2104
|
|
|
2105
|
+
## Capability source probes (#445 C0)
|
|
2106
|
+
|
|
2107
|
+
The 2026-10-30 [receipt](verification/2026-10-30-445-capability-probes.md) records what Kimi 2.3.2, Qwen 0.26.1 and Codex 0.166.0 expose for capability discovery, probed in isolated, credential-free homes with a network-denied profile and real config files checked unchanged. Listing legs are live; skill execution on each agent needs an account and stays **UNVERIFIED** until #445 C4. Design consequences: [Capability invocation (#445)](specs/2026-09-19-agent-hub-design.md#capability-invocation-445).
|
|
2108
|
+
|
|
2103
2109
|
## Model and effort capability verification (#266 AC1)
|
|
2104
2110
|
|
|
2105
2111
|
The 2026-10-29 [version-bound capability table](specs/2026-09-19-agent-hub-design.md#vendor-model-and-effort-controls-266-ac1)
|
|
@@ -1170,7 +1170,18 @@ local hub work available. Fake-hub/broker tests are not a live TLS/proxy or X7 r
|
|
|
1170
1170
|
|
|
1171
1171
|
This implements frozen D3/D5 from issue #446, comment 6242775004, with the
|
|
1172
1172
|
approved X4 refinements and R1 clarifications. X5 metadata views are described
|
|
1173
|
-
below;
|
|
1173
|
+
below; X7 live two-machine receipts remain separate.
|
|
1174
|
+
|
|
1175
|
+
### X6 as built (#446, task 187)
|
|
1176
|
+
|
|
1177
|
+
- Cross-hub invocations are read-only and require consent by pinning on both hubs. The source person pins the exact `peer@label`, capability id, digest and class `read` in the existing machine-local `invocation.share`; the target independently requires its current local share pin, effective read class, native schema and every #445 D4 gate. There is no inheritance between peers, instances or labels.
|
|
1178
|
+
- Discovery stays manual: the target person's existing `ahub capabilities` provides the digest out of band. X6 adds no remote discovery RPC or implicit sharing from room exposure alone.
|
|
1179
|
+
- Both sides validate arguments: bounded flat scalar shape at source, actual capability schema at target. Task, approval and other local refs never cross. Write, external and destructive invocations and approval/permission messages remain excluded.
|
|
1180
|
+
- Requests and results use closed, bounded encrypted application envelopes and opaque host provenance at the single room boundary. Generic hub/digest or raw agent invocation envelopes remain refused. A private or uncleared request is refused, never converted into an executable stub; an uncleared result is withheld without its body.
|
|
1181
|
+
- Target admission is bound to the signed room/member and current local capability. Durable invocation identity and result correlation include room, member fingerprint, peer ids, request id and digest. Dedupe never dispatches the same invocation twice; a mismatched or forged result cannot settle another call.
|
|
1182
|
+
- Local and remote nested calls retain the invocation turn identity and refuse depth 2 before another dispatch. Uncertain handoff, lost acknowledgement/result or restored uncertainty stays `needs_review`, never a silent retry. Signed application admission is distinct from a capability result.
|
|
1183
|
+
- Automated proof uses two fake hubs with the real built-in broker and crypto. Native account execution and live two-Mac X7 receipts remain separate and unverified.
|
|
1184
|
+
|
|
1174
1185
|
|
|
1175
1186
|
### Egress and authority
|
|
1176
1187
|
|
|
@@ -3496,6 +3507,7 @@ the queue.
|
|
|
3496
3507
|
- Flow authority remains the separately validated `roles` map; executable checks remain trusted `checks`. Example checks are display data and do not execute. Managed instruction content keeps surrounding text and quotes forged headers.
|
|
3497
3508
|
- Acceptance criteria with ids (#507b) reuse the #481 S8 `criteria` checklist, the only criteria model: `sendReview` lists the recorded criteria (`- <id>: <result>`, `(optional)` for a non-required one) ahead of the profile's checklist lines in every profile, and `Tasks.review` records on the verdict the ids that the reviewer's `unmet` items name, through the pure `unmetCriteriaIds` (an item names a criterion only by starting with its exact recorded id, followed by the end of the item or a delimiter; each id once, in first-named order). The ids come from the board's checklist at the verdict, never from the caller, and are stored as `HistoryEntry.unmetCriteria` (validated: at most 32 distinct bounded ids, on a saved review verdict only) and shown in the public history. The note text is unchanged. A PII task lists and records none; a task with no checklist, a verdict with no unmet items and an item with no matching id add nothing, so every existing request and verdict is byte-identical. #513 packing and #514 learning refer to these ids.
|
|
3498
3509
|
- Recurring unmet criteria (#514a, `src/hub/learning.ts`, `Tasks.proposeCriterionCandidates`) reuse the #481 S3 recurrence path: after a saved verdict that named criteria, the pure `groupUnmetCriteria` groups the ids reviews recorded as `unmetCriteria` in the last 30 days by profile, profile class (the alias when the task has one) and criterion, over rows that are not PII and whose text the candidate screen allows. A group that this verdict named, that has at least 3 tasks forming at least 2 separate pieces of work (`workUnits`) and no card yet (`Criterion candidate key: <profile>:<class>:<id>` in a hub-filed backlog card's detail, the same dedupe as `Review finding key:`) files ONE backlog `plan` card for a person: ids and counts only, plus the `profile_overrides.review_checklist` fragment as text (`overridesDiff`). Nothing is applied: `profile_overrides` is not machine-local (it is read from the tracked `.agenthub/config.json` and the untracked `config.local.json`, layers merged, and is in neither `MACHINE_LOCAL` list), so the person chooses the layer and merges the line into their own checklist, which replaces as a unit. `selectGroups` applies those filters before the per-pass cap of 64 cards, so groups that are settled (already carded, unnamed, under threshold) never use up the cap and starve a new one. A withheld verdict, an evidence-only verdict (#852), a private turn and a PII task file nothing and count for nothing. Per-profile `profile_overrides` application, thresholds as config and decay are not part of this step.
|
|
3510
|
+
- `ahub learn list|accept|reject` (#514b, `Tasks.learnCandidates|learnAcceptPlan|learnClose`, the `learn_*` task ops) is the person's verdict on those cards, and it amends the #514a card text: "nothing in the hub writes it" became "the hub writes it only when the person runs `ahub learn accept`" (cards filed earlier keep their old sentence, which is harmless: the command reads only the key line). Person only end to end: `classifyPeerCommand` gives `learn` no entry (so `console`, no `CONDUCTOR_*` entry), the daemon's generic console-op gate refuses every non-person caller including the configured conductor, and each `Tasks.learn*` method refuses any `by` but the person. `parseCandidateCard` reads ONLY the first line of the detail (`Criterion candidate key: <profile>:<class>:<criterion>`) from a `plan` card whose first history entry is the hub, re-checks the three parts against the shapes the loaders accept and the title against the key; the task count and the line are recomputed from the board (`learnEvidence`, `overridesDiff`), so no text of the card is parsed as a value or written. Accept refuses a card for another profile than the project's, a config that does not load, a list that would pass the loader's 16 lines, and a card with no evidence left; it extends the checklist the profile uses now (the person's, else the shipped one) because `review_checklist` replaces as a unit, skips a line already present, and writes through `settingsSet` with the one managed definition `LEARN_CHECKLIST` (`src/hub/settings.ts`): a function argument that no request field can supply, resolved by no ordinary lookup (`settingDef` does not know it; `undoableDef` does, for undo only), so `settings set`, a dashboard session and a conductor see an unknown setting. The write is to `config.local.json` only, through the same loader check, tracked-file refusal, atomic replace and single undo record as every setting, `restart` class. The card closes through `Tasks.cancel` after the write succeeded (a failed write leaves it open), the verdict in its history and its dedupe key kept, so the criterion is never filed again. The settings event, notice and undo reply for this key carry the line count, never the person's lines.
|
|
3499
3511
|
- Override maps are bounded to 32 class entries and existing base classes; plan selections use existing fields. Role text is 2000 characters, agents 16000, examples 16 strings of 2000 characters, review checklist 16 lines of 500 characters (#507). Native ingestion/retention/measurement remains unverified; configuration changes apply after daemon/MCP restart.
|
|
3500
3512
|
|
|
3501
3513
|
|
|
@@ -6304,3 +6316,84 @@ sessions do not. A new row never grants conductor permission automatically.
|
|
|
6304
6316
|
Safety floors and contract constants are not converted by H1. A later exposed
|
|
6305
6317
|
safety floor must allow tightening only in schema bounds and have its own test;
|
|
6306
6318
|
wire/storage/protocol contracts remain constants.
|
|
6319
|
+
|
|
6320
|
+
## Capability invocation (#445)
|
|
6321
|
+
|
|
6322
|
+
X6 remote reads require an exact source `peer@label` capability/digest/`read` pin plus the target's independent current local read pin and schema/D4 admission. The target person's `ahub capabilities` supplies the digest out of band; discovery remains manual, with no remote listing RPC. Room exposure alone grants no invocation, and no task or approval refs cross. See "X6 as built" under #446.
|
|
6323
|
+
|
|
6324
|
+
Plan of record: the #445 C0 freeze (issue comment of 2026-10-17, owner decisions O1-O5 of 2026-10-18, go-ahead 2026-10-25). Phases C1-C4 implement it in order and amend this section when they differ. C1 (discovery) and C2 (read invocations) are implemented, see "C1 as built" and "C2 as built" below; C3 adds the write/external approval and audit path below; live receipts (C4) remain unverified. The [C0 receipt](../verification/2026-10-30-445-capability-probes.md) records what the native sources actually expose.
|
|
6325
|
+
|
|
6326
|
+
Delegated execution, never shared execution: a peer publishes capabilities, another peer asks for one, and the target runs it in its own turn with its own permissions and sandbox. The hub never calls a native tool itself (Codex `mcpServer/tool/call` and `command/exec` are excluded) and never hands out credentials or MCP connections.
|
|
6327
|
+
|
|
6328
|
+
### Decisions
|
|
6329
|
+
|
|
6330
|
+
- **D1 manifest.** An adapter-definition field (`registry-types.ts`), one discovery path per adapter. Each `Capability` has `id` (`${kind}:${name}`, unique per peer id), `kind` (`command|skill|mcp_tool`), `name` (`/^[A-Za-z0-9_.:-]{1,64}$/`), a `sanitize()`d description of at most 300 characters, an optional flat `inputSchema` (absent means `{ input: string }`), the publisher's optional `sideEffect` (`read|write|external|destructive`), `source` (`acp|codex-skills|published`), `invoke` (`native|prompt`) and a `digest` over the canonical fields. Sources: ACP `available_commands_update` (`invoke: native`, the prompt `/name input`); Codex `skills/list` (`invoke: native`, the `turn/start` item `{ type: "skill", name, path }`); `hub_capabilities_publish` for Claude and others and Codex MCP tools (`invoke: prompt`). No cost hint.
|
|
6331
|
+
- **D2 opt-in and discovery.** `invocation.share` in `.agenthub/config.local.json` only (`invocation` is machine-local and never editable from settings): a positive allowlist keyed by peer id, each entry pinned by digest with the person's `class`. A changed digest unlists the entry until re-pinned; an instance id never inherits its kind's entries. `hub_capabilities { peer? }` lists shared entries only (id, schema, effective class, the target's permission mode); unshared ones appear only in the person's `ahub capabilities`.
|
|
6332
|
+
- **D3 authority.** Callers hold the new `invoke` hub capability; the conductor gets nothing extra. Effective class is the higher of the declaration and the person's `class` (O1); a side that names no class contributes nothing, and only when neither names one is the class `destructive` (O6). Gates: `read` needs no task and no approval; `write` and `external` need an open task whose owner or reviewer is the caller plus person approval on every call through `requestPermission` with a `personOnly` flag that refuses unattended mode (O2). The person-only prompt shows caller, target, capability, effective class, task id and argument digest, plus already PII-screened arguments sanitized and capped at 200 characters per value with a cut marker. Argument previews stay on person approval surfaces only, never logs, events, history or peer-visible views; `destructive` is refused (O3). Every hub-side refusal is a `PreEffectToolRefusal` with a precise reason before any envelope. A callee refuses with `[REFUSED] <reason>` (outcome `refused`); a native rejection is `failed`. Each invocation is a row in an `invocations` table in `hub.db` (ids, digest, class, task, approval id, args sha256, outcome, times, result bytes) plus an ids-only event; a task-scoped one adds history through `Tasks.recordInvocation`. Bodies are never logged.
|
|
6333
|
+
- **D4 PII.** `hub_invoke` is refused during a PII turn, for a PII task, and when the Tasks PII screen finds or cannot rule out PII in free-text args. The result is bounded to 8 KiB, `sanitize()`d, given a hub-written untrusted header, addressed to the caller only and never captured to memory.
|
|
6334
|
+
- **D5 transport.** New envelope kinds `invocation` and `invocation_result`, journalled, delivered alone and never condensed, priority `status`. `hub_invoke` returns `{ invocation, state: "queued" }` after admission and any required person approval. Deadline `invocation.timeout_s` (default 600): still queued means `expired`; delivered without a result means `timeout` for `read` and `needs_review` otherwise. No caller cancel; the person uses `ahub queue resolve`. Offline, paused, held, off-roster and pull-only targets are refused (O4). Depth 1: no `hub_invoke` inside an invocation turn. `invocation` joins `DAEMON_FEATURES`; upgrade and restart refuse while invocation rows are open on a target without it.
|
|
6335
|
+
- **D6 failures.** An uncertain `write` or `external` outcome is `needs_review` and never retried; a `read` may be redelivered once, on a `failed_safe` receipt only.
|
|
6336
|
+
- **O5.** `invoke: prompt` capabilities are listed and invocable, labelled as model-mediated requests.
|
|
6337
|
+
- **O6 (2026-10-30).** `destructive` applies only when neither the publisher nor the person's share entry names a class. It relaxes the freeze's "undeclared reads as `destructive`", which with O1 would have refused every native capability, since no native listing declares a class.
|
|
6338
|
+
|
|
6339
|
+
### What C0 found (binding on C1)
|
|
6340
|
+
|
|
6341
|
+
Probed on Kimi 2.3.2, Qwen 0.26.1 and Codex 0.166.0 in isolated, credential-free homes; the card's older versions were not probed.
|
|
6342
|
+
|
|
6343
|
+
- **Pins.** A source lists only on the version its receipt names. Any other version lists nothing until a new receipt covers it.
|
|
6344
|
+
- **ACP listings need a session.** Both agents refuse `session/new` without auth (`-32000`) and send `available_commands_update` only after a session exists, so C1 reads the list from an authenticated peer's session, never from a probe of its own.
|
|
6345
|
+
- **Builtins must be filtered.** Kimi and Qwen mix session builtins (Kimi `compact`; Qwen `clear`, `config`, `export`, `update`, `remember`, `forget`, `auth`) into the same list as skills. C1 lists only entries it can tell are skills: Qwen by `_meta.source`, Kimi by its `skill:` prefix. A builtin is never listed.
|
|
6346
|
+
- **No schema, no class.** No native listing carries an input schema or a side-effect class (Kimi and Qwen give only an `input.hint`), so native capabilities get `{ input: string }` and their effective class is the person's share `class` (O6). A share entry without a class leaves the capability `destructive`, so refused.
|
|
6347
|
+
- **Names.** Kimi names contain `:` and `.` and fit the regex. The id of a Kimi skill is `command:skill:<name>`; C1 keeps the native name unchanged rather than stripping the prefix.
|
|
6348
|
+
- **Native rejection.** Qwen rejects a command outside its ACP set with `-32603`; Kimi ends the turn normally with an "Unknown ACP command" text chunk (source reading). C2 maps Qwen's error code and matches Kimi's text to `failed`.
|
|
6349
|
+
- **Codex.** `skills/list` answers with no thread and no auth. The `skill` input item is in the stable schema; a live skill turn is unverified. `mcpServer/tool/call` has no approval gate in the 0.166.0 source, which confirms its exclusion.
|
|
6350
|
+
- **Unverified until C4.** Executing a skill through `/name input` on Kimi or Qwen and a Codex `skill` turn (each needs an account), and listings under a real login.
|
|
6351
|
+
|
|
6352
|
+
### C1 as built (#445, task 178)
|
|
6353
|
+
|
|
6354
|
+
C1 lists capabilities and nothing else: no `hub_invoke`, no `invoke` capability, no envelope kinds, no `invocations` table. These points are where the build fixes or differs from the freeze above.
|
|
6355
|
+
|
|
6356
|
+
- **Where.** `src/hub/capabilities.ts` holds the pure rules (digest, sanitizer, flat schema, publish validation, share listing). The two native parsers and their version pins are vendor-specific, so they live in `src/adapters/native-capabilities.ts`, where the #451 registry guard allows vendor literals. `AcpPeer` and `CodexPeer` implement a new optional `PeerAdapter.capabilityManifest()`; the daemon reads it per call, never a cached or probed copy. The per-definition `capabilities.manifest` field of D1 is not used: the port on the adapter is.
|
|
6357
|
+
- **No class is declared natively.** `Capability.sideEffect` is optional and absent for every native listing, so the digest carries `null` for it. `effectiveClass` is the higher of the declared and the person's class, a side that names none contributes nothing, and only when neither names one is the class `destructive` (O6). A value that is not one of the four reads as `destructive`. The `exec` class is removed from the manifest type.
|
|
6358
|
+
- **Pins.** `NATIVE_PINS` is the one place the probed versions are written: Kimi 2.3.2 (`Kimi Code CLI`), Qwen 0.26.1 (`qwen-code`), Codex 0.166.0. A listing on any other version is empty. The repository's Codex pin is 0.154.0, so a pinned Codex lists nothing until a receipt covers it; this is intended and shown in `ahub help capabilities`.
|
|
6359
|
+
- **ACP lists belong to a session.** The `available_commands_update` of the current session is kept whatever the turn state. One that arrives before `session/new` is answered waits and counts only when it names the session that was bound; one for another session is dropped. A restart clears the list. An offline peer lists nothing.
|
|
6360
|
+
- **Codex.** `skills/list { cwds: [project], forceReload: false }` goes through the attached app-server connection with a hub id, so the TUI never sees the answer. Enabled skills only; the short description is used when there is one. An unattached, offline or unanswered peer rejects, which the daemon shows as no list.
|
|
6361
|
+
- **Config.** `invocation` is a machine-local block validated by the closed schema (`invocation.share` only in C1; `timeout_s` arrives with C2) and applies live. `hub_capabilities` reads the share from the running config on every call.
|
|
6362
|
+
- **Tools.** `hub_capabilities { peer? }` is a read tool, exposed to read-only projects; it needs no task and no approval. `hub_capabilities_publish { capabilities }` is the source for a session the hub cannot read natively: a peer whose adapter implements `capabilityManifest` is refused, every field is checked, an unknown field refuses the whole call, the invoke form is forced to `prompt`, the list replaces the last one and is bound to the publishing connection (the daemon's connection generation), and it is dropped when the peer goes offline. Both calls are refused during a PII turn and for a peer holding PII, and neither is captured to memory (`CAPABILITY_TOOLS`).
|
|
6363
|
+
- **The person's view.** `ahub capabilities` (op `capabilities_list`, Console only, no `CONDUCTOR_TASK_OPS` entry) shows every found entry with its state (`listed`, `unshared` or `digest-changed`) and the class that would apply, for every peer, shared or not. The audit label is `capabilities` with no argument echoed.
|
|
6364
|
+
- **Not decided here.** Whether a Codex skill can be run through a `skill` turn item, and how a Kimi or Qwen `/name input` behaves under a real login, stay C0's unverified list and move to C2 and C4.
|
|
6365
|
+
|
|
6366
|
+
### C2 as built (#445, task 179)
|
|
6367
|
+
|
|
6368
|
+
C2 runs `read` invocations end to end. `write` and `external` stay refused (the person-approval path is C3) and `destructive` is refused for good (O3). These points are where the build fixes or differs from the freeze.
|
|
6369
|
+
|
|
6370
|
+
- **Where.** `src/hub/invocations.ts` holds the pure gates (`invocationRefusal`), the argument check (`checkArgs`), the two texts the hub writes (`requestText`, `renderInvocationResult`), `refusedBy` and the `InvocationLedger`. The daemon's `hub_invoke` branch sits beside the C1 capability tools in `taskOpBody`. Adapters are unchanged.
|
|
6371
|
+
- **Every invocation is a model-mediated request in C2.** The freeze's `invoke: native` form (the target's own `/name input` path, a Codex `skill` turn item) is not implemented: it needs a live account to verify and is C4. The target is handed one hub-written envelope naming the caller and the capability, and answers it as an ordinary turn under its own permissions and sandbox. `invoke: native` capabilities are therefore invocable only as that request, and the request says so.
|
|
6372
|
+
- **Transport.** The request is an envelope from the hub to the target alone (`kind: invocation`, `priority: status`, `refs.invocation` = its own id). The callee's answer is taken over inside the bus, before anything is built from it (`Bus.onAnswer`): it is recognised by what the peer was handed (a delivery holding an `invocation` request) when it names that request, or names no parent while that invocation's own turn remains open, never by its recipient address. Once the turn ends, an unnamed later message is ordinary traffic and is published; an explicit late or repeated reply naming the request remains captured and dropped. Important messages are not steered into the invocation's open turn, so they cannot change its reply parent. A headless agent whose final answer is normally shared with every sender therefore cannot leak that invocation's answer. A taken answer is published nowhere, stored nowhere and emitted in no event, so no tap, queue, digest, recall preface, journal row or memory path sees it, and a late or repeated answer is dropped rather than broadcast. The daemon settles the row and publishes ONE `invocation_result` from the hub to the caller only, with a hub-written first line, the callee's text sanitized (a forged hub header is quoted) and cut to 8 KiB by code point. An explicit `hub_send` with `reply_to` the request is the same answer. A reply starting `[REFUSED]` is `refused`. The id of the request, the ledger row and the result are one id. Both kinds are in `KINDS` (`envelope.ts`), which the journal, the restart reader, the deferred-notice check and the held-delivery hints all read.
|
|
6373
|
+
- **Alone, never condensed.** `Bus.take` and `Bus.takeBudget` end a batch at an invocation or result envelope, and never put one beside another. The daemon's tap handles these envelopes first and returns, so no log line carries a body and no memory citation sees one.
|
|
6374
|
+
- **Gates, all before any envelope, one reason each.** Order: PII context, depth, own capability, off roster, offline, paused, held, pull-only, no shared capability (an unshared entry and a missing one read the same, so an agent learns nothing about what the person withheld), digest changed, destructive, write or external (C3), task not yours or PII, per-caller and per-target open caps (4 and 8). Then the arguments against the capability's flat schema (a native form refuses a newline), then the Tasks pattern check and, when the screen is on, the model screen on free-text arguments, with unknown refused. Every fact is read again after that await and the gates run again. Depth is the target holding a `delivered` row: the caller of a nested invocation is the target of an open one.
|
|
6375
|
+
- **Result privacy.** The callee's answer goes through the pattern check, the secret check, the path policy and, when `signals.pii_screen` is on, the model screen, with unknown treated as PII, and it is checked again after that await. The full answer is screened, rather than only its shortened refusal reason, before any reason is forwarded. If the callee's turn is private (`turns.get(target).private`, `contextPii`) or the scoped task is PII, or any check does not clear it, an answer starting `[REFUSED]` retains outcome `refused` and returns only the fixed reason `<target> declined; the reason was withheld`; no callee text is kept. An ordinary unsafe answer retains outcome `withheld` and its existing fixed text, with no body kept anywhere. Unknown or a screen failure follows the same refusal-versus-ordinary distinction; a clean refusal retains its screened reason.
|
|
6376
|
+
- **Pause and budget.** A target paused by the person, the conductor or the budget, or whose quota window is exhausted, is refused at call time. A pause that starts while the request is queued leaves it queued under its deadline. The caller is charged the ordinary per-sender and per-pair limits as for a message to the target (the request itself is a hub envelope, which no limit applies to), and every invocation is `priority: status`.
|
|
6377
|
+
- **Deadline and receipts.** `invocation.timeout_s` (10 to 86400, default 600). A 5 s sweep (`invocationSweepMs` is a test seam) withdraws a request whose ledger row is `queued` and that is still waiting in the target's delivery queue at its deadline (`expired`, no effect). A ledger row can still say `queued` after its request was handed over but before the adapter reports acceptance; if the request is no longer in that queue, it may already be running and receives `timeout`, as does a `delivered` read with no result. The bus marks a row `delivered` from `onDelivered` (actual transport admission) and the daemon settles it `failed` on `undeliverable`. A row settles once, so a late answer after a timeout changes nothing. The freeze's one redelivery of a read after a `failed_safe` receipt is the bus's own retry (it requeues the original, at most `bus.max_attempts` times) rather than a second mechanism.
|
|
6378
|
+
- **Record.** The `invocations` table in `hub.db` holds ids, digest, class, task, a digest of the arguments, outcome, times and result bytes, never an argument or a result. An ids-only `invocation` event is written at queue and at settle. `hub_invoke` joins the capture skip list. The task history entry (`Tasks.recordInvocation`) is C3, with the first call that can change something.
|
|
6379
|
+
- **Feature flag.** `invocation` is in `DAEMON_FEATURES`. The Claude plugin refuses `hub_invoke` with a clear message against a hub without it, and recovery refuses a target without the feature while any invocation row is open. `PROTOCOL` stays 16.
|
|
6380
|
+
- **Not in C2.** The `invoke` hub capability is a name in `capabilities.<peer>` and defaults on, as every other (an unlisted peer has all of them). Per-class gates beyond read, the approval path, `personOnly`, the PII task refusals for write, and every live receipt are C3 and C4.
|
|
6381
|
+
|
|
6382
|
+
|
|
6383
|
+
|
|
6384
|
+
### C3 as built (#445, task 180)
|
|
6385
|
+
|
|
6386
|
+
- Write/external admission requires an open non-PII task whose owner or reviewer is the caller. Unattended refuses before prompting. Destructive remains refused. Person approval uses allow-once/deny only; the existing native forceAsk policy is unchanged.
|
|
6387
|
+
- The person-only prompt names caller, target, capability digest, effective class, task and arguments digest. It also shows screened, sanitized arguments, at most 200 Unicode characters per value, marked when cut. The prompt is available through the authenticated console/dashboard permission path; no argument preview is logged, emitted as an event, placed in task history or returned in a peer-visible permission view.
|
|
6388
|
+
- Admission rechecks session generations, current capability/share/digest/class, task scope/privacy, turn privacy, roster, pause/hold, depth, open caps and shutdown after asynchronous screens/approval. A changed binding refuses the call; an approval never follows a changed task or capability.
|
|
6389
|
+
- Control requests for `hub_invoke` cover the supported 3600-second approval ceiling plus admission/transport margin. MCP cancellation and client abandonment cancel only that connection's pending request; closing its control connection aborts the pending permission. Current admission checks the abort fence after every await, so a late allow cannot dispatch a disconnected or abandoned call. An already admitted native turn is never cancelled through this path.
|
|
6390
|
+
- The existing ledger approval column stores the actual person approval id. Task-scoped calls record queued metadata through host-only `Tasks.recordInvocation` before publication, and terminal metadata after settle-once. History carries ids, digests, class, approval and outcome only; it changes no task state and emits no task notification. A failed queued audit prevents delivery.
|
|
6391
|
+
- Only a `read` may automatically redeliver, at most once and only after `failed_safe`, independent of `bus.max_attempts`. Write/external safe refusal ends failed without replay; an ambiguous receipt, thrown handoff, restored uncertain delivery or delivered deadline is `needs_review` and holds the journal. Recipient retry cannot replay invocation requests; the person's existing queue resolution remains explicit. A queued request never handed over still expires without effect.
|
|
6392
|
+
- Capability execution remains model-mediated under the callee's own sandbox and permissions. Native forms, live-account execution and C4 receipts are not supplied by this change.
|
|
6393
|
+
|
|
6394
|
+
|
|
6395
|
+
## Amendment: shipped writing, analysis and ops packs (#513b)
|
|
6396
|
+
|
|
6397
|
+
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
|
+
|
|
6399
|
+
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.
|