Migration 019 grandfathers rows that exist when it runs, but two paths
sidestep it and silently flipped pre-existing groups to the lean
defaults:
- A legacy group with no container_configs row at upgrade time gets its
row from the startup backfill, which ran AFTER the migration and
stamped '{}'. The backfill now stamps the same legacy all-on state —
any group it touches by definition pre-dates the feature.
- During the update window (bind-mounted runner source already new, old
host still running), container.json lacks the harnessCapabilities
field and the runner defaulted it to {} = all-off. A missing field can
only mean a pre-capability host (new hosts always emit the resolved
map), so the runner now defaults it to legacy all-on.
Regression tests cover both: the migration→backfill boot sequence, and
the raw-config mapping (extracted as configFromRaw for testability).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Migration 019 now stamps existing container_configs rows with
{agent-teams:on, workflow:on} — their pre-feature behavior — so upgrading
changes nothing for current groups. Only rows inserted after the migration
(new groups; every group on a fresh install, which has none yet) get the
lean column default. Operators opt existing groups into the lean defaults
per group via ncl.
Verified e2e on a real main->branch upgrade: existing groups keep teams +
Workflow through the full reconcile+spawn path (settings.json retains the
teams env key, no disableWorkflows, container.json shows both on); a new
group gets {}. Drops both [BREAKING] CHANGELOG entries — the change is now
non-breaking. Also corrects the allowlist CHANGELOG line (allowedTools is
fully inert, not Glob/Grep-promoting) and notes the added SDK-pin guard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Correctness:
- reconciler + PreCompact hook unified into one read-modify-write with a
single atomic write, and the parsed value is shape-validated: a settings.json
holding valid-JSON-non-object (null, a scalar, {"env":"bad"}) no longer
throws a TypeError that bricked the group — it warns and skips, like the old
hook. Fixes divergent atomic/non-atomic writers and a double read per spawn.
- config update wraps both DB writes in one transaction — a failing scalar
update can no longer leave the harness override half-applied.
- parseHarnessCapabilitiesArg returns a single key->directive map, so a repeated
key resolves last-wins (was: clears always beat sets regardless of order).
- validation uses Object.hasOwn, not `key in` — inherited names like
`constructor`/`toString` are no longer accepted as capabilities.
- createContainerConfig now binds cli_scope + harness_capabilities (were
silently dropped to the column default).
Security honesty:
- agent-teams=off is documented as spawn-time hygiene, not a hard in-container
boundary: settings.json is RW-mounted so an agent can rewrite it for the
container lifetime. workflow=off keeps its disallowedTools backstop. Follow-up
filed to mount the managed settings source read-only.
- dispatch escalation gate protects canonical field names and normalizes each
incoming arg key — no spelling-variant bypass.
Altitude / cleanup:
- capabilities carry their host mechanism in the registry; the reconciler
iterates it, so adding a key is one registry entry (+ one runner map line if
it blocks a tool). Runner unknown-key lockstep + warning loop deleted (host
drops unknown keys at resolve; warns once per distinct problem, not per spawn).
- resolved view is structured {state, source}; source is 'override' only for a
VALID stored override (an invalid value reports 'default', not a lie).
- ReportFindings joins the fixed disallow list (headless has no UI to receive
it; ~1.9KB/turn). PreToolUse hook built once in the constructor with a Set.
- drift guard: assert the installed SDK version too (SDK now pinned exactly),
and dual-capture the fixture (tools vs toolsBare). This EMPIRICALLY corrected
a prior claim: allowedTools is fully inert here (surfaces are byte-identical),
it does not promote Glob/Grep — comment fixed to match.
Host 663 + container 125 green; wire re-probed (default drops
Workflow/DesignSync/ReportFindings; workflow=on restores only Workflow).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TOOL_ALLOWLIST named five tools that don't exist on claude-code 2.1.197
(Task was renamed Agent upstream; TodoWrite, TeamCreate, TeamDelete, and
ToolSearch are gone). Remove the phantoms and correct the comment (and
the stale claim in docs/agent-runner-details.md): allowedTools is a
permission auto-approve list, not an availability filter — paired wire
captures show no allowlist effect on the offered tool surface, and this
runner's bypassPermissions moots its permission role. No wire-visible
change.
Add a wire-captured fixture (sdk-tools-baseline.json, regenerated via
dump-sdk-tools.ts inside the agent image with a zero-API-cost 401-stub
capture) and claude.tools.test.ts, which fails on any future
claude-code CLI or Agent SDK bump until the fixture is regenerated and
the lists re-verified. The SDK dep is pinned exactly (0.3.197, was
caret) so drift can only enter deliberately.
Wire measurement on this pin also shows run-to-run NONDETERMINISM in
the offered surface: conditional tools (Glob/Grep) flicker, and
disallowedTools stripping of flag-gated tools is per-query best-effort.
The fixture pairs captures within one regen run to control for this,
records a disallow probe as a diagnostic, and the dump-script header
documents the variance; the deterministic tool block remains the
runner's PreToolUse hook.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- api-details.md: ChannelDefaults/ChannelContextDefaults interfaces, five-tier getChannelDefaults resolution chain, creation helpers, and messaging_group_agents.threads runtime semantics
- setup-wiring.md: two-level model, shared-identity mention-suppression pattern (reusable by iMessage/Signal), and the trunk-update back-compat contract with the one deliberate isGroup exception
- isolation-model.md: cross-reference from the entity model's engage columns to the defaults docs
- CLAUDE.md: channel-defaults.ts key-file row and a short Channel defaults paragraph
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJkDAnLGWJUqjgJNXpyqGZ
Scheduled tasks move from MCP tools to a first-class ncl resource:
list / get / create / update / cancel / pause / resume / run / append-log,
with args declared on every verb (strict validation, fix-carrying errors)
and deep per-verb help with executable examples.
A series is CronJob-like: the live (pending/paused) row is the next
occurrence, completed rows are its run history. tasks list reads as a
compact run-history table, server-rendered so the container agent prints
the same aligned table. Short named ids (<slug>-<hex>) are
filesystem/thread-safe and copy-pasteable. Agents keep a per-series work
journal: the run log at tasks/<series>.md (append-log + auto-logged
final text).
Each series runs in its own isolated system session
(system:tasks:<series>); the live-task cap is dropped — isolation
replaces throttling. Spent task sessions are GC'd by the sweep.
--script on a task runs a bash gate BEFORE the agent wakes. It prints a
JSON verdict as its last stdout line: {"wakeAgent": bool, "data": {...}}.
wakeAgent:false handles the occurrence without waking the agent; data is
threaded into the fire's prompt, so a gate can fetch/inspect and hand the
agent exactly what changed. Failing scripts back off exponentially
(2·2^(n-1) min, cap 60) and auto-pause the series after 8 consecutive
failures with a host-written run-log note; manual runs are excluded from
the streak.
BREAKING: the schedule_task / list_tasks / update_task / cancel_task /
pause_task / resume_task MCP tools are removed. Agents schedule via
ncl tasks (available under cli_scope=group). Existing task rows keep
firing — storage (messages_in kind='task') is unchanged; only the
management surface moved. Migration: docs/ncl-tasks-migration.md.
Review round (gavrielc):
- Recurrence frequency guard: create/update refuse a recurrence more
frequent than 4 fires/day with a warning steering to gate scripts;
--dangerously-override-recurrence-limit bypasses after explicit user
confirmation. Counted over the next 24h in the instance TZ. Guarded on
update too, so create-slow-then-update cannot sneak past.
- ncl tasks help scripts: resource help topics (new helpTopics seam on
registerResource) carry the full gate-script guide — contract, examples,
state, failure/backoff semantics, testing directions.
- Agent-facing scheduling instructions slimmed: no recurring-frequency
prose in the fragment; one pointer to the help topic.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The section still described the pre-envelope model where every provider
result was written to messages_out verbatim. Since the envelope parser
landed, the agent-runner parses the final text for <message to="name">
blocks (dispatchResultText, poll-loop.ts): one messages_out row per
block, bare/<internal> text is scratchpad (logged, never sent), unknown
destinations are dropped, unwrapped output gets a one-time re-wrap
nudge, and non-retryable error results are delivered as error notices
instead of being dropped.
Correct docs (and one code comment) that describe systems that no longer
exist in v2: mark docs/SPEC.md as the historical v1 spec; replace the
impossible "write messages_in (to self)" / stale list_tasks scheduling
model with the real messages_out system-action path in
agent-runner-details.md and architecture.md; drop the false
MAX_CONCURRENT_CONTAINERS cap claim; remove the per-group agent-runner-src
overlay from the live DB map (source is a shared read-only mount) and fix
the insertTask attribution to src/modules/scheduling/db.ts; correct the
container-skills count to 8; repoint the deleted-doc comment in
src/claude-md-compose.ts; and replace the AGENT_PROVIDER / hand-edit
container.json provider-config instructions in add-opencode and add-mnemon
with the real `ncl groups config update --provider` flow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rewrite docs/SECURITY.md against the real v2 codepaths: the actual
buildMounts mount table, the allowReadWrite allowlist schema the mount
validator enforces, and the true defaults (egress open, no CPU/mem
limits, additional mounts blocked until an allowlist exists). Replace the
deleted v1 perimeter (main/non-main groups, ephemeral containers, IPC
authorization, /dev/null .env shadow, data/sessions path) with a v2 Trust
Model built on user_roles and long-lived per-session containers. Drop the
dangling /add-golden-registry reference. Mark docs/docker-sandboxes.md and
docs/APPLE-CONTAINER-NETWORKING.md as v1-historical and update the
docs/README.md portal rows. Remove the Apple Container native-runtime
claims from README.md (no runtime seam exists; container-runtime.ts
hardcodes docker).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The container self-registration barrel imported mock.js, so every container
registered a 'mock' provider returning canned text. A typo'd --provider mock
would silently 'work' instead of failing loudly. Drop the import (mock.ts stays
for direct test use) and tidy the now-stale host comments and docs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Agent templates: folder-only templates under templates/ (context/instructions.md +
optional context extras, .mcp.json, skills/). Stamping via ncl groups create
--template writes the provider-neutral instructions.prepend.md (inlined at the top
of CLAUDE.md/AGENTS.md every spawn), copies context extras preserving their
template-relative layout, writes MCP servers to container config, and installs the
per-group skills overlay. Includes docs (docs/templates.md, templates/README.md).
Setup-wizard wiring ships separately on top of this.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pass CONTAINER_CPU_LIMIT / CONTAINER_MEMORY_LIMIT through to `docker run`
as --cpus / --memory in buildContainerArgs. Both default to empty, so spawn
args are byte-identical to today unless an operator opts in — no risk of
OOM-ing existing workloads. Caps an agent container's CPU/memory so one agent
can't monopolize the host. Swap is a deployment concern (--memory is a hard
cap on a swapless host); not managed here.
Structural tests assert each flag is pushed and guarded by its env knob,
matching the existing buildContainerArgs structural-test convention.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>