Files
nanoclaw/CLAUDE.md
T
gavrielc 86becf8dea chore: delete v1 reference code
Removes src/v1/ (37 files) and container/agent-runner/src/v1/ (3 files)
along with the v1 reference note in CLAUDE.md and the now-obsolete
tsconfig exclude. v1 was already out of the runtime path; this just
removes the dead weight.

~8,800 LOC removed, zero runtime change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:23:47 +03:00

14 KiB

NanoClaw

Personal Claude assistant. See README.md for philosophy and setup. Architecture lives in docs/.

Quick Context

The host is a single Node process that orchestrates per-session agent containers. Platform messages land via channel adapters, route through an entity model (users → messaging groups → agent groups → sessions), get written into the session's inbound DB, and wake a container. The agent-runner inside the container polls the DB, calls Claude, and writes back to the outbound DB. The host polls the outbound DB and delivers through the same adapter.

Everything is a message. There is no IPC, no file watcher, no stdin piping between host and container. The two session DBs are the sole IO surface.

Entity Model

users (id "<channel>:<handle>", kind, display_name)
user_roles (user_id, role, agent_group_id)       — owner | admin (global or scoped)
agent_group_members (user_id, agent_group_id)    — unprivileged access gate
user_dms (user_id, channel_type, messaging_group_id) — cold-DM cache

agent_groups (workspace, memory, CLAUDE.md, personality, container config)
    ↕ many-to-many via messaging_group_agents (session_mode, trigger_rules, priority)
messaging_groups (one chat/channel on one platform; unknown_sender_policy)

sessions (agent_group_id + messaging_group_id + thread_id → per-session container)

Privilege is user-level (owner/admin), not agent-group-level. See docs/isolation-model.md for the three isolation levels (agent-shared, shared, separate agents).

Two-DB Session Split

Each session has two SQLite files under data/v2-sessions/<session_id>/:

  • inbound.db — host writes, container reads. messages_in, routing, destinations, pending_questions, processing_ack.
  • outbound.db — container writes, host reads. messages_out, session_state.

Exactly one writer per file — no cross-mount lock contention. Heartbeat is a file touch at /workspace/.heartbeat, not a DB update. Host uses even seq numbers, container uses odd.

Central DB

data/v2.db holds everything that isn't per-session: users, user_roles, agent_groups, messaging_groups, wiring, pending_approvals, user_dms, chat_sdk_* (for the Chat SDK bridge), schema_version. Migrations live at src/db/migrations/.

Key Files

File Purpose
src/index.ts Entry point: init DB, migrations, channel adapters, delivery polls, sweep, shutdown
src/router.ts Inbound routing: messaging group → agent group → session → inbound.db → wake
src/delivery.ts Polls outbound.db, delivers via adapter, handles system actions (schedule, approvals, etc.)
src/host-sweep.ts 60s sweep: processing_ack sync, stale detection, due-message wake, recurrence
src/session-manager.ts Resolves sessions; opens inbound.db / outbound.db; manages heartbeat path
src/container-runner.ts Spawns per-agent-group Docker containers with session DB + outbox mounts, OneCLI ensureAgent
src/container-runtime.ts Runtime selection (Docker vs Apple containers), orphan cleanup
src/access.ts pickApprover, pickApprovalDelivery, admin resolution for NANOCLAW_ADMIN_USER_IDS
src/onecli-approvals.ts OneCLI credentialed-action approval bridge
src/user-dm.ts Cold-DM resolution + user_dms cache
src/group-init.ts Per-agent-group filesystem scaffold (CLAUDE.md, skills, agent-runner-src overlay)
src/db/ DB layer — agent_groups, messaging_groups, sessions, user_roles, user_dms, pending_*, migrations
src/channels/ Channel adapter infra (registry, Chat SDK bridge); specific channel adapters are skill-installed from the channels branch
src/providers/ Host-side provider container-config (claude baked in; opencode etc. installed from the providers branch)
container/agent-runner/src/ Agent-runner: poll loop, formatter, provider abstraction, MCP tools, destinations
container/skills/ Container skills mounted into every agent session
groups/<folder>/ Per-agent-group filesystem (CLAUDE.md, skills, per-group agent-runner-src/ overlay)
scripts/init-first-agent.ts Bootstrap the first DM-wired agent (used by /init-first-agent skill)

Channels and Providers (skill-installed)

Trunk does not ship any specific channel adapter or non-default agent provider. The codebase is the registry/infra; the actual adapters and providers live on long-lived sibling branches and get copied in by skills:

  • channels branch — Discord, Slack, Telegram, WhatsApp, Teams, Linear, GitHub, iMessage, Webex, Resend, Matrix, Google Chat, WhatsApp Cloud (+ helpers, tests, channel-specific setup steps). Installed via /add-<channel> skills.
  • providers branch — OpenCode (and any future non-default agent providers). Installed via /add-opencode.

Each /add-<name> skill is idempotent: git fetch origin <branch> → copy module(s) into the standard paths → append a self-registration import to the relevant barrel → pnpm install <pkg>@<pinned-version> → build.

Self-Modification

One tier of agent self-modification today:

  1. install_packages / add_mcp_server / request_rebuild — changes to the per-agent-group container config only (apt/npm deps, wire an existing MCP server). Admin approval, rebuild, container restart. container/agent-runner/src/mcp-tools/self-mod.ts.

A second tier (direct source-level self-edits via a draft/activate flow) is planned but not yet implemented.

Secrets / Credentials / OneCLI

API keys, OAuth tokens, and auth credentials are managed by the OneCLI gateway. Secrets are injected into per-agent containers at request time — none are passed in env vars or through chat context. src/onecli-approvals.ts, ensureAgent() in container-runner.ts. Run onecli --help.

Skills

Four types of skills. See CONTRIBUTING.md for the full taxonomy.

  • Channel/provider install skills — copy the relevant module(s) in from the channels or providers branch, wire imports, install pinned deps (e.g. /add-discord, /add-slack, /add-whatsapp, /add-opencode).
  • Utility skills — ship code files alongside SKILL.md (e.g. /claw).
  • Operational skills — instruction-only workflows (/setup, /debug, /customize, /init-first-agent, /manage-channels, /init-onecli, /update-nanoclaw).
  • Container skills — loaded inside agent containers at runtime (container/skills/: welcome, self-customize, agent-browser, slack-formatting).
Skill When to Use
/setup First-time install, auth, service config
/init-first-agent Bootstrap the first DM-wired agent (channel pick → identity → wire → welcome DM)
/manage-channels Wire channels to agent groups with isolation level decisions
/customize Adding channels, integrations, behavior changes
/debug Container issues, logs, troubleshooting
/update-nanoclaw Bring upstream updates into a customized install
/init-onecli Install OneCLI Agent Vault and migrate .env credentials

Contributing

Before creating a PR, adding a skill, or preparing any contribution, you MUST read CONTRIBUTING.md. It covers accepted change types, the four skill types and their guidelines, SKILL.md format rules, and the pre-submission checklist.

Development

Run commands directly — don't tell the user to run them.

# Host (Node + pnpm)
pnpm run dev          # Host with hot reload
pnpm run build        # Compile host TypeScript (src/)
./container/build.sh  # Rebuild agent container image (nanoclaw-agent:latest)
pnpm test             # Host tests (vitest)

# Agent-runner (Bun — separate package tree under container/agent-runner/)
cd container/agent-runner && bun install   # After editing agent-runner deps
cd container/agent-runner && bun test      # Container tests (bun:test)

Container typecheck is a separate tsconfig — if you edit container/agent-runner/src/, run pnpm exec tsc -p container/agent-runner/tsconfig.json --noEmit from root (or bun run typecheck from container/agent-runner/).

Service management:

# macOS (launchd)
launchctl load   ~/Library/LaunchAgents/com.nanoclaw.plist
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
launchctl kickstart -k gui/$(id -u)/com.nanoclaw  # restart

# Linux (systemd)
systemctl --user start|stop|restart nanoclaw

Host logs: logs/nanoclaw.log (normal) and logs/nanoclaw.error.log (errors only — some delivery/approval failures only show up here).

Supply Chain Security (pnpm)

This project uses pnpm with minimumReleaseAge: 4320 (3 days) in pnpm-workspace.yaml. New package versions must exist on the npm registry for 3 days before pnpm will resolve them.

Rules — do not bypass without explicit human approval:

  • minimumReleaseAgeExclude: Never add entries without human sign-off. If a package must bypass the release age gate, the human must approve and the entry must pin the exact version being excluded (e.g. package@1.2.3), never a range.
  • onlyBuiltDependencies: Never add packages to this list without human approval — build scripts execute arbitrary code during install.
  • pnpm install --frozen-lockfile should be used in CI, automation, and container builds. Never run bare pnpm install in those contexts.

Docs Index

Doc Purpose
docs/architecture.md Full architecture writeup
docs/api-details.md Host API + DB schema details
docs/db.md DB architecture overview: three-DB model, cross-mount rules, readers/writers map
docs/db-central.md Central DB (data/v2.db) — every table + migration system
docs/db-session.md Per-session inbound.db + outbound.db schemas + seq parity
docs/agent-runner-details.md Agent-runner internals + MCP tool interface
docs/isolation-model.md Three-level channel isolation model
docs/setup-wiring.md What's wired, what's open in the setup flow
docs/checklist.md Rolling status checklist across all subsystems
docs/architecture-diagram.md Diagram version of the architecture
docs/build-and-runtime.md Runtime split (Node host + Bun container), lockfiles, image build surface, CI, key invariants

Container Build Cache

The container buildkit caches the build context aggressively. --no-cache alone does NOT invalidate COPY steps — the builder's volume retains stale files. To force a truly clean rebuild, prune the builder then re-run ./container/build.sh.

Container Runtime (Bun)

The agent container runs on Bun; the host runs on Node (pnpm). They communicate only via session DBs — no shared modules. Details and rationale: docs/build-and-runtime.md.

Gotchas — trigger + action:

  • Adding or bumping a runtime dep in container/agent-runner/ → edit package.json, then cd container/agent-runner && bun install and commit the updated bun.lock. Do not run pnpm install there — agent-runner is not a pnpm workspace.
  • Bumping @anthropic-ai/claude-agent-sdk, @modelcontextprotocol/sdk, or any agent-runner runtime dep → no minimumReleaseAge policy applies to this tree. Check the release date on npm, pin deliberately, never bun update blindly.
  • Writing a new named-param SQL insert/update in the container → use $name in both SQL and JS keys: .run({ $id: msg.id }). bun:sqlite does not auto-strip the prefix the way better-sqlite3 does on the host. Positional ? params work normally.
  • Adding a test in container/agent-runner/src/ → import from bun:test, not vitest. Vitest runs on Node and can't load bun:sqlite. vitest.config.ts excludes this tree.
  • Adding a Node CLI the agent invokes at runtime (like agent-browser, claude-code, vercel) → put it in the Dockerfile's pnpm global-install block, pinned to an exact version via a new ARG. Don't use bun install -g — that bypasses the pnpm supply-chain policy.
  • Changing the Dockerfile entrypoint or the dynamic-spawn command (src/container-runner.ts line ~301) → keep exec bun ... so signals forward cleanly. The image has no /app/dist; don't reintroduce a tsc build step.
  • Changing session-DB pragmas (container/agent-runner/src/db/connection.ts) → journal_mode=DELETE is load-bearing for cross-mount visibility. Read the comment block at the top of the file first.

CJK font support

Agent containers ship without CJK fonts by default (~200MB saved). If you notice signals the user works with Chinese/Japanese/Korean content — conversing in CJK, CJK timezone (e.g., Asia/Tokyo, Asia/Shanghai, Asia/Seoul, Asia/Taipei, Asia/Hong_Kong), system locale hint, or mentions of needing to render CJK in screenshots/PDFs/scraped pages — offer to enable it:

# Ensure .env has INSTALL_CJK_FONTS=true (overwrite or append)
grep -q '^INSTALL_CJK_FONTS=' .env && sed -i.bak 's/^INSTALL_CJK_FONTS=.*/INSTALL_CJK_FONTS=true/' .env && rm -f .env.bak || echo 'INSTALL_CJK_FONTS=true' >> .env

# Rebuild and restart so new sessions pick up the new image
./container/build.sh
launchctl kickstart -k gui/$(id -u)/com.nanoclaw   # macOS
# systemctl --user restart nanoclaw                # Linux

container/build.sh reads INSTALL_CJK_FONTS from .env and passes it through as a Docker build-arg. Without CJK fonts, Chromium-rendered screenshots and PDFs containing CJK text show tofu (empty rectangles) instead of characters.