@byollm/protocol 0.1.0-alpha.1 → 0.1.0-alpha.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,29 +1,386 @@
1
- // src/audience.ts
1
+ import {
2
+ CONSOLE_FRAME_VERSION,
3
+ CONSOLE_MAX_DATA_BYTES,
4
+ ConsoleBye,
5
+ ConsoleFrame,
6
+ ConsoleHello,
7
+ ConsoleResize,
8
+ ConsoleStdin,
9
+ ConsoleStdout,
10
+ ENVELOPE_BODY_VERSION,
11
+ PublicIdentity,
12
+ consoleDataBytes,
13
+ consoleEnvelope,
14
+ consoleOrder,
15
+ decodeConsoleData,
16
+ decodeEnvelopeInner,
17
+ encodeConsoleData,
18
+ encodeEnvelopeInner,
19
+ envelopeSignedBody,
20
+ fromBase64Url,
21
+ toBase64Url
22
+ } from "./chunk-J3HTAGMX.js";
23
+
24
+ // src/wire.ts
25
+ import { z as z9 } from "zod";
26
+
27
+ // src/keys.ts
28
+ import {
29
+ createHash,
30
+ createPrivateKey,
31
+ createPublicKey,
32
+ generateKeyPairSync,
33
+ sign,
34
+ verify
35
+ } from "crypto";
36
+ import { z } from "zod";
37
+ var StoredKeys = z.object({
38
+ version: z.literal(1),
39
+ identityPublic: z.string().min(1),
40
+ identityPrivate: z.string().min(1),
41
+ encryptionPublic: z.string().min(1),
42
+ encryptionPrivate: z.string().min(1),
43
+ encryptionSig: z.string().min(1),
44
+ createdAt: z.number().int().positive()
45
+ }).strict();
46
+ var ENCRYPTION_KEY_CONTEXT = "byollm/v1/encryption-key";
47
+ function rawPublic(key) {
48
+ const jwk = key.export({ format: "jwk" });
49
+ const x = jwk.x;
50
+ if (typeof x !== "string") throw new Error("key has no raw public component");
51
+ return x;
52
+ }
53
+ function importPublic(raw, crv) {
54
+ return createPublicKey({ key: { kty: "OKP", crv, x: raw }, format: "jwk" });
55
+ }
56
+ function importPrivate(stored) {
57
+ return createPrivateKey({
58
+ key: Buffer.from(stored, "base64"),
59
+ type: "pkcs8",
60
+ format: "der"
61
+ });
62
+ }
63
+ var exportPrivate = (key) => key.export({ type: "pkcs8", format: "der" }).toString("base64");
64
+ function generateKeys(now) {
65
+ const identity = generateKeyPairSync("ed25519");
66
+ const encryption = generateKeyPairSync("x25519");
67
+ const encryptionPublic = rawPublic(encryption.publicKey);
68
+ return {
69
+ version: 1,
70
+ identityPublic: rawPublic(identity.publicKey),
71
+ identityPrivate: exportPrivate(identity.privateKey),
72
+ encryptionPublic,
73
+ encryptionPrivate: exportPrivate(encryption.privateKey),
74
+ encryptionSig: sign(
75
+ null,
76
+ Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${encryptionPublic}`),
77
+ identity.privateKey
78
+ ).toString("base64url"),
79
+ createdAt: now
80
+ };
81
+ }
82
+ function publicIdentityOf(keys) {
83
+ return {
84
+ identity: keys.identityPublic,
85
+ encryption: keys.encryptionPublic,
86
+ encryptionSig: keys.encryptionSig
87
+ };
88
+ }
89
+ function verifyPublicIdentity(identity) {
90
+ try {
91
+ return verify(
92
+ null,
93
+ Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${identity.encryption}`),
94
+ importPublic(identity.identity, "Ed25519"),
95
+ Buffer.from(identity.encryptionSig, "base64url")
96
+ );
97
+ } catch {
98
+ return false;
99
+ }
100
+ }
101
+ function signWith(keys, data) {
102
+ return sign(null, data, importPrivate(keys.identityPrivate)).toString(
103
+ "base64url"
104
+ );
105
+ }
106
+ function verifyWith(identityPublic, data, signature) {
107
+ try {
108
+ return verify(
109
+ null,
110
+ data,
111
+ importPublic(identityPublic, "Ed25519"),
112
+ Buffer.from(signature, "base64url")
113
+ );
114
+ } catch {
115
+ return false;
116
+ }
117
+ }
118
+ var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
119
+ function fingerprint(identityPublic) {
120
+ const digest = createHash("sha256").update(Buffer.from(identityPublic, "base64url")).digest();
121
+ let bits = 0;
122
+ let value = 0;
123
+ let out = "";
124
+ for (const byte of digest.subarray(0, 15)) {
125
+ value = value << 8 | byte;
126
+ bits += 8;
127
+ while (bits >= 5) {
128
+ out += ALPHABET.charAt(value >>> bits - 5 & 31);
129
+ bits -= 5;
130
+ }
131
+ }
132
+ const groups = out.match(/.{1,4}/g) ?? [];
133
+ return `BYOLLM-${groups.join("-")}`;
134
+ }
135
+ var keyId = (identityPublic) => fingerprint(identityPublic);
136
+
137
+ // src/succession.ts
2
138
  import { z as z2 } from "zod";
139
+ var SUCCESSION_CONTEXT = "byollm/v1/site-succession";
140
+ var RETIREMENT_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
141
+ var MAX_SUCCESSION_CHAIN = 64;
142
+ var Succession = z2.object({
143
+ /**
144
+ * The predecessor's public identity — K1, in full.
145
+ *
146
+ * The whole identity rather than the key id, because a daemon meeting a
147
+ * chain it has not seen before has to *verify* each link, and a key id is
148
+ * a fingerprint: enough to compare, never enough to check a signature.
149
+ */
150
+ identity: PublicIdentity,
151
+ /** K1's signature over the statement naming K1 and its successor. */
152
+ signature: z2.string().min(1)
153
+ }).strict();
154
+ function successionStatement(fromKeyId, toKeyId) {
155
+ return Buffer.from(`${SUCCESSION_CONTEXT}:${fromKeyId}:${toKeyId}`);
156
+ }
157
+ function signSuccession(previous, next) {
158
+ return {
159
+ identity: {
160
+ identity: previous.identityPublic,
161
+ encryption: previous.encryptionPublic,
162
+ encryptionSig: previous.encryptionSig
163
+ },
164
+ signature: signWith(
165
+ previous,
166
+ successionStatement(keyId(previous.identityPublic), keyId(next.identity))
167
+ )
168
+ };
169
+ }
170
+ function verifyLink(link, toKeyId) {
171
+ if (!verifyPublicIdentity(link.identity)) return false;
172
+ return verifyWith(
173
+ link.identity.identity,
174
+ successionStatement(keyId(link.identity.identity), toKeyId),
175
+ link.signature
176
+ );
177
+ }
178
+ function walkSuccession(input) {
179
+ const { current, chain, approved } = input;
180
+ if (chain.length === 0) return { path: [current], failure: "no-chain" };
181
+ if (chain.length > MAX_SUCCESSION_CHAIN)
182
+ return { path: [current], failure: "too-long" };
183
+ const steps = [...chain].reverse();
184
+ const path = [current];
185
+ let succeeding = current;
186
+ for (const link of steps) {
187
+ if (!verifyLink(link, succeeding)) return { path, failure: "broken-link" };
188
+ const previous = keyId(link.identity.identity);
189
+ path.unshift(previous);
190
+ if (approved(previous)) return { path, from: previous };
191
+ succeeding = previous;
192
+ }
193
+ return { path, failure: "unknown-origin" };
194
+ }
195
+
196
+ // src/audience.ts
197
+ import { z as z4 } from "zod";
3
198
 
4
199
  // src/backends.ts
5
- import { z } from "zod";
6
- var BackendClass = z.enum(["http", "process"]);
7
- var BackendAccount = z.enum(["open", "subscription"]);
200
+ import { isIP } from "net";
201
+ import { z as z3 } from "zod";
202
+ var BackendClass = z3.enum(["http", "process"]);
203
+ var BACKEND_CLASSES = Object.freeze(BackendClass.options);
204
+ var BackendCost = z3.enum(["free", "metered", "subscription"]);
8
205
  var backend = (b) => Object.freeze(b);
9
206
  var BACKENDS = Object.freeze({
207
+ // -- free: local compute, costs electricity ------------------------------
208
+ ollama: backend({
209
+ id: "ollama",
210
+ label: "Ollama (local)",
211
+ class: "http",
212
+ cost: "free",
213
+ adversarialCorpus: "http",
214
+ defaultBaseUrl: "http://127.0.0.1:11434/v1"
215
+ }),
216
+ mlx: backend({
217
+ id: "mlx",
218
+ label: "MLX (mlx_lm.server, local)",
219
+ class: "http",
220
+ cost: "free",
221
+ adversarialCorpus: "http",
222
+ defaultBaseUrl: "http://127.0.0.1:8080/v1"
223
+ }),
224
+ llamacpp: backend({
225
+ id: "llamacpp",
226
+ label: "llama.cpp server (local)",
227
+ class: "http",
228
+ cost: "free",
229
+ adversarialCorpus: "http",
230
+ defaultBaseUrl: "http://127.0.0.1:8080/v1"
231
+ }),
232
+ vllm: backend({
233
+ id: "vllm",
234
+ label: "vLLM (local)",
235
+ class: "http",
236
+ cost: "free",
237
+ adversarialCorpus: "http",
238
+ defaultBaseUrl: "http://127.0.0.1:8000/v1"
239
+ }),
240
+ lmstudio: backend({
241
+ id: "lmstudio",
242
+ label: "LM Studio (local)",
243
+ class: "http",
244
+ cost: "free",
245
+ adversarialCorpus: "http",
246
+ defaultBaseUrl: "http://127.0.0.1:1234/v1"
247
+ }),
248
+ jan: backend({
249
+ id: "jan",
250
+ label: "Jan (local)",
251
+ class: "http",
252
+ cost: "free",
253
+ adversarialCorpus: "http",
254
+ defaultBaseUrl: "http://127.0.0.1:1337/v1"
255
+ }),
256
+ localai: backend({
257
+ id: "localai",
258
+ label: "LocalAI (local)",
259
+ class: "http",
260
+ cost: "free",
261
+ adversarialCorpus: "http",
262
+ defaultBaseUrl: "http://127.0.0.1:8080/v1"
263
+ }),
264
+ // -- metered: the owner's money, per token -------------------------------
265
+ /**
266
+ * Note the pair: `anthropic` and {@link BACKENDS."claude-cli"} reach the
267
+ * same vendor and land in different cost classes. That is not an
268
+ * inconsistency — it is the axis working. One bills a key per token, the
269
+ * other runs under a personal plan whose terms cover one person's work. Who
270
+ * pays and under what terms is the question; which company is not.
271
+ */
272
+ anthropic: backend({
273
+ id: "anthropic",
274
+ label: "Anthropic (your API key)",
275
+ class: "http",
276
+ cost: "metered",
277
+ adversarialCorpus: "http",
278
+ defaultBaseUrl: "https://api.anthropic.com/v1"
279
+ }),
280
+ openai: backend({
281
+ id: "openai",
282
+ label: "OpenAI (your API key)",
283
+ class: "http",
284
+ cost: "metered",
285
+ adversarialCorpus: "http",
286
+ defaultBaseUrl: "https://api.openai.com/v1"
287
+ }),
288
+ gemini: backend({
289
+ id: "gemini",
290
+ label: "Google Gemini (your API key)",
291
+ class: "http",
292
+ cost: "metered",
293
+ adversarialCorpus: "http",
294
+ defaultBaseUrl: "https://generativelanguage.googleapis.com/v1beta/openai"
295
+ }),
296
+ grok: backend({
297
+ id: "grok",
298
+ label: "xAI Grok (your API key)",
299
+ class: "http",
300
+ cost: "metered",
301
+ adversarialCorpus: "http",
302
+ defaultBaseUrl: "https://api.x.ai/v1"
303
+ }),
304
+ groq: backend({
305
+ id: "groq",
306
+ label: "Groq (your API key)",
307
+ class: "http",
308
+ cost: "metered",
309
+ adversarialCorpus: "http",
310
+ defaultBaseUrl: "https://api.groq.com/openai/v1"
311
+ }),
312
+ openrouter: backend({
313
+ id: "openrouter",
314
+ label: "OpenRouter (your API key)",
315
+ class: "http",
316
+ cost: "metered",
317
+ adversarialCorpus: "http",
318
+ defaultBaseUrl: "https://openrouter.ai/api/v1"
319
+ }),
320
+ together: backend({
321
+ id: "together",
322
+ label: "Together AI (your API key)",
323
+ class: "http",
324
+ cost: "metered",
325
+ adversarialCorpus: "http",
326
+ defaultBaseUrl: "https://api.together.xyz/v1"
327
+ }),
328
+ deepseek: backend({
329
+ id: "deepseek",
330
+ label: "DeepSeek (your API key)",
331
+ class: "http",
332
+ cost: "metered",
333
+ adversarialCorpus: "http",
334
+ defaultBaseUrl: "https://api.deepseek.com/v1"
335
+ }),
336
+ mistral: backend({
337
+ id: "mistral",
338
+ label: "Mistral (your API key)",
339
+ class: "http",
340
+ cost: "metered",
341
+ adversarialCorpus: "http",
342
+ defaultBaseUrl: "https://api.mistral.ai/v1"
343
+ }),
344
+ // -- the escape hatch ----------------------------------------------------
10
345
  "openai-http": backend({
11
346
  id: "openai-http",
12
- label: "OpenAI-compatible HTTP server (Ollama, MLX, llama.cpp, vLLM)",
347
+ label: "Any OpenAI-compatible server",
13
348
  class: "http",
14
- account: "open",
349
+ // Unknown until the base URL is known: local means free, remote means
350
+ // metered, and the owner does not get to say otherwise
351
+ // ({@link MUSTS.REMOTE_IS_NEVER_FREE}).
352
+ cost: null,
15
353
  adversarialCorpus: "http"
16
354
  }),
355
+ // -- subscription: someone else's terms ----------------------------------
17
356
  "claude-cli": backend({
18
357
  id: "claude-cli",
19
358
  label: "Claude CLI (your subscription)",
20
359
  class: "process",
21
- account: "subscription",
360
+ cost: "subscription",
361
+ adversarialCorpus: "process"
362
+ }),
363
+ /**
364
+ * OpenAI's Codex CLI, on a ChatGPT plan — byollm_016 stage 3.
365
+ *
366
+ * `subscription`, so `SUBSCRIPTION_SELF_LOCK` pins it to its owner's own
367
+ * work whatever the config says. That is load-bearing here in a way it is
368
+ * not for `claude-cli`: Codex is an *agent*, and its default feature set
369
+ * includes a shell tool, browser control and computer use. The daemon
370
+ * disables every one of them, verified against the shipped binary rather
371
+ * than assumed — see `codex-cli.ts` — but the self-lock is the floor under
372
+ * that verification rather than a duplicate of it.
373
+ */
374
+ "codex-cli": backend({
375
+ id: "codex-cli",
376
+ label: "Codex CLI (your ChatGPT plan)",
377
+ class: "process",
378
+ cost: "subscription",
22
379
  adversarialCorpus: "process"
23
380
  })
24
381
  });
25
382
  var BACKEND_IDS = Object.freeze(Object.keys(BACKENDS));
26
- var BackendIdSchema = z.enum(
383
+ var BackendIdSchema = z3.enum(
27
384
  BACKEND_IDS
28
385
  );
29
386
  function isBackendId(value) {
@@ -32,66 +389,343 @@ function isBackendId(value) {
32
389
  function backendDescriptor(id) {
33
390
  return BACKENDS[id];
34
391
  }
392
+ function isLocalHost(hostname) {
393
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
394
+ if (host === "localhost" || host.endsWith(".localhost")) return true;
395
+ const version = isIP(host);
396
+ if (version === 0) return false;
397
+ if (version === 6) {
398
+ if (host === "::1") return true;
399
+ return /^f[cd]/.test(host);
400
+ }
401
+ if (host.startsWith("127.")) return true;
402
+ if (host.startsWith("10.")) return true;
403
+ if (host.startsWith("192.168.")) return true;
404
+ return /^172\.(1[6-9]|2\d|3[01])\./.test(host);
405
+ }
406
+ function isCloudTaggedModel(model) {
407
+ return /:[^:]*cloud$/.test(model);
408
+ }
409
+ function resolveCost(id, baseUrl, model) {
410
+ return classifyCost(id, baseUrl, model).cost;
411
+ }
412
+ function backendName(id) {
413
+ return BACKENDS[id].label.replace(/\s*\([^)]*\)$/, "");
414
+ }
415
+ function classifyCost(id, baseUrl, model) {
416
+ const declared = BACKENDS[id].cost;
417
+ if (declared === "free" && model !== void 0 && isCloudTaggedModel(model)) {
418
+ return {
419
+ cost: "metered",
420
+ because: `its model tag ends in \`:cloud\`, so the work runs on your provider's cloud account rather than on this machine \u2014 ${backendName(id)} serves hosted models through the same local address as local ones`
421
+ };
422
+ }
423
+ if (declared !== null) {
424
+ const label = BACKENDS[id].label;
425
+ return {
426
+ cost: declared,
427
+ because: {
428
+ subscription: `${label} runs on an account you subscribe to`,
429
+ metered: `${label} bills per token`,
430
+ free: `${label} runs on this machine`
431
+ }[declared]
432
+ };
433
+ }
434
+ if (model !== void 0 && isCloudTaggedModel(model)) {
435
+ return {
436
+ cost: "metered",
437
+ because: `its model tag ends in \`:cloud\`, so the work runs on your provider's cloud account rather than on this machine`
438
+ };
439
+ }
440
+ if (baseUrl === void 0) {
441
+ return {
442
+ cost: "metered",
443
+ because: "it has no address, so where the work runs cannot be checked"
444
+ };
445
+ }
446
+ try {
447
+ return isLocalHost(new URL(baseUrl).hostname) ? { cost: "free", because: "it runs on this machine" } : {
448
+ cost: "metered",
449
+ because: "its address is not on this machine, so the work leaves it"
450
+ };
451
+ } catch {
452
+ return {
453
+ cost: "metered",
454
+ because: "its address cannot be read, so where the work runs is unknown"
455
+ };
456
+ }
457
+ }
35
458
 
36
459
  // src/audience.ts
37
- var Audience = z2.enum(["self", "named", "public"]);
38
- var OfferScope = z2.enum(["self", "named", "public"]);
460
+ var Audience = z4.enum(["private", "team"]);
461
+ var OfferScope = z4.enum(["private", "team"]);
39
462
  var AUDIENCES = Object.freeze(Audience.options);
40
463
  var OFFER_SCOPES = Object.freeze(OfferScope.options);
41
- var MatchRefusal = z2.enum([
464
+ var MatchRefusal = z4.enum([
42
465
  /** The daemon advertises no capability for this kind. */
43
466
  "no-capability",
44
- /** Job is `self` but this daemon belongs to a different user. */
467
+ /** Job is `private` but this daemon belongs to a different user. */
45
468
  "audience-self-other-owner",
46
- /** Job is `named` but this daemon's local allowlist does not admit the owner. */
469
+ /**
470
+ * Job is `team` and nothing this device verified admits the job's owner.
471
+ *
472
+ * The id predates the grant and is kept, because ids are public and cited
473
+ * by conformance output. What it means has not moved: this device was not
474
+ * shown anything it could check.
475
+ */
47
476
  "not-locally-allowed",
48
- /** Job is `named`/`public` but the server's own allowlist excludes this runner. */
477
+ /** Job is `team` but the server's own allowlist excludes this runner. */
49
478
  "not-in-server-allowlist",
50
- /** The backend offers only `self` and the job belongs to someone else. */
479
+ /** The service offers only `private` and the job belongs to someone else. */
51
480
  "offer-scope-too-narrow",
52
- /** The matched backend is subscription-class, which is locked to `self`. */
53
- "subscription-self-lock"
481
+ /** The matched backend is subscription-class, which is locked to `private`. */
482
+ "subscription-self-lock",
483
+ /** The backend spends the owner's money and they have not agreed to share it. */
484
+ "metered-no-spend-consent",
485
+ /** The backend is shared but has spent its ceiling for now. */
486
+ "metered-ceiling-reached"
54
487
  ]);
55
488
  var ALLOWED = Object.freeze({ ok: true });
56
489
  var refuse = (refusal) => Object.freeze({ ok: false, refusal });
57
- function effectiveOfferScope(configured, account) {
58
- return account === "subscription" ? "self" : configured;
490
+ function effectiveOfferScope(configured, cost, spend) {
491
+ if (cost === "subscription") return "private";
492
+ if (cost === "metered" && spend?.acknowledged !== true) return "private";
493
+ return configured;
59
494
  }
60
495
  function matchAudience(job, daemon) {
61
496
  const sameOwner = job.owner === daemon.owner;
62
- if (job.audience === "self" && !sameOwner) {
497
+ if (job.audience === "private" && !sameOwner) {
63
498
  return refuse("audience-self-other-owner");
64
499
  }
65
- if (job.audience === "named" && !sameOwner && job.audienceAllow !== void 0 && !job.audienceAllow.includes(daemon.owner)) {
500
+ if (job.audience === "team" && !sameOwner && job.audienceAllow !== void 0 && !job.audienceAllow.includes(daemon.owner)) {
66
501
  return refuse("not-in-server-allowlist");
67
502
  }
68
- const scope = effectiveOfferScope(daemon.offerScope, daemon.account);
503
+ const scope = effectiveOfferScope(
504
+ daemon.offerScope,
505
+ daemon.cost,
506
+ daemon.spend
507
+ );
69
508
  if (sameOwner) {
70
509
  return ALLOWED;
71
510
  }
72
- if (daemon.account === "subscription") {
511
+ if (daemon.cost === "subscription") {
73
512
  return refuse("subscription-self-lock");
74
513
  }
514
+ if (daemon.cost === "metered") {
515
+ if (daemon.spend?.acknowledged !== true) {
516
+ return refuse("metered-no-spend-consent");
517
+ }
518
+ if (daemon.spend.ceilingReached === true) {
519
+ return refuse("metered-ceiling-reached");
520
+ }
521
+ }
75
522
  switch (scope) {
76
- case "self":
523
+ case "private":
77
524
  return refuse("offer-scope-too-narrow");
78
- case "named":
79
- return daemon.locallyAllows(job.owner) ? ALLOWED : refuse("not-locally-allowed");
80
- case "public":
81
- return ALLOWED;
525
+ case "team":
526
+ return daemon.admits(job.owner) ? ALLOWED : refuse("not-locally-allowed");
82
527
  }
83
528
  }
84
529
  var REFUSAL_MESSAGES = Object.freeze({
85
- "no-capability": "no backend on this machine is configured and healthy for that job kind",
86
- "audience-self-other-owner": "the job is private to its owner and this machine is paired to someone else",
87
- "not-locally-allowed": "the job's owner is not on this machine's allowlist (byollm allow <server> <user>)",
88
- "not-in-server-allowlist": "the app restricted this job to named runners and this machine is not one of them",
89
- "offer-scope-too-narrow": "this backend is offered to its owner only (byollm offer <backend> named|public to widen)",
90
- "subscription-self-lock": "subscription-backed models run their owner's work only \u2014 this is a protocol rule, not a setting"
530
+ "no-capability": "no backend on this device is configured and healthy for that job kind",
531
+ "audience-self-other-owner": "the job is private to its owner and this device is paired to someone else",
532
+ "not-locally-allowed": "nothing this device can verify says the job's owner may use it",
533
+ "not-in-server-allowlist": "the app restricted this job to named runners and this device is not one of them",
534
+ "offer-scope-too-narrow": "this service is offered to its owner only (`byollm offer <service> team` to widen)",
535
+ "subscription-self-lock": "subscription-backed models run their owner's work only \u2014 this is a protocol rule, not a setting",
536
+ "metered-no-spend-consent": "this backend bills its owner per token, and they have not agreed to spend it on other people's work",
537
+ "metered-ceiling-reached": "this backend is shared but has reached the spend ceiling its owner set"
91
538
  });
92
539
 
540
+ // src/job.ts
541
+ import { z as z7 } from "zod";
542
+
543
+ // src/grant.ts
544
+ import { Buffer as Buffer2 } from "buffer";
545
+ import { z as z5 } from "zod";
546
+ var GRANT_MAX_AGE_MS = 12e4;
547
+ var CLOCK_SKEW_WARN_MS = 3e4;
548
+ var CLOCK_ATTRIBUTION_MS = 5e3;
549
+ var GRANT_CONTEXT = "byollm/v1/grant";
550
+ var SignedGrant = z5.object({
551
+ /**
552
+ * This grant's own id — what makes it single-use.
553
+ *
554
+ * **Not the job id, and the difference is load-bearing.** Binding
555
+ * single-use to `jobId` would refuse a legitimate retry: a claim that
556
+ * times out is re-claimed, the control plane authors a second grant for
557
+ * the same job, and a device that recorded the job id as spent would
558
+ * reject its own recovery. A fresh id per authorship replays nothing and
559
+ * retries fine.
560
+ */
561
+ grantId: z5.string().min(1),
562
+ /**
563
+ * The job this grant admits, and only this one.
564
+ *
565
+ * A grant lifted from one job and presented for another is the obvious
566
+ * attack, and this field is why it fails.
567
+ */
568
+ jobId: z5.string().min(1),
569
+ /**
570
+ * The site the work came from, **as a key id** — byollm-review 2026-08-27.
571
+ *
572
+ * This was `siteId`, holding the site's id in the control plane's
573
+ * namespace, and it was signed by the engine and read by nobody. A signed
574
+ * field nobody checks is not a weak guarantee, it is the appearance of
575
+ * one: the design says "the grant carries the site", and nothing anywhere
576
+ * compared it to anything.
577
+ *
578
+ * It could not be compared. Job ids are chosen per site, so a grant
579
+ * authored for (site A, `job_1`) satisfied every device check against a
580
+ * stub naming (site B, `job_1`) — but the device holds sites only by the
581
+ * key ids it pinned, and had no way to relate a control-plane uuid to
582
+ * one. Checking the field would have meant a lookup through the party the
583
+ * grant exists to distrust.
584
+ *
585
+ * So the namespace changes to the one the device already has, and the
586
+ * name changes with it: this is the same value as {@link JobStub.site},
587
+ * compared directly, no lookup and nothing to believe. The control-plane
588
+ * id is not carried alongside — it had no reader, and keeping an
589
+ * unchecked field beside a checked one is how this hole was dug.
590
+ */
591
+ site: z5.string().min(1),
592
+ /** Whose job it is — the person the site enqueued for. */
593
+ user: z5.string().min(1),
594
+ /**
595
+ * Whose device it is for.
596
+ *
597
+ * Passed to {@link verifyGrant} rather than read out of the document, for
598
+ * the reason every verifier here takes its subject as an argument: a
599
+ * verifier that recovered the owner from the signed bytes would accept a
600
+ * genuine grant belonging to somebody else and pass every check.
601
+ */
602
+ owner: z5.string().min(1),
603
+ /** The site purpose this job serves — byollm_016 Amendment L. */
604
+ purpose: z5.string().min(1),
605
+ /** The kind of work. */
606
+ kind: z5.string().min(1),
607
+ /**
608
+ * The service the control plane resolved this (purpose, kind) to, from
609
+ * the user's own mapping.
610
+ *
611
+ * Selection is the control plane's; **offer-consistency is the
612
+ * device's**. A device verifies it actually offers this service, at a
613
+ * scope that includes {@link user}, before running anything.
614
+ */
615
+ service: z5.string().min(1),
616
+ /** When the control plane signed it — epoch ms, the only anchor for age. */
617
+ issuedAt: z5.number().int().positive(),
618
+ /** Base64url Ed25519 over {@link grantStatement}. */
619
+ signature: z5.string().min(1)
620
+ }).strict();
621
+ var GRANT_SIGNED_FIELDS = Object.freeze(
622
+ Object.keys(SignedGrant.shape).filter((key) => key !== "signature").sort()
623
+ );
624
+ function grantStatement(claims) {
625
+ return Buffer2.from(
626
+ JSON.stringify([
627
+ GRANT_CONTEXT,
628
+ ...GRANT_SIGNED_FIELDS.map((field) => claims[field])
629
+ ]),
630
+ "utf8"
631
+ );
632
+ }
633
+ function signGrant(keys, claims) {
634
+ return { ...claims, signature: signWith(keys, grantStatement(claims)) };
635
+ }
636
+ function verifyGrant(input) {
637
+ const { grant, now } = input;
638
+ if (grant.owner !== input.owner) return "wrong-owner";
639
+ if (grant.jobId !== input.jobId) return "wrong-job";
640
+ const age = now - grant.issuedAt;
641
+ if (age < -CLOCK_SKEW_WARN_MS) return "from-the-future";
642
+ if (age > (input.maxAgeMs ?? GRANT_MAX_AGE_MS)) return "expired";
643
+ return verifyWith(
644
+ input.controlPlanePublic,
645
+ grantStatement(grant),
646
+ grant.signature
647
+ ) ? null : "bad-signature";
648
+ }
649
+
650
+ // src/json-size.ts
651
+ function jsonLength(value) {
652
+ try {
653
+ return count(value, /* @__PURE__ */ new Set());
654
+ } catch {
655
+ return void 0;
656
+ }
657
+ }
658
+ var OutOfDomain = class extends Error {
659
+ };
660
+ function bail() {
661
+ throw new OutOfDomain("not a JSON value");
662
+ }
663
+ function count(value, seen) {
664
+ if (value === null) return 4;
665
+ switch (typeof value) {
666
+ case "boolean":
667
+ return value ? 4 : 5;
668
+ // true / false
669
+ case "number":
670
+ return Number.isFinite(value) ? String(value).length : 4;
671
+ case "string":
672
+ return stringLength(value);
673
+ case "object":
674
+ break;
675
+ default:
676
+ return bail();
677
+ }
678
+ const object = value;
679
+ if (seen.has(object)) return bail();
680
+ seen.add(object);
681
+ try {
682
+ if (Array.isArray(object)) {
683
+ let total2 = 2 + Math.max(0, object.length - 1);
684
+ for (const element of object) {
685
+ total2 += element === void 0 || typeof element === "function" ? 4 : count(element, seen);
686
+ }
687
+ return total2;
688
+ }
689
+ if ("toJSON" in object) return bail();
690
+ const proto = Object.getPrototypeOf(object);
691
+ if (proto !== Object.prototype) return bail();
692
+ let total = 2;
693
+ let first = true;
694
+ for (const [key, entry] of Object.entries(object)) {
695
+ if (entry === void 0 || typeof entry === "function") continue;
696
+ total += (first ? 0 : 1) + stringLength(key) + 1 + count(entry, seen);
697
+ first = false;
698
+ }
699
+ return total;
700
+ } finally {
701
+ seen.delete(object);
702
+ }
703
+ }
704
+ function stringLength(text) {
705
+ let total = 2;
706
+ for (let index = 0; index < text.length; index += 1) {
707
+ const code = text.charCodeAt(index);
708
+ if (code === 34 || code === 92) {
709
+ total += 2;
710
+ } else if (code < 32) {
711
+ total += code === 8 || code === 9 || code === 10 || code === 12 || code === 13 ? 2 : 6;
712
+ } else if (code >= 55296 && code <= 57343) {
713
+ const paired = code <= 56319 && index + 1 < text.length && text.charCodeAt(index + 1) >= 56320 && text.charCodeAt(index + 1) <= 57343;
714
+ if (paired) {
715
+ total += 2;
716
+ index += 1;
717
+ } else {
718
+ total += 6;
719
+ }
720
+ } else {
721
+ total += 1;
722
+ }
723
+ }
724
+ return total;
725
+ }
726
+
93
727
  // src/kinds.ts
94
- import { z as z3 } from "zod";
728
+ import { z as z6 } from "zod";
95
729
  var PAYLOAD_LIMITS = Object.freeze({
96
730
  /** Max characters in any single text field. */
97
731
  maxTextChars: 1e6,
@@ -100,146 +734,1493 @@ var PAYLOAD_LIMITS = Object.freeze({
100
734
  /** Max characters across the whole payload. */
101
735
  maxTotalChars: 4e6
102
736
  });
103
- var ChatMessage = z3.object({
104
- role: z3.enum(["system", "user", "assistant"]),
105
- content: z3.string().max(PAYLOAD_LIMITS.maxTextChars)
106
- });
107
- var GeneratePayload = z3.object({
108
- prompt: z3.string().min(1).max(PAYLOAD_LIMITS.maxTextChars),
109
- system: z3.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
110
- }).strict();
111
- var ChatPayload = z3.object({
112
- messages: z3.array(ChatMessage).min(1).max(PAYLOAD_LIMITS.maxMessages),
113
- system: z3.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
737
+ var ChatMessage = z6.object({
738
+ role: z6.enum(["system", "user", "assistant"]),
739
+ content: z6.string().max(PAYLOAD_LIMITS.maxTextChars)
114
740
  }).strict();
115
- var JobKind = z3.enum(["llm.generate", "llm.chat"]);
741
+ var GeneratePayload = z6.object({
742
+ prompt: z6.string().min(1).max(PAYLOAD_LIMITS.maxTextChars),
743
+ system: z6.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
744
+ }).strict().refine(
745
+ (payload) => payload.prompt.length + (payload.system?.length ?? 0) <= PAYLOAD_LIMITS.maxTotalChars,
746
+ {
747
+ message: `payload exceeds ${String(PAYLOAD_LIMITS.maxTotalChars)} characters`
748
+ }
749
+ );
750
+ var ChatPayload = z6.object({
751
+ messages: z6.array(ChatMessage).min(1).max(PAYLOAD_LIMITS.maxMessages),
752
+ system: z6.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
753
+ }).strict().refine(
754
+ (payload) => payload.messages.reduce((sum, m) => sum + m.content.length, 0) + (payload.system?.length ?? 0) <= PAYLOAD_LIMITS.maxTotalChars,
755
+ {
756
+ message: `payload exceeds ${String(PAYLOAD_LIMITS.maxTotalChars)} characters`
757
+ }
758
+ );
759
+ var JobKind = z6.enum(["llm.generate", "llm.chat"]);
116
760
  var JOB_KINDS = Object.freeze(JobKind.options);
117
- var KindedPayload = z3.discriminatedUnion("kind", [
118
- z3.object({ kind: z3.literal("llm.generate"), payload: GeneratePayload }),
119
- z3.object({ kind: z3.literal("llm.chat"), payload: ChatPayload })
761
+ var KindedPayload = z6.discriminatedUnion("kind", [
762
+ // Strict on the wrappers too. A union member that strips is a door beside
763
+ // the one that is locked: the payloads inside are strict, and an extra key
764
+ // on the envelope vanished just as quietly.
765
+ z6.object({ kind: z6.literal("llm.generate"), payload: GeneratePayload }).strict(),
766
+ z6.object({ kind: z6.literal("llm.chat"), payload: ChatPayload }).strict()
120
767
  ]);
121
768
  function isJobKind(value) {
122
769
  return JOB_KINDS.includes(value);
123
770
  }
124
- function payloadTextLength(kinded) {
125
- if (kinded.kind === "llm.generate") {
126
- return kinded.payload.prompt.length + (kinded.payload.system?.length ?? 0);
771
+ function payloadTextLength(kinded) {
772
+ if (kinded.kind === "llm.generate") {
773
+ return kinded.payload.prompt.length + (kinded.payload.system?.length ?? 0);
774
+ }
775
+ const messages = kinded.payload.messages.reduce(
776
+ (sum, m) => sum + m.content.length,
777
+ 0
778
+ );
779
+ return messages + (kinded.payload.system?.length ?? 0);
780
+ }
781
+
782
+ // src/job.ts
783
+ var JobState = z7.enum([
784
+ "queued",
785
+ "claimed",
786
+ "running",
787
+ "ok",
788
+ "error",
789
+ "canceled",
790
+ "expired"
791
+ ]);
792
+ var TERMINAL_STATES = Object.freeze([
793
+ "ok",
794
+ "error",
795
+ "canceled",
796
+ "expired"
797
+ ]);
798
+ function isTerminal(state) {
799
+ return TERMINAL_STATES.includes(state);
800
+ }
801
+ var TRANSITIONS = Object.freeze({
802
+ queued: ["claimed", "expired", "canceled"],
803
+ // A claimed job returns to `queued` when its lease expires un-renewed
804
+ // ({@link MUSTS.LEASE_RECLAIMABLE}).
805
+ claimed: ["running", "queued", "canceled", "error"],
806
+ running: ["ok", "error", "canceled", "queued"],
807
+ ok: [],
808
+ error: [],
809
+ canceled: [],
810
+ expired: []
811
+ });
812
+ function canTransition(from, to) {
813
+ return TRANSITIONS[from].includes(to);
814
+ }
815
+ var Lease = z7.object({
816
+ /**
817
+ * Identifies *this* grant, not just its holder.
818
+ *
819
+ * A runner can hold a job, release it, and claim it again — three leases,
820
+ * one runner id. Without an id for the grant itself, a lease-scoped request
821
+ * names a mutable target ambiguously, and a replayed release from the first
822
+ * grant lands on the third: the job returns to the queue while the daemon
823
+ * is mid-execution, and the work runs twice on the owner's hardware.
824
+ *
825
+ * That was a live hole, found in review after signed requests shipped. The
826
+ * signature scheme's replay argument rests on endpoints being idempotent —
827
+ * and release *is*, per lease, but not across leases, because nothing in
828
+ * the request said which one.
829
+ */
830
+ id: z7.string().min(1),
831
+ /** The runner holding the lease. */
832
+ runnerId: z7.string().min(1),
833
+ /** Epoch milliseconds after which the claim is void. */
834
+ expiresAt: z7.number().int().positive()
835
+ }).strict();
836
+ var JobPayload = z7.union([GeneratePayload, ChatPayload]);
837
+ var ClaimedJob = z7.object({
838
+ id: z7.string().min(1),
839
+ kind: JobKind,
840
+ payload: JobPayload,
841
+ audience: Audience,
842
+ /** The app's id for the user who enqueued it. */
843
+ owner: z7.string().min(1),
844
+ /**
845
+ * Which site's job — V1-3.
846
+ *
847
+ * The stub has always carried it; the opened job did not, so everything
848
+ * downstream of the payload — the ingress line above all — recorded a job
849
+ * id that belongs to a site without saying which. Two sites can choose
850
+ * the same id, and the meter is the product.
851
+ *
852
+ * Optional so a caller assembling a job by hand is not forced to invent
853
+ * one, and so this reads as what it is: a fact about where the work came
854
+ * from, not a second copy of the routing key.
855
+ */
856
+ site: z7.string().min(1).optional(),
857
+ /**
858
+ * Which of the owner's services runs this — resolved, not requested.
859
+ *
860
+ * The daemon picks the backend from this, so it has to be the answer
861
+ * rather than a wish. On a relayed route it is copied off the **grant**,
862
+ * where a control plane put the person's own mapping and signed it; a
863
+ * site never named it and could not.
864
+ *
865
+ * It used to be what the site asked for, which made a job that selected a
866
+ * non-default service liable to be served by the default instead — the
867
+ * substitution `NO_PAYLOAD_ROUTING` forbids. Amendment L removed the
868
+ * asking; what is left is the answering.
869
+ *
870
+ * Optional, because direct mode has no control plane to resolve anything
871
+ * and the owner's own defaults answer under the ambiguity law.
872
+ */
873
+ service: z7.string().min(1).optional(),
874
+ /**
875
+ * When the work stops being worth doing — the stub's own TTL, carried on
876
+ * so the daemon can stop at it (B199).
877
+ *
878
+ * **A third clock, and not the lease.** The lease bounds how long this
879
+ * device holds the claim; this bounds how long the answer is wanted. A box
880
+ * ground two chat jobs toward `maxWallClockMs` — ten minutes — while their
881
+ * TTL was two, holding both slots and claiming nothing the whole time.
882
+ *
883
+ * Optional because the field is carried rather than required: a caller
884
+ * assembling a `ClaimedJob` without it gets the unclamped ceiling, which is
885
+ * the behaviour that existed before. The stub always has it, and the
886
+ * runner's own call site already spread it — this makes the type say so.
887
+ */
888
+ deadlineAt: z7.number().int().positive().optional(),
889
+ lease: Lease
890
+ }).strict();
891
+ var ResultProvenance = z7.object({
892
+ /** The audience the job ran under. */
893
+ audience: Audience,
894
+ /** The runner that produced it. */
895
+ runnerId: z7.string().min(1),
896
+ /** The runner owner's id in this app's namespace. */
897
+ runnerOwner: z7.string().min(1),
898
+ /** Which backend class produced it — an HTTP call or a sandboxed spawn. */
899
+ backendClass: BackendClass,
900
+ /** The model the runner reports having used. */
901
+ model: z7.string().min(1),
902
+ /**
903
+ * False only for `self` jobs. When true the app MUST treat `text` as
904
+ * untrusted third-party content.
905
+ */
906
+ untrusted: z7.boolean()
907
+ }).strict();
908
+ function provenanceFor(input) {
909
+ return {
910
+ audience: input.audience,
911
+ runnerId: input.runnerId,
912
+ runnerOwner: input.runnerOwner,
913
+ backendClass: input.backendClass,
914
+ model: input.model,
915
+ untrusted: input.audience !== "private"
916
+ };
917
+ }
918
+ var StopReasonSchema = z7.enum([
919
+ "end",
920
+ "length",
921
+ "stop-sequence",
922
+ "unknown"
923
+ ]);
924
+ var RunMetadata = z7.object({
925
+ /** Which model actually served it. */
926
+ model: z7.string().min(1),
927
+ backendClass: BackendClass,
928
+ /** Wall-clock milliseconds the backend call took. */
929
+ durationMs: z7.number().int().nonnegative(),
930
+ /**
931
+ * Why generation stopped — B064 step 4.
932
+ *
933
+ * Here rather than on {@link JobResultOk} because the outcome is the
934
+ * ANSWER and this is the daemon's signed account of how it was produced.
935
+ * Why generation ended is the same kind of fact as how long it took.
936
+ *
937
+ * **Optional because it is meaningless, not because it is new.** A
938
+ * cancelled job has no model to have stopped and an error has no
939
+ * generation to have ended, and `ran` travels on those arms too.
940
+ * Instruction 10 forbids optionality bought for compatibility — a
941
+ * version gate wearing a question mark — and this is not that: it is
942
+ * absent exactly where it would be a fact about nothing. Present on
943
+ * every `ok` result, always, because the whole value is the difference
944
+ * between `end`, `length` and `unknown` and that difference only exists
945
+ * if it is always there.
946
+ */
947
+ stop: StopReasonSchema.optional(),
948
+ /**
949
+ * Whether the adapter could read a stop signal at all — B105's lesson,
950
+ * carried to the wire.
951
+ *
952
+ * Without this the site re-commits the defect B064's third mapping kind
953
+ * was introduced to fix. `unknown` is TWO facts: an adapter that cannot
954
+ * report one, and an adapter that reported a word we do not map — and
955
+ * `openai-http` maps `stop` and `length` and nothing else, so the second
956
+ * is the common case rather than the corner.
957
+ *
958
+ * A site told only `unknown` would say "we do not know why this stopped"
959
+ * for a `claude-cli` job forever, which is true, and for a
960
+ * `content_filter` result, which is not the same thing at all.
961
+ */
962
+ stopReported: z7.boolean().optional()
963
+ }).strict();
964
+ var JobResultOk = z7.object({
965
+ outcome: z7.literal("ok"),
966
+ text: z7.string(),
967
+ /** Optional reference to a stored artifact; never a local path. */
968
+ artifactUrl: z7.url().optional()
969
+ }).strict();
970
+ var JobResultError = z7.object({
971
+ outcome: z7.literal("error"),
972
+ code: z7.string().min(1),
973
+ message: z7.string().min(1),
974
+ /** Whether the app may reasonably re-enqueue. */
975
+ retryable: z7.boolean()
976
+ }).strict();
977
+ var JobResultCanceled = z7.object({
978
+ outcome: z7.literal("canceled")
979
+ }).strict();
980
+ var JobOutcome = z7.discriminatedUnion("outcome", [
981
+ JobResultOk,
982
+ JobResultError,
983
+ JobResultCanceled
984
+ ]);
985
+ var RefusalReason = z7.enum([
986
+ /**
987
+ * Two or more services answer this kind and the owner has named no default,
988
+ * so the kind is withheld. Nobody may pick on the owner's behalf — the wrong
989
+ * guess is the metered one.
990
+ *
991
+ * Told apart from its neighbour deliberately, and the line is whether a
992
+ * requester can walk a namespace. There are two kinds; asking about one
993
+ * enumerates nothing they could not learn from what the device advertises,
994
+ * and the difference is actionable — "the owner has not chosen" is fixable
995
+ * by the owner, "the default cannot serve you" is not. It is also already
996
+ * what a team member sees on the devices page: `awaitingDefault` carries
997
+ * exactly this, by kind, for exactly this reason.
998
+ */
999
+ "default-ambiguity",
1000
+ /**
1001
+ * A default exists and this requester can never use it — byollm_016's
1002
+ * defaults-meet-audiences corner.
1003
+ *
1004
+ * The specimen: an owner's default for `llm.chat` is their Claude
1005
+ * subscription, self-locked by `SUBSCRIPTION_SELF_LOCK`. A team member's
1006
+ * job resolves to it and can never be served by it. That must be a refusal
1007
+ * on the spot, not a wait that expires an hour later looking like nobody
1008
+ * was online.
1009
+ *
1010
+ * Bounded like the value above, and unprobeable for the same reason: the
1011
+ * requester named nothing, so there is no name space to walk.
1012
+ */
1013
+ "default-unusable"
1014
+ ]);
1015
+ var JobRefused = z7.object({
1016
+ outcome: z7.literal("refused"),
1017
+ reason: RefusalReason,
1018
+ /** Plain words for a human reading a log, never parsed. */
1019
+ message: z7.string().min(1)
1020
+ }).strict();
1021
+ var REFUSAL_TEXT = Object.freeze({
1022
+ "default-ambiguity": "this device serves that kind from more than one service and its owner has not chosen which",
1023
+ "default-unusable": "this device's default for that kind cannot run work for you"
1024
+ });
1025
+ var SealedOutcome = z7.object({ outcome: JobOutcome, ran: RunMetadata }).strict();
1026
+ var DeliveredResult = z7.object({
1027
+ jobId: z7.string().min(1),
1028
+ state: JobState,
1029
+ outcome: JobOutcome.optional(),
1030
+ provenance: ResultProvenance.optional(),
1031
+ /**
1032
+ * Present, and always `true`, when this did not come from a runner —
1033
+ * {@link MUSTS.FALLBACK_LABELED}.
1034
+ *
1035
+ * The app's own `onNoRunner` value produced it: a hosted model, a cached
1036
+ * answer, an apology. It never travels on the wire, because nothing on
1037
+ * the wire produced it; it exists so that a result which did not come
1038
+ * from the user's own compute cannot be reported as though it did.
1039
+ *
1040
+ * A literal rather than a boolean, so `fallback: false` is not a
1041
+ * spelling anybody can reach for. The absence of this field means a
1042
+ * runner ran the job, and the *server* stamps it — an app cannot supply
1043
+ * a substitute that hides what it is.
1044
+ */
1045
+ fallback: z7.literal(true).optional()
1046
+ }).strict();
1047
+ var SizeClass = z7.enum(["small", "medium", "large", "unbounded"]);
1048
+ var SIZE_CLASSES = Object.freeze(SizeClass.options);
1049
+ var MAX_ENVELOPE_BYTES = 6 * 1024 * 1024;
1050
+ function envelopeBytes(envelope) {
1051
+ const counted = jsonLength(envelope);
1052
+ if (counted !== void 0) return counted;
1053
+ const serialised = JSON.stringify(envelope);
1054
+ return serialised === void 0 ? 0 : serialised.length;
1055
+ }
1056
+ function describeBytes(bytes) {
1057
+ return `${(Math.ceil(bytes / (1024 * 1024) * 10) / 10).toFixed(1)} MB`;
1058
+ }
1059
+ function tooLargeMessage(input) {
1060
+ return `this message is ${describeBytes(input.bytes)} and the limit is ${describeBytes(input.limit)} \u2014 it is a limit on one message rather than on how many you send. Split the work into smaller jobs and send them separately.`;
1061
+ }
1062
+ var SIZE_CLASS_LIMITS = Object.freeze({
1063
+ small: 4e3,
1064
+ medium: 64e3,
1065
+ large: Number.POSITIVE_INFINITY
1066
+ });
1067
+ function sizeClassCeiling(sizeClass) {
1068
+ if (sizeClass === "unbounded") return Number.POSITIVE_INFINITY;
1069
+ return SIZE_CLASS_LIMITS[sizeClass];
1070
+ }
1071
+ function sizeClassOf(textChars) {
1072
+ if (textChars <= SIZE_CLASS_LIMITS.small) return "small";
1073
+ if (textChars <= SIZE_CLASS_LIMITS.medium) return "medium";
1074
+ return "large";
1075
+ }
1076
+ var JobStub = z7.object({
1077
+ id: z7.string().min(1),
1078
+ kind: JobKind,
1079
+ /** The app's id for the user who enqueued it. */
1080
+ owner: z7.string().min(1),
1081
+ /**
1082
+ * Which site this job belongs to — byollm_009 Amendment A §A.3.
1083
+ *
1084
+ * **The site's identity key id**, not an id somebody assigned it. §6 has
1085
+ * listed `site` since this spec was frozen; the schema never carried it,
1086
+ * which is the drift the amendment closes.
1087
+ *
1088
+ * A key id rather than an opaque handle for one reason above the others:
1089
+ * it makes the stub *self-describing* instead of a pointer into somebody
1090
+ * else's table. A daemon holds this key id already, from pinning, so it
1091
+ * can check `stub.site` against the payload envelope's `senderKeyId`
1092
+ * without a lookup and without trusting the party that routed it. An
1093
+ * opaque id can only be believed.
1094
+ *
1095
+ * It also avoids inventing a second namespace for a thing that has a
1096
+ * canonical one — the shape of finding 41 (two owner namespaces compared
1097
+ * for equality) and of finding fourteen before it.
1098
+ *
1099
+ * Rotation is a designed transition rather than a cost: a site publishes a
1100
+ * new identity signed by the outgoing one, both are valid through an
1101
+ * overlap window, and a daemon re-keys its own map by verifying that
1102
+ * signature against the key it already pinned (§A.3.1).
1103
+ */
1104
+ site: z7.string().min(1),
1105
+ audience: Audience,
1106
+ // `audienceAllow` is **not** here, and its absence is the enforcement —
1107
+ // cloud_008 §0.2.
1108
+ //
1109
+ // It was a list of the people who may run a job, travelling to every
1110
+ // routing party on every shared job. byollm_001 Rev 1 §B settled who
1111
+ // decides that long before this schema existed: *the daemon's own list
1112
+ // decides, not the server's*, and `allowlist.predicateFor(origin)` is the
1113
+ // enforcement in both lanes. So this was a second answer to a question the
1114
+ // daemon already owned — able only to agree, in which case it was
1115
+ // redundant, or to disagree, in which case nothing said which wins.
1116
+ //
1117
+ // The rule it leaves behind, which decides the next field too: **a class
1118
+ // the router acts on may travel; membership never does.** `audience` stays
1119
+ // for exactly that reason — the relay narrows on it. A roster does not
1120
+ // travel, so `ROSTER_NOT_DISCLOSED` holds here by absence, which is the
1121
+ // strongest way for a MUST to hold.
1122
+ //
1123
+ // The site keeps its own copy on `JobRecord` and still filters candidates
1124
+ // with it before offering. That is server-internal, where the party
1125
+ // holding the list authored it.
1126
+ /**
1127
+ * Which of the site's declared purposes this job serves — Amendment L.
1128
+ *
1129
+ * **A need, never a name.** The site's vocabulary is its own purposes;
1130
+ * the person's is their services; and the two never meet. This field says
1131
+ * "writing-assistant", and a control plane joins it to whatever that
1132
+ * person mapped it to. The site learns only whether the slot was
1133
+ * satisfiable.
1134
+ *
1135
+ * It replaced `service`, which let a site name one of the owner's
1136
+ * services directly. That field is gone from both routes (Amendment L
1137
+ * rider) and its refusal machinery with it — including the collapsed
1138
+ * `select-unavailable`, which existed so that "no such service" and "not
1139
+ * offered to you" could not be told apart. There is nothing left to
1140
+ * probe: **a vocabulary that never crosses the boundary cannot be
1141
+ * enumerated across it**, which is a stronger guarantee than the one the
1142
+ * collapse gave.
1143
+ *
1144
+ * It travels for the reason the absent `audienceAllow` establishes: *a
1145
+ * class the router acts on may travel; membership never does.* A purpose
1146
+ * is a class, and the control plane acts on it.
1147
+ *
1148
+ * Optional because direct mode has no control plane to hold a mapping and
1149
+ * is kind-only: the owner's own config and defaults answer, under the
1150
+ * ambiguity law as shipped. Absent on a relayed route resolves against
1151
+ * the site's reserved purpose, which a site that declared its own
1152
+ * purposes will not have mapped — so the slot reads as unmapped and the
1153
+ * site falls back, loudly enough and without a special case.
1154
+ *
1155
+ * A **stub** field and never a payload field, which is the line
1156
+ * `NO_PAYLOAD_ROUTING` draws: the prompt cannot reach it, so no amount of
1157
+ * user text can influence what runs.
1158
+ */
1159
+ purpose: z7.string().min(1).optional(),
1160
+ sizeClass: SizeClass,
1161
+ /** Reserved for byollm_006. False until streaming exists. */
1162
+ streaming: z7.boolean(),
1163
+ /** Epoch ms after which the work is pointless; bounds ciphertext retention. */
1164
+ deadlineAt: z7.number().int().positive()
1165
+ }).strict();
1166
+ var ClaimedStub = JobStub.extend({
1167
+ lease: Lease,
1168
+ grant: SignedGrant.optional()
1169
+ }).strict();
1170
+
1171
+ // src/envelope.ts
1172
+ import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2 } from "crypto";
1173
+ import sodium from "libsodium-wrappers";
1174
+ import { z as z8 } from "zod";
1175
+ var readied;
1176
+ async function cryptoReady() {
1177
+ readied ??= sodium.ready;
1178
+ await readied;
1179
+ }
1180
+ var ENVELOPE_MAX_AGE_MS = 24 * 60 * 6e4;
1181
+ var EnvelopeDirection = z8.enum(["payload", "result"]);
1182
+ var SealedEnvelope = z8.object({
1183
+ /** Base64url `crypto_box_seal` output over the signed plaintext. */
1184
+ ciphertext: z8.string().min(1),
1185
+ /** Who this was sealed to — the recipient checks it is them. */
1186
+ recipientKeyId: z8.string().min(1),
1187
+ /** Who signed it — the recipient checks this against its pin. */
1188
+ senderKeyId: z8.string().min(1),
1189
+ direction: EnvelopeDirection,
1190
+ /**
1191
+ * When this ciphertext stops being worth keeping.
1192
+ *
1193
+ * Carried *on* the envelope rather than recomputed by the opener. An
1194
+ * earlier version derived it from the job's creation time, which meant
1195
+ * two systems had to agree on a timestamp to the millisecond — and they
1196
+ * did not, once a real database rounded it. A bound value that has to be
1197
+ * reconstructed is a bound value that eventually is not.
1198
+ *
1199
+ * Not trusted as written: it is also inside the signature, so a changed
1200
+ * deadline fails to verify.
1201
+ */
1202
+ deadlineAt: z8.number().int().positive()
1203
+ }).strict();
1204
+ var signedBody = (context, plaintext) => envelopeSignedBody(context, plaintext);
1205
+ var rawX25519 = (key, part) => {
1206
+ const jwk = key.export({ format: "jwk" });
1207
+ const value = part === "x" ? jwk.x : jwk.d;
1208
+ if (typeof value !== "string") throw new Error("not an X25519 key");
1209
+ return new Uint8Array(Buffer.from(value, "base64url"));
1210
+ };
1211
+ async function seal(input) {
1212
+ await cryptoReady();
1213
+ const body = signedBody(input.context, input.plaintext);
1214
+ const signature = signWith(input.senderKeys, body);
1215
+ const inner = encodeEnvelopeInner(body, signature);
1216
+ const recipient = new Uint8Array(
1217
+ Buffer.from(input.recipientEncryptionPublic, "base64url")
1218
+ );
1219
+ const ciphertext = sodium.crypto_box_seal(
1220
+ new Uint8Array(Buffer.from(inner, "utf8")),
1221
+ recipient
1222
+ );
1223
+ return {
1224
+ ciphertext: Buffer.from(ciphertext).toString("base64url"),
1225
+ recipientKeyId: input.context.recipientKeyId,
1226
+ senderKeyId: input.context.senderKeyId,
1227
+ direction: input.context.direction,
1228
+ deadlineAt: input.context.deadlineAt
1229
+ };
1230
+ }
1231
+ async function open(input) {
1232
+ await cryptoReady();
1233
+ const { envelope, expected } = input;
1234
+ if (envelope.recipientKeyId !== expected.recipientKeyId || envelope.senderKeyId !== expected.senderKeyId || envelope.direction !== expected.direction) {
1235
+ return { ok: false, reason: "not-for-us" };
1236
+ }
1237
+ let inner;
1238
+ try {
1239
+ const priv = createPrivateKey2({
1240
+ key: Buffer.from(input.recipientKeys.encryptionPrivate, "base64"),
1241
+ type: "pkcs8",
1242
+ format: "der"
1243
+ });
1244
+ const pub = createPublicKey2(priv);
1245
+ const opened = sodium.crypto_box_seal_open(
1246
+ new Uint8Array(Buffer.from(envelope.ciphertext, "base64url")),
1247
+ rawX25519(pub, "x"),
1248
+ rawX25519(priv, "d")
1249
+ );
1250
+ inner = Buffer.from(opened).toString("utf8");
1251
+ } catch {
1252
+ return { ok: false, reason: "unopenable" };
1253
+ }
1254
+ const parsed = decodeEnvelopeInner(inner);
1255
+ if (parsed === void 0) return { ok: false, reason: "malformed" };
1256
+ const body = parsed.body;
1257
+ if (!verifyWith(input.senderIdentityPublic, body, parsed.signature)) {
1258
+ return { ok: false, reason: "bad-signature" };
1259
+ }
1260
+ let claims;
1261
+ try {
1262
+ claims = JSON.parse(new TextDecoder().decode(body));
1263
+ } catch {
1264
+ return { ok: false, reason: "malformed" };
1265
+ }
1266
+ if (claims["jobId"] !== expected.jobId || claims["senderKeyId"] !== expected.senderKeyId || claims["recipientKeyId"] !== expected.recipientKeyId || claims["deadlineAt"] !== envelope.deadlineAt || claims["direction"] !== expected.direction) {
1267
+ return { ok: false, reason: "context-mismatch" };
1268
+ }
1269
+ if (typeof claims["plaintext"] !== "string") {
1270
+ return { ok: false, reason: "malformed" };
1271
+ }
1272
+ return { ok: true, plaintext: claims["plaintext"] };
1273
+ }
1274
+
1275
+ // src/wire.ts
1276
+ var PROTOCOL_VERSION = "1";
1277
+ var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze([
1278
+ PROTOCOL_VERSION
1279
+ ]);
1280
+ var MIN_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0] ?? PROTOCOL_VERSION;
1281
+ function declaredVersion(input) {
1282
+ const { body, query } = input;
1283
+ if (typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion")) {
1284
+ return body.protocolVersion;
1285
+ }
1286
+ return query?.get("protocolVersion") ?? void 0;
1287
+ }
1288
+ function checkProtocolVersion(body) {
1289
+ const declared = typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion") ? body.protocolVersion : void 0;
1290
+ if (typeof declared !== "string" || declared.length === 0) {
1291
+ return {
1292
+ error: "unsupported-protocol-version",
1293
+ message: `this request declared no protocol version. Upgrade the daemon: \`${UPGRADE_COMMAND}\`.`,
1294
+ supported: SUPPORTED_PROTOCOL_VERSIONS,
1295
+ minimum: MIN_PROTOCOL_VERSION
1296
+ };
1297
+ }
1298
+ if (!SUPPORTED_PROTOCOL_VERSIONS.includes(declared)) {
1299
+ return {
1300
+ error: "unsupported-protocol-version",
1301
+ message: `this server speaks protocol ${SUPPORTED_PROTOCOL_VERSIONS.join(", ")} and the daemon asked for ${declared}. ` + (declared < MIN_PROTOCOL_VERSION ? `Upgrade the daemon: \`${UPGRADE_COMMAND}\`.` : "This daemon is newer than the server; the server needs upgrading."),
1302
+ supported: SUPPORTED_PROTOCOL_VERSIONS,
1303
+ minimum: MIN_PROTOCOL_VERSION
1304
+ };
1305
+ }
1306
+ return null;
1307
+ }
1308
+ var UPGRADE_COMMAND = "npm i -g byollm@latest";
1309
+ var PROTOCOL_PREFIX = "/byollm";
1310
+ var ENDPOINTS = Object.freeze([
1311
+ "pair",
1312
+ "claim",
1313
+ "fetch",
1314
+ "heartbeat",
1315
+ "result",
1316
+ "release"
1317
+ ]);
1318
+ var Capability = z9.object({
1319
+ kind: JobKind,
1320
+ /**
1321
+ * The owner's name for the service answering this kind — byollm_016.
1322
+ *
1323
+ * A device advertises *which* of its services serves a kind, not merely
1324
+ * that something does. **A site never sees this**, and never did after
1325
+ * Amendment L: it is what a control plane resolves a person's mapping
1326
+ * against, so that the service a grant names is one this device actually
1327
+ * offers rather than one somebody invented.
1328
+ *
1329
+ * `isDefault` used to sit beside it, saying which row an unselected job
1330
+ * took. Nothing selects any more — a job names a purpose and a person's
1331
+ * mapping names a service — so there is no unselected job for a default
1332
+ * to catch, and the field went with the machinery it served.
1333
+ */
1334
+ service: z9.string().min(1),
1335
+ backendId: BackendIdSchema,
1336
+ backendClass: BackendClass,
1337
+ model: z9.string().min(1),
1338
+ /**
1339
+ * Models this device's CLI knows about — byollm_017 ruling 3.
1340
+ *
1341
+ * **Suggestions, not a vocabulary.** Free text is always allowed: the
1342
+ * promise is that a model released this morning works this morning, and a
1343
+ * frozen list anywhere a person picks from breaks that on the first day
1344
+ * it matters. What makes free text safe is ruling 2 — a model is probed
1345
+ * before it is stored, so "found is not works" is answered by the device
1346
+ * rather than by a list.
1347
+ *
1348
+ * Announced with the capability rather than kept in the dashboard,
1349
+ * because the answer is "what does THIS device's CLI know" and only the
1350
+ * device can say. A list held cloud-side would be one more thing to
1351
+ * update on release day, and wrong for anybody who had not upgraded.
1352
+ *
1353
+ * Optional, and empty is legal. A backend with nothing to suggest — a
1354
+ * local server serving one model — is not a backend in an error state,
1355
+ * and a reader must not render an absent list as "no models available".
1356
+ */
1357
+ knownModels: z9.array(z9.string().min(1)).optional(),
1358
+ offerScope: OfferScope
1359
+ }).strict();
1360
+ var CapabilityMatrix = z9.array(Capability);
1361
+ var WithheldKind = z9.object({
1362
+ kind: JobKind,
1363
+ claimants: z9.array(
1364
+ z9.object({ service: z9.string().min(1), offer: OfferScope }).strict()
1365
+ ).min(2)
1366
+ }).strict();
1367
+ var GrantRef = z9.object({ jobId: z9.string().min(1), leaseId: z9.string().min(1) }).strict();
1368
+ var PairStartRequest = z9.object({
1369
+ protocolVersion: z9.literal(PROTOCOL_VERSION),
1370
+ action: z9.literal("start"),
1371
+ daemon: z9.object({
1372
+ version: z9.string().min(1),
1373
+ /** Shown in the app's runner list so a user can tell their machines apart. */
1374
+ label: z9.string().min(1).max(120),
1375
+ platform: z9.enum(["darwin", "linux", "win32"])
1376
+ }).strict(),
1377
+ /**
1378
+ * This machine's public keys (byollm_009 §5).
1379
+ *
1380
+ * Pairing is where the two parties learn each other's identities, because
1381
+ * it is the one moment a human is already deciding to trust: the approval
1382
+ * click. A key exchanged anywhere else would be a key nobody chose.
1383
+ */
1384
+ device: PublicIdentity,
1385
+ capabilities: CapabilityMatrix
1386
+ }).strict();
1387
+ var PairStartResponse = z9.object({
1388
+ /** Secret the daemon polls with. Never shown to the user. */
1389
+ deviceCode: z9.string().min(20),
1390
+ /** Short code the user reads and confirms in the browser. */
1391
+ userCode: z9.string().min(4).max(16),
1392
+ /** Where the user approves. Must be on the server's own origin. */
1393
+ verificationUrl: z9.url(),
1394
+ /** Epoch ms after which the code is dead ({@link MUSTS.PAIR_CODE_EXPIRES}). */
1395
+ expiresAt: z9.number().int().positive(),
1396
+ /** How often the daemon may poll. */
1397
+ pollIntervalMs: z9.number().int().min(500).max(6e4)
1398
+ }).strict();
1399
+ var PairPollRequest = z9.object({
1400
+ protocolVersion: z9.literal(PROTOCOL_VERSION),
1401
+ action: z9.literal("poll"),
1402
+ deviceCode: z9.string().min(20)
1403
+ }).strict();
1404
+ var PairPollResponse = z9.discriminatedUnion("status", [
1405
+ z9.object({ status: z9.literal("pending") }).strict(),
1406
+ z9.object({ status: z9.literal("denied") }).strict(),
1407
+ z9.object({ status: z9.literal("expired") }).strict(),
1408
+ z9.object({
1409
+ status: z9.literal("approved"),
1410
+ // `runnerToken` is gone — cloud_008 §2.4, finding 37.
1411
+ //
1412
+ // It was minted here, hashed into `RunnerRecord.tokenHash`, written to
1413
+ // the daemon's pairings file, and then **never sent, never looked up
1414
+ // and never compared**. `getRunnerByTokenHash` existed on both stores
1415
+ // and was called by nothing but a test asserting it returns null.
1416
+ //
1417
+ // Not merely dead wire, which is what `audienceAllow` and
1418
+ // `HeartbeatResponse.leases` were. This was a *secret*: minted,
1419
+ // transmitted, and written to two disks at rest, for nothing. A
1420
+ // credential with no purpose is a liability rather than clutter,
1421
+ // because the only thing it can ever do is leak.
1422
+ //
1423
+ // `REQUESTS_SIGNED_NOT_BEARER` was already the rule and was already
1424
+ // enforced — every authenticated call is signed by the device's pinned
1425
+ // identity key. This removes the thing the MUST is named after.
1426
+ runnerId: z9.string().min(1),
1427
+ /** The app's id for the approving user — this daemon's owner forever. */
1428
+ owner: z9.string().min(1),
1429
+ /** Display name for the trust UI, if the app offers one. */
1430
+ ownerLabel: z9.string().optional(),
1431
+ /**
1432
+ * The sites this pairing covers, for the daemon to pin (byollm_009 §5),
1433
+ * keyed by each site's identity key id — cloud_009 §5.
1434
+ *
1435
+ * Returned only on approval: a pending or denied poll learns nothing,
1436
+ * so an unapproved code cannot be used to enumerate a site's keys.
1437
+ *
1438
+ * **One pairing per upstream, not one per site.** A user who connects a
1439
+ * site on a web dashboard has no reason to go back to a laptop and run
1440
+ * a command, so which sites a pairing covers is a projection of consent
1441
+ * — refreshed on the heartbeat — rather than something frozen at
1442
+ * pairing. A direct site answers with exactly one entry, which is the
1443
+ * same shape and not a special case.
1444
+ *
1445
+ * Keyed by the id `stub.site` carries (Amendment A §A.3), so the
1446
+ * runner's lookup is a map read rather than a join across two
1447
+ * namespaces.
1448
+ */
1449
+ sites: z9.record(z9.string().min(1), PublicIdentity),
1450
+ /**
1451
+ * The control plane's grant-signing key, pinned here — Amendment J.
1452
+ *
1453
+ * **Pairing is when, and that is the whole question.** Pairing is
1454
+ * already the ceremony where an owner proves out of band that this
1455
+ * device is theirs, so a key learned here rides trust that has already
1456
+ * happened. The rejected alternative is trust-on-first-grant, and it is
1457
+ * rejected because it hands the decision back to the relay: a daemon
1458
+ * that learns whose signature to trust from the first grant to arrive
1459
+ * has its admission authority chosen by whoever controls delivery.
1460
+ *
1461
+ * Optional on the wire, and only on the wire: a direct-mode server has
1462
+ * no control plane and signs nothing, and a daemon that receives no key
1463
+ * serves its owner alone. It is not optional for a relay with a control
1464
+ * plane — one that omitted it would be asking devices to accept grants
1465
+ * from nobody in particular, and would find every job refused.
1466
+ *
1467
+ * Rotation is Amendment C's, with no path where a grant teaches a
1468
+ * daemon a new key.
1469
+ */
1470
+ controlPlanePublic: z9.string().min(1).optional()
1471
+ }).strict()
1472
+ ]);
1473
+ var PairRequest = z9.discriminatedUnion("action", [
1474
+ PairStartRequest,
1475
+ PairPollRequest
1476
+ ]);
1477
+ var ClaimRequest = z9.object({
1478
+ protocolVersion: z9.literal(PROTOCOL_VERSION),
1479
+ runnerId: z9.string().min(1),
1480
+ /** Re-sent on every claim so a server never matches against a stale matrix. */
1481
+ capabilities: CapabilityMatrix,
1482
+ /** Upper bound on jobs to return; the server may return fewer. */
1483
+ max: z9.number().int().min(1).max(64)
1484
+ }).strict();
1485
+ var ClaimResponse = z9.object({
1486
+ /**
1487
+ * Stubs, not jobs. The payload arrives from `fetch`, sealed to whichever
1488
+ * device claimed — see {@link JobStub} for the exhaustive metadata list.
1489
+ */
1490
+ jobs: z9.array(ClaimedStub),
1491
+ /** Lease duration granted, so the daemon knows its renewal deadline. */
1492
+ leaseMs: z9.number().int().positive()
1493
+ }).strict();
1494
+ var HeartbeatRequest = z9.object({
1495
+ protocolVersion: z9.literal(PROTOCOL_VERSION),
1496
+ runnerId: z9.string().min(1),
1497
+ daemonVersion: z9.string().min(1),
1498
+ capabilities: CapabilityMatrix,
1499
+ /**
1500
+ * Kinds this device is withholding, and why it can be said.
1501
+ *
1502
+ * Optional so a daemon that has nothing withheld sends nothing, and so an
1503
+ * older daemon against a newer hub is simply a device with no withheld
1504
+ * kinds rather than a parse failure.
1505
+ */
1506
+ withheld: z9.array(WithheldKind).default([]),
1507
+ /**
1508
+ * Leases this daemon believes it holds; the server renews exactly these.
1509
+ *
1510
+ * Lease ids rather than job ids, so a replayed heartbeat cannot renew a
1511
+ * grant the runner no longer holds — see {@link Lease.id}.
1512
+ */
1513
+ activeLeases: z9.array(GrantRef),
1514
+ /** True while the owner has the daemon paused; the server stops offering work. */
1515
+ paused: z9.boolean()
1516
+ }).strict();
1517
+ var HeartbeatResponse = z9.object({
1518
+ /**
1519
+ * The sites this daemon may serve, right now — cloud_008 finding 59.
1520
+ *
1521
+ * Revocation used to be a boolean, and it was device-wide: the daemon
1522
+ * plane refused every call when the (owner, hub-site) consent was gone,
1523
+ * heartbeat answered `revoked: true` with `lost: all`, and the daemon
1524
+ * dropped its whole pairing by origin. Under a hub that is one site's
1525
+ * revocation ending a machine's relationship with every other site it
1526
+ * served — the amplification finding 48 warned about, arriving through
1527
+ * the one field nobody thought of as tenancy.
1528
+ *
1529
+ * So the answer is the set. A site that leaves it is revoked *for that
1530
+ * site*: the daemon drops that pin and keeps the rest. An empty set is
1531
+ * what "revoked" used to mean, and the daemon can see that for itself
1532
+ * rather than being told a second time — two fields for one fact is how
1533
+ * they drift.
1534
+ */
1535
+ sites: z9.record(z9.string().min(1), PublicIdentity),
1536
+ /**
1537
+ * How a site's current key traces back to one this daemon already holds —
1538
+ * byollm_009 Amendment C.
1539
+ *
1540
+ * Keyed by the same id as `sites`, and **additive on purpose**: `sites`
1541
+ * remains the one statement of which key is current, and this says only
1542
+ * how that key got there. Two fields for one fact is how they drift; this
1543
+ * is two facts, and the second is evidence about the first.
1544
+ *
1545
+ * Optional because a site that has never rotated has no chain, which is
1546
+ * every site today. A daemon that receives one for an id it already holds
1547
+ * ignores it: the pin it has is the pin it approved.
1548
+ *
1549
+ * §12 carries what this adds to the metadata surface — a site's rotation
1550
+ * history is public by construction, because a daemon that cannot read it
1551
+ * cannot verify it.
1552
+ */
1553
+ successions: z9.record(
1554
+ z9.string().min(1),
1555
+ z9.object({
1556
+ /** Oldest last, as the projection carries it. */
1557
+ succeeds: z9.array(Succession).max(MAX_SUCCESSION_CHAIN),
1558
+ /**
1559
+ * Until when the superseded key may still sign work — epoch ms.
1560
+ *
1561
+ * The daemon holds its own clock against this, for the reason it
1562
+ * holds its own allowlist: a projection that could extend the
1563
+ * window indefinitely would be a two-key site forever, decided by
1564
+ * the party this design does not trust.
1565
+ */
1566
+ retiringUntil: z9.number().int().positive().optional()
1567
+ }).strict()
1568
+ ).optional(),
1569
+ /**
1570
+ * Per-job cancel (byollm_001 Rev 1 §C). The daemon aborts these jobs'
1571
+ * in-flight backend calls and reports them `canceled`.
1572
+ *
1573
+ * **The grant, not the id** — V1-3. Job ids are chosen per site, so two
1574
+ * sites may pick the same one, and a bare id told a daemon holding both
1575
+ * to abort whichever it happened to have filed under that name. The lease
1576
+ * is the unique grant and the daemon already keys its work by it; this is
1577
+ * the same shape `activeLeases` sends in the other direction.
1578
+ */
1579
+ cancel: z9.array(
1580
+ z9.object({ jobId: z9.string().min(1), leaseId: z9.string().min(1) }).strict()
1581
+ ),
1582
+ // `leases` is deliberately absent — cloud_008 §1.4b, finding 16.
1583
+ //
1584
+ // It carried "these leases were renewed, and here is the new expiry", and
1585
+ // **no daemon ever read it.** A mutation returning an empty list while
1586
+ // renewing correctly survived every test, which is what made it visible.
1587
+ //
1588
+ // It is neither a class nor membership, so Amendment A's rule does not
1589
+ // decide it — the older test does: nothing reads it, so it is dead wire.
1590
+ // §6's exhaustiveness is a commitment about what an upstream can see, and
1591
+ // it applies to every message rather than only to the stub.
1592
+ //
1593
+ // `lost` is the actionable signal and always was: a daemon stops work on
1594
+ // a lease it no longer holds. "Renewed" was the same question answered a
1595
+ // second time, and a second answer can only agree or contradict.
1596
+ //
1597
+ // Renewal itself is untouched — the upstream still extends the grants a
1598
+ // heartbeat names, which is what §0.6 fixed. What ended is telling the
1599
+ // daemon about it in a field it ignored. If an upstream ever needs to
1600
+ // push lease decisions, that is a new field with a reader, added on
1601
+ // purpose.
1602
+ /**
1603
+ * Jobs the daemon thinks it holds but the server has reassigned or
1604
+ * expired. The daemon must stop work on these and not report results.
1605
+ *
1606
+ * Named by grant rather than by id, for V1-3's reason: a bare id is
1607
+ * ambiguous across sites, and "the lease you no longer hold" is exactly
1608
+ * what this field means anyway.
1609
+ */
1610
+ lost: z9.array(
1611
+ z9.object({ jobId: z9.string().min(1), leaseId: z9.string().min(1) }).strict()
1612
+ ),
1613
+ /** Server clock, so a daemon with a skewed clock still honors leases. */
1614
+ serverTime: z9.number().int().positive(),
1615
+ /**
1616
+ * Sites whose disclosure the user must read again before work moves —
1617
+ * cloud_008 finding 48, named rather than counted.
1618
+ *
1619
+ * A **subset of `sites`**, deliberately: a paused site keeps its pin, so
1620
+ * re-consenting never costs a re-pair. The daemon can say which site is
1621
+ * waiting and the user can go and read it, which is the difference
1622
+ * between a machine that is quietly idle and one that says why.
1623
+ *
1624
+ * Not `revoked`, which is a human ending a relationship, and not
1625
+ * `paused`, which on the request side already means "this daemon's
1626
+ * operator stopped it" — one word with two subjects on two halves of one
1627
+ * exchange is a confusion nobody untangles from a log.
1628
+ */
1629
+ awaitingConsent: z9.array(z9.string().min(1)),
1630
+ /**
1631
+ * A version this daemon should move itself to — B053.
1632
+ *
1633
+ * The channel the auto-updater reads, and it is the channel the daemon
1634
+ * already polls rather than a new phone-home, which is what the ruling
1635
+ * asked for (016 §Auto-update).
1636
+ *
1637
+ * **The hub may not send this to every daemon.** This schema is
1638
+ * `.strict()`, so a daemon built before the field exists does not ignore
1639
+ * it — it rejects the whole heartbeat and stops working. Which would mean
1640
+ * the message carrying the update is the message that breaks the machines
1641
+ * it was meant to update.
1642
+ *
1643
+ * That is decidable without any new handshake, because the request
1644
+ * already carries `daemonVersion`. {@link mayOfferUpdate} is the rule,
1645
+ * kept here as code rather than as a paragraph in a runbook, so both
1646
+ * sides read the same one.
1647
+ *
1648
+ * Exact versions only, never a tag: the daemon refuses anything else, and
1649
+ * a fleet resolving one tag at different minutes is a fleet on different
1650
+ * builds reporting one number.
1651
+ */
1652
+ updateTo: z9.string().min(1).optional()
1653
+ }).strict();
1654
+ var ResultDisposition = z9.enum(["ok", "error", "canceled"]);
1655
+ var ResultRequest = z9.object({
1656
+ protocolVersion: z9.literal(PROTOCOL_VERSION),
1657
+ runnerId: z9.string().min(1),
1658
+ jobId: z9.string().min(1),
1659
+ /**
1660
+ * The grant this result was produced under — cloud_008 §1.4a.
1661
+ *
1662
+ * `fetch` has always named its lease, with the reasoning written beside
1663
+ * it: a request that names only the job would be answerable for whatever
1664
+ * lease exists when it arrives. **The operation that writes the result did
1665
+ * not**, on either plane, and checked only the runner id — which survives
1666
+ * a claim-release-reclaim cycle, so a device whose grant had been swept
1667
+ * and reissued could still land a result for a job it no longer held.
1668
+ *
1669
+ * Found by tracing a mutation that survived in §0.6: the lease lapsed, the
1670
+ * sweep requeued, the daemon re-claimed under a new grant, and the
1671
+ * original run finished and posted anyway. The relay marked the job done
1672
+ * with a result the site cannot open — it verifies the envelope against
1673
+ * the *current* holder's device, so the crypto contains the substitution —
1674
+ * and then refused the real holder's result as a replay. A lost job, in
1675
+ * silence.
1676
+ *
1677
+ * `LEASE_HONORED` is a statement about a lease *instance*. That was
1678
+ * learned once already, when a replayed release yanked a later grant, and
1679
+ * it applies here for the same reason.
1680
+ */
1681
+ leaseId: z9.string().min(1),
1682
+ /**
1683
+ * The outcome, sealed to the site and signed by the device.
1684
+ *
1685
+ * The return leg of the payload envelope, and sealed for the same reason:
1686
+ * a model's answer is as sensitive as the prompt that produced it, and an
1687
+ * intermediary that cannot read one must not be handed the other.
1688
+ */
1689
+ envelope: SealedEnvelope,
1690
+ /**
1691
+ * The sealed outcome's discriminator, in the clear.
1692
+ *
1693
+ * Checked against the envelope once opened. It is a routing hint, not a
1694
+ * fact: believing it unverified would let a daemon mark a job `ok` while
1695
+ * sealing an error, and only the app would ever find out.
1696
+ */
1697
+ disposition: ResultDisposition
1698
+ // `model`, `backendClass` and `durationMs` are **inside the envelope** —
1699
+ // cloud_008 §2.5. See {@link RunMetadata}.
1700
+ //
1701
+ // They were here, in the clear, and that was two problems wearing one
1702
+ // coat. On the direct plane the site recorded unauthenticated fields
1703
+ // beside an authenticated answer: a daemon could seal one result and
1704
+ // declare a different model, and only the unsigned half would reach the
1705
+ // app. Through a relay they reached a third party that acts on none of
1706
+ // them — `model` in particular being the sort of detail Amendment A's
1707
+ // rule keeps off the wire.
1708
+ //
1709
+ // `disposition` stays, and the difference is the test: a relay *routes*
1710
+ // on it, so it is a class a routing party consumes. Nobody between the
1711
+ // two ends consumes these.
1712
+ }).strict();
1713
+ var ResultResponse = z9.object({
1714
+ /**
1715
+ * False when this submission wrote nothing — the daemon should discard,
1716
+ * not retry ({@link MUSTS.RESULT_IDEMPOTENT}).
1717
+ */
1718
+ accepted: z9.boolean(),
1719
+ /**
1720
+ * True when this device had already recorded this job's result.
1721
+ *
1722
+ * The difference between "already recorded" and "you no longer hold this"
1723
+ * — cloud_008 §3.6. A daemon whose acknowledgment was lost is in the first
1724
+ * case and needs to hear it: its answer is safely on disk. Reporting a
1725
+ * stale lease instead invents a worry about a result that is already
1726
+ * stored, and sends its owner looking for a routing problem.
1727
+ *
1728
+ * Set only for the device that finished the job. A different device gets
1729
+ * the same refusal it would get for a job that is *not* terminal, so a job
1730
+ * id cannot be used as a terminality probe.
1731
+ */
1732
+ duplicate: z9.boolean().optional(),
1733
+ /** The job's state after this submission. */
1734
+ state: z9.string().min(1)
1735
+ }).strict();
1736
+ var ReleaseRequest = z9.object({
1737
+ protocolVersion: z9.literal(PROTOCOL_VERSION),
1738
+ runnerId: z9.string().min(1),
1739
+ /**
1740
+ * Which leases to release — the grant, not just the job.
1741
+ *
1742
+ * A release naming only a job id releases whatever lease exists at the
1743
+ * moment it arrives, which for a replayed request is not the lease the
1744
+ * daemon meant. See {@link Lease.id}.
1745
+ */
1746
+ leases: z9.array(GrantRef),
1747
+ /**
1748
+ * Why, so the app's runner list can say something true.
1749
+ *
1750
+ * `refused` is load-bearing, not cosmetic: the server cannot evaluate
1751
+ * what a device will admit (§4.2), so it may legitimately offer
1752
+ * a job this daemon then declines. The server MUST record the refusal and
1753
+ * stop offering that job to that runner, or the pair would spin between
1754
+ * claim and release forever.
1755
+ */
1756
+ reason: z9.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
1757
+ }).strict();
1758
+ var ReleaseResponse = z9.object({
1759
+ released: z9.array(z9.string().min(1))
1760
+ }).strict();
1761
+ var WireErrorCode = z9.enum([
1762
+ "bad-request",
1763
+ "unsupported-protocol-version",
1764
+ /**
1765
+ * The daemon is older than this hub will serve — B052.
1766
+ *
1767
+ * Distinct from `unsupported-protocol-version`, which is about the
1768
+ * contract; this is about the build. A daemon can speak protocol 1
1769
+ * perfectly and still be old enough that we would rather move it than keep
1770
+ * carrying it — and the two need different remedies in the message, since
1771
+ * one is "your daemon and this server disagree" and the other is "yours
1772
+ * works, and it is time".
1773
+ *
1774
+ * The floor is the backstop to the auto-updater (B053), and the only lever
1775
+ * that reaches a machine which never opted into offers.
1776
+ */
1777
+ "daemon-below-floor",
1778
+ // "We do not know who you are." Exactly 401, and only that — cloud_008
1779
+ // §1.4d.
1780
+ "unauthorized",
1781
+ /**
1782
+ * "We know exactly who you are, and the answer is no." Exactly 403.
1783
+ *
1784
+ * Five refusals across both planes served 403 with `unauthorized`, whose
1785
+ * table entry is 401: a revoked device, a site claiming another site's
1786
+ * stub, a job you do not hold, a device belonging to another owner, a
1787
+ * relay that does not route for you. Every one of them is an *identified*
1788
+ * caller being refused.
1789
+ *
1790
+ * Collapsing the two loses a distinction that matters everywhere it is
1791
+ * read: a revoked daemon would look like an unsigned one in every log and
1792
+ * every client branch, and "check your keys" is the wrong advice for both
1793
+ * of them in opposite directions.
1794
+ */
1795
+ "forbidden",
1796
+ "revoked",
1797
+ "not-found",
1798
+ // Claimed, but the site has not sealed the payload yet — cloud_008 §1.4.
1799
+ //
1800
+ // A daemon must retry rather than abandon: the job is legitimately still
1801
+ // its own until the lease or the awaiting-payload clock says otherwise.
1802
+ // That is why it cannot be `not-found` or `server-error`, and why it was
1803
+ // the protocol gap that produced a bare 409 in the first place.
1804
+ "not-ready",
1805
+ /**
1806
+ * The job is over, and this call is about a job — V1-6, and the code the
1807
+ * site plane has been serving without one (V1-13).
1808
+ *
1809
+ * Distinct from `not-found`, which says "no such job", and from
1810
+ * `not-ready`, which says "not yet, keep asking". This one says "yes, and
1811
+ * it finished" — so a daemon must stop rather than retry, and a replayed
1812
+ * request must not be able to reopen it.
1813
+ */
1814
+ "too-late",
1815
+ // The caller's clock is too far from ours to judge a signature's freshness.
1816
+ //
1817
+ // Split out from `unauthorized` because the remedy is completely different
1818
+ // and only the server can tell them apart: a bad signature means the key is
1819
+ // wrong, this means the machine's time is wrong. A daemon reporting it as a
1820
+ // generic rejection sends its owner looking at their network.
1821
+ "clock-skew",
1822
+ "rate-limited",
1823
+ "server-error"
1824
+ ]);
1825
+ var WireError = z9.object({
1826
+ error: WireErrorCode,
1827
+ message: z9.string().min(1),
1828
+ /**
1829
+ * What this server speaks, on `unsupported-protocol-version` — §B.4.
1830
+ *
1831
+ * The refusal has carried these since the version handshake existed and
1832
+ * the enumeration did not model them, so the one error that exists to be
1833
+ * *acted on* was the one that failed to parse as a wire error. Found by
1834
+ * the relay's own suite the day the relay started sending it: a refusal
1835
+ * outside the enumerated shape is a refusal a client cannot branch on,
1836
+ * which is the whole reason §1.4 enumerates them.
1837
+ *
1838
+ * Modelled the way `clock-skew`'s two fields already are — code-specific
1839
+ * extras, refused on any other code by the refinement below.
1840
+ */
1841
+ supported: z9.array(z9.string().min(1)).optional(),
1842
+ minimum: z9.string().min(1).optional(),
1843
+ /**
1844
+ * The oldest daemon this hub serves, on `daemon-below-floor` — B052.
1845
+ *
1846
+ * Carried for the same reason `supported` and `minimum` are: a refusal
1847
+ * that cannot be branched on is a refusal a client can only print. The
1848
+ * message already names the floor for a person; this names it for the
1849
+ * code, so a surface can say "you are two versions under" without
1850
+ * parsing English.
1851
+ */
1852
+ floor: z9.string().min(1).optional(),
1853
+ /** Seconds; mirrors Retry-After for `rate-limited` and `server-error`. */
1854
+ retryAfter: z9.number().int().nonnegative().optional(),
1855
+ /**
1856
+ * The server's clock, and the window it allows. `clock-skew` only.
1857
+ *
1858
+ * So the far side can say *how far off* rather than *that something is
1859
+ * wrong* — the difference between "adjust your clock by four minutes" and
1860
+ * "something is wrong with your connection". Not a disclosure: the
1861
+ * heartbeat response returns the same value, and so does every `Date`
1862
+ * header.
1863
+ */
1864
+ serverTime: z9.number().int().positive().optional(),
1865
+ maxSkewMs: z9.number().int().positive().optional()
1866
+ }).strict().superRefine((error, ctx) => {
1867
+ const skew = error.error === "clock-skew";
1868
+ const carried = error.serverTime !== void 0 || error.maxSkewMs !== void 0;
1869
+ if (skew && !carried) {
1870
+ ctx.addIssue({
1871
+ code: "custom",
1872
+ message: "clock-skew must carry serverTime and maxSkewMs"
1873
+ });
1874
+ }
1875
+ if (!skew && carried) {
1876
+ ctx.addIssue({
1877
+ code: "custom",
1878
+ message: `${error.error} must not carry serverTime or maxSkewMs`
1879
+ });
1880
+ }
1881
+ const floored = error.error === "daemon-below-floor";
1882
+ if (floored && error.floor === void 0) {
1883
+ ctx.addIssue({
1884
+ code: "custom",
1885
+ message: "daemon-below-floor must carry floor"
1886
+ });
1887
+ }
1888
+ if (!floored && error.floor !== void 0) {
1889
+ ctx.addIssue({
1890
+ code: "custom",
1891
+ message: `${error.error} must not carry floor`
1892
+ });
1893
+ }
1894
+ const version = error.error === "unsupported-protocol-version";
1895
+ const versionFields = error.supported !== void 0 || error.minimum !== void 0;
1896
+ if (version && !versionFields) {
1897
+ ctx.addIssue({
1898
+ code: "custom",
1899
+ message: "unsupported-protocol-version must carry supported and minimum"
1900
+ });
1901
+ }
1902
+ if (!version && versionFields) {
1903
+ ctx.addIssue({
1904
+ code: "custom",
1905
+ message: `${error.error} must not carry supported or minimum`
1906
+ });
1907
+ }
1908
+ });
1909
+ var ERROR_STATUS = Object.freeze({
1910
+ "bad-request": 400,
1911
+ "unsupported-protocol-version": 400,
1912
+ /**
1913
+ * 426 Upgrade Required — B052, and it is the one status that says this.
1914
+ *
1915
+ * Not 403, which this daemon reads as a permission problem and which
1916
+ * sits beside `revoked` in every log. Not 400, which reads as a
1917
+ * malformed request; the request was perfect and the sender is old.
1918
+ *
1919
+ * It also fails safely on a daemon that predates the code: 426 is not in
1920
+ * that switch, so it lands on the 4xx default — `rejected`, which is
1921
+ * "never retried: the request is wrong, and repeating it stays wrong".
1922
+ * Vaguer than the remedy, and the right behaviour, which is what a
1923
+ * fallback has to be.
1924
+ */
1925
+ "daemon-below-floor": 426,
1926
+ unauthorized: 401,
1927
+ forbidden: 403,
1928
+ revoked: 403,
1929
+ "not-found": 404,
1930
+ // 409, not 404: the job exists and is yours, it is simply not ready.
1931
+ "not-ready": 409,
1932
+ // The same 409 as `not-ready` and the opposite instruction: that one says
1933
+ // keep asking, this one says stop. The status is the class of the
1934
+ // problem — a request that does not fit the resource's state — and the
1935
+ // code is what a caller acts on.
1936
+ "too-late": 409,
1937
+ // 401 alongside `unauthorized`, because that is what it is — the
1938
+ // signature could not be judged. The code is what carries the remedy.
1939
+ "clock-skew": 401,
1940
+ "rate-limited": 429,
1941
+ "server-error": 500
1942
+ });
1943
+ var FetchRequest = z9.object({
1944
+ // `literal`, like every other request — V1-17. This one said
1945
+ // `string().min(1)`, so a daemon speaking a version this server does not
1946
+ // know got past the handshake on the one endpoint that hands over a
1947
+ // sealed payload. The version check exists so that a mismatch is a named
1948
+ // refusal rather than a schema failure three fields later; here it was
1949
+ // neither.
1950
+ protocolVersion: z9.literal(PROTOCOL_VERSION),
1951
+ runnerId: z9.string().min(1),
1952
+ jobId: z9.string().min(1),
1953
+ /**
1954
+ * The grant this daemon holds.
1955
+ *
1956
+ * Named, not inferred: a fetch is lease-scoped, and a request that names
1957
+ * only the job would be answerable for whatever lease exists when it
1958
+ * arrives ({@link Lease.id}).
1959
+ */
1960
+ leaseId: z9.string().min(1)
1961
+ }).strict();
1962
+ var FetchResponse = z9.object({
1963
+ /**
1964
+ * The work, sealed to the device that claimed it — byollm_009 §6.
1965
+ *
1966
+ * Not plaintext. The site opens its own at-rest envelope and re-seals to
1967
+ * the claiming device's key, signed by the site's identity, so the work
1968
+ * is readable only by the machine that took it and only if it came from
1969
+ * the site that machine pinned.
1970
+ */
1971
+ envelope: SealedEnvelope
1972
+ }).strict();
1973
+
1974
+ // src/update-offer.ts
1975
+ var UPDATE_OFFER_SINCE = "0.1.0-alpha.83";
1976
+ function parse(version) {
1977
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(
1978
+ version
1979
+ );
1980
+ if (match === null) return void 0;
1981
+ return {
1982
+ release: [Number(match[1]), Number(match[2]), Number(match[3])],
1983
+ pre: match[4] === void 0 ? [] : match[4].split(".").map((part) => /^\d+$/.test(part) ? Number(part) : part)
1984
+ };
1985
+ }
1986
+ function compareVersions(a, b) {
1987
+ const left = parse(a);
1988
+ const right = parse(b);
1989
+ if (left === void 0 || right === void 0) return void 0;
1990
+ for (let i = 0; i < 3; i += 1) {
1991
+ const diff = (left.release[i] ?? 0) - (right.release[i] ?? 0);
1992
+ if (diff !== 0) return diff < 0 ? -1 : 1;
127
1993
  }
128
- const messages = kinded.payload.messages.reduce(
129
- (sum, m) => sum + m.content.length,
130
- 0
131
- );
132
- return messages + (kinded.payload.system?.length ?? 0);
1994
+ if (left.pre.length === 0 && right.pre.length > 0) return 1;
1995
+ if (left.pre.length > 0 && right.pre.length === 0) return -1;
1996
+ for (let i = 0; i < Math.max(left.pre.length, right.pre.length); i += 1) {
1997
+ const l = left.pre[i];
1998
+ const r = right.pre[i];
1999
+ if (l === void 0) return -1;
2000
+ if (r === void 0) return 1;
2001
+ if (l === r) continue;
2002
+ if (typeof l === "number" && typeof r === "number") return l < r ? -1 : 1;
2003
+ if (typeof l === "number") return -1;
2004
+ if (typeof r === "number") return 1;
2005
+ return l < r ? -1 : 1;
2006
+ }
2007
+ return 0;
2008
+ }
2009
+ function mayOfferUpdate(daemonVersion) {
2010
+ const order = compareVersions(daemonVersion, UPDATE_OFFER_SINCE);
2011
+ return order !== void 0 && order >= 0;
2012
+ }
2013
+ function checkDaemonFloor(input) {
2014
+ const order = compareVersions(input.daemonVersion, input.floor);
2015
+ if (order === void 0 || order >= 0) return null;
2016
+ return {
2017
+ error: "daemon-below-floor",
2018
+ message: `byollm ${input.daemonVersion} is below the supported floor (${input.floor}). Run \`${input.upgradeCommand}\`, then \`byollm start\`.`,
2019
+ floor: input.floor
2020
+ };
2021
+ }
2022
+ function updateOfferFor(input) {
2023
+ const { offer, daemonVersion } = input;
2024
+ if (offer === void 0) return {};
2025
+ if (exactOffer(offer) === void 0) return {};
2026
+ if (!mayOfferUpdate(daemonVersion)) return {};
2027
+ if (daemonVersion === offer) return {};
2028
+ return { updateTo: offer };
2029
+ }
2030
+ function exactOffer(value) {
2031
+ return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(value) ? value : void 0;
2032
+ }
2033
+ function withoutComments(source) {
2034
+ return source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/^[ \t]*\/\/.*$/gm, " ");
2035
+ }
2036
+ function mentionsWireField(source, field) {
2037
+ return new RegExp(`\\b${field}\\b`).test(withoutComments(source));
133
2038
  }
134
2039
 
135
- // src/job.ts
136
- import { z as z4 } from "zod";
137
- var JobState = z4.enum([
138
- "queued",
139
- "claimed",
140
- "running",
141
- "ok",
142
- "error",
143
- "canceled",
144
- "expired"
145
- ]);
146
- var TERMINAL_STATES = Object.freeze([
147
- "ok",
148
- "error",
149
- "canceled",
150
- "expired"
151
- ]);
152
- function isTerminal(state) {
153
- return TERMINAL_STATES.includes(state);
2040
+ // src/about.ts
2041
+ var ABOUT = `# About BYOLLM
2042
+
2043
+ **What BYOLLM is**
2044
+
2045
+ BYOLLM lets you use your own AI on websites. You install one small program on
2046
+ your computer. Then, websites that support BYOLLM can use the AI you already
2047
+ have \u2014 a free model running on your machine, or an AI service you already pay
2048
+ for \u2014 instead of the website paying for AI and passing the cost to you.
2049
+
2050
+ **Why it matters**
2051
+
2052
+ For you:
2053
+
2054
+ - Your favorite model, everywhere you go.
2055
+ - New models the moment you get them \u2013 not when a site gets around to adding
2056
+ them.
2057
+ - Encrypted end-to-end. Your prompts go to your own device; byollm.cloud can't
2058
+ read them.
2059
+ - Sites never learn which model you use, and your subscriptions are never
2060
+ shared.
2061
+ - Pay less. Sites that don't pay for AI can charge you less \u2013 or nothing.
2062
+
2063
+ For sites and developers:
2064
+
2065
+ - Zero AI bills. Your users bring their own compute.
2066
+ - No floating money \u2013 you don't pay LLM bills up front and hope to collect
2067
+ later, and you never ask people to prepay just to try you.
2068
+ - Free trials that cost you nothing to offer.
2069
+ - Ship the AI features you kept private for fear of the API bill.
2070
+ - One small integration. Your users choose the models.
2071
+
2072
+ **Your device**
2073
+
2074
+ The \`byollm\` program runs on your computer. It knows which AI services you have
2075
+ set up: free open-source models on your machine, metered services you pay per
2076
+ use, or your own subscriptions like Claude Pro/Max. When a website you have
2077
+ enabled sends work, your device runs it with the service you chose. Your
2078
+ prompts are encrypted end-to-end to your own device. byollm.cloud passes them
2079
+ along and cannot read them.
2080
+
2081
+ **Sites**
2082
+
2083
+ A website that wants to use BYOLLM says what it needs \u2014 "writing help," "chat,"
2084
+ and so on. When you connect the site, you pick which of your services answers
2085
+ each one. The site never learns which model you use. You can turn a site off at
2086
+ any time, and it stops getting your work.
2087
+
2088
+ **Teams (optional)**
2089
+
2090
+ A team lets you share what runs on your devices with people you name \u2014 the free
2091
+ open-source models on your machine, or a metered service with a spending limit
2092
+ you set. Your subscription accounts (like Claude Pro/Max) are never shared with
2093
+ anyone. That is a rule, not a setting.
2094
+
2095
+ **byollm.cloud (or your own relay)**
2096
+
2097
+ Many sites, many devices, many people. byollm.cloud keeps track of who has
2098
+ allowed what and sends each job to the right device. It never sees your
2099
+ prompts. If you would rather run this part yourself, the relay is open source \u2014
2100
+ you can run your own instead of using byollm.cloud.`;
2101
+ var ABOUT_SHORT_LEDE = "BYOLLM \u2013 Bring Your Own LLM \u2013 lets you use your own AI on websites you authorize. A small program installed on your machine lets you use your own models and subscriptions on any BYOLLM-integrated site, including new models the moment you get access \u2013 no site updates required. BYOLLM Cloud connects sites to your devices with end-to-end encryption, so no one, including us, can see your data.";
2102
+ var ABOUT_SHORT_TAIL = "Sites can charge you less because you bring your own \u2013 see why that matters \u2192. Teams can optionally share the free or metered services on their devices with people they name. Personal subscriptions are never shared.";
2103
+ var ABOUT_SHORT = `${ABOUT_SHORT_LEDE}
2104
+
2105
+ ${ABOUT_SHORT_TAIL}`;
2106
+
2107
+ // src/signing.ts
2108
+ import { createHash as createHash2 } from "crypto";
2109
+ import { z as z10 } from "zod";
2110
+ var MAX_CLOCK_SKEW_MS = 12e4;
2111
+ var RequestSignature = z10.object({
2112
+ /** Which runner is calling. The server looks up its pinned identity. */
2113
+ runnerId: z10.string().min(1),
2114
+ /** Epoch ms, bounded by {@link MAX_CLOCK_SKEW_MS}. */
2115
+ issuedAt: z10.number().int().positive(),
2116
+ /** Base64url Ed25519 signature over {@link canonicalRequest}. */
2117
+ signature: z10.string().min(1)
2118
+ }).strict();
2119
+ function canonicalRequest(input) {
2120
+ const digest = createHash2("sha256").update(input.body, "utf8").digest("hex");
2121
+ return Buffer.from(
2122
+ [
2123
+ "byollm/v1/request",
2124
+ input.endpoint,
2125
+ input.runnerId,
2126
+ String(input.issuedAt),
2127
+ digest
2128
+ ].join("\n"),
2129
+ "utf8"
2130
+ );
154
2131
  }
155
- var TRANSITIONS = Object.freeze({
156
- queued: ["claimed", "expired", "canceled"],
157
- // A claimed job returns to `queued` when its lease expires un-renewed
158
- // ({@link MUSTS.LEASE_RECLAIMABLE}).
159
- claimed: ["running", "queued", "canceled", "error"],
160
- running: ["ok", "error", "canceled", "queued"],
161
- ok: [],
162
- error: [],
163
- canceled: [],
164
- expired: []
165
- });
166
- function canTransition(from, to) {
167
- return TRANSITIONS[from].includes(to);
2132
+ function signRequest(keys, input) {
2133
+ return {
2134
+ runnerId: input.runnerId,
2135
+ issuedAt: input.issuedAt,
2136
+ signature: signWith(keys, canonicalRequest(input))
2137
+ };
168
2138
  }
169
- var Lease = z4.object({
170
- /** The runner holding the lease. */
171
- runnerId: z4.string().min(1),
172
- /** Epoch milliseconds after which the claim is void. */
173
- expiresAt: z4.number().int().positive()
2139
+ function signSiteRequest(keys, input) {
2140
+ return signRequest(keys, {
2141
+ endpoint: siteEndpoint(input.endpoint),
2142
+ runnerId: input.siteId,
2143
+ issuedAt: input.issuedAt,
2144
+ body: input.body
2145
+ });
2146
+ }
2147
+ function verifySiteRequest(input) {
2148
+ return verifyRequest({
2149
+ ...input,
2150
+ endpoint: siteEndpoint(input.endpoint)
2151
+ });
2152
+ }
2153
+ var siteEndpoint = (endpoint) => `site/${endpoint}`;
2154
+ function verifyRequest(input) {
2155
+ const skew = input.maxSkewMs ?? MAX_CLOCK_SKEW_MS;
2156
+ if (Math.abs(input.now - input.signature.issuedAt) > skew) return "stale";
2157
+ const ok = verifyWith(
2158
+ input.identityPublic,
2159
+ canonicalRequest({
2160
+ endpoint: input.endpoint,
2161
+ runnerId: input.signature.runnerId,
2162
+ issuedAt: input.signature.issuedAt,
2163
+ body: input.body
2164
+ }),
2165
+ input.signature.signature
2166
+ );
2167
+ return ok ? null : "bad-signature";
2168
+ }
2169
+
2170
+ // src/manifest.ts
2171
+ import { z as z11 } from "zod";
2172
+ var RESERVED_PURPOSE = "default";
2173
+ var RENDERABLE = /^[^\p{Cc}\p{Cf}\p{Cs}\p{Co}]+$/u;
2174
+ var renderable = (max, what) => z11.string().min(1).max(max).regex(
2175
+ RENDERABLE,
2176
+ `a ${what} is text a person reads \u2014 no control characters, direction overrides or zero-width padding`
2177
+ ).refine((value) => value.trim() !== "", {
2178
+ message: `a ${what} cannot be blank`
174
2179
  });
175
- var JobPayload = z4.union([GeneratePayload, ChatPayload]);
176
- var ClaimedJob = z4.object({
177
- id: z4.string().min(1),
178
- kind: JobKind,
179
- payload: JobPayload,
180
- audience: Audience,
181
- /** The app's id for the user who enqueued it. */
182
- owner: z4.string().min(1),
183
- /** Runner owners the app restricted a `named` job to, if any. */
184
- audienceAllow: z4.array(z4.string().min(1)).optional(),
185
- lease: Lease
186
- }).strict();
187
- var ResultProvenance = z4.object({
188
- /** The audience the job ran under. */
189
- audience: Audience,
190
- /** The runner that produced it. */
191
- runnerId: z4.string().min(1),
192
- /** The runner owner's id in this app's namespace. */
193
- runnerOwner: z4.string().min(1),
194
- /** Which backend class produced it — an HTTP call or a sandboxed spawn. */
195
- backendClass: BackendClass,
196
- /** The model the runner reports having used. */
197
- model: z4.string().min(1),
2180
+ var PurposeKey = z11.string().regex(
2181
+ /^[a-z0-9][a-z0-9-]*$/,
2182
+ "a purpose key is a lowercase slug \u2014 letters, digits and hyphens"
2183
+ ).max(64);
2184
+ var Purpose = z11.object({
198
2185
  /**
199
- * False only for `self` jobs. When true the app MUST treat `text` as
200
- * untrusted third-party content.
2186
+ * What a person reads on the consent screen. The only rendered field.
2187
+ *
2188
+ * Declared rather than derived from the key, because a key is a
2189
+ * compromise between machines and this is not. "Writing Assistant" is
2190
+ * what somebody understands; `writing-assistant` is what travels.
2191
+ */
2192
+ label: renderable(80, "label"),
2193
+ /** One line of context for the consent screen. Optional. */
2194
+ description: renderable(280, "description").optional(),
2195
+ /**
2196
+ * The kinds this purpose uses.
2197
+ *
2198
+ * A purpose may span kinds, and a mapping is per (purpose, kind) — so a
2199
+ * person can send this purpose's chat to one service and its generation
2200
+ * to another. Listing a kind here is what makes that slot appear.
201
2201
  */
202
- untrusted: z4.boolean()
2202
+ kinds: z11.array(JobKind).min(1).max(JOB_KINDS.length).refine((kinds) => new Set(kinds).size === kinds.length, {
2203
+ message: "a purpose lists each kind once"
2204
+ })
203
2205
  }).strict();
204
- function provenanceFor(input) {
2206
+ var MAX_PURPOSES = 32;
2207
+ var Manifest = z11.record(PurposeKey, Purpose).refine((manifest) => Object.keys(manifest).length > 0, {
2208
+ message: "a manifest declares at least one purpose"
2209
+ }).refine((manifest) => Object.keys(manifest).length <= MAX_PURPOSES, {
2210
+ message: `a manifest declares at most ${String(MAX_PURPOSES)} purposes \u2014 a consent screen is a set of questions somebody answers one at a time`
2211
+ }).refine((manifest) => !(RESERVED_PURPOSE in manifest), {
2212
+ message: `"${RESERVED_PURPOSE}" is reserved for a site that declares no purposes of its own \u2014 give this one a name from your own vocabulary`
2213
+ });
2214
+ function singlePurposeManifest(input) {
205
2215
  return {
206
- audience: input.audience,
207
- runnerId: input.runnerId,
208
- runnerOwner: input.runnerOwner,
209
- backendClass: input.backendClass,
210
- model: input.model,
211
- untrusted: input.audience !== "self"
2216
+ [RESERVED_PURPOSE]: { label: input.label, kinds: [...input.kinds] }
212
2217
  };
213
2218
  }
214
- var JobResultOk = z4.object({
215
- outcome: z4.literal("ok"),
216
- text: z4.string(),
217
- /** Optional reference to a stored artifact; never a local path. */
218
- artifactUrl: z4.url().optional()
219
- }).strict();
220
- var JobResultError = z4.object({
221
- outcome: z4.literal("error"),
222
- code: z4.string().min(1),
223
- message: z4.string().min(1),
224
- /** Whether the app may reasonably re-enqueue. */
225
- retryable: z4.boolean()
226
- }).strict();
227
- var JobResultCanceled = z4.object({
228
- outcome: z4.literal("canceled")
229
- }).strict();
230
- var JobOutcome = z4.discriminatedUnion("outcome", [
231
- JobResultOk,
232
- JobResultError,
233
- JobResultCanceled
234
- ]);
235
- var DeliveredResult = z4.object({
236
- jobId: z4.string().min(1),
237
- state: JobState,
238
- outcome: JobOutcome.optional(),
239
- provenance: ResultProvenance.optional()
240
- }).strict();
241
2219
 
242
2220
  // src/musts.ts
2221
+ function kindsOf(must2) {
2222
+ return typeof must2.verifiedBy === "string" ? [must2.verifiedBy] : must2.verifiedBy;
2223
+ }
243
2224
  var must = (m) => Object.freeze(m);
244
2225
  var MUSTS = Object.freeze({
245
2226
  // ---- Pairing and identity -------------------------------------------
@@ -247,31 +2228,122 @@ var MUSTS = Object.freeze({
247
2228
  id: "PAIR_ONE_USER",
248
2229
  statement: "A runner token MUST be bound to exactly one user; a daemon MUST refuse work not attributable to its paired user.",
249
2230
  enforcedBy: "both",
2231
+ verifiedBy: "conformance",
250
2232
  source: "byollm_001 \xA7MUSTs"
251
2233
  }),
252
2234
  PAIR_INTERACTIVE: must({
253
2235
  id: "PAIR_INTERACTIVE",
254
2236
  statement: "Pairing MUST be interactive (device-code approval in the app's own session); a long-lived pasted secret MUST NOT be accepted as pairing.",
255
2237
  enforcedBy: "server",
2238
+ verifiedBy: "conformance",
256
2239
  source: "byollm_001 \xA7Endpoints.1"
257
2240
  }),
258
2241
  PAIR_CODE_EXPIRES: must({
259
2242
  id: "PAIR_CODE_EXPIRES",
260
2243
  statement: "An unapproved device code MUST expire and MUST NOT be redeemable after expiry.",
261
2244
  enforcedBy: "server",
2245
+ verifiedBy: "conformance",
262
2246
  source: "byollm_001 \xA7Endpoints.1"
263
2247
  }),
264
2248
  // ---- Typed job kinds --------------------------------------------------
2249
+ VERSION_HANDSHAKE_REQUIRED: must({
2250
+ id: "VERSION_HANDSHAKE_REQUIRED",
2251
+ statement: "Every protocol request MUST declare a protocol version, and a server MUST refuse an absent or unsupported one with a structured error naming what it supports \u2014 never a generic parse failure.",
2252
+ enforcedBy: "both",
2253
+ verifiedBy: "conformance",
2254
+ source: "byollm_009 \xA74"
2255
+ }),
2256
+ SITE_KEY_BY_STUB: must({
2257
+ id: "SITE_KEY_BY_STUB",
2258
+ statement: "A daemon MUST verify a job's payload against the pinned key of the site the stub names, and MUST refuse a job naming a site it has not pinned. It MUST NOT fall back to another pinned key, and MUST refuse an envelope whose declared sender disagrees with the stub's site.",
2259
+ enforcedBy: "daemon",
2260
+ // Adversarial, and the reason is the finding that produced it: the
2261
+ // honest paths pass with every site check deleted, because `open`
2262
+ // refuses a signature from the wrong key anyway. What distinguishes an
2263
+ // enforced rule from a coincidence here is a hostile pairing of stub and
2264
+ // envelope, which no conformance client would ever send.
2265
+ verifiedBy: "adversarial",
2266
+ source: "byollm_009 \xA7A.3"
2267
+ }),
2268
+ SITES_LOCALLY_APPROVED: must({
2269
+ id: "SITES_LOCALLY_APPROVED",
2270
+ statement: "A daemon MUST NOT run work for a site on an upstream's word alone. An upstream may propose a site set; work for any site in it MUST additionally carry a grant signed by the control-plane key this daemon pinned at pairing. A key that has changed for an id this daemon already pinned MUST be refused for the life of the pairing, including after that id has left the set and returned. A **verified succession** is not a changed key: a new key id carrying a signature, by a key this daemon has already pinned, over a statement naming both key ids MUST be accepted \u2014 provided the control plane projects the same successor \u2014 and MUST be announced rather than applied silently. The first job from a site this daemon has never served MUST be announced at the machine.",
2271
+ enforcedBy: "daemon",
2272
+ // Two kinds, and the second is the one that matters — V1-1.
2273
+ //
2274
+ // `construction`: the daemon cannot serve a site that is not in its
2275
+ // pinned map, and admission refuses before a payload is fetched — and
2276
+ // since byollm_016 Amendment K, being in the map is no longer sufficient
2277
+ // either: a signed grant is, and the relay proposing the set cannot
2278
+ // produce one.
2279
+ //
2280
+ // `adversarial`: the property that survives is about a *sequence* —
2281
+ // remove the id, re-offer it under a different key — which no honest
2282
+ // upstream sends and which the fence above does not see. That was the
2283
+ // bypass: the pin was deleted with the id, so the comparison had nothing
2284
+ // to compare against and the substitution arrived as a stranger.
2285
+ // **Not `conformance`, and that is a live gap rather than a judgement.**
2286
+ // Amendment C's succession clause is a rule about two implementations
2287
+ // agreeing, which is what a conformance check is for — but rotating a
2288
+ // site's key is not something `ConformanceTarget` can express, and adding
2289
+ // an optional hook that most targets omit would produce a check reporting
2290
+ // success for a reason unrelated to the property it claims. That is this
2291
+ // project's most-repeated bug, and it is not worth reintroducing for a
2292
+ // stronger-sounding word in a table. The rotation path is verified by
2293
+ // `site-rotation.test.ts` (both directions, against the shipped runner)
2294
+ // and `relay/test/rotation.test.ts` (both planes, against the reference
2295
+ // relay); the missing piece is a second *independent* implementation to
2296
+ // check them against, and there is not one yet.
2297
+ verifiedBy: ["construction", "adversarial"],
2298
+ source: "byollm_009 \xA7B.2, Amendment C"
2299
+ }),
2300
+ KEYS_EXCHANGED_AT_CONSENT: must({
2301
+ id: "KEYS_EXCHANGED_AT_CONSENT",
2302
+ statement: "Pairing MUST exchange both parties' public identities; each side MUST verify that the encryption key is signed by the identity presenting it, and MUST pin the identity. Keys MUST NOT be delivered before approval.",
2303
+ enforcedBy: "both",
2304
+ verifiedBy: "conformance",
2305
+ source: "byollm_009 \xA75"
2306
+ }),
2307
+ REQUESTS_SIGNED_NOT_BEARER: must({
2308
+ id: "REQUESTS_SIGNED_NOT_BEARER",
2309
+ statement: "Every authenticated request MUST be signed by the calling device's pinned identity key, over the endpoint, the runner id, a timestamp and the exact request body. A server MUST NOT accept a bearer credential in place of a signature.",
2310
+ enforcedBy: "both",
2311
+ verifiedBy: "conformance",
2312
+ source: "byollm_009 \xA74.2"
2313
+ }),
2314
+ LEASE_SCOPED_BY_GRANT: must({
2315
+ id: "LEASE_SCOPED_BY_GRANT",
2316
+ statement: "A lease-scoped request MUST name the lease it acts on, and a server MUST apply it only to that lease. Naming the job and the runner is not sufficient: both survive a claim-release-reclaim cycle.",
2317
+ enforcedBy: "both",
2318
+ verifiedBy: "conformance",
2319
+ source: "byollm_009 \xA74.2"
2320
+ }),
2321
+ STUB_METADATA_EXHAUSTIVE: must({
2322
+ id: "STUB_METADATA_EXHAUSTIVE",
2323
+ statement: "A claim MUST answer with stubs carrying exactly the enumerated fields and no payload. An endpoint MUST NOT emit a stub carrying others, and an upstream MUST NOT require any.",
2324
+ enforcedBy: "both",
2325
+ verifiedBy: "conformance",
2326
+ source: "byollm_009 \xA76"
2327
+ }),
2328
+ ENVELOPE_SEALED_AND_SIGNED: must({
2329
+ id: "ENVELOPE_SEALED_AND_SIGNED",
2330
+ statement: "A stored payload MUST be sealed, and MUST be signed by the sender's identity key. An endpoint MUST refuse an envelope whose signature does not verify against the identity it pinned.",
2331
+ enforcedBy: "server",
2332
+ verifiedBy: "conformance",
2333
+ source: "byollm_009 \xA76"
2334
+ }),
265
2335
  KIND_TYPED_ONLY: must({
266
2336
  id: "KIND_TYPED_ONLY",
267
2337
  statement: "Job kinds MUST resolve against handlers baked into the daemon. A daemon MUST refuse an unknown kind rather than guess.",
268
2338
  enforcedBy: "daemon",
2339
+ verifiedBy: "conformance",
269
2340
  source: "byollm_001 \xA7Jobs are typed data"
270
2341
  }),
271
2342
  KIND_NO_CODE: must({
272
2343
  id: "KIND_NO_CODE",
273
2344
  statement: "A server MUST NOT be able to convey code, a shell string, or a path to execute; payloads are data handed to a model only.",
274
2345
  enforcedBy: "daemon",
2346
+ verifiedBy: "conformance",
275
2347
  source: "byollm_001 \xA7Jobs are typed data; byollm_004 \xA71"
276
2348
  }),
277
2349
  // ---- Capability and claiming -----------------------------------------
@@ -279,18 +2351,21 @@ var MUSTS = Object.freeze({
279
2351
  id: "CLAIM_REQUIRES_CAPABILITY",
280
2352
  statement: "A daemon MUST NOT be given a job whose kind is absent from its advertised capability matrix.",
281
2353
  enforcedBy: "both",
2354
+ verifiedBy: "conformance",
282
2355
  source: "byollm_001 \xA7MUSTs"
283
2356
  }),
284
2357
  CAPABILITY_IS_DETECTED: must({
285
2358
  id: "CAPABILITY_IS_DETECTED",
286
2359
  statement: "An advertised capability matrix MUST be the intersection of owner config and detected, healthy reality \u2014 never config alone.",
287
2360
  enforcedBy: "daemon",
2361
+ verifiedBy: "conformance",
288
2362
  source: "byollm_002 \xA7Routing"
289
2363
  }),
290
2364
  CLAIM_ATOMIC: must({
291
2365
  id: "CLAIM_ATOMIC",
292
2366
  statement: "Claiming MUST be atomic: a job MUST NOT be handed to two runners concurrently.",
293
2367
  enforcedBy: "server",
2368
+ verifiedBy: "conformance",
294
2369
  source: "byollm_001 \xA7Endpoints.2"
295
2370
  }),
296
2371
  // ---- Leases -----------------------------------------------------------
@@ -298,12 +2373,14 @@ var MUSTS = Object.freeze({
298
2373
  id: "LEASE_HONORED",
299
2374
  statement: "A daemon MUST stop work on a job whose lease it has failed to renew, and MUST NOT report a result for an expired lease it no longer holds.",
300
2375
  enforcedBy: "daemon",
2376
+ verifiedBy: "conformance",
301
2377
  source: "byollm_001 \xA7MUSTs"
302
2378
  }),
303
2379
  LEASE_RECLAIMABLE: must({
304
2380
  id: "LEASE_RECLAIMABLE",
305
2381
  statement: "A lease that expires un-renewed MUST make its job claimable again with no loss of the job.",
306
2382
  enforcedBy: "server",
2383
+ verifiedBy: "conformance",
307
2384
  source: "byollm_001 \xA7Endpoints.2"
308
2385
  }),
309
2386
  // ---- Audience and offer scope ----------------------------------------
@@ -311,24 +2388,56 @@ var MUSTS = Object.freeze({
311
2388
  id: "AUDIENCE_BOTH_SIDES",
312
2389
  statement: "A job MUST run on a daemon only if the daemon's offer scope admits the job's owner AND the job's audience admits the daemon's owner.",
313
2390
  enforcedBy: "both",
2391
+ verifiedBy: "conformance",
314
2392
  source: "byollm_001 \xA7The audience model"
315
2393
  }),
316
2394
  SUBSCRIPTION_SELF_LOCK: must({
317
2395
  id: "SUBSCRIPTION_SELF_LOCK",
318
- statement: "A subscription-class backend's offer scope MUST be 'self' and MUST NOT be widened by configuration.",
2396
+ statement: "A subscription-class backend's offer scope MUST be 'private' and MUST NOT be widened by configuration.",
319
2397
  enforcedBy: "daemon",
2398
+ verifiedBy: "conformance",
320
2399
  source: "byollm_001 \xA7The audience model"
321
2400
  }),
2401
+ METERED_DEFAULTS_SELF: must({
2402
+ id: "METERED_DEFAULTS_SELF",
2403
+ statement: "A metered backend's effective offer scope MUST be 'private' unless the owner has explicitly acknowledged spending money on others' work.",
2404
+ enforcedBy: "daemon",
2405
+ verifiedBy: "conformance",
2406
+ source: "byollm_007 \xA74"
2407
+ }),
2408
+ METERED_REQUIRES_CEILING: must({
2409
+ id: "METERED_REQUIRES_CEILING",
2410
+ statement: "A widened metered backend MUST carry a spend ceiling, and the daemon MUST refuse community work once it is reached.",
2411
+ enforcedBy: "daemon",
2412
+ verifiedBy: "conformance",
2413
+ source: "byollm_007 \xA74"
2414
+ }),
2415
+ COST_NOT_CONFIGURABLE: must({
2416
+ id: "COST_NOT_CONFIGURABLE",
2417
+ statement: "A built-in provider's cost class MUST NOT be overridable by configuration.",
2418
+ enforcedBy: "daemon",
2419
+ verifiedBy: "conformance",
2420
+ source: "byollm_007 \xA72"
2421
+ }),
2422
+ REMOTE_IS_NEVER_FREE: must({
2423
+ id: "REMOTE_IS_NEVER_FREE",
2424
+ statement: "A generic HTTP backend whose base URL is not loopback or private MUST be treated as metered.",
2425
+ enforcedBy: "daemon",
2426
+ verifiedBy: "conformance",
2427
+ source: "byollm_007 \xA72"
2428
+ }),
322
2429
  NAMED_LOCAL_ALLOWLIST: must({
323
2430
  id: "NAMED_LOCAL_ALLOWLIST",
324
- statement: "A 'named' job MUST be admitted only by the daemon's own local (server origin, user id) allowlist \u2014 never on the server's assertion alone.",
2431
+ statement: "A 'team' job MUST be admitted only by something the device itself verified, keyed by (server origin, user id) \u2014 never on the routing party's assertion alone.",
325
2432
  enforcedBy: "daemon",
2433
+ verifiedBy: "conformance",
326
2434
  source: "byollm_001 Rev 1 \xA7B"
327
2435
  }),
328
2436
  REFUSAL_NOT_REOFFERED: must({
329
2437
  id: "REFUSAL_NOT_REOFFERED",
330
2438
  statement: "A server MUST NOT re-offer a job to a runner that released it with reason 'refused'.",
331
2439
  enforcedBy: "server",
2440
+ verifiedBy: "conformance",
332
2441
  source: "byollm_001 Rev 1 \xA7B (loop resolved in build review)"
333
2442
  }),
334
2443
  // ---- Revocation and cancel -------------------------------------------
@@ -336,12 +2445,14 @@ var MUSTS = Object.freeze({
336
2445
  id: "REVOCATION_HONORED",
337
2446
  statement: "A revoked daemon MUST stop claiming and MUST abandon in-flight work by the next heartbeat at the latest.",
338
2447
  enforcedBy: "daemon",
2448
+ verifiedBy: "conformance",
339
2449
  source: "byollm_001 \xA7MUSTs"
340
2450
  }),
341
2451
  CANCEL_HONORED: must({
342
2452
  id: "CANCEL_HONORED",
343
2453
  statement: "A job id in a heartbeat response's cancel list MUST abort that job's in-flight backend call and be reported as 'canceled'.",
344
2454
  enforcedBy: "daemon",
2455
+ verifiedBy: "conformance",
345
2456
  source: "byollm_001 Rev 1 \xA7C"
346
2457
  }),
347
2458
  // ---- Lifecycle, dependencies, delivery -------------------------------
@@ -349,37 +2460,43 @@ var MUSTS = Object.freeze({
349
2460
  id: "DEPENDS_ON_GATING",
350
2461
  statement: "A job MUST NOT be claimable until every job in its dependsOn set has reached the 'ok' state.",
351
2462
  enforcedBy: "server",
2463
+ verifiedBy: "conformance",
352
2464
  source: "byollm_001 Rev 1 \xA7E"
353
2465
  }),
354
2466
  TTL_EXPIRY: must({
355
2467
  id: "TTL_EXPIRY",
356
2468
  statement: "An unclaimed job MUST become 'expired' once its TTL elapses, and the TTL clock MUST start when the job becomes claimable, not at enqueue.",
357
2469
  enforcedBy: "server",
2470
+ verifiedBy: "conformance",
358
2471
  source: "byollm_001 Rev 1 \xA7D (TTL clock resolved in build review)"
359
2472
  }),
360
2473
  NO_RUNNER_SIGNAL: must({
361
2474
  id: "NO_RUNNER_SIGNAL",
362
2475
  statement: "A server MUST surface noRunnerAvailable when no runner with matching capability has heartbeated within the liveness window, and MUST NOT raise it for a job still blocked on dependencies.",
363
2476
  enforcedBy: "server",
2477
+ verifiedBy: "conformance",
364
2478
  source: "byollm_001 Rev 1 \xA7D"
365
2479
  }),
366
2480
  RESULT_IDEMPOTENT: must({
367
2481
  id: "RESULT_IDEMPOTENT",
368
2482
  statement: "Result submission MUST be idempotent by job id; the first terminal outcome wins and later submissions MUST NOT change it.",
369
2483
  enforcedBy: "server",
2484
+ verifiedBy: "conformance",
370
2485
  source: "byollm_001 \xA7Endpoints.4"
371
2486
  }),
372
- RESULT_PROVENANCE: must({
373
- id: "RESULT_PROVENANCE",
374
- statement: "A result from a non-'self' job MUST carry its provenance (audience and runner) to the delivery seam so an app never treats volunteer output as first-party.",
2487
+ PROVENANCE_NAMES_DEVICE: must({
2488
+ id: "PROVENANCE_NAMES_DEVICE",
2489
+ statement: "A result MUST carry the claiming device's key id and its relationship to the requester, to the delivery seam, so an app never treats volunteer output as first-party. The key id MUST be the device the upstream granted the lease to, and a result whose signature does not verify against that device MUST be refused rather than recorded.",
375
2490
  enforcedBy: "server",
376
- source: "byollm_003 Rev 1 \xA7Return-trip"
2491
+ verifiedBy: "conformance",
2492
+ source: "byollm_009 \xA711"
377
2493
  }),
378
2494
  // ---- The trust surface -------------------------------------------------
379
2495
  INGRESS_LOGGED_BEFORE_EXECUTION: must({
380
2496
  id: "INGRESS_LOGGED_BEFORE_EXECUTION",
381
2497
  statement: "Every executed prompt MUST be appended to the local ingress log before execution begins.",
382
2498
  enforcedBy: "daemon",
2499
+ verifiedBy: "conformance",
383
2500
  source: "byollm_001 \xA7MUSTs"
384
2501
  }),
385
2502
  // ---- Execution isolation (byollm_004) ---------------------------------
@@ -387,224 +2504,171 @@ var MUSTS = Object.freeze({
387
2504
  id: "NO_SHELL_INTERPOLATION",
388
2505
  statement: "Process-class backends MUST be invoked with a fixed argv array and the payload delivered on stdin; payload text MUST NOT reach a command line.",
389
2506
  enforcedBy: "daemon",
2507
+ verifiedBy: "adversarial",
390
2508
  source: "byollm_004 \xA72"
391
2509
  }),
2510
+ /**
2511
+ * Amended for byollm_016 Phase B, and the amendment is deliberately narrow.
2512
+ *
2513
+ * A site may now name a **service** on the stub. The temptation is to read
2514
+ * that as a crack in this law, so the statement below says exactly where the
2515
+ * line is: a name selects from a menu the owner published, and resolves to a
2516
+ * model, backend, base URL and flags **only** through that owner's own
2517
+ * config. The site supplies a key; the owner supplies every value it maps
2518
+ * to. A name the owner does not advertise is refused rather than
2519
+ * substituted, because substitution is how "you may pick from my list" turns
2520
+ * into "you may ask for anything and get something".
2521
+ *
2522
+ * Two properties keep it from drifting into "sites demand models":
2523
+ *
2524
+ * 1. **Nothing the site sends is ever a value.** No model string, no URL,
2525
+ * no flag crosses the wire — only a key that means nothing off this
2526
+ * owner's machine.
2527
+ * 2. **It is a stub field, never a payload field.** The prompt cannot
2528
+ * reach it. That is unchanged and is the sentence the second clause
2529
+ * below still enforces verbatim.
2530
+ */
392
2531
  NO_PAYLOAD_ROUTING: must({
393
2532
  id: "NO_PAYLOAD_ROUTING",
394
- statement: "Model, backend, base URL, and flags MUST come from owner config only; a payload MUST NOT influence any of them.",
2533
+ statement: "Model, backend, base URL, and flags MUST come from owner config only; a payload MUST NOT influence any of them. A stub MAY name a service the owner advertises, which selects among that owner's own config entries and MUST NOT introduce any value the owner did not write; an unadvertised name MUST be refused, never substituted.",
395
2534
  enforcedBy: "daemon",
396
- source: "byollm_004 \xA72"
2535
+ verifiedBy: "adversarial",
2536
+ source: "byollm_004 \xA72, amended byollm_016 \xA7Phase B"
397
2537
  }),
398
2538
  STRIPPED_CHILD_ENV: must({
399
2539
  id: "STRIPPED_CHILD_ENV",
400
2540
  statement: "Process-class children MUST spawn with an allowlisted environment, a scratch cwd, no inherited descriptors beyond std streams, and hard timeout and output-size caps.",
401
2541
  enforcedBy: "daemon",
2542
+ verifiedBy: "adversarial",
402
2543
  source: "byollm_004 \xA72"
403
2544
  }),
404
2545
  HTTP_BASE_URL_SAFE: must({
405
2546
  id: "HTTP_BASE_URL_SAFE",
406
2547
  statement: "HTTP-class backends MUST send requests only to the owner-configured base URL and MUST refuse base URLs resolving to cloud-metadata or link-local addresses.",
407
2548
  enforcedBy: "daemon",
2549
+ verifiedBy: "adversarial",
408
2550
  source: "byollm_004 Rev 1 \xA7Backend taxonomy"
409
2551
  }),
410
2552
  OUTPUT_INERT: must({
411
2553
  id: "OUTPUT_INERT",
412
2554
  statement: "Returned text MUST be treated as inert bytes: never evaluated, never written to a payload-named path, never interpolated into a shell or into terminal control sequences when logged.",
413
2555
  enforcedBy: "daemon",
2556
+ verifiedBy: "adversarial",
414
2557
  source: "byollm_004 \xA72"
415
2558
  }),
416
2559
  COMMUNITY_BUDGETS: must({
417
2560
  id: "COMMUNITY_BUDGETS",
418
2561
  statement: "Jobs whose owner is not the daemon's owner MUST be subject to the owner's rate limits, daily cap, and resource budget.",
419
2562
  enforcedBy: "daemon",
2563
+ verifiedBy: "adversarial",
420
2564
  source: "byollm_004 \xA74"
2565
+ }),
2566
+ REVOCATION_IMMEDIATE: must({
2567
+ id: "REVOCATION_IMMEDIATE",
2568
+ statement: "Revocation MUST take effect at the upstream at once \u2014 a revoked runner MUST NOT be granted further work from the moment the record changes \u2014 and MUST reach the daemon by its next heartbeat.",
2569
+ // Both, and stated as one sentence with two obligations rather than
2570
+ // folded into REVOCATION_HONORED. That one binds the *daemon*: a revoked
2571
+ // daemon stops claiming and abandons in-flight work. This binds the
2572
+ // *upstream*. byollm_009 §5 is explicit that the pair is the point — "a
2573
+ // revocation enforced at one end survives a compromise of that end" — and
2574
+ // one entry covering both would make a compromised daemon look compliant.
2575
+ enforcedBy: "both",
2576
+ verifiedBy: "conformance",
2577
+ source: "byollm_009 \xA711"
2578
+ }),
2579
+ CONSENT_BEFORE_ROUTE: must({
2580
+ id: "CONSENT_BEFORE_ROUTE",
2581
+ statement: "An upstream MUST NOT route a job to a device without a record binding that user, that site and that scope. There MUST be no discovery path by which a device receives work it was never granted.",
2582
+ enforcedBy: "server",
2583
+ verifiedBy: "conformance",
2584
+ source: "byollm_009 \xA711"
2585
+ }),
2586
+ ROSTER_NOT_DISCLOSED: must({
2587
+ id: "ROSTER_NOT_DISCLOSED",
2588
+ statement: "A site MUST NOT learn the membership of a group whose compute it uses, and MUST NOT publish membership to a routing party. No wire message may carry a list of who may run a job.",
2589
+ // Checkable since cloud_008 §0.2 took `audienceAllow` off the stub: the
2590
+ // property now holds by *absence*, and absence is exactly what a strict
2591
+ // schema and a serialised stub can be asked about. Before that it was a
2592
+ // sentence — and one this project cited in code comments, tests and two
2593
+ // specs as though it were enforced data, which is why it is worth
2594
+ // stating precisely rather than generously.
2595
+ enforcedBy: "both",
2596
+ verifiedBy: "conformance",
2597
+ source: "byollm_009 \xA711"
2598
+ }),
2599
+ EFFECTIVE_OFFER_ONLY: must({
2600
+ id: "EFFECTIVE_OFFER_ONLY",
2601
+ statement: "A daemon MUST declare effective offers only. An upstream MUST NOT receive raw config, allowlists, or capacity the owner has not shared, and MUST act on the declared offer rather than on what was asked for.",
2602
+ enforcedBy: "both",
2603
+ verifiedBy: "conformance",
2604
+ source: "byollm_009 \xA711"
2605
+ }),
2606
+ FALLBACK_LABELED: must({
2607
+ id: "FALLBACK_LABELED",
2608
+ statement: "Work served by anything other than the user's own compute MUST be labelled as such wherever it is reported, and MUST NOT be silently substituted.",
2609
+ // `construction` today, and deliberately not `conformance`. Nothing on
2610
+ // the wire yet distinguishes a fallback from any other community job —
2611
+ // the ledger that would give it a surface is unbuilt — so a check would
2612
+ // have to assert something it cannot observe. Promoted the day that
2613
+ // surface exists. Marking it `conformance` now would put "verified"
2614
+ // beside a property no third party can see, which is the one thing the
2615
+ // kinds exist to prevent.
2616
+ enforcedBy: "both",
2617
+ verifiedBy: "construction",
2618
+ source: "byollm_009 \xA711"
2619
+ }),
2620
+ RELAY_BLIND: must({
2621
+ id: "RELAY_BLIND",
2622
+ statement: "A relay MUST NOT hold any key capable of decrypting a payload, a result, or a delta frame.",
2623
+ // Operator: a third party can read the relay's types and see there is
2624
+ // nowhere to put such a key, but the kit certifies a *server* and cannot
2625
+ // reach inside somebody's deployment to prove what it holds.
2626
+ enforcedBy: "server",
2627
+ verifiedBy: "operator",
2628
+ source: "byollm_009 \xA711"
2629
+ }),
2630
+ SHARED_COMPUTE_DISCLOSED: must({
2631
+ id: "SHARED_COMPUTE_DISCLOSED",
2632
+ statement: "Before a user's work first runs on compute they do not own, they MUST be told in plain language that the machine's owner can see it.",
2633
+ // Operator, and cloud_008 §0.3 is why the classification now comes with a
2634
+ // standing answer rather than a standing question. The screen is not
2635
+ // wire-observable, but the *string the server composes* is, and it is
2636
+ // now unit-tested with the two false sentences forbidden by name. The
2637
+ // kind stays `operator` because a third-party site can still render
2638
+ // whatever it likes; what changed is that the part inside our own
2639
+ // boundary stopped depending on somebody remembering to audit it.
2640
+ enforcedBy: "server",
2641
+ verifiedBy: "operator",
2642
+ source: "byollm_009 \xA711"
421
2643
  })
422
2644
  });
423
- var MUST_IDS = Object.freeze(Object.keys(MUSTS));
424
-
425
- // src/wire.ts
426
- import { z as z5 } from "zod";
427
- var PROTOCOL_VERSION = "0";
428
- var PROTOCOL_PREFIX = "/byollm";
429
- var ENDPOINTS = Object.freeze([
430
- "pair",
431
- "claim",
432
- "heartbeat",
433
- "result",
434
- "release"
435
- ]);
436
- var Capability = z5.object({
437
- kind: JobKind,
438
- backendId: BackendIdSchema,
439
- backendClass: BackendClass,
440
- model: z5.string().min(1),
441
- offerScope: OfferScope
442
- }).strict();
443
- var CapabilityMatrix = z5.array(Capability);
444
- var PairStartRequest = z5.object({
445
- protocolVersion: z5.literal(PROTOCOL_VERSION),
446
- action: z5.literal("start"),
447
- daemon: z5.object({
448
- version: z5.string().min(1),
449
- /** Shown in the app's runner list so a user can tell their machines apart. */
450
- label: z5.string().min(1).max(120),
451
- platform: z5.enum(["darwin", "linux", "win32"])
452
- }),
453
- capabilities: CapabilityMatrix
454
- }).strict();
455
- var PairStartResponse = z5.object({
456
- /** Secret the daemon polls with. Never shown to the user. */
457
- deviceCode: z5.string().min(20),
458
- /** Short code the user reads and confirms in the browser. */
459
- userCode: z5.string().min(4).max(16),
460
- /** Where the user approves. Must be on the server's own origin. */
461
- verificationUrl: z5.url(),
462
- /** Epoch ms after which the code is dead ({@link MUSTS.PAIR_CODE_EXPIRES}). */
463
- expiresAt: z5.number().int().positive(),
464
- /** How often the daemon may poll. */
465
- pollIntervalMs: z5.number().int().min(500).max(6e4)
466
- }).strict();
467
- var PairPollRequest = z5.object({
468
- protocolVersion: z5.literal(PROTOCOL_VERSION),
469
- action: z5.literal("poll"),
470
- deviceCode: z5.string().min(20)
471
- }).strict();
472
- var PairPollResponse = z5.discriminatedUnion("status", [
473
- z5.object({ status: z5.literal("pending") }).strict(),
474
- z5.object({ status: z5.literal("denied") }).strict(),
475
- z5.object({ status: z5.literal("expired") }).strict(),
476
- z5.object({
477
- status: z5.literal("approved"),
478
- /** Bearer token for every later call. Scoped to exactly one user. */
479
- runnerToken: z5.string().min(20),
480
- runnerId: z5.string().min(1),
481
- /** The app's id for the approving user — this daemon's owner forever. */
482
- owner: z5.string().min(1),
483
- /** Display name for the trust UI, if the app offers one. */
484
- ownerLabel: z5.string().optional()
485
- }).strict()
486
- ]);
487
- var PairRequest = z5.discriminatedUnion("action", [
488
- PairStartRequest,
489
- PairPollRequest
490
- ]);
491
- var ClaimRequest = z5.object({
492
- protocolVersion: z5.literal(PROTOCOL_VERSION),
493
- runnerId: z5.string().min(1),
494
- /** Re-sent on every claim so a server never matches against a stale matrix. */
495
- capabilities: CapabilityMatrix,
496
- /** Upper bound on jobs to return; the server may return fewer. */
497
- max: z5.number().int().min(1).max(64)
498
- }).strict();
499
- var ClaimResponse = z5.object({
500
- jobs: z5.array(ClaimedJob),
501
- /** Lease duration granted, so the daemon knows its renewal deadline. */
502
- leaseMs: z5.number().int().positive()
503
- }).strict();
504
- var HeartbeatRequest = z5.object({
505
- protocolVersion: z5.literal(PROTOCOL_VERSION),
506
- runnerId: z5.string().min(1),
507
- daemonVersion: z5.string().min(1),
508
- capabilities: CapabilityMatrix,
509
- /** Jobs this daemon believes it holds; the server renews their leases. */
510
- activeJobIds: z5.array(z5.string().min(1)),
511
- /** True while the owner has the daemon paused; the server stops offering work. */
512
- paused: z5.boolean()
513
- }).strict();
514
- var HeartbeatResponse = z5.object({
515
- /** Once true, the daemon stops claiming and abandons in-flight work. */
516
- revoked: z5.boolean(),
517
- /**
518
- * Per-job cancel (byollm_001 Rev 1 §C). The daemon aborts these jobs'
519
- * in-flight backend calls and reports them `canceled`.
520
- */
521
- cancel: z5.array(z5.string().min(1)),
522
- /** Jobs whose leases were renewed, with their new expiry. */
523
- leases: z5.array(
524
- z5.object({
525
- jobId: z5.string().min(1),
526
- expiresAt: z5.number().int().positive()
527
- }).strict()
528
- ),
529
- /**
530
- * Jobs the daemon thinks it holds but the server has reassigned or
531
- * expired. The daemon must stop work on these and not report results.
532
- */
533
- lost: z5.array(z5.string().min(1)),
534
- /** Server clock, so a daemon with a skewed clock still honors leases. */
535
- serverTime: z5.number().int().positive()
536
- }).strict();
537
- var ResultRequest = z5.object({
538
- protocolVersion: z5.literal(PROTOCOL_VERSION),
539
- runnerId: z5.string().min(1),
540
- jobId: z5.string().min(1),
541
- outcome: JobOutcome,
542
- /** Which model actually served it, for the result's provenance. */
543
- model: z5.string().min(1),
544
- backendClass: BackendClass,
545
- /** Wall-clock milliseconds the backend call took. */
546
- durationMs: z5.number().int().nonnegative()
547
- }).strict();
548
- var ResultResponse = z5.object({
549
- /**
550
- * False when the submission lost an idempotency race or the lease was
551
- * already gone — the daemon should discard, not retry
552
- * ({@link MUSTS.RESULT_IDEMPOTENT}).
553
- */
554
- accepted: z5.boolean(),
555
- /** The job's state after this submission. */
556
- state: z5.string().min(1)
557
- }).strict();
558
- var ReleaseRequest = z5.object({
559
- protocolVersion: z5.literal(PROTOCOL_VERSION),
560
- runnerId: z5.string().min(1),
561
- jobIds: z5.array(z5.string().min(1)),
562
- /**
563
- * Why, so the app's runner list can say something true.
564
- *
565
- * `refused` is load-bearing, not cosmetic: the server cannot evaluate a
566
- * daemon's *local* `named` allowlist (§4.2), so it may legitimately offer
567
- * a job this daemon then declines. The server MUST record the refusal and
568
- * stop offering that job to that runner, or the pair would spin between
569
- * claim and release forever.
570
- */
571
- reason: z5.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
572
- }).strict();
573
- var ReleaseResponse = z5.object({
574
- released: z5.array(z5.string().min(1))
575
- }).strict();
576
- var WireErrorCode = z5.enum([
577
- "bad-request",
578
- "unsupported-protocol-version",
579
- "unauthorized",
580
- "revoked",
581
- "not-found",
582
- "rate-limited",
583
- "server-error"
584
- ]);
585
- var WireError = z5.object({
586
- error: WireErrorCode,
587
- message: z5.string().min(1),
588
- /** Seconds; mirrors Retry-After for `rate-limited` and `server-error`. */
589
- retryAfter: z5.number().int().nonnegative().optional()
590
- }).strict();
591
- var ERROR_STATUS = Object.freeze({
592
- "bad-request": 400,
593
- "unsupported-protocol-version": 400,
594
- unauthorized: 401,
595
- revoked: 403,
596
- "not-found": 404,
597
- "rate-limited": 429,
598
- "server-error": 500
2645
+ var RETIRED_MUSTS = Object.freeze({
2646
+ RESULT_PROVENANCE: {
2647
+ supersededBy: "PROVENANCE_NAMES_DEVICE",
2648
+ note: "Strengthened, not renamed: attribution is now by proof of possession \u2014 the result's signature must verify against the device the upstream granted the lease to \u2014 rather than by a provenance label travelling beside it. byollm_009 \xA711 states the stronger form."
2649
+ }
599
2650
  });
2651
+ var MUST_IDS = Object.freeze(Object.keys(MUSTS));
2652
+ function mustsVerifiedBy(kind) {
2653
+ return MUST_IDS.filter((id) => kindsOf(MUSTS[id]).includes(kind));
2654
+ }
600
2655
  export {
2656
+ ABOUT,
2657
+ ABOUT_SHORT,
2658
+ ABOUT_SHORT_LEDE,
2659
+ ABOUT_SHORT_TAIL,
601
2660
  AUDIENCES,
602
2661
  Audience,
603
2662
  BACKENDS,
2663
+ BACKEND_CLASSES,
604
2664
  BACKEND_IDS,
605
- BackendAccount,
606
2665
  BackendClass,
2666
+ BackendCost,
607
2667
  BackendIdSchema,
2668
+ CLOCK_ATTRIBUTION_MS,
2669
+ CLOCK_SKEW_WARN_MS,
2670
+ CONSOLE_FRAME_VERSION,
2671
+ CONSOLE_MAX_DATA_BYTES,
608
2672
  Capability,
609
2673
  CapabilityMatrix,
610
2674
  ChatMessage,
@@ -612,24 +2676,49 @@ export {
612
2676
  ClaimRequest,
613
2677
  ClaimResponse,
614
2678
  ClaimedJob,
2679
+ ClaimedStub,
2680
+ ConsoleBye,
2681
+ ConsoleFrame,
2682
+ ConsoleHello,
2683
+ ConsoleResize,
2684
+ ConsoleStdin,
2685
+ ConsoleStdout,
615
2686
  DeliveredResult,
2687
+ ENCRYPTION_KEY_CONTEXT,
616
2688
  ENDPOINTS,
2689
+ ENVELOPE_BODY_VERSION,
2690
+ ENVELOPE_MAX_AGE_MS,
617
2691
  ERROR_STATUS,
2692
+ EnvelopeDirection,
2693
+ FetchRequest,
2694
+ FetchResponse,
2695
+ GRANT_CONTEXT,
2696
+ GRANT_MAX_AGE_MS,
2697
+ GRANT_SIGNED_FIELDS,
618
2698
  GeneratePayload,
2699
+ GrantRef,
619
2700
  HeartbeatRequest,
620
2701
  HeartbeatResponse,
621
2702
  JOB_KINDS,
622
2703
  JobKind,
623
2704
  JobOutcome,
624
2705
  JobPayload,
2706
+ JobRefused,
625
2707
  JobResultCanceled,
626
2708
  JobResultError,
627
2709
  JobResultOk,
628
2710
  JobState,
2711
+ JobStub,
629
2712
  KindedPayload,
630
2713
  Lease,
2714
+ MAX_CLOCK_SKEW_MS,
2715
+ MAX_ENVELOPE_BYTES,
2716
+ MAX_PURPOSES,
2717
+ MAX_SUCCESSION_CHAIN,
2718
+ MIN_PROTOCOL_VERSION,
631
2719
  MUSTS,
632
2720
  MUST_IDS,
2721
+ Manifest,
633
2722
  MatchRefusal,
634
2723
  OFFER_SCOPES,
635
2724
  OfferScope,
@@ -641,23 +2730,99 @@ export {
641
2730
  PairRequest,
642
2731
  PairStartRequest,
643
2732
  PairStartResponse,
2733
+ PublicIdentity,
2734
+ Purpose,
644
2735
  REFUSAL_MESSAGES,
2736
+ REFUSAL_TEXT,
2737
+ RESERVED_PURPOSE,
2738
+ RETIREMENT_WINDOW_MS,
2739
+ RefusalReason,
645
2740
  ReleaseRequest,
646
2741
  ReleaseResponse,
2742
+ RequestSignature,
2743
+ ResultDisposition,
647
2744
  ResultProvenance,
648
2745
  ResultRequest,
649
2746
  ResultResponse,
2747
+ RunMetadata,
2748
+ SIZE_CLASSES,
2749
+ SIZE_CLASS_LIMITS,
2750
+ SUCCESSION_CONTEXT,
2751
+ SUPPORTED_PROTOCOL_VERSIONS,
2752
+ SealedEnvelope,
2753
+ SealedOutcome,
2754
+ SignedGrant,
2755
+ SizeClass,
2756
+ StopReasonSchema,
2757
+ StoredKeys,
2758
+ Succession,
650
2759
  TERMINAL_STATES,
2760
+ UPDATE_OFFER_SINCE,
2761
+ UPGRADE_COMMAND,
651
2762
  WireError,
652
2763
  WireErrorCode,
2764
+ WithheldKind,
653
2765
  backendDescriptor,
2766
+ backendName,
654
2767
  canTransition,
2768
+ canonicalRequest,
2769
+ checkDaemonFloor,
2770
+ checkProtocolVersion,
2771
+ classifyCost,
2772
+ compareVersions,
2773
+ consoleDataBytes,
2774
+ consoleEnvelope,
2775
+ consoleOrder,
2776
+ cryptoReady,
2777
+ declaredVersion,
2778
+ decodeConsoleData,
2779
+ decodeEnvelopeInner,
2780
+ describeBytes,
655
2781
  effectiveOfferScope,
2782
+ encodeConsoleData,
2783
+ encodeEnvelopeInner,
2784
+ envelopeBytes,
2785
+ envelopeSignedBody,
2786
+ fingerprint,
2787
+ fromBase64Url,
2788
+ generateKeys,
2789
+ grantStatement,
656
2790
  isBackendId,
2791
+ isCloudTaggedModel,
657
2792
  isJobKind,
2793
+ isLocalHost,
658
2794
  isTerminal,
2795
+ keyId,
2796
+ kindsOf,
659
2797
  matchAudience,
2798
+ mayOfferUpdate,
2799
+ mentionsWireField,
2800
+ mustsVerifiedBy,
2801
+ open,
660
2802
  payloadTextLength,
661
- provenanceFor
2803
+ provenanceFor,
2804
+ publicIdentityOf,
2805
+ resolveCost,
2806
+ seal,
2807
+ signGrant,
2808
+ signRequest,
2809
+ signSiteRequest,
2810
+ signSuccession,
2811
+ signWith,
2812
+ singlePurposeManifest,
2813
+ sizeClassCeiling,
2814
+ sizeClassOf,
2815
+ successionStatement,
2816
+ toBase64Url,
2817
+ tooLargeMessage,
2818
+ updateOfferFor,
2819
+ verifyGrant,
2820
+ verifyLink,
2821
+ verifyPublicIdentity,
2822
+ verifyRequest,
2823
+ verifySiteRequest,
2824
+ verifyWith,
2825
+ walkSuccession,
2826
+ withoutComments
662
2827
  };
663
2828
  //# sourceMappingURL=index.js.map