@synadia-ai/agent-fabric 0.1.0 → 0.2.0

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/dist/index.cjs CHANGED
@@ -3,13 +3,14 @@
3
3
  var agentService = require('@synadia-ai/agent-service');
4
4
  var node_async_hooks = require('node:async_hooks');
5
5
  var agents = require('@synadia-ai/agents');
6
+ var transportNode = require('@nats-io/transport-node');
6
7
  var node_child_process = require('node:child_process');
7
8
  var node_crypto = require('node:crypto');
8
9
  var promises = require('node:fs/promises');
9
10
  var node_os = require('node:os');
10
11
  var node_path = require('node:path');
11
- var natsCore = require('@nats-io/nats-core');
12
12
  var node_fs = require('node:fs');
13
+ var natsCore = require('@nats-io/nats-core');
13
14
 
14
15
  // src/tracing.ts
15
16
  var EDGE_RECORD_VERSION = 1;
@@ -213,6 +214,229 @@ function lineageField(extras, wireName) {
213
214
  }
214
215
  return value;
215
216
  }
217
+ var CALLER_INBOX_PREFIX = "_INBOX.agents";
218
+ var CANNOT_ANSWER = "The caller is a command line and cannot answer; continue with your best judgement.";
219
+ var DEFAULT_DISCOVER_TIMEOUT_MS = 1e3;
220
+ var NATS_URL_VAR = "NATS_URL";
221
+ var NATS_CREDS_VAR = "NATS_CREDS";
222
+ var FabricCallerError = class extends Error {
223
+ constructor(reason, message) {
224
+ super(message);
225
+ this.reason = reason;
226
+ this.name = "FabricCallerError";
227
+ }
228
+ reason;
229
+ };
230
+ var CallerStream = class {
231
+ #inner;
232
+ #sink;
233
+ constructor(inner, sink) {
234
+ this.#inner = inner;
235
+ this.#sink = sink;
236
+ }
237
+ /** The subject the prompt is published to. */
238
+ get subject() {
239
+ return this.#inner.subject;
240
+ }
241
+ /** Stop the stream, as `PromptStream.cancel()`. */
242
+ cancel() {
243
+ this.#sink.reject(notSent("it was cancelled"));
244
+ this.#inner.cancel();
245
+ }
246
+ async *[Symbol.asyncIterator]() {
247
+ try {
248
+ for await (const msg of this.#inner) yield msg;
249
+ } catch (err) {
250
+ this.#sink.reject(err);
251
+ throw err;
252
+ } finally {
253
+ this.#sink.reject(notSent("its stream ended first"));
254
+ }
255
+ }
256
+ };
257
+ function notSent(why) {
258
+ return new FabricCallerError("not-sent", `the prompt never went out, so it has no root: ${why}`);
259
+ }
260
+ var ROOT_SINK_KEY = "agentFabricCallerRootSink";
261
+ function rootDeferred() {
262
+ let settled = false;
263
+ let resolve3;
264
+ let reject2;
265
+ const rootId = new Promise((res, rej) => {
266
+ resolve3 = res;
267
+ reject2 = rej;
268
+ });
269
+ rootId.catch(() => {
270
+ });
271
+ const once = (fn) => (v) => {
272
+ if (settled) return;
273
+ settled = true;
274
+ fn(v);
275
+ };
276
+ return { rootId, sink: { resolve: once(resolve3), reject: once(reject2) } };
277
+ }
278
+ async function fabricCaller(options) {
279
+ const creds = options.creds;
280
+ if (typeof creds !== "string" || creds.length === 0) {
281
+ throw new FabricCallerError(
282
+ "config",
283
+ `no creds file: pass creds, or set ${NATS_CREDS_VAR} to the creds file the console gave you`
284
+ );
285
+ }
286
+ const url = options.url ?? process.env[NATS_URL_VAR];
287
+ if (url === void 0 || url.length === 0) {
288
+ throw new FabricCallerError(
289
+ "config",
290
+ `no server URL: pass url, or set ${NATS_URL_VAR} to the URL the console showed next to your creds file`
291
+ );
292
+ }
293
+ const tracing = fabricTracing(
294
+ options.edgeSubject !== void 0 ? { edgeSubject: options.edgeSubject } : {}
295
+ );
296
+ if (options.name !== void 0) {
297
+ try {
298
+ agents.assertValidSenderName(options.name);
299
+ } catch (err) {
300
+ throw new FabricCallerError("config", `name: ${errorMessage(err)}`);
301
+ }
302
+ }
303
+ let bundle;
304
+ try {
305
+ bundle = await agents.resolveNatsConnectionBundle({ url, creds }, { identity: "signed" });
306
+ } catch (err) {
307
+ throw new FabricCallerError("config", `creds file ${creds}: ${errorMessage(err)}`);
308
+ }
309
+ let nc;
310
+ try {
311
+ nc = await transportNode.connect(
312
+ agents.withAgentReconnectDefaults({
313
+ ...bundle.connectionOptions,
314
+ inboxPrefix: CALLER_INBOX_PREFIX,
315
+ ...options.name !== void 0 ? { name: options.name } : {},
316
+ // A caller is short-lived: a wrong URL fails now, naming the URL,
317
+ // instead of retrying forever before the first connection.
318
+ waitOnFirstConnect: false
319
+ })
320
+ );
321
+ } catch (err) {
322
+ bundle.wipe();
323
+ throw new FabricCallerError("connect", `could not connect to ${url}: ${errorMessage(err)}`);
324
+ }
325
+ let agents$1;
326
+ try {
327
+ agents$1 = new agents.Agents({
328
+ nc,
329
+ identity: {
330
+ signer: bundle.signer,
331
+ ...options.name !== void 0 ? { name: options.name } : {}
332
+ },
333
+ interceptors: [rootCapturing(tracing.promptInterceptor)]
334
+ });
335
+ } catch (err) {
336
+ await nc.close();
337
+ bundle.wipe();
338
+ throw err;
339
+ }
340
+ try {
341
+ await agents$1.selfId();
342
+ } catch (err) {
343
+ await agents$1.close();
344
+ await nc.close();
345
+ bundle.wipe();
346
+ throw new FabricCallerError(
347
+ "identity",
348
+ `the server at ${url} would not tell this creds file's identity, which signing needs: ${errorMessage(err)}`
349
+ );
350
+ }
351
+ const list = (opts = {}) => agents$1.discover({ timeoutMs: opts.timeoutMs ?? DEFAULT_DISCOVER_TIMEOUT_MS });
352
+ const find = async (who, opts = {}) => pickAgent(await list(opts), who);
353
+ const prompt = async (who, text2, promptOptions = {}) => {
354
+ const target = typeof who === "string" ? await find(who) : who;
355
+ const { sink, rootId } = rootDeferred();
356
+ let inner;
357
+ try {
358
+ inner = await target.prompt(text2, {
359
+ ...promptOptions,
360
+ context: { ...promptOptions.context ?? {}, [ROOT_SINK_KEY]: sink }
361
+ });
362
+ } catch (err) {
363
+ sink.reject(err);
364
+ throw err;
365
+ }
366
+ return { stream: new CallerStream(inner, sink), rootId };
367
+ };
368
+ const ask = async (who, text2, askOptions = {}) => {
369
+ const { onQuery, ...promptOptions } = askOptions;
370
+ const { stream, rootId } = await prompt(who, text2, promptOptions);
371
+ let reply = "";
372
+ for await (const msg of stream) {
373
+ if (msg.type === "response") reply += msg.text;
374
+ else if (msg.type === "query") {
375
+ const answer = onQuery !== void 0 ? await onQuery(msg) : "";
376
+ await msg.reply(answer.trim() === "" ? CANNOT_ANSWER : answer);
377
+ }
378
+ }
379
+ return { text: reply, rootId: await rootId };
380
+ };
381
+ let closed = false;
382
+ const close = async () => {
383
+ if (closed) return;
384
+ closed = true;
385
+ try {
386
+ await agents$1.close();
387
+ await nc.close();
388
+ } finally {
389
+ bundle.wipe();
390
+ }
391
+ };
392
+ return { agents: agents$1, url, list, find, prompt, ask, close };
393
+ }
394
+ function fabricCallerFromEnv(options = {}) {
395
+ return fabricCaller({ ...options, creds: process.env[NATS_CREDS_VAR] ?? "" });
396
+ }
397
+ function pickAgent(found, who) {
398
+ const byAddress = found.find((a) => a.promptSubject === who);
399
+ if (byAddress !== void 0) return byAddress;
400
+ const ofKind = distinctAddresses(found.filter((a) => a.agent === who));
401
+ if (ofKind.length === 1) return ofKind[0];
402
+ if (ofKind.length > 1) {
403
+ throw new FabricCallerError(
404
+ "ambiguous",
405
+ `${ofKind.length} agents of kind "${who}": ${ofKind.map((a) => a.promptSubject).join(", ")}; name one by its address`
406
+ );
407
+ }
408
+ const there = distinctAddresses(found).map((a) => a.promptSubject).join(", ");
409
+ throw new FabricCallerError("not-found", `no agent "${who}"; there: ${there || "nobody"}`);
410
+ }
411
+ function distinctAddresses(agents) {
412
+ const seen = /* @__PURE__ */ new Map();
413
+ for (const a of agents) if (!seen.has(a.promptSubject)) seen.set(a.promptSubject, a);
414
+ return [...seen.values()];
415
+ }
416
+ function rootCapturing(inner) {
417
+ return {
418
+ async beforePrompt(ctx) {
419
+ const sink = ctx.context[ROOT_SINK_KEY];
420
+ let extras;
421
+ try {
422
+ extras = await inner.beforePrompt(ctx);
423
+ } catch (err) {
424
+ sink?.reject(err);
425
+ throw err;
426
+ }
427
+ const root = extras?.fields?.[ROOT_ID_FIELD];
428
+ if (typeof root === "string") sink?.resolve(root);
429
+ else sink?.reject(notSent("tracing minted no root"));
430
+ return extras;
431
+ },
432
+ async beforePublish(ctx, extras) {
433
+ await inner.beforePublish?.(ctx, extras);
434
+ }
435
+ };
436
+ }
437
+ function errorMessage(err) {
438
+ return err instanceof Error ? err.message : String(err);
439
+ }
216
440
  var OUTSIDE_RUN_ID = /[^A-Za-z0-9_-]/gu;
217
441
  function durableRunId(toolCallId) {
218
442
  if (!validToolCallId(toolCallId)) {
@@ -896,6 +1120,7 @@ var MAX_READ_BYTES = CHUNK_SIZE;
896
1120
  var MAX_PREPARE_OBJECTS = 64;
897
1121
  var MAX_VOLUME_OBJECTS = 65536;
898
1122
  var MAX_VOLUME_BYTES = 8 * 2 ** 30;
1123
+ var MAX_ADMISSION_SLOT = 4294967295;
899
1124
  var WRITER_SLOTS = 8;
900
1125
  var RETENTION_SLOTS = 8;
901
1126
  var GRANT_SLOTS = 16;
@@ -910,6 +1135,7 @@ var CODES = [
910
1135
  "exists",
911
1136
  "conflict",
912
1137
  "fenced",
1138
+ "generation_retired",
913
1139
  "intent_changed",
914
1140
  "capacity",
915
1141
  "summary_limit",
@@ -1082,16 +1308,11 @@ function validateGrantHandle(value) {
1082
1308
  uint(v.generation, void 0, 1);
1083
1309
  }
1084
1310
  function validateCommand(value) {
1085
- const v = record(value, [
1086
- "creation_id",
1087
- "writer",
1088
- "number",
1089
- "operation_id",
1090
- "base",
1091
- "root",
1092
- "trace",
1093
- "retention"
1094
- ]);
1311
+ const v = record(
1312
+ value,
1313
+ ["creation_id", "writer", "number", "operation_id", "base", "root", "trace", "retention"],
1314
+ ["remove_path"]
1315
+ );
1095
1316
  validateTarget({ volume: "v", creation_id: v.creation_id });
1096
1317
  validateWriter(v.writer);
1097
1318
  uint(v.number, void 0, 1);
@@ -1107,6 +1328,10 @@ function validateCommand(value) {
1107
1328
  r.enabled || r.slot === 0 && r.generation === 0 && r.until === 0,
1108
1329
  "disabled retention must be zero"
1109
1330
  );
1331
+ if (v.remove_path !== void 0) {
1332
+ validatePath(v.remove_path);
1333
+ check(v.remove_path !== "/" && !r.enabled, "removal cannot retain or remove the root");
1334
+ }
1110
1335
  }
1111
1336
  function validateReceipt(value) {
1112
1337
  const v = record(value, ["volume", "principal", "command", "head", "retention"]);
@@ -1136,19 +1361,24 @@ function sameValue(a, b) {
1136
1361
  return Object.keys(left).length === Object.keys(right).length && Object.keys(left).every((key) => Object.hasOwn(right, key) && sameValue(left[key], right[key]));
1137
1362
  }
1138
1363
  function validateVolumeView(value) {
1139
- const v = record(value, [
1140
- "target",
1141
- "owner",
1142
- "head",
1143
- "revision",
1144
- "closed",
1145
- "epoch",
1146
- "objects",
1147
- "bytes",
1148
- "writers",
1149
- "retentions",
1150
- "grants"
1151
- ]);
1364
+ const v = record(
1365
+ value,
1366
+ [
1367
+ "target",
1368
+ "owner",
1369
+ "head",
1370
+ "revision",
1371
+ "closed",
1372
+ "epoch",
1373
+ "objects",
1374
+ "bytes",
1375
+ "writers",
1376
+ "retentions",
1377
+ "grants"
1378
+ ],
1379
+ ["lifetime"]
1380
+ );
1381
+ if (v.lifetime !== void 0) validateLifetimeView(v.lifetime);
1152
1382
  validateTarget(v.target);
1153
1383
  validatePrincipal(v.owner);
1154
1384
  validateHead(v.head);
@@ -1170,12 +1400,12 @@ function validateVolumeView(value) {
1170
1400
  if (slot.principal !== void 0) {
1171
1401
  validatePrincipal(slot.principal);
1172
1402
  check(slot.principal.account === v.owner.account, "writer belongs to another account");
1173
- } else check(!slot.write && slot.number === 0, "writer identity absent");
1403
+ } else check(!slot.write, "writer identity absent");
1174
1404
  if (slot.number === 0) check(slot.receipt === void 0, "unused writer has a receipt");
1175
1405
  else {
1176
1406
  validateReceipt(slot.receipt);
1177
1407
  check(
1178
- slot.write && sameValue(slot.receipt.principal, slot.principal) && slot.receipt.volume === v.target.volume && slot.receipt.command.creation_id === v.target.creation_id && slot.receipt.command.writer.slot === index && slot.receipt.command.writer.generation === slot.generation && slot.receipt.command.number === slot.number && slot.receipt.head.generation <= v.head.generation,
1408
+ (slot.principal === void 0 && !slot.write || slot.write && sameValue(slot.receipt.principal, slot.principal)) && slot.receipt.principal.account === v.owner.account && slot.receipt.volume === v.target.volume && slot.receipt.command.creation_id === v.target.creation_id && slot.receipt.command.writer.slot === index && slot.receipt.command.writer.generation === slot.generation && slot.receipt.command.number === slot.number && slot.receipt.head.generation <= v.head.generation,
1179
1409
  "writer receipt differs from slot"
1180
1410
  );
1181
1411
  }
@@ -1237,7 +1467,8 @@ function validateInfo(value) {
1237
1467
  check(Array.isArray(v.scopes) && v.scopes.length <= 16, "scope count exceeds bound");
1238
1468
  const seen = /* @__PURE__ */ new Set();
1239
1469
  for (const raw of v.scopes) {
1240
- const scope = record(raw, ["volume", "objects", "bytes"]);
1470
+ const scope = record(raw, ["volume", "objects", "bytes"], ["admission"]);
1471
+ if (scope.admission !== void 0) validateAdmissionBinding(scope.admission);
1241
1472
  validateVolume(scope.volume);
1242
1473
  uint(scope.objects, MAX_VOLUME_OBJECTS, 1);
1243
1474
  uint(scope.bytes, MAX_VOLUME_BYTES, 4);
@@ -1250,6 +1481,130 @@ function validateContentObject(value) {
1250
1481
  validateContentID(v.id);
1251
1482
  check(v.kind === "chnk" || v.kind === "fobj" || v.kind === "tree", "unsupported object kind");
1252
1483
  }
1484
+ function validateAdmissionBinding(value) {
1485
+ const v = record(value, ["slot", "generation", "operation_id"]);
1486
+ uint(v.slot, MAX_ADMISSION_SLOT);
1487
+ uint(v.generation, void 0, 1);
1488
+ validateAction({ operation_id: v.operation_id, trace: {} });
1489
+ }
1490
+ function validateLifetimeView(value) {
1491
+ const v = record(value, [
1492
+ "admission",
1493
+ "phase",
1494
+ "expires_at",
1495
+ "grace_until",
1496
+ "max_lease_seconds",
1497
+ "grace_seconds",
1498
+ "recovery_reads",
1499
+ "reserve_objects",
1500
+ "reserve_bytes"
1501
+ ]);
1502
+ validateAdmissionBinding(v.admission);
1503
+ for (const key of ["expires_at", "grace_until", "max_lease_seconds", "grace_seconds"])
1504
+ uint(v[key]);
1505
+ bool(v.recovery_reads);
1506
+ uint(v.reserve_objects, 128);
1507
+ uint(v.reserve_bytes, 8 * 2 ** 20);
1508
+ check(v.reserve_objects === 0 === (v.reserve_bytes === 0), "incomplete metadata reserve");
1509
+ check(
1510
+ v.phase === "active" || v.phase === "shrinking" || v.phase === "expiring",
1511
+ "invalid lifetime phase"
1512
+ );
1513
+ check(
1514
+ v.phase === "expiring" ? v.expires_at > 0 && (v.grace_until === 0 || v.grace_until >= v.expires_at) : v.grace_until === 0,
1515
+ "invalid lifetime grace"
1516
+ );
1517
+ check(
1518
+ v.expires_at === 0 ? v.grace_seconds === 0 : v.max_lease_seconds > 0 && v.grace_seconds > 0,
1519
+ "invalid lease policy"
1520
+ );
1521
+ }
1522
+ function validateRenewRequest(value) {
1523
+ const v = record(value, ["target", "until"]);
1524
+ validateTarget(v.target);
1525
+ uint(v.until, void 0, 1);
1526
+ }
1527
+ function validateRemovalRequest(value) {
1528
+ const v = record(value, ["target", "command"]);
1529
+ validateTarget(v.target);
1530
+ validateCommand(v.command);
1531
+ check(
1532
+ v.command.remove_path && v.command.creation_id === v.target.creation_id,
1533
+ "invalid removal command"
1534
+ );
1535
+ }
1536
+ function validateRemovalPlan(value) {
1537
+ const v = record(value, [
1538
+ "target",
1539
+ "slot",
1540
+ "generation",
1541
+ "sequence",
1542
+ "command",
1543
+ "added_objects",
1544
+ "added_bytes"
1545
+ ]);
1546
+ validateRemovalRequest({ target: v.target, command: v.command });
1547
+ uint(v.slot, MAX_ADMISSION_SLOT);
1548
+ uint(v.generation, void 0, 1);
1549
+ uint(v.sequence, void 0, 1);
1550
+ uint(v.added_objects, 128);
1551
+ uint(v.added_bytes, 8 * 2 ** 20);
1552
+ }
1553
+ function validateVolumeReleaseRequest(value) {
1554
+ const v = record(value, ["target", "operation_id", "end_promises"]);
1555
+ validateTarget(v.target);
1556
+ validateAction({ operation_id: v.operation_id, trace: {} });
1557
+ bool(v.end_promises);
1558
+ }
1559
+ function validateVolumeReleasePlan(value) {
1560
+ const v = record(value, [
1561
+ "request",
1562
+ "incarnation",
1563
+ "slot",
1564
+ "generation",
1565
+ "sequence",
1566
+ "head",
1567
+ "effects"
1568
+ ]);
1569
+ validateVolumeReleaseRequest(v.request);
1570
+ text(v.incarnation, 64, 64);
1571
+ check(/^[0-9a-f]{64}$/.test(v.incarnation), "invalid release incarnation");
1572
+ uint(v.slot, MAX_ADMISSION_SLOT);
1573
+ uint(v.generation, void 0, 1);
1574
+ uint(v.sequence, void 0, 1);
1575
+ validateHead(v.head);
1576
+ const effects = record(v.effects, [
1577
+ "objects",
1578
+ "bytes",
1579
+ "retentions",
1580
+ "read_grants",
1581
+ "access_slots",
1582
+ "references"
1583
+ ]);
1584
+ uint(effects.objects, MAX_VOLUME_OBJECTS);
1585
+ uint(effects.bytes, MAX_VOLUME_BYTES);
1586
+ uint(effects.retentions, RETENTION_SLOTS);
1587
+ uint(effects.read_grants, GRANT_SLOTS);
1588
+ uint(effects.access_slots, WRITER_SLOTS);
1589
+ uint(effects.references, RETENTION_SLOTS);
1590
+ }
1591
+ function validateVolumeReleaseResult(value) {
1592
+ const v = record(value, [
1593
+ "target",
1594
+ "slot",
1595
+ "generation",
1596
+ "operation_id",
1597
+ "disposed",
1598
+ "capacity_returned"
1599
+ ]);
1600
+ validateTarget(v.target);
1601
+ uint(v.slot, MAX_ADMISSION_SLOT);
1602
+ uint(v.generation, void 0, 1);
1603
+ validateAction({ operation_id: v.operation_id, trace: {} });
1604
+ bool(v.disposed);
1605
+ bool(v.capacity_returned);
1606
+ check(!v.capacity_returned || v.disposed, "capacity returned before disposal");
1607
+ }
1253
1608
  function validateObjectSpec(value) {
1254
1609
  const v = record(value, ["object", "length"]);
1255
1610
  validateContentObject(v.object);
@@ -2004,6 +2359,45 @@ function validateReferenceRetainResult(value) {
2004
2359
  previous = ref.path;
2005
2360
  }
2006
2361
  }
2362
+ var MAX_BOOTSTRAP_BYTES = 4096;
2363
+ function decodeScratchPadBootstrap(data) {
2364
+ const value = record(parseStrictJSON(data, MAX_BOOTSTRAP_BYTES), [
2365
+ "version",
2366
+ "principal",
2367
+ "volume",
2368
+ "admission",
2369
+ "creation"
2370
+ ]);
2371
+ check(value.version === 1, "unsupported ScratchPad bootstrap version");
2372
+ validatePrincipal(value.principal);
2373
+ validateVolume(value.volume);
2374
+ validateAdmissionBinding(value.admission);
2375
+ validateAction(value.creation);
2376
+ return value;
2377
+ }
2378
+ function readScratchPadBootstrapFile(path) {
2379
+ check(typeof path === "string" && path.length > 0, "bootstrap file path is required");
2380
+ let fd;
2381
+ try {
2382
+ fd = node_fs.openSync(path, node_fs.constants.O_RDONLY | node_fs.constants.O_NONBLOCK | (node_fs.constants.O_NOFOLLOW ?? 0));
2383
+ const stat = node_fs.fstatSync(fd);
2384
+ check(stat.isFile(), "bootstrap must be a regular file");
2385
+ check(stat.size <= MAX_BOOTSTRAP_BYTES, "bootstrap exceeds its byte bound");
2386
+ const bytes = Buffer.alloc(MAX_BOOTSTRAP_BYTES + 1);
2387
+ let count2 = 0;
2388
+ while (count2 < bytes.length) {
2389
+ const read = node_fs.readSync(fd, bytes, count2, bytes.length - count2, count2);
2390
+ if (read === 0) break;
2391
+ count2 += read;
2392
+ }
2393
+ return decodeScratchPadBootstrap(bytes.subarray(0, count2));
2394
+ } catch (cause) {
2395
+ if (cause instanceof NativeScratchPadError) throw cause;
2396
+ throw invalid("cannot read ScratchPad bootstrap file", cause);
2397
+ } finally {
2398
+ if (fd !== void 0) node_fs.closeSync(fd);
2399
+ }
2400
+ }
2007
2401
 
2008
2402
  // src/scratchpad-cli.ts
2009
2403
  var DEFAULT_SP_BIN = "sp";
@@ -2162,6 +2556,15 @@ function cleanupCannotHelp(view, at = now()) {
2162
2556
  return room ? void 0 : "every retention slot holds a live pin";
2163
2557
  }
2164
2558
  function spCliScratchPad(options) {
2559
+ const bootstrap = options.bootstrapFile === void 0 ? void 0 : readScratchPadBootstrapFile(options.bootstrapFile);
2560
+ const selectedVolume = options.volume ?? bootstrap?.volume;
2561
+ if (!selectedVolume || !VOLUME_RE.test(selectedVolume))
2562
+ throw new Error("ScratchPad: a valid volume or bootstrap file is required");
2563
+ const volumeName = selectedVolume;
2564
+ if (bootstrap && bootstrap.volume !== volumeName)
2565
+ throw new Error("ScratchPad: bootstrap differs from the configured volume");
2566
+ if (bootstrap && !options.dir)
2567
+ throw new Error("ScratchPad: bootstrap import requires a persistent dir");
2165
2568
  if (!("creds" in options.nats) || options.nats.creds === void 0) {
2166
2569
  throw new Error(
2167
2570
  "ScratchPad: the sp CLI needs NATS_URL and NATS_CREDS; a NATS context or an nkey seed alone is not supported"
@@ -2169,7 +2572,7 @@ function spCliScratchPad(options) {
2169
2572
  }
2170
2573
  const connection = ["--url", options.nats.url, "--creds", options.nats.creds];
2171
2574
  const bin = options.bin ?? DEFAULT_SP_BIN;
2172
- const configuredDir = options.dir ?? node_path.join(node_os.tmpdir(), `scratchpad-${options.volume}`);
2575
+ const configuredDir = options.dir ?? node_path.join(node_os.tmpdir(), `scratchpad-${volumeName}`);
2173
2576
  const grantTtlS = Math.min(options.grantTtlS ?? DEFAULT_GRANT_TTL_S, DEFAULT_GRANT_TTL_S);
2174
2577
  let dir;
2175
2578
  let binding;
@@ -2218,8 +2621,18 @@ function spCliScratchPad(options) {
2218
2621
  async function sp(command, args, opts = {}) {
2219
2622
  const trace = opts.trace ? ["--thread-id", opts.trace.threadId, "--root-id", opts.trace.rootId] : [];
2220
2623
  if (opts.trace && opts.toolCallId !== void 0) trace.push("--tool-call-id", opts.toolCallId);
2221
- const volume = ["read", "resolve"].includes(command) ? [] : ["--volume", options.volume];
2222
- const argv = [command, ...connection, "--dir", await workspace(), ...volume, ...args, ...trace];
2624
+ const volume = ["read", "resolve"].includes(command) ? [] : ["--volume", volumeName];
2625
+ const bootstrapArgs = command === "init" && options.bootstrapFile ? ["--bootstrap-file", options.bootstrapFile] : [];
2626
+ const argv = [
2627
+ command,
2628
+ ...connection,
2629
+ "--dir",
2630
+ await workspace(),
2631
+ ...volume,
2632
+ ...bootstrapArgs,
2633
+ ...args,
2634
+ ...trace
2635
+ ];
2223
2636
  const maxStdout = opts.maxStdout ?? MAX_STDOUT;
2224
2637
  const { code, stdout, stderr } = await new Promise((resolve3) => {
2225
2638
  const child = node_child_process.execFile(
@@ -2280,7 +2693,7 @@ function spCliScratchPad(options) {
2280
2693
  false
2281
2694
  );
2282
2695
  }
2283
- if (r.target.volume !== options.volume) {
2696
+ if (r.target.volume !== volumeName) {
2284
2697
  throw new ScratchPadError(
2285
2698
  `sp ${command}: the workspace is bound to another volume`,
2286
2699
  "invalid",
@@ -2575,6 +2988,7 @@ function spCliScratchPad(options) {
2575
2988
  return pad;
2576
2989
  }
2577
2990
  var SCRATCHPAD_VOLUME_VAR = "SCRATCHPAD_VOLUME";
2991
+ var SCRATCHPAD_BOOTSTRAP_FILE_VAR = "SCRATCHPAD_BOOTSTRAP_FILE";
2578
2992
  var SCRATCHPAD_BIN_VAR = "SCRATCHPAD_BIN";
2579
2993
  var SCRATCHPAD_DIR_VAR = "SCRATCHPAD_DIR";
2580
2994
  var SCRATCHPAD_GRANT_TTL_S_VAR = "SCRATCHPAD_GRANT_TTL_S";
@@ -2584,7 +2998,8 @@ function scratchPadFromEnv(env = process.env, options = {}) {
2584
2998
  return value === void 0 || value === "" ? void 0 : value;
2585
2999
  };
2586
3000
  const volume = optional(SCRATCHPAD_VOLUME_VAR);
2587
- if (volume === void 0) return void 0;
3001
+ const bootstrapFile = optional(SCRATCHPAD_BOOTSTRAP_FILE_VAR);
3002
+ if (volume === void 0 && bootstrapFile === void 0) return void 0;
2588
3003
  const ttl = optional(SCRATCHPAD_GRANT_TTL_S_VAR);
2589
3004
  let grantTtlS;
2590
3005
  if (ttl !== void 0) {
@@ -2600,7 +3015,8 @@ function scratchPadFromEnv(env = process.env, options = {}) {
2600
3015
  const bin = optional(SCRATCHPAD_BIN_VAR);
2601
3016
  const dir = optional(SCRATCHPAD_DIR_VAR);
2602
3017
  return spCliScratchPad({
2603
- volume,
3018
+ ...volume === void 0 ? {} : { volume },
3019
+ ...bootstrapFile === void 0 ? {} : { bootstrapFile },
2604
3020
  nats,
2605
3021
  ...bin !== void 0 ? { bin } : {},
2606
3022
  ...dir !== void 0 ? { dir } : {},
@@ -2695,6 +3111,12 @@ function loopGuard() {
2695
3111
  waiting
2696
3112
  };
2697
3113
  }
3114
+ function normalizeCommand(command) {
3115
+ return {
3116
+ ...command,
3117
+ trace: Object.fromEntries(Object.entries(command.trace).filter(([, value]) => value !== ""))
3118
+ };
3119
+ }
2698
3120
  function pathRequest(v) {
2699
3121
  validateTarget(v.target);
2700
3122
  validateContentID(v.root);
@@ -2708,11 +3130,27 @@ function validateRequest(verb, value) {
2708
3130
  validateEmpty(value);
2709
3131
  return;
2710
3132
  case "volume.create": {
2711
- const v = record(value, ["volume", "action"]);
3133
+ const v = record(value, ["volume", "action"], ["admission"]);
2712
3134
  validateVolume(v.volume);
2713
3135
  validateAction(v.action);
3136
+ if (v.admission !== void 0) validateAdmissionBinding(v.admission);
2714
3137
  return;
2715
3138
  }
3139
+ case "volume.renew":
3140
+ validateRenewRequest(value);
3141
+ return;
3142
+ case "volume.remove.plan":
3143
+ validateRemovalRequest(value);
3144
+ return;
3145
+ case "volume.remove.apply":
3146
+ validateRemovalPlan(value);
3147
+ return;
3148
+ case "volume.release.plan":
3149
+ validateVolumeReleaseRequest(value);
3150
+ return;
3151
+ case "volume.release.apply":
3152
+ validateVolumeReleasePlan(value);
3153
+ return;
2716
3154
  case "volume.get": {
2717
3155
  const v = record(value, ["target"]);
2718
3156
  validateTarget(v.target);
@@ -2777,6 +3215,7 @@ function validateRequest(verb, value) {
2777
3215
  const v = record(value, ["target", "command"]);
2778
3216
  validateTarget(v.target);
2779
3217
  validateCommand(v.command);
3218
+ check(v.command.remove_path === void 0, "removal requires volume.remove.apply");
2780
3219
  check(
2781
3220
  v.target.creation_id === v.command.creation_id,
2782
3221
  "command names another creation identity"
@@ -3031,6 +3470,88 @@ var ScratchPadTransport = class {
3031
3470
  options
3032
3471
  );
3033
3472
  this.matching(result.target.volume === expected.volume, true, "reply names another volume");
3473
+ this.matching(
3474
+ sameValue(result.lifetime?.admission, expected.admission),
3475
+ true,
3476
+ "creation reply names another admission"
3477
+ );
3478
+ return result;
3479
+ }
3480
+ async renewLease(request, options = {}) {
3481
+ const expected = structuredClone(request);
3482
+ const result = await this.view("volume.renew", expected.target, expected, true, options);
3483
+ this.matching(
3484
+ result.lifetime !== void 0 && result.lifetime.expires_at >= expected.until,
3485
+ true,
3486
+ "renewal reply differs from saved deadline"
3487
+ );
3488
+ return result;
3489
+ }
3490
+ async planRemoval(request, options = {}) {
3491
+ const expected = structuredClone(request);
3492
+ const result = await this.control(
3493
+ "volume.remove.plan",
3494
+ expected,
3495
+ validateRemovalPlan,
3496
+ false,
3497
+ options
3498
+ );
3499
+ this.matching(
3500
+ sameValue(result.target, expected.target) && sameValue(
3501
+ normalizeCommand(result.command),
3502
+ normalizeCommand({ ...expected.command, root: result.command.root })
3503
+ ),
3504
+ false,
3505
+ "removal plan changed requested intent"
3506
+ );
3507
+ return result;
3508
+ }
3509
+ async applyRemoval(plan, options = {}) {
3510
+ const expected = structuredClone(plan);
3511
+ const result = await this.control(
3512
+ "volume.remove.apply",
3513
+ expected,
3514
+ validateReceipt,
3515
+ true,
3516
+ options
3517
+ );
3518
+ this.matching(
3519
+ result.volume === expected.target.volume && sameValue(normalizeCommand(result.command), normalizeCommand(expected.command)),
3520
+ true,
3521
+ "removal receipt changed saved command"
3522
+ );
3523
+ return result;
3524
+ }
3525
+ async planVolumeRelease(request, options = {}) {
3526
+ const expected = structuredClone(request);
3527
+ const result = await this.control(
3528
+ "volume.release.plan",
3529
+ expected,
3530
+ validateVolumeReleasePlan,
3531
+ false,
3532
+ options
3533
+ );
3534
+ this.matching(
3535
+ sameValue(result.request, expected),
3536
+ false,
3537
+ "release plan changed requested disposition"
3538
+ );
3539
+ return result;
3540
+ }
3541
+ async applyVolumeRelease(plan, options = {}) {
3542
+ const expected = structuredClone(plan);
3543
+ const result = await this.control(
3544
+ "volume.release.apply",
3545
+ expected,
3546
+ validateVolumeReleaseResult,
3547
+ true,
3548
+ options
3549
+ );
3550
+ this.matching(
3551
+ sameValue(result.target, expected.request.target) && result.slot === expected.slot && result.generation === expected.generation && result.operation_id === expected.request.operation_id,
3552
+ true,
3553
+ "release reply changed saved lifetime or operation"
3554
+ );
3034
3555
  return result;
3035
3556
  }
3036
3557
  getVolume(target, options = {}) {
@@ -3105,12 +3626,8 @@ var ScratchPadTransport = class {
3105
3626
  true,
3106
3627
  options
3107
3628
  );
3108
- const normalize = (command) => ({
3109
- ...command,
3110
- trace: Object.fromEntries(Object.entries(command.trace).filter(([, value]) => value !== ""))
3111
- });
3112
3629
  this.matching(
3113
- result.volume === expected.target.volume && sameValue(normalize(result.command), normalize(expected.command)),
3630
+ result.volume === expected.target.volume && sameValue(normalizeCommand(result.command), normalizeCommand(expected.command)),
3114
3631
  true,
3115
3632
  "receipt names another command"
3116
3633
  );
@@ -3622,13 +4139,48 @@ function validateState(value) {
3622
4139
  const s = record(
3623
4140
  value,
3624
4141
  ["version", "principal", "volume", "creator", "creation", "next", "grants"],
3625
- ["target", "writer", "pending", "last"]
4142
+ [
4143
+ "target",
4144
+ "writer",
4145
+ "pending",
4146
+ "last",
4147
+ "admission",
4148
+ "removal",
4149
+ "disposal",
4150
+ "renewal",
4151
+ "disposal_plan",
4152
+ "release_acceptance_unknown"
4153
+ ]
3626
4154
  );
3627
4155
  check(s.version === 1, "unsupported workspace version");
3628
4156
  validatePrincipal(s.principal);
3629
4157
  validateVolume(s.volume);
3630
4158
  text(s.creator, 512, 1);
3631
4159
  validateAction(s.creation);
4160
+ if (s.admission !== void 0) validateAdmissionBinding(s.admission);
4161
+ if (s.removal !== void 0) {
4162
+ validateReceipt(s.removal);
4163
+ check(
4164
+ s.removal.command.remove_path && s.removal.volume === s.volume && sameValue(s.removal.principal, s.principal) && s.removal.command.creation_id === s.target?.creation_id,
4165
+ "saved removal differs from workspace"
4166
+ );
4167
+ }
4168
+ if (s.disposal !== void 0) {
4169
+ validateVolumeReleaseResult(s.disposal);
4170
+ check(
4171
+ s.disposal.capacity_returned && !s.pending && sameValue(s.disposal.target, s.target) && s.disposal.slot === s.admission?.slot && s.disposal.generation === s.admission?.generation,
4172
+ "unfinished or foreign disposal"
4173
+ );
4174
+ }
4175
+ if (s.disposal_plan !== void 0) {
4176
+ validateVolumeReleasePlan(s.disposal_plan);
4177
+ check(
4178
+ s.disposal && sameValue(s.disposal_plan.request.target, s.disposal.target) && s.disposal_plan.slot === s.disposal.slot && s.disposal_plan.generation === s.disposal.generation && s.disposal_plan.request.operation_id === s.disposal.operation_id,
4179
+ "completed release plan differs from disposal"
4180
+ );
4181
+ }
4182
+ if (s.release_acceptance_unknown !== void 0)
4183
+ check(s.release_acceptance_unknown === true && s.disposal, "invalid release acceptance marker");
3632
4184
  uint(s.next, void 0, 1);
3633
4185
  if (s.target !== void 0) {
3634
4186
  validateTarget(s.target);
@@ -3638,6 +4190,13 @@ function validateState(value) {
3638
4190
  validateWriter(s.writer);
3639
4191
  check(s.writer.slot === 0 && s.target, "workspace writer binding is invalid");
3640
4192
  }
4193
+ if (s.renewal !== void 0) {
4194
+ validateRenewRequest(s.renewal);
4195
+ check(
4196
+ s.admission && sameValue(s.renewal.target, s.target) && !s.disposal && !["renew", "volume_release"].includes(s.pending?.kind ?? ""),
4197
+ "saved renewal conflicts with the workspace lifetime"
4198
+ );
4199
+ }
3641
4200
  check(Array.isArray(s.grants) && s.grants.length <= 16, "workspace grant ledger exceeds bound");
3642
4201
  for (const raw of s.grants) {
3643
4202
  const g = record(raw, ["request", "handle", "kind"]);
@@ -3672,6 +4231,25 @@ function validateState(value) {
3672
4231
  check(s.target, "pending operation has no target");
3673
4232
  const p = s.pending;
3674
4233
  switch (p.kind) {
4234
+ case "renew":
4235
+ record(p, ["kind", "request"]);
4236
+ validateRenewRequest(p.request);
4237
+ break;
4238
+ case "remove":
4239
+ record(p, ["kind", "plan"]);
4240
+ validateRemovalPlan(p.plan);
4241
+ check(
4242
+ sameValue(p.plan.target, s.target) && sameValue(p.plan.command.writer, s.writer) && p.plan.command.number === s.next,
4243
+ "saved removal writer differs"
4244
+ );
4245
+ break;
4246
+ case "volume_release":
4247
+ record(p, ["kind", "plan"], ["fence_accepted"]);
4248
+ if (p.fence_accepted !== void 0)
4249
+ check(p.fence_accepted === true, "invalid release fence marker");
4250
+ validateVolumeReleasePlan(p.plan);
4251
+ check(sameValue(p.plan.request.target, s.target), "saved disposal names another target");
4252
+ break;
3675
4253
  case "publish":
3676
4254
  record(p, ["kind", "command", "path", "ttl"], ["expiry_revision", "reference"]);
3677
4255
  if (p.reference !== void 0) {
@@ -3720,6 +4298,11 @@ function validateState(value) {
3720
4298
  }
3721
4299
  if ("request" in p)
3722
4300
  check(sameValue(p.request.target, s.target), "pending operation names another target");
4301
+ if (p.kind === "remove" || p.kind === "volume_release")
4302
+ check(
4303
+ s.admission && p.plan.slot === s.admission.slot && p.plan.generation === s.admission.generation,
4304
+ "saved plan names another lifetime"
4305
+ );
3723
4306
  }
3724
4307
  }
3725
4308
  function validateGrant(value) {
@@ -3759,12 +4342,18 @@ var NativeScratchPad = class extends ScratchPadReader {
3759
4342
  tail = Promise.resolve();
3760
4343
  closing = false;
3761
4344
  closePromise;
3762
- run(work) {
4345
+ run(work, allowRenewal = false, allowDisposed = false) {
3763
4346
  if (this.closing)
3764
4347
  return Promise.reject(
3765
4348
  new NativeScratchPadError("closed", "native client is closed", { local: true })
3766
4349
  );
3767
- const result = this.tail.then(work);
4350
+ const result = this.tail.then(() => {
4351
+ if (this.state.renewal && !allowRenewal)
4352
+ refuse("busy", "recover the saved renewal before another mutation");
4353
+ if (this.state.disposal && !allowDisposed)
4354
+ refuse("fenced", "this workspace lifetime has been released");
4355
+ return work();
4356
+ });
3768
4357
  this.tail = result.then(
3769
4358
  () => {
3770
4359
  },
@@ -3798,7 +4387,8 @@ var NativeScratchPad = class extends ScratchPadReader {
3798
4387
  if (!this.state.target) {
3799
4388
  const view2 = await this.transport.createVolume({
3800
4389
  volume: this.state.volume,
3801
- action: this.state.creation
4390
+ action: this.state.creation,
4391
+ ...this.state.admission ? { admission: this.state.admission } : {}
3802
4392
  });
3803
4393
  if (!sameValue(view2.owner, this.principal))
3804
4394
  throw unknown(void 0, "volume creation names another owner");
@@ -3810,6 +4400,10 @@ var NativeScratchPad = class extends ScratchPadReader {
3810
4400
  sameValue(view.owner, this.principal),
3811
4401
  "workspace principal is no longer the volume owner"
3812
4402
  );
4403
+ check(
4404
+ sameValue(view.lifetime?.admission, this.state.admission),
4405
+ "workspace lifetime differs from volume"
4406
+ );
3813
4407
  return view;
3814
4408
  }
3815
4409
  async initialize() {
@@ -3838,6 +4432,7 @@ var NativeScratchPad = class extends ScratchPadReader {
3838
4432
  const state = { ...this.state, writer: result, next: 1 };
3839
4433
  delete state.pending;
3840
4434
  delete state.last;
4435
+ delete state.removal;
3841
4436
  this.save(state, true, true);
3842
4437
  return result;
3843
4438
  }
@@ -3869,15 +4464,228 @@ var NativeScratchPad = class extends ScratchPadReader {
3869
4464
  });
3870
4465
  }
3871
4466
  describe() {
4467
+ return this.run(
4468
+ async () => {
4469
+ const volume = await this.ensureTarget();
4470
+ const pending = this.state.pending?.kind ?? (this.state.renewal ? "renew" : void 0);
4471
+ return pending ? { volume, pending } : { volume };
4472
+ },
4473
+ true,
4474
+ true
4475
+ );
4476
+ }
4477
+ /** Save an absolute deadline before sending it. Retries never extend it implicitly. */
4478
+ renewLease(until) {
4479
+ return this.run(async () => {
4480
+ const request = { target: this.target(), until };
4481
+ validateRenewRequest(request);
4482
+ if (this.state.disposal) refuse("fenced", "this workspace lifetime has been released");
4483
+ if (this.state.pending?.kind === "volume_release")
4484
+ refuse("busy", "the saved volume release must settle before renewal");
4485
+ if (this.state.pending?.kind === "renew") {
4486
+ if (!sameValue(this.state.pending.request, request))
4487
+ refuse("busy", "recover the saved renewal before changing its deadline");
4488
+ return (await this.lifecycle(this.state.pending, false)).volume;
4489
+ }
4490
+ const saved = this.state.renewal;
4491
+ if (saved && !sameValue(saved, request))
4492
+ refuse("busy", "recover the saved renewal before changing its deadline");
4493
+ if (!saved) this.save({ ...this.state, renewal: request });
4494
+ return this.resumeRenewal(request, !saved);
4495
+ }, true);
4496
+ }
4497
+ async resumeRenewal(request, fresh) {
4498
+ let volume;
4499
+ try {
4500
+ volume = await this.transport.renewLease(request);
4501
+ if (!sameValue(volume.lifetime?.admission, this.state.admission))
4502
+ throw unknown(void 0, "renewal names another lifetime");
4503
+ } catch (error) {
4504
+ if (error instanceof NativeScratchPadError && !error.unknown && (fresh || error.code === "expired")) {
4505
+ const state2 = { ...this.state };
4506
+ delete state2.renewal;
4507
+ this.save(state2, true);
4508
+ throw error;
4509
+ }
4510
+ throw unknown(error, "the saved renewal stays pending; call recover()");
4511
+ }
4512
+ const state = { ...this.state };
4513
+ delete state.renewal;
4514
+ this.save(state, true);
4515
+ return structuredClone(volume);
4516
+ }
4517
+ /** Read-only plan for an initialized writer. The caller chooses the path to remove. */
4518
+ planRemoval(path, options) {
4519
+ return this.run(async () => {
4520
+ if (this.state.pending)
4521
+ refuse("busy", "recover the pending operation before planning removal");
4522
+ check(
4523
+ this.state.writer && this.state.admission,
4524
+ "removal requires an initialized reusable writer"
4525
+ );
4526
+ const view = await this.ensureTarget();
4527
+ return this.transport.planRemoval({
4528
+ target: this.target(),
4529
+ command: {
4530
+ creation_id: this.target().creation_id,
4531
+ writer: this.state.writer,
4532
+ number: this.state.next,
4533
+ operation_id: options.operationId,
4534
+ base: view.head,
4535
+ root: view.head.root,
4536
+ trace: wireTrace(options.trace),
4537
+ remove_path: path,
4538
+ retention: { enabled: false, slot: 0, generation: 0, until: 0 }
4539
+ }
4540
+ });
4541
+ });
4542
+ }
4543
+ applyRemoval(plan) {
4544
+ const saved = structuredClone(plan);
4545
+ return this.run(async () => {
4546
+ validateRemovalPlan(saved);
4547
+ check(
4548
+ sameValue(saved.target, this.target()) && saved.slot === this.state.admission?.slot && saved.generation === this.state.admission.generation,
4549
+ "removal plan names another lifetime"
4550
+ );
4551
+ if (!this.state.pending && this.state.removal && sameValue(this.state.removal.command, saved.command))
4552
+ return structuredClone(this.state.removal);
4553
+ return (await this.beginLifecycle({ kind: "remove", plan: saved })).receipt;
4554
+ });
4555
+ }
4556
+ /** Lists every promise affected. EndPromises must be explicitly true to authorize release. */
4557
+ planVolumeRelease(options) {
3872
4558
  return this.run(async () => {
3873
- const volume = await this.ensureTarget();
3874
- return this.state.pending ? { volume, pending: this.state.pending.kind } : { volume };
4559
+ if (this.state.pending)
4560
+ refuse("busy", "recover the pending operation before planning release");
4561
+ check(this.state.admission, "volume release requires a reusable lifetime");
4562
+ return this.transport.planVolumeRelease({
4563
+ target: this.target(),
4564
+ operation_id: options.operationId,
4565
+ end_promises: options.endPromises ?? false
4566
+ });
4567
+ });
4568
+ }
4569
+ applyVolumeRelease(plan) {
4570
+ const saved = structuredClone(plan);
4571
+ return this.run(
4572
+ async () => {
4573
+ validateVolumeReleasePlan(saved);
4574
+ if (!this.state.pending && this.state.disposal) {
4575
+ check(sameValue(this.state.disposal_plan, saved), "completed release differs from plan");
4576
+ return this.releaseOutcome();
4577
+ }
4578
+ return (await this.beginLifecycle({ kind: "volume_release", plan: saved })).release;
4579
+ },
4580
+ false,
4581
+ true
4582
+ );
4583
+ }
4584
+ async beginLifecycle(pending) {
4585
+ if (this.state.disposal) refuse("fenced", "this workspace lifetime has been released");
4586
+ if (this.state.pending) {
4587
+ const saved = this.state.pending;
4588
+ const intent = saved.kind === "volume_release" ? { kind: saved.kind, plan: saved.plan } : saved;
4589
+ if (!sameValue(intent, pending))
4590
+ refuse("busy", "recover the saved operation before changing intent");
4591
+ return this.lifecycle(saved, false);
4592
+ }
4593
+ this.change(pending);
4594
+ return this.lifecycle(pending, true);
4595
+ }
4596
+ async lifecycle(pending, fresh) {
4597
+ let result;
4598
+ try {
4599
+ if (pending.kind === "renew") {
4600
+ const volume = await this.transport.renewLease(pending.request);
4601
+ if (!sameValue(volume.lifetime?.admission, this.state.admission))
4602
+ throw unknown(void 0, "renewal names another lifetime");
4603
+ result = { kind: pending.kind, volume };
4604
+ } else if (pending.kind === "remove") {
4605
+ const receipt = await this.transport.applyRemoval(pending.plan);
4606
+ if (!sameValue(receipt.principal, this.principal))
4607
+ throw unknown(void 0, "removal names another principal");
4608
+ result = { kind: pending.kind, receipt };
4609
+ } else {
4610
+ result = {
4611
+ kind: pending.kind,
4612
+ release: await this.transport.applyVolumeRelease(pending.plan)
4613
+ };
4614
+ }
4615
+ } catch (error) {
4616
+ if (pending.kind === "volume_release" && error instanceof NativeScratchPadError && error.code === "generation_retired" && !error.unknown) {
4617
+ const release = {
4618
+ target: pending.plan.request.target,
4619
+ slot: pending.plan.slot,
4620
+ generation: pending.plan.generation,
4621
+ operation_id: pending.plan.request.operation_id,
4622
+ disposed: true,
4623
+ capacity_returned: true
4624
+ };
4625
+ return this.finishRelease(pending.plan, release, !pending.fence_accepted);
4626
+ }
4627
+ if (pending.kind === "volume_release" && !fresh && !pending.fence_accepted && error instanceof NativeScratchPadError && !error.unknown && error.code === "conflict" && await this.releaseFenced(pending.plan)) {
4628
+ const state2 = { ...this.state };
4629
+ delete state2.pending;
4630
+ this.save(state2, true);
4631
+ refuse("fenced", "the saved release cannot take effect; review a new release plan");
4632
+ }
4633
+ if (error instanceof NativeScratchPadError && !error.unknown && (fresh || pending.kind === "renew" && error.code === "expired")) {
4634
+ const state2 = { ...this.state };
4635
+ delete state2.pending;
4636
+ this.save(state2, true);
4637
+ throw error;
4638
+ }
4639
+ throw unknown(error, `the saved ${pending.kind} request stays pending; call recover()`);
4640
+ }
4641
+ if (pending.kind === "volume_release" && result.release) {
4642
+ if (result.release.capacity_returned)
4643
+ return this.finishRelease(pending.plan, result.release, false);
4644
+ this.save({ ...this.state, pending: { ...pending, fence_accepted: true } }, true);
4645
+ return structuredClone(result);
4646
+ }
4647
+ const state = { ...this.state };
4648
+ delete state.pending;
4649
+ if (result.receipt) {
4650
+ state.removal = result.receipt;
4651
+ state.next = result.receipt.command.number + 1;
4652
+ }
4653
+ this.save(state, true);
4654
+ return structuredClone(result);
4655
+ }
4656
+ async releaseFenced(plan) {
4657
+ let current;
4658
+ try {
4659
+ current = await this.transport.planVolumeRelease(plan.request);
4660
+ } catch {
4661
+ return false;
4662
+ }
4663
+ return sameValue(current.request, plan.request) && current.incarnation === plan.incarnation && current.slot === plan.slot && current.generation === plan.generation && current.sequence > plan.sequence;
4664
+ }
4665
+ releaseOutcome() {
4666
+ check(this.state.disposal, "workspace has no completed release");
4667
+ return structuredClone({
4668
+ ...this.state.disposal,
4669
+ ...this.state.release_acceptance_unknown ? { acceptance_unknown: true } : {}
3875
4670
  });
3876
4671
  }
4672
+ finishRelease(plan, disposal, acceptanceUnknown) {
4673
+ const state = { ...this.state, disposal, disposal_plan: plan };
4674
+ delete state.pending;
4675
+ if (acceptanceUnknown) state.release_acceptance_unknown = true;
4676
+ this.save(state, true);
4677
+ return { kind: "volume_release", release: this.releaseOutcome() };
4678
+ }
3877
4679
  recover() {
3878
- return this.run(() => this.recoverPending());
4680
+ return this.run(() => this.recoverPending(), true, true);
3879
4681
  }
3880
4682
  async recoverPending() {
4683
+ if (this.state.renewal)
4684
+ return { kind: "renew", volume: await this.resumeRenewal(this.state.renewal, false) };
4685
+ const lifecycle = this.state.pending;
4686
+ if (lifecycle?.kind === "renew" || lifecycle?.kind === "remove" || lifecycle?.kind === "volume_release")
4687
+ return this.lifecycle(lifecycle, false);
4688
+ if (this.state.disposal) return { kind: "volume_release", release: this.releaseOutcome() };
3881
4689
  if (this.state.pending?.kind === "reset")
3882
4690
  refuse("fenced", "reset outcome needs an explicit resetWriter with the observed generation");
3883
4691
  await this.initialize();
@@ -3892,6 +4700,8 @@ var NativeScratchPad = class extends ScratchPadReader {
3892
4700
  }
3893
4701
  }
3894
4702
  if (pending) {
4703
+ if (pending.kind === "renew" || pending.kind === "remove" || pending.kind === "volume_release")
4704
+ return this.lifecycle(pending, false);
3895
4705
  await this.control(pending, false);
3896
4706
  return { kind: pending.kind };
3897
4707
  }
@@ -3915,6 +4725,9 @@ var NativeScratchPad = class extends ScratchPadReader {
3915
4725
  validateAction({ operation_id: options.operationId, trace });
3916
4726
  if (recover) await this.recoverPending();
3917
4727
  await this.initialize();
4728
+ const scope = this.scope;
4729
+ if (!scope || !sameValue(scope.admission, this.state.admission) || this.state.disposal)
4730
+ refuse("fenced", "workspace has no matching current reservation");
3918
4731
  const pending = this.state.pending;
3919
4732
  if (pending) {
3920
4733
  if (pending.kind !== "publish" || pending.command.operation_id !== options.operationId)
@@ -3939,7 +4752,7 @@ var NativeScratchPad = class extends ScratchPadReader {
3939
4752
  let ordinal = 0, totalBytes = 0;
3940
4753
  const put = (object, data) => {
3941
4754
  totalBytes += data.length;
3942
- if (totalBytes > this.scope.bytes || ordinal + 1 > this.scope.objects)
4755
+ if (totalBytes > scope.bytes || ordinal + 1 > scope.objects)
3943
4756
  refuse("capacity", "artifact exceeds this volume's configured capacity");
3944
4757
  this.store.putObject(ordinal++, object, data);
3945
4758
  };
@@ -3969,7 +4782,7 @@ var NativeScratchPad = class extends ScratchPadReader {
3969
4782
  "capacity",
3970
4783
  "all retention slots hold live or protected handoffs; cleanup cannot free a slot"
3971
4784
  );
3972
- const needsCleanup = fullRetention || view.objects + objects.length > this.scope.objects || view.bytes + objects.reduce((n, object) => n + object.length, 0) > this.scope.bytes;
4785
+ const needsCleanup = fullRetention || view.objects + objects.length > scope.objects || view.bytes + objects.reduce((n, object) => n + object.length, 0) > scope.bytes;
3973
4786
  if (needsCleanup) view = await this.maintainInternal();
3974
4787
  const slot = view.retentions.findIndex((entry) => !entry.pin);
3975
4788
  if (slot < 0)
@@ -3977,13 +4790,20 @@ var NativeScratchPad = class extends ScratchPadReader {
3977
4790
  "capacity",
3978
4791
  "all retention slots are occupied; release finished handoffs or wait for their expiry"
3979
4792
  );
3980
- if (view.objects + objects.length > this.scope.objects || view.bytes + objects.reduce((n, object) => n + object.length, 0) > this.scope.bytes)
4793
+ if (view.objects + objects.length > scope.objects || view.bytes + objects.reduce((n, object) => n + object.length, 0) > scope.bytes)
3981
4794
  refuse("capacity", "live roots and this artifact exceed the volume's capacity");
3982
4795
  const writer = this.state.writer;
3983
4796
  const actual = view.writers[writer.slot];
3984
4797
  if (actual.generation !== writer.generation || actual.number + 1 !== this.state.next)
3985
4798
  refuse("fenced", "remote writer position differs from the durable workspace");
3986
4799
  if (view.closed) refuse("closed", "volume is closed by another maintenance interval");
4800
+ const deadlineClock = now2();
4801
+ const until = Math.min(
4802
+ deadlineClock + ttl,
4803
+ view.lifetime?.expires_at || Number.MAX_SAFE_INTEGER
4804
+ );
4805
+ if (until <= deadlineClock)
4806
+ refuse("expired", "the observed lifetime has no remaining retention window");
3987
4807
  const command = {
3988
4808
  creation_id: this.target().creation_id,
3989
4809
  writer,
@@ -3996,7 +4816,7 @@ var NativeScratchPad = class extends ScratchPadReader {
3996
4816
  enabled: true,
3997
4817
  slot,
3998
4818
  generation: view.retentions[slot].generation,
3999
- until: now2() + ttl
4819
+ until
4000
4820
  }
4001
4821
  };
4002
4822
  const publication = {
@@ -4327,6 +5147,27 @@ var NativeScratchPad = class extends ScratchPadReader {
4327
5147
  return this.transport.getVolume(this.target());
4328
5148
  }
4329
5149
  async control(control, fresh) {
5150
+ if (control.kind === "apply" && this.state.admission) {
5151
+ let view2;
5152
+ try {
5153
+ view2 = await this.transport.getVolume(control.request.target);
5154
+ } catch (error) {
5155
+ throw unknown(error, "private collection completion has not been observed");
5156
+ }
5157
+ if (view2.epoch > control.request.revision) {
5158
+ const state3 = { ...this.state };
5159
+ delete state3.pending;
5160
+ this.save(state3, true);
5161
+ refuse("fenced", "a later collection epoch superseded the saved interval");
5162
+ }
5163
+ if (view2.epoch < control.request.revision)
5164
+ throw unknown(void 0, "saved collection epoch has not been observed");
5165
+ if (view2.closed) refuse("closed", "the private worker has not reopened the saved interval");
5166
+ const state2 = { ...this.state };
5167
+ delete state2.pending;
5168
+ this.save(state2, true);
5169
+ return;
5170
+ }
4330
5171
  let failure, view, handle;
4331
5172
  let settled = false;
4332
5173
  try {
@@ -4358,13 +5199,13 @@ var NativeScratchPad = class extends ScratchPadReader {
4358
5199
  settled = true;
4359
5200
  }
4360
5201
  } else if (control.kind === "close") {
4361
- if (view.closed && view.revision === control.request.revision + 1) {
5202
+ if (this.state.admission ? view.epoch === control.request.revision + 1 : view.closed && view.revision === control.request.revision + 1) {
4362
5203
  failure = void 0;
4363
5204
  settled = true;
4364
5205
  } else if (view.revision !== control.request.revision) {
4365
5206
  settled = true;
4366
5207
  failure = new NativeScratchPadError(
4367
- "conflict",
5208
+ this.state.admission ? "fenced" : "conflict",
4368
5209
  "saved close is fenced by another revision"
4369
5210
  );
4370
5211
  }
@@ -4378,9 +5219,15 @@ var NativeScratchPad = class extends ScratchPadReader {
4378
5219
  if (error instanceof NativeScratchPadError && !error.unknown && (fresh || control.kind === "release" || control.kind === "grant" && error.code === "expired"))
4379
5220
  settled = true;
4380
5221
  }
5222
+ if (control.kind === "close" && this.state.admission && view && view.epoch > control.request.revision + 1) {
5223
+ const state2 = { ...this.state };
5224
+ delete state2.pending;
5225
+ this.save(state2, true);
5226
+ refuse("fenced", "a later collection epoch superseded the saved close");
5227
+ }
4381
5228
  if (!settled)
4382
5229
  throw unknown(failure, `the saved ${control.kind} request stays pending; call recover()`);
4383
- if (!failure && control.kind === "close" && (!view?.closed || view.revision !== control.request.revision + 1))
5230
+ if (!failure && control.kind === "close" && (this.state.admission ? view?.epoch !== control.request.revision + 1 : !view?.closed || view.revision !== control.request.revision + 1))
4384
5231
  throw unknown(void 0, "close returned another maintenance interval");
4385
5232
  const state = { ...this.state };
4386
5233
  delete state.pending;
@@ -4391,10 +5238,13 @@ var NativeScratchPad = class extends ScratchPadReader {
4391
5238
  );
4392
5239
  state.grants.push({ request: control.request, handle, kind: control.provenance });
4393
5240
  }
4394
- if (!failure && control.kind === "close")
5241
+ if (!failure && control.kind === "close" && (!this.state.admission || view.closed))
4395
5242
  state.pending = {
4396
5243
  kind: "apply",
4397
- request: { target: this.target(), revision: view.revision }
5244
+ request: {
5245
+ target: this.target(),
5246
+ revision: this.state.admission ? control.request.revision + 1 : view.revision
5247
+ }
4398
5248
  };
4399
5249
  this.save(state, true);
4400
5250
  if (failure)
@@ -4408,32 +5258,59 @@ var NativeScratchPad = class extends ScratchPadReader {
4408
5258
  }
4409
5259
  };
4410
5260
  async function nativeScratchPad(options) {
4411
- validateVolume(options.volume);
5261
+ if (options.volume !== void 0) validateVolume(options.volume);
4412
5262
  text(options.creator, 512, 1);
4413
5263
  const ttl = options.ttlSeconds ?? 600;
4414
5264
  uint(ttl, MAX_GRANT_SECONDS, 1);
4415
5265
  const transport = new ScratchPadTransport(options.nc, options);
4416
5266
  const info = await transport.info();
4417
- const scope = info.scopes.find((scope2) => scope2.volume === options.volume);
4418
- if (!scope) refuse("denied", "authenticated principal owns no provisioned scope for this volume");
5267
+ const bootstrap = options.bootstrapFile === void 0 ? void 0 : readScratchPadBootstrapFile(options.bootstrapFile);
5268
+ const volume = options.volume ?? bootstrap?.volume;
5269
+ validateVolume(volume);
5270
+ if (bootstrap) {
5271
+ check(
5272
+ sameValue(bootstrap.principal, info.principal),
5273
+ "bootstrap names another authenticated principal"
5274
+ );
5275
+ check(bootstrap.volume === volume, "bootstrap differs from the configured volume");
5276
+ }
5277
+ const scope = info.scopes.find((scope2) => scope2.volume === volume);
4419
5278
  const store = await WorkspaceStore.open(options.dir);
4420
5279
  try {
4421
5280
  let state = store.load();
4422
5281
  if (state === void 0) {
5282
+ if (!scope)
5283
+ refuse("denied", "authenticated principal owns no provisioned scope for this volume");
5284
+ if (bootstrap && !sameValue(scope.admission, bootstrap.admission))
5285
+ refuse("fenced", "bootstrap does not match the current admission");
4423
5286
  state = {
4424
5287
  version: 1,
4425
5288
  principal: info.principal,
4426
- volume: options.volume,
5289
+ volume,
4427
5290
  creator: options.creator,
4428
- creation: { operation_id: node_crypto.randomUUID(), trace: {} },
5291
+ creation: bootstrap ? structuredClone(bootstrap.creation) : { operation_id: node_crypto.randomUUID(), trace: {} },
5292
+ ...scope.admission ? { admission: structuredClone(scope.admission) } : {},
4429
5293
  next: 1,
4430
5294
  grants: []
4431
5295
  };
4432
5296
  store.save(state);
4433
5297
  }
4434
5298
  validateState(state);
5299
+ if (bootstrap) {
5300
+ check(
5301
+ sameValue(state.principal, bootstrap.principal) && state.volume === bootstrap.volume && sameValue(state.admission, bootstrap.admission) && sameValue(state.creation, bootstrap.creation),
5302
+ "workspace differs from the bootstrap creation request"
5303
+ );
5304
+ if (state.disposal || state.disposal_plan || state.pending?.kind === "volume_release")
5305
+ refuse("fenced", "bootstrap cannot reopen a workspace with a saved disposal");
5306
+ }
5307
+ if (!bootstrap && !state.target && (!scope || !sameValue(state.admission, scope.admission)))
5308
+ refuse(
5309
+ "fenced",
5310
+ "saved creation does not match the current admission; it cannot adopt a new lifetime"
5311
+ );
4435
5312
  check(
4436
- sameValue(state.principal, info.principal) && state.volume === options.volume && state.creator === options.creator,
5313
+ sameValue(state.principal, info.principal) && state.volume === volume && state.creator === options.creator,
4437
5314
  "workspace is bound to another principal, volume, or creator"
4438
5315
  );
4439
5316
  return new NativeScratchPad(transport, store, state, scope, ttl);
@@ -4444,14 +5321,16 @@ async function nativeScratchPad(options) {
4444
5321
  }
4445
5322
  async function nativeScratchPadFromEnv(env, options) {
4446
5323
  const volume = env["SCRATCHPAD_VOLUME"];
4447
- if (!volume) return void 0;
5324
+ const bootstrapFile = env["SCRATCHPAD_BOOTSTRAP_FILE"] || void 0;
5325
+ if (!volume && !bootstrapFile) return void 0;
4448
5326
  const dir = env["SCRATCHPAD_DIR"];
4449
5327
  check(dir, "SCRATCHPAD_DIR must name a persistent private directory for the native client");
4450
5328
  const value = env["SCRATCHPAD_GRANT_TTL_S"];
4451
5329
  check(value === void 0 || /^[0-9]+$/.test(value), "SCRATCHPAD_GRANT_TTL_S must be an integer");
4452
5330
  return nativeScratchPad({
4453
5331
  ...options,
4454
- volume,
5332
+ ...volume ? { volume } : {},
5333
+ ...bootstrapFile ? { bootstrapFile } : {},
4455
5334
  dir,
4456
5335
  ...value === void 0 ? {} : { ttlSeconds: Number(value) }
4457
5336
  });
@@ -4474,6 +5353,7 @@ var SP_VARS = {
4474
5353
  creds: "SP_NATS_CREDS",
4475
5354
  dir: SCRATCHPAD_DIR_VAR,
4476
5355
  volume: SCRATCHPAD_VOLUME_VAR,
5356
+ bootstrapFile: SCRATCHPAD_BOOTSTRAP_FILE_VAR,
4477
5357
  creator: "SP_CREATOR",
4478
5358
  threadId: "SP_THREAD_ID",
4479
5359
  rootId: "SP_ROOT_ID",
@@ -4491,8 +5371,13 @@ async function resolveScratchPad(ctx, env) {
4491
5371
  const fromEnv = env[variable];
4492
5372
  return fromEnv === void 0 || fromEnv === "" ? void 0 : fromEnv;
4493
5373
  };
4494
- const volume = pick("volume", SP_VARS.volume);
5374
+ const configuredBootstrap = pick("bootstrapFile", SP_VARS.bootstrapFile);
5375
+ const bootstrapFile = configuredBootstrap === void 0 ? void 0 : node_path.resolve(expandHome(configuredBootstrap));
5376
+ const bootstrap = bootstrapFile === void 0 ? void 0 : readScratchPadBootstrapFile(bootstrapFile);
5377
+ const volume = pick("volume", SP_VARS.volume) ?? bootstrap?.volume;
4495
5378
  if (volume === void 0) return void 0;
5379
+ if (bootstrap && bootstrap.volume !== volume)
5380
+ throw new Error("ScratchPad bootstrap differs from the configured volume");
4496
5381
  let url = pick("url", "NATS_URL");
4497
5382
  let creds = pick("creds", "NATS_CREDS");
4498
5383
  if (url === void 0 || creds === void 0) {
@@ -4522,6 +5407,8 @@ async function resolveScratchPad(ctx, env) {
4522
5407
  if (dir === void 0 && ctx.settings.stateDir !== void 0) {
4523
5408
  dir = node_path.join(ctx.settings.stateDir, "scratchpad");
4524
5409
  }
5410
+ if (bootstrap && dir === void 0)
5411
+ throw new Error("ScratchPad bootstrap requires a persistent workspace directory");
4525
5412
  if (dir !== void 0) {
4526
5413
  dir = node_path.resolve(expandHome(dir));
4527
5414
  try {
@@ -4536,6 +5423,7 @@ async function resolveScratchPad(ctx, env) {
4536
5423
  url,
4537
5424
  creds: expandHome(creds),
4538
5425
  volume,
5426
+ ...bootstrapFile === void 0 ? {} : { bootstrapFile },
4539
5427
  ...dir !== void 0 ? { dir } : {},
4540
5428
  ...grants !== void 0 ? { grants } : {}
4541
5429
  };
@@ -4543,7 +5431,13 @@ async function resolveScratchPad(ctx, env) {
4543
5431
  function exportScratchPad(settings, env) {
4544
5432
  env[SP_VARS.url] = settings.url;
4545
5433
  env[SP_VARS.creds] = settings.creds;
4546
- env[SP_VARS.volume] = settings.volume;
5434
+ if (settings.bootstrapFile !== void 0) {
5435
+ env[SP_VARS.bootstrapFile] = settings.bootstrapFile;
5436
+ delete env[SP_VARS.volume];
5437
+ } else {
5438
+ env[SP_VARS.volume] = settings.volume;
5439
+ delete env[SP_VARS.bootstrapFile];
5440
+ }
4547
5441
  if (settings.dir !== void 0) env[SP_VARS.dir] = settings.dir;
4548
5442
  if (settings.grants !== void 0) env[SP_VARS.grants] = settings.grants;
4549
5443
  }
@@ -4660,6 +5554,7 @@ function spPort(settings, creator, env, skillDir) {
4660
5554
  if (bin === void 0) return void 0;
4661
5555
  const port = spCliScratchPad({
4662
5556
  volume: settings.volume,
5557
+ ...settings.bootstrapFile === void 0 ? {} : { bootstrapFile: settings.bootstrapFile },
4663
5558
  nats: { url: settings.url, creds: settings.creds },
4664
5559
  bin,
4665
5560
  creator,
@@ -4730,6 +5625,10 @@ function expandHome(path) {
4730
5625
  return path;
4731
5626
  }
4732
5627
 
5628
+ exports.CALLER_INBOX_PREFIX = CALLER_INBOX_PREFIX;
5629
+ exports.CANNOT_ANSWER = CANNOT_ANSWER;
5630
+ exports.CallerStream = CallerStream;
5631
+ exports.DEFAULT_DISCOVER_TIMEOUT_MS = DEFAULT_DISCOVER_TIMEOUT_MS;
4733
5632
  exports.DEFAULT_EDGE_SUBJECT = DEFAULT_EDGE_SUBJECT;
4734
5633
  exports.DEFAULT_GRANT_REQUEST_TIMEOUT_MS = DEFAULT_GRANT_REQUEST_TIMEOUT_MS;
4735
5634
  exports.DEFAULT_GRANT_TTL_S = DEFAULT_GRANT_TTL_S;
@@ -4738,9 +5637,13 @@ exports.DEFAULT_SP_BIN = DEFAULT_SP_BIN;
4738
5637
  exports.EDGE_RECORD_VERSION = EDGE_RECORD_VERSION;
4739
5638
  exports.EDGE_SUBJECT_OPTION = EDGE_SUBJECT_OPTION;
4740
5639
  exports.EDGE_SUBJECT_VAR = EDGE_SUBJECT_VAR;
5640
+ exports.FabricCallerError = FabricCallerError;
4741
5641
  exports.GRANT_REQUEST_ENDPOINT = GRANT_REQUEST_ENDPOINT;
4742
5642
  exports.HARNESS_THREAD_ID_MAX = HARNESS_THREAD_ID_MAX;
4743
5643
  exports.LOOP_GUARD_CODE = LOOP_GUARD_CODE;
5644
+ exports.MAX_BOOTSTRAP_BYTES = MAX_BOOTSTRAP_BYTES;
5645
+ exports.NATS_CREDS_VAR = NATS_CREDS_VAR;
5646
+ exports.NATS_URL_VAR = NATS_URL_VAR;
4744
5647
  exports.NativeScratchPad = NativeScratchPad;
4745
5648
  exports.NativeScratchPadError = NativeScratchPadError;
4746
5649
  exports.READS_REFERENCES_KEY = READS_REFERENCES_KEY;
@@ -4749,6 +5652,7 @@ exports.REFERENCE_PREFIX = REFERENCE_PREFIX;
4749
5652
  exports.ROOT_ID_FIELD = ROOT_ID_FIELD;
4750
5653
  exports.ROOT_ID_HEADER = ROOT_ID_HEADER;
4751
5654
  exports.SCRATCHPAD_BIN_VAR = SCRATCHPAD_BIN_VAR;
5655
+ exports.SCRATCHPAD_BOOTSTRAP_FILE_VAR = SCRATCHPAD_BOOTSTRAP_FILE_VAR;
4752
5656
  exports.SCRATCHPAD_DIR_VAR = SCRATCHPAD_DIR_VAR;
4753
5657
  exports.SCRATCHPAD_GRANT_TTL_S_VAR = SCRATCHPAD_GRANT_TTL_S_VAR;
4754
5658
  exports.SCRATCHPAD_OPTION = SCRATCHPAD_OPTION;
@@ -4777,6 +5681,7 @@ exports.buildServedRecord = buildServedRecord;
4777
5681
  exports.countTraceRecordDropped = countTraceRecordDropped;
4778
5682
  exports.countTraceRecordPublished = countTraceRecordPublished;
4779
5683
  exports.decodeReference = decodeReference;
5684
+ exports.decodeScratchPadBootstrap = decodeScratchPadBootstrap;
4780
5685
  exports.decodeShortReference = decodeShortReference;
4781
5686
  exports.durableHeaders = durableHeaders;
4782
5687
  exports.durableRunId = durableRunId;
@@ -4785,6 +5690,8 @@ exports.encodeGrantReply = encodeGrantReply;
4785
5690
  exports.encodeGrantRequest = encodeGrantRequest;
4786
5691
  exports.encodeReference = encodeReference;
4787
5692
  exports.exportScratchPad = exportScratchPad;
5693
+ exports.fabricCaller = fabricCaller;
5694
+ exports.fabricCallerFromEnv = fabricCallerFromEnv;
4788
5695
  exports.fabricTracing = fabricTracing;
4789
5696
  exports.findReferences = findReferences;
4790
5697
  exports.grantRequestEndpoint = grantRequestEndpoint;
@@ -4795,6 +5702,7 @@ exports.loopGuard = loopGuard;
4795
5702
  exports.loopGuardDescription = loopGuardDescription;
4796
5703
  exports.nativeScratchPad = nativeScratchPad;
4797
5704
  exports.nativeScratchPadFromEnv = nativeScratchPadFromEnv;
5705
+ exports.pickAgent = pickAgent;
4798
5706
  exports.randomThreadId = randomThreadId;
4799
5707
  exports.readRecordedGrants = readRecordedGrants;
4800
5708
  exports.readsReferences = readsReferences;