@unifedev/thread-pages 0.3.2

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/bridge.ts ADDED
@@ -0,0 +1,1721 @@
1
+ /**
2
+ * Pure transport and capability contract for an untrusted Thread Page.
3
+ *
4
+ * This module deliberately has no bb SDK, DOM, fetch, or filesystem imports.
5
+ * A trusted outer shell may map these narrow methods onto bb APIs, but page
6
+ * code can only speak this protocol and cannot name an SDK method, URL, host,
7
+ * or filesystem path.
8
+ */
9
+
10
+ export const BRIDGE_PROTOCOL_VERSION = 1 as const;
11
+ export const BRIDGE_MAX_ID_LENGTH = 96;
12
+ export const BRIDGE_MAX_METHOD_LENGTH = 96;
13
+ export const BRIDGE_MAX_PAGE_REVISION_LENGTH = 128;
14
+ export const BRIDGE_MAX_SERIALIZED_BYTES = 64 * 1024;
15
+ export const BRIDGE_MAX_JSON_DEPTH = 16;
16
+ export const BRIDGE_MAX_JSON_NODES = 10_000;
17
+ export const BRIDGE_MAX_CONFIRMATION_TTL_MS = 5 * 60_000;
18
+ export const BRIDGE_MAX_STORAGE_VALUE_BYTES = 32 * 1024;
19
+
20
+ const MAX_ERROR_MESSAGE_LENGTH = 512;
21
+ const MAX_PROMPT_LENGTH = 32 * 1024;
22
+ const MAX_RESULT_TEXT_LENGTH = 64 * 1024;
23
+ const MAX_TITLE_LENGTH = 240;
24
+ const MAX_ITEMS = 200;
25
+
26
+ const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
27
+ const METHOD_PATTERN =
28
+ /^[a-z][a-zA-Z0-9]*(?:\.[a-z][a-zA-Z0-9]*)+$/;
29
+ const ENTITY_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
30
+ const OPAQUE_TOKEN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~:-]*$/;
31
+ const STORAGE_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
32
+ const UNSAFE_OBJECT_KEYS = new Set(["__proto__", "prototype", "constructor"]);
33
+
34
+ export type JsonPrimitive = null | boolean | number | string;
35
+ export type JsonObject = { [key: string]: JsonValue };
36
+ export type JsonValue = JsonPrimitive | JsonObject | JsonValue[];
37
+
38
+ export type BridgeValidationIssueCode =
39
+ | "invalid_type"
40
+ | "invalid_value"
41
+ | "missing_key"
42
+ | "unknown_key"
43
+ | "not_json_safe"
44
+ | "too_deep"
45
+ | "too_large";
46
+
47
+ export interface BridgeValidationIssue {
48
+ readonly code: BridgeValidationIssueCode;
49
+ readonly path: string;
50
+ readonly message: string;
51
+ }
52
+
53
+ export type BridgeValidationResult<T> =
54
+ | { readonly ok: true; readonly value: T }
55
+ | { readonly ok: false; readonly issues: readonly BridgeValidationIssue[] };
56
+
57
+ export const BRIDGE_ERROR_CODES = [
58
+ "invalid_json",
59
+ "request_too_large",
60
+ "response_too_large",
61
+ "invalid_request",
62
+ "invalid_response",
63
+ "unsupported_version",
64
+ "unknown_method",
65
+ "invalid_params",
66
+ "stale_page",
67
+ "confirmation_required",
68
+ "confirmation_invalid",
69
+ "not_found",
70
+ "conflict",
71
+ "unavailable",
72
+ "cancelled",
73
+ "rate_limited",
74
+ "handler_error",
75
+ "invalid_result",
76
+ ] as const;
77
+
78
+ export type BridgeErrorCode = (typeof BRIDGE_ERROR_CODES)[number];
79
+
80
+ export interface BridgeError {
81
+ readonly code: BridgeErrorCode;
82
+ readonly message: string;
83
+ }
84
+
85
+ export type BridgeContractResult<T> =
86
+ | { readonly ok: true; readonly value: T }
87
+ | {
88
+ readonly ok: false;
89
+ readonly error: BridgeError;
90
+ readonly issues?: readonly BridgeValidationIssue[];
91
+ };
92
+
93
+ export interface BridgeRequest {
94
+ readonly v: 1;
95
+ readonly id: string;
96
+ readonly method: string;
97
+ readonly params: JsonValue;
98
+ readonly pageRevision: string;
99
+ }
100
+
101
+ export interface BridgeSuccessResponse {
102
+ readonly v: 1;
103
+ readonly id: string;
104
+ readonly ok: true;
105
+ readonly result: JsonValue;
106
+ }
107
+
108
+ export interface BridgeFailureResponse {
109
+ readonly v: 1;
110
+ readonly id: string;
111
+ readonly ok: false;
112
+ readonly error: BridgeError;
113
+ }
114
+
115
+ export type BridgeResponse = BridgeSuccessResponse | BridgeFailureResponse;
116
+
117
+ export type BridgeEffect =
118
+ | "read"
119
+ | "navigation"
120
+ | "current-thread-write"
121
+ | "cross-thread-write"
122
+ | "destructive"
123
+ | "device";
124
+
125
+ export type BridgeConfirmationRequirement = "none" | "trusted-outer";
126
+
127
+ export type BridgeValidator<T> = (
128
+ value: unknown,
129
+ ) => BridgeValidationResult<T>;
130
+
131
+ export interface CapabilityMetadata<Params = unknown> {
132
+ readonly method: string;
133
+ readonly description: string;
134
+ readonly effect: BridgeEffect;
135
+ readonly confirmation: BridgeConfirmationRequirement;
136
+ /** Text is only a hint for trusted chrome; it never grants authority. */
137
+ readonly summarize?: (params: Params) => string;
138
+ }
139
+
140
+ export interface CapabilitySpec<Params = unknown, Result = unknown>
141
+ extends CapabilityMetadata<Params> {
142
+ readonly validateParams: BridgeValidator<Params>;
143
+ readonly validateResult: BridgeValidator<Result>;
144
+ }
145
+
146
+ type AnyCapabilitySpec = CapabilitySpec<any, any>;
147
+
148
+ export interface CapabilityRegistry {
149
+ get(method: string): AnyCapabilitySpec | undefined;
150
+ list(): readonly AnyCapabilitySpec[];
151
+ }
152
+
153
+ export interface CapabilityDescriptor {
154
+ readonly method: string;
155
+ readonly effect: BridgeEffect;
156
+ readonly confirmation: BridgeConfirmationRequirement;
157
+ }
158
+
159
+ function valid<T>(value: T): BridgeValidationResult<T> {
160
+ return { ok: true, value };
161
+ }
162
+
163
+ function invalid<T>(
164
+ path: string,
165
+ message: string,
166
+ code: BridgeValidationIssueCode = "invalid_value",
167
+ ): BridgeValidationResult<T> {
168
+ return { ok: false, issues: [{ code, path, message }] };
169
+ }
170
+
171
+ function contractFailure<T>(
172
+ code: BridgeErrorCode,
173
+ message: string,
174
+ issues?: readonly BridgeValidationIssue[],
175
+ ): BridgeContractResult<T> {
176
+ return {
177
+ ok: false,
178
+ error: { code, message: boundedErrorMessage(message) },
179
+ ...(issues ? { issues } : {}),
180
+ };
181
+ }
182
+
183
+ function boundedErrorMessage(message: string): string {
184
+ const normalized = message.trim() || "Bridge request failed";
185
+ return normalized.length <= MAX_ERROR_MESSAGE_LENGTH
186
+ ? normalized
187
+ : `${normalized.slice(0, MAX_ERROR_MESSAGE_LENGTH - 1)}…`;
188
+ }
189
+
190
+ function utf8Bytes(value: string): number {
191
+ return new TextEncoder().encode(value).byteLength;
192
+ }
193
+
194
+ function pathForKey(parent: string, key: string): string {
195
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
196
+ ? `${parent}.${key}`
197
+ : `${parent}[${JSON.stringify(key)}]`;
198
+ }
199
+
200
+ function isCanonicalArrayIndex(key: string, length: number): boolean {
201
+ if (!/^(0|[1-9][0-9]*)$/.test(key)) return false;
202
+ const index = Number(key);
203
+ return Number.isSafeInteger(index) && index >= 0 && index < length;
204
+ }
205
+
206
+ export interface JsonValidationLimits {
207
+ readonly maxBytes?: number;
208
+ readonly maxDepth?: number;
209
+ readonly maxNodes?: number;
210
+ }
211
+
212
+ /** Validate without JSON.stringify coercion or silent property loss. */
213
+ export function validateJsonValue(
214
+ input: unknown,
215
+ limits: JsonValidationLimits = {},
216
+ ): BridgeValidationResult<JsonValue> {
217
+ const maxBytes = limits.maxBytes ?? BRIDGE_MAX_SERIALIZED_BYTES;
218
+ const maxDepth = limits.maxDepth ?? BRIDGE_MAX_JSON_DEPTH;
219
+ const maxNodes = limits.maxNodes ?? BRIDGE_MAX_JSON_NODES;
220
+ const ancestors = new Set<object>();
221
+ let nodes = 0;
222
+
223
+ function visit(value: unknown, path: string, depth: number): BridgeValidationIssue | null {
224
+ nodes += 1;
225
+ if (nodes > maxNodes) {
226
+ return {
227
+ code: "too_large",
228
+ path,
229
+ message: `JSON value exceeds ${maxNodes} nodes`,
230
+ };
231
+ }
232
+ if (depth > maxDepth) {
233
+ return {
234
+ code: "too_deep",
235
+ path,
236
+ message: `JSON value exceeds depth ${maxDepth}`,
237
+ };
238
+ }
239
+ if (
240
+ value === null ||
241
+ typeof value === "string" ||
242
+ typeof value === "boolean"
243
+ ) {
244
+ return null;
245
+ }
246
+ if (typeof value === "number") {
247
+ return Number.isFinite(value)
248
+ ? null
249
+ : {
250
+ code: "not_json_safe",
251
+ path,
252
+ message: "JSON numbers must be finite",
253
+ };
254
+ }
255
+ if (typeof value !== "object") {
256
+ return {
257
+ code: "not_json_safe",
258
+ path,
259
+ message: `Unsupported JSON value type: ${typeof value}`,
260
+ };
261
+ }
262
+ if (ancestors.has(value)) {
263
+ return {
264
+ code: "not_json_safe",
265
+ path,
266
+ message: "Cyclic values are not JSON-safe",
267
+ };
268
+ }
269
+
270
+ ancestors.add(value);
271
+ try {
272
+ if (Array.isArray(value)) {
273
+ const keys = Reflect.ownKeys(value);
274
+ for (const key of keys) {
275
+ if (typeof key === "symbol") {
276
+ return {
277
+ code: "not_json_safe",
278
+ path,
279
+ message: "Symbol properties are not JSON-safe",
280
+ };
281
+ }
282
+ if (key !== "length" && !isCanonicalArrayIndex(key, value.length)) {
283
+ return {
284
+ code: "not_json_safe",
285
+ path: pathForKey(path, key),
286
+ message: "Arrays may not have extra properties",
287
+ };
288
+ }
289
+ }
290
+ for (let index = 0; index < value.length; index += 1) {
291
+ if (!Object.prototype.hasOwnProperty.call(value, index)) {
292
+ return {
293
+ code: "not_json_safe",
294
+ path: `${path}[${index}]`,
295
+ message: "Sparse arrays are not JSON-safe",
296
+ };
297
+ }
298
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
299
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
300
+ return {
301
+ code: "not_json_safe",
302
+ path: `${path}[${index}]`,
303
+ message: "Array entries must be enumerable data properties",
304
+ };
305
+ }
306
+ const issue = visit(descriptor.value, `${path}[${index}]`, depth + 1);
307
+ if (issue) return issue;
308
+ }
309
+ return null;
310
+ }
311
+
312
+ const prototype = Object.getPrototypeOf(value);
313
+ if (prototype !== Object.prototype && prototype !== null) {
314
+ return {
315
+ code: "not_json_safe",
316
+ path,
317
+ message: "Only plain objects are JSON-safe",
318
+ };
319
+ }
320
+ for (const key of Reflect.ownKeys(value)) {
321
+ if (typeof key === "symbol") {
322
+ return {
323
+ code: "not_json_safe",
324
+ path,
325
+ message: "Symbol properties are not JSON-safe",
326
+ };
327
+ }
328
+ if (UNSAFE_OBJECT_KEYS.has(key)) {
329
+ return {
330
+ code: "not_json_safe",
331
+ path: pathForKey(path, key),
332
+ message: "Unsafe object key",
333
+ };
334
+ }
335
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
336
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
337
+ return {
338
+ code: "not_json_safe",
339
+ path: pathForKey(path, key),
340
+ message: "Object entries must be enumerable data properties",
341
+ };
342
+ }
343
+ const issue = visit(descriptor.value, pathForKey(path, key), depth + 1);
344
+ if (issue) return issue;
345
+ }
346
+ return null;
347
+ } catch {
348
+ return {
349
+ code: "not_json_safe",
350
+ path,
351
+ message: "Value could not be safely inspected",
352
+ };
353
+ } finally {
354
+ ancestors.delete(value);
355
+ }
356
+ }
357
+
358
+ const issue = visit(input, "$", 0);
359
+ if (issue) return { ok: false, issues: [issue] };
360
+
361
+ let serialized: string;
362
+ try {
363
+ serialized = JSON.stringify(input);
364
+ } catch {
365
+ return invalid("$", "Value could not be serialized as JSON", "not_json_safe");
366
+ }
367
+ if (utf8Bytes(serialized) > maxBytes) {
368
+ return invalid(
369
+ "$",
370
+ `Serialized JSON exceeds ${maxBytes} bytes`,
371
+ "too_large",
372
+ );
373
+ }
374
+
375
+ // Round-trip to detach callers from objects that may be mutated after check.
376
+ return valid(JSON.parse(serialized) as JsonValue);
377
+ }
378
+
379
+ function decodeJsonInput(
380
+ input: unknown,
381
+ sizeCode: "request_too_large" | "response_too_large",
382
+ ): BridgeContractResult<JsonValue> {
383
+ let parsed = input;
384
+ if (typeof input === "string") {
385
+ if (utf8Bytes(input) > BRIDGE_MAX_SERIALIZED_BYTES) {
386
+ return contractFailure(sizeCode, "Bridge message is too large");
387
+ }
388
+ try {
389
+ parsed = JSON.parse(input);
390
+ } catch {
391
+ return contractFailure("invalid_json", "Bridge message is not valid JSON");
392
+ }
393
+ }
394
+ const json = validateJsonValue(parsed);
395
+ if (!json.ok) {
396
+ const tooLarge = json.issues.some((entry) => entry.code === "too_large");
397
+ return contractFailure(
398
+ tooLarge ? sizeCode : sizeCode === "request_too_large" ? "invalid_request" : "invalid_response",
399
+ tooLarge ? "Bridge message is too large" : "Bridge message is not strict JSON",
400
+ json.issues,
401
+ );
402
+ }
403
+ return { ok: true, value: json.value };
404
+ }
405
+
406
+ function asObject(
407
+ value: JsonValue,
408
+ allowed: readonly string[],
409
+ required: readonly string[],
410
+ path = "$",
411
+ ): BridgeValidationResult<JsonObject> {
412
+ if (value === null || Array.isArray(value) || typeof value !== "object") {
413
+ return invalid(path, "Expected an object", "invalid_type");
414
+ }
415
+ const allowedSet = new Set(allowed);
416
+ for (const key of Object.keys(value)) {
417
+ if (!allowedSet.has(key)) {
418
+ return invalid(pathForKey(path, key), "Unknown key", "unknown_key");
419
+ }
420
+ }
421
+ for (const key of required) {
422
+ if (!Object.prototype.hasOwnProperty.call(value, key)) {
423
+ return invalid(pathForKey(path, key), "Missing required key", "missing_key");
424
+ }
425
+ }
426
+ return valid(value);
427
+ }
428
+
429
+ function stringValue(
430
+ value: JsonValue,
431
+ path: string,
432
+ options: {
433
+ min?: number;
434
+ max: number;
435
+ pattern?: RegExp;
436
+ label?: string;
437
+ },
438
+ ): BridgeValidationResult<string> {
439
+ if (typeof value !== "string") {
440
+ return invalid(path, "Expected a string", "invalid_type");
441
+ }
442
+ const min = options.min ?? 0;
443
+ if (value.length < min || value.length > options.max) {
444
+ return invalid(
445
+ path,
446
+ `${options.label ?? "String"} length must be ${min}–${options.max}`,
447
+ "too_large",
448
+ );
449
+ }
450
+ if (options.pattern && !options.pattern.test(value)) {
451
+ return invalid(path, `${options.label ?? "String"} has an invalid format`);
452
+ }
453
+ return valid(value);
454
+ }
455
+
456
+ function booleanValue(value: JsonValue, path: string): BridgeValidationResult<boolean> {
457
+ return typeof value === "boolean"
458
+ ? valid(value)
459
+ : invalid(path, "Expected a boolean", "invalid_type");
460
+ }
461
+
462
+ function integerValue(
463
+ value: JsonValue,
464
+ path: string,
465
+ min: number,
466
+ max: number,
467
+ ): BridgeValidationResult<number> {
468
+ if (
469
+ typeof value !== "number" ||
470
+ !Number.isSafeInteger(value) ||
471
+ value < min ||
472
+ value > max
473
+ ) {
474
+ return invalid(path, `Expected an integer from ${min} to ${max}`);
475
+ }
476
+ return valid(value);
477
+ }
478
+
479
+ function enumValue<const T extends readonly string[]>(
480
+ value: JsonValue,
481
+ path: string,
482
+ values: T,
483
+ ): BridgeValidationResult<T[number]> {
484
+ return typeof value === "string" && values.includes(value)
485
+ ? valid(value as T[number])
486
+ : invalid(path, `Expected one of: ${values.join(", ")}`);
487
+ }
488
+
489
+ function entityId(value: JsonValue, path: string): BridgeValidationResult<string> {
490
+ return stringValue(value, path, {
491
+ min: 1,
492
+ max: 128,
493
+ pattern: ENTITY_ID_PATTERN,
494
+ label: "Entity id",
495
+ });
496
+ }
497
+
498
+ function nullableEntityId(
499
+ value: JsonValue,
500
+ path: string,
501
+ ): BridgeValidationResult<string | null> {
502
+ return value === null ? valid(null) : entityId(value, path);
503
+ }
504
+
505
+ function opaqueToken(
506
+ value: JsonValue,
507
+ path: string,
508
+ max = 512,
509
+ ): BridgeValidationResult<string> {
510
+ return stringValue(value, path, {
511
+ min: 1,
512
+ max,
513
+ pattern: OPAQUE_TOKEN_PATTERN,
514
+ label: "Opaque token",
515
+ });
516
+ }
517
+
518
+ function jsonValidator<T>(
519
+ parser: (value: JsonValue) => BridgeValidationResult<T>,
520
+ ): BridgeValidator<T> {
521
+ return (input) => {
522
+ const json = validateJsonValue(input);
523
+ return json.ok ? parser(json.value) : json;
524
+ };
525
+ }
526
+
527
+ function noParams(value: JsonValue): BridgeValidationResult<null> {
528
+ if (value === null) return valid(null);
529
+ const object = asObject(value, [], []);
530
+ return object.ok ? valid(null) : object;
531
+ }
532
+
533
+ export function decodeBridgeRequest(input: unknown): BridgeContractResult<BridgeRequest> {
534
+ const decoded = decodeJsonInput(input, "request_too_large");
535
+ if (!decoded.ok) return decoded;
536
+ const object = asObject(
537
+ decoded.value,
538
+ ["v", "id", "method", "params", "pageRevision"],
539
+ ["v", "id", "method", "params", "pageRevision"],
540
+ );
541
+ if (!object.ok) {
542
+ return contractFailure("invalid_request", "Invalid bridge request envelope", object.issues);
543
+ }
544
+ const value = object.value;
545
+ if (value.v !== BRIDGE_PROTOCOL_VERSION) {
546
+ return contractFailure("unsupported_version", "Unsupported bridge protocol version");
547
+ }
548
+ const id = stringValue(value.id, "$.id", {
549
+ min: 1,
550
+ max: BRIDGE_MAX_ID_LENGTH,
551
+ pattern: ID_PATTERN,
552
+ label: "Request id",
553
+ });
554
+ if (!id.ok) return contractFailure("invalid_request", "Invalid request id", id.issues);
555
+ const method = stringValue(value.method, "$.method", {
556
+ min: 3,
557
+ max: BRIDGE_MAX_METHOD_LENGTH,
558
+ pattern: METHOD_PATTERN,
559
+ label: "Method name",
560
+ });
561
+ if (!method.ok) {
562
+ return contractFailure("invalid_request", "Invalid method name", method.issues);
563
+ }
564
+ const revision = stringValue(value.pageRevision, "$.pageRevision", {
565
+ min: 1,
566
+ max: BRIDGE_MAX_PAGE_REVISION_LENGTH,
567
+ pattern: ID_PATTERN,
568
+ label: "Page revision",
569
+ });
570
+ if (!revision.ok) {
571
+ return contractFailure("invalid_request", "Invalid page revision", revision.issues);
572
+ }
573
+ return {
574
+ ok: true,
575
+ value: {
576
+ v: 1,
577
+ id: id.value,
578
+ method: method.value,
579
+ params: value.params!,
580
+ pageRevision: revision.value,
581
+ },
582
+ };
583
+ }
584
+
585
+ function isBridgeErrorCode(value: JsonValue): value is BridgeErrorCode {
586
+ return typeof value === "string" &&
587
+ (BRIDGE_ERROR_CODES as readonly string[]).includes(value);
588
+ }
589
+
590
+ export function decodeBridgeResponse(input: unknown): BridgeContractResult<BridgeResponse> {
591
+ const decoded = decodeJsonInput(input, "response_too_large");
592
+ if (!decoded.ok) return decoded;
593
+ if (decoded.value === null || Array.isArray(decoded.value) || typeof decoded.value !== "object") {
594
+ return contractFailure("invalid_response", "Invalid bridge response envelope");
595
+ }
596
+ const okValue = decoded.value.ok;
597
+ if (typeof okValue !== "boolean") {
598
+ return contractFailure("invalid_response", "Response ok flag must be boolean");
599
+ }
600
+ const expected = okValue
601
+ ? asObject(decoded.value, ["v", "id", "ok", "result"], ["v", "id", "ok", "result"])
602
+ : asObject(decoded.value, ["v", "id", "ok", "error"], ["v", "id", "ok", "error"]);
603
+ if (!expected.ok) {
604
+ return contractFailure("invalid_response", "Invalid bridge response envelope", expected.issues);
605
+ }
606
+ if (expected.value.v !== 1) {
607
+ return contractFailure("unsupported_version", "Unsupported bridge protocol version");
608
+ }
609
+ const id = stringValue(expected.value.id, "$.id", {
610
+ min: 1,
611
+ max: BRIDGE_MAX_ID_LENGTH,
612
+ pattern: ID_PATTERN,
613
+ label: "Response id",
614
+ });
615
+ if (!id.ok) return contractFailure("invalid_response", "Invalid response id", id.issues);
616
+
617
+ if (okValue) {
618
+ return {
619
+ ok: true,
620
+ value: { v: 1, id: id.value, ok: true, result: expected.value.result! },
621
+ };
622
+ }
623
+
624
+ const errorObject = asObject(expected.value.error!, ["code", "message"], ["code", "message"], "$.error");
625
+ if (!errorObject.ok) {
626
+ return contractFailure("invalid_response", "Invalid bridge error", errorObject.issues);
627
+ }
628
+ if (!isBridgeErrorCode(errorObject.value.code!)) {
629
+ return contractFailure("invalid_response", "Unknown bridge error code");
630
+ }
631
+ const message = stringValue(errorObject.value.message!, "$.error.message", {
632
+ min: 1,
633
+ max: MAX_ERROR_MESSAGE_LENGTH,
634
+ label: "Error message",
635
+ });
636
+ if (!message.ok) {
637
+ return contractFailure("invalid_response", "Invalid bridge error message", message.issues);
638
+ }
639
+ return {
640
+ ok: true,
641
+ value: {
642
+ v: 1,
643
+ id: id.value,
644
+ ok: false,
645
+ error: { code: errorObject.value.code, message: message.value },
646
+ },
647
+ };
648
+ }
649
+
650
+ function safeResponseId(id: unknown): string {
651
+ return typeof id === "string" &&
652
+ id.length >= 1 &&
653
+ id.length <= BRIDGE_MAX_ID_LENGTH &&
654
+ ID_PATTERN.test(id)
655
+ ? id
656
+ : "invalid";
657
+ }
658
+
659
+ export function makeBridgeFailureResponse(
660
+ id: unknown,
661
+ code: BridgeErrorCode,
662
+ message: string,
663
+ ): BridgeFailureResponse {
664
+ return {
665
+ v: 1,
666
+ id: safeResponseId(id),
667
+ ok: false,
668
+ error: { code, message: boundedErrorMessage(message) },
669
+ };
670
+ }
671
+
672
+ export function encodeBridgeResponse(
673
+ response: BridgeResponse,
674
+ ): BridgeContractResult<string> {
675
+ const decoded = decodeBridgeResponse(response);
676
+ if (!decoded.ok) return decoded;
677
+ const serialized = JSON.stringify(decoded.value);
678
+ if (utf8Bytes(serialized) > BRIDGE_MAX_SERIALIZED_BYTES) {
679
+ return contractFailure("response_too_large", "Bridge response is too large");
680
+ }
681
+ return { ok: true, value: serialized };
682
+ }
683
+
684
+ const EFFECTS: readonly BridgeEffect[] = [
685
+ "read",
686
+ "navigation",
687
+ "current-thread-write",
688
+ "cross-thread-write",
689
+ "destructive",
690
+ "device",
691
+ ];
692
+
693
+ const EFFECTS_REQUIRING_CONFIRMATION = new Set<BridgeEffect>([
694
+ "cross-thread-write",
695
+ "destructive",
696
+ "device",
697
+ ]);
698
+
699
+ export function createCapabilityRegistry(
700
+ specifications: readonly AnyCapabilitySpec[],
701
+ ): CapabilityRegistry {
702
+ const byMethod = new Map<string, AnyCapabilitySpec>();
703
+ const list: AnyCapabilitySpec[] = [];
704
+ for (const original of specifications) {
705
+ if (
706
+ original.method.length < 3 ||
707
+ original.method.length > BRIDGE_MAX_METHOD_LENGTH ||
708
+ !METHOD_PATTERN.test(original.method)
709
+ ) {
710
+ throw new TypeError(`Invalid bridge capability method: ${original.method}`);
711
+ }
712
+ if (byMethod.has(original.method)) {
713
+ throw new TypeError(`Duplicate bridge capability method: ${original.method}`);
714
+ }
715
+ if (!EFFECTS.includes(original.effect)) {
716
+ throw new TypeError(`Invalid effect for ${original.method}`);
717
+ }
718
+ if (
719
+ original.confirmation !== "none" &&
720
+ original.confirmation !== "trusted-outer"
721
+ ) {
722
+ throw new TypeError(`Invalid confirmation policy for ${original.method}`);
723
+ }
724
+ if (
725
+ EFFECTS_REQUIRING_CONFIRMATION.has(original.effect) &&
726
+ original.confirmation !== "trusted-outer"
727
+ ) {
728
+ throw new TypeError(
729
+ `${original.method} must require trusted outer confirmation`,
730
+ );
731
+ }
732
+ if (
733
+ original.method === "projects.create" &&
734
+ original.confirmation !== "trusted-outer"
735
+ ) {
736
+ throw new TypeError(
737
+ "projects.create must require trusted outer confirmation",
738
+ );
739
+ }
740
+ if (
741
+ typeof original.description !== "string" ||
742
+ original.description.trim().length === 0 ||
743
+ original.description.length > 240
744
+ ) {
745
+ throw new TypeError(`Invalid description for ${original.method}`);
746
+ }
747
+ if (
748
+ typeof original.validateParams !== "function" ||
749
+ typeof original.validateResult !== "function" ||
750
+ (original.summarize !== undefined && typeof original.summarize !== "function")
751
+ ) {
752
+ throw new TypeError(`Invalid validators for ${original.method}`);
753
+ }
754
+ const specification = Object.freeze({ ...original });
755
+ byMethod.set(specification.method, specification);
756
+ list.push(specification);
757
+ }
758
+ const frozenList = Object.freeze(list.slice());
759
+ return Object.freeze({
760
+ get(method: string) {
761
+ return byMethod.get(method);
762
+ },
763
+ list() {
764
+ return frozenList;
765
+ },
766
+ });
767
+ }
768
+
769
+ const VALIDATED_INVOCATION = Symbol("validated-thread-page-invocation");
770
+
771
+ export interface ValidatedBridgeInvocation {
772
+ readonly request: BridgeRequest;
773
+ readonly capability: AnyCapabilitySpec;
774
+ readonly params: unknown;
775
+ readonly [VALIDATED_INVOCATION]: true;
776
+ }
777
+
778
+ export function resolveBridgeInvocation(
779
+ input: unknown,
780
+ registry: CapabilityRegistry = strictParityCapabilityRegistry,
781
+ expectedPageRevision?: string,
782
+ ): BridgeContractResult<ValidatedBridgeInvocation> {
783
+ const decoded = decodeBridgeRequest(input);
784
+ if (!decoded.ok) return decoded;
785
+ if (
786
+ expectedPageRevision !== undefined &&
787
+ decoded.value.pageRevision !== expectedPageRevision
788
+ ) {
789
+ return contractFailure("stale_page", "The Thread Page revision has changed");
790
+ }
791
+ const capability = registry.get(decoded.value.method);
792
+ if (!capability) {
793
+ return contractFailure("unknown_method", "Unknown Thread Page capability");
794
+ }
795
+ const params = capability.validateParams(decoded.value.params);
796
+ if (!params.ok) {
797
+ return contractFailure(
798
+ "invalid_params",
799
+ `Invalid parameters for ${capability.method}`,
800
+ params.issues,
801
+ );
802
+ }
803
+ const normalizedParams = validateJsonValue(params.value);
804
+ if (!normalizedParams.ok) {
805
+ return contractFailure(
806
+ "invalid_params",
807
+ `Parameter validator for ${capability.method} produced non-JSON data`,
808
+ normalizedParams.issues,
809
+ );
810
+ }
811
+ const invocation = {
812
+ request: decoded.value,
813
+ capability,
814
+ params: normalizedParams.value,
815
+ } as Omit<ValidatedBridgeInvocation, typeof VALIDATED_INVOCATION> & {
816
+ [VALIDATED_INVOCATION]?: true;
817
+ };
818
+ Object.defineProperty(invocation, VALIDATED_INVOCATION, {
819
+ enumerable: false,
820
+ value: true,
821
+ });
822
+ return { ok: true, value: Object.freeze(invocation) as ValidatedBridgeInvocation };
823
+ }
824
+
825
+ const TRUSTED_CONFIRMATION = Symbol("trusted-outer-confirmation");
826
+ const CONFIRMED_REQUEST = Symbol("confirmed-request-fingerprint");
827
+
828
+ export interface TrustedOuterConfirmation {
829
+ readonly source: "trusted-outer";
830
+ readonly requestId: string;
831
+ readonly method: string;
832
+ readonly pageRevision: string;
833
+ readonly confirmedAtMs: number;
834
+ readonly expiresAtMs: number;
835
+ readonly humanSummary: string;
836
+ readonly [TRUSTED_CONFIRMATION]: true;
837
+ readonly [CONFIRMED_REQUEST]: string;
838
+ }
839
+
840
+ export interface TrustedOuterConfirmationOptions {
841
+ readonly confirmedAtMs: number;
842
+ readonly expiresAtMs: number;
843
+ /** Trusted chrome may replace the capability's generic summary. */
844
+ readonly humanSummary?: string;
845
+ }
846
+
847
+ function invocationFingerprint(invocation: ValidatedBridgeInvocation): string {
848
+ return JSON.stringify({
849
+ id: invocation.request.id,
850
+ method: invocation.request.method,
851
+ params: invocation.request.params,
852
+ pageRevision: invocation.request.pageRevision,
853
+ });
854
+ }
855
+
856
+ export function createTrustedOuterConfirmation(
857
+ invocation: ValidatedBridgeInvocation,
858
+ options: TrustedOuterConfirmationOptions,
859
+ ): TrustedOuterConfirmation {
860
+ if (invocation[VALIDATED_INVOCATION] !== true) {
861
+ throw new TypeError("Confirmation requires a validated bridge invocation");
862
+ }
863
+ if (
864
+ !Number.isSafeInteger(options.confirmedAtMs) ||
865
+ !Number.isSafeInteger(options.expiresAtMs) ||
866
+ options.confirmedAtMs < 0 ||
867
+ options.expiresAtMs <= options.confirmedAtMs ||
868
+ options.expiresAtMs - options.confirmedAtMs > BRIDGE_MAX_CONFIRMATION_TTL_MS
869
+ ) {
870
+ throw new TypeError("Invalid trusted confirmation lifetime");
871
+ }
872
+ const generated = invocation.capability.summarize?.(invocation.params) ??
873
+ invocation.capability.description;
874
+ const summary = options.humanSummary ?? generated;
875
+ if (typeof summary !== "string" || summary.trim().length === 0 || summary.length > 512) {
876
+ throw new TypeError("Invalid trusted confirmation summary");
877
+ }
878
+ const confirmation = {
879
+ source: "trusted-outer" as const,
880
+ requestId: invocation.request.id,
881
+ method: invocation.request.method,
882
+ pageRevision: invocation.request.pageRevision,
883
+ confirmedAtMs: options.confirmedAtMs,
884
+ expiresAtMs: options.expiresAtMs,
885
+ humanSummary: summary,
886
+ } as Omit<TrustedOuterConfirmation, typeof TRUSTED_CONFIRMATION | typeof CONFIRMED_REQUEST> &
887
+ Partial<Pick<TrustedOuterConfirmation, typeof TRUSTED_CONFIRMATION | typeof CONFIRMED_REQUEST>>;
888
+ Object.defineProperties(confirmation, {
889
+ [TRUSTED_CONFIRMATION]: { enumerable: false, value: true },
890
+ [CONFIRMED_REQUEST]: {
891
+ enumerable: false,
892
+ value: invocationFingerprint(invocation),
893
+ },
894
+ });
895
+ return Object.freeze(confirmation) as TrustedOuterConfirmation;
896
+ }
897
+
898
+ export function authorizeBridgeInvocation(
899
+ invocation: ValidatedBridgeInvocation,
900
+ confirmation: unknown,
901
+ nowMs: number,
902
+ ): BridgeContractResult<ValidatedBridgeInvocation> {
903
+ if (invocation.capability.confirmation === "none") {
904
+ return { ok: true, value: invocation };
905
+ }
906
+ if (
907
+ typeof confirmation !== "object" ||
908
+ confirmation === null ||
909
+ (confirmation as Partial<TrustedOuterConfirmation>)[TRUSTED_CONFIRMATION] !== true
910
+ ) {
911
+ return contractFailure(
912
+ "confirmation_required",
913
+ "This action requires confirmation in trusted Thread Page chrome",
914
+ );
915
+ }
916
+ const trusted = confirmation as TrustedOuterConfirmation;
917
+ if (
918
+ !Number.isSafeInteger(nowMs) ||
919
+ nowMs < trusted.confirmedAtMs ||
920
+ nowMs >= trusted.expiresAtMs ||
921
+ trusted.requestId !== invocation.request.id ||
922
+ trusted.method !== invocation.request.method ||
923
+ trusted.pageRevision !== invocation.request.pageRevision ||
924
+ trusted[CONFIRMED_REQUEST] !== invocationFingerprint(invocation)
925
+ ) {
926
+ return contractFailure(
927
+ "confirmation_invalid",
928
+ "Trusted confirmation is expired or does not match this request",
929
+ );
930
+ }
931
+ return { ok: true, value: invocation };
932
+ }
933
+
934
+ export function completeBridgeInvocation(
935
+ invocation: ValidatedBridgeInvocation,
936
+ result: unknown,
937
+ ): BridgeResponse {
938
+ const validated = invocation.capability.validateResult(result);
939
+ if (!validated.ok) {
940
+ return makeBridgeFailureResponse(
941
+ invocation.request.id,
942
+ "invalid_result",
943
+ `Invalid result for ${invocation.capability.method}`,
944
+ );
945
+ }
946
+ const json = validateJsonValue(validated.value);
947
+ if (!json.ok) {
948
+ return makeBridgeFailureResponse(
949
+ invocation.request.id,
950
+ "invalid_result",
951
+ `Result validator for ${invocation.capability.method} produced non-JSON data`,
952
+ );
953
+ }
954
+ const response: BridgeSuccessResponse = {
955
+ v: 1,
956
+ id: invocation.request.id,
957
+ ok: true,
958
+ result: json.value,
959
+ };
960
+ const encoded = encodeBridgeResponse(response);
961
+ return encoded.ok
962
+ ? response
963
+ : makeBridgeFailureResponse(
964
+ invocation.request.id,
965
+ "response_too_large",
966
+ "Bridge response is too large",
967
+ );
968
+ }
969
+
970
+ export interface ContextGetResult {
971
+ readonly protocolVersion: 1;
972
+ readonly thread: {
973
+ readonly id: string;
974
+ readonly title: string;
975
+ readonly projectId: string | null;
976
+ };
977
+ readonly page: { readonly revision: string; readonly readOnly: boolean };
978
+ readonly capabilities: readonly CapabilityDescriptor[];
979
+ }
980
+
981
+ export interface ThreadActivityParams {
982
+ readonly limit: number;
983
+ }
984
+
985
+ export type ThreadActivityState =
986
+ | "working"
987
+ | "idle"
988
+ | "waiting"
989
+ | "failed"
990
+ | "stopped";
991
+
992
+ export interface ThreadActivityItem {
993
+ readonly kind: string;
994
+ readonly done: boolean;
995
+ readonly atMs: number;
996
+ readonly label: string;
997
+ readonly text: string;
998
+ }
999
+
1000
+ export interface ThreadActivityResult {
1001
+ readonly state: ThreadActivityState;
1002
+ readonly updatedAtMs: number;
1003
+ readonly items: readonly ThreadActivityItem[];
1004
+ }
1005
+
1006
+ export interface ThreadsSnapshotParams {
1007
+ readonly projectId: string | null;
1008
+ readonly includeArchived: boolean;
1009
+ readonly limit: number;
1010
+ readonly cursor: string | null;
1011
+ }
1012
+
1013
+ export type ThreadSnapshotStatus =
1014
+ | "idle"
1015
+ | "active"
1016
+ | "waiting"
1017
+ | "failed"
1018
+ | "stopped";
1019
+
1020
+ export interface ThreadSnapshotItem {
1021
+ readonly id: string;
1022
+ readonly title: string;
1023
+ readonly projectId: string | null;
1024
+ readonly parentThreadId: string | null;
1025
+ readonly status: ThreadSnapshotStatus;
1026
+ readonly archived: boolean;
1027
+ readonly page: { readonly available: boolean; readonly revision: string | null };
1028
+ readonly updatedAtMs: number;
1029
+ }
1030
+
1031
+ export interface ThreadsSnapshotResult {
1032
+ readonly threads: readonly ThreadSnapshotItem[];
1033
+ readonly nextCursor: string | null;
1034
+ readonly generatedAtMs: number;
1035
+ }
1036
+
1037
+ export interface ThreadReplyParams {
1038
+ readonly result: JsonValue;
1039
+ readonly mode: "queue" | "steer";
1040
+ readonly title?: string;
1041
+ readonly idempotencyKey?: string;
1042
+ }
1043
+
1044
+ export interface ThreadDeliveryResult {
1045
+ readonly delivery: "started" | "queued" | "steered";
1046
+ readonly duplicate: boolean;
1047
+ }
1048
+
1049
+ export interface ThreadsContinueParams {
1050
+ readonly threadId: string;
1051
+ readonly prompt: string;
1052
+ readonly mode: "queue" | "steer";
1053
+ }
1054
+
1055
+ export interface ThreadsContinueResult extends ThreadDeliveryResult {
1056
+ readonly threadId: string;
1057
+ }
1058
+
1059
+ export interface ThreadsSpawnParams {
1060
+ readonly projectId: string;
1061
+ readonly prompt: string;
1062
+ readonly title?: string;
1063
+ readonly providerId?: string;
1064
+ readonly model?: string;
1065
+ readonly reasoningLevel?: string;
1066
+ }
1067
+
1068
+ export interface ThreadTargetParams { readonly threadId: string }
1069
+ export interface OpenResult { readonly opened: boolean }
1070
+ export interface ArchiveResult { readonly archived: boolean }
1071
+ export interface StopResult { readonly stopped: boolean }
1072
+
1073
+ export interface NavigationOpenExternalParams {
1074
+ readonly url: string;
1075
+ /** Presentation hint only; trusted chrome derives authority from `url`. */
1076
+ readonly label?: string;
1077
+ }
1078
+
1079
+ export interface ProjectChoice {
1080
+ readonly id: string;
1081
+ readonly name: string;
1082
+ readonly kind: "standard" | "personal";
1083
+ }
1084
+
1085
+ export interface ProjectsBrowseParams {
1086
+ readonly startProjectId: string | null;
1087
+ }
1088
+
1089
+ export interface ProjectsBrowseResult {
1090
+ readonly selection: null | {
1091
+ readonly token: string;
1092
+ readonly displayPath: string;
1093
+ readonly hostName: string;
1094
+ };
1095
+ }
1096
+
1097
+ export interface ProjectsCreateParams {
1098
+ readonly selectionToken: string;
1099
+ readonly name?: string;
1100
+ }
1101
+
1102
+ export interface ProviderChoice {
1103
+ readonly id: string;
1104
+ readonly displayName: string;
1105
+ readonly available: boolean;
1106
+ readonly models: readonly {
1107
+ readonly id: string;
1108
+ readonly displayName: string;
1109
+ }[];
1110
+ }
1111
+
1112
+ function title(value: JsonValue, path: string): BridgeValidationResult<string> {
1113
+ return stringValue(value, path, { max: MAX_TITLE_LENGTH, label: "Title" });
1114
+ }
1115
+
1116
+ function prompt(value: JsonValue, path: string): BridgeValidationResult<string> {
1117
+ return stringValue(value, path, {
1118
+ min: 1,
1119
+ max: MAX_PROMPT_LENGTH,
1120
+ label: "Prompt",
1121
+ });
1122
+ }
1123
+
1124
+ function timestamp(value: JsonValue, path: string): BridgeValidationResult<number> {
1125
+ return integerValue(value, path, 0, Number.MAX_SAFE_INTEGER);
1126
+ }
1127
+
1128
+ function parseCapabilityDescriptor(
1129
+ value: JsonValue,
1130
+ path: string,
1131
+ ): BridgeValidationResult<CapabilityDescriptor> {
1132
+ const object = asObject(value, ["method", "effect", "confirmation"], ["method", "effect", "confirmation"], path);
1133
+ if (!object.ok) return object;
1134
+ const method = stringValue(object.value.method!, `${path}.method`, {
1135
+ min: 3,
1136
+ max: BRIDGE_MAX_METHOD_LENGTH,
1137
+ pattern: METHOD_PATTERN,
1138
+ label: "Method name",
1139
+ });
1140
+ if (!method.ok) return method;
1141
+ const effect = enumValue(object.value.effect!, `${path}.effect`, EFFECTS);
1142
+ if (!effect.ok) return effect;
1143
+ const confirmation = enumValue(
1144
+ object.value.confirmation!,
1145
+ `${path}.confirmation`,
1146
+ ["none", "trusted-outer"] as const,
1147
+ );
1148
+ if (!confirmation.ok) return confirmation;
1149
+ return valid({ method: method.value, effect: effect.value, confirmation: confirmation.value });
1150
+ }
1151
+
1152
+ function parseContextResult(value: JsonValue): BridgeValidationResult<ContextGetResult> {
1153
+ const root = asObject(value, ["protocolVersion", "thread", "page", "capabilities"], ["protocolVersion", "thread", "page", "capabilities"]);
1154
+ if (!root.ok) return root;
1155
+ if (root.value.protocolVersion !== 1) return invalid("$.protocolVersion", "Expected protocol version 1");
1156
+ const thread = asObject(root.value.thread!, ["id", "title", "projectId"], ["id", "title", "projectId"], "$.thread");
1157
+ if (!thread.ok) return thread;
1158
+ const threadId = entityId(thread.value.id!, "$.thread.id");
1159
+ if (!threadId.ok) return threadId;
1160
+ const threadTitle = title(thread.value.title!, "$.thread.title");
1161
+ if (!threadTitle.ok) return threadTitle;
1162
+ const projectId = nullableEntityId(thread.value.projectId!, "$.thread.projectId");
1163
+ if (!projectId.ok) return projectId;
1164
+ const page = asObject(root.value.page!, ["revision", "readOnly"], ["revision", "readOnly"], "$.page");
1165
+ if (!page.ok) return page;
1166
+ const revision = stringValue(page.value.revision!, "$.page.revision", {
1167
+ min: 1,
1168
+ max: BRIDGE_MAX_PAGE_REVISION_LENGTH,
1169
+ pattern: ID_PATTERN,
1170
+ label: "Page revision",
1171
+ });
1172
+ if (!revision.ok) return revision;
1173
+ const readOnly = booleanValue(page.value.readOnly!, "$.page.readOnly");
1174
+ if (!readOnly.ok) return readOnly;
1175
+ if (!Array.isArray(root.value.capabilities) || root.value.capabilities.length > 64) {
1176
+ return invalid("$.capabilities", "Expected at most 64 capabilities");
1177
+ }
1178
+ const capabilities: CapabilityDescriptor[] = [];
1179
+ for (let index = 0; index < root.value.capabilities.length; index += 1) {
1180
+ const item = parseCapabilityDescriptor(root.value.capabilities[index]!, `$.capabilities[${index}]`);
1181
+ if (!item.ok) return item;
1182
+ capabilities.push(item.value);
1183
+ }
1184
+ return valid({
1185
+ protocolVersion: 1,
1186
+ thread: { id: threadId.value, title: threadTitle.value, projectId: projectId.value },
1187
+ page: { revision: revision.value, readOnly: readOnly.value },
1188
+ capabilities,
1189
+ });
1190
+ }
1191
+
1192
+ function parseActivityParams(value: JsonValue): BridgeValidationResult<ThreadActivityParams> {
1193
+ const object = asObject(value, ["limit"], []);
1194
+ if (!object.ok) return object;
1195
+ const limit = object.value.limit === undefined
1196
+ ? valid(8)
1197
+ : integerValue(object.value.limit, "$.limit", 1, 20);
1198
+ return limit.ok ? valid({ limit: limit.value }) : limit;
1199
+ }
1200
+
1201
+ function parseActivityItem(
1202
+ value: JsonValue,
1203
+ path: string,
1204
+ ): BridgeValidationResult<ThreadActivityItem> {
1205
+ const object = asObject(
1206
+ value,
1207
+ ["kind", "done", "atMs", "label", "text"],
1208
+ ["kind", "done", "atMs", "label", "text"],
1209
+ path,
1210
+ );
1211
+ if (!object.ok) return object;
1212
+ const kind = stringValue(object.value.kind!, path + ".kind", {
1213
+ min: 1,
1214
+ max: 80,
1215
+ label: "Activity kind",
1216
+ });
1217
+ if (!kind.ok) return kind;
1218
+ const done = booleanValue(object.value.done!, path + ".done");
1219
+ if (!done.ok) return done;
1220
+ const atMs = timestamp(object.value.atMs!, path + ".atMs");
1221
+ if (!atMs.ok) return atMs;
1222
+ const label = stringValue(object.value.label!, path + ".label", {
1223
+ min: 1,
1224
+ max: 80,
1225
+ label: "Activity label",
1226
+ });
1227
+ if (!label.ok) return label;
1228
+ const text = stringValue(object.value.text!, path + ".text", {
1229
+ max: 200,
1230
+ label: "Activity text",
1231
+ });
1232
+ if (!text.ok) return text;
1233
+ return valid({
1234
+ kind: kind.value,
1235
+ done: done.value,
1236
+ atMs: atMs.value,
1237
+ label: label.value,
1238
+ text: text.value,
1239
+ });
1240
+ }
1241
+
1242
+ function parseActivityResult(
1243
+ value: JsonValue,
1244
+ ): BridgeValidationResult<ThreadActivityResult> {
1245
+ const object = asObject(
1246
+ value,
1247
+ ["state", "updatedAtMs", "items"],
1248
+ ["state", "updatedAtMs", "items"],
1249
+ );
1250
+ if (!object.ok) return object;
1251
+ const state = enumValue(
1252
+ object.value.state!,
1253
+ "$.state",
1254
+ ["working", "idle", "waiting", "failed", "stopped"] as const,
1255
+ );
1256
+ if (!state.ok) return state;
1257
+ const updatedAtMs = timestamp(object.value.updatedAtMs!, "$.updatedAtMs");
1258
+ if (!updatedAtMs.ok) return updatedAtMs;
1259
+ if (!Array.isArray(object.value.items) || object.value.items.length > 20) {
1260
+ return invalid("$.items", "Expected at most 20 activity items");
1261
+ }
1262
+ const items: ThreadActivityItem[] = [];
1263
+ for (let index = 0; index < object.value.items.length; index += 1) {
1264
+ const item = parseActivityItem(
1265
+ object.value.items[index]!,
1266
+ "$.items[" + index + "]",
1267
+ );
1268
+ if (!item.ok) return item;
1269
+ items.push(item.value);
1270
+ }
1271
+ return valid({ state: state.value, updatedAtMs: updatedAtMs.value, items });
1272
+ }
1273
+
1274
+ function parseSnapshotParams(value: JsonValue): BridgeValidationResult<ThreadsSnapshotParams> {
1275
+ const object = asObject(value, ["projectId", "includeArchived", "limit", "cursor"], []);
1276
+ if (!object.ok) return object;
1277
+ const projectId = object.value.projectId === undefined
1278
+ ? valid<string | null>(null)
1279
+ : nullableEntityId(object.value.projectId, "$.projectId");
1280
+ if (!projectId.ok) return projectId;
1281
+ const includeArchived = object.value.includeArchived === undefined
1282
+ ? valid(false)
1283
+ : booleanValue(object.value.includeArchived, "$.includeArchived");
1284
+ if (!includeArchived.ok) return includeArchived;
1285
+ const limit = object.value.limit === undefined
1286
+ ? valid(100)
1287
+ : integerValue(object.value.limit, "$.limit", 1, MAX_ITEMS);
1288
+ if (!limit.ok) return limit;
1289
+ const cursor = object.value.cursor === undefined || object.value.cursor === null
1290
+ ? valid<string | null>(null)
1291
+ : opaqueToken(object.value.cursor, "$.cursor");
1292
+ if (!cursor.ok) return cursor;
1293
+ return valid({ projectId: projectId.value, includeArchived: includeArchived.value, limit: limit.value, cursor: cursor.value });
1294
+ }
1295
+
1296
+ function parseThreadSnapshotItem(value: JsonValue, path: string): BridgeValidationResult<ThreadSnapshotItem> {
1297
+ const object = asObject(value, ["id", "title", "projectId", "parentThreadId", "status", "archived", "page", "updatedAtMs"], ["id", "title", "projectId", "parentThreadId", "status", "archived", "page", "updatedAtMs"], path);
1298
+ if (!object.ok) return object;
1299
+ const id = entityId(object.value.id!, `${path}.id`); if (!id.ok) return id;
1300
+ const itemTitle = title(object.value.title!, `${path}.title`); if (!itemTitle.ok) return itemTitle;
1301
+ const projectId = nullableEntityId(object.value.projectId!, `${path}.projectId`); if (!projectId.ok) return projectId;
1302
+ const parentThreadId = nullableEntityId(object.value.parentThreadId!, `${path}.parentThreadId`); if (!parentThreadId.ok) return parentThreadId;
1303
+ const status = enumValue(object.value.status!, `${path}.status`, ["idle", "active", "waiting", "failed", "stopped"] as const); if (!status.ok) return status;
1304
+ const archived = booleanValue(object.value.archived!, `${path}.archived`); if (!archived.ok) return archived;
1305
+ const page = asObject(object.value.page!, ["available", "revision"], ["available", "revision"], `${path}.page`); if (!page.ok) return page;
1306
+ const available = booleanValue(page.value.available!, `${path}.page.available`); if (!available.ok) return available;
1307
+ const revision = page.value.revision === null ? valid<string | null>(null) : stringValue(page.value.revision!, `${path}.page.revision`, { min: 1, max: BRIDGE_MAX_PAGE_REVISION_LENGTH, pattern: ID_PATTERN, label: "Page revision" }); if (!revision.ok) return revision;
1308
+ const updatedAtMs = timestamp(object.value.updatedAtMs!, `${path}.updatedAtMs`); if (!updatedAtMs.ok) return updatedAtMs;
1309
+ return valid({ id: id.value, title: itemTitle.value, projectId: projectId.value, parentThreadId: parentThreadId.value, status: status.value, archived: archived.value, page: { available: available.value, revision: revision.value }, updatedAtMs: updatedAtMs.value });
1310
+ }
1311
+
1312
+ function parseSnapshotResult(value: JsonValue): BridgeValidationResult<ThreadsSnapshotResult> {
1313
+ const object = asObject(value, ["threads", "nextCursor", "generatedAtMs"], ["threads", "nextCursor", "generatedAtMs"]);
1314
+ if (!object.ok) return object;
1315
+ if (!Array.isArray(object.value.threads) || object.value.threads.length > MAX_ITEMS) return invalid("$.threads", `Expected at most ${MAX_ITEMS} threads`);
1316
+ const threads: ThreadSnapshotItem[] = [];
1317
+ for (let index = 0; index < object.value.threads.length; index += 1) {
1318
+ const item = parseThreadSnapshotItem(object.value.threads[index]!, `$.threads[${index}]`); if (!item.ok) return item; threads.push(item.value);
1319
+ }
1320
+ const nextCursor = object.value.nextCursor === null ? valid<string | null>(null) : opaqueToken(object.value.nextCursor!, "$.nextCursor"); if (!nextCursor.ok) return nextCursor;
1321
+ const generatedAtMs = timestamp(object.value.generatedAtMs!, "$.generatedAtMs"); if (!generatedAtMs.ok) return generatedAtMs;
1322
+ return valid({ threads, nextCursor: nextCursor.value, generatedAtMs: generatedAtMs.value });
1323
+ }
1324
+
1325
+ function parseReplyParams(value: JsonValue): BridgeValidationResult<ThreadReplyParams> {
1326
+ const object = asObject(value, ["result", "mode", "title", "idempotencyKey"], ["result"]); if (!object.ok) return object;
1327
+ const result = validateJsonValue(object.value.result); if (!result.ok) return result;
1328
+ const mode = object.value.mode === undefined ? valid<"queue" | "steer">("queue") : enumValue(object.value.mode, "$.mode", ["queue", "steer"] as const); if (!mode.ok) return mode;
1329
+ const replyTitle = object.value.title === undefined ? undefined : title(object.value.title, "$.title"); if (replyTitle && !replyTitle.ok) return replyTitle;
1330
+ const idempotencyKey = object.value.idempotencyKey === undefined ? undefined : stringValue(object.value.idempotencyKey, "$.idempotencyKey", { min: 1, max: BRIDGE_MAX_ID_LENGTH, pattern: ID_PATTERN, label: "Idempotency key" }); if (idempotencyKey && !idempotencyKey.ok) return idempotencyKey;
1331
+ return valid({ result: result.value, mode: mode.value, ...(replyTitle ? { title: replyTitle.value } : {}), ...(idempotencyKey ? { idempotencyKey: idempotencyKey.value } : {}) });
1332
+ }
1333
+
1334
+ function parseDeliveryResult(value: JsonValue): BridgeValidationResult<ThreadDeliveryResult> {
1335
+ const object = asObject(value, ["delivery", "duplicate"], ["delivery", "duplicate"]); if (!object.ok) return object;
1336
+ const delivery = enumValue(object.value.delivery!, "$.delivery", ["started", "queued", "steered"] as const); if (!delivery.ok) return delivery;
1337
+ const duplicate = booleanValue(object.value.duplicate!, "$.duplicate"); if (!duplicate.ok) return duplicate;
1338
+ return valid({ delivery: delivery.value, duplicate: duplicate.value });
1339
+ }
1340
+
1341
+ function parseContinueParams(value: JsonValue): BridgeValidationResult<ThreadsContinueParams> {
1342
+ const object = asObject(value, ["threadId", "prompt", "mode"], ["threadId", "prompt"]); if (!object.ok) return object;
1343
+ const threadId = entityId(object.value.threadId!, "$.threadId"); if (!threadId.ok) return threadId;
1344
+ const text = prompt(object.value.prompt!, "$.prompt"); if (!text.ok) return text;
1345
+ const mode = object.value.mode === undefined ? valid<"queue" | "steer">("queue") : enumValue(object.value.mode, "$.mode", ["queue", "steer"] as const); if (!mode.ok) return mode;
1346
+ return valid({ threadId: threadId.value, prompt: text.value, mode: mode.value });
1347
+ }
1348
+
1349
+ function parseContinueResult(value: JsonValue): BridgeValidationResult<ThreadsContinueResult> {
1350
+ const object = asObject(value, ["threadId", "delivery", "duplicate"], ["threadId", "delivery", "duplicate"]); if (!object.ok) return object;
1351
+ const threadId = entityId(object.value.threadId!, "$.threadId"); if (!threadId.ok) return threadId;
1352
+ const delivery = parseDeliveryResult({ delivery: object.value.delivery!, duplicate: object.value.duplicate! }); if (!delivery.ok) return delivery;
1353
+ return valid({ threadId: threadId.value, ...delivery.value });
1354
+ }
1355
+
1356
+ function optionalSafeName(value: JsonValue, path: string, max = 160): BridgeValidationResult<string> {
1357
+ return stringValue(value, path, { min: 1, max, pattern: /^[^\u0000-\u001f\u007f]+$/, label: "Name" });
1358
+ }
1359
+
1360
+ function parseSpawnParams(value: JsonValue): BridgeValidationResult<ThreadsSpawnParams> {
1361
+ const object = asObject(value, ["projectId", "prompt", "title", "providerId", "model", "reasoningLevel"], ["projectId", "prompt"]); if (!object.ok) return object;
1362
+ const projectId = entityId(object.value.projectId!, "$.projectId"); if (!projectId.ok) return projectId;
1363
+ const text = prompt(object.value.prompt!, "$.prompt"); if (!text.ok) return text;
1364
+ const threadTitle = object.value.title === undefined ? undefined : title(object.value.title, "$.title"); if (threadTitle && !threadTitle.ok) return threadTitle;
1365
+ const providerId = object.value.providerId === undefined ? undefined : entityId(object.value.providerId, "$.providerId"); if (providerId && !providerId.ok) return providerId;
1366
+ const model = object.value.model === undefined ? undefined : optionalSafeName(object.value.model, "$.model"); if (model && !model.ok) return model;
1367
+ const reasoningLevel = object.value.reasoningLevel === undefined ? undefined : entityId(object.value.reasoningLevel, "$.reasoningLevel"); if (reasoningLevel && !reasoningLevel.ok) return reasoningLevel;
1368
+ return valid({ projectId: projectId.value, prompt: text.value, ...(threadTitle ? { title: threadTitle.value } : {}), ...(providerId ? { providerId: providerId.value } : {}), ...(model ? { model: model.value } : {}), ...(reasoningLevel ? { reasoningLevel: reasoningLevel.value } : {}) });
1369
+ }
1370
+
1371
+ function parseThreadTarget(value: JsonValue): BridgeValidationResult<ThreadTargetParams> {
1372
+ const object = asObject(value, ["threadId"], ["threadId"]); if (!object.ok) return object;
1373
+ const threadId = entityId(object.value.threadId!, "$.threadId"); return threadId.ok ? valid({ threadId: threadId.value }) : threadId;
1374
+ }
1375
+
1376
+ function parseBooleanResult(key: "opened", value: JsonValue): BridgeValidationResult<OpenResult>;
1377
+ function parseBooleanResult(key: "archived", value: JsonValue): BridgeValidationResult<ArchiveResult>;
1378
+ function parseBooleanResult(key: "stopped", value: JsonValue): BridgeValidationResult<StopResult>;
1379
+ function parseBooleanResult(key: "opened" | "archived" | "stopped", value: JsonValue): BridgeValidationResult<OpenResult | ArchiveResult | StopResult> {
1380
+ const object = asObject(value, [key], [key]); if (!object.ok) return object;
1381
+ const flag = booleanValue(object.value[key]!, `$.${key}`);
1382
+ if (!flag.ok) return flag;
1383
+ if (key === "opened") return valid({ opened: flag.value });
1384
+ if (key === "archived") return valid({ archived: flag.value });
1385
+ return valid({ stopped: flag.value });
1386
+ }
1387
+
1388
+ function parseExternalHttpUrl(
1389
+ value: JsonValue,
1390
+ path: string,
1391
+ ): BridgeValidationResult<string> {
1392
+ const bounded = stringValue(value, path, {
1393
+ min: 1,
1394
+ max: 2_048,
1395
+ pattern: /^[^\u0000-\u0020\u007f]+$/,
1396
+ label: "External URL",
1397
+ });
1398
+ if (!bounded.ok) return bounded;
1399
+ let parsed: URL;
1400
+ try {
1401
+ parsed = new URL(bounded.value);
1402
+ } catch {
1403
+ return invalid(path, "Expected an absolute http or https URL");
1404
+ }
1405
+ if (
1406
+ (parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
1407
+ !parsed.hostname ||
1408
+ parsed.username !== "" ||
1409
+ parsed.password !== ""
1410
+ ) {
1411
+ return invalid(path, "Expected an absolute http or https URL without credentials");
1412
+ }
1413
+ return valid(bounded.value);
1414
+ }
1415
+
1416
+ function parseOpenExternalParams(
1417
+ value: JsonValue,
1418
+ ): BridgeValidationResult<NavigationOpenExternalParams> {
1419
+ const object = asObject(value, ["url", "label"], ["url"]);
1420
+ if (!object.ok) return object;
1421
+ const url = parseExternalHttpUrl(object.value.url!, "$.url");
1422
+ if (!url.ok) return url;
1423
+ const label = object.value.label === undefined
1424
+ ? undefined
1425
+ : stringValue(object.value.label, "$.label", {
1426
+ min: 1,
1427
+ max: 160,
1428
+ label: "Target label",
1429
+ });
1430
+ if (label && !label.ok) return label;
1431
+ return valid({ url: url.value, ...(label ? { label: label.value } : {}) });
1432
+ }
1433
+
1434
+ function parseProjectChoice(value: JsonValue, path: string): BridgeValidationResult<ProjectChoice> {
1435
+ const object = asObject(value, ["id", "name", "kind"], ["id", "name", "kind"], path); if (!object.ok) return object;
1436
+ const id = entityId(object.value.id!, `${path}.id`); if (!id.ok) return id;
1437
+ const name = title(object.value.name!, `${path}.name`); if (!name.ok) return name;
1438
+ const kind = enumValue(object.value.kind!, `${path}.kind`, ["standard", "personal"] as const); if (!kind.ok) return kind;
1439
+ return valid({ id: id.value, name: name.value, kind: kind.value });
1440
+ }
1441
+
1442
+ function parseProjectsResult(value: JsonValue): BridgeValidationResult<{ projects: readonly ProjectChoice[] }> {
1443
+ const object = asObject(value, ["projects"], ["projects"]); if (!object.ok) return object;
1444
+ if (!Array.isArray(object.value.projects) || object.value.projects.length > MAX_ITEMS) return invalid("$.projects", `Expected at most ${MAX_ITEMS} projects`);
1445
+ const projects: ProjectChoice[] = [];
1446
+ for (let index = 0; index < object.value.projects.length; index += 1) { const item = parseProjectChoice(object.value.projects[index]!, `$.projects[${index}]`); if (!item.ok) return item; projects.push(item.value); }
1447
+ return valid({ projects });
1448
+ }
1449
+
1450
+ function parseBrowseParams(value: JsonValue): BridgeValidationResult<ProjectsBrowseParams> {
1451
+ const object = asObject(value, ["startProjectId"], []); if (!object.ok) return object;
1452
+ const startProjectId = object.value.startProjectId === undefined || object.value.startProjectId === null ? valid<string | null>(null) : entityId(object.value.startProjectId, "$.startProjectId");
1453
+ return startProjectId.ok ? valid({ startProjectId: startProjectId.value }) : startProjectId;
1454
+ }
1455
+
1456
+ function parseBrowseResult(value: JsonValue): BridgeValidationResult<ProjectsBrowseResult> {
1457
+ const object = asObject(value, ["selection"], ["selection"]); if (!object.ok) return object;
1458
+ if (object.value.selection === null) return valid({ selection: null });
1459
+ const selection = asObject(object.value.selection!, ["token", "displayPath", "hostName"], ["token", "displayPath", "hostName"], "$.selection"); if (!selection.ok) return selection;
1460
+ const token = opaqueToken(selection.value.token!, "$.selection.token"); if (!token.ok) return token;
1461
+ const displayPath = stringValue(selection.value.displayPath!, "$.selection.displayPath", { min: 1, max: 1024, label: "Display path" }); if (!displayPath.ok) return displayPath;
1462
+ const hostName = title(selection.value.hostName!, "$.selection.hostName"); if (!hostName.ok) return hostName;
1463
+ return valid({ selection: { token: token.value, displayPath: displayPath.value, hostName: hostName.value } });
1464
+ }
1465
+
1466
+ function parseCreateProjectParams(value: JsonValue): BridgeValidationResult<ProjectsCreateParams> {
1467
+ const object = asObject(value, ["selectionToken", "name"], ["selectionToken"]); if (!object.ok) return object;
1468
+ const selectionToken = opaqueToken(object.value.selectionToken!, "$.selectionToken"); if (!selectionToken.ok) return selectionToken;
1469
+ const name = object.value.name === undefined ? undefined : title(object.value.name, "$.name"); if (name && !name.ok) return name;
1470
+ return valid({ selectionToken: selectionToken.value, ...(name ? { name: name.value } : {}) });
1471
+ }
1472
+
1473
+ function parseProviderChoice(value: JsonValue, path: string): BridgeValidationResult<ProviderChoice> {
1474
+ const object = asObject(value, ["id", "displayName", "available", "models"], ["id", "displayName", "available", "models"], path); if (!object.ok) return object;
1475
+ const id = entityId(object.value.id!, `${path}.id`); if (!id.ok) return id;
1476
+ const displayName = title(object.value.displayName!, `${path}.displayName`); if (!displayName.ok) return displayName;
1477
+ const available = booleanValue(object.value.available!, `${path}.available`); if (!available.ok) return available;
1478
+ if (!Array.isArray(object.value.models) || object.value.models.length > MAX_ITEMS) return invalid(`${path}.models`, `Expected at most ${MAX_ITEMS} models`);
1479
+ const models: { id: string; displayName: string }[] = [];
1480
+ for (let index = 0; index < object.value.models.length; index += 1) {
1481
+ const model = asObject(object.value.models[index]!, ["id", "displayName"], ["id", "displayName"], `${path}.models[${index}]`); if (!model.ok) return model;
1482
+ const modelId = optionalSafeName(model.value.id!, `${path}.models[${index}].id`); if (!modelId.ok) return modelId;
1483
+ const modelName = title(model.value.displayName!, `${path}.models[${index}].displayName`); if (!modelName.ok) return modelName;
1484
+ models.push({ id: modelId.value, displayName: modelName.value });
1485
+ }
1486
+ return valid({ id: id.value, displayName: displayName.value, available: available.value, models });
1487
+ }
1488
+
1489
+ function parseProvidersResult(value: JsonValue): BridgeValidationResult<{ providers: readonly ProviderChoice[] }> {
1490
+ const object = asObject(value, ["providers"], ["providers"]); if (!object.ok) return object;
1491
+ if (!Array.isArray(object.value.providers) || object.value.providers.length > 64) return invalid("$.providers", "Expected at most 64 providers");
1492
+ const providers: ProviderChoice[] = [];
1493
+ for (let index = 0; index < object.value.providers.length; index += 1) { const item = parseProviderChoice(object.value.providers[index]!, `$.providers[${index}]`); if (!item.ok) return item; providers.push(item.value); }
1494
+ return valid({ providers });
1495
+ }
1496
+
1497
+ function parseStorageKey(value: JsonValue, path = "$.key"): BridgeValidationResult<string> {
1498
+ return stringValue(value, path, { min: 1, max: 128, pattern: STORAGE_KEY_PATTERN, label: "Storage key" });
1499
+ }
1500
+
1501
+ function parseStorageGetParams(value: JsonValue): BridgeValidationResult<{ key: string }> {
1502
+ const object = asObject(value, ["key"], ["key"]); if (!object.ok) return object;
1503
+ const key = parseStorageKey(object.value.key!); return key.ok ? valid({ key: key.value }) : key;
1504
+ }
1505
+
1506
+ function parseStorageGetResult(value: JsonValue): BridgeValidationResult<{ found: false } | { found: true; value: JsonValue }> {
1507
+ if (value === null || Array.isArray(value) || typeof value !== "object" || typeof value.found !== "boolean") return invalid("$.found", "Expected a boolean found flag");
1508
+ const object = value.found ? asObject(value, ["found", "value"], ["found", "value"]) : asObject(value, ["found"], ["found"]); if (!object.ok) return object;
1509
+ if (!value.found) return valid({ found: false });
1510
+ const stored = validateJsonValue(object.value.value, { maxBytes: BRIDGE_MAX_STORAGE_VALUE_BYTES, maxDepth: 12 });
1511
+ return stored.ok ? valid({ found: true, value: stored.value }) : stored;
1512
+ }
1513
+
1514
+ function parseStorageSetParams(value: JsonValue): BridgeValidationResult<{ key: string; value: JsonValue }> {
1515
+ const object = asObject(value, ["key", "value"], ["key", "value"]); if (!object.ok) return object;
1516
+ const key = parseStorageKey(object.value.key!); if (!key.ok) return key;
1517
+ const stored = validateJsonValue(object.value.value, { maxBytes: BRIDGE_MAX_STORAGE_VALUE_BYTES, maxDepth: 12 }); if (!stored.ok) return stored;
1518
+ return valid({ key: key.value, value: stored.value });
1519
+ }
1520
+
1521
+ function parseStoredResult(value: JsonValue): BridgeValidationResult<{ stored: boolean }> {
1522
+ const object = asObject(value, ["stored"], ["stored"]); if (!object.ok) return object;
1523
+ const stored = booleanValue(object.value.stored!, "$.stored"); return stored.ok ? valid({ stored: stored.value }) : stored;
1524
+ }
1525
+
1526
+ function parseVoiceParams(value: JsonValue): BridgeValidationResult<{ language?: string; prompt?: string; maxDurationSeconds: number }> {
1527
+ const object = asObject(value, ["language", "prompt", "maxDurationSeconds"], []); if (!object.ok) return object;
1528
+ const language = object.value.language === undefined ? undefined : stringValue(object.value.language, "$.language", { min: 2, max: 64, pattern: /^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/, label: "Language" }); if (language && !language.ok) return language;
1529
+ const voicePrompt = object.value.prompt === undefined ? undefined : stringValue(object.value.prompt, "$.prompt", { max: 1000, label: "Transcription prompt" }); if (voicePrompt && !voicePrompt.ok) return voicePrompt;
1530
+ const maxDurationSeconds = object.value.maxDurationSeconds === undefined ? valid(120) : integerValue(object.value.maxDurationSeconds, "$.maxDurationSeconds", 1, 120); if (!maxDurationSeconds.ok) return maxDurationSeconds;
1531
+ return valid({ ...(language ? { language: language.value } : {}), ...(voicePrompt ? { prompt: voicePrompt.value } : {}), maxDurationSeconds: maxDurationSeconds.value });
1532
+ }
1533
+
1534
+ function parseVoiceResult(value: JsonValue): BridgeValidationResult<{ text: string }> {
1535
+ const object = asObject(value, ["text"], ["text"]); if (!object.ok) return object;
1536
+ const text = stringValue(object.value.text!, "$.text", { max: MAX_RESULT_TEXT_LENGTH, label: "Transcription" }); return text.ok ? valid({ text: text.value }) : text;
1537
+ }
1538
+
1539
+ function excerpt(text: string): string {
1540
+ const singleLine = text.replace(/\s+/g, " ").trim();
1541
+ return singleLine.length <= 80 ? singleLine : `${singleLine.slice(0, 79)}…`;
1542
+ }
1543
+
1544
+ const strictParitySpecs = [
1545
+ {
1546
+ method: "context.get",
1547
+ description: "Read the current Thread Page context and capability roster.",
1548
+ effect: "read",
1549
+ confirmation: "none",
1550
+ validateParams: jsonValidator(noParams),
1551
+ validateResult: jsonValidator(parseContextResult),
1552
+ },
1553
+ {
1554
+ method: "thread.activity",
1555
+ description: "Read this thread's current state and recent presented activity.",
1556
+ effect: "read",
1557
+ confirmation: "none",
1558
+ validateParams: jsonValidator(parseActivityParams),
1559
+ validateResult: jsonValidator(parseActivityResult),
1560
+ },
1561
+ {
1562
+ method: "threads.snapshot",
1563
+ description: "Read a bounded, projected snapshot of threads and page status.",
1564
+ effect: "read",
1565
+ confirmation: "none",
1566
+ validateParams: jsonValidator(parseSnapshotParams),
1567
+ validateResult: jsonValidator(parseSnapshotResult),
1568
+ },
1569
+ {
1570
+ method: "thread.reply",
1571
+ description: "Reply to the Thread Page's owning thread.",
1572
+ effect: "current-thread-write",
1573
+ confirmation: "none",
1574
+ validateParams: jsonValidator(parseReplyParams),
1575
+ validateResult: jsonValidator(parseDeliveryResult),
1576
+ },
1577
+ {
1578
+ method: "threads.continue",
1579
+ description: "Send a prompt to another existing thread.",
1580
+ effect: "cross-thread-write",
1581
+ confirmation: "trusted-outer",
1582
+ summarize: (params: ThreadsContinueParams) => `Continue thread ${params.threadId}: ${excerpt(params.prompt)}`,
1583
+ validateParams: jsonValidator(parseContinueParams),
1584
+ validateResult: jsonValidator(parseContinueResult),
1585
+ },
1586
+ {
1587
+ method: "threads.spawn",
1588
+ description: "Start a visible root thread in a selected project.",
1589
+ effect: "cross-thread-write",
1590
+ confirmation: "trusted-outer",
1591
+ summarize: (params: ThreadsSpawnParams) => `Start a thread in ${params.projectId}: ${excerpt(params.prompt)}`,
1592
+ validateParams: jsonValidator(parseSpawnParams),
1593
+ validateResult: jsonValidator((value) => {
1594
+ const object = asObject(value, ["threadId"], ["threadId"]); if (!object.ok) return object;
1595
+ const threadId = entityId(object.value.threadId!, "$.threadId"); return threadId.ok ? valid({ threadId: threadId.value }) : threadId;
1596
+ }),
1597
+ },
1598
+ {
1599
+ method: "threads.openPage",
1600
+ description: "Open another Thread Page using trusted client navigation.",
1601
+ effect: "navigation",
1602
+ confirmation: "none",
1603
+ summarize: (params: ThreadTargetParams) => `Open the Thread Page for ${params.threadId}`,
1604
+ validateParams: jsonValidator(parseThreadTarget),
1605
+ validateResult: jsonValidator((value) => parseBooleanResult("opened", value)),
1606
+ },
1607
+ {
1608
+ method: "threads.openBb",
1609
+ description: "Open a thread in the bb application.",
1610
+ effect: "navigation",
1611
+ confirmation: "none",
1612
+ summarize: (params: ThreadTargetParams) => `Open thread ${params.threadId} in bb`,
1613
+ validateParams: jsonValidator(parseThreadTarget),
1614
+ validateResult: jsonValidator((value) => parseBooleanResult("opened", value)),
1615
+ },
1616
+ {
1617
+ method: "threads.stop",
1618
+ description: "Stop the selected thread's active provider runtime.",
1619
+ effect: "destructive",
1620
+ confirmation: "trusted-outer",
1621
+ summarize: (params: ThreadTargetParams) => `Stop thread ${params.threadId}`,
1622
+ validateParams: jsonValidator(parseThreadTarget),
1623
+ validateResult: jsonValidator((value) => parseBooleanResult("stopped", value)),
1624
+ },
1625
+ {
1626
+ method: "threads.archive",
1627
+ description: "Archive a selected thread.",
1628
+ effect: "destructive",
1629
+ confirmation: "trusted-outer",
1630
+ summarize: (params: ThreadTargetParams) => `Archive thread ${params.threadId}`,
1631
+ validateParams: jsonValidator(parseThreadTarget),
1632
+ validateResult: jsonValidator((value) => parseBooleanResult("archived", value)),
1633
+ },
1634
+ {
1635
+ method: "navigation.openExternal",
1636
+ description: "Open an external http or https URL through trusted client chrome.",
1637
+ effect: "navigation",
1638
+ confirmation: "trusted-outer",
1639
+ summarize: (params: NavigationOpenExternalParams) => {
1640
+ const target = new URL(params.url);
1641
+ return `Open ${params.label ? `“${params.label}” at ` : ""}${target.origin}`;
1642
+ },
1643
+ validateParams: jsonValidator(parseOpenExternalParams),
1644
+ validateResult: jsonValidator((value) => parseBooleanResult("opened", value)),
1645
+ },
1646
+ {
1647
+ method: "projects.list",
1648
+ description: "Read safe project choices without host or path details.",
1649
+ effect: "read",
1650
+ confirmation: "none",
1651
+ validateParams: jsonValidator(noParams),
1652
+ validateResult: jsonValidator(parseProjectsResult),
1653
+ },
1654
+ {
1655
+ method: "projects.browse",
1656
+ description: "Open a trusted folder picker and return an opaque selection token.",
1657
+ effect: "device",
1658
+ confirmation: "trusted-outer",
1659
+ summarize: () => "Choose a project folder on this device",
1660
+ validateParams: jsonValidator(parseBrowseParams),
1661
+ validateResult: jsonValidator(parseBrowseResult),
1662
+ },
1663
+ {
1664
+ method: "projects.create",
1665
+ description: "Create a project from a trusted folder-picker selection.",
1666
+ effect: "cross-thread-write",
1667
+ confirmation: "trusted-outer",
1668
+ summarize: (params: ProjectsCreateParams) => `Create project ${params.name ? `“${params.name}”` : "from the selected folder"}`,
1669
+ validateParams: jsonValidator(parseCreateProjectParams),
1670
+ validateResult: jsonValidator((value) => {
1671
+ const object = asObject(value, ["project"], ["project"]); if (!object.ok) return object;
1672
+ const project = parseProjectChoice(object.value.project!, "$.project"); return project.ok ? valid({ project: project.value }) : project;
1673
+ }),
1674
+ },
1675
+ {
1676
+ method: "providers.list",
1677
+ description: "Read available provider and model choices.",
1678
+ effect: "read",
1679
+ confirmation: "none",
1680
+ validateParams: jsonValidator(noParams),
1681
+ validateResult: jsonValidator(parseProvidersResult),
1682
+ },
1683
+ {
1684
+ method: "storage.get",
1685
+ description: "Read small JSON state scoped to the owning Thread Page.",
1686
+ effect: "read",
1687
+ confirmation: "none",
1688
+ validateParams: jsonValidator(parseStorageGetParams),
1689
+ validateResult: jsonValidator(parseStorageGetResult),
1690
+ },
1691
+ {
1692
+ method: "storage.set",
1693
+ description: "Write small JSON state scoped to the owning Thread Page.",
1694
+ effect: "current-thread-write",
1695
+ confirmation: "none",
1696
+ validateParams: jsonValidator(parseStorageSetParams),
1697
+ validateResult: jsonValidator(parseStoredResult),
1698
+ },
1699
+ {
1700
+ method: "voice.captureAndTranscribe",
1701
+ description: "Record and transcribe voice through trusted client chrome.",
1702
+ effect: "device",
1703
+ confirmation: "trusted-outer",
1704
+ summarize: () => "Allow this Thread Page to record and transcribe voice",
1705
+ validateParams: jsonValidator(parseVoiceParams),
1706
+ validateResult: jsonValidator(parseVoiceResult),
1707
+ },
1708
+ ] as const satisfies readonly AnyCapabilitySpec[];
1709
+
1710
+ export const strictParityCapabilityRegistry =
1711
+ createCapabilityRegistry(strictParitySpecs);
1712
+
1713
+ export function capabilityDescriptors(
1714
+ registry: CapabilityRegistry = strictParityCapabilityRegistry,
1715
+ ): readonly CapabilityDescriptor[] {
1716
+ return registry.list().map(({ method, effect, confirmation }) => ({
1717
+ method,
1718
+ effect,
1719
+ confirmation,
1720
+ }));
1721
+ }