@byollm/protocol 0.1.0-alpha.3 → 0.1.0-alpha.31

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,6 +2,7 @@
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
8
  var BackendCost = z.enum(["free", "metered", "subscription"]);
@@ -177,13 +178,16 @@ function backendDescriptor(id) {
177
178
  function isLocalHost(hostname) {
178
179
  const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
179
180
  if (host === "localhost" || host.endsWith(".localhost")) return true;
180
- if (host === "::1") 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
+ }
181
187
  if (host.startsWith("127.")) return true;
182
188
  if (host.startsWith("10.")) return true;
183
189
  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;
190
+ return /^172\.(1[6-9]|2\d|3[01])\./.test(host);
187
191
  }
188
192
  function resolveCost(id, baseUrl) {
189
193
  const declared = BACKENDS[id].cost;
@@ -290,11 +294,21 @@ var ChatMessage = z3.object({
290
294
  var GeneratePayload = z3.object({
291
295
  prompt: z3.string().min(1).max(PAYLOAD_LIMITS.maxTextChars),
292
296
  system: z3.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
293
- }).strict();
297
+ }).strict().refine(
298
+ (payload) => payload.prompt.length + (payload.system?.length ?? 0) <= PAYLOAD_LIMITS.maxTotalChars,
299
+ {
300
+ message: `payload exceeds ${String(PAYLOAD_LIMITS.maxTotalChars)} characters`
301
+ }
302
+ );
294
303
  var ChatPayload = z3.object({
295
304
  messages: z3.array(ChatMessage).min(1).max(PAYLOAD_LIMITS.maxMessages),
296
305
  system: z3.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
297
- }).strict();
306
+ }).strict().refine(
307
+ (payload) => payload.messages.reduce((sum, m) => sum + m.content.length, 0) + (payload.system?.length ?? 0) <= PAYLOAD_LIMITS.maxTotalChars,
308
+ {
309
+ message: `payload exceeds ${String(PAYLOAD_LIMITS.maxTotalChars)} characters`
310
+ }
311
+ );
298
312
  var JobKind = z3.enum(["llm.generate", "llm.chat"]);
299
313
  var JOB_KINDS = Object.freeze(JobKind.options);
300
314
  var KindedPayload = z3.discriminatedUnion("kind", [
@@ -350,6 +364,21 @@ function canTransition(from, to) {
350
364
  return TRANSITIONS[from].includes(to);
351
365
  }
352
366
  var Lease = z4.object({
367
+ /**
368
+ * Identifies *this* grant, not just its holder.
369
+ *
370
+ * A runner can hold a job, release it, and claim it again — three leases,
371
+ * one runner id. Without an id for the grant itself, a lease-scoped request
372
+ * names a mutable target ambiguously, and a replayed release from the first
373
+ * grant lands on the third: the job returns to the queue while the daemon
374
+ * is mid-execution, and the work runs twice on the owner's hardware.
375
+ *
376
+ * That was a live hole, found in review after signed requests shipped. The
377
+ * signature scheme's replay argument rests on endpoints being idempotent —
378
+ * and release *is*, per lease, but not across leases, because nothing in
379
+ * the request said which one.
380
+ */
381
+ id: z4.string().min(1),
353
382
  /** The runner holding the lease. */
354
383
  runnerId: z4.string().min(1),
355
384
  /** Epoch milliseconds after which the claim is void. */
@@ -363,8 +392,19 @@ var ClaimedJob = z4.object({
363
392
  audience: Audience,
364
393
  /** The app's id for the user who enqueued it. */
365
394
  owner: z4.string().min(1),
366
- /** Runner owners the app restricted a `named` job to, if any. */
367
- audienceAllow: z4.array(z4.string().min(1)).optional(),
395
+ /**
396
+ * Which site's job — V1-3.
397
+ *
398
+ * The stub has always carried it; the opened job did not, so everything
399
+ * downstream of the payload — the ingress line above all — recorded a job
400
+ * id that belongs to a site without saying which. Two sites can choose
401
+ * the same id, and the meter is the product.
402
+ *
403
+ * Optional so a caller assembling a job by hand is not forced to invent
404
+ * one, and so this reads as what it is: a fact about where the work came
405
+ * from, not a second copy of the routing key.
406
+ */
407
+ site: z4.string().min(1).optional(),
368
408
  lease: Lease
369
409
  }).strict();
370
410
  var ResultProvenance = z4.object({
@@ -394,6 +434,13 @@ function provenanceFor(input) {
394
434
  untrusted: input.audience !== "self"
395
435
  };
396
436
  }
437
+ var RunMetadata = z4.object({
438
+ /** Which model actually served it. */
439
+ model: z4.string().min(1),
440
+ backendClass: BackendClass,
441
+ /** Wall-clock milliseconds the backend call took. */
442
+ durationMs: z4.number().int().nonnegative()
443
+ }).strict();
397
444
  var JobResultOk = z4.object({
398
445
  outcome: z4.literal("ok"),
399
446
  text: z4.string(),
@@ -415,14 +462,403 @@ var JobOutcome = z4.discriminatedUnion("outcome", [
415
462
  JobResultError,
416
463
  JobResultCanceled
417
464
  ]);
465
+ var SealedOutcome = z4.object({ outcome: JobOutcome, ran: RunMetadata }).strict();
418
466
  var DeliveredResult = z4.object({
419
467
  jobId: z4.string().min(1),
420
468
  state: JobState,
421
469
  outcome: JobOutcome.optional(),
422
470
  provenance: ResultProvenance.optional()
423
471
  }).strict();
472
+ var SizeClass = z4.enum(["small", "medium", "large", "unbounded"]);
473
+ var SIZE_CLASS_LIMITS = Object.freeze({
474
+ small: 4e3,
475
+ medium: 64e3,
476
+ large: Number.POSITIVE_INFINITY
477
+ });
478
+ function sizeClassCeiling(sizeClass) {
479
+ if (sizeClass === "unbounded") return Number.POSITIVE_INFINITY;
480
+ return SIZE_CLASS_LIMITS[sizeClass];
481
+ }
482
+ function sizeClassOf(textChars) {
483
+ if (textChars <= SIZE_CLASS_LIMITS.small) return "small";
484
+ if (textChars <= SIZE_CLASS_LIMITS.medium) return "medium";
485
+ return "large";
486
+ }
487
+ var JobStub = z4.object({
488
+ id: z4.string().min(1),
489
+ kind: JobKind,
490
+ /** The app's id for the user who enqueued it. */
491
+ owner: z4.string().min(1),
492
+ /**
493
+ * Which site this job belongs to — byollm_009 Amendment A §A.3.
494
+ *
495
+ * **The site's identity key id**, not an id somebody assigned it. §6 has
496
+ * listed `site` since this spec was frozen; the schema never carried it,
497
+ * which is the drift the amendment closes.
498
+ *
499
+ * A key id rather than an opaque handle for one reason above the others:
500
+ * it makes the stub *self-describing* instead of a pointer into somebody
501
+ * else's table. A daemon holds this key id already, from pinning, so it
502
+ * can check `stub.site` against the payload envelope's `senderKeyId`
503
+ * without a lookup and without trusting the party that routed it. An
504
+ * opaque id can only be believed.
505
+ *
506
+ * It also avoids inventing a second namespace for a thing that has a
507
+ * canonical one — the shape of finding 41 (two owner namespaces compared
508
+ * for equality) and of finding fourteen before it.
509
+ *
510
+ * Rotation is a designed transition rather than a cost: a site publishes a
511
+ * new identity signed by the outgoing one, both are valid through an
512
+ * overlap window, and a daemon re-keys its own map by verifying that
513
+ * signature against the key it already pinned (§A.3.1).
514
+ */
515
+ site: z4.string().min(1),
516
+ audience: Audience,
517
+ // `audienceAllow` is **not** here, and its absence is the enforcement —
518
+ // cloud_008 §0.2.
519
+ //
520
+ // It was a list of the people who may run a job, travelling to every
521
+ // routing party on every `named` job. byollm_001 Rev 1 §B settled who
522
+ // decides that long before this schema existed: *the daemon's own list
523
+ // decides, not the server's*, and `allowlist.predicateFor(origin)` is the
524
+ // enforcement in both lanes. So this was a second answer to a question the
525
+ // daemon already owned — able only to agree, in which case it was
526
+ // redundant, or to disagree, in which case nothing said which wins.
527
+ //
528
+ // The rule it leaves behind, which decides the next field too: **a class
529
+ // the router acts on may travel; membership never does.** `audience` stays
530
+ // for exactly that reason — the relay narrows on it. A roster does not
531
+ // travel, so `ROSTER_NOT_DISCLOSED` holds here by absence, which is the
532
+ // strongest way for a MUST to hold.
533
+ //
534
+ // The site keeps its own copy on `JobRecord` and still filters candidates
535
+ // with it before offering. That is server-internal, where the party
536
+ // holding the list authored it.
537
+ sizeClass: SizeClass,
538
+ /** Reserved for byollm_006. False until streaming exists. */
539
+ streaming: z4.boolean(),
540
+ /** Epoch ms after which the work is pointless; bounds ciphertext retention. */
541
+ deadlineAt: z4.number().int().positive()
542
+ }).strict();
543
+ var ClaimedStub = JobStub.extend({ lease: Lease }).strict();
544
+
545
+ // src/envelope.ts
546
+ import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2 } from "crypto";
547
+ import sodium from "libsodium-wrappers";
548
+ import { z as z6 } from "zod";
549
+
550
+ // src/keys.ts
551
+ import {
552
+ createHash,
553
+ createPrivateKey,
554
+ createPublicKey,
555
+ generateKeyPairSync,
556
+ sign,
557
+ verify
558
+ } from "crypto";
559
+ import { z as z5 } from "zod";
560
+ var PublicIdentity = z5.object({
561
+ /** Raw Ed25519 public key. The pinned one. */
562
+ identity: z5.string().min(1),
563
+ /** Raw X25519 public key, for sealing to this party. */
564
+ encryption: z5.string().min(1),
565
+ /**
566
+ * Ed25519 signature over the encryption key, by the identity key.
567
+ *
568
+ * This is what stops an upstream substituting an encryption key of its
569
+ * own while relaying a genuine identity: the receiver pins the identity
570
+ * and refuses any encryption key not signed by it.
571
+ */
572
+ encryptionSig: z5.string().min(1)
573
+ }).strict();
574
+ var StoredKeys = z5.object({
575
+ version: z5.literal(1),
576
+ identityPublic: z5.string().min(1),
577
+ identityPrivate: z5.string().min(1),
578
+ encryptionPublic: z5.string().min(1),
579
+ encryptionPrivate: z5.string().min(1),
580
+ encryptionSig: z5.string().min(1),
581
+ createdAt: z5.number().int().positive()
582
+ }).strict();
583
+ var ENCRYPTION_KEY_CONTEXT = "byollm/v1/encryption-key";
584
+ function rawPublic(key) {
585
+ const jwk = key.export({ format: "jwk" });
586
+ const x = jwk.x;
587
+ if (typeof x !== "string") throw new Error("key has no raw public component");
588
+ return x;
589
+ }
590
+ function importPublic(raw, crv) {
591
+ return createPublicKey({ key: { kty: "OKP", crv, x: raw }, format: "jwk" });
592
+ }
593
+ function importPrivate(stored) {
594
+ return createPrivateKey({
595
+ key: Buffer.from(stored, "base64"),
596
+ type: "pkcs8",
597
+ format: "der"
598
+ });
599
+ }
600
+ var exportPrivate = (key) => key.export({ type: "pkcs8", format: "der" }).toString("base64");
601
+ function generateKeys(now) {
602
+ const identity = generateKeyPairSync("ed25519");
603
+ const encryption = generateKeyPairSync("x25519");
604
+ const encryptionPublic = rawPublic(encryption.publicKey);
605
+ return {
606
+ version: 1,
607
+ identityPublic: rawPublic(identity.publicKey),
608
+ identityPrivate: exportPrivate(identity.privateKey),
609
+ encryptionPublic,
610
+ encryptionPrivate: exportPrivate(encryption.privateKey),
611
+ encryptionSig: sign(
612
+ null,
613
+ Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${encryptionPublic}`),
614
+ identity.privateKey
615
+ ).toString("base64url"),
616
+ createdAt: now
617
+ };
618
+ }
619
+ function publicIdentityOf(keys) {
620
+ return {
621
+ identity: keys.identityPublic,
622
+ encryption: keys.encryptionPublic,
623
+ encryptionSig: keys.encryptionSig
624
+ };
625
+ }
626
+ function verifyPublicIdentity(identity) {
627
+ try {
628
+ return verify(
629
+ null,
630
+ Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${identity.encryption}`),
631
+ importPublic(identity.identity, "Ed25519"),
632
+ Buffer.from(identity.encryptionSig, "base64url")
633
+ );
634
+ } catch {
635
+ return false;
636
+ }
637
+ }
638
+ function signWith(keys, data) {
639
+ return sign(null, data, importPrivate(keys.identityPrivate)).toString(
640
+ "base64url"
641
+ );
642
+ }
643
+ function verifyWith(identityPublic, data, signature) {
644
+ try {
645
+ return verify(
646
+ null,
647
+ data,
648
+ importPublic(identityPublic, "Ed25519"),
649
+ Buffer.from(signature, "base64url")
650
+ );
651
+ } catch {
652
+ return false;
653
+ }
654
+ }
655
+ var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
656
+ function fingerprint(identityPublic) {
657
+ const digest = createHash("sha256").update(Buffer.from(identityPublic, "base64url")).digest();
658
+ let bits = 0;
659
+ let value = 0;
660
+ let out = "";
661
+ for (const byte of digest.subarray(0, 15)) {
662
+ value = value << 8 | byte;
663
+ bits += 8;
664
+ while (bits >= 5) {
665
+ out += ALPHABET.charAt(value >>> bits - 5 & 31);
666
+ bits -= 5;
667
+ }
668
+ }
669
+ const groups = out.match(/.{1,4}/g) ?? [];
670
+ return `BYOLLM-${groups.join("-")}`;
671
+ }
672
+ var keyId = (identityPublic) => fingerprint(identityPublic);
673
+
674
+ // src/envelope.ts
675
+ var readied;
676
+ async function cryptoReady() {
677
+ readied ??= sodium.ready;
678
+ await readied;
679
+ }
680
+ var ENVELOPE_MAX_AGE_MS = 24 * 60 * 6e4;
681
+ var EnvelopeDirection = z6.enum(["payload", "result"]);
682
+ var SealedEnvelope = z6.object({
683
+ /** Base64url `crypto_box_seal` output over the signed plaintext. */
684
+ ciphertext: z6.string().min(1),
685
+ /** Who this was sealed to — the recipient checks it is them. */
686
+ recipientKeyId: z6.string().min(1),
687
+ /** Who signed it — the recipient checks this against its pin. */
688
+ senderKeyId: z6.string().min(1),
689
+ direction: EnvelopeDirection,
690
+ /**
691
+ * When this ciphertext stops being worth keeping.
692
+ *
693
+ * Carried *on* the envelope rather than recomputed by the opener. An
694
+ * earlier version derived it from the job's creation time, which meant
695
+ * two systems had to agree on a timestamp to the millisecond — and they
696
+ * did not, once a real database rounded it. A bound value that has to be
697
+ * reconstructed is a bound value that eventually is not.
698
+ *
699
+ * Not trusted as written: it is also inside the signature, so a changed
700
+ * deadline fails to verify.
701
+ */
702
+ deadlineAt: z6.number().int().positive()
703
+ }).strict();
704
+ function signedBody(context, plaintext) {
705
+ return Buffer.from(
706
+ JSON.stringify({
707
+ v: "byollm/v1/envelope",
708
+ jobId: context.jobId,
709
+ senderKeyId: context.senderKeyId,
710
+ recipientKeyId: context.recipientKeyId,
711
+ deadlineAt: context.deadlineAt,
712
+ direction: context.direction,
713
+ plaintext
714
+ }),
715
+ "utf8"
716
+ );
717
+ }
718
+ var rawX25519 = (key, part) => {
719
+ const jwk = key.export({ format: "jwk" });
720
+ const value = part === "x" ? jwk.x : jwk.d;
721
+ if (typeof value !== "string") throw new Error("not an X25519 key");
722
+ return new Uint8Array(Buffer.from(value, "base64url"));
723
+ };
724
+ async function seal(input) {
725
+ await cryptoReady();
726
+ const body = signedBody(input.context, input.plaintext);
727
+ const signature = signWith(input.senderKeys, body);
728
+ const inner = JSON.stringify({ body: body.toString("base64url"), signature });
729
+ const recipient = new Uint8Array(
730
+ Buffer.from(input.recipientEncryptionPublic, "base64url")
731
+ );
732
+ const ciphertext = sodium.crypto_box_seal(
733
+ new Uint8Array(Buffer.from(inner, "utf8")),
734
+ recipient
735
+ );
736
+ return {
737
+ ciphertext: Buffer.from(ciphertext).toString("base64url"),
738
+ recipientKeyId: input.context.recipientKeyId,
739
+ senderKeyId: input.context.senderKeyId,
740
+ direction: input.context.direction,
741
+ deadlineAt: input.context.deadlineAt
742
+ };
743
+ }
744
+ async function open(input) {
745
+ await cryptoReady();
746
+ const { envelope, expected } = input;
747
+ if (envelope.recipientKeyId !== expected.recipientKeyId || envelope.senderKeyId !== expected.senderKeyId || envelope.direction !== expected.direction) {
748
+ return { ok: false, reason: "not-for-us" };
749
+ }
750
+ let inner;
751
+ try {
752
+ const priv = createPrivateKey2({
753
+ key: Buffer.from(input.recipientKeys.encryptionPrivate, "base64"),
754
+ type: "pkcs8",
755
+ format: "der"
756
+ });
757
+ const pub = createPublicKey2(priv);
758
+ const opened = sodium.crypto_box_seal_open(
759
+ new Uint8Array(Buffer.from(envelope.ciphertext, "base64url")),
760
+ rawX25519(pub, "x"),
761
+ rawX25519(priv, "d")
762
+ );
763
+ inner = Buffer.from(opened).toString("utf8");
764
+ } catch {
765
+ return { ok: false, reason: "unopenable" };
766
+ }
767
+ let parsed;
768
+ try {
769
+ parsed = JSON.parse(inner);
770
+ } catch {
771
+ return { ok: false, reason: "malformed" };
772
+ }
773
+ if (typeof parsed.body !== "string" || typeof parsed.signature !== "string") {
774
+ return { ok: false, reason: "malformed" };
775
+ }
776
+ const body = Buffer.from(parsed.body, "base64url");
777
+ if (!verifyWith(input.senderIdentityPublic, body, parsed.signature)) {
778
+ return { ok: false, reason: "bad-signature" };
779
+ }
780
+ let claims;
781
+ try {
782
+ claims = JSON.parse(body.toString("utf8"));
783
+ } catch {
784
+ return { ok: false, reason: "malformed" };
785
+ }
786
+ if (claims["jobId"] !== expected.jobId || claims["senderKeyId"] !== expected.senderKeyId || claims["recipientKeyId"] !== expected.recipientKeyId || claims["deadlineAt"] !== envelope.deadlineAt || claims["direction"] !== expected.direction) {
787
+ return { ok: false, reason: "context-mismatch" };
788
+ }
789
+ if (typeof claims["plaintext"] !== "string") {
790
+ return { ok: false, reason: "malformed" };
791
+ }
792
+ return { ok: true, plaintext: claims["plaintext"] };
793
+ }
794
+
795
+ // src/signing.ts
796
+ import { createHash as createHash2 } from "crypto";
797
+ import { z as z7 } from "zod";
798
+ var MAX_CLOCK_SKEW_MS = 12e4;
799
+ var RequestSignature = z7.object({
800
+ /** Which runner is calling. The server looks up its pinned identity. */
801
+ runnerId: z7.string().min(1),
802
+ /** Epoch ms, bounded by {@link MAX_CLOCK_SKEW_MS}. */
803
+ issuedAt: z7.number().int().positive(),
804
+ /** Base64url Ed25519 signature over {@link canonicalRequest}. */
805
+ signature: z7.string().min(1)
806
+ }).strict();
807
+ function canonicalRequest(input) {
808
+ const digest = createHash2("sha256").update(input.body, "utf8").digest("hex");
809
+ return Buffer.from(
810
+ [
811
+ "byollm/v1/request",
812
+ input.endpoint,
813
+ input.runnerId,
814
+ String(input.issuedAt),
815
+ digest
816
+ ].join("\n"),
817
+ "utf8"
818
+ );
819
+ }
820
+ function signRequest(keys, input) {
821
+ return {
822
+ runnerId: input.runnerId,
823
+ issuedAt: input.issuedAt,
824
+ signature: signWith(keys, canonicalRequest(input))
825
+ };
826
+ }
827
+ function signSiteRequest(keys, input) {
828
+ return signRequest(keys, {
829
+ endpoint: siteEndpoint(input.endpoint),
830
+ runnerId: input.siteId,
831
+ issuedAt: input.issuedAt,
832
+ body: input.body
833
+ });
834
+ }
835
+ function verifySiteRequest(input) {
836
+ return verifyRequest({
837
+ ...input,
838
+ endpoint: siteEndpoint(input.endpoint)
839
+ });
840
+ }
841
+ var siteEndpoint = (endpoint) => `site/${endpoint}`;
842
+ function verifyRequest(input) {
843
+ const skew = input.maxSkewMs ?? MAX_CLOCK_SKEW_MS;
844
+ if (Math.abs(input.now - input.signature.issuedAt) > skew) return "stale";
845
+ const ok = verifyWith(
846
+ input.identityPublic,
847
+ canonicalRequest({
848
+ endpoint: input.endpoint,
849
+ runnerId: input.signature.runnerId,
850
+ issuedAt: input.signature.issuedAt,
851
+ body: input.body
852
+ }),
853
+ input.signature.signature
854
+ );
855
+ return ok ? null : "bad-signature";
856
+ }
424
857
 
425
858
  // src/musts.ts
859
+ function kindsOf(must2) {
860
+ return typeof must2.verifiedBy === "string" ? [must2.verifiedBy] : must2.verifiedBy;
861
+ }
426
862
  var must = (m) => Object.freeze(m);
427
863
  var MUSTS = Object.freeze({
428
864
  // ---- Pairing and identity -------------------------------------------
@@ -430,31 +866,108 @@ var MUSTS = Object.freeze({
430
866
  id: "PAIR_ONE_USER",
431
867
  statement: "A runner token MUST be bound to exactly one user; a daemon MUST refuse work not attributable to its paired user.",
432
868
  enforcedBy: "both",
869
+ verifiedBy: "conformance",
433
870
  source: "byollm_001 \xA7MUSTs"
434
871
  }),
435
872
  PAIR_INTERACTIVE: must({
436
873
  id: "PAIR_INTERACTIVE",
437
874
  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.",
438
875
  enforcedBy: "server",
876
+ verifiedBy: "conformance",
439
877
  source: "byollm_001 \xA7Endpoints.1"
440
878
  }),
441
879
  PAIR_CODE_EXPIRES: must({
442
880
  id: "PAIR_CODE_EXPIRES",
443
881
  statement: "An unapproved device code MUST expire and MUST NOT be redeemable after expiry.",
444
882
  enforcedBy: "server",
883
+ verifiedBy: "conformance",
445
884
  source: "byollm_001 \xA7Endpoints.1"
446
885
  }),
447
886
  // ---- Typed job kinds --------------------------------------------------
887
+ VERSION_HANDSHAKE_REQUIRED: must({
888
+ id: "VERSION_HANDSHAKE_REQUIRED",
889
+ 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.",
890
+ enforcedBy: "both",
891
+ verifiedBy: "conformance",
892
+ source: "byollm_009 \xA74"
893
+ }),
894
+ SITE_KEY_BY_STUB: must({
895
+ id: "SITE_KEY_BY_STUB",
896
+ 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.",
897
+ enforcedBy: "daemon",
898
+ // Adversarial, and the reason is the finding that produced it: the
899
+ // honest paths pass with every site check deleted, because `open`
900
+ // refuses a signature from the wrong key anyway. What distinguishes an
901
+ // enforced rule from a coincidence here is a hostile pairing of stub and
902
+ // envelope, which no conformance client would ever send.
903
+ verifiedBy: "adversarial",
904
+ source: "byollm_009 \xA7A.3"
905
+ }),
906
+ SITES_LOCALLY_APPROVED: must({
907
+ id: "SITES_LOCALLY_APPROVED",
908
+ statement: "A daemon MUST NOT run work for a site it has not approved on the machine itself. An upstream may propose a site set; a site the daemon has never approved MUST be offered to its owner and served nothing until they approve it. A key that has changed for an already-approved id 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 approved, over a statement naming both key ids MUST be accepted without a new local approval \u2014 provided the control plane projects the same successor \u2014 and MUST be announced rather than applied silently.",
909
+ enforcedBy: "daemon",
910
+ // Two kinds, and the second is the one that matters — V1-1.
911
+ //
912
+ // `construction`: the daemon cannot serve a site that is not in its
913
+ // pinned map, and admission refuses before a payload is fetched, so the
914
+ // ordinary path cannot reach a site nobody approved.
915
+ //
916
+ // `adversarial`: the property that survives is about a *sequence* —
917
+ // remove the id, re-offer it under a different key — which no honest
918
+ // upstream sends and which the fence above does not see. That was the
919
+ // bypass: the pin was deleted with the id, so the comparison had nothing
920
+ // to compare against and the substitution arrived as a stranger.
921
+ verifiedBy: ["construction", "adversarial"],
922
+ source: "byollm_009 \xA7B.2"
923
+ }),
924
+ KEYS_EXCHANGED_AT_CONSENT: must({
925
+ id: "KEYS_EXCHANGED_AT_CONSENT",
926
+ 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.",
927
+ enforcedBy: "both",
928
+ verifiedBy: "conformance",
929
+ source: "byollm_009 \xA75"
930
+ }),
931
+ REQUESTS_SIGNED_NOT_BEARER: must({
932
+ id: "REQUESTS_SIGNED_NOT_BEARER",
933
+ 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.",
934
+ enforcedBy: "both",
935
+ verifiedBy: "conformance",
936
+ source: "byollm_009 \xA74.2"
937
+ }),
938
+ LEASE_SCOPED_BY_GRANT: must({
939
+ id: "LEASE_SCOPED_BY_GRANT",
940
+ 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.",
941
+ enforcedBy: "both",
942
+ verifiedBy: "conformance",
943
+ source: "byollm_009 \xA74.2"
944
+ }),
945
+ STUB_METADATA_EXHAUSTIVE: must({
946
+ id: "STUB_METADATA_EXHAUSTIVE",
947
+ 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.",
948
+ enforcedBy: "both",
949
+ verifiedBy: "conformance",
950
+ source: "byollm_009 \xA76"
951
+ }),
952
+ ENVELOPE_SEALED_AND_SIGNED: must({
953
+ id: "ENVELOPE_SEALED_AND_SIGNED",
954
+ 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.",
955
+ enforcedBy: "server",
956
+ verifiedBy: "conformance",
957
+ source: "byollm_009 \xA76"
958
+ }),
448
959
  KIND_TYPED_ONLY: must({
449
960
  id: "KIND_TYPED_ONLY",
450
961
  statement: "Job kinds MUST resolve against handlers baked into the daemon. A daemon MUST refuse an unknown kind rather than guess.",
451
962
  enforcedBy: "daemon",
963
+ verifiedBy: "conformance",
452
964
  source: "byollm_001 \xA7Jobs are typed data"
453
965
  }),
454
966
  KIND_NO_CODE: must({
455
967
  id: "KIND_NO_CODE",
456
968
  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.",
457
969
  enforcedBy: "daemon",
970
+ verifiedBy: "conformance",
458
971
  source: "byollm_001 \xA7Jobs are typed data; byollm_004 \xA71"
459
972
  }),
460
973
  // ---- Capability and claiming -----------------------------------------
@@ -462,18 +975,21 @@ var MUSTS = Object.freeze({
462
975
  id: "CLAIM_REQUIRES_CAPABILITY",
463
976
  statement: "A daemon MUST NOT be given a job whose kind is absent from its advertised capability matrix.",
464
977
  enforcedBy: "both",
978
+ verifiedBy: "conformance",
465
979
  source: "byollm_001 \xA7MUSTs"
466
980
  }),
467
981
  CAPABILITY_IS_DETECTED: must({
468
982
  id: "CAPABILITY_IS_DETECTED",
469
983
  statement: "An advertised capability matrix MUST be the intersection of owner config and detected, healthy reality \u2014 never config alone.",
470
984
  enforcedBy: "daemon",
985
+ verifiedBy: "conformance",
471
986
  source: "byollm_002 \xA7Routing"
472
987
  }),
473
988
  CLAIM_ATOMIC: must({
474
989
  id: "CLAIM_ATOMIC",
475
990
  statement: "Claiming MUST be atomic: a job MUST NOT be handed to two runners concurrently.",
476
991
  enforcedBy: "server",
992
+ verifiedBy: "conformance",
477
993
  source: "byollm_001 \xA7Endpoints.2"
478
994
  }),
479
995
  // ---- Leases -----------------------------------------------------------
@@ -481,12 +997,14 @@ var MUSTS = Object.freeze({
481
997
  id: "LEASE_HONORED",
482
998
  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.",
483
999
  enforcedBy: "daemon",
1000
+ verifiedBy: "conformance",
484
1001
  source: "byollm_001 \xA7MUSTs"
485
1002
  }),
486
1003
  LEASE_RECLAIMABLE: must({
487
1004
  id: "LEASE_RECLAIMABLE",
488
1005
  statement: "A lease that expires un-renewed MUST make its job claimable again with no loss of the job.",
489
1006
  enforcedBy: "server",
1007
+ verifiedBy: "conformance",
490
1008
  source: "byollm_001 \xA7Endpoints.2"
491
1009
  }),
492
1010
  // ---- Audience and offer scope ----------------------------------------
@@ -494,48 +1012,56 @@ var MUSTS = Object.freeze({
494
1012
  id: "AUDIENCE_BOTH_SIDES",
495
1013
  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.",
496
1014
  enforcedBy: "both",
1015
+ verifiedBy: "conformance",
497
1016
  source: "byollm_001 \xA7The audience model"
498
1017
  }),
499
1018
  SUBSCRIPTION_SELF_LOCK: must({
500
1019
  id: "SUBSCRIPTION_SELF_LOCK",
501
1020
  statement: "A subscription-class backend's offer scope MUST be 'self' and MUST NOT be widened by configuration.",
502
1021
  enforcedBy: "daemon",
1022
+ verifiedBy: "conformance",
503
1023
  source: "byollm_001 \xA7The audience model"
504
1024
  }),
505
1025
  METERED_DEFAULTS_SELF: must({
506
1026
  id: "METERED_DEFAULTS_SELF",
507
1027
  statement: "A metered backend's effective offer scope MUST be 'self' unless the owner has explicitly acknowledged spending money on others' work.",
508
1028
  enforcedBy: "daemon",
1029
+ verifiedBy: "conformance",
509
1030
  source: "byollm_007 \xA74"
510
1031
  }),
511
1032
  METERED_REQUIRES_CEILING: must({
512
1033
  id: "METERED_REQUIRES_CEILING",
513
1034
  statement: "A widened metered backend MUST carry a spend ceiling, and the daemon MUST refuse community work once it is reached.",
514
1035
  enforcedBy: "daemon",
1036
+ verifiedBy: "conformance",
515
1037
  source: "byollm_007 \xA74"
516
1038
  }),
517
1039
  COST_NOT_CONFIGURABLE: must({
518
1040
  id: "COST_NOT_CONFIGURABLE",
519
1041
  statement: "A built-in provider's cost class MUST NOT be overridable by configuration.",
520
1042
  enforcedBy: "daemon",
1043
+ verifiedBy: "conformance",
521
1044
  source: "byollm_007 \xA72"
522
1045
  }),
523
1046
  REMOTE_IS_NEVER_FREE: must({
524
1047
  id: "REMOTE_IS_NEVER_FREE",
525
1048
  statement: "A generic HTTP backend whose base URL is not loopback or private MUST be treated as metered.",
526
1049
  enforcedBy: "daemon",
1050
+ verifiedBy: "conformance",
527
1051
  source: "byollm_007 \xA72"
528
1052
  }),
529
1053
  NAMED_LOCAL_ALLOWLIST: must({
530
1054
  id: "NAMED_LOCAL_ALLOWLIST",
531
1055
  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.",
532
1056
  enforcedBy: "daemon",
1057
+ verifiedBy: "conformance",
533
1058
  source: "byollm_001 Rev 1 \xA7B"
534
1059
  }),
535
1060
  REFUSAL_NOT_REOFFERED: must({
536
1061
  id: "REFUSAL_NOT_REOFFERED",
537
1062
  statement: "A server MUST NOT re-offer a job to a runner that released it with reason 'refused'.",
538
1063
  enforcedBy: "server",
1064
+ verifiedBy: "conformance",
539
1065
  source: "byollm_001 Rev 1 \xA7B (loop resolved in build review)"
540
1066
  }),
541
1067
  // ---- Revocation and cancel -------------------------------------------
@@ -543,12 +1069,14 @@ var MUSTS = Object.freeze({
543
1069
  id: "REVOCATION_HONORED",
544
1070
  statement: "A revoked daemon MUST stop claiming and MUST abandon in-flight work by the next heartbeat at the latest.",
545
1071
  enforcedBy: "daemon",
1072
+ verifiedBy: "conformance",
546
1073
  source: "byollm_001 \xA7MUSTs"
547
1074
  }),
548
1075
  CANCEL_HONORED: must({
549
1076
  id: "CANCEL_HONORED",
550
1077
  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'.",
551
1078
  enforcedBy: "daemon",
1079
+ verifiedBy: "conformance",
552
1080
  source: "byollm_001 Rev 1 \xA7C"
553
1081
  }),
554
1082
  // ---- Lifecycle, dependencies, delivery -------------------------------
@@ -556,37 +1084,43 @@ var MUSTS = Object.freeze({
556
1084
  id: "DEPENDS_ON_GATING",
557
1085
  statement: "A job MUST NOT be claimable until every job in its dependsOn set has reached the 'ok' state.",
558
1086
  enforcedBy: "server",
1087
+ verifiedBy: "conformance",
559
1088
  source: "byollm_001 Rev 1 \xA7E"
560
1089
  }),
561
1090
  TTL_EXPIRY: must({
562
1091
  id: "TTL_EXPIRY",
563
1092
  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.",
564
1093
  enforcedBy: "server",
1094
+ verifiedBy: "conformance",
565
1095
  source: "byollm_001 Rev 1 \xA7D (TTL clock resolved in build review)"
566
1096
  }),
567
1097
  NO_RUNNER_SIGNAL: must({
568
1098
  id: "NO_RUNNER_SIGNAL",
569
1099
  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.",
570
1100
  enforcedBy: "server",
1101
+ verifiedBy: "conformance",
571
1102
  source: "byollm_001 Rev 1 \xA7D"
572
1103
  }),
573
1104
  RESULT_IDEMPOTENT: must({
574
1105
  id: "RESULT_IDEMPOTENT",
575
1106
  statement: "Result submission MUST be idempotent by job id; the first terminal outcome wins and later submissions MUST NOT change it.",
576
1107
  enforcedBy: "server",
1108
+ verifiedBy: "conformance",
577
1109
  source: "byollm_001 \xA7Endpoints.4"
578
1110
  }),
579
- RESULT_PROVENANCE: must({
580
- id: "RESULT_PROVENANCE",
581
- 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.",
1111
+ PROVENANCE_NAMES_DEVICE: must({
1112
+ id: "PROVENANCE_NAMES_DEVICE",
1113
+ 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.",
582
1114
  enforcedBy: "server",
583
- source: "byollm_003 Rev 1 \xA7Return-trip"
1115
+ verifiedBy: "conformance",
1116
+ source: "byollm_009 \xA711"
584
1117
  }),
585
1118
  // ---- The trust surface -------------------------------------------------
586
1119
  INGRESS_LOGGED_BEFORE_EXECUTION: must({
587
1120
  id: "INGRESS_LOGGED_BEFORE_EXECUTION",
588
1121
  statement: "Every executed prompt MUST be appended to the local ingress log before execution begins.",
589
1122
  enforcedBy: "daemon",
1123
+ verifiedBy: "conformance",
590
1124
  source: "byollm_001 \xA7MUSTs"
591
1125
  }),
592
1126
  // ---- Execution isolation (byollm_004) ---------------------------------
@@ -594,178 +1128,484 @@ var MUSTS = Object.freeze({
594
1128
  id: "NO_SHELL_INTERPOLATION",
595
1129
  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.",
596
1130
  enforcedBy: "daemon",
1131
+ verifiedBy: "adversarial",
597
1132
  source: "byollm_004 \xA72"
598
1133
  }),
599
1134
  NO_PAYLOAD_ROUTING: must({
600
1135
  id: "NO_PAYLOAD_ROUTING",
601
1136
  statement: "Model, backend, base URL, and flags MUST come from owner config only; a payload MUST NOT influence any of them.",
602
1137
  enforcedBy: "daemon",
1138
+ verifiedBy: "adversarial",
603
1139
  source: "byollm_004 \xA72"
604
1140
  }),
605
1141
  STRIPPED_CHILD_ENV: must({
606
1142
  id: "STRIPPED_CHILD_ENV",
607
1143
  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.",
608
1144
  enforcedBy: "daemon",
1145
+ verifiedBy: "adversarial",
609
1146
  source: "byollm_004 \xA72"
610
1147
  }),
611
1148
  HTTP_BASE_URL_SAFE: must({
612
1149
  id: "HTTP_BASE_URL_SAFE",
613
1150
  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.",
614
1151
  enforcedBy: "daemon",
1152
+ verifiedBy: "adversarial",
615
1153
  source: "byollm_004 Rev 1 \xA7Backend taxonomy"
616
1154
  }),
617
1155
  OUTPUT_INERT: must({
618
1156
  id: "OUTPUT_INERT",
619
1157
  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.",
620
1158
  enforcedBy: "daemon",
1159
+ verifiedBy: "adversarial",
621
1160
  source: "byollm_004 \xA72"
622
1161
  }),
623
1162
  COMMUNITY_BUDGETS: must({
624
1163
  id: "COMMUNITY_BUDGETS",
625
1164
  statement: "Jobs whose owner is not the daemon's owner MUST be subject to the owner's rate limits, daily cap, and resource budget.",
626
1165
  enforcedBy: "daemon",
1166
+ verifiedBy: "adversarial",
627
1167
  source: "byollm_004 \xA74"
1168
+ }),
1169
+ REVOCATION_IMMEDIATE: must({
1170
+ id: "REVOCATION_IMMEDIATE",
1171
+ 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.",
1172
+ // Both, and stated as one sentence with two obligations rather than
1173
+ // folded into REVOCATION_HONORED. That one binds the *daemon*: a revoked
1174
+ // daemon stops claiming and abandons in-flight work. This binds the
1175
+ // *upstream*. byollm_009 §5 is explicit that the pair is the point — "a
1176
+ // revocation enforced at one end survives a compromise of that end" — and
1177
+ // one entry covering both would make a compromised daemon look compliant.
1178
+ enforcedBy: "both",
1179
+ verifiedBy: "conformance",
1180
+ source: "byollm_009 \xA711"
1181
+ }),
1182
+ CONSENT_BEFORE_ROUTE: must({
1183
+ id: "CONSENT_BEFORE_ROUTE",
1184
+ 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.",
1185
+ enforcedBy: "server",
1186
+ verifiedBy: "conformance",
1187
+ source: "byollm_009 \xA711"
1188
+ }),
1189
+ ROSTER_NOT_DISCLOSED: must({
1190
+ id: "ROSTER_NOT_DISCLOSED",
1191
+ 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.",
1192
+ // Checkable since cloud_008 §0.2 took `audienceAllow` off the stub: the
1193
+ // property now holds by *absence*, and absence is exactly what a strict
1194
+ // schema and a serialised stub can be asked about. Before that it was a
1195
+ // sentence — and one this project cited in code comments, tests and two
1196
+ // specs as though it were enforced data, which is why it is worth
1197
+ // stating precisely rather than generously.
1198
+ enforcedBy: "both",
1199
+ verifiedBy: "conformance",
1200
+ source: "byollm_009 \xA711"
1201
+ }),
1202
+ EFFECTIVE_OFFER_ONLY: must({
1203
+ id: "EFFECTIVE_OFFER_ONLY",
1204
+ 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.",
1205
+ enforcedBy: "both",
1206
+ verifiedBy: "conformance",
1207
+ source: "byollm_009 \xA711"
1208
+ }),
1209
+ FALLBACK_LABELED: must({
1210
+ id: "FALLBACK_LABELED",
1211
+ 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.",
1212
+ // `construction` today, and deliberately not `conformance`. Nothing on
1213
+ // the wire yet distinguishes a fallback from any other community job —
1214
+ // the ledger that would give it a surface is unbuilt — so a check would
1215
+ // have to assert something it cannot observe. Promoted the day that
1216
+ // surface exists. Marking it `conformance` now would put "verified"
1217
+ // beside a property no third party can see, which is the one thing the
1218
+ // kinds exist to prevent.
1219
+ enforcedBy: "both",
1220
+ verifiedBy: "construction",
1221
+ source: "byollm_009 \xA711"
1222
+ }),
1223
+ RELAY_BLIND: must({
1224
+ id: "RELAY_BLIND",
1225
+ statement: "A relay MUST NOT hold any key capable of decrypting a payload, a result, or a delta frame.",
1226
+ // Operator: a third party can read the relay's types and see there is
1227
+ // nowhere to put such a key, but the kit certifies a *server* and cannot
1228
+ // reach inside somebody's deployment to prove what it holds.
1229
+ enforcedBy: "server",
1230
+ verifiedBy: "operator",
1231
+ source: "byollm_009 \xA711"
1232
+ }),
1233
+ SHARED_COMPUTE_DISCLOSED: must({
1234
+ id: "SHARED_COMPUTE_DISCLOSED",
1235
+ 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.",
1236
+ // Operator, and cloud_008 §0.3 is why the classification now comes with a
1237
+ // standing answer rather than a standing question. The screen is not
1238
+ // wire-observable, but the *string the server composes* is, and it is
1239
+ // now unit-tested with the two false sentences forbidden by name. The
1240
+ // kind stays `operator` because a third-party site can still render
1241
+ // whatever it likes; what changed is that the part inside our own
1242
+ // boundary stopped depending on somebody remembering to audit it.
1243
+ enforcedBy: "server",
1244
+ verifiedBy: "operator",
1245
+ source: "byollm_009 \xA711"
628
1246
  })
629
1247
  });
1248
+ var RETIRED_MUSTS = Object.freeze({
1249
+ RESULT_PROVENANCE: {
1250
+ supersededBy: "PROVENANCE_NAMES_DEVICE",
1251
+ 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."
1252
+ }
1253
+ });
630
1254
  var MUST_IDS = Object.freeze(Object.keys(MUSTS));
1255
+ function mustsVerifiedBy(kind) {
1256
+ return MUST_IDS.filter((id) => kindsOf(MUSTS[id]).includes(kind));
1257
+ }
631
1258
 
632
1259
  // src/wire.ts
633
- import { z as z5 } from "zod";
1260
+ import { z as z8 } from "zod";
634
1261
  var PROTOCOL_VERSION = "0";
1262
+ var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze([
1263
+ PROTOCOL_VERSION
1264
+ ]);
1265
+ var MIN_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0] ?? PROTOCOL_VERSION;
1266
+ function declaredVersion(input) {
1267
+ const { body, query } = input;
1268
+ if (typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion")) {
1269
+ return body.protocolVersion;
1270
+ }
1271
+ return query?.get("protocolVersion") ?? void 0;
1272
+ }
1273
+ function checkProtocolVersion(body) {
1274
+ const declared = typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion") ? body.protocolVersion : void 0;
1275
+ if (typeof declared !== "string" || declared.length === 0) {
1276
+ return {
1277
+ error: "unsupported-protocol-version",
1278
+ message: `this request declared no protocol version. Upgrade the daemon: \`${UPGRADE_COMMAND}\`.`,
1279
+ supported: SUPPORTED_PROTOCOL_VERSIONS,
1280
+ minimum: MIN_PROTOCOL_VERSION
1281
+ };
1282
+ }
1283
+ if (!SUPPORTED_PROTOCOL_VERSIONS.includes(declared)) {
1284
+ return {
1285
+ error: "unsupported-protocol-version",
1286
+ 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."),
1287
+ supported: SUPPORTED_PROTOCOL_VERSIONS,
1288
+ minimum: MIN_PROTOCOL_VERSION
1289
+ };
1290
+ }
1291
+ return null;
1292
+ }
1293
+ var UPGRADE_COMMAND = "npm i -g byollm@latest";
635
1294
  var PROTOCOL_PREFIX = "/byollm";
636
1295
  var ENDPOINTS = Object.freeze([
637
1296
  "pair",
638
1297
  "claim",
1298
+ "fetch",
639
1299
  "heartbeat",
640
1300
  "result",
641
1301
  "release"
642
1302
  ]);
643
- var Capability = z5.object({
1303
+ var Capability = z8.object({
644
1304
  kind: JobKind,
645
1305
  backendId: BackendIdSchema,
646
1306
  backendClass: BackendClass,
647
- model: z5.string().min(1),
1307
+ model: z8.string().min(1),
648
1308
  offerScope: OfferScope
649
1309
  }).strict();
650
- var CapabilityMatrix = z5.array(Capability);
651
- var PairStartRequest = z5.object({
652
- protocolVersion: z5.literal(PROTOCOL_VERSION),
653
- action: z5.literal("start"),
654
- daemon: z5.object({
655
- version: z5.string().min(1),
1310
+ var CapabilityMatrix = z8.array(Capability);
1311
+ var PairStartRequest = z8.object({
1312
+ protocolVersion: z8.literal(PROTOCOL_VERSION),
1313
+ action: z8.literal("start"),
1314
+ daemon: z8.object({
1315
+ version: z8.string().min(1),
656
1316
  /** Shown in the app's runner list so a user can tell their machines apart. */
657
- label: z5.string().min(1).max(120),
658
- platform: z5.enum(["darwin", "linux", "win32"])
1317
+ label: z8.string().min(1).max(120),
1318
+ platform: z8.enum(["darwin", "linux", "win32"])
659
1319
  }),
1320
+ /**
1321
+ * This machine's public keys (byollm_009 §5).
1322
+ *
1323
+ * Pairing is where the two parties learn each other's identities, because
1324
+ * it is the one moment a human is already deciding to trust: the approval
1325
+ * click. A key exchanged anywhere else would be a key nobody chose.
1326
+ */
1327
+ device: PublicIdentity,
660
1328
  capabilities: CapabilityMatrix
661
1329
  }).strict();
662
- var PairStartResponse = z5.object({
1330
+ var PairStartResponse = z8.object({
663
1331
  /** Secret the daemon polls with. Never shown to the user. */
664
- deviceCode: z5.string().min(20),
1332
+ deviceCode: z8.string().min(20),
665
1333
  /** Short code the user reads and confirms in the browser. */
666
- userCode: z5.string().min(4).max(16),
1334
+ userCode: z8.string().min(4).max(16),
667
1335
  /** Where the user approves. Must be on the server's own origin. */
668
- verificationUrl: z5.url(),
1336
+ verificationUrl: z8.url(),
669
1337
  /** Epoch ms after which the code is dead ({@link MUSTS.PAIR_CODE_EXPIRES}). */
670
- expiresAt: z5.number().int().positive(),
1338
+ expiresAt: z8.number().int().positive(),
671
1339
  /** How often the daemon may poll. */
672
- pollIntervalMs: z5.number().int().min(500).max(6e4)
1340
+ pollIntervalMs: z8.number().int().min(500).max(6e4)
673
1341
  }).strict();
674
- var PairPollRequest = z5.object({
675
- protocolVersion: z5.literal(PROTOCOL_VERSION),
676
- action: z5.literal("poll"),
677
- deviceCode: z5.string().min(20)
1342
+ var PairPollRequest = z8.object({
1343
+ protocolVersion: z8.literal(PROTOCOL_VERSION),
1344
+ action: z8.literal("poll"),
1345
+ deviceCode: z8.string().min(20)
678
1346
  }).strict();
679
- var PairPollResponse = z5.discriminatedUnion("status", [
680
- z5.object({ status: z5.literal("pending") }).strict(),
681
- z5.object({ status: z5.literal("denied") }).strict(),
682
- z5.object({ status: z5.literal("expired") }).strict(),
683
- z5.object({
684
- status: z5.literal("approved"),
685
- /** Bearer token for every later call. Scoped to exactly one user. */
686
- runnerToken: z5.string().min(20),
687
- runnerId: z5.string().min(1),
1347
+ var PairPollResponse = z8.discriminatedUnion("status", [
1348
+ z8.object({ status: z8.literal("pending") }).strict(),
1349
+ z8.object({ status: z8.literal("denied") }).strict(),
1350
+ z8.object({ status: z8.literal("expired") }).strict(),
1351
+ z8.object({
1352
+ status: z8.literal("approved"),
1353
+ // `runnerToken` is gone cloud_008 §2.4, finding 37.
1354
+ //
1355
+ // It was minted here, hashed into `RunnerRecord.tokenHash`, written to
1356
+ // the daemon's pairings file, and then **never sent, never looked up
1357
+ // and never compared**. `getRunnerByTokenHash` existed on both stores
1358
+ // and was called by nothing but a test asserting it returns null.
1359
+ //
1360
+ // Not merely dead wire, which is what `audienceAllow` and
1361
+ // `HeartbeatResponse.leases` were. This was a *secret*: minted,
1362
+ // transmitted, and written to two disks at rest, for nothing. A
1363
+ // credential with no purpose is a liability rather than clutter,
1364
+ // because the only thing it can ever do is leak.
1365
+ //
1366
+ // `REQUESTS_SIGNED_NOT_BEARER` was already the rule and was already
1367
+ // enforced — every authenticated call is signed by the device's pinned
1368
+ // identity key. This removes the thing the MUST is named after.
1369
+ runnerId: z8.string().min(1),
688
1370
  /** The app's id for the approving user — this daemon's owner forever. */
689
- owner: z5.string().min(1),
1371
+ owner: z8.string().min(1),
690
1372
  /** Display name for the trust UI, if the app offers one. */
691
- ownerLabel: z5.string().optional()
1373
+ ownerLabel: z8.string().optional(),
1374
+ /**
1375
+ * The sites this pairing covers, for the daemon to pin (byollm_009 §5),
1376
+ * keyed by each site's identity key id — cloud_009 §5.
1377
+ *
1378
+ * Returned only on approval: a pending or denied poll learns nothing,
1379
+ * so an unapproved code cannot be used to enumerate a site's keys.
1380
+ *
1381
+ * **One pairing per upstream, not one per site.** A user who connects a
1382
+ * site on a web dashboard has no reason to go back to a laptop and run
1383
+ * a command, so which sites a pairing covers is a projection of consent
1384
+ * — refreshed on the heartbeat — rather than something frozen at
1385
+ * pairing. A direct site answers with exactly one entry, which is the
1386
+ * same shape and not a special case.
1387
+ *
1388
+ * Keyed by the id `stub.site` carries (Amendment A §A.3), so the
1389
+ * runner's lookup is a map read rather than a join across two
1390
+ * namespaces.
1391
+ */
1392
+ sites: z8.record(z8.string().min(1), PublicIdentity)
692
1393
  }).strict()
693
1394
  ]);
694
- var PairRequest = z5.discriminatedUnion("action", [
1395
+ var PairRequest = z8.discriminatedUnion("action", [
695
1396
  PairStartRequest,
696
1397
  PairPollRequest
697
1398
  ]);
698
- var ClaimRequest = z5.object({
699
- protocolVersion: z5.literal(PROTOCOL_VERSION),
700
- runnerId: z5.string().min(1),
1399
+ var ClaimRequest = z8.object({
1400
+ protocolVersion: z8.literal(PROTOCOL_VERSION),
1401
+ runnerId: z8.string().min(1),
701
1402
  /** Re-sent on every claim so a server never matches against a stale matrix. */
702
1403
  capabilities: CapabilityMatrix,
703
1404
  /** Upper bound on jobs to return; the server may return fewer. */
704
- max: z5.number().int().min(1).max(64)
1405
+ max: z8.number().int().min(1).max(64)
705
1406
  }).strict();
706
- var ClaimResponse = z5.object({
707
- jobs: z5.array(ClaimedJob),
1407
+ var ClaimResponse = z8.object({
1408
+ /**
1409
+ * Stubs, not jobs. The payload arrives from `fetch`, sealed to whichever
1410
+ * device claimed — see {@link JobStub} for the exhaustive metadata list.
1411
+ */
1412
+ jobs: z8.array(ClaimedStub),
708
1413
  /** Lease duration granted, so the daemon knows its renewal deadline. */
709
- leaseMs: z5.number().int().positive()
1414
+ leaseMs: z8.number().int().positive()
710
1415
  }).strict();
711
- var HeartbeatRequest = z5.object({
712
- protocolVersion: z5.literal(PROTOCOL_VERSION),
713
- runnerId: z5.string().min(1),
714
- daemonVersion: z5.string().min(1),
1416
+ var HeartbeatRequest = z8.object({
1417
+ protocolVersion: z8.literal(PROTOCOL_VERSION),
1418
+ runnerId: z8.string().min(1),
1419
+ daemonVersion: z8.string().min(1),
715
1420
  capabilities: CapabilityMatrix,
716
- /** Jobs this daemon believes it holds; the server renews their leases. */
717
- activeJobIds: z5.array(z5.string().min(1)),
1421
+ /**
1422
+ * Leases this daemon believes it holds; the server renews exactly these.
1423
+ *
1424
+ * Lease ids rather than job ids, so a replayed heartbeat cannot renew a
1425
+ * grant the runner no longer holds — see {@link Lease.id}.
1426
+ */
1427
+ activeLeases: z8.array(
1428
+ z8.object({ jobId: z8.string().min(1), leaseId: z8.string().min(1) })
1429
+ ),
718
1430
  /** True while the owner has the daemon paused; the server stops offering work. */
719
- paused: z5.boolean()
1431
+ paused: z8.boolean()
720
1432
  }).strict();
721
- var HeartbeatResponse = z5.object({
722
- /** Once true, the daemon stops claiming and abandons in-flight work. */
723
- revoked: z5.boolean(),
1433
+ var HeartbeatResponse = z8.object({
1434
+ /**
1435
+ * The sites this daemon may serve, right now — cloud_008 finding 59.
1436
+ *
1437
+ * Revocation used to be a boolean, and it was device-wide: the daemon
1438
+ * plane refused every call when the (owner, hub-site) consent was gone,
1439
+ * heartbeat answered `revoked: true` with `lost: all`, and the daemon
1440
+ * dropped its whole pairing by origin. Under a hub that is one site's
1441
+ * revocation ending a machine's relationship with every other site it
1442
+ * served — the amplification finding 48 warned about, arriving through
1443
+ * the one field nobody thought of as tenancy.
1444
+ *
1445
+ * So the answer is the set. A site that leaves it is revoked *for that
1446
+ * site*: the daemon drops that pin and keeps the rest. An empty set is
1447
+ * what "revoked" used to mean, and the daemon can see that for itself
1448
+ * rather than being told a second time — two fields for one fact is how
1449
+ * they drift.
1450
+ */
1451
+ sites: z8.record(z8.string().min(1), PublicIdentity),
724
1452
  /**
725
1453
  * Per-job cancel (byollm_001 Rev 1 §C). The daemon aborts these jobs'
726
1454
  * in-flight backend calls and reports them `canceled`.
1455
+ *
1456
+ * **The grant, not the id** — V1-3. Job ids are chosen per site, so two
1457
+ * sites may pick the same one, and a bare id told a daemon holding both
1458
+ * to abort whichever it happened to have filed under that name. The lease
1459
+ * is the unique grant and the daemon already keys its work by it; this is
1460
+ * the same shape `activeLeases` sends in the other direction.
727
1461
  */
728
- cancel: z5.array(z5.string().min(1)),
729
- /** Jobs whose leases were renewed, with their new expiry. */
730
- leases: z5.array(
731
- z5.object({
732
- jobId: z5.string().min(1),
733
- expiresAt: z5.number().int().positive()
734
- }).strict()
1462
+ cancel: z8.array(
1463
+ z8.object({ jobId: z8.string().min(1), leaseId: z8.string().min(1) }).strict()
735
1464
  ),
1465
+ // `leases` is deliberately absent — cloud_008 §1.4b, finding 16.
1466
+ //
1467
+ // It carried "these leases were renewed, and here is the new expiry", and
1468
+ // **no daemon ever read it.** A mutation returning an empty list while
1469
+ // renewing correctly survived every test, which is what made it visible.
1470
+ //
1471
+ // It is neither a class nor membership, so Amendment A's rule does not
1472
+ // decide it — the older test does: nothing reads it, so it is dead wire.
1473
+ // §6's exhaustiveness is a commitment about what an upstream can see, and
1474
+ // it applies to every message rather than only to the stub.
1475
+ //
1476
+ // `lost` is the actionable signal and always was: a daemon stops work on
1477
+ // a lease it no longer holds. "Renewed" was the same question answered a
1478
+ // second time, and a second answer can only agree or contradict.
1479
+ //
1480
+ // Renewal itself is untouched — the upstream still extends the grants a
1481
+ // heartbeat names, which is what §0.6 fixed. What ended is telling the
1482
+ // daemon about it in a field it ignored. If an upstream ever needs to
1483
+ // push lease decisions, that is a new field with a reader, added on
1484
+ // purpose.
736
1485
  /**
737
1486
  * Jobs the daemon thinks it holds but the server has reassigned or
738
1487
  * expired. The daemon must stop work on these and not report results.
1488
+ *
1489
+ * Named by grant rather than by id, for V1-3's reason: a bare id is
1490
+ * ambiguous across sites, and "the lease you no longer hold" is exactly
1491
+ * what this field means anyway.
739
1492
  */
740
- lost: z5.array(z5.string().min(1)),
1493
+ lost: z8.array(
1494
+ z8.object({ jobId: z8.string().min(1), leaseId: z8.string().min(1) }).strict()
1495
+ ),
741
1496
  /** Server clock, so a daemon with a skewed clock still honors leases. */
742
- serverTime: z5.number().int().positive()
1497
+ serverTime: z8.number().int().positive(),
1498
+ /**
1499
+ * Sites whose disclosure the user must read again before work moves —
1500
+ * cloud_008 finding 48, named rather than counted.
1501
+ *
1502
+ * A **subset of `sites`**, deliberately: a paused site keeps its pin, so
1503
+ * re-consenting never costs a re-pair. The daemon can say which site is
1504
+ * waiting and the user can go and read it, which is the difference
1505
+ * between a machine that is quietly idle and one that says why.
1506
+ *
1507
+ * Not `revoked`, which is a human ending a relationship, and not
1508
+ * `paused`, which on the request side already means "this daemon's
1509
+ * operator stopped it" — one word with two subjects on two halves of one
1510
+ * exchange is a confusion nobody untangles from a log.
1511
+ */
1512
+ awaitingConsent: z8.array(z8.string().min(1))
743
1513
  }).strict();
744
- var ResultRequest = z5.object({
745
- protocolVersion: z5.literal(PROTOCOL_VERSION),
746
- runnerId: z5.string().min(1),
747
- jobId: z5.string().min(1),
748
- outcome: JobOutcome,
749
- /** Which model actually served it, for the result's provenance. */
750
- model: z5.string().min(1),
751
- backendClass: BackendClass,
752
- /** Wall-clock milliseconds the backend call took. */
753
- durationMs: z5.number().int().nonnegative()
1514
+ var ResultDisposition = z8.enum(["ok", "error", "canceled"]);
1515
+ var ResultRequest = z8.object({
1516
+ protocolVersion: z8.literal(PROTOCOL_VERSION),
1517
+ runnerId: z8.string().min(1),
1518
+ jobId: z8.string().min(1),
1519
+ /**
1520
+ * The grant this result was produced under — cloud_008 §1.4a.
1521
+ *
1522
+ * `fetch` has always named its lease, with the reasoning written beside
1523
+ * it: a request that names only the job would be answerable for whatever
1524
+ * lease exists when it arrives. **The operation that writes the result did
1525
+ * not**, on either plane, and checked only the runner id — which survives
1526
+ * a claim-release-reclaim cycle, so a device whose grant had been swept
1527
+ * and reissued could still land a result for a job it no longer held.
1528
+ *
1529
+ * Found by tracing a mutation that survived in §0.6: the lease lapsed, the
1530
+ * sweep requeued, the daemon re-claimed under a new grant, and the
1531
+ * original run finished and posted anyway. The relay marked the job done
1532
+ * with a result the site cannot open — it verifies the envelope against
1533
+ * the *current* holder's device, so the crypto contains the substitution —
1534
+ * and then refused the real holder's result as a replay. A lost job, in
1535
+ * silence.
1536
+ *
1537
+ * `LEASE_HONORED` is a statement about a lease *instance*. That was
1538
+ * learned once already, when a replayed release yanked a later grant, and
1539
+ * it applies here for the same reason.
1540
+ */
1541
+ leaseId: z8.string().min(1),
1542
+ /**
1543
+ * The outcome, sealed to the site and signed by the device.
1544
+ *
1545
+ * The return leg of the payload envelope, and sealed for the same reason:
1546
+ * a model's answer is as sensitive as the prompt that produced it, and an
1547
+ * intermediary that cannot read one must not be handed the other.
1548
+ */
1549
+ envelope: SealedEnvelope,
1550
+ /**
1551
+ * The sealed outcome's discriminator, in the clear.
1552
+ *
1553
+ * Checked against the envelope once opened. It is a routing hint, not a
1554
+ * fact: believing it unverified would let a daemon mark a job `ok` while
1555
+ * sealing an error, and only the app would ever find out.
1556
+ */
1557
+ disposition: ResultDisposition
1558
+ // `model`, `backendClass` and `durationMs` are **inside the envelope** —
1559
+ // cloud_008 §2.5. See {@link RunMetadata}.
1560
+ //
1561
+ // They were here, in the clear, and that was two problems wearing one
1562
+ // coat. On the direct plane the site recorded unauthenticated fields
1563
+ // beside an authenticated answer: a daemon could seal one result and
1564
+ // declare a different model, and only the unsigned half would reach the
1565
+ // app. Through a relay they reached a third party that acts on none of
1566
+ // them — `model` in particular being the sort of detail Amendment A's
1567
+ // rule keeps off the wire.
1568
+ //
1569
+ // `disposition` stays, and the difference is the test: a relay *routes*
1570
+ // on it, so it is a class a routing party consumes. Nobody between the
1571
+ // two ends consumes these.
754
1572
  }).strict();
755
- var ResultResponse = z5.object({
1573
+ var ResultResponse = z8.object({
756
1574
  /**
757
- * False when the submission lost an idempotency race or the lease was
758
- * already gone the daemon should discard, not retry
759
- * ({@link MUSTS.RESULT_IDEMPOTENT}).
1575
+ * False when this submission wrote nothing the daemon should discard,
1576
+ * not retry ({@link MUSTS.RESULT_IDEMPOTENT}).
760
1577
  */
761
- accepted: z5.boolean(),
1578
+ accepted: z8.boolean(),
1579
+ /**
1580
+ * True when this device had already recorded this job's result.
1581
+ *
1582
+ * The difference between "already recorded" and "you no longer hold this"
1583
+ * — cloud_008 §3.6. A daemon whose acknowledgment was lost is in the first
1584
+ * case and needs to hear it: its answer is safely on disk. Reporting a
1585
+ * stale lease instead invents a worry about a result that is already
1586
+ * stored, and sends its owner looking for a routing problem.
1587
+ *
1588
+ * Set only for the device that finished the job. A different device gets
1589
+ * the same refusal it would get for a job that is *not* terminal, so a job
1590
+ * id cannot be used as a terminality probe.
1591
+ */
1592
+ duplicate: z8.boolean().optional(),
762
1593
  /** The job's state after this submission. */
763
- state: z5.string().min(1)
1594
+ state: z8.string().min(1)
764
1595
  }).strict();
765
- var ReleaseRequest = z5.object({
766
- protocolVersion: z5.literal(PROTOCOL_VERSION),
767
- runnerId: z5.string().min(1),
768
- jobIds: z5.array(z5.string().min(1)),
1596
+ var ReleaseRequest = z8.object({
1597
+ protocolVersion: z8.literal(PROTOCOL_VERSION),
1598
+ runnerId: z8.string().min(1),
1599
+ /**
1600
+ * Which leases to release — the grant, not just the job.
1601
+ *
1602
+ * A release naming only a job id releases whatever lease exists at the
1603
+ * moment it arrives, which for a replayed request is not the lease the
1604
+ * daemon meant. See {@link Lease.id}.
1605
+ */
1606
+ leases: z8.array(
1607
+ z8.object({ jobId: z8.string().min(1), leaseId: z8.string().min(1) })
1608
+ ),
769
1609
  /**
770
1610
  * Why, so the app's runner list can say something true.
771
1611
  *
@@ -775,35 +1615,172 @@ var ReleaseRequest = z5.object({
775
1615
  * stop offering that job to that runner, or the pair would spin between
776
1616
  * claim and release forever.
777
1617
  */
778
- reason: z5.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
1618
+ reason: z8.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
779
1619
  }).strict();
780
- var ReleaseResponse = z5.object({
781
- released: z5.array(z5.string().min(1))
1620
+ var ReleaseResponse = z8.object({
1621
+ released: z8.array(z8.string().min(1))
782
1622
  }).strict();
783
- var WireErrorCode = z5.enum([
1623
+ var WireErrorCode = z8.enum([
784
1624
  "bad-request",
785
1625
  "unsupported-protocol-version",
1626
+ // "We do not know who you are." Exactly 401, and only that — cloud_008
1627
+ // §1.4d.
786
1628
  "unauthorized",
1629
+ /**
1630
+ * "We know exactly who you are, and the answer is no." Exactly 403.
1631
+ *
1632
+ * Five refusals across both planes served 403 with `unauthorized`, whose
1633
+ * table entry is 401: a revoked device, a site claiming another site's
1634
+ * stub, a job you do not hold, a device belonging to another owner, a
1635
+ * relay that does not route for you. Every one of them is an *identified*
1636
+ * caller being refused.
1637
+ *
1638
+ * Collapsing the two loses a distinction that matters everywhere it is
1639
+ * read: a revoked daemon would look like an unsigned one in every log and
1640
+ * every client branch, and "check your keys" is the wrong advice for both
1641
+ * of them in opposite directions.
1642
+ */
1643
+ "forbidden",
787
1644
  "revoked",
788
1645
  "not-found",
1646
+ // Claimed, but the site has not sealed the payload yet — cloud_008 §1.4.
1647
+ //
1648
+ // A daemon must retry rather than abandon: the job is legitimately still
1649
+ // its own until the lease or the awaiting-payload clock says otherwise.
1650
+ // That is why it cannot be `not-found` or `server-error`, and why it was
1651
+ // the protocol gap that produced a bare 409 in the first place.
1652
+ "not-ready",
1653
+ /**
1654
+ * The job is over, and this call is about a job — V1-6, and the code the
1655
+ * site plane has been serving without one (V1-13).
1656
+ *
1657
+ * Distinct from `not-found`, which says "no such job", and from
1658
+ * `not-ready`, which says "not yet, keep asking". This one says "yes, and
1659
+ * it finished" — so a daemon must stop rather than retry, and a replayed
1660
+ * request must not be able to reopen it.
1661
+ */
1662
+ "too-late",
1663
+ // The caller's clock is too far from ours to judge a signature's freshness.
1664
+ //
1665
+ // Split out from `unauthorized` because the remedy is completely different
1666
+ // and only the server can tell them apart: a bad signature means the key is
1667
+ // wrong, this means the machine's time is wrong. A daemon reporting it as a
1668
+ // generic rejection sends its owner looking at their network.
1669
+ "clock-skew",
789
1670
  "rate-limited",
790
1671
  "server-error"
791
1672
  ]);
792
- var WireError = z5.object({
1673
+ var WireError = z8.object({
793
1674
  error: WireErrorCode,
794
- message: z5.string().min(1),
1675
+ message: z8.string().min(1),
1676
+ /**
1677
+ * What this server speaks, on `unsupported-protocol-version` — §B.4.
1678
+ *
1679
+ * The refusal has carried these since the version handshake existed and
1680
+ * the enumeration did not model them, so the one error that exists to be
1681
+ * *acted on* was the one that failed to parse as a wire error. Found by
1682
+ * the relay's own suite the day the relay started sending it: a refusal
1683
+ * outside the enumerated shape is a refusal a client cannot branch on,
1684
+ * which is the whole reason §1.4 enumerates them.
1685
+ *
1686
+ * Modelled the way `clock-skew`'s two fields already are — code-specific
1687
+ * extras, refused on any other code by the refinement below.
1688
+ */
1689
+ supported: z8.array(z8.string().min(1)).optional(),
1690
+ minimum: z8.string().min(1).optional(),
795
1691
  /** Seconds; mirrors Retry-After for `rate-limited` and `server-error`. */
796
- retryAfter: z5.number().int().nonnegative().optional()
797
- }).strict();
1692
+ retryAfter: z8.number().int().nonnegative().optional(),
1693
+ /**
1694
+ * The server's clock, and the window it allows. `clock-skew` only.
1695
+ *
1696
+ * So the far side can say *how far off* rather than *that something is
1697
+ * wrong* — the difference between "adjust your clock by four minutes" and
1698
+ * "something is wrong with your connection". Not a disclosure: the
1699
+ * heartbeat response returns the same value, and so does every `Date`
1700
+ * header.
1701
+ */
1702
+ serverTime: z8.number().int().positive().optional(),
1703
+ maxSkewMs: z8.number().int().positive().optional()
1704
+ }).strict().superRefine((error, ctx) => {
1705
+ const skew = error.error === "clock-skew";
1706
+ const carried = error.serverTime !== void 0 || error.maxSkewMs !== void 0;
1707
+ if (skew && !carried) {
1708
+ ctx.addIssue({
1709
+ code: "custom",
1710
+ message: "clock-skew must carry serverTime and maxSkewMs"
1711
+ });
1712
+ }
1713
+ if (!skew && carried) {
1714
+ ctx.addIssue({
1715
+ code: "custom",
1716
+ message: `${error.error} must not carry serverTime or maxSkewMs`
1717
+ });
1718
+ }
1719
+ const version = error.error === "unsupported-protocol-version";
1720
+ const versionFields = error.supported !== void 0 || error.minimum !== void 0;
1721
+ if (version && !versionFields) {
1722
+ ctx.addIssue({
1723
+ code: "custom",
1724
+ message: "unsupported-protocol-version must carry supported and minimum"
1725
+ });
1726
+ }
1727
+ if (!version && versionFields) {
1728
+ ctx.addIssue({
1729
+ code: "custom",
1730
+ message: `${error.error} must not carry supported or minimum`
1731
+ });
1732
+ }
1733
+ });
798
1734
  var ERROR_STATUS = Object.freeze({
799
1735
  "bad-request": 400,
800
1736
  "unsupported-protocol-version": 400,
801
1737
  unauthorized: 401,
1738
+ forbidden: 403,
802
1739
  revoked: 403,
803
1740
  "not-found": 404,
1741
+ // 409, not 404: the job exists and is yours, it is simply not ready.
1742
+ "not-ready": 409,
1743
+ // The same 409 as `not-ready` and the opposite instruction: that one says
1744
+ // keep asking, this one says stop. The status is the class of the
1745
+ // problem — a request that does not fit the resource's state — and the
1746
+ // code is what a caller acts on.
1747
+ "too-late": 409,
1748
+ // 401 alongside `unauthorized`, because that is what it is — the
1749
+ // signature could not be judged. The code is what carries the remedy.
1750
+ "clock-skew": 401,
804
1751
  "rate-limited": 429,
805
1752
  "server-error": 500
806
1753
  });
1754
+ var FetchRequest = z8.object({
1755
+ // `literal`, like every other request — V1-17. This one said
1756
+ // `string().min(1)`, so a daemon speaking a version this server does not
1757
+ // know got past the handshake on the one endpoint that hands over a
1758
+ // sealed payload. The version check exists so that a mismatch is a named
1759
+ // refusal rather than a schema failure three fields later; here it was
1760
+ // neither.
1761
+ protocolVersion: z8.literal(PROTOCOL_VERSION),
1762
+ runnerId: z8.string().min(1),
1763
+ jobId: z8.string().min(1),
1764
+ /**
1765
+ * The grant this daemon holds.
1766
+ *
1767
+ * Named, not inferred: a fetch is lease-scoped, and a request that names
1768
+ * only the job would be answerable for whatever lease exists when it
1769
+ * arrives ({@link Lease.id}).
1770
+ */
1771
+ leaseId: z8.string().min(1)
1772
+ }).strict();
1773
+ var FetchResponse = z8.object({
1774
+ /**
1775
+ * The work, sealed to the device that claimed it — byollm_009 §6.
1776
+ *
1777
+ * Not plaintext. The site opens its own at-rest envelope and re-seals to
1778
+ * the claiming device's key, signed by the site's identity, so the work
1779
+ * is readable only by the machine that took it and only if it came from
1780
+ * the site that machine pinned.
1781
+ */
1782
+ envelope: SealedEnvelope
1783
+ }).strict();
807
1784
  export {
808
1785
  AUDIENCES,
809
1786
  Audience,
@@ -819,9 +1796,15 @@ export {
819
1796
  ClaimRequest,
820
1797
  ClaimResponse,
821
1798
  ClaimedJob,
1799
+ ClaimedStub,
822
1800
  DeliveredResult,
1801
+ ENCRYPTION_KEY_CONTEXT,
823
1802
  ENDPOINTS,
1803
+ ENVELOPE_MAX_AGE_MS,
824
1804
  ERROR_STATUS,
1805
+ EnvelopeDirection,
1806
+ FetchRequest,
1807
+ FetchResponse,
825
1808
  GeneratePayload,
826
1809
  HeartbeatRequest,
827
1810
  HeartbeatResponse,
@@ -833,8 +1816,11 @@ export {
833
1816
  JobResultError,
834
1817
  JobResultOk,
835
1818
  JobState,
1819
+ JobStub,
836
1820
  KindedPayload,
837
1821
  Lease,
1822
+ MAX_CLOCK_SKEW_MS,
1823
+ MIN_PROTOCOL_VERSION,
838
1824
  MUSTS,
839
1825
  MUST_IDS,
840
1826
  MatchRefusal,
@@ -848,25 +1834,56 @@ export {
848
1834
  PairRequest,
849
1835
  PairStartRequest,
850
1836
  PairStartResponse,
1837
+ PublicIdentity,
851
1838
  REFUSAL_MESSAGES,
852
1839
  ReleaseRequest,
853
1840
  ReleaseResponse,
1841
+ RequestSignature,
1842
+ ResultDisposition,
854
1843
  ResultProvenance,
855
1844
  ResultRequest,
856
1845
  ResultResponse,
1846
+ RunMetadata,
1847
+ SIZE_CLASS_LIMITS,
1848
+ SUPPORTED_PROTOCOL_VERSIONS,
1849
+ SealedEnvelope,
1850
+ SealedOutcome,
1851
+ SizeClass,
1852
+ StoredKeys,
857
1853
  TERMINAL_STATES,
858
1854
  WireError,
859
1855
  WireErrorCode,
860
1856
  backendDescriptor,
861
1857
  canTransition,
1858
+ canonicalRequest,
1859
+ checkProtocolVersion,
1860
+ cryptoReady,
1861
+ declaredVersion,
862
1862
  effectiveOfferScope,
1863
+ fingerprint,
1864
+ generateKeys,
863
1865
  isBackendId,
864
1866
  isJobKind,
865
1867
  isLocalHost,
866
1868
  isTerminal,
1869
+ keyId,
1870
+ kindsOf,
867
1871
  matchAudience,
1872
+ mustsVerifiedBy,
1873
+ open,
868
1874
  payloadTextLength,
869
1875
  provenanceFor,
870
- resolveCost
1876
+ publicIdentityOf,
1877
+ resolveCost,
1878
+ seal,
1879
+ signRequest,
1880
+ signSiteRequest,
1881
+ signWith,
1882
+ sizeClassCeiling,
1883
+ sizeClassOf,
1884
+ verifyPublicIdentity,
1885
+ verifyRequest,
1886
+ verifySiteRequest,
1887
+ verifyWith
871
1888
  };
872
1889
  //# sourceMappingURL=index.js.map