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