@openparachute/agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (501) hide show
  1. package/.claude/scheduled_tasks.lock +1 -0
  2. package/.claude/settings.json +5 -0
  3. package/.claude/skills/add-atomic-chat-tool/SKILL.md +243 -0
  4. package/.claude/skills/add-atomic-chat-tool/atomic-chat-mcp-stdio.ts +229 -0
  5. package/.claude/skills/add-codex/SKILL.md +161 -0
  6. package/.claude/skills/add-dashboard/SKILL.md +138 -0
  7. package/.claude/skills/add-dashboard/resources/dashboard-pusher.ts +495 -0
  8. package/.claude/skills/add-emacs/SKILL.md +296 -0
  9. package/.claude/skills/add-gcal-tool/SKILL.md +210 -0
  10. package/.claude/skills/add-gchat/REMOVE.md +6 -0
  11. package/.claude/skills/add-gchat/SKILL.md +92 -0
  12. package/.claude/skills/add-gchat/VERIFY.md +3 -0
  13. package/.claude/skills/add-github/REMOVE.md +6 -0
  14. package/.claude/skills/add-github/SKILL.md +148 -0
  15. package/.claude/skills/add-github/VERIFY.md +3 -0
  16. package/.claude/skills/add-gmail-tool/SKILL.md +229 -0
  17. package/.claude/skills/add-imessage/REMOVE.md +6 -0
  18. package/.claude/skills/add-imessage/SKILL.md +113 -0
  19. package/.claude/skills/add-imessage/VERIFY.md +3 -0
  20. package/.claude/skills/add-karpathy-llm-wiki/SKILL.md +110 -0
  21. package/.claude/skills/add-karpathy-llm-wiki/llm-wiki.md +75 -0
  22. package/.claude/skills/add-linear/REMOVE.md +6 -0
  23. package/.claude/skills/add-linear/SKILL.md +168 -0
  24. package/.claude/skills/add-linear/VERIFY.md +3 -0
  25. package/.claude/skills/add-macos-statusbar/SKILL.md +133 -0
  26. package/.claude/skills/add-macos-statusbar/add/src/statusbar.swift +147 -0
  27. package/.claude/skills/add-matrix/REMOVE.md +6 -0
  28. package/.claude/skills/add-matrix/SKILL.md +148 -0
  29. package/.claude/skills/add-matrix/VERIFY.md +3 -0
  30. package/.claude/skills/add-ollama-provider/SKILL.md +179 -0
  31. package/.claude/skills/add-ollama-tool/SKILL.md +193 -0
  32. package/.claude/skills/add-opencode/SKILL.md +229 -0
  33. package/.claude/skills/add-parallel/SKILL.md +290 -0
  34. package/.claude/skills/add-resend/REMOVE.md +6 -0
  35. package/.claude/skills/add-resend/SKILL.md +93 -0
  36. package/.claude/skills/add-resend/VERIFY.md +3 -0
  37. package/.claude/skills/add-signal/REMOVE.md +13 -0
  38. package/.claude/skills/add-signal/SKILL.md +318 -0
  39. package/.claude/skills/add-signal/VERIFY.md +5 -0
  40. package/.claude/skills/add-slack/REMOVE.md +6 -0
  41. package/.claude/skills/add-slack/SKILL.md +112 -0
  42. package/.claude/skills/add-slack/VERIFY.md +3 -0
  43. package/.claude/skills/add-teams/REMOVE.md +6 -0
  44. package/.claude/skills/add-teams/SKILL.md +207 -0
  45. package/.claude/skills/add-teams/VERIFY.md +3 -0
  46. package/.claude/skills/add-vercel/SKILL.md +147 -0
  47. package/.claude/skills/add-vercel/container-skills/vercel-cli/SKILL.md +103 -0
  48. package/.claude/skills/add-webex/REMOVE.md +6 -0
  49. package/.claude/skills/add-webex/SKILL.md +88 -0
  50. package/.claude/skills/add-webex/VERIFY.md +3 -0
  51. package/.claude/skills/add-wechat/REMOVE.md +49 -0
  52. package/.claude/skills/add-wechat/SKILL.md +170 -0
  53. package/.claude/skills/add-wechat/scripts/wire-dm.ts +172 -0
  54. package/.claude/skills/add-whatsapp/SKILL.md +264 -0
  55. package/.claude/skills/add-whatsapp-cloud/REMOVE.md +6 -0
  56. package/.claude/skills/add-whatsapp-cloud/SKILL.md +95 -0
  57. package/.claude/skills/add-whatsapp-cloud/VERIFY.md +3 -0
  58. package/.claude/skills/claw/SKILL.md +131 -0
  59. package/.claude/skills/claw/scripts/claw +374 -0
  60. package/.claude/skills/convert-to-apple-container/SKILL.md +212 -0
  61. package/.claude/skills/customize/SKILL.md +110 -0
  62. package/.claude/skills/debug/SKILL.md +349 -0
  63. package/.claude/skills/get-qodo-rules/SKILL.md +122 -0
  64. package/.claude/skills/get-qodo-rules/references/output-format.md +41 -0
  65. package/.claude/skills/get-qodo-rules/references/pagination.md +33 -0
  66. package/.claude/skills/get-qodo-rules/references/repository-scope.md +26 -0
  67. package/.claude/skills/init-first-agent/SKILL.md +120 -0
  68. package/.claude/skills/init-onecli/SKILL.md +270 -0
  69. package/.claude/skills/manage-channels/SKILL.md +87 -0
  70. package/.claude/skills/manage-mounts/SKILL.md +47 -0
  71. package/.claude/skills/migrate-from-openclaw/MIGRATE_CRONS.md +100 -0
  72. package/.claude/skills/migrate-from-openclaw/SKILL.md +447 -0
  73. package/.claude/skills/migrate-from-openclaw/scripts/discover-openclaw.ts +734 -0
  74. package/.claude/skills/migrate-from-openclaw/scripts/extract-channel-credentials.ts +476 -0
  75. package/.claude/skills/migrate-nanoclaw/SKILL.md +484 -0
  76. package/.claude/skills/migrate-nanoclaw/diagnostics.md +51 -0
  77. package/.claude/skills/qodo-pr-resolver/SKILL.md +326 -0
  78. package/.claude/skills/qodo-pr-resolver/resources/providers.md +329 -0
  79. package/.claude/skills/update-nanoclaw/SKILL.md +243 -0
  80. package/.claude/skills/update-nanoclaw/diagnostics.md +48 -0
  81. package/.claude/skills/update-skills/SKILL.md +130 -0
  82. package/.claude/skills/use-native-credential-proxy/SKILL.md +167 -0
  83. package/.claude/skills/x-integration/SKILL.md +417 -0
  84. package/.claude/skills/x-integration/agent.ts +243 -0
  85. package/.claude/skills/x-integration/host.ts +155 -0
  86. package/.claude/skills/x-integration/lib/browser.ts +148 -0
  87. package/.claude/skills/x-integration/lib/config.ts +62 -0
  88. package/.claude/skills/x-integration/scripts/like.ts +56 -0
  89. package/.claude/skills/x-integration/scripts/post.ts +66 -0
  90. package/.claude/skills/x-integration/scripts/quote.ts +80 -0
  91. package/.claude/skills/x-integration/scripts/reply.ts +74 -0
  92. package/.claude/skills/x-integration/scripts/retweet.ts +62 -0
  93. package/.claude/skills/x-integration/scripts/setup.ts +87 -0
  94. package/.github/CODEOWNERS +10 -0
  95. package/.github/PULL_REQUEST_TEMPLATE.md +18 -0
  96. package/.github/workflows/bump-version.yml +35 -0
  97. package/.github/workflows/ci.yml +39 -0
  98. package/.github/workflows/label-pr.yml +40 -0
  99. package/.github/workflows/update-tokens.yml +43 -0
  100. package/.husky/pre-commit +1 -0
  101. package/.mcp.json +3 -0
  102. package/.nvmrc +1 -0
  103. package/.parachute/module.json +14 -0
  104. package/.prettierrc +4 -0
  105. package/CHANGELOG.md +215 -0
  106. package/CLAUDE.md +307 -0
  107. package/CODE_OF_CONDUCT.md +128 -0
  108. package/CONTRIBUTING.md +159 -0
  109. package/CONTRIBUTORS.md +26 -0
  110. package/LICENSE +21 -0
  111. package/README.md +190 -0
  112. package/README_ja.md +194 -0
  113. package/README_zh.md +194 -0
  114. package/assets/nanoclaw-favicon.png +0 -0
  115. package/assets/nanoclaw-icon.png +0 -0
  116. package/assets/nanoclaw-logo-dark.png +0 -0
  117. package/assets/nanoclaw-logo.png +0 -0
  118. package/assets/nanoclaw-profile.jpeg +0 -0
  119. package/assets/nanoclaw-sales.png +0 -0
  120. package/assets/social-preview.jpg +0 -0
  121. package/config-examples/mount-allowlist.json +25 -0
  122. package/container/.dockerignore +2 -0
  123. package/container/CLAUDE.md +21 -0
  124. package/container/Dockerfile +121 -0
  125. package/container/agent-runner/bun.lock +243 -0
  126. package/container/agent-runner/package.json +22 -0
  127. package/container/agent-runner/scripts/sdk-signal-probe.ts +169 -0
  128. package/container/agent-runner/src/config.ts +55 -0
  129. package/container/agent-runner/src/db/connection.ts +267 -0
  130. package/container/agent-runner/src/db/index.ts +20 -0
  131. package/container/agent-runner/src/db/messages-in.ts +138 -0
  132. package/container/agent-runner/src/db/messages-out.ts +143 -0
  133. package/container/agent-runner/src/db/session-routing.ts +30 -0
  134. package/container/agent-runner/src/db/session-state.test.ts +100 -0
  135. package/container/agent-runner/src/db/session-state.ts +79 -0
  136. package/container/agent-runner/src/destinations.ts +135 -0
  137. package/container/agent-runner/src/formatter.test.ts +167 -0
  138. package/container/agent-runner/src/formatter.ts +260 -0
  139. package/container/agent-runner/src/index.ts +110 -0
  140. package/container/agent-runner/src/integration.test.ts +121 -0
  141. package/container/agent-runner/src/mcp-tools/agents.instructions.md +26 -0
  142. package/container/agent-runner/src/mcp-tools/agents.ts +66 -0
  143. package/container/agent-runner/src/mcp-tools/core.instructions.md +27 -0
  144. package/container/agent-runner/src/mcp-tools/core.ts +262 -0
  145. package/container/agent-runner/src/mcp-tools/index.ts +22 -0
  146. package/container/agent-runner/src/mcp-tools/interactive.instructions.md +22 -0
  147. package/container/agent-runner/src/mcp-tools/interactive.ts +169 -0
  148. package/container/agent-runner/src/mcp-tools/scheduling.instructions.md +40 -0
  149. package/container/agent-runner/src/mcp-tools/scheduling.ts +299 -0
  150. package/container/agent-runner/src/mcp-tools/self-mod.instructions.md +25 -0
  151. package/container/agent-runner/src/mcp-tools/self-mod.ts +120 -0
  152. package/container/agent-runner/src/mcp-tools/server.ts +54 -0
  153. package/container/agent-runner/src/mcp-tools/types.ts +6 -0
  154. package/container/agent-runner/src/poll-loop.test.ts +248 -0
  155. package/container/agent-runner/src/poll-loop.ts +437 -0
  156. package/container/agent-runner/src/providers/claude.ts +379 -0
  157. package/container/agent-runner/src/providers/factory.test.ts +19 -0
  158. package/container/agent-runner/src/providers/factory.ts +13 -0
  159. package/container/agent-runner/src/providers/index.ts +6 -0
  160. package/container/agent-runner/src/providers/mock.ts +77 -0
  161. package/container/agent-runner/src/providers/provider-registry.ts +33 -0
  162. package/container/agent-runner/src/providers/types.ts +82 -0
  163. package/container/agent-runner/src/scheduling/task-script.ts +121 -0
  164. package/container/agent-runner/src/timezone.test.ts +93 -0
  165. package/container/agent-runner/src/timezone.ts +107 -0
  166. package/container/agent-runner/tsconfig.json +14 -0
  167. package/container/build.sh +48 -0
  168. package/container/entrypoint.sh +16 -0
  169. package/container/skills/agent-browser/SKILL.md +159 -0
  170. package/container/skills/frontend-engineer/SKILL.md +157 -0
  171. package/container/skills/self-customize/SKILL.md +87 -0
  172. package/container/skills/slack-formatting/SKILL.md +94 -0
  173. package/container/skills/vercel-cli/SKILL.md +111 -0
  174. package/container/skills/welcome/SKILL.md +85 -0
  175. package/docs/APPLE-CONTAINER-NETWORKING.md +90 -0
  176. package/docs/BRANCH-FORK-MAINTENANCE.md +81 -0
  177. package/docs/README.md +25 -0
  178. package/docs/SDK_DEEP_DIVE.md +643 -0
  179. package/docs/SECURITY.md +162 -0
  180. package/docs/agent-runner-details.md +749 -0
  181. package/docs/api-details.md +365 -0
  182. package/docs/architecture-diagram.html +422 -0
  183. package/docs/architecture-diagram.md +215 -0
  184. package/docs/architecture.md +751 -0
  185. package/docs/audit/2026-04-30-channel-endpoint-audit.md +36 -0
  186. package/docs/build-and-runtime.md +80 -0
  187. package/docs/cross-mount-stress/README.md +112 -0
  188. package/docs/cross-mount-stress/container-writer-retry.mjs +55 -0
  189. package/docs/cross-mount-stress/container-writer-slow.mjs +42 -0
  190. package/docs/cross-mount-stress/container-writer.mjs +47 -0
  191. package/docs/cross-mount-stress/host-writer-retry.mjs +55 -0
  192. package/docs/cross-mount-stress/host-writer-slow.mjs +43 -0
  193. package/docs/cross-mount-stress/host-writer.mjs +47 -0
  194. package/docs/db-central.md +316 -0
  195. package/docs/db-session.md +183 -0
  196. package/docs/db.md +119 -0
  197. package/docs/design/2026-04-29-vault-management-ui.md +231 -0
  198. package/docs/design/2026-04-30-channel-wiring-rework.md +234 -0
  199. package/docs/design/2026-05-01-channel-wiring-approvals-deep-dive.md +272 -0
  200. package/docs/design/2026-05-02-channel-policy-and-approval-routing.md +250 -0
  201. package/docs/docker-sandboxes.md +359 -0
  202. package/docs/isolation-model.md +88 -0
  203. package/docs/ollama.md +79 -0
  204. package/docs/parachute-integration.md +109 -0
  205. package/docs/post-night-rebirth-reflections.md +151 -0
  206. package/eslint.config.js +32 -0
  207. package/package.json +54 -0
  208. package/pnpm-workspace.yaml +8 -0
  209. package/repo-tokens/README.md +113 -0
  210. package/repo-tokens/action.yml +186 -0
  211. package/repo-tokens/badge.svg +23 -0
  212. package/repo-tokens/examples/green.svg +14 -0
  213. package/repo-tokens/examples/red.svg +14 -0
  214. package/repo-tokens/examples/yellow-green.svg +14 -0
  215. package/repo-tokens/examples/yellow.svg +14 -0
  216. package/scripts/chat.ts +101 -0
  217. package/scripts/cleanup-sessions.sh +150 -0
  218. package/scripts/init-cli-agent.ts +171 -0
  219. package/scripts/init-first-agent.ts +377 -0
  220. package/scripts/parachute.ts +158 -0
  221. package/scripts/run-migrations.ts +105 -0
  222. package/scripts/sanity-live-poll.ts +95 -0
  223. package/scripts/seed-discord.ts +79 -0
  224. package/scripts/test-v2-agent.ts +106 -0
  225. package/scripts/test-v2-channel-e2e.ts +265 -0
  226. package/scripts/test-v2-host.ts +184 -0
  227. package/src/channels/adapter.ts +214 -0
  228. package/src/channels/ask-question.ts +46 -0
  229. package/src/channels/channel-registry.test.ts +421 -0
  230. package/src/channels/channel-registry.ts +313 -0
  231. package/src/channels/chat-sdk-bridge.test.ts +84 -0
  232. package/src/channels/chat-sdk-bridge.ts +652 -0
  233. package/src/channels/cli.ts +276 -0
  234. package/src/channels/discord.ts +90 -0
  235. package/src/channels/index.ts +17 -0
  236. package/src/channels/telegram-markdown-sanitize.test.ts +78 -0
  237. package/src/channels/telegram-markdown-sanitize.ts +55 -0
  238. package/src/channels/telegram-pairing.test.ts +254 -0
  239. package/src/channels/telegram-pairing.ts +339 -0
  240. package/src/channels/telegram.ts +279 -0
  241. package/src/channels/trust-hint.test.ts +48 -0
  242. package/src/channels/trust-hint.ts +75 -0
  243. package/src/claude-md-compose.migrate.test.ts +64 -0
  244. package/src/claude-md-compose.ts +205 -0
  245. package/src/command-gate.ts +63 -0
  246. package/src/config.test.ts +93 -0
  247. package/src/config.ts +108 -0
  248. package/src/container-config.ts +167 -0
  249. package/src/container-runner.test.ts +32 -0
  250. package/src/container-runner.ts +576 -0
  251. package/src/container-runtime.test.ts +169 -0
  252. package/src/container-runtime.ts +92 -0
  253. package/src/db/_bun-sqlite-shim.ts +88 -0
  254. package/src/db/agent-activity.test.ts +155 -0
  255. package/src/db/agent-activity.ts +121 -0
  256. package/src/db/agent-groups.ts +77 -0
  257. package/src/db/connection.migrate.test.ts +143 -0
  258. package/src/db/connection.ts +224 -0
  259. package/src/db/db-v2.test.ts +440 -0
  260. package/src/db/dropped-messages.ts +44 -0
  261. package/src/db/index.ts +40 -0
  262. package/src/db/messaging-groups.ts +252 -0
  263. package/src/db/migrations/001-initial.ts +112 -0
  264. package/src/db/migrations/002-chat-sdk-state.ts +36 -0
  265. package/src/db/migrations/008-dropped-messages.ts +27 -0
  266. package/src/db/migrations/009-drop-pending-credentials.ts +13 -0
  267. package/src/db/migrations/010-engage-modes.ts +103 -0
  268. package/src/db/migrations/011-pending-sender-approvals.ts +40 -0
  269. package/src/db/migrations/012-channel-registration.ts +48 -0
  270. package/src/db/migrations/013-approval-render-metadata.ts +27 -0
  271. package/src/db/migrations/014-secrets.ts +44 -0
  272. package/src/db/migrations/015-secrets-drop-host-pattern.ts +18 -0
  273. package/src/db/migrations/016-secret-assignments.ts +30 -0
  274. package/src/db/migrations/017-agent-activity.ts +40 -0
  275. package/src/db/migrations/018-oauth-app-configs.ts +34 -0
  276. package/src/db/migrations/019-oauth-app-connections.ts +48 -0
  277. package/src/db/migrations/020-agent-app-connections.ts +28 -0
  278. package/src/db/migrations/021-pending-oauth-states.ts +35 -0
  279. package/src/db/migrations/022-app-connections-provider.ts +25 -0
  280. package/src/db/migrations/023-agent-group-secret-mode.test.ts +124 -0
  281. package/src/db/migrations/023-agent-group-secret-mode.ts +65 -0
  282. package/src/db/migrations/024-collapse-approvals.test.ts +249 -0
  283. package/src/db/migrations/024-collapse-approvals.ts +182 -0
  284. package/src/db/migrations/025-secret-mode-check.test.ts +155 -0
  285. package/src/db/migrations/025-secret-mode-check.ts +49 -0
  286. package/src/db/migrations/026-user-dms-bot-id.test.ts +116 -0
  287. package/src/db/migrations/026-user-dms-bot-id.ts +54 -0
  288. package/src/db/migrations/027-provider-credentials.ts +41 -0
  289. package/src/db/migrations/_test-helpers.ts +41 -0
  290. package/src/db/migrations/index.ts +127 -0
  291. package/src/db/migrations/module-agent-to-agent-destinations.ts +84 -0
  292. package/src/db/migrations/module-approvals-pending-approvals.ts +42 -0
  293. package/src/db/migrations/module-approvals-title-options.ts +40 -0
  294. package/src/db/schema.ts +258 -0
  295. package/src/db/session-db.test.ts +93 -0
  296. package/src/db/session-db.ts +325 -0
  297. package/src/db/sessions.ts +241 -0
  298. package/src/delivery.test.ts +148 -0
  299. package/src/delivery.ts +445 -0
  300. package/src/env.ts +74 -0
  301. package/src/group-folder.test.ts +35 -0
  302. package/src/group-folder.ts +44 -0
  303. package/src/group-init.ts +92 -0
  304. package/src/host-core.test.ts +456 -0
  305. package/src/host-sweep.test.ts +146 -0
  306. package/src/host-sweep.ts +287 -0
  307. package/src/index.ts +227 -0
  308. package/src/install-slug.ts +33 -0
  309. package/src/log.test.ts +81 -0
  310. package/src/log.ts +117 -0
  311. package/src/mcp/http.ts +72 -0
  312. package/src/mcp/server.ts +92 -0
  313. package/src/mcp/stdio.ts +51 -0
  314. package/src/mcp/tools/activity.ts +88 -0
  315. package/src/mcp/tools/agent-groups.ts +183 -0
  316. package/src/mcp/tools/approvals.ts +122 -0
  317. package/src/mcp/tools/channels.ts +199 -0
  318. package/src/mcp/tools/index.ts +27 -0
  319. package/src/mcp/tools/oauth.ts +48 -0
  320. package/src/mcp/tools/secrets.ts +169 -0
  321. package/src/mcp/tools/sessions.ts +135 -0
  322. package/src/mcp/types.ts +51 -0
  323. package/src/modules/agent-to-agent/agent-route.test.ts +46 -0
  324. package/src/modules/agent-to-agent/agent-route.ts +223 -0
  325. package/src/modules/agent-to-agent/create-agent.ts +127 -0
  326. package/src/modules/agent-to-agent/db/agent-destinations.ts +135 -0
  327. package/src/modules/agent-to-agent/index.ts +22 -0
  328. package/src/modules/agent-to-agent/write-destinations.ts +59 -0
  329. package/src/modules/approvals/agent.md +45 -0
  330. package/src/modules/approvals/index.ts +21 -0
  331. package/src/modules/approvals/picks.test.ts +291 -0
  332. package/src/modules/approvals/primitive.ts +279 -0
  333. package/src/modules/approvals/project.md +27 -0
  334. package/src/modules/approvals/response-handler.ts +87 -0
  335. package/src/modules/index.ts +24 -0
  336. package/src/modules/interactive/agent.md +21 -0
  337. package/src/modules/interactive/index.ts +69 -0
  338. package/src/modules/interactive/project.md +12 -0
  339. package/src/modules/mount-security/index.ts +448 -0
  340. package/src/modules/mount-security/migrate.test.ts +91 -0
  341. package/src/modules/permissions/access.ts +28 -0
  342. package/src/modules/permissions/channel-approval.test.ts +389 -0
  343. package/src/modules/permissions/channel-approval.ts +188 -0
  344. package/src/modules/permissions/db/agent-group-members.ts +44 -0
  345. package/src/modules/permissions/db/pending-channel-approvals.test.ts +86 -0
  346. package/src/modules/permissions/db/pending-channel-approvals.ts +66 -0
  347. package/src/modules/permissions/db/pending-sender-approvals.ts +60 -0
  348. package/src/modules/permissions/db/user-dms.ts +58 -0
  349. package/src/modules/permissions/db/user-roles.ts +85 -0
  350. package/src/modules/permissions/db/users.ts +38 -0
  351. package/src/modules/permissions/index.ts +421 -0
  352. package/src/modules/permissions/permissions.test.ts +358 -0
  353. package/src/modules/permissions/sender-approval.test.ts +470 -0
  354. package/src/modules/permissions/sender-approval.ts +165 -0
  355. package/src/modules/permissions/user-dm.ts +200 -0
  356. package/src/modules/provider-credentials/db.ts +121 -0
  357. package/src/modules/provider-credentials/index.ts +12 -0
  358. package/src/modules/provider-credentials/spawn.test.ts +206 -0
  359. package/src/modules/provider-credentials/spawn.ts +114 -0
  360. package/src/modules/scheduling/actions.ts +113 -0
  361. package/src/modules/scheduling/db.test.ts +282 -0
  362. package/src/modules/scheduling/db.ts +148 -0
  363. package/src/modules/scheduling/index.ts +34 -0
  364. package/src/modules/scheduling/recurrence.test.ts +98 -0
  365. package/src/modules/scheduling/recurrence.ts +54 -0
  366. package/src/modules/self-mod/agent.md +30 -0
  367. package/src/modules/self-mod/apply.ts +85 -0
  368. package/src/modules/self-mod/index.ts +30 -0
  369. package/src/modules/self-mod/project.md +39 -0
  370. package/src/modules/self-mod/request.ts +91 -0
  371. package/src/modules/typing/index.ts +165 -0
  372. package/src/oauth/agent-app-connections.ts +103 -0
  373. package/src/oauth/app-configs.test.ts +64 -0
  374. package/src/oauth/app-configs.ts +114 -0
  375. package/src/oauth/app-connections.test.ts +109 -0
  376. package/src/oauth/app-connections.ts +178 -0
  377. package/src/oauth/crypto.ts +56 -0
  378. package/src/oauth/flow.ts +104 -0
  379. package/src/oauth/providers/google.test.ts +38 -0
  380. package/src/oauth/providers/google.ts +46 -0
  381. package/src/oauth/providers/index.ts +48 -0
  382. package/src/oauth/state-store.test.ts +54 -0
  383. package/src/oauth/state-store.ts +93 -0
  384. package/src/parachute/README.md +27 -0
  385. package/src/parachute/create-agent.test.ts +83 -0
  386. package/src/parachute/create-agent.ts +122 -0
  387. package/src/parachute/group-status.test.ts +165 -0
  388. package/src/parachute/group-status.ts +136 -0
  389. package/src/parachute/types.ts +41 -0
  390. package/src/parachute/vault-mcp.test.ts +251 -0
  391. package/src/parachute/vault-mcp.ts +232 -0
  392. package/src/platform-id.test.ts +104 -0
  393. package/src/platform-id.ts +109 -0
  394. package/src/providers/index.ts +6 -0
  395. package/src/providers/provider-container-registry.ts +58 -0
  396. package/src/response-registry.ts +45 -0
  397. package/src/router.ts +530 -0
  398. package/src/secrets/crypto.test.ts +45 -0
  399. package/src/secrets/crypto.ts +55 -0
  400. package/src/secrets/index.ts +355 -0
  401. package/src/secrets/master-key.ts +70 -0
  402. package/src/secrets/secrets.test.ts +354 -0
  403. package/src/session-manager.migrate.test.ts +59 -0
  404. package/src/session-manager.ts +433 -0
  405. package/src/startup-bootstrap.test.ts +226 -0
  406. package/src/startup-bootstrap.ts +207 -0
  407. package/src/state-sqlite.ts +182 -0
  408. package/src/timezone.test.ts +64 -0
  409. package/src/timezone.ts +37 -0
  410. package/src/types.ts +230 -0
  411. package/src/web/auth.test.ts +335 -0
  412. package/src/web/auth.ts +214 -0
  413. package/src/web/discord-validate.test.ts +77 -0
  414. package/src/web/discord-validate.ts +88 -0
  415. package/src/web/hub-discovery.test.ts +98 -0
  416. package/src/web/hub-discovery.ts +69 -0
  417. package/src/web/routes/activity.ts +106 -0
  418. package/src/web/routes/agent-provider.test.ts +282 -0
  419. package/src/web/routes/agent-provider.ts +309 -0
  420. package/src/web/routes/approvals.ts +185 -0
  421. package/src/web/routes/apps.ts +434 -0
  422. package/src/web/routes/channels-mg-detail.test.ts +324 -0
  423. package/src/web/routes/channels-mga-detail.test.ts +425 -0
  424. package/src/web/routes/channels.ts +489 -0
  425. package/src/web/routes/oauth-providers.ts +42 -0
  426. package/src/web/routes/secrets.test.ts +175 -0
  427. package/src/web/routes/secrets.ts +282 -0
  428. package/src/web/routes/sessions.ts +123 -0
  429. package/src/web/routes/settings.test.ts +106 -0
  430. package/src/web/routes/settings.ts +247 -0
  431. package/src/web/routes/setup-status.ts +205 -0
  432. package/src/web/routes/vaults.test.ts +389 -0
  433. package/src/web/routes/vaults.ts +225 -0
  434. package/src/web/server-version.test.ts +16 -0
  435. package/src/web/server.ts +1003 -0
  436. package/src/web/services-manifest.test.ts +120 -0
  437. package/src/web/services-manifest.ts +61 -0
  438. package/src/web/static-serve.test.ts +255 -0
  439. package/src/web/static-serve.ts +104 -0
  440. package/src/web/telegram-validate.test.ts +116 -0
  441. package/src/web/telegram-validate.ts +107 -0
  442. package/src/web/vault-proxy.test.ts +214 -0
  443. package/src/web/vault-proxy.ts +120 -0
  444. package/src/web/wire-channel.ts +181 -0
  445. package/src/webhook-server.ts +134 -0
  446. package/tsconfig.json +21 -0
  447. package/vitest.config.ts +18 -0
  448. package/web/README.md +63 -0
  449. package/web/ui/index.html +13 -0
  450. package/web/ui/package.json +35 -0
  451. package/web/ui/pnpm-lock.yaml +2164 -0
  452. package/web/ui/scripts/verify-base.mjs +31 -0
  453. package/web/ui/src/App.tsx +88 -0
  454. package/web/ui/src/components/ActivityFeed.tsx +444 -0
  455. package/web/ui/src/components/AgentGroupPicker.tsx +263 -0
  456. package/web/ui/src/components/AgentProviderCards.tsx +220 -0
  457. package/web/ui/src/components/CredentialForm.tsx +214 -0
  458. package/web/ui/src/components/ScopeGrants.tsx +74 -0
  459. package/web/ui/src/components/StatusDot.tsx +43 -0
  460. package/web/ui/src/components/VaultPicker.tsx +127 -0
  461. package/web/ui/src/components/setup/AdapterInstallStep.tsx +178 -0
  462. package/web/ui/src/components/setup/AgentGroupStep.tsx +43 -0
  463. package/web/ui/src/components/setup/ChannelPickStep.tsx +74 -0
  464. package/web/ui/src/components/setup/DoneStep.tsx +49 -0
  465. package/web/ui/src/components/setup/PrereqStep.tsx +129 -0
  466. package/web/ui/src/components/setup/TestConnectionStep.tsx +108 -0
  467. package/web/ui/src/components/setup/TestMessageStep.tsx +104 -0
  468. package/web/ui/src/components/setup/WireChannelStep.tsx +166 -0
  469. package/web/ui/src/components/setup/types.ts +105 -0
  470. package/web/ui/src/lib/api.test.ts +410 -0
  471. package/web/ui/src/lib/api.ts +1210 -0
  472. package/web/ui/src/lib/auth.test.ts +139 -0
  473. package/web/ui/src/lib/auth.ts +348 -0
  474. package/web/ui/src/lib/channel-adapters.ts +136 -0
  475. package/web/ui/src/main.tsx +19 -0
  476. package/web/ui/src/routes/ApprovalsList.tsx +294 -0
  477. package/web/ui/src/routes/Apps.tsx +613 -0
  478. package/web/ui/src/routes/ChannelWireDetail.test.tsx +233 -0
  479. package/web/ui/src/routes/ChannelWireDetail.tsx +403 -0
  480. package/web/ui/src/routes/ChannelsList.tsx +158 -0
  481. package/web/ui/src/routes/GroupDetail.tsx +755 -0
  482. package/web/ui/src/routes/GroupList.tsx +187 -0
  483. package/web/ui/src/routes/MessagingGroupDetail.test.tsx +233 -0
  484. package/web/ui/src/routes/MessagingGroupDetail.tsx +306 -0
  485. package/web/ui/src/routes/NewGroupWizard.tsx +390 -0
  486. package/web/ui/src/routes/OAuthCallback.tsx +56 -0
  487. package/web/ui/src/routes/SecretsList.tsx +921 -0
  488. package/web/ui/src/routes/SessionsList.tsx +220 -0
  489. package/web/ui/src/routes/SettingsAgentProvider.tsx +109 -0
  490. package/web/ui/src/routes/SettingsApprovals.tsx +234 -0
  491. package/web/ui/src/routes/SetupWizard.tsx +219 -0
  492. package/web/ui/src/routes/VaultDetail.test.tsx +361 -0
  493. package/web/ui/src/routes/VaultDetail.tsx +960 -0
  494. package/web/ui/src/routes/VaultsList.tsx +295 -0
  495. package/web/ui/src/routes/WireChannelPage.tsx +413 -0
  496. package/web/ui/src/styles.css +608 -0
  497. package/web/ui/src/test/setup.ts +23 -0
  498. package/web/ui/src/vite-env.d.ts +10 -0
  499. package/web/ui/tsconfig.json +20 -0
  500. package/web/ui/vite.config.ts +34 -0
  501. package/web/ui/vitest.config.ts +25 -0
package/src/router.ts ADDED
@@ -0,0 +1,530 @@
1
+ /**
2
+ * Inbound message routing.
3
+ *
4
+ * Channel adapter event → resolve messaging group → sender resolver →
5
+ * resolve/pick agent → access gate → resolve/create session → write
6
+ * messages_in → wake container.
7
+ *
8
+ * Two module hooks (registered by the permissions module):
9
+ * - `setSenderResolver` runs BEFORE agent resolution so user rows get
10
+ * upserted even if the message ends up dropped by agent wiring.
11
+ * Without the module, userId is null and downstream code tolerates it.
12
+ * - `setAccessGate` runs AFTER agent resolution so policy decisions can
13
+ * branch on the target agent group. Without the module, access is
14
+ * allow-all.
15
+ *
16
+ * `dropped_messages` is core audit infra. Core writes rows for structural
17
+ * drops (no agent wired, no trigger match); the access gate writes rows
18
+ * for policy refusals.
19
+ */
20
+ import { getChannelAdapter } from './channels/channel-registry.js';
21
+ import { consumeTrustHint } from './channels/trust-hint.js';
22
+ import { gateCommand } from './command-gate.js';
23
+ import { getAgentGroup, getAllAgentGroups } from './db/agent-groups.js';
24
+ import { recordDroppedMessage } from './db/dropped-messages.js';
25
+ import {
26
+ createMessagingGroup,
27
+ getMessagingGroupAgents,
28
+ getMessagingGroupWithAgentCount,
29
+ updateMessagingGroup,
30
+ } from './db/messaging-groups.js';
31
+ import { decodePlatformIdAs } from './platform-id.js';
32
+ import { wireDmToAgent } from './web/wire-channel.js';
33
+ import { findSessionForAgent } from './db/sessions.js';
34
+ import { startTypingRefresh } from './modules/typing/index.js';
35
+ import { log } from './log.js';
36
+ import { resolveSession, writeSessionMessage, writeOutboundDirect } from './session-manager.js';
37
+ import { wakeContainer } from './container-runner.js';
38
+ import { getSession } from './db/sessions.js';
39
+ import type { AgentGroup, MessagingGroup, MessagingGroupAgent } from './types.js';
40
+ import type { InboundEvent } from './channels/adapter.js';
41
+
42
+ function generateId(): string {
43
+ return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
44
+ }
45
+
46
+ /**
47
+ * Sender-resolver hook. Runs before agent resolution.
48
+ *
49
+ * The permissions module registers this to extract the sender's namespaced
50
+ * user id and upsert the users row. Returns null when the payload doesn't
51
+ * carry enough info to identify a sender. Without the hook, every message
52
+ * arrives at the gate with userId=null.
53
+ */
54
+ export type SenderResolverFn = (event: InboundEvent) => string | null;
55
+
56
+ let senderResolver: SenderResolverFn | null = null;
57
+
58
+ export function setSenderResolver(fn: SenderResolverFn): void {
59
+ if (senderResolver) {
60
+ log.warn('Sender resolver overwritten');
61
+ }
62
+ senderResolver = fn;
63
+ }
64
+
65
+ /**
66
+ * Access-gate hook. Runs after agent resolution.
67
+ *
68
+ * The permissions module registers this; without it, core defaults to
69
+ * allow-all. The gate receives the raw event so it can extract the sender
70
+ * name for audit-trail purposes, and it is responsible for recording its
71
+ * own `dropped_messages` row on refusal (structural drops are already
72
+ * recorded by core before the gate runs).
73
+ */
74
+ export type AccessGateResult = { allowed: true } | { allowed: false; reason: string };
75
+
76
+ export type AccessGateFn = (
77
+ event: InboundEvent,
78
+ userId: string | null,
79
+ mg: MessagingGroup,
80
+ agentGroupId: string,
81
+ ) => AccessGateResult;
82
+
83
+ let accessGate: AccessGateFn | null = null;
84
+
85
+ export function setAccessGate(fn: AccessGateFn): void {
86
+ if (accessGate) {
87
+ log.warn('Access gate overwritten');
88
+ }
89
+ accessGate = fn;
90
+ }
91
+
92
+ /**
93
+ * Per-wiring sender-scope hook. Runs alongside the access gate for each
94
+ * agent that would otherwise engage — lets the permissions module enforce
95
+ * `sender_scope='known'` on wirings that are stricter than the messaging
96
+ * group's `unknown_sender_policy`. When the hook isn't registered (module
97
+ * not installed), sender_scope is a no-op.
98
+ */
99
+ export type SenderScopeGateFn = (
100
+ event: InboundEvent,
101
+ userId: string | null,
102
+ mg: MessagingGroup,
103
+ agent: MessagingGroupAgent,
104
+ ) => AccessGateResult;
105
+
106
+ let senderScopeGate: SenderScopeGateFn | null = null;
107
+
108
+ export function setSenderScopeGate(fn: SenderScopeGateFn): void {
109
+ if (senderScopeGate) {
110
+ log.warn('Sender-scope gate overwritten');
111
+ }
112
+ senderScopeGate = fn;
113
+ }
114
+
115
+ /**
116
+ * Channel-registration hook. Runs when the router sees a mention/DM on a
117
+ * messaging group that has no wirings AND hasn't been denied. The hook is
118
+ * expected to escalate to an owner (card, etc.) and arrange for future
119
+ * replay via routeInbound after approval. Fire-and-forget from the
120
+ * router's perspective.
121
+ *
122
+ * Registered by the permissions module. Without the module the router
123
+ * silently records the drop with reason='no_agent_wired' and moves on.
124
+ */
125
+ export type ChannelRequestGateFn = (mg: MessagingGroup, event: InboundEvent) => Promise<void>;
126
+
127
+ let channelRequestGate: ChannelRequestGateFn | null = null;
128
+
129
+ export function setChannelRequestGate(fn: ChannelRequestGateFn): void {
130
+ if (channelRequestGate) {
131
+ log.warn('Channel-request gate overwritten');
132
+ }
133
+ channelRequestGate = fn;
134
+ }
135
+
136
+ function safeParseContent(raw: string): { text?: string; sender?: string; senderId?: string } {
137
+ try {
138
+ return JSON.parse(raw);
139
+ } catch {
140
+ return { text: raw };
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Route an inbound message from a channel adapter to the correct session.
146
+ * Creates messaging group + session if they don't exist yet.
147
+ */
148
+ export async function routeInbound(event: InboundEvent): Promise<void> {
149
+ // 0. Apply the adapter's thread policy. Non-threaded adapters (Telegram,
150
+ // WhatsApp, iMessage, email) collapse threads to the channel.
151
+ // By-channel-type (not by-bot) lookup is correct here: we only read
152
+ // `supportsThreads`, which is a property of the channel itself, not of
153
+ // a specific bot identity. Per-bot resolution (`getChannelAdapterForPlatformId`)
154
+ // is reserved for delivery, where the outbound adapter must match the
155
+ // bot dimension encoded in the v2 platform_id.
156
+ const adapter = getChannelAdapter(event.channelType);
157
+ if (adapter && !adapter.supportsThreads) {
158
+ event = { ...event, threadId: null };
159
+ }
160
+
161
+ const isMention = event.message.isMention === true;
162
+
163
+ // 1. Combined lookup: messaging_group row + count of wired agents in a
164
+ // single query. Cheap short-circuit for the common "unwired channel"
165
+ // case — one DB read and we're out, no auto-create, no sender
166
+ // resolution, no log spam.
167
+ const found = getMessagingGroupWithAgentCount(event.channelType, event.platformId);
168
+
169
+ let mg: MessagingGroup;
170
+ let agentCount: number;
171
+ if (!found) {
172
+ // No messaging_groups row. Auto-create only when the message warrants
173
+ // attention (the bot was addressed — @mention or DM). Plain chatter in
174
+ // channels we merely sit in stays silent — no row, no DB writes.
175
+ if (!isMention) return;
176
+ const mgId = `mg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
177
+ mg = {
178
+ id: mgId,
179
+ channel_type: event.channelType,
180
+ platform_id: event.platformId,
181
+ name: null,
182
+ is_group: event.message.isGroup ? 1 : 0,
183
+ unknown_sender_policy: 'request_approval',
184
+ denied_at: null,
185
+ created_at: new Date().toISOString(),
186
+ };
187
+ createMessagingGroup(mg);
188
+ log.info('Auto-created messaging group', {
189
+ id: mgId,
190
+ channelType: event.channelType,
191
+ platformId: event.platformId,
192
+ });
193
+ agentCount = 0;
194
+ } else {
195
+ mg = found.mg;
196
+ agentCount = found.agentCount;
197
+ }
198
+
199
+ // 1b. No wirings — either silent drop (plain chatter / denied channel) or
200
+ // escalate to owner for channel-registration approval.
201
+ if (agentCount === 0) {
202
+ if (!isMention) return;
203
+ if (mg.denied_at) {
204
+ log.debug('Message dropped — channel was denied by owner', {
205
+ messagingGroupId: mg.id,
206
+ deniedAt: mg.denied_at,
207
+ });
208
+ return;
209
+ }
210
+
211
+ const parsedUnwired = safeParseContent(event.message.content);
212
+
213
+ // Trust hint: if the operator just wired this bot at /channels/new and
214
+ // is now DM'ing it, treat the message as trusted self-traffic instead
215
+ // of escalating through the channel-registration approval flow. The
216
+ // hint is single-use and bound to (channelType, botId, operatorUserId)
217
+ // — Discord wires record no hint (no operator user id captured), so
218
+ // this branch only fires for Telegram self-DMs in the trust window.
219
+ const decoded = decodePlatformIdAs(event.platformId, 'v2');
220
+ const senderId = parsedUnwired.senderId;
221
+ if (decoded.botId && senderId && consumeTrustHint(event.channelType, decoded.botId, senderId)) {
222
+ const targetGroups = getAllAgentGroups();
223
+ if (targetGroups.length === 0) {
224
+ // Hint already consumed (single-use) but there's no agent group to
225
+ // wire to. Falls through to the approval cascade below; surface a
226
+ // warn so an operator who hits this can correlate it with their
227
+ // missing agent-group setup.
228
+ log.warn('Trust hint consumed but no agent groups — operator message dropped', {
229
+ messagingGroupId: mg.id,
230
+ channelType: event.channelType,
231
+ botId: decoded.botId,
232
+ });
233
+ }
234
+ if (targetGroups.length > 0) {
235
+ // Multi-agent-group installs: this picks the first group by DB
236
+ // insert order. Good enough for the trust-hint use case (operator
237
+ // just wired a bot and is DM'ing it now — usually the install only
238
+ // has one group anyway), but a richer "wire to the most recently
239
+ // wired group" or operator-prompted pick is a follow-up if needed.
240
+ const target = targetGroups[0]!;
241
+ log.info('Channel inbound auto-wired via operator trust hint', {
242
+ messagingGroupId: mg.id,
243
+ channelType: event.channelType,
244
+ botId: decoded.botId,
245
+ targetAgentGroupId: target.id,
246
+ });
247
+ // Drop the auto-created request_approval row in favor of a fresh
248
+ // wire built with the trusted defaults (strict policy, all-senders
249
+ // MGA). wireDmToAgent is idempotent — if we beat it to a wire that
250
+ // already exists, it returns the existing rows.
251
+ updateMessagingGroup(mg.id, { unknown_sender_policy: 'strict' });
252
+ wireDmToAgent({
253
+ channelType: event.channelType as 'discord' | 'telegram',
254
+ agentGroup: target,
255
+ botId: decoded.botId,
256
+ botUserId: decoded.native,
257
+ });
258
+ // Re-run the standard route from the now-wired path so engage
259
+ // checks, sender resolution, and session creation behave exactly
260
+ // as if this were a normal message on a pre-wired channel.
261
+ await routeInbound(event);
262
+ return;
263
+ }
264
+ }
265
+
266
+ recordDroppedMessage({
267
+ channel_type: event.channelType,
268
+ platform_id: event.platformId,
269
+ user_id: null,
270
+ sender_name: parsedUnwired.sender ?? null,
271
+ reason: 'no_agent_wired',
272
+ messaging_group_id: mg.id,
273
+ agent_group_id: null,
274
+ });
275
+
276
+ if (channelRequestGate) {
277
+ // Fire-and-forget escalation. The gate is expected to build a card,
278
+ // persist pending_channel_approvals, and replay the event via
279
+ // routeInbound after approval. Errors are logged internally — the
280
+ // user's message still stays dropped here either way.
281
+ void channelRequestGate(mg, event).catch((err) =>
282
+ log.error('Channel-request gate threw', { messagingGroupId: mg.id, err }),
283
+ );
284
+ } else {
285
+ log.warn('MESSAGE DROPPED — no agent groups wired and no channel-request gate registered', {
286
+ messagingGroupId: mg.id,
287
+ channelType: event.channelType,
288
+ platformId: event.platformId,
289
+ });
290
+ }
291
+ return;
292
+ }
293
+
294
+ // 2. Sender resolution (permissions module upserts the users row as a
295
+ // side effect so later role/access lookups find a real record).
296
+ // Without the module, userId is null — downstream tolerates it.
297
+ const userId: string | null = senderResolver ? senderResolver(event) : null;
298
+
299
+ // 3. Fetch wired agents in full (we already know the count is > 0; now
300
+ // we need their actual rows for fan-out).
301
+ const agents = getMessagingGroupAgents(mg.id);
302
+
303
+ // 4. Fan-out: evaluate each wired agent independently against engage_mode,
304
+ // sender_scope, and access gate. An agent that engages gets its own
305
+ // session and container wake. An agent that declines but has
306
+ // ignored_message_policy='accumulate' still gets the message stored in
307
+ // its session (trigger=0) so the context is available when it does
308
+ // engage later. Drop policy = skip silently.
309
+ //
310
+ // Subscribe (for mention-sticky wirings on threaded platforms) fires
311
+ // once per message from this loop — the first engaging mention-sticky
312
+ // wiring triggers adapter.subscribe(...); subsequent wirings don't
313
+ // re-subscribe (chat.subscribe is idempotent anyway, but the flag
314
+ // avoids the extra await).
315
+ const parsed = safeParseContent(event.message.content);
316
+ const messageText = parsed.text ?? '';
317
+
318
+ let engagedCount = 0;
319
+ let accumulatedCount = 0;
320
+ let subscribed = false;
321
+
322
+ for (const agent of agents) {
323
+ const agentGroup = getAgentGroup(agent.agent_group_id);
324
+ if (!agentGroup) continue;
325
+
326
+ const engages = evaluateEngage(agent, messageText, isMention, mg, event.threadId);
327
+
328
+ const accessOk = engages && (!accessGate || accessGate(event, userId, mg, agent.agent_group_id).allowed);
329
+ const scopeOk = engages && (!senderScopeGate || senderScopeGate(event, userId, mg, agent).allowed);
330
+
331
+ if (engages && accessOk && scopeOk) {
332
+ await deliverToAgent(agent, agentGroup, mg, event, userId, adapter?.supportsThreads === true, true);
333
+ engagedCount++;
334
+
335
+ // Mention-sticky: ask the adapter to subscribe the thread so the
336
+ // platform's subscribed-message path carries follow-ups without
337
+ // requiring another @mention. Threaded-adapter only; DMs and
338
+ // non-threaded platforms skip.
339
+ if (
340
+ !subscribed &&
341
+ agent.engage_mode === 'mention-sticky' &&
342
+ adapter?.supportsThreads &&
343
+ adapter.subscribe &&
344
+ event.threadId !== null &&
345
+ mg.is_group !== 0
346
+ ) {
347
+ subscribed = true;
348
+ // Fire-and-forget — subscribe is platform-side bookkeeping and
349
+ // shouldn't block message routing. Errors are logged inside the
350
+ // adapter (or by the promise rejection handler below).
351
+ void adapter.subscribe(event.platformId, event.threadId).catch((err) => {
352
+ log.warn('adapter.subscribe failed', { channelType: event.channelType, threadId: event.threadId, err });
353
+ });
354
+ }
355
+ } else if (agent.ignored_message_policy === 'accumulate') {
356
+ await deliverToAgent(agent, agentGroup, mg, event, userId, adapter?.supportsThreads === true, false);
357
+ accumulatedCount++;
358
+ } else {
359
+ log.debug('Message not engaged for agent (drop policy)', {
360
+ agentGroupId: agent.agent_group_id,
361
+ engage_mode: agent.engage_mode,
362
+ engages,
363
+ accessOk,
364
+ scopeOk,
365
+ });
366
+ }
367
+ }
368
+
369
+ if (engagedCount + accumulatedCount === 0) {
370
+ recordDroppedMessage({
371
+ channel_type: event.channelType,
372
+ platform_id: event.platformId,
373
+ user_id: userId,
374
+ sender_name: parsed.sender ?? null,
375
+ reason: 'no_agent_engaged',
376
+ messaging_group_id: mg.id,
377
+ agent_group_id: null,
378
+ });
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Decide whether a given wired agent should engage on this message.
384
+ *
385
+ * 'pattern' — regex test on text; '.' = always
386
+ * 'mention' — bot must be mentioned on the platform. Resolved by
387
+ * the adapter (SDK-level) and forwarded as
388
+ * `event.message.isMention`. Agent display name
389
+ * (`agent_group.name`) is irrelevant — users address
390
+ * the bot via its platform username (@botname on
391
+ * Telegram, user-id mention on Slack/Discord), not
392
+ * via the agent's Paraclaw-side display name. If a
393
+ * user wants to disambiguate between multiple agents
394
+ * wired to one chat, use engage_mode='pattern' with
395
+ * the disambiguator as the regex.
396
+ * 'mention-sticky' — platform mention OR an active per-thread session
397
+ * already exists for this (agent, mg, thread). The
398
+ * session existence IS our subscription state; once
399
+ * a thread has engaged us once, follow-ups arrive
400
+ * with no mention and should still fire.
401
+ */
402
+ function evaluateEngage(
403
+ agent: MessagingGroupAgent,
404
+ text: string,
405
+ isMention: boolean,
406
+ mg: MessagingGroup,
407
+ threadId: string | null,
408
+ ): boolean {
409
+ switch (agent.engage_mode) {
410
+ case 'pattern': {
411
+ const pat = agent.engage_pattern ?? '.';
412
+ if (pat === '.') return true;
413
+ try {
414
+ return new RegExp(pat).test(text);
415
+ } catch {
416
+ // Bad regex: fail open so admin sees the agent responding + can fix.
417
+ return true;
418
+ }
419
+ }
420
+ case 'mention':
421
+ return isMention;
422
+ case 'mention-sticky': {
423
+ if (isMention) return true;
424
+ // Sticky follow-up: session already exists for this (agent, mg, thread)
425
+ // — the thread was activated before, keep firing.
426
+ if (mg.is_group === 0) return false; // DMs never use mention-sticky sensibly
427
+ const existing = findSessionForAgent(agent.agent_group_id, mg.id, threadId);
428
+ return existing !== undefined;
429
+ }
430
+ default:
431
+ return false;
432
+ }
433
+ }
434
+
435
+ async function deliverToAgent(
436
+ agent: MessagingGroupAgent,
437
+ agentGroup: AgentGroup,
438
+ mg: MessagingGroup,
439
+ event: InboundEvent,
440
+ userId: string | null,
441
+ adapterSupportsThreads: boolean,
442
+ wake: boolean,
443
+ ): Promise<void> {
444
+ // Apply the adapter thread policy: threaded adapter in a group chat →
445
+ // per-thread session regardless of wiring. agent-shared preserved (it's
446
+ // a cross-channel directive the adapter doesn't know about). DMs collapse
447
+ // sub-threads to one session (is_group=0 short-circuit).
448
+ let effectiveSessionMode = agent.session_mode;
449
+ if (adapterSupportsThreads && effectiveSessionMode !== 'agent-shared' && mg.is_group !== 0) {
450
+ effectiveSessionMode = 'per-thread';
451
+ }
452
+
453
+ const { session, created } = resolveSession(agent.agent_group_id, mg.id, event.threadId, effectiveSessionMode);
454
+
455
+ // The inbound row's (channel_type, platform_id, thread_id) is the address
456
+ // the agent's reply will be delivered to. Normally it mirrors the source
457
+ // (stamped from the event). When the caller supplied `replyTo` (CLI admin
458
+ // transport acting on operator intent), the reply is redirected there.
459
+ const deliveryAddr = event.replyTo ?? {
460
+ channelType: event.channelType,
461
+ platformId: event.platformId,
462
+ threadId: event.threadId,
463
+ };
464
+
465
+ // Command gate: classify slash commands before they reach the container.
466
+ // Filtered commands are dropped silently. Denied admin commands get a
467
+ // permission-denied response written directly to messages_out.
468
+ if (event.message.kind === 'chat' || event.message.kind === 'chat-sdk') {
469
+ const gate = gateCommand(event.message.content, userId, agent.agent_group_id);
470
+ if (gate.action === 'filter') {
471
+ log.debug('Filtered command dropped by gate', { agentGroupId: agent.agent_group_id });
472
+ return;
473
+ }
474
+ if (gate.action === 'deny') {
475
+ writeOutboundDirect(session.agent_group_id, session.id, {
476
+ id: `deny-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
477
+ kind: 'chat',
478
+ platformId: deliveryAddr.platformId,
479
+ channelType: deliveryAddr.channelType,
480
+ threadId: deliveryAddr.threadId,
481
+ content: JSON.stringify({ text: `Permission denied: ${gate.command} requires admin access.` }),
482
+ });
483
+ log.info('Admin command denied by gate', { command: gate.command, userId, agentGroupId: agent.agent_group_id });
484
+ return;
485
+ }
486
+ }
487
+
488
+ writeSessionMessage(session.agent_group_id, session.id, {
489
+ id: messageIdForAgent(event.message.id, agent.agent_group_id),
490
+ kind: event.message.kind,
491
+ timestamp: event.message.timestamp,
492
+ platformId: deliveryAddr.platformId,
493
+ channelType: deliveryAddr.channelType,
494
+ threadId: deliveryAddr.threadId,
495
+ content: event.message.content,
496
+ trigger: wake ? 1 : 0,
497
+ });
498
+
499
+ log.info('Message routed', {
500
+ sessionId: session.id,
501
+ agentGroup: agent.agent_group_id,
502
+ engage_mode: agent.engage_mode,
503
+ kind: event.message.kind,
504
+ userId,
505
+ wake,
506
+ created,
507
+ agentGroupName: agentGroup.name,
508
+ });
509
+
510
+ if (wake) {
511
+ // Typing indicator + wake are only for the engaged branch; accumulated
512
+ // messages sit silently until a real trigger fires.
513
+ startTypingRefresh(session.id, session.agent_group_id, event.channelType, event.platformId, event.threadId);
514
+ const freshSession = getSession(session.id);
515
+ if (freshSession) {
516
+ await wakeContainer(freshSession);
517
+ }
518
+ }
519
+ }
520
+
521
+ /**
522
+ * When fanning out, the same inbound message lands in multiple per-agent
523
+ * session DBs. messages_in.id is PRIMARY KEY, so reuse of the raw id would
524
+ * collide across sessions (or, more subtly, within one session if re-routed
525
+ * after a retry). Namespace by agent_group_id to keep ids unique per session.
526
+ */
527
+ function messageIdForAgent(baseId: string | undefined, agentGroupId: string): string {
528
+ const id = baseId && baseId.length > 0 ? baseId : generateId();
529
+ return `${id}:${agentGroupId}`;
530
+ }
@@ -0,0 +1,45 @@
1
+ import crypto from 'crypto';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { decryptSecret, encryptSecret } from './crypto.js';
5
+
6
+ describe('secret crypto', () => {
7
+ const key = crypto.randomBytes(32);
8
+
9
+ it('round-trips a plaintext value', () => {
10
+ const ct = encryptSecret('xoxb-1234-secret', key);
11
+ expect(decryptSecret(ct, key)).toBe('xoxb-1234-secret');
12
+ });
13
+
14
+ it('produces a different ciphertext each call (random IV)', () => {
15
+ const a = encryptSecret('same-value', key);
16
+ const b = encryptSecret('same-value', key);
17
+ expect(a).not.toBe(b);
18
+ expect(decryptSecret(a, key)).toBe('same-value');
19
+ expect(decryptSecret(b, key)).toBe('same-value');
20
+ });
21
+
22
+ it('rejects tampered ciphertext', () => {
23
+ const ct = encryptSecret('original', key);
24
+ const buf = Buffer.from(ct, 'base64');
25
+ buf[buf.length - 1] ^= 0x01;
26
+ const tampered = buf.toString('base64');
27
+ expect(() => decryptSecret(tampered, key)).toThrow();
28
+ });
29
+
30
+ it('rejects ciphertext encrypted under a different key', () => {
31
+ const otherKey = crypto.randomBytes(32);
32
+ const ct = encryptSecret('only-original-key', key);
33
+ expect(() => decryptSecret(ct, otherKey)).toThrow();
34
+ });
35
+
36
+ it('handles empty strings', () => {
37
+ const ct = encryptSecret('', key);
38
+ expect(decryptSecret(ct, key)).toBe('');
39
+ });
40
+
41
+ it('handles unicode', () => {
42
+ const v = '🔐 résumé — 秘密';
43
+ expect(decryptSecret(encryptSecret(v, key), key)).toBe(v);
44
+ });
45
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * AES-256-GCM encryption for secret values.
3
+ *
4
+ * Wire format (base64-encoded):
5
+ * 12-byte IV || ciphertext || 16-byte auth tag
6
+ *
7
+ * Each call generates a fresh random IV — never reuse an IV with the same
8
+ * key (catastrophic for GCM). The auth tag is appended so decryption fails
9
+ * loudly on tampering.
10
+ *
11
+ * Domain separation: encryptSecret/decryptSecret accept a 32-byte key. Callers
12
+ * MUST NOT pass the raw master key — they pass a per-domain HKDF derivation
13
+ * (see `deriveKey` below). That way if a future subsystem (e.g. an outbox
14
+ * cookie signer) needs symmetric crypto from the same master, its key is
15
+ * cryptographically separated and a bug in one domain can't decrypt the other.
16
+ */
17
+ import crypto from 'crypto';
18
+
19
+ const ALGO = 'aes-256-gcm';
20
+ const IV_LEN = 12;
21
+ const TAG_LEN = 16;
22
+ const KEY_LEN = 32;
23
+
24
+ /**
25
+ * HKDF-SHA256 with an empty salt and a domain-specific `info` string. The
26
+ * empty salt is fine — the master key is already 256 bits of CSPRNG output,
27
+ * so HKDF degenerates to HKDF-Expand and the domain-tag in `info` does the
28
+ * real work. Use `paraclaw.<subsystem>.v<n>`; bumping `v` is a key rotation
29
+ * for that subsystem only.
30
+ */
31
+ export function deriveKey(masterKey: Buffer, info: string): Buffer {
32
+ if (masterKey.length !== KEY_LEN) {
33
+ throw new Error(`master key must be ${KEY_LEN} bytes, got ${masterKey.length}`);
34
+ }
35
+ return Buffer.from(crypto.hkdfSync('sha256', masterKey, Buffer.alloc(0), info, KEY_LEN));
36
+ }
37
+
38
+ export function encryptSecret(plaintext: string, key: Buffer): string {
39
+ const iv = crypto.randomBytes(IV_LEN);
40
+ const cipher = crypto.createCipheriv(ALGO, key, iv);
41
+ const ct = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
42
+ const tag = cipher.getAuthTag();
43
+ return Buffer.concat([iv, ct, tag]).toString('base64');
44
+ }
45
+
46
+ export function decryptSecret(encoded: string, key: Buffer): string {
47
+ const buf = Buffer.from(encoded, 'base64');
48
+ if (buf.length < IV_LEN + TAG_LEN) throw new Error('ciphertext too short');
49
+ const iv = buf.subarray(0, IV_LEN);
50
+ const tag = buf.subarray(buf.length - TAG_LEN);
51
+ const ct = buf.subarray(IV_LEN, buf.length - TAG_LEN);
52
+ const decipher = crypto.createDecipheriv(ALGO, key, iv);
53
+ decipher.setAuthTag(tag);
54
+ return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8');
55
+ }