v3: multi-agent collab bus + dashboard (session core, handshake, exec onboarding, kickoff/relay, security hardening)#1
Merged
Merged
Conversation
…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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
codex-agent-dashboard.serviceetc.) verified stable after cutover, oldcodex-agent-telegram-bridge.serviceretired.Test plan
🤖 Generated with Claude Code