@byok-sdk/protocol 0.2.0 → 0.4.0

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.
@@ -34,6 +34,7 @@ export declare const RuntimeCapabilitiesSchema: z.ZodObject<{
34
34
  steer: z.ZodOptional<z.ZodBoolean>;
35
35
  resume: z.ZodOptional<z.ZodBoolean>;
36
36
  approvalInteractive: z.ZodOptional<z.ZodBoolean>;
37
+ mcpToolsets: z.ZodOptional<z.ZodBoolean>;
37
38
  permissionModes: z.ZodOptional<z.ZodArray<z.ZodString>>;
38
39
  }, z.core.$strip>;
39
40
  export type RuntimeCapabilities = z.infer<typeof RuntimeCapabilitiesSchema>;
@@ -54,10 +55,26 @@ export declare const RuntimeInfoSchema: z.ZodObject<{
54
55
  steer: z.ZodOptional<z.ZodBoolean>;
55
56
  resume: z.ZodOptional<z.ZodBoolean>;
56
57
  approvalInteractive: z.ZodOptional<z.ZodBoolean>;
58
+ mcpToolsets: z.ZodOptional<z.ZodBoolean>;
57
59
  permissionModes: z.ZodOptional<z.ZodArray<z.ZodString>>;
58
60
  }, z.core.$strip>>;
59
61
  }, z.core.$strip>;
60
62
  export type RuntimeInfo = z.infer<typeof RuntimeInfoSchema>;
63
+ /** Maximum logical toolsets one daemon may advertise as locally configured. */
64
+ export declare const CONFIGURED_TOOLSETS_MAX_ITEMS = 64;
65
+ /**
66
+ * Logical, host-owned MCP toolset identifier. A task may request this name,
67
+ * but the executable/server definition behind it exists only in the daemon's
68
+ * local configuration and never crosses the SaaS wire.
69
+ */
70
+ export declare const ToolsetIdSchema: z.ZodString;
71
+ export type ToolsetId = z.infer<typeof ToolsetIdSchema>;
72
+ /**
73
+ * Device-local inventory projected for discovery. IDs only: executable MCP
74
+ * definitions, arguments, environment and credentials never enter this shape.
75
+ * Empty means known-none; omission on the containing message means unknown.
76
+ */
77
+ export declare const ConfiguredToolsetsSchema: z.ZodArray<z.ZodString>;
61
78
  /** daemon -> server: opening handshake. */
62
79
  export declare const ConnHelloPayloadSchema: z.ZodObject<{
63
80
  protocolVersions: z.ZodArray<z.ZodNumber>;
@@ -76,9 +93,11 @@ export declare const ConnHelloPayloadSchema: z.ZodObject<{
76
93
  steer: z.ZodOptional<z.ZodBoolean>;
77
94
  resume: z.ZodOptional<z.ZodBoolean>;
78
95
  approvalInteractive: z.ZodOptional<z.ZodBoolean>;
96
+ mcpToolsets: z.ZodOptional<z.ZodBoolean>;
79
97
  permissionModes: z.ZodOptional<z.ZodArray<z.ZodString>>;
80
98
  }, z.core.$strip>>;
81
99
  }, z.core.$strip>>>;
100
+ configuredToolsets: z.ZodOptional<z.ZodArray<z.ZodString>>;
82
101
  cursor: z.ZodOptional<z.ZodNumber>;
83
102
  }, z.core.$strip>;
84
103
  export type ConnHelloPayload = z.infer<typeof ConnHelloPayloadSchema>;
@@ -89,6 +108,28 @@ export declare const ConnAckPayloadSchema: z.ZodObject<{
89
108
  serverTime: z.ZodISODateTime;
90
109
  }, z.core.$strip>;
91
110
  export type ConnAckPayload = z.infer<typeof ConnAckPayloadSchema>;
111
+ /**
112
+ * The web-selected runtime/model target carried end to end with a task.
113
+ *
114
+ * This is an additive v1 field. The discriminated union makes lane ownership
115
+ * fail closed at decode time: subscription credentials can only belong to the
116
+ * vendor CLIs, while a BYOK provider can only be executed through Pi.
117
+ */
118
+ export declare const DispatchSelectionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
119
+ lane: z.ZodLiteral<"subscription">;
120
+ runtimeId: z.ZodEnum<{
121
+ claude: "claude";
122
+ codex: "codex";
123
+ }>;
124
+ providerId: z.ZodNull;
125
+ modelId: z.ZodString;
126
+ }, z.core.$strict>, z.ZodObject<{
127
+ lane: z.ZodLiteral<"byok">;
128
+ runtimeId: z.ZodLiteral<"pi">;
129
+ providerId: z.ZodString;
130
+ modelId: z.ZodString;
131
+ }, z.core.$strict>], "lane">;
132
+ export type DispatchSelection = z.infer<typeof DispatchSelectionSchema>;
92
133
  /**
93
134
  * server -> daemon: offer a task for a device to claim.
94
135
  *
@@ -122,6 +163,20 @@ export declare const TaskOfferPayloadSchema: z.ZodObject<{
122
163
  codex: "codex";
123
164
  pi: "pi";
124
165
  }>>;
166
+ dispatchSelection: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
167
+ lane: z.ZodLiteral<"subscription">;
168
+ runtimeId: z.ZodEnum<{
169
+ claude: "claude";
170
+ codex: "codex";
171
+ }>;
172
+ providerId: z.ZodNull;
173
+ modelId: z.ZodString;
174
+ }, z.core.$strict>, z.ZodObject<{
175
+ lane: z.ZodLiteral<"byok">;
176
+ runtimeId: z.ZodLiteral<"pi">;
177
+ providerId: z.ZodString;
178
+ modelId: z.ZodString;
179
+ }, z.core.$strict>], "lane">>;
125
180
  sessionRef: z.ZodOptional<z.ZodString>;
126
181
  workspaceHint: z.ZodOptional<z.ZodString>;
127
182
  limits: z.ZodOptional<z.ZodObject<{
@@ -130,6 +185,66 @@ export declare const TaskOfferPayloadSchema: z.ZodObject<{
130
185
  }, z.core.$strip>>;
131
186
  }, z.core.$strip>;
132
187
  export type TaskOfferPayload = z.infer<typeof TaskOfferPayloadSchema>;
188
+ /** Every named toolset is required; duplicates are rejected instead of silently de-duplicated. */
189
+ export declare const RequiredToolsetsSchema: z.ZodArray<z.ZodString>;
190
+ /**
191
+ * Additive v1 offer variant for tasks whose semantics require local MCP
192
+ * tools. This is a distinct message type rather than an optional field on
193
+ * `task.offer`: an older v1 daemon skips an unknown message type, whereas it
194
+ * would legally strip an unknown optional control field and run the task
195
+ * without its required tools. The whole payload is strict because every
196
+ * field here affects execution authority.
197
+ */
198
+ export declare const TaskOfferWithToolsetsPayloadSchema: z.ZodObject<{
199
+ instruction: z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
200
+ blobRef: z.ZodObject<{
201
+ blobId: z.ZodString;
202
+ contentHash: z.ZodString;
203
+ size: z.ZodNumber;
204
+ contentType: z.ZodString;
205
+ url: z.ZodOptional<z.ZodString>;
206
+ }, z.core.$strip>;
207
+ }, z.core.$strict>]>;
208
+ policy: z.ZodObject<{
209
+ mode: z.ZodEnum<{
210
+ auto: "auto";
211
+ confirm: "confirm";
212
+ plan: "plan";
213
+ readonly: "readonly";
214
+ }>;
215
+ allowTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
216
+ denyTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
217
+ workspaceRoot: z.ZodOptional<z.ZodString>;
218
+ network: z.ZodOptional<z.ZodBoolean>;
219
+ }, z.core.$strict>;
220
+ runtime: z.ZodOptional<z.ZodEnum<{
221
+ claude: "claude";
222
+ codex: "codex";
223
+ pi: "pi";
224
+ }>>;
225
+ dispatchSelection: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
226
+ lane: z.ZodLiteral<"subscription">;
227
+ runtimeId: z.ZodEnum<{
228
+ claude: "claude";
229
+ codex: "codex";
230
+ }>;
231
+ providerId: z.ZodNull;
232
+ modelId: z.ZodString;
233
+ }, z.core.$strict>, z.ZodObject<{
234
+ lane: z.ZodLiteral<"byok">;
235
+ runtimeId: z.ZodLiteral<"pi">;
236
+ providerId: z.ZodString;
237
+ modelId: z.ZodString;
238
+ }, z.core.$strict>], "lane">>;
239
+ sessionRef: z.ZodOptional<z.ZodString>;
240
+ workspaceHint: z.ZodOptional<z.ZodString>;
241
+ limits: z.ZodOptional<z.ZodObject<{
242
+ maxDurationMs: z.ZodOptional<z.ZodNumber>;
243
+ maxTokens: z.ZodOptional<z.ZodNumber>;
244
+ }, z.core.$strip>>;
245
+ requiredToolsets: z.ZodArray<z.ZodString>;
246
+ }, z.core.$strict>;
247
+ export type TaskOfferWithToolsetsPayload = z.infer<typeof TaskOfferWithToolsetsPayloadSchema>;
133
248
  /**
134
249
  * server -> daemon: approve a pending `task.await_approval` request.
135
250
  *
@@ -241,6 +356,7 @@ export declare const TaskClaimPayloadSchema: z.ZodObject<{
241
356
  steer: z.ZodOptional<z.ZodBoolean>;
242
357
  resume: z.ZodOptional<z.ZodBoolean>;
243
358
  approvalInteractive: z.ZodOptional<z.ZodBoolean>;
359
+ mcpToolsets: z.ZodOptional<z.ZodBoolean>;
244
360
  permissionModes: z.ZodOptional<z.ZodArray<z.ZodString>>;
245
361
  }, z.core.$strip>>;
246
362
  }, z.core.$strip>;
@@ -356,7 +472,125 @@ export declare const TaskAwaitApprovalPayloadSchema: z.ZodObject<{
356
472
  approvalId: z.ZodOptional<z.ZodString>;
357
473
  }, z.core.$strip>;
358
474
  export type TaskAwaitApprovalPayload = z.infer<typeof TaskAwaitApprovalPayloadSchema>;
359
- /** daemon -> server: task finished successfully. */
475
+ /**
476
+ * Hard cap (1 MiB) on a single `task.complete.document` (see
477
+ * {@link TaskCompletePayloadSchema}), measured as the UTF-8 byte length of
478
+ * its canonical JSON encoding — NOT as a node/key count, since a document is
479
+ * schema-neutral and its object shape is unbounded by design.
480
+ *
481
+ * The cap is a REJECT-AT-BOUNDARY limit on both sides: a daemon that
482
+ * produces an over-cap document reports `task.fail` instead of sending it,
483
+ * and a server rejects an over-cap document at schema validation. It is
484
+ * never truncated — a truncated JSON document is not valid JSON, so
485
+ * "shrinking to fit" can only hand the consumer garbage.
486
+ *
487
+ * 1 MiB is the conservative ceiling declared by the first real consumer
488
+ * (`docs/researches/2026-08-12-salesko-consumption-evidence.md` §1/§2:
489
+ * smallest real frame 8.4 KiB, typical 48-96 KiB, 512 KiB comfortable).
490
+ * Producers should stay at or under ~512 KiB (docs/protocol.md); the extra
491
+ * headroom exists because raising a protocol cap later is additive while
492
+ * lowering one is breaking. A result too big for this channel belongs in
493
+ * `artifactRefs` (the multi-file/binary/oversized channel), not here.
494
+ */
495
+ export declare const RESULT_DOCUMENT_MAX_BYTES = 1048576;
496
+ /**
497
+ * Outcome of {@link checkResultDocument}.
498
+ *
499
+ * `bytes` is the measured canonical JSON UTF-8 byte length, present on both
500
+ * the accept and the over-cap rejection so a caller can name the actual size
501
+ * in a failure reason. `canonical` is the CANONICAL SNAPSHOT — the value a
502
+ * sender must actually put on the wire; see {@link checkResultDocument}.
503
+ */
504
+ export type ResultDocumentCheck = {
505
+ readonly ok: true;
506
+ readonly bytes: number;
507
+ readonly canonical: unknown;
508
+ } | {
509
+ readonly ok: false;
510
+ readonly reason: 'not-serializable';
511
+ } | {
512
+ readonly ok: false;
513
+ readonly reason: 'over-cap';
514
+ readonly bytes: number;
515
+ } | {
516
+ readonly ok: false;
517
+ readonly reason: 'not-plain-json';
518
+ };
519
+ /**
520
+ * THE single authority for "is this a legal `task.complete.document`", and
521
+ * the one place its canonical form is produced. `TaskCompletePayloadSchema`'s
522
+ * own refinement calls it, and the daemon-side pre-send gate
523
+ * (`packages/client`'s `task-runner.ts`) imports and calls the exact same
524
+ * function rather than re-deriving any part of it: a daemon that measured or
525
+ * judged a document even slightly differently from the server that validates
526
+ * it would either reject documents the wire would have accepted, or hand the
527
+ * server a payload it is about to reject after the runtime session already
528
+ * ended.
529
+ *
530
+ * **The contract is: a document must be PLAIN JSON DATA.** Not "an object
531
+ * that happens to survive `JSON.stringify`" — that bar is far too low, and
532
+ * two concrete attacks/mistakes live under it:
533
+ *
534
+ * 1. `JSON.stringify` succeeding does not mean the value was preserved. An
535
+ * `undefined`-valued key, a `NaN`, a function-valued property, or a
536
+ * `Date` all serialize "successfully" while silently becoming something
537
+ * else (dropped, `null`, or a string). The result is a well-formed,
538
+ * under-cap document that is not what the producer had — a confidently
539
+ * wrong terminal result, the worst outcome this channel has.
540
+ * 2. `toJSON(key)` receives the property key it is being serialized under,
541
+ * so an object can legally answer one way at the root (`key === ''`,
542
+ * where this function measures it) and a completely different way when
543
+ * nested inside the envelope payload (`key === 'document'`, where the
544
+ * codec actually serializes it). A root-only measurement is therefore
545
+ * not a bound on what goes on the wire at all. The same hole exists for
546
+ * any getter that answers differently on a second read.
547
+ *
548
+ * Both die together via the same mechanism. The steps:
549
+ *
550
+ * 1. `JSON.stringify` must succeed and not return `undefined`.
551
+ * 2. Its UTF-8 byte length must be within {@link RESULT_DOCUMENT_MAX_BYTES}.
552
+ * 3. `JSON.parse` that string — the CANONICAL SNAPSHOT. It is pure data:
553
+ * no `toJSON`, no getters, no prototype, nothing left that can answer
554
+ * differently a second time.
555
+ * 4. The original must be structurally equal to the snapshot
556
+ * ({@link isSameJsonData}). Any mismatch means the value was not plain
557
+ * JSON data, and it is rejected rather than silently transformed.
558
+ *
559
+ * On success the snapshot is returned as `canonical`, and **every sender
560
+ * must put THAT on the wire, never the original reference** — which is what
561
+ * closes the contextual-`toJSON`/unstable-getter hole for good: pure data
562
+ * serializes identically at the root and nested, so what was measured is
563
+ * necessarily what is sent. The check is idempotent on pure data, so the
564
+ * server re-running it on an already-parsed payload is a no-op that always
565
+ * agrees.
566
+ */
567
+ export declare function checkResultDocument(document: unknown): ResultDocumentCheck;
568
+ /**
569
+ * daemon -> server: task finished successfully.
570
+ *
571
+ * `document` (additive-minor, docs/protocol.md "Freeze rule"): the OPTIONAL
572
+ * structured terminal result of the task — one JSON value the product on the
573
+ * other side consumes as the task's actual output, as opposed to `summary`
574
+ * (human-readable prose) or `artifactRefs` (files). Deliberately
575
+ * `z.unknown()`: this SDK never understands, validates, or transforms the
576
+ * product's own document schema — that validation belongs to the consumer.
577
+ * The only constraints the wire imposes are the ones
578
+ * {@link checkResultDocument} enforces: it must be PLAIN JSON DATA (equal to
579
+ * its own JSON round trip — see that function for why "stringify succeeded"
580
+ * is not enough), and its canonical JSON UTF-8 encoding must be at most
581
+ * {@link RESULT_DOCUMENT_MAX_BYTES}. An over-cap document is REJECTED here,
582
+ * never truncated (see that constant's own doc comment). A sender puts the
583
+ * check's `canonical` snapshot on the wire, never the original object.
584
+ *
585
+ * Unlike `approvalId` on `task.await_approval` above, emitting this field IS
586
+ * gated on a capability flag (`result-document`, `version.ts`): a pre-
587
+ * `result-document` server strips it silently as an unknown key (the
588
+ * tolerant `z.object()` behavior §1 mandates), and silently losing the
589
+ * task's primary structured result is not a tolerable degradation the way
590
+ * losing an observability hint is. So a daemon sends `document` only to a
591
+ * server that advertised the flag, and fails the task loudly otherwise —
592
+ * see `packages/client`'s `task-runner.ts`.
593
+ */
360
594
  export declare const TaskCompletePayloadSchema: z.ZodObject<{
361
595
  summary: z.ZodString;
362
596
  sessionRef: z.ZodString;
@@ -367,6 +601,7 @@ export declare const TaskCompletePayloadSchema: z.ZodObject<{
367
601
  contentType: z.ZodString;
368
602
  url: z.ZodOptional<z.ZodString>;
369
603
  }, z.core.$strip>>>;
604
+ document: z.ZodOptional<z.ZodUnknown>;
370
605
  }, z.core.$strip>;
371
606
  export type TaskCompletePayload = z.infer<typeof TaskCompletePayloadSchema>;
372
607
  /** daemon -> server: task failed. */
@@ -464,9 +699,11 @@ export declare const MESSAGE_PAYLOAD_SCHEMAS: {
464
699
  steer: z.ZodOptional<z.ZodBoolean>;
465
700
  resume: z.ZodOptional<z.ZodBoolean>;
466
701
  approvalInteractive: z.ZodOptional<z.ZodBoolean>;
702
+ mcpToolsets: z.ZodOptional<z.ZodBoolean>;
467
703
  permissionModes: z.ZodOptional<z.ZodArray<z.ZodString>>;
468
704
  }, z.core.$strip>>;
469
705
  }, z.core.$strip>>>;
706
+ configuredToolsets: z.ZodOptional<z.ZodArray<z.ZodString>>;
470
707
  cursor: z.ZodOptional<z.ZodNumber>;
471
708
  }, z.core.$strip>;
472
709
  readonly 'conn.ack': z.ZodObject<{
@@ -501,6 +738,20 @@ export declare const MESSAGE_PAYLOAD_SCHEMAS: {
501
738
  codex: "codex";
502
739
  pi: "pi";
503
740
  }>>;
741
+ dispatchSelection: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
742
+ lane: z.ZodLiteral<"subscription">;
743
+ runtimeId: z.ZodEnum<{
744
+ claude: "claude";
745
+ codex: "codex";
746
+ }>;
747
+ providerId: z.ZodNull;
748
+ modelId: z.ZodString;
749
+ }, z.core.$strict>, z.ZodObject<{
750
+ lane: z.ZodLiteral<"byok">;
751
+ runtimeId: z.ZodLiteral<"pi">;
752
+ providerId: z.ZodString;
753
+ modelId: z.ZodString;
754
+ }, z.core.$strict>], "lane">>;
504
755
  sessionRef: z.ZodOptional<z.ZodString>;
505
756
  workspaceHint: z.ZodOptional<z.ZodString>;
506
757
  limits: z.ZodOptional<z.ZodObject<{
@@ -508,6 +759,55 @@ export declare const MESSAGE_PAYLOAD_SCHEMAS: {
508
759
  maxTokens: z.ZodOptional<z.ZodNumber>;
509
760
  }, z.core.$strip>>;
510
761
  }, z.core.$strip>;
762
+ readonly 'task.offer_with_toolsets': z.ZodObject<{
763
+ instruction: z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
764
+ blobRef: z.ZodObject<{
765
+ blobId: z.ZodString;
766
+ contentHash: z.ZodString;
767
+ size: z.ZodNumber;
768
+ contentType: z.ZodString;
769
+ url: z.ZodOptional<z.ZodString>;
770
+ }, z.core.$strip>;
771
+ }, z.core.$strict>]>;
772
+ policy: z.ZodObject<{
773
+ mode: z.ZodEnum<{
774
+ auto: "auto";
775
+ confirm: "confirm";
776
+ plan: "plan";
777
+ readonly: "readonly";
778
+ }>;
779
+ allowTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
780
+ denyTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
781
+ workspaceRoot: z.ZodOptional<z.ZodString>;
782
+ network: z.ZodOptional<z.ZodBoolean>;
783
+ }, z.core.$strict>;
784
+ runtime: z.ZodOptional<z.ZodEnum<{
785
+ claude: "claude";
786
+ codex: "codex";
787
+ pi: "pi";
788
+ }>>;
789
+ dispatchSelection: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
790
+ lane: z.ZodLiteral<"subscription">;
791
+ runtimeId: z.ZodEnum<{
792
+ claude: "claude";
793
+ codex: "codex";
794
+ }>;
795
+ providerId: z.ZodNull;
796
+ modelId: z.ZodString;
797
+ }, z.core.$strict>, z.ZodObject<{
798
+ lane: z.ZodLiteral<"byok">;
799
+ runtimeId: z.ZodLiteral<"pi">;
800
+ providerId: z.ZodString;
801
+ modelId: z.ZodString;
802
+ }, z.core.$strict>], "lane">>;
803
+ sessionRef: z.ZodOptional<z.ZodString>;
804
+ workspaceHint: z.ZodOptional<z.ZodString>;
805
+ limits: z.ZodOptional<z.ZodObject<{
806
+ maxDurationMs: z.ZodOptional<z.ZodNumber>;
807
+ maxTokens: z.ZodOptional<z.ZodNumber>;
808
+ }, z.core.$strip>>;
809
+ requiredToolsets: z.ZodArray<z.ZodString>;
810
+ }, z.core.$strict>;
511
811
  readonly 'task.approve': z.ZodObject<{
512
812
  approvalId: z.ZodOptional<z.ZodString>;
513
813
  }, z.core.$strip>;
@@ -533,6 +833,7 @@ export declare const MESSAGE_PAYLOAD_SCHEMAS: {
533
833
  steer: z.ZodOptional<z.ZodBoolean>;
534
834
  resume: z.ZodOptional<z.ZodBoolean>;
535
835
  approvalInteractive: z.ZodOptional<z.ZodBoolean>;
836
+ mcpToolsets: z.ZodOptional<z.ZodBoolean>;
536
837
  permissionModes: z.ZodOptional<z.ZodArray<z.ZodString>>;
537
838
  }, z.core.$strip>>;
538
839
  }, z.core.$strip>;
@@ -603,6 +904,7 @@ export declare const MESSAGE_PAYLOAD_SCHEMAS: {
603
904
  contentType: z.ZodString;
604
905
  url: z.ZodOptional<z.ZodString>;
605
906
  }, z.core.$strip>>>;
907
+ document: z.ZodOptional<z.ZodUnknown>;
606
908
  }, z.core.$strip>;
607
909
  readonly 'task.fail': z.ZodObject<{
608
910
  reason: z.ZodString;
@@ -630,7 +932,7 @@ export declare const MESSAGE_TYPES: MessageType[];
630
932
  * (`envelope.ts`) to decide which branches require envelope `seq` (M1
631
933
  * redelivery cursor).
632
934
  */
633
- export declare const SERVER_TO_DAEMON_TYPES: readonly ["conn.ack", "task.offer", "task.approve", "task.reject", "task.cancel", "task.steer"];
935
+ export declare const SERVER_TO_DAEMON_TYPES: readonly ["conn.ack", "task.offer", "task.offer_with_toolsets", "task.approve", "task.reject", "task.cancel", "task.steer"];
634
936
  /**
635
937
  * Message types the daemon sends to the server — the flip side of
636
938
  * {@link SERVER_TO_DAEMON_TYPES}. `conn.hello` is deliberately excluded: it's
package/dist/version.d.ts CHANGED
@@ -66,5 +66,39 @@ export declare const PROTOCOL_VERSION = 1;
66
66
  * participate in targeting at all -- the same N/N-1-safe shape as every
67
67
  * other flag here, just consumed for observability instead of gating.
68
68
  */
69
- export declare const CAPABILITY_FLAGS: readonly ['steer', 'blob-upload', 'interactive-approval', 'approval_resolved', 'approval-targeting'];
69
+ /**
70
+ * `result-document` (additive-minor): a SERVER-advertised flag meaning "I
71
+ * understand the optional `task.complete.document` field" (`messages.ts`).
72
+ * Functionally gating, like `approval_resolved` and unlike
73
+ * `approval-targeting`.
74
+ *
75
+ * This is the N/N-1 answer for that new daemon -> server FIELD. An old
76
+ * server's `CAPABILITY_FLAGS`/`conn.ack.capabilities` never includes it, and
77
+ * its `TaskCompletePayloadSchema` is a tolerant (non-`.strict()`)
78
+ * `z.object()`, so a `document` sent to it would be silently STRIPPED on
79
+ * parse and vanish without a trace. That is exactly why emission is gated
80
+ * here rather than sent unconditionally the way `approvalId` is: `document`
81
+ * carries the task's primary structured RESULT, so losing it silently is
82
+ * data loss, not a missed observability hint. A new daemon talking to an old
83
+ * server therefore never sends `document` at all, and — if its configured
84
+ * extractor did produce one — reports `task.fail` (retryable: false; the
85
+ * same server will strip it on every retry too) instead of completing the
86
+ * task with its main result quietly deleted (`packages/client`'s
87
+ * `task-runner.ts`). A new server talking to an old daemon is unaffected:
88
+ * the field is optional, and an old daemon simply never sets it.
89
+ *
90
+ * `dispatch-selection` (additive-minor) is a correctness gate for the
91
+ * optional `task.offer.dispatchSelection` control field. An older v1 daemon
92
+ * legally strips unknown optional fields, so a server must never send an
93
+ * authoritative provider/model selection unless the target connection
94
+ * advertises this flag. Absence means reject before task creation, not send
95
+ * a legacy runtime-only offer that could reach a different provider.
96
+ *
97
+ * `toolset-selection` (additive-minor) means the daemon understands
98
+ * `task.offer_with_toolsets` and can resolve its logical ids against local
99
+ * MCP configuration. The distinct message type is also the N/N-1 safety
100
+ * boundary for long-poll: an older daemon skips it as unknown and therefore
101
+ * cannot accidentally execute the instruction without the required tools.
102
+ */
103
+ export declare const CAPABILITY_FLAGS: readonly ['steer', 'blob-upload', 'interactive-approval', 'approval_resolved', 'approval-targeting', 'result-document', 'dispatch-selection', 'toolset-selection'];
70
104
  export type CapabilityFlag = (typeof CAPABILITY_FLAGS)[number];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byok-sdk/protocol",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "BYOK SDK wire protocol: envelope schema, message types, and codec helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",