@byollm/protocol 0.1.0-alpha.57 → 0.1.0-alpha.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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", "public"]);
259
- var OfferScope = z2.enum(["private", "team", "public"]);
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 `self` but this daemon belongs to a different user. */
265
+ /** Job is `private` but this daemon belongs to a different user. */
266
266
  "audience-self-other-owner",
267
- /** Job is `named` but this daemon's local allowlist does not admit the owner. */
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 `named`/`public` but the server's own allowlist excludes this runner. */
275
+ /** Job is `team` but the server's own allowlist excludes this runner. */
270
276
  "not-in-server-allowlist",
271
- /** The backend offers only `self` and the job belongs to someone else. */
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 `self`. */
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.locallyAllows(job.owner) ? ALLOWED : refuse("not-locally-allowed");
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": "the job's owner is not on this device's allowlist (byollm allow <server> <user>)",
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 backend is offered to its owner only (byollm offer <backend> named|public to widen)",
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"
@@ -384,8 +388,222 @@ function payloadTextLength(kinded) {
384
388
  }
385
389
 
386
390
  // src/job.ts
391
+ import { z as z6 } from "zod";
392
+
393
+ // src/grant.ts
394
+ import { Buffer as Buffer2 } from "buffer";
395
+ import { z as z5 } from "zod";
396
+
397
+ // src/keys.ts
398
+ import {
399
+ createHash,
400
+ createPrivateKey,
401
+ createPublicKey,
402
+ generateKeyPairSync,
403
+ sign,
404
+ verify
405
+ } from "crypto";
387
406
  import { z as z4 } from "zod";
388
- var JobState = z4.enum([
407
+ var PublicIdentity = z4.object({
408
+ /** Raw Ed25519 public key. The pinned one. */
409
+ identity: z4.string().min(1),
410
+ /** Raw X25519 public key, for sealing to this party. */
411
+ encryption: z4.string().min(1),
412
+ /**
413
+ * Ed25519 signature over the encryption key, by the identity key.
414
+ *
415
+ * This is what stops an upstream substituting an encryption key of its
416
+ * own while relaying a genuine identity: the receiver pins the identity
417
+ * and refuses any encryption key not signed by it.
418
+ */
419
+ encryptionSig: z4.string().min(1)
420
+ }).strict();
421
+ var StoredKeys = z4.object({
422
+ version: z4.literal(1),
423
+ identityPublic: z4.string().min(1),
424
+ identityPrivate: z4.string().min(1),
425
+ encryptionPublic: z4.string().min(1),
426
+ encryptionPrivate: z4.string().min(1),
427
+ encryptionSig: z4.string().min(1),
428
+ createdAt: z4.number().int().positive()
429
+ }).strict();
430
+ var ENCRYPTION_KEY_CONTEXT = "byollm/v1/encryption-key";
431
+ function rawPublic(key) {
432
+ const jwk = key.export({ format: "jwk" });
433
+ const x = jwk.x;
434
+ if (typeof x !== "string") throw new Error("key has no raw public component");
435
+ return x;
436
+ }
437
+ function importPublic(raw, crv) {
438
+ return createPublicKey({ key: { kty: "OKP", crv, x: raw }, format: "jwk" });
439
+ }
440
+ function importPrivate(stored) {
441
+ return createPrivateKey({
442
+ key: Buffer.from(stored, "base64"),
443
+ type: "pkcs8",
444
+ format: "der"
445
+ });
446
+ }
447
+ var exportPrivate = (key) => key.export({ type: "pkcs8", format: "der" }).toString("base64");
448
+ function generateKeys(now) {
449
+ const identity = generateKeyPairSync("ed25519");
450
+ const encryption = generateKeyPairSync("x25519");
451
+ const encryptionPublic = rawPublic(encryption.publicKey);
452
+ return {
453
+ version: 1,
454
+ identityPublic: rawPublic(identity.publicKey),
455
+ identityPrivate: exportPrivate(identity.privateKey),
456
+ encryptionPublic,
457
+ encryptionPrivate: exportPrivate(encryption.privateKey),
458
+ encryptionSig: sign(
459
+ null,
460
+ Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${encryptionPublic}`),
461
+ identity.privateKey
462
+ ).toString("base64url"),
463
+ createdAt: now
464
+ };
465
+ }
466
+ function publicIdentityOf(keys) {
467
+ return {
468
+ identity: keys.identityPublic,
469
+ encryption: keys.encryptionPublic,
470
+ encryptionSig: keys.encryptionSig
471
+ };
472
+ }
473
+ function verifyPublicIdentity(identity) {
474
+ try {
475
+ return verify(
476
+ null,
477
+ Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${identity.encryption}`),
478
+ importPublic(identity.identity, "Ed25519"),
479
+ Buffer.from(identity.encryptionSig, "base64url")
480
+ );
481
+ } catch {
482
+ return false;
483
+ }
484
+ }
485
+ function signWith(keys, data) {
486
+ return sign(null, data, importPrivate(keys.identityPrivate)).toString(
487
+ "base64url"
488
+ );
489
+ }
490
+ function verifyWith(identityPublic, data, signature) {
491
+ try {
492
+ return verify(
493
+ null,
494
+ data,
495
+ importPublic(identityPublic, "Ed25519"),
496
+ Buffer.from(signature, "base64url")
497
+ );
498
+ } catch {
499
+ return false;
500
+ }
501
+ }
502
+ var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
503
+ function fingerprint(identityPublic) {
504
+ const digest = createHash("sha256").update(Buffer.from(identityPublic, "base64url")).digest();
505
+ let bits = 0;
506
+ let value = 0;
507
+ let out = "";
508
+ for (const byte of digest.subarray(0, 15)) {
509
+ value = value << 8 | byte;
510
+ bits += 8;
511
+ while (bits >= 5) {
512
+ out += ALPHABET.charAt(value >>> bits - 5 & 31);
513
+ bits -= 5;
514
+ }
515
+ }
516
+ const groups = out.match(/.{1,4}/g) ?? [];
517
+ return `BYOLLM-${groups.join("-")}`;
518
+ }
519
+ var keyId = (identityPublic) => fingerprint(identityPublic);
520
+
521
+ // src/grant.ts
522
+ var GRANT_MAX_AGE_MS = 12e4;
523
+ var CLOCK_SKEW_WARN_MS = 3e4;
524
+ var CLOCK_ATTRIBUTION_MS = 5e3;
525
+ var GRANT_CONTEXT = "byollm/v1/grant";
526
+ var SignedGrant = z5.object({
527
+ /**
528
+ * This grant's own id — what makes it single-use.
529
+ *
530
+ * **Not the job id, and the difference is load-bearing.** Binding
531
+ * single-use to `jobId` would refuse a legitimate retry: a claim that
532
+ * times out is re-claimed, the control plane authors a second grant for
533
+ * the same job, and a device that recorded the job id as spent would
534
+ * reject its own recovery. A fresh id per authorship replays nothing and
535
+ * retries fine.
536
+ */
537
+ grantId: z5.string().min(1),
538
+ /**
539
+ * The job this grant admits, and only this one.
540
+ *
541
+ * A grant lifted from one job and presented for another is the obvious
542
+ * attack, and this field is why it fails.
543
+ */
544
+ jobId: z5.string().min(1),
545
+ /** The site the work came from. */
546
+ siteId: z5.string().min(1),
547
+ /** Whose job it is — the person the site enqueued for. */
548
+ user: z5.string().min(1),
549
+ /**
550
+ * Whose device it is for.
551
+ *
552
+ * Passed to {@link verifyGrant} rather than read out of the document, for
553
+ * the reason every verifier here takes its subject as an argument: a
554
+ * verifier that recovered the owner from the signed bytes would accept a
555
+ * genuine grant belonging to somebody else and pass every check.
556
+ */
557
+ owner: z5.string().min(1),
558
+ /** The site purpose this job serves — byollm_016 Amendment L. */
559
+ purpose: z5.string().min(1),
560
+ /** The kind of work. */
561
+ kind: z5.string().min(1),
562
+ /**
563
+ * The service the control plane resolved this (purpose, kind) to, from
564
+ * the user's own mapping.
565
+ *
566
+ * Selection is the control plane's; **offer-consistency is the
567
+ * device's**. A device verifies it actually offers this service, at a
568
+ * scope that includes {@link user}, before running anything.
569
+ */
570
+ service: z5.string().min(1),
571
+ /** When the control plane signed it — epoch ms, the only anchor for age. */
572
+ issuedAt: z5.number().int().positive(),
573
+ /** Base64url Ed25519 over {@link grantStatement}. */
574
+ signature: z5.string().min(1)
575
+ }).strict();
576
+ var GRANT_SIGNED_FIELDS = Object.freeze(
577
+ Object.keys(SignedGrant.shape).filter((key) => key !== "signature").sort()
578
+ );
579
+ function grantStatement(claims) {
580
+ return Buffer2.from(
581
+ JSON.stringify([
582
+ GRANT_CONTEXT,
583
+ ...GRANT_SIGNED_FIELDS.map((field) => claims[field])
584
+ ]),
585
+ "utf8"
586
+ );
587
+ }
588
+ function signGrant(keys, claims) {
589
+ return { ...claims, signature: signWith(keys, grantStatement(claims)) };
590
+ }
591
+ function verifyGrant(input) {
592
+ const { grant, now } = input;
593
+ if (grant.owner !== input.owner) return "wrong-owner";
594
+ if (grant.jobId !== input.jobId) return "wrong-job";
595
+ const age = now - grant.issuedAt;
596
+ if (age < 0) return "from-the-future";
597
+ if (age > (input.maxAgeMs ?? GRANT_MAX_AGE_MS)) return "expired";
598
+ return verifyWith(
599
+ input.controlPlanePublic,
600
+ grantStatement(grant),
601
+ grant.signature
602
+ ) ? null : "bad-signature";
603
+ }
604
+
605
+ // src/job.ts
606
+ var JobState = z6.enum([
389
607
  "queued",
390
608
  "claimed",
391
609
  "running",
@@ -417,7 +635,7 @@ var TRANSITIONS = Object.freeze({
417
635
  function canTransition(from, to) {
418
636
  return TRANSITIONS[from].includes(to);
419
637
  }
420
- var Lease = z4.object({
638
+ var Lease = z6.object({
421
639
  /**
422
640
  * Identifies *this* grant, not just its holder.
423
641
  *
@@ -432,20 +650,20 @@ var Lease = z4.object({
432
650
  * and release *is*, per lease, but not across leases, because nothing in
433
651
  * the request said which one.
434
652
  */
435
- id: z4.string().min(1),
653
+ id: z6.string().min(1),
436
654
  /** The runner holding the lease. */
437
- runnerId: z4.string().min(1),
655
+ runnerId: z6.string().min(1),
438
656
  /** Epoch milliseconds after which the claim is void. */
439
- expiresAt: z4.number().int().positive()
657
+ expiresAt: z6.number().int().positive()
440
658
  });
441
- var JobPayload = z4.union([GeneratePayload, ChatPayload]);
442
- var ClaimedJob = z4.object({
443
- id: z4.string().min(1),
659
+ var JobPayload = z6.union([GeneratePayload, ChatPayload]);
660
+ var ClaimedJob = z6.object({
661
+ id: z6.string().min(1),
444
662
  kind: JobKind,
445
663
  payload: JobPayload,
446
664
  audience: Audience,
447
665
  /** The app's id for the user who enqueued it. */
448
- owner: z4.string().min(1),
666
+ owner: z6.string().min(1),
449
667
  /**
450
668
  * Which site's job — V1-3.
451
669
  *
@@ -458,39 +676,42 @@ var ClaimedJob = z4.object({
458
676
  * one, and so this reads as what it is: a fact about where the work came
459
677
  * from, not a second copy of the routing key.
460
678
  */
461
- site: z4.string().min(1).optional(),
679
+ site: z6.string().min(1).optional(),
462
680
  /**
463
- * Which service the site named, if it named one byollm_016 Phase B.
681
+ * Which of the owner's services runs thisresolved, not requested.
682
+ *
683
+ * The daemon picks the backend from this, so it has to be the answer
684
+ * rather than a wish. On a relayed route it is copied off the **grant**,
685
+ * where a control plane put the person's own mapping and signed it; a
686
+ * site never named it and could not.
464
687
  *
465
- * The same shape `site` documents above, and added for the same reason:
466
- * the stub carries it, the opened job did not, and everything downstream
467
- * of the payload lost it. Here that loss was not cosmetic — the daemon
468
- * picks the backend from this, so a job that selected a non-default
469
- * service would have been served by the default instead, which is the
470
- * substitution `NO_PAYLOAD_ROUTING` forbids.
688
+ * It used to be what the site asked for, which made a job that selected a
689
+ * non-default service liable to be served by the default instead the
690
+ * substitution `NO_PAYLOAD_ROUTING` forbids. Amendment L removed the
691
+ * asking; what is left is the answering.
471
692
  *
472
- * Optional, because absent means "the owner's default" and that is every
473
- * job written before selection existed.
693
+ * Optional, because direct mode has no control plane to resolve anything
694
+ * and the owner's own defaults answer under the ambiguity law.
474
695
  */
475
- service: z4.string().min(1).optional(),
696
+ service: z6.string().min(1).optional(),
476
697
  lease: Lease
477
698
  }).strict();
478
- var ResultProvenance = z4.object({
699
+ var ResultProvenance = z6.object({
479
700
  /** The audience the job ran under. */
480
701
  audience: Audience,
481
702
  /** The runner that produced it. */
482
- runnerId: z4.string().min(1),
703
+ runnerId: z6.string().min(1),
483
704
  /** The runner owner's id in this app's namespace. */
484
- runnerOwner: z4.string().min(1),
705
+ runnerOwner: z6.string().min(1),
485
706
  /** Which backend class produced it — an HTTP call or a sandboxed spawn. */
486
707
  backendClass: BackendClass,
487
708
  /** The model the runner reports having used. */
488
- model: z4.string().min(1),
709
+ model: z6.string().min(1),
489
710
  /**
490
711
  * False only for `self` jobs. When true the app MUST treat `text` as
491
712
  * untrusted third-party content.
492
713
  */
493
- untrusted: z4.boolean()
714
+ untrusted: z6.boolean()
494
715
  }).strict();
495
716
  function provenanceFor(input) {
496
717
  return {
@@ -502,62 +723,47 @@ function provenanceFor(input) {
502
723
  untrusted: input.audience !== "private"
503
724
  };
504
725
  }
505
- var RunMetadata = z4.object({
726
+ var RunMetadata = z6.object({
506
727
  /** Which model actually served it. */
507
- model: z4.string().min(1),
728
+ model: z6.string().min(1),
508
729
  backendClass: BackendClass,
509
730
  /** Wall-clock milliseconds the backend call took. */
510
- durationMs: z4.number().int().nonnegative()
731
+ durationMs: z6.number().int().nonnegative()
511
732
  }).strict();
512
- var JobResultOk = z4.object({
513
- outcome: z4.literal("ok"),
514
- text: z4.string(),
733
+ var JobResultOk = z6.object({
734
+ outcome: z6.literal("ok"),
735
+ text: z6.string(),
515
736
  /** Optional reference to a stored artifact; never a local path. */
516
- artifactUrl: z4.url().optional()
737
+ artifactUrl: z6.url().optional()
517
738
  }).strict();
518
- var JobResultError = z4.object({
519
- outcome: z4.literal("error"),
520
- code: z4.string().min(1),
521
- message: z4.string().min(1),
739
+ var JobResultError = z6.object({
740
+ outcome: z6.literal("error"),
741
+ code: z6.string().min(1),
742
+ message: z6.string().min(1),
522
743
  /** Whether the app may reasonably re-enqueue. */
523
- retryable: z4.boolean()
744
+ retryable: z6.boolean()
524
745
  }).strict();
525
- var JobResultCanceled = z4.object({
526
- outcome: z4.literal("canceled")
746
+ var JobResultCanceled = z6.object({
747
+ outcome: z6.literal("canceled")
527
748
  }).strict();
528
- var JobOutcome = z4.discriminatedUnion("outcome", [
749
+ var JobOutcome = z6.discriminatedUnion("outcome", [
529
750
  JobResultOk,
530
751
  JobResultError,
531
752
  JobResultCanceled
532
753
  ]);
533
- var RefusalReason = z4.enum([
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",
754
+ var RefusalReason = z6.enum([
550
755
  /**
551
756
  * Two or more services answer this kind and the owner has named no default,
552
757
  * so the kind is withheld. Nobody may pick on the owner's behalf — the wrong
553
758
  * guess is the metered one.
554
759
  *
555
- * Not collapsed into the value above, and the reason is that a kind is not
556
- * probeable. There are two kinds; a requester asking about one is not
557
- * enumerating a namespace, and learns nothing they could not learn by
558
- * looking at what the device advertises. It is also already what a roster
559
- * member sees on the devices page `awaitingDefault` carries exactly this,
560
- * by kind, for exactly this reason.
760
+ * Told apart from its neighbour deliberately, and the line is whether a
761
+ * requester can walk a namespace. There are two kinds; asking about one
762
+ * enumerates nothing they could not learn from what the device advertises,
763
+ * and the difference is actionable "the owner has not chosen" is fixable
764
+ * by the owner, "the default cannot serve you" is not. It is also already
765
+ * what a team member sees on the devices page: `awaitingDefault` carries
766
+ * exactly this, by kind, for exactly this reason.
561
767
  */
562
768
  "default-ambiguity",
563
769
  /**
@@ -566,34 +772,28 @@ var RefusalReason = z4.enum([
566
772
  *
567
773
  * The specimen: an owner's default for `llm.chat` is their Claude
568
774
  * subscription, self-locked by `SUBSCRIPTION_SELF_LOCK`. A team member's
569
- * unselected job resolves to it and can never be served by it. That must be
570
- * a refusal on the spot, not a wait that expires an hour later looking like
571
- * nobody was online.
775
+ * job resolves to it and can never be served by it. That must be a refusal
776
+ * on the spot, not a wait that expires an hour later looking like nobody
777
+ * was online.
572
778
  *
573
- * Bounded like the value above and probeable for the same reason it is not:
574
- * the requester named nothing, so there is no name space to walk.
779
+ * Bounded like the value above, and unprobeable for the same reason: the
780
+ * requester named nothing, so there is no name space to walk.
575
781
  */
576
782
  "default-unusable"
577
783
  ]);
578
- var JobRefused = z4.object({
579
- outcome: z4.literal("refused"),
784
+ var JobRefused = z6.object({
785
+ outcome: z6.literal("refused"),
580
786
  reason: RefusalReason,
581
787
  /** Plain words for a human reading a log, never parsed. */
582
- message: z4.string().min(1)
788
+ message: z6.string().min(1)
583
789
  }).strict();
584
790
  var REFUSAL_TEXT = Object.freeze({
585
- "select-unavailable": "that service is not available to you on this device",
586
791
  "default-ambiguity": "this device serves that kind from more than one service and its owner has not chosen which",
587
792
  "default-unusable": "this device's default for that kind cannot run work for you"
588
793
  });
589
- var REFUSED_SELECTION = Object.freeze({
590
- outcome: "refused",
591
- reason: "select-unavailable",
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),
794
+ var SealedOutcome = z6.object({ outcome: JobOutcome, ran: RunMetadata }).strict();
795
+ var DeliveredResult = z6.object({
796
+ jobId: z6.string().min(1),
597
797
  state: JobState,
598
798
  outcome: JobOutcome.optional(),
599
799
  provenance: ResultProvenance.optional(),
@@ -611,9 +811,9 @@ var DeliveredResult = z4.object({
611
811
  * runner ran the job, and the *server* stamps it — an app cannot supply
612
812
  * a substitute that hides what it is.
613
813
  */
614
- fallback: z4.literal(true).optional()
814
+ fallback: z6.literal(true).optional()
615
815
  }).strict();
616
- var SizeClass = z4.enum(["small", "medium", "large", "unbounded"]);
816
+ var SizeClass = z6.enum(["small", "medium", "large", "unbounded"]);
617
817
  var SIZE_CLASS_LIMITS = Object.freeze({
618
818
  small: 4e3,
619
819
  medium: 64e3,
@@ -628,11 +828,11 @@ function sizeClassOf(textChars) {
628
828
  if (textChars <= SIZE_CLASS_LIMITS.medium) return "medium";
629
829
  return "large";
630
830
  }
631
- var JobStub = z4.object({
632
- id: z4.string().min(1),
831
+ var JobStub = z6.object({
832
+ id: z6.string().min(1),
633
833
  kind: JobKind,
634
834
  /** The app's id for the user who enqueued it. */
635
- owner: z4.string().min(1),
835
+ owner: z6.string().min(1),
636
836
  /**
637
837
  * Which site this job belongs to — byollm_009 Amendment A §A.3.
638
838
  *
@@ -656,13 +856,13 @@ var JobStub = z4.object({
656
856
  * overlap window, and a daemon re-keys its own map by verifying that
657
857
  * signature against the key it already pinned (§A.3.1).
658
858
  */
659
- site: z4.string().min(1),
859
+ site: z6.string().min(1),
660
860
  audience: Audience,
661
861
  // `audienceAllow` is **not** here, and its absence is the enforcement —
662
862
  // cloud_008 §0.2.
663
863
  //
664
864
  // It was a list of the people who may run a job, travelling to every
665
- // routing party on every `named` job. byollm_001 Rev 1 §B settled who
865
+ // routing party on every shared job. byollm_001 Rev 1 §B settled who
666
866
  // decides that long before this schema existed: *the daemon's own list
667
867
  // decides, not the server's*, and `allowlist.predicateFor(origin)` is the
668
868
  // enforcement in both lanes. So this was a second answer to a question the
@@ -679,181 +879,68 @@ var JobStub = z4.object({
679
879
  // with it before offering. That is server-internal, where the party
680
880
  // holding the list authored it.
681
881
  /**
682
- * Which of the owner's services should answerbyollm_016 Phase B.
683
- *
684
- * **A selection from a menu, never a demand.** The owner advertises named
685
- * services; a site may name one of them, and that is the entire power the
686
- * field grants. It carries no model, no base URL, no flags — the daemon
687
- * resolves the name against its own config and nothing else, so what
688
- * actually runs is still decided exclusively by the person who owns the
689
- * hardware. A name that is not on that owner's menu is refused
690
- * (`select-unadvertised`), never silently substituted, because a
691
- * substitution is how "select" would quietly become "whatever we had".
692
- *
693
- * Absent means "the owner's default for this kind", which is the only
694
- * behaviour Phase A had.
695
- *
696
- * It travels because the router matches on it, under the rule the absent
697
- * `audienceAllow` above establishes: *a class the router acts on may
698
- * travel; membership never does.* This is a class.
699
- *
700
- * It is a **stub** field and never a payload field, which is the line
882
+ * Which of the site's declared purposes this job serves Amendment L.
883
+ *
884
+ * **A need, never a name.** The site's vocabulary is its own purposes;
885
+ * the person's is their services; and the two never meet. This field says
886
+ * "writing-assistant", and a control plane joins it to whatever that
887
+ * person mapped it to. The site learns only whether the slot was
888
+ * satisfiable.
889
+ *
890
+ * It replaced `service`, which let a site name one of the owner's
891
+ * services directly. That field is gone from both routes (Amendment L
892
+ * rider) and its refusal machinery with it — including the collapsed
893
+ * `select-unavailable`, which existed so that "no such service" and "not
894
+ * offered to you" could not be told apart. There is nothing left to
895
+ * probe: **a vocabulary that never crosses the boundary cannot be
896
+ * enumerated across it**, which is a stronger guarantee than the one the
897
+ * collapse gave.
898
+ *
899
+ * It travels for the reason the absent `audienceAllow` establishes: *a
900
+ * class the router acts on may travel; membership never does.* A purpose
901
+ * is a class, and the control plane acts on it.
902
+ *
903
+ * Optional because direct mode has no control plane to hold a mapping and
904
+ * is kind-only: the owner's own config and defaults answer, under the
905
+ * ambiguity law as shipped. Absent on a relayed route resolves against
906
+ * the site's reserved purpose, which a site that declared its own
907
+ * purposes will not have mapped — so the slot reads as unmapped and the
908
+ * site falls back, loudly enough and without a special case.
909
+ *
910
+ * A **stub** field and never a payload field, which is the line
701
911
  * `NO_PAYLOAD_ROUTING` draws: the prompt cannot reach it, so no amount of
702
912
  * user text can influence what runs.
703
913
  */
704
- service: z4.string().min(1).optional(),
914
+ purpose: z6.string().min(1).optional(),
705
915
  sizeClass: SizeClass,
706
916
  /** Reserved for byollm_006. False until streaming exists. */
707
- streaming: z4.boolean(),
917
+ streaming: z6.boolean(),
708
918
  /** Epoch ms after which the work is pointless; bounds ciphertext retention. */
709
- deadlineAt: z4.number().int().positive()
919
+ deadlineAt: z6.number().int().positive()
920
+ }).strict();
921
+ var ClaimedStub = JobStub.extend({
922
+ lease: Lease,
923
+ grant: SignedGrant.optional()
710
924
  }).strict();
711
- var ClaimedStub = JobStub.extend({ lease: Lease }).strict();
712
925
 
713
926
  // src/envelope.ts
714
927
  import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2 } from "crypto";
715
928
  import sodium from "libsodium-wrappers";
716
- import { z as z6 } from "zod";
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
929
+ import { z as z7 } from "zod";
843
930
  var readied;
844
931
  async function cryptoReady() {
845
932
  readied ??= sodium.ready;
846
933
  await readied;
847
934
  }
848
935
  var ENVELOPE_MAX_AGE_MS = 24 * 60 * 6e4;
849
- var EnvelopeDirection = z6.enum(["payload", "result"]);
850
- var SealedEnvelope = z6.object({
936
+ var EnvelopeDirection = z7.enum(["payload", "result"]);
937
+ var SealedEnvelope = z7.object({
851
938
  /** Base64url `crypto_box_seal` output over the signed plaintext. */
852
- ciphertext: z6.string().min(1),
939
+ ciphertext: z7.string().min(1),
853
940
  /** Who this was sealed to — the recipient checks it is them. */
854
- recipientKeyId: z6.string().min(1),
941
+ recipientKeyId: z7.string().min(1),
855
942
  /** Who signed it — the recipient checks this against its pin. */
856
- senderKeyId: z6.string().min(1),
943
+ senderKeyId: z7.string().min(1),
857
944
  direction: EnvelopeDirection,
858
945
  /**
859
946
  * When this ciphertext stops being worth keeping.
@@ -867,7 +954,7 @@ var SealedEnvelope = z6.object({
867
954
  * Not trusted as written: it is also inside the signature, so a changed
868
955
  * deadline fails to verify.
869
956
  */
870
- deadlineAt: z6.number().int().positive()
957
+ deadlineAt: z7.number().int().positive()
871
958
  }).strict();
872
959
  function signedBody(context, plaintext) {
873
960
  return Buffer.from(
@@ -962,15 +1049,15 @@ async function open(input) {
962
1049
 
963
1050
  // src/signing.ts
964
1051
  import { createHash as createHash2 } from "crypto";
965
- import { z as z7 } from "zod";
1052
+ import { z as z8 } from "zod";
966
1053
  var MAX_CLOCK_SKEW_MS = 12e4;
967
- var RequestSignature = z7.object({
1054
+ var RequestSignature = z8.object({
968
1055
  /** Which runner is calling. The server looks up its pinned identity. */
969
- runnerId: z7.string().min(1),
1056
+ runnerId: z8.string().min(1),
970
1057
  /** Epoch ms, bounded by {@link MAX_CLOCK_SKEW_MS}. */
971
- issuedAt: z7.number().int().positive(),
1058
+ issuedAt: z8.number().int().positive(),
972
1059
  /** Base64url Ed25519 signature over {@link canonicalRequest}. */
973
- signature: z7.string().min(1)
1060
+ signature: z8.string().min(1)
974
1061
  }).strict();
975
1062
  function canonicalRequest(input) {
976
1063
  const digest = createHash2("sha256").update(input.body, "utf8").digest("hex");
@@ -1023,73 +1110,50 @@ function verifyRequest(input) {
1023
1110
  return ok ? null : "bad-signature";
1024
1111
  }
1025
1112
 
1026
- // src/roster.ts
1027
- import { Buffer as Buffer2 } from "buffer";
1028
- import { z as z8 } from "zod";
1029
- var ROSTER_MAX_AGE_MS = 60 * 6e4;
1030
- var ROSTER_CONTEXT = "byollm/v1/roster";
1031
- var SignedRoster = z8.object({
1032
- /** Whose devices this roster governs. */
1033
- owner: z8.string().min(1),
1113
+ // src/manifest.ts
1114
+ import { z as z9 } from "zod";
1115
+ var RESERVED_PURPOSE = "default";
1116
+ var PurposeKey = z9.string().regex(
1117
+ /^[a-z0-9][a-z0-9-]*$/,
1118
+ "a purpose key is a lowercase slug \u2014 letters, digits and hyphens"
1119
+ ).max(64);
1120
+ var Purpose = z9.object({
1034
1121
  /**
1035
- * Who may have their `team` work run on this owner's devices.
1122
+ * What a person reads on the consent screen. The only rendered field.
1036
1123
  *
1037
- * Owner ids, sorted, so the same membership always produces the same
1038
- * bytes a document that differed by iteration order would produce a new
1039
- * signature every read and no way to tell a change from a shuffle.
1124
+ * Declared rather than derived from the key, because a key is a
1125
+ * compromise between machines and this is not. "Writing Assistant" is
1126
+ * what somebody understands; `writing-assistant` is what travels.
1040
1127
  */
1041
- members: z8.array(z8.string().min(1)),
1128
+ label: z9.string().min(1).max(80),
1129
+ /** One line of context for the consent screen. Optional. */
1130
+ description: z9.string().min(1).max(280).optional(),
1042
1131
  /**
1043
- * When the control plane signed it — epoch ms, and the **only** honest
1044
- * anchor for age.
1132
+ * The kinds this purpose uses.
1045
1133
  *
1046
- * Not when the daemon received it. A relay that simply withholds updates
1047
- * would otherwise keep a removed member served forever, which is the
1048
- * attack {@link ROSTER_MAX_AGE_MS} exists to bound.
1134
+ * A purpose may span kinds, and a mapping is per (purpose, kind) — so a
1135
+ * person can send this purpose's chat to one service and its generation
1136
+ * to another. Listing a kind here is what makes that slot appear.
1049
1137
  */
1050
- issuedAt: z8.number().int().positive(),
1051
- /** Base64url Ed25519 over {@link rosterStatement}. */
1052
- signature: z8.string().min(1)
1138
+ kinds: z9.array(JobKind).min(1)
1053
1139
  }).strict();
1054
- function rosterStatement(input) {
1055
- return Buffer2.from(
1056
- [
1057
- ROSTER_CONTEXT,
1058
- input.owner,
1059
- String(input.issuedAt),
1060
- [...input.members].sort().join("\0")
1061
- ].join("\n"),
1062
- "utf8"
1063
- );
1064
- }
1065
- function signRoster(keys, input) {
1066
- const members = [...input.members].sort();
1140
+ var Manifest = z9.record(PurposeKey, Purpose).refine((manifest) => Object.keys(manifest).length > 0, {
1141
+ message: "a manifest declares at least one purpose"
1142
+ }).refine((manifest) => !(RESERVED_PURPOSE in manifest), {
1143
+ 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`
1144
+ });
1145
+ function singlePurposeManifest(input) {
1067
1146
  return {
1068
- owner: input.owner,
1069
- members,
1070
- issuedAt: input.issuedAt,
1071
- signature: signWith(keys, rosterStatement({ ...input, members }))
1147
+ [RESERVED_PURPOSE]: { label: input.label, kinds: [...input.kinds] }
1072
1148
  };
1073
1149
  }
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
1150
 
1087
1151
  // src/succession.ts
1088
- import { z as z9 } from "zod";
1152
+ import { z as z10 } from "zod";
1089
1153
  var SUCCESSION_CONTEXT = "byollm/v1/site-succession";
1090
1154
  var RETIREMENT_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
1091
1155
  var MAX_SUCCESSION_CHAIN = 64;
1092
- var Succession = z9.object({
1156
+ var Succession = z10.object({
1093
1157
  /**
1094
1158
  * The predecessor's public identity — K1, in full.
1095
1159
  *
@@ -1099,7 +1163,7 @@ var Succession = z9.object({
1099
1163
  */
1100
1164
  identity: PublicIdentity,
1101
1165
  /** K1's signature over the statement naming K1 and its successor. */
1102
- signature: z9.string().min(1)
1166
+ signature: z10.string().min(1)
1103
1167
  }).strict();
1104
1168
  function successionStatement(fromKeyId, toKeyId) {
1105
1169
  return Buffer.from(`${SUCCESSION_CONTEXT}:${fromKeyId}:${toKeyId}`);
@@ -1193,13 +1257,15 @@ var MUSTS = Object.freeze({
1193
1257
  }),
1194
1258
  SITES_LOCALLY_APPROVED: must({
1195
1259
  id: "SITES_LOCALLY_APPROVED",
1196
- 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. A **verified succession** is not a changed key: a new key id carrying a signature, by a key this daemon has already approved, over a statement naming both key ids MUST be accepted without a new local approval \u2014 provided the control plane projects the same successor \u2014 and MUST be announced rather than applied silently.",
1260
+ 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
1261
  enforcedBy: "daemon",
1198
1262
  // Two kinds, and the second is the one that matters — V1-1.
1199
1263
  //
1200
1264
  // `construction`: the daemon cannot serve a site that is not in its
1201
- // pinned map, and admission refuses before a payload is fetched, so the
1202
- // ordinary path cannot reach a site nobody approved.
1265
+ // pinned map, and admission refuses before a payload is fetched and
1266
+ // since byollm_016 Amendment K, being in the map is no longer sufficient
1267
+ // either: a signed grant is, and the relay proposing the set cannot
1268
+ // produce one.
1203
1269
  //
1204
1270
  // `adversarial`: the property that survives is about a *sequence* —
1205
1271
  // remove the id, re-offer it under a different key — which no honest
@@ -1352,7 +1418,7 @@ var MUSTS = Object.freeze({
1352
1418
  }),
1353
1419
  NAMED_LOCAL_ALLOWLIST: must({
1354
1420
  id: "NAMED_LOCAL_ALLOWLIST",
1355
- 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.",
1421
+ 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
1422
  enforcedBy: "daemon",
1357
1423
  verifiedBy: "conformance",
1358
1424
  source: "byollm_001 Rev 1 \xA7B"
@@ -1578,7 +1644,7 @@ function mustsVerifiedBy(kind) {
1578
1644
  }
1579
1645
 
1580
1646
  // src/wire.ts
1581
- import { z as z10 } from "zod";
1647
+ import { z as z11 } from "zod";
1582
1648
  var PROTOCOL_VERSION = "0";
1583
1649
  var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze([
1584
1650
  PROTOCOL_VERSION
@@ -1621,46 +1687,43 @@ var ENDPOINTS = Object.freeze([
1621
1687
  "result",
1622
1688
  "release"
1623
1689
  ]);
1624
- var Capability = z10.object({
1690
+ var Capability = z11.object({
1625
1691
  kind: JobKind,
1626
1692
  /**
1627
1693
  * The owner's name for the service answering this kind — byollm_016.
1628
1694
  *
1629
1695
  * A device advertises *which* of its services serves a kind, not merely
1630
- * that something does. Phase B lets a job select by this name; until then
1631
- * it is what a device page shows and what a default is chosen between.
1696
+ * that something does. **A site never sees this**, and never did after
1697
+ * Amendment L: it is what a control plane resolves a person's mapping
1698
+ * against, so that the service a grant names is one this device actually
1699
+ * offers rather than one somebody invented.
1700
+ *
1701
+ * `isDefault` used to sit beside it, saying which row an unselected job
1702
+ * took. Nothing selects any more — a job names a purpose and a person's
1703
+ * mapping names a service — so there is no unselected job for a default
1704
+ * to catch, and the field went with the machinery it served.
1632
1705
  */
1633
- service: z10.string().min(1),
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(),
1706
+ service: z11.string().min(1),
1644
1707
  backendId: BackendIdSchema,
1645
1708
  backendClass: BackendClass,
1646
- model: z10.string().min(1),
1709
+ model: z11.string().min(1),
1647
1710
  offerScope: OfferScope
1648
1711
  }).strict();
1649
- var CapabilityMatrix = z10.array(Capability);
1650
- var WithheldKind = z10.object({
1712
+ var CapabilityMatrix = z11.array(Capability);
1713
+ var WithheldKind = z11.object({
1651
1714
  kind: JobKind,
1652
- claimants: z10.array(
1653
- z10.object({ service: z10.string().min(1), offer: OfferScope }).strict()
1715
+ claimants: z11.array(
1716
+ z11.object({ service: z11.string().min(1), offer: OfferScope }).strict()
1654
1717
  ).min(2)
1655
1718
  }).strict();
1656
- var PairStartRequest = z10.object({
1657
- protocolVersion: z10.literal(PROTOCOL_VERSION),
1658
- action: z10.literal("start"),
1659
- daemon: z10.object({
1660
- version: z10.string().min(1),
1719
+ var PairStartRequest = z11.object({
1720
+ protocolVersion: z11.literal(PROTOCOL_VERSION),
1721
+ action: z11.literal("start"),
1722
+ daemon: z11.object({
1723
+ version: z11.string().min(1),
1661
1724
  /** Shown in the app's runner list so a user can tell their machines apart. */
1662
- label: z10.string().min(1).max(120),
1663
- platform: z10.enum(["darwin", "linux", "win32"])
1725
+ label: z11.string().min(1).max(120),
1726
+ platform: z11.enum(["darwin", "linux", "win32"])
1664
1727
  }),
1665
1728
  /**
1666
1729
  * This machine's public keys (byollm_009 §5).
@@ -1672,29 +1735,29 @@ var PairStartRequest = z10.object({
1672
1735
  device: PublicIdentity,
1673
1736
  capabilities: CapabilityMatrix
1674
1737
  }).strict();
1675
- var PairStartResponse = z10.object({
1738
+ var PairStartResponse = z11.object({
1676
1739
  /** Secret the daemon polls with. Never shown to the user. */
1677
- deviceCode: z10.string().min(20),
1740
+ deviceCode: z11.string().min(20),
1678
1741
  /** Short code the user reads and confirms in the browser. */
1679
- userCode: z10.string().min(4).max(16),
1742
+ userCode: z11.string().min(4).max(16),
1680
1743
  /** Where the user approves. Must be on the server's own origin. */
1681
- verificationUrl: z10.url(),
1744
+ verificationUrl: z11.url(),
1682
1745
  /** Epoch ms after which the code is dead ({@link MUSTS.PAIR_CODE_EXPIRES}). */
1683
- expiresAt: z10.number().int().positive(),
1746
+ expiresAt: z11.number().int().positive(),
1684
1747
  /** How often the daemon may poll. */
1685
- pollIntervalMs: z10.number().int().min(500).max(6e4)
1748
+ pollIntervalMs: z11.number().int().min(500).max(6e4)
1686
1749
  }).strict();
1687
- var PairPollRequest = z10.object({
1688
- protocolVersion: z10.literal(PROTOCOL_VERSION),
1689
- action: z10.literal("poll"),
1690
- deviceCode: z10.string().min(20)
1750
+ var PairPollRequest = z11.object({
1751
+ protocolVersion: z11.literal(PROTOCOL_VERSION),
1752
+ action: z11.literal("poll"),
1753
+ deviceCode: z11.string().min(20)
1691
1754
  }).strict();
1692
- var PairPollResponse = z10.discriminatedUnion("status", [
1693
- z10.object({ status: z10.literal("pending") }).strict(),
1694
- z10.object({ status: z10.literal("denied") }).strict(),
1695
- z10.object({ status: z10.literal("expired") }).strict(),
1696
- z10.object({
1697
- status: z10.literal("approved"),
1755
+ var PairPollResponse = z11.discriminatedUnion("status", [
1756
+ z11.object({ status: z11.literal("pending") }).strict(),
1757
+ z11.object({ status: z11.literal("denied") }).strict(),
1758
+ z11.object({ status: z11.literal("expired") }).strict(),
1759
+ z11.object({
1760
+ status: z11.literal("approved"),
1698
1761
  // `runnerToken` is gone — cloud_008 §2.4, finding 37.
1699
1762
  //
1700
1763
  // It was minted here, hashed into `RunnerRecord.tokenHash`, written to
@@ -1711,11 +1774,11 @@ var PairPollResponse = z10.discriminatedUnion("status", [
1711
1774
  // `REQUESTS_SIGNED_NOT_BEARER` was already the rule and was already
1712
1775
  // enforced — every authenticated call is signed by the device's pinned
1713
1776
  // identity key. This removes the thing the MUST is named after.
1714
- runnerId: z10.string().min(1),
1777
+ runnerId: z11.string().min(1),
1715
1778
  /** The app's id for the approving user — this daemon's owner forever. */
1716
- owner: z10.string().min(1),
1779
+ owner: z11.string().min(1),
1717
1780
  /** Display name for the trust UI, if the app offers one. */
1718
- ownerLabel: z10.string().optional(),
1781
+ ownerLabel: z11.string().optional(),
1719
1782
  /**
1720
1783
  * The sites this pairing covers, for the daemon to pin (byollm_009 §5),
1721
1784
  * keyed by each site's identity key id — cloud_009 §5.
@@ -1734,55 +1797,55 @@ var PairPollResponse = z10.discriminatedUnion("status", [
1734
1797
  * runner's lookup is a map read rather than a join across two
1735
1798
  * namespaces.
1736
1799
  */
1737
- sites: z10.record(z10.string().min(1), PublicIdentity),
1800
+ sites: z11.record(z11.string().min(1), PublicIdentity),
1738
1801
  /**
1739
- * The control plane's roster-signing key, pinned here — Amendment G.
1802
+ * The control plane's grant-signing key, pinned here — Amendment J.
1740
1803
  *
1741
1804
  * **Pairing is when, and that is the whole question.** Pairing is
1742
1805
  * already the ceremony where an owner proves out of band that this
1743
1806
  * device is theirs, so a key learned here rides trust that has already
1744
- * happened. The rejected alternative is trust-on-first-roster, and it
1745
- * is rejected because it hands the decision back to the relay: a daemon
1746
- * that learns whose signature to trust from the first roster to arrive
1747
- * has its membership authority chosen by whoever controls delivery.
1807
+ * happened. The rejected alternative is trust-on-first-grant, and it is
1808
+ * rejected because it hands the decision back to the relay: a daemon
1809
+ * that learns whose signature to trust from the first grant to arrive
1810
+ * has its admission authority chosen by whoever controls delivery.
1748
1811
  *
1749
1812
  * Optional on the wire, and only on the wire: a direct-mode server has
1750
- * no control plane and signs no rosters, and a daemon that never
1751
- * receives one simply serves no `team` job through it. It is not
1752
- * optional for a hub a hub that omitted it would be asking devices to
1753
- * accept rosters from nobody in particular.
1813
+ * no control plane and signs nothing, and a daemon that receives no key
1814
+ * serves its owner alone. It is not optional for a relay with a control
1815
+ * planeone that omitted it would be asking devices to accept grants
1816
+ * from nobody in particular, and would find every job refused.
1754
1817
  *
1755
- * Rotation is Amendment C's, with no path where a roster teaches a
1818
+ * Rotation is Amendment C's, with no path where a grant teaches a
1756
1819
  * daemon a new key.
1757
1820
  */
1758
- controlPlanePublic: z10.string().min(1).optional()
1821
+ controlPlanePublic: z11.string().min(1).optional()
1759
1822
  }).strict()
1760
1823
  ]);
1761
- var PairRequest = z10.discriminatedUnion("action", [
1824
+ var PairRequest = z11.discriminatedUnion("action", [
1762
1825
  PairStartRequest,
1763
1826
  PairPollRequest
1764
1827
  ]);
1765
- var ClaimRequest = z10.object({
1766
- protocolVersion: z10.literal(PROTOCOL_VERSION),
1767
- runnerId: z10.string().min(1),
1828
+ var ClaimRequest = z11.object({
1829
+ protocolVersion: z11.literal(PROTOCOL_VERSION),
1830
+ runnerId: z11.string().min(1),
1768
1831
  /** Re-sent on every claim so a server never matches against a stale matrix. */
1769
1832
  capabilities: CapabilityMatrix,
1770
1833
  /** Upper bound on jobs to return; the server may return fewer. */
1771
- max: z10.number().int().min(1).max(64)
1834
+ max: z11.number().int().min(1).max(64)
1772
1835
  }).strict();
1773
- var ClaimResponse = z10.object({
1836
+ var ClaimResponse = z11.object({
1774
1837
  /**
1775
1838
  * Stubs, not jobs. The payload arrives from `fetch`, sealed to whichever
1776
1839
  * device claimed — see {@link JobStub} for the exhaustive metadata list.
1777
1840
  */
1778
- jobs: z10.array(ClaimedStub),
1841
+ jobs: z11.array(ClaimedStub),
1779
1842
  /** Lease duration granted, so the daemon knows its renewal deadline. */
1780
- leaseMs: z10.number().int().positive()
1843
+ leaseMs: z11.number().int().positive()
1781
1844
  }).strict();
1782
- var HeartbeatRequest = z10.object({
1783
- protocolVersion: z10.literal(PROTOCOL_VERSION),
1784
- runnerId: z10.string().min(1),
1785
- daemonVersion: z10.string().min(1),
1845
+ var HeartbeatRequest = z11.object({
1846
+ protocolVersion: z11.literal(PROTOCOL_VERSION),
1847
+ runnerId: z11.string().min(1),
1848
+ daemonVersion: z11.string().min(1),
1786
1849
  capabilities: CapabilityMatrix,
1787
1850
  /**
1788
1851
  * Kinds this device is withholding, and why it can be said.
@@ -1791,20 +1854,20 @@ var HeartbeatRequest = z10.object({
1791
1854
  * older daemon against a newer hub is simply a device with no withheld
1792
1855
  * kinds rather than a parse failure.
1793
1856
  */
1794
- withheld: z10.array(WithheldKind).default([]),
1857
+ withheld: z11.array(WithheldKind).default([]),
1795
1858
  /**
1796
1859
  * Leases this daemon believes it holds; the server renews exactly these.
1797
1860
  *
1798
1861
  * Lease ids rather than job ids, so a replayed heartbeat cannot renew a
1799
1862
  * grant the runner no longer holds — see {@link Lease.id}.
1800
1863
  */
1801
- activeLeases: z10.array(
1802
- z10.object({ jobId: z10.string().min(1), leaseId: z10.string().min(1) })
1864
+ activeLeases: z11.array(
1865
+ z11.object({ jobId: z11.string().min(1), leaseId: z11.string().min(1) })
1803
1866
  ),
1804
1867
  /** True while the owner has the daemon paused; the server stops offering work. */
1805
- paused: z10.boolean()
1868
+ paused: z11.boolean()
1806
1869
  }).strict();
1807
- var HeartbeatResponse = z10.object({
1870
+ var HeartbeatResponse = z11.object({
1808
1871
  /**
1809
1872
  * The sites this daemon may serve, right now — cloud_008 finding 59.
1810
1873
  *
@@ -1822,7 +1885,7 @@ var HeartbeatResponse = z10.object({
1822
1885
  * rather than being told a second time — two fields for one fact is how
1823
1886
  * they drift.
1824
1887
  */
1825
- sites: z10.record(z10.string().min(1), PublicIdentity),
1888
+ sites: z11.record(z11.string().min(1), PublicIdentity),
1826
1889
  /**
1827
1890
  * How a site's current key traces back to one this daemon already holds —
1828
1891
  * byollm_009 Amendment C.
@@ -1840,11 +1903,11 @@ var HeartbeatResponse = z10.object({
1840
1903
  * history is public by construction, because a daemon that cannot read it
1841
1904
  * cannot verify it.
1842
1905
  */
1843
- successions: z10.record(
1844
- z10.string().min(1),
1845
- z10.object({
1906
+ successions: z11.record(
1907
+ z11.string().min(1),
1908
+ z11.object({
1846
1909
  /** Oldest last, as the projection carries it. */
1847
- succeeds: z10.array(Succession).max(MAX_SUCCESSION_CHAIN),
1910
+ succeeds: z11.array(Succession).max(MAX_SUCCESSION_CHAIN),
1848
1911
  /**
1849
1912
  * Until when the superseded key may still sign work — epoch ms.
1850
1913
  *
@@ -1853,7 +1916,7 @@ var HeartbeatResponse = z10.object({
1853
1916
  * window indefinitely would be a two-key site forever, decided by
1854
1917
  * the party this design does not trust.
1855
1918
  */
1856
- retiringUntil: z10.number().int().positive().optional()
1919
+ retiringUntil: z11.number().int().positive().optional()
1857
1920
  }).strict()
1858
1921
  ).optional(),
1859
1922
  /**
@@ -1866,8 +1929,8 @@ var HeartbeatResponse = z10.object({
1866
1929
  * is the unique grant and the daemon already keys its work by it; this is
1867
1930
  * the same shape `activeLeases` sends in the other direction.
1868
1931
  */
1869
- cancel: z10.array(
1870
- z10.object({ jobId: z10.string().min(1), leaseId: z10.string().min(1) }).strict()
1932
+ cancel: z11.array(
1933
+ z11.object({ jobId: z11.string().min(1), leaseId: z11.string().min(1) }).strict()
1871
1934
  ),
1872
1935
  // `leases` is deliberately absent — cloud_008 §1.4b, finding 16.
1873
1936
  //
@@ -1897,11 +1960,11 @@ var HeartbeatResponse = z10.object({
1897
1960
  * ambiguous across sites, and "the lease you no longer hold" is exactly
1898
1961
  * what this field means anyway.
1899
1962
  */
1900
- lost: z10.array(
1901
- z10.object({ jobId: z10.string().min(1), leaseId: z10.string().min(1) }).strict()
1963
+ lost: z11.array(
1964
+ z11.object({ jobId: z11.string().min(1), leaseId: z11.string().min(1) }).strict()
1902
1965
  ),
1903
1966
  /** Server clock, so a daemon with a skewed clock still honors leases. */
1904
- serverTime: z10.number().int().positive(),
1967
+ serverTime: z11.number().int().positive(),
1905
1968
  /**
1906
1969
  * Sites whose disclosure the user must read again before work moves —
1907
1970
  * cloud_008 finding 48, named rather than counted.
@@ -1916,28 +1979,13 @@ var HeartbeatResponse = z10.object({
1916
1979
  * operator stopped it" — one word with two subjects on two halves of one
1917
1980
  * exchange is a confusion nobody untangles from a log.
1918
1981
  */
1919
- awaitingConsent: z10.array(z10.string().min(1)),
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()
1982
+ awaitingConsent: z11.array(z11.string().min(1))
1935
1983
  }).strict();
1936
- var ResultDisposition = z10.enum(["ok", "error", "canceled"]);
1937
- var ResultRequest = z10.object({
1938
- protocolVersion: z10.literal(PROTOCOL_VERSION),
1939
- runnerId: z10.string().min(1),
1940
- jobId: z10.string().min(1),
1984
+ var ResultDisposition = z11.enum(["ok", "error", "canceled"]);
1985
+ var ResultRequest = z11.object({
1986
+ protocolVersion: z11.literal(PROTOCOL_VERSION),
1987
+ runnerId: z11.string().min(1),
1988
+ jobId: z11.string().min(1),
1941
1989
  /**
1942
1990
  * The grant this result was produced under — cloud_008 §1.4a.
1943
1991
  *
@@ -1960,7 +2008,7 @@ var ResultRequest = z10.object({
1960
2008
  * learned once already, when a replayed release yanked a later grant, and
1961
2009
  * it applies here for the same reason.
1962
2010
  */
1963
- leaseId: z10.string().min(1),
2011
+ leaseId: z11.string().min(1),
1964
2012
  /**
1965
2013
  * The outcome, sealed to the site and signed by the device.
1966
2014
  *
@@ -1992,12 +2040,12 @@ var ResultRequest = z10.object({
1992
2040
  // on it, so it is a class a routing party consumes. Nobody between the
1993
2041
  // two ends consumes these.
1994
2042
  }).strict();
1995
- var ResultResponse = z10.object({
2043
+ var ResultResponse = z11.object({
1996
2044
  /**
1997
2045
  * False when this submission wrote nothing — the daemon should discard,
1998
2046
  * not retry ({@link MUSTS.RESULT_IDEMPOTENT}).
1999
2047
  */
2000
- accepted: z10.boolean(),
2048
+ accepted: z11.boolean(),
2001
2049
  /**
2002
2050
  * True when this device had already recorded this job's result.
2003
2051
  *
@@ -2011,13 +2059,13 @@ var ResultResponse = z10.object({
2011
2059
  * the same refusal it would get for a job that is *not* terminal, so a job
2012
2060
  * id cannot be used as a terminality probe.
2013
2061
  */
2014
- duplicate: z10.boolean().optional(),
2062
+ duplicate: z11.boolean().optional(),
2015
2063
  /** The job's state after this submission. */
2016
- state: z10.string().min(1)
2064
+ state: z11.string().min(1)
2017
2065
  }).strict();
2018
- var ReleaseRequest = z10.object({
2019
- protocolVersion: z10.literal(PROTOCOL_VERSION),
2020
- runnerId: z10.string().min(1),
2066
+ var ReleaseRequest = z11.object({
2067
+ protocolVersion: z11.literal(PROTOCOL_VERSION),
2068
+ runnerId: z11.string().min(1),
2021
2069
  /**
2022
2070
  * Which leases to release — the grant, not just the job.
2023
2071
  *
@@ -2025,24 +2073,24 @@ var ReleaseRequest = z10.object({
2025
2073
  * moment it arrives, which for a replayed request is not the lease the
2026
2074
  * daemon meant. See {@link Lease.id}.
2027
2075
  */
2028
- leases: z10.array(
2029
- z10.object({ jobId: z10.string().min(1), leaseId: z10.string().min(1) })
2076
+ leases: z11.array(
2077
+ z11.object({ jobId: z11.string().min(1), leaseId: z11.string().min(1) })
2030
2078
  ),
2031
2079
  /**
2032
2080
  * Why, so the app's runner list can say something true.
2033
2081
  *
2034
- * `refused` is load-bearing, not cosmetic: the server cannot evaluate a
2035
- * daemon's *local* `named` allowlist (§4.2), so it may legitimately offer
2082
+ * `refused` is load-bearing, not cosmetic: the server cannot evaluate
2083
+ * what a device will admit (§4.2), so it may legitimately offer
2036
2084
  * a job this daemon then declines. The server MUST record the refusal and
2037
2085
  * stop offering that job to that runner, or the pair would spin between
2038
2086
  * claim and release forever.
2039
2087
  */
2040
- reason: z10.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
2088
+ reason: z11.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
2041
2089
  }).strict();
2042
- var ReleaseResponse = z10.object({
2043
- released: z10.array(z10.string().min(1))
2090
+ var ReleaseResponse = z11.object({
2091
+ released: z11.array(z11.string().min(1))
2044
2092
  }).strict();
2045
- var WireErrorCode = z10.enum([
2093
+ var WireErrorCode = z11.enum([
2046
2094
  "bad-request",
2047
2095
  "unsupported-protocol-version",
2048
2096
  // "We do not know who you are." Exactly 401, and only that — cloud_008
@@ -2092,9 +2140,9 @@ var WireErrorCode = z10.enum([
2092
2140
  "rate-limited",
2093
2141
  "server-error"
2094
2142
  ]);
2095
- var WireError = z10.object({
2143
+ var WireError = z11.object({
2096
2144
  error: WireErrorCode,
2097
- message: z10.string().min(1),
2145
+ message: z11.string().min(1),
2098
2146
  /**
2099
2147
  * What this server speaks, on `unsupported-protocol-version` — §B.4.
2100
2148
  *
@@ -2108,10 +2156,10 @@ var WireError = z10.object({
2108
2156
  * Modelled the way `clock-skew`'s two fields already are — code-specific
2109
2157
  * extras, refused on any other code by the refinement below.
2110
2158
  */
2111
- supported: z10.array(z10.string().min(1)).optional(),
2112
- minimum: z10.string().min(1).optional(),
2159
+ supported: z11.array(z11.string().min(1)).optional(),
2160
+ minimum: z11.string().min(1).optional(),
2113
2161
  /** Seconds; mirrors Retry-After for `rate-limited` and `server-error`. */
2114
- retryAfter: z10.number().int().nonnegative().optional(),
2162
+ retryAfter: z11.number().int().nonnegative().optional(),
2115
2163
  /**
2116
2164
  * The server's clock, and the window it allows. `clock-skew` only.
2117
2165
  *
@@ -2121,8 +2169,8 @@ var WireError = z10.object({
2121
2169
  * heartbeat response returns the same value, and so does every `Date`
2122
2170
  * header.
2123
2171
  */
2124
- serverTime: z10.number().int().positive().optional(),
2125
- maxSkewMs: z10.number().int().positive().optional()
2172
+ serverTime: z11.number().int().positive().optional(),
2173
+ maxSkewMs: z11.number().int().positive().optional()
2126
2174
  }).strict().superRefine((error, ctx) => {
2127
2175
  const skew = error.error === "clock-skew";
2128
2176
  const carried = error.serverTime !== void 0 || error.maxSkewMs !== void 0;
@@ -2173,16 +2221,16 @@ var ERROR_STATUS = Object.freeze({
2173
2221
  "rate-limited": 429,
2174
2222
  "server-error": 500
2175
2223
  });
2176
- var FetchRequest = z10.object({
2224
+ var FetchRequest = z11.object({
2177
2225
  // `literal`, like every other request — V1-17. This one said
2178
2226
  // `string().min(1)`, so a daemon speaking a version this server does not
2179
2227
  // know got past the handshake on the one endpoint that hands over a
2180
2228
  // sealed payload. The version check exists so that a mismatch is a named
2181
2229
  // refusal rather than a schema failure three fields later; here it was
2182
2230
  // neither.
2183
- protocolVersion: z10.literal(PROTOCOL_VERSION),
2184
- runnerId: z10.string().min(1),
2185
- jobId: z10.string().min(1),
2231
+ protocolVersion: z11.literal(PROTOCOL_VERSION),
2232
+ runnerId: z11.string().min(1),
2233
+ jobId: z11.string().min(1),
2186
2234
  /**
2187
2235
  * The grant this daemon holds.
2188
2236
  *
@@ -2190,9 +2238,9 @@ var FetchRequest = z10.object({
2190
2238
  * only the job would be answerable for whatever lease exists when it
2191
2239
  * arrives ({@link Lease.id}).
2192
2240
  */
2193
- leaseId: z10.string().min(1)
2241
+ leaseId: z11.string().min(1)
2194
2242
  }).strict();
2195
- var FetchResponse = z10.object({
2243
+ var FetchResponse = z11.object({
2196
2244
  /**
2197
2245
  * The work, sealed to the device that claimed it — byollm_009 §6.
2198
2246
  *
@@ -2211,6 +2259,8 @@ export {
2211
2259
  BackendClass,
2212
2260
  BackendCost,
2213
2261
  BackendIdSchema,
2262
+ CLOCK_ATTRIBUTION_MS,
2263
+ CLOCK_SKEW_WARN_MS,
2214
2264
  Capability,
2215
2265
  CapabilityMatrix,
2216
2266
  ChatMessage,
@@ -2227,6 +2277,9 @@ export {
2227
2277
  EnvelopeDirection,
2228
2278
  FetchRequest,
2229
2279
  FetchResponse,
2280
+ GRANT_CONTEXT,
2281
+ GRANT_MAX_AGE_MS,
2282
+ GRANT_SIGNED_FIELDS,
2230
2283
  GeneratePayload,
2231
2284
  HeartbeatRequest,
2232
2285
  HeartbeatResponse,
@@ -2247,6 +2300,7 @@ export {
2247
2300
  MIN_PROTOCOL_VERSION,
2248
2301
  MUSTS,
2249
2302
  MUST_IDS,
2303
+ Manifest,
2250
2304
  MatchRefusal,
2251
2305
  OFFER_SCOPES,
2252
2306
  OfferScope,
@@ -2259,10 +2313,10 @@ export {
2259
2313
  PairStartRequest,
2260
2314
  PairStartResponse,
2261
2315
  PublicIdentity,
2316
+ Purpose,
2262
2317
  REFUSAL_MESSAGES,
2318
+ RESERVED_PURPOSE,
2263
2319
  RETIREMENT_WINDOW_MS,
2264
- ROSTER_CONTEXT,
2265
- ROSTER_MAX_AGE_MS,
2266
2320
  RefusalReason,
2267
2321
  ReleaseRequest,
2268
2322
  ReleaseResponse,
@@ -2277,7 +2331,7 @@ export {
2277
2331
  SUPPORTED_PROTOCOL_VERSIONS,
2278
2332
  SealedEnvelope,
2279
2333
  SealedOutcome,
2280
- SignedRoster,
2334
+ SignedGrant,
2281
2335
  SizeClass,
2282
2336
  StoredKeys,
2283
2337
  Succession,
@@ -2296,6 +2350,7 @@ export {
2296
2350
  effectiveOfferScope,
2297
2351
  fingerprint,
2298
2352
  generateKeys,
2353
+ grantStatement,
2299
2354
  isBackendId,
2300
2355
  isCloudTaggedModel,
2301
2356
  isJobKind,
@@ -2310,20 +2365,20 @@ export {
2310
2365
  provenanceFor,
2311
2366
  publicIdentityOf,
2312
2367
  resolveCost,
2313
- rosterStatement,
2314
2368
  seal,
2369
+ signGrant,
2315
2370
  signRequest,
2316
- signRoster,
2317
2371
  signSiteRequest,
2318
2372
  signSuccession,
2319
2373
  signWith,
2374
+ singlePurposeManifest,
2320
2375
  sizeClassCeiling,
2321
2376
  sizeClassOf,
2322
2377
  successionStatement,
2378
+ verifyGrant,
2323
2379
  verifyLink,
2324
2380
  verifyPublicIdentity,
2325
2381
  verifyRequest,
2326
- verifyRoster,
2327
2382
  verifySiteRequest,
2328
2383
  verifyWith,
2329
2384
  walkSuccession