@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,233 @@
1
+ /**
2
+ * ChannelWireDetail tests cover the contracts the design doc names for
3
+ * the per-MGA route:
4
+ * 1. The 3-radio engage-mode editor renders all options; the row matching
5
+ * the DB is selected. "Only when mentioned" is the surface for Aaron's
6
+ * "respond only to mentions" toggle.
7
+ * 2. Saving the form calls updateChannelWire with the right input shape
8
+ * and reflects the new wire state back in the form.
9
+ * 3. Server 404 surfaces as the "no wire" empty state with a Back CTA
10
+ * (no Retry — there's nothing to come back from).
11
+ * 4. Unknown errors surface as the load-error banner with a Retry button.
12
+ * 5. Metadata block links back to the parent /channels/mg/:id route and
13
+ * to the /groups/:folder route.
14
+ * 6. Delete confirms via window.confirm and calls deleteChannelWire.
15
+ *
16
+ * The api module is mocked so we don't need a live server.
17
+ */
18
+ import { render, screen, waitFor } from '@testing-library/react';
19
+ import userEvent from '@testing-library/user-event';
20
+ import { MemoryRouter, Route, Routes } from 'react-router-dom';
21
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
22
+
23
+ import * as api from '../lib/api.ts';
24
+ import { ChannelWireDetail } from './ChannelWireDetail.tsx';
25
+
26
+ vi.mock('../lib/api.ts', async () => {
27
+ const actual = await vi.importActual<typeof api>('../lib/api.ts');
28
+ return {
29
+ ...actual,
30
+ getChannelWireDetail: vi.fn(),
31
+ updateChannelWire: vi.fn(),
32
+ deleteChannelWire: vi.fn(),
33
+ };
34
+ });
35
+
36
+ function renderAt(path: string) {
37
+ return render(
38
+ <MemoryRouter initialEntries={[path]}>
39
+ <Routes>
40
+ <Route path="/channels/mga/:id" element={<ChannelWireDetail />} />
41
+ <Route path="/channels" element={<div>channels list</div>} />
42
+ </Routes>
43
+ </MemoryRouter>,
44
+ );
45
+ }
46
+
47
+ const baseWire: api.ChannelWireView = {
48
+ id: 'mga_1',
49
+ channelType: 'telegram',
50
+ messagingGroupId: 'mg_1',
51
+ platformId: 'telegram:111111:222222',
52
+ displayName: 'Aaron DM',
53
+ agentGroupId: 'ag_1',
54
+ agentGroupFolder: 'main',
55
+ agentGroupName: 'Main agent',
56
+ engageMode: 'mention',
57
+ engagePattern: null,
58
+ senderScope: 'all',
59
+ ignoredMessagePolicy: 'drop',
60
+ priority: 0,
61
+ createdAt: '2026-04-20T10:00:00Z',
62
+ };
63
+
64
+ beforeEach(() => {
65
+ vi.mocked(api.getChannelWireDetail).mockResolvedValue(baseWire);
66
+ });
67
+
68
+ afterEach(() => {
69
+ vi.clearAllMocks();
70
+ });
71
+
72
+ describe('ChannelWireDetail — render', () => {
73
+ it('renders the engage-mode radios with the DB value selected', async () => {
74
+ renderAt('/channels/mga/mga_1');
75
+
76
+ await waitFor(() => {
77
+ expect(screen.getByText('Routing rules')).toBeInTheDocument();
78
+ });
79
+
80
+ const mention = screen.getByRole('radio', { name: /Only when mentioned/ });
81
+ const all = screen.getByRole('radio', { name: /Every message/ });
82
+ const pattern = screen.getByRole('radio', { name: /Pattern match/ });
83
+
84
+ expect(mention).toBeChecked();
85
+ expect(all).not.toBeChecked();
86
+ expect(pattern).not.toBeChecked();
87
+ });
88
+
89
+ it('shows the regex input only when pattern mode is selected', async () => {
90
+ vi.mocked(api.getChannelWireDetail).mockResolvedValue({
91
+ ...baseWire,
92
+ engageMode: 'pattern',
93
+ engagePattern: '^/ask\\b',
94
+ });
95
+ renderAt('/channels/mga/mga_1');
96
+
97
+ await waitFor(() => {
98
+ expect(screen.getByLabelText(/Engage pattern/)).toBeInTheDocument();
99
+ });
100
+ expect(screen.getByLabelText(/Engage pattern/)).toHaveValue('^/ask\\b');
101
+ });
102
+
103
+ it('renders the metadata block with links back to MG and to the agent group', async () => {
104
+ renderAt('/channels/mga/mga_1');
105
+
106
+ await waitFor(() => {
107
+ expect(screen.getByText('Wire details')).toBeInTheDocument();
108
+ });
109
+
110
+ const mgLink = screen.getByRole('link', { name: 'mg_1' });
111
+ expect(mgLink).toHaveAttribute('href', '/channels/mg/mg_1');
112
+
113
+ const groupLinks = screen.getAllByRole('link', { name: 'Main agent' });
114
+ expect(groupLinks[0]).toHaveAttribute('href', '/groups/main');
115
+ });
116
+ });
117
+
118
+ describe('ChannelWireDetail — save', () => {
119
+ it('calls updateChannelWire with the engage-mode change', async () => {
120
+ vi.mocked(api.updateChannelWire).mockResolvedValue({
121
+ ...baseWire,
122
+ engageMode: 'all',
123
+ });
124
+
125
+ const user = userEvent.setup();
126
+ renderAt('/channels/mga/mga_1');
127
+
128
+ await waitFor(() => {
129
+ expect(screen.getByRole('radio', { name: /Only when mentioned/ })).toBeChecked();
130
+ });
131
+
132
+ await user.click(screen.getByRole('radio', { name: /Every message/ }));
133
+ await user.click(screen.getByRole('button', { name: /Save routing rules/ }));
134
+
135
+ await waitFor(() => {
136
+ expect(api.updateChannelWire).toHaveBeenCalledWith('mga_1', {
137
+ engageMode: 'all',
138
+ engagePattern: null,
139
+ senderScope: 'all',
140
+ ignoredMessagePolicy: 'drop',
141
+ priority: 0,
142
+ });
143
+ });
144
+
145
+ await waitFor(() => {
146
+ expect(screen.getByRole('radio', { name: /Every message/ })).toBeChecked();
147
+ });
148
+ });
149
+
150
+ it('surfaces an error banner when the PATCH fails', async () => {
151
+ vi.mocked(api.updateChannelWire).mockRejectedValue(new api.HttpError(400, 'invalid engageMode: xyz'));
152
+
153
+ const user = userEvent.setup();
154
+ renderAt('/channels/mga/mga_1');
155
+
156
+ await waitFor(() => {
157
+ expect(screen.getByRole('radio', { name: /Only when mentioned/ })).toBeChecked();
158
+ });
159
+
160
+ await user.click(screen.getByRole('radio', { name: /Every message/ }));
161
+ await user.click(screen.getByRole('button', { name: /Save routing rules/ }));
162
+
163
+ await waitFor(() => {
164
+ expect(screen.getByText(/Couldn't save:/)).toBeInTheDocument();
165
+ });
166
+ expect(screen.getByText(/invalid engageMode: xyz/)).toBeInTheDocument();
167
+ });
168
+ });
169
+
170
+ describe('ChannelWireDetail — delete', () => {
171
+ it('calls deleteChannelWire after confirm', async () => {
172
+ vi.mocked(api.deleteChannelWire).mockResolvedValue(undefined);
173
+ const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
174
+
175
+ const user = userEvent.setup();
176
+ renderAt('/channels/mga/mga_1');
177
+
178
+ await waitFor(() => {
179
+ expect(screen.getByRole('button', { name: /Remove wire/ })).toBeInTheDocument();
180
+ });
181
+
182
+ await user.click(screen.getByRole('button', { name: /Remove wire/ }));
183
+
184
+ await waitFor(() => {
185
+ expect(api.deleteChannelWire).toHaveBeenCalledWith('mga_1');
186
+ });
187
+
188
+ confirmSpy.mockRestore();
189
+ });
190
+
191
+ it('does not call deleteChannelWire when confirm is dismissed', async () => {
192
+ const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false);
193
+
194
+ const user = userEvent.setup();
195
+ renderAt('/channels/mga/mga_1');
196
+
197
+ await waitFor(() => {
198
+ expect(screen.getByRole('button', { name: /Remove wire/ })).toBeInTheDocument();
199
+ });
200
+
201
+ await user.click(screen.getByRole('button', { name: /Remove wire/ }));
202
+
203
+ expect(api.deleteChannelWire).not.toHaveBeenCalled();
204
+ confirmSpy.mockRestore();
205
+ });
206
+ });
207
+
208
+ describe('ChannelWireDetail — error states', () => {
209
+ it('renders 404 empty state with Back CTA only', async () => {
210
+ vi.mocked(api.getChannelWireDetail).mockRejectedValue(
211
+ new api.HttpError(404, 'channel wire not found: mga_missing'),
212
+ );
213
+
214
+ renderAt('/channels/mga/mga_missing');
215
+
216
+ await waitFor(() => {
217
+ expect(screen.getByText(/No wire with id/)).toBeInTheDocument();
218
+ });
219
+ expect(screen.getByRole('button', { name: 'Back to channels' })).toBeInTheDocument();
220
+ expect(screen.queryByRole('button', { name: 'Retry' })).not.toBeInTheDocument();
221
+ });
222
+
223
+ it('renders generic load error with Retry button on non-404', async () => {
224
+ vi.mocked(api.getChannelWireDetail).mockRejectedValue(new api.HttpError(500, 'internal'));
225
+
226
+ renderAt('/channels/mga/mga_1');
227
+
228
+ await waitFor(() => {
229
+ expect(screen.getByText(/Couldn't load this wire/)).toBeInTheDocument();
230
+ });
231
+ expect(screen.getByRole('button', { name: 'Retry' })).toBeInTheDocument();
232
+ });
233
+ });
@@ -0,0 +1,403 @@
1
+ /**
2
+ * /channels/mga/:id — per-wire (messaging-group ↔ agent-group) detail +
3
+ * routing-rules editor.
4
+ *
5
+ * What's here:
6
+ * - read-only metadata (linked target group, parent messaging group, channel,
7
+ * platform id, priority, created)
8
+ * - the routing rules editor: engage mode (mention | all | pattern), pattern,
9
+ * sender scope, ignored-message policy, priority
10
+ * - delete-wire action with native confirm
11
+ *
12
+ * The MGA-id is a UUID generated server-side; the disambiguation prefix
13
+ * `mga/` keeps the route clean against per-MG routes (`mg/`).
14
+ *
15
+ * `engageMode = 'mention'` is the surface for "respond only to mentions".
16
+ * The DB-side enum has a third state `mention-sticky` that the server
17
+ * collapses to `'mention'` for display and preserves on PATCH so a future
18
+ * sticky-aware editor doesn't silently downgrade existing wires.
19
+ */
20
+ import { useCallback, useEffect, useState } from 'react';
21
+ import { Link, useParams } from 'react-router-dom';
22
+ import {
23
+ deleteChannelWire,
24
+ getChannelWireDetail,
25
+ HttpError,
26
+ updateChannelWire,
27
+ type ChannelWireView,
28
+ type EngageMode,
29
+ type IgnoredMessagePolicy,
30
+ type SenderScope,
31
+ type UpdateChannelWireInput,
32
+ } from '../lib/api.ts';
33
+
34
+ type State =
35
+ | { kind: 'loading' }
36
+ | { kind: 'ok'; wire: ChannelWireView }
37
+ | { kind: 'error'; status: number | null; message: string };
38
+
39
+ export function ChannelWireDetail() {
40
+ const { id: rawId } = useParams<{ id: string }>();
41
+ const id = rawId ?? '';
42
+ const [state, setState] = useState<State>({ kind: 'loading' });
43
+ const [reloadKey, setReloadKey] = useState(0);
44
+ const [saving, setSaving] = useState(false);
45
+ const [saveError, setSaveError] = useState<string | null>(null);
46
+ const [deleting, setDeleting] = useState(false);
47
+
48
+ const reload = useCallback(() => setReloadKey((k) => k + 1), []);
49
+
50
+ useEffect(() => {
51
+ if (!id) {
52
+ setState({ kind: 'error', status: null, message: 'no wire id in URL' });
53
+ return;
54
+ }
55
+ let cancelled = false;
56
+ getChannelWireDetail(id)
57
+ .then((wire) => !cancelled && setState({ kind: 'ok', wire }))
58
+ .catch((err) => {
59
+ if (cancelled) return;
60
+ const status = err instanceof HttpError ? err.status : null;
61
+ setState({
62
+ kind: 'error',
63
+ status,
64
+ message: err instanceof Error ? err.message : String(err),
65
+ });
66
+ });
67
+ return () => {
68
+ cancelled = true;
69
+ };
70
+ }, [id, reloadKey]);
71
+
72
+ if (state.kind === 'loading') {
73
+ return (
74
+ <div>
75
+ <h2>Channel wire</h2>
76
+ <ul className="skeleton-list" aria-busy="true">
77
+ <li className="skeleton skeleton-row" />
78
+ <li className="skeleton skeleton-row" />
79
+ </ul>
80
+ </div>
81
+ );
82
+ }
83
+
84
+ if (state.kind === 'error') {
85
+ return (
86
+ <div>
87
+ <h2>Channel wire</h2>
88
+ <div className="error-banner">
89
+ {state.status === 404 ? (
90
+ <>
91
+ No wire with id <code>{id}</code> — it may have been removed.
92
+ </>
93
+ ) : (
94
+ <>
95
+ Couldn't load this wire: <code>{state.message}</code>
96
+ </>
97
+ )}
98
+ </div>
99
+ <div className="actions" style={{ marginTop: '1rem' }}>
100
+ <Link to="/channels">
101
+ <button className="secondary">Back to channels</button>
102
+ </Link>
103
+ {state.status !== 404 && <button onClick={reload}>Retry</button>}
104
+ </div>
105
+ </div>
106
+ );
107
+ }
108
+
109
+ const { wire } = state;
110
+
111
+ const onSave = async (input: UpdateChannelWireInput) => {
112
+ setSaving(true);
113
+ setSaveError(null);
114
+ try {
115
+ const updated = await updateChannelWire(id, input);
116
+ setState({ kind: 'ok', wire: updated });
117
+ } catch (err) {
118
+ setSaveError(err instanceof Error ? err.message : String(err));
119
+ } finally {
120
+ setSaving(false);
121
+ }
122
+ };
123
+
124
+ const onDelete = async () => {
125
+ if (
126
+ !confirm(
127
+ `Remove ${wire.channelType} wire to ${wire.agentGroupName}? Inbound messages on this thread will fall back to the unwired-channel guard (silently dropped).`,
128
+ )
129
+ ) {
130
+ return;
131
+ }
132
+ setDeleting(true);
133
+ setSaveError(null);
134
+ try {
135
+ await deleteChannelWire(id);
136
+ setState({ kind: 'error', status: 404, message: 'wire deleted' });
137
+ } catch (err) {
138
+ setSaveError(err instanceof Error ? err.message : String(err));
139
+ setDeleting(false);
140
+ }
141
+ };
142
+
143
+ return (
144
+ <div>
145
+ <div className="list-header">
146
+ <h2 style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flexWrap: 'wrap' }}>
147
+ <Link to="/channels" className="muted" style={{ textDecoration: 'none' }}>
148
+ Channels
149
+ </Link>
150
+ <span className="dim">/</span>
151
+ <span style={{ textTransform: 'capitalize' }}>{wire.channelType}</span>
152
+ <span className="dim">→</span>
153
+ <Link to={`/groups/${encodeURIComponent(wire.agentGroupFolder)}`}>{wire.agentGroupName}</Link>
154
+ </h2>
155
+ </div>
156
+
157
+ <section
158
+ style={{
159
+ background: 'white',
160
+ border: '1px solid var(--border)',
161
+ borderRadius: '8px',
162
+ padding: '1rem 1.25rem',
163
+ marginBottom: '1rem',
164
+ }}
165
+ >
166
+ <h3 style={{ marginTop: 0 }}>Wire details</h3>
167
+ <div className="kv">
168
+ <div>id</div>
169
+ <div>
170
+ <code>{wire.id}</code>
171
+ </div>
172
+ <div>channel</div>
173
+ <div style={{ textTransform: 'capitalize' }}>{wire.channelType}</div>
174
+ <div>messaging group</div>
175
+ <div>
176
+ <Link to={`/channels/mg/${encodeURIComponent(wire.messagingGroupId)}`}>
177
+ <code>{wire.messagingGroupId}</code>
178
+ </Link>
179
+ {wire.displayName && <span className="dim"> · {wire.displayName}</span>}
180
+ </div>
181
+ <div>platform id</div>
182
+ <div>
183
+ <code>{wire.platformId}</code>
184
+ </div>
185
+ <div>agent group</div>
186
+ <div>
187
+ <Link to={`/groups/${encodeURIComponent(wire.agentGroupFolder)}`}>{wire.agentGroupName}</Link>
188
+ </div>
189
+ <div>created</div>
190
+ <div>
191
+ <code>{wire.createdAt}</code>
192
+ </div>
193
+ </div>
194
+ </section>
195
+
196
+ <RoutingRulesEditor wire={wire} saving={saving} onSave={onSave} />
197
+
198
+ {saveError && (
199
+ <div className="error-banner" style={{ marginTop: '0.5rem' }}>
200
+ Couldn't save: <code>{saveError}</code>
201
+ </div>
202
+ )}
203
+
204
+ <section
205
+ style={{
206
+ background: 'white',
207
+ border: '1px solid var(--border)',
208
+ borderRadius: '8px',
209
+ padding: '1rem 1.25rem',
210
+ marginTop: '1rem',
211
+ }}
212
+ >
213
+ <h3 style={{ marginTop: 0 }}>Danger zone</h3>
214
+ <p className="muted">
215
+ Removing this wire stops the agent from receiving inbound messages on this thread. The messaging group itself
216
+ stays — you can re-wire it later from <Link to="/channels/new">Channels → New</Link>.
217
+ </p>
218
+ <button
219
+ className="secondary"
220
+ onClick={onDelete}
221
+ disabled={deleting || saving}
222
+ style={{ borderColor: 'var(--error)', color: 'var(--error)' }}
223
+ >
224
+ {deleting ? 'Removing…' : 'Remove wire'}
225
+ </button>
226
+ </section>
227
+ </div>
228
+ );
229
+ }
230
+
231
+ interface EngageChoice {
232
+ value: EngageMode;
233
+ label: string;
234
+ blurb: string;
235
+ }
236
+
237
+ const ENGAGE_CHOICES: EngageChoice[] = [
238
+ {
239
+ value: 'mention',
240
+ label: 'Only when mentioned',
241
+ blurb: 'The agent responds only when @-tagged. Best for shared channels where the agent is one of many participants.',
242
+ },
243
+ {
244
+ value: 'all',
245
+ label: 'Every message',
246
+ blurb: 'The agent responds to every message in this thread. Right for DMs and dedicated channels.',
247
+ },
248
+ {
249
+ value: 'pattern',
250
+ label: 'Pattern match',
251
+ blurb: 'The agent responds when the message text matches a regex. Use for command-prefix channels (e.g. ^/ask\\b).',
252
+ },
253
+ ];
254
+
255
+ function RoutingRulesEditor({
256
+ wire,
257
+ saving,
258
+ onSave,
259
+ }: {
260
+ wire: ChannelWireView;
261
+ saving: boolean;
262
+ onSave: (input: UpdateChannelWireInput) => void;
263
+ }) {
264
+ const [engageMode, setEngageMode] = useState<EngageMode>(wire.engageMode);
265
+ const [engagePattern, setEngagePattern] = useState(wire.engagePattern ?? '');
266
+ const [senderScope, setSenderScope] = useState<SenderScope>(wire.senderScope);
267
+ const [ignoredMessagePolicy, setIgnoredMessagePolicy] = useState<IgnoredMessagePolicy>(wire.ignoredMessagePolicy);
268
+ const [priority, setPriority] = useState(String(wire.priority));
269
+
270
+ useEffect(() => {
271
+ setEngageMode(wire.engageMode);
272
+ setEngagePattern(wire.engagePattern ?? '');
273
+ setSenderScope(wire.senderScope);
274
+ setIgnoredMessagePolicy(wire.ignoredMessagePolicy);
275
+ setPriority(String(wire.priority));
276
+ }, [wire]);
277
+
278
+ const onSubmit = (e: React.FormEvent) => {
279
+ e.preventDefault();
280
+ const parsedPriority = Number.parseInt(priority, 10);
281
+ onSave({
282
+ engageMode,
283
+ engagePattern: engageMode === 'pattern' ? engagePattern.trim() || null : null,
284
+ senderScope,
285
+ ignoredMessagePolicy,
286
+ priority: Number.isFinite(parsedPriority) ? parsedPriority : wire.priority,
287
+ });
288
+ };
289
+
290
+ return (
291
+ <form
292
+ onSubmit={onSubmit}
293
+ style={{
294
+ background: 'white',
295
+ border: '1px solid var(--border)',
296
+ borderRadius: '8px',
297
+ padding: '1rem 1.25rem',
298
+ marginBottom: '1rem',
299
+ }}
300
+ >
301
+ <h3 style={{ marginTop: 0 }}>Routing rules</h3>
302
+
303
+ <div role="radiogroup" aria-label="Engage mode" style={{ marginBottom: '1rem' }}>
304
+ <label htmlFor="engageMode-mention" className="muted" style={{ display: 'block', marginBottom: '0.4rem' }}>
305
+ When should this agent engage?
306
+ </label>
307
+ {ENGAGE_CHOICES.map((choice) => {
308
+ const selected = engageMode === choice.value;
309
+ return (
310
+ <label
311
+ key={choice.value}
312
+ htmlFor={`engageMode-${choice.value}`}
313
+ style={{
314
+ display: 'block',
315
+ padding: '0.6rem 0.75rem',
316
+ borderRadius: '6px',
317
+ background: selected ? 'var(--accent-bg, #eef4ff)' : 'transparent',
318
+ border: selected ? '1px solid var(--accent, #5076ff)' : '1px solid transparent',
319
+ cursor: saving ? 'progress' : 'pointer',
320
+ marginBottom: '0.4rem',
321
+ }}
322
+ >
323
+ <input
324
+ id={`engageMode-${choice.value}`}
325
+ type="radio"
326
+ name="engageMode"
327
+ value={choice.value}
328
+ checked={selected}
329
+ disabled={saving}
330
+ onChange={() => setEngageMode(choice.value)}
331
+ />{' '}
332
+ <strong>{choice.label}</strong>
333
+ <p className="muted" style={{ margin: '0.25rem 0 0 1.6rem' }}>
334
+ {choice.blurb}
335
+ </p>
336
+ </label>
337
+ );
338
+ })}
339
+ </div>
340
+
341
+ {engageMode === 'pattern' && (
342
+ <div className="row">
343
+ <label htmlFor="engagePattern">Engage pattern (regex)</label>
344
+ <input
345
+ id="engagePattern"
346
+ type="text"
347
+ value={engagePattern}
348
+ onChange={(e) => setEngagePattern(e.target.value)}
349
+ placeholder="^/ask\b"
350
+ disabled={saving}
351
+ />
352
+ </div>
353
+ )}
354
+
355
+ <div className="row">
356
+ <label htmlFor="senderScope">Who can talk to this agent?</label>
357
+ <select
358
+ id="senderScope"
359
+ value={senderScope}
360
+ onChange={(e) => setSenderScope(e.target.value as SenderScope)}
361
+ disabled={saving}
362
+ >
363
+ <option value="all">all — anyone in the thread</option>
364
+ <option value="allowlist">allowlist — only members of the agent group</option>
365
+ </select>
366
+ </div>
367
+
368
+ <div className="row">
369
+ <label htmlFor="ignoredMessagePolicy">What about messages the agent ignores?</label>
370
+ <select
371
+ id="ignoredMessagePolicy"
372
+ value={ignoredMessagePolicy}
373
+ onChange={(e) => setIgnoredMessagePolicy(e.target.value as IgnoredMessagePolicy)}
374
+ disabled={saving}
375
+ >
376
+ <option value="drop">drop — discard, no record</option>
377
+ <option value="silent">silent — log them in the conversation but don't reply</option>
378
+ </select>
379
+ </div>
380
+
381
+ <div className="row">
382
+ <label htmlFor="priority">Priority</label>
383
+ <input
384
+ id="priority"
385
+ type="number"
386
+ value={priority}
387
+ onChange={(e) => setPriority(e.target.value)}
388
+ disabled={saving}
389
+ style={{ width: '6rem' }}
390
+ />
391
+ <p className="dim" style={{ marginTop: '0.25rem', fontSize: '0.8rem' }}>
392
+ Higher wins when multiple wires could match the same inbound.
393
+ </p>
394
+ </div>
395
+
396
+ <div className="actions">
397
+ <button type="submit" disabled={saving}>
398
+ {saving ? 'Saving…' : 'Save routing rules'}
399
+ </button>
400
+ </div>
401
+ </form>
402
+ );
403
+ }