Commit Graph

32 Commits

Author SHA1 Message Date
gavrielc
aebcffe180 feat: per-group CLI scope (disabled/group/global)
Add cli_scope column to container_configs with three levels:
- disabled: agent never learns about ncl (instructions excluded from
  CLAUDE.md) and host dispatch rejects any cli_request
- group (default): agent can only access groups, sessions, destinations,
  and members resources, scoped to its own agent group with auto-filled
  --id/--agent_group_id/--group args. Help output reflects the scope.
- global: unrestricted access (current behavior)

Enforcement is host-side only — no image rebuild or env var needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-09 20:02:31 +03:00
gavrielc
31ccc61b27 feat(db): move container config from filesystem to DB
Source of truth for container runtime config moves from
groups/<folder>/container.json to a new container_configs table.
The file becomes a materialized view written at spawn time.

- New container_configs table with scalar columns (provider, model,
  effort, image_tag, assistant_name, max_messages_per_prompt) and
  JSON columns (mcp_servers, packages_apt, packages_npm, skills,
  additional_mounts)
- Startup backfill seeds DB from existing container.json files
- materializeContainerJson() replaces readContainerConfig + ensureRuntimeFields
- Self-mod handlers (install_packages, add_mcp_server) write to DB
- Provider cascade simplified: session -> container_configs -> 'claude'
- ncl groups config-{get,update,add-mcp-server,remove-mcp-server,
  add-package,remove-package} custom operations
- restartAgentGroupContainers() helper for config change propagation
- Container side unchanged (still reads /workspace/agent/container.json)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 22:27:55 +03:00
gavrielc
719f97e483 feat(permissions): unknown-channel registration flow with owner approval
When the router sees a mention or DM on a messaging group that isn't wired
to any agent, it now escalates to an owner for approval instead of silently
dropping. Mirrors the existing unknown-sender approval pattern (ACTION-ITEMS
item 22).

Schema (migration 012):
- `messaging_groups.denied_at TEXT NULL` — timestamp set on deny so future
  mentions stop escalating. ALTER TABLE ADD COLUMN, FK-safe (unlike the
  rebuild that bit migration 011).
- `pending_channel_approvals` — PK on `messaging_group_id` gives free
  in-flight dedup. One card per channel, no spam on rapid retries.

Router:
- New hook `setChannelRequestGate(mg, event) => Promise<void>`, invoked
  from the no-wirings branch when the message was addressed to the bot
  (isMention=true). Hook is fire-and-forget.
- Checks `mg.denied_at` before escalating — denied channels drop silently
  and do not re-prompt.
- The two "no-wirings" branches (fresh auto-create and existing mg with
  no agents) are consolidated into one escalation path that calls the
  gate once. Without the module, behavior is log + record (no regression).

Permissions module:
- `channel-approval.ts::requestChannelApproval` — MVP picker: target
  agent is `getAllAgentGroups()[0]`, card names it explicitly ("Wire it
  to <Andy>?"). Approver via existing `pickApprover` + `pickApprovalDelivery`
  primitives.
- Response handler: same click-auth pattern as sender-approval (clicker
  must be the designated approver OR have admin privilege over the
  target agent group).
- Approve defaults per the feature spec:
    engage_mode = 'mention-sticky' for groups, 'pattern' + '.' for DMs
    sender_scope = 'known'
    ignored_message_policy = 'accumulate'
    session_mode = 'shared'
  DM vs group inferred from the original event's threadId (non-null →
  group) because the auto-created mg has a placeholder is_group=0 until
  the adapter fills it in.
- Triggering sender is auto-added to agent_group_members so sender_scope=
  'known' doesn't bounce the replayed message into a sender-approval
  cascade.
- Deny: stamps messaging_groups.denied_at, clears pending row.
- Failure modes — no owner, no agent groups, no reachable DM — log and
  drop without creating a pending row, letting a future attempt try
  again (same as sender-approval).

9 new integration tests cover every branch: mention triggers card, DM
triggers card, dedup, approve creates correct wiring + admits sender +
replays, approve-on-DM uses pattern/'.' defaults, deny sets denied_at
and future mentions drop silently, unauthorized clicker rejected,
no-owner drops, no-agent-groups drops.

168 tests pass (was 159; +9).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:34:00 +03:00
gavrielc
16b9499532 feat(routing): engage modes + sender scope + accumulate/drop + per-agent fan-out
Replaces the opaque trigger_rules JSON + response_scope enum on
messaging_group_agents with four explicit orthogonal columns:

    engage_mode            'pattern' | 'mention' | 'mention-sticky'
    engage_pattern         regex source; required when mode='pattern';
                           '.' is the "always" sentinel
    sender_scope           'all' | 'known'
    ignored_message_policy 'drop' | 'accumulate'

Inbound routing becomes a fan-out — every wired agent is evaluated
independently. A match gets its own session + container wake. A miss
with accumulate keeps the message as context-only (trigger=0) in that
agent's session, so when the agent does eventually engage it sees the
prior chatter.

## Schema

- Migration 010 (`engage-modes`): adds the 4 new columns, backfills
  from trigger_rules.pattern + requiresTrigger + response_scope, drops
  the legacy columns.
- messages_in gains `trigger INTEGER NOT NULL DEFAULT 1` (session DB
  schema + `migrateMessagesInTable` forward-compat).
- countDueMessages gates waking on `trigger = 1`.

## Routing

- `pickAgent` (returns one) → loop over all wired agents. Per agent:
  evaluate engage_mode; run access gate + sender-scope gate; on full
  match → resolveSession + writeSessionMessage(trigger=1) + wake. On
  miss with accumulate → writeSessionMessage(trigger=0), no wake. On
  miss with drop → skip.
- New `findSessionForAgent(agentGroupId, mgId, threadId)` scopes
  session lookup by agent so fan-out doesn't cross sessions.
- `messageIdForAgent` namespaces inbound message ids by agent_group_id
  so PRIMARY KEY doesn't collide across per-agent session DBs.

## Adapter layer

- `ConversationConfig` replaces `triggerPattern` + `requiresTrigger`
  with `engageMode` + `engagePattern`.
- Chat SDK bridge stores `Map<platformId, ConversationConfig[]>` (multi-
  agent per conversation) and applies union gating pre-onInbound:
    * onSubscribedMessage: engage if any wiring keeps firing in
      subscribed state (mention-sticky or pattern)
    * onNewMention: engage on mention; only subscribes the thread if
      at least one wiring is `mention-sticky`
    * onDirectMessage: engage per mode; sticky follows same rule
- Bridge no longer unconditionally calls `thread.subscribe()`.

## Sender scope

- Permissions module registers a second hook `setSenderScopeGate` that
  runs per-wiring after the existing access gate. `sender_scope='known'`
  requires canAccessAgentGroup(); `'all'` is a no-op. Not installed →
  no-op everywhere (default allow).

## Container side

- Host passes `NANOCLAW_MAX_MESSAGES_PER_PROMPT` (reuses existing
  MAX_MESSAGES_PER_PROMPT config; was dead code from v1).
- `getPendingMessages` queries `ORDER BY seq DESC LIMIT N`, reverses to
  chronological order for the prompt — accumulated context rides along
  with trigger rows up to the cap.
- `MessageInRow` gains `trigger: number` so the container can tell them
  apart in downstream code (container still processes both; only the
  host uses `trigger=0` for don't-wake).

## Defaults (per ACTION-ITEMS item 1 decision)

- DM (is_group=0): `engage_mode='pattern'`, `engage_pattern='.'` (always)
- Threaded group: `engage_mode='mention-sticky'` (seed-discord)
- Non-threaded group / CLI: pattern '.' in bootstrap scripts

## Tests

- src/host-core.test.ts: 3 new cases — fan-out (2 agents, 2 sessions,
  2 wakes), accumulate (trigger=0 + no wake), drop (no session created).
- Existing 10 host-core tests still pass.
- Migration 010 runs on an empty DB in 0-row path — verified.

Closes: ACTION-ITEMS items 1, 4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 01:30:04 +03:00
gavrielc
cc784ff94b refactor(v2): remove trigger_credential_collection MCP tool
Drops the in-chat credential-collection flow introduced in e92b245. Agents
can no longer collect API keys via a secure modal — users must add secrets
through OneCLI directly. Keeps the OneCLI manual-approval handler and
threaded-routing work from the same commit intact.

Removed:
* container/agent-runner/src/mcp-tools/credentials.ts (MCP tool)
* src/credentials.ts (host-side modal/OneCLI pipeline)
* src/db/credentials.ts + migration 005 (pending_credentials table)
* src/onecli-secrets.ts (createSecret CLI facade, only caller was credentials.ts)
* findCredentialResponse from agent-runner DB layer
* PendingCredential types
* Four credential hooks from ChannelSetup (getCredentialForModal,
  onCredentialReject, onCredentialSubmit, onCredentialChannelUnsupported)
* Credential card/modal handling in chat-sdk-bridge (nccr/nccm prefixes,
  Modal/TextInput imports)
* credential_request text fallback in WhatsApp adapter
* request_credential system-action case in delivery.ts

Added:
* Migration 009 drops pending_credentials on existing installs.

Vercel skill now tells the agent to ask the user to register the token via
OneCLI instead of invoking the removed tool.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 21:41:41 +03:00
gavrielc
81d45b5be9 refactor(v2): remove builder-agent dev-agent/worktree/swap flow
The dev-agent-in-worktree approach for source self-modification is abandoned
in favor of a direct draft/activate flow with OS-level RO enforcement
(planned, not yet implemented). Strip the whole subgraph:
src/builder-agent/, pending-swaps DB module + migration 006, builder-agent
MCP tools, and all host wiring (startup sweep, approval routing, deadman,
worktree mount, freeze gate). Tool descriptions in self-mod.ts / agents.ts
no longer cross-reference create_dev_agent. CLAUDE.md + v2-checklist updated
to describe the new direction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:15:13 +03:00
gavrielc
20a24dfd13 style: apply prettier formatting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:15:13 +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
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
2df81e0b32 fix(v2/approvals): render correct title + selected label after click
Approval cards bypass the deliverMessage path that populates
pending_questions, so the post-click lookup found nothing and the
card edit fell back to " Question" + the raw option value
("approve"/"reject"). Store title and normalized options on
pending_approvals as well, and look up either table via a shared
getAskQuestionRender helper so the chat-sdk post-click edit and the
Discord interaction callback render the per-card title and the
selectedLabel (e.g. " Approved").

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:31:44 +00:00
Koshkoshinsk
d92d75e173 feat(v2/approvals): per-card titles and structured options
Approval cards now carry a required title (Add MCP Request, Install
Packages Request, Rebuild Request, Credentials Request) and structured
options with distinct pre-click label, post-click selectedLabel (e.g.
" Approved" / " Rejected"), and value used for click routing. The
title and normalized options are persisted in pending_questions so the
post-click card edit can render the correct per-type title and selected
label on both chat-sdk channels and Discord interactions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:31:44 +00:00
gavrielc
e92b245399 feat(v2): OneCLI 0.3.1 — approvals, credential collection, threaded routing
Three features built on top of @onecli-sh/sdk 0.3.1, landed together because
they share wiring surfaces (session DB schema, delivery dispatcher, Chat SDK
bridge, channel adapter contract).

## OneCLI manual-approval handler

* `src/onecli-approvals.ts` — long-polls OneCLI via the SDK's
  `configureManualApproval`; on each request, delivers an `ask_question` card
  to the admin agent group's first messaging group, persists a
  `pending_approvals` row, and waits on an in-memory Promise resolved by the
  admin's button click or an expiry timer. Expired cards are edited to
  "Expired (...)" and a startup sweep flushes any rows left over from a
  previous process.
* Short 11-byte approval id (`oa-<8 base36>`) instead of the SDK's UUID so the
  Telegram 64-byte `callback_data` limit is respected; the OneCLI UUID stays
  in the persisted payload for audit.
* Migration 003 consolidated: `pending_approvals` now has the OneCLI-aware
  columns from the start (`agent_group_id`, `channel_type`, `platform_id`,
  `platform_message_id`, `expires_at`, `status`), `session_id` relaxed to
  nullable so cross-session approvals fit.
* `handleQuestionResponse` in `src/index.ts` now routes OneCLI approvals
  through `resolveOneCLIApproval` before falling back to the
  session-bound approval path.

## Credential collection from chat

New `trigger_credential_collection` MCP tool — the agent researches a
third-party API, calls the tool with `{name, hostPattern, headerName,
valueFormat, description}`, and blocks until the host reports saved, rejected,
or failed. The credential value never enters the agent's context: the user
submits it into a Chat SDK Modal on the host side, the host writes it to
OneCLI via a thin facade (`src/onecli-secrets.ts` — shells out to
`onecli secrets create`, shape mirrors the SDK we expect upstream), and only
the status string flows back to the container via a system message.

* `src/credentials.ts` — host-side handler: delivers the card to the
  conversation's own channel (not the admin channel — credential collection
  is a user-facing flow, distinct from admin approval), persists a
  `pending_credentials` row, drives the submit → `createSecret` → notify
  pipeline. Falls back gracefully when the channel doesn't support modals.
* `src/db/credentials.ts` + migration 005: `pending_credentials` table.
* `src/channels/chat-sdk-bridge.ts`: renders a `credential_request` card,
  handles the `nccr:` action prefix by opening a Modal with a TextInput,
  registers an `onModalSubmit` handler for the `nccm:` callback prefix.
* `container/agent-runner/src/mcp-tools/credentials.ts`: the blocking MCP
  tool, mirroring the `ask_user_question` polling pattern.
* `container/agent-runner/src/db/messages-in.ts`: `findCredentialResponse`
  helper to pick up the system message the host writes back.

## Threaded adapter routing

The destination layer previously didn't carry thread context, so agent replies
to Discord always landed in the root channel regardless of which thread the
inbound came from.

* `ChannelAdapter.supportsThreads: boolean` — declared by every channel skill
  at `createChatSdkBridge`. Threaded: Discord, Slack, Teams, Google Chat,
  Linear, GitHub, Webex. Non-threaded: Telegram, WhatsApp Cloud, Matrix,
  Resend, iMessage.
* `src/router.ts`: non-threaded adapters strip `threadId` at ingest (threads
  collapse to channel-level sessions). Threaded adapters override the
  wiring's `session_mode` to `'per-thread'` so each thread = a session
  (except `agent-shared`, which is preserved as a cross-channel intent the
  adapter can't know about).
* `session_routing` table in `inbound.db` — single-row default reply routing
  written by the host on every container wake from
  `session.messaging_group_id` + `session.thread_id`. Forward-compat
  `CREATE TABLE IF NOT EXISTS` handles older session DBs lazily.
* `container/agent-runner/src/db/session-routing.ts` — container-side reader.
* `send_message` / `send_file` / `ask_user_question` / `send_card` /
  scheduling tools all default their routing (channel, platform, **and**
  thread) from the session when no explicit `to` is given. Explicit `to`
  uses the destination's channel with `thread_id = null` (cross-destination
  sends start a new conversation elsewhere).
* `poll-loop.ts::sendToDestination` (the final-text single-destination
  shortcut) now inherits `thread_id` from `RoutingContext` too — this was
  the root cause of Discord replies landing in the root channel even after
  `send_message` was wired correctly.

## Related cleanups

* `src/container-runner.ts`: OneCLI agent identifier switched from the lossy
  folder-derived string to `agent_group.id`, making `getAgentGroup(externalId)`
  a trivial reverse lookup for per-agent scoping.
* `wakeContainer` race fix via an in-flight promise map — concurrent wakes
  during the async buildContainerArgs / OneCLI `applyContainerConfig` window
  no longer double-spawn containers against the same session directory.
* `src/db/db-v2.test.ts`: dropped the brittle `expect(row.v).toBe(N)` schema
  version assertion — it had to be bumped on every migration addition.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:18:21 +03: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
d8fbd3b239 feat: agent-to-agent communication, dynamic agent creation, self-modification tools
Agent-to-agent: host routes messages with channel_type='agent' to target
agent's inbound.db, enriches with sender info, wakes target container.
Bidirectional routing works via inherited routing context.

Dynamic agents: create_agent MCP tool + system action handler creates
agent groups, folders, and optional CLAUDE.md on the fly.

Self-modification: install_packages (apt/npm, requires admin approval),
add_mcp_server (no approval), request_rebuild (builds per-agent-group
Docker image with approved packages). Approval flow reuses interactive
card infrastructure with pending_approvals table.

Also includes fixes from prior session: attachment download, reply context
extraction, message editing (platform message ID tracking), delivery retry
limits, and card update on button click.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 01:11:06 +03:00
gavrielc
57a6491c7e v2: channel isolation model, manage-channels skill, refactored channel skills
- Add three-level isolation model (shared session, same agent, separate agent)
  with agent-shared session mode for cross-channel shared sessions
- Create /manage-channels skill for wiring channels to agent groups
- Refactor all 12 v2 channel skills: lean SKILL.md + VERIFY.md + REMOVE.md
  with structured Channel Info section for platform-specific metadata
- Create /add-discord-v2 skill (was missing)
- Add step 5a to setup SKILL.md invoking /manage-channels after channel install
- Update setup/verify.ts to check all 12 channel token types
- Add docs/v2-isolation-model.md explaining the isolation model
- Update v2-checklist.md and v2-setup-wiring.md to reflect completed work

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:19:19 +03:00
gavrielc
9486d56b01 v2: make v2 the main entry point, move v1 to src/v1/
- Move all v1 files (index, router, container-runner, db, ipc, types,
  logger, channels/registry, and all utilities) to src/v1/ as a
  fully self-contained archive with no shared dependencies
- Rename v2 files to remove -v2 suffix (index-v2.ts → index.ts, etc.)
- Update all imports across v2 source, tests, and setup files
- Migrate shared utilities (config, env, container-runtime, mount-security,
  timezone, group-folder) from pino logger to v2 log module
- Migrate setup/ files from logger to log with argument order swap
- Container agent-runner: move v1 entry to v1/, rename v2 to index.ts
- Update setup skill to offer all 13 v2 channels
- Install all Chat SDK adapter packages
- dist/index.js now runs v2; dist/v1/index.js runs v1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:40:36 +03:00
exe.dev user
ee599b9f0c feat: add reply/quoted message context support
Add generic reply context fields to NewMessage (reply_to_message_id,
reply_to_message_content, reply_to_sender_name) so any channel can
pass quoted message context to the agent.

- Add thread_id and reply_to_* fields to NewMessage interface
- Add DB migration for reply context columns on messages table
- Update storeMessage/getMessagesSince/getNewMessages to persist and
  retrieve reply fields
- Render reply context as <quoted_message> XML in formatMessages
- Add DB and formatting tests

Co-Authored-By: Alfred-the-buttler <leon.alfred.bot@gmail.com>
Co-Authored-By: moktamd <moktamd@users.noreply.github.com>
Co-Authored-By: gurixs-carson <gurixs-carson@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:05:24 +00:00
Gabi Simons
675acffeb1 feat: add script field to ScheduledTask type and database layer
Adds optional `script` field to the ScheduledTask interface, with a
migration for existing DBs and full support in createTask/updateTask.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 14:04:11 +02:00
Gabi Simons
0210aa9ef1 refactor: implement multi-channel architecture (#500)
* refactor: implement channel architecture and dynamic setup

- Introduced ChannelRegistry for dynamic channel loading
- Decoupled WhatsApp from core index.ts and config.ts
- Updated setup wizard to support ENABLED_CHANNELS selection
- Refactored IPC and group registration to be channel-aware
- Verified with 359 passing tests and clean typecheck

* style: fix formatting in config.ts to pass CI

* refactor(setup): full platform-agnostic transformation

- Harmonized all instructional text and help prompts
- Implemented conditional guards for WhatsApp-specific steps
- Normalized CLI terminology across all 4 initial channels
- Unified troubleshooting and verification logic
- Verified 369 tests pass with clean typecheck

* feat(skills): transform WhatsApp into a pluggable skill

- Created .claude/skills/add-whatsapp with full 5-phase interactive setup
- Fixed TS7006 'implicit any' error in IpcDeps
- Added auto-creation of STORE_DIR to prevent crashes on fresh installs
- Verified with 369 passing tests and clean typecheck

* refactor(skills): move WhatsApp from core to pluggable skill

- Move src/channels/whatsapp.ts to add-whatsapp skill add/ folder
- Move src/channels/whatsapp.test.ts to skill add/ folder
- Move src/whatsapp-auth.ts to skill add/ folder
- Create modify/ for barrel file (src/channels/index.ts)
- Create tests/ with skill package validation test
- Update manifest with adds/modifies lists
- Remove WhatsApp deps from core package.json (now skill-managed)
- Remove WhatsApp-specific ghost language from types.ts
- Update SKILL.md to reflect skill-apply workflow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(skills): move setup/whatsapp-auth.ts into WhatsApp skill

The WhatsApp auth setup step is channel-specific — move it from core
to the add-whatsapp skill so core stays minimal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(skills): convert Telegram skill to pluggable channel pattern

Replace the old direct-integration approach (modifying src/index.ts,
src/config.ts, src/routing.test.ts) with self-registration via the
channel registry, matching the WhatsApp skill pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(skills): fix add-whatsapp build failure and improve auth flow

- Add missing @types/qrcode-terminal to manifest npm_dependencies
  (build failed after skill apply without it)
- Make QR-browser the recommended auth method (terminal QR too small,
  pairing codes expire too fast)
- Remove "replace vs alongside" question — channels are additive
- Add pairing code retry guidance and QR-browser fallback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove hardcoded WhatsApp default and stale Baileys comment

- ENABLED_CHANNELS now defaults to empty (fresh installs must configure
  channels explicitly via /setup; existing installs already have .env)
- Remove Baileys-specific comment from storeMessageDirect() in db.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(skills): convert Discord, Slack, Gmail skills to pluggable channel pattern

All channel skills now use the same self-registration pattern:
- registerChannel() factory at module load time
- Barrel file append (src/channels/index.ts) instead of orchestrator modifications
- No more *_ONLY flags (DISCORD_ONLY, SLACK_ONLY) — use ENABLED_CHANNELS instead
- Removed ~2500 lines of old modify/ files (src/index.ts, src/config.ts, src/routing.test.ts)

Gmail retains its container-runner.ts and agent-runner modifications (MCP
mount + server config) since those are independent of channel wiring.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: use getRegisteredChannels instead of ENABLED_CHANNELS

Remove the ENABLED_CHANNELS env var entirely. The orchestrator now
iterates getRegisteredChannelNames() from the channel registry —
channels self-register via barrel imports and their factories return
null when credentials are missing, so unconfigured channels are
skipped automatically.

Deleted setup/channels.ts (and its tests) since its sole purpose was
writing ENABLED_CHANNELS to .env. Refactored verify, groups, and
environment setup steps to detect channels by credential presence
instead of reading ENABLED_CHANNELS.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add breaking change notice and whatsapp migration instructions

CHANGELOG.md documents the pluggable channel architecture shift and
provides migration steps for existing WhatsApp users.

CLAUDE.md updated: Quick Context reflects multi-channel architecture,
Key Files lists registry.ts instead of whatsapp.ts, and a new
Troubleshooting section directs users to /add-whatsapp if WhatsApp
stops connecting after upgrade.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: rewrite READMEs for pluggable multi-channel architecture

Reflects the architectural shift from a hardcoded WhatsApp bot to a
pluggable channel platform. Adds upgrading notice, Mermaid architecture
diagram, CI/License/TypeScript/PRs badges, and clarifies that slash
commands run inside the Claude Code CLI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: move pluggable channel architecture details to SPEC.md

Revert READMEs to original tone with only two targeted changes:
- Add upgrading notice for WhatsApp breaking change
- Mention pluggable channels in "What It Supports"

Move Mermaid diagram, channel registry internals, factory pattern
explanation, and self-registration walkthrough into docs/SPEC.md.
Update stale WhatsApp-specific references in SPEC.md to be
channel-agnostic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: move upgrading notice to CHANGELOG, add changelog link

Remove the "Upgrading from Pre-Pluggable Versions" section from
README.md — breaking change details belong in the CHANGELOG. Add a
Changelog section linking to CHANGELOG.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: expand CHANGELOG with full PR #500 changes

Cover all changes: channel registry, WhatsApp moved to skill, removed
core dependencies, all 5 skills simplified, orchestrator refactored,
setup decoupled. Use Claude Code CLI instructions for migration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: bump version to 1.2.0 for pluggable channel architecture

Minor version bump — new functionality (pluggable channels) with a
managed migration path for existing WhatsApp users. Update version
references in CHANGELOG and update skill.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix skill application

* fix: use slotted barrel file to prevent channel merge conflicts

Pre-allocate a named comment slot for each channel in
src/channels/index.ts, separated by blank lines. Each skill's
modify file only touches its own slot, so three-way merges
never conflict when applying multiple channels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve real chat ID during setup for token-based channels

Instead of registering with `pending@telegram` (which never matches
incoming messages), the setup skill now runs an inline bot that waits
for the user to send /chatid, capturing the real chat ID before
registration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: setup delegates to channel skills, fix group sync and Discord metadata

- Restructure setup SKILL.md to delegate channel setup to individual
  channel skills (/add-whatsapp, /add-telegram, etc.) instead of
  reimplementing auth/registration inline with broken placeholder JIDs
- Move channel selection to step 5 where it's immediately acted on
- Fix setup/groups.ts: write sync script to temp file instead of passing
  via node -e which broke on shell escaping of newlines
- Fix Discord onChatMetadata missing channel and isGroup parameters
- Add .tmp-* to .gitignore for temp sync script cleanup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: align add-whatsapp skill with main setup patterns

Add headless detection for auth method selection, structured inline
error handling, dedicated number DM flow, and reorder questions to
match main's trigger-first flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add missing auth script to package.json

The add-whatsapp skill adds src/whatsapp-auth.ts but doesn't add
the corresponding npm script. Setup and SKILL.md reference `npm run auth`
for WhatsApp QR terminal authentication.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: update Discord skill tests to match onChatMetadata signature

The onChatMetadata callback now takes 5 arguments (jid, timestamp,
name, channel, isGroup) but the Discord skill tests only expected 3.
This caused skill application to roll back on test failure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: replace 'pluggable' jargon with clearer language

User-facing text now says "multi-channel" or describes what it does.
Developer-facing text uses "self-registering" or "channel registry".
Also removes extra badge row from README.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: align Chinese README with English version

Remove extra badges, replace pluggable jargon, remove upgrade section
(now in CHANGELOG), add missing intro line and changelog section,
fix setup FAQ answer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: warn on installed-but-unconfigured channels instead of silent skip

Channels with missing credentials now emit WARN logs naming the exact
missing variable, so misconfigurations surface instead of being hidden.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: simplify changelog to one-liner with compare link

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add isMain flag and channel-prefixed group folders

Replace MAIN_GROUP_FOLDER constant with explicit isMain boolean on
RegisteredGroup. Group folders now use channel prefix convention
(e.g., whatsapp_main, telegram_family-chat) to prevent cross-channel
collisions.

- Add isMain to RegisteredGroup type and SQLite schema (with migration)
- Replace all folder-based main group checks with group.isMain
- Add --is-main flag to setup/register.ts
- Strip isMain from IPC payload (defense in depth)
- Update MCP tool description for channel-prefixed naming
- Update all channel SKILL.md files and documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: gavrielc <gabicohen22@yahoo.com>
Co-authored-by: Koshkoshinski <daniel.milliner@gmail.com>
2026-03-03 00:35:45 +02:00
gavrielc
51788de3b9 Skills engine v0.1 + multi-channel infrastructure (#307)
* refactor: multi-channel infrastructure with explicit channel/is_group tracking

- Add channels[] array and findChannel() routing in index.ts, replacing
  hardcoded whatsapp.* calls with channel-agnostic callbacks
- Add channel TEXT and is_group INTEGER columns to chats table with
  COALESCE upsert to protect existing values from null overwrites
- is_group defaults to 0 (safe: unknown chats excluded from groups)
- WhatsApp passes explicit channel='whatsapp' and isGroup to onChatMetadata
- getAvailableGroups filters on is_group instead of JID pattern matching
- findChannel logs warnings instead of silently dropping unroutable JIDs
- Migration backfills channel/is_group from JID patterns for existing DBs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: skills engine v0.1 — deterministic skill packages with rerere resolution

Three-way merge engine for applying skill packages on top of a core
codebase. Skills declare which files they add/modify, and the engine
uses git merge-file for conflict detection with git rerere for
automatic resolution of previously-seen conflicts.

Key components:
- apply: three-way merge with backup/rollback safety net
- replay: clean-slate replay for uninstall and rebase
- update: core version updates with deletion detection
- rebase: bake applied skills into base (one-way)
- manifest: validation with path traversal protection
- resolution-cache: pre-computed rerere resolutions
- structured: npm deps, env vars, docker-compose merging
- CI: per-skill test matrix with conflict detection

151 unit tests covering merge, rerere, backup, replay, uninstall,
update, rebase, structured ops, and edge cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add Discord and Telegram skill packages

Skill packages for adding Discord and Telegram channels to NanoClaw.
Each package includes:
- Channel implementation (add/src/channels/)
- Three-way merge targets for index.ts, config.ts, routing.test.ts
- Intent docs explaining merge invariants
- Standalone integration tests
- manifest.yaml with dependency/conflict declarations

Applied via: npx tsx scripts/apply-skill.ts .claude/skills/add-discord
These are inert until applied — no runtime impact.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* remove unused docs (skills-system-status, implementation-guide)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 01:55:00 +02:00
gavrielc
9261a25531 feat: add is_bot_message column and support dedicated phone numbers (#235)
* feat: add is_bot_message column and support dedicated phone numbers

Replace fragile content-prefix bot detection with an explicit
is_bot_message database column. The old prefix check (content NOT LIKE
'Andy:%') is kept as a backstop for pre-migration messages.

- Add is_bot_message column with automatic backfill migration
- Add ASSISTANT_HAS_OWN_NUMBER env var to skip name prefix when the
  assistant has its own WhatsApp number
- Move prefix logic into WhatsApp channel (no longer a router concern)
- Remove prefixAssistantName from Channel interface
- Load .env via dotenv so launchd-managed processes pick up config
- WhatsApp bot detection: fromMe for own number, prefix match for shared

Based on #160 and #173.

Co-Authored-By: Stefan Gasser <stefan@stefangasser.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: extract shared .env parser and remove dotenv dependency

Extract .env parsing into src/env.ts, used by both config.ts and
container-runner.ts. Reads only requested keys without loading secrets
into process.env, avoiding leaking API keys to child processes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Stefan Gasser <stefan@stefangasser.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 15:31:57 +02:00
gavrielc
2b56fecfdc Refactor index (#156)
* feat: add Telegram channel with agent swarm support

Add Telegram as a messaging channel that can run alongside WhatsApp
or standalone (TELEGRAM_ONLY mode). Includes bot pool support for
agent swarms where each subagent appears as a different bot identity
in the group.

- Add grammy dependency for Telegram Bot API
- Route messages through tg: JID prefix convention
- Add storeMessageDirect for non-Baileys channels
- Add sender field to IPC send_message for swarm identity
- Support TELEGRAM_BOT_TOKEN, TELEGRAM_ONLY, TELEGRAM_BOT_POOL config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add index.ts refactor plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: extract channel abstraction, IPC, and router from index.ts

Break the 1088-line monolith into focused modules:
- src/channels/whatsapp.ts: WhatsAppChannel class implementing Channel interface
- src/ipc.ts: IPC watcher and task processing with dependency injection
- src/router.ts: message formatting, outbound routing, channel lookup
- src/types.ts: Channel interface, OnInboundMessage, OnChatMetadata types

Also adds regression test suite (98 tests), updates all documentation
and skill files to reflect the new architecture.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: add test workflow for PRs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove accidentally committed pool-bot assets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ci): remove grammy from base dependencies

Grammy is installed by the /add-telegram skill, not a base dependency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 00:36:37 +02:00
gavrielc
6f02ee530b Adds Agent Swarms
* feat: streaming container mode, IPC messaging, agent teams support

Major architectural shift from single-shot container runs to long-lived
streaming containers with IPC-based message injection.

- Agent runner: query loop with AsyncIterable prompt to keep stdin open
  for agent teams (fixes isSingleUserTurn premature shutdown)
- New standalone stdio MCP server (ipc-mcp-stdio.ts) inheritable by
  subagents, with send_message and schedule_task tools
- Streaming output: parse OUTPUT_START/END markers in real-time, send
  results to WhatsApp as they arrive
- IPC file-based messaging: host writes to ipc/{group}/input/, agent
  polls for follow-up messages without respawning containers
- Per-group settings.json with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1
- SDK bumped to 0.2.34 for TeamCreate tool support
- Container idle timeout (30min) with _close sentinel for shutdown
- Orphaned container cleanup on startup
- alwaysRespond flag for groups that skip trigger pattern check
- Uncaught exception/rejection handlers with timestamps in logger
- Combined SDK documentation into single deep dive reference

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove unused ipc-mcp.ts (replaced by ipc-mcp-stdio.ts)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: clarify agent communication model in docs and tool descriptions

- CLAUDE.md (main + global): split communication instructions into
  "responding to messages" vs "scheduled tasks" sections
- send_message tool: note that scheduled task output is not sent to user
- Remove structured output (outputFormat) — not needed with current flow
- Regular output is sent to WhatsApp; scheduled task output is only logged

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: ignore dynamic group data while preserving base structure

Only track groups/main/CLAUDE.md and groups/global/CLAUDE.md. All other
group directories and files are ignored to prevent tracking user-specific
session data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve critical bugs in streaming container mode

Bug 1 (scheduled task hang): Task scheduler now passes onOutput callback
with idle timer that writes _close sentinel after IDLE_TIMEOUT, so
containers exit cleanly instead of blocking queue slots for 30 minutes.
Scheduled tasks stay alive for interactive follow-up via IPC.

Bug 2 (timeout disabled): Remove resetTimeout() from stderr handler.
SDK writes debug logs continuously, resetting the timer on every line.
Timeout now only resets on actual output markers in stdout.

Bug 3 (trigger bypass): Piped messages in startMessageLoop now check
trigger pattern for non-main groups. Non-trigger messages accumulate in
DB and are pulled as context via getMessagesSince when a trigger arrives.

Bug 7 (non-atomic IPC writes): GroupQueue.sendMessage uses temp file +
rename for atomic writes, matching ipc-mcp-stdio.ts pattern.

Also: flip isVerbose back to false (debug leftover), add isScheduledTask
to host-side ContainerInput interface.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: idle timer not starting + scheduled task groupFolder missing

Two bugs that prevented the scheduled task idle timeout fix from working:

1. onOutput was only called when parsed.result !== null, but session
   update markers have result: null. The idle timer never started for
   "silent" query completions, leaving containers parked at
   waitForIpcMessage until hard timeout.

2. Scheduler's onProcess callback didn't pass groupFolder to
   queue.registerProcess, so closeStdin no-oped (groupFolder was null).
   The _close sentinel was never written even when the idle timer fired.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: duplicate messages and timestamp rollback in piping path

Two bugs introduced by the trigger context accumulation change:

1. processGroupMessages didn't advance lastAgentTimestamp until after
   the container finished. The piping path's getMessagesSince(lastAgent
   Timestamp) re-fetched messages already sent as the initial prompt,
   causing duplicates.

2. processGroupMessages overwrote lastAgentTimestamp with the original
   batch timestamp on completion, rolling back any advancement made by
   the piping path while the container was running.

Fix: advance lastAgentTimestamp immediately after building the prompt,
before starting the container. This matches the piping path behavior
and eliminates both the overlap and the rollback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: container idles 30 extra minutes after _close during query

When _close was detected during pollIpcDuringQuery, it was consumed
(deleted) and stream.end() was called. But after runQuery returned,
main() still emitted a session-update marker (resetting the host's idle
timer) and called waitForIpcMessage (which polled forever since _close
was already gone). The container had to wait for a second _close.

Fix: runQuery now returns closedDuringQuery. When true, main() skips
the session-update marker and waitForIpcMessage, exiting immediately.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resume branching, internal tags, and output forwarding

- Fix resume branching: pass resumeSessionAt with last assistant UUID
  to anchor each query loop resume to the correct conversation tree
  position. Prevents agent responses landing on invisible branches
  when agent teams subagents create parallel JSONL entries.

- Add <internal> tag stripping: agent can wrap internal reasoning in
  <internal> tags which are logged but not sent to WhatsApp. Prevents
  duplicate messages and internal monologue reaching users.

- Forward scheduled task output: scheduled tasks now send result text
  to WhatsApp (with <internal> stripping), matching regular message
  behavior. No more special-case instructions.

- Update Communication guidance in CLAUDE.md: simplified to "your
  output is sent to the user or group" with soft guidance on
  <internal> tags and send_message usage.

- Add messaging behavior docs to schedule_task tool: prompts the
  scheduling agent to include guidance on whether the task should
  always/conditionally/never message the user.

- Mount security: containerPath now optional, defaults to basename
  of hostPath.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: cursor rollback on error, flush guard, verbose logging

- Roll back lastAgentTimestamp on container error so retries can
  re-process the messages instead of silently losing them.

- Add guard flag to flushOutgoingQueue to prevent duplicate sends
  from concurrent flushes during rapid WA reconnects.

- Revert isVerbose from hardcoded false back to env-based check
  (LOG_LEVEL=debug|trace).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: orphan container cleanup was silently failing

The startup cleanup used `container ls --format {{.Names}}` which is
Docker Go-template syntax. Apple Container only supports `--format json`
or `--format table`. The command errored with exit code 64, but the
catch block silently swallowed it — orphan containers were never cleaned
up on restart.

Fixed to use `--format json` and parse `configuration.id` from the
JSON output. Also filters by `status: running` and logs a warning on
failure instead of silently catching.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add Discord badge and community section

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: idle timer reset on null results and flush queue message loss

- Only reset idle timer on actual results (non-null), not session-update
  markers. Prevents containers staying alive 30 extra minutes after the
  agent finishes work.
- flushOutgoingQueue now uses shift() instead of splice(0) so unattempted
  messages stay in the queue if an unexpected error bails the loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add Agent Swarms to README

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: update Telegram skill for current architecture

Rewrite integration instructions to match the per-group queue/SQLite
architecture: remove onMessage callback pattern (store to DB, let
message loop pick up), fix startSchedulerLoop signature, add
TELEGRAM_ONLY service startup, SQLite registration, data/env/env sync,
@mention-to-trigger translation, and BotFather group privacy docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: Telegram skill message chunking, media placeholders, chat discovery

- Split long messages at Telegram's 4096 char limit to prevent silent
  send failures
- Store placeholder text for non-text messages (photos, voice, stickers,
  etc.) so the agent knows media was sent
- Update getAvailableGroups filter to include tg: chats so the agent can
  discover and register Telegram chats via IPC
- Fix removal step numbering

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: update REQUIREMENTS.md and SPEC.md for SQLite architecture

- Replace all registered_groups.json / sessions.json / router_state.json
  references with SQLite equivalents
- Fix CONTAINER_TIMEOUT default (300000 → 1800000)
- Add missing config exports (IDLE_TIMEOUT, MAX_CONCURRENT_CONTAINERS)
- Update folder structure: add missing src files (logger, group-queue,
  mount-security), remove non-existent utils.ts, list all skills
- Fix agent-runner entry (ipc-mcp.ts → ipc-mcp-stdio.ts)
- Update startup sequence to reflect per-group queue architecture
- Fix env mounting description (data/env/env, not extracted vars)
- Update troubleshooting to use sqlite3 commands

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: fix README architecture description, revert SPEC.md env error

- README: update architecture blurb to mention per-group queue, add
  group-queue.ts to key files, update file descriptions
- SPEC.md: restore correct credential filtering description (only auth
  vars are extracted from .env, not the full file)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 02:50:43 +02:00
gavrielc
f26468c9b0 fix: setup skill reliability, requiresTrigger option, agent-browser visibility
Setup skill fixes:
- Run QR auth in foreground with long timeout, not background
- Replace fragile message-based registration with DB group sync lookup
- Personal chats: ask for phone number instead of querying empty DB
- Consolidate trigger word + security model + channel selection into one step
- Remove `timeout` shell command (unavailable on macOS), use Bash tool timeout
- Query 40 groups, display 10 at a time, support name lookup

requiresTrigger support:
- Add requiresTrigger field to RegisteredGroup type and DB schema
- Skip trigger check when requiresTrigger is false (for solo/personal chats)
- Main group still always processes all messages (unchanged)

Agent-browser visibility:
- Append global CLAUDE.md to non-main agent system prompts via SDK
- Add browser tool docs to global and main CLAUDE.md
- Update skill description to be broader (not just "web testing")
- Reference agent-browser.md in root CLAUDE.md key files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 01:39:31 +02:00
gavrielc
eac9a6acfd feat: per-group queue, SQLite state, graceful shutdown
Add per-group container locking with global concurrency limit to prevent
concurrent containers for the same group (#89) and cap total containers.
Fix message batching bug where lastAgentTimestamp advanced to trigger
message instead of latest in batch, causing redundant re-processing.
Move router state, sessions, and registered groups from JSON files to
SQLite with automatic one-time migration. Add SIGTERM/SIGINT handlers
with graceful shutdown (SIGTERM -> grace period -> SIGKILL). Add startup
recovery for messages missed during crash. Remove dead code: utils.ts,
Session type, isScheduledTask flag, ContainerConfig.env, getTaskRunLogs,
GroupQueue.isActive.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 07:38:07 +02:00
gavrielc
21c66df2b1 Add prettier
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 17:14:17 +02:00
gavrielc
48822ff67d Add mount security allowlist for external directory access (#14)
* Add secure mount allowlist validation

Addresses arbitrary host mount vulnerability by validating additional
mounts against an external allowlist stored at ~/.config/nanoclaw/.
This location is never mounted into containers, making it tamper-proof.

Security measures:
- Allowlist cached in memory (edits require process restart)
- Real path resolution (blocks symlink and .. traversal attacks)
- Blocked patterns for sensitive paths (.ssh, .gnupg, .aws, etc.)
- Non-main groups forced to read-only when nonMainReadOnly is true
- Container path validation prevents /workspace/extra escape

https://claude.ai/code/session_01BPqdNy4EAHHJcdtZ27TXkh

* Add mount allowlist setup to /setup skill

Interactive walkthrough that asks users:
- Whether they want agents to access external directories
- Which directories to allow (with paths)
- Read-write vs read-only for each
- Whether non-main groups should be restricted to read-only

Creates ~/.config/nanoclaw/mount-allowlist.json based on answers.

https://claude.ai/code/session_01BPqdNy4EAHHJcdtZ27TXkh

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-01 22:55:08 +02:00
Gavriel
572338b9a6 Add context_mode option for scheduled tasks
Scheduled tasks can now run in either:
- "group" mode: uses the group's conversation session for context
- "isolated" mode: runs with a fresh session (previous behavior)

The tool description guides the agent on when to use each mode and
prompts them to ask the user if unclear. Group mode is now the default.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 22:23:50 +02:00
gavrielc
09c0e8142e Add containerized agent execution with Apple Container
- Agents run in isolated Linux VMs via Apple Container
- All groups get Bash access (safe - sandboxed in container)
- Browser automation via agent-browser + Chromium
- Per-group configurable additional directory mounts
- File-based IPC for messages and scheduled tasks
- Container image with Node.js 22, Chromium, agent-browser

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 22:55:57 +02:00
gavrielc
fa13b14dae Add built-in scheduler with group-scoped tasks
- Custom nanoclaw MCP server with scheduling tools (schedule_task,
  list_tasks, get_task, update_task, pause/resume/cancel_task, send_message)
- Tasks run as full agents in their group's context
- Support for cron, interval, and one-time schedules
- Task run logging with duration and results
- Main channel has Bash access for admin tasks (query DB, manage groups)
- Other groups restricted to file operations only
- Updated docs and requirements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 21:00:37 +02:00
gavrielc
22bd3d7c58 Store and display sender's WhatsApp name
Use pushName from baileys to get the sender's display name instead
of just the phone number. Falls back to phone number if no name.

Includes migration to add sender_name column to existing databases.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 19:32:18 +02:00
gavrielc
78426c764d Extract config and types into separate files, clean up index.ts
- src/config.ts: configuration constants
- src/types.ts: TypeScript interfaces
- src/index.ts: remove section comments, streamline code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 19:17:40 +02:00