Skip to content

feat(db): move container config from filesystem to DB#2351

Merged
gavrielc merged 13 commits into
mainfrom
feat/container-config-to-db
May 9, 2026
Merged

feat(db): move container config from filesystem to DB#2351
gavrielc merged 13 commits into
mainfrom
feat/container-config-to-db

Conversation

@gavrielc
Copy link
Copy Markdown
Collaborator

@gavrielc gavrielc commented May 8, 2026

Summary

What changed

  • New container_configs table with scalar columns (provider, model, effort, image_tag, assistant_name, max_messages_per_prompt) and JSON columns (mcp_servers, packages_apt, packages_npm, skills, additional_mounts)
  • Startup backfill seeds DB from existing container.json files on first run (idempotent)
  • materializeContainerJson() replaces readContainerConfig + ensureRuntimeFields — reads DB, writes file at spawn time
  • Self-mod handlers (install_packages, add_mcp_server) write to DB instead of file
  • Provider cascade simplified from 3-step (session → agent_groups.agent_provider → container.json → claude) to 2-step (session → container_configs.provider → claude)
  • ncl groups config-* custom operations: config-get, config-update, config-add-mcp-server, config-remove-mcp-server, config-add-package, config-remove-package
  • restartAgentGroupContainers() helper extracted for config-change propagation

Why

Container config was split-brained: provider lived in both the DB and the file with a cascade nobody used, self-mod wrote to the file, ncl wrote to the DB, and skills told users to edit the file directly. Adding model/effort (PRs #2233, #2280) would have made this worse. Single source of truth in the DB eliminates the split, makes config queryable (SELECT * FROM container_configs WHERE model = 'claude-opus-4-6'), and gives operators ncl commands for all config fields.

Test plan

  • 328 tests pass (including new migration running in every test DB)
  • resolveProviderName tests updated for 2-param cascade
  • TypeScript compiles clean (only pre-existing telegram error)
  • Manual: start service with existing container.json files, verify backfill populates container_configs
  • Manual: ncl groups config-get --id <id> shows correct config
  • Manual: ncl groups config-update --id <id> --model claude-sonnet-4-6 restarts container
  • Manual: install_packages / add_mcp_server approval flow writes to DB

🤖 Generated with Claude Code

@gavrielc gavrielc requested a review from gabi-simons as a code owner May 8, 2026 19:26
Source of truth for container runtime config moves from
groups/<folder>/container.json to a new container_configs table.
The file becomes a materialized view written at spawn time.

- New container_configs table with scalar columns (provider, model,
  effort, image_tag, assistant_name, max_messages_per_prompt) and
  JSON columns (mcp_servers, packages_apt, packages_npm, skills,
  additional_mounts)
- Startup backfill seeds DB from existing container.json files
- materializeContainerJson() replaces readContainerConfig + ensureRuntimeFields
- Self-mod handlers (install_packages, add_mcp_server) write to DB
- Provider cascade simplified: session -> container_configs -> 'claude'
- ncl groups config-{get,update,add-mcp-server,remove-mcp-server,
  add-package,remove-package} custom operations
- restartAgentGroupContainers() helper for config change propagation
- Container side unchanged (still reads /workspace/agent/container.json)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mintlify
Copy link
Copy Markdown

mintlify Bot commented May 8, 2026

Docs PR opened: glifocat/nanoclaw-docs#286

Updated nine docs pages to reflect the move of container config from groups/<folder>/container.json to the new container_configs DB table.

gavrielc and others added 2 commits May 8, 2026 22:33
- config-add/remove-package now rebuild image + restart containers
- Deduplicate packages in self-mod install_packages handler
- Add runtime whitelist guards for SQL column interpolation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gavrielc and others added 2 commits May 9, 2026 12:04
`ncl groups config get` now works alongside `ncl groups config-get`.
Parser joins all positionals with dashes; dispatcher falls back by
trimming the last segment as a target ID (`ncl groups get abc123`).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Operation keys like 'config get' read naturally and crud.ts normalizes
spaces to dashes for the registry name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gavrielc gavrielc force-pushed the feat/container-config-to-db branch from de191b3 to 37b5496 Compare May 9, 2026 09:07
gavrielc and others added 2 commits May 9, 2026 12:08
Provider is now managed via `ncl groups config update --provider`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
config add/remove-package should only update the DB and restart.
Image rebuild is handled by the self-mod approval flow or manually.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gavrielc gavrielc force-pushed the feat/container-config-to-db branch from 8c38c1e to 08698da Compare May 9, 2026 09:10
gavrielc and others added 6 commits May 9, 2026 19:02
Decouple container restart from config updates — config CLI ops now only
write to the DB; restart is a separate `ncl groups restart` command with
--rebuild and --message flags. Add on_wake column to messages_in so wake
messages are only picked up by a fresh container's first poll, preventing
dying containers from stealing them during the SIGTERM grace window.
killContainer accepts an onExit callback for race-free respawn. Agent-
called restart auto-scopes to the calling session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add cli_scope column to container_configs with three levels:
- disabled: agent never learns about ncl (instructions excluded from
  CLAUDE.md) and host dispatch rejects any cli_request
- group (default): agent can only access groups, sessions, destinations,
  and members resources, scoped to its own agent group with auto-filled
  --id/--agent_group_id/--group args. Help output reflects the scope.
- global: unrestricted access (current behavior)

Enforcement is host-side only — no image rebuild or env var needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When init-first-agent creates an agent group for an owner, set
cli_scope to 'global' so the owner's personal agent has full ncl
access. All other agent groups remain 'group'-scoped by default.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Group-scoped agents could previously:
- See all agent groups via `groups list` (generic list skips --id filter)
- Look up any session by UUID via `sessions get`
- Request cli_scope change to global via config update approval

Fixed by:
- Post-handler filtering: list results filtered, get results verified
  against caller's agent_group_id
- Pre-handler --id check scoped to resources where id IS the group ID
  (groups, destinations) so session UUIDs aren't falsely rejected
- cli_scope/cli-scope args blocked outright for group-scoped agents,
  before the approval gate

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CLAUDE.md: new key files, updated groups verbs, rewritten self-mod
  section, new Container Config and Container Restart sections
- db-central.md: container_configs table (§1.15), migrations 014+015
- db-session.md: messages_in schema with trigger, source_session_id,
  on_wake columns
- schema.ts: comment no longer references disk-based config
- cli.instructions.md: rewritten for scope-aware usage, auto-fill,
  restart/config ops, group-scoped examples

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant