@byollm/protocol 0.1.0-alpha.2 → 0.1.0-alpha.20
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 +66 -2
- package/dist/index.d.ts +921 -80
- package/dist/index.js +1158 -105
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -2,23 +2,166 @@
|
|
|
2
2
|
import { z as z2 } from "zod";
|
|
3
3
|
|
|
4
4
|
// src/backends.ts
|
|
5
|
+
import { isIP } from "net";
|
|
5
6
|
import { z } from "zod";
|
|
6
7
|
var BackendClass = z.enum(["http", "process"]);
|
|
7
|
-
var
|
|
8
|
+
var BackendCost = z.enum(["free", "metered", "subscription"]);
|
|
8
9
|
var backend = (b) => Object.freeze(b);
|
|
9
10
|
var BACKENDS = Object.freeze({
|
|
11
|
+
// -- free: local compute, costs electricity ------------------------------
|
|
12
|
+
ollama: backend({
|
|
13
|
+
id: "ollama",
|
|
14
|
+
label: "Ollama (local)",
|
|
15
|
+
class: "http",
|
|
16
|
+
cost: "free",
|
|
17
|
+
adversarialCorpus: "http",
|
|
18
|
+
defaultBaseUrl: "http://127.0.0.1:11434/v1"
|
|
19
|
+
}),
|
|
20
|
+
mlx: backend({
|
|
21
|
+
id: "mlx",
|
|
22
|
+
label: "MLX (mlx_lm.server, local)",
|
|
23
|
+
class: "http",
|
|
24
|
+
cost: "free",
|
|
25
|
+
adversarialCorpus: "http",
|
|
26
|
+
defaultBaseUrl: "http://127.0.0.1:8080/v1"
|
|
27
|
+
}),
|
|
28
|
+
llamacpp: backend({
|
|
29
|
+
id: "llamacpp",
|
|
30
|
+
label: "llama.cpp server (local)",
|
|
31
|
+
class: "http",
|
|
32
|
+
cost: "free",
|
|
33
|
+
adversarialCorpus: "http",
|
|
34
|
+
defaultBaseUrl: "http://127.0.0.1:8080/v1"
|
|
35
|
+
}),
|
|
36
|
+
vllm: backend({
|
|
37
|
+
id: "vllm",
|
|
38
|
+
label: "vLLM (local)",
|
|
39
|
+
class: "http",
|
|
40
|
+
cost: "free",
|
|
41
|
+
adversarialCorpus: "http",
|
|
42
|
+
defaultBaseUrl: "http://127.0.0.1:8000/v1"
|
|
43
|
+
}),
|
|
44
|
+
lmstudio: backend({
|
|
45
|
+
id: "lmstudio",
|
|
46
|
+
label: "LM Studio (local)",
|
|
47
|
+
class: "http",
|
|
48
|
+
cost: "free",
|
|
49
|
+
adversarialCorpus: "http",
|
|
50
|
+
defaultBaseUrl: "http://127.0.0.1:1234/v1"
|
|
51
|
+
}),
|
|
52
|
+
jan: backend({
|
|
53
|
+
id: "jan",
|
|
54
|
+
label: "Jan (local)",
|
|
55
|
+
class: "http",
|
|
56
|
+
cost: "free",
|
|
57
|
+
adversarialCorpus: "http",
|
|
58
|
+
defaultBaseUrl: "http://127.0.0.1:1337/v1"
|
|
59
|
+
}),
|
|
60
|
+
localai: backend({
|
|
61
|
+
id: "localai",
|
|
62
|
+
label: "LocalAI (local)",
|
|
63
|
+
class: "http",
|
|
64
|
+
cost: "free",
|
|
65
|
+
adversarialCorpus: "http",
|
|
66
|
+
defaultBaseUrl: "http://127.0.0.1:8080/v1"
|
|
67
|
+
}),
|
|
68
|
+
// -- metered: the owner's money, per token -------------------------------
|
|
69
|
+
/**
|
|
70
|
+
* Note the pair: `anthropic` and {@link BACKENDS."claude-cli"} reach the
|
|
71
|
+
* same vendor and land in different cost classes. That is not an
|
|
72
|
+
* inconsistency — it is the axis working. One bills a key per token, the
|
|
73
|
+
* other runs under a personal plan whose terms cover one person's work. Who
|
|
74
|
+
* pays and under what terms is the question; which company is not.
|
|
75
|
+
*/
|
|
76
|
+
anthropic: backend({
|
|
77
|
+
id: "anthropic",
|
|
78
|
+
label: "Anthropic (your API key)",
|
|
79
|
+
class: "http",
|
|
80
|
+
cost: "metered",
|
|
81
|
+
adversarialCorpus: "http",
|
|
82
|
+
defaultBaseUrl: "https://api.anthropic.com/v1"
|
|
83
|
+
}),
|
|
84
|
+
openai: backend({
|
|
85
|
+
id: "openai",
|
|
86
|
+
label: "OpenAI (your API key)",
|
|
87
|
+
class: "http",
|
|
88
|
+
cost: "metered",
|
|
89
|
+
adversarialCorpus: "http",
|
|
90
|
+
defaultBaseUrl: "https://api.openai.com/v1"
|
|
91
|
+
}),
|
|
92
|
+
gemini: backend({
|
|
93
|
+
id: "gemini",
|
|
94
|
+
label: "Google Gemini (your API key)",
|
|
95
|
+
class: "http",
|
|
96
|
+
cost: "metered",
|
|
97
|
+
adversarialCorpus: "http",
|
|
98
|
+
defaultBaseUrl: "https://generativelanguage.googleapis.com/v1beta/openai"
|
|
99
|
+
}),
|
|
100
|
+
grok: backend({
|
|
101
|
+
id: "grok",
|
|
102
|
+
label: "xAI Grok (your API key)",
|
|
103
|
+
class: "http",
|
|
104
|
+
cost: "metered",
|
|
105
|
+
adversarialCorpus: "http",
|
|
106
|
+
defaultBaseUrl: "https://api.x.ai/v1"
|
|
107
|
+
}),
|
|
108
|
+
groq: backend({
|
|
109
|
+
id: "groq",
|
|
110
|
+
label: "Groq (your API key)",
|
|
111
|
+
class: "http",
|
|
112
|
+
cost: "metered",
|
|
113
|
+
adversarialCorpus: "http",
|
|
114
|
+
defaultBaseUrl: "https://api.groq.com/openai/v1"
|
|
115
|
+
}),
|
|
116
|
+
openrouter: backend({
|
|
117
|
+
id: "openrouter",
|
|
118
|
+
label: "OpenRouter (your API key)",
|
|
119
|
+
class: "http",
|
|
120
|
+
cost: "metered",
|
|
121
|
+
adversarialCorpus: "http",
|
|
122
|
+
defaultBaseUrl: "https://openrouter.ai/api/v1"
|
|
123
|
+
}),
|
|
124
|
+
together: backend({
|
|
125
|
+
id: "together",
|
|
126
|
+
label: "Together AI (your API key)",
|
|
127
|
+
class: "http",
|
|
128
|
+
cost: "metered",
|
|
129
|
+
adversarialCorpus: "http",
|
|
130
|
+
defaultBaseUrl: "https://api.together.xyz/v1"
|
|
131
|
+
}),
|
|
132
|
+
deepseek: backend({
|
|
133
|
+
id: "deepseek",
|
|
134
|
+
label: "DeepSeek (your API key)",
|
|
135
|
+
class: "http",
|
|
136
|
+
cost: "metered",
|
|
137
|
+
adversarialCorpus: "http",
|
|
138
|
+
defaultBaseUrl: "https://api.deepseek.com/v1"
|
|
139
|
+
}),
|
|
140
|
+
mistral: backend({
|
|
141
|
+
id: "mistral",
|
|
142
|
+
label: "Mistral (your API key)",
|
|
143
|
+
class: "http",
|
|
144
|
+
cost: "metered",
|
|
145
|
+
adversarialCorpus: "http",
|
|
146
|
+
defaultBaseUrl: "https://api.mistral.ai/v1"
|
|
147
|
+
}),
|
|
148
|
+
// -- the escape hatch ----------------------------------------------------
|
|
10
149
|
"openai-http": backend({
|
|
11
150
|
id: "openai-http",
|
|
12
|
-
label: "OpenAI-compatible
|
|
151
|
+
label: "Any OpenAI-compatible server",
|
|
13
152
|
class: "http",
|
|
14
|
-
|
|
153
|
+
// Unknown until the base URL is known: local means free, remote means
|
|
154
|
+
// metered, and the owner does not get to say otherwise
|
|
155
|
+
// ({@link MUSTS.REMOTE_IS_NEVER_FREE}).
|
|
156
|
+
cost: null,
|
|
15
157
|
adversarialCorpus: "http"
|
|
16
158
|
}),
|
|
159
|
+
// -- subscription: someone else's terms ----------------------------------
|
|
17
160
|
"claude-cli": backend({
|
|
18
161
|
id: "claude-cli",
|
|
19
162
|
label: "Claude CLI (your subscription)",
|
|
20
163
|
class: "process",
|
|
21
|
-
|
|
164
|
+
cost: "subscription",
|
|
22
165
|
adversarialCorpus: "process"
|
|
23
166
|
})
|
|
24
167
|
});
|
|
@@ -32,6 +175,30 @@ function isBackendId(value) {
|
|
|
32
175
|
function backendDescriptor(id) {
|
|
33
176
|
return BACKENDS[id];
|
|
34
177
|
}
|
|
178
|
+
function isLocalHost(hostname) {
|
|
179
|
+
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
180
|
+
if (host === "localhost" || host.endsWith(".localhost")) return true;
|
|
181
|
+
const version = isIP(host);
|
|
182
|
+
if (version === 0) return false;
|
|
183
|
+
if (version === 6) {
|
|
184
|
+
if (host === "::1") return true;
|
|
185
|
+
return /^f[cd]/.test(host);
|
|
186
|
+
}
|
|
187
|
+
if (host.startsWith("127.")) return true;
|
|
188
|
+
if (host.startsWith("10.")) return true;
|
|
189
|
+
if (host.startsWith("192.168.")) return true;
|
|
190
|
+
return /^172\.(1[6-9]|2\d|3[01])\./.test(host);
|
|
191
|
+
}
|
|
192
|
+
function resolveCost(id, baseUrl) {
|
|
193
|
+
const declared = BACKENDS[id].cost;
|
|
194
|
+
if (declared !== null) return declared;
|
|
195
|
+
if (baseUrl === void 0) return "metered";
|
|
196
|
+
try {
|
|
197
|
+
return isLocalHost(new URL(baseUrl).hostname) ? "free" : "metered";
|
|
198
|
+
} catch {
|
|
199
|
+
return "metered";
|
|
200
|
+
}
|
|
201
|
+
}
|
|
35
202
|
|
|
36
203
|
// src/audience.ts
|
|
37
204
|
var Audience = z2.enum(["self", "named", "public"]);
|
|
@@ -50,12 +217,18 @@ var MatchRefusal = z2.enum([
|
|
|
50
217
|
/** The backend offers only `self` and the job belongs to someone else. */
|
|
51
218
|
"offer-scope-too-narrow",
|
|
52
219
|
/** The matched backend is subscription-class, which is locked to `self`. */
|
|
53
|
-
"subscription-self-lock"
|
|
220
|
+
"subscription-self-lock",
|
|
221
|
+
/** The backend spends the owner's money and they have not agreed to share it. */
|
|
222
|
+
"metered-no-spend-consent",
|
|
223
|
+
/** The backend is shared but has spent its ceiling for now. */
|
|
224
|
+
"metered-ceiling-reached"
|
|
54
225
|
]);
|
|
55
226
|
var ALLOWED = Object.freeze({ ok: true });
|
|
56
227
|
var refuse = (refusal) => Object.freeze({ ok: false, refusal });
|
|
57
|
-
function effectiveOfferScope(configured,
|
|
58
|
-
|
|
228
|
+
function effectiveOfferScope(configured, cost, spend) {
|
|
229
|
+
if (cost === "subscription") return "self";
|
|
230
|
+
if (cost === "metered" && spend?.acknowledged !== true) return "self";
|
|
231
|
+
return configured;
|
|
59
232
|
}
|
|
60
233
|
function matchAudience(job, daemon) {
|
|
61
234
|
const sameOwner = job.owner === daemon.owner;
|
|
@@ -65,13 +238,25 @@ function matchAudience(job, daemon) {
|
|
|
65
238
|
if (job.audience === "named" && !sameOwner && job.audienceAllow !== void 0 && !job.audienceAllow.includes(daemon.owner)) {
|
|
66
239
|
return refuse("not-in-server-allowlist");
|
|
67
240
|
}
|
|
68
|
-
const scope = effectiveOfferScope(
|
|
241
|
+
const scope = effectiveOfferScope(
|
|
242
|
+
daemon.offerScope,
|
|
243
|
+
daemon.cost,
|
|
244
|
+
daemon.spend
|
|
245
|
+
);
|
|
69
246
|
if (sameOwner) {
|
|
70
247
|
return ALLOWED;
|
|
71
248
|
}
|
|
72
|
-
if (daemon.
|
|
249
|
+
if (daemon.cost === "subscription") {
|
|
73
250
|
return refuse("subscription-self-lock");
|
|
74
251
|
}
|
|
252
|
+
if (daemon.cost === "metered") {
|
|
253
|
+
if (daemon.spend?.acknowledged !== true) {
|
|
254
|
+
return refuse("metered-no-spend-consent");
|
|
255
|
+
}
|
|
256
|
+
if (daemon.spend.ceilingReached === true) {
|
|
257
|
+
return refuse("metered-ceiling-reached");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
75
260
|
switch (scope) {
|
|
76
261
|
case "self":
|
|
77
262
|
return refuse("offer-scope-too-narrow");
|
|
@@ -87,7 +272,9 @@ var REFUSAL_MESSAGES = Object.freeze({
|
|
|
87
272
|
"not-locally-allowed": "the job's owner is not on this machine's allowlist (byollm allow <server> <user>)",
|
|
88
273
|
"not-in-server-allowlist": "the app restricted this job to named runners and this machine is not one of them",
|
|
89
274
|
"offer-scope-too-narrow": "this backend is offered to its owner only (byollm offer <backend> named|public to widen)",
|
|
90
|
-
"subscription-self-lock": "subscription-backed models run their owner's work only \u2014 this is a protocol rule, not a setting"
|
|
275
|
+
"subscription-self-lock": "subscription-backed models run their owner's work only \u2014 this is a protocol rule, not a setting",
|
|
276
|
+
"metered-no-spend-consent": "this backend bills its owner per token, and they have not agreed to spend it on other people's work",
|
|
277
|
+
"metered-ceiling-reached": "this backend is shared but has reached the spend ceiling its owner set"
|
|
91
278
|
});
|
|
92
279
|
|
|
93
280
|
// src/kinds.ts
|
|
@@ -167,6 +354,21 @@ function canTransition(from, to) {
|
|
|
167
354
|
return TRANSITIONS[from].includes(to);
|
|
168
355
|
}
|
|
169
356
|
var Lease = z4.object({
|
|
357
|
+
/**
|
|
358
|
+
* Identifies *this* grant, not just its holder.
|
|
359
|
+
*
|
|
360
|
+
* A runner can hold a job, release it, and claim it again — three leases,
|
|
361
|
+
* one runner id. Without an id for the grant itself, a lease-scoped request
|
|
362
|
+
* names a mutable target ambiguously, and a replayed release from the first
|
|
363
|
+
* grant lands on the third: the job returns to the queue while the daemon
|
|
364
|
+
* is mid-execution, and the work runs twice on the owner's hardware.
|
|
365
|
+
*
|
|
366
|
+
* That was a live hole, found in review after signed requests shipped. The
|
|
367
|
+
* signature scheme's replay argument rests on endpoints being idempotent —
|
|
368
|
+
* and release *is*, per lease, but not across leases, because nothing in
|
|
369
|
+
* the request said which one.
|
|
370
|
+
*/
|
|
371
|
+
id: z4.string().min(1),
|
|
170
372
|
/** The runner holding the lease. */
|
|
171
373
|
runnerId: z4.string().min(1),
|
|
172
374
|
/** Epoch milliseconds after which the claim is void. */
|
|
@@ -180,8 +382,6 @@ var ClaimedJob = z4.object({
|
|
|
180
382
|
audience: Audience,
|
|
181
383
|
/** The app's id for the user who enqueued it. */
|
|
182
384
|
owner: z4.string().min(1),
|
|
183
|
-
/** Runner owners the app restricted a `named` job to, if any. */
|
|
184
|
-
audienceAllow: z4.array(z4.string().min(1)).optional(),
|
|
185
385
|
lease: Lease
|
|
186
386
|
}).strict();
|
|
187
387
|
var ResultProvenance = z4.object({
|
|
@@ -211,6 +411,13 @@ function provenanceFor(input) {
|
|
|
211
411
|
untrusted: input.audience !== "self"
|
|
212
412
|
};
|
|
213
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();
|
|
214
421
|
var JobResultOk = z4.object({
|
|
215
422
|
outcome: z4.literal("ok"),
|
|
216
423
|
text: z4.string(),
|
|
@@ -232,12 +439,398 @@ var JobOutcome = z4.discriminatedUnion("outcome", [
|
|
|
232
439
|
JobResultError,
|
|
233
440
|
JobResultCanceled
|
|
234
441
|
]);
|
|
442
|
+
var SealedOutcome = z4.object({ outcome: JobOutcome, ran: RunMetadata }).strict();
|
|
235
443
|
var DeliveredResult = z4.object({
|
|
236
444
|
jobId: z4.string().min(1),
|
|
237
445
|
state: JobState,
|
|
238
446
|
outcome: JobOutcome.optional(),
|
|
239
447
|
provenance: ResultProvenance.optional()
|
|
240
448
|
}).strict();
|
|
449
|
+
var SizeClass = z4.enum(["small", "medium", "large", "unbounded"]);
|
|
450
|
+
var SIZE_CLASS_LIMITS = Object.freeze({
|
|
451
|
+
small: 4e3,
|
|
452
|
+
medium: 64e3,
|
|
453
|
+
large: Number.POSITIVE_INFINITY
|
|
454
|
+
});
|
|
455
|
+
function sizeClassCeiling(sizeClass) {
|
|
456
|
+
if (sizeClass === "unbounded") return Number.POSITIVE_INFINITY;
|
|
457
|
+
return SIZE_CLASS_LIMITS[sizeClass];
|
|
458
|
+
}
|
|
459
|
+
function sizeClassOf(textChars) {
|
|
460
|
+
if (textChars <= SIZE_CLASS_LIMITS.small) return "small";
|
|
461
|
+
if (textChars <= SIZE_CLASS_LIMITS.medium) return "medium";
|
|
462
|
+
return "large";
|
|
463
|
+
}
|
|
464
|
+
var JobStub = z4.object({
|
|
465
|
+
id: z4.string().min(1),
|
|
466
|
+
kind: JobKind,
|
|
467
|
+
/** The app's id for the user who enqueued it. */
|
|
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),
|
|
493
|
+
audience: Audience,
|
|
494
|
+
// `audienceAllow` is **not** here, and its absence is the enforcement —
|
|
495
|
+
// cloud_008 §0.2.
|
|
496
|
+
//
|
|
497
|
+
// It was a list of the people who may run a job, travelling to every
|
|
498
|
+
// routing party on every `named` job. byollm_001 Rev 1 §B settled who
|
|
499
|
+
// decides that long before this schema existed: *the daemon's own list
|
|
500
|
+
// decides, not the server's*, and `allowlist.predicateFor(origin)` is the
|
|
501
|
+
// enforcement in both lanes. So this was a second answer to a question the
|
|
502
|
+
// daemon already owned — able only to agree, in which case it was
|
|
503
|
+
// redundant, or to disagree, in which case nothing said which wins.
|
|
504
|
+
//
|
|
505
|
+
// The rule it leaves behind, which decides the next field too: **a class
|
|
506
|
+
// the router acts on may travel; membership never does.** `audience` stays
|
|
507
|
+
// for exactly that reason — the relay narrows on it. A roster does not
|
|
508
|
+
// travel, so `ROSTER_NOT_DISCLOSED` holds here by absence, which is the
|
|
509
|
+
// strongest way for a MUST to hold.
|
|
510
|
+
//
|
|
511
|
+
// The site keeps its own copy on `JobRecord` and still filters candidates
|
|
512
|
+
// with it before offering. That is server-internal, where the party
|
|
513
|
+
// holding the list authored it.
|
|
514
|
+
sizeClass: SizeClass,
|
|
515
|
+
/** Reserved for byollm_006. False until streaming exists. */
|
|
516
|
+
streaming: z4.boolean(),
|
|
517
|
+
/** Epoch ms after which the work is pointless; bounds ciphertext retention. */
|
|
518
|
+
deadlineAt: z4.number().int().positive()
|
|
519
|
+
}).strict();
|
|
520
|
+
var ClaimedStub = JobStub.extend({ lease: Lease }).strict();
|
|
521
|
+
|
|
522
|
+
// src/envelope.ts
|
|
523
|
+
import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2 } from "crypto";
|
|
524
|
+
import sodium from "libsodium-wrappers";
|
|
525
|
+
import { z as z6 } from "zod";
|
|
526
|
+
|
|
527
|
+
// src/keys.ts
|
|
528
|
+
import {
|
|
529
|
+
createHash,
|
|
530
|
+
createPrivateKey,
|
|
531
|
+
createPublicKey,
|
|
532
|
+
generateKeyPairSync,
|
|
533
|
+
sign,
|
|
534
|
+
verify
|
|
535
|
+
} from "crypto";
|
|
536
|
+
import { z as z5 } from "zod";
|
|
537
|
+
var PublicIdentity = z5.object({
|
|
538
|
+
/** Raw Ed25519 public key. The pinned one. */
|
|
539
|
+
identity: z5.string().min(1),
|
|
540
|
+
/** Raw X25519 public key, for sealing to this party. */
|
|
541
|
+
encryption: z5.string().min(1),
|
|
542
|
+
/**
|
|
543
|
+
* Ed25519 signature over the encryption key, by the identity key.
|
|
544
|
+
*
|
|
545
|
+
* This is what stops an upstream substituting an encryption key of its
|
|
546
|
+
* own while relaying a genuine identity: the receiver pins the identity
|
|
547
|
+
* and refuses any encryption key not signed by it.
|
|
548
|
+
*/
|
|
549
|
+
encryptionSig: z5.string().min(1)
|
|
550
|
+
}).strict();
|
|
551
|
+
var StoredKeys = z5.object({
|
|
552
|
+
version: z5.literal(1),
|
|
553
|
+
identityPublic: z5.string().min(1),
|
|
554
|
+
identityPrivate: z5.string().min(1),
|
|
555
|
+
encryptionPublic: z5.string().min(1),
|
|
556
|
+
encryptionPrivate: z5.string().min(1),
|
|
557
|
+
encryptionSig: z5.string().min(1),
|
|
558
|
+
createdAt: z5.number().int().positive()
|
|
559
|
+
}).strict();
|
|
560
|
+
var ENCRYPTION_KEY_CONTEXT = "byollm/v1/encryption-key";
|
|
561
|
+
function rawPublic(key) {
|
|
562
|
+
const jwk = key.export({ format: "jwk" });
|
|
563
|
+
const x = jwk.x;
|
|
564
|
+
if (typeof x !== "string") throw new Error("key has no raw public component");
|
|
565
|
+
return x;
|
|
566
|
+
}
|
|
567
|
+
function importPublic(raw, crv) {
|
|
568
|
+
return createPublicKey({ key: { kty: "OKP", crv, x: raw }, format: "jwk" });
|
|
569
|
+
}
|
|
570
|
+
function importPrivate(stored) {
|
|
571
|
+
return createPrivateKey({
|
|
572
|
+
key: Buffer.from(stored, "base64"),
|
|
573
|
+
type: "pkcs8",
|
|
574
|
+
format: "der"
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
var exportPrivate = (key) => key.export({ type: "pkcs8", format: "der" }).toString("base64");
|
|
578
|
+
function generateKeys(now) {
|
|
579
|
+
const identity = generateKeyPairSync("ed25519");
|
|
580
|
+
const encryption = generateKeyPairSync("x25519");
|
|
581
|
+
const encryptionPublic = rawPublic(encryption.publicKey);
|
|
582
|
+
return {
|
|
583
|
+
version: 1,
|
|
584
|
+
identityPublic: rawPublic(identity.publicKey),
|
|
585
|
+
identityPrivate: exportPrivate(identity.privateKey),
|
|
586
|
+
encryptionPublic,
|
|
587
|
+
encryptionPrivate: exportPrivate(encryption.privateKey),
|
|
588
|
+
encryptionSig: sign(
|
|
589
|
+
null,
|
|
590
|
+
Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${encryptionPublic}`),
|
|
591
|
+
identity.privateKey
|
|
592
|
+
).toString("base64url"),
|
|
593
|
+
createdAt: now
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
function publicIdentityOf(keys) {
|
|
597
|
+
return {
|
|
598
|
+
identity: keys.identityPublic,
|
|
599
|
+
encryption: keys.encryptionPublic,
|
|
600
|
+
encryptionSig: keys.encryptionSig
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
function verifyPublicIdentity(identity) {
|
|
604
|
+
try {
|
|
605
|
+
return verify(
|
|
606
|
+
null,
|
|
607
|
+
Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${identity.encryption}`),
|
|
608
|
+
importPublic(identity.identity, "Ed25519"),
|
|
609
|
+
Buffer.from(identity.encryptionSig, "base64url")
|
|
610
|
+
);
|
|
611
|
+
} catch {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function signWith(keys, data) {
|
|
616
|
+
return sign(null, data, importPrivate(keys.identityPrivate)).toString(
|
|
617
|
+
"base64url"
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
function verifyWith(identityPublic, data, signature) {
|
|
621
|
+
try {
|
|
622
|
+
return verify(
|
|
623
|
+
null,
|
|
624
|
+
data,
|
|
625
|
+
importPublic(identityPublic, "Ed25519"),
|
|
626
|
+
Buffer.from(signature, "base64url")
|
|
627
|
+
);
|
|
628
|
+
} catch {
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
633
|
+
function fingerprint(identityPublic) {
|
|
634
|
+
const digest = createHash("sha256").update(Buffer.from(identityPublic, "base64url")).digest();
|
|
635
|
+
let bits = 0;
|
|
636
|
+
let value = 0;
|
|
637
|
+
let out = "";
|
|
638
|
+
for (const byte of digest.subarray(0, 15)) {
|
|
639
|
+
value = value << 8 | byte;
|
|
640
|
+
bits += 8;
|
|
641
|
+
while (bits >= 5) {
|
|
642
|
+
out += ALPHABET.charAt(value >>> bits - 5 & 31);
|
|
643
|
+
bits -= 5;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const groups = out.match(/.{1,4}/g) ?? [];
|
|
647
|
+
return `BYOLLM-${groups.join("-")}`;
|
|
648
|
+
}
|
|
649
|
+
var keyId = (identityPublic) => fingerprint(identityPublic);
|
|
650
|
+
|
|
651
|
+
// src/envelope.ts
|
|
652
|
+
var readied;
|
|
653
|
+
async function cryptoReady() {
|
|
654
|
+
readied ??= sodium.ready;
|
|
655
|
+
await readied;
|
|
656
|
+
}
|
|
657
|
+
var ENVELOPE_MAX_AGE_MS = 24 * 60 * 6e4;
|
|
658
|
+
var EnvelopeDirection = z6.enum(["payload", "result"]);
|
|
659
|
+
var SealedEnvelope = z6.object({
|
|
660
|
+
/** Base64url `crypto_box_seal` output over the signed plaintext. */
|
|
661
|
+
ciphertext: z6.string().min(1),
|
|
662
|
+
/** Who this was sealed to — the recipient checks it is them. */
|
|
663
|
+
recipientKeyId: z6.string().min(1),
|
|
664
|
+
/** Who signed it — the recipient checks this against its pin. */
|
|
665
|
+
senderKeyId: z6.string().min(1),
|
|
666
|
+
direction: EnvelopeDirection,
|
|
667
|
+
/**
|
|
668
|
+
* When this ciphertext stops being worth keeping.
|
|
669
|
+
*
|
|
670
|
+
* Carried *on* the envelope rather than recomputed by the opener. An
|
|
671
|
+
* earlier version derived it from the job's creation time, which meant
|
|
672
|
+
* two systems had to agree on a timestamp to the millisecond — and they
|
|
673
|
+
* did not, once a real database rounded it. A bound value that has to be
|
|
674
|
+
* reconstructed is a bound value that eventually is not.
|
|
675
|
+
*
|
|
676
|
+
* Not trusted as written: it is also inside the signature, so a changed
|
|
677
|
+
* deadline fails to verify.
|
|
678
|
+
*/
|
|
679
|
+
deadlineAt: z6.number().int().positive()
|
|
680
|
+
}).strict();
|
|
681
|
+
function signedBody(context, plaintext) {
|
|
682
|
+
return Buffer.from(
|
|
683
|
+
JSON.stringify({
|
|
684
|
+
v: "byollm/v1/envelope",
|
|
685
|
+
jobId: context.jobId,
|
|
686
|
+
senderKeyId: context.senderKeyId,
|
|
687
|
+
recipientKeyId: context.recipientKeyId,
|
|
688
|
+
deadlineAt: context.deadlineAt,
|
|
689
|
+
direction: context.direction,
|
|
690
|
+
plaintext
|
|
691
|
+
}),
|
|
692
|
+
"utf8"
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
var rawX25519 = (key, part) => {
|
|
696
|
+
const jwk = key.export({ format: "jwk" });
|
|
697
|
+
const value = part === "x" ? jwk.x : jwk.d;
|
|
698
|
+
if (typeof value !== "string") throw new Error("not an X25519 key");
|
|
699
|
+
return new Uint8Array(Buffer.from(value, "base64url"));
|
|
700
|
+
};
|
|
701
|
+
async function seal(input) {
|
|
702
|
+
await cryptoReady();
|
|
703
|
+
const body = signedBody(input.context, input.plaintext);
|
|
704
|
+
const signature = signWith(input.senderKeys, body);
|
|
705
|
+
const inner = JSON.stringify({ body: body.toString("base64url"), signature });
|
|
706
|
+
const recipient = new Uint8Array(
|
|
707
|
+
Buffer.from(input.recipientEncryptionPublic, "base64url")
|
|
708
|
+
);
|
|
709
|
+
const ciphertext = sodium.crypto_box_seal(
|
|
710
|
+
new Uint8Array(Buffer.from(inner, "utf8")),
|
|
711
|
+
recipient
|
|
712
|
+
);
|
|
713
|
+
return {
|
|
714
|
+
ciphertext: Buffer.from(ciphertext).toString("base64url"),
|
|
715
|
+
recipientKeyId: input.context.recipientKeyId,
|
|
716
|
+
senderKeyId: input.context.senderKeyId,
|
|
717
|
+
direction: input.context.direction,
|
|
718
|
+
deadlineAt: input.context.deadlineAt
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
async function open(input) {
|
|
722
|
+
await cryptoReady();
|
|
723
|
+
const { envelope, expected } = input;
|
|
724
|
+
if (envelope.recipientKeyId !== expected.recipientKeyId || envelope.senderKeyId !== expected.senderKeyId || envelope.direction !== expected.direction) {
|
|
725
|
+
return { ok: false, reason: "not-for-us" };
|
|
726
|
+
}
|
|
727
|
+
let inner;
|
|
728
|
+
try {
|
|
729
|
+
const priv = createPrivateKey2({
|
|
730
|
+
key: Buffer.from(input.recipientKeys.encryptionPrivate, "base64"),
|
|
731
|
+
type: "pkcs8",
|
|
732
|
+
format: "der"
|
|
733
|
+
});
|
|
734
|
+
const pub = createPublicKey2(priv);
|
|
735
|
+
const opened = sodium.crypto_box_seal_open(
|
|
736
|
+
new Uint8Array(Buffer.from(envelope.ciphertext, "base64url")),
|
|
737
|
+
rawX25519(pub, "x"),
|
|
738
|
+
rawX25519(priv, "d")
|
|
739
|
+
);
|
|
740
|
+
inner = Buffer.from(opened).toString("utf8");
|
|
741
|
+
} catch {
|
|
742
|
+
return { ok: false, reason: "unopenable" };
|
|
743
|
+
}
|
|
744
|
+
let parsed;
|
|
745
|
+
try {
|
|
746
|
+
parsed = JSON.parse(inner);
|
|
747
|
+
} catch {
|
|
748
|
+
return { ok: false, reason: "malformed" };
|
|
749
|
+
}
|
|
750
|
+
if (typeof parsed.body !== "string" || typeof parsed.signature !== "string") {
|
|
751
|
+
return { ok: false, reason: "malformed" };
|
|
752
|
+
}
|
|
753
|
+
const body = Buffer.from(parsed.body, "base64url");
|
|
754
|
+
if (!verifyWith(input.senderIdentityPublic, body, parsed.signature)) {
|
|
755
|
+
return { ok: false, reason: "bad-signature" };
|
|
756
|
+
}
|
|
757
|
+
let claims;
|
|
758
|
+
try {
|
|
759
|
+
claims = JSON.parse(body.toString("utf8"));
|
|
760
|
+
} catch {
|
|
761
|
+
return { ok: false, reason: "malformed" };
|
|
762
|
+
}
|
|
763
|
+
if (claims["jobId"] !== expected.jobId || claims["senderKeyId"] !== expected.senderKeyId || claims["recipientKeyId"] !== expected.recipientKeyId || claims["deadlineAt"] !== envelope.deadlineAt || claims["direction"] !== expected.direction) {
|
|
764
|
+
return { ok: false, reason: "context-mismatch" };
|
|
765
|
+
}
|
|
766
|
+
if (typeof claims["plaintext"] !== "string") {
|
|
767
|
+
return { ok: false, reason: "malformed" };
|
|
768
|
+
}
|
|
769
|
+
return { ok: true, plaintext: claims["plaintext"] };
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// src/signing.ts
|
|
773
|
+
import { createHash as createHash2 } from "crypto";
|
|
774
|
+
import { z as z7 } from "zod";
|
|
775
|
+
var MAX_CLOCK_SKEW_MS = 12e4;
|
|
776
|
+
var RequestSignature = z7.object({
|
|
777
|
+
/** Which runner is calling. The server looks up its pinned identity. */
|
|
778
|
+
runnerId: z7.string().min(1),
|
|
779
|
+
/** Epoch ms, bounded by {@link MAX_CLOCK_SKEW_MS}. */
|
|
780
|
+
issuedAt: z7.number().int().positive(),
|
|
781
|
+
/** Base64url Ed25519 signature over {@link canonicalRequest}. */
|
|
782
|
+
signature: z7.string().min(1)
|
|
783
|
+
}).strict();
|
|
784
|
+
function canonicalRequest(input) {
|
|
785
|
+
const digest = createHash2("sha256").update(input.body, "utf8").digest("hex");
|
|
786
|
+
return Buffer.from(
|
|
787
|
+
[
|
|
788
|
+
"byollm/v1/request",
|
|
789
|
+
input.endpoint,
|
|
790
|
+
input.runnerId,
|
|
791
|
+
String(input.issuedAt),
|
|
792
|
+
digest
|
|
793
|
+
].join("\n"),
|
|
794
|
+
"utf8"
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
function signRequest(keys, input) {
|
|
798
|
+
return {
|
|
799
|
+
runnerId: input.runnerId,
|
|
800
|
+
issuedAt: input.issuedAt,
|
|
801
|
+
signature: signWith(keys, canonicalRequest(input))
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
function signSiteRequest(keys, input) {
|
|
805
|
+
return signRequest(keys, {
|
|
806
|
+
endpoint: siteEndpoint(input.endpoint),
|
|
807
|
+
runnerId: input.siteId,
|
|
808
|
+
issuedAt: input.issuedAt,
|
|
809
|
+
body: input.body
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
function verifySiteRequest(input) {
|
|
813
|
+
return verifyRequest({
|
|
814
|
+
...input,
|
|
815
|
+
endpoint: siteEndpoint(input.endpoint)
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
var siteEndpoint = (endpoint) => `site/${endpoint}`;
|
|
819
|
+
function verifyRequest(input) {
|
|
820
|
+
const skew = input.maxSkewMs ?? MAX_CLOCK_SKEW_MS;
|
|
821
|
+
if (Math.abs(input.now - input.signature.issuedAt) > skew) return "stale";
|
|
822
|
+
const ok = verifyWith(
|
|
823
|
+
input.identityPublic,
|
|
824
|
+
canonicalRequest({
|
|
825
|
+
endpoint: input.endpoint,
|
|
826
|
+
runnerId: input.signature.runnerId,
|
|
827
|
+
issuedAt: input.signature.issuedAt,
|
|
828
|
+
body: input.body
|
|
829
|
+
}),
|
|
830
|
+
input.signature.signature
|
|
831
|
+
);
|
|
832
|
+
return ok ? null : "bad-signature";
|
|
833
|
+
}
|
|
241
834
|
|
|
242
835
|
// src/musts.ts
|
|
243
836
|
var must = (m) => Object.freeze(m);
|
|
@@ -247,31 +840,78 @@ var MUSTS = Object.freeze({
|
|
|
247
840
|
id: "PAIR_ONE_USER",
|
|
248
841
|
statement: "A runner token MUST be bound to exactly one user; a daemon MUST refuse work not attributable to its paired user.",
|
|
249
842
|
enforcedBy: "both",
|
|
843
|
+
verifiedBy: "conformance",
|
|
250
844
|
source: "byollm_001 \xA7MUSTs"
|
|
251
845
|
}),
|
|
252
846
|
PAIR_INTERACTIVE: must({
|
|
253
847
|
id: "PAIR_INTERACTIVE",
|
|
254
848
|
statement: "Pairing MUST be interactive (device-code approval in the app's own session); a long-lived pasted secret MUST NOT be accepted as pairing.",
|
|
255
849
|
enforcedBy: "server",
|
|
850
|
+
verifiedBy: "conformance",
|
|
256
851
|
source: "byollm_001 \xA7Endpoints.1"
|
|
257
852
|
}),
|
|
258
853
|
PAIR_CODE_EXPIRES: must({
|
|
259
854
|
id: "PAIR_CODE_EXPIRES",
|
|
260
855
|
statement: "An unapproved device code MUST expire and MUST NOT be redeemable after expiry.",
|
|
261
856
|
enforcedBy: "server",
|
|
857
|
+
verifiedBy: "conformance",
|
|
262
858
|
source: "byollm_001 \xA7Endpoints.1"
|
|
263
859
|
}),
|
|
264
860
|
// ---- Typed job kinds --------------------------------------------------
|
|
861
|
+
VERSION_HANDSHAKE_REQUIRED: must({
|
|
862
|
+
id: "VERSION_HANDSHAKE_REQUIRED",
|
|
863
|
+
statement: "Every protocol request MUST declare a protocol version, and a server MUST refuse an absent or unsupported one with a structured error naming what it supports \u2014 never a generic parse failure.",
|
|
864
|
+
enforcedBy: "both",
|
|
865
|
+
verifiedBy: "conformance",
|
|
866
|
+
source: "byollm_009 \xA74"
|
|
867
|
+
}),
|
|
868
|
+
KEYS_EXCHANGED_AT_CONSENT: must({
|
|
869
|
+
id: "KEYS_EXCHANGED_AT_CONSENT",
|
|
870
|
+
statement: "Pairing MUST exchange both parties' public identities; each side MUST verify that the encryption key is signed by the identity presenting it, and MUST pin the identity. Keys MUST NOT be delivered before approval.",
|
|
871
|
+
enforcedBy: "both",
|
|
872
|
+
verifiedBy: "conformance",
|
|
873
|
+
source: "byollm_009 \xA75"
|
|
874
|
+
}),
|
|
875
|
+
REQUESTS_SIGNED_NOT_BEARER: must({
|
|
876
|
+
id: "REQUESTS_SIGNED_NOT_BEARER",
|
|
877
|
+
statement: "Every authenticated request MUST be signed by the calling device's pinned identity key, over the endpoint, the runner id, a timestamp and the exact request body. A server MUST NOT accept a bearer credential in place of a signature.",
|
|
878
|
+
enforcedBy: "both",
|
|
879
|
+
verifiedBy: "conformance",
|
|
880
|
+
source: "byollm_009 \xA74.2"
|
|
881
|
+
}),
|
|
882
|
+
LEASE_SCOPED_BY_GRANT: must({
|
|
883
|
+
id: "LEASE_SCOPED_BY_GRANT",
|
|
884
|
+
statement: "A lease-scoped request MUST name the lease it acts on, and a server MUST apply it only to that lease. Naming the job and the runner is not sufficient: both survive a claim-release-reclaim cycle.",
|
|
885
|
+
enforcedBy: "both",
|
|
886
|
+
verifiedBy: "conformance",
|
|
887
|
+
source: "byollm_009 \xA74.2"
|
|
888
|
+
}),
|
|
889
|
+
STUB_METADATA_EXHAUSTIVE: must({
|
|
890
|
+
id: "STUB_METADATA_EXHAUSTIVE",
|
|
891
|
+
statement: "A claim MUST answer with stubs carrying exactly the enumerated fields and no payload. An endpoint MUST NOT emit a stub carrying others, and an upstream MUST NOT require any.",
|
|
892
|
+
enforcedBy: "both",
|
|
893
|
+
verifiedBy: "conformance",
|
|
894
|
+
source: "byollm_009 \xA76"
|
|
895
|
+
}),
|
|
896
|
+
ENVELOPE_SEALED_AND_SIGNED: must({
|
|
897
|
+
id: "ENVELOPE_SEALED_AND_SIGNED",
|
|
898
|
+
statement: "A stored payload MUST be sealed, and MUST be signed by the sender's identity key. An endpoint MUST refuse an envelope whose signature does not verify against the identity it pinned.",
|
|
899
|
+
enforcedBy: "server",
|
|
900
|
+
verifiedBy: "conformance",
|
|
901
|
+
source: "byollm_009 \xA76"
|
|
902
|
+
}),
|
|
265
903
|
KIND_TYPED_ONLY: must({
|
|
266
904
|
id: "KIND_TYPED_ONLY",
|
|
267
905
|
statement: "Job kinds MUST resolve against handlers baked into the daemon. A daemon MUST refuse an unknown kind rather than guess.",
|
|
268
906
|
enforcedBy: "daemon",
|
|
907
|
+
verifiedBy: "conformance",
|
|
269
908
|
source: "byollm_001 \xA7Jobs are typed data"
|
|
270
909
|
}),
|
|
271
910
|
KIND_NO_CODE: must({
|
|
272
911
|
id: "KIND_NO_CODE",
|
|
273
912
|
statement: "A server MUST NOT be able to convey code, a shell string, or a path to execute; payloads are data handed to a model only.",
|
|
274
913
|
enforcedBy: "daemon",
|
|
914
|
+
verifiedBy: "conformance",
|
|
275
915
|
source: "byollm_001 \xA7Jobs are typed data; byollm_004 \xA71"
|
|
276
916
|
}),
|
|
277
917
|
// ---- Capability and claiming -----------------------------------------
|
|
@@ -279,18 +919,21 @@ var MUSTS = Object.freeze({
|
|
|
279
919
|
id: "CLAIM_REQUIRES_CAPABILITY",
|
|
280
920
|
statement: "A daemon MUST NOT be given a job whose kind is absent from its advertised capability matrix.",
|
|
281
921
|
enforcedBy: "both",
|
|
922
|
+
verifiedBy: "conformance",
|
|
282
923
|
source: "byollm_001 \xA7MUSTs"
|
|
283
924
|
}),
|
|
284
925
|
CAPABILITY_IS_DETECTED: must({
|
|
285
926
|
id: "CAPABILITY_IS_DETECTED",
|
|
286
927
|
statement: "An advertised capability matrix MUST be the intersection of owner config and detected, healthy reality \u2014 never config alone.",
|
|
287
928
|
enforcedBy: "daemon",
|
|
929
|
+
verifiedBy: "conformance",
|
|
288
930
|
source: "byollm_002 \xA7Routing"
|
|
289
931
|
}),
|
|
290
932
|
CLAIM_ATOMIC: must({
|
|
291
933
|
id: "CLAIM_ATOMIC",
|
|
292
934
|
statement: "Claiming MUST be atomic: a job MUST NOT be handed to two runners concurrently.",
|
|
293
935
|
enforcedBy: "server",
|
|
936
|
+
verifiedBy: "conformance",
|
|
294
937
|
source: "byollm_001 \xA7Endpoints.2"
|
|
295
938
|
}),
|
|
296
939
|
// ---- Leases -----------------------------------------------------------
|
|
@@ -298,12 +941,14 @@ var MUSTS = Object.freeze({
|
|
|
298
941
|
id: "LEASE_HONORED",
|
|
299
942
|
statement: "A daemon MUST stop work on a job whose lease it has failed to renew, and MUST NOT report a result for an expired lease it no longer holds.",
|
|
300
943
|
enforcedBy: "daemon",
|
|
944
|
+
verifiedBy: "conformance",
|
|
301
945
|
source: "byollm_001 \xA7MUSTs"
|
|
302
946
|
}),
|
|
303
947
|
LEASE_RECLAIMABLE: must({
|
|
304
948
|
id: "LEASE_RECLAIMABLE",
|
|
305
949
|
statement: "A lease that expires un-renewed MUST make its job claimable again with no loss of the job.",
|
|
306
950
|
enforcedBy: "server",
|
|
951
|
+
verifiedBy: "conformance",
|
|
307
952
|
source: "byollm_001 \xA7Endpoints.2"
|
|
308
953
|
}),
|
|
309
954
|
// ---- Audience and offer scope ----------------------------------------
|
|
@@ -311,24 +956,56 @@ var MUSTS = Object.freeze({
|
|
|
311
956
|
id: "AUDIENCE_BOTH_SIDES",
|
|
312
957
|
statement: "A job MUST run on a daemon only if the daemon's offer scope admits the job's owner AND the job's audience admits the daemon's owner.",
|
|
313
958
|
enforcedBy: "both",
|
|
959
|
+
verifiedBy: "conformance",
|
|
314
960
|
source: "byollm_001 \xA7The audience model"
|
|
315
961
|
}),
|
|
316
962
|
SUBSCRIPTION_SELF_LOCK: must({
|
|
317
963
|
id: "SUBSCRIPTION_SELF_LOCK",
|
|
318
964
|
statement: "A subscription-class backend's offer scope MUST be 'self' and MUST NOT be widened by configuration.",
|
|
319
965
|
enforcedBy: "daemon",
|
|
966
|
+
verifiedBy: "conformance",
|
|
320
967
|
source: "byollm_001 \xA7The audience model"
|
|
321
968
|
}),
|
|
969
|
+
METERED_DEFAULTS_SELF: must({
|
|
970
|
+
id: "METERED_DEFAULTS_SELF",
|
|
971
|
+
statement: "A metered backend's effective offer scope MUST be 'self' unless the owner has explicitly acknowledged spending money on others' work.",
|
|
972
|
+
enforcedBy: "daemon",
|
|
973
|
+
verifiedBy: "conformance",
|
|
974
|
+
source: "byollm_007 \xA74"
|
|
975
|
+
}),
|
|
976
|
+
METERED_REQUIRES_CEILING: must({
|
|
977
|
+
id: "METERED_REQUIRES_CEILING",
|
|
978
|
+
statement: "A widened metered backend MUST carry a spend ceiling, and the daemon MUST refuse community work once it is reached.",
|
|
979
|
+
enforcedBy: "daemon",
|
|
980
|
+
verifiedBy: "conformance",
|
|
981
|
+
source: "byollm_007 \xA74"
|
|
982
|
+
}),
|
|
983
|
+
COST_NOT_CONFIGURABLE: must({
|
|
984
|
+
id: "COST_NOT_CONFIGURABLE",
|
|
985
|
+
statement: "A built-in provider's cost class MUST NOT be overridable by configuration.",
|
|
986
|
+
enforcedBy: "daemon",
|
|
987
|
+
verifiedBy: "conformance",
|
|
988
|
+
source: "byollm_007 \xA72"
|
|
989
|
+
}),
|
|
990
|
+
REMOTE_IS_NEVER_FREE: must({
|
|
991
|
+
id: "REMOTE_IS_NEVER_FREE",
|
|
992
|
+
statement: "A generic HTTP backend whose base URL is not loopback or private MUST be treated as metered.",
|
|
993
|
+
enforcedBy: "daemon",
|
|
994
|
+
verifiedBy: "conformance",
|
|
995
|
+
source: "byollm_007 \xA72"
|
|
996
|
+
}),
|
|
322
997
|
NAMED_LOCAL_ALLOWLIST: must({
|
|
323
998
|
id: "NAMED_LOCAL_ALLOWLIST",
|
|
324
999
|
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.",
|
|
325
1000
|
enforcedBy: "daemon",
|
|
1001
|
+
verifiedBy: "conformance",
|
|
326
1002
|
source: "byollm_001 Rev 1 \xA7B"
|
|
327
1003
|
}),
|
|
328
1004
|
REFUSAL_NOT_REOFFERED: must({
|
|
329
1005
|
id: "REFUSAL_NOT_REOFFERED",
|
|
330
1006
|
statement: "A server MUST NOT re-offer a job to a runner that released it with reason 'refused'.",
|
|
331
1007
|
enforcedBy: "server",
|
|
1008
|
+
verifiedBy: "conformance",
|
|
332
1009
|
source: "byollm_001 Rev 1 \xA7B (loop resolved in build review)"
|
|
333
1010
|
}),
|
|
334
1011
|
// ---- Revocation and cancel -------------------------------------------
|
|
@@ -336,12 +1013,14 @@ var MUSTS = Object.freeze({
|
|
|
336
1013
|
id: "REVOCATION_HONORED",
|
|
337
1014
|
statement: "A revoked daemon MUST stop claiming and MUST abandon in-flight work by the next heartbeat at the latest.",
|
|
338
1015
|
enforcedBy: "daemon",
|
|
1016
|
+
verifiedBy: "conformance",
|
|
339
1017
|
source: "byollm_001 \xA7MUSTs"
|
|
340
1018
|
}),
|
|
341
1019
|
CANCEL_HONORED: must({
|
|
342
1020
|
id: "CANCEL_HONORED",
|
|
343
1021
|
statement: "A job id in a heartbeat response's cancel list MUST abort that job's in-flight backend call and be reported as 'canceled'.",
|
|
344
1022
|
enforcedBy: "daemon",
|
|
1023
|
+
verifiedBy: "conformance",
|
|
345
1024
|
source: "byollm_001 Rev 1 \xA7C"
|
|
346
1025
|
}),
|
|
347
1026
|
// ---- Lifecycle, dependencies, delivery -------------------------------
|
|
@@ -349,37 +1028,43 @@ var MUSTS = Object.freeze({
|
|
|
349
1028
|
id: "DEPENDS_ON_GATING",
|
|
350
1029
|
statement: "A job MUST NOT be claimable until every job in its dependsOn set has reached the 'ok' state.",
|
|
351
1030
|
enforcedBy: "server",
|
|
1031
|
+
verifiedBy: "conformance",
|
|
352
1032
|
source: "byollm_001 Rev 1 \xA7E"
|
|
353
1033
|
}),
|
|
354
1034
|
TTL_EXPIRY: must({
|
|
355
1035
|
id: "TTL_EXPIRY",
|
|
356
1036
|
statement: "An unclaimed job MUST become 'expired' once its TTL elapses, and the TTL clock MUST start when the job becomes claimable, not at enqueue.",
|
|
357
1037
|
enforcedBy: "server",
|
|
1038
|
+
verifiedBy: "conformance",
|
|
358
1039
|
source: "byollm_001 Rev 1 \xA7D (TTL clock resolved in build review)"
|
|
359
1040
|
}),
|
|
360
1041
|
NO_RUNNER_SIGNAL: must({
|
|
361
1042
|
id: "NO_RUNNER_SIGNAL",
|
|
362
1043
|
statement: "A server MUST surface noRunnerAvailable when no runner with matching capability has heartbeated within the liveness window, and MUST NOT raise it for a job still blocked on dependencies.",
|
|
363
1044
|
enforcedBy: "server",
|
|
1045
|
+
verifiedBy: "conformance",
|
|
364
1046
|
source: "byollm_001 Rev 1 \xA7D"
|
|
365
1047
|
}),
|
|
366
1048
|
RESULT_IDEMPOTENT: must({
|
|
367
1049
|
id: "RESULT_IDEMPOTENT",
|
|
368
1050
|
statement: "Result submission MUST be idempotent by job id; the first terminal outcome wins and later submissions MUST NOT change it.",
|
|
369
1051
|
enforcedBy: "server",
|
|
1052
|
+
verifiedBy: "conformance",
|
|
370
1053
|
source: "byollm_001 \xA7Endpoints.4"
|
|
371
1054
|
}),
|
|
372
|
-
|
|
373
|
-
id: "
|
|
374
|
-
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.",
|
|
375
1058
|
enforcedBy: "server",
|
|
376
|
-
|
|
1059
|
+
verifiedBy: "conformance",
|
|
1060
|
+
source: "byollm_009 \xA711"
|
|
377
1061
|
}),
|
|
378
1062
|
// ---- The trust surface -------------------------------------------------
|
|
379
1063
|
INGRESS_LOGGED_BEFORE_EXECUTION: must({
|
|
380
1064
|
id: "INGRESS_LOGGED_BEFORE_EXECUTION",
|
|
381
1065
|
statement: "Every executed prompt MUST be appended to the local ingress log before execution begins.",
|
|
382
1066
|
enforcedBy: "daemon",
|
|
1067
|
+
verifiedBy: "conformance",
|
|
383
1068
|
source: "byollm_001 \xA7MUSTs"
|
|
384
1069
|
}),
|
|
385
1070
|
// ---- Execution isolation (byollm_004) ---------------------------------
|
|
@@ -387,178 +1072,420 @@ var MUSTS = Object.freeze({
|
|
|
387
1072
|
id: "NO_SHELL_INTERPOLATION",
|
|
388
1073
|
statement: "Process-class backends MUST be invoked with a fixed argv array and the payload delivered on stdin; payload text MUST NOT reach a command line.",
|
|
389
1074
|
enforcedBy: "daemon",
|
|
1075
|
+
verifiedBy: "adversarial",
|
|
390
1076
|
source: "byollm_004 \xA72"
|
|
391
1077
|
}),
|
|
392
1078
|
NO_PAYLOAD_ROUTING: must({
|
|
393
1079
|
id: "NO_PAYLOAD_ROUTING",
|
|
394
1080
|
statement: "Model, backend, base URL, and flags MUST come from owner config only; a payload MUST NOT influence any of them.",
|
|
395
1081
|
enforcedBy: "daemon",
|
|
1082
|
+
verifiedBy: "adversarial",
|
|
396
1083
|
source: "byollm_004 \xA72"
|
|
397
1084
|
}),
|
|
398
1085
|
STRIPPED_CHILD_ENV: must({
|
|
399
1086
|
id: "STRIPPED_CHILD_ENV",
|
|
400
1087
|
statement: "Process-class children MUST spawn with an allowlisted environment, a scratch cwd, no inherited descriptors beyond std streams, and hard timeout and output-size caps.",
|
|
401
1088
|
enforcedBy: "daemon",
|
|
1089
|
+
verifiedBy: "adversarial",
|
|
402
1090
|
source: "byollm_004 \xA72"
|
|
403
1091
|
}),
|
|
404
1092
|
HTTP_BASE_URL_SAFE: must({
|
|
405
1093
|
id: "HTTP_BASE_URL_SAFE",
|
|
406
1094
|
statement: "HTTP-class backends MUST send requests only to the owner-configured base URL and MUST refuse base URLs resolving to cloud-metadata or link-local addresses.",
|
|
407
1095
|
enforcedBy: "daemon",
|
|
1096
|
+
verifiedBy: "adversarial",
|
|
408
1097
|
source: "byollm_004 Rev 1 \xA7Backend taxonomy"
|
|
409
1098
|
}),
|
|
410
1099
|
OUTPUT_INERT: must({
|
|
411
1100
|
id: "OUTPUT_INERT",
|
|
412
1101
|
statement: "Returned text MUST be treated as inert bytes: never evaluated, never written to a payload-named path, never interpolated into a shell or into terminal control sequences when logged.",
|
|
413
1102
|
enforcedBy: "daemon",
|
|
1103
|
+
verifiedBy: "adversarial",
|
|
414
1104
|
source: "byollm_004 \xA72"
|
|
415
1105
|
}),
|
|
416
1106
|
COMMUNITY_BUDGETS: must({
|
|
417
1107
|
id: "COMMUNITY_BUDGETS",
|
|
418
1108
|
statement: "Jobs whose owner is not the daemon's owner MUST be subject to the owner's rate limits, daily cap, and resource budget.",
|
|
419
1109
|
enforcedBy: "daemon",
|
|
1110
|
+
verifiedBy: "adversarial",
|
|
420
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"
|
|
421
1190
|
})
|
|
422
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
|
+
});
|
|
423
1198
|
var MUST_IDS = Object.freeze(Object.keys(MUSTS));
|
|
1199
|
+
function mustsVerifiedBy(kind) {
|
|
1200
|
+
return MUST_IDS.filter((id) => MUSTS[id].verifiedBy === kind);
|
|
1201
|
+
}
|
|
424
1202
|
|
|
425
1203
|
// src/wire.ts
|
|
426
|
-
import { z as
|
|
1204
|
+
import { z as z8 } from "zod";
|
|
427
1205
|
var PROTOCOL_VERSION = "0";
|
|
1206
|
+
var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze([
|
|
1207
|
+
PROTOCOL_VERSION
|
|
1208
|
+
]);
|
|
1209
|
+
var MIN_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0] ?? PROTOCOL_VERSION;
|
|
1210
|
+
function checkProtocolVersion(body) {
|
|
1211
|
+
const declared = typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion") ? body.protocolVersion : void 0;
|
|
1212
|
+
if (typeof declared !== "string" || declared.length === 0) {
|
|
1213
|
+
return {
|
|
1214
|
+
error: "unsupported-protocol-version",
|
|
1215
|
+
message: `this request declared no protocol version. Upgrade the daemon: \`${UPGRADE_COMMAND}\`.`,
|
|
1216
|
+
supported: SUPPORTED_PROTOCOL_VERSIONS,
|
|
1217
|
+
minimum: MIN_PROTOCOL_VERSION
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
if (!SUPPORTED_PROTOCOL_VERSIONS.includes(declared)) {
|
|
1221
|
+
return {
|
|
1222
|
+
error: "unsupported-protocol-version",
|
|
1223
|
+
message: `this server speaks protocol ${SUPPORTED_PROTOCOL_VERSIONS.join(", ")} and the daemon asked for ${declared}. ` + (declared < MIN_PROTOCOL_VERSION ? `Upgrade the daemon: \`${UPGRADE_COMMAND}\`.` : "This daemon is newer than the server; the server needs upgrading."),
|
|
1224
|
+
supported: SUPPORTED_PROTOCOL_VERSIONS,
|
|
1225
|
+
minimum: MIN_PROTOCOL_VERSION
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
return null;
|
|
1229
|
+
}
|
|
1230
|
+
var UPGRADE_COMMAND = "npm i -g byollm@latest";
|
|
428
1231
|
var PROTOCOL_PREFIX = "/byollm";
|
|
429
1232
|
var ENDPOINTS = Object.freeze([
|
|
430
1233
|
"pair",
|
|
431
1234
|
"claim",
|
|
1235
|
+
"fetch",
|
|
432
1236
|
"heartbeat",
|
|
433
1237
|
"result",
|
|
434
1238
|
"release"
|
|
435
1239
|
]);
|
|
436
|
-
var Capability =
|
|
1240
|
+
var Capability = z8.object({
|
|
437
1241
|
kind: JobKind,
|
|
438
1242
|
backendId: BackendIdSchema,
|
|
439
1243
|
backendClass: BackendClass,
|
|
440
|
-
model:
|
|
1244
|
+
model: z8.string().min(1),
|
|
441
1245
|
offerScope: OfferScope
|
|
442
1246
|
}).strict();
|
|
443
|
-
var CapabilityMatrix =
|
|
444
|
-
var PairStartRequest =
|
|
445
|
-
protocolVersion:
|
|
446
|
-
action:
|
|
447
|
-
daemon:
|
|
448
|
-
version:
|
|
1247
|
+
var CapabilityMatrix = z8.array(Capability);
|
|
1248
|
+
var PairStartRequest = z8.object({
|
|
1249
|
+
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1250
|
+
action: z8.literal("start"),
|
|
1251
|
+
daemon: z8.object({
|
|
1252
|
+
version: z8.string().min(1),
|
|
449
1253
|
/** Shown in the app's runner list so a user can tell their machines apart. */
|
|
450
|
-
label:
|
|
451
|
-
platform:
|
|
1254
|
+
label: z8.string().min(1).max(120),
|
|
1255
|
+
platform: z8.enum(["darwin", "linux", "win32"])
|
|
452
1256
|
}),
|
|
1257
|
+
/**
|
|
1258
|
+
* This machine's public keys (byollm_009 §5).
|
|
1259
|
+
*
|
|
1260
|
+
* Pairing is where the two parties learn each other's identities, because
|
|
1261
|
+
* it is the one moment a human is already deciding to trust: the approval
|
|
1262
|
+
* click. A key exchanged anywhere else would be a key nobody chose.
|
|
1263
|
+
*/
|
|
1264
|
+
device: PublicIdentity,
|
|
453
1265
|
capabilities: CapabilityMatrix
|
|
454
1266
|
}).strict();
|
|
455
|
-
var PairStartResponse =
|
|
1267
|
+
var PairStartResponse = z8.object({
|
|
456
1268
|
/** Secret the daemon polls with. Never shown to the user. */
|
|
457
|
-
deviceCode:
|
|
1269
|
+
deviceCode: z8.string().min(20),
|
|
458
1270
|
/** Short code the user reads and confirms in the browser. */
|
|
459
|
-
userCode:
|
|
1271
|
+
userCode: z8.string().min(4).max(16),
|
|
460
1272
|
/** Where the user approves. Must be on the server's own origin. */
|
|
461
|
-
verificationUrl:
|
|
1273
|
+
verificationUrl: z8.url(),
|
|
462
1274
|
/** Epoch ms after which the code is dead ({@link MUSTS.PAIR_CODE_EXPIRES}). */
|
|
463
|
-
expiresAt:
|
|
1275
|
+
expiresAt: z8.number().int().positive(),
|
|
464
1276
|
/** How often the daemon may poll. */
|
|
465
|
-
pollIntervalMs:
|
|
1277
|
+
pollIntervalMs: z8.number().int().min(500).max(6e4)
|
|
466
1278
|
}).strict();
|
|
467
|
-
var PairPollRequest =
|
|
468
|
-
protocolVersion:
|
|
469
|
-
action:
|
|
470
|
-
deviceCode:
|
|
1279
|
+
var PairPollRequest = z8.object({
|
|
1280
|
+
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1281
|
+
action: z8.literal("poll"),
|
|
1282
|
+
deviceCode: z8.string().min(20)
|
|
471
1283
|
}).strict();
|
|
472
|
-
var PairPollResponse =
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
status:
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
1284
|
+
var PairPollResponse = z8.discriminatedUnion("status", [
|
|
1285
|
+
z8.object({ status: z8.literal("pending") }).strict(),
|
|
1286
|
+
z8.object({ status: z8.literal("denied") }).strict(),
|
|
1287
|
+
z8.object({ status: z8.literal("expired") }).strict(),
|
|
1288
|
+
z8.object({
|
|
1289
|
+
status: z8.literal("approved"),
|
|
1290
|
+
// `runnerToken` is gone — cloud_008 §2.4, finding 37.
|
|
1291
|
+
//
|
|
1292
|
+
// It was minted here, hashed into `RunnerRecord.tokenHash`, written to
|
|
1293
|
+
// the daemon's pairings file, and then **never sent, never looked up
|
|
1294
|
+
// and never compared**. `getRunnerByTokenHash` existed on both stores
|
|
1295
|
+
// and was called by nothing but a test asserting it returns null.
|
|
1296
|
+
//
|
|
1297
|
+
// Not merely dead wire, which is what `audienceAllow` and
|
|
1298
|
+
// `HeartbeatResponse.leases` were. This was a *secret*: minted,
|
|
1299
|
+
// transmitted, and written to two disks at rest, for nothing. A
|
|
1300
|
+
// credential with no purpose is a liability rather than clutter,
|
|
1301
|
+
// because the only thing it can ever do is leak.
|
|
1302
|
+
//
|
|
1303
|
+
// `REQUESTS_SIGNED_NOT_BEARER` was already the rule and was already
|
|
1304
|
+
// enforced — every authenticated call is signed by the device's pinned
|
|
1305
|
+
// identity key. This removes the thing the MUST is named after.
|
|
1306
|
+
runnerId: z8.string().min(1),
|
|
481
1307
|
/** The app's id for the approving user — this daemon's owner forever. */
|
|
482
|
-
owner:
|
|
1308
|
+
owner: z8.string().min(1),
|
|
483
1309
|
/** Display name for the trust UI, if the app offers one. */
|
|
484
|
-
ownerLabel:
|
|
1310
|
+
ownerLabel: z8.string().optional(),
|
|
1311
|
+
/**
|
|
1312
|
+
* The site's public keys, for the daemon to pin (byollm_009 §5).
|
|
1313
|
+
*
|
|
1314
|
+
* Returned only on approval — a pending or denied poll learns nothing,
|
|
1315
|
+
* so an unapproved code cannot be used to enumerate a site's keys.
|
|
1316
|
+
*/
|
|
1317
|
+
site: PublicIdentity
|
|
485
1318
|
}).strict()
|
|
486
1319
|
]);
|
|
487
|
-
var PairRequest =
|
|
1320
|
+
var PairRequest = z8.discriminatedUnion("action", [
|
|
488
1321
|
PairStartRequest,
|
|
489
1322
|
PairPollRequest
|
|
490
1323
|
]);
|
|
491
|
-
var ClaimRequest =
|
|
492
|
-
protocolVersion:
|
|
493
|
-
runnerId:
|
|
1324
|
+
var ClaimRequest = z8.object({
|
|
1325
|
+
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1326
|
+
runnerId: z8.string().min(1),
|
|
494
1327
|
/** Re-sent on every claim so a server never matches against a stale matrix. */
|
|
495
1328
|
capabilities: CapabilityMatrix,
|
|
496
1329
|
/** Upper bound on jobs to return; the server may return fewer. */
|
|
497
|
-
max:
|
|
1330
|
+
max: z8.number().int().min(1).max(64)
|
|
498
1331
|
}).strict();
|
|
499
|
-
var ClaimResponse =
|
|
500
|
-
|
|
1332
|
+
var ClaimResponse = z8.object({
|
|
1333
|
+
/**
|
|
1334
|
+
* Stubs, not jobs. The payload arrives from `fetch`, sealed to whichever
|
|
1335
|
+
* device claimed — see {@link JobStub} for the exhaustive metadata list.
|
|
1336
|
+
*/
|
|
1337
|
+
jobs: z8.array(ClaimedStub),
|
|
501
1338
|
/** Lease duration granted, so the daemon knows its renewal deadline. */
|
|
502
|
-
leaseMs:
|
|
1339
|
+
leaseMs: z8.number().int().positive()
|
|
503
1340
|
}).strict();
|
|
504
|
-
var HeartbeatRequest =
|
|
505
|
-
protocolVersion:
|
|
506
|
-
runnerId:
|
|
507
|
-
daemonVersion:
|
|
1341
|
+
var HeartbeatRequest = z8.object({
|
|
1342
|
+
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1343
|
+
runnerId: z8.string().min(1),
|
|
1344
|
+
daemonVersion: z8.string().min(1),
|
|
508
1345
|
capabilities: CapabilityMatrix,
|
|
509
|
-
/**
|
|
510
|
-
|
|
1346
|
+
/**
|
|
1347
|
+
* Leases this daemon believes it holds; the server renews exactly these.
|
|
1348
|
+
*
|
|
1349
|
+
* Lease ids rather than job ids, so a replayed heartbeat cannot renew a
|
|
1350
|
+
* grant the runner no longer holds — see {@link Lease.id}.
|
|
1351
|
+
*/
|
|
1352
|
+
activeLeases: z8.array(
|
|
1353
|
+
z8.object({ jobId: z8.string().min(1), leaseId: z8.string().min(1) })
|
|
1354
|
+
),
|
|
511
1355
|
/** True while the owner has the daemon paused; the server stops offering work. */
|
|
512
|
-
paused:
|
|
1356
|
+
paused: z8.boolean()
|
|
513
1357
|
}).strict();
|
|
514
|
-
var HeartbeatResponse =
|
|
1358
|
+
var HeartbeatResponse = z8.object({
|
|
515
1359
|
/** Once true, the daemon stops claiming and abandons in-flight work. */
|
|
516
|
-
revoked:
|
|
1360
|
+
revoked: z8.boolean(),
|
|
517
1361
|
/**
|
|
518
1362
|
* Per-job cancel (byollm_001 Rev 1 §C). The daemon aborts these jobs'
|
|
519
1363
|
* in-flight backend calls and reports them `canceled`.
|
|
520
1364
|
*/
|
|
521
|
-
cancel:
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
1365
|
+
cancel: z8.array(z8.string().min(1)),
|
|
1366
|
+
// `leases` is deliberately absent — cloud_008 §1.4b, finding 16.
|
|
1367
|
+
//
|
|
1368
|
+
// It carried "these leases were renewed, and here is the new expiry", and
|
|
1369
|
+
// **no daemon ever read it.** A mutation returning an empty list while
|
|
1370
|
+
// renewing correctly survived every test, which is what made it visible.
|
|
1371
|
+
//
|
|
1372
|
+
// It is neither a class nor membership, so Amendment A's rule does not
|
|
1373
|
+
// decide it — the older test does: nothing reads it, so it is dead wire.
|
|
1374
|
+
// §6's exhaustiveness is a commitment about what an upstream can see, and
|
|
1375
|
+
// it applies to every message rather than only to the stub.
|
|
1376
|
+
//
|
|
1377
|
+
// `lost` is the actionable signal and always was: a daemon stops work on
|
|
1378
|
+
// a lease it no longer holds. "Renewed" was the same question answered a
|
|
1379
|
+
// second time, and a second answer can only agree or contradict.
|
|
1380
|
+
//
|
|
1381
|
+
// Renewal itself is untouched — the upstream still extends the grants a
|
|
1382
|
+
// heartbeat names, which is what §0.6 fixed. What ended is telling the
|
|
1383
|
+
// daemon about it in a field it ignored. If an upstream ever needs to
|
|
1384
|
+
// push lease decisions, that is a new field with a reader, added on
|
|
1385
|
+
// purpose.
|
|
529
1386
|
/**
|
|
530
1387
|
* Jobs the daemon thinks it holds but the server has reassigned or
|
|
531
1388
|
* expired. The daemon must stop work on these and not report results.
|
|
532
1389
|
*/
|
|
533
|
-
lost:
|
|
1390
|
+
lost: z8.array(z8.string().min(1)),
|
|
534
1391
|
/** Server clock, so a daemon with a skewed clock still honors leases. */
|
|
535
|
-
serverTime:
|
|
1392
|
+
serverTime: z8.number().int().positive()
|
|
536
1393
|
}).strict();
|
|
537
|
-
var
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
/**
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
1394
|
+
var ResultDisposition = z8.enum(["ok", "error", "canceled"]);
|
|
1395
|
+
var ResultRequest = z8.object({
|
|
1396
|
+
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1397
|
+
runnerId: z8.string().min(1),
|
|
1398
|
+
jobId: z8.string().min(1),
|
|
1399
|
+
/**
|
|
1400
|
+
* The grant this result was produced under — cloud_008 §1.4a.
|
|
1401
|
+
*
|
|
1402
|
+
* `fetch` has always named its lease, with the reasoning written beside
|
|
1403
|
+
* it: a request that names only the job would be answerable for whatever
|
|
1404
|
+
* lease exists when it arrives. **The operation that writes the result did
|
|
1405
|
+
* not**, on either plane, and checked only the runner id — which survives
|
|
1406
|
+
* a claim-release-reclaim cycle, so a device whose grant had been swept
|
|
1407
|
+
* and reissued could still land a result for a job it no longer held.
|
|
1408
|
+
*
|
|
1409
|
+
* Found by tracing a mutation that survived in §0.6: the lease lapsed, the
|
|
1410
|
+
* sweep requeued, the daemon re-claimed under a new grant, and the
|
|
1411
|
+
* original run finished and posted anyway. The relay marked the job done
|
|
1412
|
+
* with a result the site cannot open — it verifies the envelope against
|
|
1413
|
+
* the *current* holder's device, so the crypto contains the substitution —
|
|
1414
|
+
* and then refused the real holder's result as a replay. A lost job, in
|
|
1415
|
+
* silence.
|
|
1416
|
+
*
|
|
1417
|
+
* `LEASE_HONORED` is a statement about a lease *instance*. That was
|
|
1418
|
+
* learned once already, when a replayed release yanked a later grant, and
|
|
1419
|
+
* it applies here for the same reason.
|
|
1420
|
+
*/
|
|
1421
|
+
leaseId: z8.string().min(1),
|
|
1422
|
+
/**
|
|
1423
|
+
* The outcome, sealed to the site and signed by the device.
|
|
1424
|
+
*
|
|
1425
|
+
* The return leg of the payload envelope, and sealed for the same reason:
|
|
1426
|
+
* a model's answer is as sensitive as the prompt that produced it, and an
|
|
1427
|
+
* intermediary that cannot read one must not be handed the other.
|
|
1428
|
+
*/
|
|
1429
|
+
envelope: SealedEnvelope,
|
|
1430
|
+
/**
|
|
1431
|
+
* The sealed outcome's discriminator, in the clear.
|
|
1432
|
+
*
|
|
1433
|
+
* Checked against the envelope once opened. It is a routing hint, not a
|
|
1434
|
+
* fact: believing it unverified would let a daemon mark a job `ok` while
|
|
1435
|
+
* sealing an error, and only the app would ever find out.
|
|
1436
|
+
*/
|
|
1437
|
+
disposition: ResultDisposition
|
|
1438
|
+
// `model`, `backendClass` and `durationMs` are **inside the envelope** —
|
|
1439
|
+
// cloud_008 §2.5. See {@link RunMetadata}.
|
|
1440
|
+
//
|
|
1441
|
+
// They were here, in the clear, and that was two problems wearing one
|
|
1442
|
+
// coat. On the direct plane the site recorded unauthenticated fields
|
|
1443
|
+
// beside an authenticated answer: a daemon could seal one result and
|
|
1444
|
+
// declare a different model, and only the unsigned half would reach the
|
|
1445
|
+
// app. Through a relay they reached a third party that acts on none of
|
|
1446
|
+
// them — `model` in particular being the sort of detail Amendment A's
|
|
1447
|
+
// rule keeps off the wire.
|
|
1448
|
+
//
|
|
1449
|
+
// `disposition` stays, and the difference is the test: a relay *routes*
|
|
1450
|
+
// on it, so it is a class a routing party consumes. Nobody between the
|
|
1451
|
+
// two ends consumes these.
|
|
547
1452
|
}).strict();
|
|
548
|
-
var ResultResponse =
|
|
1453
|
+
var ResultResponse = z8.object({
|
|
549
1454
|
/**
|
|
550
|
-
* False when
|
|
551
|
-
*
|
|
552
|
-
* ({@link MUSTS.RESULT_IDEMPOTENT}).
|
|
1455
|
+
* False when this submission wrote nothing — the daemon should discard,
|
|
1456
|
+
* not retry ({@link MUSTS.RESULT_IDEMPOTENT}).
|
|
553
1457
|
*/
|
|
554
|
-
accepted:
|
|
1458
|
+
accepted: z8.boolean(),
|
|
1459
|
+
/**
|
|
1460
|
+
* True when this device had already recorded this job's result.
|
|
1461
|
+
*
|
|
1462
|
+
* The difference between "already recorded" and "you no longer hold this"
|
|
1463
|
+
* — cloud_008 §3.6. A daemon whose acknowledgment was lost is in the first
|
|
1464
|
+
* case and needs to hear it: its answer is safely on disk. Reporting a
|
|
1465
|
+
* stale lease instead invents a worry about a result that is already
|
|
1466
|
+
* stored, and sends its owner looking for a routing problem.
|
|
1467
|
+
*
|
|
1468
|
+
* Set only for the device that finished the job. A different device gets
|
|
1469
|
+
* the same refusal it would get for a job that is *not* terminal, so a job
|
|
1470
|
+
* id cannot be used as a terminality probe.
|
|
1471
|
+
*/
|
|
1472
|
+
duplicate: z8.boolean().optional(),
|
|
555
1473
|
/** The job's state after this submission. */
|
|
556
|
-
state:
|
|
1474
|
+
state: z8.string().min(1)
|
|
557
1475
|
}).strict();
|
|
558
|
-
var ReleaseRequest =
|
|
559
|
-
protocolVersion:
|
|
560
|
-
runnerId:
|
|
561
|
-
|
|
1476
|
+
var ReleaseRequest = z8.object({
|
|
1477
|
+
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1478
|
+
runnerId: z8.string().min(1),
|
|
1479
|
+
/**
|
|
1480
|
+
* Which leases to release — the grant, not just the job.
|
|
1481
|
+
*
|
|
1482
|
+
* A release naming only a job id releases whatever lease exists at the
|
|
1483
|
+
* moment it arrives, which for a replayed request is not the lease the
|
|
1484
|
+
* daemon meant. See {@link Lease.id}.
|
|
1485
|
+
*/
|
|
1486
|
+
leases: z8.array(
|
|
1487
|
+
z8.object({ jobId: z8.string().min(1), leaseId: z8.string().min(1) })
|
|
1488
|
+
),
|
|
562
1489
|
/**
|
|
563
1490
|
* Why, so the app's runner list can say something true.
|
|
564
1491
|
*
|
|
@@ -568,42 +1495,129 @@ var ReleaseRequest = z5.object({
|
|
|
568
1495
|
* stop offering that job to that runner, or the pair would spin between
|
|
569
1496
|
* claim and release forever.
|
|
570
1497
|
*/
|
|
571
|
-
reason:
|
|
1498
|
+
reason: z8.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
|
|
572
1499
|
}).strict();
|
|
573
|
-
var ReleaseResponse =
|
|
574
|
-
released:
|
|
1500
|
+
var ReleaseResponse = z8.object({
|
|
1501
|
+
released: z8.array(z8.string().min(1))
|
|
575
1502
|
}).strict();
|
|
576
|
-
var WireErrorCode =
|
|
1503
|
+
var WireErrorCode = z8.enum([
|
|
577
1504
|
"bad-request",
|
|
578
1505
|
"unsupported-protocol-version",
|
|
1506
|
+
// "We do not know who you are." Exactly 401, and only that — cloud_008
|
|
1507
|
+
// §1.4d.
|
|
579
1508
|
"unauthorized",
|
|
1509
|
+
/**
|
|
1510
|
+
* "We know exactly who you are, and the answer is no." Exactly 403.
|
|
1511
|
+
*
|
|
1512
|
+
* Five refusals across both planes served 403 with `unauthorized`, whose
|
|
1513
|
+
* table entry is 401: a revoked device, a site claiming another site's
|
|
1514
|
+
* stub, a job you do not hold, a device belonging to another owner, a
|
|
1515
|
+
* relay that does not route for you. Every one of them is an *identified*
|
|
1516
|
+
* caller being refused.
|
|
1517
|
+
*
|
|
1518
|
+
* Collapsing the two loses a distinction that matters everywhere it is
|
|
1519
|
+
* read: a revoked daemon would look like an unsigned one in every log and
|
|
1520
|
+
* every client branch, and "check your keys" is the wrong advice for both
|
|
1521
|
+
* of them in opposite directions.
|
|
1522
|
+
*/
|
|
1523
|
+
"forbidden",
|
|
580
1524
|
"revoked",
|
|
581
1525
|
"not-found",
|
|
1526
|
+
// Claimed, but the site has not sealed the payload yet — cloud_008 §1.4.
|
|
1527
|
+
//
|
|
1528
|
+
// A daemon must retry rather than abandon: the job is legitimately still
|
|
1529
|
+
// its own until the lease or the awaiting-payload clock says otherwise.
|
|
1530
|
+
// That is why it cannot be `not-found` or `server-error`, and why it was
|
|
1531
|
+
// the protocol gap that produced a bare 409 in the first place.
|
|
1532
|
+
"not-ready",
|
|
1533
|
+
// The caller's clock is too far from ours to judge a signature's freshness.
|
|
1534
|
+
//
|
|
1535
|
+
// Split out from `unauthorized` because the remedy is completely different
|
|
1536
|
+
// and only the server can tell them apart: a bad signature means the key is
|
|
1537
|
+
// wrong, this means the machine's time is wrong. A daemon reporting it as a
|
|
1538
|
+
// generic rejection sends its owner looking at their network.
|
|
1539
|
+
"clock-skew",
|
|
582
1540
|
"rate-limited",
|
|
583
1541
|
"server-error"
|
|
584
1542
|
]);
|
|
585
|
-
var WireError =
|
|
1543
|
+
var WireError = z8.object({
|
|
586
1544
|
error: WireErrorCode,
|
|
587
|
-
message:
|
|
1545
|
+
message: z8.string().min(1),
|
|
588
1546
|
/** Seconds; mirrors Retry-After for `rate-limited` and `server-error`. */
|
|
589
|
-
retryAfter:
|
|
590
|
-
|
|
1547
|
+
retryAfter: z8.number().int().nonnegative().optional(),
|
|
1548
|
+
/**
|
|
1549
|
+
* The server's clock, and the window it allows. `clock-skew` only.
|
|
1550
|
+
*
|
|
1551
|
+
* So the far side can say *how far off* rather than *that something is
|
|
1552
|
+
* wrong* — the difference between "adjust your clock by four minutes" and
|
|
1553
|
+
* "something is wrong with your connection". Not a disclosure: the
|
|
1554
|
+
* heartbeat response returns the same value, and so does every `Date`
|
|
1555
|
+
* header.
|
|
1556
|
+
*/
|
|
1557
|
+
serverTime: z8.number().int().positive().optional(),
|
|
1558
|
+
maxSkewMs: z8.number().int().positive().optional()
|
|
1559
|
+
}).strict().superRefine((error, ctx) => {
|
|
1560
|
+
const skew = error.error === "clock-skew";
|
|
1561
|
+
const carried = error.serverTime !== void 0 || error.maxSkewMs !== void 0;
|
|
1562
|
+
if (skew && !carried) {
|
|
1563
|
+
ctx.addIssue({
|
|
1564
|
+
code: "custom",
|
|
1565
|
+
message: "clock-skew must carry serverTime and maxSkewMs"
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
if (!skew && carried) {
|
|
1569
|
+
ctx.addIssue({
|
|
1570
|
+
code: "custom",
|
|
1571
|
+
message: `${error.error} must not carry serverTime or maxSkewMs`
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
});
|
|
591
1575
|
var ERROR_STATUS = Object.freeze({
|
|
592
1576
|
"bad-request": 400,
|
|
593
1577
|
"unsupported-protocol-version": 400,
|
|
594
1578
|
unauthorized: 401,
|
|
1579
|
+
forbidden: 403,
|
|
595
1580
|
revoked: 403,
|
|
596
1581
|
"not-found": 404,
|
|
1582
|
+
// 409, not 404: the job exists and is yours, it is simply not ready.
|
|
1583
|
+
"not-ready": 409,
|
|
1584
|
+
// 401 alongside `unauthorized`, because that is what it is — the
|
|
1585
|
+
// signature could not be judged. The code is what carries the remedy.
|
|
1586
|
+
"clock-skew": 401,
|
|
597
1587
|
"rate-limited": 429,
|
|
598
1588
|
"server-error": 500
|
|
599
1589
|
});
|
|
1590
|
+
var FetchRequest = z8.object({
|
|
1591
|
+
protocolVersion: z8.string().min(1),
|
|
1592
|
+
runnerId: z8.string().min(1),
|
|
1593
|
+
jobId: z8.string().min(1),
|
|
1594
|
+
/**
|
|
1595
|
+
* The grant this daemon holds.
|
|
1596
|
+
*
|
|
1597
|
+
* Named, not inferred: a fetch is lease-scoped, and a request that names
|
|
1598
|
+
* only the job would be answerable for whatever lease exists when it
|
|
1599
|
+
* arrives ({@link Lease.id}).
|
|
1600
|
+
*/
|
|
1601
|
+
leaseId: z8.string().min(1)
|
|
1602
|
+
}).strict();
|
|
1603
|
+
var FetchResponse = z8.object({
|
|
1604
|
+
/**
|
|
1605
|
+
* The work, sealed to the device that claimed it — byollm_009 §6.
|
|
1606
|
+
*
|
|
1607
|
+
* Not plaintext. The site opens its own at-rest envelope and re-seals to
|
|
1608
|
+
* the claiming device's key, signed by the site's identity, so the work
|
|
1609
|
+
* is readable only by the machine that took it and only if it came from
|
|
1610
|
+
* the site that machine pinned.
|
|
1611
|
+
*/
|
|
1612
|
+
envelope: SealedEnvelope
|
|
1613
|
+
}).strict();
|
|
600
1614
|
export {
|
|
601
1615
|
AUDIENCES,
|
|
602
1616
|
Audience,
|
|
603
1617
|
BACKENDS,
|
|
604
1618
|
BACKEND_IDS,
|
|
605
|
-
BackendAccount,
|
|
606
1619
|
BackendClass,
|
|
1620
|
+
BackendCost,
|
|
607
1621
|
BackendIdSchema,
|
|
608
1622
|
Capability,
|
|
609
1623
|
CapabilityMatrix,
|
|
@@ -612,9 +1626,14 @@ export {
|
|
|
612
1626
|
ClaimRequest,
|
|
613
1627
|
ClaimResponse,
|
|
614
1628
|
ClaimedJob,
|
|
1629
|
+
ClaimedStub,
|
|
615
1630
|
DeliveredResult,
|
|
616
1631
|
ENDPOINTS,
|
|
1632
|
+
ENVELOPE_MAX_AGE_MS,
|
|
617
1633
|
ERROR_STATUS,
|
|
1634
|
+
EnvelopeDirection,
|
|
1635
|
+
FetchRequest,
|
|
1636
|
+
FetchResponse,
|
|
618
1637
|
GeneratePayload,
|
|
619
1638
|
HeartbeatRequest,
|
|
620
1639
|
HeartbeatResponse,
|
|
@@ -626,8 +1645,11 @@ export {
|
|
|
626
1645
|
JobResultError,
|
|
627
1646
|
JobResultOk,
|
|
628
1647
|
JobState,
|
|
1648
|
+
JobStub,
|
|
629
1649
|
KindedPayload,
|
|
630
1650
|
Lease,
|
|
1651
|
+
MAX_CLOCK_SKEW_MS,
|
|
1652
|
+
MIN_PROTOCOL_VERSION,
|
|
631
1653
|
MUSTS,
|
|
632
1654
|
MUST_IDS,
|
|
633
1655
|
MatchRefusal,
|
|
@@ -641,23 +1663,54 @@ export {
|
|
|
641
1663
|
PairRequest,
|
|
642
1664
|
PairStartRequest,
|
|
643
1665
|
PairStartResponse,
|
|
1666
|
+
PublicIdentity,
|
|
644
1667
|
REFUSAL_MESSAGES,
|
|
645
1668
|
ReleaseRequest,
|
|
646
1669
|
ReleaseResponse,
|
|
1670
|
+
RequestSignature,
|
|
1671
|
+
ResultDisposition,
|
|
647
1672
|
ResultProvenance,
|
|
648
1673
|
ResultRequest,
|
|
649
1674
|
ResultResponse,
|
|
1675
|
+
RunMetadata,
|
|
1676
|
+
SIZE_CLASS_LIMITS,
|
|
1677
|
+
SUPPORTED_PROTOCOL_VERSIONS,
|
|
1678
|
+
SealedEnvelope,
|
|
1679
|
+
SealedOutcome,
|
|
1680
|
+
SizeClass,
|
|
1681
|
+
StoredKeys,
|
|
650
1682
|
TERMINAL_STATES,
|
|
651
1683
|
WireError,
|
|
652
1684
|
WireErrorCode,
|
|
653
1685
|
backendDescriptor,
|
|
654
1686
|
canTransition,
|
|
1687
|
+
canonicalRequest,
|
|
1688
|
+
checkProtocolVersion,
|
|
1689
|
+
cryptoReady,
|
|
655
1690
|
effectiveOfferScope,
|
|
1691
|
+
fingerprint,
|
|
1692
|
+
generateKeys,
|
|
656
1693
|
isBackendId,
|
|
657
1694
|
isJobKind,
|
|
1695
|
+
isLocalHost,
|
|
658
1696
|
isTerminal,
|
|
1697
|
+
keyId,
|
|
659
1698
|
matchAudience,
|
|
1699
|
+
mustsVerifiedBy,
|
|
1700
|
+
open,
|
|
660
1701
|
payloadTextLength,
|
|
661
|
-
provenanceFor
|
|
1702
|
+
provenanceFor,
|
|
1703
|
+
publicIdentityOf,
|
|
1704
|
+
resolveCost,
|
|
1705
|
+
seal,
|
|
1706
|
+
signRequest,
|
|
1707
|
+
signSiteRequest,
|
|
1708
|
+
signWith,
|
|
1709
|
+
sizeClassCeiling,
|
|
1710
|
+
sizeClassOf,
|
|
1711
|
+
verifyPublicIdentity,
|
|
1712
|
+
verifyRequest,
|
|
1713
|
+
verifySiteRequest,
|
|
1714
|
+
verifyWith
|
|
662
1715
|
};
|
|
663
1716
|
//# sourceMappingURL=index.js.map
|