@byollm/protocol 0.1.0-alpha.1 → 0.1.0-alpha.10
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 +5 -1
- package/dist/index.d.ts +811 -66
- package/dist/index.js +866 -88
- package/dist/index.js.map +1 -1
- package/package.json +11 -2
package/dist/index.js
CHANGED
|
@@ -4,21 +4,163 @@ import { z as z2 } from "zod";
|
|
|
4
4
|
// src/backends.ts
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
var BackendClass = z.enum(["http", "process"]);
|
|
7
|
-
var
|
|
7
|
+
var BackendCost = z.enum(["free", "metered", "subscription"]);
|
|
8
8
|
var backend = (b) => Object.freeze(b);
|
|
9
9
|
var BACKENDS = Object.freeze({
|
|
10
|
+
// -- free: local compute, costs electricity ------------------------------
|
|
11
|
+
ollama: backend({
|
|
12
|
+
id: "ollama",
|
|
13
|
+
label: "Ollama (local)",
|
|
14
|
+
class: "http",
|
|
15
|
+
cost: "free",
|
|
16
|
+
adversarialCorpus: "http",
|
|
17
|
+
defaultBaseUrl: "http://127.0.0.1:11434/v1"
|
|
18
|
+
}),
|
|
19
|
+
mlx: backend({
|
|
20
|
+
id: "mlx",
|
|
21
|
+
label: "MLX (mlx_lm.server, local)",
|
|
22
|
+
class: "http",
|
|
23
|
+
cost: "free",
|
|
24
|
+
adversarialCorpus: "http",
|
|
25
|
+
defaultBaseUrl: "http://127.0.0.1:8080/v1"
|
|
26
|
+
}),
|
|
27
|
+
llamacpp: backend({
|
|
28
|
+
id: "llamacpp",
|
|
29
|
+
label: "llama.cpp server (local)",
|
|
30
|
+
class: "http",
|
|
31
|
+
cost: "free",
|
|
32
|
+
adversarialCorpus: "http",
|
|
33
|
+
defaultBaseUrl: "http://127.0.0.1:8080/v1"
|
|
34
|
+
}),
|
|
35
|
+
vllm: backend({
|
|
36
|
+
id: "vllm",
|
|
37
|
+
label: "vLLM (local)",
|
|
38
|
+
class: "http",
|
|
39
|
+
cost: "free",
|
|
40
|
+
adversarialCorpus: "http",
|
|
41
|
+
defaultBaseUrl: "http://127.0.0.1:8000/v1"
|
|
42
|
+
}),
|
|
43
|
+
lmstudio: backend({
|
|
44
|
+
id: "lmstudio",
|
|
45
|
+
label: "LM Studio (local)",
|
|
46
|
+
class: "http",
|
|
47
|
+
cost: "free",
|
|
48
|
+
adversarialCorpus: "http",
|
|
49
|
+
defaultBaseUrl: "http://127.0.0.1:1234/v1"
|
|
50
|
+
}),
|
|
51
|
+
jan: backend({
|
|
52
|
+
id: "jan",
|
|
53
|
+
label: "Jan (local)",
|
|
54
|
+
class: "http",
|
|
55
|
+
cost: "free",
|
|
56
|
+
adversarialCorpus: "http",
|
|
57
|
+
defaultBaseUrl: "http://127.0.0.1:1337/v1"
|
|
58
|
+
}),
|
|
59
|
+
localai: backend({
|
|
60
|
+
id: "localai",
|
|
61
|
+
label: "LocalAI (local)",
|
|
62
|
+
class: "http",
|
|
63
|
+
cost: "free",
|
|
64
|
+
adversarialCorpus: "http",
|
|
65
|
+
defaultBaseUrl: "http://127.0.0.1:8080/v1"
|
|
66
|
+
}),
|
|
67
|
+
// -- metered: the owner's money, per token -------------------------------
|
|
68
|
+
/**
|
|
69
|
+
* Note the pair: `anthropic` and {@link BACKENDS."claude-cli"} reach the
|
|
70
|
+
* same vendor and land in different cost classes. That is not an
|
|
71
|
+
* inconsistency — it is the axis working. One bills a key per token, the
|
|
72
|
+
* other runs under a personal plan whose terms cover one person's work. Who
|
|
73
|
+
* pays and under what terms is the question; which company is not.
|
|
74
|
+
*/
|
|
75
|
+
anthropic: backend({
|
|
76
|
+
id: "anthropic",
|
|
77
|
+
label: "Anthropic (your API key)",
|
|
78
|
+
class: "http",
|
|
79
|
+
cost: "metered",
|
|
80
|
+
adversarialCorpus: "http",
|
|
81
|
+
defaultBaseUrl: "https://api.anthropic.com/v1"
|
|
82
|
+
}),
|
|
83
|
+
openai: backend({
|
|
84
|
+
id: "openai",
|
|
85
|
+
label: "OpenAI (your API key)",
|
|
86
|
+
class: "http",
|
|
87
|
+
cost: "metered",
|
|
88
|
+
adversarialCorpus: "http",
|
|
89
|
+
defaultBaseUrl: "https://api.openai.com/v1"
|
|
90
|
+
}),
|
|
91
|
+
gemini: backend({
|
|
92
|
+
id: "gemini",
|
|
93
|
+
label: "Google Gemini (your API key)",
|
|
94
|
+
class: "http",
|
|
95
|
+
cost: "metered",
|
|
96
|
+
adversarialCorpus: "http",
|
|
97
|
+
defaultBaseUrl: "https://generativelanguage.googleapis.com/v1beta/openai"
|
|
98
|
+
}),
|
|
99
|
+
grok: backend({
|
|
100
|
+
id: "grok",
|
|
101
|
+
label: "xAI Grok (your API key)",
|
|
102
|
+
class: "http",
|
|
103
|
+
cost: "metered",
|
|
104
|
+
adversarialCorpus: "http",
|
|
105
|
+
defaultBaseUrl: "https://api.x.ai/v1"
|
|
106
|
+
}),
|
|
107
|
+
groq: backend({
|
|
108
|
+
id: "groq",
|
|
109
|
+
label: "Groq (your API key)",
|
|
110
|
+
class: "http",
|
|
111
|
+
cost: "metered",
|
|
112
|
+
adversarialCorpus: "http",
|
|
113
|
+
defaultBaseUrl: "https://api.groq.com/openai/v1"
|
|
114
|
+
}),
|
|
115
|
+
openrouter: backend({
|
|
116
|
+
id: "openrouter",
|
|
117
|
+
label: "OpenRouter (your API key)",
|
|
118
|
+
class: "http",
|
|
119
|
+
cost: "metered",
|
|
120
|
+
adversarialCorpus: "http",
|
|
121
|
+
defaultBaseUrl: "https://openrouter.ai/api/v1"
|
|
122
|
+
}),
|
|
123
|
+
together: backend({
|
|
124
|
+
id: "together",
|
|
125
|
+
label: "Together AI (your API key)",
|
|
126
|
+
class: "http",
|
|
127
|
+
cost: "metered",
|
|
128
|
+
adversarialCorpus: "http",
|
|
129
|
+
defaultBaseUrl: "https://api.together.xyz/v1"
|
|
130
|
+
}),
|
|
131
|
+
deepseek: backend({
|
|
132
|
+
id: "deepseek",
|
|
133
|
+
label: "DeepSeek (your API key)",
|
|
134
|
+
class: "http",
|
|
135
|
+
cost: "metered",
|
|
136
|
+
adversarialCorpus: "http",
|
|
137
|
+
defaultBaseUrl: "https://api.deepseek.com/v1"
|
|
138
|
+
}),
|
|
139
|
+
mistral: backend({
|
|
140
|
+
id: "mistral",
|
|
141
|
+
label: "Mistral (your API key)",
|
|
142
|
+
class: "http",
|
|
143
|
+
cost: "metered",
|
|
144
|
+
adversarialCorpus: "http",
|
|
145
|
+
defaultBaseUrl: "https://api.mistral.ai/v1"
|
|
146
|
+
}),
|
|
147
|
+
// -- the escape hatch ----------------------------------------------------
|
|
10
148
|
"openai-http": backend({
|
|
11
149
|
id: "openai-http",
|
|
12
|
-
label: "OpenAI-compatible
|
|
150
|
+
label: "Any OpenAI-compatible server",
|
|
13
151
|
class: "http",
|
|
14
|
-
|
|
152
|
+
// Unknown until the base URL is known: local means free, remote means
|
|
153
|
+
// metered, and the owner does not get to say otherwise
|
|
154
|
+
// ({@link MUSTS.REMOTE_IS_NEVER_FREE}).
|
|
155
|
+
cost: null,
|
|
15
156
|
adversarialCorpus: "http"
|
|
16
157
|
}),
|
|
158
|
+
// -- subscription: someone else's terms ----------------------------------
|
|
17
159
|
"claude-cli": backend({
|
|
18
160
|
id: "claude-cli",
|
|
19
161
|
label: "Claude CLI (your subscription)",
|
|
20
162
|
class: "process",
|
|
21
|
-
|
|
163
|
+
cost: "subscription",
|
|
22
164
|
adversarialCorpus: "process"
|
|
23
165
|
})
|
|
24
166
|
});
|
|
@@ -32,6 +174,27 @@ function isBackendId(value) {
|
|
|
32
174
|
function backendDescriptor(id) {
|
|
33
175
|
return BACKENDS[id];
|
|
34
176
|
}
|
|
177
|
+
function isLocalHost(hostname) {
|
|
178
|
+
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
179
|
+
if (host === "localhost" || host.endsWith(".localhost")) return true;
|
|
180
|
+
if (host === "::1") return true;
|
|
181
|
+
if (host.startsWith("127.")) return true;
|
|
182
|
+
if (host.startsWith("10.")) return true;
|
|
183
|
+
if (host.startsWith("192.168.")) return true;
|
|
184
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(host)) return true;
|
|
185
|
+
if (/^f[cd]/.test(host)) return true;
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
function resolveCost(id, baseUrl) {
|
|
189
|
+
const declared = BACKENDS[id].cost;
|
|
190
|
+
if (declared !== null) return declared;
|
|
191
|
+
if (baseUrl === void 0) return "metered";
|
|
192
|
+
try {
|
|
193
|
+
return isLocalHost(new URL(baseUrl).hostname) ? "free" : "metered";
|
|
194
|
+
} catch {
|
|
195
|
+
return "metered";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
35
198
|
|
|
36
199
|
// src/audience.ts
|
|
37
200
|
var Audience = z2.enum(["self", "named", "public"]);
|
|
@@ -50,12 +213,18 @@ var MatchRefusal = z2.enum([
|
|
|
50
213
|
/** The backend offers only `self` and the job belongs to someone else. */
|
|
51
214
|
"offer-scope-too-narrow",
|
|
52
215
|
/** The matched backend is subscription-class, which is locked to `self`. */
|
|
53
|
-
"subscription-self-lock"
|
|
216
|
+
"subscription-self-lock",
|
|
217
|
+
/** The backend spends the owner's money and they have not agreed to share it. */
|
|
218
|
+
"metered-no-spend-consent",
|
|
219
|
+
/** The backend is shared but has spent its ceiling for now. */
|
|
220
|
+
"metered-ceiling-reached"
|
|
54
221
|
]);
|
|
55
222
|
var ALLOWED = Object.freeze({ ok: true });
|
|
56
223
|
var refuse = (refusal) => Object.freeze({ ok: false, refusal });
|
|
57
|
-
function effectiveOfferScope(configured,
|
|
58
|
-
|
|
224
|
+
function effectiveOfferScope(configured, cost, spend) {
|
|
225
|
+
if (cost === "subscription") return "self";
|
|
226
|
+
if (cost === "metered" && spend?.acknowledged !== true) return "self";
|
|
227
|
+
return configured;
|
|
59
228
|
}
|
|
60
229
|
function matchAudience(job, daemon) {
|
|
61
230
|
const sameOwner = job.owner === daemon.owner;
|
|
@@ -65,13 +234,25 @@ function matchAudience(job, daemon) {
|
|
|
65
234
|
if (job.audience === "named" && !sameOwner && job.audienceAllow !== void 0 && !job.audienceAllow.includes(daemon.owner)) {
|
|
66
235
|
return refuse("not-in-server-allowlist");
|
|
67
236
|
}
|
|
68
|
-
const scope = effectiveOfferScope(
|
|
237
|
+
const scope = effectiveOfferScope(
|
|
238
|
+
daemon.offerScope,
|
|
239
|
+
daemon.cost,
|
|
240
|
+
daemon.spend
|
|
241
|
+
);
|
|
69
242
|
if (sameOwner) {
|
|
70
243
|
return ALLOWED;
|
|
71
244
|
}
|
|
72
|
-
if (daemon.
|
|
245
|
+
if (daemon.cost === "subscription") {
|
|
73
246
|
return refuse("subscription-self-lock");
|
|
74
247
|
}
|
|
248
|
+
if (daemon.cost === "metered") {
|
|
249
|
+
if (daemon.spend?.acknowledged !== true) {
|
|
250
|
+
return refuse("metered-no-spend-consent");
|
|
251
|
+
}
|
|
252
|
+
if (daemon.spend.ceilingReached === true) {
|
|
253
|
+
return refuse("metered-ceiling-reached");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
75
256
|
switch (scope) {
|
|
76
257
|
case "self":
|
|
77
258
|
return refuse("offer-scope-too-narrow");
|
|
@@ -87,7 +268,9 @@ var REFUSAL_MESSAGES = Object.freeze({
|
|
|
87
268
|
"not-locally-allowed": "the job's owner is not on this machine's allowlist (byollm allow <server> <user>)",
|
|
88
269
|
"not-in-server-allowlist": "the app restricted this job to named runners and this machine is not one of them",
|
|
89
270
|
"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"
|
|
271
|
+
"subscription-self-lock": "subscription-backed models run their owner's work only \u2014 this is a protocol rule, not a setting",
|
|
272
|
+
"metered-no-spend-consent": "this backend bills its owner per token, and they have not agreed to spend it on other people's work",
|
|
273
|
+
"metered-ceiling-reached": "this backend is shared but has reached the spend ceiling its owner set"
|
|
91
274
|
});
|
|
92
275
|
|
|
93
276
|
// src/kinds.ts
|
|
@@ -167,6 +350,21 @@ function canTransition(from, to) {
|
|
|
167
350
|
return TRANSITIONS[from].includes(to);
|
|
168
351
|
}
|
|
169
352
|
var Lease = z4.object({
|
|
353
|
+
/**
|
|
354
|
+
* Identifies *this* grant, not just its holder.
|
|
355
|
+
*
|
|
356
|
+
* A runner can hold a job, release it, and claim it again — three leases,
|
|
357
|
+
* one runner id. Without an id for the grant itself, a lease-scoped request
|
|
358
|
+
* names a mutable target ambiguously, and a replayed release from the first
|
|
359
|
+
* grant lands on the third: the job returns to the queue while the daemon
|
|
360
|
+
* is mid-execution, and the work runs twice on the owner's hardware.
|
|
361
|
+
*
|
|
362
|
+
* That was a live hole, found in review after signed requests shipped. The
|
|
363
|
+
* signature scheme's replay argument rests on endpoints being idempotent —
|
|
364
|
+
* and release *is*, per lease, but not across leases, because nothing in
|
|
365
|
+
* the request said which one.
|
|
366
|
+
*/
|
|
367
|
+
id: z4.string().min(1),
|
|
170
368
|
/** The runner holding the lease. */
|
|
171
369
|
runnerId: z4.string().min(1),
|
|
172
370
|
/** Epoch milliseconds after which the claim is void. */
|
|
@@ -238,6 +436,348 @@ var DeliveredResult = z4.object({
|
|
|
238
436
|
outcome: JobOutcome.optional(),
|
|
239
437
|
provenance: ResultProvenance.optional()
|
|
240
438
|
}).strict();
|
|
439
|
+
var SizeClass = z4.enum(["small", "medium", "large", "unbounded"]);
|
|
440
|
+
var SIZE_CLASS_LIMITS = Object.freeze({
|
|
441
|
+
small: 4e3,
|
|
442
|
+
medium: 64e3,
|
|
443
|
+
large: Number.POSITIVE_INFINITY
|
|
444
|
+
});
|
|
445
|
+
function sizeClassCeiling(sizeClass) {
|
|
446
|
+
if (sizeClass === "unbounded") return Number.POSITIVE_INFINITY;
|
|
447
|
+
return SIZE_CLASS_LIMITS[sizeClass];
|
|
448
|
+
}
|
|
449
|
+
function sizeClassOf(textChars) {
|
|
450
|
+
if (textChars <= SIZE_CLASS_LIMITS.small) return "small";
|
|
451
|
+
if (textChars <= SIZE_CLASS_LIMITS.medium) return "medium";
|
|
452
|
+
return "large";
|
|
453
|
+
}
|
|
454
|
+
var JobStub = z4.object({
|
|
455
|
+
id: z4.string().min(1),
|
|
456
|
+
kind: JobKind,
|
|
457
|
+
/** The app's id for the user who enqueued it. */
|
|
458
|
+
owner: z4.string().min(1),
|
|
459
|
+
audience: Audience,
|
|
460
|
+
audienceAllow: z4.array(z4.string().min(1)).optional(),
|
|
461
|
+
sizeClass: SizeClass,
|
|
462
|
+
/** Reserved for byollm_006. False until streaming exists. */
|
|
463
|
+
streaming: z4.boolean(),
|
|
464
|
+
/** Epoch ms after which the work is pointless; bounds ciphertext retention. */
|
|
465
|
+
deadlineAt: z4.number().int().positive()
|
|
466
|
+
}).strict();
|
|
467
|
+
var ClaimedStub = JobStub.extend({ lease: Lease }).strict();
|
|
468
|
+
|
|
469
|
+
// src/envelope.ts
|
|
470
|
+
import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2 } from "crypto";
|
|
471
|
+
import sodium from "libsodium-wrappers";
|
|
472
|
+
import { z as z6 } from "zod";
|
|
473
|
+
|
|
474
|
+
// src/keys.ts
|
|
475
|
+
import {
|
|
476
|
+
createHash,
|
|
477
|
+
createPrivateKey,
|
|
478
|
+
createPublicKey,
|
|
479
|
+
generateKeyPairSync,
|
|
480
|
+
sign,
|
|
481
|
+
verify
|
|
482
|
+
} from "crypto";
|
|
483
|
+
import { z as z5 } from "zod";
|
|
484
|
+
var PublicIdentity = z5.object({
|
|
485
|
+
/** Raw Ed25519 public key. The pinned one. */
|
|
486
|
+
identity: z5.string().min(1),
|
|
487
|
+
/** Raw X25519 public key, for sealing to this party. */
|
|
488
|
+
encryption: z5.string().min(1),
|
|
489
|
+
/**
|
|
490
|
+
* Ed25519 signature over the encryption key, by the identity key.
|
|
491
|
+
*
|
|
492
|
+
* This is what stops an upstream substituting an encryption key of its
|
|
493
|
+
* own while relaying a genuine identity: the receiver pins the identity
|
|
494
|
+
* and refuses any encryption key not signed by it.
|
|
495
|
+
*/
|
|
496
|
+
encryptionSig: z5.string().min(1)
|
|
497
|
+
}).strict();
|
|
498
|
+
var StoredKeys = z5.object({
|
|
499
|
+
version: z5.literal(1),
|
|
500
|
+
identityPublic: z5.string().min(1),
|
|
501
|
+
identityPrivate: z5.string().min(1),
|
|
502
|
+
encryptionPublic: z5.string().min(1),
|
|
503
|
+
encryptionPrivate: z5.string().min(1),
|
|
504
|
+
encryptionSig: z5.string().min(1),
|
|
505
|
+
createdAt: z5.number().int().positive()
|
|
506
|
+
}).strict();
|
|
507
|
+
var ENCRYPTION_KEY_CONTEXT = "byollm/v1/encryption-key";
|
|
508
|
+
function rawPublic(key) {
|
|
509
|
+
const jwk = key.export({ format: "jwk" });
|
|
510
|
+
const x = jwk.x;
|
|
511
|
+
if (typeof x !== "string") throw new Error("key has no raw public component");
|
|
512
|
+
return x;
|
|
513
|
+
}
|
|
514
|
+
function importPublic(raw, crv) {
|
|
515
|
+
return createPublicKey({ key: { kty: "OKP", crv, x: raw }, format: "jwk" });
|
|
516
|
+
}
|
|
517
|
+
function importPrivate(stored) {
|
|
518
|
+
return createPrivateKey({
|
|
519
|
+
key: Buffer.from(stored, "base64"),
|
|
520
|
+
type: "pkcs8",
|
|
521
|
+
format: "der"
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
var exportPrivate = (key) => key.export({ type: "pkcs8", format: "der" }).toString("base64");
|
|
525
|
+
function generateKeys(now) {
|
|
526
|
+
const identity = generateKeyPairSync("ed25519");
|
|
527
|
+
const encryption = generateKeyPairSync("x25519");
|
|
528
|
+
const encryptionPublic = rawPublic(encryption.publicKey);
|
|
529
|
+
return {
|
|
530
|
+
version: 1,
|
|
531
|
+
identityPublic: rawPublic(identity.publicKey),
|
|
532
|
+
identityPrivate: exportPrivate(identity.privateKey),
|
|
533
|
+
encryptionPublic,
|
|
534
|
+
encryptionPrivate: exportPrivate(encryption.privateKey),
|
|
535
|
+
encryptionSig: sign(
|
|
536
|
+
null,
|
|
537
|
+
Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${encryptionPublic}`),
|
|
538
|
+
identity.privateKey
|
|
539
|
+
).toString("base64url"),
|
|
540
|
+
createdAt: now
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function publicIdentityOf(keys) {
|
|
544
|
+
return {
|
|
545
|
+
identity: keys.identityPublic,
|
|
546
|
+
encryption: keys.encryptionPublic,
|
|
547
|
+
encryptionSig: keys.encryptionSig
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function verifyPublicIdentity(identity) {
|
|
551
|
+
try {
|
|
552
|
+
return verify(
|
|
553
|
+
null,
|
|
554
|
+
Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${identity.encryption}`),
|
|
555
|
+
importPublic(identity.identity, "Ed25519"),
|
|
556
|
+
Buffer.from(identity.encryptionSig, "base64url")
|
|
557
|
+
);
|
|
558
|
+
} catch {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
function signWith(keys, data) {
|
|
563
|
+
return sign(null, data, importPrivate(keys.identityPrivate)).toString(
|
|
564
|
+
"base64url"
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
function verifyWith(identityPublic, data, signature) {
|
|
568
|
+
try {
|
|
569
|
+
return verify(
|
|
570
|
+
null,
|
|
571
|
+
data,
|
|
572
|
+
importPublic(identityPublic, "Ed25519"),
|
|
573
|
+
Buffer.from(signature, "base64url")
|
|
574
|
+
);
|
|
575
|
+
} catch {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
580
|
+
function fingerprint(identityPublic) {
|
|
581
|
+
const digest = createHash("sha256").update(Buffer.from(identityPublic, "base64url")).digest();
|
|
582
|
+
let bits = 0;
|
|
583
|
+
let value = 0;
|
|
584
|
+
let out = "";
|
|
585
|
+
for (const byte of digest.subarray(0, 15)) {
|
|
586
|
+
value = value << 8 | byte;
|
|
587
|
+
bits += 8;
|
|
588
|
+
while (bits >= 5) {
|
|
589
|
+
out += ALPHABET.charAt(value >>> bits - 5 & 31);
|
|
590
|
+
bits -= 5;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const groups = out.match(/.{1,4}/g) ?? [];
|
|
594
|
+
return `BYOLLM-${groups.join("-")}`;
|
|
595
|
+
}
|
|
596
|
+
var keyId = (identityPublic) => fingerprint(identityPublic);
|
|
597
|
+
|
|
598
|
+
// src/envelope.ts
|
|
599
|
+
var readied;
|
|
600
|
+
async function cryptoReady() {
|
|
601
|
+
readied ??= sodium.ready;
|
|
602
|
+
await readied;
|
|
603
|
+
}
|
|
604
|
+
var ENVELOPE_MAX_AGE_MS = 24 * 60 * 6e4;
|
|
605
|
+
var EnvelopeDirection = z6.enum(["payload", "result"]);
|
|
606
|
+
var SealedEnvelope = z6.object({
|
|
607
|
+
/** Base64url `crypto_box_seal` output over the signed plaintext. */
|
|
608
|
+
ciphertext: z6.string().min(1),
|
|
609
|
+
/** Who this was sealed to — the recipient checks it is them. */
|
|
610
|
+
recipientKeyId: z6.string().min(1),
|
|
611
|
+
/** Who signed it — the recipient checks this against its pin. */
|
|
612
|
+
senderKeyId: z6.string().min(1),
|
|
613
|
+
direction: EnvelopeDirection,
|
|
614
|
+
/**
|
|
615
|
+
* When this ciphertext stops being worth keeping.
|
|
616
|
+
*
|
|
617
|
+
* Carried *on* the envelope rather than recomputed by the opener. An
|
|
618
|
+
* earlier version derived it from the job's creation time, which meant
|
|
619
|
+
* two systems had to agree on a timestamp to the millisecond — and they
|
|
620
|
+
* did not, once a real database rounded it. A bound value that has to be
|
|
621
|
+
* reconstructed is a bound value that eventually is not.
|
|
622
|
+
*
|
|
623
|
+
* Not trusted as written: it is also inside the signature, so a changed
|
|
624
|
+
* deadline fails to verify.
|
|
625
|
+
*/
|
|
626
|
+
deadlineAt: z6.number().int().positive()
|
|
627
|
+
}).strict();
|
|
628
|
+
function signedBody(context, plaintext) {
|
|
629
|
+
return Buffer.from(
|
|
630
|
+
JSON.stringify({
|
|
631
|
+
v: "byollm/v1/envelope",
|
|
632
|
+
jobId: context.jobId,
|
|
633
|
+
senderKeyId: context.senderKeyId,
|
|
634
|
+
recipientKeyId: context.recipientKeyId,
|
|
635
|
+
deadlineAt: context.deadlineAt,
|
|
636
|
+
direction: context.direction,
|
|
637
|
+
plaintext
|
|
638
|
+
}),
|
|
639
|
+
"utf8"
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
var rawX25519 = (key, part) => {
|
|
643
|
+
const jwk = key.export({ format: "jwk" });
|
|
644
|
+
const value = part === "x" ? jwk.x : jwk.d;
|
|
645
|
+
if (typeof value !== "string") throw new Error("not an X25519 key");
|
|
646
|
+
return new Uint8Array(Buffer.from(value, "base64url"));
|
|
647
|
+
};
|
|
648
|
+
async function seal(input) {
|
|
649
|
+
await cryptoReady();
|
|
650
|
+
const body = signedBody(input.context, input.plaintext);
|
|
651
|
+
const signature = signWith(input.senderKeys, body);
|
|
652
|
+
const inner = JSON.stringify({ body: body.toString("base64url"), signature });
|
|
653
|
+
const recipient = new Uint8Array(
|
|
654
|
+
Buffer.from(input.recipientEncryptionPublic, "base64url")
|
|
655
|
+
);
|
|
656
|
+
const ciphertext = sodium.crypto_box_seal(
|
|
657
|
+
new Uint8Array(Buffer.from(inner, "utf8")),
|
|
658
|
+
recipient
|
|
659
|
+
);
|
|
660
|
+
return {
|
|
661
|
+
ciphertext: Buffer.from(ciphertext).toString("base64url"),
|
|
662
|
+
recipientKeyId: input.context.recipientKeyId,
|
|
663
|
+
senderKeyId: input.context.senderKeyId,
|
|
664
|
+
direction: input.context.direction,
|
|
665
|
+
deadlineAt: input.context.deadlineAt
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
async function open(input) {
|
|
669
|
+
await cryptoReady();
|
|
670
|
+
const { envelope, expected } = input;
|
|
671
|
+
if (envelope.recipientKeyId !== expected.recipientKeyId || envelope.senderKeyId !== expected.senderKeyId || envelope.direction !== expected.direction) {
|
|
672
|
+
return { ok: false, reason: "not-for-us" };
|
|
673
|
+
}
|
|
674
|
+
let inner;
|
|
675
|
+
try {
|
|
676
|
+
const priv = createPrivateKey2({
|
|
677
|
+
key: Buffer.from(input.recipientKeys.encryptionPrivate, "base64"),
|
|
678
|
+
type: "pkcs8",
|
|
679
|
+
format: "der"
|
|
680
|
+
});
|
|
681
|
+
const pub = createPublicKey2(priv);
|
|
682
|
+
const opened = sodium.crypto_box_seal_open(
|
|
683
|
+
new Uint8Array(Buffer.from(envelope.ciphertext, "base64url")),
|
|
684
|
+
rawX25519(pub, "x"),
|
|
685
|
+
rawX25519(priv, "d")
|
|
686
|
+
);
|
|
687
|
+
inner = Buffer.from(opened).toString("utf8");
|
|
688
|
+
} catch {
|
|
689
|
+
return { ok: false, reason: "unopenable" };
|
|
690
|
+
}
|
|
691
|
+
let parsed;
|
|
692
|
+
try {
|
|
693
|
+
parsed = JSON.parse(inner);
|
|
694
|
+
} catch {
|
|
695
|
+
return { ok: false, reason: "malformed" };
|
|
696
|
+
}
|
|
697
|
+
if (typeof parsed.body !== "string" || typeof parsed.signature !== "string") {
|
|
698
|
+
return { ok: false, reason: "malformed" };
|
|
699
|
+
}
|
|
700
|
+
const body = Buffer.from(parsed.body, "base64url");
|
|
701
|
+
if (!verifyWith(input.senderIdentityPublic, body, parsed.signature)) {
|
|
702
|
+
return { ok: false, reason: "bad-signature" };
|
|
703
|
+
}
|
|
704
|
+
let claims;
|
|
705
|
+
try {
|
|
706
|
+
claims = JSON.parse(body.toString("utf8"));
|
|
707
|
+
} catch {
|
|
708
|
+
return { ok: false, reason: "malformed" };
|
|
709
|
+
}
|
|
710
|
+
if (claims["jobId"] !== expected.jobId || claims["senderKeyId"] !== expected.senderKeyId || claims["recipientKeyId"] !== expected.recipientKeyId || claims["deadlineAt"] !== envelope.deadlineAt || claims["direction"] !== expected.direction) {
|
|
711
|
+
return { ok: false, reason: "context-mismatch" };
|
|
712
|
+
}
|
|
713
|
+
if (typeof claims["plaintext"] !== "string") {
|
|
714
|
+
return { ok: false, reason: "malformed" };
|
|
715
|
+
}
|
|
716
|
+
return { ok: true, plaintext: claims["plaintext"] };
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/signing.ts
|
|
720
|
+
import { createHash as createHash2 } from "crypto";
|
|
721
|
+
import { z as z7 } from "zod";
|
|
722
|
+
var MAX_CLOCK_SKEW_MS = 12e4;
|
|
723
|
+
var RequestSignature = z7.object({
|
|
724
|
+
/** Which runner is calling. The server looks up its pinned identity. */
|
|
725
|
+
runnerId: z7.string().min(1),
|
|
726
|
+
/** Epoch ms, bounded by {@link MAX_CLOCK_SKEW_MS}. */
|
|
727
|
+
issuedAt: z7.number().int().positive(),
|
|
728
|
+
/** Base64url Ed25519 signature over {@link canonicalRequest}. */
|
|
729
|
+
signature: z7.string().min(1)
|
|
730
|
+
}).strict();
|
|
731
|
+
function canonicalRequest(input) {
|
|
732
|
+
const digest = createHash2("sha256").update(input.body, "utf8").digest("hex");
|
|
733
|
+
return Buffer.from(
|
|
734
|
+
[
|
|
735
|
+
"byollm/v1/request",
|
|
736
|
+
input.endpoint,
|
|
737
|
+
input.runnerId,
|
|
738
|
+
String(input.issuedAt),
|
|
739
|
+
digest
|
|
740
|
+
].join("\n"),
|
|
741
|
+
"utf8"
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
function signRequest(keys, input) {
|
|
745
|
+
return {
|
|
746
|
+
runnerId: input.runnerId,
|
|
747
|
+
issuedAt: input.issuedAt,
|
|
748
|
+
signature: signWith(keys, canonicalRequest(input))
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
function signSiteRequest(keys, input) {
|
|
752
|
+
return signRequest(keys, {
|
|
753
|
+
endpoint: siteEndpoint(input.endpoint),
|
|
754
|
+
runnerId: input.siteId,
|
|
755
|
+
issuedAt: input.issuedAt,
|
|
756
|
+
body: input.body
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
function verifySiteRequest(input) {
|
|
760
|
+
return verifyRequest({
|
|
761
|
+
...input,
|
|
762
|
+
endpoint: siteEndpoint(input.endpoint)
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
var siteEndpoint = (endpoint) => `site/${endpoint}`;
|
|
766
|
+
function verifyRequest(input) {
|
|
767
|
+
const skew = input.maxSkewMs ?? MAX_CLOCK_SKEW_MS;
|
|
768
|
+
if (Math.abs(input.now - input.signature.issuedAt) > skew) return "stale";
|
|
769
|
+
const ok = verifyWith(
|
|
770
|
+
input.identityPublic,
|
|
771
|
+
canonicalRequest({
|
|
772
|
+
endpoint: input.endpoint,
|
|
773
|
+
runnerId: input.signature.runnerId,
|
|
774
|
+
issuedAt: input.signature.issuedAt,
|
|
775
|
+
body: input.body
|
|
776
|
+
}),
|
|
777
|
+
input.signature.signature
|
|
778
|
+
);
|
|
779
|
+
return ok ? null : "bad-signature";
|
|
780
|
+
}
|
|
241
781
|
|
|
242
782
|
// src/musts.ts
|
|
243
783
|
var must = (m) => Object.freeze(m);
|
|
@@ -247,31 +787,78 @@ var MUSTS = Object.freeze({
|
|
|
247
787
|
id: "PAIR_ONE_USER",
|
|
248
788
|
statement: "A runner token MUST be bound to exactly one user; a daemon MUST refuse work not attributable to its paired user.",
|
|
249
789
|
enforcedBy: "both",
|
|
790
|
+
verifiedBy: "conformance",
|
|
250
791
|
source: "byollm_001 \xA7MUSTs"
|
|
251
792
|
}),
|
|
252
793
|
PAIR_INTERACTIVE: must({
|
|
253
794
|
id: "PAIR_INTERACTIVE",
|
|
254
795
|
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
796
|
enforcedBy: "server",
|
|
797
|
+
verifiedBy: "conformance",
|
|
256
798
|
source: "byollm_001 \xA7Endpoints.1"
|
|
257
799
|
}),
|
|
258
800
|
PAIR_CODE_EXPIRES: must({
|
|
259
801
|
id: "PAIR_CODE_EXPIRES",
|
|
260
802
|
statement: "An unapproved device code MUST expire and MUST NOT be redeemable after expiry.",
|
|
261
803
|
enforcedBy: "server",
|
|
804
|
+
verifiedBy: "conformance",
|
|
262
805
|
source: "byollm_001 \xA7Endpoints.1"
|
|
263
806
|
}),
|
|
264
807
|
// ---- Typed job kinds --------------------------------------------------
|
|
808
|
+
VERSION_HANDSHAKE_REQUIRED: must({
|
|
809
|
+
id: "VERSION_HANDSHAKE_REQUIRED",
|
|
810
|
+
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.",
|
|
811
|
+
enforcedBy: "both",
|
|
812
|
+
verifiedBy: "conformance",
|
|
813
|
+
source: "byollm_009 \xA74"
|
|
814
|
+
}),
|
|
815
|
+
KEYS_EXCHANGED_AT_CONSENT: must({
|
|
816
|
+
id: "KEYS_EXCHANGED_AT_CONSENT",
|
|
817
|
+
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.",
|
|
818
|
+
enforcedBy: "both",
|
|
819
|
+
verifiedBy: "conformance",
|
|
820
|
+
source: "byollm_009 \xA75"
|
|
821
|
+
}),
|
|
822
|
+
REQUESTS_SIGNED_NOT_BEARER: must({
|
|
823
|
+
id: "REQUESTS_SIGNED_NOT_BEARER",
|
|
824
|
+
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.",
|
|
825
|
+
enforcedBy: "both",
|
|
826
|
+
verifiedBy: "conformance",
|
|
827
|
+
source: "byollm_009 \xA74.2"
|
|
828
|
+
}),
|
|
829
|
+
LEASE_SCOPED_BY_GRANT: must({
|
|
830
|
+
id: "LEASE_SCOPED_BY_GRANT",
|
|
831
|
+
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.",
|
|
832
|
+
enforcedBy: "both",
|
|
833
|
+
verifiedBy: "conformance",
|
|
834
|
+
source: "byollm_009 \xA74.2"
|
|
835
|
+
}),
|
|
836
|
+
STUB_METADATA_EXHAUSTIVE: must({
|
|
837
|
+
id: "STUB_METADATA_EXHAUSTIVE",
|
|
838
|
+
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.",
|
|
839
|
+
enforcedBy: "both",
|
|
840
|
+
verifiedBy: "conformance",
|
|
841
|
+
source: "byollm_009 \xA76"
|
|
842
|
+
}),
|
|
843
|
+
ENVELOPE_SEALED_AND_SIGNED: must({
|
|
844
|
+
id: "ENVELOPE_SEALED_AND_SIGNED",
|
|
845
|
+
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.",
|
|
846
|
+
enforcedBy: "server",
|
|
847
|
+
verifiedBy: "conformance",
|
|
848
|
+
source: "byollm_009 \xA76"
|
|
849
|
+
}),
|
|
265
850
|
KIND_TYPED_ONLY: must({
|
|
266
851
|
id: "KIND_TYPED_ONLY",
|
|
267
852
|
statement: "Job kinds MUST resolve against handlers baked into the daemon. A daemon MUST refuse an unknown kind rather than guess.",
|
|
268
853
|
enforcedBy: "daemon",
|
|
854
|
+
verifiedBy: "conformance",
|
|
269
855
|
source: "byollm_001 \xA7Jobs are typed data"
|
|
270
856
|
}),
|
|
271
857
|
KIND_NO_CODE: must({
|
|
272
858
|
id: "KIND_NO_CODE",
|
|
273
859
|
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
860
|
enforcedBy: "daemon",
|
|
861
|
+
verifiedBy: "conformance",
|
|
275
862
|
source: "byollm_001 \xA7Jobs are typed data; byollm_004 \xA71"
|
|
276
863
|
}),
|
|
277
864
|
// ---- Capability and claiming -----------------------------------------
|
|
@@ -279,18 +866,21 @@ var MUSTS = Object.freeze({
|
|
|
279
866
|
id: "CLAIM_REQUIRES_CAPABILITY",
|
|
280
867
|
statement: "A daemon MUST NOT be given a job whose kind is absent from its advertised capability matrix.",
|
|
281
868
|
enforcedBy: "both",
|
|
869
|
+
verifiedBy: "conformance",
|
|
282
870
|
source: "byollm_001 \xA7MUSTs"
|
|
283
871
|
}),
|
|
284
872
|
CAPABILITY_IS_DETECTED: must({
|
|
285
873
|
id: "CAPABILITY_IS_DETECTED",
|
|
286
874
|
statement: "An advertised capability matrix MUST be the intersection of owner config and detected, healthy reality \u2014 never config alone.",
|
|
287
875
|
enforcedBy: "daemon",
|
|
876
|
+
verifiedBy: "conformance",
|
|
288
877
|
source: "byollm_002 \xA7Routing"
|
|
289
878
|
}),
|
|
290
879
|
CLAIM_ATOMIC: must({
|
|
291
880
|
id: "CLAIM_ATOMIC",
|
|
292
881
|
statement: "Claiming MUST be atomic: a job MUST NOT be handed to two runners concurrently.",
|
|
293
882
|
enforcedBy: "server",
|
|
883
|
+
verifiedBy: "conformance",
|
|
294
884
|
source: "byollm_001 \xA7Endpoints.2"
|
|
295
885
|
}),
|
|
296
886
|
// ---- Leases -----------------------------------------------------------
|
|
@@ -298,12 +888,14 @@ var MUSTS = Object.freeze({
|
|
|
298
888
|
id: "LEASE_HONORED",
|
|
299
889
|
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
890
|
enforcedBy: "daemon",
|
|
891
|
+
verifiedBy: "conformance",
|
|
301
892
|
source: "byollm_001 \xA7MUSTs"
|
|
302
893
|
}),
|
|
303
894
|
LEASE_RECLAIMABLE: must({
|
|
304
895
|
id: "LEASE_RECLAIMABLE",
|
|
305
896
|
statement: "A lease that expires un-renewed MUST make its job claimable again with no loss of the job.",
|
|
306
897
|
enforcedBy: "server",
|
|
898
|
+
verifiedBy: "conformance",
|
|
307
899
|
source: "byollm_001 \xA7Endpoints.2"
|
|
308
900
|
}),
|
|
309
901
|
// ---- Audience and offer scope ----------------------------------------
|
|
@@ -311,24 +903,56 @@ var MUSTS = Object.freeze({
|
|
|
311
903
|
id: "AUDIENCE_BOTH_SIDES",
|
|
312
904
|
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
905
|
enforcedBy: "both",
|
|
906
|
+
verifiedBy: "conformance",
|
|
314
907
|
source: "byollm_001 \xA7The audience model"
|
|
315
908
|
}),
|
|
316
909
|
SUBSCRIPTION_SELF_LOCK: must({
|
|
317
910
|
id: "SUBSCRIPTION_SELF_LOCK",
|
|
318
911
|
statement: "A subscription-class backend's offer scope MUST be 'self' and MUST NOT be widened by configuration.",
|
|
319
912
|
enforcedBy: "daemon",
|
|
913
|
+
verifiedBy: "conformance",
|
|
320
914
|
source: "byollm_001 \xA7The audience model"
|
|
321
915
|
}),
|
|
916
|
+
METERED_DEFAULTS_SELF: must({
|
|
917
|
+
id: "METERED_DEFAULTS_SELF",
|
|
918
|
+
statement: "A metered backend's effective offer scope MUST be 'self' unless the owner has explicitly acknowledged spending money on others' work.",
|
|
919
|
+
enforcedBy: "daemon",
|
|
920
|
+
verifiedBy: "conformance",
|
|
921
|
+
source: "byollm_007 \xA74"
|
|
922
|
+
}),
|
|
923
|
+
METERED_REQUIRES_CEILING: must({
|
|
924
|
+
id: "METERED_REQUIRES_CEILING",
|
|
925
|
+
statement: "A widened metered backend MUST carry a spend ceiling, and the daemon MUST refuse community work once it is reached.",
|
|
926
|
+
enforcedBy: "daemon",
|
|
927
|
+
verifiedBy: "conformance",
|
|
928
|
+
source: "byollm_007 \xA74"
|
|
929
|
+
}),
|
|
930
|
+
COST_NOT_CONFIGURABLE: must({
|
|
931
|
+
id: "COST_NOT_CONFIGURABLE",
|
|
932
|
+
statement: "A built-in provider's cost class MUST NOT be overridable by configuration.",
|
|
933
|
+
enforcedBy: "daemon",
|
|
934
|
+
verifiedBy: "conformance",
|
|
935
|
+
source: "byollm_007 \xA72"
|
|
936
|
+
}),
|
|
937
|
+
REMOTE_IS_NEVER_FREE: must({
|
|
938
|
+
id: "REMOTE_IS_NEVER_FREE",
|
|
939
|
+
statement: "A generic HTTP backend whose base URL is not loopback or private MUST be treated as metered.",
|
|
940
|
+
enforcedBy: "daemon",
|
|
941
|
+
verifiedBy: "conformance",
|
|
942
|
+
source: "byollm_007 \xA72"
|
|
943
|
+
}),
|
|
322
944
|
NAMED_LOCAL_ALLOWLIST: must({
|
|
323
945
|
id: "NAMED_LOCAL_ALLOWLIST",
|
|
324
946
|
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
947
|
enforcedBy: "daemon",
|
|
948
|
+
verifiedBy: "conformance",
|
|
326
949
|
source: "byollm_001 Rev 1 \xA7B"
|
|
327
950
|
}),
|
|
328
951
|
REFUSAL_NOT_REOFFERED: must({
|
|
329
952
|
id: "REFUSAL_NOT_REOFFERED",
|
|
330
953
|
statement: "A server MUST NOT re-offer a job to a runner that released it with reason 'refused'.",
|
|
331
954
|
enforcedBy: "server",
|
|
955
|
+
verifiedBy: "conformance",
|
|
332
956
|
source: "byollm_001 Rev 1 \xA7B (loop resolved in build review)"
|
|
333
957
|
}),
|
|
334
958
|
// ---- Revocation and cancel -------------------------------------------
|
|
@@ -336,12 +960,14 @@ var MUSTS = Object.freeze({
|
|
|
336
960
|
id: "REVOCATION_HONORED",
|
|
337
961
|
statement: "A revoked daemon MUST stop claiming and MUST abandon in-flight work by the next heartbeat at the latest.",
|
|
338
962
|
enforcedBy: "daemon",
|
|
963
|
+
verifiedBy: "conformance",
|
|
339
964
|
source: "byollm_001 \xA7MUSTs"
|
|
340
965
|
}),
|
|
341
966
|
CANCEL_HONORED: must({
|
|
342
967
|
id: "CANCEL_HONORED",
|
|
343
968
|
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
969
|
enforcedBy: "daemon",
|
|
970
|
+
verifiedBy: "conformance",
|
|
345
971
|
source: "byollm_001 Rev 1 \xA7C"
|
|
346
972
|
}),
|
|
347
973
|
// ---- Lifecycle, dependencies, delivery -------------------------------
|
|
@@ -349,30 +975,35 @@ var MUSTS = Object.freeze({
|
|
|
349
975
|
id: "DEPENDS_ON_GATING",
|
|
350
976
|
statement: "A job MUST NOT be claimable until every job in its dependsOn set has reached the 'ok' state.",
|
|
351
977
|
enforcedBy: "server",
|
|
978
|
+
verifiedBy: "conformance",
|
|
352
979
|
source: "byollm_001 Rev 1 \xA7E"
|
|
353
980
|
}),
|
|
354
981
|
TTL_EXPIRY: must({
|
|
355
982
|
id: "TTL_EXPIRY",
|
|
356
983
|
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
984
|
enforcedBy: "server",
|
|
985
|
+
verifiedBy: "conformance",
|
|
358
986
|
source: "byollm_001 Rev 1 \xA7D (TTL clock resolved in build review)"
|
|
359
987
|
}),
|
|
360
988
|
NO_RUNNER_SIGNAL: must({
|
|
361
989
|
id: "NO_RUNNER_SIGNAL",
|
|
362
990
|
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
991
|
enforcedBy: "server",
|
|
992
|
+
verifiedBy: "conformance",
|
|
364
993
|
source: "byollm_001 Rev 1 \xA7D"
|
|
365
994
|
}),
|
|
366
995
|
RESULT_IDEMPOTENT: must({
|
|
367
996
|
id: "RESULT_IDEMPOTENT",
|
|
368
997
|
statement: "Result submission MUST be idempotent by job id; the first terminal outcome wins and later submissions MUST NOT change it.",
|
|
369
998
|
enforcedBy: "server",
|
|
999
|
+
verifiedBy: "conformance",
|
|
370
1000
|
source: "byollm_001 \xA7Endpoints.4"
|
|
371
1001
|
}),
|
|
372
1002
|
RESULT_PROVENANCE: must({
|
|
373
1003
|
id: "RESULT_PROVENANCE",
|
|
374
1004
|
statement: "A result from a non-'self' job MUST carry its provenance (audience and runner) to the delivery seam so an app never treats volunteer output as first-party.",
|
|
375
1005
|
enforcedBy: "server",
|
|
1006
|
+
verifiedBy: "conformance",
|
|
376
1007
|
source: "byollm_003 Rev 1 \xA7Return-trip"
|
|
377
1008
|
}),
|
|
378
1009
|
// ---- The trust surface -------------------------------------------------
|
|
@@ -380,6 +1011,7 @@ var MUSTS = Object.freeze({
|
|
|
380
1011
|
id: "INGRESS_LOGGED_BEFORE_EXECUTION",
|
|
381
1012
|
statement: "Every executed prompt MUST be appended to the local ingress log before execution begins.",
|
|
382
1013
|
enforcedBy: "daemon",
|
|
1014
|
+
verifiedBy: "conformance",
|
|
383
1015
|
source: "byollm_001 \xA7MUSTs"
|
|
384
1016
|
}),
|
|
385
1017
|
// ---- Execution isolation (byollm_004) ---------------------------------
|
|
@@ -387,178 +1019,263 @@ var MUSTS = Object.freeze({
|
|
|
387
1019
|
id: "NO_SHELL_INTERPOLATION",
|
|
388
1020
|
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
1021
|
enforcedBy: "daemon",
|
|
1022
|
+
verifiedBy: "adversarial",
|
|
390
1023
|
source: "byollm_004 \xA72"
|
|
391
1024
|
}),
|
|
392
1025
|
NO_PAYLOAD_ROUTING: must({
|
|
393
1026
|
id: "NO_PAYLOAD_ROUTING",
|
|
394
1027
|
statement: "Model, backend, base URL, and flags MUST come from owner config only; a payload MUST NOT influence any of them.",
|
|
395
1028
|
enforcedBy: "daemon",
|
|
1029
|
+
verifiedBy: "adversarial",
|
|
396
1030
|
source: "byollm_004 \xA72"
|
|
397
1031
|
}),
|
|
398
1032
|
STRIPPED_CHILD_ENV: must({
|
|
399
1033
|
id: "STRIPPED_CHILD_ENV",
|
|
400
1034
|
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
1035
|
enforcedBy: "daemon",
|
|
1036
|
+
verifiedBy: "adversarial",
|
|
402
1037
|
source: "byollm_004 \xA72"
|
|
403
1038
|
}),
|
|
404
1039
|
HTTP_BASE_URL_SAFE: must({
|
|
405
1040
|
id: "HTTP_BASE_URL_SAFE",
|
|
406
1041
|
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
1042
|
enforcedBy: "daemon",
|
|
1043
|
+
verifiedBy: "adversarial",
|
|
408
1044
|
source: "byollm_004 Rev 1 \xA7Backend taxonomy"
|
|
409
1045
|
}),
|
|
410
1046
|
OUTPUT_INERT: must({
|
|
411
1047
|
id: "OUTPUT_INERT",
|
|
412
1048
|
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
1049
|
enforcedBy: "daemon",
|
|
1050
|
+
verifiedBy: "adversarial",
|
|
414
1051
|
source: "byollm_004 \xA72"
|
|
415
1052
|
}),
|
|
416
1053
|
COMMUNITY_BUDGETS: must({
|
|
417
1054
|
id: "COMMUNITY_BUDGETS",
|
|
418
1055
|
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
1056
|
enforcedBy: "daemon",
|
|
1057
|
+
verifiedBy: "adversarial",
|
|
420
1058
|
source: "byollm_004 \xA74"
|
|
421
1059
|
})
|
|
422
1060
|
});
|
|
423
1061
|
var MUST_IDS = Object.freeze(Object.keys(MUSTS));
|
|
1062
|
+
function mustsVerifiedBy(kind) {
|
|
1063
|
+
return MUST_IDS.filter((id) => MUSTS[id].verifiedBy === kind);
|
|
1064
|
+
}
|
|
424
1065
|
|
|
425
1066
|
// src/wire.ts
|
|
426
|
-
import { z as
|
|
1067
|
+
import { z as z8 } from "zod";
|
|
427
1068
|
var PROTOCOL_VERSION = "0";
|
|
1069
|
+
var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze([
|
|
1070
|
+
PROTOCOL_VERSION
|
|
1071
|
+
]);
|
|
1072
|
+
var MIN_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0] ?? PROTOCOL_VERSION;
|
|
1073
|
+
function checkProtocolVersion(body) {
|
|
1074
|
+
const declared = typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion") ? body.protocolVersion : void 0;
|
|
1075
|
+
if (typeof declared !== "string" || declared.length === 0) {
|
|
1076
|
+
return {
|
|
1077
|
+
error: "unsupported-protocol-version",
|
|
1078
|
+
message: "this request declared no protocol version. Upgrade the daemon: `npm i -g byollm@alpha`.",
|
|
1079
|
+
supported: SUPPORTED_PROTOCOL_VERSIONS,
|
|
1080
|
+
minimum: MIN_PROTOCOL_VERSION
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
if (!SUPPORTED_PROTOCOL_VERSIONS.includes(declared)) {
|
|
1084
|
+
return {
|
|
1085
|
+
error: "unsupported-protocol-version",
|
|
1086
|
+
message: `this server speaks protocol ${SUPPORTED_PROTOCOL_VERSIONS.join(", ")} and the daemon asked for ${declared}. ` + (declared < MIN_PROTOCOL_VERSION ? "Upgrade the daemon: `npm i -g byollm@alpha`." : "This daemon is newer than the server; the server needs upgrading."),
|
|
1087
|
+
supported: SUPPORTED_PROTOCOL_VERSIONS,
|
|
1088
|
+
minimum: MIN_PROTOCOL_VERSION
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
428
1093
|
var PROTOCOL_PREFIX = "/byollm";
|
|
429
1094
|
var ENDPOINTS = Object.freeze([
|
|
430
1095
|
"pair",
|
|
431
1096
|
"claim",
|
|
1097
|
+
"fetch",
|
|
432
1098
|
"heartbeat",
|
|
433
1099
|
"result",
|
|
434
1100
|
"release"
|
|
435
1101
|
]);
|
|
436
|
-
var Capability =
|
|
1102
|
+
var Capability = z8.object({
|
|
437
1103
|
kind: JobKind,
|
|
438
1104
|
backendId: BackendIdSchema,
|
|
439
1105
|
backendClass: BackendClass,
|
|
440
|
-
model:
|
|
1106
|
+
model: z8.string().min(1),
|
|
441
1107
|
offerScope: OfferScope
|
|
442
1108
|
}).strict();
|
|
443
|
-
var CapabilityMatrix =
|
|
444
|
-
var PairStartRequest =
|
|
445
|
-
protocolVersion:
|
|
446
|
-
action:
|
|
447
|
-
daemon:
|
|
448
|
-
version:
|
|
1109
|
+
var CapabilityMatrix = z8.array(Capability);
|
|
1110
|
+
var PairStartRequest = z8.object({
|
|
1111
|
+
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1112
|
+
action: z8.literal("start"),
|
|
1113
|
+
daemon: z8.object({
|
|
1114
|
+
version: z8.string().min(1),
|
|
449
1115
|
/** Shown in the app's runner list so a user can tell their machines apart. */
|
|
450
|
-
label:
|
|
451
|
-
platform:
|
|
1116
|
+
label: z8.string().min(1).max(120),
|
|
1117
|
+
platform: z8.enum(["darwin", "linux", "win32"])
|
|
452
1118
|
}),
|
|
1119
|
+
/**
|
|
1120
|
+
* This machine's public keys (byollm_009 §5).
|
|
1121
|
+
*
|
|
1122
|
+
* Pairing is where the two parties learn each other's identities, because
|
|
1123
|
+
* it is the one moment a human is already deciding to trust: the approval
|
|
1124
|
+
* click. A key exchanged anywhere else would be a key nobody chose.
|
|
1125
|
+
*/
|
|
1126
|
+
device: PublicIdentity,
|
|
453
1127
|
capabilities: CapabilityMatrix
|
|
454
1128
|
}).strict();
|
|
455
|
-
var PairStartResponse =
|
|
1129
|
+
var PairStartResponse = z8.object({
|
|
456
1130
|
/** Secret the daemon polls with. Never shown to the user. */
|
|
457
|
-
deviceCode:
|
|
1131
|
+
deviceCode: z8.string().min(20),
|
|
458
1132
|
/** Short code the user reads and confirms in the browser. */
|
|
459
|
-
userCode:
|
|
1133
|
+
userCode: z8.string().min(4).max(16),
|
|
460
1134
|
/** Where the user approves. Must be on the server's own origin. */
|
|
461
|
-
verificationUrl:
|
|
1135
|
+
verificationUrl: z8.url(),
|
|
462
1136
|
/** Epoch ms after which the code is dead ({@link MUSTS.PAIR_CODE_EXPIRES}). */
|
|
463
|
-
expiresAt:
|
|
1137
|
+
expiresAt: z8.number().int().positive(),
|
|
464
1138
|
/** How often the daemon may poll. */
|
|
465
|
-
pollIntervalMs:
|
|
1139
|
+
pollIntervalMs: z8.number().int().min(500).max(6e4)
|
|
466
1140
|
}).strict();
|
|
467
|
-
var PairPollRequest =
|
|
468
|
-
protocolVersion:
|
|
469
|
-
action:
|
|
470
|
-
deviceCode:
|
|
1141
|
+
var PairPollRequest = z8.object({
|
|
1142
|
+
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1143
|
+
action: z8.literal("poll"),
|
|
1144
|
+
deviceCode: z8.string().min(20)
|
|
471
1145
|
}).strict();
|
|
472
|
-
var PairPollResponse =
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
status:
|
|
1146
|
+
var PairPollResponse = z8.discriminatedUnion("status", [
|
|
1147
|
+
z8.object({ status: z8.literal("pending") }).strict(),
|
|
1148
|
+
z8.object({ status: z8.literal("denied") }).strict(),
|
|
1149
|
+
z8.object({ status: z8.literal("expired") }).strict(),
|
|
1150
|
+
z8.object({
|
|
1151
|
+
status: z8.literal("approved"),
|
|
478
1152
|
/** Bearer token for every later call. Scoped to exactly one user. */
|
|
479
|
-
runnerToken:
|
|
480
|
-
runnerId:
|
|
1153
|
+
runnerToken: z8.string().min(20),
|
|
1154
|
+
runnerId: z8.string().min(1),
|
|
481
1155
|
/** The app's id for the approving user — this daemon's owner forever. */
|
|
482
|
-
owner:
|
|
1156
|
+
owner: z8.string().min(1),
|
|
483
1157
|
/** Display name for the trust UI, if the app offers one. */
|
|
484
|
-
ownerLabel:
|
|
1158
|
+
ownerLabel: z8.string().optional(),
|
|
1159
|
+
/**
|
|
1160
|
+
* The site's public keys, for the daemon to pin (byollm_009 §5).
|
|
1161
|
+
*
|
|
1162
|
+
* Returned only on approval — a pending or denied poll learns nothing,
|
|
1163
|
+
* so an unapproved code cannot be used to enumerate a site's keys.
|
|
1164
|
+
*/
|
|
1165
|
+
site: PublicIdentity
|
|
485
1166
|
}).strict()
|
|
486
1167
|
]);
|
|
487
|
-
var PairRequest =
|
|
1168
|
+
var PairRequest = z8.discriminatedUnion("action", [
|
|
488
1169
|
PairStartRequest,
|
|
489
1170
|
PairPollRequest
|
|
490
1171
|
]);
|
|
491
|
-
var ClaimRequest =
|
|
492
|
-
protocolVersion:
|
|
493
|
-
runnerId:
|
|
1172
|
+
var ClaimRequest = z8.object({
|
|
1173
|
+
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1174
|
+
runnerId: z8.string().min(1),
|
|
494
1175
|
/** Re-sent on every claim so a server never matches against a stale matrix. */
|
|
495
1176
|
capabilities: CapabilityMatrix,
|
|
496
1177
|
/** Upper bound on jobs to return; the server may return fewer. */
|
|
497
|
-
max:
|
|
1178
|
+
max: z8.number().int().min(1).max(64)
|
|
498
1179
|
}).strict();
|
|
499
|
-
var ClaimResponse =
|
|
500
|
-
|
|
1180
|
+
var ClaimResponse = z8.object({
|
|
1181
|
+
/**
|
|
1182
|
+
* Stubs, not jobs. The payload arrives from `fetch`, sealed to whichever
|
|
1183
|
+
* device claimed — see {@link JobStub} for the exhaustive metadata list.
|
|
1184
|
+
*/
|
|
1185
|
+
jobs: z8.array(ClaimedStub),
|
|
501
1186
|
/** Lease duration granted, so the daemon knows its renewal deadline. */
|
|
502
|
-
leaseMs:
|
|
1187
|
+
leaseMs: z8.number().int().positive()
|
|
503
1188
|
}).strict();
|
|
504
|
-
var HeartbeatRequest =
|
|
505
|
-
protocolVersion:
|
|
506
|
-
runnerId:
|
|
507
|
-
daemonVersion:
|
|
1189
|
+
var HeartbeatRequest = z8.object({
|
|
1190
|
+
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1191
|
+
runnerId: z8.string().min(1),
|
|
1192
|
+
daemonVersion: z8.string().min(1),
|
|
508
1193
|
capabilities: CapabilityMatrix,
|
|
509
|
-
/**
|
|
510
|
-
|
|
1194
|
+
/**
|
|
1195
|
+
* Leases this daemon believes it holds; the server renews exactly these.
|
|
1196
|
+
*
|
|
1197
|
+
* Lease ids rather than job ids, so a replayed heartbeat cannot renew a
|
|
1198
|
+
* grant the runner no longer holds — see {@link Lease.id}.
|
|
1199
|
+
*/
|
|
1200
|
+
activeLeases: z8.array(
|
|
1201
|
+
z8.object({ jobId: z8.string().min(1), leaseId: z8.string().min(1) })
|
|
1202
|
+
),
|
|
511
1203
|
/** True while the owner has the daemon paused; the server stops offering work. */
|
|
512
|
-
paused:
|
|
1204
|
+
paused: z8.boolean()
|
|
513
1205
|
}).strict();
|
|
514
|
-
var HeartbeatResponse =
|
|
1206
|
+
var HeartbeatResponse = z8.object({
|
|
515
1207
|
/** Once true, the daemon stops claiming and abandons in-flight work. */
|
|
516
|
-
revoked:
|
|
1208
|
+
revoked: z8.boolean(),
|
|
517
1209
|
/**
|
|
518
1210
|
* Per-job cancel (byollm_001 Rev 1 §C). The daemon aborts these jobs'
|
|
519
1211
|
* in-flight backend calls and reports them `canceled`.
|
|
520
1212
|
*/
|
|
521
|
-
cancel:
|
|
1213
|
+
cancel: z8.array(z8.string().min(1)),
|
|
522
1214
|
/** Jobs whose leases were renewed, with their new expiry. */
|
|
523
|
-
leases:
|
|
524
|
-
|
|
525
|
-
jobId:
|
|
526
|
-
expiresAt:
|
|
1215
|
+
leases: z8.array(
|
|
1216
|
+
z8.object({
|
|
1217
|
+
jobId: z8.string().min(1),
|
|
1218
|
+
expiresAt: z8.number().int().positive()
|
|
527
1219
|
}).strict()
|
|
528
1220
|
),
|
|
529
1221
|
/**
|
|
530
1222
|
* Jobs the daemon thinks it holds but the server has reassigned or
|
|
531
1223
|
* expired. The daemon must stop work on these and not report results.
|
|
532
1224
|
*/
|
|
533
|
-
lost:
|
|
1225
|
+
lost: z8.array(z8.string().min(1)),
|
|
534
1226
|
/** Server clock, so a daemon with a skewed clock still honors leases. */
|
|
535
|
-
serverTime:
|
|
1227
|
+
serverTime: z8.number().int().positive()
|
|
536
1228
|
}).strict();
|
|
537
|
-
var
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
1229
|
+
var ResultDisposition = z8.enum(["ok", "error", "canceled"]);
|
|
1230
|
+
var ResultRequest = z8.object({
|
|
1231
|
+
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1232
|
+
runnerId: z8.string().min(1),
|
|
1233
|
+
jobId: z8.string().min(1),
|
|
1234
|
+
/**
|
|
1235
|
+
* The outcome, sealed to the site and signed by the device.
|
|
1236
|
+
*
|
|
1237
|
+
* The return leg of the payload envelope, and sealed for the same reason:
|
|
1238
|
+
* a model's answer is as sensitive as the prompt that produced it, and an
|
|
1239
|
+
* intermediary that cannot read one must not be handed the other.
|
|
1240
|
+
*/
|
|
1241
|
+
envelope: SealedEnvelope,
|
|
1242
|
+
/**
|
|
1243
|
+
* The sealed outcome's discriminator, in the clear.
|
|
1244
|
+
*
|
|
1245
|
+
* Checked against the envelope once opened. It is a routing hint, not a
|
|
1246
|
+
* fact: believing it unverified would let a daemon mark a job `ok` while
|
|
1247
|
+
* sealing an error, and only the app would ever find out.
|
|
1248
|
+
*/
|
|
1249
|
+
disposition: ResultDisposition,
|
|
542
1250
|
/** Which model actually served it, for the result's provenance. */
|
|
543
|
-
model:
|
|
1251
|
+
model: z8.string().min(1),
|
|
544
1252
|
backendClass: BackendClass,
|
|
545
1253
|
/** Wall-clock milliseconds the backend call took. */
|
|
546
|
-
durationMs:
|
|
1254
|
+
durationMs: z8.number().int().nonnegative()
|
|
547
1255
|
}).strict();
|
|
548
|
-
var ResultResponse =
|
|
1256
|
+
var ResultResponse = z8.object({
|
|
549
1257
|
/**
|
|
550
1258
|
* False when the submission lost an idempotency race or the lease was
|
|
551
1259
|
* already gone — the daemon should discard, not retry
|
|
552
1260
|
* ({@link MUSTS.RESULT_IDEMPOTENT}).
|
|
553
1261
|
*/
|
|
554
|
-
accepted:
|
|
1262
|
+
accepted: z8.boolean(),
|
|
555
1263
|
/** The job's state after this submission. */
|
|
556
|
-
state:
|
|
1264
|
+
state: z8.string().min(1)
|
|
557
1265
|
}).strict();
|
|
558
|
-
var ReleaseRequest =
|
|
559
|
-
protocolVersion:
|
|
560
|
-
runnerId:
|
|
561
|
-
|
|
1266
|
+
var ReleaseRequest = z8.object({
|
|
1267
|
+
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1268
|
+
runnerId: z8.string().min(1),
|
|
1269
|
+
/**
|
|
1270
|
+
* Which leases to release — the grant, not just the job.
|
|
1271
|
+
*
|
|
1272
|
+
* A release naming only a job id releases whatever lease exists at the
|
|
1273
|
+
* moment it arrives, which for a replayed request is not the lease the
|
|
1274
|
+
* daemon meant. See {@link Lease.id}.
|
|
1275
|
+
*/
|
|
1276
|
+
leases: z8.array(
|
|
1277
|
+
z8.object({ jobId: z8.string().min(1), leaseId: z8.string().min(1) })
|
|
1278
|
+
),
|
|
562
1279
|
/**
|
|
563
1280
|
* Why, so the app's runner list can say something true.
|
|
564
1281
|
*
|
|
@@ -568,12 +1285,12 @@ var ReleaseRequest = z5.object({
|
|
|
568
1285
|
* stop offering that job to that runner, or the pair would spin between
|
|
569
1286
|
* claim and release forever.
|
|
570
1287
|
*/
|
|
571
|
-
reason:
|
|
1288
|
+
reason: z8.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
|
|
572
1289
|
}).strict();
|
|
573
|
-
var ReleaseResponse =
|
|
574
|
-
released:
|
|
1290
|
+
var ReleaseResponse = z8.object({
|
|
1291
|
+
released: z8.array(z8.string().min(1))
|
|
575
1292
|
}).strict();
|
|
576
|
-
var WireErrorCode =
|
|
1293
|
+
var WireErrorCode = z8.enum([
|
|
577
1294
|
"bad-request",
|
|
578
1295
|
"unsupported-protocol-version",
|
|
579
1296
|
"unauthorized",
|
|
@@ -582,11 +1299,11 @@ var WireErrorCode = z5.enum([
|
|
|
582
1299
|
"rate-limited",
|
|
583
1300
|
"server-error"
|
|
584
1301
|
]);
|
|
585
|
-
var WireError =
|
|
1302
|
+
var WireError = z8.object({
|
|
586
1303
|
error: WireErrorCode,
|
|
587
|
-
message:
|
|
1304
|
+
message: z8.string().min(1),
|
|
588
1305
|
/** Seconds; mirrors Retry-After for `rate-limited` and `server-error`. */
|
|
589
|
-
retryAfter:
|
|
1306
|
+
retryAfter: z8.number().int().nonnegative().optional()
|
|
590
1307
|
}).strict();
|
|
591
1308
|
var ERROR_STATUS = Object.freeze({
|
|
592
1309
|
"bad-request": 400,
|
|
@@ -597,13 +1314,37 @@ var ERROR_STATUS = Object.freeze({
|
|
|
597
1314
|
"rate-limited": 429,
|
|
598
1315
|
"server-error": 500
|
|
599
1316
|
});
|
|
1317
|
+
var FetchRequest = z8.object({
|
|
1318
|
+
protocolVersion: z8.string().min(1),
|
|
1319
|
+
runnerId: z8.string().min(1),
|
|
1320
|
+
jobId: z8.string().min(1),
|
|
1321
|
+
/**
|
|
1322
|
+
* The grant this daemon holds.
|
|
1323
|
+
*
|
|
1324
|
+
* Named, not inferred: a fetch is lease-scoped, and a request that names
|
|
1325
|
+
* only the job would be answerable for whatever lease exists when it
|
|
1326
|
+
* arrives ({@link Lease.id}).
|
|
1327
|
+
*/
|
|
1328
|
+
leaseId: z8.string().min(1)
|
|
1329
|
+
}).strict();
|
|
1330
|
+
var FetchResponse = z8.object({
|
|
1331
|
+
/**
|
|
1332
|
+
* The work, sealed to the device that claimed it — byollm_009 §6.
|
|
1333
|
+
*
|
|
1334
|
+
* Not plaintext. The site opens its own at-rest envelope and re-seals to
|
|
1335
|
+
* the claiming device's key, signed by the site's identity, so the work
|
|
1336
|
+
* is readable only by the machine that took it and only if it came from
|
|
1337
|
+
* the site that machine pinned.
|
|
1338
|
+
*/
|
|
1339
|
+
envelope: SealedEnvelope
|
|
1340
|
+
}).strict();
|
|
600
1341
|
export {
|
|
601
1342
|
AUDIENCES,
|
|
602
1343
|
Audience,
|
|
603
1344
|
BACKENDS,
|
|
604
1345
|
BACKEND_IDS,
|
|
605
|
-
BackendAccount,
|
|
606
1346
|
BackendClass,
|
|
1347
|
+
BackendCost,
|
|
607
1348
|
BackendIdSchema,
|
|
608
1349
|
Capability,
|
|
609
1350
|
CapabilityMatrix,
|
|
@@ -612,9 +1353,14 @@ export {
|
|
|
612
1353
|
ClaimRequest,
|
|
613
1354
|
ClaimResponse,
|
|
614
1355
|
ClaimedJob,
|
|
1356
|
+
ClaimedStub,
|
|
615
1357
|
DeliveredResult,
|
|
616
1358
|
ENDPOINTS,
|
|
1359
|
+
ENVELOPE_MAX_AGE_MS,
|
|
617
1360
|
ERROR_STATUS,
|
|
1361
|
+
EnvelopeDirection,
|
|
1362
|
+
FetchRequest,
|
|
1363
|
+
FetchResponse,
|
|
618
1364
|
GeneratePayload,
|
|
619
1365
|
HeartbeatRequest,
|
|
620
1366
|
HeartbeatResponse,
|
|
@@ -626,8 +1372,11 @@ export {
|
|
|
626
1372
|
JobResultError,
|
|
627
1373
|
JobResultOk,
|
|
628
1374
|
JobState,
|
|
1375
|
+
JobStub,
|
|
629
1376
|
KindedPayload,
|
|
630
1377
|
Lease,
|
|
1378
|
+
MAX_CLOCK_SKEW_MS,
|
|
1379
|
+
MIN_PROTOCOL_VERSION,
|
|
631
1380
|
MUSTS,
|
|
632
1381
|
MUST_IDS,
|
|
633
1382
|
MatchRefusal,
|
|
@@ -641,23 +1390,52 @@ export {
|
|
|
641
1390
|
PairRequest,
|
|
642
1391
|
PairStartRequest,
|
|
643
1392
|
PairStartResponse,
|
|
1393
|
+
PublicIdentity,
|
|
644
1394
|
REFUSAL_MESSAGES,
|
|
645
1395
|
ReleaseRequest,
|
|
646
1396
|
ReleaseResponse,
|
|
1397
|
+
RequestSignature,
|
|
1398
|
+
ResultDisposition,
|
|
647
1399
|
ResultProvenance,
|
|
648
1400
|
ResultRequest,
|
|
649
1401
|
ResultResponse,
|
|
1402
|
+
SIZE_CLASS_LIMITS,
|
|
1403
|
+
SUPPORTED_PROTOCOL_VERSIONS,
|
|
1404
|
+
SealedEnvelope,
|
|
1405
|
+
SizeClass,
|
|
1406
|
+
StoredKeys,
|
|
650
1407
|
TERMINAL_STATES,
|
|
651
1408
|
WireError,
|
|
652
1409
|
WireErrorCode,
|
|
653
1410
|
backendDescriptor,
|
|
654
1411
|
canTransition,
|
|
1412
|
+
canonicalRequest,
|
|
1413
|
+
checkProtocolVersion,
|
|
1414
|
+
cryptoReady,
|
|
655
1415
|
effectiveOfferScope,
|
|
1416
|
+
fingerprint,
|
|
1417
|
+
generateKeys,
|
|
656
1418
|
isBackendId,
|
|
657
1419
|
isJobKind,
|
|
1420
|
+
isLocalHost,
|
|
658
1421
|
isTerminal,
|
|
1422
|
+
keyId,
|
|
659
1423
|
matchAudience,
|
|
1424
|
+
mustsVerifiedBy,
|
|
1425
|
+
open,
|
|
660
1426
|
payloadTextLength,
|
|
661
|
-
provenanceFor
|
|
1427
|
+
provenanceFor,
|
|
1428
|
+
publicIdentityOf,
|
|
1429
|
+
resolveCost,
|
|
1430
|
+
seal,
|
|
1431
|
+
signRequest,
|
|
1432
|
+
signSiteRequest,
|
|
1433
|
+
signWith,
|
|
1434
|
+
sizeClassCeiling,
|
|
1435
|
+
sizeClassOf,
|
|
1436
|
+
verifyPublicIdentity,
|
|
1437
|
+
verifyRequest,
|
|
1438
|
+
verifySiteRequest,
|
|
1439
|
+
verifyWith
|
|
662
1440
|
};
|
|
663
1441
|
//# sourceMappingURL=index.js.map
|