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

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
@@ -448,6 +448,7 @@ declare function isTerminal(state: JobState): boolean;
448
448
  declare function canTransition(from: JobState, to: JobState): boolean;
449
449
  /** A lease: the right to work on a job until `expiresAt`. */
450
450
  declare const Lease: z.ZodObject<{
451
+ id: z.ZodString;
451
452
  runnerId: z.ZodString;
452
453
  expiresAt: z.ZodNumber;
453
454
  }, z.core.$strip>;
@@ -504,6 +505,7 @@ declare const ClaimedJob: z.ZodObject<{
504
505
  owner: z.ZodString;
505
506
  audienceAllow: z.ZodOptional<z.ZodArray<z.ZodString>>;
506
507
  lease: z.ZodObject<{
508
+ id: z.ZodString;
507
509
  runnerId: z.ZodString;
508
510
  expiresAt: z.ZodNumber;
509
511
  }, z.core.$strip>;
@@ -614,6 +616,353 @@ declare const DeliveredResult: z.ZodObject<{
614
616
  }, z.core.$strict>>;
615
617
  }, z.core.$strict>;
616
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
+ /** Why a signed request was refused. Never returned to the caller verbatim. */
950
+ type SignatureFailure = "stale" | "bad-signature";
951
+ /**
952
+ * Verify a signed request against a runner's pinned identity key.
953
+ *
954
+ * Freshness is checked in **both** directions. A clock far ahead is as much a
955
+ * problem as one behind: it would let a captured request stay replayable long
956
+ * after it was made, which is the one thing the window exists to bound.
957
+ */
958
+ declare function verifyRequest(input: {
959
+ identityPublic: string;
960
+ endpoint: string;
961
+ body: string;
962
+ signature: RequestSignature;
963
+ now: number;
964
+ maxSkewMs?: number;
965
+ }): SignatureFailure | null;
617
966
 
618
967
  /**
619
968
  * The normative MUSTs of protocol v0, as data.
@@ -630,6 +979,30 @@ type DeliveredResult = z.infer<typeof DeliveredResult>;
630
979
  */
631
980
  /** Which side of the wire is obliged to enforce a given MUST. */
632
981
  type MustEnforcer = "daemon" | "server" | "both";
982
+ /**
983
+ * How a MUST is actually verified — which is not the same question as who
984
+ * enforces it, and is the one that decides what "byollm-compatible" means.
985
+ *
986
+ * The conformance kit's credibility rests on an implicit claim that every
987
+ * MUST is checkable. Ten of them were not, and the kit reported that honestly
988
+ * while nothing acted on it. Making the kind explicit turns "uncovered" from
989
+ * a number needing a paragraph of explanation into a number that should be
990
+ * zero.
991
+ *
992
+ * - `conformance` — the kit asserts it against *any* implementation. This is
993
+ * the strong kind: a third party runs the suite and learns something.
994
+ * - `adversarial` — proved by the reference daemon's own suites in this repo
995
+ * (the hostile-payload corpus, or its unit tests). Real verification, and
996
+ * it runs in CI — but it proves things about *our* daemon, not about
997
+ * someone else's, so the kit cannot carry it.
998
+ * - `construction` — true by the shape of the code, where a test could only
999
+ * sample. A reviewer verifies it; a suite cannot.
1000
+ * - `operator` — a claim about how someone runs a deployment, verifiable only
1001
+ * by audit or by reading source. The honest category, and the one that
1002
+ * exists so a property nobody can check from outside is *labelled* as such
1003
+ * rather than laundered by association with the checkable ones.
1004
+ */
1005
+ type MustVerification = "conformance" | "adversarial" | "construction" | "operator";
633
1006
  /** A single normative requirement of the protocol. */
634
1007
  interface Must {
635
1008
  /** Stable public id, cited by conformance output. */
@@ -638,6 +1011,11 @@ interface Must {
638
1011
  readonly statement: string;
639
1012
  /** Which implementation is obliged to enforce it. */
640
1013
  readonly enforcedBy: MustEnforcer;
1014
+ /**
1015
+ * How this is verified. `conformance` is the only kind the kit can assert;
1016
+ * see {@link MustVerification} for why the others exist.
1017
+ */
1018
+ readonly verifiedBy: MustVerification;
641
1019
  /** Spec section this was adjudicated in. */
642
1020
  readonly source: string;
643
1021
  }
@@ -653,6 +1031,12 @@ declare const MUSTS: Readonly<{
653
1031
  readonly PAIR_ONE_USER: Must;
654
1032
  readonly PAIR_INTERACTIVE: Must;
655
1033
  readonly PAIR_CODE_EXPIRES: Must;
1034
+ readonly VERSION_HANDSHAKE_REQUIRED: Must;
1035
+ readonly KEYS_EXCHANGED_AT_CONSENT: Must;
1036
+ readonly REQUESTS_SIGNED_NOT_BEARER: Must;
1037
+ readonly LEASE_SCOPED_BY_GRANT: Must;
1038
+ readonly STUB_METADATA_EXHAUSTIVE: Must;
1039
+ readonly ENVELOPE_SEALED_AND_SIGNED: Must;
656
1040
  readonly KIND_TYPED_ONLY: Must;
657
1041
  readonly KIND_NO_CODE: Must;
658
1042
  readonly CLAIM_REQUIRES_CAPABILITY: Must;
@@ -686,14 +1070,64 @@ declare const MUSTS: Readonly<{
686
1070
  /** The id of any normative MUST. */
687
1071
  type MustId = keyof typeof MUSTS;
688
1072
  /** All MUST ids, for coverage checks. */
689
- 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" | "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")[];
1073
+ 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")[];
1074
+ /** Every MUST verified a particular way. */
1075
+ declare function mustsVerifiedBy(kind: MustVerification): MustId[];
690
1076
 
691
1077
  /** Protocol version carried on every request; servers refuse what they can't speak. */
692
1078
  declare const PROTOCOL_VERSION: "0";
1079
+ /**
1080
+ * Every protocol version this build can serve, **oldest first**.
1081
+ *
1082
+ * One entry today. It is a list rather than a constant because the shape of
1083
+ * the check is the point: a server supporting two versions through a
1084
+ * migration should not need a different code path from one supporting one.
1085
+ */
1086
+ declare const SUPPORTED_PROTOCOL_VERSIONS: readonly string[];
1087
+ /**
1088
+ * The oldest version this build will talk to — derived, not declared.
1089
+ *
1090
+ * Stating it separately would be a second thing to keep in step with the list
1091
+ * above, and the failure would be silent: a minimum that no longer matches
1092
+ * what is supported produces a refusal naming a version the server would in
1093
+ * fact have accepted.
1094
+ */
1095
+ declare const MIN_PROTOCOL_VERSION: string;
1096
+ /** A structured refusal, so a daemon can say something useful to its owner. */
1097
+ interface VersionRefusal {
1098
+ readonly error: "unsupported-protocol-version";
1099
+ readonly message: string;
1100
+ readonly supported: readonly string[];
1101
+ readonly minimum: string;
1102
+ }
1103
+ /**
1104
+ * Check the protocol version on an incoming request
1105
+ * ({@link MUSTS.VERSION_HANDSHAKE_REQUIRED}).
1106
+ *
1107
+ * Returns a refusal, or `null` to proceed.
1108
+ *
1109
+ * **A missing version is refused the same way a wrong one is.** That is the
1110
+ * half worth stating: before this existed, the version travelled as a
1111
+ * `z.literal` inside each endpoint's schema, so a mismatch surfaced as a
1112
+ * generic `bad-request` — a daemon and a server discovered they disagreed by
1113
+ * failing, with nothing in the response naming the disagreement. An error a
1114
+ * user cannot act on is barely better than a hang.
1115
+ *
1116
+ * The message names the fix, because the person reading it is usually the one
1117
+ * who has to apply it.
1118
+ */
1119
+ declare function checkProtocolVersion(body: unknown): VersionRefusal | null;
693
1120
  /** The path prefix all endpoints mount under. */
694
1121
  declare const PROTOCOL_PREFIX: "/byollm";
695
- /** The five endpoint names, in the order byollm_001 lists them. */
696
- declare const ENDPOINTS: readonly ["pair", "claim", "heartbeat", "result", "release"];
1122
+ /**
1123
+ * The endpoint names, in the order byollm_001 lists them, plus `fetch`.
1124
+ *
1125
+ * `fetch` is byollm_009 §6's second phase: a claim returns a stub, and the
1126
+ * payload is collected separately by the device that took it. Two steps
1127
+ * rather than one because a payload can only be sealed once its recipient is
1128
+ * known — which is also what makes multi-device free.
1129
+ */
1130
+ declare const ENDPOINTS: readonly ["pair", "claim", "fetch", "heartbeat", "result", "release"];
697
1131
  type Endpoint = (typeof ENDPOINTS)[number];
698
1132
  /**
699
1133
  * One entry of the capability matrix: a kind this daemon can actually serve,
@@ -799,6 +1233,11 @@ declare const PairStartRequest: z.ZodObject<{
799
1233
  win32: "win32";
800
1234
  }>;
801
1235
  }, z.core.$strip>;
1236
+ device: z.ZodObject<{
1237
+ identity: z.ZodString;
1238
+ encryption: z.ZodString;
1239
+ encryptionSig: z.ZodString;
1240
+ }, z.core.$strict>;
802
1241
  capabilities: z.ZodArray<z.ZodObject<{
803
1242
  kind: z.ZodEnum<{
804
1243
  "llm.generate": "llm.generate";
@@ -863,6 +1302,11 @@ declare const PairPollResponse: z.ZodDiscriminatedUnion<[z.ZodObject<{
863
1302
  runnerId: z.ZodString;
864
1303
  owner: z.ZodString;
865
1304
  ownerLabel: z.ZodOptional<z.ZodString>;
1305
+ site: z.ZodObject<{
1306
+ identity: z.ZodString;
1307
+ encryption: z.ZodString;
1308
+ encryptionSig: z.ZodString;
1309
+ }, z.core.$strict>;
866
1310
  }, z.core.$strict>], "status">;
867
1311
  type PairPollResponse = z.infer<typeof PairPollResponse>;
868
1312
  declare const PairRequest: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -877,6 +1321,11 @@ declare const PairRequest: z.ZodDiscriminatedUnion<[z.ZodObject<{
877
1321
  win32: "win32";
878
1322
  }>;
879
1323
  }, z.core.$strip>;
1324
+ device: z.ZodObject<{
1325
+ identity: z.ZodString;
1326
+ encryption: z.ZodString;
1327
+ encryptionSig: z.ZodString;
1328
+ }, z.core.$strict>;
880
1329
  capabilities: z.ZodArray<z.ZodObject<{
881
1330
  kind: z.ZodEnum<{
882
1331
  "llm.generate": "llm.generate";
@@ -968,28 +1417,23 @@ declare const ClaimResponse: z.ZodObject<{
968
1417
  "llm.generate": "llm.generate";
969
1418
  "llm.chat": "llm.chat";
970
1419
  }>;
971
- payload: z.ZodUnion<readonly [z.ZodObject<{
972
- prompt: z.ZodString;
973
- system: z.ZodOptional<z.ZodString>;
974
- }, z.core.$strict>, z.ZodObject<{
975
- messages: z.ZodArray<z.ZodObject<{
976
- role: z.ZodEnum<{
977
- system: "system";
978
- user: "user";
979
- assistant: "assistant";
980
- }>;
981
- content: z.ZodString;
982
- }, z.core.$strip>>;
983
- system: z.ZodOptional<z.ZodString>;
984
- }, z.core.$strict>]>;
1420
+ owner: z.ZodString;
985
1421
  audience: z.ZodEnum<{
986
1422
  self: "self";
987
1423
  named: "named";
988
1424
  public: "public";
989
1425
  }>;
990
- owner: z.ZodString;
991
1426
  audienceAllow: z.ZodOptional<z.ZodArray<z.ZodString>>;
1427
+ sizeClass: z.ZodEnum<{
1428
+ small: "small";
1429
+ medium: "medium";
1430
+ large: "large";
1431
+ unbounded: "unbounded";
1432
+ }>;
1433
+ streaming: z.ZodBoolean;
1434
+ deadlineAt: z.ZodNumber;
992
1435
  lease: z.ZodObject<{
1436
+ id: z.ZodString;
993
1437
  runnerId: z.ZodString;
994
1438
  expiresAt: z.ZodNumber;
995
1439
  }, z.core.$strip>;
@@ -1037,7 +1481,10 @@ declare const HeartbeatRequest: z.ZodObject<{
1037
1481
  public: "public";
1038
1482
  }>;
1039
1483
  }, z.core.$strict>>;
1040
- activeJobIds: z.ZodArray<z.ZodString>;
1484
+ activeLeases: z.ZodArray<z.ZodObject<{
1485
+ jobId: z.ZodString;
1486
+ leaseId: z.ZodString;
1487
+ }, z.core.$strip>>;
1041
1488
  paused: z.ZodBoolean;
1042
1489
  }, z.core.$strict>;
1043
1490
  type HeartbeatRequest = z.infer<typeof HeartbeatRequest>;
@@ -1052,22 +1499,43 @@ declare const HeartbeatResponse: z.ZodObject<{
1052
1499
  serverTime: z.ZodNumber;
1053
1500
  }, z.core.$strict>;
1054
1501
  type HeartbeatResponse = z.infer<typeof HeartbeatResponse>;
1502
+ /**
1503
+ * What an intermediary learns about how a job ended — byollm_009 §6.
1504
+ *
1505
+ * The discriminator and nothing else. A relay has to know a job reached a
1506
+ * terminal state, and whether it failed, because that decides whether the job
1507
+ * leaves the queue or the app may re-enqueue. It does not have to know what
1508
+ * the model said, or what an error said, and this is where that line is drawn.
1509
+ *
1510
+ * Kept identical to `JobOutcome`'s discriminator rather than coarsened to
1511
+ * ok/not-ok: a cancelled job and a failed one are different routing outcomes,
1512
+ * and collapsing them would make the relay guess.
1513
+ */
1514
+ declare const ResultDisposition: z.ZodEnum<{
1515
+ ok: "ok";
1516
+ error: "error";
1517
+ canceled: "canceled";
1518
+ }>;
1519
+ type ResultDisposition = z.infer<typeof ResultDisposition>;
1055
1520
  declare const ResultRequest: z.ZodObject<{
1056
1521
  protocolVersion: z.ZodLiteral<"0">;
1057
1522
  runnerId: z.ZodString;
1058
1523
  jobId: z.ZodString;
1059
- outcome: z.ZodDiscriminatedUnion<[z.ZodObject<{
1060
- outcome: z.ZodLiteral<"ok">;
1061
- text: z.ZodString;
1062
- artifactUrl: z.ZodOptional<z.ZodURL>;
1063
- }, z.core.$strict>, z.ZodObject<{
1064
- outcome: z.ZodLiteral<"error">;
1065
- code: z.ZodString;
1066
- message: z.ZodString;
1067
- retryable: z.ZodBoolean;
1068
- }, z.core.$strict>, z.ZodObject<{
1069
- outcome: z.ZodLiteral<"canceled">;
1070
- }, z.core.$strict>], "outcome">;
1524
+ envelope: z.ZodObject<{
1525
+ ciphertext: z.ZodString;
1526
+ recipientKeyId: z.ZodString;
1527
+ senderKeyId: z.ZodString;
1528
+ direction: z.ZodEnum<{
1529
+ payload: "payload";
1530
+ result: "result";
1531
+ }>;
1532
+ deadlineAt: z.ZodNumber;
1533
+ }, z.core.$strict>;
1534
+ disposition: z.ZodEnum<{
1535
+ ok: "ok";
1536
+ error: "error";
1537
+ canceled: "canceled";
1538
+ }>;
1071
1539
  model: z.ZodString;
1072
1540
  backendClass: z.ZodEnum<{
1073
1541
  http: "http";
@@ -1084,7 +1552,10 @@ type ResultResponse = z.infer<typeof ResultResponse>;
1084
1552
  declare const ReleaseRequest: z.ZodObject<{
1085
1553
  protocolVersion: z.ZodLiteral<"0">;
1086
1554
  runnerId: z.ZodString;
1087
- jobIds: z.ZodArray<z.ZodString>;
1555
+ leases: z.ZodArray<z.ZodObject<{
1556
+ jobId: z.ZodString;
1557
+ leaseId: z.ZodString;
1558
+ }, z.core.$strip>>;
1088
1559
  reason: z.ZodEnum<{
1089
1560
  revoked: "revoked";
1090
1561
  shutdown: "shutdown";
@@ -1107,9 +1578,9 @@ type ReleaseResponse = z.infer<typeof ReleaseResponse>;
1107
1578
  * with no response at all.
1108
1579
  */
1109
1580
  declare const WireErrorCode: z.ZodEnum<{
1581
+ "unsupported-protocol-version": "unsupported-protocol-version";
1110
1582
  revoked: "revoked";
1111
1583
  "bad-request": "bad-request";
1112
- "unsupported-protocol-version": "unsupported-protocol-version";
1113
1584
  unauthorized: "unauthorized";
1114
1585
  "not-found": "not-found";
1115
1586
  "rate-limited": "rate-limited";
@@ -1118,9 +1589,9 @@ declare const WireErrorCode: z.ZodEnum<{
1118
1589
  type WireErrorCode = z.infer<typeof WireErrorCode>;
1119
1590
  declare const WireError: z.ZodObject<{
1120
1591
  error: z.ZodEnum<{
1592
+ "unsupported-protocol-version": "unsupported-protocol-version";
1121
1593
  revoked: "revoked";
1122
1594
  "bad-request": "bad-request";
1123
- "unsupported-protocol-version": "unsupported-protocol-version";
1124
1595
  unauthorized: "unauthorized";
1125
1596
  "not-found": "not-found";
1126
1597
  "rate-limited": "rate-limited";
@@ -1132,5 +1603,25 @@ declare const WireError: z.ZodObject<{
1132
1603
  type WireError = z.infer<typeof WireError>;
1133
1604
  /** HTTP status each error code is served with. */
1134
1605
  declare const ERROR_STATUS: Readonly<Record<WireErrorCode, number>>;
1606
+ declare const FetchRequest: z.ZodObject<{
1607
+ protocolVersion: z.ZodString;
1608
+ runnerId: z.ZodString;
1609
+ jobId: z.ZodString;
1610
+ leaseId: z.ZodString;
1611
+ }, z.core.$strict>;
1612
+ type FetchRequest = z.infer<typeof FetchRequest>;
1613
+ declare const FetchResponse: z.ZodObject<{
1614
+ envelope: z.ZodObject<{
1615
+ ciphertext: z.ZodString;
1616
+ recipientKeyId: z.ZodString;
1617
+ senderKeyId: z.ZodString;
1618
+ direction: z.ZodEnum<{
1619
+ payload: "payload";
1620
+ result: "result";
1621
+ }>;
1622
+ deadlineAt: z.ZodNumber;
1623
+ }, z.core.$strict>;
1624
+ }, z.core.$strict>;
1625
+ type FetchResponse = z.infer<typeof FetchResponse>;
1135
1626
 
1136
- export { AUDIENCES, Audience, BACKENDS, BACKEND_IDS, BackendClass, BackendCost, 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, type SpendConsent, TERMINAL_STATES, WireError, WireErrorCode, backendDescriptor, canTransition, effectiveOfferScope, isBackendId, isJobKind, isLocalHost, isTerminal, matchAudience, payloadTextLength, provenanceFor, resolveCost };
1627
+ 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, signWith, sizeClassCeiling, sizeClassOf, verifyPublicIdentity, verifyRequest, verifyWith };