Agents: per-instance container reconciliation (create/remove on CRUD) #52
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#52
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 each agent instance to have its own long-lived Docker container and state volume, reconciled against the SQLite
agentstable on startup and on CRUD mutations, so that creatingdev-frontendfrom the dashboard (A6) spins upclaude-hooks-dev-frontendwith its own/statevolume — and deleting it tears everything down without leaving orphans.Context
Today three containers are hardcoded in the systemd wrapper:
claude-hooks-boss,claude-hooks-dev,claude-hooks-reviewer. With A1 landed, the set of containers is dynamic: one per SQLiteagentsrow, namedclaude-hooks-<instance-name>.Reconciliation happens at:
docker ps, create missing, stop+remove extras.V1 scope: reconcile via shelling out to
docker run/docker rm. Later we can evaluate Docker's Go / SDK if the surface grows.Acceptance criteria
Container naming & volume
claude-hooks-<instance-name>.claude-hooks-<instance-name>-state(mounted at/statein-container).~/.config/claude-hooks/tokens/<type>— shared across instances of the same type per the milestone's non-goal on per-agent tokens.agents.json).Reconciliation
src/container-reconcile.ts(or extendsrc/container.ts):reconcileAll(): Promise<{created: string[], removed: string[], unchanged: string[]}>— compares SQLite agents ↔ running containers.reconcileOne(name): Promise<"created" | "removed" | "unchanged">— idempotent for a single instance.reconcileAll()afterloadWebhookConfig(), beforestartSweeper(). Log the outcome as one line per class (created / removed / unchanged).reconcileOne(name)after the SQLite mutation commits. A6 does not calldockerdirectly; it always goes through this module.Lifecycle rules
docker run -d --name <container> --restart unless-stopped -v <volume>:/state -v <creds>:/home/claude/.config/claude-code/.credentials.json:ro --env-file <tmpenv> <image>. Same as today'sjust containers-rebuildpath.docker stop(15s grace) thendocker rm. The state volume persists unless the operator passes--wipevia a separate CLI command (not in scope).Systemd + justfile
ExecStartPrefromjust containers-uptojust agents-sync(new recipe) that invokes the same reconcileAll path the service runs on startup — so pre-service-start has a consistent view.README.md: creating a new instance from the dashboard triggersreconcileOneautomatically;just agents-syncis the manual fallback.Tests
reconcileAllwith fake docker runner: added row → create called; missing row for running container → remove called; matching → no-op.reconcileOnewith a non-existent name → returns"unchanged", no docker calls.Out of scope
just containers-rebuildrecipe handles that separately; reconcile assumes image is local or pullable).unless-stopped.just wipe-agent <name>recipe, separate story if we decide we need it.References
justfilerecipescontainers-up,containers-down,containers-rebuild.src/container.ts.Dependencies
main(after A1 lands).