Make the agent provider a first-class, operator-chosen property instead of a
Claude-only assumption. Trunk gains the seams; the actual non-default payloads
(Codex first) install from the `providers` branch.
Setup
- A provider registry feeds a hard-wired setup picker (Claude | Codex). Picking
a non-default provider installs its payload (setup/add-codex.sh, channel-style),
runs a vault-only auth walkthrough (--step provider-auth), and records the pick
on the first agent before its first spawn.
- Picking Claude changes nothing — default installs are byte-for-byte unaffected.
Provider as a DB property
- Provider lives on container_configs.provider (materialized to container.json,
read by resolveProviderName). Creation stays provider-agnostic; the picked
provider is applied via the picked-provider seam. The deprecated
agent_groups.agent_provider path is not used.
Switching + memory
- Switch a live group with `ncl groups config update --provider` + restart.
- Memory never migrates at runtime — each provider keeps its own store. The
/migrate-memory skill carries a group's memory across a switch in either
direction (flat CLAUDE.local.md <-> memory/ scaffold). group-init seeds an
imported-agent-memory note for non-default providers; the runner's memory
definition reads it first turn. See docs/provider-migration.md.
No install-wide default, no runtime provider guard — switching is operator-by-
convention, consistent with the no-install-gating posture.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Injects credentials as request-time stubs so no credential is ever written
into a container or to disk. Gateway and CLI versions move to versions.json
(machine-checkable pins); breaking upgrades are documented in
docs/onecli-upgrades.md as an agent-executable runbook (detect / why / fix /
verify / rollback), and the update flow follows linked docs and diffs the
pins.
BREAKING: requires a gateway upgrade; the doc carries the steps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the standalone bash uninstall.sh with a TypeScript flow inside the
setup driver (setup/uninstall/): scan (slug-scoped inventory), plan (pure
ordered removal actions), remove (per-action executor that absorbs failures
into notes), and flow (clack UI). uninstall.sh is now a 3-line pointer that
execs nanoclaw.sh --uninstall.
- nanoclaw.sh --uninstall short-circuits before diagnostics/bootstrap; with
no node_modules it prints manual cleanup commands and exits 1
- setup:auto routes --uninstall before initProgressionLog so an uninstall
never resets logs/setup.log
- fresh setup runs detect an existing install (service registration or
data/v2.db) and offer keep-and-continue (default) or uninstall-and-exit;
suppressed on fail()-retry and sg re-exec resumes
- self-deletion safety: static imports only, dist/ + node_modules/ removed
dead last, nothing but console.log after the runtime tail
- --yes never deletes orphan ag-* vault agents; their manual delete
commands (by vault uuid) are printed instead
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CONTRIBUTING still described feature skills as installed by merging a
skill/* branch, a design the shipped skills no longer use: /add-slack,
/add-telegram and the rest install by additive fetch from the channels
and providers registry branches (git fetch + git show per file), with
registration tests and a REMOVE.md. Rewrite the skill-type section to
match, point the authoring bar at docs/skill-guidelines.md, fix the
README FAQ line that sent every contribution to the registry branches,
and delete docs/skills-as-branches.md (the superseded merge-based
design, including a marketplace flow that was never the shipped path).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three public docs establishing the skills-based customization contract:
- docs/customizing.md: the short doorway. The problem (merge fights on
update), the idea (every change is a skill), how to work (edit first,
skillify after), the one rule (/update-nanoclaw, never raw git pull),
and the two-sided deal.
- docs/skills-model.md: the full model. Recipes, skill anatomy, the
two kinds of skills, registry branches (additive fetch, never merge),
a test for every integration point, upgrading, migrations and the
startup tripwire, the maintainer commitments, and the registry
review rule.
- docs/skill-guidelines.md: the authoritative checklist for writing a
skill. Two principles (minimal integration surface; a test per
functional integration point), anatomy, change shapes, testing
doctrine with archetypes, anti-patterns, worked examples.
Also: CLAUDE.md docs index rows for the three docs, and .gitignore
entries for local-only working artifacts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Place agent containers on a Docker `--internal` network (no internet route)
with the OneCLI gateway attached, aliased host.docker.internal. The injected
proxy URL resolves only to the gateway, so a non-proxy-aware client or raw
socket has nowhere to go — closing the HTTPS_PROXY-bypass hole. The agent is
non-root with no NET_ADMIN, so it cannot undo this. Self-healing: the gateway
is re-attached at every spawn and on each host-sweep tick.
Fail-fast: when lockdown is enabled but the network/gateway can't be
established, refuse to spawn and surface a clear EgressLockdownError rather
than silently falling back to open egress. The host-sweep re-heal is the lone
exception — a heal failure there is logged, not fatal, since running agents
stay on the internal net (no leak) until the gateway returns.
Off by default — opt in with NANOCLAW_EGRESS_LOCKDOWN=true (so OSS users get
the prior behavior unchanged on pull). Also NANOCLAW_EGRESS_NETWORK and
ONECLI_GATEWAY_CONTAINER.
The lockdown logic lives in its own src/egress-lockdown.ts; container-runtime.ts
keeps only the generic runtime surface. Documented in docs/SECURITY.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Claude Agent SDK adds a per-request cch=<hash> to the front of every
prompt; it changes each turn, and Ollama's prompt cache only reuses a
prompt whose start is unchanged, so it re-reads the whole prompt every
time (slow). A tiny proxy filters the hash out (pins cch to a constant) so
caching kicks in. In our setup (31B on Apple Silicon) follow-up replies
went ~80s -> ~4s; numbers vary by model/hardware. Ollama ignores the hash,
so output is unchanged.
Scope: only the Claude-Code-CLI -> Ollama path; Codex/OpenCode emit no cch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
claw, x-integration, add-parallel, and convert-to-apple-container target
removed v1 architecture (v1 DB schema, file-IPC) or install via a forbidden
branch-merge of a stale branch — they can't be made conformant and are retired.
Cleans up the references to them in README.md, docs/SPEC.md, CONTRIBUTING.md,
and CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refuse to start unless this install reached the current version through a
sanctioned path (setup / update / migrate). A raw `git pull` that skips
migrations now fails loudly with a self-healing message instead of
silently breaking.
- src/upgrade-state.ts: marker at data/upgrade-state.json, getCodeVersion,
isUpgradeCurrent, enforceUpgradeTripwire (fails closed on missing /
corrupt / mismatched marker)
- src/index.ts: gate wired in at startup step 0.5, before DB init
- scripts/upgrade-state.ts: get/set CLI (also the override / recovery cmd)
- setup/service.ts, /update-nanoclaw, /migrate-nanoclaw: stamp on success;
update/migrate also self-update their own skill first
- CHANGELOG [BREAKING] entry bridges existing installs via the skills'
breaking-change check
- docs/upgrade-recovery.md: clearing the tripwire
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sweep of outbound strings, doc URLs, comments, and clone instructions
that were missed in the original org rename. One both-match case in
setup/lib/channels-remote.sh (URL detection) accepts either name so
existing forks with a `qwibitai` remote continue to resolve cleanly;
everywhere else is a straight rename.
Historical mentions left intact:
- CHANGELOG.md (v2.0.0 entry, frozen history)
- .claude/skills/add-gmail-tool/SKILL.md (pre-v2 qwibitai skill — historical)
- repo-tokens/badge.svg (auto-regenerated by update-tokens.yml)
Extracted the helpers we use (JID parsing, trigger mapping, channel
auth registry, generateId, v2PlatformId) into setup/migrate-v2/shared.ts.
Deleted setup/migrate-v1/ entirely — no code references it anymore.
Updated README, CLAUDE.md, docs/v1-to-v2-changes.md, and
docs/migration-dev.md to reference the new paths and migrate-v2.sh
entry point.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New entry point: `bash migrate-v2.sh` from the v2 checkout.
Replaces the old setup-embedded migration flow with a standalone
4-phase script + rewritten Claude skill for the interactive parts.
Phase 0: Bootstrap (Node/pnpm/deps via setup.sh) + find v1
Phase 1: Core state (env, DB, groups, sessions, tasks)
Phase 2: Channels (clack multiselect, auth copy, code install)
Phase 3: Infrastructure (OneCLI, auth, Docker, skills, container build)
Service switchover: stop v1 → start v2 → test → keep or revert
Phase 4: Handoff → exec claude "/migrate-from-v1"
The skill handles: owner seeding, access policy, CLAUDE.local.md
cleanup, container config validation, fork customization porting.
Key fixes found during testing:
- triggerToEngage: requires_trigger=0 must override non-empty pattern
- unknown_sender_policy defaults to 'public' (strict drops all msgs
before owner is seeded)
- Service revert must stop v2 (parse unit name from step log, not
early tsx one-liner that can fail)
- Session continuity: copy JSONL from -workspace-group/ to
-workspace-agent/ and write continuation:claude into outbound.db
- container_config.additionalMounts written directly to container.json
(same shape in v1 and v2)
- EXIT trap writes handoff.json; explicit write_handoff before exec
Includes migrate-v2-reset.sh for dev iteration and docs/migration-dev.md
for testing/debugging reference.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`bash nanoclaw.sh` detects a v1 install before channel pairing and does a
best-effort automated port of operationally important state. Hands off to
a new `/migrate-from-v1` skill for owner seeding and fork customizations.
Between the timezone and channel steps, `setup/auto.ts` calls
`runMigrateV1()` which orchestrates these registered sub-steps (each a
separate entry in the progression log with its own raw log + status
block — failures never abort the chain):
- **migrate-detect** — scans siblings of the v2 checkout + common $HOME
locations; `$NANOCLAW_V1_PATH` overrides authoritatively. Relaxed
`package.json` check lets forks + partial installs still match; DB
presence is the strongest signal.
- **migrate-validate** — asserts v1 DB shape (tables + required
columns); writes `schema-mismatch.json` on failure. Subsequent steps
short-circuit their DB-dependent parts but still run.
- **migrate-db** — seeds `agent_groups` + `messaging_groups` +
`messaging_group_agents` from v1's `registered_groups`. JID
decomposition (`dc:123` → `channel_type='discord'`,
`platform_id='discord:123'`); `trigger_pattern` + `requires_trigger`
→ `engage_mode` + `engage_pattern` (mirrors migration 010 backfill).
Users + user_roles are NOT seeded — the skill does that with an owner
interview. Idempotent: existing rows reused, not duplicated.
- **migrate-groups** — rsync group folders. v1 `CLAUDE.md` → v2
`CLAUDE.local.md` (v2 composes `CLAUDE.md` at container spawn); v1
`container_config` JSON → `.v1-container-config.json` sidecar for the
skill to translate. Tight v1-pattern scan (`/workspace/ipc/tasks`,
`store/messages.db`, `[PR_CONTEXT:`, etc.) flags files referencing
v1-specific infrastructure — content is NOT modified, just flagged in
the handoff.
- **migrate-env** — merges v1 `.env` into v2 `.env`, never overwriting
existing v2 keys.
- **migrate-channel-auth** — per-channel registry tracks v1 env keys,
v2 required keys (with source-of-key instructions — e.g. Discord
needs `DISCORD_PUBLIC_KEY` which v1 never stored), and candidate
on-disk auth state paths (Baileys keystore, matrix sync state,
etc.). Missing required v2 keys surface as actionable followups and
flip the step to `partial`.
- **migrate-channels** — runs `setup/install-<channel>.sh` for each
detected channel in non-interactive mode. Install-script output is
captured to `logs/setup-migration/install-<channel>.log` sidecars
(silent under the parent spinner). Channels with no v2 adapter get
a `not_supported` followup but don't degrade status.
- **migrate-tasks** — v1 `scheduled_tasks` → `messages_in` rows with
`kind='task'` in each session's `inbound.db`. `schedule_type`
mapping (cron / interval / once → v2 cron). Idempotent: skips v1
task ids already present. Inactive rows dumped to
`inactive-tasks.json` for reference.
Everything writes to `logs/setup-migration/handoff.json` — the source
of truth the skill consumes.
`.claude/skills/migrate-from-v1/SKILL.md`:
- **Phase A** (always): owner seeding + v1 access policy flip
(`unknown_sender_policy` public/strict) via `AskUserQuestion`. Pulls
sender candidates from v1's `messages` table as hints.
- **Phase B** (if followups exist): walks
`handoff.followups` — translates `.v1-container-config.json`
sidecars, handles `not_supported` channels, fills in missing
required keys with instructions on where to get them.
- **Phase C** (fork-aware): `git log <upstream>..HEAD` in v1. Empty →
"no customizations to port." Non-empty → scope choice (mechanical /
full interview / reference-only). Portable categories
(`container/skills/*`, `.claude/skills/*`, docs) scan+copy with
`scanForV1Patterns`. Non-portable (`src/*`,
`container/agent-runner/src/*`) stash to `docs/v1-fork-reference/`
— explicit "don't translate v1 infra to v2" warning because v1's
IPC file queue / single DB don't exist in v2.
Clearly marked in README, CLAUDE.md, SKILL.md header, and via a `p.warn`
that fires once per run when v1 is detected. Users with no v1 install
see a silent skip — no prompts, no noise.
Verified end-to-end against a live v1 install (300 discord + 1
discord-supervisor groups, fork with ~15 commits of PR-factory work):
- Detect → validate → db (301 rows seeded) → groups (301 CLAUDE.local.md
+ 178 other files + 1 container_config sidecar) → env (4 keys copied)
→ channel-auth (flagged missing `DISCORD_APPLICATION_ID` +
`DISCORD_PUBLIC_KEY`) → channels (discord installed, discord-supervisor
→ not_supported) → tasks (0 rows, skipped)
- Idempotent re-run: 0 rows created, 903 rows reused; tasks skip if
id already present
- Fresh-user case: silent skip, no prompts, straight to "You're ready!"
- Schema-mismatch case: recorded to `schema-mismatch.json`, chain
continues
- Unit tests for the pure transforms (`parseJid`,
`inferChannelType`, `triggerToEngage`, `scanForV1Patterns`,
`looksLikeV1Install`)
- Validate `requiredV2Keys` for telegram/slack/matrix/teams/webex/
resend/linear against the actual Chat SDK packages (Discord was
verified from real error output)
- Widen candidate auth file paths for WhatsApp/Matrix/iMessage based
on real non-Discord v1 installs once we have some
See docs/v1-to-v2-changes.md for the v1 → v2 architecture diff.
Removes transient analysis/proposal/checklist docs whose purpose is served once v2 ships: REFACTOR.md, docs/v1-vs-v2/, docs/checklist.md, docs/shared-source.md, docs/claude-md-composition.md, docs/module-contract.md, docs/DEBUG_CHECKLIST.md. Updates CLAUDE.md and docs/README.md index rows accordingly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
install_packages and add_mcp_server already did the right thing on approve
(install auto-rebuilt+killed, add_mcp_server just killed), so request_rebuild
was redundant plumbing agents sometimes called after an install — wasting an
admin approval round-trip. Delete it end-to-end:
- container/agent-runner/src/mcp-tools/self-mod.ts: remove requestRebuild
tool + registration; update install_packages description.
- src/modules/self-mod/{request,apply,index}.ts: drop handleRequestRebuild
+ applyRequestRebuild + registrations; rewrite the rebuild-failed notify
to point admins at retrying install_packages instead.
- src/modules/{approvals,self-mod}/{agent,project}.md and skill/self-
customize/SKILL.md: scrub agent-facing references; clarify that
add_mcp_server needs no rebuild (bun runs TS directly).
- docs/{module-contract,architecture-diagram,checklist,db-central,shared-
source,v1-vs-v2/*}.md, CLAUDE.md, pending-approvals migration comment,
approvals/index.ts docstring, REFACTOR.md: trailing references.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Document the selective-mode gotcha for auto-created OneCLI agents
(no secrets injected by default) with the CLI commands to inspect
and fix it. Note that approval policies are not configurable via
the SDK or `onecli@1.3.0` CLI — web UI only.
Replace stale `NANOCLAW_ADMIN_USER_IDS` / `src/access.ts` references
across CLAUDE.md, docs/architecture.md, docs/checklist.md, and
docs/module-contract.md. Admin gating now runs host-side in
src/command-gate.ts against `user_roles`; approver picks live in
src/modules/approvals/primitive.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the per-group "written once at init, owned by the group" CLAUDE.md
with a host-regenerated entry point that imports:
- a shared base (`container/CLAUDE.md` mounted RO at `/app/CLAUDE.md`)
- optional per-skill fragments (skills that ship `instructions.md`)
- optional per-MCP-server fragments (inline `instructions` field in
`container.json`)
- per-group agent memory (`CLAUDE.local.md`, auto-loaded by Claude Code)
Principle: RW = per-group memory, RO = shared content. Source/skills/base
are shared; personality, config, working files, and Claude state stay
per-group.
Key changes:
- New `src/claude-md-compose.ts` — per-spawn composition +
`migrateGroupsToClaudeLocal()` one-time cutover.
- New `container/CLAUDE.md` — shared base, seeded verbatim from the
former `groups/global/CLAUDE.md`.
- `src/container-runner.ts` — swap `/workspace/global` mount for RO
`/app/CLAUDE.md`; call `composeGroupClaudeMd()` after
`initGroupFilesystem()`.
- `src/group-init.ts` — drop `.claude-global.md` symlink + initial
`CLAUDE.md` write; seed `CLAUDE.local.md` from `opts.instructions`.
- `src/index.ts` — call `migrateGroupsToClaudeLocal()` at startup.
- `src/container-config.ts` — add optional `instructions` field to
`McpServerConfig` (inline per-MCP guidance fragment).
- `container/Dockerfile` — drop dead `/workspace/global` mkdir.
- Remove obsolete `scripts/migrate-group-claude-md.ts`.
Migration (runs once at host startup, idempotent):
- Delete `.claude-global.md` symlinks in each group.
- Rename each `groups/<folder>/CLAUDE.md` → `CLAUDE.local.md`
(preserves existing per-group content as memory).
- Delete `groups/global/` directory.
Design docs: `docs/claude-md-composition.md` and `docs/shared-source.md`
(the latter is the sibling design discussion this refactor builds on).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the per-group agent-runner-src copy model with a single shared
read-only mount. Source and skills are now RO + shared; personality,
config, working files, and Claude state stay RW + per-group.
Key changes:
- Mount container/agent-runner/src/ RO at /app/src (all groups share one copy)
- Mount container/skills/ RO at /app/skills; per-group skill selection via
symlinks in .claude-shared/skills/ based on container.json "skills" field
- Mount container.json as nested RO bind on top of RW group dir
- Move all NANOCLAW_* env vars to container.json (runner reads at startup)
- New runner config.ts module replaces process.env reads
- Move command gate (filtered/admin) from container to host router
- Dockerfile: remove source COPY, split CLI installs (claude-code last),
move agent-runner deps above CLIs for better layer caching
- Add writeOutboundDirect for router denial responses
- Design doc at docs/shared-src.md
Not included (follow-up): DB migration to drop agent_provider columns,
cleanup of orphaned agent-runner-src directories.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Documents and implements the output contract from docs/setup-flow.md:
Level 1: clack UI — branded, concise, product content
Level 2: logs/setup.log — append-only, linear, structured entries for
humans + AI agents reviewing a run
Level 3: logs/setup-steps/NN-name.log — full raw stdout+stderr per step
Every scripted sub-step, including bootstrap, emits at all three levels.
Bootstrap now runs under a bash-side clack-alike spinner with live elapsed
time; its apt/pnpm output is captured to 01-bootstrap.log and summarised
as a progression entry. setup.sh's legacy log() routes to the raw log
instead of contaminating the progression log.
Telegram install becomes fully branded: setup/auto.ts owns the BotFather
instructions (clack note), token paste (clack password with format
validation), and getMe check (clack spinner). add-telegram.sh drops to a
non-interactive installer that reads TELEGRAM_BOT_TOKEN from env, logs to
stderr, and emits a single ADD_TELEGRAM status block on stdout.
The Anthropic credential flow is the one intentional break — register-
claude-token.sh still inherits the TTY for claude setup-token's browser
dance; it logs as an 'interactive' progression entry with the method.
setup/logs.ts centralises the level 2/3 formatting: reset, header, step,
userInput, complete, abort, stepRawLog. User answers (display name, agent
name, channel choice, telegram_token preview) log as their own entries so
the setup path is reconstructable from the progression log alone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers the gap item 5 left open: request_approval presupposes a wired
channel, so unknown-channel cases (new DM, @mention in unwired group,
bot added to fresh group) short-circuit at no_agent_wired before the
approval flow runs.
Design:
- Owner-sender auto-wire fast path (exactly one agent group → wire
silently; multiple → card)
- Card with one button per existing agent group + "Create new" + "Ignore"
- New pending_channel_approvals table, UNIQUE(messaging_group_id)
- nca- action-id prefix paralleling nsa- / ncq-
- Handler lives alongside handleSenderApprovalResponse
- "Create new" sub-flow is intentionally open scope
Cross-reference added to item 5 so the scope boundary is explicit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
These three constants were carried over from v1's polling + IPC architecture
and have zero callers in the v2 runtime:
- POLL_INTERVAL (2000ms) — v1 message loop; replaced by event-driven
delivery + delivery.ts's ACTIVE_POLL_MS (hardcoded 1000ms)
- SCHEDULER_POLL_INTERVAL (60000ms) — v1 task scheduler; replaced by
host-sweep.ts's SWEEP_INTERVAL_MS (hardcoded 60_000)
- IPC_POLL_INTERVAL (1000ms) — v1 file-based IPC; meaningless in v2's
session-DB architecture
Grep confirms no imports in src/, container/, or tests. Docs/SPEC.md
updated to match.
Ref: docs/v1-vs-v2/ACTION-ITEMS.md item 15.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- docs/v1-vs-v2/: full v1→v2 regression analysis (SUMMARY + 21 per-module
docs + ACTION-ITEMS rollup with decisions + timezone recreation spec).
- container/agent-runner/scripts/sdk-signal-probe.ts: empirical harness
used to characterise Claude Agent SDK event/hook/stderr timing for the
stuck-detection design in item 9.
- src/channels/chat-sdk-bridge.ts: document the conversations Map staleness
in a code comment; fix deferred to when dynamic group registration lands
(ACTION-ITEMS item 17).
No runtime behavior change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a new operational skill that routes any agent group to a local
Ollama instance instead of the Anthropic API. Ollama speaks the
Anthropic /v1/messages endpoint natively, so no new provider code is
needed — just env var overrides and a model setting in the shared
settings file.
The skill also documents and applies two prerequisite source changes:
- ContainerConfig gains env and blockedHosts fields (container-config.ts)
- container-runner wires those fields as -e and --add-host Docker flags
- Dockerfile home dir set to chmod 777 so containers running as the
host uid can write ~/.claude config (discovered during implementation)
docs/ollama.md covers the architecture, OneCLI proxy bypass rationale,
network isolation via blockedHosts, model selection tradeoffs for Apple
Silicon, and revert instructions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PR #5 review flagged three behavior changes that shouldn't have slipped
in. This commit reverts each to match the pre-refactor behavior exactly.
1. User upsert ordering. Split the router hook into two setters:
setSenderResolver (runs before agent resolution) and setAccessGate
(runs after). Restores the pre-PR sequence where the users row is
upserted even if the message is dropped by wiring or trigger rules.
2. dropped_messages audit. Moved src/modules/permissions/db/dropped-messages.ts
back to src/db/dropped-messages.ts. The table is core audit infra, not
permissions-specific. Router re-writes rows for no_agent_wired and
no_trigger_match; the access gate writes rows for policy refusals.
3. Permissionless container fallback. Dropped. poll-loop restores the
original deny-all check when NANOCLAW_ADMIN_USER_IDS is empty.
Module contract doc updated with the two-hook shape.
Validation: host build clean, 137/137 host tests, 17/17 container
tests, typecheck clean, service boots to "NanoClaw running" with
permissions module registering both hooks and clean SIGTERM shutdown.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves user-roles / users / agent-group-members / user-dms /
dropped-messages / user-dm / canAccessAgentGroup into
src/modules/permissions/. Module registers a single inbound-gate that
owns sender resolution, access decision, unknown-sender policy, and
drop-audit recording.
Router slimmed from 357 → 179 lines; the inline fallback chain
(extractAndUpsertUser / enforceAccess / handleUnknownSender /
recordDroppedMessage) is gone — without the permissions module core
defaults to allow-all with userId=null.
container-runner's admin-ID query is now inline SQL guarded by
sqlite_master on user_roles, keeping core free of any import from the
permissions module. The container-side formatter falls back to
permissionless mode when NANOCLAW_ADMIN_USER_IDS is empty: every sender
with an identifiable senderId is treated as admin.
Module contract doc formalizes the tier model and the dependency rule
(core ← default modules ← optional modules). One transitional violation
flagged: src/access.ts (core) imports from the permissions module for
its remaining approver-picking helpers; resolves in the planned PR #7
re-tier.
Validation: host build clean, 137/137 host tests, 17/17 container
tests, typecheck clean, service boots to "NanoClaw running" with
permissions module registering its gate and clean SIGTERM shutdown.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codifies the interface between core and modules: the four registries
(delivery actions, inbound gate, response dispatcher, MCP tool
self-registration), default modules (typing, mount-security),
guarded-inline fallbacks, MODULE-HOOK skill-edit markers, and module
migration naming.
Authoritative reference for downstream extraction PRs and install
skills. See REFACTOR_PLAN.md for broader context.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renamed 12 docs/v2-*.md → docs/*.md (already in index from earlier git mv).
Rewrote CLAUDE.md to describe the codebase as just "the codebase" rather
than "v2"; added a "Channels and Providers (skill-installed)" section
reflecting the new model and updated the docs index links.
Agent (general-purpose) cleaned the 12 doc bodies:
- Dropped "NanoClaw v2" / "v2 schema" / "(v2)" prose throughout
- Rewrote inter-doc cross-references docs/v2-X.md → docs/X.md
- Architecture, agent-runner-details: collapsed v1↔v2 comparison tables
into present-tense facts; added notes that trunk only ships `claude`
and that channel adapters are skill-installed from the `channels` branch
- Setup-wiring, checklist: dropped v1→v2 migration items that no longer
apply
- Frozen runtime paths preserved: data/v2.db, data/v2-sessions/,
container name nanoclaw-v2
git grep confirms remaining `\bv2\b` matches in docs/ are only those
runtime paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- docs/v2-build-and-runtime.md: new — runtime split rationale (Node host,
Bun container), lockfile topology, supply-chain trade-offs, image build
surface, two session-wake paths, CI shape, key invariants. Indexed from
CLAUDE.md v2 Docs Index.
- CLAUDE.md: Container Runtime (Bun) section with trigger/action gotchas
a contributor editing the container must know (named-param prefix rule,
bun:test vs vitest, bun.lock regeneration, no minimumReleaseAge for the
Bun tree, no tsc build step, DELETE pragma invariant). CJK font support
section for Claude sessions outside of /setup to proactively offer when
they detect CJK signals. Development section updated with Bun commands.
- .claude/skills/setup/SKILL.md: step 3b — auto-enable CJK fonts without
asking if the user is already writing in CJK; otherwise ask only on clear
signals (CJK timezone from step 2a). 3c renumbered from old 3b.
Add agent-facing rules to CLAUDE.md covering minimumReleaseAgeExclude,
onlyBuiltDependencies, and frozen lockfile requirements — all require
human sign-off. Add comprehensive human-facing section to
docs/SECURITY.md with rationale, exclusion procedure (exact version
pin, approval, expiry), and build script allowlist documentation.
Drops the in-chat credential-collection flow introduced in e92b245. Agents
can no longer collect API keys via a secure modal — users must add secrets
through OneCLI directly. Keeps the OneCLI manual-approval handler and
threaded-routing work from the same commit intact.
Removed:
* container/agent-runner/src/mcp-tools/credentials.ts (MCP tool)
* src/credentials.ts (host-side modal/OneCLI pipeline)
* src/db/credentials.ts + migration 005 (pending_credentials table)
* src/onecli-secrets.ts (createSecret CLI facade, only caller was credentials.ts)
* findCredentialResponse from agent-runner DB layer
* PendingCredential types
* Four credential hooks from ChannelSetup (getCredentialForModal,
onCredentialReject, onCredentialSubmit, onCredentialChannelUnsupported)
* Credential card/modal handling in chat-sdk-bridge (nccr/nccm prefixes,
Modal/TextInput imports)
* credential_request text fallback in WhatsApp adapter
* request_credential system-action case in delivery.ts
Added:
* Migration 009 drops pending_credentials on existing installs.
Vercel skill now tells the agent to ask the user to register the token via
OneCLI instead of invoking the removed tool.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
update_task lets the agent adjust prompt/recurrence/processAfter/script
on a live scheduled task without losing the series id the user already
knows. Empty string clears recurrence/script.
list_tasks now groups by series_id so recurring tasks show as one row
(the live pending/paused occurrence) instead of one per firing — the
id displayed is the stable series handle that update/cancel/pause/resume
all match against.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The dev-agent-in-worktree approach for source self-modification is abandoned
in favor of a direct draft/activate flow with OS-level RO enforcement
(planned, not yet implemented). Strip the whole subgraph:
src/builder-agent/, pending-swaps DB module + migration 006, builder-agent
MCP tools, and all host wiring (startup sweep, approval routing, deadman,
worktree mount, freeze gate). Tool descriptions in self-mod.ts / agents.ts
no longer cross-reference create_dev_agent. CLAUDE.md + v2-checklist updated
to describe the new direction.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Checkpoints the builder-agent dev-agent/worktree/swap flow (create_dev_agent,
request_swap, classifier, deadman, promote) before pivoting to a unified
draft-activate approach with OS-level RO enforcement. Lifts container_config
out of the agent_groups row into groups/<folder>/container.json so install_packages,
add_mcp_server, and rebuild flows can eventually route through the same draft
path as source edits.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- create_agent is not admin-gated (host has no role check on the system
action; agentTools unconditionally in the container MCP tool list).
- install_packages / add_mcp_server approval is owner/admin via
pickApprover, not "admin-only".
- Chat-first setup bootstrap + post-handoff welcome are partially done
via /setup + /init-first-agent (still TODO: single top-level entrypoint,
welcome prompt expansion).
- Add entries for cold-DM infrastructure (ChannelAdapter.openDM,
ensureUserDm, user_dms cache) and /init-first-agent skill under
Channel Adapters.
- Add entry for delivery ACL throw-on-unauthorized + implicit-origin
allow + auto-create agent_destinations on wire (the silent-drop bug
fix from the welcome-DM end-to-end test).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the agent-group-centric "main group" concept with user-level
privileges and adds the cold-DM infrastructure needed for proactive
outbound messaging (pairing, approvals, welcome flows).
Privilege model
- New tables: users, user_roles (owner global-only; admin global or
scoped to an agent_group), agent_group_members (explicit non-
privileged access; admin/owner imply membership), user_dms (cold-DM
resolution cache).
- Removed agent_groups.is_admin, messaging_groups.admin_user_id. Replaced
with messaging_groups.unknown_sender_policy (strict | request_approval
| public) for per-chat unknown-sender gating.
- src/access.ts: canAccessAgentGroup, pickApprover, pickApprovalDelivery.
- src/router.ts: access gate on every inbound, honoring
unknown_sender_policy for unknown senders.
- src/channels/telegram.ts: pairing interceptor upserts the paired user
and promotes them to owner if hasAnyOwner() is false (first-pair-wins).
Cold DM infrastructure
- ChannelAdapter.openDM?(handle) — optional method. Chat-SDK-bridge wires
it to chat.openDM() for resolution-required channels (Discord, Slack,
Teams, Webex, gChat); direct-addressable channels (Telegram, WhatsApp,
iMessage, Matrix, Resend) fall through to the handle directly.
- src/user-dm.ts: ensureUserDm(userId) — resolves + caches via user_dms.
Approval routing
- onecli-approvals + delivery use pickApprover + pickApprovalDelivery:
scoped admins → global admins → owners (dedup), first reachable via
ensureUserDm, same-channel-kind tie-break. Approvals land in the
approver's DM, not the origin chat.
Delivery fixes
- delivery.ts ACL rejection now throws instead of returning undefined —
the outer loop previously marked rejected messages as delivered.
- Implicit-origin allow: session.messaging_group_id === target skips the
destination check.
- createMessagingGroupAgent auto-creates the companion agent_destinations
row (normalized local_name from the messaging group's name, collision-
broken within the agent's namespace).
Container
- container-runner.ts: /workspace/global always read-only; drops
NANOCLAW_IS_ADMIN; adds NANOCLAW_ADMIN_USER_IDS (owners + global admins
+ scoped admins for this agent group). Agent-runner poll-loop gates
slash commands against that set.
New skill: /init-first-agent
- Walks the operator through standing up the first agent for a channel:
channel pick → identity lookup (reads each channel SKILL.md's
## Channel Info > how-to-find-id) → DM platform_id resolution (direct-
addressable, cold-DM via "user DMs bot first + sqlite lookup", or
Telegram pair-code fallback) → run scripts/init-first-agent.ts →
verify via tail of nanoclaw.log.
- scripts/init-first-agent.ts: parameterized helper that upserts the
user + grants owner (if none), creates dm-with-<display-name> agent
group + initGroupFilesystem, reuses/creates the DM messaging_group,
wires it (auto-creates destination), resolves the session, and writes
a kind:'chat' / sender:'system' welcome message into inbound.db. Host
sweep wakes the container and the agent DMs the operator via the
normal delivery path.
/manage-channels rewrite
- Drops --is-main / --jid / main-vs-non-main isolation references.
- First-channel flow delegates to /init-first-agent.
- Explains createMessagingGroupAgent auto-creates destinations.
- Adds a privileged-users show section.
setup/
- register.ts: drop --is-main, --jid, --local-name, --trigger
requiresTrigger defaults; call initGroupFilesystem; normalize to
v2 schema (no is_admin, no admin_user_id, sets unknown_sender_policy
'strict'); let createMessagingGroupAgent handle the destination row.
- pair-telegram.ts: emit PAIRED_USER_ID (namespaced "telegram:<id>")
instead of ADMIN_USER_ID; update header comment.
- register.test.ts deleted — was v1-only, tested a registered_groups
table that no longer exists.
Docs
- v2-architecture-diagram.{md,html}: ER diagram updated to drop
is_admin/admin_user_id, add unknown_sender_policy, and include
users/user_roles/agent_group_members/user_dms.
- v2-architecture-draft.md: approval-routing paragraph rewritten for
pickApprover/pickApprovalDelivery/ensureUserDm; SQL schema block
updated; admin-verification paragraph references
NANOCLAW_ADMIN_USER_IDS.
- v2-setup-wiring.md: entity-model sketch rewritten.
- v2-checklist.md: marked privilege refactor / container filtering /
approval routing / unknown-sender gating done; removed obsolete
admin_user_id and main-vs-non-main items.
Scripts
- scripts/init-first-agent.ts (new) replaces scripts/welcome-owner-dm.ts
(removed; welcome-owner was a Discord-specific one-off).
- test-v2-host.ts, test-v2-channel-e2e.ts, seed-discord.ts: drop
is_admin + admin_user_id, use unknown_sender_policy.
Tests
- src/access.test.ts (new): 14 tests for canAccessAgentGroup, role
helpers, pickApprover, ensureUserDm, pickApprovalDelivery.
- src/db/db-v2.test.ts: adds 3 tests for the auto-created
agent_destinations row (normalized name, no duplicates, collision
break within an agent group).
- host-core.test.ts, channel-registry.test.ts: updated fixtures to
use unknown_sender_policy: 'public' where the test exercises routing
rather than the access gate.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Approval cards now carry a required title (Add MCP Request, Install
Packages Request, Rebuild Request, Credentials Request) and structured
options with distinct pre-click label, post-click selectedLabel (e.g.
"✅ Approved" / "❌ Rejected"), and value used for click routing. The
title and normalized options are persisted in pending_questions so the
post-click card edit can render the correct per-type title and selected
label on both chat-sdk channels and Discord interactions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Install approval now auto-rebuilds the image and kills the container,
replacing the prior two-card flow where the agent had to call
request_rebuild separately after install_packages was approved.
Queues a processAfter=+5s synthetic prompt so the respawned container
verifies the new packages and reports back to the user.
Adds two v2-checklist gaps found along the way:
- /remote-control and /remote-control-end are v1 host-level commands
not ported to v2
- messaging_groups.admin_user_id is hardcoded null at registration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>