diff --git a/bun.lock b/bun.lock index 99b8815..b6088b1 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,7 @@ "@clerk/themes": "^2.4.19", "@modelcontextprotocol/sdk": "1.26.0", "@onkernel/managed-auth-react": "0.5.3", - "@onkernel/sdk": "^0.109.0", + "@onkernel/sdk": "^0.110.0", "@posthog/mcp": "0.10.1", "@types/jsonwebtoken": "^9.0.10", "@types/redis": "^4.0.11", @@ -154,7 +154,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.109.0", "", {}, "sha512-G8Rxz4bC4fiar1GqoJWeXJeHmXJxDJHnh4LsPZdT8Tf8I9752ZM8CB8FmvvWoGzKlTRTeXlzgKYliH1bW/d93Q=="], + "@onkernel/sdk": ["@onkernel/sdk@0.110.0", "", {}, "sha512-lUMXEcp8FsQkcVV8AzGU+dkrA9f5ucaaYMIDA6Y2N9oHCtcBENiUKFJpxZTA/+Oc9by8Zj+3H10qajCsWUgDiw=="], "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eJopQrUk0WR7jViYDC29+Rp50xGvs4GtWOXBeqCoFMzutkkO3CZvHehA4JqnjfWMTSS8toqvRhCSOpOz62Wf9w=="], diff --git a/package.json b/package.json index 184da1a..aef4a69 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@clerk/themes": "^2.4.19", "@modelcontextprotocol/sdk": "1.26.0", "@onkernel/managed-auth-react": "0.5.3", - "@onkernel/sdk": "^0.109.0", + "@onkernel/sdk": "^0.110.0", "@posthog/mcp": "0.10.1", "@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 fd5ed75..301f24d 100644 --- a/src/lib/mcp/tools/vault-credential-flow.test.ts +++ b/src/lib/mcp/tools/vault-credential-flow.test.ts @@ -8,6 +8,7 @@ const spec = { fields: [ { name: "username", + label: "Membership Number or Username", type: "text", required: true, sensitive: false, @@ -168,6 +169,10 @@ describe("MCP credential flow", () => { ); const items = tools.find((tool) => tool.name === "manage_vault_items"); expect(credentials?.inputSchema.properties).toHaveProperty("spec"); + expect(JSON.stringify(credentials?.inputSchema)).toContain('"label"'); + expect(JSON.stringify(credentials?.inputSchema)).toContain( + "128 UTF-8 bytes", + ); expect(credentials?.inputSchema.properties).toHaveProperty( "expected_item_id", ); @@ -333,6 +338,9 @@ describe("MCP credential flow", () => { ); expect(created.item.action.url).toBe(pending.action.url); expect(created.item.spec.fields).toEqual(spec.fields); + expect(created.item.spec.fields[0].label).toBe( + "Membership Number or Username", + ); expect( ( await fixture.call("manage_vault_items", { @@ -496,6 +504,17 @@ describe("MCP credential flow", () => { fields: [{ name: "password", type: "password", sensitive: false }], }, }, + ...[ + "", + " Username", + "Username ", + "User\nname", + "User\u200bname", + "x".repeat(129), + ].map((label) => ({ + action: "create", + spec: { fields: [{ name: "username", label, type: "text" }] }, + })), { action: "create", spec: { diff --git a/src/lib/mcp/tools/vault-credentials.ts b/src/lib/mcp/tools/vault-credentials.ts index 32e3cc1..9ece44d 100644 --- a/src/lib/mcp/tools/vault-credentials.ts +++ b/src/lib/mcp/tools/vault-credentials.ts @@ -18,11 +18,23 @@ import { const text = () => z.string().refine((value) => Buffer.byteLength(value, "utf8") <= 16384); const fieldName = () => z.string().regex(/^[a-zA-Z][a-zA-Z0-9_]{0,63}$/); +const fieldLabel = () => + z + .string() + .min(1) + .refine((label) => label === label.trim()) + .refine((label) => Buffer.byteLength(label, "utf8") <= 128) + .refine((label) => !/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(label)); const definition = z .object({ name: fieldName().describe( "Stable field name used for updates and browser fills.", ), + label: fieldLabel() + .optional() + .describe( + "Optional non-secret display text for users. Must be nonempty, have no leading or trailing whitespace, be at most 128 UTF-8 bytes, and contain no control, formatting, or line-separator characters. The form falls back to name. Labels never affect updates or browser fills.", + ), type: z.enum(["text", "email", "password", "totp"]), required: z.boolean().optional(), sensitive: z @@ -101,7 +113,7 @@ export function registerVaultCredentialTools( ) { server.tool( "manage_vault_credentials", - '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. 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. 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.', vaultToolInput({ ...vaultItemSchema, key: vaultKeySchema(), diff --git a/src/lib/mcp/vault-responses.ts b/src/lib/mcp/vault-responses.ts index 32477eb..cc6509d 100644 --- a/src/lib/mcp/vault-responses.ts +++ b/src/lib/mcp/vault-responses.ts @@ -38,7 +38,7 @@ export const vaultItemFields: OutputFields = { ...fields( "provider wallet user_id payment_method_id card_id amount currency merchant merchant_name merchant_url context expires_at description", ), - fields: fields("name type required sensitive"), + fields: fields("name label type required sensitive"), provider_config: fields("id name"), authorization: { method: null, @@ -332,7 +332,7 @@ export function vaultItemResponse( "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 collect operation.", 'Use manage_vault_items with action: "invoke" and operation: "collect" 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 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.", + "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 fill with manage_vault_items using a fill 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.", ] : [ diff --git a/src/lib/mcp/vault-steering.test.ts b/src/lib/mcp/vault-steering.test.ts index 3427482..4622fbc 100644 --- a/src/lib/mcp/vault-steering.test.ts +++ b/src/lib/mcp/vault-steering.test.ts @@ -14,6 +14,7 @@ const credential = { fields: [ { name: "username", + label: "Membership Number or Username", type: "text", required: true, sensitive: false, @@ -63,7 +64,12 @@ describe("vault OpenAPI steering", () => { ...credential, spec: { fields: [ - { name: "username", type: "text", sensitive: false }, + { + name: "username", + label: null, + type: "text", + sensitive: false, + }, { name: "password", type: "password" }, ], }, @@ -162,6 +168,7 @@ describe("vault OpenAPI steering", () => { expect(result.item.spec.description).toBe("Hacker News"); expect(result.item.spec.fields[0]).toEqual({ name: "username", + label: "Membership Number or Username", type: "text", required: true, sensitive: false, @@ -287,7 +294,12 @@ describe("vault OpenAPI steering", () => { spec: { ...credential.spec, fields: [ - { name: "password", type: "password", value: "private-value" }, + { + name: "password", + label: "Account Password", + type: "password", + value: "private-value", + }, { name: "username", type: "text", sensitive: false }, ], }, @@ -296,7 +308,7 @@ describe("vault OpenAPI steering", () => { ), ); expect(result.item.spec.fields).toEqual([ - { name: "password", type: "password" }, + { name: "password", label: "Account Password", type: "password" }, { name: "username", type: "text", sensitive: false }, ]); expect(JSON.stringify(result)).not.toContain("private-value");