From 635ab4e91aae41376b0067a1aeb8180860636bd4 Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Fri, 25 Sep 2026 03:02:46 +0000 Subject: [PATCH 1/9] Add 1Password brokered approval to vault credential tools --- README.md | 4 +- bun.lock | 4 +- docs/vault-payments.md | 76 +++- package.json | 2 +- .../mcp/tools/vault-credential-flow.test.ts | 11 +- src/lib/mcp/tools/vault-credentials.ts | 126 +++++- src/lib/mcp/tools/vault-items.ts | 6 +- src/lib/mcp/tools/vault-onepassword.test.ts | 400 ++++++++++++++++++ src/lib/mcp/tools/vaults.ts | 2 +- src/lib/mcp/vault-responses.ts | 128 ++++-- 10 files changed, 692 insertions(+), 67 deletions(-) create mode 100644 src/lib/mcp/tools/vault-onepassword.test.ts diff --git a/README.md b/README.md index 6fdff7db..2bdfe961 100644 --- a/README.md +++ b/README.md @@ -340,10 +340,10 @@ Call `get_connection_context` before deciding whether to create or select a proj - `manage_vaults` - Create, list, get, and delete project-owned vaults; use one per end user. - `manage_vault_wallets` - Connect Kernel-managed or configured Link/AgentCard wallets, import Link grants from a trusted backend, and inspect live payment methods. - `manage_vault_cards` - Create or update card requests according to the API's lifecycle rules; does not implicitly authorize Link cards. -- `manage_vault_credentials` - Create credential definitions for private human collection; update values or description with version and optional immutable item identity preconditions. +- `manage_vault_credentials` - Create credentials through one of two user-chosen paths: Kernel-hosted collection (definitions for private human collection; update values or description with version and optional immutable item identity preconditions) or 1Password brokered approval (connect a 1Password account, then create a login request the user approves in the 1Password app). Agents ask the user which path they prefer before creating credentials. - `manage_vault_items` - List, get, invoke advertised operations (including fill with value-free bindings), observe events, and delete vault items. Read credential definitions, presence, version, collection links, and explicitly non-sensitive values; sensitive values remain hidden. `collect` reopens the full form; provider approvals remain user actions. Ready is not login or payment success. -See [Vault payments](docs/vault-payments.md) for both provider flows, safety rules, and response shapes. `manage_browsers` accepts creation-only `vaults` references (max 20); existing sessions and pools cannot gain vault bindings. The six vault tools share the `vaults` toolset and prepare/observe credentials rather than submitting merchant payments. They are exposed only when `GET /org/entitlements` reports `features.vaults.enabled: true` for the current credential; missing or unavailable entitlements hide them. Toolset configuration cannot override this access check. Credential create → collect → readiness → fill is supported entirely through MCP tools. `prepare_checkout` remains API/CLI-only. The SDK dependency is pinned in `bun.lock`. +See [Vault payments](docs/vault-payments.md) for both provider flows, safety rules, and response shapes. `manage_browsers` accepts creation-only `vaults` references (max 20); existing sessions and pools cannot gain vault bindings. The six vault tools share the `vaults` toolset and prepare/observe credentials rather than submitting merchant payments. They are exposed only when `GET /org/entitlements` reports `features.vaults.enabled: true` for the current credential; missing or unavailable entitlements hide them. Toolset configuration cannot override this access check. Credential create → collect → readiness → fill is supported entirely through MCP tools. `prepare_checkout` remains API/CLI-only. The SDK dependency is pinned in `bun.lock`; it currently points at a temporary stlc development preview (TODO: replace with the official `@onkernel/sdk` release that includes 1Password vault credentials). ### Standalone tools diff --git a/bun.lock b/bun.lock index 6e1ac27d..f23d2ad1 100644 --- a/bun.lock +++ b/bun.lock @@ -11,7 +11,7 @@ "@modelcontextprotocol/core": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "@onkernel/managed-auth-react": "0.5.3", - "@onkernel/sdk": "0.112.0", + "@onkernel/sdk": "git+ssh://git@github.com/kernel/kernel-node-sdk-staging.git#bc922deaa45c2af412ae987d27208b1be474d73d", "@posthog/mcp": "0.17.0", "@types/jsonwebtoken": "^9.0.10", "@types/redis": "^4.0.11", @@ -160,7 +160,7 @@ "@onkernel/managed-auth-react": ["@onkernel/managed-auth-react@0.5.3", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5Ps7h7HknAxL4ZLnJCzsLhW8MLRr0SH1P8+eALBZXCmnx/K+g5ra2ar3iLoWFQ8vf/EvpuDF1LigIT6tDjRKdA=="], - "@onkernel/sdk": ["@onkernel/sdk@0.112.0", "", {}, "sha512-seNX3Z/mXUaYKtcQ/ihEA5Pm1TsvfMl5K+xq+7zUwB1MlFadZtwLcG1c718ccYnxU5dpmDs57wS6nW7xlzfafA=="], + "@onkernel/sdk": ["@onkernel/sdk@git+ssh://git@github.com/kernel/kernel-node-sdk-staging.git#bc922deaa45c2af412ae987d27208b1be474d73d", {}, "bc922deaa45c2af412ae987d27208b1be474d73d"], "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eJopQrUk0WR7jViYDC29+Rp50xGvs4GtWOXBeqCoFMzutkkO3CZvHehA4JqnjfWMTSS8toqvRhCSOpOz62Wf9w=="], diff --git a/docs/vault-payments.md b/docs/vault-payments.md index d8c663ab..602798f4 100644 --- a/docs/vault-payments.md +++ b/docs/vault-payments.md @@ -13,12 +13,24 @@ there is no per-item test flag. AgentCard configuration responses report the introspected `test_mode`. A development or staging MCP endpoint does not make a card request a test transaction. -The released Node SDK dependency is pinned in `bun.lock`. + + +The Node SDK dependency is temporarily pinned to the stlc development preview +`kernel-node-sdk-staging@bc922deaa45c2af412ae987d27208b1be474d73d` in `bun.lock`. ## Credential collection and observation -Use one vault per end user, such as `user-123`. Create credential definitions with -`manage_vault_credentials`: use only the recognizable site name for `description`, and +Credentials have two paths. Before creating a credential, ask the user which they +prefer and pass it as `provider`; never choose for them: + +- `provider: "kernel"` (Kernel-hosted collection): the user enters values in a + Kernel-hosted form, and the agent fills them with value-free bindings. +- `provider: "1password"` (1Password brokered approval): the user connects their + 1Password account once and approves each login request in the 1Password app. See + [1Password brokered approval](#1password-brokered-approval). + +Use one vault per end user, such as `user-123`. For Kernel-hosted collection, create +credential definitions with `manage_vault_credentials`: use only the recognizable site name for `description`, and set `sensitive: false` explicitly for ordinary usernames/emails. Passwords and TOTP seeds must be sensitive. Payment-card data belongs in wallet/card items, not credentials. @@ -51,14 +63,25 @@ fall back to payment aliases. ```json { "action": "create", + "provider": "kernel", "vault": "user-123", "key": "login", "spec": { "description": "Example", - "fields": { - "username": { "type": "text", "required": true, "sensitive": false }, - "password": { "type": "password", "required": true, "sensitive": true } - } + "fields": [ + { + "name": "username", + "type": "text", + "required": true, + "sensitive": false + }, + { + "name": "password", + "type": "password", + "required": true, + "sensitive": true + } + ] } } ``` @@ -105,6 +128,43 @@ Definitions cannot be changed. Never solicit secret replacement values in chat; prefer `collect` for human edits. Requests are not automatically retried. `prepare_checkout` remains API/CLI-only. +### 1Password brokered approval + +1. Connect the account with `manage_vault_credentials`, `action: "connect_account"`, + `provider: "1password"`, the user's vault, and a new key. Give the returned + 1Password authorization URL only to the account owner, outside the + agent-controlled browser; they verify the account on the consent screen. +2. Observe the account with `manage_vault_items` `get` until `state.status` is + `connected`, then create the credential: + + ```json + { + "action": "create", + "provider": "1password", + "vault": "user-123", + "key": "example-login", + "spec": { + "account_id": "", + "website": "https://example.com/login" + } + } + ``` + + Optional `goal`, `reason`, and `keywords` describe the request to the account owner. + 1Password credentials store no values or selectors and cannot be updated. + +3. With a browser created with the vault attached, and after explicit user approval, + invoke the advertised `1pw_request_access` with `inputs: {"browser_id": "..."}`. +4. Approval is a human action in the account owner's 1Password app. MCP output never + includes the native approval link, access-request IDs or references, provider + paths or identities, OAuth tokens, or integration keys, and the agent must never + open, approve, or relay an approval. Invoke the advertised `1pw_poll_access` with + `browser_id` to observe the decision. +5. When the item is ready, invoke the advertised `1pw_fill` with `browser_id` and the + exact current `page_url`. The extension selects fields and submits the form. + `fill_submitted` does not confirm login; `fill_failed` and `fill_unknown` are tool + errors, and `fill_unknown` must not be retried in the same browser. + ## Tools and scope The six vault tools are exposed only when the current credential's @@ -121,7 +181,7 @@ The `vaults` toolset configuration can further restrict access, never grant it. | `manage_vaults` | `create`, `list`, `get`, `delete` | | `manage_vault_wallets` | `create`, `payment_methods` | | `manage_vault_cards` | `create`, `update` | -| `manage_vault_credentials` | `create`, `update` | +| `manage_vault_credentials` | `create`, `update`, `connect_account` | | `manage_vault_items` | `list`, `get`, `invoke`, `events`, `delete` | Provider configurations are organization-owned and do not accept a project diff --git a/package.json b/package.json index 8db06dc7..58f63ffb 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@modelcontextprotocol/core": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "@onkernel/managed-auth-react": "0.5.3", - "@onkernel/sdk": "0.112.0", + "@onkernel/sdk": "git+ssh://git@github.com/kernel/kernel-node-sdk-staging.git#bc922deaa45c2af412ae987d27208b1be474d73d", "@posthog/mcp": "0.17.0", "@types/jsonwebtoken": "^9.0.10", "@types/redis": "^4.0.11", diff --git a/src/lib/mcp/tools/vault-credential-flow.test.ts b/src/lib/mcp/tools/vault-credential-flow.test.ts index 5dfebd23..a1bf7631 100644 --- a/src/lib/mcp/tools/vault-credential-flow.test.ts +++ b/src/lib/mcp/tools/vault-credential-flow.test.ts @@ -99,6 +99,7 @@ describe("MCP credential flow", () => { spec: { fields: { username: { value: target.vault } } }, } : { + provider: "kernel", spec: { ...spec, fields: spec.fields.map((field) => @@ -323,6 +324,7 @@ describe("MCP credential flow", () => { await fixture.call("manage_vault_credentials", { ...target, action: "create", + provider: "kernel", spec, }), ); @@ -363,7 +365,10 @@ describe("MCP credential flow", () => { "GET", "POST", ]); - expect(fixture.requests[2].body).toEqual({ type: "credential", spec }); + expect(fixture.requests[2].body).toEqual({ + type: "credential", + spec: { provider: "kernel", ...spec }, + }); expect(fixture.requests.at(-1)?.body).toEqual({ type: "fill", ...fill }); expect(fixture.requests[5].path).toContain("wait=60"); } finally { @@ -407,6 +412,7 @@ describe("MCP credential flow", () => { const result = await fixture.call("manage_vault_credentials", { ...target, action: "create", + provider: "kernel", spec: { ...spec, fields: [ @@ -458,6 +464,7 @@ describe("MCP credential flow", () => { action, ...(action === "create" ? { + provider: "kernel", spec: { fields: [ { @@ -569,7 +576,7 @@ describe("MCP credential flow", () => { action: operation, ...(operation === "update" ? { version: 2, spec: { description: "Example" } } - : { spec }), + : { provider: "kernel", spec }), }); expect(result.isError).toBe(true); expect(JSON.stringify(result)).not.toContain("private-"); diff --git a/src/lib/mcp/tools/vault-credentials.ts b/src/lib/mcp/tools/vault-credentials.ts index a24efc7f..c64ca457 100644 --- a/src/lib/mcp/tools/vault-credentials.ts +++ b/src/lib/mcp/tools/vault-credentials.ts @@ -75,8 +75,53 @@ const createSpec = z ), }) .strict(); +const onePasswordCreateSpec = z + .object({ + account_id: z + .string() + .min(1) + .describe( + "Immutable id (not key) of a connected 1Password credential_account item in the same vault.", + ), + website: z + .string() + .url() + .max(2083) + .regex(/^https:\/\//) + .describe("HTTPS login page for the single requested login."), + goal: z + .string() + .max(140) + .optional() + .describe("Short request goal shown to the account owner."), + reason: z.string().max(100).optional(), + keywords: z.array(z.string().min(1).max(50)).min(1).max(5).optional(), + }) + .strict(); + +function onePasswordCredentialSpec( + spec: z.infer, +) { + return { + provider: "1password" as const, + account_id: spec.account_id, + requests: { + version: 2, + ...(spec.goal !== undefined && { goal: spec.goal }), + entries: [ + { + type: "login", + parameters: { website: spec.website }, + ...(spec.reason !== undefined && { reason: spec.reason }), + ...(spec.keywords !== undefined && { keywords: spec.keywords }), + }, + ], + }, + }; +} + function publicCredentialFieldNames(item: VaultItem): Set { - if (item.type !== "credential") return new Set(); + if (item.type !== "credential" || !("fields" in item.spec)) return new Set(); const names = new Set(); const publicNames = new Set(); for (const field of item.spec.fields) { @@ -115,17 +160,30 @@ export function registerVaultCredentialTools( "manage_vault_credentials", { description: - 'Create or update credential items in a per-end-user vault. Use only the recognizable site name as description; explicitly set sensitive:false for ordinary usernames/emails. Each field may include an optional non-secret human-readable label; name remains the stable key for updates and browser fills. Passwords and TOTP seeds must be sensitive. Never store payment-card data here. For human collection, omit values and present the returned bearer collection URL privately to the intended user, outside the agent-controlled browser. Never ask for passwords or TOTP seeds in chat. TOTP seeds require trusted provisioning and have no hosted input. On create, fields is an ordered array of named definitions: inspect the website and list fields in its natural top-to-bottom order because this directly controls the user-facing collection form. Update fields remain keyed by name and contain only value. Updates require the latest version and optionally expected_item_id from an earlier read; definitions are immutable. Omitted values are preserved; null or empty strings clear supported values. Clearing required TOTP is unsupported. Hosted forms require populated required inputs. To reopen collection, use manage_vault_items with action: "invoke" and operation: "collect". Use manage_vault_items get with wait for readiness, then invoke fill with fill parameters. For edits to already-ready items compare versions without wait. Explicitly non-sensitive text/email values are returned; sensitive values and TOTP seeds are omitted. Writes are never automatically retried; reconcile conflicts or uncertain outcomes before any further write.', + 'Create or update credential items in a per-end-user vault. There are two credential paths. Before creating any credential, ask the user which they prefer and set provider to match; never choose for them. provider:"kernel" is Kernel-hosted collection: the user enters values in a Kernel-hosted form and the agent fills them with value-free bindings. provider:"1password" is 1Password brokered approval: the user connects their 1Password account once, approves each login request in the 1Password app, and the 1Password extension fills and submits; Kernel stores no values. ' + + 'Kernel path: use only the recognizable site name as description; explicitly set sensitive:false for ordinary usernames/emails. Each field may include an optional non-secret human-readable label; name remains the stable key for updates and browser fills. Passwords and TOTP seeds must be sensitive. Never store payment-card data here. For human collection, omit values and present the returned bearer collection URL privately to the intended user, outside the agent-controlled browser. Never ask for passwords or TOTP seeds in chat. TOTP seeds require trusted provisioning and have no hosted input. On create, fields is an ordered array of named definitions: inspect the website and list fields in its natural top-to-bottom order because this directly controls the user-facing collection form. Update fields remain keyed by name and contain only value. Updates require the latest version and optionally expected_item_id from an earlier read; definitions are immutable. Omitted values are preserved; null or empty strings clear supported values. Clearing required TOTP is unsupported. Hosted forms require populated required inputs. To reopen collection, use manage_vault_items with action: "invoke" and operation: "collect". Use manage_vault_items get with wait for readiness, then invoke fill with fill parameters. For edits to already-ready items compare versions without wait. Explicitly non-sensitive text/email values are returned; sensitive values and TOTP seeds are omitted. ' + + '1Password path: first use action "connect_account" with provider:"1password" and a new key; present the returned 1Password authorization URL only to the account owner, outside the agent-controlled browser, and let them verify the account on the consent screen. Once manage_vault_items get reports the account connected, create the credential with provider:"1password" and spec {account_id, website, optional goal/reason/keywords}. 1Password credentials cannot be updated. Approval is a human action in the 1Password app: MCP never returns the native approval link, access-request references, tokens, or integration keys, and you must never open, approve, or relay an approval yourself. This is unrelated to manage_credential_providers. ' + + "Writes are never automatically retried; reconcile conflicts or uncertain outcomes before any further write.", inputSchema: vaultToolInput({ ...vaultItemSchema, key: vaultKeySchema(), - action: z.enum(["create", "update"]), + action: z.enum(["create", "update", "connect_account"]), + provider: z + .enum(["kernel", "1password"]) + .describe( + '(create, connect_account) The path the user chose. Ask the user before creating. connect_account supports only "1password".', + ) + .optional(), spec: z - .union([createSpec, updateSpec]) + .union([createSpec, onePasswordCreateSpec, updateSpec]) + .describe( + "(create, update) Kernel create: description and ordered fields. 1Password create: account_id, website, and optional goal/reason/keywords. Update (Kernel only): description and/or fields keyed by name.", + ) .refine( (spec) => Buffer.byteLength(JSON.stringify(spec), "utf8") <= 128 * 1024, - ), + ) + .optional(), version: z .number() .int() @@ -159,11 +217,59 @@ export function registerVaultCredentialTools( const options = { maxRetries: 0, signal: ctx.mcpReq.signal }; try { if ( - params.action === "create" && + params.action !== "update" && (params.version !== undefined || params.expected_item_id !== undefined) ) return errorResponse("version and expected_item_id are update-only."); + if (params.action === "update" && params.provider !== undefined) + return errorResponse( + "provider is fixed at creation; omit it for update.", + ); + if (params.action !== "update" && params.provider === undefined) + return errorResponse( + 'provider is required. Ask the user whether they prefer Kernel-hosted collection (provider: "kernel") or 1Password brokered approval (provider: "1password") before creating credentials.', + ); + const target = { project, vault: params.vault, key: params.key }; + if (params.action === "connect_account") { + if (params.provider !== "1password") + return errorResponse( + 'connect_account supports only provider: "1password".', + ); + if (params.spec !== undefined) + return errorResponse("spec is not accepted for connect_account."); + const account = await client.vaults.items.upsert( + params.key, + { + id_or_name: params.vault, + type: "credential_account", + spec: { + provider: "1password", + authorization: { + method: "oauth", + client: { type: "kernel_managed" }, + }, + }, + }, + options, + ); + return vaultItemResponse(account, target); + } + if (params.spec === undefined) + return errorResponse("spec is required for create and update."); + if (params.action === "create" && params.provider === "1password") { + const spec = onePasswordCreateSpec.parse(params.spec); + const credential = await client.vaults.items.upsert( + params.key, + { + id_or_name: params.vault, + type: "credential", + spec: onePasswordCredentialSpec(spec), + }, + options, + ); + return vaultItemResponse(credential, target); + } let item: VaultItem; let writtenValues: (string | undefined)[]; if (params.action === "create") { @@ -173,7 +279,7 @@ export function registerVaultCredentialTools( { id_or_name: params.vault, type: "credential", - spec, + spec: { provider: "kernel", ...spec }, }, options, ); @@ -203,11 +309,7 @@ export function registerVaultCredentialTools( .filter(([name]) => !publicNames.has(name)) .map(([, field]) => field.value ?? undefined); } - return vaultItemResponse( - item, - { project, vault: params.vault, key: params.key }, - writtenValues, - ); + return vaultItemResponse(item, target, writtenValues); } catch (error) { throwVaultError("manage_vault_credentials", params.action, error); } diff --git a/src/lib/mcp/tools/vault-items.ts b/src/lib/mcp/tools/vault-items.ts index 6b920b9c..679973f7 100644 --- a/src/lib/mcp/tools/vault-items.ts +++ b/src/lib/mcp/tools/vault-items.ts @@ -29,7 +29,7 @@ export function registerVaultItemTools( "manage_vault_items", { description: - 'Inspect credential and payment vault items and immutable audit events. "list" reads items without renewing collection links; "get" reads state, safe field metadata, version, required user actions, available_operations, and available_expansions. MCP returns explicitly non-sensitive text/email values; sensitive values and TOTP seeds are omitted. For credentials, present the collection URL only to the intended user, outside the agent-controlled browser; never ask for passwords or TOTP seeds in chat. Reopen collection using its advertised operation when available; TOTP has no hosted input. wait observes readiness, not edits to ready credentials: compare versions using get without wait. Use manage_vault_credentials for credential creation and updates; use a per-user vault, site-name-only description, and sensitive:false for ordinary usernames/emails. Never store credit card data in credential items. "invoke" fetches the item again and submits only an advertised operation; read its description and obtain explicit user approval first. Provider actions (OAuth, enrollment, MFA, approval) must be completed by the user, not invoked as operations. "events" observes outcomes; use the last event ID as after. "delete" invalidates an item credential; confirm with the user first. Unresolved payments can block item and parent deletion; the API decides whether explicit abandonment is allowed, and deletion never proves a payment did not occur. recovery_required is not decline or expiry: stop payment attempts and reconcile with the provider or support; no reset exists. Credential ready means required values exist, not that login succeeded; payment ready does not mean paid. For browser field writes, supply operation-specific inputs with browser_id and ordered field/selector bindings; values stay server-side until entering the browser. Link cards use the advertised browser field-writing operation, not aliases or egress substitution: inputs.page_url must be the exact current HTTPS top-level page URL at the approved merchant origin, and the browser must retain its vault attachment. Browser field writes return no card values but do not isolate them from browser/CDP access or explicitly submit checkout; failed or unknown writes may leave partial changes. Never automatically retry or fall back to aliases. AgentCard aliases and checkout hold/approval/replay remain supported. Follow each advertised operation\'s API contract for inputs and outcome handling; never substitute another operation or retry an uncertain attempt. Requests are never automatically retried. Do not retry failed, timed-out, rejected, or indeterminate payments; inspect state/events instead.', + 'Inspect credential and payment vault items and immutable audit events. "list" reads items without renewing collection links; "get" reads state, safe field metadata, version, required user actions, available_operations, and available_expansions. MCP returns explicitly non-sensitive text/email values; sensitive values and TOTP seeds are omitted. For credentials, present the collection URL only to the intended user, outside the agent-controlled browser; never ask for passwords or TOTP seeds in chat. Reopen collection using its advertised operation when available; TOTP has no hosted input. wait observes readiness, not edits to ready credentials: compare versions using get without wait. Use manage_vault_credentials for credential creation and updates; use a per-user vault, site-name-only description, and sensitive:false for ordinary usernames/emails. Credentials follow one of two user-chosen paths: Kernel-hosted collection (collect, fill) or 1Password brokered approval (1pw_request_access, 1pw_poll_access, 1pw_reconcile_access, 1pw_fill on the credential; 1pw_recover on its credential_account). For 1Password, approval happens in the account owner\'s 1Password app; MCP never returns the native approval link or access-request references, and the agent never approves on the user\'s behalf. 1pw_fill submits the form but does not confirm login; never retry fill_unknown in the same browser. Never store credit card data in credential items. "invoke" fetches the item again and submits only an advertised operation; read its description and obtain explicit user approval first. Provider actions (OAuth, enrollment, MFA, approval) must be completed by the user, not invoked as operations. "events" observes outcomes; use the last event ID as after. "delete" invalidates an item credential; confirm with the user first. Unresolved payments can block item and parent deletion; the API decides whether explicit abandonment is allowed, and deletion never proves a payment did not occur. recovery_required is not decline or expiry: stop payment attempts and reconcile with the provider or support; no reset exists. Credential ready means required values exist, not that login succeeded; payment ready does not mean paid. For browser field writes, supply operation-specific inputs with browser_id and ordered field/selector bindings; values stay server-side until entering the browser. Link cards use the advertised browser field-writing operation, not aliases or egress substitution: inputs.page_url must be the exact current HTTPS top-level page URL at the approved merchant origin, and the browser must retain its vault attachment. Browser field writes return no card values but do not isolate them from browser/CDP access or explicitly submit checkout; failed or unknown writes may leave partial changes. Never automatically retry or fall back to aliases. AgentCard aliases and checkout hold/approval/replay remain supported. Follow each advertised operation\'s API contract for inputs and outcome handling; never substitute another operation or retry an uncertain attempt. Requests are never automatically retried. Do not retry failed, timed-out, rejected, or indeterminate payments; inspect state/events instead.', inputSchema: vaultToolInput({ ...vaultItemSchema, action: z.enum(["list", "get", "invoke", "events", "delete"]), @@ -180,7 +180,9 @@ export function registerVaultItemTools( projected !== null && "status" in projected && (projected.status === "failed" || - projected.status === "unknown") && { + projected.status === "unknown" || + projected.status === "fill_failed" || + projected.status === "fill_unknown") && { isError: true as const, }), }; diff --git a/src/lib/mcp/tools/vault-onepassword.test.ts b/src/lib/mcp/tools/vault-onepassword.test.ts new file mode 100644 index 00000000..885e01dd --- /dev/null +++ b/src/lib/mcp/tools/vault-onepassword.test.ts @@ -0,0 +1,400 @@ +import { describe, expect, test } from "bun:test"; +import { toolResultJSON } from "@/lib/mcp/mcp-test-fixtures"; +import { connectVaultTest } from "./vaults.test-fixtures"; + +const target = { vault: "user-123", key: "example-login" }; +const reference = "private-access-request-reference"; +const approvalLink = `onepassword://grant-brokered-access?access_request_reference=${reference}`; +const oauthURL = + "https://1password.example/oauth/authorize?client_id=kernel&state=opaque"; + +const account = { + id: "vi_account", + key: "onepassword", + type: "credential_account", + spec: { + provider: "1password", + authorization: { method: "oauth", client: { type: "kernel_managed" } }, + }, + state: { provider: "1password", status: "pending_authorization" }, + action: { name: "1password_oauth", url: oauthURL }, + available_operations: [], + available_expansions: [], + created_at: "2026-09-25T00:00:00Z", + updated_at: "2026-09-25T00:00:00Z", +}; + +const requests = { + version: 2, + goal: "Sign in to Example", + entries: [ + { + type: "login", + parameters: { website: "https://example.com/login" }, + reason: "Check order status", + keywords: ["example"], + }, + ], +}; + +const pendingCredential = { + id: "vi_credential", + key: target.key, + type: "credential", + version: 1, + spec: { provider: "1password", account_id: account.id, requests }, + state: { + provider: "1password", + status: "pending_authorization", + access_request_id: reference, + access_request: { + id: reference, + path: "private-provider-path", + identity: "private-provider-identity", + createdAt: "2026-09-25T00:00:00Z", + state: "pending", + has_autofill_token: false, + granted_count: 0, + entries: [{ ...requests.entries[0], id: "private-entry-id" }], + }, + }, + action: { + name: "1password_access_approval", + url: approvalLink, + instructions: `Present ${approvalLink} to the account owner.`, + }, + available_operations: [ + { type: "1pw_poll_access", description: "Check the request." }, + ], + available_expansions: [], + created_at: "2026-09-25T00:00:00Z", + updated_at: "2026-09-25T00:00:00Z", +}; + +const readyCredential = { + ...pendingCredential, + state: { + provider: "1password", + status: "ready", + access_request: { + ...pendingCredential.state.access_request, + state: "resolved", + has_autofill_token: true, + granted_count: 1, + }, + }, + action: undefined, + available_operations: [{ type: "1pw_fill", description: "Fill and submit." }], +}; + +function expectNoReferences(value: unknown) { + const text = JSON.stringify(value); + for (const privateValue of [ + reference, + "onepassword://", + "private-provider-path", + "private-provider-identity", + "private-entry-id", + "/approval/", + ]) + expect(text).not.toContain(privateValue); +} + +describe("1Password vault credentials", () => { + test("steers agents to ask which credential path the user prefers", async () => { + const fixture = await connectVaultTest([]); + try { + const { tools } = await fixture.client.listTools(); + const descriptionOf = (name: string) => + tools.find((tool) => tool.name === name)?.description ?? ""; + const credentials = descriptionOf("manage_vault_credentials"); + expect(credentials).toContain("two credential paths"); + expect(credentials).toContain("ask the user which they prefer"); + expect(credentials).toContain("Kernel-hosted collection"); + expect(credentials).toContain("1Password brokered approval"); + expect(credentials).toContain("never open, approve, or relay"); + expect(descriptionOf("manage_vaults")).toContain( + "Ask the user which they prefer", + ); + expect(descriptionOf("manage_vault_items")).toContain( + "1pw_request_access", + ); + expect(fixture.requests).toEqual([]); + } finally { + await fixture.close(); + } + }); + + test.each([ + { action: "create", spec: { fields: [{ name: "a", type: "text" }] } }, + { action: "connect_account" }, + ])("requires an explicit provider for $action", async (args) => { + const fixture = await connectVaultTest([]); + try { + const result = await fixture.call("manage_vault_credentials", { + ...target, + ...args, + }); + expect(result.isError).toBe(true); + expect(JSON.stringify(result)).toContain("Ask the user"); + expect(fixture.requests).toHaveLength(0); + } finally { + await fixture.close(); + } + }); + + test.each([ + { action: "connect_account", provider: "kernel" }, + { + action: "connect_account", + provider: "1password", + spec: { account_id: "vi_account", website: "https://example.com" }, + }, + { action: "update", provider: "1password", version: 1, spec: {} }, + { + action: "create", + provider: "1password", + spec: { account_id: "vi_account", website: "http://example.com" }, + }, + { + action: "create", + provider: "1password", + spec: { + account_id: "vi_account", + website: "https://example.com", + integration_key: "private-integration-key", + }, + }, + { + action: "create", + provider: "1password", + spec: { + account_id: "vi_account", + website: "https://example.com", + keywords: [], + }, + }, + ])("rejects invalid 1Password writes without requests (%#)", async (args) => { + const fixture = await connectVaultTest([]); + try { + const result = await fixture.call("manage_vault_credentials", { + ...target, + ...args, + }); + expect(result.isError).toBe(true); + expect(JSON.stringify(result)).not.toContain("private-integration-key"); + expect(fixture.requests).toHaveLength(0); + } finally { + await fixture.close(); + } + }); + + test("connects a Kernel-managed 1Password account for human consent", async () => { + const fixture = await connectVaultTest([Response.json(account)]); + try { + const result = toolResultJSON( + await fixture.call("manage_vault_credentials", { + vault: target.vault, + key: account.key, + action: "connect_account", + provider: "1password", + }), + ); + expect(fixture.requests[0]).toMatchObject({ + method: "PUT", + body: { + type: "credential_account", + spec: { + provider: "1password", + authorization: { + method: "oauth", + client: { type: "kernel_managed" }, + }, + }, + }, + }); + expect(result.item.action).toEqual({ + name: "1password_oauth", + url: oauthURL, + }); + expect(result.item.state.status).toBe("pending_authorization"); + expect(result.guidance.join(" ")).toContain("only to the account owner"); + expect(result.guidance.join(" ")).toContain("account_id"); + } finally { + await fixture.close(); + } + }); + + test("creates a 1Password credential from a single login request", async () => { + const fixture = await connectVaultTest([Response.json(pendingCredential)]); + try { + const result = await fixture.call("manage_vault_credentials", { + ...target, + action: "create", + provider: "1password", + spec: { + account_id: account.id, + website: "https://example.com/login", + goal: requests.goal, + reason: "Check order status", + keywords: ["example"], + }, + }); + expect(result.isError).toBeUndefined(); + expect(fixture.requests).toHaveLength(1); + expect(fixture.requests[0]).toMatchObject({ + method: "PUT", + body: { + type: "credential", + spec: { provider: "1password", account_id: account.id, requests }, + }, + }); + expectNoReferences(result); + } finally { + await fixture.close(); + } + }); + + test("withholds approval links and request references from item output", async () => { + const legacyApprovalPage = { + ...pendingCredential, + action: { + ...pendingCredential.action, + url: `https://api.example/vault/onepassword/approval/vi_credential/${reference}`, + }, + }; + for (const item of [pendingCredential, legacyApprovalPage]) { + const fixture = await connectVaultTest([Response.json(item)]); + try { + const result = toolResultJSON( + await fixture.call("manage_vault_items", { + ...target, + action: "get", + }), + ); + expectNoReferences(result); + expect(result.item.action).toEqual({ + name: "1password_access_approval", + }); + expect(result.item.spec.requests).toEqual(requests); + expect(result.item.state).toEqual({ + provider: "1password", + status: "pending_authorization", + access_request: { + state: "pending", + has_autofill_token: false, + granted_count: 0, + entries: requests.entries, + }, + }); + const guidance = result.guidance.join(" "); + expect(guidance).toContain("withholds the native approval link"); + expect(guidance).toContain("1pw_poll_access"); + expect(guidance).not.toContain("collection URL"); + } finally { + await fixture.close(); + } + } + }); + + test("request access returns a pending item without the approval link", async () => { + const requestable = { + ...pendingCredential, + state: { provider: "1password", status: "pending_authorization" }, + action: undefined, + available_operations: [ + { type: "1pw_request_access", description: "Request access." }, + ], + }; + const fixture = await connectVaultTest([ + Response.json(requestable), + Response.json(pendingCredential), + ]); + try { + const result = await fixture.call("manage_vault_items", { + ...target, + action: "invoke", + operation: "1pw_request_access", + inputs: { browser_id: "browser-1", reason: "Check order status" }, + }); + expect(result.isError).toBeUndefined(); + expect(fixture.requests[1].body).toEqual({ + type: "1pw_request_access", + browser_id: "browser-1", + reason: "Check order status", + }); + expectNoReferences(result); + } finally { + await fixture.close(); + } + }); + + test.each([ + { status: "fill_submitted", isError: undefined }, + { status: "fill_failed", error_code: "fillFailed", isError: true }, + { status: "fill_unknown", isError: true }, + ])( + "reports 1pw_fill $status without treating it as login success", + async ({ status, error_code, isError }) => { + const fixture = await connectVaultTest([ + Response.json(readyCredential), + Response.json({ + type: "1pw_fill", + status, + ...(error_code && { error_code }), + detail: `private ${reference}`, + }), + ]); + try { + const result = await fixture.call("manage_vault_items", { + ...target, + action: "invoke", + operation: "1pw_fill", + inputs: { + browser_id: "browser-1", + page_url: "https://example.com/login", + }, + }); + expect(result.isError).toBe(isError); + const body = toolResultJSON(result); + expect(body.result).toEqual({ + type: "1pw_fill", + status, + ...(error_code && { error_code }), + }); + expectNoReferences(result); + } finally { + await fixture.close(); + } + }, + ); + + test("curates 1Password operation errors", async () => { + const fixture = await connectVaultTest([ + Response.json({ + ...pendingCredential, + action: undefined, + available_operations: [ + { type: "1pw_request_access", description: "Request access." }, + ], + }), + Response.json( + { code: "conflict", message: `private ${approvalLink}` }, + { status: 409 }, + ), + ]); + try { + const result = await fixture.call("manage_vault_items", { + ...target, + action: "invoke", + operation: "1pw_request_access", + inputs: { browser_id: "browser-1" }, + }); + expect(result.isError).toBe(true); + expectNoReferences(result); + expect(fixture.requests).toHaveLength(2); + } finally { + await fixture.close(); + } + }); +}); diff --git a/src/lib/mcp/tools/vaults.ts b/src/lib/mcp/tools/vaults.ts index 6724376a..6668906d 100644 --- a/src/lib/mcp/tools/vaults.ts +++ b/src/lib/mcp/tools/vaults.ts @@ -42,7 +42,7 @@ export function registerVaultCapabilities( "manage_vaults", { description: - 'Manage project-owned vaults for end-user credentials and payment items. Use a separate vault per end user, with an immutable name such as user-123; do not mix unrelated users. Vaults store credentials, not authenticated browser sessions, and do not submit website forms or merchant payments. "create" creates or retrieves a vault by immutable name; "list" lists the effective project only; "get" reads one; "delete" invalidates the vault and every item credential. Confirm deletion with the user first; unresolved payment operations block deletion and require provider/support reconciliation. Connect a payment wallet with manage_vault_wallets, configure a card with manage_vault_cards, and inspect credentials or payment items with manage_vault_items. Use manage_vault_credentials to create definitions or update values, then manage_vault_items to collect, observe readiness, and invoke fill with value-free bindings. For credentials, inspect the website and create the named field definitions in its natural top-to-bottom order because that array order controls the user-facing form. Use only the recognizable site name as description and set sensitive:false explicitly for ordinary usernames/emails; passwords and TOTP seeds must be sensitive. Never put credit card data in credential items. Attach vaults when creating a browser; bindings cannot change later. Requests are not automatically retried.', + 'Manage project-owned vaults for end-user credentials and payment items. Use a separate vault per end user, with an immutable name such as user-123; do not mix unrelated users. Vaults store credentials, not authenticated browser sessions, and do not submit website forms or merchant payments. "create" creates or retrieves a vault by immutable name; "list" lists the effective project only; "get" reads one; "delete" invalidates the vault and every item credential. Confirm deletion with the user first; unresolved payment operations block deletion and require provider/support reconciliation. Connect a payment wallet with manage_vault_wallets, configure a card with manage_vault_cards, and inspect credentials or payment items with manage_vault_items. Credentials have two paths: Kernel-hosted collection or 1Password brokered approval. Ask the user which they prefer before creating credentials with manage_vault_credentials. For Kernel-hosted collection, create definitions or update values, then use manage_vault_items to collect, observe readiness, and invoke fill with value-free bindings. For 1Password, connect the account, create the credential, and let the user approve access in their 1Password app. For credentials, inspect the website and create the named field definitions in its natural top-to-bottom order because that array order controls the user-facing form. Use only the recognizable site name as description and set sensitive:false explicitly for ordinary usernames/emails; passwords and TOTP seeds must be sensitive. Never put credit card data in credential items. Attach vaults when creating a browser; bindings cannot change later. Requests are not automatically retried.', inputSchema: vaultToolInput({ ...vaultProjectSchema, action: z.enum(["create", "list", "get", "delete"]), diff --git a/src/lib/mcp/vault-responses.ts b/src/lib/mcp/vault-responses.ts index 916bef44..e9ae5e61 100644 --- a/src/lib/mcp/vault-responses.ts +++ b/src/lib/mcp/vault-responses.ts @@ -19,6 +19,15 @@ export const vaultProviderConfigFields = fields( ); const operationFields = fields("type description"); const totalFields = fields("type display_text amount"); +// Access-request IDs, provider paths, identities, and entry IDs are omitted. +const onePasswordRequestEntryFields = { + ...fields("type reason keywords"), + parameters: fields("website"), +}; +const onePasswordRequestFields = { + ...fields("version goal"), + entries: onePasswordRequestEntryFields, +}; const paymentMethodFields = { ...fields("id provider type is_default"), display: fields("label brand last4"), @@ -35,8 +44,9 @@ export const vaultItemFields: OutputFields = { expanded: { payment_methods: paymentMethodFields }, spec: { ...fields( - "provider wallet user_id payment_method_id card_id amount currency merchant merchant_name merchant_url context expires_at description", + "provider wallet user_id payment_method_id card_id amount currency merchant merchant_name merchant_url context expires_at description account_id", ), + requests: onePasswordRequestFields, fields: fields("name label type required sensitive"), provider_config: fields("id name"), authorization: { @@ -54,6 +64,11 @@ export const vaultItemFields: OutputFields = { state: { ...fields("provider status status_reason user_id domains"), fields: { "*": fields("has_value") }, + access_request: { + ...fields("state has_autofill_token granted_count goal"), + request: onePasswordRequestFields, + entries: onePasswordRequestEntryFields, + }, preparation: fields( "id status browser_id merchant_origin environment created_at expires_at approval_url", ), @@ -66,7 +81,7 @@ export const vaultItemFields: OutputFields = { }; export const vaultOperationResultFields: OutputFields = { - ...fields("type status"), + ...fields("type status error_code"), fields: fields("index status error_code"), }; @@ -77,6 +92,10 @@ export const vaultEventFields: OutputFields = { ), }; +// Native 1Password approvals are human actions. Their links carry access-request +// references, so only the action name reaches MCP output, whatever the scheme. +const onePasswordAccessApproval = "1password_access_approval"; + const urlFields = new Set([ "url", "approval_url", @@ -185,6 +204,14 @@ export function projectVaultOutput( } result[key] = projectVaultOutput(field, children); } + if ( + allowed === vaultItemFields && + z + .object({ name: z.literal(onePasswordAccessApproval) }) + .safeParse(result.action).success + ) { + result.action = { name: onePasswordAccessApproval }; + } if (allowed === vaultItemFields && result.type === "credential") { const credential = credentialValuesSchema.safeParse(value); if (credential.success) { @@ -294,11 +321,25 @@ export function vaultItemResponse( secrets: (string | undefined)[] = [], ) { const projected = projectVaultOutput(item, vaultItemFields); + const typed = z + .object({ + type: z.string(), + spec: z.object({ provider: z.string().optional() }).optional(), + }) + .safeParse(projected); + const onePasswordAccount = + typed.success && typed.data.type === "credential_account"; + const onePasswordCredential = + typed.success && + typed.data.type === "credential" && + typed.data.spec?.provider === "1password"; const credential = - projected !== null && - typeof projected === "object" && - "type" in projected && - projected.type === "credential"; + typed.success && typed.data.type === "credential" && !onePasswordCredential; + const onePasswordGuidance = onePasswordAccount + ? onePasswordAccountGuidance + : onePasswordCredential + ? onePasswordCredentialGuidance + : undefined; const advertised = advertisedOperationsSchema.safeParse(projected); const payment = z .object({ @@ -327,42 +368,55 @@ export function vaultItemResponse( .filter(safeHint) : [], }, - guidance: credential - ? [ - "Present the collection URL only to the intended user in a private surface, outside the agent-controlled browser. It is a bearer credential. Never ask for passwords or TOTP seeds in chat; TOTP seeds require trusted backend provisioning, not hosted collection.", - "MCP returns field definitions, has_value, version, collection expiry, and explicitly non-sensitive text/email values. Sensitive values and TOTP seeds are never returned. Ready means required values exist, not that login succeeded. Listing does not renew collection links; use get or the advertised collection operation.", - 'Use manage_vault_items with action: "invoke" and the advertised collection operation to reopen the full form without clearing values or changing readiness or version. wait observes readiness, not edits to ready items. Compare versions with get without wait; a change can also come from an API update, so it does not identify a specific form submission.', - "Create or update credentials with manage_vault_credentials. On create, inspect the website and list the named field definitions in its natural top-to-bottom order; that array order directly controls the user-facing collection form. Use optional non-secret labels for human-readable text; stable names remain authoritative for state, updates, and fill. Use a per-user vault, a recognizable site-name-only description, and sensitive:false for usernames/emails. Passwords and TOTP must be sensitive. Updates require the current version; supply expected_item_id when bound to an earlier read. Omitted values remain; null or empty strings clear supported fields, including required text/email/password fields. Hosted forms still require populated required inputs. Do not store payment-card data in credential items.", - "Invocation hints are not approval to execute. Invoke the advertised browser field-writing operation with manage_vault_items using an inputs object containing browser_id and ordered fields of field/selector bindings, never values. Bind the vault at browser creation, authorize the destination, and follow the advertised description. Fill does not submit or navigate; real values enter the browser and may be read by an agent with browser access. Never retry an uncertain fill or fall back to aliases.", - ] - : [ - "Ask the user to complete returned provider actions. Never request card data or OAuth codes/tokens in chat; imported grants must come from a trusted backend. Read operation descriptions and obtain explicit user approval before invoking.", - "Invocation hints are not approval to execute. Availability may change; invoke rechecks the advertised operations. Ready does not mean paid.", - ...(payment.success && payment.data.type === "wallet" - ? [ - "Wallets connect a payment provider; they are not fillable cards. Use manage_vault_cards to configure a purchase request, then inspect that card's state and advertised operations.", - ] - : []), - ...(cardProvider === "link" - ? [ - "Link cards use browser field writes for checkout only when advertised. Link does not expose aliases or support egress substitution; do not use aliases from older responses, which fail closed on supported payment shapes. The browser must retain this vault attachment in the same project. The exact current HTTPS top-level page URL must have the origin of spec.merchant_url. The card must remain ready and unexpired with stored card material and a non-deleted parent wallet; lifecycle and destination checks still apply.", - "When the field-writing operation is advertised, pass inputs with browser_id, exact current top-level page_url (including path, query, and fragment), and ordered field/selector bindings, never values. A combined expiration field requires format MM/YY or MM/YYYY. Attach the vault at browser creation. The operation returns no card values and does not explicitly submit checkout; browser access can expose written values. Failed or unknown writes may leave partial changes. Never automatically retry or fall back to aliases. Completion means fields were written, not that the payment succeeded.", - ] - : []), - ...(cardProvider === "agentcard" - ? [ - "AgentCard aliases remain supported for explicitly chosen egress-substitution integrations: use only returned state.aliases in a browser created with this vault attached, respecting returned permitted domains. Checkout hold, approval, and replay remain supported; observe checkout authorization and approval URLs. Never fall back to aliases after an uncertain fill or preparation.", - "For checkout preparation, supply the API-required checkout context and deliver the returned approval URL and keep the approval page open. Poll the item until ready_to_submit, then submit native Pay before state.preparation.expires_at. Readiness lasts at most 30 seconds; polling does not extend it. Preparations are single-use even after failure or expiry. Preparation consumed means claimed, not payment success.", - ] - : []), - "Observe get/events for outcomes. Do not retry failed, timed-out, rejected, or indeterminate payments or reconfigure a card to retry them.", - "recovery_required is an unresolved original outcome, not decline or expiry. Stop payment attempts; reconcile with the provider or support. No reset exists, and deletion may be blocked for this item and its parents.", - ], + guidance: + onePasswordGuidance ?? + (credential + ? [ + "Present the collection URL only to the intended user in a private surface, outside the agent-controlled browser. It is a bearer credential. Never ask for passwords or TOTP seeds in chat; TOTP seeds require trusted backend provisioning, not hosted collection.", + "MCP returns field definitions, has_value, version, collection expiry, and explicitly non-sensitive text/email values. Sensitive values and TOTP seeds are never returned. Ready means required values exist, not that login succeeded. Listing does not renew collection links; use get or the advertised collection operation.", + 'Use manage_vault_items with action: "invoke" and the advertised collection operation to reopen the full form without clearing values or changing readiness or version. wait observes readiness, not edits to ready items. Compare versions with get without wait; a change can also come from an API update, so it does not identify a specific form submission.', + "Create or update credentials with manage_vault_credentials. On create, inspect the website and list the named field definitions in its natural top-to-bottom order; that array order directly controls the user-facing collection form. Use optional non-secret labels for human-readable text; stable names remain authoritative for state, updates, and fill. Use a per-user vault, a recognizable site-name-only description, and sensitive:false for usernames/emails. Passwords and TOTP must be sensitive. Updates require the current version; supply expected_item_id when bound to an earlier read. Omitted values remain; null or empty strings clear supported fields, including required text/email/password fields. Hosted forms still require populated required inputs. Do not store payment-card data in credential items.", + "Invocation hints are not approval to execute. Invoke the advertised browser field-writing operation with manage_vault_items using an inputs object containing browser_id and ordered fields of field/selector bindings, never values. Bind the vault at browser creation, authorize the destination, and follow the advertised description. Fill does not submit or navigate; real values enter the browser and may be read by an agent with browser access. Never retry an uncertain fill or fall back to aliases.", + ] + : [ + "Ask the user to complete returned provider actions. Never request card data or OAuth codes/tokens in chat; imported grants must come from a trusted backend. Read operation descriptions and obtain explicit user approval before invoking.", + "Invocation hints are not approval to execute. Availability may change; invoke rechecks the advertised operations. Ready does not mean paid.", + ...(payment.success && payment.data.type === "wallet" + ? [ + "Wallets connect a payment provider; they are not fillable cards. Use manage_vault_cards to configure a purchase request, then inspect that card's state and advertised operations.", + ] + : []), + ...(cardProvider === "link" + ? [ + "Link cards use browser field writes for checkout only when advertised. Link does not expose aliases or support egress substitution; do not use aliases from older responses, which fail closed on supported payment shapes. The browser must retain this vault attachment in the same project. The exact current HTTPS top-level page URL must have the origin of spec.merchant_url. The card must remain ready and unexpired with stored card material and a non-deleted parent wallet; lifecycle and destination checks still apply.", + "When the field-writing operation is advertised, pass inputs with browser_id, exact current top-level page_url (including path, query, and fragment), and ordered field/selector bindings, never values. A combined expiration field requires format MM/YY or MM/YYYY. Attach the vault at browser creation. The operation returns no card values and does not explicitly submit checkout; browser access can expose written values. Failed or unknown writes may leave partial changes. Never automatically retry or fall back to aliases. Completion means fields were written, not that the payment succeeded.", + ] + : []), + ...(cardProvider === "agentcard" + ? [ + "AgentCard aliases remain supported for explicitly chosen egress-substitution integrations: use only returned state.aliases in a browser created with this vault attached, respecting returned permitted domains. Checkout hold, approval, and replay remain supported; observe checkout authorization and approval URLs. Never fall back to aliases after an uncertain fill or preparation.", + "For checkout preparation, supply the API-required checkout context and deliver the returned approval URL and keep the approval page open. Poll the item until ready_to_submit, then submit native Pay before state.preparation.expires_at. Readiness lasts at most 30 seconds; polling does not extend it. Preparations are single-use even after failure or expiry. Preparation consumed means claimed, not payment success.", + ] + : []), + "Observe get/events for outcomes. Do not retry failed, timed-out, rejected, or indeterminate payments or reconfigure a card to retry them.", + "recovery_required is an unresolved original outcome, not decline or expiry. Stop payment attempts; reconcile with the provider or support. No reset exists, and deletion may be blocked for this item and its parents.", + ]), }, secrets, ); } +const onePasswordAccountGuidance = [ + "This credential_account connects a 1Password account; it is not a fillable credential. If an action URL is present, present it only to the account owner, outside the agent-controlled browser, and let them complete 1Password consent and verify the account shown there. Never ask for 1Password passwords, Secret Keys, OAuth codes, tokens, or integration keys in chat.", + 'Observe with get until state.status is connected, then create 1Password credentials with manage_vault_credentials, provider: "1password", and account_id set to this item\'s id. declined or reconnect_required need the user to connect again. Invoke 1pw_recover only when advertised and after explicit user approval; never delete the account to recover.', +]; + +const onePasswordCredentialGuidance = [ + "1Password credentials hold no values in Kernel. After explicit user approval, invoke the advertised 1pw_request_access with inputs {browser_id} and optional goal, reason, and keywords, using a browser created with this vault attached.", + "Approval is a human action in the account owner's 1Password app. MCP withholds the native approval link and access-request references; never open, approve, or relay an approval yourself. Tell the owner a request is waiting in 1Password, then invoke the advertised 1pw_poll_access with {browser_id} to observe the decision. Do not issue a second request while one is pending.", + "When ready, invoke the advertised 1pw_fill with {browser_id, page_url}, where page_url is the exact current top-level URL on the requested login origin. The extension selects fields and submits; you cannot supply selectors or values. fill_submitted does not confirm login. fill_unknown may have submitted; never retry it in the same browser. 1pw_reconcile_access only abandons an unconfirmed request after the user checks 1Password for an existing one, requires acknowledge_unconfirmed: true, and can lead to duplicate requests.", +]; + const vaultErrorMessages = new Map([ [ "invalid_request", From 8e84eb48c8b2bc172808fbe430d67a4673eaa9e1 Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Fri, 25 Sep 2026 03:10:14 +0000 Subject: [PATCH 2/9] Accept Kernel provider on update and spell out 1Password invoke shape --- src/lib/mcp/tools/vault-credentials.ts | 8 ++-- src/lib/mcp/tools/vault-onepassword.test.ts | 50 ++++++++++++++++++++- src/lib/mcp/vault-responses.ts | 8 ++-- 3 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/lib/mcp/tools/vault-credentials.ts b/src/lib/mcp/tools/vault-credentials.ts index c64ca457..869666fe 100644 --- a/src/lib/mcp/tools/vault-credentials.ts +++ b/src/lib/mcp/tools/vault-credentials.ts @@ -171,7 +171,7 @@ export function registerVaultCredentialTools( provider: z .enum(["kernel", "1password"]) .describe( - '(create, connect_account) The path the user chose. Ask the user before creating. connect_account supports only "1password".', + '(create, connect_account) The path the user chose. Ask the user before creating. connect_account supports only "1password". Update accepts only Kernel credentials.', ) .optional(), spec: z @@ -222,10 +222,8 @@ export function registerVaultCredentialTools( params.expected_item_id !== undefined) ) return errorResponse("version and expected_item_id are update-only."); - if (params.action === "update" && params.provider !== undefined) - return errorResponse( - "provider is fixed at creation; omit it for update.", - ); + if (params.action === "update" && params.provider === "1password") + return errorResponse("1Password credentials cannot be updated."); if (params.action !== "update" && params.provider === undefined) return errorResponse( 'provider is required. Ask the user whether they prefer Kernel-hosted collection (provider: "kernel") or 1Password brokered approval (provider: "1password") before creating credentials.', diff --git a/src/lib/mcp/tools/vault-onepassword.test.ts b/src/lib/mcp/tools/vault-onepassword.test.ts index 885e01dd..57c1d898 100644 --- a/src/lib/mcp/tools/vault-onepassword.test.ts +++ b/src/lib/mcp/tools/vault-onepassword.test.ts @@ -150,7 +150,12 @@ describe("1Password vault credentials", () => { provider: "1password", spec: { account_id: "vi_account", website: "https://example.com" }, }, - { action: "update", provider: "1password", version: 1, spec: {} }, + { + action: "update", + provider: "1password", + version: 1, + spec: { description: "Example" }, + }, { action: "create", provider: "1password", @@ -189,6 +194,46 @@ describe("1Password vault credentials", () => { } }); + test("accepts the Kernel provider on update", async () => { + const fixture = await connectVaultTest([ + Response.json({ + id: "vi_kernel", + key: target.key, + type: "credential", + version: 3, + spec: { + provider: "kernel", + description: "Example", + fields: [{ name: "username", type: "text", sensitive: false }], + }, + state: { + provider: "kernel", + status: "ready", + fields: { username: { has_value: true, value: "alice" } }, + }, + available_operations: [], + available_expansions: [], + }), + ]); + try { + const result = await fixture.call("manage_vault_credentials", { + ...target, + action: "update", + provider: "kernel", + version: 2, + spec: { description: "Example" }, + }); + expect(result.isError).toBeUndefined(); + expect(fixture.requests[0].body).toEqual({ + type: "credential", + version: 2, + spec: { description: "Example" }, + }); + } finally { + await fixture.close(); + } + }); + test("connects a Kernel-managed 1Password account for human consent", async () => { const fixture = await connectVaultTest([Response.json(account)]); try { @@ -289,7 +334,8 @@ describe("1Password vault credentials", () => { }); const guidance = result.guidance.join(" "); expect(guidance).toContain("withholds the native approval link"); - expect(guidance).toContain("1pw_poll_access"); + expect(guidance).toContain('action: "invoke"'); + expect(guidance).toContain('operation: "1pw_poll_access"'); expect(guidance).not.toContain("collection URL"); } finally { await fixture.close(); diff --git a/src/lib/mcp/vault-responses.ts b/src/lib/mcp/vault-responses.ts index e9ae5e61..f1a79a0c 100644 --- a/src/lib/mcp/vault-responses.ts +++ b/src/lib/mcp/vault-responses.ts @@ -408,13 +408,13 @@ export function vaultItemResponse( const onePasswordAccountGuidance = [ "This credential_account connects a 1Password account; it is not a fillable credential. If an action URL is present, present it only to the account owner, outside the agent-controlled browser, and let them complete 1Password consent and verify the account shown there. Never ask for 1Password passwords, Secret Keys, OAuth codes, tokens, or integration keys in chat.", - 'Observe with get until state.status is connected, then create 1Password credentials with manage_vault_credentials, provider: "1password", and account_id set to this item\'s id. declined or reconnect_required need the user to connect again. Invoke 1pw_recover only when advertised and after explicit user approval; never delete the account to recover.', + 'Observe with manage_vault_items action: "get" until state.status is connected, then create 1Password credentials with manage_vault_credentials, provider: "1password", and account_id set to this item\'s id. declined or reconnect_required need the user to connect again. Only when 1pw_recover is advertised and after explicit user approval, call manage_vault_items with action: "invoke" and operation: "1pw_recover"; never delete the account to recover.', ]; const onePasswordCredentialGuidance = [ - "1Password credentials hold no values in Kernel. After explicit user approval, invoke the advertised 1pw_request_access with inputs {browser_id} and optional goal, reason, and keywords, using a browser created with this vault attached.", - "Approval is a human action in the account owner's 1Password app. MCP withholds the native approval link and access-request references; never open, approve, or relay an approval yourself. Tell the owner a request is waiting in 1Password, then invoke the advertised 1pw_poll_access with {browser_id} to observe the decision. Do not issue a second request while one is pending.", - "When ready, invoke the advertised 1pw_fill with {browser_id, page_url}, where page_url is the exact current top-level URL on the requested login origin. The extension selects fields and submits; you cannot supply selectors or values. fill_submitted does not confirm login. fill_unknown may have submitted; never retry it in the same browser. 1pw_reconcile_access only abandons an unconfirmed request after the user checks 1Password for an existing one, requires acknowledge_unconfirmed: true, and can lead to duplicate requests.", + 'Operations use manage_vault_items with action: "invoke", operation set to the advertised 1pw_* type, and inputs for that operation. 1Password credentials hold no values in Kernel. After explicit user approval, invoke operation: "1pw_request_access" with inputs {browser_id} and optional goal, reason, and keywords, using a browser created with this vault attached.', + 'Approval is a human action in the account owner\'s 1Password app. MCP withholds the native approval link and access-request references; never open, approve, or relay an approval yourself. Tell the owner a request is waiting in 1Password, then invoke operation: "1pw_poll_access" with inputs {browser_id} to observe the decision. Do not issue a second request while one is pending.', + 'When ready, invoke operation: "1pw_fill" with inputs {browser_id, page_url}, where page_url is the exact current top-level URL on the requested login origin. The extension selects fields and submits; you cannot supply selectors or values. fill_submitted does not confirm login. fill_unknown may have submitted; never retry it in the same browser. operation: "1pw_reconcile_access" only abandons an unconfirmed request after the user checks 1Password for an existing one, requires inputs {acknowledge_unconfirmed: true}, and can lead to duplicate requests.', ]; const vaultErrorMessages = new Map([ From 0849eae3dc7d522012a992d0cea1bf2532b8b1f6 Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:18:20 +0000 Subject: [PATCH 3/9] Forward native 1Password approval links to the account owner --- docs/vault-payments.md | 15 ++- src/lib/mcp/tools/vault-credentials.ts | 2 +- src/lib/mcp/tools/vault-items.ts | 2 +- src/lib/mcp/tools/vault-onepassword.test.ts | 114 +++++++++++++------- src/lib/mcp/vault-responses.ts | 42 +++++++- 5 files changed, 124 insertions(+), 51 deletions(-) diff --git a/docs/vault-payments.md b/docs/vault-payments.md index 602798f4..a3f78cf6 100644 --- a/docs/vault-payments.md +++ b/docs/vault-payments.md @@ -155,11 +155,16 @@ prefer `collect` for human edits. Requests are not automatically retried. 3. With a browser created with the vault attached, and after explicit user approval, invoke the advertised `1pw_request_access` with `inputs: {"browser_id": "..."}`. -4. Approval is a human action in the account owner's 1Password app. MCP output never - includes the native approval link, access-request IDs or references, provider - paths or identities, OAuth tokens, or integration keys, and the agent must never - open, approve, or relay an approval. Invoke the advertised `1pw_poll_access` with - `browser_id` to observe the decision. +4. Approval is a human action in the account owner's 1Password app. The pending item + returns `action: {"name": "1password_access_approval", "url": "onepassword://grant-brokered-access?access_request_reference=..."}`. + Give that link, unmodified, only to the account owner in a private surface outside + the agent-controlled browser; they open it on a device with the 1Password app and + choose, approve, or deny the login there. The link grants nothing until they + approve, but it identifies the request, so the agent must never open, decode, or + approve it. MCP forwards only links in that exact native form and never returns + access-request IDs, provider paths or identities, OAuth tokens, or integration + keys. Invoke the advertised `1pw_poll_access` with `browser_id` to observe the + decision. 5. When the item is ready, invoke the advertised `1pw_fill` with `browser_id` and the exact current `page_url`. The extension selects fields and submits the form. `fill_submitted` does not confirm login; `fill_failed` and `fill_unknown` are tool diff --git a/src/lib/mcp/tools/vault-credentials.ts b/src/lib/mcp/tools/vault-credentials.ts index 869666fe..6baa258d 100644 --- a/src/lib/mcp/tools/vault-credentials.ts +++ b/src/lib/mcp/tools/vault-credentials.ts @@ -162,7 +162,7 @@ export function registerVaultCredentialTools( description: 'Create or update credential items in a per-end-user vault. There are two credential paths. Before creating any credential, ask the user which they prefer and set provider to match; never choose for them. provider:"kernel" is Kernel-hosted collection: the user enters values in a Kernel-hosted form and the agent fills them with value-free bindings. provider:"1password" is 1Password brokered approval: the user connects their 1Password account once, approves each login request in the 1Password app, and the 1Password extension fills and submits; Kernel stores no values. ' + 'Kernel path: use only the recognizable site name as description; explicitly set sensitive:false for ordinary usernames/emails. Each field may include an optional non-secret human-readable label; name remains the stable key for updates and browser fills. Passwords and TOTP seeds must be sensitive. Never store payment-card data here. For human collection, omit values and present the returned bearer collection URL privately to the intended user, outside the agent-controlled browser. Never ask for passwords or TOTP seeds in chat. TOTP seeds require trusted provisioning and have no hosted input. On create, fields is an ordered array of named definitions: inspect the website and list fields in its natural top-to-bottom order because this directly controls the user-facing collection form. Update fields remain keyed by name and contain only value. Updates require the latest version and optionally expected_item_id from an earlier read; definitions are immutable. Omitted values are preserved; null or empty strings clear supported values. Clearing required TOTP is unsupported. Hosted forms require populated required inputs. To reopen collection, use manage_vault_items with action: "invoke" and operation: "collect". Use manage_vault_items get with wait for readiness, then invoke fill with fill parameters. For edits to already-ready items compare versions without wait. Explicitly non-sensitive text/email values are returned; sensitive values and TOTP seeds are omitted. ' + - '1Password path: first use action "connect_account" with provider:"1password" and a new key; present the returned 1Password authorization URL only to the account owner, outside the agent-controlled browser, and let them verify the account on the consent screen. Once manage_vault_items get reports the account connected, create the credential with provider:"1password" and spec {account_id, website, optional goal/reason/keywords}. 1Password credentials cannot be updated. Approval is a human action in the 1Password app: MCP never returns the native approval link, access-request references, tokens, or integration keys, and you must never open, approve, or relay an approval yourself. This is unrelated to manage_credential_providers. ' + + '1Password path: first use action "connect_account" with provider:"1password" and a new key; present the returned 1Password authorization URL only to the account owner, outside the agent-controlled browser, and let them verify the account on the consent screen. Once manage_vault_items get reports the account connected, create the credential with provider:"1password" and spec {account_id, website, optional goal/reason/keywords}. 1Password credentials cannot be updated. Approval is a human action in the 1Password app: after 1pw_request_access, give the returned native onepassword:// approval link unmodified only to the account owner, outside the agent-controlled browser, and never open, decode, or approve it yourself. MCP never returns access-request IDs, tokens, or integration keys. This is unrelated to manage_credential_providers. ' + "Writes are never automatically retried; reconcile conflicts or uncertain outcomes before any further write.", inputSchema: vaultToolInput({ ...vaultItemSchema, diff --git a/src/lib/mcp/tools/vault-items.ts b/src/lib/mcp/tools/vault-items.ts index 679973f7..82ab5943 100644 --- a/src/lib/mcp/tools/vault-items.ts +++ b/src/lib/mcp/tools/vault-items.ts @@ -29,7 +29,7 @@ export function registerVaultItemTools( "manage_vault_items", { description: - 'Inspect credential and payment vault items and immutable audit events. "list" reads items without renewing collection links; "get" reads state, safe field metadata, version, required user actions, available_operations, and available_expansions. MCP returns explicitly non-sensitive text/email values; sensitive values and TOTP seeds are omitted. For credentials, present the collection URL only to the intended user, outside the agent-controlled browser; never ask for passwords or TOTP seeds in chat. Reopen collection using its advertised operation when available; TOTP has no hosted input. wait observes readiness, not edits to ready credentials: compare versions using get without wait. Use manage_vault_credentials for credential creation and updates; use a per-user vault, site-name-only description, and sensitive:false for ordinary usernames/emails. Credentials follow one of two user-chosen paths: Kernel-hosted collection (collect, fill) or 1Password brokered approval (1pw_request_access, 1pw_poll_access, 1pw_reconcile_access, 1pw_fill on the credential; 1pw_recover on its credential_account). For 1Password, approval happens in the account owner\'s 1Password app; MCP never returns the native approval link or access-request references, and the agent never approves on the user\'s behalf. 1pw_fill submits the form but does not confirm login; never retry fill_unknown in the same browser. Never store credit card data in credential items. "invoke" fetches the item again and submits only an advertised operation; read its description and obtain explicit user approval first. Provider actions (OAuth, enrollment, MFA, approval) must be completed by the user, not invoked as operations. "events" observes outcomes; use the last event ID as after. "delete" invalidates an item credential; confirm with the user first. Unresolved payments can block item and parent deletion; the API decides whether explicit abandonment is allowed, and deletion never proves a payment did not occur. recovery_required is not decline or expiry: stop payment attempts and reconcile with the provider or support; no reset exists. Credential ready means required values exist, not that login succeeded; payment ready does not mean paid. For browser field writes, supply operation-specific inputs with browser_id and ordered field/selector bindings; values stay server-side until entering the browser. Link cards use the advertised browser field-writing operation, not aliases or egress substitution: inputs.page_url must be the exact current HTTPS top-level page URL at the approved merchant origin, and the browser must retain its vault attachment. Browser field writes return no card values but do not isolate them from browser/CDP access or explicitly submit checkout; failed or unknown writes may leave partial changes. Never automatically retry or fall back to aliases. AgentCard aliases and checkout hold/approval/replay remain supported. Follow each advertised operation\'s API contract for inputs and outcome handling; never substitute another operation or retry an uncertain attempt. Requests are never automatically retried. Do not retry failed, timed-out, rejected, or indeterminate payments; inspect state/events instead.', + 'Inspect credential and payment vault items and immutable audit events. "list" reads items without renewing collection links; "get" reads state, safe field metadata, version, required user actions, available_operations, and available_expansions. MCP returns explicitly non-sensitive text/email values; sensitive values and TOTP seeds are omitted. For credentials, present the collection URL only to the intended user, outside the agent-controlled browser; never ask for passwords or TOTP seeds in chat. Reopen collection using its advertised operation when available; TOTP has no hosted input. wait observes readiness, not edits to ready credentials: compare versions using get without wait. Use manage_vault_credentials for credential creation and updates; use a per-user vault, site-name-only description, and sensitive:false for ordinary usernames/emails. Credentials follow one of two user-chosen paths: Kernel-hosted collection (collect, fill) or 1Password brokered approval (1pw_request_access, 1pw_poll_access, 1pw_reconcile_access, 1pw_fill on the credential; 1pw_recover on its credential_account). For 1Password, approval happens in the account owner\'s 1Password app: give the native onepassword:// approval link only to the owner, outside the agent-controlled browser, and never open or approve it yourself; MCP never returns access-request IDs. 1pw_fill submits the form but does not confirm login; never retry fill_unknown in the same browser. Never store credit card data in credential items. "invoke" fetches the item again and submits only an advertised operation; read its description and obtain explicit user approval first. Provider actions (OAuth, enrollment, MFA, approval) must be completed by the user, not invoked as operations. "events" observes outcomes; use the last event ID as after. "delete" invalidates an item credential; confirm with the user first. Unresolved payments can block item and parent deletion; the API decides whether explicit abandonment is allowed, and deletion never proves a payment did not occur. recovery_required is not decline or expiry: stop payment attempts and reconcile with the provider or support; no reset exists. Credential ready means required values exist, not that login succeeded; payment ready does not mean paid. For browser field writes, supply operation-specific inputs with browser_id and ordered field/selector bindings; values stay server-side until entering the browser. Link cards use the advertised browser field-writing operation, not aliases or egress substitution: inputs.page_url must be the exact current HTTPS top-level page URL at the approved merchant origin, and the browser must retain its vault attachment. Browser field writes return no card values but do not isolate them from browser/CDP access or explicitly submit checkout; failed or unknown writes may leave partial changes. Never automatically retry or fall back to aliases. AgentCard aliases and checkout hold/approval/replay remain supported. Follow each advertised operation\'s API contract for inputs and outcome handling; never substitute another operation or retry an uncertain attempt. Requests are never automatically retried. Do not retry failed, timed-out, rejected, or indeterminate payments; inspect state/events instead.', inputSchema: vaultToolInput({ ...vaultItemSchema, action: z.enum(["list", "get", "invoke", "events", "delete"]), diff --git a/src/lib/mcp/tools/vault-onepassword.test.ts b/src/lib/mcp/tools/vault-onepassword.test.ts index 57c1d898..1612f88b 100644 --- a/src/lib/mcp/tools/vault-onepassword.test.ts +++ b/src/lib/mcp/tools/vault-onepassword.test.ts @@ -3,8 +3,11 @@ import { toolResultJSON } from "@/lib/mcp/mcp-test-fixtures"; import { connectVaultTest } from "./vaults.test-fixtures"; const target = { vault: "user-123", key: "example-login" }; -const reference = "private-access-request-reference"; +const requestID = "private-access-request-id"; +const reference = "eyJpZCI6InByaXZhdGUtYWNjZXNzLXJlcXVlc3QtaWQifQ"; const approvalLink = `onepassword://grant-brokered-access?access_request_reference=${reference}`; +const approvalInstructions = + "Present this link to the account owner to open on their device with the 1Password app."; const oauthURL = "https://1password.example/oauth/authorize?client_id=kernel&state=opaque"; @@ -46,9 +49,9 @@ const pendingCredential = { state: { provider: "1password", status: "pending_authorization", - access_request_id: reference, + access_request_id: requestID, access_request: { - id: reference, + id: requestID, path: "private-provider-path", identity: "private-provider-identity", createdAt: "2026-09-25T00:00:00Z", @@ -61,7 +64,7 @@ const pendingCredential = { action: { name: "1password_access_approval", url: approvalLink, - instructions: `Present ${approvalLink} to the account owner.`, + instructions: approvalInstructions, }, available_operations: [ { type: "1pw_poll_access", description: "Check the request." }, @@ -87,11 +90,11 @@ const readyCredential = { available_operations: [{ type: "1pw_fill", description: "Fill and submit." }], }; -function expectNoReferences(value: unknown) { +function expectNoReferences(value: unknown, { approvalLink = false } = {}) { const text = JSON.stringify(value); for (const privateValue of [ - reference, - "onepassword://", + requestID, + ...(approvalLink ? [] : [reference, "access_request_reference="]), "private-provider-path", "private-provider-identity", "private-entry-id", @@ -112,7 +115,7 @@ describe("1Password vault credentials", () => { expect(credentials).toContain("ask the user which they prefer"); expect(credentials).toContain("Kernel-hosted collection"); expect(credentials).toContain("1Password brokered approval"); - expect(credentials).toContain("never open, approve, or relay"); + expect(credentials).toContain("never open, decode, or approve"); expect(descriptionOf("manage_vaults")).toContain( "Ask the user which they prefer", ); @@ -294,22 +297,64 @@ describe("1Password vault credentials", () => { spec: { provider: "1password", account_id: account.id, requests }, }, }); - expectNoReferences(result); + expectNoReferences(result, { approvalLink: true }); } finally { await fixture.close(); } }); - test("withholds approval links and request references from item output", async () => { - const legacyApprovalPage = { - ...pendingCredential, - action: { - ...pendingCredential.action, - url: `https://api.example/vault/onepassword/approval/vi_credential/${reference}`, - }, - }; - for (const item of [pendingCredential, legacyApprovalPage]) { - const fixture = await connectVaultTest([Response.json(item)]); + test("forwards only the native approval link for the account owner", async () => { + const fixture = await connectVaultTest([Response.json(pendingCredential)]); + try { + const result = toolResultJSON( + await fixture.call("manage_vault_items", { ...target, action: "get" }), + ); + expectNoReferences(result, { approvalLink: true }); + expect(result.item.action).toEqual({ + name: "1password_access_approval", + url: approvalLink, + instructions: approvalInstructions, + }); + expect(result.item.spec.requests).toEqual(requests); + expect(result.item.state).toEqual({ + provider: "1password", + status: "pending_authorization", + access_request: { + state: "pending", + has_autofill_token: false, + granted_count: 0, + entries: requests.entries, + }, + }); + const guidance = result.guidance.join(" "); + expect(guidance).toContain("unmodified only to the account owner"); + expect(guidance).toContain("never open it in a browser"); + expect(guidance).toContain('action: "invoke"'); + expect(guidance).toContain('operation: "1pw_poll_access"'); + expect(guidance).not.toContain("collection URL"); + } finally { + await fixture.close(); + } + }); + + test.each([ + `https://api.example/vault/onepassword/approval/vi_credential/${reference}`, + `onepassword://grant-brokered-access?access_request_reference=${reference}&access_token=secret`, + `onepassword://grant-brokered-access?access_request_reference=${reference}&access_request_reference=${reference}`, + `onepassword://other-host?access_request_reference=${reference}`, + `onepassword://user:pass@grant-brokered-access?access_request_reference=${reference}`, + `onepassword://grant-brokered-access/path?access_request_reference=${reference}`, + `onepassword://grant-brokered-access?access_request_reference=${reference}#fragment`, + "onepassword://grant-brokered-access?access_request_reference=not%20base64url", + ])( + "reduces non-native approval links to the action name: %s", + async (url) => { + const fixture = await connectVaultTest([ + Response.json({ + ...pendingCredential, + action: { ...pendingCredential.action, url }, + }), + ]); try { const result = toolResultJSON( await fixture.call("manage_vault_items", { @@ -318,32 +363,20 @@ describe("1Password vault credentials", () => { }), ); expectNoReferences(result); + expect(JSON.stringify(result)).not.toContain("secret"); expect(result.item.action).toEqual({ name: "1password_access_approval", }); - expect(result.item.spec.requests).toEqual(requests); - expect(result.item.state).toEqual({ - provider: "1password", - status: "pending_authorization", - access_request: { - state: "pending", - has_autofill_token: false, - granted_count: 0, - entries: requests.entries, - }, - }); - const guidance = result.guidance.join(" "); - expect(guidance).toContain("withholds the native approval link"); - expect(guidance).toContain('action: "invoke"'); - expect(guidance).toContain('operation: "1pw_poll_access"'); - expect(guidance).not.toContain("collection URL"); + expect(result.guidance.join(" ")).toContain( + "tell the owner the approval link is unavailable", + ); } finally { await fixture.close(); } - } - }); + }, + ); - test("request access returns a pending item without the approval link", async () => { + test("request access returns the pending item with its approval link", async () => { const requestable = { ...pendingCredential, state: { provider: "1password", status: "pending_authorization" }, @@ -369,7 +402,8 @@ describe("1Password vault credentials", () => { browser_id: "browser-1", reason: "Check order status", }); - expectNoReferences(result); + expectNoReferences(result, { approvalLink: true }); + expect(toolResultJSON(result).item.action.url).toBe(approvalLink); } finally { await fixture.close(); } @@ -388,7 +422,7 @@ describe("1Password vault credentials", () => { type: "1pw_fill", status, ...(error_code && { error_code }), - detail: `private ${reference}`, + detail: `private ${reference} ${requestID}`, }), ]); try { diff --git a/src/lib/mcp/vault-responses.ts b/src/lib/mcp/vault-responses.ts index f1a79a0c..3c41b3cb 100644 --- a/src/lib/mcp/vault-responses.ts +++ b/src/lib/mcp/vault-responses.ts @@ -92,10 +92,39 @@ export const vaultEventFields: OutputFields = { ), }; -// Native 1Password approvals are human actions. Their links carry access-request -// references, so only the action name reaches MCP output, whatever the scheme. +// Native 1Password approvals are human actions: the account owner opens the link +// in their 1Password app, and it grants nothing until they approve there. Only a +// link in the exact native form is forwarded; anything else, including legacy +// nonce approval pages, is reduced to the action name. const onePasswordAccessApproval = "1password_access_approval"; +const onePasswordApprovalActionSchema = z.object({ + name: z.literal(onePasswordAccessApproval), + url: z.string().refine(isNativeOnePasswordApprovalLink), + instructions: z.string().optional(), +}); + +function isNativeOnePasswordApprovalLink(value: string): boolean { + try { + const url = new URL(value); + const references = url.searchParams.getAll("access_request_reference"); + return ( + url.protocol === "onepassword:" && + url.host === "grant-brokered-access" && + !url.username && + !url.password && + !url.port && + url.pathname === "" && + !url.hash && + [...url.searchParams.keys()].length === 1 && + references.length === 1 && + /^[A-Za-z0-9_-]{1,65536}$/.test(references[0]) + ); + } catch { + return false; + } +} + const urlFields = new Set([ "url", "approval_url", @@ -210,7 +239,12 @@ export function projectVaultOutput( .object({ name: z.literal(onePasswordAccessApproval) }) .safeParse(result.action).success ) { - result.action = { name: onePasswordAccessApproval }; + const approval = onePasswordApprovalActionSchema.safeParse( + Reflect.get(value, "action"), + ); + result.action = approval.success + ? approval.data + : { name: onePasswordAccessApproval }; } if (allowed === vaultItemFields && result.type === "credential") { const credential = credentialValuesSchema.safeParse(value); @@ -413,7 +447,7 @@ const onePasswordAccountGuidance = [ const onePasswordCredentialGuidance = [ 'Operations use manage_vault_items with action: "invoke", operation set to the advertised 1pw_* type, and inputs for that operation. 1Password credentials hold no values in Kernel. After explicit user approval, invoke operation: "1pw_request_access" with inputs {browser_id} and optional goal, reason, and keywords, using a browser created with this vault attached.', - 'Approval is a human action in the account owner\'s 1Password app. MCP withholds the native approval link and access-request references; never open, approve, or relay an approval yourself. Tell the owner a request is waiting in 1Password, then invoke operation: "1pw_poll_access" with inputs {browser_id} to observe the decision. Do not issue a second request while one is pending.', + 'Approval is a human action in the account owner\'s 1Password app. When action.name is 1password_access_approval with a url, give that onepassword:// link unmodified only to the account owner, in a private surface outside the agent-controlled browser, to open on a device with the 1Password app; they choose the login and approve or deny there. The link grants nothing until they approve, but it identifies the request: never open it in a browser, decode it, post it where others can see it, or approve on their behalf. Without a url, MCP received no native link: tell the owner the approval link is unavailable and do not request again while pending. Invoke operation: "1pw_poll_access" with inputs {browser_id} to observe the decision. Do not issue a second request while one is pending.', 'When ready, invoke operation: "1pw_fill" with inputs {browser_id, page_url}, where page_url is the exact current top-level URL on the requested login origin. The extension selects fields and submits; you cannot supply selectors or values. fill_submitted does not confirm login. fill_unknown may have submitted; never retry it in the same browser. operation: "1pw_reconcile_access" only abandons an unconfirmed request after the user checks 1Password for an existing one, requires inputs {acknowledge_unconfirmed: true}, and can lead to duplicate requests.', ]; From 9b61e494a8c363252f01615340a238c578e81c76 Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:24:10 +0000 Subject: [PATCH 4/9] Forward only the native approval URL, not API instructions --- docs/vault-payments.md | 3 ++- src/lib/mcp/tools/vault-onepassword.test.ts | 30 ++++++++++++++++++--- src/lib/mcp/vault-responses.ts | 21 ++++++--------- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/docs/vault-payments.md b/docs/vault-payments.md index a3f78cf6..57705961 100644 --- a/docs/vault-payments.md +++ b/docs/vault-payments.md @@ -161,7 +161,8 @@ prefer `collect` for human edits. Requests are not automatically retried. the agent-controlled browser; they open it on a device with the 1Password app and choose, approve, or deny the login there. The link grants nothing until they approve, but it identifies the request, so the agent must never open, decode, or - approve it. MCP forwards only links in that exact native form and never returns + approve it. MCP forwards only links in that exact native form, without the API's free-text + instructions, and never returns access-request IDs, provider paths or identities, OAuth tokens, or integration keys. Invoke the advertised `1pw_poll_access` with `browser_id` to observe the decision. diff --git a/src/lib/mcp/tools/vault-onepassword.test.ts b/src/lib/mcp/tools/vault-onepassword.test.ts index 1612f88b..7432339d 100644 --- a/src/lib/mcp/tools/vault-onepassword.test.ts +++ b/src/lib/mcp/tools/vault-onepassword.test.ts @@ -6,8 +6,7 @@ const target = { vault: "user-123", key: "example-login" }; const requestID = "private-access-request-id"; const reference = "eyJpZCI6InByaXZhdGUtYWNjZXNzLXJlcXVlc3QtaWQifQ"; const approvalLink = `onepassword://grant-brokered-access?access_request_reference=${reference}`; -const approvalInstructions = - "Present this link to the account owner to open on their device with the 1Password app."; +const approvalInstructions = `Present ${approvalLink} for ${requestID}.`; const oauthURL = "https://1password.example/oauth/authorize?client_id=kernel&state=opaque"; @@ -313,8 +312,8 @@ describe("1Password vault credentials", () => { expect(result.item.action).toEqual({ name: "1password_access_approval", url: approvalLink, - instructions: approvalInstructions, }); + expect(JSON.stringify(result)).not.toContain(approvalInstructions); expect(result.item.spec.requests).toEqual(requests); expect(result.item.state).toEqual({ provider: "1password", @@ -337,6 +336,31 @@ describe("1Password vault credentials", () => { } }); + test("forwards the native approval link whatever the instructions hold", async () => { + for (const instructions of [undefined, null, 42]) { + const fixture = await connectVaultTest([ + Response.json({ + ...pendingCredential, + action: { ...pendingCredential.action, instructions }, + }), + ]); + try { + const result = toolResultJSON( + await fixture.call("manage_vault_items", { + ...target, + action: "get", + }), + ); + expect(result.item.action).toEqual({ + name: "1password_access_approval", + url: approvalLink, + }); + } finally { + await fixture.close(); + } + } + }); + test.each([ `https://api.example/vault/onepassword/approval/vi_credential/${reference}`, `onepassword://grant-brokered-access?access_request_reference=${reference}&access_token=secret`, diff --git a/src/lib/mcp/vault-responses.ts b/src/lib/mcp/vault-responses.ts index 3c41b3cb..affcf778 100644 --- a/src/lib/mcp/vault-responses.ts +++ b/src/lib/mcp/vault-responses.ts @@ -94,16 +94,11 @@ export const vaultEventFields: OutputFields = { // Native 1Password approvals are human actions: the account owner opens the link // in their 1Password app, and it grants nothing until they approve there. Only a -// link in the exact native form is forwarded; anything else, including legacy -// nonce approval pages, is reduced to the action name. +// link in the exact native form is forwarded, without the API's free-text +// instructions; anything else, including legacy nonce approval pages, is reduced +// to the action name. const onePasswordAccessApproval = "1password_access_approval"; -const onePasswordApprovalActionSchema = z.object({ - name: z.literal(onePasswordAccessApproval), - url: z.string().refine(isNativeOnePasswordApprovalLink), - instructions: z.string().optional(), -}); - function isNativeOnePasswordApprovalLink(value: string): boolean { try { const url = new URL(value); @@ -239,11 +234,11 @@ export function projectVaultOutput( .object({ name: z.literal(onePasswordAccessApproval) }) .safeParse(result.action).success ) { - const approval = onePasswordApprovalActionSchema.safeParse( - Reflect.get(value, "action"), - ); - result.action = approval.success - ? approval.data + const url = z + .object({ url: z.string().refine(isNativeOnePasswordApprovalLink) }) + .safeParse(Reflect.get(value, "action")); + result.action = url.success + ? { name: onePasswordAccessApproval, url: url.data.url } : { name: onePasswordAccessApproval }; } if (allowed === vaultItemFields && result.type === "credential") { From 0aca0097e3551c52dfaa5a3043a188bf88724d07 Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Fri, 25 Sep 2026 20:54:25 +0000 Subject: [PATCH 5/9] Rename 1Password access operations and drop request reset --- bun.lock | 4 +- docs/vault-payments.md | 23 +++-- package.json | 2 +- src/lib/mcp/tools/vault-credentials.ts | 4 +- src/lib/mcp/tools/vault-items.ts | 2 +- src/lib/mcp/tools/vault-onepassword.test.ts | 109 ++++++++++++++++++-- src/lib/mcp/vault-responses.ts | 10 +- 7 files changed, 126 insertions(+), 28 deletions(-) diff --git a/bun.lock b/bun.lock index f23d2ad1..5b354c97 100644 --- a/bun.lock +++ b/bun.lock @@ -11,7 +11,7 @@ "@modelcontextprotocol/core": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "@onkernel/managed-auth-react": "0.5.3", - "@onkernel/sdk": "git+ssh://git@github.com/kernel/kernel-node-sdk-staging.git#bc922deaa45c2af412ae987d27208b1be474d73d", + "@onkernel/sdk": "git+ssh://git@github.com/kernel/kernel-node-sdk-staging.git#b9931e08abc8e38cd8f194a2896c910378b810c9", "@posthog/mcp": "0.17.0", "@types/jsonwebtoken": "^9.0.10", "@types/redis": "^4.0.11", @@ -160,7 +160,7 @@ "@onkernel/managed-auth-react": ["@onkernel/managed-auth-react@0.5.3", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5Ps7h7HknAxL4ZLnJCzsLhW8MLRr0SH1P8+eALBZXCmnx/K+g5ra2ar3iLoWFQ8vf/EvpuDF1LigIT6tDjRKdA=="], - "@onkernel/sdk": ["@onkernel/sdk@git+ssh://git@github.com/kernel/kernel-node-sdk-staging.git#bc922deaa45c2af412ae987d27208b1be474d73d", {}, "bc922deaa45c2af412ae987d27208b1be474d73d"], + "@onkernel/sdk": ["@onkernel/sdk@git+ssh://git@github.com/kernel/kernel-node-sdk-staging.git#b9931e08abc8e38cd8f194a2896c910378b810c9", {}, "b9931e08abc8e38cd8f194a2896c910378b810c9"], "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eJopQrUk0WR7jViYDC29+Rp50xGvs4GtWOXBeqCoFMzutkkO3CZvHehA4JqnjfWMTSS8toqvRhCSOpOz62Wf9w=="], diff --git a/docs/vault-payments.md b/docs/vault-payments.md index 57705961..395b2759 100644 --- a/docs/vault-payments.md +++ b/docs/vault-payments.md @@ -16,7 +16,7 @@ card request a test transaction. The Node SDK dependency is temporarily pinned to the stlc development preview -`kernel-node-sdk-staging@bc922deaa45c2af412ae987d27208b1be474d73d` in `bun.lock`. +`kernel-node-sdk-staging@b9931e08abc8e38cd8f194a2896c910378b810c9` in `bun.lock`. ## Credential collection and observation @@ -133,7 +133,12 @@ prefer `collect` for human edits. Requests are not automatically retried. 1. Connect the account with `manage_vault_credentials`, `action: "connect_account"`, `provider: "1password"`, the user's vault, and a new key. Give the returned 1Password authorization URL only to the account owner, outside the - agent-controlled browser; they verify the account on the consent screen. + agent-controlled browser; they verify the account on the consent screen. If the + account later reports `declined` or `reconnect_required`, connect again on the + same key. When `1pw_recover` is advertised, Kernel can recover a failed account + link: after explicit user approval, invoke it, give the returned link to the + account owner the same way, and connect again on the same key once recovery + completes. Never delete the account to recover. 2. Observe the account with `manage_vault_items` `get` until `state.status` is `connected`, then create the credential: @@ -154,7 +159,8 @@ prefer `collect` for human edits. Requests are not automatically retried. 1Password credentials store no values or selectors and cannot be updated. 3. With a browser created with the vault attached, and after explicit user approval, - invoke the advertised `1pw_request_access` with `inputs: {"browser_id": "..."}`. + invoke the advertised `1pw_create_access_request` with `inputs: {"browser_id": "..."}`. + Kernel loads the 1Password extension into that browser on demand. 4. Approval is a human action in the account owner's 1Password app. The pending item returns `action: {"name": "1password_access_approval", "url": "onepassword://grant-brokered-access?access_request_reference=..."}`. Give that link, unmodified, only to the account owner in a private surface outside @@ -163,12 +169,15 @@ prefer `collect` for human edits. Requests are not automatically retried. approve, but it identifies the request, so the agent must never open, decode, or approve it. MCP forwards only links in that exact native form, without the API's free-text instructions, and never returns - access-request IDs, provider paths or identities, OAuth tokens, or integration - keys. Invoke the advertised `1pw_poll_access` with `browser_id` to observe the - decision. + access-request IDs, provider paths or identities, or OAuth tokens. Invoke the + advertised `1pw_access_request_status` with `browser_id` to observe the decision. + If the item stays `pending_authorization` with no action and no advertised + operations, a request may already have reached 1Password. There is no reset: + stop, ask the owner to check 1Password, and never delete or recreate the item to + retry. 5. When the item is ready, invoke the advertised `1pw_fill` with `browser_id` and the exact current `page_url`. The extension selects fields and submits the form. - `fill_submitted` does not confirm login; `fill_failed` and `fill_unknown` are tool + `fill_submitted` means the form was submitted, not that login succeeded; `fill_failed` and `fill_unknown` are tool errors, and `fill_unknown` must not be retried in the same browser. ## Tools and scope diff --git a/package.json b/package.json index 58f63ffb..a3e81e18 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@modelcontextprotocol/core": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "@onkernel/managed-auth-react": "0.5.3", - "@onkernel/sdk": "git+ssh://git@github.com/kernel/kernel-node-sdk-staging.git#bc922deaa45c2af412ae987d27208b1be474d73d", + "@onkernel/sdk": "git+ssh://git@github.com/kernel/kernel-node-sdk-staging.git#b9931e08abc8e38cd8f194a2896c910378b810c9", "@posthog/mcp": "0.17.0", "@types/jsonwebtoken": "^9.0.10", "@types/redis": "^4.0.11", diff --git a/src/lib/mcp/tools/vault-credentials.ts b/src/lib/mcp/tools/vault-credentials.ts index 6baa258d..69279e29 100644 --- a/src/lib/mcp/tools/vault-credentials.ts +++ b/src/lib/mcp/tools/vault-credentials.ts @@ -160,9 +160,9 @@ export function registerVaultCredentialTools( "manage_vault_credentials", { description: - 'Create or update credential items in a per-end-user vault. There are two credential paths. Before creating any credential, ask the user which they prefer and set provider to match; never choose for them. provider:"kernel" is Kernel-hosted collection: the user enters values in a Kernel-hosted form and the agent fills them with value-free bindings. provider:"1password" is 1Password brokered approval: the user connects their 1Password account once, approves each login request in the 1Password app, and the 1Password extension fills and submits; Kernel stores no values. ' + + 'Create or update credential items in a per-end-user vault. There are two credential paths. Before creating any credential, ask the user which they prefer and set provider to match; never choose for them. provider:"kernel" is Kernel-hosted collection: the user enters values in a Kernel-hosted form and the agent fills them with value-free bindings. provider:"1password" is 1Password brokered approval: the user connects their 1Password account once, approves each login request in the 1Password app, and the 1Password extension, loaded into the browser on demand, fills and submits; Kernel stores no values. ' + 'Kernel path: use only the recognizable site name as description; explicitly set sensitive:false for ordinary usernames/emails. Each field may include an optional non-secret human-readable label; name remains the stable key for updates and browser fills. Passwords and TOTP seeds must be sensitive. Never store payment-card data here. For human collection, omit values and present the returned bearer collection URL privately to the intended user, outside the agent-controlled browser. Never ask for passwords or TOTP seeds in chat. TOTP seeds require trusted provisioning and have no hosted input. On create, fields is an ordered array of named definitions: inspect the website and list fields in its natural top-to-bottom order because this directly controls the user-facing collection form. Update fields remain keyed by name and contain only value. Updates require the latest version and optionally expected_item_id from an earlier read; definitions are immutable. Omitted values are preserved; null or empty strings clear supported values. Clearing required TOTP is unsupported. Hosted forms require populated required inputs. To reopen collection, use manage_vault_items with action: "invoke" and operation: "collect". Use manage_vault_items get with wait for readiness, then invoke fill with fill parameters. For edits to already-ready items compare versions without wait. Explicitly non-sensitive text/email values are returned; sensitive values and TOTP seeds are omitted. ' + - '1Password path: first use action "connect_account" with provider:"1password" and a new key; present the returned 1Password authorization URL only to the account owner, outside the agent-controlled browser, and let them verify the account on the consent screen. Once manage_vault_items get reports the account connected, create the credential with provider:"1password" and spec {account_id, website, optional goal/reason/keywords}. 1Password credentials cannot be updated. Approval is a human action in the 1Password app: after 1pw_request_access, give the returned native onepassword:// approval link unmodified only to the account owner, outside the agent-controlled browser, and never open, decode, or approve it yourself. MCP never returns access-request IDs, tokens, or integration keys. This is unrelated to manage_credential_providers. ' + + '1Password path: first use action "connect_account" with provider:"1password" and a new key; present the returned 1Password authorization URL only to the account owner, outside the agent-controlled browser, and let them verify the account on the consent screen. Once manage_vault_items get reports the account connected, create the credential with provider:"1password" and spec {account_id, website, optional goal/reason/keywords}. 1Password credentials cannot be updated. Approval is a human action in the 1Password app: after 1pw_create_access_request, give the returned native onepassword:// approval link unmodified only to the account owner, outside the agent-controlled browser, and never open, decode, or approve it yourself. MCP never returns access-request IDs or tokens. This is unrelated to manage_credential_providers. ' + "Writes are never automatically retried; reconcile conflicts or uncertain outcomes before any further write.", inputSchema: vaultToolInput({ ...vaultItemSchema, diff --git a/src/lib/mcp/tools/vault-items.ts b/src/lib/mcp/tools/vault-items.ts index 82ab5943..c6edb796 100644 --- a/src/lib/mcp/tools/vault-items.ts +++ b/src/lib/mcp/tools/vault-items.ts @@ -29,7 +29,7 @@ export function registerVaultItemTools( "manage_vault_items", { description: - 'Inspect credential and payment vault items and immutable audit events. "list" reads items without renewing collection links; "get" reads state, safe field metadata, version, required user actions, available_operations, and available_expansions. MCP returns explicitly non-sensitive text/email values; sensitive values and TOTP seeds are omitted. For credentials, present the collection URL only to the intended user, outside the agent-controlled browser; never ask for passwords or TOTP seeds in chat. Reopen collection using its advertised operation when available; TOTP has no hosted input. wait observes readiness, not edits to ready credentials: compare versions using get without wait. Use manage_vault_credentials for credential creation and updates; use a per-user vault, site-name-only description, and sensitive:false for ordinary usernames/emails. Credentials follow one of two user-chosen paths: Kernel-hosted collection (collect, fill) or 1Password brokered approval (1pw_request_access, 1pw_poll_access, 1pw_reconcile_access, 1pw_fill on the credential; 1pw_recover on its credential_account). For 1Password, approval happens in the account owner\'s 1Password app: give the native onepassword:// approval link only to the owner, outside the agent-controlled browser, and never open or approve it yourself; MCP never returns access-request IDs. 1pw_fill submits the form but does not confirm login; never retry fill_unknown in the same browser. Never store credit card data in credential items. "invoke" fetches the item again and submits only an advertised operation; read its description and obtain explicit user approval first. Provider actions (OAuth, enrollment, MFA, approval) must be completed by the user, not invoked as operations. "events" observes outcomes; use the last event ID as after. "delete" invalidates an item credential; confirm with the user first. Unresolved payments can block item and parent deletion; the API decides whether explicit abandonment is allowed, and deletion never proves a payment did not occur. recovery_required is not decline or expiry: stop payment attempts and reconcile with the provider or support; no reset exists. Credential ready means required values exist, not that login succeeded; payment ready does not mean paid. For browser field writes, supply operation-specific inputs with browser_id and ordered field/selector bindings; values stay server-side until entering the browser. Link cards use the advertised browser field-writing operation, not aliases or egress substitution: inputs.page_url must be the exact current HTTPS top-level page URL at the approved merchant origin, and the browser must retain its vault attachment. Browser field writes return no card values but do not isolate them from browser/CDP access or explicitly submit checkout; failed or unknown writes may leave partial changes. Never automatically retry or fall back to aliases. AgentCard aliases and checkout hold/approval/replay remain supported. Follow each advertised operation\'s API contract for inputs and outcome handling; never substitute another operation or retry an uncertain attempt. Requests are never automatically retried. Do not retry failed, timed-out, rejected, or indeterminate payments; inspect state/events instead.', + 'Inspect credential and payment vault items and immutable audit events. "list" reads items without renewing collection links; "get" reads state, safe field metadata, version, required user actions, available_operations, and available_expansions. MCP returns explicitly non-sensitive text/email values; sensitive values and TOTP seeds are omitted. For credentials, present the collection URL only to the intended user, outside the agent-controlled browser; never ask for passwords or TOTP seeds in chat. Reopen collection using its advertised operation when available; TOTP has no hosted input. wait observes readiness, not edits to ready credentials: compare versions using get without wait. Use manage_vault_credentials for credential creation and updates; use a per-user vault, site-name-only description, and sensitive:false for ordinary usernames/emails. Credentials follow one of two user-chosen paths: Kernel-hosted collection (collect, fill) or 1Password brokered approval (1pw_create_access_request, 1pw_access_request_status, 1pw_fill on the credential; 1pw_recover to recover a failed account link on its credential_account). For 1Password, approval happens in the account owner\'s 1Password app: give the native onepassword:// approval link only to the owner, outside the agent-controlled browser, and never open or approve it yourself; MCP never returns access-request IDs. 1pw_fill can submit the form but does not prove login; never retry fill_unknown in the same browser. An uncertain access request stays blocked with no advertised operations; never delete or recreate the item to retry it. Never store credit card data in credential items. "invoke" fetches the item again and submits only an advertised operation; read its description and obtain explicit user approval first. Provider actions (OAuth, enrollment, MFA, approval) must be completed by the user, not invoked as operations. "events" observes outcomes; use the last event ID as after. "delete" invalidates an item credential; confirm with the user first. Unresolved payments can block item and parent deletion; the API decides whether explicit abandonment is allowed, and deletion never proves a payment did not occur. recovery_required is not decline or expiry: stop payment attempts and reconcile with the provider or support; no reset exists. Credential ready means required values exist, not that login succeeded; payment ready does not mean paid. For browser field writes, supply operation-specific inputs with browser_id and ordered field/selector bindings; values stay server-side until entering the browser. Link cards use the advertised browser field-writing operation, not aliases or egress substitution: inputs.page_url must be the exact current HTTPS top-level page URL at the approved merchant origin, and the browser must retain its vault attachment. Browser field writes return no card values but do not isolate them from browser/CDP access or explicitly submit checkout; failed or unknown writes may leave partial changes. Never automatically retry or fall back to aliases. AgentCard aliases and checkout hold/approval/replay remain supported. Follow each advertised operation\'s API contract for inputs and outcome handling; never substitute another operation or retry an uncertain attempt. Requests are never automatically retried. Do not retry failed, timed-out, rejected, or indeterminate payments; inspect state/events instead.', inputSchema: vaultToolInput({ ...vaultItemSchema, action: z.enum(["list", "get", "invoke", "events", "delete"]), diff --git a/src/lib/mcp/tools/vault-onepassword.test.ts b/src/lib/mcp/tools/vault-onepassword.test.ts index 7432339d..cf0f7786 100644 --- a/src/lib/mcp/tools/vault-onepassword.test.ts +++ b/src/lib/mcp/tools/vault-onepassword.test.ts @@ -66,7 +66,7 @@ const pendingCredential = { instructions: approvalInstructions, }, available_operations: [ - { type: "1pw_poll_access", description: "Check the request." }, + { type: "1pw_access_request_status", description: "Check the request." }, ], available_expansions: [], created_at: "2026-09-25T00:00:00Z", @@ -118,9 +118,14 @@ describe("1Password vault credentials", () => { expect(descriptionOf("manage_vaults")).toContain( "Ask the user which they prefer", ); - expect(descriptionOf("manage_vault_items")).toContain( - "1pw_request_access", - ); + const items = descriptionOf("manage_vault_items"); + expect(items).toContain("1pw_create_access_request"); + expect(items).toContain("1pw_access_request_status"); + expect(items).toContain("recover a failed account link"); + for (const { description } of tools) { + expect(description).not.toContain("reconcile_access"); + expect(description).not.toMatch(/integration key|Family/i); + } expect(fixture.requests).toEqual([]); } finally { await fixture.close(); @@ -272,6 +277,50 @@ describe("1Password vault credentials", () => { } }); + test("offers recovery of a failed account link only when advertised", async () => { + const recoverable = { + ...account, + state: { provider: "1password", status: "reconnect_required" }, + action: undefined, + available_operations: [ + { type: "1pw_recover", description: "Get a recovery link." }, + ], + }; + const fixture = await connectVaultTest([ + Response.json(recoverable), + Response.json(recoverable), + Response.json({ ...recoverable, action: account.action }), + ]); + try { + const read = toolResultJSON( + await fixture.call("manage_vault_items", { + vault: target.vault, + key: account.key, + action: "get", + }), + ); + const guidance = read.guidance.join(" "); + expect(guidance).toContain("recover a failed account link"); + expect(guidance).toContain("connect again on the same key"); + expect(guidance).not.toMatch(/integration key|Family/i); + const result = toolResultJSON( + await fixture.call("manage_vault_items", { + vault: target.vault, + key: account.key, + action: "invoke", + operation: "1pw_recover", + }), + ); + expect(fixture.requests[2].body).toEqual({ type: "1pw_recover" }); + expect(result.item.action).toEqual({ + name: "1password_oauth", + url: oauthURL, + }); + } finally { + await fixture.close(); + } + }); + test("creates a 1Password credential from a single login request", async () => { const fixture = await connectVaultTest([Response.json(pendingCredential)]); try { @@ -329,7 +378,7 @@ describe("1Password vault credentials", () => { expect(guidance).toContain("unmodified only to the account owner"); expect(guidance).toContain("never open it in a browser"); expect(guidance).toContain('action: "invoke"'); - expect(guidance).toContain('operation: "1pw_poll_access"'); + expect(guidance).toContain('operation: "1pw_access_request_status"'); expect(guidance).not.toContain("collection URL"); } finally { await fixture.close(); @@ -406,7 +455,7 @@ describe("1Password vault credentials", () => { state: { provider: "1password", status: "pending_authorization" }, action: undefined, available_operations: [ - { type: "1pw_request_access", description: "Request access." }, + { type: "1pw_create_access_request", description: "Request access." }, ], }; const fixture = await connectVaultTest([ @@ -417,12 +466,12 @@ describe("1Password vault credentials", () => { const result = await fixture.call("manage_vault_items", { ...target, action: "invoke", - operation: "1pw_request_access", + operation: "1pw_create_access_request", inputs: { browser_id: "browser-1", reason: "Check order status" }, }); expect(result.isError).toBeUndefined(); expect(fixture.requests[1].body).toEqual({ - type: "1pw_request_access", + type: "1pw_create_access_request", browser_id: "browser-1", reason: "Check order status", }); @@ -473,13 +522,53 @@ describe("1Password vault credentials", () => { }, ); + test.each(["1pw_create_access_request", "1pw_reconcile_access"])( + "keeps an uncertain access request blocked: %s", + async (operation) => { + const uncertain = { + ...pendingCredential, + state: { provider: "1password", status: "pending_authorization" }, + action: undefined, + available_operations: [], + }; + const fixture = await connectVaultTest([ + Response.json(uncertain), + Response.json(uncertain), + ]); + try { + const read = toolResultJSON( + await fixture.call("manage_vault_items", { + ...target, + action: "get", + }), + ); + expect(read.guidance.join(" ")).toContain( + "never delete or recreate the item to retry", + ); + const result = await fixture.call("manage_vault_items", { + ...target, + action: "invoke", + operation, + inputs: { browser_id: "browser-1" }, + }); + expect(result.isError).toBe(true); + expect(fixture.requests).toHaveLength(2); + expect(fixture.requests.every(({ method }) => method === "GET")).toBe( + true, + ); + } finally { + await fixture.close(); + } + }, + ); + test("curates 1Password operation errors", async () => { const fixture = await connectVaultTest([ Response.json({ ...pendingCredential, action: undefined, available_operations: [ - { type: "1pw_request_access", description: "Request access." }, + { type: "1pw_create_access_request", description: "Request access." }, ], }), Response.json( @@ -491,7 +580,7 @@ describe("1Password vault credentials", () => { const result = await fixture.call("manage_vault_items", { ...target, action: "invoke", - operation: "1pw_request_access", + operation: "1pw_create_access_request", inputs: { browser_id: "browser-1" }, }); expect(result.isError).toBe(true); diff --git a/src/lib/mcp/vault-responses.ts b/src/lib/mcp/vault-responses.ts index affcf778..371b3dc2 100644 --- a/src/lib/mcp/vault-responses.ts +++ b/src/lib/mcp/vault-responses.ts @@ -436,14 +436,14 @@ export function vaultItemResponse( } const onePasswordAccountGuidance = [ - "This credential_account connects a 1Password account; it is not a fillable credential. If an action URL is present, present it only to the account owner, outside the agent-controlled browser, and let them complete 1Password consent and verify the account shown there. Never ask for 1Password passwords, Secret Keys, OAuth codes, tokens, or integration keys in chat.", - 'Observe with manage_vault_items action: "get" until state.status is connected, then create 1Password credentials with manage_vault_credentials, provider: "1password", and account_id set to this item\'s id. declined or reconnect_required need the user to connect again. Only when 1pw_recover is advertised and after explicit user approval, call manage_vault_items with action: "invoke" and operation: "1pw_recover"; never delete the account to recover.', + "This credential_account connects a 1Password account; it is not a fillable credential. If an action URL is present, present it only to the account owner, outside the agent-controlled browser, and let them complete 1Password consent and verify the account shown there. Never ask for 1Password passwords, Secret Keys, OAuth codes, or tokens in chat.", + 'Observe with manage_vault_items action: "get" until state.status is connected, then create 1Password credentials with manage_vault_credentials, provider: "1password", and account_id set to this item\'s id. declined or reconnect_required need the user to connect again with connect_account on the same key. 1pw_recover is advertised only when Kernel can recover a failed account link: after explicit user approval, call manage_vault_items with action: "invoke" and operation: "1pw_recover", present the returned link to the account owner the same way, and once recovery completes connect again on the same key. Never delete the account to recover.', ]; const onePasswordCredentialGuidance = [ - 'Operations use manage_vault_items with action: "invoke", operation set to the advertised 1pw_* type, and inputs for that operation. 1Password credentials hold no values in Kernel. After explicit user approval, invoke operation: "1pw_request_access" with inputs {browser_id} and optional goal, reason, and keywords, using a browser created with this vault attached.', - 'Approval is a human action in the account owner\'s 1Password app. When action.name is 1password_access_approval with a url, give that onepassword:// link unmodified only to the account owner, in a private surface outside the agent-controlled browser, to open on a device with the 1Password app; they choose the login and approve or deny there. The link grants nothing until they approve, but it identifies the request: never open it in a browser, decode it, post it where others can see it, or approve on their behalf. Without a url, MCP received no native link: tell the owner the approval link is unavailable and do not request again while pending. Invoke operation: "1pw_poll_access" with inputs {browser_id} to observe the decision. Do not issue a second request while one is pending.', - 'When ready, invoke operation: "1pw_fill" with inputs {browser_id, page_url}, where page_url is the exact current top-level URL on the requested login origin. The extension selects fields and submits; you cannot supply selectors or values. fill_submitted does not confirm login. fill_unknown may have submitted; never retry it in the same browser. operation: "1pw_reconcile_access" only abandons an unconfirmed request after the user checks 1Password for an existing one, requires inputs {acknowledge_unconfirmed: true}, and can lead to duplicate requests.', + 'Operations use manage_vault_items with action: "invoke", operation set to the advertised 1pw_* type, and inputs for that operation. 1Password credentials hold no values in Kernel. After explicit user approval, invoke operation: "1pw_create_access_request" with inputs {browser_id} and optional goal, reason, and keywords, using a browser created with this vault attached; Kernel loads the 1Password extension into that browser on demand.', + 'Approval is a human action in the account owner\'s 1Password app. When action.name is 1password_access_approval with a url, give that onepassword:// link unmodified only to the account owner, in a private surface outside the agent-controlled browser, to open on a device with the 1Password app; they choose the login and approve or deny there. The link grants nothing until they approve, but it identifies the request: never open it in a browser, decode it, post it where others can see it, or approve on their behalf. Without a url, MCP received no native link: tell the owner the approval link is unavailable and do not request again while pending. Invoke operation: "1pw_access_request_status" with inputs {browser_id} to observe the decision. Do not issue a second request while one is pending. If the item stays pending_authorization with no action and no advertised operations, a request may already have reached 1Password: stop, tell the owner to check 1Password, and never delete or recreate the item to retry.', + 'When ready, invoke operation: "1pw_fill" with inputs {browser_id, page_url}, where page_url is the exact current top-level URL on the requested login origin. The extension selects fields and submits; you cannot supply selectors or values. fill_submitted means the form was submitted, not that login succeeded: check the page. fill_unknown may have submitted; never retry it in the same browser.', ]; const vaultErrorMessages = new Map([ From a5bc57d818cdf7b64ee16b85ebdc57c70520cc7e Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:40:59 +0000 Subject: [PATCH 6/9] Align 1Password vault tools with account keys, multi-login requests, and stored tokens --- README.md | 2 +- bun.lock | 4 +- docs/vault-payments.md | 65 +++-- package.json | 2 +- src/lib/mcp/tools/vault-credentials.ts | 53 ++-- src/lib/mcp/tools/vault-items.ts | 10 +- src/lib/mcp/tools/vault-onepassword.test.ts | 266 ++++++++++++++++++-- src/lib/mcp/tools/vaults.ts | 2 +- src/lib/mcp/vault-responses.ts | 39 ++- 9 files changed, 367 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index e864d90f..1f1469c2 100644 --- a/README.md +++ b/README.md @@ -340,7 +340,7 @@ Call `get_connection_context` before deciding whether to create or select a proj - `manage_vaults` - Create, list, get, and delete project-owned vaults; use one per end user. - `manage_vault_wallets` - Connect Kernel-managed or configured Link/AgentCard wallets, import Link grants from a trusted backend, and inspect live payment methods. - `manage_vault_cards` - Create or update card requests according to the API's lifecycle rules; does not implicitly authorize Link cards. -- `manage_vault_credentials` - Create credentials through one of two user-chosen paths: Kernel-hosted collection (definitions for private human collection; update values or description with version and optional immutable item identity preconditions) or 1Password brokered approval (connect a 1Password account, then create a login request the user approves in the 1Password app). Agents ask the user which path they prefer before creating credentials. +- `manage_vault_credentials` - Create credentials through one of two user-chosen paths: Kernel-hosted collection (definitions for private human collection; update values or description with version and optional immutable item identity preconditions) or 1Password brokered approval (connect a 1Password account, then create a request for 1-5 logins the user approves in the 1Password app). Agents reuse an existing credential for the site first, otherwise ask the user where their login lives before creating credentials. - `manage_vault_items` - List, get, invoke advertised operations (including fill with value-free bindings), observe events, and delete vault items. Read credential definitions, presence, version, collection links, and explicitly non-sensitive values; sensitive values remain hidden. `collect` reopens the full form; provider approvals remain user actions. Ready is not login or payment success. See [Vault payments](docs/vault-payments.md) for both provider flows, safety rules, and response shapes. `manage_browsers` accepts creation-only `vaults` references (max 20); existing sessions and pools cannot gain vault bindings. The six vault tools share the `vaults` toolset and prepare/observe credentials rather than submitting merchant payments. They are exposed only when `GET /org/entitlements` reports `features.vaults.enabled: true` for the current credential; missing or unavailable entitlements hide them. Toolset configuration cannot override this access check. Credential create → collect → readiness → fill is supported entirely through MCP tools. `prepare_checkout` remains API/CLI-only. The SDK dependency is pinned in `bun.lock`; it currently points at a temporary stlc development preview (TODO: replace with the official `@onkernel/sdk` release that includes 1Password vault credentials). diff --git a/bun.lock b/bun.lock index 5b354c97..b6be03fc 100644 --- a/bun.lock +++ b/bun.lock @@ -11,7 +11,7 @@ "@modelcontextprotocol/core": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "@onkernel/managed-auth-react": "0.5.3", - "@onkernel/sdk": "git+ssh://git@github.com/kernel/kernel-node-sdk-staging.git#b9931e08abc8e38cd8f194a2896c910378b810c9", + "@onkernel/sdk": "git+ssh://git@github.com/kernel/kernel-node-sdk-staging.git#be64af0301e30156e15529856b5107efc258964c", "@posthog/mcp": "0.17.0", "@types/jsonwebtoken": "^9.0.10", "@types/redis": "^4.0.11", @@ -160,7 +160,7 @@ "@onkernel/managed-auth-react": ["@onkernel/managed-auth-react@0.5.3", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5Ps7h7HknAxL4ZLnJCzsLhW8MLRr0SH1P8+eALBZXCmnx/K+g5ra2ar3iLoWFQ8vf/EvpuDF1LigIT6tDjRKdA=="], - "@onkernel/sdk": ["@onkernel/sdk@git+ssh://git@github.com/kernel/kernel-node-sdk-staging.git#b9931e08abc8e38cd8f194a2896c910378b810c9", {}, "b9931e08abc8e38cd8f194a2896c910378b810c9"], + "@onkernel/sdk": ["@onkernel/sdk@git+ssh://git@github.com/kernel/kernel-node-sdk-staging.git#be64af0301e30156e15529856b5107efc258964c", {}, "be64af0301e30156e15529856b5107efc258964c"], "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eJopQrUk0WR7jViYDC29+Rp50xGvs4GtWOXBeqCoFMzutkkO3CZvHehA4JqnjfWMTSS8toqvRhCSOpOz62Wf9w=="], diff --git a/docs/vault-payments.md b/docs/vault-payments.md index 395b2759..b7bb3073 100644 --- a/docs/vault-payments.md +++ b/docs/vault-payments.md @@ -16,7 +16,7 @@ card request a test transaction. The Node SDK dependency is temporarily pinned to the stlc development preview -`kernel-node-sdk-staging@b9931e08abc8e38cd8f194a2896c910378b810c9` in `bun.lock`. +`kernel-node-sdk-staging@be64af0301e30156e15529856b5107efc258964c` in `bun.lock`. ## Credential collection and observation @@ -130,6 +130,15 @@ prefer `collect` for human edits. Requests are not automatically retried. ### 1Password brokered approval +Before creating anything, list the vault and reuse a ready credential whose +`spec.requests.entries` websites cover the login page, or a connected +`credential_account` the owner confirms is theirs. If none fits, ask where the +user's login lives, for example: "Is your example.com login saved in your own +1Password, or would you rather enter it in a secure Kernel form?" 1Password +supports only logins in the owner's own non-shared vault, not shared-vault items or +passkeys; use Kernel-hosted collection for those, or when the user declines +1Password or that path fails. + 1. Connect the account with `manage_vault_credentials`, `action: "connect_account"`, `provider: "1password"`, the user's vault, and a new key. Give the returned 1Password authorization URL only to the account owner, outside the @@ -140,7 +149,8 @@ prefer `collect` for human edits. Requests are not automatically retried. account owner the same way, and connect again on the same key once recovery completes. Never delete the account to recover. 2. Observe the account with `manage_vault_items` `get` until `state.status` is - `connected`, then create the credential: + `connected`. Confirm with the owner which site logins to request (1-5, approved + together), then create the credential: ```json { @@ -149,36 +159,55 @@ prefer `collect` for human edits. Requests are not automatically retried. "vault": "user-123", "key": "example-login", "spec": { - "account_id": "", - "website": "https://example.com/login" + "account": "", + "logins": [{ "website": "https://example.com/login" }] } } ``` - Optional `goal`, `reason`, and `keywords` describe the request to the account owner. - 1Password credentials store no values or selectors and cannot be updated. + Optional `goal`, and per-login `reason` and `keywords`, describe the request to + the account owner. 1Password credentials store no values or selectors and cannot + be updated. 3. With a browser created with the vault attached, and after explicit user approval, invoke the advertised `1pw_create_access_request` with `inputs: {"browser_id": "..."}`. - Kernel loads the 1Password extension into that browser on demand. + Kernel loads the 1Password extension into that browser on demand. Request-time + `reason` and `keywords` apply only to a single-login credential. 4. Approval is a human action in the account owner's 1Password app. The pending item returns `action: {"name": "1password_access_approval", "url": "onepassword://grant-brokered-access?access_request_reference=..."}`. Give that link, unmodified, only to the account owner in a private surface outside the agent-controlled browser; they open it on a device with the 1Password app and choose, approve, or deny the login there. The link grants nothing until they approve, but it identifies the request, so the agent must never open, decode, or - approve it. MCP forwards only links in that exact native form, without the API's free-text - instructions, and never returns - access-request IDs, provider paths or identities, or OAuth tokens. Invoke the - advertised `1pw_access_request_status` with `browser_id` to observe the decision. - If the item stays `pending_authorization` with no action and no advertised - operations, a request may already have reached 1Password. There is no reset: - stop, ask the owner to check 1Password, and never delete or recreate the item to - retry. + approve it. MCP forwards only links in that exact native form, without the API's + free-text instructions, and never returns access-request IDs, provider paths or + identities, or OAuth tokens. Invoke the advertised `1pw_access_request_status` + with `browser_id` to observe the decision. + - `declined`: the owner denied the request. Do not request again unless they + ask; offer Kernel-hosted collection. + - `failed`: a confirmed failure. Ask the end-user before deleting and recreating + the credential for at most one new request, or offer Kernel-hosted collection. + - `pending_authorization` with no action and no advertised operations: check that + the `credential_account` named by `spec.account` is connected. If it is, a + request may already have reached 1Password. There is no reset: stop, ask the + owner to check 1Password, and never delete or recreate the item to retry. 5. When the item is ready, invoke the advertised `1pw_fill` with `browser_id` and the - exact current `page_url`. The extension selects fields and submits the form. - `fill_submitted` means the form was submitted, not that login succeeded; `fill_failed` and `fill_unknown` are tool - errors, and `fill_unknown` must not be retried in the same browser. + exact current `page_url`. When several approved logins share the page origin, ask + the owner which one to use and pass its `entry_id` from `state.access_request` + entries. The extension selects fields and submits the form. `fill_submitted` + means the form was submitted, not that login succeeded, so check the page. + `fill_failed` and `fill_unknown` are tool errors; `noExistingCredentials` means the + owner's 1Password has no usable login for the page, and `fill_unknown` must not be + retried in the same browser. + +The Kernel API also supports 1Password credentials backed by a customer-supplied +access token and integration key instead of a connected account. The integrating +developer creates them and replaces tokens (`1pw_update_access_token`) through the +Kernel API. MCP accepts neither secret: it rejects them in create specs, refuses +`1pw_update_access_token`, and never returns them. It can read such credentials, +including optional `access_token_expires_at`, and request, observe, and fill them like +account-backed credentials; request and fill are unavailable while the token is +expired. ## Tools and scope diff --git a/package.json b/package.json index a3e81e18..ea833dd1 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@modelcontextprotocol/core": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "@onkernel/managed-auth-react": "0.5.3", - "@onkernel/sdk": "git+ssh://git@github.com/kernel/kernel-node-sdk-staging.git#b9931e08abc8e38cd8f194a2896c910378b810c9", + "@onkernel/sdk": "git+ssh://git@github.com/kernel/kernel-node-sdk-staging.git#be64af0301e30156e15529856b5107efc258964c", "@posthog/mcp": "0.17.0", "@types/jsonwebtoken": "^9.0.10", "@types/redis": "^4.0.11", diff --git a/src/lib/mcp/tools/vault-credentials.ts b/src/lib/mcp/tools/vault-credentials.ts index 69279e29..69c63047 100644 --- a/src/lib/mcp/tools/vault-credentials.ts +++ b/src/lib/mcp/tools/vault-credentials.ts @@ -75,27 +75,38 @@ const createSpec = z ), }) .strict(); -const onePasswordCreateSpec = z +const onePasswordLogin = z .object({ - account_id: z - .string() - .min(1) - .describe( - "Immutable id (not key) of a connected 1Password credential_account item in the same vault.", - ), website: z .string() .url() .max(2083) .regex(/^https:\/\//) - .describe("HTTPS login page for the single requested login."), + .describe("HTTPS login page for this login."), + reason: z.string().max(100).optional(), + keywords: z.array(z.string().min(1).max(50)).min(1).max(5).optional(), + }) + .strict(); +const onePasswordCreateSpec = z + .object({ + account: z + .string() + .min(1) + .describe( + "Key (not id) of a connected 1Password credential_account item in the same vault.", + ), + logins: z + .array(onePasswordLogin) + .min(1) + .max(5) + .describe( + "1-5 logins the account owner approves together in one request, each with the site it signs in to.", + ), goal: z .string() .max(140) .optional() .describe("Short request goal shown to the account owner."), - reason: z.string().max(100).optional(), - keywords: z.array(z.string().min(1).max(50)).min(1).max(5).optional(), }) .strict(); @@ -104,18 +115,16 @@ function onePasswordCredentialSpec( ) { return { provider: "1password" as const, - account_id: spec.account_id, + account: spec.account, requests: { version: 2, ...(spec.goal !== undefined && { goal: spec.goal }), - entries: [ - { - type: "login", - parameters: { website: spec.website }, - ...(spec.reason !== undefined && { reason: spec.reason }), - ...(spec.keywords !== undefined && { keywords: spec.keywords }), - }, - ], + entries: spec.logins.map((login) => ({ + type: "login", + parameters: { website: login.website }, + ...(login.reason !== undefined && { reason: login.reason }), + ...(login.keywords !== undefined && { keywords: login.keywords }), + })), }, }; } @@ -160,9 +169,9 @@ export function registerVaultCredentialTools( "manage_vault_credentials", { description: - 'Create or update credential items in a per-end-user vault. There are two credential paths. Before creating any credential, ask the user which they prefer and set provider to match; never choose for them. provider:"kernel" is Kernel-hosted collection: the user enters values in a Kernel-hosted form and the agent fills them with value-free bindings. provider:"1password" is 1Password brokered approval: the user connects their 1Password account once, approves each login request in the 1Password app, and the 1Password extension, loaded into the browser on demand, fills and submits; Kernel stores no values. ' + + 'Create or update credential items in a per-end-user vault. First list the vault with manage_vault_items and reuse an existing credential for the site: fill a ready Kernel credential, 1pw_fill a ready 1Password credential, and reuse a connected 1Password credential_account for new 1Password credentials. Never claim access the vault does not hold. There are two credential paths. Before creating any credential, ask the user which they prefer by asking where their login for the site lives, for example: "Is your example.com login saved in your own 1Password, or would you rather enter it in a secure Kernel form?" Set provider to match; never choose for them. provider:"kernel" is Kernel-hosted collection: the user enters values in a Kernel-hosted form and the agent fills them with value-free bindings. provider:"1password" is 1Password brokered approval: the user connects their 1Password account once, approves each login request in the 1Password app, and the 1Password extension, loaded into the browser on demand, fills and submits; Kernel stores no values. 1Password supports only logins in the owner\'s own non-shared vault, not shared-vault items or passkeys; use Kernel-hosted collection for those, or if the user declines 1Password or that path fails. ' + 'Kernel path: use only the recognizable site name as description; explicitly set sensitive:false for ordinary usernames/emails. Each field may include an optional non-secret human-readable label; name remains the stable key for updates and browser fills. Passwords and TOTP seeds must be sensitive. Never store payment-card data here. For human collection, omit values and present the returned bearer collection URL privately to the intended user, outside the agent-controlled browser. Never ask for passwords or TOTP seeds in chat. TOTP seeds require trusted provisioning and have no hosted input. On create, fields is an ordered array of named definitions: inspect the website and list fields in its natural top-to-bottom order because this directly controls the user-facing collection form. Update fields remain keyed by name and contain only value. Updates require the latest version and optionally expected_item_id from an earlier read; definitions are immutable. Omitted values are preserved; null or empty strings clear supported values. Clearing required TOTP is unsupported. Hosted forms require populated required inputs. To reopen collection, use manage_vault_items with action: "invoke" and operation: "collect". Use manage_vault_items get with wait for readiness, then invoke fill with fill parameters. For edits to already-ready items compare versions without wait. Explicitly non-sensitive text/email values are returned; sensitive values and TOTP seeds are omitted. ' + - '1Password path: first use action "connect_account" with provider:"1password" and a new key; present the returned 1Password authorization URL only to the account owner, outside the agent-controlled browser, and let them verify the account on the consent screen. Once manage_vault_items get reports the account connected, create the credential with provider:"1password" and spec {account_id, website, optional goal/reason/keywords}. 1Password credentials cannot be updated. Approval is a human action in the 1Password app: after 1pw_create_access_request, give the returned native onepassword:// approval link unmodified only to the account owner, outside the agent-controlled browser, and never open, decode, or approve it yourself. MCP never returns access-request IDs or tokens. This is unrelated to manage_credential_providers. ' + + '1Password path: reuse a connected credential_account in the vault once the owner confirms it is their account; otherwise use action "connect_account" with provider:"1password" and a new key, and present the returned 1Password authorization URL only to the account owner, outside the agent-controlled browser, and let them verify the account on the consent screen. Once manage_vault_items get reports the account connected, confirm with the owner which site logins to request (1-5, approved together), then create the credential with provider:"1password" and spec {account: the account item key, logins: [{website, optional reason/keywords}], optional goal}. 1Password credentials cannot be updated. Approval is a human action in the 1Password app: after 1pw_create_access_request, give the returned native onepassword:// approval link unmodified only to the account owner, outside the agent-controlled browser, and never open, decode, or approve it yourself. MCP never returns access-request IDs or tokens. Credentials backed by a customer-supplied 1Password access token and integration key are created and rotated by the integrating developer through the Kernel API, not through MCP; never ask for or accept those secrets in chat. This is unrelated to manage_credential_providers. ' + "Writes are never automatically retried; reconcile conflicts or uncertain outcomes before any further write.", inputSchema: vaultToolInput({ ...vaultItemSchema, @@ -177,7 +186,7 @@ export function registerVaultCredentialTools( spec: z .union([createSpec, onePasswordCreateSpec, updateSpec]) .describe( - "(create, update) Kernel create: description and ordered fields. 1Password create: account_id, website, and optional goal/reason/keywords. Update (Kernel only): description and/or fields keyed by name.", + "(create, update) Kernel create: description and ordered fields. 1Password create: account key, 1-5 logins, and optional goal. Update (Kernel only): description and/or fields keyed by name.", ) .refine( (spec) => diff --git a/src/lib/mcp/tools/vault-items.ts b/src/lib/mcp/tools/vault-items.ts index c6edb796..7ca3c18a 100644 --- a/src/lib/mcp/tools/vault-items.ts +++ b/src/lib/mcp/tools/vault-items.ts @@ -29,7 +29,7 @@ export function registerVaultItemTools( "manage_vault_items", { description: - 'Inspect credential and payment vault items and immutable audit events. "list" reads items without renewing collection links; "get" reads state, safe field metadata, version, required user actions, available_operations, and available_expansions. MCP returns explicitly non-sensitive text/email values; sensitive values and TOTP seeds are omitted. For credentials, present the collection URL only to the intended user, outside the agent-controlled browser; never ask for passwords or TOTP seeds in chat. Reopen collection using its advertised operation when available; TOTP has no hosted input. wait observes readiness, not edits to ready credentials: compare versions using get without wait. Use manage_vault_credentials for credential creation and updates; use a per-user vault, site-name-only description, and sensitive:false for ordinary usernames/emails. Credentials follow one of two user-chosen paths: Kernel-hosted collection (collect, fill) or 1Password brokered approval (1pw_create_access_request, 1pw_access_request_status, 1pw_fill on the credential; 1pw_recover to recover a failed account link on its credential_account). For 1Password, approval happens in the account owner\'s 1Password app: give the native onepassword:// approval link only to the owner, outside the agent-controlled browser, and never open or approve it yourself; MCP never returns access-request IDs. 1pw_fill can submit the form but does not prove login; never retry fill_unknown in the same browser. An uncertain access request stays blocked with no advertised operations; never delete or recreate the item to retry it. Never store credit card data in credential items. "invoke" fetches the item again and submits only an advertised operation; read its description and obtain explicit user approval first. Provider actions (OAuth, enrollment, MFA, approval) must be completed by the user, not invoked as operations. "events" observes outcomes; use the last event ID as after. "delete" invalidates an item credential; confirm with the user first. Unresolved payments can block item and parent deletion; the API decides whether explicit abandonment is allowed, and deletion never proves a payment did not occur. recovery_required is not decline or expiry: stop payment attempts and reconcile with the provider or support; no reset exists. Credential ready means required values exist, not that login succeeded; payment ready does not mean paid. For browser field writes, supply operation-specific inputs with browser_id and ordered field/selector bindings; values stay server-side until entering the browser. Link cards use the advertised browser field-writing operation, not aliases or egress substitution: inputs.page_url must be the exact current HTTPS top-level page URL at the approved merchant origin, and the browser must retain its vault attachment. Browser field writes return no card values but do not isolate them from browser/CDP access or explicitly submit checkout; failed or unknown writes may leave partial changes. Never automatically retry or fall back to aliases. AgentCard aliases and checkout hold/approval/replay remain supported. Follow each advertised operation\'s API contract for inputs and outcome handling; never substitute another operation or retry an uncertain attempt. Requests are never automatically retried. Do not retry failed, timed-out, rejected, or indeterminate payments; inspect state/events instead.', + 'Inspect credential and payment vault items and immutable audit events. "list" reads items without renewing collection links; "get" reads state, safe field metadata, version, required user actions, available_operations, and available_expansions. MCP returns explicitly non-sensitive text/email values; sensitive values and TOTP seeds are omitted. For credentials, present the collection URL only to the intended user, outside the agent-controlled browser; never ask for passwords or TOTP seeds in chat. Reopen collection using its advertised operation when available; TOTP has no hosted input. wait observes readiness, not edits to ready credentials: compare versions using get without wait. Use manage_vault_credentials for credential creation and updates; use a per-user vault, site-name-only description, and sensitive:false for ordinary usernames/emails. At a login page, list first and reuse a ready credential for that site; 1Password credentials show requested websites in spec.requests. Credentials follow one of two user-chosen paths: Kernel-hosted collection (collect, fill) or 1Password brokered approval (1pw_create_access_request, 1pw_access_request_status, 1pw_fill on the credential; 1pw_recover to recover a failed account link on its credential_account). For 1Password, approval happens in the account owner\'s 1Password app: give the native onepassword:// approval link only to the owner, outside the agent-controlled browser, and never open or approve it yourself; MCP never returns access-request IDs. 1pw_fill can submit the form but does not prove login; when several approved logins share the page origin, ask the owner which to use and pass its entry_id. Never retry fill_unknown in the same browser. An uncertain access request stays blocked with no advertised operations; never delete or recreate the item to retry it. Only after a confirmed failed status may you, with the end-user\'s approval, delete and recreate the credential for one new request. 1pw_update_access_token takes a secret token and is refused here; the integrating developer uses the Kernel API. Never store credit card data in credential items. "invoke" fetches the item again and submits only an advertised operation; read its description and obtain explicit user approval first. Provider actions (OAuth, enrollment, MFA, approval) must be completed by the user, not invoked as operations. "events" observes outcomes; use the last event ID as after. "delete" invalidates an item credential; confirm with the user first. Unresolved payments can block item and parent deletion; the API decides whether explicit abandonment is allowed, and deletion never proves a payment did not occur. recovery_required is not decline or expiry: stop payment attempts and reconcile with the provider or support; no reset exists. Credential ready means required values exist, not that login succeeded; payment ready does not mean paid. For browser field writes, supply operation-specific inputs with browser_id and ordered field/selector bindings; values stay server-side until entering the browser. Link cards use the advertised browser field-writing operation, not aliases or egress substitution: inputs.page_url must be the exact current HTTPS top-level page URL at the approved merchant origin, and the browser must retain its vault attachment. Browser field writes return no card values but do not isolate them from browser/CDP access or explicitly submit checkout; failed or unknown writes may leave partial changes. Never automatically retry or fall back to aliases. AgentCard aliases and checkout hold/approval/replay remain supported. Follow each advertised operation\'s API contract for inputs and outcome handling; never substitute another operation or retry an uncertain attempt. Requests are never automatically retried. Do not retry failed, timed-out, rejected, or indeterminate payments; inspect state/events instead.', inputSchema: vaultToolInput({ ...vaultItemSchema, action: z.enum(["list", "get", "invoke", "events", "delete"]), @@ -128,6 +128,10 @@ export function registerVaultItemTools( case "invoke": { if (!params.operation) return errorResponse("operation is required for invoke."); + if (params.operation === "1pw_update_access_token") + return errorResponse( + "1pw_update_access_token takes a secret access token and is not available through MCP. The integrating developer replaces it through the Kernel API; never ask for tokens in chat.", + ); const item = await client.vaults.items.retrieve( params.key, { id_or_name: params.vault }, @@ -174,7 +178,9 @@ export function registerVaultItemTools( ...jsonResponse({ result: projected, guidance: - "Inspect item state and events for the outcome. Do not automatically retry an uncertain operation.", + projected.type === "1pw_fill" + ? "fill_submitted means the 1Password extension reported submitting the form, not that login succeeded: check the page before continuing. Do not automatically retry a failed or uncertain fill." + : "Inspect item state and events for the outcome. Do not automatically retry an uncertain operation.", }), ...(typeof projected === "object" && projected !== null && diff --git a/src/lib/mcp/tools/vault-onepassword.test.ts b/src/lib/mcp/tools/vault-onepassword.test.ts index 0dec6dde..1af67511 100644 --- a/src/lib/mcp/tools/vault-onepassword.test.ts +++ b/src/lib/mcp/tools/vault-onepassword.test.ts @@ -44,7 +44,7 @@ const pendingCredential = { key: target.key, type: "credential", version: 1, - spec: { provider: "1password", account_id: account.id, requests }, + spec: { provider: "1password", account: account.key, requests }, state: { provider: "1password", status: "pending_authorization", @@ -57,7 +57,7 @@ const pendingCredential = { state: "pending", has_autofill_token: false, granted_count: 0, - entries: [{ ...requests.entries[0], id: "private-entry-id" }], + entries: [{ ...requests.entries[0], id: "entry-1" }], }, }, action: { @@ -96,7 +96,6 @@ function expectNoReferences(value: unknown, { approvalLink = false } = {}) { ...(approvalLink ? [] : [reference, "access_request_reference="]), "private-provider-path", "private-provider-identity", - "private-entry-id", "/approval/", ]) expect(text).not.toContain(privateValue); @@ -115,8 +114,12 @@ describe("1Password vault credentials", () => { expect(credentials).toContain("Kernel-hosted collection"); expect(credentials).toContain("1Password brokered approval"); expect(credentials).toContain("never open, decode, or approve"); + expect(credentials).toContain("where their login for the site lives"); + expect(credentials).toContain("First list the vault"); + expect(credentials).toContain("not shared-vault items or passkeys"); + expect(credentials).toContain("not through MCP"); expect(descriptionOf("manage_vaults")).toContain( - "Ask the user which they prefer", + "Reuse an existing credential for the site first", ); const items = descriptionOf("manage_vault_items"); expect(items).toContain("1pw_create_access_request"); @@ -124,7 +127,7 @@ describe("1Password vault credentials", () => { expect(items).toContain("recover a failed account link"); for (const { description } of tools) { expect(description).not.toContain("reconcile_access"); - expect(description).not.toMatch(/integration key|Family/i); + expect(description).not.toMatch(/Family/i); } expect(fixture.requests).toEqual([]); } finally { @@ -155,7 +158,10 @@ describe("1Password vault credentials", () => { { action: "connect_account", provider: "1password", - spec: { account_id: "vi_account", website: "https://example.com" }, + spec: { + account: "onepassword", + logins: [{ website: "https://example.com" }], + }, }, { action: "update", @@ -166,14 +172,17 @@ describe("1Password vault credentials", () => { { action: "create", provider: "1password", - spec: { account_id: "vi_account", website: "http://example.com" }, + spec: { + account: "onepassword", + logins: [{ website: "http://example.com" }], + }, }, { action: "create", provider: "1password", spec: { - account_id: "vi_account", - website: "https://example.com", + account: "onepassword", + logins: [{ website: "https://example.com" }], integration_key: "private-integration-key", }, }, @@ -181,11 +190,39 @@ describe("1Password vault credentials", () => { action: "create", provider: "1password", spec: { - account_id: "vi_account", - website: "https://example.com", - keywords: [], + logins: [{ website: "https://example.com" }], + access_token: "private-access-token", + integration_key: "private-integration-key", + }, + }, + { + action: "create", + provider: "1password", + spec: { + account: "onepassword", + logins: [{ website: "https://example.com", keywords: [] }], + }, + }, + { + action: "create", + provider: "1password", + spec: { account: "onepassword", logins: [] }, + }, + { + action: "create", + provider: "1password", + spec: { + account: "onepassword", + logins: Array.from({ length: 6 }, (_, i) => ({ + website: `https://site${i}.example/login`, + })), }, }, + { + action: "create", + provider: "1password", + spec: { account_id: "vi_account", website: "https://example.com" }, + }, ])("rejects invalid 1Password writes without requests (%#)", async (args) => { const fixture = await connectVaultTest([]); try { @@ -195,6 +232,7 @@ describe("1Password vault credentials", () => { }); expect(result.isError).toBe(true); expect(JSON.stringify(result)).not.toContain("private-integration-key"); + expect(JSON.stringify(result)).not.toContain("private-access-token"); expect(fixture.requests).toHaveLength(0); } finally { await fixture.close(); @@ -271,7 +309,9 @@ describe("1Password vault credentials", () => { }); expect(result.item.state.status).toBe("pending_authorization"); expect(result.guidance.join(" ")).toContain("only to the account owner"); - expect(result.guidance.join(" ")).toContain("account_id"); + expect(result.guidance.join(" ")).toContain( + "account set to this item's key", + ); } finally { await fixture.close(); } @@ -329,11 +369,15 @@ describe("1Password vault credentials", () => { action: "create", provider: "1password", spec: { - account_id: account.id, - website: "https://example.com/login", + account: account.key, goal: requests.goal, - reason: "Check order status", - keywords: ["example"], + logins: [ + { + website: "https://example.com/login", + reason: "Check order status", + keywords: ["example"], + }, + ], }, }); expect(result.isError).toBeUndefined(); @@ -342,7 +386,7 @@ describe("1Password vault credentials", () => { method: "PUT", body: { type: "credential", - spec: { provider: "1password", account_id: account.id, requests }, + spec: { provider: "1password", account: account.key, requests }, }, }); expectNoReferences(result, { approvalLink: true }); @@ -371,7 +415,7 @@ describe("1Password vault credentials", () => { state: "pending", has_autofill_token: false, granted_count: 0, - entries: requests.entries, + entries: [{ ...requests.entries[0], id: "entry-1" }], }, }); const guidance = result.guidance.join(" "); @@ -515,6 +559,7 @@ describe("1Password vault credentials", () => { status, ...(error_code && { error_code }), }); + expect(body.guidance).toContain("not that login succeeded"); expectNoReferences(result); } finally { await fixture.close(); @@ -522,6 +567,186 @@ describe("1Password vault credentials", () => { }, ); + test("requests several logins and fills the one the owner picks", async () => { + const logins = [ + { website: "https://example.com/login" }, + { website: "https://example.com/login", reason: "Work account" }, + { website: "https://shop.example/signin" }, + ]; + const multiRequests = { + version: 2, + entries: logins.map(({ website, ...rest }) => ({ + type: "login", + parameters: { website }, + ...rest, + })), + }; + const ready = { + ...readyCredential, + spec: { + provider: "1password", + account: account.key, + requests: multiRequests, + }, + state: { + ...readyCredential.state, + access_request: { + ...readyCredential.state.access_request, + granted_count: 3, + entries: multiRequests.entries.map((entry, i) => ({ + ...entry, + id: `entry-${i + 1}`, + })), + }, + }, + }; + const fixture = await connectVaultTest([ + Response.json({ ...pendingCredential, spec: ready.spec }), + Response.json(ready), + Response.json(ready), + Response.json({ type: "1pw_fill", status: "fill_submitted" }), + ]); + try { + await fixture.call("manage_vault_credentials", { + ...target, + action: "create", + provider: "1password", + spec: { account: account.key, logins }, + }); + expect(fixture.requests[0].body).toMatchObject({ + spec: { + provider: "1password", + account: account.key, + requests: multiRequests, + }, + }); + const read = toolResultJSON( + await fixture.call("manage_vault_items", { ...target, action: "get" }), + ); + expect( + read.item.state.access_request.entries.map( + (entry: { id: string }) => entry.id, + ), + ).toEqual(["entry-1", "entry-2", "entry-3"]); + expect(read.guidance.join(" ")).toContain( + "ask the owner which one to use and add entry_id", + ); + await fixture.call("manage_vault_items", { + ...target, + action: "invoke", + operation: "1pw_fill", + inputs: { + browser_id: "browser-1", + page_url: "https://example.com/login", + entry_id: "entry-2", + }, + }); + expect(fixture.requests[3].body).toEqual({ + type: "1pw_fill", + browser_id: "browser-1", + page_url: "https://example.com/login", + entry_id: "entry-2", + }); + } finally { + await fixture.close(); + } + }); + + test.each([ + { + status: "failed", + expected: "ask the end-user before deleting and recreating", + }, + { status: "declined", expected: "do not request again unless they ask" }, + ])( + "tells the agent what a $status request allows", + async ({ status, expected }) => { + const fixture = await connectVaultTest([ + Response.json({ + ...pendingCredential, + state: { provider: "1password", status }, + action: undefined, + available_operations: [], + }), + ]); + try { + const read = toolResultJSON( + await fixture.call("manage_vault_items", { + ...target, + action: "get", + }), + ); + const guidance = read.guidance.join(" "); + expect(guidance).toContain(expected); + expect(guidance).toContain("offer Kernel-hosted collection"); + expect(fixture.requests).toHaveLength(1); + } finally { + await fixture.close(); + } + }, + ); + + test("describes stored-token credentials without exposing or accepting tokens", async () => { + const storedToken = { + ...readyCredential, + spec: { + provider: "1password", + requests, + access_token_expires_at: "2026-10-01T00:00:00Z", + access_token: "private-access-token", + integration_key: "private-integration-key", + }, + available_operations: [ + ...readyCredential.available_operations, + { type: "1pw_update_access_token", description: "Replace the token." }, + ], + }; + const fixture = await connectVaultTest([Response.json(storedToken)]); + try { + const read = await fixture.call("manage_vault_items", { + ...target, + action: "get", + }); + const body = toolResultJSON(read); + expect(body.item.spec).toEqual({ + provider: "1password", + requests, + access_token_expires_at: "2026-10-01T00:00:00Z", + }); + expect(body.guidance.join(" ")).toContain( + "1pw_update_access_token is not available through MCP", + ); + const update = await fixture.call("manage_vault_items", { + ...target, + action: "invoke", + operation: "1pw_update_access_token", + inputs: { access_token: "private-access-token" }, + }); + expect(update.isError).toBe(true); + for (const result of [read, update]) { + const text = JSON.stringify(result); + expect(text).not.toContain("private-access-token"); + expect(text).not.toContain("private-integration-key"); + } + expect(fixture.requests).toHaveLength(1); + } finally { + await fixture.close(); + } + }); + + test("keeps account-backed guidance free of stored-token steps", async () => { + const fixture = await connectVaultTest([Response.json(readyCredential)]); + try { + const read = toolResultJSON( + await fixture.call("manage_vault_items", { ...target, action: "get" }), + ); + expect(read.item.spec.account).toBe(account.key); + expect(read.guidance.join(" ")).not.toContain("customer-supplied"); + } finally { + await fixture.close(); + } + }); + test.each(["1pw_create_access_request", "1pw_reconcile_access"])( "keeps an uncertain access request blocked: %s", async (operation) => { @@ -545,6 +770,9 @@ describe("1Password vault credentials", () => { expect(read.guidance.join(" ")).toContain( "never delete or recreate the item to retry", ); + expect(read.guidance.join(" ")).toContain( + "first check that the credential_account named by spec.account is connected", + ); const result = await fixture.call("manage_vault_items", { ...target, action: "invoke", diff --git a/src/lib/mcp/tools/vaults.ts b/src/lib/mcp/tools/vaults.ts index 6668906d..124af335 100644 --- a/src/lib/mcp/tools/vaults.ts +++ b/src/lib/mcp/tools/vaults.ts @@ -42,7 +42,7 @@ export function registerVaultCapabilities( "manage_vaults", { description: - 'Manage project-owned vaults for end-user credentials and payment items. Use a separate vault per end user, with an immutable name such as user-123; do not mix unrelated users. Vaults store credentials, not authenticated browser sessions, and do not submit website forms or merchant payments. "create" creates or retrieves a vault by immutable name; "list" lists the effective project only; "get" reads one; "delete" invalidates the vault and every item credential. Confirm deletion with the user first; unresolved payment operations block deletion and require provider/support reconciliation. Connect a payment wallet with manage_vault_wallets, configure a card with manage_vault_cards, and inspect credentials or payment items with manage_vault_items. Credentials have two paths: Kernel-hosted collection or 1Password brokered approval. Ask the user which they prefer before creating credentials with manage_vault_credentials. For Kernel-hosted collection, create definitions or update values, then use manage_vault_items to collect, observe readiness, and invoke fill with value-free bindings. For 1Password, connect the account, create the credential, and let the user approve access in their 1Password app. For credentials, inspect the website and create the named field definitions in its natural top-to-bottom order because that array order controls the user-facing form. Use only the recognizable site name as description and set sensitive:false explicitly for ordinary usernames/emails; passwords and TOTP seeds must be sensitive. Never put credit card data in credential items. Attach vaults when creating a browser; bindings cannot change later. Requests are not automatically retried.', + 'Manage project-owned vaults for end-user credentials and payment items. Use a separate vault per end user, with an immutable name such as user-123; do not mix unrelated users. Vaults store credentials, not authenticated browser sessions, and do not submit website forms or merchant payments. "create" creates or retrieves a vault by immutable name; "list" lists the effective project only; "get" reads one; "delete" invalidates the vault and every item credential. Confirm deletion with the user first; unresolved payment operations block deletion and require provider/support reconciliation. Connect a payment wallet with manage_vault_wallets, configure a card with manage_vault_cards, and inspect credentials or payment items with manage_vault_items. Credentials have two paths: Kernel-hosted collection or 1Password brokered approval. Reuse an existing credential for the site first; otherwise ask the user which they prefer, meaning where their login lives, before creating credentials with manage_vault_credentials. For Kernel-hosted collection, create definitions or update values, then use manage_vault_items to collect, observe readiness, and invoke fill with value-free bindings. For 1Password, connect the account, create the credential, and let the user approve access in their 1Password app. For credentials, inspect the website and create the named field definitions in its natural top-to-bottom order because that array order controls the user-facing form. Use only the recognizable site name as description and set sensitive:false explicitly for ordinary usernames/emails; passwords and TOTP seeds must be sensitive. Never put credit card data in credential items. Attach vaults when creating a browser; bindings cannot change later. Requests are not automatically retried.', inputSchema: vaultToolInput({ ...vaultProjectSchema, action: z.enum(["create", "list", "get", "delete"]), diff --git a/src/lib/mcp/vault-responses.ts b/src/lib/mcp/vault-responses.ts index bb6ecb97..a2a66d68 100644 --- a/src/lib/mcp/vault-responses.ts +++ b/src/lib/mcp/vault-responses.ts @@ -13,7 +13,8 @@ export const vaultProviderConfigFields = fields( ); const operationFields = fields("type description"); const totalFields = fields("type display_text amount"); -// Access-request IDs, provider paths, identities, and entry IDs are omitted. +// Access-request IDs, provider paths, and identities are omitted. Returned +// entry IDs are kept so 1pw_fill can select among approved logins. const onePasswordRequestEntryFields = { ...fields("type reason keywords"), parameters: fields("website"), @@ -22,6 +23,10 @@ const onePasswordRequestFields = { ...fields("version goal"), entries: onePasswordRequestEntryFields, }; +const onePasswordReturnedEntryFields = { + ...onePasswordRequestEntryFields, + ...fields("id"), +}; const paymentMethodFields = { ...fields("id provider type is_default"), display: fields("label brand last4"), @@ -38,7 +43,7 @@ export const vaultItemFields: OutputFields = { expanded: { payment_methods: paymentMethodFields }, spec: { ...fields( - "provider wallet user_id payment_method_id card_id amount currency merchant merchant_name merchant_url context expires_at description account_id", + "provider wallet user_id payment_method_id card_id amount currency merchant merchant_name merchant_url context expires_at description account access_token_expires_at", ), requests: onePasswordRequestFields, fields: fields("name label type required sensitive"), @@ -60,8 +65,11 @@ export const vaultItemFields: OutputFields = { fields: { "*": fields("has_value") }, access_request: { ...fields("state has_autofill_token granted_count goal"), - request: onePasswordRequestFields, - entries: onePasswordRequestEntryFields, + request: { + ...fields("version goal"), + entries: onePasswordReturnedEntryFields, + }, + entries: onePasswordReturnedEntryFields, }, preparation: fields( "id status browser_id merchant_origin environment created_at expires_at approval_url", @@ -347,7 +355,12 @@ export function vaultItemResponse( const typed = z .object({ type: z.string(), - spec: z.object({ provider: z.string().optional() }).optional(), + spec: z + .object({ + provider: z.string().optional(), + account: z.string().optional(), + }) + .optional(), }) .safeParse(projected); const onePasswordAccount = @@ -361,7 +374,9 @@ export function vaultItemResponse( const onePasswordGuidance = onePasswordAccount ? onePasswordAccountGuidance : onePasswordCredential - ? onePasswordCredentialGuidance + ? typed.data.spec?.account === undefined + ? [...onePasswordCredentialGuidance, onePasswordStoredTokenGuidance] + : onePasswordCredentialGuidance : undefined; const advertised = advertisedOperationsSchema.safeParse(projected); const payment = z @@ -431,15 +446,19 @@ export function vaultItemResponse( const onePasswordAccountGuidance = [ "This credential_account connects a 1Password account; it is not a fillable credential. If an action URL is present, present it only to the account owner, outside the agent-controlled browser, and let them complete 1Password consent and verify the account shown there. Never ask for 1Password passwords, Secret Keys, OAuth codes, or tokens in chat.", - 'Observe with manage_vault_items action: "get" until state.status is connected, then create 1Password credentials with manage_vault_credentials, provider: "1password", and account_id set to this item\'s id. declined or reconnect_required need the user to connect again with connect_account on the same key. 1pw_recover is advertised only when Kernel can recover a failed account link: after explicit user approval, call manage_vault_items with action: "invoke" and operation: "1pw_recover", present the returned link to the account owner the same way, and once recovery completes connect again on the same key. Never delete the account to recover.', + 'Observe with manage_vault_items action: "get" until state.status is connected. Before reusing a connected account for a new credential, confirm with the owner that it is their 1Password account; then create 1Password credentials with manage_vault_credentials, provider: "1password", and account set to this item\'s key. declined or reconnect_required need the user to connect again with connect_account on the same key. 1pw_recover is advertised only when Kernel can recover a failed account link: after explicit user approval, call manage_vault_items with action: "invoke" and operation: "1pw_recover", present the returned link to the account owner the same way, and once recovery completes connect again on the same key. Never delete the account to recover.', ]; const onePasswordCredentialGuidance = [ - 'Operations use manage_vault_items with action: "invoke", operation set to the advertised 1pw_* type, and inputs for that operation. 1Password credentials hold no values in Kernel. After explicit user approval, invoke operation: "1pw_create_access_request" with inputs {browser_id} and optional goal, reason, and keywords, using a browser created with this vault attached; Kernel loads the 1Password extension into that browser on demand.', - 'Approval is a human action in the account owner\'s 1Password app. When action.name is 1password_access_approval with a url, give that onepassword:// link unmodified only to the account owner, in a private surface outside the agent-controlled browser, to open on a device with the 1Password app; they choose the login and approve or deny there. The link grants nothing until they approve, but it identifies the request: never open it in a browser, decode it, post it where others can see it, or approve on their behalf. Without a url, MCP received no native link: tell the owner the approval link is unavailable and do not request again while pending. Invoke operation: "1pw_access_request_status" with inputs {browser_id} to observe the decision. Do not issue a second request while one is pending. If the item stays pending_authorization with no action and no advertised operations, a request may already have reached 1Password: stop, tell the owner to check 1Password, and never delete or recreate the item to retry.', - 'When ready, invoke operation: "1pw_fill" with inputs {browser_id, page_url}, where page_url is the exact current top-level URL on the requested login origin. The extension selects fields and submits; you cannot supply selectors or values. fill_submitted means the form was submitted, not that login succeeded: check the page. fill_unknown may have submitted; never retry it in the same browser.', + 'Operations use manage_vault_items with action: "invoke", operation set to the advertised 1pw_* type, and inputs for that operation. 1Password credentials hold no values in Kernel; spec.requests.entries lists the 1-5 requested logins and their websites. Only logins in the owner\'s own non-shared 1Password vault are supported, not shared-vault items or passkeys. After explicit user approval, invoke operation: "1pw_create_access_request" with inputs {browser_id} and an optional goal (reason and keywords only for a single-login request), using a browser created with this vault attached; Kernel loads the 1Password extension into that browser on demand.', + 'Approval is a human action in the account owner\'s 1Password app. When action.name is 1password_access_approval with a url, give that onepassword:// link unmodified only to the account owner, in a private surface outside the agent-controlled browser, to open on a device with the 1Password app; they choose the login and approve or deny there. The link grants nothing until they approve, but it identifies the request: never open it in a browser, decode it, post it where others can see it, or approve on their behalf. Without a url, MCP received no native link: tell the owner the approval link is unavailable and do not request again while pending. Invoke operation: "1pw_access_request_status" with inputs {browser_id} to observe the decision. Do not issue a second request while one is pending.', + "declined means the owner denied the request: do not request again unless they ask, and offer Kernel-hosted collection instead. failed is a confirmed failure: ask the end-user before deleting and recreating this credential for at most one new request, or offer Kernel-hosted collection. If the item stays pending_authorization with no action and no advertised operations, first check that the credential_account named by spec.account is connected; if it is, a request may already have reached 1Password: stop, tell the owner to check 1Password, and never delete or recreate the item to retry.", + 'When ready, invoke operation: "1pw_fill" with inputs {browser_id, page_url}, where page_url is the exact current top-level URL on a requested login origin. If several approved logins share that origin, ask the owner which one to use and add entry_id from state.access_request entries; never guess. The extension selects fields and submits; you cannot supply selectors or values. fill_submitted means the form was submitted, not that login succeeded: check the page. fill_failed with noExistingCredentials means the owner\'s 1Password has no usable login for the page: tell the owner instead of retrying. fill_unknown may have submitted; never retry it in the same browser.', ]; +const onePasswordStoredTokenGuidance = + "This credential has no account: it uses a customer-supplied 1Password access token stored encrypted by Kernel, and spec.access_token_expires_at is optional expiry metadata. The integrating developer replaces the token through the Kernel API; 1pw_update_access_token is not available through MCP. Never ask for or accept 1Password tokens or integration keys in chat. While the token is expired, request and fill are unavailable."; + export function throwVaultError( tool: string, action: string, From 5fbed03c54548b783aaf4dd17f8a088f8d8381e8 Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:52:07 +0000 Subject: [PATCH 7/9] Reject mismatched credential specs locally and drop refused token hints --- src/lib/mcp/tools/vault-credentials.ts | 22 +++++++-- src/lib/mcp/tools/vault-onepassword.test.ts | 50 +++++++++++++++++++++ src/lib/mcp/vault-responses.ts | 1 + 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/lib/mcp/tools/vault-credentials.ts b/src/lib/mcp/tools/vault-credentials.ts index 69c63047..59ecfa5f 100644 --- a/src/lib/mcp/tools/vault-credentials.ts +++ b/src/lib/mcp/tools/vault-credentials.ts @@ -265,13 +265,17 @@ export function registerVaultCredentialTools( if (params.spec === undefined) return errorResponse("spec is required for create and update."); if (params.action === "create" && params.provider === "1password") { - const spec = onePasswordCreateSpec.parse(params.spec); + const spec = onePasswordCreateSpec.safeParse(params.spec); + if (!spec.success) + return errorResponse( + 'provider: "1password" create requires spec {account, logins, goal?}. No request was sent.', + ); const credential = await client.vaults.items.upsert( params.key, { id_or_name: params.vault, type: "credential", - spec: onePasswordCredentialSpec(spec), + spec: onePasswordCredentialSpec(spec.data), }, options, ); @@ -280,7 +284,12 @@ export function registerVaultCredentialTools( let item: VaultItem; let writtenValues: (string | undefined)[]; if (params.action === "create") { - const spec = createSpec.parse(params.spec); + const parsed = createSpec.safeParse(params.spec); + if (!parsed.success) + return errorResponse( + 'provider: "kernel" create requires spec {description?, fields}. No request was sent.', + ); + const spec = parsed.data; item = await client.vaults.items.upsert( params.key, { @@ -297,7 +306,12 @@ export function registerVaultCredentialTools( } else { if (params.version === undefined) return errorResponse("version is required for update."); - const spec = updateSpec.parse(params.spec); + const parsed = updateSpec.safeParse(params.spec); + if (!parsed.success) + return errorResponse( + "update requires spec {description?, fields?} keyed by field name. No request was sent.", + ); + const spec = parsed.data; item = await client.vaults.items.update( params.key, { diff --git a/src/lib/mcp/tools/vault-onepassword.test.ts b/src/lib/mcp/tools/vault-onepassword.test.ts index 1af67511..d87c6779 100644 --- a/src/lib/mcp/tools/vault-onepassword.test.ts +++ b/src/lib/mcp/tools/vault-onepassword.test.ts @@ -239,6 +239,50 @@ describe("1Password vault credentials", () => { } }); + test.each([ + { + action: "create", + provider: "1password", + spec: { + fields: [{ name: "password", type: "password", value: "hunter2" }], + }, + }, + { + action: "create", + provider: "kernel", + spec: { + account: "onepassword", + logins: [{ website: "https://example.com" }], + }, + }, + { + action: "update", + version: 1, + spec: { + fields: [{ name: "password", type: "password", value: "hunter2" }], + }, + }, + ])( + "rejects a spec that does not match $provider $action before sending", + async (args) => { + const fixture = await connectVaultTest([]); + try { + const result = await fixture.call("manage_vault_credentials", { + ...target, + ...args, + }); + expect(result.isError).toBe(true); + const text = JSON.stringify(result); + expect(text).toContain("No request was sent"); + expect(text).not.toContain("hunter2"); + expect(text).not.toContain("payment"); + expect(fixture.requests).toHaveLength(0); + } finally { + await fixture.close(); + } + }, + ); + test("accepts the Kernel provider on update", async () => { const fixture = await connectVaultTest([ Response.json({ @@ -716,6 +760,12 @@ describe("1Password vault credentials", () => { expect(body.guidance.join(" ")).toContain( "1pw_update_access_token is not available through MCP", ); + expect( + body.hints.invocation.map( + (hint: { arguments: { operation: string } }) => + hint.arguments.operation, + ), + ).toEqual(["1pw_fill"]); const update = await fixture.call("manage_vault_items", { ...target, action: "invoke", diff --git a/src/lib/mcp/vault-responses.ts b/src/lib/mcp/vault-responses.ts index a2a66d68..89d2ff2e 100644 --- a/src/lib/mcp/vault-responses.ts +++ b/src/lib/mcp/vault-responses.ts @@ -398,6 +398,7 @@ export function vaultItemResponse( observation: vaultObservationHints(target).filter(safeHint), invocation: advertised.success ? advertised.data.available_operations + .filter(({ type }) => type !== "1pw_update_access_token") .map(({ type }) => ({ tool: "manage_vault_items", arguments: { ...target, action: "invoke", operation: type }, From 12389cb523a243b86123acd8e4770cf258384012 Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:06:44 +0000 Subject: [PATCH 8/9] Let agents request 1Password access again after an unmatched approval --- src/lib/mcp/tools/vault-onepassword.test.ts | 47 +++++++++++++++++++++ src/lib/mcp/vault-responses.ts | 4 +- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/lib/mcp/tools/vault-onepassword.test.ts b/src/lib/mcp/tools/vault-onepassword.test.ts index d87c6779..f9754449 100644 --- a/src/lib/mcp/tools/vault-onepassword.test.ts +++ b/src/lib/mcp/tools/vault-onepassword.test.ts @@ -784,6 +784,53 @@ describe("1Password vault credentials", () => { } }); + test("lets the owner request again after an approval without a usable login", async () => { + const statusReason = + "1Password did not return a uniquely matched login; request access again"; + const reopened = { + ...pendingCredential, + state: { + ...pendingCredential.state, + status_reason: statusReason, + access_request: { + ...pendingCredential.state.access_request, + state: "resolved", + }, + }, + action: undefined, + available_operations: [ + { + type: "1pw_create_access_request", + description: "Request access to the login.", + }, + ], + }; + const fixture = await connectVaultTest([Response.json(reopened)]); + try { + const read = toolResultJSON( + await fixture.call("manage_vault_items", { ...target, action: "get" }), + ); + expect(read.item.state.status_reason).toBe(statusReason); + expect(read.item.action).toBeUndefined(); + expect( + read.hints.invocation.map( + (hint: { arguments: { operation: string } }) => + hint.arguments.operation, + ), + ).toEqual(["1pw_create_access_request"]); + const guidance = read.guidance.join(" "); + expect(guidance).toContain( + "1pw_create_access_request is advertised again, the earlier request finished without a usable login", + ); + expect(guidance).toContain( + "with their approval, request access once more", + ); + expect(fixture.requests).toHaveLength(1); + } finally { + await fixture.close(); + } + }); + test("keeps account-backed guidance free of stored-token steps", async () => { const fixture = await connectVaultTest([Response.json(readyCredential)]); try { diff --git a/src/lib/mcp/vault-responses.ts b/src/lib/mcp/vault-responses.ts index 89d2ff2e..d612ec0d 100644 --- a/src/lib/mcp/vault-responses.ts +++ b/src/lib/mcp/vault-responses.ts @@ -452,8 +452,8 @@ const onePasswordAccountGuidance = [ const onePasswordCredentialGuidance = [ 'Operations use manage_vault_items with action: "invoke", operation set to the advertised 1pw_* type, and inputs for that operation. 1Password credentials hold no values in Kernel; spec.requests.entries lists the 1-5 requested logins and their websites. Only logins in the owner\'s own non-shared 1Password vault are supported, not shared-vault items or passkeys. After explicit user approval, invoke operation: "1pw_create_access_request" with inputs {browser_id} and an optional goal (reason and keywords only for a single-login request), using a browser created with this vault attached; Kernel loads the 1Password extension into that browser on demand.', - 'Approval is a human action in the account owner\'s 1Password app. When action.name is 1password_access_approval with a url, give that onepassword:// link unmodified only to the account owner, in a private surface outside the agent-controlled browser, to open on a device with the 1Password app; they choose the login and approve or deny there. The link grants nothing until they approve, but it identifies the request: never open it in a browser, decode it, post it where others can see it, or approve on their behalf. Without a url, MCP received no native link: tell the owner the approval link is unavailable and do not request again while pending. Invoke operation: "1pw_access_request_status" with inputs {browser_id} to observe the decision. Do not issue a second request while one is pending.', - "declined means the owner denied the request: do not request again unless they ask, and offer Kernel-hosted collection instead. failed is a confirmed failure: ask the end-user before deleting and recreating this credential for at most one new request, or offer Kernel-hosted collection. If the item stays pending_authorization with no action and no advertised operations, first check that the credential_account named by spec.account is connected; if it is, a request may already have reached 1Password: stop, tell the owner to check 1Password, and never delete or recreate the item to retry.", + 'Approval is a human action in the account owner\'s 1Password app. When action.name is 1password_access_approval with a url, give that onepassword:// link unmodified only to the account owner, in a private surface outside the agent-controlled browser, to open on a device with the 1Password app; they choose the login and approve or deny there. The link grants nothing until they approve, but it identifies the request: never open it in a browser, decode it, post it where others can see it, or approve on their behalf. Without a url, MCP received no native link: tell the owner the approval link is unavailable and do not request again while pending. Invoke operation: "1pw_access_request_status" with inputs {browser_id} to observe the decision. Do not issue a second request while an approval action or 1pw_access_request_status is present.', + "declined means the owner denied the request: do not request again unless they ask, and offer Kernel-hosted collection instead. If the item is pending_authorization with no action and 1pw_create_access_request is advertised again, the earlier request finished without a usable login: tell the owner the status_reason and, with their approval, request access once more. failed is a confirmed failure: read status_reason, then ask the end-user before deleting and recreating this credential for at most one new request, or offer Kernel-hosted collection. If the item stays pending_authorization with no action and no advertised operations, first check that the credential_account named by spec.account is connected; if it is, a request may already have reached 1Password: stop, tell the owner to check 1Password, and never delete or recreate the item to retry.", 'When ready, invoke operation: "1pw_fill" with inputs {browser_id, page_url}, where page_url is the exact current top-level URL on a requested login origin. If several approved logins share that origin, ask the owner which one to use and add entry_id from state.access_request entries; never guess. The extension selects fields and submits; you cannot supply selectors or values. fill_submitted means the form was submitted, not that login succeeded: check the page. fill_failed with noExistingCredentials means the owner\'s 1Password has no usable login for the page: tell the owner instead of retrying. fill_unknown may have submitted; never retry it in the same browser.', ]; From 19cdcf1c1d5fc977852fcbe2226e2c70beba286e Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:12:37 +0000 Subject: [PATCH 9/9] Pin @onkernel/sdk 0.114.0 --- README.md | 2 +- bun.lock | 4 ++-- docs/vault-payments.md | 5 +---- package.json | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1f1469c2..f01c7603 100644 --- a/README.md +++ b/README.md @@ -343,7 +343,7 @@ Call `get_connection_context` before deciding whether to create or select a proj - `manage_vault_credentials` - Create credentials through one of two user-chosen paths: Kernel-hosted collection (definitions for private human collection; update values or description with version and optional immutable item identity preconditions) or 1Password brokered approval (connect a 1Password account, then create a request for 1-5 logins the user approves in the 1Password app). Agents reuse an existing credential for the site first, otherwise ask the user where their login lives before creating credentials. - `manage_vault_items` - List, get, invoke advertised operations (including fill with value-free bindings), observe events, and delete vault items. Read credential definitions, presence, version, collection links, and explicitly non-sensitive values; sensitive values remain hidden. `collect` reopens the full form; provider approvals remain user actions. Ready is not login or payment success. -See [Vault payments](docs/vault-payments.md) for both provider flows, safety rules, and response shapes. `manage_browsers` accepts creation-only `vaults` references (max 20); existing sessions and pools cannot gain vault bindings. The six vault tools share the `vaults` toolset and prepare/observe credentials rather than submitting merchant payments. They are exposed only when `GET /org/entitlements` reports `features.vaults.enabled: true` for the current credential; missing or unavailable entitlements hide them. Toolset configuration cannot override this access check. Credential create → collect → readiness → fill is supported entirely through MCP tools. `prepare_checkout` remains API/CLI-only. The SDK dependency is pinned in `bun.lock`; it currently points at a temporary stlc development preview (TODO: replace with the official `@onkernel/sdk` release that includes 1Password vault credentials). +See [Vault payments](docs/vault-payments.md) for both provider flows, safety rules, and response shapes. `manage_browsers` accepts creation-only `vaults` references (max 20); existing sessions and pools cannot gain vault bindings. The six vault tools share the `vaults` toolset and prepare/observe credentials rather than submitting merchant payments. They are exposed only when `GET /org/entitlements` reports `features.vaults.enabled: true` for the current credential; missing or unavailable entitlements hide them. Toolset configuration cannot override this access check. Credential create → collect → readiness → fill is supported entirely through MCP tools. `prepare_checkout` remains API/CLI-only. The SDK dependency is pinned in `bun.lock`. ### Standalone tools diff --git a/bun.lock b/bun.lock index b6be03fc..5b3b372e 100644 --- a/bun.lock +++ b/bun.lock @@ -11,7 +11,7 @@ "@modelcontextprotocol/core": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "@onkernel/managed-auth-react": "0.5.3", - "@onkernel/sdk": "git+ssh://git@github.com/kernel/kernel-node-sdk-staging.git#be64af0301e30156e15529856b5107efc258964c", + "@onkernel/sdk": "0.114.0", "@posthog/mcp": "0.17.0", "@types/jsonwebtoken": "^9.0.10", "@types/redis": "^4.0.11", @@ -160,7 +160,7 @@ "@onkernel/managed-auth-react": ["@onkernel/managed-auth-react@0.5.3", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5Ps7h7HknAxL4ZLnJCzsLhW8MLRr0SH1P8+eALBZXCmnx/K+g5ra2ar3iLoWFQ8vf/EvpuDF1LigIT6tDjRKdA=="], - "@onkernel/sdk": ["@onkernel/sdk@git+ssh://git@github.com/kernel/kernel-node-sdk-staging.git#be64af0301e30156e15529856b5107efc258964c", {}, "be64af0301e30156e15529856b5107efc258964c"], + "@onkernel/sdk": ["@onkernel/sdk@0.114.0", "", {}, "sha512-bCvF01WU4oW0YgjJgKojM4pY8YPTqJuFE2no4FDu6NHJHEHgmH4rgVsF4ab966zMjo3cXrmLPhRsNiBw6pAczQ=="], "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eJopQrUk0WR7jViYDC29+Rp50xGvs4GtWOXBeqCoFMzutkkO3CZvHehA4JqnjfWMTSS8toqvRhCSOpOz62Wf9w=="], diff --git a/docs/vault-payments.md b/docs/vault-payments.md index b7bb3073..843ff915 100644 --- a/docs/vault-payments.md +++ b/docs/vault-payments.md @@ -13,10 +13,7 @@ there is no per-item test flag. AgentCard configuration responses report the introspected `test_mode`. A development or staging MCP endpoint does not make a card request a test transaction. - - -The Node SDK dependency is temporarily pinned to the stlc development preview -`kernel-node-sdk-staging@be64af0301e30156e15529856b5107efc258964c` in `bun.lock`. +The released Node SDK dependency is pinned in `bun.lock`. ## Credential collection and observation diff --git a/package.json b/package.json index ea833dd1..a0a424b1 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@modelcontextprotocol/core": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "@onkernel/managed-auth-react": "0.5.3", - "@onkernel/sdk": "git+ssh://git@github.com/kernel/kernel-node-sdk-staging.git#be64af0301e30156e15529856b5107efc258964c", + "@onkernel/sdk": "0.114.0", "@posthog/mcp": "0.17.0", "@types/jsonwebtoken": "^9.0.10", "@types/redis": "^4.0.11",