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