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>
Set package.json to 2.1.0 to match the CHANGELOG entry for the upgrade
tripwire (a [BREAKING] change warrants a minor bump). The startup
tripwire reads package.json as the source of truth, so this is the
version the gate will enforce.
bump-version.yml previously ran `pnpm version patch` on every push to
main, which would patch a deliberate 2.1.0 up to 2.1.1. It now skips the
auto-bump when the pushed commits already changed package.json
themselves. fetch-depth: 0 so the before/after diff has both tips.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The startup tripwire message was written for a coding agent and gave a
human no direction — only the bare `set` override (which skips the
migrations the gate guards). Add one human-addressed stanza pointing to
/update-nanoclaw as the correct fix. The tested CODING AGENT block is
left byte-for-byte unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three skills that were broken on v2 (branch-merge installs of stale branches,
SKILL.md-only with no shipped code, dead v1 schema targets) rewritten to the
additive standard:
- use-native-credential-proxy: a skill-owned .env credential proxy + one-line
seam reach-in (behavior + wiring tests, REMOVE.md). Explicit OneCLI opt-out;
the credential-home inversion is flagged in docs/skill-smells.md.
- add-ollama-tool: an atomic-chat-shaped MCP-tool skill — bun stdio server,
container registration + wiring tests, idempotent REMOVE.md.
- migrate-from-openclaw: retargeted at v2 (data/v2.db, ncl, OneCLI SecretRef,
v2 recurrence) with a transform unit test and a REMOVE.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Anatomy + correctness pass driven by the audit: REMOVE.md added to every
file-creating skill (gmail/gcal/rtk/mnemon/vercel/macos-statusbar/karpathy);
dead/fabricated reach-ins removed (mnemon's nonexistent OpenCode path and
migration-doc reach-in; migrate-from-v1's nonexistent scanForV1Patterns);
structural Dockerfile dep-tests where a CLI binary was unguarded; debug and
customize rewritten off stale v1 architecture onto v2 (data/v2.db, ncl,
two-DB sessions); update-skills + migrate-nanoclaw's branch-merge reapply
converted to additive re-runs; diff-against-past framing and non-step callouts
stripped throughout. Direct-DB / credential / telemetry smells flagged, not
actioned (see docs/skill-smells.md).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The provider-multipoint archetype: each skill now fetches and runs a
barrel-driven registration test in BOTH trees (host listProviderContainerConfigNames,
container listProviderNames) — pushed to origin/providers — instead of relying on
the shipped *.factory.test.ts, which imports the provider module directly,
self-registers, and stays green when a barrel line is deleted. Adds a structural
Dockerfile dep-test for codex's @openai/codex CLI binary, and a cross-runtime
REMOVE.md that reverses both barrels, the copied files, the dependency, and the
Dockerfile edits. Drops the grep-based "Verify" section.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All 15 channel skills brought to the slack/deltachat standard: SKILL.md fetches
and runs the behavior registration test (pushed to origin/channels) in a
"Build and validate" step; REMOVE.md rewritten to delete every copied file and
remove the channel's actual env vars + package (each was individually wrong —
e.g. discord falsely claimed "no package to uninstall"); the VERIFY.md
anti-pattern deleted across the fleet; REMOVE.md created for emacs and whatsapp.
linear drops its stale bridge-patch step and relies on the bridge default +
the wiring's engage mode.
Co-Authored-By: Claude Opus 4.8 (1M context) <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>
The behavior test mocks @nanoco/nanoclaw-dashboard (its startDashboard binds a
real port), so the test alone passes with the dependency missing. The build
step is what catches a missing dep (TS2307 on the `await import(...)`), so the
validate step must run `pnpm run build` before the tests. Make that explicit so
it survives edits — a dependency install is an integration point that needs a
red-on-missing check.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The registration tests now import the real barrel and assert the registry
contains the channel (not a structural source parse). Update the validate-step
prose accordingly: the test also goes red if the adapter package isn't
installed (the unmocked barrel import throws), so it implicitly verifies the
dependency-install step — and a structural check would falsely pass when the
barrel can't evaluate or the dep is missing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Retrofits the Slack skill to the two core principles and establishes the
template for the Chat SDK channel family (discord, telegram, teams, gchat,
webex, linear, github, …), which all share this single-barrel-import shape:
- Fetch and run a new src/channels/slack-registration.test.ts (lives on the
channels branch next to the adapter, copied in via git show). Structural
barrel parse asserting the `import './slack.js';` line — the one reach-in
that fires the adapter's top-level registerChannelAdapter. Hermetic (does
not import @chat-adapter/slack); red-on-delete. SKILL.md gains a
build+validate step that also notes the build leg guards the adapter's
createChatSdkBridge core-API consumption.
- REMOVE.md now deletes the import line and rm's the adapter and its test
(was a soft comment-out), and re-syncs .env to the container.
- Drop VERIFY.md — tests are the verification; its manual check is covered by
Next Steps / webhook setup.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Retrofits the existing deltachat channel skill to the two core principles
(minimal integration surface + a test for every functional integration point):
- Fetch and run a new src/channels/deltachat-registration.test.ts (lives on
the channels branch next to the adapter, copied in via git show like the
adapter itself). It guards the skill's one reach-in — the
`import './deltachat.js';` line in the channel barrel that fires the
adapter's top-level registerChannelAdapter. Structural barrel parse rather
than importing it, so the native @deltachat/stdio-rpc-server isn't pulled
into the host test process; the build leg covers that the import resolves.
Red-on-delete of the barrel line. SKILL.md gains a build+validate step.
- REMOVE.md now deletes the import line and rm's the adapter and its test
(was a soft comment-out), per the anatomy rule that remove reverses every
change including copied test files.
- Drop VERIFY.md — tests are the verification; its manual log/connectivity/
e2e checks were operational and already covered in Troubleshooting.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add red-on-delete integration tests for both reach-ins:
- index.ts mcpServers registration (AST, container/Bun tree)
- buildContainerArgs env-forward call (AST, host/Node tree)
- Extract env forwarding into src/atomic-chat-env.ts so the container-runner
reach-in is a single call (minimal integration point)
- Drop the redundant providers/claude.ts allowlist edit — the allow-pattern is
derived from registered MCP server names
- Move removal into a standalone REMOVE.md; SKILL.md reads as a self-contained,
present-tense artifact
- Document the shared stderr logger reach-in as a known hotspot (registry candidate)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First exemplar of the skills upgradeability model, built example-first.
Giving the skill real tests surfaced — and the build caught — two silent
drifts that would break any adopter today.
What conformance means here:
- Minimal integration point: all startup logic lives in startDashboard() in
the skill's own file; the edit to src/index.ts is a single colocated block
(dynamic import + await call) in main() — no top-of-file import.
- Behavior test (dashboard-pusher.test.ts): real in-memory DB + real pusher
+ fake dashboard HTTP endpoint; asserts the /api/ingest snapshot on both
the enabled and disabled paths.
- Wiring test (dashboard-wiring.test.ts): TS-AST assertion that index.ts
dynamically imports the pusher and awaits startDashboard() colocated in
main(), after DB init and before the boot-complete log — catches deletion
AND misplacement, which a grep can't.
- Build catches drift: fixed imports of five DB modules that moved into
src/modules/, and a stale g.container_config (now getContainerConfig()).
apply copies all three files and runs the tests; remove deletes them and the
single index.ts block. apply/remove stay markdown prose; the tests are the
verification.
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>
A bare * in the pre-filled secret_url path doesn't survive (the gateway
URL-encodes everything, so an unencoded * collapses to just /, which only
exact-matches the path /). Leave the path blank instead so the created
secret matches all of huggingface.co, not a single endpoint.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop "(host pattern pre-filled)" and "— no restart needed" from the HF
setup instructions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The not-signed-in message hardcoded both a local and a hosted OneCLI
dashboard URL because the container can't tell which gateway it's behind.
But the gateway already tells us: a credential-less proxied request comes
back with the right URL in its error body —
- credential_not_found → secret_url (pre-filled "new secret" form)
- access_restricted → manage_url (grant this agent access)
- app_not_connected → connect_url
Capture whoami's body + status (drop -f so the JSON survives the 401),
extract that URL, and present it. It's always the correct gateway, local
or hosted, with zero extra wiring. The secret_url's pre-filled `path`
defaults to the failing request path (/api/whoami-v2), so broaden it to
/* — otherwise the created secret wouldn't cover the upload endpoints.
Falls back to generic text when there's no gateway JSON to read.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The default OneCLI secret mode for auto-created agents is `all`, not
`selective` — a fresh agent created via ensureAgent({name, identifier})
comes back with secretMode "all", so matching vault secrets inject
automatically. Drop the now-unnecessary per-agent assignment step.
- upload-trace.ts: remove step 3 (set-secret-mode) from the not-authed
message; creating the token and adding it to the vault is enough
- CLAUDE.md: trim the secret-mode gotcha to reflect `all` as the default
- init-onecli skill: replace stale `onecli start` (gone in 1.4.x) and the
`ps aux | grep onecli` check with the real Docker Compose start path
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a runner-handled /upload-trace slash command (admin-gated, like /clear)
that uploads the current session's Claude Code transcript to the user's own
private {hf_user}/nanoclaw-traces dataset, browsable in the HF Agent Trace
Viewer. The transcript is already in the format the viewer auto-detects, so
the command just locates the newest one and pushes it via the Hub commit API.
Auth is handled by the OneCLI gateway: curl goes out through the injected
HTTPS_PROXY, which adds the user's HF token — no credential ever touches
agent code. A missing/unassigned token yields a clear setup message.
- container/agent-runner/src/upload-trace.ts: isUploadTraceCommand() + uploadTrace()
- poll-loop.ts: recognize and handle /upload-trace in the runner
- command-gate.ts: admin-gate /upload-trace on the host
- upload-trace.test.ts: unit + integration coverage for the command
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude-code CLI 2.1.128 -> 2.1.154 (Dockerfile build-arg). agent-runner SDK 0.2.128 -> 0.3.154: the 0.3 major moved @anthropic-ai/sdk and @modelcontextprotocol/sdk from regular deps to peer deps, so add @anthropic-ai/sdk ^0.100.0 as a direct dep and raise @modelcontextprotocol/sdk to ^1.29.0. Regenerate bun.lock. Typecheck + agent-runner tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>