docs: OpenAPI and API.md cover every route; drop removed endpoints; guard config writes
- openapi-extra/run.js describes tmux pane runs only: the removed
POST /api/run/{id}/message and stream-json run_stream/run_input_ack are gone.
- 108 operations added to OpenAPI (lanes, run, settings, reports, cc-config,
pets, ai-proxies, tool-profiles, ai-tools, snapshots, locks, ...);
openapi.yaml regenerated.
- docs/API.md: 74 routes added, 4 endpoints that no longer exist removed.
- Role is `developer`, not `member` (CLI.md, DATABASE.md, OVERVIEW.md);
`agenthub update` -> `update-check`; help lists `server install-system|tools`
and `users provision`; ARCHITECTURE.md's stale paths and names fixed.
- cc-config/codex-config writes carry sameOriginGuard (an MCP server or plugin
is code Claude runs later), with a test; `command -v` takes its name as $1.
This commit is contained in:
+235
-148
@@ -22,6 +22,10 @@ Complete REST API and WebSocket documentation for Agent Dashboard.
|
||||
- [Remote Data Sources](#remote-data-sources)
|
||||
- [Query Explorer](#query-explorer)
|
||||
- [Scheduled reports](#scheduled-reports)
|
||||
- [Webhooks](#webhooks)
|
||||
- [Web Push](#web-push)
|
||||
- [Updates](#updates)
|
||||
- [Import history](#import-history)
|
||||
- [WebSocket API](#websocket-api)
|
||||
- [Error Handling](#error-handling)
|
||||
- [Rate Limiting](#rate-limiting)
|
||||
@@ -327,6 +331,25 @@ by one, 0.5 s as one batch.
|
||||
GET /api/homes { homes: [{ id: "me" | <username>, label, readOnly, dir? }] }
|
||||
GET /api/homes/:who/fs/list|trash|zip|download as the workspace routes above
|
||||
POST /api/homes/:who/fs/mkdir|newfile|rename|move|copy|delete|upload|trash/restore|trash/purge
|
||||
GET /api/homes/trash { items, errors, keepDays } — every Trash the caller may open (below)
|
||||
```
|
||||
|
||||
One line per route, for searching (bodies and answers as the workspace routes above; `copy` takes `fromLane` instead of `fromHome`, `trash/restore` and `trash/purge` also take `store`):
|
||||
|
||||
```
|
||||
GET /api/homes/:who/fs/download?path=
|
||||
GET /api/homes/:who/fs/zip?p=<path>&p=…
|
||||
GET /api/homes/:who/fs/trash
|
||||
POST /api/homes/:who/fs/newfile
|
||||
POST /api/homes/:who/fs/rename
|
||||
POST /api/homes/:who/fs/move
|
||||
POST /api/homes/:who/fs/copy
|
||||
POST /api/homes/:who/fs/delete
|
||||
POST /api/homes/:who/fs/trash/restore
|
||||
POST /api/homes/:who/fs/trash/purge
|
||||
POST /api/homes/:who/fs/upload?path=&conflict=
|
||||
POST /api/homes/:who/fs/upload-check
|
||||
POST /api/homes/:who/fs/upload-batch?dir=&conflict=
|
||||
```
|
||||
|
||||
The same File Explorer on a user's HOME (`server/lib/homes.js`): under S3 their
|
||||
@@ -549,6 +572,14 @@ Error `409 ENOCWD` is returned when the workspace's folder no longer exists (or
|
||||
|
||||
Error `409 ERUNLIVE` is returned if the workspace already has a live run; stop it first with the `stop` action. The same code and status come back when the workspace's tmux pane is already running an agent while a `prompt` or `resumeSessionId` was sent — that argv cannot be typed over a live agent, so the request is refused instead of answered 200 with nothing done. A start that asks for neither still adopts the existing pane silently.
|
||||
|
||||
#### Other workspace actions
|
||||
|
||||
```http
|
||||
POST /api/lanes/:id/:action
|
||||
```
|
||||
|
||||
The one control route behind `start` (above), `stop`, `message`, `clear`, `reset`, `remove` and `purge` (same-origin). Any other `:action` is `400 EBADACTION`, an unknown workspace `404 ENOLANE`. `reset`, `remove` and `purge` are destructive and need `{ "confirm": true }` (`400 ECONFIRM`) — see below.
|
||||
|
||||
#### Preflight a destructive workspace action
|
||||
|
||||
```http
|
||||
@@ -1274,6 +1305,33 @@ anything is spawned, and `args` travels as an array of strings straight into
|
||||
argv — neither is ever joined into a command string. An unknown name returns
|
||||
`400 ENOHOOK` with the allowed list; a workspace with no slot returns `409 ENOSLOT`.
|
||||
|
||||
#### Scaffold a profile, sync MCP servers, read an integration
|
||||
|
||||
```http
|
||||
POST /api/lanes/:id/profile/init
|
||||
POST /api/lanes/:id/mcp/sync
|
||||
GET /api/lanes/:id/integrations/:name
|
||||
```
|
||||
|
||||
`profile/init` (same-origin) detects a Node.js project in the workspace's OWN folder and scaffolds `.agenthub/profile/` there — the HTTP twin of `agenthub lanes profile init`. `{ force?: true }` overwrites; answers `{ scaffolded: true, written, todos }`, or `{ scaffolded: false, reason }` (200) when no Node.js project is found; `400 EPROFILEEXISTS` without `force`. `mcp/sync` (same-origin) copies the source repo's `mcpServers` from `~/.claude.json` into `<workspace>/.mcp.json` (paths relocated) and seeds Playwright profiles → `{ servers, profilesSeeded }`; `400 ENOMCPCONFIG` when the source has none. Never automatic — a session calls it. `integrations/:name` → `{ enabled }`, read from `.agenthub/profile/integrations.env`. All three `404 ENOLANE` for an unknown workspace; under S3 the two writes refuse a member's folder the way the runtime routes do.
|
||||
|
||||
#### CI status and base sync
|
||||
|
||||
```http
|
||||
GET /api/lanes/:id/ci/status?ref=<branch|sha>
|
||||
POST /api/lanes/:id/sync-base
|
||||
```
|
||||
|
||||
`ci/status` is ONE snapshot of GitHub Actions for `ref` through `gh` in the workspace's folder — not a poll loop: `{ state: "pending"|"success"|"failure", ref, checks: [{ name, conclusion, url }] }`; `400 EBADREF` without `ref`. `sync-base` (same-origin, under the workspace lock, needs the workspace profile) merges the profile's base branch INTO the workspace's branch behind a migration-number collision preflight: body `{ mode?: "check"|"merge"|"continue" (default merge), branch? }`. Every documented outcome — clean, a collision, a conflict left in place — is a 200 with `code` 0/4/5; only a malformed request, a missing profile or an out-of-order `continue` answers an `error`. It never writes the stage.
|
||||
|
||||
#### Housekeeping
|
||||
|
||||
```http
|
||||
POST /api/lanes/gc
|
||||
```
|
||||
|
||||
Same-origin, admin only (`403 EADMIN` for a developer or analyst — it is machine-wide). Reaps orphaned Playwright MCP processes under `LANES_ROOT` and caps oversized logs under `LANES_ROOT/.state/`. `{ dryRun?: true }` reports without acting. Answers `{ reaped, capped }`.
|
||||
|
||||
#### Tail a workspace's log
|
||||
|
||||
```http
|
||||
@@ -1564,6 +1622,17 @@ Both `GET /api/workflows` and `/skill-usage` also take `from=YYYY-MM-DD&to=YYYY-
|
||||
query failure.
|
||||
|
||||
|
||||
#### Workflow-tool runs
|
||||
|
||||
```http
|
||||
GET /api/workflows/runs?status=&session_id=&limit=&offset=
|
||||
GET /api/workflows/runs/:runId
|
||||
GET /api/workflows/session/:id
|
||||
```
|
||||
|
||||
`runs` lists the fleets the Claude Code **Workflow** tool launched, ingested from their on-disk journals (`server/lib/workflow-ingest.js`): `{ runs, total, counts, limit, offset }`, newest first, `phases`/`progress` parsed into arrays, `counts` per status; `status` (omit or `all` for every one) or `session_id` filters, `limit` 1–1000 (default 50). A developer sees only runs of their own sessions. `runs/:runId` → `{ workflow, agents, events }` — the run, its inner agents and their events (oldest first, at most 5000); `404 WORKFLOW_NOT_FOUND`. `session/:id` is the session drill-in of the Workflow analytics page: `{ session, tree, toolTimeline, swimLanes, events }` (the agent tree, every tool call in order, one swim lane per agent, the first 500 events); `404` for an unknown session.
|
||||
|
||||
|
||||
### Locks
|
||||
|
||||
Cross-workspace named locks (`server/lib/named-lock.js`) — the OTHER axis from a
|
||||
@@ -1772,6 +1841,16 @@ classDiagram
|
||||
|
||||
---
|
||||
|
||||
#### Create Session
|
||||
|
||||
```http
|
||||
POST /api/sessions
|
||||
```
|
||||
|
||||
Body `{ id, name?, cwd?, model?, metadata? }` (`id` required, else `400 INVALID_INPUT`). An existing id answers `200 { session, created: false }` unchanged; a new one is inserted `active` and broadcast as `session_created` → `201 { session, created: true }`. Sessions normally come from hooks; in server mode a developer or analyst is refused (`403 EADMIN`).
|
||||
|
||||
---
|
||||
|
||||
#### Update Session
|
||||
|
||||
```http
|
||||
@@ -1961,53 +2040,24 @@ curl http://localhost:4820/api/sessions/sess_abc123/stats
|
||||
|
||||
---
|
||||
|
||||
#### Get Session Agents
|
||||
#### Session transcripts
|
||||
|
||||
```http
|
||||
GET /api/sessions/:id/agents
|
||||
GET /api/sessions/:id/transcripts
|
||||
GET /api/sessions/:id/transcript?agent_id=&run_id=&limit=&after=&before=&offset=
|
||||
```
|
||||
|
||||
Returns all agents for a session.
|
||||
`transcripts` lists the session's conversation files — the main one first, then each subagent's and compaction's — as `{ transcripts: [{ id, name, type: "main"|"subagent"|"compaction", subagent_type?, db_agent_id, … }] }`; `db_agent_id` links an entry to its `agents` row (`null` when none matches). A Codex, Antigravity, Cursor, omp or OpenCode session lists its own tool's journal instead.
|
||||
|
||||
**Path Parameters:**
|
||||
`transcript` reads one of them as structured messages: no `agent_id` = the main conversation, `agent_id` (+ `run_id` for a Workflow-tool agent) = a subagent's — each one plain name, never a path. `limit` defaults to 50 (max 200); `after=<line>` returns only messages past that JSONL line (the chat's tail), `before=<line>` the page before it (history), `offset` is the legacy pager. Answers `{ messages, total, has_more, last_line, first_line }` (empty when the file is gone). The live file under the Claude projects folder is preferred, else the snapshot taken at import. `404 NOT_FOUND` for an unknown session.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `id` | string | Session ID |
|
||||
---
|
||||
|
||||
**Example Request:**
|
||||
#### Get Session Agents
|
||||
|
||||
```bash
|
||||
curl http://localhost:4820/api/sessions/sess_abc123/agents
|
||||
```
|
||||
There is no `/api/sessions/:id/agents` route: a session's agents come with `GET /api/sessions/:id` (its `agents` array) or from `GET /api/agents?session_id=<id>` (see [Agents](#agents)).
|
||||
|
||||
**Example Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": [
|
||||
{
|
||||
"id": "sess_abc123-main",
|
||||
"session_id": "sess_abc123",
|
||||
"name": "Main Agent - my-project",
|
||||
"type": "main",
|
||||
"subagent_type": null,
|
||||
"status": "idle",
|
||||
"current_tool": null,
|
||||
"task": null,
|
||||
"started_at": "2024-03-18T12:00:00Z",
|
||||
"ended_at": null,
|
||||
"updated_at": "2024-03-18T12:05:00Z",
|
||||
"parent_agent_id": null,
|
||||
"awaiting_input_since": "2024-03-18T12:05:00Z",
|
||||
"awaiting_reason": "stop",
|
||||
"cost": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> **Note on `cost`** — `/api/agents` and `/api/sessions/:id/agents` attach a `cost` (USD) to each agent: the agent's **own** cost, computed server-side from the per-agent token buckets stored in `agents.metadata.tokens` and priced at the current pricing rules (at the agent's start date, so promo/standard cutovers apply — see [Pricing](#pricing)). It is `0` for main agents (whose cost is the session total, reported by `/api/pricing/cost/:sessionId`), for compaction pseudo-agents, and for any subagent whose transcript is unavailable. This lets a subagent card show only what that subagent spent instead of the whole session's total.
|
||||
> **Note on `cost`** — `/api/agents` and `GET /api/sessions/:id` (its `agents`) attach a `cost` (USD) to each agent: the agent's **own** cost, computed server-side from the per-agent token buckets stored in `agents.metadata.tokens` and priced at the current pricing rules (at the agent's start date, so promo/standard cutovers apply — see [Pricing](#pricing)). It is `0` for main agents (whose cost is the session total, reported by `/api/pricing/cost/:sessionId`), for compaction pseudo-agents, and for any subagent whose transcript is unavailable. This lets a subagent card show only what that subagent spent instead of the whole session's total.
|
||||
|
||||
> **Note on `status` vs Waiting** — agents are persisted with one of `idle | connected | working | completed | error`. The yellow **Waiting** badge surfaced in the dashboard is a UI overlay derived from `awaiting_input_since` being non-null on a non-terminal agent (typically `idle` after a `Stop`, or `connected` right after `SessionStart`). Filter `?status=idle` on `/api/agents` and inspect `awaiting_input_since` to enumerate currently-waiting main agents.
|
||||
|
||||
@@ -2071,54 +2121,18 @@ curl http://localhost:4820/api/agents/agent_xyz789
|
||||
|
||||
---
|
||||
|
||||
#### Get Agent Tools
|
||||
#### Create or update an agent
|
||||
|
||||
```http
|
||||
GET /api/agents/:id/tools
|
||||
POST /api/agents
|
||||
PATCH /api/agents/:id
|
||||
```
|
||||
|
||||
Returns tool executions for an agent.
|
||||
`POST` body `{ id, session_id, name, type?, subagent_type?, status?, task?, parent_agent_id?, metadata? }` (`id`, `session_id`, `name` required, else `400 INVALID_INPUT`): an existing id answers `200 { agent, created: false }` (`404 ENOTFOUND` when it is another user's in server mode); a new one is inserted (`type` default `main`, `status` default `waiting`), broadcast as `agent_created` → `201 { agent, created: true }`. `PATCH` takes any of `name`, `status`, `task`, `current_tool`, `ended_at`, `metadata` and answers `{ agent }`; `404 NOT_FOUND` for an unknown id. Agents normally come from hooks.
|
||||
|
||||
**Path Parameters:**
|
||||
#### Tool Execution Flow
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `id` | string | Agent ID |
|
||||
|
||||
**Example Request:**
|
||||
|
||||
```bash
|
||||
curl http://localhost:4820/api/agents/agent_xyz789/tools
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"tools": [
|
||||
{
|
||||
"id": 1,
|
||||
"agent_id": "agent_xyz789",
|
||||
"tool_name": "bash",
|
||||
"duration_ms": 1234,
|
||||
"success": 1,
|
||||
"error_message": null,
|
||||
"created_at": "2024-03-18T12:01:00Z"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"agent_id": "agent_xyz789",
|
||||
"tool_name": "view",
|
||||
"duration_ms": 45,
|
||||
"success": 1,
|
||||
"error_message": null,
|
||||
"created_at": "2024-03-18T12:02:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Tool Execution Flow:**
|
||||
Tool calls are recorded as `PreToolUse`/`PostToolUse` events, not as a separate table — read them with `GET /api/events?agent_id=<id>` or `?tool_name=<name>`.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
@@ -2129,13 +2143,13 @@ sequenceDiagram
|
||||
participant DB as Database
|
||||
|
||||
Agent->>PreHook: Tool about to execute
|
||||
PreHook->>DB: Set current_tool
|
||||
PreHook->>DB: Store PreToolUse event, set current_tool
|
||||
|
||||
Agent->>Tool: Execute (bash, view, etc.)
|
||||
Tool-->>Agent: Result
|
||||
|
||||
Agent->>PostHook: Tool completed
|
||||
PostHook->>DB: Create tool_execution record
|
||||
PostHook->>DB: Store PostToolUse event
|
||||
PostHook->>DB: Clear current_tool
|
||||
PostHook->>DB: Update token counts + cost
|
||||
```
|
||||
@@ -2154,6 +2168,16 @@ Returns `{ events, limit, offset, total }`, newest first, filtered by `event_typ
|
||||
|
||||
---
|
||||
|
||||
#### Event filter values
|
||||
|
||||
```http
|
||||
GET /api/events/facets
|
||||
```
|
||||
|
||||
`{ event_types, tool_names }` — the distinct values in the events table, sorted, for the Events page's filters (a developer's: only their own sessions' events).
|
||||
|
||||
---
|
||||
|
||||
### Data cleanup
|
||||
|
||||
#### Clean up old data
|
||||
@@ -2198,50 +2222,57 @@ POST /api/settings/uninstall
|
||||
|
||||
Settings → System's **Uninstall AgentHub** (same-origin only). Body `{ "confirm": true }` — anything else is `400 ECONFIRM`; in server mode `403 ESERVERMODE` (a shared dashboard is removed with `agenthub uninstall` on the host). Spawns `scripts/uninstall.js --yes` detached and answers `202 { ok: true, log }` at once; the worker then stops this server and removes everything `agenthub uninstall --yes` does (see [CLI.md](CLI.md)), the database included. `log` is `$TMPDIR/agenthub-uninstall-<uid>.log`, the only record left afterwards.
|
||||
|
||||
#### Clear, re-import, export and restore data
|
||||
|
||||
```http
|
||||
POST /api/settings/clear-data
|
||||
POST /api/settings/reimport
|
||||
GET /api/settings/export
|
||||
POST /api/settings/import
|
||||
POST /api/settings/reset-pricing
|
||||
```
|
||||
|
||||
`clear-data` deletes every session, agent, event, token row, workflow run, fired alert and webhook delivery in one transaction (alert rules, webhook targets, pricing, snapshots and reports survive) → `{ ok: true, cleared: <table counts before> }`. `reimport` re-reads the Claude projects folder (`importAllSessions`) → `{ ok: true, imported, skipped, errors, … }`; `500 IMPORT_FAILED`. `export` streams one JSON attachment (`agent-monitor-export-<date>.json`: `format`, `version`, `exported_at`, `sessions`, `agents`, `events`, `token_usage`, `workflows`, `dashboard_runs`, `alert_rules`, `model_pricing`), events written in pages so a large database never sits in memory. `import` (same-origin) restores such a file sent as multipart `file` (≤ 512 MB, else `413 FILE_TOO_LARGE`): non-destructive — a session already here is skipped whole — → `{ ok: true, source, format, …counts }`; `400 NO_FILE` / `BAD_UPLOAD` / `INVALID_FORMAT`. `reset-pricing` reseeds the default model prices (and intro rates), then waits up to 3 s for the automatic price sync → `{ ok: true, pricing }`; `{ scope: "claude" }` resets only the `claude-…` rows. All five are admin-only in server mode, and `export` is staff-only.
|
||||
|
||||
#### Hooks for Claude Code, Codex and Antigravity
|
||||
|
||||
```http
|
||||
POST /api/settings/reinstall-hooks
|
||||
GET /api/settings/codex-hooks
|
||||
POST /api/settings/codex-hooks
|
||||
DELETE /api/settings/codex-hooks
|
||||
GET /api/settings/antigravity-hooks
|
||||
POST /api/settings/antigravity-hooks
|
||||
DELETE /api/settings/antigravity-hooks
|
||||
```
|
||||
|
||||
`reinstall-hooks` rewrites AgentHub's hooks in Claude Code's `settings.json` → `{ ok, hooks }` (`500 HOOK_INSTALL_FAILED`). `codex-hooks` reads, installs (or reinstalls) and removes AgentHub's entries in `$CODEX_HOME/hooks.json` (`server/lib/codex-hooks.js`); every answer is the status `{ supported, path, script, readable, installed, trusted, events: { <Event>: { installed, trusted } }, error? }` — `trusted` is read from Codex's own `config.toml`, which AgentHub never writes. `antigravity-hooks` does the same for the named hook `agenthub` in `~/.gemini/config/hooks.json` (status `{ supported, path, script, readable, installed, enabled, events }`). Only on a click; the writes are same-origin; `400 EBADJSON` / `EUNSUPPORTED` / `EFOREIGN` (a forwarder script that is not AgentHub's), else `500 ESAVE`.
|
||||
|
||||
#### Privacy
|
||||
|
||||
```http
|
||||
GET /api/settings/privacy
|
||||
PUT /api/settings/privacy
|
||||
```
|
||||
|
||||
`{ redact_secrets, ignore_cwd, env_owned: { redact_secrets, ignore_cwd } }` — opt-in redaction of secrets in stored payloads, and the folders whose hooks are dropped (`server/lib/privacy.js`). `PUT` (same-origin) takes either field (`redact_secrets` boolean, `ignore_cwd` a list of absolute folders) and answers `{ ok: true, …state }`; `400 INVALID_INPUT` / `EBADPATH` (not absolute, a filesystem root, over 100 entries), `409 EENVOWNED` when the environment sets it, `500 ESAVE`.
|
||||
|
||||
#### Push ingest token
|
||||
|
||||
```http
|
||||
GET /api/settings/push-token
|
||||
POST /api/settings/push-token
|
||||
DELETE /api/settings/push-token
|
||||
```
|
||||
|
||||
The token for [`POST /api/hooks/ingest-batch`](#push-ingest-post-apihooksingest-batch). `GET` → `{ enabled, env_owned }` — never the token. `POST` (same-origin) makes a new one (the old one stops working) and returns it THIS ONCE: `{ ok: true, token, enabled, env_owned }`; `DELETE` turns push ingest off → `{ ok: true, enabled: false, env_owned }`. `409 EENVOWNED` when `AGENTHUB_PUSH_TOKEN_SHA256` comes from the environment.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Tools
|
||||
|
||||
#### List All Tools
|
||||
|
||||
```http
|
||||
GET /api/tools
|
||||
```
|
||||
|
||||
Returns all tool executions across all sessions.
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `limit` | integer | 100 | Max tools to return |
|
||||
| `tool_name` | string | - | Filter by tool name |
|
||||
| `success` | boolean | - | Filter by success status |
|
||||
|
||||
**Example Request:**
|
||||
|
||||
```bash
|
||||
curl http://localhost:4820/api/tools?limit=50&tool_name=bash
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"tools": [
|
||||
{
|
||||
"id": 1,
|
||||
"agent_id": "agent_xyz789",
|
||||
"tool_name": "bash",
|
||||
"duration_ms": 1234,
|
||||
"success": 1,
|
||||
"error_message": null,
|
||||
"created_at": "2024-03-18T12:01:00Z"
|
||||
}
|
||||
],
|
||||
"total": 156
|
||||
}
|
||||
```
|
||||
There is no `/api/tools` route. Per-tool counts come from `GET /api/sessions/:id/stats` (`tools_used`, `tool_call_count`) and `GET /api/events/facets`; individual calls from `GET /api/events?tool_name=<name>`.
|
||||
|
||||
---
|
||||
|
||||
@@ -2290,6 +2321,27 @@ A ready-made Prometheus config, recording rules and three Grafana boards built o
|
||||
|
||||
---
|
||||
|
||||
#### Overview counts
|
||||
|
||||
```http
|
||||
GET /api/stats?tz_offset=&sources=&tool=
|
||||
```
|
||||
|
||||
The Dashboard's header numbers: the overview counts (sessions, agents, events…), `events_today` (in the local day of `tz_offset`, the browser's `getTimezoneOffset()`), `ws_connections`, `agents_by_status` and `sessions_by_status` (`{ <status>: count }`). `sources`/`tool` narrow it like `/api/sessions`; a malformed `tool` is `400 INVALID_INPUT`.
|
||||
|
||||
#### Health and API docs
|
||||
|
||||
```http
|
||||
GET /api/health
|
||||
GET /api/openapi.json
|
||||
GET /api/redoc
|
||||
GET /api/redoc/redoc.standalone.js
|
||||
```
|
||||
|
||||
`health` → `{ status: "ok", version, timestamp }`, exempt from every login. `openapi.json` is this API's OpenAPI 3.0 document (also browsable at `/api/docs`, Swagger UI); `redoc` renders it with ReDoc, whose script `redoc.standalone.js` is served locally (no CDN).
|
||||
|
||||
---
|
||||
|
||||
### Pricing
|
||||
|
||||
#### List Pricing Rules
|
||||
@@ -2535,41 +2587,28 @@ POST /api/alerts/rules/preview
|
||||
|
||||
### Notifications
|
||||
|
||||
#### Get Session Notifications
|
||||
Notifications are not stored per session and have no REST read. Hook `Notification` events land in the events table (`GET /api/events?session_id=<id>&event_type=Notification`), and the dashboard's alerts feed is [`GET /api/alerts`](#alerts-feed).
|
||||
#### Alert rules
|
||||
|
||||
```http
|
||||
GET /api/sessions/:id/notifications
|
||||
GET /api/alerts/rules
|
||||
POST /api/alerts/rules
|
||||
PATCH /api/alerts/rules/:id
|
||||
DELETE /api/alerts/rules/:id
|
||||
```
|
||||
|
||||
Returns notifications for a session.
|
||||
`GET` → `{ rules: [{ id, name, rule_type, config, enabled, cooldown_seconds, … }] }` (`config` parsed). `POST` body `{ name, rule_type, config, enabled?, cooldown_seconds? }`: `rule_type` is one of `event_pattern`, `inactivity`, `status_duration`, `token_threshold`, `cost_threshold`, `budget`, and `config` is validated for it (`validateRuleConfig`); `cooldown_seconds` defaults to 300 → `201 { rule }`. `PATCH` takes `name`, `config` (checked against the stored type — `rule_type` is immutable), `enabled`, `cooldown_seconds` → `{ rule }`. `DELETE` → `{ ok: true }`, its fired alerts cascading away. `400 INVALID_INPUT`, `404 NOT_FOUND`. In server mode rules are staff-only and their writes admin-only. See [Spend Budgets](#spend-budgets) for `budget` and the read-only preview.
|
||||
|
||||
**Path Parameters:**
|
||||
#### Alerts feed
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `id` | string | Session ID |
|
||||
|
||||
**Example Request:**
|
||||
|
||||
```bash
|
||||
curl http://localhost:4820/api/sessions/sess_abc123/notifications
|
||||
```http
|
||||
GET /api/alerts?unacked=true&limit=&offset=
|
||||
POST /api/alerts/:id/ack
|
||||
POST /api/alerts/ack-all
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
`GET` → `{ alerts, total, unacked, limit, offset }`, newest first (`limit` 1–200, default 50; `unacked=true` only the unacknowledged). A developer's feed is their own sessions' alerts (one with no session — a budget — is staff-only); an analyst's rows carry `can_ack`. `:id/ack` acknowledges one (`:id` numeric) → `{ alert }`, broadcast as `alert_updated`; `404 NOT_FOUND`. `ack-all` → `{ ok: true, acknowledged }` (a developer's or analyst's: their own sessions' only).
|
||||
|
||||
```json
|
||||
{
|
||||
"notifications": [
|
||||
{
|
||||
"id": 1,
|
||||
"session_id": "sess_abc123",
|
||||
"notification_type": "backgroundTaskComplete",
|
||||
"message": "Explore agent completed",
|
||||
"created_at": "2024-03-18T12:05:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Remote Data Sources
|
||||
|
||||
@@ -2895,6 +2934,7 @@ POST /api/run/:id/permission-mode {mode} → Shift+Tab steps, each read back
|
||||
POST /api/run Start/attach run — Body: { laneId, cwd?, model?, permissionMode?, effort?, resumeSessionId?, initialPrompt?, sandbox?, approval?, resumeLast?, bypass? }
|
||||
GET /api/run/:id Run handle (returns live run state)
|
||||
DELETE /api/run/:id[?archive=1] Kill the pane → {ok}. `archive=1` (the UI's Stop) also archives the session it held → {ok, archived: <session id> | null}; without it (Sleep, the CLI) the session stays listed to resume
|
||||
DELETE /api/run/history/:id Forget one CLOSED Terminal tab (`tool = 'shell'`) from the history now → {removed}; 409 ERUNLIVE while open, 404 ENOTFOUND for any other row
|
||||
```
|
||||
|
||||
`:id` must be a workspace pane name, `agenthub-lane-<n>`; anything else answers 404 without reaching tmux, so these two routes can never touch a tmux session the dashboard did not start.
|
||||
@@ -3145,6 +3185,53 @@ POST /api/settings/compact { confirm: true } → 202 — started (or alr
|
||||
Settings → Data → **Session cleanup** (`server/lib/db-retention.js`). Automatic retention is OFF unless `days` (1–3650, `AGENTHUB_RETENTION_DAYS` in the settings file) is set; nothing is deleted by default. When set, the hourly maintenance (`DASHBOARD_DB_MAINTAIN_MS`) purges finished (completed/error/abandoned) sessions that started more than `days` ago — the same rule and the same deletion (`purgeSessionIds`: events, agents, token usage, the session) as `POST /api/settings/cleanup` with `purge_days` — 25 sessions per transaction with a yield between, at most 5000 per run. A new database is created with `auto_vacuum = INCREMENTAL`, an existing one up to 32 MB is converted by a VACUUM at startup; the same hourly run then returns free pages to the OS (`incremental_vacuum`, in 1024-page slices, at most 65536 pages per run) once more than 1024 are free. A larger, older database is converted only by **Compact**: a full VACUUM in a worker thread followed by a TRUNCATE checkpoint — it holds the write lock while it runs (tens of seconds on a GB-size file), so hooks wait meanwhile; it runs only on a click. In server mode both writes are an admin's (`/settings` is in `ADMIN_WRITE_PREFIXES`).
|
||||
---
|
||||
|
||||
### Webhooks
|
||||
|
||||
```http
|
||||
GET /api/webhooks/providers
|
||||
GET /api/webhooks
|
||||
POST /api/webhooks
|
||||
PATCH /api/webhooks/:id
|
||||
DELETE /api/webhooks/:id
|
||||
POST /api/webhooks/:id/test
|
||||
GET /api/webhooks/:id/deliveries?limit=&offset=
|
||||
```
|
||||
|
||||
Where fired alerts are delivered (`server/lib/webhook-providers.js`). `providers` → `{ providers }`, the catalog with each provider's fields (secrets flagged). A target is answered REDACTED: `{ id, name, type, enabled, url_preview (masked), has_secret, headers (values redacted, generic family only), config (secret fields as ••••), rule_ids, created_at, updated_at, last_delivery }`. `POST` body `{ name, type, url?, enabled?, secret?, headers?, rule_ids?, config? }` — `type` one of the catalog ids (`slack`, `discord`, `teams`, `google_chat`, `mattermost`, `rocketchat`, `telegram`, `pagerduty`, `opsgenie`, `splunk_oncall`, `zapier`, `make`, `pipedream`, `generic`); `url` is required for providers that need one; `secret`/`headers` apply to the generic family only; `rule_ids` limits it to those alert rules (omit = all) → `201 { target }`. `PATCH` changes only the keys present (`config` is merged over the stored one, so a secret need not be re-sent) → `{ target }`. `DELETE` → `{ ok: true }` (its delivery log cascades). `test` sends a synthetic alert and always answers 200 `{ ok, status, attempts, error }`. `deliveries` → `{ deliveries, limit, offset }` (`limit` 1–200, default 20). `400 INVALID_INPUT`, `404 NOT_FOUND`. Staff-only in server mode, writes admin-only.
|
||||
|
||||
### Web Push
|
||||
|
||||
```http
|
||||
GET /api/push/vapid-public-key
|
||||
POST /api/push/subscribe
|
||||
DELETE /api/push/subscribe
|
||||
POST /api/push/send
|
||||
```
|
||||
|
||||
`vapid-public-key` → `{ publicKey }` for `PushManager.subscribe`. `subscribe` stores a browser subscription `{ endpoint, keys: { p256dh, auth } }` (upsert by `endpoint`; in server mode tagged with the user) → `{ ok: true }`; `DELETE` takes `{ endpoint }` in the BODY (a user removes only their own) → `{ ok: true }`. `send` `{ title, body, url? }` pushes to every subscription (in server mode: the caller's own) and fires a native notification in the desktop app → `{ ok: true, native, pushed, failed }`. `400` with `{ error: { message } }` on a missing field.
|
||||
|
||||
### Updates
|
||||
|
||||
```http
|
||||
GET /api/updates/status
|
||||
POST /api/updates/check
|
||||
POST /api/updates/apply
|
||||
```
|
||||
|
||||
Release-based update detection (`server/lib/update-check.js`). `status` and `check` answer the same object — `{ git_repo, update_available, install_kind: "checkout"|"plugin", local_version, latest_release, latest_release_url, latest_release_notes, release_check, commits_behind, commits_behind_release, manual_command, situation, last_plugin_update, repo_root, … }` (`repo_root` is `null` for a developer); `check` also broadcasts it as `update_status`. `apply` (admin-only in server mode) picks its mode by install kind, never from the request: a fast-forwardable checkout merges the release `--ff-only`, rebuilds, answers `{ …, applied: true }` and then restarts the server; a plugin-cache install with a newer release hands off to the detached plugin-update chain → `{ …, applied: true, mode: "plugin" }`. Nothing to apply is `409 UPDATE_NOT_APPLICABLE` (`reason`: `up_to_date`, `plugin_managed`, `not_fast_forwardable`). `500 UPDATE_STATUS_FAILED` / `UPDATE_CHECK_FAILED` / `UPDATE_APPLY_FAILED`.
|
||||
|
||||
### Import history
|
||||
|
||||
```http
|
||||
GET /api/import/guide
|
||||
POST /api/import/rescan
|
||||
POST /api/import/scan-path
|
||||
POST /api/import/upload
|
||||
```
|
||||
|
||||
Bring Claude Code history in (Settings → Import). `guide` → `{ platform, default_projects_dir, default_projects_dir_display, default_projects_dir_exists, default_projects_dir_stats: { projects, jsonl_files }, archive_command, supported_extensions, max_upload_bytes, max_upload_files, steps }`. `rescan` re-reads the default projects folder → `{ ok: true, source: "default", … }`. `scan-path` `{ path }` (absolute, `~` expanded, a directory) imports from it → `{ ok: true, source: "path", path, imported, skipped, backfilled, errors, sessions_seen, files_scanned }`; `400 INVALID_INPUT` / `PATH_NOT_FOUND` / `NOT_A_DIRECTORY`. `upload` takes multipart `files` (`.jsonl`, `.meta.json`, `.zip`, `.tar`, `.tar.gz`, `.tgz`, `.gz`; up to `AGENTHUB_IMPORT_MAX_FILES` = 2000 files and `AGENTHUB_IMPORT_MAX_BYTES` = 1 GB), extracts archives into a private temp dir and imports → `{ ok: true, source: "upload", files_received, rejected_files, entries_extracted, entries_skipped, …counts }`; `400 NO_FILES` / `NO_JSONL`, `413` when an archive passes the extraction limits. Imports are idempotent, and progress is broadcast as `import.progress`. Staff-only in server mode, admin-only to run; `500 IMPORT_FAILED`.
|
||||
|
||||
|
||||
## WebSocket API
|
||||
|
||||
### Connection
|
||||
|
||||
+2
-2
@@ -109,7 +109,7 @@ The CLI talks to the local dashboard server — **API-backed commands require it
|
||||
| `agenthub server tools` | One Linux user per account: publish this install's scripts and every installed AI tool under `/opt/agenthub` (`tools/bin/<command>`), where every account's pane runs them. The server also does this at start and after each install or update. Prints each tool's shared copy, or why it could not be shared |
|
||||
| `agenthub server off` / `agenthub server status` | Back to local mode (accounts kept) / show the mode and account count |
|
||||
| `agenthub accounts list\|add <name>\|remove <name>` | The **Claude account pool**: new Claude sessions spread over these accounts (each a `claude setup-token` token, typed hidden or read from stdin). `list` shows live panes, $ and tokens in the last 5 h / 7 d, and quota. With `--tool codex` the same for the **Codex account pool**: `add <name> --tool codex --api-key` (key typed hidden or on stdin) or `--from <dir>` (a `codex login` home or its `auth.json`). `pin <account> <user>` / `unpin <user>` (add `--tool codex` for Codex) switches one user's sessions to that account (a Claude login goes into their home at once, running sessions follow without a restart) (server mode; `list` shows a `pinned` column). See [SERVER-MODE.md](SERVER-MODE.md#claude-account-pool) |
|
||||
| `agenthub users list\|add <name> [--role admin\|analyst\|member]\|passwd <name>\|role <name> <role>\|disable <name>\|enable <name>\|provision <name>\|--all\|assign <workspace id> <name>\|audit [--limit N]` | Server mode's accounts, written straight to the database. Once `install-system` ran, `add` first creates the Linux user `ah-<name>`; a name must then be lower case: a letter, then up to 28 letters, digits, `_` or `-`. `disable`/`enable` lock or unlock that Linux user too, and `provision` gives users made earlier their Linux user (idempotent); `assign` gives a workspace made before server mode an owner; `audit` lists admin/analyst reads of members' sessions. Inside a dashboard pane in server mode, every command acts as the workspace's owner through the pane's `AGENTHUB_PANE_TOKEN`. A password is typed hidden twice, or read from the first line of stdin |
|
||||
| `agenthub users list\|add <name> [--role admin\|analyst\|developer]\|passwd <name>\|role <name> <role>\|disable <name>\|enable <name>\|provision <name>\|--all\|assign <workspace id> <name>\|audit [--limit N]` | Server mode's accounts, written straight to the database. Once `install-system` ran, `add` first creates the Linux user `ah-<name>`; a name must then be lower case: a letter, then up to 28 letters, digits, `_` or `-`. `disable`/`enable` lock or unlock that Linux user too, and `provision` gives users made earlier their Linux user (idempotent); `assign` gives a workspace made before server mode an owner; `audit` lists admin/analyst reads of developers' sessions. Inside a dashboard pane in server mode, every command acts as the workspace's owner through the pane's `AGENTHUB_PANE_TOKEN`. A password is typed hidden twice, or read from the first line of stdin |
|
||||
|
||||
### Interactive REPL
|
||||
|
||||
@@ -273,7 +273,7 @@ Omit `<id>` and the command addresses the workspace owning the current directory
|
||||
|
||||
Resolution order, shared by every cwd-resolving command: `--workspace` (old name `--lane`)/leading id → `AGENTHUB_LANE_ID` (exported into every tmux pane the dashboard starts) → `--tool <id>`, else the harness's marker (`CLAUDECODE` means claude; Codex has no equivalent marker, so a codex session outside a dashboard pane passes `--tool codex`) → the workspace owning the directory. One directory can back a `claude` workspace and a `codex` workspace at once, so when two candidates remain the command fails and names both instead of picking one — see [Which workspace a CLI command addresses](LANES.md#which-workspace-a-cli-command-addresses).
|
||||
|
||||
Because a workspace's services are fully detached, restarting the dashboard (or `agenthub update`) never stops a running stack.
|
||||
Because a workspace's services are fully detached, restarting the dashboard (or applying an update — `agenthub update-check` reports one, the dashboard's Update button applies it) never stops a running stack.
|
||||
|
||||
### Integrations
|
||||
|
||||
|
||||
+1
-1
@@ -619,7 +619,7 @@ Server mode's accounts ([SERVER-MODE.md](SERVER-MODE.md), `server/lib/auth.js`).
|
||||
|
||||
| Table | Columns |
|
||||
| --- | --- |
|
||||
| `users` | `id`, `username` (unique, case-insensitive), `role` (`admin`\|`analyst`\|`member`), `password_hash` (`scrypt$<salt>$<hash>`), `disabled`, `created_at` |
|
||||
| `users` | `id`, `username` (unique, case-insensitive), `role` (`admin`\|`analyst`\|`developer` — `member` before, migrated on start), `password_hash` (`scrypt$<salt>$<hash>`), `disabled`, `created_at` |
|
||||
| `auth_sessions` | `token_hash` (SHA-256 of the cookie token, PK), `user_id` → `users.id` (cascade), `created_at`, `expires_at` (30 days) |
|
||||
| `account_pins` | `user_id` → `users.id` (cascade), `tool` (`claude`\|`codex`), `account` (a pool account's name), `pinned_at`; PK (`user_id`, `tool`) — an admin's pin of a user to one pool account (`server/lib/account-pins.js`) |
|
||||
|
||||
|
||||
+1
-1
@@ -107,7 +107,7 @@ Internal build — all rights reserved.
|
||||
Prometheus rules + Grafana boards for `/api/metrics`
|
||||
(`docker-compose.monitoring.yml`, [docs/MONITORING.md](MONITORING.md)).
|
||||
- **Server mode for a shared dashboard** (`agenthub server setup`): a login on
|
||||
every page and API call, with admin / member / analyst roles. It is off by
|
||||
every page and API call, with admin / developer / analyst roles. It is off by
|
||||
default, and a local install does not change. It is login only, without
|
||||
per-user isolation yet ([docs/SERVER-MODE.md](SERVER-MODE.md)).
|
||||
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ graph TB
|
||||
|----------|-------------|----------|
|
||||
| [client/README.md](../client/README.md) | React frontend architecture, components, state management | Frontend developers |
|
||||
| [server/README.md](../server/README.md) | Express backend, database, WebSocket, API | Backend developers |
|
||||
| [API.md](./API.md) | REST API endpoints (sessions, agents, events, stats, analytics, hooks, pricing, workflows, settings, import history, **cc-config**, **run**), WebSocket protocol (including `run_stream` / `run_status` / `run_input_ack` for the Run page) | Integration developers |
|
||||
| [API.md](./API.md) | REST API endpoints (sessions, agents, events, stats, analytics, hooks, pricing, workflows, settings, import history, **cc-config**, **run**), WebSocket protocol (JSON broadcast messages, plus the binary `/ws-pty/:runId` terminal stream of a lane pane) | Integration developers |
|
||||
| [SERVER-MODE.md](./SERVER-MODE.md) | Server mode: logins and roles for a dashboard shared over the web (`agenthub server setup`), reverse proxy setup | Operators |
|
||||
| [DATABASE.md](./DATABASE.md) | SQLite schema, queries, performance | Database administrators |
|
||||
| [HOOKS.md](./HOOKS.md) | Claude Code hook system integration | Hook developers |
|
||||
|
||||
Reference in New Issue
Block a user