@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.d.ts CHANGED
@@ -22,9 +22,9 @@ type BackendClass = z.infer<typeof BackendClass>;
22
22
  *
23
23
  * This replaced a two-valued `account` field that conflated two unrelated
24
24
  * constraints and, in doing so, left a hole: `openai-http` was "open", but it
25
- * accepts an API key, so an owner could point it at a paid endpoint, offer it
26
- * `public`, and donate their credit balance to strangers. The community
27
- * budgets cap job *count*, not spend.
25
+ * accepts an API key, so an owner could point it at a paid endpoint, share it,
26
+ * and donate their credit balance to strangers. The community budgets cap job
27
+ * *count*, not spend.
28
28
  *
29
29
  * - `free` — local compute. Costs electricity, not money. Shareable.
30
30
  * - `metered` — per-token billing against the owner's account. Legal to
@@ -165,7 +165,7 @@ declare function backendDescriptor(id: BackendId): BackendDescriptor;
165
165
  * an act by the machine's owner against their own account, and the threat
166
166
  * model here is a hostile *job*, not an owner routing around a rule that
167
167
  * exists to protect them. What this catches is the accident — a remote paid
168
- * endpoint offered `public` because nobody thought about the bill. See
168
+ * endpoint offered to a team because nobody thought about the bill. See
169
169
  * `docs/security.md` §4a.
170
170
  */
171
171
  declare function isLocalHost(hostname: string): boolean;
@@ -270,54 +270,53 @@ declare function classifyCost(id: BackendId, baseUrl: string | undefined, model:
270
270
  * Who may run a job, declared by the app that enqueued it.
271
271
  *
272
272
  * - `private` — only the job owner's own devices.
273
- * - `team` — a device whose owner has this person on their roster.
274
- * - `public` — any device offering `public` compute.
273
+ * - `team` — a device whose owner admits this person.
275
274
  *
276
275
  * **One vocabulary, ruled 2026-08-24.** These were `self | named | public`
277
- * while {@link OfferScope} became `private | team | public`, which would have
278
- * left every seam where the two meet speaking two languages for one idea, and
279
- * every doc explaining "self versus private" for ever. They are still
280
- * independent axes — a job says who may run it, a service says who it will run
281
- * for — and a job runs only where both agree
282
- * ({@link MUSTS.AUDIENCE_BOTH_SIDES}). Sharing the words costs nothing and
283
- * saves a mapping layer nobody would have enjoyed maintaining.
276
+ * while {@link OfferScope} used different words for the same idea, which would
277
+ * have left every seam where the two meet speaking two languages, and every
278
+ * doc explaining "self versus private" for ever. They are still independent
279
+ * axes — a job says who may run it, a service says who it will run for — and a
280
+ * job runs only where both agree ({@link MUSTS.AUDIENCE_BOTH_SIDES}).
281
+ *
282
+ * **`public` is gone, ruled 2026-08-26 (byollm_016).** Not deprecated,
283
+ * removed, and removed from the OSS daemon too rather than parked as a
284
+ * community posture. The argument was a measurement rather than a preference:
285
+ * device-side admission had never once been exercised end to end, because
286
+ * every cross-user test ran against a publicly offered service and
287
+ * {@link matchAudience} returned ALLOWED for those *without consulting the
288
+ * device at all*. `public` was the off switch for admission, and an enum with
289
+ * a value that skips verification is a fail-open waiting for the wiring bug
290
+ * that reaches it. There is now no such value.
284
291
  */
285
292
  declare const Audience: z.ZodEnum<{
286
293
  private: "private";
287
294
  team: "team";
288
- public: "public";
289
295
  }>;
290
296
  type Audience = z.infer<typeof Audience>;
291
297
  /**
292
298
  * What a device's owner is willing to run for other people, per service.
293
299
  *
294
300
  * - `private` — the owner's own work only.
295
- * - `team` — the owner's roster. Membership is **central**, not per-person:
296
- * the device follows the roster rather than holding its own copy of who is
297
- * in it (byollm_016, 2026-08-24).
298
- * - `public` — any job, from anyone. This is the community posture the daemon
299
- * has always had (`byollm offer <service> public`), bounded by community
300
- * budgets. byollm_016 parks it as a **hosted-product** surface the cloud
301
- * dashboard does not offer it which is not the same as removing it from
302
- * the daemon, and the CLI still accepts it.
303
- *
304
- * Deliberately *not* the same words as {@link Audience}, which they used to
305
- * share. They are independent axes — a job says who may run it, a service says
306
- * who it will run for — and identical spellings made that read as one concept
307
- * with two homes. A job runs only where both agree
308
- * ({@link MUSTS.AUDIENCE_BOTH_SIDES}); {@link matches} is the one place the
309
- * correspondence lives.
301
+ * - `team` — whoever the owner's authority admits. Membership is **central**,
302
+ * not per-person: the device follows what it is told by a signature it can
303
+ * check, rather than holding its own copy of who is in it (byollm_016).
304
+ *
305
+ * Two values, and no third that means "everyone". See {@link Audience} for
306
+ * why `public` was removed rather than parked, and note the shape of the
307
+ * remaining enum: **every value left requires the device to verify
308
+ * something.** `private` checks the owner; `team` checks admission. That is
309
+ * the property, not an accident of there being two.
310
310
  */
311
311
  declare const OfferScope: z.ZodEnum<{
312
312
  private: "private";
313
313
  team: "team";
314
- public: "public";
315
314
  }>;
316
315
  type OfferScope = z.infer<typeof OfferScope>;
317
316
  /** All audience values, in widening order. */
318
- declare const AUDIENCES: readonly ("private" | "team" | "public")[];
317
+ declare const AUDIENCES: readonly ("private" | "team")[];
319
318
  /** All offer scopes, in widening order. */
320
- declare const OFFER_SCOPES: readonly ("private" | "team" | "public")[];
319
+ declare const OFFER_SCOPES: readonly ("private" | "team")[];
321
320
  /**
322
321
  * Why a job was refused. Distinct codes because byollm_002 requires that
323
322
  * different truths never share a message — "no matching work" and "refused on
@@ -356,7 +355,7 @@ interface SpendConsent {
356
355
  * its matcher call, so no code path can observe a scope wider than the cost
357
356
  * class allows:
358
357
  *
359
- * - `subscription` is locked to `self` regardless of config
358
+ * - `subscription` is locked to `private` regardless of config
360
359
  * ({@link MUSTS.SUBSCRIPTION_SELF_LOCK}) — someone else's terms.
361
360
  * - `metered` narrows to `private` unless the owner has explicitly acknowledged
362
361
  * the spend ({@link MUSTS.METERED_DEFAULTS_SELF}) — their money.
@@ -374,7 +373,8 @@ interface MatchJob {
374
373
  readonly audience: Audience;
375
374
  /**
376
375
  * Optional server-side restriction on which runner owners may take a
377
- * `named` job. Defence in depth only the daemon's local allowlist is the
376
+ * `team` job. Defence in depth only, and direct-mode only it never
377
+ * reaches a daemon (cloud_008 §0.2), so the device's own admission is the
378
378
  * enforcing side ({@link MUSTS.NAMED_LOCAL_ALLOWLIST}).
379
379
  */
380
380
  readonly audienceAllow?: readonly string[] | undefined;
@@ -390,13 +390,24 @@ interface MatchDaemon {
390
390
  /** What the owner agreed to spend on others, for a `metered` backend. */
391
391
  readonly spend?: SpendConsent | undefined;
392
392
  /**
393
- * Does this daemon's *local* allowlist admit the given owner for the server
394
- * origin the job came from? Supplied as a predicate so the protocol package
395
- * stays free of file I/O; the daemon passes its allowlist, the server
396
- * passes a conservative `() => true` because it cannot know a remote
397
- * daemon's local list and must not pretend to.
393
+ * Has something **this device verified** admitted the job's owner?
394
+ *
395
+ * A predicate rather than a value so the protocol package stays free of
396
+ * both file I/O and signature state. What supplies it has changed twice and
397
+ * will change again — a local allowlist, then a held roster, and now a
398
+ * claim-time signed grant (Amendment J) — and the law it feeds has not
399
+ * changed at all: a `team` service runs a stranger's work only when
400
+ * somebody this device can check said so.
401
+ *
402
+ * The server passes a conservative `() => true`: it cannot know what a
403
+ * remote device verified and must not pretend to. The device is the
404
+ * enforcing side, which is the whole point of asking here.
405
+ *
406
+ * Named for the question, not for where the answer lives. This was called
407
+ * `locallyAllows`, and "locally" stopped being true the moment the answer
408
+ * came from a document somebody else signed.
398
409
  */
399
- readonly locallyAllows: (owner: string) => boolean;
410
+ readonly admits: (owner: string) => boolean;
400
411
  }
401
412
  /**
402
413
  * Decide whether a job may run on a daemon.
@@ -405,8 +416,8 @@ interface MatchDaemon {
405
416
  * 1. the job's audience must admit the daemon's owner, and
406
417
  * 2. the backend's offer scope must admit the job's owner.
407
418
  *
408
- * The full nine-way matrix (three audiences × three offer scopes) is asserted
409
- * by the conformance kit. The function is pure and total so both the daemon
419
+ * The full four-way matrix (two audiences × two offer scopes) is asserted by
420
+ * the conformance kit. The function is pure and total so both the daemon
410
421
  * and the server can run the identical rule — the daemon refuses, and the
411
422
  * server refuses too (byollm_003 §Server-side MUSTs).
412
423
  *
@@ -418,7 +429,7 @@ interface MatchDaemon {
418
429
  * owner: "bob",
419
430
  * offerScope: "team",
420
431
  * cost: "free",
421
- * locallyAllows: (o) => o === "alice",
432
+ * admits: (o) => o === "alice",
422
433
  * },
423
434
  * );
424
435
  * // result.ok === true
@@ -443,9 +454,9 @@ declare const REFUSAL_MESSAGES: Readonly<Record<MatchRefusal, string>>;
443
454
  * per-field limits were real and the aggregate one was a number in a frozen
444
455
  * object.
445
456
  *
446
- * byollm_004 §4 requires stricter limits for community (`named`/`public`)
447
- * jobs; those are applied on top of these by the daemon's budget check, which
448
- * knows the job's audience. These are the absolute ceilings for any job.
457
+ * byollm_004 §4 requires stricter limits for community (`team`) jobs; those
458
+ * are applied on top of these by the daemon's budget check, which knows the
459
+ * job's audience. These are the absolute ceilings for any job.
449
460
  */
450
461
  declare const PAYLOAD_LIMITS: Readonly<{
451
462
  /** Max characters in any single text field. */
@@ -562,11 +573,11 @@ declare function payloadTextLength(kinded: KindedPayload): number;
562
573
  declare const JobState: z.ZodEnum<{
563
574
  ok: "ok";
564
575
  error: "error";
576
+ expired: "expired";
565
577
  queued: "queued";
566
578
  claimed: "claimed";
567
579
  running: "running";
568
580
  canceled: "canceled";
569
- expired: "expired";
570
581
  }>;
571
582
  type JobState = z.infer<typeof JobState>;
572
583
  /** States from which a job never moves again. */
@@ -629,7 +640,6 @@ declare const ClaimedJob: z.ZodObject<{
629
640
  audience: z.ZodEnum<{
630
641
  private: "private";
631
642
  team: "team";
632
- public: "public";
633
643
  }>;
634
644
  owner: z.ZodString;
635
645
  site: z.ZodOptional<z.ZodString>;
@@ -644,7 +654,7 @@ type ClaimedJob = z.infer<typeof ClaimedJob>;
644
654
  /**
645
655
  * The provenance that travels with every result to the delivery seam.
646
656
  *
647
- * byollm_003 Rev 1: a `named`/`public` result is attacker-controlled text.
657
+ * byollm_003 Rev 1: a `team` result is attacker-controlled text.
648
658
  * The app must never render volunteer output as its own AI's answer without
649
659
  * knowing that is what it is ({@link MUSTS.PROVENANCE_NAMES_DEVICE}).
650
660
  */
@@ -652,7 +662,6 @@ declare const ResultProvenance: z.ZodObject<{
652
662
  audience: z.ZodEnum<{
653
663
  private: "private";
654
664
  team: "team";
655
- public: "public";
656
665
  }>;
657
666
  runnerId: z.ZodString;
658
667
  runnerOwner: z.ZodString;
@@ -744,7 +753,6 @@ type JobOutcome = z.infer<typeof JobOutcome>;
744
753
  * list rather than three private vocabularies.
745
754
  */
746
755
  declare const RefusalReason: z.ZodEnum<{
747
- "select-unavailable": "select-unavailable";
748
756
  "default-ambiguity": "default-ambiguity";
749
757
  "default-unusable": "default-unusable";
750
758
  }>;
@@ -774,7 +782,6 @@ type RefusalReason = z.infer<typeof RefusalReason>;
774
782
  declare const JobRefused: z.ZodObject<{
775
783
  outcome: z.ZodLiteral<"refused">;
776
784
  reason: z.ZodEnum<{
777
- "select-unavailable": "select-unavailable";
778
785
  "default-ambiguity": "default-ambiguity";
779
786
  "default-unusable": "default-unusable";
780
787
  }>;
@@ -818,11 +825,11 @@ declare const DeliveredResult: z.ZodObject<{
818
825
  state: z.ZodEnum<{
819
826
  ok: "ok";
820
827
  error: "error";
828
+ expired: "expired";
821
829
  queued: "queued";
822
830
  claimed: "claimed";
823
831
  running: "running";
824
832
  canceled: "canceled";
825
- expired: "expired";
826
833
  }>;
827
834
  outcome: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
828
835
  outcome: z.ZodLiteral<"ok">;
@@ -840,7 +847,6 @@ declare const DeliveredResult: z.ZodObject<{
840
847
  audience: z.ZodEnum<{
841
848
  private: "private";
842
849
  team: "team";
843
- public: "public";
844
850
  }>;
845
851
  runnerId: z.ZodString;
846
852
  runnerOwner: z.ZodString;
@@ -919,9 +925,8 @@ declare const JobStub: z.ZodObject<{
919
925
  audience: z.ZodEnum<{
920
926
  private: "private";
921
927
  team: "team";
922
- public: "public";
923
928
  }>;
924
- service: z.ZodOptional<z.ZodString>;
929
+ purpose: z.ZodOptional<z.ZodString>;
925
930
  sizeClass: z.ZodEnum<{
926
931
  small: "small";
927
932
  medium: "medium";
@@ -932,7 +937,23 @@ declare const JobStub: z.ZodObject<{
932
937
  deadlineAt: z.ZodNumber;
933
938
  }, z.core.$strict>;
934
939
  type JobStub = z.infer<typeof JobStub>;
935
- /** A stub, plus the lease the claiming runner now holds for it. */
940
+ /**
941
+ * A stub, plus the lease the claiming runner now holds for it — and, on a
942
+ * relayed route, the grant that says it may run at all.
943
+ *
944
+ * The grant lives here rather than on {@link JobStub} because of *when* it is
945
+ * authored. A stub exists from enqueue; a grant is written at claim, against
946
+ * the membership and mapping true at that moment. That timing is the whole of
947
+ * Amendment J: a job queued yesterday for somebody removed this morning gets
948
+ * no grant when it is finally claimed, and a roster held on the device could
949
+ * never have known.
950
+ *
951
+ * Optional, and the absence is meaningful rather than lenient. A device that
952
+ * pinned a control-plane key at pairing **requires** one — a claimed job
953
+ * arriving without it is refused, not admitted by default. A device that
954
+ * pinned none is in direct mode, where there is no control plane to author
955
+ * anything and the owner's own work is the only work that runs.
956
+ */
936
957
  declare const ClaimedStub: z.ZodObject<{
937
958
  id: z.ZodString;
938
959
  kind: z.ZodEnum<{
@@ -944,9 +965,8 @@ declare const ClaimedStub: z.ZodObject<{
944
965
  audience: z.ZodEnum<{
945
966
  private: "private";
946
967
  team: "team";
947
- public: "public";
948
968
  }>;
949
- service: z.ZodOptional<z.ZodString>;
969
+ purpose: z.ZodOptional<z.ZodString>;
950
970
  sizeClass: z.ZodEnum<{
951
971
  small: "small";
952
972
  medium: "medium";
@@ -960,6 +980,18 @@ declare const ClaimedStub: z.ZodObject<{
960
980
  runnerId: z.ZodString;
961
981
  expiresAt: z.ZodNumber;
962
982
  }, z.core.$strip>;
983
+ grant: z.ZodOptional<z.ZodObject<{
984
+ grantId: z.ZodString;
985
+ jobId: z.ZodString;
986
+ siteId: z.ZodString;
987
+ user: z.ZodString;
988
+ owner: z.ZodString;
989
+ purpose: z.ZodString;
990
+ kind: z.ZodString;
991
+ service: z.ZodString;
992
+ issuedAt: z.ZodNumber;
993
+ signature: z.ZodString;
994
+ }, z.core.$strict>>;
963
995
  }, z.core.$strict>;
964
996
  type ClaimedStub = z.infer<typeof ClaimedStub>;
965
997
 
@@ -1297,105 +1329,273 @@ declare function verifyRequest(input: {
1297
1329
  }): SignatureFailure | null;
1298
1330
 
1299
1331
  /**
1300
- * The roster a daemon holds, and how it knows the roster is real.
1332
+ * What a site says it needs byollm_016 Amendment L.
1333
+ *
1334
+ * A site declares **purposes**, and each purpose lists the job kinds it uses.
1335
+ * A person then maps each purpose to one of their own services, on the consent
1336
+ * screen, and that mapping *is* the consent. The control plane joins the two
1337
+ * at claim time and signs the result into a grant.
1338
+ *
1339
+ * ## Why a site declares needs instead of naming services
1340
+ *
1341
+ * Because it cannot name one. The site's vocabulary is its own purposes; the
1342
+ * person's vocabulary is their services; and the two never meet. A site asks
1343
+ * for "writing assistant, llm.chat" and learns only whether that slot is
1344
+ * satisfiable — never which model answered, never whose machine, never even
1345
+ * the name of the service. Key-vs-value reaches its strongest form here: the
1346
+ * site cannot describe what it wants *or* name it, only ask for what it
1347
+ * declared.
1348
+ *
1349
+ * ## Keys are ids; labels are prose
1350
+ *
1351
+ * They are separate fields and nothing derives one from the other, which is
1352
+ * the amendment's ruling and worth restating where somebody will read it. A
1353
+ * key travels on every job and is what mappings are stored against, so it is
1354
+ * stable-or-nothing: renaming one deletes a purpose and creates another,
1355
+ * unmapping everybody who had chosen for it. A label is changeable whenever
1356
+ * the site likes and is the **only** thing a consent screen renders.
1357
+ */
1358
+ /**
1359
+ * The purpose a site gets when it declares no purposes of its own.
1360
+ *
1361
+ * Reserved, and refused by {@link Manifest} rather than by whatever handles
1362
+ * registration. A site with a single undifferentiated use has one purpose —
1363
+ * everything it does — and that purpose needs an id because mappings are
1364
+ * keyed by one. An id taken from the site's own vocabulary would collide the
1365
+ * day it declared a real purpose of the same name.
1366
+ *
1367
+ * **Never rendered.** "default → your Claude" tells a person nothing; a
1368
+ * consent screen shows the site's own name for this slot, because that is
1369
+ * what a single-purpose site's one purpose actually is.
1370
+ */
1371
+ declare const RESERVED_PURPOSE = "default";
1372
+ declare const Purpose: z.ZodObject<{
1373
+ label: z.ZodString;
1374
+ description: z.ZodOptional<z.ZodString>;
1375
+ kinds: z.ZodArray<z.ZodEnum<{
1376
+ "llm.generate": "llm.generate";
1377
+ "llm.chat": "llm.chat";
1378
+ }>>;
1379
+ }, z.core.$strict>;
1380
+ type Purpose = z.infer<typeof Purpose>;
1381
+ /**
1382
+ * Everything a site needs, by purpose key.
1383
+ *
1384
+ * At least one purpose: a site that declares none is a site that can enqueue
1385
+ * nothing, and accepting it would mean the first refusal a person saw came
1386
+ * from a job rather than from registration.
1387
+ */
1388
+ declare const Manifest: z.ZodRecord<z.ZodString, z.ZodObject<{
1389
+ label: z.ZodString;
1390
+ description: z.ZodOptional<z.ZodString>;
1391
+ kinds: z.ZodArray<z.ZodEnum<{
1392
+ "llm.generate": "llm.generate";
1393
+ "llm.chat": "llm.chat";
1394
+ }>>;
1395
+ }, z.core.$strict>>;
1396
+ type Manifest = z.infer<typeof Manifest>;
1397
+ /**
1398
+ * The manifest a site with no declared purposes is treated as having.
1399
+ *
1400
+ * The sugar in Amendment L, made explicit rather than special-cased
1401
+ * downstream: everything after this point sees a manifest with one purpose,
1402
+ * so no consent screen, mapping table or resolver needs a branch for the
1403
+ * flat-list case.
1404
+ *
1405
+ * The label is the caller's — a site's own name — because it is the one thing
1406
+ * that can make "everything this site does" read as a sentence about a
1407
+ * particular site rather than about software in general.
1408
+ */
1409
+ declare function singlePurposeManifest(input: {
1410
+ readonly label: string;
1411
+ readonly kinds: readonly JobKind[];
1412
+ }): Manifest;
1413
+
1414
+ /**
1415
+ * One job, one signature, one answer — byollm_016 Amendment J.
1416
+ *
1417
+ * A grant is the control plane's signed statement that a particular job may
1418
+ * run on a particular device, authored at claim time and verified against the
1419
+ * key that device pinned when it paired.
1420
+ *
1421
+ * ## What it replaced, and why the replacement is smaller
1422
+ *
1423
+ * Until 2026-08-26 a device held a signed **roster** and answered admission
1424
+ * from it. Amendment G's four properties were right and the mechanism was a
1425
+ * cache — one that bought nothing. On the cloud route the job path and the
1426
+ * roster path share fate: jobs arrive through the relay, so if the relay is
1427
+ * unreachable there are no jobs to admit and a locally held roster adds no
1428
+ * availability. What it did add was staleness, which is the only reason
1429
+ * `ROSTER_MAX_AGE_MS` existed: a bound on how long a removed person keeps
1430
+ * running. Authoring at claim collapses that bound to this document's own
1431
+ * lifetime — add somebody and their next job runs, remove them and their next
1432
+ * claim fails, including jobs already queued.
1433
+ *
1434
+ * It also collapses four questions into one signature. Consented, member,
1435
+ * admitted, and *which service* were four mechanisms answering separately;
1436
+ * they are now four fields of one statement, and the device verifies once.
1437
+ *
1438
+ * ## What it is not
1439
+ *
1440
+ * Amendment G property 1 outlawed admitting on a per-job assertion, and this
1441
+ * is per-job. The distinction is authorship: G outlawed trusting the
1442
+ * **relay's or site's unsigned** claim. A grant is signed by the control
1443
+ * plane with a key the device pinned at pairing, so the relay can withhold it
1444
+ * and cannot forge it — exactly the power a relay has over a job.
1445
+ * `RELAY_BLIND` is untouched: the relay delivers, it never authors.
1446
+ *
1447
+ * ## What the device still checks for itself
1448
+ *
1449
+ * A grant is necessary and not sufficient. Four checks stay on the device and
1450
+ * none of them is delegated:
1451
+ *
1452
+ * 1. the signature, against the pinned key;
1453
+ * 2. replay — {@link SignedGrant.grantId} is single-use;
1454
+ * 3. offer-consistency — the named service is one this device actually
1455
+ * offers, at a scope that includes this user;
1456
+ * 4. **private is absolute** — a `private` service runs for the paired owner
1457
+ * and nobody else, so no compromise of a control plane can grant somebody
1458
+ * else's job onto it.
1459
+ */
1460
+ /**
1461
+ * How long a grant is honoured after it was signed. Ruled 120s (2026-08-26).
1462
+ *
1463
+ * This bounds **acceptance**, not execution: a job admitted inside the window
1464
+ * runs to completion however long it takes. So the number only has to cover
1465
+ * the trip from the control plane signing to the device checking — claim,
1466
+ * deliver, verify — and every second past that is a second a captured grant
1467
+ * stays useful.
1301
1468
  *
1302
- * byollm_001 Amendment G, RATIFIED 2026-08-25. A `team` job is admitted by a
1303
- * list **this device holds**, signed by the owner's control plane and verified
1304
- * against a key pinned at pairing never by an assertion from the party
1305
- * routing the job, per-job or in bulk.
1469
+ * Two minutes is generous for that trip and mean for the capture. It is also
1470
+ * the number ordinary clock drift is measured against, which is why
1471
+ * {@link CLOCK_SKEW_WARN_MS} sits well inside it: a device whose clock is off
1472
+ * by half the window would refuse real work, and must be told before it does.
1306
1473
  *
1307
- * The relay's power over this is exactly denial: it can withhold a roster as
1308
- * it can withhold a job, and it can forge neither. That is what lets the hub
1309
- * go on reading and filtering rosters for its own routing without being
1310
- * trusted for admission non-authorship, not blindness.
1474
+ * The verifier's policy, deliberately not a field on the document. An
1475
+ * `expiresAt` the signer chose would let whoever signs decide how long their
1476
+ * own statement stays good, and the party with the most reason to want a
1477
+ * longer window is the party being bounded.
1311
1478
  */
1479
+ declare const GRANT_MAX_AGE_MS = 120000;
1312
1480
  /**
1313
- * How old a held roster may be before a device stops honouring it.
1481
+ * Clock disagreement past which a device says so, before it starts refusing.
1314
1482
  *
1315
- * One hour, and the shortest constant in this protocol on purpose: every other
1316
- * one bounds how long a *thing* stays valid, and this one alone bounds how
1317
- * long a *person* keeps access after the owner has said no.
1483
+ * Skew eats {@link GRANT_MAX_AGE_MS} directly a device 60s behind its
1484
+ * relay's clock has half a window left, and one 120s behind has none and
1485
+ * refuses everything for a reason no refusal message would otherwise name.
1486
+ * Thirty seconds is a quarter of the window: far enough out to be a real
1487
+ * problem, early enough to be a warning rather than an outage.
1488
+ */
1489
+ declare const CLOCK_SKEW_WARN_MS = 30000;
1490
+ /**
1491
+ * Skew past which a freshness refusal names the clock instead of the grant.
1318
1492
  *
1319
- * A failure bound, not a sync interval. A daemon refreshes on every heartbeat
1320
- * and a removal propagates in seconds; the hour is what a device gets when
1321
- * that conversation stops working enough that a closed laptop or a flaky
1322
- * café network does not narrow a working device, and not so much that a
1323
- * removed teammate outlives the owner's patience.
1493
+ * Five seconds, because below that the clock is not the story and saying so
1494
+ * would send somebody to check ntp about an unrelated failure. Above it, "this
1495
+ * grant expired" and "your clock is wrong" are the same event wearing
1496
+ * different words, and only one of them can be acted on.
1324
1497
  */
1325
- declare const ROSTER_MAX_AGE_MS: number;
1498
+ declare const CLOCK_ATTRIBUTION_MS = 5000;
1326
1499
  /**
1327
- * The domain separator, and why a roster gets its own.
1500
+ * The domain separator.
1328
1501
  *
1329
1502
  * Every signature in this system says what kind of statement it is before it
1330
- * says anything else. Without that, bytes signed for one purpose verify for
1331
- * another: a roster document and a request body are both "some bytes this key
1332
- * signed", and a scheme that could not tell them apart would let a captured
1333
- * roster be replayed as a request — or worse, let a control plane that signs
1334
- * one thing be held to have signed the other.
1335
- */
1336
- declare const ROSTER_CONTEXT = "byollm/v1/roster";
1337
- declare const SignedRoster: z.ZodObject<{
1503
+ * says anything else. Without it, bytes signed for one purpose verify for
1504
+ * another a grant and a request are both "bytes this key signed", and a
1505
+ * scheme that could not tell them apart would let one be replayed as the
1506
+ * other.
1507
+ */
1508
+ declare const GRANT_CONTEXT = "byollm/v1/grant";
1509
+ declare const SignedGrant: z.ZodObject<{
1510
+ grantId: z.ZodString;
1511
+ jobId: z.ZodString;
1512
+ siteId: z.ZodString;
1513
+ user: z.ZodString;
1338
1514
  owner: z.ZodString;
1339
- members: z.ZodArray<z.ZodString>;
1515
+ purpose: z.ZodString;
1516
+ kind: z.ZodString;
1517
+ service: z.ZodString;
1340
1518
  issuedAt: z.ZodNumber;
1341
1519
  signature: z.ZodString;
1342
1520
  }, z.core.$strict>;
1343
- type SignedRoster = z.infer<typeof SignedRoster>;
1521
+ type SignedGrant = z.infer<typeof SignedGrant>;
1522
+ /** Everything a grant says, before it is signed. */
1523
+ type GrantClaims = Omit<SignedGrant, "signature">;
1524
+ /**
1525
+ * Every field of {@link SignedGrant} except the signature, sorted.
1526
+ *
1527
+ * **Derived from the schema, never written out by hand.** The unsigned-field
1528
+ * attack is that somebody adds a field to the document, forgets to add it to
1529
+ * the bytes, and ships a value an intermediary can rewrite without breaking
1530
+ * any signature. A hand-maintained list is exactly the shape that fails: it
1531
+ * does not grow when the code does, and nothing about adding a field reminds
1532
+ * you it exists.
1533
+ *
1534
+ * Reading the shape closes it structurally rather than by review. A new field
1535
+ * is signed the moment it is declared, and grant.test.ts asserts this list
1536
+ * still covers the schema so a future zod version that hides `shape` fails
1537
+ * loudly instead of silently signing less.
1538
+ */
1539
+ declare const GRANT_SIGNED_FIELDS: readonly (keyof GrantClaims)[];
1344
1540
  /**
1345
1541
  * The exact bytes both sides sign and verify.
1346
1542
  *
1347
- * Every field that decides what the roster *means* is in here. Leave one out
1348
- * and it becomes something an intermediary can change without breaking the
1349
- * signature `owner` most of all: a roster whose owner was not signed over
1350
- * could be lifted from one account and delivered to another's devices, and
1351
- * every signature check would pass.
1543
+ * JSON-encoded rather than joined with a separator, because a separator can
1544
+ * be imitated. Newline-joining `["a", "b\nc"]` and `["a\nb", "c"]` produces
1545
+ * identical bytes, so two different grants would share a signature and the
1546
+ * values here include a site id and a user id, at least one of which comes
1547
+ * from somebody else's namespace. JSON escapes the separator it uses, so no
1548
+ * arrangement of field values can spell a different document.
1352
1549
  *
1353
- * Members are joined with NUL, which cannot appear in an id, so no arrangement
1354
- * of member names can imitate a different membership. Joining on a comma would
1355
- * let `["a,b"]` and `["a","b"]` sign identically.
1550
+ * The context string leads, and the field order is the schema's own sorted
1551
+ * keys, so the encoding is canonical without anyone maintaining a list.
1356
1552
  */
1357
- declare function rosterStatement(input: {
1358
- owner: string;
1359
- members: readonly string[];
1360
- issuedAt: number;
1361
- }): Uint8Array;
1362
- /** Sign a roster with the control plane's own key. */
1363
- declare function signRoster(keys: Pick<StoredKeys, "identityPrivate">, input: {
1364
- owner: string;
1365
- members: readonly string[];
1366
- issuedAt: number;
1367
- }): SignedRoster;
1368
- /** Why a roster was refused. Typed for logs; never returned to a caller. */
1369
- type RosterRefusal = "bad-signature" | "wrong-owner" | "stale" | "from-the-future"
1553
+ declare function grantStatement(claims: GrantClaims): Uint8Array;
1554
+ /** Sign a grant with the control plane's own key. */
1555
+ declare function signGrant(keys: Pick<StoredKeys, "identityPrivate">, claims: GrantClaims): SignedGrant;
1370
1556
  /**
1371
- * A roster arrived and this device pinned no key to check it against.
1557
+ * Why a grant was refused.
1372
1558
  *
1373
- * Not a bad signature nothing was checked. It is the one refusal that is
1374
- * evidence *about the pairing* rather than about the document: an upstream
1375
- * that sends rosters has a control plane, so a device holding no key from
1376
- * it paired before roster sync existed and can never verify one. The remedy
1377
- * is re-pairing, and this is the only refusal here that has one.
1559
+ * Split by remedy, because these send somebody to different places: fix your
1560
+ * clock, take it up with the relay, or nothing at all you are being
1561
+ * attacked and the refusal worked.
1562
+ *
1563
+ * There is deliberately no `no-pinned-key` here. A device that pinned no
1564
+ * control-plane key never reaches this function: it is in direct mode, and
1565
+ * the question "is this grant good" does not arise. A value nothing can
1566
+ * return is a branch every caller has to handle and no test can reach.
1378
1567
  */
1379
- | "no-pinned-key";
1568
+ type GrantRefusal =
1569
+ /** The signature does not verify against the pinned key. */
1570
+ "bad-signature"
1571
+ /** Genuine, and for a different device's owner. */
1572
+ | "wrong-owner"
1573
+ /** Genuine, and lifted from a different job. */
1574
+ | "wrong-job"
1575
+ /** Older than {@link GRANT_MAX_AGE_MS}. */
1576
+ | "expired"
1380
1577
  /**
1381
- * Is this roster one this device may admit people from, right now?
1578
+ * Issued in the future.
1382
1579
  *
1383
- * `owner` is passed in rather than read out of the document, for the reason
1384
- * {@link verifyLink} takes its successor as an argument: a verifier that
1385
- * recovered the owner from the signed bytes would accept a genuine roster
1386
- * belonging to somebody else, and every check would pass.
1580
+ * Checked, and not as pedantry: an `issuedAt` ahead of now extends a
1581
+ * grant's life past the bound, which is the whole thing being enforced.
1582
+ */
1583
+ | "from-the-future";
1584
+ /**
1585
+ * Is this grant one this device may act on, right now?
1387
1586
  *
1388
- * Age is checked in **both** directions. A clock far ahead is as much a
1389
- * problem as one behind: an `issuedAt` in the future would extend a roster's
1390
- * life past the bound, which is the whole thing being enforced.
1587
+ * Document-level checks only. Replay, offer-consistency and the private rule
1588
+ * need state this function does not have and are the device's to apply see
1589
+ * the class comment for the full list of four.
1391
1590
  */
1392
- declare function verifyRoster(input: {
1393
- roster: SignedRoster;
1591
+ declare function verifyGrant(input: {
1592
+ grant: SignedGrant;
1394
1593
  owner: string;
1594
+ jobId: string;
1395
1595
  controlPlanePublic: string;
1396
1596
  now: number;
1397
1597
  maxAgeMs?: number;
1398
- }): RosterRefusal | null;
1598
+ }): GrantRefusal | null;
1399
1599
 
1400
1600
  /**
1401
1601
  * Rotation — byollm_009 Amendment C.
@@ -1762,7 +1962,6 @@ declare const Capability: z.ZodObject<{
1762
1962
  "llm.chat": "llm.chat";
1763
1963
  }>;
1764
1964
  service: z.ZodString;
1765
- isDefault: z.ZodBoolean;
1766
1965
  backendId: z.ZodEnum<{
1767
1966
  ollama: "ollama";
1768
1967
  mlx: "mlx";
@@ -1792,7 +1991,6 @@ declare const Capability: z.ZodObject<{
1792
1991
  offerScope: z.ZodEnum<{
1793
1992
  private: "private";
1794
1993
  team: "team";
1795
- public: "public";
1796
1994
  }>;
1797
1995
  }, z.core.$strict>;
1798
1996
  type Capability = z.infer<typeof Capability>;
@@ -1803,7 +2001,6 @@ declare const CapabilityMatrix: z.ZodArray<z.ZodObject<{
1803
2001
  "llm.chat": "llm.chat";
1804
2002
  }>;
1805
2003
  service: z.ZodString;
1806
- isDefault: z.ZodBoolean;
1807
2004
  backendId: z.ZodEnum<{
1808
2005
  ollama: "ollama";
1809
2006
  mlx: "mlx";
@@ -1833,7 +2030,6 @@ declare const CapabilityMatrix: z.ZodArray<z.ZodObject<{
1833
2030
  offerScope: z.ZodEnum<{
1834
2031
  private: "private";
1835
2032
  team: "team";
1836
- public: "public";
1837
2033
  }>;
1838
2034
  }, z.core.$strict>>;
1839
2035
  type CapabilityMatrix = z.infer<typeof CapabilityMatrix>;
@@ -1861,7 +2057,6 @@ declare const WithheldKind: z.ZodObject<{
1861
2057
  offer: z.ZodEnum<{
1862
2058
  private: "private";
1863
2059
  team: "team";
1864
- public: "public";
1865
2060
  }>;
1866
2061
  }, z.core.$strict>>;
1867
2062
  }, z.core.$strict>;
@@ -1896,7 +2091,6 @@ declare const PairStartRequest: z.ZodObject<{
1896
2091
  "llm.chat": "llm.chat";
1897
2092
  }>;
1898
2093
  service: z.ZodString;
1899
- isDefault: z.ZodBoolean;
1900
2094
  backendId: z.ZodEnum<{
1901
2095
  ollama: "ollama";
1902
2096
  mlx: "mlx";
@@ -1926,7 +2120,6 @@ declare const PairStartRequest: z.ZodObject<{
1926
2120
  offerScope: z.ZodEnum<{
1927
2121
  private: "private";
1928
2122
  team: "team";
1929
- public: "public";
1930
2123
  }>;
1931
2124
  }, z.core.$strict>>;
1932
2125
  }, z.core.$strict>;
@@ -1987,7 +2180,6 @@ declare const PairRequest: z.ZodDiscriminatedUnion<[z.ZodObject<{
1987
2180
  "llm.chat": "llm.chat";
1988
2181
  }>;
1989
2182
  service: z.ZodString;
1990
- isDefault: z.ZodBoolean;
1991
2183
  backendId: z.ZodEnum<{
1992
2184
  ollama: "ollama";
1993
2185
  mlx: "mlx";
@@ -2017,7 +2209,6 @@ declare const PairRequest: z.ZodDiscriminatedUnion<[z.ZodObject<{
2017
2209
  offerScope: z.ZodEnum<{
2018
2210
  private: "private";
2019
2211
  team: "team";
2020
- public: "public";
2021
2212
  }>;
2022
2213
  }, z.core.$strict>>;
2023
2214
  }, z.core.$strict>, z.ZodObject<{
@@ -2035,7 +2226,6 @@ declare const ClaimRequest: z.ZodObject<{
2035
2226
  "llm.chat": "llm.chat";
2036
2227
  }>;
2037
2228
  service: z.ZodString;
2038
- isDefault: z.ZodBoolean;
2039
2229
  backendId: z.ZodEnum<{
2040
2230
  ollama: "ollama";
2041
2231
  mlx: "mlx";
@@ -2065,7 +2255,6 @@ declare const ClaimRequest: z.ZodObject<{
2065
2255
  offerScope: z.ZodEnum<{
2066
2256
  private: "private";
2067
2257
  team: "team";
2068
- public: "public";
2069
2258
  }>;
2070
2259
  }, z.core.$strict>>;
2071
2260
  max: z.ZodNumber;
@@ -2083,9 +2272,8 @@ declare const ClaimResponse: z.ZodObject<{
2083
2272
  audience: z.ZodEnum<{
2084
2273
  private: "private";
2085
2274
  team: "team";
2086
- public: "public";
2087
2275
  }>;
2088
- service: z.ZodOptional<z.ZodString>;
2276
+ purpose: z.ZodOptional<z.ZodString>;
2089
2277
  sizeClass: z.ZodEnum<{
2090
2278
  small: "small";
2091
2279
  medium: "medium";
@@ -2099,6 +2287,18 @@ declare const ClaimResponse: z.ZodObject<{
2099
2287
  runnerId: z.ZodString;
2100
2288
  expiresAt: z.ZodNumber;
2101
2289
  }, z.core.$strip>;
2290
+ grant: z.ZodOptional<z.ZodObject<{
2291
+ grantId: z.ZodString;
2292
+ jobId: z.ZodString;
2293
+ siteId: z.ZodString;
2294
+ user: z.ZodString;
2295
+ owner: z.ZodString;
2296
+ purpose: z.ZodString;
2297
+ kind: z.ZodString;
2298
+ service: z.ZodString;
2299
+ issuedAt: z.ZodNumber;
2300
+ signature: z.ZodString;
2301
+ }, z.core.$strict>>;
2102
2302
  }, z.core.$strict>>;
2103
2303
  leaseMs: z.ZodNumber;
2104
2304
  }, z.core.$strict>;
@@ -2113,7 +2313,6 @@ declare const HeartbeatRequest: z.ZodObject<{
2113
2313
  "llm.chat": "llm.chat";
2114
2314
  }>;
2115
2315
  service: z.ZodString;
2116
- isDefault: z.ZodBoolean;
2117
2316
  backendId: z.ZodEnum<{
2118
2317
  ollama: "ollama";
2119
2318
  mlx: "mlx";
@@ -2143,7 +2342,6 @@ declare const HeartbeatRequest: z.ZodObject<{
2143
2342
  offerScope: z.ZodEnum<{
2144
2343
  private: "private";
2145
2344
  team: "team";
2146
- public: "public";
2147
2345
  }>;
2148
2346
  }, z.core.$strict>>;
2149
2347
  withheld: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -2156,7 +2354,6 @@ declare const HeartbeatRequest: z.ZodObject<{
2156
2354
  offer: z.ZodEnum<{
2157
2355
  private: "private";
2158
2356
  team: "team";
2159
- public: "public";
2160
2357
  }>;
2161
2358
  }, z.core.$strict>>;
2162
2359
  }, z.core.$strict>>>;
@@ -2194,12 +2391,6 @@ declare const HeartbeatResponse: z.ZodObject<{
2194
2391
  }, z.core.$strict>>;
2195
2392
  serverTime: z.ZodNumber;
2196
2393
  awaitingConsent: z.ZodArray<z.ZodString>;
2197
- roster: z.ZodOptional<z.ZodObject<{
2198
- owner: z.ZodString;
2199
- members: z.ZodArray<z.ZodString>;
2200
- issuedAt: z.ZodNumber;
2201
- signature: z.ZodString;
2202
- }, z.core.$strict>>;
2203
2394
  }, z.core.$strict>;
2204
2395
  type HeartbeatResponse = z.infer<typeof HeartbeatResponse>;
2205
2396
  /**
@@ -2335,4 +2526,4 @@ declare const FetchResponse: z.ZodObject<{
2335
2526
  }, z.core.$strict>;
2336
2527
  type FetchResponse = z.infer<typeof FetchResponse>;
2337
2528
 
2338
- export { AUDIENCES, Audience, BACKENDS, BACKEND_IDS, BackendClass, BackendCost, type BackendDescriptor, type BackendId, BackendIdSchema, Capability, CapabilityMatrix, ChatMessage, ChatPayload, ClaimRequest, ClaimResponse, ClaimedJob, ClaimedStub, DeliveredResult, ENCRYPTION_KEY_CONTEXT, ENDPOINTS, ENVELOPE_MAX_AGE_MS, ERROR_STATUS, type Endpoint, type EnvelopeContext, EnvelopeDirection, type EnvelopeFailure, FetchRequest, FetchResponse, GeneratePayload, HeartbeatRequest, HeartbeatResponse, JOB_KINDS, JobKind, JobOutcome, JobPayload, JobRefused, JobResultCanceled, JobResultError, JobResultOk, JobState, JobStub, KindedPayload, Lease, MAX_CLOCK_SKEW_MS, MAX_SUCCESSION_CHAIN, MIN_PROTOCOL_VERSION, MUSTS, MUST_IDS, type MatchDaemon, type MatchJob, MatchRefusal, type MatchResult, type Must, type MustEnforcer, type MustId, type MustVerification, type MustVerifiedBy, OFFER_SCOPES, OfferScope, type OpenResult, PAYLOAD_LIMITS, PROTOCOL_PREFIX, PROTOCOL_VERSION, PairPollRequest, PairPollResponse, PairRequest, PairStartRequest, PairStartResponse, type PayloadFor, PublicIdentity, REFUSAL_MESSAGES, RETIREMENT_WINDOW_MS, ROSTER_CONTEXT, ROSTER_MAX_AGE_MS, RefusalReason, ReleaseRequest, ReleaseResponse, RequestSignature, ResultDisposition, ResultProvenance, ResultRequest, ResultResponse, type RosterRefusal, RunMetadata, SIZE_CLASS_LIMITS, SUCCESSION_CONTEXT, SUPPORTED_PROTOCOL_VERSIONS, SealedEnvelope, SealedOutcome, type SignatureFailure, SignedRoster, SizeClass, type SpendConsent, StoredKeys, Succession, type SuccessionFailure, type SuccessionWalk, TERMINAL_STATES, type VersionRefusal, WireError, WireErrorCode, WithheldKind, backendDescriptor, backendName, canTransition, canonicalRequest, checkProtocolVersion, classifyCost, cryptoReady, declaredVersion, effectiveOfferScope, fingerprint, generateKeys, isBackendId, isCloudTaggedModel, isJobKind, isLocalHost, isTerminal, keyId, kindsOf, matchAudience, mustsVerifiedBy, open, payloadTextLength, provenanceFor, publicIdentityOf, resolveCost, rosterStatement, seal, signRequest, signRoster, signSiteRequest, signSuccession, signWith, sizeClassCeiling, sizeClassOf, successionStatement, verifyLink, verifyPublicIdentity, verifyRequest, verifyRoster, verifySiteRequest, verifyWith, walkSuccession };
2529
+ export { AUDIENCES, Audience, BACKENDS, BACKEND_IDS, BackendClass, BackendCost, type BackendDescriptor, type BackendId, BackendIdSchema, CLOCK_ATTRIBUTION_MS, CLOCK_SKEW_WARN_MS, Capability, CapabilityMatrix, ChatMessage, ChatPayload, ClaimRequest, ClaimResponse, ClaimedJob, ClaimedStub, DeliveredResult, ENCRYPTION_KEY_CONTEXT, ENDPOINTS, ENVELOPE_MAX_AGE_MS, ERROR_STATUS, type Endpoint, type EnvelopeContext, EnvelopeDirection, type EnvelopeFailure, FetchRequest, FetchResponse, GRANT_CONTEXT, GRANT_MAX_AGE_MS, GRANT_SIGNED_FIELDS, GeneratePayload, type GrantClaims, type GrantRefusal, HeartbeatRequest, HeartbeatResponse, JOB_KINDS, JobKind, JobOutcome, JobPayload, JobRefused, JobResultCanceled, JobResultError, JobResultOk, JobState, JobStub, KindedPayload, Lease, MAX_CLOCK_SKEW_MS, MAX_SUCCESSION_CHAIN, MIN_PROTOCOL_VERSION, MUSTS, MUST_IDS, Manifest, type MatchDaemon, type MatchJob, MatchRefusal, type MatchResult, type Must, type MustEnforcer, type MustId, type MustVerification, type MustVerifiedBy, OFFER_SCOPES, OfferScope, type OpenResult, PAYLOAD_LIMITS, PROTOCOL_PREFIX, PROTOCOL_VERSION, PairPollRequest, PairPollResponse, PairRequest, PairStartRequest, PairStartResponse, type PayloadFor, PublicIdentity, Purpose, REFUSAL_MESSAGES, RESERVED_PURPOSE, RETIREMENT_WINDOW_MS, RefusalReason, ReleaseRequest, ReleaseResponse, RequestSignature, ResultDisposition, ResultProvenance, ResultRequest, ResultResponse, RunMetadata, SIZE_CLASS_LIMITS, SUCCESSION_CONTEXT, SUPPORTED_PROTOCOL_VERSIONS, SealedEnvelope, SealedOutcome, type SignatureFailure, SignedGrant, SizeClass, type SpendConsent, StoredKeys, Succession, type SuccessionFailure, type SuccessionWalk, TERMINAL_STATES, type VersionRefusal, WireError, WireErrorCode, WithheldKind, backendDescriptor, backendName, canTransition, canonicalRequest, checkProtocolVersion, classifyCost, cryptoReady, declaredVersion, effectiveOfferScope, fingerprint, generateKeys, grantStatement, isBackendId, isCloudTaggedModel, isJobKind, isLocalHost, isTerminal, keyId, kindsOf, matchAudience, mustsVerifiedBy, open, payloadTextLength, provenanceFor, publicIdentityOf, resolveCost, seal, signGrant, signRequest, signSiteRequest, signSuccession, signWith, singlePurposeManifest, sizeClassCeiling, sizeClassOf, successionStatement, verifyGrant, verifyLink, verifyPublicIdentity, verifyRequest, verifySiteRequest, verifyWith, walkSuccession };