@staix/agent-hub 0.12.28 → 0.12.29
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 +6 -0
- package/README.md +2 -2
- package/docs/agent-notes/daemon.md +3 -0
- package/docs/operations.md +45 -7
- package/docs/quickstart.md +1 -1
- package/docs/security.md +26 -1
- package/docs/smoke.md +54 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +8 -1
- package/docs/verification/2026-10-16-0.12.29.md +25 -0
- package/docs/verification/2026-10-16-273-live-verification.md +150 -0
- package/docs/verified.json +11 -11
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +1 -1
- package/src/cli/console.ts +3 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/identity-audit.ts +2 -2
- package/src/cli/identity.ts +14 -0
- package/src/cli/main.ts +42 -25
- package/src/cli/upgrade-runtime.ts +7 -2
- package/src/hub/daemon.ts +7 -4
- package/src/hub/events.ts +1 -1
- package/src/hub/lifecycle.ts +2 -2
- package/src/hub/manager.ts +14 -10
- package/src/hub/ui.ts +10 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Issue and pull request numbers in the entries for 0.7.7 and earlier refer to the previous repository, archived on 2026-09-30 when this repository's history was rewritten; the one exception is the open smoke-check issue, formerly #12, which moved here as #1. Numbers in newer entries refer to this repository.
|
|
4
4
|
|
|
5
|
+
## 0.12.29
|
|
6
|
+
|
|
7
|
+
- Let an explicitly authorized agent run live verification through `ahub --live-test`, with operational console authority scoped to the invocation and its ahub continuations, the initiating peer retained in a mandatory delegation audit, and ordinary agent permissions unchanged (#408).
|
|
8
|
+
|
|
9
|
+
- Remove the site caveat card accent edge and refresh the #273 live-verification ledger, including the machine-wide upgrade isolation requirements (#406, #273).
|
|
10
|
+
|
|
5
11
|
## 0.12.28
|
|
6
12
|
|
|
7
13
|
- Recover an offline Pi owner with person-selected preserved history via `ahub pi --replace-session --session-file`, retaining prior identity metadata and queued work while refusing live owners and unsettled effects (#403).
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Native multi-agent hub for one developer's machine: Claude Code, Codex, Kimi Cod
|
|
|
4
4
|
hub-owned local-LLM worker, with optional Qwen Code and Jcode support, collaborate as peers in independent project directories, with
|
|
5
5
|
task-aware model routing (Switchyard) in front of a self-hosted gateway (OmniRoute).
|
|
6
6
|
|
|
7
|
-
Status: 0.12.
|
|
7
|
+
Status: 0.12.29, control protocol 16. Durable delivery records distinguish queued
|
|
8
8
|
work from uncertain execution. The [smoke checklist](docs/smoke.md) records
|
|
9
9
|
verified paths and remaining prerequisites.
|
|
10
10
|
|
|
@@ -33,7 +33,7 @@ cd <your project> && ahub init && ahub up
|
|
|
33
33
|
Or install the same version from GitHub:
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
bun add -g github:STAIxBWLB/agent-hub#v0.12.
|
|
36
|
+
bun add -g github:STAIxBWLB/agent-hub#v0.12.29 && ahub setup
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
The installed commands remain `ahub` and `agent-hub`.
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Daemon, control protocol and recovery
|
|
2
2
|
|
|
3
|
+
- For explicitly user-authorized live verification, use the per-invocation CLI delegation in `docs/security.md` §Agent CLI identity and conductor authority; keep caller markers and record native evidence separately from fixtures.
|
|
4
|
+
- Keep `export const LIVE_TEST_SUPPORTED = true;` in `src/cli/identity.ts`: delegated recovery staging reads this declaration before any source effect.
|
|
5
|
+
|
|
3
6
|
- Console attention navigation selects a delivery only by an exact visible row id. A missing id or row prints read-only guidance; never select by peer or fall back to the first Queue row.
|
|
4
7
|
|
|
5
8
|
Scope: the operator console, control WS and its protocol, state files, the dashboard listener and settings, the Claude channel's reconnect, `ahub setup`, git snapshots, upgrade and recovery. Read before editing `src/cli/console.ts`, `src/cli/console-state.ts`, `src/cli/attention.ts`, `src/ui/needs.ts`, `src/hub/daemon.ts`, `src/hub/ui.ts`, `src/hub/settings.ts`, `src/hub/control-client.ts`, `src/adapters/claude-channel.ts`, `src/hub/restart.ts`, `src/hub/recovery-store.ts`, `src/hub/snapshots.ts`, `src/hub/manager.ts`, `src/hub/lifecycle.ts`, `src/hub/crash.ts`, `src/hub/start-mode.ts`, `src/hub/terminal-open.ts`, `src/hub/recovery-error.ts`, `src/hub/db-timeout.ts`, `src/cli/setup.ts`, `src/cli/upgrade*.ts`, `src/cli/terminal-recovery.ts`, `src/cli/recovery-package.ts` or `src/cli/facts-hook.ts`, or before adding a native peer.
|
package/docs/operations.md
CHANGED
|
@@ -1,8 +1,46 @@
|
|
|
1
1
|
# Operations guide
|
|
2
2
|
|
|
3
|
-
This guide describes ahub 0.12.
|
|
3
|
+
This guide describes ahub 0.12.29 and control protocol 16. Live verification
|
|
4
4
|
results and remaining prerequisites are recorded separately in [the smoke ledger](smoke.md).
|
|
5
5
|
|
|
6
|
+
## Agent-operated live verification
|
|
7
|
+
|
|
8
|
+
When the user explicitly authorizes a live test, its agent may run console
|
|
9
|
+
commands using `ahub --live-test [--project <path|id>] <command> [args...]`.
|
|
10
|
+
For example, `ahub --live-test --project <scratch-path> ui --no-open` starts the
|
|
11
|
+
real dashboard listener and prints its one-time link without opening a browser.
|
|
12
|
+
`ahub --project <scratch-path> --live-test reset --all --yes` exercises reset on
|
|
13
|
+
that selected disposable project. The global options precede the command and
|
|
14
|
+
may appear in either order; a flag after the command is not delegation.
|
|
15
|
+
|
|
16
|
+
This grants console authority for operational console-class and peer-lifecycle conductor-class commands, including destructive actions, for one
|
|
17
|
+
invocation and its ahub continuations. Use only the user-authorized targets and
|
|
18
|
+
commands; destructive live checks belong in disposable projects. Existing
|
|
19
|
+
project discovery still applies: initialize the scratch folder as its own Git
|
|
20
|
+
repository before `ahub init`, then confirm `status --json` reports that exact
|
|
21
|
+
`cwd` before any destructive check.
|
|
22
|
+
Existing confirmations, process ownership checks, native identity fences and recovery
|
|
23
|
+
locks still apply. Do not remove agent markers. The initiating peer and command
|
|
24
|
+
label are audited before effects; an unavailable audit blocks delegation. A new
|
|
25
|
+
unflagged invocation has ordinary peer authority again. Native tools launched
|
|
26
|
+
by the command keep their own peer identity and receive no delegation flag.
|
|
27
|
+
Allowed-class commands keep peer identity and permissions even with the flag:
|
|
28
|
+
`--live-test say` sends as the peer. Peer starts, pause and resume may use
|
|
29
|
+
delegated console authority. Flagged task assign/escalate refuse; omit the flag
|
|
30
|
+
to use the ordinary actor-preserving conductor path when the peer has that role.
|
|
31
|
+
Person verdicts and user-authored workflow writes are not delegated. Dashboard
|
|
32
|
+
sessions issued through the flag retain operational controls but refuse message
|
|
33
|
+
and task/planning content writes. Use native peer tools for agent-authored work.
|
|
34
|
+
|
|
35
|
+
Use a CLI/coordinator containing this feature throughout the experiment. Older
|
|
36
|
+
daemons do not understand the new delegated audit outcome; run the candidate
|
|
37
|
+
daemon to capture its delegated event and console notice. This policy does not
|
|
38
|
+
change external computer-use restrictions. Recovery staging refuses old targets
|
|
39
|
+
without live-test support before source effects. Delegated headless recovery restores through the ordinary target CLI, carrying
|
|
40
|
+
its operationId on the delegated console connection; existing native ownership
|
|
41
|
+
and session-identity checks remain. Record actual observations in the
|
|
42
|
+
smoke ledger; the presence of this option proves no outstanding #273 live leg.
|
|
43
|
+
|
|
6
44
|
## Command output
|
|
7
45
|
|
|
8
46
|
`ahub status`, `board`, `budget`, `doctor`, `projects`, `queue list`, `turns` and `report` print shared console tables and labelled details.
|
|
@@ -1621,21 +1659,21 @@ section is what they run.
|
|
|
1621
1659
|
|
|
1622
1660
|
Upgrade running projects with the target release's own coordinator. It accepts
|
|
1623
1661
|
a running source on control protocol 9 (0.6.x), 10 (0.7.0 through 0.12.0),
|
|
1624
|
-
11 (0.12.1 and 0.12.2), 12 (0.12.3), 13 (0.12.4 through 0.12.15), 14 (0.12.16), 15 (0.12.17 through 0.12.19) or 16 (0.12.20 through 0.12.
|
|
1662
|
+
11 (0.12.1 and 0.12.2), 12 (0.12.3), 13 (0.12.4 through 0.12.15), 14 (0.12.16), 15 (0.12.17 through 0.12.19) or 16 (0.12.20 through 0.12.29), and only
|
|
1625
1663
|
a target on its own protocol, so the target's coordinator fits every supported
|
|
1626
1664
|
source and carries every recovery fix released up to it. Protocol 8 and older
|
|
1627
1665
|
(0.5.x and earlier) are refused as `manual-bootstrap-required`. Run from the
|
|
1628
1666
|
project directory, without replacing the global CLI first:
|
|
1629
1667
|
|
|
1630
1668
|
```bash
|
|
1631
|
-
bunx --package @staix/agent-hub@0.12.
|
|
1632
|
-
bunx --package @staix/agent-hub@0.12.
|
|
1669
|
+
bunx --package @staix/agent-hub@0.12.29 ahub upgrade --to 0.12.29 --dry-run
|
|
1670
|
+
bunx --package @staix/agent-hub@0.12.29 ahub upgrade --to 0.12.29 --yes
|
|
1633
1671
|
```
|
|
1634
1672
|
|
|
1635
1673
|
| Running now | Coordinator to use |
|
|
1636
1674
|
| --- | --- |
|
|
1637
1675
|
| 0.6.x (protocol 9) | the target's, through `bunx` as above |
|
|
1638
|
-
| 0.7.0 through 0.12.0 (protocol 10), 0.12.1 and 0.12.2 (protocol 11), 0.12.3 (protocol 12), 0.12.4 through 0.12.15 (protocol 13), 0.12.16 (protocol 14), 0.12.17 through 0.12.19 (protocol 15), 0.12.20 through 0.12.
|
|
1676
|
+
| 0.7.0 through 0.12.0 (protocol 10), 0.12.1 and 0.12.2 (protocol 11), 0.12.3 (protocol 12), 0.12.4 through 0.12.15 (protocol 13), 0.12.16 (protocol 14), 0.12.17 through 0.12.19 (protocol 15), 0.12.20 through 0.12.29 (protocol 16) | the target's, through `bunx` as above |
|
|
1639
1677
|
| any supported source, with the installed CLI already at the target | `ahub upgrade` below, which is the same coordinator |
|
|
1640
1678
|
| 0.5.x or earlier (protocol 8 and older) | not supported: bootstrap by hand with the matching CLI |
|
|
1641
1679
|
|
|
@@ -1667,14 +1705,14 @@ projects first:
|
|
|
1667
1705
|
|
|
1668
1706
|
```bash
|
|
1669
1707
|
ahub restart --dry-run
|
|
1670
|
-
ahub upgrade --to 0.12.
|
|
1708
|
+
ahub upgrade --to 0.12.29 --dry-run
|
|
1671
1709
|
```
|
|
1672
1710
|
|
|
1673
1711
|
Apply only after reviewing the plan:
|
|
1674
1712
|
|
|
1675
1713
|
```bash
|
|
1676
1714
|
ahub restart --yes
|
|
1677
|
-
ahub upgrade --to 0.12.
|
|
1715
|
+
ahub upgrade --to 0.12.29 --yes
|
|
1678
1716
|
ahub recovery status [<operation-id>]
|
|
1679
1717
|
ahub recovery resume [<operation-id>]
|
|
1680
1718
|
ahub recovery abort [<operation-id>]
|
package/docs/quickstart.md
CHANGED
package/docs/security.md
CHANGED
|
@@ -71,10 +71,35 @@ or the native Claude/Codex shell marker when the hub marker is absent. It connec
|
|
|
71
71
|
as that peer in tools mode. Messages and task changes retain that actor;
|
|
72
72
|
`say` defaults to status priority and important messages still require the peer's
|
|
73
73
|
capability. Conflicting or malformed markers fail closed. There is no `--as-user`.
|
|
74
|
-
|
|
74
|
+
By default, console operations are refused before connecting, including permission answers
|
|
75
75
|
and permission modes, queue resolution, budget overrides, benchmark runs, research
|
|
76
76
|
backfill and task labels, stopping a peer (`ahub stop`), lifecycle and recovery
|
|
77
77
|
operations, and `ask`.
|
|
78
|
+
For live verification explicitly authorized by the user, an agent may invoke
|
|
79
|
+
`ahub --live-test [--project <path|id>] <command> [args...]`. The flag delegates
|
|
80
|
+
the existing console authority for console-class operations and conductor-class peer starts/holds, including approvals,
|
|
81
|
+
settings, roster and lifecycle operations. It does not restrict effects to a
|
|
82
|
+
scratch project: the agent must stay within the authorized commands and targets.
|
|
83
|
+
Malformed or conflicting caller markers still refuse. The original peer and
|
|
84
|
+
closed command label are recorded with outcome `delegated` before execution;
|
|
85
|
+
if that audit cannot be written, the command refuses. The current daemon emits
|
|
86
|
+
`agent_cli` with `delegated: true` and `refused: false`. Allowed-class commands retain the peer identity and capability checks, including
|
|
87
|
+
messages, proposals and reviews. Peer starts, pause and resume may delegate
|
|
88
|
+
operational console authority; task assign/escalate under the flag refuse. `--as-user`, person task labels,
|
|
89
|
+
person-authored planning/release/memory/goal writes, SDLC record, benchmark run
|
|
90
|
+
and the pet composer are refused. No argument text is copied into this audit.
|
|
91
|
+
Delegated dashboard restrictions are bound to its server-issued ticket/session:
|
|
92
|
+
operational settings/start/stop controls remain, but message, proposal and
|
|
93
|
+
workflow-history actions refuse even if the body tries to clear the restriction.
|
|
94
|
+
The flag is not stored in settings or exported to native agent children. Ahub's
|
|
95
|
+
own daemon launch, console subprocesses, recovery runner, interactive follow-up commands and upgrade coordinator
|
|
96
|
+
handover carry it explicitly in argv. Each new top-level invocation needs its
|
|
97
|
+
own flag. An older CLI does not support it; use a candidate with this feature
|
|
98
|
+
for the continuation too. Staging rejects a target without the pinned
|
|
99
|
+
`LIVE_TEST_SUPPORTED` declaration before preparation or commit, including a
|
|
100
|
+
previously cached stage. Delegated headless-native restoration uses the ordinary target CLI with its
|
|
101
|
+
operationId through that operational console authority. Native and TUI
|
|
102
|
+
restoration keep their existing identity and ownership checks. External computer-use restrictions remain unchanged.
|
|
78
103
|
The operator uses a plain terminal or `ahub console`. A Claude `!` command that
|
|
79
104
|
inherits the agent markers follows the same rule. The console strips control
|
|
80
105
|
sequences from agent and daemon text before it quotes forged headers, and its
|
package/docs/smoke.md
CHANGED
|
@@ -2,6 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
`scripts/check.sh` covers everything against fakes. The legs below need real accounts and an interactive terminal, so they are run by hand and recorded here.
|
|
4
4
|
|
|
5
|
+
## Issue #273 operator execution plan (2026-10-16)
|
|
6
|
+
|
|
7
|
+
The [execution plan and blocker record](verification/2026-10-16-273-live-verification.md)
|
|
8
|
+
maps the outstanding issue entries to their procedures and required observations.
|
|
9
|
+
The installed CLI and running project daemon both reported 0.12.28. This is an
|
|
10
|
+
environment readback only. No outstanding live leg passed in this session:
|
|
11
|
+
the installed 0.12.28 CLI refused `ahub ui` from Codex, and LCU refused Terminal access.
|
|
12
|
+
These are dated observations. From 0.12.29, authorized agents use the audited
|
|
13
|
+
`--live-test` route described below and in the execution plan; the external LCU
|
|
14
|
+
restriction remains separate. The operator initially chose procedure preparation;
|
|
15
|
+
#273 remains open.
|
|
16
|
+
|
|
17
|
+
## Agent-operated verification (#408)
|
|
18
|
+
|
|
19
|
+
For user-authorized live checks, the candidate CLI supports
|
|
20
|
+
`ahub --live-test [--project <path|id>] <command> [args...]`, using audited console
|
|
21
|
+
authority for that invocation. Follow [the operating procedure](operations.md#agent-operated-live-verification)
|
|
22
|
+
and record each actual result below. References to a person in historical
|
|
23
|
+
procedures describe their original authority requirement; an explicitly
|
|
24
|
+
authorized agent may now execute those CLI steps through this option. Native
|
|
25
|
+
account readiness, real terminal/browser observations, sign-off requirements and
|
|
26
|
+
external computer-use restrictions still apply. No pending #273 item is checked
|
|
27
|
+
by adding this path.
|
|
28
|
+
|
|
29
|
+
2026-10-16, macOS, candidate CLI/source `40d13bc4`, daemon 0.12.28/protocol 16:
|
|
30
|
+
an actual Codex-marked shell with a separate hub home and disposable Git project
|
|
31
|
+
ran delegated `init`, `up --no-console`, `ui --no-open`, `permission pi ask`,
|
|
32
|
+
`kill` and `reset --all --yes`, all exit 0. Unflagged `status --json` reported
|
|
33
|
+
the exact scratch cwd. The dashboard URL returned HTTP
|
|
34
|
+
200 and HTML; no browser or ticket/session exchange was performed in that shell run. The daemon recorded
|
|
35
|
+
`agent_cli` events for init/up/daemon/ui/permission with peer `codex`,
|
|
36
|
+
`delegated: true` and `refused: false`. The reset archive mode was 0700 and the
|
|
37
|
+
scratch daemon was stopped. This verifies the delegated CLI path, without
|
|
38
|
+
claiming a model turn, terminal look, conversation restoration or the complete
|
|
39
|
+
#214/#273 live criteria. The automated console-subprocess and recovery-target
|
|
40
|
+
checks remain separate fixture evidence.
|
|
41
|
+
|
|
42
|
+
2026-10-16 review follow-up, source `fcbb18ec`: the same isolated Git scratch
|
|
43
|
+
project was started from a Codex-marked shell. Flagged `say` produced a bus
|
|
44
|
+
envelope from `codex`; person task-label writing refused. A delegated dashboard
|
|
45
|
+
ticket exchanged for a session with HTTP 200, its snapshot returned HTTP 200,
|
|
46
|
+
and a send action trying `liveTest: false` returned HTTP 403. The scratch daemon
|
|
47
|
+
was stopped (kill exit 0). This was an actual CLI/HTTP run, without a model turn
|
|
48
|
+
or browser visual inspection.
|
|
49
|
+
|
|
50
|
+
Task #65 follow-up, source `5fbe50b6`: operational peer starts, pause and resume
|
|
51
|
+
may delegate console authority; allowed-class messages/proposals/reviews retain
|
|
52
|
+
the peer, while flagged task assign/escalate refuse. The temporary headless
|
|
53
|
+
staging ceiling is removed. The recovery driver -> real target CLI -> scripted
|
|
54
|
+
control server regression verifies headless Pi mode, saved sessionId,
|
|
55
|
+
operationId, console hello and initiating-peer audit (225 related tests passed).
|
|
56
|
+
This is fixture/CLI proof, not an actual native Pi restoration; that live leg
|
|
57
|
+
remains owed in #273.
|
|
58
|
+
|
|
5
59
|
## Hub memory (#258)
|
|
6
60
|
|
|
7
61
|
Not run yet (2026-10-12). Source and fakes cover tiers, the graph, retrieval, sync between two simulated homes and a bare remote, leases, import and suggestions (`test/memory-*.test.ts`).
|
|
@@ -1947,10 +1947,17 @@ task list polling does not. `hub_status` stays conductor-only.
|
|
|
1947
1947
|
proposer as the actor with reason `manual` (on the `reassigned` or `reserved`
|
|
1948
1948
|
entry), and writes no conduct audit event; during a PII turn it is refused
|
|
1949
1949
|
like the conductor's.
|
|
1950
|
+
## Amendment: delegated agent live verification (#408)
|
|
1951
|
+
|
|
1952
|
+
- Decision, 2026-10-16: an agent explicitly authorized by the user to perform a live test may use the global `--live-test` flag, before the command and in either order with `--project`. Duplicate global options refuse. Default command classification and malformed/conflicting identity refusals stay unchanged.
|
|
1953
|
+
- Console-class operations and conductor-class peer starts/pause/resume may use console authority after the original caller is validated; allowed-class commands retain peer identity and capability checks. Flagged task assign/escalate refuse rather than writing user-attributed history. A mandatory ids-only audit records its peer, closed command label and `delegated` outcome before execution. The daemon emits an `agent_cli` event with `delegated: true`, `refused: false`, and a console notice naming the peer. User impersonation, person labels and person-authored workflow writes refuse. Delegated dashboard tickets/sessions allow operational controls but refuse user-attributed content and history actions on both project and unified surfaces; request bodies cannot widen the session.
|
|
1954
|
+
- This is a user-authorized operating policy, not a sandbox or authenticated grant. It includes console mutations and requires the agent to remain within the authorized targets and operations. Command-specific confirmations, process ownership, provider identity and recovery locks still apply.
|
|
1955
|
+
- No settings/environment opt-in persists. Native argv does not receive the flag. Ahub's own daemon launch, console subprocesses, recovery runner, interactive continuation commands and newer-version coordinator handover carry it explicitly; older CLIs cannot accept it. The recovery driver's target CLI environment preserves the initiating peer for its delegation audit. Staging checks the target's pinned LIVE_TEST_SUPPORTED declaration before its cached-stage return and before prepare/commit; unsupported old targets refuse without source effects. Headless-native restoration uses the ordinary target CLI with the operationId on its delegated console connection; no headless staging ceiling is added. External computer-use policies remain outside this change. No #273 live success is inferred from this implementation.
|
|
1956
|
+
|
|
1950
1957
|
## Amendment: ahub reset (issue #214)
|
|
1951
1958
|
|
|
1952
1959
|
`ahub reset [--all] [--yes]` returns a project's hub to a clean state. It is
|
|
1953
|
-
|
|
1960
|
+
console-only by default (explicit authorized delegation is described in #408) and a dry run
|
|
1954
1961
|
unless `--yes`: the dry run reads `hub.db` read-only (with `immutable=1` when
|
|
1955
1962
|
`hub.db-wal` or `-shm` is absent, so it creates neither) and prints delivery ids,
|
|
1956
1963
|
peer names and counts per category, never task or message text. With `--yes`
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# 0.12.29 release verification
|
|
2
|
+
|
|
3
|
+
This record covers release preparation and source review. Publication and installation receipts are recorded separately after their gates succeed.
|
|
4
|
+
|
|
5
|
+
## Candidate
|
|
6
|
+
|
|
7
|
+
0.12.29 carries PR #409 (#408), the site correction in PR #406 and the #273 execution-plan corrections in PR #407. Control protocol remains 16.
|
|
8
|
+
|
|
9
|
+
- Explicit `--live-test` delegation permits operational console and peer lifecycle actions with a mandatory initiating-peer audit. Allowed agent commands retain their peer identity; person-attributed writes refuse under delegation.
|
|
10
|
+
- Dashboard delegation is bound to the issued session and checked server-side. Recovery refuses unsupported target packages before effects; supported headless restores use the ordinary target CLI with the recovery operation id.
|
|
11
|
+
- The #273 ledger includes the current native checks and makes machine-wide upgrade isolation explicit. No native leg is marked passed by these preparation changes.
|
|
12
|
+
|
|
13
|
+
## Document review
|
|
14
|
+
|
|
15
|
+
Security, Operations and the design specification describe the reviewed delegation boundary. README, Quickstart and the current Operations examples name 0.12.29. The stale area notes are checked against the CLI identity, console, recovery, dashboard and audit changes; their existing rules remain applicable. Document stamps name the release preparation commit, which must remain an ancestor of the merge commit.
|
|
16
|
+
|
|
17
|
+
## Recorded checks
|
|
18
|
+
|
|
19
|
+
- PR #409 head a8b9a56b: independent source review and board tasks #64/#65 approved. Exact-head Ubuntu and macOS full gates passed (`check: OK`); the headless restoration regression is fixture/CLI proof, not native Pi restoration.
|
|
20
|
+
- Original PR #407 head c637a088: Ubuntu, macOS and seeded guards passed in CI run 38791043404. The corrected execution plan was independently reviewed at 780853b6.
|
|
21
|
+
- The final release head requires its own Ubuntu/macOS `scripts/check.sh` and seeded guards before merging. Committed document freshness and stamp ancestry are checked again before tagging. The release workflow requires green CI for the identical tree before npm publication.
|
|
22
|
+
|
|
23
|
+
## Not verified by this record
|
|
24
|
+
|
|
25
|
+
Native, account, visual and research checks remain tracked in #273 and #1. Package publication, global CLI/plugin installation and running-hub upgrade are separate operational results; this preparation record does not claim them.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Issue #273 operator execution plan
|
|
2
|
+
|
|
3
|
+
## Scope and dated observations
|
|
4
|
+
|
|
5
|
+
- Date: 2026-10-16, Asia/Seoul.
|
|
6
|
+
- Initial observation source: main `426bab99`, release 0.12.28, protocol 16. The following CLI refusals describe that installed version, before #408.
|
|
7
|
+
- Issue #273 was read live, including its 2026-10-14 additions at initial preparation; the matrix below was refreshed for the five 2026-10-16 additions before release. Every checkbox remained unchecked.
|
|
8
|
+
- `ahub --version` returned `0.12.28`.
|
|
9
|
+
- `ahub status --json` returned daemon version `0.12.28`, protocol `16`, `stopping: false`, Claude idle, Codex busy, Kimi offline and Pi idle. These are status observations, not model-delivery or restoration receipts.
|
|
10
|
+
- `ahub ui` exited 1 with `ahub: codex cannot run ahub ui; the person runs it in ahub console or a terminal`.
|
|
11
|
+
- LCU app selection for `com.apple.Terminal` returned `Computer Use is not allowed to use the app 'com.apple.Terminal' for safety reasons.` No Terminal inspection or input occurred.
|
|
12
|
+
- The operator selected procedure and blocker preparation before live execution. No identity markers were removed, no console connection was impersonated, and no lifecycle, reset, roster, quota or provider experiment was run.
|
|
13
|
+
|
|
14
|
+
## Supported execution route and remaining boundaries
|
|
15
|
+
|
|
16
|
+
From 0.12.29, an explicitly authorized agent runs operational CLI steps with
|
|
17
|
+
`ahub --live-test <command> [args...]`, including `init`, `up`, `console`,
|
|
18
|
+
`settings`, `reset`, `upgrade`, `recovery` and roster operations. Follow
|
|
19
|
+
[Agent-operated live verification](../operations.md#agent-operated-live-verification).
|
|
20
|
+
Record the initiating peer and the mandatory delegation audit (`agent_cli`
|
|
21
|
+
event or retained CLI audit), alongside the command and operational receipts.
|
|
22
|
+
The original 0.12.28 refusal above is historical evidence, not a current CLI
|
|
23
|
+
blocker. Identity markers remain intact; the supported option supplies authority.
|
|
24
|
+
|
|
25
|
+
Allowed-class commands retain the agent's peer identity, including messaging
|
|
26
|
+
and review verdicts. The flag refuses person-attributed writes, including
|
|
27
|
+
`--as-user`, task labels, task assign/escalate and planning/memory writes.
|
|
28
|
+
Delegated dashboard sessions permit operational controls but refuse sending,
|
|
29
|
+
proposals and person-attributed planning/history actions; changing a request body
|
|
30
|
+
cannot remove that restriction. Exercises requiring those actions or a person's
|
|
31
|
+
sign-off need the ordinary authorized actor path and cannot be completed merely
|
|
32
|
+
by adding the flag.
|
|
33
|
+
|
|
34
|
+
LCU's Terminal denial is a separate external computer-use restriction and remains
|
|
35
|
+
in force. CLI delegation does not grant Terminal app access or prove native
|
|
36
|
+
visual, account or accessibility observations. Record any inaccessible native
|
|
37
|
+
surface as unverified while continuing reachable authorized CLI checks.
|
|
38
|
+
|
|
39
|
+
## Execution order
|
|
40
|
+
|
|
41
|
+
Use a disposable Git project under the workspace scratchpad, outside this
|
|
42
|
+
checkout, and initialize its own Git boundary before `ahub init`. An authorized
|
|
43
|
+
agent runs `ahub --live-test init`, reviews the resulting configuration and starts
|
|
44
|
+
it with `ahub --live-test up`. Record its absolute path, project id and delegation
|
|
45
|
+
audit. Keep destructive recovery/reset experiments separate from the project
|
|
46
|
+
used for browser and model checks. Recovery can hold a machine-wide lock, so
|
|
47
|
+
inspect `ahub --live-test recovery status` before starting an operation and wait
|
|
48
|
+
for unrelated operations to finish. Upgrade isolation has the additional
|
|
49
|
+
machine-wide and shared-configuration requirements below.
|
|
50
|
+
|
|
51
|
+
Use installed, authenticated native tools. Record each version before its first
|
|
52
|
+
turn. Do not change credentials, buy access or exhaust quota to produce a result.
|
|
53
|
+
Do not run another session's stop or reset. Keep identity fencing in effect and
|
|
54
|
+
use the documented delegation route rather than removing identity markers.
|
|
55
|
+
|
|
56
|
+
1. Run reachable operational checks through `ahub --live-test`; retain their delegation audits and readbacks. Inspect terminal/dashboard rendering only through an available permitted native surface.
|
|
57
|
+
2. Run native messaging and role/planner exercises on the disposable project using the ordinary peer or person path as required by attribution; the delegation flag does not authorize person-content writes.
|
|
58
|
+
3. Run roster and recovery experiments with disposable conversations only, using the delegated operational CLI route and recording native continuity separately.
|
|
59
|
+
4. Run the real repository SDLC cycle only with an explicitly selected issue and release window, retaining required person verdicts.
|
|
60
|
+
5. Record each result in `docs/smoke.md`, retaining any unverified remainder and external blocker.
|
|
61
|
+
|
|
62
|
+
## Required observations by issue entry
|
|
63
|
+
|
|
64
|
+
All rows below are pending. A procedure reference is not evidence of execution.
|
|
65
|
+
Section names refer to [the smoke checklist](../smoke.md).
|
|
66
|
+
|
|
67
|
+
| Entry | Procedure or setup | Evidence required before checking the issue box |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| #201 AC2/AC7, #213 AC7 | Console layout and readability | Light/dark terminal, stream and panels; terminal name, dimensions, readable text and observed clipping |
|
|
70
|
+
| #205 AC3 | Claude without channel evidence | Managed Claude push and settlement beside plain Claude tools-only; correlated message and settlement ids |
|
|
71
|
+
| #206 AC4 | Upgrade procedure below | Two distinct releases, unmanaged Claude marked reconnect-only, same native conversation after reconnect |
|
|
72
|
+
| #214 AC6 | ahub reset on a scratch project | Both reset scopes, queue/board checks, archive permissions and restored board, same project id |
|
|
73
|
+
| #215 AC6 | Expired-lease procedure below | Real expired lease and missing transcript, daemon/CLI/lock/project/archive outcomes, completion or audited disposition |
|
|
74
|
+
| #225 AC9 | Target-crash procedure below | Target killed after peers-restored, supported resume, original conversation and restart count readback |
|
|
75
|
+
| #269 AC10 | Settings from the dashboard and the terminal, parts 1-2 | Light/dark, phone width and keyboard path; actual Orca and terminal.open launches with native Pi attached |
|
|
76
|
+
| #285 phase 1 | Record table, #285 live item 1 | Real Kimi turn, status context source/value, usage_update timing relative to correlated prompt result |
|
|
77
|
+
| #284 phase 1 | One-shot command output | status/board/budget/doctor, light/dark, 80 and wider columns, console rendering and full suggested ids |
|
|
78
|
+
| #298 | Unified dashboard slow start | Real browser unconfirmed text, 30-second hold and keyboard focus after Start; a deterministic slow native start is a prerequisite |
|
|
79
|
+
| #296 | Ending Codex turn | Correlated native turn/steer response, elapsed time and exact refusal; a successful steer alone does not prove refusal matching |
|
|
80
|
+
| #315 Codex | Live roster return | Removal exit, printed resume command, restored same thread and peer reattachment |
|
|
81
|
+
| #315 Pi | Live roster return | Retained terminal/process, admission refused off roster, same TUI reattaches; prepare blocked until retained TUI ends |
|
|
82
|
+
| #316 trust and ingestion | Kimi TUI pull mode | Actual trust screen, project MCP tools and model-visible managed instructions/tool descriptions |
|
|
83
|
+
| #316 messaging/discovery | Kimi TUI pull mode | Correlated hub_inbox/hub_send exchange with another peer; trusted project and overlay discovery |
|
|
84
|
+
| #316 headless return | Kimi TUI pull mode | TUI to headless to TUI, session/config restoration readback; argv fixtures are insufficient |
|
|
85
|
+
| #256 identity/modes | Qwen Code 0.25.0 production peer | Real ACP supported identity and advertised modes; stop on CLI/ACP mismatch |
|
|
86
|
+
| #256 native provider | Qwen Code 0.25.0 production peer | Actual model identity, delivery, exact tool call/result and mode confirmation |
|
|
87
|
+
| #256 stop/recovery | Qwen Code 0.25.0 production peer | Owned stop and controlled/crash recovery retain the recorded conversation |
|
|
88
|
+
| #256 quota | Observe an organic refusal only | Version and exact sanitized native refusal; unknown remains failed delivery |
|
|
89
|
+
| #259 AC11 | Research-profile exercise | Two native agents, three sources, configured routing/classes, plans, role/tool ingestion, deterministic citations and person sign-off |
|
|
90
|
+
| #259 converters | Intended formats and configured converters | Actual conversion, review and undo previews; timing, caps and named-only fallback |
|
|
91
|
+
| #259 guidance | Same native profile sessions | Native profile/custom-stage guidance retention; preserve separate #295 and research measurements |
|
|
92
|
+
| #257 AC6 | SDLC gates | Real repository issue through merge/release under advise; issue/PR/head ids, gate blockers and release report |
|
|
93
|
+
| #295 AC16 | Native role/task cycle | Role text ingestion, retention after actual compaction, assignment/reassignment notice and review duty |
|
|
94
|
+
| #280 AC11 | Console attention, Global attention status, Approved character, Dashboard character | Per-phase light/dark, tmux, colour off, blocks/ascii and actual dashboard look |
|
|
95
|
+
| #263 AC11 | Planning and release controls | Native planner creates two releases; planned/actual dates read back |
|
|
96
|
+
| #263 board/intake | Project board | Desktop/phone browser against real hub, permission/state readback and actual GitHub issue intake |
|
|
97
|
+
| #306 | Observe an organic Kimi weekly/monthly refusal only | CLI version and exact sanitized prefix; do not deliberately consume quota |
|
|
98
|
+
| #403 | Offline Pi preserved-history recovery | Actual selected native session restored with identity/queue retained; a live owner refused |
|
|
99
|
+
| #398 | Pull-only important-message advisory | Real Kimi TUI and plain Claude see the tool-result advisory; hub_inbox drains it |
|
|
100
|
+
| #391 pet lifecycle | Standalone pet window | Actual stop changes it to Offline and host exits; native screenshots of states |
|
|
101
|
+
| #391 dashboard CSP | Split dashboard views | Browser-console capture of CSP behavior, separate from the earlier owner browser look |
|
|
102
|
+
| #394 VoiceOver | Office desk More control | Native VoiceOver traversal; DOM/click/Enter/Space regressions remain separate |
|
|
103
|
+
|
|
104
|
+
## Upgrade with unmanaged Claude (#206)
|
|
105
|
+
|
|
106
|
+
1. An authorized operator starts the disposable project on a known older release, records its version/project id and attaches an unmanaged Claude session using the documented plain-Claude tools-only setup. Record its native session id and a harmless conversation marker.
|
|
107
|
+
2. Use an isolated test environment for the hub home/registry and shared Claude plugin setup. Upgrade is machine-wide, even when invoked from the scratch cwd: it plans all registered running projects and changes the shared package/plugin; `--project` does not scope it. Run the target coordinator with `ahub --live-test upgrade --to 0.12.29 --dry-run` and retain the complete plan. Verify every planned root is a disposable fixture, record the shared CLI/plugin fate, and confirm the intended unmanaged Claude is reconnect-only. Stop without applying if any non-disposable project or shared setup is in scope. A separate AGENTHUB_HOME alone does not isolate the shared plugin; use a dedicated native configuration/test environment too.
|
|
108
|
+
3. The authorized agent applies that same target with `ahub --live-test upgrade --to 0.12.29 --yes` after reviewing the plan (a person may use the ordinary interactive path). Retain the delegation audit. Record operation id, source and target versions, terminal fate and plugin reconnect outcome. Do not substitute a restart at the same version.
|
|
109
|
+
4. Read back `ahub status`, the original native session id and marker, and one correlated hub message receipt. Record whether a second terminal appeared or a conversation was replaced.
|
|
110
|
+
5. Capture final operation and lock status. A refusal or failed reconnect is a failed leg, with its exact wording; it is not a pass based on the dry-run plan.
|
|
111
|
+
|
|
112
|
+
## Expired preparation lease with missing Codex transcript (#215)
|
|
113
|
+
|
|
114
|
+
This needs an isolated failure-injection setup before it is executable. There is
|
|
115
|
+
no public `ahub recovery prepare` CLI. Do not invent one or edit production
|
|
116
|
+
recovery JSON to claim a native reproduction. `test/upgrade.test.ts`,
|
|
117
|
+
`expiredLease`, is a scripted-driver fixture and is only a setup reference.
|
|
118
|
+
|
|
119
|
+
1. Give disposable Codex its own native session store and complete a harmless turn so its transcript exists. Record project id, daemon instance, native thread id and transcript path. Keep the original transcript outside the fixture's missing-path condition; never remove a production transcript.
|
|
120
|
+
2. In a controlled scratch-only harness, stop the coordinator after real source preparation and before commit, preserving its operation receipt. Record the daemon's preparation, terminal close receipts and runner identity. The harness and its pause point must be reviewed before use.
|
|
121
|
+
3. Make only the disposable transcript unavailable and allow the real preparation lease to expire. Confirm expiry from the daemon log/readback rather than changing a clock or receipt timestamp.
|
|
122
|
+
4. The authorized agent runs `ahub --live-test recovery status <operation-id>` and the supported next action it offers. Capture the exact missing-session refusal. Restore the disposable transcript and resume, or use the offered audited disposition with a reason. Do not force a fresh conversation and label it restoration.
|
|
123
|
+
5. Record daemon and CLI outcomes, operation/lock state, project id before/after, archive paths and modes, and the final continuity or lostContinuity disposition. Preserve failed intermediate receipts.
|
|
124
|
+
|
|
125
|
+
## Target crash after peers-restored (#225)
|
|
126
|
+
|
|
127
|
+
1. In a separate disposable project, establish native conversations with harmless markers and start a controlled recovery. Save its operation id and source conversation ids.
|
|
128
|
+
2. Observe the real `peers-restored` phase before release. A controlled pause at that boundary is required: if the operation already completed, this run cannot demonstrate this failure point. Do not fabricate the phase by editing a receipt.
|
|
129
|
+
3. Verify the target pid belongs to this disposable project's daemon and record its instance id. The authorized operator ends only that target daemon through a permitted process-control surface. Record native launcher/process state after the exit.
|
|
130
|
+
4. Run `ahub --live-test recovery status <operation-id>`, then its offered `ahub --live-test recovery resume <operation-id>`. Retain the delegation audits. If a surviving launcher blocks it, end the identified disposable launcher as instructed and retry the supported action.
|
|
131
|
+
5. Verify the new target instance, recorded restart count, same native conversations and markers, no duplicate terminal, final operation status and lock release. A missing snapshot or failed native resume remains a failure, with the precise refusal and the supported disposition recorded.
|
|
132
|
+
|
|
133
|
+
## Recording and closure
|
|
134
|
+
|
|
135
|
+
For each execution record: date/time, source commit, hub/native versions, project
|
|
136
|
+
id, exact command or interaction, expected observation, actual observation,
|
|
137
|
+
correlated ids, sanitized evidence path and pass/fail/unverified verdict. Record
|
|
138
|
+
partial success at the substep level. Keep raw tokens, cookies, private task text
|
|
139
|
+
and account details out of committed evidence.
|
|
140
|
+
|
|
141
|
+
On a confirmed defect, preserve its failure wording and open a linked defect
|
|
142
|
+
issue within the user's authorized scope. Check a #273 item only after its dated
|
|
143
|
+
record covers the entire item, or a failed run is recorded with a linked defect.
|
|
144
|
+
Environment/tool restrictions remain unverified prerequisites. They do not meet
|
|
145
|
+
the issue's failed-product-run alternative. #273 cannot close from this plan.
|
|
146
|
+
|
|
147
|
+
The initial preparation ran no full check or native model turn. Its original
|
|
148
|
+
commit c637a088 subsequently passed Ubuntu/macOS full gates and seeded guards
|
|
149
|
+
in CI run 38791043404. The release candidate must earn its own exact-head checks;
|
|
150
|
+
none of those automated results completes a live leg.
|
package/docs/verified.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"documents": {
|
|
4
4
|
"README.md": {
|
|
5
|
-
"verifiedAgainst": "
|
|
5
|
+
"verifiedAgainst": "0463ea1f251cea17ec7f32a2861de8c554fe4062",
|
|
6
6
|
"paths": [
|
|
7
7
|
"package.json",
|
|
8
8
|
"src/",
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
]
|
|
12
12
|
},
|
|
13
13
|
"docs/security.md": {
|
|
14
|
-
"verifiedAgainst": "
|
|
14
|
+
"verifiedAgainst": "0463ea1f251cea17ec7f32a2861de8c554fe4062",
|
|
15
15
|
"paths": [
|
|
16
16
|
"src/",
|
|
17
17
|
"templates/"
|
|
18
18
|
]
|
|
19
19
|
},
|
|
20
20
|
"docs/operations.md": {
|
|
21
|
-
"verifiedAgainst": "
|
|
21
|
+
"verifiedAgainst": "0463ea1f251cea17ec7f32a2861de8c554fe4062",
|
|
22
22
|
"paths": [
|
|
23
23
|
"package.json",
|
|
24
24
|
"src/",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
]
|
|
36
36
|
},
|
|
37
37
|
"docs/quickstart.md": {
|
|
38
|
-
"verifiedAgainst": "
|
|
38
|
+
"verifiedAgainst": "0463ea1f251cea17ec7f32a2861de8c554fe4062",
|
|
39
39
|
"paths": [
|
|
40
40
|
"package.json",
|
|
41
41
|
"src/",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
]
|
|
44
44
|
},
|
|
45
45
|
"docs/agent-notes/adapters.md": {
|
|
46
|
-
"verifiedAgainst": "
|
|
46
|
+
"verifiedAgainst": "0463ea1f251cea17ec7f32a2861de8c554fe4062",
|
|
47
47
|
"paths": [
|
|
48
48
|
"src/adapters/",
|
|
49
49
|
"src/pi/",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
]
|
|
75
75
|
},
|
|
76
76
|
"docs/agent-notes/budget.md": {
|
|
77
|
-
"verifiedAgainst": "
|
|
77
|
+
"verifiedAgainst": "0463ea1f251cea17ec7f32a2861de8c554fe4062",
|
|
78
78
|
"paths": [
|
|
79
79
|
"src/hub/budget.ts",
|
|
80
80
|
"src/cli/statusline-tee.ts",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
]
|
|
87
87
|
},
|
|
88
88
|
"docs/agent-notes/bus.md": {
|
|
89
|
-
"verifiedAgainst": "
|
|
89
|
+
"verifiedAgainst": "0463ea1f251cea17ec7f32a2861de8c554fe4062",
|
|
90
90
|
"paths": [
|
|
91
91
|
"src/hub/bus.ts",
|
|
92
92
|
"src/hub/envelope.ts",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
]
|
|
99
99
|
},
|
|
100
100
|
"docs/agent-notes/daemon.md": {
|
|
101
|
-
"verifiedAgainst": "
|
|
101
|
+
"verifiedAgainst": "0463ea1f251cea17ec7f32a2861de8c554fe4062",
|
|
102
102
|
"paths": [
|
|
103
103
|
"src/hub/daemon.ts",
|
|
104
104
|
"src/hub/control-client.ts",
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
]
|
|
156
156
|
},
|
|
157
157
|
"docs/agent-notes/tasks.md": {
|
|
158
|
-
"verifiedAgainst": "
|
|
158
|
+
"verifiedAgainst": "0463ea1f251cea17ec7f32a2861de8c554fe4062",
|
|
159
159
|
"paths": [
|
|
160
160
|
"src/hub/tasks.ts",
|
|
161
161
|
"src/hub/board.ts",
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
]
|
|
173
173
|
},
|
|
174
174
|
"docs/agent-notes/tests.md": {
|
|
175
|
-
"verifiedAgainst": "
|
|
175
|
+
"verifiedAgainst": "0463ea1f251cea17ec7f32a2861de8c554fe4062",
|
|
176
176
|
"paths": [
|
|
177
177
|
"test/",
|
|
178
178
|
"scripts/check.sh",
|
|
@@ -191,7 +191,7 @@
|
|
|
191
191
|
]
|
|
192
192
|
},
|
|
193
193
|
"docs/agent-notes/README.md": {
|
|
194
|
-
"verifiedAgainst": "
|
|
194
|
+
"verifiedAgainst": "0463ea1f251cea17ec7f32a2861de8c554fe4062",
|
|
195
195
|
"paths": [
|
|
196
196
|
"src/cli/console-state.ts",
|
|
197
197
|
"src/cli/attention.ts",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-hub",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.29",
|
|
4
4
|
"description": "Channel between Claude Code and the agent-hub daemon: peer messages from Codex, Kimi and the local worker arrive as channel events; hub_send replies.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Young Joon Lee",
|
|
@@ -16157,7 +16157,7 @@ import { join as join4 } from "path";
|
|
|
16157
16157
|
// package.json
|
|
16158
16158
|
var package_default = {
|
|
16159
16159
|
name: "@staix/agent-hub",
|
|
16160
|
-
version: "0.12.
|
|
16160
|
+
version: "0.12.29",
|
|
16161
16161
|
description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
|
|
16162
16162
|
license: "MIT",
|
|
16163
16163
|
type: "module",
|
package/src/cli/console.ts
CHANGED
|
@@ -28,6 +28,8 @@ export interface ConsoleOptions {
|
|
|
28
28
|
terminal?: ConsoleTerminal;
|
|
29
29
|
color?: boolean;
|
|
30
30
|
noPet?: boolean;
|
|
31
|
+
/** Explicit delegation inherited only by this console's ahub subprocesses. */
|
|
32
|
+
liveTest?: boolean;
|
|
31
33
|
/** Injected command runner must use argv, closed stdin, and return a cancellation handle. */
|
|
32
34
|
runCommand?: (args: string[], output: (text: string) => void, done: () => void, columns?: number) => (() => void);
|
|
33
35
|
pollMs?: number;
|
|
@@ -188,7 +190,7 @@ export async function runConsole(options: ConsoleOptions): Promise<void> {
|
|
|
188
190
|
finally { polling = false; }
|
|
189
191
|
};
|
|
190
192
|
const run = options.runCommand ?? ((args, output, finished, commandColumns) => {
|
|
191
|
-
const child = Bun.spawn([process.execPath, fileURLToPath(new URL("./main.ts", import.meta.url)), "--project", options.cwd, ...args], {
|
|
193
|
+
const child = Bun.spawn([process.execPath, fileURLToPath(new URL("./main.ts", import.meta.url)), "--project", options.cwd, ...(options.liveTest ? ["--live-test"] : []), ...args], {
|
|
192
194
|
cwd: options.cwd, stdin: "ignore", stdout: "pipe", stderr: "pipe",
|
|
193
195
|
env: { ...process.env, COLUMNS: String(commandColumns), NO_COLOR: "1" },
|
|
194
196
|
});
|
package/src/cli/help.ts
CHANGED
|
@@ -11,6 +11,7 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
11
11
|
["ahub projects remove <id>", "forget a stopped registration (keeps project files)"],
|
|
12
12
|
["ahub setup [--yes]", "install or update the Claude Code channel plugin from this package, then run doctor"],
|
|
13
13
|
["ahub init [--profile dev|research|writing|general] [--shadow] [--dry-run --json]", "preview or write .agenthub/config.json and the AGENTS.md marker block (drops a legacy CLAUDE.md block)"],
|
|
14
|
+
["ahub --live-test [--project <path|id>] <command> [args...]", "user-authorized operational console delegation; ordinary calls retain peer identity; person-authored content refuses"],
|
|
14
15
|
["ahub help [command]", "this help, or one command's entries; also --help and -h"],
|
|
15
16
|
["ahub version | --version", "print the installed version"],
|
|
16
17
|
]],
|
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { cliCommandLabel } from "./identity.ts";
|
|
4
4
|
|
|
5
5
|
export interface CliAudit {
|
|
6
|
-
v: 1; id: string; at: number; peer: string; command: string; outcome: "run" | "refused" | "invalid";
|
|
6
|
+
v: 1; id: string; at: number; peer: string; command: string; outcome: "run" | "refused" | "invalid" | "delegated";
|
|
7
7
|
}
|
|
8
8
|
const MAX_FILES = 256;
|
|
9
9
|
const MAX_BYTES = 64 * 1024;
|
|
@@ -14,7 +14,7 @@ const valid = (row: any): row is CliAudit => row?.v === 1 && typeof row.id === "
|
|
|
14
14
|
&& typeof row.at === "number" && Number.isFinite(row.at) && row.at > 0
|
|
15
15
|
&& typeof row.peer === "string" && /^[a-z][a-z0-9-]{0,31}$/.test(row.peer) && !/\s/.test(row.peer)
|
|
16
16
|
&& typeof row.command === "string" && row.command.length <= 64 && row.command === cliCommandLabel(...splitLabel(row.command))
|
|
17
|
-
&& ["run", "refused", "invalid"].includes(row.outcome);
|
|
17
|
+
&& ["run", "refused", "invalid", "delegated"].includes(row.outcome);
|
|
18
18
|
const splitLabel = (label: string): [string, string[]] => { const [command = "unknown", ...args] = label.split(" "); return [command, args]; };
|
|
19
19
|
|
|
20
20
|
/**
|
package/src/cli/identity.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { isSupportedPeer } from "../hub/peer-catalog.ts";
|
|
2
2
|
|
|
3
|
+
/** Recovery reads this declaration before delegating to another package's CLI. */
|
|
4
|
+
export const LIVE_TEST_SUPPORTED = true;
|
|
5
|
+
|
|
6
|
+
/** Person verdicts and user-authored workflow content are never delegated. */
|
|
7
|
+
export function liveTestRefusal(command: string, args: readonly string[]): boolean {
|
|
8
|
+
if (args.some(arg => arg === "--as-user" || arg.startsWith("--as-user="))) return true;
|
|
9
|
+
const sub = args.find(arg => arg !== "--json");
|
|
10
|
+
return ["task", "role", "goal"].includes(command)
|
|
11
|
+
|| (command === "backlog" && sub !== "list") || (command === "release" && !["list", "show"].includes(sub ?? ""))
|
|
12
|
+
|| (command === "memory" && sub !== "graph")
|
|
13
|
+
|| (command === "board" && args[0] === "import") || (command === "sdlc" && args[0] === "record")
|
|
14
|
+
|| (command === "bench" && args.find(arg => arg !== "--json") === "run") || command === "pet";
|
|
15
|
+
}
|
|
16
|
+
|
|
3
17
|
export type CliIdentity =
|
|
4
18
|
| { role: "console" }
|
|
5
19
|
| { role: "tools"; peer: string }
|
package/src/cli/main.ts
CHANGED
|
@@ -63,7 +63,7 @@ import { addonReport, appendRecords, formatAddons, formatResearch, LABELS, label
|
|
|
63
63
|
import { summarize, summarizeByTask } from "../hub/report.ts";
|
|
64
64
|
import { hasTree, planUndo, repoOf, restore, Turns } from "../hub/snapshots.ts";
|
|
65
65
|
import { pathWarnings } from "../hub/conflicts.ts";
|
|
66
|
-
import { classifyPeerCommand, cliCommandLabel, detectCliIdentity, peerCommandRefusal } from "./identity.ts";
|
|
66
|
+
import { classifyPeerCommand, cliCommandLabel, detectCliIdentity, liveTestRefusal, peerCommandRefusal } from "./identity.ts";
|
|
67
67
|
import { recordCliAudit } from "./identity-audit.ts";
|
|
68
68
|
import { runConsole } from "./console.ts";
|
|
69
69
|
import { paint, permissionBoundary, resolveColor, wrap, type Span } from "./console-state.ts";
|
|
@@ -83,10 +83,16 @@ const since = (): number => {
|
|
|
83
83
|
|
|
84
84
|
const argv = process.argv.slice(2);
|
|
85
85
|
let selector: string | undefined;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
let liveTest = false;
|
|
87
|
+
while (argv[0] === "--project" || argv[0] === "--live-test") {
|
|
88
|
+
if (argv.shift() === "--live-test") {
|
|
89
|
+
if (liveTest) fail("--live-test may appear only once");
|
|
90
|
+
liveTest = true;
|
|
91
|
+
} else {
|
|
92
|
+
if (selector !== undefined) fail("--project may appear only once");
|
|
93
|
+
selector = argv.shift();
|
|
94
|
+
if (!selector || selector.startsWith("--")) fail("--project needs a path or project ID");
|
|
95
|
+
}
|
|
90
96
|
}
|
|
91
97
|
const [given = "help", ...rest] = argv;
|
|
92
98
|
// `ahub <command> -h` alone asks for that command's help instead of running it. Elsewhere `-h` may be message text
|
|
@@ -114,17 +120,27 @@ const projectConfig = () => {
|
|
|
114
120
|
return config;
|
|
115
121
|
};
|
|
116
122
|
const stateDir = selected.stateDir;
|
|
117
|
-
const
|
|
123
|
+
const caller = detectCliIdentity(process.env);
|
|
118
124
|
const commandLabel = cliCommandLabel(cmd, args);
|
|
119
125
|
const commandAccess = classifyPeerCommand(cmd, args);
|
|
126
|
+
// Agent-authored content keeps its peer. Operational starts/holds may borrow console authority for live verification.
|
|
127
|
+
const delegationRefused = liveTest && caller.role === "tools" && commandAccess !== "allowed" && liveTestRefusal(cmd, args);
|
|
128
|
+
const delegatedPeer = liveTest && caller.role === "tools" && commandAccess !== "allowed" && !delegationRefused ? caller.peer : undefined;
|
|
129
|
+
const identity = delegatedPeer ? { role: "console" as const } : caller;
|
|
120
130
|
function audit(outcome: "run" | "refused" | "invalid"): void {
|
|
121
|
-
if (
|
|
122
|
-
try { recordCliAudit(stateDir,
|
|
123
|
-
catch {
|
|
131
|
+
if (caller.role === "console") return;
|
|
132
|
+
try { recordCliAudit(stateDir, caller.role === "tools" ? caller.peer : "unknown", commandLabel, delegatedPeer && outcome === "run" ? "delegated" : outcome); }
|
|
133
|
+
catch {
|
|
134
|
+
if (delegatedPeer) fail("live-test delegation audit unavailable; nothing was executed");
|
|
135
|
+
console.error("ahub: CLI audit could not be recorded (outbox unavailable or full)");
|
|
136
|
+
}
|
|
124
137
|
}
|
|
125
138
|
if (identity.role === "invalid") { audit("invalid"); fail(`${identity.reason}; use ahub console or a terminal with no agent markers`); }
|
|
139
|
+
if (delegationRefused) { audit("refused"); fail("live-test cannot write person-attributed content; use peer tools or a person's terminal"); }
|
|
126
140
|
if (identity.role === "tools" && commandAccess === "console") { audit("refused"); fail(peerCommandRefusal(identity.peer, commandLabel)); }
|
|
127
|
-
if (identity.role === "tools") audit("run");
|
|
141
|
+
if (identity.role === "tools" || delegatedPeer) audit("run");
|
|
142
|
+
// Forward only to ahub's own continuation commands, never to native agent argv or an environment flag.
|
|
143
|
+
const liveTestArgs = liveTest ? ["--live-test"] : [];
|
|
128
144
|
const invokedFrom = process.cwd(); // relative paths a person types (a bench suite) are theirs, not the project's
|
|
129
145
|
try { process.chdir(cwd); } catch { fail(`project directory is unavailable: ${cwd}`); }
|
|
130
146
|
/** One-shot output flags do not alter the fetched JSON document. */
|
|
@@ -381,7 +397,7 @@ async function hold(t: "pause" | "resume"): Promise<void> {
|
|
|
381
397
|
}
|
|
382
398
|
|
|
383
399
|
function spawnRecovery(operation: RecoveryOperation, quiet = false): void {
|
|
384
|
-
const child = spawn(process.execPath, [join(operation.sourceRoot, "src/cli/main.js"), "recovery-run", operation.id], {
|
|
400
|
+
const child = spawn(process.execPath, [join(operation.sourceRoot, "src/cli/main.js"), ...liveTestArgs, "recovery-run", operation.id], {
|
|
385
401
|
cwd, detached: true, stdio: "ignore", env: { ...process.env, AGENTHUB_RECOVERY_OPERATION: operation.id },
|
|
386
402
|
});
|
|
387
403
|
child.on("error", () => console.error(`runner launch failed; use ${recoveryCommand(operation, "resume")}`));
|
|
@@ -415,7 +431,7 @@ async function onTerminal(screens: (io: ScreenIO) => Promise<void>): Promise<voi
|
|
|
415
431
|
// The command owns the terminal while it runs; a reader whose input already ended has nothing to pause.
|
|
416
432
|
if (!ended) rl.pause();
|
|
417
433
|
const done = (code: number) => { if (!ended) rl.resume(); resolve(code); };
|
|
418
|
-
const child = spawn(process.execPath, argv, { cwd, stdio: "inherit" });
|
|
434
|
+
const child = spawn(process.execPath, [argv[0]!, ...liveTestArgs, ...argv.slice(1)], { cwd, stdio: "inherit" });
|
|
419
435
|
child.on("error", () => done(1));
|
|
420
436
|
child.on("close", (code) => done(code ?? 1));
|
|
421
437
|
}),
|
|
@@ -444,7 +460,7 @@ function upgradeHost(kind: "restart" | "upgrade", version: string): UpgradeHost
|
|
|
444
460
|
lock: recoveryLock,
|
|
445
461
|
read: (id) => readOperation<RecoveryOperation>(id),
|
|
446
462
|
runner: recoveryRunner,
|
|
447
|
-
live: (op) => liveProjects(op, makeRecoveryDriver().inspect),
|
|
463
|
+
live: (op) => liveProjects(op, makeRecoveryDriver(undefined, delegatedPeer).inspect),
|
|
448
464
|
endPeer: (planned, peer) => endPlannedPeer(planned, peer),
|
|
449
465
|
runnerStoppable: (id) => signedRunner(id) !== undefined,
|
|
450
466
|
stopRunner: (id) => stopSignedRunner(id),
|
|
@@ -470,7 +486,7 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
470
486
|
if (to !== undefined) { try { exactVersion(to); } catch (error) { fail((error as Error).message); } }
|
|
471
487
|
// An older CLI must not coordinate a newer target (docs/operations.md): the target's own coordinator takes over.
|
|
472
488
|
if (kind === "upgrade" && newerVersion(to!, VERSION) && process.env.AGENTHUB_UPGRADE_HANDOVER !== "1") {
|
|
473
|
-
const argv = ["x", "--package", `@staix/agent-hub@${to}`, "ahub", "upgrade", "--to", to!, ...rest];
|
|
489
|
+
const argv = ["x", "--package", `@staix/agent-hub@${to}`, "ahub", ...liveTestArgs, "upgrade", "--to", to!, ...rest];
|
|
474
490
|
console.error(`ahub: ${to} is newer than this CLI (${VERSION}), so its own coordinator runs the upgrade: bun ${argv.join(" ")}`);
|
|
475
491
|
if (interactive) {
|
|
476
492
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -532,31 +548,31 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
532
548
|
const runner = recoveryRunner(id);
|
|
533
549
|
if (action === "status") {
|
|
534
550
|
// #215: abort and resume are offered by what the open sources show, read as the runner's errors read them.
|
|
535
|
-
const live = runner ? {} : await liveProjects(operation, makeRecoveryDriver().inspect);
|
|
551
|
+
const live = runner ? {} : await liveProjects(operation, makeRecoveryDriver(undefined, delegatedPeer).inspect);
|
|
536
552
|
console.log(JSON.stringify(publicOperation(operation, runner, live), null, 2));
|
|
537
553
|
}
|
|
538
|
-
else if (action === "abort") { await abortRecovery(id, makeRecoveryDriver()); console.log("preflight cancelled; no committed transition was rolled back"); }
|
|
554
|
+
else if (action === "abort") { await abortRecovery(id, makeRecoveryDriver(undefined, delegatedPeer)); console.log("preflight cancelled; no committed transition was rolled back"); }
|
|
539
555
|
else if (action === "dispose") {
|
|
540
|
-
//
|
|
556
|
+
// Console authority (including explicit --live-test delegation); the reason is kept in the operation's audit.
|
|
541
557
|
const { one, rest: flags } = takeFlags(rest, ["--fresh-session", "--reason"], []);
|
|
542
558
|
const stop = flags.length === 1 && flags[0] === "--stop-and-archive";
|
|
543
559
|
const reason = one["--reason"]?.trim() ?? "";
|
|
544
560
|
if (!reason || reason.length > 500 || (flags.length && !stop) || stop === !!one["--fresh-session"]) fail(usage);
|
|
545
|
-
const result = await disposeRecovery(id, stop ? { stop: true } : { fresh: one["--fresh-session"]! }, reason, makeRecoveryDriver());
|
|
561
|
+
const result = await disposeRecovery(id, stop ? { stop: true } : { fresh: one["--fresh-session"]! }, reason, makeRecoveryDriver(undefined, delegatedPeer));
|
|
546
562
|
if (!stop) { console.log(`${one["--fresh-session"]}: a fresh session is accepted and the lost one is recorded; resuming`); return spawnRecovery(result); }
|
|
547
563
|
console.log(JSON.stringify(publicOperation(result), null, 2));
|
|
548
564
|
console.log(`${result.plan.kind} abandoned, not completed; the recovery lock is released. A project whose target ran starts again with that version's CLI.`);
|
|
549
565
|
}
|
|
550
566
|
else if (["completed", "cancelled"].includes(operation.phase)) console.log(`recovery is already ${operation.phase}`);
|
|
551
567
|
// #215: an operation being abandoned is never resumed, whichever coordinator started it (an older one would).
|
|
552
|
-
else if (operation.disposition) fail(`a stop-and-archive of this operation is partway; nothing was resumed; ${nextActionsText(operation, await liveProjects(operation, makeRecoveryDriver().inspect))}`);
|
|
568
|
+
else if (operation.disposition) fail(`a stop-and-archive of this operation is partway; nothing was resumed; ${nextActionsText(operation, await liveProjects(operation, makeRecoveryDriver(undefined, delegatedPeer).inspect))}`);
|
|
553
569
|
else if (runner === "unknown") console.log(`whether a runner holds this operation could not be read; nothing was started; ${recoveryCommand(operation, "status")}`);
|
|
554
570
|
else if (runner) console.log(`runner ${runner} is still working on this operation; ${recoveryCommand(operation, "status")}`);
|
|
555
571
|
else spawnRecovery(operation);
|
|
556
572
|
},
|
|
557
573
|
"recovery-run": async () => {
|
|
558
574
|
if (args.length !== 1 || process.env.AGENTHUB_RECOVERY_OPERATION !== args[0]) fail("recovery-run is an internal command");
|
|
559
|
-
const result = await runRecovery(args[0]!, makeRecoveryDriver());
|
|
575
|
+
const result = await runRecovery(args[0]!, makeRecoveryDriver(undefined, delegatedPeer));
|
|
560
576
|
if (result.phase !== "completed") process.exitCode = 1;
|
|
561
577
|
},
|
|
562
578
|
help: () => {
|
|
@@ -593,12 +609,13 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
593
609
|
return console.log("dashboard manager stopped; project hubs were kept running");
|
|
594
610
|
}
|
|
595
611
|
let url: string;
|
|
596
|
-
if (args.includes("--all")) url = await openManager();
|
|
612
|
+
if (args.includes("--all")) url = await openManager({ liveTest: !!delegatedPeer });
|
|
597
613
|
else {
|
|
598
614
|
const hub = await connect();
|
|
599
615
|
try {
|
|
600
|
-
const res = await hub.request({ t: "ui", ...(settings ? { settings: true } : {}) }, 10_000);
|
|
616
|
+
const res = await hub.request({ t: "ui", ...(settings ? { settings: true } : {}), ...(delegatedPeer ? { liveTest: true } : {}) }, 10_000);
|
|
601
617
|
if (!res.ok) fail(res.error);
|
|
618
|
+
if (delegatedPeer && res.liveTest !== true) fail("this hub cannot restrict delegated dashboard content; use a live-test-capable hub");
|
|
602
619
|
// A hub older than #269 ignores the field and would hand out an ordinary session without saying so.
|
|
603
620
|
if (settings && res.settings !== true) fail("this hub does not open settings sessions; upgrade the running hub to use ahub ui --settings");
|
|
604
621
|
url = res.url;
|
|
@@ -673,17 +690,17 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
673
690
|
if (args.some(arg => arg !== "--panels" && arg !== "--no-pet" && !arg.startsWith("--color=")) || args.filter(arg => arg.startsWith("--color=")).length > 1) fail("usage: ahub console [--panels] [--no-pet] [--color=auto|always|never]");
|
|
674
691
|
const color = resolveColor(args.find(arg => arg.startsWith("--color="))?.slice("--color=".length), { isTTY: !!process.stdin.isTTY && !!process.stdout.isTTY, TERM: process.env.TERM, NO_COLOR: process.env.NO_COLOR });
|
|
675
692
|
if (color instanceof Error) return fail("usage: ahub console [--panels] [--no-pet] [--color=auto|always|never]");
|
|
676
|
-
await runConsole({ client: await connect(), cwd, stateDir, panels: args.includes("--panels"), noPet: args.includes("--no-pet"), color });
|
|
693
|
+
await runConsole({ client: await connect(), cwd, stateDir, liveTest, panels: args.includes("--panels"), noPet: args.includes("--no-pet"), color });
|
|
677
694
|
},
|
|
678
695
|
|
|
679
696
|
up: async () => {
|
|
680
697
|
const unattended = unattendedEnv || args.includes("--unattended");
|
|
681
698
|
if (unattended) console.error(UNATTENDED_WARNING);
|
|
682
|
-
const status = await startProject(registeredProject(), { unattended });
|
|
699
|
+
const status = await startProject(registeredProject(), { unattended, liveTest });
|
|
683
700
|
console.log(`ahub up (ws://127.0.0.1:${status.controlPort}), state in ${stateDir}`);
|
|
684
701
|
try { if (loadPetConfig(cwd).window) console.log(await startPetWindow(stateDir, cwd)); }
|
|
685
702
|
catch { console.error("Pet window was not started; run ahub pet --window to inspect it."); }
|
|
686
|
-
if (process.stdin.isTTY && process.stdout.isTTY && !args.includes("--no-console")) await runConsole({ client: await connect(), cwd, stateDir });
|
|
703
|
+
if (process.stdin.isTTY && process.stdout.isTTY && !args.includes("--no-console")) await runConsole({ client: await connect(), cwd, stateDir, liveTest });
|
|
687
704
|
},
|
|
688
705
|
|
|
689
706
|
claude: async () => {
|
|
@@ -323,7 +323,7 @@ export function preserveSource(plan: UpgradePlan, home = hubHome()): string {
|
|
|
323
323
|
return dest;
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
-
export function makeRecoveryDriver(run: RunCommand = runCommand): RecoveryDriver {
|
|
326
|
+
export function makeRecoveryDriver(run: RunCommand = runCommand, liveTestPeer?: string): RecoveryDriver {
|
|
327
327
|
const now = () => Date.now();
|
|
328
328
|
const sleep = (ms: number) => Bun.sleep(ms);
|
|
329
329
|
const env = (op: RecoveryOperation, project?: Project): NodeJS.ProcessEnv => {
|
|
@@ -333,6 +333,7 @@ export function makeRecoveryDriver(run: RunCommand = runCommand): RecoveryDriver
|
|
|
333
333
|
(project && /(?:_API_KEY|_ACCESS_TOKEN|_SECRET|_TOKEN)$/.test(key))) delete value[key];
|
|
334
334
|
value.AGENTHUB_HOME = hubHome(); value.AGENTHUB_RECOVERY_OPERATION = op.id;
|
|
335
335
|
value.AGENTHUB_UNATTENDED = "0";
|
|
336
|
+
if (liveTestPeer) value.AGENTHUB_PEER_ID = liveTestPeer;
|
|
336
337
|
if (project) {
|
|
337
338
|
value.AGENTHUB_PROJECT_DIR = project.root; value.AGENTHUB_STATE_DIR = project.stateDir;
|
|
338
339
|
delete value.CODEX_HOME; delete value.CLAUDE_CONFIG_DIR;
|
|
@@ -345,7 +346,7 @@ export function makeRecoveryDriver(run: RunCommand = runCommand): RecoveryDriver
|
|
|
345
346
|
return value;
|
|
346
347
|
};
|
|
347
348
|
const command = async (op: RecoveryOperation, args: string[], project?: Project) => {
|
|
348
|
-
const result = await run([process.execPath, join(op.targetRoot!, "src/cli/main.js"), ...(project ? ["--project", project.root] : []), ...args],
|
|
349
|
+
const result = await run([process.execPath, join(op.targetRoot!, "src/cli/main.js"), ...(project ? ["--project", project.root] : []), ...(liveTestPeer ? ["--live-test"] : []), ...args],
|
|
349
350
|
{ env: env(op, project), cwd: project?.root ?? op.plan.projects[0]!.project.root, timeoutMs: 180_000 });
|
|
350
351
|
if (result.code !== 0) throw new Error(`target command ${args[0]} failed; inspect recovery state before retrying`);
|
|
351
352
|
return result;
|
|
@@ -448,6 +449,10 @@ export function makeRecoveryDriver(run: RunCommand = runCommand): RecoveryDriver
|
|
|
448
449
|
if (packageDigest(op.sourceRoot) !== op.plan.sourceDigest) throw new FinalRefusal("preserved source changed");
|
|
449
450
|
const target = op.plan.kind === "restart" ? { root: op.sourceRoot, digest: verifyPackage(op.sourceRoot, op.plan.version) }
|
|
450
451
|
: await stageRelease(op.plan.version, op.plan.integrity!, run);
|
|
452
|
+
const identityPath = join(target.root, "src/cli/identity.ts");
|
|
453
|
+
if (liveTestPeer && (!existsSync(identityPath) || !readFileSync(identityPath, "utf8").includes("export const LIVE_TEST_SUPPORTED = true;"))) {
|
|
454
|
+
throw new FinalRefusal("target does not support live-test delegation; choose a supporting target before retrying; runtimes unchanged");
|
|
455
|
+
}
|
|
451
456
|
// #215: the protocol and waiver checks are properties of these bytes. Once they passed (the runner recorded the
|
|
452
457
|
// digest), a resume does not check again, so a transient probe failure cannot refuse an operation for good;
|
|
453
458
|
// changed bytes are the runner's final "staged target changed".
|
package/src/hub/daemon.ts
CHANGED
|
@@ -1459,9 +1459,12 @@ export async function startDaemon(opts: DaemonOptions) {
|
|
|
1459
1459
|
supervision.checkRound();
|
|
1460
1460
|
try {
|
|
1461
1461
|
for (const audit of drainCliAudits(opts.stateDir)) {
|
|
1462
|
-
const
|
|
1463
|
-
|
|
1464
|
-
|
|
1462
|
+
const delegated = audit.outcome === "delegated";
|
|
1463
|
+
const refused = audit.outcome !== "run" && !delegated;
|
|
1464
|
+
event({ type: "agent_cli", peer: audit.peer, command: audit.command, refused, ...(delegated ? { delegated: true } : {}) });
|
|
1465
|
+
notify(refused ? `${audit.peer} refused ahub ${audit.command}` : delegated
|
|
1466
|
+
? `${audit.peer} ran ahub ${audit.command} with delegated live-test console authority`
|
|
1467
|
+
: `${audit.peer} ran ahub ${audit.command} as ${audit.peer}`);
|
|
1465
1468
|
}
|
|
1466
1469
|
} catch { log("CLI audit outbox unavailable; inspect state before retrying an agent command"); }
|
|
1467
1470
|
for (const peer of bus.knownPeers()) {
|
|
@@ -4204,7 +4207,7 @@ export async function startDaemon(opts: DaemonOptions) {
|
|
|
4204
4207
|
writeStatus();
|
|
4205
4208
|
// #269: `ahub ui --settings` is the same person's command; its ticket marks the session it opens.
|
|
4206
4209
|
if (msg.settings === true) notify("settings session opened from ahub ui --settings: for 15 minutes that dashboard session may change permission modes, routing and start settings");
|
|
4207
|
-
return void reply({ t: "ui", ok: true, url: dashboard.issue(msg.settings === true), ...(msg.settings === true ? { settings: true } : {}) });
|
|
4210
|
+
return void reply({ t: "ui", ok: true, url: dashboard.issue(msg.settings === true, msg.liveTest === true), ...(msg.settings === true ? { settings: true } : {}), ...(msg.liveTest === true ? { liveTest: true } : {}) });
|
|
4208
4211
|
} catch {
|
|
4209
4212
|
return void reply({ t: "ui", ok: false, error: "could not start the dashboard" });
|
|
4210
4213
|
}
|
package/src/hub/events.ts
CHANGED
|
@@ -43,7 +43,7 @@ export type HubEvent =
|
|
|
43
43
|
| ({ type: "turn_end"; peer: string; turn: string; ms: number; tokens?: number; files?: number; snapshotMs?: number; preparedRoles?: RoleIdentity[] | null } & Partial<TaskAttribution>)
|
|
44
44
|
| { type: "native_turn_end"; peer: string; id?: string }
|
|
45
45
|
| { type: "conduct"; peer: string; action: string; task?: number; target?: string }
|
|
46
|
-
| { type: "agent_cli"; peer: string; command: string; refused: boolean }
|
|
46
|
+
| { type: "agent_cli"; peer: string; command: string; refused: boolean; delegated?: true }
|
|
47
47
|
| { type: "permission"; id: string; peer: string; event: "requested" | "answered" | "expired" | "cancelled"; latencyMs?: number; surface?: "console" | "dashboard" | "terminal"; option?: "allow_once" | "allow_always" | "reject_once" | "reject_always" }
|
|
48
48
|
/** Completed native turns that received supervision, not feed enqueues or inferred costs. */
|
|
49
49
|
| { type: "supervision_turn"; peer: string; turn: string; tokens?: number; ms?: number }
|
package/src/hub/lifecycle.ts
CHANGED
|
@@ -138,7 +138,7 @@ export async function runProjectDaemon(project: Project, unattended = false): Pr
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
export async function startProject(project: Project, options: { unattended?: boolean; env?: NodeJS.ProcessEnv } = {}): Promise<any> {
|
|
141
|
+
export async function startProject(project: Project, options: { unattended?: boolean; env?: NodeJS.ProcessEnv; liveTest?: boolean } = {}): Promise<any> {
|
|
142
142
|
assertLifecycleAvailable();
|
|
143
143
|
const registry = new Registry();
|
|
144
144
|
try {
|
|
@@ -160,7 +160,7 @@ export async function startProject(project: Project, options: { unattended?: boo
|
|
|
160
160
|
// Native sessions can outlive the operation which launched them. An expired
|
|
161
161
|
// operation ID must not make a later ordinary startup load a retired snapshot.
|
|
162
162
|
if (launchEnv.AGENTHUB_RECOVERY_OPERATION !== recoveryLock()) delete launchEnv.AGENTHUB_RECOVERY_OPERATION;
|
|
163
|
-
const child = spawn(process.execPath, [join(import.meta.dir, "../cli/main.ts"), "--project", project.root, "daemon",
|
|
163
|
+
const child = spawn(process.execPath, [join(import.meta.dir, "../cli/main.ts"), "--project", project.root, ...(options.liveTest ? ["--live-test"] : []), "daemon",
|
|
164
164
|
...(options.unattended ? ["--unattended"] : [])], {
|
|
165
165
|
cwd: project.root, detached: true, stdio: ["ignore", log, log],
|
|
166
166
|
env: { ...launchEnv, AGENTHUB_STATE_DIR: project.stateDir, AGENTHUB_PROJECT_DIR: project.root,
|
package/src/hub/manager.ts
CHANGED
|
@@ -21,12 +21,12 @@ export type Lifecycle = {
|
|
|
21
21
|
startProject(project: Project, options?: { unattended?: boolean; env?: NodeJS.ProcessEnv }): Promise<any>;
|
|
22
22
|
stopProject(project: Project, expectedInstance?: string): Promise<void>;
|
|
23
23
|
};
|
|
24
|
-
export type ManagerOptions = { registry?: Registry; lifecycle?: Lifecycle; home?: string; cli?: string; orphanWatchMs?: number;
|
|
24
|
+
export type ManagerOptions = { registry?: Registry; lifecycle?: Lifecycle; home?: string; cli?: string; orphanWatchMs?: number; liveTest?: boolean;
|
|
25
25
|
/** Test clock seam: maps nominal forwarding deadlines, never comes from dashboard input. */
|
|
26
26
|
forwardTimeoutMs?: (action: string, nominalMs: number) => number };
|
|
27
27
|
type ManagerHandle = { stop(): Promise<void>; stopped: Promise<void> };
|
|
28
28
|
type Manifest = { port: number; protocol: number; instanceId: string; pid: number; pidSignature?: string };
|
|
29
|
-
const active = new Map<string, { handle: ManagerHandle; issue(): string }>();
|
|
29
|
+
const active = new Map<string, { handle: ManagerHandle; issue(liveTest?: boolean): string }>();
|
|
30
30
|
const CONTROL_TIMEOUT = 3000;
|
|
31
31
|
// A current CLI may authenticate the previous protocol-9 manager while
|
|
32
32
|
// refreshing it, then launch the current manager implementation.
|
|
@@ -178,7 +178,7 @@ export async function startManager(options: ManagerOptions = {}): Promise<Manage
|
|
|
178
178
|
};
|
|
179
179
|
dashboard = startDashboard({ projects: list, snapshot: (after, input = {}) => act({ ...input, action: "snapshot", after }), action: act });
|
|
180
180
|
server = Bun.serve({ hostname: "127.0.0.1", port: 0, maxRequestBodySize: 1024,
|
|
181
|
-
fetch(req) {
|
|
181
|
+
async fetch(req) {
|
|
182
182
|
if (req.headers.has("origin")) return new Response("forbidden", { status: 403 });
|
|
183
183
|
if (req.headers.get("authorization") !== `Bearer ${token}`) return new Response("unauthorized", { status: 401 });
|
|
184
184
|
if (req.method !== "POST") return new Response("POST required", { status: 405 });
|
|
@@ -186,7 +186,10 @@ export async function startManager(options: ManagerOptions = {}): Promise<Manage
|
|
|
186
186
|
if (expected && expected !== instanceId) return Response.json({ ok: false, error: "manager instance changed" }, { status: 409 });
|
|
187
187
|
if (stopping) return Response.json({ ok: false, error: "manager is stopping" }, { status: 409 });
|
|
188
188
|
const path = new URL(req.url).pathname;
|
|
189
|
-
if (path === "/open")
|
|
189
|
+
if (path === "/open") {
|
|
190
|
+
const input = await req.json().catch(() => ({})) as { liveTest?: boolean };
|
|
191
|
+
return Response.json({ ok: true, instanceId, protocol: PROTOCOL, url: dashboard!.issue(false, input.liveTest === true), ...(input.liveTest === true ? { liveTest: true } : {}) });
|
|
192
|
+
}
|
|
190
193
|
if (path === "/stop") { setTimeout(() => void stop(), 0); return Response.json({ ok: true, instanceId }); }
|
|
191
194
|
return new Response("not found", { status: 404 });
|
|
192
195
|
},
|
|
@@ -204,31 +207,32 @@ export async function startManager(options: ManagerOptions = {}): Promise<Manage
|
|
|
204
207
|
}, options.orphanWatchMs ?? 10_000);
|
|
205
208
|
orphanWatch.unref?.();
|
|
206
209
|
const handle = { stop, stopped };
|
|
207
|
-
active.set(home, { handle, issue: () => dashboard!.issue() });
|
|
210
|
+
active.set(home, { handle, issue: (liveTest) => dashboard!.issue(false, liveTest) });
|
|
208
211
|
return handle;
|
|
209
212
|
} catch (error) { cleanup(); throw error; }
|
|
210
213
|
}
|
|
211
214
|
|
|
212
|
-
async function managerRequest(home: string, path: "/open" | "/stop"): Promise<{ status: Manifest; body: any }> {
|
|
215
|
+
async function managerRequest(home: string, path: "/open" | "/stop", liveTest = false): Promise<{ status: Manifest; body: any }> {
|
|
213
216
|
const status = readManifest(home);
|
|
214
217
|
if (!status) throw new Error("manager manifest is unavailable");
|
|
215
218
|
if (!SUPPORTED_MANAGER_PROTOCOLS.has(status.protocol)) throw new Error("manager protocol is incompatible; use its matching CLI to stop it");
|
|
216
219
|
const token = readFileSync(files(home).token, "utf8").trim();
|
|
217
220
|
if (!token) throw new Error("manager token is unavailable");
|
|
218
221
|
const response = await fetch(`http://127.0.0.1:${status.port}${path}`, {
|
|
219
|
-
method: "POST", headers: { authorization: `Bearer ${token}`, "x-agenthub-instance": status.instanceId }, signal: AbortSignal.timeout(CONTROL_TIMEOUT),
|
|
222
|
+
method: "POST", headers: { authorization: `Bearer ${token}`, "x-agenthub-instance": status.instanceId }, ...(liveTest ? { body: JSON.stringify({ liveTest: true }) } : {}), signal: AbortSignal.timeout(CONTROL_TIMEOUT),
|
|
220
223
|
});
|
|
221
224
|
if (!response.ok) throw new Error(`manager refused request (${response.status}); ownership was not verified`);
|
|
222
225
|
const body = await response.json() as { ok?: boolean; instanceId?: string; url?: string };
|
|
223
226
|
if (!body.ok || body.instanceId !== status.instanceId) throw new Error("manager identity changed; refresh before retrying");
|
|
227
|
+
if (liveTest && (body as { liveTest?: boolean }).liveTest !== true) throw new Error("manager cannot restrict delegated dashboard content; use a live-test-capable manager");
|
|
224
228
|
return { status, body };
|
|
225
229
|
}
|
|
226
230
|
|
|
227
231
|
export async function openManager(options: ManagerOptions = {}): Promise<string> {
|
|
228
232
|
const home = options.home ?? hubHome();
|
|
229
233
|
const local = active.get(home);
|
|
230
|
-
if (local) return local.issue();
|
|
231
|
-
try { return (await managerRequest(home, "/open")).body.url; }
|
|
234
|
+
if (local) return local.issue(options.liveTest);
|
|
235
|
+
try { return (await managerRequest(home, "/open", options.liveTest)).body.url; }
|
|
232
236
|
catch (error) {
|
|
233
237
|
const old = readManifest(home);
|
|
234
238
|
if (old && processLiveness(old.pid, old.pidSignature) !== "gone") throw error;
|
|
@@ -246,7 +250,7 @@ export async function openManager(options: ManagerOptions = {}): Promise<string>
|
|
|
246
250
|
const deadline = Date.now() + 8000;
|
|
247
251
|
let error: unknown;
|
|
248
252
|
while (Date.now() < deadline) {
|
|
249
|
-
try { return (await managerRequest(home, "/open")).body.url; }
|
|
253
|
+
try { return (await managerRequest(home, "/open", options.liveTest)).body.url; }
|
|
250
254
|
catch (caught) { error = caught; await Bun.sleep(100); }
|
|
251
255
|
}
|
|
252
256
|
throw new Error(`manager readiness not confirmed: ${safeError(error)}; see ${join(f.dir, "manager.log")}`);
|
package/src/hub/ui.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
11
11
|
|
|
12
12
|
/** What a request's session may do (#269): `settings` while a session opened by `ahub ui --settings` is inside its window. */
|
|
13
|
-
export interface DashboardSession { settings: boolean; settingsUntil?: number }
|
|
13
|
+
export interface DashboardSession { settings: boolean; settingsUntil?: number; liveTest?: true }
|
|
14
14
|
export interface DashboardOptions {
|
|
15
15
|
snapshot: (after: number, input?: Record<string, unknown>, session?: DashboardSession) => unknown | Promise<unknown>;
|
|
16
16
|
action: (input: Record<string, unknown>, session?: DashboardSession) => unknown | Promise<unknown>;
|
|
@@ -30,8 +30,8 @@ const secret = () => randomBytes(32).toString("hex");
|
|
|
30
30
|
/** Separate browser surface. Only the authenticated control console may construct it and issue tickets. */
|
|
31
31
|
export function startDashboard(options: DashboardOptions) {
|
|
32
32
|
const now = options.now ?? Date.now;
|
|
33
|
-
const tickets = new Map<string, { expires: number; settings: boolean }>();
|
|
34
|
-
const sessions = new Map<string, { expires: number; settingsUntil?: number }>();
|
|
33
|
+
const tickets = new Map<string, { expires: number; settings: boolean; liveTest?: true }>();
|
|
34
|
+
const sessions = new Map<string, { expires: number; settingsUntil?: number; liveTest?: true }>();
|
|
35
35
|
// Inject the same pure public-board model used by the console before computing CSP hashes.
|
|
36
36
|
const html = readFileSync(new URL("../ui/index.html", import.meta.url), "utf8")
|
|
37
37
|
.replace("/* BOARD_MODEL */", () => `const boardColumns = ${boardColumns.toString()}; const peerPalette = ${JSON.stringify(PEER_PALETTE)};`)
|
|
@@ -57,7 +57,7 @@ export function startDashboard(options: DashboardOptions) {
|
|
|
57
57
|
}
|
|
58
58
|
const sessionOf = (cookie: string | undefined): DashboardSession => {
|
|
59
59
|
const until = cookie ? sessions.get(cookie)?.settingsUntil : undefined;
|
|
60
|
-
return until !== undefined && until > now() ? { settings: true, settingsUntil: until } : { settings: false };
|
|
60
|
+
return { ...(until !== undefined && until > now() ? { settings: true, settingsUntil: until } : { settings: false }), ...(cookie && sessions.get(cookie)?.liveTest ? { liveTest: true as const } : {}) };
|
|
61
61
|
};
|
|
62
62
|
const server = Bun.serve({
|
|
63
63
|
hostname: "127.0.0.1",
|
|
@@ -99,7 +99,7 @@ export function startDashboard(options: DashboardOptions) {
|
|
|
99
99
|
if (!issued || !tickets.delete(ticket)) return reject(401, "ticket expired or used; run ahub ui");
|
|
100
100
|
if (sessions.size >= LIMIT) return reject(429, "too many sessions; try later");
|
|
101
101
|
const session = secret();
|
|
102
|
-
sessions.set(session, { expires: now() + SESSION_MS, ...(issued.settings ? { settingsUntil: now() + SETTINGS_MS } : {}) });
|
|
102
|
+
sessions.set(session, { expires: now() + SESSION_MS, ...(issued.settings ? { settingsUntil: now() + SETTINGS_MS } : {}), ...(issued.liveTest ? { liveTest: true } : {}) });
|
|
103
103
|
return json({ ok: true }, 200, { "Set-Cookie": `${cookieName}=${session}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${SESSION_MS / 1000}` });
|
|
104
104
|
}
|
|
105
105
|
if (path === "/projects") {
|
|
@@ -131,6 +131,9 @@ export function startDashboard(options: DashboardOptions) {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
try {
|
|
134
|
+
if (sessionOf(cookie).liveTest && !["start", "stop", "pause", "resume", "permit", "start_peer", "setting", "setting_undo", "setting_preview", "sdlc_queue", "sdlc_release"].includes(String(input.action))) {
|
|
135
|
+
return reject(403, "live-test dashboard cannot write person-attributed content; use peer tools");
|
|
136
|
+
}
|
|
134
137
|
if (input.action === "sdlc_queue" || input.action === "sdlc_release") server.timeout(req, 35);
|
|
135
138
|
else if (input.action === "backlog_split") server.timeout(req, Math.ceil((BACKLOG_SPLIT_SCREEN_TIMEOUT_MS + HUB_DB_BUSY_TIMEOUT_MS + 5_000) / 1000) + 5);
|
|
136
139
|
return json(await options.action(input, sessionOf(cookie)));
|
|
@@ -149,11 +152,11 @@ export function startDashboard(options: DashboardOptions) {
|
|
|
149
152
|
return {
|
|
150
153
|
origin,
|
|
151
154
|
/** `settings`: the ticket of `ahub ui --settings`, whose session may raise authority for SETTINGS_MS (#269). */
|
|
152
|
-
issue(settings = false): string {
|
|
155
|
+
issue(settings = false, liveTest = false): string {
|
|
153
156
|
prune(tickets);
|
|
154
157
|
if (tickets.size >= LIMIT) tickets.delete(tickets.keys().next().value!);
|
|
155
158
|
const ticket = secret();
|
|
156
|
-
tickets.set(ticket, { expires: now() + TICKET_MS, settings });
|
|
159
|
+
tickets.set(ticket, { expires: now() + TICKET_MS, settings, ...(liveTest ? { liveTest: true } : {}) });
|
|
157
160
|
return `${origin}/#${ticket}`;
|
|
158
161
|
},
|
|
159
162
|
stop() {
|