@byollm/protocol 0.1.0-alpha.2 → 0.1.0-alpha.20

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
@@ -2,23 +2,166 @@
2
2
  import { z as z2 } from "zod";
3
3
 
4
4
  // src/backends.ts
5
+ import { isIP } from "net";
5
6
  import { z } from "zod";
6
7
  var BackendClass = z.enum(["http", "process"]);
7
- var BackendAccount = z.enum(["open", "subscription"]);
8
+ var BackendCost = z.enum(["free", "metered", "subscription"]);
8
9
  var backend = (b) => Object.freeze(b);
9
10
  var BACKENDS = Object.freeze({
11
+ // -- free: local compute, costs electricity ------------------------------
12
+ ollama: backend({
13
+ id: "ollama",
14
+ label: "Ollama (local)",
15
+ class: "http",
16
+ cost: "free",
17
+ adversarialCorpus: "http",
18
+ defaultBaseUrl: "http://127.0.0.1:11434/v1"
19
+ }),
20
+ mlx: backend({
21
+ id: "mlx",
22
+ label: "MLX (mlx_lm.server, local)",
23
+ class: "http",
24
+ cost: "free",
25
+ adversarialCorpus: "http",
26
+ defaultBaseUrl: "http://127.0.0.1:8080/v1"
27
+ }),
28
+ llamacpp: backend({
29
+ id: "llamacpp",
30
+ label: "llama.cpp server (local)",
31
+ class: "http",
32
+ cost: "free",
33
+ adversarialCorpus: "http",
34
+ defaultBaseUrl: "http://127.0.0.1:8080/v1"
35
+ }),
36
+ vllm: backend({
37
+ id: "vllm",
38
+ label: "vLLM (local)",
39
+ class: "http",
40
+ cost: "free",
41
+ adversarialCorpus: "http",
42
+ defaultBaseUrl: "http://127.0.0.1:8000/v1"
43
+ }),
44
+ lmstudio: backend({
45
+ id: "lmstudio",
46
+ label: "LM Studio (local)",
47
+ class: "http",
48
+ cost: "free",
49
+ adversarialCorpus: "http",
50
+ defaultBaseUrl: "http://127.0.0.1:1234/v1"
51
+ }),
52
+ jan: backend({
53
+ id: "jan",
54
+ label: "Jan (local)",
55
+ class: "http",
56
+ cost: "free",
57
+ adversarialCorpus: "http",
58
+ defaultBaseUrl: "http://127.0.0.1:1337/v1"
59
+ }),
60
+ localai: backend({
61
+ id: "localai",
62
+ label: "LocalAI (local)",
63
+ class: "http",
64
+ cost: "free",
65
+ adversarialCorpus: "http",
66
+ defaultBaseUrl: "http://127.0.0.1:8080/v1"
67
+ }),
68
+ // -- metered: the owner's money, per token -------------------------------
69
+ /**
70
+ * Note the pair: `anthropic` and {@link BACKENDS."claude-cli"} reach the
71
+ * same vendor and land in different cost classes. That is not an
72
+ * inconsistency — it is the axis working. One bills a key per token, the
73
+ * other runs under a personal plan whose terms cover one person's work. Who
74
+ * pays and under what terms is the question; which company is not.
75
+ */
76
+ anthropic: backend({
77
+ id: "anthropic",
78
+ label: "Anthropic (your API key)",
79
+ class: "http",
80
+ cost: "metered",
81
+ adversarialCorpus: "http",
82
+ defaultBaseUrl: "https://api.anthropic.com/v1"
83
+ }),
84
+ openai: backend({
85
+ id: "openai",
86
+ label: "OpenAI (your API key)",
87
+ class: "http",
88
+ cost: "metered",
89
+ adversarialCorpus: "http",
90
+ defaultBaseUrl: "https://api.openai.com/v1"
91
+ }),
92
+ gemini: backend({
93
+ id: "gemini",
94
+ label: "Google Gemini (your API key)",
95
+ class: "http",
96
+ cost: "metered",
97
+ adversarialCorpus: "http",
98
+ defaultBaseUrl: "https://generativelanguage.googleapis.com/v1beta/openai"
99
+ }),
100
+ grok: backend({
101
+ id: "grok",
102
+ label: "xAI Grok (your API key)",
103
+ class: "http",
104
+ cost: "metered",
105
+ adversarialCorpus: "http",
106
+ defaultBaseUrl: "https://api.x.ai/v1"
107
+ }),
108
+ groq: backend({
109
+ id: "groq",
110
+ label: "Groq (your API key)",
111
+ class: "http",
112
+ cost: "metered",
113
+ adversarialCorpus: "http",
114
+ defaultBaseUrl: "https://api.groq.com/openai/v1"
115
+ }),
116
+ openrouter: backend({
117
+ id: "openrouter",
118
+ label: "OpenRouter (your API key)",
119
+ class: "http",
120
+ cost: "metered",
121
+ adversarialCorpus: "http",
122
+ defaultBaseUrl: "https://openrouter.ai/api/v1"
123
+ }),
124
+ together: backend({
125
+ id: "together",
126
+ label: "Together AI (your API key)",
127
+ class: "http",
128
+ cost: "metered",
129
+ adversarialCorpus: "http",
130
+ defaultBaseUrl: "https://api.together.xyz/v1"
131
+ }),
132
+ deepseek: backend({
133
+ id: "deepseek",
134
+ label: "DeepSeek (your API key)",
135
+ class: "http",
136
+ cost: "metered",
137
+ adversarialCorpus: "http",
138
+ defaultBaseUrl: "https://api.deepseek.com/v1"
139
+ }),
140
+ mistral: backend({
141
+ id: "mistral",
142
+ label: "Mistral (your API key)",
143
+ class: "http",
144
+ cost: "metered",
145
+ adversarialCorpus: "http",
146
+ defaultBaseUrl: "https://api.mistral.ai/v1"
147
+ }),
148
+ // -- the escape hatch ----------------------------------------------------
10
149
  "openai-http": backend({
11
150
  id: "openai-http",
12
- label: "OpenAI-compatible HTTP server (Ollama, MLX, llama.cpp, vLLM)",
151
+ label: "Any OpenAI-compatible server",
13
152
  class: "http",
14
- account: "open",
153
+ // Unknown until the base URL is known: local means free, remote means
154
+ // metered, and the owner does not get to say otherwise
155
+ // ({@link MUSTS.REMOTE_IS_NEVER_FREE}).
156
+ cost: null,
15
157
  adversarialCorpus: "http"
16
158
  }),
159
+ // -- subscription: someone else's terms ----------------------------------
17
160
  "claude-cli": backend({
18
161
  id: "claude-cli",
19
162
  label: "Claude CLI (your subscription)",
20
163
  class: "process",
21
- account: "subscription",
164
+ cost: "subscription",
22
165
  adversarialCorpus: "process"
23
166
  })
24
167
  });
@@ -32,6 +175,30 @@ function isBackendId(value) {
32
175
  function backendDescriptor(id) {
33
176
  return BACKENDS[id];
34
177
  }
178
+ function isLocalHost(hostname) {
179
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
180
+ if (host === "localhost" || host.endsWith(".localhost")) return true;
181
+ const version = isIP(host);
182
+ if (version === 0) return false;
183
+ if (version === 6) {
184
+ if (host === "::1") return true;
185
+ return /^f[cd]/.test(host);
186
+ }
187
+ if (host.startsWith("127.")) return true;
188
+ if (host.startsWith("10.")) return true;
189
+ if (host.startsWith("192.168.")) return true;
190
+ return /^172\.(1[6-9]|2\d|3[01])\./.test(host);
191
+ }
192
+ function resolveCost(id, baseUrl) {
193
+ const declared = BACKENDS[id].cost;
194
+ if (declared !== null) return declared;
195
+ if (baseUrl === void 0) return "metered";
196
+ try {
197
+ return isLocalHost(new URL(baseUrl).hostname) ? "free" : "metered";
198
+ } catch {
199
+ return "metered";
200
+ }
201
+ }
35
202
 
36
203
  // src/audience.ts
37
204
  var Audience = z2.enum(["self", "named", "public"]);
@@ -50,12 +217,18 @@ var MatchRefusal = z2.enum([
50
217
  /** The backend offers only `self` and the job belongs to someone else. */
51
218
  "offer-scope-too-narrow",
52
219
  /** The matched backend is subscription-class, which is locked to `self`. */
53
- "subscription-self-lock"
220
+ "subscription-self-lock",
221
+ /** The backend spends the owner's money and they have not agreed to share it. */
222
+ "metered-no-spend-consent",
223
+ /** The backend is shared but has spent its ceiling for now. */
224
+ "metered-ceiling-reached"
54
225
  ]);
55
226
  var ALLOWED = Object.freeze({ ok: true });
56
227
  var refuse = (refusal) => Object.freeze({ ok: false, refusal });
57
- function effectiveOfferScope(configured, account) {
58
- return account === "subscription" ? "self" : configured;
228
+ function effectiveOfferScope(configured, cost, spend) {
229
+ if (cost === "subscription") return "self";
230
+ if (cost === "metered" && spend?.acknowledged !== true) return "self";
231
+ return configured;
59
232
  }
60
233
  function matchAudience(job, daemon) {
61
234
  const sameOwner = job.owner === daemon.owner;
@@ -65,13 +238,25 @@ function matchAudience(job, daemon) {
65
238
  if (job.audience === "named" && !sameOwner && job.audienceAllow !== void 0 && !job.audienceAllow.includes(daemon.owner)) {
66
239
  return refuse("not-in-server-allowlist");
67
240
  }
68
- const scope = effectiveOfferScope(daemon.offerScope, daemon.account);
241
+ const scope = effectiveOfferScope(
242
+ daemon.offerScope,
243
+ daemon.cost,
244
+ daemon.spend
245
+ );
69
246
  if (sameOwner) {
70
247
  return ALLOWED;
71
248
  }
72
- if (daemon.account === "subscription") {
249
+ if (daemon.cost === "subscription") {
73
250
  return refuse("subscription-self-lock");
74
251
  }
252
+ if (daemon.cost === "metered") {
253
+ if (daemon.spend?.acknowledged !== true) {
254
+ return refuse("metered-no-spend-consent");
255
+ }
256
+ if (daemon.spend.ceilingReached === true) {
257
+ return refuse("metered-ceiling-reached");
258
+ }
259
+ }
75
260
  switch (scope) {
76
261
  case "self":
77
262
  return refuse("offer-scope-too-narrow");
@@ -87,7 +272,9 @@ var REFUSAL_MESSAGES = Object.freeze({
87
272
  "not-locally-allowed": "the job's owner is not on this machine's allowlist (byollm allow <server> <user>)",
88
273
  "not-in-server-allowlist": "the app restricted this job to named runners and this machine is not one of them",
89
274
  "offer-scope-too-narrow": "this backend is offered to its owner only (byollm offer <backend> named|public to widen)",
90
- "subscription-self-lock": "subscription-backed models run their owner's work only \u2014 this is a protocol rule, not a setting"
275
+ "subscription-self-lock": "subscription-backed models run their owner's work only \u2014 this is a protocol rule, not a setting",
276
+ "metered-no-spend-consent": "this backend bills its owner per token, and they have not agreed to spend it on other people's work",
277
+ "metered-ceiling-reached": "this backend is shared but has reached the spend ceiling its owner set"
91
278
  });
92
279
 
93
280
  // src/kinds.ts
@@ -167,6 +354,21 @@ function canTransition(from, to) {
167
354
  return TRANSITIONS[from].includes(to);
168
355
  }
169
356
  var Lease = z4.object({
357
+ /**
358
+ * Identifies *this* grant, not just its holder.
359
+ *
360
+ * A runner can hold a job, release it, and claim it again — three leases,
361
+ * one runner id. Without an id for the grant itself, a lease-scoped request
362
+ * names a mutable target ambiguously, and a replayed release from the first
363
+ * grant lands on the third: the job returns to the queue while the daemon
364
+ * is mid-execution, and the work runs twice on the owner's hardware.
365
+ *
366
+ * That was a live hole, found in review after signed requests shipped. The
367
+ * signature scheme's replay argument rests on endpoints being idempotent —
368
+ * and release *is*, per lease, but not across leases, because nothing in
369
+ * the request said which one.
370
+ */
371
+ id: z4.string().min(1),
170
372
  /** The runner holding the lease. */
171
373
  runnerId: z4.string().min(1),
172
374
  /** Epoch milliseconds after which the claim is void. */
@@ -180,8 +382,6 @@ var ClaimedJob = z4.object({
180
382
  audience: Audience,
181
383
  /** The app's id for the user who enqueued it. */
182
384
  owner: z4.string().min(1),
183
- /** Runner owners the app restricted a `named` job to, if any. */
184
- audienceAllow: z4.array(z4.string().min(1)).optional(),
185
385
  lease: Lease
186
386
  }).strict();
187
387
  var ResultProvenance = z4.object({
@@ -211,6 +411,13 @@ function provenanceFor(input) {
211
411
  untrusted: input.audience !== "self"
212
412
  };
213
413
  }
414
+ var RunMetadata = z4.object({
415
+ /** Which model actually served it. */
416
+ model: z4.string().min(1),
417
+ backendClass: BackendClass,
418
+ /** Wall-clock milliseconds the backend call took. */
419
+ durationMs: z4.number().int().nonnegative()
420
+ }).strict();
214
421
  var JobResultOk = z4.object({
215
422
  outcome: z4.literal("ok"),
216
423
  text: z4.string(),
@@ -232,12 +439,398 @@ var JobOutcome = z4.discriminatedUnion("outcome", [
232
439
  JobResultError,
233
440
  JobResultCanceled
234
441
  ]);
442
+ var SealedOutcome = z4.object({ outcome: JobOutcome, ran: RunMetadata }).strict();
235
443
  var DeliveredResult = z4.object({
236
444
  jobId: z4.string().min(1),
237
445
  state: JobState,
238
446
  outcome: JobOutcome.optional(),
239
447
  provenance: ResultProvenance.optional()
240
448
  }).strict();
449
+ var SizeClass = z4.enum(["small", "medium", "large", "unbounded"]);
450
+ var SIZE_CLASS_LIMITS = Object.freeze({
451
+ small: 4e3,
452
+ medium: 64e3,
453
+ large: Number.POSITIVE_INFINITY
454
+ });
455
+ function sizeClassCeiling(sizeClass) {
456
+ if (sizeClass === "unbounded") return Number.POSITIVE_INFINITY;
457
+ return SIZE_CLASS_LIMITS[sizeClass];
458
+ }
459
+ function sizeClassOf(textChars) {
460
+ if (textChars <= SIZE_CLASS_LIMITS.small) return "small";
461
+ if (textChars <= SIZE_CLASS_LIMITS.medium) return "medium";
462
+ return "large";
463
+ }
464
+ var JobStub = z4.object({
465
+ id: z4.string().min(1),
466
+ kind: JobKind,
467
+ /** The app's id for the user who enqueued it. */
468
+ owner: z4.string().min(1),
469
+ /**
470
+ * Which site this job belongs to — byollm_009 Amendment A §A.3.
471
+ *
472
+ * **The site's identity key id**, not an id somebody assigned it. §6 has
473
+ * listed `site` since this spec was frozen; the schema never carried it,
474
+ * which is the drift the amendment closes.
475
+ *
476
+ * A key id rather than an opaque handle for one reason above the others:
477
+ * it makes the stub *self-describing* instead of a pointer into somebody
478
+ * else's table. A daemon holds this key id already, from pinning, so it
479
+ * can check `stub.site` against the payload envelope's `senderKeyId`
480
+ * without a lookup and without trusting the party that routed it. An
481
+ * opaque id can only be believed.
482
+ *
483
+ * It also avoids inventing a second namespace for a thing that has a
484
+ * canonical one — the shape of finding 41 (two owner namespaces compared
485
+ * for equality) and of finding fourteen before it.
486
+ *
487
+ * Rotation is a designed transition rather than a cost: a site publishes a
488
+ * new identity signed by the outgoing one, both are valid through an
489
+ * overlap window, and a daemon re-keys its own map by verifying that
490
+ * signature against the key it already pinned (§A.3.1).
491
+ */
492
+ site: z4.string().min(1),
493
+ audience: Audience,
494
+ // `audienceAllow` is **not** here, and its absence is the enforcement —
495
+ // cloud_008 §0.2.
496
+ //
497
+ // It was a list of the people who may run a job, travelling to every
498
+ // routing party on every `named` job. byollm_001 Rev 1 §B settled who
499
+ // decides that long before this schema existed: *the daemon's own list
500
+ // decides, not the server's*, and `allowlist.predicateFor(origin)` is the
501
+ // enforcement in both lanes. So this was a second answer to a question the
502
+ // daemon already owned — able only to agree, in which case it was
503
+ // redundant, or to disagree, in which case nothing said which wins.
504
+ //
505
+ // The rule it leaves behind, which decides the next field too: **a class
506
+ // the router acts on may travel; membership never does.** `audience` stays
507
+ // for exactly that reason — the relay narrows on it. A roster does not
508
+ // travel, so `ROSTER_NOT_DISCLOSED` holds here by absence, which is the
509
+ // strongest way for a MUST to hold.
510
+ //
511
+ // The site keeps its own copy on `JobRecord` and still filters candidates
512
+ // with it before offering. That is server-internal, where the party
513
+ // holding the list authored it.
514
+ sizeClass: SizeClass,
515
+ /** Reserved for byollm_006. False until streaming exists. */
516
+ streaming: z4.boolean(),
517
+ /** Epoch ms after which the work is pointless; bounds ciphertext retention. */
518
+ deadlineAt: z4.number().int().positive()
519
+ }).strict();
520
+ var ClaimedStub = JobStub.extend({ lease: Lease }).strict();
521
+
522
+ // src/envelope.ts
523
+ import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2 } from "crypto";
524
+ import sodium from "libsodium-wrappers";
525
+ import { z as z6 } from "zod";
526
+
527
+ // src/keys.ts
528
+ import {
529
+ createHash,
530
+ createPrivateKey,
531
+ createPublicKey,
532
+ generateKeyPairSync,
533
+ sign,
534
+ verify
535
+ } from "crypto";
536
+ import { z as z5 } from "zod";
537
+ var PublicIdentity = z5.object({
538
+ /** Raw Ed25519 public key. The pinned one. */
539
+ identity: z5.string().min(1),
540
+ /** Raw X25519 public key, for sealing to this party. */
541
+ encryption: z5.string().min(1),
542
+ /**
543
+ * Ed25519 signature over the encryption key, by the identity key.
544
+ *
545
+ * This is what stops an upstream substituting an encryption key of its
546
+ * own while relaying a genuine identity: the receiver pins the identity
547
+ * and refuses any encryption key not signed by it.
548
+ */
549
+ encryptionSig: z5.string().min(1)
550
+ }).strict();
551
+ var StoredKeys = z5.object({
552
+ version: z5.literal(1),
553
+ identityPublic: z5.string().min(1),
554
+ identityPrivate: z5.string().min(1),
555
+ encryptionPublic: z5.string().min(1),
556
+ encryptionPrivate: z5.string().min(1),
557
+ encryptionSig: z5.string().min(1),
558
+ createdAt: z5.number().int().positive()
559
+ }).strict();
560
+ var ENCRYPTION_KEY_CONTEXT = "byollm/v1/encryption-key";
561
+ function rawPublic(key) {
562
+ const jwk = key.export({ format: "jwk" });
563
+ const x = jwk.x;
564
+ if (typeof x !== "string") throw new Error("key has no raw public component");
565
+ return x;
566
+ }
567
+ function importPublic(raw, crv) {
568
+ return createPublicKey({ key: { kty: "OKP", crv, x: raw }, format: "jwk" });
569
+ }
570
+ function importPrivate(stored) {
571
+ return createPrivateKey({
572
+ key: Buffer.from(stored, "base64"),
573
+ type: "pkcs8",
574
+ format: "der"
575
+ });
576
+ }
577
+ var exportPrivate = (key) => key.export({ type: "pkcs8", format: "der" }).toString("base64");
578
+ function generateKeys(now) {
579
+ const identity = generateKeyPairSync("ed25519");
580
+ const encryption = generateKeyPairSync("x25519");
581
+ const encryptionPublic = rawPublic(encryption.publicKey);
582
+ return {
583
+ version: 1,
584
+ identityPublic: rawPublic(identity.publicKey),
585
+ identityPrivate: exportPrivate(identity.privateKey),
586
+ encryptionPublic,
587
+ encryptionPrivate: exportPrivate(encryption.privateKey),
588
+ encryptionSig: sign(
589
+ null,
590
+ Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${encryptionPublic}`),
591
+ identity.privateKey
592
+ ).toString("base64url"),
593
+ createdAt: now
594
+ };
595
+ }
596
+ function publicIdentityOf(keys) {
597
+ return {
598
+ identity: keys.identityPublic,
599
+ encryption: keys.encryptionPublic,
600
+ encryptionSig: keys.encryptionSig
601
+ };
602
+ }
603
+ function verifyPublicIdentity(identity) {
604
+ try {
605
+ return verify(
606
+ null,
607
+ Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${identity.encryption}`),
608
+ importPublic(identity.identity, "Ed25519"),
609
+ Buffer.from(identity.encryptionSig, "base64url")
610
+ );
611
+ } catch {
612
+ return false;
613
+ }
614
+ }
615
+ function signWith(keys, data) {
616
+ return sign(null, data, importPrivate(keys.identityPrivate)).toString(
617
+ "base64url"
618
+ );
619
+ }
620
+ function verifyWith(identityPublic, data, signature) {
621
+ try {
622
+ return verify(
623
+ null,
624
+ data,
625
+ importPublic(identityPublic, "Ed25519"),
626
+ Buffer.from(signature, "base64url")
627
+ );
628
+ } catch {
629
+ return false;
630
+ }
631
+ }
632
+ var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
633
+ function fingerprint(identityPublic) {
634
+ const digest = createHash("sha256").update(Buffer.from(identityPublic, "base64url")).digest();
635
+ let bits = 0;
636
+ let value = 0;
637
+ let out = "";
638
+ for (const byte of digest.subarray(0, 15)) {
639
+ value = value << 8 | byte;
640
+ bits += 8;
641
+ while (bits >= 5) {
642
+ out += ALPHABET.charAt(value >>> bits - 5 & 31);
643
+ bits -= 5;
644
+ }
645
+ }
646
+ const groups = out.match(/.{1,4}/g) ?? [];
647
+ return `BYOLLM-${groups.join("-")}`;
648
+ }
649
+ var keyId = (identityPublic) => fingerprint(identityPublic);
650
+
651
+ // src/envelope.ts
652
+ var readied;
653
+ async function cryptoReady() {
654
+ readied ??= sodium.ready;
655
+ await readied;
656
+ }
657
+ var ENVELOPE_MAX_AGE_MS = 24 * 60 * 6e4;
658
+ var EnvelopeDirection = z6.enum(["payload", "result"]);
659
+ var SealedEnvelope = z6.object({
660
+ /** Base64url `crypto_box_seal` output over the signed plaintext. */
661
+ ciphertext: z6.string().min(1),
662
+ /** Who this was sealed to — the recipient checks it is them. */
663
+ recipientKeyId: z6.string().min(1),
664
+ /** Who signed it — the recipient checks this against its pin. */
665
+ senderKeyId: z6.string().min(1),
666
+ direction: EnvelopeDirection,
667
+ /**
668
+ * When this ciphertext stops being worth keeping.
669
+ *
670
+ * Carried *on* the envelope rather than recomputed by the opener. An
671
+ * earlier version derived it from the job's creation time, which meant
672
+ * two systems had to agree on a timestamp to the millisecond — and they
673
+ * did not, once a real database rounded it. A bound value that has to be
674
+ * reconstructed is a bound value that eventually is not.
675
+ *
676
+ * Not trusted as written: it is also inside the signature, so a changed
677
+ * deadline fails to verify.
678
+ */
679
+ deadlineAt: z6.number().int().positive()
680
+ }).strict();
681
+ function signedBody(context, plaintext) {
682
+ return Buffer.from(
683
+ JSON.stringify({
684
+ v: "byollm/v1/envelope",
685
+ jobId: context.jobId,
686
+ senderKeyId: context.senderKeyId,
687
+ recipientKeyId: context.recipientKeyId,
688
+ deadlineAt: context.deadlineAt,
689
+ direction: context.direction,
690
+ plaintext
691
+ }),
692
+ "utf8"
693
+ );
694
+ }
695
+ var rawX25519 = (key, part) => {
696
+ const jwk = key.export({ format: "jwk" });
697
+ const value = part === "x" ? jwk.x : jwk.d;
698
+ if (typeof value !== "string") throw new Error("not an X25519 key");
699
+ return new Uint8Array(Buffer.from(value, "base64url"));
700
+ };
701
+ async function seal(input) {
702
+ await cryptoReady();
703
+ const body = signedBody(input.context, input.plaintext);
704
+ const signature = signWith(input.senderKeys, body);
705
+ const inner = JSON.stringify({ body: body.toString("base64url"), signature });
706
+ const recipient = new Uint8Array(
707
+ Buffer.from(input.recipientEncryptionPublic, "base64url")
708
+ );
709
+ const ciphertext = sodium.crypto_box_seal(
710
+ new Uint8Array(Buffer.from(inner, "utf8")),
711
+ recipient
712
+ );
713
+ return {
714
+ ciphertext: Buffer.from(ciphertext).toString("base64url"),
715
+ recipientKeyId: input.context.recipientKeyId,
716
+ senderKeyId: input.context.senderKeyId,
717
+ direction: input.context.direction,
718
+ deadlineAt: input.context.deadlineAt
719
+ };
720
+ }
721
+ async function open(input) {
722
+ await cryptoReady();
723
+ const { envelope, expected } = input;
724
+ if (envelope.recipientKeyId !== expected.recipientKeyId || envelope.senderKeyId !== expected.senderKeyId || envelope.direction !== expected.direction) {
725
+ return { ok: false, reason: "not-for-us" };
726
+ }
727
+ let inner;
728
+ try {
729
+ const priv = createPrivateKey2({
730
+ key: Buffer.from(input.recipientKeys.encryptionPrivate, "base64"),
731
+ type: "pkcs8",
732
+ format: "der"
733
+ });
734
+ const pub = createPublicKey2(priv);
735
+ const opened = sodium.crypto_box_seal_open(
736
+ new Uint8Array(Buffer.from(envelope.ciphertext, "base64url")),
737
+ rawX25519(pub, "x"),
738
+ rawX25519(priv, "d")
739
+ );
740
+ inner = Buffer.from(opened).toString("utf8");
741
+ } catch {
742
+ return { ok: false, reason: "unopenable" };
743
+ }
744
+ let parsed;
745
+ try {
746
+ parsed = JSON.parse(inner);
747
+ } catch {
748
+ return { ok: false, reason: "malformed" };
749
+ }
750
+ if (typeof parsed.body !== "string" || typeof parsed.signature !== "string") {
751
+ return { ok: false, reason: "malformed" };
752
+ }
753
+ const body = Buffer.from(parsed.body, "base64url");
754
+ if (!verifyWith(input.senderIdentityPublic, body, parsed.signature)) {
755
+ return { ok: false, reason: "bad-signature" };
756
+ }
757
+ let claims;
758
+ try {
759
+ claims = JSON.parse(body.toString("utf8"));
760
+ } catch {
761
+ return { ok: false, reason: "malformed" };
762
+ }
763
+ if (claims["jobId"] !== expected.jobId || claims["senderKeyId"] !== expected.senderKeyId || claims["recipientKeyId"] !== expected.recipientKeyId || claims["deadlineAt"] !== envelope.deadlineAt || claims["direction"] !== expected.direction) {
764
+ return { ok: false, reason: "context-mismatch" };
765
+ }
766
+ if (typeof claims["plaintext"] !== "string") {
767
+ return { ok: false, reason: "malformed" };
768
+ }
769
+ return { ok: true, plaintext: claims["plaintext"] };
770
+ }
771
+
772
+ // src/signing.ts
773
+ import { createHash as createHash2 } from "crypto";
774
+ import { z as z7 } from "zod";
775
+ var MAX_CLOCK_SKEW_MS = 12e4;
776
+ var RequestSignature = z7.object({
777
+ /** Which runner is calling. The server looks up its pinned identity. */
778
+ runnerId: z7.string().min(1),
779
+ /** Epoch ms, bounded by {@link MAX_CLOCK_SKEW_MS}. */
780
+ issuedAt: z7.number().int().positive(),
781
+ /** Base64url Ed25519 signature over {@link canonicalRequest}. */
782
+ signature: z7.string().min(1)
783
+ }).strict();
784
+ function canonicalRequest(input) {
785
+ const digest = createHash2("sha256").update(input.body, "utf8").digest("hex");
786
+ return Buffer.from(
787
+ [
788
+ "byollm/v1/request",
789
+ input.endpoint,
790
+ input.runnerId,
791
+ String(input.issuedAt),
792
+ digest
793
+ ].join("\n"),
794
+ "utf8"
795
+ );
796
+ }
797
+ function signRequest(keys, input) {
798
+ return {
799
+ runnerId: input.runnerId,
800
+ issuedAt: input.issuedAt,
801
+ signature: signWith(keys, canonicalRequest(input))
802
+ };
803
+ }
804
+ function signSiteRequest(keys, input) {
805
+ return signRequest(keys, {
806
+ endpoint: siteEndpoint(input.endpoint),
807
+ runnerId: input.siteId,
808
+ issuedAt: input.issuedAt,
809
+ body: input.body
810
+ });
811
+ }
812
+ function verifySiteRequest(input) {
813
+ return verifyRequest({
814
+ ...input,
815
+ endpoint: siteEndpoint(input.endpoint)
816
+ });
817
+ }
818
+ var siteEndpoint = (endpoint) => `site/${endpoint}`;
819
+ function verifyRequest(input) {
820
+ const skew = input.maxSkewMs ?? MAX_CLOCK_SKEW_MS;
821
+ if (Math.abs(input.now - input.signature.issuedAt) > skew) return "stale";
822
+ const ok = verifyWith(
823
+ input.identityPublic,
824
+ canonicalRequest({
825
+ endpoint: input.endpoint,
826
+ runnerId: input.signature.runnerId,
827
+ issuedAt: input.signature.issuedAt,
828
+ body: input.body
829
+ }),
830
+ input.signature.signature
831
+ );
832
+ return ok ? null : "bad-signature";
833
+ }
241
834
 
242
835
  // src/musts.ts
243
836
  var must = (m) => Object.freeze(m);
@@ -247,31 +840,78 @@ var MUSTS = Object.freeze({
247
840
  id: "PAIR_ONE_USER",
248
841
  statement: "A runner token MUST be bound to exactly one user; a daemon MUST refuse work not attributable to its paired user.",
249
842
  enforcedBy: "both",
843
+ verifiedBy: "conformance",
250
844
  source: "byollm_001 \xA7MUSTs"
251
845
  }),
252
846
  PAIR_INTERACTIVE: must({
253
847
  id: "PAIR_INTERACTIVE",
254
848
  statement: "Pairing MUST be interactive (device-code approval in the app's own session); a long-lived pasted secret MUST NOT be accepted as pairing.",
255
849
  enforcedBy: "server",
850
+ verifiedBy: "conformance",
256
851
  source: "byollm_001 \xA7Endpoints.1"
257
852
  }),
258
853
  PAIR_CODE_EXPIRES: must({
259
854
  id: "PAIR_CODE_EXPIRES",
260
855
  statement: "An unapproved device code MUST expire and MUST NOT be redeemable after expiry.",
261
856
  enforcedBy: "server",
857
+ verifiedBy: "conformance",
262
858
  source: "byollm_001 \xA7Endpoints.1"
263
859
  }),
264
860
  // ---- Typed job kinds --------------------------------------------------
861
+ VERSION_HANDSHAKE_REQUIRED: must({
862
+ id: "VERSION_HANDSHAKE_REQUIRED",
863
+ statement: "Every protocol request MUST declare a protocol version, and a server MUST refuse an absent or unsupported one with a structured error naming what it supports \u2014 never a generic parse failure.",
864
+ enforcedBy: "both",
865
+ verifiedBy: "conformance",
866
+ source: "byollm_009 \xA74"
867
+ }),
868
+ KEYS_EXCHANGED_AT_CONSENT: must({
869
+ id: "KEYS_EXCHANGED_AT_CONSENT",
870
+ 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.",
871
+ enforcedBy: "both",
872
+ verifiedBy: "conformance",
873
+ source: "byollm_009 \xA75"
874
+ }),
875
+ REQUESTS_SIGNED_NOT_BEARER: must({
876
+ id: "REQUESTS_SIGNED_NOT_BEARER",
877
+ statement: "Every authenticated request MUST be signed by the calling device's pinned identity key, over the endpoint, the runner id, a timestamp and the exact request body. A server MUST NOT accept a bearer credential in place of a signature.",
878
+ enforcedBy: "both",
879
+ verifiedBy: "conformance",
880
+ source: "byollm_009 \xA74.2"
881
+ }),
882
+ LEASE_SCOPED_BY_GRANT: must({
883
+ id: "LEASE_SCOPED_BY_GRANT",
884
+ statement: "A lease-scoped request MUST name the lease it acts on, and a server MUST apply it only to that lease. Naming the job and the runner is not sufficient: both survive a claim-release-reclaim cycle.",
885
+ enforcedBy: "both",
886
+ verifiedBy: "conformance",
887
+ source: "byollm_009 \xA74.2"
888
+ }),
889
+ STUB_METADATA_EXHAUSTIVE: must({
890
+ id: "STUB_METADATA_EXHAUSTIVE",
891
+ statement: "A claim MUST answer with stubs carrying exactly the enumerated fields and no payload. An endpoint MUST NOT emit a stub carrying others, and an upstream MUST NOT require any.",
892
+ enforcedBy: "both",
893
+ verifiedBy: "conformance",
894
+ source: "byollm_009 \xA76"
895
+ }),
896
+ ENVELOPE_SEALED_AND_SIGNED: must({
897
+ id: "ENVELOPE_SEALED_AND_SIGNED",
898
+ statement: "A stored payload MUST be sealed, and MUST be signed by the sender's identity key. An endpoint MUST refuse an envelope whose signature does not verify against the identity it pinned.",
899
+ enforcedBy: "server",
900
+ verifiedBy: "conformance",
901
+ source: "byollm_009 \xA76"
902
+ }),
265
903
  KIND_TYPED_ONLY: must({
266
904
  id: "KIND_TYPED_ONLY",
267
905
  statement: "Job kinds MUST resolve against handlers baked into the daemon. A daemon MUST refuse an unknown kind rather than guess.",
268
906
  enforcedBy: "daemon",
907
+ verifiedBy: "conformance",
269
908
  source: "byollm_001 \xA7Jobs are typed data"
270
909
  }),
271
910
  KIND_NO_CODE: must({
272
911
  id: "KIND_NO_CODE",
273
912
  statement: "A server MUST NOT be able to convey code, a shell string, or a path to execute; payloads are data handed to a model only.",
274
913
  enforcedBy: "daemon",
914
+ verifiedBy: "conformance",
275
915
  source: "byollm_001 \xA7Jobs are typed data; byollm_004 \xA71"
276
916
  }),
277
917
  // ---- Capability and claiming -----------------------------------------
@@ -279,18 +919,21 @@ var MUSTS = Object.freeze({
279
919
  id: "CLAIM_REQUIRES_CAPABILITY",
280
920
  statement: "A daemon MUST NOT be given a job whose kind is absent from its advertised capability matrix.",
281
921
  enforcedBy: "both",
922
+ verifiedBy: "conformance",
282
923
  source: "byollm_001 \xA7MUSTs"
283
924
  }),
284
925
  CAPABILITY_IS_DETECTED: must({
285
926
  id: "CAPABILITY_IS_DETECTED",
286
927
  statement: "An advertised capability matrix MUST be the intersection of owner config and detected, healthy reality \u2014 never config alone.",
287
928
  enforcedBy: "daemon",
929
+ verifiedBy: "conformance",
288
930
  source: "byollm_002 \xA7Routing"
289
931
  }),
290
932
  CLAIM_ATOMIC: must({
291
933
  id: "CLAIM_ATOMIC",
292
934
  statement: "Claiming MUST be atomic: a job MUST NOT be handed to two runners concurrently.",
293
935
  enforcedBy: "server",
936
+ verifiedBy: "conformance",
294
937
  source: "byollm_001 \xA7Endpoints.2"
295
938
  }),
296
939
  // ---- Leases -----------------------------------------------------------
@@ -298,12 +941,14 @@ var MUSTS = Object.freeze({
298
941
  id: "LEASE_HONORED",
299
942
  statement: "A daemon MUST stop work on a job whose lease it has failed to renew, and MUST NOT report a result for an expired lease it no longer holds.",
300
943
  enforcedBy: "daemon",
944
+ verifiedBy: "conformance",
301
945
  source: "byollm_001 \xA7MUSTs"
302
946
  }),
303
947
  LEASE_RECLAIMABLE: must({
304
948
  id: "LEASE_RECLAIMABLE",
305
949
  statement: "A lease that expires un-renewed MUST make its job claimable again with no loss of the job.",
306
950
  enforcedBy: "server",
951
+ verifiedBy: "conformance",
307
952
  source: "byollm_001 \xA7Endpoints.2"
308
953
  }),
309
954
  // ---- Audience and offer scope ----------------------------------------
@@ -311,24 +956,56 @@ var MUSTS = Object.freeze({
311
956
  id: "AUDIENCE_BOTH_SIDES",
312
957
  statement: "A job MUST run on a daemon only if the daemon's offer scope admits the job's owner AND the job's audience admits the daemon's owner.",
313
958
  enforcedBy: "both",
959
+ verifiedBy: "conformance",
314
960
  source: "byollm_001 \xA7The audience model"
315
961
  }),
316
962
  SUBSCRIPTION_SELF_LOCK: must({
317
963
  id: "SUBSCRIPTION_SELF_LOCK",
318
964
  statement: "A subscription-class backend's offer scope MUST be 'self' and MUST NOT be widened by configuration.",
319
965
  enforcedBy: "daemon",
966
+ verifiedBy: "conformance",
320
967
  source: "byollm_001 \xA7The audience model"
321
968
  }),
969
+ METERED_DEFAULTS_SELF: must({
970
+ id: "METERED_DEFAULTS_SELF",
971
+ statement: "A metered backend's effective offer scope MUST be 'self' unless the owner has explicitly acknowledged spending money on others' work.",
972
+ enforcedBy: "daemon",
973
+ verifiedBy: "conformance",
974
+ source: "byollm_007 \xA74"
975
+ }),
976
+ METERED_REQUIRES_CEILING: must({
977
+ id: "METERED_REQUIRES_CEILING",
978
+ statement: "A widened metered backend MUST carry a spend ceiling, and the daemon MUST refuse community work once it is reached.",
979
+ enforcedBy: "daemon",
980
+ verifiedBy: "conformance",
981
+ source: "byollm_007 \xA74"
982
+ }),
983
+ COST_NOT_CONFIGURABLE: must({
984
+ id: "COST_NOT_CONFIGURABLE",
985
+ statement: "A built-in provider's cost class MUST NOT be overridable by configuration.",
986
+ enforcedBy: "daemon",
987
+ verifiedBy: "conformance",
988
+ source: "byollm_007 \xA72"
989
+ }),
990
+ REMOTE_IS_NEVER_FREE: must({
991
+ id: "REMOTE_IS_NEVER_FREE",
992
+ statement: "A generic HTTP backend whose base URL is not loopback or private MUST be treated as metered.",
993
+ enforcedBy: "daemon",
994
+ verifiedBy: "conformance",
995
+ source: "byollm_007 \xA72"
996
+ }),
322
997
  NAMED_LOCAL_ALLOWLIST: must({
323
998
  id: "NAMED_LOCAL_ALLOWLIST",
324
999
  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.",
325
1000
  enforcedBy: "daemon",
1001
+ verifiedBy: "conformance",
326
1002
  source: "byollm_001 Rev 1 \xA7B"
327
1003
  }),
328
1004
  REFUSAL_NOT_REOFFERED: must({
329
1005
  id: "REFUSAL_NOT_REOFFERED",
330
1006
  statement: "A server MUST NOT re-offer a job to a runner that released it with reason 'refused'.",
331
1007
  enforcedBy: "server",
1008
+ verifiedBy: "conformance",
332
1009
  source: "byollm_001 Rev 1 \xA7B (loop resolved in build review)"
333
1010
  }),
334
1011
  // ---- Revocation and cancel -------------------------------------------
@@ -336,12 +1013,14 @@ var MUSTS = Object.freeze({
336
1013
  id: "REVOCATION_HONORED",
337
1014
  statement: "A revoked daemon MUST stop claiming and MUST abandon in-flight work by the next heartbeat at the latest.",
338
1015
  enforcedBy: "daemon",
1016
+ verifiedBy: "conformance",
339
1017
  source: "byollm_001 \xA7MUSTs"
340
1018
  }),
341
1019
  CANCEL_HONORED: must({
342
1020
  id: "CANCEL_HONORED",
343
1021
  statement: "A job id in a heartbeat response's cancel list MUST abort that job's in-flight backend call and be reported as 'canceled'.",
344
1022
  enforcedBy: "daemon",
1023
+ verifiedBy: "conformance",
345
1024
  source: "byollm_001 Rev 1 \xA7C"
346
1025
  }),
347
1026
  // ---- Lifecycle, dependencies, delivery -------------------------------
@@ -349,37 +1028,43 @@ var MUSTS = Object.freeze({
349
1028
  id: "DEPENDS_ON_GATING",
350
1029
  statement: "A job MUST NOT be claimable until every job in its dependsOn set has reached the 'ok' state.",
351
1030
  enforcedBy: "server",
1031
+ verifiedBy: "conformance",
352
1032
  source: "byollm_001 Rev 1 \xA7E"
353
1033
  }),
354
1034
  TTL_EXPIRY: must({
355
1035
  id: "TTL_EXPIRY",
356
1036
  statement: "An unclaimed job MUST become 'expired' once its TTL elapses, and the TTL clock MUST start when the job becomes claimable, not at enqueue.",
357
1037
  enforcedBy: "server",
1038
+ verifiedBy: "conformance",
358
1039
  source: "byollm_001 Rev 1 \xA7D (TTL clock resolved in build review)"
359
1040
  }),
360
1041
  NO_RUNNER_SIGNAL: must({
361
1042
  id: "NO_RUNNER_SIGNAL",
362
1043
  statement: "A server MUST surface noRunnerAvailable when no runner with matching capability has heartbeated within the liveness window, and MUST NOT raise it for a job still blocked on dependencies.",
363
1044
  enforcedBy: "server",
1045
+ verifiedBy: "conformance",
364
1046
  source: "byollm_001 Rev 1 \xA7D"
365
1047
  }),
366
1048
  RESULT_IDEMPOTENT: must({
367
1049
  id: "RESULT_IDEMPOTENT",
368
1050
  statement: "Result submission MUST be idempotent by job id; the first terminal outcome wins and later submissions MUST NOT change it.",
369
1051
  enforcedBy: "server",
1052
+ verifiedBy: "conformance",
370
1053
  source: "byollm_001 \xA7Endpoints.4"
371
1054
  }),
372
- RESULT_PROVENANCE: must({
373
- id: "RESULT_PROVENANCE",
374
- statement: "A result from a non-'self' job MUST carry its provenance (audience and runner) to the delivery seam so an app never treats volunteer output as first-party.",
1055
+ PROVENANCE_NAMES_DEVICE: must({
1056
+ id: "PROVENANCE_NAMES_DEVICE",
1057
+ statement: "A result MUST carry the claiming device's key id and its relationship to the requester, to the delivery seam, so an app never treats volunteer output as first-party. The key id MUST be the device the upstream granted the lease to, and a result whose signature does not verify against that device MUST be refused rather than recorded.",
375
1058
  enforcedBy: "server",
376
- source: "byollm_003 Rev 1 \xA7Return-trip"
1059
+ verifiedBy: "conformance",
1060
+ source: "byollm_009 \xA711"
377
1061
  }),
378
1062
  // ---- The trust surface -------------------------------------------------
379
1063
  INGRESS_LOGGED_BEFORE_EXECUTION: must({
380
1064
  id: "INGRESS_LOGGED_BEFORE_EXECUTION",
381
1065
  statement: "Every executed prompt MUST be appended to the local ingress log before execution begins.",
382
1066
  enforcedBy: "daemon",
1067
+ verifiedBy: "conformance",
383
1068
  source: "byollm_001 \xA7MUSTs"
384
1069
  }),
385
1070
  // ---- Execution isolation (byollm_004) ---------------------------------
@@ -387,178 +1072,420 @@ var MUSTS = Object.freeze({
387
1072
  id: "NO_SHELL_INTERPOLATION",
388
1073
  statement: "Process-class backends MUST be invoked with a fixed argv array and the payload delivered on stdin; payload text MUST NOT reach a command line.",
389
1074
  enforcedBy: "daemon",
1075
+ verifiedBy: "adversarial",
390
1076
  source: "byollm_004 \xA72"
391
1077
  }),
392
1078
  NO_PAYLOAD_ROUTING: must({
393
1079
  id: "NO_PAYLOAD_ROUTING",
394
1080
  statement: "Model, backend, base URL, and flags MUST come from owner config only; a payload MUST NOT influence any of them.",
395
1081
  enforcedBy: "daemon",
1082
+ verifiedBy: "adversarial",
396
1083
  source: "byollm_004 \xA72"
397
1084
  }),
398
1085
  STRIPPED_CHILD_ENV: must({
399
1086
  id: "STRIPPED_CHILD_ENV",
400
1087
  statement: "Process-class children MUST spawn with an allowlisted environment, a scratch cwd, no inherited descriptors beyond std streams, and hard timeout and output-size caps.",
401
1088
  enforcedBy: "daemon",
1089
+ verifiedBy: "adversarial",
402
1090
  source: "byollm_004 \xA72"
403
1091
  }),
404
1092
  HTTP_BASE_URL_SAFE: must({
405
1093
  id: "HTTP_BASE_URL_SAFE",
406
1094
  statement: "HTTP-class backends MUST send requests only to the owner-configured base URL and MUST refuse base URLs resolving to cloud-metadata or link-local addresses.",
407
1095
  enforcedBy: "daemon",
1096
+ verifiedBy: "adversarial",
408
1097
  source: "byollm_004 Rev 1 \xA7Backend taxonomy"
409
1098
  }),
410
1099
  OUTPUT_INERT: must({
411
1100
  id: "OUTPUT_INERT",
412
1101
  statement: "Returned text MUST be treated as inert bytes: never evaluated, never written to a payload-named path, never interpolated into a shell or into terminal control sequences when logged.",
413
1102
  enforcedBy: "daemon",
1103
+ verifiedBy: "adversarial",
414
1104
  source: "byollm_004 \xA72"
415
1105
  }),
416
1106
  COMMUNITY_BUDGETS: must({
417
1107
  id: "COMMUNITY_BUDGETS",
418
1108
  statement: "Jobs whose owner is not the daemon's owner MUST be subject to the owner's rate limits, daily cap, and resource budget.",
419
1109
  enforcedBy: "daemon",
1110
+ verifiedBy: "adversarial",
420
1111
  source: "byollm_004 \xA74"
1112
+ }),
1113
+ REVOCATION_IMMEDIATE: must({
1114
+ id: "REVOCATION_IMMEDIATE",
1115
+ 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.",
1116
+ // Both, and stated as one sentence with two obligations rather than
1117
+ // folded into REVOCATION_HONORED. That one binds the *daemon*: a revoked
1118
+ // daemon stops claiming and abandons in-flight work. This binds the
1119
+ // *upstream*. byollm_009 §5 is explicit that the pair is the point — "a
1120
+ // revocation enforced at one end survives a compromise of that end" — and
1121
+ // one entry covering both would make a compromised daemon look compliant.
1122
+ enforcedBy: "both",
1123
+ verifiedBy: "conformance",
1124
+ source: "byollm_009 \xA711"
1125
+ }),
1126
+ CONSENT_BEFORE_ROUTE: must({
1127
+ id: "CONSENT_BEFORE_ROUTE",
1128
+ 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.",
1129
+ enforcedBy: "server",
1130
+ verifiedBy: "conformance",
1131
+ source: "byollm_009 \xA711"
1132
+ }),
1133
+ ROSTER_NOT_DISCLOSED: must({
1134
+ id: "ROSTER_NOT_DISCLOSED",
1135
+ 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.",
1136
+ // Checkable since cloud_008 §0.2 took `audienceAllow` off the stub: the
1137
+ // property now holds by *absence*, and absence is exactly what a strict
1138
+ // schema and a serialised stub can be asked about. Before that it was a
1139
+ // sentence — and one this project cited in code comments, tests and two
1140
+ // specs as though it were enforced data, which is why it is worth
1141
+ // stating precisely rather than generously.
1142
+ enforcedBy: "both",
1143
+ verifiedBy: "conformance",
1144
+ source: "byollm_009 \xA711"
1145
+ }),
1146
+ EFFECTIVE_OFFER_ONLY: must({
1147
+ id: "EFFECTIVE_OFFER_ONLY",
1148
+ 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.",
1149
+ enforcedBy: "both",
1150
+ verifiedBy: "conformance",
1151
+ source: "byollm_009 \xA711"
1152
+ }),
1153
+ FALLBACK_LABELED: must({
1154
+ id: "FALLBACK_LABELED",
1155
+ 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.",
1156
+ // `construction` today, and deliberately not `conformance`. Nothing on
1157
+ // the wire yet distinguishes a fallback from any other community job —
1158
+ // the ledger that would give it a surface is unbuilt — so a check would
1159
+ // have to assert something it cannot observe. Promoted the day that
1160
+ // surface exists. Marking it `conformance` now would put "verified"
1161
+ // beside a property no third party can see, which is the one thing the
1162
+ // kinds exist to prevent.
1163
+ enforcedBy: "both",
1164
+ verifiedBy: "construction",
1165
+ source: "byollm_009 \xA711"
1166
+ }),
1167
+ RELAY_BLIND: must({
1168
+ id: "RELAY_BLIND",
1169
+ statement: "A relay MUST NOT hold any key capable of decrypting a payload, a result, or a delta frame.",
1170
+ // Operator: a third party can read the relay's types and see there is
1171
+ // nowhere to put such a key, but the kit certifies a *server* and cannot
1172
+ // reach inside somebody's deployment to prove what it holds.
1173
+ enforcedBy: "server",
1174
+ verifiedBy: "operator",
1175
+ source: "byollm_009 \xA711"
1176
+ }),
1177
+ SHARED_COMPUTE_DISCLOSED: must({
1178
+ id: "SHARED_COMPUTE_DISCLOSED",
1179
+ 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.",
1180
+ // Operator, and cloud_008 §0.3 is why the classification now comes with a
1181
+ // standing answer rather than a standing question. The screen is not
1182
+ // wire-observable, but the *string the server composes* is, and it is
1183
+ // now unit-tested with the two false sentences forbidden by name. The
1184
+ // kind stays `operator` because a third-party site can still render
1185
+ // whatever it likes; what changed is that the part inside our own
1186
+ // boundary stopped depending on somebody remembering to audit it.
1187
+ enforcedBy: "server",
1188
+ verifiedBy: "operator",
1189
+ source: "byollm_009 \xA711"
421
1190
  })
422
1191
  });
1192
+ var RETIRED_MUSTS = Object.freeze({
1193
+ RESULT_PROVENANCE: {
1194
+ supersededBy: "PROVENANCE_NAMES_DEVICE",
1195
+ 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."
1196
+ }
1197
+ });
423
1198
  var MUST_IDS = Object.freeze(Object.keys(MUSTS));
1199
+ function mustsVerifiedBy(kind) {
1200
+ return MUST_IDS.filter((id) => MUSTS[id].verifiedBy === kind);
1201
+ }
424
1202
 
425
1203
  // src/wire.ts
426
- import { z as z5 } from "zod";
1204
+ import { z as z8 } from "zod";
427
1205
  var PROTOCOL_VERSION = "0";
1206
+ var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze([
1207
+ PROTOCOL_VERSION
1208
+ ]);
1209
+ var MIN_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0] ?? PROTOCOL_VERSION;
1210
+ function checkProtocolVersion(body) {
1211
+ const declared = typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion") ? body.protocolVersion : void 0;
1212
+ if (typeof declared !== "string" || declared.length === 0) {
1213
+ return {
1214
+ error: "unsupported-protocol-version",
1215
+ message: `this request declared no protocol version. Upgrade the daemon: \`${UPGRADE_COMMAND}\`.`,
1216
+ supported: SUPPORTED_PROTOCOL_VERSIONS,
1217
+ minimum: MIN_PROTOCOL_VERSION
1218
+ };
1219
+ }
1220
+ if (!SUPPORTED_PROTOCOL_VERSIONS.includes(declared)) {
1221
+ return {
1222
+ error: "unsupported-protocol-version",
1223
+ 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."),
1224
+ supported: SUPPORTED_PROTOCOL_VERSIONS,
1225
+ minimum: MIN_PROTOCOL_VERSION
1226
+ };
1227
+ }
1228
+ return null;
1229
+ }
1230
+ var UPGRADE_COMMAND = "npm i -g byollm@latest";
428
1231
  var PROTOCOL_PREFIX = "/byollm";
429
1232
  var ENDPOINTS = Object.freeze([
430
1233
  "pair",
431
1234
  "claim",
1235
+ "fetch",
432
1236
  "heartbeat",
433
1237
  "result",
434
1238
  "release"
435
1239
  ]);
436
- var Capability = z5.object({
1240
+ var Capability = z8.object({
437
1241
  kind: JobKind,
438
1242
  backendId: BackendIdSchema,
439
1243
  backendClass: BackendClass,
440
- model: z5.string().min(1),
1244
+ model: z8.string().min(1),
441
1245
  offerScope: OfferScope
442
1246
  }).strict();
443
- var CapabilityMatrix = z5.array(Capability);
444
- var PairStartRequest = z5.object({
445
- protocolVersion: z5.literal(PROTOCOL_VERSION),
446
- action: z5.literal("start"),
447
- daemon: z5.object({
448
- version: z5.string().min(1),
1247
+ var CapabilityMatrix = z8.array(Capability);
1248
+ var PairStartRequest = z8.object({
1249
+ protocolVersion: z8.literal(PROTOCOL_VERSION),
1250
+ action: z8.literal("start"),
1251
+ daemon: z8.object({
1252
+ version: z8.string().min(1),
449
1253
  /** Shown in the app's runner list so a user can tell their machines apart. */
450
- label: z5.string().min(1).max(120),
451
- platform: z5.enum(["darwin", "linux", "win32"])
1254
+ label: z8.string().min(1).max(120),
1255
+ platform: z8.enum(["darwin", "linux", "win32"])
452
1256
  }),
1257
+ /**
1258
+ * This machine's public keys (byollm_009 §5).
1259
+ *
1260
+ * Pairing is where the two parties learn each other's identities, because
1261
+ * it is the one moment a human is already deciding to trust: the approval
1262
+ * click. A key exchanged anywhere else would be a key nobody chose.
1263
+ */
1264
+ device: PublicIdentity,
453
1265
  capabilities: CapabilityMatrix
454
1266
  }).strict();
455
- var PairStartResponse = z5.object({
1267
+ var PairStartResponse = z8.object({
456
1268
  /** Secret the daemon polls with. Never shown to the user. */
457
- deviceCode: z5.string().min(20),
1269
+ deviceCode: z8.string().min(20),
458
1270
  /** Short code the user reads and confirms in the browser. */
459
- userCode: z5.string().min(4).max(16),
1271
+ userCode: z8.string().min(4).max(16),
460
1272
  /** Where the user approves. Must be on the server's own origin. */
461
- verificationUrl: z5.url(),
1273
+ verificationUrl: z8.url(),
462
1274
  /** Epoch ms after which the code is dead ({@link MUSTS.PAIR_CODE_EXPIRES}). */
463
- expiresAt: z5.number().int().positive(),
1275
+ expiresAt: z8.number().int().positive(),
464
1276
  /** How often the daemon may poll. */
465
- pollIntervalMs: z5.number().int().min(500).max(6e4)
1277
+ pollIntervalMs: z8.number().int().min(500).max(6e4)
466
1278
  }).strict();
467
- var PairPollRequest = z5.object({
468
- protocolVersion: z5.literal(PROTOCOL_VERSION),
469
- action: z5.literal("poll"),
470
- deviceCode: z5.string().min(20)
1279
+ var PairPollRequest = z8.object({
1280
+ protocolVersion: z8.literal(PROTOCOL_VERSION),
1281
+ action: z8.literal("poll"),
1282
+ deviceCode: z8.string().min(20)
471
1283
  }).strict();
472
- var PairPollResponse = z5.discriminatedUnion("status", [
473
- z5.object({ status: z5.literal("pending") }).strict(),
474
- z5.object({ status: z5.literal("denied") }).strict(),
475
- z5.object({ status: z5.literal("expired") }).strict(),
476
- z5.object({
477
- status: z5.literal("approved"),
478
- /** Bearer token for every later call. Scoped to exactly one user. */
479
- runnerToken: z5.string().min(20),
480
- runnerId: z5.string().min(1),
1284
+ var PairPollResponse = z8.discriminatedUnion("status", [
1285
+ z8.object({ status: z8.literal("pending") }).strict(),
1286
+ z8.object({ status: z8.literal("denied") }).strict(),
1287
+ z8.object({ status: z8.literal("expired") }).strict(),
1288
+ z8.object({
1289
+ status: z8.literal("approved"),
1290
+ // `runnerToken` is gone cloud_008 §2.4, finding 37.
1291
+ //
1292
+ // It was minted here, hashed into `RunnerRecord.tokenHash`, written to
1293
+ // the daemon's pairings file, and then **never sent, never looked up
1294
+ // and never compared**. `getRunnerByTokenHash` existed on both stores
1295
+ // and was called by nothing but a test asserting it returns null.
1296
+ //
1297
+ // Not merely dead wire, which is what `audienceAllow` and
1298
+ // `HeartbeatResponse.leases` were. This was a *secret*: minted,
1299
+ // transmitted, and written to two disks at rest, for nothing. A
1300
+ // credential with no purpose is a liability rather than clutter,
1301
+ // because the only thing it can ever do is leak.
1302
+ //
1303
+ // `REQUESTS_SIGNED_NOT_BEARER` was already the rule and was already
1304
+ // enforced — every authenticated call is signed by the device's pinned
1305
+ // identity key. This removes the thing the MUST is named after.
1306
+ runnerId: z8.string().min(1),
481
1307
  /** The app's id for the approving user — this daemon's owner forever. */
482
- owner: z5.string().min(1),
1308
+ owner: z8.string().min(1),
483
1309
  /** Display name for the trust UI, if the app offers one. */
484
- ownerLabel: z5.string().optional()
1310
+ ownerLabel: z8.string().optional(),
1311
+ /**
1312
+ * The site's public keys, for the daemon to pin (byollm_009 §5).
1313
+ *
1314
+ * Returned only on approval — a pending or denied poll learns nothing,
1315
+ * so an unapproved code cannot be used to enumerate a site's keys.
1316
+ */
1317
+ site: PublicIdentity
485
1318
  }).strict()
486
1319
  ]);
487
- var PairRequest = z5.discriminatedUnion("action", [
1320
+ var PairRequest = z8.discriminatedUnion("action", [
488
1321
  PairStartRequest,
489
1322
  PairPollRequest
490
1323
  ]);
491
- var ClaimRequest = z5.object({
492
- protocolVersion: z5.literal(PROTOCOL_VERSION),
493
- runnerId: z5.string().min(1),
1324
+ var ClaimRequest = z8.object({
1325
+ protocolVersion: z8.literal(PROTOCOL_VERSION),
1326
+ runnerId: z8.string().min(1),
494
1327
  /** Re-sent on every claim so a server never matches against a stale matrix. */
495
1328
  capabilities: CapabilityMatrix,
496
1329
  /** Upper bound on jobs to return; the server may return fewer. */
497
- max: z5.number().int().min(1).max(64)
1330
+ max: z8.number().int().min(1).max(64)
498
1331
  }).strict();
499
- var ClaimResponse = z5.object({
500
- jobs: z5.array(ClaimedJob),
1332
+ var ClaimResponse = z8.object({
1333
+ /**
1334
+ * Stubs, not jobs. The payload arrives from `fetch`, sealed to whichever
1335
+ * device claimed — see {@link JobStub} for the exhaustive metadata list.
1336
+ */
1337
+ jobs: z8.array(ClaimedStub),
501
1338
  /** Lease duration granted, so the daemon knows its renewal deadline. */
502
- leaseMs: z5.number().int().positive()
1339
+ leaseMs: z8.number().int().positive()
503
1340
  }).strict();
504
- var HeartbeatRequest = z5.object({
505
- protocolVersion: z5.literal(PROTOCOL_VERSION),
506
- runnerId: z5.string().min(1),
507
- daemonVersion: z5.string().min(1),
1341
+ var HeartbeatRequest = z8.object({
1342
+ protocolVersion: z8.literal(PROTOCOL_VERSION),
1343
+ runnerId: z8.string().min(1),
1344
+ daemonVersion: z8.string().min(1),
508
1345
  capabilities: CapabilityMatrix,
509
- /** Jobs this daemon believes it holds; the server renews their leases. */
510
- activeJobIds: z5.array(z5.string().min(1)),
1346
+ /**
1347
+ * Leases this daemon believes it holds; the server renews exactly these.
1348
+ *
1349
+ * Lease ids rather than job ids, so a replayed heartbeat cannot renew a
1350
+ * grant the runner no longer holds — see {@link Lease.id}.
1351
+ */
1352
+ activeLeases: z8.array(
1353
+ z8.object({ jobId: z8.string().min(1), leaseId: z8.string().min(1) })
1354
+ ),
511
1355
  /** True while the owner has the daemon paused; the server stops offering work. */
512
- paused: z5.boolean()
1356
+ paused: z8.boolean()
513
1357
  }).strict();
514
- var HeartbeatResponse = z5.object({
1358
+ var HeartbeatResponse = z8.object({
515
1359
  /** Once true, the daemon stops claiming and abandons in-flight work. */
516
- revoked: z5.boolean(),
1360
+ revoked: z8.boolean(),
517
1361
  /**
518
1362
  * Per-job cancel (byollm_001 Rev 1 §C). The daemon aborts these jobs'
519
1363
  * in-flight backend calls and reports them `canceled`.
520
1364
  */
521
- cancel: z5.array(z5.string().min(1)),
522
- /** Jobs whose leases were renewed, with their new expiry. */
523
- leases: z5.array(
524
- z5.object({
525
- jobId: z5.string().min(1),
526
- expiresAt: z5.number().int().positive()
527
- }).strict()
528
- ),
1365
+ cancel: z8.array(z8.string().min(1)),
1366
+ // `leases` is deliberately absent cloud_008 §1.4b, finding 16.
1367
+ //
1368
+ // It carried "these leases were renewed, and here is the new expiry", and
1369
+ // **no daemon ever read it.** A mutation returning an empty list while
1370
+ // renewing correctly survived every test, which is what made it visible.
1371
+ //
1372
+ // It is neither a class nor membership, so Amendment A's rule does not
1373
+ // decide it — the older test does: nothing reads it, so it is dead wire.
1374
+ // §6's exhaustiveness is a commitment about what an upstream can see, and
1375
+ // it applies to every message rather than only to the stub.
1376
+ //
1377
+ // `lost` is the actionable signal and always was: a daemon stops work on
1378
+ // a lease it no longer holds. "Renewed" was the same question answered a
1379
+ // second time, and a second answer can only agree or contradict.
1380
+ //
1381
+ // Renewal itself is untouched — the upstream still extends the grants a
1382
+ // heartbeat names, which is what §0.6 fixed. What ended is telling the
1383
+ // daemon about it in a field it ignored. If an upstream ever needs to
1384
+ // push lease decisions, that is a new field with a reader, added on
1385
+ // purpose.
529
1386
  /**
530
1387
  * Jobs the daemon thinks it holds but the server has reassigned or
531
1388
  * expired. The daemon must stop work on these and not report results.
532
1389
  */
533
- lost: z5.array(z5.string().min(1)),
1390
+ lost: z8.array(z8.string().min(1)),
534
1391
  /** Server clock, so a daemon with a skewed clock still honors leases. */
535
- serverTime: z5.number().int().positive()
1392
+ serverTime: z8.number().int().positive()
536
1393
  }).strict();
537
- var ResultRequest = z5.object({
538
- protocolVersion: z5.literal(PROTOCOL_VERSION),
539
- runnerId: z5.string().min(1),
540
- jobId: z5.string().min(1),
541
- outcome: JobOutcome,
542
- /** Which model actually served it, for the result's provenance. */
543
- model: z5.string().min(1),
544
- backendClass: BackendClass,
545
- /** Wall-clock milliseconds the backend call took. */
546
- durationMs: z5.number().int().nonnegative()
1394
+ var ResultDisposition = z8.enum(["ok", "error", "canceled"]);
1395
+ var ResultRequest = z8.object({
1396
+ protocolVersion: z8.literal(PROTOCOL_VERSION),
1397
+ runnerId: z8.string().min(1),
1398
+ jobId: z8.string().min(1),
1399
+ /**
1400
+ * The grant this result was produced under — cloud_008 §1.4a.
1401
+ *
1402
+ * `fetch` has always named its lease, with the reasoning written beside
1403
+ * it: a request that names only the job would be answerable for whatever
1404
+ * lease exists when it arrives. **The operation that writes the result did
1405
+ * not**, on either plane, and checked only the runner id — which survives
1406
+ * a claim-release-reclaim cycle, so a device whose grant had been swept
1407
+ * and reissued could still land a result for a job it no longer held.
1408
+ *
1409
+ * Found by tracing a mutation that survived in §0.6: the lease lapsed, the
1410
+ * sweep requeued, the daemon re-claimed under a new grant, and the
1411
+ * original run finished and posted anyway. The relay marked the job done
1412
+ * with a result the site cannot open — it verifies the envelope against
1413
+ * the *current* holder's device, so the crypto contains the substitution —
1414
+ * and then refused the real holder's result as a replay. A lost job, in
1415
+ * silence.
1416
+ *
1417
+ * `LEASE_HONORED` is a statement about a lease *instance*. That was
1418
+ * learned once already, when a replayed release yanked a later grant, and
1419
+ * it applies here for the same reason.
1420
+ */
1421
+ leaseId: z8.string().min(1),
1422
+ /**
1423
+ * The outcome, sealed to the site and signed by the device.
1424
+ *
1425
+ * The return leg of the payload envelope, and sealed for the same reason:
1426
+ * a model's answer is as sensitive as the prompt that produced it, and an
1427
+ * intermediary that cannot read one must not be handed the other.
1428
+ */
1429
+ envelope: SealedEnvelope,
1430
+ /**
1431
+ * The sealed outcome's discriminator, in the clear.
1432
+ *
1433
+ * Checked against the envelope once opened. It is a routing hint, not a
1434
+ * fact: believing it unverified would let a daemon mark a job `ok` while
1435
+ * sealing an error, and only the app would ever find out.
1436
+ */
1437
+ disposition: ResultDisposition
1438
+ // `model`, `backendClass` and `durationMs` are **inside the envelope** —
1439
+ // cloud_008 §2.5. See {@link RunMetadata}.
1440
+ //
1441
+ // They were here, in the clear, and that was two problems wearing one
1442
+ // coat. On the direct plane the site recorded unauthenticated fields
1443
+ // beside an authenticated answer: a daemon could seal one result and
1444
+ // declare a different model, and only the unsigned half would reach the
1445
+ // app. Through a relay they reached a third party that acts on none of
1446
+ // them — `model` in particular being the sort of detail Amendment A's
1447
+ // rule keeps off the wire.
1448
+ //
1449
+ // `disposition` stays, and the difference is the test: a relay *routes*
1450
+ // on it, so it is a class a routing party consumes. Nobody between the
1451
+ // two ends consumes these.
547
1452
  }).strict();
548
- var ResultResponse = z5.object({
1453
+ var ResultResponse = z8.object({
549
1454
  /**
550
- * False when the submission lost an idempotency race or the lease was
551
- * already gone the daemon should discard, not retry
552
- * ({@link MUSTS.RESULT_IDEMPOTENT}).
1455
+ * False when this submission wrote nothing the daemon should discard,
1456
+ * not retry ({@link MUSTS.RESULT_IDEMPOTENT}).
553
1457
  */
554
- accepted: z5.boolean(),
1458
+ accepted: z8.boolean(),
1459
+ /**
1460
+ * True when this device had already recorded this job's result.
1461
+ *
1462
+ * The difference between "already recorded" and "you no longer hold this"
1463
+ * — cloud_008 §3.6. A daemon whose acknowledgment was lost is in the first
1464
+ * case and needs to hear it: its answer is safely on disk. Reporting a
1465
+ * stale lease instead invents a worry about a result that is already
1466
+ * stored, and sends its owner looking for a routing problem.
1467
+ *
1468
+ * Set only for the device that finished the job. A different device gets
1469
+ * the same refusal it would get for a job that is *not* terminal, so a job
1470
+ * id cannot be used as a terminality probe.
1471
+ */
1472
+ duplicate: z8.boolean().optional(),
555
1473
  /** The job's state after this submission. */
556
- state: z5.string().min(1)
1474
+ state: z8.string().min(1)
557
1475
  }).strict();
558
- var ReleaseRequest = z5.object({
559
- protocolVersion: z5.literal(PROTOCOL_VERSION),
560
- runnerId: z5.string().min(1),
561
- jobIds: z5.array(z5.string().min(1)),
1476
+ var ReleaseRequest = z8.object({
1477
+ protocolVersion: z8.literal(PROTOCOL_VERSION),
1478
+ runnerId: z8.string().min(1),
1479
+ /**
1480
+ * Which leases to release — the grant, not just the job.
1481
+ *
1482
+ * A release naming only a job id releases whatever lease exists at the
1483
+ * moment it arrives, which for a replayed request is not the lease the
1484
+ * daemon meant. See {@link Lease.id}.
1485
+ */
1486
+ leases: z8.array(
1487
+ z8.object({ jobId: z8.string().min(1), leaseId: z8.string().min(1) })
1488
+ ),
562
1489
  /**
563
1490
  * Why, so the app's runner list can say something true.
564
1491
  *
@@ -568,42 +1495,129 @@ var ReleaseRequest = z5.object({
568
1495
  * stop offering that job to that runner, or the pair would spin between
569
1496
  * claim and release forever.
570
1497
  */
571
- reason: z5.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
1498
+ reason: z8.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
572
1499
  }).strict();
573
- var ReleaseResponse = z5.object({
574
- released: z5.array(z5.string().min(1))
1500
+ var ReleaseResponse = z8.object({
1501
+ released: z8.array(z8.string().min(1))
575
1502
  }).strict();
576
- var WireErrorCode = z5.enum([
1503
+ var WireErrorCode = z8.enum([
577
1504
  "bad-request",
578
1505
  "unsupported-protocol-version",
1506
+ // "We do not know who you are." Exactly 401, and only that — cloud_008
1507
+ // §1.4d.
579
1508
  "unauthorized",
1509
+ /**
1510
+ * "We know exactly who you are, and the answer is no." Exactly 403.
1511
+ *
1512
+ * Five refusals across both planes served 403 with `unauthorized`, whose
1513
+ * table entry is 401: a revoked device, a site claiming another site's
1514
+ * stub, a job you do not hold, a device belonging to another owner, a
1515
+ * relay that does not route for you. Every one of them is an *identified*
1516
+ * caller being refused.
1517
+ *
1518
+ * Collapsing the two loses a distinction that matters everywhere it is
1519
+ * read: a revoked daemon would look like an unsigned one in every log and
1520
+ * every client branch, and "check your keys" is the wrong advice for both
1521
+ * of them in opposite directions.
1522
+ */
1523
+ "forbidden",
580
1524
  "revoked",
581
1525
  "not-found",
1526
+ // Claimed, but the site has not sealed the payload yet — cloud_008 §1.4.
1527
+ //
1528
+ // A daemon must retry rather than abandon: the job is legitimately still
1529
+ // its own until the lease or the awaiting-payload clock says otherwise.
1530
+ // That is why it cannot be `not-found` or `server-error`, and why it was
1531
+ // the protocol gap that produced a bare 409 in the first place.
1532
+ "not-ready",
1533
+ // The caller's clock is too far from ours to judge a signature's freshness.
1534
+ //
1535
+ // Split out from `unauthorized` because the remedy is completely different
1536
+ // and only the server can tell them apart: a bad signature means the key is
1537
+ // wrong, this means the machine's time is wrong. A daemon reporting it as a
1538
+ // generic rejection sends its owner looking at their network.
1539
+ "clock-skew",
582
1540
  "rate-limited",
583
1541
  "server-error"
584
1542
  ]);
585
- var WireError = z5.object({
1543
+ var WireError = z8.object({
586
1544
  error: WireErrorCode,
587
- message: z5.string().min(1),
1545
+ message: z8.string().min(1),
588
1546
  /** Seconds; mirrors Retry-After for `rate-limited` and `server-error`. */
589
- retryAfter: z5.number().int().nonnegative().optional()
590
- }).strict();
1547
+ retryAfter: z8.number().int().nonnegative().optional(),
1548
+ /**
1549
+ * The server's clock, and the window it allows. `clock-skew` only.
1550
+ *
1551
+ * So the far side can say *how far off* rather than *that something is
1552
+ * wrong* — the difference between "adjust your clock by four minutes" and
1553
+ * "something is wrong with your connection". Not a disclosure: the
1554
+ * heartbeat response returns the same value, and so does every `Date`
1555
+ * header.
1556
+ */
1557
+ serverTime: z8.number().int().positive().optional(),
1558
+ maxSkewMs: z8.number().int().positive().optional()
1559
+ }).strict().superRefine((error, ctx) => {
1560
+ const skew = error.error === "clock-skew";
1561
+ const carried = error.serverTime !== void 0 || error.maxSkewMs !== void 0;
1562
+ if (skew && !carried) {
1563
+ ctx.addIssue({
1564
+ code: "custom",
1565
+ message: "clock-skew must carry serverTime and maxSkewMs"
1566
+ });
1567
+ }
1568
+ if (!skew && carried) {
1569
+ ctx.addIssue({
1570
+ code: "custom",
1571
+ message: `${error.error} must not carry serverTime or maxSkewMs`
1572
+ });
1573
+ }
1574
+ });
591
1575
  var ERROR_STATUS = Object.freeze({
592
1576
  "bad-request": 400,
593
1577
  "unsupported-protocol-version": 400,
594
1578
  unauthorized: 401,
1579
+ forbidden: 403,
595
1580
  revoked: 403,
596
1581
  "not-found": 404,
1582
+ // 409, not 404: the job exists and is yours, it is simply not ready.
1583
+ "not-ready": 409,
1584
+ // 401 alongside `unauthorized`, because that is what it is — the
1585
+ // signature could not be judged. The code is what carries the remedy.
1586
+ "clock-skew": 401,
597
1587
  "rate-limited": 429,
598
1588
  "server-error": 500
599
1589
  });
1590
+ var FetchRequest = z8.object({
1591
+ protocolVersion: z8.string().min(1),
1592
+ runnerId: z8.string().min(1),
1593
+ jobId: z8.string().min(1),
1594
+ /**
1595
+ * The grant this daemon holds.
1596
+ *
1597
+ * Named, not inferred: a fetch is lease-scoped, and a request that names
1598
+ * only the job would be answerable for whatever lease exists when it
1599
+ * arrives ({@link Lease.id}).
1600
+ */
1601
+ leaseId: z8.string().min(1)
1602
+ }).strict();
1603
+ var FetchResponse = z8.object({
1604
+ /**
1605
+ * The work, sealed to the device that claimed it — byollm_009 §6.
1606
+ *
1607
+ * Not plaintext. The site opens its own at-rest envelope and re-seals to
1608
+ * the claiming device's key, signed by the site's identity, so the work
1609
+ * is readable only by the machine that took it and only if it came from
1610
+ * the site that machine pinned.
1611
+ */
1612
+ envelope: SealedEnvelope
1613
+ }).strict();
600
1614
  export {
601
1615
  AUDIENCES,
602
1616
  Audience,
603
1617
  BACKENDS,
604
1618
  BACKEND_IDS,
605
- BackendAccount,
606
1619
  BackendClass,
1620
+ BackendCost,
607
1621
  BackendIdSchema,
608
1622
  Capability,
609
1623
  CapabilityMatrix,
@@ -612,9 +1626,14 @@ export {
612
1626
  ClaimRequest,
613
1627
  ClaimResponse,
614
1628
  ClaimedJob,
1629
+ ClaimedStub,
615
1630
  DeliveredResult,
616
1631
  ENDPOINTS,
1632
+ ENVELOPE_MAX_AGE_MS,
617
1633
  ERROR_STATUS,
1634
+ EnvelopeDirection,
1635
+ FetchRequest,
1636
+ FetchResponse,
618
1637
  GeneratePayload,
619
1638
  HeartbeatRequest,
620
1639
  HeartbeatResponse,
@@ -626,8 +1645,11 @@ export {
626
1645
  JobResultError,
627
1646
  JobResultOk,
628
1647
  JobState,
1648
+ JobStub,
629
1649
  KindedPayload,
630
1650
  Lease,
1651
+ MAX_CLOCK_SKEW_MS,
1652
+ MIN_PROTOCOL_VERSION,
631
1653
  MUSTS,
632
1654
  MUST_IDS,
633
1655
  MatchRefusal,
@@ -641,23 +1663,54 @@ export {
641
1663
  PairRequest,
642
1664
  PairStartRequest,
643
1665
  PairStartResponse,
1666
+ PublicIdentity,
644
1667
  REFUSAL_MESSAGES,
645
1668
  ReleaseRequest,
646
1669
  ReleaseResponse,
1670
+ RequestSignature,
1671
+ ResultDisposition,
647
1672
  ResultProvenance,
648
1673
  ResultRequest,
649
1674
  ResultResponse,
1675
+ RunMetadata,
1676
+ SIZE_CLASS_LIMITS,
1677
+ SUPPORTED_PROTOCOL_VERSIONS,
1678
+ SealedEnvelope,
1679
+ SealedOutcome,
1680
+ SizeClass,
1681
+ StoredKeys,
650
1682
  TERMINAL_STATES,
651
1683
  WireError,
652
1684
  WireErrorCode,
653
1685
  backendDescriptor,
654
1686
  canTransition,
1687
+ canonicalRequest,
1688
+ checkProtocolVersion,
1689
+ cryptoReady,
655
1690
  effectiveOfferScope,
1691
+ fingerprint,
1692
+ generateKeys,
656
1693
  isBackendId,
657
1694
  isJobKind,
1695
+ isLocalHost,
658
1696
  isTerminal,
1697
+ keyId,
659
1698
  matchAudience,
1699
+ mustsVerifiedBy,
1700
+ open,
660
1701
  payloadTextLength,
661
- provenanceFor
1702
+ provenanceFor,
1703
+ publicIdentityOf,
1704
+ resolveCost,
1705
+ seal,
1706
+ signRequest,
1707
+ signSiteRequest,
1708
+ signWith,
1709
+ sizeClassCeiling,
1710
+ sizeClassOf,
1711
+ verifyPublicIdentity,
1712
+ verifyRequest,
1713
+ verifySiteRequest,
1714
+ verifyWith
662
1715
  };
663
1716
  //# sourceMappingURL=index.js.map