@byollm/server 0.1.0-alpha.1 → 0.1.0-alpha.11
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 +78 -11
- package/bin/keygen.mjs +21 -0
- package/dist/{chunk-HL6EYHQ7.js → chunk-4NIHWQAT.js} +253 -53
- package/dist/chunk-4NIHWQAT.js.map +1 -0
- package/dist/{handlers-D7lWfwno.d.ts → handlers-DgW0QNTf.d.ts} +27 -4
- package/dist/index.d.ts +176 -12
- package/dist/index.js +429 -27
- package/dist/index.js.map +1 -1
- package/dist/next.d.ts +40 -8
- package/dist/next.js +8 -2
- package/dist/next.js.map +1 -1
- package/dist/{store-D23N6iiP.d.ts → store-Cj5b6A9j.d.ts} +140 -7
- package/dist/supabase/index.d.ts +1 -1
- package/dist/supabase/index.js +91 -24
- package/dist/supabase/index.js.map +1 -1
- package/package.json +16 -4
- package/supabase/migrations/20260809000000_byollm_runner.sql +22 -3
- package/dist/chunk-HL6EYHQ7.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ByollmHandlers,
|
|
3
3
|
SERVED_PROTOCOL_VERSION,
|
|
4
|
-
bearerFrom,
|
|
5
4
|
createFetchHandler,
|
|
6
5
|
generateDeviceCode,
|
|
7
6
|
generateJobId,
|
|
7
|
+
generateLeaseId,
|
|
8
8
|
generateRunnerId,
|
|
9
9
|
generateRunnerToken,
|
|
10
10
|
generateUserCode,
|
|
11
11
|
hashSecret,
|
|
12
|
+
resealForDevice,
|
|
12
13
|
routeEndpoint,
|
|
13
|
-
secretsMatch
|
|
14
|
-
|
|
14
|
+
secretsMatch,
|
|
15
|
+
signatureFrom
|
|
16
|
+
} from "./chunk-4NIHWQAT.js";
|
|
15
17
|
import {
|
|
16
18
|
NoRunnerAvailableError,
|
|
17
19
|
PollingDelivery,
|
|
@@ -20,19 +22,238 @@ import {
|
|
|
20
22
|
|
|
21
23
|
// src/app.ts
|
|
22
24
|
import {
|
|
25
|
+
ENVELOPE_MAX_AGE_MS,
|
|
26
|
+
KindedPayload,
|
|
27
|
+
keyId as keyId2,
|
|
28
|
+
payloadTextLength,
|
|
29
|
+
publicIdentityOf as publicIdentityOf2,
|
|
30
|
+
seal,
|
|
31
|
+
sizeClassOf,
|
|
23
32
|
backendDescriptor,
|
|
24
33
|
matchAudience
|
|
25
34
|
} from "@byollm/protocol";
|
|
35
|
+
|
|
36
|
+
// src/cloud.ts
|
|
37
|
+
import {
|
|
38
|
+
JobOutcome,
|
|
39
|
+
keyId,
|
|
40
|
+
open,
|
|
41
|
+
publicIdentityOf,
|
|
42
|
+
provenanceFor,
|
|
43
|
+
signSiteRequest
|
|
44
|
+
} from "@byollm/protocol";
|
|
45
|
+
var CloudLane = class {
|
|
46
|
+
#options;
|
|
47
|
+
#store;
|
|
48
|
+
#siteKeys;
|
|
49
|
+
#now;
|
|
50
|
+
#fetch;
|
|
51
|
+
constructor(deps) {
|
|
52
|
+
this.#options = deps.options;
|
|
53
|
+
this.#store = deps.store;
|
|
54
|
+
this.#siteKeys = deps.siteKeys;
|
|
55
|
+
this.#now = deps.now;
|
|
56
|
+
this.#fetch = deps.options.fetch ?? globalThis.fetch;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Publish a job's stub for routing.
|
|
60
|
+
*
|
|
61
|
+
* The stub and nothing else — byollm_009 §6 makes that exhaustive by
|
|
62
|
+
* construction, so this cannot leak a payload even by mistake: there is no
|
|
63
|
+
* field on `JobStub` to put one in.
|
|
64
|
+
*/
|
|
65
|
+
async publish(record) {
|
|
66
|
+
const stub = {
|
|
67
|
+
id: record.id,
|
|
68
|
+
kind: record.kind,
|
|
69
|
+
owner: record.owner,
|
|
70
|
+
audience: record.audience,
|
|
71
|
+
...record.audienceAllow === void 0 ? {} : { audienceAllow: [...record.audienceAllow] },
|
|
72
|
+
sizeClass: record.sizeClass,
|
|
73
|
+
streaming: false,
|
|
74
|
+
// The relay needs *a* deadline to bound routing. A job without one gets
|
|
75
|
+
// the envelope's, which is the outer bound on how long the ciphertext
|
|
76
|
+
// is worth carrying — never longer than the work could possibly matter.
|
|
77
|
+
deadlineAt: record.deadlineAt ?? record.createdAt + ENVELOPE_TTL_FALLBACK
|
|
78
|
+
};
|
|
79
|
+
await this.#post("enqueue", {
|
|
80
|
+
siteId: this.#options.siteId,
|
|
81
|
+
stub
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* One cycle: seal for anything claimed, collect anything finished.
|
|
86
|
+
*
|
|
87
|
+
* Idempotent and safe to call as often as you like. Exposed as a single
|
|
88
|
+
* cycle rather than hidden behind a timer so a caller decides its own
|
|
89
|
+
* cadence — a serverless site runs it on a cron, a long-lived one on an
|
|
90
|
+
* interval, and a test runs it exactly when it means to.
|
|
91
|
+
*/
|
|
92
|
+
async pump() {
|
|
93
|
+
const sealed = [];
|
|
94
|
+
const refused = [];
|
|
95
|
+
const completed = [];
|
|
96
|
+
const pending = await this.#get("pending");
|
|
97
|
+
for (const claim of pending.jobs) {
|
|
98
|
+
const record = await this.#store.get(claim.jobId);
|
|
99
|
+
if (!record) continue;
|
|
100
|
+
const resealed = await resealForDevice({
|
|
101
|
+
siteKeys: this.#siteKeys,
|
|
102
|
+
job: {
|
|
103
|
+
id: record.id,
|
|
104
|
+
envelope: record.envelope,
|
|
105
|
+
createdAt: record.createdAt
|
|
106
|
+
},
|
|
107
|
+
device: claim.device
|
|
108
|
+
});
|
|
109
|
+
if (!resealed.ok) {
|
|
110
|
+
refused.push(claim.jobId);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
await this.#store.adopt({
|
|
114
|
+
jobId: claim.jobId,
|
|
115
|
+
leaseId: claim.leaseId,
|
|
116
|
+
expiresAt: claim.awaitingUntil,
|
|
117
|
+
now: this.#now()
|
|
118
|
+
});
|
|
119
|
+
await this.#post("payload", {
|
|
120
|
+
siteId: this.#options.siteId,
|
|
121
|
+
jobId: claim.jobId,
|
|
122
|
+
envelope: resealed.envelope
|
|
123
|
+
});
|
|
124
|
+
sealed.push(claim.jobId);
|
|
125
|
+
}
|
|
126
|
+
const finished = await this.#get("results");
|
|
127
|
+
for (const done of finished.jobs) {
|
|
128
|
+
const record = await this.#store.get(done.jobId);
|
|
129
|
+
if (!record || record.state === "ok" || record.state === "error") {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const outcome = await this.#openResult(done);
|
|
133
|
+
if (!outcome) {
|
|
134
|
+
refused.push(done.jobId);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
await this.#store.complete({
|
|
138
|
+
jobId: done.jobId,
|
|
139
|
+
// The grant, not the machine: this site never paired with the device
|
|
140
|
+
// that ran it, and the signature it verified above is the stronger
|
|
141
|
+
// claim about who did.
|
|
142
|
+
holder: { by: "lease", leaseId: done.leaseId },
|
|
143
|
+
outcome,
|
|
144
|
+
provenance: provenanceFor({
|
|
145
|
+
audience: record.audience,
|
|
146
|
+
runnerId: done.runnerId,
|
|
147
|
+
runnerOwner: keyId(done.device.identity),
|
|
148
|
+
backendClass: "http",
|
|
149
|
+
model: "unknown"
|
|
150
|
+
}),
|
|
151
|
+
now: this.#now()
|
|
152
|
+
});
|
|
153
|
+
completed.push(done.jobId);
|
|
154
|
+
}
|
|
155
|
+
return { sealed, completed, refused };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Open a sealed result and verify it came from the device that claimed it.
|
|
159
|
+
*
|
|
160
|
+
* The relay says which device ran the job; this checks that claim against a
|
|
161
|
+
* signature the relay cannot produce. A relay that named the wrong device
|
|
162
|
+
* gets a refusal, not a stored result — which is what keeps `RELAY_BLIND`
|
|
163
|
+
* from quietly becoming `RELAY_TRUSTED`.
|
|
164
|
+
*/
|
|
165
|
+
async #openResult(done) {
|
|
166
|
+
const opened = await open({
|
|
167
|
+
envelope: done.envelope,
|
|
168
|
+
recipientKeys: this.#siteKeys,
|
|
169
|
+
senderIdentityPublic: done.device.identity,
|
|
170
|
+
expected: {
|
|
171
|
+
jobId: done.jobId,
|
|
172
|
+
senderKeyId: keyId(done.device.identity),
|
|
173
|
+
recipientKeyId: keyId(publicIdentityOf(this.#siteKeys).identity),
|
|
174
|
+
direction: "result"
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
if (!opened.ok) return null;
|
|
178
|
+
let parsed;
|
|
179
|
+
try {
|
|
180
|
+
parsed = JSON.parse(opened.plaintext);
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const outcome = JobOutcome.safeParse(parsed);
|
|
185
|
+
if (!outcome.success) return null;
|
|
186
|
+
if (outcome.data.outcome !== done.disposition) return null;
|
|
187
|
+
return outcome.data;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Sign a site-plane call with this site's identity key.
|
|
191
|
+
*
|
|
192
|
+
* The same scheme the daemon uses against an upstream, because the site is
|
|
193
|
+
* in the same position: an outbound caller whose key the relay already holds
|
|
194
|
+
* for other reasons. Nothing else authenticates this plane — a relay that
|
|
195
|
+
* took the `siteId` in a body at face value would let anyone enqueue work in
|
|
196
|
+
* a site's name and read who claimed it.
|
|
197
|
+
*/
|
|
198
|
+
#headers(endpoint, rawBody) {
|
|
199
|
+
const signature = signSiteRequest(this.#siteKeys, {
|
|
200
|
+
endpoint,
|
|
201
|
+
siteId: this.#options.siteId,
|
|
202
|
+
issuedAt: this.#now(),
|
|
203
|
+
body: rawBody
|
|
204
|
+
});
|
|
205
|
+
return {
|
|
206
|
+
"x-byollm-site": this.#options.siteId,
|
|
207
|
+
"x-byollm-issued-at": String(signature.issuedAt),
|
|
208
|
+
"x-byollm-signature": signature.signature
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
async #post(endpoint, body) {
|
|
212
|
+
const rawBody = JSON.stringify(body);
|
|
213
|
+
const response = await this.#fetch(
|
|
214
|
+
`${this.#options.relayOrigin}/relay/site/${endpoint}`,
|
|
215
|
+
{
|
|
216
|
+
method: "POST",
|
|
217
|
+
headers: {
|
|
218
|
+
"content-type": "application/json",
|
|
219
|
+
...this.#headers(endpoint, rawBody)
|
|
220
|
+
},
|
|
221
|
+
body: rawBody
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
return response.json();
|
|
225
|
+
}
|
|
226
|
+
async #get(endpoint) {
|
|
227
|
+
const url = `${this.#options.relayOrigin}/relay/site/${endpoint}?siteId=${encodeURIComponent(this.#options.siteId)}`;
|
|
228
|
+
const response = await this.#fetch(url, {
|
|
229
|
+
headers: this.#headers(endpoint, "")
|
|
230
|
+
});
|
|
231
|
+
return response.json();
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
var ENVELOPE_TTL_FALLBACK = 24 * 60 * 6e4;
|
|
235
|
+
|
|
236
|
+
// src/app.ts
|
|
26
237
|
var DEFAULT_LIVENESS_MS = 35e3;
|
|
27
238
|
var ByollmApp = class {
|
|
28
239
|
#store;
|
|
240
|
+
#siteKeys;
|
|
29
241
|
#now;
|
|
30
242
|
#livenessMs;
|
|
31
243
|
#delivery;
|
|
244
|
+
/** Present only in the cloud lane; the site's side of the relay. */
|
|
245
|
+
cloud;
|
|
32
246
|
constructor(options) {
|
|
33
247
|
this.#store = options.store;
|
|
248
|
+
this.#siteKeys = options.siteKeys;
|
|
34
249
|
this.#now = options.now ?? Date.now;
|
|
35
250
|
this.#livenessMs = options.livenessMs ?? DEFAULT_LIVENESS_MS;
|
|
251
|
+
this.cloud = options.lane === void 0 ? void 0 : new CloudLane({
|
|
252
|
+
options: options.lane,
|
|
253
|
+
store: options.store,
|
|
254
|
+
siteKeys: options.siteKeys,
|
|
255
|
+
now: this.#now
|
|
256
|
+
});
|
|
36
257
|
const deps = {
|
|
37
258
|
...options.noRunnerGraceMs === void 0 ? {} : { graceMs: options.noRunnerGraceMs },
|
|
38
259
|
read: (jobId) => this.result(jobId),
|
|
@@ -66,7 +287,45 @@ var ByollmApp = class {
|
|
|
66
287
|
* the app is obliged to disclose that to whoever reads it.
|
|
67
288
|
*/
|
|
68
289
|
async enqueue(input) {
|
|
69
|
-
const
|
|
290
|
+
const parsed = KindedPayload.safeParse({
|
|
291
|
+
kind: input.kind,
|
|
292
|
+
payload: input.payload
|
|
293
|
+
});
|
|
294
|
+
if (!parsed.success) {
|
|
295
|
+
const detail = parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
296
|
+
throw new Error(`invalid ${input.kind} payload \u2014 ${detail}`);
|
|
297
|
+
}
|
|
298
|
+
const createdAt = this.#now();
|
|
299
|
+
const envelopeDeadlineAt = createdAt + ENVELOPE_MAX_AGE_MS;
|
|
300
|
+
const jobId = input.id ?? generateJobId();
|
|
301
|
+
const senderKeyId = keyId2(publicIdentityOf2(this.#siteKeys).identity);
|
|
302
|
+
const envelope = await seal({
|
|
303
|
+
plaintext: JSON.stringify(parsed.data.payload),
|
|
304
|
+
senderKeys: this.#siteKeys,
|
|
305
|
+
recipientEncryptionPublic: this.#siteKeys.encryptionPublic,
|
|
306
|
+
context: {
|
|
307
|
+
jobId,
|
|
308
|
+
senderKeyId,
|
|
309
|
+
recipientKeyId: senderKeyId,
|
|
310
|
+
deadlineAt: envelopeDeadlineAt,
|
|
311
|
+
direction: "payload"
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
const record = await this.#store.create(
|
|
315
|
+
{
|
|
316
|
+
...input,
|
|
317
|
+
id: jobId,
|
|
318
|
+
envelope,
|
|
319
|
+
sizeClass: sizeClassOf(
|
|
320
|
+
payloadTextLength({
|
|
321
|
+
kind: input.kind,
|
|
322
|
+
payload: parsed.data.payload
|
|
323
|
+
})
|
|
324
|
+
)
|
|
325
|
+
},
|
|
326
|
+
createdAt
|
|
327
|
+
);
|
|
328
|
+
await this.cloud?.publish(record);
|
|
70
329
|
return {
|
|
71
330
|
id: record.id,
|
|
72
331
|
record,
|
|
@@ -136,7 +395,18 @@ var ByollmApp = class {
|
|
|
136
395
|
{
|
|
137
396
|
owner: runner.owner,
|
|
138
397
|
offerScope: capability.offerScope,
|
|
139
|
-
|
|
398
|
+
// A generic backend's cost depends on its base URL, which the
|
|
399
|
+
// server never sees; assume the expensive reading (byollm_007 §4).
|
|
400
|
+
cost: backendDescriptor(capability.backendId).cost ?? "metered",
|
|
401
|
+
// Consent is the daemon's to hold, and it has already applied it:
|
|
402
|
+
// the offer scope arriving here is the *effective* one, so a
|
|
403
|
+
// metered backend nobody agreed to share advertises `self` and is
|
|
404
|
+
// refused by the scope rule above. Re-deriving consent from
|
|
405
|
+
// `false` here would instead refuse every backend an owner
|
|
406
|
+
// deliberately shared, because the server has no way to learn they
|
|
407
|
+
// did — the signal would be wrong in the direction that breaks
|
|
408
|
+
// working setups.
|
|
409
|
+
spend: { acknowledged: true },
|
|
140
410
|
// Same conservative assumption the claim path makes: the server
|
|
141
411
|
// cannot see a remote daemon's local allowlist (protocol §4.2).
|
|
142
412
|
locallyAllows: () => true
|
|
@@ -218,6 +488,45 @@ function normalizeUserCode(input) {
|
|
|
218
488
|
return bare.length === 8 ? `${bare.slice(0, 4)}-${bare.slice(4)}` : bare;
|
|
219
489
|
}
|
|
220
490
|
|
|
491
|
+
// src/keys.ts
|
|
492
|
+
import { StoredKeys, generateKeys, publicIdentityOf as publicIdentityOf3 } from "@byollm/protocol";
|
|
493
|
+
import { fingerprint } from "@byollm/protocol";
|
|
494
|
+
var generateSiteKeys = (now = Date.now()) => generateKeys(now);
|
|
495
|
+
function siteKeysFromEnv(variable = "BYOLLM_SITE_KEYS", env = process.env) {
|
|
496
|
+
const raw = env[variable];
|
|
497
|
+
if (raw === void 0 || raw === "") {
|
|
498
|
+
throw new Error(
|
|
499
|
+
`${variable} is not set. Generate a site identity once with \`npx @byollm/server keygen\` and set it as ${variable}. Do not generate keys at startup: every instance would get a different identity and daemons would pin one and be refused by another.`
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
let parsed;
|
|
503
|
+
try {
|
|
504
|
+
parsed = JSON.parse(Buffer.from(raw, "base64").toString("utf8"));
|
|
505
|
+
} catch {
|
|
506
|
+
throw new Error(
|
|
507
|
+
`${variable} is not base64-encoded JSON. It should be exactly what \`npx @byollm/server keygen\` printed.`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
const result = StoredKeys.safeParse(parsed);
|
|
511
|
+
if (!result.success) {
|
|
512
|
+
throw new Error(
|
|
513
|
+
`${variable} does not contain a valid site identity. Regenerate it with \`npx @byollm/server keygen\` \u2014 and if this site has already paired daemons, they will need to pair again.`
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return result.data;
|
|
517
|
+
}
|
|
518
|
+
function formatSiteKeys(keys) {
|
|
519
|
+
const encoded = Buffer.from(JSON.stringify(keys)).toString("base64");
|
|
520
|
+
return `# Set this as BYOLLM_SITE_KEYS. It is a secret: it is this site's
|
|
521
|
+
# identity, and anything holding it can be this site.
|
|
522
|
+
BYOLLM_SITE_KEYS=${encoded}
|
|
523
|
+
|
|
524
|
+
# Fingerprint (not secret \u2014 show it to users so they can check what
|
|
525
|
+
# their daemon pinned):
|
|
526
|
+
# ${fingerprint(publicIdentityOf3(keys).identity)}
|
|
527
|
+
`;
|
|
528
|
+
}
|
|
529
|
+
|
|
221
530
|
// src/memory.ts
|
|
222
531
|
import {
|
|
223
532
|
backendDescriptor as backendDescriptor2,
|
|
@@ -236,7 +545,7 @@ var MemoryStore = class {
|
|
|
236
545
|
}
|
|
237
546
|
// -- jobs ---------------------------------------------------------------
|
|
238
547
|
create(input, now) {
|
|
239
|
-
const id = input.id
|
|
548
|
+
const id = input.id;
|
|
240
549
|
const existing = this.#jobs.get(id);
|
|
241
550
|
if (existing) return Promise.resolve(existing);
|
|
242
551
|
const dependsOn = [...input.dependsOn ?? []];
|
|
@@ -246,7 +555,8 @@ var MemoryStore = class {
|
|
|
246
555
|
const job = {
|
|
247
556
|
id,
|
|
248
557
|
kind: input.kind,
|
|
249
|
-
|
|
558
|
+
envelope: input.envelope,
|
|
559
|
+
sizeClass: input.sizeClass,
|
|
250
560
|
audience: input.audience ?? "self",
|
|
251
561
|
owner: input.owner,
|
|
252
562
|
audienceAllow: input.audienceAllow ? [...input.audienceAllow] : void 0,
|
|
@@ -264,7 +574,7 @@ var MemoryStore = class {
|
|
|
264
574
|
provenance: null,
|
|
265
575
|
updatedAt: now
|
|
266
576
|
};
|
|
267
|
-
this.#
|
|
577
|
+
this.#write(id, job);
|
|
268
578
|
return Promise.resolve(job);
|
|
269
579
|
}
|
|
270
580
|
get(jobId) {
|
|
@@ -283,13 +593,16 @@ var MemoryStore = class {
|
|
|
283
593
|
...job,
|
|
284
594
|
state: "claimed",
|
|
285
595
|
lease: {
|
|
596
|
+
// A fresh id per grant. Two claims of the same job by the same
|
|
597
|
+
// runner are two different leases, and must be distinguishable.
|
|
598
|
+
id: generateLeaseId(),
|
|
286
599
|
runnerId: args.runnerId,
|
|
287
600
|
expiresAt: args.now + args.leaseMs
|
|
288
601
|
},
|
|
289
602
|
attempts: job.attempts + 1,
|
|
290
603
|
updatedAt: args.now
|
|
291
604
|
};
|
|
292
|
-
this.#
|
|
605
|
+
this.#write(job.id, updated);
|
|
293
606
|
claimed.push(updated);
|
|
294
607
|
}
|
|
295
608
|
return Promise.resolve(claimed);
|
|
@@ -313,9 +626,16 @@ var MemoryStore = class {
|
|
|
313
626
|
{
|
|
314
627
|
owner: args.runnerOwner,
|
|
315
628
|
offerScope: capability.offerScope,
|
|
316
|
-
// From the registry, not a local guess — the
|
|
317
|
-
//
|
|
318
|
-
|
|
629
|
+
// From the registry, not a local guess — the cost rules must mean the
|
|
630
|
+
// same thing on both sides of the wire. The server cannot see a
|
|
631
|
+
// remote daemon's base URL, so a generic backend with no declared
|
|
632
|
+
// cost is treated as metered: the expensive side, and the daemon
|
|
633
|
+
// refuses anyway if it disagrees (byollm_007 §2).
|
|
634
|
+
cost: backendDescriptor2(capability.backendId).cost ?? "metered",
|
|
635
|
+
// Nor can it see the owner's spend consent. It offers; the daemon is
|
|
636
|
+
// the enforcing side and releases with `refused` if its own rules say
|
|
637
|
+
// no — the same shape as the `named` allowlist.
|
|
638
|
+
spend: { acknowledged: true },
|
|
319
639
|
// The server cannot see a remote daemon's local allowlist and must
|
|
320
640
|
// not pretend to (protocol §4.2). It admits the job here; the daemon
|
|
321
641
|
// is the enforcing side and releases with `refused` if its own list
|
|
@@ -329,9 +649,9 @@ var MemoryStore = class {
|
|
|
329
649
|
this.#expireDueSync(args.now);
|
|
330
650
|
const renewed = [];
|
|
331
651
|
const lost = [];
|
|
332
|
-
for (const jobId of args.
|
|
652
|
+
for (const { jobId, leaseId } of args.leases) {
|
|
333
653
|
const job = this.#jobs.get(jobId);
|
|
334
|
-
if (!job || job.lease?.runnerId !== args.runnerId) {
|
|
654
|
+
if (!job || job.lease?.runnerId !== args.runnerId || job.lease.id !== leaseId) {
|
|
335
655
|
lost.push(jobId);
|
|
336
656
|
continue;
|
|
337
657
|
}
|
|
@@ -340,16 +660,40 @@ var MemoryStore = class {
|
|
|
340
660
|
continue;
|
|
341
661
|
}
|
|
342
662
|
const expiresAt = args.now + args.leaseMs;
|
|
343
|
-
this.#
|
|
663
|
+
this.#write(jobId, {
|
|
344
664
|
...job,
|
|
345
665
|
state: "running",
|
|
346
|
-
|
|
666
|
+
// Renewal extends the existing grant; it does not mint a new one.
|
|
667
|
+
lease: { ...job.lease, expiresAt },
|
|
347
668
|
updatedAt: args.now
|
|
348
669
|
});
|
|
349
670
|
renewed.push({ jobId, expiresAt });
|
|
350
671
|
}
|
|
351
672
|
return Promise.resolve({ renewed, lost });
|
|
352
673
|
}
|
|
674
|
+
adopt(args) {
|
|
675
|
+
const job = this.#jobs.get(args.jobId);
|
|
676
|
+
if (!job) return Promise.resolve(null);
|
|
677
|
+
if (job.state !== "queued" && job.state !== "claimed") {
|
|
678
|
+
return Promise.resolve(null);
|
|
679
|
+
}
|
|
680
|
+
if (job.lease && job.lease.id !== args.leaseId) {
|
|
681
|
+
return Promise.resolve(null);
|
|
682
|
+
}
|
|
683
|
+
const updated = {
|
|
684
|
+
...job,
|
|
685
|
+
state: "claimed",
|
|
686
|
+
lease: {
|
|
687
|
+
id: args.leaseId,
|
|
688
|
+
// No runner: this site never paired with the machine holding it.
|
|
689
|
+
runnerId: "",
|
|
690
|
+
expiresAt: args.expiresAt
|
|
691
|
+
},
|
|
692
|
+
updatedAt: args.now
|
|
693
|
+
};
|
|
694
|
+
this.#write(updated.id, updated);
|
|
695
|
+
return Promise.resolve(updated);
|
|
696
|
+
}
|
|
353
697
|
complete(args) {
|
|
354
698
|
const job = this.#jobs.get(args.jobId);
|
|
355
699
|
if (!job) return Promise.resolve({ accepted: false, job: null });
|
|
@@ -359,7 +703,8 @@ var MemoryStore = class {
|
|
|
359
703
|
if (job.state === "expired") {
|
|
360
704
|
return Promise.resolve({ accepted: false, job });
|
|
361
705
|
}
|
|
362
|
-
|
|
706
|
+
const holds = args.holder.by === "runner" ? job.lease?.runnerId === args.holder.runnerId : job.lease?.id === args.holder.leaseId;
|
|
707
|
+
if (!holds) {
|
|
363
708
|
return Promise.resolve({ accepted: false, job });
|
|
364
709
|
}
|
|
365
710
|
const state = args.outcome.outcome === "ok" ? "ok" : args.outcome.outcome === "canceled" ? "canceled" : "error";
|
|
@@ -371,7 +716,7 @@ var MemoryStore = class {
|
|
|
371
716
|
provenance: args.provenance,
|
|
372
717
|
updatedAt: args.now
|
|
373
718
|
};
|
|
374
|
-
this.#
|
|
719
|
+
this.#write(job.id, updated);
|
|
375
720
|
this.#cancelRequests.delete(job.id);
|
|
376
721
|
if (state === "ok") this.#unblockDependents(job.id, args.now);
|
|
377
722
|
return Promise.resolve({ accepted: true, job: updated });
|
|
@@ -392,16 +737,66 @@ var MemoryStore = class {
|
|
|
392
737
|
(depId) => this.#jobs.get(depId)?.state === "ok"
|
|
393
738
|
);
|
|
394
739
|
if (ready) {
|
|
395
|
-
this.#
|
|
740
|
+
this.#write(job.id, { ...job, claimableAt: now, updatedAt: now });
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Watchers, by job id (byollm_009 §8.3).
|
|
746
|
+
*
|
|
747
|
+
* A `Set` per job so an unsubscribe removes exactly the handler it
|
|
748
|
+
* registered — two waiters on the same job are ordinary, and removing by
|
|
749
|
+
* job id alone would silently cancel someone else's wait.
|
|
750
|
+
*/
|
|
751
|
+
#watchers = /* @__PURE__ */ new Map();
|
|
752
|
+
subscribe(jobId, onChange) {
|
|
753
|
+
const existing = this.#watchers.get(jobId) ?? /* @__PURE__ */ new Set();
|
|
754
|
+
existing.add(onChange);
|
|
755
|
+
this.#watchers.set(jobId, existing);
|
|
756
|
+
let live = true;
|
|
757
|
+
return () => {
|
|
758
|
+
if (!live) return;
|
|
759
|
+
live = false;
|
|
760
|
+
const set = this.#watchers.get(jobId);
|
|
761
|
+
set?.delete(onChange);
|
|
762
|
+
if (set?.size === 0) this.#watchers.delete(jobId);
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* The single write path for a job.
|
|
767
|
+
*
|
|
768
|
+
* Every mutation goes through here so notification cannot be forgotten by
|
|
769
|
+
* a future one. Nine call sites existed when the push seam was added, and
|
|
770
|
+
* "remember to notify" is not a property nine call sites keep.
|
|
771
|
+
*/
|
|
772
|
+
#write(jobId, record) {
|
|
773
|
+
this.#jobs.set(jobId, record);
|
|
774
|
+
this.#notify(jobId);
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Tell anyone watching that a job changed.
|
|
778
|
+
*
|
|
779
|
+
* A throwing watcher must not corrupt the store's own bookkeeping, so each
|
|
780
|
+
* is isolated: this runs inside write paths, and one bad listener taking
|
|
781
|
+
* out an unrelated write would be a far worse failure than a missed
|
|
782
|
+
* notification.
|
|
783
|
+
*/
|
|
784
|
+
#notify(jobId) {
|
|
785
|
+
for (const watcher of this.#watchers.get(jobId) ?? []) {
|
|
786
|
+
try {
|
|
787
|
+
watcher();
|
|
788
|
+
} catch {
|
|
396
789
|
}
|
|
397
790
|
}
|
|
398
791
|
}
|
|
399
792
|
release(args) {
|
|
400
793
|
const released = [];
|
|
401
|
-
for (const jobId of args.
|
|
794
|
+
for (const { jobId, leaseId } of args.leases) {
|
|
402
795
|
const job = this.#jobs.get(jobId);
|
|
403
|
-
if (!job || job.lease?.runnerId !== args.runnerId)
|
|
404
|
-
|
|
796
|
+
if (!job || job.lease?.runnerId !== args.runnerId || job.lease.id !== leaseId) {
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
this.#write(jobId, {
|
|
405
800
|
...job,
|
|
406
801
|
state: "queued",
|
|
407
802
|
lease: null,
|
|
@@ -443,7 +838,7 @@ var MemoryStore = class {
|
|
|
443
838
|
claimableAt: now,
|
|
444
839
|
updatedAt: now
|
|
445
840
|
};
|
|
446
|
-
this.#
|
|
841
|
+
this.#write(job.id, requeued);
|
|
447
842
|
changed.push(requeued);
|
|
448
843
|
}
|
|
449
844
|
}
|
|
@@ -458,7 +853,7 @@ var MemoryStore = class {
|
|
|
458
853
|
lease: null,
|
|
459
854
|
updatedAt: now
|
|
460
855
|
};
|
|
461
|
-
this.#
|
|
856
|
+
this.#write(job.id, expired);
|
|
462
857
|
changed.push(expired);
|
|
463
858
|
}
|
|
464
859
|
return changed;
|
|
@@ -473,7 +868,7 @@ var MemoryStore = class {
|
|
|
473
868
|
lease: null,
|
|
474
869
|
updatedAt: now
|
|
475
870
|
};
|
|
476
|
-
this.#
|
|
871
|
+
this.#write(jobId, canceled);
|
|
477
872
|
return Promise.resolve(canceled);
|
|
478
873
|
}
|
|
479
874
|
if (job.state === "claimed" || job.state === "running") {
|
|
@@ -525,6 +920,9 @@ var MemoryStore = class {
|
|
|
525
920
|
id: args.runnerId,
|
|
526
921
|
owner: args.owner,
|
|
527
922
|
tokenHash: args.tokenHash,
|
|
923
|
+
// Carried from the pairing, not re-supplied at approval: the user
|
|
924
|
+
// approved a specific machine, and the runner must be that machine.
|
|
925
|
+
device: pairing.device,
|
|
528
926
|
label: pairing.label,
|
|
529
927
|
platform: pairing.platform,
|
|
530
928
|
daemonVersion: pairing.daemonVersion,
|
|
@@ -613,22 +1011,26 @@ function capabilityFor(capabilities, kind) {
|
|
|
613
1011
|
export {
|
|
614
1012
|
ByollmApp,
|
|
615
1013
|
ByollmHandlers,
|
|
1014
|
+
CloudLane,
|
|
616
1015
|
MemoryStore,
|
|
617
1016
|
NoRunnerAvailableError,
|
|
618
1017
|
PollingDelivery,
|
|
619
1018
|
ResultTimeoutError,
|
|
620
1019
|
SERVED_PROTOCOL_VERSION,
|
|
621
|
-
bearerFrom,
|
|
622
1020
|
capabilityFor,
|
|
623
1021
|
createFetchHandler,
|
|
1022
|
+
formatSiteKeys,
|
|
624
1023
|
generateDeviceCode,
|
|
625
1024
|
generateJobId,
|
|
626
1025
|
generateRunnerId,
|
|
627
1026
|
generateRunnerToken,
|
|
1027
|
+
generateSiteKeys,
|
|
628
1028
|
generateUserCode,
|
|
629
1029
|
hashSecret,
|
|
630
1030
|
normalizeUserCode,
|
|
631
1031
|
routeEndpoint,
|
|
632
|
-
secretsMatch
|
|
1032
|
+
secretsMatch,
|
|
1033
|
+
signatureFrom,
|
|
1034
|
+
siteKeysFromEnv
|
|
633
1035
|
};
|
|
634
1036
|
//# sourceMappingURL=index.js.map
|