Token refresh updated internal array and .env file but not process.env,
so docker exec -e kept injecting the stale startup token. Now syncs
to process.env after refresh. Container kill on refresh is no longer
needed since docker exec picks up the latest token each turn.
- Replace better-sqlite3 with bun:sqlite (native, no native addon build)
- Change all spawn('node') to spawn('bun') for agent processes
- Update package.json scripts: node→bun, tsx→bun, npm→bun
- Add bun-types for tsc compatibility
- Add bun:sqlite→better-sqlite3 shim for vitest (tests run on Node.js)
- Update Dockerfile: install bun alongside Node.js (CLIs need Node)
- Update setup/platform.ts: getNodePath() resolves bun binary
- Remove better-sqlite3 from production dependencies (devDep only for tests)
When OAuth tokens rotate or refresh, all reviewer containers are
automatically removed. The next reviewer turn recreates them with
the latest token, preventing 401 auth failures.
Uses callback pattern to avoid circular dependencies:
- token-rotation.ts: onTokenRotated(cb)
- token-refresh.ts: onTokenRefreshed(cb)
- index.ts: registers recreateAllReviewerContainers as callback
Discord file attachments are downloaded to data/attachments/ on the
host. Mount this directory read-only in the reviewer container at the
same absolute path so both owner and reviewer can access uploaded
files (WAV, images, etc.) using the same path.
The credential proxy cached the OAuth token at startup, but Anthropic
no longer supports raw OAuth Bearer tokens on the API. The Claude Code
SDK handles OAuth internally, so the proxy was breaking auth.
Now injects the real token both at container creation and at each
docker exec, so token rotation is picked up without recreating the
container.
Git worktrees reference the parent repo's .git directory via absolute
path. Without mounting it, all git commands fail inside the container.
Now reads the .git file, resolves the parent .git path, and mounts it
read-only at the same host path so references resolve naturally.
Container mounted data/sessions/{folder}/ but CLAUDE.md was written
to data/sessions/{folder}-reviewer/.claude/ by paired-execution-context.
The reviewer never saw its prompts (verdict protocol, paired room rules).
Now mounts {folder}-reviewer/ to match, so the CLAUDE.md with platform
and paired room prompts is visible inside the container.
Container reviewer was running without prompts because the container
path in agent-runner.ts skipped prepareGroupEnvironment() entirely.
This meant no CLAUDE.md (platform + paired room prompts + memory
briefing) and no roomRoleContext were passed to the container.
- Add prepareContainerSessionEnvironment() to write CLAUDE.md, sync
skills, and ensure settings.json for the reviewer session directory
- Pass roomRoleContext through ReviewerContainerInput so the runner
can prepend the [ROOM_ROLE] header
- Add roomRoleContext field to ReviewerContainerInput interface
Instead of docker run --rm per turn (~300ms overhead), containers are
now created once per channel with docker run -d and reused across turns.
Each turn runs via docker exec inside the warm container.
- ensurePersistentContainer: creates or reuses container per channel
- stopReviewerContainer: explicit stop for /clear or shutdown
- Container stays alive with sleep infinity, turns exec node runner
- cleanupOrphans handles stale containers on restart
Container runner was calling onOutput with fire-and-forget (void),
so work items were still in 'produced' status when the drain loop
checked after container exit. This caused the same message to be
delivered through both reviewer and owner channels.
Now chains onOutput calls and waits for completion before resolving,
matching the host-process runner's outputChain pattern.
Host paths passed via envOverrides don't exist inside the container,
causing ENOENT when the SDK tries to spawn with a non-existent CWD.
Now remaps EJCLAW_WORK_DIR to /workspace/project and CLAUDE_CONFIG_DIR
to /home/node/.claude (matching the container mount points).
- Compare git HEAD at reviewer approval vs owner finalize to detect
new code changes after DONE verdict. Re-trigger reviewer if changed,
complete if unchanged (commit-only is fine).
- Record approved HEAD in source_ref when reviewer says DONE
- Add pnpm store mount, pre-flight checks, idle timeout for containers
- Clean up orphaned containers on shutdown
- Detect pnpm projects and mount global store read-only so hardlinks resolve
- npm/yarn/bun need no extra mounts (node_modules is self-contained)
- Pre-flight check: verify Docker running + image exists (cached after first call)
- Add activity-based idle timeout (reset on stdout/stderr like host runner)
- Clean up orphaned containers on shutdown (not just startup)
- Add UNIFIED_MODE flag (default on, disable with UNIFIED_MODE=0)
- Register all 3 Discord bots in one process (claude, codex, review)
- Load all registered groups regardless of agent_type (codex/owner priority)
- Start credential proxy and container cleanup at unified startup
- shouldServiceProcessChat returns true in unified mode
- Add findChannelByName for role-based response routing
- Backward compatible: UNIFIED_MODE=0 restores per-service behavior
- Rewrite codex-runner to use `codex app-server` JSON-RPC instead of
`codex exec`. Supports streaming (item/agentMessage/delta), session
persistence (thread/start, thread/resume), and mid-turn message
injection (turn/steer with IPC polling during execution).
- Fix token refresh not syncing to per-group session directories,
causing 401 errors on running agents.
- Fix typing indicator staying on when codex returns null result
by always clearing typing on any streaming callback.
- Update README and CLAUDE.md to reflect dual-service architecture,
host process mode, and codex app-server integration.
- Add token-refresh module that auto-renews Claude OAuth tokens 30min before expiry
- Fix Codex session loss on service restart by resuming previous sessions
- Inject nanoclaw MCP server into Codex config.toml for send_message/schedule_task
- Sanitize ANTHROPIC_API_KEY and CLAUDE_CODE_OAUTH_TOKEN from Codex subprocess env
CODEX_CONFIG_DIR is not recognized by Codex CLI. CODEX_HOME is the correct
env var that controls the root directory for sessions, state, and config.
Without this, all groups sharing the same workDir would have their sessions
collide, causing resume --last to pick up wrong group's session.
- Make STORE_DIR, DATA_DIR, GROUPS_DIR configurable via env vars
(NANOCLAW_STORE_DIR, NANOCLAW_DATA_DIR, NANOCLAW_GROUPS_DIR)
for running two instances from one codebase
- Remove broken 'both' agent type sequential loop from processGroupMessages
- Delete dead code: container-runtime, mount-security, credential-proxy,
Dockerfiles, and related config constants (~1,085 lines removed)
- Add codex instance launchd plist template
- Enrich spawned process PATH with ~/.npm-global/bin so codex CLI is
findable when running under launchd
- Clear typing indicator in streaming callback after sending response,
instead of waiting for runner process to fully exit
- Read/write agent_type column in getAllRegisteredGroups/setRegisteredGroup
(was missing, causing all groups to fall back to claude-code)
- Strip CLAUDECODE env var to prevent nested session detection
- Add node binary path to spawned process PATH for launchd compatibility
- Clear stale container-era sessions that reference /workspace/group
- Rewrite container-runner.ts to spawn node processes directly instead
of Docker/Apple Container
- Runner paths configurable via env vars (NANOCLAW_GROUP_DIR, etc.)
with container-path defaults for backwards compat
- Pass real credentials directly (no credential proxy needed)
- Remove ensureContainerSystemRunning() and startCredentialProxy()
from startup
- Add build:runners script for building agent-runner and codex-runner
- Fix TS strict mode errors in agent-runner ipc-mcp-stdio.ts
- Add Codex as alternative agent type (agentType: 'codex' per group)
- Codex container runner with session resume and 100-turn limit
- Dual Discord bot support (DISCORD_BOT_TOKEN + DISCORD_CODEX_BOT_TOKEN)
- Each bot handles only its agentType's registered groups
- Discord typing indicator refreshes every 8s (was single-shot, expired after ~10s)
- Agent runner 100-turn limit to prevent infinite bot-to-bot loops
- Apple Container runtime support (convert-to-apple-container skill)
- Session commands (/compact) support
- DB migration for agent_type column
* feat: implement credential proxy for enhanced container environment isolation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address PR review — bind proxy to loopback, scope OAuth injection, add tests
- Bind credential proxy to 127.0.0.1 instead of 0.0.0.0 (security)
- OAuth mode: only inject Authorization on token exchange endpoint
- Add 5 integration tests for credential-proxy.ts
- Remove dangling comment
- Extract host gateway into container-runtime.ts abstraction
- Update Apple Container skill for credential proxy compatibility
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: scope OAuth token injection by header presence instead of path
Path-based matching missed auth probe requests the CLI sends before
the token exchange. Now the proxy replaces Authorization only when
the container actually sends one, leaving x-api-key-only requests
(post-exchange) untouched.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: bind credential proxy to docker0 bridge IP on Linux
On bare-metal Linux Docker, containers reach the host via the bridge IP
(e.g. 172.17.0.1), not loopback. Detect the docker0 interface address
via os.networkInterfaces() and bind there instead of 0.0.0.0, so the
proxy is reachable by containers but not exposed to the LAN.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: bind credential proxy to loopback on WSL
WSL uses Docker Desktop with the same VM routing as macOS, so
127.0.0.1 is correct and secure. Without this, the fallback to
0.0.0.0 was triggered because WSL has no docker0 interface.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: detect WSL via /proc instead of env var
WSL_DISTRO_NAME isn't set under systemd. Use
/proc/sys/fs/binfmt_misc/WSLInterop which is always present on WSL.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: shadow .env file in container to prevent agents from reading secrets
The main agent's container mounts the project root read-only, which
inadvertently exposed the .env file containing API keys. Mount /dev/null
over /workspace/project/.env to shadow it — secrets are already passed
via stdin and never need to be read from disk inside the container.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: adapt .env shadowing and runtime for Apple Container
Apple Container (VirtioFS) only supports directory mounts, not file
mounts. The previous /dev/null host-side mount over .env crashes with
VZErrorDomain "A directory sharing device configuration is invalid".
- Dockerfile: entrypoint now shadows .env via mount --bind inside the
container, then drops privileges via setpriv to the host UID/GID
- container-runner: main containers skip --user and pass RUN_UID/RUN_GID
env vars so entrypoint starts as root for mount --bind
- container-runtime: switch to Apple Container CLI (container), fix
cleanupOrphans to use container list --format json
- Skill: add Dockerfile and container-runner.ts to
convert-to-apple-container skill (v1.1.0)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: revert src to Docker runtime, keep Apple Container in skill only
The source files should remain Docker-compatible. The Apple Container
adaptations live in the convert-to-apple-container skill and are applied
on demand.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Support ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN for custom API endpoints
- Add documentation for third-party/open-source model usage
Co-authored-by: wenglixin <wenglixin@menusifu.cn>
* fix(db): remove unique constraint on folder to support multi-channel agents
* ci: implement automated skill drift detection and self-healing PRs
* fix: align registration logic with Gavriel's feedback and fix build/test issues from Daniel Mi
* style: conform to prettier standards for CI validation
* test: fix branch naming inconsistency in CI (master vs main)
* fix(ci): robust module resolution by removing file extensions in scripts
* refactor(ci): simplify skill validation by removing redundant combination tests
* style: conform skills-engine to prettier, unify logging in index.ts and cleanup unused imports
* refactor: extract multi-channel DB changes to separate branch
Move channel column, folder suffix logic, and related migrations
to feat/multi-channel-db-v2 for independent review. This PR now
contains only CI/CD optimizations, Prettier formatting, and
logging improvements.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Containers had no TZ set, so any time-aware code inside ran in UTC while
the host interpreted bare timestamps as local time. Now TIMEZONE from
config.ts is passed via -e TZ= to the container args.
Also rejects Z-suffixed or offset-suffixed timestamps in the container's
schedule_task validation, since bare timestamps are expected to be local
time and silently accepting UTC suffixes would cause an offset mismatch.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The container agent-runner had 'Andy' hardcoded as the sender name in
archived conversation transcripts. This ignored the configurable
ASSISTANT_NAME setting, so users who changed their assistant's name
(via .env or config) would still see 'Andy' in transcripts.
- Add assistantName field to ContainerInput interface (both host and
container copies)
- Pass ASSISTANT_NAME from config through to container in index.ts
and task-scheduler.ts
- Thread assistantName through createPreCompactHook and
formatTranscriptMarkdown in the agent-runner
- Use 'AssistantNameMissing' as fallback instead of 'Andy' so a
missing name is visible rather than silently wrong
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The main group's project root was mounted read-write, allowing the
container agent to modify host application code (e.g. dist/container-runner.js)
to inject arbitrary mounts on next restart — a full sandbox escape.
Fix: mount the project root read-only. Writable paths the agent needs
(group folder, IPC, .claude/) are already mounted separately. The
agent-runner source is now copied into a per-group writable location
so agents can still customize container-side behavior without affecting
host code or other groups.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
copyFileSync crashes with EISDIR when a skill contains subdirectories
like scripts/. Skills support nested folders (scripts/, examples/,
templates/) per the Claude Code spec. Use fs.cpSync to handle the
complete skill structure.
Move all container-runtime-specific logic (binary name, mount args,
stop command, startup check, orphan cleanup) into a single file so
swapping runtimes only requires replacing this one file.
Neutralize "Apple Container" references in comments and docs that
would become incorrect after a runtime swap. References that list
both runtimes as options are left unchanged.
No behavior change — Apple Container remains the default runtime.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: WA 515 stream error reconnect exiting early before key sync
Pass isReconnect flag on 515 reconnect so the registered-creds check
doesn't bail out before the handshake completes (caused "logging in..."
hang after successful pairing).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: container permission errors on Docker with non-default uid
Make /home/node world-writable in the Dockerfile so the SDK can write
.claude.json. Add --user flag matching host uid/gid in container-runner
so bind-mounted files are accessible. Skip when running as root (uid 0),
as the container's node user (uid 1000), or on native Windows.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: write ASSISTANT_NAME to .env during setup
When a custom assistant name is chosen, persist it to .env so config.ts
picks it up at runtime. Uses temp file for cross-platform sed
compatibility (macOS/Linux/WSL).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>