@webskill/sdk 0.28.1 → 0.30.0-next.1
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/agent.d.ts +2 -2
- package/dist/agent.js +32 -766
- package/dist/{approval-5BmspsRi.js → approval-DnUytrkq.js} +6 -1
- package/dist/browser.d.ts +9 -9
- package/dist/browser.js +131 -38
- package/dist/controller.d.ts +88 -0
- package/dist/controller.js +3 -0
- package/dist/device-DMUgqNzr.js +1782 -0
- package/dist/{dist-CTsxblSD.js → dist-Ww6TChcf.js} +2 -2
- package/dist/{geometry-BXMPNuM6.js → geometry-1xQbK4hB.js} +14 -78
- package/dist/governance.d.ts +3 -3
- package/dist/{index-DJeYqzlE.d.ts → index-1ASrUwzn.d.ts} +2 -2
- package/dist/{index-CkauSvus.d.ts → index-Cj2HedfL.d.ts} +4 -3
- package/dist/{index-DU8sKyQq.d.ts → index-UbNuzd2a.d.ts} +4 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +5 -6
- package/dist/{linkedDocument-0iiVPd5t.js → linkedDocument-GE-VV_Gj.js} +112 -2
- package/dist/mcp.d.ts +2 -2
- package/dist/node.d.ts +3 -3
- package/dist/node.js +2 -2
- package/dist/{openUiLibrary-BnhYUlqY.js → openUiLibrary-BjRDYyTe.js} +1 -1
- package/dist/{skillVersionStore-S4_HJ_Fl-DzZM_UIw.d.ts → skillVersionStore-S4_HJ_Fl-5K8jrAg_.d.ts} +1 -1
- package/dist/testing.d.ts +88 -2
- package/dist/testing.js +1287 -1
- package/dist/toolSource-DMCTnSVg.js +743 -0
- package/dist/types-BXRIoabj.js +76 -0
- package/dist/types-DmcXN3qR-Dy7ZDstc.d.ts +265 -0
- package/dist/{types-wKp3YBSM-167azx1H.d.ts → types-wKp3YBSM-CSeFUL9R.d.ts} +3 -2
- package/dist/ui-react.css +2687 -0
- package/dist/ui-react.d.ts +1045 -5
- package/dist/ui-react.js +5878 -820
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui.d.ts +3 -3
- package/dist/ui.js +1 -1
- package/dist/{webSkillApi-rX2e3SVB.js → webSkillApi-TeWPPFDn.js} +1 -1
- package/package.json +18 -4
- package/dist/kind-Dc8x0HWz.js +0 -110
|
@@ -0,0 +1,1782 @@
|
|
|
1
|
+
import { t as WebSkillError } from "./errors-BDZNpC13.js";
|
|
2
|
+
import { l as PERCEIVE_PAGE_TOOL, n as FOCUS_PAGE_TAB_TOOL, o as PAGE_ACTION_TOOL, r as LIST_PAGE_TABS_TOOL, t as CLOSE_PAGE_TAB_TOOL } from "./toolSource-DMCTnSVg.js";
|
|
3
|
+
import { Validator } from "@cfworker/json-schema";
|
|
4
|
+
|
|
5
|
+
//#region ../controller/src/constants.ts
|
|
6
|
+
/** @stable */
|
|
7
|
+
const CONTROLLER_PROTOCOL_VERSION = "0.1";
|
|
8
|
+
/** @experimental */
|
|
9
|
+
const CONTROLLER_TOOL_NAMES = [
|
|
10
|
+
"browser_status",
|
|
11
|
+
PERCEIVE_PAGE_TOOL,
|
|
12
|
+
PAGE_ACTION_TOOL,
|
|
13
|
+
LIST_PAGE_TABS_TOOL,
|
|
14
|
+
FOCUS_PAGE_TAB_TOOL,
|
|
15
|
+
CLOSE_PAGE_TAB_TOOL
|
|
16
|
+
];
|
|
17
|
+
/** @experimental */
|
|
18
|
+
const CONTROLLER_PROTOCOL_LIMITS = {
|
|
19
|
+
maxMessageBytes: 16 * 1024 * 1024,
|
|
20
|
+
oversizeWindowMs: 6e4,
|
|
21
|
+
oversizeThreshold: 3,
|
|
22
|
+
invocationContextMaxBytes: 4096,
|
|
23
|
+
pairingCodeTtlMaxMs: 6e5,
|
|
24
|
+
pairingAttemptsMax: 5,
|
|
25
|
+
deviceConfirmTimeoutMs: 12e4,
|
|
26
|
+
grantTtlMaxMs: 36e5,
|
|
27
|
+
heartbeatIntervalMs: 2e4,
|
|
28
|
+
heartbeatTimeoutMs: 45e3,
|
|
29
|
+
reconnectMaxDelayMs: 3e4,
|
|
30
|
+
localAuthorizationTimeoutMs: 12e4,
|
|
31
|
+
idempotencyCacheEntries: 256,
|
|
32
|
+
idempotencyCacheTtlMs: 6e5
|
|
33
|
+
};
|
|
34
|
+
const CONTROLLER_JSONRPC_CODES = {
|
|
35
|
+
CONTROLLER_MESSAGE_INVALID: -32602,
|
|
36
|
+
CONTROLLER_CAPABILITY_MISSING: -32602,
|
|
37
|
+
CONTROLLER_NOT_PAIRED: -32001,
|
|
38
|
+
CONTROLLER_PROTOCOL_UNSUPPORTED: -32002,
|
|
39
|
+
CONTROLLER_PAIRING_REJECTED: -32003,
|
|
40
|
+
CONTROLLER_PAIRING_RATE_LIMITED: -32004,
|
|
41
|
+
CONTROLLER_HUMAN_IN_CONTROL: -32e3,
|
|
42
|
+
CONTROLLER_INVOCATION_DUPLICATE: -32005
|
|
43
|
+
};
|
|
44
|
+
/** @experimental */
|
|
45
|
+
const CONTROLLER_ERROR_RETRYABLE = {
|
|
46
|
+
CONTROLLER_PROTOCOL_UNSUPPORTED: false,
|
|
47
|
+
CONTROLLER_MESSAGE_INVALID: false,
|
|
48
|
+
CONTROLLER_NOT_PAIRED: false,
|
|
49
|
+
CONTROLLER_PAIRING_REJECTED: false,
|
|
50
|
+
CONTROLLER_PAIRING_RATE_LIMITED: true,
|
|
51
|
+
CONTROLLER_CAPABILITY_MISSING: false,
|
|
52
|
+
CONTROLLER_HUMAN_IN_CONTROL: true,
|
|
53
|
+
CONTROLLER_INVOCATION_DUPLICATE: false
|
|
54
|
+
};
|
|
55
|
+
const CONTROLLER_WRITE_TOOLS = [
|
|
56
|
+
PAGE_ACTION_TOOL,
|
|
57
|
+
FOCUS_PAGE_TAB_TOOL,
|
|
58
|
+
CLOSE_PAGE_TAB_TOOL
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region ../controller/src/errors.ts
|
|
63
|
+
var ControllerRpcError = class extends Error {
|
|
64
|
+
code;
|
|
65
|
+
retryable;
|
|
66
|
+
constructor(code, message) {
|
|
67
|
+
super(message);
|
|
68
|
+
this.name = "ControllerRpcError";
|
|
69
|
+
this.code = code;
|
|
70
|
+
this.retryable = CONTROLLER_ERROR_RETRYABLE[code];
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
function controllerError(code, message) {
|
|
74
|
+
return new ControllerRpcError(code, message);
|
|
75
|
+
}
|
|
76
|
+
function asControllerRpcError(error) {
|
|
77
|
+
if (error instanceof ControllerRpcError) return error;
|
|
78
|
+
if (error instanceof WebSkillError && error.code.startsWith("CONTROLLER_")) return new ControllerRpcError(error.code, error.message);
|
|
79
|
+
return new ControllerRpcError("CONTROLLER_MESSAGE_INVALID", error instanceof Error ? error.message : String(error));
|
|
80
|
+
}
|
|
81
|
+
function toJsonRpcError(error) {
|
|
82
|
+
const normalized = asControllerRpcError(error);
|
|
83
|
+
return {
|
|
84
|
+
code: CONTROLLER_JSONRPC_CODES[normalized.code],
|
|
85
|
+
message: normalized.message,
|
|
86
|
+
data: {
|
|
87
|
+
code: normalized.code,
|
|
88
|
+
retryable: normalized.retryable
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region ../controller/src/jsonRpc.ts
|
|
95
|
+
function byteLength(value) {
|
|
96
|
+
return new TextEncoder().encode(value).byteLength;
|
|
97
|
+
}
|
|
98
|
+
function isJsonRpcObject(value) {
|
|
99
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && value.jsonrpc === "2.0";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region ../controller/schema/protocol.schema.json
|
|
104
|
+
var protocol_schema_default = {
|
|
105
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
106
|
+
$id: "https://webskill.dev/schema/controller/0.1/protocol.schema.json",
|
|
107
|
+
title: "Protocol",
|
|
108
|
+
type: "object",
|
|
109
|
+
$defs: {
|
|
110
|
+
"ProtocolVersion": {
|
|
111
|
+
"title": "ProtocolVersion",
|
|
112
|
+
"type": "string",
|
|
113
|
+
"pattern": "^[0-9]+\\.[0-9]+$",
|
|
114
|
+
"maxLength": 16
|
|
115
|
+
},
|
|
116
|
+
"IsoDateTime": {
|
|
117
|
+
"title": "IsoDateTime",
|
|
118
|
+
"type": "string",
|
|
119
|
+
"format": "date-time",
|
|
120
|
+
"maxLength": 40
|
|
121
|
+
},
|
|
122
|
+
"Base64Url": {
|
|
123
|
+
"title": "Base64Url",
|
|
124
|
+
"type": "string",
|
|
125
|
+
"pattern": "^[A-Za-z0-9_-]*$",
|
|
126
|
+
"maxLength": 8192
|
|
127
|
+
},
|
|
128
|
+
"Origin": {
|
|
129
|
+
"title": "Origin",
|
|
130
|
+
"type": "string",
|
|
131
|
+
"pattern": "^https?://[^/]+$",
|
|
132
|
+
"maxLength": 2048
|
|
133
|
+
},
|
|
134
|
+
"BoundedString": {
|
|
135
|
+
"title": "BoundedString",
|
|
136
|
+
"type": "string",
|
|
137
|
+
"maxLength": 4096
|
|
138
|
+
},
|
|
139
|
+
"InvocationMeta": {
|
|
140
|
+
"title": "InvocationMeta",
|
|
141
|
+
"type": "object",
|
|
142
|
+
"required": ["webskill/invocationId"],
|
|
143
|
+
"properties": {
|
|
144
|
+
"webskill/invocationId": {
|
|
145
|
+
"type": "string",
|
|
146
|
+
"pattern": "^[A-Za-z0-9._:-]{1,128}$"
|
|
147
|
+
},
|
|
148
|
+
"webskill/deviceEpoch": { "$ref": "#/$defs/BoundedString" },
|
|
149
|
+
"webskill/sequence": {
|
|
150
|
+
"type": "integer",
|
|
151
|
+
"minimum": 1
|
|
152
|
+
},
|
|
153
|
+
"webskill/context": {
|
|
154
|
+
"type": "object",
|
|
155
|
+
"maxProperties": 128
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
"additionalProperties": true
|
|
159
|
+
},
|
|
160
|
+
"Principal": {
|
|
161
|
+
"title": "Principal",
|
|
162
|
+
"type": "object",
|
|
163
|
+
"additionalProperties": false,
|
|
164
|
+
"required": ["displayName"],
|
|
165
|
+
"properties": { "displayName": { "$ref": "#/$defs/BoundedString" } }
|
|
166
|
+
},
|
|
167
|
+
"HelloParams": {
|
|
168
|
+
"title": "HelloParams",
|
|
169
|
+
"type": "object",
|
|
170
|
+
"additionalProperties": false,
|
|
171
|
+
"required": [
|
|
172
|
+
"protocolVersions",
|
|
173
|
+
"hostName",
|
|
174
|
+
"hostVersion"
|
|
175
|
+
],
|
|
176
|
+
"properties": {
|
|
177
|
+
"protocolVersions": {
|
|
178
|
+
"type": "array",
|
|
179
|
+
"minItems": 1,
|
|
180
|
+
"maxItems": 8,
|
|
181
|
+
"items": { "$ref": "#/$defs/ProtocolVersion" }
|
|
182
|
+
},
|
|
183
|
+
"hostName": { "$ref": "#/$defs/BoundedString" },
|
|
184
|
+
"hostVersion": { "$ref": "#/$defs/BoundedString" },
|
|
185
|
+
"deviceFingerprint": { "$ref": "#/$defs/BoundedString" }
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
"HelloResult": {
|
|
189
|
+
"title": "HelloResult",
|
|
190
|
+
"type": "object",
|
|
191
|
+
"additionalProperties": false,
|
|
192
|
+
"required": [
|
|
193
|
+
"protocolVersion",
|
|
194
|
+
"controllerInstanceId",
|
|
195
|
+
"controllerName",
|
|
196
|
+
"excludedOrigins"
|
|
197
|
+
],
|
|
198
|
+
"properties": {
|
|
199
|
+
"protocolVersion": { "$ref": "#/$defs/ProtocolVersion" },
|
|
200
|
+
"controllerInstanceId": { "$ref": "#/$defs/BoundedString" },
|
|
201
|
+
"controllerName": { "$ref": "#/$defs/BoundedString" },
|
|
202
|
+
"excludedOrigins": {
|
|
203
|
+
"type": "array",
|
|
204
|
+
"maxItems": 128,
|
|
205
|
+
"items": { "$ref": "#/$defs/Origin" }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
"PairStartParams": {
|
|
210
|
+
"title": "PairStartParams",
|
|
211
|
+
"type": "object",
|
|
212
|
+
"additionalProperties": false,
|
|
213
|
+
"required": ["publicKey", "deviceName"],
|
|
214
|
+
"properties": {
|
|
215
|
+
"publicKey": { "$ref": "#/$defs/Base64Url" },
|
|
216
|
+
"deviceName": { "$ref": "#/$defs/BoundedString" }
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
"PairStartResult": {
|
|
220
|
+
"title": "PairStartResult",
|
|
221
|
+
"type": "object",
|
|
222
|
+
"additionalProperties": false,
|
|
223
|
+
"required": [
|
|
224
|
+
"sessionId",
|
|
225
|
+
"code",
|
|
226
|
+
"expiresAt"
|
|
227
|
+
],
|
|
228
|
+
"properties": {
|
|
229
|
+
"sessionId": { "$ref": "#/$defs/BoundedString" },
|
|
230
|
+
"code": {
|
|
231
|
+
"type": "string",
|
|
232
|
+
"pattern": "^[A-Z0-9-]+$",
|
|
233
|
+
"maxLength": 32
|
|
234
|
+
},
|
|
235
|
+
"expiresAt": { "$ref": "#/$defs/IsoDateTime" }
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
"PairApprovedParams": {
|
|
239
|
+
"title": "PairApprovedParams",
|
|
240
|
+
"type": "object",
|
|
241
|
+
"additionalProperties": false,
|
|
242
|
+
"required": [
|
|
243
|
+
"sessionId",
|
|
244
|
+
"approvalNonce",
|
|
245
|
+
"controllerInstanceId",
|
|
246
|
+
"principal",
|
|
247
|
+
"capabilities",
|
|
248
|
+
"excludedOrigins"
|
|
249
|
+
],
|
|
250
|
+
"properties": {
|
|
251
|
+
"sessionId": { "$ref": "#/$defs/BoundedString" },
|
|
252
|
+
"approvalNonce": { "$ref": "#/$defs/Base64Url" },
|
|
253
|
+
"controllerInstanceId": { "$ref": "#/$defs/BoundedString" },
|
|
254
|
+
"principal": { "$ref": "#/$defs/Principal" },
|
|
255
|
+
"capabilities": {
|
|
256
|
+
"type": "array",
|
|
257
|
+
"maxItems": 128,
|
|
258
|
+
"items": { "$ref": "#/$defs/BoundedString" }
|
|
259
|
+
},
|
|
260
|
+
"excludedOrigins": {
|
|
261
|
+
"type": "array",
|
|
262
|
+
"maxItems": 128,
|
|
263
|
+
"items": { "$ref": "#/$defs/Origin" }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
"PairRejectedParams": {
|
|
268
|
+
"title": "PairRejectedParams",
|
|
269
|
+
"type": "object",
|
|
270
|
+
"additionalProperties": false,
|
|
271
|
+
"required": ["sessionId", "code"],
|
|
272
|
+
"properties": {
|
|
273
|
+
"sessionId": { "$ref": "#/$defs/BoundedString" },
|
|
274
|
+
"code": { "$ref": "#/$defs/BoundedString" }
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
"PairConfirmParams": {
|
|
278
|
+
"title": "PairConfirmParams",
|
|
279
|
+
"type": "object",
|
|
280
|
+
"additionalProperties": false,
|
|
281
|
+
"required": ["sessionId", "accepted"],
|
|
282
|
+
"properties": {
|
|
283
|
+
"sessionId": { "$ref": "#/$defs/BoundedString" },
|
|
284
|
+
"accepted": { "type": "boolean" },
|
|
285
|
+
"signature": { "$ref": "#/$defs/Base64Url" }
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
"PairConfirmResult": {
|
|
289
|
+
"title": "PairConfirmResult",
|
|
290
|
+
"type": "object",
|
|
291
|
+
"additionalProperties": false,
|
|
292
|
+
"properties": {
|
|
293
|
+
"grant": { "$ref": "#/$defs/BoundedString" },
|
|
294
|
+
"expiresAt": { "$ref": "#/$defs/IsoDateTime" }
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
"AuthPresentParams": {
|
|
298
|
+
"title": "AuthPresentParams",
|
|
299
|
+
"type": "object",
|
|
300
|
+
"additionalProperties": false,
|
|
301
|
+
"required": ["grant"],
|
|
302
|
+
"properties": { "grant": { "$ref": "#/$defs/BoundedString" } }
|
|
303
|
+
},
|
|
304
|
+
"AuthPresentResult": {
|
|
305
|
+
"title": "AuthPresentResult",
|
|
306
|
+
"type": "object",
|
|
307
|
+
"additionalProperties": false,
|
|
308
|
+
"required": ["expiresAt"],
|
|
309
|
+
"properties": { "expiresAt": { "$ref": "#/$defs/IsoDateTime" } }
|
|
310
|
+
},
|
|
311
|
+
"AuthChallengeParams": {
|
|
312
|
+
"title": "AuthChallengeParams",
|
|
313
|
+
"type": "object",
|
|
314
|
+
"additionalProperties": false,
|
|
315
|
+
"properties": {}
|
|
316
|
+
},
|
|
317
|
+
"AuthChallengeResult": {
|
|
318
|
+
"title": "AuthChallengeResult",
|
|
319
|
+
"type": "object",
|
|
320
|
+
"additionalProperties": false,
|
|
321
|
+
"required": ["nonce", "expiresAt"],
|
|
322
|
+
"properties": {
|
|
323
|
+
"nonce": { "$ref": "#/$defs/Base64Url" },
|
|
324
|
+
"expiresAt": { "$ref": "#/$defs/IsoDateTime" }
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
"AuthRenewParams": {
|
|
328
|
+
"title": "AuthRenewParams",
|
|
329
|
+
"type": "object",
|
|
330
|
+
"additionalProperties": false,
|
|
331
|
+
"required": ["nonce", "signature"],
|
|
332
|
+
"properties": {
|
|
333
|
+
"nonce": { "$ref": "#/$defs/Base64Url" },
|
|
334
|
+
"signature": { "$ref": "#/$defs/Base64Url" }
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
"AuthRenewResult": {
|
|
338
|
+
"title": "AuthRenewResult",
|
|
339
|
+
"type": "object",
|
|
340
|
+
"additionalProperties": false,
|
|
341
|
+
"required": ["grant", "expiresAt"],
|
|
342
|
+
"properties": {
|
|
343
|
+
"grant": { "$ref": "#/$defs/BoundedString" },
|
|
344
|
+
"expiresAt": { "$ref": "#/$defs/IsoDateTime" }
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
"AuthRevokedParams": {
|
|
348
|
+
"title": "AuthRevokedParams",
|
|
349
|
+
"type": "object",
|
|
350
|
+
"additionalProperties": false,
|
|
351
|
+
"required": ["reason"],
|
|
352
|
+
"properties": { "reason": { "$ref": "#/$defs/BoundedString" } }
|
|
353
|
+
},
|
|
354
|
+
"OriginsParams": {
|
|
355
|
+
"title": "OriginsParams",
|
|
356
|
+
"type": "object",
|
|
357
|
+
"additionalProperties": false,
|
|
358
|
+
"required": ["excludedOrigins"],
|
|
359
|
+
"properties": { "excludedOrigins": {
|
|
360
|
+
"type": "array",
|
|
361
|
+
"maxItems": 128,
|
|
362
|
+
"items": { "$ref": "#/$defs/Origin" }
|
|
363
|
+
} }
|
|
364
|
+
},
|
|
365
|
+
"HumanControlParams": {
|
|
366
|
+
"title": "HumanControlParams",
|
|
367
|
+
"type": "object",
|
|
368
|
+
"additionalProperties": false,
|
|
369
|
+
"required": ["inControl", "at"],
|
|
370
|
+
"properties": {
|
|
371
|
+
"inControl": { "type": "boolean" },
|
|
372
|
+
"at": { "$ref": "#/$defs/IsoDateTime" }
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
"ReadyParams": {
|
|
376
|
+
"title": "ReadyParams",
|
|
377
|
+
"type": "object",
|
|
378
|
+
"additionalProperties": false,
|
|
379
|
+
"required": [
|
|
380
|
+
"deviceEpoch",
|
|
381
|
+
"writeHighWatermark",
|
|
382
|
+
"hostVersion",
|
|
383
|
+
"writeStateRecoverable"
|
|
384
|
+
],
|
|
385
|
+
"properties": {
|
|
386
|
+
"deviceEpoch": { "$ref": "#/$defs/BoundedString" },
|
|
387
|
+
"writeHighWatermark": {
|
|
388
|
+
"type": "integer",
|
|
389
|
+
"minimum": 0
|
|
390
|
+
},
|
|
391
|
+
"hostVersion": { "$ref": "#/$defs/BoundedString" },
|
|
392
|
+
"writeStateRecoverable": { "type": "boolean" }
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
"PingParams": {
|
|
396
|
+
"title": "PingParams",
|
|
397
|
+
"type": "object",
|
|
398
|
+
"additionalProperties": false,
|
|
399
|
+
"properties": {}
|
|
400
|
+
},
|
|
401
|
+
"PingResult": {
|
|
402
|
+
"title": "PingResult",
|
|
403
|
+
"type": "object",
|
|
404
|
+
"additionalProperties": false,
|
|
405
|
+
"properties": {}
|
|
406
|
+
},
|
|
407
|
+
"ByeParams": {
|
|
408
|
+
"title": "ByeParams",
|
|
409
|
+
"type": "object",
|
|
410
|
+
"additionalProperties": false,
|
|
411
|
+
"required": ["code", "message"],
|
|
412
|
+
"properties": {
|
|
413
|
+
"code": { "$ref": "#/$defs/BoundedString" },
|
|
414
|
+
"message": { "$ref": "#/$defs/BoundedString" }
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
//#endregion
|
|
421
|
+
//#region ../controller/src/validate.ts
|
|
422
|
+
/** @experimental */
|
|
423
|
+
const CONTROLLER_PROTOCOL_SCHEMA = protocol_schema_default;
|
|
424
|
+
const METHOD_DEFINITIONS = {
|
|
425
|
+
"webskill/hello": "Hello",
|
|
426
|
+
"webskill/pair.start": "PairStart",
|
|
427
|
+
"webskill/pair.approved": "PairApproved",
|
|
428
|
+
"webskill/pair.rejected": "PairRejected",
|
|
429
|
+
"webskill/pair.confirm": "PairConfirm",
|
|
430
|
+
"webskill/auth.present": "AuthPresent",
|
|
431
|
+
"webskill/auth.challenge": "AuthChallenge",
|
|
432
|
+
"webskill/auth.renew": "AuthRenew",
|
|
433
|
+
"webskill/auth.revoked": "AuthRevoked",
|
|
434
|
+
"webskill/origins": "Origins",
|
|
435
|
+
"webskill/control.human": "HumanControl",
|
|
436
|
+
"webskill/ready": "Ready",
|
|
437
|
+
"webskill/ping": "Ping",
|
|
438
|
+
"webskill/bye": "Bye"
|
|
439
|
+
};
|
|
440
|
+
/** @experimental */
|
|
441
|
+
function validateControllerMessage(method, kind, value) {
|
|
442
|
+
const definition = METHOD_DEFINITIONS[method];
|
|
443
|
+
if (!definition) return {
|
|
444
|
+
valid: false,
|
|
445
|
+
errors: [`Unknown controller method: ${method}`]
|
|
446
|
+
};
|
|
447
|
+
const def = protocol_schema_default.$defs[`${definition}${kind === "params" ? "Params" : "Result"}`];
|
|
448
|
+
if (!def) return {
|
|
449
|
+
valid: false,
|
|
450
|
+
errors: [`Missing schema for ${method} ${kind}`]
|
|
451
|
+
};
|
|
452
|
+
const result = new Validator({
|
|
453
|
+
...def,
|
|
454
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
455
|
+
$defs: protocol_schema_default.$defs
|
|
456
|
+
}, "2020-12", false).validate(value);
|
|
457
|
+
return {
|
|
458
|
+
valid: result.valid,
|
|
459
|
+
errors: result.errors.map((error) => `${error.instanceLocation || "/"}: ${error.error}`)
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
//#endregion
|
|
464
|
+
//#region ../controller/src/transport.ts
|
|
465
|
+
var SocketChannel = class {
|
|
466
|
+
options;
|
|
467
|
+
socket;
|
|
468
|
+
pending = /* @__PURE__ */ new Map();
|
|
469
|
+
sequence = 0;
|
|
470
|
+
opened = false;
|
|
471
|
+
closed = false;
|
|
472
|
+
connectPromise;
|
|
473
|
+
oversizeTimes = [];
|
|
474
|
+
maxMessageBytes;
|
|
475
|
+
defaultTimeoutMs;
|
|
476
|
+
mcpCallbacks = {};
|
|
477
|
+
constructor(options) {
|
|
478
|
+
this.options = options;
|
|
479
|
+
this.maxMessageBytes = options.maxMessageBytes ?? CONTROLLER_PROTOCOL_LIMITS.maxMessageBytes;
|
|
480
|
+
this.defaultTimeoutMs = options.requestTimeoutMs ?? CONTROLLER_PROTOCOL_LIMITS.heartbeatTimeoutMs;
|
|
481
|
+
}
|
|
482
|
+
async connect() {
|
|
483
|
+
if (this.opened && !this.closed) return;
|
|
484
|
+
if (this.connectPromise) return await this.connectPromise;
|
|
485
|
+
this.connectPromise = this.openSocket();
|
|
486
|
+
try {
|
|
487
|
+
await this.connectPromise;
|
|
488
|
+
} finally {
|
|
489
|
+
this.connectPromise = void 0;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async openSocket() {
|
|
493
|
+
const socket = this.options.socket ?? this.createSocket();
|
|
494
|
+
this.socket = socket;
|
|
495
|
+
if (this.options.socket) {
|
|
496
|
+
this.opened = true;
|
|
497
|
+
this.attach(socket);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
await new Promise((resolve, reject) => {
|
|
501
|
+
socket.onopen = () => {
|
|
502
|
+
this.opened = true;
|
|
503
|
+
resolve();
|
|
504
|
+
};
|
|
505
|
+
socket.onerror = (event) => {
|
|
506
|
+
const error = /* @__PURE__ */ new Error(`Controller WebSocket failed: ${String(event)}`);
|
|
507
|
+
this.options.onerror?.(error);
|
|
508
|
+
if (!this.opened) reject(error);
|
|
509
|
+
};
|
|
510
|
+
this.attach(socket);
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
createSocket() {
|
|
514
|
+
const Ctor = this.options.WebSocket ?? globalThis.WebSocket;
|
|
515
|
+
if (!Ctor) throw new Error("WebSocket is not available in this environment");
|
|
516
|
+
return new Ctor(this.options.url);
|
|
517
|
+
}
|
|
518
|
+
emitError(error) {
|
|
519
|
+
this.options.onerror?.(error);
|
|
520
|
+
this.mcpCallbacks.onerror?.(error);
|
|
521
|
+
}
|
|
522
|
+
attach(socket) {
|
|
523
|
+
socket.onclose = () => {
|
|
524
|
+
if (!this.opened) return;
|
|
525
|
+
this.handleClose();
|
|
526
|
+
};
|
|
527
|
+
socket.onmessage = (event) => {
|
|
528
|
+
this.handleMessage(event.data).catch((error) => this.emitError(error instanceof Error ? error : new Error(String(error))));
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
sendRaw(value) {
|
|
532
|
+
if (!this.socket || !this.opened || this.closed) throw new Error("Controller WebSocket is not connected");
|
|
533
|
+
const text = JSON.stringify(value);
|
|
534
|
+
if (byteLength(text) > this.maxMessageBytes) throw controllerError("CONTROLLER_MESSAGE_INVALID", "Controller message exceeds the protocol size limit");
|
|
535
|
+
this.socket.send(text);
|
|
536
|
+
}
|
|
537
|
+
validate(method, kind, value) {
|
|
538
|
+
const result = validateControllerMessage(method, kind, value);
|
|
539
|
+
if (!result.valid) throw controllerError("CONTROLLER_MESSAGE_INVALID", `Invalid ${method} ${kind}: ${result.errors.join("; ")}`);
|
|
540
|
+
}
|
|
541
|
+
async request(method, params, timeoutMs) {
|
|
542
|
+
await this.connect();
|
|
543
|
+
this.validate(method, "params", params);
|
|
544
|
+
const id = `ws-${++this.sequence}`;
|
|
545
|
+
const promise = new Promise((resolve, reject) => {
|
|
546
|
+
const timeout = setTimeout(() => {
|
|
547
|
+
this.pending.delete(id);
|
|
548
|
+
reject(/* @__PURE__ */ new Error(`Controller request timed out: ${method}`));
|
|
549
|
+
}, timeoutMs ?? this.defaultTimeoutMs);
|
|
550
|
+
this.pending.set(id, {
|
|
551
|
+
method,
|
|
552
|
+
resolve,
|
|
553
|
+
reject,
|
|
554
|
+
timeout
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
try {
|
|
558
|
+
this.sendRaw({
|
|
559
|
+
jsonrpc: "2.0",
|
|
560
|
+
id,
|
|
561
|
+
method,
|
|
562
|
+
params
|
|
563
|
+
});
|
|
564
|
+
} catch (error) {
|
|
565
|
+
const pending = this.pending.get(id);
|
|
566
|
+
if (pending?.timeout) clearTimeout(pending.timeout);
|
|
567
|
+
this.pending.delete(id);
|
|
568
|
+
throw error;
|
|
569
|
+
}
|
|
570
|
+
return await promise;
|
|
571
|
+
}
|
|
572
|
+
async notify(method, params) {
|
|
573
|
+
await this.connect();
|
|
574
|
+
this.validate(method, "params", params);
|
|
575
|
+
this.sendRaw({
|
|
576
|
+
jsonrpc: "2.0",
|
|
577
|
+
method,
|
|
578
|
+
params
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
registerOversize(message) {
|
|
582
|
+
const now = Date.now();
|
|
583
|
+
const windowMs = CONTROLLER_PROTOCOL_LIMITS.oversizeWindowMs;
|
|
584
|
+
this.oversizeTimes = this.oversizeTimes.filter((time) => now - time <= windowMs);
|
|
585
|
+
this.oversizeTimes.push(now);
|
|
586
|
+
if (message?.id !== void 0) this.sendRaw({
|
|
587
|
+
jsonrpc: "2.0",
|
|
588
|
+
id: message.id,
|
|
589
|
+
error: toJsonRpcError(controllerError("CONTROLLER_MESSAGE_INVALID", "Controller message exceeds the protocol size limit"))
|
|
590
|
+
});
|
|
591
|
+
if (this.oversizeTimes.length >= CONTROLLER_PROTOCOL_LIMITS.oversizeThreshold) this.socket?.close(1009, "Controller message size limit exceeded repeatedly");
|
|
592
|
+
}
|
|
593
|
+
async handleMessage(raw) {
|
|
594
|
+
const text = typeof raw === "string" ? raw : String(raw);
|
|
595
|
+
if (byteLength(text) > this.maxMessageBytes) {
|
|
596
|
+
let parsed;
|
|
597
|
+
try {
|
|
598
|
+
const candidate = JSON.parse(text);
|
|
599
|
+
if (isJsonRpcObject(candidate)) parsed = candidate;
|
|
600
|
+
} catch {
|
|
601
|
+
parsed = void 0;
|
|
602
|
+
}
|
|
603
|
+
this.registerOversize(parsed);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
let message;
|
|
607
|
+
try {
|
|
608
|
+
message = JSON.parse(text);
|
|
609
|
+
} catch {
|
|
610
|
+
this.options.onerror?.(/* @__PURE__ */ new Error("Invalid JSON received on the controller channel"));
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (!isJsonRpcObject(message)) {
|
|
614
|
+
this.options.onerror?.(/* @__PURE__ */ new Error("Invalid JSON-RPC object received on the controller channel"));
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (message.id !== void 0 && message.method === void 0) {
|
|
618
|
+
const pending = this.pending.get(message.id);
|
|
619
|
+
if (!pending) {
|
|
620
|
+
this.options.onmessage?.(message);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
this.pending.delete(message.id);
|
|
624
|
+
if (pending.timeout) clearTimeout(pending.timeout);
|
|
625
|
+
if (message.error) {
|
|
626
|
+
const code = message.error.data?.code ?? "CONTROLLER_MESSAGE_INVALID";
|
|
627
|
+
pending.reject(new ControllerRpcError(code, message.error.message));
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
this.validate(pending.method, "result", message.result);
|
|
632
|
+
pending.resolve(message.result);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
pending.reject(error instanceof Error ? error : new Error(String(error)));
|
|
635
|
+
}
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (typeof message.method !== "string") return;
|
|
639
|
+
if (!message.method.startsWith("webskill/")) {
|
|
640
|
+
if (this.options.isAuthorized?.() !== true) {
|
|
641
|
+
if (message.id !== void 0) this.sendRaw({
|
|
642
|
+
jsonrpc: "2.0",
|
|
643
|
+
id: message.id,
|
|
644
|
+
error: toJsonRpcError(controllerError("CONTROLLER_NOT_PAIRED", "The controller device is not paired"))
|
|
645
|
+
});
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
this.options.onmessage?.(message);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
if (message.id === void 0) {
|
|
652
|
+
try {
|
|
653
|
+
this.validate(message.method, "params", message.params);
|
|
654
|
+
await this.options.onControlNotification?.(message.method, message.params);
|
|
655
|
+
} catch (error) {
|
|
656
|
+
this.options.onerror?.(error instanceof Error ? error : new Error(String(error)));
|
|
657
|
+
}
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
try {
|
|
661
|
+
this.validate(message.method, "params", message.params);
|
|
662
|
+
const result = await this.options.onControlRequest?.(message.method, message.params);
|
|
663
|
+
this.validate(message.method, "result", result);
|
|
664
|
+
this.sendRaw({
|
|
665
|
+
jsonrpc: "2.0",
|
|
666
|
+
id: message.id,
|
|
667
|
+
result
|
|
668
|
+
});
|
|
669
|
+
} catch (error) {
|
|
670
|
+
this.sendRaw({
|
|
671
|
+
jsonrpc: "2.0",
|
|
672
|
+
id: message.id,
|
|
673
|
+
error: toJsonRpcError(error)
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
handleClose() {
|
|
678
|
+
if (this.closed) return;
|
|
679
|
+
this.closed = true;
|
|
680
|
+
this.opened = false;
|
|
681
|
+
const error = /* @__PURE__ */ new Error("Controller WebSocket closed");
|
|
682
|
+
for (const pending of this.pending.values()) {
|
|
683
|
+
if (pending.timeout) clearTimeout(pending.timeout);
|
|
684
|
+
pending.reject(error);
|
|
685
|
+
}
|
|
686
|
+
this.pending.clear();
|
|
687
|
+
this.options.onclose?.();
|
|
688
|
+
this.mcpCallbacks.onclose?.();
|
|
689
|
+
}
|
|
690
|
+
async close(code = 1e3, reason = "Controller channel closed") {
|
|
691
|
+
if (!this.socket || this.closed) return;
|
|
692
|
+
this.socket.close(code, reason);
|
|
693
|
+
this.handleClose();
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
/** @experimental */
|
|
697
|
+
function createWebSocketControllerTransport(options) {
|
|
698
|
+
const channel = new SocketChannel(options);
|
|
699
|
+
const transport = {
|
|
700
|
+
get connected() {
|
|
701
|
+
return channel.opened && !channel.closed;
|
|
702
|
+
},
|
|
703
|
+
onControlRequest: void 0,
|
|
704
|
+
onControlNotification: void 0,
|
|
705
|
+
onmessage: void 0,
|
|
706
|
+
onerror: void 0,
|
|
707
|
+
onclose: void 0,
|
|
708
|
+
connect: () => channel.connect(),
|
|
709
|
+
start: () => channel.connect(),
|
|
710
|
+
request: (method, params, timeoutMs) => channel.request(method, params, timeoutMs),
|
|
711
|
+
notify: (method, params) => channel.notify(method, params),
|
|
712
|
+
send: async (message) => channel.sendRaw(message),
|
|
713
|
+
close: (code, reason) => channel.close(code, reason)
|
|
714
|
+
};
|
|
715
|
+
for (const key of [
|
|
716
|
+
"onControlRequest",
|
|
717
|
+
"onControlNotification",
|
|
718
|
+
"onmessage",
|
|
719
|
+
"onerror",
|
|
720
|
+
"onclose"
|
|
721
|
+
]) {
|
|
722
|
+
const isMcpLifecycle = key === "onerror" || key === "onclose";
|
|
723
|
+
Object.defineProperty(transport, key, {
|
|
724
|
+
get: () => isMcpLifecycle ? channel.mcpCallbacks[key] : options[key],
|
|
725
|
+
set: (value) => {
|
|
726
|
+
if (isMcpLifecycle) channel.mcpCallbacks[key] = value;
|
|
727
|
+
else options[key] = value;
|
|
728
|
+
},
|
|
729
|
+
enumerable: true,
|
|
730
|
+
configurable: true
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
return transport;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
//#endregion
|
|
737
|
+
//#region ../controller/src/device/crypto.ts
|
|
738
|
+
function bytesToBase64Url(bytes) {
|
|
739
|
+
let binary = "";
|
|
740
|
+
const chunk = 32768;
|
|
741
|
+
for (let index = 0; index < bytes.length; index += chunk) binary += String.fromCharCode(...bytes.subarray(index, index + chunk));
|
|
742
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
743
|
+
}
|
|
744
|
+
function base64UrlToBytes(value) {
|
|
745
|
+
const base64 = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
746
|
+
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
747
|
+
const binary = atob(padded);
|
|
748
|
+
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
749
|
+
}
|
|
750
|
+
async function generateDeviceKeyPair() {
|
|
751
|
+
return await crypto.subtle.generateKey({
|
|
752
|
+
name: "ECDSA",
|
|
753
|
+
namedCurve: "P-256"
|
|
754
|
+
}, false, ["sign", "verify"]);
|
|
755
|
+
}
|
|
756
|
+
async function exportPublicKey(keyPair) {
|
|
757
|
+
return bytesToBase64Url(new Uint8Array(await crypto.subtle.exportKey("spki", keyPair.publicKey)));
|
|
758
|
+
}
|
|
759
|
+
async function signDeviceText(keyPair, value) {
|
|
760
|
+
const signature = await crypto.subtle.sign({
|
|
761
|
+
name: "ECDSA",
|
|
762
|
+
hash: "SHA-256"
|
|
763
|
+
}, keyPair.privateKey, new TextEncoder().encode(value));
|
|
764
|
+
return bytesToBase64Url(new Uint8Array(signature));
|
|
765
|
+
}
|
|
766
|
+
async function fingerprintOfBytes(bytes, bytesToShow = 10) {
|
|
767
|
+
const buffer = new ArrayBuffer(bytes.byteLength);
|
|
768
|
+
new Uint8Array(buffer).set(bytes);
|
|
769
|
+
return [...new Uint8Array(await crypto.subtle.digest("SHA-256", buffer)).subarray(0, bytesToShow)].map((byte) => byte.toString(16).padStart(2, "0")).join("").toUpperCase().match(/.{1,4}/g)?.join("-") ?? "";
|
|
770
|
+
}
|
|
771
|
+
async function deviceFingerprint(keyPair) {
|
|
772
|
+
return await fingerprintOfBytes(base64UrlToBytes(await exportPublicKey(keyPair)), 10);
|
|
773
|
+
}
|
|
774
|
+
/** @experimental */
|
|
775
|
+
async function instanceFingerprint(instanceId) {
|
|
776
|
+
return await fingerprintOfBytes(new TextEncoder().encode(instanceId), 8);
|
|
777
|
+
}
|
|
778
|
+
function stableJson(value) {
|
|
779
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
780
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
781
|
+
const object = value;
|
|
782
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableJson(object[key])}`).join(",")}}`;
|
|
783
|
+
}
|
|
784
|
+
async function argsDigest(args) {
|
|
785
|
+
return bytesToBase64Url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(stableJson(args)))));
|
|
786
|
+
}
|
|
787
|
+
function randomToken(bytes = 32) {
|
|
788
|
+
return bytesToBase64Url(crypto.getRandomValues(new Uint8Array(bytes)));
|
|
789
|
+
}
|
|
790
|
+
function originFromControllerUrl(raw) {
|
|
791
|
+
const url = new URL(raw);
|
|
792
|
+
if (url.protocol === "wss:") return `https://${url.host}`;
|
|
793
|
+
if (url.protocol === "ws:") return `http://${url.host}`;
|
|
794
|
+
throw new WebSkillError("VALIDATION_FAILED", "Controller URL must use ws:// or wss://");
|
|
795
|
+
}
|
|
796
|
+
function isLoopbackHost(hostname) {
|
|
797
|
+
const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
798
|
+
return host === "::1" || host === "localhost" || host.endsWith(".localhost") || /^127(?:\.[0-9]{1,3}){3}$/.test(host);
|
|
799
|
+
}
|
|
800
|
+
function assertControllerUrl(raw) {
|
|
801
|
+
let url;
|
|
802
|
+
try {
|
|
803
|
+
url = new URL(raw);
|
|
804
|
+
} catch {
|
|
805
|
+
throw new WebSkillError("VALIDATION_FAILED", "Controller URL must be an absolute WebSocket URL");
|
|
806
|
+
}
|
|
807
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new WebSkillError("VALIDATION_FAILED", "Controller URL must use ws:// or wss://");
|
|
808
|
+
if (url.protocol === "ws:" && !isLoopbackHost(url.hostname)) throw new WebSkillError("VALIDATION_FAILED", "Non-loopback controllers must use wss://");
|
|
809
|
+
return url;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
//#endregion
|
|
813
|
+
//#region ../controller/src/device/tools.ts
|
|
814
|
+
function isWriteTool(name) {
|
|
815
|
+
return CONTROLLER_WRITE_TOOLS.includes(name);
|
|
816
|
+
}
|
|
817
|
+
function browserStatusTool() {
|
|
818
|
+
return {
|
|
819
|
+
name: "browser_status",
|
|
820
|
+
description: "Read the extension version, protocol version, connection state, human-control state and excluded origins.",
|
|
821
|
+
inputSchema: {
|
|
822
|
+
type: "object",
|
|
823
|
+
properties: {},
|
|
824
|
+
additionalProperties: false
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
async function listControllerTools(sources) {
|
|
829
|
+
const tools = [browserStatusTool()];
|
|
830
|
+
for (const source of sources) {
|
|
831
|
+
const specs = await source.listToolSpecs();
|
|
832
|
+
for (const spec of specs) {
|
|
833
|
+
if (!CONTROLLER_TOOL_NAMES.includes(spec.name)) continue;
|
|
834
|
+
tools.push({
|
|
835
|
+
name: spec.name,
|
|
836
|
+
...spec.description === void 0 ? {} : { description: spec.description },
|
|
837
|
+
inputSchema: spec.inputSchema
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return tools;
|
|
842
|
+
}
|
|
843
|
+
async function controllerInstructions(sources) {
|
|
844
|
+
const parts = [];
|
|
845
|
+
for (const source of sources) {
|
|
846
|
+
if (!(await source.listToolSpecs()).some((spec) => CONTROLLER_TOOL_NAMES.includes(spec.name))) continue;
|
|
847
|
+
const prompt = await source.systemPrompt?.();
|
|
848
|
+
if (prompt) parts.push(prompt);
|
|
849
|
+
}
|
|
850
|
+
return parts.length > 0 ? parts.join("\n\n") : void 0;
|
|
851
|
+
}
|
|
852
|
+
async function callControllerTool(sources, name, args, invocationId) {
|
|
853
|
+
const source = sources.find((candidate) => candidate.canHandle(name));
|
|
854
|
+
if (!source) throw controllerError("CONTROLLER_CAPABILITY_MISSING", `Controller tool is unavailable: ${name}`);
|
|
855
|
+
return await source.call(name, args, {
|
|
856
|
+
runId: invocationId ?? `controller-${name}`,
|
|
857
|
+
...invocationId === void 0 ? {} : { invocationId }
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
function toolResultError(error) {
|
|
861
|
+
const value = error;
|
|
862
|
+
const code = value?.code ?? "TOOL_EXECUTION_FAILED";
|
|
863
|
+
const message = value?.message ?? String(error);
|
|
864
|
+
const retryable = value?.data?.retryable === true || value?.details?.retryable === true || code in CONTROLLER_ERROR_RETRYABLE && CONTROLLER_ERROR_RETRYABLE[code];
|
|
865
|
+
return {
|
|
866
|
+
isError: true,
|
|
867
|
+
content: [{
|
|
868
|
+
type: "text",
|
|
869
|
+
text: message
|
|
870
|
+
}],
|
|
871
|
+
_meta: { "webskill/error": {
|
|
872
|
+
code,
|
|
873
|
+
message,
|
|
874
|
+
retryable
|
|
875
|
+
} }
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
function mapToolResult(result) {
|
|
879
|
+
if (!result.ok) return toolResultError(result.error ?? {
|
|
880
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
881
|
+
message: "Controller tool failed"
|
|
882
|
+
});
|
|
883
|
+
const content = [];
|
|
884
|
+
let structuredContent;
|
|
885
|
+
for (const part of result.content) if (part.type === "text") content.push({
|
|
886
|
+
type: "text",
|
|
887
|
+
text: part.text ?? ""
|
|
888
|
+
});
|
|
889
|
+
else if (part.type === "json") {
|
|
890
|
+
const data = part.data ?? part.text;
|
|
891
|
+
content.push({
|
|
892
|
+
type: "text",
|
|
893
|
+
text: typeof data === "string" ? data : JSON.stringify(data)
|
|
894
|
+
});
|
|
895
|
+
if (typeof data === "object" && data !== null && !Array.isArray(data)) structuredContent = data;
|
|
896
|
+
} else if (part.type === "image") content.push({
|
|
897
|
+
type: "image",
|
|
898
|
+
mimeType: part.mimeType,
|
|
899
|
+
data: part.data
|
|
900
|
+
});
|
|
901
|
+
else return toolResultError(new ControllerRpcError("CONTROLLER_CAPABILITY_MISSING", `Controller result type is unsupported: ${part.type}`));
|
|
902
|
+
return {
|
|
903
|
+
content,
|
|
904
|
+
...structuredContent === void 0 ? {} : { structuredContent }
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
//#endregion
|
|
909
|
+
//#region ../controller/src/device/index.ts
|
|
910
|
+
const INVOCATION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
911
|
+
function errorCode(error) {
|
|
912
|
+
if (error instanceof WebSkillError) return error.code;
|
|
913
|
+
if (error instanceof ControllerRpcError) return error.code;
|
|
914
|
+
return "TOOL_EXECUTION_FAILED";
|
|
915
|
+
}
|
|
916
|
+
var DeviceSession = class {
|
|
917
|
+
options;
|
|
918
|
+
stateValue = "idle";
|
|
919
|
+
human = false;
|
|
920
|
+
transport;
|
|
921
|
+
pairing;
|
|
922
|
+
pendingPairing;
|
|
923
|
+
pendingApproval;
|
|
924
|
+
pairingCodeValue;
|
|
925
|
+
controllerInstance;
|
|
926
|
+
controllerName = "Controller";
|
|
927
|
+
excluded = /* @__PURE__ */ new Set();
|
|
928
|
+
keyPair;
|
|
929
|
+
connectPromise;
|
|
930
|
+
reconnectTimer;
|
|
931
|
+
heartbeatTimer;
|
|
932
|
+
renewTimer;
|
|
933
|
+
pairingTimer;
|
|
934
|
+
reconnectAttempt = 0;
|
|
935
|
+
intentionalClose = false;
|
|
936
|
+
mcpServer;
|
|
937
|
+
mcpStart;
|
|
938
|
+
writeQueue = Promise.resolve();
|
|
939
|
+
writeCache = /* @__PURE__ */ new Map();
|
|
940
|
+
restoredPairing = false;
|
|
941
|
+
writeState;
|
|
942
|
+
writeRecoverable = true;
|
|
943
|
+
lifecycleGeneration = 0;
|
|
944
|
+
lastToolNames = "";
|
|
945
|
+
constructor(options) {
|
|
946
|
+
this.options = options;
|
|
947
|
+
this.excluded.add(originFromControllerUrl(options.url));
|
|
948
|
+
for (const origin of options.excludedOrigins ?? []) this.excluded.add(origin);
|
|
949
|
+
}
|
|
950
|
+
get state() {
|
|
951
|
+
return this.stateValue;
|
|
952
|
+
}
|
|
953
|
+
get humanInControl() {
|
|
954
|
+
return this.human;
|
|
955
|
+
}
|
|
956
|
+
get writeStateRecoverable() {
|
|
957
|
+
return this.writeRecoverable;
|
|
958
|
+
}
|
|
959
|
+
get excludedOrigins() {
|
|
960
|
+
return [...this.excluded].sort();
|
|
961
|
+
}
|
|
962
|
+
get pairingCode() {
|
|
963
|
+
return this.pairingCodeValue;
|
|
964
|
+
}
|
|
965
|
+
get controllerInstanceId() {
|
|
966
|
+
return this.controllerInstance;
|
|
967
|
+
}
|
|
968
|
+
get controllerOrigin() {
|
|
969
|
+
return originFromControllerUrl(this.options.url);
|
|
970
|
+
}
|
|
971
|
+
now() {
|
|
972
|
+
return this.options.now?.() ?? Date.now();
|
|
973
|
+
}
|
|
974
|
+
setState(state) {
|
|
975
|
+
if (this.stateValue === state) return;
|
|
976
|
+
this.stateValue = state;
|
|
977
|
+
this.options.onStateChange?.(state);
|
|
978
|
+
}
|
|
979
|
+
reportError(error) {
|
|
980
|
+
const code = errorCode(error);
|
|
981
|
+
this.options.onError?.({
|
|
982
|
+
code,
|
|
983
|
+
message: error instanceof Error ? error.message : String(error)
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
audit(event) {
|
|
987
|
+
const audited = event.type === "controller.call" && typeof event.data["tool"] === "string" ? {
|
|
988
|
+
...event,
|
|
989
|
+
data: {
|
|
990
|
+
...event.data,
|
|
991
|
+
operation: isWriteTool(event.data["tool"]) ? "write" : "read"
|
|
992
|
+
}
|
|
993
|
+
} : event;
|
|
994
|
+
const pending = Promise.resolve(this.options.audit?.(audited));
|
|
995
|
+
pending.catch((error) => this.reportError(error));
|
|
996
|
+
return pending;
|
|
997
|
+
}
|
|
998
|
+
async connect() {
|
|
999
|
+
if (this.transport?.connected && [
|
|
1000
|
+
"ready",
|
|
1001
|
+
"unpaired",
|
|
1002
|
+
"pairing",
|
|
1003
|
+
"awaiting-confirmation"
|
|
1004
|
+
].includes(this.stateValue)) return;
|
|
1005
|
+
if (this.connectPromise) return await this.connectPromise;
|
|
1006
|
+
this.intentionalClose = false;
|
|
1007
|
+
this.connectPromise = this.open();
|
|
1008
|
+
try {
|
|
1009
|
+
await this.connectPromise;
|
|
1010
|
+
} finally {
|
|
1011
|
+
this.connectPromise = void 0;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
async open() {
|
|
1015
|
+
assertControllerUrl(this.options.url);
|
|
1016
|
+
this.clearReconnect();
|
|
1017
|
+
this.setState("connecting");
|
|
1018
|
+
const transport = createWebSocketControllerTransport({
|
|
1019
|
+
url: this.options.url,
|
|
1020
|
+
...this.options.WebSocket === void 0 ? {} : { WebSocket: this.options.WebSocket },
|
|
1021
|
+
isAuthorized: () => this.stateValue === "ready",
|
|
1022
|
+
onControlRequest: (method, params) => this.onControlRequest(method, params),
|
|
1023
|
+
onControlNotification: (method, params) => this.onControlNotification(method, params),
|
|
1024
|
+
onclose: () => this.onTransportClose(),
|
|
1025
|
+
onerror: (error) => this.reportError(error)
|
|
1026
|
+
});
|
|
1027
|
+
this.transport = transport;
|
|
1028
|
+
try {
|
|
1029
|
+
await transport.connect();
|
|
1030
|
+
this.setState("handshaking");
|
|
1031
|
+
const hello = await transport.request("webskill/hello", {
|
|
1032
|
+
protocolVersions: ["0.1"],
|
|
1033
|
+
hostName: this.options.deviceName,
|
|
1034
|
+
hostVersion: this.options.hostVersion,
|
|
1035
|
+
...this.keyPair === void 0 ? {} : { deviceFingerprint: await deviceFingerprint(this.keyPair) }
|
|
1036
|
+
});
|
|
1037
|
+
if (hello.protocolVersion !== "0.1") throw controllerError("CONTROLLER_PROTOCOL_UNSUPPORTED", `Controller protocol ${hello.protocolVersion} is not supported`);
|
|
1038
|
+
this.controllerInstance = hello.controllerInstanceId;
|
|
1039
|
+
this.controllerName = hello.controllerName;
|
|
1040
|
+
this.mergeOrigins(hello.excludedOrigins);
|
|
1041
|
+
this.startHeartbeat();
|
|
1042
|
+
this.pairing = await this.options.store.loadPairing();
|
|
1043
|
+
if (this.pairing) this.mergeOrigins(this.pairing.declaredOrigins);
|
|
1044
|
+
this.options.onExcludedOriginsChange?.(this.excludedOrigins);
|
|
1045
|
+
if (!this.pairing) {
|
|
1046
|
+
this.setState("unpaired");
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
if (this.pairing.controllerInstanceId !== hello.controllerInstanceId) {
|
|
1050
|
+
const error = controllerError("CONTROLLER_NOT_PAIRED", "Controller instance changed; pair the device again");
|
|
1051
|
+
this.reportError(error);
|
|
1052
|
+
this.setState("failed");
|
|
1053
|
+
this.intentionalClose = true;
|
|
1054
|
+
await transport.close(1e3, "Controller instance changed");
|
|
1055
|
+
throw error;
|
|
1056
|
+
}
|
|
1057
|
+
this.restoredPairing = true;
|
|
1058
|
+
const writeState = await this.options.store.loadWriteState?.();
|
|
1059
|
+
this.writeState = writeState?.epoch === this.pairing.sessionId && Number.isSafeInteger(writeState.highWatermark) && writeState.highWatermark >= 0 ? writeState : void 0;
|
|
1060
|
+
this.writeRecoverable = this.writeState !== void 0;
|
|
1061
|
+
await this.authorizeExistingPairing();
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
this.reportError(error);
|
|
1064
|
+
this.stopHeartbeat();
|
|
1065
|
+
if (!this.intentionalClose && this.pairing) {
|
|
1066
|
+
this.setState("offline");
|
|
1067
|
+
this.scheduleReconnect();
|
|
1068
|
+
} else this.setState("failed");
|
|
1069
|
+
throw error;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
async authorizeExistingPairing() {
|
|
1073
|
+
const record = this.pairing;
|
|
1074
|
+
if (!record) return;
|
|
1075
|
+
this.setState("authorizing");
|
|
1076
|
+
try {
|
|
1077
|
+
const result = await this.transport.request("webskill/auth.present", { grant: record.grant });
|
|
1078
|
+
this.pairing = {
|
|
1079
|
+
...record,
|
|
1080
|
+
grantExpiresAt: result.expiresAt
|
|
1081
|
+
};
|
|
1082
|
+
await this.options.store.savePairing(this.pairing);
|
|
1083
|
+
await this.becomeReady();
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
if (error instanceof ControllerRpcError && error.code === "CONTROLLER_NOT_PAIRED") try {
|
|
1086
|
+
await this.renewGrant();
|
|
1087
|
+
await this.becomeReady();
|
|
1088
|
+
return;
|
|
1089
|
+
} catch {
|
|
1090
|
+
await this.options.store.savePairing(void 0);
|
|
1091
|
+
this.pairing = void 0;
|
|
1092
|
+
this.setState("unpaired");
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
throw error;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
async renewGrant() {
|
|
1099
|
+
const record = this.pairing;
|
|
1100
|
+
if (!record) throw controllerError("CONTROLLER_NOT_PAIRED", "No paired device grant is available");
|
|
1101
|
+
const keyPair = this.keyPair ?? await this.options.store.loadKeyPair();
|
|
1102
|
+
if (!keyPair) throw controllerError("CONTROLLER_NOT_PAIRED", "The device key pair is unavailable");
|
|
1103
|
+
this.keyPair = keyPair;
|
|
1104
|
+
const challenge = await this.transport.request("webskill/auth.challenge", {});
|
|
1105
|
+
if (Date.parse(challenge.expiresAt) <= this.now()) throw controllerError("CONTROLLER_NOT_PAIRED", "The grant renewal challenge has expired");
|
|
1106
|
+
const signature = await signDeviceText(keyPair, [
|
|
1107
|
+
"webskill-controller/0.1",
|
|
1108
|
+
"auth.renew",
|
|
1109
|
+
record.controllerInstanceId,
|
|
1110
|
+
challenge.nonce
|
|
1111
|
+
].join("\n"));
|
|
1112
|
+
const renewed = await this.transport.request("webskill/auth.renew", {
|
|
1113
|
+
nonce: challenge.nonce,
|
|
1114
|
+
signature
|
|
1115
|
+
});
|
|
1116
|
+
this.pairing = {
|
|
1117
|
+
...record,
|
|
1118
|
+
grant: renewed.grant,
|
|
1119
|
+
grantExpiresAt: renewed.expiresAt
|
|
1120
|
+
};
|
|
1121
|
+
await this.options.store.savePairing(this.pairing);
|
|
1122
|
+
}
|
|
1123
|
+
async startWriteEpoch(sessionId) {
|
|
1124
|
+
this.restoredPairing = false;
|
|
1125
|
+
this.writeRecoverable = true;
|
|
1126
|
+
this.writeCache.clear();
|
|
1127
|
+
this.writeState = {
|
|
1128
|
+
epoch: sessionId,
|
|
1129
|
+
highWatermark: 0
|
|
1130
|
+
};
|
|
1131
|
+
await this.options.store.saveWriteState?.(this.writeState);
|
|
1132
|
+
}
|
|
1133
|
+
async persistWriteState() {
|
|
1134
|
+
if (this.writeState) await this.options.store.saveWriteState?.(this.writeState);
|
|
1135
|
+
}
|
|
1136
|
+
async becomeReady() {
|
|
1137
|
+
this.reconnectAttempt = 0;
|
|
1138
|
+
await this.startMcp();
|
|
1139
|
+
const epoch = this.pairing?.sessionId ?? this.writeState?.epoch;
|
|
1140
|
+
if (!epoch) throw controllerError("CONTROLLER_NOT_PAIRED", "The device has no pairing epoch for the ready notification");
|
|
1141
|
+
this.setState("ready");
|
|
1142
|
+
await this.transport?.notify("webskill/ready", {
|
|
1143
|
+
deviceEpoch: epoch,
|
|
1144
|
+
writeHighWatermark: this.writeState?.highWatermark ?? 0,
|
|
1145
|
+
hostVersion: this.options.hostVersion,
|
|
1146
|
+
writeStateRecoverable: this.writeRecoverable
|
|
1147
|
+
});
|
|
1148
|
+
this.audit({
|
|
1149
|
+
type: "controller.connection",
|
|
1150
|
+
data: {
|
|
1151
|
+
state: "ready",
|
|
1152
|
+
controllerOrigin: this.controllerOrigin,
|
|
1153
|
+
controllerInstance: this.controllerInstance ?? ""
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
this.scheduleRenewal();
|
|
1157
|
+
}
|
|
1158
|
+
async startPairing() {
|
|
1159
|
+
if (this.stateValue === "ready") throw controllerError("CONTROLLER_PAIRING_REJECTED", "The device is already paired");
|
|
1160
|
+
if (!this.transport?.connected || [
|
|
1161
|
+
"idle",
|
|
1162
|
+
"offline",
|
|
1163
|
+
"failed"
|
|
1164
|
+
].includes(this.stateValue)) await this.connect();
|
|
1165
|
+
if (!this.transport?.connected || this.stateValue !== "unpaired") throw controllerError("CONTROLLER_PAIRING_REJECTED", "The controller is not ready for pairing");
|
|
1166
|
+
this.keyPair = await this.options.store.loadKeyPair() ?? await generateDeviceKeyPair();
|
|
1167
|
+
await this.options.store.saveKeyPair(this.keyPair);
|
|
1168
|
+
const publicKey = await exportPublicKey(this.keyPair);
|
|
1169
|
+
const fingerprint = await deviceFingerprint(this.keyPair);
|
|
1170
|
+
const started = await this.transport.request("webskill/pair.start", {
|
|
1171
|
+
publicKey,
|
|
1172
|
+
deviceName: this.options.deviceName
|
|
1173
|
+
});
|
|
1174
|
+
this.pendingPairing = {
|
|
1175
|
+
...started,
|
|
1176
|
+
publicKey,
|
|
1177
|
+
deviceFingerprint: fingerprint
|
|
1178
|
+
};
|
|
1179
|
+
this.pendingApproval = void 0;
|
|
1180
|
+
this.pairingCodeValue = {
|
|
1181
|
+
...started,
|
|
1182
|
+
deviceFingerprint: fingerprint
|
|
1183
|
+
};
|
|
1184
|
+
this.setState("pairing");
|
|
1185
|
+
this.audit({
|
|
1186
|
+
type: "controller.pairing",
|
|
1187
|
+
data: {
|
|
1188
|
+
phase: "started",
|
|
1189
|
+
sessionId: started.sessionId
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
const ttl = Math.max(1, Date.parse(started.expiresAt) - this.now());
|
|
1193
|
+
this.pairingTimer = setTimeout(() => {
|
|
1194
|
+
if (this.stateValue === "pairing" || this.stateValue === "awaiting-confirmation") {
|
|
1195
|
+
this.pendingPairing = void 0;
|
|
1196
|
+
this.pendingApproval = void 0;
|
|
1197
|
+
this.pairingCodeValue = void 0;
|
|
1198
|
+
this.setState("unpaired");
|
|
1199
|
+
this.reportError(controllerError("CONTROLLER_PAIRING_REJECTED", "The pairing session has expired"));
|
|
1200
|
+
}
|
|
1201
|
+
}, ttl);
|
|
1202
|
+
return this.pairingCodeValue;
|
|
1203
|
+
}
|
|
1204
|
+
onPairApproved(approval) {
|
|
1205
|
+
const pending = this.pendingPairing;
|
|
1206
|
+
if (!pending || pending.sessionId !== approval.sessionId || this.stateValue !== "pairing") return;
|
|
1207
|
+
if (approval.controllerInstanceId !== this.controllerInstance) {
|
|
1208
|
+
this.reportError(controllerError("CONTROLLER_NOT_PAIRED", "The approval came from a different controller instance"));
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
this.pendingApproval = approval;
|
|
1212
|
+
this.mergeOrigins(approval.excludedOrigins);
|
|
1213
|
+
this.setState("awaiting-confirmation");
|
|
1214
|
+
this.confirmApprovedPairing(approval).catch((error) => this.reportError(error));
|
|
1215
|
+
}
|
|
1216
|
+
async confirmApprovedPairing(approval) {
|
|
1217
|
+
let accepted = false;
|
|
1218
|
+
let timer;
|
|
1219
|
+
try {
|
|
1220
|
+
accepted = await Promise.race([this.options.confirmPairing({
|
|
1221
|
+
...approval,
|
|
1222
|
+
controllerName: this.controllerName
|
|
1223
|
+
}), new Promise((_, reject) => {
|
|
1224
|
+
timer = setTimeout(() => reject(controllerError("CONTROLLER_PAIRING_REJECTED", "Device-side pairing confirmation timed out")), CONTROLLER_PROTOCOL_LIMITS.deviceConfirmTimeoutMs);
|
|
1225
|
+
})]);
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
this.reportError(error);
|
|
1228
|
+
} finally {
|
|
1229
|
+
if (timer) clearTimeout(timer);
|
|
1230
|
+
}
|
|
1231
|
+
if (!this.pendingPairing || this.pendingPairing.sessionId !== approval.sessionId) return;
|
|
1232
|
+
let signature;
|
|
1233
|
+
if (accepted) signature = await signDeviceText(this.keyPair, [
|
|
1234
|
+
"webskill-controller/0.1",
|
|
1235
|
+
"pair.confirm",
|
|
1236
|
+
approval.sessionId,
|
|
1237
|
+
approval.approvalNonce
|
|
1238
|
+
].join("\n"));
|
|
1239
|
+
const result = await this.transport.request("webskill/pair.confirm", {
|
|
1240
|
+
sessionId: approval.sessionId,
|
|
1241
|
+
accepted,
|
|
1242
|
+
...signature === void 0 ? {} : { signature }
|
|
1243
|
+
});
|
|
1244
|
+
if (!accepted) {
|
|
1245
|
+
this.pendingPairing = void 0;
|
|
1246
|
+
this.pendingApproval = void 0;
|
|
1247
|
+
this.pairingCodeValue = void 0;
|
|
1248
|
+
this.setState("unpaired");
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
if (!result.grant || !result.expiresAt) throw controllerError("CONTROLLER_PAIRING_REJECTED", "The controller did not return a device grant");
|
|
1252
|
+
const record = {
|
|
1253
|
+
controllerUrl: this.options.url,
|
|
1254
|
+
controllerInstanceId: approval.controllerInstanceId,
|
|
1255
|
+
controllerName: this.controllerName,
|
|
1256
|
+
principalDisplayName: approval.principal.displayName,
|
|
1257
|
+
sessionId: approval.sessionId,
|
|
1258
|
+
grant: result.grant,
|
|
1259
|
+
grantExpiresAt: result.expiresAt,
|
|
1260
|
+
declaredOrigins: this.excludedOrigins,
|
|
1261
|
+
pairedAt: new Date(this.now()).toISOString()
|
|
1262
|
+
};
|
|
1263
|
+
this.pairing = record;
|
|
1264
|
+
await this.options.store.savePairing(record);
|
|
1265
|
+
await this.startWriteEpoch(record.sessionId);
|
|
1266
|
+
this.pendingPairing = void 0;
|
|
1267
|
+
this.pendingApproval = void 0;
|
|
1268
|
+
this.pairingCodeValue = void 0;
|
|
1269
|
+
this.clearPairingTimer();
|
|
1270
|
+
this.audit({
|
|
1271
|
+
type: "controller.pairing",
|
|
1272
|
+
data: {
|
|
1273
|
+
phase: "confirmed",
|
|
1274
|
+
sessionId: approval.sessionId,
|
|
1275
|
+
principal: approval.principal.displayName
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
await this.becomeReady();
|
|
1279
|
+
}
|
|
1280
|
+
onPairRejected(rejected) {
|
|
1281
|
+
if (this.pendingPairing?.sessionId !== rejected.sessionId) return;
|
|
1282
|
+
this.pendingPairing = void 0;
|
|
1283
|
+
this.pendingApproval = void 0;
|
|
1284
|
+
this.pairingCodeValue = void 0;
|
|
1285
|
+
this.clearPairingTimer();
|
|
1286
|
+
this.setState("unpaired");
|
|
1287
|
+
this.reportError(controllerError("CONTROLLER_PAIRING_REJECTED", rejected.code));
|
|
1288
|
+
this.audit({
|
|
1289
|
+
type: "controller.pairing",
|
|
1290
|
+
data: {
|
|
1291
|
+
phase: "rejected",
|
|
1292
|
+
sessionId: rejected.sessionId
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
mergeOrigins(origins) {
|
|
1297
|
+
const previous = this.excludedOrigins;
|
|
1298
|
+
for (const origin of origins) this.excluded.add(origin);
|
|
1299
|
+
const next = this.excludedOrigins;
|
|
1300
|
+
if (next.length !== previous.length) {
|
|
1301
|
+
this.options.onExcludedOriginsChange?.(next);
|
|
1302
|
+
this.audit({
|
|
1303
|
+
type: "controller.origins",
|
|
1304
|
+
data: {
|
|
1305
|
+
effective: next,
|
|
1306
|
+
removedFromDeclaration: previous.filter((origin) => !origins.includes(origin))
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
if (this.pairing) {
|
|
1310
|
+
this.pairing = {
|
|
1311
|
+
...this.pairing,
|
|
1312
|
+
declaredOrigins: next
|
|
1313
|
+
};
|
|
1314
|
+
this.options.store.savePairing(this.pairing).catch((error) => this.reportError(error));
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
onControlRequest(method, _params) {
|
|
1319
|
+
if (method === "webskill/ping") return {};
|
|
1320
|
+
if (method === "webskill/bye") {
|
|
1321
|
+
this.transport?.close();
|
|
1322
|
+
return {};
|
|
1323
|
+
}
|
|
1324
|
+
throw controllerError("CONTROLLER_CAPABILITY_MISSING", `Unsupported controller request: ${method}`);
|
|
1325
|
+
}
|
|
1326
|
+
onControlNotification(method, params) {
|
|
1327
|
+
switch (method) {
|
|
1328
|
+
case "webskill/pair.approved":
|
|
1329
|
+
this.onPairApproved(params);
|
|
1330
|
+
return;
|
|
1331
|
+
case "webskill/pair.rejected":
|
|
1332
|
+
this.onPairRejected(params);
|
|
1333
|
+
return;
|
|
1334
|
+
case "webskill/origins":
|
|
1335
|
+
this.mergeOrigins(params.excludedOrigins);
|
|
1336
|
+
return;
|
|
1337
|
+
case "webskill/auth.revoked":
|
|
1338
|
+
this.onRevoked(params.reason);
|
|
1339
|
+
return;
|
|
1340
|
+
case "webskill/bye":
|
|
1341
|
+
this.transport?.close();
|
|
1342
|
+
return;
|
|
1343
|
+
default: this.reportError(controllerError("CONTROLLER_CAPABILITY_MISSING", `Unsupported controller notification: ${method}`));
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
async onRevoked(reason) {
|
|
1347
|
+
this.lifecycleGeneration += 1;
|
|
1348
|
+
await this.options.store.savePairing(void 0);
|
|
1349
|
+
this.pairing = void 0;
|
|
1350
|
+
this.writeState = void 0;
|
|
1351
|
+
this.restoredPairing = false;
|
|
1352
|
+
this.writeRecoverable = true;
|
|
1353
|
+
this.writeCache.clear();
|
|
1354
|
+
await this.options.store.saveWriteState?.(void 0);
|
|
1355
|
+
await this.stopMcp();
|
|
1356
|
+
this.setState("unpaired");
|
|
1357
|
+
this.audit({
|
|
1358
|
+
type: "controller.pairing",
|
|
1359
|
+
data: {
|
|
1360
|
+
phase: "unpaired",
|
|
1361
|
+
reason
|
|
1362
|
+
}
|
|
1363
|
+
});
|
|
1364
|
+
this.reportError(controllerError("CONTROLLER_NOT_PAIRED", `The device grant was revoked: ${reason}`));
|
|
1365
|
+
}
|
|
1366
|
+
startHeartbeat() {
|
|
1367
|
+
this.stopHeartbeat();
|
|
1368
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1369
|
+
if (!this.transport?.connected) return;
|
|
1370
|
+
this.transport.request("webskill/ping", {}, CONTROLLER_PROTOCOL_LIMITS.heartbeatTimeoutMs).catch((error) => {
|
|
1371
|
+
this.reportError(error);
|
|
1372
|
+
this.transport?.close(1001, "Controller heartbeat timed out");
|
|
1373
|
+
});
|
|
1374
|
+
}, CONTROLLER_PROTOCOL_LIMITS.heartbeatIntervalMs);
|
|
1375
|
+
}
|
|
1376
|
+
stopHeartbeat() {
|
|
1377
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
1378
|
+
this.heartbeatTimer = void 0;
|
|
1379
|
+
}
|
|
1380
|
+
scheduleRenewal() {
|
|
1381
|
+
if (this.renewTimer) clearTimeout(this.renewTimer);
|
|
1382
|
+
const record = this.pairing;
|
|
1383
|
+
if (!record) return;
|
|
1384
|
+
const remaining = Math.max(1, Date.parse(record.grantExpiresAt) - this.now());
|
|
1385
|
+
this.renewTimer = setTimeout(() => {
|
|
1386
|
+
this.renewGrant().then(() => this.scheduleRenewal()).catch((error) => {
|
|
1387
|
+
this.reportError(error);
|
|
1388
|
+
this.onRevoked("grant-renewal-failed");
|
|
1389
|
+
});
|
|
1390
|
+
}, Math.max(1, Math.floor(remaining * .8)));
|
|
1391
|
+
}
|
|
1392
|
+
onTransportClose() {
|
|
1393
|
+
this.lifecycleGeneration += 1;
|
|
1394
|
+
this.stopHeartbeat();
|
|
1395
|
+
if (this.renewTimer) clearTimeout(this.renewTimer);
|
|
1396
|
+
this.stopMcp();
|
|
1397
|
+
if (this.pendingPairing && (this.stateValue === "pairing" || this.stateValue === "awaiting-confirmation")) {
|
|
1398
|
+
this.pendingPairing = void 0;
|
|
1399
|
+
this.pendingApproval = void 0;
|
|
1400
|
+
this.pairingCodeValue = void 0;
|
|
1401
|
+
this.clearPairingTimer();
|
|
1402
|
+
this.setState("unpaired");
|
|
1403
|
+
}
|
|
1404
|
+
if (!this.intentionalClose && this.pairing) {
|
|
1405
|
+
this.setState("offline");
|
|
1406
|
+
this.scheduleReconnect();
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
clearReconnect() {
|
|
1410
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
1411
|
+
this.reconnectTimer = void 0;
|
|
1412
|
+
}
|
|
1413
|
+
scheduleReconnect() {
|
|
1414
|
+
if (this.reconnectTimer || this.intentionalClose || !this.pairing) return;
|
|
1415
|
+
const base = Math.min(1e3 * 2 ** this.reconnectAttempt, CONTROLLER_PROTOCOL_LIMITS.reconnectMaxDelayMs);
|
|
1416
|
+
this.reconnectAttempt += 1;
|
|
1417
|
+
const jitter = base * (Math.random() * .4 - .2);
|
|
1418
|
+
this.reconnectTimer = setTimeout(() => {
|
|
1419
|
+
this.reconnectTimer = void 0;
|
|
1420
|
+
this.connect().catch(() => void 0);
|
|
1421
|
+
}, Math.max(100, Math.floor(base + jitter)));
|
|
1422
|
+
}
|
|
1423
|
+
async disconnect(reason = "User disconnected") {
|
|
1424
|
+
this.lifecycleGeneration += 1;
|
|
1425
|
+
this.intentionalClose = true;
|
|
1426
|
+
this.clearReconnect();
|
|
1427
|
+
this.stopHeartbeat();
|
|
1428
|
+
this.clearPairingTimer();
|
|
1429
|
+
if (this.renewTimer) clearTimeout(this.renewTimer);
|
|
1430
|
+
const transport = this.transport;
|
|
1431
|
+
this.transport = void 0;
|
|
1432
|
+
if (transport?.connected) {
|
|
1433
|
+
await transport.notify("webskill/bye", {
|
|
1434
|
+
code: "disconnect",
|
|
1435
|
+
message: reason
|
|
1436
|
+
}).catch(() => void 0);
|
|
1437
|
+
await transport.close(1e3, reason);
|
|
1438
|
+
}
|
|
1439
|
+
await this.stopMcp();
|
|
1440
|
+
this.setState("idle");
|
|
1441
|
+
}
|
|
1442
|
+
async unpair() {
|
|
1443
|
+
const sessionId = this.pairing?.sessionId ?? this.pendingPairing?.sessionId;
|
|
1444
|
+
this.audit({
|
|
1445
|
+
type: "controller.pairing",
|
|
1446
|
+
data: {
|
|
1447
|
+
phase: "unpaired",
|
|
1448
|
+
...sessionId === void 0 ? {} : { sessionId }
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
await this.disconnect("Device unpaired");
|
|
1452
|
+
await this.options.store.clear();
|
|
1453
|
+
this.pairing = void 0;
|
|
1454
|
+
this.keyPair = void 0;
|
|
1455
|
+
this.writeState = void 0;
|
|
1456
|
+
this.restoredPairing = false;
|
|
1457
|
+
this.writeRecoverable = true;
|
|
1458
|
+
this.writeCache.clear();
|
|
1459
|
+
this.pendingPairing = void 0;
|
|
1460
|
+
this.pendingApproval = void 0;
|
|
1461
|
+
this.pairingCodeValue = void 0;
|
|
1462
|
+
this.excluded = /* @__PURE__ */ new Set([this.controllerOrigin]);
|
|
1463
|
+
this.setState("idle");
|
|
1464
|
+
}
|
|
1465
|
+
setHumanInControl(inControl) {
|
|
1466
|
+
if (this.human === inControl) return;
|
|
1467
|
+
this.lifecycleGeneration += 1;
|
|
1468
|
+
this.human = inControl;
|
|
1469
|
+
this.audit({
|
|
1470
|
+
type: "controller.human-control",
|
|
1471
|
+
data: { inControl }
|
|
1472
|
+
});
|
|
1473
|
+
if (this.transport?.connected) this.transport.notify("webskill/control.human", {
|
|
1474
|
+
inControl,
|
|
1475
|
+
at: new Date(this.now()).toISOString()
|
|
1476
|
+
}).catch((error) => this.reportError(error));
|
|
1477
|
+
}
|
|
1478
|
+
clearPairingTimer() {
|
|
1479
|
+
if (this.pairingTimer) clearTimeout(this.pairingTimer);
|
|
1480
|
+
this.pairingTimer = void 0;
|
|
1481
|
+
}
|
|
1482
|
+
async startMcp() {
|
|
1483
|
+
if (this.mcpServer || this.mcpStart) return await this.mcpStart;
|
|
1484
|
+
this.mcpStart = this.startMcpServer();
|
|
1485
|
+
try {
|
|
1486
|
+
await this.mcpStart;
|
|
1487
|
+
} finally {
|
|
1488
|
+
this.mcpStart = void 0;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
async startMcpServer() {
|
|
1492
|
+
const transport = this.transport;
|
|
1493
|
+
if (!transport) throw controllerError("CONTROLLER_NOT_PAIRED", "The device transport is unavailable");
|
|
1494
|
+
let ServerCtor;
|
|
1495
|
+
let ValidatorCtor;
|
|
1496
|
+
let ListToolsSchema;
|
|
1497
|
+
let CallToolSchema;
|
|
1498
|
+
let McpErrorCtor;
|
|
1499
|
+
try {
|
|
1500
|
+
const [serverModule, validatorModule, typesModule] = await Promise.all([
|
|
1501
|
+
import("@modelcontextprotocol/sdk/server/index.js"),
|
|
1502
|
+
import("@modelcontextprotocol/sdk/validation/cfworker"),
|
|
1503
|
+
import("@modelcontextprotocol/sdk/types.js")
|
|
1504
|
+
]);
|
|
1505
|
+
ServerCtor = serverModule.Server;
|
|
1506
|
+
ValidatorCtor = validatorModule.CfWorkerJsonSchemaValidator;
|
|
1507
|
+
ListToolsSchema = typesModule.ListToolsRequestSchema;
|
|
1508
|
+
CallToolSchema = typesModule.CallToolRequestSchema;
|
|
1509
|
+
McpErrorCtor = typesModule.McpError;
|
|
1510
|
+
} catch (error) {
|
|
1511
|
+
throw new WebSkillError("MCP_ENDPOINT_UNAVAILABLE", "The MCP SDK is required to start the controller device", error);
|
|
1512
|
+
}
|
|
1513
|
+
const instructions = await controllerInstructions(this.options.tools);
|
|
1514
|
+
const server = new ServerCtor({
|
|
1515
|
+
name: "webskill-extension",
|
|
1516
|
+
version: this.options.hostVersion
|
|
1517
|
+
}, {
|
|
1518
|
+
capabilities: { tools: { listChanged: true } },
|
|
1519
|
+
...instructions === void 0 ? {} : { instructions },
|
|
1520
|
+
jsonSchemaValidator: new ValidatorCtor()
|
|
1521
|
+
});
|
|
1522
|
+
server.setRequestHandler(ListToolsSchema, async () => ({ tools: await this.listTools() }));
|
|
1523
|
+
server.setRequestHandler(CallToolSchema, async (request) => {
|
|
1524
|
+
try {
|
|
1525
|
+
return await this.onToolCall(request.params);
|
|
1526
|
+
} catch (error) {
|
|
1527
|
+
if (error instanceof ControllerRpcError) {
|
|
1528
|
+
const rpc = toJsonRpcError(error);
|
|
1529
|
+
throw new McpErrorCtor(rpc.code, rpc.message, rpc.data);
|
|
1530
|
+
}
|
|
1531
|
+
throw error;
|
|
1532
|
+
}
|
|
1533
|
+
});
|
|
1534
|
+
await server.connect(transport);
|
|
1535
|
+
if (transport !== this.transport || !transport.connected) {
|
|
1536
|
+
await server.close();
|
|
1537
|
+
throw controllerError("CONTROLLER_MESSAGE_INVALID", "Controller transport changed while starting MCP");
|
|
1538
|
+
}
|
|
1539
|
+
this.mcpServer = server;
|
|
1540
|
+
}
|
|
1541
|
+
async stopMcp() {
|
|
1542
|
+
const server = this.mcpServer;
|
|
1543
|
+
this.mcpServer = void 0;
|
|
1544
|
+
if (server) await server.close().catch(() => void 0);
|
|
1545
|
+
}
|
|
1546
|
+
async listTools() {
|
|
1547
|
+
const tools = await listControllerTools(this.options.tools);
|
|
1548
|
+
this.lastToolNames = tools.map((tool) => tool.name).sort().join("\n");
|
|
1549
|
+
return tools;
|
|
1550
|
+
}
|
|
1551
|
+
readInvocation(params) {
|
|
1552
|
+
const input = params;
|
|
1553
|
+
if (input._meta !== void 0 && (typeof input._meta !== "object" || input._meta === null || Array.isArray(input._meta))) throw controllerError("CONTROLLER_MESSAGE_INVALID", "Tool _meta must be a JSON object");
|
|
1554
|
+
const meta = input._meta ?? {};
|
|
1555
|
+
const id = meta["webskill/invocationId"];
|
|
1556
|
+
if (typeof id !== "string" || !INVOCATION_ID.test(id)) throw controllerError("CONTROLLER_MESSAGE_INVALID", "A valid webskill/invocationId is required for every tool call");
|
|
1557
|
+
const epoch = meta["webskill/deviceEpoch"];
|
|
1558
|
+
if (epoch !== void 0 && (typeof epoch !== "string" || !INVOCATION_ID.test(epoch))) throw controllerError("CONTROLLER_MESSAGE_INVALID", "webskill/deviceEpoch must match the pairing session ID");
|
|
1559
|
+
const sequence = meta["webskill/sequence"];
|
|
1560
|
+
if (sequence !== void 0 && (!Number.isSafeInteger(sequence) || sequence < 1)) throw controllerError("CONTROLLER_MESSAGE_INVALID", "webskill/sequence must be a positive integer");
|
|
1561
|
+
const context = meta["webskill/context"];
|
|
1562
|
+
if (context !== void 0 && (typeof context !== "object" || context === null || Array.isArray(context))) throw controllerError("CONTROLLER_MESSAGE_INVALID", "webskill/context must be a JSON object");
|
|
1563
|
+
if (context !== void 0 && byteLength(JSON.stringify(context)) > CONTROLLER_PROTOCOL_LIMITS.invocationContextMaxBytes) throw controllerError("CONTROLLER_MESSAGE_INVALID", "webskill/context exceeds the protocol size limit");
|
|
1564
|
+
const args = input.arguments ?? {};
|
|
1565
|
+
if (typeof args !== "object" || args === null || Array.isArray(args)) throw controllerError("CONTROLLER_MESSAGE_INVALID", "Tool arguments must be a JSON object");
|
|
1566
|
+
return {
|
|
1567
|
+
id,
|
|
1568
|
+
...epoch === void 0 ? {} : { epoch },
|
|
1569
|
+
...sequence === void 0 ? {} : { sequence },
|
|
1570
|
+
...context === void 0 ? {} : { context },
|
|
1571
|
+
args
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
async onToolCall(params) {
|
|
1575
|
+
try {
|
|
1576
|
+
const { id, epoch, sequence, context, args } = this.readInvocation(params);
|
|
1577
|
+
const toolName = params.name;
|
|
1578
|
+
if (typeof toolName !== "string" || !toolName) throw controllerError("CONTROLLER_CAPABILITY_MISSING", "A tool name is required");
|
|
1579
|
+
if (isWriteTool(toolName)) {
|
|
1580
|
+
const result = await this.executeWrite(id, epoch, sequence, toolName, args, context, this.human);
|
|
1581
|
+
try {
|
|
1582
|
+
await this.refreshTools();
|
|
1583
|
+
} catch (error) {
|
|
1584
|
+
this.reportError(error);
|
|
1585
|
+
}
|
|
1586
|
+
return result;
|
|
1587
|
+
}
|
|
1588
|
+
if (this.human && toolName !== "browser_status") return toolResultError(controllerError("CONTROLLER_HUMAN_IN_CONTROL", "The user is in control of the browser"));
|
|
1589
|
+
if (!(await listControllerTools(this.options.tools)).some((tool) => tool.name === toolName)) {
|
|
1590
|
+
await this.audit({
|
|
1591
|
+
type: "controller.call",
|
|
1592
|
+
data: {
|
|
1593
|
+
invocationId: id,
|
|
1594
|
+
tool: toolName,
|
|
1595
|
+
phase: "rejected",
|
|
1596
|
+
code: "CONTROLLER_CAPABILITY_MISSING"
|
|
1597
|
+
}
|
|
1598
|
+
});
|
|
1599
|
+
throw controllerError("CONTROLLER_CAPABILITY_MISSING", `Controller tool is unavailable: ${toolName}`);
|
|
1600
|
+
}
|
|
1601
|
+
await this.audit({
|
|
1602
|
+
type: "controller.call",
|
|
1603
|
+
data: {
|
|
1604
|
+
invocationId: id,
|
|
1605
|
+
tool: toolName,
|
|
1606
|
+
phase: "started",
|
|
1607
|
+
...context === void 0 ? {} : { context }
|
|
1608
|
+
}
|
|
1609
|
+
});
|
|
1610
|
+
const result = this.fitResult(await this.executeRead(toolName, args));
|
|
1611
|
+
await this.auditCall(id, toolName, result, context);
|
|
1612
|
+
await this.refreshTools();
|
|
1613
|
+
return result;
|
|
1614
|
+
} catch (error) {
|
|
1615
|
+
if (error instanceof ControllerRpcError && ["CONTROLLER_MESSAGE_INVALID", "CONTROLLER_CAPABILITY_MISSING"].includes(error.code)) throw error;
|
|
1616
|
+
this.reportError(error);
|
|
1617
|
+
return toolResultError(error);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
fitResult(result) {
|
|
1621
|
+
if (byteLength(JSON.stringify(result)) <= CONTROLLER_PROTOCOL_LIMITS.maxMessageBytes) return result;
|
|
1622
|
+
return toolResultError(controllerError("CONTROLLER_MESSAGE_INVALID", "Controller tool result exceeds the protocol size limit"));
|
|
1623
|
+
}
|
|
1624
|
+
async executeRead(name, args, invocationId) {
|
|
1625
|
+
if (name === "browser_status") return mapToolResult({
|
|
1626
|
+
ok: true,
|
|
1627
|
+
content: [{
|
|
1628
|
+
type: "json",
|
|
1629
|
+
data: {
|
|
1630
|
+
hostVersion: this.options.hostVersion,
|
|
1631
|
+
protocolVersion: "0.1",
|
|
1632
|
+
humanInControl: this.human,
|
|
1633
|
+
connectionState: this.stateValue,
|
|
1634
|
+
excludedOrigins: this.excludedOrigins,
|
|
1635
|
+
deviceEpoch: this.writeState?.epoch ?? this.pairing?.sessionId ?? "",
|
|
1636
|
+
writeHighWatermark: this.writeState?.highWatermark ?? 0,
|
|
1637
|
+
writeStateRecoverable: this.writeRecoverable
|
|
1638
|
+
}
|
|
1639
|
+
}]
|
|
1640
|
+
});
|
|
1641
|
+
const result = await callControllerTool(this.options.tools, name, args, invocationId);
|
|
1642
|
+
const mapped = mapToolResult(result);
|
|
1643
|
+
if (result.authorization) return {
|
|
1644
|
+
...mapped,
|
|
1645
|
+
_meta: {
|
|
1646
|
+
...mapped._meta,
|
|
1647
|
+
"webskill/authorization": result.authorization
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
return mapped;
|
|
1651
|
+
}
|
|
1652
|
+
async executeWrite(id, epoch, sequence, name, args, context, rejectNewInvocation = false) {
|
|
1653
|
+
this.pruneWriteCache();
|
|
1654
|
+
if (epoch === void 0 || sequence === void 0) throw controllerError("CONTROLLER_MESSAGE_INVALID", "Write calls require webskill/deviceEpoch and webskill/sequence");
|
|
1655
|
+
const state = this.writeState;
|
|
1656
|
+
const duplicate = (message) => toolResultError(controllerError("CONTROLLER_INVOCATION_DUPLICATE", message));
|
|
1657
|
+
if (this.restoredPairing && state === void 0 || state?.epoch !== epoch) return duplicate("The invocation epoch or recoverable write state is not valid for this pairing");
|
|
1658
|
+
const existing = this.writeCache.get(id);
|
|
1659
|
+
if (existing) {
|
|
1660
|
+
if (existing.epoch !== epoch || existing.sequence !== sequence || existing.tool !== name) return toolResultError(controllerError("CONTROLLER_MESSAGE_INVALID", "The invocation ID was reused with different protocol metadata"));
|
|
1661
|
+
const [digest, originalDigest] = await Promise.all([argsDigest(args), existing.argsDigest]);
|
|
1662
|
+
if (digest !== originalDigest) return toolResultError(controllerError("CONTROLLER_MESSAGE_INVALID", "The invocation ID was reused with different tool arguments"));
|
|
1663
|
+
return await existing.promise;
|
|
1664
|
+
}
|
|
1665
|
+
if (rejectNewInvocation) return toolResultError(controllerError("CONTROLLER_HUMAN_IN_CONTROL", "The user is in control of the browser"));
|
|
1666
|
+
if (sequence <= state.highWatermark) return duplicate("The invocation sequence is outside the recoverable result window; do not retry it without user confirmation");
|
|
1667
|
+
if (sequence > state.highWatermark + CONTROLLER_PROTOCOL_LIMITS.idempotencyCacheEntries) throw controllerError("CONTROLLER_MESSAGE_INVALID", "The invocation sequence exceeds the acceptable forward window");
|
|
1668
|
+
this.writeState = {
|
|
1669
|
+
epoch,
|
|
1670
|
+
highWatermark: sequence
|
|
1671
|
+
};
|
|
1672
|
+
const generation = this.lifecycleGeneration;
|
|
1673
|
+
const execute = async () => {
|
|
1674
|
+
try {
|
|
1675
|
+
await this.persistWriteState();
|
|
1676
|
+
if (generation !== this.lifecycleGeneration || this.human || this.stateValue !== "ready") return toolResultError(controllerError(this.human ? "CONTROLLER_HUMAN_IN_CONTROL" : "CONTROLLER_NOT_PAIRED", "The queued write was cancelled before it could execute"));
|
|
1677
|
+
await this.audit({
|
|
1678
|
+
type: "controller.call",
|
|
1679
|
+
data: {
|
|
1680
|
+
invocationId: id,
|
|
1681
|
+
tool: name,
|
|
1682
|
+
phase: "started",
|
|
1683
|
+
...context === void 0 ? {} : { context }
|
|
1684
|
+
}
|
|
1685
|
+
});
|
|
1686
|
+
if (!(await listControllerTools(this.options.tools)).some((tool) => tool.name === name)) {
|
|
1687
|
+
await this.audit({
|
|
1688
|
+
type: "controller.call",
|
|
1689
|
+
data: {
|
|
1690
|
+
invocationId: id,
|
|
1691
|
+
tool: name,
|
|
1692
|
+
phase: "rejected",
|
|
1693
|
+
code: "CONTROLLER_CAPABILITY_MISSING"
|
|
1694
|
+
}
|
|
1695
|
+
});
|
|
1696
|
+
throw controllerError("CONTROLLER_CAPABILITY_MISSING", `Controller tool is unavailable: ${name}`);
|
|
1697
|
+
}
|
|
1698
|
+
if (generation !== this.lifecycleGeneration || this.human || this.stateValue !== "ready") return toolResultError(controllerError(this.human ? "CONTROLLER_HUMAN_IN_CONTROL" : "CONTROLLER_NOT_PAIRED", "The queued write was cancelled before it could execute"));
|
|
1699
|
+
const result = this.fitResult(await this.executeRead(name, args, id));
|
|
1700
|
+
try {
|
|
1701
|
+
await this.auditCall(id, name, result, context);
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
this.reportError(error);
|
|
1704
|
+
return {
|
|
1705
|
+
...result,
|
|
1706
|
+
_meta: {
|
|
1707
|
+
...result._meta,
|
|
1708
|
+
"webskill/auditIncomplete": true
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
return result;
|
|
1713
|
+
} catch (error) {
|
|
1714
|
+
if (error instanceof ControllerRpcError && ["CONTROLLER_MESSAGE_INVALID", "CONTROLLER_CAPABILITY_MISSING"].includes(error.code)) throw error;
|
|
1715
|
+
this.reportError(error);
|
|
1716
|
+
return toolResultError(error);
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
const queued = this.writeQueue.then(execute, execute);
|
|
1720
|
+
const entry = {
|
|
1721
|
+
epoch,
|
|
1722
|
+
sequence,
|
|
1723
|
+
tool: name,
|
|
1724
|
+
argsDigest: argsDigest(args),
|
|
1725
|
+
promise: queued.then((result) => {
|
|
1726
|
+
entry.result = result;
|
|
1727
|
+
entry.completedAt = this.now();
|
|
1728
|
+
this.pruneWriteCache();
|
|
1729
|
+
return result;
|
|
1730
|
+
}, (error) => {
|
|
1731
|
+
entry.completedAt = this.now();
|
|
1732
|
+
this.pruneWriteCache();
|
|
1733
|
+
throw error;
|
|
1734
|
+
})
|
|
1735
|
+
};
|
|
1736
|
+
this.writeCache.set(id, entry);
|
|
1737
|
+
this.writeQueue = queued.then(() => void 0, () => void 0);
|
|
1738
|
+
return await entry.promise;
|
|
1739
|
+
}
|
|
1740
|
+
pruneWriteCache() {
|
|
1741
|
+
const now = this.now();
|
|
1742
|
+
for (const [key, entry] of this.writeCache) if (entry.completedAt !== void 0 && now - entry.completedAt > CONTROLLER_PROTOCOL_LIMITS.idempotencyCacheTtlMs) this.writeCache.delete(key);
|
|
1743
|
+
if (this.writeCache.size <= CONTROLLER_PROTOCOL_LIMITS.idempotencyCacheEntries) return;
|
|
1744
|
+
for (const [key, entry] of this.writeCache) {
|
|
1745
|
+
if (entry.completedAt === void 0) continue;
|
|
1746
|
+
this.writeCache.delete(key);
|
|
1747
|
+
if (this.writeCache.size <= CONTROLLER_PROTOCOL_LIMITS.idempotencyCacheEntries) return;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
async auditCall(invocationId, tool, result, context) {
|
|
1751
|
+
const failure = result._meta?.["webskill/error"];
|
|
1752
|
+
await this.audit({
|
|
1753
|
+
type: "controller.call",
|
|
1754
|
+
data: {
|
|
1755
|
+
invocationId,
|
|
1756
|
+
tool,
|
|
1757
|
+
ok: result.isError !== true,
|
|
1758
|
+
...failure?.code === void 0 ? {} : { code: failure.code },
|
|
1759
|
+
...result._meta?.["webskill/authorization"] === void 0 ? {} : { authorization: result._meta["webskill/authorization"] },
|
|
1760
|
+
...context === void 0 ? {} : { context }
|
|
1761
|
+
}
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
async refreshTools() {
|
|
1765
|
+
if (this.stateValue !== "ready" || !this.transport?.connected) return;
|
|
1766
|
+
const names = (await listControllerTools(this.options.tools)).map((tool) => tool.name).sort().join("\n");
|
|
1767
|
+
if (names === this.lastToolNames) return;
|
|
1768
|
+
this.lastToolNames = names;
|
|
1769
|
+
await this.transport.send({
|
|
1770
|
+
jsonrpc: "2.0",
|
|
1771
|
+
method: "notifications/tools/list_changed"
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
};
|
|
1775
|
+
/** @experimental */
|
|
1776
|
+
function createControllerDevice(options) {
|
|
1777
|
+
assertControllerUrl(options.url);
|
|
1778
|
+
return new DeviceSession(options);
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
//#endregion
|
|
1782
|
+
export { createWebSocketControllerTransport as a, ControllerRpcError as c, CONTROLLER_PROTOCOL_VERSION as d, CONTROLLER_TOOL_NAMES as f, randomToken as i, controllerError as l, base64UrlToBytes as n, CONTROLLER_PROTOCOL_SCHEMA as o, instanceFingerprint as r, validateControllerMessage as s, createControllerDevice as t, CONTROLLER_PROTOCOL_LIMITS as u };
|