@byollm/protocol 0.1.0-alpha.3 → 0.1.0-alpha.30
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 +74 -2
- package/dist/index.d.ts +759 -51
- package/dist/index.js +1117 -100
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { z as z2 } from "zod";
|
|
3
3
|
|
|
4
4
|
// src/backends.ts
|
|
5
|
+
import { isIP } from "net";
|
|
5
6
|
import { z } from "zod";
|
|
6
7
|
var BackendClass = z.enum(["http", "process"]);
|
|
7
8
|
var BackendCost = z.enum(["free", "metered", "subscription"]);
|
|
@@ -177,13 +178,16 @@ function backendDescriptor(id) {
|
|
|
177
178
|
function isLocalHost(hostname) {
|
|
178
179
|
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
179
180
|
if (host === "localhost" || host.endsWith(".localhost")) return true;
|
|
180
|
-
|
|
181
|
+
const version = isIP(host);
|
|
182
|
+
if (version === 0) return false;
|
|
183
|
+
if (version === 6) {
|
|
184
|
+
if (host === "::1") return true;
|
|
185
|
+
return /^f[cd]/.test(host);
|
|
186
|
+
}
|
|
181
187
|
if (host.startsWith("127.")) return true;
|
|
182
188
|
if (host.startsWith("10.")) return true;
|
|
183
189
|
if (host.startsWith("192.168.")) return true;
|
|
184
|
-
|
|
185
|
-
if (/^f[cd]/.test(host)) return true;
|
|
186
|
-
return false;
|
|
190
|
+
return /^172\.(1[6-9]|2\d|3[01])\./.test(host);
|
|
187
191
|
}
|
|
188
192
|
function resolveCost(id, baseUrl) {
|
|
189
193
|
const declared = BACKENDS[id].cost;
|
|
@@ -290,11 +294,21 @@ var ChatMessage = z3.object({
|
|
|
290
294
|
var GeneratePayload = z3.object({
|
|
291
295
|
prompt: z3.string().min(1).max(PAYLOAD_LIMITS.maxTextChars),
|
|
292
296
|
system: z3.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
|
|
293
|
-
}).strict()
|
|
297
|
+
}).strict().refine(
|
|
298
|
+
(payload) => payload.prompt.length + (payload.system?.length ?? 0) <= PAYLOAD_LIMITS.maxTotalChars,
|
|
299
|
+
{
|
|
300
|
+
message: `payload exceeds ${String(PAYLOAD_LIMITS.maxTotalChars)} characters`
|
|
301
|
+
}
|
|
302
|
+
);
|
|
294
303
|
var ChatPayload = z3.object({
|
|
295
304
|
messages: z3.array(ChatMessage).min(1).max(PAYLOAD_LIMITS.maxMessages),
|
|
296
305
|
system: z3.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
|
|
297
|
-
}).strict()
|
|
306
|
+
}).strict().refine(
|
|
307
|
+
(payload) => payload.messages.reduce((sum, m) => sum + m.content.length, 0) + (payload.system?.length ?? 0) <= PAYLOAD_LIMITS.maxTotalChars,
|
|
308
|
+
{
|
|
309
|
+
message: `payload exceeds ${String(PAYLOAD_LIMITS.maxTotalChars)} characters`
|
|
310
|
+
}
|
|
311
|
+
);
|
|
298
312
|
var JobKind = z3.enum(["llm.generate", "llm.chat"]);
|
|
299
313
|
var JOB_KINDS = Object.freeze(JobKind.options);
|
|
300
314
|
var KindedPayload = z3.discriminatedUnion("kind", [
|
|
@@ -350,6 +364,21 @@ function canTransition(from, to) {
|
|
|
350
364
|
return TRANSITIONS[from].includes(to);
|
|
351
365
|
}
|
|
352
366
|
var Lease = z4.object({
|
|
367
|
+
/**
|
|
368
|
+
* Identifies *this* grant, not just its holder.
|
|
369
|
+
*
|
|
370
|
+
* A runner can hold a job, release it, and claim it again — three leases,
|
|
371
|
+
* one runner id. Without an id for the grant itself, a lease-scoped request
|
|
372
|
+
* names a mutable target ambiguously, and a replayed release from the first
|
|
373
|
+
* grant lands on the third: the job returns to the queue while the daemon
|
|
374
|
+
* is mid-execution, and the work runs twice on the owner's hardware.
|
|
375
|
+
*
|
|
376
|
+
* That was a live hole, found in review after signed requests shipped. The
|
|
377
|
+
* signature scheme's replay argument rests on endpoints being idempotent —
|
|
378
|
+
* and release *is*, per lease, but not across leases, because nothing in
|
|
379
|
+
* the request said which one.
|
|
380
|
+
*/
|
|
381
|
+
id: z4.string().min(1),
|
|
353
382
|
/** The runner holding the lease. */
|
|
354
383
|
runnerId: z4.string().min(1),
|
|
355
384
|
/** Epoch milliseconds after which the claim is void. */
|
|
@@ -363,8 +392,19 @@ var ClaimedJob = z4.object({
|
|
|
363
392
|
audience: Audience,
|
|
364
393
|
/** The app's id for the user who enqueued it. */
|
|
365
394
|
owner: z4.string().min(1),
|
|
366
|
-
/**
|
|
367
|
-
|
|
395
|
+
/**
|
|
396
|
+
* Which site's job — V1-3.
|
|
397
|
+
*
|
|
398
|
+
* The stub has always carried it; the opened job did not, so everything
|
|
399
|
+
* downstream of the payload — the ingress line above all — recorded a job
|
|
400
|
+
* id that belongs to a site without saying which. Two sites can choose
|
|
401
|
+
* the same id, and the meter is the product.
|
|
402
|
+
*
|
|
403
|
+
* Optional so a caller assembling a job by hand is not forced to invent
|
|
404
|
+
* one, and so this reads as what it is: a fact about where the work came
|
|
405
|
+
* from, not a second copy of the routing key.
|
|
406
|
+
*/
|
|
407
|
+
site: z4.string().min(1).optional(),
|
|
368
408
|
lease: Lease
|
|
369
409
|
}).strict();
|
|
370
410
|
var ResultProvenance = z4.object({
|
|
@@ -394,6 +434,13 @@ function provenanceFor(input) {
|
|
|
394
434
|
untrusted: input.audience !== "self"
|
|
395
435
|
};
|
|
396
436
|
}
|
|
437
|
+
var RunMetadata = z4.object({
|
|
438
|
+
/** Which model actually served it. */
|
|
439
|
+
model: z4.string().min(1),
|
|
440
|
+
backendClass: BackendClass,
|
|
441
|
+
/** Wall-clock milliseconds the backend call took. */
|
|
442
|
+
durationMs: z4.number().int().nonnegative()
|
|
443
|
+
}).strict();
|
|
397
444
|
var JobResultOk = z4.object({
|
|
398
445
|
outcome: z4.literal("ok"),
|
|
399
446
|
text: z4.string(),
|
|
@@ -415,14 +462,403 @@ var JobOutcome = z4.discriminatedUnion("outcome", [
|
|
|
415
462
|
JobResultError,
|
|
416
463
|
JobResultCanceled
|
|
417
464
|
]);
|
|
465
|
+
var SealedOutcome = z4.object({ outcome: JobOutcome, ran: RunMetadata }).strict();
|
|
418
466
|
var DeliveredResult = z4.object({
|
|
419
467
|
jobId: z4.string().min(1),
|
|
420
468
|
state: JobState,
|
|
421
469
|
outcome: JobOutcome.optional(),
|
|
422
470
|
provenance: ResultProvenance.optional()
|
|
423
471
|
}).strict();
|
|
472
|
+
var SizeClass = z4.enum(["small", "medium", "large", "unbounded"]);
|
|
473
|
+
var SIZE_CLASS_LIMITS = Object.freeze({
|
|
474
|
+
small: 4e3,
|
|
475
|
+
medium: 64e3,
|
|
476
|
+
large: Number.POSITIVE_INFINITY
|
|
477
|
+
});
|
|
478
|
+
function sizeClassCeiling(sizeClass) {
|
|
479
|
+
if (sizeClass === "unbounded") return Number.POSITIVE_INFINITY;
|
|
480
|
+
return SIZE_CLASS_LIMITS[sizeClass];
|
|
481
|
+
}
|
|
482
|
+
function sizeClassOf(textChars) {
|
|
483
|
+
if (textChars <= SIZE_CLASS_LIMITS.small) return "small";
|
|
484
|
+
if (textChars <= SIZE_CLASS_LIMITS.medium) return "medium";
|
|
485
|
+
return "large";
|
|
486
|
+
}
|
|
487
|
+
var JobStub = z4.object({
|
|
488
|
+
id: z4.string().min(1),
|
|
489
|
+
kind: JobKind,
|
|
490
|
+
/** The app's id for the user who enqueued it. */
|
|
491
|
+
owner: z4.string().min(1),
|
|
492
|
+
/**
|
|
493
|
+
* Which site this job belongs to — byollm_009 Amendment A §A.3.
|
|
494
|
+
*
|
|
495
|
+
* **The site's identity key id**, not an id somebody assigned it. §6 has
|
|
496
|
+
* listed `site` since this spec was frozen; the schema never carried it,
|
|
497
|
+
* which is the drift the amendment closes.
|
|
498
|
+
*
|
|
499
|
+
* A key id rather than an opaque handle for one reason above the others:
|
|
500
|
+
* it makes the stub *self-describing* instead of a pointer into somebody
|
|
501
|
+
* else's table. A daemon holds this key id already, from pinning, so it
|
|
502
|
+
* can check `stub.site` against the payload envelope's `senderKeyId`
|
|
503
|
+
* without a lookup and without trusting the party that routed it. An
|
|
504
|
+
* opaque id can only be believed.
|
|
505
|
+
*
|
|
506
|
+
* It also avoids inventing a second namespace for a thing that has a
|
|
507
|
+
* canonical one — the shape of finding 41 (two owner namespaces compared
|
|
508
|
+
* for equality) and of finding fourteen before it.
|
|
509
|
+
*
|
|
510
|
+
* Rotation is a designed transition rather than a cost: a site publishes a
|
|
511
|
+
* new identity signed by the outgoing one, both are valid through an
|
|
512
|
+
* overlap window, and a daemon re-keys its own map by verifying that
|
|
513
|
+
* signature against the key it already pinned (§A.3.1).
|
|
514
|
+
*/
|
|
515
|
+
site: z4.string().min(1),
|
|
516
|
+
audience: Audience,
|
|
517
|
+
// `audienceAllow` is **not** here, and its absence is the enforcement —
|
|
518
|
+
// cloud_008 §0.2.
|
|
519
|
+
//
|
|
520
|
+
// It was a list of the people who may run a job, travelling to every
|
|
521
|
+
// routing party on every `named` job. byollm_001 Rev 1 §B settled who
|
|
522
|
+
// decides that long before this schema existed: *the daemon's own list
|
|
523
|
+
// decides, not the server's*, and `allowlist.predicateFor(origin)` is the
|
|
524
|
+
// enforcement in both lanes. So this was a second answer to a question the
|
|
525
|
+
// daemon already owned — able only to agree, in which case it was
|
|
526
|
+
// redundant, or to disagree, in which case nothing said which wins.
|
|
527
|
+
//
|
|
528
|
+
// The rule it leaves behind, which decides the next field too: **a class
|
|
529
|
+
// the router acts on may travel; membership never does.** `audience` stays
|
|
530
|
+
// for exactly that reason — the relay narrows on it. A roster does not
|
|
531
|
+
// travel, so `ROSTER_NOT_DISCLOSED` holds here by absence, which is the
|
|
532
|
+
// strongest way for a MUST to hold.
|
|
533
|
+
//
|
|
534
|
+
// The site keeps its own copy on `JobRecord` and still filters candidates
|
|
535
|
+
// with it before offering. That is server-internal, where the party
|
|
536
|
+
// holding the list authored it.
|
|
537
|
+
sizeClass: SizeClass,
|
|
538
|
+
/** Reserved for byollm_006. False until streaming exists. */
|
|
539
|
+
streaming: z4.boolean(),
|
|
540
|
+
/** Epoch ms after which the work is pointless; bounds ciphertext retention. */
|
|
541
|
+
deadlineAt: z4.number().int().positive()
|
|
542
|
+
}).strict();
|
|
543
|
+
var ClaimedStub = JobStub.extend({ lease: Lease }).strict();
|
|
544
|
+
|
|
545
|
+
// src/envelope.ts
|
|
546
|
+
import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2 } from "crypto";
|
|
547
|
+
import sodium from "libsodium-wrappers";
|
|
548
|
+
import { z as z6 } from "zod";
|
|
549
|
+
|
|
550
|
+
// src/keys.ts
|
|
551
|
+
import {
|
|
552
|
+
createHash,
|
|
553
|
+
createPrivateKey,
|
|
554
|
+
createPublicKey,
|
|
555
|
+
generateKeyPairSync,
|
|
556
|
+
sign,
|
|
557
|
+
verify
|
|
558
|
+
} from "crypto";
|
|
559
|
+
import { z as z5 } from "zod";
|
|
560
|
+
var PublicIdentity = z5.object({
|
|
561
|
+
/** Raw Ed25519 public key. The pinned one. */
|
|
562
|
+
identity: z5.string().min(1),
|
|
563
|
+
/** Raw X25519 public key, for sealing to this party. */
|
|
564
|
+
encryption: z5.string().min(1),
|
|
565
|
+
/**
|
|
566
|
+
* Ed25519 signature over the encryption key, by the identity key.
|
|
567
|
+
*
|
|
568
|
+
* This is what stops an upstream substituting an encryption key of its
|
|
569
|
+
* own while relaying a genuine identity: the receiver pins the identity
|
|
570
|
+
* and refuses any encryption key not signed by it.
|
|
571
|
+
*/
|
|
572
|
+
encryptionSig: z5.string().min(1)
|
|
573
|
+
}).strict();
|
|
574
|
+
var StoredKeys = z5.object({
|
|
575
|
+
version: z5.literal(1),
|
|
576
|
+
identityPublic: z5.string().min(1),
|
|
577
|
+
identityPrivate: z5.string().min(1),
|
|
578
|
+
encryptionPublic: z5.string().min(1),
|
|
579
|
+
encryptionPrivate: z5.string().min(1),
|
|
580
|
+
encryptionSig: z5.string().min(1),
|
|
581
|
+
createdAt: z5.number().int().positive()
|
|
582
|
+
}).strict();
|
|
583
|
+
var ENCRYPTION_KEY_CONTEXT = "byollm/v1/encryption-key";
|
|
584
|
+
function rawPublic(key) {
|
|
585
|
+
const jwk = key.export({ format: "jwk" });
|
|
586
|
+
const x = jwk.x;
|
|
587
|
+
if (typeof x !== "string") throw new Error("key has no raw public component");
|
|
588
|
+
return x;
|
|
589
|
+
}
|
|
590
|
+
function importPublic(raw, crv) {
|
|
591
|
+
return createPublicKey({ key: { kty: "OKP", crv, x: raw }, format: "jwk" });
|
|
592
|
+
}
|
|
593
|
+
function importPrivate(stored) {
|
|
594
|
+
return createPrivateKey({
|
|
595
|
+
key: Buffer.from(stored, "base64"),
|
|
596
|
+
type: "pkcs8",
|
|
597
|
+
format: "der"
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
var exportPrivate = (key) => key.export({ type: "pkcs8", format: "der" }).toString("base64");
|
|
601
|
+
function generateKeys(now) {
|
|
602
|
+
const identity = generateKeyPairSync("ed25519");
|
|
603
|
+
const encryption = generateKeyPairSync("x25519");
|
|
604
|
+
const encryptionPublic = rawPublic(encryption.publicKey);
|
|
605
|
+
return {
|
|
606
|
+
version: 1,
|
|
607
|
+
identityPublic: rawPublic(identity.publicKey),
|
|
608
|
+
identityPrivate: exportPrivate(identity.privateKey),
|
|
609
|
+
encryptionPublic,
|
|
610
|
+
encryptionPrivate: exportPrivate(encryption.privateKey),
|
|
611
|
+
encryptionSig: sign(
|
|
612
|
+
null,
|
|
613
|
+
Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${encryptionPublic}`),
|
|
614
|
+
identity.privateKey
|
|
615
|
+
).toString("base64url"),
|
|
616
|
+
createdAt: now
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function publicIdentityOf(keys) {
|
|
620
|
+
return {
|
|
621
|
+
identity: keys.identityPublic,
|
|
622
|
+
encryption: keys.encryptionPublic,
|
|
623
|
+
encryptionSig: keys.encryptionSig
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
function verifyPublicIdentity(identity) {
|
|
627
|
+
try {
|
|
628
|
+
return verify(
|
|
629
|
+
null,
|
|
630
|
+
Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${identity.encryption}`),
|
|
631
|
+
importPublic(identity.identity, "Ed25519"),
|
|
632
|
+
Buffer.from(identity.encryptionSig, "base64url")
|
|
633
|
+
);
|
|
634
|
+
} catch {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function signWith(keys, data) {
|
|
639
|
+
return sign(null, data, importPrivate(keys.identityPrivate)).toString(
|
|
640
|
+
"base64url"
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
function verifyWith(identityPublic, data, signature) {
|
|
644
|
+
try {
|
|
645
|
+
return verify(
|
|
646
|
+
null,
|
|
647
|
+
data,
|
|
648
|
+
importPublic(identityPublic, "Ed25519"),
|
|
649
|
+
Buffer.from(signature, "base64url")
|
|
650
|
+
);
|
|
651
|
+
} catch {
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
656
|
+
function fingerprint(identityPublic) {
|
|
657
|
+
const digest = createHash("sha256").update(Buffer.from(identityPublic, "base64url")).digest();
|
|
658
|
+
let bits = 0;
|
|
659
|
+
let value = 0;
|
|
660
|
+
let out = "";
|
|
661
|
+
for (const byte of digest.subarray(0, 15)) {
|
|
662
|
+
value = value << 8 | byte;
|
|
663
|
+
bits += 8;
|
|
664
|
+
while (bits >= 5) {
|
|
665
|
+
out += ALPHABET.charAt(value >>> bits - 5 & 31);
|
|
666
|
+
bits -= 5;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const groups = out.match(/.{1,4}/g) ?? [];
|
|
670
|
+
return `BYOLLM-${groups.join("-")}`;
|
|
671
|
+
}
|
|
672
|
+
var keyId = (identityPublic) => fingerprint(identityPublic);
|
|
673
|
+
|
|
674
|
+
// src/envelope.ts
|
|
675
|
+
var readied;
|
|
676
|
+
async function cryptoReady() {
|
|
677
|
+
readied ??= sodium.ready;
|
|
678
|
+
await readied;
|
|
679
|
+
}
|
|
680
|
+
var ENVELOPE_MAX_AGE_MS = 24 * 60 * 6e4;
|
|
681
|
+
var EnvelopeDirection = z6.enum(["payload", "result"]);
|
|
682
|
+
var SealedEnvelope = z6.object({
|
|
683
|
+
/** Base64url `crypto_box_seal` output over the signed plaintext. */
|
|
684
|
+
ciphertext: z6.string().min(1),
|
|
685
|
+
/** Who this was sealed to — the recipient checks it is them. */
|
|
686
|
+
recipientKeyId: z6.string().min(1),
|
|
687
|
+
/** Who signed it — the recipient checks this against its pin. */
|
|
688
|
+
senderKeyId: z6.string().min(1),
|
|
689
|
+
direction: EnvelopeDirection,
|
|
690
|
+
/**
|
|
691
|
+
* When this ciphertext stops being worth keeping.
|
|
692
|
+
*
|
|
693
|
+
* Carried *on* the envelope rather than recomputed by the opener. An
|
|
694
|
+
* earlier version derived it from the job's creation time, which meant
|
|
695
|
+
* two systems had to agree on a timestamp to the millisecond — and they
|
|
696
|
+
* did not, once a real database rounded it. A bound value that has to be
|
|
697
|
+
* reconstructed is a bound value that eventually is not.
|
|
698
|
+
*
|
|
699
|
+
* Not trusted as written: it is also inside the signature, so a changed
|
|
700
|
+
* deadline fails to verify.
|
|
701
|
+
*/
|
|
702
|
+
deadlineAt: z6.number().int().positive()
|
|
703
|
+
}).strict();
|
|
704
|
+
function signedBody(context, plaintext) {
|
|
705
|
+
return Buffer.from(
|
|
706
|
+
JSON.stringify({
|
|
707
|
+
v: "byollm/v1/envelope",
|
|
708
|
+
jobId: context.jobId,
|
|
709
|
+
senderKeyId: context.senderKeyId,
|
|
710
|
+
recipientKeyId: context.recipientKeyId,
|
|
711
|
+
deadlineAt: context.deadlineAt,
|
|
712
|
+
direction: context.direction,
|
|
713
|
+
plaintext
|
|
714
|
+
}),
|
|
715
|
+
"utf8"
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
var rawX25519 = (key, part) => {
|
|
719
|
+
const jwk = key.export({ format: "jwk" });
|
|
720
|
+
const value = part === "x" ? jwk.x : jwk.d;
|
|
721
|
+
if (typeof value !== "string") throw new Error("not an X25519 key");
|
|
722
|
+
return new Uint8Array(Buffer.from(value, "base64url"));
|
|
723
|
+
};
|
|
724
|
+
async function seal(input) {
|
|
725
|
+
await cryptoReady();
|
|
726
|
+
const body = signedBody(input.context, input.plaintext);
|
|
727
|
+
const signature = signWith(input.senderKeys, body);
|
|
728
|
+
const inner = JSON.stringify({ body: body.toString("base64url"), signature });
|
|
729
|
+
const recipient = new Uint8Array(
|
|
730
|
+
Buffer.from(input.recipientEncryptionPublic, "base64url")
|
|
731
|
+
);
|
|
732
|
+
const ciphertext = sodium.crypto_box_seal(
|
|
733
|
+
new Uint8Array(Buffer.from(inner, "utf8")),
|
|
734
|
+
recipient
|
|
735
|
+
);
|
|
736
|
+
return {
|
|
737
|
+
ciphertext: Buffer.from(ciphertext).toString("base64url"),
|
|
738
|
+
recipientKeyId: input.context.recipientKeyId,
|
|
739
|
+
senderKeyId: input.context.senderKeyId,
|
|
740
|
+
direction: input.context.direction,
|
|
741
|
+
deadlineAt: input.context.deadlineAt
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
async function open(input) {
|
|
745
|
+
await cryptoReady();
|
|
746
|
+
const { envelope, expected } = input;
|
|
747
|
+
if (envelope.recipientKeyId !== expected.recipientKeyId || envelope.senderKeyId !== expected.senderKeyId || envelope.direction !== expected.direction) {
|
|
748
|
+
return { ok: false, reason: "not-for-us" };
|
|
749
|
+
}
|
|
750
|
+
let inner;
|
|
751
|
+
try {
|
|
752
|
+
const priv = createPrivateKey2({
|
|
753
|
+
key: Buffer.from(input.recipientKeys.encryptionPrivate, "base64"),
|
|
754
|
+
type: "pkcs8",
|
|
755
|
+
format: "der"
|
|
756
|
+
});
|
|
757
|
+
const pub = createPublicKey2(priv);
|
|
758
|
+
const opened = sodium.crypto_box_seal_open(
|
|
759
|
+
new Uint8Array(Buffer.from(envelope.ciphertext, "base64url")),
|
|
760
|
+
rawX25519(pub, "x"),
|
|
761
|
+
rawX25519(priv, "d")
|
|
762
|
+
);
|
|
763
|
+
inner = Buffer.from(opened).toString("utf8");
|
|
764
|
+
} catch {
|
|
765
|
+
return { ok: false, reason: "unopenable" };
|
|
766
|
+
}
|
|
767
|
+
let parsed;
|
|
768
|
+
try {
|
|
769
|
+
parsed = JSON.parse(inner);
|
|
770
|
+
} catch {
|
|
771
|
+
return { ok: false, reason: "malformed" };
|
|
772
|
+
}
|
|
773
|
+
if (typeof parsed.body !== "string" || typeof parsed.signature !== "string") {
|
|
774
|
+
return { ok: false, reason: "malformed" };
|
|
775
|
+
}
|
|
776
|
+
const body = Buffer.from(parsed.body, "base64url");
|
|
777
|
+
if (!verifyWith(input.senderIdentityPublic, body, parsed.signature)) {
|
|
778
|
+
return { ok: false, reason: "bad-signature" };
|
|
779
|
+
}
|
|
780
|
+
let claims;
|
|
781
|
+
try {
|
|
782
|
+
claims = JSON.parse(body.toString("utf8"));
|
|
783
|
+
} catch {
|
|
784
|
+
return { ok: false, reason: "malformed" };
|
|
785
|
+
}
|
|
786
|
+
if (claims["jobId"] !== expected.jobId || claims["senderKeyId"] !== expected.senderKeyId || claims["recipientKeyId"] !== expected.recipientKeyId || claims["deadlineAt"] !== envelope.deadlineAt || claims["direction"] !== expected.direction) {
|
|
787
|
+
return { ok: false, reason: "context-mismatch" };
|
|
788
|
+
}
|
|
789
|
+
if (typeof claims["plaintext"] !== "string") {
|
|
790
|
+
return { ok: false, reason: "malformed" };
|
|
791
|
+
}
|
|
792
|
+
return { ok: true, plaintext: claims["plaintext"] };
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// src/signing.ts
|
|
796
|
+
import { createHash as createHash2 } from "crypto";
|
|
797
|
+
import { z as z7 } from "zod";
|
|
798
|
+
var MAX_CLOCK_SKEW_MS = 12e4;
|
|
799
|
+
var RequestSignature = z7.object({
|
|
800
|
+
/** Which runner is calling. The server looks up its pinned identity. */
|
|
801
|
+
runnerId: z7.string().min(1),
|
|
802
|
+
/** Epoch ms, bounded by {@link MAX_CLOCK_SKEW_MS}. */
|
|
803
|
+
issuedAt: z7.number().int().positive(),
|
|
804
|
+
/** Base64url Ed25519 signature over {@link canonicalRequest}. */
|
|
805
|
+
signature: z7.string().min(1)
|
|
806
|
+
}).strict();
|
|
807
|
+
function canonicalRequest(input) {
|
|
808
|
+
const digest = createHash2("sha256").update(input.body, "utf8").digest("hex");
|
|
809
|
+
return Buffer.from(
|
|
810
|
+
[
|
|
811
|
+
"byollm/v1/request",
|
|
812
|
+
input.endpoint,
|
|
813
|
+
input.runnerId,
|
|
814
|
+
String(input.issuedAt),
|
|
815
|
+
digest
|
|
816
|
+
].join("\n"),
|
|
817
|
+
"utf8"
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
function signRequest(keys, input) {
|
|
821
|
+
return {
|
|
822
|
+
runnerId: input.runnerId,
|
|
823
|
+
issuedAt: input.issuedAt,
|
|
824
|
+
signature: signWith(keys, canonicalRequest(input))
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function signSiteRequest(keys, input) {
|
|
828
|
+
return signRequest(keys, {
|
|
829
|
+
endpoint: siteEndpoint(input.endpoint),
|
|
830
|
+
runnerId: input.siteId,
|
|
831
|
+
issuedAt: input.issuedAt,
|
|
832
|
+
body: input.body
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
function verifySiteRequest(input) {
|
|
836
|
+
return verifyRequest({
|
|
837
|
+
...input,
|
|
838
|
+
endpoint: siteEndpoint(input.endpoint)
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
var siteEndpoint = (endpoint) => `site/${endpoint}`;
|
|
842
|
+
function verifyRequest(input) {
|
|
843
|
+
const skew = input.maxSkewMs ?? MAX_CLOCK_SKEW_MS;
|
|
844
|
+
if (Math.abs(input.now - input.signature.issuedAt) > skew) return "stale";
|
|
845
|
+
const ok = verifyWith(
|
|
846
|
+
input.identityPublic,
|
|
847
|
+
canonicalRequest({
|
|
848
|
+
endpoint: input.endpoint,
|
|
849
|
+
runnerId: input.signature.runnerId,
|
|
850
|
+
issuedAt: input.signature.issuedAt,
|
|
851
|
+
body: input.body
|
|
852
|
+
}),
|
|
853
|
+
input.signature.signature
|
|
854
|
+
);
|
|
855
|
+
return ok ? null : "bad-signature";
|
|
856
|
+
}
|
|
424
857
|
|
|
425
858
|
// src/musts.ts
|
|
859
|
+
function kindsOf(must2) {
|
|
860
|
+
return typeof must2.verifiedBy === "string" ? [must2.verifiedBy] : must2.verifiedBy;
|
|
861
|
+
}
|
|
426
862
|
var must = (m) => Object.freeze(m);
|
|
427
863
|
var MUSTS = Object.freeze({
|
|
428
864
|
// ---- Pairing and identity -------------------------------------------
|
|
@@ -430,31 +866,108 @@ var MUSTS = Object.freeze({
|
|
|
430
866
|
id: "PAIR_ONE_USER",
|
|
431
867
|
statement: "A runner token MUST be bound to exactly one user; a daemon MUST refuse work not attributable to its paired user.",
|
|
432
868
|
enforcedBy: "both",
|
|
869
|
+
verifiedBy: "conformance",
|
|
433
870
|
source: "byollm_001 \xA7MUSTs"
|
|
434
871
|
}),
|
|
435
872
|
PAIR_INTERACTIVE: must({
|
|
436
873
|
id: "PAIR_INTERACTIVE",
|
|
437
874
|
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
875
|
enforcedBy: "server",
|
|
876
|
+
verifiedBy: "conformance",
|
|
439
877
|
source: "byollm_001 \xA7Endpoints.1"
|
|
440
878
|
}),
|
|
441
879
|
PAIR_CODE_EXPIRES: must({
|
|
442
880
|
id: "PAIR_CODE_EXPIRES",
|
|
443
881
|
statement: "An unapproved device code MUST expire and MUST NOT be redeemable after expiry.",
|
|
444
882
|
enforcedBy: "server",
|
|
883
|
+
verifiedBy: "conformance",
|
|
445
884
|
source: "byollm_001 \xA7Endpoints.1"
|
|
446
885
|
}),
|
|
447
886
|
// ---- Typed job kinds --------------------------------------------------
|
|
887
|
+
VERSION_HANDSHAKE_REQUIRED: must({
|
|
888
|
+
id: "VERSION_HANDSHAKE_REQUIRED",
|
|
889
|
+
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.",
|
|
890
|
+
enforcedBy: "both",
|
|
891
|
+
verifiedBy: "conformance",
|
|
892
|
+
source: "byollm_009 \xA74"
|
|
893
|
+
}),
|
|
894
|
+
SITE_KEY_BY_STUB: must({
|
|
895
|
+
id: "SITE_KEY_BY_STUB",
|
|
896
|
+
statement: "A daemon MUST verify a job's payload against the pinned key of the site the stub names, and MUST refuse a job naming a site it has not pinned. It MUST NOT fall back to another pinned key, and MUST refuse an envelope whose declared sender disagrees with the stub's site.",
|
|
897
|
+
enforcedBy: "daemon",
|
|
898
|
+
// Adversarial, and the reason is the finding that produced it: the
|
|
899
|
+
// honest paths pass with every site check deleted, because `open`
|
|
900
|
+
// refuses a signature from the wrong key anyway. What distinguishes an
|
|
901
|
+
// enforced rule from a coincidence here is a hostile pairing of stub and
|
|
902
|
+
// envelope, which no conformance client would ever send.
|
|
903
|
+
verifiedBy: "adversarial",
|
|
904
|
+
source: "byollm_009 \xA7A.3"
|
|
905
|
+
}),
|
|
906
|
+
SITES_LOCALLY_APPROVED: must({
|
|
907
|
+
id: "SITES_LOCALLY_APPROVED",
|
|
908
|
+
statement: "A daemon MUST NOT run work for a site it has not approved on the machine itself. An upstream may propose a site set; a site the daemon has never approved MUST be offered to its owner and served nothing until they approve it. A key that has changed for an already-approved id MUST be refused for the life of the pairing, including after that id has left the set and returned.",
|
|
909
|
+
enforcedBy: "daemon",
|
|
910
|
+
// Two kinds, and the second is the one that matters — V1-1.
|
|
911
|
+
//
|
|
912
|
+
// `construction`: the daemon cannot serve a site that is not in its
|
|
913
|
+
// pinned map, and admission refuses before a payload is fetched, so the
|
|
914
|
+
// ordinary path cannot reach a site nobody approved.
|
|
915
|
+
//
|
|
916
|
+
// `adversarial`: the property that survives is about a *sequence* —
|
|
917
|
+
// remove the id, re-offer it under a different key — which no honest
|
|
918
|
+
// upstream sends and which the fence above does not see. That was the
|
|
919
|
+
// bypass: the pin was deleted with the id, so the comparison had nothing
|
|
920
|
+
// to compare against and the substitution arrived as a stranger.
|
|
921
|
+
verifiedBy: ["construction", "adversarial"],
|
|
922
|
+
source: "byollm_009 \xA7B.2"
|
|
923
|
+
}),
|
|
924
|
+
KEYS_EXCHANGED_AT_CONSENT: must({
|
|
925
|
+
id: "KEYS_EXCHANGED_AT_CONSENT",
|
|
926
|
+
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.",
|
|
927
|
+
enforcedBy: "both",
|
|
928
|
+
verifiedBy: "conformance",
|
|
929
|
+
source: "byollm_009 \xA75"
|
|
930
|
+
}),
|
|
931
|
+
REQUESTS_SIGNED_NOT_BEARER: must({
|
|
932
|
+
id: "REQUESTS_SIGNED_NOT_BEARER",
|
|
933
|
+
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.",
|
|
934
|
+
enforcedBy: "both",
|
|
935
|
+
verifiedBy: "conformance",
|
|
936
|
+
source: "byollm_009 \xA74.2"
|
|
937
|
+
}),
|
|
938
|
+
LEASE_SCOPED_BY_GRANT: must({
|
|
939
|
+
id: "LEASE_SCOPED_BY_GRANT",
|
|
940
|
+
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.",
|
|
941
|
+
enforcedBy: "both",
|
|
942
|
+
verifiedBy: "conformance",
|
|
943
|
+
source: "byollm_009 \xA74.2"
|
|
944
|
+
}),
|
|
945
|
+
STUB_METADATA_EXHAUSTIVE: must({
|
|
946
|
+
id: "STUB_METADATA_EXHAUSTIVE",
|
|
947
|
+
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.",
|
|
948
|
+
enforcedBy: "both",
|
|
949
|
+
verifiedBy: "conformance",
|
|
950
|
+
source: "byollm_009 \xA76"
|
|
951
|
+
}),
|
|
952
|
+
ENVELOPE_SEALED_AND_SIGNED: must({
|
|
953
|
+
id: "ENVELOPE_SEALED_AND_SIGNED",
|
|
954
|
+
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.",
|
|
955
|
+
enforcedBy: "server",
|
|
956
|
+
verifiedBy: "conformance",
|
|
957
|
+
source: "byollm_009 \xA76"
|
|
958
|
+
}),
|
|
448
959
|
KIND_TYPED_ONLY: must({
|
|
449
960
|
id: "KIND_TYPED_ONLY",
|
|
450
961
|
statement: "Job kinds MUST resolve against handlers baked into the daemon. A daemon MUST refuse an unknown kind rather than guess.",
|
|
451
962
|
enforcedBy: "daemon",
|
|
963
|
+
verifiedBy: "conformance",
|
|
452
964
|
source: "byollm_001 \xA7Jobs are typed data"
|
|
453
965
|
}),
|
|
454
966
|
KIND_NO_CODE: must({
|
|
455
967
|
id: "KIND_NO_CODE",
|
|
456
968
|
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
969
|
enforcedBy: "daemon",
|
|
970
|
+
verifiedBy: "conformance",
|
|
458
971
|
source: "byollm_001 \xA7Jobs are typed data; byollm_004 \xA71"
|
|
459
972
|
}),
|
|
460
973
|
// ---- Capability and claiming -----------------------------------------
|
|
@@ -462,18 +975,21 @@ var MUSTS = Object.freeze({
|
|
|
462
975
|
id: "CLAIM_REQUIRES_CAPABILITY",
|
|
463
976
|
statement: "A daemon MUST NOT be given a job whose kind is absent from its advertised capability matrix.",
|
|
464
977
|
enforcedBy: "both",
|
|
978
|
+
verifiedBy: "conformance",
|
|
465
979
|
source: "byollm_001 \xA7MUSTs"
|
|
466
980
|
}),
|
|
467
981
|
CAPABILITY_IS_DETECTED: must({
|
|
468
982
|
id: "CAPABILITY_IS_DETECTED",
|
|
469
983
|
statement: "An advertised capability matrix MUST be the intersection of owner config and detected, healthy reality \u2014 never config alone.",
|
|
470
984
|
enforcedBy: "daemon",
|
|
985
|
+
verifiedBy: "conformance",
|
|
471
986
|
source: "byollm_002 \xA7Routing"
|
|
472
987
|
}),
|
|
473
988
|
CLAIM_ATOMIC: must({
|
|
474
989
|
id: "CLAIM_ATOMIC",
|
|
475
990
|
statement: "Claiming MUST be atomic: a job MUST NOT be handed to two runners concurrently.",
|
|
476
991
|
enforcedBy: "server",
|
|
992
|
+
verifiedBy: "conformance",
|
|
477
993
|
source: "byollm_001 \xA7Endpoints.2"
|
|
478
994
|
}),
|
|
479
995
|
// ---- Leases -----------------------------------------------------------
|
|
@@ -481,12 +997,14 @@ var MUSTS = Object.freeze({
|
|
|
481
997
|
id: "LEASE_HONORED",
|
|
482
998
|
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
999
|
enforcedBy: "daemon",
|
|
1000
|
+
verifiedBy: "conformance",
|
|
484
1001
|
source: "byollm_001 \xA7MUSTs"
|
|
485
1002
|
}),
|
|
486
1003
|
LEASE_RECLAIMABLE: must({
|
|
487
1004
|
id: "LEASE_RECLAIMABLE",
|
|
488
1005
|
statement: "A lease that expires un-renewed MUST make its job claimable again with no loss of the job.",
|
|
489
1006
|
enforcedBy: "server",
|
|
1007
|
+
verifiedBy: "conformance",
|
|
490
1008
|
source: "byollm_001 \xA7Endpoints.2"
|
|
491
1009
|
}),
|
|
492
1010
|
// ---- Audience and offer scope ----------------------------------------
|
|
@@ -494,48 +1012,56 @@ var MUSTS = Object.freeze({
|
|
|
494
1012
|
id: "AUDIENCE_BOTH_SIDES",
|
|
495
1013
|
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
1014
|
enforcedBy: "both",
|
|
1015
|
+
verifiedBy: "conformance",
|
|
497
1016
|
source: "byollm_001 \xA7The audience model"
|
|
498
1017
|
}),
|
|
499
1018
|
SUBSCRIPTION_SELF_LOCK: must({
|
|
500
1019
|
id: "SUBSCRIPTION_SELF_LOCK",
|
|
501
1020
|
statement: "A subscription-class backend's offer scope MUST be 'self' and MUST NOT be widened by configuration.",
|
|
502
1021
|
enforcedBy: "daemon",
|
|
1022
|
+
verifiedBy: "conformance",
|
|
503
1023
|
source: "byollm_001 \xA7The audience model"
|
|
504
1024
|
}),
|
|
505
1025
|
METERED_DEFAULTS_SELF: must({
|
|
506
1026
|
id: "METERED_DEFAULTS_SELF",
|
|
507
1027
|
statement: "A metered backend's effective offer scope MUST be 'self' unless the owner has explicitly acknowledged spending money on others' work.",
|
|
508
1028
|
enforcedBy: "daemon",
|
|
1029
|
+
verifiedBy: "conformance",
|
|
509
1030
|
source: "byollm_007 \xA74"
|
|
510
1031
|
}),
|
|
511
1032
|
METERED_REQUIRES_CEILING: must({
|
|
512
1033
|
id: "METERED_REQUIRES_CEILING",
|
|
513
1034
|
statement: "A widened metered backend MUST carry a spend ceiling, and the daemon MUST refuse community work once it is reached.",
|
|
514
1035
|
enforcedBy: "daemon",
|
|
1036
|
+
verifiedBy: "conformance",
|
|
515
1037
|
source: "byollm_007 \xA74"
|
|
516
1038
|
}),
|
|
517
1039
|
COST_NOT_CONFIGURABLE: must({
|
|
518
1040
|
id: "COST_NOT_CONFIGURABLE",
|
|
519
1041
|
statement: "A built-in provider's cost class MUST NOT be overridable by configuration.",
|
|
520
1042
|
enforcedBy: "daemon",
|
|
1043
|
+
verifiedBy: "conformance",
|
|
521
1044
|
source: "byollm_007 \xA72"
|
|
522
1045
|
}),
|
|
523
1046
|
REMOTE_IS_NEVER_FREE: must({
|
|
524
1047
|
id: "REMOTE_IS_NEVER_FREE",
|
|
525
1048
|
statement: "A generic HTTP backend whose base URL is not loopback or private MUST be treated as metered.",
|
|
526
1049
|
enforcedBy: "daemon",
|
|
1050
|
+
verifiedBy: "conformance",
|
|
527
1051
|
source: "byollm_007 \xA72"
|
|
528
1052
|
}),
|
|
529
1053
|
NAMED_LOCAL_ALLOWLIST: must({
|
|
530
1054
|
id: "NAMED_LOCAL_ALLOWLIST",
|
|
531
1055
|
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
1056
|
enforcedBy: "daemon",
|
|
1057
|
+
verifiedBy: "conformance",
|
|
533
1058
|
source: "byollm_001 Rev 1 \xA7B"
|
|
534
1059
|
}),
|
|
535
1060
|
REFUSAL_NOT_REOFFERED: must({
|
|
536
1061
|
id: "REFUSAL_NOT_REOFFERED",
|
|
537
1062
|
statement: "A server MUST NOT re-offer a job to a runner that released it with reason 'refused'.",
|
|
538
1063
|
enforcedBy: "server",
|
|
1064
|
+
verifiedBy: "conformance",
|
|
539
1065
|
source: "byollm_001 Rev 1 \xA7B (loop resolved in build review)"
|
|
540
1066
|
}),
|
|
541
1067
|
// ---- Revocation and cancel -------------------------------------------
|
|
@@ -543,12 +1069,14 @@ var MUSTS = Object.freeze({
|
|
|
543
1069
|
id: "REVOCATION_HONORED",
|
|
544
1070
|
statement: "A revoked daemon MUST stop claiming and MUST abandon in-flight work by the next heartbeat at the latest.",
|
|
545
1071
|
enforcedBy: "daemon",
|
|
1072
|
+
verifiedBy: "conformance",
|
|
546
1073
|
source: "byollm_001 \xA7MUSTs"
|
|
547
1074
|
}),
|
|
548
1075
|
CANCEL_HONORED: must({
|
|
549
1076
|
id: "CANCEL_HONORED",
|
|
550
1077
|
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
1078
|
enforcedBy: "daemon",
|
|
1079
|
+
verifiedBy: "conformance",
|
|
552
1080
|
source: "byollm_001 Rev 1 \xA7C"
|
|
553
1081
|
}),
|
|
554
1082
|
// ---- Lifecycle, dependencies, delivery -------------------------------
|
|
@@ -556,37 +1084,43 @@ var MUSTS = Object.freeze({
|
|
|
556
1084
|
id: "DEPENDS_ON_GATING",
|
|
557
1085
|
statement: "A job MUST NOT be claimable until every job in its dependsOn set has reached the 'ok' state.",
|
|
558
1086
|
enforcedBy: "server",
|
|
1087
|
+
verifiedBy: "conformance",
|
|
559
1088
|
source: "byollm_001 Rev 1 \xA7E"
|
|
560
1089
|
}),
|
|
561
1090
|
TTL_EXPIRY: must({
|
|
562
1091
|
id: "TTL_EXPIRY",
|
|
563
1092
|
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
1093
|
enforcedBy: "server",
|
|
1094
|
+
verifiedBy: "conformance",
|
|
565
1095
|
source: "byollm_001 Rev 1 \xA7D (TTL clock resolved in build review)"
|
|
566
1096
|
}),
|
|
567
1097
|
NO_RUNNER_SIGNAL: must({
|
|
568
1098
|
id: "NO_RUNNER_SIGNAL",
|
|
569
1099
|
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
1100
|
enforcedBy: "server",
|
|
1101
|
+
verifiedBy: "conformance",
|
|
571
1102
|
source: "byollm_001 Rev 1 \xA7D"
|
|
572
1103
|
}),
|
|
573
1104
|
RESULT_IDEMPOTENT: must({
|
|
574
1105
|
id: "RESULT_IDEMPOTENT",
|
|
575
1106
|
statement: "Result submission MUST be idempotent by job id; the first terminal outcome wins and later submissions MUST NOT change it.",
|
|
576
1107
|
enforcedBy: "server",
|
|
1108
|
+
verifiedBy: "conformance",
|
|
577
1109
|
source: "byollm_001 \xA7Endpoints.4"
|
|
578
1110
|
}),
|
|
579
|
-
|
|
580
|
-
id: "
|
|
581
|
-
statement: "A result
|
|
1111
|
+
PROVENANCE_NAMES_DEVICE: must({
|
|
1112
|
+
id: "PROVENANCE_NAMES_DEVICE",
|
|
1113
|
+
statement: "A result MUST carry the claiming device's key id and its relationship to the requester, to the delivery seam, so an app never treats volunteer output as first-party. The key id MUST be the device the upstream granted the lease to, and a result whose signature does not verify against that device MUST be refused rather than recorded.",
|
|
582
1114
|
enforcedBy: "server",
|
|
583
|
-
|
|
1115
|
+
verifiedBy: "conformance",
|
|
1116
|
+
source: "byollm_009 \xA711"
|
|
584
1117
|
}),
|
|
585
1118
|
// ---- The trust surface -------------------------------------------------
|
|
586
1119
|
INGRESS_LOGGED_BEFORE_EXECUTION: must({
|
|
587
1120
|
id: "INGRESS_LOGGED_BEFORE_EXECUTION",
|
|
588
1121
|
statement: "Every executed prompt MUST be appended to the local ingress log before execution begins.",
|
|
589
1122
|
enforcedBy: "daemon",
|
|
1123
|
+
verifiedBy: "conformance",
|
|
590
1124
|
source: "byollm_001 \xA7MUSTs"
|
|
591
1125
|
}),
|
|
592
1126
|
// ---- Execution isolation (byollm_004) ---------------------------------
|
|
@@ -594,178 +1128,484 @@ var MUSTS = Object.freeze({
|
|
|
594
1128
|
id: "NO_SHELL_INTERPOLATION",
|
|
595
1129
|
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
1130
|
enforcedBy: "daemon",
|
|
1131
|
+
verifiedBy: "adversarial",
|
|
597
1132
|
source: "byollm_004 \xA72"
|
|
598
1133
|
}),
|
|
599
1134
|
NO_PAYLOAD_ROUTING: must({
|
|
600
1135
|
id: "NO_PAYLOAD_ROUTING",
|
|
601
1136
|
statement: "Model, backend, base URL, and flags MUST come from owner config only; a payload MUST NOT influence any of them.",
|
|
602
1137
|
enforcedBy: "daemon",
|
|
1138
|
+
verifiedBy: "adversarial",
|
|
603
1139
|
source: "byollm_004 \xA72"
|
|
604
1140
|
}),
|
|
605
1141
|
STRIPPED_CHILD_ENV: must({
|
|
606
1142
|
id: "STRIPPED_CHILD_ENV",
|
|
607
1143
|
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
1144
|
enforcedBy: "daemon",
|
|
1145
|
+
verifiedBy: "adversarial",
|
|
609
1146
|
source: "byollm_004 \xA72"
|
|
610
1147
|
}),
|
|
611
1148
|
HTTP_BASE_URL_SAFE: must({
|
|
612
1149
|
id: "HTTP_BASE_URL_SAFE",
|
|
613
1150
|
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
1151
|
enforcedBy: "daemon",
|
|
1152
|
+
verifiedBy: "adversarial",
|
|
615
1153
|
source: "byollm_004 Rev 1 \xA7Backend taxonomy"
|
|
616
1154
|
}),
|
|
617
1155
|
OUTPUT_INERT: must({
|
|
618
1156
|
id: "OUTPUT_INERT",
|
|
619
1157
|
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
1158
|
enforcedBy: "daemon",
|
|
1159
|
+
verifiedBy: "adversarial",
|
|
621
1160
|
source: "byollm_004 \xA72"
|
|
622
1161
|
}),
|
|
623
1162
|
COMMUNITY_BUDGETS: must({
|
|
624
1163
|
id: "COMMUNITY_BUDGETS",
|
|
625
1164
|
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
1165
|
enforcedBy: "daemon",
|
|
1166
|
+
verifiedBy: "adversarial",
|
|
627
1167
|
source: "byollm_004 \xA74"
|
|
1168
|
+
}),
|
|
1169
|
+
REVOCATION_IMMEDIATE: must({
|
|
1170
|
+
id: "REVOCATION_IMMEDIATE",
|
|
1171
|
+
statement: "Revocation MUST take effect at the upstream at once \u2014 a revoked runner MUST NOT be granted further work from the moment the record changes \u2014 and MUST reach the daemon by its next heartbeat.",
|
|
1172
|
+
// Both, and stated as one sentence with two obligations rather than
|
|
1173
|
+
// folded into REVOCATION_HONORED. That one binds the *daemon*: a revoked
|
|
1174
|
+
// daemon stops claiming and abandons in-flight work. This binds the
|
|
1175
|
+
// *upstream*. byollm_009 §5 is explicit that the pair is the point — "a
|
|
1176
|
+
// revocation enforced at one end survives a compromise of that end" — and
|
|
1177
|
+
// one entry covering both would make a compromised daemon look compliant.
|
|
1178
|
+
enforcedBy: "both",
|
|
1179
|
+
verifiedBy: "conformance",
|
|
1180
|
+
source: "byollm_009 \xA711"
|
|
1181
|
+
}),
|
|
1182
|
+
CONSENT_BEFORE_ROUTE: must({
|
|
1183
|
+
id: "CONSENT_BEFORE_ROUTE",
|
|
1184
|
+
statement: "An upstream MUST NOT route a job to a device without a record binding that user, that site and that scope. There MUST be no discovery path by which a device receives work it was never granted.",
|
|
1185
|
+
enforcedBy: "server",
|
|
1186
|
+
verifiedBy: "conformance",
|
|
1187
|
+
source: "byollm_009 \xA711"
|
|
1188
|
+
}),
|
|
1189
|
+
ROSTER_NOT_DISCLOSED: must({
|
|
1190
|
+
id: "ROSTER_NOT_DISCLOSED",
|
|
1191
|
+
statement: "A site MUST NOT learn the membership of a group whose compute it uses, and MUST NOT publish membership to a routing party. No wire message may carry a list of who may run a job.",
|
|
1192
|
+
// Checkable since cloud_008 §0.2 took `audienceAllow` off the stub: the
|
|
1193
|
+
// property now holds by *absence*, and absence is exactly what a strict
|
|
1194
|
+
// schema and a serialised stub can be asked about. Before that it was a
|
|
1195
|
+
// sentence — and one this project cited in code comments, tests and two
|
|
1196
|
+
// specs as though it were enforced data, which is why it is worth
|
|
1197
|
+
// stating precisely rather than generously.
|
|
1198
|
+
enforcedBy: "both",
|
|
1199
|
+
verifiedBy: "conformance",
|
|
1200
|
+
source: "byollm_009 \xA711"
|
|
1201
|
+
}),
|
|
1202
|
+
EFFECTIVE_OFFER_ONLY: must({
|
|
1203
|
+
id: "EFFECTIVE_OFFER_ONLY",
|
|
1204
|
+
statement: "A daemon MUST declare effective offers only. An upstream MUST NOT receive raw config, allowlists, or capacity the owner has not shared, and MUST act on the declared offer rather than on what was asked for.",
|
|
1205
|
+
enforcedBy: "both",
|
|
1206
|
+
verifiedBy: "conformance",
|
|
1207
|
+
source: "byollm_009 \xA711"
|
|
1208
|
+
}),
|
|
1209
|
+
FALLBACK_LABELED: must({
|
|
1210
|
+
id: "FALLBACK_LABELED",
|
|
1211
|
+
statement: "Work served by anything other than the user's own compute MUST be labelled as such wherever it is reported, and MUST NOT be silently substituted.",
|
|
1212
|
+
// `construction` today, and deliberately not `conformance`. Nothing on
|
|
1213
|
+
// the wire yet distinguishes a fallback from any other community job —
|
|
1214
|
+
// the ledger that would give it a surface is unbuilt — so a check would
|
|
1215
|
+
// have to assert something it cannot observe. Promoted the day that
|
|
1216
|
+
// surface exists. Marking it `conformance` now would put "verified"
|
|
1217
|
+
// beside a property no third party can see, which is the one thing the
|
|
1218
|
+
// kinds exist to prevent.
|
|
1219
|
+
enforcedBy: "both",
|
|
1220
|
+
verifiedBy: "construction",
|
|
1221
|
+
source: "byollm_009 \xA711"
|
|
1222
|
+
}),
|
|
1223
|
+
RELAY_BLIND: must({
|
|
1224
|
+
id: "RELAY_BLIND",
|
|
1225
|
+
statement: "A relay MUST NOT hold any key capable of decrypting a payload, a result, or a delta frame.",
|
|
1226
|
+
// Operator: a third party can read the relay's types and see there is
|
|
1227
|
+
// nowhere to put such a key, but the kit certifies a *server* and cannot
|
|
1228
|
+
// reach inside somebody's deployment to prove what it holds.
|
|
1229
|
+
enforcedBy: "server",
|
|
1230
|
+
verifiedBy: "operator",
|
|
1231
|
+
source: "byollm_009 \xA711"
|
|
1232
|
+
}),
|
|
1233
|
+
SHARED_COMPUTE_DISCLOSED: must({
|
|
1234
|
+
id: "SHARED_COMPUTE_DISCLOSED",
|
|
1235
|
+
statement: "Before a user's work first runs on compute they do not own, they MUST be told in plain language that the machine's owner can see it.",
|
|
1236
|
+
// Operator, and cloud_008 §0.3 is why the classification now comes with a
|
|
1237
|
+
// standing answer rather than a standing question. The screen is not
|
|
1238
|
+
// wire-observable, but the *string the server composes* is, and it is
|
|
1239
|
+
// now unit-tested with the two false sentences forbidden by name. The
|
|
1240
|
+
// kind stays `operator` because a third-party site can still render
|
|
1241
|
+
// whatever it likes; what changed is that the part inside our own
|
|
1242
|
+
// boundary stopped depending on somebody remembering to audit it.
|
|
1243
|
+
enforcedBy: "server",
|
|
1244
|
+
verifiedBy: "operator",
|
|
1245
|
+
source: "byollm_009 \xA711"
|
|
628
1246
|
})
|
|
629
1247
|
});
|
|
1248
|
+
var RETIRED_MUSTS = Object.freeze({
|
|
1249
|
+
RESULT_PROVENANCE: {
|
|
1250
|
+
supersededBy: "PROVENANCE_NAMES_DEVICE",
|
|
1251
|
+
note: "Strengthened, not renamed: attribution is now by proof of possession \u2014 the result's signature must verify against the device the upstream granted the lease to \u2014 rather than by a provenance label travelling beside it. byollm_009 \xA711 states the stronger form."
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
630
1254
|
var MUST_IDS = Object.freeze(Object.keys(MUSTS));
|
|
1255
|
+
function mustsVerifiedBy(kind) {
|
|
1256
|
+
return MUST_IDS.filter((id) => kindsOf(MUSTS[id]).includes(kind));
|
|
1257
|
+
}
|
|
631
1258
|
|
|
632
1259
|
// src/wire.ts
|
|
633
|
-
import { z as
|
|
1260
|
+
import { z as z8 } from "zod";
|
|
634
1261
|
var PROTOCOL_VERSION = "0";
|
|
1262
|
+
var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze([
|
|
1263
|
+
PROTOCOL_VERSION
|
|
1264
|
+
]);
|
|
1265
|
+
var MIN_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0] ?? PROTOCOL_VERSION;
|
|
1266
|
+
function declaredVersion(input) {
|
|
1267
|
+
const { body, query } = input;
|
|
1268
|
+
if (typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion")) {
|
|
1269
|
+
return body.protocolVersion;
|
|
1270
|
+
}
|
|
1271
|
+
return query?.get("protocolVersion") ?? void 0;
|
|
1272
|
+
}
|
|
1273
|
+
function checkProtocolVersion(body) {
|
|
1274
|
+
const declared = typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion") ? body.protocolVersion : void 0;
|
|
1275
|
+
if (typeof declared !== "string" || declared.length === 0) {
|
|
1276
|
+
return {
|
|
1277
|
+
error: "unsupported-protocol-version",
|
|
1278
|
+
message: `this request declared no protocol version. Upgrade the daemon: \`${UPGRADE_COMMAND}\`.`,
|
|
1279
|
+
supported: SUPPORTED_PROTOCOL_VERSIONS,
|
|
1280
|
+
minimum: MIN_PROTOCOL_VERSION
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
if (!SUPPORTED_PROTOCOL_VERSIONS.includes(declared)) {
|
|
1284
|
+
return {
|
|
1285
|
+
error: "unsupported-protocol-version",
|
|
1286
|
+
message: `this server speaks protocol ${SUPPORTED_PROTOCOL_VERSIONS.join(", ")} and the daemon asked for ${declared}. ` + (declared < MIN_PROTOCOL_VERSION ? `Upgrade the daemon: \`${UPGRADE_COMMAND}\`.` : "This daemon is newer than the server; the server needs upgrading."),
|
|
1287
|
+
supported: SUPPORTED_PROTOCOL_VERSIONS,
|
|
1288
|
+
minimum: MIN_PROTOCOL_VERSION
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
var UPGRADE_COMMAND = "npm i -g byollm@latest";
|
|
635
1294
|
var PROTOCOL_PREFIX = "/byollm";
|
|
636
1295
|
var ENDPOINTS = Object.freeze([
|
|
637
1296
|
"pair",
|
|
638
1297
|
"claim",
|
|
1298
|
+
"fetch",
|
|
639
1299
|
"heartbeat",
|
|
640
1300
|
"result",
|
|
641
1301
|
"release"
|
|
642
1302
|
]);
|
|
643
|
-
var Capability =
|
|
1303
|
+
var Capability = z8.object({
|
|
644
1304
|
kind: JobKind,
|
|
645
1305
|
backendId: BackendIdSchema,
|
|
646
1306
|
backendClass: BackendClass,
|
|
647
|
-
model:
|
|
1307
|
+
model: z8.string().min(1),
|
|
648
1308
|
offerScope: OfferScope
|
|
649
1309
|
}).strict();
|
|
650
|
-
var CapabilityMatrix =
|
|
651
|
-
var PairStartRequest =
|
|
652
|
-
protocolVersion:
|
|
653
|
-
action:
|
|
654
|
-
daemon:
|
|
655
|
-
version:
|
|
1310
|
+
var CapabilityMatrix = z8.array(Capability);
|
|
1311
|
+
var PairStartRequest = z8.object({
|
|
1312
|
+
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1313
|
+
action: z8.literal("start"),
|
|
1314
|
+
daemon: z8.object({
|
|
1315
|
+
version: z8.string().min(1),
|
|
656
1316
|
/** Shown in the app's runner list so a user can tell their machines apart. */
|
|
657
|
-
label:
|
|
658
|
-
platform:
|
|
1317
|
+
label: z8.string().min(1).max(120),
|
|
1318
|
+
platform: z8.enum(["darwin", "linux", "win32"])
|
|
659
1319
|
}),
|
|
1320
|
+
/**
|
|
1321
|
+
* This machine's public keys (byollm_009 §5).
|
|
1322
|
+
*
|
|
1323
|
+
* Pairing is where the two parties learn each other's identities, because
|
|
1324
|
+
* it is the one moment a human is already deciding to trust: the approval
|
|
1325
|
+
* click. A key exchanged anywhere else would be a key nobody chose.
|
|
1326
|
+
*/
|
|
1327
|
+
device: PublicIdentity,
|
|
660
1328
|
capabilities: CapabilityMatrix
|
|
661
1329
|
}).strict();
|
|
662
|
-
var PairStartResponse =
|
|
1330
|
+
var PairStartResponse = z8.object({
|
|
663
1331
|
/** Secret the daemon polls with. Never shown to the user. */
|
|
664
|
-
deviceCode:
|
|
1332
|
+
deviceCode: z8.string().min(20),
|
|
665
1333
|
/** Short code the user reads and confirms in the browser. */
|
|
666
|
-
userCode:
|
|
1334
|
+
userCode: z8.string().min(4).max(16),
|
|
667
1335
|
/** Where the user approves. Must be on the server's own origin. */
|
|
668
|
-
verificationUrl:
|
|
1336
|
+
verificationUrl: z8.url(),
|
|
669
1337
|
/** Epoch ms after which the code is dead ({@link MUSTS.PAIR_CODE_EXPIRES}). */
|
|
670
|
-
expiresAt:
|
|
1338
|
+
expiresAt: z8.number().int().positive(),
|
|
671
1339
|
/** How often the daemon may poll. */
|
|
672
|
-
pollIntervalMs:
|
|
1340
|
+
pollIntervalMs: z8.number().int().min(500).max(6e4)
|
|
673
1341
|
}).strict();
|
|
674
|
-
var PairPollRequest =
|
|
675
|
-
protocolVersion:
|
|
676
|
-
action:
|
|
677
|
-
deviceCode:
|
|
1342
|
+
var PairPollRequest = z8.object({
|
|
1343
|
+
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1344
|
+
action: z8.literal("poll"),
|
|
1345
|
+
deviceCode: z8.string().min(20)
|
|
678
1346
|
}).strict();
|
|
679
|
-
var PairPollResponse =
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
status:
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
1347
|
+
var PairPollResponse = z8.discriminatedUnion("status", [
|
|
1348
|
+
z8.object({ status: z8.literal("pending") }).strict(),
|
|
1349
|
+
z8.object({ status: z8.literal("denied") }).strict(),
|
|
1350
|
+
z8.object({ status: z8.literal("expired") }).strict(),
|
|
1351
|
+
z8.object({
|
|
1352
|
+
status: z8.literal("approved"),
|
|
1353
|
+
// `runnerToken` is gone — cloud_008 §2.4, finding 37.
|
|
1354
|
+
//
|
|
1355
|
+
// It was minted here, hashed into `RunnerRecord.tokenHash`, written to
|
|
1356
|
+
// the daemon's pairings file, and then **never sent, never looked up
|
|
1357
|
+
// and never compared**. `getRunnerByTokenHash` existed on both stores
|
|
1358
|
+
// and was called by nothing but a test asserting it returns null.
|
|
1359
|
+
//
|
|
1360
|
+
// Not merely dead wire, which is what `audienceAllow` and
|
|
1361
|
+
// `HeartbeatResponse.leases` were. This was a *secret*: minted,
|
|
1362
|
+
// transmitted, and written to two disks at rest, for nothing. A
|
|
1363
|
+
// credential with no purpose is a liability rather than clutter,
|
|
1364
|
+
// because the only thing it can ever do is leak.
|
|
1365
|
+
//
|
|
1366
|
+
// `REQUESTS_SIGNED_NOT_BEARER` was already the rule and was already
|
|
1367
|
+
// enforced — every authenticated call is signed by the device's pinned
|
|
1368
|
+
// identity key. This removes the thing the MUST is named after.
|
|
1369
|
+
runnerId: z8.string().min(1),
|
|
688
1370
|
/** The app's id for the approving user — this daemon's owner forever. */
|
|
689
|
-
owner:
|
|
1371
|
+
owner: z8.string().min(1),
|
|
690
1372
|
/** Display name for the trust UI, if the app offers one. */
|
|
691
|
-
ownerLabel:
|
|
1373
|
+
ownerLabel: z8.string().optional(),
|
|
1374
|
+
/**
|
|
1375
|
+
* The sites this pairing covers, for the daemon to pin (byollm_009 §5),
|
|
1376
|
+
* keyed by each site's identity key id — cloud_009 §5.
|
|
1377
|
+
*
|
|
1378
|
+
* Returned only on approval: a pending or denied poll learns nothing,
|
|
1379
|
+
* so an unapproved code cannot be used to enumerate a site's keys.
|
|
1380
|
+
*
|
|
1381
|
+
* **One pairing per upstream, not one per site.** A user who connects a
|
|
1382
|
+
* site on a web dashboard has no reason to go back to a laptop and run
|
|
1383
|
+
* a command, so which sites a pairing covers is a projection of consent
|
|
1384
|
+
* — refreshed on the heartbeat — rather than something frozen at
|
|
1385
|
+
* pairing. A direct site answers with exactly one entry, which is the
|
|
1386
|
+
* same shape and not a special case.
|
|
1387
|
+
*
|
|
1388
|
+
* Keyed by the id `stub.site` carries (Amendment A §A.3), so the
|
|
1389
|
+
* runner's lookup is a map read rather than a join across two
|
|
1390
|
+
* namespaces.
|
|
1391
|
+
*/
|
|
1392
|
+
sites: z8.record(z8.string().min(1), PublicIdentity)
|
|
692
1393
|
}).strict()
|
|
693
1394
|
]);
|
|
694
|
-
var PairRequest =
|
|
1395
|
+
var PairRequest = z8.discriminatedUnion("action", [
|
|
695
1396
|
PairStartRequest,
|
|
696
1397
|
PairPollRequest
|
|
697
1398
|
]);
|
|
698
|
-
var ClaimRequest =
|
|
699
|
-
protocolVersion:
|
|
700
|
-
runnerId:
|
|
1399
|
+
var ClaimRequest = z8.object({
|
|
1400
|
+
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1401
|
+
runnerId: z8.string().min(1),
|
|
701
1402
|
/** Re-sent on every claim so a server never matches against a stale matrix. */
|
|
702
1403
|
capabilities: CapabilityMatrix,
|
|
703
1404
|
/** Upper bound on jobs to return; the server may return fewer. */
|
|
704
|
-
max:
|
|
1405
|
+
max: z8.number().int().min(1).max(64)
|
|
705
1406
|
}).strict();
|
|
706
|
-
var ClaimResponse =
|
|
707
|
-
|
|
1407
|
+
var ClaimResponse = z8.object({
|
|
1408
|
+
/**
|
|
1409
|
+
* Stubs, not jobs. The payload arrives from `fetch`, sealed to whichever
|
|
1410
|
+
* device claimed — see {@link JobStub} for the exhaustive metadata list.
|
|
1411
|
+
*/
|
|
1412
|
+
jobs: z8.array(ClaimedStub),
|
|
708
1413
|
/** Lease duration granted, so the daemon knows its renewal deadline. */
|
|
709
|
-
leaseMs:
|
|
1414
|
+
leaseMs: z8.number().int().positive()
|
|
710
1415
|
}).strict();
|
|
711
|
-
var HeartbeatRequest =
|
|
712
|
-
protocolVersion:
|
|
713
|
-
runnerId:
|
|
714
|
-
daemonVersion:
|
|
1416
|
+
var HeartbeatRequest = z8.object({
|
|
1417
|
+
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1418
|
+
runnerId: z8.string().min(1),
|
|
1419
|
+
daemonVersion: z8.string().min(1),
|
|
715
1420
|
capabilities: CapabilityMatrix,
|
|
716
|
-
/**
|
|
717
|
-
|
|
1421
|
+
/**
|
|
1422
|
+
* Leases this daemon believes it holds; the server renews exactly these.
|
|
1423
|
+
*
|
|
1424
|
+
* Lease ids rather than job ids, so a replayed heartbeat cannot renew a
|
|
1425
|
+
* grant the runner no longer holds — see {@link Lease.id}.
|
|
1426
|
+
*/
|
|
1427
|
+
activeLeases: z8.array(
|
|
1428
|
+
z8.object({ jobId: z8.string().min(1), leaseId: z8.string().min(1) })
|
|
1429
|
+
),
|
|
718
1430
|
/** True while the owner has the daemon paused; the server stops offering work. */
|
|
719
|
-
paused:
|
|
1431
|
+
paused: z8.boolean()
|
|
720
1432
|
}).strict();
|
|
721
|
-
var HeartbeatResponse =
|
|
722
|
-
/**
|
|
723
|
-
|
|
1433
|
+
var HeartbeatResponse = z8.object({
|
|
1434
|
+
/**
|
|
1435
|
+
* The sites this daemon may serve, right now — cloud_008 finding 59.
|
|
1436
|
+
*
|
|
1437
|
+
* Revocation used to be a boolean, and it was device-wide: the daemon
|
|
1438
|
+
* plane refused every call when the (owner, hub-site) consent was gone,
|
|
1439
|
+
* heartbeat answered `revoked: true` with `lost: all`, and the daemon
|
|
1440
|
+
* dropped its whole pairing by origin. Under a hub that is one site's
|
|
1441
|
+
* revocation ending a machine's relationship with every other site it
|
|
1442
|
+
* served — the amplification finding 48 warned about, arriving through
|
|
1443
|
+
* the one field nobody thought of as tenancy.
|
|
1444
|
+
*
|
|
1445
|
+
* So the answer is the set. A site that leaves it is revoked *for that
|
|
1446
|
+
* site*: the daemon drops that pin and keeps the rest. An empty set is
|
|
1447
|
+
* what "revoked" used to mean, and the daemon can see that for itself
|
|
1448
|
+
* rather than being told a second time — two fields for one fact is how
|
|
1449
|
+
* they drift.
|
|
1450
|
+
*/
|
|
1451
|
+
sites: z8.record(z8.string().min(1), PublicIdentity),
|
|
724
1452
|
/**
|
|
725
1453
|
* Per-job cancel (byollm_001 Rev 1 §C). The daemon aborts these jobs'
|
|
726
1454
|
* in-flight backend calls and reports them `canceled`.
|
|
1455
|
+
*
|
|
1456
|
+
* **The grant, not the id** — V1-3. Job ids are chosen per site, so two
|
|
1457
|
+
* sites may pick the same one, and a bare id told a daemon holding both
|
|
1458
|
+
* to abort whichever it happened to have filed under that name. The lease
|
|
1459
|
+
* is the unique grant and the daemon already keys its work by it; this is
|
|
1460
|
+
* the same shape `activeLeases` sends in the other direction.
|
|
727
1461
|
*/
|
|
728
|
-
cancel:
|
|
729
|
-
|
|
730
|
-
leases: z5.array(
|
|
731
|
-
z5.object({
|
|
732
|
-
jobId: z5.string().min(1),
|
|
733
|
-
expiresAt: z5.number().int().positive()
|
|
734
|
-
}).strict()
|
|
1462
|
+
cancel: z8.array(
|
|
1463
|
+
z8.object({ jobId: z8.string().min(1), leaseId: z8.string().min(1) }).strict()
|
|
735
1464
|
),
|
|
1465
|
+
// `leases` is deliberately absent — cloud_008 §1.4b, finding 16.
|
|
1466
|
+
//
|
|
1467
|
+
// It carried "these leases were renewed, and here is the new expiry", and
|
|
1468
|
+
// **no daemon ever read it.** A mutation returning an empty list while
|
|
1469
|
+
// renewing correctly survived every test, which is what made it visible.
|
|
1470
|
+
//
|
|
1471
|
+
// It is neither a class nor membership, so Amendment A's rule does not
|
|
1472
|
+
// decide it — the older test does: nothing reads it, so it is dead wire.
|
|
1473
|
+
// §6's exhaustiveness is a commitment about what an upstream can see, and
|
|
1474
|
+
// it applies to every message rather than only to the stub.
|
|
1475
|
+
//
|
|
1476
|
+
// `lost` is the actionable signal and always was: a daemon stops work on
|
|
1477
|
+
// a lease it no longer holds. "Renewed" was the same question answered a
|
|
1478
|
+
// second time, and a second answer can only agree or contradict.
|
|
1479
|
+
//
|
|
1480
|
+
// Renewal itself is untouched — the upstream still extends the grants a
|
|
1481
|
+
// heartbeat names, which is what §0.6 fixed. What ended is telling the
|
|
1482
|
+
// daemon about it in a field it ignored. If an upstream ever needs to
|
|
1483
|
+
// push lease decisions, that is a new field with a reader, added on
|
|
1484
|
+
// purpose.
|
|
736
1485
|
/**
|
|
737
1486
|
* Jobs the daemon thinks it holds but the server has reassigned or
|
|
738
1487
|
* expired. The daemon must stop work on these and not report results.
|
|
1488
|
+
*
|
|
1489
|
+
* Named by grant rather than by id, for V1-3's reason: a bare id is
|
|
1490
|
+
* ambiguous across sites, and "the lease you no longer hold" is exactly
|
|
1491
|
+
* what this field means anyway.
|
|
739
1492
|
*/
|
|
740
|
-
lost:
|
|
1493
|
+
lost: z8.array(
|
|
1494
|
+
z8.object({ jobId: z8.string().min(1), leaseId: z8.string().min(1) }).strict()
|
|
1495
|
+
),
|
|
741
1496
|
/** Server clock, so a daemon with a skewed clock still honors leases. */
|
|
742
|
-
serverTime:
|
|
1497
|
+
serverTime: z8.number().int().positive(),
|
|
1498
|
+
/**
|
|
1499
|
+
* Sites whose disclosure the user must read again before work moves —
|
|
1500
|
+
* cloud_008 finding 48, named rather than counted.
|
|
1501
|
+
*
|
|
1502
|
+
* A **subset of `sites`**, deliberately: a paused site keeps its pin, so
|
|
1503
|
+
* re-consenting never costs a re-pair. The daemon can say which site is
|
|
1504
|
+
* waiting and the user can go and read it, which is the difference
|
|
1505
|
+
* between a machine that is quietly idle and one that says why.
|
|
1506
|
+
*
|
|
1507
|
+
* Not `revoked`, which is a human ending a relationship, and not
|
|
1508
|
+
* `paused`, which on the request side already means "this daemon's
|
|
1509
|
+
* operator stopped it" — one word with two subjects on two halves of one
|
|
1510
|
+
* exchange is a confusion nobody untangles from a log.
|
|
1511
|
+
*/
|
|
1512
|
+
awaitingConsent: z8.array(z8.string().min(1))
|
|
743
1513
|
}).strict();
|
|
744
|
-
var
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
/**
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1514
|
+
var ResultDisposition = z8.enum(["ok", "error", "canceled"]);
|
|
1515
|
+
var ResultRequest = z8.object({
|
|
1516
|
+
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1517
|
+
runnerId: z8.string().min(1),
|
|
1518
|
+
jobId: z8.string().min(1),
|
|
1519
|
+
/**
|
|
1520
|
+
* The grant this result was produced under — cloud_008 §1.4a.
|
|
1521
|
+
*
|
|
1522
|
+
* `fetch` has always named its lease, with the reasoning written beside
|
|
1523
|
+
* it: a request that names only the job would be answerable for whatever
|
|
1524
|
+
* lease exists when it arrives. **The operation that writes the result did
|
|
1525
|
+
* not**, on either plane, and checked only the runner id — which survives
|
|
1526
|
+
* a claim-release-reclaim cycle, so a device whose grant had been swept
|
|
1527
|
+
* and reissued could still land a result for a job it no longer held.
|
|
1528
|
+
*
|
|
1529
|
+
* Found by tracing a mutation that survived in §0.6: the lease lapsed, the
|
|
1530
|
+
* sweep requeued, the daemon re-claimed under a new grant, and the
|
|
1531
|
+
* original run finished and posted anyway. The relay marked the job done
|
|
1532
|
+
* with a result the site cannot open — it verifies the envelope against
|
|
1533
|
+
* the *current* holder's device, so the crypto contains the substitution —
|
|
1534
|
+
* and then refused the real holder's result as a replay. A lost job, in
|
|
1535
|
+
* silence.
|
|
1536
|
+
*
|
|
1537
|
+
* `LEASE_HONORED` is a statement about a lease *instance*. That was
|
|
1538
|
+
* learned once already, when a replayed release yanked a later grant, and
|
|
1539
|
+
* it applies here for the same reason.
|
|
1540
|
+
*/
|
|
1541
|
+
leaseId: z8.string().min(1),
|
|
1542
|
+
/**
|
|
1543
|
+
* The outcome, sealed to the site and signed by the device.
|
|
1544
|
+
*
|
|
1545
|
+
* The return leg of the payload envelope, and sealed for the same reason:
|
|
1546
|
+
* a model's answer is as sensitive as the prompt that produced it, and an
|
|
1547
|
+
* intermediary that cannot read one must not be handed the other.
|
|
1548
|
+
*/
|
|
1549
|
+
envelope: SealedEnvelope,
|
|
1550
|
+
/**
|
|
1551
|
+
* The sealed outcome's discriminator, in the clear.
|
|
1552
|
+
*
|
|
1553
|
+
* Checked against the envelope once opened. It is a routing hint, not a
|
|
1554
|
+
* fact: believing it unverified would let a daemon mark a job `ok` while
|
|
1555
|
+
* sealing an error, and only the app would ever find out.
|
|
1556
|
+
*/
|
|
1557
|
+
disposition: ResultDisposition
|
|
1558
|
+
// `model`, `backendClass` and `durationMs` are **inside the envelope** —
|
|
1559
|
+
// cloud_008 §2.5. See {@link RunMetadata}.
|
|
1560
|
+
//
|
|
1561
|
+
// They were here, in the clear, and that was two problems wearing one
|
|
1562
|
+
// coat. On the direct plane the site recorded unauthenticated fields
|
|
1563
|
+
// beside an authenticated answer: a daemon could seal one result and
|
|
1564
|
+
// declare a different model, and only the unsigned half would reach the
|
|
1565
|
+
// app. Through a relay they reached a third party that acts on none of
|
|
1566
|
+
// them — `model` in particular being the sort of detail Amendment A's
|
|
1567
|
+
// rule keeps off the wire.
|
|
1568
|
+
//
|
|
1569
|
+
// `disposition` stays, and the difference is the test: a relay *routes*
|
|
1570
|
+
// on it, so it is a class a routing party consumes. Nobody between the
|
|
1571
|
+
// two ends consumes these.
|
|
754
1572
|
}).strict();
|
|
755
|
-
var ResultResponse =
|
|
1573
|
+
var ResultResponse = z8.object({
|
|
756
1574
|
/**
|
|
757
|
-
* False when
|
|
758
|
-
*
|
|
759
|
-
* ({@link MUSTS.RESULT_IDEMPOTENT}).
|
|
1575
|
+
* False when this submission wrote nothing — the daemon should discard,
|
|
1576
|
+
* not retry ({@link MUSTS.RESULT_IDEMPOTENT}).
|
|
760
1577
|
*/
|
|
761
|
-
accepted:
|
|
1578
|
+
accepted: z8.boolean(),
|
|
1579
|
+
/**
|
|
1580
|
+
* True when this device had already recorded this job's result.
|
|
1581
|
+
*
|
|
1582
|
+
* The difference between "already recorded" and "you no longer hold this"
|
|
1583
|
+
* — cloud_008 §3.6. A daemon whose acknowledgment was lost is in the first
|
|
1584
|
+
* case and needs to hear it: its answer is safely on disk. Reporting a
|
|
1585
|
+
* stale lease instead invents a worry about a result that is already
|
|
1586
|
+
* stored, and sends its owner looking for a routing problem.
|
|
1587
|
+
*
|
|
1588
|
+
* Set only for the device that finished the job. A different device gets
|
|
1589
|
+
* the same refusal it would get for a job that is *not* terminal, so a job
|
|
1590
|
+
* id cannot be used as a terminality probe.
|
|
1591
|
+
*/
|
|
1592
|
+
duplicate: z8.boolean().optional(),
|
|
762
1593
|
/** The job's state after this submission. */
|
|
763
|
-
state:
|
|
1594
|
+
state: z8.string().min(1)
|
|
764
1595
|
}).strict();
|
|
765
|
-
var ReleaseRequest =
|
|
766
|
-
protocolVersion:
|
|
767
|
-
runnerId:
|
|
768
|
-
|
|
1596
|
+
var ReleaseRequest = z8.object({
|
|
1597
|
+
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1598
|
+
runnerId: z8.string().min(1),
|
|
1599
|
+
/**
|
|
1600
|
+
* Which leases to release — the grant, not just the job.
|
|
1601
|
+
*
|
|
1602
|
+
* A release naming only a job id releases whatever lease exists at the
|
|
1603
|
+
* moment it arrives, which for a replayed request is not the lease the
|
|
1604
|
+
* daemon meant. See {@link Lease.id}.
|
|
1605
|
+
*/
|
|
1606
|
+
leases: z8.array(
|
|
1607
|
+
z8.object({ jobId: z8.string().min(1), leaseId: z8.string().min(1) })
|
|
1608
|
+
),
|
|
769
1609
|
/**
|
|
770
1610
|
* Why, so the app's runner list can say something true.
|
|
771
1611
|
*
|
|
@@ -775,35 +1615,172 @@ var ReleaseRequest = z5.object({
|
|
|
775
1615
|
* stop offering that job to that runner, or the pair would spin between
|
|
776
1616
|
* claim and release forever.
|
|
777
1617
|
*/
|
|
778
|
-
reason:
|
|
1618
|
+
reason: z8.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
|
|
779
1619
|
}).strict();
|
|
780
|
-
var ReleaseResponse =
|
|
781
|
-
released:
|
|
1620
|
+
var ReleaseResponse = z8.object({
|
|
1621
|
+
released: z8.array(z8.string().min(1))
|
|
782
1622
|
}).strict();
|
|
783
|
-
var WireErrorCode =
|
|
1623
|
+
var WireErrorCode = z8.enum([
|
|
784
1624
|
"bad-request",
|
|
785
1625
|
"unsupported-protocol-version",
|
|
1626
|
+
// "We do not know who you are." Exactly 401, and only that — cloud_008
|
|
1627
|
+
// §1.4d.
|
|
786
1628
|
"unauthorized",
|
|
1629
|
+
/**
|
|
1630
|
+
* "We know exactly who you are, and the answer is no." Exactly 403.
|
|
1631
|
+
*
|
|
1632
|
+
* Five refusals across both planes served 403 with `unauthorized`, whose
|
|
1633
|
+
* table entry is 401: a revoked device, a site claiming another site's
|
|
1634
|
+
* stub, a job you do not hold, a device belonging to another owner, a
|
|
1635
|
+
* relay that does not route for you. Every one of them is an *identified*
|
|
1636
|
+
* caller being refused.
|
|
1637
|
+
*
|
|
1638
|
+
* Collapsing the two loses a distinction that matters everywhere it is
|
|
1639
|
+
* read: a revoked daemon would look like an unsigned one in every log and
|
|
1640
|
+
* every client branch, and "check your keys" is the wrong advice for both
|
|
1641
|
+
* of them in opposite directions.
|
|
1642
|
+
*/
|
|
1643
|
+
"forbidden",
|
|
787
1644
|
"revoked",
|
|
788
1645
|
"not-found",
|
|
1646
|
+
// Claimed, but the site has not sealed the payload yet — cloud_008 §1.4.
|
|
1647
|
+
//
|
|
1648
|
+
// A daemon must retry rather than abandon: the job is legitimately still
|
|
1649
|
+
// its own until the lease or the awaiting-payload clock says otherwise.
|
|
1650
|
+
// That is why it cannot be `not-found` or `server-error`, and why it was
|
|
1651
|
+
// the protocol gap that produced a bare 409 in the first place.
|
|
1652
|
+
"not-ready",
|
|
1653
|
+
/**
|
|
1654
|
+
* The job is over, and this call is about a job — V1-6, and the code the
|
|
1655
|
+
* site plane has been serving without one (V1-13).
|
|
1656
|
+
*
|
|
1657
|
+
* Distinct from `not-found`, which says "no such job", and from
|
|
1658
|
+
* `not-ready`, which says "not yet, keep asking". This one says "yes, and
|
|
1659
|
+
* it finished" — so a daemon must stop rather than retry, and a replayed
|
|
1660
|
+
* request must not be able to reopen it.
|
|
1661
|
+
*/
|
|
1662
|
+
"too-late",
|
|
1663
|
+
// The caller's clock is too far from ours to judge a signature's freshness.
|
|
1664
|
+
//
|
|
1665
|
+
// Split out from `unauthorized` because the remedy is completely different
|
|
1666
|
+
// and only the server can tell them apart: a bad signature means the key is
|
|
1667
|
+
// wrong, this means the machine's time is wrong. A daemon reporting it as a
|
|
1668
|
+
// generic rejection sends its owner looking at their network.
|
|
1669
|
+
"clock-skew",
|
|
789
1670
|
"rate-limited",
|
|
790
1671
|
"server-error"
|
|
791
1672
|
]);
|
|
792
|
-
var WireError =
|
|
1673
|
+
var WireError = z8.object({
|
|
793
1674
|
error: WireErrorCode,
|
|
794
|
-
message:
|
|
1675
|
+
message: z8.string().min(1),
|
|
1676
|
+
/**
|
|
1677
|
+
* What this server speaks, on `unsupported-protocol-version` — §B.4.
|
|
1678
|
+
*
|
|
1679
|
+
* The refusal has carried these since the version handshake existed and
|
|
1680
|
+
* the enumeration did not model them, so the one error that exists to be
|
|
1681
|
+
* *acted on* was the one that failed to parse as a wire error. Found by
|
|
1682
|
+
* the relay's own suite the day the relay started sending it: a refusal
|
|
1683
|
+
* outside the enumerated shape is a refusal a client cannot branch on,
|
|
1684
|
+
* which is the whole reason §1.4 enumerates them.
|
|
1685
|
+
*
|
|
1686
|
+
* Modelled the way `clock-skew`'s two fields already are — code-specific
|
|
1687
|
+
* extras, refused on any other code by the refinement below.
|
|
1688
|
+
*/
|
|
1689
|
+
supported: z8.array(z8.string().min(1)).optional(),
|
|
1690
|
+
minimum: z8.string().min(1).optional(),
|
|
795
1691
|
/** Seconds; mirrors Retry-After for `rate-limited` and `server-error`. */
|
|
796
|
-
retryAfter:
|
|
797
|
-
|
|
1692
|
+
retryAfter: z8.number().int().nonnegative().optional(),
|
|
1693
|
+
/**
|
|
1694
|
+
* The server's clock, and the window it allows. `clock-skew` only.
|
|
1695
|
+
*
|
|
1696
|
+
* So the far side can say *how far off* rather than *that something is
|
|
1697
|
+
* wrong* — the difference between "adjust your clock by four minutes" and
|
|
1698
|
+
* "something is wrong with your connection". Not a disclosure: the
|
|
1699
|
+
* heartbeat response returns the same value, and so does every `Date`
|
|
1700
|
+
* header.
|
|
1701
|
+
*/
|
|
1702
|
+
serverTime: z8.number().int().positive().optional(),
|
|
1703
|
+
maxSkewMs: z8.number().int().positive().optional()
|
|
1704
|
+
}).strict().superRefine((error, ctx) => {
|
|
1705
|
+
const skew = error.error === "clock-skew";
|
|
1706
|
+
const carried = error.serverTime !== void 0 || error.maxSkewMs !== void 0;
|
|
1707
|
+
if (skew && !carried) {
|
|
1708
|
+
ctx.addIssue({
|
|
1709
|
+
code: "custom",
|
|
1710
|
+
message: "clock-skew must carry serverTime and maxSkewMs"
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
if (!skew && carried) {
|
|
1714
|
+
ctx.addIssue({
|
|
1715
|
+
code: "custom",
|
|
1716
|
+
message: `${error.error} must not carry serverTime or maxSkewMs`
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
const version = error.error === "unsupported-protocol-version";
|
|
1720
|
+
const versionFields = error.supported !== void 0 || error.minimum !== void 0;
|
|
1721
|
+
if (version && !versionFields) {
|
|
1722
|
+
ctx.addIssue({
|
|
1723
|
+
code: "custom",
|
|
1724
|
+
message: "unsupported-protocol-version must carry supported and minimum"
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
if (!version && versionFields) {
|
|
1728
|
+
ctx.addIssue({
|
|
1729
|
+
code: "custom",
|
|
1730
|
+
message: `${error.error} must not carry supported or minimum`
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
});
|
|
798
1734
|
var ERROR_STATUS = Object.freeze({
|
|
799
1735
|
"bad-request": 400,
|
|
800
1736
|
"unsupported-protocol-version": 400,
|
|
801
1737
|
unauthorized: 401,
|
|
1738
|
+
forbidden: 403,
|
|
802
1739
|
revoked: 403,
|
|
803
1740
|
"not-found": 404,
|
|
1741
|
+
// 409, not 404: the job exists and is yours, it is simply not ready.
|
|
1742
|
+
"not-ready": 409,
|
|
1743
|
+
// The same 409 as `not-ready` and the opposite instruction: that one says
|
|
1744
|
+
// keep asking, this one says stop. The status is the class of the
|
|
1745
|
+
// problem — a request that does not fit the resource's state — and the
|
|
1746
|
+
// code is what a caller acts on.
|
|
1747
|
+
"too-late": 409,
|
|
1748
|
+
// 401 alongside `unauthorized`, because that is what it is — the
|
|
1749
|
+
// signature could not be judged. The code is what carries the remedy.
|
|
1750
|
+
"clock-skew": 401,
|
|
804
1751
|
"rate-limited": 429,
|
|
805
1752
|
"server-error": 500
|
|
806
1753
|
});
|
|
1754
|
+
var FetchRequest = z8.object({
|
|
1755
|
+
// `literal`, like every other request — V1-17. This one said
|
|
1756
|
+
// `string().min(1)`, so a daemon speaking a version this server does not
|
|
1757
|
+
// know got past the handshake on the one endpoint that hands over a
|
|
1758
|
+
// sealed payload. The version check exists so that a mismatch is a named
|
|
1759
|
+
// refusal rather than a schema failure three fields later; here it was
|
|
1760
|
+
// neither.
|
|
1761
|
+
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1762
|
+
runnerId: z8.string().min(1),
|
|
1763
|
+
jobId: z8.string().min(1),
|
|
1764
|
+
/**
|
|
1765
|
+
* The grant this daemon holds.
|
|
1766
|
+
*
|
|
1767
|
+
* Named, not inferred: a fetch is lease-scoped, and a request that names
|
|
1768
|
+
* only the job would be answerable for whatever lease exists when it
|
|
1769
|
+
* arrives ({@link Lease.id}).
|
|
1770
|
+
*/
|
|
1771
|
+
leaseId: z8.string().min(1)
|
|
1772
|
+
}).strict();
|
|
1773
|
+
var FetchResponse = z8.object({
|
|
1774
|
+
/**
|
|
1775
|
+
* The work, sealed to the device that claimed it — byollm_009 §6.
|
|
1776
|
+
*
|
|
1777
|
+
* Not plaintext. The site opens its own at-rest envelope and re-seals to
|
|
1778
|
+
* the claiming device's key, signed by the site's identity, so the work
|
|
1779
|
+
* is readable only by the machine that took it and only if it came from
|
|
1780
|
+
* the site that machine pinned.
|
|
1781
|
+
*/
|
|
1782
|
+
envelope: SealedEnvelope
|
|
1783
|
+
}).strict();
|
|
807
1784
|
export {
|
|
808
1785
|
AUDIENCES,
|
|
809
1786
|
Audience,
|
|
@@ -819,9 +1796,15 @@ export {
|
|
|
819
1796
|
ClaimRequest,
|
|
820
1797
|
ClaimResponse,
|
|
821
1798
|
ClaimedJob,
|
|
1799
|
+
ClaimedStub,
|
|
822
1800
|
DeliveredResult,
|
|
1801
|
+
ENCRYPTION_KEY_CONTEXT,
|
|
823
1802
|
ENDPOINTS,
|
|
1803
|
+
ENVELOPE_MAX_AGE_MS,
|
|
824
1804
|
ERROR_STATUS,
|
|
1805
|
+
EnvelopeDirection,
|
|
1806
|
+
FetchRequest,
|
|
1807
|
+
FetchResponse,
|
|
825
1808
|
GeneratePayload,
|
|
826
1809
|
HeartbeatRequest,
|
|
827
1810
|
HeartbeatResponse,
|
|
@@ -833,8 +1816,11 @@ export {
|
|
|
833
1816
|
JobResultError,
|
|
834
1817
|
JobResultOk,
|
|
835
1818
|
JobState,
|
|
1819
|
+
JobStub,
|
|
836
1820
|
KindedPayload,
|
|
837
1821
|
Lease,
|
|
1822
|
+
MAX_CLOCK_SKEW_MS,
|
|
1823
|
+
MIN_PROTOCOL_VERSION,
|
|
838
1824
|
MUSTS,
|
|
839
1825
|
MUST_IDS,
|
|
840
1826
|
MatchRefusal,
|
|
@@ -848,25 +1834,56 @@ export {
|
|
|
848
1834
|
PairRequest,
|
|
849
1835
|
PairStartRequest,
|
|
850
1836
|
PairStartResponse,
|
|
1837
|
+
PublicIdentity,
|
|
851
1838
|
REFUSAL_MESSAGES,
|
|
852
1839
|
ReleaseRequest,
|
|
853
1840
|
ReleaseResponse,
|
|
1841
|
+
RequestSignature,
|
|
1842
|
+
ResultDisposition,
|
|
854
1843
|
ResultProvenance,
|
|
855
1844
|
ResultRequest,
|
|
856
1845
|
ResultResponse,
|
|
1846
|
+
RunMetadata,
|
|
1847
|
+
SIZE_CLASS_LIMITS,
|
|
1848
|
+
SUPPORTED_PROTOCOL_VERSIONS,
|
|
1849
|
+
SealedEnvelope,
|
|
1850
|
+
SealedOutcome,
|
|
1851
|
+
SizeClass,
|
|
1852
|
+
StoredKeys,
|
|
857
1853
|
TERMINAL_STATES,
|
|
858
1854
|
WireError,
|
|
859
1855
|
WireErrorCode,
|
|
860
1856
|
backendDescriptor,
|
|
861
1857
|
canTransition,
|
|
1858
|
+
canonicalRequest,
|
|
1859
|
+
checkProtocolVersion,
|
|
1860
|
+
cryptoReady,
|
|
1861
|
+
declaredVersion,
|
|
862
1862
|
effectiveOfferScope,
|
|
1863
|
+
fingerprint,
|
|
1864
|
+
generateKeys,
|
|
863
1865
|
isBackendId,
|
|
864
1866
|
isJobKind,
|
|
865
1867
|
isLocalHost,
|
|
866
1868
|
isTerminal,
|
|
1869
|
+
keyId,
|
|
1870
|
+
kindsOf,
|
|
867
1871
|
matchAudience,
|
|
1872
|
+
mustsVerifiedBy,
|
|
1873
|
+
open,
|
|
868
1874
|
payloadTextLength,
|
|
869
1875
|
provenanceFor,
|
|
870
|
-
|
|
1876
|
+
publicIdentityOf,
|
|
1877
|
+
resolveCost,
|
|
1878
|
+
seal,
|
|
1879
|
+
signRequest,
|
|
1880
|
+
signSiteRequest,
|
|
1881
|
+
signWith,
|
|
1882
|
+
sizeClassCeiling,
|
|
1883
|
+
sizeClassOf,
|
|
1884
|
+
verifyPublicIdentity,
|
|
1885
|
+
verifyRequest,
|
|
1886
|
+
verifySiteRequest,
|
|
1887
|
+
verifyWith
|
|
871
1888
|
};
|
|
872
1889
|
//# sourceMappingURL=index.js.map
|