@trusty-squire/mcp 1.1.13-rc.8 → 1.1.13

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 (162) hide show
  1. package/README.md +139 -62
  2. package/assets/login/vnc-input.js +9 -0
  3. package/assets/login/vnc.html +214 -202
  4. package/assets/screencaps/connect-walkthrough.png +0 -0
  5. package/assets/screencaps/connect-walkthrough.svg +1 -1
  6. package/dist/api-client.d.ts +26 -3
  7. package/dist/api-client.d.ts.map +1 -1
  8. package/dist/api-client.js +19 -31
  9. package/dist/api-client.js.map +1 -1
  10. package/dist/bin.js +2 -3
  11. package/dist/bin.js.map +1 -1
  12. package/dist/bot/browser.d.ts +134 -77
  13. package/dist/bot/browser.d.ts.map +1 -1
  14. package/dist/bot/browser.js +1788 -1543
  15. package/dist/bot/browser.js.map +1 -1
  16. package/dist/bot/compact-observation-v2.d.ts +167 -0
  17. package/dist/bot/compact-observation-v2.d.ts.map +1 -0
  18. package/dist/bot/compact-observation-v2.js +1038 -0
  19. package/dist/bot/compact-observation-v2.js.map +1 -0
  20. package/dist/bot/credential-shape.d.ts +2 -0
  21. package/dist/bot/credential-shape.d.ts.map +1 -1
  22. package/dist/bot/credential-shape.js +21 -8
  23. package/dist/bot/credential-shape.js.map +1 -1
  24. package/dist/bot/element-fingerprint.d.ts +9 -0
  25. package/dist/bot/element-fingerprint.d.ts.map +1 -0
  26. package/dist/bot/element-fingerprint.js +147 -0
  27. package/dist/bot/element-fingerprint.js.map +1 -0
  28. package/dist/bot/email-verification.d.ts +7 -1
  29. package/dist/bot/email-verification.d.ts.map +1 -1
  30. package/dist/bot/email-verification.js +48 -14
  31. package/dist/bot/email-verification.js.map +1 -1
  32. package/dist/bot/google-login.d.ts +28 -52
  33. package/dist/bot/google-login.d.ts.map +1 -1
  34. package/dist/bot/google-login.js +246 -1119
  35. package/dist/bot/google-login.js.map +1 -1
  36. package/dist/bot/install-completion.d.ts +1 -2
  37. package/dist/bot/install-completion.d.ts.map +1 -1
  38. package/dist/bot/install-completion.js +1 -2
  39. package/dist/bot/install-completion.js.map +1 -1
  40. package/dist/bot/login-state.d.ts +13 -7
  41. package/dist/bot/login-state.d.ts.map +1 -1
  42. package/dist/bot/login-state.js +50 -114
  43. package/dist/bot/login-state.js.map +1 -1
  44. package/dist/bot/oauth-providers.d.ts +1 -1
  45. package/dist/bot/oauth-providers.d.ts.map +1 -1
  46. package/dist/bot/oauth-providers.js +3 -3
  47. package/dist/bot/oauth-providers.js.map +1 -1
  48. package/dist/bot/oauth-scope.d.ts +4 -0
  49. package/dist/bot/oauth-scope.d.ts.map +1 -0
  50. package/dist/bot/oauth-scope.js +58 -0
  51. package/dist/bot/oauth-scope.js.map +1 -0
  52. package/dist/bot/operator-browser-watchdog.d.ts +127 -0
  53. package/dist/bot/operator-browser-watchdog.d.ts.map +1 -0
  54. package/dist/bot/operator-browser-watchdog.js +438 -0
  55. package/dist/bot/operator-browser-watchdog.js.map +1 -0
  56. package/dist/bot/owner-process-reaper-worker.d.ts +2 -0
  57. package/dist/bot/owner-process-reaper-worker.d.ts.map +1 -0
  58. package/dist/bot/owner-process-reaper-worker.js +17 -0
  59. package/dist/bot/owner-process-reaper-worker.js.map +1 -0
  60. package/dist/bot/owner-process-reaper.d.ts +103 -0
  61. package/dist/bot/owner-process-reaper.d.ts.map +1 -0
  62. package/dist/bot/owner-process-reaper.js +1058 -0
  63. package/dist/bot/owner-process-reaper.js.map +1 -0
  64. package/dist/bot/pay-operator.d.ts +13 -2
  65. package/dist/bot/pay-operator.d.ts.map +1 -1
  66. package/dist/bot/pay-operator.js +319 -123
  67. package/dist/bot/pay-operator.js.map +1 -1
  68. package/dist/bot/profile.d.ts +10 -1
  69. package/dist/bot/profile.d.ts.map +1 -1
  70. package/dist/bot/profile.js +137 -11
  71. package/dist/bot/profile.js.map +1 -1
  72. package/dist/bot/promote-to-skill.js +1 -1
  73. package/dist/bot/promote-to-skill.js.map +1 -1
  74. package/dist/bot/provision-session.d.ts +60 -164
  75. package/dist/bot/provision-session.d.ts.map +1 -1
  76. package/dist/bot/provision-session.js +2049 -1546
  77. package/dist/bot/provision-session.js.map +1 -1
  78. package/dist/bot/remote-login-display.d.ts +61 -0
  79. package/dist/bot/remote-login-display.d.ts.map +1 -0
  80. package/dist/bot/remote-login-display.js +765 -0
  81. package/dist/bot/remote-login-display.js.map +1 -0
  82. package/dist/bot/session/hosts.d.ts +8 -0
  83. package/dist/bot/session/hosts.d.ts.map +1 -0
  84. package/dist/bot/session/hosts.js +36 -0
  85. package/dist/bot/session/hosts.js.map +1 -0
  86. package/dist/bot/session/lifecycle.d.ts +56 -0
  87. package/dist/bot/session/lifecycle.d.ts.map +1 -0
  88. package/dist/bot/session/lifecycle.js +962 -0
  89. package/dist/bot/session/lifecycle.js.map +1 -0
  90. package/dist/bot/session/model.d.ts +175 -0
  91. package/dist/bot/session/model.d.ts.map +1 -0
  92. package/dist/bot/session/model.js +77 -0
  93. package/dist/bot/session/model.js.map +1 -0
  94. package/dist/install/cli.d.ts +29 -9
  95. package/dist/install/cli.d.ts.map +1 -1
  96. package/dist/install/cli.js +243 -322
  97. package/dist/install/cli.js.map +1 -1
  98. package/dist/install/interactive.d.ts +1 -0
  99. package/dist/install/interactive.d.ts.map +1 -1
  100. package/dist/install/interactive.js +19 -5
  101. package/dist/install/interactive.js.map +1 -1
  102. package/dist/install/ui.d.ts.map +1 -1
  103. package/dist/install/ui.js +3 -15
  104. package/dist/install/ui.js.map +1 -1
  105. package/dist/operator-env.js +1 -1
  106. package/dist/operator-env.js.map +1 -1
  107. package/dist/server-instance-registry.d.ts +103 -0
  108. package/dist/server-instance-registry.d.ts.map +1 -0
  109. package/dist/server-instance-registry.js +392 -0
  110. package/dist/server-instance-registry.js.map +1 -0
  111. package/dist/server.d.ts +15 -1
  112. package/dist/server.d.ts.map +1 -1
  113. package/dist/server.js +164 -62
  114. package/dist/server.js.map +1 -1
  115. package/dist/session-guard.d.ts +28 -0
  116. package/dist/session-guard.d.ts.map +1 -0
  117. package/dist/session-guard.js +84 -0
  118. package/dist/session-guard.js.map +1 -0
  119. package/dist/session.d.ts +57 -13
  120. package/dist/session.d.ts.map +1 -1
  121. package/dist/session.js +246 -127
  122. package/dist/session.js.map +1 -1
  123. package/dist/tools/audit-log.d.ts +16 -2
  124. package/dist/tools/audit-log.d.ts.map +1 -1
  125. package/dist/tools/audit-log.js +223 -13
  126. package/dist/tools/audit-log.js.map +1 -1
  127. package/dist/tools/audit-rollup.d.ts +80 -0
  128. package/dist/tools/audit-rollup.d.ts.map +1 -0
  129. package/dist/tools/audit-rollup.js +0 -0
  130. package/dist/tools/audit-rollup.js.map +1 -0
  131. package/dist/tools/fetch-credential.d.ts +37 -0
  132. package/dist/tools/fetch-credential.d.ts.map +1 -0
  133. package/dist/tools/fetch-credential.js +153 -0
  134. package/dist/tools/fetch-credential.js.map +1 -0
  135. package/dist/tools/index.d.ts +3 -1
  136. package/dist/tools/index.d.ts.map +1 -1
  137. package/dist/tools/index.js +7 -3
  138. package/dist/tools/index.js.map +1 -1
  139. package/dist/tools/operate-pay.d.ts +4 -4
  140. package/dist/tools/operate-pay.d.ts.map +1 -1
  141. package/dist/tools/operate-pay.js +163 -96
  142. package/dist/tools/operate-pay.js.map +1 -1
  143. package/dist/tools/provision-drive.d.ts +43 -35
  144. package/dist/tools/provision-drive.d.ts.map +1 -1
  145. package/dist/tools/provision-drive.js +268 -164
  146. package/dist/tools/provision-drive.js.map +1 -1
  147. package/dist/tools/use-credential.d.ts.map +1 -1
  148. package/dist/tools/use-credential.js +3 -2
  149. package/dist/tools/use-credential.js.map +1 -1
  150. package/package.json +4 -6
  151. package/dist/bot/operator-direct-identity.d.ts +0 -26
  152. package/dist/bot/operator-direct-identity.d.ts.map +0 -1
  153. package/dist/bot/operator-direct-identity.js +0 -120
  154. package/dist/bot/operator-direct-identity.js.map +0 -1
  155. package/dist/bot/operator-profile-pool.d.ts +0 -97
  156. package/dist/bot/operator-profile-pool.d.ts.map +0 -1
  157. package/dist/bot/operator-profile-pool.js +0 -855
  158. package/dist/bot/operator-profile-pool.js.map +0 -1
  159. package/dist/bot/xvfb.d.ts +0 -13
  160. package/dist/bot/xvfb.d.ts.map +0 -1
  161. package/dist/bot/xvfb.js +0 -146
  162. package/dist/bot/xvfb.js.map +0 -1
@@ -6,51 +6,44 @@
6
6
  // in place; the consent-at-install prompt is the remaining hardening.
7
7
  import { z } from "zod";
8
8
  import { constants, generateKeyPairSync, privateDecrypt } from "node:crypto";
9
- import { startProvisionSession, observe, captureScreenshot, act, cartAdd, formSelectMany, TargetStaleError, extractCredentials, captchaGate, awaitVerification, finishProvisionSession, finishProvisionSessionWithPreparation, observedHostsForSession, currentProvisionUrl, stashSecretSlot, readSecretSlotValue, getSessionUserEmail, generatePassword, rememberRecipe, verifyActiveRecipePostcondition, verifySavedRecipePostcondition, captureAndPromoteSession, replayOperatorRecipe, emitProvisionMeasurement, checkoutShapeSignatureForSession, manualCardEntryBlockReason, } from "../bot/provision-session.js";
9
+ import { startProvisionSession, observe, captureScreenshot, observeQuery, act, cartAdd, cartClear, formSelectMany, TargetStaleError, extractCredentials, captchaGate, awaitVerification, finishProvisionSession, finishProvisionSessionWithPreparation, observedHostsForSession, currentProvisionUrl, stashSecretSlot, readSecretSlotValue, getSessionUserEmail, generatePassword, rememberRecipe, verifyActiveRecipePostcondition, verifySavedRecipePostcondition, captureAndPromoteSession, replayOperatorRecipe, emitProvisionMeasurement, checkoutShapeSignatureForSession, manualCardEntryBlockReason, } from "../bot/provision-session.js";
10
10
  import { signSkillForPublish } from "../skill-cli/signing.js";
11
11
  import { readRecipe, readRecipeForTask, readRecipeForCheckoutShape, readRecipeFromFile, renderOperatorRecipeHint, recipeEntryUrl, fillTemplate, operatorRecipeDomain, checkoutShapeKey, isCheckoutShapeKey, isSameRecipeDomain, OperatorVerbSchema, RecipeHoleSchema, PostconditionSchema, } from "../bot/operator-recipe.js";
12
12
  import { isMaskedDisplay } from "../bot/credential-shape.js";
13
13
  import { renderSkillHint, serviceSlugFromUrl } from "../bot/skill-hint.js";
14
14
  import { clientFromEnv, generateProvisionId } from "../skill-registry-client.js";
15
15
  import { openSessionStorage } from "../session.js";
16
- // PR2 — read the install-time inbox-read consent. Default-OFF: a missing flag
17
- // (older sessions, no session file) means "not consented", so awaitVerification
18
- // fails closed and hands the code request back to the user. Operator/housekeeper
19
- // deployments set consent_operator_inbox_otp=true.
16
+ import { servingAccountId } from "../session-guard.js";
17
+ // Read the install-time inbox-read preference. Inbox reads default on; an
18
+ // explicit false in the saved advanced configuration remains an opt-out.
20
19
  async function readInboxConsent() {
21
20
  try {
22
- const storage = await openSessionStorage();
23
- const data = await storage.read();
24
- return data?.consent_operator_inbox_otp === true;
21
+ const accountId = resolveAccountId();
22
+ if (accountId === undefined)
23
+ return true;
24
+ const data = await (await openSessionStorage()).read(accountId);
25
+ return data?.consent_operator_inbox_otp !== false;
25
26
  }
26
27
  catch {
27
- return false;
28
+ return true;
28
29
  }
29
30
  }
30
- // The account the install is bound to. Operator/CI runs set it in the env;
31
- // end-user installs bind it in session.json (connect writes it there, NOT to the
32
- // MCP config env). Reading env-only silently disabled BOTH the hint read and the
33
- // auto-promote write for every end-user install — resolve from the session file
34
- // as the fallback so the registry loop works off a normal `connect`.
35
- async function resolveAccountId() {
36
- const fromEnv = process.env.TRUSTY_SQUIRE_ACCOUNT_ID;
37
- if (fromEnv !== undefined && fromEnv.length > 0)
38
- return fromEnv;
39
- try {
40
- const data = await (await openSessionStorage()).read();
41
- const id = data?.account_id;
42
- return id !== undefined && id.length > 0 ? id : undefined;
43
- }
44
- catch {
45
- return undefined;
46
- }
31
+ // The account THIS server adopted, as published by its SessionGuard. Never
32
+ // re-resolve it here from TRUSTY_SQUIRE_ACCOUNT_ID or the store's
33
+ // current-account pointer: a server launched from a pre-pin config binds by
34
+ // fallback, and re-resolving after another account connects would hand this
35
+ // tool the wrong account — applying that account's consent setting and
36
+ // attributing registry reads/publishes to it. That is exactly the "acting as
37
+ // an account it never bound to" defect this work exists to remove.
38
+ function resolveAccountId() {
39
+ return servingAccountId();
47
40
  }
48
41
  // Best-effort: ask the registry for a known route for this service so the agent
49
42
  // drives on rails instead of ad-hoc. Returns undefined on any miss (no skill,
50
43
  // no registry configured, network error) — the agent just drives without it.
51
44
  async function resolveRouteHint(serviceUrl) {
52
45
  try {
53
- const accountId = await resolveAccountId();
46
+ const accountId = resolveAccountId();
54
47
  if (accountId === undefined || accountId.length === 0)
55
48
  return undefined;
56
49
  const client = clientFromEnv(accountId);
@@ -90,7 +83,7 @@ async function autoPromoteProvision(sessionId) {
90
83
  : "";
91
84
  return `rejected:${promoted.error_kind ?? "unknown"}${detail}`;
92
85
  }
93
- const accountId = await resolveAccountId();
86
+ const accountId = resolveAccountId();
94
87
  if (accountId === undefined || accountId.length === 0)
95
88
  return "produced:no_account";
96
89
  const client = clientFromEnv(accountId);
@@ -126,7 +119,7 @@ async function autoPromoteProvision(sessionId) {
126
119
  export async function publishRecipeToRegistry(file) {
127
120
  try {
128
121
  const recipe = await readRecipeFromFile(file);
129
- const accountId = await resolveAccountId();
122
+ const accountId = resolveAccountId();
130
123
  if (accountId === undefined || accountId.length === 0)
131
124
  return "skipped:no_account";
132
125
  const client = clientFromEnv(accountId);
@@ -159,7 +152,7 @@ export async function resolveRecipeForTask(verb, serviceUrl) {
159
152
  return await readRecipeForTask(verb, serviceUrl);
160
153
  }
161
154
  catch (localErr) {
162
- const accountId = await resolveAccountId();
155
+ const accountId = resolveAccountId();
163
156
  if (accountId === undefined || accountId.length === 0)
164
157
  throw localErr;
165
158
  const client = clientFromEnv(accountId);
@@ -183,7 +176,7 @@ export async function resolveCheckoutLegRecipe(verb, signature) {
183
176
  return await readRecipeForCheckoutShape(verb, signature);
184
177
  }
185
178
  catch {
186
- const accountId = await resolveAccountId();
179
+ const accountId = resolveAccountId();
187
180
  if (accountId === undefined || accountId.length === 0)
188
181
  return null;
189
182
  const client = clientFromEnv(accountId);
@@ -278,15 +271,15 @@ const startSchema = z.object({
278
271
  // Operate tasks that act AS the user (drive a gated app on an existing
279
272
  // account) set this so start fails closed to a connect hand-back if no live
280
273
  // Google session exists — rather than driving into a mid-task login wall.
281
- require_live_identity: z.boolean().optional(),
282
274
  });
283
- const OBSERVE_DELTA_CONTRACT = "Compact observations carry their elements in `el_table`: a TAB-delimited table whose FIRST line is the " +
275
+ const OBSERVE_DELTA_CONTRACT = "The following observation rules are V1-only (TRUSTY_SQUIRE_OBSERVE_V2=off or shadow). " +
276
+ "In V1 compact observations, elements are carried in `el_table`: a TAB-delimited table whose FIRST line is the " +
284
277
  "header (tab-joined column names, a subset of ref,label,tag,role,type,value_len,checked,href,testId," +
285
278
  "topmost,occluded_by, always starting ref,label,tag) and each following line is ONE element (tab-joined " +
286
279
  "cells in header order). An empty cell = that field is absent for that element; value_len is a number, " +
287
280
  "checked/topmost are true/false; a tab, newline, carriage-return or backslash inside a cell is " +
288
281
  "backslash-escaped (\\t \\n \\r \\\\). el_table is absent when the emit has no element rows. " +
289
- "stable refs remain reusable across observes while their controls still exist. " +
282
+ "In V1, stable refs remain reusable across observes while their controls still exist. " +
290
283
  "The first observe, a URL change, or high churn returns delta:false as a full resync: discard the prior " +
291
284
  "element map and rebuild it from this el_table (or from snapshot_file — the table may omit collapsed " +
292
285
  "chrome links, which the file keeps); a delta:false with NO snapshot_file already has the complete, " +
@@ -295,17 +288,40 @@ const OBSERVE_DELTA_CONTRACT = "Compact observations carry their elements in `el
295
288
  "in removed, and retain the remaining elements counted by unchanged. text_unchanged:true means reuse the " +
296
289
  "prior text because text is empty. snapshot_file points to the complete current snapshot (all elements, " +
297
290
  "with path). An empty delta (no el_table) means nothing changed, not an empty page. " +
298
- 'detail:"full" instead returns the legacy `elements` JSON array (every field), never el_table. ' +
291
+ 'In V1, detail:"full" instead returns the legacy `elements` JSON array (every field), never el_table. ' +
299
292
  "If a control you can see in `text`/the screenshot has NO row in el_table (a bare unlabeled clickable " +
300
293
  'div — e.g. some SPA "Add To Cart" buttons), it has no ref: click it with operate_act click/js_click ' +
301
294
  'target=`text="…"` or `css=…` (see operate_act). `click` respects actionability and throws if an overlay ' +
302
295
  "intercepts; dismiss the overlay or deliberately use `js_click`, which directly dispatches through a " +
303
- "transparent overlay. ";
296
+ "transparent overlay. Under default compact-v2, only refs and @labels from the current action map are " +
297
+ "accepted. A ref is a durable element fingerprint: it stays valid across acts and benign re-renders on the same " +
298
+ "document, so one observation can drive several acts. On opaque `stale_ref`, call operate_observe and choose a " +
299
+ "new ref; do not retry the old one or use V1 replacement candidates or locator fallback. ";
300
+ const COMPACT_V2_CONTRACT = "When format is `compact-v2`, use its action map: session_id is the continuation handle; `url` is the live page URL; `stage` is a finite enum; " +
301
+ "semantic carries the page title and primary visible heading; safe_table rows use [ref,role,facts?], where role is " +
302
+ "b=button,l=link,t=textbox,s=select,c=checkbox,r=radio,tb=tab,m=menuitem,f=file. ref is an opaque durable " +
303
+ "element handle. facts is a pipe-delimited string: an optional first unkeyed segment is the row's @label alias, " +
304
+ "a slug of its short label that operate_act also accepts as a target. " +
305
+ "The label is followed by any present s=<state bitset>, a=<action>, " +
306
+ "f=<field>, q=<choice position>/<choice total>, and x=<frame> segments. Fact-only rows begin with a keyed segment. " +
307
+ "State bitset codes are c=checked,u=unchecked,d=disabled,r=required; frame codes are x=s for a same-origin child " +
308
+ "and x=x for a cross-origin child, while an omitted x means the main frame. Actions are search,close,next,previous,submit," +
309
+ "continue,login,signup,add_to_cart,view_cart,checkout,payment,destructive; fields are email,password,username,name,phone,search,address," +
310
+ "city,region,postal,country,date,quantity,promo,payment. Short labels are included for viewport-prioritized controls; " +
311
+ "labels are exactly what the page renders. The row form omits field " +
312
+ "values purely as a size budget: read a value off the page with operate_screenshot, or with an explicitly " +
313
+ "selected V1 session. For a named product/control from the task, " +
314
+ "call operate_observe_query with those task words; it returns matching actionable refs with labels " +
315
+ "and code-owned facts. Use overflow.next_cursor to page. `detail:full` keeps the V2 format while V2 is enabled; " +
316
+ "set TRUSTY_SQUIRE_OBSERVE_V2=off for the legacy format. A delta:true delta retains the preceding V2 table, then upserts tuple rows in safe_table, " +
317
+ "removes refs in removed, and updates stage or semantic only when either changed. Omitted semantic title/heading remains from the preceding V2 page. " +
318
+ "A delta with none of those fields means the view is unchanged. ";
304
319
  export const provisionStartTool = {
305
320
  name: "operate_start",
306
321
  description: "Begin an interactive website task: opens a scoped browser on the " +
307
322
  "user's machine at service_url and returns the initial compact observation " +
308
- "{session_id, url, text, el_table, delta, snapshot_file}. " +
323
+ "(legacy el_table/delta or compact-v2 safe_table). " +
324
+ COMPACT_V2_CONTRACT +
309
325
  OBSERVE_DELTA_CONTRACT +
310
326
  "YOU are the planner — read the observation, then drive the signup, setup, or " +
311
327
  "checkout with operate_act (and operate_pay for a purchase), re-read with " +
@@ -327,7 +343,6 @@ export const provisionStartTool = {
327
343
  },
328
344
  allowed_hosts: { type: "array", items: { type: "string" } },
329
345
  extra_allowed_hosts: { type: "array", items: { type: "string" } },
330
- require_live_identity: { type: "boolean" },
331
346
  },
332
347
  },
333
348
  async handler(args, api) {
@@ -339,7 +354,6 @@ export const provisionStartTool = {
339
354
  consentInboxRead,
340
355
  ...(args.proxy !== undefined ? { proxyUrl: args.proxy } : {}),
341
356
  ...(extra.length > 0 ? { extraAllowedHosts: extra } : {}),
342
- ...(args.require_live_identity === true ? { requireLiveIdentity: true } : {}),
343
357
  ...(hint !== undefined ? { hint } : {}),
344
358
  // Thread the api-client so the captcha gate can spend a vaulted 2Captcha key.
345
359
  ...(api !== null ? { api } : {}),
@@ -348,24 +362,19 @@ export const provisionStartTool = {
348
362
  };
349
363
  const observeSchema = z.object({
350
364
  session_id: z.string().min(1),
351
- // Payload verbosity. Default "compact" (stable-ref element/text deltas plus a
352
- // complete snapshot pointer). Pass "full" for the legacy
353
- // screen+accessibility+full-field payload on a genuinely ambiguous step.
365
+ // Payload verbosity within the selected observation mode. In V2 both values
366
+ // return the compact action map; in V1, full requests the legacy expanded payload.
354
367
  detail: z.enum(["compact", "full"]).optional(),
355
368
  });
356
369
  export const provisionObserveTool = {
357
370
  name: "operate_observe",
358
- description: "Re-read the current page of an operate session. DEFAULT is a COMPACT " +
359
- "payload whose elements ride in `el_table` (a tab-delimited table; each row's " +
360
- "stable `ref` is the operate_act.target) with compact label/role/href/value_len " +
361
- "columns and `frame_origin` on child-frame controls; ordinary same- and " +
362
- "cross-origin frames are included, while known captcha challenge frames stay " +
363
- "behind the dedicated captcha flow. Path is retained only in snapshot_file, " +
364
- "and redundant screen/accessibility trees are omitted. " +
371
+ description: "Re-read the current page of an operate session. The default compact-v2 mode returns the compact " +
372
+ 'safe_table action map; `detail:"full"` stays in that format and does not restore legacy fields. ' +
373
+ COMPACT_V2_CONTRACT +
374
+ "Only explicitly selected V1 modes use el_table, reusable stable refs, locator fallbacks, snapshot_file, " +
375
+ "or the legacy expanded elements payload. " +
365
376
  OBSERVE_DELTA_CONTRACT +
366
- "Pass " +
367
- 'detail:"full" for the legacy screen+accessibility+full-field payload on a ' +
368
- "genuinely ambiguous step.",
377
+ 'In V1 only, pass detail:"full" for the legacy screen+accessibility+full-field payload on a genuinely ambiguous step.',
369
378
  inputSchema: observeSchema,
370
379
  jsonInputSchema: {
371
380
  type: "object",
@@ -396,18 +405,19 @@ const screenshotSchema = z.object({
396
405
  });
397
406
  export const provisionScreenshotTool = {
398
407
  name: "operate_screenshot",
399
- description: "Debugging tool: capture a screenshot of what the operate session's browser actually RENDERS — " +
408
+ description: "WARNING: EXPENSIVE — a screenshot is a full image and costs far more context than any " +
409
+ "observation. Reach for it ONLY when the DOM serialization (Compact V2 safe_table, " +
410
+ "operate_observe, operate_observe_query) is NOT sufficient to determine the page state; " +
411
+ "if the tables already tell you what the page is doing, do not take one. " +
412
+ "Debugging tool: capture a screenshot of what the operate session's browser actually RENDERS — " +
400
413
  "the whole page (default: viewport; full_page:true for the whole scrollable page) or ONE specific " +
401
414
  "frame in isolation via frame_index or frame_url_contains, so a cross-origin challenge iframe (a " +
402
415
  "3-D Secure ACS frame, a captcha) can be captured on its own even when it won't show clearly inside " +
403
- "a full-page shot. Use this when text/el_table from operate_observe isn't enough to tell what state " +
416
+ "a full-page shot. Use this when safe_table from Compact V2, or text/el_table from an explicitly " +
417
+ "selected V1 session, isn't enough to tell what state " +
404
418
  "a stuck page is actually in — a challenge that never advances, an unexpected layout, a captcha you " +
405
419
  "need to SEE. Read-only: never navigates, clicks, types, submits, or steals focus; it only reads " +
406
- "pixels. Money-fence: refuses (screenshot_unavailable_sealed_context) during an active card fill or " +
407
- "when a frame included in this capture still holds a sealed secret or card-shaped value. It fails closed " +
408
- "when that frame cannot be checked, while capture-time masking remains a second fence. A historical fill " +
409
- "does not block an isolated 3-D Secure/challenge frame or a page navigated away from the sealed form. " +
410
- "If you need to see a page WHILE a card fill is in progress, finish or cancel the payment step first.",
420
+ "pixels. The image is the page's real pixels, whatever the page is showing.",
411
421
  inputSchema: screenshotSchema,
412
422
  jsonInputSchema: {
413
423
  type: "object",
@@ -429,6 +439,50 @@ export const provisionScreenshotTool = {
429
439
  });
430
440
  },
431
441
  };
442
+ const observeQuerySchema = z.object({
443
+ session_id: z.string().min(1),
444
+ // This string is matched only inside the live session; returned rows remain
445
+ // the compact-v2 enum-only action map.
446
+ query: z.string().max(160).default(""),
447
+ role: z
448
+ .enum(["button", "link", "textbox", "select", "checkbox", "radio", "tab", "menuitem", "file"])
449
+ .optional(),
450
+ cursor: z.string().max(1_024).optional(),
451
+ });
452
+ export const provisionObserveQueryTool = {
453
+ name: "operate_observe_query",
454
+ description: "Page compact-v2 overflow or named-control lookup. Supply the product/control words already in the task; " +
455
+ "matching happens only inside the live browser and returns actionable opaque refs plus finite role/state/action " +
456
+ "enums. Use overflow.next_cursor to page controls, or hint_overflow.next_cursor with an empty query to page " +
457
+ "trusted start routing metadata; never read a snapshot file.",
458
+ inputSchema: observeQuerySchema,
459
+ jsonInputSchema: {
460
+ type: "object",
461
+ required: ["session_id"],
462
+ properties: {
463
+ session_id: { type: "string" },
464
+ query: { type: "string" },
465
+ role: {
466
+ type: "string",
467
+ enum: [
468
+ "button",
469
+ "link",
470
+ "textbox",
471
+ "select",
472
+ "checkbox",
473
+ "radio",
474
+ "tab",
475
+ "menuitem",
476
+ "file",
477
+ ],
478
+ },
479
+ cursor: { type: "string" },
480
+ },
481
+ },
482
+ async handler(args) {
483
+ return await observeQuery(args.session_id, args.query, args.role, args.cursor);
484
+ },
485
+ };
432
486
  // Shared credential destination shape. Both operate_act(kind:"extract") and
433
487
  // the legacy operate_extract alias validate and execute this exact contract.
434
488
  // The credentials terminal also reuses it below.
@@ -459,7 +513,8 @@ const storeJsonProps = {
459
513
  const formSelectionsSchema = z
460
514
  .record(z.string().min(1).max(200), z.string().min(1).max(4096))
461
515
  .refine((value) => Object.keys(value).length > 0, "Provide at least one selection")
462
- .refine((value) => Object.keys(value).length <= 12, "At most 12 selections per call");
516
+ .refine((value) => Object.keys(value).length <= 12, "At most 12 selections per call")
517
+ .describe("Map each current Compact V2 @e: ref or @label, or V1 observed label/ref, to its visible option text.");
463
518
  const actSchema = z
464
519
  .object({
465
520
  session_id: z.string().min(1),
@@ -479,6 +534,7 @@ const actSchema = z
479
534
  "scroll",
480
535
  "upload",
481
536
  "cart_add",
537
+ "cart_clear",
482
538
  "select_many",
483
539
  "extract",
484
540
  "solve_captcha",
@@ -496,6 +552,7 @@ const actSchema = z
496
552
  path: z.string().min(1).max(4096).optional(),
497
553
  url: z.string().url().optional(),
498
554
  key: z.string().min(1).max(40).optional(),
555
+ provider: z.enum(["google", "github"]).optional(),
499
556
  // allow_host: a bare hostname to cross into mid-session.
500
557
  host: z.string().min(1).max(253).optional(),
501
558
  // type_secret: the sealed slot whose value to type into `target`.
@@ -661,6 +718,7 @@ const ACTION_KINDS = [
661
718
  "scroll",
662
719
  "upload",
663
720
  "cart_add",
721
+ "cart_clear",
664
722
  "select_many",
665
723
  "extract",
666
724
  "solve_captcha",
@@ -682,13 +740,21 @@ const ACTION_REPAIR_BY_KIND = {
682
740
  safe_alternative: "Retry cart_add with the same stable idempotency_key for the same product_identity and options_hash. Do not replace it with click: cart_add reserves the mutation and exact-line post-verifies the cart." +
683
741
  manualCardRecovery,
684
742
  },
743
+ cart_clear: {
744
+ example: {
745
+ session_id: "<session_id>",
746
+ kind: "cart_clear",
747
+ },
748
+ safe_alternative: "Retry cart_clear with only session_id — it empties the cart so a following cart_add reaches a known quantity, regardless of what accumulated in the persistent browser profile's cart across earlier sessions." +
749
+ manualCardRecovery,
750
+ },
685
751
  select_many: {
686
752
  example: {
687
753
  session_id: "<session_id>",
688
754
  kind: "select_many",
689
- selections: { "Observed field label": "Visible option label" },
755
+ selections: { "@e:<current-handle>": "Visible option label" },
690
756
  },
691
- safe_alternative: "Retry select_many with selections in the intended order. It selects sequentially, re-observes after each success, and returns partial results; do not replace it with parallel select calls." +
757
+ safe_alternative: "Retry select_many with selections in the intended order. Compact V2 requires current @e: handles; V1 accepts observed labels or refs. It selects sequentially, re-observes after each success, and returns partial results; do not replace it with parallel select calls." +
692
758
  manualCardRecovery,
693
759
  },
694
760
  extract: {
@@ -698,7 +764,7 @@ const ACTION_REPAIR_BY_KIND = {
698
764
  into_slot: "sealed_secret",
699
765
  secret_label: "API key",
700
766
  },
701
- safe_alternative: "Retry extract after navigating to the credential page. Use into_slot with optional secret_label to seal a value without exposing it, or store with a service to vault it; never put credential values in arguments or seal a still-masked value." +
767
+ safe_alternative: "Retry extract after navigating to the credential page. Use into_slot with optional secret_label to hold a value in a session slot for a later type_secret, or store with a service to vault it; never put credential values in arguments." +
702
768
  manualCardRecovery,
703
769
  },
704
770
  solve_captcha: {
@@ -715,9 +781,9 @@ const ACTION_REPAIR_BY_KIND = {
715
781
  kind: "await_verification",
716
782
  sender: "service.example",
717
783
  into_slot: "otp",
718
- grant_inbox_consent: false,
784
+ grant_inbox_consent: true,
719
785
  },
720
- safe_alternative: "Retry await_verification with sender scoped to the service and prefer into_slot so the OTP stays sealed. Leave grant_inbox_consent false unless the user explicitly agrees; only retry with grant_inbox_consent:true after that explicit yes." +
786
+ safe_alternative: "Retry await_verification with sender scoped to the service and prefer into_slot so the OTP stays sealed. Inbox reading is on by default; pass grant_inbox_consent:false to opt out for this session." +
721
787
  manualCardRecovery,
722
788
  },
723
789
  login_prepare_signup: {
@@ -818,9 +884,17 @@ function buildAction(args) {
818
884
  case "js_click":
819
885
  return { kind: "js_click", target: need(args.target, "target") };
820
886
  case "oauth_click":
821
- return { kind: "oauth_click", target: need(args.target, "target") };
887
+ return {
888
+ kind: "oauth_click",
889
+ target: need(args.target, "target"),
890
+ ...(args.provider === undefined ? {} : { provider: args.provider }),
891
+ };
822
892
  case "oauth_login":
823
- return { kind: "oauth_login", target: need(args.target, "target") };
893
+ return {
894
+ kind: "oauth_login",
895
+ target: need(args.target, "target"),
896
+ ...(args.provider === undefined ? {} : { provider: args.provider }),
897
+ };
824
898
  case "type":
825
899
  return {
826
900
  kind: "type",
@@ -867,6 +941,7 @@ function buildAction(args) {
867
941
  case "upload":
868
942
  return { kind: "upload", target: need(args.target, "target"), path: need(args.path, "path") };
869
943
  case "cart_add":
944
+ case "cart_clear":
870
945
  case "select_many":
871
946
  case "extract":
872
947
  case "solve_captcha":
@@ -880,29 +955,34 @@ function buildAction(args) {
880
955
  async function handleCartAdd(args) {
881
956
  return await cartAdd(args.session_id, args.product_identity, args.options_hash, args.idempotency_key);
882
957
  }
958
+ async function handleCartClear(args) {
959
+ return await cartClear(args.session_id);
960
+ }
883
961
  async function handleFormSelectMany(args) {
884
962
  return await formSelectMany(args.session_id, args.selections);
885
963
  }
886
964
  async function handleExtract(args, api) {
887
965
  const extracted = await extractCredentials(args.session_id);
888
- // Sealed transfer: capture the primary secret into a session-local slot and
889
- // return ONLY a masked handle. The value never reaches the host. A later
890
- // type_secret enters it into another site's form. Mutually exclusive with
891
- // store (a slotted secret is being shuttled, not vaulted, in this call).
966
+ // Slot transfer: capture the primary secret into a session-local slot so a
967
+ // later type_secret can enter it into another site's form without the host
968
+ // having to relay it. Mutually exclusive with store (a slotted secret is
969
+ // being shuttled, not vaulted, in this call).
892
970
  if (args.into_slot !== undefined) {
893
971
  const values = extracted.credentials;
894
- // A still-masked display (Google's OAuth secret shows "GOCSPX-••••" with a
895
- // copy button) must NOT be sealed — the slot would hold junk. Reject any
896
- // value with mask glyphs (canonical isMaskedDisplay) or the truncated-capture
897
- // marker, and prefer a full value over the masked api_key when the page has both.
898
972
  const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
899
- const candidates = Object.entries(values).filter(([k, v]) => !k.endsWith("_truncated") && typeof v === "string" && v.length >= 8 && !isMaskedDisplay(v));
973
+ const candidates = Object.entries(values).filter(([k, v]) => !k.endsWith("_truncated") && typeof v === "string" && v.length >= 8);
974
+ // A value that still LOOKS masked is never refused — it is simply ranked
975
+ // last, so a fully revealed sibling wins when the page shows both.
976
+ const ranked = [
977
+ ...candidates.filter(([, v]) => !isMaskedDisplay(v)),
978
+ ...candidates.filter(([, v]) => isMaskedDisplay(v)),
979
+ ];
900
980
  // When the page shows several credentials (Google's client ID + secret),
901
981
  // a secret_label picks the right one by field name; otherwise take the
902
982
  // first full value. Falling back avoids a hard fail when the label misses.
903
983
  const wantKey = args.secret_label !== undefined ? norm(args.secret_label) : null;
904
- const matched = wantKey !== null ? candidates.find(([k]) => norm(k).includes(wantKey)) : undefined;
905
- const full = (matched ?? candidates[0])?.[1];
984
+ const matched = wantKey !== null ? ranked.find(([k]) => norm(k).includes(wantKey)) : undefined;
985
+ const full = (matched ?? ranked[0])?.[1];
906
986
  if (typeof full !== "string" || full.length === 0) {
907
987
  return {
908
988
  session_id: extracted.session_id,
@@ -910,21 +990,17 @@ async function handleExtract(args, api) {
910
990
  candidate_count: extracted.candidate_count,
911
991
  sealed: false,
912
992
  slot: null,
913
- blocked_reason: "the secret is still masked/hidden — reveal it first (click the " +
914
- 'show/reveal/copy control near the key), then operate_act { kind: "extract" } again',
993
+ blocked_reason: "no credential value was found on this page — navigate to the keys/settings " +
994
+ 'page, then operate_act { kind: "extract" } again',
915
995
  };
916
996
  }
917
997
  const handle = stashSecretSlot(args.session_id, args.into_slot, full);
918
- // Strip raw credential VALUES from the response — host gets the handle only.
919
998
  return {
920
999
  session_id: extracted.session_id,
921
1000
  url: extracted.url,
922
1001
  candidate_count: extracted.candidate_count,
923
1002
  sealed: true,
924
1003
  slot: handle,
925
- ...(extracted.blocked_reason !== undefined
926
- ? { blocked_reason: extracted.blocked_reason }
927
- : {}),
928
1004
  };
929
1005
  }
930
1006
  if (args.store === undefined || Object.keys(extracted.credentials).length === 0) {
@@ -943,18 +1019,19 @@ async function handleAwaitVerification(args) {
943
1019
  return await awaitVerification(args.session_id, {
944
1020
  ...(args.sender !== undefined ? { sender: args.sender } : {}),
945
1021
  ...(args.into_slot !== undefined ? { intoSlot: args.into_slot } : {}),
946
- ...(args.grant_inbox_consent === true ? { grantConsent: true } : {}),
1022
+ ...(args.grant_inbox_consent !== undefined ? { grantConsent: args.grant_inbox_consent } : {}),
947
1023
  });
948
1024
  }
949
1025
  export const provisionActTool = {
950
1026
  name: "operate_act",
951
- description: "Take one action in an operate session, then return the resulting " +
952
- "observation. kinds: click (target=element ref, preferably an el_table row's ref), " +
1027
+ description: "Take one action in an operate session. Under compact-v2 the default follow-up is a compact delta " +
1028
+ "when its action map is unchanged; call operate_observe or operate_observe_query when you need a new map. " +
1029
+ "kinds: click (target=element ref, preferably a safe_table row's ref), " +
953
1030
  "type (target + text; model-supplied card-number-shaped text is refused — card payment " +
954
1031
  "must use operate_pay, which fills a vaulted card without exposing it to the model), " +
955
1032
  "" +
956
- "TARGET FALLBACK (clicking or typing only) — when a control you can SEE (in the " +
957
- "observation text or screenshot) has NO ref in el_table (a bare click-handler " +
1033
+ "V1-ONLY TARGET FALLBACK (clicking or typing only) — when a control you can SEE (in the " +
1034
+ "V1 observation text or screenshot) has NO ref in el_table (a bare click-handler " +
958
1035
  '<div> with no role/label, e.g. a SPA "Add To Cart"), pass target as a locator ' +
959
1036
  'instead of a ref: `text="Add To Cart"` (a matching clickable or typeable ' +
960
1037
  "element, case-insensitive, hidden descendants ignored, open shadow roots " +
@@ -965,8 +1042,9 @@ export const provisionActTool = {
965
1042
  "exact text= or a css= selector). `click` is actionability-checked and throws " +
966
1043
  "if an overlay intercepts; dismiss the overlay or deliberately use `js_click`, " +
967
1044
  "the explicit direct DOM dispatch that fires through a transparent overlay. " +
968
- "Prefer a real ref when one exists; reach for " +
969
- "text=/css= only when none does. " +
1045
+ "Prefer a real ref when one exists; reach for text=/css= only when none does. Compact V2 never " +
1046
+ "accepts locator fallback: it requires an opaque handle from the current snapshot/page/generation-" +
1047
+ "scoped action map, with membership as the sole action boundary. " +
970
1048
  "Observed child-frame refs and locator matches retain their frame origin. " +
971
1049
  "Same-registrable-domain frames are reachable; other frame actions must pass " +
972
1050
  "the goto/allow_host domain scope, opaque frames are refused, and type_secret " +
@@ -997,14 +1075,20 @@ export const provisionActTool = {
997
1075
  "already signed into). " +
998
1076
  "cart_add (product_identity + options_hash + idempotency_key — reserve an " +
999
1077
  "idempotent cart mutation, exact-line post-verify it, and return its postcondition), " +
1078
+ "cart_clear (no arguments beyond session_id — empty the cart so a following cart_add " +
1079
+ "reaches a KNOWN quantity; the browser profile persists across sessions, so a cart " +
1080
+ "can carry quantity over from an earlier run. Call cart_clear before the first cart_add " +
1081
+ "whenever the cart's starting quantity is not already known to be zero), " +
1000
1082
  "select_many (ordered selections map — select sequentially, re-observe after " +
1001
- "each success, and retain partial results), extract (into_slot/secret_label/store — " +
1083
+ "each success, and retain partial results; Compact V2 keys are current safe_table @e: refs or @labels, " +
1084
+ "while V1 keys may be observed labels or refs), extract (into_slot/secret_label/store — " +
1002
1085
  "reveal masked keys and extract credentials from the current page, sealed slot or vaulted), " +
1003
1086
  "solve_captcha (detect and drive the in-session captcha gate; settled=false carries a " +
1004
1087
  "needs_user{gate,message,remedy} — FAIL FAST and relay it to the user), " +
1005
1088
  "await_verification (sender/into_slot/grant_inbox_consent — read the user's OWN inbox " +
1006
- "through their signed-in browser session for an email verification code/link, sealed " +
1007
- "into a slot with into_slot). Sealed username/password login lifecycle, never exposing raw " +
1089
+ "through their signed-in browser session for an email verification code/link; pass into_slot " +
1090
+ "to seal the code into a slot and fill it with type_secret instead of handling it yourself). " +
1091
+ "Sealed username/password login lifecycle, never exposing raw " +
1008
1092
  "values: login_prepare_signup (login_slot/password_slot/password_length — seal the user's " +
1009
1093
  "captured email and a generated strong password into session slots; fill the signup form " +
1010
1094
  "with type_secret using the returned slots), login_store_signup (service + login_hosts, " +
@@ -1018,12 +1102,15 @@ export const provisionActTool = {
1018
1102
  "Squire-known address, contact, product_query, or quantity input; type_secret and " +
1019
1103
  "operate_pay record credential and card provenance from their sealed sources. " +
1020
1104
  "When repairing a replay fallback field, pass its replay_step_index and replay_hole. " +
1021
- "Stable target refs remain reusable while their element exists. If an @e: ref is stale, " +
1022
- "the response is {status:target_stale, replacement_candidates, retry_policy:do_not_retry_old_ref}; " +
1023
- "call operate_observe and choose a new ref instead of retrying the old one. " +
1105
+ "Under default compact-v2, target is either an @e: ref from the current action map or the @label alias " +
1106
+ "of exactly one of its rows; a @label matching several rows returns ambiguous_target listing their refs, never " +
1107
+ "a guess. Refs survive acts and benign re-renders on the same document; a real navigation retires them. On " +
1108
+ "opaque `stale_ref`, call operate_observe and choose a new ref; do not retry the old one. " +
1109
+ "In V1, stable target refs remain reusable while their element exists; a stale @e: " +
1110
+ "ref returns {status:target_stale, replacement_candidates, retry_policy:do_not_retry_old_ref}. " +
1024
1111
  'detail (default "compact") controls the returned payload: "none" skips it ' +
1025
1112
  "entirely for chained fills (then operate_observe before the next ref action), " +
1026
- '"full" returns the legacy screen+accessibility payload. ' +
1113
+ 'in V1, "full" returns the legacy screen+accessibility payload. ' +
1027
1114
  "For legible product/variant hints on a cart-affecting action, pass product_identity and options_hash together; returned checkout_state is best-effort informational state and never a payment charge input. " +
1028
1115
  OBSERVE_DELTA_CONTRACT,
1029
1116
  inputSchema: actSchema,
@@ -1050,6 +1137,7 @@ export const provisionActTool = {
1050
1137
  "scroll",
1051
1138
  "upload",
1052
1139
  "cart_add",
1140
+ "cart_clear",
1053
1141
  "select_many",
1054
1142
  "extract",
1055
1143
  "solve_captcha",
@@ -1065,6 +1153,11 @@ export const provisionActTool = {
1065
1153
  path: { type: "string" },
1066
1154
  url: { type: "string" },
1067
1155
  key: { type: "string" },
1156
+ provider: {
1157
+ type: "string",
1158
+ enum: ["google", "github"],
1159
+ description: "OAuth provider for oauth_login/oauth_click. Declare it for JavaScript-driven controls whose label or destination does not identify the provider.",
1160
+ },
1068
1161
  host: { type: "string" },
1069
1162
  slot: { type: "string" },
1070
1163
  product_identity: { type: "string", minLength: 1 },
@@ -1075,7 +1168,7 @@ export const provisionActTool = {
1075
1168
  minProperties: 1,
1076
1169
  maxProperties: 12,
1077
1170
  additionalProperties: { type: "string" },
1078
- description: "Map each observed field label or @e: ref to its visible option text.",
1171
+ description: "Map each current Compact V2 @e: ref or @label, or V1 observed label/ref, to its visible option text.",
1079
1172
  },
1080
1173
  into_slot: { type: "string" },
1081
1174
  secret_label: { type: "string" },
@@ -1111,52 +1204,57 @@ export const provisionActTool = {
1111
1204
  },
1112
1205
  schemaRepair: actionSchemaRepair,
1113
1206
  async handler(args, api) {
1114
- switch (args.kind) {
1115
- case "cart_add":
1116
- return await handleCartAdd(args);
1117
- case "select_many":
1118
- return await handleFormSelectMany(args);
1119
- case "extract":
1120
- return await handleExtract(args, api);
1121
- case "solve_captcha":
1122
- return await handleCaptcha(args);
1123
- case "await_verification":
1124
- return await handleAwaitVerification(args);
1125
- case "login_prepare_signup":
1126
- return await handlePrepareLogin(args);
1127
- case "login_store_signup":
1128
- return await handleStoreLogin(args, api);
1129
- case "login_load_saved":
1130
- return await handleSealVaultCredential({
1131
- ...args,
1132
- fields: args.fields ?? ["login", "password"],
1133
- slot_prefix: args.slot_prefix ?? "vault",
1134
- }, api);
1135
- }
1136
- // Keep the defense-in-depth guard in act() for internal/replay callers,
1137
- // while this public tool surface makes the safe recovery explicit at the
1138
- // exact point a small model tried a forbidden manual PAN entry.
1139
- if (args.kind === "type") {
1140
- const reason = manualCardEntryBlockReason(args.text ?? "");
1141
- if (reason !== null) {
1142
- return {
1143
- status: "manual_card_entry_refused",
1144
- reason,
1145
- safe_alternative: "operate_pay",
1146
- missing_prerequisite: "verified_cart_total",
1147
- };
1207
+ const result = await (async () => {
1208
+ switch (args.kind) {
1209
+ case "cart_add":
1210
+ return await handleCartAdd(args);
1211
+ case "cart_clear":
1212
+ return await handleCartClear(args);
1213
+ case "select_many":
1214
+ return await handleFormSelectMany(args);
1215
+ case "extract":
1216
+ return await handleExtract(args, api);
1217
+ case "solve_captcha":
1218
+ return await handleCaptcha(args);
1219
+ case "await_verification":
1220
+ return await handleAwaitVerification(args);
1221
+ case "login_prepare_signup":
1222
+ return await handlePrepareLogin(args);
1223
+ case "login_store_signup":
1224
+ return await handleStoreLogin(args, api);
1225
+ case "login_load_saved":
1226
+ return await handleSealVaultCredential({
1227
+ ...args,
1228
+ fields: args.fields ?? ["login", "password"],
1229
+ slot_prefix: args.slot_prefix ?? "vault",
1230
+ }, api);
1148
1231
  }
1149
- }
1150
- try {
1151
- return await act(args.session_id, buildAction(args), args.detail ?? "compact", args.product_identity !== undefined && args.options_hash !== undefined
1152
- ? { productIdentity: args.product_identity, optionsHash: args.options_hash }
1153
- : undefined);
1154
- }
1155
- catch (err) {
1156
- if (err instanceof TargetStaleError)
1157
- return err.result;
1158
- throw err;
1159
- }
1232
+ // Keep the defense-in-depth guard in act() for internal/replay callers,
1233
+ // while this public tool surface makes the safe recovery explicit at the
1234
+ // exact point a small model tried a forbidden manual PAN entry.
1235
+ if (args.kind === "type") {
1236
+ const reason = manualCardEntryBlockReason(args.text ?? "");
1237
+ if (reason !== null) {
1238
+ return {
1239
+ status: "manual_card_entry_refused",
1240
+ reason,
1241
+ safe_alternative: "operate_pay",
1242
+ missing_prerequisite: "verified_cart_total",
1243
+ };
1244
+ }
1245
+ }
1246
+ try {
1247
+ return await act(args.session_id, buildAction(args), args.detail ?? "compact", args.product_identity !== undefined && args.options_hash !== undefined
1248
+ ? { productIdentity: args.product_identity, optionsHash: args.options_hash }
1249
+ : undefined);
1250
+ }
1251
+ catch (err) {
1252
+ if (err instanceof TargetStaleError)
1253
+ return err.result;
1254
+ throw err;
1255
+ }
1256
+ })();
1257
+ return result;
1160
1258
  },
1161
1259
  };
1162
1260
  // The former standalone tool objects in the following sections are NOT part of
@@ -1194,13 +1292,12 @@ export const operateCartAddTool = {
1194
1292
  };
1195
1293
  const formSelectManySchema = z.object({
1196
1294
  session_id: z.string().min(1),
1197
- // A label/ref → visible option map. Labels deliberately match the existing
1198
- // select targeting grammar, while refs remain useful when labels collide.
1199
1295
  selections: formSelectionsSchema,
1200
1296
  });
1201
1297
  export const operateFormSelectManyTool = {
1202
1298
  name: "operate_form_select_many",
1203
- description: "Select several related form options sequentially from a label/ref-to-option map. " +
1299
+ description: "Select several related form options sequentially from a target-to-option map. " +
1300
+ "Compact V2 requires current @e: handles; V1 accepts observed labels or refs. " +
1204
1301
  "Selections run in order; after every successful selection the browser is re-observed " +
1205
1302
  "before the next one resolves, so variant changes cannot poison later refs. Each field " +
1206
1303
  "reports selected or failed independently; successful selections are not rolled back when " +
@@ -1217,7 +1314,7 @@ export const operateFormSelectManyTool = {
1217
1314
  minProperties: 1,
1218
1315
  maxProperties: 12,
1219
1316
  additionalProperties: { type: "string" },
1220
- description: "Map each observed field label or @e: ref to its visible option text.",
1317
+ description: "Map each current Compact V2 @e: ref or @label, or V1 observed label/ref, to its visible option text.",
1221
1318
  },
1222
1319
  },
1223
1320
  },
@@ -1241,7 +1338,8 @@ export const provisionExtractTool = {
1241
1338
  "the page is a login wall / anti-bot interstitial with NO credential present " +
1242
1339
  "(do not treat the empty result as a real key) — drive an interactive login " +
1243
1340
  "or hand back to the user. Call when you have navigated to the keys page. " +
1244
- "With `into_slot`, a still-masked value is refused (reveal it first); pass " +
1341
+ "With `into_slot`, a value that still looks masked is ranked behind a fully " +
1342
+ "revealed sibling but never refused; pass " +
1245
1343
  '`secret_label` (e.g. "client secret") to pick the right one when the page ' +
1246
1344
  "shows several credentials.",
1247
1345
  inputSchema: extractSchema,
@@ -1311,8 +1409,9 @@ export const provisionAwaitVerificationTool = {
1311
1409
  "operate_act and continue. The session stays live; this is a resumable " +
1312
1410
  "hand-back, not a failure. Scoped search-and-extract — reads only the matching " +
1313
1411
  "recent mail, never the whole inbox. If a needs_user(verification_code) says " +
1314
- "inbox reading isn't consented, ask the user; on an explicit yes retry with " +
1315
- "grant_inbox_consent:true.",
1412
+ "inbox reading is disabled, ask the user; on an explicit yes retry with " +
1413
+ "grant_inbox_consent:true. Pass grant_inbox_consent:false to disable inbox reading " +
1414
+ "for the current session.",
1316
1415
  inputSchema: verifySchema,
1317
1416
  jsonInputSchema: {
1318
1417
  type: "object",
@@ -1394,6 +1493,7 @@ const finishOutcomeSchema = z.union([
1394
1493
  }),
1395
1494
  ]);
1396
1495
  async function handleFinishOutcome(sessionId, outcome, api) {
1496
+ let successfulOutcome = false;
1397
1497
  const { finish, prepared } = await finishProvisionSessionWithPreparation(sessionId, async () => {
1398
1498
  if (outcome.kind === "credentials") {
1399
1499
  const extracted = await extractCredentials(sessionId);
@@ -1404,7 +1504,8 @@ async function handleFinishOutcome(sessionId, outcome, api) {
1404
1504
  const autoPromote = stored !== null && blocked === undefined
1405
1505
  ? await autoPromoteProvision(sessionId)
1406
1506
  : undefined;
1407
- emitProvisionMeasurement(sessionId, stored !== null && blocked === undefined ? "success" : "fail");
1507
+ successfulOutcome = stored !== null && blocked === undefined;
1508
+ emitProvisionMeasurement(sessionId, successfulOutcome ? "success" : "fail");
1408
1509
  return {
1409
1510
  kind: "credentials",
1410
1511
  candidate_count: extracted.candidate_count,
@@ -1417,14 +1518,18 @@ async function handleFinishOutcome(sessionId, outcome, api) {
1417
1518
  ? undefined
1418
1519
  : ((await verifyActiveRecipePostcondition(sessionId, outcome.verify_recipe)) ??
1419
1520
  (await verifySavedRecipePostcondition(sessionId, await readRecipe(outcome.verify_recipe))));
1420
- emitProvisionMeasurement(sessionId, verified === undefined || verified.confirmed ? "success" : "fail");
1521
+ successfulOutcome =
1522
+ outcome.verify_recipe === undefined
1523
+ ? outcome.data?.confirmed === "true"
1524
+ : verified?.confirmed === true;
1525
+ emitProvisionMeasurement(sessionId, successfulOutcome ? "success" : "fail");
1421
1526
  return {
1422
1527
  kind: "result",
1423
1528
  summary: (outcome.summary ?? "").slice(0, 4000),
1424
1529
  ...(verified !== undefined ? { verified } : {}),
1425
1530
  ...(outcome.data !== undefined ? { data: outcome.data } : {}),
1426
1531
  };
1427
- });
1532
+ }, () => successfulOutcome);
1428
1533
  return { ...prepared, url: finish.url };
1429
1534
  }
1430
1535
  // Not part of the default MCP tool surface (dropped from OPERATE_TOOLS). Kept
@@ -1445,7 +1550,8 @@ export const provisionFinishTaskTool = {
1445
1550
  "operate_extract's store), for signups/key-provisioning. kind='result' " +
1446
1551
  "reports a `summary` (+ optional `data` map) for any other task — a design " +
1447
1552
  "review's findings, extracted data, or 'task done' (put confirmed:true in " +
1448
- "data). Use operate_finish instead to abort without an outcome. For a soft " +
1553
+ "data only after a clean success; false or omitted preserves prior login state). " +
1554
+ "Use operate_finish instead to abort without an outcome. For a soft " +
1449
1555
  "no-match with kind='result' (for example, no exact or authentic item in " +
1450
1556
  "stock), first relay the closest candidates and why they were rejected, then " +
1451
1557
  "let the user choose a substitute, broader search, or another site before " +
@@ -1491,9 +1597,11 @@ export const provisionFinishTool = {
1491
1597
  description: "Finish an operate task and close its session. outcome.kind='none' closes " +
1492
1598
  "without a reported outcome (and remains the default for compatibility); " +
1493
1599
  "'credentials' extracts and vault-stores a credential using required `store`; " +
1494
- "'result' reports required `summary` or `data` and can verify_recipe before closing. " +
1495
- "All credential values remain server-side; after Chrome closes, an eligible clean profile " +
1496
- "may return to the closed warm slot for the next task.",
1600
+ "'result' reports required `summary` or `data`; it succeeds only when verify_recipe " +
1601
+ "confirms or data.confirmed is true. " +
1602
+ "All credential values remain server-side; after Chrome closes, its private per-session " +
1603
+ "profile is destroyed. An explicit successful outcome atomically saves portable login state " +
1604
+ "for a later task; no-outcome and failed closes preserve the prior snapshot.",
1497
1605
  inputSchema: finishSchema,
1498
1606
  jsonInputSchema: {
1499
1607
  type: "object",
@@ -1661,7 +1769,6 @@ const useSchema = z
1661
1769
  verb: OperatorVerbSchema.optional(),
1662
1770
  service_url: z.string().url().optional(),
1663
1771
  params: z.record(z.string().max(2000)).optional(),
1664
- require_live_identity: z.boolean().optional(),
1665
1772
  // After the host repairs one missed step, resume the same live session at
1666
1773
  // replay.next_index instead of starting over.
1667
1774
  session_id: z.string().min(1).optional(),
@@ -1721,7 +1828,6 @@ export const provisionUseTool = {
1721
1828
  verb: { type: "string", enum: OperatorVerbSchema.options },
1722
1829
  service_url: { type: "string" },
1723
1830
  params: { type: "object" },
1724
- require_live_identity: { type: "boolean" },
1725
1831
  session_id: { type: "string" },
1726
1832
  resume_from: { type: "integer" },
1727
1833
  leg: { type: "string", enum: ["checkout"] },
@@ -1748,7 +1854,6 @@ export const provisionUseTool = {
1748
1854
  const cold = await startProvisionSession({
1749
1855
  serviceUrl: args.service_url,
1750
1856
  consentInboxRead: await readInboxConsent(),
1751
- ...(args.require_live_identity === true ? { requireLiveIdentity: true } : {}),
1752
1857
  ...(api !== null ? { api } : {}),
1753
1858
  });
1754
1859
  if (cold.needs_user !== undefined)
@@ -1789,7 +1894,6 @@ export const provisionUseTool = {
1789
1894
  const cold = await startProvisionSession({
1790
1895
  serviceUrl: args.service_url,
1791
1896
  consentInboxRead: await readInboxConsent(),
1792
- ...(args.require_live_identity === true ? { requireLiveIdentity: true } : {}),
1793
1897
  ...(api !== null ? { api } : {}),
1794
1898
  });
1795
1899
  if (cold.needs_user !== undefined)
@@ -1814,7 +1918,6 @@ export const provisionUseTool = {
1814
1918
  consentInboxRead,
1815
1919
  ...(recipe.allowed_hosts.length > 0 ? { extraAllowedHosts: recipe.allowed_hosts } : {}),
1816
1920
  hint: renderOperatorRecipeHint(recipe),
1817
- ...(args.require_live_identity === true ? { requireLiveIdentity: true } : {}),
1818
1921
  ...(api !== null ? { api } : {}),
1819
1922
  });
1820
1923
  if (started.needs_user !== undefined)
@@ -2128,13 +2231,14 @@ export const operateLoginTool = {
2128
2231
  // operate_act's kinds and operate_recipe_save/run delegate to, and as direct handles
2129
2232
  // for tests that pin down that folded behavior.
2130
2233
  // operate_screenshot (2026-08-23) is a deliberate, narrow addition to this cut: a
2131
- // read-only debugging instrument (page/frame pixels, money-fence redacted) with no
2234
+ // read-only debugging instrument (the page's or one frame's real pixels) with no
2132
2235
  // alias/kind it could fold into — operate_act's kinds all DO something; this only
2133
2236
  // looks.
2134
2237
  export const OPERATE_TOOLS = [
2135
2238
  provisionStartTool,
2136
2239
  provisionObserveTool,
2137
2240
  provisionScreenshotTool,
2241
+ provisionObserveQueryTool,
2138
2242
  provisionActTool,
2139
2243
  operateRecipeSaveTool,
2140
2244
  operateRecipeRunTool,