@byollm/protocol 0.1.0-alpha.14 → 0.1.0-alpha.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -2
- package/dist/index.d.ts +78 -14
- package/dist/index.js +245 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
> [!WARNING]
|
|
2
|
-
> **Alpha (`0.1.0-alpha.
|
|
2
|
+
> **Alpha (`0.1.0-alpha.16`) — under active development. Don't use this yet.**
|
|
3
3
|
>
|
|
4
4
|
> Install it deliberately: `npm install @byollm/protocol@alpha`.
|
|
5
5
|
>
|
|
@@ -11,7 +11,34 @@
|
|
|
11
11
|
> npm assigns `latest` on a first publish and won't let it be removed, so a
|
|
12
12
|
> bare install resolves here too. This notice is the only guard — deliberately
|
|
13
13
|
> not an npm deprecation, which would read as *abandoned* rather than *early*.
|
|
14
|
-
> Ask for `@alpha` explicitly so your lockfile records that you meant to
|
|
14
|
+
> Ask for `@alpha` explicitly so your lockfile records that you meant to.>
|
|
15
|
+
> **`alpha.15` is a breaking wire change, and it breaks daemons and relays —
|
|
16
|
+
> not app authors.** If you call `app.enqueue(...)` and read results, nothing
|
|
17
|
+
> in your code changes. If you run a daemon or an upstream, every package must
|
|
18
|
+
> move together: a mixed pair refuses on both sides, because both ends parse
|
|
19
|
+
> `.strict()`.
|
|
20
|
+
>
|
|
21
|
+
> What moved, all of it reconciling the frozen `byollm_009` with its code:
|
|
22
|
+
> `JobStub` gains `site` (the site's identity key id) and loses
|
|
23
|
+
> `audienceAllow`; `ResultRequest` gains `leaseId`; `HeartbeatResponse` loses
|
|
24
|
+
> `leases`, which nothing read; `WireErrorCode` gains `not-ready`,
|
|
25
|
+
> `clock-skew` and `forbidden`, and `403` is `forbidden` rather than
|
|
26
|
+
> `unauthorized`. `RESULT_PROVENANCE` is superseded by
|
|
27
|
+
> `PROVENANCE_NAMES_DEVICE`. See `byollm_009` Amendment A.>
|
|
28
|
+
> **`alpha.16` is a breaking wire change — daemons and relays again, not app
|
|
29
|
+
> authors.** `app.enqueue(...)` and reading results are unchanged. All five
|
|
30
|
+
> packages move together: both ends parse `.strict()`, so a mixed pair
|
|
31
|
+
> refuses.
|
|
32
|
+
>
|
|
33
|
+
> What moved, all of it Tier 2 of `cloud_008`: `model`, `backendClass` and
|
|
34
|
+
> `durationMs` come off `ResultRequest` and are sealed **inside** the result
|
|
35
|
+
> envelope as `SealedOutcome = { outcome, ran }` — so a daemon can no longer
|
|
36
|
+
> declare a model it did not sign, and a relay carries neither.
|
|
37
|
+
> `HeartbeatResponse` loses `leases` (nothing read it) and now reports real
|
|
38
|
+
> cancellations instead of an empty list. `WireErrorCode` gains `forbidden`
|
|
39
|
+
> for 403, leaving `unauthorized` at exactly 401. The relay gained a
|
|
40
|
+
> site-plane `cancel` endpoint, honours `stub.deadlineAt`, honours
|
|
41
|
+
> `stub.audience`, and remembers a refusal.
|
|
15
42
|
>
|
|
16
43
|
> **`alpha.3` is a breaking change.** `BackendDescriptor.account` is gone —
|
|
17
44
|
> read `cost` (`free` / `metered` / `subscription`) instead. Four new MUSTs
|
package/dist/index.d.ts
CHANGED
|
@@ -515,7 +515,7 @@ type ClaimedJob = z.infer<typeof ClaimedJob>;
|
|
|
515
515
|
*
|
|
516
516
|
* byollm_003 Rev 1: a `named`/`public` result is attacker-controlled text.
|
|
517
517
|
* The app must never render volunteer output as its own AI's answer without
|
|
518
|
-
* knowing that is what it is ({@link MUSTS.
|
|
518
|
+
* knowing that is what it is ({@link MUSTS.PROVENANCE_NAMES_DEVICE}).
|
|
519
519
|
*/
|
|
520
520
|
declare const ResultProvenance: z.ZodObject<{
|
|
521
521
|
audience: z.ZodEnum<{
|
|
@@ -544,6 +544,30 @@ declare function provenanceFor(input: {
|
|
|
544
544
|
backendClass: BackendClass;
|
|
545
545
|
model: string;
|
|
546
546
|
}): ResultProvenance;
|
|
547
|
+
/**
|
|
548
|
+
* What the daemon did, sealed with the answer — cloud_008 §2.5.
|
|
549
|
+
*
|
|
550
|
+
* These travelled in the clear on `ResultRequest`, which meant two things at
|
|
551
|
+
* once. On the direct plane the site believed unauthenticated fields beside
|
|
552
|
+
* an authenticated envelope — a daemon could seal one answer and *declare* it
|
|
553
|
+
* came from a different model, and only the field it did not sign would be
|
|
554
|
+
* recorded. Through a relay they reached a third party that acts on none of
|
|
555
|
+
* them, and `model` in particular is the kind of detail Amendment A's rule
|
|
556
|
+
* keeps off the wire.
|
|
557
|
+
*
|
|
558
|
+
* Sealed, they are the daemon's signed statement about its own run: the site
|
|
559
|
+
* opens them, nothing in between sees them, and the disposition check that
|
|
560
|
+
* already compares clear-text against ciphertext extends to cover them.
|
|
561
|
+
*/
|
|
562
|
+
declare const RunMetadata: z.ZodObject<{
|
|
563
|
+
model: z.ZodString;
|
|
564
|
+
backendClass: z.ZodEnum<{
|
|
565
|
+
http: "http";
|
|
566
|
+
process: "process";
|
|
567
|
+
}>;
|
|
568
|
+
durationMs: z.ZodNumber;
|
|
569
|
+
}, z.core.$strict>;
|
|
570
|
+
type RunMetadata = z.infer<typeof RunMetadata>;
|
|
547
571
|
/** Successful outcome. */
|
|
548
572
|
declare const JobResultOk: z.ZodObject<{
|
|
549
573
|
outcome: z.ZodLiteral<"ok">;
|
|
@@ -574,6 +598,37 @@ declare const JobOutcome: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
574
598
|
outcome: z.ZodLiteral<"canceled">;
|
|
575
599
|
}, z.core.$strict>], "outcome">;
|
|
576
600
|
type JobOutcome = z.infer<typeof JobOutcome>;
|
|
601
|
+
/**
|
|
602
|
+
* The plaintext inside a result envelope.
|
|
603
|
+
*
|
|
604
|
+
* The outcome and how it was produced, together, because they are one
|
|
605
|
+
* statement by one signer. A site that opened only the outcome would be
|
|
606
|
+
* trusting the envelope for the answer and the request body for everything
|
|
607
|
+
* about it.
|
|
608
|
+
*/
|
|
609
|
+
declare const SealedOutcome: z.ZodObject<{
|
|
610
|
+
outcome: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
611
|
+
outcome: z.ZodLiteral<"ok">;
|
|
612
|
+
text: z.ZodString;
|
|
613
|
+
artifactUrl: z.ZodOptional<z.ZodURL>;
|
|
614
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
615
|
+
outcome: z.ZodLiteral<"error">;
|
|
616
|
+
code: z.ZodString;
|
|
617
|
+
message: z.ZodString;
|
|
618
|
+
retryable: z.ZodBoolean;
|
|
619
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
620
|
+
outcome: z.ZodLiteral<"canceled">;
|
|
621
|
+
}, z.core.$strict>], "outcome">;
|
|
622
|
+
ran: z.ZodObject<{
|
|
623
|
+
model: z.ZodString;
|
|
624
|
+
backendClass: z.ZodEnum<{
|
|
625
|
+
http: "http";
|
|
626
|
+
process: "process";
|
|
627
|
+
}>;
|
|
628
|
+
durationMs: z.ZodNumber;
|
|
629
|
+
}, z.core.$strict>;
|
|
630
|
+
}, z.core.$strict>;
|
|
631
|
+
type SealedOutcome = z.infer<typeof SealedOutcome>;
|
|
577
632
|
/** A completed job as delivered to the app, provenance attached. */
|
|
578
633
|
declare const DeliveredResult: z.ZodObject<{
|
|
579
634
|
jobId: z.ZodString;
|
|
@@ -676,6 +731,7 @@ declare const JobStub: z.ZodObject<{
|
|
|
676
731
|
"llm.chat": "llm.chat";
|
|
677
732
|
}>;
|
|
678
733
|
owner: z.ZodString;
|
|
734
|
+
site: z.ZodString;
|
|
679
735
|
audience: z.ZodEnum<{
|
|
680
736
|
self: "self";
|
|
681
737
|
named: "named";
|
|
@@ -699,6 +755,7 @@ declare const ClaimedStub: z.ZodObject<{
|
|
|
699
755
|
"llm.chat": "llm.chat";
|
|
700
756
|
}>;
|
|
701
757
|
owner: z.ZodString;
|
|
758
|
+
site: z.ZodString;
|
|
702
759
|
audience: z.ZodEnum<{
|
|
703
760
|
self: "self";
|
|
704
761
|
named: "named";
|
|
@@ -1125,7 +1182,7 @@ declare const MUSTS: Readonly<{
|
|
|
1125
1182
|
readonly TTL_EXPIRY: Must;
|
|
1126
1183
|
readonly NO_RUNNER_SIGNAL: Must;
|
|
1127
1184
|
readonly RESULT_IDEMPOTENT: Must;
|
|
1128
|
-
readonly
|
|
1185
|
+
readonly PROVENANCE_NAMES_DEVICE: Must;
|
|
1129
1186
|
readonly INGRESS_LOGGED_BEFORE_EXECUTION: Must;
|
|
1130
1187
|
readonly NO_SHELL_INTERPOLATION: Must;
|
|
1131
1188
|
readonly NO_PAYLOAD_ROUTING: Must;
|
|
@@ -1133,11 +1190,18 @@ declare const MUSTS: Readonly<{
|
|
|
1133
1190
|
readonly HTTP_BASE_URL_SAFE: Must;
|
|
1134
1191
|
readonly OUTPUT_INERT: Must;
|
|
1135
1192
|
readonly COMMUNITY_BUDGETS: Must;
|
|
1193
|
+
readonly REVOCATION_IMMEDIATE: Must;
|
|
1194
|
+
readonly CONSENT_BEFORE_ROUTE: Must;
|
|
1195
|
+
readonly ROSTER_NOT_DISCLOSED: Must;
|
|
1196
|
+
readonly EFFECTIVE_OFFER_ONLY: Must;
|
|
1197
|
+
readonly FALLBACK_LABELED: Must;
|
|
1198
|
+
readonly RELAY_BLIND: Must;
|
|
1199
|
+
readonly SHARED_COMPUTE_DISCLOSED: Must;
|
|
1136
1200
|
}>;
|
|
1137
1201
|
/** The id of any normative MUST. */
|
|
1138
1202
|
type MustId = keyof typeof MUSTS;
|
|
1139
1203
|
/** All MUST ids, for coverage checks. */
|
|
1140
|
-
declare const MUST_IDS: readonly ("PAIR_ONE_USER" | "PAIR_INTERACTIVE" | "PAIR_CODE_EXPIRES" | "VERSION_HANDSHAKE_REQUIRED" | "KEYS_EXCHANGED_AT_CONSENT" | "REQUESTS_SIGNED_NOT_BEARER" | "LEASE_SCOPED_BY_GRANT" | "STUB_METADATA_EXHAUSTIVE" | "ENVELOPE_SEALED_AND_SIGNED" | "KIND_TYPED_ONLY" | "KIND_NO_CODE" | "CLAIM_REQUIRES_CAPABILITY" | "CAPABILITY_IS_DETECTED" | "CLAIM_ATOMIC" | "LEASE_HONORED" | "LEASE_RECLAIMABLE" | "AUDIENCE_BOTH_SIDES" | "SUBSCRIPTION_SELF_LOCK" | "METERED_DEFAULTS_SELF" | "METERED_REQUIRES_CEILING" | "COST_NOT_CONFIGURABLE" | "REMOTE_IS_NEVER_FREE" | "NAMED_LOCAL_ALLOWLIST" | "REFUSAL_NOT_REOFFERED" | "REVOCATION_HONORED" | "CANCEL_HONORED" | "DEPENDS_ON_GATING" | "TTL_EXPIRY" | "NO_RUNNER_SIGNAL" | "RESULT_IDEMPOTENT" | "
|
|
1204
|
+
declare const MUST_IDS: readonly ("PAIR_ONE_USER" | "PAIR_INTERACTIVE" | "PAIR_CODE_EXPIRES" | "VERSION_HANDSHAKE_REQUIRED" | "KEYS_EXCHANGED_AT_CONSENT" | "REQUESTS_SIGNED_NOT_BEARER" | "LEASE_SCOPED_BY_GRANT" | "STUB_METADATA_EXHAUSTIVE" | "ENVELOPE_SEALED_AND_SIGNED" | "KIND_TYPED_ONLY" | "KIND_NO_CODE" | "CLAIM_REQUIRES_CAPABILITY" | "CAPABILITY_IS_DETECTED" | "CLAIM_ATOMIC" | "LEASE_HONORED" | "LEASE_RECLAIMABLE" | "AUDIENCE_BOTH_SIDES" | "SUBSCRIPTION_SELF_LOCK" | "METERED_DEFAULTS_SELF" | "METERED_REQUIRES_CEILING" | "COST_NOT_CONFIGURABLE" | "REMOTE_IS_NEVER_FREE" | "NAMED_LOCAL_ALLOWLIST" | "REFUSAL_NOT_REOFFERED" | "REVOCATION_HONORED" | "CANCEL_HONORED" | "DEPENDS_ON_GATING" | "TTL_EXPIRY" | "NO_RUNNER_SIGNAL" | "RESULT_IDEMPOTENT" | "PROVENANCE_NAMES_DEVICE" | "INGRESS_LOGGED_BEFORE_EXECUTION" | "NO_SHELL_INTERPOLATION" | "NO_PAYLOAD_ROUTING" | "STRIPPED_CHILD_ENV" | "HTTP_BASE_URL_SAFE" | "OUTPUT_INERT" | "COMMUNITY_BUDGETS" | "REVOCATION_IMMEDIATE" | "CONSENT_BEFORE_ROUTE" | "ROSTER_NOT_DISCLOSED" | "EFFECTIVE_OFFER_ONLY" | "FALLBACK_LABELED" | "RELAY_BLIND" | "SHARED_COMPUTE_DISCLOSED")[];
|
|
1141
1205
|
/** Every MUST verified a particular way. */
|
|
1142
1206
|
declare function mustsVerifiedBy(kind: MustVerification): MustId[];
|
|
1143
1207
|
|
|
@@ -1485,6 +1549,7 @@ declare const ClaimResponse: z.ZodObject<{
|
|
|
1485
1549
|
"llm.chat": "llm.chat";
|
|
1486
1550
|
}>;
|
|
1487
1551
|
owner: z.ZodString;
|
|
1552
|
+
site: z.ZodString;
|
|
1488
1553
|
audience: z.ZodEnum<{
|
|
1489
1554
|
self: "self";
|
|
1490
1555
|
named: "named";
|
|
@@ -1557,10 +1622,6 @@ type HeartbeatRequest = z.infer<typeof HeartbeatRequest>;
|
|
|
1557
1622
|
declare const HeartbeatResponse: z.ZodObject<{
|
|
1558
1623
|
revoked: z.ZodBoolean;
|
|
1559
1624
|
cancel: z.ZodArray<z.ZodString>;
|
|
1560
|
-
leases: z.ZodArray<z.ZodObject<{
|
|
1561
|
-
jobId: z.ZodString;
|
|
1562
|
-
expiresAt: z.ZodNumber;
|
|
1563
|
-
}, z.core.$strict>>;
|
|
1564
1625
|
lost: z.ZodArray<z.ZodString>;
|
|
1565
1626
|
serverTime: z.ZodNumber;
|
|
1566
1627
|
}, z.core.$strict>;
|
|
@@ -1587,6 +1648,7 @@ declare const ResultRequest: z.ZodObject<{
|
|
|
1587
1648
|
protocolVersion: z.ZodLiteral<"0">;
|
|
1588
1649
|
runnerId: z.ZodString;
|
|
1589
1650
|
jobId: z.ZodString;
|
|
1651
|
+
leaseId: z.ZodString;
|
|
1590
1652
|
envelope: z.ZodObject<{
|
|
1591
1653
|
ciphertext: z.ZodString;
|
|
1592
1654
|
recipientKeyId: z.ZodString;
|
|
@@ -1602,12 +1664,6 @@ declare const ResultRequest: z.ZodObject<{
|
|
|
1602
1664
|
error: "error";
|
|
1603
1665
|
canceled: "canceled";
|
|
1604
1666
|
}>;
|
|
1605
|
-
model: z.ZodString;
|
|
1606
|
-
backendClass: z.ZodEnum<{
|
|
1607
|
-
http: "http";
|
|
1608
|
-
process: "process";
|
|
1609
|
-
}>;
|
|
1610
|
-
durationMs: z.ZodNumber;
|
|
1611
1667
|
}, z.core.$strict>;
|
|
1612
1668
|
type ResultRequest = z.infer<typeof ResultRequest>;
|
|
1613
1669
|
declare const ResultResponse: z.ZodObject<{
|
|
@@ -1648,7 +1704,10 @@ declare const WireErrorCode: z.ZodEnum<{
|
|
|
1648
1704
|
revoked: "revoked";
|
|
1649
1705
|
"bad-request": "bad-request";
|
|
1650
1706
|
unauthorized: "unauthorized";
|
|
1707
|
+
forbidden: "forbidden";
|
|
1651
1708
|
"not-found": "not-found";
|
|
1709
|
+
"not-ready": "not-ready";
|
|
1710
|
+
"clock-skew": "clock-skew";
|
|
1652
1711
|
"rate-limited": "rate-limited";
|
|
1653
1712
|
"server-error": "server-error";
|
|
1654
1713
|
}>;
|
|
@@ -1659,12 +1718,17 @@ declare const WireError: z.ZodObject<{
|
|
|
1659
1718
|
revoked: "revoked";
|
|
1660
1719
|
"bad-request": "bad-request";
|
|
1661
1720
|
unauthorized: "unauthorized";
|
|
1721
|
+
forbidden: "forbidden";
|
|
1662
1722
|
"not-found": "not-found";
|
|
1723
|
+
"not-ready": "not-ready";
|
|
1724
|
+
"clock-skew": "clock-skew";
|
|
1663
1725
|
"rate-limited": "rate-limited";
|
|
1664
1726
|
"server-error": "server-error";
|
|
1665
1727
|
}>;
|
|
1666
1728
|
message: z.ZodString;
|
|
1667
1729
|
retryAfter: z.ZodOptional<z.ZodNumber>;
|
|
1730
|
+
serverTime: z.ZodOptional<z.ZodNumber>;
|
|
1731
|
+
maxSkewMs: z.ZodOptional<z.ZodNumber>;
|
|
1668
1732
|
}, z.core.$strict>;
|
|
1669
1733
|
type WireError = z.infer<typeof WireError>;
|
|
1670
1734
|
/** HTTP status each error code is served with. */
|
|
@@ -1690,4 +1754,4 @@ declare const FetchResponse: z.ZodObject<{
|
|
|
1690
1754
|
}, z.core.$strict>;
|
|
1691
1755
|
type FetchResponse = z.infer<typeof FetchResponse>;
|
|
1692
1756
|
|
|
1693
|
-
export { AUDIENCES, Audience, BACKENDS, BACKEND_IDS, BackendClass, BackendCost, type BackendDescriptor, type BackendId, BackendIdSchema, Capability, CapabilityMatrix, ChatMessage, ChatPayload, ClaimRequest, ClaimResponse, ClaimedJob, ClaimedStub, DeliveredResult, ENDPOINTS, ENVELOPE_MAX_AGE_MS, ERROR_STATUS, type Endpoint, type EnvelopeContext, EnvelopeDirection, type EnvelopeFailure, FetchRequest, FetchResponse, GeneratePayload, HeartbeatRequest, HeartbeatResponse, JOB_KINDS, JobKind, JobOutcome, JobPayload, JobResultCanceled, JobResultError, JobResultOk, JobState, JobStub, KindedPayload, Lease, MAX_CLOCK_SKEW_MS, MIN_PROTOCOL_VERSION, MUSTS, MUST_IDS, type MatchDaemon, type MatchJob, MatchRefusal, type MatchResult, type Must, type MustEnforcer, type MustId, type MustVerification, OFFER_SCOPES, OfferScope, type OpenResult, PAYLOAD_LIMITS, PROTOCOL_PREFIX, PROTOCOL_VERSION, PairPollRequest, PairPollResponse, PairRequest, PairStartRequest, PairStartResponse, type PayloadFor, PublicIdentity, REFUSAL_MESSAGES, ReleaseRequest, ReleaseResponse, RequestSignature, ResultDisposition, ResultProvenance, ResultRequest, ResultResponse, SIZE_CLASS_LIMITS, SUPPORTED_PROTOCOL_VERSIONS, SealedEnvelope, type SignatureFailure, SizeClass, type SpendConsent, StoredKeys, TERMINAL_STATES, type VersionRefusal, WireError, WireErrorCode, backendDescriptor, canTransition, canonicalRequest, checkProtocolVersion, cryptoReady, effectiveOfferScope, fingerprint, generateKeys, isBackendId, isJobKind, isLocalHost, isTerminal, keyId, matchAudience, mustsVerifiedBy, open, payloadTextLength, provenanceFor, publicIdentityOf, resolveCost, seal, signRequest, signSiteRequest, signWith, sizeClassCeiling, sizeClassOf, verifyPublicIdentity, verifyRequest, verifySiteRequest, verifyWith };
|
|
1757
|
+
export { AUDIENCES, Audience, BACKENDS, BACKEND_IDS, BackendClass, BackendCost, type BackendDescriptor, type BackendId, BackendIdSchema, Capability, CapabilityMatrix, ChatMessage, ChatPayload, ClaimRequest, ClaimResponse, ClaimedJob, ClaimedStub, DeliveredResult, ENDPOINTS, ENVELOPE_MAX_AGE_MS, ERROR_STATUS, type Endpoint, type EnvelopeContext, EnvelopeDirection, type EnvelopeFailure, FetchRequest, FetchResponse, GeneratePayload, HeartbeatRequest, HeartbeatResponse, JOB_KINDS, JobKind, JobOutcome, JobPayload, JobResultCanceled, JobResultError, JobResultOk, JobState, JobStub, KindedPayload, Lease, MAX_CLOCK_SKEW_MS, MIN_PROTOCOL_VERSION, MUSTS, MUST_IDS, type MatchDaemon, type MatchJob, MatchRefusal, type MatchResult, type Must, type MustEnforcer, type MustId, type MustVerification, OFFER_SCOPES, OfferScope, type OpenResult, PAYLOAD_LIMITS, PROTOCOL_PREFIX, PROTOCOL_VERSION, PairPollRequest, PairPollResponse, PairRequest, PairStartRequest, PairStartResponse, type PayloadFor, PublicIdentity, REFUSAL_MESSAGES, ReleaseRequest, ReleaseResponse, RequestSignature, ResultDisposition, ResultProvenance, ResultRequest, ResultResponse, RunMetadata, SIZE_CLASS_LIMITS, SUPPORTED_PROTOCOL_VERSIONS, SealedEnvelope, SealedOutcome, type SignatureFailure, SizeClass, type SpendConsent, StoredKeys, TERMINAL_STATES, type VersionRefusal, WireError, WireErrorCode, backendDescriptor, canTransition, canonicalRequest, checkProtocolVersion, cryptoReady, effectiveOfferScope, fingerprint, generateKeys, isBackendId, isJobKind, isLocalHost, isTerminal, keyId, matchAudience, mustsVerifiedBy, open, payloadTextLength, provenanceFor, publicIdentityOf, resolveCost, seal, signRequest, signSiteRequest, signWith, sizeClassCeiling, sizeClassOf, verifyPublicIdentity, verifyRequest, verifySiteRequest, verifyWith };
|
package/dist/index.js
CHANGED
|
@@ -411,6 +411,13 @@ function provenanceFor(input) {
|
|
|
411
411
|
untrusted: input.audience !== "self"
|
|
412
412
|
};
|
|
413
413
|
}
|
|
414
|
+
var RunMetadata = z4.object({
|
|
415
|
+
/** Which model actually served it. */
|
|
416
|
+
model: z4.string().min(1),
|
|
417
|
+
backendClass: BackendClass,
|
|
418
|
+
/** Wall-clock milliseconds the backend call took. */
|
|
419
|
+
durationMs: z4.number().int().nonnegative()
|
|
420
|
+
}).strict();
|
|
414
421
|
var JobResultOk = z4.object({
|
|
415
422
|
outcome: z4.literal("ok"),
|
|
416
423
|
text: z4.string(),
|
|
@@ -432,6 +439,7 @@ var JobOutcome = z4.discriminatedUnion("outcome", [
|
|
|
432
439
|
JobResultError,
|
|
433
440
|
JobResultCanceled
|
|
434
441
|
]);
|
|
442
|
+
var SealedOutcome = z4.object({ outcome: JobOutcome, ran: RunMetadata }).strict();
|
|
435
443
|
var DeliveredResult = z4.object({
|
|
436
444
|
jobId: z4.string().min(1),
|
|
437
445
|
state: JobState,
|
|
@@ -458,6 +466,30 @@ var JobStub = z4.object({
|
|
|
458
466
|
kind: JobKind,
|
|
459
467
|
/** The app's id for the user who enqueued it. */
|
|
460
468
|
owner: z4.string().min(1),
|
|
469
|
+
/**
|
|
470
|
+
* Which site this job belongs to — byollm_009 Amendment A §A.3.
|
|
471
|
+
*
|
|
472
|
+
* **The site's identity key id**, not an id somebody assigned it. §6 has
|
|
473
|
+
* listed `site` since this spec was frozen; the schema never carried it,
|
|
474
|
+
* which is the drift the amendment closes.
|
|
475
|
+
*
|
|
476
|
+
* A key id rather than an opaque handle for one reason above the others:
|
|
477
|
+
* it makes the stub *self-describing* instead of a pointer into somebody
|
|
478
|
+
* else's table. A daemon holds this key id already, from pinning, so it
|
|
479
|
+
* can check `stub.site` against the payload envelope's `senderKeyId`
|
|
480
|
+
* without a lookup and without trusting the party that routed it. An
|
|
481
|
+
* opaque id can only be believed.
|
|
482
|
+
*
|
|
483
|
+
* It also avoids inventing a second namespace for a thing that has a
|
|
484
|
+
* canonical one — the shape of finding 41 (two owner namespaces compared
|
|
485
|
+
* for equality) and of finding fourteen before it.
|
|
486
|
+
*
|
|
487
|
+
* Rotation is a designed transition rather than a cost: a site publishes a
|
|
488
|
+
* new identity signed by the outgoing one, both are valid through an
|
|
489
|
+
* overlap window, and a daemon re-keys its own map by verifying that
|
|
490
|
+
* signature against the key it already pinned (§A.3.1).
|
|
491
|
+
*/
|
|
492
|
+
site: z4.string().min(1),
|
|
461
493
|
audience: Audience,
|
|
462
494
|
// `audienceAllow` is **not** here, and its absence is the enforcement —
|
|
463
495
|
// cloud_008 §0.2.
|
|
@@ -1020,12 +1052,12 @@ var MUSTS = Object.freeze({
|
|
|
1020
1052
|
verifiedBy: "conformance",
|
|
1021
1053
|
source: "byollm_001 \xA7Endpoints.4"
|
|
1022
1054
|
}),
|
|
1023
|
-
|
|
1024
|
-
id: "
|
|
1025
|
-
statement: "A result
|
|
1055
|
+
PROVENANCE_NAMES_DEVICE: must({
|
|
1056
|
+
id: "PROVENANCE_NAMES_DEVICE",
|
|
1057
|
+
statement: "A result MUST carry the claiming device's key id and its relationship to the requester, to the delivery seam, so an app never treats volunteer output as first-party. The key id MUST be the device the upstream granted the lease to, and a result whose signature does not verify against that device MUST be refused rather than recorded.",
|
|
1026
1058
|
enforcedBy: "server",
|
|
1027
1059
|
verifiedBy: "conformance",
|
|
1028
|
-
source: "
|
|
1060
|
+
source: "byollm_009 \xA711"
|
|
1029
1061
|
}),
|
|
1030
1062
|
// ---- The trust surface -------------------------------------------------
|
|
1031
1063
|
INGRESS_LOGGED_BEFORE_EXECUTION: must({
|
|
@@ -1077,8 +1109,92 @@ var MUSTS = Object.freeze({
|
|
|
1077
1109
|
enforcedBy: "daemon",
|
|
1078
1110
|
verifiedBy: "adversarial",
|
|
1079
1111
|
source: "byollm_004 \xA74"
|
|
1112
|
+
}),
|
|
1113
|
+
REVOCATION_IMMEDIATE: must({
|
|
1114
|
+
id: "REVOCATION_IMMEDIATE",
|
|
1115
|
+
statement: "Revocation MUST take effect at the upstream at once \u2014 a revoked runner MUST NOT be granted further work from the moment the record changes \u2014 and MUST reach the daemon by its next heartbeat.",
|
|
1116
|
+
// Both, and stated as one sentence with two obligations rather than
|
|
1117
|
+
// folded into REVOCATION_HONORED. That one binds the *daemon*: a revoked
|
|
1118
|
+
// daemon stops claiming and abandons in-flight work. This binds the
|
|
1119
|
+
// *upstream*. byollm_009 §5 is explicit that the pair is the point — "a
|
|
1120
|
+
// revocation enforced at one end survives a compromise of that end" — and
|
|
1121
|
+
// one entry covering both would make a compromised daemon look compliant.
|
|
1122
|
+
enforcedBy: "both",
|
|
1123
|
+
verifiedBy: "conformance",
|
|
1124
|
+
source: "byollm_009 \xA711"
|
|
1125
|
+
}),
|
|
1126
|
+
CONSENT_BEFORE_ROUTE: must({
|
|
1127
|
+
id: "CONSENT_BEFORE_ROUTE",
|
|
1128
|
+
statement: "An upstream MUST NOT route a job to a device without a record binding that user, that site and that scope. There MUST be no discovery path by which a device receives work it was never granted.",
|
|
1129
|
+
enforcedBy: "server",
|
|
1130
|
+
verifiedBy: "conformance",
|
|
1131
|
+
source: "byollm_009 \xA711"
|
|
1132
|
+
}),
|
|
1133
|
+
ROSTER_NOT_DISCLOSED: must({
|
|
1134
|
+
id: "ROSTER_NOT_DISCLOSED",
|
|
1135
|
+
statement: "A site MUST NOT learn the membership of a group whose compute it uses, and MUST NOT publish membership to a routing party. No wire message may carry a list of who may run a job.",
|
|
1136
|
+
// Checkable since cloud_008 §0.2 took `audienceAllow` off the stub: the
|
|
1137
|
+
// property now holds by *absence*, and absence is exactly what a strict
|
|
1138
|
+
// schema and a serialised stub can be asked about. Before that it was a
|
|
1139
|
+
// sentence — and one this project cited in code comments, tests and two
|
|
1140
|
+
// specs as though it were enforced data, which is why it is worth
|
|
1141
|
+
// stating precisely rather than generously.
|
|
1142
|
+
enforcedBy: "both",
|
|
1143
|
+
verifiedBy: "conformance",
|
|
1144
|
+
source: "byollm_009 \xA711"
|
|
1145
|
+
}),
|
|
1146
|
+
EFFECTIVE_OFFER_ONLY: must({
|
|
1147
|
+
id: "EFFECTIVE_OFFER_ONLY",
|
|
1148
|
+
statement: "A daemon MUST declare effective offers only. An upstream MUST NOT receive raw config, allowlists, or capacity the owner has not shared, and MUST act on the declared offer rather than on what was asked for.",
|
|
1149
|
+
enforcedBy: "both",
|
|
1150
|
+
verifiedBy: "conformance",
|
|
1151
|
+
source: "byollm_009 \xA711"
|
|
1152
|
+
}),
|
|
1153
|
+
FALLBACK_LABELED: must({
|
|
1154
|
+
id: "FALLBACK_LABELED",
|
|
1155
|
+
statement: "Work served by anything other than the user's own compute MUST be labelled as such wherever it is reported, and MUST NOT be silently substituted.",
|
|
1156
|
+
// `construction` today, and deliberately not `conformance`. Nothing on
|
|
1157
|
+
// the wire yet distinguishes a fallback from any other community job —
|
|
1158
|
+
// the ledger that would give it a surface is unbuilt — so a check would
|
|
1159
|
+
// have to assert something it cannot observe. Promoted the day that
|
|
1160
|
+
// surface exists. Marking it `conformance` now would put "verified"
|
|
1161
|
+
// beside a property no third party can see, which is the one thing the
|
|
1162
|
+
// kinds exist to prevent.
|
|
1163
|
+
enforcedBy: "both",
|
|
1164
|
+
verifiedBy: "construction",
|
|
1165
|
+
source: "byollm_009 \xA711"
|
|
1166
|
+
}),
|
|
1167
|
+
RELAY_BLIND: must({
|
|
1168
|
+
id: "RELAY_BLIND",
|
|
1169
|
+
statement: "A relay MUST NOT hold any key capable of decrypting a payload, a result, or a delta frame.",
|
|
1170
|
+
// Operator: a third party can read the relay's types and see there is
|
|
1171
|
+
// nowhere to put such a key, but the kit certifies a *server* and cannot
|
|
1172
|
+
// reach inside somebody's deployment to prove what it holds.
|
|
1173
|
+
enforcedBy: "server",
|
|
1174
|
+
verifiedBy: "operator",
|
|
1175
|
+
source: "byollm_009 \xA711"
|
|
1176
|
+
}),
|
|
1177
|
+
SHARED_COMPUTE_DISCLOSED: must({
|
|
1178
|
+
id: "SHARED_COMPUTE_DISCLOSED",
|
|
1179
|
+
statement: "Before a user's work first runs on compute they do not own, they MUST be told in plain language that the machine's owner can see it.",
|
|
1180
|
+
// Operator, and cloud_008 §0.3 is why the classification now comes with a
|
|
1181
|
+
// standing answer rather than a standing question. The screen is not
|
|
1182
|
+
// wire-observable, but the *string the server composes* is, and it is
|
|
1183
|
+
// now unit-tested with the two false sentences forbidden by name. The
|
|
1184
|
+
// kind stays `operator` because a third-party site can still render
|
|
1185
|
+
// whatever it likes; what changed is that the part inside our own
|
|
1186
|
+
// boundary stopped depending on somebody remembering to audit it.
|
|
1187
|
+
enforcedBy: "server",
|
|
1188
|
+
verifiedBy: "operator",
|
|
1189
|
+
source: "byollm_009 \xA711"
|
|
1080
1190
|
})
|
|
1081
1191
|
});
|
|
1192
|
+
var RETIRED_MUSTS = Object.freeze({
|
|
1193
|
+
RESULT_PROVENANCE: {
|
|
1194
|
+
supersededBy: "PROVENANCE_NAMES_DEVICE",
|
|
1195
|
+
note: "Strengthened, not renamed: attribution is now by proof of possession \u2014 the result's signature must verify against the device the upstream granted the lease to \u2014 rather than by a provenance label travelling beside it. byollm_009 \xA711 states the stronger form."
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1082
1198
|
var MUST_IDS = Object.freeze(Object.keys(MUSTS));
|
|
1083
1199
|
function mustsVerifiedBy(kind) {
|
|
1084
1200
|
return MUST_IDS.filter((id) => MUSTS[id].verifiedBy === kind);
|
|
@@ -1233,13 +1349,26 @@ var HeartbeatResponse = z8.object({
|
|
|
1233
1349
|
* in-flight backend calls and reports them `canceled`.
|
|
1234
1350
|
*/
|
|
1235
1351
|
cancel: z8.array(z8.string().min(1)),
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1352
|
+
// `leases` is deliberately absent — cloud_008 §1.4b, finding 16.
|
|
1353
|
+
//
|
|
1354
|
+
// It carried "these leases were renewed, and here is the new expiry", and
|
|
1355
|
+
// **no daemon ever read it.** A mutation returning an empty list while
|
|
1356
|
+
// renewing correctly survived every test, which is what made it visible.
|
|
1357
|
+
//
|
|
1358
|
+
// It is neither a class nor membership, so Amendment A's rule does not
|
|
1359
|
+
// decide it — the older test does: nothing reads it, so it is dead wire.
|
|
1360
|
+
// §6's exhaustiveness is a commitment about what an upstream can see, and
|
|
1361
|
+
// it applies to every message rather than only to the stub.
|
|
1362
|
+
//
|
|
1363
|
+
// `lost` is the actionable signal and always was: a daemon stops work on
|
|
1364
|
+
// a lease it no longer holds. "Renewed" was the same question answered a
|
|
1365
|
+
// second time, and a second answer can only agree or contradict.
|
|
1366
|
+
//
|
|
1367
|
+
// Renewal itself is untouched — the upstream still extends the grants a
|
|
1368
|
+
// heartbeat names, which is what §0.6 fixed. What ended is telling the
|
|
1369
|
+
// daemon about it in a field it ignored. If an upstream ever needs to
|
|
1370
|
+
// push lease decisions, that is a new field with a reader, added on
|
|
1371
|
+
// purpose.
|
|
1243
1372
|
/**
|
|
1244
1373
|
* Jobs the daemon thinks it holds but the server has reassigned or
|
|
1245
1374
|
* expired. The daemon must stop work on these and not report results.
|
|
@@ -1253,6 +1382,29 @@ var ResultRequest = z8.object({
|
|
|
1253
1382
|
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1254
1383
|
runnerId: z8.string().min(1),
|
|
1255
1384
|
jobId: z8.string().min(1),
|
|
1385
|
+
/**
|
|
1386
|
+
* The grant this result was produced under — cloud_008 §1.4a.
|
|
1387
|
+
*
|
|
1388
|
+
* `fetch` has always named its lease, with the reasoning written beside
|
|
1389
|
+
* it: a request that names only the job would be answerable for whatever
|
|
1390
|
+
* lease exists when it arrives. **The operation that writes the result did
|
|
1391
|
+
* not**, on either plane, and checked only the runner id — which survives
|
|
1392
|
+
* a claim-release-reclaim cycle, so a device whose grant had been swept
|
|
1393
|
+
* and reissued could still land a result for a job it no longer held.
|
|
1394
|
+
*
|
|
1395
|
+
* Found by tracing a mutation that survived in §0.6: the lease lapsed, the
|
|
1396
|
+
* sweep requeued, the daemon re-claimed under a new grant, and the
|
|
1397
|
+
* original run finished and posted anyway. The relay marked the job done
|
|
1398
|
+
* with a result the site cannot open — it verifies the envelope against
|
|
1399
|
+
* the *current* holder's device, so the crypto contains the substitution —
|
|
1400
|
+
* and then refused the real holder's result as a replay. A lost job, in
|
|
1401
|
+
* silence.
|
|
1402
|
+
*
|
|
1403
|
+
* `LEASE_HONORED` is a statement about a lease *instance*. That was
|
|
1404
|
+
* learned once already, when a replayed release yanked a later grant, and
|
|
1405
|
+
* it applies here for the same reason.
|
|
1406
|
+
*/
|
|
1407
|
+
leaseId: z8.string().min(1),
|
|
1256
1408
|
/**
|
|
1257
1409
|
* The outcome, sealed to the site and signed by the device.
|
|
1258
1410
|
*
|
|
@@ -1268,12 +1420,21 @@ var ResultRequest = z8.object({
|
|
|
1268
1420
|
* fact: believing it unverified would let a daemon mark a job `ok` while
|
|
1269
1421
|
* sealing an error, and only the app would ever find out.
|
|
1270
1422
|
*/
|
|
1271
|
-
disposition: ResultDisposition
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1423
|
+
disposition: ResultDisposition
|
|
1424
|
+
// `model`, `backendClass` and `durationMs` are **inside the envelope** —
|
|
1425
|
+
// cloud_008 §2.5. See {@link RunMetadata}.
|
|
1426
|
+
//
|
|
1427
|
+
// They were here, in the clear, and that was two problems wearing one
|
|
1428
|
+
// coat. On the direct plane the site recorded unauthenticated fields
|
|
1429
|
+
// beside an authenticated answer: a daemon could seal one result and
|
|
1430
|
+
// declare a different model, and only the unsigned half would reach the
|
|
1431
|
+
// app. Through a relay they reached a third party that acts on none of
|
|
1432
|
+
// them — `model` in particular being the sort of detail Amendment A's
|
|
1433
|
+
// rule keeps off the wire.
|
|
1434
|
+
//
|
|
1435
|
+
// `disposition` stays, and the difference is the test: a relay *routes*
|
|
1436
|
+
// on it, so it is a class a routing party consumes. Nobody between the
|
|
1437
|
+
// two ends consumes these.
|
|
1277
1438
|
}).strict();
|
|
1278
1439
|
var ResultResponse = z8.object({
|
|
1279
1440
|
/**
|
|
@@ -1315,9 +1476,40 @@ var ReleaseResponse = z8.object({
|
|
|
1315
1476
|
var WireErrorCode = z8.enum([
|
|
1316
1477
|
"bad-request",
|
|
1317
1478
|
"unsupported-protocol-version",
|
|
1479
|
+
// "We do not know who you are." Exactly 401, and only that — cloud_008
|
|
1480
|
+
// §1.4d.
|
|
1318
1481
|
"unauthorized",
|
|
1482
|
+
/**
|
|
1483
|
+
* "We know exactly who you are, and the answer is no." Exactly 403.
|
|
1484
|
+
*
|
|
1485
|
+
* Five refusals across both planes served 403 with `unauthorized`, whose
|
|
1486
|
+
* table entry is 401: a revoked device, a site claiming another site's
|
|
1487
|
+
* stub, a job you do not hold, a device belonging to another owner, a
|
|
1488
|
+
* relay that does not route for you. Every one of them is an *identified*
|
|
1489
|
+
* caller being refused.
|
|
1490
|
+
*
|
|
1491
|
+
* Collapsing the two loses a distinction that matters everywhere it is
|
|
1492
|
+
* read: a revoked daemon would look like an unsigned one in every log and
|
|
1493
|
+
* every client branch, and "check your keys" is the wrong advice for both
|
|
1494
|
+
* of them in opposite directions.
|
|
1495
|
+
*/
|
|
1496
|
+
"forbidden",
|
|
1319
1497
|
"revoked",
|
|
1320
1498
|
"not-found",
|
|
1499
|
+
// Claimed, but the site has not sealed the payload yet — cloud_008 §1.4.
|
|
1500
|
+
//
|
|
1501
|
+
// A daemon must retry rather than abandon: the job is legitimately still
|
|
1502
|
+
// its own until the lease or the awaiting-payload clock says otherwise.
|
|
1503
|
+
// That is why it cannot be `not-found` or `server-error`, and why it was
|
|
1504
|
+
// the protocol gap that produced a bare 409 in the first place.
|
|
1505
|
+
"not-ready",
|
|
1506
|
+
// The caller's clock is too far from ours to judge a signature's freshness.
|
|
1507
|
+
//
|
|
1508
|
+
// Split out from `unauthorized` because the remedy is completely different
|
|
1509
|
+
// and only the server can tell them apart: a bad signature means the key is
|
|
1510
|
+
// wrong, this means the machine's time is wrong. A daemon reporting it as a
|
|
1511
|
+
// generic rejection sends its owner looking at their network.
|
|
1512
|
+
"clock-skew",
|
|
1321
1513
|
"rate-limited",
|
|
1322
1514
|
"server-error"
|
|
1323
1515
|
]);
|
|
@@ -1325,14 +1517,46 @@ var WireError = z8.object({
|
|
|
1325
1517
|
error: WireErrorCode,
|
|
1326
1518
|
message: z8.string().min(1),
|
|
1327
1519
|
/** Seconds; mirrors Retry-After for `rate-limited` and `server-error`. */
|
|
1328
|
-
retryAfter: z8.number().int().nonnegative().optional()
|
|
1329
|
-
|
|
1520
|
+
retryAfter: z8.number().int().nonnegative().optional(),
|
|
1521
|
+
/**
|
|
1522
|
+
* The server's clock, and the window it allows. `clock-skew` only.
|
|
1523
|
+
*
|
|
1524
|
+
* So the far side can say *how far off* rather than *that something is
|
|
1525
|
+
* wrong* — the difference between "adjust your clock by four minutes" and
|
|
1526
|
+
* "something is wrong with your connection". Not a disclosure: the
|
|
1527
|
+
* heartbeat response returns the same value, and so does every `Date`
|
|
1528
|
+
* header.
|
|
1529
|
+
*/
|
|
1530
|
+
serverTime: z8.number().int().positive().optional(),
|
|
1531
|
+
maxSkewMs: z8.number().int().positive().optional()
|
|
1532
|
+
}).strict().superRefine((error, ctx) => {
|
|
1533
|
+
const skew = error.error === "clock-skew";
|
|
1534
|
+
const carried = error.serverTime !== void 0 || error.maxSkewMs !== void 0;
|
|
1535
|
+
if (skew && !carried) {
|
|
1536
|
+
ctx.addIssue({
|
|
1537
|
+
code: "custom",
|
|
1538
|
+
message: "clock-skew must carry serverTime and maxSkewMs"
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
if (!skew && carried) {
|
|
1542
|
+
ctx.addIssue({
|
|
1543
|
+
code: "custom",
|
|
1544
|
+
message: `${error.error} must not carry serverTime or maxSkewMs`
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1330
1548
|
var ERROR_STATUS = Object.freeze({
|
|
1331
1549
|
"bad-request": 400,
|
|
1332
1550
|
"unsupported-protocol-version": 400,
|
|
1333
1551
|
unauthorized: 401,
|
|
1552
|
+
forbidden: 403,
|
|
1334
1553
|
revoked: 403,
|
|
1335
1554
|
"not-found": 404,
|
|
1555
|
+
// 409, not 404: the job exists and is yours, it is simply not ready.
|
|
1556
|
+
"not-ready": 409,
|
|
1557
|
+
// 401 alongside `unauthorized`, because that is what it is — the
|
|
1558
|
+
// signature could not be judged. The code is what carries the remedy.
|
|
1559
|
+
"clock-skew": 401,
|
|
1336
1560
|
"rate-limited": 429,
|
|
1337
1561
|
"server-error": 500
|
|
1338
1562
|
});
|
|
@@ -1421,9 +1645,11 @@ export {
|
|
|
1421
1645
|
ResultProvenance,
|
|
1422
1646
|
ResultRequest,
|
|
1423
1647
|
ResultResponse,
|
|
1648
|
+
RunMetadata,
|
|
1424
1649
|
SIZE_CLASS_LIMITS,
|
|
1425
1650
|
SUPPORTED_PROTOCOL_VERSIONS,
|
|
1426
1651
|
SealedEnvelope,
|
|
1652
|
+
SealedOutcome,
|
|
1427
1653
|
SizeClass,
|
|
1428
1654
|
StoredKeys,
|
|
1429
1655
|
TERMINAL_STATES,
|