969b06253c
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.
111 lines
6.3 KiB
Markdown
111 lines
6.3 KiB
Markdown
# Settings persistence + editable CODEX_HOME
|
|
|
|
Issue: #40 follow-up to part B. Date: 2026-09-29. Status: self-reviewed (user
|
|
asked for autonomous work with self-review per part).
|
|
|
|
## Problem
|
|
|
|
`setClaudeHome` persists through `writeEnvFile`, which writes `<repo>/.env`.
|
|
For a plugin install that directory is the plugin CACHE, replaced on every
|
|
update, so a saved CLAUDE_HOME silently reverts. Two more faults ride along:
|
|
|
|
- The data dir (database, runtime, `ai-tools.json`) is `getDataDir()` =
|
|
`<CLAUDE_HOME>/agenthub`, recomputed on every call. Changing CLAUDE_HOME at
|
|
runtime moves every later `getDataDir()` caller mid-process, and the next
|
|
start opens an EMPTY database under the new home — indistinguishable from
|
|
data loss.
|
|
- `writeEnvFile` appends a blank line on every save.
|
|
|
|
An editable CODEX_HOME (deferred from part B) has the same persistence problem,
|
|
plus one of its own: the AI Router layer `<name>.config.toml` lives in
|
|
`$CODEX_HOME`, so after a change a codex lane on a router would start with
|
|
`-p <name>` against a home that has no such layer.
|
|
|
|
## Design
|
|
|
|
1. **A settings file outside both the repo and CLAUDE_HOME:**
|
|
`$XDG_CONFIG_HOME/agenthub/settings.env` (default `~/.config/agenthub/`),
|
|
overridable with `AGENTHUB_SETTINGS_FILE` (the server test runner points it
|
|
at a temp path). Dir 0700, file 0600, atomic write, one `KEY=value` per
|
|
line, no blank-line growth. New module `server/lib/settings-env.js`.
|
|
2. **Load order at startup** (`server/index.js`, replacing `loadDotEnv`): the
|
|
real environment wins, then `<repo>/.env`, then the settings file
|
|
(`loadEnvFiles({owned: [.env], files: [settings]})`). The checkout's `.env`
|
|
ranks above the per-user file because it is more specific: a dev checkout
|
|
whose `.env` names its own `DASHBOARD_DATA_DIR` must not be pointed at the
|
|
live database by a pin saved from the plugin dashboard. A key is only set
|
|
when still unset, exactly as before. The keys the real environment or `.env`
|
|
supplied are remembered (`envOwned`), so a route can say a saved value will
|
|
be overridden at the next start. Keys that came from a file are named in
|
|
`AGENTHUB_FILE_KEYS`: a self-restart spawns the successor with this
|
|
process's env, and without the marker the successor would count them as the
|
|
real environment's and warn falsely. A saved path may not contain a line
|
|
break (it would inject a second `KEY=`).
|
|
3. **Pin the data dir on the first CLAUDE_HOME change.** Before switching,
|
|
`setClaudeHome` writes `DASHBOARD_DATA_DIR=<the data dir in use>` to the
|
|
settings file and to `process.env` when neither already sets it. The
|
|
database and runtime dir stay where the plugin bootstrap and the CLI (which
|
|
never read the settings file) look for them. The discovery file
|
|
(`<CLAUDE_HOME>/.agenthub.json`) is NOT pinned and follows CLAUDE_HOME, as it
|
|
did before this change; hooks under a different CLAUDE_HOME then fall back to
|
|
port 4820.
|
|
4. **`validateHomeDir(label, input)`** in `settings-env.js` is the one path
|
|
check both homes use: `~/` expansion, absolute, `path.resolve`, exists, is
|
|
a directory, and neither IS nor CONTAINS the OS home (`EBADHOME`) — `/`,
|
|
`/home`, `~` itself, or a symlink, bind mount or differently-cased path to
|
|
any of them, compared by device and inode against the home and each of its
|
|
ancestors.
|
|
5. **`setCodexHome`** (`server/lib/codex-home.js`) and
|
|
`GET/PUT /api/settings/codex-home`. PUT re-applies every AI Router profile a
|
|
codex lane uses (`reapplyCodexLayers` in `router-apply.js`), so the layer
|
|
exists in the new home, and answers
|
|
`{ ok, codex_home, env_owned, routers: { updated, failed } }`.
|
|
`GET/PUT /api/settings/claude-home` gain `env_owned` (additive).
|
|
6. **UI:** a shared `HomeDirSetting` component renders CLAUDE_HOME (Claude Code
|
|
→ AgentHub) and CODEX_HOME (new Codex → AgentHub sub-tab). It shows the
|
|
env-owned warning, the router re-apply failures, and — for CODEX_HOME —
|
|
that a running codex pane keeps its old home until restarted.
|
|
|
|
## Not changed
|
|
|
|
Existing `<repo>/.env` files keep working, and rank above the settings file: a
|
|
key a `.env` sets is `env_owned`, so a value saved from the UI for it is
|
|
overridden at the next start, and the UI says so. The file is per OS user, so every AgentHub server
|
|
of that user (a plugin install and a dev checkout) reads the same one. Stores that live
|
|
under CLAUDE_HOME by design (`agenthub-routers/`, `agenthub-profiles/`,
|
|
`agenthub-git-credentials/`) still follow it. The CLI and bootstrap do not read
|
|
the settings file; pinning the data dir is what keeps them consistent.
|
|
|
|
## Tests
|
|
|
|
Server: settings-env parse/load order/`envOwned`/write (no blank lines, 0600),
|
|
`validateHomeDir` refusals, `setClaudeHome` pinning `DASHBOARD_DATA_DIR`,
|
|
`setCodexHome` + the route (refusal 400, success, router re-apply called),
|
|
OpenAPI entry. Client: `HomeDirSetting` save/error/env warning, the Codex
|
|
sub-tab. Browser check on an isolated server.
|
|
|
|
## Found in the whole-change review, fixed
|
|
|
|
- **A dev checkout's `.env` lost to the plugin dashboard's pin.** The settings
|
|
file ranked above `<repo>/.env`, so a `DASHBOARD_DATA_DIR` pinned from the
|
|
plugin dashboard pointed a dev checkout's server at the live database. The
|
|
order is now real environment > `.env` > settings file, and a `.env` key
|
|
counts as `env_owned`.
|
|
- **An old line could win.** `writeSetting` replaced only the first line for a
|
|
key; it now drops every earlier one, `KEY = value` included, and writes
|
|
through a symlinked settings file to its target.
|
|
- **A case-insensitive or bind-mounted path to the home passed.**
|
|
`validateHomeDir` now compares device and inode with the home and each
|
|
ancestor.
|
|
- **Another site could set a home.** `PUT /api/settings/claude-home` and
|
|
`/codex-home` carry `sameOriginGuard` (403 `EBADORIGIN`): a home decides
|
|
which config a pane runs.
|
|
- **Lanes silently lost their router.** The router store is
|
|
`<CLAUDE_HOME>/agenthub-routers/`, so a new home may lack a lane's profile;
|
|
the claude-home PUT answers `routers: {missing: [...]}`
|
|
(`lanesWithMissingRouter`) and the UI warns that those lanes start on native.
|
|
- **Stale paths after a change.** The Claude hook status (`settings.js`),
|
|
`scripts/install-hooks.js` and `scripts/import-history.js` read their
|
|
CLAUDE_HOME-derived paths per call, not at load. `cc-watcher.js` still
|
|
watches the startup home until a restart.
|