@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/README.md +1 -1
- package/dist/index.d.ts +526 -35
- package/dist/index.js +629 -77
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -350,6 +350,21 @@ function canTransition(from, to) {
|
|
|
350
350
|
return TRANSITIONS[from].includes(to);
|
|
351
351
|
}
|
|
352
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),
|
|
353
368
|
/** The runner holding the lease. */
|
|
354
369
|
runnerId: z4.string().min(1),
|
|
355
370
|
/** Epoch milliseconds after which the claim is void. */
|
|
@@ -421,6 +436,333 @@ var DeliveredResult = z4.object({
|
|
|
421
436
|
outcome: JobOutcome.optional(),
|
|
422
437
|
provenance: ResultProvenance.optional()
|
|
423
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
|
+
}
|
|
424
766
|
|
|
425
767
|
// src/musts.ts
|
|
426
768
|
var must = (m) => Object.freeze(m);
|
|
@@ -430,31 +772,78 @@ var MUSTS = Object.freeze({
|
|
|
430
772
|
id: "PAIR_ONE_USER",
|
|
431
773
|
statement: "A runner token MUST be bound to exactly one user; a daemon MUST refuse work not attributable to its paired user.",
|
|
432
774
|
enforcedBy: "both",
|
|
775
|
+
verifiedBy: "conformance",
|
|
433
776
|
source: "byollm_001 \xA7MUSTs"
|
|
434
777
|
}),
|
|
435
778
|
PAIR_INTERACTIVE: must({
|
|
436
779
|
id: "PAIR_INTERACTIVE",
|
|
437
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.",
|
|
438
781
|
enforcedBy: "server",
|
|
782
|
+
verifiedBy: "conformance",
|
|
439
783
|
source: "byollm_001 \xA7Endpoints.1"
|
|
440
784
|
}),
|
|
441
785
|
PAIR_CODE_EXPIRES: must({
|
|
442
786
|
id: "PAIR_CODE_EXPIRES",
|
|
443
787
|
statement: "An unapproved device code MUST expire and MUST NOT be redeemable after expiry.",
|
|
444
788
|
enforcedBy: "server",
|
|
789
|
+
verifiedBy: "conformance",
|
|
445
790
|
source: "byollm_001 \xA7Endpoints.1"
|
|
446
791
|
}),
|
|
447
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
|
+
}),
|
|
448
835
|
KIND_TYPED_ONLY: must({
|
|
449
836
|
id: "KIND_TYPED_ONLY",
|
|
450
837
|
statement: "Job kinds MUST resolve against handlers baked into the daemon. A daemon MUST refuse an unknown kind rather than guess.",
|
|
451
838
|
enforcedBy: "daemon",
|
|
839
|
+
verifiedBy: "conformance",
|
|
452
840
|
source: "byollm_001 \xA7Jobs are typed data"
|
|
453
841
|
}),
|
|
454
842
|
KIND_NO_CODE: must({
|
|
455
843
|
id: "KIND_NO_CODE",
|
|
456
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.",
|
|
457
845
|
enforcedBy: "daemon",
|
|
846
|
+
verifiedBy: "conformance",
|
|
458
847
|
source: "byollm_001 \xA7Jobs are typed data; byollm_004 \xA71"
|
|
459
848
|
}),
|
|
460
849
|
// ---- Capability and claiming -----------------------------------------
|
|
@@ -462,18 +851,21 @@ var MUSTS = Object.freeze({
|
|
|
462
851
|
id: "CLAIM_REQUIRES_CAPABILITY",
|
|
463
852
|
statement: "A daemon MUST NOT be given a job whose kind is absent from its advertised capability matrix.",
|
|
464
853
|
enforcedBy: "both",
|
|
854
|
+
verifiedBy: "conformance",
|
|
465
855
|
source: "byollm_001 \xA7MUSTs"
|
|
466
856
|
}),
|
|
467
857
|
CAPABILITY_IS_DETECTED: must({
|
|
468
858
|
id: "CAPABILITY_IS_DETECTED",
|
|
469
859
|
statement: "An advertised capability matrix MUST be the intersection of owner config and detected, healthy reality \u2014 never config alone.",
|
|
470
860
|
enforcedBy: "daemon",
|
|
861
|
+
verifiedBy: "conformance",
|
|
471
862
|
source: "byollm_002 \xA7Routing"
|
|
472
863
|
}),
|
|
473
864
|
CLAIM_ATOMIC: must({
|
|
474
865
|
id: "CLAIM_ATOMIC",
|
|
475
866
|
statement: "Claiming MUST be atomic: a job MUST NOT be handed to two runners concurrently.",
|
|
476
867
|
enforcedBy: "server",
|
|
868
|
+
verifiedBy: "conformance",
|
|
477
869
|
source: "byollm_001 \xA7Endpoints.2"
|
|
478
870
|
}),
|
|
479
871
|
// ---- Leases -----------------------------------------------------------
|
|
@@ -481,12 +873,14 @@ var MUSTS = Object.freeze({
|
|
|
481
873
|
id: "LEASE_HONORED",
|
|
482
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.",
|
|
483
875
|
enforcedBy: "daemon",
|
|
876
|
+
verifiedBy: "conformance",
|
|
484
877
|
source: "byollm_001 \xA7MUSTs"
|
|
485
878
|
}),
|
|
486
879
|
LEASE_RECLAIMABLE: must({
|
|
487
880
|
id: "LEASE_RECLAIMABLE",
|
|
488
881
|
statement: "A lease that expires un-renewed MUST make its job claimable again with no loss of the job.",
|
|
489
882
|
enforcedBy: "server",
|
|
883
|
+
verifiedBy: "conformance",
|
|
490
884
|
source: "byollm_001 \xA7Endpoints.2"
|
|
491
885
|
}),
|
|
492
886
|
// ---- Audience and offer scope ----------------------------------------
|
|
@@ -494,48 +888,56 @@ var MUSTS = Object.freeze({
|
|
|
494
888
|
id: "AUDIENCE_BOTH_SIDES",
|
|
495
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.",
|
|
496
890
|
enforcedBy: "both",
|
|
891
|
+
verifiedBy: "conformance",
|
|
497
892
|
source: "byollm_001 \xA7The audience model"
|
|
498
893
|
}),
|
|
499
894
|
SUBSCRIPTION_SELF_LOCK: must({
|
|
500
895
|
id: "SUBSCRIPTION_SELF_LOCK",
|
|
501
896
|
statement: "A subscription-class backend's offer scope MUST be 'self' and MUST NOT be widened by configuration.",
|
|
502
897
|
enforcedBy: "daemon",
|
|
898
|
+
verifiedBy: "conformance",
|
|
503
899
|
source: "byollm_001 \xA7The audience model"
|
|
504
900
|
}),
|
|
505
901
|
METERED_DEFAULTS_SELF: must({
|
|
506
902
|
id: "METERED_DEFAULTS_SELF",
|
|
507
903
|
statement: "A metered backend's effective offer scope MUST be 'self' unless the owner has explicitly acknowledged spending money on others' work.",
|
|
508
904
|
enforcedBy: "daemon",
|
|
905
|
+
verifiedBy: "conformance",
|
|
509
906
|
source: "byollm_007 \xA74"
|
|
510
907
|
}),
|
|
511
908
|
METERED_REQUIRES_CEILING: must({
|
|
512
909
|
id: "METERED_REQUIRES_CEILING",
|
|
513
910
|
statement: "A widened metered backend MUST carry a spend ceiling, and the daemon MUST refuse community work once it is reached.",
|
|
514
911
|
enforcedBy: "daemon",
|
|
912
|
+
verifiedBy: "conformance",
|
|
515
913
|
source: "byollm_007 \xA74"
|
|
516
914
|
}),
|
|
517
915
|
COST_NOT_CONFIGURABLE: must({
|
|
518
916
|
id: "COST_NOT_CONFIGURABLE",
|
|
519
917
|
statement: "A built-in provider's cost class MUST NOT be overridable by configuration.",
|
|
520
918
|
enforcedBy: "daemon",
|
|
919
|
+
verifiedBy: "conformance",
|
|
521
920
|
source: "byollm_007 \xA72"
|
|
522
921
|
}),
|
|
523
922
|
REMOTE_IS_NEVER_FREE: must({
|
|
524
923
|
id: "REMOTE_IS_NEVER_FREE",
|
|
525
924
|
statement: "A generic HTTP backend whose base URL is not loopback or private MUST be treated as metered.",
|
|
526
925
|
enforcedBy: "daemon",
|
|
926
|
+
verifiedBy: "conformance",
|
|
527
927
|
source: "byollm_007 \xA72"
|
|
528
928
|
}),
|
|
529
929
|
NAMED_LOCAL_ALLOWLIST: must({
|
|
530
930
|
id: "NAMED_LOCAL_ALLOWLIST",
|
|
531
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.",
|
|
532
932
|
enforcedBy: "daemon",
|
|
933
|
+
verifiedBy: "conformance",
|
|
533
934
|
source: "byollm_001 Rev 1 \xA7B"
|
|
534
935
|
}),
|
|
535
936
|
REFUSAL_NOT_REOFFERED: must({
|
|
536
937
|
id: "REFUSAL_NOT_REOFFERED",
|
|
537
938
|
statement: "A server MUST NOT re-offer a job to a runner that released it with reason 'refused'.",
|
|
538
939
|
enforcedBy: "server",
|
|
940
|
+
verifiedBy: "conformance",
|
|
539
941
|
source: "byollm_001 Rev 1 \xA7B (loop resolved in build review)"
|
|
540
942
|
}),
|
|
541
943
|
// ---- Revocation and cancel -------------------------------------------
|
|
@@ -543,12 +945,14 @@ var MUSTS = Object.freeze({
|
|
|
543
945
|
id: "REVOCATION_HONORED",
|
|
544
946
|
statement: "A revoked daemon MUST stop claiming and MUST abandon in-flight work by the next heartbeat at the latest.",
|
|
545
947
|
enforcedBy: "daemon",
|
|
948
|
+
verifiedBy: "conformance",
|
|
546
949
|
source: "byollm_001 \xA7MUSTs"
|
|
547
950
|
}),
|
|
548
951
|
CANCEL_HONORED: must({
|
|
549
952
|
id: "CANCEL_HONORED",
|
|
550
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'.",
|
|
551
954
|
enforcedBy: "daemon",
|
|
955
|
+
verifiedBy: "conformance",
|
|
552
956
|
source: "byollm_001 Rev 1 \xA7C"
|
|
553
957
|
}),
|
|
554
958
|
// ---- Lifecycle, dependencies, delivery -------------------------------
|
|
@@ -556,30 +960,35 @@ var MUSTS = Object.freeze({
|
|
|
556
960
|
id: "DEPENDS_ON_GATING",
|
|
557
961
|
statement: "A job MUST NOT be claimable until every job in its dependsOn set has reached the 'ok' state.",
|
|
558
962
|
enforcedBy: "server",
|
|
963
|
+
verifiedBy: "conformance",
|
|
559
964
|
source: "byollm_001 Rev 1 \xA7E"
|
|
560
965
|
}),
|
|
561
966
|
TTL_EXPIRY: must({
|
|
562
967
|
id: "TTL_EXPIRY",
|
|
563
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.",
|
|
564
969
|
enforcedBy: "server",
|
|
970
|
+
verifiedBy: "conformance",
|
|
565
971
|
source: "byollm_001 Rev 1 \xA7D (TTL clock resolved in build review)"
|
|
566
972
|
}),
|
|
567
973
|
NO_RUNNER_SIGNAL: must({
|
|
568
974
|
id: "NO_RUNNER_SIGNAL",
|
|
569
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.",
|
|
570
976
|
enforcedBy: "server",
|
|
977
|
+
verifiedBy: "conformance",
|
|
571
978
|
source: "byollm_001 Rev 1 \xA7D"
|
|
572
979
|
}),
|
|
573
980
|
RESULT_IDEMPOTENT: must({
|
|
574
981
|
id: "RESULT_IDEMPOTENT",
|
|
575
982
|
statement: "Result submission MUST be idempotent by job id; the first terminal outcome wins and later submissions MUST NOT change it.",
|
|
576
983
|
enforcedBy: "server",
|
|
984
|
+
verifiedBy: "conformance",
|
|
577
985
|
source: "byollm_001 \xA7Endpoints.4"
|
|
578
986
|
}),
|
|
579
987
|
RESULT_PROVENANCE: must({
|
|
580
988
|
id: "RESULT_PROVENANCE",
|
|
581
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.",
|
|
582
990
|
enforcedBy: "server",
|
|
991
|
+
verifiedBy: "conformance",
|
|
583
992
|
source: "byollm_003 Rev 1 \xA7Return-trip"
|
|
584
993
|
}),
|
|
585
994
|
// ---- The trust surface -------------------------------------------------
|
|
@@ -587,6 +996,7 @@ var MUSTS = Object.freeze({
|
|
|
587
996
|
id: "INGRESS_LOGGED_BEFORE_EXECUTION",
|
|
588
997
|
statement: "Every executed prompt MUST be appended to the local ingress log before execution begins.",
|
|
589
998
|
enforcedBy: "daemon",
|
|
999
|
+
verifiedBy: "conformance",
|
|
590
1000
|
source: "byollm_001 \xA7MUSTs"
|
|
591
1001
|
}),
|
|
592
1002
|
// ---- Execution isolation (byollm_004) ---------------------------------
|
|
@@ -594,178 +1004,263 @@ var MUSTS = Object.freeze({
|
|
|
594
1004
|
id: "NO_SHELL_INTERPOLATION",
|
|
595
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.",
|
|
596
1006
|
enforcedBy: "daemon",
|
|
1007
|
+
verifiedBy: "adversarial",
|
|
597
1008
|
source: "byollm_004 \xA72"
|
|
598
1009
|
}),
|
|
599
1010
|
NO_PAYLOAD_ROUTING: must({
|
|
600
1011
|
id: "NO_PAYLOAD_ROUTING",
|
|
601
1012
|
statement: "Model, backend, base URL, and flags MUST come from owner config only; a payload MUST NOT influence any of them.",
|
|
602
1013
|
enforcedBy: "daemon",
|
|
1014
|
+
verifiedBy: "adversarial",
|
|
603
1015
|
source: "byollm_004 \xA72"
|
|
604
1016
|
}),
|
|
605
1017
|
STRIPPED_CHILD_ENV: must({
|
|
606
1018
|
id: "STRIPPED_CHILD_ENV",
|
|
607
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.",
|
|
608
1020
|
enforcedBy: "daemon",
|
|
1021
|
+
verifiedBy: "adversarial",
|
|
609
1022
|
source: "byollm_004 \xA72"
|
|
610
1023
|
}),
|
|
611
1024
|
HTTP_BASE_URL_SAFE: must({
|
|
612
1025
|
id: "HTTP_BASE_URL_SAFE",
|
|
613
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.",
|
|
614
1027
|
enforcedBy: "daemon",
|
|
1028
|
+
verifiedBy: "adversarial",
|
|
615
1029
|
source: "byollm_004 Rev 1 \xA7Backend taxonomy"
|
|
616
1030
|
}),
|
|
617
1031
|
OUTPUT_INERT: must({
|
|
618
1032
|
id: "OUTPUT_INERT",
|
|
619
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.",
|
|
620
1034
|
enforcedBy: "daemon",
|
|
1035
|
+
verifiedBy: "adversarial",
|
|
621
1036
|
source: "byollm_004 \xA72"
|
|
622
1037
|
}),
|
|
623
1038
|
COMMUNITY_BUDGETS: must({
|
|
624
1039
|
id: "COMMUNITY_BUDGETS",
|
|
625
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.",
|
|
626
1041
|
enforcedBy: "daemon",
|
|
1042
|
+
verifiedBy: "adversarial",
|
|
627
1043
|
source: "byollm_004 \xA74"
|
|
628
1044
|
})
|
|
629
1045
|
});
|
|
630
1046
|
var MUST_IDS = Object.freeze(Object.keys(MUSTS));
|
|
1047
|
+
function mustsVerifiedBy(kind) {
|
|
1048
|
+
return MUST_IDS.filter((id) => MUSTS[id].verifiedBy === kind);
|
|
1049
|
+
}
|
|
631
1050
|
|
|
632
1051
|
// src/wire.ts
|
|
633
|
-
import { z as
|
|
1052
|
+
import { z as z8 } from "zod";
|
|
634
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
|
+
}
|
|
635
1078
|
var PROTOCOL_PREFIX = "/byollm";
|
|
636
1079
|
var ENDPOINTS = Object.freeze([
|
|
637
1080
|
"pair",
|
|
638
1081
|
"claim",
|
|
1082
|
+
"fetch",
|
|
639
1083
|
"heartbeat",
|
|
640
1084
|
"result",
|
|
641
1085
|
"release"
|
|
642
1086
|
]);
|
|
643
|
-
var Capability =
|
|
1087
|
+
var Capability = z8.object({
|
|
644
1088
|
kind: JobKind,
|
|
645
1089
|
backendId: BackendIdSchema,
|
|
646
1090
|
backendClass: BackendClass,
|
|
647
|
-
model:
|
|
1091
|
+
model: z8.string().min(1),
|
|
648
1092
|
offerScope: OfferScope
|
|
649
1093
|
}).strict();
|
|
650
|
-
var CapabilityMatrix =
|
|
651
|
-
var PairStartRequest =
|
|
652
|
-
protocolVersion:
|
|
653
|
-
action:
|
|
654
|
-
daemon:
|
|
655
|
-
version:
|
|
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),
|
|
656
1100
|
/** Shown in the app's runner list so a user can tell their machines apart. */
|
|
657
|
-
label:
|
|
658
|
-
platform:
|
|
1101
|
+
label: z8.string().min(1).max(120),
|
|
1102
|
+
platform: z8.enum(["darwin", "linux", "win32"])
|
|
659
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,
|
|
660
1112
|
capabilities: CapabilityMatrix
|
|
661
1113
|
}).strict();
|
|
662
|
-
var PairStartResponse =
|
|
1114
|
+
var PairStartResponse = z8.object({
|
|
663
1115
|
/** Secret the daemon polls with. Never shown to the user. */
|
|
664
|
-
deviceCode:
|
|
1116
|
+
deviceCode: z8.string().min(20),
|
|
665
1117
|
/** Short code the user reads and confirms in the browser. */
|
|
666
|
-
userCode:
|
|
1118
|
+
userCode: z8.string().min(4).max(16),
|
|
667
1119
|
/** Where the user approves. Must be on the server's own origin. */
|
|
668
|
-
verificationUrl:
|
|
1120
|
+
verificationUrl: z8.url(),
|
|
669
1121
|
/** Epoch ms after which the code is dead ({@link MUSTS.PAIR_CODE_EXPIRES}). */
|
|
670
|
-
expiresAt:
|
|
1122
|
+
expiresAt: z8.number().int().positive(),
|
|
671
1123
|
/** How often the daemon may poll. */
|
|
672
|
-
pollIntervalMs:
|
|
1124
|
+
pollIntervalMs: z8.number().int().min(500).max(6e4)
|
|
673
1125
|
}).strict();
|
|
674
|
-
var PairPollRequest =
|
|
675
|
-
protocolVersion:
|
|
676
|
-
action:
|
|
677
|
-
deviceCode:
|
|
1126
|
+
var PairPollRequest = z8.object({
|
|
1127
|
+
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1128
|
+
action: z8.literal("poll"),
|
|
1129
|
+
deviceCode: z8.string().min(20)
|
|
678
1130
|
}).strict();
|
|
679
|
-
var PairPollResponse =
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
status:
|
|
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"),
|
|
685
1137
|
/** Bearer token for every later call. Scoped to exactly one user. */
|
|
686
|
-
runnerToken:
|
|
687
|
-
runnerId:
|
|
1138
|
+
runnerToken: z8.string().min(20),
|
|
1139
|
+
runnerId: z8.string().min(1),
|
|
688
1140
|
/** The app's id for the approving user — this daemon's owner forever. */
|
|
689
|
-
owner:
|
|
1141
|
+
owner: z8.string().min(1),
|
|
690
1142
|
/** Display name for the trust UI, if the app offers one. */
|
|
691
|
-
ownerLabel:
|
|
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
|
|
692
1151
|
}).strict()
|
|
693
1152
|
]);
|
|
694
|
-
var PairRequest =
|
|
1153
|
+
var PairRequest = z8.discriminatedUnion("action", [
|
|
695
1154
|
PairStartRequest,
|
|
696
1155
|
PairPollRequest
|
|
697
1156
|
]);
|
|
698
|
-
var ClaimRequest =
|
|
699
|
-
protocolVersion:
|
|
700
|
-
runnerId:
|
|
1157
|
+
var ClaimRequest = z8.object({
|
|
1158
|
+
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1159
|
+
runnerId: z8.string().min(1),
|
|
701
1160
|
/** Re-sent on every claim so a server never matches against a stale matrix. */
|
|
702
1161
|
capabilities: CapabilityMatrix,
|
|
703
1162
|
/** Upper bound on jobs to return; the server may return fewer. */
|
|
704
|
-
max:
|
|
1163
|
+
max: z8.number().int().min(1).max(64)
|
|
705
1164
|
}).strict();
|
|
706
|
-
var ClaimResponse =
|
|
707
|
-
|
|
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),
|
|
708
1171
|
/** Lease duration granted, so the daemon knows its renewal deadline. */
|
|
709
|
-
leaseMs:
|
|
1172
|
+
leaseMs: z8.number().int().positive()
|
|
710
1173
|
}).strict();
|
|
711
|
-
var HeartbeatRequest =
|
|
712
|
-
protocolVersion:
|
|
713
|
-
runnerId:
|
|
714
|
-
daemonVersion:
|
|
1174
|
+
var HeartbeatRequest = z8.object({
|
|
1175
|
+
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1176
|
+
runnerId: z8.string().min(1),
|
|
1177
|
+
daemonVersion: z8.string().min(1),
|
|
715
1178
|
capabilities: CapabilityMatrix,
|
|
716
|
-
/**
|
|
717
|
-
|
|
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
|
+
),
|
|
718
1188
|
/** True while the owner has the daemon paused; the server stops offering work. */
|
|
719
|
-
paused:
|
|
1189
|
+
paused: z8.boolean()
|
|
720
1190
|
}).strict();
|
|
721
|
-
var HeartbeatResponse =
|
|
1191
|
+
var HeartbeatResponse = z8.object({
|
|
722
1192
|
/** Once true, the daemon stops claiming and abandons in-flight work. */
|
|
723
|
-
revoked:
|
|
1193
|
+
revoked: z8.boolean(),
|
|
724
1194
|
/**
|
|
725
1195
|
* Per-job cancel (byollm_001 Rev 1 §C). The daemon aborts these jobs'
|
|
726
1196
|
* in-flight backend calls and reports them `canceled`.
|
|
727
1197
|
*/
|
|
728
|
-
cancel:
|
|
1198
|
+
cancel: z8.array(z8.string().min(1)),
|
|
729
1199
|
/** Jobs whose leases were renewed, with their new expiry. */
|
|
730
|
-
leases:
|
|
731
|
-
|
|
732
|
-
jobId:
|
|
733
|
-
expiresAt:
|
|
1200
|
+
leases: z8.array(
|
|
1201
|
+
z8.object({
|
|
1202
|
+
jobId: z8.string().min(1),
|
|
1203
|
+
expiresAt: z8.number().int().positive()
|
|
734
1204
|
}).strict()
|
|
735
1205
|
),
|
|
736
1206
|
/**
|
|
737
1207
|
* Jobs the daemon thinks it holds but the server has reassigned or
|
|
738
1208
|
* expired. The daemon must stop work on these and not report results.
|
|
739
1209
|
*/
|
|
740
|
-
lost:
|
|
1210
|
+
lost: z8.array(z8.string().min(1)),
|
|
741
1211
|
/** Server clock, so a daemon with a skewed clock still honors leases. */
|
|
742
|
-
serverTime:
|
|
1212
|
+
serverTime: z8.number().int().positive()
|
|
743
1213
|
}).strict();
|
|
744
|
-
var
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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,
|
|
749
1235
|
/** Which model actually served it, for the result's provenance. */
|
|
750
|
-
model:
|
|
1236
|
+
model: z8.string().min(1),
|
|
751
1237
|
backendClass: BackendClass,
|
|
752
1238
|
/** Wall-clock milliseconds the backend call took. */
|
|
753
|
-
durationMs:
|
|
1239
|
+
durationMs: z8.number().int().nonnegative()
|
|
754
1240
|
}).strict();
|
|
755
|
-
var ResultResponse =
|
|
1241
|
+
var ResultResponse = z8.object({
|
|
756
1242
|
/**
|
|
757
1243
|
* False when the submission lost an idempotency race or the lease was
|
|
758
1244
|
* already gone — the daemon should discard, not retry
|
|
759
1245
|
* ({@link MUSTS.RESULT_IDEMPOTENT}).
|
|
760
1246
|
*/
|
|
761
|
-
accepted:
|
|
1247
|
+
accepted: z8.boolean(),
|
|
762
1248
|
/** The job's state after this submission. */
|
|
763
|
-
state:
|
|
1249
|
+
state: z8.string().min(1)
|
|
764
1250
|
}).strict();
|
|
765
|
-
var ReleaseRequest =
|
|
766
|
-
protocolVersion:
|
|
767
|
-
runnerId:
|
|
768
|
-
|
|
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
|
+
),
|
|
769
1264
|
/**
|
|
770
1265
|
* Why, so the app's runner list can say something true.
|
|
771
1266
|
*
|
|
@@ -775,12 +1270,12 @@ var ReleaseRequest = z5.object({
|
|
|
775
1270
|
* stop offering that job to that runner, or the pair would spin between
|
|
776
1271
|
* claim and release forever.
|
|
777
1272
|
*/
|
|
778
|
-
reason:
|
|
1273
|
+
reason: z8.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
|
|
779
1274
|
}).strict();
|
|
780
|
-
var ReleaseResponse =
|
|
781
|
-
released:
|
|
1275
|
+
var ReleaseResponse = z8.object({
|
|
1276
|
+
released: z8.array(z8.string().min(1))
|
|
782
1277
|
}).strict();
|
|
783
|
-
var WireErrorCode =
|
|
1278
|
+
var WireErrorCode = z8.enum([
|
|
784
1279
|
"bad-request",
|
|
785
1280
|
"unsupported-protocol-version",
|
|
786
1281
|
"unauthorized",
|
|
@@ -789,11 +1284,11 @@ var WireErrorCode = z5.enum([
|
|
|
789
1284
|
"rate-limited",
|
|
790
1285
|
"server-error"
|
|
791
1286
|
]);
|
|
792
|
-
var WireError =
|
|
1287
|
+
var WireError = z8.object({
|
|
793
1288
|
error: WireErrorCode,
|
|
794
|
-
message:
|
|
1289
|
+
message: z8.string().min(1),
|
|
795
1290
|
/** Seconds; mirrors Retry-After for `rate-limited` and `server-error`. */
|
|
796
|
-
retryAfter:
|
|
1291
|
+
retryAfter: z8.number().int().nonnegative().optional()
|
|
797
1292
|
}).strict();
|
|
798
1293
|
var ERROR_STATUS = Object.freeze({
|
|
799
1294
|
"bad-request": 400,
|
|
@@ -804,6 +1299,30 @@ var ERROR_STATUS = Object.freeze({
|
|
|
804
1299
|
"rate-limited": 429,
|
|
805
1300
|
"server-error": 500
|
|
806
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();
|
|
807
1326
|
export {
|
|
808
1327
|
AUDIENCES,
|
|
809
1328
|
Audience,
|
|
@@ -819,9 +1338,14 @@ export {
|
|
|
819
1338
|
ClaimRequest,
|
|
820
1339
|
ClaimResponse,
|
|
821
1340
|
ClaimedJob,
|
|
1341
|
+
ClaimedStub,
|
|
822
1342
|
DeliveredResult,
|
|
823
1343
|
ENDPOINTS,
|
|
1344
|
+
ENVELOPE_MAX_AGE_MS,
|
|
824
1345
|
ERROR_STATUS,
|
|
1346
|
+
EnvelopeDirection,
|
|
1347
|
+
FetchRequest,
|
|
1348
|
+
FetchResponse,
|
|
825
1349
|
GeneratePayload,
|
|
826
1350
|
HeartbeatRequest,
|
|
827
1351
|
HeartbeatResponse,
|
|
@@ -833,8 +1357,11 @@ export {
|
|
|
833
1357
|
JobResultError,
|
|
834
1358
|
JobResultOk,
|
|
835
1359
|
JobState,
|
|
1360
|
+
JobStub,
|
|
836
1361
|
KindedPayload,
|
|
837
1362
|
Lease,
|
|
1363
|
+
MAX_CLOCK_SKEW_MS,
|
|
1364
|
+
MIN_PROTOCOL_VERSION,
|
|
838
1365
|
MUSTS,
|
|
839
1366
|
MUST_IDS,
|
|
840
1367
|
MatchRefusal,
|
|
@@ -848,25 +1375,50 @@ export {
|
|
|
848
1375
|
PairRequest,
|
|
849
1376
|
PairStartRequest,
|
|
850
1377
|
PairStartResponse,
|
|
1378
|
+
PublicIdentity,
|
|
851
1379
|
REFUSAL_MESSAGES,
|
|
852
1380
|
ReleaseRequest,
|
|
853
1381
|
ReleaseResponse,
|
|
1382
|
+
RequestSignature,
|
|
1383
|
+
ResultDisposition,
|
|
854
1384
|
ResultProvenance,
|
|
855
1385
|
ResultRequest,
|
|
856
1386
|
ResultResponse,
|
|
1387
|
+
SIZE_CLASS_LIMITS,
|
|
1388
|
+
SUPPORTED_PROTOCOL_VERSIONS,
|
|
1389
|
+
SealedEnvelope,
|
|
1390
|
+
SizeClass,
|
|
1391
|
+
StoredKeys,
|
|
857
1392
|
TERMINAL_STATES,
|
|
858
1393
|
WireError,
|
|
859
1394
|
WireErrorCode,
|
|
860
1395
|
backendDescriptor,
|
|
861
1396
|
canTransition,
|
|
1397
|
+
canonicalRequest,
|
|
1398
|
+
checkProtocolVersion,
|
|
1399
|
+
cryptoReady,
|
|
862
1400
|
effectiveOfferScope,
|
|
1401
|
+
fingerprint,
|
|
1402
|
+
generateKeys,
|
|
863
1403
|
isBackendId,
|
|
864
1404
|
isJobKind,
|
|
865
1405
|
isLocalHost,
|
|
866
1406
|
isTerminal,
|
|
1407
|
+
keyId,
|
|
867
1408
|
matchAudience,
|
|
1409
|
+
mustsVerifiedBy,
|
|
1410
|
+
open,
|
|
868
1411
|
payloadTextLength,
|
|
869
1412
|
provenanceFor,
|
|
870
|
-
|
|
1413
|
+
publicIdentityOf,
|
|
1414
|
+
resolveCost,
|
|
1415
|
+
seal,
|
|
1416
|
+
signRequest,
|
|
1417
|
+
signWith,
|
|
1418
|
+
sizeClassCeiling,
|
|
1419
|
+
sizeClassOf,
|
|
1420
|
+
verifyPublicIdentity,
|
|
1421
|
+
verifyRequest,
|
|
1422
|
+
verifyWith
|
|
871
1423
|
};
|
|
872
1424
|
//# sourceMappingURL=index.js.map
|