@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/CHANGELOG.md +14 -1
- package/README.md +104 -0
- package/dist/cli.cjs +681 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +679 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +973 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +258 -9
- package/dist/index.d.ts +258 -9
- package/dist/index.js +962 -67
- package/dist/index.js.map +1 -1
- package/examples/answer-questions.mjs +33 -0
- package/examples/ask.mjs +22 -0
- package/examples/every.mjs +28 -0
- package/examples/list-agents.mjs +21 -0
- package/examples/prompt.mjs +25 -0
- package/package.json +6 -2
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(
|
|
1086
|
-
|
|
1087
|
-
"writer",
|
|
1088
|
-
"
|
|
1089
|
-
|
|
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(
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
|
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-${
|
|
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",
|
|
2222
|
-
const
|
|
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 !==
|
|
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
|
-
|
|
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(
|
|
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
|
-
[
|
|
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(
|
|
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
|
-
|
|
3874
|
-
|
|
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 >
|
|
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 >
|
|
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 >
|
|
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
|
|
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: {
|
|
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
|
|
4418
|
-
|
|
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
|
|
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 ===
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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;
|