@datalayer/agent-runtimes 1.5.8 → 1.5.10
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.
- package/dist-embed/datalayer-app-main.js +7 -7
- package/lib/apps/apps/appspec.d.ts +2 -2
- package/lib/apps/apps/appspec.js +87 -2
- package/lib/apps/embed/embeddedSecrets.d.ts +16 -0
- package/lib/apps/embed/embeddedSecrets.js +34 -0
- package/lib/protocols/mcp/client.d.ts +73 -0
- package/lib/protocols/mcp/client.js +136 -0
- package/lib/protocols/mcp/index.d.ts +1 -0
- package/lib/protocols/mcp/index.js +3 -1
- package/lib/protocols/web-bot-auth/tap.d.ts +56 -0
- package/lib/protocols/web-bot-auth/tap.js +185 -0
- package/lib/specs/actions.js +12 -0
- package/lib/specs/apps.d.ts +3 -0
- package/lib/specs/apps.js +673 -14
- package/lib/specs/appspecSchema.js +181 -4
- package/lib/specs/protocols.js +8 -7
- package/lib/types/agentspecs.d.ts +67 -3
- package/package.json +3 -1
- package/scripts/codegen/generate_apps.py +2 -2
- package/scripts/codegen/generate_protocols.py +1 -1
|
@@ -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/actions.js
CHANGED
|
@@ -704,6 +704,12 @@ export const PROTOCOL_TOOL_CLASSES = {
|
|
|
704
704
|
pap_disconnect: ['write'],
|
|
705
705
|
},
|
|
706
706
|
},
|
|
707
|
+
'signed-fetch': {
|
|
708
|
+
connection: 'signed-fetch',
|
|
709
|
+
tools: {
|
|
710
|
+
signed_fetch: ['read'],
|
|
711
|
+
},
|
|
712
|
+
},
|
|
707
713
|
ucp: {
|
|
708
714
|
connection: 'ucp',
|
|
709
715
|
tools: {
|
|
@@ -793,8 +799,10 @@ export const APP_BEHAVIOURS = {
|
|
|
793
799
|
'earthdata.search_earth_datagranules': 'do_it',
|
|
794
800
|
'earthdata.search_earth_datasets': 'do_it',
|
|
795
801
|
},
|
|
802
|
+
'errand-runner': {},
|
|
796
803
|
'event-response': {},
|
|
797
804
|
'exchange-desk': {},
|
|
805
|
+
'guest-desk': {},
|
|
798
806
|
'inbox-triage': {
|
|
799
807
|
'google-workspace.append_table_rows': 'leave_to_me',
|
|
800
808
|
'google-workspace.batch_modify_gmail_message_labels': 'do_it',
|
|
@@ -981,6 +989,7 @@ export const APP_BEHAVIOURS = {
|
|
|
981
989
|
'shop-concierge': {},
|
|
982
990
|
'shopping-desk': {},
|
|
983
991
|
'sign-in-desk': {},
|
|
992
|
+
'signed-reader': {},
|
|
984
993
|
'supplier-comparison': {},
|
|
985
994
|
'support-desk': {},
|
|
986
995
|
'travel-desk': {},
|
|
@@ -1005,8 +1014,10 @@ export const APP_ESCALATIONS = {
|
|
|
1005
1014
|
'data-quality': {},
|
|
1006
1015
|
decide: {},
|
|
1007
1016
|
'disaster-assessment': {},
|
|
1017
|
+
'errand-runner': {},
|
|
1008
1018
|
'event-response': {},
|
|
1009
1019
|
'exchange-desk': {},
|
|
1020
|
+
'guest-desk': {},
|
|
1010
1021
|
'inbox-triage': {
|
|
1011
1022
|
'google-workspace.batch_modify_gmail_message_labels': [
|
|
1012
1023
|
{
|
|
@@ -1055,6 +1066,7 @@ export const APP_ESCALATIONS = {
|
|
|
1055
1066
|
'shop-concierge': {},
|
|
1056
1067
|
'shopping-desk': {},
|
|
1057
1068
|
'sign-in-desk': {},
|
|
1069
|
+
'signed-reader': {},
|
|
1058
1070
|
'supplier-comparison': {},
|
|
1059
1071
|
'support-desk': {},
|
|
1060
1072
|
'travel-desk': {},
|
package/lib/specs/apps.d.ts
CHANGED
|
@@ -16,8 +16,10 @@ export declare const CUSTOMER_INTERVIEW_APP_0_0_1: AppSpec;
|
|
|
16
16
|
export declare const DATA_QUALITY_APP_0_0_1: AppSpec;
|
|
17
17
|
export declare const DECIDE_APP_0_0_1: AppSpec;
|
|
18
18
|
export declare const DISASTER_ASSESSMENT_APP_0_0_1: AppSpec;
|
|
19
|
+
export declare const ERRAND_RUNNER_APP_0_0_1: AppSpec;
|
|
19
20
|
export declare const EVENT_RESPONSE_APP_0_0_1: AppSpec;
|
|
20
21
|
export declare const EXCHANGE_DESK_APP_0_0_1: AppSpec;
|
|
22
|
+
export declare const GUEST_DESK_APP_0_0_1: AppSpec;
|
|
21
23
|
export declare const INBOX_TRIAGE_APP_0_0_1: AppSpec;
|
|
22
24
|
export declare const LIVE_SHOP_CHAT_APP_0_0_1: AppSpec;
|
|
23
25
|
export declare const MODEL_CHOICE_APP_0_0_1: AppSpec;
|
|
@@ -33,6 +35,7 @@ export declare const SHIP_OR_FIX_APP_0_0_1: AppSpec;
|
|
|
33
35
|
export declare const SHOP_CONCIERGE_APP_0_0_1: AppSpec;
|
|
34
36
|
export declare const SHOPPING_DESK_APP_0_0_1: AppSpec;
|
|
35
37
|
export declare const SIGN_IN_DESK_APP_0_0_1: AppSpec;
|
|
38
|
+
export declare const SIGNED_READER_APP_0_0_1: AppSpec;
|
|
36
39
|
export declare const SUPPLIER_COMPARISON_APP_0_0_1: AppSpec;
|
|
37
40
|
export declare const SUPPORT_DESK_APP_0_0_1: AppSpec;
|
|
38
41
|
export declare const TRAVEL_DESK_APP_0_0_1: AppSpec;
|