mixdog 1.2.6 → 1.3.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 (246) hide show
  1. package/LICENSES/simple-icons-CC0-1.0.txt +30 -0
  2. package/NOTICE.md +19 -0
  3. package/package.json +4 -1
  4. package/scripts/check-model-recommendations.mjs +89 -0
  5. package/scripts/gate-local.mjs +7 -0
  6. package/scripts/lib/isolated-test-env.mjs +16 -0
  7. package/src/agents/reviewer/AGENT.md +4 -2
  8. package/src/agents/reviewer/agent.json +1 -1
  9. package/src/defaults/shipped-history.json +7 -0
  10. package/src/defaults/skills/browser-use/SKILL.md +5 -0
  11. package/src/defaults/skills/memory-management/SKILL.md +4 -1
  12. package/src/defaults/skills/setup/references/actions.md +11 -4
  13. package/src/defaults/skills/setup-guide/SKILL.md +175 -0
  14. package/src/defaults/skills/setup-guide/model-recommendations.json +333 -0
  15. package/src/lib/rules-builder.cjs +55 -6
  16. package/src/rules/agent/AGENT.md +4 -0
  17. package/src/rules/agent-mode/AGENT_MODE.md +24 -0
  18. package/src/rules/agent-mode/MIXY.md +21 -0
  19. package/src/rules/agent-mode/ROUTINE.md +6 -0
  20. package/src/rules/lead/LEAD.md +7 -3
  21. package/src/rules/shared/70-delivery.md +3 -0
  22. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +86 -0
  23. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +77 -14
  24. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +182 -13
  25. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +4 -2
  26. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +215 -0
  27. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -4
  28. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +10 -1
  29. package/src/runtime/agent/orchestrator/mcp/client.mjs +115 -11
  30. package/src/runtime/agent/orchestrator/mcp/oauth-account.mjs +97 -0
  31. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +49 -0
  32. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +59 -9
  33. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +32 -7
  34. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +45 -6
  35. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +195 -6
  36. package/src/runtime/agent/orchestrator/mcp/server-connectors.mjs +17 -0
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +1 -0
  39. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +10 -3
  40. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
  41. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  42. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +17 -12
  43. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -0
  44. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +96 -1
  45. package/src/runtime/agent/orchestrator/runtime-core/context-status-shape.mjs +4 -0
  46. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +6 -4
  47. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +12 -0
  48. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -0
  49. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +12 -6
  50. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +32 -5
  51. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -4
  52. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +4 -1
  53. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +50 -20
  54. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +2 -0
  55. package/src/runtime/agent/orchestrator/session/agent-home.d.mts +4 -0
  56. package/src/runtime/agent/orchestrator/session/agent-home.mjs +33 -0
  57. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +62 -0
  58. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +31 -0
  59. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +241 -0
  60. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +162 -0
  61. package/src/runtime/agent/orchestrator/session/agent-room.mjs +641 -0
  62. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +7 -1
  63. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +2 -0
  64. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +6 -2
  65. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +165 -0
  66. package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +95 -0
  67. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +221 -0
  68. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +195 -0
  69. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +167 -0
  70. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -1
  71. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +10 -0
  72. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +16 -1
  73. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +74 -15
  74. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +9 -1
  75. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +5 -4
  76. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -3
  77. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +7 -0
  78. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +1 -0
  79. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -1
  80. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  81. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -0
  82. package/src/runtime/agent/orchestrator/session/manager/pending-lifecycle-epoch.mjs +0 -8
  83. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +44 -16
  84. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +22 -5
  85. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +39 -4
  86. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +27 -5
  87. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +55 -8
  88. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -0
  89. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -2
  90. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +6 -0
  91. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +58 -0
  92. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +24 -0
  93. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
  94. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +70 -0
  95. package/src/runtime/agent/orchestrator/session/routine-result.mjs +277 -0
  96. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +8 -0
  97. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +28 -9
  98. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +16 -2
  99. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -12
  100. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +46 -0
  101. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +0 -4
  102. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +44 -1
  103. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +12 -0
  104. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +113 -5
  105. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +4 -0
  106. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +25 -1
  107. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +8 -0
  108. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +6 -0
  109. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -0
  110. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +75 -57
  111. package/src/runtime/browser-bridge/client.mjs +24 -0
  112. package/src/runtime/channels/lib/scheduler.mjs +40 -12
  113. package/src/runtime/channels/lib/schedules-db.mjs +71 -12
  114. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +36 -16
  115. package/src/runtime/channels/lib/webhook.mjs +0 -1
  116. package/src/runtime/channels/lib/webhooks-db.mjs +100 -10
  117. package/src/runtime/channels/lib/worker-main.mjs +17 -20
  118. package/src/runtime/computer-bridge/client.mjs +28 -1
  119. package/src/runtime/connectors/gmail-app-password.mjs +690 -0
  120. package/src/runtime/connectors/google-mcp.mjs +446 -0
  121. package/src/runtime/memory/lib/core-memory-access.mjs +69 -0
  122. package/src/runtime/memory/lib/core-memory-file.mjs +16 -8
  123. package/src/runtime/memory/lib/core-memory-index.mjs +15 -9
  124. package/src/runtime/memory/lib/core-memory-management.mjs +10 -4
  125. package/src/runtime/memory/lib/core-memory-store.mjs +53 -30
  126. package/src/runtime/memory/lib/core-memory-uniqueness.mjs +10 -6
  127. package/src/runtime/memory/lib/http-router/session-routes.mjs +3 -2
  128. package/src/runtime/memory/lib/memory-action-handlers/core-actions.mjs +44 -14
  129. package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +11 -0
  130. package/src/runtime/memory/lib/memory.mjs +7 -1
  131. package/src/runtime/memory/lib/query-core-recall.mjs +9 -1
  132. package/src/runtime/memory/lib/query-hybrid-search.mjs +2 -0
  133. package/src/runtime/memory/lib/tool-call-handler.mjs +2 -0
  134. package/src/runtime/memory/tool-defs.mjs +5 -0
  135. package/src/runtime/shared/agent-avatar-sprites.mjs +445 -0
  136. package/src/runtime/shared/agent-avatar.d.mts +64 -0
  137. package/src/runtime/shared/agent-avatar.mjs +320 -0
  138. package/src/runtime/shared/agent-route-config.d.mts +3 -0
  139. package/src/runtime/shared/agent-route-config.mjs +16 -0
  140. package/src/runtime/shared/atomic-file.mjs +14 -1
  141. package/src/runtime/shared/config.mjs +8 -1
  142. package/src/runtime/shared/connector-accounts.mjs +15 -0
  143. package/src/runtime/shared/connector-catalog.d.mts +52 -0
  144. package/src/runtime/shared/connector-catalog.mjs +230 -0
  145. package/src/runtime/shared/connector-triggers.d.mts +3 -0
  146. package/src/runtime/shared/connector-triggers.mjs +43 -0
  147. package/src/runtime/shared/google-oauth-client.mjs +24 -0
  148. package/src/runtime/shared/held-request-host.d.mts +12 -0
  149. package/src/runtime/shared/held-request-host.mjs +71 -0
  150. package/src/runtime/shared/plain-preview.d.mts +1 -0
  151. package/src/runtime/shared/plain-preview.mjs +28 -0
  152. package/src/runtime/shared/routine-limits.d.mts +11 -0
  153. package/src/runtime/shared/routine-limits.mjs +21 -0
  154. package/src/runtime/shared/routine-trigger.d.mts +10 -0
  155. package/src/runtime/shared/routine-trigger.mjs +44 -0
  156. package/src/runtime/shared/schedule-model-ref.mjs +9 -10
  157. package/src/runtime/shared/slow-span.mjs +4 -12
  158. package/src/runtime/shared/statusline/statusline-segments.mjs +13 -1
  159. package/src/runtime/shared/transcript-metadata.mjs +10 -0
  160. package/src/runtime/shared/turn-changes.mjs +0 -8
  161. package/src/runtime/shared/usage-readout-totals.mjs +56 -0
  162. package/src/runtime/shared/user-data-guard.mjs +114 -36
  163. package/src/session-runtime/agent-home-delivery.mjs +459 -0
  164. package/src/session-runtime/agent-profile-changes.mjs +21 -0
  165. package/src/session-runtime/agent-room-delivery.mjs +92 -0
  166. package/src/session-runtime/agent-task-sessions.mjs +200 -0
  167. package/src/session-runtime/boot/apis.mjs +50 -18
  168. package/src/session-runtime/boot/facade.mjs +5 -8
  169. package/src/session-runtime/boot/infrastructure.mjs +28 -17
  170. package/src/session-runtime/boot/tools.mjs +3 -0
  171. package/src/session-runtime/channel-config-api.mjs +53 -5
  172. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +36 -20
  173. package/src/session-runtime/cwd-plugins/plugin-status.mjs +6 -1
  174. package/src/session-runtime/held-requests.mjs +228 -0
  175. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +121 -0
  176. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +5 -1
  177. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +31 -2
  178. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +31 -2
  179. package/src/session-runtime/internal-tool-executor.mjs +2 -0
  180. package/src/session-runtime/lifecycle/session-catalog.mjs +9 -0
  181. package/src/session-runtime/lifecycle/session-switch/resume.mjs +4 -0
  182. package/src/session-runtime/lifecycle/shared.mjs +6 -0
  183. package/src/session-runtime/mcp-glue/server-catalog.mjs +8 -0
  184. package/src/session-runtime/mcp-glue/server-input.mjs +3 -0
  185. package/src/session-runtime/mcp-oauth-login.mjs +112 -15
  186. package/src/session-runtime/model-route/set-route.mjs +14 -3
  187. package/src/session-runtime/owned-automation-run.mjs +229 -0
  188. package/src/session-runtime/resource-api.mjs +5 -0
  189. package/src/session-runtime/resource-mcp-api.mjs +107 -13
  190. package/src/session-runtime/resource-skills-api.mjs +12 -0
  191. package/src/session-runtime/room-member-gate.mjs +48 -0
  192. package/src/session-runtime/routine-home-turn.mjs +156 -0
  193. package/src/session-runtime/routine-owner.mjs +48 -0
  194. package/src/session-runtime/schedule-session-run.mjs +30 -64
  195. package/src/session-runtime/services/agent-tool/helpers.mjs +10 -0
  196. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +19 -4
  197. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +2 -0
  198. package/src/session-runtime/services/channel-admin.mjs +93 -24
  199. package/src/session-runtime/services/memory-access.mjs +37 -0
  200. package/src/session-runtime/services/provider-admin.mjs +74 -39
  201. package/src/session-runtime/services/schedule-access.mjs +91 -0
  202. package/src/session-runtime/session-create/session-options.mjs +34 -7
  203. package/src/session-runtime/session-hooks.mjs +16 -8
  204. package/src/session-runtime/setup-tool/executor.mjs +68 -6
  205. package/src/session-runtime/setup-tool/extended-actions.mjs +72 -14
  206. package/src/session-runtime/setup-tool/settings-contract.mjs +8 -6
  207. package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
  208. package/src/session-runtime/skills-api.mjs +16 -1
  209. package/src/session-runtime/tool-policy-refresh.mjs +238 -29
  210. package/src/session-runtime/turn/session-ops.mjs +8 -1
  211. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +10 -2
  212. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +3 -0
  213. package/src/session-runtime/webhook-session-run.mjs +49 -67
  214. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +239 -0
  215. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +556 -22
  216. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +11 -1
  217. package/src/session-runtime/workflow-agents-api.mjs +4 -1
  218. package/src/standalone/daemon-agent-control.mjs +1 -8
  219. package/src/standalone/daemon-session-control.mjs +11 -2
  220. package/src/standalone/daemon.mjs +18 -4
  221. package/src/standalone/session-protocol.mjs +27 -0
  222. package/src/standalone/session-service/session-calls/session-turns.mjs +9 -0
  223. package/src/standalone/session-service.mjs +19 -0
  224. package/src/tui/app/slash-commands.mjs +20 -0
  225. package/src/tui/dist/index.mjs +164 -86
  226. package/src/tui/session/context-state.mjs +6 -1
  227. package/src/tui/session/notice-surface.mjs +35 -4
  228. package/src/tui/session/queue-helpers.mjs +2 -1
  229. package/src/tui/session/session-api/extensions.mjs +4 -0
  230. package/src/tui/session/session-api/intake/submission.mjs +5 -0
  231. package/src/tui/session/session-api/integrations.mjs +8 -0
  232. package/src/tui/session/session-api/lifecycle/session-reset.mjs +13 -3
  233. package/src/tui/session/session-api/lifecycle.mjs +17 -1
  234. package/src/tui/session/session-api/routes.mjs +48 -3
  235. package/src/tui/session/session-flow/drain.mjs +12 -3
  236. package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -1
  237. package/src/tui/session/session-flow/queue/take-entries.mjs +3 -2
  238. package/src/tui/session/session-flow/steering.mjs +58 -24
  239. package/src/tui/session/tool-approval.mjs +26 -5
  240. package/src/tui/session/transcript-intake.mjs +6 -2
  241. package/src/tui/session/transcript-spill.mjs +5 -1
  242. package/src/tui/session/tui-steering-persist.mjs +65 -42
  243. package/src/tui/session/turn-live-call.mjs +56 -0
  244. package/src/tui/session/turn.mjs +22 -6
  245. package/src/session-runtime/automation-agents.mjs +0 -94
  246. package/src/session-runtime/automation-workflow.mjs +0 -59
@@ -1,18 +1,33 @@
1
1
  import { normalizeMcpTransportUrl } from './client-config.mjs';
2
+ import { withAccountParams } from './oauth-account.mjs';
2
3
  import { loginFailed, reauthenticationRequired } from './oauth-shared.mjs';
3
4
 
4
5
  /**
5
6
  * The SDK's OAuth client provider over one connection's credential `cache`
6
7
  * and login `flow` state.
7
8
  */
8
- export function createOAuthProvider({ cache, flow, lifetimeSignal }) {
9
+ export function createOAuthProvider({ cache, flow, lifetimeSignal, client = null, credentialStore = null }) {
9
10
  const { creds, ready, update } = cache;
10
11
  // The SDK saves only inside a login, which holds the lock and has just
11
12
  // adopted the stored entry.
12
13
  const save = async (change) => {
13
14
  if (!(await update(change))) throw loginFailed();
14
15
  };
16
+ // A pre-registered client (oauth-client.mjs) is the static client
17
+ // information: nothing is registered, and its secret (the user's lives in
18
+ // the keychain) goes with the token requests.
19
+ const staticClient = async () => {
20
+ const secret = client.secretAccount
21
+ ? await credentialStore?.getSecretAsync(client.secretAccount)
22
+ : client.clientSecret;
23
+ return { client_id: client.clientId, ...(secret ? { client_secret: secret } : {}) };
24
+ };
15
25
  return {
26
+ /** A pre-registered client: a refresh needs no stored registration. */
27
+ preRegistered: Boolean(client),
28
+ // A pre-registered client's provider issues tokens for its own APIs
29
+ // (Google): no RFC 8707 resource indicator is sent.
30
+ ...(client ? { validateResourceURL: async () => undefined } : {}),
16
31
  get redirectUrl() {
17
32
  return creds.redirectUrl;
18
33
  },
@@ -23,11 +38,13 @@ export function createOAuthProvider({ cache, flow, lifetimeSignal }) {
23
38
  grant_types: ['authorization_code', 'refresh_token'],
24
39
  response_types: ['code'],
25
40
  token_endpoint_auth_method: 'none',
41
+ ...(client?.scopes ? { scope: client.scopes.join(' ') } : {}),
26
42
  };
27
43
  },
28
44
  state: () => flow.state,
29
45
  clientInformation: async () => {
30
46
  await ready();
47
+ if (client) return staticClient();
31
48
  // A connect that cannot open a sign-in never registers a client (the
32
49
  // SDK registers one after dropping a rejected registration): the next
33
50
  // explicit sign-in registers it.
@@ -46,6 +63,11 @@ export function createOAuthProvider({ cache, flow, lifetimeSignal }) {
46
63
  lifetimeSignal.throwIfAborted();
47
64
  await save(() => {
48
65
  creds.tokens = value;
66
+ // A browser sign-in may be another account: its identity replaces the
67
+ // stored one (null when the server named nobody). A refresh keeps it
68
+ // unless the response names the account again.
69
+ if (flow.browserOpened) creds.identity = flow.identity ?? null;
70
+ else if (flow.identity) creds.identity = flow.identity;
49
71
  });
50
72
  },
51
73
  saveCodeVerifier: (value) => {
@@ -56,10 +78,11 @@ export function createOAuthProvider({ cache, flow, lifetimeSignal }) {
56
78
  return flow.verifier;
57
79
  },
58
80
  discoveryState: () => creds.discovery,
59
- saveDiscoveryState: (value) =>
60
- save(() => {
61
- creds.discovery = value;
62
- }),
81
+ // Kept in memory and stored with the login's next write (the client
82
+ // registration or the tokens), sparing a keychain write of its own.
83
+ saveDiscoveryState: (value) => {
84
+ creds.discovery = value;
85
+ },
63
86
  // The SDK invalidates 'all' when the token endpoint rejects the client
64
87
  // (invalid_client / unauthorized_client) and 'tokens' when it rejects the
65
88
  // grant (invalid_grant). The registration goes in both cases: an
@@ -88,8 +111,24 @@ export function createOAuthProvider({ cache, flow, lifetimeSignal }) {
88
111
  !flow.verifier
89
112
  )
90
113
  throw loginFailed();
114
+ // A pre-registered client is stored with this login's redirect, as a
115
+ // registration would be: the exchange and later refreshes (and the
116
+ // revocation on 연결 해제) read them from the entry.
117
+ if (client) {
118
+ const issuedTo = await staticClient();
119
+ await save(() => {
120
+ creds.clientInformation = issuedTo;
121
+ });
122
+ }
91
123
  flow.browserOpened = true;
92
- await flow.launchBrowser(target.href);
124
+ // A pre-registered client asks for its own scopes (the server's list
125
+ // may name far more) plus its provider's parameters (Google's
126
+ // access_type=offline, without which no refresh token is issued).
127
+ if (client?.scopes) target.searchParams.set('scope', client.scopes.join(' '));
128
+ for (const [key, value] of Object.entries(client?.authParams || {})) target.searchParams.set(key, value);
129
+ // The explicit sign-in's account mode (mcp-oauth-login.mjs) decides the
130
+ // provider's documented account-choice parameter, if any.
131
+ await flow.launchBrowser(withAccountParams(target.href, flow.launchBrowser.accountMode || 'connect').href);
93
132
  },
94
133
  };
95
134
  }
@@ -1,4 +1,4 @@
1
- import { createHash } from 'node:crypto';
1
+ import { createHash, randomBytes } from 'node:crypto';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import keychain from '../../../../lib/keychain-cjs.cjs';
4
4
  import { openInBrowser } from '../../../shared/open-url.mjs';
@@ -12,8 +12,10 @@ import {
12
12
  TOKENS_FLAG,
13
13
  createCredentialCache,
14
14
  credentialFile,
15
+ readIdentityFile,
15
16
  readStoredCredentials,
16
17
  withCredentialLock,
18
+ writeIdentityFile,
17
19
  writeRevisionMarker,
18
20
  } from './oauth-credentials.mjs';
19
21
  import { createLoginFlow } from './oauth-login.mjs';
@@ -55,18 +57,198 @@ export function hasStoredMcpOAuthTokens(url) {
55
57
  }
56
58
  }
57
59
 
60
+ /** Who the stored sign-in of one MCP server URL belongs to, without a keychain
61
+ * read: `{ account, workspace, forcedChoice }` (empty strings when the server
62
+ * named nobody), or null when signed out. */
63
+ export function storedMcpOAuthIdentity(url) {
64
+ return readIdentityFile(mcpOAuthAccount(normalizeMcpTransportUrl(url)));
65
+ }
66
+
67
+ // RFC 7009 revocation of the stored grant, only where the authorization server
68
+ // advertises its revocation endpoint (RFC 8414 metadata): it revokes the token
69
+ // this client was issued, nothing workspace-wide. True when the server
70
+ // confirmed it.
71
+ async function revokeStoredGrant(saved, fetchImpl) {
72
+ let value;
73
+ try {
74
+ value = JSON.parse(saved);
75
+ } catch {
76
+ return false;
77
+ }
78
+ const endpoint = value?.discovery?.authorizationServerMetadata?.revocation_endpoint;
79
+ const token = value?.tokens?.refresh_token || value?.tokens?.access_token;
80
+ const clientId = value?.clientInformation?.client_id;
81
+ if (typeof endpoint !== 'string' || !/^https:\/\//.test(endpoint) || !token || !clientId) return false;
82
+ const body = new URLSearchParams({
83
+ token,
84
+ token_type_hint: value.tokens.refresh_token ? 'refresh_token' : 'access_token',
85
+ client_id: clientId,
86
+ });
87
+ if (value.clientInformation.client_secret) body.set('client_secret', value.clientInformation.client_secret);
88
+ try {
89
+ const response = await fetchImpl(endpoint, {
90
+ method: 'POST',
91
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
92
+ body,
93
+ redirect: 'error',
94
+ signal: AbortSignal.timeout(5_000),
95
+ });
96
+ return response.ok;
97
+ } catch {
98
+ return false;
99
+ }
100
+ }
101
+
58
102
  /** Log out of one MCP server URL: delete its stored tokens and client
59
103
  * registration and drop them from live connections, which then need an
60
- * explicit reconnect to log in again. */
61
- export async function clearMcpOAuth({ url, credentialStore = keychain }) {
104
+ * explicit reconnect to log in again. With `revoke`, the grant is first
105
+ * revoked at the authorization server where it documents a revocation
106
+ * endpoint. Resolves `{ revoked }`. */
107
+ export async function clearMcpOAuth({ url, credentialStore = keychain, revoke = false, fetchImpl = fetch }) {
62
108
  url = normalizeMcpTransportUrl(url);
63
109
  const account = mcpOAuthAccount(url);
110
+ let revoked = false;
64
111
  await withCredentialLock(account, async () => {
112
+ const saved = await readStoredCredentials(credentialStore, account);
113
+ if (revoke && saved != null) revoked = await revokeStoredGrant(saved, fetchImpl);
65
114
  await writeRevisionMarker(account, null);
66
- if ((await readStoredCredentials(credentialStore, account)) != null)
67
- await credentialStore.deleteSecretAsync(account);
115
+ if (saved != null) await credentialStore.deleteSecretAsync(account);
116
+ await writeIdentityFile(account, null);
68
117
  });
69
118
  scopes.get(credentialStore)?.get(url)?.oauth.clear();
119
+ return { revoked };
120
+ }
121
+
122
+ async function storeEntry(account, url, value, credentialStore) {
123
+ const revision = randomBytes(16).toString('hex');
124
+ const { tokens, clientInformation, redirectUrl, discovery, identity } = value;
125
+ await writeRevisionMarker(account, revision, Boolean(tokens));
126
+ await credentialStore.setSecretAsync(
127
+ account,
128
+ JSON.stringify({ url, revision, tokens, clientInformation, redirectUrl, discovery, identity })
129
+ );
130
+ await writeIdentityFile(account, value);
131
+ }
132
+
133
+ /**
134
+ * 계정 바꾸기 for one MCP server URL, transactionally: a browser sign-in with
135
+ * the provider's documented account choice that runs beside the stored
136
+ * sign-in, reusing its client registration and discovery but never its tokens,
137
+ * and stores nothing. `done` resolves the new sign-in (`{ tokens, identity, … }`)
138
+ * or rejects (cancel, denial, timeout), leaving the stored sign-in and every
139
+ * live connection as they were. `answer(pasted)` completes it with a pasted
140
+ * redirect URL (remote sign-in); `cancel()` aborts it.
141
+ */
142
+ export function startMcpOAuthSwitch({
143
+ url,
144
+ launch,
145
+ serverName,
146
+ credentialStore = keychain,
147
+ fetchImpl = fetch,
148
+ timeoutMs = MCP_OAUTH_TIMEOUT_MS,
149
+ oauthClient = null,
150
+ }) {
151
+ url = normalizeMcpTransportUrl(url);
152
+ const lifetime = new AbortController();
153
+ const creds = {
154
+ tokens: undefined,
155
+ clientInformation: undefined,
156
+ redirectUrl: undefined,
157
+ discovery: undefined,
158
+ identity: undefined,
159
+ // Never null: the in-memory entry always exists.
160
+ observed: 'switch',
161
+ };
162
+ // An in-memory entry: a registration made during the switch stays here
163
+ // until the commit stores it with the tokens.
164
+ const cache = {
165
+ creds,
166
+ held: new Set(),
167
+ ready: async () => {},
168
+ adoptStored: async () => {},
169
+ update: async (change) => {
170
+ change();
171
+ return true;
172
+ },
173
+ withLock: (fn) => fn(),
174
+ };
175
+ const flow = {
176
+ listener: undefined,
177
+ state: undefined,
178
+ verifier: undefined,
179
+ allowBrowser: false,
180
+ browserOpened: false,
181
+ launchBrowser: launch,
182
+ finishCallback: undefined,
183
+ };
184
+ const provider = createOAuthProvider({
185
+ cache,
186
+ flow,
187
+ lifetimeSignal: lifetime.signal,
188
+ client: oauthClient,
189
+ credentialStore,
190
+ });
191
+ const loginFlow = createLoginFlow({
192
+ url,
193
+ cache,
194
+ flow,
195
+ provider,
196
+ lifetimeSignal: lifetime.signal,
197
+ openBrowser: launch,
198
+ fetchImpl,
199
+ timeoutMs,
200
+ });
201
+ const done = (async () => {
202
+ const saved = await readStoredCredentials(credentialStore, mcpOAuthAccount(url));
203
+ if (saved) {
204
+ const value = JSON.parse(saved);
205
+ if (value.url === url) {
206
+ creds.clientInformation = value.clientInformation;
207
+ creds.redirectUrl = value.redirectUrl;
208
+ creds.discovery = value.discovery;
209
+ }
210
+ }
211
+ await loginFlow.login({}, lifetime.signal, true, launch, undefined, serverName);
212
+ const { tokens, clientInformation, redirectUrl, discovery, identity } = creds;
213
+ if (!tokens) throw new Error('MCP OAuth login failed, cancelled, or timed out');
214
+ return { tokens, clientInformation, redirectUrl, discovery, identity: identity ?? null };
215
+ })();
216
+ return {
217
+ done,
218
+ answer(pasted) {
219
+ if (!flow.finishCallback || !flow.browserOpened || !flow.state || !creds.redirectUrl) {
220
+ throw new Error(MCP_OAUTH_CALLBACK_NOT_PENDING);
221
+ }
222
+ const { code } = parsePastedMcpOAuthCallback(pasted, { redirectUrl: creds.redirectUrl, state: flow.state });
223
+ loginFlow.stopListening();
224
+ loginFlow.answerCallback(code);
225
+ },
226
+ cancel() {
227
+ lifetime.abort();
228
+ flow.finishCallback?.();
229
+ loginFlow.closeListener();
230
+ },
231
+ };
232
+ }
233
+
234
+ /** Store the sign-in a switch returned as the URL's sign-in; resolves a
235
+ * `rollback()` that restores the previous one (or none). Live connections
236
+ * adopt either through the revision marker on their next request. */
237
+ export async function commitMcpOAuthSwitch({ url, signIn, credentialStore = keychain }) {
238
+ url = normalizeMcpTransportUrl(url);
239
+ const account = mcpOAuthAccount(url);
240
+ let previous = null;
241
+ await withCredentialLock(account, async () => {
242
+ previous = await readStoredCredentials(credentialStore, account);
243
+ await storeEntry(account, url, signIn, credentialStore);
244
+ });
245
+ return () =>
246
+ withCredentialLock(account, async () => {
247
+ if (previous) return storeEntry(account, url, JSON.parse(previous), credentialStore);
248
+ await writeRevisionMarker(account, null);
249
+ await credentialStore.deleteSecretAsync(account);
250
+ await writeIdentityFile(account, null);
251
+ });
70
252
  }
71
253
 
72
254
  // The keychain is the only durable store. Exact URLs, including query strings,
@@ -166,6 +348,7 @@ function createSharedOAuth({
166
348
  openBrowser = openInBrowser,
167
349
  fetchImpl = fetch,
168
350
  timeoutMs = MCP_OAUTH_TIMEOUT_MS,
351
+ oauthClient = null,
169
352
  }) {
170
353
  const lifetime = new AbortController();
171
354
  const cache = createCredentialCache({ url, credentialStore, account: mcpOAuthAccount(url) });
@@ -179,7 +362,13 @@ function createSharedOAuth({
179
362
  launchBrowser: openBrowser,
180
363
  finishCallback: undefined,
181
364
  };
182
- const provider = createOAuthProvider({ cache, flow, lifetimeSignal: lifetime.signal });
365
+ const provider = createOAuthProvider({
366
+ cache,
367
+ flow,
368
+ lifetimeSignal: lifetime.signal,
369
+ client: oauthClient,
370
+ credentialStore,
371
+ });
183
372
  const { login, stopListening, closeListener, answerCallback } = createLoginFlow({
184
373
  url,
185
374
  cache,
@@ -0,0 +1,17 @@
1
+ // Which catalog connector each connected MCP server is (connector-catalog.mjs
2
+ // connectorOfServer), recorded by the client as servers connect, so a tool
3
+ // name alone (`mcp__<server>__<tool>`) finds its catalog defaults at dispatch.
4
+ import { connectorOfServer } from '../../../shared/connector-catalog.mjs';
5
+
6
+ const byServer = new Map();
7
+
8
+ export function rememberServerConnector(name, cfg) {
9
+ const id = connectorOfServer(cfg);
10
+ if (id) byServer.set(String(name), id);
11
+ else byServer.delete(String(name));
12
+ }
13
+
14
+ /** The catalog id of connected server `name`, or ''. */
15
+ export function serverConnector(name) {
16
+ return byServer.get(String(name)) || '';
17
+ }
@@ -10,7 +10,7 @@ import { createNpmVersionSource, maxSemver } from './npm-cli-version.mjs';
10
10
  // source release just to advance the user-agent version.
11
11
  // Claude Opus 5.5 requires 2.1.280 or newer. The release workflow raises this
12
12
  // to the published CLI version (scripts/sync-client-version-floors.mjs).
13
- export const DEFAULT_CLI_VERSION = '2.1.302';
13
+ export const DEFAULT_CLI_VERSION = '2.1.304';
14
14
  export const CLAUDE_CLI_NPM_PACKAGE = '@anthropic-ai/claude-code';
15
15
 
16
16
  const CACHE_SCHEMA_VERSION = 1;
@@ -209,6 +209,7 @@ export function describeAnthropicOAuthCredentials() {
209
209
  return {
210
210
  ...oauthCredentialStatus({ hasRefresh, expiresAt, detail, refreshSkewMs: TOKEN_REFRESH_SKEW_MS }),
211
211
  ...accountIdentityFields(creds.identity),
212
+ plan: creds.subscriptionType || null,
212
213
  };
213
214
  } catch (err) {
214
215
  return {
@@ -218,7 +218,12 @@ async function provisionProject(context, tierId) {
218
218
  }
219
219
 
220
220
  /** Resolve account eligibility and return the project from a fresh status load. */
221
- export async function discoverProject(
221
+ export async function discoverProject(accessToken, options = {}) {
222
+ return (await discoverAccount(accessToken, options)).projectId;
223
+ }
224
+
225
+ /** Project plus the account's tier id (paid tier first), from the same status load. */
226
+ export async function discoverAccount(
222
227
  accessToken,
223
228
  { fetchFn = fetch, onProgress = null, signal = null, email = '' } = {}
224
229
  ) {
@@ -235,7 +240,8 @@ export async function discoverProject(
235
240
  const refreshed = await loadProjectState(context);
236
241
  const projectId = extractProjectId(refreshed);
237
242
  if (!projectId) throw new Error('[antigravity-oauth] loadCodeAssist did not return a cloudaicompanionProject');
238
- return projectId;
243
+ const tierId = refreshed.paidTier?.id || refreshed.currentTier?.id;
244
+ return { projectId, tierId: typeof tierId === 'string' ? tierId : '' };
239
245
  }
240
246
 
241
247
  export async function exchangeAuthorizationCode({ code, verifier, fetchFn = fetch, onProgress = null, signal = null }) {
@@ -267,12 +273,13 @@ export async function exchangeAuthorizationCode({ code, verifier, fetchFn = fetc
267
273
  throw new Error('[antigravity-oauth] token exchange response missing access_token or refresh_token');
268
274
  }
269
275
  const { email, userId } = await fetchAccountInfo(json.access_token, { fetchFn, signal });
270
- const projectId = await discoverProject(json.access_token, { fetchFn, onProgress, signal, email });
276
+ const { projectId, tierId } = await discoverAccount(json.access_token, { fetchFn, onProgress, signal, email });
271
277
  const tokens = {
272
278
  access_token: json.access_token,
273
279
  refresh_token: json.refresh_token,
274
280
  expires_at: typeof json.expires_in === 'number' ? Date.now() + json.expires_in * 1000 : 0,
275
281
  project_id: projectId,
282
+ tier_id: tierId,
276
283
  email,
277
284
  user_id: userId,
278
285
  };
@@ -204,6 +204,7 @@ export function loadTokens() {
204
204
  refresh_token: String(raw.refresh_token),
205
205
  expires_at: _normalizeExpiresAt(raw.expires_at ?? raw.expiresAt),
206
206
  project_id: String(raw.project_id || raw.projectId || ''),
207
+ tier_id: String(raw.tier_id || ''),
207
208
  email: String(raw.email || ''),
208
209
  user_id: String(raw.user_id || raw.userId || ''),
209
210
  source: 'own',
@@ -222,6 +223,7 @@ export function saveTokens(tokens) {
222
223
  refresh_token: tokens.refresh_token,
223
224
  expires_at: tokens.expires_at || 0,
224
225
  project_id: tokens.project_id || tokens.projectId || '',
226
+ tier_id: tokens.tier_id || '',
225
227
  email: tokens.email || '',
226
228
  user_id: tokens.user_id || tokens.userId || '',
227
229
  },
@@ -274,6 +276,7 @@ export function describeAntigravityOAuthCredentials() {
274
276
  ...accountIdentityFields({ id: tokens.user_id, email: tokens.email }),
275
277
  email: tokens.email || '',
276
278
  projectId: tokens.project_id || '',
279
+ plan: tokens.tier_id || null,
277
280
  };
278
281
  } catch {
279
282
  return credentialsNotSet();
@@ -340,6 +343,7 @@ export function refreshTokens({ fetchFn = fetch } = {}) {
340
343
  refresh_token: json.refresh_token || current.refresh_token,
341
344
  expires_at: typeof json.expires_in === 'number' ? Date.now() + json.expires_in * 1000 : 0,
342
345
  project_id: current.project_id,
346
+ tier_id: current.tier_id,
343
347
  email: current.email,
344
348
  user_id: current.user_id,
345
349
  };
@@ -4,7 +4,7 @@ import { createRemoteVersionSource } from './npm-cli-version.mjs';
4
4
  // documented version endpoint; the official installer script embeds the
5
5
  // current build (…/lab/<build>/…), parsed best-effort. Any format change or
6
6
  // failure falls back to the floor.
7
- export const CURSOR_CLIENT_VERSION_FLOOR = 'cli-2026.10.16-e3cb207';
7
+ export const CURSOR_CLIENT_VERSION_FLOOR = 'cli-2026.10.19-6bbf2c4';
8
8
  const BUILD_PATTERN = /downloads\.cursor\.com\/lab\/(\d{4}\.\d{2}\.\d{2}-[0-9a-f]{6,40})\//;
9
9
  export const CURSOR_INSTALL_URL = 'https://cursor.com/install';
10
10
 
@@ -14,14 +14,6 @@ import { retryAfterMsFromError, sleepWithAbort } from './retry-classifier.mjs';
14
14
 
15
15
  const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
16
16
 
17
- // Explicit quota / billing refusals: waiting never helps, so no retry layer
18
- // may treat them as an ordinary rate limit.
19
- const QUOTA_OR_BILLING_CODE_RE = /quota|billing|usage_limit|usage_not_included/;
20
-
21
- export function isQuotaOrBillingCode(code) {
22
- return QUOTA_OR_BILLING_CODE_RE.test(String(code || '').toLowerCase());
23
- }
24
-
25
17
  /**
26
18
  * Parse an HTTP error body ONCE into its contract fields. `message` is the
27
19
  * body's own message (untruncated); callers truncate for diagnostics.
@@ -211,7 +203,8 @@ export function applyErrorContract(err, { status, headers, code, error, event }
211
203
  err.status = n;
212
204
  err.httpStatus = n;
213
205
  }
214
- const wireHeaders = headers ?? error?.headers ?? event?.error?.headers ?? event?.response?.error?.headers ?? event?.headers;
206
+ const wireHeaders =
207
+ headers ?? error?.headers ?? event?.error?.headers ?? event?.response?.error?.headers ?? event?.headers;
215
208
  if (wireHeaders && typeof wireHeaders === 'object') err.headers = wireHeaders;
216
209
  const rawCode = code ?? error?.code ?? error?.type ?? null;
217
210
  if (typeof rawCode === 'string' && rawCode.trim()) err.code = rawCode.trim().toLowerCase();
@@ -225,7 +218,13 @@ export function applyErrorContract(err, { status, headers, code, error, event }
225
218
  if (err.retryAfterAt == null) err.retryAfterAt = Date.now() + retryAfterMs;
226
219
  }
227
220
  const type = typeof error?.type === 'string' ? error.type.trim().toLowerCase() : '';
228
- const mapped = typedWireErrorMessage({ code: err.code || '', type, error, headers: err.headers, resetAt: err.resetAt });
221
+ const mapped = typedWireErrorMessage({
222
+ code: err.code || '',
223
+ type,
224
+ error,
225
+ headers: err.headers,
226
+ resetAt: err.resetAt,
227
+ });
229
228
  if (mapped) applyTypedWireMessage(err, mapped, error?.message);
230
229
  } catch {}
231
230
  return err;
@@ -238,7 +237,12 @@ export function applyErrorContract(err, { status, headers, code, error, event }
238
237
  * makes unknown codes default-retryable).
239
238
  */
240
239
  export function markContentFilter(err, reason) {
241
- if (String(reason || '').trim().toLowerCase() !== 'content_filter') return err;
240
+ if (
241
+ String(reason || '')
242
+ .trim()
243
+ .toLowerCase() !== 'content_filter'
244
+ )
245
+ return err;
242
246
  err.code = 'content_filter';
243
247
  err.providerErrorCode = 'content_filter';
244
248
  return err;
@@ -260,7 +264,8 @@ export function httpResponseError({ label, status, headers, text, initialRespons
260
264
  }
261
265
 
262
266
  // The route decides the HTTP/SSE label: the api-key `openai` provider is not OAuth.
263
- export const httpLabel = (auth) => (auth?.type === 'openai-direct' ? 'OpenAI HTTP fallback' : 'OpenAI OAuth HTTP fallback');
267
+ export const httpLabel = (auth) =>
268
+ auth?.type === 'openai-direct' ? 'OpenAI HTTP fallback' : 'OpenAI OAuth HTTP fallback';
264
269
 
265
270
  /**
266
271
  * Changing transport must not bypass the server's retry deadline.
@@ -92,6 +92,15 @@ export function identityFromTokens(...tokens) {
92
92
  return normalizeAccountIdentity({ id: [sub, accountId].filter(Boolean).join(':'), email });
93
93
  }
94
94
 
95
+ /** ChatGPT plan (e.g. plus, pro) from the first token carrying the claim, else null. */
96
+ export function planFromTokens(...tokens) {
97
+ for (const token of tokens) {
98
+ const plan = decodeJwtPayload(token)?.['https://api.openai.com/auth']?.chatgpt_plan_type;
99
+ if (typeof plan === 'string' && plan) return plan;
100
+ }
101
+ return null;
102
+ }
103
+
95
104
  /** mtime of the token file, 0 when it is missing or unreadable. */
96
105
  export function tokensFileMtimeMs() {
97
106
  try {
@@ -165,6 +174,7 @@ export function describeOpenAIOAuthCredentials() {
165
174
  ...oauthCredentialStatus({ hasRefresh, expiresAt, detail: source, refreshSkewMs: TOKEN_REFRESH_SKEW_MS }),
166
175
  ...accountIdentityFields(tokens.identity),
167
176
  ...(accountId ? { accountId } : {}),
177
+ plan: planFromTokens(tokens.id_token, tokens.access_token),
168
178
  };
169
179
  } catch (err) {
170
180
  return {
@@ -1,3 +1,5 @@
1
+ import { loginPageUrl } from '../session/human-gate/requests.mjs';
2
+
1
3
  const ASK_USER_MAX_QUESTIONS = 4;
2
4
  export const ASK_USER_MAX_TEXT = 4000;
3
5
 
@@ -50,13 +52,106 @@ export const ASK_USER_TOOL_DEFS = Object.freeze([
50
52
  required: ['question', 'header', 'options'],
51
53
  },
52
54
  },
55
+ handoff: {
56
+ type: 'object',
57
+ description:
58
+ 'Instead of questions: secret = the user types a password/token into the focused field (you never see it; result provided/skipped/timeout); takeover = the user does a step (sign-in, CAPTCHA, 2FA) and says done.',
59
+ properties: {
60
+ kind: { type: 'string', enum: ['secret', 'takeover'] },
61
+ surface: { type: 'string', enum: ['browser', 'computer'] },
62
+ target: { type: 'string', maxLength: 200, description: 'Site/app and form.' },
63
+ field: { type: 'string', maxLength: 80, description: 'secret: field label.' },
64
+ instructions: { type: 'string', maxLength: 2000, description: 'takeover: what to do.' },
65
+ need: {
66
+ type: 'string',
67
+ enum: ['sign_in', 'action', 'confirmation'],
68
+ description:
69
+ 'takeover: sign_in (sign-in, 2FA), action (CAPTCHA, other step), confirmation (identity/payment check). Default action.',
70
+ },
71
+ purpose: {
72
+ type: 'string',
73
+ enum: ['login'],
74
+ description: 'browser takeover: login = sign-in window on url.',
75
+ },
76
+ url: { type: 'string', maxLength: 2048, description: 'login: sign-in page (https).' },
77
+ tab: { type: 'string', maxLength: 64, description: 'browser takeover: page ID from list_tabs.' },
78
+ window_id: { type: 'string', maxLength: 128, description: 'computer takeover: exact window id.' },
79
+ app: { type: 'string', maxLength: 120, description: 'computer takeover: app name.' },
80
+ },
81
+ required: ['kind', 'surface'],
82
+ additionalProperties: false,
83
+ },
53
84
  },
54
- required: ['questions'],
55
85
  additionalProperties: false,
56
86
  },
57
87
  },
58
88
  ]);
59
89
 
90
+ /** The handoff of an ask_user call ({ kind, surface, target, field, instructions }), or null. */
91
+ export function validateAskUserHandoff(args) {
92
+ const handoff = args?.handoff;
93
+ if (handoff == null) return null;
94
+ if (args.questions != null) throw new Error('ask_user takes questions or a handoff, not both');
95
+ if (typeof handoff !== 'object' || Array.isArray(handoff)) throw new Error('ask_user handoff must be an object');
96
+ if (handoff.kind !== 'secret' && handoff.kind !== 'takeover') {
97
+ throw new Error('ask_user handoff kind is secret or takeover');
98
+ }
99
+ if (handoff.surface !== 'browser' && handoff.surface !== 'computer') {
100
+ throw new Error('ask_user handoff surface is browser or computer');
101
+ }
102
+ const text = (key, limit) => (typeof handoff[key] === 'string' ? handoff[key].trim().slice(0, limit) : '');
103
+ if (handoff.kind === 'takeover' && !text('instructions', 2000)) {
104
+ throw new Error('ask_user takeover needs instructions');
105
+ }
106
+ const login = handoffLogin(handoff);
107
+ let need = login ? 'sign_in' : 'action';
108
+ if (['sign_in', 'action', 'confirmation'].includes(handoff.need)) need = handoff.need;
109
+ return {
110
+ kind: handoff.kind,
111
+ surface: handoff.surface,
112
+ target: text('target', 200),
113
+ field: text('field', 80),
114
+ instructions: text('instructions', 2000),
115
+ ...(handoff.kind === 'takeover' ? { need, identity: handoffIdentity(handoff), ...(login ? { login } : {}) } : {}),
116
+ };
117
+ }
118
+
119
+ /** A login takeover's sign-in page ({ url, origin }); null without the
120
+ * explicit purpose, so every other takeover keeps the side panel. */
121
+ function handoffLogin(handoff) {
122
+ if (handoff.purpose == null) return null;
123
+ if (handoff.purpose !== 'login' || handoff.kind !== 'takeover' || handoff.surface !== 'browser') {
124
+ throw new Error('ask_user purpose login is a browser takeover');
125
+ }
126
+ const page = loginPageUrl(handoff.url);
127
+ if (!page) throw new Error('ask_user login needs an https url (http only on localhost)');
128
+ return page;
129
+ }
130
+
131
+ /** The window a takeover is about, validated per surface: a browser page ID
132
+ * (list_tabs p1, v1 or a page name) or a computer window id and app. Shapes
133
+ * that are not plain tokens are dropped, never passed on. */
134
+ const TOKEN = /^[\w.:#@-]+$/;
135
+ function handoffIdentity(handoff) {
136
+ const token = (key, limit) => {
137
+ const value = typeof handoff[key] === 'string' ? handoff[key].trim() : '';
138
+ return value && value.length <= limit && TOKEN.test(value) ? value : '';
139
+ };
140
+ if (handoff.surface === 'browser') {
141
+ const tab = token('tab', 64);
142
+ return tab ? { tab } : {};
143
+ }
144
+ const windowId = token('window_id', 128);
145
+ const app =
146
+ typeof handoff.app === 'string'
147
+ ? handoff.app
148
+ .replace(/[\r\n]+/g, ' ')
149
+ .trim()
150
+ .slice(0, 120)
151
+ : '';
152
+ return { ...(windowId ? { windowId } : {}), ...(app ? { app } : {}) };
153
+ }
154
+
60
155
  /** Throws a clear error when the questions are malformed; returns the list. */
61
156
  export function validateAskUserQuestions(args) {
62
157
  const questions = args?.questions;