From 82e18e67aab72a55a48dc6fc931ed72588e584b3 Mon Sep 17 00:00:00 2001 From: masnwilliams <43387599+masnwilliams@users.noreply.github.com> Date: Fri, 13 Nov 2026 17:08:31 +0000 Subject: [PATCH 1/2] Format files from the 0.148.0 coverage update --- src/lib/mcp/tools/config-registry.test.ts | 69 ++++++++++--------- src/lib/mcp/tools/proxies.test.ts | 5 +- src/lib/mcp/tools/proxies.ts | 4 +- src/lib/mcp/tools/vault-managed-auth.test.ts | 4 +- src/lib/mcp/tools/vault-onepassword.test.ts | 8 +-- .../mcp/tools/vault-provider-configs.test.ts | 10 ++- src/lib/mcp/vault-responses.ts | 2 +- src/lib/mcp/vault-schemas.ts | 19 ++--- 8 files changed, 56 insertions(+), 65 deletions(-) diff --git a/src/lib/mcp/tools/config-registry.test.ts b/src/lib/mcp/tools/config-registry.test.ts index 2029e4a7..2aac8ce7 100644 --- a/src/lib/mcp/tools/config-registry.test.ts +++ b/src/lib/mcp/tools/config-registry.test.ts @@ -1,59 +1,66 @@ /// -import { describe, expect, test } from 'bun:test'; -import { connectTestMcp, toolResultJSON } from '@/lib/mcp/mcp-test-fixtures'; -import { registerConfigRegistryTools } from '@/lib/mcp/tools/config-registry'; +import { describe, expect, test } from "bun:test"; +import { connectTestMcp, toolResultJSON } from "@/lib/mcp/mcp-test-fixtures"; +import { registerConfigRegistryTools } from "@/lib/mcp/tools/config-registry"; -describe('manage_config_registry SDK parity', () => { - test('preserves a recommended reserved ISP IP in the nested create recipe', async () => { +describe("manage_config_registry SDK parity", () => { + test("preserves a recommended reserved ISP IP in the nested create recipe", async () => { const recommendation = { - type: 'recommendation', + type: "recommendation", browser: { stealth: true }, evidence: {}, - match_scope: 'exact', - matched_target: 'https://example.com', + match_scope: "exact", + matched_target: "https://example.com", proxy: { - mode: 'managed', + mode: "managed", create: { - type: 'isp', + type: "isp", config: { - country: 'US', - ip_address: '203.0.113.10', + country: "US", + ip_address: "203.0.113.10", }, }, }, }; let lookupInput: unknown; - const { client, close } = await connectTestMcp(registerConfigRegistryTools, { - configRegistry: { - lookup: async (input: unknown) => { - lookupInput = input; - return { - target: { url: 'https://example.com' }, - recommendation, - working_configurations: [recommendation], - }; + const { client, close } = await connectTestMcp( + registerConfigRegistryTools, + { + configRegistry: { + lookup: async (input: unknown) => { + lookupInput = input; + return { + target: { url: "https://example.com" }, + recommendation, + working_configurations: [recommendation], + }; + }, }, }, - }); + ); try { - const tool = (await client.listTools()).tools.find(({ name }) => name === 'manage_config_registry'); - expect(tool?.description).toContain('config.ip_address'); + const tool = (await client.listTools()).tools.find( + ({ name }) => name === "manage_config_registry", + ); + expect(tool?.description).toContain("config.ip_address"); const result = await client.callTool({ - name: 'manage_config_registry', + name: "manage_config_registry", arguments: { - action: 'lookup', - url: 'https://example.com', - allowed_proxy_countries: ['US'], + action: "lookup", + url: "https://example.com", + allowed_proxy_countries: ["US"], }, }); expect(lookupInput).toEqual({ - url: 'https://example.com', - allowed_proxy_countries: ['US'], + url: "https://example.com", + allowed_proxy_countries: ["US"], }); - expect(toolResultJSON(result).recommendation.proxy.create.config.ip_address).toBe('203.0.113.10'); + expect( + toolResultJSON(result).recommendation.proxy.create.config.ip_address, + ).toBe("203.0.113.10"); } finally { await close(); } diff --git a/src/lib/mcp/tools/proxies.test.ts b/src/lib/mcp/tools/proxies.test.ts index f4cd91e2..ec58bf03 100644 --- a/src/lib/mcp/tools/proxies.test.ts +++ b/src/lib/mcp/tools/proxies.test.ts @@ -452,10 +452,7 @@ describe("manage_proxies", () => { proxy_count: 1, }); expect(calls).toEqual([ - [ - "list", - { country: "GB", status: "active", limit: 5, offset: 0 }, - ], + ["list", { country: "GB", status: "active", limit: 5, offset: 0 }], ["get", "203.0.113.10"], ]); } finally { diff --git a/src/lib/mcp/tools/proxies.ts b/src/lib/mcp/tools/proxies.ts index ac438085..57ba8d9b 100644 --- a/src/lib/mcp/tools/proxies.ts +++ b/src/lib/mcp/tools/proxies.ts @@ -200,9 +200,7 @@ export function registerProxyTools( } if ( params.ip_address !== undefined && - !["create", "list", "move", "get_reserved_ip"].includes( - params.action, - ) + !["create", "list", "move", "get_reserved_ip"].includes(params.action) ) { return errorResponse( "error: ip_address applies only to list, move, and get_reserved_ip; for create use config.ip_address.", diff --git a/src/lib/mcp/tools/vault-managed-auth.test.ts b/src/lib/mcp/tools/vault-managed-auth.test.ts index 1774029f..814c5c27 100644 --- a/src/lib/mcp/tools/vault-managed-auth.test.ts +++ b/src/lib/mcp/tools/vault-managed-auth.test.ts @@ -42,9 +42,7 @@ describe("managed auth vault credentials", () => { expect(credentialTool?.inputSchema.properties?.provider).toMatchObject({ enum: ["kernel", "1password", "managed_auth"], }); - expect(credentialTool?.description).toContain( - 'provider:"managed_auth"', - ); + expect(credentialTool?.description).toContain('provider:"managed_auth"'); expect(credentialTool?.description).toContain( "follows future saved-credential changes", ); diff --git a/src/lib/mcp/tools/vault-onepassword.test.ts b/src/lib/mcp/tools/vault-onepassword.test.ts index e73c42fe..ffcea92d 100644 --- a/src/lib/mcp/tools/vault-onepassword.test.ts +++ b/src/lib/mcp/tools/vault-onepassword.test.ts @@ -516,7 +516,9 @@ describe("1Password vault credentials", () => { expect(guidance).toContain("it needs no browser"); expect(guidance).toContain("every 2 seconds"); expect(guidance).toContain("after 3 minutes"); - expect(guidance).toContain("mcp intentionally omits access_request_reference"); + expect(guidance).toContain( + "mcp intentionally omits access_request_reference", + ); expect(guidance).toContain( "create a browser with this vault attached (KERNEL loads the 1password extension into it on demand)", ); @@ -933,9 +935,7 @@ describe("1Password vault credentials", () => { expect(body.guidance.join(" ")).toContain( "may import an access request already approved", ); - expect(body.guidance.join(" ")).toContain( - "mcp intentionally omits them", - ); + expect(body.guidance.join(" ")).toContain("mcp intentionally omits them"); expect(body.item.state.access_request).not.toHaveProperty("resolved"); expectNoReferences(read); expect( diff --git a/src/lib/mcp/tools/vault-provider-configs.test.ts b/src/lib/mcp/tools/vault-provider-configs.test.ts index 15f65b26..5d2b0066 100644 --- a/src/lib/mcp/tools/vault-provider-configs.test.ts +++ b/src/lib/mcp/tools/vault-provider-configs.test.ts @@ -3,10 +3,7 @@ import { organizationWideAuthInfo, projectScopedAuthInfo, } from "@/lib/mcp/auth-context.test-fixtures"; -import { - toolResultJSON, - toolResultText, -} from "@/lib/mcp/mcp-test-fixtures"; +import { toolResultJSON, toolResultText } from "@/lib/mcp/mcp-test-fixtures"; import { connectVaultTest, item, linkSpec } from "./vaults.test-fixtures"; const tool = "manage_vault_provider_configs"; @@ -64,8 +61,9 @@ describe("vault provider config SDK routing", () => { "manage_vault_wallets", "manage_vault_cards", ]) { - expect(tools.find((candidate) => candidate.name === name)?.description) - .toContain("vgs"); + expect( + tools.find((candidate) => candidate.name === name)?.description, + ).toContain("vgs"); } const results = [ diff --git a/src/lib/mcp/vault-responses.ts b/src/lib/mcp/vault-responses.ts index 0a5cb1e6..f0b00b4b 100644 --- a/src/lib/mcp/vault-responses.ts +++ b/src/lib/mcp/vault-responses.ts @@ -741,7 +741,7 @@ const shopPayCardGuidance = [ "a shop pay card is one purchase. spec.amount is the shopper's approval limit in minor units, up to 1000000; spec.currency must be usd. spec.context is the 100-500 character explanation shown to the shopper, and spec.totals must contain exactly one positive total no greater than amount. payment_method_id selects a card from the wallet's payment_methods expansion; omission uses the shopper's preferred card.", `after explicit user approval, invoke authorize with browser_id and the exact current top-level https page_url while final checkout is open. KERNEL confirms that checkout is open, sends the page to shop as the merchant, and limits fill to that origin in any browser with this vault attached. omitting both authorizes for spec.merchant_url. checkout inspection errors happen before shop pay receives a payment request: ${shopPayCheckoutRecovery} when item.action contains an approval url, share it unchanged only with the shopper, then observe get/events until fill is advertised.`, 'invoke advertised fill with ordered field/selector bindings and never values. card fields include number, expiration (format "MM/YY" or "MM/YYYY"), cvc, exp_month, and exp_year. shop pay can also supply its stored billing address. billing_first_name and billing_last_name are available only when shop pay returns them separately; billing_name is never split. request only needed billing fields because an absent or empty field returns field_unavailable before any browser write. fill never submits checkout or proves payment.', - 'after the merchant outcome is observed, invoke confirm_transaction exactly once. shop pay accepts transaction_type `PURCHASE`, `AUTHORIZATION`, or `CAPTURE`. required inputs are status, transaction_type, actual amount, matching uppercase currency, and occurred_at. for `APPROVED`, optional order_id and https order_url identify the order. for another outcome, optional failure_code explains why; payment_declined is used only with `DECLINED`, which takes no other code. reporting consumes the card and removes its stored card material whatever the outcome. if the outcome is unknown, reconcile manually rather than retrying checkout or guessing.', + "after the merchant outcome is observed, invoke confirm_transaction exactly once. shop pay accepts transaction_type `PURCHASE`, `AUTHORIZATION`, or `CAPTURE`. required inputs are status, transaction_type, actual amount, matching uppercase currency, and occurred_at. for `APPROVED`, optional order_id and https order_url identify the order. for another outcome, optional failure_code explains why; payment_declined is used only with `DECLINED`, which takes no other code. reporting consumes the card and removes its stored card material whatever the outcome. if the outcome is unknown, reconcile manually rather than retrying checkout or guessing.", ]; const shopPayRecoveryGuidance = diff --git a/src/lib/mcp/vault-schemas.ts b/src/lib/mcp/vault-schemas.ts index 953d991a..0cb7311d 100644 --- a/src/lib/mcp/vault-schemas.ts +++ b/src/lib/mcp/vault-schemas.ts @@ -172,7 +172,8 @@ export const passkeyInputsSchema = z .strict(); export type PasskeyInputs = z.infer; -export const addPaymentMethodInputsShape = '{ enrollment?: "hosted" | "server" }'; +export const addPaymentMethodInputsShape = + '{ enrollment?: "hosted" | "server" }'; export const addPaymentMethodInputsSchema = z .object({ enrollment: z @@ -285,13 +286,7 @@ export const confirmTransactionInputsShape = "{ status, transaction_type, amount, currency, occurred_at, order_id?, order_url?, failure_code? }"; export const confirmTransactionInputsSchema = z .object({ - status: z.enum([ - "APPROVED", - "DECLINED", - "PENDING", - "ERROR", - "CANCELLED", - ]), + status: z.enum(["APPROVED", "DECLINED", "PENDING", "ERROR", "CANCELLED"]), transaction_type: z.enum([ "PURCHASE", "AUTHORIZATION", @@ -306,13 +301,11 @@ export const confirmTransactionInputsSchema = z currency: z.string().regex(/^[A-Z]{3}$/), occurred_at: z.string().datetime({ offset: true }), order_id: z.string().min(1).max(255).optional(), - order_url: z + order_url: z.string().url().startsWith("https://").max(2048).optional(), + failure_code: z .string() - .url() - .startsWith("https://") - .max(2048) + .regex(/^[a-z0-9_]{1,64}$/) .optional(), - failure_code: z.string().regex(/^[a-z0-9_]{1,64}$/).optional(), }) .strict(); export type ConfirmTransactionInputs = z.infer< From 35ecb0bd0e10515e3236cd89310a713d82e79f25 Mon Sep 17 00:00:00 2001 From: masnwilliams <43387599+masnwilliams@users.noreply.github.com> Date: Fri, 13 Nov 2026 17:08:31 +0000 Subject: [PATCH 2/2] Shorten manage_browsers and manage_vault_wallets descriptions Move save_profile result and retry semantics into its response note and error note, and wallet add/remove and token-refresh detail into the relevant parameter descriptions, keeping only what is needed to choose an action in each tool description. --- src/lib/mcp/tools/browsers.test.ts | 62 +++++++++++++++++++++++------- src/lib/mcp/tools/browsers.ts | 19 ++++++--- src/lib/mcp/tools/vault-wallets.ts | 8 ++-- 3 files changed, 66 insertions(+), 23 deletions(-) diff --git a/src/lib/mcp/tools/browsers.test.ts b/src/lib/mcp/tools/browsers.test.ts index 79deda7e..cd56c975 100644 --- a/src/lib/mcp/tools/browsers.test.ts +++ b/src/lib/mcp/tools/browsers.test.ts @@ -1551,25 +1551,17 @@ describe("manage_browsers save profile", () => { expect(profileName?.description).toContain( "create when no matching profile exists", ); - expect(browser?.description).toContain("leased_at"); - expect(browser?.description).toContain( - "active cdp, playwright, and webdriver connections close and must reconnect", - ); - expect(browser?.description).toContain( - "call save_profile at a stable point, such as right after a login completes", - ); - expect(browser?.description).toContain( - "a browser_busy conflict is temporary; retry the call", + expect(profileName?.description).toContain( + "saving does not attach it or change save_profile_changes", ); expect(browser?.description).toContain( - "browser_not_active (the browser is no longer running) are not retryable", + "save_profile restarts the browser: cdp, playwright, and webdriver connections close", ); expect(browser?.description).toContain( - "caches, service worker storage, and google drive indexeddb data are omitted", + "call it at a stable point, such as right after a login completes", ); - expect(browser?.description).toContain("the most recently started wins"); expect(browser?.description).toContain( - "a successful save restarts timeout_seconds", + "mint a read or control live-view url", ); } finally { await close(); @@ -1636,11 +1628,53 @@ describe("manage_browsers save profile", () => { expect(byId).toEqual({ id: "prf_existing", name: "existing-profile", + note: expect.any(String), }); - expect(byName).toEqual({ + expect(byName).toMatchObject({ id: "prf_created_from_name", name: "checkout.snapshot-1", }); + for (const text of [ + "start a new webdriver session", + "caches, service worker storage, and google drive indexeddb data are omitted", + "a successful save restarts timeout_seconds", + "the most recently started wins", + ]) { + expect(byId.note).toContain(text); + } + } finally { + await close(); + } + }); + + test("returns retry guidance when a save fails", async () => { + const { client, close } = await connectTestMcp( + registerBrowserCapabilities, + { + browsers: { + saveProfile: async () => { + throw new Error("browser_busy"); + }, + }, + }, + ); + + try { + const result = await client.callTool({ + name: "manage_browsers", + arguments: { + action: "save_profile", + session_id: "brr_123", + profile_name: "profile", + }, + }); + expect(result.isError).toBe(true); + const text = toolResultText(result); + expect(text).toContain("a browser_busy conflict is temporary"); + expect(text).toContain( + "browser_not_active (the browser is no longer running) are not retryable", + ); + expect(text).toContain("check the profile before calling again"); } finally { await close(); } diff --git a/src/lib/mcp/tools/browsers.ts b/src/lib/mcp/tools/browsers.ts index 6e8eaf91..edf248d6 100644 --- a/src/lib/mcp/tools/browsers.ts +++ b/src/lib/mcp/tools/browsers.ts @@ -261,6 +261,10 @@ const maxRawTelemetryEvents = 5; // the page envelope while ensuring one response cannot carry multiple max-size records. const maxRawTelemetryResponseBytes = 1024 * 1024; const saveProfileTimeoutMs = 190_000; +const saveProfileNote = + "the browser restarted: reconnect cdp and playwright clients, start a new webdriver session, and expect restored tabs to have reloaded without in-memory page state. caches, service worker storage, and google drive indexeddb data are omitted from the saved profile but remain in the running browser. a successful save restarts timeout_seconds. browsers already using this profile keep their in-memory state; pools with refresh_on_profile_update replace idle sessions from the new snapshot, and other browsers use it when they next load the profile. among overlapping save_profile calls to the same profile, the most recently started wins even if an earlier call finishes later; an automatic session-end save with save_profile_changes is ordered against them by finish time."; +const saveProfileErrorNote = + "a browser_busy conflict is temporary; retry the call. browser_lease_changed (a pooled browser was released or reacquired after the call began) and browser_not_active (the browser is no longer running) are not retryable against that browser. save_profile waits up to 190 seconds and is not retried automatically; after a timeout the save may still finish, so check the profile before calling again. a failed save leaves timeout_seconds unchanged."; function rawTelemetryPageError(items: TelemetryEnvelope[]) { for (const { event } of items) { @@ -499,7 +503,7 @@ export function registerBrowserCapabilities( "manage_browsers", { description: - 'manage the browser sessions that every web task runs in, and their archived telemetry. once a session exists, drive it with webmcp, browser_repl, execute_playwright_code, or computer_action, preferring the first that can carry the current step. use "list" to choose an existing session, "create" before browser control (optionally with start_replay), "update" to change supported session settings, "get" for full details, "get_telemetry" to diagnose active or deleted sessions, "create_live_view" to mint an unrevokable read-only or control-only live-view bearer url for a headful browser, "save_profile" to snapshot a running session to an existing or new profile without ending it, and "delete" when finished. live sessions can be addressed by id or by the name given at creation or set on update; deleted sessions only by id. browser records include leased_at only for leased pooled sessions; it is the time the session was last leased. get_telemetry compacts events by default; set compact=false with explicit categories and a limit of at most 5 when raw headers, request data, response bodies, or other omitted fields are needed. save_profile accepts exactly one destination: profile_id must identify an existing profile, while profile_name overwrites the matching profile in the browser project or creates it. saving restarts the browser: active cdp, playwright, and webdriver connections close and must reconnect, webdriver clients need a new session, restored tabs reload, and in-memory page state is lost, so call save_profile at a stable point, such as right after a login completes, not in the middle of a page action. the browser is stopped only while its user data is copied, and the action returns after the restarted browser accepts cdp connections and the profile upload completes. caches, service worker storage, and google drive indexeddb data are omitted from the saved profile but remain in the running browser. saving neither attaches the destination profile nor changes save_profile_changes. active browsers already using that profile keep their in-memory state; pools with refresh_on_profile_update enabled replace idle sessions from the new snapshot, while other browsers use it when they next load the profile. among overlapping save_profile calls to the same profile, the most recently started wins even if an earlier call finishes later; an automatic session-end save with save_profile_changes is ordered against them by finish time. a successful save restarts timeout_seconds, while a failed save leaves it unchanged. a browser_busy conflict is temporary; retry the call. browser_lease_changed (a pooled browser was released or reacquired after the call began) and browser_not_active (the browser is no longer running) are not retryable against that browser. save_profile waits up to 190 seconds and is not retried automatically; after a timeout the save may still finish, so check the profile before calling again.', + 'manage the browser sessions that every web task runs in, and their archived telemetry. once a session exists, drive it with webmcp, browser_repl, execute_playwright_code, or computer_action, preferring the first that can carry the current step. use "list" to choose an existing session, "create" before browser control, "update" to change supported session settings, "get" for full details, "get_telemetry" to diagnose active or deleted sessions, "create_live_view" to mint a read or control live-view url for a headful browser, "save_profile" to snapshot a running session to a profile without ending it, and "delete" when finished. live view urls are bearer credentials that cannot be revoked before they expire. save_profile restarts the browser: cdp, playwright, and webdriver connections close and tabs reload, so call it at a stable point, such as right after a login completes. live sessions can be addressed by id or by the name given at creation or set on update; deleted sessions only by id. get_telemetry compacts events by default; set compact=false with explicit categories and a limit of at most 5 when raw headers, request data, response bodies, or other omitted fields are needed.', inputSchema: z.strictObject( { ...projectSelectionInputSchema(), @@ -648,14 +652,14 @@ export function registerBrowserCapabilities( .max(255) .regex(/^[a-zA-Z0-9._-]{1,255}$/) .describe( - "(create, update) profile name to load saved cookies/logins. (save_profile) destination profile name to overwrite, or create when no matching profile exists in the browser project. must be 1-255 letters, digits, '.', '_' or '-'; cannot use with profile_id.", + "(create, update) profile name to load saved cookies/logins. (save_profile) destination profile name to overwrite, or create when no matching profile exists in the browser project; saving does not attach it or change save_profile_changes. must be 1-255 letters, digits, '.', '_' or '-'; cannot use with profile_id.", ) .optional(), profile_id: z .string() .min(1) .describe( - "(create, update) existing profile id to load. (save_profile) existing destination profile id to overwrite. cannot use with profile_name.", + "(create, update) existing profile id to load. (save_profile) existing destination profile id to overwrite; saving does not attach it or change save_profile_changes. cannot use with profile_name.", ) .optional(), save_profile_changes: z @@ -1281,7 +1285,7 @@ export function registerBrowserCapabilities( { name: params.profile_name }, options, ); - return jsonResponse(profile); + return jsonResponse({ ...profile, note: saveProfileNote }); } case "delete": { if (!params.session_id) @@ -1293,7 +1297,12 @@ export function registerBrowserCapabilities( } } } catch (error) { - throwToolError("manage_browsers", params.action, error); + throwToolError( + "manage_browsers", + params.action, + error, + params.action === "save_profile" ? saveProfileErrorNote : undefined, + ); } }, ); diff --git a/src/lib/mcp/tools/vault-wallets.ts b/src/lib/mcp/tools/vault-wallets.ts index 1c28018c..114de7cb 100644 --- a/src/lib/mcp/tools/vault-wallets.ts +++ b/src/lib/mcp/tools/vault-wallets.ts @@ -31,7 +31,7 @@ export function registerVaultWalletTools( "manage_vault_wallets", { description: - 'connect payment wallets without exposing secrets. shop pay wallets use spec:{authorization:{method:"oauth",client:{type:"kernel_managed"}}}; creation returns a shop_pay_oauth action whose url must be shared unchanged with the shopper so they can sign in to shop and approve the connection. once connected, payment_methods lists their saved shop pay cards. KERNEL wallets use spec:{}. where add_payment_method is enabled, creation returns an empty connected wallet and "add_payment_method" starts each card enrollment, including the first; enrollment hosted (the default) returns a cardholder card_enrollment action, while enrollment server returns a card_ingest action for the issuer backend when configured. otherwise creation returns the legacy default card\'s card_enrollment action. share oauth and enrollment urls only with the intended user in a private surface, never open, alter, or log them. embedded card-capture tokens are bearer secrets and are intentionally not returned by mcp. "create" creates or retrieves an identical wallet by immutable key. hosted connection/enrollment actions are for the user; a valid imported link grant creates a connected wallet. "payment_methods" requests the advertised live payment_methods expansion. "add_payment_method" takes no payment details and returns one pending_enrollment method. hosted returns a separate single-use cardholder link expiring after 30 minutes; server returns a single-use issuer card_ingest url expiring after 5 minutes. failed or expired enrollment does not affect the other methods. "remove_payment_method" requires explicit user confirmation and a payment_method_id; it removes only that card, leaves the wallet connected even when no cards remain, and is not proof that a payment did not occur. an unresolved purchase can block removal with purchase_unresolved. if removal has an uncertain provider outcome, the method remains removal_pending and refuses payments; inspect payment_methods and retry removal later only as the advertised operation and status_reason direct. both operations must be advertised and require vaults:write plus vaults:use for scoped api keys. manage_vault_items get can request any advertised expansion, including live link_user_info and link_shipping_addresses data that KERNEL does not store; unavailable expansions return an api error. select link payment_method_id explicitly. for KERNEL card items, omitting payment_method_id selects only the legacy default card (is_default:true), never another method; wallets created with payment methods enabled have no default. KERNEL payment_request items always require an explicit active, eligible method. a connected link wallet returns item.description with the checkout flow; follow it. pay with link in this order: reach the final checkout page, choose a payment_method_id with "payment_methods", create the card with manage_vault_cards, then authorize it with browser_id and page_url from that checkout; read the card\'s available_operations descriptions for each next step. agentcard card_id may be omitted for cardholder selection at checkout approval. capabilities are advisory; absent means unknown. KERNEL-managed link oauth remains supported. customer-managed link requires authorization.client.provider_config and write-only authorization.tokens supplied by a trusted backend, never chat; config credentials do not authorize a user. token_refresh defaults to `kernel`: send the access and refresh tokens from one grant, then stop refreshing it because KERNEL owns rotation. token_refresh `customer` sends only the access token; the trusted backend keeps and rotates the refresh token, and KERNEL never refreshes or revokes the grant. before each later step that reaches link, that backend replaces the stored access token through the KERNEL api; update_access_token is intentionally unavailable through mcp because it accepts a secret. if link rejects a customer-refreshed token, update it before retrying the same unstarted provider step, but never replay an uncertain payment. token_refresh is fixed at wallet creation. duplicate create never replaces a grant; bindings cannot change. agentcard spec.provider_config is optional. with a customer-owned config, user_id imports an existing connection after read-only validation with those credentials and does not require prior enrollment in this organization; a failed validation creates nothing and can be corrected and retried. with KERNEL-managed credentials, user_id must already be enrolled in this organization under the same config. no in-place imported reauthorization: obtain a fresh grant under a new wallet key for new payments only; retain unresolved old payments for reconciliation. never provide card data or oauth codes. vgs wallet creation is deliberately unavailable through mcp because its useful card flow requires pan/cvc aliases; provision it with a trusted backend and the direct api/sdk instead of putting payment aliases in model-facing arguments. existing api-created vgs wallets remain inspectable with manage_vault_items, but they have no payment_methods expansion. requests are not automatically retried.', + 'connect payment wallets without exposing secrets. "create" creates or retrieves an identical wallet by immutable key; spec describes each provider. KERNEL creation returns an empty connected wallet where add_payment_method is enabled, otherwise a hosted card_enrollment action; shop pay creation returns a shop_pay_oauth action whose url must be shared unchanged with the shopper. share oauth and enrollment urls only with the intended user in a private surface; never open, alter, or log them. embedded card-capture tokens are bearer secrets and are intentionally not returned by mcp. a valid imported link grant creates a connected wallet. "payment_methods" requests the advertised live payment_methods expansion. on a KERNEL wallet that advertises them, "add_payment_method" starts one card enrollment and "remove_payment_method" removes one card after explicit user confirmation; neither takes card details, and both need vaults:write plus vaults:use for scoped api keys. manage_vault_items get can request any advertised expansion, including live link_user_info and link_shipping_addresses data that KERNEL does not store; unavailable expansions return an api error. select link payment_method_id explicitly; never automatically choose a default. KERNEL card items that omit payment_method_id use only the legacy default card; KERNEL payment_request items always require one. a connected link wallet returns item.description with the checkout flow; follow it. pay with link in this order: reach the final checkout page, choose a payment_method_id with "payment_methods", create the card with manage_vault_cards, then authorize it with browser_id and page_url from that checkout; read the card\'s available_operations descriptions for each next step. agentcard card_id may be omitted for cardholder selection at checkout approval. capabilities are advisory; absent means unknown. customer-managed link wallets need write-only tokens from a trusted backend, never chat; config credentials do not authorize a user, and token replacement is unavailable through mcp. duplicate create never replaces a grant; bindings and token_refresh cannot change. no in-place imported reauthorization: obtain a fresh grant under a new wallet key for new payments only; retain unresolved old payments for reconciliation. vgs wallets are provisioned only by a trusted backend through the direct api/sdk. never provide card data or oauth codes. requests are not automatically retried.', inputSchema: vaultToolInput({ ...vaultItemSchema, key: vaultKeySchema(), @@ -49,13 +49,13 @@ export function registerVaultWalletTools( .min(1) .max(128) .describe( - "(remove_payment_method) id from the wallet's payment_methods expansion.", + "(remove_payment_method) id from the wallet's payment_methods expansion. removes only that card and leaves the wallet connected even when no cards remain. purchase_unresolved means an unresolved purchase blocks removal; an uncertain provider outcome leaves the method removal_pending, refusing payments, until a later advertised retry.", ) .optional(), enrollment: z .enum(["hosted", "server"]) .describe( - "(add_payment_method) hosted (default) returns a 30-minute card_enrollment link for the cardholder. server returns a 5-minute card_ingest url for the issuer's backend and requires organization card-ingest configuration.", + "(add_payment_method) hosted (default) returns a single-use 30-minute card_enrollment link for the cardholder. server returns a single-use 5-minute card_ingest url for the issuer's backend and requires organization card-ingest configuration. failed or expired enrollment does not affect other methods.", ) .optional(), spec: z @@ -66,7 +66,7 @@ export function registerVaultWalletTools( shopPayWalletSpecSchema, ]) .describe( - '(create) specification object, not a {type, spec} envelope. embedded provider must match provider. link: {"authorization":{"method":"oauth","client":{"type":"kernel_managed"}}}, or customer_managed with provider_config (exactly one id/name), write-only tokens from a trusted backend, and optional token_refresh. omit token_refresh or use `kernel` with access_token plus refresh_token; use `customer` with access_token only when the backend keeps the refresh token. agentcard: {} to enroll, optionally provider_config and/or user_id; customer-owned configs validate and import the existing user before creation, while KERNEL-managed credentials require prior enrollment under the same config. KERNEL: {}; creation may return an empty connected wallet, then add_payment_method uses enrollment hosted for a cardholder link or server for an issuer card-ingest url. shop pay: {"authorization":{"method":"oauth","client":{"type":"kernel_managed"}}}; creation returns a shop_pay_oauth action and the connected wallet exposes saved cards through payment_methods. no card data in tool input.', + '(create) specification object, not a {type, spec} envelope. embedded provider must match provider. link: {"authorization":{"method":"oauth","client":{"type":"kernel_managed"}}}, or customer_managed with provider_config (exactly one id/name), write-only tokens from a trusted backend, and optional token_refresh. omit token_refresh or use `kernel` with access_token plus refresh_token, then stop refreshing that grant; use `customer` with access_token only when the backend keeps and rotates the refresh token and replaces the access token through the KERNEL api. token_refresh is fixed at creation. agentcard: {} to enroll, optionally provider_config and/or user_id; customer-owned configs validate and import the existing user before creation (a failed validation creates nothing and can be corrected and retried), while KERNEL-managed credentials require prior enrollment under the same config. KERNEL: {}; creation may return an empty connected wallet, then add_payment_method uses enrollment hosted for a cardholder link or server for an issuer card-ingest url. shop pay: {"authorization":{"method":"oauth","client":{"type":"kernel_managed"}}}; creation returns a shop_pay_oauth action and the connected wallet exposes saved cards through payment_methods. no card data in tool input.', ) .optional(), }),