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

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
@@ -4,21 +4,163 @@ import { z as z2 } from "zod";
4
4
  // src/backends.ts
5
5
  import { z } from "zod";
6
6
  var BackendClass = z.enum(["http", "process"]);
7
- var BackendAccount = z.enum(["open", "subscription"]);
7
+ var BackendCost = z.enum(["free", "metered", "subscription"]);
8
8
  var backend = (b) => Object.freeze(b);
9
9
  var BACKENDS = Object.freeze({
10
+ // -- free: local compute, costs electricity ------------------------------
11
+ ollama: backend({
12
+ id: "ollama",
13
+ label: "Ollama (local)",
14
+ class: "http",
15
+ cost: "free",
16
+ adversarialCorpus: "http",
17
+ defaultBaseUrl: "http://127.0.0.1:11434/v1"
18
+ }),
19
+ mlx: backend({
20
+ id: "mlx",
21
+ label: "MLX (mlx_lm.server, local)",
22
+ class: "http",
23
+ cost: "free",
24
+ adversarialCorpus: "http",
25
+ defaultBaseUrl: "http://127.0.0.1:8080/v1"
26
+ }),
27
+ llamacpp: backend({
28
+ id: "llamacpp",
29
+ label: "llama.cpp server (local)",
30
+ class: "http",
31
+ cost: "free",
32
+ adversarialCorpus: "http",
33
+ defaultBaseUrl: "http://127.0.0.1:8080/v1"
34
+ }),
35
+ vllm: backend({
36
+ id: "vllm",
37
+ label: "vLLM (local)",
38
+ class: "http",
39
+ cost: "free",
40
+ adversarialCorpus: "http",
41
+ defaultBaseUrl: "http://127.0.0.1:8000/v1"
42
+ }),
43
+ lmstudio: backend({
44
+ id: "lmstudio",
45
+ label: "LM Studio (local)",
46
+ class: "http",
47
+ cost: "free",
48
+ adversarialCorpus: "http",
49
+ defaultBaseUrl: "http://127.0.0.1:1234/v1"
50
+ }),
51
+ jan: backend({
52
+ id: "jan",
53
+ label: "Jan (local)",
54
+ class: "http",
55
+ cost: "free",
56
+ adversarialCorpus: "http",
57
+ defaultBaseUrl: "http://127.0.0.1:1337/v1"
58
+ }),
59
+ localai: backend({
60
+ id: "localai",
61
+ label: "LocalAI (local)",
62
+ class: "http",
63
+ cost: "free",
64
+ adversarialCorpus: "http",
65
+ defaultBaseUrl: "http://127.0.0.1:8080/v1"
66
+ }),
67
+ // -- metered: the owner's money, per token -------------------------------
68
+ /**
69
+ * Note the pair: `anthropic` and {@link BACKENDS."claude-cli"} reach the
70
+ * same vendor and land in different cost classes. That is not an
71
+ * inconsistency — it is the axis working. One bills a key per token, the
72
+ * other runs under a personal plan whose terms cover one person's work. Who
73
+ * pays and under what terms is the question; which company is not.
74
+ */
75
+ anthropic: backend({
76
+ id: "anthropic",
77
+ label: "Anthropic (your API key)",
78
+ class: "http",
79
+ cost: "metered",
80
+ adversarialCorpus: "http",
81
+ defaultBaseUrl: "https://api.anthropic.com/v1"
82
+ }),
83
+ openai: backend({
84
+ id: "openai",
85
+ label: "OpenAI (your API key)",
86
+ class: "http",
87
+ cost: "metered",
88
+ adversarialCorpus: "http",
89
+ defaultBaseUrl: "https://api.openai.com/v1"
90
+ }),
91
+ gemini: backend({
92
+ id: "gemini",
93
+ label: "Google Gemini (your API key)",
94
+ class: "http",
95
+ cost: "metered",
96
+ adversarialCorpus: "http",
97
+ defaultBaseUrl: "https://generativelanguage.googleapis.com/v1beta/openai"
98
+ }),
99
+ grok: backend({
100
+ id: "grok",
101
+ label: "xAI Grok (your API key)",
102
+ class: "http",
103
+ cost: "metered",
104
+ adversarialCorpus: "http",
105
+ defaultBaseUrl: "https://api.x.ai/v1"
106
+ }),
107
+ groq: backend({
108
+ id: "groq",
109
+ label: "Groq (your API key)",
110
+ class: "http",
111
+ cost: "metered",
112
+ adversarialCorpus: "http",
113
+ defaultBaseUrl: "https://api.groq.com/openai/v1"
114
+ }),
115
+ openrouter: backend({
116
+ id: "openrouter",
117
+ label: "OpenRouter (your API key)",
118
+ class: "http",
119
+ cost: "metered",
120
+ adversarialCorpus: "http",
121
+ defaultBaseUrl: "https://openrouter.ai/api/v1"
122
+ }),
123
+ together: backend({
124
+ id: "together",
125
+ label: "Together AI (your API key)",
126
+ class: "http",
127
+ cost: "metered",
128
+ adversarialCorpus: "http",
129
+ defaultBaseUrl: "https://api.together.xyz/v1"
130
+ }),
131
+ deepseek: backend({
132
+ id: "deepseek",
133
+ label: "DeepSeek (your API key)",
134
+ class: "http",
135
+ cost: "metered",
136
+ adversarialCorpus: "http",
137
+ defaultBaseUrl: "https://api.deepseek.com/v1"
138
+ }),
139
+ mistral: backend({
140
+ id: "mistral",
141
+ label: "Mistral (your API key)",
142
+ class: "http",
143
+ cost: "metered",
144
+ adversarialCorpus: "http",
145
+ defaultBaseUrl: "https://api.mistral.ai/v1"
146
+ }),
147
+ // -- the escape hatch ----------------------------------------------------
10
148
  "openai-http": backend({
11
149
  id: "openai-http",
12
- label: "OpenAI-compatible HTTP server (Ollama, MLX, llama.cpp, vLLM)",
150
+ label: "Any OpenAI-compatible server",
13
151
  class: "http",
14
- account: "open",
152
+ // Unknown until the base URL is known: local means free, remote means
153
+ // metered, and the owner does not get to say otherwise
154
+ // ({@link MUSTS.REMOTE_IS_NEVER_FREE}).
155
+ cost: null,
15
156
  adversarialCorpus: "http"
16
157
  }),
158
+ // -- subscription: someone else's terms ----------------------------------
17
159
  "claude-cli": backend({
18
160
  id: "claude-cli",
19
161
  label: "Claude CLI (your subscription)",
20
162
  class: "process",
21
- account: "subscription",
163
+ cost: "subscription",
22
164
  adversarialCorpus: "process"
23
165
  })
24
166
  });
@@ -32,6 +174,27 @@ function isBackendId(value) {
32
174
  function backendDescriptor(id) {
33
175
  return BACKENDS[id];
34
176
  }
177
+ function isLocalHost(hostname) {
178
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
179
+ if (host === "localhost" || host.endsWith(".localhost")) return true;
180
+ if (host === "::1") return true;
181
+ if (host.startsWith("127.")) return true;
182
+ if (host.startsWith("10.")) return true;
183
+ if (host.startsWith("192.168.")) return true;
184
+ if (/^172\.(1[6-9]|2\d|3[01])\./.test(host)) return true;
185
+ if (/^f[cd]/.test(host)) return true;
186
+ return false;
187
+ }
188
+ function resolveCost(id, baseUrl) {
189
+ const declared = BACKENDS[id].cost;
190
+ if (declared !== null) return declared;
191
+ if (baseUrl === void 0) return "metered";
192
+ try {
193
+ return isLocalHost(new URL(baseUrl).hostname) ? "free" : "metered";
194
+ } catch {
195
+ return "metered";
196
+ }
197
+ }
35
198
 
36
199
  // src/audience.ts
37
200
  var Audience = z2.enum(["self", "named", "public"]);
@@ -50,12 +213,18 @@ var MatchRefusal = z2.enum([
50
213
  /** The backend offers only `self` and the job belongs to someone else. */
51
214
  "offer-scope-too-narrow",
52
215
  /** The matched backend is subscription-class, which is locked to `self`. */
53
- "subscription-self-lock"
216
+ "subscription-self-lock",
217
+ /** The backend spends the owner's money and they have not agreed to share it. */
218
+ "metered-no-spend-consent",
219
+ /** The backend is shared but has spent its ceiling for now. */
220
+ "metered-ceiling-reached"
54
221
  ]);
55
222
  var ALLOWED = Object.freeze({ ok: true });
56
223
  var refuse = (refusal) => Object.freeze({ ok: false, refusal });
57
- function effectiveOfferScope(configured, account) {
58
- return account === "subscription" ? "self" : configured;
224
+ function effectiveOfferScope(configured, cost, spend) {
225
+ if (cost === "subscription") return "self";
226
+ if (cost === "metered" && spend?.acknowledged !== true) return "self";
227
+ return configured;
59
228
  }
60
229
  function matchAudience(job, daemon) {
61
230
  const sameOwner = job.owner === daemon.owner;
@@ -65,13 +234,25 @@ function matchAudience(job, daemon) {
65
234
  if (job.audience === "named" && !sameOwner && job.audienceAllow !== void 0 && !job.audienceAllow.includes(daemon.owner)) {
66
235
  return refuse("not-in-server-allowlist");
67
236
  }
68
- const scope = effectiveOfferScope(daemon.offerScope, daemon.account);
237
+ const scope = effectiveOfferScope(
238
+ daemon.offerScope,
239
+ daemon.cost,
240
+ daemon.spend
241
+ );
69
242
  if (sameOwner) {
70
243
  return ALLOWED;
71
244
  }
72
- if (daemon.account === "subscription") {
245
+ if (daemon.cost === "subscription") {
73
246
  return refuse("subscription-self-lock");
74
247
  }
248
+ if (daemon.cost === "metered") {
249
+ if (daemon.spend?.acknowledged !== true) {
250
+ return refuse("metered-no-spend-consent");
251
+ }
252
+ if (daemon.spend.ceilingReached === true) {
253
+ return refuse("metered-ceiling-reached");
254
+ }
255
+ }
75
256
  switch (scope) {
76
257
  case "self":
77
258
  return refuse("offer-scope-too-narrow");
@@ -87,7 +268,9 @@ var REFUSAL_MESSAGES = Object.freeze({
87
268
  "not-locally-allowed": "the job's owner is not on this machine's allowlist (byollm allow <server> <user>)",
88
269
  "not-in-server-allowlist": "the app restricted this job to named runners and this machine is not one of them",
89
270
  "offer-scope-too-narrow": "this backend is offered to its owner only (byollm offer <backend> named|public to widen)",
90
- "subscription-self-lock": "subscription-backed models run their owner's work only \u2014 this is a protocol rule, not a setting"
271
+ "subscription-self-lock": "subscription-backed models run their owner's work only \u2014 this is a protocol rule, not a setting",
272
+ "metered-no-spend-consent": "this backend bills its owner per token, and they have not agreed to spend it on other people's work",
273
+ "metered-ceiling-reached": "this backend is shared but has reached the spend ceiling its owner set"
91
274
  });
92
275
 
93
276
  // src/kinds.ts
@@ -167,6 +350,21 @@ function canTransition(from, to) {
167
350
  return TRANSITIONS[from].includes(to);
168
351
  }
169
352
  var Lease = z4.object({
353
+ /**
354
+ * Identifies *this* grant, not just its holder.
355
+ *
356
+ * A runner can hold a job, release it, and claim it again — three leases,
357
+ * one runner id. Without an id for the grant itself, a lease-scoped request
358
+ * names a mutable target ambiguously, and a replayed release from the first
359
+ * grant lands on the third: the job returns to the queue while the daemon
360
+ * is mid-execution, and the work runs twice on the owner's hardware.
361
+ *
362
+ * That was a live hole, found in review after signed requests shipped. The
363
+ * signature scheme's replay argument rests on endpoints being idempotent —
364
+ * and release *is*, per lease, but not across leases, because nothing in
365
+ * the request said which one.
366
+ */
367
+ id: z4.string().min(1),
170
368
  /** The runner holding the lease. */
171
369
  runnerId: z4.string().min(1),
172
370
  /** Epoch milliseconds after which the claim is void. */
@@ -238,6 +436,333 @@ var DeliveredResult = z4.object({
238
436
  outcome: JobOutcome.optional(),
239
437
  provenance: ResultProvenance.optional()
240
438
  }).strict();
439
+ var SizeClass = z4.enum(["small", "medium", "large", "unbounded"]);
440
+ var SIZE_CLASS_LIMITS = Object.freeze({
441
+ small: 4e3,
442
+ medium: 64e3,
443
+ large: Number.POSITIVE_INFINITY
444
+ });
445
+ function sizeClassCeiling(sizeClass) {
446
+ if (sizeClass === "unbounded") return Number.POSITIVE_INFINITY;
447
+ return SIZE_CLASS_LIMITS[sizeClass];
448
+ }
449
+ function sizeClassOf(textChars) {
450
+ if (textChars <= SIZE_CLASS_LIMITS.small) return "small";
451
+ if (textChars <= SIZE_CLASS_LIMITS.medium) return "medium";
452
+ return "large";
453
+ }
454
+ var JobStub = z4.object({
455
+ id: z4.string().min(1),
456
+ kind: JobKind,
457
+ /** The app's id for the user who enqueued it. */
458
+ owner: z4.string().min(1),
459
+ audience: Audience,
460
+ audienceAllow: z4.array(z4.string().min(1)).optional(),
461
+ sizeClass: SizeClass,
462
+ /** Reserved for byollm_006. False until streaming exists. */
463
+ streaming: z4.boolean(),
464
+ /** Epoch ms after which the work is pointless; bounds ciphertext retention. */
465
+ deadlineAt: z4.number().int().positive()
466
+ }).strict();
467
+ var ClaimedStub = JobStub.extend({ lease: Lease }).strict();
468
+
469
+ // src/envelope.ts
470
+ import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2 } from "crypto";
471
+ import sodium from "libsodium-wrappers";
472
+ import { z as z6 } from "zod";
473
+
474
+ // src/keys.ts
475
+ import {
476
+ createHash,
477
+ createPrivateKey,
478
+ createPublicKey,
479
+ generateKeyPairSync,
480
+ sign,
481
+ verify
482
+ } from "crypto";
483
+ import { z as z5 } from "zod";
484
+ var PublicIdentity = z5.object({
485
+ /** Raw Ed25519 public key. The pinned one. */
486
+ identity: z5.string().min(1),
487
+ /** Raw X25519 public key, for sealing to this party. */
488
+ encryption: z5.string().min(1),
489
+ /**
490
+ * Ed25519 signature over the encryption key, by the identity key.
491
+ *
492
+ * This is what stops an upstream substituting an encryption key of its
493
+ * own while relaying a genuine identity: the receiver pins the identity
494
+ * and refuses any encryption key not signed by it.
495
+ */
496
+ encryptionSig: z5.string().min(1)
497
+ }).strict();
498
+ var StoredKeys = z5.object({
499
+ version: z5.literal(1),
500
+ identityPublic: z5.string().min(1),
501
+ identityPrivate: z5.string().min(1),
502
+ encryptionPublic: z5.string().min(1),
503
+ encryptionPrivate: z5.string().min(1),
504
+ encryptionSig: z5.string().min(1),
505
+ createdAt: z5.number().int().positive()
506
+ }).strict();
507
+ var ENCRYPTION_KEY_CONTEXT = "byollm/v1/encryption-key";
508
+ function rawPublic(key) {
509
+ const jwk = key.export({ format: "jwk" });
510
+ const x = jwk.x;
511
+ if (typeof x !== "string") throw new Error("key has no raw public component");
512
+ return x;
513
+ }
514
+ function importPublic(raw, crv) {
515
+ return createPublicKey({ key: { kty: "OKP", crv, x: raw }, format: "jwk" });
516
+ }
517
+ function importPrivate(stored) {
518
+ return createPrivateKey({
519
+ key: Buffer.from(stored, "base64"),
520
+ type: "pkcs8",
521
+ format: "der"
522
+ });
523
+ }
524
+ var exportPrivate = (key) => key.export({ type: "pkcs8", format: "der" }).toString("base64");
525
+ function generateKeys(now) {
526
+ const identity = generateKeyPairSync("ed25519");
527
+ const encryption = generateKeyPairSync("x25519");
528
+ const encryptionPublic = rawPublic(encryption.publicKey);
529
+ return {
530
+ version: 1,
531
+ identityPublic: rawPublic(identity.publicKey),
532
+ identityPrivate: exportPrivate(identity.privateKey),
533
+ encryptionPublic,
534
+ encryptionPrivate: exportPrivate(encryption.privateKey),
535
+ encryptionSig: sign(
536
+ null,
537
+ Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${encryptionPublic}`),
538
+ identity.privateKey
539
+ ).toString("base64url"),
540
+ createdAt: now
541
+ };
542
+ }
543
+ function publicIdentityOf(keys) {
544
+ return {
545
+ identity: keys.identityPublic,
546
+ encryption: keys.encryptionPublic,
547
+ encryptionSig: keys.encryptionSig
548
+ };
549
+ }
550
+ function verifyPublicIdentity(identity) {
551
+ try {
552
+ return verify(
553
+ null,
554
+ Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${identity.encryption}`),
555
+ importPublic(identity.identity, "Ed25519"),
556
+ Buffer.from(identity.encryptionSig, "base64url")
557
+ );
558
+ } catch {
559
+ return false;
560
+ }
561
+ }
562
+ function signWith(keys, data) {
563
+ return sign(null, data, importPrivate(keys.identityPrivate)).toString(
564
+ "base64url"
565
+ );
566
+ }
567
+ function verifyWith(identityPublic, data, signature) {
568
+ try {
569
+ return verify(
570
+ null,
571
+ data,
572
+ importPublic(identityPublic, "Ed25519"),
573
+ Buffer.from(signature, "base64url")
574
+ );
575
+ } catch {
576
+ return false;
577
+ }
578
+ }
579
+ var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
580
+ function fingerprint(identityPublic) {
581
+ const digest = createHash("sha256").update(Buffer.from(identityPublic, "base64url")).digest();
582
+ let bits = 0;
583
+ let value = 0;
584
+ let out = "";
585
+ for (const byte of digest.subarray(0, 15)) {
586
+ value = value << 8 | byte;
587
+ bits += 8;
588
+ while (bits >= 5) {
589
+ out += ALPHABET.charAt(value >>> bits - 5 & 31);
590
+ bits -= 5;
591
+ }
592
+ }
593
+ const groups = out.match(/.{1,4}/g) ?? [];
594
+ return `BYOLLM-${groups.join("-")}`;
595
+ }
596
+ var keyId = (identityPublic) => fingerprint(identityPublic);
597
+
598
+ // src/envelope.ts
599
+ var readied;
600
+ async function cryptoReady() {
601
+ readied ??= sodium.ready;
602
+ await readied;
603
+ }
604
+ var ENVELOPE_MAX_AGE_MS = 24 * 60 * 6e4;
605
+ var EnvelopeDirection = z6.enum(["payload", "result"]);
606
+ var SealedEnvelope = z6.object({
607
+ /** Base64url `crypto_box_seal` output over the signed plaintext. */
608
+ ciphertext: z6.string().min(1),
609
+ /** Who this was sealed to — the recipient checks it is them. */
610
+ recipientKeyId: z6.string().min(1),
611
+ /** Who signed it — the recipient checks this against its pin. */
612
+ senderKeyId: z6.string().min(1),
613
+ direction: EnvelopeDirection,
614
+ /**
615
+ * When this ciphertext stops being worth keeping.
616
+ *
617
+ * Carried *on* the envelope rather than recomputed by the opener. An
618
+ * earlier version derived it from the job's creation time, which meant
619
+ * two systems had to agree on a timestamp to the millisecond — and they
620
+ * did not, once a real database rounded it. A bound value that has to be
621
+ * reconstructed is a bound value that eventually is not.
622
+ *
623
+ * Not trusted as written: it is also inside the signature, so a changed
624
+ * deadline fails to verify.
625
+ */
626
+ deadlineAt: z6.number().int().positive()
627
+ }).strict();
628
+ function signedBody(context, plaintext) {
629
+ return Buffer.from(
630
+ JSON.stringify({
631
+ v: "byollm/v1/envelope",
632
+ jobId: context.jobId,
633
+ senderKeyId: context.senderKeyId,
634
+ recipientKeyId: context.recipientKeyId,
635
+ deadlineAt: context.deadlineAt,
636
+ direction: context.direction,
637
+ plaintext
638
+ }),
639
+ "utf8"
640
+ );
641
+ }
642
+ var rawX25519 = (key, part) => {
643
+ const jwk = key.export({ format: "jwk" });
644
+ const value = part === "x" ? jwk.x : jwk.d;
645
+ if (typeof value !== "string") throw new Error("not an X25519 key");
646
+ return new Uint8Array(Buffer.from(value, "base64url"));
647
+ };
648
+ async function seal(input) {
649
+ await cryptoReady();
650
+ const body = signedBody(input.context, input.plaintext);
651
+ const signature = signWith(input.senderKeys, body);
652
+ const inner = JSON.stringify({ body: body.toString("base64url"), signature });
653
+ const recipient = new Uint8Array(
654
+ Buffer.from(input.recipientEncryptionPublic, "base64url")
655
+ );
656
+ const ciphertext = sodium.crypto_box_seal(
657
+ new Uint8Array(Buffer.from(inner, "utf8")),
658
+ recipient
659
+ );
660
+ return {
661
+ ciphertext: Buffer.from(ciphertext).toString("base64url"),
662
+ recipientKeyId: input.context.recipientKeyId,
663
+ senderKeyId: input.context.senderKeyId,
664
+ direction: input.context.direction,
665
+ deadlineAt: input.context.deadlineAt
666
+ };
667
+ }
668
+ async function open(input) {
669
+ await cryptoReady();
670
+ const { envelope, expected } = input;
671
+ if (envelope.recipientKeyId !== expected.recipientKeyId || envelope.senderKeyId !== expected.senderKeyId || envelope.direction !== expected.direction) {
672
+ return { ok: false, reason: "not-for-us" };
673
+ }
674
+ let inner;
675
+ try {
676
+ const priv = createPrivateKey2({
677
+ key: Buffer.from(input.recipientKeys.encryptionPrivate, "base64"),
678
+ type: "pkcs8",
679
+ format: "der"
680
+ });
681
+ const pub = createPublicKey2(priv);
682
+ const opened = sodium.crypto_box_seal_open(
683
+ new Uint8Array(Buffer.from(envelope.ciphertext, "base64url")),
684
+ rawX25519(pub, "x"),
685
+ rawX25519(priv, "d")
686
+ );
687
+ inner = Buffer.from(opened).toString("utf8");
688
+ } catch {
689
+ return { ok: false, reason: "unopenable" };
690
+ }
691
+ let parsed;
692
+ try {
693
+ parsed = JSON.parse(inner);
694
+ } catch {
695
+ return { ok: false, reason: "malformed" };
696
+ }
697
+ if (typeof parsed.body !== "string" || typeof parsed.signature !== "string") {
698
+ return { ok: false, reason: "malformed" };
699
+ }
700
+ const body = Buffer.from(parsed.body, "base64url");
701
+ if (!verifyWith(input.senderIdentityPublic, body, parsed.signature)) {
702
+ return { ok: false, reason: "bad-signature" };
703
+ }
704
+ let claims;
705
+ try {
706
+ claims = JSON.parse(body.toString("utf8"));
707
+ } catch {
708
+ return { ok: false, reason: "malformed" };
709
+ }
710
+ if (claims["jobId"] !== expected.jobId || claims["senderKeyId"] !== expected.senderKeyId || claims["recipientKeyId"] !== expected.recipientKeyId || claims["deadlineAt"] !== envelope.deadlineAt || claims["direction"] !== expected.direction) {
711
+ return { ok: false, reason: "context-mismatch" };
712
+ }
713
+ if (typeof claims["plaintext"] !== "string") {
714
+ return { ok: false, reason: "malformed" };
715
+ }
716
+ return { ok: true, plaintext: claims["plaintext"] };
717
+ }
718
+
719
+ // src/signing.ts
720
+ import { createHash as createHash2 } from "crypto";
721
+ import { z as z7 } from "zod";
722
+ var MAX_CLOCK_SKEW_MS = 12e4;
723
+ var RequestSignature = z7.object({
724
+ /** Which runner is calling. The server looks up its pinned identity. */
725
+ runnerId: z7.string().min(1),
726
+ /** Epoch ms, bounded by {@link MAX_CLOCK_SKEW_MS}. */
727
+ issuedAt: z7.number().int().positive(),
728
+ /** Base64url Ed25519 signature over {@link canonicalRequest}. */
729
+ signature: z7.string().min(1)
730
+ }).strict();
731
+ function canonicalRequest(input) {
732
+ const digest = createHash2("sha256").update(input.body, "utf8").digest("hex");
733
+ return Buffer.from(
734
+ [
735
+ "byollm/v1/request",
736
+ input.endpoint,
737
+ input.runnerId,
738
+ String(input.issuedAt),
739
+ digest
740
+ ].join("\n"),
741
+ "utf8"
742
+ );
743
+ }
744
+ function signRequest(keys, input) {
745
+ return {
746
+ runnerId: input.runnerId,
747
+ issuedAt: input.issuedAt,
748
+ signature: signWith(keys, canonicalRequest(input))
749
+ };
750
+ }
751
+ function verifyRequest(input) {
752
+ const skew = input.maxSkewMs ?? MAX_CLOCK_SKEW_MS;
753
+ if (Math.abs(input.now - input.signature.issuedAt) > skew) return "stale";
754
+ const ok = verifyWith(
755
+ input.identityPublic,
756
+ canonicalRequest({
757
+ endpoint: input.endpoint,
758
+ runnerId: input.signature.runnerId,
759
+ issuedAt: input.signature.issuedAt,
760
+ body: input.body
761
+ }),
762
+ input.signature.signature
763
+ );
764
+ return ok ? null : "bad-signature";
765
+ }
241
766
 
242
767
  // src/musts.ts
243
768
  var must = (m) => Object.freeze(m);
@@ -247,31 +772,78 @@ var MUSTS = Object.freeze({
247
772
  id: "PAIR_ONE_USER",
248
773
  statement: "A runner token MUST be bound to exactly one user; a daemon MUST refuse work not attributable to its paired user.",
249
774
  enforcedBy: "both",
775
+ verifiedBy: "conformance",
250
776
  source: "byollm_001 \xA7MUSTs"
251
777
  }),
252
778
  PAIR_INTERACTIVE: must({
253
779
  id: "PAIR_INTERACTIVE",
254
780
  statement: "Pairing MUST be interactive (device-code approval in the app's own session); a long-lived pasted secret MUST NOT be accepted as pairing.",
255
781
  enforcedBy: "server",
782
+ verifiedBy: "conformance",
256
783
  source: "byollm_001 \xA7Endpoints.1"
257
784
  }),
258
785
  PAIR_CODE_EXPIRES: must({
259
786
  id: "PAIR_CODE_EXPIRES",
260
787
  statement: "An unapproved device code MUST expire and MUST NOT be redeemable after expiry.",
261
788
  enforcedBy: "server",
789
+ verifiedBy: "conformance",
262
790
  source: "byollm_001 \xA7Endpoints.1"
263
791
  }),
264
792
  // ---- Typed job kinds --------------------------------------------------
793
+ VERSION_HANDSHAKE_REQUIRED: must({
794
+ id: "VERSION_HANDSHAKE_REQUIRED",
795
+ 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.",
796
+ enforcedBy: "both",
797
+ verifiedBy: "conformance",
798
+ source: "byollm_009 \xA74"
799
+ }),
800
+ KEYS_EXCHANGED_AT_CONSENT: must({
801
+ id: "KEYS_EXCHANGED_AT_CONSENT",
802
+ 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.",
803
+ enforcedBy: "both",
804
+ verifiedBy: "conformance",
805
+ source: "byollm_009 \xA75"
806
+ }),
807
+ REQUESTS_SIGNED_NOT_BEARER: must({
808
+ id: "REQUESTS_SIGNED_NOT_BEARER",
809
+ 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.",
810
+ enforcedBy: "both",
811
+ verifiedBy: "conformance",
812
+ source: "byollm_009 \xA74.2"
813
+ }),
814
+ LEASE_SCOPED_BY_GRANT: must({
815
+ id: "LEASE_SCOPED_BY_GRANT",
816
+ 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.",
817
+ enforcedBy: "both",
818
+ verifiedBy: "conformance",
819
+ source: "byollm_009 \xA74.2"
820
+ }),
821
+ STUB_METADATA_EXHAUSTIVE: must({
822
+ id: "STUB_METADATA_EXHAUSTIVE",
823
+ 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.",
824
+ enforcedBy: "both",
825
+ verifiedBy: "conformance",
826
+ source: "byollm_009 \xA76"
827
+ }),
828
+ ENVELOPE_SEALED_AND_SIGNED: must({
829
+ id: "ENVELOPE_SEALED_AND_SIGNED",
830
+ 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.",
831
+ enforcedBy: "server",
832
+ verifiedBy: "conformance",
833
+ source: "byollm_009 \xA76"
834
+ }),
265
835
  KIND_TYPED_ONLY: must({
266
836
  id: "KIND_TYPED_ONLY",
267
837
  statement: "Job kinds MUST resolve against handlers baked into the daemon. A daemon MUST refuse an unknown kind rather than guess.",
268
838
  enforcedBy: "daemon",
839
+ verifiedBy: "conformance",
269
840
  source: "byollm_001 \xA7Jobs are typed data"
270
841
  }),
271
842
  KIND_NO_CODE: must({
272
843
  id: "KIND_NO_CODE",
273
844
  statement: "A server MUST NOT be able to convey code, a shell string, or a path to execute; payloads are data handed to a model only.",
274
845
  enforcedBy: "daemon",
846
+ verifiedBy: "conformance",
275
847
  source: "byollm_001 \xA7Jobs are typed data; byollm_004 \xA71"
276
848
  }),
277
849
  // ---- Capability and claiming -----------------------------------------
@@ -279,18 +851,21 @@ var MUSTS = Object.freeze({
279
851
  id: "CLAIM_REQUIRES_CAPABILITY",
280
852
  statement: "A daemon MUST NOT be given a job whose kind is absent from its advertised capability matrix.",
281
853
  enforcedBy: "both",
854
+ verifiedBy: "conformance",
282
855
  source: "byollm_001 \xA7MUSTs"
283
856
  }),
284
857
  CAPABILITY_IS_DETECTED: must({
285
858
  id: "CAPABILITY_IS_DETECTED",
286
859
  statement: "An advertised capability matrix MUST be the intersection of owner config and detected, healthy reality \u2014 never config alone.",
287
860
  enforcedBy: "daemon",
861
+ verifiedBy: "conformance",
288
862
  source: "byollm_002 \xA7Routing"
289
863
  }),
290
864
  CLAIM_ATOMIC: must({
291
865
  id: "CLAIM_ATOMIC",
292
866
  statement: "Claiming MUST be atomic: a job MUST NOT be handed to two runners concurrently.",
293
867
  enforcedBy: "server",
868
+ verifiedBy: "conformance",
294
869
  source: "byollm_001 \xA7Endpoints.2"
295
870
  }),
296
871
  // ---- Leases -----------------------------------------------------------
@@ -298,12 +873,14 @@ var MUSTS = Object.freeze({
298
873
  id: "LEASE_HONORED",
299
874
  statement: "A daemon MUST stop work on a job whose lease it has failed to renew, and MUST NOT report a result for an expired lease it no longer holds.",
300
875
  enforcedBy: "daemon",
876
+ verifiedBy: "conformance",
301
877
  source: "byollm_001 \xA7MUSTs"
302
878
  }),
303
879
  LEASE_RECLAIMABLE: must({
304
880
  id: "LEASE_RECLAIMABLE",
305
881
  statement: "A lease that expires un-renewed MUST make its job claimable again with no loss of the job.",
306
882
  enforcedBy: "server",
883
+ verifiedBy: "conformance",
307
884
  source: "byollm_001 \xA7Endpoints.2"
308
885
  }),
309
886
  // ---- Audience and offer scope ----------------------------------------
@@ -311,24 +888,56 @@ var MUSTS = Object.freeze({
311
888
  id: "AUDIENCE_BOTH_SIDES",
312
889
  statement: "A job MUST run on a daemon only if the daemon's offer scope admits the job's owner AND the job's audience admits the daemon's owner.",
313
890
  enforcedBy: "both",
891
+ verifiedBy: "conformance",
314
892
  source: "byollm_001 \xA7The audience model"
315
893
  }),
316
894
  SUBSCRIPTION_SELF_LOCK: must({
317
895
  id: "SUBSCRIPTION_SELF_LOCK",
318
896
  statement: "A subscription-class backend's offer scope MUST be 'self' and MUST NOT be widened by configuration.",
319
897
  enforcedBy: "daemon",
898
+ verifiedBy: "conformance",
320
899
  source: "byollm_001 \xA7The audience model"
321
900
  }),
901
+ METERED_DEFAULTS_SELF: must({
902
+ id: "METERED_DEFAULTS_SELF",
903
+ statement: "A metered backend's effective offer scope MUST be 'self' unless the owner has explicitly acknowledged spending money on others' work.",
904
+ enforcedBy: "daemon",
905
+ verifiedBy: "conformance",
906
+ source: "byollm_007 \xA74"
907
+ }),
908
+ METERED_REQUIRES_CEILING: must({
909
+ id: "METERED_REQUIRES_CEILING",
910
+ statement: "A widened metered backend MUST carry a spend ceiling, and the daemon MUST refuse community work once it is reached.",
911
+ enforcedBy: "daemon",
912
+ verifiedBy: "conformance",
913
+ source: "byollm_007 \xA74"
914
+ }),
915
+ COST_NOT_CONFIGURABLE: must({
916
+ id: "COST_NOT_CONFIGURABLE",
917
+ statement: "A built-in provider's cost class MUST NOT be overridable by configuration.",
918
+ enforcedBy: "daemon",
919
+ verifiedBy: "conformance",
920
+ source: "byollm_007 \xA72"
921
+ }),
922
+ REMOTE_IS_NEVER_FREE: must({
923
+ id: "REMOTE_IS_NEVER_FREE",
924
+ statement: "A generic HTTP backend whose base URL is not loopback or private MUST be treated as metered.",
925
+ enforcedBy: "daemon",
926
+ verifiedBy: "conformance",
927
+ source: "byollm_007 \xA72"
928
+ }),
322
929
  NAMED_LOCAL_ALLOWLIST: must({
323
930
  id: "NAMED_LOCAL_ALLOWLIST",
324
931
  statement: "A 'named' job MUST be admitted only by the daemon's own local (server origin, user id) allowlist \u2014 never on the server's assertion alone.",
325
932
  enforcedBy: "daemon",
933
+ verifiedBy: "conformance",
326
934
  source: "byollm_001 Rev 1 \xA7B"
327
935
  }),
328
936
  REFUSAL_NOT_REOFFERED: must({
329
937
  id: "REFUSAL_NOT_REOFFERED",
330
938
  statement: "A server MUST NOT re-offer a job to a runner that released it with reason 'refused'.",
331
939
  enforcedBy: "server",
940
+ verifiedBy: "conformance",
332
941
  source: "byollm_001 Rev 1 \xA7B (loop resolved in build review)"
333
942
  }),
334
943
  // ---- Revocation and cancel -------------------------------------------
@@ -336,12 +945,14 @@ var MUSTS = Object.freeze({
336
945
  id: "REVOCATION_HONORED",
337
946
  statement: "A revoked daemon MUST stop claiming and MUST abandon in-flight work by the next heartbeat at the latest.",
338
947
  enforcedBy: "daemon",
948
+ verifiedBy: "conformance",
339
949
  source: "byollm_001 \xA7MUSTs"
340
950
  }),
341
951
  CANCEL_HONORED: must({
342
952
  id: "CANCEL_HONORED",
343
953
  statement: "A job id in a heartbeat response's cancel list MUST abort that job's in-flight backend call and be reported as 'canceled'.",
344
954
  enforcedBy: "daemon",
955
+ verifiedBy: "conformance",
345
956
  source: "byollm_001 Rev 1 \xA7C"
346
957
  }),
347
958
  // ---- Lifecycle, dependencies, delivery -------------------------------
@@ -349,30 +960,35 @@ var MUSTS = Object.freeze({
349
960
  id: "DEPENDS_ON_GATING",
350
961
  statement: "A job MUST NOT be claimable until every job in its dependsOn set has reached the 'ok' state.",
351
962
  enforcedBy: "server",
963
+ verifiedBy: "conformance",
352
964
  source: "byollm_001 Rev 1 \xA7E"
353
965
  }),
354
966
  TTL_EXPIRY: must({
355
967
  id: "TTL_EXPIRY",
356
968
  statement: "An unclaimed job MUST become 'expired' once its TTL elapses, and the TTL clock MUST start when the job becomes claimable, not at enqueue.",
357
969
  enforcedBy: "server",
970
+ verifiedBy: "conformance",
358
971
  source: "byollm_001 Rev 1 \xA7D (TTL clock resolved in build review)"
359
972
  }),
360
973
  NO_RUNNER_SIGNAL: must({
361
974
  id: "NO_RUNNER_SIGNAL",
362
975
  statement: "A server MUST surface noRunnerAvailable when no runner with matching capability has heartbeated within the liveness window, and MUST NOT raise it for a job still blocked on dependencies.",
363
976
  enforcedBy: "server",
977
+ verifiedBy: "conformance",
364
978
  source: "byollm_001 Rev 1 \xA7D"
365
979
  }),
366
980
  RESULT_IDEMPOTENT: must({
367
981
  id: "RESULT_IDEMPOTENT",
368
982
  statement: "Result submission MUST be idempotent by job id; the first terminal outcome wins and later submissions MUST NOT change it.",
369
983
  enforcedBy: "server",
984
+ verifiedBy: "conformance",
370
985
  source: "byollm_001 \xA7Endpoints.4"
371
986
  }),
372
987
  RESULT_PROVENANCE: must({
373
988
  id: "RESULT_PROVENANCE",
374
989
  statement: "A result from a non-'self' job MUST carry its provenance (audience and runner) to the delivery seam so an app never treats volunteer output as first-party.",
375
990
  enforcedBy: "server",
991
+ verifiedBy: "conformance",
376
992
  source: "byollm_003 Rev 1 \xA7Return-trip"
377
993
  }),
378
994
  // ---- The trust surface -------------------------------------------------
@@ -380,6 +996,7 @@ var MUSTS = Object.freeze({
380
996
  id: "INGRESS_LOGGED_BEFORE_EXECUTION",
381
997
  statement: "Every executed prompt MUST be appended to the local ingress log before execution begins.",
382
998
  enforcedBy: "daemon",
999
+ verifiedBy: "conformance",
383
1000
  source: "byollm_001 \xA7MUSTs"
384
1001
  }),
385
1002
  // ---- Execution isolation (byollm_004) ---------------------------------
@@ -387,178 +1004,263 @@ var MUSTS = Object.freeze({
387
1004
  id: "NO_SHELL_INTERPOLATION",
388
1005
  statement: "Process-class backends MUST be invoked with a fixed argv array and the payload delivered on stdin; payload text MUST NOT reach a command line.",
389
1006
  enforcedBy: "daemon",
1007
+ verifiedBy: "adversarial",
390
1008
  source: "byollm_004 \xA72"
391
1009
  }),
392
1010
  NO_PAYLOAD_ROUTING: must({
393
1011
  id: "NO_PAYLOAD_ROUTING",
394
1012
  statement: "Model, backend, base URL, and flags MUST come from owner config only; a payload MUST NOT influence any of them.",
395
1013
  enforcedBy: "daemon",
1014
+ verifiedBy: "adversarial",
396
1015
  source: "byollm_004 \xA72"
397
1016
  }),
398
1017
  STRIPPED_CHILD_ENV: must({
399
1018
  id: "STRIPPED_CHILD_ENV",
400
1019
  statement: "Process-class children MUST spawn with an allowlisted environment, a scratch cwd, no inherited descriptors beyond std streams, and hard timeout and output-size caps.",
401
1020
  enforcedBy: "daemon",
1021
+ verifiedBy: "adversarial",
402
1022
  source: "byollm_004 \xA72"
403
1023
  }),
404
1024
  HTTP_BASE_URL_SAFE: must({
405
1025
  id: "HTTP_BASE_URL_SAFE",
406
1026
  statement: "HTTP-class backends MUST send requests only to the owner-configured base URL and MUST refuse base URLs resolving to cloud-metadata or link-local addresses.",
407
1027
  enforcedBy: "daemon",
1028
+ verifiedBy: "adversarial",
408
1029
  source: "byollm_004 Rev 1 \xA7Backend taxonomy"
409
1030
  }),
410
1031
  OUTPUT_INERT: must({
411
1032
  id: "OUTPUT_INERT",
412
1033
  statement: "Returned text MUST be treated as inert bytes: never evaluated, never written to a payload-named path, never interpolated into a shell or into terminal control sequences when logged.",
413
1034
  enforcedBy: "daemon",
1035
+ verifiedBy: "adversarial",
414
1036
  source: "byollm_004 \xA72"
415
1037
  }),
416
1038
  COMMUNITY_BUDGETS: must({
417
1039
  id: "COMMUNITY_BUDGETS",
418
1040
  statement: "Jobs whose owner is not the daemon's owner MUST be subject to the owner's rate limits, daily cap, and resource budget.",
419
1041
  enforcedBy: "daemon",
1042
+ verifiedBy: "adversarial",
420
1043
  source: "byollm_004 \xA74"
421
1044
  })
422
1045
  });
423
1046
  var MUST_IDS = Object.freeze(Object.keys(MUSTS));
1047
+ function mustsVerifiedBy(kind) {
1048
+ return MUST_IDS.filter((id) => MUSTS[id].verifiedBy === kind);
1049
+ }
424
1050
 
425
1051
  // src/wire.ts
426
- import { z as z5 } from "zod";
1052
+ import { z as z8 } from "zod";
427
1053
  var PROTOCOL_VERSION = "0";
1054
+ var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze([
1055
+ PROTOCOL_VERSION
1056
+ ]);
1057
+ var MIN_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0] ?? PROTOCOL_VERSION;
1058
+ function checkProtocolVersion(body) {
1059
+ const declared = typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion") ? body.protocolVersion : void 0;
1060
+ if (typeof declared !== "string" || declared.length === 0) {
1061
+ return {
1062
+ error: "unsupported-protocol-version",
1063
+ message: "this request declared no protocol version. Upgrade the daemon: `npm i -g byollm@alpha`.",
1064
+ supported: SUPPORTED_PROTOCOL_VERSIONS,
1065
+ minimum: MIN_PROTOCOL_VERSION
1066
+ };
1067
+ }
1068
+ if (!SUPPORTED_PROTOCOL_VERSIONS.includes(declared)) {
1069
+ return {
1070
+ error: "unsupported-protocol-version",
1071
+ message: `this server speaks protocol ${SUPPORTED_PROTOCOL_VERSIONS.join(", ")} and the daemon asked for ${declared}. ` + (declared < MIN_PROTOCOL_VERSION ? "Upgrade the daemon: `npm i -g byollm@alpha`." : "This daemon is newer than the server; the server needs upgrading."),
1072
+ supported: SUPPORTED_PROTOCOL_VERSIONS,
1073
+ minimum: MIN_PROTOCOL_VERSION
1074
+ };
1075
+ }
1076
+ return null;
1077
+ }
428
1078
  var PROTOCOL_PREFIX = "/byollm";
429
1079
  var ENDPOINTS = Object.freeze([
430
1080
  "pair",
431
1081
  "claim",
1082
+ "fetch",
432
1083
  "heartbeat",
433
1084
  "result",
434
1085
  "release"
435
1086
  ]);
436
- var Capability = z5.object({
1087
+ var Capability = z8.object({
437
1088
  kind: JobKind,
438
1089
  backendId: BackendIdSchema,
439
1090
  backendClass: BackendClass,
440
- model: z5.string().min(1),
1091
+ model: z8.string().min(1),
441
1092
  offerScope: OfferScope
442
1093
  }).strict();
443
- var CapabilityMatrix = z5.array(Capability);
444
- var PairStartRequest = z5.object({
445
- protocolVersion: z5.literal(PROTOCOL_VERSION),
446
- action: z5.literal("start"),
447
- daemon: z5.object({
448
- version: z5.string().min(1),
1094
+ var CapabilityMatrix = z8.array(Capability);
1095
+ var PairStartRequest = z8.object({
1096
+ protocolVersion: z8.literal(PROTOCOL_VERSION),
1097
+ action: z8.literal("start"),
1098
+ daemon: z8.object({
1099
+ version: z8.string().min(1),
449
1100
  /** Shown in the app's runner list so a user can tell their machines apart. */
450
- label: z5.string().min(1).max(120),
451
- platform: z5.enum(["darwin", "linux", "win32"])
1101
+ label: z8.string().min(1).max(120),
1102
+ platform: z8.enum(["darwin", "linux", "win32"])
452
1103
  }),
1104
+ /**
1105
+ * This machine's public keys (byollm_009 §5).
1106
+ *
1107
+ * Pairing is where the two parties learn each other's identities, because
1108
+ * it is the one moment a human is already deciding to trust: the approval
1109
+ * click. A key exchanged anywhere else would be a key nobody chose.
1110
+ */
1111
+ device: PublicIdentity,
453
1112
  capabilities: CapabilityMatrix
454
1113
  }).strict();
455
- var PairStartResponse = z5.object({
1114
+ var PairStartResponse = z8.object({
456
1115
  /** Secret the daemon polls with. Never shown to the user. */
457
- deviceCode: z5.string().min(20),
1116
+ deviceCode: z8.string().min(20),
458
1117
  /** Short code the user reads and confirms in the browser. */
459
- userCode: z5.string().min(4).max(16),
1118
+ userCode: z8.string().min(4).max(16),
460
1119
  /** Where the user approves. Must be on the server's own origin. */
461
- verificationUrl: z5.url(),
1120
+ verificationUrl: z8.url(),
462
1121
  /** Epoch ms after which the code is dead ({@link MUSTS.PAIR_CODE_EXPIRES}). */
463
- expiresAt: z5.number().int().positive(),
1122
+ expiresAt: z8.number().int().positive(),
464
1123
  /** How often the daemon may poll. */
465
- pollIntervalMs: z5.number().int().min(500).max(6e4)
1124
+ pollIntervalMs: z8.number().int().min(500).max(6e4)
466
1125
  }).strict();
467
- var PairPollRequest = z5.object({
468
- protocolVersion: z5.literal(PROTOCOL_VERSION),
469
- action: z5.literal("poll"),
470
- deviceCode: z5.string().min(20)
1126
+ var PairPollRequest = z8.object({
1127
+ protocolVersion: z8.literal(PROTOCOL_VERSION),
1128
+ action: z8.literal("poll"),
1129
+ deviceCode: z8.string().min(20)
471
1130
  }).strict();
472
- var PairPollResponse = z5.discriminatedUnion("status", [
473
- z5.object({ status: z5.literal("pending") }).strict(),
474
- z5.object({ status: z5.literal("denied") }).strict(),
475
- z5.object({ status: z5.literal("expired") }).strict(),
476
- z5.object({
477
- status: z5.literal("approved"),
1131
+ var PairPollResponse = z8.discriminatedUnion("status", [
1132
+ z8.object({ status: z8.literal("pending") }).strict(),
1133
+ z8.object({ status: z8.literal("denied") }).strict(),
1134
+ z8.object({ status: z8.literal("expired") }).strict(),
1135
+ z8.object({
1136
+ status: z8.literal("approved"),
478
1137
  /** Bearer token for every later call. Scoped to exactly one user. */
479
- runnerToken: z5.string().min(20),
480
- runnerId: z5.string().min(1),
1138
+ runnerToken: z8.string().min(20),
1139
+ runnerId: z8.string().min(1),
481
1140
  /** The app's id for the approving user — this daemon's owner forever. */
482
- owner: z5.string().min(1),
1141
+ owner: z8.string().min(1),
483
1142
  /** Display name for the trust UI, if the app offers one. */
484
- ownerLabel: z5.string().optional()
1143
+ ownerLabel: z8.string().optional(),
1144
+ /**
1145
+ * The site's public keys, for the daemon to pin (byollm_009 §5).
1146
+ *
1147
+ * Returned only on approval — a pending or denied poll learns nothing,
1148
+ * so an unapproved code cannot be used to enumerate a site's keys.
1149
+ */
1150
+ site: PublicIdentity
485
1151
  }).strict()
486
1152
  ]);
487
- var PairRequest = z5.discriminatedUnion("action", [
1153
+ var PairRequest = z8.discriminatedUnion("action", [
488
1154
  PairStartRequest,
489
1155
  PairPollRequest
490
1156
  ]);
491
- var ClaimRequest = z5.object({
492
- protocolVersion: z5.literal(PROTOCOL_VERSION),
493
- runnerId: z5.string().min(1),
1157
+ var ClaimRequest = z8.object({
1158
+ protocolVersion: z8.literal(PROTOCOL_VERSION),
1159
+ runnerId: z8.string().min(1),
494
1160
  /** Re-sent on every claim so a server never matches against a stale matrix. */
495
1161
  capabilities: CapabilityMatrix,
496
1162
  /** Upper bound on jobs to return; the server may return fewer. */
497
- max: z5.number().int().min(1).max(64)
1163
+ max: z8.number().int().min(1).max(64)
498
1164
  }).strict();
499
- var ClaimResponse = z5.object({
500
- jobs: z5.array(ClaimedJob),
1165
+ var ClaimResponse = z8.object({
1166
+ /**
1167
+ * Stubs, not jobs. The payload arrives from `fetch`, sealed to whichever
1168
+ * device claimed — see {@link JobStub} for the exhaustive metadata list.
1169
+ */
1170
+ jobs: z8.array(ClaimedStub),
501
1171
  /** Lease duration granted, so the daemon knows its renewal deadline. */
502
- leaseMs: z5.number().int().positive()
1172
+ leaseMs: z8.number().int().positive()
503
1173
  }).strict();
504
- var HeartbeatRequest = z5.object({
505
- protocolVersion: z5.literal(PROTOCOL_VERSION),
506
- runnerId: z5.string().min(1),
507
- daemonVersion: z5.string().min(1),
1174
+ var HeartbeatRequest = z8.object({
1175
+ protocolVersion: z8.literal(PROTOCOL_VERSION),
1176
+ runnerId: z8.string().min(1),
1177
+ daemonVersion: z8.string().min(1),
508
1178
  capabilities: CapabilityMatrix,
509
- /** Jobs this daemon believes it holds; the server renews their leases. */
510
- activeJobIds: z5.array(z5.string().min(1)),
1179
+ /**
1180
+ * Leases this daemon believes it holds; the server renews exactly these.
1181
+ *
1182
+ * Lease ids rather than job ids, so a replayed heartbeat cannot renew a
1183
+ * grant the runner no longer holds — see {@link Lease.id}.
1184
+ */
1185
+ activeLeases: z8.array(
1186
+ z8.object({ jobId: z8.string().min(1), leaseId: z8.string().min(1) })
1187
+ ),
511
1188
  /** True while the owner has the daemon paused; the server stops offering work. */
512
- paused: z5.boolean()
1189
+ paused: z8.boolean()
513
1190
  }).strict();
514
- var HeartbeatResponse = z5.object({
1191
+ var HeartbeatResponse = z8.object({
515
1192
  /** Once true, the daemon stops claiming and abandons in-flight work. */
516
- revoked: z5.boolean(),
1193
+ revoked: z8.boolean(),
517
1194
  /**
518
1195
  * Per-job cancel (byollm_001 Rev 1 §C). The daemon aborts these jobs'
519
1196
  * in-flight backend calls and reports them `canceled`.
520
1197
  */
521
- cancel: z5.array(z5.string().min(1)),
1198
+ cancel: z8.array(z8.string().min(1)),
522
1199
  /** Jobs whose leases were renewed, with their new expiry. */
523
- leases: z5.array(
524
- z5.object({
525
- jobId: z5.string().min(1),
526
- expiresAt: z5.number().int().positive()
1200
+ leases: z8.array(
1201
+ z8.object({
1202
+ jobId: z8.string().min(1),
1203
+ expiresAt: z8.number().int().positive()
527
1204
  }).strict()
528
1205
  ),
529
1206
  /**
530
1207
  * Jobs the daemon thinks it holds but the server has reassigned or
531
1208
  * expired. The daemon must stop work on these and not report results.
532
1209
  */
533
- lost: z5.array(z5.string().min(1)),
1210
+ lost: z8.array(z8.string().min(1)),
534
1211
  /** Server clock, so a daemon with a skewed clock still honors leases. */
535
- serverTime: z5.number().int().positive()
1212
+ serverTime: z8.number().int().positive()
536
1213
  }).strict();
537
- var ResultRequest = z5.object({
538
- protocolVersion: z5.literal(PROTOCOL_VERSION),
539
- runnerId: z5.string().min(1),
540
- jobId: z5.string().min(1),
541
- outcome: JobOutcome,
1214
+ var ResultDisposition = z8.enum(["ok", "error", "canceled"]);
1215
+ var ResultRequest = z8.object({
1216
+ protocolVersion: z8.literal(PROTOCOL_VERSION),
1217
+ runnerId: z8.string().min(1),
1218
+ jobId: z8.string().min(1),
1219
+ /**
1220
+ * The outcome, sealed to the site and signed by the device.
1221
+ *
1222
+ * The return leg of the payload envelope, and sealed for the same reason:
1223
+ * a model's answer is as sensitive as the prompt that produced it, and an
1224
+ * intermediary that cannot read one must not be handed the other.
1225
+ */
1226
+ envelope: SealedEnvelope,
1227
+ /**
1228
+ * The sealed outcome's discriminator, in the clear.
1229
+ *
1230
+ * Checked against the envelope once opened. It is a routing hint, not a
1231
+ * fact: believing it unverified would let a daemon mark a job `ok` while
1232
+ * sealing an error, and only the app would ever find out.
1233
+ */
1234
+ disposition: ResultDisposition,
542
1235
  /** Which model actually served it, for the result's provenance. */
543
- model: z5.string().min(1),
1236
+ model: z8.string().min(1),
544
1237
  backendClass: BackendClass,
545
1238
  /** Wall-clock milliseconds the backend call took. */
546
- durationMs: z5.number().int().nonnegative()
1239
+ durationMs: z8.number().int().nonnegative()
547
1240
  }).strict();
548
- var ResultResponse = z5.object({
1241
+ var ResultResponse = z8.object({
549
1242
  /**
550
1243
  * False when the submission lost an idempotency race or the lease was
551
1244
  * already gone — the daemon should discard, not retry
552
1245
  * ({@link MUSTS.RESULT_IDEMPOTENT}).
553
1246
  */
554
- accepted: z5.boolean(),
1247
+ accepted: z8.boolean(),
555
1248
  /** The job's state after this submission. */
556
- state: z5.string().min(1)
1249
+ state: z8.string().min(1)
557
1250
  }).strict();
558
- var ReleaseRequest = z5.object({
559
- protocolVersion: z5.literal(PROTOCOL_VERSION),
560
- runnerId: z5.string().min(1),
561
- jobIds: z5.array(z5.string().min(1)),
1251
+ var ReleaseRequest = z8.object({
1252
+ protocolVersion: z8.literal(PROTOCOL_VERSION),
1253
+ runnerId: z8.string().min(1),
1254
+ /**
1255
+ * Which leases to release — the grant, not just the job.
1256
+ *
1257
+ * A release naming only a job id releases whatever lease exists at the
1258
+ * moment it arrives, which for a replayed request is not the lease the
1259
+ * daemon meant. See {@link Lease.id}.
1260
+ */
1261
+ leases: z8.array(
1262
+ z8.object({ jobId: z8.string().min(1), leaseId: z8.string().min(1) })
1263
+ ),
562
1264
  /**
563
1265
  * Why, so the app's runner list can say something true.
564
1266
  *
@@ -568,12 +1270,12 @@ var ReleaseRequest = z5.object({
568
1270
  * stop offering that job to that runner, or the pair would spin between
569
1271
  * claim and release forever.
570
1272
  */
571
- reason: z5.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
1273
+ reason: z8.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
572
1274
  }).strict();
573
- var ReleaseResponse = z5.object({
574
- released: z5.array(z5.string().min(1))
1275
+ var ReleaseResponse = z8.object({
1276
+ released: z8.array(z8.string().min(1))
575
1277
  }).strict();
576
- var WireErrorCode = z5.enum([
1278
+ var WireErrorCode = z8.enum([
577
1279
  "bad-request",
578
1280
  "unsupported-protocol-version",
579
1281
  "unauthorized",
@@ -582,11 +1284,11 @@ var WireErrorCode = z5.enum([
582
1284
  "rate-limited",
583
1285
  "server-error"
584
1286
  ]);
585
- var WireError = z5.object({
1287
+ var WireError = z8.object({
586
1288
  error: WireErrorCode,
587
- message: z5.string().min(1),
1289
+ message: z8.string().min(1),
588
1290
  /** Seconds; mirrors Retry-After for `rate-limited` and `server-error`. */
589
- retryAfter: z5.number().int().nonnegative().optional()
1291
+ retryAfter: z8.number().int().nonnegative().optional()
590
1292
  }).strict();
591
1293
  var ERROR_STATUS = Object.freeze({
592
1294
  "bad-request": 400,
@@ -597,13 +1299,37 @@ var ERROR_STATUS = Object.freeze({
597
1299
  "rate-limited": 429,
598
1300
  "server-error": 500
599
1301
  });
1302
+ var FetchRequest = z8.object({
1303
+ protocolVersion: z8.string().min(1),
1304
+ runnerId: z8.string().min(1),
1305
+ jobId: z8.string().min(1),
1306
+ /**
1307
+ * The grant this daemon holds.
1308
+ *
1309
+ * Named, not inferred: a fetch is lease-scoped, and a request that names
1310
+ * only the job would be answerable for whatever lease exists when it
1311
+ * arrives ({@link Lease.id}).
1312
+ */
1313
+ leaseId: z8.string().min(1)
1314
+ }).strict();
1315
+ var FetchResponse = z8.object({
1316
+ /**
1317
+ * The work, sealed to the device that claimed it — byollm_009 §6.
1318
+ *
1319
+ * Not plaintext. The site opens its own at-rest envelope and re-seals to
1320
+ * the claiming device's key, signed by the site's identity, so the work
1321
+ * is readable only by the machine that took it and only if it came from
1322
+ * the site that machine pinned.
1323
+ */
1324
+ envelope: SealedEnvelope
1325
+ }).strict();
600
1326
  export {
601
1327
  AUDIENCES,
602
1328
  Audience,
603
1329
  BACKENDS,
604
1330
  BACKEND_IDS,
605
- BackendAccount,
606
1331
  BackendClass,
1332
+ BackendCost,
607
1333
  BackendIdSchema,
608
1334
  Capability,
609
1335
  CapabilityMatrix,
@@ -612,9 +1338,14 @@ export {
612
1338
  ClaimRequest,
613
1339
  ClaimResponse,
614
1340
  ClaimedJob,
1341
+ ClaimedStub,
615
1342
  DeliveredResult,
616
1343
  ENDPOINTS,
1344
+ ENVELOPE_MAX_AGE_MS,
617
1345
  ERROR_STATUS,
1346
+ EnvelopeDirection,
1347
+ FetchRequest,
1348
+ FetchResponse,
618
1349
  GeneratePayload,
619
1350
  HeartbeatRequest,
620
1351
  HeartbeatResponse,
@@ -626,8 +1357,11 @@ export {
626
1357
  JobResultError,
627
1358
  JobResultOk,
628
1359
  JobState,
1360
+ JobStub,
629
1361
  KindedPayload,
630
1362
  Lease,
1363
+ MAX_CLOCK_SKEW_MS,
1364
+ MIN_PROTOCOL_VERSION,
631
1365
  MUSTS,
632
1366
  MUST_IDS,
633
1367
  MatchRefusal,
@@ -641,23 +1375,50 @@ export {
641
1375
  PairRequest,
642
1376
  PairStartRequest,
643
1377
  PairStartResponse,
1378
+ PublicIdentity,
644
1379
  REFUSAL_MESSAGES,
645
1380
  ReleaseRequest,
646
1381
  ReleaseResponse,
1382
+ RequestSignature,
1383
+ ResultDisposition,
647
1384
  ResultProvenance,
648
1385
  ResultRequest,
649
1386
  ResultResponse,
1387
+ SIZE_CLASS_LIMITS,
1388
+ SUPPORTED_PROTOCOL_VERSIONS,
1389
+ SealedEnvelope,
1390
+ SizeClass,
1391
+ StoredKeys,
650
1392
  TERMINAL_STATES,
651
1393
  WireError,
652
1394
  WireErrorCode,
653
1395
  backendDescriptor,
654
1396
  canTransition,
1397
+ canonicalRequest,
1398
+ checkProtocolVersion,
1399
+ cryptoReady,
655
1400
  effectiveOfferScope,
1401
+ fingerprint,
1402
+ generateKeys,
656
1403
  isBackendId,
657
1404
  isJobKind,
1405
+ isLocalHost,
658
1406
  isTerminal,
1407
+ keyId,
659
1408
  matchAudience,
1409
+ mustsVerifiedBy,
1410
+ open,
660
1411
  payloadTextLength,
661
- provenanceFor
1412
+ provenanceFor,
1413
+ publicIdentityOf,
1414
+ resolveCost,
1415
+ seal,
1416
+ signRequest,
1417
+ signWith,
1418
+ sizeClassCeiling,
1419
+ sizeClassOf,
1420
+ verifyPublicIdentity,
1421
+ verifyRequest,
1422
+ verifyWith
662
1423
  };
663
1424
  //# sourceMappingURL=index.js.map