@byok-sdk/protocol 0.30.2 → 0.32.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,5 @@
1
+ import { z } from 'zod';
2
+ /** Any well-formed open-vocabulary token, without a membership rule. */
3
+ export declare function openVocabularyToken(): z.ZodString;
4
+ /** A well-formed token that is none of `known`: the raw value a receiver keeps as `reported<Field>`. */
5
+ export declare function unrecognizedToken(known: readonly string[]): z.ZodString;
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * Sealed payload v1 on the hosted wire (#654 design §8.1, §8.3, §8.6).
3
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:
4
+ * Sealed is a property of the payload, not a message type: each sealable
5
+ * type's payload is a plaintext branch or a sealed branch. The sealed branch is
6
+ * the plaintext fields without the ones one data category moves into the
7
+ * ciphertext, plus a sealed member; its marker field is gated on
8
+ * `sealed-payload-v1`, and the payload is strict, so a decoder that does not
9
+ * know the branch refuses the whole message. The Host reads only the sealed
10
+ * member's outer fields. It checks the shape here:
9
11
  * version, suite, encodings, a legal padded length, and exactly one of inline
10
12
  * `ciphertext` and `ciphertextRef`. Inline is the only legal form up to
11
13
  * {@link SEALED_INLINE_MAX_PADDED_LENGTH}; above it, the ciphertext is a blob.
@@ -17,8 +19,8 @@
17
19
  * length function; a cross-package test keeps the two equal.
18
20
  */
19
21
  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 { SEALED_SPLIT_TABLE, isSealableMessageType, isSealedPayload } from './sealed-split-table';
23
+ export type { SealableMessageType, SealedDigestRule, SealedSplitEntry } from './sealed-split-table';
22
24
  export declare const SEALED_PAYLOAD_VERSION = 1;
23
25
  /** The only suite v1 admits. A new suite is a new constant and a new capability, never a parser branch. */
24
26
  export declare const SEALED_PAYLOAD_SUITE = "byok-sealed-v1.hkdf-sha256.aes-256-gcm";
@@ -41,10 +43,10 @@ export declare const SEALED_DECLINE_CODES: readonly ['sealed_key_unavailable', '
41
43
  export type SealedDeclineCode = (typeof SEALED_DECLINE_CODES)[number];
42
44
  /**
43
45
  * 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
46
+ * It lists each output type that has a plaintext form on such a task, with
47
+ * the codes that each field moved or dropped by its sealed branch may hold.
48
+ * Such a field that is not listed here must be absent. An output type with a
49
+ * sealed branch that is not listed has no plaintext form. An
48
50
  * interrupted task reports `daemon_interrupted`; a device that cannot seal
49
51
  * reports `sealed_key_unavailable`. The Host check, the daemon and the member
50
52
  * display read this one list, and compare by exact match.
@@ -62,11 +64,11 @@ export declare const SEALED_TASK_PLAIN_CODES: Readonly<{
62
64
  }>;
63
65
  /**
64
66
  * 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`
67
+ * output is sealed: each field that the `task-output` sealed branch of `type`
66
68
  * 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
69
+ * A type that has a sealed output branch and is not listed there is never
68
70
  * legal. A plaintext decline also names its code as `code`. A type with no
69
- * sealed output form is always legal.
71
+ * sealed output branch is always legal.
70
72
  */
71
73
  export declare function isSealedTaskPlainOutput(type: string, payload: object): boolean;
72
74
  /** Padded frames above this length travel as a ciphertext blob (§8.6). */
@@ -1,49 +1,41 @@
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
1
  import type { SealedPayloadV1Category } from './sealed-payload';
12
2
  /**
13
- * How a sealed message type treats one digest field of its plaintext type
14
- * (D16, design §9.3).
3
+ * How the sealed branch of a type treats one digest field of its plaintext
4
+ * branch (D16, design §9.3).
15
5
  *
16
6
  * - `ciphertext-sha256`: the SHA-256 of the sealed bytes, for a Host that
17
7
  * checks the bytes it stores (a ciphertext blob).
18
8
  * - `keyed-mac`: a keyed MAC under the scope's MAC key; the Host forwards it.
19
9
  * - `sealed`: the field moves into the ciphertext with its parent.
20
10
  * - `plain`: a digest that no sealed content derives, kept in plaintext.
21
- * - `forbidden`: the sealed type does not carry it in v1.
11
+ * - `forbidden`: the sealed branch does not carry it in v1.
22
12
  */
23
13
  export type SealedDigestRule = 'ciphertext-sha256' | 'keyed-mac' | 'sealed' | 'plain' | 'forbidden';
24
- /** One sealed message type in the split table. */
14
+ /** One sealable message type in the split table. */
25
15
  export interface SealedSplitEntry {
26
16
  /**
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.
17
+ * The payload field whose presence marks the sealed branch: `sealing` (the
18
+ * task's sealing policy) on `task.offer`, whose task may seal only its
19
+ * output and so carry no `sealed` member; `sealed` everywhere else. The AAD
20
+ * and the content MAC bind the wire type as the family.
29
21
  */
30
- readonly plainType: MessageType;
22
+ readonly marker: 'sealing' | 'sealed';
31
23
  readonly category: SealedPayloadV1Category;
32
24
  /**
33
25
  * 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.
26
+ * AAD and the content MAC (§8.2, §9.1). The sealed branch requires them.
35
27
  */
36
28
  readonly identity: readonly string[];
37
29
  /**
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.
30
+ * Top-level fields of the plaintext branch that move into the ciphertext.
31
+ * The sealed branch does not have them; the receiver merges the opened
32
+ * object back and validates the plaintext branch.
41
33
  */
42
34
  readonly moved: readonly string[];
43
- /** Top-level fields of the plaintext type that the sealed type drops in v1. */
35
+ /** Top-level fields of the plaintext branch that the sealed branch drops in v1. */
44
36
  readonly forbidden: readonly string[];
45
37
  /**
46
- * Every path of the plaintext type whose name contains `digest`, `hash` or
38
+ * Every path of the plaintext branch whose name contains `digest`, `hash` or
47
39
  * `revision` (case-insensitive), with its rule. A schema test keeps this
48
40
  * complete.
49
41
  */
@@ -51,7 +43,7 @@ export interface SealedSplitEntry {
51
43
  readonly [path: string]: SealedDigestRule;
52
44
  };
53
45
  /**
54
- * UTF-8 byte limits of moved fields that replace the plaintext type's limit
46
+ * UTF-8 byte limits of moved fields that replace the plaintext branch's limit
55
47
  * of that field after the merge (decision 12b). The category limit still
56
48
  * bounds the whole opened content.
57
49
  */
@@ -66,36 +58,47 @@ export interface SealedSplitEntry {
66
58
  readonly inlineOnly?: true;
67
59
  }
68
60
  /**
69
- * The split table (design §8.3): each sealed message type, the plaintext type
70
- * it seals, its category, identity, moved and forbidden fields, and the rule
71
- * of each digest field. It is the one authority; the schemas, the Host's
72
- * comparison projection and the receiver's merge read it.
61
+ * The split table (design §8.3): each sealable message type, the marker of its
62
+ * sealed branch, its category, identity, moved and forbidden fields, and the
63
+ * rule of each digest field. It is the one authority; the schemas, the gates,
64
+ * the Host's comparison projection and the receiver's merge read it.
73
65
  *
74
- * A sealed offer also carries the task's `sealing`. When the categories
75
- * include `task-input`, `instruction` is sealed; otherwise it stays plaintext
76
- * and only the task output is sealed.
66
+ * A sealed offer carries the task's `sealing`. When the categories include
67
+ * `task-input`, `instruction` is sealed; otherwise it stays plaintext and only
68
+ * the task output is sealed. A prepared Execution is never sealed.
77
69
  *
78
- * Blob bytes have no sealing rule yet, so a sealed type carries no plaintext
70
+ * Blob bytes have no sealing rule yet, so a sealed branch carries no plaintext
79
71
  * blob reference: `artifactRefs` and the artifact `blobRef` are dropped until
80
72
  * that rule exists. A sealed artifact is inline content, so its `inline`
81
73
  * takes up to 2 MiB instead of the plaintext 64 KB (decision 12b).
82
74
  */
83
75
  export declare const SEALED_SPLIT_TABLE: Readonly<{
84
- readonly 'task.offer_sealed': SealedSplitEntry;
85
- readonly 'task.offer_with_toolsets_sealed': SealedSplitEntry;
86
- readonly 'task.offer_for_agent_sealed': SealedSplitEntry;
87
- readonly 'task.offer_for_agent_with_egress_sealed': SealedSplitEntry;
88
- readonly 'task.offer_for_agent_with_egress_fresh_sealed': SealedSplitEntry;
89
- readonly 'task.steer_sealed': {
90
- readonly plainType: 'task.steer';
76
+ readonly 'task.offer': {
77
+ readonly marker: 'sealing';
78
+ readonly category: 'task-input';
79
+ readonly identity: readonly [];
80
+ readonly moved: readonly ["instruction"];
81
+ readonly forbidden: readonly ["preparation"];
82
+ readonly digests: {
83
+ readonly 'instruction.blobRef.contentHash': 'sealed';
84
+ readonly 'agentRef.profileRevision': 'plain';
85
+ readonly 'dispatchSelection.providerProfile.profileRevision': 'plain';
86
+ readonly 'dispatchSelection.providerProfile.profileHash': 'plain';
87
+ readonly 'egressPolicy.policyRevision': 'plain';
88
+ readonly 'preparation.requestDigest': 'forbidden';
89
+ readonly 'preparation.artifactDigest': 'forbidden';
90
+ };
91
+ };
92
+ readonly 'task.steer': {
93
+ readonly marker: 'sealed';
91
94
  readonly category: 'task-input';
92
95
  readonly identity: readonly ["steerId"];
93
96
  readonly moved: readonly ["text"];
94
97
  readonly forbidden: readonly [];
95
98
  readonly digests: {};
96
99
  };
97
- readonly 'task.complete_sealed': {
98
- readonly plainType: 'task.complete';
100
+ readonly 'task.complete': {
101
+ readonly marker: 'sealed';
99
102
  readonly category: 'task-output';
100
103
  readonly identity: readonly [];
101
104
  readonly moved: readonly ["summary", "finalMessage", "document"];
@@ -108,8 +111,8 @@ export declare const SEALED_SPLIT_TABLE: Readonly<{
108
111
  readonly 'preparedObservation.requestDigest': 'forbidden';
109
112
  };
110
113
  };
111
- readonly 'task.fail_sealed': {
112
- readonly plainType: 'task.fail';
114
+ readonly 'task.fail': {
115
+ readonly marker: 'sealed';
113
116
  readonly category: 'task-output';
114
117
  readonly identity: readonly [];
115
118
  readonly moved: readonly ["reason"];
@@ -121,8 +124,8 @@ export declare const SEALED_SPLIT_TABLE: Readonly<{
121
124
  readonly 'preparedObservation.requestDigest': 'forbidden';
122
125
  };
123
126
  };
124
- readonly 'task.decline_sealed': {
125
- readonly plainType: 'task.decline';
127
+ readonly 'task.decline': {
128
+ readonly marker: 'sealed';
126
129
  readonly category: 'task-output';
127
130
  readonly identity: readonly [];
128
131
  readonly moved: readonly ["reason"];
@@ -131,8 +134,8 @@ export declare const SEALED_SPLIT_TABLE: Readonly<{
131
134
  readonly 'agentRef.profileRevision': 'plain';
132
135
  };
133
136
  };
134
- readonly 'task.cancelled_sealed': {
135
- readonly plainType: 'task.cancelled';
137
+ readonly 'task.cancelled': {
138
+ readonly marker: 'sealed';
136
139
  readonly category: 'task-output';
137
140
  readonly identity: readonly [];
138
141
  readonly moved: readonly ["reason"];
@@ -143,8 +146,8 @@ export declare const SEALED_SPLIT_TABLE: Readonly<{
143
146
  readonly 'agentHomeProjection.projectionHash': 'plain';
144
147
  };
145
148
  };
146
- readonly 'task.progress_sealed': {
147
- readonly plainType: 'task.progress';
149
+ readonly 'task.progress': {
150
+ readonly marker: 'sealed';
148
151
  readonly category: 'task-output';
149
152
  readonly identity: readonly ["seq"];
150
153
  readonly moved: readonly ["events"];
@@ -154,8 +157,8 @@ export declare const SEALED_SPLIT_TABLE: Readonly<{
154
157
  };
155
158
  readonly inlineOnly: true;
156
159
  };
157
- readonly 'task.artifact_sealed': {
158
- readonly plainType: 'task.artifact';
160
+ readonly 'task.artifact': {
161
+ readonly marker: 'sealed';
159
162
  readonly category: 'task-output';
160
163
  readonly identity: readonly ["artifactId"];
161
164
  readonly moved: readonly ["inline"];
@@ -167,8 +170,8 @@ export declare const SEALED_SPLIT_TABLE: Readonly<{
167
170
  readonly inline: number;
168
171
  };
169
172
  };
170
- readonly 'task.await_approval_sealed': {
171
- readonly plainType: 'task.await_approval';
173
+ readonly 'task.await_approval': {
174
+ readonly marker: 'sealed';
172
175
  readonly category: 'task-output';
173
176
  readonly identity: readonly ["approvalId"];
174
177
  readonly moved: readonly ["summary"];
@@ -177,5 +180,8 @@ export declare const SEALED_SPLIT_TABLE: Readonly<{
177
180
  readonly inlineOnly: true;
178
181
  };
179
182
  }>;
180
- export type SealedMessageType = keyof typeof SEALED_SPLIT_TABLE;
181
- export declare function isSealedMessageType(type: string): type is SealedMessageType;
183
+ /** A message type with a sealed branch. */
184
+ export type SealableMessageType = keyof typeof SEALED_SPLIT_TABLE;
185
+ export declare function isSealableMessageType(type: string): type is SealableMessageType;
186
+ /** Whether a `type` payload takes its sealed branch: it carries the type's marker field. */
187
+ export declare function isSealedPayload(type: string, payload: unknown): boolean;
package/dist/version.d.ts CHANGED
@@ -2,14 +2,25 @@
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. Every v3 change is a version bump, a gated addition
5
+ * Wire protocol version. Every v4 change is a version bump, a gated addition
6
6
  * or a strip-safe addition: see docs/protocol.md "Freeze rule", which is the
7
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
8
+ * receiver-capability gates and additions. No v4 release has been published
9
+ * yet, so `src/__tests__/compat.test.ts` has no published v4 decoder to prove
10
10
  * additions against; cross-release gates stay unproven until one exists.
11
11
  *
12
- * v3 makes `schema` required on the `terminalProjection.mode:
12
+ * v4 completes the ADR-034 cutover and retires the approval paths kept for
13
+ * pre-M5 peers. It removes the v3 handshake answer, the runtime-only offers
14
+ * (with and without toolsets) with their sealed twins, and the
15
+ * `strict-agent-only` flag: every offer is Agent-bound. `approvalId` is
16
+ * required on `task.await_approval`, `task.approve` and `task.reject`, so a
17
+ * decision resolves only the approval it names. `task.approval_resolved` is
18
+ * baseline: the daemon reports every locally resolved approval, and the
19
+ * server no longer infers one from later task activity. The three approval
20
+ * capability flags of v3 are gone. A v3 envelope is rejected; there is no v3 reader. Drain, then
21
+ * upgrade server and client together.
22
+ *
23
+ * v3 made `schema` required on the `terminalProjection.mode:
13
24
  * 'result-document'` arm (`terminal-projection.ts`, `result-schema.ts`): the
14
25
  * offer freezes the outcome schema, and the outcome reaches the Host only as
15
26
  * `task.complete.document`. That narrows a strict shape, so it is a wire
@@ -31,9 +42,7 @@ export declare const MAILBOX_READ_AHEAD_CAPABILITY = "mailbox-read-ahead";
31
42
  * attest tool executables. The artifact names its counted tools in
32
43
  * `toolNames`.
33
44
  */
34
- export declare const PROTOCOL_VERSION = 3;
35
- /** Host declares that this device accepts only Agent-bound offer variants. */
36
- export declare const STRICT_AGENT_ONLY_CAPABILITY: 'strict-agent-only';
45
+ export declare const PROTOCOL_VERSION = 4;
37
46
  /**
38
47
  * The device runs `homeAccess: 'memory-reader'` Agent offers: each reader
39
48
  * Attempt gets its own run directory under the Agent home, and readers are
@@ -48,58 +57,17 @@ export declare const AGENT_HOME_READERS_CAPABILITY: 'agent-home-readers';
48
57
  * flags may be introduced without a protocol version bump; unrecognized flags
49
58
  * must be ignored by both sides.
50
59
  *
51
- * `interactive-approval` is RESERVED as of this addition: it gates the
52
- * (currently unexercised) approval seam — a server must not route an
53
- * approval-requiring policy to a daemon that hasn't advertised this flag. No
54
- * bundled runtime adapter emits it yet; that's expected until interactive
55
- * approval is actually wired up in a later wave.
56
- *
57
- * `approval_resolved` (additive-minor): a SERVER-advertised flag meaning
58
- * "I understand the `task.approval_resolved` message" (`messages.ts`). This
59
- * is the N/N-1 answer for that new daemon -> server message: an old server's
60
- * advertised capabilities never include it, so a new daemon talking to an old
61
- * server never sends `task.approval_resolved` at all (see `packages/client`'s
62
- * `task-runner.ts`) and falls back to the pre-existing implicit-resume inference
63
- * (the `relay.onTaskActivity` hook in `packages/server/src/index.ts`)
64
- * unconditionally, exactly as before this flag existed. Unlike
65
- * `interactive-approval`, this one IS exercised the moment both sides
66
- * support it — there is no reserved/dormant period for it.
67
- */
68
- /**
69
- * `approval-targeting` (M5, additive-minor): unlike `approval_resolved`
70
- * above, this flag is purely INFORMATIONAL/semantic, not a functional gate.
71
- * `task.await_approval`/`task.approve`/`task.reject` all carry their new
72
- * `approvalId` field UNCONDITIONALLY on both sides once each peer is
73
- * upgraded -- the wire is tolerant (a plain, non-`.strict()` `z.object()`
74
- * field, `messages.ts`), so no version/capability negotiation is needed just
75
- * to send it safely; an older peer that doesn't recognize the field simply
76
- * never reads it. Receivers decide whether to apply exact-match targeting
77
- * by FIELD PRESENCE on the specific message at hand (does this particular
78
- * `task.approve`/`task.reject`/`onApprovalResolved` payload carry an
79
- * `approvalId`, and does a stored one exist to compare it against?), never
80
- * by checking this flag -- see `approveTask`/`rejectTask` (`@byok-sdk/cloud`)
81
- * and `TaskRunner.handleApprove`/`handleReject` (`packages/client`'s
82
- * `task-runner.ts`). This flag exists only so each side can advertise
83
- * whether the OTHER side is new enough to participate in targeting at all (an
84
- * embedder could observe it through the old
85
- * reference server's `ConnectionHub.getDeviceCapabilities`; `@byok-sdk/server`
86
- * now reads it only internally) -- the same N/N-1-safe shape as every other
87
- * flag here, just consumed for observability instead of gating.
88
- */
89
- /**
90
60
  * `dispatch-selection` (additive-minor) is a correctness gate for the
91
- * optional `task.offer.dispatchSelection` control field. An older v1 daemon
61
+ * optional offer `dispatchSelection` control field. An older v1 daemon
92
62
  * legally strips unknown optional fields, so a server must never send an
93
63
  * authoritative provider/model selection unless the target connection
94
64
  * advertises this flag. Absence means reject before task creation, not send
95
- * a legacy runtime-only offer that could reach a different provider.
65
+ * an offer that could reach a different provider.
96
66
  *
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.
67
+ * `toolset-selection` (additive-minor) means the daemon understands an
68
+ * offer's `requiredToolsets` and can resolve its logical ids against local
69
+ * MCP configuration.
102
70
  */
103
71
  export declare const CUSTOM_HARNESS_CAPABILITY: 'custom-harness';
104
- 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-memory-read-write-if-admitted", 'agent-message-draft', "sealed-payload-v1", "sealed-output-ciphertext-ref", "runtime-failure-class"];
72
+ export declare const CAPABILITY_FLAGS: readonly ['steer', 'blob-upload', 'dispatch-selection', "provider-profile-binding", 'toolset-selection', 'agent-home-contract', "agent-home-readers", "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-memory-read-write-if-admitted", 'agent-message-draft', "sealed-payload-v1", "sealed-output-ciphertext-ref", "runtime-failure-class", "runtime-unavailable-cause"];
105
73
  export type CapabilityFlag = (typeof CAPABILITY_FLAGS)[number];
@@ -2,7 +2,7 @@
2
2
  * The one table of receiver-capability gates and wire additions
3
3
  * (docs/protocol.md "Freeze rule").
4
4
  *
5
- * A v3 change takes exactly one of three paths: a version bump, a gated
5
+ * A v4 change takes exactly one of three paths: a version bump, a gated
6
6
  * addition, or a strip-safe addition. Envelope and HTTP rows have explicit
7
7
  * kinds. The last two are declared here.
8
8
  *
@@ -19,11 +19,15 @@
19
19
  * strip-safe rows and the v2 `result-document` gate are gone. The device
20
20
  * gates kept below name capabilities a daemon advertises only when it is
21
21
  * configured for them (Agent home, egress, input preparation, provisioning,
22
- * memory intents and so on): they gate on the receiver's configuration. The
23
- * server-advertised `approval_resolved` gate is kept as the daemon's sending
24
- * condition for `task.approval_resolved`. The first stable v3 decoder is
25
- * 0.27.0. Cross-release checks use its actual registry tarball
26
- * (`src/__tests__/published-compat.test.ts`).
22
+ * memory intents and so on): they gate on the receiver's configuration.
23
+ *
24
+ * v4 baseline: every v4 server reads `task.approval_resolved`, so its
25
+ * `approval_resolved` gate is gone. One `task.offer` replaces the Agent offer
26
+ * types and sealed is a payload branch, so a gate that a v3 type carried is a
27
+ * field or value row here. The offer is strict, so a daemon that does not know
28
+ * one of its fields stops instead of stripping it. The daemon also refuses an offer whose rows name a flag it did not advertise. No
29
+ * v4 release has been published yet, so no published decoder proves these rows
30
+ * across releases.
27
31
  *
28
32
  * Senders derive their admission flags from {@link requiredReceiverCapabilities}
29
33
  * and nowhere else, so a gate cannot exist in one place and be missing in
@@ -32,7 +36,7 @@
32
36
  */
33
37
  import type { MessageType } from './messages';
34
38
  import type { CapabilityFlag } from './version';
35
- type HttpWireSurface = 'ChallengeRequest' | 'ChallengeResponse' | 'TokenRequest' | 'TokenResponse' | 'EventsPollQuery' | 'EventsPollResponse' | 'MessagesSendRequest' | 'MessagesSendResponse';
39
+ type HttpWireSurface = 'ChallengeRequest' | 'ChallengeResponse' | 'TokenRequest' | 'TokenResponse' | 'PairRequest' | 'PairResponse' | 'EventsPollQuery' | 'EventsPollResponse' | 'MessagesSendRequest' | 'MessagesSendResponse' | 'PresencePublishRequest';
36
40
  export type WireAddition = {
37
41
  readonly kind: 'envelope';
38
42
  readonly rule: 'gated';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byok-sdk/protocol",
3
- "version": "0.30.2",
3
+ "version": "0.32.0",
4
4
  "description": "BYOK SDK wire protocol: envelope schema, message types, and codec helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,6 +29,10 @@
29
29
  "./constants": {
30
30
  "types": "./dist/constants.d.ts",
31
31
  "import": "./dist/constants.js"
32
+ },
33
+ "./agent-memory": {
34
+ "types": "./dist/agent-memory.d.ts",
35
+ "import": "./dist/agent-memory.js"
32
36
  }
33
37
  },
34
38
  "files": [
@@ -49,10 +53,9 @@
49
53
  },
50
54
  "dependencies": {
51
55
  "@cfworker/json-schema": "4.1.1",
52
- "zod": "^4.4.3"
56
+ "zod": "^4.6.6"
53
57
  },
54
58
  "devDependencies": {
55
- "byok-protocol-floor": "https://registry.npmjs.org/@byok-sdk/protocol/-/protocol-0.27.0.tgz",
56
- "byok-protocol-previous": "https://registry.npmjs.org/@byok-sdk/protocol/-/protocol-0.26.1.tgz"
59
+ "byok-protocol-previous": "https://registry.npmjs.org/@byok-sdk/protocol/-/protocol-0.30.2.tgz"
57
60
  }
58
61
  }
@@ -1,28 +0,0 @@
1
- /**
2
- * The device-level half of the task lane's capability gate (contract §8.1).
3
- *
4
- * Contract §8.1 fixes ONE name, `host-mcp-task-context`, carried on two
5
- * independent channels that must both be in place before a host may route a
6
- * tool invocation through the task lane:
7
- *
8
- * 1. **Deployment level** — the ADR-010 `CapabilityDeclarationSchema` a
9
- * deployment serves (`@byok-sdk/cloud`'s `CLOUD_CAPABILITIES`). §8.2(1)
10
- * governs it: a deployment declares the capability only once it is
11
- * completely implemented.
12
- * 2. **Device level** — this constant: a `conn.hello` capability string a host
13
- * consumes through `declaresCapabilities`. §8.3 governs it: a device whose
14
- * build cannot serve the lane simply does not advertise it, and the host
15
- * answers `unavailable` rather than reaching for the device assertion the
16
- * task lane does not accept.
17
- *
18
- * It lives in `@byok-sdk/protocol` for the same reason every other daemon
19
- * capability flag does: this package is the wire vocabulary both sides compile
20
- * against, and a name spelled twice is a name that can drift. It is registered
21
- * in `CAPABILITY_FLAGS` (`./version`), which is what makes it a value a daemon
22
- * can actually send.
23
- *
24
- * The spelling satisfies `@byok-sdk/core`'s `CAPABILITY_NAME_PATTERN`
25
- * (`/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/`) on purpose: the deployment-level channel
26
- * carries this exact literal, so a name legal in only one of the two channels
27
- * would leave them naming different things.
28
- */