@byok-sdk/protocol 0.26.1 → 0.27.0-rc.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.
@@ -1,28 +1,4 @@
1
1
  import { z } from 'zod';
2
- export { INPUT_PREPARATION_WIRE_VERSION } from './input-preparation-version';
3
- /**
4
- * Capability required before a task-free remote input preparation — or a
5
- * prepared Execution — is admitted: `agent-input-preparation-v<N>`, where
6
- * `<N>` is {@link INPUT_PREPARATION_WIRE_VERSION}.
7
- *
8
- * The version is IN the token because the relay wire has none. A device
9
- * declares exactly the one token of the contract it speaks, and a cloud admits
10
- * exactly the one token of the contract it speaks, so a device and a cloud on
11
- * different contract versions never exchange a preparation at all: the cloud
12
- * refuses at enqueue (`agent_capability_missing`, before any receipt or
13
- * mailbox row) instead of relaying a payload the other side would refuse with
14
- * a strict-schema 422 and a device would then redeliver forever. There is no
15
- * dual token and no accepted older one. The retired unversioned token
16
- * `agent-input-preparation` (versions up to 4) is admitted nowhere.
17
- *
18
- * The token gates NEW admissions only. A row enqueued before a version cut is
19
- * still in flight afterwards, and its completion is accepted only by a cloud
20
- * on the same version as the device answering it; across the cut it is a 422
21
- * and a stalled device cursor. Draining in-flight rows and upgrading cloud and
22
- * devices as a pair is the operator precondition for the cut (`docs/spec.md`,
23
- * bounded admission); nothing here parses an older receipt.
24
- */
25
- export declare const AGENT_INPUT_PREPARATION_CAPABILITY: "agent-input-preparation-v9";
26
2
  /** The preparation surface uses the package-wide lowercase `sha256:<hex>` transport form. */
27
3
  export declare const InputPreparationContentHashSchema: z.ZodString;
28
4
  /**
@@ -577,6 +553,13 @@ export declare const InputPreparationArtifactSummarySchema: z.ZodObject<{
577
553
  }, z.core.$strict>;
578
554
  /** The immutable binding a receipt carries and a later consumer must re-present. */
579
555
  export declare const InputPreparationBindingSchema: z.ZodObject<{
556
+ terminalProjection: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
557
+ mode: z.ZodLiteral<"none">;
558
+ }, z.core.$strict>, z.ZodObject<{
559
+ mode: z.ZodLiteral<"result-document">;
560
+ contract: z.ZodString;
561
+ schema: z.ZodCustom<import("./result-schema").ResultSchema, import("./result-schema").ResultSchema>;
562
+ }, z.core.$strict>], "mode">>;
580
563
  agentMemory: z.ZodEnum<{
581
564
  none: "none";
582
565
  read: "read";
@@ -636,6 +619,13 @@ export declare const InputPreparationReceiptSummarySchema: z.ZodObject<{
636
619
  reserved: "reserved";
637
620
  }>;
638
621
  binding: z.ZodObject<{
622
+ terminalProjection: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
623
+ mode: z.ZodLiteral<"none">;
624
+ }, z.core.$strict>, z.ZodObject<{
625
+ mode: z.ZodLiteral<"result-document">;
626
+ contract: z.ZodString;
627
+ schema: z.ZodCustom<import("./result-schema").ResultSchema, import("./result-schema").ResultSchema>;
628
+ }, z.core.$strict>], "mode">>;
639
629
  agentMemory: z.ZodEnum<{
640
630
  none: "none";
641
631
  read: "read";
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { type PlainJsonCheck } from './plain-json';
2
3
  export declare const RuntimeIdSchema: z.ZodEnum<{
3
4
  claude: "claude";
4
5
  codex: "codex";
@@ -372,6 +373,7 @@ export declare const TaskOfferForAgentPayloadSchema: z.ZodObject<{
372
373
  }, z.core.$strict>, z.ZodObject<{
373
374
  mode: z.ZodLiteral<"result-document">;
374
375
  contract: z.ZodString;
376
+ schema: z.ZodCustom<import("./result-schema").ResultSchema, import("./result-schema").ResultSchema>;
375
377
  }, z.core.$strict>], "mode">>;
376
378
  sessionRef: z.ZodOptional<z.ZodString>;
377
379
  limits: z.ZodOptional<z.ZodObject<{
@@ -438,6 +440,7 @@ export declare const TaskOfferForAgentWithEgressPayloadSchema: z.ZodObject<{
438
440
  }, z.core.$strict>, z.ZodObject<{
439
441
  mode: z.ZodLiteral<"result-document">;
440
442
  contract: z.ZodString;
443
+ schema: z.ZodCustom<import("./result-schema").ResultSchema, import("./result-schema").ResultSchema>;
441
444
  }, z.core.$strict>], "mode">>;
442
445
  limits: z.ZodOptional<z.ZodObject<{
443
446
  maxDurationMs: z.ZodOptional<z.ZodNumber>;
@@ -541,6 +544,7 @@ export declare const TaskOfferForAgentWithEgressFreshPayloadSchema: z.ZodObject<
541
544
  }, z.core.$strict>, z.ZodObject<{
542
545
  mode: z.ZodLiteral<"result-document">;
543
546
  contract: z.ZodString;
547
+ schema: z.ZodCustom<import("./result-schema").ResultSchema, import("./result-schema").ResultSchema>;
544
548
  }, z.core.$strict>], "mode">>;
545
549
  limits: z.ZodOptional<z.ZodObject<{
546
550
  maxDurationMs: z.ZodOptional<z.ZodNumber>;
@@ -648,6 +652,7 @@ export declare const TaskOfferPreparedPayloadSchema: z.ZodObject<{
648
652
  }, z.core.$strict>, z.ZodObject<{
649
653
  mode: z.ZodLiteral<"result-document">;
650
654
  contract: z.ZodString;
655
+ schema: z.ZodCustom<import("./result-schema").ResultSchema, import("./result-schema").ResultSchema>;
651
656
  }, z.core.$strict>], "mode">>;
652
657
  limits: z.ZodOptional<z.ZodObject<{
653
658
  maxDurationMs: z.ZodOptional<z.ZodNumber>;
@@ -1222,7 +1227,7 @@ export type TaskStartedPayload = z.infer<typeof TaskStartedPayloadSchema>;
1222
1227
  * writeup): declining does *not* introduce a new `Declined` terminal state.
1223
1228
  * It maps onto the existing `Failed` state via a new `Offered -> Failed`
1224
1229
  * transition. `reason`/`retryable` intentionally mirror `TaskFailPayload`
1225
- * exactly, because a pre-claim decline and a post-claim failure are the same
1230
+ * semantics, because a pre-claim decline and a post-claim failure are the same
1226
1231
  * outcome from the dispatcher's point of view (this attempt produced no
1227
1232
  * result; here's whether retrying — e.g. offering to a different device —
1228
1233
  * makes sense), and keeping the state machine minimal avoids forking every
@@ -1235,6 +1240,10 @@ export declare const TaskDeclinePayloadSchema: z.ZodObject<{
1235
1240
  agentId: z.ZodString;
1236
1241
  profileRevision: z.ZodString;
1237
1242
  }, z.core.$strict>>;
1243
+ code: z.ZodOptional<z.ZodEnum<{
1244
+ agent_home_reader_busy: "agent_home_reader_busy";
1245
+ agent_home_writer_busy: "agent_home_writer_busy";
1246
+ }>>;
1238
1247
  }, z.core.$strip>;
1239
1248
  export type TaskDeclinePayload = z.infer<typeof TaskDeclinePayloadSchema>;
1240
1249
  /**
@@ -1392,21 +1401,7 @@ export declare const RESULT_DOCUMENT_MAX_BYTES = 1048576;
1392
1401
  * in a failure reason. `canonical` is the CANONICAL SNAPSHOT — the value a
1393
1402
  * sender must actually put on the wire; see {@link checkResultDocument}.
1394
1403
  */
1395
- export type ResultDocumentCheck = {
1396
- readonly ok: true;
1397
- readonly bytes: number;
1398
- readonly canonical: unknown;
1399
- } | {
1400
- readonly ok: false;
1401
- readonly reason: 'not-serializable';
1402
- } | {
1403
- readonly ok: false;
1404
- readonly reason: 'over-cap';
1405
- readonly bytes: number;
1406
- } | {
1407
- readonly ok: false;
1408
- readonly reason: 'not-plain-json';
1409
- };
1404
+ export type ResultDocumentCheck = PlainJsonCheck;
1410
1405
  /**
1411
1406
  * THE single authority for "is this a legal `task.complete.document`", and
1412
1407
  * the one place its canonical form is produced. `TaskCompletePayloadSchema`'s
@@ -1444,7 +1439,7 @@ export type ResultDocumentCheck = {
1444
1439
  * no `toJSON`, no getters, no prototype, nothing left that can answer
1445
1440
  * differently a second time.
1446
1441
  * 4. The original must be structurally equal to the snapshot
1447
- * ({@link isSameJsonData}). Any mismatch means the value was not plain
1442
+ * (`isSameJsonData`, `plain-json.ts`). Any mismatch means the value was not plain
1448
1443
  * JSON data, and it is rejected rather than silently transformed.
1449
1444
  *
1450
1445
  * On success the snapshot is returned as `canonical`, and **every sender
@@ -1566,7 +1561,7 @@ export type TerminalAgentHomeMemoryChange = z.infer<typeof TerminalAgentHomeMemo
1566
1561
  /**
1567
1562
  * daemon -> server: task finished successfully.
1568
1563
  *
1569
- * `document` (additive-minor, docs/protocol.md "Freeze rule"): the OPTIONAL
1564
+ * `document` (docs/protocol.md §7.2): the OPTIONAL
1570
1565
  * structured terminal result of the task — one JSON value the product on the
1571
1566
  * other side consumes as the task's actual output, as opposed to `summary`
1572
1567
  * (human-readable prose) or `artifactRefs` (files). Deliberately
@@ -1580,14 +1575,12 @@ export type TerminalAgentHomeMemoryChange = z.infer<typeof TerminalAgentHomeMemo
1580
1575
  * never truncated (see that constant's own doc comment). A sender puts the
1581
1576
  * check's `canonical` snapshot on the wire, never the original object.
1582
1577
  *
1583
- * Unlike `approvalId` on `task.await_approval` above, emitting this field IS
1584
- * gated on a capability flag (`result-document`, `version.ts`): a pre-
1585
- * `result-document` server strips it silently as an unknown key (the
1586
- * tolerant `z.object()` behavior §1 mandates), and silently losing the
1587
- * task's primary structured result is not a tolerable degradation the way
1588
- * losing an observability hint is. So a daemon sends `document` only to a
1589
- * server that advertised the flag, and fails the task loudly otherwise —
1590
- * see `packages/client`'s `task-runner.ts`.
1578
+ * Wire v3: every v3 server keeps this field, so no capability flag gates it
1579
+ * (the v2 `result-document` flag is gone). For a
1580
+ * `terminalProjection.mode: 'result-document'` offer it carries the outcome
1581
+ * the daemon validated against the offer's frozen schema
1582
+ * (`validateResultOutcome`, `result-schema.ts`, at most
1583
+ * `RESULT_OUTCOME_MAX_BYTES`).
1591
1584
  */
1592
1585
  export declare const TaskCompletePayloadSchema: z.ZodObject<{
1593
1586
  harnessId: z.ZodOptional<z.ZodString>;
@@ -1950,6 +1943,7 @@ declare const TASK_OFFER_PAYLOAD_SCHEMAS: {
1950
1943
  }, z.core.$strict>, z.ZodObject<{
1951
1944
  mode: z.ZodLiteral<"result-document">;
1952
1945
  contract: z.ZodString;
1946
+ schema: z.ZodCustom<import("./result-schema").ResultSchema, import("./result-schema").ResultSchema>;
1953
1947
  }, z.core.$strict>], "mode">>;
1954
1948
  sessionRef: z.ZodOptional<z.ZodString>;
1955
1949
  limits: z.ZodOptional<z.ZodObject<{
@@ -2010,6 +2004,7 @@ declare const TASK_OFFER_PAYLOAD_SCHEMAS: {
2010
2004
  }, z.core.$strict>, z.ZodObject<{
2011
2005
  mode: z.ZodLiteral<"result-document">;
2012
2006
  contract: z.ZodString;
2007
+ schema: z.ZodCustom<import("./result-schema").ResultSchema, import("./result-schema").ResultSchema>;
2013
2008
  }, z.core.$strict>], "mode">>;
2014
2009
  limits: z.ZodOptional<z.ZodObject<{
2015
2010
  maxDurationMs: z.ZodOptional<z.ZodNumber>;
@@ -2106,6 +2101,7 @@ declare const TASK_OFFER_PAYLOAD_SCHEMAS: {
2106
2101
  }, z.core.$strict>, z.ZodObject<{
2107
2102
  mode: z.ZodLiteral<"result-document">;
2108
2103
  contract: z.ZodString;
2104
+ schema: z.ZodCustom<import("./result-schema").ResultSchema, import("./result-schema").ResultSchema>;
2109
2105
  }, z.core.$strict>], "mode">>;
2110
2106
  limits: z.ZodOptional<z.ZodObject<{
2111
2107
  maxDurationMs: z.ZodOptional<z.ZodNumber>;
@@ -2192,6 +2188,7 @@ declare const TASK_OFFER_PAYLOAD_SCHEMAS: {
2192
2188
  }, z.core.$strict>, z.ZodObject<{
2193
2189
  mode: z.ZodLiteral<"result-document">;
2194
2190
  contract: z.ZodString;
2191
+ schema: z.ZodCustom<import("./result-schema").ResultSchema, import("./result-schema").ResultSchema>;
2195
2192
  }, z.core.$strict>], "mode">>;
2196
2193
  limits: z.ZodOptional<z.ZodObject<{
2197
2194
  maxDurationMs: z.ZodOptional<z.ZodNumber>;
@@ -2402,6 +2399,7 @@ export declare const MESSAGE_PAYLOAD_SCHEMAS: {
2402
2399
  }, z.core.$strict>, z.ZodObject<{
2403
2400
  mode: z.ZodLiteral<"result-document">;
2404
2401
  contract: z.ZodString;
2402
+ schema: z.ZodCustom<import("./result-schema").ResultSchema, import("./result-schema").ResultSchema>;
2405
2403
  }, z.core.$strict>], "mode">>;
2406
2404
  sessionRef: z.ZodOptional<z.ZodString>;
2407
2405
  limits: z.ZodOptional<z.ZodObject<{
@@ -2462,6 +2460,7 @@ export declare const MESSAGE_PAYLOAD_SCHEMAS: {
2462
2460
  }, z.core.$strict>, z.ZodObject<{
2463
2461
  mode: z.ZodLiteral<"result-document">;
2464
2462
  contract: z.ZodString;
2463
+ schema: z.ZodCustom<import("./result-schema").ResultSchema, import("./result-schema").ResultSchema>;
2465
2464
  }, z.core.$strict>], "mode">>;
2466
2465
  limits: z.ZodOptional<z.ZodObject<{
2467
2466
  maxDurationMs: z.ZodOptional<z.ZodNumber>;
@@ -2558,6 +2557,7 @@ export declare const MESSAGE_PAYLOAD_SCHEMAS: {
2558
2557
  }, z.core.$strict>, z.ZodObject<{
2559
2558
  mode: z.ZodLiteral<"result-document">;
2560
2559
  contract: z.ZodString;
2560
+ schema: z.ZodCustom<import("./result-schema").ResultSchema, import("./result-schema").ResultSchema>;
2561
2561
  }, z.core.$strict>], "mode">>;
2562
2562
  limits: z.ZodOptional<z.ZodObject<{
2563
2563
  maxDurationMs: z.ZodOptional<z.ZodNumber>;
@@ -2644,6 +2644,7 @@ export declare const MESSAGE_PAYLOAD_SCHEMAS: {
2644
2644
  }, z.core.$strict>, z.ZodObject<{
2645
2645
  mode: z.ZodLiteral<"result-document">;
2646
2646
  contract: z.ZodString;
2647
+ schema: z.ZodCustom<import("./result-schema").ResultSchema, import("./result-schema").ResultSchema>;
2647
2648
  }, z.core.$strict>], "mode">>;
2648
2649
  limits: z.ZodOptional<z.ZodObject<{
2649
2650
  maxDurationMs: z.ZodOptional<z.ZodNumber>;
@@ -3113,6 +3114,10 @@ export declare const MESSAGE_PAYLOAD_SCHEMAS: {
3113
3114
  agentId: z.ZodString;
3114
3115
  profileRevision: z.ZodString;
3115
3116
  }, z.core.$strict>>;
3117
+ code: z.ZodOptional<z.ZodEnum<{
3118
+ agent_home_reader_busy: "agent_home_reader_busy";
3119
+ agent_home_writer_busy: "agent_home_writer_busy";
3120
+ }>>;
3116
3121
  }, z.core.$strip>;
3117
3122
  readonly 'task.progress': z.ZodObject<{
3118
3123
  seq: z.ZodNumber;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Plain-JSON-data canonicalization shared by every bounded JSON channel of
3
+ * the wire: `task.complete.document` (`checkResultDocument`, messages.ts) and
4
+ * the result-document schema and outcome (`result-schema.ts`). One rule, one
5
+ * implementation, so the channels cannot disagree about what "plain JSON
6
+ * data" means or how its canonical UTF-8 size is measured.
7
+ *
8
+ * Zod-free and free of node builtins.
9
+ */
10
+ /** Outcome of {@link canonicalizePlainJson} without a depth bound. */
11
+ export type PlainJsonCheck = {
12
+ readonly ok: true;
13
+ readonly bytes: number;
14
+ readonly canonical: unknown;
15
+ } | {
16
+ readonly ok: false;
17
+ readonly reason: 'not-serializable';
18
+ } | {
19
+ readonly ok: false;
20
+ readonly reason: 'over-cap';
21
+ readonly bytes: number;
22
+ } | {
23
+ readonly ok: false;
24
+ readonly reason: 'not-plain-json';
25
+ };
26
+ /** Outcome of {@link canonicalizePlainJson} with a container-nesting bound. */
27
+ export type BoundedPlainJsonCheck = PlainJsonCheck | {
28
+ readonly ok: false;
29
+ readonly reason: 'too-deep';
30
+ readonly bytes: number;
31
+ };
32
+ /**
33
+ * Container nesting depth of pure JSON data (a `JSON.parse` output), computed
34
+ * iteratively so no input can exhaust the call stack. A scalar is depth 0,
35
+ * `{}` and `[]` are depth 1. Stops as soon as `limit` is exceeded.
36
+ */
37
+ export declare function jsonDepthExceeds(value: unknown, limit: number): boolean;
38
+ /**
39
+ * Canonicalize `value` as plain JSON data within `maxBytes` canonical UTF-8
40
+ * bytes:
41
+ *
42
+ * 1. `JSON.stringify` must succeed and not return `undefined`.
43
+ * 2. Its UTF-8 byte length must be at most `maxBytes`.
44
+ * 3. `JSON.parse` that string: the CANONICAL SNAPSHOT (pure data).
45
+ * 4. With `maxDepth`, the snapshot's container nesting must be at most
46
+ * `maxDepth`. This runs before step 5, so the recursive comparison
47
+ * below is bounded by it.
48
+ * 5. The original must be structurally equal to the snapshot
49
+ * (`isSameJsonData`).
50
+ *
51
+ * On success a sender uses `canonical`, never the original reference.
52
+ */
53
+ export declare function canonicalizePlainJson(value: unknown, maxBytes: number): PlainJsonCheck;
54
+ export declare function canonicalizePlainJson(value: unknown, maxBytes: number, maxDepth: number): BoundedPlainJsonCheck;
@@ -1,6 +1,4 @@
1
1
  import { z } from 'zod';
2
- /** Daemon capability required before an exact local provider profile may be offered. */
3
- export declare const PROVIDER_PROFILE_BINDING_CAPABILITY: 'provider-profile-binding';
4
2
  /** Opaque, portable local profile identity. It is one logical id, never a path. */
5
3
  export declare const ProviderProfileRefSchema: z.ZodString;
6
4
  export type ProviderProfileRef = z.infer<typeof ProviderProfileRefSchema>;
@@ -23,8 +23,6 @@ import { z } from 'zod';
23
23
  * Audiences, route paths and product names are Host vocabulary and stay out of
24
24
  * this package.
25
25
  */
26
- /** Device capability required before a Host may enqueue `provider.provisioning.available`. */
27
- export declare const PROVIDER_PROVISIONING_CAPABILITY: 'provider-provisioning.v1';
28
26
  /**
29
27
  * Device-proof `operation` for the signed claim that registers the device's
30
28
  * provider-secret sealing public key (`{ keyId, epoch, publicJwk }`).
@@ -0,0 +1,95 @@
1
+ /** Maximum canonical JSON UTF-8 size of a result-document schema. */
2
+ export declare const RESULT_SCHEMA_MAX_BYTES = 16384;
3
+ /** Maximum canonical JSON UTF-8 size of a submitted outcome document. */
4
+ export declare const RESULT_OUTCOME_MAX_BYTES = 65536;
5
+ /**
6
+ * Maximum subschema nesting below the root (each `properties` member, `items`
7
+ * and `oneOf` branch is one level). With the 16 KiB cap this bounds how much
8
+ * schema structure one validation walks.
9
+ */
10
+ export declare const RESULT_SCHEMA_MAX_DEPTH = 32;
11
+ /**
12
+ * Maximum JSON container nesting of a schema or outcome document, checked
13
+ * iteratively before any recursive routine runs on it.
14
+ */
15
+ export declare const RESULT_JSON_MAX_DEPTH = 128;
16
+ /** Maximum number of issues one failed check reports. */
17
+ export declare const RESULT_ISSUES_MAX_COUNT = 20;
18
+ /** Maximum UTF-8 size of the reported issue list, as JSON. */
19
+ export declare const RESULT_ISSUES_MAX_BYTES = 4096;
20
+ export declare const RESULT_SCHEMA_KEYWORDS: readonly ["type", "properties", "required", "additionalProperties", "items", "enum", "const", "oneOf", "minLength", "maxLength", "minItems", "maxItems", "minimum", "maximum", "description"];
21
+ export type ResultSchemaKeyword = (typeof RESULT_SCHEMA_KEYWORDS)[number];
22
+ export declare const RESULT_SCHEMA_INSTANCE_TYPES: readonly ["object", "array", "string", "number", "integer", "boolean", "null"];
23
+ export type ResultSchemaInstanceType = (typeof RESULT_SCHEMA_INSTANCE_TYPES)[number];
24
+ /** A schema inside the admitted subset. Only {@link checkResultSchema} proves a value is one. */
25
+ export interface ResultSchema {
26
+ readonly type?: ResultSchemaInstanceType | readonly ResultSchemaInstanceType[];
27
+ readonly properties?: {
28
+ readonly [name: string]: ResultSchema;
29
+ };
30
+ readonly required?: readonly string[];
31
+ readonly additionalProperties?: false;
32
+ readonly items?: ResultSchema;
33
+ readonly enum?: readonly unknown[];
34
+ readonly const?: unknown;
35
+ readonly oneOf?: readonly ResultSchema[];
36
+ readonly minLength?: number;
37
+ readonly maxLength?: number;
38
+ readonly minItems?: number;
39
+ readonly maxItems?: number;
40
+ readonly minimum?: number;
41
+ readonly maximum?: number;
42
+ readonly description?: string;
43
+ }
44
+ /**
45
+ * One bounded failure detail. `pointer` is an RFC 6901 JSON Pointer into the
46
+ * checked document: the schema for admission issues, the outcome for
47
+ * validation issues (`''` is the document root). `keyword` is the JSON Schema
48
+ * keyword at fault, or a document-level code: `not-serializable`,
49
+ * `not-plain-json`, `over-cap`, `too-deep` (JSON nesting), `schema` (a
50
+ * subschema that is not an object) or `depth` (subschema nesting).
51
+ */
52
+ export interface ResultIssue {
53
+ readonly pointer: string;
54
+ readonly keyword: string;
55
+ readonly message: string;
56
+ }
57
+ /** Document-level failure shared by the schema and outcome checks. */
58
+ export type ResultDocumentFailureReason = 'not-serializable' | 'not-plain-json' | 'over-cap' | 'too-deep';
59
+ interface ResultFailure<Reason extends string> {
60
+ readonly ok: false;
61
+ readonly reason: Reason;
62
+ /** Canonical UTF-8 size, when it was measured. */
63
+ readonly bytes?: number;
64
+ /** At least one issue; at most {@link RESULT_ISSUES_MAX_COUNT} and {@link RESULT_ISSUES_MAX_BYTES}. */
65
+ readonly issues: readonly ResultIssue[];
66
+ /** True when issues were dropped to respect the caps. */
67
+ readonly truncated: boolean;
68
+ }
69
+ export type ResultSchemaCheck = {
70
+ readonly ok: true;
71
+ readonly bytes: number;
72
+ readonly canonical: ResultSchema;
73
+ } | ResultFailure<ResultDocumentFailureReason | 'not-subset'>;
74
+ export type ResultOutcomeCheck = {
75
+ readonly ok: true;
76
+ readonly bytes: number;
77
+ readonly canonical: unknown;
78
+ } | ResultFailure<'schema-not-admitted' | ResultDocumentFailureReason | 'schema-mismatch'>;
79
+ /**
80
+ * The admission guard for a result-document schema. Accepts only plain JSON
81
+ * data of at most {@link RESULT_SCHEMA_MAX_BYTES} canonical UTF-8 bytes that
82
+ * lies inside the subset (module comment). On success, `canonical` is the
83
+ * snapshot every party freezes and validates against.
84
+ */
85
+ export declare function checkResultSchema(schema: unknown): ResultSchemaCheck;
86
+ /**
87
+ * Validate one outcome document against a result schema. The guard
88
+ * ({@link checkResultSchema}) always runs first; a schema it refuses is
89
+ * `schema-not-admitted` and never reaches the library. The outcome must be
90
+ * plain JSON data of at most {@link RESULT_OUTCOME_MAX_BYTES} canonical UTF-8
91
+ * bytes, checked before validation. On success, `canonical` is the snapshot
92
+ * a sender stages and puts on the wire.
93
+ */
94
+ export declare function validateResultOutcome(schema: unknown, outcome: unknown): ResultOutcomeCheck;
95
+ export {};
@@ -26,5 +26,3 @@
26
26
  * carries this exact literal, so a name legal in only one of the two channels
27
27
  * would leave them naming different things.
28
28
  */
29
- /** Contract §8.1's capability gate for `byok-task-assertion-v1` tool authority. */
30
- export declare const HOST_MCP_TASK_CONTEXT_CAPABILITY: 'host-mcp-task-context';
@@ -1,12 +1,19 @@
1
1
  import { z } from 'zod';
2
- /** Capability required before an offer may select terminal projection behavior. */
3
- export declare const TERMINAL_PROJECTION_SELECTION_CAPABILITY: 'terminal-projection-selection';
2
+ import { type ResultSchema } from './result-schema';
4
3
  export declare const TerminalProjectionContractSchema: z.ZodString;
5
- /** Offer-scoped authority: explicit bypass or one required structured result. */
4
+ /**
5
+ * The frozen outcome schema of a result-document offer (wire v3). The value
6
+ * is kept exactly as sent; {@link checkResultSchema} decides admission, so
7
+ * every decode (Host submit, server/cloud parse, daemon admission) applies
8
+ * the one subset guard and reports its capped issues.
9
+ */
10
+ export declare const ResultDocumentSchemaSchema: z.ZodCustom<ResultSchema, ResultSchema>;
11
+ /** Offer-scoped authority: explicit bypass or one required structured result with its frozen schema. */
6
12
  export declare const TerminalProjectionSelectionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
7
13
  mode: z.ZodLiteral<"none">;
8
14
  }, z.core.$strict>, z.ZodObject<{
9
15
  mode: z.ZodLiteral<"result-document">;
10
16
  contract: z.ZodString;
17
+ schema: z.ZodCustom<ResultSchema, ResultSchema>;
11
18
  }, z.core.$strict>], "mode">;
12
19
  export type TerminalProjectionSelection = z.infer<typeof TerminalProjectionSelectionSchema>;
package/dist/version.d.ts CHANGED
@@ -2,15 +2,22 @@
2
2
  export declare const MAILBOX_READ_AHEAD_MAX_SEQS = 4096;
3
3
  export declare const MAILBOX_READ_AHEAD_CAPABILITY = "mailbox-read-ahead";
4
4
  /**
5
- * Wire protocol version. Bump on breaking (non-additive) changes to the envelope
6
- * or message shapes. Additive changes (new optional fields, new message types)
7
- * do not require a bump — servers negotiate the highest common version and
8
- * daemons/servers must ignore unknown fields and unknown message types.
5
+ * Wire protocol version. Every v3 change is a version bump, a gated addition
6
+ * or a strip-safe addition: see docs/protocol.md "Freeze rule", which is the
7
+ * one statement of the rule, and `src/wire-additions.ts`, which declares the
8
+ * receiver-capability gates and additions. No v3 release has been published
9
+ * yet, so `src/__tests__/compat.test.ts` has no published v3 decoder to prove
10
+ * additions against; cross-release gates stay unproven until one exists.
9
11
  *
10
- * FROZEN (see docs/protocol.md "Freeze rule"): `PROTOCOL_VERSION` does not
11
- * bump for additive changes (new optional fields, new message types, new
12
- * `AgentEvent` variants, new capability flags) — only for a breaking one
13
- * (changing, removing, or retyping anything that already exists).
12
+ * v3 makes `schema` required on the `terminalProjection.mode:
13
+ * 'result-document'` arm (`terminal-projection.ts`, `result-schema.ts`): the
14
+ * offer freezes the outcome schema, and the outcome reaches the Host only as
15
+ * `task.complete.document`. That narrows a strict shape, so it is a wire
16
+ * major. A v2 envelope is rejected; there is no v2 reader and no capability
17
+ * negotiation between the two result-document shapes. Drain, then upgrade
18
+ * server and client together. Every addition v2 declared is part of the v3
19
+ * baseline, and the v2 `result-document` capability flag is gone: every v3
20
+ * server keeps `task.complete.document`.
14
21
  *
15
22
  * v2 removed the task offer's `policy` (`PermissionPolicy`), the runtime
16
23
  * capability `permissionModes` and the input-preparation `permissionMode`.
@@ -23,18 +30,8 @@ export declare const MAILBOX_READ_AHEAD_CAPABILITY = "mailbox-read-ahead";
23
30
  * and the readiness reason `executor_identity_unproven`. The SDK does not
24
31
  * attest tool executables. The artifact names its counted tools in
25
32
  * `toolNames`.
26
- *
27
- * IMPORTANT: changing this constant, or changing/removing/retyping any
28
- * already-frozen schema in this package, requires a DELIBERATE update to the
29
- * committed golden fixtures in `src/__tests__/golden/` (`v<N>.frozen.json`,
30
- * `v<N>.envelopes.ndjson`) — see `src/__tests__/freeze-guard.test.ts`, which
31
- * fails loudly on exactly that kind of drift. A passing freeze-guard run
32
- * after such a change means either (a) the change was genuinely additive and
33
- * the golden was regenerated with justification, or (b) this constant was
34
- * bumped alongside a new golden generation for the new version — never a
35
- * silent edit to either file to make the test pass.
36
33
  */
37
- export declare const PROTOCOL_VERSION = 2;
34
+ export declare const PROTOCOL_VERSION = 3;
38
35
  /** Host declares that this device accepts only Agent-bound offer variants. */
39
36
  export declare const STRICT_AGENT_ONLY_CAPABILITY: 'strict-agent-only';
40
37
  /**
@@ -89,27 +86,6 @@ export declare const AGENT_HOME_READERS_CAPABILITY: 'agent-home-readers';
89
86
  * other flag here, just consumed for observability instead of gating.
90
87
  */
91
88
  /**
92
- * `result-document` (additive-minor): a SERVER-advertised flag meaning "I
93
- * understand the optional `task.complete.document` field" (`messages.ts`).
94
- * Functionally gating, like `approval_resolved` and unlike
95
- * `approval-targeting`.
96
- *
97
- * This is the N/N-1 answer for that new daemon -> server FIELD. An old
98
- * server's transport advertisement (`conn.ack.capabilities` on WS or
99
- * `EventsPollResponse.capabilities` on long-poll) never includes it, and
100
- * its `TaskCompletePayloadSchema` is a tolerant (non-`.strict()`)
101
- * `z.object()`, so a `document` sent to it would be silently STRIPPED on
102
- * parse and vanish without a trace. That is exactly why emission is gated
103
- * here rather than sent unconditionally the way `approvalId` is: `document`
104
- * carries the task's primary structured RESULT, so losing it silently is
105
- * data loss, not a missed observability hint. A new daemon talking to an old
106
- * server therefore never sends `document` at all, and — if its configured
107
- * extractor did produce one — reports `task.fail` (retryable: false; the
108
- * same server will strip it on every retry too) instead of completing the
109
- * task with its main result quietly deleted (`packages/client`'s
110
- * `task-runner.ts`). A new server talking to an old daemon is unaffected:
111
- * the field is optional, and an old daemon simply never sets it.
112
- *
113
89
  * `dispatch-selection` (additive-minor) is a correctness gate for the
114
90
  * optional `task.offer.dispatchSelection` control field. An older v1 daemon
115
91
  * legally strips unknown optional fields, so a server must never send an
@@ -124,5 +100,5 @@ export declare const AGENT_HOME_READERS_CAPABILITY: 'agent-home-readers';
124
100
  * cannot accidentally execute the instruction without the required tools.
125
101
  */
126
102
  export declare const CUSTOM_HARNESS_CAPABILITY: 'custom-harness';
127
- export declare const CAPABILITY_FLAGS: readonly ['steer', 'blob-upload', 'interactive-approval', 'approval_resolved', 'approval-targeting', 'result-document', 'dispatch-selection', "provider-profile-binding", 'toolset-selection', 'agent-home-contract', "agent-home-readers", "strict-agent-only", "agent-egress-policy", "agent-egress-reliable-ack", "agent-message-egress", "agent-egress-fresh-session", "agent-content-workspace-read", "agent-content-transcript-read", "agent-content-artifact-read", "agent-home-projection", "agent-input-preparation-v9", "terminal-projection-selection", "host-mcp-task-context", "custom-harness", "mailbox-read-ahead", "provider-provisioning.v1", "agent-memory-intent.v1"];
103
+ export declare const CAPABILITY_FLAGS: readonly ['steer', 'blob-upload', 'interactive-approval', 'approval_resolved', 'approval-targeting', 'dispatch-selection', "provider-profile-binding", 'toolset-selection', 'agent-home-contract', "agent-home-readers", "strict-agent-only", "agent-egress-policy", "agent-egress-reliable-ack", "agent-message-egress", "agent-egress-fresh-session", "agent-content-workspace-read", "agent-content-transcript-read", "agent-content-artifact-read", "agent-home-projection", "agent-input-preparation-v9", "terminal-projection-selection", "host-mcp-task-context", "custom-harness", "mailbox-read-ahead", "provider-provisioning.v1", "agent-memory-intent.v1"];
128
104
  export type CapabilityFlag = (typeof CAPABILITY_FLAGS)[number];
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The one table of receiver-capability gates and wire additions
3
+ * (docs/protocol.md "Freeze rule").
4
+ *
5
+ * A v3 change takes exactly one of three paths: a version bump, a gated
6
+ * addition, or a strip-safe addition. Envelope and HTTP rows have explicit
7
+ * kinds. The last two are declared here.
8
+ *
9
+ * - `gated`: the sender must not emit the surface unless the receiver
10
+ * durably advertised `capability`. A field entry applies when the payload
11
+ * carries the field (and, with `value`, when the field equals it); a type
12
+ * entry applies to every message of that type. Server-to-daemon surfaces
13
+ * are gated by a device flag, daemon-to-server surfaces by a server flag.
14
+ * - `strip-safe`: an optional field an older receiver drops without changing
15
+ * what it executes or acknowledges.
16
+ *
17
+ * v3 baseline: every v3 decoder reads every surface v2 had, so the v2
18
+ * strip-safe rows and the v2 `result-document` gate are gone. The device
19
+ * gates kept below name capabilities a daemon advertises only when it is
20
+ * configured for them (Agent home, egress, input preparation, provisioning,
21
+ * memory intents and so on): they gate on the receiver's configuration. The
22
+ * server-advertised `approval_resolved` gate is kept as the daemon's sending
23
+ * condition for `task.approval_resolved`. No v3 release has been
24
+ * published, so no row is proven against a published v3 decoder yet
25
+ * (`src/__tests__/compat.test.ts`).
26
+ *
27
+ * Senders derive their admission flags from {@link requiredReceiverCapabilities}
28
+ * and nowhere else, so a gate cannot exist in one place and be missing in
29
+ * another. This module imports no runtime code (types only) so it stays free
30
+ * of zod.
31
+ */
32
+ import type { MessageType } from './messages';
33
+ import type { CapabilityFlag } from './version';
34
+ type HttpWireSurface = 'ChallengeRequest' | 'ChallengeResponse' | 'TokenRequest' | 'TokenResponse' | 'EventsPollQuery' | 'EventsPollResponse' | 'MessagesSendRequest' | 'MessagesSendResponse';
35
+ export type WireAddition = {
36
+ readonly kind: 'envelope';
37
+ readonly rule: 'gated';
38
+ readonly surface: MessageType;
39
+ readonly field?: string;
40
+ readonly value?: string;
41
+ readonly capability: CapabilityFlag;
42
+ readonly advertisedBy: 'device' | 'server';
43
+ } | {
44
+ readonly kind: 'envelope';
45
+ readonly rule: 'strip-safe';
46
+ readonly surface: MessageType;
47
+ readonly field: string;
48
+ } | {
49
+ readonly kind: 'http';
50
+ readonly rule: 'strip-safe';
51
+ readonly surface: HttpWireSurface;
52
+ readonly field: string;
53
+ };
54
+ export declare const WIRE_ADDITIONS: readonly WireAddition[];
55
+ /**
56
+ * Every capability the receiver must have durably advertised before a sender
57
+ * may emit this message. Deduplicated, in table order. A sender refuses (before
58
+ * creating the task or appending to the mailbox) when any is missing.
59
+ */
60
+ export declare function requiredReceiverCapabilities(type: MessageType, payload: unknown): CapabilityFlag[];
61
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byok-sdk/protocol",
3
- "version": "0.26.1",
3
+ "version": "0.27.0-rc.2",
4
4
  "description": "BYOK SDK wire protocol: envelope schema, message types, and codec helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -26,9 +26,9 @@
26
26
  "import": "./dist/index.js"
27
27
  },
28
28
  "./package.json": "./package.json",
29
- "./input-preparation-version": {
30
- "types": "./dist/input-preparation-version.d.ts",
31
- "import": "./dist/input-preparation-version.js"
29
+ "./constants": {
30
+ "types": "./dist/constants.d.ts",
31
+ "import": "./dist/constants.js"
32
32
  }
33
33
  },
34
34
  "files": [
@@ -48,6 +48,10 @@
48
48
  "clean": "rm -rf dist"
49
49
  },
50
50
  "dependencies": {
51
+ "@cfworker/json-schema": "4.1.1",
51
52
  "zod": "^4.4.3"
53
+ },
54
+ "devDependencies": {
55
+ "byok-protocol-previous": "https://registry.npmjs.org/@byok-sdk/protocol/-/protocol-0.26.1.tgz"
52
56
  }
53
57
  }
@@ -1,12 +0,0 @@
1
- /**
2
- * The ONE version of the input-preparation contract, shared by this relay wire
3
- * (the `agent.input.preparation` payload and the completion receipt summary)
4
- * and by the device-local request, receipt and artifact
5
- * (`@byok-sdk/client`'s `INPUT_PREPARATION_VERSION`, which is this value).
6
- *
7
- * The relay wire carries no version field of its own, so a version change is
8
- * made visible where admission actually happens: in the capability token
9
- * below. See `INPUT_PREPARATION_VERSION` in the client for what each version
10
- * changed.
11
- */
12
- export declare const INPUT_PREPARATION_WIRE_VERSION: 9;
@@ -1,6 +0,0 @@
1
- // src/input-preparation-version.ts
2
- var INPUT_PREPARATION_WIRE_VERSION = 9;
3
-
4
- export { INPUT_PREPARATION_WIRE_VERSION };
5
- //# sourceMappingURL=input-preparation-version.js.map
6
- //# sourceMappingURL=input-preparation-version.js.map