@byollm/protocol 0.1.0-alpha.82 → 0.1.0-alpha.84
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/index.d.ts +2548 -2408
- package/dist/index.js +1392 -1252
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,79 +1,198 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
**What BYOLLM is**
|
|
5
|
-
|
|
6
|
-
BYOLLM lets you use your own AI on websites. You install one small program on
|
|
7
|
-
your computer. Then, websites that support BYOLLM can use the AI you already
|
|
8
|
-
have \u2014 a free model running on your machine, or an AI service you already pay
|
|
9
|
-
for \u2014 instead of the website paying for AI and passing the cost to you.
|
|
10
|
-
|
|
11
|
-
**Why it matters**
|
|
12
|
-
|
|
13
|
-
For you:
|
|
14
|
-
|
|
15
|
-
- Your favorite model, everywhere you go.
|
|
16
|
-
- New models the moment you get them \u2013 not when a site gets around to adding
|
|
17
|
-
them.
|
|
18
|
-
- Encrypted end-to-end. Your prompts go to your own device; byollm.cloud can't
|
|
19
|
-
read them.
|
|
20
|
-
- Sites never learn which model you use, and your subscriptions are never
|
|
21
|
-
shared.
|
|
22
|
-
- Pay less. Sites that don't pay for AI can charge you less \u2013 or nothing.
|
|
23
|
-
|
|
24
|
-
For sites and developers:
|
|
25
|
-
|
|
26
|
-
- Zero AI bills. Your users bring their own compute.
|
|
27
|
-
- No floating money \u2013 you don't pay LLM bills up front and hope to collect
|
|
28
|
-
later, and you never ask people to prepay just to try you.
|
|
29
|
-
- Free trials that cost you nothing to offer.
|
|
30
|
-
- Ship the AI features you kept private for fear of the API bill.
|
|
31
|
-
- One small integration. Your users choose the models.
|
|
32
|
-
|
|
33
|
-
**Your device**
|
|
34
|
-
|
|
35
|
-
The \`byollm\` program runs on your computer. It knows which AI services you have
|
|
36
|
-
set up: free open-source models on your machine, metered services you pay per
|
|
37
|
-
use, or your own subscriptions like Claude Pro/Max. When a website you have
|
|
38
|
-
enabled sends work, your device runs it with the service you chose. Your
|
|
39
|
-
prompts are encrypted end-to-end to your own device. byollm.cloud passes them
|
|
40
|
-
along and cannot read them.
|
|
41
|
-
|
|
42
|
-
**Sites**
|
|
43
|
-
|
|
44
|
-
A website that wants to use BYOLLM says what it needs \u2014 "writing help," "chat,"
|
|
45
|
-
and so on. When you connect the site, you pick which of your services answers
|
|
46
|
-
each one. The site never learns which model you use. You can turn a site off at
|
|
47
|
-
any time, and it stops getting your work.
|
|
48
|
-
|
|
49
|
-
**Teams (optional)**
|
|
50
|
-
|
|
51
|
-
A team lets you share what runs on your devices with people you name \u2014 the free
|
|
52
|
-
open-source models on your machine, or a metered service with a spending limit
|
|
53
|
-
you set. Your subscription accounts (like Claude Pro/Max) are never shared with
|
|
54
|
-
anyone. That is a rule, not a setting.
|
|
55
|
-
|
|
56
|
-
**byollm.cloud (or your own relay)**
|
|
1
|
+
// src/wire.ts
|
|
2
|
+
import { z as z9 } from "zod";
|
|
57
3
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
4
|
+
// src/keys.ts
|
|
5
|
+
import {
|
|
6
|
+
createHash,
|
|
7
|
+
createPrivateKey,
|
|
8
|
+
createPublicKey,
|
|
9
|
+
generateKeyPairSync,
|
|
10
|
+
sign,
|
|
11
|
+
verify
|
|
12
|
+
} from "crypto";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
var PublicIdentity = z.object({
|
|
15
|
+
/** Raw Ed25519 public key. The pinned one. */
|
|
16
|
+
identity: z.string().min(1),
|
|
17
|
+
/** Raw X25519 public key, for sealing to this party. */
|
|
18
|
+
encryption: z.string().min(1),
|
|
19
|
+
/**
|
|
20
|
+
* Ed25519 signature over the encryption key, by the identity key.
|
|
21
|
+
*
|
|
22
|
+
* This is what stops an upstream substituting an encryption key of its
|
|
23
|
+
* own while relaying a genuine identity: the receiver pins the identity
|
|
24
|
+
* and refuses any encryption key not signed by it.
|
|
25
|
+
*/
|
|
26
|
+
encryptionSig: z.string().min(1)
|
|
27
|
+
}).strict();
|
|
28
|
+
var StoredKeys = z.object({
|
|
29
|
+
version: z.literal(1),
|
|
30
|
+
identityPublic: z.string().min(1),
|
|
31
|
+
identityPrivate: z.string().min(1),
|
|
32
|
+
encryptionPublic: z.string().min(1),
|
|
33
|
+
encryptionPrivate: z.string().min(1),
|
|
34
|
+
encryptionSig: z.string().min(1),
|
|
35
|
+
createdAt: z.number().int().positive()
|
|
36
|
+
}).strict();
|
|
37
|
+
var ENCRYPTION_KEY_CONTEXT = "byollm/v1/encryption-key";
|
|
38
|
+
function rawPublic(key) {
|
|
39
|
+
const jwk = key.export({ format: "jwk" });
|
|
40
|
+
const x = jwk.x;
|
|
41
|
+
if (typeof x !== "string") throw new Error("key has no raw public component");
|
|
42
|
+
return x;
|
|
43
|
+
}
|
|
44
|
+
function importPublic(raw, crv) {
|
|
45
|
+
return createPublicKey({ key: { kty: "OKP", crv, x: raw }, format: "jwk" });
|
|
46
|
+
}
|
|
47
|
+
function importPrivate(stored) {
|
|
48
|
+
return createPrivateKey({
|
|
49
|
+
key: Buffer.from(stored, "base64"),
|
|
50
|
+
type: "pkcs8",
|
|
51
|
+
format: "der"
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
var exportPrivate = (key) => key.export({ type: "pkcs8", format: "der" }).toString("base64");
|
|
55
|
+
function generateKeys(now) {
|
|
56
|
+
const identity = generateKeyPairSync("ed25519");
|
|
57
|
+
const encryption = generateKeyPairSync("x25519");
|
|
58
|
+
const encryptionPublic = rawPublic(encryption.publicKey);
|
|
59
|
+
return {
|
|
60
|
+
version: 1,
|
|
61
|
+
identityPublic: rawPublic(identity.publicKey),
|
|
62
|
+
identityPrivate: exportPrivate(identity.privateKey),
|
|
63
|
+
encryptionPublic,
|
|
64
|
+
encryptionPrivate: exportPrivate(encryption.privateKey),
|
|
65
|
+
encryptionSig: sign(
|
|
66
|
+
null,
|
|
67
|
+
Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${encryptionPublic}`),
|
|
68
|
+
identity.privateKey
|
|
69
|
+
).toString("base64url"),
|
|
70
|
+
createdAt: now
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function publicIdentityOf(keys) {
|
|
74
|
+
return {
|
|
75
|
+
identity: keys.identityPublic,
|
|
76
|
+
encryption: keys.encryptionPublic,
|
|
77
|
+
encryptionSig: keys.encryptionSig
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function verifyPublicIdentity(identity) {
|
|
81
|
+
try {
|
|
82
|
+
return verify(
|
|
83
|
+
null,
|
|
84
|
+
Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${identity.encryption}`),
|
|
85
|
+
importPublic(identity.identity, "Ed25519"),
|
|
86
|
+
Buffer.from(identity.encryptionSig, "base64url")
|
|
87
|
+
);
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function signWith(keys, data) {
|
|
93
|
+
return sign(null, data, importPrivate(keys.identityPrivate)).toString(
|
|
94
|
+
"base64url"
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
function verifyWith(identityPublic, data, signature) {
|
|
98
|
+
try {
|
|
99
|
+
return verify(
|
|
100
|
+
null,
|
|
101
|
+
data,
|
|
102
|
+
importPublic(identityPublic, "Ed25519"),
|
|
103
|
+
Buffer.from(signature, "base64url")
|
|
104
|
+
);
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
110
|
+
function fingerprint(identityPublic) {
|
|
111
|
+
const digest = createHash("sha256").update(Buffer.from(identityPublic, "base64url")).digest();
|
|
112
|
+
let bits = 0;
|
|
113
|
+
let value = 0;
|
|
114
|
+
let out = "";
|
|
115
|
+
for (const byte of digest.subarray(0, 15)) {
|
|
116
|
+
value = value << 8 | byte;
|
|
117
|
+
bits += 8;
|
|
118
|
+
while (bits >= 5) {
|
|
119
|
+
out += ALPHABET.charAt(value >>> bits - 5 & 31);
|
|
120
|
+
bits -= 5;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const groups = out.match(/.{1,4}/g) ?? [];
|
|
124
|
+
return `BYOLLM-${groups.join("-")}`;
|
|
125
|
+
}
|
|
126
|
+
var keyId = (identityPublic) => fingerprint(identityPublic);
|
|
65
127
|
|
|
66
|
-
|
|
128
|
+
// src/succession.ts
|
|
129
|
+
import { z as z2 } from "zod";
|
|
130
|
+
var SUCCESSION_CONTEXT = "byollm/v1/site-succession";
|
|
131
|
+
var RETIREMENT_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
132
|
+
var MAX_SUCCESSION_CHAIN = 64;
|
|
133
|
+
var Succession = z2.object({
|
|
134
|
+
/**
|
|
135
|
+
* The predecessor's public identity — K1, in full.
|
|
136
|
+
*
|
|
137
|
+
* The whole identity rather than the key id, because a daemon meeting a
|
|
138
|
+
* chain it has not seen before has to *verify* each link, and a key id is
|
|
139
|
+
* a fingerprint: enough to compare, never enough to check a signature.
|
|
140
|
+
*/
|
|
141
|
+
identity: PublicIdentity,
|
|
142
|
+
/** K1's signature over the statement naming K1 and its successor. */
|
|
143
|
+
signature: z2.string().min(1)
|
|
144
|
+
}).strict();
|
|
145
|
+
function successionStatement(fromKeyId, toKeyId) {
|
|
146
|
+
return Buffer.from(`${SUCCESSION_CONTEXT}:${fromKeyId}:${toKeyId}`);
|
|
147
|
+
}
|
|
148
|
+
function signSuccession(previous, next) {
|
|
149
|
+
return {
|
|
150
|
+
identity: {
|
|
151
|
+
identity: previous.identityPublic,
|
|
152
|
+
encryption: previous.encryptionPublic,
|
|
153
|
+
encryptionSig: previous.encryptionSig
|
|
154
|
+
},
|
|
155
|
+
signature: signWith(
|
|
156
|
+
previous,
|
|
157
|
+
successionStatement(keyId(previous.identityPublic), keyId(next.identity))
|
|
158
|
+
)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function verifyLink(link, toKeyId) {
|
|
162
|
+
if (!verifyPublicIdentity(link.identity)) return false;
|
|
163
|
+
return verifyWith(
|
|
164
|
+
link.identity.identity,
|
|
165
|
+
successionStatement(keyId(link.identity.identity), toKeyId),
|
|
166
|
+
link.signature
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
function walkSuccession(input) {
|
|
170
|
+
const { current, chain, approved } = input;
|
|
171
|
+
if (chain.length === 0) return { path: [current], failure: "no-chain" };
|
|
172
|
+
if (chain.length > MAX_SUCCESSION_CHAIN)
|
|
173
|
+
return { path: [current], failure: "too-long" };
|
|
174
|
+
const steps = [...chain].reverse();
|
|
175
|
+
const path = [current];
|
|
176
|
+
let succeeding = current;
|
|
177
|
+
for (const link of steps) {
|
|
178
|
+
if (!verifyLink(link, succeeding)) return { path, failure: "broken-link" };
|
|
179
|
+
const previous = keyId(link.identity.identity);
|
|
180
|
+
path.unshift(previous);
|
|
181
|
+
if (approved(previous)) return { path, from: previous };
|
|
182
|
+
succeeding = previous;
|
|
183
|
+
}
|
|
184
|
+
return { path, failure: "unknown-origin" };
|
|
185
|
+
}
|
|
67
186
|
|
|
68
187
|
// src/audience.ts
|
|
69
|
-
import { z as
|
|
188
|
+
import { z as z4 } from "zod";
|
|
70
189
|
|
|
71
190
|
// src/backends.ts
|
|
72
191
|
import { isIP } from "net";
|
|
73
|
-
import { z } from "zod";
|
|
74
|
-
var BackendClass =
|
|
192
|
+
import { z as z3 } from "zod";
|
|
193
|
+
var BackendClass = z3.enum(["http", "process"]);
|
|
75
194
|
var BACKEND_CLASSES = Object.freeze(BackendClass.options);
|
|
76
|
-
var BackendCost =
|
|
195
|
+
var BackendCost = z3.enum(["free", "metered", "subscription"]);
|
|
77
196
|
var backend = (b) => Object.freeze(b);
|
|
78
197
|
var BACKENDS = Object.freeze({
|
|
79
198
|
// -- free: local compute, costs electricity ------------------------------
|
|
@@ -252,7 +371,7 @@ var BACKENDS = Object.freeze({
|
|
|
252
371
|
})
|
|
253
372
|
});
|
|
254
373
|
var BACKEND_IDS = Object.freeze(Object.keys(BACKENDS));
|
|
255
|
-
var BackendIdSchema =
|
|
374
|
+
var BackendIdSchema = z3.enum(
|
|
256
375
|
BACKEND_IDS
|
|
257
376
|
);
|
|
258
377
|
function isBackendId(value) {
|
|
@@ -323,11 +442,11 @@ function classifyCost(id, baseUrl, model) {
|
|
|
323
442
|
}
|
|
324
443
|
|
|
325
444
|
// src/audience.ts
|
|
326
|
-
var Audience =
|
|
327
|
-
var OfferScope =
|
|
445
|
+
var Audience = z4.enum(["private", "team"]);
|
|
446
|
+
var OfferScope = z4.enum(["private", "team"]);
|
|
328
447
|
var AUDIENCES = Object.freeze(Audience.options);
|
|
329
448
|
var OFFER_SCOPES = Object.freeze(OfferScope.options);
|
|
330
|
-
var MatchRefusal =
|
|
449
|
+
var MatchRefusal = z4.enum([
|
|
331
450
|
/** The daemon advertises no capability for this kind. */
|
|
332
451
|
"no-capability",
|
|
333
452
|
/** Job is `private` but this daemon belongs to a different user. */
|
|
@@ -403,193 +522,12 @@ var REFUSAL_MESSAGES = Object.freeze({
|
|
|
403
522
|
"metered-ceiling-reached": "this backend is shared but has reached the spend ceiling its owner set"
|
|
404
523
|
});
|
|
405
524
|
|
|
406
|
-
// src/kinds.ts
|
|
407
|
-
import { z as z3 } from "zod";
|
|
408
|
-
var PAYLOAD_LIMITS = Object.freeze({
|
|
409
|
-
/** Max characters in any single text field. */
|
|
410
|
-
maxTextChars: 1e6,
|
|
411
|
-
/** Max messages in an `llm.chat` conversation. */
|
|
412
|
-
maxMessages: 256,
|
|
413
|
-
/** Max characters across the whole payload. */
|
|
414
|
-
maxTotalChars: 4e6
|
|
415
|
-
});
|
|
416
|
-
var ChatMessage = z3.object({
|
|
417
|
-
role: z3.enum(["system", "user", "assistant"]),
|
|
418
|
-
content: z3.string().max(PAYLOAD_LIMITS.maxTextChars)
|
|
419
|
-
}).strict();
|
|
420
|
-
var GeneratePayload = z3.object({
|
|
421
|
-
prompt: z3.string().min(1).max(PAYLOAD_LIMITS.maxTextChars),
|
|
422
|
-
system: z3.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
|
|
423
|
-
}).strict().refine(
|
|
424
|
-
(payload) => payload.prompt.length + (payload.system?.length ?? 0) <= PAYLOAD_LIMITS.maxTotalChars,
|
|
425
|
-
{
|
|
426
|
-
message: `payload exceeds ${String(PAYLOAD_LIMITS.maxTotalChars)} characters`
|
|
427
|
-
}
|
|
428
|
-
);
|
|
429
|
-
var ChatPayload = z3.object({
|
|
430
|
-
messages: z3.array(ChatMessage).min(1).max(PAYLOAD_LIMITS.maxMessages),
|
|
431
|
-
system: z3.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
|
|
432
|
-
}).strict().refine(
|
|
433
|
-
(payload) => payload.messages.reduce((sum, m) => sum + m.content.length, 0) + (payload.system?.length ?? 0) <= PAYLOAD_LIMITS.maxTotalChars,
|
|
434
|
-
{
|
|
435
|
-
message: `payload exceeds ${String(PAYLOAD_LIMITS.maxTotalChars)} characters`
|
|
436
|
-
}
|
|
437
|
-
);
|
|
438
|
-
var JobKind = z3.enum(["llm.generate", "llm.chat"]);
|
|
439
|
-
var JOB_KINDS = Object.freeze(JobKind.options);
|
|
440
|
-
var KindedPayload = z3.discriminatedUnion("kind", [
|
|
441
|
-
// Strict on the wrappers too. A union member that strips is a door beside
|
|
442
|
-
// the one that is locked: the payloads inside are strict, and an extra key
|
|
443
|
-
// on the envelope vanished just as quietly.
|
|
444
|
-
z3.object({ kind: z3.literal("llm.generate"), payload: GeneratePayload }).strict(),
|
|
445
|
-
z3.object({ kind: z3.literal("llm.chat"), payload: ChatPayload }).strict()
|
|
446
|
-
]);
|
|
447
|
-
function isJobKind(value) {
|
|
448
|
-
return JOB_KINDS.includes(value);
|
|
449
|
-
}
|
|
450
|
-
function payloadTextLength(kinded) {
|
|
451
|
-
if (kinded.kind === "llm.generate") {
|
|
452
|
-
return kinded.payload.prompt.length + (kinded.payload.system?.length ?? 0);
|
|
453
|
-
}
|
|
454
|
-
const messages = kinded.payload.messages.reduce(
|
|
455
|
-
(sum, m) => sum + m.content.length,
|
|
456
|
-
0
|
|
457
|
-
);
|
|
458
|
-
return messages + (kinded.payload.system?.length ?? 0);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
525
|
// src/job.ts
|
|
462
|
-
import { z as
|
|
526
|
+
import { z as z7 } from "zod";
|
|
463
527
|
|
|
464
528
|
// src/grant.ts
|
|
465
529
|
import { Buffer as Buffer2 } from "buffer";
|
|
466
530
|
import { z as z5 } from "zod";
|
|
467
|
-
|
|
468
|
-
// src/keys.ts
|
|
469
|
-
import {
|
|
470
|
-
createHash,
|
|
471
|
-
createPrivateKey,
|
|
472
|
-
createPublicKey,
|
|
473
|
-
generateKeyPairSync,
|
|
474
|
-
sign,
|
|
475
|
-
verify
|
|
476
|
-
} from "crypto";
|
|
477
|
-
import { z as z4 } from "zod";
|
|
478
|
-
var PublicIdentity = z4.object({
|
|
479
|
-
/** Raw Ed25519 public key. The pinned one. */
|
|
480
|
-
identity: z4.string().min(1),
|
|
481
|
-
/** Raw X25519 public key, for sealing to this party. */
|
|
482
|
-
encryption: z4.string().min(1),
|
|
483
|
-
/**
|
|
484
|
-
* Ed25519 signature over the encryption key, by the identity key.
|
|
485
|
-
*
|
|
486
|
-
* This is what stops an upstream substituting an encryption key of its
|
|
487
|
-
* own while relaying a genuine identity: the receiver pins the identity
|
|
488
|
-
* and refuses any encryption key not signed by it.
|
|
489
|
-
*/
|
|
490
|
-
encryptionSig: z4.string().min(1)
|
|
491
|
-
}).strict();
|
|
492
|
-
var StoredKeys = z4.object({
|
|
493
|
-
version: z4.literal(1),
|
|
494
|
-
identityPublic: z4.string().min(1),
|
|
495
|
-
identityPrivate: z4.string().min(1),
|
|
496
|
-
encryptionPublic: z4.string().min(1),
|
|
497
|
-
encryptionPrivate: z4.string().min(1),
|
|
498
|
-
encryptionSig: z4.string().min(1),
|
|
499
|
-
createdAt: z4.number().int().positive()
|
|
500
|
-
}).strict();
|
|
501
|
-
var ENCRYPTION_KEY_CONTEXT = "byollm/v1/encryption-key";
|
|
502
|
-
function rawPublic(key) {
|
|
503
|
-
const jwk = key.export({ format: "jwk" });
|
|
504
|
-
const x = jwk.x;
|
|
505
|
-
if (typeof x !== "string") throw new Error("key has no raw public component");
|
|
506
|
-
return x;
|
|
507
|
-
}
|
|
508
|
-
function importPublic(raw, crv) {
|
|
509
|
-
return createPublicKey({ key: { kty: "OKP", crv, x: raw }, format: "jwk" });
|
|
510
|
-
}
|
|
511
|
-
function importPrivate(stored) {
|
|
512
|
-
return createPrivateKey({
|
|
513
|
-
key: Buffer.from(stored, "base64"),
|
|
514
|
-
type: "pkcs8",
|
|
515
|
-
format: "der"
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
var exportPrivate = (key) => key.export({ type: "pkcs8", format: "der" }).toString("base64");
|
|
519
|
-
function generateKeys(now) {
|
|
520
|
-
const identity = generateKeyPairSync("ed25519");
|
|
521
|
-
const encryption = generateKeyPairSync("x25519");
|
|
522
|
-
const encryptionPublic = rawPublic(encryption.publicKey);
|
|
523
|
-
return {
|
|
524
|
-
version: 1,
|
|
525
|
-
identityPublic: rawPublic(identity.publicKey),
|
|
526
|
-
identityPrivate: exportPrivate(identity.privateKey),
|
|
527
|
-
encryptionPublic,
|
|
528
|
-
encryptionPrivate: exportPrivate(encryption.privateKey),
|
|
529
|
-
encryptionSig: sign(
|
|
530
|
-
null,
|
|
531
|
-
Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${encryptionPublic}`),
|
|
532
|
-
identity.privateKey
|
|
533
|
-
).toString("base64url"),
|
|
534
|
-
createdAt: now
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
function publicIdentityOf(keys) {
|
|
538
|
-
return {
|
|
539
|
-
identity: keys.identityPublic,
|
|
540
|
-
encryption: keys.encryptionPublic,
|
|
541
|
-
encryptionSig: keys.encryptionSig
|
|
542
|
-
};
|
|
543
|
-
}
|
|
544
|
-
function verifyPublicIdentity(identity) {
|
|
545
|
-
try {
|
|
546
|
-
return verify(
|
|
547
|
-
null,
|
|
548
|
-
Buffer.from(`${ENCRYPTION_KEY_CONTEXT}:${identity.encryption}`),
|
|
549
|
-
importPublic(identity.identity, "Ed25519"),
|
|
550
|
-
Buffer.from(identity.encryptionSig, "base64url")
|
|
551
|
-
);
|
|
552
|
-
} catch {
|
|
553
|
-
return false;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
function signWith(keys, data) {
|
|
557
|
-
return sign(null, data, importPrivate(keys.identityPrivate)).toString(
|
|
558
|
-
"base64url"
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
|
-
function verifyWith(identityPublic, data, signature) {
|
|
562
|
-
try {
|
|
563
|
-
return verify(
|
|
564
|
-
null,
|
|
565
|
-
data,
|
|
566
|
-
importPublic(identityPublic, "Ed25519"),
|
|
567
|
-
Buffer.from(signature, "base64url")
|
|
568
|
-
);
|
|
569
|
-
} catch {
|
|
570
|
-
return false;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
574
|
-
function fingerprint(identityPublic) {
|
|
575
|
-
const digest = createHash("sha256").update(Buffer.from(identityPublic, "base64url")).digest();
|
|
576
|
-
let bits = 0;
|
|
577
|
-
let value = 0;
|
|
578
|
-
let out = "";
|
|
579
|
-
for (const byte of digest.subarray(0, 15)) {
|
|
580
|
-
value = value << 8 | byte;
|
|
581
|
-
bits += 8;
|
|
582
|
-
while (bits >= 5) {
|
|
583
|
-
out += ALPHABET.charAt(value >>> bits - 5 & 31);
|
|
584
|
-
bits -= 5;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
const groups = out.match(/.{1,4}/g) ?? [];
|
|
588
|
-
return `BYOLLM-${groups.join("-")}`;
|
|
589
|
-
}
|
|
590
|
-
var keyId = (identityPublic) => fingerprint(identityPublic);
|
|
591
|
-
|
|
592
|
-
// src/grant.ts
|
|
593
531
|
var GRANT_MAX_AGE_MS = 12e4;
|
|
594
532
|
var CLOCK_SKEW_WARN_MS = 3e4;
|
|
595
533
|
var CLOCK_ATTRIBUTION_MS = 5e3;
|
|
@@ -694,8 +632,63 @@ function verifyGrant(input) {
|
|
|
694
632
|
) ? null : "bad-signature";
|
|
695
633
|
}
|
|
696
634
|
|
|
635
|
+
// src/kinds.ts
|
|
636
|
+
import { z as z6 } from "zod";
|
|
637
|
+
var PAYLOAD_LIMITS = Object.freeze({
|
|
638
|
+
/** Max characters in any single text field. */
|
|
639
|
+
maxTextChars: 1e6,
|
|
640
|
+
/** Max messages in an `llm.chat` conversation. */
|
|
641
|
+
maxMessages: 256,
|
|
642
|
+
/** Max characters across the whole payload. */
|
|
643
|
+
maxTotalChars: 4e6
|
|
644
|
+
});
|
|
645
|
+
var ChatMessage = z6.object({
|
|
646
|
+
role: z6.enum(["system", "user", "assistant"]),
|
|
647
|
+
content: z6.string().max(PAYLOAD_LIMITS.maxTextChars)
|
|
648
|
+
}).strict();
|
|
649
|
+
var GeneratePayload = z6.object({
|
|
650
|
+
prompt: z6.string().min(1).max(PAYLOAD_LIMITS.maxTextChars),
|
|
651
|
+
system: z6.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
|
|
652
|
+
}).strict().refine(
|
|
653
|
+
(payload) => payload.prompt.length + (payload.system?.length ?? 0) <= PAYLOAD_LIMITS.maxTotalChars,
|
|
654
|
+
{
|
|
655
|
+
message: `payload exceeds ${String(PAYLOAD_LIMITS.maxTotalChars)} characters`
|
|
656
|
+
}
|
|
657
|
+
);
|
|
658
|
+
var ChatPayload = z6.object({
|
|
659
|
+
messages: z6.array(ChatMessage).min(1).max(PAYLOAD_LIMITS.maxMessages),
|
|
660
|
+
system: z6.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
|
|
661
|
+
}).strict().refine(
|
|
662
|
+
(payload) => payload.messages.reduce((sum, m) => sum + m.content.length, 0) + (payload.system?.length ?? 0) <= PAYLOAD_LIMITS.maxTotalChars,
|
|
663
|
+
{
|
|
664
|
+
message: `payload exceeds ${String(PAYLOAD_LIMITS.maxTotalChars)} characters`
|
|
665
|
+
}
|
|
666
|
+
);
|
|
667
|
+
var JobKind = z6.enum(["llm.generate", "llm.chat"]);
|
|
668
|
+
var JOB_KINDS = Object.freeze(JobKind.options);
|
|
669
|
+
var KindedPayload = z6.discriminatedUnion("kind", [
|
|
670
|
+
// Strict on the wrappers too. A union member that strips is a door beside
|
|
671
|
+
// the one that is locked: the payloads inside are strict, and an extra key
|
|
672
|
+
// on the envelope vanished just as quietly.
|
|
673
|
+
z6.object({ kind: z6.literal("llm.generate"), payload: GeneratePayload }).strict(),
|
|
674
|
+
z6.object({ kind: z6.literal("llm.chat"), payload: ChatPayload }).strict()
|
|
675
|
+
]);
|
|
676
|
+
function isJobKind(value) {
|
|
677
|
+
return JOB_KINDS.includes(value);
|
|
678
|
+
}
|
|
679
|
+
function payloadTextLength(kinded) {
|
|
680
|
+
if (kinded.kind === "llm.generate") {
|
|
681
|
+
return kinded.payload.prompt.length + (kinded.payload.system?.length ?? 0);
|
|
682
|
+
}
|
|
683
|
+
const messages = kinded.payload.messages.reduce(
|
|
684
|
+
(sum, m) => sum + m.content.length,
|
|
685
|
+
0
|
|
686
|
+
);
|
|
687
|
+
return messages + (kinded.payload.system?.length ?? 0);
|
|
688
|
+
}
|
|
689
|
+
|
|
697
690
|
// src/job.ts
|
|
698
|
-
var JobState =
|
|
691
|
+
var JobState = z7.enum([
|
|
699
692
|
"queued",
|
|
700
693
|
"claimed",
|
|
701
694
|
"running",
|
|
@@ -727,7 +720,7 @@ var TRANSITIONS = Object.freeze({
|
|
|
727
720
|
function canTransition(from, to) {
|
|
728
721
|
return TRANSITIONS[from].includes(to);
|
|
729
722
|
}
|
|
730
|
-
var Lease =
|
|
723
|
+
var Lease = z7.object({
|
|
731
724
|
/**
|
|
732
725
|
* Identifies *this* grant, not just its holder.
|
|
733
726
|
*
|
|
@@ -742,20 +735,20 @@ var Lease = z6.object({
|
|
|
742
735
|
* and release *is*, per lease, but not across leases, because nothing in
|
|
743
736
|
* the request said which one.
|
|
744
737
|
*/
|
|
745
|
-
id:
|
|
738
|
+
id: z7.string().min(1),
|
|
746
739
|
/** The runner holding the lease. */
|
|
747
|
-
runnerId:
|
|
740
|
+
runnerId: z7.string().min(1),
|
|
748
741
|
/** Epoch milliseconds after which the claim is void. */
|
|
749
|
-
expiresAt:
|
|
742
|
+
expiresAt: z7.number().int().positive()
|
|
750
743
|
}).strict();
|
|
751
|
-
var JobPayload =
|
|
752
|
-
var ClaimedJob =
|
|
753
|
-
id:
|
|
744
|
+
var JobPayload = z7.union([GeneratePayload, ChatPayload]);
|
|
745
|
+
var ClaimedJob = z7.object({
|
|
746
|
+
id: z7.string().min(1),
|
|
754
747
|
kind: JobKind,
|
|
755
748
|
payload: JobPayload,
|
|
756
749
|
audience: Audience,
|
|
757
750
|
/** The app's id for the user who enqueued it. */
|
|
758
|
-
owner:
|
|
751
|
+
owner: z7.string().min(1),
|
|
759
752
|
/**
|
|
760
753
|
* Which site's job — V1-3.
|
|
761
754
|
*
|
|
@@ -768,7 +761,7 @@ var ClaimedJob = z6.object({
|
|
|
768
761
|
* one, and so this reads as what it is: a fact about where the work came
|
|
769
762
|
* from, not a second copy of the routing key.
|
|
770
763
|
*/
|
|
771
|
-
site:
|
|
764
|
+
site: z7.string().min(1).optional(),
|
|
772
765
|
/**
|
|
773
766
|
* Which of the owner's services runs this — resolved, not requested.
|
|
774
767
|
*
|
|
@@ -785,25 +778,25 @@ var ClaimedJob = z6.object({
|
|
|
785
778
|
* Optional, because direct mode has no control plane to resolve anything
|
|
786
779
|
* and the owner's own defaults answer under the ambiguity law.
|
|
787
780
|
*/
|
|
788
|
-
service:
|
|
781
|
+
service: z7.string().min(1).optional(),
|
|
789
782
|
lease: Lease
|
|
790
783
|
}).strict();
|
|
791
|
-
var ResultProvenance =
|
|
784
|
+
var ResultProvenance = z7.object({
|
|
792
785
|
/** The audience the job ran under. */
|
|
793
786
|
audience: Audience,
|
|
794
787
|
/** The runner that produced it. */
|
|
795
|
-
runnerId:
|
|
788
|
+
runnerId: z7.string().min(1),
|
|
796
789
|
/** The runner owner's id in this app's namespace. */
|
|
797
|
-
runnerOwner:
|
|
790
|
+
runnerOwner: z7.string().min(1),
|
|
798
791
|
/** Which backend class produced it — an HTTP call or a sandboxed spawn. */
|
|
799
792
|
backendClass: BackendClass,
|
|
800
793
|
/** The model the runner reports having used. */
|
|
801
|
-
model:
|
|
794
|
+
model: z7.string().min(1),
|
|
802
795
|
/**
|
|
803
796
|
* False only for `self` jobs. When true the app MUST treat `text` as
|
|
804
797
|
* untrusted third-party content.
|
|
805
798
|
*/
|
|
806
|
-
untrusted:
|
|
799
|
+
untrusted: z7.boolean()
|
|
807
800
|
}).strict();
|
|
808
801
|
function provenanceFor(input) {
|
|
809
802
|
return {
|
|
@@ -815,35 +808,35 @@ function provenanceFor(input) {
|
|
|
815
808
|
untrusted: input.audience !== "private"
|
|
816
809
|
};
|
|
817
810
|
}
|
|
818
|
-
var RunMetadata =
|
|
811
|
+
var RunMetadata = z7.object({
|
|
819
812
|
/** Which model actually served it. */
|
|
820
|
-
model:
|
|
813
|
+
model: z7.string().min(1),
|
|
821
814
|
backendClass: BackendClass,
|
|
822
815
|
/** Wall-clock milliseconds the backend call took. */
|
|
823
|
-
durationMs:
|
|
816
|
+
durationMs: z7.number().int().nonnegative()
|
|
824
817
|
}).strict();
|
|
825
|
-
var JobResultOk =
|
|
826
|
-
outcome:
|
|
827
|
-
text:
|
|
818
|
+
var JobResultOk = z7.object({
|
|
819
|
+
outcome: z7.literal("ok"),
|
|
820
|
+
text: z7.string(),
|
|
828
821
|
/** Optional reference to a stored artifact; never a local path. */
|
|
829
|
-
artifactUrl:
|
|
822
|
+
artifactUrl: z7.url().optional()
|
|
830
823
|
}).strict();
|
|
831
|
-
var JobResultError =
|
|
832
|
-
outcome:
|
|
833
|
-
code:
|
|
834
|
-
message:
|
|
824
|
+
var JobResultError = z7.object({
|
|
825
|
+
outcome: z7.literal("error"),
|
|
826
|
+
code: z7.string().min(1),
|
|
827
|
+
message: z7.string().min(1),
|
|
835
828
|
/** Whether the app may reasonably re-enqueue. */
|
|
836
|
-
retryable:
|
|
829
|
+
retryable: z7.boolean()
|
|
837
830
|
}).strict();
|
|
838
|
-
var JobResultCanceled =
|
|
839
|
-
outcome:
|
|
831
|
+
var JobResultCanceled = z7.object({
|
|
832
|
+
outcome: z7.literal("canceled")
|
|
840
833
|
}).strict();
|
|
841
|
-
var JobOutcome =
|
|
834
|
+
var JobOutcome = z7.discriminatedUnion("outcome", [
|
|
842
835
|
JobResultOk,
|
|
843
836
|
JobResultError,
|
|
844
837
|
JobResultCanceled
|
|
845
838
|
]);
|
|
846
|
-
var RefusalReason =
|
|
839
|
+
var RefusalReason = z7.enum([
|
|
847
840
|
/**
|
|
848
841
|
* Two or more services answer this kind and the owner has named no default,
|
|
849
842
|
* so the kind is withheld. Nobody may pick on the owner's behalf — the wrong
|
|
@@ -873,19 +866,19 @@ var RefusalReason = z6.enum([
|
|
|
873
866
|
*/
|
|
874
867
|
"default-unusable"
|
|
875
868
|
]);
|
|
876
|
-
var JobRefused =
|
|
877
|
-
outcome:
|
|
869
|
+
var JobRefused = z7.object({
|
|
870
|
+
outcome: z7.literal("refused"),
|
|
878
871
|
reason: RefusalReason,
|
|
879
872
|
/** Plain words for a human reading a log, never parsed. */
|
|
880
|
-
message:
|
|
873
|
+
message: z7.string().min(1)
|
|
881
874
|
}).strict();
|
|
882
875
|
var REFUSAL_TEXT = Object.freeze({
|
|
883
876
|
"default-ambiguity": "this device serves that kind from more than one service and its owner has not chosen which",
|
|
884
877
|
"default-unusable": "this device's default for that kind cannot run work for you"
|
|
885
878
|
});
|
|
886
|
-
var SealedOutcome =
|
|
887
|
-
var DeliveredResult =
|
|
888
|
-
jobId:
|
|
879
|
+
var SealedOutcome = z7.object({ outcome: JobOutcome, ran: RunMetadata }).strict();
|
|
880
|
+
var DeliveredResult = z7.object({
|
|
881
|
+
jobId: z7.string().min(1),
|
|
889
882
|
state: JobState,
|
|
890
883
|
outcome: JobOutcome.optional(),
|
|
891
884
|
provenance: ResultProvenance.optional(),
|
|
@@ -903,9 +896,9 @@ var DeliveredResult = z6.object({
|
|
|
903
896
|
* runner ran the job, and the *server* stamps it — an app cannot supply
|
|
904
897
|
* a substitute that hides what it is.
|
|
905
898
|
*/
|
|
906
|
-
fallback:
|
|
899
|
+
fallback: z7.literal(true).optional()
|
|
907
900
|
}).strict();
|
|
908
|
-
var SizeClass =
|
|
901
|
+
var SizeClass = z7.enum(["small", "medium", "large", "unbounded"]);
|
|
909
902
|
var SIZE_CLASSES = Object.freeze(SizeClass.options);
|
|
910
903
|
var MAX_ENVELOPE_BYTES = 10 * 1024 * 1024;
|
|
911
904
|
function envelopeBytes(envelope) {
|
|
@@ -926,11 +919,11 @@ function sizeClassOf(textChars) {
|
|
|
926
919
|
if (textChars <= SIZE_CLASS_LIMITS.medium) return "medium";
|
|
927
920
|
return "large";
|
|
928
921
|
}
|
|
929
|
-
var JobStub =
|
|
930
|
-
id:
|
|
922
|
+
var JobStub = z7.object({
|
|
923
|
+
id: z7.string().min(1),
|
|
931
924
|
kind: JobKind,
|
|
932
925
|
/** The app's id for the user who enqueued it. */
|
|
933
|
-
owner:
|
|
926
|
+
owner: z7.string().min(1),
|
|
934
927
|
/**
|
|
935
928
|
* Which site this job belongs to — byollm_009 Amendment A §A.3.
|
|
936
929
|
*
|
|
@@ -954,7 +947,7 @@ var JobStub = z6.object({
|
|
|
954
947
|
* overlap window, and a daemon re-keys its own map by verifying that
|
|
955
948
|
* signature against the key it already pinned (§A.3.1).
|
|
956
949
|
*/
|
|
957
|
-
site:
|
|
950
|
+
site: z7.string().min(1),
|
|
958
951
|
audience: Audience,
|
|
959
952
|
// `audienceAllow` is **not** here, and its absence is the enforcement —
|
|
960
953
|
// cloud_008 §0.2.
|
|
@@ -1009,12 +1002,12 @@ var JobStub = z6.object({
|
|
|
1009
1002
|
* `NO_PAYLOAD_ROUTING` draws: the prompt cannot reach it, so no amount of
|
|
1010
1003
|
* user text can influence what runs.
|
|
1011
1004
|
*/
|
|
1012
|
-
purpose:
|
|
1005
|
+
purpose: z7.string().min(1).optional(),
|
|
1013
1006
|
sizeClass: SizeClass,
|
|
1014
1007
|
/** Reserved for byollm_006. False until streaming exists. */
|
|
1015
|
-
streaming:
|
|
1008
|
+
streaming: z7.boolean(),
|
|
1016
1009
|
/** Epoch ms after which the work is pointless; bounds ciphertext retention. */
|
|
1017
|
-
deadlineAt:
|
|
1010
|
+
deadlineAt: z7.number().int().positive()
|
|
1018
1011
|
}).strict();
|
|
1019
1012
|
var ClaimedStub = JobStub.extend({
|
|
1020
1013
|
lease: Lease,
|
|
@@ -1024,21 +1017,21 @@ var ClaimedStub = JobStub.extend({
|
|
|
1024
1017
|
// src/envelope.ts
|
|
1025
1018
|
import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2 } from "crypto";
|
|
1026
1019
|
import sodium from "libsodium-wrappers";
|
|
1027
|
-
import { z as
|
|
1020
|
+
import { z as z8 } from "zod";
|
|
1028
1021
|
var readied;
|
|
1029
1022
|
async function cryptoReady() {
|
|
1030
1023
|
readied ??= sodium.ready;
|
|
1031
1024
|
await readied;
|
|
1032
1025
|
}
|
|
1033
1026
|
var ENVELOPE_MAX_AGE_MS = 24 * 60 * 6e4;
|
|
1034
|
-
var EnvelopeDirection =
|
|
1035
|
-
var SealedEnvelope =
|
|
1027
|
+
var EnvelopeDirection = z8.enum(["payload", "result"]);
|
|
1028
|
+
var SealedEnvelope = z8.object({
|
|
1036
1029
|
/** Base64url `crypto_box_seal` output over the signed plaintext. */
|
|
1037
|
-
ciphertext:
|
|
1030
|
+
ciphertext: z8.string().min(1),
|
|
1038
1031
|
/** Who this was sealed to — the recipient checks it is them. */
|
|
1039
|
-
recipientKeyId:
|
|
1032
|
+
recipientKeyId: z8.string().min(1),
|
|
1040
1033
|
/** Who signed it — the recipient checks this against its pin. */
|
|
1041
|
-
senderKeyId:
|
|
1034
|
+
senderKeyId: z8.string().min(1),
|
|
1042
1035
|
direction: EnvelopeDirection,
|
|
1043
1036
|
/**
|
|
1044
1037
|
* When this ciphertext stops being worth keeping.
|
|
@@ -1052,7 +1045,7 @@ var SealedEnvelope = z7.object({
|
|
|
1052
1045
|
* Not trusted as written: it is also inside the signature, so a changed
|
|
1053
1046
|
* deadline fails to verify.
|
|
1054
1047
|
*/
|
|
1055
|
-
deadlineAt:
|
|
1048
|
+
deadlineAt: z8.number().int().positive()
|
|
1056
1049
|
}).strict();
|
|
1057
1050
|
function signedBody(context, plaintext) {
|
|
1058
1051
|
return Buffer.from(
|
|
@@ -1145,756 +1138,147 @@ async function open(input) {
|
|
|
1145
1138
|
return { ok: true, plaintext: claims["plaintext"] };
|
|
1146
1139
|
}
|
|
1147
1140
|
|
|
1148
|
-
// src/
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
function canonicalRequest(input) {
|
|
1161
|
-
const digest = createHash2("sha256").update(input.body, "utf8").digest("hex");
|
|
1162
|
-
return Buffer.from(
|
|
1163
|
-
[
|
|
1164
|
-
"byollm/v1/request",
|
|
1165
|
-
input.endpoint,
|
|
1166
|
-
input.runnerId,
|
|
1167
|
-
String(input.issuedAt),
|
|
1168
|
-
digest
|
|
1169
|
-
].join("\n"),
|
|
1170
|
-
"utf8"
|
|
1171
|
-
);
|
|
1172
|
-
}
|
|
1173
|
-
function signRequest(keys, input) {
|
|
1174
|
-
return {
|
|
1175
|
-
runnerId: input.runnerId,
|
|
1176
|
-
issuedAt: input.issuedAt,
|
|
1177
|
-
signature: signWith(keys, canonicalRequest(input))
|
|
1178
|
-
};
|
|
1179
|
-
}
|
|
1180
|
-
function signSiteRequest(keys, input) {
|
|
1181
|
-
return signRequest(keys, {
|
|
1182
|
-
endpoint: siteEndpoint(input.endpoint),
|
|
1183
|
-
runnerId: input.siteId,
|
|
1184
|
-
issuedAt: input.issuedAt,
|
|
1185
|
-
body: input.body
|
|
1186
|
-
});
|
|
1141
|
+
// src/wire.ts
|
|
1142
|
+
var PROTOCOL_VERSION = "1";
|
|
1143
|
+
var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze([
|
|
1144
|
+
PROTOCOL_VERSION
|
|
1145
|
+
]);
|
|
1146
|
+
var MIN_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0] ?? PROTOCOL_VERSION;
|
|
1147
|
+
function declaredVersion(input) {
|
|
1148
|
+
const { body, query } = input;
|
|
1149
|
+
if (typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion")) {
|
|
1150
|
+
return body.protocolVersion;
|
|
1151
|
+
}
|
|
1152
|
+
return query?.get("protocolVersion") ?? void 0;
|
|
1187
1153
|
}
|
|
1188
|
-
function
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1154
|
+
function checkProtocolVersion(body) {
|
|
1155
|
+
const declared = typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion") ? body.protocolVersion : void 0;
|
|
1156
|
+
if (typeof declared !== "string" || declared.length === 0) {
|
|
1157
|
+
return {
|
|
1158
|
+
error: "unsupported-protocol-version",
|
|
1159
|
+
message: `this request declared no protocol version. Upgrade the daemon: \`${UPGRADE_COMMAND}\`.`,
|
|
1160
|
+
supported: SUPPORTED_PROTOCOL_VERSIONS,
|
|
1161
|
+
minimum: MIN_PROTOCOL_VERSION
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
if (!SUPPORTED_PROTOCOL_VERSIONS.includes(declared)) {
|
|
1165
|
+
return {
|
|
1166
|
+
error: "unsupported-protocol-version",
|
|
1167
|
+
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."),
|
|
1168
|
+
supported: SUPPORTED_PROTOCOL_VERSIONS,
|
|
1169
|
+
minimum: MIN_PROTOCOL_VERSION
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
return null;
|
|
1193
1173
|
}
|
|
1194
|
-
var
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
input.signature.signature
|
|
1207
|
-
);
|
|
1208
|
-
return ok ? null : "bad-signature";
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
// src/manifest.ts
|
|
1212
|
-
import { z as z9 } from "zod";
|
|
1213
|
-
var RESERVED_PURPOSE = "default";
|
|
1214
|
-
var RENDERABLE = /^[^\p{Cc}\p{Cf}\p{Cs}\p{Co}]+$/u;
|
|
1215
|
-
var renderable = (max, what) => z9.string().min(1).max(max).regex(
|
|
1216
|
-
RENDERABLE,
|
|
1217
|
-
`a ${what} is text a person reads \u2014 no control characters, direction overrides or zero-width padding`
|
|
1218
|
-
).refine((value) => value.trim() !== "", {
|
|
1219
|
-
message: `a ${what} cannot be blank`
|
|
1220
|
-
});
|
|
1221
|
-
var PurposeKey = z9.string().regex(
|
|
1222
|
-
/^[a-z0-9][a-z0-9-]*$/,
|
|
1223
|
-
"a purpose key is a lowercase slug \u2014 letters, digits and hyphens"
|
|
1224
|
-
).max(64);
|
|
1225
|
-
var Purpose = z9.object({
|
|
1174
|
+
var UPGRADE_COMMAND = "npm i -g byollm@latest";
|
|
1175
|
+
var PROTOCOL_PREFIX = "/byollm";
|
|
1176
|
+
var ENDPOINTS = Object.freeze([
|
|
1177
|
+
"pair",
|
|
1178
|
+
"claim",
|
|
1179
|
+
"fetch",
|
|
1180
|
+
"heartbeat",
|
|
1181
|
+
"result",
|
|
1182
|
+
"release"
|
|
1183
|
+
]);
|
|
1184
|
+
var Capability = z9.object({
|
|
1185
|
+
kind: JobKind,
|
|
1226
1186
|
/**
|
|
1227
|
-
*
|
|
1187
|
+
* The owner's name for the service answering this kind — byollm_016.
|
|
1228
1188
|
*
|
|
1229
|
-
*
|
|
1230
|
-
*
|
|
1231
|
-
*
|
|
1189
|
+
* A device advertises *which* of its services serves a kind, not merely
|
|
1190
|
+
* that something does. **A site never sees this**, and never did after
|
|
1191
|
+
* Amendment L: it is what a control plane resolves a person's mapping
|
|
1192
|
+
* against, so that the service a grant names is one this device actually
|
|
1193
|
+
* offers rather than one somebody invented.
|
|
1194
|
+
*
|
|
1195
|
+
* `isDefault` used to sit beside it, saying which row an unselected job
|
|
1196
|
+
* took. Nothing selects any more — a job names a purpose and a person's
|
|
1197
|
+
* mapping names a service — so there is no unselected job for a default
|
|
1198
|
+
* to catch, and the field went with the machinery it served.
|
|
1232
1199
|
*/
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1200
|
+
service: z9.string().min(1),
|
|
1201
|
+
backendId: BackendIdSchema,
|
|
1202
|
+
backendClass: BackendClass,
|
|
1203
|
+
model: z9.string().min(1),
|
|
1236
1204
|
/**
|
|
1237
|
-
*
|
|
1205
|
+
* Models this device's CLI knows about — byollm_017 ruling 3.
|
|
1238
1206
|
*
|
|
1239
|
-
*
|
|
1240
|
-
*
|
|
1241
|
-
*
|
|
1207
|
+
* **Suggestions, not a vocabulary.** Free text is always allowed: the
|
|
1208
|
+
* promise is that a model released this morning works this morning, and a
|
|
1209
|
+
* frozen list anywhere a person picks from breaks that on the first day
|
|
1210
|
+
* it matters. What makes free text safe is ruling 2 — a model is probed
|
|
1211
|
+
* before it is stored, so "found is not works" is answered by the device
|
|
1212
|
+
* rather than by a list.
|
|
1213
|
+
*
|
|
1214
|
+
* Announced with the capability rather than kept in the dashboard,
|
|
1215
|
+
* because the answer is "what does THIS device's CLI know" and only the
|
|
1216
|
+
* device can say. A list held cloud-side would be one more thing to
|
|
1217
|
+
* update on release day, and wrong for anybody who had not upgraded.
|
|
1218
|
+
*
|
|
1219
|
+
* Optional, and empty is legal. A backend with nothing to suggest — a
|
|
1220
|
+
* local server serving one model — is not a backend in an error state,
|
|
1221
|
+
* and a reader must not render an absent list as "no models available".
|
|
1242
1222
|
*/
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
})
|
|
1223
|
+
knownModels: z9.array(z9.string().min(1)).optional(),
|
|
1224
|
+
offerScope: OfferScope
|
|
1246
1225
|
}).strict();
|
|
1247
|
-
var
|
|
1248
|
-
var
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
});
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
var RETIREMENT_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1265
|
-
var MAX_SUCCESSION_CHAIN = 64;
|
|
1266
|
-
var Succession = z10.object({
|
|
1226
|
+
var CapabilityMatrix = z9.array(Capability);
|
|
1227
|
+
var WithheldKind = z9.object({
|
|
1228
|
+
kind: JobKind,
|
|
1229
|
+
claimants: z9.array(
|
|
1230
|
+
z9.object({ service: z9.string().min(1), offer: OfferScope }).strict()
|
|
1231
|
+
).min(2)
|
|
1232
|
+
}).strict();
|
|
1233
|
+
var GrantRef = z9.object({ jobId: z9.string().min(1), leaseId: z9.string().min(1) }).strict();
|
|
1234
|
+
var PairStartRequest = z9.object({
|
|
1235
|
+
protocolVersion: z9.literal(PROTOCOL_VERSION),
|
|
1236
|
+
action: z9.literal("start"),
|
|
1237
|
+
daemon: z9.object({
|
|
1238
|
+
version: z9.string().min(1),
|
|
1239
|
+
/** Shown in the app's runner list so a user can tell their machines apart. */
|
|
1240
|
+
label: z9.string().min(1).max(120),
|
|
1241
|
+
platform: z9.enum(["darwin", "linux", "win32"])
|
|
1242
|
+
}).strict(),
|
|
1267
1243
|
/**
|
|
1268
|
-
*
|
|
1244
|
+
* This machine's public keys (byollm_009 §5).
|
|
1269
1245
|
*
|
|
1270
|
-
*
|
|
1271
|
-
*
|
|
1272
|
-
*
|
|
1246
|
+
* Pairing is where the two parties learn each other's identities, because
|
|
1247
|
+
* it is the one moment a human is already deciding to trust: the approval
|
|
1248
|
+
* click. A key exchanged anywhere else would be a key nobody chose.
|
|
1273
1249
|
*/
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
signature: z10.string().min(1)
|
|
1250
|
+
device: PublicIdentity,
|
|
1251
|
+
capabilities: CapabilityMatrix
|
|
1277
1252
|
}).strict();
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
function walkSuccession(input) {
|
|
1303
|
-
const { current, chain, approved } = input;
|
|
1304
|
-
if (chain.length === 0) return { path: [current], failure: "no-chain" };
|
|
1305
|
-
if (chain.length > MAX_SUCCESSION_CHAIN)
|
|
1306
|
-
return { path: [current], failure: "too-long" };
|
|
1307
|
-
const steps = [...chain].reverse();
|
|
1308
|
-
const path = [current];
|
|
1309
|
-
let succeeding = current;
|
|
1310
|
-
for (const link of steps) {
|
|
1311
|
-
if (!verifyLink(link, succeeding)) return { path, failure: "broken-link" };
|
|
1312
|
-
const previous = keyId(link.identity.identity);
|
|
1313
|
-
path.unshift(previous);
|
|
1314
|
-
if (approved(previous)) return { path, from: previous };
|
|
1315
|
-
succeeding = previous;
|
|
1316
|
-
}
|
|
1317
|
-
return { path, failure: "unknown-origin" };
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
// src/musts.ts
|
|
1321
|
-
function kindsOf(must2) {
|
|
1322
|
-
return typeof must2.verifiedBy === "string" ? [must2.verifiedBy] : must2.verifiedBy;
|
|
1323
|
-
}
|
|
1324
|
-
var must = (m) => Object.freeze(m);
|
|
1325
|
-
var MUSTS = Object.freeze({
|
|
1326
|
-
// ---- Pairing and identity -------------------------------------------
|
|
1327
|
-
PAIR_ONE_USER: must({
|
|
1328
|
-
id: "PAIR_ONE_USER",
|
|
1329
|
-
statement: "A runner token MUST be bound to exactly one user; a daemon MUST refuse work not attributable to its paired user.",
|
|
1330
|
-
enforcedBy: "both",
|
|
1331
|
-
verifiedBy: "conformance",
|
|
1332
|
-
source: "byollm_001 \xA7MUSTs"
|
|
1333
|
-
}),
|
|
1334
|
-
PAIR_INTERACTIVE: must({
|
|
1335
|
-
id: "PAIR_INTERACTIVE",
|
|
1336
|
-
statement: "Pairing MUST be interactive (device-code approval in the app's own session); a long-lived pasted secret MUST NOT be accepted as pairing.",
|
|
1337
|
-
enforcedBy: "server",
|
|
1338
|
-
verifiedBy: "conformance",
|
|
1339
|
-
source: "byollm_001 \xA7Endpoints.1"
|
|
1340
|
-
}),
|
|
1341
|
-
PAIR_CODE_EXPIRES: must({
|
|
1342
|
-
id: "PAIR_CODE_EXPIRES",
|
|
1343
|
-
statement: "An unapproved device code MUST expire and MUST NOT be redeemable after expiry.",
|
|
1344
|
-
enforcedBy: "server",
|
|
1345
|
-
verifiedBy: "conformance",
|
|
1346
|
-
source: "byollm_001 \xA7Endpoints.1"
|
|
1347
|
-
}),
|
|
1348
|
-
// ---- Typed job kinds --------------------------------------------------
|
|
1349
|
-
VERSION_HANDSHAKE_REQUIRED: must({
|
|
1350
|
-
id: "VERSION_HANDSHAKE_REQUIRED",
|
|
1351
|
-
statement: "Every protocol request MUST declare a protocol version, and a server MUST refuse an absent or unsupported one with a structured error naming what it supports \u2014 never a generic parse failure.",
|
|
1352
|
-
enforcedBy: "both",
|
|
1353
|
-
verifiedBy: "conformance",
|
|
1354
|
-
source: "byollm_009 \xA74"
|
|
1355
|
-
}),
|
|
1356
|
-
SITE_KEY_BY_STUB: must({
|
|
1357
|
-
id: "SITE_KEY_BY_STUB",
|
|
1358
|
-
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.",
|
|
1359
|
-
enforcedBy: "daemon",
|
|
1360
|
-
// Adversarial, and the reason is the finding that produced it: the
|
|
1361
|
-
// honest paths pass with every site check deleted, because `open`
|
|
1362
|
-
// refuses a signature from the wrong key anyway. What distinguishes an
|
|
1363
|
-
// enforced rule from a coincidence here is a hostile pairing of stub and
|
|
1364
|
-
// envelope, which no conformance client would ever send.
|
|
1365
|
-
verifiedBy: "adversarial",
|
|
1366
|
-
source: "byollm_009 \xA7A.3"
|
|
1367
|
-
}),
|
|
1368
|
-
SITES_LOCALLY_APPROVED: must({
|
|
1369
|
-
id: "SITES_LOCALLY_APPROVED",
|
|
1370
|
-
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.",
|
|
1371
|
-
enforcedBy: "daemon",
|
|
1372
|
-
// Two kinds, and the second is the one that matters — V1-1.
|
|
1253
|
+
var PairStartResponse = z9.object({
|
|
1254
|
+
/** Secret the daemon polls with. Never shown to the user. */
|
|
1255
|
+
deviceCode: z9.string().min(20),
|
|
1256
|
+
/** Short code the user reads and confirms in the browser. */
|
|
1257
|
+
userCode: z9.string().min(4).max(16),
|
|
1258
|
+
/** Where the user approves. Must be on the server's own origin. */
|
|
1259
|
+
verificationUrl: z9.url(),
|
|
1260
|
+
/** Epoch ms after which the code is dead ({@link MUSTS.PAIR_CODE_EXPIRES}). */
|
|
1261
|
+
expiresAt: z9.number().int().positive(),
|
|
1262
|
+
/** How often the daemon may poll. */
|
|
1263
|
+
pollIntervalMs: z9.number().int().min(500).max(6e4)
|
|
1264
|
+
}).strict();
|
|
1265
|
+
var PairPollRequest = z9.object({
|
|
1266
|
+
protocolVersion: z9.literal(PROTOCOL_VERSION),
|
|
1267
|
+
action: z9.literal("poll"),
|
|
1268
|
+
deviceCode: z9.string().min(20)
|
|
1269
|
+
}).strict();
|
|
1270
|
+
var PairPollResponse = z9.discriminatedUnion("status", [
|
|
1271
|
+
z9.object({ status: z9.literal("pending") }).strict(),
|
|
1272
|
+
z9.object({ status: z9.literal("denied") }).strict(),
|
|
1273
|
+
z9.object({ status: z9.literal("expired") }).strict(),
|
|
1274
|
+
z9.object({
|
|
1275
|
+
status: z9.literal("approved"),
|
|
1276
|
+
// `runnerToken` is gone — cloud_008 §2.4, finding 37.
|
|
1373
1277
|
//
|
|
1374
|
-
//
|
|
1375
|
-
//
|
|
1376
|
-
//
|
|
1377
|
-
//
|
|
1378
|
-
// produce one.
|
|
1379
|
-
//
|
|
1380
|
-
// `adversarial`: the property that survives is about a *sequence* —
|
|
1381
|
-
// remove the id, re-offer it under a different key — which no honest
|
|
1382
|
-
// upstream sends and which the fence above does not see. That was the
|
|
1383
|
-
// bypass: the pin was deleted with the id, so the comparison had nothing
|
|
1384
|
-
// to compare against and the substitution arrived as a stranger.
|
|
1385
|
-
// **Not `conformance`, and that is a live gap rather than a judgement.**
|
|
1386
|
-
// Amendment C's succession clause is a rule about two implementations
|
|
1387
|
-
// agreeing, which is what a conformance check is for — but rotating a
|
|
1388
|
-
// site's key is not something `ConformanceTarget` can express, and adding
|
|
1389
|
-
// an optional hook that most targets omit would produce a check reporting
|
|
1390
|
-
// success for a reason unrelated to the property it claims. That is this
|
|
1391
|
-
// project's most-repeated bug, and it is not worth reintroducing for a
|
|
1392
|
-
// stronger-sounding word in a table. The rotation path is verified by
|
|
1393
|
-
// `site-rotation.test.ts` (both directions, against the shipped runner)
|
|
1394
|
-
// and `relay/test/rotation.test.ts` (both planes, against the reference
|
|
1395
|
-
// relay); the missing piece is a second *independent* implementation to
|
|
1396
|
-
// check them against, and there is not one yet.
|
|
1397
|
-
verifiedBy: ["construction", "adversarial"],
|
|
1398
|
-
source: "byollm_009 \xA7B.2, Amendment C"
|
|
1399
|
-
}),
|
|
1400
|
-
KEYS_EXCHANGED_AT_CONSENT: must({
|
|
1401
|
-
id: "KEYS_EXCHANGED_AT_CONSENT",
|
|
1402
|
-
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.",
|
|
1403
|
-
enforcedBy: "both",
|
|
1404
|
-
verifiedBy: "conformance",
|
|
1405
|
-
source: "byollm_009 \xA75"
|
|
1406
|
-
}),
|
|
1407
|
-
REQUESTS_SIGNED_NOT_BEARER: must({
|
|
1408
|
-
id: "REQUESTS_SIGNED_NOT_BEARER",
|
|
1409
|
-
statement: "Every authenticated request MUST be signed by the calling device's pinned identity key, over the endpoint, the runner id, a timestamp and the exact request body. A server MUST NOT accept a bearer credential in place of a signature.",
|
|
1410
|
-
enforcedBy: "both",
|
|
1411
|
-
verifiedBy: "conformance",
|
|
1412
|
-
source: "byollm_009 \xA74.2"
|
|
1413
|
-
}),
|
|
1414
|
-
LEASE_SCOPED_BY_GRANT: must({
|
|
1415
|
-
id: "LEASE_SCOPED_BY_GRANT",
|
|
1416
|
-
statement: "A lease-scoped request MUST name the lease it acts on, and a server MUST apply it only to that lease. Naming the job and the runner is not sufficient: both survive a claim-release-reclaim cycle.",
|
|
1417
|
-
enforcedBy: "both",
|
|
1418
|
-
verifiedBy: "conformance",
|
|
1419
|
-
source: "byollm_009 \xA74.2"
|
|
1420
|
-
}),
|
|
1421
|
-
STUB_METADATA_EXHAUSTIVE: must({
|
|
1422
|
-
id: "STUB_METADATA_EXHAUSTIVE",
|
|
1423
|
-
statement: "A claim MUST answer with stubs carrying exactly the enumerated fields and no payload. An endpoint MUST NOT emit a stub carrying others, and an upstream MUST NOT require any.",
|
|
1424
|
-
enforcedBy: "both",
|
|
1425
|
-
verifiedBy: "conformance",
|
|
1426
|
-
source: "byollm_009 \xA76"
|
|
1427
|
-
}),
|
|
1428
|
-
ENVELOPE_SEALED_AND_SIGNED: must({
|
|
1429
|
-
id: "ENVELOPE_SEALED_AND_SIGNED",
|
|
1430
|
-
statement: "A stored payload MUST be sealed, and MUST be signed by the sender's identity key. An endpoint MUST refuse an envelope whose signature does not verify against the identity it pinned.",
|
|
1431
|
-
enforcedBy: "server",
|
|
1432
|
-
verifiedBy: "conformance",
|
|
1433
|
-
source: "byollm_009 \xA76"
|
|
1434
|
-
}),
|
|
1435
|
-
KIND_TYPED_ONLY: must({
|
|
1436
|
-
id: "KIND_TYPED_ONLY",
|
|
1437
|
-
statement: "Job kinds MUST resolve against handlers baked into the daemon. A daemon MUST refuse an unknown kind rather than guess.",
|
|
1438
|
-
enforcedBy: "daemon",
|
|
1439
|
-
verifiedBy: "conformance",
|
|
1440
|
-
source: "byollm_001 \xA7Jobs are typed data"
|
|
1441
|
-
}),
|
|
1442
|
-
KIND_NO_CODE: must({
|
|
1443
|
-
id: "KIND_NO_CODE",
|
|
1444
|
-
statement: "A server MUST NOT be able to convey code, a shell string, or a path to execute; payloads are data handed to a model only.",
|
|
1445
|
-
enforcedBy: "daemon",
|
|
1446
|
-
verifiedBy: "conformance",
|
|
1447
|
-
source: "byollm_001 \xA7Jobs are typed data; byollm_004 \xA71"
|
|
1448
|
-
}),
|
|
1449
|
-
// ---- Capability and claiming -----------------------------------------
|
|
1450
|
-
CLAIM_REQUIRES_CAPABILITY: must({
|
|
1451
|
-
id: "CLAIM_REQUIRES_CAPABILITY",
|
|
1452
|
-
statement: "A daemon MUST NOT be given a job whose kind is absent from its advertised capability matrix.",
|
|
1453
|
-
enforcedBy: "both",
|
|
1454
|
-
verifiedBy: "conformance",
|
|
1455
|
-
source: "byollm_001 \xA7MUSTs"
|
|
1456
|
-
}),
|
|
1457
|
-
CAPABILITY_IS_DETECTED: must({
|
|
1458
|
-
id: "CAPABILITY_IS_DETECTED",
|
|
1459
|
-
statement: "An advertised capability matrix MUST be the intersection of owner config and detected, healthy reality \u2014 never config alone.",
|
|
1460
|
-
enforcedBy: "daemon",
|
|
1461
|
-
verifiedBy: "conformance",
|
|
1462
|
-
source: "byollm_002 \xA7Routing"
|
|
1463
|
-
}),
|
|
1464
|
-
CLAIM_ATOMIC: must({
|
|
1465
|
-
id: "CLAIM_ATOMIC",
|
|
1466
|
-
statement: "Claiming MUST be atomic: a job MUST NOT be handed to two runners concurrently.",
|
|
1467
|
-
enforcedBy: "server",
|
|
1468
|
-
verifiedBy: "conformance",
|
|
1469
|
-
source: "byollm_001 \xA7Endpoints.2"
|
|
1470
|
-
}),
|
|
1471
|
-
// ---- Leases -----------------------------------------------------------
|
|
1472
|
-
LEASE_HONORED: must({
|
|
1473
|
-
id: "LEASE_HONORED",
|
|
1474
|
-
statement: "A daemon MUST stop work on a job whose lease it has failed to renew, and MUST NOT report a result for an expired lease it no longer holds.",
|
|
1475
|
-
enforcedBy: "daemon",
|
|
1476
|
-
verifiedBy: "conformance",
|
|
1477
|
-
source: "byollm_001 \xA7MUSTs"
|
|
1478
|
-
}),
|
|
1479
|
-
LEASE_RECLAIMABLE: must({
|
|
1480
|
-
id: "LEASE_RECLAIMABLE",
|
|
1481
|
-
statement: "A lease that expires un-renewed MUST make its job claimable again with no loss of the job.",
|
|
1482
|
-
enforcedBy: "server",
|
|
1483
|
-
verifiedBy: "conformance",
|
|
1484
|
-
source: "byollm_001 \xA7Endpoints.2"
|
|
1485
|
-
}),
|
|
1486
|
-
// ---- Audience and offer scope ----------------------------------------
|
|
1487
|
-
AUDIENCE_BOTH_SIDES: must({
|
|
1488
|
-
id: "AUDIENCE_BOTH_SIDES",
|
|
1489
|
-
statement: "A job MUST run on a daemon only if the daemon's offer scope admits the job's owner AND the job's audience admits the daemon's owner.",
|
|
1490
|
-
enforcedBy: "both",
|
|
1491
|
-
verifiedBy: "conformance",
|
|
1492
|
-
source: "byollm_001 \xA7The audience model"
|
|
1493
|
-
}),
|
|
1494
|
-
SUBSCRIPTION_SELF_LOCK: must({
|
|
1495
|
-
id: "SUBSCRIPTION_SELF_LOCK",
|
|
1496
|
-
statement: "A subscription-class backend's offer scope MUST be 'private' and MUST NOT be widened by configuration.",
|
|
1497
|
-
enforcedBy: "daemon",
|
|
1498
|
-
verifiedBy: "conformance",
|
|
1499
|
-
source: "byollm_001 \xA7The audience model"
|
|
1500
|
-
}),
|
|
1501
|
-
METERED_DEFAULTS_SELF: must({
|
|
1502
|
-
id: "METERED_DEFAULTS_SELF",
|
|
1503
|
-
statement: "A metered backend's effective offer scope MUST be 'private' unless the owner has explicitly acknowledged spending money on others' work.",
|
|
1504
|
-
enforcedBy: "daemon",
|
|
1505
|
-
verifiedBy: "conformance",
|
|
1506
|
-
source: "byollm_007 \xA74"
|
|
1507
|
-
}),
|
|
1508
|
-
METERED_REQUIRES_CEILING: must({
|
|
1509
|
-
id: "METERED_REQUIRES_CEILING",
|
|
1510
|
-
statement: "A widened metered backend MUST carry a spend ceiling, and the daemon MUST refuse community work once it is reached.",
|
|
1511
|
-
enforcedBy: "daemon",
|
|
1512
|
-
verifiedBy: "conformance",
|
|
1513
|
-
source: "byollm_007 \xA74"
|
|
1514
|
-
}),
|
|
1515
|
-
COST_NOT_CONFIGURABLE: must({
|
|
1516
|
-
id: "COST_NOT_CONFIGURABLE",
|
|
1517
|
-
statement: "A built-in provider's cost class MUST NOT be overridable by configuration.",
|
|
1518
|
-
enforcedBy: "daemon",
|
|
1519
|
-
verifiedBy: "conformance",
|
|
1520
|
-
source: "byollm_007 \xA72"
|
|
1521
|
-
}),
|
|
1522
|
-
REMOTE_IS_NEVER_FREE: must({
|
|
1523
|
-
id: "REMOTE_IS_NEVER_FREE",
|
|
1524
|
-
statement: "A generic HTTP backend whose base URL is not loopback or private MUST be treated as metered.",
|
|
1525
|
-
enforcedBy: "daemon",
|
|
1526
|
-
verifiedBy: "conformance",
|
|
1527
|
-
source: "byollm_007 \xA72"
|
|
1528
|
-
}),
|
|
1529
|
-
NAMED_LOCAL_ALLOWLIST: must({
|
|
1530
|
-
id: "NAMED_LOCAL_ALLOWLIST",
|
|
1531
|
-
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.",
|
|
1532
|
-
enforcedBy: "daemon",
|
|
1533
|
-
verifiedBy: "conformance",
|
|
1534
|
-
source: "byollm_001 Rev 1 \xA7B"
|
|
1535
|
-
}),
|
|
1536
|
-
REFUSAL_NOT_REOFFERED: must({
|
|
1537
|
-
id: "REFUSAL_NOT_REOFFERED",
|
|
1538
|
-
statement: "A server MUST NOT re-offer a job to a runner that released it with reason 'refused'.",
|
|
1539
|
-
enforcedBy: "server",
|
|
1540
|
-
verifiedBy: "conformance",
|
|
1541
|
-
source: "byollm_001 Rev 1 \xA7B (loop resolved in build review)"
|
|
1542
|
-
}),
|
|
1543
|
-
// ---- Revocation and cancel -------------------------------------------
|
|
1544
|
-
REVOCATION_HONORED: must({
|
|
1545
|
-
id: "REVOCATION_HONORED",
|
|
1546
|
-
statement: "A revoked daemon MUST stop claiming and MUST abandon in-flight work by the next heartbeat at the latest.",
|
|
1547
|
-
enforcedBy: "daemon",
|
|
1548
|
-
verifiedBy: "conformance",
|
|
1549
|
-
source: "byollm_001 \xA7MUSTs"
|
|
1550
|
-
}),
|
|
1551
|
-
CANCEL_HONORED: must({
|
|
1552
|
-
id: "CANCEL_HONORED",
|
|
1553
|
-
statement: "A job id in a heartbeat response's cancel list MUST abort that job's in-flight backend call and be reported as 'canceled'.",
|
|
1554
|
-
enforcedBy: "daemon",
|
|
1555
|
-
verifiedBy: "conformance",
|
|
1556
|
-
source: "byollm_001 Rev 1 \xA7C"
|
|
1557
|
-
}),
|
|
1558
|
-
// ---- Lifecycle, dependencies, delivery -------------------------------
|
|
1559
|
-
DEPENDS_ON_GATING: must({
|
|
1560
|
-
id: "DEPENDS_ON_GATING",
|
|
1561
|
-
statement: "A job MUST NOT be claimable until every job in its dependsOn set has reached the 'ok' state.",
|
|
1562
|
-
enforcedBy: "server",
|
|
1563
|
-
verifiedBy: "conformance",
|
|
1564
|
-
source: "byollm_001 Rev 1 \xA7E"
|
|
1565
|
-
}),
|
|
1566
|
-
TTL_EXPIRY: must({
|
|
1567
|
-
id: "TTL_EXPIRY",
|
|
1568
|
-
statement: "An unclaimed job MUST become 'expired' once its TTL elapses, and the TTL clock MUST start when the job becomes claimable, not at enqueue.",
|
|
1569
|
-
enforcedBy: "server",
|
|
1570
|
-
verifiedBy: "conformance",
|
|
1571
|
-
source: "byollm_001 Rev 1 \xA7D (TTL clock resolved in build review)"
|
|
1572
|
-
}),
|
|
1573
|
-
NO_RUNNER_SIGNAL: must({
|
|
1574
|
-
id: "NO_RUNNER_SIGNAL",
|
|
1575
|
-
statement: "A server MUST surface noRunnerAvailable when no runner with matching capability has heartbeated within the liveness window, and MUST NOT raise it for a job still blocked on dependencies.",
|
|
1576
|
-
enforcedBy: "server",
|
|
1577
|
-
verifiedBy: "conformance",
|
|
1578
|
-
source: "byollm_001 Rev 1 \xA7D"
|
|
1579
|
-
}),
|
|
1580
|
-
RESULT_IDEMPOTENT: must({
|
|
1581
|
-
id: "RESULT_IDEMPOTENT",
|
|
1582
|
-
statement: "Result submission MUST be idempotent by job id; the first terminal outcome wins and later submissions MUST NOT change it.",
|
|
1583
|
-
enforcedBy: "server",
|
|
1584
|
-
verifiedBy: "conformance",
|
|
1585
|
-
source: "byollm_001 \xA7Endpoints.4"
|
|
1586
|
-
}),
|
|
1587
|
-
PROVENANCE_NAMES_DEVICE: must({
|
|
1588
|
-
id: "PROVENANCE_NAMES_DEVICE",
|
|
1589
|
-
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.",
|
|
1590
|
-
enforcedBy: "server",
|
|
1591
|
-
verifiedBy: "conformance",
|
|
1592
|
-
source: "byollm_009 \xA711"
|
|
1593
|
-
}),
|
|
1594
|
-
// ---- The trust surface -------------------------------------------------
|
|
1595
|
-
INGRESS_LOGGED_BEFORE_EXECUTION: must({
|
|
1596
|
-
id: "INGRESS_LOGGED_BEFORE_EXECUTION",
|
|
1597
|
-
statement: "Every executed prompt MUST be appended to the local ingress log before execution begins.",
|
|
1598
|
-
enforcedBy: "daemon",
|
|
1599
|
-
verifiedBy: "conformance",
|
|
1600
|
-
source: "byollm_001 \xA7MUSTs"
|
|
1601
|
-
}),
|
|
1602
|
-
// ---- Execution isolation (byollm_004) ---------------------------------
|
|
1603
|
-
NO_SHELL_INTERPOLATION: must({
|
|
1604
|
-
id: "NO_SHELL_INTERPOLATION",
|
|
1605
|
-
statement: "Process-class backends MUST be invoked with a fixed argv array and the payload delivered on stdin; payload text MUST NOT reach a command line.",
|
|
1606
|
-
enforcedBy: "daemon",
|
|
1607
|
-
verifiedBy: "adversarial",
|
|
1608
|
-
source: "byollm_004 \xA72"
|
|
1609
|
-
}),
|
|
1610
|
-
/**
|
|
1611
|
-
* Amended for byollm_016 Phase B, and the amendment is deliberately narrow.
|
|
1612
|
-
*
|
|
1613
|
-
* A site may now name a **service** on the stub. The temptation is to read
|
|
1614
|
-
* that as a crack in this law, so the statement below says exactly where the
|
|
1615
|
-
* line is: a name selects from a menu the owner published, and resolves to a
|
|
1616
|
-
* model, backend, base URL and flags **only** through that owner's own
|
|
1617
|
-
* config. The site supplies a key; the owner supplies every value it maps
|
|
1618
|
-
* to. A name the owner does not advertise is refused rather than
|
|
1619
|
-
* substituted, because substitution is how "you may pick from my list" turns
|
|
1620
|
-
* into "you may ask for anything and get something".
|
|
1621
|
-
*
|
|
1622
|
-
* Two properties keep it from drifting into "sites demand models":
|
|
1623
|
-
*
|
|
1624
|
-
* 1. **Nothing the site sends is ever a value.** No model string, no URL,
|
|
1625
|
-
* no flag crosses the wire — only a key that means nothing off this
|
|
1626
|
-
* owner's machine.
|
|
1627
|
-
* 2. **It is a stub field, never a payload field.** The prompt cannot
|
|
1628
|
-
* reach it. That is unchanged and is the sentence the second clause
|
|
1629
|
-
* below still enforces verbatim.
|
|
1630
|
-
*/
|
|
1631
|
-
NO_PAYLOAD_ROUTING: must({
|
|
1632
|
-
id: "NO_PAYLOAD_ROUTING",
|
|
1633
|
-
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.",
|
|
1634
|
-
enforcedBy: "daemon",
|
|
1635
|
-
verifiedBy: "adversarial",
|
|
1636
|
-
source: "byollm_004 \xA72, amended byollm_016 \xA7Phase B"
|
|
1637
|
-
}),
|
|
1638
|
-
STRIPPED_CHILD_ENV: must({
|
|
1639
|
-
id: "STRIPPED_CHILD_ENV",
|
|
1640
|
-
statement: "Process-class children MUST spawn with an allowlisted environment, a scratch cwd, no inherited descriptors beyond std streams, and hard timeout and output-size caps.",
|
|
1641
|
-
enforcedBy: "daemon",
|
|
1642
|
-
verifiedBy: "adversarial",
|
|
1643
|
-
source: "byollm_004 \xA72"
|
|
1644
|
-
}),
|
|
1645
|
-
HTTP_BASE_URL_SAFE: must({
|
|
1646
|
-
id: "HTTP_BASE_URL_SAFE",
|
|
1647
|
-
statement: "HTTP-class backends MUST send requests only to the owner-configured base URL and MUST refuse base URLs resolving to cloud-metadata or link-local addresses.",
|
|
1648
|
-
enforcedBy: "daemon",
|
|
1649
|
-
verifiedBy: "adversarial",
|
|
1650
|
-
source: "byollm_004 Rev 1 \xA7Backend taxonomy"
|
|
1651
|
-
}),
|
|
1652
|
-
OUTPUT_INERT: must({
|
|
1653
|
-
id: "OUTPUT_INERT",
|
|
1654
|
-
statement: "Returned text MUST be treated as inert bytes: never evaluated, never written to a payload-named path, never interpolated into a shell or into terminal control sequences when logged.",
|
|
1655
|
-
enforcedBy: "daemon",
|
|
1656
|
-
verifiedBy: "adversarial",
|
|
1657
|
-
source: "byollm_004 \xA72"
|
|
1658
|
-
}),
|
|
1659
|
-
COMMUNITY_BUDGETS: must({
|
|
1660
|
-
id: "COMMUNITY_BUDGETS",
|
|
1661
|
-
statement: "Jobs whose owner is not the daemon's owner MUST be subject to the owner's rate limits, daily cap, and resource budget.",
|
|
1662
|
-
enforcedBy: "daemon",
|
|
1663
|
-
verifiedBy: "adversarial",
|
|
1664
|
-
source: "byollm_004 \xA74"
|
|
1665
|
-
}),
|
|
1666
|
-
REVOCATION_IMMEDIATE: must({
|
|
1667
|
-
id: "REVOCATION_IMMEDIATE",
|
|
1668
|
-
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.",
|
|
1669
|
-
// Both, and stated as one sentence with two obligations rather than
|
|
1670
|
-
// folded into REVOCATION_HONORED. That one binds the *daemon*: a revoked
|
|
1671
|
-
// daemon stops claiming and abandons in-flight work. This binds the
|
|
1672
|
-
// *upstream*. byollm_009 §5 is explicit that the pair is the point — "a
|
|
1673
|
-
// revocation enforced at one end survives a compromise of that end" — and
|
|
1674
|
-
// one entry covering both would make a compromised daemon look compliant.
|
|
1675
|
-
enforcedBy: "both",
|
|
1676
|
-
verifiedBy: "conformance",
|
|
1677
|
-
source: "byollm_009 \xA711"
|
|
1678
|
-
}),
|
|
1679
|
-
CONSENT_BEFORE_ROUTE: must({
|
|
1680
|
-
id: "CONSENT_BEFORE_ROUTE",
|
|
1681
|
-
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.",
|
|
1682
|
-
enforcedBy: "server",
|
|
1683
|
-
verifiedBy: "conformance",
|
|
1684
|
-
source: "byollm_009 \xA711"
|
|
1685
|
-
}),
|
|
1686
|
-
ROSTER_NOT_DISCLOSED: must({
|
|
1687
|
-
id: "ROSTER_NOT_DISCLOSED",
|
|
1688
|
-
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.",
|
|
1689
|
-
// Checkable since cloud_008 §0.2 took `audienceAllow` off the stub: the
|
|
1690
|
-
// property now holds by *absence*, and absence is exactly what a strict
|
|
1691
|
-
// schema and a serialised stub can be asked about. Before that it was a
|
|
1692
|
-
// sentence — and one this project cited in code comments, tests and two
|
|
1693
|
-
// specs as though it were enforced data, which is why it is worth
|
|
1694
|
-
// stating precisely rather than generously.
|
|
1695
|
-
enforcedBy: "both",
|
|
1696
|
-
verifiedBy: "conformance",
|
|
1697
|
-
source: "byollm_009 \xA711"
|
|
1698
|
-
}),
|
|
1699
|
-
EFFECTIVE_OFFER_ONLY: must({
|
|
1700
|
-
id: "EFFECTIVE_OFFER_ONLY",
|
|
1701
|
-
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.",
|
|
1702
|
-
enforcedBy: "both",
|
|
1703
|
-
verifiedBy: "conformance",
|
|
1704
|
-
source: "byollm_009 \xA711"
|
|
1705
|
-
}),
|
|
1706
|
-
FALLBACK_LABELED: must({
|
|
1707
|
-
id: "FALLBACK_LABELED",
|
|
1708
|
-
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.",
|
|
1709
|
-
// `construction` today, and deliberately not `conformance`. Nothing on
|
|
1710
|
-
// the wire yet distinguishes a fallback from any other community job —
|
|
1711
|
-
// the ledger that would give it a surface is unbuilt — so a check would
|
|
1712
|
-
// have to assert something it cannot observe. Promoted the day that
|
|
1713
|
-
// surface exists. Marking it `conformance` now would put "verified"
|
|
1714
|
-
// beside a property no third party can see, which is the one thing the
|
|
1715
|
-
// kinds exist to prevent.
|
|
1716
|
-
enforcedBy: "both",
|
|
1717
|
-
verifiedBy: "construction",
|
|
1718
|
-
source: "byollm_009 \xA711"
|
|
1719
|
-
}),
|
|
1720
|
-
RELAY_BLIND: must({
|
|
1721
|
-
id: "RELAY_BLIND",
|
|
1722
|
-
statement: "A relay MUST NOT hold any key capable of decrypting a payload, a result, or a delta frame.",
|
|
1723
|
-
// Operator: a third party can read the relay's types and see there is
|
|
1724
|
-
// nowhere to put such a key, but the kit certifies a *server* and cannot
|
|
1725
|
-
// reach inside somebody's deployment to prove what it holds.
|
|
1726
|
-
enforcedBy: "server",
|
|
1727
|
-
verifiedBy: "operator",
|
|
1728
|
-
source: "byollm_009 \xA711"
|
|
1729
|
-
}),
|
|
1730
|
-
SHARED_COMPUTE_DISCLOSED: must({
|
|
1731
|
-
id: "SHARED_COMPUTE_DISCLOSED",
|
|
1732
|
-
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.",
|
|
1733
|
-
// Operator, and cloud_008 §0.3 is why the classification now comes with a
|
|
1734
|
-
// standing answer rather than a standing question. The screen is not
|
|
1735
|
-
// wire-observable, but the *string the server composes* is, and it is
|
|
1736
|
-
// now unit-tested with the two false sentences forbidden by name. The
|
|
1737
|
-
// kind stays `operator` because a third-party site can still render
|
|
1738
|
-
// whatever it likes; what changed is that the part inside our own
|
|
1739
|
-
// boundary stopped depending on somebody remembering to audit it.
|
|
1740
|
-
enforcedBy: "server",
|
|
1741
|
-
verifiedBy: "operator",
|
|
1742
|
-
source: "byollm_009 \xA711"
|
|
1743
|
-
})
|
|
1744
|
-
});
|
|
1745
|
-
var RETIRED_MUSTS = Object.freeze({
|
|
1746
|
-
RESULT_PROVENANCE: {
|
|
1747
|
-
supersededBy: "PROVENANCE_NAMES_DEVICE",
|
|
1748
|
-
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."
|
|
1749
|
-
}
|
|
1750
|
-
});
|
|
1751
|
-
var MUST_IDS = Object.freeze(Object.keys(MUSTS));
|
|
1752
|
-
function mustsVerifiedBy(kind) {
|
|
1753
|
-
return MUST_IDS.filter((id) => kindsOf(MUSTS[id]).includes(kind));
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
// src/wire.ts
|
|
1757
|
-
import { z as z11 } from "zod";
|
|
1758
|
-
var PROTOCOL_VERSION = "1";
|
|
1759
|
-
var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze([
|
|
1760
|
-
PROTOCOL_VERSION
|
|
1761
|
-
]);
|
|
1762
|
-
var MIN_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0] ?? PROTOCOL_VERSION;
|
|
1763
|
-
function declaredVersion(input) {
|
|
1764
|
-
const { body, query } = input;
|
|
1765
|
-
if (typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion")) {
|
|
1766
|
-
return body.protocolVersion;
|
|
1767
|
-
}
|
|
1768
|
-
return query?.get("protocolVersion") ?? void 0;
|
|
1769
|
-
}
|
|
1770
|
-
function checkProtocolVersion(body) {
|
|
1771
|
-
const declared = typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion") ? body.protocolVersion : void 0;
|
|
1772
|
-
if (typeof declared !== "string" || declared.length === 0) {
|
|
1773
|
-
return {
|
|
1774
|
-
error: "unsupported-protocol-version",
|
|
1775
|
-
message: `this request declared no protocol version. Upgrade the daemon: \`${UPGRADE_COMMAND}\`.`,
|
|
1776
|
-
supported: SUPPORTED_PROTOCOL_VERSIONS,
|
|
1777
|
-
minimum: MIN_PROTOCOL_VERSION
|
|
1778
|
-
};
|
|
1779
|
-
}
|
|
1780
|
-
if (!SUPPORTED_PROTOCOL_VERSIONS.includes(declared)) {
|
|
1781
|
-
return {
|
|
1782
|
-
error: "unsupported-protocol-version",
|
|
1783
|
-
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."),
|
|
1784
|
-
supported: SUPPORTED_PROTOCOL_VERSIONS,
|
|
1785
|
-
minimum: MIN_PROTOCOL_VERSION
|
|
1786
|
-
};
|
|
1787
|
-
}
|
|
1788
|
-
return null;
|
|
1789
|
-
}
|
|
1790
|
-
var UPGRADE_COMMAND = "npm i -g byollm@latest";
|
|
1791
|
-
var PROTOCOL_PREFIX = "/byollm";
|
|
1792
|
-
var ENDPOINTS = Object.freeze([
|
|
1793
|
-
"pair",
|
|
1794
|
-
"claim",
|
|
1795
|
-
"fetch",
|
|
1796
|
-
"heartbeat",
|
|
1797
|
-
"result",
|
|
1798
|
-
"release"
|
|
1799
|
-
]);
|
|
1800
|
-
var Capability = z11.object({
|
|
1801
|
-
kind: JobKind,
|
|
1802
|
-
/**
|
|
1803
|
-
* The owner's name for the service answering this kind — byollm_016.
|
|
1804
|
-
*
|
|
1805
|
-
* A device advertises *which* of its services serves a kind, not merely
|
|
1806
|
-
* that something does. **A site never sees this**, and never did after
|
|
1807
|
-
* Amendment L: it is what a control plane resolves a person's mapping
|
|
1808
|
-
* against, so that the service a grant names is one this device actually
|
|
1809
|
-
* offers rather than one somebody invented.
|
|
1810
|
-
*
|
|
1811
|
-
* `isDefault` used to sit beside it, saying which row an unselected job
|
|
1812
|
-
* took. Nothing selects any more — a job names a purpose and a person's
|
|
1813
|
-
* mapping names a service — so there is no unselected job for a default
|
|
1814
|
-
* to catch, and the field went with the machinery it served.
|
|
1815
|
-
*/
|
|
1816
|
-
service: z11.string().min(1),
|
|
1817
|
-
backendId: BackendIdSchema,
|
|
1818
|
-
backendClass: BackendClass,
|
|
1819
|
-
model: z11.string().min(1),
|
|
1820
|
-
/**
|
|
1821
|
-
* Models this device's CLI knows about — byollm_017 ruling 3.
|
|
1822
|
-
*
|
|
1823
|
-
* **Suggestions, not a vocabulary.** Free text is always allowed: the
|
|
1824
|
-
* promise is that a model released this morning works this morning, and a
|
|
1825
|
-
* frozen list anywhere a person picks from breaks that on the first day
|
|
1826
|
-
* it matters. What makes free text safe is ruling 2 — a model is probed
|
|
1827
|
-
* before it is stored, so "found is not works" is answered by the device
|
|
1828
|
-
* rather than by a list.
|
|
1829
|
-
*
|
|
1830
|
-
* Announced with the capability rather than kept in the dashboard,
|
|
1831
|
-
* because the answer is "what does THIS device's CLI know" and only the
|
|
1832
|
-
* device can say. A list held cloud-side would be one more thing to
|
|
1833
|
-
* update on release day, and wrong for anybody who had not upgraded.
|
|
1834
|
-
*
|
|
1835
|
-
* Optional, and empty is legal. A backend with nothing to suggest — a
|
|
1836
|
-
* local server serving one model — is not a backend in an error state,
|
|
1837
|
-
* and a reader must not render an absent list as "no models available".
|
|
1838
|
-
*/
|
|
1839
|
-
knownModels: z11.array(z11.string().min(1)).optional(),
|
|
1840
|
-
offerScope: OfferScope
|
|
1841
|
-
}).strict();
|
|
1842
|
-
var CapabilityMatrix = z11.array(Capability);
|
|
1843
|
-
var WithheldKind = z11.object({
|
|
1844
|
-
kind: JobKind,
|
|
1845
|
-
claimants: z11.array(
|
|
1846
|
-
z11.object({ service: z11.string().min(1), offer: OfferScope }).strict()
|
|
1847
|
-
).min(2)
|
|
1848
|
-
}).strict();
|
|
1849
|
-
var GrantRef = z11.object({ jobId: z11.string().min(1), leaseId: z11.string().min(1) }).strict();
|
|
1850
|
-
var PairStartRequest = z11.object({
|
|
1851
|
-
protocolVersion: z11.literal(PROTOCOL_VERSION),
|
|
1852
|
-
action: z11.literal("start"),
|
|
1853
|
-
daemon: z11.object({
|
|
1854
|
-
version: z11.string().min(1),
|
|
1855
|
-
/** Shown in the app's runner list so a user can tell their machines apart. */
|
|
1856
|
-
label: z11.string().min(1).max(120),
|
|
1857
|
-
platform: z11.enum(["darwin", "linux", "win32"])
|
|
1858
|
-
}).strict(),
|
|
1859
|
-
/**
|
|
1860
|
-
* This machine's public keys (byollm_009 §5).
|
|
1861
|
-
*
|
|
1862
|
-
* Pairing is where the two parties learn each other's identities, because
|
|
1863
|
-
* it is the one moment a human is already deciding to trust: the approval
|
|
1864
|
-
* click. A key exchanged anywhere else would be a key nobody chose.
|
|
1865
|
-
*/
|
|
1866
|
-
device: PublicIdentity,
|
|
1867
|
-
capabilities: CapabilityMatrix
|
|
1868
|
-
}).strict();
|
|
1869
|
-
var PairStartResponse = z11.object({
|
|
1870
|
-
/** Secret the daemon polls with. Never shown to the user. */
|
|
1871
|
-
deviceCode: z11.string().min(20),
|
|
1872
|
-
/** Short code the user reads and confirms in the browser. */
|
|
1873
|
-
userCode: z11.string().min(4).max(16),
|
|
1874
|
-
/** Where the user approves. Must be on the server's own origin. */
|
|
1875
|
-
verificationUrl: z11.url(),
|
|
1876
|
-
/** Epoch ms after which the code is dead ({@link MUSTS.PAIR_CODE_EXPIRES}). */
|
|
1877
|
-
expiresAt: z11.number().int().positive(),
|
|
1878
|
-
/** How often the daemon may poll. */
|
|
1879
|
-
pollIntervalMs: z11.number().int().min(500).max(6e4)
|
|
1880
|
-
}).strict();
|
|
1881
|
-
var PairPollRequest = z11.object({
|
|
1882
|
-
protocolVersion: z11.literal(PROTOCOL_VERSION),
|
|
1883
|
-
action: z11.literal("poll"),
|
|
1884
|
-
deviceCode: z11.string().min(20)
|
|
1885
|
-
}).strict();
|
|
1886
|
-
var PairPollResponse = z11.discriminatedUnion("status", [
|
|
1887
|
-
z11.object({ status: z11.literal("pending") }).strict(),
|
|
1888
|
-
z11.object({ status: z11.literal("denied") }).strict(),
|
|
1889
|
-
z11.object({ status: z11.literal("expired") }).strict(),
|
|
1890
|
-
z11.object({
|
|
1891
|
-
status: z11.literal("approved"),
|
|
1892
|
-
// `runnerToken` is gone — cloud_008 §2.4, finding 37.
|
|
1893
|
-
//
|
|
1894
|
-
// It was minted here, hashed into `RunnerRecord.tokenHash`, written to
|
|
1895
|
-
// the daemon's pairings file, and then **never sent, never looked up
|
|
1896
|
-
// and never compared**. `getRunnerByTokenHash` existed on both stores
|
|
1897
|
-
// and was called by nothing but a test asserting it returns null.
|
|
1278
|
+
// It was minted here, hashed into `RunnerRecord.tokenHash`, written to
|
|
1279
|
+
// the daemon's pairings file, and then **never sent, never looked up
|
|
1280
|
+
// and never compared**. `getRunnerByTokenHash` existed on both stores
|
|
1281
|
+
// and was called by nothing but a test asserting it returns null.
|
|
1898
1282
|
//
|
|
1899
1283
|
// Not merely dead wire, which is what `audienceAllow` and
|
|
1900
1284
|
// `HeartbeatResponse.leases` were. This was a *secret*: minted,
|
|
@@ -1905,11 +1289,11 @@ var PairPollResponse = z11.discriminatedUnion("status", [
|
|
|
1905
1289
|
// `REQUESTS_SIGNED_NOT_BEARER` was already the rule and was already
|
|
1906
1290
|
// enforced — every authenticated call is signed by the device's pinned
|
|
1907
1291
|
// identity key. This removes the thing the MUST is named after.
|
|
1908
|
-
runnerId:
|
|
1292
|
+
runnerId: z9.string().min(1),
|
|
1909
1293
|
/** The app's id for the approving user — this daemon's owner forever. */
|
|
1910
|
-
owner:
|
|
1294
|
+
owner: z9.string().min(1),
|
|
1911
1295
|
/** Display name for the trust UI, if the app offers one. */
|
|
1912
|
-
ownerLabel:
|
|
1296
|
+
ownerLabel: z9.string().optional(),
|
|
1913
1297
|
/**
|
|
1914
1298
|
* The sites this pairing covers, for the daemon to pin (byollm_009 §5),
|
|
1915
1299
|
* keyed by each site's identity key id — cloud_009 §5.
|
|
@@ -1928,7 +1312,7 @@ var PairPollResponse = z11.discriminatedUnion("status", [
|
|
|
1928
1312
|
* runner's lookup is a map read rather than a join across two
|
|
1929
1313
|
* namespaces.
|
|
1930
1314
|
*/
|
|
1931
|
-
sites:
|
|
1315
|
+
sites: z9.record(z9.string().min(1), PublicIdentity),
|
|
1932
1316
|
/**
|
|
1933
1317
|
* The control plane's grant-signing key, pinned here — Amendment J.
|
|
1934
1318
|
*
|
|
@@ -1949,34 +1333,34 @@ var PairPollResponse = z11.discriminatedUnion("status", [
|
|
|
1949
1333
|
* Rotation is Amendment C's, with no path where a grant teaches a
|
|
1950
1334
|
* daemon a new key.
|
|
1951
1335
|
*/
|
|
1952
|
-
controlPlanePublic:
|
|
1336
|
+
controlPlanePublic: z9.string().min(1).optional()
|
|
1953
1337
|
}).strict()
|
|
1954
1338
|
]);
|
|
1955
|
-
var PairRequest =
|
|
1339
|
+
var PairRequest = z9.discriminatedUnion("action", [
|
|
1956
1340
|
PairStartRequest,
|
|
1957
1341
|
PairPollRequest
|
|
1958
1342
|
]);
|
|
1959
|
-
var ClaimRequest =
|
|
1960
|
-
protocolVersion:
|
|
1961
|
-
runnerId:
|
|
1343
|
+
var ClaimRequest = z9.object({
|
|
1344
|
+
protocolVersion: z9.literal(PROTOCOL_VERSION),
|
|
1345
|
+
runnerId: z9.string().min(1),
|
|
1962
1346
|
/** Re-sent on every claim so a server never matches against a stale matrix. */
|
|
1963
1347
|
capabilities: CapabilityMatrix,
|
|
1964
1348
|
/** Upper bound on jobs to return; the server may return fewer. */
|
|
1965
|
-
max:
|
|
1349
|
+
max: z9.number().int().min(1).max(64)
|
|
1966
1350
|
}).strict();
|
|
1967
|
-
var ClaimResponse =
|
|
1351
|
+
var ClaimResponse = z9.object({
|
|
1968
1352
|
/**
|
|
1969
1353
|
* Stubs, not jobs. The payload arrives from `fetch`, sealed to whichever
|
|
1970
1354
|
* device claimed — see {@link JobStub} for the exhaustive metadata list.
|
|
1971
1355
|
*/
|
|
1972
|
-
jobs:
|
|
1356
|
+
jobs: z9.array(ClaimedStub),
|
|
1973
1357
|
/** Lease duration granted, so the daemon knows its renewal deadline. */
|
|
1974
|
-
leaseMs:
|
|
1358
|
+
leaseMs: z9.number().int().positive()
|
|
1975
1359
|
}).strict();
|
|
1976
|
-
var HeartbeatRequest =
|
|
1977
|
-
protocolVersion:
|
|
1978
|
-
runnerId:
|
|
1979
|
-
daemonVersion:
|
|
1360
|
+
var HeartbeatRequest = z9.object({
|
|
1361
|
+
protocolVersion: z9.literal(PROTOCOL_VERSION),
|
|
1362
|
+
runnerId: z9.string().min(1),
|
|
1363
|
+
daemonVersion: z9.string().min(1),
|
|
1980
1364
|
capabilities: CapabilityMatrix,
|
|
1981
1365
|
/**
|
|
1982
1366
|
* Kinds this device is withholding, and why it can be said.
|
|
@@ -1985,18 +1369,18 @@ var HeartbeatRequest = z11.object({
|
|
|
1985
1369
|
* older daemon against a newer hub is simply a device with no withheld
|
|
1986
1370
|
* kinds rather than a parse failure.
|
|
1987
1371
|
*/
|
|
1988
|
-
withheld:
|
|
1372
|
+
withheld: z9.array(WithheldKind).default([]),
|
|
1989
1373
|
/**
|
|
1990
1374
|
* Leases this daemon believes it holds; the server renews exactly these.
|
|
1991
1375
|
*
|
|
1992
1376
|
* Lease ids rather than job ids, so a replayed heartbeat cannot renew a
|
|
1993
1377
|
* grant the runner no longer holds — see {@link Lease.id}.
|
|
1994
1378
|
*/
|
|
1995
|
-
activeLeases:
|
|
1379
|
+
activeLeases: z9.array(GrantRef),
|
|
1996
1380
|
/** True while the owner has the daemon paused; the server stops offering work. */
|
|
1997
|
-
paused:
|
|
1381
|
+
paused: z9.boolean()
|
|
1998
1382
|
}).strict();
|
|
1999
|
-
var HeartbeatResponse =
|
|
1383
|
+
var HeartbeatResponse = z9.object({
|
|
2000
1384
|
/**
|
|
2001
1385
|
* The sites this daemon may serve, right now — cloud_008 finding 59.
|
|
2002
1386
|
*
|
|
@@ -2014,7 +1398,7 @@ var HeartbeatResponse = z11.object({
|
|
|
2014
1398
|
* rather than being told a second time — two fields for one fact is how
|
|
2015
1399
|
* they drift.
|
|
2016
1400
|
*/
|
|
2017
|
-
sites:
|
|
1401
|
+
sites: z9.record(z9.string().min(1), PublicIdentity),
|
|
2018
1402
|
/**
|
|
2019
1403
|
* How a site's current key traces back to one this daemon already holds —
|
|
2020
1404
|
* byollm_009 Amendment C.
|
|
@@ -2032,11 +1416,11 @@ var HeartbeatResponse = z11.object({
|
|
|
2032
1416
|
* history is public by construction, because a daemon that cannot read it
|
|
2033
1417
|
* cannot verify it.
|
|
2034
1418
|
*/
|
|
2035
|
-
successions:
|
|
2036
|
-
|
|
2037
|
-
|
|
1419
|
+
successions: z9.record(
|
|
1420
|
+
z9.string().min(1),
|
|
1421
|
+
z9.object({
|
|
2038
1422
|
/** Oldest last, as the projection carries it. */
|
|
2039
|
-
succeeds:
|
|
1423
|
+
succeeds: z9.array(Succession).max(MAX_SUCCESSION_CHAIN),
|
|
2040
1424
|
/**
|
|
2041
1425
|
* Until when the superseded key may still sign work — epoch ms.
|
|
2042
1426
|
*
|
|
@@ -2045,7 +1429,7 @@ var HeartbeatResponse = z11.object({
|
|
|
2045
1429
|
* window indefinitely would be a two-key site forever, decided by
|
|
2046
1430
|
* the party this design does not trust.
|
|
2047
1431
|
*/
|
|
2048
|
-
retiringUntil:
|
|
1432
|
+
retiringUntil: z9.number().int().positive().optional()
|
|
2049
1433
|
}).strict()
|
|
2050
1434
|
).optional(),
|
|
2051
1435
|
/**
|
|
@@ -2058,8 +1442,8 @@ var HeartbeatResponse = z11.object({
|
|
|
2058
1442
|
* is the unique grant and the daemon already keys its work by it; this is
|
|
2059
1443
|
* the same shape `activeLeases` sends in the other direction.
|
|
2060
1444
|
*/
|
|
2061
|
-
cancel:
|
|
2062
|
-
|
|
1445
|
+
cancel: z9.array(
|
|
1446
|
+
z9.object({ jobId: z9.string().min(1), leaseId: z9.string().min(1) }).strict()
|
|
2063
1447
|
),
|
|
2064
1448
|
// `leases` is deliberately absent — cloud_008 §1.4b, finding 16.
|
|
2065
1449
|
//
|
|
@@ -2089,11 +1473,11 @@ var HeartbeatResponse = z11.object({
|
|
|
2089
1473
|
* ambiguous across sites, and "the lease you no longer hold" is exactly
|
|
2090
1474
|
* what this field means anyway.
|
|
2091
1475
|
*/
|
|
2092
|
-
lost:
|
|
2093
|
-
|
|
1476
|
+
lost: z9.array(
|
|
1477
|
+
z9.object({ jobId: z9.string().min(1), leaseId: z9.string().min(1) }).strict()
|
|
2094
1478
|
),
|
|
2095
1479
|
/** Server clock, so a daemon with a skewed clock still honors leases. */
|
|
2096
|
-
serverTime:
|
|
1480
|
+
serverTime: z9.number().int().positive(),
|
|
2097
1481
|
/**
|
|
2098
1482
|
* Sites whose disclosure the user must read again before work moves —
|
|
2099
1483
|
* cloud_008 finding 48, named rather than counted.
|
|
@@ -2108,13 +1492,36 @@ var HeartbeatResponse = z11.object({
|
|
|
2108
1492
|
* operator stopped it" — one word with two subjects on two halves of one
|
|
2109
1493
|
* exchange is a confusion nobody untangles from a log.
|
|
2110
1494
|
*/
|
|
2111
|
-
awaitingConsent:
|
|
1495
|
+
awaitingConsent: z9.array(z9.string().min(1)),
|
|
1496
|
+
/**
|
|
1497
|
+
* A version this daemon should move itself to — B053.
|
|
1498
|
+
*
|
|
1499
|
+
* The channel the auto-updater reads, and it is the channel the daemon
|
|
1500
|
+
* already polls rather than a new phone-home, which is what the ruling
|
|
1501
|
+
* asked for (016 §Auto-update).
|
|
1502
|
+
*
|
|
1503
|
+
* **The hub may not send this to every daemon.** This schema is
|
|
1504
|
+
* `.strict()`, so a daemon built before the field exists does not ignore
|
|
1505
|
+
* it — it rejects the whole heartbeat and stops working. Which would mean
|
|
1506
|
+
* the message carrying the update is the message that breaks the machines
|
|
1507
|
+
* it was meant to update.
|
|
1508
|
+
*
|
|
1509
|
+
* That is decidable without any new handshake, because the request
|
|
1510
|
+
* already carries `daemonVersion`. {@link mayOfferUpdate} is the rule,
|
|
1511
|
+
* kept here as code rather than as a paragraph in a runbook, so both
|
|
1512
|
+
* sides read the same one.
|
|
1513
|
+
*
|
|
1514
|
+
* Exact versions only, never a tag: the daemon refuses anything else, and
|
|
1515
|
+
* a fleet resolving one tag at different minutes is a fleet on different
|
|
1516
|
+
* builds reporting one number.
|
|
1517
|
+
*/
|
|
1518
|
+
updateTo: z9.string().min(1).optional()
|
|
2112
1519
|
}).strict();
|
|
2113
|
-
var ResultDisposition =
|
|
2114
|
-
var ResultRequest =
|
|
2115
|
-
protocolVersion:
|
|
2116
|
-
runnerId:
|
|
2117
|
-
jobId:
|
|
1520
|
+
var ResultDisposition = z9.enum(["ok", "error", "canceled"]);
|
|
1521
|
+
var ResultRequest = z9.object({
|
|
1522
|
+
protocolVersion: z9.literal(PROTOCOL_VERSION),
|
|
1523
|
+
runnerId: z9.string().min(1),
|
|
1524
|
+
jobId: z9.string().min(1),
|
|
2118
1525
|
/**
|
|
2119
1526
|
* The grant this result was produced under — cloud_008 §1.4a.
|
|
2120
1527
|
*
|
|
@@ -2137,7 +1544,7 @@ var ResultRequest = z11.object({
|
|
|
2137
1544
|
* learned once already, when a replayed release yanked a later grant, and
|
|
2138
1545
|
* it applies here for the same reason.
|
|
2139
1546
|
*/
|
|
2140
|
-
leaseId:
|
|
1547
|
+
leaseId: z9.string().min(1),
|
|
2141
1548
|
/**
|
|
2142
1549
|
* The outcome, sealed to the site and signed by the device.
|
|
2143
1550
|
*
|
|
@@ -2169,12 +1576,12 @@ var ResultRequest = z11.object({
|
|
|
2169
1576
|
// on it, so it is a class a routing party consumes. Nobody between the
|
|
2170
1577
|
// two ends consumes these.
|
|
2171
1578
|
}).strict();
|
|
2172
|
-
var ResultResponse =
|
|
1579
|
+
var ResultResponse = z9.object({
|
|
2173
1580
|
/**
|
|
2174
1581
|
* False when this submission wrote nothing — the daemon should discard,
|
|
2175
1582
|
* not retry ({@link MUSTS.RESULT_IDEMPOTENT}).
|
|
2176
1583
|
*/
|
|
2177
|
-
accepted:
|
|
1584
|
+
accepted: z9.boolean(),
|
|
2178
1585
|
/**
|
|
2179
1586
|
* True when this device had already recorded this job's result.
|
|
2180
1587
|
*
|
|
@@ -2188,196 +1595,923 @@ var ResultResponse = z11.object({
|
|
|
2188
1595
|
* the same refusal it would get for a job that is *not* terminal, so a job
|
|
2189
1596
|
* id cannot be used as a terminality probe.
|
|
2190
1597
|
*/
|
|
2191
|
-
duplicate:
|
|
1598
|
+
duplicate: z9.boolean().optional(),
|
|
2192
1599
|
/** The job's state after this submission. */
|
|
2193
|
-
state:
|
|
1600
|
+
state: z9.string().min(1)
|
|
1601
|
+
}).strict();
|
|
1602
|
+
var ReleaseRequest = z9.object({
|
|
1603
|
+
protocolVersion: z9.literal(PROTOCOL_VERSION),
|
|
1604
|
+
runnerId: z9.string().min(1),
|
|
1605
|
+
/**
|
|
1606
|
+
* Which leases to release — the grant, not just the job.
|
|
1607
|
+
*
|
|
1608
|
+
* A release naming only a job id releases whatever lease exists at the
|
|
1609
|
+
* moment it arrives, which for a replayed request is not the lease the
|
|
1610
|
+
* daemon meant. See {@link Lease.id}.
|
|
1611
|
+
*/
|
|
1612
|
+
leases: z9.array(GrantRef),
|
|
1613
|
+
/**
|
|
1614
|
+
* Why, so the app's runner list can say something true.
|
|
1615
|
+
*
|
|
1616
|
+
* `refused` is load-bearing, not cosmetic: the server cannot evaluate
|
|
1617
|
+
* what a device will admit (§4.2), so it may legitimately offer
|
|
1618
|
+
* a job this daemon then declines. The server MUST record the refusal and
|
|
1619
|
+
* stop offering that job to that runner, or the pair would spin between
|
|
1620
|
+
* claim and release forever.
|
|
1621
|
+
*/
|
|
1622
|
+
reason: z9.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
|
|
2194
1623
|
}).strict();
|
|
2195
|
-
var
|
|
2196
|
-
|
|
2197
|
-
|
|
1624
|
+
var ReleaseResponse = z9.object({
|
|
1625
|
+
released: z9.array(z9.string().min(1))
|
|
1626
|
+
}).strict();
|
|
1627
|
+
var WireErrorCode = z9.enum([
|
|
1628
|
+
"bad-request",
|
|
1629
|
+
"unsupported-protocol-version",
|
|
1630
|
+
/**
|
|
1631
|
+
* The daemon is older than this hub will serve — B052.
|
|
1632
|
+
*
|
|
1633
|
+
* Distinct from `unsupported-protocol-version`, which is about the
|
|
1634
|
+
* contract; this is about the build. A daemon can speak protocol 1
|
|
1635
|
+
* perfectly and still be old enough that we would rather move it than keep
|
|
1636
|
+
* carrying it — and the two need different remedies in the message, since
|
|
1637
|
+
* one is "your daemon and this server disagree" and the other is "yours
|
|
1638
|
+
* works, and it is time".
|
|
1639
|
+
*
|
|
1640
|
+
* The floor is the backstop to the auto-updater (B053), and the only lever
|
|
1641
|
+
* that reaches a machine which never opted into offers.
|
|
1642
|
+
*/
|
|
1643
|
+
"daemon-below-floor",
|
|
1644
|
+
// "We do not know who you are." Exactly 401, and only that — cloud_008
|
|
1645
|
+
// §1.4d.
|
|
1646
|
+
"unauthorized",
|
|
1647
|
+
/**
|
|
1648
|
+
* "We know exactly who you are, and the answer is no." Exactly 403.
|
|
1649
|
+
*
|
|
1650
|
+
* Five refusals across both planes served 403 with `unauthorized`, whose
|
|
1651
|
+
* table entry is 401: a revoked device, a site claiming another site's
|
|
1652
|
+
* stub, a job you do not hold, a device belonging to another owner, a
|
|
1653
|
+
* relay that does not route for you. Every one of them is an *identified*
|
|
1654
|
+
* caller being refused.
|
|
1655
|
+
*
|
|
1656
|
+
* Collapsing the two loses a distinction that matters everywhere it is
|
|
1657
|
+
* read: a revoked daemon would look like an unsigned one in every log and
|
|
1658
|
+
* every client branch, and "check your keys" is the wrong advice for both
|
|
1659
|
+
* of them in opposite directions.
|
|
1660
|
+
*/
|
|
1661
|
+
"forbidden",
|
|
1662
|
+
"revoked",
|
|
1663
|
+
"not-found",
|
|
1664
|
+
// Claimed, but the site has not sealed the payload yet — cloud_008 §1.4.
|
|
1665
|
+
//
|
|
1666
|
+
// A daemon must retry rather than abandon: the job is legitimately still
|
|
1667
|
+
// its own until the lease or the awaiting-payload clock says otherwise.
|
|
1668
|
+
// That is why it cannot be `not-found` or `server-error`, and why it was
|
|
1669
|
+
// the protocol gap that produced a bare 409 in the first place.
|
|
1670
|
+
"not-ready",
|
|
1671
|
+
/**
|
|
1672
|
+
* The job is over, and this call is about a job — V1-6, and the code the
|
|
1673
|
+
* site plane has been serving without one (V1-13).
|
|
1674
|
+
*
|
|
1675
|
+
* Distinct from `not-found`, which says "no such job", and from
|
|
1676
|
+
* `not-ready`, which says "not yet, keep asking". This one says "yes, and
|
|
1677
|
+
* it finished" — so a daemon must stop rather than retry, and a replayed
|
|
1678
|
+
* request must not be able to reopen it.
|
|
1679
|
+
*/
|
|
1680
|
+
"too-late",
|
|
1681
|
+
// The caller's clock is too far from ours to judge a signature's freshness.
|
|
1682
|
+
//
|
|
1683
|
+
// Split out from `unauthorized` because the remedy is completely different
|
|
1684
|
+
// and only the server can tell them apart: a bad signature means the key is
|
|
1685
|
+
// wrong, this means the machine's time is wrong. A daemon reporting it as a
|
|
1686
|
+
// generic rejection sends its owner looking at their network.
|
|
1687
|
+
"clock-skew",
|
|
1688
|
+
"rate-limited",
|
|
1689
|
+
"server-error"
|
|
1690
|
+
]);
|
|
1691
|
+
var WireError = z9.object({
|
|
1692
|
+
error: WireErrorCode,
|
|
1693
|
+
message: z9.string().min(1),
|
|
1694
|
+
/**
|
|
1695
|
+
* What this server speaks, on `unsupported-protocol-version` — §B.4.
|
|
1696
|
+
*
|
|
1697
|
+
* The refusal has carried these since the version handshake existed and
|
|
1698
|
+
* the enumeration did not model them, so the one error that exists to be
|
|
1699
|
+
* *acted on* was the one that failed to parse as a wire error. Found by
|
|
1700
|
+
* the relay's own suite the day the relay started sending it: a refusal
|
|
1701
|
+
* outside the enumerated shape is a refusal a client cannot branch on,
|
|
1702
|
+
* which is the whole reason §1.4 enumerates them.
|
|
1703
|
+
*
|
|
1704
|
+
* Modelled the way `clock-skew`'s two fields already are — code-specific
|
|
1705
|
+
* extras, refused on any other code by the refinement below.
|
|
1706
|
+
*/
|
|
1707
|
+
supported: z9.array(z9.string().min(1)).optional(),
|
|
1708
|
+
minimum: z9.string().min(1).optional(),
|
|
1709
|
+
/**
|
|
1710
|
+
* The oldest daemon this hub serves, on `daemon-below-floor` — B052.
|
|
1711
|
+
*
|
|
1712
|
+
* Carried for the same reason `supported` and `minimum` are: a refusal
|
|
1713
|
+
* that cannot be branched on is a refusal a client can only print. The
|
|
1714
|
+
* message already names the floor for a person; this names it for the
|
|
1715
|
+
* code, so a surface can say "you are two versions under" without
|
|
1716
|
+
* parsing English.
|
|
1717
|
+
*/
|
|
1718
|
+
floor: z9.string().min(1).optional(),
|
|
1719
|
+
/** Seconds; mirrors Retry-After for `rate-limited` and `server-error`. */
|
|
1720
|
+
retryAfter: z9.number().int().nonnegative().optional(),
|
|
1721
|
+
/**
|
|
1722
|
+
* The server's clock, and the window it allows. `clock-skew` only.
|
|
1723
|
+
*
|
|
1724
|
+
* So the far side can say *how far off* rather than *that something is
|
|
1725
|
+
* wrong* — the difference between "adjust your clock by four minutes" and
|
|
1726
|
+
* "something is wrong with your connection". Not a disclosure: the
|
|
1727
|
+
* heartbeat response returns the same value, and so does every `Date`
|
|
1728
|
+
* header.
|
|
1729
|
+
*/
|
|
1730
|
+
serverTime: z9.number().int().positive().optional(),
|
|
1731
|
+
maxSkewMs: z9.number().int().positive().optional()
|
|
1732
|
+
}).strict().superRefine((error, ctx) => {
|
|
1733
|
+
const skew = error.error === "clock-skew";
|
|
1734
|
+
const carried = error.serverTime !== void 0 || error.maxSkewMs !== void 0;
|
|
1735
|
+
if (skew && !carried) {
|
|
1736
|
+
ctx.addIssue({
|
|
1737
|
+
code: "custom",
|
|
1738
|
+
message: "clock-skew must carry serverTime and maxSkewMs"
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
if (!skew && carried) {
|
|
1742
|
+
ctx.addIssue({
|
|
1743
|
+
code: "custom",
|
|
1744
|
+
message: `${error.error} must not carry serverTime or maxSkewMs`
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
const floored = error.error === "daemon-below-floor";
|
|
1748
|
+
if (floored && error.floor === void 0) {
|
|
1749
|
+
ctx.addIssue({
|
|
1750
|
+
code: "custom",
|
|
1751
|
+
message: "daemon-below-floor must carry floor"
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
if (!floored && error.floor !== void 0) {
|
|
1755
|
+
ctx.addIssue({
|
|
1756
|
+
code: "custom",
|
|
1757
|
+
message: `${error.error} must not carry floor`
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
const version = error.error === "unsupported-protocol-version";
|
|
1761
|
+
const versionFields = error.supported !== void 0 || error.minimum !== void 0;
|
|
1762
|
+
if (version && !versionFields) {
|
|
1763
|
+
ctx.addIssue({
|
|
1764
|
+
code: "custom",
|
|
1765
|
+
message: "unsupported-protocol-version must carry supported and minimum"
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
if (!version && versionFields) {
|
|
1769
|
+
ctx.addIssue({
|
|
1770
|
+
code: "custom",
|
|
1771
|
+
message: `${error.error} must not carry supported or minimum`
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
});
|
|
1775
|
+
var ERROR_STATUS = Object.freeze({
|
|
1776
|
+
"bad-request": 400,
|
|
1777
|
+
"unsupported-protocol-version": 400,
|
|
1778
|
+
/**
|
|
1779
|
+
* 426 Upgrade Required — B052, and it is the one status that says this.
|
|
1780
|
+
*
|
|
1781
|
+
* Not 403, which this daemon reads as a permission problem and which
|
|
1782
|
+
* sits beside `revoked` in every log. Not 400, which reads as a
|
|
1783
|
+
* malformed request; the request was perfect and the sender is old.
|
|
1784
|
+
*
|
|
1785
|
+
* It also fails safely on a daemon that predates the code: 426 is not in
|
|
1786
|
+
* that switch, so it lands on the 4xx default — `rejected`, which is
|
|
1787
|
+
* "never retried: the request is wrong, and repeating it stays wrong".
|
|
1788
|
+
* Vaguer than the remedy, and the right behaviour, which is what a
|
|
1789
|
+
* fallback has to be.
|
|
1790
|
+
*/
|
|
1791
|
+
"daemon-below-floor": 426,
|
|
1792
|
+
unauthorized: 401,
|
|
1793
|
+
forbidden: 403,
|
|
1794
|
+
revoked: 403,
|
|
1795
|
+
"not-found": 404,
|
|
1796
|
+
// 409, not 404: the job exists and is yours, it is simply not ready.
|
|
1797
|
+
"not-ready": 409,
|
|
1798
|
+
// The same 409 as `not-ready` and the opposite instruction: that one says
|
|
1799
|
+
// keep asking, this one says stop. The status is the class of the
|
|
1800
|
+
// problem — a request that does not fit the resource's state — and the
|
|
1801
|
+
// code is what a caller acts on.
|
|
1802
|
+
"too-late": 409,
|
|
1803
|
+
// 401 alongside `unauthorized`, because that is what it is — the
|
|
1804
|
+
// signature could not be judged. The code is what carries the remedy.
|
|
1805
|
+
"clock-skew": 401,
|
|
1806
|
+
"rate-limited": 429,
|
|
1807
|
+
"server-error": 500
|
|
1808
|
+
});
|
|
1809
|
+
var FetchRequest = z9.object({
|
|
1810
|
+
// `literal`, like every other request — V1-17. This one said
|
|
1811
|
+
// `string().min(1)`, so a daemon speaking a version this server does not
|
|
1812
|
+
// know got past the handshake on the one endpoint that hands over a
|
|
1813
|
+
// sealed payload. The version check exists so that a mismatch is a named
|
|
1814
|
+
// refusal rather than a schema failure three fields later; here it was
|
|
1815
|
+
// neither.
|
|
1816
|
+
protocolVersion: z9.literal(PROTOCOL_VERSION),
|
|
1817
|
+
runnerId: z9.string().min(1),
|
|
1818
|
+
jobId: z9.string().min(1),
|
|
2198
1819
|
/**
|
|
2199
|
-
*
|
|
1820
|
+
* The grant this daemon holds.
|
|
2200
1821
|
*
|
|
2201
|
-
*
|
|
2202
|
-
*
|
|
2203
|
-
*
|
|
1822
|
+
* Named, not inferred: a fetch is lease-scoped, and a request that names
|
|
1823
|
+
* only the job would be answerable for whatever lease exists when it
|
|
1824
|
+
* arrives ({@link Lease.id}).
|
|
2204
1825
|
*/
|
|
2205
|
-
|
|
1826
|
+
leaseId: z9.string().min(1)
|
|
1827
|
+
}).strict();
|
|
1828
|
+
var FetchResponse = z9.object({
|
|
2206
1829
|
/**
|
|
2207
|
-
*
|
|
1830
|
+
* The work, sealed to the device that claimed it — byollm_009 §6.
|
|
2208
1831
|
*
|
|
2209
|
-
*
|
|
2210
|
-
*
|
|
2211
|
-
*
|
|
2212
|
-
*
|
|
2213
|
-
* claim and release forever.
|
|
1832
|
+
* Not plaintext. The site opens its own at-rest envelope and re-seals to
|
|
1833
|
+
* the claiming device's key, signed by the site's identity, so the work
|
|
1834
|
+
* is readable only by the machine that took it and only if it came from
|
|
1835
|
+
* the site that machine pinned.
|
|
2214
1836
|
*/
|
|
2215
|
-
|
|
1837
|
+
envelope: SealedEnvelope
|
|
2216
1838
|
}).strict();
|
|
2217
|
-
|
|
2218
|
-
|
|
1839
|
+
|
|
1840
|
+
// src/update-offer.ts
|
|
1841
|
+
var UPDATE_OFFER_SINCE = "0.1.0-alpha.83";
|
|
1842
|
+
function parse(version) {
|
|
1843
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(
|
|
1844
|
+
version
|
|
1845
|
+
);
|
|
1846
|
+
if (match === null) return void 0;
|
|
1847
|
+
return {
|
|
1848
|
+
release: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
1849
|
+
pre: match[4] === void 0 ? [] : match[4].split(".").map((part) => /^\d+$/.test(part) ? Number(part) : part)
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
function compareVersions(a, b) {
|
|
1853
|
+
const left = parse(a);
|
|
1854
|
+
const right = parse(b);
|
|
1855
|
+
if (left === void 0 || right === void 0) return void 0;
|
|
1856
|
+
for (let i = 0; i < 3; i += 1) {
|
|
1857
|
+
const diff = (left.release[i] ?? 0) - (right.release[i] ?? 0);
|
|
1858
|
+
if (diff !== 0) return diff < 0 ? -1 : 1;
|
|
1859
|
+
}
|
|
1860
|
+
if (left.pre.length === 0 && right.pre.length > 0) return 1;
|
|
1861
|
+
if (left.pre.length > 0 && right.pre.length === 0) return -1;
|
|
1862
|
+
for (let i = 0; i < Math.max(left.pre.length, right.pre.length); i += 1) {
|
|
1863
|
+
const l = left.pre[i];
|
|
1864
|
+
const r = right.pre[i];
|
|
1865
|
+
if (l === void 0) return -1;
|
|
1866
|
+
if (r === void 0) return 1;
|
|
1867
|
+
if (l === r) continue;
|
|
1868
|
+
if (typeof l === "number" && typeof r === "number") return l < r ? -1 : 1;
|
|
1869
|
+
if (typeof l === "number") return -1;
|
|
1870
|
+
if (typeof r === "number") return 1;
|
|
1871
|
+
return l < r ? -1 : 1;
|
|
1872
|
+
}
|
|
1873
|
+
return 0;
|
|
1874
|
+
}
|
|
1875
|
+
function mayOfferUpdate(daemonVersion) {
|
|
1876
|
+
const order = compareVersions(daemonVersion, UPDATE_OFFER_SINCE);
|
|
1877
|
+
return order !== void 0 && order >= 0;
|
|
1878
|
+
}
|
|
1879
|
+
function checkDaemonFloor(input) {
|
|
1880
|
+
const order = compareVersions(input.daemonVersion, input.floor);
|
|
1881
|
+
if (order === void 0 || order >= 0) return null;
|
|
1882
|
+
return {
|
|
1883
|
+
error: "daemon-below-floor",
|
|
1884
|
+
message: `byollm ${input.daemonVersion} is below the supported floor (${input.floor}). Run \`${input.upgradeCommand}\`, then \`byollm start\`.`,
|
|
1885
|
+
floor: input.floor
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
function updateOfferFor(input) {
|
|
1889
|
+
const { offer, daemonVersion } = input;
|
|
1890
|
+
if (offer === void 0) return {};
|
|
1891
|
+
if (exactOffer(offer) === void 0) return {};
|
|
1892
|
+
if (!mayOfferUpdate(daemonVersion)) return {};
|
|
1893
|
+
if (daemonVersion === offer) return {};
|
|
1894
|
+
return { updateTo: offer };
|
|
1895
|
+
}
|
|
1896
|
+
function exactOffer(value) {
|
|
1897
|
+
return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(value) ? value : void 0;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// src/about.ts
|
|
1901
|
+
var ABOUT = `# About BYOLLM
|
|
1902
|
+
|
|
1903
|
+
**What BYOLLM is**
|
|
1904
|
+
|
|
1905
|
+
BYOLLM lets you use your own AI on websites. You install one small program on
|
|
1906
|
+
your computer. Then, websites that support BYOLLM can use the AI you already
|
|
1907
|
+
have \u2014 a free model running on your machine, or an AI service you already pay
|
|
1908
|
+
for \u2014 instead of the website paying for AI and passing the cost to you.
|
|
1909
|
+
|
|
1910
|
+
**Why it matters**
|
|
1911
|
+
|
|
1912
|
+
For you:
|
|
1913
|
+
|
|
1914
|
+
- Your favorite model, everywhere you go.
|
|
1915
|
+
- New models the moment you get them \u2013 not when a site gets around to adding
|
|
1916
|
+
them.
|
|
1917
|
+
- Encrypted end-to-end. Your prompts go to your own device; byollm.cloud can't
|
|
1918
|
+
read them.
|
|
1919
|
+
- Sites never learn which model you use, and your subscriptions are never
|
|
1920
|
+
shared.
|
|
1921
|
+
- Pay less. Sites that don't pay for AI can charge you less \u2013 or nothing.
|
|
1922
|
+
|
|
1923
|
+
For sites and developers:
|
|
1924
|
+
|
|
1925
|
+
- Zero AI bills. Your users bring their own compute.
|
|
1926
|
+
- No floating money \u2013 you don't pay LLM bills up front and hope to collect
|
|
1927
|
+
later, and you never ask people to prepay just to try you.
|
|
1928
|
+
- Free trials that cost you nothing to offer.
|
|
1929
|
+
- Ship the AI features you kept private for fear of the API bill.
|
|
1930
|
+
- One small integration. Your users choose the models.
|
|
1931
|
+
|
|
1932
|
+
**Your device**
|
|
1933
|
+
|
|
1934
|
+
The \`byollm\` program runs on your computer. It knows which AI services you have
|
|
1935
|
+
set up: free open-source models on your machine, metered services you pay per
|
|
1936
|
+
use, or your own subscriptions like Claude Pro/Max. When a website you have
|
|
1937
|
+
enabled sends work, your device runs it with the service you chose. Your
|
|
1938
|
+
prompts are encrypted end-to-end to your own device. byollm.cloud passes them
|
|
1939
|
+
along and cannot read them.
|
|
1940
|
+
|
|
1941
|
+
**Sites**
|
|
1942
|
+
|
|
1943
|
+
A website that wants to use BYOLLM says what it needs \u2014 "writing help," "chat,"
|
|
1944
|
+
and so on. When you connect the site, you pick which of your services answers
|
|
1945
|
+
each one. The site never learns which model you use. You can turn a site off at
|
|
1946
|
+
any time, and it stops getting your work.
|
|
1947
|
+
|
|
1948
|
+
**Teams (optional)**
|
|
1949
|
+
|
|
1950
|
+
A team lets you share what runs on your devices with people you name \u2014 the free
|
|
1951
|
+
open-source models on your machine, or a metered service with a spending limit
|
|
1952
|
+
you set. Your subscription accounts (like Claude Pro/Max) are never shared with
|
|
1953
|
+
anyone. That is a rule, not a setting.
|
|
1954
|
+
|
|
1955
|
+
**byollm.cloud (or your own relay)**
|
|
1956
|
+
|
|
1957
|
+
Many sites, many devices, many people. byollm.cloud keeps track of who has
|
|
1958
|
+
allowed what and sends each job to the right device. It never sees your
|
|
1959
|
+
prompts. If you would rather run this part yourself, the relay is open source \u2014
|
|
1960
|
+
you can run your own instead of using byollm.cloud.`;
|
|
1961
|
+
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.";
|
|
1962
|
+
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.";
|
|
1963
|
+
var ABOUT_SHORT = `${ABOUT_SHORT_LEDE}
|
|
1964
|
+
|
|
1965
|
+
${ABOUT_SHORT_TAIL}`;
|
|
1966
|
+
|
|
1967
|
+
// src/signing.ts
|
|
1968
|
+
import { createHash as createHash2 } from "crypto";
|
|
1969
|
+
import { z as z10 } from "zod";
|
|
1970
|
+
var MAX_CLOCK_SKEW_MS = 12e4;
|
|
1971
|
+
var RequestSignature = z10.object({
|
|
1972
|
+
/** Which runner is calling. The server looks up its pinned identity. */
|
|
1973
|
+
runnerId: z10.string().min(1),
|
|
1974
|
+
/** Epoch ms, bounded by {@link MAX_CLOCK_SKEW_MS}. */
|
|
1975
|
+
issuedAt: z10.number().int().positive(),
|
|
1976
|
+
/** Base64url Ed25519 signature over {@link canonicalRequest}. */
|
|
1977
|
+
signature: z10.string().min(1)
|
|
2219
1978
|
}).strict();
|
|
2220
|
-
|
|
2221
|
-
"
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
1979
|
+
function canonicalRequest(input) {
|
|
1980
|
+
const digest = createHash2("sha256").update(input.body, "utf8").digest("hex");
|
|
1981
|
+
return Buffer.from(
|
|
1982
|
+
[
|
|
1983
|
+
"byollm/v1/request",
|
|
1984
|
+
input.endpoint,
|
|
1985
|
+
input.runnerId,
|
|
1986
|
+
String(input.issuedAt),
|
|
1987
|
+
digest
|
|
1988
|
+
].join("\n"),
|
|
1989
|
+
"utf8"
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
function signRequest(keys, input) {
|
|
1993
|
+
return {
|
|
1994
|
+
runnerId: input.runnerId,
|
|
1995
|
+
issuedAt: input.issuedAt,
|
|
1996
|
+
signature: signWith(keys, canonicalRequest(input))
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
function signSiteRequest(keys, input) {
|
|
2000
|
+
return signRequest(keys, {
|
|
2001
|
+
endpoint: siteEndpoint(input.endpoint),
|
|
2002
|
+
runnerId: input.siteId,
|
|
2003
|
+
issuedAt: input.issuedAt,
|
|
2004
|
+
body: input.body
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
function verifySiteRequest(input) {
|
|
2008
|
+
return verifyRequest({
|
|
2009
|
+
...input,
|
|
2010
|
+
endpoint: siteEndpoint(input.endpoint)
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
var siteEndpoint = (endpoint) => `site/${endpoint}`;
|
|
2014
|
+
function verifyRequest(input) {
|
|
2015
|
+
const skew = input.maxSkewMs ?? MAX_CLOCK_SKEW_MS;
|
|
2016
|
+
if (Math.abs(input.now - input.signature.issuedAt) > skew) return "stale";
|
|
2017
|
+
const ok = verifyWith(
|
|
2018
|
+
input.identityPublic,
|
|
2019
|
+
canonicalRequest({
|
|
2020
|
+
endpoint: input.endpoint,
|
|
2021
|
+
runnerId: input.signature.runnerId,
|
|
2022
|
+
issuedAt: input.signature.issuedAt,
|
|
2023
|
+
body: input.body
|
|
2024
|
+
}),
|
|
2025
|
+
input.signature.signature
|
|
2026
|
+
);
|
|
2027
|
+
return ok ? null : "bad-signature";
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// src/manifest.ts
|
|
2031
|
+
import { z as z11 } from "zod";
|
|
2032
|
+
var RESERVED_PURPOSE = "default";
|
|
2033
|
+
var RENDERABLE = /^[^\p{Cc}\p{Cf}\p{Cs}\p{Co}]+$/u;
|
|
2034
|
+
var renderable = (max, what) => z11.string().min(1).max(max).regex(
|
|
2035
|
+
RENDERABLE,
|
|
2036
|
+
`a ${what} is text a person reads \u2014 no control characters, direction overrides or zero-width padding`
|
|
2037
|
+
).refine((value) => value.trim() !== "", {
|
|
2038
|
+
message: `a ${what} cannot be blank`
|
|
2039
|
+
});
|
|
2040
|
+
var PurposeKey = z11.string().regex(
|
|
2041
|
+
/^[a-z0-9][a-z0-9-]*$/,
|
|
2042
|
+
"a purpose key is a lowercase slug \u2014 letters, digits and hyphens"
|
|
2043
|
+
).max(64);
|
|
2044
|
+
var Purpose = z11.object({
|
|
2226
2045
|
/**
|
|
2227
|
-
*
|
|
2228
|
-
*
|
|
2229
|
-
* Five refusals across both planes served 403 with `unauthorized`, whose
|
|
2230
|
-
* table entry is 401: a revoked device, a site claiming another site's
|
|
2231
|
-
* stub, a job you do not hold, a device belonging to another owner, a
|
|
2232
|
-
* relay that does not route for you. Every one of them is an *identified*
|
|
2233
|
-
* caller being refused.
|
|
2046
|
+
* What a person reads on the consent screen. The only rendered field.
|
|
2234
2047
|
*
|
|
2235
|
-
*
|
|
2236
|
-
*
|
|
2237
|
-
*
|
|
2238
|
-
* of them in opposite directions.
|
|
2048
|
+
* Declared rather than derived from the key, because a key is a
|
|
2049
|
+
* compromise between machines and this is not. "Writing Assistant" is
|
|
2050
|
+
* what somebody understands; `writing-assistant` is what travels.
|
|
2239
2051
|
*/
|
|
2240
|
-
"
|
|
2241
|
-
|
|
2242
|
-
"
|
|
2243
|
-
// Claimed, but the site has not sealed the payload yet — cloud_008 §1.4.
|
|
2244
|
-
//
|
|
2245
|
-
// A daemon must retry rather than abandon: the job is legitimately still
|
|
2246
|
-
// its own until the lease or the awaiting-payload clock says otherwise.
|
|
2247
|
-
// That is why it cannot be `not-found` or `server-error`, and why it was
|
|
2248
|
-
// the protocol gap that produced a bare 409 in the first place.
|
|
2249
|
-
"not-ready",
|
|
2052
|
+
label: renderable(80, "label"),
|
|
2053
|
+
/** One line of context for the consent screen. Optional. */
|
|
2054
|
+
description: renderable(280, "description").optional(),
|
|
2250
2055
|
/**
|
|
2251
|
-
* The
|
|
2252
|
-
* site plane has been serving without one (V1-13).
|
|
2056
|
+
* The kinds this purpose uses.
|
|
2253
2057
|
*
|
|
2254
|
-
*
|
|
2255
|
-
*
|
|
2256
|
-
*
|
|
2257
|
-
* request must not be able to reopen it.
|
|
2058
|
+
* A purpose may span kinds, and a mapping is per (purpose, kind) — so a
|
|
2059
|
+
* person can send this purpose's chat to one service and its generation
|
|
2060
|
+
* to another. Listing a kind here is what makes that slot appear.
|
|
2258
2061
|
*/
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2062
|
+
kinds: z11.array(JobKind).min(1).max(JOB_KINDS.length).refine((kinds) => new Set(kinds).size === kinds.length, {
|
|
2063
|
+
message: "a purpose lists each kind once"
|
|
2064
|
+
})
|
|
2065
|
+
}).strict();
|
|
2066
|
+
var MAX_PURPOSES = 32;
|
|
2067
|
+
var Manifest = z11.record(PurposeKey, Purpose).refine((manifest) => Object.keys(manifest).length > 0, {
|
|
2068
|
+
message: "a manifest declares at least one purpose"
|
|
2069
|
+
}).refine((manifest) => Object.keys(manifest).length <= MAX_PURPOSES, {
|
|
2070
|
+
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`
|
|
2071
|
+
}).refine((manifest) => !(RESERVED_PURPOSE in manifest), {
|
|
2072
|
+
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`
|
|
2073
|
+
});
|
|
2074
|
+
function singlePurposeManifest(input) {
|
|
2075
|
+
return {
|
|
2076
|
+
[RESERVED_PURPOSE]: { label: input.label, kinds: [...input.kinds] }
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
// src/musts.ts
|
|
2081
|
+
function kindsOf(must2) {
|
|
2082
|
+
return typeof must2.verifiedBy === "string" ? [must2.verifiedBy] : must2.verifiedBy;
|
|
2083
|
+
}
|
|
2084
|
+
var must = (m) => Object.freeze(m);
|
|
2085
|
+
var MUSTS = Object.freeze({
|
|
2086
|
+
// ---- Pairing and identity -------------------------------------------
|
|
2087
|
+
PAIR_ONE_USER: must({
|
|
2088
|
+
id: "PAIR_ONE_USER",
|
|
2089
|
+
statement: "A runner token MUST be bound to exactly one user; a daemon MUST refuse work not attributable to its paired user.",
|
|
2090
|
+
enforcedBy: "both",
|
|
2091
|
+
verifiedBy: "conformance",
|
|
2092
|
+
source: "byollm_001 \xA7MUSTs"
|
|
2093
|
+
}),
|
|
2094
|
+
PAIR_INTERACTIVE: must({
|
|
2095
|
+
id: "PAIR_INTERACTIVE",
|
|
2096
|
+
statement: "Pairing MUST be interactive (device-code approval in the app's own session); a long-lived pasted secret MUST NOT be accepted as pairing.",
|
|
2097
|
+
enforcedBy: "server",
|
|
2098
|
+
verifiedBy: "conformance",
|
|
2099
|
+
source: "byollm_001 \xA7Endpoints.1"
|
|
2100
|
+
}),
|
|
2101
|
+
PAIR_CODE_EXPIRES: must({
|
|
2102
|
+
id: "PAIR_CODE_EXPIRES",
|
|
2103
|
+
statement: "An unapproved device code MUST expire and MUST NOT be redeemable after expiry.",
|
|
2104
|
+
enforcedBy: "server",
|
|
2105
|
+
verifiedBy: "conformance",
|
|
2106
|
+
source: "byollm_001 \xA7Endpoints.1"
|
|
2107
|
+
}),
|
|
2108
|
+
// ---- Typed job kinds --------------------------------------------------
|
|
2109
|
+
VERSION_HANDSHAKE_REQUIRED: must({
|
|
2110
|
+
id: "VERSION_HANDSHAKE_REQUIRED",
|
|
2111
|
+
statement: "Every protocol request MUST declare a protocol version, and a server MUST refuse an absent or unsupported one with a structured error naming what it supports \u2014 never a generic parse failure.",
|
|
2112
|
+
enforcedBy: "both",
|
|
2113
|
+
verifiedBy: "conformance",
|
|
2114
|
+
source: "byollm_009 \xA74"
|
|
2115
|
+
}),
|
|
2116
|
+
SITE_KEY_BY_STUB: must({
|
|
2117
|
+
id: "SITE_KEY_BY_STUB",
|
|
2118
|
+
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.",
|
|
2119
|
+
enforcedBy: "daemon",
|
|
2120
|
+
// Adversarial, and the reason is the finding that produced it: the
|
|
2121
|
+
// honest paths pass with every site check deleted, because `open`
|
|
2122
|
+
// refuses a signature from the wrong key anyway. What distinguishes an
|
|
2123
|
+
// enforced rule from a coincidence here is a hostile pairing of stub and
|
|
2124
|
+
// envelope, which no conformance client would ever send.
|
|
2125
|
+
verifiedBy: "adversarial",
|
|
2126
|
+
source: "byollm_009 \xA7A.3"
|
|
2127
|
+
}),
|
|
2128
|
+
SITES_LOCALLY_APPROVED: must({
|
|
2129
|
+
id: "SITES_LOCALLY_APPROVED",
|
|
2130
|
+
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.",
|
|
2131
|
+
enforcedBy: "daemon",
|
|
2132
|
+
// Two kinds, and the second is the one that matters — V1-1.
|
|
2133
|
+
//
|
|
2134
|
+
// `construction`: the daemon cannot serve a site that is not in its
|
|
2135
|
+
// pinned map, and admission refuses before a payload is fetched — and
|
|
2136
|
+
// since byollm_016 Amendment K, being in the map is no longer sufficient
|
|
2137
|
+
// either: a signed grant is, and the relay proposing the set cannot
|
|
2138
|
+
// produce one.
|
|
2139
|
+
//
|
|
2140
|
+
// `adversarial`: the property that survives is about a *sequence* —
|
|
2141
|
+
// remove the id, re-offer it under a different key — which no honest
|
|
2142
|
+
// upstream sends and which the fence above does not see. That was the
|
|
2143
|
+
// bypass: the pin was deleted with the id, so the comparison had nothing
|
|
2144
|
+
// to compare against and the substitution arrived as a stranger.
|
|
2145
|
+
// **Not `conformance`, and that is a live gap rather than a judgement.**
|
|
2146
|
+
// Amendment C's succession clause is a rule about two implementations
|
|
2147
|
+
// agreeing, which is what a conformance check is for — but rotating a
|
|
2148
|
+
// site's key is not something `ConformanceTarget` can express, and adding
|
|
2149
|
+
// an optional hook that most targets omit would produce a check reporting
|
|
2150
|
+
// success for a reason unrelated to the property it claims. That is this
|
|
2151
|
+
// project's most-repeated bug, and it is not worth reintroducing for a
|
|
2152
|
+
// stronger-sounding word in a table. The rotation path is verified by
|
|
2153
|
+
// `site-rotation.test.ts` (both directions, against the shipped runner)
|
|
2154
|
+
// and `relay/test/rotation.test.ts` (both planes, against the reference
|
|
2155
|
+
// relay); the missing piece is a second *independent* implementation to
|
|
2156
|
+
// check them against, and there is not one yet.
|
|
2157
|
+
verifiedBy: ["construction", "adversarial"],
|
|
2158
|
+
source: "byollm_009 \xA7B.2, Amendment C"
|
|
2159
|
+
}),
|
|
2160
|
+
KEYS_EXCHANGED_AT_CONSENT: must({
|
|
2161
|
+
id: "KEYS_EXCHANGED_AT_CONSENT",
|
|
2162
|
+
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.",
|
|
2163
|
+
enforcedBy: "both",
|
|
2164
|
+
verifiedBy: "conformance",
|
|
2165
|
+
source: "byollm_009 \xA75"
|
|
2166
|
+
}),
|
|
2167
|
+
REQUESTS_SIGNED_NOT_BEARER: must({
|
|
2168
|
+
id: "REQUESTS_SIGNED_NOT_BEARER",
|
|
2169
|
+
statement: "Every authenticated request MUST be signed by the calling device's pinned identity key, over the endpoint, the runner id, a timestamp and the exact request body. A server MUST NOT accept a bearer credential in place of a signature.",
|
|
2170
|
+
enforcedBy: "both",
|
|
2171
|
+
verifiedBy: "conformance",
|
|
2172
|
+
source: "byollm_009 \xA74.2"
|
|
2173
|
+
}),
|
|
2174
|
+
LEASE_SCOPED_BY_GRANT: must({
|
|
2175
|
+
id: "LEASE_SCOPED_BY_GRANT",
|
|
2176
|
+
statement: "A lease-scoped request MUST name the lease it acts on, and a server MUST apply it only to that lease. Naming the job and the runner is not sufficient: both survive a claim-release-reclaim cycle.",
|
|
2177
|
+
enforcedBy: "both",
|
|
2178
|
+
verifiedBy: "conformance",
|
|
2179
|
+
source: "byollm_009 \xA74.2"
|
|
2180
|
+
}),
|
|
2181
|
+
STUB_METADATA_EXHAUSTIVE: must({
|
|
2182
|
+
id: "STUB_METADATA_EXHAUSTIVE",
|
|
2183
|
+
statement: "A claim MUST answer with stubs carrying exactly the enumerated fields and no payload. An endpoint MUST NOT emit a stub carrying others, and an upstream MUST NOT require any.",
|
|
2184
|
+
enforcedBy: "both",
|
|
2185
|
+
verifiedBy: "conformance",
|
|
2186
|
+
source: "byollm_009 \xA76"
|
|
2187
|
+
}),
|
|
2188
|
+
ENVELOPE_SEALED_AND_SIGNED: must({
|
|
2189
|
+
id: "ENVELOPE_SEALED_AND_SIGNED",
|
|
2190
|
+
statement: "A stored payload MUST be sealed, and MUST be signed by the sender's identity key. An endpoint MUST refuse an envelope whose signature does not verify against the identity it pinned.",
|
|
2191
|
+
enforcedBy: "server",
|
|
2192
|
+
verifiedBy: "conformance",
|
|
2193
|
+
source: "byollm_009 \xA76"
|
|
2194
|
+
}),
|
|
2195
|
+
KIND_TYPED_ONLY: must({
|
|
2196
|
+
id: "KIND_TYPED_ONLY",
|
|
2197
|
+
statement: "Job kinds MUST resolve against handlers baked into the daemon. A daemon MUST refuse an unknown kind rather than guess.",
|
|
2198
|
+
enforcedBy: "daemon",
|
|
2199
|
+
verifiedBy: "conformance",
|
|
2200
|
+
source: "byollm_001 \xA7Jobs are typed data"
|
|
2201
|
+
}),
|
|
2202
|
+
KIND_NO_CODE: must({
|
|
2203
|
+
id: "KIND_NO_CODE",
|
|
2204
|
+
statement: "A server MUST NOT be able to convey code, a shell string, or a path to execute; payloads are data handed to a model only.",
|
|
2205
|
+
enforcedBy: "daemon",
|
|
2206
|
+
verifiedBy: "conformance",
|
|
2207
|
+
source: "byollm_001 \xA7Jobs are typed data; byollm_004 \xA71"
|
|
2208
|
+
}),
|
|
2209
|
+
// ---- Capability and claiming -----------------------------------------
|
|
2210
|
+
CLAIM_REQUIRES_CAPABILITY: must({
|
|
2211
|
+
id: "CLAIM_REQUIRES_CAPABILITY",
|
|
2212
|
+
statement: "A daemon MUST NOT be given a job whose kind is absent from its advertised capability matrix.",
|
|
2213
|
+
enforcedBy: "both",
|
|
2214
|
+
verifiedBy: "conformance",
|
|
2215
|
+
source: "byollm_001 \xA7MUSTs"
|
|
2216
|
+
}),
|
|
2217
|
+
CAPABILITY_IS_DETECTED: must({
|
|
2218
|
+
id: "CAPABILITY_IS_DETECTED",
|
|
2219
|
+
statement: "An advertised capability matrix MUST be the intersection of owner config and detected, healthy reality \u2014 never config alone.",
|
|
2220
|
+
enforcedBy: "daemon",
|
|
2221
|
+
verifiedBy: "conformance",
|
|
2222
|
+
source: "byollm_002 \xA7Routing"
|
|
2223
|
+
}),
|
|
2224
|
+
CLAIM_ATOMIC: must({
|
|
2225
|
+
id: "CLAIM_ATOMIC",
|
|
2226
|
+
statement: "Claiming MUST be atomic: a job MUST NOT be handed to two runners concurrently.",
|
|
2227
|
+
enforcedBy: "server",
|
|
2228
|
+
verifiedBy: "conformance",
|
|
2229
|
+
source: "byollm_001 \xA7Endpoints.2"
|
|
2230
|
+
}),
|
|
2231
|
+
// ---- Leases -----------------------------------------------------------
|
|
2232
|
+
LEASE_HONORED: must({
|
|
2233
|
+
id: "LEASE_HONORED",
|
|
2234
|
+
statement: "A daemon MUST stop work on a job whose lease it has failed to renew, and MUST NOT report a result for an expired lease it no longer holds.",
|
|
2235
|
+
enforcedBy: "daemon",
|
|
2236
|
+
verifiedBy: "conformance",
|
|
2237
|
+
source: "byollm_001 \xA7MUSTs"
|
|
2238
|
+
}),
|
|
2239
|
+
LEASE_RECLAIMABLE: must({
|
|
2240
|
+
id: "LEASE_RECLAIMABLE",
|
|
2241
|
+
statement: "A lease that expires un-renewed MUST make its job claimable again with no loss of the job.",
|
|
2242
|
+
enforcedBy: "server",
|
|
2243
|
+
verifiedBy: "conformance",
|
|
2244
|
+
source: "byollm_001 \xA7Endpoints.2"
|
|
2245
|
+
}),
|
|
2246
|
+
// ---- Audience and offer scope ----------------------------------------
|
|
2247
|
+
AUDIENCE_BOTH_SIDES: must({
|
|
2248
|
+
id: "AUDIENCE_BOTH_SIDES",
|
|
2249
|
+
statement: "A job MUST run on a daemon only if the daemon's offer scope admits the job's owner AND the job's audience admits the daemon's owner.",
|
|
2250
|
+
enforcedBy: "both",
|
|
2251
|
+
verifiedBy: "conformance",
|
|
2252
|
+
source: "byollm_001 \xA7The audience model"
|
|
2253
|
+
}),
|
|
2254
|
+
SUBSCRIPTION_SELF_LOCK: must({
|
|
2255
|
+
id: "SUBSCRIPTION_SELF_LOCK",
|
|
2256
|
+
statement: "A subscription-class backend's offer scope MUST be 'private' and MUST NOT be widened by configuration.",
|
|
2257
|
+
enforcedBy: "daemon",
|
|
2258
|
+
verifiedBy: "conformance",
|
|
2259
|
+
source: "byollm_001 \xA7The audience model"
|
|
2260
|
+
}),
|
|
2261
|
+
METERED_DEFAULTS_SELF: must({
|
|
2262
|
+
id: "METERED_DEFAULTS_SELF",
|
|
2263
|
+
statement: "A metered backend's effective offer scope MUST be 'private' unless the owner has explicitly acknowledged spending money on others' work.",
|
|
2264
|
+
enforcedBy: "daemon",
|
|
2265
|
+
verifiedBy: "conformance",
|
|
2266
|
+
source: "byollm_007 \xA74"
|
|
2267
|
+
}),
|
|
2268
|
+
METERED_REQUIRES_CEILING: must({
|
|
2269
|
+
id: "METERED_REQUIRES_CEILING",
|
|
2270
|
+
statement: "A widened metered backend MUST carry a spend ceiling, and the daemon MUST refuse community work once it is reached.",
|
|
2271
|
+
enforcedBy: "daemon",
|
|
2272
|
+
verifiedBy: "conformance",
|
|
2273
|
+
source: "byollm_007 \xA74"
|
|
2274
|
+
}),
|
|
2275
|
+
COST_NOT_CONFIGURABLE: must({
|
|
2276
|
+
id: "COST_NOT_CONFIGURABLE",
|
|
2277
|
+
statement: "A built-in provider's cost class MUST NOT be overridable by configuration.",
|
|
2278
|
+
enforcedBy: "daemon",
|
|
2279
|
+
verifiedBy: "conformance",
|
|
2280
|
+
source: "byollm_007 \xA72"
|
|
2281
|
+
}),
|
|
2282
|
+
REMOTE_IS_NEVER_FREE: must({
|
|
2283
|
+
id: "REMOTE_IS_NEVER_FREE",
|
|
2284
|
+
statement: "A generic HTTP backend whose base URL is not loopback or private MUST be treated as metered.",
|
|
2285
|
+
enforcedBy: "daemon",
|
|
2286
|
+
verifiedBy: "conformance",
|
|
2287
|
+
source: "byollm_007 \xA72"
|
|
2288
|
+
}),
|
|
2289
|
+
NAMED_LOCAL_ALLOWLIST: must({
|
|
2290
|
+
id: "NAMED_LOCAL_ALLOWLIST",
|
|
2291
|
+
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.",
|
|
2292
|
+
enforcedBy: "daemon",
|
|
2293
|
+
verifiedBy: "conformance",
|
|
2294
|
+
source: "byollm_001 Rev 1 \xA7B"
|
|
2295
|
+
}),
|
|
2296
|
+
REFUSAL_NOT_REOFFERED: must({
|
|
2297
|
+
id: "REFUSAL_NOT_REOFFERED",
|
|
2298
|
+
statement: "A server MUST NOT re-offer a job to a runner that released it with reason 'refused'.",
|
|
2299
|
+
enforcedBy: "server",
|
|
2300
|
+
verifiedBy: "conformance",
|
|
2301
|
+
source: "byollm_001 Rev 1 \xA7B (loop resolved in build review)"
|
|
2302
|
+
}),
|
|
2303
|
+
// ---- Revocation and cancel -------------------------------------------
|
|
2304
|
+
REVOCATION_HONORED: must({
|
|
2305
|
+
id: "REVOCATION_HONORED",
|
|
2306
|
+
statement: "A revoked daemon MUST stop claiming and MUST abandon in-flight work by the next heartbeat at the latest.",
|
|
2307
|
+
enforcedBy: "daemon",
|
|
2308
|
+
verifiedBy: "conformance",
|
|
2309
|
+
source: "byollm_001 \xA7MUSTs"
|
|
2310
|
+
}),
|
|
2311
|
+
CANCEL_HONORED: must({
|
|
2312
|
+
id: "CANCEL_HONORED",
|
|
2313
|
+
statement: "A job id in a heartbeat response's cancel list MUST abort that job's in-flight backend call and be reported as 'canceled'.",
|
|
2314
|
+
enforcedBy: "daemon",
|
|
2315
|
+
verifiedBy: "conformance",
|
|
2316
|
+
source: "byollm_001 Rev 1 \xA7C"
|
|
2317
|
+
}),
|
|
2318
|
+
// ---- Lifecycle, dependencies, delivery -------------------------------
|
|
2319
|
+
DEPENDS_ON_GATING: must({
|
|
2320
|
+
id: "DEPENDS_ON_GATING",
|
|
2321
|
+
statement: "A job MUST NOT be claimable until every job in its dependsOn set has reached the 'ok' state.",
|
|
2322
|
+
enforcedBy: "server",
|
|
2323
|
+
verifiedBy: "conformance",
|
|
2324
|
+
source: "byollm_001 Rev 1 \xA7E"
|
|
2325
|
+
}),
|
|
2326
|
+
TTL_EXPIRY: must({
|
|
2327
|
+
id: "TTL_EXPIRY",
|
|
2328
|
+
statement: "An unclaimed job MUST become 'expired' once its TTL elapses, and the TTL clock MUST start when the job becomes claimable, not at enqueue.",
|
|
2329
|
+
enforcedBy: "server",
|
|
2330
|
+
verifiedBy: "conformance",
|
|
2331
|
+
source: "byollm_001 Rev 1 \xA7D (TTL clock resolved in build review)"
|
|
2332
|
+
}),
|
|
2333
|
+
NO_RUNNER_SIGNAL: must({
|
|
2334
|
+
id: "NO_RUNNER_SIGNAL",
|
|
2335
|
+
statement: "A server MUST surface noRunnerAvailable when no runner with matching capability has heartbeated within the liveness window, and MUST NOT raise it for a job still blocked on dependencies.",
|
|
2336
|
+
enforcedBy: "server",
|
|
2337
|
+
verifiedBy: "conformance",
|
|
2338
|
+
source: "byollm_001 Rev 1 \xA7D"
|
|
2339
|
+
}),
|
|
2340
|
+
RESULT_IDEMPOTENT: must({
|
|
2341
|
+
id: "RESULT_IDEMPOTENT",
|
|
2342
|
+
statement: "Result submission MUST be idempotent by job id; the first terminal outcome wins and later submissions MUST NOT change it.",
|
|
2343
|
+
enforcedBy: "server",
|
|
2344
|
+
verifiedBy: "conformance",
|
|
2345
|
+
source: "byollm_001 \xA7Endpoints.4"
|
|
2346
|
+
}),
|
|
2347
|
+
PROVENANCE_NAMES_DEVICE: must({
|
|
2348
|
+
id: "PROVENANCE_NAMES_DEVICE",
|
|
2349
|
+
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.",
|
|
2350
|
+
enforcedBy: "server",
|
|
2351
|
+
verifiedBy: "conformance",
|
|
2352
|
+
source: "byollm_009 \xA711"
|
|
2353
|
+
}),
|
|
2354
|
+
// ---- The trust surface -------------------------------------------------
|
|
2355
|
+
INGRESS_LOGGED_BEFORE_EXECUTION: must({
|
|
2356
|
+
id: "INGRESS_LOGGED_BEFORE_EXECUTION",
|
|
2357
|
+
statement: "Every executed prompt MUST be appended to the local ingress log before execution begins.",
|
|
2358
|
+
enforcedBy: "daemon",
|
|
2359
|
+
verifiedBy: "conformance",
|
|
2360
|
+
source: "byollm_001 \xA7MUSTs"
|
|
2361
|
+
}),
|
|
2362
|
+
// ---- Execution isolation (byollm_004) ---------------------------------
|
|
2363
|
+
NO_SHELL_INTERPOLATION: must({
|
|
2364
|
+
id: "NO_SHELL_INTERPOLATION",
|
|
2365
|
+
statement: "Process-class backends MUST be invoked with a fixed argv array and the payload delivered on stdin; payload text MUST NOT reach a command line.",
|
|
2366
|
+
enforcedBy: "daemon",
|
|
2367
|
+
verifiedBy: "adversarial",
|
|
2368
|
+
source: "byollm_004 \xA72"
|
|
2369
|
+
}),
|
|
2273
2370
|
/**
|
|
2274
|
-
*
|
|
2371
|
+
* Amended for byollm_016 Phase B, and the amendment is deliberately narrow.
|
|
2275
2372
|
*
|
|
2276
|
-
*
|
|
2277
|
-
*
|
|
2278
|
-
*
|
|
2279
|
-
*
|
|
2280
|
-
*
|
|
2281
|
-
*
|
|
2373
|
+
* A site may now name a **service** on the stub. The temptation is to read
|
|
2374
|
+
* that as a crack in this law, so the statement below says exactly where the
|
|
2375
|
+
* line is: a name selects from a menu the owner published, and resolves to a
|
|
2376
|
+
* model, backend, base URL and flags **only** through that owner's own
|
|
2377
|
+
* config. The site supplies a key; the owner supplies every value it maps
|
|
2378
|
+
* to. A name the owner does not advertise is refused rather than
|
|
2379
|
+
* substituted, because substitution is how "you may pick from my list" turns
|
|
2380
|
+
* into "you may ask for anything and get something".
|
|
2282
2381
|
*
|
|
2283
|
-
*
|
|
2284
|
-
* extras, refused on any other code by the refinement below.
|
|
2285
|
-
*/
|
|
2286
|
-
supported: z11.array(z11.string().min(1)).optional(),
|
|
2287
|
-
minimum: z11.string().min(1).optional(),
|
|
2288
|
-
/** Seconds; mirrors Retry-After for `rate-limited` and `server-error`. */
|
|
2289
|
-
retryAfter: z11.number().int().nonnegative().optional(),
|
|
2290
|
-
/**
|
|
2291
|
-
* The server's clock, and the window it allows. `clock-skew` only.
|
|
2382
|
+
* Two properties keep it from drifting into "sites demand models":
|
|
2292
2383
|
*
|
|
2293
|
-
*
|
|
2294
|
-
*
|
|
2295
|
-
*
|
|
2296
|
-
*
|
|
2297
|
-
*
|
|
2384
|
+
* 1. **Nothing the site sends is ever a value.** No model string, no URL,
|
|
2385
|
+
* no flag crosses the wire — only a key that means nothing off this
|
|
2386
|
+
* owner's machine.
|
|
2387
|
+
* 2. **It is a stub field, never a payload field.** The prompt cannot
|
|
2388
|
+
* reach it. That is unchanged and is the sentence the second clause
|
|
2389
|
+
* below still enforces verbatim.
|
|
2298
2390
|
*/
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2391
|
+
NO_PAYLOAD_ROUTING: must({
|
|
2392
|
+
id: "NO_PAYLOAD_ROUTING",
|
|
2393
|
+
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.",
|
|
2394
|
+
enforcedBy: "daemon",
|
|
2395
|
+
verifiedBy: "adversarial",
|
|
2396
|
+
source: "byollm_004 \xA72, amended byollm_016 \xA7Phase B"
|
|
2397
|
+
}),
|
|
2398
|
+
STRIPPED_CHILD_ENV: must({
|
|
2399
|
+
id: "STRIPPED_CHILD_ENV",
|
|
2400
|
+
statement: "Process-class children MUST spawn with an allowlisted environment, a scratch cwd, no inherited descriptors beyond std streams, and hard timeout and output-size caps.",
|
|
2401
|
+
enforcedBy: "daemon",
|
|
2402
|
+
verifiedBy: "adversarial",
|
|
2403
|
+
source: "byollm_004 \xA72"
|
|
2404
|
+
}),
|
|
2405
|
+
HTTP_BASE_URL_SAFE: must({
|
|
2406
|
+
id: "HTTP_BASE_URL_SAFE",
|
|
2407
|
+
statement: "HTTP-class backends MUST send requests only to the owner-configured base URL and MUST refuse base URLs resolving to cloud-metadata or link-local addresses.",
|
|
2408
|
+
enforcedBy: "daemon",
|
|
2409
|
+
verifiedBy: "adversarial",
|
|
2410
|
+
source: "byollm_004 Rev 1 \xA7Backend taxonomy"
|
|
2411
|
+
}),
|
|
2412
|
+
OUTPUT_INERT: must({
|
|
2413
|
+
id: "OUTPUT_INERT",
|
|
2414
|
+
statement: "Returned text MUST be treated as inert bytes: never evaluated, never written to a payload-named path, never interpolated into a shell or into terminal control sequences when logged.",
|
|
2415
|
+
enforcedBy: "daemon",
|
|
2416
|
+
verifiedBy: "adversarial",
|
|
2417
|
+
source: "byollm_004 \xA72"
|
|
2418
|
+
}),
|
|
2419
|
+
COMMUNITY_BUDGETS: must({
|
|
2420
|
+
id: "COMMUNITY_BUDGETS",
|
|
2421
|
+
statement: "Jobs whose owner is not the daemon's owner MUST be subject to the owner's rate limits, daily cap, and resource budget.",
|
|
2422
|
+
enforcedBy: "daemon",
|
|
2423
|
+
verifiedBy: "adversarial",
|
|
2424
|
+
source: "byollm_004 \xA74"
|
|
2425
|
+
}),
|
|
2426
|
+
REVOCATION_IMMEDIATE: must({
|
|
2427
|
+
id: "REVOCATION_IMMEDIATE",
|
|
2428
|
+
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.",
|
|
2429
|
+
// Both, and stated as one sentence with two obligations rather than
|
|
2430
|
+
// folded into REVOCATION_HONORED. That one binds the *daemon*: a revoked
|
|
2431
|
+
// daemon stops claiming and abandons in-flight work. This binds the
|
|
2432
|
+
// *upstream*. byollm_009 §5 is explicit that the pair is the point — "a
|
|
2433
|
+
// revocation enforced at one end survives a compromise of that end" — and
|
|
2434
|
+
// one entry covering both would make a compromised daemon look compliant.
|
|
2435
|
+
enforcedBy: "both",
|
|
2436
|
+
verifiedBy: "conformance",
|
|
2437
|
+
source: "byollm_009 \xA711"
|
|
2438
|
+
}),
|
|
2439
|
+
CONSENT_BEFORE_ROUTE: must({
|
|
2440
|
+
id: "CONSENT_BEFORE_ROUTE",
|
|
2441
|
+
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.",
|
|
2442
|
+
enforcedBy: "server",
|
|
2443
|
+
verifiedBy: "conformance",
|
|
2444
|
+
source: "byollm_009 \xA711"
|
|
2445
|
+
}),
|
|
2446
|
+
ROSTER_NOT_DISCLOSED: must({
|
|
2447
|
+
id: "ROSTER_NOT_DISCLOSED",
|
|
2448
|
+
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.",
|
|
2449
|
+
// Checkable since cloud_008 §0.2 took `audienceAllow` off the stub: the
|
|
2450
|
+
// property now holds by *absence*, and absence is exactly what a strict
|
|
2451
|
+
// schema and a serialised stub can be asked about. Before that it was a
|
|
2452
|
+
// sentence — and one this project cited in code comments, tests and two
|
|
2453
|
+
// specs as though it were enforced data, which is why it is worth
|
|
2454
|
+
// stating precisely rather than generously.
|
|
2455
|
+
enforcedBy: "both",
|
|
2456
|
+
verifiedBy: "conformance",
|
|
2457
|
+
source: "byollm_009 \xA711"
|
|
2458
|
+
}),
|
|
2459
|
+
EFFECTIVE_OFFER_ONLY: must({
|
|
2460
|
+
id: "EFFECTIVE_OFFER_ONLY",
|
|
2461
|
+
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.",
|
|
2462
|
+
enforcedBy: "both",
|
|
2463
|
+
verifiedBy: "conformance",
|
|
2464
|
+
source: "byollm_009 \xA711"
|
|
2465
|
+
}),
|
|
2466
|
+
FALLBACK_LABELED: must({
|
|
2467
|
+
id: "FALLBACK_LABELED",
|
|
2468
|
+
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.",
|
|
2469
|
+
// `construction` today, and deliberately not `conformance`. Nothing on
|
|
2470
|
+
// the wire yet distinguishes a fallback from any other community job —
|
|
2471
|
+
// the ledger that would give it a surface is unbuilt — so a check would
|
|
2472
|
+
// have to assert something it cannot observe. Promoted the day that
|
|
2473
|
+
// surface exists. Marking it `conformance` now would put "verified"
|
|
2474
|
+
// beside a property no third party can see, which is the one thing the
|
|
2475
|
+
// kinds exist to prevent.
|
|
2476
|
+
enforcedBy: "both",
|
|
2477
|
+
verifiedBy: "construction",
|
|
2478
|
+
source: "byollm_009 \xA711"
|
|
2479
|
+
}),
|
|
2480
|
+
RELAY_BLIND: must({
|
|
2481
|
+
id: "RELAY_BLIND",
|
|
2482
|
+
statement: "A relay MUST NOT hold any key capable of decrypting a payload, a result, or a delta frame.",
|
|
2483
|
+
// Operator: a third party can read the relay's types and see there is
|
|
2484
|
+
// nowhere to put such a key, but the kit certifies a *server* and cannot
|
|
2485
|
+
// reach inside somebody's deployment to prove what it holds.
|
|
2486
|
+
enforcedBy: "server",
|
|
2487
|
+
verifiedBy: "operator",
|
|
2488
|
+
source: "byollm_009 \xA711"
|
|
2489
|
+
}),
|
|
2490
|
+
SHARED_COMPUTE_DISCLOSED: must({
|
|
2491
|
+
id: "SHARED_COMPUTE_DISCLOSED",
|
|
2492
|
+
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.",
|
|
2493
|
+
// Operator, and cloud_008 §0.3 is why the classification now comes with a
|
|
2494
|
+
// standing answer rather than a standing question. The screen is not
|
|
2495
|
+
// wire-observable, but the *string the server composes* is, and it is
|
|
2496
|
+
// now unit-tested with the two false sentences forbidden by name. The
|
|
2497
|
+
// kind stays `operator` because a third-party site can still render
|
|
2498
|
+
// whatever it likes; what changed is that the part inside our own
|
|
2499
|
+
// boundary stopped depending on somebody remembering to audit it.
|
|
2500
|
+
enforcedBy: "server",
|
|
2501
|
+
verifiedBy: "operator",
|
|
2502
|
+
source: "byollm_009 \xA711"
|
|
2503
|
+
})
|
|
2330
2504
|
});
|
|
2331
|
-
var
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
revoked: 403,
|
|
2337
|
-
"not-found": 404,
|
|
2338
|
-
// 409, not 404: the job exists and is yours, it is simply not ready.
|
|
2339
|
-
"not-ready": 409,
|
|
2340
|
-
// The same 409 as `not-ready` and the opposite instruction: that one says
|
|
2341
|
-
// keep asking, this one says stop. The status is the class of the
|
|
2342
|
-
// problem — a request that does not fit the resource's state — and the
|
|
2343
|
-
// code is what a caller acts on.
|
|
2344
|
-
"too-late": 409,
|
|
2345
|
-
// 401 alongside `unauthorized`, because that is what it is — the
|
|
2346
|
-
// signature could not be judged. The code is what carries the remedy.
|
|
2347
|
-
"clock-skew": 401,
|
|
2348
|
-
"rate-limited": 429,
|
|
2349
|
-
"server-error": 500
|
|
2505
|
+
var RETIRED_MUSTS = Object.freeze({
|
|
2506
|
+
RESULT_PROVENANCE: {
|
|
2507
|
+
supersededBy: "PROVENANCE_NAMES_DEVICE",
|
|
2508
|
+
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."
|
|
2509
|
+
}
|
|
2350
2510
|
});
|
|
2351
|
-
var
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
// sealed payload. The version check exists so that a mismatch is a named
|
|
2356
|
-
// refusal rather than a schema failure three fields later; here it was
|
|
2357
|
-
// neither.
|
|
2358
|
-
protocolVersion: z11.literal(PROTOCOL_VERSION),
|
|
2359
|
-
runnerId: z11.string().min(1),
|
|
2360
|
-
jobId: z11.string().min(1),
|
|
2361
|
-
/**
|
|
2362
|
-
* The grant this daemon holds.
|
|
2363
|
-
*
|
|
2364
|
-
* Named, not inferred: a fetch is lease-scoped, and a request that names
|
|
2365
|
-
* only the job would be answerable for whatever lease exists when it
|
|
2366
|
-
* arrives ({@link Lease.id}).
|
|
2367
|
-
*/
|
|
2368
|
-
leaseId: z11.string().min(1)
|
|
2369
|
-
}).strict();
|
|
2370
|
-
var FetchResponse = z11.object({
|
|
2371
|
-
/**
|
|
2372
|
-
* The work, sealed to the device that claimed it — byollm_009 §6.
|
|
2373
|
-
*
|
|
2374
|
-
* Not plaintext. The site opens its own at-rest envelope and re-seals to
|
|
2375
|
-
* the claiming device's key, signed by the site's identity, so the work
|
|
2376
|
-
* is readable only by the machine that took it and only if it came from
|
|
2377
|
-
* the site that machine pinned.
|
|
2378
|
-
*/
|
|
2379
|
-
envelope: SealedEnvelope
|
|
2380
|
-
}).strict();
|
|
2511
|
+
var MUST_IDS = Object.freeze(Object.keys(MUSTS));
|
|
2512
|
+
function mustsVerifiedBy(kind) {
|
|
2513
|
+
return MUST_IDS.filter((id) => kindsOf(MUSTS[id]).includes(kind));
|
|
2514
|
+
}
|
|
2381
2515
|
export {
|
|
2382
2516
|
ABOUT,
|
|
2383
2517
|
ABOUT_SHORT,
|
|
@@ -2472,6 +2606,8 @@ export {
|
|
|
2472
2606
|
StoredKeys,
|
|
2473
2607
|
Succession,
|
|
2474
2608
|
TERMINAL_STATES,
|
|
2609
|
+
UPDATE_OFFER_SINCE,
|
|
2610
|
+
UPGRADE_COMMAND,
|
|
2475
2611
|
WireError,
|
|
2476
2612
|
WireErrorCode,
|
|
2477
2613
|
WithheldKind,
|
|
@@ -2479,8 +2615,10 @@ export {
|
|
|
2479
2615
|
backendName,
|
|
2480
2616
|
canTransition,
|
|
2481
2617
|
canonicalRequest,
|
|
2618
|
+
checkDaemonFloor,
|
|
2482
2619
|
checkProtocolVersion,
|
|
2483
2620
|
classifyCost,
|
|
2621
|
+
compareVersions,
|
|
2484
2622
|
cryptoReady,
|
|
2485
2623
|
declaredVersion,
|
|
2486
2624
|
effectiveOfferScope,
|
|
@@ -2496,6 +2634,7 @@ export {
|
|
|
2496
2634
|
keyId,
|
|
2497
2635
|
kindsOf,
|
|
2498
2636
|
matchAudience,
|
|
2637
|
+
mayOfferUpdate,
|
|
2499
2638
|
mustsVerifiedBy,
|
|
2500
2639
|
open,
|
|
2501
2640
|
payloadTextLength,
|
|
@@ -2512,6 +2651,7 @@ export {
|
|
|
2512
2651
|
sizeClassCeiling,
|
|
2513
2652
|
sizeClassOf,
|
|
2514
2653
|
successionStatement,
|
|
2654
|
+
updateOfferFor,
|
|
2515
2655
|
verifyGrant,
|
|
2516
2656
|
verifyLink,
|
|
2517
2657
|
verifyPublicIdentity,
|