@datalayer/agent-runtimes 1.5.8 → 1.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -304,12 +304,47 @@ function parsePap(data) {
304
304
  operations: records(company.operations).map(operation => ({
305
305
  action: text(operation.action),
306
306
  tool: text(operation.tool),
307
+ ...(isData(operation.allows)
308
+ ? { allows: parsePapStanding(operation.allows) }
309
+ : {}),
307
310
  summary: text(operation.summary),
308
311
  userApprovalRequired: flag(operation.user_approval_required, true),
309
312
  })),
310
313
  },
311
314
  };
312
315
  }
316
+ /** A standing permission an operation gives (PAP_DATALAYER R-07), as agentspecs reads it. */
317
+ function parsePapStanding(data) {
318
+ return {
319
+ connection: text(data.connection),
320
+ action: oneOf(data.action, ['write', 'buy'], 'buy'),
321
+ limits: records(data.limits).map(limit => ({
322
+ path: text(limit.path),
323
+ ...(typeof limit.maximum === 'string'
324
+ ? { maximum: text(limit.maximum) }
325
+ : {}),
326
+ ...(typeof limit.allowed === 'string'
327
+ ? { allowed: text(limit.allowed) }
328
+ : {}),
329
+ })),
330
+ uses: text(data.uses),
331
+ days: text(data.days),
332
+ };
333
+ }
334
+ /** A standing permission as its document says it: every field, none has a default. */
335
+ function dumpPapStanding(standing) {
336
+ return {
337
+ connection: standing.connection,
338
+ action: standing.action,
339
+ limits: standing.limits.map(limit => ({
340
+ path: limit.path,
341
+ ...(limit.maximum !== undefined ? { maximum: limit.maximum } : {}),
342
+ ...(limit.allowed !== undefined ? { allowed: limit.allowed } : {}),
343
+ })),
344
+ uses: standing.uses,
345
+ days: standing.days,
346
+ };
347
+ }
313
348
  /** Personal Agent Protocol as its document says it, nothing at its default. */
314
349
  function dumpPap(pap) {
315
350
  if (!pap)
@@ -337,7 +372,11 @@ function dumpPap(pap) {
337
372
  .list('registered_agents', pap.company.registeredAgents)
338
373
  .list('operations', pap.company.operations.map(operation => new Writer()
339
374
  .text('action', operation.action, '\u0000')
340
- .text('tool', operation.tool, '\u0000')
375
+ // An operation that allows a standing permission calls no tool (R-07).
376
+ .text('tool', operation.tool, operation.allows ? '' : '\u0000')
377
+ .present('allows', operation.allows
378
+ ? dumpPapStanding(operation.allows)
379
+ : undefined)
341
380
  .text('summary', operation.summary, '\u0000')
342
381
  .value('user_approval_required', operation.userApprovalRequired, true).data)).data).data;
343
382
  }
@@ -0,0 +1,73 @@
1
+ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
2
+ import type { CallToolResult, GetPromptResult, ReadResourceResult, Tool } from '@modelcontextprotocol/sdk/types.js';
3
+ /** A listed tool as a client reads it. */
4
+ export interface McpToolReading {
5
+ name: string;
6
+ title: string | null;
7
+ description: string | null;
8
+ /** Its parameters' names, sorted. */
9
+ parameters: string[];
10
+ /** Those it requires, sorted. */
11
+ required: string[];
12
+ /** Whether it says an output schema (answers structured content). */
13
+ structured: boolean;
14
+ readOnly: boolean;
15
+ destructive: boolean;
16
+ idempotent: boolean;
17
+ openWorld: boolean;
18
+ }
19
+ /** A tool's answer as a client reads it. */
20
+ export interface McpCallReading {
21
+ isError: boolean;
22
+ /** Its text parts, joined by a newline. */
23
+ text: string;
24
+ structured: Record<string, unknown> | null;
25
+ }
26
+ /** A text resource's contents as a client reads them. */
27
+ export interface McpResourceReading {
28
+ uri: string;
29
+ mimeType: string | null;
30
+ text: string;
31
+ }
32
+ /** A prompt's message as a client reads it. */
33
+ export interface McpPromptReading {
34
+ role: 'user' | 'assistant';
35
+ text: string;
36
+ }
37
+ /** A listed tool: its parameters, whether it answers structured, its hints. */
38
+ export declare function toolReading(tool: Tool): McpToolReading;
39
+ /** A tool's answer: refused or not, its text parts joined, its structured content. */
40
+ export declare function callReading(result: CallToolResult): McpCallReading;
41
+ /** A resource's text contents; a binary one is refused, never decoded here. */
42
+ export declare function resourceReading(result: ReadResourceResult): McpResourceReading[];
43
+ /** A prompt's messages as text; a message of another kind is refused. */
44
+ export declare function promptReading(result: GetPromptResult): McpPromptReading[];
45
+ /** Where an `McpClient` connects: a Streamable HTTP endpoint, or any transport. */
46
+ export type McpServerAddress = {
47
+ url: string;
48
+ /** Headers sent with every request (a bearer, for one). */
49
+ headers?: Record<string, string>;
50
+ } | {
51
+ transport: Transport;
52
+ };
53
+ /**
54
+ * An MCP client: connects, lists and calls tools, reads resources, gets
55
+ * prompts, and answers each as the readings above. The SDK's `Client` does
56
+ * the protocol (the initialize handshake, the version negotiated, a
57
+ * structured answer checked against the tool's output schema).
58
+ */
59
+ export declare class McpClient {
60
+ private readonly client;
61
+ private readonly agreed;
62
+ private constructor();
63
+ /** Connected to `server`, said to it as `name` at `version`. */
64
+ static connect(server: McpServerAddress, name?: string, version?: string): Promise<McpClient>;
65
+ /** The protocol version agreed with the server. */
66
+ get protocolVersion(): string;
67
+ /** Every tool the server lists, through its pages. */
68
+ listTools(): Promise<McpToolReading[]>;
69
+ callTool(name: string, args: Record<string, unknown>): Promise<McpCallReading>;
70
+ readResource(uri: string): Promise<McpResourceReading[]>;
71
+ getPrompt(name: string, args: Record<string, string>): Promise<McpPromptReading[]>;
72
+ close(): Promise<void>;
73
+ }
@@ -0,0 +1,136 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * An MCP client in TypeScript, on the official `@modelcontextprotocol/sdk`
7
+ * (AGENT_PROTOCOLS PR-51, decided 2026-10-13), and what it reads from a
8
+ * server's answers — the twin of `agent_runtimes.protocols.mcp.reading`.
9
+ *
10
+ * The readings are the plain shape both languages agree on, held to
11
+ * `agent_runtimes/protocols/mcp/fixtures/session.json` by the pytest and the
12
+ * vitest. A tool's missing annotations are read with the specification's
13
+ * defaults (`readOnlyHint` false, `destructiveHint` true, `idempotentHint`
14
+ * false, `openWorldHint` true), so a server that says nothing is never read
15
+ * as harmless; `destructiveHint` means something only when the tool is not
16
+ * read-only, so a read-only tool is never destructive.
17
+ *
18
+ * @module protocols/mcp/client
19
+ */
20
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
21
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
22
+ const sorted = (values) => [...values].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
23
+ /** A listed tool: its parameters, whether it answers structured, its hints. */
24
+ export function toolReading(tool) {
25
+ const hints = tool.annotations;
26
+ const readOnly = hints?.readOnlyHint === true;
27
+ return {
28
+ name: tool.name,
29
+ title: tool.title ?? null,
30
+ description: tool.description ?? null,
31
+ parameters: sorted(Object.keys(tool.inputSchema.properties ?? {})),
32
+ required: sorted(tool.inputSchema.required ?? []),
33
+ structured: tool.outputSchema !== undefined,
34
+ readOnly,
35
+ destructive: !readOnly && hints?.destructiveHint !== false,
36
+ idempotent: hints?.idempotentHint === true,
37
+ openWorld: hints?.openWorldHint !== false,
38
+ };
39
+ }
40
+ /** A tool's answer: refused or not, its text parts joined, its structured content. */
41
+ export function callReading(result) {
42
+ return {
43
+ isError: result.isError === true,
44
+ text: result.content
45
+ .flatMap(part => (part.type === 'text' ? [part.text] : []))
46
+ .join('\n'),
47
+ structured: result.structuredContent ?? null,
48
+ };
49
+ }
50
+ /** A resource's text contents; a binary one is refused, never decoded here. */
51
+ export function resourceReading(result) {
52
+ return result.contents.map(part => {
53
+ if (!('text' in part) || typeof part.text !== 'string') {
54
+ throw new Error(`${part.uri} is not text.`);
55
+ }
56
+ return { uri: part.uri, mimeType: part.mimeType ?? null, text: part.text };
57
+ });
58
+ }
59
+ /** A prompt's messages as text; a message of another kind is refused. */
60
+ export function promptReading(result) {
61
+ return result.messages.map(message => {
62
+ if (message.content.type !== 'text') {
63
+ throw new Error(`A ${message.role} message is not text.`);
64
+ }
65
+ return { role: message.role, text: message.content.text };
66
+ });
67
+ }
68
+ /**
69
+ * An MCP client: connects, lists and calls tools, reads resources, gets
70
+ * prompts, and answers each as the readings above. The SDK's `Client` does
71
+ * the protocol (the initialize handshake, the version negotiated, a
72
+ * structured answer checked against the tool's output schema).
73
+ */
74
+ export class McpClient {
75
+ client;
76
+ agreed;
77
+ constructor(client, agreed) {
78
+ this.client = client;
79
+ this.agreed = agreed;
80
+ }
81
+ /** Connected to `server`, said to it as `name` at `version`. */
82
+ static async connect(server, name = 'agent-runtimes', version = '1.0.0') {
83
+ const transport = 'transport' in server
84
+ ? server.transport
85
+ : new StreamableHTTPClientTransport(new URL(server.url), {
86
+ requestInit: server.headers
87
+ ? { headers: server.headers }
88
+ : undefined,
89
+ });
90
+ // The SDK hands the version agreed at initialize to the transport alone.
91
+ let agreed;
92
+ const setProtocolVersion = transport.setProtocolVersion?.bind(transport);
93
+ transport.setProtocolVersion = (protocolVersion) => {
94
+ agreed = protocolVersion;
95
+ setProtocolVersion?.(protocolVersion);
96
+ };
97
+ const client = new Client({ name, version });
98
+ await client.connect(transport);
99
+ return new McpClient(client, () => agreed);
100
+ }
101
+ /** The protocol version agreed with the server. */
102
+ get protocolVersion() {
103
+ const version = this.agreed();
104
+ if (!version) {
105
+ throw new Error('The MCP client is not connected.');
106
+ }
107
+ return version;
108
+ }
109
+ /** Every tool the server lists, through its pages. */
110
+ async listTools() {
111
+ const tools = [];
112
+ let cursor;
113
+ do {
114
+ const page = await this.client.listTools(cursor ? { cursor } : {});
115
+ tools.push(...page.tools.map(toolReading));
116
+ cursor = page.nextCursor;
117
+ } while (cursor);
118
+ return tools;
119
+ }
120
+ async callTool(name, args) {
121
+ const result = await this.client.callTool({ name, arguments: args });
122
+ if (!Array.isArray(result.content)) {
123
+ throw new Error(`${name} answered in the form before MCP 2024-11-05.`);
124
+ }
125
+ return callReading(result);
126
+ }
127
+ async readResource(uri) {
128
+ return resourceReading(await this.client.readResource({ uri }));
129
+ }
130
+ async getPrompt(name, args) {
131
+ return promptReading(await this.client.getPrompt({ name, arguments: args }));
132
+ }
133
+ async close() {
134
+ await this.client.close();
135
+ }
136
+ }
@@ -2,3 +2,4 @@ export declare const MCP_PLUGIN = "agent-runtimes.protocols.mcp";
2
2
  /** The MCP specification version of the `@modelcontextprotocol/sdk` installed (1.32: `LATEST_PROTOCOL_VERSION`); Python's `mcp` 2.1 speaks 2026-07-28. */
3
3
  export declare const MCP_SPECIFICATION_VERSION = "2025-11-25";
4
4
  export declare const McpPlugin: import("@datalayer/reactor").ReactorPlugin<Record<string, never>, unknown, unknown>;
5
+ export * from './client';
@@ -7,7 +7,8 @@
7
7
  * declares no point yet: MCP's optional capabilities and extensions become
8
8
  * `mcp.extension.<name>` points when the MCP client and server read them
9
9
  * from contributions (AGENT_PROTOCOLS PR-01). The twin of
10
- * `agent_runtimes.protocols.mcp`.
10
+ * `agent_runtimes.protocols.mcp`. Its client, on the official
11
+ * `@modelcontextprotocol/sdk`, is `./client` (PR-51).
11
12
  *
12
13
  * @module protocols/mcp
13
14
  */
@@ -36,3 +37,4 @@ export const McpPlugin = definePlugin({
36
37
  }, { id: 'mcp', order: DEFAULT_ORDER }),
37
38
  ],
38
39
  });
40
+ export * from './client';
@@ -0,0 +1,56 @@
1
+ import { type HttpSigner, type NonceStore } from './index';
2
+ /** Draft until Visa confirms it. */
3
+ export declare const TAP_STATUS = "draft";
4
+ /** Where the profile was read, 2026-10-13. */
5
+ export declare const TAP_SOURCES: readonly ["https://developer.visa.com/capabilities/trusted-agent-protocol/trusted-agent-protocol-specifications", "https://developer.visa.com/capabilities/trusted-agent-protocol/overview", "https://github.com/visa/trusted-agent-protocol"];
6
+ /** Visa's key directory, as its page says it. */
7
+ export declare const VISA_JWKS = "https://mcp.visa.com/.well-known/jwks";
8
+ export declare const TAP_BROWSE_TAG = "agent-browser-auth";
9
+ export declare const TAP_PAY_TAG = "agent-payer-auth";
10
+ export type TapIntent = 'browse' | 'pay';
11
+ export declare const TAP_TAGS: Record<TapIntent, string>;
12
+ export declare const TAP_REQUIRED_COMPONENTS: readonly ["@authority", "@path"];
13
+ export declare const TAP_REQUIRED_PARAMS: readonly ["created", "keyid", "alg", "expires", "nonce", "tag"];
14
+ /** `alg` as Visa writes it. */
15
+ export declare const TAP_ALG = "Ed25519";
16
+ /** The longest window, and how long a nonce is remembered. */
17
+ export declare const TAP_MAX_WINDOW_SECONDS: number;
18
+ export interface TapSignOptions {
19
+ intent: TapIntent;
20
+ label?: string;
21
+ /** Covered: at least `@authority` and `@path` (the default). */
22
+ components?: string[];
23
+ /** Unix seconds; now by default. */
24
+ created?: number;
25
+ /** Seconds, at most 8 minutes (the default). */
26
+ lifetime?: number;
27
+ nonce?: string;
28
+ }
29
+ export interface TapSignedHeaders {
30
+ 'Signature-Input': string;
31
+ Signature: string;
32
+ }
33
+ /** What verifying a TAP signature found; `reason` when it does not hold. */
34
+ export interface TapVerification {
35
+ verified: boolean;
36
+ reason?: string;
37
+ intent?: TapIntent;
38
+ keyid?: string;
39
+ nonce?: string;
40
+ }
41
+ /** Sign one request as a TAP agent browsing or paying, under an Ed25519 key. */
42
+ export declare function signTapRequest(request: {
43
+ method: string;
44
+ url: string;
45
+ }, signer: HttpSigner, options: TapSignOptions): Promise<TapSignedHeaders>;
46
+ /**
47
+ * Verify the TAP signature of one request against Ed25519 keys known by
48
+ * `keyid` (Visa's directory, or a registry's). A request with no TAP tag is
49
+ * no trusted agent's; the merchant decides what to do with it.
50
+ */
51
+ export declare function verifyTapRequest(request: Request, keys: Map<string, JsonWebKey>, options: {
52
+ nonceStore: NonceStore;
53
+ now?: number;
54
+ }): Promise<TapVerification>;
55
+ /** The `keyid` of an Ed25519 public JWK, as a TAP key list is indexed when it says none. */
56
+ export declare function tapKeys(jwks: JsonWebKey[]): Promise<Map<string, JsonWebKey>>;
@@ -0,0 +1,185 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * Visa's Trusted Agent Protocol as a profile over Web Bot Auth
7
+ * (AGENT_PROTOCOLS PR-24) — the twin of
8
+ * `agent_runtimes.protocols.web_bot_auth.tap`.
9
+ *
10
+ * **Draft.** Built on 2026-10-13 from Visa's public *Merchant
11
+ * Specifications* and its reference repository (`TAP_SOURCES`), not
12
+ * confirmed by Visa: the product says it *arriving* until it is. Visa
13
+ * publishes no test vector; `fixtures/tap.json` is one computed in Python
14
+ * and reproduced here byte for byte.
15
+ *
16
+ * The agent-recognition signature: `Signature-Input` and `Signature` alone
17
+ * (Visa names no `Signature-Agent`, so keys are handed in by `keyid`),
18
+ * covering at least `@authority` and `@path`, with `created`, `keyid`,
19
+ * `alg="Ed25519"`, `expires`, `nonce` and `tag` — `agent-browser-auth` to
20
+ * browse, `agent-payer-auth` to pay —, `created` and `expires` at most 8
21
+ * minutes apart, a nonce seen in the last 8 minutes a replay. Signed and
22
+ * verified with Cloudflare's `http-message-sig`, the RFC 9421 library under
23
+ * `web-bot-auth`.
24
+ *
25
+ * @module protocols/web-bot-auth/tap
26
+ */
27
+ import { createSignature, verifySignature, webcrypto } from 'http-message-sig';
28
+ import { generateNonce, jwkThumbprint, } from './index';
29
+ /** Draft until Visa confirms it. */
30
+ export const TAP_STATUS = 'draft';
31
+ /** Where the profile was read, 2026-10-13. */
32
+ export const TAP_SOURCES = [
33
+ 'https://developer.visa.com/capabilities/trusted-agent-protocol/trusted-agent-protocol-specifications',
34
+ 'https://developer.visa.com/capabilities/trusted-agent-protocol/overview',
35
+ 'https://github.com/visa/trusted-agent-protocol',
36
+ ];
37
+ /** Visa's key directory, as its page says it. */
38
+ export const VISA_JWKS = 'https://mcp.visa.com/.well-known/jwks';
39
+ export const TAP_BROWSE_TAG = 'agent-browser-auth';
40
+ export const TAP_PAY_TAG = 'agent-payer-auth';
41
+ export const TAP_TAGS = {
42
+ browse: TAP_BROWSE_TAG,
43
+ pay: TAP_PAY_TAG,
44
+ };
45
+ export const TAP_REQUIRED_COMPONENTS = ['@authority', '@path'];
46
+ export const TAP_REQUIRED_PARAMS = [
47
+ 'created',
48
+ 'keyid',
49
+ 'alg',
50
+ 'expires',
51
+ 'nonce',
52
+ 'tag',
53
+ ];
54
+ /** `alg` as Visa writes it. */
55
+ export const TAP_ALG = 'Ed25519';
56
+ /** The longest window, and how long a nonce is remembered. */
57
+ export const TAP_MAX_WINDOW_SECONDS = 8 * 60;
58
+ /** Sign one request as a TAP agent browsing or paying, under an Ed25519 key. */
59
+ export async function signTapRequest(request, signer, options) {
60
+ if (signer.algorithm !== 'ed25519') {
61
+ throw new Error('a TAP signature is Ed25519');
62
+ }
63
+ const components = options.components ?? [...TAP_REQUIRED_COMPONENTS];
64
+ const missing = TAP_REQUIRED_COMPONENTS.filter(name => !components.includes(name));
65
+ if (missing.length) {
66
+ throw new Error(`a TAP signature covers ${missing.join(', ')}`);
67
+ }
68
+ const lifetime = options.lifetime ?? TAP_MAX_WINDOW_SECONDS;
69
+ if (!(lifetime > 0 && lifetime <= TAP_MAX_WINDOW_SECONDS)) {
70
+ throw new Error(`a TAP signature lives at most ${TAP_MAX_WINDOW_SECONDS}s (8 minutes)`);
71
+ }
72
+ const created = options.created ?? Math.floor(Date.now() / 1000);
73
+ const fields = await createSignature({
74
+ kind: 'request',
75
+ method: request.method,
76
+ targetUri: request.url,
77
+ fields: [],
78
+ }, {
79
+ label: options.label ?? 'sig2',
80
+ components,
81
+ parameters: {
82
+ created,
83
+ keyid: signer.keyid,
84
+ alg: TAP_ALG,
85
+ expires: created + lifetime,
86
+ nonce: options.nonce ?? generateNonce(),
87
+ tag: TAP_TAGS[options.intent],
88
+ },
89
+ signer: { algorithm: TAP_ALG, sign: data => signer.sign(data) },
90
+ });
91
+ return {
92
+ 'Signature-Input': fields.signatureInput,
93
+ Signature: fields.signature,
94
+ };
95
+ }
96
+ const TAP_LABEL = /(?:^|,)\s*([a-z*][a-z0-9_.*-]*)=\([^)]*\)[^,]*;tag="(?:agent-browser-auth|agent-payer-auth)"/g;
97
+ /**
98
+ * Verify the TAP signature of one request against Ed25519 keys known by
99
+ * `keyid` (Visa's directory, or a registry's). A request with no TAP tag is
100
+ * no trusted agent's; the merchant decides what to do with it.
101
+ */
102
+ export async function verifyTapRequest(request, keys, options) {
103
+ const input = request.headers.get('signature-input');
104
+ if (!input || !request.headers.get('signature')) {
105
+ return { verified: false, reason: 'the request is not signed' };
106
+ }
107
+ const labels = [...input.matchAll(TAP_LABEL)].map(match => match[1]);
108
+ if (labels.length === 0) {
109
+ return { verified: false, reason: 'no signature carries a TAP tag' };
110
+ }
111
+ if (labels.length > 1) {
112
+ return {
113
+ verified: false,
114
+ reason: 'more than one signature carries a TAP tag',
115
+ };
116
+ }
117
+ const now = options.now ?? Math.floor(Date.now() / 1000);
118
+ let keyid;
119
+ let unknownKey = false;
120
+ try {
121
+ const verified = await verifySignature(request, {
122
+ label: labels[0],
123
+ policy: {
124
+ algorithms: [TAP_ALG],
125
+ requiredComponents: [...TAP_REQUIRED_COMPONENTS],
126
+ requiredParameters: [...TAP_REQUIRED_PARAMS],
127
+ now,
128
+ validate: signature => {
129
+ const { created, expires, tag } = signature.parameters;
130
+ return ((tag === TAP_BROWSE_TAG || tag === TAP_PAY_TAG) &&
131
+ created !== undefined &&
132
+ expires !== undefined &&
133
+ expires - created <= TAP_MAX_WINDOW_SECONDS &&
134
+ now < expires);
135
+ },
136
+ },
137
+ resolveVerifier: async (candidate) => {
138
+ keyid = candidate.parameters.keyid;
139
+ const jwk = keyid === undefined ? undefined : keys.get(keyid);
140
+ if (!jwk || jwk.kty !== 'OKP' || jwk.crv !== 'Ed25519') {
141
+ unknownKey = true;
142
+ throw new Error('the key is not a known Ed25519 key');
143
+ }
144
+ const key = await crypto.subtle.importKey('jwk', jwk, { name: 'Ed25519' }, false, ['verify']);
145
+ const verifier = webcrypto.verifier(key);
146
+ return {
147
+ algorithm: TAP_ALG,
148
+ verify: (data, signature) => verifier.verify(data, signature),
149
+ };
150
+ },
151
+ });
152
+ const { tag, nonce } = verified.parameters;
153
+ if (nonce === undefined || keyid === undefined) {
154
+ return { verified: false, reason: 'the signature has no nonce', keyid };
155
+ }
156
+ const fresh = await options.nonceStore.checkAndRecord(`tap ${keyid}`, nonce, new Date((now + TAP_MAX_WINDOW_SECONDS) * 1000));
157
+ if (!fresh) {
158
+ return { verified: false, reason: 'the nonce was seen before', keyid };
159
+ }
160
+ return {
161
+ verified: true,
162
+ intent: tag === TAP_PAY_TAG ? 'pay' : 'browse',
163
+ keyid,
164
+ nonce,
165
+ };
166
+ }
167
+ catch (error) {
168
+ return {
169
+ verified: false,
170
+ reason: unknownKey
171
+ ? 'the key is not a known Ed25519 key'
172
+ : error.message,
173
+ keyid,
174
+ };
175
+ }
176
+ }
177
+ /** The `keyid` of an Ed25519 public JWK, as a TAP key list is indexed when it says none. */
178
+ export async function tapKeys(jwks) {
179
+ const keys = new Map();
180
+ for (const jwk of jwks) {
181
+ const kid = jwk.kid;
182
+ keys.set(kid ?? (await jwkThumbprint(jwk)), jwk);
183
+ }
184
+ return keys;
185
+ }
package/lib/specs/apps.js CHANGED
@@ -4444,13 +4444,15 @@ export const SHOPPING_DESK_APP_0_0_1 = {
4444
4444
  },
4445
4445
  ],
4446
4446
  verified: {
4447
- live: [],
4447
+ live: [
4448
+ "Drilled on r1 on 13 October 2026 against the hosted test merchants, as its Preview's runtime reaches the gateway's ucp and acp toolsets: at Example Store (store.examples.datalayer.run) a search, a checkout filled, the exact purchase asked of the person, approved on Waiting for you, bought once and refused a second time, its order read; at Example Outfitters (outfitters.examples.datalayer.run) the same checkout over ACP. Test merchants only: paid with a test instrument.",
4449
+ ],
4448
4450
  recorded: [
4449
4451
  "The purchase is the developers' UCP example (examples/protocols/ucp/shopping.py) against its local test store, Example Store, run in its own tests.",
4450
4452
  "The checkout at Example Outfitters is the developers' ACP example (examples/protocols/agentic_commerce/checkout.py), run in its own tests.",
4451
4453
  ],
4452
4454
  unverified: [
4453
- 'It has not run in the Studio against the hosted Example Store and Example Outfitters (store.examples.datalayer.run, outfitters.examples.datalayer.run) from this example yet.',
4455
+ "A model's turns in the Studio's Preview were not watched in that drill (no dev server ran).",
4454
4456
  ],
4455
4457
  },
4456
4458
  },
@@ -4480,8 +4482,8 @@ export const SHOPPING_DESK_APP_0_0_1 = {
4480
4482
  documents: [],
4481
4483
  alternatives: [],
4482
4484
  },
4483
- enabled: false,
4484
- unavailable_because: 'It buys at shops for you over UCP and ACP, and the services that reach a shop for you are not shipped yet.',
4485
+ enabled: true,
4486
+ unavailable_because: '',
4485
4487
  tags: ['example', 'companies'],
4486
4488
  icon: 'apps',
4487
4489
  emoji: '🛍️',
@@ -5281,12 +5283,15 @@ export const TRAVEL_DESK_APP_0_0_1 = {
5281
5283
  },
5282
5284
  ],
5283
5285
  verified: {
5284
- live: [],
5286
+ live: [
5287
+ "Drilled on r1 on 13 October 2026 against the hosted Example Air (air.examples.datalayer.run), as its Preview's runtime reaches the gateway's pact toolset: its card read, asked as the application alone, the person's sign-in by device code approved at the Brand, then asked for the person and answered from the booking (bookings:read used, its receipt verified).",
5288
+ ],
5285
5289
  recorded: [
5286
5290
  "The journey is the developers' PACT example (examples/protocols/pact/personal_agent.py) against its local Provider, Example Air, run in its own tests (version 1.3.112).",
5287
5291
  ],
5288
5292
  unverified: [
5289
- 'It has not run in the Studio against the hosted Example Air (air.examples.datalayer.run) from this example yet; the hosted Example Air keeps registrations in memory, so an application is registered there again after each restart.',
5293
+ 'The hosted Example Air keeps registrations in memory: an application is registered there (POST /example/register, its issuer and keys) again after each restart.',
5294
+ "A model's turns in the Studio's Preview were not watched in that drill (no dev server ran); the step-up to bookings:change was not drilled live.",
5290
5295
  ],
5291
5296
  },
5292
5297
  },
@@ -5316,8 +5321,8 @@ export const TRAVEL_DESK_APP_0_0_1 = {
5316
5321
  documents: [],
5317
5322
  alternatives: [],
5318
5323
  },
5319
- enabled: false,
5320
- unavailable_because: 'It asks an airline for you over PACT, and the services that reach a business for you are not shipped yet.',
5324
+ enabled: true,
5325
+ unavailable_because: '',
5321
5326
  tags: ['example', 'companies'],
5322
5327
  icon: 'apps',
5323
5328
  emoji: '✈️',
@@ -8338,12 +8343,15 @@ export const APP_SOURCES = {
8338
8343
  },
8339
8344
  ],
8340
8345
  verified: {
8346
+ live: [
8347
+ "Drilled on r1 on 13 October 2026 against the hosted test merchants, as its Preview's runtime reaches the gateway's ucp and acp toolsets: at Example Store (store.examples.datalayer.run) a search, a checkout filled, the exact purchase asked of the person, approved on Waiting for you, bought once and refused a second time, its order read; at Example Outfitters (outfitters.examples.datalayer.run) the same checkout over ACP. Test merchants only: paid with a test instrument.",
8348
+ ],
8341
8349
  recorded: [
8342
8350
  "The purchase is the developers' UCP example (examples/protocols/ucp/shopping.py) against its local test store, Example Store, run in its own tests.",
8343
8351
  "The checkout at Example Outfitters is the developers' ACP example (examples/protocols/agentic_commerce/checkout.py), run in its own tests.",
8344
8352
  ],
8345
8353
  unverified: [
8346
- 'It has not run in the Studio against the hosted Example Store and Example Outfitters (store.examples.datalayer.run, outfitters.examples.datalayer.run) from this example yet.',
8354
+ "A model's turns in the Studio's Preview were not watched in that drill (no dev server ran).",
8347
8355
  ],
8348
8356
  },
8349
8357
  },
@@ -8354,8 +8362,6 @@ export const APP_SOURCES = {
8354
8362
  deployment: {
8355
8363
  hosted: {},
8356
8364
  },
8357
- enabled: false,
8358
- unavailable_because: 'It buys at shops for you over UCP and ACP, and the services that reach a shop for you are not shipped yet.',
8359
8365
  tags: ['example', 'companies'],
8360
8366
  emoji: '🛍️',
8361
8367
  },
@@ -8883,11 +8889,15 @@ export const APP_SOURCES = {
8883
8889
  },
8884
8890
  ],
8885
8891
  verified: {
8892
+ live: [
8893
+ "Drilled on r1 on 13 October 2026 against the hosted Example Air (air.examples.datalayer.run), as its Preview's runtime reaches the gateway's pact toolset: its card read, asked as the application alone, the person's sign-in by device code approved at the Brand, then asked for the person and answered from the booking (bookings:read used, its receipt verified).",
8894
+ ],
8886
8895
  recorded: [
8887
8896
  "The journey is the developers' PACT example (examples/protocols/pact/personal_agent.py) against its local Provider, Example Air, run in its own tests (version 1.3.112).",
8888
8897
  ],
8889
8898
  unverified: [
8890
- 'It has not run in the Studio against the hosted Example Air (air.examples.datalayer.run) from this example yet; the hosted Example Air keeps registrations in memory, so an application is registered there again after each restart.',
8899
+ 'The hosted Example Air keeps registrations in memory: an application is registered there (POST /example/register, its issuer and keys) again after each restart.',
8900
+ "A model's turns in the Studio's Preview were not watched in that drill (no dev server ran); the step-up to bookings:change was not drilled live.",
8891
8901
  ],
8892
8902
  },
8893
8903
  },
@@ -8898,8 +8908,6 @@ export const APP_SOURCES = {
8898
8908
  deployment: {
8899
8909
  hosted: {},
8900
8910
  },
8901
- enabled: false,
8902
- unavailable_because: 'It asks an airline for you over PACT, and the services that reach a business for you are not shipped yet.',
8903
8911
  tags: ['example', 'companies'],
8904
8912
  emoji: '✈️',
8905
8913
  },