@byollm/protocol 0.1.0-alpha.57 → 0.1.0-alpha.59
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 +2 -2
- package/dist/index.d.ts +370 -166
- package/dist/index.js +555 -466
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -255,22 +255,28 @@ function classifyCost(id, baseUrl, model) {
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
// src/audience.ts
|
|
258
|
-
var Audience = z2.enum(["private", "team"
|
|
259
|
-
var OfferScope = z2.enum(["private", "team"
|
|
258
|
+
var Audience = z2.enum(["private", "team"]);
|
|
259
|
+
var OfferScope = z2.enum(["private", "team"]);
|
|
260
260
|
var AUDIENCES = Object.freeze(Audience.options);
|
|
261
261
|
var OFFER_SCOPES = Object.freeze(OfferScope.options);
|
|
262
262
|
var MatchRefusal = z2.enum([
|
|
263
263
|
/** The daemon advertises no capability for this kind. */
|
|
264
264
|
"no-capability",
|
|
265
|
-
/** Job is `
|
|
265
|
+
/** Job is `private` but this daemon belongs to a different user. */
|
|
266
266
|
"audience-self-other-owner",
|
|
267
|
-
/**
|
|
267
|
+
/**
|
|
268
|
+
* Job is `team` and nothing this device verified admits the job's owner.
|
|
269
|
+
*
|
|
270
|
+
* The id predates the grant and is kept, because ids are public and cited
|
|
271
|
+
* by conformance output. What it means has not moved: this device was not
|
|
272
|
+
* shown anything it could check.
|
|
273
|
+
*/
|
|
268
274
|
"not-locally-allowed",
|
|
269
|
-
/** Job is `
|
|
275
|
+
/** Job is `team` but the server's own allowlist excludes this runner. */
|
|
270
276
|
"not-in-server-allowlist",
|
|
271
|
-
/** The
|
|
277
|
+
/** The service offers only `private` and the job belongs to someone else. */
|
|
272
278
|
"offer-scope-too-narrow",
|
|
273
|
-
/** The matched backend is subscription-class, which is locked to `
|
|
279
|
+
/** The matched backend is subscription-class, which is locked to `private`. */
|
|
274
280
|
"subscription-self-lock",
|
|
275
281
|
/** The backend spends the owner's money and they have not agreed to share it. */
|
|
276
282
|
"metered-no-spend-consent",
|
|
@@ -315,17 +321,15 @@ function matchAudience(job, daemon) {
|
|
|
315
321
|
case "private":
|
|
316
322
|
return refuse("offer-scope-too-narrow");
|
|
317
323
|
case "team":
|
|
318
|
-
return daemon.
|
|
319
|
-
case "public":
|
|
320
|
-
return ALLOWED;
|
|
324
|
+
return daemon.admits(job.owner) ? ALLOWED : refuse("not-locally-allowed");
|
|
321
325
|
}
|
|
322
326
|
}
|
|
323
327
|
var REFUSAL_MESSAGES = Object.freeze({
|
|
324
328
|
"no-capability": "no backend on this device is configured and healthy for that job kind",
|
|
325
329
|
"audience-self-other-owner": "the job is private to its owner and this device is paired to someone else",
|
|
326
|
-
"not-locally-allowed": "
|
|
330
|
+
"not-locally-allowed": "nothing this device can verify says the job's owner may use it",
|
|
327
331
|
"not-in-server-allowlist": "the app restricted this job to named runners and this device is not one of them",
|
|
328
|
-
"offer-scope-too-narrow": "this
|
|
332
|
+
"offer-scope-too-narrow": "this service is offered to its owner only (`byollm offer <service> team` to widen)",
|
|
329
333
|
"subscription-self-lock": "subscription-backed models run their owner's work only \u2014 this is a protocol rule, not a setting",
|
|
330
334
|
"metered-no-spend-consent": "this backend bills its owner per token, and they have not agreed to spend it on other people's work",
|
|
331
335
|
"metered-ceiling-reached": "this backend is shared but has reached the spend ceiling its owner set"
|
|
@@ -344,7 +348,7 @@ var PAYLOAD_LIMITS = Object.freeze({
|
|
|
344
348
|
var ChatMessage = z3.object({
|
|
345
349
|
role: z3.enum(["system", "user", "assistant"]),
|
|
346
350
|
content: z3.string().max(PAYLOAD_LIMITS.maxTextChars)
|
|
347
|
-
});
|
|
351
|
+
}).strict();
|
|
348
352
|
var GeneratePayload = z3.object({
|
|
349
353
|
prompt: z3.string().min(1).max(PAYLOAD_LIMITS.maxTextChars),
|
|
350
354
|
system: z3.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
|
|
@@ -366,8 +370,11 @@ var ChatPayload = z3.object({
|
|
|
366
370
|
var JobKind = z3.enum(["llm.generate", "llm.chat"]);
|
|
367
371
|
var JOB_KINDS = Object.freeze(JobKind.options);
|
|
368
372
|
var KindedPayload = z3.discriminatedUnion("kind", [
|
|
369
|
-
|
|
370
|
-
|
|
373
|
+
// Strict on the wrappers too. A union member that strips is a door beside
|
|
374
|
+
// the one that is locked: the payloads inside are strict, and an extra key
|
|
375
|
+
// on the envelope vanished just as quietly.
|
|
376
|
+
z3.object({ kind: z3.literal("llm.generate"), payload: GeneratePayload }).strict(),
|
|
377
|
+
z3.object({ kind: z3.literal("llm.chat"), payload: ChatPayload }).strict()
|
|
371
378
|
]);
|
|
372
379
|
function isJobKind(value) {
|
|
373
380
|
return JOB_KINDS.includes(value);
|
|
@@ -384,8 +391,243 @@ function payloadTextLength(kinded) {
|
|
|
384
391
|
}
|
|
385
392
|
|
|
386
393
|
// src/job.ts
|
|
394
|
+
import { z as z6 } from "zod";
|
|
395
|
+
|
|
396
|
+
// src/grant.ts
|
|
397
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
398
|
+
import { z as z5 } from "zod";
|
|
399
|
+
|
|
400
|
+
// src/keys.ts
|
|
401
|
+
import {
|
|
402
|
+
createHash,
|
|
403
|
+
createPrivateKey,
|
|
404
|
+
createPublicKey,
|
|
405
|
+
generateKeyPairSync,
|
|
406
|
+
sign,
|
|
407
|
+
verify
|
|
408
|
+
} from "crypto";
|
|
387
409
|
import { z as z4 } from "zod";
|
|
388
|
-
var
|
|
410
|
+
var PublicIdentity = z4.object({
|
|
411
|
+
/** Raw Ed25519 public key. The pinned one. */
|
|
412
|
+
identity: z4.string().min(1),
|
|
413
|
+
/** Raw X25519 public key, for sealing to this party. */
|
|
414
|
+
encryption: z4.string().min(1),
|
|
415
|
+
/**
|
|
416
|
+
* Ed25519 signature over the encryption key, by the identity key.
|
|
417
|
+
*
|
|
418
|
+
* This is what stops an upstream substituting an encryption key of its
|
|
419
|
+
* own while relaying a genuine identity: the receiver pins the identity
|
|
420
|
+
* and refuses any encryption key not signed by it.
|
|
421
|
+
*/
|
|
422
|
+
encryptionSig: z4.string().min(1)
|
|
423
|
+
}).strict();
|
|
424
|
+
var StoredKeys = z4.object({
|
|
425
|
+
version: z4.literal(1),
|
|
426
|
+
identityPublic: z4.string().min(1),
|
|
427
|
+
identityPrivate: z4.string().min(1),
|
|
428
|
+
encryptionPublic: z4.string().min(1),
|
|
429
|
+
encryptionPrivate: z4.string().min(1),
|
|
430
|
+
encryptionSig: z4.string().min(1),
|
|
431
|
+
createdAt: z4.number().int().positive()
|
|
432
|
+
}).strict();
|
|
433
|
+
var ENCRYPTION_KEY_CONTEXT = "byollm/v1/encryption-key";
|
|
434
|
+
function rawPublic(key) {
|
|
435
|
+
const jwk = key.export({ format: "jwk" });
|
|
436
|
+
const x = jwk.x;
|
|
437
|
+
if (typeof x !== "string") throw new Error("key has no raw public component");
|
|
438
|
+
return x;
|
|
439
|
+
}
|
|
440
|
+
function importPublic(raw, crv) {
|
|
441
|
+
return createPublicKey({ key: { kty: "OKP", crv, x: raw }, format: "jwk" });
|
|
442
|
+
}
|
|
443
|
+
function importPrivate(stored) {
|
|
444
|
+
return createPrivateKey({
|
|
445
|
+
key: Buffer.from(stored, "base64"),
|
|
446
|
+
type: "pkcs8",
|
|
447
|
+
format: "der"
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
var exportPrivate = (key) => key.export({ type: "pkcs8", format: "der" }).toString("base64");
|
|
451
|
+
function generateKeys(now) {
|
|
452
|
+
const identity = generateKeyPairSync("ed25519");
|
|
453
|
+
const encryption = generateKeyPairSync("x25519");
|
|
454
|
+
const encryptionPublic = rawPublic(encryption.publicKey);
|
|
455
|
+
return {
|
|
456
|
+
version: 1,
|
|
457
|
+
identityPublic: rawPublic(identity.publicKey),
|
|
458
|
+
identityPrivate: exportPrivate(identity.privateKey),
|
|
459
|
+
encryptionPublic,
|
|
460
|
+
encryptionPrivate: exportPrivate(encryption.privateKey),
|
|
461
|
+
encryptionSig: sign(
|
|
462
|
+
null,
|
|
463
|
+
Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${encryptionPublic}`),
|
|
464
|
+
identity.privateKey
|
|
465
|
+
).toString("base64url"),
|
|
466
|
+
createdAt: now
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function publicIdentityOf(keys) {
|
|
470
|
+
return {
|
|
471
|
+
identity: keys.identityPublic,
|
|
472
|
+
encryption: keys.encryptionPublic,
|
|
473
|
+
encryptionSig: keys.encryptionSig
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
function verifyPublicIdentity(identity) {
|
|
477
|
+
try {
|
|
478
|
+
return verify(
|
|
479
|
+
null,
|
|
480
|
+
Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${identity.encryption}`),
|
|
481
|
+
importPublic(identity.identity, "Ed25519"),
|
|
482
|
+
Buffer.from(identity.encryptionSig, "base64url")
|
|
483
|
+
);
|
|
484
|
+
} catch {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function signWith(keys, data) {
|
|
489
|
+
return sign(null, data, importPrivate(keys.identityPrivate)).toString(
|
|
490
|
+
"base64url"
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
function verifyWith(identityPublic, data, signature) {
|
|
494
|
+
try {
|
|
495
|
+
return verify(
|
|
496
|
+
null,
|
|
497
|
+
data,
|
|
498
|
+
importPublic(identityPublic, "Ed25519"),
|
|
499
|
+
Buffer.from(signature, "base64url")
|
|
500
|
+
);
|
|
501
|
+
} catch {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
506
|
+
function fingerprint(identityPublic) {
|
|
507
|
+
const digest = createHash("sha256").update(Buffer.from(identityPublic, "base64url")).digest();
|
|
508
|
+
let bits = 0;
|
|
509
|
+
let value = 0;
|
|
510
|
+
let out = "";
|
|
511
|
+
for (const byte of digest.subarray(0, 15)) {
|
|
512
|
+
value = value << 8 | byte;
|
|
513
|
+
bits += 8;
|
|
514
|
+
while (bits >= 5) {
|
|
515
|
+
out += ALPHABET.charAt(value >>> bits - 5 & 31);
|
|
516
|
+
bits -= 5;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const groups = out.match(/.{1,4}/g) ?? [];
|
|
520
|
+
return `BYOLLM-${groups.join("-")}`;
|
|
521
|
+
}
|
|
522
|
+
var keyId = (identityPublic) => fingerprint(identityPublic);
|
|
523
|
+
|
|
524
|
+
// src/grant.ts
|
|
525
|
+
var GRANT_MAX_AGE_MS = 12e4;
|
|
526
|
+
var CLOCK_SKEW_WARN_MS = 3e4;
|
|
527
|
+
var CLOCK_ATTRIBUTION_MS = 5e3;
|
|
528
|
+
var GRANT_CONTEXT = "byollm/v1/grant";
|
|
529
|
+
var SignedGrant = z5.object({
|
|
530
|
+
/**
|
|
531
|
+
* This grant's own id — what makes it single-use.
|
|
532
|
+
*
|
|
533
|
+
* **Not the job id, and the difference is load-bearing.** Binding
|
|
534
|
+
* single-use to `jobId` would refuse a legitimate retry: a claim that
|
|
535
|
+
* times out is re-claimed, the control plane authors a second grant for
|
|
536
|
+
* the same job, and a device that recorded the job id as spent would
|
|
537
|
+
* reject its own recovery. A fresh id per authorship replays nothing and
|
|
538
|
+
* retries fine.
|
|
539
|
+
*/
|
|
540
|
+
grantId: z5.string().min(1),
|
|
541
|
+
/**
|
|
542
|
+
* The job this grant admits, and only this one.
|
|
543
|
+
*
|
|
544
|
+
* A grant lifted from one job and presented for another is the obvious
|
|
545
|
+
* attack, and this field is why it fails.
|
|
546
|
+
*/
|
|
547
|
+
jobId: z5.string().min(1),
|
|
548
|
+
/**
|
|
549
|
+
* The site the work came from, **as a key id** — byollm-review 2026-08-27.
|
|
550
|
+
*
|
|
551
|
+
* This was `siteId`, holding the site's id in the control plane's
|
|
552
|
+
* namespace, and it was signed by the engine and read by nobody. A signed
|
|
553
|
+
* field nobody checks is not a weak guarantee, it is the appearance of
|
|
554
|
+
* one: the design says "the grant carries the site", and nothing anywhere
|
|
555
|
+
* compared it to anything.
|
|
556
|
+
*
|
|
557
|
+
* It could not be compared. Job ids are chosen per site, so a grant
|
|
558
|
+
* authored for (site A, `job_1`) satisfied every device check against a
|
|
559
|
+
* stub naming (site B, `job_1`) — but the device holds sites only by the
|
|
560
|
+
* key ids it pinned, and had no way to relate a control-plane uuid to
|
|
561
|
+
* one. Checking the field would have meant a lookup through the party the
|
|
562
|
+
* grant exists to distrust.
|
|
563
|
+
*
|
|
564
|
+
* So the namespace changes to the one the device already has, and the
|
|
565
|
+
* name changes with it: this is the same value as {@link JobStub.site},
|
|
566
|
+
* compared directly, no lookup and nothing to believe. The control-plane
|
|
567
|
+
* id is not carried alongside — it had no reader, and keeping an
|
|
568
|
+
* unchecked field beside a checked one is how this hole was dug.
|
|
569
|
+
*/
|
|
570
|
+
site: z5.string().min(1),
|
|
571
|
+
/** Whose job it is — the person the site enqueued for. */
|
|
572
|
+
user: z5.string().min(1),
|
|
573
|
+
/**
|
|
574
|
+
* Whose device it is for.
|
|
575
|
+
*
|
|
576
|
+
* Passed to {@link verifyGrant} rather than read out of the document, for
|
|
577
|
+
* the reason every verifier here takes its subject as an argument: a
|
|
578
|
+
* verifier that recovered the owner from the signed bytes would accept a
|
|
579
|
+
* genuine grant belonging to somebody else and pass every check.
|
|
580
|
+
*/
|
|
581
|
+
owner: z5.string().min(1),
|
|
582
|
+
/** The site purpose this job serves — byollm_016 Amendment L. */
|
|
583
|
+
purpose: z5.string().min(1),
|
|
584
|
+
/** The kind of work. */
|
|
585
|
+
kind: z5.string().min(1),
|
|
586
|
+
/**
|
|
587
|
+
* The service the control plane resolved this (purpose, kind) to, from
|
|
588
|
+
* the user's own mapping.
|
|
589
|
+
*
|
|
590
|
+
* Selection is the control plane's; **offer-consistency is the
|
|
591
|
+
* device's**. A device verifies it actually offers this service, at a
|
|
592
|
+
* scope that includes {@link user}, before running anything.
|
|
593
|
+
*/
|
|
594
|
+
service: z5.string().min(1),
|
|
595
|
+
/** When the control plane signed it — epoch ms, the only anchor for age. */
|
|
596
|
+
issuedAt: z5.number().int().positive(),
|
|
597
|
+
/** Base64url Ed25519 over {@link grantStatement}. */
|
|
598
|
+
signature: z5.string().min(1)
|
|
599
|
+
}).strict();
|
|
600
|
+
var GRANT_SIGNED_FIELDS = Object.freeze(
|
|
601
|
+
Object.keys(SignedGrant.shape).filter((key) => key !== "signature").sort()
|
|
602
|
+
);
|
|
603
|
+
function grantStatement(claims) {
|
|
604
|
+
return Buffer2.from(
|
|
605
|
+
JSON.stringify([
|
|
606
|
+
GRANT_CONTEXT,
|
|
607
|
+
...GRANT_SIGNED_FIELDS.map((field) => claims[field])
|
|
608
|
+
]),
|
|
609
|
+
"utf8"
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
function signGrant(keys, claims) {
|
|
613
|
+
return { ...claims, signature: signWith(keys, grantStatement(claims)) };
|
|
614
|
+
}
|
|
615
|
+
function verifyGrant(input) {
|
|
616
|
+
const { grant, now } = input;
|
|
617
|
+
if (grant.owner !== input.owner) return "wrong-owner";
|
|
618
|
+
if (grant.jobId !== input.jobId) return "wrong-job";
|
|
619
|
+
const age = now - grant.issuedAt;
|
|
620
|
+
if (age < -CLOCK_SKEW_WARN_MS) return "from-the-future";
|
|
621
|
+
if (age > (input.maxAgeMs ?? GRANT_MAX_AGE_MS)) return "expired";
|
|
622
|
+
return verifyWith(
|
|
623
|
+
input.controlPlanePublic,
|
|
624
|
+
grantStatement(grant),
|
|
625
|
+
grant.signature
|
|
626
|
+
) ? null : "bad-signature";
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// src/job.ts
|
|
630
|
+
var JobState = z6.enum([
|
|
389
631
|
"queued",
|
|
390
632
|
"claimed",
|
|
391
633
|
"running",
|
|
@@ -417,7 +659,7 @@ var TRANSITIONS = Object.freeze({
|
|
|
417
659
|
function canTransition(from, to) {
|
|
418
660
|
return TRANSITIONS[from].includes(to);
|
|
419
661
|
}
|
|
420
|
-
var Lease =
|
|
662
|
+
var Lease = z6.object({
|
|
421
663
|
/**
|
|
422
664
|
* Identifies *this* grant, not just its holder.
|
|
423
665
|
*
|
|
@@ -432,20 +674,20 @@ var Lease = z4.object({
|
|
|
432
674
|
* and release *is*, per lease, but not across leases, because nothing in
|
|
433
675
|
* the request said which one.
|
|
434
676
|
*/
|
|
435
|
-
id:
|
|
677
|
+
id: z6.string().min(1),
|
|
436
678
|
/** The runner holding the lease. */
|
|
437
|
-
runnerId:
|
|
679
|
+
runnerId: z6.string().min(1),
|
|
438
680
|
/** Epoch milliseconds after which the claim is void. */
|
|
439
|
-
expiresAt:
|
|
440
|
-
});
|
|
441
|
-
var JobPayload =
|
|
442
|
-
var ClaimedJob =
|
|
443
|
-
id:
|
|
681
|
+
expiresAt: z6.number().int().positive()
|
|
682
|
+
}).strict();
|
|
683
|
+
var JobPayload = z6.union([GeneratePayload, ChatPayload]);
|
|
684
|
+
var ClaimedJob = z6.object({
|
|
685
|
+
id: z6.string().min(1),
|
|
444
686
|
kind: JobKind,
|
|
445
687
|
payload: JobPayload,
|
|
446
688
|
audience: Audience,
|
|
447
689
|
/** The app's id for the user who enqueued it. */
|
|
448
|
-
owner:
|
|
690
|
+
owner: z6.string().min(1),
|
|
449
691
|
/**
|
|
450
692
|
* Which site's job — V1-3.
|
|
451
693
|
*
|
|
@@ -458,39 +700,42 @@ var ClaimedJob = z4.object({
|
|
|
458
700
|
* one, and so this reads as what it is: a fact about where the work came
|
|
459
701
|
* from, not a second copy of the routing key.
|
|
460
702
|
*/
|
|
461
|
-
site:
|
|
703
|
+
site: z6.string().min(1).optional(),
|
|
462
704
|
/**
|
|
463
|
-
* Which
|
|
705
|
+
* Which of the owner's services runs this — resolved, not requested.
|
|
706
|
+
*
|
|
707
|
+
* The daemon picks the backend from this, so it has to be the answer
|
|
708
|
+
* rather than a wish. On a relayed route it is copied off the **grant**,
|
|
709
|
+
* where a control plane put the person's own mapping and signed it; a
|
|
710
|
+
* site never named it and could not.
|
|
464
711
|
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
* service would have been served by the default instead, which is the
|
|
470
|
-
* substitution `NO_PAYLOAD_ROUTING` forbids.
|
|
712
|
+
* It used to be what the site asked for, which made a job that selected a
|
|
713
|
+
* non-default service liable to be served by the default instead — the
|
|
714
|
+
* substitution `NO_PAYLOAD_ROUTING` forbids. Amendment L removed the
|
|
715
|
+
* asking; what is left is the answering.
|
|
471
716
|
*
|
|
472
|
-
* Optional, because
|
|
473
|
-
*
|
|
717
|
+
* Optional, because direct mode has no control plane to resolve anything
|
|
718
|
+
* and the owner's own defaults answer under the ambiguity law.
|
|
474
719
|
*/
|
|
475
|
-
service:
|
|
720
|
+
service: z6.string().min(1).optional(),
|
|
476
721
|
lease: Lease
|
|
477
722
|
}).strict();
|
|
478
|
-
var ResultProvenance =
|
|
723
|
+
var ResultProvenance = z6.object({
|
|
479
724
|
/** The audience the job ran under. */
|
|
480
725
|
audience: Audience,
|
|
481
726
|
/** The runner that produced it. */
|
|
482
|
-
runnerId:
|
|
727
|
+
runnerId: z6.string().min(1),
|
|
483
728
|
/** The runner owner's id in this app's namespace. */
|
|
484
|
-
runnerOwner:
|
|
729
|
+
runnerOwner: z6.string().min(1),
|
|
485
730
|
/** Which backend class produced it — an HTTP call or a sandboxed spawn. */
|
|
486
731
|
backendClass: BackendClass,
|
|
487
732
|
/** The model the runner reports having used. */
|
|
488
|
-
model:
|
|
733
|
+
model: z6.string().min(1),
|
|
489
734
|
/**
|
|
490
735
|
* False only for `self` jobs. When true the app MUST treat `text` as
|
|
491
736
|
* untrusted third-party content.
|
|
492
737
|
*/
|
|
493
|
-
untrusted:
|
|
738
|
+
untrusted: z6.boolean()
|
|
494
739
|
}).strict();
|
|
495
740
|
function provenanceFor(input) {
|
|
496
741
|
return {
|
|
@@ -502,62 +747,47 @@ function provenanceFor(input) {
|
|
|
502
747
|
untrusted: input.audience !== "private"
|
|
503
748
|
};
|
|
504
749
|
}
|
|
505
|
-
var RunMetadata =
|
|
750
|
+
var RunMetadata = z6.object({
|
|
506
751
|
/** Which model actually served it. */
|
|
507
|
-
model:
|
|
752
|
+
model: z6.string().min(1),
|
|
508
753
|
backendClass: BackendClass,
|
|
509
754
|
/** Wall-clock milliseconds the backend call took. */
|
|
510
|
-
durationMs:
|
|
755
|
+
durationMs: z6.number().int().nonnegative()
|
|
511
756
|
}).strict();
|
|
512
|
-
var JobResultOk =
|
|
513
|
-
outcome:
|
|
514
|
-
text:
|
|
757
|
+
var JobResultOk = z6.object({
|
|
758
|
+
outcome: z6.literal("ok"),
|
|
759
|
+
text: z6.string(),
|
|
515
760
|
/** Optional reference to a stored artifact; never a local path. */
|
|
516
|
-
artifactUrl:
|
|
761
|
+
artifactUrl: z6.url().optional()
|
|
517
762
|
}).strict();
|
|
518
|
-
var JobResultError =
|
|
519
|
-
outcome:
|
|
520
|
-
code:
|
|
521
|
-
message:
|
|
763
|
+
var JobResultError = z6.object({
|
|
764
|
+
outcome: z6.literal("error"),
|
|
765
|
+
code: z6.string().min(1),
|
|
766
|
+
message: z6.string().min(1),
|
|
522
767
|
/** Whether the app may reasonably re-enqueue. */
|
|
523
|
-
retryable:
|
|
768
|
+
retryable: z6.boolean()
|
|
524
769
|
}).strict();
|
|
525
|
-
var JobResultCanceled =
|
|
526
|
-
outcome:
|
|
770
|
+
var JobResultCanceled = z6.object({
|
|
771
|
+
outcome: z6.literal("canceled")
|
|
527
772
|
}).strict();
|
|
528
|
-
var JobOutcome =
|
|
773
|
+
var JobOutcome = z6.discriminatedUnion("outcome", [
|
|
529
774
|
JobResultOk,
|
|
530
775
|
JobResultError,
|
|
531
776
|
JobResultCanceled
|
|
532
777
|
]);
|
|
533
|
-
var RefusalReason =
|
|
534
|
-
/**
|
|
535
|
-
* A selection this requester cannot be served — byollm_016 Phase B.
|
|
536
|
-
*
|
|
537
|
-
* **One value for two causes, and the collapse is the security property.**
|
|
538
|
-
* A named service may be unknown to this owner, or known and not offered to
|
|
539
|
-
* this requester. Those are different facts and the requester may learn
|
|
540
|
-
* neither, because telling them apart turns refusal wording into an
|
|
541
|
-
* inventory oracle: probe names, sort the answers, and enumerate a device
|
|
542
|
-
* you were never offered. The finer cause lives owner-side, where the person
|
|
543
|
-
* reading it already owns the machine — see {@link SelectionFailure}.
|
|
544
|
-
*
|
|
545
|
-
* The first draft of this enum had both causes on the wire with a comment
|
|
546
|
-
* claiming they disclosed identically. They did not; the comment described a
|
|
547
|
-
* property the code lacked, which is the more dangerous half of that mistake.
|
|
548
|
-
*/
|
|
549
|
-
"select-unavailable",
|
|
778
|
+
var RefusalReason = z6.enum([
|
|
550
779
|
/**
|
|
551
780
|
* Two or more services answer this kind and the owner has named no default,
|
|
552
781
|
* so the kind is withheld. Nobody may pick on the owner's behalf — the wrong
|
|
553
782
|
* guess is the metered one.
|
|
554
783
|
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
784
|
+
* Told apart from its neighbour deliberately, and the line is whether a
|
|
785
|
+
* requester can walk a namespace. There are two kinds; asking about one
|
|
786
|
+
* enumerates nothing they could not learn from what the device advertises,
|
|
787
|
+
* and the difference is actionable — "the owner has not chosen" is fixable
|
|
788
|
+
* by the owner, "the default cannot serve you" is not. It is also already
|
|
789
|
+
* what a team member sees on the devices page: `awaitingDefault` carries
|
|
790
|
+
* exactly this, by kind, for exactly this reason.
|
|
561
791
|
*/
|
|
562
792
|
"default-ambiguity",
|
|
563
793
|
/**
|
|
@@ -566,34 +796,28 @@ var RefusalReason = z4.enum([
|
|
|
566
796
|
*
|
|
567
797
|
* The specimen: an owner's default for `llm.chat` is their Claude
|
|
568
798
|
* subscription, self-locked by `SUBSCRIPTION_SELF_LOCK`. A team member's
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
799
|
+
* job resolves to it and can never be served by it. That must be a refusal
|
|
800
|
+
* on the spot, not a wait that expires an hour later looking like nobody
|
|
801
|
+
* was online.
|
|
572
802
|
*
|
|
573
|
-
* Bounded like the value above and
|
|
574
|
-
*
|
|
803
|
+
* Bounded like the value above, and unprobeable for the same reason: the
|
|
804
|
+
* requester named nothing, so there is no name space to walk.
|
|
575
805
|
*/
|
|
576
806
|
"default-unusable"
|
|
577
807
|
]);
|
|
578
|
-
var JobRefused =
|
|
579
|
-
outcome:
|
|
808
|
+
var JobRefused = z6.object({
|
|
809
|
+
outcome: z6.literal("refused"),
|
|
580
810
|
reason: RefusalReason,
|
|
581
811
|
/** Plain words for a human reading a log, never parsed. */
|
|
582
|
-
message:
|
|
812
|
+
message: z6.string().min(1)
|
|
583
813
|
}).strict();
|
|
584
814
|
var REFUSAL_TEXT = Object.freeze({
|
|
585
|
-
"select-unavailable": "that service is not available to you on this device",
|
|
586
815
|
"default-ambiguity": "this device serves that kind from more than one service and its owner has not chosen which",
|
|
587
816
|
"default-unusable": "this device's default for that kind cannot run work for you"
|
|
588
817
|
});
|
|
589
|
-
var
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
message: REFUSAL_TEXT["select-unavailable"]
|
|
593
|
-
});
|
|
594
|
-
var SealedOutcome = z4.object({ outcome: JobOutcome, ran: RunMetadata }).strict();
|
|
595
|
-
var DeliveredResult = z4.object({
|
|
596
|
-
jobId: z4.string().min(1),
|
|
818
|
+
var SealedOutcome = z6.object({ outcome: JobOutcome, ran: RunMetadata }).strict();
|
|
819
|
+
var DeliveredResult = z6.object({
|
|
820
|
+
jobId: z6.string().min(1),
|
|
597
821
|
state: JobState,
|
|
598
822
|
outcome: JobOutcome.optional(),
|
|
599
823
|
provenance: ResultProvenance.optional(),
|
|
@@ -611,9 +835,9 @@ var DeliveredResult = z4.object({
|
|
|
611
835
|
* runner ran the job, and the *server* stamps it — an app cannot supply
|
|
612
836
|
* a substitute that hides what it is.
|
|
613
837
|
*/
|
|
614
|
-
fallback:
|
|
838
|
+
fallback: z6.literal(true).optional()
|
|
615
839
|
}).strict();
|
|
616
|
-
var SizeClass =
|
|
840
|
+
var SizeClass = z6.enum(["small", "medium", "large", "unbounded"]);
|
|
617
841
|
var SIZE_CLASS_LIMITS = Object.freeze({
|
|
618
842
|
small: 4e3,
|
|
619
843
|
medium: 64e3,
|
|
@@ -628,11 +852,11 @@ function sizeClassOf(textChars) {
|
|
|
628
852
|
if (textChars <= SIZE_CLASS_LIMITS.medium) return "medium";
|
|
629
853
|
return "large";
|
|
630
854
|
}
|
|
631
|
-
var JobStub =
|
|
632
|
-
id:
|
|
855
|
+
var JobStub = z6.object({
|
|
856
|
+
id: z6.string().min(1),
|
|
633
857
|
kind: JobKind,
|
|
634
858
|
/** The app's id for the user who enqueued it. */
|
|
635
|
-
owner:
|
|
859
|
+
owner: z6.string().min(1),
|
|
636
860
|
/**
|
|
637
861
|
* Which site this job belongs to — byollm_009 Amendment A §A.3.
|
|
638
862
|
*
|
|
@@ -656,13 +880,13 @@ var JobStub = z4.object({
|
|
|
656
880
|
* overlap window, and a daemon re-keys its own map by verifying that
|
|
657
881
|
* signature against the key it already pinned (§A.3.1).
|
|
658
882
|
*/
|
|
659
|
-
site:
|
|
883
|
+
site: z6.string().min(1),
|
|
660
884
|
audience: Audience,
|
|
661
885
|
// `audienceAllow` is **not** here, and its absence is the enforcement —
|
|
662
886
|
// cloud_008 §0.2.
|
|
663
887
|
//
|
|
664
888
|
// It was a list of the people who may run a job, travelling to every
|
|
665
|
-
// routing party on every
|
|
889
|
+
// routing party on every shared job. byollm_001 Rev 1 §B settled who
|
|
666
890
|
// decides that long before this schema existed: *the daemon's own list
|
|
667
891
|
// decides, not the server's*, and `allowlist.predicateFor(origin)` is the
|
|
668
892
|
// enforcement in both lanes. So this was a second answer to a question the
|
|
@@ -679,181 +903,68 @@ var JobStub = z4.object({
|
|
|
679
903
|
// with it before offering. That is server-internal, where the party
|
|
680
904
|
// holding the list authored it.
|
|
681
905
|
/**
|
|
682
|
-
* Which of the
|
|
683
|
-
*
|
|
684
|
-
* **A
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
906
|
+
* Which of the site's declared purposes this job serves — Amendment L.
|
|
907
|
+
*
|
|
908
|
+
* **A need, never a name.** The site's vocabulary is its own purposes;
|
|
909
|
+
* the person's is their services; and the two never meet. This field says
|
|
910
|
+
* "writing-assistant", and a control plane joins it to whatever that
|
|
911
|
+
* person mapped it to. The site learns only whether the slot was
|
|
912
|
+
* satisfiable.
|
|
913
|
+
*
|
|
914
|
+
* It replaced `service`, which let a site name one of the owner's
|
|
915
|
+
* services directly. That field is gone from both routes (Amendment L
|
|
916
|
+
* rider) and its refusal machinery with it — including the collapsed
|
|
917
|
+
* `select-unavailable`, which existed so that "no such service" and "not
|
|
918
|
+
* offered to you" could not be told apart. There is nothing left to
|
|
919
|
+
* probe: **a vocabulary that never crosses the boundary cannot be
|
|
920
|
+
* enumerated across it**, which is a stronger guarantee than the one the
|
|
921
|
+
* collapse gave.
|
|
922
|
+
*
|
|
923
|
+
* It travels for the reason the absent `audienceAllow` establishes: *a
|
|
924
|
+
* class the router acts on may travel; membership never does.* A purpose
|
|
925
|
+
* is a class, and the control plane acts on it.
|
|
926
|
+
*
|
|
927
|
+
* Optional because direct mode has no control plane to hold a mapping and
|
|
928
|
+
* is kind-only: the owner's own config and defaults answer, under the
|
|
929
|
+
* ambiguity law as shipped. Absent on a relayed route resolves against
|
|
930
|
+
* the site's reserved purpose, which a site that declared its own
|
|
931
|
+
* purposes will not have mapped — so the slot reads as unmapped and the
|
|
932
|
+
* site falls back, loudly enough and without a special case.
|
|
933
|
+
*
|
|
934
|
+
* A **stub** field and never a payload field, which is the line
|
|
701
935
|
* `NO_PAYLOAD_ROUTING` draws: the prompt cannot reach it, so no amount of
|
|
702
936
|
* user text can influence what runs.
|
|
703
937
|
*/
|
|
704
|
-
|
|
938
|
+
purpose: z6.string().min(1).optional(),
|
|
705
939
|
sizeClass: SizeClass,
|
|
706
940
|
/** Reserved for byollm_006. False until streaming exists. */
|
|
707
|
-
streaming:
|
|
941
|
+
streaming: z6.boolean(),
|
|
708
942
|
/** Epoch ms after which the work is pointless; bounds ciphertext retention. */
|
|
709
|
-
deadlineAt:
|
|
943
|
+
deadlineAt: z6.number().int().positive()
|
|
944
|
+
}).strict();
|
|
945
|
+
var ClaimedStub = JobStub.extend({
|
|
946
|
+
lease: Lease,
|
|
947
|
+
grant: SignedGrant.optional()
|
|
710
948
|
}).strict();
|
|
711
|
-
var ClaimedStub = JobStub.extend({ lease: Lease }).strict();
|
|
712
949
|
|
|
713
950
|
// src/envelope.ts
|
|
714
951
|
import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2 } from "crypto";
|
|
715
952
|
import sodium from "libsodium-wrappers";
|
|
716
|
-
import { z as
|
|
717
|
-
|
|
718
|
-
// src/keys.ts
|
|
719
|
-
import {
|
|
720
|
-
createHash,
|
|
721
|
-
createPrivateKey,
|
|
722
|
-
createPublicKey,
|
|
723
|
-
generateKeyPairSync,
|
|
724
|
-
sign,
|
|
725
|
-
verify
|
|
726
|
-
} from "crypto";
|
|
727
|
-
import { z as z5 } from "zod";
|
|
728
|
-
var PublicIdentity = z5.object({
|
|
729
|
-
/** Raw Ed25519 public key. The pinned one. */
|
|
730
|
-
identity: z5.string().min(1),
|
|
731
|
-
/** Raw X25519 public key, for sealing to this party. */
|
|
732
|
-
encryption: z5.string().min(1),
|
|
733
|
-
/**
|
|
734
|
-
* Ed25519 signature over the encryption key, by the identity key.
|
|
735
|
-
*
|
|
736
|
-
* This is what stops an upstream substituting an encryption key of its
|
|
737
|
-
* own while relaying a genuine identity: the receiver pins the identity
|
|
738
|
-
* and refuses any encryption key not signed by it.
|
|
739
|
-
*/
|
|
740
|
-
encryptionSig: z5.string().min(1)
|
|
741
|
-
}).strict();
|
|
742
|
-
var StoredKeys = z5.object({
|
|
743
|
-
version: z5.literal(1),
|
|
744
|
-
identityPublic: z5.string().min(1),
|
|
745
|
-
identityPrivate: z5.string().min(1),
|
|
746
|
-
encryptionPublic: z5.string().min(1),
|
|
747
|
-
encryptionPrivate: z5.string().min(1),
|
|
748
|
-
encryptionSig: z5.string().min(1),
|
|
749
|
-
createdAt: z5.number().int().positive()
|
|
750
|
-
}).strict();
|
|
751
|
-
var ENCRYPTION_KEY_CONTEXT = "byollm/v1/encryption-key";
|
|
752
|
-
function rawPublic(key) {
|
|
753
|
-
const jwk = key.export({ format: "jwk" });
|
|
754
|
-
const x = jwk.x;
|
|
755
|
-
if (typeof x !== "string") throw new Error("key has no raw public component");
|
|
756
|
-
return x;
|
|
757
|
-
}
|
|
758
|
-
function importPublic(raw, crv) {
|
|
759
|
-
return createPublicKey({ key: { kty: "OKP", crv, x: raw }, format: "jwk" });
|
|
760
|
-
}
|
|
761
|
-
function importPrivate(stored) {
|
|
762
|
-
return createPrivateKey({
|
|
763
|
-
key: Buffer.from(stored, "base64"),
|
|
764
|
-
type: "pkcs8",
|
|
765
|
-
format: "der"
|
|
766
|
-
});
|
|
767
|
-
}
|
|
768
|
-
var exportPrivate = (key) => key.export({ type: "pkcs8", format: "der" }).toString("base64");
|
|
769
|
-
function generateKeys(now) {
|
|
770
|
-
const identity = generateKeyPairSync("ed25519");
|
|
771
|
-
const encryption = generateKeyPairSync("x25519");
|
|
772
|
-
const encryptionPublic = rawPublic(encryption.publicKey);
|
|
773
|
-
return {
|
|
774
|
-
version: 1,
|
|
775
|
-
identityPublic: rawPublic(identity.publicKey),
|
|
776
|
-
identityPrivate: exportPrivate(identity.privateKey),
|
|
777
|
-
encryptionPublic,
|
|
778
|
-
encryptionPrivate: exportPrivate(encryption.privateKey),
|
|
779
|
-
encryptionSig: sign(
|
|
780
|
-
null,
|
|
781
|
-
Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${encryptionPublic}`),
|
|
782
|
-
identity.privateKey
|
|
783
|
-
).toString("base64url"),
|
|
784
|
-
createdAt: now
|
|
785
|
-
};
|
|
786
|
-
}
|
|
787
|
-
function publicIdentityOf(keys) {
|
|
788
|
-
return {
|
|
789
|
-
identity: keys.identityPublic,
|
|
790
|
-
encryption: keys.encryptionPublic,
|
|
791
|
-
encryptionSig: keys.encryptionSig
|
|
792
|
-
};
|
|
793
|
-
}
|
|
794
|
-
function verifyPublicIdentity(identity) {
|
|
795
|
-
try {
|
|
796
|
-
return verify(
|
|
797
|
-
null,
|
|
798
|
-
Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${identity.encryption}`),
|
|
799
|
-
importPublic(identity.identity, "Ed25519"),
|
|
800
|
-
Buffer.from(identity.encryptionSig, "base64url")
|
|
801
|
-
);
|
|
802
|
-
} catch {
|
|
803
|
-
return false;
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
function signWith(keys, data) {
|
|
807
|
-
return sign(null, data, importPrivate(keys.identityPrivate)).toString(
|
|
808
|
-
"base64url"
|
|
809
|
-
);
|
|
810
|
-
}
|
|
811
|
-
function verifyWith(identityPublic, data, signature) {
|
|
812
|
-
try {
|
|
813
|
-
return verify(
|
|
814
|
-
null,
|
|
815
|
-
data,
|
|
816
|
-
importPublic(identityPublic, "Ed25519"),
|
|
817
|
-
Buffer.from(signature, "base64url")
|
|
818
|
-
);
|
|
819
|
-
} catch {
|
|
820
|
-
return false;
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
824
|
-
function fingerprint(identityPublic) {
|
|
825
|
-
const digest = createHash("sha256").update(Buffer.from(identityPublic, "base64url")).digest();
|
|
826
|
-
let bits = 0;
|
|
827
|
-
let value = 0;
|
|
828
|
-
let out = "";
|
|
829
|
-
for (const byte of digest.subarray(0, 15)) {
|
|
830
|
-
value = value << 8 | byte;
|
|
831
|
-
bits += 8;
|
|
832
|
-
while (bits >= 5) {
|
|
833
|
-
out += ALPHABET.charAt(value >>> bits - 5 & 31);
|
|
834
|
-
bits -= 5;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
const groups = out.match(/.{1,4}/g) ?? [];
|
|
838
|
-
return `BYOLLM-${groups.join("-")}`;
|
|
839
|
-
}
|
|
840
|
-
var keyId = (identityPublic) => fingerprint(identityPublic);
|
|
841
|
-
|
|
842
|
-
// src/envelope.ts
|
|
953
|
+
import { z as z7 } from "zod";
|
|
843
954
|
var readied;
|
|
844
955
|
async function cryptoReady() {
|
|
845
956
|
readied ??= sodium.ready;
|
|
846
957
|
await readied;
|
|
847
958
|
}
|
|
848
959
|
var ENVELOPE_MAX_AGE_MS = 24 * 60 * 6e4;
|
|
849
|
-
var EnvelopeDirection =
|
|
850
|
-
var SealedEnvelope =
|
|
960
|
+
var EnvelopeDirection = z7.enum(["payload", "result"]);
|
|
961
|
+
var SealedEnvelope = z7.object({
|
|
851
962
|
/** Base64url `crypto_box_seal` output over the signed plaintext. */
|
|
852
|
-
ciphertext:
|
|
963
|
+
ciphertext: z7.string().min(1),
|
|
853
964
|
/** Who this was sealed to — the recipient checks it is them. */
|
|
854
|
-
recipientKeyId:
|
|
965
|
+
recipientKeyId: z7.string().min(1),
|
|
855
966
|
/** Who signed it — the recipient checks this against its pin. */
|
|
856
|
-
senderKeyId:
|
|
967
|
+
senderKeyId: z7.string().min(1),
|
|
857
968
|
direction: EnvelopeDirection,
|
|
858
969
|
/**
|
|
859
970
|
* When this ciphertext stops being worth keeping.
|
|
@@ -867,7 +978,7 @@ var SealedEnvelope = z6.object({
|
|
|
867
978
|
* Not trusted as written: it is also inside the signature, so a changed
|
|
868
979
|
* deadline fails to verify.
|
|
869
980
|
*/
|
|
870
|
-
deadlineAt:
|
|
981
|
+
deadlineAt: z7.number().int().positive()
|
|
871
982
|
}).strict();
|
|
872
983
|
function signedBody(context, plaintext) {
|
|
873
984
|
return Buffer.from(
|
|
@@ -962,15 +1073,15 @@ async function open(input) {
|
|
|
962
1073
|
|
|
963
1074
|
// src/signing.ts
|
|
964
1075
|
import { createHash as createHash2 } from "crypto";
|
|
965
|
-
import { z as
|
|
1076
|
+
import { z as z8 } from "zod";
|
|
966
1077
|
var MAX_CLOCK_SKEW_MS = 12e4;
|
|
967
|
-
var RequestSignature =
|
|
1078
|
+
var RequestSignature = z8.object({
|
|
968
1079
|
/** Which runner is calling. The server looks up its pinned identity. */
|
|
969
|
-
runnerId:
|
|
1080
|
+
runnerId: z8.string().min(1),
|
|
970
1081
|
/** Epoch ms, bounded by {@link MAX_CLOCK_SKEW_MS}. */
|
|
971
|
-
issuedAt:
|
|
1082
|
+
issuedAt: z8.number().int().positive(),
|
|
972
1083
|
/** Base64url Ed25519 signature over {@link canonicalRequest}. */
|
|
973
|
-
signature:
|
|
1084
|
+
signature: z8.string().min(1)
|
|
974
1085
|
}).strict();
|
|
975
1086
|
function canonicalRequest(input) {
|
|
976
1087
|
const digest = createHash2("sha256").update(input.body, "utf8").digest("hex");
|
|
@@ -1023,73 +1134,62 @@ function verifyRequest(input) {
|
|
|
1023
1134
|
return ok ? null : "bad-signature";
|
|
1024
1135
|
}
|
|
1025
1136
|
|
|
1026
|
-
// src/
|
|
1027
|
-
import {
|
|
1028
|
-
|
|
1029
|
-
var
|
|
1030
|
-
var
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1137
|
+
// src/manifest.ts
|
|
1138
|
+
import { z as z9 } from "zod";
|
|
1139
|
+
var RESERVED_PURPOSE = "default";
|
|
1140
|
+
var RENDERABLE = /^[^\p{Cc}\p{Cf}\p{Cs}\p{Co}]+$/u;
|
|
1141
|
+
var renderable = (max, what) => z9.string().min(1).max(max).regex(
|
|
1142
|
+
RENDERABLE,
|
|
1143
|
+
`a ${what} is text a person reads \u2014 no control characters, direction overrides or zero-width padding`
|
|
1144
|
+
).refine((value) => value.trim() !== "", {
|
|
1145
|
+
message: `a ${what} cannot be blank`
|
|
1146
|
+
});
|
|
1147
|
+
var PurposeKey = z9.string().regex(
|
|
1148
|
+
/^[a-z0-9][a-z0-9-]*$/,
|
|
1149
|
+
"a purpose key is a lowercase slug \u2014 letters, digits and hyphens"
|
|
1150
|
+
).max(64);
|
|
1151
|
+
var Purpose = z9.object({
|
|
1034
1152
|
/**
|
|
1035
|
-
*
|
|
1153
|
+
* What a person reads on the consent screen. The only rendered field.
|
|
1036
1154
|
*
|
|
1037
|
-
*
|
|
1038
|
-
*
|
|
1039
|
-
*
|
|
1155
|
+
* Declared rather than derived from the key, because a key is a
|
|
1156
|
+
* compromise between machines and this is not. "Writing Assistant" is
|
|
1157
|
+
* what somebody understands; `writing-assistant` is what travels.
|
|
1040
1158
|
*/
|
|
1041
|
-
|
|
1159
|
+
label: renderable(80, "label"),
|
|
1160
|
+
/** One line of context for the consent screen. Optional. */
|
|
1161
|
+
description: renderable(280, "description").optional(),
|
|
1042
1162
|
/**
|
|
1043
|
-
*
|
|
1044
|
-
* anchor for age.
|
|
1163
|
+
* The kinds this purpose uses.
|
|
1045
1164
|
*
|
|
1046
|
-
*
|
|
1047
|
-
*
|
|
1048
|
-
*
|
|
1165
|
+
* A purpose may span kinds, and a mapping is per (purpose, kind) — so a
|
|
1166
|
+
* person can send this purpose's chat to one service and its generation
|
|
1167
|
+
* to another. Listing a kind here is what makes that slot appear.
|
|
1049
1168
|
*/
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1169
|
+
kinds: z9.array(JobKind).min(1).max(JOB_KINDS.length).refine((kinds) => new Set(kinds).size === kinds.length, {
|
|
1170
|
+
message: "a purpose lists each kind once"
|
|
1171
|
+
})
|
|
1053
1172
|
}).strict();
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
);
|
|
1064
|
-
}
|
|
1065
|
-
function signRoster(keys, input) {
|
|
1066
|
-
const members = [...input.members].sort();
|
|
1173
|
+
var MAX_PURPOSES = 32;
|
|
1174
|
+
var Manifest = z9.record(PurposeKey, Purpose).refine((manifest) => Object.keys(manifest).length > 0, {
|
|
1175
|
+
message: "a manifest declares at least one purpose"
|
|
1176
|
+
}).refine((manifest) => Object.keys(manifest).length <= MAX_PURPOSES, {
|
|
1177
|
+
message: `a manifest declares at most ${String(MAX_PURPOSES)} purposes \u2014 a consent screen is a set of questions somebody answers one at a time`
|
|
1178
|
+
}).refine((manifest) => !(RESERVED_PURPOSE in manifest), {
|
|
1179
|
+
message: `"${RESERVED_PURPOSE}" is reserved for a site that declares no purposes of its own \u2014 give this one a name from your own vocabulary`
|
|
1180
|
+
});
|
|
1181
|
+
function singlePurposeManifest(input) {
|
|
1067
1182
|
return {
|
|
1068
|
-
|
|
1069
|
-
members,
|
|
1070
|
-
issuedAt: input.issuedAt,
|
|
1071
|
-
signature: signWith(keys, rosterStatement({ ...input, members }))
|
|
1183
|
+
[RESERVED_PURPOSE]: { label: input.label, kinds: [...input.kinds] }
|
|
1072
1184
|
};
|
|
1073
1185
|
}
|
|
1074
|
-
function verifyRoster(input) {
|
|
1075
|
-
const { roster, owner, now } = input;
|
|
1076
|
-
if (roster.owner !== owner) return "wrong-owner";
|
|
1077
|
-
const age = now - roster.issuedAt;
|
|
1078
|
-
if (age < 0) return "from-the-future";
|
|
1079
|
-
if (age > (input.maxAgeMs ?? ROSTER_MAX_AGE_MS)) return "stale";
|
|
1080
|
-
return verifyWith(
|
|
1081
|
-
input.controlPlanePublic,
|
|
1082
|
-
rosterStatement(roster),
|
|
1083
|
-
roster.signature
|
|
1084
|
-
) ? null : "bad-signature";
|
|
1085
|
-
}
|
|
1086
1186
|
|
|
1087
1187
|
// src/succession.ts
|
|
1088
|
-
import { z as
|
|
1188
|
+
import { z as z10 } from "zod";
|
|
1089
1189
|
var SUCCESSION_CONTEXT = "byollm/v1/site-succession";
|
|
1090
1190
|
var RETIREMENT_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1091
1191
|
var MAX_SUCCESSION_CHAIN = 64;
|
|
1092
|
-
var Succession =
|
|
1192
|
+
var Succession = z10.object({
|
|
1093
1193
|
/**
|
|
1094
1194
|
* The predecessor's public identity — K1, in full.
|
|
1095
1195
|
*
|
|
@@ -1099,7 +1199,7 @@ var Succession = z9.object({
|
|
|
1099
1199
|
*/
|
|
1100
1200
|
identity: PublicIdentity,
|
|
1101
1201
|
/** K1's signature over the statement naming K1 and its successor. */
|
|
1102
|
-
signature:
|
|
1202
|
+
signature: z10.string().min(1)
|
|
1103
1203
|
}).strict();
|
|
1104
1204
|
function successionStatement(fromKeyId, toKeyId) {
|
|
1105
1205
|
return Buffer.from(`${SUCCESSION_CONTEXT}:${fromKeyId}:${toKeyId}`);
|
|
@@ -1193,13 +1293,15 @@ var MUSTS = Object.freeze({
|
|
|
1193
1293
|
}),
|
|
1194
1294
|
SITES_LOCALLY_APPROVED: must({
|
|
1195
1295
|
id: "SITES_LOCALLY_APPROVED",
|
|
1196
|
-
statement: "A daemon MUST NOT run work for a site
|
|
1296
|
+
statement: "A daemon MUST NOT run work for a site on an upstream's word alone. An upstream may propose a site set; work for any site in it MUST additionally carry a grant signed by the control-plane key this daemon pinned at pairing. A key that has changed for an id this daemon already pinned MUST be refused for the life of the pairing, including after that id has left the set and returned. A **verified succession** is not a changed key: a new key id carrying a signature, by a key this daemon has already pinned, over a statement naming both key ids MUST be accepted \u2014 provided the control plane projects the same successor \u2014 and MUST be announced rather than applied silently. The first job from a site this daemon has never served MUST be announced at the machine.",
|
|
1197
1297
|
enforcedBy: "daemon",
|
|
1198
1298
|
// Two kinds, and the second is the one that matters — V1-1.
|
|
1199
1299
|
//
|
|
1200
1300
|
// `construction`: the daemon cannot serve a site that is not in its
|
|
1201
|
-
// pinned map, and admission refuses before a payload is fetched
|
|
1202
|
-
//
|
|
1301
|
+
// pinned map, and admission refuses before a payload is fetched — and
|
|
1302
|
+
// since byollm_016 Amendment K, being in the map is no longer sufficient
|
|
1303
|
+
// either: a signed grant is, and the relay proposing the set cannot
|
|
1304
|
+
// produce one.
|
|
1203
1305
|
//
|
|
1204
1306
|
// `adversarial`: the property that survives is about a *sequence* —
|
|
1205
1307
|
// remove the id, re-offer it under a different key — which no honest
|
|
@@ -1352,7 +1454,7 @@ var MUSTS = Object.freeze({
|
|
|
1352
1454
|
}),
|
|
1353
1455
|
NAMED_LOCAL_ALLOWLIST: must({
|
|
1354
1456
|
id: "NAMED_LOCAL_ALLOWLIST",
|
|
1355
|
-
statement: "A '
|
|
1457
|
+
statement: "A 'team' job MUST be admitted only by something the device itself verified, keyed by (server origin, user id) \u2014 never on the routing party's assertion alone.",
|
|
1356
1458
|
enforcedBy: "daemon",
|
|
1357
1459
|
verifiedBy: "conformance",
|
|
1358
1460
|
source: "byollm_001 Rev 1 \xA7B"
|
|
@@ -1578,7 +1680,7 @@ function mustsVerifiedBy(kind) {
|
|
|
1578
1680
|
}
|
|
1579
1681
|
|
|
1580
1682
|
// src/wire.ts
|
|
1581
|
-
import { z as
|
|
1683
|
+
import { z as z11 } from "zod";
|
|
1582
1684
|
var PROTOCOL_VERSION = "0";
|
|
1583
1685
|
var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze([
|
|
1584
1686
|
PROTOCOL_VERSION
|
|
@@ -1621,47 +1723,45 @@ var ENDPOINTS = Object.freeze([
|
|
|
1621
1723
|
"result",
|
|
1622
1724
|
"release"
|
|
1623
1725
|
]);
|
|
1624
|
-
var Capability =
|
|
1726
|
+
var Capability = z11.object({
|
|
1625
1727
|
kind: JobKind,
|
|
1626
1728
|
/**
|
|
1627
1729
|
* The owner's name for the service answering this kind — byollm_016.
|
|
1628
1730
|
*
|
|
1629
1731
|
* A device advertises *which* of its services serves a kind, not merely
|
|
1630
|
-
* that something does.
|
|
1631
|
-
* it is what a
|
|
1732
|
+
* that something does. **A site never sees this**, and never did after
|
|
1733
|
+
* Amendment L: it is what a control plane resolves a person's mapping
|
|
1734
|
+
* against, so that the service a grant names is one this device actually
|
|
1735
|
+
* offers rather than one somebody invented.
|
|
1736
|
+
*
|
|
1737
|
+
* `isDefault` used to sit beside it, saying which row an unselected job
|
|
1738
|
+
* took. Nothing selects any more — a job names a purpose and a person's
|
|
1739
|
+
* mapping names a service — so there is no unselected job for a default
|
|
1740
|
+
* to catch, and the field went with the machinery it served.
|
|
1632
1741
|
*/
|
|
1633
|
-
service:
|
|
1634
|
-
/**
|
|
1635
|
-
* Whether this row is the default for its kind.
|
|
1636
|
-
*
|
|
1637
|
-
* Stated rather than inferred from being the only row, which is true in
|
|
1638
|
-
* Phase A and stops being true the moment Phase B advertises every
|
|
1639
|
-
* selectable service per kind. A consumer that learned "default means
|
|
1640
|
-
* alone" would have to unlearn it, and the ones that did not would be
|
|
1641
|
-
* quietly wrong. One field now, no second shape later.
|
|
1642
|
-
*/
|
|
1643
|
-
isDefault: z10.boolean(),
|
|
1742
|
+
service: z11.string().min(1),
|
|
1644
1743
|
backendId: BackendIdSchema,
|
|
1645
1744
|
backendClass: BackendClass,
|
|
1646
|
-
model:
|
|
1745
|
+
model: z11.string().min(1),
|
|
1647
1746
|
offerScope: OfferScope
|
|
1648
1747
|
}).strict();
|
|
1649
|
-
var CapabilityMatrix =
|
|
1650
|
-
var WithheldKind =
|
|
1748
|
+
var CapabilityMatrix = z11.array(Capability);
|
|
1749
|
+
var WithheldKind = z11.object({
|
|
1651
1750
|
kind: JobKind,
|
|
1652
|
-
claimants:
|
|
1653
|
-
|
|
1751
|
+
claimants: z11.array(
|
|
1752
|
+
z11.object({ service: z11.string().min(1), offer: OfferScope }).strict()
|
|
1654
1753
|
).min(2)
|
|
1655
1754
|
}).strict();
|
|
1656
|
-
var
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1755
|
+
var GrantRef = z11.object({ jobId: z11.string().min(1), leaseId: z11.string().min(1) }).strict();
|
|
1756
|
+
var PairStartRequest = z11.object({
|
|
1757
|
+
protocolVersion: z11.literal(PROTOCOL_VERSION),
|
|
1758
|
+
action: z11.literal("start"),
|
|
1759
|
+
daemon: z11.object({
|
|
1760
|
+
version: z11.string().min(1),
|
|
1661
1761
|
/** Shown in the app's runner list so a user can tell their machines apart. */
|
|
1662
|
-
label:
|
|
1663
|
-
platform:
|
|
1664
|
-
}),
|
|
1762
|
+
label: z11.string().min(1).max(120),
|
|
1763
|
+
platform: z11.enum(["darwin", "linux", "win32"])
|
|
1764
|
+
}).strict(),
|
|
1665
1765
|
/**
|
|
1666
1766
|
* This machine's public keys (byollm_009 §5).
|
|
1667
1767
|
*
|
|
@@ -1672,29 +1772,29 @@ var PairStartRequest = z10.object({
|
|
|
1672
1772
|
device: PublicIdentity,
|
|
1673
1773
|
capabilities: CapabilityMatrix
|
|
1674
1774
|
}).strict();
|
|
1675
|
-
var PairStartResponse =
|
|
1775
|
+
var PairStartResponse = z11.object({
|
|
1676
1776
|
/** Secret the daemon polls with. Never shown to the user. */
|
|
1677
|
-
deviceCode:
|
|
1777
|
+
deviceCode: z11.string().min(20),
|
|
1678
1778
|
/** Short code the user reads and confirms in the browser. */
|
|
1679
|
-
userCode:
|
|
1779
|
+
userCode: z11.string().min(4).max(16),
|
|
1680
1780
|
/** Where the user approves. Must be on the server's own origin. */
|
|
1681
|
-
verificationUrl:
|
|
1781
|
+
verificationUrl: z11.url(),
|
|
1682
1782
|
/** Epoch ms after which the code is dead ({@link MUSTS.PAIR_CODE_EXPIRES}). */
|
|
1683
|
-
expiresAt:
|
|
1783
|
+
expiresAt: z11.number().int().positive(),
|
|
1684
1784
|
/** How often the daemon may poll. */
|
|
1685
|
-
pollIntervalMs:
|
|
1785
|
+
pollIntervalMs: z11.number().int().min(500).max(6e4)
|
|
1686
1786
|
}).strict();
|
|
1687
|
-
var PairPollRequest =
|
|
1688
|
-
protocolVersion:
|
|
1689
|
-
action:
|
|
1690
|
-
deviceCode:
|
|
1787
|
+
var PairPollRequest = z11.object({
|
|
1788
|
+
protocolVersion: z11.literal(PROTOCOL_VERSION),
|
|
1789
|
+
action: z11.literal("poll"),
|
|
1790
|
+
deviceCode: z11.string().min(20)
|
|
1691
1791
|
}).strict();
|
|
1692
|
-
var PairPollResponse =
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
status:
|
|
1792
|
+
var PairPollResponse = z11.discriminatedUnion("status", [
|
|
1793
|
+
z11.object({ status: z11.literal("pending") }).strict(),
|
|
1794
|
+
z11.object({ status: z11.literal("denied") }).strict(),
|
|
1795
|
+
z11.object({ status: z11.literal("expired") }).strict(),
|
|
1796
|
+
z11.object({
|
|
1797
|
+
status: z11.literal("approved"),
|
|
1698
1798
|
// `runnerToken` is gone — cloud_008 §2.4, finding 37.
|
|
1699
1799
|
//
|
|
1700
1800
|
// It was minted here, hashed into `RunnerRecord.tokenHash`, written to
|
|
@@ -1711,11 +1811,11 @@ var PairPollResponse = z10.discriminatedUnion("status", [
|
|
|
1711
1811
|
// `REQUESTS_SIGNED_NOT_BEARER` was already the rule and was already
|
|
1712
1812
|
// enforced — every authenticated call is signed by the device's pinned
|
|
1713
1813
|
// identity key. This removes the thing the MUST is named after.
|
|
1714
|
-
runnerId:
|
|
1814
|
+
runnerId: z11.string().min(1),
|
|
1715
1815
|
/** The app's id for the approving user — this daemon's owner forever. */
|
|
1716
|
-
owner:
|
|
1816
|
+
owner: z11.string().min(1),
|
|
1717
1817
|
/** Display name for the trust UI, if the app offers one. */
|
|
1718
|
-
ownerLabel:
|
|
1818
|
+
ownerLabel: z11.string().optional(),
|
|
1719
1819
|
/**
|
|
1720
1820
|
* The sites this pairing covers, for the daemon to pin (byollm_009 §5),
|
|
1721
1821
|
* keyed by each site's identity key id — cloud_009 §5.
|
|
@@ -1734,55 +1834,55 @@ var PairPollResponse = z10.discriminatedUnion("status", [
|
|
|
1734
1834
|
* runner's lookup is a map read rather than a join across two
|
|
1735
1835
|
* namespaces.
|
|
1736
1836
|
*/
|
|
1737
|
-
sites:
|
|
1837
|
+
sites: z11.record(z11.string().min(1), PublicIdentity),
|
|
1738
1838
|
/**
|
|
1739
|
-
* The control plane's
|
|
1839
|
+
* The control plane's grant-signing key, pinned here — Amendment J.
|
|
1740
1840
|
*
|
|
1741
1841
|
* **Pairing is when, and that is the whole question.** Pairing is
|
|
1742
1842
|
* already the ceremony where an owner proves out of band that this
|
|
1743
1843
|
* device is theirs, so a key learned here rides trust that has already
|
|
1744
|
-
* happened. The rejected alternative is trust-on-first-
|
|
1745
|
-
*
|
|
1746
|
-
* that learns whose signature to trust from the first
|
|
1747
|
-
* has its
|
|
1844
|
+
* happened. The rejected alternative is trust-on-first-grant, and it is
|
|
1845
|
+
* rejected because it hands the decision back to the relay: a daemon
|
|
1846
|
+
* that learns whose signature to trust from the first grant to arrive
|
|
1847
|
+
* has its admission authority chosen by whoever controls delivery.
|
|
1748
1848
|
*
|
|
1749
1849
|
* Optional on the wire, and only on the wire: a direct-mode server has
|
|
1750
|
-
* no control plane and signs
|
|
1751
|
-
*
|
|
1752
|
-
*
|
|
1753
|
-
*
|
|
1850
|
+
* no control plane and signs nothing, and a daemon that receives no key
|
|
1851
|
+
* serves its owner alone. It is not optional for a relay with a control
|
|
1852
|
+
* plane — one that omitted it would be asking devices to accept grants
|
|
1853
|
+
* from nobody in particular, and would find every job refused.
|
|
1754
1854
|
*
|
|
1755
|
-
* Rotation is Amendment C's, with no path where a
|
|
1855
|
+
* Rotation is Amendment C's, with no path where a grant teaches a
|
|
1756
1856
|
* daemon a new key.
|
|
1757
1857
|
*/
|
|
1758
|
-
controlPlanePublic:
|
|
1858
|
+
controlPlanePublic: z11.string().min(1).optional()
|
|
1759
1859
|
}).strict()
|
|
1760
1860
|
]);
|
|
1761
|
-
var PairRequest =
|
|
1861
|
+
var PairRequest = z11.discriminatedUnion("action", [
|
|
1762
1862
|
PairStartRequest,
|
|
1763
1863
|
PairPollRequest
|
|
1764
1864
|
]);
|
|
1765
|
-
var ClaimRequest =
|
|
1766
|
-
protocolVersion:
|
|
1767
|
-
runnerId:
|
|
1865
|
+
var ClaimRequest = z11.object({
|
|
1866
|
+
protocolVersion: z11.literal(PROTOCOL_VERSION),
|
|
1867
|
+
runnerId: z11.string().min(1),
|
|
1768
1868
|
/** Re-sent on every claim so a server never matches against a stale matrix. */
|
|
1769
1869
|
capabilities: CapabilityMatrix,
|
|
1770
1870
|
/** Upper bound on jobs to return; the server may return fewer. */
|
|
1771
|
-
max:
|
|
1871
|
+
max: z11.number().int().min(1).max(64)
|
|
1772
1872
|
}).strict();
|
|
1773
|
-
var ClaimResponse =
|
|
1873
|
+
var ClaimResponse = z11.object({
|
|
1774
1874
|
/**
|
|
1775
1875
|
* Stubs, not jobs. The payload arrives from `fetch`, sealed to whichever
|
|
1776
1876
|
* device claimed — see {@link JobStub} for the exhaustive metadata list.
|
|
1777
1877
|
*/
|
|
1778
|
-
jobs:
|
|
1878
|
+
jobs: z11.array(ClaimedStub),
|
|
1779
1879
|
/** Lease duration granted, so the daemon knows its renewal deadline. */
|
|
1780
|
-
leaseMs:
|
|
1880
|
+
leaseMs: z11.number().int().positive()
|
|
1781
1881
|
}).strict();
|
|
1782
|
-
var HeartbeatRequest =
|
|
1783
|
-
protocolVersion:
|
|
1784
|
-
runnerId:
|
|
1785
|
-
daemonVersion:
|
|
1882
|
+
var HeartbeatRequest = z11.object({
|
|
1883
|
+
protocolVersion: z11.literal(PROTOCOL_VERSION),
|
|
1884
|
+
runnerId: z11.string().min(1),
|
|
1885
|
+
daemonVersion: z11.string().min(1),
|
|
1786
1886
|
capabilities: CapabilityMatrix,
|
|
1787
1887
|
/**
|
|
1788
1888
|
* Kinds this device is withholding, and why it can be said.
|
|
@@ -1791,20 +1891,18 @@ var HeartbeatRequest = z10.object({
|
|
|
1791
1891
|
* older daemon against a newer hub is simply a device with no withheld
|
|
1792
1892
|
* kinds rather than a parse failure.
|
|
1793
1893
|
*/
|
|
1794
|
-
withheld:
|
|
1894
|
+
withheld: z11.array(WithheldKind).default([]),
|
|
1795
1895
|
/**
|
|
1796
1896
|
* Leases this daemon believes it holds; the server renews exactly these.
|
|
1797
1897
|
*
|
|
1798
1898
|
* Lease ids rather than job ids, so a replayed heartbeat cannot renew a
|
|
1799
1899
|
* grant the runner no longer holds — see {@link Lease.id}.
|
|
1800
1900
|
*/
|
|
1801
|
-
activeLeases:
|
|
1802
|
-
z10.object({ jobId: z10.string().min(1), leaseId: z10.string().min(1) })
|
|
1803
|
-
),
|
|
1901
|
+
activeLeases: z11.array(GrantRef),
|
|
1804
1902
|
/** True while the owner has the daemon paused; the server stops offering work. */
|
|
1805
|
-
paused:
|
|
1903
|
+
paused: z11.boolean()
|
|
1806
1904
|
}).strict();
|
|
1807
|
-
var HeartbeatResponse =
|
|
1905
|
+
var HeartbeatResponse = z11.object({
|
|
1808
1906
|
/**
|
|
1809
1907
|
* The sites this daemon may serve, right now — cloud_008 finding 59.
|
|
1810
1908
|
*
|
|
@@ -1822,7 +1920,7 @@ var HeartbeatResponse = z10.object({
|
|
|
1822
1920
|
* rather than being told a second time — two fields for one fact is how
|
|
1823
1921
|
* they drift.
|
|
1824
1922
|
*/
|
|
1825
|
-
sites:
|
|
1923
|
+
sites: z11.record(z11.string().min(1), PublicIdentity),
|
|
1826
1924
|
/**
|
|
1827
1925
|
* How a site's current key traces back to one this daemon already holds —
|
|
1828
1926
|
* byollm_009 Amendment C.
|
|
@@ -1840,11 +1938,11 @@ var HeartbeatResponse = z10.object({
|
|
|
1840
1938
|
* history is public by construction, because a daemon that cannot read it
|
|
1841
1939
|
* cannot verify it.
|
|
1842
1940
|
*/
|
|
1843
|
-
successions:
|
|
1844
|
-
|
|
1845
|
-
|
|
1941
|
+
successions: z11.record(
|
|
1942
|
+
z11.string().min(1),
|
|
1943
|
+
z11.object({
|
|
1846
1944
|
/** Oldest last, as the projection carries it. */
|
|
1847
|
-
succeeds:
|
|
1945
|
+
succeeds: z11.array(Succession).max(MAX_SUCCESSION_CHAIN),
|
|
1848
1946
|
/**
|
|
1849
1947
|
* Until when the superseded key may still sign work — epoch ms.
|
|
1850
1948
|
*
|
|
@@ -1853,7 +1951,7 @@ var HeartbeatResponse = z10.object({
|
|
|
1853
1951
|
* window indefinitely would be a two-key site forever, decided by
|
|
1854
1952
|
* the party this design does not trust.
|
|
1855
1953
|
*/
|
|
1856
|
-
retiringUntil:
|
|
1954
|
+
retiringUntil: z11.number().int().positive().optional()
|
|
1857
1955
|
}).strict()
|
|
1858
1956
|
).optional(),
|
|
1859
1957
|
/**
|
|
@@ -1866,8 +1964,8 @@ var HeartbeatResponse = z10.object({
|
|
|
1866
1964
|
* is the unique grant and the daemon already keys its work by it; this is
|
|
1867
1965
|
* the same shape `activeLeases` sends in the other direction.
|
|
1868
1966
|
*/
|
|
1869
|
-
cancel:
|
|
1870
|
-
|
|
1967
|
+
cancel: z11.array(
|
|
1968
|
+
z11.object({ jobId: z11.string().min(1), leaseId: z11.string().min(1) }).strict()
|
|
1871
1969
|
),
|
|
1872
1970
|
// `leases` is deliberately absent — cloud_008 §1.4b, finding 16.
|
|
1873
1971
|
//
|
|
@@ -1897,11 +1995,11 @@ var HeartbeatResponse = z10.object({
|
|
|
1897
1995
|
* ambiguous across sites, and "the lease you no longer hold" is exactly
|
|
1898
1996
|
* what this field means anyway.
|
|
1899
1997
|
*/
|
|
1900
|
-
lost:
|
|
1901
|
-
|
|
1998
|
+
lost: z11.array(
|
|
1999
|
+
z11.object({ jobId: z11.string().min(1), leaseId: z11.string().min(1) }).strict()
|
|
1902
2000
|
),
|
|
1903
2001
|
/** Server clock, so a daemon with a skewed clock still honors leases. */
|
|
1904
|
-
serverTime:
|
|
2002
|
+
serverTime: z11.number().int().positive(),
|
|
1905
2003
|
/**
|
|
1906
2004
|
* Sites whose disclosure the user must read again before work moves —
|
|
1907
2005
|
* cloud_008 finding 48, named rather than counted.
|
|
@@ -1916,28 +2014,13 @@ var HeartbeatResponse = z10.object({
|
|
|
1916
2014
|
* operator stopped it" — one word with two subjects on two halves of one
|
|
1917
2015
|
* exchange is a confusion nobody untangles from a log.
|
|
1918
2016
|
*/
|
|
1919
|
-
awaitingConsent:
|
|
1920
|
-
/**
|
|
1921
|
-
* Who this owner's devices may serve `team` work for — Amendment G.
|
|
1922
|
-
*
|
|
1923
|
-
* Carried by the relay and authored by nobody it can reach. The daemon
|
|
1924
|
-
* verifies it against the key pinned at pairing and admits from its own
|
|
1925
|
-
* held copy, so this field is delivery and not instruction: withholding
|
|
1926
|
-
* it narrows a device, and editing it is caught.
|
|
1927
|
-
*
|
|
1928
|
-
* Optional because a roster is a cloud-mode fact — direct mode has no
|
|
1929
|
-
* control plane to author one — and because a daemon that has never
|
|
1930
|
-
* received one must narrow rather than fail. Absent is not "admit
|
|
1931
|
-
* nobody"; {@link ROSTER_MAX_AGE_MS} is what makes absence bite, and it
|
|
1932
|
-
* bites the same way for a roster withheld as for one never sent.
|
|
1933
|
-
*/
|
|
1934
|
-
roster: SignedRoster.optional()
|
|
2017
|
+
awaitingConsent: z11.array(z11.string().min(1))
|
|
1935
2018
|
}).strict();
|
|
1936
|
-
var ResultDisposition =
|
|
1937
|
-
var ResultRequest =
|
|
1938
|
-
protocolVersion:
|
|
1939
|
-
runnerId:
|
|
1940
|
-
jobId:
|
|
2019
|
+
var ResultDisposition = z11.enum(["ok", "error", "canceled"]);
|
|
2020
|
+
var ResultRequest = z11.object({
|
|
2021
|
+
protocolVersion: z11.literal(PROTOCOL_VERSION),
|
|
2022
|
+
runnerId: z11.string().min(1),
|
|
2023
|
+
jobId: z11.string().min(1),
|
|
1941
2024
|
/**
|
|
1942
2025
|
* The grant this result was produced under — cloud_008 §1.4a.
|
|
1943
2026
|
*
|
|
@@ -1960,7 +2043,7 @@ var ResultRequest = z10.object({
|
|
|
1960
2043
|
* learned once already, when a replayed release yanked a later grant, and
|
|
1961
2044
|
* it applies here for the same reason.
|
|
1962
2045
|
*/
|
|
1963
|
-
leaseId:
|
|
2046
|
+
leaseId: z11.string().min(1),
|
|
1964
2047
|
/**
|
|
1965
2048
|
* The outcome, sealed to the site and signed by the device.
|
|
1966
2049
|
*
|
|
@@ -1992,12 +2075,12 @@ var ResultRequest = z10.object({
|
|
|
1992
2075
|
// on it, so it is a class a routing party consumes. Nobody between the
|
|
1993
2076
|
// two ends consumes these.
|
|
1994
2077
|
}).strict();
|
|
1995
|
-
var ResultResponse =
|
|
2078
|
+
var ResultResponse = z11.object({
|
|
1996
2079
|
/**
|
|
1997
2080
|
* False when this submission wrote nothing — the daemon should discard,
|
|
1998
2081
|
* not retry ({@link MUSTS.RESULT_IDEMPOTENT}).
|
|
1999
2082
|
*/
|
|
2000
|
-
accepted:
|
|
2083
|
+
accepted: z11.boolean(),
|
|
2001
2084
|
/**
|
|
2002
2085
|
* True when this device had already recorded this job's result.
|
|
2003
2086
|
*
|
|
@@ -2011,13 +2094,13 @@ var ResultResponse = z10.object({
|
|
|
2011
2094
|
* the same refusal it would get for a job that is *not* terminal, so a job
|
|
2012
2095
|
* id cannot be used as a terminality probe.
|
|
2013
2096
|
*/
|
|
2014
|
-
duplicate:
|
|
2097
|
+
duplicate: z11.boolean().optional(),
|
|
2015
2098
|
/** The job's state after this submission. */
|
|
2016
|
-
state:
|
|
2099
|
+
state: z11.string().min(1)
|
|
2017
2100
|
}).strict();
|
|
2018
|
-
var ReleaseRequest =
|
|
2019
|
-
protocolVersion:
|
|
2020
|
-
runnerId:
|
|
2101
|
+
var ReleaseRequest = z11.object({
|
|
2102
|
+
protocolVersion: z11.literal(PROTOCOL_VERSION),
|
|
2103
|
+
runnerId: z11.string().min(1),
|
|
2021
2104
|
/**
|
|
2022
2105
|
* Which leases to release — the grant, not just the job.
|
|
2023
2106
|
*
|
|
@@ -2025,24 +2108,22 @@ var ReleaseRequest = z10.object({
|
|
|
2025
2108
|
* moment it arrives, which for a replayed request is not the lease the
|
|
2026
2109
|
* daemon meant. See {@link Lease.id}.
|
|
2027
2110
|
*/
|
|
2028
|
-
leases:
|
|
2029
|
-
z10.object({ jobId: z10.string().min(1), leaseId: z10.string().min(1) })
|
|
2030
|
-
),
|
|
2111
|
+
leases: z11.array(GrantRef),
|
|
2031
2112
|
/**
|
|
2032
2113
|
* Why, so the app's runner list can say something true.
|
|
2033
2114
|
*
|
|
2034
|
-
* `refused` is load-bearing, not cosmetic: the server cannot evaluate
|
|
2035
|
-
*
|
|
2115
|
+
* `refused` is load-bearing, not cosmetic: the server cannot evaluate
|
|
2116
|
+
* what a device will admit (§4.2), so it may legitimately offer
|
|
2036
2117
|
* a job this daemon then declines. The server MUST record the refusal and
|
|
2037
2118
|
* stop offering that job to that runner, or the pair would spin between
|
|
2038
2119
|
* claim and release forever.
|
|
2039
2120
|
*/
|
|
2040
|
-
reason:
|
|
2121
|
+
reason: z11.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
|
|
2041
2122
|
}).strict();
|
|
2042
|
-
var ReleaseResponse =
|
|
2043
|
-
released:
|
|
2123
|
+
var ReleaseResponse = z11.object({
|
|
2124
|
+
released: z11.array(z11.string().min(1))
|
|
2044
2125
|
}).strict();
|
|
2045
|
-
var WireErrorCode =
|
|
2126
|
+
var WireErrorCode = z11.enum([
|
|
2046
2127
|
"bad-request",
|
|
2047
2128
|
"unsupported-protocol-version",
|
|
2048
2129
|
// "We do not know who you are." Exactly 401, and only that — cloud_008
|
|
@@ -2092,9 +2173,9 @@ var WireErrorCode = z10.enum([
|
|
|
2092
2173
|
"rate-limited",
|
|
2093
2174
|
"server-error"
|
|
2094
2175
|
]);
|
|
2095
|
-
var WireError =
|
|
2176
|
+
var WireError = z11.object({
|
|
2096
2177
|
error: WireErrorCode,
|
|
2097
|
-
message:
|
|
2178
|
+
message: z11.string().min(1),
|
|
2098
2179
|
/**
|
|
2099
2180
|
* What this server speaks, on `unsupported-protocol-version` — §B.4.
|
|
2100
2181
|
*
|
|
@@ -2108,10 +2189,10 @@ var WireError = z10.object({
|
|
|
2108
2189
|
* Modelled the way `clock-skew`'s two fields already are — code-specific
|
|
2109
2190
|
* extras, refused on any other code by the refinement below.
|
|
2110
2191
|
*/
|
|
2111
|
-
supported:
|
|
2112
|
-
minimum:
|
|
2192
|
+
supported: z11.array(z11.string().min(1)).optional(),
|
|
2193
|
+
minimum: z11.string().min(1).optional(),
|
|
2113
2194
|
/** Seconds; mirrors Retry-After for `rate-limited` and `server-error`. */
|
|
2114
|
-
retryAfter:
|
|
2195
|
+
retryAfter: z11.number().int().nonnegative().optional(),
|
|
2115
2196
|
/**
|
|
2116
2197
|
* The server's clock, and the window it allows. `clock-skew` only.
|
|
2117
2198
|
*
|
|
@@ -2121,8 +2202,8 @@ var WireError = z10.object({
|
|
|
2121
2202
|
* heartbeat response returns the same value, and so does every `Date`
|
|
2122
2203
|
* header.
|
|
2123
2204
|
*/
|
|
2124
|
-
serverTime:
|
|
2125
|
-
maxSkewMs:
|
|
2205
|
+
serverTime: z11.number().int().positive().optional(),
|
|
2206
|
+
maxSkewMs: z11.number().int().positive().optional()
|
|
2126
2207
|
}).strict().superRefine((error, ctx) => {
|
|
2127
2208
|
const skew = error.error === "clock-skew";
|
|
2128
2209
|
const carried = error.serverTime !== void 0 || error.maxSkewMs !== void 0;
|
|
@@ -2173,16 +2254,16 @@ var ERROR_STATUS = Object.freeze({
|
|
|
2173
2254
|
"rate-limited": 429,
|
|
2174
2255
|
"server-error": 500
|
|
2175
2256
|
});
|
|
2176
|
-
var FetchRequest =
|
|
2257
|
+
var FetchRequest = z11.object({
|
|
2177
2258
|
// `literal`, like every other request — V1-17. This one said
|
|
2178
2259
|
// `string().min(1)`, so a daemon speaking a version this server does not
|
|
2179
2260
|
// know got past the handshake on the one endpoint that hands over a
|
|
2180
2261
|
// sealed payload. The version check exists so that a mismatch is a named
|
|
2181
2262
|
// refusal rather than a schema failure three fields later; here it was
|
|
2182
2263
|
// neither.
|
|
2183
|
-
protocolVersion:
|
|
2184
|
-
runnerId:
|
|
2185
|
-
jobId:
|
|
2264
|
+
protocolVersion: z11.literal(PROTOCOL_VERSION),
|
|
2265
|
+
runnerId: z11.string().min(1),
|
|
2266
|
+
jobId: z11.string().min(1),
|
|
2186
2267
|
/**
|
|
2187
2268
|
* The grant this daemon holds.
|
|
2188
2269
|
*
|
|
@@ -2190,9 +2271,9 @@ var FetchRequest = z10.object({
|
|
|
2190
2271
|
* only the job would be answerable for whatever lease exists when it
|
|
2191
2272
|
* arrives ({@link Lease.id}).
|
|
2192
2273
|
*/
|
|
2193
|
-
leaseId:
|
|
2274
|
+
leaseId: z11.string().min(1)
|
|
2194
2275
|
}).strict();
|
|
2195
|
-
var FetchResponse =
|
|
2276
|
+
var FetchResponse = z11.object({
|
|
2196
2277
|
/**
|
|
2197
2278
|
* The work, sealed to the device that claimed it — byollm_009 §6.
|
|
2198
2279
|
*
|
|
@@ -2211,6 +2292,8 @@ export {
|
|
|
2211
2292
|
BackendClass,
|
|
2212
2293
|
BackendCost,
|
|
2213
2294
|
BackendIdSchema,
|
|
2295
|
+
CLOCK_ATTRIBUTION_MS,
|
|
2296
|
+
CLOCK_SKEW_WARN_MS,
|
|
2214
2297
|
Capability,
|
|
2215
2298
|
CapabilityMatrix,
|
|
2216
2299
|
ChatMessage,
|
|
@@ -2227,7 +2310,11 @@ export {
|
|
|
2227
2310
|
EnvelopeDirection,
|
|
2228
2311
|
FetchRequest,
|
|
2229
2312
|
FetchResponse,
|
|
2313
|
+
GRANT_CONTEXT,
|
|
2314
|
+
GRANT_MAX_AGE_MS,
|
|
2315
|
+
GRANT_SIGNED_FIELDS,
|
|
2230
2316
|
GeneratePayload,
|
|
2317
|
+
GrantRef,
|
|
2231
2318
|
HeartbeatRequest,
|
|
2232
2319
|
HeartbeatResponse,
|
|
2233
2320
|
JOB_KINDS,
|
|
@@ -2247,6 +2334,7 @@ export {
|
|
|
2247
2334
|
MIN_PROTOCOL_VERSION,
|
|
2248
2335
|
MUSTS,
|
|
2249
2336
|
MUST_IDS,
|
|
2337
|
+
Manifest,
|
|
2250
2338
|
MatchRefusal,
|
|
2251
2339
|
OFFER_SCOPES,
|
|
2252
2340
|
OfferScope,
|
|
@@ -2259,10 +2347,10 @@ export {
|
|
|
2259
2347
|
PairStartRequest,
|
|
2260
2348
|
PairStartResponse,
|
|
2261
2349
|
PublicIdentity,
|
|
2350
|
+
Purpose,
|
|
2262
2351
|
REFUSAL_MESSAGES,
|
|
2352
|
+
RESERVED_PURPOSE,
|
|
2263
2353
|
RETIREMENT_WINDOW_MS,
|
|
2264
|
-
ROSTER_CONTEXT,
|
|
2265
|
-
ROSTER_MAX_AGE_MS,
|
|
2266
2354
|
RefusalReason,
|
|
2267
2355
|
ReleaseRequest,
|
|
2268
2356
|
ReleaseResponse,
|
|
@@ -2277,7 +2365,7 @@ export {
|
|
|
2277
2365
|
SUPPORTED_PROTOCOL_VERSIONS,
|
|
2278
2366
|
SealedEnvelope,
|
|
2279
2367
|
SealedOutcome,
|
|
2280
|
-
|
|
2368
|
+
SignedGrant,
|
|
2281
2369
|
SizeClass,
|
|
2282
2370
|
StoredKeys,
|
|
2283
2371
|
Succession,
|
|
@@ -2296,6 +2384,7 @@ export {
|
|
|
2296
2384
|
effectiveOfferScope,
|
|
2297
2385
|
fingerprint,
|
|
2298
2386
|
generateKeys,
|
|
2387
|
+
grantStatement,
|
|
2299
2388
|
isBackendId,
|
|
2300
2389
|
isCloudTaggedModel,
|
|
2301
2390
|
isJobKind,
|
|
@@ -2310,20 +2399,20 @@ export {
|
|
|
2310
2399
|
provenanceFor,
|
|
2311
2400
|
publicIdentityOf,
|
|
2312
2401
|
resolveCost,
|
|
2313
|
-
rosterStatement,
|
|
2314
2402
|
seal,
|
|
2403
|
+
signGrant,
|
|
2315
2404
|
signRequest,
|
|
2316
|
-
signRoster,
|
|
2317
2405
|
signSiteRequest,
|
|
2318
2406
|
signSuccession,
|
|
2319
2407
|
signWith,
|
|
2408
|
+
singlePurposeManifest,
|
|
2320
2409
|
sizeClassCeiling,
|
|
2321
2410
|
sizeClassOf,
|
|
2322
2411
|
successionStatement,
|
|
2412
|
+
verifyGrant,
|
|
2323
2413
|
verifyLink,
|
|
2324
2414
|
verifyPublicIdentity,
|
|
2325
2415
|
verifyRequest,
|
|
2326
|
-
verifyRoster,
|
|
2327
2416
|
verifySiteRequest,
|
|
2328
2417
|
verifyWith,
|
|
2329
2418
|
walkSuccession
|