mixdog 1.4.1 → 1.4.2

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 (326) hide show
  1. package/package.json +3 -2
  2. package/scripts/build-shipped-history.mjs +115 -33
  3. package/scripts/gate-local.mjs +172 -61
  4. package/scripts/lib/gate-scheduler.mjs +138 -32
  5. package/scripts/lib/gate-timing-reporter.mjs +16 -0
  6. package/scripts/lib/run-node-tests.mjs +54 -29
  7. package/scripts/lib/scoped-temp.mjs +1 -0
  8. package/scripts/lib/test-failure-records.mjs +14 -1
  9. package/scripts/lib/test-failure-summary.mjs +16 -4
  10. package/scripts/lib/test-spawn-ledger.mjs +12 -3
  11. package/scripts/pptx-kit-index.mjs +5 -6
  12. package/scripts/suite-health.mjs +110 -28
  13. package/scripts/test.mjs +9 -14
  14. package/src/agents/advisor/AGENT.md +4 -4
  15. package/src/agents/advisor/agent.json +1 -1
  16. package/src/agents/explore/AGENT.md +22 -0
  17. package/src/agents/explore/agent.json +6 -0
  18. package/src/cli.mjs +7 -0
  19. package/src/defaults/agents.json +7 -0
  20. package/src/defaults/shipped-history.json +18 -2
  21. package/src/defaults/skills/browser-use/SKILL.md +4 -1
  22. package/src/defaults/skills/code-tidy/SKILL.md +71 -102
  23. package/src/defaults/skills/code-tidy/references/agent-cleanup.md +66 -114
  24. package/src/defaults/skills/code-tidy/references/cleanup-report.md +19 -35
  25. package/src/defaults/skills/code-tidy/references/dead-code.md +3 -9
  26. package/src/defaults/skills/code-tidy/references/test-suite-slop.md +16 -27
  27. package/src/defaults/skills/computer-use/SKILL.md +12 -2
  28. package/src/defaults/skills/goal-management/SKILL.md +1 -1
  29. package/src/defaults/skills/setup/references/actions.md +12 -6
  30. package/src/defaults/skills/setup/references/surfaces.md +2 -2
  31. package/src/defaults/skills/setup-guide/SKILL.md +5 -2
  32. package/src/defaults/skills/setup-guide/model-recommendations.json +13 -0
  33. package/src/defaults/skills/skill-creator/SKILL.md +85 -190
  34. package/src/defaults/skills/skill-creator/references/design.md +70 -162
  35. package/src/defaults/skills/skill-creator/references/evaluation.md +33 -92
  36. package/src/defaults/skills/test-suite/SKILL.md +194 -0
  37. package/src/defaults/skills/test-suite/references/case-management.md +51 -0
  38. package/src/defaults/skills/test-suite/references/flaky-policy.md +39 -0
  39. package/src/defaults/skills/test-suite/references/reliability.md +49 -0
  40. package/src/defaults/skills/test-suite/references/suite-design.md +78 -0
  41. package/src/defaults/skills/test-suite/scripts/inventory.mjs +335 -0
  42. package/src/defaults/skills/visualize/SKILL.md +6 -2
  43. package/src/lib/rules-builder.cjs +6 -30
  44. package/src/rules/lead/LEAD.md +3 -3
  45. package/src/rules/shared/20-research.md +3 -6
  46. package/src/rules/shared/40-editing.md +1 -0
  47. package/src/rules/shared/60-verification.md +10 -9
  48. package/src/rules/shared/70-delivery.md +4 -3
  49. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/abort-link.mjs +5 -1
  50. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +10 -1
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +130 -21
  52. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +13 -0
  53. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +4 -1
  54. package/src/runtime/agent/orchestrator/config.mjs +12 -7
  55. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +7 -7
  56. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +5 -26
  57. package/src/runtime/agent/orchestrator/internal-agents.mjs +3 -14
  58. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +122 -12
  59. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -3
  60. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +13 -2
  61. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +152 -58
  62. package/src/runtime/agent/orchestrator/mcp/client.mjs +51 -18
  63. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +83 -52
  64. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +42 -26
  65. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +43 -8
  66. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +30 -28
  67. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +31 -21
  69. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +3 -1
  70. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -2
  71. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  72. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +1 -1
  73. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +40 -0
  74. package/src/runtime/agent/orchestrator/providers/custom-provider-keys.mjs +34 -0
  75. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +3 -0
  76. package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +14 -7
  77. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +3 -2
  78. package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +2 -2
  79. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +4 -1
  80. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +48 -31
  81. package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +0 -18
  82. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +138 -6
  83. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +1 -1
  84. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +100 -33
  85. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/providers/oauth-secret-fields.fixtures.mjs +45 -0
  87. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +26 -28
  88. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +3 -1
  89. package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +1 -0
  90. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +3 -1
  92. package/src/runtime/agent/orchestrator/providers/registry.mjs +33 -2
  93. package/src/runtime/agent/orchestrator/providers/vertex-ai-auth.mjs +622 -0
  94. package/src/runtime/agent/orchestrator/providers/vertex-ai-maas.mjs +185 -0
  95. package/src/runtime/agent/orchestrator/providers/vertex-ai-models.mjs +445 -0
  96. package/src/runtime/agent/orchestrator/providers/vertex-ai.mjs +567 -0
  97. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
  98. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +2 -1
  99. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  100. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -0
  101. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +14 -5
  102. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -0
  103. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +30 -5
  104. package/src/runtime/agent/orchestrator/session/agent-room.mjs +171 -37
  105. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +94 -10
  106. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +3 -3
  107. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +10 -5
  108. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +5 -5
  109. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +11 -1
  110. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -3
  111. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -6
  112. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +5 -3
  113. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +21 -2
  114. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +2 -0
  115. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +23 -1
  116. package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +13 -8
  117. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +50 -28
  118. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +1 -0
  119. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +3 -4
  120. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +2 -0
  121. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +7 -0
  122. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +15 -0
  123. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +25 -14
  124. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +17 -14
  125. package/src/runtime/agent/orchestrator/session/manager/session-scratch-environment.mjs +70 -0
  126. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +0 -3
  127. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -3
  128. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +47 -29
  129. package/src/runtime/agent/orchestrator/session/manager/turn-reap.mjs +125 -0
  130. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +11 -5
  131. package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +1 -1
  132. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +243 -38
  133. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +163 -64
  134. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +164 -4
  135. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +142 -18
  136. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +6 -2
  137. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +36 -26
  138. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +3 -5
  139. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +58 -20
  140. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +20 -15
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +96 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +59 -10
  143. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  144. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +99 -2
  145. package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +2 -0
  146. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +103 -13
  147. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +3 -1
  148. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +49 -15
  149. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +45 -27
  150. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +34 -10
  151. package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +2 -0
  152. package/src/runtime/agent/orchestrator/tools/lib/wait-until-fixture.mjs +26 -0
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -2
  154. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
  155. package/src/runtime/channels/lib/whisper-port.mjs +19 -10
  156. package/src/runtime/channels/lib/whisper-server.mjs +10 -1
  157. package/src/runtime/connectors/connector-accounts-mcp.mjs +236 -103
  158. package/src/runtime/connectors/gmail-app-password.mjs +3 -3
  159. package/src/runtime/explore/explorer.mjs +93 -0
  160. package/src/runtime/explore/tool-defs.mjs +35 -0
  161. package/src/runtime/explore/tool.mjs +123 -0
  162. package/src/runtime/memory/index.mjs +4 -0
  163. package/src/runtime/memory/lib/embedding-provider.mjs +4 -1
  164. package/src/runtime/memory/lib/embedding-worker.mjs +4 -0
  165. package/src/runtime/office/quality/assurance-structure-docx.mjs +1 -1
  166. package/src/runtime/shared/agent-route-config.mjs +17 -2
  167. package/src/runtime/shared/atomic-file.mjs +103 -2
  168. package/src/runtime/shared/background-tasks.mjs +15 -6
  169. package/src/runtime/shared/connector-accounts.mjs +5 -5
  170. package/src/runtime/shared/connector-catalog.d.mts +11 -4
  171. package/src/runtime/shared/connector-catalog.mjs +60 -52
  172. package/src/runtime/shared/custom-provider-keys.mjs +20 -0
  173. package/src/runtime/shared/file-has-data.mjs +26 -2
  174. package/src/runtime/shared/file-lock.mjs +13 -15
  175. package/src/runtime/shared/git-launch.d.mts +9 -0
  176. package/src/runtime/shared/git-launch.mjs +213 -0
  177. package/src/runtime/shared/git-runner-worker.mjs +2 -1
  178. package/src/runtime/shared/llm/cost.mjs +6 -1
  179. package/src/runtime/shared/llm/http-agent.mjs +313 -29
  180. package/src/runtime/shared/llm/model-catalog-projection.mjs +3 -2
  181. package/src/runtime/shared/llm/model-catalog.mjs +46 -6
  182. package/src/runtime/shared/llm/provider-catalog-registry.mjs +3 -0
  183. package/src/runtime/shared/llm/system-proxy.mjs +452 -0
  184. package/src/runtime/shared/provider-accounts.mjs +52 -2
  185. package/src/runtime/shared/staged-install-worker.mjs +5 -0
  186. package/src/runtime/shared/staged-update.mjs +47 -2
  187. package/src/runtime/shared/tool-surface.mjs +110 -36
  188. package/src/runtime/shared/tool-work-units.mjs +4 -0
  189. package/src/runtime/shared/turn-worktree-snapshot.mjs +414 -54
  190. package/src/runtime/shared/worktrees/activity-gate.mjs +215 -318
  191. package/src/runtime/shared/worktrees/activity-ledger-worker.mjs +56 -0
  192. package/src/runtime/shared/worktrees/activity-ledger.mjs +391 -0
  193. package/src/runtime/shared/worktrees/branch-management.mjs +9 -3
  194. package/src/runtime/shared/worktrees/git.mjs +57 -0
  195. package/src/runtime/shared/worktrees/manager.mjs +205 -137
  196. package/src/runtime/tidy/install.mjs +95 -15
  197. package/src/runtime/tidy/languages.mjs +17 -15
  198. package/src/runtime/tidy/process.mjs +7 -4
  199. package/src/runtime/tidy/resolve.mjs +19 -16
  200. package/src/runtime/tidy/runners/biome.mjs +3 -3
  201. package/src/runtime/tidy/tool.mjs +1 -1
  202. package/src/runtime/tidy/worktree.mjs +10 -6
  203. package/src/runtime/visualize-bridge/client.mjs +4 -1
  204. package/src/runtime/visualize-bridge/tool-defs.mjs +7 -2
  205. package/src/runtime/web-search/index.mjs +1 -1
  206. package/src/session-runtime/agent-task-sessions.mjs +410 -353
  207. package/src/session-runtime/boot/tools.mjs +2 -0
  208. package/src/session-runtime/bot-status.mjs +103 -103
  209. package/src/session-runtime/connector-merge-migration.mjs +18 -15
  210. package/src/session-runtime/conversation-import/discovery.mjs +159 -40
  211. package/src/session-runtime/conversation-import/service.mjs +9 -3
  212. package/src/session-runtime/handoff-attachments.mjs +1 -1
  213. package/src/session-runtime/held-requests.mjs +3 -1
  214. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -0
  215. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +8 -0
  216. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +143 -116
  217. package/src/session-runtime/lifecycle/teardown/session-close.mjs +6 -1
  218. package/src/session-runtime/lifecycle/teardown.mjs +1 -1
  219. package/src/session-runtime/mcp-glue/server-catalog.mjs +16 -10
  220. package/src/session-runtime/mcp-glue/server-input.mjs +4 -1
  221. package/src/session-runtime/mixy-heartbeat.mjs +1 -1
  222. package/src/session-runtime/provider-auth/login.mjs +2 -2
  223. package/src/session-runtime/resource-mcp-api.mjs +98 -35
  224. package/src/session-runtime/resource-mcp-connector-accounts.mjs +44 -31
  225. package/src/session-runtime/resource-mcp-secrets.mjs +28 -11
  226. package/src/session-runtime/resource-mcp-servers.mjs +5 -4
  227. package/src/session-runtime/services/agent-tool/send-flow.mjs +10 -2
  228. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +8 -3
  229. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
  230. package/src/session-runtime/services/agent-watchdog-registry.mjs +32 -4
  231. package/src/session-runtime/services/channel-worker/daemon-spawn.mjs +2 -0
  232. package/src/session-runtime/services/daemon-candidate.mjs +4 -1
  233. package/src/session-runtime/services/memory-runtime-proxy/daemon-spawn.mjs +4 -0
  234. package/src/session-runtime/services/plugin-admin.mjs +10 -6
  235. package/src/session-runtime/services/provider-admin.mjs +281 -10
  236. package/src/session-runtime/session-turn-api.mjs +2 -0
  237. package/src/session-runtime/settings-builtin-tools-api.mjs +4 -1
  238. package/src/session-runtime/settings-system-api.mjs +1 -1
  239. package/src/session-runtime/setup-tool/agent-settings-actions.mjs +13 -3
  240. package/src/session-runtime/setup-tool/connector-widen-approval.mjs +96 -0
  241. package/src/session-runtime/setup-tool/executor.mjs +6 -2
  242. package/src/session-runtime/setup-tool/tool-defs.mjs +2 -1
  243. package/src/session-runtime/turn/agent-ops.mjs +23 -0
  244. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +49 -11
  245. package/src/session-runtime/workflow-agents-api/agent-route.mjs +10 -1
  246. package/src/session-runtime/workspace/activity-guards.mjs +176 -0
  247. package/src/session-runtime/workspace/session-view.mjs +149 -0
  248. package/src/session-runtime/workspace/side-checkouts.mjs +203 -0
  249. package/src/session-runtime/workspace/transition-runner.mjs +188 -0
  250. package/src/session-runtime/workspace/transitions.mjs +108 -1066
  251. package/src/session-runtime/workspace/workspace-merge.mjs +132 -0
  252. package/src/session-runtime/workspace/worktree-intents.mjs +242 -0
  253. package/src/session-runtime/workspace/worktree-maintenance.mjs +80 -0
  254. package/src/session-runtime/workspace/worktree-setup.mjs +85 -0
  255. package/src/session-runtime/workspace/worktree-tool.mjs +54 -54
  256. package/src/standalone/daemon-desktop-runtime.mjs +2 -0
  257. package/src/standalone/daemon-session-control.mjs +151 -136
  258. package/src/standalone/daemon.mjs +7 -0
  259. package/src/standalone/desktop-service-registry.mjs +9 -0
  260. package/src/standalone/local-session-runtime.mjs +5 -1
  261. package/src/standalone/session-client/daemon-unavailable.mjs +3 -0
  262. package/src/standalone/session-client.mjs +5 -1
  263. package/src/standalone/session-frame-stream.mjs +83 -11
  264. package/src/standalone/session-protocol.mjs +4 -0
  265. package/src/standalone/session-service/agent-tree.mjs +45 -9
  266. package/src/standalone/session-service/projection/transcript-window.mjs +700 -64
  267. package/src/standalone/session-service/session-calls/session-views.mjs +160 -104
  268. package/src/standalone/session-service/stored-reader.mjs +127 -40
  269. package/src/tui/App.jsx +154 -164
  270. package/src/tui/app/app-view/floating-panel.jsx +3 -0
  271. package/src/tui/app/app-view/text-entry-labels.mjs +1 -0
  272. package/src/tui/app/clipboard.mjs +21 -10
  273. package/src/tui/app/prompt-handlers/use-prompt-history-nav.mjs +1 -1
  274. package/src/tui/app/prompt-handlers/use-prompt-interrupt.mjs +3 -0
  275. package/src/tui/app/prompt-submit/provider-prompts.mjs +3 -1
  276. package/src/tui/app/provider-setup/api-key-actions.mjs +31 -1
  277. package/src/tui/app/provider-setup/oauth-actions.mjs +25 -6
  278. package/src/tui/app/provider-setup-picker.mjs +20 -13
  279. package/src/tui/app/shell-layout/row-budgets.mjs +12 -10
  280. package/src/tui/app/slash-commands.mjs +1 -0
  281. package/src/tui/app/slash-dispatch/session-commands.mjs +46 -0
  282. package/src/tui/app/slash-dispatch.mjs +1 -1
  283. package/src/tui/app/use-btw-panel.mjs +29 -0
  284. package/src/tui/app/use-context-panel-refresh.mjs +31 -0
  285. package/src/tui/app/use-model-catalog-cache.mjs +13 -10
  286. package/src/tui/app/use-mouse-input.mjs +11 -0
  287. package/src/tui/app/use-onboarding-start.mjs +8 -3
  288. package/src/tui/app/use-panel-transition.mjs +10 -1
  289. package/src/tui/app/use-pasted-buffers.mjs +28 -20
  290. package/src/tui/app/use-prompt-draft-flow.mjs +7 -0
  291. package/src/tui/app/use-prompt-layout-rows.mjs +3 -3
  292. package/src/tui/app/use-prompt-queue-history.mjs +3 -2
  293. package/src/tui/app/use-selection-focus.mjs +11 -2
  294. package/src/tui/app/use-selection-geometry.mjs +18 -9
  295. package/src/tui/app/use-selection-paint.mjs +4 -4
  296. package/src/tui/app/use-selection-stitch.mjs +3 -3
  297. package/src/tui/app/use-slash-palette.mjs +62 -0
  298. package/src/tui/app/use-terminal-chrome.mjs +2 -2
  299. package/src/tui/app/use-transcript-activity.mjs +47 -32
  300. package/src/tui/app/use-transcript-follow.mjs +12 -4
  301. package/src/tui/app/use-transcript-scroll.mjs +46 -17
  302. package/src/tui/app/use-transcript-swap-reset.mjs +27 -0
  303. package/src/tui/app/use-transcript-window/geometry.mjs +19 -5
  304. package/src/tui/app/use-transcript-window/instance-state.mjs +4 -0
  305. package/src/tui/app/use-transcript-window/selection-shift.mjs +5 -1
  306. package/src/tui/app/use-transcript-window.mjs +21 -11
  307. package/src/tui/app/use-tui-ready.mjs +18 -0
  308. package/src/tui/app/use-welcome-prompt-hint.mjs +5 -0
  309. package/src/tui/app/use-workflow-tab-cycle.mjs +5 -3
  310. package/src/tui/components/Picker.jsx +27 -3
  311. package/src/tui/components/SignInUrl.jsx +68 -0
  312. package/src/tui/components/TextEntryPanel.jsx +76 -46
  313. package/src/tui/components/sign-in-url-layout.mjs +24 -0
  314. package/src/tui/dist/index.mjs +1896 -1341
  315. package/src/tui/session/goal-turn-state.mjs +6 -2
  316. package/src/tui/session/session-api/controls.mjs +4 -0
  317. package/src/tui/session/session-api/extensions.mjs +8 -0
  318. package/src/tui/session/session-api/intake/abort.mjs +2 -1
  319. package/src/tui/session/session-api/intake.mjs +2 -0
  320. package/src/tui/session/session-api/integrations.mjs +2 -2
  321. package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
  322. package/src/tui/session/session-api/settings.mjs +2 -2
  323. package/src/tui/session/transcript-spill/spill-writer.mjs +15 -0
  324. package/src/tui/session/transcript-spill.mjs +92 -4
  325. package/src/tui/session/transcript-store.mjs +2 -0
  326. package/src/defaults/skills/skill-creator/references/source-review.md +0 -94
@@ -0,0 +1,622 @@
1
+ import { createHash, createSign } from 'node:crypto';
2
+ import { readFileSync, statSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { googleErrorInfo, PERMISSION_TEXT, vertexHttpStatus } from './vertex-ai-models.mjs';
6
+
7
+ export const VERTEX_TOKEN_URI = 'https://oauth2.googleapis.com/token';
8
+ export const VERTEX_SCOPE = 'https://www.googleapis.com/auth/cloud-platform';
9
+ /** The `authMode` that authenticates with the gcloud login (Application Default Credentials). */
10
+ export const VERTEX_ADC_MODE = 'adc';
11
+ /** The `authMode` that sends a stored express-mode API key (Gemini models on the global endpoint only). */
12
+ export const VERTEX_API_KEY_MODE = 'api-key';
13
+ const IAM_CREDENTIALS_URI = 'https://iamcredentials.googleapis.com/v1';
14
+ // The GCE / Cloud Run / GKE metadata server: ADC's source when there is no credentials file.
15
+ const METADATA_URL = 'http://metadata.google.internal/computeMetadata/v1';
16
+ const METADATA_HEADERS = Object.freeze({ 'Metadata-Flavor': 'Google' });
17
+ // Short, so a desktop without one is barely delayed; the outcome is kept either way.
18
+ const METADATA_PROBE_MS = 1000;
19
+ const JWT_BEARER_GRANT = 'urn:ietf:params:oauth:grant-type:jwt-bearer';
20
+ const TOKEN_REFRESH_SKEW_MS = 5 * 60_000;
21
+ const ADC_FILE = 'application_default_credentials.json';
22
+ const ADC_LOGIN_HINT = 'Run `gcloud auth application-default login`.';
23
+ // Project sources ahead of the credential's own, in precedence order.
24
+ const PROJECT_ENV = Object.freeze([
25
+ 'GOOGLE_VERTEX_PROJECT',
26
+ 'GOOGLE_CLOUD_PROJECT',
27
+ 'GCP_PROJECT',
28
+ 'GCLOUD_PROJECT',
29
+ 'VERTEXAI_PROJECT',
30
+ 'ANTHROPIC_VERTEX_PROJECT_ID',
31
+ ]);
32
+ // A 403 asking for a quota project: the ErrorInfo reason USER_PROJECT_DENIED,
33
+ // or user credentials on gcloud's own OAuth client, whose projects
34
+ // (764086051850, 32555940559) are named as the ErrorInfo consumer.
35
+ const GCLOUD_CLIENT_CONSUMER = /^projects\/(?:764086051850|32555940559)$/;
36
+ // Google's message when ADC has no quota project; it has no ErrorInfo reason of its own.
37
+ const QUOTA_PROJECT_TEXT = 'requires a quota project';
38
+
39
+ function base64url(value) {
40
+ return Buffer.from(typeof value === 'string' ? value : JSON.stringify(value)).toString('base64url');
41
+ }
42
+
43
+ export function parseVertexServiceAccount(raw) {
44
+ let value;
45
+ try {
46
+ value = JSON.parse(String(raw || ''));
47
+ } catch {
48
+ throw new Error('Vertex AI service-account JSON is invalid.');
49
+ }
50
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
51
+ throw new Error('Vertex AI service-account JSON is invalid.');
52
+ }
53
+ const clientEmail = String(value.client_email || '').trim();
54
+ const privateKey = String(value.private_key || '').trim();
55
+ const projectId = String(value.project_id || '').trim();
56
+ const privateKeyId = String(value.private_key_id || '').trim();
57
+ if (!clientEmail || !privateKey || !projectId) {
58
+ throw new Error('Vertex AI service-account JSON must include client_email, private_key, and project_id.');
59
+ }
60
+ // The identity every minted access token is bound to and checked against.
61
+ // The key fingerprint does not depend on the optional private_key_id.
62
+ const keyFingerprint = createHash('sha256').update(privateKey).digest('hex');
63
+ const identity = JSON.stringify([clientEmail, privateKeyId, projectId, keyFingerprint]);
64
+ return {
65
+ kind: 'service_account',
66
+ clientEmail,
67
+ privateKey,
68
+ privateKeyId,
69
+ projectId,
70
+ identity,
71
+ tokenUri: VERTEX_TOKEN_URI,
72
+ };
73
+ }
74
+
75
+ export function createVertexJwtAssertion(account, nowMs = Date.now()) {
76
+ const issuedAt = Math.floor(nowMs / 1000);
77
+ const header = base64url({ alg: 'RS256', typ: 'JWT' });
78
+ const claims = base64url({
79
+ iss: account.clientEmail,
80
+ scope: VERTEX_SCOPE,
81
+ aud: account.tokenUri,
82
+ iat: issuedAt,
83
+ exp: issuedAt + 3600,
84
+ });
85
+ const input = `${header}.${claims}`;
86
+ const signer = createSign('RSA-SHA256');
87
+ signer.update(input);
88
+ signer.end();
89
+ return `${input}.${signer.sign(account.privateKey).toString('base64url')}`;
90
+ }
91
+
92
+ const text = (value) => (typeof value === 'string' ? value.trim() : '');
93
+ const fingerprint = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex');
94
+ // `reason` (missing | invalid | unsupported) is what the settings status line reports.
95
+ const adcError = (message, reason, extra = {}) => Object.assign(new Error(message), { reason, ...extra });
96
+ const authError = (message, extra = {}) =>
97
+ Object.assign(new Error(`Vertex AI authentication failed: ${message}`), { code: 'EVERTEXAUTH', ...extra });
98
+ const errorDetail = (payload, response) =>
99
+ String(
100
+ payload?.error_description ||
101
+ payload?.error?.message ||
102
+ (typeof payload?.error === 'string' ? payload.error : '') ||
103
+ `HTTP ${response.status}`
104
+ ).slice(0, 240);
105
+
106
+ /** gcloud's configuration directory: CLOUDSDK_CONFIG, else %APPDATA%\gcloud on Windows, else ~/.config/gcloud. */
107
+ export function gcloudConfigDir(env = process.env, platform = process.platform, home = homedir()) {
108
+ const explicit = text(env.CLOUDSDK_CONFIG);
109
+ if (explicit) return explicit;
110
+ if (platform === 'win32' && text(env.APPDATA)) return join(env.APPDATA, 'gcloud');
111
+ return join(home, '.config', 'gcloud');
112
+ }
113
+
114
+ // GOOGLE_APPLICATION_CREDENTIALS, when set, is the only source: its missing
115
+ // file is an error, never a fall-through to the gcloud login.
116
+ function adcLocation(env) {
117
+ const explicit = text(env.GOOGLE_APPLICATION_CREDENTIALS);
118
+ if (explicit) return { path: explicit, source: 'GOOGLE_APPLICATION_CREDENTIALS' };
119
+ return { path: join(gcloudConfigDir(env), ADC_FILE), source: 'gcloud' };
120
+ }
121
+
122
+ function adcFileError({ path, source }, error) {
123
+ if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') {
124
+ return adcError(`Vertex AI could not read ${path}: ${error?.code || error?.message || 'read failed'}.`, 'invalid');
125
+ }
126
+ if (source === 'gcloud') return adcError(`No gcloud login found. ${ADC_LOGIN_HINT}`, 'missing');
127
+ return adcError(`GOOGLE_APPLICATION_CREDENTIALS points to ${path}, which does not exist.`, 'invalid');
128
+ }
129
+
130
+ function readAdcFile(location) {
131
+ try {
132
+ return readFileSync(location.path, 'utf8');
133
+ } catch (error) {
134
+ throw adcFileError(location, error);
135
+ }
136
+ }
137
+
138
+ function requireFields(value, type, names, path) {
139
+ const missing = names.filter((name) => !text(value[name]));
140
+ if (missing.length)
141
+ throw adcError(`The ${type} credentials in ${path} are missing ${missing.join(', ')}.`, 'invalid');
142
+ }
143
+
144
+ // One grant of an ADC file. `nested`: an impersonation's source, which cannot impersonate again.
145
+ function adcGrant(value, path, nested = false) {
146
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
147
+ throw adcError(`The credentials in ${path} are invalid.`, 'invalid');
148
+ }
149
+ const type = text(value.type);
150
+ if (type === 'service_account') {
151
+ requireFields(value, type, ['client_email', 'private_key'], path);
152
+ const clientEmail = text(value.client_email);
153
+ const privateKey = text(value.private_key);
154
+ return {
155
+ kind: type,
156
+ account: clientEmail,
157
+ clientEmail,
158
+ privateKey,
159
+ privateKeyId: text(value.private_key_id),
160
+ projectId: text(value.project_id),
161
+ tokenUri: VERTEX_TOKEN_URI,
162
+ fingerprint: fingerprint([type, clientEmail, privateKey]),
163
+ };
164
+ }
165
+ if (type === 'authorized_user') {
166
+ requireFields(value, type, ['client_id', 'client_secret', 'refresh_token'], path);
167
+ const clientId = text(value.client_id);
168
+ const clientSecret = text(value.client_secret);
169
+ const refreshToken = text(value.refresh_token);
170
+ return {
171
+ kind: type,
172
+ account: text(value.account),
173
+ clientId,
174
+ clientSecret,
175
+ refreshToken,
176
+ projectId: '',
177
+ fingerprint: fingerprint([type, clientId, clientSecret, refreshToken]),
178
+ };
179
+ }
180
+ if (type === 'impersonated_service_account' && !nested) {
181
+ const url = text(value.service_account_impersonation_url);
182
+ const target = /\/serviceAccounts\/([^/\s:?#]+):generateAccessToken$/.exec(url)?.[1];
183
+ if (!target)
184
+ throw adcError(`The ${type} credentials in ${path} have no valid service_account_impersonation_url.`, 'invalid');
185
+ const sourceGrant = adcGrant(value.source_credentials, path, true);
186
+ const delegates = (Array.isArray(value.delegates) ? value.delegates : [])
187
+ .map(text)
188
+ .filter(Boolean)
189
+ .map((delegate) => (delegate.startsWith('projects/') ? delegate : `projects/-/serviceAccounts/${delegate}`));
190
+ return {
191
+ kind: type,
192
+ account: target,
193
+ target,
194
+ delegates,
195
+ sourceGrant,
196
+ projectId: '',
197
+ fingerprint: fingerprint([type, target, delegates, sourceGrant.fingerprint]),
198
+ };
199
+ }
200
+ throw adcError(
201
+ `Vertex AI does not support "${type || 'unknown'}" credentials${nested ? ' as an impersonation source' : ''} (${path}). ` +
202
+ 'Use a service-account key, or sign in with `gcloud auth application-default login`.',
203
+ 'unsupported',
204
+ { credentialType: type || 'unknown' }
205
+ );
206
+ }
207
+
208
+ /**
209
+ * One ADC file: its grant, the identity every token it mints is bound to (the
210
+ * path and a fingerprint of the credential, never the secret itself), and its
211
+ * project hints.
212
+ */
213
+ export function parseVertexAdc(raw, { path, source }) {
214
+ let value;
215
+ try {
216
+ value = JSON.parse(String(raw || ''));
217
+ } catch {
218
+ throw adcError(`The credentials file ${path} is not valid JSON.`, 'invalid');
219
+ }
220
+ const grant = adcGrant(value, path);
221
+ const quotaProjectId = text(value.quota_project_id);
222
+ const identity = JSON.stringify([
223
+ 'adc',
224
+ path,
225
+ grant.kind,
226
+ grant.account,
227
+ grant.projectId,
228
+ quotaProjectId,
229
+ grant.fingerprint,
230
+ ]);
231
+ return { ...grant, path, source, quotaProjectId, identity };
232
+ }
233
+
234
+ /** The current ADC file, read and parsed. */
235
+ export function readVertexAdc(env = process.env) {
236
+ const location = adcLocation(env);
237
+ return parseVertexAdc(readAdcFile(location), location);
238
+ }
239
+
240
+ // fetch function -> { promise, found }: one probe each, its outcome (null when absent) kept.
241
+ const metadataProbes = new WeakMap();
242
+
243
+ /**
244
+ * Whether a metadata server answers, and its project ({ projectId } or null).
245
+ * Probed once per fetch function; a negative outcome is kept too.
246
+ */
247
+ export function probeVertexMetadata(fetchFn = fetch) {
248
+ let probe = metadataProbes.get(fetchFn);
249
+ if (!probe) {
250
+ probe = { found: undefined };
251
+ probe.promise = (async () => {
252
+ try {
253
+ const response = await fetchFn(`${METADATA_URL}/project/project-id`, {
254
+ headers: METADATA_HEADERS,
255
+ signal: AbortSignal.timeout(METADATA_PROBE_MS),
256
+ });
257
+ // Only the metadata server itself answers with this header.
258
+ if (!response.ok || response.headers.get('metadata-flavor') !== 'Google') return null;
259
+ const projectId = text(await response.text());
260
+ return projectId ? { projectId } : null;
261
+ } catch {
262
+ return null;
263
+ }
264
+ })().then((found) => {
265
+ probe.found = found;
266
+ return found;
267
+ });
268
+ metadataProbes.set(fetchFn, probe);
269
+ }
270
+ return probe.promise;
271
+ }
272
+
273
+ // The metadata server's default service account, once a probe found one.
274
+ function metadataCredential(fetchFn) {
275
+ const found = metadataProbes.get(fetchFn)?.found;
276
+ if (!found) return null;
277
+ return {
278
+ kind: 'metadata',
279
+ account: '',
280
+ projectId: found.projectId,
281
+ quotaProjectId: '',
282
+ path: METADATA_URL,
283
+ source: 'metadata',
284
+ identity: JSON.stringify(['adc', 'metadata', found.projectId]),
285
+ };
286
+ }
287
+
288
+ // The `project =` of the [core] section of gcloud's active configuration.
289
+ function gcloudConfigProject(env) {
290
+ const dir = gcloudConfigDir(env);
291
+ let name = text(env.CLOUDSDK_ACTIVE_CONFIG_NAME);
292
+ try {
293
+ name ||= readFileSync(join(dir, 'active_config'), 'utf8').trim();
294
+ } catch {
295
+ // No active_config file: fall back to the default configuration below.
296
+ }
297
+ if (!/^[\w.-]+$/.test(name)) name = 'default';
298
+ let lines;
299
+ try {
300
+ lines = readFileSync(join(dir, 'configurations', `config_${name}`), 'utf8').split(/\r?\n/);
301
+ } catch {
302
+ return '';
303
+ }
304
+ let section = '';
305
+ for (const line of lines) {
306
+ const header = /^\s*\[([^\]]+)\]\s*$/.exec(line);
307
+ if (header) section = header[1].trim().toLowerCase();
308
+ const entry = section === 'core' && /^\s*project\s*=\s*(.*?)\s*$/.exec(line);
309
+ if (entry) return entry[1];
310
+ }
311
+ return '';
312
+ }
313
+
314
+ /**
315
+ * The project an ADC request targets when no explicit one is set: the
316
+ * environment, the file's quota project, a service account's own project,
317
+ * then gcloud's configured project; '' when none names one.
318
+ */
319
+ export function vertexAdcProject(credential, env = process.env) {
320
+ for (const name of PROJECT_ENV) {
321
+ const value = text(env[name]);
322
+ if (value) return value;
323
+ }
324
+ return credential.quotaProjectId || credential.projectId || gcloudConfigProject(env);
325
+ }
326
+
327
+ /**
328
+ * The gcloud login the settings show: its type, account and project, or why it
329
+ * cannot be used. Without a login file, a metadata server a probe has found.
330
+ * Never carries a token or key.
331
+ */
332
+ export function describeVertexAdc({ env = process.env, projectId = '', fetchFn = fetch } = {}) {
333
+ try {
334
+ const credential = readVertexAdc(env);
335
+ return {
336
+ state: 'ready',
337
+ type: credential.kind,
338
+ account: credential.account || null,
339
+ projectId: text(projectId) || vertexAdcProject(credential, env) || null,
340
+ };
341
+ } catch (error) {
342
+ const metadata = error?.reason === 'missing' ? metadataCredential(fetchFn) : null;
343
+ if (metadata) {
344
+ return {
345
+ state: 'ready',
346
+ type: metadata.kind,
347
+ account: null,
348
+ projectId: text(projectId) || vertexAdcProject(metadata, env),
349
+ };
350
+ }
351
+ return {
352
+ state: error?.reason || 'invalid',
353
+ ...(error?.credentialType ? { type: error.credentialType } : {}),
354
+ message: String(error?.message || error),
355
+ };
356
+ }
357
+ }
358
+
359
+ function principalOf(credential) {
360
+ if (credential.kind === 'metadata') return "The metadata server's service account";
361
+ if (credential.kind !== 'authorized_user') return `The service account ${credential.account}`;
362
+ return credential.account ? `The gcloud account ${credential.account}` : 'The gcloud account';
363
+ }
364
+
365
+ export class VertexAccessTokenManager {
366
+ constructor({
367
+ credentialResolver,
368
+ authMode,
369
+ env = process.env,
370
+ fetchFn = fetch,
371
+ now = () => Date.now(),
372
+ refreshSkewMs = TOKEN_REFRESH_SKEW_MS,
373
+ } = {}) {
374
+ this.credentialResolver = credentialResolver;
375
+ // VERTEX_ADC_MODE reads the gcloud login; anything else the stored service-account key.
376
+ this.authMode = typeof authMode === 'function' ? authMode : () => authMode;
377
+ this.env = env;
378
+ this.fetchFn = fetchFn;
379
+ this.now = now;
380
+ this.refreshSkewMs = refreshSkewMs;
381
+ // What the current credential was read from (the stored key, or the ADC
382
+ // file's path, size and mtime) and its parse.
383
+ this.credentialKey = '';
384
+ this.credential = null;
385
+ // { identity, token, expiresAt }: served only to the credential that minted it.
386
+ this.cached = null;
387
+ // identity -> the one token request in flight for it.
388
+ this.minting = new Map();
389
+ }
390
+
391
+ _adc() {
392
+ return this.authMode() === VERTEX_ADC_MODE;
393
+ }
394
+
395
+ _credential() {
396
+ if (this._adc()) {
397
+ const location = adcLocation(this.env);
398
+ let stat;
399
+ try {
400
+ stat = statSync(location.path);
401
+ } catch (error) {
402
+ // No gcloud login: the metadata server's account, once ready() found one.
403
+ const failure = adcFileError(location, error);
404
+ const metadata = failure.reason === 'missing' ? metadataCredential(this.fetchFn) : null;
405
+ if (!metadata) throw failure;
406
+ return this._remember(`metadata\0${metadata.identity}`, () => metadata);
407
+ }
408
+ // A rewritten file (another `gcloud auth application-default login`) is read again.
409
+ return this._remember(`adc\0${location.path}\0${stat.size}\0${stat.mtimeMs}`, () =>
410
+ parseVertexAdc(readAdcFile(location), location)
411
+ );
412
+ }
413
+ const raw = String(this.credentialResolver?.() || '').trim();
414
+ if (!raw) throw new Error('Vertex AI service-account JSON is not configured.');
415
+ return this._remember(`key\0${raw}`, () => parseVertexServiceAccount(raw));
416
+ }
417
+
418
+ // Parse first: a malformed credential must leave the previous state untouched
419
+ // and keep failing, never be remembered as the current credential.
420
+ _remember(key, parse) {
421
+ if (key !== this.credentialKey) {
422
+ this.credential = parse();
423
+ this.credentialKey = key;
424
+ }
425
+ return this.credential;
426
+ }
427
+
428
+ /** Probes the metadata server (once) when ADC has no credentials file, so the synchronous reads can use it. */
429
+ async ready() {
430
+ if (!this._adc()) return;
431
+ try {
432
+ this._credential();
433
+ } catch (error) {
434
+ if (error?.reason === 'missing') await probeVertexMetadata(this.fetchFn);
435
+ }
436
+ }
437
+
438
+ projectId() {
439
+ const credential = this._credential();
440
+ if (!credential.source) return credential.projectId;
441
+ const project = vertexAdcProject(credential, this.env);
442
+ if (!project) {
443
+ throw new Error(
444
+ 'Vertex AI could not find a Google Cloud project for the gcloud login. Enter a project ID in Settings, ' +
445
+ 'set GOOGLE_CLOUD_PROJECT, or run `gcloud config set project PROJECT_ID`.'
446
+ );
447
+ }
448
+ return project;
449
+ }
450
+
451
+ identity() {
452
+ return this._credential().identity;
453
+ }
454
+
455
+ // A request built for one credential must not run under another's.
456
+ assertIdentity(identity) {
457
+ const credential = this._credential();
458
+ if (credential.identity !== identity) {
459
+ const kind = credential.source ? 'gcloud' : 'service-account';
460
+ throw Object.assign(
461
+ new Error(`Vertex AI authentication failed: the ${kind} credential changed during the request.`),
462
+ { code: 'EVERTEXAUTH' }
463
+ );
464
+ }
465
+ }
466
+
467
+ // Drops only the token the server rejected; a fresher one stays cached.
468
+ invalidate(rejectedToken) {
469
+ if (rejectedToken && this.cached?.token === rejectedToken) this.cached = null;
470
+ }
471
+
472
+ async getAccessToken({ identity } = {}) {
473
+ if (identity !== undefined) this.assertIdentity(identity);
474
+ const credential = this._credential();
475
+ const now = this.now();
476
+ const cached = this.cached;
477
+ if (cached?.identity === credential.identity && now + this.refreshSkewMs < cached.expiresAt) {
478
+ return cached.token;
479
+ }
480
+ let minting = this.minting.get(credential.identity);
481
+ if (!minting) {
482
+ minting = this._mint(credential, now).finally(() => {
483
+ if (this.minting.get(credential.identity) === minting) this.minting.delete(credential.identity);
484
+ });
485
+ this.minting.set(credential.identity, minting);
486
+ }
487
+ return await minting;
488
+ }
489
+
490
+ async _mint(credential, now) {
491
+ const minted =
492
+ credential.kind === 'impersonated_service_account'
493
+ ? await this._impersonate(credential, now)
494
+ : await this._grant(credential, now);
495
+ this.cached = { identity: credential.identity, ...minted };
496
+ return minted.token;
497
+ }
498
+
499
+ // A token from Google's OAuth endpoint (a signed service-account JWT, or a
500
+ // gcloud user login's refresh token), or the metadata server's.
501
+ async _grant(credential, now) {
502
+ let url = VERTEX_TOKEN_URI;
503
+ let init;
504
+ if (credential.kind === 'metadata') {
505
+ url = `${METADATA_URL}/instance/service-accounts/default/token`;
506
+ init = { method: 'GET', headers: METADATA_HEADERS };
507
+ } else {
508
+ let form;
509
+ if (credential.kind === 'authorized_user') {
510
+ form = {
511
+ grant_type: 'refresh_token',
512
+ client_id: credential.clientId,
513
+ client_secret: credential.clientSecret,
514
+ refresh_token: credential.refreshToken,
515
+ };
516
+ } else {
517
+ try {
518
+ form = { grant_type: JWT_BEARER_GRANT, assertion: createVertexJwtAssertion(credential, now) };
519
+ } catch (error) {
520
+ throw authError('could not sign the service-account JWT.', { cause: error });
521
+ }
522
+ }
523
+ init = {
524
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
525
+ body: new URLSearchParams(form).toString(),
526
+ };
527
+ }
528
+ const { response, payload } = await this._post(url, init);
529
+ if (!response.ok || !payload?.access_token) {
530
+ const detail = errorDetail(payload, response);
531
+ const message =
532
+ credential.kind === 'authorized_user' && payload?.error === 'invalid_grant'
533
+ ? `the gcloud login has expired or was revoked (${detail}). Run \`gcloud auth application-default login\` again.`
534
+ : detail;
535
+ throw authError(message, { status: response.status, httpStatus: response.status });
536
+ }
537
+ const expiresIn = Math.max(1, Number(payload.expires_in) || 3600);
538
+ return { token: String(payload.access_token), expiresAt: now + expiresIn * 1000 };
539
+ }
540
+
541
+ // A short-lived token of the target service account, minted with the source credential's token.
542
+ async _impersonate(credential, now) {
543
+ const source = await this._grant(credential.sourceGrant, now);
544
+ const { response, payload } = await this._post(
545
+ `${IAM_CREDENTIALS_URI}/projects/-/serviceAccounts/${credential.target}:generateAccessToken`,
546
+ {
547
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${source.token}` },
548
+ body: JSON.stringify({ delegates: credential.delegates, scope: [VERTEX_SCOPE], lifetime: '3600s' }),
549
+ }
550
+ );
551
+ if (!response.ok || !payload?.accessToken) {
552
+ const hint =
553
+ response.status === 403
554
+ ? ' The signed-in account needs the Service Account Token Creator role (roles/iam.serviceAccountTokenCreator) on it.'
555
+ : '';
556
+ throw authError(`impersonating ${credential.target} failed (${errorDetail(payload, response)}).${hint}`, {
557
+ status: response.status,
558
+ httpStatus: response.status,
559
+ });
560
+ }
561
+ const expiresAt = Date.parse(String(payload.expireTime || ''));
562
+ return { token: String(payload.accessToken), expiresAt: Number.isFinite(expiresAt) ? expiresAt : now + 3600_000 };
563
+ }
564
+
565
+ // A POST unless `init` names another method.
566
+ async _post(url, init) {
567
+ let response;
568
+ try {
569
+ response = await this.fetchFn(url, { method: 'POST', ...init });
570
+ } catch (error) {
571
+ throw authError(error?.message || 'token request failed', { cause: error });
572
+ }
573
+ let payload = null;
574
+ try {
575
+ payload = await response.json();
576
+ } catch {
577
+ // A non-JSON body leaves the payload null; callers read the status.
578
+ }
579
+ return { response, payload };
580
+ }
581
+
582
+ /**
583
+ * A gcloud login's 403 that names a missing quota project or IAM permission,
584
+ * reworded to say what to do. Status, code and the other classification
585
+ * fields stay as they were; any other error is returned unchanged.
586
+ */
587
+ accessError(error, { project = '' } = {}) {
588
+ if (!error || typeof error !== 'object' || error.vertexAccess || !this._adc()) return error;
589
+ if (vertexHttpStatus(error) !== 403 || error.code === 'VERTEX_MODEL_NOT_ENABLED') return error;
590
+ const detail = String(error.message || '');
591
+ const info = googleErrorInfo(error);
592
+ const evidence = `${detail} ${info.message}`;
593
+ const where = project ? ` in project ${project}` : '';
594
+ if (
595
+ info.reasons.has('USER_PROJECT_DENIED') ||
596
+ GCLOUD_CLIENT_CONSUMER.test(info.metadata.consumer || '') ||
597
+ evidence.includes(QUOTA_PROJECT_TEXT)
598
+ ) {
599
+ error.vertexAccess = 'quota-project';
600
+ error.message =
601
+ `[vertex-ai] Vertex AI requires a quota project for gcloud user credentials${where}. Use a service-account key, ` +
602
+ 'or sign in as a service account: gcloud auth application-default login --impersonate-service-account=SERVICE_ACCOUNT_EMAIL ' +
603
+ `(HTTP 403: ${detail.slice(0, 200)})`;
604
+ } else if (
605
+ error.code === 'iam_permission_denied' ||
606
+ info.reasons.has('IAM_PERMISSION_DENIED') ||
607
+ PERMISSION_TEXT.test(evidence)
608
+ ) {
609
+ let principal = 'The gcloud login';
610
+ try {
611
+ principal = principalOf(this._credential());
612
+ } catch {
613
+ // No readable credential: keep the generic principal name.
614
+ }
615
+ error.vertexAccess = 'permission';
616
+ error.message =
617
+ `[vertex-ai] ${principal} does not have permission to use Vertex AI${where}. ` +
618
+ `Grant it the Vertex AI User role (roles/aiplatform.user), then try again. (HTTP 403: ${detail.slice(0, 200)})`;
619
+ }
620
+ return error;
621
+ }
622
+ }