Commit Graph

77 Commits

Author SHA1 Message Date
gavrielc
712720eef4 refactor(v2): drop @chat-adapter/shared dep — duck-type NetworkError
The only use was channel-registry.ts checking `err instanceof NetworkError`
to retry transient setup failures. Switched to a duck-type predicate
(`err.name === 'NetworkError'`) so the dep is no longer needed at trunk
level. Channel skills bring it in transitively when they install their
Chat SDK adapter package.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:33:22 +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
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
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
Koshkoshinsk
fdece8047e fix: reply in the Slack DM thread the user wrote in
- chat-sdk-bridge: forward thread.id to the router for DMs so sub-thread
  context survives into delivery. Previously hardcoded to null, which
  collapsed every reply to the DM top level.
- router: when a DM (is_group=0) is wired as `shared`, don't auto-escalate
  to per-thread — keep one session for the whole DM and let thread_id
  flow through to the adapter.
- agent-runner poll-loop: defer follow-up messages whose thread_id
  differs from the active turn's routing. Mixing threads into one
  streaming turn sent every reply to the first thread because routing
  is captured at turn start.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 11:14:05 +00:00
gavrielc
ce80e4ec3e feat(setup): add Linear channel, fix setup permissions
Enable Linear channel adapter. Fix setup permission rules: use specific
npm install entries per adapter package, replace cp -r with rsync -a to
avoid built-in cp safety prompt, add head to allow list for chained
commands. Update Linear API key URL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:00:27 +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
57c9bfc670 improve setup flow: streamline steps, add pre-approval, new manage-mounts skill
- Disable sandbox by default in project settings
- Setup: remove Apple Container option (Docker only), single channel selection
  with plain text list, move fork to end, auto-set empty mounts, add command
  pre-approval step, add UTC timezone confirmation, add wait-on-user guidance,
  add 5m timeouts for long steps
- iMessage: improve Full Disk Access UX with Finder open + drag instructions
- Add /manage-mounts skill for post-setup mount configuration
- Enable iMessage channel import

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 11:26:20 +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
exe.dev user
8ef26d323f fix(v2/telegram): await pairing interceptor work to serialize DB commits
The Telegram pairing interceptor fired DB writes (createMessagingGroup,
upsertUser, grantRole) and the pairing-success confirmation inside an
unawaited `void (async () => {...})()`. Recent changes (0d3326a user
privilege model, c483860 pairing confirmation) widened the work done
inside this closure to include an extra two DB writes and a Telegram
API round-trip, making the race between match and commit reproducible
— a paired message could appear "lost" until a second send.

Change onInbound to optionally return a Promise, await it in the
chat-sdk-bridge dispatch callbacks, and make the pairing interceptor
async so its DB writes + confirmation send complete before the handler
resolves.

Note: the upstream @chat-adapter/telegram SDK itself does not await
processUpdate in its polling loop, so the adapter's getUpdates offset
still advances before our handler resolves. A true restart-safe fix
needs a corresponding change in chat-adapter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 12:16:49 +00:00
Koshkoshinsk
c483860cd9 feat(v2/telegram): send pairing-success confirmation to paired chat
After a Telegram pair-code is successfully consumed, send a one-shot
"Pairing success! I'm spinning up the agent now, you'll get a message
from them shortly." reply to the same chat so the user knows the code
was accepted before the agent's own welcome DM arrives.

Best-effort: any sendMessage failure is logged but not rethrown, so a
Telegram outage can't undo a successful pairing or trigger the
interceptor's fail-open path.

Also includes a no-op prettier reformat in chat-sdk-bridge.ts that the
husky hook missed in the previous commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 07:59:17 +00:00
Koshkoshinsk
ed5dc5ea51 fix(v2/chat-sdk): project author into flat sender fields for router gate
The chat-sdk bridge was emitting inbound messages with a nested
author.{userId,fullName,userName} shape, but router.ts:extractAndUpsertUser
reads flat content.senderId / sender / senderName. Result: every chat-sdk
adapter (telegram, discord, slack, teams, gchat, webex, matrix, resend,
imessage, whatsapp-cloud) hit the strict access gate with userId=null and
got dropped, even for the registered owner.

Project author into the flat fields inside messageToInbound so the bridge
matches the contract documented at router.ts:14-17. Native adapters
already set these directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 07:38:02 +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
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
42467d796d style: apply prettier formatting
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
Gabi Simons
c36541ba6c feat(v2/whatsapp): add file attachments, reactions, and inbound media
- Outbound files: images, videos, audio as native media messages;
  other types as documents. First file gets text as caption.
- Reactions: send emoji reactions via Baileys react message type
- Inbound media: download images, video, audio, documents from
  incoming messages and pass as attachments to the agent
- Edit operations silently skipped (WhatsApp linked device limitation)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 11:30:06 +00:00
Gabi Simons
c02ac06258 feat(v2): add formatting, approvals, and echo filter to WhatsApp adapter
- Markdown→WhatsApp formatting: **bold**→*bold*, *italic*→_italic_,
  headings→bold, links→plaintext, code blocks preserved
- ask_question support: renders as text with /approve, /reject slash
  commands; matches replies and routes through onAction pipeline
- credential_request: text fallback (WhatsApp has no modal support)
- Bot echo filter: skip fromMe messages to prevent loops
- Formatting applied to all outbound text messages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 11:02:42 +00:00
Koshkoshinsk
f304c67318 fix(telegram): sanitize outbound markdown for legacy parse mode
The @chat-adapter/telegram adapter hardcodes parse_mode=Markdown (legacy)
but its converter emits CommonMark. Messages containing **bold** or list
bullets that round-trip to `*` produce "can't parse entities" errors and
get dropped after retries.

Add an opt-in transformOutboundText hook on the chat-sdk bridge and wire
a Telegram-specific sanitizer that downgrades **bold** to *bold*, rewrites
dash/plus list bullets to a Unicode bullet so the adapter's re-stringify
doesn't inject stray `*`, and strips unbalanced delimiters or brackets.
Only Telegram opts in; other channels are unaffected.

Workaround until upstream (vercel/chat) ships mode-aware conversion —
PR #367 adds a parseMode knob but not the converter fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:30:32 +00:00
Gabi Simons
c303b6eb14 feat(v2): add native WhatsApp adapter using Baileys v6
Direct ChannelAdapter implementation — no Chat SDK bridge.
Ports v1 infrastructure: getMessage fallback, outgoing queue,
group metadata cache, LID-to-phone mapping, auto-reconnect.
Auth via pairing code (WHATSAPP_PHONE_NUMBER) or QR code.

Text messaging only (MVP). Not yet implemented:
- File/image attachments (send and receive)
- Edit message, delete message
- Reactions
- Bot echo filtering (own messages loop back as inbound)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 15:04:24 +00:00
Koshkoshinsk
ae88d2b7c2 fix(telegram): retry adapter setup on transient network errors
Cold-start DNS/network hiccups can fail the adapter's first deleteWebhook or
getMe call, leaving the channel silently dead while the service stays up.
Wrap bridge.setup in an exponential-backoff retry (5 attempts) — if the
network is truly down we surface it instead of hanging forever.

Lives in telegram.ts so the chat-sdk bridge stays generic; other channels
can opt in by copying the small helper if they hit the same issue.
2026-04-13 12:27:45 +00: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
2454444f2e feat(telegram-pairing): accept bare 4-digit codes
Require the message to be exactly the 4 digits (optionally prefixed by
@botname). Loose matches like "my pin is 0349" are rejected to avoid false
positives from chat traffic that happens to contain a 4-digit number.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:27:06 +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
Gabi Simons
8676c07448 feat(v2): support async channel adapter factories
Channel adapter factories can now return a Promise, enabling adapters
that need async initialization like loading auth state from disk
(e.g. WhatsApp reading credentials via useMultiFileAuthState).
Existing sync factories are unaffected — await on a sync return is
a no-op. All current adapters remain synchronous.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:11:06 +00:00
gavrielc
f0e4f07ac2 refactor(v2): extract webhook server into standalone module
Aligns with upstream feat/chat-sdk-integration pattern: regex-based
routing (/webhook/{adapterName}), response streaming, cleanup function.
Updates Slack and Teams skill docs to match /webhook/{name} convention
used by all other v2 channel skills.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:36:16 +03:00
gavrielc
5a606a83d4 refactor(v2): use Chat SDK webhooks proxy and clean up webhook server
Route webhook requests through chat.webhooks[name]() instead of calling
adapter.handleWebhook() directly, getting proper auto-initialization and
signature verification. Extract Node↔Web Request/Response conversion
into reusable helpers, parse URL pathname properly for query string
safety, and support all HTTP methods (not just POST).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:36:16 +03:00
gavrielc
669a8444ef refactor(v2): extract session DB operations into src/db/session-db.ts
Move all raw SQL out of session-manager, delivery, and host-sweep into
a dedicated DB module. Make session schemas idempotent (IF NOT EXISTS)
so initSessionFolder always applies them. Revert the markdown
plain-text retry from 4c477ac.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:36:16 +03:00
Gabi Simons
7e74bfd330 feat(v2): Teams adapter env-driven app type and updated skill docs
Teams adapter now reads TEAMS_APP_TYPE and TEAMS_APP_TENANT_ID from
env, supporting both MultiTenant (default) and SingleTenant configs.
Updated add-teams-v2 skill docs with full Azure Bot setup flow,
webhook endpoint format, and app package sideloading instructions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:34:09 +00:00
Gabi Simons
9476a80ab0 feat(v2): shared webhook server for webhook-based channel adapters
Adds a shared HTTP server (port 3000, configurable via WEBHOOK_PORT)
that routes incoming webhooks to the correct Chat SDK adapter by path
(e.g. /api/webhooks/slack, /api/webhooks/teams). Required by Slack,
Teams, GitHub, Linear, and other non-gateway adapters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:33:45 +00:00
Koshkoshinsk
7bd8c6ad41 fix(v2): retry channel adapter setup on transient network errors
A NetworkError during adapter.setup() (e.g. Telegram deleteWebhook hitting
a DNS hiccup at boot) would log the failure and immediately give up,
leaving the channel permanently dead until the host process was manually
restarted — even though the host kept running and other channels worked.

Wrap the setup call in a small retry loop with backoff (2s, 5s, 10s) that
fires only on NetworkError. Misconfigs (bad tokens, invalid options) still
fail fast since they don't surface as NetworkError.

Universal across channels — applies to any adapter that throws
NetworkError from setup(), not just Telegram.
2026-04-12 09:32:15 +00:00
Koshkoshinsk
4c477acca3 fix(v2): retry as plain text when adapter rejects markdown
A single message with markdown the adapter couldn't parse (e.g. Telegram
MarkdownV2 entity errors) would fail in deliverSessionMessages and be
retried forever, blocking every subsequent reply on that session.

Catch ValidationError from postMessage and retry once with the markdown
stripped to plain text via markdownToPlainText. Files re-attach in a
follow-up post since the plain-text retry drops the files payload shape.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 09:32:12 +00:00
gavrielc
9dda75bb21 docs(v2): cross-mount invariants + diagrams; inline a2a routing
- session-manager.ts: shrink the cross-mount invariant header from 31
  lines to 12, keeping each invariant's cause and consequence inline.
- agent-runner/db/connection.ts: parallel cross-mount comment for the
  container-side reader (inbound.db must be journal_mode=DELETE).
- agent-runner/db/messages-out.ts: document that even/odd seq parity
  is load-bearing — seq is the agent-facing message ID returned by
  send_message and consumed by edit_message / add_reaction, looked
  up across both tables.
- v2-checklist.md: record the cross-mount invariants and seq parity
  under Core Architecture so future "simplifications" don't regress
  them.
- scripts/sanity-live-poll.ts: empirical validation harness for the
  three cross-mount invariants — flips each one and observes silent
  message loss / corruption.
- delivery.ts: inline routeAgentMessage at its single callsite (-17
  net lines). The wrapper added more boilerplate than it factored.
- docs/v2-architecture-diagram.{md,html}: rendered Mermaid diagrams
  of the v2 system, message flow, named destinations, entity model,
  and the two-DB split.
- channels/adapter.ts, chat-sdk-bridge.ts, credentials.ts,
  db/sessions.ts, db/db-v2.test.ts: prettier format pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:21:12 +03: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
6eb81b5737 style: prettier formatting fixes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 01:11:06 +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
d656b5ccc1 fix: Chat SDK bridge delivery and typing for non-Discord adapters
- Use platformId directly as thread ID in deliver() and setTyping()
  instead of calling encodeThreadId with Discord-shaped args — platformId
  is already in the adapter's encoded format (e.g. "telegram:6037840640")
- Add triggerTyping() in delivery.ts, call from router on message route
- Enable Telegram channel in barrel
- Verified E2E: Telegram message in → agent → typing indicator → response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:36:45 +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
1dc5750ca3 fix: uncomment Discord import in channel barrel
Discord was directly imported in src/index.ts before the barrel wiring.
Moving to the barrel without uncommenting it broke Discord.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:24:06 +03:00
gavrielc
82cb363f84 v2: split session DB into inbound/outbound for write isolation
Eliminates SQLite write contention across the host-container mount
boundary by splitting the single session.db into two files, each with
exactly one writer:

  inbound.db  — host writes (messages_in, delivered tracking)
  outbound.db — container writes (messages_out, processing_ack)

Key changes:
- Host uses even seq numbers, container uses odd (collision-free)
- Container heartbeat via file touch instead of DB UPDATE
- Scheduling MCP tools now emit system actions via messages_out
  (host applies them to inbound.db during delivery)
- Host sweep reads processing_ack + heartbeat file for stale detection
- OneCLI ensureAgent() call added (was missing from v2, caused
  applyContainerConfig to reject unknown agent identifiers)

Verified: tsc clean, 327 tests pass, real e2e through Docker works.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:17:31 +03:00
gavrielc
2b64fec0e6 fix: clean up iMessage adapter type compatibility
Replace `as never` cast with proper polyfill for channelIdFromThreadId.
Narrow GatewayAdapter cast to only the gateway code path in bridge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:42:49 +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
gavrielc
12af451069 v2: add Chat SDK channel adapters and skills for 11 platforms
Thin wrapper adapters + SKILL.md for Slack, Telegram, GitHub, Linear,
Google Chat, Teams, WhatsApp Cloud API, Resend, Matrix, Webex, iMessage.
All follow the same pattern as discord-v2.ts: readEnvFile → create*Adapter
→ createChatSdkBridge → registerChannelAdapter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:26:33 +03:00
gavrielc
8a06b01646 v2: SQLite state adapter, admin commands, compact feedback
- Replace in-memory Chat SDK state with SqliteStateAdapter — thread
  subscriptions now persist across restarts
- Add migration 002 for chat_sdk_kv, subscriptions, locks, lists tables
- Handle /clear in agent-runner (reset sessionId) — SDK has
  supportsNonInteractive:false for this command
- Pass /compact, /context, /cost, /files through to SDK as admin commands
- Skip admin commands in follow-up poll so they start fresh queries
- Emit compact_boundary events as user-visible feedback messages
- Pass NANOCLAW_ADMIN_USER_ID and NANOCLAW_ASSISTANT_NAME to containers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 03:58:35 +03:00
gavrielc
c31bb02c06 v2 phase 5: pending questions with interactive cards
End-to-end ask_user_question flow:
- Agent MCP tool writes question card to messages_out
- Host delivery creates pending_questions row, delivers as Discord Card with buttons
- Local webhook server receives Gateway INTERACTION_CREATE events
- Acknowledges interaction + updates card to show selected answer
- Routes response back to session DB as system message
- MCP tool poll picks up response and returns to agent

Key fixes:
- Poll loop now skips system messages (reserved for MCP tool responses)
- Gateway listener uses webhookUrl forwarding mode for interaction support
- Button custom_id encodes questionId + option text for self-contained routing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 03:26:16 +03:00
gavrielc
c348fabf22 v2 phase 5: scheduling fixes, media handling, command processing
- Host sweep: fix DELETE journal mode, busy_timeout, seq in recurrence INSERT
- Outbound files: delivery reads from outbox dir, passes buffers to adapter,
  cleans up after delivery. Chat SDK bridge sends files via postMessage.
- Inbound attachments: formatter includes attachment info in prompts
- Commands: categorize /commands as admin, filtered, or passthrough.
  Admin commands check sender against NANOCLAW_ADMIN_USER_ID.
  Filtered commands silently dropped. Passthrough sent raw to agent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 02:59:33 +03:00
gavrielc
afbc20a6c4 v2 phase 4+5: Discord via Chat SDK, expanded MCP tools, message seq IDs
- Chat SDK bridge + Discord adapter (gateway listener, message routing)
- MCP tools refactored into modular structure: core (send_message, send_file,
  edit_message, add_reaction), scheduling (schedule/list/cancel/pause/resume
  tasks), interactive (ask_user_question, send_card), agents (send_to_agent)
- Message seq IDs: shared integer sequence across messages_in/out so agents
  see small numeric IDs instead of platform snowflakes
- busy_timeout=5000 for session DB (poll loop + MCP server concurrent access)
- Always copy agent-runner source to fix stale cache when non-index files change
- Seed script for Discord testing, e2e test script

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 02:53:39 +03:00
gavrielc
b36f127acc style: prettier formatting fixes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 01:40:52 +03:00
gavrielc
7201fe5032 v2 phase 4: channel adapter interface, registry, and host wiring
ChannelAdapter interface with setup/deliver/teardown/setTyping lifecycle.
Self-registration pattern via channel-registry. Host wiring in index-v2
bridges inbound messages to routeInbound and outbound delivery to adapters.

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