@byollm/server 0.1.0-alpha.5 → 0.1.0-alpha.51
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 +122 -9
- package/dist/{chunk-4NIHWQAT.js → chunk-I4MSFVRA.js} +76 -39
- package/dist/chunk-I4MSFVRA.js.map +1 -0
- package/dist/{chunk-7RKXFPBZ.js → chunk-SAK63KNU.js} +17 -3
- package/dist/chunk-SAK63KNU.js.map +1 -0
- package/dist/{delivery-36nIe-b3.d.ts → delivery-C6VzgMgH.d.ts} +15 -3
- package/dist/{handlers-DgW0QNTf.d.ts → handlers-D1ni1D8r.d.ts} +1 -1
- package/dist/index.d.ts +94 -22
- package/dist/index.js +267 -66
- package/dist/index.js.map +1 -1
- package/dist/next.d.ts +2 -2
- package/dist/next.js +1 -1
- package/dist/{store-Cj5b6A9j.d.ts → store-DoUlJWaO.d.ts} +87 -13
- package/dist/supabase/index.d.ts +2 -2
- package/dist/supabase/index.js +33 -24
- package/dist/supabase/index.js.map +1 -1
- package/package.json +2 -2
- package/supabase/migrations/20260819000000_drop_runner_token.sql +87 -0
- package/supabase/migrations/20260819010000_completed_by_lease_id.sql +25 -0
- package/supabase/migrations/20260821000000_rename_collected.sql +91 -0
- package/supabase/migrations/20260824000000_one_vocabulary.sql +109 -0
- package/supabase/migrations/20260825000000_job_service.sql +20 -0
- package/dist/chunk-4NIHWQAT.js.map +0 -1
- package/dist/chunk-7RKXFPBZ.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -2,23 +2,23 @@ import {
|
|
|
2
2
|
ByollmHandlers,
|
|
3
3
|
SERVED_PROTOCOL_VERSION,
|
|
4
4
|
createFetchHandler,
|
|
5
|
+
deadlineFor,
|
|
5
6
|
generateDeviceCode,
|
|
6
7
|
generateJobId,
|
|
7
8
|
generateLeaseId,
|
|
8
9
|
generateRunnerId,
|
|
9
|
-
generateRunnerToken,
|
|
10
10
|
generateUserCode,
|
|
11
11
|
hashSecret,
|
|
12
12
|
resealForDevice,
|
|
13
13
|
routeEndpoint,
|
|
14
14
|
secretsMatch,
|
|
15
15
|
signatureFrom
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-I4MSFVRA.js";
|
|
17
17
|
import {
|
|
18
18
|
NoRunnerAvailableError,
|
|
19
19
|
PollingDelivery,
|
|
20
20
|
ResultTimeoutError
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-SAK63KNU.js";
|
|
22
22
|
|
|
23
23
|
// src/app.ts
|
|
24
24
|
import {
|
|
@@ -35,12 +35,25 @@ import {
|
|
|
35
35
|
|
|
36
36
|
// src/cloud.ts
|
|
37
37
|
import {
|
|
38
|
-
|
|
38
|
+
PROTOCOL_VERSION,
|
|
39
|
+
SealedOutcome,
|
|
39
40
|
keyId,
|
|
40
41
|
open,
|
|
41
42
|
publicIdentityOf,
|
|
42
|
-
provenanceFor
|
|
43
|
+
provenanceFor,
|
|
44
|
+
signSiteRequest
|
|
43
45
|
} from "@byollm/protocol";
|
|
46
|
+
var RelayUnavailable = class extends Error {
|
|
47
|
+
retryable;
|
|
48
|
+
/** The protocol's own code, when the relay sent one. */
|
|
49
|
+
code;
|
|
50
|
+
constructor(message, retryable, code) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = "RelayUnavailable";
|
|
53
|
+
this.retryable = retryable;
|
|
54
|
+
this.code = code;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
44
57
|
var CloudLane = class {
|
|
45
58
|
#options;
|
|
46
59
|
#store;
|
|
@@ -66,20 +79,64 @@ var CloudLane = class {
|
|
|
66
79
|
id: record.id,
|
|
67
80
|
kind: record.kind,
|
|
68
81
|
owner: record.owner,
|
|
82
|
+
// This site, by its identity key id — Amendment A §A.3. The relay
|
|
83
|
+
// already knows which site it is routing for, so this discloses nothing
|
|
84
|
+
// new to it; what it adds is that the *daemon* can check the stub
|
|
85
|
+
// against the envelope's `senderKeyId` without asking the relay.
|
|
86
|
+
site: keyId(publicIdentityOf(this.#siteKeys).identity),
|
|
69
87
|
audience: record.audience,
|
|
70
|
-
|
|
88
|
+
// `audienceAllow` is deliberately **not** published — cloud_008 §0.2.
|
|
89
|
+
//
|
|
90
|
+
// It is a list of the people who may run this job, and on the direct
|
|
91
|
+
// plane that is unremarkable: the site authored the list and the site is
|
|
92
|
+
// the upstream, so the party receiving it already has it. Through a
|
|
93
|
+
// relay it is a third party, and byollm_009 §6's enumerated metadata —
|
|
94
|
+
// "exhaustive and normative… what an upstream can see, stated as a
|
|
95
|
+
// commitment" — does not include it. It was reaching the relay on every
|
|
96
|
+
// named-audience job.
|
|
97
|
+
//
|
|
98
|
+
// Nothing is lost by withholding it, which is why this is a Tier 0 fix
|
|
99
|
+
// rather than a trade. `matchAudience` treats it as a *narrowing*:
|
|
100
|
+
// `job.audienceAllow !== undefined && !includes(daemon.owner)` refuses,
|
|
101
|
+
// and its absence simply falls through to the checks that actually
|
|
102
|
+
// enforce — the daemon's own allowlist (`NAMED_LOCAL_ALLOWLIST`) and the
|
|
103
|
+
// backend's offer scope. On this lane the relay narrows too, from the
|
|
104
|
+
// control plane's rosters. The enforcement was never here.
|
|
105
|
+
...record.service === void 0 ? {} : { service: record.service },
|
|
71
106
|
sizeClass: record.sizeClass,
|
|
72
107
|
streaming: false,
|
|
73
108
|
// The relay needs *a* deadline to bound routing. A job without one gets
|
|
74
109
|
// the envelope's, which is the outer bound on how long the ciphertext
|
|
75
110
|
// is worth carrying — never longer than the work could possibly matter.
|
|
76
|
-
|
|
111
|
+
// The same fallback the direct plane uses — cloud_008 Tier 4, finding
|
|
112
|
+
// 31. This said `createdAt + ENVELOPE_TTL_FALLBACK`, a local constant
|
|
113
|
+
// whose value happened to equal `ENVELOPE_MAX_AGE_MS`; the direct plane
|
|
114
|
+
// said `(claimableAt ?? now) + ttlMs`. One field, two meanings, and a
|
|
115
|
+
// job that was blocked on a dependency got a deadline measured from
|
|
116
|
+
// when it was *created* on one lane and from when it became *claimable*
|
|
117
|
+
// on the other.
|
|
118
|
+
deadlineAt: deadlineFor(record, this.#now())
|
|
77
119
|
};
|
|
78
|
-
await this.#post("
|
|
120
|
+
await this.#post("enqueue", {
|
|
79
121
|
siteId: this.#options.siteId,
|
|
80
122
|
stub
|
|
81
123
|
});
|
|
82
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Withdraw a job at the relay — cloud_008 §2.2.
|
|
127
|
+
*
|
|
128
|
+
* `app.cancel()` marks the site's own row terminal, which stops the *next*
|
|
129
|
+
* seal. It cannot stop a device that is already running the work, because
|
|
130
|
+
* on this lane the site is not the upstream: only the relay talks to the
|
|
131
|
+
* daemon, and it answered `cancel: []` unconditionally.
|
|
132
|
+
*
|
|
133
|
+
* So the cancellation has to travel. The relay marks the job, stops
|
|
134
|
+
* offering it, and names it to the holding device at its next heartbeat —
|
|
135
|
+
* the same path the direct plane has always had, arriving one hop later.
|
|
136
|
+
*/
|
|
137
|
+
async cancel(jobId) {
|
|
138
|
+
await this.#post("cancel", { siteId: this.#options.siteId, jobId });
|
|
139
|
+
}
|
|
83
140
|
/**
|
|
84
141
|
* One cycle: seal for anything claimed, collect anything finished.
|
|
85
142
|
*
|
|
@@ -92,7 +149,17 @@ var CloudLane = class {
|
|
|
92
149
|
const sealed = [];
|
|
93
150
|
const refused = [];
|
|
94
151
|
const completed = [];
|
|
95
|
-
|
|
152
|
+
try {
|
|
153
|
+
return await this.#cycle(sealed, refused, completed);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error instanceof RelayUnavailable && error.retryable) {
|
|
156
|
+
return { sealed, completed, refused, deferred: error.message };
|
|
157
|
+
}
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async #cycle(sealed, refused, completed) {
|
|
162
|
+
const pending = await this.#get("pending");
|
|
96
163
|
for (const claim of pending.jobs) {
|
|
97
164
|
const record = await this.#store.get(claim.jobId);
|
|
98
165
|
if (!record) continue;
|
|
@@ -109,20 +176,24 @@ var CloudLane = class {
|
|
|
109
176
|
refused.push(claim.jobId);
|
|
110
177
|
continue;
|
|
111
178
|
}
|
|
112
|
-
await this.#store.adopt({
|
|
179
|
+
const adopted = await this.#store.adopt({
|
|
113
180
|
jobId: claim.jobId,
|
|
114
181
|
leaseId: claim.leaseId,
|
|
115
|
-
expiresAt: claim.
|
|
182
|
+
expiresAt: claim.leaseExpiresAt,
|
|
116
183
|
now: this.#now()
|
|
117
184
|
});
|
|
118
|
-
|
|
185
|
+
if (!adopted) {
|
|
186
|
+
refused.push(claim.jobId);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
await this.#post("payload", {
|
|
119
190
|
siteId: this.#options.siteId,
|
|
120
191
|
jobId: claim.jobId,
|
|
121
192
|
envelope: resealed.envelope
|
|
122
193
|
});
|
|
123
194
|
sealed.push(claim.jobId);
|
|
124
195
|
}
|
|
125
|
-
const finished = await this.#get("
|
|
196
|
+
const finished = await this.#get("results");
|
|
126
197
|
for (const done of finished.jobs) {
|
|
127
198
|
const record = await this.#store.get(done.jobId);
|
|
128
199
|
if (!record || record.state === "ok" || record.state === "error") {
|
|
@@ -135,17 +206,30 @@ var CloudLane = class {
|
|
|
135
206
|
}
|
|
136
207
|
await this.#store.complete({
|
|
137
208
|
jobId: done.jobId,
|
|
209
|
+
// The relay named the device; the signature above proved it — §3.6.
|
|
210
|
+
runnerId: done.runnerId,
|
|
138
211
|
// The grant, not the machine: this site never paired with the device
|
|
139
212
|
// that ran it, and the signature it verified above is the stronger
|
|
140
213
|
// claim about who did.
|
|
141
214
|
holder: { by: "lease", leaseId: done.leaseId },
|
|
142
|
-
outcome,
|
|
215
|
+
outcome: outcome.outcome,
|
|
143
216
|
provenance: provenanceFor({
|
|
144
217
|
audience: record.audience,
|
|
145
218
|
runnerId: done.runnerId,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
219
|
+
// The owner, from the relay's own record of who claimed it — not a
|
|
220
|
+
// key id. cloud_008 §2.5: this said `keyId(device.identity)`, which
|
|
221
|
+
// put a key id where the direct plane puts a user id, so an app
|
|
222
|
+
// comparing provenance across lanes compared two namespaces and got
|
|
223
|
+
// `false` for the same person. The device's key is still what the
|
|
224
|
+
// signature was verified against, above; that is a different
|
|
225
|
+
// question from whose machine it is.
|
|
226
|
+
runnerOwner: done.runnerOwner,
|
|
227
|
+
// From the envelope, not invented — cloud_008 §2.5. These were
|
|
228
|
+
// hardcoded `"http"` and `"unknown"` because the daemon's declared
|
|
229
|
+
// values stopped at the relay, which is right: a blind relay acts
|
|
230
|
+
// on neither. Sealing them carries them past it untouched.
|
|
231
|
+
backendClass: outcome.ran.backendClass,
|
|
232
|
+
model: outcome.ran.model
|
|
149
233
|
}),
|
|
150
234
|
now: this.#now()
|
|
151
235
|
});
|
|
@@ -180,29 +264,107 @@ var CloudLane = class {
|
|
|
180
264
|
} catch {
|
|
181
265
|
return null;
|
|
182
266
|
}
|
|
183
|
-
const
|
|
184
|
-
if (!
|
|
185
|
-
if (
|
|
186
|
-
return
|
|
267
|
+
const sealed = SealedOutcome.safeParse(parsed);
|
|
268
|
+
if (!sealed.success) return null;
|
|
269
|
+
if (sealed.data.outcome.outcome !== done.disposition) return null;
|
|
270
|
+
return sealed.data;
|
|
187
271
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
272
|
+
/**
|
|
273
|
+
* Sign a site-plane call with this site's identity key.
|
|
274
|
+
*
|
|
275
|
+
* The same scheme the daemon uses against an upstream, because the site is
|
|
276
|
+
* in the same position: an outbound caller whose key the relay already holds
|
|
277
|
+
* for other reasons. Nothing else authenticates this plane — a relay that
|
|
278
|
+
* took the `siteId` in a body at face value would let anyone enqueue work in
|
|
279
|
+
* a site's name and read who claimed it.
|
|
280
|
+
*/
|
|
281
|
+
#headers(endpoint, rawBody) {
|
|
282
|
+
const signature = signSiteRequest(this.#siteKeys, {
|
|
283
|
+
endpoint,
|
|
284
|
+
siteId: this.#options.siteId,
|
|
285
|
+
issuedAt: this.#now(),
|
|
286
|
+
body: rawBody
|
|
193
287
|
});
|
|
194
|
-
return
|
|
288
|
+
return {
|
|
289
|
+
"x-byollm-site": this.#options.siteId,
|
|
290
|
+
"x-byollm-issued-at": String(signature.issuedAt),
|
|
291
|
+
"x-byollm-signature": signature.signature
|
|
292
|
+
};
|
|
195
293
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
294
|
+
/**
|
|
295
|
+
* A relay answer, checked before it is believed — alpha.31.
|
|
296
|
+
*
|
|
297
|
+
* The bug this closes is one line long and its shape is general: a response
|
|
298
|
+
* body used without looking at the status. The daemon's client has always
|
|
299
|
+
* done this properly (`client.ts` maps every status to a typed refusal); the
|
|
300
|
+
* site's lane parsed JSON and hoped.
|
|
301
|
+
*
|
|
302
|
+
* Two classes, because they need opposite handling. **Retryable** — 503 from
|
|
303
|
+
* a draining pod, 429, 5xx, and the protocol's own `not-ready` — means the
|
|
304
|
+
* work is still there and this cycle should end quietly. **Refused** — a bad
|
|
305
|
+
* signature, an unknown site, a version this relay does not speak — will
|
|
306
|
+
* still be true in five seconds, and swallowing it would leave a site
|
|
307
|
+
* silently disconnected from its own users.
|
|
308
|
+
*/
|
|
309
|
+
async #answer(response, endpoint) {
|
|
310
|
+
if (response.ok) return response.json();
|
|
311
|
+
let code = "";
|
|
312
|
+
let message;
|
|
313
|
+
try {
|
|
314
|
+
const body = await response.json();
|
|
315
|
+
code = body.error ?? "";
|
|
316
|
+
message = body.message ?? "";
|
|
317
|
+
} catch {
|
|
318
|
+
message = `HTTP ${String(response.status)}`;
|
|
319
|
+
}
|
|
320
|
+
const retryable = response.status >= 500 || response.status === 429 || code === "not-ready" || code === "server-error";
|
|
321
|
+
throw new RelayUnavailable(
|
|
322
|
+
`${endpoint}: ${code || "refused"} \u2014 ${message}`,
|
|
323
|
+
retryable,
|
|
324
|
+
code
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
async #post(endpoint, body) {
|
|
328
|
+
const rawBody = JSON.stringify({
|
|
329
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
330
|
+
...body
|
|
331
|
+
});
|
|
332
|
+
const response = await this.#fetch(
|
|
333
|
+
`${this.#options.relayOrigin}/relay/site/${endpoint}`,
|
|
334
|
+
{
|
|
335
|
+
method: "POST",
|
|
336
|
+
headers: {
|
|
337
|
+
"content-type": "application/json",
|
|
338
|
+
...this.#headers(endpoint, rawBody)
|
|
339
|
+
},
|
|
340
|
+
body: rawBody
|
|
341
|
+
}
|
|
342
|
+
);
|
|
343
|
+
return this.#answer(response, endpoint);
|
|
344
|
+
}
|
|
345
|
+
async #get(endpoint) {
|
|
346
|
+
const url = `${this.#options.relayOrigin}/relay/site/${endpoint}?siteId=${encodeURIComponent(this.#options.siteId)}&protocolVersion=${encodeURIComponent(PROTOCOL_VERSION)}`;
|
|
347
|
+
const response = await this.#fetch(url, {
|
|
348
|
+
headers: this.#headers(endpoint, "")
|
|
349
|
+
});
|
|
350
|
+
return this.#answer(response, endpoint);
|
|
200
351
|
}
|
|
201
352
|
};
|
|
202
|
-
var ENVELOPE_TTL_FALLBACK = 24 * 60 * 6e4;
|
|
203
353
|
|
|
204
354
|
// src/app.ts
|
|
205
355
|
var DEFAULT_LIVENESS_MS = 35e3;
|
|
356
|
+
var ENQUEUE_OPTIONS = Object.freeze({
|
|
357
|
+
kind: true,
|
|
358
|
+
payload: true,
|
|
359
|
+
owner: true,
|
|
360
|
+
audience: true,
|
|
361
|
+
service: true,
|
|
362
|
+
audienceAllow: true,
|
|
363
|
+
dependsOn: true,
|
|
364
|
+
ttlMs: true,
|
|
365
|
+
deadlineAt: true,
|
|
366
|
+
id: true
|
|
367
|
+
});
|
|
206
368
|
var ByollmApp = class {
|
|
207
369
|
#store;
|
|
208
370
|
#siteKeys;
|
|
@@ -255,6 +417,14 @@ var ByollmApp = class {
|
|
|
255
417
|
* the app is obliged to disclose that to whoever reads it.
|
|
256
418
|
*/
|
|
257
419
|
async enqueue(input) {
|
|
420
|
+
const unknown = Object.keys(input).filter(
|
|
421
|
+
(key) => !(key in ENQUEUE_OPTIONS)
|
|
422
|
+
);
|
|
423
|
+
if (unknown.length > 0) {
|
|
424
|
+
throw new Error(
|
|
425
|
+
`enqueue does not understand ${unknown.map((k) => `\`${k}\``).join(", ")}. An option this @byollm/server does not know is refused rather than ignored, because an ignored option is a job that runs differently than you asked with nothing to see \u2014 most often an SDK older than the code calling it. Upgrade @byollm/server, or remove the option.`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
258
428
|
const parsed = KindedPayload.safeParse({
|
|
259
429
|
kind: input.kind,
|
|
260
430
|
payload: input.payload
|
|
@@ -314,7 +484,7 @@ var ByollmApp = class {
|
|
|
314
484
|
* Check `provenance.untrusted` before rendering. It is true for every
|
|
315
485
|
* `named`/`public` job, because that text came from someone else's machine
|
|
316
486
|
* and the app must not present it as its own AI's answer
|
|
317
|
-
* ({@link MUSTS.
|
|
487
|
+
* ({@link MUSTS.PROVENANCE_NAMES_DEVICE}).
|
|
318
488
|
*/
|
|
319
489
|
async result(jobId) {
|
|
320
490
|
const job = await this.job(jobId);
|
|
@@ -328,7 +498,11 @@ var ByollmApp = class {
|
|
|
328
498
|
}
|
|
329
499
|
/** Ask a runner to stop. Queued jobs cancel at once; held jobs at the next heartbeat. */
|
|
330
500
|
async cancel(jobId) {
|
|
331
|
-
|
|
501
|
+
const cancelled = await this.#store.cancel(jobId, this.#now());
|
|
502
|
+
if (cancelled && this.cloud) {
|
|
503
|
+
await this.cloud.cancel(jobId).catch(() => void 0);
|
|
504
|
+
}
|
|
505
|
+
return cancelled;
|
|
332
506
|
}
|
|
333
507
|
/**
|
|
334
508
|
* Is there a live runner that could take a job of this shape?
|
|
@@ -350,14 +524,20 @@ var ByollmApp = class {
|
|
|
350
524
|
}
|
|
351
525
|
let capable = 0;
|
|
352
526
|
let admitted = 0;
|
|
527
|
+
let withheldSomewhere = 0;
|
|
528
|
+
let lastRefusal;
|
|
353
529
|
for (const runner of live) {
|
|
354
|
-
const
|
|
530
|
+
const forKind = runner.capabilities.filter((c) => c.kind === query.kind);
|
|
531
|
+
const capability = query.service === void 0 ? forKind.find((c) => c.isDefault) : forKind.find((c) => c.service === query.service);
|
|
532
|
+
if (capability === void 0 && query.service === void 0 && forKind.length > 0) {
|
|
533
|
+
withheldSomewhere += 1;
|
|
534
|
+
}
|
|
355
535
|
if (!capability) continue;
|
|
356
536
|
capable += 1;
|
|
357
537
|
const match = matchAudience(
|
|
358
538
|
{
|
|
359
539
|
owner: query.owner,
|
|
360
|
-
audience: query.audience ?? "
|
|
540
|
+
audience: query.audience ?? "private",
|
|
361
541
|
audienceAllow: query.audienceAllow
|
|
362
542
|
},
|
|
363
543
|
{
|
|
@@ -381,18 +561,24 @@ var ByollmApp = class {
|
|
|
381
561
|
}
|
|
382
562
|
);
|
|
383
563
|
if (match.ok) admitted += 1;
|
|
564
|
+
else lastRefusal = match.refusal;
|
|
384
565
|
}
|
|
385
566
|
if (capable === 0) {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
reason: "no-matching-capability",
|
|
389
|
-
candidates: 0
|
|
390
|
-
};
|
|
567
|
+
const reason = query.service !== void 0 ? "selection-unavailable" : withheldSomewhere > 0 ? "awaiting-default" : "no-matching-capability";
|
|
568
|
+
return { available: false, reason, candidates: 0 };
|
|
391
569
|
}
|
|
392
570
|
if (admitted === 0) {
|
|
571
|
+
const ownersDoing = lastRefusal === "offer-scope-too-narrow" || lastRefusal === "subscription-self-lock" || lastRefusal === "metered-no-spend-consent" || lastRefusal === "metered-ceiling-reached";
|
|
572
|
+
if (query.service !== void 0) {
|
|
573
|
+
return {
|
|
574
|
+
available: false,
|
|
575
|
+
reason: "selection-unavailable",
|
|
576
|
+
candidates: 0
|
|
577
|
+
};
|
|
578
|
+
}
|
|
393
579
|
return {
|
|
394
580
|
available: false,
|
|
395
|
-
reason: "audience-admits-nobody",
|
|
581
|
+
reason: ownersDoing ? "default-unusable" : "audience-admits-nobody",
|
|
396
582
|
candidates: 0
|
|
397
583
|
};
|
|
398
584
|
}
|
|
@@ -406,13 +592,10 @@ var ByollmApp = class {
|
|
|
406
592
|
* ({@link MUSTS.PAIR_ONE_USER}, {@link MUSTS.PAIR_INTERACTIVE}).
|
|
407
593
|
*/
|
|
408
594
|
async approvePairing(args) {
|
|
409
|
-
const token = generateRunnerToken();
|
|
410
595
|
return this.#store.approvePairing({
|
|
411
596
|
userCode: normalizeUserCode(args.userCode),
|
|
412
597
|
owner: args.owner,
|
|
413
598
|
runnerId: generateRunnerId(),
|
|
414
|
-
runnerToken: token,
|
|
415
|
-
tokenHash: hashSecret(token),
|
|
416
599
|
now: this.#now()
|
|
417
600
|
});
|
|
418
601
|
}
|
|
@@ -485,13 +668,27 @@ function siteKeysFromEnv(variable = "BYOLLM_SITE_KEYS", env = process.env) {
|
|
|
485
668
|
}
|
|
486
669
|
function formatSiteKeys(keys) {
|
|
487
670
|
const encoded = Buffer.from(JSON.stringify(keys)).toString("base64");
|
|
488
|
-
|
|
489
|
-
|
|
671
|
+
const pub = publicIdentityOf3(keys);
|
|
672
|
+
return `# \u2500\u2500 1. SECRET \u2014 set this on your server, and nowhere else \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
673
|
+
#
|
|
674
|
+
# This is the site's identity. Anything holding it can *be* this site,
|
|
675
|
+
# so it goes wherever your deployment keeps secrets \u2014 never in a repo,
|
|
676
|
+
# never in a browser, never pasted into a dashboard.
|
|
490
677
|
BYOLLM_SITE_KEYS=${encoded}
|
|
491
678
|
|
|
492
|
-
#
|
|
493
|
-
#
|
|
494
|
-
#
|
|
679
|
+
# \u2500\u2500 2. PUBLIC \u2014 paste this line into the byollm dashboard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
680
|
+
#
|
|
681
|
+
# The public half. It proves signatures and seals nothing, so it is
|
|
682
|
+
# safe to publish \u2014 which is the point: users pin it, and the relay
|
|
683
|
+
# cannot forge work without the secret above.
|
|
684
|
+
${JSON.stringify(pub)}
|
|
685
|
+
|
|
686
|
+
# \u2500\u2500 3. Fingerprint \u2014 what a person compares by eye \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
687
|
+
#
|
|
688
|
+
# A fingerprint is not secret. Show it on your site so somebody
|
|
689
|
+
# connecting can check it against what their daemon printed.
|
|
690
|
+
# The dashboard derives this itself, so there is nothing to paste.
|
|
691
|
+
# ${fingerprint(pub.identity)}
|
|
495
692
|
`;
|
|
496
693
|
}
|
|
497
694
|
|
|
@@ -525,12 +722,14 @@ var MemoryStore = class {
|
|
|
525
722
|
kind: input.kind,
|
|
526
723
|
envelope: input.envelope,
|
|
527
724
|
sizeClass: input.sizeClass,
|
|
528
|
-
audience: input.audience ?? "
|
|
725
|
+
audience: input.audience ?? "private",
|
|
726
|
+
service: input.service,
|
|
529
727
|
owner: input.owner,
|
|
530
728
|
audienceAllow: input.audienceAllow ? [...input.audienceAllow] : void 0,
|
|
531
729
|
dependsOn,
|
|
532
730
|
state: "queued",
|
|
533
731
|
lease: null,
|
|
732
|
+
completedByLeaseId: null,
|
|
534
733
|
createdAt: now,
|
|
535
734
|
// The TTL clock starts here only if nothing blocks the job.
|
|
536
735
|
claimableAt: blocked ? null : now,
|
|
@@ -620,11 +819,11 @@ var MemoryStore = class {
|
|
|
620
819
|
for (const { jobId, leaseId } of args.leases) {
|
|
621
820
|
const job = this.#jobs.get(jobId);
|
|
622
821
|
if (!job || job.lease?.runnerId !== args.runnerId || job.lease.id !== leaseId) {
|
|
623
|
-
lost.push(jobId);
|
|
822
|
+
lost.push({ jobId, leaseId });
|
|
624
823
|
continue;
|
|
625
824
|
}
|
|
626
825
|
if (job.state !== "claimed" && job.state !== "running") {
|
|
627
|
-
lost.push(jobId);
|
|
826
|
+
lost.push({ jobId, leaseId });
|
|
628
827
|
continue;
|
|
629
828
|
}
|
|
630
829
|
const expiresAt = args.now + args.leaseMs;
|
|
@@ -666,6 +865,11 @@ var MemoryStore = class {
|
|
|
666
865
|
const job = this.#jobs.get(args.jobId);
|
|
667
866
|
if (!job) return Promise.resolve({ accepted: false, job: null });
|
|
668
867
|
if (job.state === "ok" || job.state === "error" || job.state === "canceled") {
|
|
868
|
+
const sameDevice = job.provenance?.runnerId !== void 0 && job.provenance.runnerId === args.runnerId;
|
|
869
|
+
const sameGrant = args.holder.by === "lease" && job.completedByLeaseId !== null && job.completedByLeaseId === args.holder.leaseId;
|
|
870
|
+
if (sameDevice && sameGrant) {
|
|
871
|
+
return Promise.resolve({ accepted: false, duplicate: true, job });
|
|
872
|
+
}
|
|
669
873
|
return Promise.resolve({ accepted: false, job });
|
|
670
874
|
}
|
|
671
875
|
if (job.state === "expired") {
|
|
@@ -680,6 +884,8 @@ var MemoryStore = class {
|
|
|
680
884
|
...job,
|
|
681
885
|
state,
|
|
682
886
|
lease: null,
|
|
887
|
+
// The grant that recorded it, kept after the lease is dropped — §3.6.
|
|
888
|
+
completedByLeaseId: args.holder.by === "lease" ? args.holder.leaseId : job.lease?.id ?? null,
|
|
683
889
|
outcome: args.outcome,
|
|
684
890
|
provenance: args.provenance,
|
|
685
891
|
updatedAt: args.now
|
|
@@ -768,6 +974,7 @@ var MemoryStore = class {
|
|
|
768
974
|
...job,
|
|
769
975
|
state: "queued",
|
|
770
976
|
lease: null,
|
|
977
|
+
completedByLeaseId: null,
|
|
771
978
|
// Newly available again, so the TTL clock restarts here too.
|
|
772
979
|
claimableAt: args.now,
|
|
773
980
|
// A refusal is remembered, or the pair spins between claim and
|
|
@@ -797,6 +1004,7 @@ var MemoryStore = class {
|
|
|
797
1004
|
...job,
|
|
798
1005
|
state: "queued",
|
|
799
1006
|
lease: null,
|
|
1007
|
+
completedByLeaseId: null,
|
|
800
1008
|
// The TTL clock restarts: it measures how long a job has waited
|
|
801
1009
|
// *unclaimed*, and this job has just become available again. Without
|
|
802
1010
|
// this, a job whose runner died would expire for time it spent being
|
|
@@ -819,6 +1027,7 @@ var MemoryStore = class {
|
|
|
819
1027
|
...job,
|
|
820
1028
|
state: "expired",
|
|
821
1029
|
lease: null,
|
|
1030
|
+
completedByLeaseId: null,
|
|
822
1031
|
updatedAt: now
|
|
823
1032
|
};
|
|
824
1033
|
this.#write(job.id, expired);
|
|
@@ -834,6 +1043,7 @@ var MemoryStore = class {
|
|
|
834
1043
|
...job,
|
|
835
1044
|
state: "canceled",
|
|
836
1045
|
lease: null,
|
|
1046
|
+
completedByLeaseId: null,
|
|
837
1047
|
updatedAt: now
|
|
838
1048
|
};
|
|
839
1049
|
this.#write(jobId, canceled);
|
|
@@ -854,9 +1064,7 @@ var MemoryStore = class {
|
|
|
854
1064
|
}
|
|
855
1065
|
listCancelRequests(runnerId) {
|
|
856
1066
|
return Promise.resolve(
|
|
857
|
-
[...this.#cancelRequests].filter(
|
|
858
|
-
(jobId) => this.#jobs.get(jobId)?.lease?.runnerId === runnerId
|
|
859
|
-
)
|
|
1067
|
+
[...this.#cancelRequests].map((jobId) => ({ jobId, lease: this.#jobs.get(jobId)?.lease })).filter((row) => row.lease?.runnerId === runnerId).map((row) => ({ jobId: row.jobId, leaseId: row.lease?.id ?? "" }))
|
|
860
1068
|
);
|
|
861
1069
|
}
|
|
862
1070
|
// -- pairing and runners -------------------------------------------------
|
|
@@ -887,7 +1095,6 @@ var MemoryStore = class {
|
|
|
887
1095
|
const runner = {
|
|
888
1096
|
id: args.runnerId,
|
|
889
1097
|
owner: args.owner,
|
|
890
|
-
tokenHash: args.tokenHash,
|
|
891
1098
|
// Carried from the pairing, not re-supplied at approval: the user
|
|
892
1099
|
// approved a specific machine, and the runner must be that machine.
|
|
893
1100
|
device: pairing.device,
|
|
@@ -906,7 +1113,7 @@ var MemoryStore = class {
|
|
|
906
1113
|
state: "approved",
|
|
907
1114
|
owner: args.owner,
|
|
908
1115
|
runnerId: runner.id,
|
|
909
|
-
|
|
1116
|
+
collected: false
|
|
910
1117
|
});
|
|
911
1118
|
return Promise.resolve(runner);
|
|
912
1119
|
}
|
|
@@ -927,17 +1134,11 @@ var MemoryStore = class {
|
|
|
927
1134
|
if (pairing) {
|
|
928
1135
|
this.#pairings.set(deviceCodeHash, {
|
|
929
1136
|
...pairing,
|
|
930
|
-
|
|
1137
|
+
collected: true
|
|
931
1138
|
});
|
|
932
1139
|
}
|
|
933
1140
|
return Promise.resolve();
|
|
934
1141
|
}
|
|
935
|
-
getRunnerByTokenHash(hash) {
|
|
936
|
-
for (const runner of this.#runners.values()) {
|
|
937
|
-
if (runner.tokenHash === hash) return Promise.resolve(runner);
|
|
938
|
-
}
|
|
939
|
-
return Promise.resolve(null);
|
|
940
|
-
}
|
|
941
1142
|
getRunner(runnerId) {
|
|
942
1143
|
return Promise.resolve(this.#runners.get(runnerId) ?? null);
|
|
943
1144
|
}
|
|
@@ -983,6 +1184,7 @@ export {
|
|
|
983
1184
|
MemoryStore,
|
|
984
1185
|
NoRunnerAvailableError,
|
|
985
1186
|
PollingDelivery,
|
|
1187
|
+
RelayUnavailable,
|
|
986
1188
|
ResultTimeoutError,
|
|
987
1189
|
SERVED_PROTOCOL_VERSION,
|
|
988
1190
|
capabilityFor,
|
|
@@ -991,7 +1193,6 @@ export {
|
|
|
991
1193
|
generateDeviceCode,
|
|
992
1194
|
generateJobId,
|
|
993
1195
|
generateRunnerId,
|
|
994
|
-
generateRunnerToken,
|
|
995
1196
|
generateSiteKeys,
|
|
996
1197
|
generateUserCode,
|
|
997
1198
|
hashSecret,
|