@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,272 @@
1
+ # Channel-wiring + approvals deep dive
2
+
3
+ **Status:** Research · 2026-05-01 · follow-up to paraclaw#67
4
+
5
+ Aaron wired a second Telegram bot via `/claw/channels/new` (the fast-path landed in PR #69). The form validated the bot, captured his Telegram userId, and offered the wire button. Then he DM'd the new bot — and a "wire this channel?" approval card landed in his **first** Telegram bot's DM, asking him to approve a registration he'd just initiated three seconds earlier through the form.
6
+
7
+ This is empirically wrong on at least three axes: the approval shouldn't have fired (the form already had operator intent), the card shouldn't have been delivered through a *different* bot, and the resulting wiring used cautious approval-flow defaults instead of the trusted-operator wire-flow defaults. Aaron asked for a deep dive before any code changes.
8
+
9
+ This document reconstructs what actually happened from the live install's DB and logs, maps the architecture that produced the behavior, critiques what's wrong, and proposes three design directions. Aaron decides what to do.
10
+
11
+ ---
12
+
13
+ ## 1. Empirical trace
14
+
15
+ Live install at `~/.parachute/claw/paraclaw.db` (note: filename is `paraclaw.db`, not the `v2.db` the prompt referenced; same role). Times are UTC except where noted (log file uses local PT — UTC = local + 6h).
16
+
17
+ ### 1.1 The two bots
18
+
19
+ `messaging_groups` rows for telegram, ordered by creation:
20
+
21
+ | MG id | platform_id | name | is_group | unknown_sender_policy | created_at |
22
+ |---|---|---|---|---|---|
23
+ | `mg-…e4z1rv` | `telegram:8792496425:1190596288` | Techne DM | 0 | **strict** | 2026-04-28 05:05 |
24
+ | `mg-…ihfgeq` | `telegram:8792496425:-1003927577645` | (group) | 1 | request_approval | 2026-04-29 02:25 |
25
+ | `mg-…90uhi5` | `telegram:8757751201:-1002245300962` | (group) | 1 | request_approval | 2026-05-01 19:13:43.981 |
26
+ | `mg-…ogxhkj` | `telegram:8757751201:1190596288` | (DM) | 0 | request_approval | 2026-05-01 19:13:43.982 |
27
+
28
+ Bot1 = `8792496425` (UnforcedAGI / the original setup-wizard wire). Bot2 = `8757751201` (the new one). Aaron's Telegram user id = `1190596288`.
29
+
30
+ ### 1.2 The wirings
31
+
32
+ `messaging_group_agents` (all wired to the only existing agent group, Techne):
33
+
34
+ | MGA id | MG | engage | sender_scope | ignored_msg_policy | created_at |
35
+ |---|---|---|---|---|---|
36
+ | `mga-…gc6co9` | bot1-DM-Aaron | pattern `.` | **all** | **drop** | 2026-04-28 |
37
+ | `mga-…5i9dz9` | bot2-DM-Aaron | pattern `.` | **known** | **accumulate** | 2026-05-01 19:14:17 |
38
+ | `mga-…9977ij` | bot2-Group | pattern `.` | known | accumulate | 2026-05-01 19:15:07 |
39
+
40
+ Bot1's MGA (created by `init-first-agent.ts`) uses the trusted defaults: `sender_scope='all'`, `ignored_message_policy='drop'`, `unknown_sender_policy='strict'`. Bot2's MGA uses the cautious defaults: `sender_scope='known'`, `ignored_message_policy='accumulate'`, `unknown_sender_policy='request_approval'`. **Different rows, different behavior, both wired by the operator on the same day for the same operator.**
41
+
42
+ ### 1.3 user_dms cache
43
+
44
+ | user_id | channel_type | messaging_group_id | resolved_at |
45
+ |---|---|---|---|
46
+ | `telegram:1190596288` | telegram | `mg-…e4z1rv` (bot1 DM) | 2026-04-29 |
47
+
48
+ `user_dms` is keyed `(user_id, channel_type)` — exactly one row per user-channel. When the channel-approval flow asks "where can I reach Aaron?", this cache says "his bot1 DM" and stops.
49
+
50
+ ### 1.4 The timeline (from `~/.parachute/claw/logs/claw.log`)
51
+
52
+ ```
53
+ 13:13:43.697 Channel bot registered adapter=telegram botId=8757751201
54
+ 13:13:43.697 Channel adapter registered dynamically
55
+ 13:13:43.980 Inbound DM received channelId=telegram:8757751201:1190596288 (×3)
56
+ 13:13:43.981 Auto-created messaging group mg-…90uhi5 (telegram:8757751201:-1002245300962)
57
+ 13:13:43.982 Auto-created messaging group mg-…ogxhkj (telegram:8757751201:1190596288)
58
+ 13:13:43.983 ERROR Channel-request gate threw — UNIQUE constraint failed:
59
+ pending_channel_approvals.messaging_group_id (×2)
60
+ 13:13:44.529 Channel registration card delivered mg-…90uhi5 approver=telegram:1190596288
61
+ 13:13:44.555 Channel registration card delivered mg-…ogxhkj approver=telegram:1190596288
62
+ 13:14:17.329 Channel registration approved — wiring created mga-…5i9dz9
63
+ 13:14:17.342 Session created
64
+ 13:14:17.353 Spawning container techne
65
+ 13:14:25.204 Message delivered platformId=telegram:8757751201:1190596288
66
+ 13:15:07.386 Channel registration approved — wiring created mga-…9977ij (group)
67
+ ```
68
+
69
+ ### 1.5 What this says
70
+
71
+ **The wire form's `Wire` button never effectively ran.** Validation completed at 13:13:43.697; the dynamic register-bot path (PR #67 B2) brought the Bot2 polling loop live in the same call. **283 ms later** Telegram's `getUpdates` returned three queued DMs Aaron had sent before the form even existed. The router auto-created the bot2-DM messaging group with `unknown_sender_policy='request_approval'` (router.ts:179, hardcoded for any auto-create), saw zero wirings, and called `channelRequestGate` — which delivered the approval card. Aaron approved via the card; the channel-approval handler created the MGA with its own (cautious) defaults. By the time the operator could reach the form's "Wire" button, the MGA already existed; the wire button was a no-op idempotent on the existing MGA pair.
72
+
73
+ The wire path's defaults (`sender_scope='all'`, `unknown_sender_policy='strict'`) were never applied. The approval-handler's defaults (`sender_scope='known'`, `unknown_sender_policy='request_approval'` inherited from auto-create) won by milliseconds.
74
+
75
+ ### 1.6 The race-condition bug
76
+
77
+ Three rapid inbound DMs at 13:13:43.980 each fired `requestChannelApproval`. The dedup check (`hasInFlightChannelApproval` in `channel-approval.ts:63`) reads-then-inserts non-atomically. All three saw "no pending row" and raced the `INSERT INTO pending_channel_approvals` (PK on messaging_group_id). The first won; the other two threw `UNIQUE constraint failed` (logged at ERROR). User-visible impact: zero — the first INSERT got an approval card delivered. But the error log is misleading and the dedup is wrong by design.
78
+
79
+ ### 1.7 Why the card came through Bot1
80
+
81
+ `pickApprovalDelivery(approvers, originChannelType)` (`src/modules/approvals/primitive.ts:104`) walks the approver list, calls `ensureUserDm(userId)` per approver, returns the first one that resolves. `ensureUserDm` looks up `user_dms` first; only on a miss does it call the platform's `openDM`. Aaron's `user_dms` row was Bot1's DM — that's what got returned. The function has no notion of "approval is *about* Bot2; prefer Bot2's DM if reachable."
82
+
83
+ ### 1.8 Hypothesis verdict
84
+
85
+ The team-lead's three hypotheses:
86
+ - (a) **Half-correct.** Wire path's MGA was never created; the approval handler's MGA *was* created with `sender_scope='known'`. Aaron, as approver, was auto-added to `agent_group_members` so the replay wouldn't bounce. So the wire flow eventually produced a working channel — but via the wrong code path.
87
+ - (b) **Confirmed.** The bot2-DM `mg-…ogxhkj` was created by `routeInbound`'s auto-create, *not* by `wireDmToAgent`. There's a single MG per (channel_type, platform_id), so we don't have *duplicate* siblings — we have a `wireDmToAgent` that never inserted because `routeInbound` got there first.
88
+ - (c) **Wrong.** Platform_id encoding `telegram:8757751201:1190596288` is correct (bot2 + Aaron). PR A's encoding is solid.
89
+
90
+ Root cause: **the bot adapter goes live the moment the user clicks "Validate."** The wire button's only remaining job in the post-PR #67 flow is creating the MGA — but inbound traffic on a backlogged channel races it.
91
+
92
+ ---
93
+
94
+ ## 2. Architecture map
95
+
96
+ ### 2.1 platform_id encoding
97
+
98
+ `<channel>:<botId>:<native>` (PR A landed pre-PR #67 B1). Per channel:
99
+ - Telegram: `botId` is the bot's Telegram user id (positive int from `getMe`); `native` is the `chat_id`. For DMs `chat_id == userId`; for groups `chat_id` is a negative int.
100
+ - Discord: `botId` is the application id; `native` is `@me:<botUserId>` for DMs or a guild-channel id for guilds.
101
+
102
+ The bot dimension is what makes two Telegram bots' identical DM `chat_id`s resolve to distinct `messaging_groups` rows.
103
+
104
+ ### 2.2 Wire flow (UI → DB)
105
+
106
+ User clicks "Validate & register bot" on `/claw/channels/new`:
107
+ 1. `POST /api/channels/{adapter}/register-bot` (B2) → validates token, persists `secrets` row `CHANNEL_BOT_TOKEN:<channel>:<botId>`, calls `registerBotAdapter`. **Adapter goes live now.**
108
+ 2. UI shows resolved identity, asks for agent group + (Telegram) operator userId.
109
+ 3. User clicks "Wire" → `POST /api/groups/:folder/wire-channel` → `wireDmToAgent(...)` → INSERT messaging_groups + messaging_group_agents, defaults `sender_scope='all'`, `ignored_message_policy='drop'`, `unknown_sender_policy='strict'`.
110
+
111
+ Step 1 → 3 is **not atomic**. Anything that hits the polling loop between steps lands on an unwired channel.
112
+
113
+ ### 2.3 Inbound flow (router.ts)
114
+
115
+ Adapter polling loop receives a message → `routeInbound(event)`:
116
+ 1. Look up MG by (channel_type, platform_id). If missing AND message is mention/DM, auto-create with `unknown_sender_policy='request_approval'` (router.ts:179, hardcoded).
117
+ 2. If `agentCount === 0`: record dropped_message, fire `channelRequestGate` (the registration approval), return.
118
+ 3. Else: `senderResolver` upserts `users` row, fan out to wirings. Each wiring evaluates `engage_mode`, `accessGate`, `senderScopeGate` independently. Engaged → write to session inbound DB + wake container. Not engaged but `accumulate` → write inbound with `trigger=0`. Else drop.
119
+
120
+ ### 2.4 Approval picking (`src/modules/approvals/primitive.ts`)
121
+
122
+ - `pickApprover(agentGroupId)` returns ordered user ids: scoped admins → global admins → owners. For Aaron's install, owner-only.
123
+ - `pickApprovalDelivery(approvers, originChannelType)` walks the list, prefers same-channel approvers, calls `ensureUserDm` to resolve a reachable DM. Returns the first hit. **No bot-level preference** — picks any cached or openable DM.
124
+
125
+ ### 2.5 Identity tables
126
+
127
+ - `users(id, kind, display_name, created_at)` — id = `<channel>:<handle>` (e.g. `telegram:1190596288`). Upserted on first sender resolution.
128
+ - `user_roles(user_id, role, agent_group_id, granted_by, granted_at)` — owner / admin (global if `agent_group_id IS NULL`, scoped otherwise).
129
+ - `agent_group_members(user_id, agent_group_id, added_by, added_at)` — unprivileged-access gate; channel-approval handler auto-adds the triggering sender so the replay doesn't bounce on `sender_scope='known'`.
130
+ - `user_dms(user_id, channel_type, messaging_group_id, resolved_at)` — DM cache. **One row per user per channel.** First-cached wins.
131
+
132
+ ### 2.6 unknown_sender_policy × sender_scope
133
+
134
+ These are independent. `unknown_sender_policy` is on `messaging_groups`; `sender_scope` is on `messaging_group_agents`. Most install paths default both to the same posture (strict/all OR request_approval/known) but they drift in the auto-create + approval-handler combo: MG inherits `request_approval` (from router auto-create), MGA inherits `known` (from approval handler). Net effect: senders Aaron hasn't approved get bounced to a sender-approval cascade (`pending_sender_approvals`).
135
+
136
+ ### 2.7 Approval table zoo
137
+
138
+ Three tables, three lifecycles:
139
+ - `pending_channel_approvals` — channel-registration cards (PR #67 area). PK on `messaging_group_id`. Deleted on approve/reject.
140
+ - `pending_sender_approvals` — sender-scope cards (when `sender_scope='known'` rejects an unknown sender). Different lifecycle, different module.
141
+ - `approvals` — the modular approval primitive (paraclaw#11/#286). Used by self-mod, MCP, etc. Distinct from the channel/sender tables.
142
+
143
+ The team-lead's prompt asks me to confirm `pending_channel_approvals` and `approvals` are distinct: **yes**, separate tables, separate code paths. Channel registration was deliberately not folded into the unified primitive (see `channel-approval.ts:1-38` header comment).
144
+
145
+ ---
146
+
147
+ ## 3. Critique
148
+
149
+ ### 3.1 The validate-spawns-the-bot UX is the load-bearing problem
150
+
151
+ PR #67 B2's polish folded validate + register into one button click — for good reason (avoided doubled API quota on Telegram's getUpdates). But it converted "validate the token works" from a read-only action into a side-effecting one. The bot is *live and serving traffic* before the operator picks an agent group. A backlogged channel will race the wire.
152
+
153
+ ### 3.2 The approval cascade for self-wire is wrong
154
+
155
+ The form *just* captured the operator's userId. The system has all the context it needs to know "Aaron is wiring his own bot to his own DM." Yet the inbound message gets treated as a stranger DM'ing an unwired channel — an approval card is built and delivered to that same operator, by a different bot, asking them to approve what they're already in the middle of doing.
156
+
157
+ It's not just an annoyance; it's misleading. The card delivery via Bot1 implies "Bot1 owns this approval" or "this is a system-level event in Bot1's chat" — neither is true.
158
+
159
+ ### 3.3 The card-via-different-bot is mostly accidental
160
+
161
+ `pickApprovalDelivery` tries to find any reachable DM. For a single-bot install this happens to be the right bot. For multi-bot, it's whichever bot's DM got cached first. In Aaron's case, this caused the surprising "Bot1 telling me about Bot2" UX. It's not principled; it's just what the cache returned.
162
+
163
+ A principled answer: approval cards about Bot X should prefer delivery via Bot X if the approver is reachable there. For first-touch operators (no `user_dms` yet for that bot), fall back. For cross-bot scenarios where the approver isn't reachable on the new bot, the current behavior is fine.
164
+
165
+ ### 3.4 The race in `pending_channel_approvals` is real
166
+
167
+ Three concurrent inbounds → three concurrent `requestChannelApproval` calls → unique-key collision. The current code logs ERROR and moves on, so user-visible impact is nil, but:
168
+ - The error log misleads anyone debugging.
169
+ - If the **first** insert fails for any reason (not a duplicate — say, a constraint or transient I/O), the others *would* have succeeded, but never reach INSERT because the dedup check was non-atomic.
170
+
171
+ Atomic fix: `INSERT … ON CONFLICT DO NOTHING` and check `changes() > 0` to decide whether to deliver. Already cheap; just hasn't been done.
172
+
173
+ ### 3.5 The two MGA-default sets create a hidden invariant
174
+
175
+ `wireDmToAgent` defaults: trusted (all/drop/strict). `channel-approval.ts` handler defaults: cautious (known/accumulate/request_approval). They're meant to model two different intents:
176
+ - Operator proactively wired this bot to themselves → trust.
177
+ - An admin approved a stranger's mention/DM after-the-fact → cautious.
178
+
179
+ But which one applies depends on a milliseconds-level race that the operator can't observe. **The intent isn't legible from the data afterward** — `mga-…5i9dz9` looks identical in shape to a real "I approved a stranger" wire. The system can't recover the operator's actual intent, and neither can the operator looking back at the wirings list.
180
+
181
+ ### 3.6 Multi-bot model gaps surfaced by PR #67
182
+
183
+ PR #67 B2 introduced the secrets-backed multi-bot scan (`spawnSecretsBackedBots`) that brings every `CHANNEL_BOT_TOKEN:<channel>:<botId>` secret live on boot. Combined with `register-bot` going live immediately on validate, **the system has no notion of a "registered but inert" bot.** Once registered, a bot serves traffic forever. There's no "I want to test this token but not yet route messages" state.
184
+
185
+ ---
186
+
187
+ ## 4. Design proposals
188
+
189
+ ### A. Defer adapter spawn until after wire (atomic register+wire)
190
+
191
+ **Sketch.** Split `/api/channels/{adapter}/register-bot` into two phases. Phase 1 = validate + persist secret + return identity, **don't bring the adapter live**. Phase 2 = "wire" endpoint creates MGA *and* spawns the adapter atomically. `spawnSecretsBackedBots()` at boot only spawns secrets that have at least one wiring; orphaned secrets stay inert.
192
+
193
+ **Data model.** No schema changes. New invariant: an adapter is live only when a wiring exists. Implemented in `spawnSecretsBackedBots` (skip orphans) + in the wire endpoint (start adapter on success).
194
+
195
+ **UX.** The form's "Validate" returns identity but the bot stays cold. "Wire" is the action that brings it live. Backlogged DMs land on a wired channel — engaged immediately, no approval cascade. Operators wanting to "just verify the token works" still get the validation result without committing to a live bot.
196
+
197
+ **Solves.** The race entirely. The surprise-approval-cascade for self-wire entirely. Makes orphan-secret cleanup (a future tablet on PR #67) trivial.
198
+
199
+ **Doesn't solve.** Approval card delivery via wrong bot for *post-wire* registrations (different operator, after wire is committed). That's still routed via `pickApprovalDelivery` and still picks the cached DM.
200
+
201
+ **Migration.** Modest. Update `register-bot` to skip `registerBotAdapter`. Update `wireDmToAgent` (or a new wrapper) to call `registerBotAdapter` after MGA insert. Update `spawnSecretsBackedBots` to JOIN through messaging_groups+messaging_group_agents. Delete the existing race-doc-update from PR #67's body; it ceases to be a thing.
202
+
203
+ **Risk.** A user who validates and never wires leaves an orphan secret. We need a sweep or a UI prompt. (Discord/Telegram tokens don't expire automatically, so the orphan persists — but it's inert.) Lower-risk than current "always live" semantics.
204
+
205
+ ### B. Operator-self-wire trust hint
206
+
207
+ **Sketch.** When `/channels/new` form is submitted, persist a short-lived "trusted setup hint" naming `(channel_type, bot_id, operator_user_id, expires_at)`. The router checks for a matching hint when handling the first inbound on an unwired bot's DM. If matched, skip `channelRequestGate` and immediately wire with `wireDmToAgent`'s trusted defaults (all/drop/strict). Hint expires on use or after 10 minutes.
208
+
209
+ **Data model.** New table `pending_channel_setup_hints (channel_type, bot_id, operator_user_id, expires_at)` or in-memory map (cheaper, lost on restart — fine because hints are 10-minute things).
210
+
211
+ **UX.** Same form flow as today. The first DM Aaron sends to Bot2 just works; no card. Subsequent DMs from anyone else go through the normal approval path.
212
+
213
+ **Solves.** The "I just wired this, the next message from me should just work" UX. Preserves the polling-eager validate UX.
214
+
215
+ **Doesn't solve.** The race is still there for *other* senders (someone else DMs the bot in the validate-to-wire window — they get the approval cascade, which is correct behavior anyway). Card-via-wrong-bot for post-wire approvals is unaddressed.
216
+
217
+ **Migration.** Small. Add the table or in-memory map, hook into the form submit, hook into router.ts:218 (channel-request gate dispatch) to consult the hint first. Default hint TTL = 10 min, configurable.
218
+
219
+ **Risk.** Hint table needs cleanup (TTL sweep). In-memory variant is lost on restart, which is correct behavior — operators who don't wire within 10 min are starting over anyway.
220
+
221
+ ### C. Approval delivery follows the bot in question
222
+
223
+ **Sketch.** Extend `pickApprovalDelivery` to take an optional `preferredBotId`. For channel-registration approvals, pass the bot id of the channel being registered. The function tries to resolve a `user_dms` row for the (approver, channel_type, bot_id) combination; on miss, falls back to current behavior.
224
+
225
+ **Data model.** Existing `user_dms` is keyed `(user_id, channel_type)` — only one DM per user-channel. Need to extend to `(user_id, channel_type, bot_id)` to differentiate. Migration: ALTER TABLE adds `bot_id TEXT` column, backfill from `messaging_groups.platform_id` (parse second segment). Make `(user_id, channel_type, bot_id)` the new PK; the old `(user_id, channel_type)` constraint goes away.
226
+
227
+ **UX.** Approval cards land in the right bot. Less confusing for the operator. For first-touch (no cached DM via the new bot), still falls back so functionality doesn't break.
228
+
229
+ **Solves.** The cross-bot card delivery confusion. Doesn't solve the surprise-approval-cascade for self-wire.
230
+
231
+ **Migration.** Schema change is non-trivial but mechanical. Code change is one extra arg + a fallback.
232
+
233
+ **Risk.** Existing `user_dms` rows need a sane bot_id backfill. For Aaron's row (bot1's MG), bot_id is recoverable. For older rows from before PR A's bot dimension, bot_id is `null` and matches the legacy "single-bot per channel" assumption — needs care.
234
+
235
+ ### Recommendation
236
+
237
+ **A + B together.** A removes the race; B handles the case where the operator wants to test by DM'ing in the validate-but-not-yet-wired window (with A alone, they'd have to wire first). Together: validate is read-only, the form's wire button is the single commit point, and there's a short trust window for the operator to test from their own client.
238
+
239
+ C is a smaller cleanup that's worth doing independently — it makes the surface principled even if A+B aren't shipped together.
240
+
241
+ ---
242
+
243
+ ## 5. Open questions for Aaron
244
+
245
+ These are decisions the research can't make alone:
246
+
247
+ 1. **Trust the form's captured userId for self-wire bypass?** Proposal B trusts that the operator filling out `/channels/new` and entering their own Telegram userId is the same person as the one DM'ing the bot. If yes, the hint-based bypass is fine. If no, we need a stronger binding (e.g., the form generates a one-time token the operator includes in their first DM).
248
+
249
+ 2. **Should an inert (registered but unwired) secret persist across restarts?** Proposal A has `spawnSecretsBackedBots()` skip orphans. That means a validated-but-not-wired bot doesn't survive a restart. Alternative: keep the secret, log a warning at boot listing orphans, let the operator decide via a UI sweep. Open: which trade-off do you prefer?
250
+
251
+ 3. **Should approval cards always come from the bot in question?** Proposal C says yes-when-possible. Edge: an admin approving a registration for a bot they've never DM'd — the approval has to come from somewhere. Current fallback (any cached DM) is the simplest answer; alternative is "the bot DMs the admin first" which is more invasive.
252
+
253
+ 4. **What's the right MGA default for "operator approved a stranger's DM via card"?** Today: `sender_scope='known'`, `ignored_message_policy='accumulate'`. The reasoning (only let already-known users engage; stash others' messages until they're approved) is defensive. But it surprised Aaron when he was the "stranger." For *self*-approval (operator approving their own DM), should the defaults flip to `all`/`drop`?
254
+
255
+ 5. **How visible should the post-wire wiring shape be?** Right now the channels list shows the wiring exists but not its `sender_scope`/`ignored_message_policy`/`unknown_sender_policy`. If those shape the engagement profile materially, shouldn't they be surfaced in the channel-edit UI? (This is orthogonal to the proposals above but adjacent.)
256
+
257
+ 6. **Does PR #72 ship as-is, or wait for one of these proposals?** PR #72 is correct for the multi-bot infrastructure. The UX bugs above exist with or without it. The argument for shipping #72 first: the infrastructure is sound, and the design fixes are about the *flow*, not the *capability*. The argument against: shipping the multi-bot path before fixing the surprise-cascade UX means more operators hit the surprise.
258
+
259
+ ---
260
+
261
+ ## Appendix: file references
262
+
263
+ - Router auto-create policy: `src/router.ts:179`
264
+ - Channel-request gate dispatch: `src/router.ts:218`
265
+ - Wire-flow defaults (trusted): `src/web/wire-channel.ts:158-172`
266
+ - Approval-flow defaults (cautious): `src/modules/permissions/index.ts:344-355`
267
+ - Approval delivery picker: `src/modules/approvals/primitive.ts:104-120`
268
+ - Channel-registration handler: `src/modules/permissions/channel-approval.ts`
269
+ - pending_channel_approvals schema: `src/db/migrations/012-channel-registration.ts`
270
+ - B2 register-bot endpoint: `src/web/server.ts` (POST `/api/channels/{adapter}/register-bot`)
271
+ - B2 secrets-backed scan: `src/channels/channel-registry.ts` (`spawnSecretsBackedBots`)
272
+ - Live install evidence: `~/.parachute/claw/paraclaw.db`, `~/.parachute/claw/logs/claw.log`
@@ -0,0 +1,250 @@
1
+ # Channel policy + approval routing
2
+
3
+ **Status:** Design proposal · 2026-05-02 · paraclaw#67 follow-up
4
+
5
+ Three threads surfaced once the multi-bot wiring landed and Aaron started using two Telegram bots side by side:
6
+
7
+ 1. **Approval delivery doesn't follow the bot in question.** TechneRobot group-chat approval cards arrived through UnforcedAGI's DM — wrong bot, wrong mental model.
8
+ 2. **Per-channel policy is buried.** `unknown_sender_policy` (per-MG) and `engage_mode` / `sender_scope` / `ignored_message_policy` (per-MGA) are real, working knobs, but only the per-MGA three are surfaced in the UI. The per-MG one isn't editable anywhere outside the trust-hint code path. There's no "always allow this group" toggle, no "respond only to mentions" toggle on a wired chat.
9
+ 3. **NanoClaw heritage.** Some of these knobs predate paraclaw; some are paraclaw-era. Reusing NanoClaw shapes where they exist (instead of re-deriving) saves churn and keeps the user-facing language stable.
10
+
11
+ This doc proposes how to fix all three together. No impl until Aaron reads it.
12
+
13
+ ## 1. Current state — what exists today
14
+
15
+ ### 1a. The four MGA knobs (paraclaw migration 010, replaced legacy `trigger_rules` JSON)
16
+
17
+ `messaging_group_agents` carries four orthogonal columns governing per-wiring behavior. Migration 010 (`src/db/migrations/010-engage-modes.ts`) split them out of the legacy `trigger_rules` (opaque JSON) + `response_scope` (conflated axis) shape. Behavior at runtime, traced through `src/router.ts`:
18
+
19
+ | Column | Values | What the router actually does | Code |
20
+ |---|---|---|---|
21
+ | `engage_mode` | `pattern` \| `mention` \| `mention-sticky` | `pattern`+regex test on text (`'.'` sentinel = always); `mention` requires `event.message.isMention`; `mention-sticky` accepts mention OR an existing per-thread session for this `(agent, mg, thread)` | `evaluateEngage` at `src/router.ts:402-433` |
22
+ | `engage_pattern` | regex string, nullable | Used only when `engage_mode='pattern'`. Bad regex fails open. | `src/router.ts:411-418` |
23
+ | `sender_scope` | `all` \| `known` | `all` = no-op; `known` = `canAccessAgentGroup(userId, agent_group_id)` must allow. Enforced via the `senderScopeGate` hook the permissions module registers. | `src/modules/permissions/index.ts:175-183` |
24
+ | `ignored_message_policy` | `drop` \| `accumulate` | Branch on the *non-engaging* path: `drop` = silently skip; `accumulate` = still write the inbound row to the agent's session DB with `trigger=0`, so context is available next time it does engage | `src/router.ts:355-358` |
25
+
26
+ Note the API surface (`src/web/routes/channels.ts:8-22`) uses different enum names that translate at the route boundary — `engageMode='all'` collapses to DB `engage_mode='pattern'` + `engage_pattern='.'`; `senderScope='allowlist'` ↔ `sender_scope='known'`; `ignoredMessagePolicy='silent'` ↔ `ignored_message_policy='accumulate'`. The UI sees the API names. The DB keeps the original ones. The translator is lossy on the `mention` ↔ `mention-sticky` distinction (renders both as `mention`); the `apiToDbPatch` at `src/web/routes/channels.ts:97-127` carefully preserves sticky on round-trip.
27
+
28
+ ### 1b. The MG-level knob
29
+
30
+ `messaging_groups.unknown_sender_policy` is a single column with three values, set at MG creation (default `'strict'`):
31
+
32
+ | Value | Router behavior | Source |
33
+ |---|---|---|
34
+ | `strict` | Drop messages from senders the access gate refuses; record in `dropped_messages` (paraclaw default). | `src/modules/permissions/index.ts:106-115` |
35
+ | `request_approval` | Drop, record, AND fire a sender-approval card to admins. Used by the unwired-channel auto-set flow at `src/router.ts:183`. | `src/modules/permissions/index.ts:117-140` |
36
+ | `public` | Skip the access gate entirely — anyone in the chat can engage the agent (subject to per-MGA `sender_scope`). | `src/modules/permissions/index.ts:147-151` |
37
+
38
+ This is the per-MG "always allow" knob Aaron's asking for — it already exists end-to-end. What's missing is a UI surface to flip it.
39
+
40
+ ### 1c. UI surfaces today
41
+
42
+ - `web/ui/src/routes/ChannelsList.tsx` — per-MGA inline editor. Edits `engageMode`, `engagePattern`, `senderScope`, `ignoredMessagePolicy`, `priority`. **Does not edit `unknown_sender_policy`.**
43
+ - `web/ui/src/routes/WireChannelPage.tsx` — wire-creation only. Sets `unknown_sender_policy='strict'` on new MGs (see `src/web/wire-channel.ts:136`). No per-MG detail page.
44
+ - `web/ui/src/routes/GroupDetail.tsx` — agent-group detail. Vault attachments + activity. No channel-policy surface.
45
+ - Approval card itself — Approve / Ignore (or Approve / Reject) buttons only. No "Approve & always allow this group" shortcut.
46
+
47
+ ### 1d. Approval routing today
48
+
49
+ `pickApprovalDelivery` in `src/modules/approvals/primitive.ts:104-120`:
50
+
51
+ ```ts
52
+ if (originChannelType) {
53
+ for (const userId of approvers) {
54
+ if (channelTypeOf(userId) !== originChannelType) continue;
55
+ const mg = await ensureUserDm(userId);
56
+ if (mg) return { userId, messagingGroup: mg };
57
+ }
58
+ }
59
+ for (const userId of approvers) {
60
+ const mg = await ensureUserDm(userId);
61
+ if (mg) return { userId, messagingGroup: mg };
62
+ }
63
+ ```
64
+
65
+ `ensureUserDm` (`src/modules/permissions/user-dm.ts:52-112`) is keyed `(user_id, channel_type)` against `user_dms` — so for an operator with two Telegram bots, this **always returns whichever bot's DM was resolved first**. Aaron's live `user_dms` has exactly one row, pinned to `mg-1777352749546-e4z1rv` (the UnforcedAGI Aaron-DM). Every TechneRobot approval card delivers via UnforcedAGI. That's the bug.
66
+
67
+ ### 1e. Aaron's live state (ground truth, queried 2026-05-02)
68
+
69
+ Four telegram MGs, three wired:
70
+
71
+ | MG | Bot | Chat | `unknown_sender_policy` | MGA `engage_mode` / `engage_pattern` | `sender_scope` | `ignored_message_policy` |
72
+ |---|---|---|---|---|---|---|
73
+ | `mg-…e4z1rv` | UnforcedAGI (8792…425) | Aaron DM (1190…288) | `strict` | `pattern` / `.` | `all` | `drop` |
74
+ | `mg-…ihfgeq` | UnforcedAGI | group (-1003…645) | `request_approval` | *(unwired)* | — | — |
75
+ | `mg-…ogxhkj` | TechneRobot (8757…201) | Aaron DM (1190…288) | `request_approval` | `pattern` / `.` | `known` | `accumulate` |
76
+ | `mg-…90uhi5` | TechneRobot | group (-1002…962) | `request_approval` | `pattern` / `.` | `known` | `accumulate` |
77
+
78
+ `user_dms` has one row: Aaron → telegram → UnforcedAGI Aaron-DM. So every approval today, regardless of origin bot, goes through UnforcedAGI.
79
+
80
+ ## 2. NanoClaw heritage
81
+
82
+ Migration 010 explicitly back-compats from NanoClaw's pre-rebirth `trigger_rules` JSON + `response_scope` enum. The five-knob model in section 1a is paraclaw's split of NanoClaw's two — same axis count, cleaner names, no JSON parsing in the hot path. The default values match NanoClaw's: `requiresTrigger=true` ↔ `engage_mode='mention'`; `response_scope='allowlisted'` ↔ `sender_scope='known'`. New users see paraclaw shapes; legacy installs migrated forward.
83
+
84
+ `unknown_sender_policy` is paraclaw-era — there's no equivalent in NanoClaw migration history. NanoClaw treated unknown senders the same way it treated everyone else (`response_scope` did all the gating). Paraclaw added the MG-level dimension specifically to support `request_approval` (the unwired-channel cascade) without overloading per-MGA `sender_scope`. So the *concept* is paraclaw's; the *axis split* mimics NanoClaw's design instinct (orthogonal columns over JSON blobs).
85
+
86
+ `user_dms` is also paraclaw-era. NanoClaw single-bot setups never needed bot disambiguation in DM caching — there was only one bot per channel. The `(user_id, channel_type)` PK is a NanoClaw-shaped assumption that paraclaw inherited and never updated when multi-bot landed.
87
+
88
+ **Implication for Proposal C:** there is no NanoClaw shape to defer to. We're inventing the multi-bot DM cache from scratch. That's fine — the bot dimension is genuinely new.
89
+
90
+ **Implication for the policy UI:** NanoClaw shipped a CLI-driven config model; there was no per-channel settings page on the host UI. Paraclaw added the web UI and inherited NanoClaw's enum vocabulary, then split it. So we're not deviating from a NanoClaw UI pattern — we're filling a gap that was never built.
91
+
92
+ ## 3. Problem statement (the three asks)
93
+
94
+ 1. **Approval routing follows the bot, not the operator-channel pair.** When an approval originates from an inbound on bot X, the card should reach the approver via bot X if at all possible, falling back gracefully when the approver hasn't DM'd that bot yet.
95
+ 2. **Per-MG / per-MGA policy is editable from a single intuitive surface.** Operator should be able to flip "always allow this group" (per-MG) and "respond only to mentions" (per-MGA) without remembering enum names or visiting two pages.
96
+ 3. **The approval card itself carries quick-actions** so deciding "yes, and trust this chat going forward" is one click, not a click-then-navigate.
97
+
98
+ ## 4. Proposals
99
+
100
+ ### 4a. Proposal C — bot-aware approval delivery
101
+
102
+ **Shape: extend `user_dms` PK to `(user_id, channel_type, bot_id)`.** Drop the row when `bot_id` is empty / null only for channel types that don't have a bot dimension (none today, but the schema must allow it for future channels like email). Migration:
103
+
104
+ ```sql
105
+ ALTER TABLE user_dms RENAME TO user_dms_legacy;
106
+ CREATE TABLE user_dms (
107
+ user_id TEXT NOT NULL REFERENCES users(id),
108
+ channel_type TEXT NOT NULL,
109
+ bot_id TEXT NOT NULL DEFAULT '',
110
+ messaging_group_id TEXT NOT NULL REFERENCES messaging_groups(id),
111
+ resolved_at TEXT NOT NULL,
112
+ PRIMARY KEY (user_id, channel_type, bot_id)
113
+ );
114
+ INSERT INTO user_dms (user_id, channel_type, bot_id, messaging_group_id, resolved_at)
115
+ SELECT
116
+ u.user_id,
117
+ u.channel_type,
118
+ COALESCE(
119
+ -- Decode bot_id from the cached MG's v2 platform_id (slot 1 after channel:).
120
+ SUBSTR(mg.platform_id, LENGTH(mg.channel_type) + 2,
121
+ INSTR(SUBSTR(mg.platform_id, LENGTH(mg.channel_type) + 2), ':') - 1),
122
+ ''
123
+ ) AS bot_id,
124
+ u.messaging_group_id,
125
+ u.resolved_at
126
+ FROM user_dms_legacy u
127
+ JOIN messaging_groups mg ON mg.id = u.messaging_group_id;
128
+ DROP TABLE user_dms_legacy;
129
+ ```
130
+
131
+ Aaron's existing row would migrate to `bot_id='8792496425'` (UnforcedAGI). TechneRobot DMs become uncached on day one — `ensureUserDm(userId, botId='8757751201')` returns null on first lookup, falls through to the resolver, and re-caches.
132
+
133
+ **Resolver change:** `ensureUserDm` takes an optional `botId` parameter. When provided, the cache key includes it, and on miss the resolver picks the live adapter for `(channelType, botId)` (we already have `getChannelAdapterByBotId` from PR A). When omitted, behavior is the legacy "any bot" — preserved for callers that don't care which bot delivers.
134
+
135
+ **Routing change:** `pickApprovalDelivery` takes an optional `originBotId`. The signature becomes:
136
+
137
+ ```ts
138
+ export async function pickApprovalDelivery(
139
+ approvers: string[],
140
+ originChannelType: string,
141
+ originBotId: string | null,
142
+ ): Promise<{ userId: string; messagingGroup: MessagingGroup } | null>;
143
+ ```
144
+
145
+ Resolution order (preserving the channel-tie-break that's already there):
146
+
147
+ 1. Same channel type AND `ensureUserDm(userId, originBotId)` resolves — return that bot's DM.
148
+ 2. Same channel type AND `ensureUserDm(userId, /* any */)` resolves — return whatever bot (fallback).
149
+ 3. Different channel type AND `ensureUserDm(userId, /* any */)` resolves — cross-channel fallback (existing behavior).
150
+ 4. None — `null` (existing behavior).
151
+
152
+ Step 2 is the load-bearing fallback Aaron asked about: "what if the approver hasn't DM'd this bot yet?" — they get the card on whatever bot they have DM'd, with no extra surface. The card body should still name the origin bot ("New DM to **TechneRobot** in chat XYZ") so the approver isn't confused about which bot wired up. That's a card-rendering tweak in the modules that build cards (`channel-approval.ts:115-122`, `sender-approval.ts`).
153
+
154
+ **Default for "no DM with anyone":** unchanged. `pickApprovalDelivery` returns null, the requesting module logs and aborts. Static admin-notification channels are out of scope for this doc — would be a separate dimension on the install (`alerts:` channel concept).
155
+
156
+ **Why this shape over alternatives:**
157
+
158
+ - *Separate `(user_id, channel_type, bot_id) → mg_id` table:* same row count, less migration churn (no rename), but adds a new table the rest of the code has to learn about. The PK extension is honest about what changed.
159
+ - *Compute on the fly from `messaging_groups`:* you'd resolve "what's user X's DM with bot Y" by scanning MGs. Plausible — slot1 of `platform_id` IS the bot_id under v2 — but it loses the resolver semantics (you can't cache a *failure* to find the DM; every lookup re-attempts `openDM`). The cache is load-bearing for Discord rate limits.
160
+
161
+ ### 4b. Per-MG detail page (the granular policy UI)
162
+
163
+ **Route shape: `/claw/channels/<mga-id>` for the per-wiring surface, `/claw/channels/<mg-id>` for the per-MG surface.** Disambiguate by id prefix (`mg-` vs `mga-`) — both are routable from the existing channels list with a single click.
164
+
165
+ Why two routes and not one tabbed page: the MG-level knob (`unknown_sender_policy`) is independent of any one wiring; a user can have multiple MGAs on the same MG and the policy applies to all of them. Mounting both on one MGA-keyed URL would mislead.
166
+
167
+ **`/claw/channels/<mg-id>` (per-MG):**
168
+
169
+ ```
170
+ [ telegram ] ← channel-type pill
171
+ TechneRobot ← bot.name
172
+ group: Techne Friends · -1002245300962
173
+
174
+ Who can engage?
175
+ ◉ Strict — only members of this agent group can talk to it
176
+ ◯ Always allow — anyone in this chat can engage
177
+ ◯ Approval-gated — first message from a new sender requires admin OK
178
+
179
+ [ Wirings to this group ]
180
+ • Techne (priority 0) — engage on @mention; senders: known; ignored: accumulate [ Edit ]
181
+
182
+ [ Save ] [ Cancel ]
183
+ ```
184
+
185
+ Three radio choices map directly to `strict | public | request_approval`. Copy is operator-facing — no enum names. The wirings list links each MGA to its detail page.
186
+
187
+ **`/claw/channels/<mga-id>` (per-MGA, replaces today's inline ChannelsList editor):**
188
+
189
+ ```
190
+ [ telegram ]
191
+ TechneRobot → Techne agent group
192
+
193
+ When does it engage?
194
+ ◯ Always — every message in this chat
195
+ ◉ Mentions — when @TechneRobot is tagged
196
+ ◯ Pattern — text matches a regex: [____________]
197
+
198
+ Who can talk to it?
199
+ ◯ Anyone in the chat
200
+ ◉ Members only — owner / admin / explicit member
201
+
202
+ What about messages it ignores?
203
+ ◯ Drop — not seen, not stored
204
+ ◉ Accumulate — stored for context, no reply
205
+
206
+ Priority [0] ← higher wins when multiple wirings could match
207
+
208
+ [ Save ] [ Remove wiring ]
209
+ ```
210
+
211
+ Same fields as today's inline editor, just promoted to a route so the "Edit" button can carry richer copy and there's room to display read-only metadata (created at, agent group folder, MG link).
212
+
213
+ The existing `ChannelsList` collapses to a list of MGAs that link to per-MGA detail. The per-MGA detail page links up to the per-MG page via a "Group settings →" pill at the top.
214
+
215
+ ### 4c. Quick-action on the approval card
216
+
217
+ Replace the two-button card with three buttons on cards that originate from `request_approval` policy:
218
+
219
+ - **Approve** — current behavior (admit sender + replay).
220
+ - **Approve & always allow** — admit sender, AND flip the MG's `unknown_sender_policy` to `public`. Tracked in payload, executed by the response handler.
221
+ - **Reject** — current behavior.
222
+
223
+ Three buttons fit comfortably in Telegram and Discord chat-card layouts (they accept arbitrary button rows). The rendering happens in `chat-sdk-bridge` via `ask_question`; we extend the option list and the response handler in `src/modules/permissions/index.ts:handleSenderApprovalResponse` to interpret the new value.
224
+
225
+ **Out of scope for this doc:** mention-only quick-action on the card (it's per-MGA, requires picking which MGA to update — adds card complexity). Operators who want that can navigate to per-MGA settings; it's a one-tap path from the card's chat link.
226
+
227
+ ## 5. Migration notes
228
+
229
+ - **Proposal C:** see SQL above. One migration file (`026-user-dms-bot-id.ts`). Backfill is decode-from-platform_id — works because v2 platform_ids are guaranteed by paraclaw#67 PR A. Rollback is "drop the column"; no data loss because the legacy keying is reconstructible (any bot caches just collapse).
230
+ - **Per-MG / per-MGA UI:** no schema change. Routes added, list view trimmed, two new components.
231
+ - **Quick-actions on card:** no schema change. New option `approve_and_allow` in the sender-approval card payload; handler dispatches on it.
232
+
233
+ ## 6. Open questions for Aaron
234
+
235
+ 1. **Card copy when origin bot ≠ delivery bot.** When step-2 fallback fires (TechneRobot approval delivered via UnforcedAGI because no TechneRobot↔operator DM exists yet), the card needs to clearly say "this is about **TechneRobot** in *Techne Friends*." Should the bot-name come from the agent group name, the bot's display_name, or the platform-level bot username? My instinct: bot username (most identifiable to a Telegram operator who knows `@TechneRobot`).
236
+
237
+ 2. **`Approve & always allow` scope.** Does that flip apply to the *current MG only* (per-chat trust), or also auto-add the sender as a member? The pure interpretation is MG-only; the convenient interpretation is both. I lean MG-only — admitting the sender is what regular Approve already does, and bundling them feels like overreach.
238
+
239
+ 3. **Mention-only on the card.** Section 4c skipped this. Do you want a fourth button on cards, or are operators OK navigating to per-MGA settings to flip mention mode? Three buttons is comfortable; four starts to crowd.
240
+
241
+ 4. **Two routes vs tabbed.** I went with `/channels/<mg-id>` and `/channels/<mga-id>`. If you'd rather have `/channels/<mga-id>` as the only route and surface MG settings as a collapsed accordion, that simplifies routing at the cost of nesting unrelated knobs.
242
+
243
+ ## Phasing for impl (after this doc is signed off)
244
+
245
+ 1. **PR 1: Proposal C migration + resolver + routing change.** Backwards-compatible API (botId optional). One DB migration, two function signatures. Tests: bot-aware ensureUserDm, fallback chain in pickApprovalDelivery.
246
+ 2. **PR 2: per-MGA detail page.** Route + component. No schema change. Replaces inline editor in ChannelsList.
247
+ 3. **PR 3: per-MG detail page + UnknownSenderPolicy editor.** Route + component + new PATCH on `/api/channels/mg/:id`.
248
+ 4. **PR 4: Quick-action card.** Schema-free. Card option list grows, handler grows, tests for each branch.
249
+
250
+ Each PR is independently shippable; PR 1 unblocks the routing fix Aaron asked about first; PRs 2-4 are pure UX polish over the working data model.