@byollm/server 0.1.0-alpha.2 → 0.1.0-alpha.21

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  > [!WARNING]
2
- > **Alpha (`0.1.0-alpha.2`) — under active development. Don't use this yet.**
2
+ > **Alpha (`0.1.0-alpha.21`) — 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,93 @@
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.
75
+ >
76
+ > **`alpha.7` breaks nobody who is not implementing a relay.** `@byollm/relay`'s
77
+ > projection gains `devices` — the device keys a control plane has approved —
78
+ > and the relay refuses to pair a device that is not in it. `revoked` becomes
79
+ > `{owner, siteId}` instead of a composite string. Sites, daemons, stores and
80
+ > the wire format are all untouched; no runner re-pairs.
81
+ >
82
+ > **`alpha.5` broke store adapters, and nothing else.** If you implement
83
+ > `JobStore` yourself, two changes are required: a new `adopt(args)` method
84
+ > (record a lease granted by an upstream this store does not own), and
85
+ > `CompleteArgs.runnerId` is replaced by `holder` — a discriminated union
86
+ > naming either a runner or a lease. Apps, daemons and the wire format are
87
+ > unaffected; `@byollm/conformance` will tell you if you missed one.
88
+ >
89
+ > **`alpha.4` broke every integration.** Three things changed for you:
90
+ >
91
+ > 1. **`siteKeys` is required.** Run `npx @byollm/server@alpha keygen` once,
92
+ > set `BYOLLM_SITE_KEYS`, and pass it to `ByollmApp` and `createHandler`.
93
+ > Once — not per deploy, never at startup.
94
+ > 2. **`createHandler` takes a function.** `next build` imports route modules
95
+ > with no secrets present, so a config object fails the build.
96
+ > 3. **Every paired runner re-pairs.** Bearer tokens are replaced by per-request
97
+ > signatures against a pinned device key, so old tokens authenticate nothing.
98
+ >
99
+ > Your store adapter is unaffected: payloads and results are sealed before they
100
+ > reach it, and `JobStore` did not change.
15
101
 
16
102
  # `@byollm/server`
17
103
 
@@ -30,28 +116,68 @@ npm install @byollm/server
30
116
  ```ts
31
117
  // app/api/byollm/[...route]/route.ts
32
118
  import { createHandler } from "@byollm/server/next";
33
- import { store } from "@/lib/byollm";
119
+ import { siteKeysFromEnv } from "@byollm/server";
120
+ import { getStore } from "@/lib/byollm";
34
121
 
35
- export const { POST } = createHandler({
36
- store,
122
+ export const { POST } = createHandler(() => ({
123
+ store: getStore(),
124
+ siteKeys: siteKeysFromEnv("BYOLLM_SITE_KEYS"),
37
125
  verificationUrl: "https://your-app.com/settings/runners",
38
- });
126
+ // Next serves this route under /api, so say where it is mounted. The
127
+ // handler matches the full path and will 404 without this.
128
+ basePath: "/api/byollm",
129
+ }));
39
130
  ```
40
131
 
132
+ **Pass a function, not an object.** `next build` imports every route module to
133
+ collect page data, in an environment that has no secrets. A config object is
134
+ constructed during that import, so the build fails on credentials it cannot
135
+ have. A function is not called until the first request.
136
+
137
+ Then pair against that same path — `byollm connect https://your-app.com/api`.
138
+ The daemon appends `/byollm/<endpoint>` to whatever origin it is given, so
139
+ connecting to the bare domain looks for `/byollm/claim` and finds nothing.
140
+ To serve at `/byollm` instead, put the route at `app/byollm/[...route]/route.ts`,
141
+ drop `basePath`, and pair against the bare domain.
142
+
41
143
  **2. Pick a store.**
42
144
 
43
145
  ```ts
44
146
  // lib/byollm.ts
45
- import { ByollmApp, MemoryStore } from "@byollm/server";
147
+ import { ByollmApp, MemoryStore, siteKeysFromEnv } from "@byollm/server";
46
148
 
47
- export const store = new MemoryStore();
48
- export const app = new ByollmApp({ store });
149
+ // Lazily, and memoized, for the same reason the mount takes a function: a
150
+ // module-scope `new` runs during `next build`.
151
+ let store: MemoryStore | undefined;
152
+ export function getStore(): MemoryStore {
153
+ return (store ??= new MemoryStore());
154
+ }
155
+
156
+ let app: ByollmApp | undefined;
157
+ export function getApp(): ByollmApp {
158
+ return (app ??= new ByollmApp({
159
+ store: getStore(),
160
+ siteKeys: siteKeysFromEnv("BYOLLM_SITE_KEYS"),
161
+ }));
162
+ }
49
163
  ```
50
164
 
165
+ Generate that identity once, and keep it:
166
+
167
+ ```bash
168
+ npx @byollm/server@alpha keygen # prints BYOLLM_SITE_KEYS=...
169
+ ```
170
+
171
+ Once, not per deploy and never at startup — a daemon pins this identity when
172
+ its owner approves the pairing, and regenerating it means every paired machine
173
+ must pair again. Generating at startup fails only under horizontal scale: each
174
+ instance would have a different identity, and a daemon would be refused by
175
+ whichever one it did not pair with.
176
+
51
177
  **3. Enqueue.**
52
178
 
53
179
  ```ts
54
- const job = await app.enqueue({
180
+ const job = await getApp().enqueue({
55
181
  kind: "llm.generate",
56
182
  audience: "self", // this user's own machine only — the default
57
183
  owner: userId,
@@ -75,7 +201,7 @@ types the code their daemon showed them:
75
201
 
76
202
  ```ts
77
203
  // The owner comes from YOUR session. A daemon can never assert who it is.
78
- const runner = await app.approvePairing({
204
+ const runner = await getApp().approvePairing({
79
205
  userCode: formData.get("code"),
80
206
  owner: session.userId,
81
207
  });
@@ -103,6 +229,7 @@ import {
103
229
  const store = supabaseStore({ client: serviceRoleClient });
104
230
  const app = new ByollmApp({
105
231
  store,
232
+ siteKeys: siteKeysFromEnv("BYOLLM_SITE_KEYS"),
106
233
  delivery: supabaseRealtimeDelivery(serviceRoleClient),
107
234
  });
108
235
  ```
@@ -123,7 +250,7 @@ and `untrusted` is derived from the audience — you cannot mark volunteer
123
250
  output as first-party:
124
251
 
125
252
  ```ts
126
- const { outcome, provenance } = await app.result(jobId);
253
+ const { outcome, provenance } = await getApp().result(jobId);
127
254
  if (provenance?.untrusted) {
128
255
  // Do not render as trusted HTML. Do not feed to a privileged step.
129
256
  // Disclose where it came from.
package/bin/keygen.mjs ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `npx @byollm/server keygen` — make a site identity, once.
4
+ *
5
+ * Prints an env-file fragment. Deliberately not written to a file: key
6
+ * material that lands on disk by default tends to end up committed, and the
7
+ * one place it should live is wherever this deployment keeps its secrets.
8
+ */
9
+ import { formatSiteKeys, generateSiteKeys } from "../dist/index.js";
10
+
11
+ if (process.argv.includes("--help") || process.argv.includes("-h")) {
12
+ process.stdout.write(
13
+ "usage: npx @byollm/server keygen\n\n" +
14
+ "Generates this site's byollm identity and prints it as an env line.\n" +
15
+ "Run it once. Store the result as a secret. Regenerating it makes every\n" +
16
+ "daemon that has paired with this site pair again.\n",
17
+ );
18
+ process.exit(0);
19
+ }
20
+
21
+ process.stdout.write(formatSiteKeys(generateSiteKeys()));