@byollm/server 0.1.0-alpha.9 → 0.1.0-alpha.91
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 +180 -9
- package/dist/{chunk-4NIHWQAT.js → chunk-36Y77FUD.js} +106 -50
- package/dist/chunk-36Y77FUD.js.map +1 -0
- package/dist/{chunk-7RKXFPBZ.js → chunk-I3ER27QG.js} +19 -5
- package/dist/chunk-I3ER27QG.js.map +1 -0
- package/dist/{delivery-36nIe-b3.d.ts → delivery-CaGbp0Tc.d.ts} +35 -5
- package/dist/{handlers-DgW0QNTf.d.ts → handlers-CTV3Jc6Q.d.ts} +2 -2
- package/dist/index.d.ts +92 -24
- package/dist/index.js +317 -104
- 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-Cx2_bck1.d.ts} +130 -18
- package/dist/supabase/index.d.ts +2 -2
- package/dist/supabase/index.js +62 -29
- 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/supabase/migrations/20260827000000_job_purpose.sql +36 -0
- package/dist/chunk-4NIHWQAT.js.map +0 -1
- package/dist/chunk-7RKXFPBZ.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
> [!WARNING]
|
|
2
|
-
> **Alpha (`0.1.0-alpha.
|
|
2
|
+
> **Alpha (`0.1.0-alpha.91`) — under active development. Don't use this yet.**
|
|
3
3
|
>
|
|
4
4
|
> Install it deliberately: `npm install @byollm/server@alpha`.
|
|
5
5
|
>
|
|
@@ -11,7 +11,67 @@
|
|
|
11
11
|
> npm assigns `latest` on a first publish and won't let it be removed, so a
|
|
12
12
|
> bare install resolves here too. This notice is the only guard — deliberately
|
|
13
13
|
> not an npm deprecation, which would read as *abandoned* rather than *early*.
|
|
14
|
-
> Ask for `@alpha` explicitly so your lockfile records that you meant to
|
|
14
|
+
> Ask for `@alpha` explicitly so your lockfile records that you meant to.>
|
|
15
|
+
> **`alpha.15` is a breaking wire change, and it breaks daemons and relays —
|
|
16
|
+
> not app authors.** If you call `app.enqueue(...)` and read results, nothing
|
|
17
|
+
> in your code changes. If you run a daemon or an upstream, every package must
|
|
18
|
+
> move together: a mixed pair refuses on both sides, because both ends parse
|
|
19
|
+
> `.strict()`.
|
|
20
|
+
>
|
|
21
|
+
> What moved, all of it reconciling the frozen `byollm_009` with its code:
|
|
22
|
+
> `JobStub` gains `site` (the site's identity key id) and loses
|
|
23
|
+
> `audienceAllow`; `ResultRequest` gains `leaseId`; `HeartbeatResponse` loses
|
|
24
|
+
> `leases`, which nothing read; `WireErrorCode` gains `not-ready`,
|
|
25
|
+
> `clock-skew` and `forbidden`, and `403` is `forbidden` rather than
|
|
26
|
+
> `unauthorized`. `RESULT_PROVENANCE` is superseded by
|
|
27
|
+
> `PROVENANCE_NAMES_DEVICE`. See `byollm_009` Amendment A.>
|
|
28
|
+
> **`alpha.16` is a breaking wire change — daemons and relays again, not app
|
|
29
|
+
> authors.** `app.enqueue(...)` and reading results are unchanged. All five
|
|
30
|
+
> packages move together: both ends parse `.strict()`, so a mixed pair
|
|
31
|
+
> refuses.
|
|
32
|
+
>
|
|
33
|
+
> What moved, all of it Tier 2 of `cloud_008`: `model`, `backendClass` and
|
|
34
|
+
> `durationMs` come off `ResultRequest` and are sealed **inside** the result
|
|
35
|
+
> envelope as `SealedOutcome = { outcome, ran }` — so a daemon can no longer
|
|
36
|
+
> declare a model it did not sign, and a relay carries neither.
|
|
37
|
+
> `HeartbeatResponse` loses `leases` (nothing read it) and now reports real
|
|
38
|
+
> cancellations instead of an empty list. `WireErrorCode` gains `forbidden`
|
|
39
|
+
> for 403, leaving `unauthorized` at exactly 401. The relay gained a
|
|
40
|
+
> site-plane `cancel` endpoint, honours `stub.deadlineAt`, honours
|
|
41
|
+
> `stub.audience`, and remembers a refusal.>
|
|
42
|
+
> **`alpha.17` is additive** — no wire change. It exports `ReleaseReason`,
|
|
43
|
+
> which `RoutingStore.releaseLeases` names and the package did not export, so
|
|
44
|
+
> the interface was unimplementable outside this repo.>
|
|
45
|
+
> **`alpha.18` is a breaking wire change — daemons and relays, not app
|
|
46
|
+
> authors.** `app.enqueue(...)` and reading results are unchanged. All five
|
|
47
|
+
> packages move together.
|
|
48
|
+
>
|
|
49
|
+
> The **bearer token is gone**: off `PairPollResponse`, off the runner row,
|
|
50
|
+
> off the daemon's pairings file, out of the adapter's schema. It was minted,
|
|
51
|
+
> hashed and stored on two disks and never sent, looked up or compared —
|
|
52
|
+
> `REQUESTS_SIGNED_NOT_BEARER` was enforced by signatures the whole time. If
|
|
53
|
+
> you run the Supabase adapter, apply
|
|
54
|
+
> `20260819000000_drop_runner_token.sql`; `byollm_approve_pairing` now takes
|
|
55
|
+
> one argument. A pairings file written by an older daemon still loads.
|
|
56
|
+
>
|
|
57
|
+
> `model`, `backendClass` and `durationMs` moved **inside** the sealed result
|
|
58
|
+
> (`SealedOutcome = { outcome, ran }`), so a daemon cannot declare a model it
|
|
59
|
+
> did not sign and a relay carries none of them. Writing a `RoutingStore`?
|
|
60
|
+
> `releaseLeases` takes an optional `reason` and `complete` requires
|
|
61
|
+
> `leaseId`, and **an implementation that ignores either still typechecks** —
|
|
62
|
+
> run the store contract tests.>
|
|
63
|
+
> **`alpha.19` is additive on the wire and a behaviour change in every
|
|
64
|
+
> store.** `ResultResponse` gains an optional `duplicate`. Nothing is removed,
|
|
65
|
+
> so an older daemon keeps working — but the *order* two rules are checked in
|
|
66
|
+
> has changed, and a `RoutingStore` implementation must change with it.
|
|
67
|
+
>
|
|
68
|
+
> `complete` now checks **terminal state before holder**, scoped to the device
|
|
69
|
+
> that finished the job: a replay from that device is answered `duplicate:
|
|
70
|
+
> true` with a 2xx, and anyone else gets exactly the refusal they would get
|
|
71
|
+
> for a job that is not terminal. Previously `RESULT_IDEMPOTENT` held only
|
|
72
|
+
> because the lease is nulled on success, so the holder check tripped first —
|
|
73
|
+
> deleting the idempotency branch failed no test. Run the store contract
|
|
74
|
+
> tests; the compiler cannot see this.
|
|
15
75
|
>
|
|
16
76
|
> **`alpha.7` breaks nobody who is not implementing a relay.** `@byollm/relay`'s
|
|
17
77
|
> projection gains `devices` — the device keys a control plane has approved —
|
|
@@ -39,6 +99,48 @@
|
|
|
39
99
|
> Your store adapter is unaffected: payloads and results are sealed before they
|
|
40
100
|
> reach it, and `JobStore` did not change.
|
|
41
101
|
|
|
102
|
+
<!-- release-note 0.1.0-alpha.21 -->
|
|
103
|
+
> [!NOTE]
|
|
104
|
+
> **`0.1.0-alpha.20` is not a complete release — do not pin it.** Four
|
|
105
|
+
> packages published and `@byollm/server` did not: a Sigstore
|
|
106
|
+
> transparency-log 409 on its provenance attestation. The workflow's
|
|
107
|
+
> "already published" guard correctly refuses to resume a partial publish,
|
|
108
|
+
> so `0.1.0-alpha.21` is that release, whole.
|
|
109
|
+
>
|
|
110
|
+
> If you run the Supabase adapter, `alpha.21` needs
|
|
111
|
+
> `20260819010000_completed_by_lease_id.sql`: alpha.19 shipped §3.6's
|
|
112
|
+
> ordering without the column it stores the grant in.
|
|
113
|
+
|
|
114
|
+
<!-- release-note 0.1.0-alpha.40 -->
|
|
115
|
+
**`byollm start` — stop keeping a terminal open.** The daemon can now run
|
|
116
|
+
under your computer's own supervisor and restart itself if it stops: a launchd
|
|
117
|
+
agent on macOS, a `systemd --user` unit on Linux, a logon task on Windows. All
|
|
118
|
+
user-level — no root, no system directories, and `byollm stop` takes it
|
|
119
|
+
away. `byollm status` gained a line saying whether it is actually supervised
|
|
120
|
+
right now, including the state that matters most: installed but not running,
|
|
121
|
+
which looks fine from an app's dashboard and serves nothing.
|
|
122
|
+
|
|
123
|
+
If you are running via `npx`, install properly first (`npm install -g
|
|
124
|
+
byollm@latest`) — `install` refuses to supervise a copy in npx's cache, because
|
|
125
|
+
npm deletes that directory and the service would fail at some later boot.
|
|
126
|
+
|
|
127
|
+
<!-- release-note 0.1.0-alpha.41 -->
|
|
128
|
+
**`onNoRunner` takes a string.** Your fallback answer is your own value, not
|
|
129
|
+
wire data, and handing back a whole result record for it was ceremony — the
|
|
130
|
+
README's own example got the shape wrong, which is how this was found.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const { outcome, fallback } = await job.result({
|
|
134
|
+
onNoRunner: () => runOnHostedModel(transcript),
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Whatever you return, `result()` labels it `fallback: true` — the stamp is
|
|
139
|
+
applied by the wait, not taken from you, so an answer that did not run on
|
|
140
|
+
somebody's device cannot be reported as though it did (`FALLBACK_LABELED`).
|
|
141
|
+
Both delivery channels do it, polling and Supabase Realtime. Records still
|
|
142
|
+
work; they just get labelled too.
|
|
143
|
+
|
|
42
144
|
# `@byollm/server`
|
|
43
145
|
|
|
44
146
|
What app developers drop into their backend: framework-agnostic protocol
|
|
@@ -109,7 +211,7 @@ npx @byollm/server@alpha keygen # prints BYOLLM_SITE_KEYS=...
|
|
|
109
211
|
```
|
|
110
212
|
|
|
111
213
|
Once, not per deploy and never at startup — a daemon pins this identity when
|
|
112
|
-
its owner approves the pairing, and regenerating it means every paired
|
|
214
|
+
its owner approves the pairing, and regenerating it means every paired device
|
|
113
215
|
must pair again. Generating at startup fails only under horizontal scale: each
|
|
114
216
|
instance would have a different identity, and a daemon would be refused by
|
|
115
217
|
whichever one it did not pair with.
|
|
@@ -119,21 +221,76 @@ whichever one it did not pair with.
|
|
|
119
221
|
```ts
|
|
120
222
|
const job = await getApp().enqueue({
|
|
121
223
|
kind: "llm.generate",
|
|
122
|
-
audience: "
|
|
123
|
-
owner:
|
|
224
|
+
audience: "private", // this user's own device only — the default
|
|
225
|
+
owner: ownerId, // who this is depends on your mode — see below
|
|
124
226
|
payload: { prompt: `Summarize this transcript:\n\n${transcript}` },
|
|
125
227
|
});
|
|
126
228
|
|
|
127
|
-
const { outcome } = await job.result({
|
|
229
|
+
const { outcome, fallback } = await job.result({
|
|
128
230
|
timeoutMs: 120_000,
|
|
231
|
+
// A string is enough — it is your own fallback answer, not wire data.
|
|
129
232
|
onNoRunner: () => runOnHostedModel(transcript),
|
|
130
233
|
});
|
|
234
|
+
|
|
235
|
+
// `fallback` is true when nobody's device ran it and this came from your
|
|
236
|
+
// own substitute. Say so wherever you show the answer: work that did not run
|
|
237
|
+
// on the user's compute must not be presented as though it did.
|
|
131
238
|
```
|
|
132
239
|
|
|
133
240
|
`result()` is sugar over your delivery channel with a timeout and a
|
|
134
241
|
`noRunnerAvailable` path — never a bare promise that hangs forever. If nobody
|
|
135
242
|
is online to run the job, you find out and can fall back.
|
|
136
243
|
|
|
244
|
+
## Who `owner` is — this differs by mode
|
|
245
|
+
|
|
246
|
+
`owner` names the person whose devices should do the work, and the two
|
|
247
|
+
connection modes do not use the same names for people. Getting this wrong is
|
|
248
|
+
the one integration mistake that produces no error: the job enqueues, returns
|
|
249
|
+
an id, and never routes.
|
|
250
|
+
|
|
251
|
+
**Direct** — daemons reach your own handlers, and you are the only party who
|
|
252
|
+
knows who anybody is. `owner` is your own user id, from your session, never
|
|
253
|
+
from the client. That is what the example above shows.
|
|
254
|
+
|
|
255
|
+
**Cloud** — daemons reach `hub.byollm.cloud`, which has its own identity
|
|
256
|
+
space: rosters, consents and budgets all speak **BYOLLM ids**. Your user id
|
|
257
|
+
means nothing there. `owner` must be the person's BYOLLM id.
|
|
258
|
+
|
|
259
|
+
Ask them for it. Every signed-in person can read it on their byollm.cloud
|
|
260
|
+
account page under **Your BYOLLM id**, with a copy button. Add a settings
|
|
261
|
+
field, have them paste it, store it against your own user record.
|
|
262
|
+
|
|
263
|
+
The id names them and authorises nothing — someone holding it can address work
|
|
264
|
+
to a person and cannot deliver it, because the consent row is what opens the
|
|
265
|
+
route. So a pasted id is not a credential you are being trusted with, and a
|
|
266
|
+
wrong one is harmless. Two things still matter: it must come from the person's
|
|
267
|
+
own account rather than being inferred by your app, and they must connect your
|
|
268
|
+
site on byollm.cloud before anything routes.
|
|
269
|
+
|
|
270
|
+
**A wrong id is silence, not an error.** The relay routes on a consented
|
|
271
|
+
`(site, owner)` pair, so a mistyped id — or one belonging to somebody who has
|
|
272
|
+
not connected your site — matches no route. The job waits, then expires. So
|
|
273
|
+
check ids when you receive them rather than when you enqueue:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
const { available } = await getApp().runnerAvailability({
|
|
277
|
+
kind: "llm.generate",
|
|
278
|
+
owner: pastedByollmId,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
if (!available) {
|
|
282
|
+
// Existence-neutral, deliberately.
|
|
283
|
+
return "That id has no devices for you. Check it, or connect this site on byollm.cloud.";
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Keep that message vague on purpose. A typo'd id and an id belonging to somebody
|
|
288
|
+
who has not connected you give the same answer, and that is the system working
|
|
289
|
+
rather than a limitation to route around — telling them apart would make this
|
|
290
|
+
call an account-existence oracle: probe a guess, sort the answers, enumerate.
|
|
291
|
+
Never write "no such account". If you ever get an answer that *does*
|
|
292
|
+
distinguish them, that is a bug worth reporting.
|
|
293
|
+
|
|
137
294
|
## The approval page
|
|
138
295
|
|
|
139
296
|
Pairing is a device-code exchange, so you need one page where a signed-in user
|
|
@@ -147,7 +304,7 @@ const runner = await getApp().approvePairing({
|
|
|
147
304
|
});
|
|
148
305
|
```
|
|
149
306
|
|
|
150
|
-
`app.pendingPairing(code)` tells you what they are about to approve —
|
|
307
|
+
`app.pendingPairing(code)` tells you what they are about to approve — device
|
|
151
308
|
label, platform, and which models it is offering — so the page can show it.
|
|
152
309
|
|
|
153
310
|
## Stores
|
|
@@ -185,7 +342,7 @@ everything the browser does.
|
|
|
185
342
|
## Two things the API makes you confront
|
|
186
343
|
|
|
187
344
|
**Community results are untrusted.** A `named`/`public` result came from
|
|
188
|
-
someone else's
|
|
345
|
+
someone else's device and can be anything. Every result carries provenance,
|
|
189
346
|
and `untrusted` is derived from the audience — you cannot mark volunteer
|
|
190
347
|
output as first-party:
|
|
191
348
|
|
|
@@ -200,7 +357,7 @@ if (provenance?.untrusted) {
|
|
|
200
357
|
**Jobs can depend on each other.** `dependsOn: [jobId]` keeps a job
|
|
201
358
|
unclaimable until its dependencies are `ok`. One field and one claim
|
|
202
359
|
predicate, not a DAG engine — so the two halves of a piece of work can land on
|
|
203
|
-
two different people's
|
|
360
|
+
two different people's devices, in order, without your app orchestrating the
|
|
204
361
|
wait.
|
|
205
362
|
|
|
206
363
|
## Certifying an adapter
|
|
@@ -213,3 +370,17 @@ A server is byollm-compatible when the kit passes. See
|
|
|
213
370
|
[`@byollm/conformance`](../conformance).
|
|
214
371
|
|
|
215
372
|
MIT
|
|
373
|
+
|
|
374
|
+
<!-- family:start -->
|
|
375
|
+
|
|
376
|
+
## The rest of byollm
|
|
377
|
+
|
|
378
|
+
Six packages, and they are only interesting together:
|
|
379
|
+
|
|
380
|
+
- [`byollm`](https://www.npmjs.com/package/byollm) — the daemon — runs models on your own machine and answers for it
|
|
381
|
+
- [`@byollm/protocol`](https://www.npmjs.com/package/@byollm/protocol) — the wire: envelopes, signatures and the closed vocabularies both ends validate against
|
|
382
|
+
- [`@byollm/relay`](https://www.npmjs.com/package/@byollm/relay) — the broker that holds jobs between a site and a device, and can read neither
|
|
383
|
+
- [`@byollm/control-plane`](https://www.npmjs.com/package/@byollm/control-plane) — who may ask whom, and the policy store behind it
|
|
384
|
+
- [`@byollm/conformance`](https://www.npmjs.com/package/@byollm/conformance) — the kit that proves an implementation is one — including a posture audit that holds nothing but a URL
|
|
385
|
+
|
|
386
|
+
<!-- family:end -->
|
|
@@ -9,9 +9,6 @@ var USER_CODE_ALPHABET = "ABCDEFGHJKMNPQRTWXYZ2346789";
|
|
|
9
9
|
function generateDeviceCode() {
|
|
10
10
|
return randomBytes(32).toString("base64url");
|
|
11
11
|
}
|
|
12
|
-
function generateRunnerToken() {
|
|
13
|
-
return randomBytes(32).toString("base64url");
|
|
14
|
-
}
|
|
15
12
|
function generateRunnerId() {
|
|
16
13
|
return `runner_${randomUUID()}`;
|
|
17
14
|
}
|
|
@@ -41,10 +38,31 @@ function secretsMatch(aHex, bHex) {
|
|
|
41
38
|
}
|
|
42
39
|
var generateLeaseId = () => randomUUID();
|
|
43
40
|
|
|
41
|
+
// src/sealed-outcome.ts
|
|
42
|
+
import { SealedOutcome } from "@byollm/protocol";
|
|
43
|
+
function openSealedOutcome(input) {
|
|
44
|
+
let parsed;
|
|
45
|
+
try {
|
|
46
|
+
parsed = JSON.parse(input.plaintext);
|
|
47
|
+
} catch {
|
|
48
|
+
return { ok: false, why: "the sealed result was not valid JSON" };
|
|
49
|
+
}
|
|
50
|
+
const sealed = SealedOutcome.safeParse(parsed);
|
|
51
|
+
if (!sealed.success) {
|
|
52
|
+
return { ok: false, why: "the sealed result was not an outcome" };
|
|
53
|
+
}
|
|
54
|
+
if (sealed.data.outcome.outcome !== input.disposition) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
why: "the declared disposition is not the one that was sealed"
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return { ok: true, value: sealed.data };
|
|
61
|
+
}
|
|
62
|
+
|
|
44
63
|
// src/handlers.ts
|
|
45
64
|
import {
|
|
46
65
|
FetchRequest,
|
|
47
|
-
JobOutcome,
|
|
48
66
|
keyId as keyId2,
|
|
49
67
|
open as open2,
|
|
50
68
|
publicIdentityOf as publicIdentityOf2,
|
|
@@ -102,6 +120,11 @@ async function resealForDevice(input) {
|
|
|
102
120
|
return { ok: true, envelope };
|
|
103
121
|
}
|
|
104
122
|
|
|
123
|
+
// src/records.ts
|
|
124
|
+
function deadlineFor(job, now) {
|
|
125
|
+
return job.deadlineAt ?? (job.claimableAt ?? now) + job.ttlMs;
|
|
126
|
+
}
|
|
127
|
+
|
|
105
128
|
// src/handlers.ts
|
|
106
129
|
var DEFAULTS = {
|
|
107
130
|
leaseMs: 6e4,
|
|
@@ -130,6 +153,8 @@ var ByollmHandlers = class {
|
|
|
130
153
|
#pollIntervalMs;
|
|
131
154
|
#now;
|
|
132
155
|
#siteKeys;
|
|
156
|
+
/** This site's identity key id — Amendment A's `stub.site`. Derived once. */
|
|
157
|
+
#siteKeyId;
|
|
133
158
|
constructor(config) {
|
|
134
159
|
this.#store = config.store;
|
|
135
160
|
if (!verifyPublicIdentity(publicIdentityOf2(config.siteKeys))) {
|
|
@@ -138,6 +163,7 @@ var ByollmHandlers = class {
|
|
|
138
163
|
);
|
|
139
164
|
}
|
|
140
165
|
this.#siteKeys = config.siteKeys;
|
|
166
|
+
this.#siteKeyId = keyId2(publicIdentityOf2(config.siteKeys).identity);
|
|
141
167
|
this.#verificationUrl = config.verificationUrl;
|
|
142
168
|
this.#leaseMs = config.leaseMs ?? DEFAULTS.leaseMs;
|
|
143
169
|
this.#pairingTtlMs = config.pairingTtlMs ?? DEFAULTS.pairingTtlMs;
|
|
@@ -162,8 +188,7 @@ var ByollmHandlers = class {
|
|
|
162
188
|
auth,
|
|
163
189
|
body,
|
|
164
190
|
HeartbeatRequest,
|
|
165
|
-
this.#heartbeat.bind(this)
|
|
166
|
-
{ allowRevoked: true }
|
|
191
|
+
this.#heartbeat.bind(this)
|
|
167
192
|
);
|
|
168
193
|
case "fetch":
|
|
169
194
|
return this.#authed(auth, body, FetchRequest, this.#fetch.bind(this));
|
|
@@ -264,7 +289,7 @@ var ByollmHandlers = class {
|
|
|
264
289
|
state: "pending",
|
|
265
290
|
owner: null,
|
|
266
291
|
runnerId: null,
|
|
267
|
-
|
|
292
|
+
collected: false,
|
|
268
293
|
label: request.daemon.label,
|
|
269
294
|
platform: request.daemon.platform,
|
|
270
295
|
daemonVersion: request.daemon.version,
|
|
@@ -293,15 +318,19 @@ var ByollmHandlers = class {
|
|
|
293
318
|
if (pairing.expiresAt <= now && pairing.state === "pending") {
|
|
294
319
|
return ok({ status: "expired" });
|
|
295
320
|
}
|
|
296
|
-
if (pairing.state === "approved" && pairing.
|
|
321
|
+
if (pairing.state === "approved" && !pairing.collected && pairing.runnerId !== null && pairing.owner !== null) {
|
|
297
322
|
const response = {
|
|
298
323
|
status: "approved",
|
|
299
|
-
runnerToken: pairing.runnerTokenOnce,
|
|
300
324
|
runnerId: pairing.runnerId,
|
|
301
325
|
owner: pairing.owner,
|
|
302
326
|
// Only on approval: a pending or denied poll learns nothing, so an
|
|
303
327
|
// unapproved code cannot be used to enumerate a site's keys.
|
|
304
|
-
|
|
328
|
+
//
|
|
329
|
+
// One entry, because a direct site *is* one site — the same shape a
|
|
330
|
+
// hub answers with rather than a special case (cloud_009 §5). The
|
|
331
|
+
// daemon's lookup is one map read on every lane, which is what keeps
|
|
332
|
+
// the two lanes one protocol.
|
|
333
|
+
sites: { [this.#siteKeyId]: publicIdentityOf2(this.#siteKeys) }
|
|
305
334
|
};
|
|
306
335
|
await this.#store.consumePairingToken(pairing.deviceCodeHash);
|
|
307
336
|
return ok(response);
|
|
@@ -331,6 +360,17 @@ var ByollmHandlers = class {
|
|
|
331
360
|
kind: job.kind,
|
|
332
361
|
audience: job.audience,
|
|
333
362
|
owner: job.owner,
|
|
363
|
+
// This site, named by its identity key id — Amendment A §A.3. The
|
|
364
|
+
// daemon pinned this exact value at pairing, so it can check the stub
|
|
365
|
+
// against the envelope it later opens rather than taking our word for
|
|
366
|
+
// which site sent it. On this plane that is redundant, which is the
|
|
367
|
+
// point: the direct and relayed stubs are the same shape, and a daemon
|
|
368
|
+
// serving both cannot tell which upstream it is talking to.
|
|
369
|
+
site: this.#siteKeyId,
|
|
370
|
+
// byollm_016 Phase B. Present only when the site named one, and
|
|
371
|
+
// omitted rather than sent as undefined — the stub is `.strict()` and
|
|
372
|
+
// an explicit undefined is not the same as an absent key.
|
|
373
|
+
...job.purpose === void 0 ? {} : { purpose: job.purpose },
|
|
334
374
|
// Bucketed, not measured: an exact size is a stronger fingerprint
|
|
335
375
|
// than routing needs (byollm_009 §6).
|
|
336
376
|
sizeClass: job.sizeClass,
|
|
@@ -339,8 +379,19 @@ var ByollmHandlers = class {
|
|
|
339
379
|
// The stub's deadline bounds how long a captured envelope is worth
|
|
340
380
|
// keeping, so it is always present — falling back to the TTL window
|
|
341
381
|
// when the app named no absolute one.
|
|
342
|
-
deadlineAt:
|
|
343
|
-
|
|
382
|
+
deadlineAt: deadlineFor(job, now),
|
|
383
|
+
// `audienceAllow` is not sent — cloud_008 §0.2. The list stays on
|
|
384
|
+
// `JobRecord`, where `claim` already filtered candidates with it; the
|
|
385
|
+
// daemon's own allowlist is what decides `named` (byollm_001 Rev 1
|
|
386
|
+
// §B) and always was.
|
|
387
|
+
//
|
|
388
|
+
// Removing it from `JobStub` did **not** make this line a type error.
|
|
389
|
+
// A conditional spread is not excess-property-checked, so the field
|
|
390
|
+
// would have gone on being sent to a daemon whose `.strict()` parse
|
|
391
|
+
// now rejects the entire claim response — every daemon on the version
|
|
392
|
+
// pair, refusing all work, for a field nobody read. Worth stating
|
|
393
|
+
// where it happened: the schema is the contract, and the compiler
|
|
394
|
+
// does not enforce it through a spread.
|
|
344
395
|
// No fallback. A job returned from `claim` holds a lease by
|
|
345
396
|
// definition, and synthesising one here would hand the daemon a lease
|
|
346
397
|
// id the store has never heard of — every later release naming it
|
|
@@ -358,18 +409,6 @@ var ByollmHandlers = class {
|
|
|
358
409
|
return fail("unauthorized", "runner id does not match the signing key");
|
|
359
410
|
}
|
|
360
411
|
const now = this.#now();
|
|
361
|
-
const revoked = runner.revokedAt !== null;
|
|
362
|
-
if (revoked) {
|
|
363
|
-
const held = await this.#store.listClaimedBy(runner.id);
|
|
364
|
-
const response2 = {
|
|
365
|
-
revoked: true,
|
|
366
|
-
cancel: [],
|
|
367
|
-
leases: [],
|
|
368
|
-
lost: held.map((job) => job.id),
|
|
369
|
-
serverTime: now
|
|
370
|
-
};
|
|
371
|
-
return ok(response2);
|
|
372
|
-
}
|
|
373
412
|
await this.#store.touchRunner({
|
|
374
413
|
runnerId: runner.id,
|
|
375
414
|
capabilities: request.capabilities,
|
|
@@ -377,7 +416,7 @@ var ByollmHandlers = class {
|
|
|
377
416
|
paused: request.paused,
|
|
378
417
|
now
|
|
379
418
|
});
|
|
380
|
-
const {
|
|
419
|
+
const { lost } = await this.#store.renewLeases({
|
|
381
420
|
runnerId: runner.id,
|
|
382
421
|
leases: request.activeLeases,
|
|
383
422
|
leaseMs: this.#leaseMs,
|
|
@@ -385,9 +424,11 @@ var ByollmHandlers = class {
|
|
|
385
424
|
});
|
|
386
425
|
const cancel = await this.#store.listCancelRequests(runner.id);
|
|
387
426
|
const response = {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
427
|
+
sites: { [this.#siteKeyId]: publicIdentityOf2(this.#siteKeys) },
|
|
428
|
+
// A direct site has no disclosure of its own to go stale: consent to it
|
|
429
|
+
// *is* the pairing, and withdrawing it empties the set above.
|
|
430
|
+
awaitingConsent: [],
|
|
431
|
+
cancel: [...cancel],
|
|
391
432
|
lost: [...lost],
|
|
392
433
|
serverTime: now
|
|
393
434
|
};
|
|
@@ -407,18 +448,35 @@ var ByollmHandlers = class {
|
|
|
407
448
|
audience: job.audience,
|
|
408
449
|
runnerId: runner.id,
|
|
409
450
|
runnerOwner: runner.owner,
|
|
410
|
-
|
|
411
|
-
|
|
451
|
+
// From the envelope the device signed, not from the request beside it
|
|
452
|
+
// — cloud_008 §2.5. A daemon can no longer seal one answer and declare
|
|
453
|
+
// it came from a different model.
|
|
454
|
+
backendClass: outcome.value.ran.backendClass,
|
|
455
|
+
model: outcome.value.ran.model
|
|
412
456
|
});
|
|
413
|
-
const {
|
|
457
|
+
const {
|
|
458
|
+
accepted,
|
|
459
|
+
duplicate,
|
|
460
|
+
job: updated
|
|
461
|
+
} = await this.#store.complete({
|
|
414
462
|
jobId: request.jobId,
|
|
415
|
-
|
|
416
|
-
|
|
463
|
+
// Who is asking, for the duplicate answer only — §3.6. Authorisation
|
|
464
|
+
// is `holder`, below, and still is.
|
|
465
|
+
runnerId: runner.id,
|
|
466
|
+
// The grant, not the runner — cloud_008 §1.4a. `CompleteHolder`'s own
|
|
467
|
+
// docstring already called the lease "the more exact check anyway";
|
|
468
|
+
// this plane simply had no lease id to give it until now.
|
|
469
|
+
holder: { by: "lease", leaseId: request.leaseId },
|
|
470
|
+
outcome: outcome.value.outcome,
|
|
417
471
|
provenance,
|
|
418
472
|
now
|
|
419
473
|
});
|
|
420
474
|
const response = {
|
|
421
475
|
accepted,
|
|
476
|
+
// Only when true — cloud_008 §3.6. Absent means "not a duplicate", and
|
|
477
|
+
// an optional field that is always present is a required one wearing a
|
|
478
|
+
// question mark.
|
|
479
|
+
...duplicate === true ? { duplicate: true } : {},
|
|
422
480
|
state: updated?.state ?? job.state
|
|
423
481
|
};
|
|
424
482
|
return ok(response);
|
|
@@ -453,18 +511,12 @@ var ByollmHandlers = class {
|
|
|
453
511
|
if (!opened.ok) {
|
|
454
512
|
return refuse("the result did not verify as coming from this device");
|
|
455
513
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
}
|
|
462
|
-
const outcome = JobOutcome.safeParse(parsed);
|
|
463
|
-
if (!outcome.success) return refuse("the sealed result was not an outcome");
|
|
464
|
-
if (outcome.data.outcome !== request.disposition) {
|
|
465
|
-
return refuse("the declared disposition is not the one that was sealed");
|
|
466
|
-
}
|
|
467
|
-
return { ok: true, value: outcome.data };
|
|
514
|
+
const outcome = openSealedOutcome({
|
|
515
|
+
plaintext: opened.plaintext,
|
|
516
|
+
disposition: request.disposition
|
|
517
|
+
});
|
|
518
|
+
if (!outcome.ok) return refuse(outcome.why);
|
|
519
|
+
return { ok: true, value: outcome.value };
|
|
468
520
|
}
|
|
469
521
|
// -- 5. release -----------------------------------------------------------
|
|
470
522
|
async #release(request, runner) {
|
|
@@ -495,10 +547,13 @@ function leaseOf(job) {
|
|
|
495
547
|
import {
|
|
496
548
|
ENDPOINTS,
|
|
497
549
|
ERROR_STATUS as ERROR_STATUS2,
|
|
550
|
+
MAX_ENVELOPE_BYTES,
|
|
551
|
+
tooLargeMessage,
|
|
498
552
|
PROTOCOL_PREFIX,
|
|
499
553
|
checkProtocolVersion
|
|
500
554
|
} from "@byollm/protocol";
|
|
501
|
-
var MAX_BODY_BYTES =
|
|
555
|
+
var MAX_BODY_BYTES = MAX_ENVELOPE_BYTES + 512 * 1024;
|
|
556
|
+
var tooLarge = (bytes) => tooLargeMessage({ bytes, limit: MAX_BODY_BYTES });
|
|
502
557
|
function normalizeBasePath(basePath) {
|
|
503
558
|
const trimmed = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
|
|
504
559
|
if (!trimmed.startsWith("/")) {
|
|
@@ -548,7 +603,7 @@ function createFetchHandler(config) {
|
|
|
548
603
|
if (declared !== null && Number(declared) > MAX_BODY_BYTES) {
|
|
549
604
|
return json(400, {
|
|
550
605
|
error: "bad-request",
|
|
551
|
-
message:
|
|
606
|
+
message: tooLarge(Number(declared))
|
|
552
607
|
});
|
|
553
608
|
}
|
|
554
609
|
let body;
|
|
@@ -559,7 +614,7 @@ function createFetchHandler(config) {
|
|
|
559
614
|
if (text.length > MAX_BODY_BYTES) {
|
|
560
615
|
return json(400, {
|
|
561
616
|
error: "bad-request",
|
|
562
|
-
message:
|
|
617
|
+
message: tooLarge(text.length)
|
|
563
618
|
});
|
|
564
619
|
}
|
|
565
620
|
body = JSON.parse(text);
|
|
@@ -605,13 +660,14 @@ function json(status, body) {
|
|
|
605
660
|
|
|
606
661
|
export {
|
|
607
662
|
generateDeviceCode,
|
|
608
|
-
generateRunnerToken,
|
|
609
663
|
generateRunnerId,
|
|
610
664
|
generateJobId,
|
|
611
665
|
generateUserCode,
|
|
612
666
|
hashSecret,
|
|
613
667
|
secretsMatch,
|
|
614
668
|
generateLeaseId,
|
|
669
|
+
openSealedOutcome,
|
|
670
|
+
deadlineFor,
|
|
615
671
|
resealForDevice,
|
|
616
672
|
ByollmHandlers,
|
|
617
673
|
SERVED_PROTOCOL_VERSION,
|
|
@@ -619,4 +675,4 @@ export {
|
|
|
619
675
|
signatureFrom,
|
|
620
676
|
createFetchHandler
|
|
621
677
|
};
|
|
622
|
-
//# sourceMappingURL=chunk-
|
|
678
|
+
//# sourceMappingURL=chunk-36Y77FUD.js.map
|