@byollm/protocol 0.1.0-alpha.82 → 0.1.0-alpha.84

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,79 +1,198 @@
1
- // src/about.ts
2
- var ABOUT = `# About BYOLLM
3
-
4
- **What BYOLLM is**
5
-
6
- BYOLLM lets you use your own AI on websites. You install one small program on
7
- your computer. Then, websites that support BYOLLM can use the AI you already
8
- have \u2014 a free model running on your machine, or an AI service you already pay
9
- for \u2014 instead of the website paying for AI and passing the cost to you.
10
-
11
- **Why it matters**
12
-
13
- For you:
14
-
15
- - Your favorite model, everywhere you go.
16
- - New models the moment you get them \u2013 not when a site gets around to adding
17
- them.
18
- - Encrypted end-to-end. Your prompts go to your own device; byollm.cloud can't
19
- read them.
20
- - Sites never learn which model you use, and your subscriptions are never
21
- shared.
22
- - Pay less. Sites that don't pay for AI can charge you less \u2013 or nothing.
23
-
24
- For sites and developers:
25
-
26
- - Zero AI bills. Your users bring their own compute.
27
- - No floating money \u2013 you don't pay LLM bills up front and hope to collect
28
- later, and you never ask people to prepay just to try you.
29
- - Free trials that cost you nothing to offer.
30
- - Ship the AI features you kept private for fear of the API bill.
31
- - One small integration. Your users choose the models.
32
-
33
- **Your device**
34
-
35
- The \`byollm\` program runs on your computer. It knows which AI services you have
36
- set up: free open-source models on your machine, metered services you pay per
37
- use, or your own subscriptions like Claude Pro/Max. When a website you have
38
- enabled sends work, your device runs it with the service you chose. Your
39
- prompts are encrypted end-to-end to your own device. byollm.cloud passes them
40
- along and cannot read them.
41
-
42
- **Sites**
43
-
44
- A website that wants to use BYOLLM says what it needs \u2014 "writing help," "chat,"
45
- and so on. When you connect the site, you pick which of your services answers
46
- each one. The site never learns which model you use. You can turn a site off at
47
- any time, and it stops getting your work.
48
-
49
- **Teams (optional)**
50
-
51
- A team lets you share what runs on your devices with people you name \u2014 the free
52
- open-source models on your machine, or a metered service with a spending limit
53
- you set. Your subscription accounts (like Claude Pro/Max) are never shared with
54
- anyone. That is a rule, not a setting.
55
-
56
- **byollm.cloud (or your own relay)**
1
+ // src/wire.ts
2
+ import { z as z9 } from "zod";
57
3
 
58
- Many sites, many devices, many people. byollm.cloud keeps track of who has
59
- allowed what and sends each job to the right device. It never sees your
60
- prompts. If you would rather run this part yourself, the relay is open source \u2014
61
- you can run your own instead of using byollm.cloud.`;
62
- var ABOUT_SHORT_LEDE = "BYOLLM \u2013 Bring Your Own LLM \u2013 lets you use your own AI on websites you authorize. A small program installed on your machine lets you use your own models and subscriptions on any BYOLLM-integrated site, including new models the moment you get access \u2013 no site updates required. BYOLLM Cloud connects sites to your devices with end-to-end encryption, so no one, including us, can see your data.";
63
- var ABOUT_SHORT_TAIL = "Sites can charge you less because you bring your own \u2013 see why that matters \u2192. Teams can optionally share the free or metered services on their devices with people they name. Personal subscriptions are never shared.";
64
- var ABOUT_SHORT = `${ABOUT_SHORT_LEDE}
4
+ // src/keys.ts
5
+ import {
6
+ createHash,
7
+ createPrivateKey,
8
+ createPublicKey,
9
+ generateKeyPairSync,
10
+ sign,
11
+ verify
12
+ } from "crypto";
13
+ import { z } from "zod";
14
+ var PublicIdentity = z.object({
15
+ /** Raw Ed25519 public key. The pinned one. */
16
+ identity: z.string().min(1),
17
+ /** Raw X25519 public key, for sealing to this party. */
18
+ encryption: z.string().min(1),
19
+ /**
20
+ * Ed25519 signature over the encryption key, by the identity key.
21
+ *
22
+ * This is what stops an upstream substituting an encryption key of its
23
+ * own while relaying a genuine identity: the receiver pins the identity
24
+ * and refuses any encryption key not signed by it.
25
+ */
26
+ encryptionSig: z.string().min(1)
27
+ }).strict();
28
+ var StoredKeys = z.object({
29
+ version: z.literal(1),
30
+ identityPublic: z.string().min(1),
31
+ identityPrivate: z.string().min(1),
32
+ encryptionPublic: z.string().min(1),
33
+ encryptionPrivate: z.string().min(1),
34
+ encryptionSig: z.string().min(1),
35
+ createdAt: z.number().int().positive()
36
+ }).strict();
37
+ var ENCRYPTION_KEY_CONTEXT = "byollm/v1/encryption-key";
38
+ function rawPublic(key) {
39
+ const jwk = key.export({ format: "jwk" });
40
+ const x = jwk.x;
41
+ if (typeof x !== "string") throw new Error("key has no raw public component");
42
+ return x;
43
+ }
44
+ function importPublic(raw, crv) {
45
+ return createPublicKey({ key: { kty: "OKP", crv, x: raw }, format: "jwk" });
46
+ }
47
+ function importPrivate(stored) {
48
+ return createPrivateKey({
49
+ key: Buffer.from(stored, "base64"),
50
+ type: "pkcs8",
51
+ format: "der"
52
+ });
53
+ }
54
+ var exportPrivate = (key) => key.export({ type: "pkcs8", format: "der" }).toString("base64");
55
+ function generateKeys(now) {
56
+ const identity = generateKeyPairSync("ed25519");
57
+ const encryption = generateKeyPairSync("x25519");
58
+ const encryptionPublic = rawPublic(encryption.publicKey);
59
+ return {
60
+ version: 1,
61
+ identityPublic: rawPublic(identity.publicKey),
62
+ identityPrivate: exportPrivate(identity.privateKey),
63
+ encryptionPublic,
64
+ encryptionPrivate: exportPrivate(encryption.privateKey),
65
+ encryptionSig: sign(
66
+ null,
67
+ Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${encryptionPublic}`),
68
+ identity.privateKey
69
+ ).toString("base64url"),
70
+ createdAt: now
71
+ };
72
+ }
73
+ function publicIdentityOf(keys) {
74
+ return {
75
+ identity: keys.identityPublic,
76
+ encryption: keys.encryptionPublic,
77
+ encryptionSig: keys.encryptionSig
78
+ };
79
+ }
80
+ function verifyPublicIdentity(identity) {
81
+ try {
82
+ return verify(
83
+ null,
84
+ Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${identity.encryption}`),
85
+ importPublic(identity.identity, "Ed25519"),
86
+ Buffer.from(identity.encryptionSig, "base64url")
87
+ );
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+ function signWith(keys, data) {
93
+ return sign(null, data, importPrivate(keys.identityPrivate)).toString(
94
+ "base64url"
95
+ );
96
+ }
97
+ function verifyWith(identityPublic, data, signature) {
98
+ try {
99
+ return verify(
100
+ null,
101
+ data,
102
+ importPublic(identityPublic, "Ed25519"),
103
+ Buffer.from(signature, "base64url")
104
+ );
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
109
+ var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
110
+ function fingerprint(identityPublic) {
111
+ const digest = createHash("sha256").update(Buffer.from(identityPublic, "base64url")).digest();
112
+ let bits = 0;
113
+ let value = 0;
114
+ let out = "";
115
+ for (const byte of digest.subarray(0, 15)) {
116
+ value = value << 8 | byte;
117
+ bits += 8;
118
+ while (bits >= 5) {
119
+ out += ALPHABET.charAt(value >>> bits - 5 & 31);
120
+ bits -= 5;
121
+ }
122
+ }
123
+ const groups = out.match(/.{1,4}/g) ?? [];
124
+ return `BYOLLM-${groups.join("-")}`;
125
+ }
126
+ var keyId = (identityPublic) => fingerprint(identityPublic);
65
127
 
66
- ${ABOUT_SHORT_TAIL}`;
128
+ // src/succession.ts
129
+ import { z as z2 } from "zod";
130
+ var SUCCESSION_CONTEXT = "byollm/v1/site-succession";
131
+ var RETIREMENT_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
132
+ var MAX_SUCCESSION_CHAIN = 64;
133
+ var Succession = z2.object({
134
+ /**
135
+ * The predecessor's public identity — K1, in full.
136
+ *
137
+ * The whole identity rather than the key id, because a daemon meeting a
138
+ * chain it has not seen before has to *verify* each link, and a key id is
139
+ * a fingerprint: enough to compare, never enough to check a signature.
140
+ */
141
+ identity: PublicIdentity,
142
+ /** K1's signature over the statement naming K1 and its successor. */
143
+ signature: z2.string().min(1)
144
+ }).strict();
145
+ function successionStatement(fromKeyId, toKeyId) {
146
+ return Buffer.from(`${SUCCESSION_CONTEXT}:${fromKeyId}:${toKeyId}`);
147
+ }
148
+ function signSuccession(previous, next) {
149
+ return {
150
+ identity: {
151
+ identity: previous.identityPublic,
152
+ encryption: previous.encryptionPublic,
153
+ encryptionSig: previous.encryptionSig
154
+ },
155
+ signature: signWith(
156
+ previous,
157
+ successionStatement(keyId(previous.identityPublic), keyId(next.identity))
158
+ )
159
+ };
160
+ }
161
+ function verifyLink(link, toKeyId) {
162
+ if (!verifyPublicIdentity(link.identity)) return false;
163
+ return verifyWith(
164
+ link.identity.identity,
165
+ successionStatement(keyId(link.identity.identity), toKeyId),
166
+ link.signature
167
+ );
168
+ }
169
+ function walkSuccession(input) {
170
+ const { current, chain, approved } = input;
171
+ if (chain.length === 0) return { path: [current], failure: "no-chain" };
172
+ if (chain.length > MAX_SUCCESSION_CHAIN)
173
+ return { path: [current], failure: "too-long" };
174
+ const steps = [...chain].reverse();
175
+ const path = [current];
176
+ let succeeding = current;
177
+ for (const link of steps) {
178
+ if (!verifyLink(link, succeeding)) return { path, failure: "broken-link" };
179
+ const previous = keyId(link.identity.identity);
180
+ path.unshift(previous);
181
+ if (approved(previous)) return { path, from: previous };
182
+ succeeding = previous;
183
+ }
184
+ return { path, failure: "unknown-origin" };
185
+ }
67
186
 
68
187
  // src/audience.ts
69
- import { z as z2 } from "zod";
188
+ import { z as z4 } from "zod";
70
189
 
71
190
  // src/backends.ts
72
191
  import { isIP } from "net";
73
- import { z } from "zod";
74
- var BackendClass = z.enum(["http", "process"]);
192
+ import { z as z3 } from "zod";
193
+ var BackendClass = z3.enum(["http", "process"]);
75
194
  var BACKEND_CLASSES = Object.freeze(BackendClass.options);
76
- var BackendCost = z.enum(["free", "metered", "subscription"]);
195
+ var BackendCost = z3.enum(["free", "metered", "subscription"]);
77
196
  var backend = (b) => Object.freeze(b);
78
197
  var BACKENDS = Object.freeze({
79
198
  // -- free: local compute, costs electricity ------------------------------
@@ -252,7 +371,7 @@ var BACKENDS = Object.freeze({
252
371
  })
253
372
  });
254
373
  var BACKEND_IDS = Object.freeze(Object.keys(BACKENDS));
255
- var BackendIdSchema = z.enum(
374
+ var BackendIdSchema = z3.enum(
256
375
  BACKEND_IDS
257
376
  );
258
377
  function isBackendId(value) {
@@ -323,11 +442,11 @@ function classifyCost(id, baseUrl, model) {
323
442
  }
324
443
 
325
444
  // src/audience.ts
326
- var Audience = z2.enum(["private", "team"]);
327
- var OfferScope = z2.enum(["private", "team"]);
445
+ var Audience = z4.enum(["private", "team"]);
446
+ var OfferScope = z4.enum(["private", "team"]);
328
447
  var AUDIENCES = Object.freeze(Audience.options);
329
448
  var OFFER_SCOPES = Object.freeze(OfferScope.options);
330
- var MatchRefusal = z2.enum([
449
+ var MatchRefusal = z4.enum([
331
450
  /** The daemon advertises no capability for this kind. */
332
451
  "no-capability",
333
452
  /** Job is `private` but this daemon belongs to a different user. */
@@ -403,193 +522,12 @@ var REFUSAL_MESSAGES = Object.freeze({
403
522
  "metered-ceiling-reached": "this backend is shared but has reached the spend ceiling its owner set"
404
523
  });
405
524
 
406
- // src/kinds.ts
407
- import { z as z3 } from "zod";
408
- var PAYLOAD_LIMITS = Object.freeze({
409
- /** Max characters in any single text field. */
410
- maxTextChars: 1e6,
411
- /** Max messages in an `llm.chat` conversation. */
412
- maxMessages: 256,
413
- /** Max characters across the whole payload. */
414
- maxTotalChars: 4e6
415
- });
416
- var ChatMessage = z3.object({
417
- role: z3.enum(["system", "user", "assistant"]),
418
- content: z3.string().max(PAYLOAD_LIMITS.maxTextChars)
419
- }).strict();
420
- var GeneratePayload = z3.object({
421
- prompt: z3.string().min(1).max(PAYLOAD_LIMITS.maxTextChars),
422
- system: z3.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
423
- }).strict().refine(
424
- (payload) => payload.prompt.length + (payload.system?.length ?? 0) <= PAYLOAD_LIMITS.maxTotalChars,
425
- {
426
- message: `payload exceeds ${String(PAYLOAD_LIMITS.maxTotalChars)} characters`
427
- }
428
- );
429
- var ChatPayload = z3.object({
430
- messages: z3.array(ChatMessage).min(1).max(PAYLOAD_LIMITS.maxMessages),
431
- system: z3.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
432
- }).strict().refine(
433
- (payload) => payload.messages.reduce((sum, m) => sum + m.content.length, 0) + (payload.system?.length ?? 0) <= PAYLOAD_LIMITS.maxTotalChars,
434
- {
435
- message: `payload exceeds ${String(PAYLOAD_LIMITS.maxTotalChars)} characters`
436
- }
437
- );
438
- var JobKind = z3.enum(["llm.generate", "llm.chat"]);
439
- var JOB_KINDS = Object.freeze(JobKind.options);
440
- var KindedPayload = z3.discriminatedUnion("kind", [
441
- // Strict on the wrappers too. A union member that strips is a door beside
442
- // the one that is locked: the payloads inside are strict, and an extra key
443
- // on the envelope vanished just as quietly.
444
- z3.object({ kind: z3.literal("llm.generate"), payload: GeneratePayload }).strict(),
445
- z3.object({ kind: z3.literal("llm.chat"), payload: ChatPayload }).strict()
446
- ]);
447
- function isJobKind(value) {
448
- return JOB_KINDS.includes(value);
449
- }
450
- function payloadTextLength(kinded) {
451
- if (kinded.kind === "llm.generate") {
452
- return kinded.payload.prompt.length + (kinded.payload.system?.length ?? 0);
453
- }
454
- const messages = kinded.payload.messages.reduce(
455
- (sum, m) => sum + m.content.length,
456
- 0
457
- );
458
- return messages + (kinded.payload.system?.length ?? 0);
459
- }
460
-
461
525
  // src/job.ts
462
- import { z as z6 } from "zod";
526
+ import { z as z7 } from "zod";
463
527
 
464
528
  // src/grant.ts
465
529
  import { Buffer as Buffer2 } from "buffer";
466
530
  import { z as z5 } from "zod";
467
-
468
- // src/keys.ts
469
- import {
470
- createHash,
471
- createPrivateKey,
472
- createPublicKey,
473
- generateKeyPairSync,
474
- sign,
475
- verify
476
- } from "crypto";
477
- import { z as z4 } from "zod";
478
- var PublicIdentity = z4.object({
479
- /** Raw Ed25519 public key. The pinned one. */
480
- identity: z4.string().min(1),
481
- /** Raw X25519 public key, for sealing to this party. */
482
- encryption: z4.string().min(1),
483
- /**
484
- * Ed25519 signature over the encryption key, by the identity key.
485
- *
486
- * This is what stops an upstream substituting an encryption key of its
487
- * own while relaying a genuine identity: the receiver pins the identity
488
- * and refuses any encryption key not signed by it.
489
- */
490
- encryptionSig: z4.string().min(1)
491
- }).strict();
492
- var StoredKeys = z4.object({
493
- version: z4.literal(1),
494
- identityPublic: z4.string().min(1),
495
- identityPrivate: z4.string().min(1),
496
- encryptionPublic: z4.string().min(1),
497
- encryptionPrivate: z4.string().min(1),
498
- encryptionSig: z4.string().min(1),
499
- createdAt: z4.number().int().positive()
500
- }).strict();
501
- var ENCRYPTION_KEY_CONTEXT = "byollm/v1/encryption-key";
502
- function rawPublic(key) {
503
- const jwk = key.export({ format: "jwk" });
504
- const x = jwk.x;
505
- if (typeof x !== "string") throw new Error("key has no raw public component");
506
- return x;
507
- }
508
- function importPublic(raw, crv) {
509
- return createPublicKey({ key: { kty: "OKP", crv, x: raw }, format: "jwk" });
510
- }
511
- function importPrivate(stored) {
512
- return createPrivateKey({
513
- key: Buffer.from(stored, "base64"),
514
- type: "pkcs8",
515
- format: "der"
516
- });
517
- }
518
- var exportPrivate = (key) => key.export({ type: "pkcs8", format: "der" }).toString("base64");
519
- function generateKeys(now) {
520
- const identity = generateKeyPairSync("ed25519");
521
- const encryption = generateKeyPairSync("x25519");
522
- const encryptionPublic = rawPublic(encryption.publicKey);
523
- return {
524
- version: 1,
525
- identityPublic: rawPublic(identity.publicKey),
526
- identityPrivate: exportPrivate(identity.privateKey),
527
- encryptionPublic,
528
- encryptionPrivate: exportPrivate(encryption.privateKey),
529
- encryptionSig: sign(
530
- null,
531
- Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${encryptionPublic}`),
532
- identity.privateKey
533
- ).toString("base64url"),
534
- createdAt: now
535
- };
536
- }
537
- function publicIdentityOf(keys) {
538
- return {
539
- identity: keys.identityPublic,
540
- encryption: keys.encryptionPublic,
541
- encryptionSig: keys.encryptionSig
542
- };
543
- }
544
- function verifyPublicIdentity(identity) {
545
- try {
546
- return verify(
547
- null,
548
- Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${identity.encryption}`),
549
- importPublic(identity.identity, "Ed25519"),
550
- Buffer.from(identity.encryptionSig, "base64url")
551
- );
552
- } catch {
553
- return false;
554
- }
555
- }
556
- function signWith(keys, data) {
557
- return sign(null, data, importPrivate(keys.identityPrivate)).toString(
558
- "base64url"
559
- );
560
- }
561
- function verifyWith(identityPublic, data, signature) {
562
- try {
563
- return verify(
564
- null,
565
- data,
566
- importPublic(identityPublic, "Ed25519"),
567
- Buffer.from(signature, "base64url")
568
- );
569
- } catch {
570
- return false;
571
- }
572
- }
573
- var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
574
- function fingerprint(identityPublic) {
575
- const digest = createHash("sha256").update(Buffer.from(identityPublic, "base64url")).digest();
576
- let bits = 0;
577
- let value = 0;
578
- let out = "";
579
- for (const byte of digest.subarray(0, 15)) {
580
- value = value << 8 | byte;
581
- bits += 8;
582
- while (bits >= 5) {
583
- out += ALPHABET.charAt(value >>> bits - 5 & 31);
584
- bits -= 5;
585
- }
586
- }
587
- const groups = out.match(/.{1,4}/g) ?? [];
588
- return `BYOLLM-${groups.join("-")}`;
589
- }
590
- var keyId = (identityPublic) => fingerprint(identityPublic);
591
-
592
- // src/grant.ts
593
531
  var GRANT_MAX_AGE_MS = 12e4;
594
532
  var CLOCK_SKEW_WARN_MS = 3e4;
595
533
  var CLOCK_ATTRIBUTION_MS = 5e3;
@@ -694,8 +632,63 @@ function verifyGrant(input) {
694
632
  ) ? null : "bad-signature";
695
633
  }
696
634
 
635
+ // src/kinds.ts
636
+ import { z as z6 } from "zod";
637
+ var PAYLOAD_LIMITS = Object.freeze({
638
+ /** Max characters in any single text field. */
639
+ maxTextChars: 1e6,
640
+ /** Max messages in an `llm.chat` conversation. */
641
+ maxMessages: 256,
642
+ /** Max characters across the whole payload. */
643
+ maxTotalChars: 4e6
644
+ });
645
+ var ChatMessage = z6.object({
646
+ role: z6.enum(["system", "user", "assistant"]),
647
+ content: z6.string().max(PAYLOAD_LIMITS.maxTextChars)
648
+ }).strict();
649
+ var GeneratePayload = z6.object({
650
+ prompt: z6.string().min(1).max(PAYLOAD_LIMITS.maxTextChars),
651
+ system: z6.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
652
+ }).strict().refine(
653
+ (payload) => payload.prompt.length + (payload.system?.length ?? 0) <= PAYLOAD_LIMITS.maxTotalChars,
654
+ {
655
+ message: `payload exceeds ${String(PAYLOAD_LIMITS.maxTotalChars)} characters`
656
+ }
657
+ );
658
+ var ChatPayload = z6.object({
659
+ messages: z6.array(ChatMessage).min(1).max(PAYLOAD_LIMITS.maxMessages),
660
+ system: z6.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
661
+ }).strict().refine(
662
+ (payload) => payload.messages.reduce((sum, m) => sum + m.content.length, 0) + (payload.system?.length ?? 0) <= PAYLOAD_LIMITS.maxTotalChars,
663
+ {
664
+ message: `payload exceeds ${String(PAYLOAD_LIMITS.maxTotalChars)} characters`
665
+ }
666
+ );
667
+ var JobKind = z6.enum(["llm.generate", "llm.chat"]);
668
+ var JOB_KINDS = Object.freeze(JobKind.options);
669
+ var KindedPayload = z6.discriminatedUnion("kind", [
670
+ // Strict on the wrappers too. A union member that strips is a door beside
671
+ // the one that is locked: the payloads inside are strict, and an extra key
672
+ // on the envelope vanished just as quietly.
673
+ z6.object({ kind: z6.literal("llm.generate"), payload: GeneratePayload }).strict(),
674
+ z6.object({ kind: z6.literal("llm.chat"), payload: ChatPayload }).strict()
675
+ ]);
676
+ function isJobKind(value) {
677
+ return JOB_KINDS.includes(value);
678
+ }
679
+ function payloadTextLength(kinded) {
680
+ if (kinded.kind === "llm.generate") {
681
+ return kinded.payload.prompt.length + (kinded.payload.system?.length ?? 0);
682
+ }
683
+ const messages = kinded.payload.messages.reduce(
684
+ (sum, m) => sum + m.content.length,
685
+ 0
686
+ );
687
+ return messages + (kinded.payload.system?.length ?? 0);
688
+ }
689
+
697
690
  // src/job.ts
698
- var JobState = z6.enum([
691
+ var JobState = z7.enum([
699
692
  "queued",
700
693
  "claimed",
701
694
  "running",
@@ -727,7 +720,7 @@ var TRANSITIONS = Object.freeze({
727
720
  function canTransition(from, to) {
728
721
  return TRANSITIONS[from].includes(to);
729
722
  }
730
- var Lease = z6.object({
723
+ var Lease = z7.object({
731
724
  /**
732
725
  * Identifies *this* grant, not just its holder.
733
726
  *
@@ -742,20 +735,20 @@ var Lease = z6.object({
742
735
  * and release *is*, per lease, but not across leases, because nothing in
743
736
  * the request said which one.
744
737
  */
745
- id: z6.string().min(1),
738
+ id: z7.string().min(1),
746
739
  /** The runner holding the lease. */
747
- runnerId: z6.string().min(1),
740
+ runnerId: z7.string().min(1),
748
741
  /** Epoch milliseconds after which the claim is void. */
749
- expiresAt: z6.number().int().positive()
742
+ expiresAt: z7.number().int().positive()
750
743
  }).strict();
751
- var JobPayload = z6.union([GeneratePayload, ChatPayload]);
752
- var ClaimedJob = z6.object({
753
- id: z6.string().min(1),
744
+ var JobPayload = z7.union([GeneratePayload, ChatPayload]);
745
+ var ClaimedJob = z7.object({
746
+ id: z7.string().min(1),
754
747
  kind: JobKind,
755
748
  payload: JobPayload,
756
749
  audience: Audience,
757
750
  /** The app's id for the user who enqueued it. */
758
- owner: z6.string().min(1),
751
+ owner: z7.string().min(1),
759
752
  /**
760
753
  * Which site's job — V1-3.
761
754
  *
@@ -768,7 +761,7 @@ var ClaimedJob = z6.object({
768
761
  * one, and so this reads as what it is: a fact about where the work came
769
762
  * from, not a second copy of the routing key.
770
763
  */
771
- site: z6.string().min(1).optional(),
764
+ site: z7.string().min(1).optional(),
772
765
  /**
773
766
  * Which of the owner's services runs this — resolved, not requested.
774
767
  *
@@ -785,25 +778,25 @@ var ClaimedJob = z6.object({
785
778
  * Optional, because direct mode has no control plane to resolve anything
786
779
  * and the owner's own defaults answer under the ambiguity law.
787
780
  */
788
- service: z6.string().min(1).optional(),
781
+ service: z7.string().min(1).optional(),
789
782
  lease: Lease
790
783
  }).strict();
791
- var ResultProvenance = z6.object({
784
+ var ResultProvenance = z7.object({
792
785
  /** The audience the job ran under. */
793
786
  audience: Audience,
794
787
  /** The runner that produced it. */
795
- runnerId: z6.string().min(1),
788
+ runnerId: z7.string().min(1),
796
789
  /** The runner owner's id in this app's namespace. */
797
- runnerOwner: z6.string().min(1),
790
+ runnerOwner: z7.string().min(1),
798
791
  /** Which backend class produced it — an HTTP call or a sandboxed spawn. */
799
792
  backendClass: BackendClass,
800
793
  /** The model the runner reports having used. */
801
- model: z6.string().min(1),
794
+ model: z7.string().min(1),
802
795
  /**
803
796
  * False only for `self` jobs. When true the app MUST treat `text` as
804
797
  * untrusted third-party content.
805
798
  */
806
- untrusted: z6.boolean()
799
+ untrusted: z7.boolean()
807
800
  }).strict();
808
801
  function provenanceFor(input) {
809
802
  return {
@@ -815,35 +808,35 @@ function provenanceFor(input) {
815
808
  untrusted: input.audience !== "private"
816
809
  };
817
810
  }
818
- var RunMetadata = z6.object({
811
+ var RunMetadata = z7.object({
819
812
  /** Which model actually served it. */
820
- model: z6.string().min(1),
813
+ model: z7.string().min(1),
821
814
  backendClass: BackendClass,
822
815
  /** Wall-clock milliseconds the backend call took. */
823
- durationMs: z6.number().int().nonnegative()
816
+ durationMs: z7.number().int().nonnegative()
824
817
  }).strict();
825
- var JobResultOk = z6.object({
826
- outcome: z6.literal("ok"),
827
- text: z6.string(),
818
+ var JobResultOk = z7.object({
819
+ outcome: z7.literal("ok"),
820
+ text: z7.string(),
828
821
  /** Optional reference to a stored artifact; never a local path. */
829
- artifactUrl: z6.url().optional()
822
+ artifactUrl: z7.url().optional()
830
823
  }).strict();
831
- var JobResultError = z6.object({
832
- outcome: z6.literal("error"),
833
- code: z6.string().min(1),
834
- message: z6.string().min(1),
824
+ var JobResultError = z7.object({
825
+ outcome: z7.literal("error"),
826
+ code: z7.string().min(1),
827
+ message: z7.string().min(1),
835
828
  /** Whether the app may reasonably re-enqueue. */
836
- retryable: z6.boolean()
829
+ retryable: z7.boolean()
837
830
  }).strict();
838
- var JobResultCanceled = z6.object({
839
- outcome: z6.literal("canceled")
831
+ var JobResultCanceled = z7.object({
832
+ outcome: z7.literal("canceled")
840
833
  }).strict();
841
- var JobOutcome = z6.discriminatedUnion("outcome", [
834
+ var JobOutcome = z7.discriminatedUnion("outcome", [
842
835
  JobResultOk,
843
836
  JobResultError,
844
837
  JobResultCanceled
845
838
  ]);
846
- var RefusalReason = z6.enum([
839
+ var RefusalReason = z7.enum([
847
840
  /**
848
841
  * Two or more services answer this kind and the owner has named no default,
849
842
  * so the kind is withheld. Nobody may pick on the owner's behalf — the wrong
@@ -873,19 +866,19 @@ var RefusalReason = z6.enum([
873
866
  */
874
867
  "default-unusable"
875
868
  ]);
876
- var JobRefused = z6.object({
877
- outcome: z6.literal("refused"),
869
+ var JobRefused = z7.object({
870
+ outcome: z7.literal("refused"),
878
871
  reason: RefusalReason,
879
872
  /** Plain words for a human reading a log, never parsed. */
880
- message: z6.string().min(1)
873
+ message: z7.string().min(1)
881
874
  }).strict();
882
875
  var REFUSAL_TEXT = Object.freeze({
883
876
  "default-ambiguity": "this device serves that kind from more than one service and its owner has not chosen which",
884
877
  "default-unusable": "this device's default for that kind cannot run work for you"
885
878
  });
886
- var SealedOutcome = z6.object({ outcome: JobOutcome, ran: RunMetadata }).strict();
887
- var DeliveredResult = z6.object({
888
- jobId: z6.string().min(1),
879
+ var SealedOutcome = z7.object({ outcome: JobOutcome, ran: RunMetadata }).strict();
880
+ var DeliveredResult = z7.object({
881
+ jobId: z7.string().min(1),
889
882
  state: JobState,
890
883
  outcome: JobOutcome.optional(),
891
884
  provenance: ResultProvenance.optional(),
@@ -903,9 +896,9 @@ var DeliveredResult = z6.object({
903
896
  * runner ran the job, and the *server* stamps it — an app cannot supply
904
897
  * a substitute that hides what it is.
905
898
  */
906
- fallback: z6.literal(true).optional()
899
+ fallback: z7.literal(true).optional()
907
900
  }).strict();
908
- var SizeClass = z6.enum(["small", "medium", "large", "unbounded"]);
901
+ var SizeClass = z7.enum(["small", "medium", "large", "unbounded"]);
909
902
  var SIZE_CLASSES = Object.freeze(SizeClass.options);
910
903
  var MAX_ENVELOPE_BYTES = 10 * 1024 * 1024;
911
904
  function envelopeBytes(envelope) {
@@ -926,11 +919,11 @@ function sizeClassOf(textChars) {
926
919
  if (textChars <= SIZE_CLASS_LIMITS.medium) return "medium";
927
920
  return "large";
928
921
  }
929
- var JobStub = z6.object({
930
- id: z6.string().min(1),
922
+ var JobStub = z7.object({
923
+ id: z7.string().min(1),
931
924
  kind: JobKind,
932
925
  /** The app's id for the user who enqueued it. */
933
- owner: z6.string().min(1),
926
+ owner: z7.string().min(1),
934
927
  /**
935
928
  * Which site this job belongs to — byollm_009 Amendment A §A.3.
936
929
  *
@@ -954,7 +947,7 @@ var JobStub = z6.object({
954
947
  * overlap window, and a daemon re-keys its own map by verifying that
955
948
  * signature against the key it already pinned (§A.3.1).
956
949
  */
957
- site: z6.string().min(1),
950
+ site: z7.string().min(1),
958
951
  audience: Audience,
959
952
  // `audienceAllow` is **not** here, and its absence is the enforcement —
960
953
  // cloud_008 §0.2.
@@ -1009,12 +1002,12 @@ var JobStub = z6.object({
1009
1002
  * `NO_PAYLOAD_ROUTING` draws: the prompt cannot reach it, so no amount of
1010
1003
  * user text can influence what runs.
1011
1004
  */
1012
- purpose: z6.string().min(1).optional(),
1005
+ purpose: z7.string().min(1).optional(),
1013
1006
  sizeClass: SizeClass,
1014
1007
  /** Reserved for byollm_006. False until streaming exists. */
1015
- streaming: z6.boolean(),
1008
+ streaming: z7.boolean(),
1016
1009
  /** Epoch ms after which the work is pointless; bounds ciphertext retention. */
1017
- deadlineAt: z6.number().int().positive()
1010
+ deadlineAt: z7.number().int().positive()
1018
1011
  }).strict();
1019
1012
  var ClaimedStub = JobStub.extend({
1020
1013
  lease: Lease,
@@ -1024,21 +1017,21 @@ var ClaimedStub = JobStub.extend({
1024
1017
  // src/envelope.ts
1025
1018
  import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2 } from "crypto";
1026
1019
  import sodium from "libsodium-wrappers";
1027
- import { z as z7 } from "zod";
1020
+ import { z as z8 } from "zod";
1028
1021
  var readied;
1029
1022
  async function cryptoReady() {
1030
1023
  readied ??= sodium.ready;
1031
1024
  await readied;
1032
1025
  }
1033
1026
  var ENVELOPE_MAX_AGE_MS = 24 * 60 * 6e4;
1034
- var EnvelopeDirection = z7.enum(["payload", "result"]);
1035
- var SealedEnvelope = z7.object({
1027
+ var EnvelopeDirection = z8.enum(["payload", "result"]);
1028
+ var SealedEnvelope = z8.object({
1036
1029
  /** Base64url `crypto_box_seal` output over the signed plaintext. */
1037
- ciphertext: z7.string().min(1),
1030
+ ciphertext: z8.string().min(1),
1038
1031
  /** Who this was sealed to — the recipient checks it is them. */
1039
- recipientKeyId: z7.string().min(1),
1032
+ recipientKeyId: z8.string().min(1),
1040
1033
  /** Who signed it — the recipient checks this against its pin. */
1041
- senderKeyId: z7.string().min(1),
1034
+ senderKeyId: z8.string().min(1),
1042
1035
  direction: EnvelopeDirection,
1043
1036
  /**
1044
1037
  * When this ciphertext stops being worth keeping.
@@ -1052,7 +1045,7 @@ var SealedEnvelope = z7.object({
1052
1045
  * Not trusted as written: it is also inside the signature, so a changed
1053
1046
  * deadline fails to verify.
1054
1047
  */
1055
- deadlineAt: z7.number().int().positive()
1048
+ deadlineAt: z8.number().int().positive()
1056
1049
  }).strict();
1057
1050
  function signedBody(context, plaintext) {
1058
1051
  return Buffer.from(
@@ -1145,756 +1138,147 @@ async function open(input) {
1145
1138
  return { ok: true, plaintext: claims["plaintext"] };
1146
1139
  }
1147
1140
 
1148
- // src/signing.ts
1149
- import { createHash as createHash2 } from "crypto";
1150
- import { z as z8 } from "zod";
1151
- var MAX_CLOCK_SKEW_MS = 12e4;
1152
- var RequestSignature = z8.object({
1153
- /** Which runner is calling. The server looks up its pinned identity. */
1154
- runnerId: z8.string().min(1),
1155
- /** Epoch ms, bounded by {@link MAX_CLOCK_SKEW_MS}. */
1156
- issuedAt: z8.number().int().positive(),
1157
- /** Base64url Ed25519 signature over {@link canonicalRequest}. */
1158
- signature: z8.string().min(1)
1159
- }).strict();
1160
- function canonicalRequest(input) {
1161
- const digest = createHash2("sha256").update(input.body, "utf8").digest("hex");
1162
- return Buffer.from(
1163
- [
1164
- "byollm/v1/request",
1165
- input.endpoint,
1166
- input.runnerId,
1167
- String(input.issuedAt),
1168
- digest
1169
- ].join("\n"),
1170
- "utf8"
1171
- );
1172
- }
1173
- function signRequest(keys, input) {
1174
- return {
1175
- runnerId: input.runnerId,
1176
- issuedAt: input.issuedAt,
1177
- signature: signWith(keys, canonicalRequest(input))
1178
- };
1179
- }
1180
- function signSiteRequest(keys, input) {
1181
- return signRequest(keys, {
1182
- endpoint: siteEndpoint(input.endpoint),
1183
- runnerId: input.siteId,
1184
- issuedAt: input.issuedAt,
1185
- body: input.body
1186
- });
1141
+ // src/wire.ts
1142
+ var PROTOCOL_VERSION = "1";
1143
+ var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze([
1144
+ PROTOCOL_VERSION
1145
+ ]);
1146
+ var MIN_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0] ?? PROTOCOL_VERSION;
1147
+ function declaredVersion(input) {
1148
+ const { body, query } = input;
1149
+ if (typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion")) {
1150
+ return body.protocolVersion;
1151
+ }
1152
+ return query?.get("protocolVersion") ?? void 0;
1187
1153
  }
1188
- function verifySiteRequest(input) {
1189
- return verifyRequest({
1190
- ...input,
1191
- endpoint: siteEndpoint(input.endpoint)
1192
- });
1154
+ function checkProtocolVersion(body) {
1155
+ const declared = typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion") ? body.protocolVersion : void 0;
1156
+ if (typeof declared !== "string" || declared.length === 0) {
1157
+ return {
1158
+ error: "unsupported-protocol-version",
1159
+ message: `this request declared no protocol version. Upgrade the daemon: \`${UPGRADE_COMMAND}\`.`,
1160
+ supported: SUPPORTED_PROTOCOL_VERSIONS,
1161
+ minimum: MIN_PROTOCOL_VERSION
1162
+ };
1163
+ }
1164
+ if (!SUPPORTED_PROTOCOL_VERSIONS.includes(declared)) {
1165
+ return {
1166
+ error: "unsupported-protocol-version",
1167
+ 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."),
1168
+ supported: SUPPORTED_PROTOCOL_VERSIONS,
1169
+ minimum: MIN_PROTOCOL_VERSION
1170
+ };
1171
+ }
1172
+ return null;
1193
1173
  }
1194
- var siteEndpoint = (endpoint) => `site/${endpoint}`;
1195
- function verifyRequest(input) {
1196
- const skew = input.maxSkewMs ?? MAX_CLOCK_SKEW_MS;
1197
- if (Math.abs(input.now - input.signature.issuedAt) > skew) return "stale";
1198
- const ok = verifyWith(
1199
- input.identityPublic,
1200
- canonicalRequest({
1201
- endpoint: input.endpoint,
1202
- runnerId: input.signature.runnerId,
1203
- issuedAt: input.signature.issuedAt,
1204
- body: input.body
1205
- }),
1206
- input.signature.signature
1207
- );
1208
- return ok ? null : "bad-signature";
1209
- }
1210
-
1211
- // src/manifest.ts
1212
- import { z as z9 } from "zod";
1213
- var RESERVED_PURPOSE = "default";
1214
- var RENDERABLE = /^[^\p{Cc}\p{Cf}\p{Cs}\p{Co}]+$/u;
1215
- var renderable = (max, what) => z9.string().min(1).max(max).regex(
1216
- RENDERABLE,
1217
- `a ${what} is text a person reads \u2014 no control characters, direction overrides or zero-width padding`
1218
- ).refine((value) => value.trim() !== "", {
1219
- message: `a ${what} cannot be blank`
1220
- });
1221
- var PurposeKey = z9.string().regex(
1222
- /^[a-z0-9][a-z0-9-]*$/,
1223
- "a purpose key is a lowercase slug \u2014 letters, digits and hyphens"
1224
- ).max(64);
1225
- var Purpose = z9.object({
1174
+ var UPGRADE_COMMAND = "npm i -g byollm@latest";
1175
+ var PROTOCOL_PREFIX = "/byollm";
1176
+ var ENDPOINTS = Object.freeze([
1177
+ "pair",
1178
+ "claim",
1179
+ "fetch",
1180
+ "heartbeat",
1181
+ "result",
1182
+ "release"
1183
+ ]);
1184
+ var Capability = z9.object({
1185
+ kind: JobKind,
1226
1186
  /**
1227
- * What a person reads on the consent screen. The only rendered field.
1187
+ * The owner's name for the service answering this kind byollm_016.
1228
1188
  *
1229
- * Declared rather than derived from the key, because a key is a
1230
- * compromise between machines and this is not. "Writing Assistant" is
1231
- * what somebody understands; `writing-assistant` is what travels.
1189
+ * A device advertises *which* of its services serves a kind, not merely
1190
+ * that something does. **A site never sees this**, and never did after
1191
+ * Amendment L: it is what a control plane resolves a person's mapping
1192
+ * against, so that the service a grant names is one this device actually
1193
+ * offers rather than one somebody invented.
1194
+ *
1195
+ * `isDefault` used to sit beside it, saying which row an unselected job
1196
+ * took. Nothing selects any more — a job names a purpose and a person's
1197
+ * mapping names a service — so there is no unselected job for a default
1198
+ * to catch, and the field went with the machinery it served.
1232
1199
  */
1233
- label: renderable(80, "label"),
1234
- /** One line of context for the consent screen. Optional. */
1235
- description: renderable(280, "description").optional(),
1200
+ service: z9.string().min(1),
1201
+ backendId: BackendIdSchema,
1202
+ backendClass: BackendClass,
1203
+ model: z9.string().min(1),
1236
1204
  /**
1237
- * The kinds this purpose uses.
1205
+ * Models this device's CLI knows about — byollm_017 ruling 3.
1238
1206
  *
1239
- * A purpose may span kinds, and a mapping is per (purpose, kind) so a
1240
- * person can send this purpose's chat to one service and its generation
1241
- * to another. Listing a kind here is what makes that slot appear.
1207
+ * **Suggestions, not a vocabulary.** Free text is always allowed: the
1208
+ * promise is that a model released this morning works this morning, and a
1209
+ * frozen list anywhere a person picks from breaks that on the first day
1210
+ * it matters. What makes free text safe is ruling 2 — a model is probed
1211
+ * before it is stored, so "found is not works" is answered by the device
1212
+ * rather than by a list.
1213
+ *
1214
+ * Announced with the capability rather than kept in the dashboard,
1215
+ * because the answer is "what does THIS device's CLI know" and only the
1216
+ * device can say. A list held cloud-side would be one more thing to
1217
+ * update on release day, and wrong for anybody who had not upgraded.
1218
+ *
1219
+ * Optional, and empty is legal. A backend with nothing to suggest — a
1220
+ * local server serving one model — is not a backend in an error state,
1221
+ * and a reader must not render an absent list as "no models available".
1242
1222
  */
1243
- kinds: z9.array(JobKind).min(1).max(JOB_KINDS.length).refine((kinds) => new Set(kinds).size === kinds.length, {
1244
- message: "a purpose lists each kind once"
1245
- })
1223
+ knownModels: z9.array(z9.string().min(1)).optional(),
1224
+ offerScope: OfferScope
1246
1225
  }).strict();
1247
- var MAX_PURPOSES = 32;
1248
- var Manifest = z9.record(PurposeKey, Purpose).refine((manifest) => Object.keys(manifest).length > 0, {
1249
- message: "a manifest declares at least one purpose"
1250
- }).refine((manifest) => Object.keys(manifest).length <= MAX_PURPOSES, {
1251
- message: `a manifest declares at most ${String(MAX_PURPOSES)} purposes \u2014 a consent screen is a set of questions somebody answers one at a time`
1252
- }).refine((manifest) => !(RESERVED_PURPOSE in manifest), {
1253
- message: `"${RESERVED_PURPOSE}" is reserved for a site that declares no purposes of its own \u2014 give this one a name from your own vocabulary`
1254
- });
1255
- function singlePurposeManifest(input) {
1256
- return {
1257
- [RESERVED_PURPOSE]: { label: input.label, kinds: [...input.kinds] }
1258
- };
1259
- }
1260
-
1261
- // src/succession.ts
1262
- import { z as z10 } from "zod";
1263
- var SUCCESSION_CONTEXT = "byollm/v1/site-succession";
1264
- var RETIREMENT_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
1265
- var MAX_SUCCESSION_CHAIN = 64;
1266
- var Succession = z10.object({
1226
+ var CapabilityMatrix = z9.array(Capability);
1227
+ var WithheldKind = z9.object({
1228
+ kind: JobKind,
1229
+ claimants: z9.array(
1230
+ z9.object({ service: z9.string().min(1), offer: OfferScope }).strict()
1231
+ ).min(2)
1232
+ }).strict();
1233
+ var GrantRef = z9.object({ jobId: z9.string().min(1), leaseId: z9.string().min(1) }).strict();
1234
+ var PairStartRequest = z9.object({
1235
+ protocolVersion: z9.literal(PROTOCOL_VERSION),
1236
+ action: z9.literal("start"),
1237
+ daemon: z9.object({
1238
+ version: z9.string().min(1),
1239
+ /** Shown in the app's runner list so a user can tell their machines apart. */
1240
+ label: z9.string().min(1).max(120),
1241
+ platform: z9.enum(["darwin", "linux", "win32"])
1242
+ }).strict(),
1267
1243
  /**
1268
- * The predecessor's public identity K1, in full.
1244
+ * This machine's public keys (byollm_009 §5).
1269
1245
  *
1270
- * The whole identity rather than the key id, because a daemon meeting a
1271
- * chain it has not seen before has to *verify* each link, and a key id is
1272
- * a fingerprint: enough to compare, never enough to check a signature.
1246
+ * Pairing is where the two parties learn each other's identities, because
1247
+ * it is the one moment a human is already deciding to trust: the approval
1248
+ * click. A key exchanged anywhere else would be a key nobody chose.
1273
1249
  */
1274
- identity: PublicIdentity,
1275
- /** K1's signature over the statement naming K1 and its successor. */
1276
- signature: z10.string().min(1)
1250
+ device: PublicIdentity,
1251
+ capabilities: CapabilityMatrix
1277
1252
  }).strict();
1278
- function successionStatement(fromKeyId, toKeyId) {
1279
- return Buffer.from(`${SUCCESSION_CONTEXT}:${fromKeyId}:${toKeyId}`);
1280
- }
1281
- function signSuccession(previous, next) {
1282
- return {
1283
- identity: {
1284
- identity: previous.identityPublic,
1285
- encryption: previous.encryptionPublic,
1286
- encryptionSig: previous.encryptionSig
1287
- },
1288
- signature: signWith(
1289
- previous,
1290
- successionStatement(keyId(previous.identityPublic), keyId(next.identity))
1291
- )
1292
- };
1293
- }
1294
- function verifyLink(link, toKeyId) {
1295
- if (!verifyPublicIdentity(link.identity)) return false;
1296
- return verifyWith(
1297
- link.identity.identity,
1298
- successionStatement(keyId(link.identity.identity), toKeyId),
1299
- link.signature
1300
- );
1301
- }
1302
- function walkSuccession(input) {
1303
- const { current, chain, approved } = input;
1304
- if (chain.length === 0) return { path: [current], failure: "no-chain" };
1305
- if (chain.length > MAX_SUCCESSION_CHAIN)
1306
- return { path: [current], failure: "too-long" };
1307
- const steps = [...chain].reverse();
1308
- const path = [current];
1309
- let succeeding = current;
1310
- for (const link of steps) {
1311
- if (!verifyLink(link, succeeding)) return { path, failure: "broken-link" };
1312
- const previous = keyId(link.identity.identity);
1313
- path.unshift(previous);
1314
- if (approved(previous)) return { path, from: previous };
1315
- succeeding = previous;
1316
- }
1317
- return { path, failure: "unknown-origin" };
1318
- }
1319
-
1320
- // src/musts.ts
1321
- function kindsOf(must2) {
1322
- return typeof must2.verifiedBy === "string" ? [must2.verifiedBy] : must2.verifiedBy;
1323
- }
1324
- var must = (m) => Object.freeze(m);
1325
- var MUSTS = Object.freeze({
1326
- // ---- Pairing and identity -------------------------------------------
1327
- PAIR_ONE_USER: must({
1328
- id: "PAIR_ONE_USER",
1329
- statement: "A runner token MUST be bound to exactly one user; a daemon MUST refuse work not attributable to its paired user.",
1330
- enforcedBy: "both",
1331
- verifiedBy: "conformance",
1332
- source: "byollm_001 \xA7MUSTs"
1333
- }),
1334
- PAIR_INTERACTIVE: must({
1335
- id: "PAIR_INTERACTIVE",
1336
- 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.",
1337
- enforcedBy: "server",
1338
- verifiedBy: "conformance",
1339
- source: "byollm_001 \xA7Endpoints.1"
1340
- }),
1341
- PAIR_CODE_EXPIRES: must({
1342
- id: "PAIR_CODE_EXPIRES",
1343
- statement: "An unapproved device code MUST expire and MUST NOT be redeemable after expiry.",
1344
- enforcedBy: "server",
1345
- verifiedBy: "conformance",
1346
- source: "byollm_001 \xA7Endpoints.1"
1347
- }),
1348
- // ---- Typed job kinds --------------------------------------------------
1349
- VERSION_HANDSHAKE_REQUIRED: must({
1350
- id: "VERSION_HANDSHAKE_REQUIRED",
1351
- 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.",
1352
- enforcedBy: "both",
1353
- verifiedBy: "conformance",
1354
- source: "byollm_009 \xA74"
1355
- }),
1356
- SITE_KEY_BY_STUB: must({
1357
- id: "SITE_KEY_BY_STUB",
1358
- statement: "A daemon MUST verify a job's payload against the pinned key of the site the stub names, and MUST refuse a job naming a site it has not pinned. It MUST NOT fall back to another pinned key, and MUST refuse an envelope whose declared sender disagrees with the stub's site.",
1359
- enforcedBy: "daemon",
1360
- // Adversarial, and the reason is the finding that produced it: the
1361
- // honest paths pass with every site check deleted, because `open`
1362
- // refuses a signature from the wrong key anyway. What distinguishes an
1363
- // enforced rule from a coincidence here is a hostile pairing of stub and
1364
- // envelope, which no conformance client would ever send.
1365
- verifiedBy: "adversarial",
1366
- source: "byollm_009 \xA7A.3"
1367
- }),
1368
- SITES_LOCALLY_APPROVED: must({
1369
- id: "SITES_LOCALLY_APPROVED",
1370
- statement: "A daemon MUST NOT run work for a site on an upstream's word alone. An upstream may propose a site set; work for any site in it MUST additionally carry a grant signed by the control-plane key this daemon pinned at pairing. A key that has changed for an id this daemon already pinned MUST be refused for the life of the pairing, including after that id has left the set and returned. A **verified succession** is not a changed key: a new key id carrying a signature, by a key this daemon has already pinned, over a statement naming both key ids MUST be accepted \u2014 provided the control plane projects the same successor \u2014 and MUST be announced rather than applied silently. The first job from a site this daemon has never served MUST be announced at the machine.",
1371
- enforcedBy: "daemon",
1372
- // Two kinds, and the second is the one that matters — V1-1.
1253
+ var PairStartResponse = z9.object({
1254
+ /** Secret the daemon polls with. Never shown to the user. */
1255
+ deviceCode: z9.string().min(20),
1256
+ /** Short code the user reads and confirms in the browser. */
1257
+ userCode: z9.string().min(4).max(16),
1258
+ /** Where the user approves. Must be on the server's own origin. */
1259
+ verificationUrl: z9.url(),
1260
+ /** Epoch ms after which the code is dead ({@link MUSTS.PAIR_CODE_EXPIRES}). */
1261
+ expiresAt: z9.number().int().positive(),
1262
+ /** How often the daemon may poll. */
1263
+ pollIntervalMs: z9.number().int().min(500).max(6e4)
1264
+ }).strict();
1265
+ var PairPollRequest = z9.object({
1266
+ protocolVersion: z9.literal(PROTOCOL_VERSION),
1267
+ action: z9.literal("poll"),
1268
+ deviceCode: z9.string().min(20)
1269
+ }).strict();
1270
+ var PairPollResponse = z9.discriminatedUnion("status", [
1271
+ z9.object({ status: z9.literal("pending") }).strict(),
1272
+ z9.object({ status: z9.literal("denied") }).strict(),
1273
+ z9.object({ status: z9.literal("expired") }).strict(),
1274
+ z9.object({
1275
+ status: z9.literal("approved"),
1276
+ // `runnerToken` is gone — cloud_008 §2.4, finding 37.
1373
1277
  //
1374
- // `construction`: the daemon cannot serve a site that is not in its
1375
- // pinned map, and admission refuses before a payload is fetched — and
1376
- // since byollm_016 Amendment K, being in the map is no longer sufficient
1377
- // either: a signed grant is, and the relay proposing the set cannot
1378
- // produce one.
1379
- //
1380
- // `adversarial`: the property that survives is about a *sequence* —
1381
- // remove the id, re-offer it under a different key — which no honest
1382
- // upstream sends and which the fence above does not see. That was the
1383
- // bypass: the pin was deleted with the id, so the comparison had nothing
1384
- // to compare against and the substitution arrived as a stranger.
1385
- // **Not `conformance`, and that is a live gap rather than a judgement.**
1386
- // Amendment C's succession clause is a rule about two implementations
1387
- // agreeing, which is what a conformance check is for — but rotating a
1388
- // site's key is not something `ConformanceTarget` can express, and adding
1389
- // an optional hook that most targets omit would produce a check reporting
1390
- // success for a reason unrelated to the property it claims. That is this
1391
- // project's most-repeated bug, and it is not worth reintroducing for a
1392
- // stronger-sounding word in a table. The rotation path is verified by
1393
- // `site-rotation.test.ts` (both directions, against the shipped runner)
1394
- // and `relay/test/rotation.test.ts` (both planes, against the reference
1395
- // relay); the missing piece is a second *independent* implementation to
1396
- // check them against, and there is not one yet.
1397
- verifiedBy: ["construction", "adversarial"],
1398
- source: "byollm_009 \xA7B.2, Amendment C"
1399
- }),
1400
- KEYS_EXCHANGED_AT_CONSENT: must({
1401
- id: "KEYS_EXCHANGED_AT_CONSENT",
1402
- statement: "Pairing MUST exchange both parties' public identities; each side MUST verify that the encryption key is signed by the identity presenting it, and MUST pin the identity. Keys MUST NOT be delivered before approval.",
1403
- enforcedBy: "both",
1404
- verifiedBy: "conformance",
1405
- source: "byollm_009 \xA75"
1406
- }),
1407
- REQUESTS_SIGNED_NOT_BEARER: must({
1408
- id: "REQUESTS_SIGNED_NOT_BEARER",
1409
- 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.",
1410
- enforcedBy: "both",
1411
- verifiedBy: "conformance",
1412
- source: "byollm_009 \xA74.2"
1413
- }),
1414
- LEASE_SCOPED_BY_GRANT: must({
1415
- id: "LEASE_SCOPED_BY_GRANT",
1416
- 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.",
1417
- enforcedBy: "both",
1418
- verifiedBy: "conformance",
1419
- source: "byollm_009 \xA74.2"
1420
- }),
1421
- STUB_METADATA_EXHAUSTIVE: must({
1422
- id: "STUB_METADATA_EXHAUSTIVE",
1423
- 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.",
1424
- enforcedBy: "both",
1425
- verifiedBy: "conformance",
1426
- source: "byollm_009 \xA76"
1427
- }),
1428
- ENVELOPE_SEALED_AND_SIGNED: must({
1429
- id: "ENVELOPE_SEALED_AND_SIGNED",
1430
- 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.",
1431
- enforcedBy: "server",
1432
- verifiedBy: "conformance",
1433
- source: "byollm_009 \xA76"
1434
- }),
1435
- KIND_TYPED_ONLY: must({
1436
- id: "KIND_TYPED_ONLY",
1437
- statement: "Job kinds MUST resolve against handlers baked into the daemon. A daemon MUST refuse an unknown kind rather than guess.",
1438
- enforcedBy: "daemon",
1439
- verifiedBy: "conformance",
1440
- source: "byollm_001 \xA7Jobs are typed data"
1441
- }),
1442
- KIND_NO_CODE: must({
1443
- id: "KIND_NO_CODE",
1444
- 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.",
1445
- enforcedBy: "daemon",
1446
- verifiedBy: "conformance",
1447
- source: "byollm_001 \xA7Jobs are typed data; byollm_004 \xA71"
1448
- }),
1449
- // ---- Capability and claiming -----------------------------------------
1450
- CLAIM_REQUIRES_CAPABILITY: must({
1451
- id: "CLAIM_REQUIRES_CAPABILITY",
1452
- statement: "A daemon MUST NOT be given a job whose kind is absent from its advertised capability matrix.",
1453
- enforcedBy: "both",
1454
- verifiedBy: "conformance",
1455
- source: "byollm_001 \xA7MUSTs"
1456
- }),
1457
- CAPABILITY_IS_DETECTED: must({
1458
- id: "CAPABILITY_IS_DETECTED",
1459
- statement: "An advertised capability matrix MUST be the intersection of owner config and detected, healthy reality \u2014 never config alone.",
1460
- enforcedBy: "daemon",
1461
- verifiedBy: "conformance",
1462
- source: "byollm_002 \xA7Routing"
1463
- }),
1464
- CLAIM_ATOMIC: must({
1465
- id: "CLAIM_ATOMIC",
1466
- statement: "Claiming MUST be atomic: a job MUST NOT be handed to two runners concurrently.",
1467
- enforcedBy: "server",
1468
- verifiedBy: "conformance",
1469
- source: "byollm_001 \xA7Endpoints.2"
1470
- }),
1471
- // ---- Leases -----------------------------------------------------------
1472
- LEASE_HONORED: must({
1473
- id: "LEASE_HONORED",
1474
- 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.",
1475
- enforcedBy: "daemon",
1476
- verifiedBy: "conformance",
1477
- source: "byollm_001 \xA7MUSTs"
1478
- }),
1479
- LEASE_RECLAIMABLE: must({
1480
- id: "LEASE_RECLAIMABLE",
1481
- statement: "A lease that expires un-renewed MUST make its job claimable again with no loss of the job.",
1482
- enforcedBy: "server",
1483
- verifiedBy: "conformance",
1484
- source: "byollm_001 \xA7Endpoints.2"
1485
- }),
1486
- // ---- Audience and offer scope ----------------------------------------
1487
- AUDIENCE_BOTH_SIDES: must({
1488
- id: "AUDIENCE_BOTH_SIDES",
1489
- 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.",
1490
- enforcedBy: "both",
1491
- verifiedBy: "conformance",
1492
- source: "byollm_001 \xA7The audience model"
1493
- }),
1494
- SUBSCRIPTION_SELF_LOCK: must({
1495
- id: "SUBSCRIPTION_SELF_LOCK",
1496
- statement: "A subscription-class backend's offer scope MUST be 'private' and MUST NOT be widened by configuration.",
1497
- enforcedBy: "daemon",
1498
- verifiedBy: "conformance",
1499
- source: "byollm_001 \xA7The audience model"
1500
- }),
1501
- METERED_DEFAULTS_SELF: must({
1502
- id: "METERED_DEFAULTS_SELF",
1503
- statement: "A metered backend's effective offer scope MUST be 'private' unless the owner has explicitly acknowledged spending money on others' work.",
1504
- enforcedBy: "daemon",
1505
- verifiedBy: "conformance",
1506
- source: "byollm_007 \xA74"
1507
- }),
1508
- METERED_REQUIRES_CEILING: must({
1509
- id: "METERED_REQUIRES_CEILING",
1510
- statement: "A widened metered backend MUST carry a spend ceiling, and the daemon MUST refuse community work once it is reached.",
1511
- enforcedBy: "daemon",
1512
- verifiedBy: "conformance",
1513
- source: "byollm_007 \xA74"
1514
- }),
1515
- COST_NOT_CONFIGURABLE: must({
1516
- id: "COST_NOT_CONFIGURABLE",
1517
- statement: "A built-in provider's cost class MUST NOT be overridable by configuration.",
1518
- enforcedBy: "daemon",
1519
- verifiedBy: "conformance",
1520
- source: "byollm_007 \xA72"
1521
- }),
1522
- REMOTE_IS_NEVER_FREE: must({
1523
- id: "REMOTE_IS_NEVER_FREE",
1524
- statement: "A generic HTTP backend whose base URL is not loopback or private MUST be treated as metered.",
1525
- enforcedBy: "daemon",
1526
- verifiedBy: "conformance",
1527
- source: "byollm_007 \xA72"
1528
- }),
1529
- NAMED_LOCAL_ALLOWLIST: must({
1530
- id: "NAMED_LOCAL_ALLOWLIST",
1531
- statement: "A 'team' job MUST be admitted only by something the device itself verified, keyed by (server origin, user id) \u2014 never on the routing party's assertion alone.",
1532
- enforcedBy: "daemon",
1533
- verifiedBy: "conformance",
1534
- source: "byollm_001 Rev 1 \xA7B"
1535
- }),
1536
- REFUSAL_NOT_REOFFERED: must({
1537
- id: "REFUSAL_NOT_REOFFERED",
1538
- statement: "A server MUST NOT re-offer a job to a runner that released it with reason 'refused'.",
1539
- enforcedBy: "server",
1540
- verifiedBy: "conformance",
1541
- source: "byollm_001 Rev 1 \xA7B (loop resolved in build review)"
1542
- }),
1543
- // ---- Revocation and cancel -------------------------------------------
1544
- REVOCATION_HONORED: must({
1545
- id: "REVOCATION_HONORED",
1546
- statement: "A revoked daemon MUST stop claiming and MUST abandon in-flight work by the next heartbeat at the latest.",
1547
- enforcedBy: "daemon",
1548
- verifiedBy: "conformance",
1549
- source: "byollm_001 \xA7MUSTs"
1550
- }),
1551
- CANCEL_HONORED: must({
1552
- id: "CANCEL_HONORED",
1553
- 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'.",
1554
- enforcedBy: "daemon",
1555
- verifiedBy: "conformance",
1556
- source: "byollm_001 Rev 1 \xA7C"
1557
- }),
1558
- // ---- Lifecycle, dependencies, delivery -------------------------------
1559
- DEPENDS_ON_GATING: must({
1560
- id: "DEPENDS_ON_GATING",
1561
- statement: "A job MUST NOT be claimable until every job in its dependsOn set has reached the 'ok' state.",
1562
- enforcedBy: "server",
1563
- verifiedBy: "conformance",
1564
- source: "byollm_001 Rev 1 \xA7E"
1565
- }),
1566
- TTL_EXPIRY: must({
1567
- id: "TTL_EXPIRY",
1568
- 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.",
1569
- enforcedBy: "server",
1570
- verifiedBy: "conformance",
1571
- source: "byollm_001 Rev 1 \xA7D (TTL clock resolved in build review)"
1572
- }),
1573
- NO_RUNNER_SIGNAL: must({
1574
- id: "NO_RUNNER_SIGNAL",
1575
- 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.",
1576
- enforcedBy: "server",
1577
- verifiedBy: "conformance",
1578
- source: "byollm_001 Rev 1 \xA7D"
1579
- }),
1580
- RESULT_IDEMPOTENT: must({
1581
- id: "RESULT_IDEMPOTENT",
1582
- statement: "Result submission MUST be idempotent by job id; the first terminal outcome wins and later submissions MUST NOT change it.",
1583
- enforcedBy: "server",
1584
- verifiedBy: "conformance",
1585
- source: "byollm_001 \xA7Endpoints.4"
1586
- }),
1587
- PROVENANCE_NAMES_DEVICE: must({
1588
- id: "PROVENANCE_NAMES_DEVICE",
1589
- statement: "A result MUST carry the claiming device's key id and its relationship to the requester, to the delivery seam, so an app never treats volunteer output as first-party. The key id MUST be the device the upstream granted the lease to, and a result whose signature does not verify against that device MUST be refused rather than recorded.",
1590
- enforcedBy: "server",
1591
- verifiedBy: "conformance",
1592
- source: "byollm_009 \xA711"
1593
- }),
1594
- // ---- The trust surface -------------------------------------------------
1595
- INGRESS_LOGGED_BEFORE_EXECUTION: must({
1596
- id: "INGRESS_LOGGED_BEFORE_EXECUTION",
1597
- statement: "Every executed prompt MUST be appended to the local ingress log before execution begins.",
1598
- enforcedBy: "daemon",
1599
- verifiedBy: "conformance",
1600
- source: "byollm_001 \xA7MUSTs"
1601
- }),
1602
- // ---- Execution isolation (byollm_004) ---------------------------------
1603
- NO_SHELL_INTERPOLATION: must({
1604
- id: "NO_SHELL_INTERPOLATION",
1605
- 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.",
1606
- enforcedBy: "daemon",
1607
- verifiedBy: "adversarial",
1608
- source: "byollm_004 \xA72"
1609
- }),
1610
- /**
1611
- * Amended for byollm_016 Phase B, and the amendment is deliberately narrow.
1612
- *
1613
- * A site may now name a **service** on the stub. The temptation is to read
1614
- * that as a crack in this law, so the statement below says exactly where the
1615
- * line is: a name selects from a menu the owner published, and resolves to a
1616
- * model, backend, base URL and flags **only** through that owner's own
1617
- * config. The site supplies a key; the owner supplies every value it maps
1618
- * to. A name the owner does not advertise is refused rather than
1619
- * substituted, because substitution is how "you may pick from my list" turns
1620
- * into "you may ask for anything and get something".
1621
- *
1622
- * Two properties keep it from drifting into "sites demand models":
1623
- *
1624
- * 1. **Nothing the site sends is ever a value.** No model string, no URL,
1625
- * no flag crosses the wire — only a key that means nothing off this
1626
- * owner's machine.
1627
- * 2. **It is a stub field, never a payload field.** The prompt cannot
1628
- * reach it. That is unchanged and is the sentence the second clause
1629
- * below still enforces verbatim.
1630
- */
1631
- NO_PAYLOAD_ROUTING: must({
1632
- id: "NO_PAYLOAD_ROUTING",
1633
- statement: "Model, backend, base URL, and flags MUST come from owner config only; a payload MUST NOT influence any of them. A stub MAY name a service the owner advertises, which selects among that owner's own config entries and MUST NOT introduce any value the owner did not write; an unadvertised name MUST be refused, never substituted.",
1634
- enforcedBy: "daemon",
1635
- verifiedBy: "adversarial",
1636
- source: "byollm_004 \xA72, amended byollm_016 \xA7Phase B"
1637
- }),
1638
- STRIPPED_CHILD_ENV: must({
1639
- id: "STRIPPED_CHILD_ENV",
1640
- 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.",
1641
- enforcedBy: "daemon",
1642
- verifiedBy: "adversarial",
1643
- source: "byollm_004 \xA72"
1644
- }),
1645
- HTTP_BASE_URL_SAFE: must({
1646
- id: "HTTP_BASE_URL_SAFE",
1647
- 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.",
1648
- enforcedBy: "daemon",
1649
- verifiedBy: "adversarial",
1650
- source: "byollm_004 Rev 1 \xA7Backend taxonomy"
1651
- }),
1652
- OUTPUT_INERT: must({
1653
- id: "OUTPUT_INERT",
1654
- 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.",
1655
- enforcedBy: "daemon",
1656
- verifiedBy: "adversarial",
1657
- source: "byollm_004 \xA72"
1658
- }),
1659
- COMMUNITY_BUDGETS: must({
1660
- id: "COMMUNITY_BUDGETS",
1661
- statement: "Jobs whose owner is not the daemon's owner MUST be subject to the owner's rate limits, daily cap, and resource budget.",
1662
- enforcedBy: "daemon",
1663
- verifiedBy: "adversarial",
1664
- source: "byollm_004 \xA74"
1665
- }),
1666
- REVOCATION_IMMEDIATE: must({
1667
- id: "REVOCATION_IMMEDIATE",
1668
- statement: "Revocation MUST take effect at the upstream at once \u2014 a revoked runner MUST NOT be granted further work from the moment the record changes \u2014 and MUST reach the daemon by its next heartbeat.",
1669
- // Both, and stated as one sentence with two obligations rather than
1670
- // folded into REVOCATION_HONORED. That one binds the *daemon*: a revoked
1671
- // daemon stops claiming and abandons in-flight work. This binds the
1672
- // *upstream*. byollm_009 §5 is explicit that the pair is the point — "a
1673
- // revocation enforced at one end survives a compromise of that end" — and
1674
- // one entry covering both would make a compromised daemon look compliant.
1675
- enforcedBy: "both",
1676
- verifiedBy: "conformance",
1677
- source: "byollm_009 \xA711"
1678
- }),
1679
- CONSENT_BEFORE_ROUTE: must({
1680
- id: "CONSENT_BEFORE_ROUTE",
1681
- statement: "An upstream MUST NOT route a job to a device without a record binding that user, that site and that scope. There MUST be no discovery path by which a device receives work it was never granted.",
1682
- enforcedBy: "server",
1683
- verifiedBy: "conformance",
1684
- source: "byollm_009 \xA711"
1685
- }),
1686
- ROSTER_NOT_DISCLOSED: must({
1687
- id: "ROSTER_NOT_DISCLOSED",
1688
- statement: "A site MUST NOT learn the membership of a group whose compute it uses, and MUST NOT publish membership to a routing party. No wire message may carry a list of who may run a job.",
1689
- // Checkable since cloud_008 §0.2 took `audienceAllow` off the stub: the
1690
- // property now holds by *absence*, and absence is exactly what a strict
1691
- // schema and a serialised stub can be asked about. Before that it was a
1692
- // sentence — and one this project cited in code comments, tests and two
1693
- // specs as though it were enforced data, which is why it is worth
1694
- // stating precisely rather than generously.
1695
- enforcedBy: "both",
1696
- verifiedBy: "conformance",
1697
- source: "byollm_009 \xA711"
1698
- }),
1699
- EFFECTIVE_OFFER_ONLY: must({
1700
- id: "EFFECTIVE_OFFER_ONLY",
1701
- statement: "A daemon MUST declare effective offers only. An upstream MUST NOT receive raw config, allowlists, or capacity the owner has not shared, and MUST act on the declared offer rather than on what was asked for.",
1702
- enforcedBy: "both",
1703
- verifiedBy: "conformance",
1704
- source: "byollm_009 \xA711"
1705
- }),
1706
- FALLBACK_LABELED: must({
1707
- id: "FALLBACK_LABELED",
1708
- statement: "Work served by anything other than the user's own compute MUST be labelled as such wherever it is reported, and MUST NOT be silently substituted.",
1709
- // `construction` today, and deliberately not `conformance`. Nothing on
1710
- // the wire yet distinguishes a fallback from any other community job —
1711
- // the ledger that would give it a surface is unbuilt — so a check would
1712
- // have to assert something it cannot observe. Promoted the day that
1713
- // surface exists. Marking it `conformance` now would put "verified"
1714
- // beside a property no third party can see, which is the one thing the
1715
- // kinds exist to prevent.
1716
- enforcedBy: "both",
1717
- verifiedBy: "construction",
1718
- source: "byollm_009 \xA711"
1719
- }),
1720
- RELAY_BLIND: must({
1721
- id: "RELAY_BLIND",
1722
- statement: "A relay MUST NOT hold any key capable of decrypting a payload, a result, or a delta frame.",
1723
- // Operator: a third party can read the relay's types and see there is
1724
- // nowhere to put such a key, but the kit certifies a *server* and cannot
1725
- // reach inside somebody's deployment to prove what it holds.
1726
- enforcedBy: "server",
1727
- verifiedBy: "operator",
1728
- source: "byollm_009 \xA711"
1729
- }),
1730
- SHARED_COMPUTE_DISCLOSED: must({
1731
- id: "SHARED_COMPUTE_DISCLOSED",
1732
- statement: "Before a user's work first runs on compute they do not own, they MUST be told in plain language that the machine's owner can see it.",
1733
- // Operator, and cloud_008 §0.3 is why the classification now comes with a
1734
- // standing answer rather than a standing question. The screen is not
1735
- // wire-observable, but the *string the server composes* is, and it is
1736
- // now unit-tested with the two false sentences forbidden by name. The
1737
- // kind stays `operator` because a third-party site can still render
1738
- // whatever it likes; what changed is that the part inside our own
1739
- // boundary stopped depending on somebody remembering to audit it.
1740
- enforcedBy: "server",
1741
- verifiedBy: "operator",
1742
- source: "byollm_009 \xA711"
1743
- })
1744
- });
1745
- var RETIRED_MUSTS = Object.freeze({
1746
- RESULT_PROVENANCE: {
1747
- supersededBy: "PROVENANCE_NAMES_DEVICE",
1748
- note: "Strengthened, not renamed: attribution is now by proof of possession \u2014 the result's signature must verify against the device the upstream granted the lease to \u2014 rather than by a provenance label travelling beside it. byollm_009 \xA711 states the stronger form."
1749
- }
1750
- });
1751
- var MUST_IDS = Object.freeze(Object.keys(MUSTS));
1752
- function mustsVerifiedBy(kind) {
1753
- return MUST_IDS.filter((id) => kindsOf(MUSTS[id]).includes(kind));
1754
- }
1755
-
1756
- // src/wire.ts
1757
- import { z as z11 } from "zod";
1758
- var PROTOCOL_VERSION = "1";
1759
- var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze([
1760
- PROTOCOL_VERSION
1761
- ]);
1762
- var MIN_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0] ?? PROTOCOL_VERSION;
1763
- function declaredVersion(input) {
1764
- const { body, query } = input;
1765
- if (typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion")) {
1766
- return body.protocolVersion;
1767
- }
1768
- return query?.get("protocolVersion") ?? void 0;
1769
- }
1770
- function checkProtocolVersion(body) {
1771
- const declared = typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion") ? body.protocolVersion : void 0;
1772
- if (typeof declared !== "string" || declared.length === 0) {
1773
- return {
1774
- error: "unsupported-protocol-version",
1775
- message: `this request declared no protocol version. Upgrade the daemon: \`${UPGRADE_COMMAND}\`.`,
1776
- supported: SUPPORTED_PROTOCOL_VERSIONS,
1777
- minimum: MIN_PROTOCOL_VERSION
1778
- };
1779
- }
1780
- if (!SUPPORTED_PROTOCOL_VERSIONS.includes(declared)) {
1781
- return {
1782
- error: "unsupported-protocol-version",
1783
- message: `this server speaks protocol ${SUPPORTED_PROTOCOL_VERSIONS.join(", ")} and the daemon asked for ${declared}. ` + (declared < MIN_PROTOCOL_VERSION ? `Upgrade the daemon: \`${UPGRADE_COMMAND}\`.` : "This daemon is newer than the server; the server needs upgrading."),
1784
- supported: SUPPORTED_PROTOCOL_VERSIONS,
1785
- minimum: MIN_PROTOCOL_VERSION
1786
- };
1787
- }
1788
- return null;
1789
- }
1790
- var UPGRADE_COMMAND = "npm i -g byollm@latest";
1791
- var PROTOCOL_PREFIX = "/byollm";
1792
- var ENDPOINTS = Object.freeze([
1793
- "pair",
1794
- "claim",
1795
- "fetch",
1796
- "heartbeat",
1797
- "result",
1798
- "release"
1799
- ]);
1800
- var Capability = z11.object({
1801
- kind: JobKind,
1802
- /**
1803
- * The owner's name for the service answering this kind — byollm_016.
1804
- *
1805
- * A device advertises *which* of its services serves a kind, not merely
1806
- * that something does. **A site never sees this**, and never did after
1807
- * Amendment L: it is what a control plane resolves a person's mapping
1808
- * against, so that the service a grant names is one this device actually
1809
- * offers rather than one somebody invented.
1810
- *
1811
- * `isDefault` used to sit beside it, saying which row an unselected job
1812
- * took. Nothing selects any more — a job names a purpose and a person's
1813
- * mapping names a service — so there is no unselected job for a default
1814
- * to catch, and the field went with the machinery it served.
1815
- */
1816
- service: z11.string().min(1),
1817
- backendId: BackendIdSchema,
1818
- backendClass: BackendClass,
1819
- model: z11.string().min(1),
1820
- /**
1821
- * Models this device's CLI knows about — byollm_017 ruling 3.
1822
- *
1823
- * **Suggestions, not a vocabulary.** Free text is always allowed: the
1824
- * promise is that a model released this morning works this morning, and a
1825
- * frozen list anywhere a person picks from breaks that on the first day
1826
- * it matters. What makes free text safe is ruling 2 — a model is probed
1827
- * before it is stored, so "found is not works" is answered by the device
1828
- * rather than by a list.
1829
- *
1830
- * Announced with the capability rather than kept in the dashboard,
1831
- * because the answer is "what does THIS device's CLI know" and only the
1832
- * device can say. A list held cloud-side would be one more thing to
1833
- * update on release day, and wrong for anybody who had not upgraded.
1834
- *
1835
- * Optional, and empty is legal. A backend with nothing to suggest — a
1836
- * local server serving one model — is not a backend in an error state,
1837
- * and a reader must not render an absent list as "no models available".
1838
- */
1839
- knownModels: z11.array(z11.string().min(1)).optional(),
1840
- offerScope: OfferScope
1841
- }).strict();
1842
- var CapabilityMatrix = z11.array(Capability);
1843
- var WithheldKind = z11.object({
1844
- kind: JobKind,
1845
- claimants: z11.array(
1846
- z11.object({ service: z11.string().min(1), offer: OfferScope }).strict()
1847
- ).min(2)
1848
- }).strict();
1849
- var GrantRef = z11.object({ jobId: z11.string().min(1), leaseId: z11.string().min(1) }).strict();
1850
- var PairStartRequest = z11.object({
1851
- protocolVersion: z11.literal(PROTOCOL_VERSION),
1852
- action: z11.literal("start"),
1853
- daemon: z11.object({
1854
- version: z11.string().min(1),
1855
- /** Shown in the app's runner list so a user can tell their machines apart. */
1856
- label: z11.string().min(1).max(120),
1857
- platform: z11.enum(["darwin", "linux", "win32"])
1858
- }).strict(),
1859
- /**
1860
- * This machine's public keys (byollm_009 §5).
1861
- *
1862
- * Pairing is where the two parties learn each other's identities, because
1863
- * it is the one moment a human is already deciding to trust: the approval
1864
- * click. A key exchanged anywhere else would be a key nobody chose.
1865
- */
1866
- device: PublicIdentity,
1867
- capabilities: CapabilityMatrix
1868
- }).strict();
1869
- var PairStartResponse = z11.object({
1870
- /** Secret the daemon polls with. Never shown to the user. */
1871
- deviceCode: z11.string().min(20),
1872
- /** Short code the user reads and confirms in the browser. */
1873
- userCode: z11.string().min(4).max(16),
1874
- /** Where the user approves. Must be on the server's own origin. */
1875
- verificationUrl: z11.url(),
1876
- /** Epoch ms after which the code is dead ({@link MUSTS.PAIR_CODE_EXPIRES}). */
1877
- expiresAt: z11.number().int().positive(),
1878
- /** How often the daemon may poll. */
1879
- pollIntervalMs: z11.number().int().min(500).max(6e4)
1880
- }).strict();
1881
- var PairPollRequest = z11.object({
1882
- protocolVersion: z11.literal(PROTOCOL_VERSION),
1883
- action: z11.literal("poll"),
1884
- deviceCode: z11.string().min(20)
1885
- }).strict();
1886
- var PairPollResponse = z11.discriminatedUnion("status", [
1887
- z11.object({ status: z11.literal("pending") }).strict(),
1888
- z11.object({ status: z11.literal("denied") }).strict(),
1889
- z11.object({ status: z11.literal("expired") }).strict(),
1890
- z11.object({
1891
- status: z11.literal("approved"),
1892
- // `runnerToken` is gone — cloud_008 §2.4, finding 37.
1893
- //
1894
- // It was minted here, hashed into `RunnerRecord.tokenHash`, written to
1895
- // the daemon's pairings file, and then **never sent, never looked up
1896
- // and never compared**. `getRunnerByTokenHash` existed on both stores
1897
- // and was called by nothing but a test asserting it returns null.
1278
+ // It was minted here, hashed into `RunnerRecord.tokenHash`, written to
1279
+ // the daemon's pairings file, and then **never sent, never looked up
1280
+ // and never compared**. `getRunnerByTokenHash` existed on both stores
1281
+ // and was called by nothing but a test asserting it returns null.
1898
1282
  //
1899
1283
  // Not merely dead wire, which is what `audienceAllow` and
1900
1284
  // `HeartbeatResponse.leases` were. This was a *secret*: minted,
@@ -1905,11 +1289,11 @@ var PairPollResponse = z11.discriminatedUnion("status", [
1905
1289
  // `REQUESTS_SIGNED_NOT_BEARER` was already the rule and was already
1906
1290
  // enforced — every authenticated call is signed by the device's pinned
1907
1291
  // identity key. This removes the thing the MUST is named after.
1908
- runnerId: z11.string().min(1),
1292
+ runnerId: z9.string().min(1),
1909
1293
  /** The app's id for the approving user — this daemon's owner forever. */
1910
- owner: z11.string().min(1),
1294
+ owner: z9.string().min(1),
1911
1295
  /** Display name for the trust UI, if the app offers one. */
1912
- ownerLabel: z11.string().optional(),
1296
+ ownerLabel: z9.string().optional(),
1913
1297
  /**
1914
1298
  * The sites this pairing covers, for the daemon to pin (byollm_009 §5),
1915
1299
  * keyed by each site's identity key id — cloud_009 §5.
@@ -1928,7 +1312,7 @@ var PairPollResponse = z11.discriminatedUnion("status", [
1928
1312
  * runner's lookup is a map read rather than a join across two
1929
1313
  * namespaces.
1930
1314
  */
1931
- sites: z11.record(z11.string().min(1), PublicIdentity),
1315
+ sites: z9.record(z9.string().min(1), PublicIdentity),
1932
1316
  /**
1933
1317
  * The control plane's grant-signing key, pinned here — Amendment J.
1934
1318
  *
@@ -1949,34 +1333,34 @@ var PairPollResponse = z11.discriminatedUnion("status", [
1949
1333
  * Rotation is Amendment C's, with no path where a grant teaches a
1950
1334
  * daemon a new key.
1951
1335
  */
1952
- controlPlanePublic: z11.string().min(1).optional()
1336
+ controlPlanePublic: z9.string().min(1).optional()
1953
1337
  }).strict()
1954
1338
  ]);
1955
- var PairRequest = z11.discriminatedUnion("action", [
1339
+ var PairRequest = z9.discriminatedUnion("action", [
1956
1340
  PairStartRequest,
1957
1341
  PairPollRequest
1958
1342
  ]);
1959
- var ClaimRequest = z11.object({
1960
- protocolVersion: z11.literal(PROTOCOL_VERSION),
1961
- runnerId: z11.string().min(1),
1343
+ var ClaimRequest = z9.object({
1344
+ protocolVersion: z9.literal(PROTOCOL_VERSION),
1345
+ runnerId: z9.string().min(1),
1962
1346
  /** Re-sent on every claim so a server never matches against a stale matrix. */
1963
1347
  capabilities: CapabilityMatrix,
1964
1348
  /** Upper bound on jobs to return; the server may return fewer. */
1965
- max: z11.number().int().min(1).max(64)
1349
+ max: z9.number().int().min(1).max(64)
1966
1350
  }).strict();
1967
- var ClaimResponse = z11.object({
1351
+ var ClaimResponse = z9.object({
1968
1352
  /**
1969
1353
  * Stubs, not jobs. The payload arrives from `fetch`, sealed to whichever
1970
1354
  * device claimed — see {@link JobStub} for the exhaustive metadata list.
1971
1355
  */
1972
- jobs: z11.array(ClaimedStub),
1356
+ jobs: z9.array(ClaimedStub),
1973
1357
  /** Lease duration granted, so the daemon knows its renewal deadline. */
1974
- leaseMs: z11.number().int().positive()
1358
+ leaseMs: z9.number().int().positive()
1975
1359
  }).strict();
1976
- var HeartbeatRequest = z11.object({
1977
- protocolVersion: z11.literal(PROTOCOL_VERSION),
1978
- runnerId: z11.string().min(1),
1979
- daemonVersion: z11.string().min(1),
1360
+ var HeartbeatRequest = z9.object({
1361
+ protocolVersion: z9.literal(PROTOCOL_VERSION),
1362
+ runnerId: z9.string().min(1),
1363
+ daemonVersion: z9.string().min(1),
1980
1364
  capabilities: CapabilityMatrix,
1981
1365
  /**
1982
1366
  * Kinds this device is withholding, and why it can be said.
@@ -1985,18 +1369,18 @@ var HeartbeatRequest = z11.object({
1985
1369
  * older daemon against a newer hub is simply a device with no withheld
1986
1370
  * kinds rather than a parse failure.
1987
1371
  */
1988
- withheld: z11.array(WithheldKind).default([]),
1372
+ withheld: z9.array(WithheldKind).default([]),
1989
1373
  /**
1990
1374
  * Leases this daemon believes it holds; the server renews exactly these.
1991
1375
  *
1992
1376
  * Lease ids rather than job ids, so a replayed heartbeat cannot renew a
1993
1377
  * grant the runner no longer holds — see {@link Lease.id}.
1994
1378
  */
1995
- activeLeases: z11.array(GrantRef),
1379
+ activeLeases: z9.array(GrantRef),
1996
1380
  /** True while the owner has the daemon paused; the server stops offering work. */
1997
- paused: z11.boolean()
1381
+ paused: z9.boolean()
1998
1382
  }).strict();
1999
- var HeartbeatResponse = z11.object({
1383
+ var HeartbeatResponse = z9.object({
2000
1384
  /**
2001
1385
  * The sites this daemon may serve, right now — cloud_008 finding 59.
2002
1386
  *
@@ -2014,7 +1398,7 @@ var HeartbeatResponse = z11.object({
2014
1398
  * rather than being told a second time — two fields for one fact is how
2015
1399
  * they drift.
2016
1400
  */
2017
- sites: z11.record(z11.string().min(1), PublicIdentity),
1401
+ sites: z9.record(z9.string().min(1), PublicIdentity),
2018
1402
  /**
2019
1403
  * How a site's current key traces back to one this daemon already holds —
2020
1404
  * byollm_009 Amendment C.
@@ -2032,11 +1416,11 @@ var HeartbeatResponse = z11.object({
2032
1416
  * history is public by construction, because a daemon that cannot read it
2033
1417
  * cannot verify it.
2034
1418
  */
2035
- successions: z11.record(
2036
- z11.string().min(1),
2037
- z11.object({
1419
+ successions: z9.record(
1420
+ z9.string().min(1),
1421
+ z9.object({
2038
1422
  /** Oldest last, as the projection carries it. */
2039
- succeeds: z11.array(Succession).max(MAX_SUCCESSION_CHAIN),
1423
+ succeeds: z9.array(Succession).max(MAX_SUCCESSION_CHAIN),
2040
1424
  /**
2041
1425
  * Until when the superseded key may still sign work — epoch ms.
2042
1426
  *
@@ -2045,7 +1429,7 @@ var HeartbeatResponse = z11.object({
2045
1429
  * window indefinitely would be a two-key site forever, decided by
2046
1430
  * the party this design does not trust.
2047
1431
  */
2048
- retiringUntil: z11.number().int().positive().optional()
1432
+ retiringUntil: z9.number().int().positive().optional()
2049
1433
  }).strict()
2050
1434
  ).optional(),
2051
1435
  /**
@@ -2058,8 +1442,8 @@ var HeartbeatResponse = z11.object({
2058
1442
  * is the unique grant and the daemon already keys its work by it; this is
2059
1443
  * the same shape `activeLeases` sends in the other direction.
2060
1444
  */
2061
- cancel: z11.array(
2062
- z11.object({ jobId: z11.string().min(1), leaseId: z11.string().min(1) }).strict()
1445
+ cancel: z9.array(
1446
+ z9.object({ jobId: z9.string().min(1), leaseId: z9.string().min(1) }).strict()
2063
1447
  ),
2064
1448
  // `leases` is deliberately absent — cloud_008 §1.4b, finding 16.
2065
1449
  //
@@ -2089,11 +1473,11 @@ var HeartbeatResponse = z11.object({
2089
1473
  * ambiguous across sites, and "the lease you no longer hold" is exactly
2090
1474
  * what this field means anyway.
2091
1475
  */
2092
- lost: z11.array(
2093
- z11.object({ jobId: z11.string().min(1), leaseId: z11.string().min(1) }).strict()
1476
+ lost: z9.array(
1477
+ z9.object({ jobId: z9.string().min(1), leaseId: z9.string().min(1) }).strict()
2094
1478
  ),
2095
1479
  /** Server clock, so a daemon with a skewed clock still honors leases. */
2096
- serverTime: z11.number().int().positive(),
1480
+ serverTime: z9.number().int().positive(),
2097
1481
  /**
2098
1482
  * Sites whose disclosure the user must read again before work moves —
2099
1483
  * cloud_008 finding 48, named rather than counted.
@@ -2108,13 +1492,36 @@ var HeartbeatResponse = z11.object({
2108
1492
  * operator stopped it" — one word with two subjects on two halves of one
2109
1493
  * exchange is a confusion nobody untangles from a log.
2110
1494
  */
2111
- awaitingConsent: z11.array(z11.string().min(1))
1495
+ awaitingConsent: z9.array(z9.string().min(1)),
1496
+ /**
1497
+ * A version this daemon should move itself to — B053.
1498
+ *
1499
+ * The channel the auto-updater reads, and it is the channel the daemon
1500
+ * already polls rather than a new phone-home, which is what the ruling
1501
+ * asked for (016 §Auto-update).
1502
+ *
1503
+ * **The hub may not send this to every daemon.** This schema is
1504
+ * `.strict()`, so a daemon built before the field exists does not ignore
1505
+ * it — it rejects the whole heartbeat and stops working. Which would mean
1506
+ * the message carrying the update is the message that breaks the machines
1507
+ * it was meant to update.
1508
+ *
1509
+ * That is decidable without any new handshake, because the request
1510
+ * already carries `daemonVersion`. {@link mayOfferUpdate} is the rule,
1511
+ * kept here as code rather than as a paragraph in a runbook, so both
1512
+ * sides read the same one.
1513
+ *
1514
+ * Exact versions only, never a tag: the daemon refuses anything else, and
1515
+ * a fleet resolving one tag at different minutes is a fleet on different
1516
+ * builds reporting one number.
1517
+ */
1518
+ updateTo: z9.string().min(1).optional()
2112
1519
  }).strict();
2113
- var ResultDisposition = z11.enum(["ok", "error", "canceled"]);
2114
- var ResultRequest = z11.object({
2115
- protocolVersion: z11.literal(PROTOCOL_VERSION),
2116
- runnerId: z11.string().min(1),
2117
- jobId: z11.string().min(1),
1520
+ var ResultDisposition = z9.enum(["ok", "error", "canceled"]);
1521
+ var ResultRequest = z9.object({
1522
+ protocolVersion: z9.literal(PROTOCOL_VERSION),
1523
+ runnerId: z9.string().min(1),
1524
+ jobId: z9.string().min(1),
2118
1525
  /**
2119
1526
  * The grant this result was produced under — cloud_008 §1.4a.
2120
1527
  *
@@ -2137,7 +1544,7 @@ var ResultRequest = z11.object({
2137
1544
  * learned once already, when a replayed release yanked a later grant, and
2138
1545
  * it applies here for the same reason.
2139
1546
  */
2140
- leaseId: z11.string().min(1),
1547
+ leaseId: z9.string().min(1),
2141
1548
  /**
2142
1549
  * The outcome, sealed to the site and signed by the device.
2143
1550
  *
@@ -2169,12 +1576,12 @@ var ResultRequest = z11.object({
2169
1576
  // on it, so it is a class a routing party consumes. Nobody between the
2170
1577
  // two ends consumes these.
2171
1578
  }).strict();
2172
- var ResultResponse = z11.object({
1579
+ var ResultResponse = z9.object({
2173
1580
  /**
2174
1581
  * False when this submission wrote nothing — the daemon should discard,
2175
1582
  * not retry ({@link MUSTS.RESULT_IDEMPOTENT}).
2176
1583
  */
2177
- accepted: z11.boolean(),
1584
+ accepted: z9.boolean(),
2178
1585
  /**
2179
1586
  * True when this device had already recorded this job's result.
2180
1587
  *
@@ -2188,196 +1595,923 @@ var ResultResponse = z11.object({
2188
1595
  * the same refusal it would get for a job that is *not* terminal, so a job
2189
1596
  * id cannot be used as a terminality probe.
2190
1597
  */
2191
- duplicate: z11.boolean().optional(),
1598
+ duplicate: z9.boolean().optional(),
2192
1599
  /** The job's state after this submission. */
2193
- state: z11.string().min(1)
1600
+ state: z9.string().min(1)
1601
+ }).strict();
1602
+ var ReleaseRequest = z9.object({
1603
+ protocolVersion: z9.literal(PROTOCOL_VERSION),
1604
+ runnerId: z9.string().min(1),
1605
+ /**
1606
+ * Which leases to release — the grant, not just the job.
1607
+ *
1608
+ * A release naming only a job id releases whatever lease exists at the
1609
+ * moment it arrives, which for a replayed request is not the lease the
1610
+ * daemon meant. See {@link Lease.id}.
1611
+ */
1612
+ leases: z9.array(GrantRef),
1613
+ /**
1614
+ * Why, so the app's runner list can say something true.
1615
+ *
1616
+ * `refused` is load-bearing, not cosmetic: the server cannot evaluate
1617
+ * what a device will admit (§4.2), so it may legitimately offer
1618
+ * a job this daemon then declines. The server MUST record the refusal and
1619
+ * stop offering that job to that runner, or the pair would spin between
1620
+ * claim and release forever.
1621
+ */
1622
+ reason: z9.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
2194
1623
  }).strict();
2195
- var ReleaseRequest = z11.object({
2196
- protocolVersion: z11.literal(PROTOCOL_VERSION),
2197
- runnerId: z11.string().min(1),
1624
+ var ReleaseResponse = z9.object({
1625
+ released: z9.array(z9.string().min(1))
1626
+ }).strict();
1627
+ var WireErrorCode = z9.enum([
1628
+ "bad-request",
1629
+ "unsupported-protocol-version",
1630
+ /**
1631
+ * The daemon is older than this hub will serve — B052.
1632
+ *
1633
+ * Distinct from `unsupported-protocol-version`, which is about the
1634
+ * contract; this is about the build. A daemon can speak protocol 1
1635
+ * perfectly and still be old enough that we would rather move it than keep
1636
+ * carrying it — and the two need different remedies in the message, since
1637
+ * one is "your daemon and this server disagree" and the other is "yours
1638
+ * works, and it is time".
1639
+ *
1640
+ * The floor is the backstop to the auto-updater (B053), and the only lever
1641
+ * that reaches a machine which never opted into offers.
1642
+ */
1643
+ "daemon-below-floor",
1644
+ // "We do not know who you are." Exactly 401, and only that — cloud_008
1645
+ // §1.4d.
1646
+ "unauthorized",
1647
+ /**
1648
+ * "We know exactly who you are, and the answer is no." Exactly 403.
1649
+ *
1650
+ * Five refusals across both planes served 403 with `unauthorized`, whose
1651
+ * table entry is 401: a revoked device, a site claiming another site's
1652
+ * stub, a job you do not hold, a device belonging to another owner, a
1653
+ * relay that does not route for you. Every one of them is an *identified*
1654
+ * caller being refused.
1655
+ *
1656
+ * Collapsing the two loses a distinction that matters everywhere it is
1657
+ * read: a revoked daemon would look like an unsigned one in every log and
1658
+ * every client branch, and "check your keys" is the wrong advice for both
1659
+ * of them in opposite directions.
1660
+ */
1661
+ "forbidden",
1662
+ "revoked",
1663
+ "not-found",
1664
+ // Claimed, but the site has not sealed the payload yet — cloud_008 §1.4.
1665
+ //
1666
+ // A daemon must retry rather than abandon: the job is legitimately still
1667
+ // its own until the lease or the awaiting-payload clock says otherwise.
1668
+ // That is why it cannot be `not-found` or `server-error`, and why it was
1669
+ // the protocol gap that produced a bare 409 in the first place.
1670
+ "not-ready",
1671
+ /**
1672
+ * The job is over, and this call is about a job — V1-6, and the code the
1673
+ * site plane has been serving without one (V1-13).
1674
+ *
1675
+ * Distinct from `not-found`, which says "no such job", and from
1676
+ * `not-ready`, which says "not yet, keep asking". This one says "yes, and
1677
+ * it finished" — so a daemon must stop rather than retry, and a replayed
1678
+ * request must not be able to reopen it.
1679
+ */
1680
+ "too-late",
1681
+ // The caller's clock is too far from ours to judge a signature's freshness.
1682
+ //
1683
+ // Split out from `unauthorized` because the remedy is completely different
1684
+ // and only the server can tell them apart: a bad signature means the key is
1685
+ // wrong, this means the machine's time is wrong. A daemon reporting it as a
1686
+ // generic rejection sends its owner looking at their network.
1687
+ "clock-skew",
1688
+ "rate-limited",
1689
+ "server-error"
1690
+ ]);
1691
+ var WireError = z9.object({
1692
+ error: WireErrorCode,
1693
+ message: z9.string().min(1),
1694
+ /**
1695
+ * What this server speaks, on `unsupported-protocol-version` — §B.4.
1696
+ *
1697
+ * The refusal has carried these since the version handshake existed and
1698
+ * the enumeration did not model them, so the one error that exists to be
1699
+ * *acted on* was the one that failed to parse as a wire error. Found by
1700
+ * the relay's own suite the day the relay started sending it: a refusal
1701
+ * outside the enumerated shape is a refusal a client cannot branch on,
1702
+ * which is the whole reason §1.4 enumerates them.
1703
+ *
1704
+ * Modelled the way `clock-skew`'s two fields already are — code-specific
1705
+ * extras, refused on any other code by the refinement below.
1706
+ */
1707
+ supported: z9.array(z9.string().min(1)).optional(),
1708
+ minimum: z9.string().min(1).optional(),
1709
+ /**
1710
+ * The oldest daemon this hub serves, on `daemon-below-floor` — B052.
1711
+ *
1712
+ * Carried for the same reason `supported` and `minimum` are: a refusal
1713
+ * that cannot be branched on is a refusal a client can only print. The
1714
+ * message already names the floor for a person; this names it for the
1715
+ * code, so a surface can say "you are two versions under" without
1716
+ * parsing English.
1717
+ */
1718
+ floor: z9.string().min(1).optional(),
1719
+ /** Seconds; mirrors Retry-After for `rate-limited` and `server-error`. */
1720
+ retryAfter: z9.number().int().nonnegative().optional(),
1721
+ /**
1722
+ * The server's clock, and the window it allows. `clock-skew` only.
1723
+ *
1724
+ * So the far side can say *how far off* rather than *that something is
1725
+ * wrong* — the difference between "adjust your clock by four minutes" and
1726
+ * "something is wrong with your connection". Not a disclosure: the
1727
+ * heartbeat response returns the same value, and so does every `Date`
1728
+ * header.
1729
+ */
1730
+ serverTime: z9.number().int().positive().optional(),
1731
+ maxSkewMs: z9.number().int().positive().optional()
1732
+ }).strict().superRefine((error, ctx) => {
1733
+ const skew = error.error === "clock-skew";
1734
+ const carried = error.serverTime !== void 0 || error.maxSkewMs !== void 0;
1735
+ if (skew && !carried) {
1736
+ ctx.addIssue({
1737
+ code: "custom",
1738
+ message: "clock-skew must carry serverTime and maxSkewMs"
1739
+ });
1740
+ }
1741
+ if (!skew && carried) {
1742
+ ctx.addIssue({
1743
+ code: "custom",
1744
+ message: `${error.error} must not carry serverTime or maxSkewMs`
1745
+ });
1746
+ }
1747
+ const floored = error.error === "daemon-below-floor";
1748
+ if (floored && error.floor === void 0) {
1749
+ ctx.addIssue({
1750
+ code: "custom",
1751
+ message: "daemon-below-floor must carry floor"
1752
+ });
1753
+ }
1754
+ if (!floored && error.floor !== void 0) {
1755
+ ctx.addIssue({
1756
+ code: "custom",
1757
+ message: `${error.error} must not carry floor`
1758
+ });
1759
+ }
1760
+ const version = error.error === "unsupported-protocol-version";
1761
+ const versionFields = error.supported !== void 0 || error.minimum !== void 0;
1762
+ if (version && !versionFields) {
1763
+ ctx.addIssue({
1764
+ code: "custom",
1765
+ message: "unsupported-protocol-version must carry supported and minimum"
1766
+ });
1767
+ }
1768
+ if (!version && versionFields) {
1769
+ ctx.addIssue({
1770
+ code: "custom",
1771
+ message: `${error.error} must not carry supported or minimum`
1772
+ });
1773
+ }
1774
+ });
1775
+ var ERROR_STATUS = Object.freeze({
1776
+ "bad-request": 400,
1777
+ "unsupported-protocol-version": 400,
1778
+ /**
1779
+ * 426 Upgrade Required — B052, and it is the one status that says this.
1780
+ *
1781
+ * Not 403, which this daemon reads as a permission problem and which
1782
+ * sits beside `revoked` in every log. Not 400, which reads as a
1783
+ * malformed request; the request was perfect and the sender is old.
1784
+ *
1785
+ * It also fails safely on a daemon that predates the code: 426 is not in
1786
+ * that switch, so it lands on the 4xx default — `rejected`, which is
1787
+ * "never retried: the request is wrong, and repeating it stays wrong".
1788
+ * Vaguer than the remedy, and the right behaviour, which is what a
1789
+ * fallback has to be.
1790
+ */
1791
+ "daemon-below-floor": 426,
1792
+ unauthorized: 401,
1793
+ forbidden: 403,
1794
+ revoked: 403,
1795
+ "not-found": 404,
1796
+ // 409, not 404: the job exists and is yours, it is simply not ready.
1797
+ "not-ready": 409,
1798
+ // The same 409 as `not-ready` and the opposite instruction: that one says
1799
+ // keep asking, this one says stop. The status is the class of the
1800
+ // problem — a request that does not fit the resource's state — and the
1801
+ // code is what a caller acts on.
1802
+ "too-late": 409,
1803
+ // 401 alongside `unauthorized`, because that is what it is — the
1804
+ // signature could not be judged. The code is what carries the remedy.
1805
+ "clock-skew": 401,
1806
+ "rate-limited": 429,
1807
+ "server-error": 500
1808
+ });
1809
+ var FetchRequest = z9.object({
1810
+ // `literal`, like every other request — V1-17. This one said
1811
+ // `string().min(1)`, so a daemon speaking a version this server does not
1812
+ // know got past the handshake on the one endpoint that hands over a
1813
+ // sealed payload. The version check exists so that a mismatch is a named
1814
+ // refusal rather than a schema failure three fields later; here it was
1815
+ // neither.
1816
+ protocolVersion: z9.literal(PROTOCOL_VERSION),
1817
+ runnerId: z9.string().min(1),
1818
+ jobId: z9.string().min(1),
2198
1819
  /**
2199
- * Which leases to release — the grant, not just the job.
1820
+ * The grant this daemon holds.
2200
1821
  *
2201
- * A release naming only a job id releases whatever lease exists at the
2202
- * moment it arrives, which for a replayed request is not the lease the
2203
- * daemon meant. See {@link Lease.id}.
1822
+ * Named, not inferred: a fetch is lease-scoped, and a request that names
1823
+ * only the job would be answerable for whatever lease exists when it
1824
+ * arrives ({@link Lease.id}).
2204
1825
  */
2205
- leases: z11.array(GrantRef),
1826
+ leaseId: z9.string().min(1)
1827
+ }).strict();
1828
+ var FetchResponse = z9.object({
2206
1829
  /**
2207
- * Why, so the app's runner list can say something true.
1830
+ * The work, sealed to the device that claimed it byollm_009 §6.
2208
1831
  *
2209
- * `refused` is load-bearing, not cosmetic: the server cannot evaluate
2210
- * what a device will admit (§4.2), so it may legitimately offer
2211
- * a job this daemon then declines. The server MUST record the refusal and
2212
- * stop offering that job to that runner, or the pair would spin between
2213
- * claim and release forever.
1832
+ * Not plaintext. The site opens its own at-rest envelope and re-seals to
1833
+ * the claiming device's key, signed by the site's identity, so the work
1834
+ * is readable only by the machine that took it and only if it came from
1835
+ * the site that machine pinned.
2214
1836
  */
2215
- reason: z11.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
1837
+ envelope: SealedEnvelope
2216
1838
  }).strict();
2217
- var ReleaseResponse = z11.object({
2218
- released: z11.array(z11.string().min(1))
1839
+
1840
+ // src/update-offer.ts
1841
+ var UPDATE_OFFER_SINCE = "0.1.0-alpha.83";
1842
+ function parse(version) {
1843
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(
1844
+ version
1845
+ );
1846
+ if (match === null) return void 0;
1847
+ return {
1848
+ release: [Number(match[1]), Number(match[2]), Number(match[3])],
1849
+ pre: match[4] === void 0 ? [] : match[4].split(".").map((part) => /^\d+$/.test(part) ? Number(part) : part)
1850
+ };
1851
+ }
1852
+ function compareVersions(a, b) {
1853
+ const left = parse(a);
1854
+ const right = parse(b);
1855
+ if (left === void 0 || right === void 0) return void 0;
1856
+ for (let i = 0; i < 3; i += 1) {
1857
+ const diff = (left.release[i] ?? 0) - (right.release[i] ?? 0);
1858
+ if (diff !== 0) return diff < 0 ? -1 : 1;
1859
+ }
1860
+ if (left.pre.length === 0 && right.pre.length > 0) return 1;
1861
+ if (left.pre.length > 0 && right.pre.length === 0) return -1;
1862
+ for (let i = 0; i < Math.max(left.pre.length, right.pre.length); i += 1) {
1863
+ const l = left.pre[i];
1864
+ const r = right.pre[i];
1865
+ if (l === void 0) return -1;
1866
+ if (r === void 0) return 1;
1867
+ if (l === r) continue;
1868
+ if (typeof l === "number" && typeof r === "number") return l < r ? -1 : 1;
1869
+ if (typeof l === "number") return -1;
1870
+ if (typeof r === "number") return 1;
1871
+ return l < r ? -1 : 1;
1872
+ }
1873
+ return 0;
1874
+ }
1875
+ function mayOfferUpdate(daemonVersion) {
1876
+ const order = compareVersions(daemonVersion, UPDATE_OFFER_SINCE);
1877
+ return order !== void 0 && order >= 0;
1878
+ }
1879
+ function checkDaemonFloor(input) {
1880
+ const order = compareVersions(input.daemonVersion, input.floor);
1881
+ if (order === void 0 || order >= 0) return null;
1882
+ return {
1883
+ error: "daemon-below-floor",
1884
+ message: `byollm ${input.daemonVersion} is below the supported floor (${input.floor}). Run \`${input.upgradeCommand}\`, then \`byollm start\`.`,
1885
+ floor: input.floor
1886
+ };
1887
+ }
1888
+ function updateOfferFor(input) {
1889
+ const { offer, daemonVersion } = input;
1890
+ if (offer === void 0) return {};
1891
+ if (exactOffer(offer) === void 0) return {};
1892
+ if (!mayOfferUpdate(daemonVersion)) return {};
1893
+ if (daemonVersion === offer) return {};
1894
+ return { updateTo: offer };
1895
+ }
1896
+ function exactOffer(value) {
1897
+ return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(value) ? value : void 0;
1898
+ }
1899
+
1900
+ // src/about.ts
1901
+ var ABOUT = `# About BYOLLM
1902
+
1903
+ **What BYOLLM is**
1904
+
1905
+ BYOLLM lets you use your own AI on websites. You install one small program on
1906
+ your computer. Then, websites that support BYOLLM can use the AI you already
1907
+ have \u2014 a free model running on your machine, or an AI service you already pay
1908
+ for \u2014 instead of the website paying for AI and passing the cost to you.
1909
+
1910
+ **Why it matters**
1911
+
1912
+ For you:
1913
+
1914
+ - Your favorite model, everywhere you go.
1915
+ - New models the moment you get them \u2013 not when a site gets around to adding
1916
+ them.
1917
+ - Encrypted end-to-end. Your prompts go to your own device; byollm.cloud can't
1918
+ read them.
1919
+ - Sites never learn which model you use, and your subscriptions are never
1920
+ shared.
1921
+ - Pay less. Sites that don't pay for AI can charge you less \u2013 or nothing.
1922
+
1923
+ For sites and developers:
1924
+
1925
+ - Zero AI bills. Your users bring their own compute.
1926
+ - No floating money \u2013 you don't pay LLM bills up front and hope to collect
1927
+ later, and you never ask people to prepay just to try you.
1928
+ - Free trials that cost you nothing to offer.
1929
+ - Ship the AI features you kept private for fear of the API bill.
1930
+ - One small integration. Your users choose the models.
1931
+
1932
+ **Your device**
1933
+
1934
+ The \`byollm\` program runs on your computer. It knows which AI services you have
1935
+ set up: free open-source models on your machine, metered services you pay per
1936
+ use, or your own subscriptions like Claude Pro/Max. When a website you have
1937
+ enabled sends work, your device runs it with the service you chose. Your
1938
+ prompts are encrypted end-to-end to your own device. byollm.cloud passes them
1939
+ along and cannot read them.
1940
+
1941
+ **Sites**
1942
+
1943
+ A website that wants to use BYOLLM says what it needs \u2014 "writing help," "chat,"
1944
+ and so on. When you connect the site, you pick which of your services answers
1945
+ each one. The site never learns which model you use. You can turn a site off at
1946
+ any time, and it stops getting your work.
1947
+
1948
+ **Teams (optional)**
1949
+
1950
+ A team lets you share what runs on your devices with people you name \u2014 the free
1951
+ open-source models on your machine, or a metered service with a spending limit
1952
+ you set. Your subscription accounts (like Claude Pro/Max) are never shared with
1953
+ anyone. That is a rule, not a setting.
1954
+
1955
+ **byollm.cloud (or your own relay)**
1956
+
1957
+ Many sites, many devices, many people. byollm.cloud keeps track of who has
1958
+ allowed what and sends each job to the right device. It never sees your
1959
+ prompts. If you would rather run this part yourself, the relay is open source \u2014
1960
+ you can run your own instead of using byollm.cloud.`;
1961
+ var ABOUT_SHORT_LEDE = "BYOLLM \u2013 Bring Your Own LLM \u2013 lets you use your own AI on websites you authorize. A small program installed on your machine lets you use your own models and subscriptions on any BYOLLM-integrated site, including new models the moment you get access \u2013 no site updates required. BYOLLM Cloud connects sites to your devices with end-to-end encryption, so no one, including us, can see your data.";
1962
+ var ABOUT_SHORT_TAIL = "Sites can charge you less because you bring your own \u2013 see why that matters \u2192. Teams can optionally share the free or metered services on their devices with people they name. Personal subscriptions are never shared.";
1963
+ var ABOUT_SHORT = `${ABOUT_SHORT_LEDE}
1964
+
1965
+ ${ABOUT_SHORT_TAIL}`;
1966
+
1967
+ // src/signing.ts
1968
+ import { createHash as createHash2 } from "crypto";
1969
+ import { z as z10 } from "zod";
1970
+ var MAX_CLOCK_SKEW_MS = 12e4;
1971
+ var RequestSignature = z10.object({
1972
+ /** Which runner is calling. The server looks up its pinned identity. */
1973
+ runnerId: z10.string().min(1),
1974
+ /** Epoch ms, bounded by {@link MAX_CLOCK_SKEW_MS}. */
1975
+ issuedAt: z10.number().int().positive(),
1976
+ /** Base64url Ed25519 signature over {@link canonicalRequest}. */
1977
+ signature: z10.string().min(1)
2219
1978
  }).strict();
2220
- var WireErrorCode = z11.enum([
2221
- "bad-request",
2222
- "unsupported-protocol-version",
2223
- // "We do not know who you are." Exactly 401, and only that — cloud_008
2224
- // §1.4d.
2225
- "unauthorized",
1979
+ function canonicalRequest(input) {
1980
+ const digest = createHash2("sha256").update(input.body, "utf8").digest("hex");
1981
+ return Buffer.from(
1982
+ [
1983
+ "byollm/v1/request",
1984
+ input.endpoint,
1985
+ input.runnerId,
1986
+ String(input.issuedAt),
1987
+ digest
1988
+ ].join("\n"),
1989
+ "utf8"
1990
+ );
1991
+ }
1992
+ function signRequest(keys, input) {
1993
+ return {
1994
+ runnerId: input.runnerId,
1995
+ issuedAt: input.issuedAt,
1996
+ signature: signWith(keys, canonicalRequest(input))
1997
+ };
1998
+ }
1999
+ function signSiteRequest(keys, input) {
2000
+ return signRequest(keys, {
2001
+ endpoint: siteEndpoint(input.endpoint),
2002
+ runnerId: input.siteId,
2003
+ issuedAt: input.issuedAt,
2004
+ body: input.body
2005
+ });
2006
+ }
2007
+ function verifySiteRequest(input) {
2008
+ return verifyRequest({
2009
+ ...input,
2010
+ endpoint: siteEndpoint(input.endpoint)
2011
+ });
2012
+ }
2013
+ var siteEndpoint = (endpoint) => `site/${endpoint}`;
2014
+ function verifyRequest(input) {
2015
+ const skew = input.maxSkewMs ?? MAX_CLOCK_SKEW_MS;
2016
+ if (Math.abs(input.now - input.signature.issuedAt) > skew) return "stale";
2017
+ const ok = verifyWith(
2018
+ input.identityPublic,
2019
+ canonicalRequest({
2020
+ endpoint: input.endpoint,
2021
+ runnerId: input.signature.runnerId,
2022
+ issuedAt: input.signature.issuedAt,
2023
+ body: input.body
2024
+ }),
2025
+ input.signature.signature
2026
+ );
2027
+ return ok ? null : "bad-signature";
2028
+ }
2029
+
2030
+ // src/manifest.ts
2031
+ import { z as z11 } from "zod";
2032
+ var RESERVED_PURPOSE = "default";
2033
+ var RENDERABLE = /^[^\p{Cc}\p{Cf}\p{Cs}\p{Co}]+$/u;
2034
+ var renderable = (max, what) => z11.string().min(1).max(max).regex(
2035
+ RENDERABLE,
2036
+ `a ${what} is text a person reads \u2014 no control characters, direction overrides or zero-width padding`
2037
+ ).refine((value) => value.trim() !== "", {
2038
+ message: `a ${what} cannot be blank`
2039
+ });
2040
+ var PurposeKey = z11.string().regex(
2041
+ /^[a-z0-9][a-z0-9-]*$/,
2042
+ "a purpose key is a lowercase slug \u2014 letters, digits and hyphens"
2043
+ ).max(64);
2044
+ var Purpose = z11.object({
2226
2045
  /**
2227
- * "We know exactly who you are, and the answer is no." Exactly 403.
2228
- *
2229
- * Five refusals across both planes served 403 with `unauthorized`, whose
2230
- * table entry is 401: a revoked device, a site claiming another site's
2231
- * stub, a job you do not hold, a device belonging to another owner, a
2232
- * relay that does not route for you. Every one of them is an *identified*
2233
- * caller being refused.
2046
+ * What a person reads on the consent screen. The only rendered field.
2234
2047
  *
2235
- * Collapsing the two loses a distinction that matters everywhere it is
2236
- * read: a revoked daemon would look like an unsigned one in every log and
2237
- * every client branch, and "check your keys" is the wrong advice for both
2238
- * of them in opposite directions.
2048
+ * Declared rather than derived from the key, because a key is a
2049
+ * compromise between machines and this is not. "Writing Assistant" is
2050
+ * what somebody understands; `writing-assistant` is what travels.
2239
2051
  */
2240
- "forbidden",
2241
- "revoked",
2242
- "not-found",
2243
- // Claimed, but the site has not sealed the payload yet — cloud_008 §1.4.
2244
- //
2245
- // A daemon must retry rather than abandon: the job is legitimately still
2246
- // its own until the lease or the awaiting-payload clock says otherwise.
2247
- // That is why it cannot be `not-found` or `server-error`, and why it was
2248
- // the protocol gap that produced a bare 409 in the first place.
2249
- "not-ready",
2052
+ label: renderable(80, "label"),
2053
+ /** One line of context for the consent screen. Optional. */
2054
+ description: renderable(280, "description").optional(),
2250
2055
  /**
2251
- * The job is over, and this call is about a job — V1-6, and the code the
2252
- * site plane has been serving without one (V1-13).
2056
+ * The kinds this purpose uses.
2253
2057
  *
2254
- * Distinct from `not-found`, which says "no such job", and from
2255
- * `not-ready`, which says "not yet, keep asking". This one says "yes, and
2256
- * it finished" so a daemon must stop rather than retry, and a replayed
2257
- * request must not be able to reopen it.
2058
+ * A purpose may span kinds, and a mapping is per (purpose, kind) — so a
2059
+ * person can send this purpose's chat to one service and its generation
2060
+ * to another. Listing a kind here is what makes that slot appear.
2258
2061
  */
2259
- "too-late",
2260
- // The caller's clock is too far from ours to judge a signature's freshness.
2261
- //
2262
- // Split out from `unauthorized` because the remedy is completely different
2263
- // and only the server can tell them apart: a bad signature means the key is
2264
- // wrong, this means the machine's time is wrong. A daemon reporting it as a
2265
- // generic rejection sends its owner looking at their network.
2266
- "clock-skew",
2267
- "rate-limited",
2268
- "server-error"
2269
- ]);
2270
- var WireError = z11.object({
2271
- error: WireErrorCode,
2272
- message: z11.string().min(1),
2062
+ kinds: z11.array(JobKind).min(1).max(JOB_KINDS.length).refine((kinds) => new Set(kinds).size === kinds.length, {
2063
+ message: "a purpose lists each kind once"
2064
+ })
2065
+ }).strict();
2066
+ var MAX_PURPOSES = 32;
2067
+ var Manifest = z11.record(PurposeKey, Purpose).refine((manifest) => Object.keys(manifest).length > 0, {
2068
+ message: "a manifest declares at least one purpose"
2069
+ }).refine((manifest) => Object.keys(manifest).length <= MAX_PURPOSES, {
2070
+ message: `a manifest declares at most ${String(MAX_PURPOSES)} purposes \u2014 a consent screen is a set of questions somebody answers one at a time`
2071
+ }).refine((manifest) => !(RESERVED_PURPOSE in manifest), {
2072
+ message: `"${RESERVED_PURPOSE}" is reserved for a site that declares no purposes of its own \u2014 give this one a name from your own vocabulary`
2073
+ });
2074
+ function singlePurposeManifest(input) {
2075
+ return {
2076
+ [RESERVED_PURPOSE]: { label: input.label, kinds: [...input.kinds] }
2077
+ };
2078
+ }
2079
+
2080
+ // src/musts.ts
2081
+ function kindsOf(must2) {
2082
+ return typeof must2.verifiedBy === "string" ? [must2.verifiedBy] : must2.verifiedBy;
2083
+ }
2084
+ var must = (m) => Object.freeze(m);
2085
+ var MUSTS = Object.freeze({
2086
+ // ---- Pairing and identity -------------------------------------------
2087
+ PAIR_ONE_USER: must({
2088
+ id: "PAIR_ONE_USER",
2089
+ statement: "A runner token MUST be bound to exactly one user; a daemon MUST refuse work not attributable to its paired user.",
2090
+ enforcedBy: "both",
2091
+ verifiedBy: "conformance",
2092
+ source: "byollm_001 \xA7MUSTs"
2093
+ }),
2094
+ PAIR_INTERACTIVE: must({
2095
+ id: "PAIR_INTERACTIVE",
2096
+ 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.",
2097
+ enforcedBy: "server",
2098
+ verifiedBy: "conformance",
2099
+ source: "byollm_001 \xA7Endpoints.1"
2100
+ }),
2101
+ PAIR_CODE_EXPIRES: must({
2102
+ id: "PAIR_CODE_EXPIRES",
2103
+ statement: "An unapproved device code MUST expire and MUST NOT be redeemable after expiry.",
2104
+ enforcedBy: "server",
2105
+ verifiedBy: "conformance",
2106
+ source: "byollm_001 \xA7Endpoints.1"
2107
+ }),
2108
+ // ---- Typed job kinds --------------------------------------------------
2109
+ VERSION_HANDSHAKE_REQUIRED: must({
2110
+ id: "VERSION_HANDSHAKE_REQUIRED",
2111
+ 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.",
2112
+ enforcedBy: "both",
2113
+ verifiedBy: "conformance",
2114
+ source: "byollm_009 \xA74"
2115
+ }),
2116
+ SITE_KEY_BY_STUB: must({
2117
+ id: "SITE_KEY_BY_STUB",
2118
+ 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.",
2119
+ enforcedBy: "daemon",
2120
+ // Adversarial, and the reason is the finding that produced it: the
2121
+ // honest paths pass with every site check deleted, because `open`
2122
+ // refuses a signature from the wrong key anyway. What distinguishes an
2123
+ // enforced rule from a coincidence here is a hostile pairing of stub and
2124
+ // envelope, which no conformance client would ever send.
2125
+ verifiedBy: "adversarial",
2126
+ source: "byollm_009 \xA7A.3"
2127
+ }),
2128
+ SITES_LOCALLY_APPROVED: must({
2129
+ id: "SITES_LOCALLY_APPROVED",
2130
+ statement: "A daemon MUST NOT run work for a site on an upstream's word alone. An upstream may propose a site set; work for any site in it MUST additionally carry a grant signed by the control-plane key this daemon pinned at pairing. A key that has changed for an id this daemon already pinned MUST be refused for the life of the pairing, including after that id has left the set and returned. A **verified succession** is not a changed key: a new key id carrying a signature, by a key this daemon has already pinned, over a statement naming both key ids MUST be accepted \u2014 provided the control plane projects the same successor \u2014 and MUST be announced rather than applied silently. The first job from a site this daemon has never served MUST be announced at the machine.",
2131
+ enforcedBy: "daemon",
2132
+ // Two kinds, and the second is the one that matters — V1-1.
2133
+ //
2134
+ // `construction`: the daemon cannot serve a site that is not in its
2135
+ // pinned map, and admission refuses before a payload is fetched — and
2136
+ // since byollm_016 Amendment K, being in the map is no longer sufficient
2137
+ // either: a signed grant is, and the relay proposing the set cannot
2138
+ // produce one.
2139
+ //
2140
+ // `adversarial`: the property that survives is about a *sequence* —
2141
+ // remove the id, re-offer it under a different key — which no honest
2142
+ // upstream sends and which the fence above does not see. That was the
2143
+ // bypass: the pin was deleted with the id, so the comparison had nothing
2144
+ // to compare against and the substitution arrived as a stranger.
2145
+ // **Not `conformance`, and that is a live gap rather than a judgement.**
2146
+ // Amendment C's succession clause is a rule about two implementations
2147
+ // agreeing, which is what a conformance check is for — but rotating a
2148
+ // site's key is not something `ConformanceTarget` can express, and adding
2149
+ // an optional hook that most targets omit would produce a check reporting
2150
+ // success for a reason unrelated to the property it claims. That is this
2151
+ // project's most-repeated bug, and it is not worth reintroducing for a
2152
+ // stronger-sounding word in a table. The rotation path is verified by
2153
+ // `site-rotation.test.ts` (both directions, against the shipped runner)
2154
+ // and `relay/test/rotation.test.ts` (both planes, against the reference
2155
+ // relay); the missing piece is a second *independent* implementation to
2156
+ // check them against, and there is not one yet.
2157
+ verifiedBy: ["construction", "adversarial"],
2158
+ source: "byollm_009 \xA7B.2, Amendment C"
2159
+ }),
2160
+ KEYS_EXCHANGED_AT_CONSENT: must({
2161
+ id: "KEYS_EXCHANGED_AT_CONSENT",
2162
+ 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.",
2163
+ enforcedBy: "both",
2164
+ verifiedBy: "conformance",
2165
+ source: "byollm_009 \xA75"
2166
+ }),
2167
+ REQUESTS_SIGNED_NOT_BEARER: must({
2168
+ id: "REQUESTS_SIGNED_NOT_BEARER",
2169
+ 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.",
2170
+ enforcedBy: "both",
2171
+ verifiedBy: "conformance",
2172
+ source: "byollm_009 \xA74.2"
2173
+ }),
2174
+ LEASE_SCOPED_BY_GRANT: must({
2175
+ id: "LEASE_SCOPED_BY_GRANT",
2176
+ 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.",
2177
+ enforcedBy: "both",
2178
+ verifiedBy: "conformance",
2179
+ source: "byollm_009 \xA74.2"
2180
+ }),
2181
+ STUB_METADATA_EXHAUSTIVE: must({
2182
+ id: "STUB_METADATA_EXHAUSTIVE",
2183
+ 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.",
2184
+ enforcedBy: "both",
2185
+ verifiedBy: "conformance",
2186
+ source: "byollm_009 \xA76"
2187
+ }),
2188
+ ENVELOPE_SEALED_AND_SIGNED: must({
2189
+ id: "ENVELOPE_SEALED_AND_SIGNED",
2190
+ 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.",
2191
+ enforcedBy: "server",
2192
+ verifiedBy: "conformance",
2193
+ source: "byollm_009 \xA76"
2194
+ }),
2195
+ KIND_TYPED_ONLY: must({
2196
+ id: "KIND_TYPED_ONLY",
2197
+ statement: "Job kinds MUST resolve against handlers baked into the daemon. A daemon MUST refuse an unknown kind rather than guess.",
2198
+ enforcedBy: "daemon",
2199
+ verifiedBy: "conformance",
2200
+ source: "byollm_001 \xA7Jobs are typed data"
2201
+ }),
2202
+ KIND_NO_CODE: must({
2203
+ id: "KIND_NO_CODE",
2204
+ 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.",
2205
+ enforcedBy: "daemon",
2206
+ verifiedBy: "conformance",
2207
+ source: "byollm_001 \xA7Jobs are typed data; byollm_004 \xA71"
2208
+ }),
2209
+ // ---- Capability and claiming -----------------------------------------
2210
+ CLAIM_REQUIRES_CAPABILITY: must({
2211
+ id: "CLAIM_REQUIRES_CAPABILITY",
2212
+ statement: "A daemon MUST NOT be given a job whose kind is absent from its advertised capability matrix.",
2213
+ enforcedBy: "both",
2214
+ verifiedBy: "conformance",
2215
+ source: "byollm_001 \xA7MUSTs"
2216
+ }),
2217
+ CAPABILITY_IS_DETECTED: must({
2218
+ id: "CAPABILITY_IS_DETECTED",
2219
+ statement: "An advertised capability matrix MUST be the intersection of owner config and detected, healthy reality \u2014 never config alone.",
2220
+ enforcedBy: "daemon",
2221
+ verifiedBy: "conformance",
2222
+ source: "byollm_002 \xA7Routing"
2223
+ }),
2224
+ CLAIM_ATOMIC: must({
2225
+ id: "CLAIM_ATOMIC",
2226
+ statement: "Claiming MUST be atomic: a job MUST NOT be handed to two runners concurrently.",
2227
+ enforcedBy: "server",
2228
+ verifiedBy: "conformance",
2229
+ source: "byollm_001 \xA7Endpoints.2"
2230
+ }),
2231
+ // ---- Leases -----------------------------------------------------------
2232
+ LEASE_HONORED: must({
2233
+ id: "LEASE_HONORED",
2234
+ 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.",
2235
+ enforcedBy: "daemon",
2236
+ verifiedBy: "conformance",
2237
+ source: "byollm_001 \xA7MUSTs"
2238
+ }),
2239
+ LEASE_RECLAIMABLE: must({
2240
+ id: "LEASE_RECLAIMABLE",
2241
+ statement: "A lease that expires un-renewed MUST make its job claimable again with no loss of the job.",
2242
+ enforcedBy: "server",
2243
+ verifiedBy: "conformance",
2244
+ source: "byollm_001 \xA7Endpoints.2"
2245
+ }),
2246
+ // ---- Audience and offer scope ----------------------------------------
2247
+ AUDIENCE_BOTH_SIDES: must({
2248
+ id: "AUDIENCE_BOTH_SIDES",
2249
+ 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.",
2250
+ enforcedBy: "both",
2251
+ verifiedBy: "conformance",
2252
+ source: "byollm_001 \xA7The audience model"
2253
+ }),
2254
+ SUBSCRIPTION_SELF_LOCK: must({
2255
+ id: "SUBSCRIPTION_SELF_LOCK",
2256
+ statement: "A subscription-class backend's offer scope MUST be 'private' and MUST NOT be widened by configuration.",
2257
+ enforcedBy: "daemon",
2258
+ verifiedBy: "conformance",
2259
+ source: "byollm_001 \xA7The audience model"
2260
+ }),
2261
+ METERED_DEFAULTS_SELF: must({
2262
+ id: "METERED_DEFAULTS_SELF",
2263
+ statement: "A metered backend's effective offer scope MUST be 'private' unless the owner has explicitly acknowledged spending money on others' work.",
2264
+ enforcedBy: "daemon",
2265
+ verifiedBy: "conformance",
2266
+ source: "byollm_007 \xA74"
2267
+ }),
2268
+ METERED_REQUIRES_CEILING: must({
2269
+ id: "METERED_REQUIRES_CEILING",
2270
+ statement: "A widened metered backend MUST carry a spend ceiling, and the daemon MUST refuse community work once it is reached.",
2271
+ enforcedBy: "daemon",
2272
+ verifiedBy: "conformance",
2273
+ source: "byollm_007 \xA74"
2274
+ }),
2275
+ COST_NOT_CONFIGURABLE: must({
2276
+ id: "COST_NOT_CONFIGURABLE",
2277
+ statement: "A built-in provider's cost class MUST NOT be overridable by configuration.",
2278
+ enforcedBy: "daemon",
2279
+ verifiedBy: "conformance",
2280
+ source: "byollm_007 \xA72"
2281
+ }),
2282
+ REMOTE_IS_NEVER_FREE: must({
2283
+ id: "REMOTE_IS_NEVER_FREE",
2284
+ statement: "A generic HTTP backend whose base URL is not loopback or private MUST be treated as metered.",
2285
+ enforcedBy: "daemon",
2286
+ verifiedBy: "conformance",
2287
+ source: "byollm_007 \xA72"
2288
+ }),
2289
+ NAMED_LOCAL_ALLOWLIST: must({
2290
+ id: "NAMED_LOCAL_ALLOWLIST",
2291
+ statement: "A 'team' job MUST be admitted only by something the device itself verified, keyed by (server origin, user id) \u2014 never on the routing party's assertion alone.",
2292
+ enforcedBy: "daemon",
2293
+ verifiedBy: "conformance",
2294
+ source: "byollm_001 Rev 1 \xA7B"
2295
+ }),
2296
+ REFUSAL_NOT_REOFFERED: must({
2297
+ id: "REFUSAL_NOT_REOFFERED",
2298
+ statement: "A server MUST NOT re-offer a job to a runner that released it with reason 'refused'.",
2299
+ enforcedBy: "server",
2300
+ verifiedBy: "conformance",
2301
+ source: "byollm_001 Rev 1 \xA7B (loop resolved in build review)"
2302
+ }),
2303
+ // ---- Revocation and cancel -------------------------------------------
2304
+ REVOCATION_HONORED: must({
2305
+ id: "REVOCATION_HONORED",
2306
+ statement: "A revoked daemon MUST stop claiming and MUST abandon in-flight work by the next heartbeat at the latest.",
2307
+ enforcedBy: "daemon",
2308
+ verifiedBy: "conformance",
2309
+ source: "byollm_001 \xA7MUSTs"
2310
+ }),
2311
+ CANCEL_HONORED: must({
2312
+ id: "CANCEL_HONORED",
2313
+ 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'.",
2314
+ enforcedBy: "daemon",
2315
+ verifiedBy: "conformance",
2316
+ source: "byollm_001 Rev 1 \xA7C"
2317
+ }),
2318
+ // ---- Lifecycle, dependencies, delivery -------------------------------
2319
+ DEPENDS_ON_GATING: must({
2320
+ id: "DEPENDS_ON_GATING",
2321
+ statement: "A job MUST NOT be claimable until every job in its dependsOn set has reached the 'ok' state.",
2322
+ enforcedBy: "server",
2323
+ verifiedBy: "conformance",
2324
+ source: "byollm_001 Rev 1 \xA7E"
2325
+ }),
2326
+ TTL_EXPIRY: must({
2327
+ id: "TTL_EXPIRY",
2328
+ 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.",
2329
+ enforcedBy: "server",
2330
+ verifiedBy: "conformance",
2331
+ source: "byollm_001 Rev 1 \xA7D (TTL clock resolved in build review)"
2332
+ }),
2333
+ NO_RUNNER_SIGNAL: must({
2334
+ id: "NO_RUNNER_SIGNAL",
2335
+ 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.",
2336
+ enforcedBy: "server",
2337
+ verifiedBy: "conformance",
2338
+ source: "byollm_001 Rev 1 \xA7D"
2339
+ }),
2340
+ RESULT_IDEMPOTENT: must({
2341
+ id: "RESULT_IDEMPOTENT",
2342
+ statement: "Result submission MUST be idempotent by job id; the first terminal outcome wins and later submissions MUST NOT change it.",
2343
+ enforcedBy: "server",
2344
+ verifiedBy: "conformance",
2345
+ source: "byollm_001 \xA7Endpoints.4"
2346
+ }),
2347
+ PROVENANCE_NAMES_DEVICE: must({
2348
+ id: "PROVENANCE_NAMES_DEVICE",
2349
+ 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.",
2350
+ enforcedBy: "server",
2351
+ verifiedBy: "conformance",
2352
+ source: "byollm_009 \xA711"
2353
+ }),
2354
+ // ---- The trust surface -------------------------------------------------
2355
+ INGRESS_LOGGED_BEFORE_EXECUTION: must({
2356
+ id: "INGRESS_LOGGED_BEFORE_EXECUTION",
2357
+ statement: "Every executed prompt MUST be appended to the local ingress log before execution begins.",
2358
+ enforcedBy: "daemon",
2359
+ verifiedBy: "conformance",
2360
+ source: "byollm_001 \xA7MUSTs"
2361
+ }),
2362
+ // ---- Execution isolation (byollm_004) ---------------------------------
2363
+ NO_SHELL_INTERPOLATION: must({
2364
+ id: "NO_SHELL_INTERPOLATION",
2365
+ 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.",
2366
+ enforcedBy: "daemon",
2367
+ verifiedBy: "adversarial",
2368
+ source: "byollm_004 \xA72"
2369
+ }),
2273
2370
  /**
2274
- * What this server speaks, on `unsupported-protocol-version` §B.4.
2371
+ * Amended for byollm_016 Phase B, and the amendment is deliberately narrow.
2275
2372
  *
2276
- * The refusal has carried these since the version handshake existed and
2277
- * the enumeration did not model them, so the one error that exists to be
2278
- * *acted on* was the one that failed to parse as a wire error. Found by
2279
- * the relay's own suite the day the relay started sending it: a refusal
2280
- * outside the enumerated shape is a refusal a client cannot branch on,
2281
- * which is the whole reason §1.4 enumerates them.
2373
+ * A site may now name a **service** on the stub. The temptation is to read
2374
+ * that as a crack in this law, so the statement below says exactly where the
2375
+ * line is: a name selects from a menu the owner published, and resolves to a
2376
+ * model, backend, base URL and flags **only** through that owner's own
2377
+ * config. The site supplies a key; the owner supplies every value it maps
2378
+ * to. A name the owner does not advertise is refused rather than
2379
+ * substituted, because substitution is how "you may pick from my list" turns
2380
+ * into "you may ask for anything and get something".
2282
2381
  *
2283
- * Modelled the way `clock-skew`'s two fields already are code-specific
2284
- * extras, refused on any other code by the refinement below.
2285
- */
2286
- supported: z11.array(z11.string().min(1)).optional(),
2287
- minimum: z11.string().min(1).optional(),
2288
- /** Seconds; mirrors Retry-After for `rate-limited` and `server-error`. */
2289
- retryAfter: z11.number().int().nonnegative().optional(),
2290
- /**
2291
- * The server's clock, and the window it allows. `clock-skew` only.
2382
+ * Two properties keep it from drifting into "sites demand models":
2292
2383
  *
2293
- * So the far side can say *how far off* rather than *that something is
2294
- * wrong* the difference between "adjust your clock by four minutes" and
2295
- * "something is wrong with your connection". Not a disclosure: the
2296
- * heartbeat response returns the same value, and so does every `Date`
2297
- * header.
2384
+ * 1. **Nothing the site sends is ever a value.** No model string, no URL,
2385
+ * no flag crosses the wire only a key that means nothing off this
2386
+ * owner's machine.
2387
+ * 2. **It is a stub field, never a payload field.** The prompt cannot
2388
+ * reach it. That is unchanged and is the sentence the second clause
2389
+ * below still enforces verbatim.
2298
2390
  */
2299
- serverTime: z11.number().int().positive().optional(),
2300
- maxSkewMs: z11.number().int().positive().optional()
2301
- }).strict().superRefine((error, ctx) => {
2302
- const skew = error.error === "clock-skew";
2303
- const carried = error.serverTime !== void 0 || error.maxSkewMs !== void 0;
2304
- if (skew && !carried) {
2305
- ctx.addIssue({
2306
- code: "custom",
2307
- message: "clock-skew must carry serverTime and maxSkewMs"
2308
- });
2309
- }
2310
- if (!skew && carried) {
2311
- ctx.addIssue({
2312
- code: "custom",
2313
- message: `${error.error} must not carry serverTime or maxSkewMs`
2314
- });
2315
- }
2316
- const version = error.error === "unsupported-protocol-version";
2317
- const versionFields = error.supported !== void 0 || error.minimum !== void 0;
2318
- if (version && !versionFields) {
2319
- ctx.addIssue({
2320
- code: "custom",
2321
- message: "unsupported-protocol-version must carry supported and minimum"
2322
- });
2323
- }
2324
- if (!version && versionFields) {
2325
- ctx.addIssue({
2326
- code: "custom",
2327
- message: `${error.error} must not carry supported or minimum`
2328
- });
2329
- }
2391
+ NO_PAYLOAD_ROUTING: must({
2392
+ id: "NO_PAYLOAD_ROUTING",
2393
+ statement: "Model, backend, base URL, and flags MUST come from owner config only; a payload MUST NOT influence any of them. A stub MAY name a service the owner advertises, which selects among that owner's own config entries and MUST NOT introduce any value the owner did not write; an unadvertised name MUST be refused, never substituted.",
2394
+ enforcedBy: "daemon",
2395
+ verifiedBy: "adversarial",
2396
+ source: "byollm_004 \xA72, amended byollm_016 \xA7Phase B"
2397
+ }),
2398
+ STRIPPED_CHILD_ENV: must({
2399
+ id: "STRIPPED_CHILD_ENV",
2400
+ 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.",
2401
+ enforcedBy: "daemon",
2402
+ verifiedBy: "adversarial",
2403
+ source: "byollm_004 \xA72"
2404
+ }),
2405
+ HTTP_BASE_URL_SAFE: must({
2406
+ id: "HTTP_BASE_URL_SAFE",
2407
+ 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.",
2408
+ enforcedBy: "daemon",
2409
+ verifiedBy: "adversarial",
2410
+ source: "byollm_004 Rev 1 \xA7Backend taxonomy"
2411
+ }),
2412
+ OUTPUT_INERT: must({
2413
+ id: "OUTPUT_INERT",
2414
+ 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.",
2415
+ enforcedBy: "daemon",
2416
+ verifiedBy: "adversarial",
2417
+ source: "byollm_004 \xA72"
2418
+ }),
2419
+ COMMUNITY_BUDGETS: must({
2420
+ id: "COMMUNITY_BUDGETS",
2421
+ statement: "Jobs whose owner is not the daemon's owner MUST be subject to the owner's rate limits, daily cap, and resource budget.",
2422
+ enforcedBy: "daemon",
2423
+ verifiedBy: "adversarial",
2424
+ source: "byollm_004 \xA74"
2425
+ }),
2426
+ REVOCATION_IMMEDIATE: must({
2427
+ id: "REVOCATION_IMMEDIATE",
2428
+ 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.",
2429
+ // Both, and stated as one sentence with two obligations rather than
2430
+ // folded into REVOCATION_HONORED. That one binds the *daemon*: a revoked
2431
+ // daemon stops claiming and abandons in-flight work. This binds the
2432
+ // *upstream*. byollm_009 §5 is explicit that the pair is the point — "a
2433
+ // revocation enforced at one end survives a compromise of that end" — and
2434
+ // one entry covering both would make a compromised daemon look compliant.
2435
+ enforcedBy: "both",
2436
+ verifiedBy: "conformance",
2437
+ source: "byollm_009 \xA711"
2438
+ }),
2439
+ CONSENT_BEFORE_ROUTE: must({
2440
+ id: "CONSENT_BEFORE_ROUTE",
2441
+ 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.",
2442
+ enforcedBy: "server",
2443
+ verifiedBy: "conformance",
2444
+ source: "byollm_009 \xA711"
2445
+ }),
2446
+ ROSTER_NOT_DISCLOSED: must({
2447
+ id: "ROSTER_NOT_DISCLOSED",
2448
+ 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.",
2449
+ // Checkable since cloud_008 §0.2 took `audienceAllow` off the stub: the
2450
+ // property now holds by *absence*, and absence is exactly what a strict
2451
+ // schema and a serialised stub can be asked about. Before that it was a
2452
+ // sentence — and one this project cited in code comments, tests and two
2453
+ // specs as though it were enforced data, which is why it is worth
2454
+ // stating precisely rather than generously.
2455
+ enforcedBy: "both",
2456
+ verifiedBy: "conformance",
2457
+ source: "byollm_009 \xA711"
2458
+ }),
2459
+ EFFECTIVE_OFFER_ONLY: must({
2460
+ id: "EFFECTIVE_OFFER_ONLY",
2461
+ 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.",
2462
+ enforcedBy: "both",
2463
+ verifiedBy: "conformance",
2464
+ source: "byollm_009 \xA711"
2465
+ }),
2466
+ FALLBACK_LABELED: must({
2467
+ id: "FALLBACK_LABELED",
2468
+ 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.",
2469
+ // `construction` today, and deliberately not `conformance`. Nothing on
2470
+ // the wire yet distinguishes a fallback from any other community job —
2471
+ // the ledger that would give it a surface is unbuilt — so a check would
2472
+ // have to assert something it cannot observe. Promoted the day that
2473
+ // surface exists. Marking it `conformance` now would put "verified"
2474
+ // beside a property no third party can see, which is the one thing the
2475
+ // kinds exist to prevent.
2476
+ enforcedBy: "both",
2477
+ verifiedBy: "construction",
2478
+ source: "byollm_009 \xA711"
2479
+ }),
2480
+ RELAY_BLIND: must({
2481
+ id: "RELAY_BLIND",
2482
+ statement: "A relay MUST NOT hold any key capable of decrypting a payload, a result, or a delta frame.",
2483
+ // Operator: a third party can read the relay's types and see there is
2484
+ // nowhere to put such a key, but the kit certifies a *server* and cannot
2485
+ // reach inside somebody's deployment to prove what it holds.
2486
+ enforcedBy: "server",
2487
+ verifiedBy: "operator",
2488
+ source: "byollm_009 \xA711"
2489
+ }),
2490
+ SHARED_COMPUTE_DISCLOSED: must({
2491
+ id: "SHARED_COMPUTE_DISCLOSED",
2492
+ 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.",
2493
+ // Operator, and cloud_008 §0.3 is why the classification now comes with a
2494
+ // standing answer rather than a standing question. The screen is not
2495
+ // wire-observable, but the *string the server composes* is, and it is
2496
+ // now unit-tested with the two false sentences forbidden by name. The
2497
+ // kind stays `operator` because a third-party site can still render
2498
+ // whatever it likes; what changed is that the part inside our own
2499
+ // boundary stopped depending on somebody remembering to audit it.
2500
+ enforcedBy: "server",
2501
+ verifiedBy: "operator",
2502
+ source: "byollm_009 \xA711"
2503
+ })
2330
2504
  });
2331
- var ERROR_STATUS = Object.freeze({
2332
- "bad-request": 400,
2333
- "unsupported-protocol-version": 400,
2334
- unauthorized: 401,
2335
- forbidden: 403,
2336
- revoked: 403,
2337
- "not-found": 404,
2338
- // 409, not 404: the job exists and is yours, it is simply not ready.
2339
- "not-ready": 409,
2340
- // The same 409 as `not-ready` and the opposite instruction: that one says
2341
- // keep asking, this one says stop. The status is the class of the
2342
- // problem — a request that does not fit the resource's state — and the
2343
- // code is what a caller acts on.
2344
- "too-late": 409,
2345
- // 401 alongside `unauthorized`, because that is what it is — the
2346
- // signature could not be judged. The code is what carries the remedy.
2347
- "clock-skew": 401,
2348
- "rate-limited": 429,
2349
- "server-error": 500
2505
+ var RETIRED_MUSTS = Object.freeze({
2506
+ RESULT_PROVENANCE: {
2507
+ supersededBy: "PROVENANCE_NAMES_DEVICE",
2508
+ 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."
2509
+ }
2350
2510
  });
2351
- var FetchRequest = z11.object({
2352
- // `literal`, like every other request — V1-17. This one said
2353
- // `string().min(1)`, so a daemon speaking a version this server does not
2354
- // know got past the handshake on the one endpoint that hands over a
2355
- // sealed payload. The version check exists so that a mismatch is a named
2356
- // refusal rather than a schema failure three fields later; here it was
2357
- // neither.
2358
- protocolVersion: z11.literal(PROTOCOL_VERSION),
2359
- runnerId: z11.string().min(1),
2360
- jobId: z11.string().min(1),
2361
- /**
2362
- * The grant this daemon holds.
2363
- *
2364
- * Named, not inferred: a fetch is lease-scoped, and a request that names
2365
- * only the job would be answerable for whatever lease exists when it
2366
- * arrives ({@link Lease.id}).
2367
- */
2368
- leaseId: z11.string().min(1)
2369
- }).strict();
2370
- var FetchResponse = z11.object({
2371
- /**
2372
- * The work, sealed to the device that claimed it — byollm_009 §6.
2373
- *
2374
- * Not plaintext. The site opens its own at-rest envelope and re-seals to
2375
- * the claiming device's key, signed by the site's identity, so the work
2376
- * is readable only by the machine that took it and only if it came from
2377
- * the site that machine pinned.
2378
- */
2379
- envelope: SealedEnvelope
2380
- }).strict();
2511
+ var MUST_IDS = Object.freeze(Object.keys(MUSTS));
2512
+ function mustsVerifiedBy(kind) {
2513
+ return MUST_IDS.filter((id) => kindsOf(MUSTS[id]).includes(kind));
2514
+ }
2381
2515
  export {
2382
2516
  ABOUT,
2383
2517
  ABOUT_SHORT,
@@ -2472,6 +2606,8 @@ export {
2472
2606
  StoredKeys,
2473
2607
  Succession,
2474
2608
  TERMINAL_STATES,
2609
+ UPDATE_OFFER_SINCE,
2610
+ UPGRADE_COMMAND,
2475
2611
  WireError,
2476
2612
  WireErrorCode,
2477
2613
  WithheldKind,
@@ -2479,8 +2615,10 @@ export {
2479
2615
  backendName,
2480
2616
  canTransition,
2481
2617
  canonicalRequest,
2618
+ checkDaemonFloor,
2482
2619
  checkProtocolVersion,
2483
2620
  classifyCost,
2621
+ compareVersions,
2484
2622
  cryptoReady,
2485
2623
  declaredVersion,
2486
2624
  effectiveOfferScope,
@@ -2496,6 +2634,7 @@ export {
2496
2634
  keyId,
2497
2635
  kindsOf,
2498
2636
  matchAudience,
2637
+ mayOfferUpdate,
2499
2638
  mustsVerifiedBy,
2500
2639
  open,
2501
2640
  payloadTextLength,
@@ -2512,6 +2651,7 @@ export {
2512
2651
  sizeClassCeiling,
2513
2652
  sizeClassOf,
2514
2653
  successionStatement,
2654
+ updateOfferFor,
2515
2655
  verifyGrant,
2516
2656
  verifyLink,
2517
2657
  verifyPublicIdentity,