Skip to content

v3: multi-agent collab bus + dashboard (session core, handshake, exec onboarding, kickoff/relay, security hardening)#1

Merged
Hsi431 merged 38 commits into
mainfrom
v3-session
Jul 6, 2026
Merged

v3: multi-agent collab bus + dashboard (session core, handshake, exec onboarding, kickoff/relay, security hardening)#1
Hsi431 merged 38 commits into
mainfrom
v3-session

Conversation

@Hsi431

@Hsi431 Hsi431 commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • Rebuilds agent-river from a one-way Telegram remote into a local multi-agent collaboration bus + observation dashboard: agents can request help from each other (including calling back into Claude) while the owner is away, and the owner watches a live feed with a handful of buttons instead of approving every step.
  • 13 work units (U1–U13) shipped on top of v2-phase1: session core + budgets, dashboard bot replacing the old Telegram bridge, agent handshake (join/approve, per-agent tokens), retirement of v1 chat/classifier surface (-9.3k lines), usability passes, exec-style one-line onboarding for any stdin/stdout agent, session kickoff + auto-relay, reliability/harvest (failure detail, /say, transcripts), session→edit-task bridge, packaging (init/quickstart/bin alias, v0.3.0), and a final pre-push security hardening pass (exchange-release token gate, runner cwd validation fail-closed, exec env allowlist, v2 outbox redaction) from a blind review.
  • 376 tests passing; live services (codex-agent-dashboard.service etc.) verified stable after cutover, old codex-agent-telegram-bridge.service retired.

Test plan

  • Full test suite green (376 tests) before each unit was committed
  • Live dashboard service restarted and confirmed stable (NRestarts=0) after final cutover
  • Blind security review (4 MAJOR findings) addressed in U13 and re-verified against actual code
  • otter/openclaw real integration (tracked as follow-up, not blocking this merge)

🤖 Generated with Claude Code

Hsi431 and others added 30 commits June 7, 2026 10:22
…axonomy

Implements §3 and §13.6 of the v2 Phase 1 spec: resolveRepo() returns the
git top-level as canonical repo identity, enforces workspace-root containment,
and maps each failure case to a specific reason code (missing_workspace_root,
invalid_input, repo_not_found, repo_access_denied, repo_outside_workspace).
Includes revalidateRepo() for TOCTOU best-effort pre-spawn check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements §8 and §13.7: parses @<agent> [repo=...] [mode=...] [--] <prompt>.
Only leading control tokens are parsed; later repo=/mode= in prose stay in the
prompt. Enforces: mode=write requires explicit repo=, forbidden modes
(danger-full-access/bypass/bypassPermissions) are always rejected, unknown or
duplicate control tokens are errors. @Opus → claude back-compat alias.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ters

Implements §4: makeClaudeAdapter() and makeCodexAdapter() both implement
run({ repoToplevel, mode, prompt, sessionId, model, signal, execFileImpl })
→ { ok, text, sessionId, tokens, outcome }. All spawns use execFile (no shell).
AbortController-based signal cancellation terminates the child process group.
Outcome taxonomy: ok | capability_blocked | provider_permission_denied |
repo_access_denied | timed_out | outcome_unknown | spawn_error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements §5 and §13.1-4: session key = (ownerUserId, chatId, agent,
repoToplevel, mode) stored as a SHA-256 hash with raw fields for audit.
runTurnWithSession() enforces: session_id updated only on success; stale
session cleared and retried once for read turns only; write turns with
outcome_unknown are never auto-rerun; schema_version=2 on all records.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ecords

Implements §7 and §13.9: in-process active-turn registry with AbortController
cancellation and process-group SIGTERM. stopTurn() and stopAllTurns() abort
the controller signal and kill -<pid> the child process group. v2-runs.jsonl
records start/end/cancelled status with schema_version=2. Tests verify signal
cancellation of mocked long-running adapters and cancelled run records.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… e2e tests

Implements §9, §10, §13.5, §13.7, §13.8: buildStartAck() shows agent/repo/
mode/session; buildStatusReport() reports without calling a model; failure
reasons surfaced for all outcome and resolver/router codes. Poller routing:
@agent messages → v2, everything else stays v1. Versioned callbacks with
v2:turn:<id>:<action> namespace (v1 callbacks preserved). Cross-repo read
review e2e test (§13.5) uses injectable adapter to verify target repo toplevel
without spawning real claude/codex.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SECURITY.md: adds v2 local-parity trust model section with explicit residual
risks (project CLAUDE.md/settings/hooks inheritance, no OS write sandbox,
acknowledged TOCTOU), capability ceiling (no danger-full-access/bypass from
Telegram), and deferred Phase 2 hardening items (OS sandbox, per-action
approval, project-settings isolation).

CHANGELOG.md: documents all v2 Phase 1 additions under Unreleased.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- detached:true in both adapters so kill switch/stop SIGTERM the process group
- handleTelegramUpdate routes @agent / /stop / /status to v2 (opt-in, owner-only)
- add v2_enabled + workspace_root policy fields and CLI flags
- tests: detached spawn options + v1/v2 routing gating (+6, 464 total)
Implements the round-2 Codex-review blockers against the amended spec
(§15.D Codex --json parser, §15.E resume-prompt-via-stdin, §15.A/B
background exec + real PID, §15.F Claude settings fail-closed, §15.G
router agent allowlist, §15.H poller lock) with test/v2-amendments.test.js.

Fixes a hang in spawnClaude/spawnCodex: when the exec callback settled
synchronously (injected execFileImpl in tests), the manual process-group
timeout was installed *after* settle() ran and never cleared, keeping the
event loop alive for timeoutMs and hanging `node --test`. Guard the timer
install with !settled.

Full suite: 483 pass / 0 fail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… round-2 fixes

Completes the last round-2 amendment: the v2:turn callback helpers were listed
under "Added" as if wired. Per spec §15.J they are scaffolding reserved for
Phase 3 (per-action approval), with no Telegram button bound in Phase 1 — now
stated explicitly. Also documents the round-2 §15.D/E/A/B/C/F/G/H/I fixes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ilbox

Adds the codex agent to the exchange auto-runner loop:
- src/agent/codex-exchange-runner.js: headless codex runner (mirrors
  exchange-runner.js) — acquires own lock, claims oldest eligible
  to=codex message, builds id-only prompt, invokes realCodexRunner
  (read-only sandbox), redacts secrets, writes reply, honors
  max_attempts/daily_max/timeout from shared policy, blocked-reply
  on terminal failure.
- src/agent/paths.js: adds codexExchangeRunnerDispatch,
  codexExchangeRunnerLock, codexExchangeRunnerLogsDir paths.
- src/agent/cli.js: exchange-runner --agent codex routes to new runner;
  --agent opus keeps existing behavior; codex-runner-service-print/
  write/status commands added; codex-runner-model accepted by
  telegram-codex-policy-set.
- src/agent/service.js: buildCodexRunnerService/writeCodexRunnerService/
  codexRunnerServiceStatus generate codex-agent-codex-runner.service +
  .timer unit files (never writes systemctl).
- scripts/no-memory-smoke.js: imports codex-exchange-runner.js.
- test/agent-codex-exchange-runner.test.js: 15 injectable tests (no real
  codex binary) covering off/disabled, message gating, dispatch lane,
  claim-before-run, success, secret redaction, retry, blocked-terminal,
  lock, daily cap, CLI routing, prompt contract, no-leak.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ng, per-key single-flight

Addresses the 3 blockers from the Opus + Codex re-review of §15.

B1 (§15.B/C) — process-group termination now confirms the whole group is gone.
Added a single authoritative `terminateGroup(pid)` in kill.js: SIGTERM → grace →
SIGKILL → poll until the group is ESRCH. spawnClaude/spawnCodex timeout and abort
paths and kill.js stopTurn/stopAllTurns all delegate to it (one mechanism, not
three half-implementations). stopTurn/stopAllTurns/handleV2Stop are now async and
resolve only after the group is confirmed gone; callers updated.

B2 (§15.H) — the real long-poll bridge now acquires the cross-process poller lock.
telegramCodexBridge acquires on start (refuses if another live poller holds it)
and releases in finally. Previously only tests called acquirePollerLock.

B3 (§15.A/§5) — one active turn per session key. The registry now stores keyDims;
handleV2Message refuses a second same-(owner,chat,agent,repo,mode) message with a
`busy` ack instead of racing the first turn's session id / outbox.

Tests: new test/v2-blockers-round3.test.js (real detached process tree for the
group kill, bridge second-process lock, same-key busy). Existing kill/ux tests
updated for the now-async stop API. Full suite 488 pass / 0 fail; test:no-memory
passes; git diff --check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	src/agent/cli.js
#	src/agent/paths.js
…llowlist + timeout

Addresses the 3 findings from Codex's re-review of round-3 + the new runner.

F1 (Blocking, §15.B/C) — adapter could report timed_out/cancelled before the
group was confirmed gone: the exec callback fires the moment the DIRECT child
dies from SIGTERM and won the settle race ahead of `await terminateGroup`. Added
a `terminating` flag in spawnClaude/spawnCodex: once a timeout/abort starts, the
exec callback no longer settles; the termination path owns the settle AFTER
terminateGroup confirms the whole group is ESRCH.

F2 (Medium) — codex exchange runner accepted any sender to=codex. pickEligible
CodexMessage now requires message.from ∈ dispatchTargetAllowlist (primary agent +
enabled exchange agents, e.g. opus), mirroring the opus runner's from-filter.

F3 (Medium) — exchange_runner_timeout_seconds was ignored: defaultCodexRunner
passed it nowhere and codex-runner.js hardcoded 120s. Threaded timeoutMs through
realCodexRunner → realCodexRunnerWithSandbox → runCodexExec (default unchanged
at CODEX_TIMEOUT_MS).

Tests: adapter abort-race (real process tree, exec callback fires mid-terminate),
untrusted-sender rejection, timeout threading. Existing codex-runner tests updated
to enable opus as a trusted exchange sender (reflects prod config). Full suite 506
pass / 0 fail; test:no-memory 506 pass; git diff --check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…codex runner)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
README led with the v1 approval-lane model only. Add a v2 section + lead the
'what it feels like' example with the real v2 flow (start ack + background
result), v2 command rows (/stop, /status, @agent repo= mode=), and refresh the
test count (360+ -> 500+). v1 lanes documented as still-available. Both EN + zh-Hant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…slim-down

Spec by Fable (lead), approved by fnata 2026-07-05. Acceptance contract
for units U1-U4; implementation dispatched to Codex per unit behind a
lead review gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ners

Spec: docs/AGENT_RIVER_V3_SESSION_DASHBOARD.md §1 (Fable). Implementation:
Codex (gpt-5.5 high), two rounds. Fable review: ran full suite (515 green),
verified spec invariants (agent-initiated clamp 6msg/20min + read-only,
participants vs allowlist, lazy expiry, budget on submit+reply), verified
no forbidden files touched, caught + had fixed unbounded session_skip
dispatch logging (dedup by message_id, regression-tested on both runners).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ge brain

Spec §2 (Fable). Implementation: Codex (gpt-5.5 high), two rounds.
Fable review: suite 522 green (ran locally), byte-offset ledger cursors
land on line boundaries, all outbound text redacted, curl transport keeps
token off argv, unit generator never touches systemctl; caught + had fixed
owner check wrongly accepting gateway_allowlist (now direct_send_user_allowlist
only, refusal regression-tested).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The test opened a session at a fixed 2026-07-05T00:00Z but read it back
with the real clock, so it turned red once real UTC passed 00:12Z (the
12-minute budget lazily expires the session). Product behavior correct;
test now pins the read-back clock too. Found by Fable during U3 review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gent auth

Spec §3 (Fable). Implementation: Codex (gpt-5.5 high), single round.
Fable review: suite 526 green (ran locally), timingSafeEqual with length
pre-check, token only ever lands in 0600 file under 0700 dir (asserted in
tests), approve returns path not token, dashboard pushes never carry the
token, CLI enforcement on submit/inbox/claim/reply + agent-initiated
session-open, owner and internal runner paths untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Fable)

Reordered execution: U4-pre (dashboard absorbs v2 routing + flushes,
additive) -> owner cutover -> U4-A deletions. Rationale: live systemd
units run from this worktree (deleting v1 first risks a restart crash
loop), and one bot token allows a single getUpdates poller.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per lead ruling in WORKORDER_U4_KILLLIST.md. Additive only: telegram.js
exports (bodies untouched, old bridge behavior identical via default
params); dashboard routes owner @-messages into maybeHandleV2 (its own
owner gate runs first) and its cycle now flushes v2 outbox + exchange +
dispatch notifications. Fable review: suite 528 green (ran locally),
verified default-param parity for the live bridge, flush adapter fails
loud on unsupported methods, real-ledger regression tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per lead per-file rulings in WORKORDER_U4_KILLLIST.md, executed after
owner cutover to the dashboard bridge. Deleted whole: direct-send,
telegram-codex(+bridge), chat, reply-context, reply-approval,
codex-reply, bridge, owner-mode (dangerous-action classifier rehomed to
hard-gate.js). Partial per ruling: telegram.js (v2 lane + transport
kept), gateway.js, cli.js, service.js, safety.js, paths.js. READMEs +
polling doc rewritten for v3 ops incl. cutover runbook.

Fable review: suite 340 green (ran locally), verified deletions match
the approved list exactly, v2 lane and dispatch callbacks intact,
owner allowlist retained; rejected + had reverted an unapproved
behavior change (memory_enabled removal — prod config has it true),
restoration verified against live config.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… /model command

Owner-reported UX failures: /session swallowed all errors into one usage
template, and runner models were invisible/unchangeable from the bot.
Errors now name the exact cause with numbers (topic length incl. 5-char
owner / 20-char agent tiers, unregistered participant with current list,
budget format, repo reason); /model shows and sets exchange/codex runner
models (owner-gated upstream). Spec §1 topic line amended by lead.

Fable review: suite 343 green (ran locally), commands scope verified,
live CLI smoke on isolated state (5-char owner topic opens; agent
initiator correctly hits U3 registration gate first).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s, /model claude|codex

Owner's real-world failure: phone keyboards autocorrect '--' to an em
dash, and the separator error had no code so it fell back to the bare
usage template. Separator now accepts ' -- ', ' — ', ' – '; the three
remaining uncoded parser errors got codes + specific zh messages; /model
targets renamed to claude/codex (opus kept as alias), display clarifies
the mailbox name.

Fable review: suite 343 green (ran locally); replayed the owner's exact
failing message against the new parser — em dash accepted, proceeds to
participant validation with a human-readable error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Written by Fable (lead). Codifies the memory-river dogfood lesson: a
capability that isn't in the agent's prompt is never used. Section A is
the behavior contract (must-ask-after-two-failures stuck protocol,
must-not rules incl. prompt-injection hygiene, how to ask under small
budgets); B/C are the poll-style CLI and exec-style stdin/stdout usage
variants.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hsi431 and others added 8 commits July 6, 2026 01:59
Owner's live test exposed the gap: /session opened an arena but never
threw the ball (topic never became a message) and runners only reply to
the sender (no inter-agent relay). Now: owner-initiated sessions
broadcast the topic as kickoff messages (CLI --no-kickoff to opt out;
agent-initiated sessions don't kickoff); after a runner writes a session
reply, Node relays it to the next participant round-robin. Relay is
runner-triggered only, budget is the sole terminator, failures downgrade
to relay_skipped reasons in the runner summary.

Fable review: suite 348 green (ran locally), verified relay guards
(active+budget+no-self), owner stays out of the relay ring, manual
replies don't relay.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… bus

agent-join --style exec --exec '<cmd>' stores the command (join push
shows it verbatim — owner approves exactly what will run, no token
needed); generic exec-runner picks eligible mail per exec agent, feeds
the message as a JSON envelope on stdin (never interpolated into the
command), caps+redacts stdout as the reply, relays session replies
(U8), kills timeouts via v2 terminateGroup with claim leases; service
generator ships a 90s timer without EnvironmentFile so third-party
commands never inherit the bot token. READMEs gain the three-step
onboarding section; scripts/poll-adapter-example.sh covers poll-style.

Fable review: suite 354 green (ran locally), verified injection line
(stdin-only), env isolation, attempts cap, skip-dedup lesson applied.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nscripts, dedup

Driven by the live gpt5.4 incident: runners now record sanitized error
text in dispatch ledgers and the dashboard pushes terminal failures with
the actual reason; /say lets the owner steer a live session (broadcast,
budget-prechecked); session close writes an idempotent full transcript
under session-transcripts/ and the close push carries the path plus the
final message in full (harvest v1, no LLM, no memory-river dependency);
dashboard-opened sessions no longer double-push session_opened.

Fable review: suite 358 green (ran locally), verified terminal-only
failure filter, transcript idempotency, /say budget guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sting gate

Session close push gains a '轉 edit task' button (repo-bound sessions
only) and /task <id> [repo=] covers the rest. Both build a pending edit
task via the existing submitAgentTask (topic + final message + transcript
path + [session:<id>] marker for duplicate warnings); the existing gate
feed then pushes 放行/拒絕. Two taps from discussion to approved edit,
zero new execution paths.

Fable review: suite 361 green (ran locally), orchestrator/tasks
untouched as constrained, owner gating + strict callback regex verified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lly binds

Owner's live-session feedback, spec §1 amended (lead): kickoff and /say
(from=owner) and session-relay forwards no longer consume budget — the
budget now measures real agent turns; the feed stops echoing owner
messages; and all three runners now honor message.repo as the spawn cwd
(codex exec -C, realpath-validated with repo_fallback logging via new
runner-repo.js) — previously sessions silently ran rooted in the
agent-river checkout regardless of repo=, and unbound sessions now get
an explicit '未綁定 repo' prompt line so agents stop assuming the host
codebase is the subject.

Fable review: suite 371 green (ran locally), claude headless settings
envelope untouched, repo field origin audited (session-open validated
only, no CLI bypass).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One command (init) now assembles the full stack: state dir, registry
seed, all four systemd unit files (never runs systemctl), telegram.env
template with comments, idempotent re-runs, printed next-steps. READMEs
gain a five-step quickstart and an explicit Linux+systemd requirement;
package.json ships the agent-river bin alias and v0.3.0 with a full
changelog (incl. the v1 retirement breaking-change note).

Fable review: suite 372 green (ran locally); codex self-caught an
init --help misfire and fixed it; verified its accidental test init
against the live system — unit files regenerated byte-identical by the
same deterministic generators, live services unaffected, config.json
untouched (idempotency held in production, involuntarily probed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Blind pre-push review (SECURITY_REVIEW_V3_PREPUSH.md) flagged 4 MAJOR
boundary crossings; all verified against real code, fixed by Fable
directly (codex was usage-limited). MINOR jsonl-memory deferred.

1. exchange-release now requires the poll-agent token like claim/reply.
2. runner cwd re-validates against workspace_root and fails CLOSED to
   home + unbound prompt (never the service checkout) — a polluted
   ledger or post-open path swap can no longer steer a runner outside
   the workspace; prefix check rejects /ws-evil vs /ws.
3. exec child processes get a minimal env allowlist (no bot token / API
   keys / agent-river token); claude/codex runners untouched.
4. v2 result text is redacted before hitting the outbox/transcript/TG.

Fable review: suite 376 green (ran locally), +4 real-behavior regression
tests, 3 U12 contract tests updated to the fail-closed behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Hsi431
Hsi431 merged commit a042d53 into main Jul 6, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant