@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
@@ -0,0 +1,421 @@
1
+ /**
2
+ * Permissions module — sender resolution + access gate.
3
+ *
4
+ * Registers two hooks into the core router:
5
+ * 1. setSenderResolver — runs before agent resolution. Parses the payload,
6
+ * derives a namespaced user id, and upserts the `users` row on first
7
+ * sight. Returns null when the payload doesn't carry enough to identify
8
+ * a sender.
9
+ * 2. setAccessGate — runs after agent resolution. Enforces the
10
+ * unknown_sender_policy (strict/request_approval/public) and the
11
+ * owner/global-admin/scoped-admin/member access hierarchy. Records its
12
+ * own `dropped_messages` row on refusal (structural drops are recorded
13
+ * by core).
14
+ *
15
+ * Without this module: sender resolution is a no-op (userId=null); the
16
+ * access gate is not registered and core defaults to allow-all.
17
+ */
18
+ import { recordDroppedMessage } from '../../db/dropped-messages.js';
19
+ import {
20
+ createMessagingGroupAgent,
21
+ getMessagingGroup,
22
+ setMessagingGroupDeniedAt,
23
+ updateMessagingGroup,
24
+ } from '../../db/messaging-groups.js';
25
+ import {
26
+ routeInbound,
27
+ setAccessGate,
28
+ setChannelRequestGate,
29
+ setSenderResolver,
30
+ setSenderScopeGate,
31
+ type AccessGateResult,
32
+ } from '../../router.js';
33
+ import type { InboundEvent } from '../../channels/adapter.js';
34
+ import { registerResponseHandler, type ResponsePayload } from '../../response-registry.js';
35
+ import { log } from '../../log.js';
36
+ import type { MessagingGroup, MessagingGroupAgent } from '../../types.js';
37
+ import { canAccessAgentGroup } from './access.js';
38
+ import { requestChannelApproval } from './channel-approval.js';
39
+ import { addMember } from './db/agent-group-members.js';
40
+ import { deletePendingChannelApproval, getPendingChannelApproval } from './db/pending-channel-approvals.js';
41
+ import { deletePendingSenderApproval, getPendingSenderApproval } from './db/pending-sender-approvals.js';
42
+ import { hasAdminPrivilege } from './db/user-roles.js';
43
+ import { getUser, upsertUser } from './db/users.js';
44
+ import { requestSenderApproval } from './sender-approval.js';
45
+
46
+ function extractAndUpsertUser(event: InboundEvent): string | null {
47
+ let content: Record<string, unknown>;
48
+ try {
49
+ content = JSON.parse(event.message.content) as Record<string, unknown>;
50
+ } catch {
51
+ return null;
52
+ }
53
+
54
+ // chat-sdk-bridge serializes author info as a nested `author.userId` and
55
+ // does NOT populate top-level `senderId`. Older adapters (v1, native) put
56
+ // `senderId` or `sender` directly at the top level. Check all three.
57
+ const senderIdField = typeof content.senderId === 'string' ? content.senderId : undefined;
58
+ const senderField = typeof content.sender === 'string' ? content.sender : undefined;
59
+ const author =
60
+ typeof content.author === 'object' && content.author !== null
61
+ ? (content.author as Record<string, unknown>)
62
+ : undefined;
63
+ const authorUserId = typeof author?.userId === 'string' ? (author.userId as string) : undefined;
64
+ const senderName =
65
+ (typeof content.senderName === 'string' ? content.senderName : undefined) ??
66
+ (typeof author?.fullName === 'string' ? (author.fullName as string) : undefined) ??
67
+ (typeof author?.userName === 'string' ? (author.userName as string) : undefined);
68
+
69
+ const rawHandle = senderIdField ?? senderField ?? authorUserId;
70
+ if (!rawHandle) return null;
71
+
72
+ const userId = rawHandle.includes(':') ? rawHandle : `${event.channelType}:${rawHandle}`;
73
+ if (!getUser(userId)) {
74
+ upsertUser({
75
+ id: userId,
76
+ kind: event.channelType,
77
+ display_name: senderName ?? null,
78
+ created_at: new Date().toISOString(),
79
+ });
80
+ }
81
+ return userId;
82
+ }
83
+
84
+ function safeParseContent(raw: string): { text?: string; sender?: string; senderId?: string } {
85
+ try {
86
+ return JSON.parse(raw);
87
+ } catch {
88
+ return { text: raw };
89
+ }
90
+ }
91
+
92
+ function handleUnknownSender(
93
+ mg: MessagingGroup,
94
+ userId: string | null,
95
+ agentGroupId: string,
96
+ accessReason: string,
97
+ event: InboundEvent,
98
+ ): void {
99
+ const parsed = safeParseContent(event.message.content);
100
+ const senderName = parsed.sender ?? null;
101
+ const dropRecord = {
102
+ channel_type: event.channelType,
103
+ platform_id: event.platformId,
104
+ user_id: userId,
105
+ sender_name: senderName,
106
+ reason: `unknown_sender_${mg.unknown_sender_policy}`,
107
+ messaging_group_id: mg.id,
108
+ agent_group_id: agentGroupId,
109
+ };
110
+
111
+ if (mg.unknown_sender_policy === 'strict') {
112
+ log.info('MESSAGE DROPPED — unknown sender (strict policy)', {
113
+ messagingGroupId: mg.id,
114
+ agentGroupId,
115
+ userId,
116
+ accessReason,
117
+ });
118
+ recordDroppedMessage(dropRecord);
119
+ return;
120
+ }
121
+
122
+ if (mg.unknown_sender_policy === 'request_approval') {
123
+ log.info('MESSAGE DROPPED — unknown sender (approval requested)', {
124
+ messagingGroupId: mg.id,
125
+ agentGroupId,
126
+ userId,
127
+ accessReason,
128
+ });
129
+ recordDroppedMessage(dropRecord);
130
+ // Fire-and-forget; pick-approver + delivery + row-insert are all async.
131
+ // If it fails it logs internally — the user's message still stays dropped
132
+ // either way. Requires a resolved userId (senderResolver populates users
133
+ // row before the gate fires); if we got here without one, there's nothing
134
+ // to identify for approval and we just stay in the "silent strict" branch.
135
+ if (userId) {
136
+ requestSenderApproval({
137
+ messagingGroupId: mg.id,
138
+ agentGroupId,
139
+ senderIdentity: userId,
140
+ senderName,
141
+ event,
142
+ }).catch((err) => log.error('Sender-approval flow threw', { err }));
143
+ }
144
+ return;
145
+ }
146
+
147
+ // 'public' should have been handled before the gate; fall through silently.
148
+ }
149
+
150
+ setSenderResolver(extractAndUpsertUser);
151
+
152
+ setAccessGate((event, userId, mg, agentGroupId): AccessGateResult => {
153
+ // Public channels skip the access check entirely.
154
+ if (mg.unknown_sender_policy === 'public') {
155
+ return { allowed: true };
156
+ }
157
+
158
+ if (!userId) {
159
+ handleUnknownSender(mg, null, agentGroupId, 'unknown_user', event);
160
+ return { allowed: false, reason: 'unknown_user' };
161
+ }
162
+
163
+ const decision = canAccessAgentGroup(userId, agentGroupId);
164
+ if (decision.allowed) {
165
+ return { allowed: true };
166
+ }
167
+
168
+ handleUnknownSender(mg, userId, agentGroupId, decision.reason, event);
169
+ return { allowed: false, reason: decision.reason };
170
+ });
171
+
172
+ /**
173
+ * Per-wiring sender-scope enforcement. Stricter than the messaging-group
174
+ * `unknown_sender_policy` — a wiring can require `sender_scope='known'`
175
+ * (explicit owner / admin / member) even on a 'public' messaging group.
176
+ *
177
+ * 'all' is a no-op; any sender passes. 'known' requires a userId that
178
+ * canAccessAgentGroup accepts (owner, admin, or group member).
179
+ */
180
+ setSenderScopeGate(
181
+ (_event: InboundEvent, userId: string | null, _mg: MessagingGroup, agent: MessagingGroupAgent): AccessGateResult => {
182
+ if (agent.sender_scope === 'all') return { allowed: true };
183
+ if (!userId) return { allowed: false, reason: 'unknown_user_scope' };
184
+ const decision = canAccessAgentGroup(userId, agent.agent_group_id);
185
+ if (decision.allowed) return { allowed: true };
186
+ return { allowed: false, reason: `sender_scope_${decision.reason}` };
187
+ },
188
+ );
189
+
190
+ /**
191
+ * Response handler for the unknown-sender approval card.
192
+ *
193
+ * Claim rule: questionId matches a row in pending_sender_approvals. If no
194
+ * such row, return false so the next handler (approvals module, interactive)
195
+ * gets a shot.
196
+ *
197
+ * Approve: add the sender to agent_group_members + re-invoke routeInbound
198
+ * with the stored event. The second routing attempt clears the gate because
199
+ * the user is now a member.
200
+ *
201
+ * Deny: delete the row (no "deny list" — a future message re-triggers a
202
+ * fresh card per ACTION-ITEMS item 5 "no denial persistence").
203
+ */
204
+ async function handleSenderApprovalResponse(payload: ResponsePayload): Promise<boolean> {
205
+ const row = getPendingSenderApproval(payload.questionId);
206
+ if (!row) return false;
207
+
208
+ // payload.userId is the raw platform userId (e.g. "6037840640"); namespace it
209
+ // with the channel type so it matches users(id) format. Then verify the
210
+ // clicker is the designated approver OR has owner/admin privilege over this
211
+ // agent group — any other click is rejected so random users can't self-admit
212
+ // via stolen card forwarding.
213
+ const clickerId = payload.userId ? `${payload.channelType}:${payload.userId}` : null;
214
+ const isAuthorized =
215
+ clickerId !== null && (clickerId === row.approver_user_id || hasAdminPrivilege(clickerId, row.agent_group_id));
216
+ if (!isAuthorized) {
217
+ log.warn('Unknown-sender approval click rejected — unauthorized clicker', {
218
+ approvalId: row.id,
219
+ clickerId,
220
+ expectedApprover: row.approver_user_id,
221
+ });
222
+ return true; // claim the response so it's not unclaimed-logged, but do nothing
223
+ }
224
+ const approverId = clickerId;
225
+ const alsoAllow = payload.value === 'approve_and_allow';
226
+ const approved = payload.value === 'approve' || alsoAllow;
227
+
228
+ if (approved) {
229
+ addMember({
230
+ user_id: row.sender_identity,
231
+ agent_group_id: row.agent_group_id,
232
+ added_by: approverId,
233
+ added_at: new Date().toISOString(),
234
+ });
235
+ log.info('Unknown sender approved — member added', {
236
+ approvalId: row.id,
237
+ senderIdentity: row.sender_identity,
238
+ agentGroupId: row.agent_group_id,
239
+ approverId,
240
+ });
241
+
242
+ if (alsoAllow) {
243
+ // Snapshot the prior policy for the audit line — read before the
244
+ // update so the "fromPolicy" field reflects pre-flip state even if
245
+ // a concurrent click had already flipped it. fromPolicy=null means
246
+ // the MG row was deleted between approval-creation and click; the
247
+ // updateMessagingGroup call below silently no-ops on a missing row.
248
+ const mg = getMessagingGroup(row.messaging_group_id);
249
+ const fromPolicy = mg?.unknown_sender_policy ?? null;
250
+ if (fromPolicy !== 'public') {
251
+ updateMessagingGroup(row.messaging_group_id, { unknown_sender_policy: 'public' });
252
+ }
253
+ log.info('Unknown-sender policy flipped to public via approval card', {
254
+ audit: 'sender_approval_policy_flip',
255
+ approvalId: row.id,
256
+ messagingGroupId: row.messaging_group_id,
257
+ agentGroupId: row.agent_group_id,
258
+ approverId,
259
+ fromPolicy,
260
+ toPolicy: 'public',
261
+ });
262
+ }
263
+
264
+ // Clear the pending row BEFORE re-routing so the gate check on the
265
+ // second attempt doesn't see the in-flight row and short-circuit.
266
+ deletePendingSenderApproval(row.id);
267
+
268
+ try {
269
+ const event = JSON.parse(row.original_message) as InboundEvent;
270
+ await routeInbound(event);
271
+ } catch (err) {
272
+ log.error('Failed to replay message after sender approval', { approvalId: row.id, err });
273
+ }
274
+ return true;
275
+ }
276
+
277
+ log.info('Unknown sender denied', {
278
+ approvalId: row.id,
279
+ senderIdentity: row.sender_identity,
280
+ agentGroupId: row.agent_group_id,
281
+ approverId,
282
+ });
283
+ deletePendingSenderApproval(row.id);
284
+ return true;
285
+ }
286
+
287
+ registerResponseHandler(handleSenderApprovalResponse);
288
+
289
+ // ── Unknown-channel registration flow ──
290
+
291
+ setChannelRequestGate(async (mg, event) => {
292
+ await requestChannelApproval({ messagingGroupId: mg.id, event });
293
+ });
294
+
295
+ /**
296
+ * Response handler for the unknown-channel registration card.
297
+ *
298
+ * Claim rule: questionId matches a pending_channel_approvals row (keyed
299
+ * by messaging_group_id). If no such row, return false so downstream
300
+ * handlers get a shot.
301
+ *
302
+ * Approve: create the wiring with MVP defaults (mention-sticky for
303
+ * groups / pattern='.' for DMs; sender_scope='known';
304
+ * ignored_message_policy='accumulate'), add the triggering sender as a
305
+ * member so sender_scope doesn't immediately bounce them into a
306
+ * sender-approval card, then replay the original event.
307
+ *
308
+ * Deny: set `messaging_groups.denied_at = now()` so future mentions on
309
+ * this channel drop silently until an admin explicitly wires it.
310
+ */
311
+ async function handleChannelApprovalResponse(payload: ResponsePayload): Promise<boolean> {
312
+ const row = getPendingChannelApproval(payload.questionId);
313
+ if (!row) return false;
314
+
315
+ // Click-auth: same pattern as sender-approval (see commit 68058cb).
316
+ // Raw platform userId → namespace with channelType → must match the
317
+ // designated approver OR have admin privilege over the target agent.
318
+ const clickerId = payload.userId ? `${payload.channelType}:${payload.userId}` : null;
319
+ const isAuthorized =
320
+ clickerId !== null && (clickerId === row.approver_user_id || hasAdminPrivilege(clickerId, row.agent_group_id));
321
+ if (!isAuthorized) {
322
+ log.warn('Channel registration click rejected — unauthorized clicker', {
323
+ messagingGroupId: row.messaging_group_id,
324
+ clickerId,
325
+ expectedApprover: row.approver_user_id,
326
+ });
327
+ return true; // claim but take no action
328
+ }
329
+ const approverId = clickerId;
330
+ const approved = payload.value === 'approve';
331
+
332
+ if (!approved) {
333
+ setMessagingGroupDeniedAt(row.messaging_group_id, new Date().toISOString());
334
+ deletePendingChannelApproval(row.messaging_group_id);
335
+ log.info('Channel registration denied', {
336
+ messagingGroupId: row.messaging_group_id,
337
+ agentGroupId: row.agent_group_id,
338
+ approverId,
339
+ });
340
+ return true;
341
+ }
342
+
343
+ // Rehydrate the original event to know (a) whether it was a DM or group
344
+ // (chooses engage_mode default), and (b) who the triggering sender was
345
+ // (auto-member-add so sender_scope='known' doesn't bounce the replay).
346
+ let event: InboundEvent;
347
+ try {
348
+ event = JSON.parse(row.original_message) as InboundEvent;
349
+ } catch (err) {
350
+ log.error('Channel registration: failed to parse stored event', {
351
+ messagingGroupId: row.messaging_group_id,
352
+ err,
353
+ });
354
+ deletePendingChannelApproval(row.messaging_group_id);
355
+ return true;
356
+ }
357
+
358
+ // Decide engage_mode from the original event. DMs (`isMention=true` &
359
+ // not in a group) get `pattern='.'` (always respond). Group mentions
360
+ // get `mention-sticky` (respond now + follow the thread).
361
+ //
362
+ // We can't read `mg.is_group` reliably here because we only auto-create
363
+ // the mg with `is_group=0` on first sight — the adapter hasn't told us
364
+ // yet whether it's actually a group. Fall back to the InboundEvent's
365
+ // `threadId`: a non-null threadId implies a threaded platform (Slack
366
+ // channel thread, Discord thread), which we treat as a group.
367
+ const isGroup = event.threadId !== null;
368
+ const engageMode: MessagingGroupAgent['engage_mode'] = isGroup ? 'mention-sticky' : 'pattern';
369
+ const engagePattern = isGroup ? null : '.';
370
+
371
+ const mgaId = `mga-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
372
+ createMessagingGroupAgent({
373
+ id: mgaId,
374
+ messaging_group_id: row.messaging_group_id,
375
+ agent_group_id: row.agent_group_id,
376
+ engage_mode: engageMode,
377
+ engage_pattern: engagePattern,
378
+ sender_scope: 'known',
379
+ ignored_message_policy: 'accumulate',
380
+ session_mode: 'shared',
381
+ priority: 0,
382
+ created_at: new Date().toISOString(),
383
+ });
384
+ log.info('Channel registration approved — wiring created', {
385
+ messagingGroupId: row.messaging_group_id,
386
+ agentGroupId: row.agent_group_id,
387
+ mgaId,
388
+ engageMode,
389
+ approverId,
390
+ });
391
+
392
+ // Auto-admit the triggering sender. Without this, the replay below
393
+ // would bounce through sender-approval (sender_scope='known' +
394
+ // sender-is-not-a-member).
395
+ const senderUserId = extractAndUpsertUser(event);
396
+ if (senderUserId) {
397
+ addMember({
398
+ user_id: senderUserId,
399
+ agent_group_id: row.agent_group_id,
400
+ added_by: approverId,
401
+ added_at: new Date().toISOString(),
402
+ });
403
+ }
404
+
405
+ // Clear the pending row BEFORE replay so the gate check on the second
406
+ // attempt sees a wired channel (agentCount > 0) and takes the fan-out
407
+ // path normally.
408
+ deletePendingChannelApproval(row.messaging_group_id);
409
+
410
+ try {
411
+ await routeInbound(event);
412
+ } catch (err) {
413
+ log.error('Failed to replay message after channel approval', {
414
+ messagingGroupId: row.messaging_group_id,
415
+ err,
416
+ });
417
+ }
418
+ return true;
419
+ }
420
+
421
+ registerResponseHandler(handleChannelApprovalResponse);