@byollm/protocol 0.1.0-alpha.10 → 0.1.0-alpha.100
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/ABOUT-SHORT.md +7 -0
- package/ABOUT.md +60 -0
- package/README.md +120 -4
- package/dist/chunk-J3HTAGMX.js +242 -0
- package/dist/chunk-J3HTAGMX.js.map +1 -0
- package/dist/index.d.ts +2666 -1532
- package/dist/index.js +1937 -550
- package/dist/index.js.map +1 -1
- package/dist/portable-C6rfiCXi.d.ts +511 -0
- package/dist/portable.d.ts +2 -0
- package/dist/portable.js +45 -0
- package/dist/portable.js.map +1 -0
- package/package.json +8 -2
package/dist/index.js
CHANGED
|
@@ -1,10 +1,207 @@
|
|
|
1
|
-
|
|
1
|
+
import {
|
|
2
|
+
CONSOLE_FRAME_VERSION,
|
|
3
|
+
CONSOLE_MAX_DATA_BYTES,
|
|
4
|
+
ConsoleBye,
|
|
5
|
+
ConsoleFrame,
|
|
6
|
+
ConsoleHello,
|
|
7
|
+
ConsoleResize,
|
|
8
|
+
ConsoleStdin,
|
|
9
|
+
ConsoleStdout,
|
|
10
|
+
ENVELOPE_BODY_VERSION,
|
|
11
|
+
PublicIdentity,
|
|
12
|
+
consoleDataBytes,
|
|
13
|
+
consoleEnvelope,
|
|
14
|
+
consoleOrder,
|
|
15
|
+
decodeConsoleData,
|
|
16
|
+
decodeEnvelopeInner,
|
|
17
|
+
encodeConsoleData,
|
|
18
|
+
encodeEnvelopeInner,
|
|
19
|
+
envelopeSignedBody,
|
|
20
|
+
fromBase64Url,
|
|
21
|
+
toBase64Url
|
|
22
|
+
} from "./chunk-J3HTAGMX.js";
|
|
23
|
+
|
|
24
|
+
// src/wire.ts
|
|
25
|
+
import { z as z9 } from "zod";
|
|
26
|
+
|
|
27
|
+
// src/keys.ts
|
|
28
|
+
import {
|
|
29
|
+
createHash,
|
|
30
|
+
createPrivateKey,
|
|
31
|
+
createPublicKey,
|
|
32
|
+
generateKeyPairSync,
|
|
33
|
+
sign,
|
|
34
|
+
verify
|
|
35
|
+
} from "crypto";
|
|
36
|
+
import { z } from "zod";
|
|
37
|
+
var StoredKeys = z.object({
|
|
38
|
+
version: z.literal(1),
|
|
39
|
+
identityPublic: z.string().min(1),
|
|
40
|
+
identityPrivate: z.string().min(1),
|
|
41
|
+
encryptionPublic: z.string().min(1),
|
|
42
|
+
encryptionPrivate: z.string().min(1),
|
|
43
|
+
encryptionSig: z.string().min(1),
|
|
44
|
+
createdAt: z.number().int().positive()
|
|
45
|
+
}).strict();
|
|
46
|
+
var ENCRYPTION_KEY_CONTEXT = "byollm/v1/encryption-key";
|
|
47
|
+
function rawPublic(key) {
|
|
48
|
+
const jwk = key.export({ format: "jwk" });
|
|
49
|
+
const x = jwk.x;
|
|
50
|
+
if (typeof x !== "string") throw new Error("key has no raw public component");
|
|
51
|
+
return x;
|
|
52
|
+
}
|
|
53
|
+
function importPublic(raw, crv) {
|
|
54
|
+
return createPublicKey({ key: { kty: "OKP", crv, x: raw }, format: "jwk" });
|
|
55
|
+
}
|
|
56
|
+
function importPrivate(stored) {
|
|
57
|
+
return createPrivateKey({
|
|
58
|
+
key: Buffer.from(stored, "base64"),
|
|
59
|
+
type: "pkcs8",
|
|
60
|
+
format: "der"
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
var exportPrivate = (key) => key.export({ type: "pkcs8", format: "der" }).toString("base64");
|
|
64
|
+
function generateKeys(now) {
|
|
65
|
+
const identity = generateKeyPairSync("ed25519");
|
|
66
|
+
const encryption = generateKeyPairSync("x25519");
|
|
67
|
+
const encryptionPublic = rawPublic(encryption.publicKey);
|
|
68
|
+
return {
|
|
69
|
+
version: 1,
|
|
70
|
+
identityPublic: rawPublic(identity.publicKey),
|
|
71
|
+
identityPrivate: exportPrivate(identity.privateKey),
|
|
72
|
+
encryptionPublic,
|
|
73
|
+
encryptionPrivate: exportPrivate(encryption.privateKey),
|
|
74
|
+
encryptionSig: sign(
|
|
75
|
+
null,
|
|
76
|
+
Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${encryptionPublic}`),
|
|
77
|
+
identity.privateKey
|
|
78
|
+
).toString("base64url"),
|
|
79
|
+
createdAt: now
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function publicIdentityOf(keys) {
|
|
83
|
+
return {
|
|
84
|
+
identity: keys.identityPublic,
|
|
85
|
+
encryption: keys.encryptionPublic,
|
|
86
|
+
encryptionSig: keys.encryptionSig
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function verifyPublicIdentity(identity) {
|
|
90
|
+
try {
|
|
91
|
+
return verify(
|
|
92
|
+
null,
|
|
93
|
+
Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${identity.encryption}`),
|
|
94
|
+
importPublic(identity.identity, "Ed25519"),
|
|
95
|
+
Buffer.from(identity.encryptionSig, "base64url")
|
|
96
|
+
);
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function signWith(keys, data) {
|
|
102
|
+
return sign(null, data, importPrivate(keys.identityPrivate)).toString(
|
|
103
|
+
"base64url"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
function verifyWith(identityPublic, data, signature) {
|
|
107
|
+
try {
|
|
108
|
+
return verify(
|
|
109
|
+
null,
|
|
110
|
+
data,
|
|
111
|
+
importPublic(identityPublic, "Ed25519"),
|
|
112
|
+
Buffer.from(signature, "base64url")
|
|
113
|
+
);
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
119
|
+
function fingerprint(identityPublic) {
|
|
120
|
+
const digest = createHash("sha256").update(Buffer.from(identityPublic, "base64url")).digest();
|
|
121
|
+
let bits = 0;
|
|
122
|
+
let value = 0;
|
|
123
|
+
let out = "";
|
|
124
|
+
for (const byte of digest.subarray(0, 15)) {
|
|
125
|
+
value = value << 8 | byte;
|
|
126
|
+
bits += 8;
|
|
127
|
+
while (bits >= 5) {
|
|
128
|
+
out += ALPHABET.charAt(value >>> bits - 5 & 31);
|
|
129
|
+
bits -= 5;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const groups = out.match(/.{1,4}/g) ?? [];
|
|
133
|
+
return `BYOLLM-${groups.join("-")}`;
|
|
134
|
+
}
|
|
135
|
+
var keyId = (identityPublic) => fingerprint(identityPublic);
|
|
136
|
+
|
|
137
|
+
// src/succession.ts
|
|
2
138
|
import { z as z2 } from "zod";
|
|
139
|
+
var SUCCESSION_CONTEXT = "byollm/v1/site-succession";
|
|
140
|
+
var RETIREMENT_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
141
|
+
var MAX_SUCCESSION_CHAIN = 64;
|
|
142
|
+
var Succession = z2.object({
|
|
143
|
+
/**
|
|
144
|
+
* The predecessor's public identity — K1, in full.
|
|
145
|
+
*
|
|
146
|
+
* The whole identity rather than the key id, because a daemon meeting a
|
|
147
|
+
* chain it has not seen before has to *verify* each link, and a key id is
|
|
148
|
+
* a fingerprint: enough to compare, never enough to check a signature.
|
|
149
|
+
*/
|
|
150
|
+
identity: PublicIdentity,
|
|
151
|
+
/** K1's signature over the statement naming K1 and its successor. */
|
|
152
|
+
signature: z2.string().min(1)
|
|
153
|
+
}).strict();
|
|
154
|
+
function successionStatement(fromKeyId, toKeyId) {
|
|
155
|
+
return Buffer.from(`${SUCCESSION_CONTEXT}:${fromKeyId}:${toKeyId}`);
|
|
156
|
+
}
|
|
157
|
+
function signSuccession(previous, next) {
|
|
158
|
+
return {
|
|
159
|
+
identity: {
|
|
160
|
+
identity: previous.identityPublic,
|
|
161
|
+
encryption: previous.encryptionPublic,
|
|
162
|
+
encryptionSig: previous.encryptionSig
|
|
163
|
+
},
|
|
164
|
+
signature: signWith(
|
|
165
|
+
previous,
|
|
166
|
+
successionStatement(keyId(previous.identityPublic), keyId(next.identity))
|
|
167
|
+
)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function verifyLink(link, toKeyId) {
|
|
171
|
+
if (!verifyPublicIdentity(link.identity)) return false;
|
|
172
|
+
return verifyWith(
|
|
173
|
+
link.identity.identity,
|
|
174
|
+
successionStatement(keyId(link.identity.identity), toKeyId),
|
|
175
|
+
link.signature
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
function walkSuccession(input) {
|
|
179
|
+
const { current, chain, approved } = input;
|
|
180
|
+
if (chain.length === 0) return { path: [current], failure: "no-chain" };
|
|
181
|
+
if (chain.length > MAX_SUCCESSION_CHAIN)
|
|
182
|
+
return { path: [current], failure: "too-long" };
|
|
183
|
+
const steps = [...chain].reverse();
|
|
184
|
+
const path = [current];
|
|
185
|
+
let succeeding = current;
|
|
186
|
+
for (const link of steps) {
|
|
187
|
+
if (!verifyLink(link, succeeding)) return { path, failure: "broken-link" };
|
|
188
|
+
const previous = keyId(link.identity.identity);
|
|
189
|
+
path.unshift(previous);
|
|
190
|
+
if (approved(previous)) return { path, from: previous };
|
|
191
|
+
succeeding = previous;
|
|
192
|
+
}
|
|
193
|
+
return { path, failure: "unknown-origin" };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/audience.ts
|
|
197
|
+
import { z as z4 } from "zod";
|
|
3
198
|
|
|
4
199
|
// src/backends.ts
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
var
|
|
200
|
+
import { isIP } from "net";
|
|
201
|
+
import { z as z3 } from "zod";
|
|
202
|
+
var BackendClass = z3.enum(["http", "process"]);
|
|
203
|
+
var BACKEND_CLASSES = Object.freeze(BackendClass.options);
|
|
204
|
+
var BackendCost = z3.enum(["free", "metered", "subscription"]);
|
|
8
205
|
var backend = (b) => Object.freeze(b);
|
|
9
206
|
var BACKENDS = Object.freeze({
|
|
10
207
|
// -- free: local compute, costs electricity ------------------------------
|
|
@@ -162,10 +359,28 @@ var BACKENDS = Object.freeze({
|
|
|
162
359
|
class: "process",
|
|
163
360
|
cost: "subscription",
|
|
164
361
|
adversarialCorpus: "process"
|
|
362
|
+
}),
|
|
363
|
+
/**
|
|
364
|
+
* OpenAI's Codex CLI, on a ChatGPT plan — byollm_016 stage 3.
|
|
365
|
+
*
|
|
366
|
+
* `subscription`, so `SUBSCRIPTION_SELF_LOCK` pins it to its owner's own
|
|
367
|
+
* work whatever the config says. That is load-bearing here in a way it is
|
|
368
|
+
* not for `claude-cli`: Codex is an *agent*, and its default feature set
|
|
369
|
+
* includes a shell tool, browser control and computer use. The daemon
|
|
370
|
+
* disables every one of them, verified against the shipped binary rather
|
|
371
|
+
* than assumed — see `codex-cli.ts` — but the self-lock is the floor under
|
|
372
|
+
* that verification rather than a duplicate of it.
|
|
373
|
+
*/
|
|
374
|
+
"codex-cli": backend({
|
|
375
|
+
id: "codex-cli",
|
|
376
|
+
label: "Codex CLI (your ChatGPT plan)",
|
|
377
|
+
class: "process",
|
|
378
|
+
cost: "subscription",
|
|
379
|
+
adversarialCorpus: "process"
|
|
165
380
|
})
|
|
166
381
|
});
|
|
167
382
|
var BACKEND_IDS = Object.freeze(Object.keys(BACKENDS));
|
|
168
|
-
var BackendIdSchema =
|
|
383
|
+
var BackendIdSchema = z3.enum(
|
|
169
384
|
BACKEND_IDS
|
|
170
385
|
);
|
|
171
386
|
function isBackendId(value) {
|
|
@@ -177,42 +392,93 @@ function backendDescriptor(id) {
|
|
|
177
392
|
function isLocalHost(hostname) {
|
|
178
393
|
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
179
394
|
if (host === "localhost" || host.endsWith(".localhost")) return true;
|
|
180
|
-
|
|
395
|
+
const version = isIP(host);
|
|
396
|
+
if (version === 0) return false;
|
|
397
|
+
if (version === 6) {
|
|
398
|
+
if (host === "::1") return true;
|
|
399
|
+
return /^f[cd]/.test(host);
|
|
400
|
+
}
|
|
181
401
|
if (host.startsWith("127.")) return true;
|
|
182
402
|
if (host.startsWith("10.")) return true;
|
|
183
403
|
if (host.startsWith("192.168.")) return true;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
404
|
+
return /^172\.(1[6-9]|2\d|3[01])\./.test(host);
|
|
405
|
+
}
|
|
406
|
+
function isCloudTaggedModel(model) {
|
|
407
|
+
return /:[^:]*cloud$/.test(model);
|
|
408
|
+
}
|
|
409
|
+
function resolveCost(id, baseUrl, model) {
|
|
410
|
+
return classifyCost(id, baseUrl, model).cost;
|
|
411
|
+
}
|
|
412
|
+
function backendName(id) {
|
|
413
|
+
return BACKENDS[id].label.replace(/\s*\([^)]*\)$/, "");
|
|
187
414
|
}
|
|
188
|
-
function
|
|
415
|
+
function classifyCost(id, baseUrl, model) {
|
|
189
416
|
const declared = BACKENDS[id].cost;
|
|
190
|
-
if (declared !==
|
|
191
|
-
|
|
417
|
+
if (declared === "free" && model !== void 0 && isCloudTaggedModel(model)) {
|
|
418
|
+
return {
|
|
419
|
+
cost: "metered",
|
|
420
|
+
because: `its model tag ends in \`:cloud\`, so the work runs on your provider's cloud account rather than on this machine \u2014 ${backendName(id)} serves hosted models through the same local address as local ones`
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
if (declared !== null) {
|
|
424
|
+
const label = BACKENDS[id].label;
|
|
425
|
+
return {
|
|
426
|
+
cost: declared,
|
|
427
|
+
because: {
|
|
428
|
+
subscription: `${label} runs on an account you subscribe to`,
|
|
429
|
+
metered: `${label} bills per token`,
|
|
430
|
+
free: `${label} runs on this machine`
|
|
431
|
+
}[declared]
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
if (model !== void 0 && isCloudTaggedModel(model)) {
|
|
435
|
+
return {
|
|
436
|
+
cost: "metered",
|
|
437
|
+
because: `its model tag ends in \`:cloud\`, so the work runs on your provider's cloud account rather than on this machine`
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
if (baseUrl === void 0) {
|
|
441
|
+
return {
|
|
442
|
+
cost: "metered",
|
|
443
|
+
because: "it has no address, so where the work runs cannot be checked"
|
|
444
|
+
};
|
|
445
|
+
}
|
|
192
446
|
try {
|
|
193
|
-
return isLocalHost(new URL(baseUrl).hostname) ? "free" : "
|
|
447
|
+
return isLocalHost(new URL(baseUrl).hostname) ? { cost: "free", because: "it runs on this machine" } : {
|
|
448
|
+
cost: "metered",
|
|
449
|
+
because: "its address is not on this machine, so the work leaves it"
|
|
450
|
+
};
|
|
194
451
|
} catch {
|
|
195
|
-
return
|
|
452
|
+
return {
|
|
453
|
+
cost: "metered",
|
|
454
|
+
because: "its address cannot be read, so where the work runs is unknown"
|
|
455
|
+
};
|
|
196
456
|
}
|
|
197
457
|
}
|
|
198
458
|
|
|
199
459
|
// src/audience.ts
|
|
200
|
-
var Audience =
|
|
201
|
-
var OfferScope =
|
|
460
|
+
var Audience = z4.enum(["private", "team"]);
|
|
461
|
+
var OfferScope = z4.enum(["private", "team"]);
|
|
202
462
|
var AUDIENCES = Object.freeze(Audience.options);
|
|
203
463
|
var OFFER_SCOPES = Object.freeze(OfferScope.options);
|
|
204
|
-
var MatchRefusal =
|
|
464
|
+
var MatchRefusal = z4.enum([
|
|
205
465
|
/** The daemon advertises no capability for this kind. */
|
|
206
466
|
"no-capability",
|
|
207
|
-
/** Job is `
|
|
467
|
+
/** Job is `private` but this daemon belongs to a different user. */
|
|
208
468
|
"audience-self-other-owner",
|
|
209
|
-
/**
|
|
469
|
+
/**
|
|
470
|
+
* Job is `team` and nothing this device verified admits the job's owner.
|
|
471
|
+
*
|
|
472
|
+
* The id predates the grant and is kept, because ids are public and cited
|
|
473
|
+
* by conformance output. What it means has not moved: this device was not
|
|
474
|
+
* shown anything it could check.
|
|
475
|
+
*/
|
|
210
476
|
"not-locally-allowed",
|
|
211
|
-
/** Job is `
|
|
477
|
+
/** Job is `team` but the server's own allowlist excludes this runner. */
|
|
212
478
|
"not-in-server-allowlist",
|
|
213
|
-
/** The
|
|
479
|
+
/** The service offers only `private` and the job belongs to someone else. */
|
|
214
480
|
"offer-scope-too-narrow",
|
|
215
|
-
/** The matched backend is subscription-class, which is locked to `
|
|
481
|
+
/** The matched backend is subscription-class, which is locked to `private`. */
|
|
216
482
|
"subscription-self-lock",
|
|
217
483
|
/** The backend spends the owner's money and they have not agreed to share it. */
|
|
218
484
|
"metered-no-spend-consent",
|
|
@@ -222,16 +488,16 @@ var MatchRefusal = z2.enum([
|
|
|
222
488
|
var ALLOWED = Object.freeze({ ok: true });
|
|
223
489
|
var refuse = (refusal) => Object.freeze({ ok: false, refusal });
|
|
224
490
|
function effectiveOfferScope(configured, cost, spend) {
|
|
225
|
-
if (cost === "subscription") return "
|
|
226
|
-
if (cost === "metered" && spend?.acknowledged !== true) return "
|
|
491
|
+
if (cost === "subscription") return "private";
|
|
492
|
+
if (cost === "metered" && spend?.acknowledged !== true) return "private";
|
|
227
493
|
return configured;
|
|
228
494
|
}
|
|
229
495
|
function matchAudience(job, daemon) {
|
|
230
496
|
const sameOwner = job.owner === daemon.owner;
|
|
231
|
-
if (job.audience === "
|
|
497
|
+
if (job.audience === "private" && !sameOwner) {
|
|
232
498
|
return refuse("audience-self-other-owner");
|
|
233
499
|
}
|
|
234
|
-
if (job.audience === "
|
|
500
|
+
if (job.audience === "team" && !sameOwner && job.audienceAllow !== void 0 && !job.audienceAllow.includes(daemon.owner)) {
|
|
235
501
|
return refuse("not-in-server-allowlist");
|
|
236
502
|
}
|
|
237
503
|
const scope = effectiveOfferScope(
|
|
@@ -254,27 +520,212 @@ function matchAudience(job, daemon) {
|
|
|
254
520
|
}
|
|
255
521
|
}
|
|
256
522
|
switch (scope) {
|
|
257
|
-
case "
|
|
523
|
+
case "private":
|
|
258
524
|
return refuse("offer-scope-too-narrow");
|
|
259
|
-
case "
|
|
260
|
-
return daemon.
|
|
261
|
-
case "public":
|
|
262
|
-
return ALLOWED;
|
|
525
|
+
case "team":
|
|
526
|
+
return daemon.admits(job.owner) ? ALLOWED : refuse("not-locally-allowed");
|
|
263
527
|
}
|
|
264
528
|
}
|
|
265
529
|
var REFUSAL_MESSAGES = Object.freeze({
|
|
266
|
-
"no-capability": "no backend on this
|
|
267
|
-
"audience-self-other-owner": "the job is private to its owner and this
|
|
268
|
-
"not-locally-allowed": "
|
|
269
|
-
"not-in-server-allowlist": "the app restricted this job to named runners and this
|
|
270
|
-
"offer-scope-too-narrow": "this
|
|
530
|
+
"no-capability": "no backend on this device is configured and healthy for that job kind",
|
|
531
|
+
"audience-self-other-owner": "the job is private to its owner and this device is paired to someone else",
|
|
532
|
+
"not-locally-allowed": "nothing this device can verify says the job's owner may use it",
|
|
533
|
+
"not-in-server-allowlist": "the app restricted this job to named runners and this device is not one of them",
|
|
534
|
+
"offer-scope-too-narrow": "this service is offered to its owner only (`byollm offer <service> team` to widen)",
|
|
271
535
|
"subscription-self-lock": "subscription-backed models run their owner's work only \u2014 this is a protocol rule, not a setting",
|
|
272
536
|
"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
537
|
"metered-ceiling-reached": "this backend is shared but has reached the spend ceiling its owner set"
|
|
274
538
|
});
|
|
275
539
|
|
|
540
|
+
// src/job.ts
|
|
541
|
+
import { z as z7 } from "zod";
|
|
542
|
+
|
|
543
|
+
// src/grant.ts
|
|
544
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
545
|
+
import { z as z5 } from "zod";
|
|
546
|
+
var GRANT_MAX_AGE_MS = 12e4;
|
|
547
|
+
var CLOCK_SKEW_WARN_MS = 3e4;
|
|
548
|
+
var CLOCK_ATTRIBUTION_MS = 5e3;
|
|
549
|
+
var GRANT_CONTEXT = "byollm/v1/grant";
|
|
550
|
+
var SignedGrant = z5.object({
|
|
551
|
+
/**
|
|
552
|
+
* This grant's own id — what makes it single-use.
|
|
553
|
+
*
|
|
554
|
+
* **Not the job id, and the difference is load-bearing.** Binding
|
|
555
|
+
* single-use to `jobId` would refuse a legitimate retry: a claim that
|
|
556
|
+
* times out is re-claimed, the control plane authors a second grant for
|
|
557
|
+
* the same job, and a device that recorded the job id as spent would
|
|
558
|
+
* reject its own recovery. A fresh id per authorship replays nothing and
|
|
559
|
+
* retries fine.
|
|
560
|
+
*/
|
|
561
|
+
grantId: z5.string().min(1),
|
|
562
|
+
/**
|
|
563
|
+
* The job this grant admits, and only this one.
|
|
564
|
+
*
|
|
565
|
+
* A grant lifted from one job and presented for another is the obvious
|
|
566
|
+
* attack, and this field is why it fails.
|
|
567
|
+
*/
|
|
568
|
+
jobId: z5.string().min(1),
|
|
569
|
+
/**
|
|
570
|
+
* The site the work came from, **as a key id** — byollm-review 2026-08-27.
|
|
571
|
+
*
|
|
572
|
+
* This was `siteId`, holding the site's id in the control plane's
|
|
573
|
+
* namespace, and it was signed by the engine and read by nobody. A signed
|
|
574
|
+
* field nobody checks is not a weak guarantee, it is the appearance of
|
|
575
|
+
* one: the design says "the grant carries the site", and nothing anywhere
|
|
576
|
+
* compared it to anything.
|
|
577
|
+
*
|
|
578
|
+
* It could not be compared. Job ids are chosen per site, so a grant
|
|
579
|
+
* authored for (site A, `job_1`) satisfied every device check against a
|
|
580
|
+
* stub naming (site B, `job_1`) — but the device holds sites only by the
|
|
581
|
+
* key ids it pinned, and had no way to relate a control-plane uuid to
|
|
582
|
+
* one. Checking the field would have meant a lookup through the party the
|
|
583
|
+
* grant exists to distrust.
|
|
584
|
+
*
|
|
585
|
+
* So the namespace changes to the one the device already has, and the
|
|
586
|
+
* name changes with it: this is the same value as {@link JobStub.site},
|
|
587
|
+
* compared directly, no lookup and nothing to believe. The control-plane
|
|
588
|
+
* id is not carried alongside — it had no reader, and keeping an
|
|
589
|
+
* unchecked field beside a checked one is how this hole was dug.
|
|
590
|
+
*/
|
|
591
|
+
site: z5.string().min(1),
|
|
592
|
+
/** Whose job it is — the person the site enqueued for. */
|
|
593
|
+
user: z5.string().min(1),
|
|
594
|
+
/**
|
|
595
|
+
* Whose device it is for.
|
|
596
|
+
*
|
|
597
|
+
* Passed to {@link verifyGrant} rather than read out of the document, for
|
|
598
|
+
* the reason every verifier here takes its subject as an argument: a
|
|
599
|
+
* verifier that recovered the owner from the signed bytes would accept a
|
|
600
|
+
* genuine grant belonging to somebody else and pass every check.
|
|
601
|
+
*/
|
|
602
|
+
owner: z5.string().min(1),
|
|
603
|
+
/** The site purpose this job serves — byollm_016 Amendment L. */
|
|
604
|
+
purpose: z5.string().min(1),
|
|
605
|
+
/** The kind of work. */
|
|
606
|
+
kind: z5.string().min(1),
|
|
607
|
+
/**
|
|
608
|
+
* The service the control plane resolved this (purpose, kind) to, from
|
|
609
|
+
* the user's own mapping.
|
|
610
|
+
*
|
|
611
|
+
* Selection is the control plane's; **offer-consistency is the
|
|
612
|
+
* device's**. A device verifies it actually offers this service, at a
|
|
613
|
+
* scope that includes {@link user}, before running anything.
|
|
614
|
+
*/
|
|
615
|
+
service: z5.string().min(1),
|
|
616
|
+
/** When the control plane signed it — epoch ms, the only anchor for age. */
|
|
617
|
+
issuedAt: z5.number().int().positive(),
|
|
618
|
+
/** Base64url Ed25519 over {@link grantStatement}. */
|
|
619
|
+
signature: z5.string().min(1)
|
|
620
|
+
}).strict();
|
|
621
|
+
var GRANT_SIGNED_FIELDS = Object.freeze(
|
|
622
|
+
Object.keys(SignedGrant.shape).filter((key) => key !== "signature").sort()
|
|
623
|
+
);
|
|
624
|
+
function grantStatement(claims) {
|
|
625
|
+
return Buffer2.from(
|
|
626
|
+
JSON.stringify([
|
|
627
|
+
GRANT_CONTEXT,
|
|
628
|
+
...GRANT_SIGNED_FIELDS.map((field) => claims[field])
|
|
629
|
+
]),
|
|
630
|
+
"utf8"
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
function signGrant(keys, claims) {
|
|
634
|
+
return { ...claims, signature: signWith(keys, grantStatement(claims)) };
|
|
635
|
+
}
|
|
636
|
+
function verifyGrant(input) {
|
|
637
|
+
const { grant, now } = input;
|
|
638
|
+
if (grant.owner !== input.owner) return "wrong-owner";
|
|
639
|
+
if (grant.jobId !== input.jobId) return "wrong-job";
|
|
640
|
+
const age = now - grant.issuedAt;
|
|
641
|
+
if (age < -CLOCK_SKEW_WARN_MS) return "from-the-future";
|
|
642
|
+
if (age > (input.maxAgeMs ?? GRANT_MAX_AGE_MS)) return "expired";
|
|
643
|
+
return verifyWith(
|
|
644
|
+
input.controlPlanePublic,
|
|
645
|
+
grantStatement(grant),
|
|
646
|
+
grant.signature
|
|
647
|
+
) ? null : "bad-signature";
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/json-size.ts
|
|
651
|
+
function jsonLength(value) {
|
|
652
|
+
try {
|
|
653
|
+
return count(value, /* @__PURE__ */ new Set());
|
|
654
|
+
} catch {
|
|
655
|
+
return void 0;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
var OutOfDomain = class extends Error {
|
|
659
|
+
};
|
|
660
|
+
function bail() {
|
|
661
|
+
throw new OutOfDomain("not a JSON value");
|
|
662
|
+
}
|
|
663
|
+
function count(value, seen) {
|
|
664
|
+
if (value === null) return 4;
|
|
665
|
+
switch (typeof value) {
|
|
666
|
+
case "boolean":
|
|
667
|
+
return value ? 4 : 5;
|
|
668
|
+
// true / false
|
|
669
|
+
case "number":
|
|
670
|
+
return Number.isFinite(value) ? String(value).length : 4;
|
|
671
|
+
case "string":
|
|
672
|
+
return stringLength(value);
|
|
673
|
+
case "object":
|
|
674
|
+
break;
|
|
675
|
+
default:
|
|
676
|
+
return bail();
|
|
677
|
+
}
|
|
678
|
+
const object = value;
|
|
679
|
+
if (seen.has(object)) return bail();
|
|
680
|
+
seen.add(object);
|
|
681
|
+
try {
|
|
682
|
+
if (Array.isArray(object)) {
|
|
683
|
+
let total2 = 2 + Math.max(0, object.length - 1);
|
|
684
|
+
for (const element of object) {
|
|
685
|
+
total2 += element === void 0 || typeof element === "function" ? 4 : count(element, seen);
|
|
686
|
+
}
|
|
687
|
+
return total2;
|
|
688
|
+
}
|
|
689
|
+
if ("toJSON" in object) return bail();
|
|
690
|
+
const proto = Object.getPrototypeOf(object);
|
|
691
|
+
if (proto !== Object.prototype) return bail();
|
|
692
|
+
let total = 2;
|
|
693
|
+
let first = true;
|
|
694
|
+
for (const [key, entry] of Object.entries(object)) {
|
|
695
|
+
if (entry === void 0 || typeof entry === "function") continue;
|
|
696
|
+
total += (first ? 0 : 1) + stringLength(key) + 1 + count(entry, seen);
|
|
697
|
+
first = false;
|
|
698
|
+
}
|
|
699
|
+
return total;
|
|
700
|
+
} finally {
|
|
701
|
+
seen.delete(object);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function stringLength(text) {
|
|
705
|
+
let total = 2;
|
|
706
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
707
|
+
const code = text.charCodeAt(index);
|
|
708
|
+
if (code === 34 || code === 92) {
|
|
709
|
+
total += 2;
|
|
710
|
+
} else if (code < 32) {
|
|
711
|
+
total += code === 8 || code === 9 || code === 10 || code === 12 || code === 13 ? 2 : 6;
|
|
712
|
+
} else if (code >= 55296 && code <= 57343) {
|
|
713
|
+
const paired = code <= 56319 && index + 1 < text.length && text.charCodeAt(index + 1) >= 56320 && text.charCodeAt(index + 1) <= 57343;
|
|
714
|
+
if (paired) {
|
|
715
|
+
total += 2;
|
|
716
|
+
index += 1;
|
|
717
|
+
} else {
|
|
718
|
+
total += 6;
|
|
719
|
+
}
|
|
720
|
+
} else {
|
|
721
|
+
total += 1;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return total;
|
|
725
|
+
}
|
|
726
|
+
|
|
276
727
|
// src/kinds.ts
|
|
277
|
-
import { z as
|
|
728
|
+
import { z as z6 } from "zod";
|
|
278
729
|
var PAYLOAD_LIMITS = Object.freeze({
|
|
279
730
|
/** Max characters in any single text field. */
|
|
280
731
|
maxTextChars: 1e6,
|
|
@@ -283,23 +734,36 @@ var PAYLOAD_LIMITS = Object.freeze({
|
|
|
283
734
|
/** Max characters across the whole payload. */
|
|
284
735
|
maxTotalChars: 4e6
|
|
285
736
|
});
|
|
286
|
-
var ChatMessage =
|
|
287
|
-
role:
|
|
288
|
-
content:
|
|
289
|
-
});
|
|
290
|
-
var GeneratePayload = z3.object({
|
|
291
|
-
prompt: z3.string().min(1).max(PAYLOAD_LIMITS.maxTextChars),
|
|
292
|
-
system: z3.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
|
|
737
|
+
var ChatMessage = z6.object({
|
|
738
|
+
role: z6.enum(["system", "user", "assistant"]),
|
|
739
|
+
content: z6.string().max(PAYLOAD_LIMITS.maxTextChars)
|
|
293
740
|
}).strict();
|
|
294
|
-
var
|
|
295
|
-
|
|
296
|
-
system:
|
|
297
|
-
}).strict()
|
|
298
|
-
|
|
741
|
+
var GeneratePayload = z6.object({
|
|
742
|
+
prompt: z6.string().min(1).max(PAYLOAD_LIMITS.maxTextChars),
|
|
743
|
+
system: z6.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
|
|
744
|
+
}).strict().refine(
|
|
745
|
+
(payload) => payload.prompt.length + (payload.system?.length ?? 0) <= PAYLOAD_LIMITS.maxTotalChars,
|
|
746
|
+
{
|
|
747
|
+
message: `payload exceeds ${String(PAYLOAD_LIMITS.maxTotalChars)} characters`
|
|
748
|
+
}
|
|
749
|
+
);
|
|
750
|
+
var ChatPayload = z6.object({
|
|
751
|
+
messages: z6.array(ChatMessage).min(1).max(PAYLOAD_LIMITS.maxMessages),
|
|
752
|
+
system: z6.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
|
|
753
|
+
}).strict().refine(
|
|
754
|
+
(payload) => payload.messages.reduce((sum, m) => sum + m.content.length, 0) + (payload.system?.length ?? 0) <= PAYLOAD_LIMITS.maxTotalChars,
|
|
755
|
+
{
|
|
756
|
+
message: `payload exceeds ${String(PAYLOAD_LIMITS.maxTotalChars)} characters`
|
|
757
|
+
}
|
|
758
|
+
);
|
|
759
|
+
var JobKind = z6.enum(["llm.generate", "llm.chat"]);
|
|
299
760
|
var JOB_KINDS = Object.freeze(JobKind.options);
|
|
300
|
-
var KindedPayload =
|
|
301
|
-
|
|
302
|
-
|
|
761
|
+
var KindedPayload = z6.discriminatedUnion("kind", [
|
|
762
|
+
// Strict on the wrappers too. A union member that strips is a door beside
|
|
763
|
+
// the one that is locked: the payloads inside are strict, and an extra key
|
|
764
|
+
// on the envelope vanished just as quietly.
|
|
765
|
+
z6.object({ kind: z6.literal("llm.generate"), payload: GeneratePayload }).strict(),
|
|
766
|
+
z6.object({ kind: z6.literal("llm.chat"), payload: ChatPayload }).strict()
|
|
303
767
|
]);
|
|
304
768
|
function isJobKind(value) {
|
|
305
769
|
return JOB_KINDS.includes(value);
|
|
@@ -316,8 +780,7 @@ function payloadTextLength(kinded) {
|
|
|
316
780
|
}
|
|
317
781
|
|
|
318
782
|
// src/job.ts
|
|
319
|
-
|
|
320
|
-
var JobState = z4.enum([
|
|
783
|
+
var JobState = z7.enum([
|
|
321
784
|
"queued",
|
|
322
785
|
"claimed",
|
|
323
786
|
"running",
|
|
@@ -349,7 +812,7 @@ var TRANSITIONS = Object.freeze({
|
|
|
349
812
|
function canTransition(from, to) {
|
|
350
813
|
return TRANSITIONS[from].includes(to);
|
|
351
814
|
}
|
|
352
|
-
var Lease =
|
|
815
|
+
var Lease = z7.object({
|
|
353
816
|
/**
|
|
354
817
|
* Identifies *this* grant, not just its holder.
|
|
355
818
|
*
|
|
@@ -364,40 +827,83 @@ var Lease = z4.object({
|
|
|
364
827
|
* and release *is*, per lease, but not across leases, because nothing in
|
|
365
828
|
* the request said which one.
|
|
366
829
|
*/
|
|
367
|
-
id:
|
|
830
|
+
id: z7.string().min(1),
|
|
368
831
|
/** The runner holding the lease. */
|
|
369
|
-
runnerId:
|
|
832
|
+
runnerId: z7.string().min(1),
|
|
370
833
|
/** Epoch milliseconds after which the claim is void. */
|
|
371
|
-
expiresAt:
|
|
372
|
-
});
|
|
373
|
-
var JobPayload =
|
|
374
|
-
var ClaimedJob =
|
|
375
|
-
id:
|
|
834
|
+
expiresAt: z7.number().int().positive()
|
|
835
|
+
}).strict();
|
|
836
|
+
var JobPayload = z7.union([GeneratePayload, ChatPayload]);
|
|
837
|
+
var ClaimedJob = z7.object({
|
|
838
|
+
id: z7.string().min(1),
|
|
376
839
|
kind: JobKind,
|
|
377
840
|
payload: JobPayload,
|
|
378
841
|
audience: Audience,
|
|
379
842
|
/** The app's id for the user who enqueued it. */
|
|
380
|
-
owner:
|
|
381
|
-
/**
|
|
382
|
-
|
|
843
|
+
owner: z7.string().min(1),
|
|
844
|
+
/**
|
|
845
|
+
* Which site's job — V1-3.
|
|
846
|
+
*
|
|
847
|
+
* The stub has always carried it; the opened job did not, so everything
|
|
848
|
+
* downstream of the payload — the ingress line above all — recorded a job
|
|
849
|
+
* id that belongs to a site without saying which. Two sites can choose
|
|
850
|
+
* the same id, and the meter is the product.
|
|
851
|
+
*
|
|
852
|
+
* Optional so a caller assembling a job by hand is not forced to invent
|
|
853
|
+
* one, and so this reads as what it is: a fact about where the work came
|
|
854
|
+
* from, not a second copy of the routing key.
|
|
855
|
+
*/
|
|
856
|
+
site: z7.string().min(1).optional(),
|
|
857
|
+
/**
|
|
858
|
+
* Which of the owner's services runs this — resolved, not requested.
|
|
859
|
+
*
|
|
860
|
+
* The daemon picks the backend from this, so it has to be the answer
|
|
861
|
+
* rather than a wish. On a relayed route it is copied off the **grant**,
|
|
862
|
+
* where a control plane put the person's own mapping and signed it; a
|
|
863
|
+
* site never named it and could not.
|
|
864
|
+
*
|
|
865
|
+
* It used to be what the site asked for, which made a job that selected a
|
|
866
|
+
* non-default service liable to be served by the default instead — the
|
|
867
|
+
* substitution `NO_PAYLOAD_ROUTING` forbids. Amendment L removed the
|
|
868
|
+
* asking; what is left is the answering.
|
|
869
|
+
*
|
|
870
|
+
* Optional, because direct mode has no control plane to resolve anything
|
|
871
|
+
* and the owner's own defaults answer under the ambiguity law.
|
|
872
|
+
*/
|
|
873
|
+
service: z7.string().min(1).optional(),
|
|
874
|
+
/**
|
|
875
|
+
* When the work stops being worth doing — the stub's own TTL, carried on
|
|
876
|
+
* so the daemon can stop at it (B199).
|
|
877
|
+
*
|
|
878
|
+
* **A third clock, and not the lease.** The lease bounds how long this
|
|
879
|
+
* device holds the claim; this bounds how long the answer is wanted. A box
|
|
880
|
+
* ground two chat jobs toward `maxWallClockMs` — ten minutes — while their
|
|
881
|
+
* TTL was two, holding both slots and claiming nothing the whole time.
|
|
882
|
+
*
|
|
883
|
+
* Optional because the field is carried rather than required: a caller
|
|
884
|
+
* assembling a `ClaimedJob` without it gets the unclamped ceiling, which is
|
|
885
|
+
* the behaviour that existed before. The stub always has it, and the
|
|
886
|
+
* runner's own call site already spread it — this makes the type say so.
|
|
887
|
+
*/
|
|
888
|
+
deadlineAt: z7.number().int().positive().optional(),
|
|
383
889
|
lease: Lease
|
|
384
890
|
}).strict();
|
|
385
|
-
var ResultProvenance =
|
|
891
|
+
var ResultProvenance = z7.object({
|
|
386
892
|
/** The audience the job ran under. */
|
|
387
893
|
audience: Audience,
|
|
388
894
|
/** The runner that produced it. */
|
|
389
|
-
runnerId:
|
|
895
|
+
runnerId: z7.string().min(1),
|
|
390
896
|
/** The runner owner's id in this app's namespace. */
|
|
391
|
-
runnerOwner:
|
|
897
|
+
runnerOwner: z7.string().min(1),
|
|
392
898
|
/** Which backend class produced it — an HTTP call or a sandboxed spawn. */
|
|
393
899
|
backendClass: BackendClass,
|
|
394
900
|
/** The model the runner reports having used. */
|
|
395
|
-
model:
|
|
901
|
+
model: z7.string().min(1),
|
|
396
902
|
/**
|
|
397
903
|
* False only for `self` jobs. When true the app MUST treat `text` as
|
|
398
904
|
* untrusted third-party content.
|
|
399
905
|
*/
|
|
400
|
-
untrusted:
|
|
906
|
+
untrusted: z7.boolean()
|
|
401
907
|
}).strict();
|
|
402
908
|
function provenanceFor(input) {
|
|
403
909
|
return {
|
|
@@ -406,37 +912,153 @@ function provenanceFor(input) {
|
|
|
406
912
|
runnerOwner: input.runnerOwner,
|
|
407
913
|
backendClass: input.backendClass,
|
|
408
914
|
model: input.model,
|
|
409
|
-
untrusted: input.audience !== "
|
|
915
|
+
untrusted: input.audience !== "private"
|
|
410
916
|
};
|
|
411
917
|
}
|
|
412
|
-
var
|
|
413
|
-
|
|
414
|
-
|
|
918
|
+
var StopReasonSchema = z7.enum([
|
|
919
|
+
"end",
|
|
920
|
+
"length",
|
|
921
|
+
"stop-sequence",
|
|
922
|
+
"unknown"
|
|
923
|
+
]);
|
|
924
|
+
var RunMetadata = z7.object({
|
|
925
|
+
/** Which model actually served it. */
|
|
926
|
+
model: z7.string().min(1),
|
|
927
|
+
backendClass: BackendClass,
|
|
928
|
+
/** Wall-clock milliseconds the backend call took. */
|
|
929
|
+
durationMs: z7.number().int().nonnegative(),
|
|
930
|
+
/**
|
|
931
|
+
* Why generation stopped — B064 step 4.
|
|
932
|
+
*
|
|
933
|
+
* Here rather than on {@link JobResultOk} because the outcome is the
|
|
934
|
+
* ANSWER and this is the daemon's signed account of how it was produced.
|
|
935
|
+
* Why generation ended is the same kind of fact as how long it took.
|
|
936
|
+
*
|
|
937
|
+
* **Optional because it is meaningless, not because it is new.** A
|
|
938
|
+
* cancelled job has no model to have stopped and an error has no
|
|
939
|
+
* generation to have ended, and `ran` travels on those arms too.
|
|
940
|
+
* Instruction 10 forbids optionality bought for compatibility — a
|
|
941
|
+
* version gate wearing a question mark — and this is not that: it is
|
|
942
|
+
* absent exactly where it would be a fact about nothing. Present on
|
|
943
|
+
* every `ok` result, always, because the whole value is the difference
|
|
944
|
+
* between `end`, `length` and `unknown` and that difference only exists
|
|
945
|
+
* if it is always there.
|
|
946
|
+
*/
|
|
947
|
+
stop: StopReasonSchema.optional(),
|
|
948
|
+
/**
|
|
949
|
+
* Whether the adapter could read a stop signal at all — B105's lesson,
|
|
950
|
+
* carried to the wire.
|
|
951
|
+
*
|
|
952
|
+
* Without this the site re-commits the defect B064's third mapping kind
|
|
953
|
+
* was introduced to fix. `unknown` is TWO facts: an adapter that cannot
|
|
954
|
+
* report one, and an adapter that reported a word we do not map — and
|
|
955
|
+
* `openai-http` maps `stop` and `length` and nothing else, so the second
|
|
956
|
+
* is the common case rather than the corner.
|
|
957
|
+
*
|
|
958
|
+
* A site told only `unknown` would say "we do not know why this stopped"
|
|
959
|
+
* for a `claude-cli` job forever, which is true, and for a
|
|
960
|
+
* `content_filter` result, which is not the same thing at all.
|
|
961
|
+
*/
|
|
962
|
+
stopReported: z7.boolean().optional()
|
|
963
|
+
}).strict();
|
|
964
|
+
var JobResultOk = z7.object({
|
|
965
|
+
outcome: z7.literal("ok"),
|
|
966
|
+
text: z7.string(),
|
|
415
967
|
/** Optional reference to a stored artifact; never a local path. */
|
|
416
|
-
artifactUrl:
|
|
968
|
+
artifactUrl: z7.url().optional()
|
|
417
969
|
}).strict();
|
|
418
|
-
var JobResultError =
|
|
419
|
-
outcome:
|
|
420
|
-
code:
|
|
421
|
-
message:
|
|
970
|
+
var JobResultError = z7.object({
|
|
971
|
+
outcome: z7.literal("error"),
|
|
972
|
+
code: z7.string().min(1),
|
|
973
|
+
message: z7.string().min(1),
|
|
422
974
|
/** Whether the app may reasonably re-enqueue. */
|
|
423
|
-
retryable:
|
|
975
|
+
retryable: z7.boolean()
|
|
424
976
|
}).strict();
|
|
425
|
-
var JobResultCanceled =
|
|
426
|
-
outcome:
|
|
977
|
+
var JobResultCanceled = z7.object({
|
|
978
|
+
outcome: z7.literal("canceled")
|
|
427
979
|
}).strict();
|
|
428
|
-
var JobOutcome =
|
|
980
|
+
var JobOutcome = z7.discriminatedUnion("outcome", [
|
|
429
981
|
JobResultOk,
|
|
430
982
|
JobResultError,
|
|
431
983
|
JobResultCanceled
|
|
432
984
|
]);
|
|
433
|
-
var
|
|
434
|
-
|
|
985
|
+
var RefusalReason = z7.enum([
|
|
986
|
+
/**
|
|
987
|
+
* Two or more services answer this kind and the owner has named no default,
|
|
988
|
+
* so the kind is withheld. Nobody may pick on the owner's behalf — the wrong
|
|
989
|
+
* guess is the metered one.
|
|
990
|
+
*
|
|
991
|
+
* Told apart from its neighbour deliberately, and the line is whether a
|
|
992
|
+
* requester can walk a namespace. There are two kinds; asking about one
|
|
993
|
+
* enumerates nothing they could not learn from what the device advertises,
|
|
994
|
+
* and the difference is actionable — "the owner has not chosen" is fixable
|
|
995
|
+
* by the owner, "the default cannot serve you" is not. It is also already
|
|
996
|
+
* what a team member sees on the devices page: `awaitingDefault` carries
|
|
997
|
+
* exactly this, by kind, for exactly this reason.
|
|
998
|
+
*/
|
|
999
|
+
"default-ambiguity",
|
|
1000
|
+
/**
|
|
1001
|
+
* A default exists and this requester can never use it — byollm_016's
|
|
1002
|
+
* defaults-meet-audiences corner.
|
|
1003
|
+
*
|
|
1004
|
+
* The specimen: an owner's default for `llm.chat` is their Claude
|
|
1005
|
+
* subscription, self-locked by `SUBSCRIPTION_SELF_LOCK`. A team member's
|
|
1006
|
+
* job resolves to it and can never be served by it. That must be a refusal
|
|
1007
|
+
* on the spot, not a wait that expires an hour later looking like nobody
|
|
1008
|
+
* was online.
|
|
1009
|
+
*
|
|
1010
|
+
* Bounded like the value above, and unprobeable for the same reason: the
|
|
1011
|
+
* requester named nothing, so there is no name space to walk.
|
|
1012
|
+
*/
|
|
1013
|
+
"default-unusable"
|
|
1014
|
+
]);
|
|
1015
|
+
var JobRefused = z7.object({
|
|
1016
|
+
outcome: z7.literal("refused"),
|
|
1017
|
+
reason: RefusalReason,
|
|
1018
|
+
/** Plain words for a human reading a log, never parsed. */
|
|
1019
|
+
message: z7.string().min(1)
|
|
1020
|
+
}).strict();
|
|
1021
|
+
var REFUSAL_TEXT = Object.freeze({
|
|
1022
|
+
"default-ambiguity": "this device serves that kind from more than one service and its owner has not chosen which",
|
|
1023
|
+
"default-unusable": "this device's default for that kind cannot run work for you"
|
|
1024
|
+
});
|
|
1025
|
+
var SealedOutcome = z7.object({ outcome: JobOutcome, ran: RunMetadata }).strict();
|
|
1026
|
+
var DeliveredResult = z7.object({
|
|
1027
|
+
jobId: z7.string().min(1),
|
|
435
1028
|
state: JobState,
|
|
436
1029
|
outcome: JobOutcome.optional(),
|
|
437
|
-
provenance: ResultProvenance.optional()
|
|
1030
|
+
provenance: ResultProvenance.optional(),
|
|
1031
|
+
/**
|
|
1032
|
+
* Present, and always `true`, when this did not come from a runner —
|
|
1033
|
+
* {@link MUSTS.FALLBACK_LABELED}.
|
|
1034
|
+
*
|
|
1035
|
+
* The app's own `onNoRunner` value produced it: a hosted model, a cached
|
|
1036
|
+
* answer, an apology. It never travels on the wire, because nothing on
|
|
1037
|
+
* the wire produced it; it exists so that a result which did not come
|
|
1038
|
+
* from the user's own compute cannot be reported as though it did.
|
|
1039
|
+
*
|
|
1040
|
+
* A literal rather than a boolean, so `fallback: false` is not a
|
|
1041
|
+
* spelling anybody can reach for. The absence of this field means a
|
|
1042
|
+
* runner ran the job, and the *server* stamps it — an app cannot supply
|
|
1043
|
+
* a substitute that hides what it is.
|
|
1044
|
+
*/
|
|
1045
|
+
fallback: z7.literal(true).optional()
|
|
438
1046
|
}).strict();
|
|
439
|
-
var SizeClass =
|
|
1047
|
+
var SizeClass = z7.enum(["small", "medium", "large", "unbounded"]);
|
|
1048
|
+
var SIZE_CLASSES = Object.freeze(SizeClass.options);
|
|
1049
|
+
var MAX_ENVELOPE_BYTES = 6 * 1024 * 1024;
|
|
1050
|
+
function envelopeBytes(envelope) {
|
|
1051
|
+
const counted = jsonLength(envelope);
|
|
1052
|
+
if (counted !== void 0) return counted;
|
|
1053
|
+
const serialised = JSON.stringify(envelope);
|
|
1054
|
+
return serialised === void 0 ? 0 : serialised.length;
|
|
1055
|
+
}
|
|
1056
|
+
function describeBytes(bytes) {
|
|
1057
|
+
return `${(Math.ceil(bytes / (1024 * 1024) * 10) / 10).toFixed(1)} MB`;
|
|
1058
|
+
}
|
|
1059
|
+
function tooLargeMessage(input) {
|
|
1060
|
+
return `this message is ${describeBytes(input.bytes)} and the limit is ${describeBytes(input.limit)} \u2014 it is a limit on one message rather than on how many you send. Split the work into smaller jobs and send them separately.`;
|
|
1061
|
+
}
|
|
440
1062
|
var SIZE_CLASS_LIMITS = Object.freeze({
|
|
441
1063
|
small: 4e3,
|
|
442
1064
|
medium: 64e3,
|
|
@@ -451,165 +1073,119 @@ function sizeClassOf(textChars) {
|
|
|
451
1073
|
if (textChars <= SIZE_CLASS_LIMITS.medium) return "medium";
|
|
452
1074
|
return "large";
|
|
453
1075
|
}
|
|
454
|
-
var JobStub =
|
|
455
|
-
id:
|
|
1076
|
+
var JobStub = z7.object({
|
|
1077
|
+
id: z7.string().min(1),
|
|
456
1078
|
kind: JobKind,
|
|
457
1079
|
/** The app's id for the user who enqueued it. */
|
|
458
|
-
owner:
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
1080
|
+
owner: z7.string().min(1),
|
|
1081
|
+
/**
|
|
1082
|
+
* Which site this job belongs to — byollm_009 Amendment A §A.3.
|
|
1083
|
+
*
|
|
1084
|
+
* **The site's identity key id**, not an id somebody assigned it. §6 has
|
|
1085
|
+
* listed `site` since this spec was frozen; the schema never carried it,
|
|
1086
|
+
* which is the drift the amendment closes.
|
|
1087
|
+
*
|
|
1088
|
+
* A key id rather than an opaque handle for one reason above the others:
|
|
1089
|
+
* it makes the stub *self-describing* instead of a pointer into somebody
|
|
1090
|
+
* else's table. A daemon holds this key id already, from pinning, so it
|
|
1091
|
+
* can check `stub.site` against the payload envelope's `senderKeyId`
|
|
1092
|
+
* without a lookup and without trusting the party that routed it. An
|
|
1093
|
+
* opaque id can only be believed.
|
|
1094
|
+
*
|
|
1095
|
+
* It also avoids inventing a second namespace for a thing that has a
|
|
1096
|
+
* canonical one — the shape of finding 41 (two owner namespaces compared
|
|
1097
|
+
* for equality) and of finding fourteen before it.
|
|
1098
|
+
*
|
|
1099
|
+
* Rotation is a designed transition rather than a cost: a site publishes a
|
|
1100
|
+
* new identity signed by the outgoing one, both are valid through an
|
|
1101
|
+
* overlap window, and a daemon re-keys its own map by verifying that
|
|
1102
|
+
* signature against the key it already pinned (§A.3.1).
|
|
1103
|
+
*/
|
|
1104
|
+
site: z7.string().min(1),
|
|
1105
|
+
audience: Audience,
|
|
1106
|
+
// `audienceAllow` is **not** here, and its absence is the enforcement —
|
|
1107
|
+
// cloud_008 §0.2.
|
|
1108
|
+
//
|
|
1109
|
+
// It was a list of the people who may run a job, travelling to every
|
|
1110
|
+
// routing party on every shared job. byollm_001 Rev 1 §B settled who
|
|
1111
|
+
// decides that long before this schema existed: *the daemon's own list
|
|
1112
|
+
// decides, not the server's*, and `allowlist.predicateFor(origin)` is the
|
|
1113
|
+
// enforcement in both lanes. So this was a second answer to a question the
|
|
1114
|
+
// daemon already owned — able only to agree, in which case it was
|
|
1115
|
+
// redundant, or to disagree, in which case nothing said which wins.
|
|
1116
|
+
//
|
|
1117
|
+
// The rule it leaves behind, which decides the next field too: **a class
|
|
1118
|
+
// the router acts on may travel; membership never does.** `audience` stays
|
|
1119
|
+
// for exactly that reason — the relay narrows on it. A roster does not
|
|
1120
|
+
// travel, so `ROSTER_NOT_DISCLOSED` holds here by absence, which is the
|
|
1121
|
+
// strongest way for a MUST to hold.
|
|
1122
|
+
//
|
|
1123
|
+
// The site keeps its own copy on `JobRecord` and still filters candidates
|
|
1124
|
+
// with it before offering. That is server-internal, where the party
|
|
1125
|
+
// holding the list authored it.
|
|
489
1126
|
/**
|
|
490
|
-
*
|
|
1127
|
+
* Which of the site's declared purposes this job serves — Amendment L.
|
|
491
1128
|
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
* and
|
|
1129
|
+
* **A need, never a name.** The site's vocabulary is its own purposes;
|
|
1130
|
+
* the person's is their services; and the two never meet. This field says
|
|
1131
|
+
* "writing-assistant", and a control plane joins it to whatever that
|
|
1132
|
+
* person mapped it to. The site learns only whether the slot was
|
|
1133
|
+
* satisfiable.
|
|
1134
|
+
*
|
|
1135
|
+
* It replaced `service`, which let a site name one of the owner's
|
|
1136
|
+
* services directly. That field is gone from both routes (Amendment L
|
|
1137
|
+
* rider) and its refusal machinery with it — including the collapsed
|
|
1138
|
+
* `select-unavailable`, which existed so that "no such service" and "not
|
|
1139
|
+
* offered to you" could not be told apart. There is nothing left to
|
|
1140
|
+
* probe: **a vocabulary that never crosses the boundary cannot be
|
|
1141
|
+
* enumerated across it**, which is a stronger guarantee than the one the
|
|
1142
|
+
* collapse gave.
|
|
1143
|
+
*
|
|
1144
|
+
* It travels for the reason the absent `audienceAllow` establishes: *a
|
|
1145
|
+
* class the router acts on may travel; membership never does.* A purpose
|
|
1146
|
+
* is a class, and the control plane acts on it.
|
|
1147
|
+
*
|
|
1148
|
+
* Optional because direct mode has no control plane to hold a mapping and
|
|
1149
|
+
* is kind-only: the owner's own config and defaults answer, under the
|
|
1150
|
+
* ambiguity law as shipped. Absent on a relayed route resolves against
|
|
1151
|
+
* the site's reserved purpose, which a site that declared its own
|
|
1152
|
+
* purposes will not have mapped — so the slot reads as unmapped and the
|
|
1153
|
+
* site falls back, loudly enough and without a special case.
|
|
1154
|
+
*
|
|
1155
|
+
* A **stub** field and never a payload field, which is the line
|
|
1156
|
+
* `NO_PAYLOAD_ROUTING` draws: the prompt cannot reach it, so no amount of
|
|
1157
|
+
* user text can influence what runs.
|
|
495
1158
|
*/
|
|
496
|
-
|
|
1159
|
+
purpose: z7.string().min(1).optional(),
|
|
1160
|
+
sizeClass: SizeClass,
|
|
1161
|
+
/** Reserved for byollm_006. False until streaming exists. */
|
|
1162
|
+
streaming: z7.boolean(),
|
|
1163
|
+
/** Epoch ms after which the work is pointless; bounds ciphertext retention. */
|
|
1164
|
+
deadlineAt: z7.number().int().positive()
|
|
497
1165
|
}).strict();
|
|
498
|
-
var
|
|
499
|
-
|
|
500
|
-
|
|
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()
|
|
1166
|
+
var ClaimedStub = JobStub.extend({
|
|
1167
|
+
lease: Lease,
|
|
1168
|
+
grant: SignedGrant.optional()
|
|
506
1169
|
}).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
1170
|
|
|
598
1171
|
// src/envelope.ts
|
|
1172
|
+
import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2 } from "crypto";
|
|
1173
|
+
import sodium from "libsodium-wrappers";
|
|
1174
|
+
import { z as z8 } from "zod";
|
|
599
1175
|
var readied;
|
|
600
1176
|
async function cryptoReady() {
|
|
601
1177
|
readied ??= sodium.ready;
|
|
602
1178
|
await readied;
|
|
603
1179
|
}
|
|
604
1180
|
var ENVELOPE_MAX_AGE_MS = 24 * 60 * 6e4;
|
|
605
|
-
var EnvelopeDirection =
|
|
606
|
-
var SealedEnvelope =
|
|
1181
|
+
var EnvelopeDirection = z8.enum(["payload", "result"]);
|
|
1182
|
+
var SealedEnvelope = z8.object({
|
|
607
1183
|
/** Base64url `crypto_box_seal` output over the signed plaintext. */
|
|
608
|
-
ciphertext:
|
|
1184
|
+
ciphertext: z8.string().min(1),
|
|
609
1185
|
/** Who this was sealed to — the recipient checks it is them. */
|
|
610
|
-
recipientKeyId:
|
|
1186
|
+
recipientKeyId: z8.string().min(1),
|
|
611
1187
|
/** Who signed it — the recipient checks this against its pin. */
|
|
612
|
-
senderKeyId:
|
|
1188
|
+
senderKeyId: z8.string().min(1),
|
|
613
1189
|
direction: EnvelopeDirection,
|
|
614
1190
|
/**
|
|
615
1191
|
* When this ciphertext stops being worth keeping.
|
|
@@ -623,22 +1199,9 @@ var SealedEnvelope = z6.object({
|
|
|
623
1199
|
* Not trusted as written: it is also inside the signature, so a changed
|
|
624
1200
|
* deadline fails to verify.
|
|
625
1201
|
*/
|
|
626
|
-
deadlineAt:
|
|
1202
|
+
deadlineAt: z8.number().int().positive()
|
|
627
1203
|
}).strict();
|
|
628
|
-
|
|
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
|
-
}
|
|
1204
|
+
var signedBody = (context, plaintext) => envelopeSignedBody(context, plaintext);
|
|
642
1205
|
var rawX25519 = (key, part) => {
|
|
643
1206
|
const jwk = key.export({ format: "jwk" });
|
|
644
1207
|
const value = part === "x" ? jwk.x : jwk.d;
|
|
@@ -649,7 +1212,7 @@ async function seal(input) {
|
|
|
649
1212
|
await cryptoReady();
|
|
650
1213
|
const body = signedBody(input.context, input.plaintext);
|
|
651
1214
|
const signature = signWith(input.senderKeys, body);
|
|
652
|
-
const inner =
|
|
1215
|
+
const inner = encodeEnvelopeInner(body, signature);
|
|
653
1216
|
const recipient = new Uint8Array(
|
|
654
1217
|
Buffer.from(input.recipientEncryptionPublic, "base64url")
|
|
655
1218
|
);
|
|
@@ -688,45 +1251,870 @@ async function open(input) {
|
|
|
688
1251
|
} catch {
|
|
689
1252
|
return { ok: false, reason: "unopenable" };
|
|
690
1253
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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");
|
|
1254
|
+
const parsed = decodeEnvelopeInner(inner);
|
|
1255
|
+
if (parsed === void 0) return { ok: false, reason: "malformed" };
|
|
1256
|
+
const body = parsed.body;
|
|
701
1257
|
if (!verifyWith(input.senderIdentityPublic, body, parsed.signature)) {
|
|
702
1258
|
return { ok: false, reason: "bad-signature" };
|
|
703
1259
|
}
|
|
704
1260
|
let claims;
|
|
705
1261
|
try {
|
|
706
|
-
claims = JSON.parse(
|
|
1262
|
+
claims = JSON.parse(new TextDecoder().decode(body));
|
|
707
1263
|
} catch {
|
|
708
1264
|
return { ok: false, reason: "malformed" };
|
|
709
1265
|
}
|
|
710
1266
|
if (claims["jobId"] !== expected.jobId || claims["senderKeyId"] !== expected.senderKeyId || claims["recipientKeyId"] !== expected.recipientKeyId || claims["deadlineAt"] !== envelope.deadlineAt || claims["direction"] !== expected.direction) {
|
|
711
1267
|
return { ok: false, reason: "context-mismatch" };
|
|
712
1268
|
}
|
|
713
|
-
if (typeof claims["plaintext"] !== "string") {
|
|
714
|
-
return { ok: false, reason: "malformed" };
|
|
1269
|
+
if (typeof claims["plaintext"] !== "string") {
|
|
1270
|
+
return { ok: false, reason: "malformed" };
|
|
1271
|
+
}
|
|
1272
|
+
return { ok: true, plaintext: claims["plaintext"] };
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// src/wire.ts
|
|
1276
|
+
var PROTOCOL_VERSION = "1";
|
|
1277
|
+
var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze([
|
|
1278
|
+
PROTOCOL_VERSION
|
|
1279
|
+
]);
|
|
1280
|
+
var MIN_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0] ?? PROTOCOL_VERSION;
|
|
1281
|
+
function declaredVersion(input) {
|
|
1282
|
+
const { body, query } = input;
|
|
1283
|
+
if (typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion")) {
|
|
1284
|
+
return body.protocolVersion;
|
|
1285
|
+
}
|
|
1286
|
+
return query?.get("protocolVersion") ?? void 0;
|
|
1287
|
+
}
|
|
1288
|
+
function checkProtocolVersion(body) {
|
|
1289
|
+
const declared = typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion") ? body.protocolVersion : void 0;
|
|
1290
|
+
if (typeof declared !== "string" || declared.length === 0) {
|
|
1291
|
+
return {
|
|
1292
|
+
error: "unsupported-protocol-version",
|
|
1293
|
+
message: `this request declared no protocol version. Upgrade the daemon: \`${UPGRADE_COMMAND}\`.`,
|
|
1294
|
+
supported: SUPPORTED_PROTOCOL_VERSIONS,
|
|
1295
|
+
minimum: MIN_PROTOCOL_VERSION
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
if (!SUPPORTED_PROTOCOL_VERSIONS.includes(declared)) {
|
|
1299
|
+
return {
|
|
1300
|
+
error: "unsupported-protocol-version",
|
|
1301
|
+
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."),
|
|
1302
|
+
supported: SUPPORTED_PROTOCOL_VERSIONS,
|
|
1303
|
+
minimum: MIN_PROTOCOL_VERSION
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
return null;
|
|
1307
|
+
}
|
|
1308
|
+
var UPGRADE_COMMAND = "npm i -g byollm@latest";
|
|
1309
|
+
var PROTOCOL_PREFIX = "/byollm";
|
|
1310
|
+
var ENDPOINTS = Object.freeze([
|
|
1311
|
+
"pair",
|
|
1312
|
+
"claim",
|
|
1313
|
+
"fetch",
|
|
1314
|
+
"heartbeat",
|
|
1315
|
+
"result",
|
|
1316
|
+
"release"
|
|
1317
|
+
]);
|
|
1318
|
+
var Capability = z9.object({
|
|
1319
|
+
kind: JobKind,
|
|
1320
|
+
/**
|
|
1321
|
+
* The owner's name for the service answering this kind — byollm_016.
|
|
1322
|
+
*
|
|
1323
|
+
* A device advertises *which* of its services serves a kind, not merely
|
|
1324
|
+
* that something does. **A site never sees this**, and never did after
|
|
1325
|
+
* Amendment L: it is what a control plane resolves a person's mapping
|
|
1326
|
+
* against, so that the service a grant names is one this device actually
|
|
1327
|
+
* offers rather than one somebody invented.
|
|
1328
|
+
*
|
|
1329
|
+
* `isDefault` used to sit beside it, saying which row an unselected job
|
|
1330
|
+
* took. Nothing selects any more — a job names a purpose and a person's
|
|
1331
|
+
* mapping names a service — so there is no unselected job for a default
|
|
1332
|
+
* to catch, and the field went with the machinery it served.
|
|
1333
|
+
*/
|
|
1334
|
+
service: z9.string().min(1),
|
|
1335
|
+
backendId: BackendIdSchema,
|
|
1336
|
+
backendClass: BackendClass,
|
|
1337
|
+
model: z9.string().min(1),
|
|
1338
|
+
/**
|
|
1339
|
+
* Models this device's CLI knows about — byollm_017 ruling 3.
|
|
1340
|
+
*
|
|
1341
|
+
* **Suggestions, not a vocabulary.** Free text is always allowed: the
|
|
1342
|
+
* promise is that a model released this morning works this morning, and a
|
|
1343
|
+
* frozen list anywhere a person picks from breaks that on the first day
|
|
1344
|
+
* it matters. What makes free text safe is ruling 2 — a model is probed
|
|
1345
|
+
* before it is stored, so "found is not works" is answered by the device
|
|
1346
|
+
* rather than by a list.
|
|
1347
|
+
*
|
|
1348
|
+
* Announced with the capability rather than kept in the dashboard,
|
|
1349
|
+
* because the answer is "what does THIS device's CLI know" and only the
|
|
1350
|
+
* device can say. A list held cloud-side would be one more thing to
|
|
1351
|
+
* update on release day, and wrong for anybody who had not upgraded.
|
|
1352
|
+
*
|
|
1353
|
+
* Optional, and empty is legal. A backend with nothing to suggest — a
|
|
1354
|
+
* local server serving one model — is not a backend in an error state,
|
|
1355
|
+
* and a reader must not render an absent list as "no models available".
|
|
1356
|
+
*/
|
|
1357
|
+
knownModels: z9.array(z9.string().min(1)).optional(),
|
|
1358
|
+
offerScope: OfferScope
|
|
1359
|
+
}).strict();
|
|
1360
|
+
var CapabilityMatrix = z9.array(Capability);
|
|
1361
|
+
var WithheldKind = z9.object({
|
|
1362
|
+
kind: JobKind,
|
|
1363
|
+
claimants: z9.array(
|
|
1364
|
+
z9.object({ service: z9.string().min(1), offer: OfferScope }).strict()
|
|
1365
|
+
).min(2)
|
|
1366
|
+
}).strict();
|
|
1367
|
+
var GrantRef = z9.object({ jobId: z9.string().min(1), leaseId: z9.string().min(1) }).strict();
|
|
1368
|
+
var PairStartRequest = z9.object({
|
|
1369
|
+
protocolVersion: z9.literal(PROTOCOL_VERSION),
|
|
1370
|
+
action: z9.literal("start"),
|
|
1371
|
+
daemon: z9.object({
|
|
1372
|
+
version: z9.string().min(1),
|
|
1373
|
+
/** Shown in the app's runner list so a user can tell their machines apart. */
|
|
1374
|
+
label: z9.string().min(1).max(120),
|
|
1375
|
+
platform: z9.enum(["darwin", "linux", "win32"])
|
|
1376
|
+
}).strict(),
|
|
1377
|
+
/**
|
|
1378
|
+
* This machine's public keys (byollm_009 §5).
|
|
1379
|
+
*
|
|
1380
|
+
* Pairing is where the two parties learn each other's identities, because
|
|
1381
|
+
* it is the one moment a human is already deciding to trust: the approval
|
|
1382
|
+
* click. A key exchanged anywhere else would be a key nobody chose.
|
|
1383
|
+
*/
|
|
1384
|
+
device: PublicIdentity,
|
|
1385
|
+
capabilities: CapabilityMatrix
|
|
1386
|
+
}).strict();
|
|
1387
|
+
var PairStartResponse = z9.object({
|
|
1388
|
+
/** Secret the daemon polls with. Never shown to the user. */
|
|
1389
|
+
deviceCode: z9.string().min(20),
|
|
1390
|
+
/** Short code the user reads and confirms in the browser. */
|
|
1391
|
+
userCode: z9.string().min(4).max(16),
|
|
1392
|
+
/** Where the user approves. Must be on the server's own origin. */
|
|
1393
|
+
verificationUrl: z9.url(),
|
|
1394
|
+
/** Epoch ms after which the code is dead ({@link MUSTS.PAIR_CODE_EXPIRES}). */
|
|
1395
|
+
expiresAt: z9.number().int().positive(),
|
|
1396
|
+
/** How often the daemon may poll. */
|
|
1397
|
+
pollIntervalMs: z9.number().int().min(500).max(6e4)
|
|
1398
|
+
}).strict();
|
|
1399
|
+
var PairPollRequest = z9.object({
|
|
1400
|
+
protocolVersion: z9.literal(PROTOCOL_VERSION),
|
|
1401
|
+
action: z9.literal("poll"),
|
|
1402
|
+
deviceCode: z9.string().min(20)
|
|
1403
|
+
}).strict();
|
|
1404
|
+
var PairPollResponse = z9.discriminatedUnion("status", [
|
|
1405
|
+
z9.object({ status: z9.literal("pending") }).strict(),
|
|
1406
|
+
z9.object({ status: z9.literal("denied") }).strict(),
|
|
1407
|
+
z9.object({ status: z9.literal("expired") }).strict(),
|
|
1408
|
+
z9.object({
|
|
1409
|
+
status: z9.literal("approved"),
|
|
1410
|
+
// `runnerToken` is gone — cloud_008 §2.4, finding 37.
|
|
1411
|
+
//
|
|
1412
|
+
// It was minted here, hashed into `RunnerRecord.tokenHash`, written to
|
|
1413
|
+
// the daemon's pairings file, and then **never sent, never looked up
|
|
1414
|
+
// and never compared**. `getRunnerByTokenHash` existed on both stores
|
|
1415
|
+
// and was called by nothing but a test asserting it returns null.
|
|
1416
|
+
//
|
|
1417
|
+
// Not merely dead wire, which is what `audienceAllow` and
|
|
1418
|
+
// `HeartbeatResponse.leases` were. This was a *secret*: minted,
|
|
1419
|
+
// transmitted, and written to two disks at rest, for nothing. A
|
|
1420
|
+
// credential with no purpose is a liability rather than clutter,
|
|
1421
|
+
// because the only thing it can ever do is leak.
|
|
1422
|
+
//
|
|
1423
|
+
// `REQUESTS_SIGNED_NOT_BEARER` was already the rule and was already
|
|
1424
|
+
// enforced — every authenticated call is signed by the device's pinned
|
|
1425
|
+
// identity key. This removes the thing the MUST is named after.
|
|
1426
|
+
runnerId: z9.string().min(1),
|
|
1427
|
+
/** The app's id for the approving user — this daemon's owner forever. */
|
|
1428
|
+
owner: z9.string().min(1),
|
|
1429
|
+
/** Display name for the trust UI, if the app offers one. */
|
|
1430
|
+
ownerLabel: z9.string().optional(),
|
|
1431
|
+
/**
|
|
1432
|
+
* The sites this pairing covers, for the daemon to pin (byollm_009 §5),
|
|
1433
|
+
* keyed by each site's identity key id — cloud_009 §5.
|
|
1434
|
+
*
|
|
1435
|
+
* Returned only on approval: a pending or denied poll learns nothing,
|
|
1436
|
+
* so an unapproved code cannot be used to enumerate a site's keys.
|
|
1437
|
+
*
|
|
1438
|
+
* **One pairing per upstream, not one per site.** A user who connects a
|
|
1439
|
+
* site on a web dashboard has no reason to go back to a laptop and run
|
|
1440
|
+
* a command, so which sites a pairing covers is a projection of consent
|
|
1441
|
+
* — refreshed on the heartbeat — rather than something frozen at
|
|
1442
|
+
* pairing. A direct site answers with exactly one entry, which is the
|
|
1443
|
+
* same shape and not a special case.
|
|
1444
|
+
*
|
|
1445
|
+
* Keyed by the id `stub.site` carries (Amendment A §A.3), so the
|
|
1446
|
+
* runner's lookup is a map read rather than a join across two
|
|
1447
|
+
* namespaces.
|
|
1448
|
+
*/
|
|
1449
|
+
sites: z9.record(z9.string().min(1), PublicIdentity),
|
|
1450
|
+
/**
|
|
1451
|
+
* The control plane's grant-signing key, pinned here — Amendment J.
|
|
1452
|
+
*
|
|
1453
|
+
* **Pairing is when, and that is the whole question.** Pairing is
|
|
1454
|
+
* already the ceremony where an owner proves out of band that this
|
|
1455
|
+
* device is theirs, so a key learned here rides trust that has already
|
|
1456
|
+
* happened. The rejected alternative is trust-on-first-grant, and it is
|
|
1457
|
+
* rejected because it hands the decision back to the relay: a daemon
|
|
1458
|
+
* that learns whose signature to trust from the first grant to arrive
|
|
1459
|
+
* has its admission authority chosen by whoever controls delivery.
|
|
1460
|
+
*
|
|
1461
|
+
* Optional on the wire, and only on the wire: a direct-mode server has
|
|
1462
|
+
* no control plane and signs nothing, and a daemon that receives no key
|
|
1463
|
+
* serves its owner alone. It is not optional for a relay with a control
|
|
1464
|
+
* plane — one that omitted it would be asking devices to accept grants
|
|
1465
|
+
* from nobody in particular, and would find every job refused.
|
|
1466
|
+
*
|
|
1467
|
+
* Rotation is Amendment C's, with no path where a grant teaches a
|
|
1468
|
+
* daemon a new key.
|
|
1469
|
+
*/
|
|
1470
|
+
controlPlanePublic: z9.string().min(1).optional()
|
|
1471
|
+
}).strict()
|
|
1472
|
+
]);
|
|
1473
|
+
var PairRequest = z9.discriminatedUnion("action", [
|
|
1474
|
+
PairStartRequest,
|
|
1475
|
+
PairPollRequest
|
|
1476
|
+
]);
|
|
1477
|
+
var ClaimRequest = z9.object({
|
|
1478
|
+
protocolVersion: z9.literal(PROTOCOL_VERSION),
|
|
1479
|
+
runnerId: z9.string().min(1),
|
|
1480
|
+
/** Re-sent on every claim so a server never matches against a stale matrix. */
|
|
1481
|
+
capabilities: CapabilityMatrix,
|
|
1482
|
+
/** Upper bound on jobs to return; the server may return fewer. */
|
|
1483
|
+
max: z9.number().int().min(1).max(64)
|
|
1484
|
+
}).strict();
|
|
1485
|
+
var ClaimResponse = z9.object({
|
|
1486
|
+
/**
|
|
1487
|
+
* Stubs, not jobs. The payload arrives from `fetch`, sealed to whichever
|
|
1488
|
+
* device claimed — see {@link JobStub} for the exhaustive metadata list.
|
|
1489
|
+
*/
|
|
1490
|
+
jobs: z9.array(ClaimedStub),
|
|
1491
|
+
/** Lease duration granted, so the daemon knows its renewal deadline. */
|
|
1492
|
+
leaseMs: z9.number().int().positive()
|
|
1493
|
+
}).strict();
|
|
1494
|
+
var HeartbeatRequest = z9.object({
|
|
1495
|
+
protocolVersion: z9.literal(PROTOCOL_VERSION),
|
|
1496
|
+
runnerId: z9.string().min(1),
|
|
1497
|
+
daemonVersion: z9.string().min(1),
|
|
1498
|
+
capabilities: CapabilityMatrix,
|
|
1499
|
+
/**
|
|
1500
|
+
* Kinds this device is withholding, and why it can be said.
|
|
1501
|
+
*
|
|
1502
|
+
* Optional so a daemon that has nothing withheld sends nothing, and so an
|
|
1503
|
+
* older daemon against a newer hub is simply a device with no withheld
|
|
1504
|
+
* kinds rather than a parse failure.
|
|
1505
|
+
*/
|
|
1506
|
+
withheld: z9.array(WithheldKind).default([]),
|
|
1507
|
+
/**
|
|
1508
|
+
* Leases this daemon believes it holds; the server renews exactly these.
|
|
1509
|
+
*
|
|
1510
|
+
* Lease ids rather than job ids, so a replayed heartbeat cannot renew a
|
|
1511
|
+
* grant the runner no longer holds — see {@link Lease.id}.
|
|
1512
|
+
*/
|
|
1513
|
+
activeLeases: z9.array(GrantRef),
|
|
1514
|
+
/** True while the owner has the daemon paused; the server stops offering work. */
|
|
1515
|
+
paused: z9.boolean()
|
|
1516
|
+
}).strict();
|
|
1517
|
+
var HeartbeatResponse = z9.object({
|
|
1518
|
+
/**
|
|
1519
|
+
* The sites this daemon may serve, right now — cloud_008 finding 59.
|
|
1520
|
+
*
|
|
1521
|
+
* Revocation used to be a boolean, and it was device-wide: the daemon
|
|
1522
|
+
* plane refused every call when the (owner, hub-site) consent was gone,
|
|
1523
|
+
* heartbeat answered `revoked: true` with `lost: all`, and the daemon
|
|
1524
|
+
* dropped its whole pairing by origin. Under a hub that is one site's
|
|
1525
|
+
* revocation ending a machine's relationship with every other site it
|
|
1526
|
+
* served — the amplification finding 48 warned about, arriving through
|
|
1527
|
+
* the one field nobody thought of as tenancy.
|
|
1528
|
+
*
|
|
1529
|
+
* So the answer is the set. A site that leaves it is revoked *for that
|
|
1530
|
+
* site*: the daemon drops that pin and keeps the rest. An empty set is
|
|
1531
|
+
* what "revoked" used to mean, and the daemon can see that for itself
|
|
1532
|
+
* rather than being told a second time — two fields for one fact is how
|
|
1533
|
+
* they drift.
|
|
1534
|
+
*/
|
|
1535
|
+
sites: z9.record(z9.string().min(1), PublicIdentity),
|
|
1536
|
+
/**
|
|
1537
|
+
* How a site's current key traces back to one this daemon already holds —
|
|
1538
|
+
* byollm_009 Amendment C.
|
|
1539
|
+
*
|
|
1540
|
+
* Keyed by the same id as `sites`, and **additive on purpose**: `sites`
|
|
1541
|
+
* remains the one statement of which key is current, and this says only
|
|
1542
|
+
* how that key got there. Two fields for one fact is how they drift; this
|
|
1543
|
+
* is two facts, and the second is evidence about the first.
|
|
1544
|
+
*
|
|
1545
|
+
* Optional because a site that has never rotated has no chain, which is
|
|
1546
|
+
* every site today. A daemon that receives one for an id it already holds
|
|
1547
|
+
* ignores it: the pin it has is the pin it approved.
|
|
1548
|
+
*
|
|
1549
|
+
* §12 carries what this adds to the metadata surface — a site's rotation
|
|
1550
|
+
* history is public by construction, because a daemon that cannot read it
|
|
1551
|
+
* cannot verify it.
|
|
1552
|
+
*/
|
|
1553
|
+
successions: z9.record(
|
|
1554
|
+
z9.string().min(1),
|
|
1555
|
+
z9.object({
|
|
1556
|
+
/** Oldest last, as the projection carries it. */
|
|
1557
|
+
succeeds: z9.array(Succession).max(MAX_SUCCESSION_CHAIN),
|
|
1558
|
+
/**
|
|
1559
|
+
* Until when the superseded key may still sign work — epoch ms.
|
|
1560
|
+
*
|
|
1561
|
+
* The daemon holds its own clock against this, for the reason it
|
|
1562
|
+
* holds its own allowlist: a projection that could extend the
|
|
1563
|
+
* window indefinitely would be a two-key site forever, decided by
|
|
1564
|
+
* the party this design does not trust.
|
|
1565
|
+
*/
|
|
1566
|
+
retiringUntil: z9.number().int().positive().optional()
|
|
1567
|
+
}).strict()
|
|
1568
|
+
).optional(),
|
|
1569
|
+
/**
|
|
1570
|
+
* Per-job cancel (byollm_001 Rev 1 §C). The daemon aborts these jobs'
|
|
1571
|
+
* in-flight backend calls and reports them `canceled`.
|
|
1572
|
+
*
|
|
1573
|
+
* **The grant, not the id** — V1-3. Job ids are chosen per site, so two
|
|
1574
|
+
* sites may pick the same one, and a bare id told a daemon holding both
|
|
1575
|
+
* to abort whichever it happened to have filed under that name. The lease
|
|
1576
|
+
* is the unique grant and the daemon already keys its work by it; this is
|
|
1577
|
+
* the same shape `activeLeases` sends in the other direction.
|
|
1578
|
+
*/
|
|
1579
|
+
cancel: z9.array(
|
|
1580
|
+
z9.object({ jobId: z9.string().min(1), leaseId: z9.string().min(1) }).strict()
|
|
1581
|
+
),
|
|
1582
|
+
// `leases` is deliberately absent — cloud_008 §1.4b, finding 16.
|
|
1583
|
+
//
|
|
1584
|
+
// It carried "these leases were renewed, and here is the new expiry", and
|
|
1585
|
+
// **no daemon ever read it.** A mutation returning an empty list while
|
|
1586
|
+
// renewing correctly survived every test, which is what made it visible.
|
|
1587
|
+
//
|
|
1588
|
+
// It is neither a class nor membership, so Amendment A's rule does not
|
|
1589
|
+
// decide it — the older test does: nothing reads it, so it is dead wire.
|
|
1590
|
+
// §6's exhaustiveness is a commitment about what an upstream can see, and
|
|
1591
|
+
// it applies to every message rather than only to the stub.
|
|
1592
|
+
//
|
|
1593
|
+
// `lost` is the actionable signal and always was: a daemon stops work on
|
|
1594
|
+
// a lease it no longer holds. "Renewed" was the same question answered a
|
|
1595
|
+
// second time, and a second answer can only agree or contradict.
|
|
1596
|
+
//
|
|
1597
|
+
// Renewal itself is untouched — the upstream still extends the grants a
|
|
1598
|
+
// heartbeat names, which is what §0.6 fixed. What ended is telling the
|
|
1599
|
+
// daemon about it in a field it ignored. If an upstream ever needs to
|
|
1600
|
+
// push lease decisions, that is a new field with a reader, added on
|
|
1601
|
+
// purpose.
|
|
1602
|
+
/**
|
|
1603
|
+
* Jobs the daemon thinks it holds but the server has reassigned or
|
|
1604
|
+
* expired. The daemon must stop work on these and not report results.
|
|
1605
|
+
*
|
|
1606
|
+
* Named by grant rather than by id, for V1-3's reason: a bare id is
|
|
1607
|
+
* ambiguous across sites, and "the lease you no longer hold" is exactly
|
|
1608
|
+
* what this field means anyway.
|
|
1609
|
+
*/
|
|
1610
|
+
lost: z9.array(
|
|
1611
|
+
z9.object({ jobId: z9.string().min(1), leaseId: z9.string().min(1) }).strict()
|
|
1612
|
+
),
|
|
1613
|
+
/** Server clock, so a daemon with a skewed clock still honors leases. */
|
|
1614
|
+
serverTime: z9.number().int().positive(),
|
|
1615
|
+
/**
|
|
1616
|
+
* Sites whose disclosure the user must read again before work moves —
|
|
1617
|
+
* cloud_008 finding 48, named rather than counted.
|
|
1618
|
+
*
|
|
1619
|
+
* A **subset of `sites`**, deliberately: a paused site keeps its pin, so
|
|
1620
|
+
* re-consenting never costs a re-pair. The daemon can say which site is
|
|
1621
|
+
* waiting and the user can go and read it, which is the difference
|
|
1622
|
+
* between a machine that is quietly idle and one that says why.
|
|
1623
|
+
*
|
|
1624
|
+
* Not `revoked`, which is a human ending a relationship, and not
|
|
1625
|
+
* `paused`, which on the request side already means "this daemon's
|
|
1626
|
+
* operator stopped it" — one word with two subjects on two halves of one
|
|
1627
|
+
* exchange is a confusion nobody untangles from a log.
|
|
1628
|
+
*/
|
|
1629
|
+
awaitingConsent: z9.array(z9.string().min(1)),
|
|
1630
|
+
/**
|
|
1631
|
+
* A version this daemon should move itself to — B053.
|
|
1632
|
+
*
|
|
1633
|
+
* The channel the auto-updater reads, and it is the channel the daemon
|
|
1634
|
+
* already polls rather than a new phone-home, which is what the ruling
|
|
1635
|
+
* asked for (016 §Auto-update).
|
|
1636
|
+
*
|
|
1637
|
+
* **The hub may not send this to every daemon.** This schema is
|
|
1638
|
+
* `.strict()`, so a daemon built before the field exists does not ignore
|
|
1639
|
+
* it — it rejects the whole heartbeat and stops working. Which would mean
|
|
1640
|
+
* the message carrying the update is the message that breaks the machines
|
|
1641
|
+
* it was meant to update.
|
|
1642
|
+
*
|
|
1643
|
+
* That is decidable without any new handshake, because the request
|
|
1644
|
+
* already carries `daemonVersion`. {@link mayOfferUpdate} is the rule,
|
|
1645
|
+
* kept here as code rather than as a paragraph in a runbook, so both
|
|
1646
|
+
* sides read the same one.
|
|
1647
|
+
*
|
|
1648
|
+
* Exact versions only, never a tag: the daemon refuses anything else, and
|
|
1649
|
+
* a fleet resolving one tag at different minutes is a fleet on different
|
|
1650
|
+
* builds reporting one number.
|
|
1651
|
+
*/
|
|
1652
|
+
updateTo: z9.string().min(1).optional()
|
|
1653
|
+
}).strict();
|
|
1654
|
+
var ResultDisposition = z9.enum(["ok", "error", "canceled"]);
|
|
1655
|
+
var ResultRequest = z9.object({
|
|
1656
|
+
protocolVersion: z9.literal(PROTOCOL_VERSION),
|
|
1657
|
+
runnerId: z9.string().min(1),
|
|
1658
|
+
jobId: z9.string().min(1),
|
|
1659
|
+
/**
|
|
1660
|
+
* The grant this result was produced under — cloud_008 §1.4a.
|
|
1661
|
+
*
|
|
1662
|
+
* `fetch` has always named its lease, with the reasoning written beside
|
|
1663
|
+
* it: a request that names only the job would be answerable for whatever
|
|
1664
|
+
* lease exists when it arrives. **The operation that writes the result did
|
|
1665
|
+
* not**, on either plane, and checked only the runner id — which survives
|
|
1666
|
+
* a claim-release-reclaim cycle, so a device whose grant had been swept
|
|
1667
|
+
* and reissued could still land a result for a job it no longer held.
|
|
1668
|
+
*
|
|
1669
|
+
* Found by tracing a mutation that survived in §0.6: the lease lapsed, the
|
|
1670
|
+
* sweep requeued, the daemon re-claimed under a new grant, and the
|
|
1671
|
+
* original run finished and posted anyway. The relay marked the job done
|
|
1672
|
+
* with a result the site cannot open — it verifies the envelope against
|
|
1673
|
+
* the *current* holder's device, so the crypto contains the substitution —
|
|
1674
|
+
* and then refused the real holder's result as a replay. A lost job, in
|
|
1675
|
+
* silence.
|
|
1676
|
+
*
|
|
1677
|
+
* `LEASE_HONORED` is a statement about a lease *instance*. That was
|
|
1678
|
+
* learned once already, when a replayed release yanked a later grant, and
|
|
1679
|
+
* it applies here for the same reason.
|
|
1680
|
+
*/
|
|
1681
|
+
leaseId: z9.string().min(1),
|
|
1682
|
+
/**
|
|
1683
|
+
* The outcome, sealed to the site and signed by the device.
|
|
1684
|
+
*
|
|
1685
|
+
* The return leg of the payload envelope, and sealed for the same reason:
|
|
1686
|
+
* a model's answer is as sensitive as the prompt that produced it, and an
|
|
1687
|
+
* intermediary that cannot read one must not be handed the other.
|
|
1688
|
+
*/
|
|
1689
|
+
envelope: SealedEnvelope,
|
|
1690
|
+
/**
|
|
1691
|
+
* The sealed outcome's discriminator, in the clear.
|
|
1692
|
+
*
|
|
1693
|
+
* Checked against the envelope once opened. It is a routing hint, not a
|
|
1694
|
+
* fact: believing it unverified would let a daemon mark a job `ok` while
|
|
1695
|
+
* sealing an error, and only the app would ever find out.
|
|
1696
|
+
*/
|
|
1697
|
+
disposition: ResultDisposition
|
|
1698
|
+
// `model`, `backendClass` and `durationMs` are **inside the envelope** —
|
|
1699
|
+
// cloud_008 §2.5. See {@link RunMetadata}.
|
|
1700
|
+
//
|
|
1701
|
+
// They were here, in the clear, and that was two problems wearing one
|
|
1702
|
+
// coat. On the direct plane the site recorded unauthenticated fields
|
|
1703
|
+
// beside an authenticated answer: a daemon could seal one result and
|
|
1704
|
+
// declare a different model, and only the unsigned half would reach the
|
|
1705
|
+
// app. Through a relay they reached a third party that acts on none of
|
|
1706
|
+
// them — `model` in particular being the sort of detail Amendment A's
|
|
1707
|
+
// rule keeps off the wire.
|
|
1708
|
+
//
|
|
1709
|
+
// `disposition` stays, and the difference is the test: a relay *routes*
|
|
1710
|
+
// on it, so it is a class a routing party consumes. Nobody between the
|
|
1711
|
+
// two ends consumes these.
|
|
1712
|
+
}).strict();
|
|
1713
|
+
var ResultResponse = z9.object({
|
|
1714
|
+
/**
|
|
1715
|
+
* False when this submission wrote nothing — the daemon should discard,
|
|
1716
|
+
* not retry ({@link MUSTS.RESULT_IDEMPOTENT}).
|
|
1717
|
+
*/
|
|
1718
|
+
accepted: z9.boolean(),
|
|
1719
|
+
/**
|
|
1720
|
+
* True when this device had already recorded this job's result.
|
|
1721
|
+
*
|
|
1722
|
+
* The difference between "already recorded" and "you no longer hold this"
|
|
1723
|
+
* — cloud_008 §3.6. A daemon whose acknowledgment was lost is in the first
|
|
1724
|
+
* case and needs to hear it: its answer is safely on disk. Reporting a
|
|
1725
|
+
* stale lease instead invents a worry about a result that is already
|
|
1726
|
+
* stored, and sends its owner looking for a routing problem.
|
|
1727
|
+
*
|
|
1728
|
+
* Set only for the device that finished the job. A different device gets
|
|
1729
|
+
* the same refusal it would get for a job that is *not* terminal, so a job
|
|
1730
|
+
* id cannot be used as a terminality probe.
|
|
1731
|
+
*/
|
|
1732
|
+
duplicate: z9.boolean().optional(),
|
|
1733
|
+
/** The job's state after this submission. */
|
|
1734
|
+
state: z9.string().min(1)
|
|
1735
|
+
}).strict();
|
|
1736
|
+
var ReleaseRequest = z9.object({
|
|
1737
|
+
protocolVersion: z9.literal(PROTOCOL_VERSION),
|
|
1738
|
+
runnerId: z9.string().min(1),
|
|
1739
|
+
/**
|
|
1740
|
+
* Which leases to release — the grant, not just the job.
|
|
1741
|
+
*
|
|
1742
|
+
* A release naming only a job id releases whatever lease exists at the
|
|
1743
|
+
* moment it arrives, which for a replayed request is not the lease the
|
|
1744
|
+
* daemon meant. See {@link Lease.id}.
|
|
1745
|
+
*/
|
|
1746
|
+
leases: z9.array(GrantRef),
|
|
1747
|
+
/**
|
|
1748
|
+
* Why, so the app's runner list can say something true.
|
|
1749
|
+
*
|
|
1750
|
+
* `refused` is load-bearing, not cosmetic: the server cannot evaluate
|
|
1751
|
+
* what a device will admit (§4.2), so it may legitimately offer
|
|
1752
|
+
* a job this daemon then declines. The server MUST record the refusal and
|
|
1753
|
+
* stop offering that job to that runner, or the pair would spin between
|
|
1754
|
+
* claim and release forever.
|
|
1755
|
+
*/
|
|
1756
|
+
reason: z9.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
|
|
1757
|
+
}).strict();
|
|
1758
|
+
var ReleaseResponse = z9.object({
|
|
1759
|
+
released: z9.array(z9.string().min(1))
|
|
1760
|
+
}).strict();
|
|
1761
|
+
var WireErrorCode = z9.enum([
|
|
1762
|
+
"bad-request",
|
|
1763
|
+
"unsupported-protocol-version",
|
|
1764
|
+
/**
|
|
1765
|
+
* The daemon is older than this hub will serve — B052.
|
|
1766
|
+
*
|
|
1767
|
+
* Distinct from `unsupported-protocol-version`, which is about the
|
|
1768
|
+
* contract; this is about the build. A daemon can speak protocol 1
|
|
1769
|
+
* perfectly and still be old enough that we would rather move it than keep
|
|
1770
|
+
* carrying it — and the two need different remedies in the message, since
|
|
1771
|
+
* one is "your daemon and this server disagree" and the other is "yours
|
|
1772
|
+
* works, and it is time".
|
|
1773
|
+
*
|
|
1774
|
+
* The floor is the backstop to the auto-updater (B053), and the only lever
|
|
1775
|
+
* that reaches a machine which never opted into offers.
|
|
1776
|
+
*/
|
|
1777
|
+
"daemon-below-floor",
|
|
1778
|
+
// "We do not know who you are." Exactly 401, and only that — cloud_008
|
|
1779
|
+
// §1.4d.
|
|
1780
|
+
"unauthorized",
|
|
1781
|
+
/**
|
|
1782
|
+
* "We know exactly who you are, and the answer is no." Exactly 403.
|
|
1783
|
+
*
|
|
1784
|
+
* Five refusals across both planes served 403 with `unauthorized`, whose
|
|
1785
|
+
* table entry is 401: a revoked device, a site claiming another site's
|
|
1786
|
+
* stub, a job you do not hold, a device belonging to another owner, a
|
|
1787
|
+
* relay that does not route for you. Every one of them is an *identified*
|
|
1788
|
+
* caller being refused.
|
|
1789
|
+
*
|
|
1790
|
+
* Collapsing the two loses a distinction that matters everywhere it is
|
|
1791
|
+
* read: a revoked daemon would look like an unsigned one in every log and
|
|
1792
|
+
* every client branch, and "check your keys" is the wrong advice for both
|
|
1793
|
+
* of them in opposite directions.
|
|
1794
|
+
*/
|
|
1795
|
+
"forbidden",
|
|
1796
|
+
"revoked",
|
|
1797
|
+
"not-found",
|
|
1798
|
+
// Claimed, but the site has not sealed the payload yet — cloud_008 §1.4.
|
|
1799
|
+
//
|
|
1800
|
+
// A daemon must retry rather than abandon: the job is legitimately still
|
|
1801
|
+
// its own until the lease or the awaiting-payload clock says otherwise.
|
|
1802
|
+
// That is why it cannot be `not-found` or `server-error`, and why it was
|
|
1803
|
+
// the protocol gap that produced a bare 409 in the first place.
|
|
1804
|
+
"not-ready",
|
|
1805
|
+
/**
|
|
1806
|
+
* The job is over, and this call is about a job — V1-6, and the code the
|
|
1807
|
+
* site plane has been serving without one (V1-13).
|
|
1808
|
+
*
|
|
1809
|
+
* Distinct from `not-found`, which says "no such job", and from
|
|
1810
|
+
* `not-ready`, which says "not yet, keep asking". This one says "yes, and
|
|
1811
|
+
* it finished" — so a daemon must stop rather than retry, and a replayed
|
|
1812
|
+
* request must not be able to reopen it.
|
|
1813
|
+
*/
|
|
1814
|
+
"too-late",
|
|
1815
|
+
// The caller's clock is too far from ours to judge a signature's freshness.
|
|
1816
|
+
//
|
|
1817
|
+
// Split out from `unauthorized` because the remedy is completely different
|
|
1818
|
+
// and only the server can tell them apart: a bad signature means the key is
|
|
1819
|
+
// wrong, this means the machine's time is wrong. A daemon reporting it as a
|
|
1820
|
+
// generic rejection sends its owner looking at their network.
|
|
1821
|
+
"clock-skew",
|
|
1822
|
+
"rate-limited",
|
|
1823
|
+
"server-error"
|
|
1824
|
+
]);
|
|
1825
|
+
var WireError = z9.object({
|
|
1826
|
+
error: WireErrorCode,
|
|
1827
|
+
message: z9.string().min(1),
|
|
1828
|
+
/**
|
|
1829
|
+
* What this server speaks, on `unsupported-protocol-version` — §B.4.
|
|
1830
|
+
*
|
|
1831
|
+
* The refusal has carried these since the version handshake existed and
|
|
1832
|
+
* the enumeration did not model them, so the one error that exists to be
|
|
1833
|
+
* *acted on* was the one that failed to parse as a wire error. Found by
|
|
1834
|
+
* the relay's own suite the day the relay started sending it: a refusal
|
|
1835
|
+
* outside the enumerated shape is a refusal a client cannot branch on,
|
|
1836
|
+
* which is the whole reason §1.4 enumerates them.
|
|
1837
|
+
*
|
|
1838
|
+
* Modelled the way `clock-skew`'s two fields already are — code-specific
|
|
1839
|
+
* extras, refused on any other code by the refinement below.
|
|
1840
|
+
*/
|
|
1841
|
+
supported: z9.array(z9.string().min(1)).optional(),
|
|
1842
|
+
minimum: z9.string().min(1).optional(),
|
|
1843
|
+
/**
|
|
1844
|
+
* The oldest daemon this hub serves, on `daemon-below-floor` — B052.
|
|
1845
|
+
*
|
|
1846
|
+
* Carried for the same reason `supported` and `minimum` are: a refusal
|
|
1847
|
+
* that cannot be branched on is a refusal a client can only print. The
|
|
1848
|
+
* message already names the floor for a person; this names it for the
|
|
1849
|
+
* code, so a surface can say "you are two versions under" without
|
|
1850
|
+
* parsing English.
|
|
1851
|
+
*/
|
|
1852
|
+
floor: z9.string().min(1).optional(),
|
|
1853
|
+
/** Seconds; mirrors Retry-After for `rate-limited` and `server-error`. */
|
|
1854
|
+
retryAfter: z9.number().int().nonnegative().optional(),
|
|
1855
|
+
/**
|
|
1856
|
+
* The server's clock, and the window it allows. `clock-skew` only.
|
|
1857
|
+
*
|
|
1858
|
+
* So the far side can say *how far off* rather than *that something is
|
|
1859
|
+
* wrong* — the difference between "adjust your clock by four minutes" and
|
|
1860
|
+
* "something is wrong with your connection". Not a disclosure: the
|
|
1861
|
+
* heartbeat response returns the same value, and so does every `Date`
|
|
1862
|
+
* header.
|
|
1863
|
+
*/
|
|
1864
|
+
serverTime: z9.number().int().positive().optional(),
|
|
1865
|
+
maxSkewMs: z9.number().int().positive().optional()
|
|
1866
|
+
}).strict().superRefine((error, ctx) => {
|
|
1867
|
+
const skew = error.error === "clock-skew";
|
|
1868
|
+
const carried = error.serverTime !== void 0 || error.maxSkewMs !== void 0;
|
|
1869
|
+
if (skew && !carried) {
|
|
1870
|
+
ctx.addIssue({
|
|
1871
|
+
code: "custom",
|
|
1872
|
+
message: "clock-skew must carry serverTime and maxSkewMs"
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
if (!skew && carried) {
|
|
1876
|
+
ctx.addIssue({
|
|
1877
|
+
code: "custom",
|
|
1878
|
+
message: `${error.error} must not carry serverTime or maxSkewMs`
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
const floored = error.error === "daemon-below-floor";
|
|
1882
|
+
if (floored && error.floor === void 0) {
|
|
1883
|
+
ctx.addIssue({
|
|
1884
|
+
code: "custom",
|
|
1885
|
+
message: "daemon-below-floor must carry floor"
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
if (!floored && error.floor !== void 0) {
|
|
1889
|
+
ctx.addIssue({
|
|
1890
|
+
code: "custom",
|
|
1891
|
+
message: `${error.error} must not carry floor`
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
const version = error.error === "unsupported-protocol-version";
|
|
1895
|
+
const versionFields = error.supported !== void 0 || error.minimum !== void 0;
|
|
1896
|
+
if (version && !versionFields) {
|
|
1897
|
+
ctx.addIssue({
|
|
1898
|
+
code: "custom",
|
|
1899
|
+
message: "unsupported-protocol-version must carry supported and minimum"
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
if (!version && versionFields) {
|
|
1903
|
+
ctx.addIssue({
|
|
1904
|
+
code: "custom",
|
|
1905
|
+
message: `${error.error} must not carry supported or minimum`
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
});
|
|
1909
|
+
var ERROR_STATUS = Object.freeze({
|
|
1910
|
+
"bad-request": 400,
|
|
1911
|
+
"unsupported-protocol-version": 400,
|
|
1912
|
+
/**
|
|
1913
|
+
* 426 Upgrade Required — B052, and it is the one status that says this.
|
|
1914
|
+
*
|
|
1915
|
+
* Not 403, which this daemon reads as a permission problem and which
|
|
1916
|
+
* sits beside `revoked` in every log. Not 400, which reads as a
|
|
1917
|
+
* malformed request; the request was perfect and the sender is old.
|
|
1918
|
+
*
|
|
1919
|
+
* It also fails safely on a daemon that predates the code: 426 is not in
|
|
1920
|
+
* that switch, so it lands on the 4xx default — `rejected`, which is
|
|
1921
|
+
* "never retried: the request is wrong, and repeating it stays wrong".
|
|
1922
|
+
* Vaguer than the remedy, and the right behaviour, which is what a
|
|
1923
|
+
* fallback has to be.
|
|
1924
|
+
*/
|
|
1925
|
+
"daemon-below-floor": 426,
|
|
1926
|
+
unauthorized: 401,
|
|
1927
|
+
forbidden: 403,
|
|
1928
|
+
revoked: 403,
|
|
1929
|
+
"not-found": 404,
|
|
1930
|
+
// 409, not 404: the job exists and is yours, it is simply not ready.
|
|
1931
|
+
"not-ready": 409,
|
|
1932
|
+
// The same 409 as `not-ready` and the opposite instruction: that one says
|
|
1933
|
+
// keep asking, this one says stop. The status is the class of the
|
|
1934
|
+
// problem — a request that does not fit the resource's state — and the
|
|
1935
|
+
// code is what a caller acts on.
|
|
1936
|
+
"too-late": 409,
|
|
1937
|
+
// 401 alongside `unauthorized`, because that is what it is — the
|
|
1938
|
+
// signature could not be judged. The code is what carries the remedy.
|
|
1939
|
+
"clock-skew": 401,
|
|
1940
|
+
"rate-limited": 429,
|
|
1941
|
+
"server-error": 500
|
|
1942
|
+
});
|
|
1943
|
+
var FetchRequest = z9.object({
|
|
1944
|
+
// `literal`, like every other request — V1-17. This one said
|
|
1945
|
+
// `string().min(1)`, so a daemon speaking a version this server does not
|
|
1946
|
+
// know got past the handshake on the one endpoint that hands over a
|
|
1947
|
+
// sealed payload. The version check exists so that a mismatch is a named
|
|
1948
|
+
// refusal rather than a schema failure three fields later; here it was
|
|
1949
|
+
// neither.
|
|
1950
|
+
protocolVersion: z9.literal(PROTOCOL_VERSION),
|
|
1951
|
+
runnerId: z9.string().min(1),
|
|
1952
|
+
jobId: z9.string().min(1),
|
|
1953
|
+
/**
|
|
1954
|
+
* The grant this daemon holds.
|
|
1955
|
+
*
|
|
1956
|
+
* Named, not inferred: a fetch is lease-scoped, and a request that names
|
|
1957
|
+
* only the job would be answerable for whatever lease exists when it
|
|
1958
|
+
* arrives ({@link Lease.id}).
|
|
1959
|
+
*/
|
|
1960
|
+
leaseId: z9.string().min(1)
|
|
1961
|
+
}).strict();
|
|
1962
|
+
var FetchResponse = z9.object({
|
|
1963
|
+
/**
|
|
1964
|
+
* The work, sealed to the device that claimed it — byollm_009 §6.
|
|
1965
|
+
*
|
|
1966
|
+
* Not plaintext. The site opens its own at-rest envelope and re-seals to
|
|
1967
|
+
* the claiming device's key, signed by the site's identity, so the work
|
|
1968
|
+
* is readable only by the machine that took it and only if it came from
|
|
1969
|
+
* the site that machine pinned.
|
|
1970
|
+
*/
|
|
1971
|
+
envelope: SealedEnvelope
|
|
1972
|
+
}).strict();
|
|
1973
|
+
|
|
1974
|
+
// src/update-offer.ts
|
|
1975
|
+
var UPDATE_OFFER_SINCE = "0.1.0-alpha.83";
|
|
1976
|
+
function parse(version) {
|
|
1977
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(
|
|
1978
|
+
version
|
|
1979
|
+
);
|
|
1980
|
+
if (match === null) return void 0;
|
|
1981
|
+
return {
|
|
1982
|
+
release: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
1983
|
+
pre: match[4] === void 0 ? [] : match[4].split(".").map((part) => /^\d+$/.test(part) ? Number(part) : part)
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
function compareVersions(a, b) {
|
|
1987
|
+
const left = parse(a);
|
|
1988
|
+
const right = parse(b);
|
|
1989
|
+
if (left === void 0 || right === void 0) return void 0;
|
|
1990
|
+
for (let i = 0; i < 3; i += 1) {
|
|
1991
|
+
const diff = (left.release[i] ?? 0) - (right.release[i] ?? 0);
|
|
1992
|
+
if (diff !== 0) return diff < 0 ? -1 : 1;
|
|
1993
|
+
}
|
|
1994
|
+
if (left.pre.length === 0 && right.pre.length > 0) return 1;
|
|
1995
|
+
if (left.pre.length > 0 && right.pre.length === 0) return -1;
|
|
1996
|
+
for (let i = 0; i < Math.max(left.pre.length, right.pre.length); i += 1) {
|
|
1997
|
+
const l = left.pre[i];
|
|
1998
|
+
const r = right.pre[i];
|
|
1999
|
+
if (l === void 0) return -1;
|
|
2000
|
+
if (r === void 0) return 1;
|
|
2001
|
+
if (l === r) continue;
|
|
2002
|
+
if (typeof l === "number" && typeof r === "number") return l < r ? -1 : 1;
|
|
2003
|
+
if (typeof l === "number") return -1;
|
|
2004
|
+
if (typeof r === "number") return 1;
|
|
2005
|
+
return l < r ? -1 : 1;
|
|
715
2006
|
}
|
|
716
|
-
return
|
|
2007
|
+
return 0;
|
|
2008
|
+
}
|
|
2009
|
+
function mayOfferUpdate(daemonVersion) {
|
|
2010
|
+
const order = compareVersions(daemonVersion, UPDATE_OFFER_SINCE);
|
|
2011
|
+
return order !== void 0 && order >= 0;
|
|
2012
|
+
}
|
|
2013
|
+
function checkDaemonFloor(input) {
|
|
2014
|
+
const order = compareVersions(input.daemonVersion, input.floor);
|
|
2015
|
+
if (order === void 0 || order >= 0) return null;
|
|
2016
|
+
return {
|
|
2017
|
+
error: "daemon-below-floor",
|
|
2018
|
+
message: `byollm ${input.daemonVersion} is below the supported floor (${input.floor}). Run \`${input.upgradeCommand}\`, then \`byollm start\`.`,
|
|
2019
|
+
floor: input.floor
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2022
|
+
function updateOfferFor(input) {
|
|
2023
|
+
const { offer, daemonVersion } = input;
|
|
2024
|
+
if (offer === void 0) return {};
|
|
2025
|
+
if (exactOffer(offer) === void 0) return {};
|
|
2026
|
+
if (!mayOfferUpdate(daemonVersion)) return {};
|
|
2027
|
+
if (daemonVersion === offer) return {};
|
|
2028
|
+
return { updateTo: offer };
|
|
2029
|
+
}
|
|
2030
|
+
function exactOffer(value) {
|
|
2031
|
+
return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(value) ? value : void 0;
|
|
2032
|
+
}
|
|
2033
|
+
function withoutComments(source) {
|
|
2034
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/^[ \t]*\/\/.*$/gm, " ");
|
|
717
2035
|
}
|
|
2036
|
+
function mentionsWireField(source, field) {
|
|
2037
|
+
return new RegExp(`\\b${field}\\b`).test(withoutComments(source));
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// src/about.ts
|
|
2041
|
+
var ABOUT = `# About BYOLLM
|
|
2042
|
+
|
|
2043
|
+
**What BYOLLM is**
|
|
2044
|
+
|
|
2045
|
+
BYOLLM lets you use your own AI on websites. You install one small program on
|
|
2046
|
+
your computer. Then, websites that support BYOLLM can use the AI you already
|
|
2047
|
+
have \u2014 a free model running on your machine, or an AI service you already pay
|
|
2048
|
+
for \u2014 instead of the website paying for AI and passing the cost to you.
|
|
2049
|
+
|
|
2050
|
+
**Why it matters**
|
|
2051
|
+
|
|
2052
|
+
For you:
|
|
2053
|
+
|
|
2054
|
+
- Your favorite model, everywhere you go.
|
|
2055
|
+
- New models the moment you get them \u2013 not when a site gets around to adding
|
|
2056
|
+
them.
|
|
2057
|
+
- Encrypted end-to-end. Your prompts go to your own device; byollm.cloud can't
|
|
2058
|
+
read them.
|
|
2059
|
+
- Sites never learn which model you use, and your subscriptions are never
|
|
2060
|
+
shared.
|
|
2061
|
+
- Pay less. Sites that don't pay for AI can charge you less \u2013 or nothing.
|
|
2062
|
+
|
|
2063
|
+
For sites and developers:
|
|
2064
|
+
|
|
2065
|
+
- Zero AI bills. Your users bring their own compute.
|
|
2066
|
+
- No floating money \u2013 you don't pay LLM bills up front and hope to collect
|
|
2067
|
+
later, and you never ask people to prepay just to try you.
|
|
2068
|
+
- Free trials that cost you nothing to offer.
|
|
2069
|
+
- Ship the AI features you kept private for fear of the API bill.
|
|
2070
|
+
- One small integration. Your users choose the models.
|
|
2071
|
+
|
|
2072
|
+
**Your device**
|
|
2073
|
+
|
|
2074
|
+
The \`byollm\` program runs on your computer. It knows which AI services you have
|
|
2075
|
+
set up: free open-source models on your machine, metered services you pay per
|
|
2076
|
+
use, or your own subscriptions like Claude Pro/Max. When a website you have
|
|
2077
|
+
enabled sends work, your device runs it with the service you chose. Your
|
|
2078
|
+
prompts are encrypted end-to-end to your own device. byollm.cloud passes them
|
|
2079
|
+
along and cannot read them.
|
|
2080
|
+
|
|
2081
|
+
**Sites**
|
|
2082
|
+
|
|
2083
|
+
A website that wants to use BYOLLM says what it needs \u2014 "writing help," "chat,"
|
|
2084
|
+
and so on. When you connect the site, you pick which of your services answers
|
|
2085
|
+
each one. The site never learns which model you use. You can turn a site off at
|
|
2086
|
+
any time, and it stops getting your work.
|
|
2087
|
+
|
|
2088
|
+
**Teams (optional)**
|
|
2089
|
+
|
|
2090
|
+
A team lets you share what runs on your devices with people you name \u2014 the free
|
|
2091
|
+
open-source models on your machine, or a metered service with a spending limit
|
|
2092
|
+
you set. Your subscription accounts (like Claude Pro/Max) are never shared with
|
|
2093
|
+
anyone. That is a rule, not a setting.
|
|
2094
|
+
|
|
2095
|
+
**byollm.cloud (or your own relay)**
|
|
2096
|
+
|
|
2097
|
+
Many sites, many devices, many people. byollm.cloud keeps track of who has
|
|
2098
|
+
allowed what and sends each job to the right device. It never sees your
|
|
2099
|
+
prompts. If you would rather run this part yourself, the relay is open source \u2014
|
|
2100
|
+
you can run your own instead of using byollm.cloud.`;
|
|
2101
|
+
var ABOUT_SHORT_LEDE = "BYOLLM \u2013 Bring Your Own LLM \u2013 lets you use your own AI on websites you authorize. A small program installed on your machine lets you use your own models and subscriptions on any BYOLLM-integrated site, including new models the moment you get access \u2013 no site updates required. BYOLLM Cloud connects sites to your devices with end-to-end encryption, so no one, including us, can see your data.";
|
|
2102
|
+
var ABOUT_SHORT_TAIL = "Sites can charge you less because you bring your own \u2013 see why that matters \u2192. Teams can optionally share the free or metered services on their devices with people they name. Personal subscriptions are never shared.";
|
|
2103
|
+
var ABOUT_SHORT = `${ABOUT_SHORT_LEDE}
|
|
2104
|
+
|
|
2105
|
+
${ABOUT_SHORT_TAIL}`;
|
|
718
2106
|
|
|
719
2107
|
// src/signing.ts
|
|
720
2108
|
import { createHash as createHash2 } from "crypto";
|
|
721
|
-
import { z as
|
|
2109
|
+
import { z as z10 } from "zod";
|
|
722
2110
|
var MAX_CLOCK_SKEW_MS = 12e4;
|
|
723
|
-
var RequestSignature =
|
|
2111
|
+
var RequestSignature = z10.object({
|
|
724
2112
|
/** Which runner is calling. The server looks up its pinned identity. */
|
|
725
|
-
runnerId:
|
|
2113
|
+
runnerId: z10.string().min(1),
|
|
726
2114
|
/** Epoch ms, bounded by {@link MAX_CLOCK_SKEW_MS}. */
|
|
727
|
-
issuedAt:
|
|
2115
|
+
issuedAt: z10.number().int().positive(),
|
|
728
2116
|
/** Base64url Ed25519 signature over {@link canonicalRequest}. */
|
|
729
|
-
signature:
|
|
2117
|
+
signature: z10.string().min(1)
|
|
730
2118
|
}).strict();
|
|
731
2119
|
function canonicalRequest(input) {
|
|
732
2120
|
const digest = createHash2("sha256").update(input.body, "utf8").digest("hex");
|
|
@@ -779,7 +2167,60 @@ function verifyRequest(input) {
|
|
|
779
2167
|
return ok ? null : "bad-signature";
|
|
780
2168
|
}
|
|
781
2169
|
|
|
2170
|
+
// src/manifest.ts
|
|
2171
|
+
import { z as z11 } from "zod";
|
|
2172
|
+
var RESERVED_PURPOSE = "default";
|
|
2173
|
+
var RENDERABLE = /^[^\p{Cc}\p{Cf}\p{Cs}\p{Co}]+$/u;
|
|
2174
|
+
var renderable = (max, what) => z11.string().min(1).max(max).regex(
|
|
2175
|
+
RENDERABLE,
|
|
2176
|
+
`a ${what} is text a person reads \u2014 no control characters, direction overrides or zero-width padding`
|
|
2177
|
+
).refine((value) => value.trim() !== "", {
|
|
2178
|
+
message: `a ${what} cannot be blank`
|
|
2179
|
+
});
|
|
2180
|
+
var PurposeKey = z11.string().regex(
|
|
2181
|
+
/^[a-z0-9][a-z0-9-]*$/,
|
|
2182
|
+
"a purpose key is a lowercase slug \u2014 letters, digits and hyphens"
|
|
2183
|
+
).max(64);
|
|
2184
|
+
var Purpose = z11.object({
|
|
2185
|
+
/**
|
|
2186
|
+
* What a person reads on the consent screen. The only rendered field.
|
|
2187
|
+
*
|
|
2188
|
+
* Declared rather than derived from the key, because a key is a
|
|
2189
|
+
* compromise between machines and this is not. "Writing Assistant" is
|
|
2190
|
+
* what somebody understands; `writing-assistant` is what travels.
|
|
2191
|
+
*/
|
|
2192
|
+
label: renderable(80, "label"),
|
|
2193
|
+
/** One line of context for the consent screen. Optional. */
|
|
2194
|
+
description: renderable(280, "description").optional(),
|
|
2195
|
+
/**
|
|
2196
|
+
* The kinds this purpose uses.
|
|
2197
|
+
*
|
|
2198
|
+
* A purpose may span kinds, and a mapping is per (purpose, kind) — so a
|
|
2199
|
+
* person can send this purpose's chat to one service and its generation
|
|
2200
|
+
* to another. Listing a kind here is what makes that slot appear.
|
|
2201
|
+
*/
|
|
2202
|
+
kinds: z11.array(JobKind).min(1).max(JOB_KINDS.length).refine((kinds) => new Set(kinds).size === kinds.length, {
|
|
2203
|
+
message: "a purpose lists each kind once"
|
|
2204
|
+
})
|
|
2205
|
+
}).strict();
|
|
2206
|
+
var MAX_PURPOSES = 32;
|
|
2207
|
+
var Manifest = z11.record(PurposeKey, Purpose).refine((manifest) => Object.keys(manifest).length > 0, {
|
|
2208
|
+
message: "a manifest declares at least one purpose"
|
|
2209
|
+
}).refine((manifest) => Object.keys(manifest).length <= MAX_PURPOSES, {
|
|
2210
|
+
message: `a manifest declares at most ${String(MAX_PURPOSES)} purposes \u2014 a consent screen is a set of questions somebody answers one at a time`
|
|
2211
|
+
}).refine((manifest) => !(RESERVED_PURPOSE in manifest), {
|
|
2212
|
+
message: `"${RESERVED_PURPOSE}" is reserved for a site that declares no purposes of its own \u2014 give this one a name from your own vocabulary`
|
|
2213
|
+
});
|
|
2214
|
+
function singlePurposeManifest(input) {
|
|
2215
|
+
return {
|
|
2216
|
+
[RESERVED_PURPOSE]: { label: input.label, kinds: [...input.kinds] }
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
|
|
782
2220
|
// src/musts.ts
|
|
2221
|
+
function kindsOf(must2) {
|
|
2222
|
+
return typeof must2.verifiedBy === "string" ? [must2.verifiedBy] : must2.verifiedBy;
|
|
2223
|
+
}
|
|
783
2224
|
var must = (m) => Object.freeze(m);
|
|
784
2225
|
var MUSTS = Object.freeze({
|
|
785
2226
|
// ---- Pairing and identity -------------------------------------------
|
|
@@ -812,6 +2253,50 @@ var MUSTS = Object.freeze({
|
|
|
812
2253
|
verifiedBy: "conformance",
|
|
813
2254
|
source: "byollm_009 \xA74"
|
|
814
2255
|
}),
|
|
2256
|
+
SITE_KEY_BY_STUB: must({
|
|
2257
|
+
id: "SITE_KEY_BY_STUB",
|
|
2258
|
+
statement: "A daemon MUST verify a job's payload against the pinned key of the site the stub names, and MUST refuse a job naming a site it has not pinned. It MUST NOT fall back to another pinned key, and MUST refuse an envelope whose declared sender disagrees with the stub's site.",
|
|
2259
|
+
enforcedBy: "daemon",
|
|
2260
|
+
// Adversarial, and the reason is the finding that produced it: the
|
|
2261
|
+
// honest paths pass with every site check deleted, because `open`
|
|
2262
|
+
// refuses a signature from the wrong key anyway. What distinguishes an
|
|
2263
|
+
// enforced rule from a coincidence here is a hostile pairing of stub and
|
|
2264
|
+
// envelope, which no conformance client would ever send.
|
|
2265
|
+
verifiedBy: "adversarial",
|
|
2266
|
+
source: "byollm_009 \xA7A.3"
|
|
2267
|
+
}),
|
|
2268
|
+
SITES_LOCALLY_APPROVED: must({
|
|
2269
|
+
id: "SITES_LOCALLY_APPROVED",
|
|
2270
|
+
statement: "A daemon MUST NOT run work for a site on an upstream's word alone. An upstream may propose a site set; work for any site in it MUST additionally carry a grant signed by the control-plane key this daemon pinned at pairing. A key that has changed for an id this daemon already pinned MUST be refused for the life of the pairing, including after that id has left the set and returned. A **verified succession** is not a changed key: a new key id carrying a signature, by a key this daemon has already pinned, over a statement naming both key ids MUST be accepted \u2014 provided the control plane projects the same successor \u2014 and MUST be announced rather than applied silently. The first job from a site this daemon has never served MUST be announced at the machine.",
|
|
2271
|
+
enforcedBy: "daemon",
|
|
2272
|
+
// Two kinds, and the second is the one that matters — V1-1.
|
|
2273
|
+
//
|
|
2274
|
+
// `construction`: the daemon cannot serve a site that is not in its
|
|
2275
|
+
// pinned map, and admission refuses before a payload is fetched — and
|
|
2276
|
+
// since byollm_016 Amendment K, being in the map is no longer sufficient
|
|
2277
|
+
// either: a signed grant is, and the relay proposing the set cannot
|
|
2278
|
+
// produce one.
|
|
2279
|
+
//
|
|
2280
|
+
// `adversarial`: the property that survives is about a *sequence* —
|
|
2281
|
+
// remove the id, re-offer it under a different key — which no honest
|
|
2282
|
+
// upstream sends and which the fence above does not see. That was the
|
|
2283
|
+
// bypass: the pin was deleted with the id, so the comparison had nothing
|
|
2284
|
+
// to compare against and the substitution arrived as a stranger.
|
|
2285
|
+
// **Not `conformance`, and that is a live gap rather than a judgement.**
|
|
2286
|
+
// Amendment C's succession clause is a rule about two implementations
|
|
2287
|
+
// agreeing, which is what a conformance check is for — but rotating a
|
|
2288
|
+
// site's key is not something `ConformanceTarget` can express, and adding
|
|
2289
|
+
// an optional hook that most targets omit would produce a check reporting
|
|
2290
|
+
// success for a reason unrelated to the property it claims. That is this
|
|
2291
|
+
// project's most-repeated bug, and it is not worth reintroducing for a
|
|
2292
|
+
// stronger-sounding word in a table. The rotation path is verified by
|
|
2293
|
+
// `site-rotation.test.ts` (both directions, against the shipped runner)
|
|
2294
|
+
// and `relay/test/rotation.test.ts` (both planes, against the reference
|
|
2295
|
+
// relay); the missing piece is a second *independent* implementation to
|
|
2296
|
+
// check them against, and there is not one yet.
|
|
2297
|
+
verifiedBy: ["construction", "adversarial"],
|
|
2298
|
+
source: "byollm_009 \xA7B.2, Amendment C"
|
|
2299
|
+
}),
|
|
815
2300
|
KEYS_EXCHANGED_AT_CONSENT: must({
|
|
816
2301
|
id: "KEYS_EXCHANGED_AT_CONSENT",
|
|
817
2302
|
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.",
|
|
@@ -908,14 +2393,14 @@ var MUSTS = Object.freeze({
|
|
|
908
2393
|
}),
|
|
909
2394
|
SUBSCRIPTION_SELF_LOCK: must({
|
|
910
2395
|
id: "SUBSCRIPTION_SELF_LOCK",
|
|
911
|
-
statement: "A subscription-class backend's offer scope MUST be '
|
|
2396
|
+
statement: "A subscription-class backend's offer scope MUST be 'private' and MUST NOT be widened by configuration.",
|
|
912
2397
|
enforcedBy: "daemon",
|
|
913
2398
|
verifiedBy: "conformance",
|
|
914
2399
|
source: "byollm_001 \xA7The audience model"
|
|
915
2400
|
}),
|
|
916
2401
|
METERED_DEFAULTS_SELF: must({
|
|
917
2402
|
id: "METERED_DEFAULTS_SELF",
|
|
918
|
-
statement: "A metered backend's effective offer scope MUST be '
|
|
2403
|
+
statement: "A metered backend's effective offer scope MUST be 'private' unless the owner has explicitly acknowledged spending money on others' work.",
|
|
919
2404
|
enforcedBy: "daemon",
|
|
920
2405
|
verifiedBy: "conformance",
|
|
921
2406
|
source: "byollm_007 \xA74"
|
|
@@ -943,7 +2428,7 @@ var MUSTS = Object.freeze({
|
|
|
943
2428
|
}),
|
|
944
2429
|
NAMED_LOCAL_ALLOWLIST: must({
|
|
945
2430
|
id: "NAMED_LOCAL_ALLOWLIST",
|
|
946
|
-
statement: "A '
|
|
2431
|
+
statement: "A 'team' job MUST be admitted only by something the device itself verified, keyed by (server origin, user id) \u2014 never on the routing party's assertion alone.",
|
|
947
2432
|
enforcedBy: "daemon",
|
|
948
2433
|
verifiedBy: "conformance",
|
|
949
2434
|
source: "byollm_001 Rev 1 \xA7B"
|
|
@@ -999,12 +2484,12 @@ var MUSTS = Object.freeze({
|
|
|
999
2484
|
verifiedBy: "conformance",
|
|
1000
2485
|
source: "byollm_001 \xA7Endpoints.4"
|
|
1001
2486
|
}),
|
|
1002
|
-
|
|
1003
|
-
id: "
|
|
1004
|
-
statement: "A result
|
|
2487
|
+
PROVENANCE_NAMES_DEVICE: must({
|
|
2488
|
+
id: "PROVENANCE_NAMES_DEVICE",
|
|
2489
|
+
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.",
|
|
1005
2490
|
enforcedBy: "server",
|
|
1006
2491
|
verifiedBy: "conformance",
|
|
1007
|
-
source: "
|
|
2492
|
+
source: "byollm_009 \xA711"
|
|
1008
2493
|
}),
|
|
1009
2494
|
// ---- The trust surface -------------------------------------------------
|
|
1010
2495
|
INGRESS_LOGGED_BEFORE_EXECUTION: must({
|
|
@@ -1022,12 +2507,33 @@ var MUSTS = Object.freeze({
|
|
|
1022
2507
|
verifiedBy: "adversarial",
|
|
1023
2508
|
source: "byollm_004 \xA72"
|
|
1024
2509
|
}),
|
|
2510
|
+
/**
|
|
2511
|
+
* Amended for byollm_016 Phase B, and the amendment is deliberately narrow.
|
|
2512
|
+
*
|
|
2513
|
+
* A site may now name a **service** on the stub. The temptation is to read
|
|
2514
|
+
* that as a crack in this law, so the statement below says exactly where the
|
|
2515
|
+
* line is: a name selects from a menu the owner published, and resolves to a
|
|
2516
|
+
* model, backend, base URL and flags **only** through that owner's own
|
|
2517
|
+
* config. The site supplies a key; the owner supplies every value it maps
|
|
2518
|
+
* to. A name the owner does not advertise is refused rather than
|
|
2519
|
+
* substituted, because substitution is how "you may pick from my list" turns
|
|
2520
|
+
* into "you may ask for anything and get something".
|
|
2521
|
+
*
|
|
2522
|
+
* Two properties keep it from drifting into "sites demand models":
|
|
2523
|
+
*
|
|
2524
|
+
* 1. **Nothing the site sends is ever a value.** No model string, no URL,
|
|
2525
|
+
* no flag crosses the wire — only a key that means nothing off this
|
|
2526
|
+
* owner's machine.
|
|
2527
|
+
* 2. **It is a stub field, never a payload field.** The prompt cannot
|
|
2528
|
+
* reach it. That is unchanged and is the sentence the second clause
|
|
2529
|
+
* below still enforces verbatim.
|
|
2530
|
+
*/
|
|
1025
2531
|
NO_PAYLOAD_ROUTING: must({
|
|
1026
2532
|
id: "NO_PAYLOAD_ROUTING",
|
|
1027
|
-
statement: "Model, backend, base URL, and flags MUST come from owner config only; a payload MUST NOT influence any of them.",
|
|
2533
|
+
statement: "Model, backend, base URL, and flags MUST come from owner config only; a payload MUST NOT influence any of them. A stub MAY name a service the owner advertises, which selects among that owner's own config entries and MUST NOT introduce any value the owner did not write; an unadvertised name MUST be refused, never substituted.",
|
|
1028
2534
|
enforcedBy: "daemon",
|
|
1029
2535
|
verifiedBy: "adversarial",
|
|
1030
|
-
source: "byollm_004 \xA72"
|
|
2536
|
+
source: "byollm_004 \xA72, amended byollm_016 \xA7Phase B"
|
|
1031
2537
|
}),
|
|
1032
2538
|
STRIPPED_CHILD_ENV: must({
|
|
1033
2539
|
id: "STRIPPED_CHILD_ENV",
|
|
@@ -1056,296 +2562,113 @@ var MUSTS = Object.freeze({
|
|
|
1056
2562
|
enforcedBy: "daemon",
|
|
1057
2563
|
verifiedBy: "adversarial",
|
|
1058
2564
|
source: "byollm_004 \xA74"
|
|
2565
|
+
}),
|
|
2566
|
+
REVOCATION_IMMEDIATE: must({
|
|
2567
|
+
id: "REVOCATION_IMMEDIATE",
|
|
2568
|
+
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.",
|
|
2569
|
+
// Both, and stated as one sentence with two obligations rather than
|
|
2570
|
+
// folded into REVOCATION_HONORED. That one binds the *daemon*: a revoked
|
|
2571
|
+
// daemon stops claiming and abandons in-flight work. This binds the
|
|
2572
|
+
// *upstream*. byollm_009 §5 is explicit that the pair is the point — "a
|
|
2573
|
+
// revocation enforced at one end survives a compromise of that end" — and
|
|
2574
|
+
// one entry covering both would make a compromised daemon look compliant.
|
|
2575
|
+
enforcedBy: "both",
|
|
2576
|
+
verifiedBy: "conformance",
|
|
2577
|
+
source: "byollm_009 \xA711"
|
|
2578
|
+
}),
|
|
2579
|
+
CONSENT_BEFORE_ROUTE: must({
|
|
2580
|
+
id: "CONSENT_BEFORE_ROUTE",
|
|
2581
|
+
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.",
|
|
2582
|
+
enforcedBy: "server",
|
|
2583
|
+
verifiedBy: "conformance",
|
|
2584
|
+
source: "byollm_009 \xA711"
|
|
2585
|
+
}),
|
|
2586
|
+
ROSTER_NOT_DISCLOSED: must({
|
|
2587
|
+
id: "ROSTER_NOT_DISCLOSED",
|
|
2588
|
+
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.",
|
|
2589
|
+
// Checkable since cloud_008 §0.2 took `audienceAllow` off the stub: the
|
|
2590
|
+
// property now holds by *absence*, and absence is exactly what a strict
|
|
2591
|
+
// schema and a serialised stub can be asked about. Before that it was a
|
|
2592
|
+
// sentence — and one this project cited in code comments, tests and two
|
|
2593
|
+
// specs as though it were enforced data, which is why it is worth
|
|
2594
|
+
// stating precisely rather than generously.
|
|
2595
|
+
enforcedBy: "both",
|
|
2596
|
+
verifiedBy: "conformance",
|
|
2597
|
+
source: "byollm_009 \xA711"
|
|
2598
|
+
}),
|
|
2599
|
+
EFFECTIVE_OFFER_ONLY: must({
|
|
2600
|
+
id: "EFFECTIVE_OFFER_ONLY",
|
|
2601
|
+
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.",
|
|
2602
|
+
enforcedBy: "both",
|
|
2603
|
+
verifiedBy: "conformance",
|
|
2604
|
+
source: "byollm_009 \xA711"
|
|
2605
|
+
}),
|
|
2606
|
+
FALLBACK_LABELED: must({
|
|
2607
|
+
id: "FALLBACK_LABELED",
|
|
2608
|
+
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.",
|
|
2609
|
+
// `construction` today, and deliberately not `conformance`. Nothing on
|
|
2610
|
+
// the wire yet distinguishes a fallback from any other community job —
|
|
2611
|
+
// the ledger that would give it a surface is unbuilt — so a check would
|
|
2612
|
+
// have to assert something it cannot observe. Promoted the day that
|
|
2613
|
+
// surface exists. Marking it `conformance` now would put "verified"
|
|
2614
|
+
// beside a property no third party can see, which is the one thing the
|
|
2615
|
+
// kinds exist to prevent.
|
|
2616
|
+
enforcedBy: "both",
|
|
2617
|
+
verifiedBy: "construction",
|
|
2618
|
+
source: "byollm_009 \xA711"
|
|
2619
|
+
}),
|
|
2620
|
+
RELAY_BLIND: must({
|
|
2621
|
+
id: "RELAY_BLIND",
|
|
2622
|
+
statement: "A relay MUST NOT hold any key capable of decrypting a payload, a result, or a delta frame.",
|
|
2623
|
+
// Operator: a third party can read the relay's types and see there is
|
|
2624
|
+
// nowhere to put such a key, but the kit certifies a *server* and cannot
|
|
2625
|
+
// reach inside somebody's deployment to prove what it holds.
|
|
2626
|
+
enforcedBy: "server",
|
|
2627
|
+
verifiedBy: "operator",
|
|
2628
|
+
source: "byollm_009 \xA711"
|
|
2629
|
+
}),
|
|
2630
|
+
SHARED_COMPUTE_DISCLOSED: must({
|
|
2631
|
+
id: "SHARED_COMPUTE_DISCLOSED",
|
|
2632
|
+
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.",
|
|
2633
|
+
// Operator, and cloud_008 §0.3 is why the classification now comes with a
|
|
2634
|
+
// standing answer rather than a standing question. The screen is not
|
|
2635
|
+
// wire-observable, but the *string the server composes* is, and it is
|
|
2636
|
+
// now unit-tested with the two false sentences forbidden by name. The
|
|
2637
|
+
// kind stays `operator` because a third-party site can still render
|
|
2638
|
+
// whatever it likes; what changed is that the part inside our own
|
|
2639
|
+
// boundary stopped depending on somebody remembering to audit it.
|
|
2640
|
+
enforcedBy: "server",
|
|
2641
|
+
verifiedBy: "operator",
|
|
2642
|
+
source: "byollm_009 \xA711"
|
|
1059
2643
|
})
|
|
1060
2644
|
});
|
|
2645
|
+
var RETIRED_MUSTS = Object.freeze({
|
|
2646
|
+
RESULT_PROVENANCE: {
|
|
2647
|
+
supersededBy: "PROVENANCE_NAMES_DEVICE",
|
|
2648
|
+
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."
|
|
2649
|
+
}
|
|
2650
|
+
});
|
|
1061
2651
|
var MUST_IDS = Object.freeze(Object.keys(MUSTS));
|
|
1062
2652
|
function mustsVerifiedBy(kind) {
|
|
1063
|
-
return MUST_IDS.filter((id) => MUSTS[id].
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// src/wire.ts
|
|
1067
|
-
import { z as z8 } from "zod";
|
|
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;
|
|
2653
|
+
return MUST_IDS.filter((id) => kindsOf(MUSTS[id]).includes(kind));
|
|
1092
2654
|
}
|
|
1093
|
-
var PROTOCOL_PREFIX = "/byollm";
|
|
1094
|
-
var ENDPOINTS = Object.freeze([
|
|
1095
|
-
"pair",
|
|
1096
|
-
"claim",
|
|
1097
|
-
"fetch",
|
|
1098
|
-
"heartbeat",
|
|
1099
|
-
"result",
|
|
1100
|
-
"release"
|
|
1101
|
-
]);
|
|
1102
|
-
var Capability = z8.object({
|
|
1103
|
-
kind: JobKind,
|
|
1104
|
-
backendId: BackendIdSchema,
|
|
1105
|
-
backendClass: BackendClass,
|
|
1106
|
-
model: z8.string().min(1),
|
|
1107
|
-
offerScope: OfferScope
|
|
1108
|
-
}).strict();
|
|
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),
|
|
1115
|
-
/** Shown in the app's runner list so a user can tell their machines apart. */
|
|
1116
|
-
label: z8.string().min(1).max(120),
|
|
1117
|
-
platform: z8.enum(["darwin", "linux", "win32"])
|
|
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,
|
|
1127
|
-
capabilities: CapabilityMatrix
|
|
1128
|
-
}).strict();
|
|
1129
|
-
var PairStartResponse = z8.object({
|
|
1130
|
-
/** Secret the daemon polls with. Never shown to the user. */
|
|
1131
|
-
deviceCode: z8.string().min(20),
|
|
1132
|
-
/** Short code the user reads and confirms in the browser. */
|
|
1133
|
-
userCode: z8.string().min(4).max(16),
|
|
1134
|
-
/** Where the user approves. Must be on the server's own origin. */
|
|
1135
|
-
verificationUrl: z8.url(),
|
|
1136
|
-
/** Epoch ms after which the code is dead ({@link MUSTS.PAIR_CODE_EXPIRES}). */
|
|
1137
|
-
expiresAt: z8.number().int().positive(),
|
|
1138
|
-
/** How often the daemon may poll. */
|
|
1139
|
-
pollIntervalMs: z8.number().int().min(500).max(6e4)
|
|
1140
|
-
}).strict();
|
|
1141
|
-
var PairPollRequest = z8.object({
|
|
1142
|
-
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1143
|
-
action: z8.literal("poll"),
|
|
1144
|
-
deviceCode: z8.string().min(20)
|
|
1145
|
-
}).strict();
|
|
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"),
|
|
1152
|
-
/** Bearer token for every later call. Scoped to exactly one user. */
|
|
1153
|
-
runnerToken: z8.string().min(20),
|
|
1154
|
-
runnerId: z8.string().min(1),
|
|
1155
|
-
/** The app's id for the approving user — this daemon's owner forever. */
|
|
1156
|
-
owner: z8.string().min(1),
|
|
1157
|
-
/** Display name for the trust UI, if the app offers one. */
|
|
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
|
|
1166
|
-
}).strict()
|
|
1167
|
-
]);
|
|
1168
|
-
var PairRequest = z8.discriminatedUnion("action", [
|
|
1169
|
-
PairStartRequest,
|
|
1170
|
-
PairPollRequest
|
|
1171
|
-
]);
|
|
1172
|
-
var ClaimRequest = z8.object({
|
|
1173
|
-
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1174
|
-
runnerId: z8.string().min(1),
|
|
1175
|
-
/** Re-sent on every claim so a server never matches against a stale matrix. */
|
|
1176
|
-
capabilities: CapabilityMatrix,
|
|
1177
|
-
/** Upper bound on jobs to return; the server may return fewer. */
|
|
1178
|
-
max: z8.number().int().min(1).max(64)
|
|
1179
|
-
}).strict();
|
|
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),
|
|
1186
|
-
/** Lease duration granted, so the daemon knows its renewal deadline. */
|
|
1187
|
-
leaseMs: z8.number().int().positive()
|
|
1188
|
-
}).strict();
|
|
1189
|
-
var HeartbeatRequest = z8.object({
|
|
1190
|
-
protocolVersion: z8.literal(PROTOCOL_VERSION),
|
|
1191
|
-
runnerId: z8.string().min(1),
|
|
1192
|
-
daemonVersion: z8.string().min(1),
|
|
1193
|
-
capabilities: CapabilityMatrix,
|
|
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
|
-
),
|
|
1203
|
-
/** True while the owner has the daemon paused; the server stops offering work. */
|
|
1204
|
-
paused: z8.boolean()
|
|
1205
|
-
}).strict();
|
|
1206
|
-
var HeartbeatResponse = z8.object({
|
|
1207
|
-
/** Once true, the daemon stops claiming and abandons in-flight work. */
|
|
1208
|
-
revoked: z8.boolean(),
|
|
1209
|
-
/**
|
|
1210
|
-
* Per-job cancel (byollm_001 Rev 1 §C). The daemon aborts these jobs'
|
|
1211
|
-
* in-flight backend calls and reports them `canceled`.
|
|
1212
|
-
*/
|
|
1213
|
-
cancel: z8.array(z8.string().min(1)),
|
|
1214
|
-
/** Jobs whose leases were renewed, with their new expiry. */
|
|
1215
|
-
leases: z8.array(
|
|
1216
|
-
z8.object({
|
|
1217
|
-
jobId: z8.string().min(1),
|
|
1218
|
-
expiresAt: z8.number().int().positive()
|
|
1219
|
-
}).strict()
|
|
1220
|
-
),
|
|
1221
|
-
/**
|
|
1222
|
-
* Jobs the daemon thinks it holds but the server has reassigned or
|
|
1223
|
-
* expired. The daemon must stop work on these and not report results.
|
|
1224
|
-
*/
|
|
1225
|
-
lost: z8.array(z8.string().min(1)),
|
|
1226
|
-
/** Server clock, so a daemon with a skewed clock still honors leases. */
|
|
1227
|
-
serverTime: z8.number().int().positive()
|
|
1228
|
-
}).strict();
|
|
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,
|
|
1250
|
-
/** Which model actually served it, for the result's provenance. */
|
|
1251
|
-
model: z8.string().min(1),
|
|
1252
|
-
backendClass: BackendClass,
|
|
1253
|
-
/** Wall-clock milliseconds the backend call took. */
|
|
1254
|
-
durationMs: z8.number().int().nonnegative()
|
|
1255
|
-
}).strict();
|
|
1256
|
-
var ResultResponse = z8.object({
|
|
1257
|
-
/**
|
|
1258
|
-
* False when the submission lost an idempotency race or the lease was
|
|
1259
|
-
* already gone — the daemon should discard, not retry
|
|
1260
|
-
* ({@link MUSTS.RESULT_IDEMPOTENT}).
|
|
1261
|
-
*/
|
|
1262
|
-
accepted: z8.boolean(),
|
|
1263
|
-
/** The job's state after this submission. */
|
|
1264
|
-
state: z8.string().min(1)
|
|
1265
|
-
}).strict();
|
|
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
|
-
),
|
|
1279
|
-
/**
|
|
1280
|
-
* Why, so the app's runner list can say something true.
|
|
1281
|
-
*
|
|
1282
|
-
* `refused` is load-bearing, not cosmetic: the server cannot evaluate a
|
|
1283
|
-
* daemon's *local* `named` allowlist (§4.2), so it may legitimately offer
|
|
1284
|
-
* a job this daemon then declines. The server MUST record the refusal and
|
|
1285
|
-
* stop offering that job to that runner, or the pair would spin between
|
|
1286
|
-
* claim and release forever.
|
|
1287
|
-
*/
|
|
1288
|
-
reason: z8.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
|
|
1289
|
-
}).strict();
|
|
1290
|
-
var ReleaseResponse = z8.object({
|
|
1291
|
-
released: z8.array(z8.string().min(1))
|
|
1292
|
-
}).strict();
|
|
1293
|
-
var WireErrorCode = z8.enum([
|
|
1294
|
-
"bad-request",
|
|
1295
|
-
"unsupported-protocol-version",
|
|
1296
|
-
"unauthorized",
|
|
1297
|
-
"revoked",
|
|
1298
|
-
"not-found",
|
|
1299
|
-
"rate-limited",
|
|
1300
|
-
"server-error"
|
|
1301
|
-
]);
|
|
1302
|
-
var WireError = z8.object({
|
|
1303
|
-
error: WireErrorCode,
|
|
1304
|
-
message: z8.string().min(1),
|
|
1305
|
-
/** Seconds; mirrors Retry-After for `rate-limited` and `server-error`. */
|
|
1306
|
-
retryAfter: z8.number().int().nonnegative().optional()
|
|
1307
|
-
}).strict();
|
|
1308
|
-
var ERROR_STATUS = Object.freeze({
|
|
1309
|
-
"bad-request": 400,
|
|
1310
|
-
"unsupported-protocol-version": 400,
|
|
1311
|
-
unauthorized: 401,
|
|
1312
|
-
revoked: 403,
|
|
1313
|
-
"not-found": 404,
|
|
1314
|
-
"rate-limited": 429,
|
|
1315
|
-
"server-error": 500
|
|
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();
|
|
1341
2655
|
export {
|
|
2656
|
+
ABOUT,
|
|
2657
|
+
ABOUT_SHORT,
|
|
2658
|
+
ABOUT_SHORT_LEDE,
|
|
2659
|
+
ABOUT_SHORT_TAIL,
|
|
1342
2660
|
AUDIENCES,
|
|
1343
2661
|
Audience,
|
|
1344
2662
|
BACKENDS,
|
|
2663
|
+
BACKEND_CLASSES,
|
|
1345
2664
|
BACKEND_IDS,
|
|
1346
2665
|
BackendClass,
|
|
1347
2666
|
BackendCost,
|
|
1348
2667
|
BackendIdSchema,
|
|
2668
|
+
CLOCK_ATTRIBUTION_MS,
|
|
2669
|
+
CLOCK_SKEW_WARN_MS,
|
|
2670
|
+
CONSOLE_FRAME_VERSION,
|
|
2671
|
+
CONSOLE_MAX_DATA_BYTES,
|
|
1349
2672
|
Capability,
|
|
1350
2673
|
CapabilityMatrix,
|
|
1351
2674
|
ChatMessage,
|
|
@@ -1354,20 +2677,33 @@ export {
|
|
|
1354
2677
|
ClaimResponse,
|
|
1355
2678
|
ClaimedJob,
|
|
1356
2679
|
ClaimedStub,
|
|
2680
|
+
ConsoleBye,
|
|
2681
|
+
ConsoleFrame,
|
|
2682
|
+
ConsoleHello,
|
|
2683
|
+
ConsoleResize,
|
|
2684
|
+
ConsoleStdin,
|
|
2685
|
+
ConsoleStdout,
|
|
1357
2686
|
DeliveredResult,
|
|
2687
|
+
ENCRYPTION_KEY_CONTEXT,
|
|
1358
2688
|
ENDPOINTS,
|
|
2689
|
+
ENVELOPE_BODY_VERSION,
|
|
1359
2690
|
ENVELOPE_MAX_AGE_MS,
|
|
1360
2691
|
ERROR_STATUS,
|
|
1361
2692
|
EnvelopeDirection,
|
|
1362
2693
|
FetchRequest,
|
|
1363
2694
|
FetchResponse,
|
|
2695
|
+
GRANT_CONTEXT,
|
|
2696
|
+
GRANT_MAX_AGE_MS,
|
|
2697
|
+
GRANT_SIGNED_FIELDS,
|
|
1364
2698
|
GeneratePayload,
|
|
2699
|
+
GrantRef,
|
|
1365
2700
|
HeartbeatRequest,
|
|
1366
2701
|
HeartbeatResponse,
|
|
1367
2702
|
JOB_KINDS,
|
|
1368
2703
|
JobKind,
|
|
1369
2704
|
JobOutcome,
|
|
1370
2705
|
JobPayload,
|
|
2706
|
+
JobRefused,
|
|
1371
2707
|
JobResultCanceled,
|
|
1372
2708
|
JobResultError,
|
|
1373
2709
|
JobResultOk,
|
|
@@ -1376,9 +2712,13 @@ export {
|
|
|
1376
2712
|
KindedPayload,
|
|
1377
2713
|
Lease,
|
|
1378
2714
|
MAX_CLOCK_SKEW_MS,
|
|
2715
|
+
MAX_ENVELOPE_BYTES,
|
|
2716
|
+
MAX_PURPOSES,
|
|
2717
|
+
MAX_SUCCESSION_CHAIN,
|
|
1379
2718
|
MIN_PROTOCOL_VERSION,
|
|
1380
2719
|
MUSTS,
|
|
1381
2720
|
MUST_IDS,
|
|
2721
|
+
Manifest,
|
|
1382
2722
|
MatchRefusal,
|
|
1383
2723
|
OFFER_SCOPES,
|
|
1384
2724
|
OfferScope,
|
|
@@ -1391,7 +2731,12 @@ export {
|
|
|
1391
2731
|
PairStartRequest,
|
|
1392
2732
|
PairStartResponse,
|
|
1393
2733
|
PublicIdentity,
|
|
2734
|
+
Purpose,
|
|
1394
2735
|
REFUSAL_MESSAGES,
|
|
2736
|
+
REFUSAL_TEXT,
|
|
2737
|
+
RESERVED_PURPOSE,
|
|
2738
|
+
RETIREMENT_WINDOW_MS,
|
|
2739
|
+
RefusalReason,
|
|
1395
2740
|
ReleaseRequest,
|
|
1396
2741
|
ReleaseResponse,
|
|
1397
2742
|
RequestSignature,
|
|
@@ -1399,28 +2744,59 @@ export {
|
|
|
1399
2744
|
ResultProvenance,
|
|
1400
2745
|
ResultRequest,
|
|
1401
2746
|
ResultResponse,
|
|
2747
|
+
RunMetadata,
|
|
2748
|
+
SIZE_CLASSES,
|
|
1402
2749
|
SIZE_CLASS_LIMITS,
|
|
2750
|
+
SUCCESSION_CONTEXT,
|
|
1403
2751
|
SUPPORTED_PROTOCOL_VERSIONS,
|
|
1404
2752
|
SealedEnvelope,
|
|
2753
|
+
SealedOutcome,
|
|
2754
|
+
SignedGrant,
|
|
1405
2755
|
SizeClass,
|
|
2756
|
+
StopReasonSchema,
|
|
1406
2757
|
StoredKeys,
|
|
2758
|
+
Succession,
|
|
1407
2759
|
TERMINAL_STATES,
|
|
2760
|
+
UPDATE_OFFER_SINCE,
|
|
2761
|
+
UPGRADE_COMMAND,
|
|
1408
2762
|
WireError,
|
|
1409
2763
|
WireErrorCode,
|
|
2764
|
+
WithheldKind,
|
|
1410
2765
|
backendDescriptor,
|
|
2766
|
+
backendName,
|
|
1411
2767
|
canTransition,
|
|
1412
2768
|
canonicalRequest,
|
|
2769
|
+
checkDaemonFloor,
|
|
1413
2770
|
checkProtocolVersion,
|
|
2771
|
+
classifyCost,
|
|
2772
|
+
compareVersions,
|
|
2773
|
+
consoleDataBytes,
|
|
2774
|
+
consoleEnvelope,
|
|
2775
|
+
consoleOrder,
|
|
1414
2776
|
cryptoReady,
|
|
2777
|
+
declaredVersion,
|
|
2778
|
+
decodeConsoleData,
|
|
2779
|
+
decodeEnvelopeInner,
|
|
2780
|
+
describeBytes,
|
|
1415
2781
|
effectiveOfferScope,
|
|
2782
|
+
encodeConsoleData,
|
|
2783
|
+
encodeEnvelopeInner,
|
|
2784
|
+
envelopeBytes,
|
|
2785
|
+
envelopeSignedBody,
|
|
1416
2786
|
fingerprint,
|
|
2787
|
+
fromBase64Url,
|
|
1417
2788
|
generateKeys,
|
|
2789
|
+
grantStatement,
|
|
1418
2790
|
isBackendId,
|
|
2791
|
+
isCloudTaggedModel,
|
|
1419
2792
|
isJobKind,
|
|
1420
2793
|
isLocalHost,
|
|
1421
2794
|
isTerminal,
|
|
1422
2795
|
keyId,
|
|
2796
|
+
kindsOf,
|
|
1423
2797
|
matchAudience,
|
|
2798
|
+
mayOfferUpdate,
|
|
2799
|
+
mentionsWireField,
|
|
1424
2800
|
mustsVerifiedBy,
|
|
1425
2801
|
open,
|
|
1426
2802
|
payloadTextLength,
|
|
@@ -1428,14 +2804,25 @@ export {
|
|
|
1428
2804
|
publicIdentityOf,
|
|
1429
2805
|
resolveCost,
|
|
1430
2806
|
seal,
|
|
2807
|
+
signGrant,
|
|
1431
2808
|
signRequest,
|
|
1432
2809
|
signSiteRequest,
|
|
2810
|
+
signSuccession,
|
|
1433
2811
|
signWith,
|
|
2812
|
+
singlePurposeManifest,
|
|
1434
2813
|
sizeClassCeiling,
|
|
1435
2814
|
sizeClassOf,
|
|
2815
|
+
successionStatement,
|
|
2816
|
+
toBase64Url,
|
|
2817
|
+
tooLargeMessage,
|
|
2818
|
+
updateOfferFor,
|
|
2819
|
+
verifyGrant,
|
|
2820
|
+
verifyLink,
|
|
1436
2821
|
verifyPublicIdentity,
|
|
1437
2822
|
verifyRequest,
|
|
1438
2823
|
verifySiteRequest,
|
|
1439
|
-
verifyWith
|
|
2824
|
+
verifyWith,
|
|
2825
|
+
walkSuccession,
|
|
2826
|
+
withoutComments
|
|
1440
2827
|
};
|
|
1441
2828
|
//# sourceMappingURL=index.js.map
|