@byollm/protocol 0.1.0-alpha.7 → 0.1.0-alpha.70

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,9 +1,78 @@
1
+ // src/about.ts
2
+ var ABOUT = `# About BYOLLM
3
+
4
+ **What BYOLLM is**
5
+
6
+ BYOLLM lets you use your own AI on websites. You install one small program on
7
+ your computer. Then, websites that support BYOLLM can use the AI you already
8
+ have \u2014 a free model running on your machine, or an AI service you already pay
9
+ for \u2014 instead of the website paying for AI and passing the cost to you.
10
+
11
+ **Why it matters**
12
+
13
+ For you:
14
+
15
+ - Your favorite model, everywhere you go.
16
+ - New models the moment you get them \u2013 not when a site gets around to adding
17
+ them.
18
+ - Encrypted end-to-end. Your prompts go to your own device; byollm.cloud can't
19
+ read them.
20
+ - Sites never learn which model you use, and your subscriptions are never
21
+ shared.
22
+ - Pay less. Sites that don't pay for AI can charge you less \u2013 or nothing.
23
+
24
+ For sites and developers:
25
+
26
+ - Zero AI bills. Your users bring their own compute.
27
+ - No floating money \u2013 you don't pay LLM bills up front and hope to collect
28
+ later, and you never ask people to prepay just to try you.
29
+ - Free trials that cost you nothing to offer.
30
+ - Ship the AI features you kept private for fear of the API bill.
31
+ - One small integration. Your users choose the models.
32
+
33
+ **Your device**
34
+
35
+ The \`byollm\` program runs on your computer. It knows which AI services you have
36
+ set up: free open-source models on your machine, metered services you pay per
37
+ use, or your own subscriptions like Claude Pro/Max. When a website you have
38
+ enabled sends work, your device runs it with the service you chose. Your
39
+ prompts are encrypted end-to-end to your own device. byollm.cloud passes them
40
+ along and cannot read them.
41
+
42
+ **Sites**
43
+
44
+ A website that wants to use BYOLLM says what it needs \u2014 "writing help," "chat,"
45
+ and so on. When you connect the site, you pick which of your services answers
46
+ each one. The site never learns which model you use. You can turn a site off at
47
+ any time, and it stops getting your work.
48
+
49
+ **Teams (optional)**
50
+
51
+ A team lets you share what runs on your devices with people you name \u2014 the free
52
+ open-source models on your machine, or a metered service with a spending limit
53
+ you set. Your subscription accounts (like Claude Pro/Max) are never shared with
54
+ anyone. That is a rule, not a setting.
55
+
56
+ **byollm.cloud (or your own relay)**
57
+
58
+ Many sites, many devices, many people. byollm.cloud keeps track of who has
59
+ allowed what and sends each job to the right device. It never sees your
60
+ prompts. If you would rather run this part yourself, the relay is open source \u2014
61
+ you can run your own instead of using byollm.cloud.`;
62
+ 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.";
63
+ 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.";
64
+ var ABOUT_SHORT = `${ABOUT_SHORT_LEDE}
65
+
66
+ ${ABOUT_SHORT_TAIL}`;
67
+
1
68
  // src/audience.ts
2
69
  import { z as z2 } from "zod";
3
70
 
4
71
  // src/backends.ts
72
+ import { isIP } from "net";
5
73
  import { z } from "zod";
6
74
  var BackendClass = z.enum(["http", "process"]);
75
+ var BACKEND_CLASSES = Object.freeze(BackendClass.options);
7
76
  var BackendCost = z.enum(["free", "metered", "subscription"]);
8
77
  var backend = (b) => Object.freeze(b);
9
78
  var BACKENDS = Object.freeze({
@@ -162,6 +231,24 @@ var BACKENDS = Object.freeze({
162
231
  class: "process",
163
232
  cost: "subscription",
164
233
  adversarialCorpus: "process"
234
+ }),
235
+ /**
236
+ * OpenAI's Codex CLI, on a ChatGPT plan — byollm_016 stage 3.
237
+ *
238
+ * `subscription`, so `SUBSCRIPTION_SELF_LOCK` pins it to its owner's own
239
+ * work whatever the config says. That is load-bearing here in a way it is
240
+ * not for `claude-cli`: Codex is an *agent*, and its default feature set
241
+ * includes a shell tool, browser control and computer use. The daemon
242
+ * disables every one of them, verified against the shipped binary rather
243
+ * than assumed — see `codex-cli.ts` — but the self-lock is the floor under
244
+ * that verification rather than a duplicate of it.
245
+ */
246
+ "codex-cli": backend({
247
+ id: "codex-cli",
248
+ label: "Codex CLI (your ChatGPT plan)",
249
+ class: "process",
250
+ cost: "subscription",
251
+ adversarialCorpus: "process"
165
252
  })
166
253
  });
167
254
  var BACKEND_IDS = Object.freeze(Object.keys(BACKENDS));
@@ -177,42 +264,87 @@ function backendDescriptor(id) {
177
264
  function isLocalHost(hostname) {
178
265
  const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
179
266
  if (host === "localhost" || host.endsWith(".localhost")) return true;
180
- if (host === "::1") return true;
267
+ const version = isIP(host);
268
+ if (version === 0) return false;
269
+ if (version === 6) {
270
+ if (host === "::1") return true;
271
+ return /^f[cd]/.test(host);
272
+ }
181
273
  if (host.startsWith("127.")) return true;
182
274
  if (host.startsWith("10.")) return true;
183
275
  if (host.startsWith("192.168.")) return true;
184
- if (/^172\.(1[6-9]|2\d|3[01])\./.test(host)) return true;
185
- if (/^f[cd]/.test(host)) return true;
186
- return false;
276
+ return /^172\.(1[6-9]|2\d|3[01])\./.test(host);
277
+ }
278
+ function isCloudTaggedModel(model) {
279
+ return /:[^:]*cloud$/.test(model);
280
+ }
281
+ function resolveCost(id, baseUrl, model) {
282
+ return classifyCost(id, baseUrl, model).cost;
283
+ }
284
+ function backendName(id) {
285
+ return BACKENDS[id].label.replace(/\s*\([^)]*\)$/, "");
187
286
  }
188
- function resolveCost(id, baseUrl) {
287
+ function classifyCost(id, baseUrl, model) {
189
288
  const declared = BACKENDS[id].cost;
190
- if (declared !== null) return declared;
191
- if (baseUrl === void 0) return "metered";
289
+ if (declared !== null) {
290
+ const label = BACKENDS[id].label;
291
+ return {
292
+ cost: declared,
293
+ because: {
294
+ subscription: `${label} runs on an account you subscribe to`,
295
+ metered: `${label} bills per token`,
296
+ free: `${label} runs on this machine`
297
+ }[declared]
298
+ };
299
+ }
300
+ if (model !== void 0 && isCloudTaggedModel(model)) {
301
+ return {
302
+ cost: "metered",
303
+ because: `its model tag ends in \`:cloud\`, so the work runs on your provider's cloud account rather than on this machine`
304
+ };
305
+ }
306
+ if (baseUrl === void 0) {
307
+ return {
308
+ cost: "metered",
309
+ because: "it has no address, so where the work runs cannot be checked"
310
+ };
311
+ }
192
312
  try {
193
- return isLocalHost(new URL(baseUrl).hostname) ? "free" : "metered";
313
+ return isLocalHost(new URL(baseUrl).hostname) ? { cost: "free", because: "it runs on this machine" } : {
314
+ cost: "metered",
315
+ because: "its address is not on this machine, so the work leaves it"
316
+ };
194
317
  } catch {
195
- return "metered";
318
+ return {
319
+ cost: "metered",
320
+ because: "its address cannot be read, so where the work runs is unknown"
321
+ };
196
322
  }
197
323
  }
198
324
 
199
325
  // src/audience.ts
200
- var Audience = z2.enum(["self", "named", "public"]);
201
- var OfferScope = z2.enum(["self", "named", "public"]);
326
+ var Audience = z2.enum(["private", "team"]);
327
+ var OfferScope = z2.enum(["private", "team"]);
202
328
  var AUDIENCES = Object.freeze(Audience.options);
203
329
  var OFFER_SCOPES = Object.freeze(OfferScope.options);
204
330
  var MatchRefusal = z2.enum([
205
331
  /** The daemon advertises no capability for this kind. */
206
332
  "no-capability",
207
- /** Job is `self` but this daemon belongs to a different user. */
333
+ /** Job is `private` but this daemon belongs to a different user. */
208
334
  "audience-self-other-owner",
209
- /** Job is `named` but this daemon's local allowlist does not admit the owner. */
335
+ /**
336
+ * Job is `team` and nothing this device verified admits the job's owner.
337
+ *
338
+ * The id predates the grant and is kept, because ids are public and cited
339
+ * by conformance output. What it means has not moved: this device was not
340
+ * shown anything it could check.
341
+ */
210
342
  "not-locally-allowed",
211
- /** Job is `named`/`public` but the server's own allowlist excludes this runner. */
343
+ /** Job is `team` but the server's own allowlist excludes this runner. */
212
344
  "not-in-server-allowlist",
213
- /** The backend offers only `self` and the job belongs to someone else. */
345
+ /** The service offers only `private` and the job belongs to someone else. */
214
346
  "offer-scope-too-narrow",
215
- /** The matched backend is subscription-class, which is locked to `self`. */
347
+ /** The matched backend is subscription-class, which is locked to `private`. */
216
348
  "subscription-self-lock",
217
349
  /** The backend spends the owner's money and they have not agreed to share it. */
218
350
  "metered-no-spend-consent",
@@ -222,16 +354,16 @@ var MatchRefusal = z2.enum([
222
354
  var ALLOWED = Object.freeze({ ok: true });
223
355
  var refuse = (refusal) => Object.freeze({ ok: false, refusal });
224
356
  function effectiveOfferScope(configured, cost, spend) {
225
- if (cost === "subscription") return "self";
226
- if (cost === "metered" && spend?.acknowledged !== true) return "self";
357
+ if (cost === "subscription") return "private";
358
+ if (cost === "metered" && spend?.acknowledged !== true) return "private";
227
359
  return configured;
228
360
  }
229
361
  function matchAudience(job, daemon) {
230
362
  const sameOwner = job.owner === daemon.owner;
231
- if (job.audience === "self" && !sameOwner) {
363
+ if (job.audience === "private" && !sameOwner) {
232
364
  return refuse("audience-self-other-owner");
233
365
  }
234
- if (job.audience === "named" && !sameOwner && job.audienceAllow !== void 0 && !job.audienceAllow.includes(daemon.owner)) {
366
+ if (job.audience === "team" && !sameOwner && job.audienceAllow !== void 0 && !job.audienceAllow.includes(daemon.owner)) {
235
367
  return refuse("not-in-server-allowlist");
236
368
  }
237
369
  const scope = effectiveOfferScope(
@@ -254,20 +386,18 @@ function matchAudience(job, daemon) {
254
386
  }
255
387
  }
256
388
  switch (scope) {
257
- case "self":
389
+ case "private":
258
390
  return refuse("offer-scope-too-narrow");
259
- case "named":
260
- return daemon.locallyAllows(job.owner) ? ALLOWED : refuse("not-locally-allowed");
261
- case "public":
262
- return ALLOWED;
391
+ case "team":
392
+ return daemon.admits(job.owner) ? ALLOWED : refuse("not-locally-allowed");
263
393
  }
264
394
  }
265
395
  var REFUSAL_MESSAGES = Object.freeze({
266
- "no-capability": "no backend on this machine is configured and healthy for that job kind",
267
- "audience-self-other-owner": "the job is private to its owner and this machine is paired to someone else",
268
- "not-locally-allowed": "the job's owner is not on this machine's allowlist (byollm allow <server> <user>)",
269
- "not-in-server-allowlist": "the app restricted this job to named runners and this machine is not one of them",
270
- "offer-scope-too-narrow": "this backend is offered to its owner only (byollm offer <backend> named|public to widen)",
396
+ "no-capability": "no backend on this device is configured and healthy for that job kind",
397
+ "audience-self-other-owner": "the job is private to its owner and this device is paired to someone else",
398
+ "not-locally-allowed": "nothing this device can verify says the job's owner may use it",
399
+ "not-in-server-allowlist": "the app restricted this job to named runners and this device is not one of them",
400
+ "offer-scope-too-narrow": "this service is offered to its owner only (`byollm offer <service> team` to widen)",
271
401
  "subscription-self-lock": "subscription-backed models run their owner's work only \u2014 this is a protocol rule, not a setting",
272
402
  "metered-no-spend-consent": "this backend bills its owner per token, and they have not agreed to spend it on other people's work",
273
403
  "metered-ceiling-reached": "this backend is shared but has reached the spend ceiling its owner set"
@@ -286,20 +416,33 @@ var PAYLOAD_LIMITS = Object.freeze({
286
416
  var ChatMessage = z3.object({
287
417
  role: z3.enum(["system", "user", "assistant"]),
288
418
  content: z3.string().max(PAYLOAD_LIMITS.maxTextChars)
289
- });
419
+ }).strict();
290
420
  var GeneratePayload = z3.object({
291
421
  prompt: z3.string().min(1).max(PAYLOAD_LIMITS.maxTextChars),
292
422
  system: z3.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
293
- }).strict();
423
+ }).strict().refine(
424
+ (payload) => payload.prompt.length + (payload.system?.length ?? 0) <= PAYLOAD_LIMITS.maxTotalChars,
425
+ {
426
+ message: `payload exceeds ${String(PAYLOAD_LIMITS.maxTotalChars)} characters`
427
+ }
428
+ );
294
429
  var ChatPayload = z3.object({
295
430
  messages: z3.array(ChatMessage).min(1).max(PAYLOAD_LIMITS.maxMessages),
296
431
  system: z3.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
297
- }).strict();
432
+ }).strict().refine(
433
+ (payload) => payload.messages.reduce((sum, m) => sum + m.content.length, 0) + (payload.system?.length ?? 0) <= PAYLOAD_LIMITS.maxTotalChars,
434
+ {
435
+ message: `payload exceeds ${String(PAYLOAD_LIMITS.maxTotalChars)} characters`
436
+ }
437
+ );
298
438
  var JobKind = z3.enum(["llm.generate", "llm.chat"]);
299
439
  var JOB_KINDS = Object.freeze(JobKind.options);
300
440
  var KindedPayload = z3.discriminatedUnion("kind", [
301
- z3.object({ kind: z3.literal("llm.generate"), payload: GeneratePayload }),
302
- z3.object({ kind: z3.literal("llm.chat"), payload: ChatPayload })
441
+ // Strict on the wrappers too. A union member that strips is a door beside
442
+ // the one that is locked: the payloads inside are strict, and an extra key
443
+ // on the envelope vanished just as quietly.
444
+ z3.object({ kind: z3.literal("llm.generate"), payload: GeneratePayload }).strict(),
445
+ z3.object({ kind: z3.literal("llm.chat"), payload: ChatPayload }).strict()
303
446
  ]);
304
447
  function isJobKind(value) {
305
448
  return JOB_KINDS.includes(value);
@@ -316,161 +459,12 @@ function payloadTextLength(kinded) {
316
459
  }
317
460
 
318
461
  // src/job.ts
319
- import { z as z4 } from "zod";
320
- var JobState = z4.enum([
321
- "queued",
322
- "claimed",
323
- "running",
324
- "ok",
325
- "error",
326
- "canceled",
327
- "expired"
328
- ]);
329
- var TERMINAL_STATES = Object.freeze([
330
- "ok",
331
- "error",
332
- "canceled",
333
- "expired"
334
- ]);
335
- function isTerminal(state) {
336
- return TERMINAL_STATES.includes(state);
337
- }
338
- var TRANSITIONS = Object.freeze({
339
- queued: ["claimed", "expired", "canceled"],
340
- // A claimed job returns to `queued` when its lease expires un-renewed
341
- // ({@link MUSTS.LEASE_RECLAIMABLE}).
342
- claimed: ["running", "queued", "canceled", "error"],
343
- running: ["ok", "error", "canceled", "queued"],
344
- ok: [],
345
- error: [],
346
- canceled: [],
347
- expired: []
348
- });
349
- function canTransition(from, to) {
350
- return TRANSITIONS[from].includes(to);
351
- }
352
- var Lease = z4.object({
353
- /**
354
- * Identifies *this* grant, not just its holder.
355
- *
356
- * A runner can hold a job, release it, and claim it again — three leases,
357
- * one runner id. Without an id for the grant itself, a lease-scoped request
358
- * names a mutable target ambiguously, and a replayed release from the first
359
- * grant lands on the third: the job returns to the queue while the daemon
360
- * is mid-execution, and the work runs twice on the owner's hardware.
361
- *
362
- * That was a live hole, found in review after signed requests shipped. The
363
- * signature scheme's replay argument rests on endpoints being idempotent —
364
- * and release *is*, per lease, but not across leases, because nothing in
365
- * the request said which one.
366
- */
367
- id: z4.string().min(1),
368
- /** The runner holding the lease. */
369
- runnerId: z4.string().min(1),
370
- /** Epoch milliseconds after which the claim is void. */
371
- expiresAt: z4.number().int().positive()
372
- });
373
- var JobPayload = z4.union([GeneratePayload, ChatPayload]);
374
- var ClaimedJob = z4.object({
375
- id: z4.string().min(1),
376
- kind: JobKind,
377
- payload: JobPayload,
378
- audience: Audience,
379
- /** The app's id for the user who enqueued it. */
380
- owner: z4.string().min(1),
381
- /** Runner owners the app restricted a `named` job to, if any. */
382
- audienceAllow: z4.array(z4.string().min(1)).optional(),
383
- lease: Lease
384
- }).strict();
385
- var ResultProvenance = z4.object({
386
- /** The audience the job ran under. */
387
- audience: Audience,
388
- /** The runner that produced it. */
389
- runnerId: z4.string().min(1),
390
- /** The runner owner's id in this app's namespace. */
391
- runnerOwner: z4.string().min(1),
392
- /** Which backend class produced it — an HTTP call or a sandboxed spawn. */
393
- backendClass: BackendClass,
394
- /** The model the runner reports having used. */
395
- model: z4.string().min(1),
396
- /**
397
- * False only for `self` jobs. When true the app MUST treat `text` as
398
- * untrusted third-party content.
399
- */
400
- untrusted: z4.boolean()
401
- }).strict();
402
- function provenanceFor(input) {
403
- return {
404
- audience: input.audience,
405
- runnerId: input.runnerId,
406
- runnerOwner: input.runnerOwner,
407
- backendClass: input.backendClass,
408
- model: input.model,
409
- untrusted: input.audience !== "self"
410
- };
411
- }
412
- var JobResultOk = z4.object({
413
- outcome: z4.literal("ok"),
414
- text: z4.string(),
415
- /** Optional reference to a stored artifact; never a local path. */
416
- artifactUrl: z4.url().optional()
417
- }).strict();
418
- var JobResultError = z4.object({
419
- outcome: z4.literal("error"),
420
- code: z4.string().min(1),
421
- message: z4.string().min(1),
422
- /** Whether the app may reasonably re-enqueue. */
423
- retryable: z4.boolean()
424
- }).strict();
425
- var JobResultCanceled = z4.object({
426
- outcome: z4.literal("canceled")
427
- }).strict();
428
- var JobOutcome = z4.discriminatedUnion("outcome", [
429
- JobResultOk,
430
- JobResultError,
431
- JobResultCanceled
432
- ]);
433
- var DeliveredResult = z4.object({
434
- jobId: z4.string().min(1),
435
- state: JobState,
436
- outcome: JobOutcome.optional(),
437
- provenance: ResultProvenance.optional()
438
- }).strict();
439
- var SizeClass = z4.enum(["small", "medium", "large", "unbounded"]);
440
- var SIZE_CLASS_LIMITS = Object.freeze({
441
- small: 4e3,
442
- medium: 64e3,
443
- large: Number.POSITIVE_INFINITY
444
- });
445
- function sizeClassCeiling(sizeClass) {
446
- if (sizeClass === "unbounded") return Number.POSITIVE_INFINITY;
447
- return SIZE_CLASS_LIMITS[sizeClass];
448
- }
449
- function sizeClassOf(textChars) {
450
- if (textChars <= SIZE_CLASS_LIMITS.small) return "small";
451
- if (textChars <= SIZE_CLASS_LIMITS.medium) return "medium";
452
- return "large";
453
- }
454
- var JobStub = z4.object({
455
- id: z4.string().min(1),
456
- kind: JobKind,
457
- /** The app's id for the user who enqueued it. */
458
- owner: z4.string().min(1),
459
- audience: Audience,
460
- audienceAllow: z4.array(z4.string().min(1)).optional(),
461
- sizeClass: SizeClass,
462
- /** Reserved for byollm_006. False until streaming exists. */
463
- streaming: z4.boolean(),
464
- /** Epoch ms after which the work is pointless; bounds ciphertext retention. */
465
- deadlineAt: z4.number().int().positive()
466
- }).strict();
467
- var ClaimedStub = JobStub.extend({ lease: Lease }).strict();
468
-
469
- // src/envelope.ts
470
- import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2 } from "crypto";
471
- import sodium from "libsodium-wrappers";
472
462
  import { z as z6 } from "zod";
473
463
 
464
+ // src/grant.ts
465
+ import { Buffer as Buffer2 } from "buffer";
466
+ import { z as z5 } from "zod";
467
+
474
468
  // src/keys.ts
475
469
  import {
476
470
  createHash,
@@ -480,12 +474,12 @@ import {
480
474
  sign,
481
475
  verify
482
476
  } from "crypto";
483
- import { z as z5 } from "zod";
484
- var PublicIdentity = z5.object({
477
+ import { z as z4 } from "zod";
478
+ var PublicIdentity = z4.object({
485
479
  /** Raw Ed25519 public key. The pinned one. */
486
- identity: z5.string().min(1),
480
+ identity: z4.string().min(1),
487
481
  /** Raw X25519 public key, for sealing to this party. */
488
- encryption: z5.string().min(1),
482
+ encryption: z4.string().min(1),
489
483
  /**
490
484
  * Ed25519 signature over the encryption key, by the identity key.
491
485
  *
@@ -493,16 +487,16 @@ var PublicIdentity = z5.object({
493
487
  * own while relaying a genuine identity: the receiver pins the identity
494
488
  * and refuses any encryption key not signed by it.
495
489
  */
496
- encryptionSig: z5.string().min(1)
490
+ encryptionSig: z4.string().min(1)
497
491
  }).strict();
498
- var StoredKeys = z5.object({
499
- version: z5.literal(1),
500
- identityPublic: z5.string().min(1),
501
- identityPrivate: z5.string().min(1),
502
- encryptionPublic: z5.string().min(1),
503
- encryptionPrivate: z5.string().min(1),
504
- encryptionSig: z5.string().min(1),
505
- createdAt: z5.number().int().positive()
492
+ var StoredKeys = z4.object({
493
+ version: z4.literal(1),
494
+ identityPublic: z4.string().min(1),
495
+ identityPrivate: z4.string().min(1),
496
+ encryptionPublic: z4.string().min(1),
497
+ encryptionPrivate: z4.string().min(1),
498
+ encryptionSig: z4.string().min(1),
499
+ createdAt: z4.number().int().positive()
506
500
  }).strict();
507
501
  var ENCRYPTION_KEY_CONTEXT = "byollm/v1/encryption-key";
508
502
  function rawPublic(key) {
@@ -595,21 +589,456 @@ function fingerprint(identityPublic) {
595
589
  }
596
590
  var keyId = (identityPublic) => fingerprint(identityPublic);
597
591
 
592
+ // src/grant.ts
593
+ var GRANT_MAX_AGE_MS = 12e4;
594
+ var CLOCK_SKEW_WARN_MS = 3e4;
595
+ var CLOCK_ATTRIBUTION_MS = 5e3;
596
+ var GRANT_CONTEXT = "byollm/v1/grant";
597
+ var SignedGrant = z5.object({
598
+ /**
599
+ * This grant's own id — what makes it single-use.
600
+ *
601
+ * **Not the job id, and the difference is load-bearing.** Binding
602
+ * single-use to `jobId` would refuse a legitimate retry: a claim that
603
+ * times out is re-claimed, the control plane authors a second grant for
604
+ * the same job, and a device that recorded the job id as spent would
605
+ * reject its own recovery. A fresh id per authorship replays nothing and
606
+ * retries fine.
607
+ */
608
+ grantId: z5.string().min(1),
609
+ /**
610
+ * The job this grant admits, and only this one.
611
+ *
612
+ * A grant lifted from one job and presented for another is the obvious
613
+ * attack, and this field is why it fails.
614
+ */
615
+ jobId: z5.string().min(1),
616
+ /**
617
+ * The site the work came from, **as a key id** — byollm-review 2026-08-27.
618
+ *
619
+ * This was `siteId`, holding the site's id in the control plane's
620
+ * namespace, and it was signed by the engine and read by nobody. A signed
621
+ * field nobody checks is not a weak guarantee, it is the appearance of
622
+ * one: the design says "the grant carries the site", and nothing anywhere
623
+ * compared it to anything.
624
+ *
625
+ * It could not be compared. Job ids are chosen per site, so a grant
626
+ * authored for (site A, `job_1`) satisfied every device check against a
627
+ * stub naming (site B, `job_1`) — but the device holds sites only by the
628
+ * key ids it pinned, and had no way to relate a control-plane uuid to
629
+ * one. Checking the field would have meant a lookup through the party the
630
+ * grant exists to distrust.
631
+ *
632
+ * So the namespace changes to the one the device already has, and the
633
+ * name changes with it: this is the same value as {@link JobStub.site},
634
+ * compared directly, no lookup and nothing to believe. The control-plane
635
+ * id is not carried alongside — it had no reader, and keeping an
636
+ * unchecked field beside a checked one is how this hole was dug.
637
+ */
638
+ site: z5.string().min(1),
639
+ /** Whose job it is — the person the site enqueued for. */
640
+ user: z5.string().min(1),
641
+ /**
642
+ * Whose device it is for.
643
+ *
644
+ * Passed to {@link verifyGrant} rather than read out of the document, for
645
+ * the reason every verifier here takes its subject as an argument: a
646
+ * verifier that recovered the owner from the signed bytes would accept a
647
+ * genuine grant belonging to somebody else and pass every check.
648
+ */
649
+ owner: z5.string().min(1),
650
+ /** The site purpose this job serves — byollm_016 Amendment L. */
651
+ purpose: z5.string().min(1),
652
+ /** The kind of work. */
653
+ kind: z5.string().min(1),
654
+ /**
655
+ * The service the control plane resolved this (purpose, kind) to, from
656
+ * the user's own mapping.
657
+ *
658
+ * Selection is the control plane's; **offer-consistency is the
659
+ * device's**. A device verifies it actually offers this service, at a
660
+ * scope that includes {@link user}, before running anything.
661
+ */
662
+ service: z5.string().min(1),
663
+ /** When the control plane signed it — epoch ms, the only anchor for age. */
664
+ issuedAt: z5.number().int().positive(),
665
+ /** Base64url Ed25519 over {@link grantStatement}. */
666
+ signature: z5.string().min(1)
667
+ }).strict();
668
+ var GRANT_SIGNED_FIELDS = Object.freeze(
669
+ Object.keys(SignedGrant.shape).filter((key) => key !== "signature").sort()
670
+ );
671
+ function grantStatement(claims) {
672
+ return Buffer2.from(
673
+ JSON.stringify([
674
+ GRANT_CONTEXT,
675
+ ...GRANT_SIGNED_FIELDS.map((field) => claims[field])
676
+ ]),
677
+ "utf8"
678
+ );
679
+ }
680
+ function signGrant(keys, claims) {
681
+ return { ...claims, signature: signWith(keys, grantStatement(claims)) };
682
+ }
683
+ function verifyGrant(input) {
684
+ const { grant, now } = input;
685
+ if (grant.owner !== input.owner) return "wrong-owner";
686
+ if (grant.jobId !== input.jobId) return "wrong-job";
687
+ const age = now - grant.issuedAt;
688
+ if (age < -CLOCK_SKEW_WARN_MS) return "from-the-future";
689
+ if (age > (input.maxAgeMs ?? GRANT_MAX_AGE_MS)) return "expired";
690
+ return verifyWith(
691
+ input.controlPlanePublic,
692
+ grantStatement(grant),
693
+ grant.signature
694
+ ) ? null : "bad-signature";
695
+ }
696
+
697
+ // src/job.ts
698
+ var JobState = z6.enum([
699
+ "queued",
700
+ "claimed",
701
+ "running",
702
+ "ok",
703
+ "error",
704
+ "canceled",
705
+ "expired"
706
+ ]);
707
+ var TERMINAL_STATES = Object.freeze([
708
+ "ok",
709
+ "error",
710
+ "canceled",
711
+ "expired"
712
+ ]);
713
+ function isTerminal(state) {
714
+ return TERMINAL_STATES.includes(state);
715
+ }
716
+ var TRANSITIONS = Object.freeze({
717
+ queued: ["claimed", "expired", "canceled"],
718
+ // A claimed job returns to `queued` when its lease expires un-renewed
719
+ // ({@link MUSTS.LEASE_RECLAIMABLE}).
720
+ claimed: ["running", "queued", "canceled", "error"],
721
+ running: ["ok", "error", "canceled", "queued"],
722
+ ok: [],
723
+ error: [],
724
+ canceled: [],
725
+ expired: []
726
+ });
727
+ function canTransition(from, to) {
728
+ return TRANSITIONS[from].includes(to);
729
+ }
730
+ var Lease = z6.object({
731
+ /**
732
+ * Identifies *this* grant, not just its holder.
733
+ *
734
+ * A runner can hold a job, release it, and claim it again — three leases,
735
+ * one runner id. Without an id for the grant itself, a lease-scoped request
736
+ * names a mutable target ambiguously, and a replayed release from the first
737
+ * grant lands on the third: the job returns to the queue while the daemon
738
+ * is mid-execution, and the work runs twice on the owner's hardware.
739
+ *
740
+ * That was a live hole, found in review after signed requests shipped. The
741
+ * signature scheme's replay argument rests on endpoints being idempotent —
742
+ * and release *is*, per lease, but not across leases, because nothing in
743
+ * the request said which one.
744
+ */
745
+ id: z6.string().min(1),
746
+ /** The runner holding the lease. */
747
+ runnerId: z6.string().min(1),
748
+ /** Epoch milliseconds after which the claim is void. */
749
+ expiresAt: z6.number().int().positive()
750
+ }).strict();
751
+ var JobPayload = z6.union([GeneratePayload, ChatPayload]);
752
+ var ClaimedJob = z6.object({
753
+ id: z6.string().min(1),
754
+ kind: JobKind,
755
+ payload: JobPayload,
756
+ audience: Audience,
757
+ /** The app's id for the user who enqueued it. */
758
+ owner: z6.string().min(1),
759
+ /**
760
+ * Which site's job — V1-3.
761
+ *
762
+ * The stub has always carried it; the opened job did not, so everything
763
+ * downstream of the payload — the ingress line above all — recorded a job
764
+ * id that belongs to a site without saying which. Two sites can choose
765
+ * the same id, and the meter is the product.
766
+ *
767
+ * Optional so a caller assembling a job by hand is not forced to invent
768
+ * one, and so this reads as what it is: a fact about where the work came
769
+ * from, not a second copy of the routing key.
770
+ */
771
+ site: z6.string().min(1).optional(),
772
+ /**
773
+ * Which of the owner's services runs this — resolved, not requested.
774
+ *
775
+ * The daemon picks the backend from this, so it has to be the answer
776
+ * rather than a wish. On a relayed route it is copied off the **grant**,
777
+ * where a control plane put the person's own mapping and signed it; a
778
+ * site never named it and could not.
779
+ *
780
+ * It used to be what the site asked for, which made a job that selected a
781
+ * non-default service liable to be served by the default instead — the
782
+ * substitution `NO_PAYLOAD_ROUTING` forbids. Amendment L removed the
783
+ * asking; what is left is the answering.
784
+ *
785
+ * Optional, because direct mode has no control plane to resolve anything
786
+ * and the owner's own defaults answer under the ambiguity law.
787
+ */
788
+ service: z6.string().min(1).optional(),
789
+ lease: Lease
790
+ }).strict();
791
+ var ResultProvenance = z6.object({
792
+ /** The audience the job ran under. */
793
+ audience: Audience,
794
+ /** The runner that produced it. */
795
+ runnerId: z6.string().min(1),
796
+ /** The runner owner's id in this app's namespace. */
797
+ runnerOwner: z6.string().min(1),
798
+ /** Which backend class produced it — an HTTP call or a sandboxed spawn. */
799
+ backendClass: BackendClass,
800
+ /** The model the runner reports having used. */
801
+ model: z6.string().min(1),
802
+ /**
803
+ * False only for `self` jobs. When true the app MUST treat `text` as
804
+ * untrusted third-party content.
805
+ */
806
+ untrusted: z6.boolean()
807
+ }).strict();
808
+ function provenanceFor(input) {
809
+ return {
810
+ audience: input.audience,
811
+ runnerId: input.runnerId,
812
+ runnerOwner: input.runnerOwner,
813
+ backendClass: input.backendClass,
814
+ model: input.model,
815
+ untrusted: input.audience !== "private"
816
+ };
817
+ }
818
+ var RunMetadata = z6.object({
819
+ /** Which model actually served it. */
820
+ model: z6.string().min(1),
821
+ backendClass: BackendClass,
822
+ /** Wall-clock milliseconds the backend call took. */
823
+ durationMs: z6.number().int().nonnegative()
824
+ }).strict();
825
+ var JobResultOk = z6.object({
826
+ outcome: z6.literal("ok"),
827
+ text: z6.string(),
828
+ /** Optional reference to a stored artifact; never a local path. */
829
+ artifactUrl: z6.url().optional()
830
+ }).strict();
831
+ var JobResultError = z6.object({
832
+ outcome: z6.literal("error"),
833
+ code: z6.string().min(1),
834
+ message: z6.string().min(1),
835
+ /** Whether the app may reasonably re-enqueue. */
836
+ retryable: z6.boolean()
837
+ }).strict();
838
+ var JobResultCanceled = z6.object({
839
+ outcome: z6.literal("canceled")
840
+ }).strict();
841
+ var JobOutcome = z6.discriminatedUnion("outcome", [
842
+ JobResultOk,
843
+ JobResultError,
844
+ JobResultCanceled
845
+ ]);
846
+ var RefusalReason = z6.enum([
847
+ /**
848
+ * Two or more services answer this kind and the owner has named no default,
849
+ * so the kind is withheld. Nobody may pick on the owner's behalf — the wrong
850
+ * guess is the metered one.
851
+ *
852
+ * Told apart from its neighbour deliberately, and the line is whether a
853
+ * requester can walk a namespace. There are two kinds; asking about one
854
+ * enumerates nothing they could not learn from what the device advertises,
855
+ * and the difference is actionable — "the owner has not chosen" is fixable
856
+ * by the owner, "the default cannot serve you" is not. It is also already
857
+ * what a team member sees on the devices page: `awaitingDefault` carries
858
+ * exactly this, by kind, for exactly this reason.
859
+ */
860
+ "default-ambiguity",
861
+ /**
862
+ * A default exists and this requester can never use it — byollm_016's
863
+ * defaults-meet-audiences corner.
864
+ *
865
+ * The specimen: an owner's default for `llm.chat` is their Claude
866
+ * subscription, self-locked by `SUBSCRIPTION_SELF_LOCK`. A team member's
867
+ * job resolves to it and can never be served by it. That must be a refusal
868
+ * on the spot, not a wait that expires an hour later looking like nobody
869
+ * was online.
870
+ *
871
+ * Bounded like the value above, and unprobeable for the same reason: the
872
+ * requester named nothing, so there is no name space to walk.
873
+ */
874
+ "default-unusable"
875
+ ]);
876
+ var JobRefused = z6.object({
877
+ outcome: z6.literal("refused"),
878
+ reason: RefusalReason,
879
+ /** Plain words for a human reading a log, never parsed. */
880
+ message: z6.string().min(1)
881
+ }).strict();
882
+ var REFUSAL_TEXT = Object.freeze({
883
+ "default-ambiguity": "this device serves that kind from more than one service and its owner has not chosen which",
884
+ "default-unusable": "this device's default for that kind cannot run work for you"
885
+ });
886
+ var SealedOutcome = z6.object({ outcome: JobOutcome, ran: RunMetadata }).strict();
887
+ var DeliveredResult = z6.object({
888
+ jobId: z6.string().min(1),
889
+ state: JobState,
890
+ outcome: JobOutcome.optional(),
891
+ provenance: ResultProvenance.optional(),
892
+ /**
893
+ * Present, and always `true`, when this did not come from a runner —
894
+ * {@link MUSTS.FALLBACK_LABELED}.
895
+ *
896
+ * The app's own `onNoRunner` value produced it: a hosted model, a cached
897
+ * answer, an apology. It never travels on the wire, because nothing on
898
+ * the wire produced it; it exists so that a result which did not come
899
+ * from the user's own compute cannot be reported as though it did.
900
+ *
901
+ * A literal rather than a boolean, so `fallback: false` is not a
902
+ * spelling anybody can reach for. The absence of this field means a
903
+ * runner ran the job, and the *server* stamps it — an app cannot supply
904
+ * a substitute that hides what it is.
905
+ */
906
+ fallback: z6.literal(true).optional()
907
+ }).strict();
908
+ var SizeClass = z6.enum(["small", "medium", "large", "unbounded"]);
909
+ var SIZE_CLASSES = Object.freeze(SizeClass.options);
910
+ var MAX_ENVELOPE_BYTES = 10 * 1024 * 1024;
911
+ function envelopeBytes(envelope) {
912
+ const serialised = JSON.stringify(envelope);
913
+ return serialised === void 0 ? 0 : serialised.length;
914
+ }
915
+ var SIZE_CLASS_LIMITS = Object.freeze({
916
+ small: 4e3,
917
+ medium: 64e3,
918
+ large: Number.POSITIVE_INFINITY
919
+ });
920
+ function sizeClassCeiling(sizeClass) {
921
+ if (sizeClass === "unbounded") return Number.POSITIVE_INFINITY;
922
+ return SIZE_CLASS_LIMITS[sizeClass];
923
+ }
924
+ function sizeClassOf(textChars) {
925
+ if (textChars <= SIZE_CLASS_LIMITS.small) return "small";
926
+ if (textChars <= SIZE_CLASS_LIMITS.medium) return "medium";
927
+ return "large";
928
+ }
929
+ var JobStub = z6.object({
930
+ id: z6.string().min(1),
931
+ kind: JobKind,
932
+ /** The app's id for the user who enqueued it. */
933
+ owner: z6.string().min(1),
934
+ /**
935
+ * Which site this job belongs to — byollm_009 Amendment A §A.3.
936
+ *
937
+ * **The site's identity key id**, not an id somebody assigned it. §6 has
938
+ * listed `site` since this spec was frozen; the schema never carried it,
939
+ * which is the drift the amendment closes.
940
+ *
941
+ * A key id rather than an opaque handle for one reason above the others:
942
+ * it makes the stub *self-describing* instead of a pointer into somebody
943
+ * else's table. A daemon holds this key id already, from pinning, so it
944
+ * can check `stub.site` against the payload envelope's `senderKeyId`
945
+ * without a lookup and without trusting the party that routed it. An
946
+ * opaque id can only be believed.
947
+ *
948
+ * It also avoids inventing a second namespace for a thing that has a
949
+ * canonical one — the shape of finding 41 (two owner namespaces compared
950
+ * for equality) and of finding fourteen before it.
951
+ *
952
+ * Rotation is a designed transition rather than a cost: a site publishes a
953
+ * new identity signed by the outgoing one, both are valid through an
954
+ * overlap window, and a daemon re-keys its own map by verifying that
955
+ * signature against the key it already pinned (§A.3.1).
956
+ */
957
+ site: z6.string().min(1),
958
+ audience: Audience,
959
+ // `audienceAllow` is **not** here, and its absence is the enforcement —
960
+ // cloud_008 §0.2.
961
+ //
962
+ // It was a list of the people who may run a job, travelling to every
963
+ // routing party on every shared job. byollm_001 Rev 1 §B settled who
964
+ // decides that long before this schema existed: *the daemon's own list
965
+ // decides, not the server's*, and `allowlist.predicateFor(origin)` is the
966
+ // enforcement in both lanes. So this was a second answer to a question the
967
+ // daemon already owned — able only to agree, in which case it was
968
+ // redundant, or to disagree, in which case nothing said which wins.
969
+ //
970
+ // The rule it leaves behind, which decides the next field too: **a class
971
+ // the router acts on may travel; membership never does.** `audience` stays
972
+ // for exactly that reason — the relay narrows on it. A roster does not
973
+ // travel, so `ROSTER_NOT_DISCLOSED` holds here by absence, which is the
974
+ // strongest way for a MUST to hold.
975
+ //
976
+ // The site keeps its own copy on `JobRecord` and still filters candidates
977
+ // with it before offering. That is server-internal, where the party
978
+ // holding the list authored it.
979
+ /**
980
+ * Which of the site's declared purposes this job serves — Amendment L.
981
+ *
982
+ * **A need, never a name.** The site's vocabulary is its own purposes;
983
+ * the person's is their services; and the two never meet. This field says
984
+ * "writing-assistant", and a control plane joins it to whatever that
985
+ * person mapped it to. The site learns only whether the slot was
986
+ * satisfiable.
987
+ *
988
+ * It replaced `service`, which let a site name one of the owner's
989
+ * services directly. That field is gone from both routes (Amendment L
990
+ * rider) and its refusal machinery with it — including the collapsed
991
+ * `select-unavailable`, which existed so that "no such service" and "not
992
+ * offered to you" could not be told apart. There is nothing left to
993
+ * probe: **a vocabulary that never crosses the boundary cannot be
994
+ * enumerated across it**, which is a stronger guarantee than the one the
995
+ * collapse gave.
996
+ *
997
+ * It travels for the reason the absent `audienceAllow` establishes: *a
998
+ * class the router acts on may travel; membership never does.* A purpose
999
+ * is a class, and the control plane acts on it.
1000
+ *
1001
+ * Optional because direct mode has no control plane to hold a mapping and
1002
+ * is kind-only: the owner's own config and defaults answer, under the
1003
+ * ambiguity law as shipped. Absent on a relayed route resolves against
1004
+ * the site's reserved purpose, which a site that declared its own
1005
+ * purposes will not have mapped — so the slot reads as unmapped and the
1006
+ * site falls back, loudly enough and without a special case.
1007
+ *
1008
+ * A **stub** field and never a payload field, which is the line
1009
+ * `NO_PAYLOAD_ROUTING` draws: the prompt cannot reach it, so no amount of
1010
+ * user text can influence what runs.
1011
+ */
1012
+ purpose: z6.string().min(1).optional(),
1013
+ sizeClass: SizeClass,
1014
+ /** Reserved for byollm_006. False until streaming exists. */
1015
+ streaming: z6.boolean(),
1016
+ /** Epoch ms after which the work is pointless; bounds ciphertext retention. */
1017
+ deadlineAt: z6.number().int().positive()
1018
+ }).strict();
1019
+ var ClaimedStub = JobStub.extend({
1020
+ lease: Lease,
1021
+ grant: SignedGrant.optional()
1022
+ }).strict();
1023
+
598
1024
  // src/envelope.ts
1025
+ import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2 } from "crypto";
1026
+ import sodium from "libsodium-wrappers";
1027
+ import { z as z7 } from "zod";
599
1028
  var readied;
600
1029
  async function cryptoReady() {
601
1030
  readied ??= sodium.ready;
602
1031
  await readied;
603
1032
  }
604
1033
  var ENVELOPE_MAX_AGE_MS = 24 * 60 * 6e4;
605
- var EnvelopeDirection = z6.enum(["payload", "result"]);
606
- var SealedEnvelope = z6.object({
1034
+ var EnvelopeDirection = z7.enum(["payload", "result"]);
1035
+ var SealedEnvelope = z7.object({
607
1036
  /** Base64url `crypto_box_seal` output over the signed plaintext. */
608
- ciphertext: z6.string().min(1),
1037
+ ciphertext: z7.string().min(1),
609
1038
  /** Who this was sealed to — the recipient checks it is them. */
610
- recipientKeyId: z6.string().min(1),
1039
+ recipientKeyId: z7.string().min(1),
611
1040
  /** Who signed it — the recipient checks this against its pin. */
612
- senderKeyId: z6.string().min(1),
1041
+ senderKeyId: z7.string().min(1),
613
1042
  direction: EnvelopeDirection,
614
1043
  /**
615
1044
  * When this ciphertext stops being worth keeping.
@@ -623,7 +1052,7 @@ var SealedEnvelope = z6.object({
623
1052
  * Not trusted as written: it is also inside the signature, so a changed
624
1053
  * deadline fails to verify.
625
1054
  */
626
- deadlineAt: z6.number().int().positive()
1055
+ deadlineAt: z7.number().int().positive()
627
1056
  }).strict();
628
1057
  function signedBody(context, plaintext) {
629
1058
  return Buffer.from(
@@ -718,15 +1147,15 @@ async function open(input) {
718
1147
 
719
1148
  // src/signing.ts
720
1149
  import { createHash as createHash2 } from "crypto";
721
- import { z as z7 } from "zod";
1150
+ import { z as z8 } from "zod";
722
1151
  var MAX_CLOCK_SKEW_MS = 12e4;
723
- var RequestSignature = z7.object({
1152
+ var RequestSignature = z8.object({
724
1153
  /** Which runner is calling. The server looks up its pinned identity. */
725
- runnerId: z7.string().min(1),
1154
+ runnerId: z8.string().min(1),
726
1155
  /** Epoch ms, bounded by {@link MAX_CLOCK_SKEW_MS}. */
727
- issuedAt: z7.number().int().positive(),
1156
+ issuedAt: z8.number().int().positive(),
728
1157
  /** Base64url Ed25519 signature over {@link canonicalRequest}. */
729
- signature: z7.string().min(1)
1158
+ signature: z8.string().min(1)
730
1159
  }).strict();
731
1160
  function canonicalRequest(input) {
732
1161
  const digest = createHash2("sha256").update(input.body, "utf8").digest("hex");
@@ -748,6 +1177,21 @@ function signRequest(keys, input) {
748
1177
  signature: signWith(keys, canonicalRequest(input))
749
1178
  };
750
1179
  }
1180
+ function signSiteRequest(keys, input) {
1181
+ return signRequest(keys, {
1182
+ endpoint: siteEndpoint(input.endpoint),
1183
+ runnerId: input.siteId,
1184
+ issuedAt: input.issuedAt,
1185
+ body: input.body
1186
+ });
1187
+ }
1188
+ function verifySiteRequest(input) {
1189
+ return verifyRequest({
1190
+ ...input,
1191
+ endpoint: siteEndpoint(input.endpoint)
1192
+ });
1193
+ }
1194
+ var siteEndpoint = (endpoint) => `site/${endpoint}`;
751
1195
  function verifyRequest(input) {
752
1196
  const skew = input.maxSkewMs ?? MAX_CLOCK_SKEW_MS;
753
1197
  if (Math.abs(input.now - input.signature.issuedAt) > skew) return "stale";
@@ -764,7 +1208,119 @@ function verifyRequest(input) {
764
1208
  return ok ? null : "bad-signature";
765
1209
  }
766
1210
 
1211
+ // src/manifest.ts
1212
+ import { z as z9 } from "zod";
1213
+ var RESERVED_PURPOSE = "default";
1214
+ var RENDERABLE = /^[^\p{Cc}\p{Cf}\p{Cs}\p{Co}]+$/u;
1215
+ var renderable = (max, what) => z9.string().min(1).max(max).regex(
1216
+ RENDERABLE,
1217
+ `a ${what} is text a person reads \u2014 no control characters, direction overrides or zero-width padding`
1218
+ ).refine((value) => value.trim() !== "", {
1219
+ message: `a ${what} cannot be blank`
1220
+ });
1221
+ var PurposeKey = z9.string().regex(
1222
+ /^[a-z0-9][a-z0-9-]*$/,
1223
+ "a purpose key is a lowercase slug \u2014 letters, digits and hyphens"
1224
+ ).max(64);
1225
+ var Purpose = z9.object({
1226
+ /**
1227
+ * What a person reads on the consent screen. The only rendered field.
1228
+ *
1229
+ * Declared rather than derived from the key, because a key is a
1230
+ * compromise between machines and this is not. "Writing Assistant" is
1231
+ * what somebody understands; `writing-assistant` is what travels.
1232
+ */
1233
+ label: renderable(80, "label"),
1234
+ /** One line of context for the consent screen. Optional. */
1235
+ description: renderable(280, "description").optional(),
1236
+ /**
1237
+ * The kinds this purpose uses.
1238
+ *
1239
+ * A purpose may span kinds, and a mapping is per (purpose, kind) — so a
1240
+ * person can send this purpose's chat to one service and its generation
1241
+ * to another. Listing a kind here is what makes that slot appear.
1242
+ */
1243
+ kinds: z9.array(JobKind).min(1).max(JOB_KINDS.length).refine((kinds) => new Set(kinds).size === kinds.length, {
1244
+ message: "a purpose lists each kind once"
1245
+ })
1246
+ }).strict();
1247
+ var MAX_PURPOSES = 32;
1248
+ var Manifest = z9.record(PurposeKey, Purpose).refine((manifest) => Object.keys(manifest).length > 0, {
1249
+ message: "a manifest declares at least one purpose"
1250
+ }).refine((manifest) => Object.keys(manifest).length <= MAX_PURPOSES, {
1251
+ 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`
1252
+ }).refine((manifest) => !(RESERVED_PURPOSE in manifest), {
1253
+ 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`
1254
+ });
1255
+ function singlePurposeManifest(input) {
1256
+ return {
1257
+ [RESERVED_PURPOSE]: { label: input.label, kinds: [...input.kinds] }
1258
+ };
1259
+ }
1260
+
1261
+ // src/succession.ts
1262
+ import { z as z10 } from "zod";
1263
+ var SUCCESSION_CONTEXT = "byollm/v1/site-succession";
1264
+ var RETIREMENT_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
1265
+ var MAX_SUCCESSION_CHAIN = 64;
1266
+ var Succession = z10.object({
1267
+ /**
1268
+ * The predecessor's public identity — K1, in full.
1269
+ *
1270
+ * The whole identity rather than the key id, because a daemon meeting a
1271
+ * chain it has not seen before has to *verify* each link, and a key id is
1272
+ * a fingerprint: enough to compare, never enough to check a signature.
1273
+ */
1274
+ identity: PublicIdentity,
1275
+ /** K1's signature over the statement naming K1 and its successor. */
1276
+ signature: z10.string().min(1)
1277
+ }).strict();
1278
+ function successionStatement(fromKeyId, toKeyId) {
1279
+ return Buffer.from(`${SUCCESSION_CONTEXT}:${fromKeyId}:${toKeyId}`);
1280
+ }
1281
+ function signSuccession(previous, next) {
1282
+ return {
1283
+ identity: {
1284
+ identity: previous.identityPublic,
1285
+ encryption: previous.encryptionPublic,
1286
+ encryptionSig: previous.encryptionSig
1287
+ },
1288
+ signature: signWith(
1289
+ previous,
1290
+ successionStatement(keyId(previous.identityPublic), keyId(next.identity))
1291
+ )
1292
+ };
1293
+ }
1294
+ function verifyLink(link, toKeyId) {
1295
+ if (!verifyPublicIdentity(link.identity)) return false;
1296
+ return verifyWith(
1297
+ link.identity.identity,
1298
+ successionStatement(keyId(link.identity.identity), toKeyId),
1299
+ link.signature
1300
+ );
1301
+ }
1302
+ function walkSuccession(input) {
1303
+ const { current, chain, approved } = input;
1304
+ if (chain.length === 0) return { path: [current], failure: "no-chain" };
1305
+ if (chain.length > MAX_SUCCESSION_CHAIN)
1306
+ return { path: [current], failure: "too-long" };
1307
+ const steps = [...chain].reverse();
1308
+ const path = [current];
1309
+ let succeeding = current;
1310
+ for (const link of steps) {
1311
+ if (!verifyLink(link, succeeding)) return { path, failure: "broken-link" };
1312
+ const previous = keyId(link.identity.identity);
1313
+ path.unshift(previous);
1314
+ if (approved(previous)) return { path, from: previous };
1315
+ succeeding = previous;
1316
+ }
1317
+ return { path, failure: "unknown-origin" };
1318
+ }
1319
+
767
1320
  // src/musts.ts
1321
+ function kindsOf(must2) {
1322
+ return typeof must2.verifiedBy === "string" ? [must2.verifiedBy] : must2.verifiedBy;
1323
+ }
768
1324
  var must = (m) => Object.freeze(m);
769
1325
  var MUSTS = Object.freeze({
770
1326
  // ---- Pairing and identity -------------------------------------------
@@ -797,6 +1353,50 @@ var MUSTS = Object.freeze({
797
1353
  verifiedBy: "conformance",
798
1354
  source: "byollm_009 \xA74"
799
1355
  }),
1356
+ SITE_KEY_BY_STUB: must({
1357
+ id: "SITE_KEY_BY_STUB",
1358
+ 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.",
1359
+ enforcedBy: "daemon",
1360
+ // Adversarial, and the reason is the finding that produced it: the
1361
+ // honest paths pass with every site check deleted, because `open`
1362
+ // refuses a signature from the wrong key anyway. What distinguishes an
1363
+ // enforced rule from a coincidence here is a hostile pairing of stub and
1364
+ // envelope, which no conformance client would ever send.
1365
+ verifiedBy: "adversarial",
1366
+ source: "byollm_009 \xA7A.3"
1367
+ }),
1368
+ SITES_LOCALLY_APPROVED: must({
1369
+ id: "SITES_LOCALLY_APPROVED",
1370
+ 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.",
1371
+ enforcedBy: "daemon",
1372
+ // Two kinds, and the second is the one that matters — V1-1.
1373
+ //
1374
+ // `construction`: the daemon cannot serve a site that is not in its
1375
+ // pinned map, and admission refuses before a payload is fetched — and
1376
+ // since byollm_016 Amendment K, being in the map is no longer sufficient
1377
+ // either: a signed grant is, and the relay proposing the set cannot
1378
+ // produce one.
1379
+ //
1380
+ // `adversarial`: the property that survives is about a *sequence* —
1381
+ // remove the id, re-offer it under a different key — which no honest
1382
+ // upstream sends and which the fence above does not see. That was the
1383
+ // bypass: the pin was deleted with the id, so the comparison had nothing
1384
+ // to compare against and the substitution arrived as a stranger.
1385
+ // **Not `conformance`, and that is a live gap rather than a judgement.**
1386
+ // Amendment C's succession clause is a rule about two implementations
1387
+ // agreeing, which is what a conformance check is for — but rotating a
1388
+ // site's key is not something `ConformanceTarget` can express, and adding
1389
+ // an optional hook that most targets omit would produce a check reporting
1390
+ // success for a reason unrelated to the property it claims. That is this
1391
+ // project's most-repeated bug, and it is not worth reintroducing for a
1392
+ // stronger-sounding word in a table. The rotation path is verified by
1393
+ // `site-rotation.test.ts` (both directions, against the shipped runner)
1394
+ // and `relay/test/rotation.test.ts` (both planes, against the reference
1395
+ // relay); the missing piece is a second *independent* implementation to
1396
+ // check them against, and there is not one yet.
1397
+ verifiedBy: ["construction", "adversarial"],
1398
+ source: "byollm_009 \xA7B.2, Amendment C"
1399
+ }),
800
1400
  KEYS_EXCHANGED_AT_CONSENT: must({
801
1401
  id: "KEYS_EXCHANGED_AT_CONSENT",
802
1402
  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.",
@@ -928,7 +1528,7 @@ var MUSTS = Object.freeze({
928
1528
  }),
929
1529
  NAMED_LOCAL_ALLOWLIST: must({
930
1530
  id: "NAMED_LOCAL_ALLOWLIST",
931
- 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.",
1531
+ 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.",
932
1532
  enforcedBy: "daemon",
933
1533
  verifiedBy: "conformance",
934
1534
  source: "byollm_001 Rev 1 \xA7B"
@@ -984,12 +1584,12 @@ var MUSTS = Object.freeze({
984
1584
  verifiedBy: "conformance",
985
1585
  source: "byollm_001 \xA7Endpoints.4"
986
1586
  }),
987
- RESULT_PROVENANCE: must({
988
- id: "RESULT_PROVENANCE",
989
- 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.",
1587
+ PROVENANCE_NAMES_DEVICE: must({
1588
+ id: "PROVENANCE_NAMES_DEVICE",
1589
+ 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.",
990
1590
  enforcedBy: "server",
991
1591
  verifiedBy: "conformance",
992
- source: "byollm_003 Rev 1 \xA7Return-trip"
1592
+ source: "byollm_009 \xA711"
993
1593
  }),
994
1594
  // ---- The trust surface -------------------------------------------------
995
1595
  INGRESS_LOGGED_BEFORE_EXECUTION: must({
@@ -1007,12 +1607,33 @@ var MUSTS = Object.freeze({
1007
1607
  verifiedBy: "adversarial",
1008
1608
  source: "byollm_004 \xA72"
1009
1609
  }),
1610
+ /**
1611
+ * Amended for byollm_016 Phase B, and the amendment is deliberately narrow.
1612
+ *
1613
+ * A site may now name a **service** on the stub. The temptation is to read
1614
+ * that as a crack in this law, so the statement below says exactly where the
1615
+ * line is: a name selects from a menu the owner published, and resolves to a
1616
+ * model, backend, base URL and flags **only** through that owner's own
1617
+ * config. The site supplies a key; the owner supplies every value it maps
1618
+ * to. A name the owner does not advertise is refused rather than
1619
+ * substituted, because substitution is how "you may pick from my list" turns
1620
+ * into "you may ask for anything and get something".
1621
+ *
1622
+ * Two properties keep it from drifting into "sites demand models":
1623
+ *
1624
+ * 1. **Nothing the site sends is ever a value.** No model string, no URL,
1625
+ * no flag crosses the wire — only a key that means nothing off this
1626
+ * owner's machine.
1627
+ * 2. **It is a stub field, never a payload field.** The prompt cannot
1628
+ * reach it. That is unchanged and is the sentence the second clause
1629
+ * below still enforces verbatim.
1630
+ */
1010
1631
  NO_PAYLOAD_ROUTING: must({
1011
1632
  id: "NO_PAYLOAD_ROUTING",
1012
- statement: "Model, backend, base URL, and flags MUST come from owner config only; a payload MUST NOT influence any of them.",
1633
+ 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.",
1013
1634
  enforcedBy: "daemon",
1014
1635
  verifiedBy: "adversarial",
1015
- source: "byollm_004 \xA72"
1636
+ source: "byollm_004 \xA72, amended byollm_016 \xA7Phase B"
1016
1637
  }),
1017
1638
  STRIPPED_CHILD_ENV: must({
1018
1639
  id: "STRIPPED_CHILD_ENV",
@@ -1041,26 +1662,117 @@ var MUSTS = Object.freeze({
1041
1662
  enforcedBy: "daemon",
1042
1663
  verifiedBy: "adversarial",
1043
1664
  source: "byollm_004 \xA74"
1665
+ }),
1666
+ REVOCATION_IMMEDIATE: must({
1667
+ id: "REVOCATION_IMMEDIATE",
1668
+ 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.",
1669
+ // Both, and stated as one sentence with two obligations rather than
1670
+ // folded into REVOCATION_HONORED. That one binds the *daemon*: a revoked
1671
+ // daemon stops claiming and abandons in-flight work. This binds the
1672
+ // *upstream*. byollm_009 §5 is explicit that the pair is the point — "a
1673
+ // revocation enforced at one end survives a compromise of that end" — and
1674
+ // one entry covering both would make a compromised daemon look compliant.
1675
+ enforcedBy: "both",
1676
+ verifiedBy: "conformance",
1677
+ source: "byollm_009 \xA711"
1678
+ }),
1679
+ CONSENT_BEFORE_ROUTE: must({
1680
+ id: "CONSENT_BEFORE_ROUTE",
1681
+ 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.",
1682
+ enforcedBy: "server",
1683
+ verifiedBy: "conformance",
1684
+ source: "byollm_009 \xA711"
1685
+ }),
1686
+ ROSTER_NOT_DISCLOSED: must({
1687
+ id: "ROSTER_NOT_DISCLOSED",
1688
+ 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.",
1689
+ // Checkable since cloud_008 §0.2 took `audienceAllow` off the stub: the
1690
+ // property now holds by *absence*, and absence is exactly what a strict
1691
+ // schema and a serialised stub can be asked about. Before that it was a
1692
+ // sentence — and one this project cited in code comments, tests and two
1693
+ // specs as though it were enforced data, which is why it is worth
1694
+ // stating precisely rather than generously.
1695
+ enforcedBy: "both",
1696
+ verifiedBy: "conformance",
1697
+ source: "byollm_009 \xA711"
1698
+ }),
1699
+ EFFECTIVE_OFFER_ONLY: must({
1700
+ id: "EFFECTIVE_OFFER_ONLY",
1701
+ 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.",
1702
+ enforcedBy: "both",
1703
+ verifiedBy: "conformance",
1704
+ source: "byollm_009 \xA711"
1705
+ }),
1706
+ FALLBACK_LABELED: must({
1707
+ id: "FALLBACK_LABELED",
1708
+ 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.",
1709
+ // `construction` today, and deliberately not `conformance`. Nothing on
1710
+ // the wire yet distinguishes a fallback from any other community job —
1711
+ // the ledger that would give it a surface is unbuilt — so a check would
1712
+ // have to assert something it cannot observe. Promoted the day that
1713
+ // surface exists. Marking it `conformance` now would put "verified"
1714
+ // beside a property no third party can see, which is the one thing the
1715
+ // kinds exist to prevent.
1716
+ enforcedBy: "both",
1717
+ verifiedBy: "construction",
1718
+ source: "byollm_009 \xA711"
1719
+ }),
1720
+ RELAY_BLIND: must({
1721
+ id: "RELAY_BLIND",
1722
+ statement: "A relay MUST NOT hold any key capable of decrypting a payload, a result, or a delta frame.",
1723
+ // Operator: a third party can read the relay's types and see there is
1724
+ // nowhere to put such a key, but the kit certifies a *server* and cannot
1725
+ // reach inside somebody's deployment to prove what it holds.
1726
+ enforcedBy: "server",
1727
+ verifiedBy: "operator",
1728
+ source: "byollm_009 \xA711"
1729
+ }),
1730
+ SHARED_COMPUTE_DISCLOSED: must({
1731
+ id: "SHARED_COMPUTE_DISCLOSED",
1732
+ 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.",
1733
+ // Operator, and cloud_008 §0.3 is why the classification now comes with a
1734
+ // standing answer rather than a standing question. The screen is not
1735
+ // wire-observable, but the *string the server composes* is, and it is
1736
+ // now unit-tested with the two false sentences forbidden by name. The
1737
+ // kind stays `operator` because a third-party site can still render
1738
+ // whatever it likes; what changed is that the part inside our own
1739
+ // boundary stopped depending on somebody remembering to audit it.
1740
+ enforcedBy: "server",
1741
+ verifiedBy: "operator",
1742
+ source: "byollm_009 \xA711"
1044
1743
  })
1045
1744
  });
1745
+ var RETIRED_MUSTS = Object.freeze({
1746
+ RESULT_PROVENANCE: {
1747
+ supersededBy: "PROVENANCE_NAMES_DEVICE",
1748
+ 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."
1749
+ }
1750
+ });
1046
1751
  var MUST_IDS = Object.freeze(Object.keys(MUSTS));
1047
1752
  function mustsVerifiedBy(kind) {
1048
- return MUST_IDS.filter((id) => MUSTS[id].verifiedBy === kind);
1753
+ return MUST_IDS.filter((id) => kindsOf(MUSTS[id]).includes(kind));
1049
1754
  }
1050
1755
 
1051
1756
  // src/wire.ts
1052
- import { z as z8 } from "zod";
1053
- var PROTOCOL_VERSION = "0";
1757
+ import { z as z11 } from "zod";
1758
+ var PROTOCOL_VERSION = "1";
1054
1759
  var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze([
1055
1760
  PROTOCOL_VERSION
1056
1761
  ]);
1057
1762
  var MIN_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0] ?? PROTOCOL_VERSION;
1763
+ function declaredVersion(input) {
1764
+ const { body, query } = input;
1765
+ if (typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion")) {
1766
+ return body.protocolVersion;
1767
+ }
1768
+ return query?.get("protocolVersion") ?? void 0;
1769
+ }
1058
1770
  function checkProtocolVersion(body) {
1059
1771
  const declared = typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion") ? body.protocolVersion : void 0;
1060
1772
  if (typeof declared !== "string" || declared.length === 0) {
1061
1773
  return {
1062
1774
  error: "unsupported-protocol-version",
1063
- message: "this request declared no protocol version. Upgrade the daemon: `npm i -g byollm@alpha`.",
1775
+ message: `this request declared no protocol version. Upgrade the daemon: \`${UPGRADE_COMMAND}\`.`,
1064
1776
  supported: SUPPORTED_PROTOCOL_VERSIONS,
1065
1777
  minimum: MIN_PROTOCOL_VERSION
1066
1778
  };
@@ -1068,13 +1780,14 @@ function checkProtocolVersion(body) {
1068
1780
  if (!SUPPORTED_PROTOCOL_VERSIONS.includes(declared)) {
1069
1781
  return {
1070
1782
  error: "unsupported-protocol-version",
1071
- message: `this server speaks protocol ${SUPPORTED_PROTOCOL_VERSIONS.join(", ")} and the daemon asked for ${declared}. ` + (declared < MIN_PROTOCOL_VERSION ? "Upgrade the daemon: `npm i -g byollm@alpha`." : "This daemon is newer than the server; the server needs upgrading."),
1783
+ 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."),
1072
1784
  supported: SUPPORTED_PROTOCOL_VERSIONS,
1073
1785
  minimum: MIN_PROTOCOL_VERSION
1074
1786
  };
1075
1787
  }
1076
1788
  return null;
1077
1789
  }
1790
+ var UPGRADE_COMMAND = "npm i -g byollm@latest";
1078
1791
  var PROTOCOL_PREFIX = "/byollm";
1079
1792
  var ENDPOINTS = Object.freeze([
1080
1793
  "pair",
@@ -1084,23 +1797,65 @@ var ENDPOINTS = Object.freeze([
1084
1797
  "result",
1085
1798
  "release"
1086
1799
  ]);
1087
- var Capability = z8.object({
1800
+ var Capability = z11.object({
1088
1801
  kind: JobKind,
1802
+ /**
1803
+ * The owner's name for the service answering this kind — byollm_016.
1804
+ *
1805
+ * A device advertises *which* of its services serves a kind, not merely
1806
+ * that something does. **A site never sees this**, and never did after
1807
+ * Amendment L: it is what a control plane resolves a person's mapping
1808
+ * against, so that the service a grant names is one this device actually
1809
+ * offers rather than one somebody invented.
1810
+ *
1811
+ * `isDefault` used to sit beside it, saying which row an unselected job
1812
+ * took. Nothing selects any more — a job names a purpose and a person's
1813
+ * mapping names a service — so there is no unselected job for a default
1814
+ * to catch, and the field went with the machinery it served.
1815
+ */
1816
+ service: z11.string().min(1),
1089
1817
  backendId: BackendIdSchema,
1090
1818
  backendClass: BackendClass,
1091
- model: z8.string().min(1),
1819
+ model: z11.string().min(1),
1820
+ /**
1821
+ * Models this device's CLI knows about — byollm_017 ruling 3.
1822
+ *
1823
+ * **Suggestions, not a vocabulary.** Free text is always allowed: the
1824
+ * promise is that a model released this morning works this morning, and a
1825
+ * frozen list anywhere a person picks from breaks that on the first day
1826
+ * it matters. What makes free text safe is ruling 2 — a model is probed
1827
+ * before it is stored, so "found is not works" is answered by the device
1828
+ * rather than by a list.
1829
+ *
1830
+ * Announced with the capability rather than kept in the dashboard,
1831
+ * because the answer is "what does THIS device's CLI know" and only the
1832
+ * device can say. A list held cloud-side would be one more thing to
1833
+ * update on release day, and wrong for anybody who had not upgraded.
1834
+ *
1835
+ * Optional, and empty is legal. A backend with nothing to suggest — a
1836
+ * local server serving one model — is not a backend in an error state,
1837
+ * and a reader must not render an absent list as "no models available".
1838
+ */
1839
+ knownModels: z11.array(z11.string().min(1)).optional(),
1092
1840
  offerScope: OfferScope
1093
1841
  }).strict();
1094
- var CapabilityMatrix = z8.array(Capability);
1095
- var PairStartRequest = z8.object({
1096
- protocolVersion: z8.literal(PROTOCOL_VERSION),
1097
- action: z8.literal("start"),
1098
- daemon: z8.object({
1099
- version: z8.string().min(1),
1842
+ var CapabilityMatrix = z11.array(Capability);
1843
+ var WithheldKind = z11.object({
1844
+ kind: JobKind,
1845
+ claimants: z11.array(
1846
+ z11.object({ service: z11.string().min(1), offer: OfferScope }).strict()
1847
+ ).min(2)
1848
+ }).strict();
1849
+ var GrantRef = z11.object({ jobId: z11.string().min(1), leaseId: z11.string().min(1) }).strict();
1850
+ var PairStartRequest = z11.object({
1851
+ protocolVersion: z11.literal(PROTOCOL_VERSION),
1852
+ action: z11.literal("start"),
1853
+ daemon: z11.object({
1854
+ version: z11.string().min(1),
1100
1855
  /** Shown in the app's runner list so a user can tell their machines apart. */
1101
- label: z8.string().min(1).max(120),
1102
- platform: z8.enum(["darwin", "linux", "win32"])
1103
- }),
1856
+ label: z11.string().min(1).max(120),
1857
+ platform: z11.enum(["darwin", "linux", "win32"])
1858
+ }).strict(),
1104
1859
  /**
1105
1860
  * This machine's public keys (byollm_009 §5).
1106
1861
  *
@@ -1111,111 +1866,278 @@ var PairStartRequest = z8.object({
1111
1866
  device: PublicIdentity,
1112
1867
  capabilities: CapabilityMatrix
1113
1868
  }).strict();
1114
- var PairStartResponse = z8.object({
1869
+ var PairStartResponse = z11.object({
1115
1870
  /** Secret the daemon polls with. Never shown to the user. */
1116
- deviceCode: z8.string().min(20),
1871
+ deviceCode: z11.string().min(20),
1117
1872
  /** Short code the user reads and confirms in the browser. */
1118
- userCode: z8.string().min(4).max(16),
1873
+ userCode: z11.string().min(4).max(16),
1119
1874
  /** Where the user approves. Must be on the server's own origin. */
1120
- verificationUrl: z8.url(),
1875
+ verificationUrl: z11.url(),
1121
1876
  /** Epoch ms after which the code is dead ({@link MUSTS.PAIR_CODE_EXPIRES}). */
1122
- expiresAt: z8.number().int().positive(),
1877
+ expiresAt: z11.number().int().positive(),
1123
1878
  /** How often the daemon may poll. */
1124
- pollIntervalMs: z8.number().int().min(500).max(6e4)
1879
+ pollIntervalMs: z11.number().int().min(500).max(6e4)
1125
1880
  }).strict();
1126
- var PairPollRequest = z8.object({
1127
- protocolVersion: z8.literal(PROTOCOL_VERSION),
1128
- action: z8.literal("poll"),
1129
- deviceCode: z8.string().min(20)
1881
+ var PairPollRequest = z11.object({
1882
+ protocolVersion: z11.literal(PROTOCOL_VERSION),
1883
+ action: z11.literal("poll"),
1884
+ deviceCode: z11.string().min(20)
1130
1885
  }).strict();
1131
- var PairPollResponse = z8.discriminatedUnion("status", [
1132
- z8.object({ status: z8.literal("pending") }).strict(),
1133
- z8.object({ status: z8.literal("denied") }).strict(),
1134
- z8.object({ status: z8.literal("expired") }).strict(),
1135
- z8.object({
1136
- status: z8.literal("approved"),
1137
- /** Bearer token for every later call. Scoped to exactly one user. */
1138
- runnerToken: z8.string().min(20),
1139
- runnerId: z8.string().min(1),
1886
+ var PairPollResponse = z11.discriminatedUnion("status", [
1887
+ z11.object({ status: z11.literal("pending") }).strict(),
1888
+ z11.object({ status: z11.literal("denied") }).strict(),
1889
+ z11.object({ status: z11.literal("expired") }).strict(),
1890
+ z11.object({
1891
+ status: z11.literal("approved"),
1892
+ // `runnerToken` is gone cloud_008 §2.4, finding 37.
1893
+ //
1894
+ // It was minted here, hashed into `RunnerRecord.tokenHash`, written to
1895
+ // the daemon's pairings file, and then **never sent, never looked up
1896
+ // and never compared**. `getRunnerByTokenHash` existed on both stores
1897
+ // and was called by nothing but a test asserting it returns null.
1898
+ //
1899
+ // Not merely dead wire, which is what `audienceAllow` and
1900
+ // `HeartbeatResponse.leases` were. This was a *secret*: minted,
1901
+ // transmitted, and written to two disks at rest, for nothing. A
1902
+ // credential with no purpose is a liability rather than clutter,
1903
+ // because the only thing it can ever do is leak.
1904
+ //
1905
+ // `REQUESTS_SIGNED_NOT_BEARER` was already the rule and was already
1906
+ // enforced — every authenticated call is signed by the device's pinned
1907
+ // identity key. This removes the thing the MUST is named after.
1908
+ runnerId: z11.string().min(1),
1140
1909
  /** The app's id for the approving user — this daemon's owner forever. */
1141
- owner: z8.string().min(1),
1910
+ owner: z11.string().min(1),
1142
1911
  /** Display name for the trust UI, if the app offers one. */
1143
- ownerLabel: z8.string().optional(),
1912
+ ownerLabel: z11.string().optional(),
1144
1913
  /**
1145
- * The site's public keys, for the daemon to pin (byollm_009 §5).
1914
+ * The sites this pairing covers, for the daemon to pin (byollm_009 §5),
1915
+ * keyed by each site's identity key id — cloud_009 §5.
1146
1916
  *
1147
- * Returned only on approval a pending or denied poll learns nothing,
1917
+ * Returned only on approval: a pending or denied poll learns nothing,
1148
1918
  * so an unapproved code cannot be used to enumerate a site's keys.
1919
+ *
1920
+ * **One pairing per upstream, not one per site.** A user who connects a
1921
+ * site on a web dashboard has no reason to go back to a laptop and run
1922
+ * a command, so which sites a pairing covers is a projection of consent
1923
+ * — refreshed on the heartbeat — rather than something frozen at
1924
+ * pairing. A direct site answers with exactly one entry, which is the
1925
+ * same shape and not a special case.
1926
+ *
1927
+ * Keyed by the id `stub.site` carries (Amendment A §A.3), so the
1928
+ * runner's lookup is a map read rather than a join across two
1929
+ * namespaces.
1149
1930
  */
1150
- site: PublicIdentity
1931
+ sites: z11.record(z11.string().min(1), PublicIdentity),
1932
+ /**
1933
+ * The control plane's grant-signing key, pinned here — Amendment J.
1934
+ *
1935
+ * **Pairing is when, and that is the whole question.** Pairing is
1936
+ * already the ceremony where an owner proves out of band that this
1937
+ * device is theirs, so a key learned here rides trust that has already
1938
+ * happened. The rejected alternative is trust-on-first-grant, and it is
1939
+ * rejected because it hands the decision back to the relay: a daemon
1940
+ * that learns whose signature to trust from the first grant to arrive
1941
+ * has its admission authority chosen by whoever controls delivery.
1942
+ *
1943
+ * Optional on the wire, and only on the wire: a direct-mode server has
1944
+ * no control plane and signs nothing, and a daemon that receives no key
1945
+ * serves its owner alone. It is not optional for a relay with a control
1946
+ * plane — one that omitted it would be asking devices to accept grants
1947
+ * from nobody in particular, and would find every job refused.
1948
+ *
1949
+ * Rotation is Amendment C's, with no path where a grant teaches a
1950
+ * daemon a new key.
1951
+ */
1952
+ controlPlanePublic: z11.string().min(1).optional()
1151
1953
  }).strict()
1152
1954
  ]);
1153
- var PairRequest = z8.discriminatedUnion("action", [
1955
+ var PairRequest = z11.discriminatedUnion("action", [
1154
1956
  PairStartRequest,
1155
1957
  PairPollRequest
1156
1958
  ]);
1157
- var ClaimRequest = z8.object({
1158
- protocolVersion: z8.literal(PROTOCOL_VERSION),
1159
- runnerId: z8.string().min(1),
1959
+ var ClaimRequest = z11.object({
1960
+ protocolVersion: z11.literal(PROTOCOL_VERSION),
1961
+ runnerId: z11.string().min(1),
1160
1962
  /** Re-sent on every claim so a server never matches against a stale matrix. */
1161
1963
  capabilities: CapabilityMatrix,
1162
1964
  /** Upper bound on jobs to return; the server may return fewer. */
1163
- max: z8.number().int().min(1).max(64)
1965
+ max: z11.number().int().min(1).max(64)
1164
1966
  }).strict();
1165
- var ClaimResponse = z8.object({
1967
+ var ClaimResponse = z11.object({
1166
1968
  /**
1167
1969
  * Stubs, not jobs. The payload arrives from `fetch`, sealed to whichever
1168
1970
  * device claimed — see {@link JobStub} for the exhaustive metadata list.
1169
1971
  */
1170
- jobs: z8.array(ClaimedStub),
1972
+ jobs: z11.array(ClaimedStub),
1171
1973
  /** Lease duration granted, so the daemon knows its renewal deadline. */
1172
- leaseMs: z8.number().int().positive()
1974
+ leaseMs: z11.number().int().positive()
1173
1975
  }).strict();
1174
- var HeartbeatRequest = z8.object({
1175
- protocolVersion: z8.literal(PROTOCOL_VERSION),
1176
- runnerId: z8.string().min(1),
1177
- daemonVersion: z8.string().min(1),
1976
+ var HeartbeatRequest = z11.object({
1977
+ protocolVersion: z11.literal(PROTOCOL_VERSION),
1978
+ runnerId: z11.string().min(1),
1979
+ daemonVersion: z11.string().min(1),
1178
1980
  capabilities: CapabilityMatrix,
1981
+ /**
1982
+ * Kinds this device is withholding, and why it can be said.
1983
+ *
1984
+ * Optional so a daemon that has nothing withheld sends nothing, and so an
1985
+ * older daemon against a newer hub is simply a device with no withheld
1986
+ * kinds rather than a parse failure.
1987
+ */
1988
+ withheld: z11.array(WithheldKind).default([]),
1179
1989
  /**
1180
1990
  * Leases this daemon believes it holds; the server renews exactly these.
1181
1991
  *
1182
1992
  * Lease ids rather than job ids, so a replayed heartbeat cannot renew a
1183
1993
  * grant the runner no longer holds — see {@link Lease.id}.
1184
1994
  */
1185
- activeLeases: z8.array(
1186
- z8.object({ jobId: z8.string().min(1), leaseId: z8.string().min(1) })
1187
- ),
1995
+ activeLeases: z11.array(GrantRef),
1188
1996
  /** True while the owner has the daemon paused; the server stops offering work. */
1189
- paused: z8.boolean()
1997
+ paused: z11.boolean()
1190
1998
  }).strict();
1191
- var HeartbeatResponse = z8.object({
1192
- /** Once true, the daemon stops claiming and abandons in-flight work. */
1193
- revoked: z8.boolean(),
1999
+ var HeartbeatResponse = z11.object({
2000
+ /**
2001
+ * The sites this daemon may serve, right now — cloud_008 finding 59.
2002
+ *
2003
+ * Revocation used to be a boolean, and it was device-wide: the daemon
2004
+ * plane refused every call when the (owner, hub-site) consent was gone,
2005
+ * heartbeat answered `revoked: true` with `lost: all`, and the daemon
2006
+ * dropped its whole pairing by origin. Under a hub that is one site's
2007
+ * revocation ending a machine's relationship with every other site it
2008
+ * served — the amplification finding 48 warned about, arriving through
2009
+ * the one field nobody thought of as tenancy.
2010
+ *
2011
+ * So the answer is the set. A site that leaves it is revoked *for that
2012
+ * site*: the daemon drops that pin and keeps the rest. An empty set is
2013
+ * what "revoked" used to mean, and the daemon can see that for itself
2014
+ * rather than being told a second time — two fields for one fact is how
2015
+ * they drift.
2016
+ */
2017
+ sites: z11.record(z11.string().min(1), PublicIdentity),
2018
+ /**
2019
+ * How a site's current key traces back to one this daemon already holds —
2020
+ * byollm_009 Amendment C.
2021
+ *
2022
+ * Keyed by the same id as `sites`, and **additive on purpose**: `sites`
2023
+ * remains the one statement of which key is current, and this says only
2024
+ * how that key got there. Two fields for one fact is how they drift; this
2025
+ * is two facts, and the second is evidence about the first.
2026
+ *
2027
+ * Optional because a site that has never rotated has no chain, which is
2028
+ * every site today. A daemon that receives one for an id it already holds
2029
+ * ignores it: the pin it has is the pin it approved.
2030
+ *
2031
+ * §12 carries what this adds to the metadata surface — a site's rotation
2032
+ * history is public by construction, because a daemon that cannot read it
2033
+ * cannot verify it.
2034
+ */
2035
+ successions: z11.record(
2036
+ z11.string().min(1),
2037
+ z11.object({
2038
+ /** Oldest last, as the projection carries it. */
2039
+ succeeds: z11.array(Succession).max(MAX_SUCCESSION_CHAIN),
2040
+ /**
2041
+ * Until when the superseded key may still sign work — epoch ms.
2042
+ *
2043
+ * The daemon holds its own clock against this, for the reason it
2044
+ * holds its own allowlist: a projection that could extend the
2045
+ * window indefinitely would be a two-key site forever, decided by
2046
+ * the party this design does not trust.
2047
+ */
2048
+ retiringUntil: z11.number().int().positive().optional()
2049
+ }).strict()
2050
+ ).optional(),
1194
2051
  /**
1195
2052
  * Per-job cancel (byollm_001 Rev 1 §C). The daemon aborts these jobs'
1196
2053
  * in-flight backend calls and reports them `canceled`.
2054
+ *
2055
+ * **The grant, not the id** — V1-3. Job ids are chosen per site, so two
2056
+ * sites may pick the same one, and a bare id told a daemon holding both
2057
+ * to abort whichever it happened to have filed under that name. The lease
2058
+ * is the unique grant and the daemon already keys its work by it; this is
2059
+ * the same shape `activeLeases` sends in the other direction.
1197
2060
  */
1198
- cancel: z8.array(z8.string().min(1)),
1199
- /** Jobs whose leases were renewed, with their new expiry. */
1200
- leases: z8.array(
1201
- z8.object({
1202
- jobId: z8.string().min(1),
1203
- expiresAt: z8.number().int().positive()
1204
- }).strict()
2061
+ cancel: z11.array(
2062
+ z11.object({ jobId: z11.string().min(1), leaseId: z11.string().min(1) }).strict()
1205
2063
  ),
2064
+ // `leases` is deliberately absent — cloud_008 §1.4b, finding 16.
2065
+ //
2066
+ // It carried "these leases were renewed, and here is the new expiry", and
2067
+ // **no daemon ever read it.** A mutation returning an empty list while
2068
+ // renewing correctly survived every test, which is what made it visible.
2069
+ //
2070
+ // It is neither a class nor membership, so Amendment A's rule does not
2071
+ // decide it — the older test does: nothing reads it, so it is dead wire.
2072
+ // §6's exhaustiveness is a commitment about what an upstream can see, and
2073
+ // it applies to every message rather than only to the stub.
2074
+ //
2075
+ // `lost` is the actionable signal and always was: a daemon stops work on
2076
+ // a lease it no longer holds. "Renewed" was the same question answered a
2077
+ // second time, and a second answer can only agree or contradict.
2078
+ //
2079
+ // Renewal itself is untouched — the upstream still extends the grants a
2080
+ // heartbeat names, which is what §0.6 fixed. What ended is telling the
2081
+ // daemon about it in a field it ignored. If an upstream ever needs to
2082
+ // push lease decisions, that is a new field with a reader, added on
2083
+ // purpose.
1206
2084
  /**
1207
2085
  * Jobs the daemon thinks it holds but the server has reassigned or
1208
2086
  * expired. The daemon must stop work on these and not report results.
2087
+ *
2088
+ * Named by grant rather than by id, for V1-3's reason: a bare id is
2089
+ * ambiguous across sites, and "the lease you no longer hold" is exactly
2090
+ * what this field means anyway.
1209
2091
  */
1210
- lost: z8.array(z8.string().min(1)),
2092
+ lost: z11.array(
2093
+ z11.object({ jobId: z11.string().min(1), leaseId: z11.string().min(1) }).strict()
2094
+ ),
1211
2095
  /** Server clock, so a daemon with a skewed clock still honors leases. */
1212
- serverTime: z8.number().int().positive()
2096
+ serverTime: z11.number().int().positive(),
2097
+ /**
2098
+ * Sites whose disclosure the user must read again before work moves —
2099
+ * cloud_008 finding 48, named rather than counted.
2100
+ *
2101
+ * A **subset of `sites`**, deliberately: a paused site keeps its pin, so
2102
+ * re-consenting never costs a re-pair. The daemon can say which site is
2103
+ * waiting and the user can go and read it, which is the difference
2104
+ * between a machine that is quietly idle and one that says why.
2105
+ *
2106
+ * Not `revoked`, which is a human ending a relationship, and not
2107
+ * `paused`, which on the request side already means "this daemon's
2108
+ * operator stopped it" — one word with two subjects on two halves of one
2109
+ * exchange is a confusion nobody untangles from a log.
2110
+ */
2111
+ awaitingConsent: z11.array(z11.string().min(1))
1213
2112
  }).strict();
1214
- var ResultDisposition = z8.enum(["ok", "error", "canceled"]);
1215
- var ResultRequest = z8.object({
1216
- protocolVersion: z8.literal(PROTOCOL_VERSION),
1217
- runnerId: z8.string().min(1),
1218
- jobId: z8.string().min(1),
2113
+ var ResultDisposition = z11.enum(["ok", "error", "canceled"]);
2114
+ var ResultRequest = z11.object({
2115
+ protocolVersion: z11.literal(PROTOCOL_VERSION),
2116
+ runnerId: z11.string().min(1),
2117
+ jobId: z11.string().min(1),
2118
+ /**
2119
+ * The grant this result was produced under — cloud_008 §1.4a.
2120
+ *
2121
+ * `fetch` has always named its lease, with the reasoning written beside
2122
+ * it: a request that names only the job would be answerable for whatever
2123
+ * lease exists when it arrives. **The operation that writes the result did
2124
+ * not**, on either plane, and checked only the runner id — which survives
2125
+ * a claim-release-reclaim cycle, so a device whose grant had been swept
2126
+ * and reissued could still land a result for a job it no longer held.
2127
+ *
2128
+ * Found by tracing a mutation that survived in §0.6: the lease lapsed, the
2129
+ * sweep requeued, the daemon re-claimed under a new grant, and the
2130
+ * original run finished and posted anyway. The relay marked the job done
2131
+ * with a result the site cannot open — it verifies the envelope against
2132
+ * the *current* holder's device, so the crypto contains the substitution —
2133
+ * and then refused the real holder's result as a replay. A lost job, in
2134
+ * silence.
2135
+ *
2136
+ * `LEASE_HONORED` is a statement about a lease *instance*. That was
2137
+ * learned once already, when a replayed release yanked a later grant, and
2138
+ * it applies here for the same reason.
2139
+ */
2140
+ leaseId: z11.string().min(1),
1219
2141
  /**
1220
2142
  * The outcome, sealed to the site and signed by the device.
1221
2143
  *
@@ -1231,26 +2153,48 @@ var ResultRequest = z8.object({
1231
2153
  * fact: believing it unverified would let a daemon mark a job `ok` while
1232
2154
  * sealing an error, and only the app would ever find out.
1233
2155
  */
1234
- disposition: ResultDisposition,
1235
- /** Which model actually served it, for the result's provenance. */
1236
- model: z8.string().min(1),
1237
- backendClass: BackendClass,
1238
- /** Wall-clock milliseconds the backend call took. */
1239
- durationMs: z8.number().int().nonnegative()
2156
+ disposition: ResultDisposition
2157
+ // `model`, `backendClass` and `durationMs` are **inside the envelope**
2158
+ // cloud_008 §2.5. See {@link RunMetadata}.
2159
+ //
2160
+ // They were here, in the clear, and that was two problems wearing one
2161
+ // coat. On the direct plane the site recorded unauthenticated fields
2162
+ // beside an authenticated answer: a daemon could seal one result and
2163
+ // declare a different model, and only the unsigned half would reach the
2164
+ // app. Through a relay they reached a third party that acts on none of
2165
+ // them — `model` in particular being the sort of detail Amendment A's
2166
+ // rule keeps off the wire.
2167
+ //
2168
+ // `disposition` stays, and the difference is the test: a relay *routes*
2169
+ // on it, so it is a class a routing party consumes. Nobody between the
2170
+ // two ends consumes these.
1240
2171
  }).strict();
1241
- var ResultResponse = z8.object({
2172
+ var ResultResponse = z11.object({
1242
2173
  /**
1243
- * False when the submission lost an idempotency race or the lease was
1244
- * already gone the daemon should discard, not retry
1245
- * ({@link MUSTS.RESULT_IDEMPOTENT}).
2174
+ * False when this submission wrote nothing the daemon should discard,
2175
+ * not retry ({@link MUSTS.RESULT_IDEMPOTENT}).
1246
2176
  */
1247
- accepted: z8.boolean(),
2177
+ accepted: z11.boolean(),
2178
+ /**
2179
+ * True when this device had already recorded this job's result.
2180
+ *
2181
+ * The difference between "already recorded" and "you no longer hold this"
2182
+ * — cloud_008 §3.6. A daemon whose acknowledgment was lost is in the first
2183
+ * case and needs to hear it: its answer is safely on disk. Reporting a
2184
+ * stale lease instead invents a worry about a result that is already
2185
+ * stored, and sends its owner looking for a routing problem.
2186
+ *
2187
+ * Set only for the device that finished the job. A different device gets
2188
+ * the same refusal it would get for a job that is *not* terminal, so a job
2189
+ * id cannot be used as a terminality probe.
2190
+ */
2191
+ duplicate: z11.boolean().optional(),
1248
2192
  /** The job's state after this submission. */
1249
- state: z8.string().min(1)
2193
+ state: z11.string().min(1)
1250
2194
  }).strict();
1251
- var ReleaseRequest = z8.object({
1252
- protocolVersion: z8.literal(PROTOCOL_VERSION),
1253
- runnerId: z8.string().min(1),
2195
+ var ReleaseRequest = z11.object({
2196
+ protocolVersion: z11.literal(PROTOCOL_VERSION),
2197
+ runnerId: z11.string().min(1),
1254
2198
  /**
1255
2199
  * Which leases to release — the grant, not just the job.
1256
2200
  *
@@ -1258,51 +2202,162 @@ var ReleaseRequest = z8.object({
1258
2202
  * moment it arrives, which for a replayed request is not the lease the
1259
2203
  * daemon meant. See {@link Lease.id}.
1260
2204
  */
1261
- leases: z8.array(
1262
- z8.object({ jobId: z8.string().min(1), leaseId: z8.string().min(1) })
1263
- ),
2205
+ leases: z11.array(GrantRef),
1264
2206
  /**
1265
2207
  * Why, so the app's runner list can say something true.
1266
2208
  *
1267
- * `refused` is load-bearing, not cosmetic: the server cannot evaluate a
1268
- * daemon's *local* `named` allowlist (§4.2), so it may legitimately offer
2209
+ * `refused` is load-bearing, not cosmetic: the server cannot evaluate
2210
+ * what a device will admit (§4.2), so it may legitimately offer
1269
2211
  * a job this daemon then declines. The server MUST record the refusal and
1270
2212
  * stop offering that job to that runner, or the pair would spin between
1271
2213
  * claim and release forever.
1272
2214
  */
1273
- reason: z8.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
2215
+ reason: z11.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
1274
2216
  }).strict();
1275
- var ReleaseResponse = z8.object({
1276
- released: z8.array(z8.string().min(1))
2217
+ var ReleaseResponse = z11.object({
2218
+ released: z11.array(z11.string().min(1))
1277
2219
  }).strict();
1278
- var WireErrorCode = z8.enum([
2220
+ var WireErrorCode = z11.enum([
1279
2221
  "bad-request",
1280
2222
  "unsupported-protocol-version",
2223
+ // "We do not know who you are." Exactly 401, and only that — cloud_008
2224
+ // §1.4d.
1281
2225
  "unauthorized",
2226
+ /**
2227
+ * "We know exactly who you are, and the answer is no." Exactly 403.
2228
+ *
2229
+ * Five refusals across both planes served 403 with `unauthorized`, whose
2230
+ * table entry is 401: a revoked device, a site claiming another site's
2231
+ * stub, a job you do not hold, a device belonging to another owner, a
2232
+ * relay that does not route for you. Every one of them is an *identified*
2233
+ * caller being refused.
2234
+ *
2235
+ * Collapsing the two loses a distinction that matters everywhere it is
2236
+ * read: a revoked daemon would look like an unsigned one in every log and
2237
+ * every client branch, and "check your keys" is the wrong advice for both
2238
+ * of them in opposite directions.
2239
+ */
2240
+ "forbidden",
1282
2241
  "revoked",
1283
2242
  "not-found",
2243
+ // Claimed, but the site has not sealed the payload yet — cloud_008 §1.4.
2244
+ //
2245
+ // A daemon must retry rather than abandon: the job is legitimately still
2246
+ // its own until the lease or the awaiting-payload clock says otherwise.
2247
+ // That is why it cannot be `not-found` or `server-error`, and why it was
2248
+ // the protocol gap that produced a bare 409 in the first place.
2249
+ "not-ready",
2250
+ /**
2251
+ * The job is over, and this call is about a job — V1-6, and the code the
2252
+ * site plane has been serving without one (V1-13).
2253
+ *
2254
+ * Distinct from `not-found`, which says "no such job", and from
2255
+ * `not-ready`, which says "not yet, keep asking". This one says "yes, and
2256
+ * it finished" — so a daemon must stop rather than retry, and a replayed
2257
+ * request must not be able to reopen it.
2258
+ */
2259
+ "too-late",
2260
+ // The caller's clock is too far from ours to judge a signature's freshness.
2261
+ //
2262
+ // Split out from `unauthorized` because the remedy is completely different
2263
+ // and only the server can tell them apart: a bad signature means the key is
2264
+ // wrong, this means the machine's time is wrong. A daemon reporting it as a
2265
+ // generic rejection sends its owner looking at their network.
2266
+ "clock-skew",
1284
2267
  "rate-limited",
1285
2268
  "server-error"
1286
2269
  ]);
1287
- var WireError = z8.object({
2270
+ var WireError = z11.object({
1288
2271
  error: WireErrorCode,
1289
- message: z8.string().min(1),
2272
+ message: z11.string().min(1),
2273
+ /**
2274
+ * What this server speaks, on `unsupported-protocol-version` — §B.4.
2275
+ *
2276
+ * The refusal has carried these since the version handshake existed and
2277
+ * the enumeration did not model them, so the one error that exists to be
2278
+ * *acted on* was the one that failed to parse as a wire error. Found by
2279
+ * the relay's own suite the day the relay started sending it: a refusal
2280
+ * outside the enumerated shape is a refusal a client cannot branch on,
2281
+ * which is the whole reason §1.4 enumerates them.
2282
+ *
2283
+ * Modelled the way `clock-skew`'s two fields already are — code-specific
2284
+ * extras, refused on any other code by the refinement below.
2285
+ */
2286
+ supported: z11.array(z11.string().min(1)).optional(),
2287
+ minimum: z11.string().min(1).optional(),
1290
2288
  /** Seconds; mirrors Retry-After for `rate-limited` and `server-error`. */
1291
- retryAfter: z8.number().int().nonnegative().optional()
1292
- }).strict();
2289
+ retryAfter: z11.number().int().nonnegative().optional(),
2290
+ /**
2291
+ * The server's clock, and the window it allows. `clock-skew` only.
2292
+ *
2293
+ * So the far side can say *how far off* rather than *that something is
2294
+ * wrong* — the difference between "adjust your clock by four minutes" and
2295
+ * "something is wrong with your connection". Not a disclosure: the
2296
+ * heartbeat response returns the same value, and so does every `Date`
2297
+ * header.
2298
+ */
2299
+ serverTime: z11.number().int().positive().optional(),
2300
+ maxSkewMs: z11.number().int().positive().optional()
2301
+ }).strict().superRefine((error, ctx) => {
2302
+ const skew = error.error === "clock-skew";
2303
+ const carried = error.serverTime !== void 0 || error.maxSkewMs !== void 0;
2304
+ if (skew && !carried) {
2305
+ ctx.addIssue({
2306
+ code: "custom",
2307
+ message: "clock-skew must carry serverTime and maxSkewMs"
2308
+ });
2309
+ }
2310
+ if (!skew && carried) {
2311
+ ctx.addIssue({
2312
+ code: "custom",
2313
+ message: `${error.error} must not carry serverTime or maxSkewMs`
2314
+ });
2315
+ }
2316
+ const version = error.error === "unsupported-protocol-version";
2317
+ const versionFields = error.supported !== void 0 || error.minimum !== void 0;
2318
+ if (version && !versionFields) {
2319
+ ctx.addIssue({
2320
+ code: "custom",
2321
+ message: "unsupported-protocol-version must carry supported and minimum"
2322
+ });
2323
+ }
2324
+ if (!version && versionFields) {
2325
+ ctx.addIssue({
2326
+ code: "custom",
2327
+ message: `${error.error} must not carry supported or minimum`
2328
+ });
2329
+ }
2330
+ });
1293
2331
  var ERROR_STATUS = Object.freeze({
1294
2332
  "bad-request": 400,
1295
2333
  "unsupported-protocol-version": 400,
1296
2334
  unauthorized: 401,
2335
+ forbidden: 403,
1297
2336
  revoked: 403,
1298
2337
  "not-found": 404,
2338
+ // 409, not 404: the job exists and is yours, it is simply not ready.
2339
+ "not-ready": 409,
2340
+ // The same 409 as `not-ready` and the opposite instruction: that one says
2341
+ // keep asking, this one says stop. The status is the class of the
2342
+ // problem — a request that does not fit the resource's state — and the
2343
+ // code is what a caller acts on.
2344
+ "too-late": 409,
2345
+ // 401 alongside `unauthorized`, because that is what it is — the
2346
+ // signature could not be judged. The code is what carries the remedy.
2347
+ "clock-skew": 401,
1299
2348
  "rate-limited": 429,
1300
2349
  "server-error": 500
1301
2350
  });
1302
- var FetchRequest = z8.object({
1303
- protocolVersion: z8.string().min(1),
1304
- runnerId: z8.string().min(1),
1305
- jobId: z8.string().min(1),
2351
+ var FetchRequest = z11.object({
2352
+ // `literal`, like every other request — V1-17. This one said
2353
+ // `string().min(1)`, so a daemon speaking a version this server does not
2354
+ // know got past the handshake on the one endpoint that hands over a
2355
+ // sealed payload. The version check exists so that a mismatch is a named
2356
+ // refusal rather than a schema failure three fields later; here it was
2357
+ // neither.
2358
+ protocolVersion: z11.literal(PROTOCOL_VERSION),
2359
+ runnerId: z11.string().min(1),
2360
+ jobId: z11.string().min(1),
1306
2361
  /**
1307
2362
  * The grant this daemon holds.
1308
2363
  *
@@ -1310,9 +2365,9 @@ var FetchRequest = z8.object({
1310
2365
  * only the job would be answerable for whatever lease exists when it
1311
2366
  * arrives ({@link Lease.id}).
1312
2367
  */
1313
- leaseId: z8.string().min(1)
2368
+ leaseId: z11.string().min(1)
1314
2369
  }).strict();
1315
- var FetchResponse = z8.object({
2370
+ var FetchResponse = z11.object({
1316
2371
  /**
1317
2372
  * The work, sealed to the device that claimed it — byollm_009 §6.
1318
2373
  *
@@ -1324,13 +2379,20 @@ var FetchResponse = z8.object({
1324
2379
  envelope: SealedEnvelope
1325
2380
  }).strict();
1326
2381
  export {
2382
+ ABOUT,
2383
+ ABOUT_SHORT,
2384
+ ABOUT_SHORT_LEDE,
2385
+ ABOUT_SHORT_TAIL,
1327
2386
  AUDIENCES,
1328
2387
  Audience,
1329
2388
  BACKENDS,
2389
+ BACKEND_CLASSES,
1330
2390
  BACKEND_IDS,
1331
2391
  BackendClass,
1332
2392
  BackendCost,
1333
2393
  BackendIdSchema,
2394
+ CLOCK_ATTRIBUTION_MS,
2395
+ CLOCK_SKEW_WARN_MS,
1334
2396
  Capability,
1335
2397
  CapabilityMatrix,
1336
2398
  ChatMessage,
@@ -1340,19 +2402,25 @@ export {
1340
2402
  ClaimedJob,
1341
2403
  ClaimedStub,
1342
2404
  DeliveredResult,
2405
+ ENCRYPTION_KEY_CONTEXT,
1343
2406
  ENDPOINTS,
1344
2407
  ENVELOPE_MAX_AGE_MS,
1345
2408
  ERROR_STATUS,
1346
2409
  EnvelopeDirection,
1347
2410
  FetchRequest,
1348
2411
  FetchResponse,
2412
+ GRANT_CONTEXT,
2413
+ GRANT_MAX_AGE_MS,
2414
+ GRANT_SIGNED_FIELDS,
1349
2415
  GeneratePayload,
2416
+ GrantRef,
1350
2417
  HeartbeatRequest,
1351
2418
  HeartbeatResponse,
1352
2419
  JOB_KINDS,
1353
2420
  JobKind,
1354
2421
  JobOutcome,
1355
2422
  JobPayload,
2423
+ JobRefused,
1356
2424
  JobResultCanceled,
1357
2425
  JobResultError,
1358
2426
  JobResultOk,
@@ -1361,9 +2429,13 @@ export {
1361
2429
  KindedPayload,
1362
2430
  Lease,
1363
2431
  MAX_CLOCK_SKEW_MS,
2432
+ MAX_ENVELOPE_BYTES,
2433
+ MAX_PURPOSES,
2434
+ MAX_SUCCESSION_CHAIN,
1364
2435
  MIN_PROTOCOL_VERSION,
1365
2436
  MUSTS,
1366
2437
  MUST_IDS,
2438
+ Manifest,
1367
2439
  MatchRefusal,
1368
2440
  OFFER_SCOPES,
1369
2441
  OfferScope,
@@ -1376,7 +2448,11 @@ export {
1376
2448
  PairStartRequest,
1377
2449
  PairStartResponse,
1378
2450
  PublicIdentity,
2451
+ Purpose,
1379
2452
  REFUSAL_MESSAGES,
2453
+ RESERVED_PURPOSE,
2454
+ RETIREMENT_WINDOW_MS,
2455
+ RefusalReason,
1380
2456
  ReleaseRequest,
1381
2457
  ReleaseResponse,
1382
2458
  RequestSignature,
@@ -1384,27 +2460,41 @@ export {
1384
2460
  ResultProvenance,
1385
2461
  ResultRequest,
1386
2462
  ResultResponse,
2463
+ RunMetadata,
2464
+ SIZE_CLASSES,
1387
2465
  SIZE_CLASS_LIMITS,
2466
+ SUCCESSION_CONTEXT,
1388
2467
  SUPPORTED_PROTOCOL_VERSIONS,
1389
2468
  SealedEnvelope,
2469
+ SealedOutcome,
2470
+ SignedGrant,
1390
2471
  SizeClass,
1391
2472
  StoredKeys,
2473
+ Succession,
1392
2474
  TERMINAL_STATES,
1393
2475
  WireError,
1394
2476
  WireErrorCode,
2477
+ WithheldKind,
1395
2478
  backendDescriptor,
2479
+ backendName,
1396
2480
  canTransition,
1397
2481
  canonicalRequest,
1398
2482
  checkProtocolVersion,
2483
+ classifyCost,
1399
2484
  cryptoReady,
2485
+ declaredVersion,
1400
2486
  effectiveOfferScope,
2487
+ envelopeBytes,
1401
2488
  fingerprint,
1402
2489
  generateKeys,
2490
+ grantStatement,
1403
2491
  isBackendId,
2492
+ isCloudTaggedModel,
1404
2493
  isJobKind,
1405
2494
  isLocalHost,
1406
2495
  isTerminal,
1407
2496
  keyId,
2497
+ kindsOf,
1408
2498
  matchAudience,
1409
2499
  mustsVerifiedBy,
1410
2500
  open,
@@ -1413,12 +2503,21 @@ export {
1413
2503
  publicIdentityOf,
1414
2504
  resolveCost,
1415
2505
  seal,
2506
+ signGrant,
1416
2507
  signRequest,
2508
+ signSiteRequest,
2509
+ signSuccession,
1417
2510
  signWith,
2511
+ singlePurposeManifest,
1418
2512
  sizeClassCeiling,
1419
2513
  sizeClassOf,
2514
+ successionStatement,
2515
+ verifyGrant,
2516
+ verifyLink,
1420
2517
  verifyPublicIdentity,
1421
2518
  verifyRequest,
1422
- verifyWith
2519
+ verifySiteRequest,
2520
+ verifyWith,
2521
+ walkSuccession
1423
2522
  };
1424
2523
  //# sourceMappingURL=index.js.map