@byollm/protocol 0.1.0-alpha.0 → 0.1.0-alpha.10

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.d.ts CHANGED
@@ -5,9 +5,10 @@ import { z } from 'zod';
5
5
  * Rev 1 §A, because the two classes have different threat surfaces.
6
6
  *
7
7
  * - `http`: an OpenAI-compatible HTTP server (Ollama, `mlx_lm.server`,
8
- * llama.cpp server, vLLM). Spawns nothing, so byollm_004 §2's argv, stdin,
9
- * env and sandbox requirements are not applicable by construction. Its
10
- * threat surface is SSRF-shaped and bounded by {@link MUSTS.HTTP_BASE_URL_SAFE}.
8
+ * llama.cpp server, vLLM, and every hosted provider that speaks the same
9
+ * wire format). Spawns nothing, so byollm_004 §2's argv, stdin, env and
10
+ * sandbox requirements are not applicable by construction. Its threat
11
+ * surface is SSRF-shaped and bounded by {@link MUSTS.HTTP_BASE_URL_SAFE}.
11
12
  * - `process`: spawns a binary (`claude` CLI today, `mlx_lm.lora` for a
12
13
  * future `train.*` kind). All of byollm_004 §2 is mandatory here.
13
14
  */
@@ -17,20 +18,26 @@ declare const BackendClass: z.ZodEnum<{
17
18
  }>;
18
19
  type BackendClass = z.infer<typeof BackendClass>;
19
20
  /**
20
- * Whose account pays for the inference.
21
+ * Who pays, and how — byollm_007.
21
22
  *
22
- * `subscription` backends run against a vendor account belonging to the
23
- * machine's owner. They are hard-locked to an offer scope of `self`
24
- * ({@link MUSTS.SUBSCRIPTION_SELF_LOCK}) one account executes one person's
25
- * work. This is orthogonal to {@link BackendClass}: `claude-cli` is both
26
- * process-class and subscription-class, while a future local `mlx_lm.lora`
27
- * backend would be process-class and open.
23
+ * This replaced a two-valued `account` field that conflated two unrelated
24
+ * constraints and, in doing so, left a hole: `openai-http` was "open", but it
25
+ * accepts an API key, so an owner could point it at a paid endpoint, offer it
26
+ * `public`, and donate their credit balance to strangers. The community
27
+ * budgets cap job *count*, not spend.
28
+ *
29
+ * - `free` — local compute. Costs electricity, not money. Shareable.
30
+ * - `metered` — per-token billing against the owner's account. Legal to
31
+ * share and ruinous to share by accident.
32
+ * - `subscription` — a vendor account whose terms forbid third-party work.
33
+ * Sharing is a terms violation, not merely expensive.
28
34
  */
29
- declare const BackendAccount: z.ZodEnum<{
30
- open: "open";
35
+ declare const BackendCost: z.ZodEnum<{
36
+ free: "free";
37
+ metered: "metered";
31
38
  subscription: "subscription";
32
39
  }>;
33
- type BackendAccount = z.infer<typeof BackendAccount>;
40
+ type BackendCost = z.infer<typeof BackendCost>;
34
41
  /** The immutable facts about a backend that the protocol reasons over. */
35
42
  interface BackendDescriptor {
36
43
  /** Stable backend id, as written in `byollm.config.json`. */
@@ -39,33 +46,86 @@ interface BackendDescriptor {
39
46
  readonly label: string;
40
47
  /** Determines which isolation requirements apply. */
41
48
  readonly class: BackendClass;
42
- /** Determines whether the offer scope can be widened past `self`. */
43
- readonly account: BackendAccount;
49
+ /**
50
+ * Who pays. Fixed here for every named provider and **not overridable by
51
+ * configuration** ({@link MUSTS.COST_NOT_CONFIGURABLE}) — `openai` is
52
+ * metered because it is, and no setting changes that.
53
+ *
54
+ * `null` only for the generic {@link BACKENDS."openai-http"} entry, whose
55
+ * cost is inferred from its base URL instead
56
+ * ({@link MUSTS.REMOTE_IS_NEVER_FREE}).
57
+ */
58
+ readonly cost: BackendCost | null;
44
59
  /**
45
60
  * Which adversarial corpus byollm_004 §5 runs against this backend. A
46
61
  * backend cannot be registered without one — the coverage check in the
47
62
  * adversarial suite enforces it.
48
63
  */
49
64
  readonly adversarialCorpus: "process" | "http";
65
+ /**
66
+ * Where this provider lives, when that is knowable. Owner config may
67
+ * override it; a provider with no default requires one to be given.
68
+ */
69
+ readonly defaultBaseUrl?: string;
50
70
  }
51
71
  /**
52
- * The v1 backend registry.
72
+ * The backend registry.
53
73
  *
54
- * byollm_001 Rev 1 §A collapses four planned backends into one HTTP-class
55
- * entry: Ollama, `mlx_lm.server`, llama.cpp server and vLLM all speak
56
- * OpenAI-compatible `/v1/chat/completions`, so they are one backend with N
57
- * owner-configured base URLs rather than four adapters. That is what puts
58
- * MLX inference in v1.
74
+ * **Providers are entries, not implementations.** Every HTTP-class provider
75
+ * below shares the single `openai-http` transport, because they all speak
76
+ * OpenAI-compatible `/v1/chat/completions`. An entry adds a stable id, a cost
77
+ * class the owner cannot override, and a default base URL. Adding a provider
78
+ * is therefore one line and no new code — which is why the adversarial corpus
79
+ * still covers all of them, and why a PR adding one is reviewable at a glance.
59
80
  */
60
81
  declare const BACKENDS: Readonly<{
82
+ readonly ollama: BackendDescriptor;
83
+ readonly mlx: BackendDescriptor;
84
+ readonly llamacpp: BackendDescriptor;
85
+ readonly vllm: BackendDescriptor;
86
+ readonly lmstudio: BackendDescriptor;
87
+ readonly jan: BackendDescriptor;
88
+ readonly localai: BackendDescriptor;
89
+ /**
90
+ * Note the pair: `anthropic` and {@link BACKENDS."claude-cli"} reach the
91
+ * same vendor and land in different cost classes. That is not an
92
+ * inconsistency — it is the axis working. One bills a key per token, the
93
+ * other runs under a personal plan whose terms cover one person's work. Who
94
+ * pays and under what terms is the question; which company is not.
95
+ */
96
+ readonly anthropic: BackendDescriptor;
97
+ readonly openai: BackendDescriptor;
98
+ readonly gemini: BackendDescriptor;
99
+ readonly grok: BackendDescriptor;
100
+ readonly groq: BackendDescriptor;
101
+ readonly openrouter: BackendDescriptor;
102
+ readonly together: BackendDescriptor;
103
+ readonly deepseek: BackendDescriptor;
104
+ readonly mistral: BackendDescriptor;
61
105
  readonly "openai-http": BackendDescriptor;
62
106
  readonly "claude-cli": BackendDescriptor;
63
107
  }>;
64
108
  /** The id of a registered backend. */
65
109
  type BackendId = keyof typeof BACKENDS;
66
110
  /** All registered backend ids — the adversarial coverage check iterates this. */
67
- declare const BACKEND_IDS: readonly ("openai-http" | "claude-cli")[];
111
+ declare const BACKEND_IDS: readonly ("ollama" | "mlx" | "llamacpp" | "vllm" | "lmstudio" | "jan" | "localai" | "anthropic" | "openai" | "gemini" | "grok" | "groq" | "openrouter" | "together" | "deepseek" | "mistral" | "openai-http" | "claude-cli")[];
68
112
  declare const BackendIdSchema: z.ZodEnum<{
113
+ ollama: "ollama";
114
+ mlx: "mlx";
115
+ llamacpp: "llamacpp";
116
+ vllm: "vllm";
117
+ lmstudio: "lmstudio";
118
+ jan: "jan";
119
+ localai: "localai";
120
+ anthropic: "anthropic";
121
+ openai: "openai";
122
+ gemini: "gemini";
123
+ grok: "grok";
124
+ groq: "groq";
125
+ openrouter: "openrouter";
126
+ together: "together";
127
+ deepseek: "deepseek";
128
+ mistral: "mistral";
69
129
  "openai-http": "openai-http";
70
130
  "claude-cli": "claude-cli";
71
131
  }>;
@@ -78,6 +138,34 @@ declare function isBackendId(value: string): value is BackendId;
78
138
  * adversarial corpus, so refusing is the safe direction.
79
139
  */
80
140
  declare function backendDescriptor(id: BackendId): BackendDescriptor;
141
+ /**
142
+ * Is this host local enough that compute there is free?
143
+ *
144
+ * Loopback and the private ranges only. This is the rule that makes
145
+ * {@link MUSTS.REMOTE_IS_NEVER_FREE} enforceable rather than a promise: an
146
+ * owner cannot reach a paid API through the generic backend and call it free,
147
+ * because "free" is derived from the address, not from what the config claims.
148
+ *
149
+ * **What this cannot see.** The address is all it reads. A proxy on
150
+ * `127.0.0.1` forwarding to a paid API classes as `free` and nothing
151
+ * downstream will contradict it. That is deliberate: standing up a relay is
152
+ * an act by the machine's owner against their own account, and the threat
153
+ * model here is a hostile *job*, not an owner routing around a rule that
154
+ * exists to protect them. What this catches is the accident — a remote paid
155
+ * endpoint offered `public` because nobody thought about the bill. See
156
+ * `docs/security.md` §4a.
157
+ */
158
+ declare function isLocalHost(hostname: string): boolean;
159
+ /**
160
+ * The cost class of a configured backend instance.
161
+ *
162
+ * For every named provider this is whatever the registry says, full stop
163
+ * ({@link MUSTS.COST_NOT_CONFIGURABLE}). For the generic `openai-http` entry
164
+ * it is inferred from the base URL, and a base URL that cannot be parsed is
165
+ * treated as `metered` — the expensive side, because guessing "free" wrong
166
+ * costs the owner money.
167
+ */
168
+ declare function resolveCost(id: BackendId, baseUrl: string | undefined): BackendCost;
81
169
 
82
170
  /**
83
171
  * Who may run a job, declared by the app that enqueued it.
@@ -121,6 +209,8 @@ declare const MatchRefusal: z.ZodEnum<{
121
209
  "not-in-server-allowlist": "not-in-server-allowlist";
122
210
  "offer-scope-too-narrow": "offer-scope-too-narrow";
123
211
  "subscription-self-lock": "subscription-self-lock";
212
+ "metered-no-spend-consent": "metered-no-spend-consent";
213
+ "metered-ceiling-reached": "metered-ceiling-reached";
124
214
  }>;
125
215
  type MatchRefusal = z.infer<typeof MatchRefusal>;
126
216
  /** The outcome of an audience match. */
@@ -130,16 +220,30 @@ type MatchResult = {
130
220
  readonly ok: false;
131
221
  readonly refusal: MatchRefusal;
132
222
  };
223
+ /** What the owner has agreed to spend on other people's work, if anything. */
224
+ interface SpendConsent {
225
+ /** The owner explicitly acknowledged that sharing this backend costs money. */
226
+ readonly acknowledged: boolean;
227
+ /** Their ceiling. Absent means no ceiling was set, which is not consent. */
228
+ readonly ceilingReached?: boolean;
229
+ }
133
230
  /**
134
231
  * The effective offer scope of a backend.
135
232
  *
136
- * A subscription-class backend is locked to `self` regardless of what config
137
- * requests ({@link MUSTS.SUBSCRIPTION_SELF_LOCK}). This is a protocol MUST,
138
- * not a setting: the lock is applied here, at the one place both the daemon's
139
- * config loader and its matcher call, so there is no code path that observes
140
- * a widened subscription scope.
233
+ * Three rules, applied at the one place both the daemon's config loader and
234
+ * its matcher call, so no code path can observe a scope wider than the cost
235
+ * class allows:
236
+ *
237
+ * - `subscription` is locked to `self` regardless of config
238
+ * ({@link MUSTS.SUBSCRIPTION_SELF_LOCK}) — someone else's terms.
239
+ * - `metered` narrows to `self` unless the owner has explicitly acknowledged
240
+ * the spend ({@link MUSTS.METERED_DEFAULTS_SELF}) — their money.
241
+ * - `free` passes through — their electricity.
242
+ *
243
+ * Note the asymmetry: subscription can never be widened, metered can be
244
+ * widened deliberately. Conflating those was byollm_007's bug.
141
245
  */
142
- declare function effectiveOfferScope(configured: OfferScope, account: BackendAccount): OfferScope;
246
+ declare function effectiveOfferScope(configured: OfferScope, cost: BackendCost, spend?: SpendConsent): OfferScope;
143
247
  /** The job-side facts a match needs. */
144
248
  interface MatchJob {
145
249
  /** The app's id for the user who enqueued the job. */
@@ -159,8 +263,10 @@ interface MatchDaemon {
159
263
  readonly owner: string;
160
264
  /** Effective scope of the backend that would run the job. */
161
265
  readonly offerScope: OfferScope;
162
- /** Account class of that backend. */
163
- readonly account: BackendAccount;
266
+ /** Who pays for that backend's tokens. */
267
+ readonly cost: BackendCost;
268
+ /** What the owner agreed to spend on others, for a `metered` backend. */
269
+ readonly spend?: SpendConsent | undefined;
164
270
  /**
165
271
  * Does this daemon's *local* allowlist admit the given owner for the server
166
272
  * origin the job came from? Supplied as a predicate so the protocol package
@@ -189,7 +295,7 @@ interface MatchDaemon {
189
295
  * {
190
296
  * owner: "bob",
191
297
  * offerScope: "named",
192
- * account: "open",
298
+ * cost: "free",
193
299
  * locallyAllows: (o) => o === "alice",
194
300
  * },
195
301
  * );
@@ -342,6 +448,7 @@ declare function isTerminal(state: JobState): boolean;
342
448
  declare function canTransition(from: JobState, to: JobState): boolean;
343
449
  /** A lease: the right to work on a job until `expiresAt`. */
344
450
  declare const Lease: z.ZodObject<{
451
+ id: z.ZodString;
345
452
  runnerId: z.ZodString;
346
453
  expiresAt: z.ZodNumber;
347
454
  }, z.core.$strip>;
@@ -398,6 +505,7 @@ declare const ClaimedJob: z.ZodObject<{
398
505
  owner: z.ZodString;
399
506
  audienceAllow: z.ZodOptional<z.ZodArray<z.ZodString>>;
400
507
  lease: z.ZodObject<{
508
+ id: z.ZodString;
401
509
  runnerId: z.ZodString;
402
510
  expiresAt: z.ZodNumber;
403
511
  }, z.core.$strip>;
@@ -508,6 +616,401 @@ declare const DeliveredResult: z.ZodObject<{
508
616
  }, z.core.$strict>>;
509
617
  }, z.core.$strict>;
510
618
  type DeliveredResult = z.infer<typeof DeliveredResult>;
619
+ /**
620
+ * How big a payload is, in buckets — byollm_009 §6.
621
+ *
622
+ * A relay routes without reading, and matching a job to a machine needs some
623
+ * notion of size. Buckets rather than byte counts because the exact figure is
624
+ * a stronger fingerprint than the routing decision requires, and because a
625
+ * bucket survives compression and encoding changes that an exact count does
626
+ * not.
627
+ *
628
+ * `unbounded` exists for streamed jobs, which have no size when they start.
629
+ * It is reserved now rather than added later: byollm_009 §8.1 — adding a
630
+ * field to a published envelope is the v2 break all over again.
631
+ */
632
+ declare const SizeClass: z.ZodEnum<{
633
+ small: "small";
634
+ medium: "medium";
635
+ large: "large";
636
+ unbounded: "unbounded";
637
+ }>;
638
+ type SizeClass = z.infer<typeof SizeClass>;
639
+ /** Where the bucket boundaries sit, in characters of payload text. */
640
+ declare const SIZE_CLASS_LIMITS: Readonly<{
641
+ small: 4000;
642
+ medium: 64000;
643
+ large: number;
644
+ }>;
645
+ /**
646
+ * The most a payload in this bucket can be.
647
+ *
648
+ * Used where a decision must be made from a stub, before the payload has been
649
+ * fetched — a budget check, for instance. Charging the bucket's ceiling is the
650
+ * conservative direction: it refuses slightly too eagerly rather than
651
+ * admitting work that turns out larger than the budget allowed.
652
+ *
653
+ * `unbounded` returns `Infinity`, which fails every ceiling. That is correct
654
+ * until byollm_006 defines how a streamed job is budgeted — failing closed on
655
+ * a case nobody has designed beats inventing an allowance for it.
656
+ */
657
+ declare function sizeClassCeiling(sizeClass: SizeClass): number;
658
+ /** Bucket a payload by its text length. */
659
+ declare function sizeClassOf(textChars: number): SizeClass;
660
+ /**
661
+ * Everything an upstream may see about a job — byollm_009 §6.
662
+ *
663
+ * **This list is exhaustive and normative.** It is a commitment about the
664
+ * metadata surface, not an accident of what the implementation happens to
665
+ * send: an upstream that requires more has exceeded the protocol, and an
666
+ * endpoint that emits more has leaked past it
667
+ * ({@link MUSTS.STUB_METADATA_EXHAUSTIVE}).
668
+ *
669
+ * What is absent is the point. No payload, no model, no prompt, no result.
670
+ * `kind` is here because capability matching happens upstream; if a later
671
+ * revision moves matching to the daemon, `kind` moves into the ciphertext.
672
+ */
673
+ declare const JobStub: z.ZodObject<{
674
+ id: z.ZodString;
675
+ kind: z.ZodEnum<{
676
+ "llm.generate": "llm.generate";
677
+ "llm.chat": "llm.chat";
678
+ }>;
679
+ owner: z.ZodString;
680
+ audience: z.ZodEnum<{
681
+ self: "self";
682
+ named: "named";
683
+ public: "public";
684
+ }>;
685
+ audienceAllow: z.ZodOptional<z.ZodArray<z.ZodString>>;
686
+ sizeClass: z.ZodEnum<{
687
+ small: "small";
688
+ medium: "medium";
689
+ large: "large";
690
+ unbounded: "unbounded";
691
+ }>;
692
+ streaming: z.ZodBoolean;
693
+ deadlineAt: z.ZodNumber;
694
+ }, z.core.$strict>;
695
+ type JobStub = z.infer<typeof JobStub>;
696
+ /** A stub, plus the lease the claiming runner now holds for it. */
697
+ declare const ClaimedStub: z.ZodObject<{
698
+ id: z.ZodString;
699
+ kind: z.ZodEnum<{
700
+ "llm.generate": "llm.generate";
701
+ "llm.chat": "llm.chat";
702
+ }>;
703
+ owner: z.ZodString;
704
+ audience: z.ZodEnum<{
705
+ self: "self";
706
+ named: "named";
707
+ public: "public";
708
+ }>;
709
+ audienceAllow: z.ZodOptional<z.ZodArray<z.ZodString>>;
710
+ sizeClass: z.ZodEnum<{
711
+ small: "small";
712
+ medium: "medium";
713
+ large: "large";
714
+ unbounded: "unbounded";
715
+ }>;
716
+ streaming: z.ZodBoolean;
717
+ deadlineAt: z.ZodNumber;
718
+ lease: z.ZodObject<{
719
+ id: z.ZodString;
720
+ runnerId: z.ZodString;
721
+ expiresAt: z.ZodNumber;
722
+ }, z.core.$strip>;
723
+ }, z.core.$strict>;
724
+ type ClaimedStub = z.infer<typeof ClaimedStub>;
725
+
726
+ /**
727
+ * Device and site keys — byollm_009 §3.
728
+ *
729
+ * **Two keypairs per party, and the split is load-bearing.** An Ed25519
730
+ * *identity* key signs; an X25519 *encryption* key receives sealed envelopes.
731
+ * The encryption key is signed by the identity key, and **the identity key is
732
+ * what gets pinned**. So "who sent this" and "who can read this" are answered
733
+ * by different keys — which is what lets an encryption key rotate without
734
+ * re-establishing trust, and what byollm_009 §6's signed-then-sealed envelope
735
+ * depends on.
736
+ *
737
+ * **No new dependency.** byollm_009 §2 says established primitives only, via
738
+ * libsodium. Everything *this* module needs — Ed25519 signing, X25519 key
739
+ * generation — Node provides natively, and using it costs nothing and adds no
740
+ * install weight to a daemon that must land fast on a stranger's laptop.
741
+ *
742
+ * libsodium becomes necessary at envelope v2, where sealing does. That is a
743
+ * real dependency decision and it belongs in the change that needs it: a
744
+ * sealed box is a specific reviewed construction, and rebuilding it out of
745
+ * Node primitives is exactly the "novel construction" §2 rules out. Deferring
746
+ * the dependency is not the same as deferring the rule.
747
+ */
748
+ /** A public identity, as it travels on the wire. All values base64url. */
749
+ declare const PublicIdentity: z.ZodObject<{
750
+ identity: z.ZodString;
751
+ encryption: z.ZodString;
752
+ encryptionSig: z.ZodString;
753
+ }, z.core.$strict>;
754
+ type PublicIdentity = z.infer<typeof PublicIdentity>;
755
+ /** Private key material, as stored on disk. Never leaves the machine. */
756
+ declare const StoredKeys: z.ZodObject<{
757
+ version: z.ZodLiteral<1>;
758
+ identityPublic: z.ZodString;
759
+ identityPrivate: z.ZodString;
760
+ encryptionPublic: z.ZodString;
761
+ encryptionPrivate: z.ZodString;
762
+ encryptionSig: z.ZodString;
763
+ createdAt: z.ZodNumber;
764
+ }, z.core.$strict>;
765
+ type StoredKeys = z.infer<typeof StoredKeys>;
766
+ /** Generate a fresh pair of keypairs and bind them together. */
767
+ declare function generateKeys(now: number): StoredKeys;
768
+ /** The public half, for the wire. */
769
+ declare function publicIdentityOf(keys: StoredKeys): PublicIdentity;
770
+ /**
771
+ * Check that an encryption key really belongs to the identity presenting it.
772
+ *
773
+ * Called on everything received, including from an upstream we otherwise
774
+ * trust — the point of pinning the identity is that nothing else needs to be
775
+ * trusted, and that only holds if this is checked every time rather than at
776
+ * first sight.
777
+ */
778
+ declare function verifyPublicIdentity(identity: PublicIdentity): boolean;
779
+ /** Sign arbitrary bytes with an identity key. */
780
+ declare function signWith(keys: StoredKeys, data: Uint8Array): string;
781
+ /** Verify bytes against a raw Ed25519 public key. */
782
+ declare function verifyWith(identityPublic: string, data: Uint8Array, signature: string): boolean;
783
+ /**
784
+ * A fingerprint a human can compare out loud.
785
+ *
786
+ * 120 bits of SHA-256 over the raw identity key, as six groups of four. Long
787
+ * enough that grinding a colliding key is not worth anyone's afternoon, short
788
+ * enough to read down a phone line — which is the whole point. A fingerprint
789
+ * nobody can be bothered to compare provides no security at all, so
790
+ * legibility is a security property here, not a nicety.
791
+ *
792
+ * Formatted with a `BYOLLM-` prefix so a pasted fingerprint is recognisable
793
+ * out of context, in a support thread or a screenshot.
794
+ */
795
+ declare function fingerprint(identityPublic: string): string;
796
+ /** The short id used in envelopes and provenance. Stable, and comparable. */
797
+ declare const keyId: (identityPublic: string) => string;
798
+
799
+ declare function cryptoReady(): Promise<void>;
800
+ /**
801
+ * How long a sealed payload is worth keeping, from creation.
802
+ *
803
+ * Bound into every envelope and recomputed when one is opened, so it lives
804
+ * here rather than in the two places that need it. Two copies of a value the
805
+ * signature depends on is the same bug as two clock readings: it works until
806
+ * they disagree, and then nothing can be opened.
807
+ *
808
+ * Not a job's TTL. That answers how long the *work* is worth doing, belongs
809
+ * to the app and the store, and may legitimately differ per deployment.
810
+ */
811
+ declare const ENVELOPE_MAX_AGE_MS: number;
812
+ /** Which leg an envelope belongs to. Bound into the signature. */
813
+ declare const EnvelopeDirection: z.ZodEnum<{
814
+ payload: "payload";
815
+ result: "result";
816
+ }>;
817
+ type EnvelopeDirection = z.infer<typeof EnvelopeDirection>;
818
+ declare const SealedEnvelope: z.ZodObject<{
819
+ ciphertext: z.ZodString;
820
+ recipientKeyId: z.ZodString;
821
+ senderKeyId: z.ZodString;
822
+ direction: z.ZodEnum<{
823
+ payload: "payload";
824
+ result: "result";
825
+ }>;
826
+ deadlineAt: z.ZodNumber;
827
+ }, z.core.$strict>;
828
+ type SealedEnvelope = z.infer<typeof SealedEnvelope>;
829
+ /** Everything the signature covers besides the plaintext itself. */
830
+ interface EnvelopeContext {
831
+ readonly jobId: string;
832
+ readonly senderKeyId: string;
833
+ readonly recipientKeyId: string;
834
+ readonly deadlineAt: number;
835
+ readonly direction: EnvelopeDirection;
836
+ }
837
+ /** Seal a plaintext to a recipient, signed by the sender's identity. */
838
+ declare function seal(input: {
839
+ plaintext: string;
840
+ senderKeys: StoredKeys;
841
+ recipientEncryptionPublic: string;
842
+ context: EnvelopeContext;
843
+ }): Promise<SealedEnvelope>;
844
+ /** Why an envelope was refused. Never distinguished to a remote caller. */
845
+ type EnvelopeFailure = "not-for-us" | "unopenable" | "malformed" | "bad-signature" | "context-mismatch";
846
+ type OpenResult = {
847
+ readonly ok: true;
848
+ readonly plaintext: string;
849
+ } | {
850
+ readonly ok: false;
851
+ readonly reason: EnvelopeFailure;
852
+ };
853
+ /**
854
+ * Open an envelope and verify it came from the pinned sender.
855
+ *
856
+ * Every failure returns rather than throws: this runs on input from the
857
+ * network, and a crash here is a denial of service on the delivery path.
858
+ *
859
+ * The context is checked against the signature, not merely read from the
860
+ * envelope. An envelope carries its own claims about who sent it and to
861
+ * whom — believing those would authenticate the attacker's assertion rather
862
+ * than the sender's key.
863
+ */
864
+ declare function open(input: {
865
+ envelope: SealedEnvelope;
866
+ recipientKeys: StoredKeys;
867
+ senderIdentityPublic: string;
868
+ /** The deadline is taken from the envelope and checked against its signature. */
869
+ expected: Omit<EnvelopeContext, "deadlineAt">;
870
+ }): Promise<OpenResult>;
871
+
872
+ /**
873
+ * Request signing — byollm_009 §4.2.
874
+ *
875
+ * Every authenticated call is signed by the calling device's identity key.
876
+ * There is no bearer token on the daemon plane: possession of a file no
877
+ * longer grants access, possession of a *key* does, and the key never leaves
878
+ * the machine.
879
+ *
880
+ * ## Why this is not the server-issued nonce the spec first described
881
+ *
882
+ * byollm_009 §4.2 says "the upstream issues a nonce; the daemon signs it".
883
+ * Implementing that costs one of two things: a round trip before every
884
+ * request, or server-side session state — and sessions reintroduce a bearer
885
+ * credential, which is the thing being removed.
886
+ *
887
+ * Signing *the request itself* gets the same property without either, because
888
+ * of something the protocol already guarantees. A captured signature is valid
889
+ * only for the exact request it covers — same endpoint, same runner, same
890
+ * body — and every authenticated endpoint here is idempotent by design:
891
+ * `RESULT_IDEMPOTENT` makes a replayed result a no-op, a replayed claim from
892
+ * the same runner returns what that runner already holds, and heartbeat and
893
+ * release are idempotent in effect. So a replay inside the freshness window
894
+ * gains an attacker nothing they could not obtain by forwarding the original,
895
+ * which a relay can do anyway.
896
+ *
897
+ * That is the whole argument, and it is worth stating because it rests
898
+ * entirely on the endpoints being idempotent. Two ways that can fail, and the
899
+ * second is the one that actually bit:
900
+ *
901
+ * 1. **A future endpoint that is not idempotent cannot use this scheme
902
+ * unchanged** — it would need a server-issued nonce.
903
+ * 2. **Idempotence must hold per *addressed instance*, not per endpoint.** A
904
+ * request that names a mutable target — a lease, a session, a
905
+ * subscription — must name the *instance*, or a replay lands on a
906
+ * different one than the sender meant and the endpoint's idempotence buys
907
+ * nothing. `release` was idempotent per lease and ambiguous across them:
908
+ * it named a job and a runner, both of which survive a
909
+ * claim-release-reclaim cycle, so a replayed release yanked a later grant.
910
+ * Fixed by giving a lease its own id and requiring it.
911
+ *
912
+ * The rule for anything added later: if a signed request can be replayed onto
913
+ * a target that has changed underneath it, the request has to say which
914
+ * target it meant.
915
+ */
916
+ /** How far a request's timestamp may be from the server's clock. */
917
+ declare const MAX_CLOCK_SKEW_MS = 120000;
918
+ /** The signed material a request carries. */
919
+ declare const RequestSignature: z.ZodObject<{
920
+ runnerId: z.ZodString;
921
+ issuedAt: z.ZodNumber;
922
+ signature: z.ZodString;
923
+ }, z.core.$strict>;
924
+ type RequestSignature = z.infer<typeof RequestSignature>;
925
+ /**
926
+ * The exact bytes both sides sign and verify.
927
+ *
928
+ * Newline-separated with a version prefix and a domain separator. Every field
929
+ * that decides what the request *does* is in here: leave one out and it
930
+ * becomes something an intermediary can change without breaking the
931
+ * signature.
932
+ *
933
+ * The body is included by hash rather than by value, so signing does not
934
+ * depend on both sides serialising JSON identically — which they would not.
935
+ */
936
+ declare function canonicalRequest(input: {
937
+ endpoint: string;
938
+ runnerId: string;
939
+ issuedAt: number;
940
+ body: string;
941
+ }): Buffer;
942
+ /** Sign an outgoing request with this machine's identity key. */
943
+ declare function signRequest(keys: StoredKeys, input: {
944
+ endpoint: string;
945
+ runnerId: string;
946
+ issuedAt: number;
947
+ body: string;
948
+ }): RequestSignature;
949
+ /**
950
+ * The same scheme, for the party at the other end: a **site** calling a relay.
951
+ *
952
+ * A site talking to a relay is in exactly the daemon's position — an outbound
953
+ * caller with an identity keypair the other side already pins — so it gets the
954
+ * daemon's authentication rather than a second scheme. Bearer tokens for the
955
+ * site plane were the alternative, and they would have reintroduced the
956
+ * credential-in-a-file that §4.2 removed from the daemon plane, on the plane
957
+ * that carries *every* site's traffic.
958
+ *
959
+ * Two things make this safe to build on the same canonical string:
960
+ *
961
+ * 1. **The endpoint is namespaced.** Site endpoints sign `site/enqueue`, never
962
+ * `enqueue`. The daemon plane's `result` and the site plane's `results` are
963
+ * one character apart, and a naming collision between planes must not be
964
+ * what stands between a signature and a replay onto the wrong handler. The
965
+ * prefix is applied *inside* these helpers, so the two ends cannot disagree
966
+ * about it — the alternative is two implementations of one bound value,
967
+ * which is this project's most-repeated bug.
968
+ * 2. **The caller slot carries the site id.** `canonicalRequest` names that
969
+ * field `runnerId` because the daemon plane got there first; here it holds
970
+ * the site id, and the verifier looks the key up in the projection's site
971
+ * registry rather than its device registry. The two registries never share
972
+ * an entry, so a device signature cannot authenticate as a site.
973
+ *
974
+ * §4.2's replay argument carries over **only because the site plane's writes
975
+ * are idempotent per addressed instance**, which is a property that had to be
976
+ * built rather than found: `enqueue` reset a job of the same id, so a replayed
977
+ * enqueue inside the freshness window returned a claimed job to the queue and
978
+ * threw away a device's live lease. Identical in shape to the `release` bug
979
+ * above, on the other plane. Anything added to the site plane later must be
980
+ * idempotent by the instance it names, or this scheme does not cover it.
981
+ */
982
+ declare function signSiteRequest(keys: StoredKeys, input: {
983
+ endpoint: string;
984
+ siteId: string;
985
+ issuedAt: number;
986
+ body: string;
987
+ }): RequestSignature;
988
+ /** Verify a site's call against the identity the control plane registered. */
989
+ declare function verifySiteRequest(input: {
990
+ identityPublic: string;
991
+ endpoint: string;
992
+ body: string;
993
+ signature: RequestSignature;
994
+ now: number;
995
+ maxSkewMs?: number;
996
+ }): SignatureFailure | null;
997
+ /** Why a signed request was refused. Never returned to the caller verbatim. */
998
+ type SignatureFailure = "stale" | "bad-signature";
999
+ /**
1000
+ * Verify a signed request against a runner's pinned identity key.
1001
+ *
1002
+ * Freshness is checked in **both** directions. A clock far ahead is as much a
1003
+ * problem as one behind: it would let a captured request stay replayable long
1004
+ * after it was made, which is the one thing the window exists to bound.
1005
+ */
1006
+ declare function verifyRequest(input: {
1007
+ identityPublic: string;
1008
+ endpoint: string;
1009
+ body: string;
1010
+ signature: RequestSignature;
1011
+ now: number;
1012
+ maxSkewMs?: number;
1013
+ }): SignatureFailure | null;
511
1014
 
512
1015
  /**
513
1016
  * The normative MUSTs of protocol v0, as data.
@@ -524,6 +1027,30 @@ type DeliveredResult = z.infer<typeof DeliveredResult>;
524
1027
  */
525
1028
  /** Which side of the wire is obliged to enforce a given MUST. */
526
1029
  type MustEnforcer = "daemon" | "server" | "both";
1030
+ /**
1031
+ * How a MUST is actually verified — which is not the same question as who
1032
+ * enforces it, and is the one that decides what "byollm-compatible" means.
1033
+ *
1034
+ * The conformance kit's credibility rests on an implicit claim that every
1035
+ * MUST is checkable. Ten of them were not, and the kit reported that honestly
1036
+ * while nothing acted on it. Making the kind explicit turns "uncovered" from
1037
+ * a number needing a paragraph of explanation into a number that should be
1038
+ * zero.
1039
+ *
1040
+ * - `conformance` — the kit asserts it against *any* implementation. This is
1041
+ * the strong kind: a third party runs the suite and learns something.
1042
+ * - `adversarial` — proved by the reference daemon's own suites in this repo
1043
+ * (the hostile-payload corpus, or its unit tests). Real verification, and
1044
+ * it runs in CI — but it proves things about *our* daemon, not about
1045
+ * someone else's, so the kit cannot carry it.
1046
+ * - `construction` — true by the shape of the code, where a test could only
1047
+ * sample. A reviewer verifies it; a suite cannot.
1048
+ * - `operator` — a claim about how someone runs a deployment, verifiable only
1049
+ * by audit or by reading source. The honest category, and the one that
1050
+ * exists so a property nobody can check from outside is *labelled* as such
1051
+ * rather than laundered by association with the checkable ones.
1052
+ */
1053
+ type MustVerification = "conformance" | "adversarial" | "construction" | "operator";
527
1054
  /** A single normative requirement of the protocol. */
528
1055
  interface Must {
529
1056
  /** Stable public id, cited by conformance output. */
@@ -532,6 +1059,11 @@ interface Must {
532
1059
  readonly statement: string;
533
1060
  /** Which implementation is obliged to enforce it. */
534
1061
  readonly enforcedBy: MustEnforcer;
1062
+ /**
1063
+ * How this is verified. `conformance` is the only kind the kit can assert;
1064
+ * see {@link MustVerification} for why the others exist.
1065
+ */
1066
+ readonly verifiedBy: MustVerification;
535
1067
  /** Spec section this was adjudicated in. */
536
1068
  readonly source: string;
537
1069
  }
@@ -547,6 +1079,12 @@ declare const MUSTS: Readonly<{
547
1079
  readonly PAIR_ONE_USER: Must;
548
1080
  readonly PAIR_INTERACTIVE: Must;
549
1081
  readonly PAIR_CODE_EXPIRES: Must;
1082
+ readonly VERSION_HANDSHAKE_REQUIRED: Must;
1083
+ readonly KEYS_EXCHANGED_AT_CONSENT: Must;
1084
+ readonly REQUESTS_SIGNED_NOT_BEARER: Must;
1085
+ readonly LEASE_SCOPED_BY_GRANT: Must;
1086
+ readonly STUB_METADATA_EXHAUSTIVE: Must;
1087
+ readonly ENVELOPE_SEALED_AND_SIGNED: Must;
550
1088
  readonly KIND_TYPED_ONLY: Must;
551
1089
  readonly KIND_NO_CODE: Must;
552
1090
  readonly CLAIM_REQUIRES_CAPABILITY: Must;
@@ -556,6 +1094,10 @@ declare const MUSTS: Readonly<{
556
1094
  readonly LEASE_RECLAIMABLE: Must;
557
1095
  readonly AUDIENCE_BOTH_SIDES: Must;
558
1096
  readonly SUBSCRIPTION_SELF_LOCK: Must;
1097
+ readonly METERED_DEFAULTS_SELF: Must;
1098
+ readonly METERED_REQUIRES_CEILING: Must;
1099
+ readonly COST_NOT_CONFIGURABLE: Must;
1100
+ readonly REMOTE_IS_NEVER_FREE: Must;
559
1101
  readonly NAMED_LOCAL_ALLOWLIST: Must;
560
1102
  readonly REFUSAL_NOT_REOFFERED: Must;
561
1103
  readonly REVOCATION_HONORED: Must;
@@ -576,14 +1118,64 @@ declare const MUSTS: Readonly<{
576
1118
  /** The id of any normative MUST. */
577
1119
  type MustId = keyof typeof MUSTS;
578
1120
  /** All MUST ids, for coverage checks. */
579
- declare const MUST_IDS: readonly ("PAIR_ONE_USER" | "PAIR_INTERACTIVE" | "PAIR_CODE_EXPIRES" | "KIND_TYPED_ONLY" | "KIND_NO_CODE" | "CLAIM_REQUIRES_CAPABILITY" | "CAPABILITY_IS_DETECTED" | "CLAIM_ATOMIC" | "LEASE_HONORED" | "LEASE_RECLAIMABLE" | "AUDIENCE_BOTH_SIDES" | "SUBSCRIPTION_SELF_LOCK" | "NAMED_LOCAL_ALLOWLIST" | "REFUSAL_NOT_REOFFERED" | "REVOCATION_HONORED" | "CANCEL_HONORED" | "DEPENDS_ON_GATING" | "TTL_EXPIRY" | "NO_RUNNER_SIGNAL" | "RESULT_IDEMPOTENT" | "RESULT_PROVENANCE" | "INGRESS_LOGGED_BEFORE_EXECUTION" | "NO_SHELL_INTERPOLATION" | "NO_PAYLOAD_ROUTING" | "STRIPPED_CHILD_ENV" | "HTTP_BASE_URL_SAFE" | "OUTPUT_INERT" | "COMMUNITY_BUDGETS")[];
1121
+ declare const MUST_IDS: readonly ("PAIR_ONE_USER" | "PAIR_INTERACTIVE" | "PAIR_CODE_EXPIRES" | "VERSION_HANDSHAKE_REQUIRED" | "KEYS_EXCHANGED_AT_CONSENT" | "REQUESTS_SIGNED_NOT_BEARER" | "LEASE_SCOPED_BY_GRANT" | "STUB_METADATA_EXHAUSTIVE" | "ENVELOPE_SEALED_AND_SIGNED" | "KIND_TYPED_ONLY" | "KIND_NO_CODE" | "CLAIM_REQUIRES_CAPABILITY" | "CAPABILITY_IS_DETECTED" | "CLAIM_ATOMIC" | "LEASE_HONORED" | "LEASE_RECLAIMABLE" | "AUDIENCE_BOTH_SIDES" | "SUBSCRIPTION_SELF_LOCK" | "METERED_DEFAULTS_SELF" | "METERED_REQUIRES_CEILING" | "COST_NOT_CONFIGURABLE" | "REMOTE_IS_NEVER_FREE" | "NAMED_LOCAL_ALLOWLIST" | "REFUSAL_NOT_REOFFERED" | "REVOCATION_HONORED" | "CANCEL_HONORED" | "DEPENDS_ON_GATING" | "TTL_EXPIRY" | "NO_RUNNER_SIGNAL" | "RESULT_IDEMPOTENT" | "RESULT_PROVENANCE" | "INGRESS_LOGGED_BEFORE_EXECUTION" | "NO_SHELL_INTERPOLATION" | "NO_PAYLOAD_ROUTING" | "STRIPPED_CHILD_ENV" | "HTTP_BASE_URL_SAFE" | "OUTPUT_INERT" | "COMMUNITY_BUDGETS")[];
1122
+ /** Every MUST verified a particular way. */
1123
+ declare function mustsVerifiedBy(kind: MustVerification): MustId[];
580
1124
 
581
1125
  /** Protocol version carried on every request; servers refuse what they can't speak. */
582
1126
  declare const PROTOCOL_VERSION: "0";
1127
+ /**
1128
+ * Every protocol version this build can serve, **oldest first**.
1129
+ *
1130
+ * One entry today. It is a list rather than a constant because the shape of
1131
+ * the check is the point: a server supporting two versions through a
1132
+ * migration should not need a different code path from one supporting one.
1133
+ */
1134
+ declare const SUPPORTED_PROTOCOL_VERSIONS: readonly string[];
1135
+ /**
1136
+ * The oldest version this build will talk to — derived, not declared.
1137
+ *
1138
+ * Stating it separately would be a second thing to keep in step with the list
1139
+ * above, and the failure would be silent: a minimum that no longer matches
1140
+ * what is supported produces a refusal naming a version the server would in
1141
+ * fact have accepted.
1142
+ */
1143
+ declare const MIN_PROTOCOL_VERSION: string;
1144
+ /** A structured refusal, so a daemon can say something useful to its owner. */
1145
+ interface VersionRefusal {
1146
+ readonly error: "unsupported-protocol-version";
1147
+ readonly message: string;
1148
+ readonly supported: readonly string[];
1149
+ readonly minimum: string;
1150
+ }
1151
+ /**
1152
+ * Check the protocol version on an incoming request
1153
+ * ({@link MUSTS.VERSION_HANDSHAKE_REQUIRED}).
1154
+ *
1155
+ * Returns a refusal, or `null` to proceed.
1156
+ *
1157
+ * **A missing version is refused the same way a wrong one is.** That is the
1158
+ * half worth stating: before this existed, the version travelled as a
1159
+ * `z.literal` inside each endpoint's schema, so a mismatch surfaced as a
1160
+ * generic `bad-request` — a daemon and a server discovered they disagreed by
1161
+ * failing, with nothing in the response naming the disagreement. An error a
1162
+ * user cannot act on is barely better than a hang.
1163
+ *
1164
+ * The message names the fix, because the person reading it is usually the one
1165
+ * who has to apply it.
1166
+ */
1167
+ declare function checkProtocolVersion(body: unknown): VersionRefusal | null;
583
1168
  /** The path prefix all endpoints mount under. */
584
1169
  declare const PROTOCOL_PREFIX: "/byollm";
585
- /** The five endpoint names, in the order byollm_001 lists them. */
586
- declare const ENDPOINTS: readonly ["pair", "claim", "heartbeat", "result", "release"];
1170
+ /**
1171
+ * The endpoint names, in the order byollm_001 lists them, plus `fetch`.
1172
+ *
1173
+ * `fetch` is byollm_009 §6's second phase: a claim returns a stub, and the
1174
+ * payload is collected separately by the device that took it. Two steps
1175
+ * rather than one because a payload can only be sealed once its recipient is
1176
+ * known — which is also what makes multi-device free.
1177
+ */
1178
+ declare const ENDPOINTS: readonly ["pair", "claim", "fetch", "heartbeat", "result", "release"];
587
1179
  type Endpoint = (typeof ENDPOINTS)[number];
588
1180
  /**
589
1181
  * One entry of the capability matrix: a kind this daemon can actually serve,
@@ -601,6 +1193,22 @@ declare const Capability: z.ZodObject<{
601
1193
  "llm.chat": "llm.chat";
602
1194
  }>;
603
1195
  backendId: z.ZodEnum<{
1196
+ ollama: "ollama";
1197
+ mlx: "mlx";
1198
+ llamacpp: "llamacpp";
1199
+ vllm: "vllm";
1200
+ lmstudio: "lmstudio";
1201
+ jan: "jan";
1202
+ localai: "localai";
1203
+ anthropic: "anthropic";
1204
+ openai: "openai";
1205
+ gemini: "gemini";
1206
+ grok: "grok";
1207
+ groq: "groq";
1208
+ openrouter: "openrouter";
1209
+ together: "together";
1210
+ deepseek: "deepseek";
1211
+ mistral: "mistral";
604
1212
  "openai-http": "openai-http";
605
1213
  "claude-cli": "claude-cli";
606
1214
  }>;
@@ -623,6 +1231,22 @@ declare const CapabilityMatrix: z.ZodArray<z.ZodObject<{
623
1231
  "llm.chat": "llm.chat";
624
1232
  }>;
625
1233
  backendId: z.ZodEnum<{
1234
+ ollama: "ollama";
1235
+ mlx: "mlx";
1236
+ llamacpp: "llamacpp";
1237
+ vllm: "vllm";
1238
+ lmstudio: "lmstudio";
1239
+ jan: "jan";
1240
+ localai: "localai";
1241
+ anthropic: "anthropic";
1242
+ openai: "openai";
1243
+ gemini: "gemini";
1244
+ grok: "grok";
1245
+ groq: "groq";
1246
+ openrouter: "openrouter";
1247
+ together: "together";
1248
+ deepseek: "deepseek";
1249
+ mistral: "mistral";
626
1250
  "openai-http": "openai-http";
627
1251
  "claude-cli": "claude-cli";
628
1252
  }>;
@@ -657,12 +1281,33 @@ declare const PairStartRequest: z.ZodObject<{
657
1281
  win32: "win32";
658
1282
  }>;
659
1283
  }, z.core.$strip>;
1284
+ device: z.ZodObject<{
1285
+ identity: z.ZodString;
1286
+ encryption: z.ZodString;
1287
+ encryptionSig: z.ZodString;
1288
+ }, z.core.$strict>;
660
1289
  capabilities: z.ZodArray<z.ZodObject<{
661
1290
  kind: z.ZodEnum<{
662
1291
  "llm.generate": "llm.generate";
663
1292
  "llm.chat": "llm.chat";
664
1293
  }>;
665
1294
  backendId: z.ZodEnum<{
1295
+ ollama: "ollama";
1296
+ mlx: "mlx";
1297
+ llamacpp: "llamacpp";
1298
+ vllm: "vllm";
1299
+ lmstudio: "lmstudio";
1300
+ jan: "jan";
1301
+ localai: "localai";
1302
+ anthropic: "anthropic";
1303
+ openai: "openai";
1304
+ gemini: "gemini";
1305
+ grok: "grok";
1306
+ groq: "groq";
1307
+ openrouter: "openrouter";
1308
+ together: "together";
1309
+ deepseek: "deepseek";
1310
+ mistral: "mistral";
666
1311
  "openai-http": "openai-http";
667
1312
  "claude-cli": "claude-cli";
668
1313
  }>;
@@ -705,6 +1350,11 @@ declare const PairPollResponse: z.ZodDiscriminatedUnion<[z.ZodObject<{
705
1350
  runnerId: z.ZodString;
706
1351
  owner: z.ZodString;
707
1352
  ownerLabel: z.ZodOptional<z.ZodString>;
1353
+ site: z.ZodObject<{
1354
+ identity: z.ZodString;
1355
+ encryption: z.ZodString;
1356
+ encryptionSig: z.ZodString;
1357
+ }, z.core.$strict>;
708
1358
  }, z.core.$strict>], "status">;
709
1359
  type PairPollResponse = z.infer<typeof PairPollResponse>;
710
1360
  declare const PairRequest: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -719,12 +1369,33 @@ declare const PairRequest: z.ZodDiscriminatedUnion<[z.ZodObject<{
719
1369
  win32: "win32";
720
1370
  }>;
721
1371
  }, z.core.$strip>;
1372
+ device: z.ZodObject<{
1373
+ identity: z.ZodString;
1374
+ encryption: z.ZodString;
1375
+ encryptionSig: z.ZodString;
1376
+ }, z.core.$strict>;
722
1377
  capabilities: z.ZodArray<z.ZodObject<{
723
1378
  kind: z.ZodEnum<{
724
1379
  "llm.generate": "llm.generate";
725
1380
  "llm.chat": "llm.chat";
726
1381
  }>;
727
1382
  backendId: z.ZodEnum<{
1383
+ ollama: "ollama";
1384
+ mlx: "mlx";
1385
+ llamacpp: "llamacpp";
1386
+ vllm: "vllm";
1387
+ lmstudio: "lmstudio";
1388
+ jan: "jan";
1389
+ localai: "localai";
1390
+ anthropic: "anthropic";
1391
+ openai: "openai";
1392
+ gemini: "gemini";
1393
+ grok: "grok";
1394
+ groq: "groq";
1395
+ openrouter: "openrouter";
1396
+ together: "together";
1397
+ deepseek: "deepseek";
1398
+ mistral: "mistral";
728
1399
  "openai-http": "openai-http";
729
1400
  "claude-cli": "claude-cli";
730
1401
  }>;
@@ -754,6 +1425,22 @@ declare const ClaimRequest: z.ZodObject<{
754
1425
  "llm.chat": "llm.chat";
755
1426
  }>;
756
1427
  backendId: z.ZodEnum<{
1428
+ ollama: "ollama";
1429
+ mlx: "mlx";
1430
+ llamacpp: "llamacpp";
1431
+ vllm: "vllm";
1432
+ lmstudio: "lmstudio";
1433
+ jan: "jan";
1434
+ localai: "localai";
1435
+ anthropic: "anthropic";
1436
+ openai: "openai";
1437
+ gemini: "gemini";
1438
+ grok: "grok";
1439
+ groq: "groq";
1440
+ openrouter: "openrouter";
1441
+ together: "together";
1442
+ deepseek: "deepseek";
1443
+ mistral: "mistral";
757
1444
  "openai-http": "openai-http";
758
1445
  "claude-cli": "claude-cli";
759
1446
  }>;
@@ -778,28 +1465,23 @@ declare const ClaimResponse: z.ZodObject<{
778
1465
  "llm.generate": "llm.generate";
779
1466
  "llm.chat": "llm.chat";
780
1467
  }>;
781
- payload: z.ZodUnion<readonly [z.ZodObject<{
782
- prompt: z.ZodString;
783
- system: z.ZodOptional<z.ZodString>;
784
- }, z.core.$strict>, z.ZodObject<{
785
- messages: z.ZodArray<z.ZodObject<{
786
- role: z.ZodEnum<{
787
- system: "system";
788
- user: "user";
789
- assistant: "assistant";
790
- }>;
791
- content: z.ZodString;
792
- }, z.core.$strip>>;
793
- system: z.ZodOptional<z.ZodString>;
794
- }, z.core.$strict>]>;
1468
+ owner: z.ZodString;
795
1469
  audience: z.ZodEnum<{
796
1470
  self: "self";
797
1471
  named: "named";
798
1472
  public: "public";
799
1473
  }>;
800
- owner: z.ZodString;
801
1474
  audienceAllow: z.ZodOptional<z.ZodArray<z.ZodString>>;
1475
+ sizeClass: z.ZodEnum<{
1476
+ small: "small";
1477
+ medium: "medium";
1478
+ large: "large";
1479
+ unbounded: "unbounded";
1480
+ }>;
1481
+ streaming: z.ZodBoolean;
1482
+ deadlineAt: z.ZodNumber;
802
1483
  lease: z.ZodObject<{
1484
+ id: z.ZodString;
803
1485
  runnerId: z.ZodString;
804
1486
  expiresAt: z.ZodNumber;
805
1487
  }, z.core.$strip>;
@@ -817,6 +1499,22 @@ declare const HeartbeatRequest: z.ZodObject<{
817
1499
  "llm.chat": "llm.chat";
818
1500
  }>;
819
1501
  backendId: z.ZodEnum<{
1502
+ ollama: "ollama";
1503
+ mlx: "mlx";
1504
+ llamacpp: "llamacpp";
1505
+ vllm: "vllm";
1506
+ lmstudio: "lmstudio";
1507
+ jan: "jan";
1508
+ localai: "localai";
1509
+ anthropic: "anthropic";
1510
+ openai: "openai";
1511
+ gemini: "gemini";
1512
+ grok: "grok";
1513
+ groq: "groq";
1514
+ openrouter: "openrouter";
1515
+ together: "together";
1516
+ deepseek: "deepseek";
1517
+ mistral: "mistral";
820
1518
  "openai-http": "openai-http";
821
1519
  "claude-cli": "claude-cli";
822
1520
  }>;
@@ -831,7 +1529,10 @@ declare const HeartbeatRequest: z.ZodObject<{
831
1529
  public: "public";
832
1530
  }>;
833
1531
  }, z.core.$strict>>;
834
- activeJobIds: z.ZodArray<z.ZodString>;
1532
+ activeLeases: z.ZodArray<z.ZodObject<{
1533
+ jobId: z.ZodString;
1534
+ leaseId: z.ZodString;
1535
+ }, z.core.$strip>>;
835
1536
  paused: z.ZodBoolean;
836
1537
  }, z.core.$strict>;
837
1538
  type HeartbeatRequest = z.infer<typeof HeartbeatRequest>;
@@ -846,22 +1547,43 @@ declare const HeartbeatResponse: z.ZodObject<{
846
1547
  serverTime: z.ZodNumber;
847
1548
  }, z.core.$strict>;
848
1549
  type HeartbeatResponse = z.infer<typeof HeartbeatResponse>;
1550
+ /**
1551
+ * What an intermediary learns about how a job ended — byollm_009 §6.
1552
+ *
1553
+ * The discriminator and nothing else. A relay has to know a job reached a
1554
+ * terminal state, and whether it failed, because that decides whether the job
1555
+ * leaves the queue or the app may re-enqueue. It does not have to know what
1556
+ * the model said, or what an error said, and this is where that line is drawn.
1557
+ *
1558
+ * Kept identical to `JobOutcome`'s discriminator rather than coarsened to
1559
+ * ok/not-ok: a cancelled job and a failed one are different routing outcomes,
1560
+ * and collapsing them would make the relay guess.
1561
+ */
1562
+ declare const ResultDisposition: z.ZodEnum<{
1563
+ ok: "ok";
1564
+ error: "error";
1565
+ canceled: "canceled";
1566
+ }>;
1567
+ type ResultDisposition = z.infer<typeof ResultDisposition>;
849
1568
  declare const ResultRequest: z.ZodObject<{
850
1569
  protocolVersion: z.ZodLiteral<"0">;
851
1570
  runnerId: z.ZodString;
852
1571
  jobId: z.ZodString;
853
- outcome: z.ZodDiscriminatedUnion<[z.ZodObject<{
854
- outcome: z.ZodLiteral<"ok">;
855
- text: z.ZodString;
856
- artifactUrl: z.ZodOptional<z.ZodURL>;
857
- }, z.core.$strict>, z.ZodObject<{
858
- outcome: z.ZodLiteral<"error">;
859
- code: z.ZodString;
860
- message: z.ZodString;
861
- retryable: z.ZodBoolean;
862
- }, z.core.$strict>, z.ZodObject<{
863
- outcome: z.ZodLiteral<"canceled">;
864
- }, z.core.$strict>], "outcome">;
1572
+ envelope: z.ZodObject<{
1573
+ ciphertext: z.ZodString;
1574
+ recipientKeyId: z.ZodString;
1575
+ senderKeyId: z.ZodString;
1576
+ direction: z.ZodEnum<{
1577
+ payload: "payload";
1578
+ result: "result";
1579
+ }>;
1580
+ deadlineAt: z.ZodNumber;
1581
+ }, z.core.$strict>;
1582
+ disposition: z.ZodEnum<{
1583
+ ok: "ok";
1584
+ error: "error";
1585
+ canceled: "canceled";
1586
+ }>;
865
1587
  model: z.ZodString;
866
1588
  backendClass: z.ZodEnum<{
867
1589
  http: "http";
@@ -878,7 +1600,10 @@ type ResultResponse = z.infer<typeof ResultResponse>;
878
1600
  declare const ReleaseRequest: z.ZodObject<{
879
1601
  protocolVersion: z.ZodLiteral<"0">;
880
1602
  runnerId: z.ZodString;
881
- jobIds: z.ZodArray<z.ZodString>;
1603
+ leases: z.ZodArray<z.ZodObject<{
1604
+ jobId: z.ZodString;
1605
+ leaseId: z.ZodString;
1606
+ }, z.core.$strip>>;
882
1607
  reason: z.ZodEnum<{
883
1608
  revoked: "revoked";
884
1609
  shutdown: "shutdown";
@@ -901,9 +1626,9 @@ type ReleaseResponse = z.infer<typeof ReleaseResponse>;
901
1626
  * with no response at all.
902
1627
  */
903
1628
  declare const WireErrorCode: z.ZodEnum<{
1629
+ "unsupported-protocol-version": "unsupported-protocol-version";
904
1630
  revoked: "revoked";
905
1631
  "bad-request": "bad-request";
906
- "unsupported-protocol-version": "unsupported-protocol-version";
907
1632
  unauthorized: "unauthorized";
908
1633
  "not-found": "not-found";
909
1634
  "rate-limited": "rate-limited";
@@ -912,9 +1637,9 @@ declare const WireErrorCode: z.ZodEnum<{
912
1637
  type WireErrorCode = z.infer<typeof WireErrorCode>;
913
1638
  declare const WireError: z.ZodObject<{
914
1639
  error: z.ZodEnum<{
1640
+ "unsupported-protocol-version": "unsupported-protocol-version";
915
1641
  revoked: "revoked";
916
1642
  "bad-request": "bad-request";
917
- "unsupported-protocol-version": "unsupported-protocol-version";
918
1643
  unauthorized: "unauthorized";
919
1644
  "not-found": "not-found";
920
1645
  "rate-limited": "rate-limited";
@@ -926,5 +1651,25 @@ declare const WireError: z.ZodObject<{
926
1651
  type WireError = z.infer<typeof WireError>;
927
1652
  /** HTTP status each error code is served with. */
928
1653
  declare const ERROR_STATUS: Readonly<Record<WireErrorCode, number>>;
1654
+ declare const FetchRequest: z.ZodObject<{
1655
+ protocolVersion: z.ZodString;
1656
+ runnerId: z.ZodString;
1657
+ jobId: z.ZodString;
1658
+ leaseId: z.ZodString;
1659
+ }, z.core.$strict>;
1660
+ type FetchRequest = z.infer<typeof FetchRequest>;
1661
+ declare const FetchResponse: z.ZodObject<{
1662
+ envelope: z.ZodObject<{
1663
+ ciphertext: z.ZodString;
1664
+ recipientKeyId: z.ZodString;
1665
+ senderKeyId: z.ZodString;
1666
+ direction: z.ZodEnum<{
1667
+ payload: "payload";
1668
+ result: "result";
1669
+ }>;
1670
+ deadlineAt: z.ZodNumber;
1671
+ }, z.core.$strict>;
1672
+ }, z.core.$strict>;
1673
+ type FetchResponse = z.infer<typeof FetchResponse>;
929
1674
 
930
- export { AUDIENCES, Audience, BACKENDS, BACKEND_IDS, BackendAccount, BackendClass, type BackendDescriptor, type BackendId, BackendIdSchema, Capability, CapabilityMatrix, ChatMessage, ChatPayload, ClaimRequest, ClaimResponse, ClaimedJob, DeliveredResult, ENDPOINTS, ERROR_STATUS, type Endpoint, GeneratePayload, HeartbeatRequest, HeartbeatResponse, JOB_KINDS, JobKind, JobOutcome, JobPayload, JobResultCanceled, JobResultError, JobResultOk, JobState, KindedPayload, Lease, MUSTS, MUST_IDS, type MatchDaemon, type MatchJob, MatchRefusal, type MatchResult, type Must, type MustEnforcer, type MustId, OFFER_SCOPES, OfferScope, PAYLOAD_LIMITS, PROTOCOL_PREFIX, PROTOCOL_VERSION, PairPollRequest, PairPollResponse, PairRequest, PairStartRequest, PairStartResponse, type PayloadFor, REFUSAL_MESSAGES, ReleaseRequest, ReleaseResponse, ResultProvenance, ResultRequest, ResultResponse, TERMINAL_STATES, WireError, WireErrorCode, backendDescriptor, canTransition, effectiveOfferScope, isBackendId, isJobKind, isTerminal, matchAudience, payloadTextLength, provenanceFor };
1675
+ export { AUDIENCES, Audience, BACKENDS, BACKEND_IDS, BackendClass, BackendCost, type BackendDescriptor, type BackendId, BackendIdSchema, Capability, CapabilityMatrix, ChatMessage, ChatPayload, ClaimRequest, ClaimResponse, ClaimedJob, ClaimedStub, DeliveredResult, ENDPOINTS, ENVELOPE_MAX_AGE_MS, ERROR_STATUS, type Endpoint, type EnvelopeContext, EnvelopeDirection, type EnvelopeFailure, FetchRequest, FetchResponse, GeneratePayload, HeartbeatRequest, HeartbeatResponse, JOB_KINDS, JobKind, JobOutcome, JobPayload, JobResultCanceled, JobResultError, JobResultOk, JobState, JobStub, KindedPayload, Lease, MAX_CLOCK_SKEW_MS, MIN_PROTOCOL_VERSION, MUSTS, MUST_IDS, type MatchDaemon, type MatchJob, MatchRefusal, type MatchResult, type Must, type MustEnforcer, type MustId, type MustVerification, OFFER_SCOPES, OfferScope, type OpenResult, PAYLOAD_LIMITS, PROTOCOL_PREFIX, PROTOCOL_VERSION, PairPollRequest, PairPollResponse, PairRequest, PairStartRequest, PairStartResponse, type PayloadFor, PublicIdentity, REFUSAL_MESSAGES, ReleaseRequest, ReleaseResponse, RequestSignature, ResultDisposition, ResultProvenance, ResultRequest, ResultResponse, SIZE_CLASS_LIMITS, SUPPORTED_PROTOCOL_VERSIONS, SealedEnvelope, type SignatureFailure, SizeClass, type SpendConsent, StoredKeys, TERMINAL_STATES, type VersionRefusal, WireError, WireErrorCode, backendDescriptor, canTransition, canonicalRequest, checkProtocolVersion, cryptoReady, effectiveOfferScope, fingerprint, generateKeys, isBackendId, isJobKind, isLocalHost, isTerminal, keyId, matchAudience, mustsVerifiedBy, open, payloadTextLength, provenanceFor, publicIdentityOf, resolveCost, seal, signRequest, signSiteRequest, signWith, sizeClassCeiling, sizeClassOf, verifyPublicIdentity, verifyRequest, verifySiteRequest, verifyWith };