Commit Graph

75 Commits

Author SHA1 Message Date
gavrielc
356a4d0a9f feat(setup): render Telegram pairing code in a focused banner
The pair-telegram step emits PAIR_TELEGRAM_ISSUED / _NEW_CODE /
_ATTEMPT blocks meant for /setup skill parsing — dumping them raw in
setup:auto left the operator squinting at key/value clutter. Intercept
the stream line-by-line, suppress the block framing, and print just
the 4-digit code inside a box with a short instruction. Wrong-code
attempts and the final success block also get short human lines.
parseStatus still runs on the full buffered output at close so
PLATFORM_ID / ADMIN_USER_ID flow through unchanged to init-first-agent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:22:53 +03:00
gavrielc
5a472c4155 fix(setup): print bot URL alongside the deep-link attempt
Headless / SSH / WSL users won't have \`open\` or \`xdg-open\` wired up,
so the deep-link fails silently and they have no clue where to go.
Always print https://t.me/<username> so the URL is at least clickable
or copy-pasteable from the terminal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:19:23 +03:00
gavrielc
e7d798b00d feat(setup): validate Telegram token via getMe and deep-link to bot
After the token is in .env, call
https://api.telegram.org/bot<TOKEN>/getMe — if ok, extract the bot's
username and \`open tg://resolve?domain=<username>\` so the Telegram
desktop app lands on the bot chat. When pair-telegram prints the
4-digit code a moment later, the user just types it into the already-
open chat instead of hunting for their bot.

Falls back to https://t.me/<username> if the tg:// scheme isn't
registered, and just warns-and-continues if getMe fails (network
hiccup shouldn't block setup).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:17:42 +03:00
gavrielc
92c28a956d feat(setup): run init-first-agent after Telegram pairing
pair-telegram only identifies the chat and operator — it returns
PLATFORM_ID and ADMIN_USER_ID but doesn't create the agent group,
grant owner, or send the welcome. scripts/init-first-agent.ts does
that, matching the pattern the /new-setup skill already uses for
channel wiring.

Also prompts for the agent's own name (default: Nano), overridable
via NANOCLAW_AGENT_NAME. displayName is hoisted out of the cli-agent
block so both cli-agent and channel wiring share the value.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:11:35 +03:00
gavrielc
9c7e1d02af feat(setup): optional Telegram wiring in setup:auto
After cli-agent, prompt the user to connect a messaging app. For now
only Telegram is offered; "skip" falls through to the existing CLI
flow.

setup/add-telegram.sh runs the scriptable half of /add-telegram: fetch
the channels branch, copy the adapter + pair-telegram files, append
the self-registration import, install @chat-adapter/telegram@4.26.0
(pinned to match the skill), rebuild, collect TELEGRAM_BOT_TOKEN via
silent paste, write .env + data/env/env, and kick the service so the
new adapter is live. Idempotent throughout.

setup:auto then runs the existing `pair-telegram` step with
--intent main. The step emits the 4-digit code in its status stream,
which is already forwarded to stdout by runStep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:04:14 +03:00
gavrielc
c87cd250b2 feat(verify): end-to-end agent ping via CLI channel
Verify now runs \`pnpm run chat ping\` silently and checks for a reply.
Emits AGENT_PING=ok|no_reply|socket_error|skipped; skipped when the
service isn't running or no groups are wired (those already fail the
verify via other checks). Kills the child after 90s so a wedged
container can't hang setup (chat.ts's own 120s timeout is too long
here). setup:auto surfaces AGENT_PING!=ok in its failure summary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 23:52:51 +03:00
gavrielc
85faa3eab0 fix(setup): rephrase display-name prompt
"Your agents" — the name is stored on the operator's user row and
applies to every future agent they wire up, not just this scratch CLI
one.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 23:49:28 +03:00
gavrielc
d02001e144 feat(setup): prompt for display name, hardcode agent persona
Before the cli-agent step, ask the operator what the agent should
call them (defaults to \$USER). The agent's own persona name is
hardcoded to "Terminal Agent" — this is the scratch CLI agent, not
one of the operator's real personas. NANOCLAW_DISPLAY_NAME still
skips the prompt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 23:46:49 +03:00
gavrielc
81838bbb34 fix(setup): clarify silent-paste prompt
Explicitly tell the user that nothing appears on screen as they paste
and that a single Enter submits. "(Input is hidden for safety.)" was
ambiguous — users kept waiting for a visible confirmation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 23:43:43 +03:00
gavrielc
1c748f1f2b refactor(setup): drop timezone step from setup:auto chain
The timezone step blocked the scripted flow on headless servers where
the resolved TZ was UTC (interactive /setup confirms, setup:auto had
to bail). Drop it from the chain — host TZ defaults to whatever the
OS reports. Users who need an explicit override run the step on
demand: `pnpm exec tsx setup/index.ts --step timezone -- --tz <zone>`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:18:38 +03:00
exe.dev user
be6cec59ad fix(setup): auto-recover from stale docker group mid-session
- container: install Docker via setup/install-docker.sh when missing,
  distinguish socket EACCES from daemon-down so we bail fast instead of
  polling 60s, and re-exec the step under `sg docker` when usermod hasn't
  reached the current shell.
- auto: after the container step, re-exec the whole driver under `sg
  docker` (with a NANOCLAW_REEXEC_SG guard) so onecli/service/verify also
  get docker-group access without a re-login. Surface the new
  docker_group_not_active error from the container step.
- service: when the systemd user manager has a stale group list, auto-
  apply \`sudo setfacl -m u:\$USER:rw /var/run/docker.sock\` so the service
  can start without waiting for the next login.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:55:04 +00:00
gavrielc
e86d0d93dd feat(setup): wire CLI agent in setup:auto
Chains `cli-agent` (wraps scripts/init-cli-agent.ts) between service and
verify. Without this wiring, the socket at data/cli.sock accepts the
connection but there's no agent group routed to `cli/local`, so
`pnpm run chat` hangs waiting for a reply.

Defaults: display name from NANOCLAW_DISPLAY_NAME env, falling back to
\$USER then "Operator". Agent persona name from NANOCLAW_AGENT_NAME,
defaulting to the display name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 18:45:52 +03:00
gavrielc
b0cae1ba4c feat(setup): chain register-claude-token.sh into setup:auto
Runs after the OneCLI install step and before mounts/service. Skips
silently when `onecli secrets list` already reports an Anthropic
secret, so re-running setup:auto on a configured install is a no-op.
Child process uses stdio:inherit so the menu + browser sign-in flow
work normally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:41:29 +03:00
gavrielc
ee5995ae16 feat(setup): add register-claude-token.sh
Bash helper that registers an Anthropic credential in OneCLI via three
paths: Claude subscription (runs `claude setup-token` under script(1)
for PTY capture), paste an existing sk-ant-oat… OAuth token, or paste
an sk-ant-api… API key.

On bash 4+ the `claude setup-token` command is pre-filled in the
readline buffer so Enter submits it. On bash 3.2 (macOS default
/bin/bash) we fall back to a plain confirmation prompt. Token
extraction strips ANSI + TTY-wrap line breaks and anchors on
sk-ant-oat…AA with a length cap (via perl; BSD grep caps {n,m} at 255).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:38:43 +03:00
gavrielc
3ce4101cd9 feat(setup): chain OneCLI install in setup:auto
The install half of the OneCLI step is fully scriptable (the gateway
and CLI install themselves via `curl | sh`, PATH + api-host + .env
updates are idempotent). Register the Anthropic secret is still
interactive — the auto driver leaves that for `/setup` §4 to handle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:13:39 +03:00
gavrielc
2311721375 feat(setup): add scripted setup driver and auto-start Docker
`pnpm run setup:auto` chains the deterministic setup steps (environment
→ timezone → container → mounts → service → verify) by spawning the
existing per-step CLI and parsing its status blocks. Config via env:
NANOCLAW_TZ, NANOCLAW_SKIP.

Credentials + channel install + /manage-channels stay interactive —
verify reports what's left and exits 0 rather than failing the driver.

Also have the container step try to start Docker when it's installed
but not running (open -a Docker on macOS, sudo systemctl start docker
on Linux) and poll `docker info` for up to 60s before giving up. Both
/setup and setup:auto pick this up automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:04:48 +03:00
gavrielc
010722803f refactor(setup): drop Apple Container support
Apple Container is no longer supported — the runtime abstraction in
src/container-runtime.ts is already Docker-only. Remove the remaining
setup-time branches that probed for it: the Apple Container runtime
option in the container build step, the APPLE_CONTAINER field emitted
by the environment check, and the `command -v container` probe in
verify. `--runtime docker` still parses for backwards compatibility
with the /setup skill.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:02:22 +03:00
Koshkoshinsk
1f7508f2aa refactor(skills): merge /new-setup-2 into unified /new-setup
Collapses the two-phase setup into a single linear skill: steps 1-6
(prereqs through end-to-end CLI ping) run straight through, steps 7-13
(naming, timezone, channel wiring, mounts, QoL, done) are skippable.
Drops the "chat now vs. continue" branch point — after the ping the
flow emits "Test Agent success, proceeding with setup" and continues
directly into the naming questions.

Also updates stale `/new-setup-2` header comments in setup/install-*.sh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:20:19 +03:00
gavrielc
77e6d3bc66 Revert "refactor(skills): merge /new-setup-2 into unified /new-setup"
This reverts commit 483969a194.
2026-04-21 15:20:06 +03:00
Koshkoshinsk
483969a194 refactor(skills): merge /new-setup-2 into unified /new-setup
Collapses the two-phase setup into a single linear skill: steps 1-6
(prereqs through end-to-end CLI ping) run straight through, steps 7-13
(naming, timezone, channel wiring, mounts, QoL, done) are skippable.
Drops the "chat now vs. continue" branch point — after the ping the
flow emits "Test Agent success, proceeding with setup" and continues
directly into the naming questions.

Also updates stale `/new-setup-2` header comments in setup/install-*.sh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 10:37:06 +00:00
gavrielc
6c26c0413a feat(router,cli): replyTo override + CLI admin-transport flows
- InboundEvent gains an optional replyTo; router stamps the row's address
  fields from it when set, so replies can route to a different channel than
  the one the inbound came in on.
- ChannelSetup adds onInboundEvent for admin-transport adapters that build
  the full event themselves.
- CLI wire format accepts {text, to, reply_to}. Routed messages go through
  onInboundEvent and do not evict an active chat client.
- init-first-agent hands the DM welcome to the running service via
  data/cli.sock — synchronous wake, no sweep wait. Fails loudly if the
  service is down; no silent fallback.
- Split the CLI scratch-agent bootstrap into scripts/init-cli-agent.ts;
  init-first-agent is DM-only.

Agents cannot set replyTo: it lives only on the inbound/router seam and is
consumed once when writing messages_in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:30:47 +03:00
Koshkoshinsk
dadf258136 feat(new-setup-2): add per-channel bundled install scripts
Twelve idempotent install scripts matching install-telegram.sh's shape,
one per channel in /new-setup-2's pick list (except Emacs, which uses
a git-merge install flow). Each bundles preflight + fetch + copy +
register + pnpm install + build so a single allowlisted bash call can
replace a chain of permission prompts. Linear's also patches
chat-sdk-bridge.ts for catchAll forwarding; Matrix's runs the
post-install ESM-extension dist patch; WhatsApp-native's covers the
deterministic install portion only — QR/pairing auth still lives in
/add-whatsapp.

Scripts only; new-setup-2/SKILL.md integration deferred pending a
decision on whether to generalize the set-env pattern from 712a0e1
across the Chat SDK channels (each /add-<channel>/SKILL.md's
Credentials section has a similar unapprovable shell chain).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:45:14 +00:00
Koshkoshinsk
712a0e1e01 feat(new-setup): wrap node/docker installs and add generic set-env step
Adds three allowlist-friendly setup helpers so /new-setup and /new-setup-2
don't hit unmatchable commands during a fresh install:

- setup/install-node.sh — idempotent Node 22 install wrapper (macOS via brew,
  Linux via NodeSource + apt). Replaces the raw `curl | sudo -E bash -` flow
  whose stdin-consuming `bash -` segment can't be pre-approved.
- setup/install-docker.sh — same pattern for Docker (brew --cask on macOS,
  get.docker.com on Linux + usermod).
- setup/set-env.ts — generic `--step set-env` that writes KEY=VALUE to .env
  (and optionally syncs to data/env/env) so channel-install flows don't
  invent `grep && sed && rm` pipelines, which split at each && and can't be
  tightly allowlisted.

new-setup-2's Telegram path now uses set-env for TELEGRAM_BOT_TOKEN and
explicitly skips /add-telegram's Credentials section. new-setup step 1 and
step 2 now call the install wrappers; the raw curl/apt entries are gone from
the allowed-tools list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:19:09 +00:00
Koshkoshinsk
a29f3e5cf4 feat(new-setup-2): bundle Telegram install into one script
Extract the /add-telegram preflight + install commands into
setup/install-telegram.sh so /new-setup-2 can run the adapter install
programmatically when the user picks Telegram, then hand off to
/add-telegram for credentials and pairing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:18:45 +00:00
Koshkoshinsk
a1079da877 fix(new-setup): always source ONECLI_URL from installer stdout
Match v1 behavior: drop getApiHost() (which was returning the CLI default
https://app.onecli.sh) and always extract the gateway URL from the install
script's stdout, then apply it via onecli config set api-host and .env.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 07:02:25 +00:00
exe.dev user
96d7656112 refactor(new-setup): rewrite probe in pure bash, drop unavailable fallback
The probe now returns a real snapshot from second zero, so every step
consults real probe fields instead of falling back to "run every step
blindly" when Node isn't installed. Also drops the redundant
CLI_AGENT_WIRED field (it gated the last step on its own end-state) and
scopes timezone out of the probe (timezone is not part of /new-setup).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:40:53 +00:00
Koshkoshinsk
5542107b9e fix(new-setup): align onecli health path and rework auth flow
onecli step:
- Poll /api/health (was /health) so the step's health check matches
  the probe's. On hosted OneCLI (app.onecli.sh) the old path returned
  non-ok, flagging the gateway as "degraded" even though install
  succeeded.
- Drop the "try `onecli start`" hint — no such subcommand exists and
  it sent the skill off chasing fabricated commands. A failed health
  poll is demoted to a soft warning; the auth step surfaces a real
  outage via `onecli secrets list`.

SKILL.md step 4: rewrite to match the /setup skill's pattern — the
user generates the token themselves, picks dashboard or CLI to
register it with OneCLI, and the skill verifies via `auth --check`.
Tokens no longer travel through chat.

Co-Authored-By: Koshkoshinsk <daniel.milliner@gmail.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:10:21 +00:00
Koshkoshinsk
0992979c5a feat(new-setup): probe host-deps and skip bootstrap when already installed
Probe now emits HOST_DEPS (ok|missing) based on whether
node_modules/better-sqlite3/build/Release/better_sqlite3.node exists
— the canonical proof that `pnpm install` ran and the native build
step succeeded. Step 1 (Node bootstrap) skips when HOST_DEPS=ok
instead of always re-running setup.sh. Probe now genuinely routes
step 1 the same way it routes every other step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:01:05 +00:00
Koshkoshinsk
77624d7854 fix(new-setup): wrap probe in shell script for single-command permission check
The chained `&& / ||` inline command tripped Claude Code's per-operation
permission check. Move the Node-missing fallback into setup/probe.sh so
the skill's `!` block is a single `bash setup/probe.sh` call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:05:54 +00:00
Koshkoshinsk
b3e8b2e047 fix(new-setup): run probe before pnpm is installed
Port probe to zero-dep plain ESM (setup/probe.mjs) so /new-setup can
inject dynamic context on a fresh machine where pnpm/node_modules
don't yet exist. Skill falls back to a STATUS: unavailable block if
Node itself isn't on PATH, and the flow treats that as "run every
step from 1" (each step is idempotent).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:03:49 +00:00
Koshkoshinsk
6db81554bf feat(new-setup): add probe step for dynamic context injection
Single upfront parallel scan the SKILL.md renders via `!`...`` so Claude
sees system state before generating its first response. Each field maps
to a routing decision (skip/run/ask) for a downstream step.

Reports: OS, SHELL, DOCKER + IMAGE_PRESENT, ONECLI_STATUS + ONECLI_URL,
ANTHROPIC_SECRET, SERVICE_STATUS, CLI_AGENT_WIRED, INFERRED_DISPLAY_NAME,
TZ_STATUS + TZ_ENV + TZ_SYSTEM. Runs in ~200ms on a fully-set-up host.

Not a replacement for per-step idempotency — each step keeps its own
checks since probe is a snapshot and can go stale by execution time.

Uses /api/health (OneCLI's actual endpoint). Anthropic secret check
uses the CLI client so it works whenever onecli is installed, even if
the direct HTTP health probe fails (different network paths).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:43:38 +00:00
Koshkoshinsk
01389ff8fc feat(new-setup): add onecli, auth, and cli-agent dispatcher steps
Aggregates the loose OneCLI install, secret registration, and first-agent
wiring commands from /setup into three new dispatcher steps. Adds
--cli-only mode to init-first-agent so /new-setup can reach a working
2-way CLI chat with the bare minimum.

- setup/onecli.ts: idempotent install + PATH + api-host + .env, polls /health
- setup/auth.ts: --check verifies secret; --create --value registers it
- setup/cli-agent.ts: wraps init-first-agent --cli-only
- scripts/init-first-agent.ts: --cli-only mode; DM mode unchanged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:43:35 +00:00
gavrielc
4857512267 refactor(v2): move setup/groups.ts off trunk + drop pino dep
setup/groups.ts is whatsapp-only — its inline syncScript imports baileys
and pino to fetch group metadata via Baileys.groupFetchAllParticipating.
On trunk it was a no-op for non-whatsapp users (returned early without
auth) and the only thing keeping pino alive.

Removed:
- setup/groups.ts (lives on `channels` branch; restored by /add-whatsapp-v2)
- `groups` STEPS entry from setup/index.ts
- pino from package.json (no longer used outside the moved file)

/add-whatsapp-v2 skill updated to copy setup/groups.ts and register both
groups + whatsapp-auth in setup/index.ts STEPS, install pino@9.6.0 along
with baileys + qrcode.

Verified: build clean, 326 host tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:34:16 +03:00
gavrielc
437ba63a2f refactor(v2): move channel adapters off v2 trunk
v2 ships with no channels baked in. All channel adapters (discord, slack,
telegram + helpers, whatsapp, whatsapp-cloud, gchat, github, imessage,
linear, matrix, resend, teams, webex) and their channel-specific setup
steps (pair-telegram, whatsapp-auth) now live on the `channels` branch
and get copied in via /add-*-v2 skills.

Removed:
- src/channels/{discord,slack,telegram*,whatsapp*,gchat,github,imessage,linear,matrix,resend,teams,webex}.ts
- setup/{pair-telegram,whatsapp-auth}.ts
- 14 channel-specific deps from package.json (@chat-adapter/*, @beeper/*,
  @bitbasti/*, @resend/chat-sdk-adapter, @whiskeysockets/baileys,
  chat-adapter-imessage, qrcode, @chat-adapter/state-memory unused)
- Their corresponding STEPS entries from setup/index.ts
- Channel imports from src/channels/index.ts

Kept:
- Channel infra: adapter.ts, channel-registry.ts (+ test), chat-sdk-bridge.ts,
  ask-question.ts, an empty-imports index.ts
- Chat SDK runtime (`chat`) for channels that copy in via Chat SDK bridge
- @chat-adapter/shared promoted from transitive to direct dep
  (channel-registry.ts uses NetworkError from it)

Verified: pnpm run build clean, 326 host tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:20:28 +03:00
gavrielc
c5d0ef8b4f feat(v2): migrate container runtime to Bun, improve image build surface
Container side:
- agent-runner switches to Bun. Drops better-sqlite3 (native compile gone),
  drops tsc build step in-image AND the tsc-on-every-session-wake in the
  entrypoint — bun runs src/index.ts directly. bun:sqlite replaces
  better-sqlite3; cross-mount DB invariants (journal_mode=DELETE, busy_timeout)
  preserved. Named params converted from @name to $name because bun:sqlite
  does not auto-strip the prefix the way better-sqlite3 does.
- Tests ported from vitest to bun:test (only describe/it/expect/before/afterEach
  used, API-compatible). vitest.config.ts excludes container/agent-runner/.
- bun.lock replaces pnpm-lock.yaml + pnpm-workspace.yaml under
  container/agent-runner/. Host pnpm workspace does NOT include this tree.

Dockerfile improvements (independent of Bun but bundled while touching the file):
- tini as PID 1 for correct SIGTERM propagation (prevents half-written
  outbound.db on shutdown).
- Extracted entrypoint.sh — readable and diffable vs the old inline printf.
- BuildKit cache mounts for apt + bun install + pnpm install.
- --no-install-recommends on apt, pinned CLAUDE_CODE_VERSION, AGENT_BROWSER,
  VERCEL, BUN_VERSION.
- CJK fonts (~200MB) behind ARG INSTALL_CJK_FONTS=false; build.sh reads from
  .env; setup/container.ts reads the same .env so /setup and manual rebuild
  stay in sync.
- PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 in case any postinstall tries to pull a
  redundant Chromium.
- /home/node 755 (was 777).

Host side:
- src/container-runner.ts dynamic spawn command collapses from
  `pnpm exec tsc --outDir /tmp/dist … && node /tmp/dist/index.js` to
  `exec bun run /app/src/index.ts` — cold start ~200-500ms faster per wake.

CI:
- oven-sh/setup-bun@v2 alongside Node/pnpm. Adds explicit container
  typecheck (was documented in CLAUDE.md, not enforced) and `bun test` for
  agent-runner tests.
2026-04-17 11:38:01 +03:00
gavrielc
22498ae69c fix: remaining npm→pnpm gaps + dockerignore for pnpm symlinks
- container/.dockerignore (new): exclude agent-runner/node_modules and
  agent-runner/dist so COPY agent-runner/ ./ doesn't clobber the
  pnpm-installed node_modules with host directories. Under npm's flat
  layout this was forgiving; under pnpm's symlink layout it's a hard
  conflict (overlay2 cannot copy onto a symlink target).
- setup/{groups,service}.ts: execSync('pnpm run build') not npm.
- setup/index.ts: usage string.
- scripts/*.ts: usage comments + seed-discord final log.
- .claude/settings.json: permission allowlist entries.
- .claude/skills/{add-whatsapp-v2,add-dashboard}/SKILL.md: docs.
- container/skills/{frontend-engineer,vercel-cli,self-customize}/SKILL.md:
  agent-facing docs still told the container agent to run npm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 09:53:00 +03:00
gavrielc
e55ed0f4e8 fix(whatsapp): upgrade Baileys 6.7→6.17, fix proto import and 515 restart
Baileys 6.7.21 silently failed the pairing handshake. Upgrade to 6.17.16
which fixes this. Three related issues:

1. proto is no longer a named ESM export in 6.17.x — use createRequire
   to import via CJS (matching the proven v1 pattern).
2. Setup auth script didn't handle the 515 stream restart that WhatsApp
   sends after successful pairing. Refactored to reconnect (matching v1's
   connectSocket(isReconnect) pattern) instead of hanging until timeout.
3. Added succeeded guard and process.exit(0) to prevent timeout race
   after successful auth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:01:55 +03:00
gavrielc
4ae9785a61 fix(setup): move OpenClaw detection into environment step
Avoids running `ls -d ~/.openclaw` as a separate Bash command which
triggers permission prompts for reading outside the project directory.
The environment step now reports OPENCLAW_PATH in its status block.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:14:25 +03:00
gabi-simons
a1a324097e refactor(telegram-pairing): remove TTL expiry from pairing codes
Pairing codes no longer expire on a timer. They are consumed on match
or invalidated by wrong guesses. Removes ttlMs/expiresAt/deadline from
the pairing primitive, setup CLI, and tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 10:54:48 +00:00
gavrielc
39d2af9981 feat(v2): track unregistered senders + setup improvements
- Add unregistered_senders table to capture dropped message origins
  (one row per sender, upserted with message_count and last_seen)
- Add inbound DM logging to chat-sdk-bridge for debugging
- Add vercel CLI to base container image
- Install vercel-cli and frontend-engineer container skills
- Default requiresTrigger to false in register step

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 12:58:40 +03:00
gavrielc
75c2fde2b5 feat(v2): builder-agent self-modification WIP + container-config as per-group file
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>
2026-04-15 21:15:13 +03:00
Koshkoshinsk
77321984ba feat(pair-telegram): emit REMINDER_TO_ASSISTANT in status block
Move the "print the pairing code as plain text" directive from three
skill docs into the CLI output itself. Every caller of pair-telegram
(init-first-agent, manage-channels, add-telegram-v2, future callers)
now sees the reminder directly in the PAIR_TELEGRAM_ISSUED and
PAIR_TELEGRAM_NEW_CODE blocks. Skill docs shortened to point at it.

Also add a short pre-tool-call sentence in init-first-agent step 3b
instructing the assistant to extract the code and ask the user to send
it in Telegram.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:50:25 +00:00
gavrielc
0d3326aae5 feat(v2): user-level privilege model + cold DM infra + init-first-agent skill
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>
2026-04-15 00:03:51 +03:00
Koshkoshinsk
65afcdc946 feat(telegram-pairing): surface wrong-code attempts + auto-regen with retry cap
- createPairing now replaces any existing pending pairing for the same intent
  (replace-by-default; no "two pending codes for one intent" state)
- tryConsume records each attempt on pending records (capped at 10); a
  wrong code invalidates the pairing immediately (one attempt per code)
- waitForPairing gains onAttempt callback for misses and rejects with a
  distinct "invalidated by wrong code" message so callers can distinguish
  TTL expiry from user-error
- pair-telegram emits PAIR_TELEGRAM_ATTEMPT on misses and auto-regenerates
  the pairing up to 5 times, emitting PAIR_TELEGRAM_NEW_CODE for each
- Skill docs updated so the host Claude knows to show new codes and
  offer another batch on max-regenerations-exceeded

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:27:09 +00:00
Koshkoshinsk
2017589683 feat(telegram): self-contained pairing for chat ownership verification
BotFather issues bot tokens with no user binding, so anyone who guesses the
bot's username can DM it and get registered as a channel. Pairing closes that
gap: setup issues a one-time 4-digit code, the operator echoes it back from
the chat they want to register, and the inbound interceptor binds
admin_user_id before the message reaches the router.

- src/channels/telegram-pairing.ts: JSON-backed store with createPairing,
  tryConsume, getStatus, waitForPairing (fs.watch + poll fallback)
- src/channels/telegram.ts: wraps bridge.setup with an onInbound interceptor
  that consumes pairing codes and upserts messaging_groups
- setup/pair-telegram.ts: CLI step issues a code and waits up to 5 min for
  the operator to echo it back, emitting PLATFORM_ID/IS_GROUP/ADMIN_USER_ID
- Skill docs: /setup reorders mounts -> service -> wire (pairing needs a
  live polling adapter); /manage-channels and /add-telegram-v2 use pairing
  instead of asking the user to discover chat IDs

All other channels still bind admin via install-time identity (OAuth/QR/token);
pairing is Telegram-only. The bridge, router, and other adapters are untouched.
2026-04-13 12:27:02 +00:00
gavrielc
e83ffbc103 feat: named destinations + permission enforcement + fire-and-forget self-mod
Replaces implicit routing context (NANOCLAW_PLATFORM_ID env vars) with
per-agent named destination maps. Agents reference channels and peer
agents by local names; the host re-validates every outbound route against
a new agent_destinations table that is both the routing map and the ACL.

Model changes:
- New migration 004 adds agent_destinations (agent_group_id, local_name,
  target_type, target_id). Backfills from existing messaging_group_agents.
- Host writes /workspace/.nanoclaw-destinations.json before every container
  wake so admin changes take effect on next start.
- Container loads map at startup, appends system-prompt addendum listing
  available destinations and the <message to="name">…</message> syntax.
- Agent main output is parsed for <message to="..."> blocks; each block
  becomes a messages_out row with routing resolved via the local map.
  Untagged text and <internal>…</internal> are scratchpad (logged only).
- send_message MCP tool now takes `to` (destination name) instead of raw
  routing fields. send_to_agent deleted (redundant — agents are just
  destinations). send_file/edit_message/add_reaction route via map too.
- Inbound formatter adds from="name" attribute via reverse-lookup so the
  agent sees a consistent namespace in both directions.

Permission enforcement:
- Host checks hasDestination() before every channel delivery AND every
  agent-to-agent route. Unauthorized messages dropped and logged.
- routeAgentMessage simplified: ~15 lines, no JSON parse, content copied
  verbatim (target formatter resolves the sender via its own local map).
- create_agent is admin-only, checked at both the container (tool not
  registered for non-admins) and the host (re-check on receive). Inserts
  bidirectional destination rows so parent↔child comms work immediately.
  Includes path-traversal guard on folder name.

Self-modification cleanup:
- add_mcp_server now requires admin approval (previously had none).
- install_packages validates package names on BOTH sides (container tool
  + host receiver) with strict regex. Max 20 packages per request.
- All three self-mod tools are fire-and-forget: write request, return
  immediately with "submitted" message. Admin approval triggers a chat
  notification to the requesting agent — no tool-call polling, no 5-min
  holds. On rebuild/mcp_server approval, the container is killed so the
  next wake picks up new config/image.
- Approval delivery extracted into requestApproval() helper (the one
  place where three call sites were literally identical).

Also folded in the phase-1 dynamic import cleanup (create_agent no longer
does `await import('./db/agent-groups.js')`) and removes NANOCLAW_PLATFORM_ID
/ CHANNEL_TYPE / THREAD_ID env-var routing entirely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:31:37 +03:00
gavrielc
5a309a0e25 fix: only send onboarding message on first wiring, not re-registration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:49:58 +03:00
gavrielc
4a999ec973 feat: auto-onboarding when a channel is registered
After wiring a channel to an agent group, register.ts writes a task
message to the session that triggers the /welcome container skill.
The agent introduces itself immediately — the user sees typing and
then a greeting without having to send a message first.

Uses kind 'task' (not 'system') so the poll loop picks it up normally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:48:56 +03:00
gavrielc
a2badbd525 fix: normalize platform ID at registration, not router lookup
Channel adapters prefix platform IDs with their channel type
(e.g. "telegram:123"). Normalize in register.ts so the DB always
stores the canonical format. Removes fallback lookup from router.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:41:07 +03:00
gavrielc
9f5c37fc4c fix: handle platform ID prefix mismatch in router, not register
Move prefix handling from register.ts to router.ts. Users register with
raw platform IDs (what they naturally have), adapters send prefixed IDs
(their internal format). Router now tries stripping the channel type
prefix when the exact lookup fails, matching either format.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:39:40 +03:00