@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,279 @@
1
+ /**
2
+ * Approvals primitive — the public API that other modules call.
3
+ *
4
+ * Two surfaces:
5
+ * - `requestApproval()` — queue an approval request, deliver the card to
6
+ * the right admin DM, record the row in `approvals` (paraclaw#11). Used
7
+ * by any module that needs admin confirmation before doing something
8
+ * sensitive.
9
+ * - `registerApprovalHandler(action, handler)` — called at module import
10
+ * time. When the admin approves a pending row with matching `action`,
11
+ * the response handler dispatches into the registered callback. Optional
12
+ * modules (self-mod, future module gates) register here.
13
+ *
14
+ * Approver picking lives here too — it used to sit in src/access.ts and got
15
+ * folded in with the PR #7 re-tier. The picks functions walk user_roles
16
+ * (owner, global admin, scoped admin) and resolve to a reachable DM via the
17
+ * permissions module's user-dm helper.
18
+ *
19
+ * Tier: default module. Permissions is an optional module, so importing from
20
+ * it here is technically a tier inversion — but the host bundles both with
21
+ * main, and the alternative (a third "permissions-primitive" default module
22
+ * exposing just user-roles/user-dms) is more churn than it's worth. Revisit
23
+ * if either module becomes genuinely optional (see REFACTOR_PLAN open q #3).
24
+ */
25
+ import { normalizeOptions, type RawOption } from '../../channels/ask-question.js';
26
+ import { getMessagingGroup } from '../../db/messaging-groups.js';
27
+ import { createApproval, getSession } from '../../db/sessions.js';
28
+ import { getDeliveryAdapter } from '../../delivery.js';
29
+ import { wakeContainer } from '../../container-runner.js';
30
+ import { log } from '../../log.js';
31
+ import { decodePlatformIdAs } from '../../platform-id.js';
32
+ import { writeSessionMessage } from '../../session-manager.js';
33
+ import type { MessagingGroup, Session } from '../../types.js';
34
+ import { getAdminsOfAgentGroup, getGlobalAdmins, getOwners } from '../permissions/db/user-roles.js';
35
+ import { ensureUserDm } from '../permissions/user-dm.js';
36
+
37
+ /** Two-button approval UI — the only options the primitive supports today. */
38
+ const APPROVAL_OPTIONS: RawOption[] = [
39
+ { label: 'Approve', selectedLabel: '✅ Approved', value: 'approve' },
40
+ { label: 'Reject', selectedLabel: '❌ Rejected', value: 'reject' },
41
+ ];
42
+
43
+ // ── Approval handler registry ──
44
+ // Modules that want to be called back when an admin approves a pending row
45
+ // register here at import time, keyed by the `action` string they used in
46
+ // their `requestApproval()` calls.
47
+
48
+ export interface ApprovalHandlerContext {
49
+ session: Session;
50
+ payload: Record<string, unknown>;
51
+ /** User ID of the admin who approved. Empty string if unknown. */
52
+ userId: string;
53
+ /** Send a system chat message to the requesting agent's session. */
54
+ notify: (text: string) => void;
55
+ }
56
+
57
+ export type ApprovalHandler = (ctx: ApprovalHandlerContext) => Promise<void>;
58
+
59
+ const approvalHandlers = new Map<string, ApprovalHandler>();
60
+
61
+ export function registerApprovalHandler(action: string, handler: ApprovalHandler): void {
62
+ if (approvalHandlers.has(action)) {
63
+ log.warn('Approval handler re-registered (overwriting)', { action });
64
+ }
65
+ approvalHandlers.set(action, handler);
66
+ }
67
+
68
+ export function getApprovalHandler(action: string): ApprovalHandler | undefined {
69
+ return approvalHandlers.get(action);
70
+ }
71
+
72
+ // ── Approver picking ──
73
+
74
+ /**
75
+ * Ordered list of user IDs eligible to approve an action for the given agent
76
+ * group. Preference: admins @ that group → global admins → owners.
77
+ */
78
+ export function pickApprover(agentGroupId: string | null): string[] {
79
+ const approvers: string[] = [];
80
+ const seen = new Set<string>();
81
+ const add = (id: string): void => {
82
+ if (!seen.has(id)) {
83
+ seen.add(id);
84
+ approvers.push(id);
85
+ }
86
+ };
87
+
88
+ if (agentGroupId) {
89
+ for (const r of getAdminsOfAgentGroup(agentGroupId)) add(r.user_id);
90
+ }
91
+ for (const r of getGlobalAdmins()) add(r.user_id);
92
+ for (const r of getOwners()) add(r.user_id);
93
+
94
+ return approvers;
95
+ }
96
+
97
+ /**
98
+ * Walk the approver list and return the first reachable
99
+ * (approverId, messagingGroup, viaFallbackBot) tuple. Returns null if
100
+ * nobody is reachable.
101
+ *
102
+ * Resolution order, when both `originChannelType` and `originBotId` are set:
103
+ *
104
+ * 1. Same-channel approver, exact `(channel, originBotId)` match —
105
+ * best case, the card delivers via the same bot the inbound
106
+ * came in on.
107
+ * 2. Same-channel approver, channel-default DM (`bot_id = ''` slot
108
+ * in `user_dms`, configured via `/agent/settings/approvals`) —
109
+ * `viaFallbackBot: true`, so callers can name the origin bot in
110
+ * the card body to avoid confusion.
111
+ * 3. Cross-channel approver, channel-default DM — same-channel
112
+ * delivery wasn't possible at all (no approver on this channel,
113
+ * or none of them have any DM cached).
114
+ * 4. None — null.
115
+ *
116
+ * When only `originChannelType` is provided (single-bot install,
117
+ * legacy callers), step 1 collapses into step 2: the bot id is
118
+ * effectively `''` and the channel-default row is the cache.
119
+ *
120
+ * Cold-resolve at step 1 hits the adapter registered for
121
+ * `(channel, originBotId)` directly — see `ensureUserDm` — so a
122
+ * cache miss for an active secondary bot triggers `openDM` on that
123
+ * bot, not on whichever bot happens to be first in the registry.
124
+ */
125
+ export async function pickApprovalDelivery(
126
+ approvers: string[],
127
+ originChannelType: string,
128
+ originBotId: string | null = null,
129
+ ): Promise<{ userId: string; messagingGroup: MessagingGroup; viaFallbackBot: boolean } | null> {
130
+ // Step 1 — same channel, exact bot match.
131
+ if (originChannelType && originBotId) {
132
+ for (const userId of approvers) {
133
+ if (channelTypeOf(userId) !== originChannelType) continue;
134
+ const mg = await ensureUserDm(userId, { botId: originBotId });
135
+ if (mg) return { userId, messagingGroup: mg, viaFallbackBot: false };
136
+ }
137
+ }
138
+ // Step 2 — same channel, channel-default DM. `viaFallbackBot` is true
139
+ // only when an originBotId was requested but didn't resolve.
140
+ if (originChannelType) {
141
+ for (const userId of approvers) {
142
+ if (channelTypeOf(userId) !== originChannelType) continue;
143
+ const mg = await ensureUserDm(userId);
144
+ if (mg) return { userId, messagingGroup: mg, viaFallbackBot: !!originBotId };
145
+ }
146
+ }
147
+ // Step 3 — cross-channel any.
148
+ for (const userId of approvers) {
149
+ const mg = await ensureUserDm(userId);
150
+ if (mg) return { userId, messagingGroup: mg, viaFallbackBot: !!originBotId };
151
+ }
152
+ return null;
153
+ }
154
+
155
+ function channelTypeOf(userId: string): string {
156
+ const idx = userId.indexOf(':');
157
+ return idx < 0 ? '' : userId.slice(0, idx);
158
+ }
159
+
160
+ // ── Request API ──
161
+
162
+ /** Send a system chat to the agent's session. Used by callers and by the response handler. */
163
+ export function notifyAgent(session: Session, text: string): void {
164
+ writeSessionMessage(session.agent_group_id, session.id, {
165
+ id: `sys-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
166
+ kind: 'chat',
167
+ timestamp: new Date().toISOString(),
168
+ platformId: session.agent_group_id,
169
+ channelType: 'agent',
170
+ threadId: null,
171
+ content: JSON.stringify({ text, sender: 'system', senderId: 'system' }),
172
+ });
173
+ const fresh = getSession(session.id);
174
+ if (fresh) {
175
+ wakeContainer(fresh).catch((err) => log.error('Failed to wake container after notification', { err }));
176
+ }
177
+ }
178
+
179
+ export interface RequestApprovalOptions {
180
+ session: Session;
181
+ agentName: string;
182
+ /** Free-form action identifier. Must match the key the consumer registered via registerApprovalHandler. */
183
+ action: string;
184
+ /** JSON-serializable opaque payload. Carried in the approvals row body, handed to the handler on approve. */
185
+ payload: Record<string, unknown>;
186
+ /** Card title shown to the admin. */
187
+ title: string;
188
+ /** Card body shown to the admin. */
189
+ question: string;
190
+ }
191
+
192
+ /**
193
+ * Queue an approval request. Picks an approver, delivers the card to their
194
+ * DM, and records the row in `approvals` (kind = action). Fire-and-forget
195
+ * from the caller's perspective — the admin's response kicks off the
196
+ * registered approval handler for this action via the response dispatcher.
197
+ */
198
+ export async function requestApproval(opts: RequestApprovalOptions): Promise<void> {
199
+ const { session, action, payload, title, question, agentName } = opts;
200
+
201
+ const approvers = pickApprover(session.agent_group_id);
202
+ if (approvers.length === 0) {
203
+ notifyAgent(session, `${action} failed: no owner or admin configured to approve.`);
204
+ return;
205
+ }
206
+
207
+ const originMg = session.messaging_group_id ? (getMessagingGroup(session.messaging_group_id) ?? null) : null;
208
+ const originChannelType = originMg?.channel_type ?? '';
209
+ // The session's MG is paraclaw-managed and gets v2-shaped on creation
210
+ // (or backfilled by startup-bootstrap), so slot1 of the platform_id is
211
+ // the bot id. v1 rows return botId=null and we route by channel only —
212
+ // same path as a single-bot install.
213
+ const originBotId = originMg ? decodePlatformIdAs(originMg.platform_id, 'v2').botId : null;
214
+
215
+ const target = await pickApprovalDelivery(approvers, originChannelType, originBotId);
216
+ if (!target) {
217
+ const hint = originBotId ? ` Ask them to DM ${originBotId} once so the bot can reach them, then retry.` : '';
218
+ notifyAgent(session, `${action} failed: no DM channel found for any eligible approver.${hint}`);
219
+ return;
220
+ }
221
+
222
+ const approvalId = `appr-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
223
+ const normalizedOptions = normalizeOptions(APPROVAL_OPTIONS);
224
+ createApproval({
225
+ id: approvalId,
226
+ kind: action,
227
+ agent_group_id: session.agent_group_id,
228
+ session_id: session.id,
229
+ body: {
230
+ title,
231
+ options: normalizedOptions,
232
+ request_id: approvalId,
233
+ payload,
234
+ platform_id: target.messagingGroup.platform_id,
235
+ channel_type: target.messagingGroup.channel_type,
236
+ thread_id: null,
237
+ platform_message_id: null,
238
+ },
239
+ created_at: new Date().toISOString(),
240
+ });
241
+
242
+ const adapter = getDeliveryAdapter();
243
+ if (adapter) {
244
+ try {
245
+ await adapter.deliver(
246
+ target.messagingGroup.channel_type,
247
+ target.messagingGroup.platform_id,
248
+ null,
249
+ 'chat-sdk',
250
+ JSON.stringify({
251
+ type: 'ask_question',
252
+ questionId: approvalId,
253
+ title,
254
+ question: appendFallbackNotice(question, target.viaFallbackBot, originBotId),
255
+ options: APPROVAL_OPTIONS,
256
+ }),
257
+ );
258
+ } catch (err) {
259
+ log.error('Failed to deliver approval card', { action, approvalId, err });
260
+ notifyAgent(session, `${action} failed: could not deliver approval request to ${target.userId}.`);
261
+ return;
262
+ }
263
+ }
264
+
265
+ log.info('Approval requested', { action, approvalId, agentName, approver: target.userId });
266
+ }
267
+
268
+ /**
269
+ * When `pickApprovalDelivery` falls back to the channel-default bot
270
+ * (because the inbound bot can't DM this approver), append a one-line
271
+ * notice to the card body. Surfaces the mismatch at the moment the
272
+ * approver is making a decision, with a pointer to where they can
273
+ * change the default if they want cards on the originating bot.
274
+ */
275
+ export function appendFallbackNotice(question: string, viaFallbackBot: boolean, originBotId: string | null): string {
276
+ if (!viaFallbackBot) return question;
277
+ const hint = originBotId ? ` (inbound bot ${originBotId})` : '';
278
+ return `${question}\n\n_Routed via your default approval bot${hint}. Change in /agent/settings/approvals._`;
279
+ }
@@ -0,0 +1,27 @@
1
+ ## Approvals module
2
+
3
+ Admin-gated approval flow for agent self-modification. Lives in `src/modules/approvals/`.
4
+
5
+ ### Flow
6
+
7
+ The container writes a `system`-kind outbound row with one of two actions — `install_packages`, `add_mcp_server`. The module's delivery-action handlers validate, route to the right approver's DM, and persist a `pending_approvals` row. When the admin clicks a button, the registered response handler applies the change (config update → image rebuild if needed → container kill) and notifies the agent via system chat.
8
+
9
+ ### Wiring
10
+
11
+ - **Delivery actions:** `install_packages`, `add_mcp_server` via `registerDeliveryAction`.
12
+ - **Response handler:** claims approval cards by `pending_approvals` row lookup.
13
+
14
+ ### Tables
15
+
16
+ `pending_approvals` (created by `module-approvals-pending-approvals.ts`). Not dropped on uninstall — approvals in flight aren't lost on reinstall.
17
+
18
+ ### Core integration
19
+
20
+ The module depends on host-side infra but does not reach into core decision paths beyond the registered hooks:
21
+
22
+ - `buildAgentGroupImage`, `killContainer` from container-runner (image rebuilds)
23
+ - `updateContainerConfig` from container-config (apt/npm/mcp edits)
24
+ - `pickApprover`, `pickApprovalDelivery` from access
25
+ - `getDeliveryAdapter` in request-approval.ts
26
+
27
+ No core code imports from this module. Removing it: delete `src/modules/approvals/`, remove the import from `src/modules/index.ts`. Delivery actions will log "Unknown system action"; button clicks on approval cards will log "Unclaimed response". Stale rows remain in `pending_approvals` until reinstall or manual cleanup.
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Handle an admin's response to an approval card.
3
+ *
4
+ * Module-initiated actions — the module called `requestApproval()` with some
5
+ * free-form `action` string and registered a handler via
6
+ * `registerApprovalHandler(action, handler)`. On approve, we look up the
7
+ * handler and call it; on reject, we notify the agent and move on.
8
+ *
9
+ * The response handler is registered via core's `registerResponseHandler`;
10
+ * core iterates handlers and the first one to return `true` claims the response.
11
+ */
12
+ import { wakeContainer } from '../../container-runner.js';
13
+ import { deleteApproval, getApproval, getSession } from '../../db/sessions.js';
14
+ import type { ResponsePayload } from '../../response-registry.js';
15
+ import { log } from '../../log.js';
16
+ import { writeSessionMessage } from '../../session-manager.js';
17
+ import type { ActionApprovalBody, Approval } from '../../types.js';
18
+ import { getApprovalHandler } from './primitive.js';
19
+
20
+ export async function handleApprovalsResponse(payload: ResponsePayload): Promise<boolean> {
21
+ const approval = getApproval(payload.questionId);
22
+ if (!approval) return false;
23
+ if (approval.kind === 'question') return false;
24
+
25
+ await handleRegisteredApproval(approval, payload.value, payload.userId ?? '');
26
+ return true;
27
+ }
28
+
29
+ async function handleRegisteredApproval(approval: Approval, selectedOption: string, userId: string): Promise<void> {
30
+ if (!approval.session_id) {
31
+ deleteApproval(approval.id);
32
+ return;
33
+ }
34
+ const session = getSession(approval.session_id);
35
+ if (!session) {
36
+ deleteApproval(approval.id);
37
+ return;
38
+ }
39
+
40
+ const notify = (text: string): void => {
41
+ writeSessionMessage(session.agent_group_id, session.id, {
42
+ id: `appr-note-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
43
+ kind: 'chat',
44
+ timestamp: new Date().toISOString(),
45
+ platformId: session.agent_group_id,
46
+ channelType: 'agent',
47
+ threadId: null,
48
+ content: JSON.stringify({ text, sender: 'system', senderId: 'system' }),
49
+ });
50
+ };
51
+
52
+ if (selectedOption !== 'approve') {
53
+ notify(`Your ${approval.kind} request was rejected by admin.`);
54
+ log.info('Approval rejected', { approvalId: approval.id, action: approval.kind, userId });
55
+ deleteApproval(approval.id);
56
+ await wakeContainer(session);
57
+ return;
58
+ }
59
+
60
+ // Approved — dispatch to the module that registered for this action.
61
+ const handler = getApprovalHandler(approval.kind);
62
+ if (!handler) {
63
+ log.warn('No approval handler registered — row dropped', {
64
+ approvalId: approval.id,
65
+ action: approval.kind,
66
+ });
67
+ notify(`Your ${approval.kind} was approved, but no handler is installed to apply it.`);
68
+ deleteApproval(approval.id);
69
+ await wakeContainer(session);
70
+ return;
71
+ }
72
+
73
+ const body = approval.body as ActionApprovalBody;
74
+ const payload = body.payload ?? {};
75
+ try {
76
+ await handler({ session, payload, userId, notify });
77
+ log.info('Approval handled', { approvalId: approval.id, action: approval.kind, userId });
78
+ } catch (err) {
79
+ log.error('Approval handler threw', { approvalId: approval.id, action: approval.kind, err });
80
+ notify(
81
+ `Your ${approval.kind} was approved, but applying it failed: ${err instanceof Error ? err.message : String(err)}.`,
82
+ );
83
+ }
84
+
85
+ deleteApproval(approval.id);
86
+ await wakeContainer(session);
87
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Modules barrel.
3
+ *
4
+ * Each module self-registers at import time. This barrel is imported by
5
+ * src/index.ts for side effects (registry registrations, typing impl setup,
6
+ * etc.). Core runs with an empty barrel — the registries have inline
7
+ * fallbacks and `sqlite_master` guards.
8
+ *
9
+ * Default modules (ship with main, direct core import):
10
+ * - src/modules/typing/ → imported directly by router/delivery/container-runner
11
+ * - src/modules/mount-security/ → imported directly by container-runner
12
+ *
13
+ * Registry-based modules (installed via /add-<name> skills, pulled from the
14
+ * `modules` branch): append imports below.
15
+ */
16
+ // Approvals (default tier) must load before self-mod (optional) so the
17
+ // registerApprovalHandler / requestApproval symbols are bound when self-mod
18
+ // registers its handlers at import time.
19
+ import './approvals/index.js';
20
+ import './interactive/index.js';
21
+ import './scheduling/index.js';
22
+ import './permissions/index.js';
23
+ import './agent-to-agent/index.js';
24
+ import './self-mod/index.js';
@@ -0,0 +1,21 @@
1
+ ## ask_user_question
2
+
3
+ Use `ask_user_question` when you need the user to pick from a small set of concrete options and you can't infer a reasonable default. This is a **blocking** call — your turn pauses until the user clicks or the timeout expires.
4
+
5
+ **When to use:**
6
+ - Confirming a destructive action ("Delete these 3 files?")
7
+ - Choosing between incompatible paths ("Keep their version or yours?")
8
+ - Gathering a required parameter that must be one of a known set
9
+
10
+ **When NOT to use:**
11
+ - Open-ended text input — just send a regular message asking.
12
+ - Yes/no confirmations where "no" is the safe default — just proceed and let the user interrupt.
13
+ - Anything you can work out from context.
14
+
15
+ **Arguments:**
16
+ - `title` (string) — short card header, e.g. "Confirm deletion"
17
+ - `question` (string) — the full question
18
+ - `options` (array) — each is either a plain string or `{ label, selectedLabel?, value? }`. `selectedLabel` replaces the button text after click; `value` is what gets returned to you
19
+ - `timeout` (number, seconds, default 300) — how long to wait before giving up
20
+
21
+ The response is the `value` (or label if no value set) of whichever option the user chose. On timeout you get an error and should proceed with a sensible default or tell the user you timed out.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Interactive module — generic ask_user_question flow.
3
+ *
4
+ * Container-side `ask_user_question` writes a chat-sdk card to outbound.db +
5
+ * polls inbound.db for a `question_response` system message. On the host side
6
+ * this module handles the button-click response: look up the `approvals` row
7
+ * (kind='question'), write the response into the session's inbound.db, wake
8
+ * the container.
9
+ *
10
+ * The `createApproval` call in `deliverMessage` (delivery.ts) stays inline in
11
+ * core — it's 15 lines guarded by `hasTable('approvals')`, modularizing it
12
+ * adds more registry surface than it saves.
13
+ */
14
+ import { getDb, hasTable } from '../../db/connection.js';
15
+ import { deleteApproval, getApproval, getSession } from '../../db/sessions.js';
16
+ import { wakeContainer } from '../../container-runner.js';
17
+ import { registerResponseHandler, type ResponsePayload } from '../../response-registry.js';
18
+ import { log } from '../../log.js';
19
+ import { writeSessionMessage } from '../../session-manager.js';
20
+ import type { QuestionApprovalBody } from '../../types.js';
21
+
22
+ async function handleInteractiveResponse(payload: ResponsePayload): Promise<boolean> {
23
+ if (!hasTable(getDb(), 'approvals')) return false;
24
+
25
+ const approval = getApproval(payload.questionId);
26
+ if (!approval || approval.kind !== 'question') return false;
27
+
28
+ if (!approval.session_id) {
29
+ deleteApproval(payload.questionId);
30
+ return true;
31
+ }
32
+ const session = getSession(approval.session_id);
33
+ if (!session) {
34
+ log.warn('Session not found for pending question', {
35
+ questionId: payload.questionId,
36
+ sessionId: approval.session_id,
37
+ });
38
+ deleteApproval(payload.questionId);
39
+ return true; // claimed — we owned this questionId even though the session is gone
40
+ }
41
+
42
+ const body = approval.body as QuestionApprovalBody;
43
+ writeSessionMessage(session.agent_group_id, session.id, {
44
+ id: `qr-${payload.questionId}-${Date.now()}`,
45
+ kind: 'system',
46
+ timestamp: new Date().toISOString(),
47
+ platformId: body.platform_id,
48
+ channelType: body.channel_type,
49
+ threadId: body.thread_id,
50
+ content: JSON.stringify({
51
+ type: 'question_response',
52
+ questionId: payload.questionId,
53
+ selectedOption: payload.value,
54
+ userId: payload.userId ?? '',
55
+ }),
56
+ });
57
+
58
+ deleteApproval(payload.questionId);
59
+ log.info('Question response routed', {
60
+ questionId: payload.questionId,
61
+ selectedOption: payload.value,
62
+ sessionId: session.id,
63
+ });
64
+
65
+ await wakeContainer(session);
66
+ return true;
67
+ }
68
+
69
+ registerResponseHandler(handleInteractiveResponse);
@@ -0,0 +1,12 @@
1
+ ## Interactive module
2
+
3
+ Generic ask_user_question flow. Lives in `src/modules/interactive/`.
4
+
5
+ The container-side MCP tool `ask_user_question` writes a chat-sdk card to outbound.db and polls inbound.db for a `question_response` system message. The host side of this is split:
6
+
7
+ - **Inline in `src/delivery.ts`:** the `deliverMessage` path intercepts `content.type === 'ask_question'` messages and writes a row to the unified `approvals` table with `kind='question'`. Guarded by `hasTable(db, 'approvals')`.
8
+ - **This module:** registers a `ResponseHandler` that runs when a button-click arrives via the channel adapter's `onAction`. It looks up the `approvals` row (filtered to `kind='question'`), writes a `question_response` system message into the session's inbound.db, wakes the container.
9
+
10
+ The `approvals` table is created by migration 024 (paraclaw#11), which collapsed the previous `pending_questions` and `pending_approvals` tables into one. The module doesn't own the schema, just the behavior. Removing the module disables the button-click response path for questions only; admin approvals (other kinds) still flow through `src/modules/approvals/`, and cards are still delivered.
11
+
12
+ `getAskQuestionRender` in `src/db/sessions.ts` resolves card render metadata for `chat-sdk-bridge.ts`. It reads from `approvals` and from the permissions module's side tables (`pending_channel_approvals`, `pending_sender_approvals`), degrading via `hasTable`. Stays in core.