@byok-sdk/protocol 0.29.0 → 0.30.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.
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Sealed payload v1 on the hosted wire (#654 design §8.1, §8.3, §8.6).
3
+ *
4
+ * Each sealed message type (`task.complete_sealed`, …) is its plaintext type
5
+ * without the fields one data category moves into the ciphertext, plus a
6
+ * sealed member. The type, not a field, is gated, so a decoder that does not
7
+ * know `sealed-payload-v1` refuses the whole message. The Host reads only the
8
+ * sealed member's outer fields. It checks the shape here:
9
+ * version, suite, encodings, a legal padded length, and exactly one of inline
10
+ * `ciphertext` and `ciphertextRef`. Inline is the only legal form up to
11
+ * {@link SEALED_INLINE_MAX_PADDED_LENGTH}; above it, the ciphertext is a blob.
12
+ * So one content has one legal wire form, and two submissions compare by
13
+ * their outer fields only.
14
+ *
15
+ * `@byok-sdk/core/sealed-payload` seals and opens the bytes. Protocol does
16
+ * not depend on core, so this module repeats the header rules and the padded
17
+ * length function; a cross-package test keeps the two equal.
18
+ */
19
+ import { z } from 'zod';
20
+ export { SEALED_SPLIT_TABLE, isSealedMessageType } from './sealed-split-table';
21
+ export type { SealedDigestRule, SealedMessageType, SealedSplitEntry } from './sealed-split-table';
22
+ export declare const SEALED_PAYLOAD_VERSION = 1;
23
+ /** The only suite v1 admits. A new suite is a new constant and a new capability, never a parser branch. */
24
+ export declare const SEALED_PAYLOAD_SUITE = "byok-sealed-v1.hkdf-sha256.aes-256-gcm";
25
+ /** C1–C4 in canonical order. A task lists its sealing categories in this order. */
26
+ export declare const SEALED_PAYLOAD_CATEGORIES: readonly ['task-input', 'agent-message', 'task-output', 'memory-projection'];
27
+ export type SealedPayloadCategory = (typeof SEALED_PAYLOAD_CATEGORIES)[number];
28
+ /**
29
+ * The categories a v1 task may seal (D17): task input and task output. Agent
30
+ * messages, memory projections, input preparation and drafts come later.
31
+ */
32
+ export declare const SEALED_PAYLOAD_V1_CATEGORIES: readonly ['task-input', 'task-output'];
33
+ export type SealedPayloadV1Category = (typeof SEALED_PAYLOAD_V1_CATEGORIES)[number];
34
+ /**
35
+ * Closed `task.decline` codes of a receiver that refuses sealed task input
36
+ * (§8.3). They are plaintext: a device without the scope key cannot seal a
37
+ * reason. A device sends them only to a server that advertised
38
+ * `sealed-payload-v1`.
39
+ */
40
+ export declare const SEALED_DECLINE_CODES: readonly ['sealed_key_unavailable', 'sealed_open_failed', 'sealed_content_mac_mismatch', 'sealing_policy_mismatch'];
41
+ export type SealedDeclineCode = (typeof SEALED_DECLINE_CODES)[number];
42
+ /**
43
+ * The closed plaintext codes of a task whose output is sealed (#654 12d).
44
+ * It lists each plaintext output type that has a plaintext form on such a
45
+ * task, with the codes that each field moved or dropped by its sealed type
46
+ * may hold. Such a field that is not listed here must be absent. An output
47
+ * type with a sealed form that is not listed has no plaintext form. An
48
+ * interrupted task reports `daemon_interrupted`; a device that cannot seal
49
+ * reports `sealed_key_unavailable`. The Host check, the daemon and the member
50
+ * display read this one list, and compare by exact match.
51
+ */
52
+ export declare const SEALED_TASK_PLAIN_CODES: Readonly<{
53
+ readonly 'task.fail': {
54
+ readonly reason: readonly ["daemon_interrupted", "sealed_key_unavailable"];
55
+ };
56
+ readonly 'task.decline': {
57
+ readonly reason: readonly ["sealed_key_unavailable", "sealed_open_failed", "sealed_content_mac_mismatch", "sealing_policy_mismatch"];
58
+ };
59
+ readonly 'task.cancelled': {
60
+ readonly reason: readonly [];
61
+ };
62
+ }>;
63
+ /**
64
+ * True when `payload` is a legal plaintext `type` message of a task whose
65
+ * output is sealed: each field that a `task-output` sealed type of `type`
66
+ * moves or drops is absent or holds one of {@link SEALED_TASK_PLAIN_CODES}.
67
+ * A type that has a sealed output form and is not listed there is never
68
+ * legal. A plaintext decline also names its code as `code`. A type with no
69
+ * sealed output form is always legal.
70
+ */
71
+ export declare function isSealedTaskPlainOutput(type: string, payload: object): boolean;
72
+ /** Padded frames above this length travel as a ciphertext blob (§8.6). */
73
+ export declare const SEALED_INLINE_MAX_PADDED_LENGTH: number;
74
+ /** Plaintext limit of every v1 category, in UTF-8 bytes of canonical JSON (D8). */
75
+ export declare const SEALED_PAYLOAD_MAX_CONTENT_LENGTH: number;
76
+ /**
77
+ * The one legal padded length of a frame of `frameLength` bytes
78
+ * (`4 + contentLength`): the next multiple of 256 up to 16 KiB, Padmé above.
79
+ */
80
+ export declare function sealedPayloadPaddedLength(frameLength: number): number;
81
+ /** The largest padded length a v1 content may have. */
82
+ export declare const SEALED_PAYLOAD_MAX_PADDED_LENGTH: number;
83
+ /**
84
+ * The byte length of canonical unpadded base64url, or `undefined`. Canonical
85
+ * means no padding, no impossible length, and zero unused bits in the last
86
+ * character, so one byte string has exactly one encoding.
87
+ */
88
+ export declare function sealedBase64UrlByteLength(value: string): number | undefined;
89
+ /** A sealed member of any v1 category. */
90
+ export declare const SealedPayloadSchema: z.ZodObject<{
91
+ v: z.ZodLiteral<1>;
92
+ suite: z.ZodLiteral<"byok-sealed-v1.hkdf-sha256.aes-256-gcm">;
93
+ category: z.ZodEnum<{
94
+ "task-input": "task-input";
95
+ "task-output": "task-output";
96
+ }>;
97
+ scope: z.ZodObject<{
98
+ id: z.ZodString;
99
+ epoch: z.ZodNumber;
100
+ keyId: z.ZodString;
101
+ }, z.core.$strict>;
102
+ chainHead: z.ZodObject<{
103
+ seq: z.ZodNumber;
104
+ hash: z.ZodString;
105
+ }, z.core.$strict>;
106
+ sender: z.ZodString;
107
+ salt: z.ZodString;
108
+ nonce: z.ZodString;
109
+ paddedLength: z.ZodNumber;
110
+ contentMac: z.ZodString;
111
+ ciphertext: z.ZodOptional<z.ZodString>;
112
+ ciphertextRef: z.ZodOptional<z.ZodObject<{
113
+ blobId: z.ZodString;
114
+ contentHash: z.ZodString;
115
+ size: z.ZodNumber;
116
+ contentType: z.ZodString;
117
+ }, z.core.$strict>>;
118
+ }, z.core.$strict>;
119
+ export type SealedPayload = z.infer<typeof SealedPayloadSchema>;
120
+ /** The sealed member of a task-input message (offers, steer). */
121
+ export declare const SealedTaskInputSchema: z.ZodObject<{
122
+ v: z.ZodLiteral<1>;
123
+ suite: z.ZodLiteral<"byok-sealed-v1.hkdf-sha256.aes-256-gcm">;
124
+ category: z.ZodLiteral<"task-input">;
125
+ scope: z.ZodObject<{
126
+ id: z.ZodString;
127
+ epoch: z.ZodNumber;
128
+ keyId: z.ZodString;
129
+ }, z.core.$strict>;
130
+ chainHead: z.ZodObject<{
131
+ seq: z.ZodNumber;
132
+ hash: z.ZodString;
133
+ }, z.core.$strict>;
134
+ sender: z.ZodString;
135
+ salt: z.ZodString;
136
+ nonce: z.ZodString;
137
+ paddedLength: z.ZodNumber;
138
+ contentMac: z.ZodString;
139
+ ciphertext: z.ZodOptional<z.ZodString>;
140
+ ciphertextRef: z.ZodOptional<z.ZodObject<{
141
+ blobId: z.ZodString;
142
+ contentHash: z.ZodString;
143
+ size: z.ZodNumber;
144
+ contentType: z.ZodString;
145
+ }, z.core.$strict>>;
146
+ }, z.core.$strict>;
147
+ /** The sealed member of a task-output message. */
148
+ export declare const SealedTaskOutputSchema: z.ZodObject<{
149
+ v: z.ZodLiteral<1>;
150
+ suite: z.ZodLiteral<"byok-sealed-v1.hkdf-sha256.aes-256-gcm">;
151
+ category: z.ZodLiteral<"task-output">;
152
+ scope: z.ZodObject<{
153
+ id: z.ZodString;
154
+ epoch: z.ZodNumber;
155
+ keyId: z.ZodString;
156
+ }, z.core.$strict>;
157
+ chainHead: z.ZodObject<{
158
+ seq: z.ZodNumber;
159
+ hash: z.ZodString;
160
+ }, z.core.$strict>;
161
+ sender: z.ZodString;
162
+ salt: z.ZodString;
163
+ nonce: z.ZodString;
164
+ paddedLength: z.ZodNumber;
165
+ contentMac: z.ZodString;
166
+ ciphertext: z.ZodOptional<z.ZodString>;
167
+ ciphertextRef: z.ZodOptional<z.ZodObject<{
168
+ blobId: z.ZodString;
169
+ contentHash: z.ZodString;
170
+ size: z.ZodNumber;
171
+ contentType: z.ZodString;
172
+ }, z.core.$strict>>;
173
+ }, z.core.$strict>;
174
+ /**
175
+ * A task's frozen sealing policy (§6.2). The offer carries it in plaintext.
176
+ * `epoch` is the epoch of the offer's task-input content, and the lowest epoch
177
+ * the task accepts.
178
+ */
179
+ export declare const TaskSealingSchema: z.ZodObject<{
180
+ scopeId: z.ZodString;
181
+ epoch: z.ZodNumber;
182
+ categories: z.ZodArray<z.ZodEnum<{
183
+ "task-input": "task-input";
184
+ "task-output": "task-output";
185
+ }>>;
186
+ }, z.core.$strict>;
187
+ export type TaskSealing = z.infer<typeof TaskSealingSchema>;
188
+ /**
189
+ * The routine template of a sealed execution (design §6.3). The offer's
190
+ * `sealed` member is then the template, family `routine.template`, which the
191
+ * member sealed once for every occurrence. The device rebuilds its binding
192
+ * from these fields: identity `{ routineId, definitionVersion }`, no device,
193
+ * the offer's `agentRef` and `sealing.categories`, and `notAfter`. They travel
194
+ * in plaintext because the AAD binds them. `definitionVersion` is an integer,
195
+ * because the binding tells `4` and `"4"` apart. The device, not the Host,
196
+ * checks `notAfter` against its clock.
197
+ */
198
+ export declare const RoutineRefSchema: z.ZodObject<{
199
+ routineId: z.ZodString;
200
+ definitionVersion: z.ZodNumber;
201
+ notAfter: z.ZodString;
202
+ }, z.core.$strict>;
203
+ /**
204
+ * The rule of a sealed offer, for `superRefine`: `sealed` is present exactly
205
+ * when the task's `sealing` includes `task-input`, and then `instruction` is
206
+ * absent and `sealed.scope` is the task's scope at the task's epoch. When the
207
+ * task seals only its output, `instruction` is plaintext and required. A
208
+ * `routineRef` names a routine template, which is sealed task input, so it
209
+ * needs a task that seals `task-input`.
210
+ *
211
+ * Whether a task's later messages must be sealed is the Host's and the
212
+ * device's check: they know the task's `sealing`, this payload does not.
213
+ */
214
+ export declare function sealedOfferRule(payload: {
215
+ readonly instruction?: unknown;
216
+ readonly sealing: TaskSealing;
217
+ readonly sealed?: SealedPayload;
218
+ readonly routineRef?: unknown;
219
+ }, ctx: z.RefinementCtx): void;
@@ -0,0 +1,173 @@
1
+ /**
2
+ * The split table of sealed payload v1 (#654 design §8.1, §9.3): for each
3
+ * sealed message type, its plaintext type, its data category, its identity
4
+ * fields, the fields that move into the ciphertext, the fields it drops, and
5
+ * the rule of each digest field (D16).
6
+ *
7
+ * Plain data with no runtime imports, so `wire-additions.ts` can derive the
8
+ * sealed gates from it and stay free of zod.
9
+ */
10
+ import type { MessageType } from './messages';
11
+ import type { SealedPayloadV1Category } from './sealed-payload';
12
+ /**
13
+ * How a sealed message type treats one digest field of its plaintext type
14
+ * (D16, design §9.3).
15
+ *
16
+ * - `ciphertext-sha256`: the SHA-256 of the sealed bytes, for a Host that
17
+ * checks the bytes it stores (a ciphertext blob).
18
+ * - `keyed-mac`: a keyed MAC under the scope's MAC key; the Host forwards it.
19
+ * - `sealed`: the field moves into the ciphertext with its parent.
20
+ * - `plain`: a digest that no sealed content derives, kept in plaintext.
21
+ * - `forbidden`: the sealed type does not carry it in v1.
22
+ */
23
+ export type SealedDigestRule = 'ciphertext-sha256' | 'keyed-mac' | 'sealed' | 'plain' | 'forbidden';
24
+ /** One sealed message type in the split table. */
25
+ export interface SealedSplitEntry {
26
+ /**
27
+ * The plaintext message type this type seals. The AAD and the content MAC
28
+ * bind it as the family, not the sealed wire type.
29
+ */
30
+ readonly plainType: MessageType;
31
+ readonly category: SealedPayloadV1Category;
32
+ /**
33
+ * Payload fields that, after the envelope `task_id`, name this write in the
34
+ * AAD and the content MAC (§8.2, §9.1). The sealed type requires them.
35
+ */
36
+ readonly identity: readonly string[];
37
+ /**
38
+ * Top-level fields of the plaintext type that move into the ciphertext.
39
+ * The sealed type does not have them; the receiver merges the opened
40
+ * object back and validates the plaintext type.
41
+ */
42
+ readonly moved: readonly string[];
43
+ /** Top-level fields of the plaintext type that the sealed type drops in v1. */
44
+ readonly forbidden: readonly string[];
45
+ /**
46
+ * Every path of the plaintext type whose name contains `digest`, `hash` or
47
+ * `revision` (case-insensitive), with its rule. A schema test keeps this
48
+ * complete.
49
+ */
50
+ readonly digests: {
51
+ readonly [path: string]: SealedDigestRule;
52
+ };
53
+ /**
54
+ * UTF-8 byte limits of moved fields that replace the plaintext type's limit
55
+ * of that field after the merge (decision 12b). The category limit still
56
+ * bounds the whole opened content.
57
+ */
58
+ readonly limits?: {
59
+ readonly [field: string]: number;
60
+ };
61
+ }
62
+ /**
63
+ * The split table (design §8.3): each sealed message type, the plaintext type
64
+ * it seals, its category, identity, moved and forbidden fields, and the rule
65
+ * of each digest field. It is the one authority; the schemas, the Host's
66
+ * comparison projection and the receiver's merge read it.
67
+ *
68
+ * A sealed offer also carries the task's `sealing`. When the categories
69
+ * include `task-input`, `instruction` is sealed; otherwise it stays plaintext
70
+ * and only the task output is sealed.
71
+ *
72
+ * Blob bytes have no sealing rule yet, so a sealed type carries no plaintext
73
+ * blob reference: `artifactRefs` and the artifact `blobRef` are dropped until
74
+ * that rule exists. A sealed artifact is inline content, so its `inline`
75
+ * takes up to 2 MiB instead of the plaintext 64 KB (decision 12b).
76
+ */
77
+ export declare const SEALED_SPLIT_TABLE: Readonly<{
78
+ readonly 'task.offer_sealed': SealedSplitEntry;
79
+ readonly 'task.offer_with_toolsets_sealed': SealedSplitEntry;
80
+ readonly 'task.offer_for_agent_sealed': SealedSplitEntry;
81
+ readonly 'task.offer_for_agent_with_egress_sealed': SealedSplitEntry;
82
+ readonly 'task.offer_for_agent_with_egress_fresh_sealed': SealedSplitEntry;
83
+ readonly 'task.steer_sealed': {
84
+ readonly plainType: 'task.steer';
85
+ readonly category: 'task-input';
86
+ readonly identity: readonly ["steerId"];
87
+ readonly moved: readonly ["text"];
88
+ readonly forbidden: readonly [];
89
+ readonly digests: {};
90
+ };
91
+ readonly 'task.complete_sealed': {
92
+ readonly plainType: 'task.complete';
93
+ readonly category: 'task-output';
94
+ readonly identity: readonly [];
95
+ readonly moved: readonly ["summary", "finalMessage", "document"];
96
+ readonly forbidden: readonly ["artifactRefs", "preparedObservation"];
97
+ readonly digests: {
98
+ readonly 'agentRef.profileRevision': 'plain';
99
+ readonly 'agentHomeProjection.profileRevision': 'plain';
100
+ readonly 'agentHomeProjection.projectionHash': 'plain';
101
+ readonly 'artifactRefs[].contentHash': 'forbidden';
102
+ readonly 'preparedObservation.requestDigest': 'forbidden';
103
+ };
104
+ };
105
+ readonly 'task.fail_sealed': {
106
+ readonly plainType: 'task.fail';
107
+ readonly category: 'task-output';
108
+ readonly identity: readonly [];
109
+ readonly moved: readonly ["reason"];
110
+ readonly forbidden: readonly ["preparedObservation"];
111
+ readonly digests: {
112
+ readonly 'agentRef.profileRevision': 'plain';
113
+ readonly 'agentHomeProjection.profileRevision': 'plain';
114
+ readonly 'agentHomeProjection.projectionHash': 'plain';
115
+ readonly 'preparedObservation.requestDigest': 'forbidden';
116
+ };
117
+ };
118
+ readonly 'task.decline_sealed': {
119
+ readonly plainType: 'task.decline';
120
+ readonly category: 'task-output';
121
+ readonly identity: readonly [];
122
+ readonly moved: readonly ["reason"];
123
+ readonly forbidden: readonly [];
124
+ readonly digests: {
125
+ readonly 'agentRef.profileRevision': 'plain';
126
+ };
127
+ };
128
+ readonly 'task.cancelled_sealed': {
129
+ readonly plainType: 'task.cancelled';
130
+ readonly category: 'task-output';
131
+ readonly identity: readonly [];
132
+ readonly moved: readonly ["reason"];
133
+ readonly forbidden: readonly [];
134
+ readonly digests: {
135
+ readonly 'agentRef.profileRevision': 'plain';
136
+ readonly 'agentHomeProjection.profileRevision': 'plain';
137
+ readonly 'agentHomeProjection.projectionHash': 'plain';
138
+ };
139
+ };
140
+ readonly 'task.progress_sealed': {
141
+ readonly plainType: 'task.progress';
142
+ readonly category: 'task-output';
143
+ readonly identity: readonly ["seq"];
144
+ readonly moved: readonly ["events"];
145
+ readonly forbidden: readonly [];
146
+ readonly digests: {
147
+ readonly 'events[].spill.blob.contentHash': 'sealed';
148
+ };
149
+ };
150
+ readonly 'task.artifact_sealed': {
151
+ readonly plainType: 'task.artifact';
152
+ readonly category: 'task-output';
153
+ readonly identity: readonly ["artifactId"];
154
+ readonly moved: readonly ["inline"];
155
+ readonly forbidden: readonly ["blobRef"];
156
+ readonly digests: {
157
+ readonly 'blobRef.contentHash': 'forbidden';
158
+ };
159
+ readonly limits: {
160
+ readonly inline: number;
161
+ };
162
+ };
163
+ readonly 'task.await_approval_sealed': {
164
+ readonly plainType: 'task.await_approval';
165
+ readonly category: 'task-output';
166
+ readonly identity: readonly ["approvalId"];
167
+ readonly moved: readonly ["summary"];
168
+ readonly forbidden: readonly [];
169
+ readonly digests: {};
170
+ };
171
+ }>;
172
+ export type SealedMessageType = keyof typeof SEALED_SPLIT_TABLE;
173
+ export declare function isSealedMessageType(type: string): type is SealedMessageType;
package/dist/version.d.ts CHANGED
@@ -100,5 +100,5 @@ export declare const AGENT_HOME_READERS_CAPABILITY: 'agent-home-readers';
100
100
  * cannot accidentally execute the instruction without the required tools.
101
101
  */
102
102
  export declare const CUSTOM_HARNESS_CAPABILITY: 'custom-harness';
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", "agent-memory-selection", 'agent-message-draft'];
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", "agent-memory-selection", "agent-memory-read-if-admitted", 'agent-message-draft', "sealed-payload-v1"];
104
104
  export type CapabilityFlag = (typeof CAPABILITY_FLAGS)[number];
@@ -26,8 +26,8 @@
26
26
  *
27
27
  * Senders derive their admission flags from {@link requiredReceiverCapabilities}
28
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.
29
+ * another. This module imports no runtime code except the plain-data split
30
+ * table, so it stays free of zod.
31
31
  */
32
32
  import type { MessageType } from './messages';
33
33
  import type { CapabilityFlag } from './version';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byok-sdk/protocol",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "BYOK SDK wire protocol: envelope schema, message types, and codec helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",