byollm 0.1.0-alpha.21 → 0.1.0-alpha.23

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.21`) — under active development. Don't use this yet.**
2
+ > **Alpha (`0.1.0-alpha.23`) — under active development. Don't use this yet.**
3
3
  >
4
4
  > Install it deliberately: `npx byollm@alpha`, or `npm install byollm@alpha`.
5
5
  >
@@ -79,6 +79,18 @@
79
79
  > `byollm offer <backend> public --cap <cents>`. Local base URLs are
80
80
  > unaffected. `byollm backends` shows the cost class per route.
81
81
 
82
+ <!-- release-note 0.1.0-alpha.21 -->
83
+ > [!NOTE]
84
+ > **`0.1.0-alpha.20` is not a complete release — do not pin it.** Four
85
+ > packages published and `@byollm/server` did not: a Sigstore
86
+ > transparency-log 409 on its provenance attestation. The workflow's
87
+ > "already published" guard correctly refuses to resume a partial publish,
88
+ > so `0.1.0-alpha.23` is that release, whole.
89
+ >
90
+ > If you run the Supabase adapter, `alpha.21` needs
91
+ > `20260819010000_completed_by_lease_id.sql`: alpha.19 shipped §3.6's
92
+ > ordering without the column it stores the grant in.
93
+
82
94
  # `byollm`
83
95
 
84
96
  What end users run. Connects **outbound** to an app you trust, claims only the
package/dist/bin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  main
4
- } from "./chunk-ZGQPZK66.js";
4
+ } from "./chunk-7EO6XN4N.js";
5
5
 
6
6
  // src/bin.ts
7
7
  process.exitCode = await main(process.argv.slice(2));
@@ -1892,6 +1892,7 @@ var Runner = class {
1892
1892
  #capabilities = [];
1893
1893
  #paused = false;
1894
1894
  #revoked = false;
1895
+ #awaitingConsent = false;
1895
1896
  #stopped = false;
1896
1897
  #lastError;
1897
1898
  #completed = 0;
@@ -2174,6 +2175,15 @@ var Runner = class {
2174
2175
  type: "heartbeat",
2175
2176
  capabilities: capabilities.length
2176
2177
  });
2178
+ if (heartbeat.awaitingConsent === true) {
2179
+ if (!this.#awaitingConsent) {
2180
+ this.#awaitingConsent = true;
2181
+ this.#options.onEvent?.({ type: "awaiting-consent" });
2182
+ }
2183
+ } else if (this.#awaitingConsent) {
2184
+ this.#awaitingConsent = false;
2185
+ this.#options.onEvent?.({ type: "consent-resumed" });
2186
+ }
2177
2187
  if (heartbeat.revoked) {
2178
2188
  this.#revoked = true;
2179
2189
  this.cancelAll();
@@ -2276,12 +2286,21 @@ var Runner = class {
2276
2286
  // everything about it.
2277
2287
  plaintext: JSON.stringify({ outcome, ran }),
2278
2288
  senderKeys: keys,
2279
- recipientEncryptionPublic: identity.sitePinned.encryption,
2289
+ // Back to the site that sent it, which is not necessarily the only
2290
+ // site this machine serves. Sealing to `sitePinned` would send site B's
2291
+ // answer to site A — unopenable there, and readable by nobody, which
2292
+ // presents as a job that ran and never came back.
2293
+ recipientEncryptionPublic: this.#pinFor(job).encryption,
2280
2294
  context: {
2281
2295
  jobId: job.id,
2282
2296
  senderKeyId: keyId(publicIdentityOf2(keys).identity),
2283
- recipientKeyId: keyId(identity.sitePinned.identity),
2284
- deadlineAt: Date.now() + ENVELOPE_MAX_AGE_MS,
2297
+ recipientKeyId: keyId(this.#pinFor(job).identity),
2298
+ // This runner's clock, not the process's — cloud_008 §31. The only
2299
+ // `Date.now()` left in this class, in the one place that stamps a
2300
+ // deadline somebody else enforces: a test that moves time moved every
2301
+ // other clock here and not this one, so a sealed result carried an
2302
+ // expiry from a different timeline than the lease it answered.
2303
+ deadlineAt: this.#now() + ENVELOPE_MAX_AGE_MS,
2285
2304
  direction: "result"
2286
2305
  }
2287
2306
  });
@@ -2345,16 +2364,17 @@ var Runner = class {
2345
2364
  );
2346
2365
  }
2347
2366
  const keys = await identity.keys();
2348
- const pinnedKeyId = keyId(identity.sitePinned.identity);
2349
- if (job.site !== pinnedKeyId) {
2367
+ const pinned = this.#pinFor(job);
2368
+ const pinnedKeyId = keyId(pinned.identity);
2369
+ if (envelope.senderKeyId !== job.site) {
2350
2370
  throw new Error(
2351
- `refusing job ${job.id}: it names site ${job.site}, but this machine is paired with ${pinnedKeyId}`
2371
+ `refusing job ${job.id}: the stub names site ${job.site}, but the payload declares it was sealed by ${envelope.senderKeyId}`
2352
2372
  );
2353
2373
  }
2354
2374
  const opened = await open({
2355
2375
  envelope,
2356
2376
  recipientKeys: keys,
2357
- senderIdentityPublic: identity.sitePinned.identity,
2377
+ senderIdentityPublic: pinned.identity,
2358
2378
  expected: {
2359
2379
  jobId: job.id,
2360
2380
  senderKeyId: pinnedKeyId,
@@ -2369,6 +2389,39 @@ var Runner = class {
2369
2389
  }
2370
2390
  return JSON.parse(opened.plaintext);
2371
2391
  }
2392
+ /**
2393
+ * The pinned key for the site a stub names — cloud_009 §5.
2394
+ *
2395
+ * One place, because both legs need it and they must agree: opening the
2396
+ * payload with site A's key while sealing the answer to site B is a job
2397
+ * that runs, produces something nobody can read, and reports success.
2398
+ *
2399
+ * A site the map does not hold is refused by name rather than falling back
2400
+ * to any key at hand. The fallback is the failure this exists to prevent —
2401
+ * the relay chooses `stub.site`, and a daemon that treats an unknown site
2402
+ * as "probably the one I know" has handed the choice of which key verifies
2403
+ * its work to the party the pinning is defending against.
2404
+ */
2405
+ #pinFor(job) {
2406
+ const identity = this.#options.identity;
2407
+ if (!identity) {
2408
+ throw new Error("this daemon has no keys, so it has no site to pin");
2409
+ }
2410
+ const fromMap = identity.sites?.get(job.site);
2411
+ if (fromMap) return fromMap;
2412
+ if (job.site === keyId(identity.sitePinned.identity)) {
2413
+ return identity.sitePinned;
2414
+ }
2415
+ const paired = [
2416
+ .../* @__PURE__ */ new Set([
2417
+ keyId(identity.sitePinned.identity),
2418
+ ...identity.sites?.keys() ?? []
2419
+ ])
2420
+ ].sort();
2421
+ throw new Error(
2422
+ `refusing job ${job.id}: it names site ${job.site}, which this machine is not paired with (paired with ${paired.join(", ")})`
2423
+ );
2424
+ }
2372
2425
  /**
2373
2426
  * Report-and-forget.
2374
2427
  *
@@ -2695,6 +2748,19 @@ function report(origin, event, io) {
2695
2748
  break;
2696
2749
  case "revoked":
2697
2750
  io.out(`${at} ${host} this runner was revoked. Stopping.
2751
+ `);
2752
+ break;
2753
+ // Not a fault and not a revocation: the terms this machine runs under
2754
+ // changed, and the person who has to read them is the one at this
2755
+ // keyboard. The daemon keeps running and keeps its pairing.
2756
+ case "awaiting-consent":
2757
+ io.out(
2758
+ `${at} ${host} paused \u2014 the terms changed and are waiting for you. Open your connections page to read them; nothing runs until you do.
2759
+ `
2760
+ );
2761
+ break;
2762
+ case "consent-resumed":
2763
+ io.out(`${at} ${host} resumed \u2014 thank you.
2698
2764
  `);
2699
2765
  break;
2700
2766
  case "error":
@@ -3158,7 +3224,7 @@ async function main(argv) {
3158
3224
  }
3159
3225
 
3160
3226
  // src/index.ts
3161
- var DAEMON_VERSION = "0.1.0-alpha.21";
3227
+ var DAEMON_VERSION = "0.1.0-alpha.23";
3162
3228
  function daemonVersion() {
3163
3229
  return {
3164
3230
  daemon: DAEMON_VERSION,
@@ -3210,4 +3276,4 @@ export {
3210
3276
  runCli,
3211
3277
  main
3212
3278
  };
3213
- //# sourceMappingURL=chunk-ZGQPZK66.js.map
3279
+ //# sourceMappingURL=chunk-7EO6XN4N.js.map