Move final JSON / plain-text state to DB (gitlab token, SDK sessions, agent secrets) #823

Closed
opened 2026-05-04 11:56:50 +00:00 by claude-desktop · 0 comments
Collaborator

User story

As an operator, I want every piece of runtime state that claude-hooks itself owns to live in the SQLite DB rather than scattered JSON / plain-text files on disk, so that backups / restores / migrations are atomic, secrets are encrypted at rest, and the dashboard's Settings → Service surface is the single source of truth.

Three storage holdouts remain after DOB-4 / DOB-5 ripped service.json and agents.json:

  1. ~/.local/state/claude-hooks/gitlab-oauth-token.json — the F3-GL GitLab OAuth access + refresh tokens.
  2. ~/.local/state/claude-hooks/sessions.json — the Claude SDK session-resume map (<forge>:<agent>:<repo>:<issue>{id, last_used_at}).
  3. ~/.config/claude-hooks/tokens/<agent> + ~/.config/claude-hooks/penpot-token — per-agent Forgejo PATs and the Penpot API token, both stored as plain-text mode-0600 files.

The Claude Code SDK config dirs under ~/.config/claude-hooks/agent-env/<agent>/ (settings.json, policy-limits.json, mcp-needs-auth-cache.json, system-prompt.md, sessions/) are owned by the SDK and stay on disk — they're the SDK's wire format. Same for ~/.config/claude-hooks/claude-credentials/.credentials.json (mounted into containers).

Acceptance criteria

GitLab OAuth → DB

  • operator_oauth_tokens already has rows for forgejo / github via upsertOperatorOAuth. Land the GitLab equivalent — oauth-gitlab.ts reads / writes the row instead of gitlab-oauth-token.json.
  • Migration 012-migrate-gitlab-oauth-to-db.ts: if the JSON file exists and no DB row for forge_type='gitlab', parse the file and insert via upsertOperatorOAuth. Then unlink the JSON file. Idempotent.
  • Drop gitlabOAuthTokenPath() + every readFile / writeFile reference.
  • Tests: existing oauth-gitlab.test.ts paths exercise DB; add a migration test seeding the JSON file then asserting the row + file removal.

Claude SDK sessions → DB

  • New table claude_sdk_sessions:
    key TEXT PRIMARY KEY, session_id TEXT NOT NULL, last_used_at INTEGER NOT NULL, created_at INTEGER NOT NULL.
    key is the <forge>:<agent>:<repo>:<issue> shape used today.
  • infrastructure/database/sessions.ts becomes a thin DB-backed accessor (getSession, setSession, pruneStale, etc.) — same external API as today, no callers re-touched.
  • Migration 013-migrate-sessions-to-db.ts: if sessions.json exists, parse + bulk-insert (INSERT OR REPLACE) every entry, then unlink the file. Idempotent.
  • Pruning hook (any existing TTL sweep) moves to a SQL DELETE WHERE last_used_at < ?.
  • Tests: round-trip + migration fixture with a populated sessions.json.

Agent / Penpot tokens → secret table

  • Two new well-known secret names in the existing secret table (already AES-256-GCM in #SC-* land):
    - agent_token:<agent> — replaces ~/.config/claude-hooks/tokens/<agent>.
    - penpot_api_token — replaces ~/.config/claude-hooks/penpot-token.
  • Container mount code (container.ts / container-reconcile.ts) reads from the secret table and writes the plaintext to a tmpfile inside the container's tokens mount at startup (mount target unchanged so the in-container Forgejo / Penpot MCP keeps working without code change).
  • Per-agent admin endpoint (/agents/:name/token PATCH or similar) writes the secret via setSecret. The existing CLI / dashboard rotate flow targets the secret name instead of the file path.
  • Migration 014-migrate-agent-tokens-to-secrets.ts: for every existing file under ~/.config/claude-hooks/tokens/, insert the plaintext into the secret table under agent_token:<filename>, then unlink the file. Same for penpot-token. Idempotent.
  • Tests: round-trip the rotate flow; assert cat tokens/<agent> no longer required.

Cleanup

  • Remove getStateRootSessionsPath() / gitlabOAuthTokenPath() exports.
  • docs/credentials.md updated to reflect the new flow (DB-only).
  • Boot-time backups (agents.db.bak-<ts>) cover everything now — no separate JSON backup recipe.

Out of scope

  • The Claude Code SDK config dirs under ~/.config/claude-hooks/agent-env/<agent>/. Those are owned by the SDK and mounted as-is into the container. The SDK reads them by path; switching them to DB would require forking the SDK.
  • ~/.config/claude-hooks/claude-credentials/.credentials.json — Anthropic API credentials in the SDK's own format. Same reasoning.
  • architect-uploads/<id>/{blob,meta.json} — the file-attachment blobs are intentionally on disk (large binaries shouldn't live in SQLite). The sidecar JSON could move to DB later but the binary cannot.
  • Backup / restore tooling overhaul. The existing agents.db.bak-* rotation is enough.
  • Re-encrypting the secret table with a different cipher. Stays AES-256-GCM.

References

  • apps/server/src/http/handlers/oauth-gitlab.ts — the JSON write path being replaced.
  • apps/server/src/infrastructure/database/sessions.ts — current file-backed session-resume store.
  • apps/server/src/infrastructure/container/container-reconcile.ts — token-file mount source.
  • apps/server/src/infrastructure/container/container.ts — Penpot token + claude-credentials mount paths.
  • docs/credentials.md — current shape of the credential mounts (will need an update).
  • DOB-4 (#819) and DOB-5 (#820) — prior storage rips that ripped agents.json and service.json.

Suggested implementation order

  1. GitLab OAuth → DB (smallest blast radius — F3-GL is rarely re-auth'd, the other two adapters already use the DB row, schema unchanged).
  2. Claude SDK sessions → DB (medium — hot path during dispatch; needs the migration to back-fill before the first read).
  3. Agent / Penpot tokens → secret table (largest — touches container mount code + rotate flow + every per-agent runtime).

Each step should ship as its own commit / PR so a regression on one doesn't block the others, even if the issue stays single.

## User story As an operator, I want every piece of runtime state that claude-hooks itself owns to live in the SQLite DB rather than scattered JSON / plain-text files on disk, so that backups / restores / migrations are atomic, secrets are encrypted at rest, and the dashboard's Settings → Service surface is the single source of truth. Three storage holdouts remain after DOB-4 / DOB-5 ripped `service.json` and `agents.json`: 1. `~/.local/state/claude-hooks/gitlab-oauth-token.json` — the F3-GL GitLab OAuth access + refresh tokens. 2. `~/.local/state/claude-hooks/sessions.json` — the Claude SDK session-resume map (`<forge>:<agent>:<repo>:<issue>` → `{id, last_used_at}`). 3. `~/.config/claude-hooks/tokens/<agent>` + `~/.config/claude-hooks/penpot-token` — per-agent Forgejo PATs and the Penpot API token, both stored as plain-text mode-0600 files. The Claude Code SDK config dirs under `~/.config/claude-hooks/agent-env/<agent>/` (settings.json, policy-limits.json, mcp-needs-auth-cache.json, system-prompt.md, sessions/) are owned by the SDK and **stay on disk** — they're the SDK's wire format. Same for `~/.config/claude-hooks/claude-credentials/.credentials.json` (mounted into containers). ## Acceptance criteria ### GitLab OAuth → DB - [ ] `operator_oauth_tokens` already has rows for forgejo / github via `upsertOperatorOAuth`. Land the GitLab equivalent — `oauth-gitlab.ts` reads / writes the row instead of `gitlab-oauth-token.json`. - [ ] Migration `012-migrate-gitlab-oauth-to-db.ts`: if the JSON file exists and no DB row for `forge_type='gitlab'`, parse the file and insert via `upsertOperatorOAuth`. Then `unlink` the JSON file. Idempotent. - [ ] Drop `gitlabOAuthTokenPath()` + every `readFile` / `writeFile` reference. - [ ] Tests: existing `oauth-gitlab.test.ts` paths exercise DB; add a migration test seeding the JSON file then asserting the row + file removal. ### Claude SDK sessions → DB - [ ] New table `claude_sdk_sessions`: `key TEXT PRIMARY KEY, session_id TEXT NOT NULL, last_used_at INTEGER NOT NULL, created_at INTEGER NOT NULL`. `key` is the `<forge>:<agent>:<repo>:<issue>` shape used today. - [ ] `infrastructure/database/sessions.ts` becomes a thin DB-backed accessor (`getSession`, `setSession`, `pruneStale`, etc.) — same external API as today, no callers re-touched. - [ ] Migration `013-migrate-sessions-to-db.ts`: if `sessions.json` exists, parse + bulk-insert (`INSERT OR REPLACE`) every entry, then `unlink` the file. Idempotent. - [ ] Pruning hook (any existing TTL sweep) moves to a SQL `DELETE WHERE last_used_at < ?`. - [ ] Tests: round-trip + migration fixture with a populated `sessions.json`. ### Agent / Penpot tokens → `secret` table - [ ] Two new well-known secret names in the existing `secret` table (already AES-256-GCM in #SC-* land): - `agent_token:<agent>` — replaces `~/.config/claude-hooks/tokens/<agent>`. - `penpot_api_token` — replaces `~/.config/claude-hooks/penpot-token`. - [ ] Container mount code (`container.ts` / `container-reconcile.ts`) reads from the secret table and writes the plaintext to a tmpfile inside the container's tokens mount at startup (mount target unchanged so the in-container Forgejo / Penpot MCP keeps working without code change). - [ ] Per-agent admin endpoint (`/agents/:name/token` PATCH or similar) writes the secret via `setSecret`. The existing CLI / dashboard rotate flow targets the secret name instead of the file path. - [ ] Migration `014-migrate-agent-tokens-to-secrets.ts`: for every existing file under `~/.config/claude-hooks/tokens/`, insert the plaintext into the secret table under `agent_token:<filename>`, then `unlink` the file. Same for `penpot-token`. Idempotent. - [ ] Tests: round-trip the rotate flow; assert `cat tokens/<agent>` no longer required. ### Cleanup - [ ] Remove `getStateRootSessionsPath()` / `gitlabOAuthTokenPath()` exports. - [ ] `docs/credentials.md` updated to reflect the new flow (DB-only). - [ ] Boot-time backups (`agents.db.bak-<ts>`) cover everything now — no separate JSON backup recipe. ## Out of scope - The Claude Code SDK config dirs under `~/.config/claude-hooks/agent-env/<agent>/`. Those are owned by the SDK and mounted as-is into the container. The SDK reads them by path; switching them to DB would require forking the SDK. - `~/.config/claude-hooks/claude-credentials/.credentials.json` — Anthropic API credentials in the SDK's own format. Same reasoning. - `architect-uploads/<id>/{blob,meta.json}` — the file-attachment blobs are intentionally on disk (large binaries shouldn't live in SQLite). The sidecar JSON could move to DB later but the binary cannot. - Backup / restore tooling overhaul. The existing `agents.db.bak-*` rotation is enough. - Re-encrypting the secret table with a different cipher. Stays AES-256-GCM. ## References - `apps/server/src/http/handlers/oauth-gitlab.ts` — the JSON write path being replaced. - `apps/server/src/infrastructure/database/sessions.ts` — current file-backed session-resume store. - `apps/server/src/infrastructure/container/container-reconcile.ts` — token-file mount source. - `apps/server/src/infrastructure/container/container.ts` — Penpot token + claude-credentials mount paths. - `docs/credentials.md` — current shape of the credential mounts (will need an update). - DOB-4 (#819) and DOB-5 (#820) — prior storage rips that ripped `agents.json` and `service.json`. ## Suggested implementation order 1. **GitLab OAuth → DB** (smallest blast radius — F3-GL is rarely re-auth'd, the other two adapters already use the DB row, schema unchanged). 2. **Claude SDK sessions → DB** (medium — hot path during dispatch; needs the migration to back-fill before the first read). 3. **Agent / Penpot tokens → `secret` table** (largest — touches container mount code + rotate flow + every per-agent runtime). Each step should ship as its own commit / PR so a regression on one doesn't block the others, even if the issue stays single.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks#823
No description provided.