@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,613 @@
1
+ /**
2
+ * `/agent/apps` — OAuth integrations management.
3
+ *
4
+ * Two stacked sections per the audit-refined brief:
5
+ * 1. **App configs** — per-provider OAuth client (paste client_id + client_secret).
6
+ * One row per *supported* provider, regardless of whether it's been
7
+ * configured yet. The CTA flips between "Add" and "Replace secret".
8
+ * 2. **Connections** — the user grants. Each row shows account_email +
9
+ * label + scopes_granted + status + agentGroupCount + delete.
10
+ *
11
+ * Add-flow: clicking "Connect with X" on a provider that has no app_config
12
+ * yet routes through the App-config form first; once saved, the same button
13
+ * proceeds to authorize.
14
+ *
15
+ * Callback handling: the server's OAuth callback redirects back to
16
+ * `?connected=<id>`. We read that on mount, refetch, and highlight the
17
+ * matching row briefly. The query param is then stripped from the URL so a
18
+ * page refresh doesn't re-trigger the highlight.
19
+ */
20
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
21
+ import { useSearchParams } from 'react-router-dom';
22
+
23
+ import { formatRelative } from '../components/StatusDot.tsx';
24
+ import {
25
+ type AppConfigView,
26
+ type AppConnectionView,
27
+ type PutAppConfigInput,
28
+ authorizeApp,
29
+ deleteAppConnection,
30
+ getAppConfig,
31
+ listAppConnections,
32
+ putAppConfig,
33
+ } from '../lib/api.ts';
34
+
35
+ /**
36
+ * Providers the UI knows about. Keep this list small — adding a provider is
37
+ * an explicit decision (the OAuth scopes, the userinfo shape, the icon, all
38
+ * vary). PR3 ships with Google as the seed; subsequent PRs add more.
39
+ */
40
+ const SUPPORTED_PROVIDERS: ProviderMeta[] = [
41
+ {
42
+ id: 'google',
43
+ label: 'Google',
44
+ description: 'Gmail, Calendar, Drive — depending on the scopes you grant.',
45
+ defaultScopes: ['openid', 'email', 'profile'],
46
+ docsUrl: 'https://console.cloud.google.com/apis/credentials',
47
+ },
48
+ ];
49
+
50
+ interface ProviderMeta {
51
+ id: string;
52
+ label: string;
53
+ description: string;
54
+ defaultScopes: string[];
55
+ /** Where the human goes to register an OAuth app for this provider. */
56
+ docsUrl: string;
57
+ }
58
+
59
+ export function Apps() {
60
+ const [searchParams, setSearchParams] = useSearchParams();
61
+ const justConnectedId = searchParams.get('connected');
62
+
63
+ // Configs are keyed by provider id; null = explicitly "no config yet"
64
+ // (404 from server), undefined = not yet loaded.
65
+ const [configs, setConfigs] = useState<Record<string, AppConfigView | null | undefined>>({});
66
+ const [connections, setConnections] = useState<AppConnectionView[] | null>(null);
67
+ const [error, setError] = useState<string | null>(null);
68
+ const [loading, setLoading] = useState(true);
69
+ const [flash, setFlash] = useState<{ kind: 'ok' | 'error'; text: string } | null>(null);
70
+ const [editingProvider, setEditingProvider] = useState<string | null>(null);
71
+ // Tracks the provider the user clicked Connect on when no config existed yet.
72
+ // After they save the config, we auto-resume the OAuth handoff for that
73
+ // provider so they don't have to re-click Connect.
74
+ const [pendingConnectProvider, setPendingConnectProvider] = useState<string | null>(null);
75
+
76
+ const reload = useCallback(async (signal: { cancelled: boolean }) => {
77
+ try {
78
+ const [conns, ...cfgs] = await Promise.all([
79
+ listAppConnections(),
80
+ ...SUPPORTED_PROVIDERS.map((p) => getAppConfig(p.id)),
81
+ ]);
82
+ if (signal.cancelled) return;
83
+ const cfgMap: Record<string, AppConfigView | null> = {};
84
+ SUPPORTED_PROVIDERS.forEach((p, i) => {
85
+ cfgMap[p.id] = cfgs[i];
86
+ });
87
+ setConfigs(cfgMap);
88
+ setConnections(conns);
89
+ setError(null);
90
+ } catch (err) {
91
+ if (signal.cancelled) return;
92
+ setError(err instanceof Error ? err.message : String(err));
93
+ } finally {
94
+ if (!signal.cancelled) setLoading(false);
95
+ }
96
+ }, []);
97
+
98
+ useEffect(() => {
99
+ const signal = { cancelled: false };
100
+ void reload(signal);
101
+ return () => {
102
+ signal.cancelled = true;
103
+ };
104
+ }, [reload]);
105
+
106
+ // Convenience for the Retry button — same call shape, throwaway signal.
107
+ const reloadNow = useCallback(() => {
108
+ void reload({ cancelled: false });
109
+ }, [reload]);
110
+
111
+ // Strip the `?connected=` param after we've consumed it for the highlight,
112
+ // so a refresh doesn't re-trigger the green flash. Kept replace:true to
113
+ // avoid leaving a redundant entry in the back-stack.
114
+ useEffect(() => {
115
+ if (!justConnectedId) return;
116
+ const t = setTimeout(() => {
117
+ setSearchParams(
118
+ (prev) => {
119
+ const p = new URLSearchParams(prev);
120
+ p.delete('connected');
121
+ return p;
122
+ },
123
+ { replace: true },
124
+ );
125
+ }, 4_000);
126
+ return () => clearTimeout(t);
127
+ }, [justConnectedId, setSearchParams]);
128
+
129
+ const authorizeAndRedirect = async (provider: string) => {
130
+ setFlash(null);
131
+ try {
132
+ const { redirectUrl } = await authorizeApp(provider);
133
+ window.location.href = redirectUrl;
134
+ } catch (err) {
135
+ setFlash({ kind: 'error', text: err instanceof Error ? err.message : String(err) });
136
+ }
137
+ };
138
+
139
+ const onConnect = async (provider: string) => {
140
+ const cfg = configs[provider];
141
+ if (!cfg) {
142
+ // No app_config yet — record the user's intent so we can auto-resume
143
+ // the OAuth handoff after they save the config (handled in onSaved).
144
+ setPendingConnectProvider(provider);
145
+ setEditingProvider(provider);
146
+ setFlash({ kind: 'ok', text: `Configure ${providerLabel(provider)} OAuth client first.` });
147
+ return;
148
+ }
149
+ await authorizeAndRedirect(provider);
150
+ };
151
+
152
+ const onDeleteConnection = async (conn: AppConnectionView) => {
153
+ if (
154
+ !window.confirm(
155
+ `Remove connection ${conn.label}? The agent groups it's assigned to (${conn.agentGroupCount}) will lose access immediately.`,
156
+ )
157
+ ) {
158
+ return;
159
+ }
160
+ setFlash(null);
161
+ try {
162
+ await deleteAppConnection(conn.id);
163
+ setFlash({ kind: 'ok', text: `Removed ${conn.label}.` });
164
+ reloadNow();
165
+ } catch (err) {
166
+ setFlash({ kind: 'error', text: err instanceof Error ? err.message : String(err) });
167
+ }
168
+ };
169
+
170
+ if (loading && connections === null) {
171
+ return (
172
+ <div>
173
+ <h2>Apps</h2>
174
+ <div className="section">
175
+ <div className="skeleton skeleton-line" style={{ width: '40%' }} />
176
+ <div className="skeleton skeleton-line" />
177
+ <div className="skeleton skeleton-line" style={{ width: '70%' }} />
178
+ </div>
179
+ </div>
180
+ );
181
+ }
182
+
183
+ return (
184
+ <div>
185
+ <h2>Apps</h2>
186
+ <p className="muted" style={{ marginTop: '-0.5rem' }}>
187
+ OAuth integrations — configure a provider once, then grant the agent access via "Connect".
188
+ </p>
189
+
190
+ {error && (
191
+ <div className="error-banner">
192
+ {error}
193
+ <button className="secondary" onClick={reloadNow} style={{ marginLeft: '0.6rem' }}>
194
+ Retry
195
+ </button>
196
+ </div>
197
+ )}
198
+ {flash && <div className={flash.kind === 'ok' ? 'status-banner' : 'error-banner'}>{flash.text}</div>}
199
+
200
+ <div className="section">
201
+ <h3>App configs</h3>
202
+ <p className="muted" style={{ marginTop: 0 }}>
203
+ One OAuth client per provider, shared across all your agent groups. Paste the client_id and
204
+ client_secret you registered with the provider.
205
+ </p>
206
+ <ul className="app-config-list">
207
+ {SUPPORTED_PROVIDERS.map((provider) => (
208
+ <AppConfigRow
209
+ key={provider.id}
210
+ provider={provider}
211
+ config={configs[provider.id] ?? null}
212
+ loading={configs[provider.id] === undefined}
213
+ editing={editingProvider === provider.id}
214
+ onEdit={() => setEditingProvider(provider.id)}
215
+ onCancel={() => {
216
+ setEditingProvider(null);
217
+ setPendingConnectProvider(null);
218
+ }}
219
+ onSaved={async (saved) => {
220
+ setConfigs((prev) => ({ ...prev, [provider.id]: saved }));
221
+ setEditingProvider(null);
222
+ if (pendingConnectProvider === provider.id && saved.hasSecret) {
223
+ // The user clicked Connect first; resume the OAuth handoff
224
+ // automatically now that the config is in place.
225
+ setPendingConnectProvider(null);
226
+ await authorizeAndRedirect(provider.id);
227
+ } else {
228
+ setPendingConnectProvider(null);
229
+ setFlash({ kind: 'ok', text: `${provider.label} OAuth client saved.` });
230
+ }
231
+ }}
232
+ />
233
+ ))}
234
+ </ul>
235
+ </div>
236
+
237
+ <div className="section">
238
+ <h3>Connections</h3>
239
+ {connections && connections.length > 0 ? (
240
+ <ConnectionsTable
241
+ connections={connections}
242
+ justConnectedId={justConnectedId}
243
+ onConnect={onConnect}
244
+ onDelete={onDeleteConnection}
245
+ configs={configs}
246
+ />
247
+ ) : (
248
+ <ConnectionsEmpty
249
+ providers={SUPPORTED_PROVIDERS}
250
+ configs={configs}
251
+ onConnect={onConnect}
252
+ />
253
+ )}
254
+ </div>
255
+ </div>
256
+ );
257
+ }
258
+
259
+ // --- App config row ---
260
+
261
+ function AppConfigRow({
262
+ provider,
263
+ config,
264
+ loading,
265
+ editing,
266
+ onEdit,
267
+ onCancel,
268
+ onSaved,
269
+ }: {
270
+ provider: ProviderMeta;
271
+ config: AppConfigView | null;
272
+ loading: boolean;
273
+ editing: boolean;
274
+ onEdit: () => void;
275
+ onCancel: () => void;
276
+ onSaved: (saved: AppConfigView) => Promise<void>;
277
+ }) {
278
+ if (editing) {
279
+ return (
280
+ <li className="app-config-row app-config-row-editing">
281
+ <AppConfigForm
282
+ provider={provider}
283
+ existing={config}
284
+ onCancel={onCancel}
285
+ onSaved={onSaved}
286
+ />
287
+ </li>
288
+ );
289
+ }
290
+ return (
291
+ <li className="app-config-row">
292
+ <div className="app-config-head">
293
+ <div>
294
+ <strong>{provider.label}</strong>
295
+ {loading ? (
296
+ <span className="dim"> · loading…</span>
297
+ ) : config ? (
298
+ <span className="tag" style={{ marginLeft: '0.5rem' }}>
299
+ configured
300
+ </span>
301
+ ) : (
302
+ <span className="tag muted" style={{ marginLeft: '0.5rem' }}>
303
+ not configured
304
+ </span>
305
+ )}
306
+ </div>
307
+ <button className="secondary" onClick={onEdit} disabled={loading}>
308
+ {config ? 'Replace secret' : 'Add config'}
309
+ </button>
310
+ </div>
311
+ <p className="dim app-config-blurb">{provider.description}</p>
312
+ {config && (
313
+ <div className="kv app-config-kv">
314
+ <div>client_id</div>
315
+ <div>
316
+ <code className="app-config-clientid">{config.client_id}</code>
317
+ </div>
318
+ <div>scopes</div>
319
+ <div>
320
+ {config.scopes_default.length > 0
321
+ ? config.scopes_default.map((s) => (
322
+ <code key={s} className="app-scope-tag">
323
+ {s}
324
+ </code>
325
+ ))
326
+ : <em className="dim">none</em>}
327
+ </div>
328
+ <div>secret</div>
329
+ <div>
330
+ {config.hasSecret ? (
331
+ <span className="tag muted">stored</span>
332
+ ) : (
333
+ <span className="tag warn">missing — add a secret to enable Connect</span>
334
+ )}
335
+ </div>
336
+ </div>
337
+ )}
338
+ </li>
339
+ );
340
+ }
341
+
342
+ function AppConfigForm({
343
+ provider,
344
+ existing,
345
+ onCancel,
346
+ onSaved,
347
+ }: {
348
+ provider: ProviderMeta;
349
+ existing: AppConfigView | null;
350
+ onCancel: () => void;
351
+ onSaved: (saved: AppConfigView) => Promise<void>;
352
+ }) {
353
+ const [clientId, setClientId] = useState(existing?.client_id ?? '');
354
+ const [clientSecret, setClientSecret] = useState('');
355
+ const [scopesText, setScopesText] = useState(
356
+ (existing?.scopes_default ?? provider.defaultScopes).join(' '),
357
+ );
358
+ const [submitting, setSubmitting] = useState(false);
359
+ const [err, setErr] = useState<string | null>(null);
360
+
361
+ const onSubmit = async (e: React.FormEvent) => {
362
+ e.preventDefault();
363
+ if (!clientId.trim() || !clientSecret.trim()) {
364
+ setErr('Both client_id and client_secret are required.');
365
+ return;
366
+ }
367
+ setSubmitting(true);
368
+ setErr(null);
369
+ try {
370
+ const input: PutAppConfigInput = {
371
+ client_id: clientId.trim(),
372
+ client_secret: clientSecret,
373
+ scopes_default: scopesText.split(/\s+/).filter(Boolean),
374
+ };
375
+ const saved = await putAppConfig(provider.id, input);
376
+ await onSaved(saved);
377
+ } catch (e2) {
378
+ setErr(e2 instanceof Error ? e2.message : String(e2));
379
+ } finally {
380
+ setSubmitting(false);
381
+ }
382
+ };
383
+
384
+ return (
385
+ <form onSubmit={onSubmit} className="app-config-form">
386
+ <div className="app-config-head">
387
+ <strong>{provider.label} OAuth client</strong>
388
+ <a href={provider.docsUrl} target="_blank" rel="noreferrer" className="dim">
389
+ Where do I get this? ↗
390
+ </a>
391
+ </div>
392
+ {err && <div className="error-banner">{err}</div>}
393
+ <div className="row">
394
+ <label htmlFor={`cid-${provider.id}`}>client_id</label>
395
+ <input
396
+ id={`cid-${provider.id}`}
397
+ type="text"
398
+ value={clientId}
399
+ onChange={(e) => setClientId(e.target.value)}
400
+ disabled={submitting}
401
+ autoComplete="off"
402
+ />
403
+ </div>
404
+ <div className="row">
405
+ <label htmlFor={`cs-${provider.id}`}>
406
+ client_secret {existing?.hasSecret && <span className="dim">(replacing existing)</span>}
407
+ </label>
408
+ <input
409
+ id={`cs-${provider.id}`}
410
+ type="password"
411
+ value={clientSecret}
412
+ onChange={(e) => setClientSecret(e.target.value)}
413
+ disabled={submitting}
414
+ autoComplete="new-password"
415
+ placeholder={existing?.hasSecret ? 'paste new secret to rotate' : 'paste from provider'}
416
+ />
417
+ </div>
418
+ <div className="row">
419
+ <label htmlFor={`scopes-${provider.id}`}>default scopes</label>
420
+ <input
421
+ id={`scopes-${provider.id}`}
422
+ type="text"
423
+ value={scopesText}
424
+ onChange={(e) => setScopesText(e.target.value)}
425
+ disabled={submitting}
426
+ placeholder={provider.defaultScopes.join(' ')}
427
+ />
428
+ <p className="dim">Space-separated. Each Connect can later add more, but never less.</p>
429
+ </div>
430
+ <div className="actions">
431
+ <button type="submit" disabled={submitting}>
432
+ {submitting ? 'Saving…' : 'Save'}
433
+ </button>
434
+ <button type="button" className="secondary" onClick={onCancel} disabled={submitting}>
435
+ Cancel
436
+ </button>
437
+ </div>
438
+ </form>
439
+ );
440
+ }
441
+
442
+ // --- Connections ---
443
+
444
+ function ConnectionsTable({
445
+ connections,
446
+ justConnectedId,
447
+ onConnect,
448
+ onDelete,
449
+ configs,
450
+ }: {
451
+ connections: AppConnectionView[];
452
+ justConnectedId: string | null;
453
+ onConnect: (provider: string) => void | Promise<void>;
454
+ onDelete: (conn: AppConnectionView) => void | Promise<void>;
455
+ configs: Record<string, AppConfigView | null | undefined>;
456
+ }) {
457
+ // Group by provider so each block has a "+ Connect another <provider>"
458
+ // row at the bottom — UX is clearer than a flat list when the user has
459
+ // multiple Google accounts (which is the common case for power users).
460
+ const byProvider = useMemo(() => {
461
+ const m = new Map<string, AppConnectionView[]>();
462
+ for (const c of connections) {
463
+ const arr = m.get(c.provider) ?? [];
464
+ arr.push(c);
465
+ m.set(c.provider, arr);
466
+ }
467
+ return m;
468
+ }, [connections]);
469
+
470
+ return (
471
+ <div className="connections-list">
472
+ {Array.from(byProvider.entries()).map(([provider, items]) => {
473
+ const cfg = configs[provider];
474
+ const canConnect = cfg && cfg.hasSecret;
475
+ return (
476
+ <div key={provider} className="connections-group">
477
+ <div className="connections-group-head">
478
+ <strong>{providerLabel(provider)}</strong>
479
+ <button
480
+ className="secondary"
481
+ onClick={() => void onConnect(provider)}
482
+ disabled={!canConnect}
483
+ title={canConnect ? undefined : 'Add a config + secret first'}
484
+ >
485
+ + Connect another
486
+ </button>
487
+ </div>
488
+ <ul className="connections-rows">
489
+ {items.map((conn) => (
490
+ <ConnectionRow
491
+ key={conn.id}
492
+ conn={conn}
493
+ highlighted={conn.id === justConnectedId}
494
+ onDelete={() => void onDelete(conn)}
495
+ />
496
+ ))}
497
+ </ul>
498
+ </div>
499
+ );
500
+ })}
501
+ </div>
502
+ );
503
+ }
504
+
505
+ function ConnectionRow({
506
+ conn,
507
+ highlighted,
508
+ onDelete,
509
+ }: {
510
+ conn: AppConnectionView;
511
+ highlighted: boolean;
512
+ onDelete: () => void;
513
+ }) {
514
+ // Briefly scroll the highlighted row into view so the user lands on it
515
+ // after the OAuth round-trip without having to hunt.
516
+ const ref = useRef<HTMLLIElement | null>(null);
517
+ useEffect(() => {
518
+ if (highlighted && ref.current) {
519
+ ref.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
520
+ }
521
+ }, [highlighted]);
522
+
523
+ return (
524
+ <li ref={ref} className={`connection-row${highlighted ? ' connection-row-flash' : ''}`}>
525
+ <div className="connection-row-main">
526
+ <div className="connection-label">
527
+ {conn.label}
528
+ <ConnectionStatusTag status={conn.status} />
529
+ </div>
530
+ <div className="dim connection-meta">
531
+ {conn.account_email && <span>{conn.account_email}</span>}
532
+ {conn.account_email && <span> · </span>}
533
+ {conn.expires_at ? (
534
+ <span title={new Date(conn.expires_at).toLocaleString()}>
535
+ expires {formatRelative(conn.expires_at)}
536
+ </span>
537
+ ) : (
538
+ <span>no expiry</span>
539
+ )}
540
+ <span> · </span>
541
+ <span>
542
+ {conn.agentGroupCount === 0
543
+ ? 'unassigned'
544
+ : `assigned to ${conn.agentGroupCount} agent${conn.agentGroupCount === 1 ? '' : 's'}`}
545
+ </span>
546
+ </div>
547
+ {conn.scopes_granted.length > 0 && (
548
+ <div className="connection-scopes">
549
+ {conn.scopes_granted.map((s) => (
550
+ <code key={s} className="app-scope-tag">
551
+ {s}
552
+ </code>
553
+ ))}
554
+ </div>
555
+ )}
556
+ </div>
557
+ <button className="danger" onClick={onDelete}>
558
+ Remove
559
+ </button>
560
+ </li>
561
+ );
562
+ }
563
+
564
+ function ConnectionStatusTag({ status }: { status: AppConnectionView['status'] }) {
565
+ switch (status) {
566
+ case 'active':
567
+ return <span className="tag" style={{ marginLeft: '0.5rem' }}>active</span>;
568
+ case 'expired':
569
+ return <span className="tag warn" style={{ marginLeft: '0.5rem' }}>expired</span>;
570
+ case 'revoked':
571
+ return <span className="tag error" style={{ marginLeft: '0.5rem' }}>revoked</span>;
572
+ }
573
+ }
574
+
575
+ function ConnectionsEmpty({
576
+ providers,
577
+ configs,
578
+ onConnect,
579
+ }: {
580
+ providers: ProviderMeta[];
581
+ configs: Record<string, AppConfigView | null | undefined>;
582
+ onConnect: (provider: string) => void | Promise<void>;
583
+ }) {
584
+ return (
585
+ <div className="empty-rich">
586
+ <p className="empty-headline">No connections yet.</p>
587
+ <p className="muted" style={{ marginTop: 0 }}>
588
+ Configure a provider above, then click Connect to grant the agent access via OAuth.
589
+ </p>
590
+ <div className="connect-cta-row">
591
+ {providers.map((p) => {
592
+ const cfg = configs[p.id];
593
+ const ready = cfg && cfg.hasSecret;
594
+ return (
595
+ <button
596
+ key={p.id}
597
+ onClick={() => void onConnect(p.id)}
598
+ disabled={!ready}
599
+ title={ready ? undefined : 'Add this provider in App configs first'}
600
+ >
601
+ Connect with {p.label}
602
+ </button>
603
+ );
604
+ })}
605
+ </div>
606
+ </div>
607
+ );
608
+ }
609
+
610
+ function providerLabel(id: string): string {
611
+ const known = SUPPORTED_PROVIDERS.find((p) => p.id === id);
612
+ return known?.label ?? id;
613
+ }