feat: per-tool settings, persistent settings file, Codex and Antigravity sessions, tokens and hooks (#40 B, C1, C2, D)

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.
This commit is contained in:
2026-09-29 16:09:19 +07:00
parent 7ec5a7cd85
commit 969b06253c
95 changed files with 9278 additions and 1696 deletions
+522 -8
View File
@@ -228,6 +228,9 @@ components:
updated_at:
type: string
format: date-time
tool:
type: string
description: 'The AI coding CLI that produced the session: `claude` (Claude Code''s hooks and imported transcripts), `codex` (Codex''s rollout journals and its opt-in hooks) or `antigravity` (agy''s conversation files and its opt-in hooks).'
agent_count:
type: integer
nullable: true
@@ -1017,11 +1020,17 @@ components:
properties:
hook_type:
type: string
description: 'Hook type from Claude Code (common values: PreToolUse, PostToolUse, Stop, SubagentStop, Notification, SessionStart, SessionEnd)'
description: 'Hook type from the CLI that sent it (Claude Code: PreToolUse, PostToolUse, Stop, SubagentStop, Notification, SessionStart, SessionEnd; Codex adds PermissionRequest and Interrupt; agy sends PreInvocation, PostToolUse and Stop)'
tool:
type: string
enum:
- claude
- codex
- antigravity
description: 'Which CLI sent the hook (`scripts/hook-handler.js <Event> [codex|antigravity]`). An allowlist: anything else is `claude`.'
data:
type: object
required:
- session_id
description: The raw hook payload. Claude Code and Codex name the session `session_id`; agy names it `conversationId` and is accepted only with a `transcriptPath` under its own CLI directory.
properties:
session_id:
type: string
@@ -1034,7 +1043,6 @@ components:
type: object
required:
- ok
- event
properties:
ok:
type: boolean
@@ -1042,6 +1050,12 @@ components:
- true
event:
$ref: '#/components/schemas/DashboardEvent'
ignored:
type: boolean
description: The hook maps to nothing AgentHub records (a Codex sub-agent's lifecycle hook, an agy event other than a turn's first PreInvocation, a PostToolUse or a Stop, or a hook from the Antigravity IDE).
duplicate:
type: boolean
description: The same hook delivered twice within 10 seconds; stored once.
PricingRule:
type: object
required:
@@ -4495,6 +4509,9 @@ components:
type: string
description: Absolute path to the active Claude Code home directory (CLAUDE_HOME, or `<homedir>/.claude` when unset).
example: /Users/son/.claude
env_owned:
type: boolean
description: 'True when the real environment, or the checkout''s own `.env`, set this variable at startup: either wins over the settings file, so a saved value is overridden again at the next start.'
SettingsClaudeHomeUpdateRequest:
type: object
description: Request body for changing the Claude Code home directory. A leading `~` is expanded to the user's home directory; the resolved path must be absolute and point to an existing directory.
@@ -4507,7 +4524,7 @@ components:
example: ~/.codefuse/engine/cc
SettingsClaudeHomeUpdateResponse:
type: object
description: Confirmation that CLAUDE_HOME was updated. The new value is applied to process.env immediately and persisted to the project `.env` file.
description: Confirmation that CLAUDE_HOME was updated. The new value is applied to process.env immediately and persisted to the settings file (`$XDG_CONFIG_HOME/agenthub/settings.env`, default `~/.config/agenthub/`, or AGENTHUB_SETTINGS_FILE).
required:
- ok
- claude_home
@@ -4520,6 +4537,17 @@ components:
type: string
description: The resolved absolute path now in effect (after `~` expansion).
example: /Users/son/.codefuse/engine/cc
env_owned:
type: boolean
description: 'True when the real environment, or the checkout''s own `.env`, set this variable at startup: either wins over the settings file, so a saved value is overridden again at the next start.'
routers:
type: object
properties:
missing:
type: array
items:
type: string
description: '`<lane id> (<title>): <profile>` for every lane naming an AI Router profile the new home''s store (`<CLAUDE_HOME>/agenthub-routers/`) does not have — such a lane starts on its base configuration.'
WorkflowToolRun:
type: object
description: 'A Claude Code Workflow-tool run (issue #167): a fleet of sub-agents spawned by the ''Workflow'' tool (or self-paced /loop). These emit no hooks; the source of truth is the on-disk run journal, ingested into the `workflows` table (see server/lib/workflow-ingest.js). Keyed by `run_id` and parented to the launching session. The JSON-blob columns `phases` and `progress` are parsed into arrays before serialization.'
@@ -4727,6 +4755,16 @@ paths:
type: string
description: Case-insensitive search across `id` / `name` / `cwd`. Composes with the status filter when both are present.
example: pricing
- name: tool
in: query
schema:
type: string
enum:
- claude
- codex
- antigravity
description: Only the sessions of one CLI. Applied before `limit`, so a busy Codex lane cannot push Claude sessions off a page.
example: claude
- $ref: '#/components/parameters/SourcesQuery'
example: local,4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11
- $ref: '#/components/parameters/LimitQuery'
@@ -5739,6 +5777,8 @@ paths:
On success the response is `{ ok: true, event: { ... } }`, where `event` echoes the normalized row that was just inserted (`session_id`, `agent_id`, `event_type`, `tool_name`, `summary`, `created_at`). Ingestion is designed to be fail-safe and non-blocking for the hook caller.
The optional `tool` (`codex`, `antigravity`; anything else is `claude`) names the CLI that sent the hook. A Codex or agy hook is mapped onto the same hook types first (a Codex PermissionRequest becomes a Notification, an Interrupt a Stop; an agy turn's first PreInvocation a UserPromptSubmit), and one that maps to nothing is answered `{ ok: true, ignored: true }`. A hook delivered twice is answered `{ ok: true, duplicate: true }` and stored once.
Validation failures return HTTP 400 with an `ErrorResponse` body (`{ error: { code, message } }`): `INVALID_INPUT` when `hook_type` or `data` is missing, and `MISSING_SESSION` when `data.session_id` is absent.
requestBody:
required: true
@@ -6472,8 +6512,20 @@ paths:
tags:
- Settings
summary: Reset pricing table to defaults
description: ⚠ DESTRUCTIVE to pricing customizations. Deletes EVERY row in the model_pricing table and re-seeds it from the dashboard's built-in DEFAULT_PRICING list. Any custom rates or custom model patterns you added are permanently lost — there is no undo. Captured session/token data is untouched (only the pricing rules used to *compute* cost change). The response returns the full freshly-seeded pricing table.
description: '⚠ DESTRUCTIVE to pricing customizations. Deletes EVERY row in the model_pricing table and re-seeds it from the dashboard''s built-in DEFAULT_PRICING list. Any custom rates or custom model patterns you added are permanently lost — there is no undo. Captured session/token data is untouched (only the pricing rules used to *compute* cost change). The response returns the full freshly-seeded pricing table. With `{"scope": "claude"}` (the Claude Code tab''s button) only `claude-…` rows are deleted and re-seeded; every other vendor''s rows and custom patterns are kept.'
operationId: resetPricing
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
scope:
type: string
enum:
- claude
description: 'Reset only `claude-…` rows. Omitted: every row.'
responses:
'200':
description: Pricing defaults restored
@@ -12256,6 +12308,462 @@ paths:
results:
- id: src_a
ok: true
/api/settings/codex-hooks:
get:
tags:
- Settings
summary: Status of AgentHub's hooks in Codex
description: 'Per event (SessionStart, UserPromptSubmit, PreToolUse, PermissionRequest, PostToolUse, Stop, Interrupt, SessionEnd): whether AgentHub''s group is in `$CODEX_HOME/hooks.json`, and whether Codex''s `config.toml` holds a trust entry for it. Codex runs a hook only once the user trusts it; AgentHub only reads that trust.'
operationId: getCodexHooks
responses:
'200':
description: Where AgentHub's Codex hooks stand
content:
application/json:
schema:
type: object
required:
- supported
- readable
- path
- script
- installed
- trusted
- events
properties:
supported:
type: boolean
description: False on Windows (the command is a POSIX script)
readable:
type: boolean
description: False when hooks.json is not a JSON object
error:
type: string
path:
type: string
description: '`$CODEX_HOME/hooks.json`'
script:
type: string
description: '`$CODEX_HOME/agenthub-hook.sh`, the forwarder'
installed:
type: boolean
trusted:
type: boolean
description: Codex's config.toml holds a trust entry for every one of AgentHub's hooks
events:
type: object
additionalProperties:
type: object
properties:
installed:
type: boolean
trusted:
type: boolean
'500':
description: 'ESAVE: the file system refused the write'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
post:
tags:
- Settings
summary: Install (or reinstall) AgentHub's hooks in Codex
description: Refuses a cross-origin request (403 EBADORIGIN). Writes the forwarder script and adds one hook of AgentHub's per event to `$CODEX_HOME/hooks.json`, keeping every other entry. The command is the script's stable path, so Codex's trust survives AgentHub updates; the server rewrites the script's content at every start. Codex asks the user to trust the new hooks the next time it starts.
operationId: installCodexHooks
responses:
'200':
description: Where AgentHub's Codex hooks stand
content:
application/json:
schema:
type: object
required:
- supported
- readable
- path
- script
- installed
- trusted
- events
properties:
supported:
type: boolean
description: False on Windows (the command is a POSIX script)
readable:
type: boolean
description: False when hooks.json is not a JSON object
error:
type: string
path:
type: string
description: '`$CODEX_HOME/hooks.json`'
script:
type: string
description: '`$CODEX_HOME/agenthub-hook.sh`, the forwarder'
installed:
type: boolean
trusted:
type: boolean
description: Codex's config.toml holds a trust entry for every one of AgentHub's hooks
events:
type: object
additionalProperties:
type: object
properties:
installed:
type: boolean
trusted:
type: boolean
'400':
description: EBADJSON (hooks.json is not a JSON object), EFOREIGN (the forwarder path holds a script that is not AgentHub's) or EUNSUPPORTED (Windows)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'500':
description: 'ESAVE: the file system refused the write'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
delete:
tags:
- Settings
summary: Remove AgentHub's hooks from Codex
description: Refuses a cross-origin request (403 EBADORIGIN). Takes AgentHub's own hooks (matched by their exact command) out of `$CODEX_HOME/hooks.json` (deleting the file when nothing else is left, or emptying it through the link when it is a symlink) and deletes the forwarder script when it is AgentHub's. 400 EBADJSON when hooks.json is not a JSON object.
operationId: removeCodexHooks
responses:
'200':
description: Where AgentHub's Codex hooks stand
content:
application/json:
schema:
type: object
required:
- supported
- readable
- path
- script
- installed
- trusted
- events
properties:
supported:
type: boolean
description: False on Windows (the command is a POSIX script)
readable:
type: boolean
description: False when hooks.json is not a JSON object
error:
type: string
path:
type: string
description: '`$CODEX_HOME/hooks.json`'
script:
type: string
description: '`$CODEX_HOME/agenthub-hook.sh`, the forwarder'
installed:
type: boolean
trusted:
type: boolean
description: Codex's config.toml holds a trust entry for every one of AgentHub's hooks
events:
type: object
additionalProperties:
type: object
properties:
installed:
type: boolean
trusted:
type: boolean
'400':
description: EBADJSON (hooks.json is not a JSON object), EFOREIGN (the forwarder path holds a script that is not AgentHub's) or EUNSUPPORTED (Windows)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'500':
description: 'ESAVE: the file system refused the write'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/api/settings/antigravity-hooks:
get:
tags:
- Settings
summary: Status of AgentHub's hook in Antigravity
description: 'Per event (PreInvocation, PostToolUse, Stop): whether AgentHub''s named hook `agenthub` in `~/.gemini/config/hooks.json` runs its forwarder. PreToolUse is never installed: agy treats its answer as a permission decision.'
operationId: getAntigravityHooks
responses:
'200':
description: Where AgentHub's Antigravity hook stands
content:
application/json:
schema:
type: object
required:
- supported
- readable
- path
- script
- installed
- enabled
- events
properties:
supported:
type: boolean
description: False on Windows (the command is a POSIX script)
readable:
type: boolean
description: False when hooks.json is not a JSON object
error:
type: string
path:
type: string
description: '`~/.gemini/config/hooks.json`'
script:
type: string
description: '`~/.gemini/config/agenthub-hook.sh`, the forwarder'
installed:
type: boolean
enabled:
type: boolean
description: 'Installed and not switched off (`"enabled": false`) in the file'
events:
type: object
additionalProperties:
type: object
properties:
installed:
type: boolean
'500':
description: 'ESAVE: the file system refused the write'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
post:
tags:
- Settings
summary: Install (or reinstall) AgentHub's hook in Antigravity
description: Refuses a cross-origin request (403 EBADORIGIN). Writes the forwarder script and sets the named hook `agenthub` in `~/.gemini/config/hooks.json`, keeping every other named hook. agy loads it at its next start, with no trust step.
operationId: installAntigravityHooks
responses:
'200':
description: Where AgentHub's Antigravity hook stands
content:
application/json:
schema:
type: object
required:
- supported
- readable
- path
- script
- installed
- enabled
- events
properties:
supported:
type: boolean
description: False on Windows (the command is a POSIX script)
readable:
type: boolean
description: False when hooks.json is not a JSON object
error:
type: string
path:
type: string
description: '`~/.gemini/config/hooks.json`'
script:
type: string
description: '`~/.gemini/config/agenthub-hook.sh`, the forwarder'
installed:
type: boolean
enabled:
type: boolean
description: 'Installed and not switched off (`"enabled": false`) in the file'
events:
type: object
additionalProperties:
type: object
properties:
installed:
type: boolean
'400':
description: EBADJSON (hooks.json is not a JSON object), EFOREIGN (the forwarder path holds a script that is not AgentHub's) or EUNSUPPORTED (Windows)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'500':
description: 'ESAVE: the file system refused the write'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
delete:
tags:
- Settings
summary: Remove AgentHub's hook from Antigravity
description: Refuses a cross-origin request (403 EBADORIGIN). Deletes the named hook `agenthub` (and the file when nothing else is left, unless it is a symlink, which is emptied through the link) and the forwarder script when it is AgentHub's. 400 EBADJSON when hooks.json is not a JSON object.
operationId: removeAntigravityHooks
responses:
'200':
description: Where AgentHub's Antigravity hook stands
content:
application/json:
schema:
type: object
required:
- supported
- readable
- path
- script
- installed
- enabled
- events
properties:
supported:
type: boolean
description: False on Windows (the command is a POSIX script)
readable:
type: boolean
description: False when hooks.json is not a JSON object
error:
type: string
path:
type: string
description: '`~/.gemini/config/hooks.json`'
script:
type: string
description: '`~/.gemini/config/agenthub-hook.sh`, the forwarder'
installed:
type: boolean
enabled:
type: boolean
description: 'Installed and not switched off (`"enabled": false`) in the file'
events:
type: object
additionalProperties:
type: object
properties:
installed:
type: boolean
'400':
description: EBADJSON (hooks.json is not a JSON object), EFOREIGN (the forwarder path holds a script that is not AgentHub's) or EUNSUPPORTED (Windows)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'500':
description: 'ESAVE: the file system refused the write'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/api/settings/codex-home:
get:
tags:
- Settings
summary: Get the active Codex home directory
description: Returns the Codex home directory (CODEX_HOME, else `<homedir>/.codex`) the dashboard reads Codex config and rollout journals from, writes AI Router layers into, and hands every codex lane pane through tmux `-e`.
operationId: getCodexHome
responses:
'200':
description: Current Codex home directory
content:
application/json:
schema:
type: object
required:
- codex_home
- env_owned
properties:
codex_home:
type: string
env_owned:
type: boolean
example:
codex_home: /Users/son/.codex
env_owned: false
put:
tags:
- Settings
summary: Update the Codex home directory
description: Same validation and persistence as PUT /api/settings/claude-home (403 EBADORIGIN from another site, settings file, EBADHOME for a path that is or contains the OS home, INVALID_PATH otherwise). On success it re-writes the AI Router layer (`<name>.config.toml`) of every codex lane on a router into the new home, reported as `routers`, because `codex -p <name>` loads it from there. A codex pane already running keeps its old home until restarted.
operationId: updateCodexHome
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- path
properties:
path:
type: string
example:
path: ~/.codex-work
responses:
'200':
description: Codex home updated
content:
application/json:
schema:
type: object
required:
- ok
- codex_home
- env_owned
- routers
properties:
ok:
type: boolean
enum:
- true
codex_home:
type: string
env_owned:
type: boolean
routers:
type: object
required:
- updated
- failed
properties:
updated:
type: integer
failed:
type: array
items:
type: string
example:
ok: true
codex_home: /Users/son/.codex-work
env_owned: false
routers:
updated: 1
failed: []
'400':
description: Invalid path (INVALID_PATH) or too broad (EBADHOME).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error:
code: EBADHOME
message: 'Too broad for CODEX_HOME: / contains your home directory'
'500':
description: The settings file could not be written (ESAVE).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/api/settings/claude-home:
get:
tags:
@@ -12276,7 +12784,7 @@ paths:
tags:
- Settings
summary: Update the Claude Code home directory
description: Changes the Claude Code home directory used for transcript/settings discovery. A leading `~/` in `path` is expanded to the OS home directory; the resolved value must be an absolute path that exists and is a directory. On success the new value is applied to process.env immediately (so subsequent reads use it) and persisted to the project `.env` file. Returns 400 INVALID_PATH when `path` is missing/not a string, or when the resolved path is not absolute, does not exist, or is not a directory.
description: Changes the Claude Code home directory used for transcript/settings discovery. A leading `~/` in `path` is expanded to the OS home directory; the resolved value must be an absolute path that exists and is a directory. On success the new value is applied to process.env immediately (so subsequent reads use it) and persisted to the settings file (`$XDG_CONFIG_HOME/agenthub/settings.env`, default `~/.config/agenthub/`, or AGENTHUB_SETTINGS_FILE — never the repo `.env`, which a plugin update replaces). The data dir (database, runtime) is derived from CLAUDE_HOME, so the first change also pins the one in use as DASHBOARD_DATA_DIR unless the environment already sets it. Refuses a cross-origin request (403 EBADORIGIN). Returns 400 EBADHOME when the path is, or contains (symlinks resolved), the OS user's home directory — `/`, `/home`, `~` — because everything under CLAUDE_HOME becomes readable through the config panel; 400 INVALID_PATH when `path` is missing/not a string, or the resolved path is not absolute, does not exist, or is not a directory.
operationId: updateClaudeHome
requestBody:
required: true
@@ -12297,7 +12805,7 @@ paths:
ok: true
claude_home: /Users/son/.codefuse/engine/cc
'400':
description: Invalid path — `path` missing or not a string, or the resolved path is not absolute / does not exist / is not a directory (code INVALID_PATH).
description: Invalid path — `path` missing or not a string, or the resolved path is not absolute / does not exist / is not a directory (code INVALID_PATH), or it is or contains the OS home directory (code EBADHOME).
content:
application/json:
schema:
@@ -12306,6 +12814,12 @@ paths:
error:
code: INVALID_PATH
message: 'Directory does not exist: /Users/son/.codefuse/engine/cc'
'500':
description: The settings file could not be written (ESAVE).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/api/workflows/runs:
get:
tags: