Companion to the core `chat` 4.29.0 bump on main and the @chat-adapter bump on
the channels branch. Pins `chat` and the channel-adapter install references on
the providers branch to the matched 4.29.0 release, so a provider-branch install
that also adds a channel stays on the same Chat SDK generation as main's bridge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
f.slice(11) silently assumed len("YYYY-MM-DD-") and was coupled to the
`dated` regex two lines up — change the date format and it points at the
wrong offset while still "working". Strip with f.replace(dated, '') so the
prefix length lives in exactly one place.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restore a sortable `YYYY-MM-DD-` prefix to the conversation archive filename
(parity with the Claude path) while keeping it thread-stable: reuse the
thread's existing file regardless of date so later exchanges keep appending to
one file, and only stamp the creation date when the file doesn't exist yet.
Exact-suffix match past the date prefix avoids substring collisions between
threads with shared prefixes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Codex provider wrote one standalone file per exchange because its
app-server keeps conversation history server-side, with no on-disk
transcript to roll up at a compaction boundary. Key the archive file on
the thread/continuation id and append each completed exchange instead, so
a session lands in one growing file — matching the Claude path's
one-file-per-session granularity.
The thread-level header (provider, continuation id) is written once when
the file is created; each appended block carries its own timestamp and
status. Distinct threads still get distinct files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Trunk's adfae67 moved global Node CLI installs into container/cli-tools.json
(a json-merge seam) so a skill adds a CLI without editing the Dockerfile. The
Codex provider still hardcoded its CLI into the Dockerfile and guarded that
shape with a test. Migrate it:
- Drop the codex ARG + RUN from this branch's reference Dockerfile.
- Replace codex-dockerfile.test.ts with codex-cli-tools.test.ts, which asserts
the @openai/codex entry in cli-tools.json (skips when the manifest is absent,
e.g. on the bare providers branch).
- setup/providers/codex install-check verifies the manifest entry, not the
Dockerfile.
Pairs with the trunk-side add-codex.sh / SKILL.md change that appends the
manifest entry instead of awk-ing the Dockerfile.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rewrite the Codex provider onto the host's capability seams. Codex runs as a
real agent provider via `codex app-server` — planning, MCP tools, server-side
history, session resume — not as an MCP tool under Claude.
- Host provider (src/providers/codex.ts, codex-agents-md.ts): registers on the
provider-container seam, composes AGENTS.md from the real config row, mounts a
per-group ~/.codex state dir, vault-only auth stub (no credential in-container).
- Container runtime (codex.ts, codex-app-server.ts): app-server transport, turn
lifecycle, racing-follow-up fix (clear the active turn on completion).
- Provider-owned per-exchange archiving (exchange-archive.ts) via onExchangeComplete.
- Codex CLI pinned to 0.138.0 in the Dockerfile (ARG + global install), guarded
by a structural dockerfile test.
- macOS first-spawn fix: pre-create the auth-stub mountpoint.
The /add-codex skill is dropped from this branch — trunk is its canonical home.
The authored-skills canonical store is deferred to a future provider-seam PR
(its stale host-contribution assertion is removed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Behavior tests in both trees that import ONLY the real barrel (host
src/providers/index.ts → listProviderContainerConfigNames; container
providers/index.ts → listProviderNames) and assert the provider is present.
Unlike the existing *.factory.test.ts (which import the provider module
directly, self-register, and stay green when the barrel line is deleted),
these go red if the barrel reach-in is removed/drifts. The opencode container
test also implicitly guards @opencode-ai/sdk via its unmocked barrel import.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
NANOCLAW_IS_MAIN no longer exists in the v2 codebase, and
groups/global/ is explicitly migrated away by composeGroupClaudeMd
(shared base now lives in container/CLAUDE.md → /app/CLAUDE.md, which
the instructions array already includes). Carrying /workspace/global
would give OpenCode more context than the Claude provider sees.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before: wrapPromptWithContext concatenated /workspace/agent/CLAUDE.md
verbatim into a <system>...</system> block in the user-message text.
That file is the host-composed entry point that contains only `@./...`
includes (the .claude-shared.md symlink to /app/CLAUDE.md, plus the
module fragments). OpenCode does not expand `@` in instruction files —
the syntax is a Claude Code convention for the model's own Read tool
to lazy-load. Result: the model received the literal lines
"@./.claude-shared.md\n@./.claude-fragments/module-...md" as text and
saw none of the actual content (workspace, memory, conversation
history, agents/core/interactive/scheduling/self-mod modules, OneCLI,
etc.). Confirmed empirically by dumping the constructed prompt for a
turn before any fix.
After: pass the concrete files via OpenCode's native `instructions`
config field. Per packages/opencode/src/session/instruction.ts on the
upstream `dev` branch, absolute paths and globs are resolved with
`fs.glob(basename, { cwd: dirname, absolute: true, include: 'file' })`,
files are read raw, and the resulting strings are concatenated into
the LLM's system prompt at packages/opencode/src/session/prompt.ts:1442.
That is the canonical channel — same one OpenCode uses for AGENTS.md /
CLAUDE.md auto-discovery.
Configured set:
/app/CLAUDE.md shared base
/workspace/agent/.claude-fragments/*.md per-skill fragments
/workspace/agent/CLAUDE.local.md per-group memory
/workspace/global/CLAUDE.md cross-group memory (non-main)
Removed: readClaudeMdForPrompt, the manual <system> wrap of its output,
and the now-unused `fs` import. The dynamic systemInstructions wrap
(assistant name + destinations) stays as-is — that varies per call.
Verified post-fix: Citiclaw (gemma4:31b via Ollama) responded to "what
self-modification tools do you have" with the exact MCP tool names and
a structured bullet list — vs. the previous ungrounded "I haven't
received instructions" response.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs in the upstream OpenCode provider that fire together when a
local backend (Ollama, llama.cpp) is slower than the hardcoded 90s
event timeout:
1. proc.kill('SIGKILL') only kills the wrapper process the spawn
returned, not the opencode-linux-*/bin/opencode child it execs into.
The child keeps holding port 4096, so the next spawnOpencodeServer()
fails with "Failed to start server on port 4096" / EADDRINUSE.
Fix: spawn detached and signal the whole process group via
process.kill(-pid, 'SIGKILL') in a new killProcessTree() helper.
2. IDLE_TIMEOUT_MS = 90_000 is hardcoded. For a local 31B model the
first prompt's time-to-first-token routinely exceeds that, tripping
the timeout. Fix: read OPENCODE_IDLE_TIMEOUT_MS from env, default
300_000 (5 min) — generous for cloud APIs, just enough for local.
Per-group override goes in container.json env (e.g. "600000" for a
slow local box), no rebuild needed since src/ is bind-mounted.
Same bugs exist on origin/providers — should be ported upstream.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bring providers up to date with main, including the channel-inbound
attachment path-traversal fix.
Resolved conflicts:
- .claude/skills/add-codex/SKILL.md: took main (newer CODEX_VERSION,
more accurate schema docs)
- container/Dockerfile: kept main's stratified pnpm-install layers for
caching, added a separate layer for @openai/codex, bumped Dockerfile
CODEX_VERSION default to 0.124.0 to match SKILL.md
Slipped through during the #2035 rebase resolution — both #2030's import
and ours landed in the merge. TypeScript dedups by symbol so it didn't
fail the typecheck, but it's noise and would've eventually tripped a
linter rule.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original approach passed ANTHROPIC_AUTH_TOKEN into the container
as an env var and disabled the proxy for the custom host (NO_PROXY) —
which works, but bypasses OneCLI entirely for that credential. The
container holds the raw secret, the gateway loses audit/rotation, and
we lose the rest of the vault's protections for this cohort.
OneCLI-native version: store the token as a generic secret with header
injection (--header-name Authorization --value-format 'Bearer {value}'
+ host-pattern matching the base URL hostname). The container only
needs ANTHROPIC_BASE_URL plus a placeholder ANTHROPIC_AUTH_TOKEN — the
proxy rewrites the Authorization header on the wire.
setup/lib/setup-config.ts — adds --anthropic-auth-token alongside the
existing --anthropic-base-url.
setup/auto.ts — runAuthStep short-circuits the auth-method prompt when
both NANOCLAW_ANTHROPIC_BASE_URL and NANOCLAW_ANTHROPIC_AUTH_TOKEN are
set: creates the OneCLI generic secret, writes ANTHROPIC_BASE_URL to
.env (so the runtime reads it), and appends `import './claude.js';` to
src/providers/index.ts (so the provider only registers when the user
has configured a custom endpoint — no branching for everyone else).
src/providers/claude.ts — drops ANTHROPIC_AUTH_TOKEN/NO_PROXY
passthrough. Reads ANTHROPIC_BASE_URL from .env, sets a placeholder
ANTHROPIC_AUTH_TOKEN in container env so the SDK includes an
Authorization header for OneCLI to overwrite.
src/providers/index.ts — removes the unconditional import; setup
appends it on demand.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Users with a custom Anthropic-compatible endpoint (ANTHROPIC_BASE_URL) were
getting 401s because the OneCLI proxy injects ANTHROPIC_API_KEY=placeholder
and forwards to api.anthropic.com, overriding the custom endpoint and key.
Add a claude provider host config that reads ANTHROPIC_BASE_URL,
ANTHROPIC_AUTH_TOKEN, and CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC from .env
and passes them into the container. Also sets NO_PROXY for the custom host so
the OneCLI proxy doesn't intercept those requests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Matches the OneCLI CLI's own format expectation ("oc_... format" per
`onecli auth login --help`) so a malformed token gets caught at setup
time rather than at first vault call.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the example.internal placeholder with the hosted gateway URL
so the advanced screen and --help suggest the canonical destination
out of the box.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without `onecli auth login`, setup-time CLI calls (e.g. `secrets list`
inside anthropicSecretExists, `secrets create` in runPasteAuth) hit a
secured remote vault unauthenticated and fail silently — the auth step
sees no existing Anthropic credential and prompts the user to add one
even when it's already in the remote vault.
Two auth surfaces matter here: the CLI's persistent store via
`onecli auth login --api-key`, and ONECLI_API_KEY in .env that the
runtime SDK reads at request time. We need both.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a single config registry that drives both CLI flags and an opt-in
advanced-settings screen, so power users can override defaults like
remote OneCLI host/token or alt Anthropic endpoints without burdening
the standard linear flow with extra prompts.
Why: advanced configurations didn't fit cleanly into the existing
sequenced setup. PR #2030 took the "add another prompt step" route for
remote OneCLI; this approach instead routes those overrides through a
single source of truth so adding the next knob (alt endpoint, custom
host pattern, …) doesn't mean another prompt-or-skip decision.
setup/lib/setup-config.ts — schema (typed entry list with surface
'flag' | 'flag+ui'), name derivation (camelCase → NANOCLAW_UPPER_SNAKE
+ --kebab-case), seeded with --onecli-api-host, --onecli-api-token,
--anthropic-base-url, plus existing NANOCLAW_SKIP / NANOCLAW_DISPLAY_NAME
as flag-only entries.
setup/lib/setup-config-parse.ts — argv parser (--key value, --key=value,
--no-bool, -- terminator), env reader, applyToEnv() bridge that writes
resolved values back to process.env so existing step code keeps reading
env vars unchanged. Also --help printer.
setup/lib/setup-config-screen.ts — interactive menu loop. Entries
render with current value as hint; selecting one opens the right prompt
type (text / password for secrets / confirm / brightSelect for enums);
"Done" returns to the main flow.
setup/auto.ts — parses argv first (--help short-circuits before any
render), folds env+flags into process.env, then offers a welcome menu:
"Standard setup" (default) vs "Advanced". The onecli step branches on
NANOCLAW_ONECLI_API_HOST: if set, skips the local-vs-fresh prompt
entirely, runs pollHealth pre-flight, then calls runQuietStep with
--remote-url. Token, when provided, writes through to ONECLI_API_KEY in
.env. Welcome copy tightened (drops the duplicate wordmark/tagline) so
the bash → clack handoff reads as one flow.
setup/onecli.ts — cherries the --remote-url implementation from PR
run()) and generalizes writeEnvOnecliUrl into a writeEnvVar helper so
ONECLI_API_KEY follows the same upsert path.
nanoclaw.sh — forwards "$@" to setup:auto so flags reach the parser;
trims the redundant "Setting up your personal AI assistant" subtitle
and the bootstrap teach line so the pre-clack section isn't competing
with the clack intro for the same role.
Token plumbing only fires in --remote-url mode; local installs are
unauthenticated against localhost and don't need it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Allow connecting to an OneCLI gateway running on another host instead
of installing one locally. Adds a third choice ('Connect to a remote
OneCLI') alongside reuse/fresh in the setup wizard, prompts for the
remote URL, validates reachability before proceeding, and passes
--remote-url to the onecli step.
In onecli.ts: extracts installOnecliCliOnly() for the remote path
(installs the CLI binary but skips the gateway), exports pollHealth
for use by auto.ts, and handles --remote-url to configure api-host
and write ONECLI_URL to .env without running the full gateway install.
Absorbs battle-tested knowledge from the v2 skill into the upstream
add-signal: registration paths (new number + linked device), CAPTCHA
flow, VoIP SMS-first timing, Java prereq, config-lock warning, wiring
SQL for groups, not_member silent-drop fix, GroupV2 groupId extraction
note, and UUID-based platform ID format.
Corrects a factual error in the upstream: DM platform IDs are
signal:{UUID} (ACI), not phone numbers.
Removes the now-redundant add-signal-v2 skill.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
setup/register.ts had two bugs that prevented new channels from being
registered via `/manage-channels`:
1. createMessagingGroupAgent was called with the legacy field names
`trigger_rules` and `response_scope`. The SQL INSERT expects
`engage_mode` / `engage_pattern` / `sender_scope` / `ignored_message_policy`
(migration 010). Every register call failed with
`RangeError: Missing named parameter "engage_mode"` after the agent
and messaging group were partially created — leaving an orphaned pair.
Now mirrors scripts/init-first-agent.ts:wireIfMissing:
- Groups (is_group=1) default to engage_mode='mention' (bot only
responds when addressed).
- DMs (is_group=0) default to engage_mode='pattern' with '.' (respond
to every message).
- An explicit --trigger overrides the pattern regex.
2. The "normalize platform_id" block unconditionally prefixed
"<channel>:" even for native IDs like WhatsApp JIDs
("120363408974444974@g.us"), iMessage emails ("user@example.com"),
or Signal phones ("+15551234567") / Signal groups ("group:abc"). But
the router (src/router.ts:158) looks up messaging_groups by the raw
event.platformId from the adapter, which for these native adapters
never has a prefix. So the prefixed row was never matched — the
message was silently dropped with no "Message routed" log.
Extracted scripts/init-first-agent.ts:namespacedPlatformId into
src/platform-id.ts so both setup paths use the same heuristic (skip
the prefix for IDs containing '@', starting with '+', or starting
with 'group:'). Prevents future drift between the two paths.
Tested by: re-running `setup/index.ts --step register` for a WhatsApp
group JID, confirming the row is created with correct engage fields
and matching platform_id, then sending a test message and observing
"Message routed" with the right agent group.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds /add-gcal-tool — a sibling of /add-gmail-tool that installs
@cocal/google-calendar-mcp with the same OneCLI stub-file pattern. Skill
applies the Dockerfile + TOOL_ALLOWLIST changes at install time; trunk
stays clean so users who never run the skill don't carry the calendar
MCP in their image.
Dropped the Phase 5 dry-run section since it hardcoded a per-install
image tag slug and duplicated Phase 4's live agent test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The upstream precedence fix (5845a5a) made agent_groups.agent_provider and
sessions.agent_provider authoritative for host-side provider contribution
(per-session mount, env passthrough), but those DB values don't propagate
into the group's container.json — and the in-container runner reads
`provider` from container.json, not from the DB. That caused a confusing
failure mode: flipping the DB column to 'codex', rebuilding, and
restarting still spawned a Claude runner because container.json had no
provider field. The old skill wording ("container receives AGENT_PROVIDER
from the resolved value") overstated the integration.
Update add-codex and add-opencode "Per group / per session" sections to
say: set `"provider": "<name>"` in the group's container.json — that's
the source the runner reads. Keep the DB columns documented for the
host-side contribution they actually drive, and spell out the
session → group → container.json → 'claude' fallback so the precedence
is still discoverable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 of the SKILL.md already contains the Dockerfile + TOOL_ALLOWLIST
edit instructions with an "ALREADY APPLIED" short-circuit. Keeping those
edits out of trunk means users who never run /add-gmail-tool don't carry
the Gmail MCP package in their image.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>