Server mode can now run each account as its own Linux user (ah-<name>)
when `agenthub server install-system` has set S3 up. The dashboard
reaches a member only through sudo -u / write-as / pane-launch, reads a
member's files only through owner- and type-checked opens, and hooks
are authenticated by peer uid. Local mode and server mode without S3
are unchanged.
EXPERIMENTAL: tested with fake sudo and fake accounts only. It has not
yet been verified on a real install with real member accounts, sudo or
a real Codex login. Do not offer it to real users yet.
- S3 core: provisioning, per-member tmux servers, the proxy key
gateway, shared tools in /opt, dropped capabilities for children
(P0–P5)
- P4: plan and quota per member home
- P6: ~/.claude.json seed; opt-in plugin baseline installed as the
member; managed-settings spike script (not run)
- P7: Codex account pool (`agenthub accounts add --tool codex`)
- Roles merged with main's admin / analyst / developer: analysts write
only their own rows, under S3 as well
- Per-account prefs: one implementation (Chat UI + onboarding)
- Security reviews F1–F49 (five rounds), each fix with a regression
test: symlink/TOCTOU-safe member reads, no host writes in a member
home, credentials and the data dir never served, owner and folder
account must agree, pane tokens bound to the pane's uid, bounded
sudo/tmux/git calls and member reads (no hang or crash from a member),
async route rejections answer 500 instead of exiting
- Docs: docs/SERVER-MODE.md (S3 model, accepted ceilings, operator
runbook), docs/plans/server-mode/S3-PLAN.md, CLAUDE.md S3 rules
Tests: server 2838/2841 (3 need 127.0.0.1:3000, held on this box);
client 1162/1162; tsc clean; headers check passes.
Co-Authored-By: Claude <noreply@anthropic.com>
Analytics parity phases 2–7 (docs/plans/analytics-parity/), built on
138e706 and ported onto v2.3.1:
- Cost by AI tool (?tool= on cost/analytics/stats/workflows, by_tool,
Codex fast/priority and flex tiers, agenthub_cost_usd_total{tool,model})
- Date ranges (from/to) on cost/analytics/workflows; Analytics range select
with Today / This week (Monday) / This month (1st) / 7d / 30d / 90d / All /
Custom; workflow filters carried into workflow-stats.js (read worker)
- Budgets and cost alerts as alert rule types (alert_rules rebuilt without
its rule_type CHECK), GET /api/budgets/status, rule preview, spend pace,
status-bar budget chip; rules stay editable in place
- Query Explorer (/query, allowlisted sort, CSV/JSON export) and scheduled
reports (report_definitions/report_runs, Settings → Reports)
- Session Replay tab and immutable snapshots (session_snapshots, /snapshot/:token)
- Remote Codex sync, push ingest with a hashed token, privacy redaction and
ignored folders (Settings → Data), Cursor sessions (no cost)
- Read-only MCP server, statusline, Grafana/Prometheus files
Also: resizable sidebar, pet run/wave animations, the workspace home keeps a
half-typed prompt, and the pre-commit hook runs only the related tests for a
small commit.
Part B: Model Pricing, Hook Configuration and CLAUDE_HOME move from
Settings → Dashboard to the Claude Code tab's AgentHub sub-tab
(ClaudeAgentHubSettings.tsx); CODEX_HOME is editable in Codex → AgentHub.
A home that is or contains the OS home is refused (EBADHOME).
Settings persistence: dashboard-saved settings go to
~/.config/agenthub/settings.env (AGENTHUB_SETTINGS_FILE), outside the plugin
cache an update wipes. Load order: real env > the checkout's .env > that file.
The first CLAUDE_HOME change pins DASHBOARD_DATA_DIR so the DB stays put.
C1: codex-usage.js reads Codex rollout journals into sessions (new column
tool) and token_usage, tracked per file in codex_rollouts; forks deduped by
total vector, sub-agent threads folded into the root; pricing-sync also
prices OpenAI models; PricingSettings with a vendor filter.
C2: codex-hooks.js installs AgentHub's hooks into $CODEX_HOME/hooks.json on
a click, through a stable forwarder; Codex trust is never written. The hook
route creates tool='codex' sessions and folds sub-agent hooks into the root.
D: antigravity-usage.js reads agy's per-conversation SQLite files (tokens
from ModelUsageStats) into tool='antigravity' sessions; antigravity-hooks.js
installs the named hook `agenthub` (PreInvocation, PostToolUse, Stop — never
PreToolUse) into ~/.gemini/config/hooks.json; Settings → Antigravity tab;
Gemini pricing.
Whole-change review fixes: sameOriginGuard on both home PUTs, routers.missing
on a CLAUDE_HOME change, reset-pricing scope, GET /api/sessions?tool=,
clear-data covering the new tables, Codex child-thread events moving no
status, hooks owning status only while current, EFOREIGN for a forwarder
that is not ours, agy quiet window and turn rules, AGENTHUB_SETTINGS_FILE
honoured from .env.
Docs, OpenAPI and the CLAUDE.md marker updated. Server 2141, client 779.
- git (#50): stage-hunk/discard-hunk take an optional `block` — one run of
consecutive +/- lines inside an @@ hunk. `onlyBlock` (server/lib/git-diff.js)
cuts git's fresh hunk down to it, folding the other blocks into the side
being checked; LaneDiffView hangs one review bar per block (unified + split)
and its arrows walk blocks.
- git exclude: entries AgentHub writes for a lane go to the COMMON git dir with
the lane's path prefix (server/lib/git-exclude.js, used by router-apply and
lane-mcp) — a linked worktree's own info/exclude is never read by git, and a
subdir lane has no .git. MCP sync no longer fails with ENOTDIR on a worktree
lane or creates a stray .git in a non-repo lane.
- codex lanes carry no pipeline: codex-ingest mirrors only idle/running,
codex-nudge is removed, agents install writes nothing for codex, the
Workspace hides stage/pipeline/feature pickers/PipelineMap for them, and
every server start clears leftover codex stage data and the old AGENTS.md
stage block.
- Changes panel: a whole file opens for reading and enters the editor on a
click on its text (caret at the clicked line), not automatically.
- macOS: LaunchAgent start-at-login (scripts/launchd-service.js, picked by
scripts/service-manager.js).
- Adds the AgentHub introduction slides and their image-embedding script.
Clean break, no compatibility alias. Renames the product name, the CLI
binary (`ccam` -> `agenthub`), the env var prefix (`CCAM_` ->
`AGENTHUB_`), the Prometheus metric namespace (`ccam_*` ->
`agenthub_*`), the tmux session prefix (`ccam-lane-<id>` ->
`agenthub-lane-<id>`), the per-repo runtime profile directory
(`.ccam/profile` -> `.agenthub/profile`), the lanes state root
(`~/.claude/ccam-lanes` -> `~/.claude/agenthub-lanes`), the bundled
plugins (`plugins/ccam*` -> `plugins/agenthub*`) and their slash
commands (`/ccam-doctor` -> `/agenthub-doctor`, `/ccam:*` ->
`/agenthub:*`).
Because there is no alias, an existing install has to be updated by
hand: rename its `.ccam/profile` directory and any `CCAM_*` variables,
and reinstall the plugin under its new name.
Two other name families are deliberately left alone here, since they are
not just strings: the `Claude-Code-Monitor` repository/marketplace name,
and the `agent-dashboard` npm package plus its `~/.claude/agent-dashboard`
runtime directory, which holds the live database and needs a migration
rather than a rename.
Verified: server 1406/1406, client 459/459, `grep -i ccam` clean across
tracked files, `openapi.yaml` regenerated (95 paths, unchanged count).
A codex session emits no hooks and rarely calls `ccam stage` on its own, so
its card sat at `idle` forever while the amber "detected" badge was the only
thing that ever moved. `codex-ingest.js` now advances the declared `stage`
from its own written detection — the one place in CCAM where an inference
moves the blue `current` ring, and only for `codex` lanes.
It stays honest: gated on `recordDetection` having written (so forward-only,
declared-wins and the detection TTL are inherited unchanged), passes NO
evidence (the node renders `passed-no-evidence` amber, never `done`), and no
template's last node carries detect rules, so a lane can never be
auto-declared `done`. `setStage` grows a `keepDetection` option — its sole
caller — so the detection columns survive the advance and the card keeps
showing which command moved it instead of looking like an agent's own claim.
Two levers then get the evidence auto-advance cannot supply:
- `codex-nudge.js` now nudges on a stage held 2+ min with no evidence,
instead of on a detection sitting ahead of the declared stage — after
auto-advance the old condition can never fire, since the two now match.
- `ccam lanes agents install` on a codex lane writes the marker-delimited
`<!-- ccam:stage:start -->` block into the lane's own AGENTS.md — the one
instruction channel Codex reads — instead of copying agent templates into
`.claude/agents/`, which it never loads. Appended, user prose preserved,
reinstall replaces exactly that block.
Verified with a real `codex exec` run: stage moved to `watching-pr` with
`evidence: null` and the detection signal intact. 1364 server tests pass.
Three defects let a lane's pipeline map claim progress that was not this
pass's work, all of them on the "going back" path a new request takes:
- `setStage` cleared the detection columns on a real transition but never
pruned the `stages` records AHEAD of the new stage. A lane that ran through
`done` and was then declared back at `plan` rendered `plan` as `current`
while `tests`/`review`/`done` still read `done`, carrying the previous
pass's evidence, and contradicting `progress` (computed from the declared
index alone). A backwards declaration now drops every record it can prove
is ahead; a key matching no node is unplaceable and left alone.
- `activateFeature` rewrote the live lane row for the incoming feature but
carried the outgoing one's `detected_*` across. A never-seen slug starts at
`stage='idle'`/`stages={}`, exactly the shape `inferredDisplayStage` renders
from detection, so the new feature opened painted with the old one's
progress — and forward-only rejected every real detection for it until the
carried-over one aged past DETECTION_TTL_MS. Now cleared, like every other
real transition; deliberately not archived, since detection is live-only.
- `codex-ingest` classified a rollout journal by raw `now - mtime`, which goes
negative for a future mtime (clock stepped back, NTP correction, a restore
that preserved timestamps) and read as "written milliseconds ago". A
finished thread's journal could replay as live work; a file claiming to be
from the future is now idle.
Also fixes `check-headers.sh`, which audited via `find` and so reported local
gitignored runtime state (a lane's own `.ccam/profile/hooks/*.sh`) that is not
this repo's source and can never be made compliant here. It now lists files
through git (tracked + untracked-but-not-ignored), so a file an agent just
wrote is still caught before it is staged while ignored state is not.
Tests: 1359 pass (4 new). Header audit exits 0.
A lane on the 16-node `ship-feature` template showed an untouched pipeline
map and never declared a stage. Four independent causes, fixed at their
shared choke points:
1. Codex emits no hooks, so `touchLaneFromHook` — the only caller of
`stage-detect.detect()`, and correctly pinned to `claude` — could never
see a `codex` lane. New `server/lib/codex-ingest.js` tails Codex's own
rollout journals (`$CODEX_HOME/sessions/**/rollout-*.jsonl`), maps its
tool calls onto the hook-shaped `{tool_name, tool_input}` the matcher
already understands, and feeds the existing detect path plus an
idle/running status mirror under the same guard hooks use. Polled every
`DASHBOARD_CODEX_POLL_MS` ms (default 15000, `0` disables).
2. `resolveLaneArg` matched on cwd alone, so with a claude and a codex lane
at one directory (the composite `idx_lanes_cwd_tool`) a `ccam stage`
call could drive the sibling lane's card. It now resolves by
`--lane` → `CCAM_LANE_ID` → `--tool`/harness marker → owning lane, and
errors naming both candidates rather than picking one. Every pane the
dashboard starts carries `CCAM_LANE_ID` (`tmux new-session -e`, or an
assignment prefix when typing into a live shell pane).
3. `recordDetection`'s declared-wins veto had no exit: a lane left at the
pipeline's last node rejected every detection for the rest of its life.
A declaration on the LAST node no longer vetoes — the pipeline is over
there, so a later detection is new work. Nothing else is relaxed, and
detection still never writes `stage` nor renders `done`.
4. The run console pre-filled the pipeline's Claude slash command into
codex panes, which Codex reads as prose — so the skill that would have
called `ccam stage` never ran. Claude lanes only now.
A codex lane's *declared* stage still depends on the session calling
`ccam stage`; `AGENTS.md` is the only lever CCAM has, so it now says so.
Verified: server 1345/1345, client 441/441, and end-to-end against a real
codex lane row (detected_stage/detected_signal set, declared stage
untouched, no node rendered done).