Move final JSON / plain-text state to DB (gitlab token, SDK sessions, agent secrets) #823
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks#823
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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.jsonandagents.json:~/.local/state/claude-hooks/gitlab-oauth-token.json— the F3-GL GitLab OAuth access + refresh tokens.~/.local/state/claude-hooks/sessions.json— the Claude SDK session-resume map (<forge>:<agent>:<repo>:<issue>→{id, last_used_at}).~/.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_tokensalready has rows for forgejo / github viaupsertOperatorOAuth. Land the GitLab equivalent —oauth-gitlab.tsreads / writes the row instead ofgitlab-oauth-token.json.012-migrate-gitlab-oauth-to-db.ts: if the JSON file exists and no DB row forforge_type='gitlab', parse the file and insert viaupsertOperatorOAuth. Thenunlinkthe JSON file. Idempotent.gitlabOAuthTokenPath()+ everyreadFile/writeFilereference.oauth-gitlab.test.tspaths exercise DB; add a migration test seeding the JSON file then asserting the row + file removal.Claude SDK sessions → DB
claude_sdk_sessions:key TEXT PRIMARY KEY, session_id TEXT NOT NULL, last_used_at INTEGER NOT NULL, created_at INTEGER NOT NULL.keyis the<forge>:<agent>:<repo>:<issue>shape used today.infrastructure/database/sessions.tsbecomes a thin DB-backed accessor (getSession,setSession,pruneStale, etc.) — same external API as today, no callers re-touched.013-migrate-sessions-to-db.ts: ifsessions.jsonexists, parse + bulk-insert (INSERT OR REPLACE) every entry, thenunlinkthe file. Idempotent.DELETE WHERE last_used_at < ?.sessions.json.Agent / Penpot tokens →
secrettablesecrettable (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.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)./agents/:name/tokenPATCH or similar) writes the secret viasetSecret. The existing CLI / dashboard rotate flow targets the secret name instead of the file path.014-migrate-agent-tokens-to-secrets.ts: for every existing file under~/.config/claude-hooks/tokens/, insert the plaintext into the secret table underagent_token:<filename>, thenunlinkthe file. Same forpenpot-token. Idempotent.cat tokens/<agent>no longer required.Cleanup
getStateRootSessionsPath()/gitlabOAuthTokenPath()exports.docs/credentials.mdupdated to reflect the new flow (DB-only).agents.db.bak-<ts>) cover everything now — no separate JSON backup recipe.Out of scope
~/.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.agents.db.bak-*rotation is enough.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).agents.jsonandservice.json.Suggested implementation order
secrettable (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.