@synadia-ai/agent-fabric 0.0.0-stage → 0.1.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 +20 -0
- package/LICENSE +201 -0
- package/README.md +184 -2
- package/THIRD_PARTY_NOTICES +23 -0
- package/dist/index.cjs +4821 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1529 -0
- package/dist/index.d.ts +1529 -0
- package/dist/index.js +4733 -0
- package/dist/index.js.map +1 -0
- package/package.json +83 -4
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,4821 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var agentService = require('@synadia-ai/agent-service');
|
|
4
|
+
var node_async_hooks = require('node:async_hooks');
|
|
5
|
+
var agents = require('@synadia-ai/agents');
|
|
6
|
+
var node_child_process = require('node:child_process');
|
|
7
|
+
var node_crypto = require('node:crypto');
|
|
8
|
+
var promises = require('node:fs/promises');
|
|
9
|
+
var node_os = require('node:os');
|
|
10
|
+
var node_path = require('node:path');
|
|
11
|
+
var natsCore = require('@nats-io/nats-core');
|
|
12
|
+
var node_fs = require('node:fs');
|
|
13
|
+
|
|
14
|
+
// src/tracing.ts
|
|
15
|
+
var EDGE_RECORD_VERSION = 1;
|
|
16
|
+
var DEFAULT_EDGE_SUBJECT = "TRACE.edges";
|
|
17
|
+
var THREAD_ID_FIELD = "thread_id";
|
|
18
|
+
var ROOT_ID_FIELD = "root_id";
|
|
19
|
+
var THREAD_ID_HEADER = "X-Synadia-Thread-ID";
|
|
20
|
+
var ROOT_ID_HEADER = "X-Synadia-Root-ID";
|
|
21
|
+
var SUBJECT_FORBIDDEN = /[\u0000-\u0020\u007f\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/;
|
|
22
|
+
function assertValidTraceOptions(options) {
|
|
23
|
+
if (options === void 0) return;
|
|
24
|
+
const subject = options.edgeSubject;
|
|
25
|
+
if (subject === void 0 || subject === null) return;
|
|
26
|
+
const reject2 = (reason) => {
|
|
27
|
+
throw new agents.NatsAgentError(`trace.edgeSubject ${JSON.stringify(subject)}: ${reason}`);
|
|
28
|
+
};
|
|
29
|
+
if (typeof subject !== "string") reject2("must be a string or null");
|
|
30
|
+
if (subject.length === 0) reject2("must not be empty");
|
|
31
|
+
for (const token of subject.split(".")) {
|
|
32
|
+
if (token.length === 0) reject2("must not contain an empty token");
|
|
33
|
+
if (SUBJECT_FORBIDDEN.test(token)) reject2("must not contain whitespace or control characters");
|
|
34
|
+
if (token.includes("*") || token.includes(">")) reject2("must not contain wildcards");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
var COUNTERS_KEY = /* @__PURE__ */ Symbol.for("@synadia-ai/agent-fabric:trace-record-counts");
|
|
38
|
+
var counters = (() => {
|
|
39
|
+
const g = globalThis;
|
|
40
|
+
return g[COUNTERS_KEY] ??= { published: 0, dropped: 0 };
|
|
41
|
+
})();
|
|
42
|
+
function countTraceRecordPublished() {
|
|
43
|
+
counters.published += 1;
|
|
44
|
+
}
|
|
45
|
+
function countTraceRecordDropped() {
|
|
46
|
+
counters.dropped += 1;
|
|
47
|
+
}
|
|
48
|
+
function traceRecordCounts() {
|
|
49
|
+
return Object.freeze({ published: counters.published, dropped: counters.dropped });
|
|
50
|
+
}
|
|
51
|
+
var STORAGE_KEY = /* @__PURE__ */ Symbol.for("@synadia-ai/agent-fabric:trace-scope");
|
|
52
|
+
var storage = (() => {
|
|
53
|
+
const g = globalThis;
|
|
54
|
+
return g[STORAGE_KEY] ??= new node_async_hooks.AsyncLocalStorage();
|
|
55
|
+
})();
|
|
56
|
+
function bindActiveTrace(scope, fn) {
|
|
57
|
+
return storage.run(scope, fn);
|
|
58
|
+
}
|
|
59
|
+
function activeTrace() {
|
|
60
|
+
return storage.getStore();
|
|
61
|
+
}
|
|
62
|
+
function traceHeaders() {
|
|
63
|
+
const scope = activeTrace();
|
|
64
|
+
if (scope === void 0) return {};
|
|
65
|
+
scope.turnCountHint += 1;
|
|
66
|
+
return {
|
|
67
|
+
[THREAD_ID_HEADER]: scope.threadId,
|
|
68
|
+
[ROOT_ID_HEADER]: scope.rootId
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
var THREAD_ID_HEX_LEN = 32;
|
|
72
|
+
var THREAD_ID_RE = new RegExp(`^[0-9a-f]{${THREAD_ID_HEX_LEN}}$`);
|
|
73
|
+
function isThreadId(value) {
|
|
74
|
+
return THREAD_ID_RE.test(value);
|
|
75
|
+
}
|
|
76
|
+
function randomThreadId() {
|
|
77
|
+
const bytes = new Uint8Array(THREAD_ID_HEX_LEN / 2);
|
|
78
|
+
crypto.getRandomValues(bytes);
|
|
79
|
+
let out = "";
|
|
80
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
var TOOL_CALL_ID_MAX = 256;
|
|
84
|
+
var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
85
|
+
function validToolCallId(toolCallId) {
|
|
86
|
+
if (LONE_SURROGATE.test(toolCallId)) return false;
|
|
87
|
+
const codePoints = Array.from(toolCallId).length;
|
|
88
|
+
return codePoints > 0 && codePoints <= TOOL_CALL_ID_MAX;
|
|
89
|
+
}
|
|
90
|
+
function buildEdgeRecord(agent, threadId, parentId, rootId, toolCallId, turnCountHint) {
|
|
91
|
+
const recordId = randomThreadId();
|
|
92
|
+
const record2 = {
|
|
93
|
+
version: EDGE_RECORD_VERSION,
|
|
94
|
+
kind: "edge",
|
|
95
|
+
record_id: recordId,
|
|
96
|
+
ts: Math.floor(Date.now() / 1e3),
|
|
97
|
+
agent,
|
|
98
|
+
thread_id: threadId,
|
|
99
|
+
parent_id: parentId,
|
|
100
|
+
root_id: rootId,
|
|
101
|
+
tool_call_id: toolCallId,
|
|
102
|
+
turn_count_hint: turnCountHint
|
|
103
|
+
};
|
|
104
|
+
return { recordId, payload: new TextEncoder().encode(JSON.stringify(record2)) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/tracing.ts
|
|
108
|
+
var TOOL_CALL_ID_CONTEXT_KEY = "toolCallId";
|
|
109
|
+
function fabricTracing(options = {}) {
|
|
110
|
+
assertValidTraceOptions(options);
|
|
111
|
+
const edgeSubject = options.edgeSubject === void 0 ? DEFAULT_EDGE_SUBJECT : options.edgeSubject;
|
|
112
|
+
return {
|
|
113
|
+
promptInterceptor: promptInterceptor(edgeSubject),
|
|
114
|
+
requestInterceptor,
|
|
115
|
+
heartbeatExtras: edgeSubject === null ? () => ({}) : () => {
|
|
116
|
+
const counts = traceRecordCounts();
|
|
117
|
+
return { records_published: counts.published, records_dropped: counts.dropped };
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function promptInterceptor(edgeSubject) {
|
|
122
|
+
return {
|
|
123
|
+
beforePrompt(ctx) {
|
|
124
|
+
const raw = ctx.context[TOOL_CALL_ID_CONTEXT_KEY];
|
|
125
|
+
const toolCallId = typeof raw === "string" && validToolCallId(raw) ? raw : null;
|
|
126
|
+
const plan = { ...mintLineage(), toolCallId };
|
|
127
|
+
return {
|
|
128
|
+
fields: { [THREAD_ID_FIELD]: plan.threadId, [ROOT_ID_FIELD]: plan.rootId },
|
|
129
|
+
state: plan
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
async beforePublish(ctx, extras) {
|
|
133
|
+
const plan = extras?.state;
|
|
134
|
+
if (edgeSubject === null || plan === void 0) return;
|
|
135
|
+
await publishEdge(ctx, edgeSubject, plan);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function mintLineage() {
|
|
140
|
+
const threadId = randomThreadId();
|
|
141
|
+
const ambient = activeTrace();
|
|
142
|
+
if (ambient === void 0) {
|
|
143
|
+
return { threadId, parentId: null, rootId: threadId, turnCountHint: 0 };
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
threadId,
|
|
147
|
+
parentId: ambient.threadId,
|
|
148
|
+
rootId: ambient.rootId,
|
|
149
|
+
turnCountHint: ambient.turnCountHint
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async function publishEdge(ctx, subject, plan) {
|
|
153
|
+
if (!ctx.identity.canSign) {
|
|
154
|
+
warnEdgesUnsigned(ctx.connection);
|
|
155
|
+
countTraceRecordDropped();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const agent = await ctx.identity.selfId();
|
|
160
|
+
const record2 = buildEdgeRecord(
|
|
161
|
+
agent,
|
|
162
|
+
plan.threadId,
|
|
163
|
+
plan.parentId,
|
|
164
|
+
plan.rootId,
|
|
165
|
+
plan.toolCallId,
|
|
166
|
+
plan.turnCountHint
|
|
167
|
+
);
|
|
168
|
+
await ctx.identity.publishSigned(subject, record2.payload, { nonce: record2.recordId });
|
|
169
|
+
countTraceRecordPublished();
|
|
170
|
+
} catch (err) {
|
|
171
|
+
countTraceRecordDropped();
|
|
172
|
+
console.warn(`@synadia-ai/agent-fabric: failed to publish edge record on ${subject}`, err);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
var warnedUnsigned = /* @__PURE__ */ new WeakSet();
|
|
176
|
+
function warnEdgesUnsigned(nc) {
|
|
177
|
+
if (warnedUnsigned.has(nc)) return;
|
|
178
|
+
warnedUnsigned.add(nc);
|
|
179
|
+
console.warn(
|
|
180
|
+
"@synadia-ai/agent-fabric: tracing is enabled but no identity signer is configured; edge records are not published (consumers ignore unsigned records). Pass identity: { signer } to sign them."
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
var requestInterceptor = {
|
|
184
|
+
async aroundRequest(ctx, next) {
|
|
185
|
+
await bindActiveTrace(scopeFor(ctx.envelope.extras ?? {}), next);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
var HALF_LINEAGE_PAIR = "envelope `thread_id` and `root_id` must be given together";
|
|
189
|
+
function scopeFor(extras) {
|
|
190
|
+
const threadId = lineageField(extras, THREAD_ID_FIELD);
|
|
191
|
+
const rootId = lineageField(extras, ROOT_ID_FIELD);
|
|
192
|
+
if (threadId === void 0 !== (rootId === void 0)) {
|
|
193
|
+
throw new agentService.RequestRejectedError(400, HALF_LINEAGE_PAIR);
|
|
194
|
+
}
|
|
195
|
+
if (threadId !== void 0 && rootId !== void 0) {
|
|
196
|
+
return { threadId, rootId, turnCountHint: 0 };
|
|
197
|
+
}
|
|
198
|
+
const minted = randomThreadId();
|
|
199
|
+
return { threadId: minted, rootId: minted, turnCountHint: 0 };
|
|
200
|
+
}
|
|
201
|
+
function lineageField(extras, wireName) {
|
|
202
|
+
const value = Object.hasOwn(extras, wireName) ? extras[wireName] : void 0;
|
|
203
|
+
if (value === void 0 || value === null) return void 0;
|
|
204
|
+
if (typeof value !== "string") {
|
|
205
|
+
throw new agentService.RequestRejectedError(400, `envelope \`${wireName}\` must be a string`);
|
|
206
|
+
}
|
|
207
|
+
if (value === "") return void 0;
|
|
208
|
+
if (!isThreadId(value)) {
|
|
209
|
+
throw new agentService.RequestRejectedError(
|
|
210
|
+
400,
|
|
211
|
+
`envelope \`${wireName}\` must be ${THREAD_ID_HEX_LEN} lowercase hex characters`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
var OUTSIDE_RUN_ID = /[^A-Za-z0-9_-]/gu;
|
|
217
|
+
function durableRunId(toolCallId) {
|
|
218
|
+
if (!validToolCallId(toolCallId)) {
|
|
219
|
+
throw new agents.NatsAgentError(
|
|
220
|
+
`durableRunId: tool call id ${JSON.stringify(toolCallId)} is empty, too long or not valid Unicode`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
const scope = activeTrace();
|
|
224
|
+
if (scope === void 0) return void 0;
|
|
225
|
+
return `${scope.threadId}-${toolCallId.replace(OUTSIDE_RUN_ID, "_")}`;
|
|
226
|
+
}
|
|
227
|
+
function durableTrace() {
|
|
228
|
+
const scope = activeTrace();
|
|
229
|
+
if (scope === void 0) return void 0;
|
|
230
|
+
return { thread_id: scope.threadId, root_id: scope.rootId };
|
|
231
|
+
}
|
|
232
|
+
function durableHeaders(trace) {
|
|
233
|
+
const ids = checked(trace);
|
|
234
|
+
if (ids === void 0) return {};
|
|
235
|
+
return { [THREAD_ID_HEADER]: ids.thread_id, [ROOT_ID_HEADER]: ids.root_id };
|
|
236
|
+
}
|
|
237
|
+
function withDurableTrace(trace, turnCount, fn) {
|
|
238
|
+
if (!Number.isSafeInteger(turnCount) || turnCount < 0) {
|
|
239
|
+
throw new agents.NatsAgentError(
|
|
240
|
+
`withDurableTrace: turn count ${String(turnCount)} is not a whole number, 0 or more`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
const ids = checked(trace);
|
|
244
|
+
if (ids === void 0) return fn();
|
|
245
|
+
return bindActiveTrace(
|
|
246
|
+
{ threadId: ids.thread_id, rootId: ids.root_id, turnCountHint: turnCount },
|
|
247
|
+
fn
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
function checked(trace) {
|
|
251
|
+
if (typeof trace !== "object" || trace === null) return void 0;
|
|
252
|
+
const { thread_id: threadId, root_id: rootId } = trace;
|
|
253
|
+
if (typeof threadId !== "string" || typeof rootId !== "string") return void 0;
|
|
254
|
+
if (!isThreadId(threadId) || !isThreadId(rootId)) return void 0;
|
|
255
|
+
return { thread_id: threadId, root_id: rootId };
|
|
256
|
+
}
|
|
257
|
+
var SERVED_RECORD_VERSION = 1;
|
|
258
|
+
var SERVED_RECORD_REASON_VERSION = 2;
|
|
259
|
+
var HARNESSES = /* @__PURE__ */ new Set(["claude", "openclaw"]);
|
|
260
|
+
var PHASES = /* @__PURE__ */ new Set(["start", "end"]);
|
|
261
|
+
var STATUSES = /* @__PURE__ */ new Set(["ok", "error", "timeout"]);
|
|
262
|
+
var REASONS = /* @__PURE__ */ new Set([
|
|
263
|
+
"final_text",
|
|
264
|
+
"no_reply",
|
|
265
|
+
"shutdown",
|
|
266
|
+
"delivery",
|
|
267
|
+
"expired"
|
|
268
|
+
]);
|
|
269
|
+
var HARNESS_THREAD_ID_MAX = 256;
|
|
270
|
+
function validHarnessThreadId(value) {
|
|
271
|
+
if (typeof value !== "string" || value.length === 0) return false;
|
|
272
|
+
if (SUBJECT_FORBIDDEN.test(value)) return false;
|
|
273
|
+
if (LONE_SURROGATE.test(value)) return false;
|
|
274
|
+
return Array.from(value).length <= HARNESS_THREAD_ID_MAX;
|
|
275
|
+
}
|
|
276
|
+
function unixSeconds() {
|
|
277
|
+
return Math.floor(Date.now() / 1e3);
|
|
278
|
+
}
|
|
279
|
+
function buildServedRecord(agent, threadId, rootId, harness, harnessThreadId, phase, status, ts = unixSeconds(), reason) {
|
|
280
|
+
if (!HARNESSES.has(harness)) {
|
|
281
|
+
throw new Error(`buildServedRecord: harness must be "claude" or "openclaw", got ${harness}`);
|
|
282
|
+
}
|
|
283
|
+
if (!PHASES.has(phase)) {
|
|
284
|
+
throw new Error(`buildServedRecord: phase must be "start" or "end", got ${phase}`);
|
|
285
|
+
}
|
|
286
|
+
if (phase === "end" && status === void 0) {
|
|
287
|
+
throw new Error("buildServedRecord: an end record needs a status");
|
|
288
|
+
}
|
|
289
|
+
if (phase === "start" && status !== void 0) {
|
|
290
|
+
throw new Error("buildServedRecord: a start record carries no status");
|
|
291
|
+
}
|
|
292
|
+
if (status !== void 0 && !STATUSES.has(status)) {
|
|
293
|
+
throw new Error(`buildServedRecord: status must be "ok", "error" or "timeout", got ${status}`);
|
|
294
|
+
}
|
|
295
|
+
if (phase === "start" && reason !== void 0) {
|
|
296
|
+
throw new Error("buildServedRecord: a start record carries no reason");
|
|
297
|
+
}
|
|
298
|
+
if (reason !== void 0 && !REASONS.has(reason)) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`buildServedRecord: reason must be "final_text", "no_reply", "shutdown", "delivery" or "expired", got ${reason}`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
if (!validHarnessThreadId(harnessThreadId)) {
|
|
304
|
+
throw new Error("buildServedRecord: harnessThreadId is not a usable harness thread id");
|
|
305
|
+
}
|
|
306
|
+
if (!Number.isInteger(ts) || ts < 0) {
|
|
307
|
+
throw new Error("buildServedRecord: ts must be a non-negative integer of unix seconds");
|
|
308
|
+
}
|
|
309
|
+
const recordId = randomThreadId();
|
|
310
|
+
const record2 = {
|
|
311
|
+
version: reason !== void 0 ? SERVED_RECORD_REASON_VERSION : SERVED_RECORD_VERSION,
|
|
312
|
+
kind: "served",
|
|
313
|
+
record_id: recordId,
|
|
314
|
+
ts,
|
|
315
|
+
agent,
|
|
316
|
+
thread_id: threadId,
|
|
317
|
+
root_id: rootId,
|
|
318
|
+
harness,
|
|
319
|
+
harness_thread_id: harnessThreadId,
|
|
320
|
+
phase,
|
|
321
|
+
...status !== void 0 ? { status } : {},
|
|
322
|
+
...reason !== void 0 ? { reason } : {}
|
|
323
|
+
};
|
|
324
|
+
return { recordId, payload: new TextEncoder().encode(JSON.stringify(record2)) };
|
|
325
|
+
}
|
|
326
|
+
var defaultLogger = () => ({
|
|
327
|
+
debug: () => void 0,
|
|
328
|
+
info: () => void 0,
|
|
329
|
+
warn: (msg, ctx) => console.warn(`@synadia-ai/agent-fabric: ${msg}`, ctx ?? ""),
|
|
330
|
+
error: (msg, ctx) => console.error(`@synadia-ai/agent-fabric: ${msg}`, ctx ?? "")
|
|
331
|
+
});
|
|
332
|
+
var ServedPublisher = class {
|
|
333
|
+
#agents;
|
|
334
|
+
#subject;
|
|
335
|
+
#logger;
|
|
336
|
+
#unsignedWarned = false;
|
|
337
|
+
// Records are built, signed and handed to the connection one after
|
|
338
|
+
// another, in the order they were due. A failed record never rejects the
|
|
339
|
+
// chain: `#send` catches everything.
|
|
340
|
+
#chain = Promise.resolve();
|
|
341
|
+
constructor(options) {
|
|
342
|
+
this.#agents = options.agents;
|
|
343
|
+
this.#subject = options.subject ?? DEFAULT_EDGE_SUBJECT;
|
|
344
|
+
this.#logger = options.logger ?? defaultLogger();
|
|
345
|
+
}
|
|
346
|
+
/** Where the records go. */
|
|
347
|
+
get subject() {
|
|
348
|
+
return this.#subject;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Start a prompt's pair: `scope` is the served prompt's trace scope —
|
|
352
|
+
* the one the fabric's request interceptor bound around the handler,
|
|
353
|
+
* `activeTrace()` inside it — and `harness` the one whose id `bind` will
|
|
354
|
+
* name. The arrival, `start`'s `ts`, is now.
|
|
355
|
+
*/
|
|
356
|
+
beginTurn(scope, harness) {
|
|
357
|
+
const arrivedAt = unixSeconds();
|
|
358
|
+
let harnessThreadId;
|
|
359
|
+
let settled = false;
|
|
360
|
+
return {
|
|
361
|
+
bind: (id) => {
|
|
362
|
+
if (settled) {
|
|
363
|
+
this.#logger.warn(
|
|
364
|
+
"served: harness thread id bound after the turn ended; no served record"
|
|
365
|
+
);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (harnessThreadId !== void 0) {
|
|
369
|
+
if (id !== harnessThreadId) {
|
|
370
|
+
this.#logger.warn("served: harness thread id already bound for this prompt; ignored");
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (!validHarnessThreadId(id)) {
|
|
375
|
+
this.#logger.warn("served: unusable harness thread id bound; no served record");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
harnessThreadId = id;
|
|
379
|
+
void this.publish(scope, harness, id, "start", void 0, arrivedAt);
|
|
380
|
+
},
|
|
381
|
+
settle: (status, atMs, reason) => {
|
|
382
|
+
if (settled) return;
|
|
383
|
+
settled = true;
|
|
384
|
+
if (harnessThreadId === void 0) return;
|
|
385
|
+
const ts = atMs === void 0 ? unixSeconds() : Math.floor(atMs / 1e3);
|
|
386
|
+
void this.publish(scope, harness, harnessThreadId, "end", status, ts, reason);
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Publish one signed record, after every record handed over before it.
|
|
392
|
+
* Fail-open and asynchronous: the returned promise never rejects and
|
|
393
|
+
* nothing awaits it on the prompt path. A record that cannot go out — no
|
|
394
|
+
* signer, a client already closed, a publish that threw — is logged and
|
|
395
|
+
* counts as dropped; one that went out counts as published. Every record
|
|
396
|
+
* moves the process-wide {@link traceRecordCounts}.
|
|
397
|
+
*/
|
|
398
|
+
publish(scope, harness, harnessThreadId, phase, status, ts, reason) {
|
|
399
|
+
const next = this.#chain.then(
|
|
400
|
+
() => this.#send(scope, harness, harnessThreadId, phase, status, ts, reason)
|
|
401
|
+
);
|
|
402
|
+
this.#chain = next;
|
|
403
|
+
return next;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Resolves once every record handed to `publish` so far has been signed
|
|
407
|
+
* and handed to the connection, or dropped. For shutdown and tests.
|
|
408
|
+
*/
|
|
409
|
+
flush() {
|
|
410
|
+
return this.#chain;
|
|
411
|
+
}
|
|
412
|
+
async #send(scope, harness, harnessThreadId, phase, status, ts, reason) {
|
|
413
|
+
try {
|
|
414
|
+
const agent = await this.#agents.selfId();
|
|
415
|
+
const record2 = buildServedRecord(
|
|
416
|
+
agent,
|
|
417
|
+
scope.threadId,
|
|
418
|
+
scope.rootId,
|
|
419
|
+
harness,
|
|
420
|
+
harnessThreadId,
|
|
421
|
+
phase,
|
|
422
|
+
status,
|
|
423
|
+
ts,
|
|
424
|
+
reason
|
|
425
|
+
);
|
|
426
|
+
await this.#agents.publishSigned(this.#subject, record2.payload, { nonce: record2.recordId });
|
|
427
|
+
countTraceRecordPublished();
|
|
428
|
+
} catch (err) {
|
|
429
|
+
countTraceRecordDropped();
|
|
430
|
+
if (isSignatureRequired(err)) {
|
|
431
|
+
if (this.#unsignedWarned) return;
|
|
432
|
+
this.#unsignedWarned = true;
|
|
433
|
+
this.#logger.warn(
|
|
434
|
+
"served: no identity signer is configured; served records are not published (consumers ignore unsigned records). Pass identity: { signer } to sign them."
|
|
435
|
+
);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
this.#logger.warn("served: failed to publish served record", {
|
|
439
|
+
subject: this.#subject,
|
|
440
|
+
phase,
|
|
441
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
function isSignatureRequired(err) {
|
|
447
|
+
return err instanceof agents.SenderSignatureRequiredError || err instanceof Error && err.name === "SenderSignatureRequiredError";
|
|
448
|
+
}
|
|
449
|
+
function servedPublisher(options) {
|
|
450
|
+
return new ServedPublisher(options);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/scratchpad.ts
|
|
454
|
+
var REFERENCE_PREFIX = "sp-ref:";
|
|
455
|
+
var SHORT_REFERENCE_PREFIX = "sp-r1:";
|
|
456
|
+
var REFERENCE_RE = /sp-(?:ref|r1):[A-Za-z0-9_-]+/g;
|
|
457
|
+
var READS_REFERENCES_KEY = "scratchpad_refs_ok";
|
|
458
|
+
function findReferences(text2) {
|
|
459
|
+
return text2.match(REFERENCE_RE) ?? [];
|
|
460
|
+
}
|
|
461
|
+
function removeReferences(text2, only) {
|
|
462
|
+
return text2.replace(REFERENCE_RE, (token) => only === void 0 || only.includes(token) ? "" : token).replace(/[ \t]+$/gm, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
463
|
+
}
|
|
464
|
+
function referenceCreator(reference) {
|
|
465
|
+
if (!reference.startsWith(REFERENCE_PREFIX)) return void 0;
|
|
466
|
+
let made;
|
|
467
|
+
try {
|
|
468
|
+
made = JSON.parse(
|
|
469
|
+
Buffer.from(reference.slice(REFERENCE_PREFIX.length), "base64url").toString("utf8")
|
|
470
|
+
);
|
|
471
|
+
} catch {
|
|
472
|
+
return void 0;
|
|
473
|
+
}
|
|
474
|
+
if (typeof made !== "object" || made === null) return void 0;
|
|
475
|
+
const creator = made["creator"];
|
|
476
|
+
return typeof creator === "string" && creator !== "" ? creator : void 0;
|
|
477
|
+
}
|
|
478
|
+
function readsReferences(metadata) {
|
|
479
|
+
return metadata[READS_REFERENCES_KEY] === "true";
|
|
480
|
+
}
|
|
481
|
+
function granteeFromIdentity(identity) {
|
|
482
|
+
const dot = identity.indexOf(".");
|
|
483
|
+
if (dot <= 0 || dot === identity.length - 1) {
|
|
484
|
+
throw new Error(`not an agent identity: ${identity}`);
|
|
485
|
+
}
|
|
486
|
+
return { account: identity.slice(0, dot), user: identity.slice(dot + 1) };
|
|
487
|
+
}
|
|
488
|
+
var GRANT_REQUEST_ENDPOINT = "sp-grant";
|
|
489
|
+
var DEFAULT_GRANT_REQUEST_TIMEOUT_MS = 1e4;
|
|
490
|
+
var PROMPT_PREFIX = "agents.prompt.";
|
|
491
|
+
var GRANT_PREFIX = `agents.${GRANT_REQUEST_ENDPOINT}.`;
|
|
492
|
+
function grantSubject(promptAddress) {
|
|
493
|
+
if (!promptAddress.startsWith(PROMPT_PREFIX) || promptAddress === PROMPT_PREFIX) {
|
|
494
|
+
throw new Error(`not a prompt address: ${promptAddress}`);
|
|
495
|
+
}
|
|
496
|
+
return GRANT_PREFIX + promptAddress.slice(PROMPT_PREFIX.length);
|
|
497
|
+
}
|
|
498
|
+
function encodeGrantRequest(request) {
|
|
499
|
+
const { trace } = request;
|
|
500
|
+
return JSON.stringify({
|
|
501
|
+
reference: request.reference,
|
|
502
|
+
grantee: request.grantee,
|
|
503
|
+
...trace !== void 0 ? {
|
|
504
|
+
trace: {
|
|
505
|
+
thread_id: trace.thread_id,
|
|
506
|
+
root_id: trace.root_id,
|
|
507
|
+
...trace.tool_call_id !== void 0 ? { tool_call_id: trace.tool_call_id } : {}
|
|
508
|
+
}
|
|
509
|
+
} : {}
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
function encodeGrantReply(reply) {
|
|
513
|
+
return JSON.stringify(
|
|
514
|
+
reply.granted ? { granted: true } : { granted: false, reason: reply.reason }
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
async function requestGrant(agents, creatorAddress, request, options = {}) {
|
|
518
|
+
let answer;
|
|
519
|
+
try {
|
|
520
|
+
const msg = await agents.requestSigned(
|
|
521
|
+
grantSubject(creatorAddress),
|
|
522
|
+
encodeGrantRequest(request),
|
|
523
|
+
{ timeoutMs: options.timeoutMs ?? DEFAULT_GRANT_REQUEST_TIMEOUT_MS }
|
|
524
|
+
);
|
|
525
|
+
answer = JSON.parse(msg.string());
|
|
526
|
+
} catch (err) {
|
|
527
|
+
return refusal(`no answer from ${creatorAddress}: ${describe(err)}`);
|
|
528
|
+
}
|
|
529
|
+
if (typeof answer !== "object" || answer === null) {
|
|
530
|
+
return refusal("refused without a reason");
|
|
531
|
+
}
|
|
532
|
+
const { granted, reason } = answer;
|
|
533
|
+
if (granted === true) return { granted: true };
|
|
534
|
+
return refusal(typeof reason === "string" ? reason : "refused without a reason");
|
|
535
|
+
}
|
|
536
|
+
async function answerGrantRequest(requester, payload, scratchPad, logger = agents.SILENT_LOGGER) {
|
|
537
|
+
const request = parseGrantRequest(payload);
|
|
538
|
+
if (request === void 0) return refusal("not a grant request");
|
|
539
|
+
let from;
|
|
540
|
+
let to;
|
|
541
|
+
try {
|
|
542
|
+
from = granteeFromIdentity(requester);
|
|
543
|
+
to = granteeFromIdentity(request.grantee);
|
|
544
|
+
} catch {
|
|
545
|
+
return refusal("grantee is not an agent identity");
|
|
546
|
+
}
|
|
547
|
+
const labels = request.trace;
|
|
548
|
+
const trace = labels !== void 0 ? { threadId: labels.thread_id, rootId: labels.root_id } : void 0;
|
|
549
|
+
let own = false;
|
|
550
|
+
try {
|
|
551
|
+
own = await scratchPad.owns(request.reference);
|
|
552
|
+
} catch {
|
|
553
|
+
}
|
|
554
|
+
if (!own) return refusal("the reference does not point into this agent's volume");
|
|
555
|
+
let held;
|
|
556
|
+
try {
|
|
557
|
+
held = await scratchPad.granted(request.reference, from);
|
|
558
|
+
} catch (err) {
|
|
559
|
+
logger.error("grant request: the ledger could not be read", { error: describe(err) });
|
|
560
|
+
return refusal("the grant could not be made");
|
|
561
|
+
}
|
|
562
|
+
if (held === void 0) {
|
|
563
|
+
return refusal(
|
|
564
|
+
"this agent holds no live read grant it made to the requester on that reference"
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
if (held !== "first-hand") {
|
|
568
|
+
return refusal(
|
|
569
|
+
"the requester's own grant was made at another agent's request; only an agent this agent handed the reference itself may ask for another"
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
if (to.account === from.account && to.user === from.user) {
|
|
573
|
+
return refusal("a grant request is for another agent, not for the requester");
|
|
574
|
+
}
|
|
575
|
+
try {
|
|
576
|
+
await scratchPad.grantRead(request.reference, to, trace, labels?.tool_call_id, "on-request");
|
|
577
|
+
} catch (err) {
|
|
578
|
+
logger.error("grant request: the grant failed", { error: describe(err) });
|
|
579
|
+
return refusal("the grant could not be made");
|
|
580
|
+
}
|
|
581
|
+
logger.info("grant request: granted", { grantee: request.grantee, requester });
|
|
582
|
+
return { granted: true };
|
|
583
|
+
}
|
|
584
|
+
function grantRequestEndpoint(options) {
|
|
585
|
+
const { scratchPad } = options;
|
|
586
|
+
const logger = options.logger ?? agents.SILENT_LOGGER;
|
|
587
|
+
const gate = new agentService.SenderGate({ minSenderTrust: "signed", logger });
|
|
588
|
+
async function decide(msg) {
|
|
589
|
+
const admission = await gate.admitPrompt(msg);
|
|
590
|
+
if (!admission.ok) return refusal(admission.description);
|
|
591
|
+
const sender = admission.sender;
|
|
592
|
+
if (sender?.trust !== "verified") return refusal("a grant request must be signed");
|
|
593
|
+
return answerGrantRequest(sender.id, msg.string(), scratchPad, logger);
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
name: GRANT_REQUEST_ENDPOINT,
|
|
597
|
+
subject: grantSubject(options.promptAddress),
|
|
598
|
+
queue: "agents",
|
|
599
|
+
handler: (err, msg) => {
|
|
600
|
+
if (err) return;
|
|
601
|
+
void decide(msg).catch((e) => {
|
|
602
|
+
logger.error("grant request failed", { error: describe(e) });
|
|
603
|
+
return refusal("the grant could not be made");
|
|
604
|
+
}).then((reply) => {
|
|
605
|
+
if (!reply.granted) logger.info("grant request refused", { reason: reply.reason });
|
|
606
|
+
msg.respond(encodeGrantReply(reply));
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
function parseGrantRequest(text2) {
|
|
612
|
+
let value;
|
|
613
|
+
try {
|
|
614
|
+
value = JSON.parse(text2);
|
|
615
|
+
} catch {
|
|
616
|
+
return void 0;
|
|
617
|
+
}
|
|
618
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
619
|
+
const { reference, grantee, trace } = value;
|
|
620
|
+
if (typeof reference !== "string" || typeof grantee !== "string") return void 0;
|
|
621
|
+
if (typeof trace !== "object" || trace === null) return { reference, grantee };
|
|
622
|
+
const { thread_id, root_id, tool_call_id } = trace;
|
|
623
|
+
if (typeof thread_id !== "string" || typeof root_id !== "string" || !isThreadId(thread_id) || !isThreadId(root_id)) {
|
|
624
|
+
return { reference, grantee };
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
reference,
|
|
628
|
+
grantee,
|
|
629
|
+
trace: {
|
|
630
|
+
thread_id,
|
|
631
|
+
root_id,
|
|
632
|
+
...typeof tool_call_id === "string" && validToolCallId(tool_call_id) ? { tool_call_id } : {}
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
function refusal(reason) {
|
|
637
|
+
return { granted: false, reason };
|
|
638
|
+
}
|
|
639
|
+
function describe(err) {
|
|
640
|
+
return err instanceof Error ? err.message : String(err);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/scratchpad-extension.ts
|
|
644
|
+
var READS_SCRATCHPAD_REFERENCES_FIELD = "reads_scratchpad_references";
|
|
645
|
+
var ScratchPadExtension = class {
|
|
646
|
+
/**
|
|
647
|
+
* For the agent's `AgentService` (`interceptors: [...]`): remembers the
|
|
648
|
+
* references in every prompt it serves.
|
|
649
|
+
*/
|
|
650
|
+
requestInterceptor;
|
|
651
|
+
#scratchPad;
|
|
652
|
+
#agents;
|
|
653
|
+
#discoverTimeoutMs;
|
|
654
|
+
#grantRequestTimeoutMs;
|
|
655
|
+
// Every reference this agent was given, exactly as it came.
|
|
656
|
+
#given = /* @__PURE__ */ new Set();
|
|
657
|
+
// Live handles by address, from discovery: a creator is asked only when
|
|
658
|
+
// it is on the fabric with a verified identity.
|
|
659
|
+
#known = /* @__PURE__ */ new Map();
|
|
660
|
+
constructor(options) {
|
|
661
|
+
this.#scratchPad = options.scratchPad;
|
|
662
|
+
this.#agents = options.agents;
|
|
663
|
+
this.#discoverTimeoutMs = options.discoverTimeoutMs;
|
|
664
|
+
this.#grantRequestTimeoutMs = options.grantRequestTimeoutMs;
|
|
665
|
+
this.requestInterceptor = {
|
|
666
|
+
aroundRequest: (ctx, next) => {
|
|
667
|
+
this.remember(ctx.envelope.prompt);
|
|
668
|
+
return next();
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Note the references in `text` as given to this agent: what it
|
|
674
|
+
* committed, or received some way the hooks do not see.
|
|
675
|
+
*/
|
|
676
|
+
remember(text2) {
|
|
677
|
+
for (const reference of findReferences(text2)) this.#given.add(reference);
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* The reply side: before a reply carrying `reference`, this agent's own,
|
|
681
|
+
* goes out, remember it and grant the served prompt's sender read on it,
|
|
682
|
+
* first-hand — only a sender whose signature verified, since a grant
|
|
683
|
+
* names a principal and an unsigned claim proves none. `true` when
|
|
684
|
+
* granted, so the reference may go into the reply; `false` for a sender
|
|
685
|
+
* that did not verify. Throws when the reference is not this agent's own
|
|
686
|
+
* or the grant fails. The commit itself stays the agent's.
|
|
687
|
+
*/
|
|
688
|
+
async grantCaller(reference, sender) {
|
|
689
|
+
this.remember(reference);
|
|
690
|
+
if (sender?.trust !== "verified") return false;
|
|
691
|
+
if (!await this.#scratchPad.owns(reference)) {
|
|
692
|
+
throw new Error("the reference points into another agent's volume; only its creator grants");
|
|
693
|
+
}
|
|
694
|
+
await this.#scratchPad.grantRead(
|
|
695
|
+
reference,
|
|
696
|
+
granteeFromIdentity(sender.id),
|
|
697
|
+
servedTrace(),
|
|
698
|
+
void 0,
|
|
699
|
+
"first-hand"
|
|
700
|
+
);
|
|
701
|
+
return true;
|
|
702
|
+
}
|
|
703
|
+
/** The discovery fields hook. */
|
|
704
|
+
discoveryFields(agent) {
|
|
705
|
+
this.#note(agent);
|
|
706
|
+
return { [READS_SCRATCHPAD_REFERENCES_FIELD]: readsReferences(agent.metadata) };
|
|
707
|
+
}
|
|
708
|
+
/** The prompt rewrite hook. */
|
|
709
|
+
async beforePrompt(ctx) {
|
|
710
|
+
const { address, target } = ctx;
|
|
711
|
+
const found = findReferences(ctx.prompt);
|
|
712
|
+
if (found.length === 0) return;
|
|
713
|
+
if (!readsReferences(target.metadata)) {
|
|
714
|
+
const prompt2 = removeReferences(ctx.prompt);
|
|
715
|
+
if (prompt2 === "") {
|
|
716
|
+
return {
|
|
717
|
+
error: `the prompt was not sent: "${address}" does not read ScratchPad references (discover_agents: reads_scratchpad_references is false), and the prompt held nothing but references; put the content they point at into the prompt itself`
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
return {
|
|
721
|
+
prompt: prompt2,
|
|
722
|
+
fields: {
|
|
723
|
+
references_removed: found.length,
|
|
724
|
+
note: `"${address}" does not read ScratchPad references: ${found.length} reference(s) were removed from your prompt, it got only the text, and nothing was granted`
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
const references = [...new Set(found)];
|
|
729
|
+
const unknown2 = references.filter((reference) => !this.#given.has(reference));
|
|
730
|
+
if (unknown2.length > 0) {
|
|
731
|
+
return {
|
|
732
|
+
error: `the prompt was not sent: ${unknown2.length} of its ScratchPad references (${unknown2[0].slice(0, 24)}\u2026) is not one you were given. Copy each reference exactly as you received it, the whole token, or leave it out.`
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
if (target.identity === void 0 || !target.idSigVerified) {
|
|
736
|
+
return {
|
|
737
|
+
error: `cannot hand a ScratchPad reference to "${address}": its identity is not verified, so no read grant can be issued; put the content in the prompt instead`
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
const grantee = granteeFromIdentity(target.identity);
|
|
741
|
+
const trace = servedTrace();
|
|
742
|
+
const toolCallId = ctx.toolCallId !== void 0 && validToolCallId(ctx.toolCallId) ? ctx.toolCallId : void 0;
|
|
743
|
+
const refused = [];
|
|
744
|
+
let reason = "";
|
|
745
|
+
for (const reference of references) {
|
|
746
|
+
try {
|
|
747
|
+
if (await this.#scratchPad.owns(reference)) {
|
|
748
|
+
await this.#scratchPad.grantRead(reference, grantee, trace, toolCallId, "first-hand");
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
} catch (err) {
|
|
752
|
+
return {
|
|
753
|
+
error: `could not grant "${address}" read on ${reference.slice(0, 32)}\u2026: ${describe2(err)}; the prompt was not sent`
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
const answer = await this.#askCreator(reference, address, target.identity, trace, toolCallId);
|
|
757
|
+
if (!answer.granted) {
|
|
758
|
+
refused.push(reference);
|
|
759
|
+
reason = answer.reason;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (refused.length === 0) return;
|
|
763
|
+
const removed = found.filter((reference) => refused.includes(reference)).length;
|
|
764
|
+
const prompt = removeReferences(ctx.prompt, refused);
|
|
765
|
+
if (prompt === "") {
|
|
766
|
+
return {
|
|
767
|
+
error: `the prompt was not sent: it held nothing but references, and their creator did not grant "${address}" read (${reason}); put the content they point at into the prompt itself`
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
return {
|
|
771
|
+
prompt,
|
|
772
|
+
fields: {
|
|
773
|
+
references_removed: removed,
|
|
774
|
+
note: `${removed} reference(s) were removed from your prompt, and "${address}" got only the text: you did not create that content, and its creator did not grant "${address}" read (${reason}). If it needs the content, put the content itself in a prompt`
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
/** The reply look hook. */
|
|
779
|
+
afterReply(ctx) {
|
|
780
|
+
this.remember(ctx.text);
|
|
781
|
+
}
|
|
782
|
+
// Ask the creator of a reference this agent was handed to grant the
|
|
783
|
+
// target read on it. The creator is the address the token names, and it
|
|
784
|
+
// must be on the fabric with a verified identity: a request to anyone
|
|
785
|
+
// else could not be answered with a grant anyway. A target that is the
|
|
786
|
+
// creator reads its own volume and needs no grant.
|
|
787
|
+
async #askCreator(reference, targetAddress, targetIdentity, trace, toolCallId) {
|
|
788
|
+
let address = referenceCreator(reference);
|
|
789
|
+
if (address === void 0 && this.#scratchPad.resolveReference) {
|
|
790
|
+
try {
|
|
791
|
+
address = (await this.#scratchPad.resolveReference(reference, trace)).creator;
|
|
792
|
+
} catch {
|
|
793
|
+
return { granted: false, reason: "the reference could not be resolved" };
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (address === void 0) {
|
|
797
|
+
return { granted: false, reason: "the reference does not name its creator" };
|
|
798
|
+
}
|
|
799
|
+
if (address === targetAddress) return { granted: true };
|
|
800
|
+
const creator = await this.#lookup(address);
|
|
801
|
+
if (creator === void 0 || !creator.idSigVerified) {
|
|
802
|
+
return {
|
|
803
|
+
granted: false,
|
|
804
|
+
reason: `its creator, ${address}, is not on the fabric with a verified identity`
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
const request = {
|
|
808
|
+
reference,
|
|
809
|
+
grantee: targetIdentity,
|
|
810
|
+
...trace !== void 0 ? {
|
|
811
|
+
trace: {
|
|
812
|
+
thread_id: trace.threadId,
|
|
813
|
+
root_id: trace.rootId,
|
|
814
|
+
...toolCallId !== void 0 ? { tool_call_id: toolCallId } : {}
|
|
815
|
+
}
|
|
816
|
+
} : {}
|
|
817
|
+
};
|
|
818
|
+
return requestGrant(
|
|
819
|
+
this.#agents,
|
|
820
|
+
address,
|
|
821
|
+
request,
|
|
822
|
+
this.#grantRequestTimeoutMs !== void 0 ? { timeoutMs: this.#grantRequestTimeoutMs } : {}
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
// The handle for an address: from discovery so far, else from a fresh one.
|
|
826
|
+
async #lookup(address) {
|
|
827
|
+
const known = this.#known.get(address);
|
|
828
|
+
if (known !== void 0) return known;
|
|
829
|
+
try {
|
|
830
|
+
const found = await this.#agents.discover(
|
|
831
|
+
this.#discoverTimeoutMs !== void 0 ? { timeoutMs: this.#discoverTimeoutMs } : {}
|
|
832
|
+
);
|
|
833
|
+
for (const handle of found) this.#note(handle);
|
|
834
|
+
} catch {
|
|
835
|
+
}
|
|
836
|
+
return this.#known.get(address);
|
|
837
|
+
}
|
|
838
|
+
// Remember a handle, preferring one whose identity verifies.
|
|
839
|
+
#note(handle) {
|
|
840
|
+
const known = this.#known.get(handle.promptSubject);
|
|
841
|
+
if (known === void 0 || handle.idSigVerified || !known.idSigVerified) {
|
|
842
|
+
this.#known.set(handle.promptSubject, handle);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
function scratchPadExtension(options) {
|
|
847
|
+
return new ScratchPadExtension(options);
|
|
848
|
+
}
|
|
849
|
+
function servedTrace() {
|
|
850
|
+
const scope = activeTrace();
|
|
851
|
+
return scope !== void 0 ? { threadId: scope.threadId, rootId: scope.rootId } : void 0;
|
|
852
|
+
}
|
|
853
|
+
function describe2(err) {
|
|
854
|
+
return err instanceof Error ? err.message : String(err);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// src/scratchpad-native/errors.ts
|
|
858
|
+
var NativeScratchPadError = class extends Error {
|
|
859
|
+
code;
|
|
860
|
+
unknown;
|
|
861
|
+
local;
|
|
862
|
+
constructor(code, message, options = {}) {
|
|
863
|
+
super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
|
|
864
|
+
this.name = "NativeScratchPadError";
|
|
865
|
+
this.code = code;
|
|
866
|
+
this.unknown = options.unknown ?? false;
|
|
867
|
+
this.local = options.local ?? false;
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
function invalid(message, cause) {
|
|
871
|
+
return new NativeScratchPadError("invalid", message, { local: true, cause });
|
|
872
|
+
}
|
|
873
|
+
function failed(mutation, message, cause) {
|
|
874
|
+
return new NativeScratchPadError(mutation ? "unknown" : "corrupt", message, {
|
|
875
|
+
unknown: mutation,
|
|
876
|
+
cause
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// node_modules/@unicode/unicode-15.0.0/General_Category/Unassigned/regex.mjs
|
|
881
|
+
var regex_default = /[\u0378\u0379\u0380-\u0383\u038B\u038D\u03A2\u0530\u0557\u0558\u058B\u058C\u0590\u05C8-\u05CF\u05EB-\u05EE\u05F5-\u05FF\u070E\u074B\u074C\u07B2-\u07BF\u07FB\u07FC\u082E\u082F\u083F\u085C\u085D\u085F\u086B-\u086F\u088F\u0892-\u0897\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FF\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A77-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0AF8\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B54\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0BFF\u0C0D\u0C11\u0C29\u0C3A\u0C3B\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5B\u0C5C\u0C5E\u0C5F\u0C64\u0C65\u0C70-\u0C76\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDC\u0CDF\u0CE4\u0CE5\u0CF0\u0CF4-\u0CFF\u0D0D\u0D11\u0D45\u0D49\u0D50-\u0D53\u0D64\u0D65\u0D80\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DE5\u0DF0\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E8B\u0EA4\u0EA6\u0EBE\u0EBF\u0EC5\u0EC7\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F6\u13F7\u13FE\u13FF\u169D-\u169F\u16F9-\u16FF\u1716-\u171E\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u181A-\u181F\u1879-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE\u1AAF\u1ACF-\u1AFF\u1B4D-\u1B4F\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C89-\u1C8F\u1CBB\u1CBC\u1CC8-\u1CCF\u1CFB-\u1CFF\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u2065\u2072\u2073\u208F\u209D-\u209F\u20C1-\u20CF\u20F1-\u20FF\u218C-\u218F\u2427-\u243F\u244B-\u245F\u2B74\u2B75\u2B96\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E5E-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u3130\u318F\u31E4-\u31EF\u321F\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA6F8-\uA6FF\uA7CB-\uA7CF\uA7D2\uA7D4\uA7DA-\uA7F1\uA82D-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C6-\uA8CD\uA8DA-\uA8DF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F\uAB6C-\uAB6F\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uD7FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC3-\uFBD2\uFD90\uFD91\uFDC8-\uFDCE\uFDD0-\uFDEF\uFE1A-\uFE1F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD\uFEFE\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFF8\uFFFE\uFFFF]|\uD800[\uDC0C\uDC27\uDC3B\uDC3E\uDC4E\uDC4F\uDC5E-\uDC7F\uDCFB-\uDCFF\uDD03-\uDD06\uDD34-\uDD36\uDD8F\uDD9D-\uDD9F\uDDA1-\uDDCF\uDDFE-\uDE7F\uDE9D-\uDE9F\uDED1-\uDEDF\uDEFC-\uDEFF\uDF24-\uDF2C\uDF4B-\uDF4F\uDF7B-\uDF7F\uDF9E\uDFC4-\uDFC7\uDFD6-\uDFFF]|\uD801[\uDC9E\uDC9F\uDCAA-\uDCAF\uDCD4-\uDCD7\uDCFC-\uDCFF\uDD28-\uDD2F\uDD64-\uDD6E\uDD7B\uDD8B\uDD93\uDD96\uDDA2\uDDB2\uDDBA\uDDBD-\uDDFF\uDF37-\uDF3F\uDF56-\uDF5F\uDF68-\uDF7F\uDF86\uDFB1\uDFBB-\uDFFF]|\uD802[\uDC06\uDC07\uDC09\uDC36\uDC39-\uDC3B\uDC3D\uDC3E\uDC56\uDC9F-\uDCA6\uDCB0-\uDCDF\uDCF3\uDCF6-\uDCFA\uDD1C-\uDD1E\uDD3A-\uDD3E\uDD40-\uDD7F\uDDB8-\uDDBB\uDDD0\uDDD1\uDE04\uDE07-\uDE0B\uDE14\uDE18\uDE36\uDE37\uDE3B-\uDE3E\uDE49-\uDE4F\uDE59-\uDE5F\uDEA0-\uDEBF\uDEE7-\uDEEA\uDEF7-\uDEFF\uDF36-\uDF38\uDF56\uDF57\uDF73-\uDF77\uDF92-\uDF98\uDF9D-\uDFA8\uDFB0-\uDFFF]|\uD803[\uDC49-\uDC7F\uDCB3-\uDCBF\uDCF3-\uDCF9\uDD28-\uDD2F\uDD3A-\uDE5F\uDE7F\uDEAA\uDEAE\uDEAF\uDEB2-\uDEFC\uDF28-\uDF2F\uDF5A-\uDF6F\uDF8A-\uDFAF\uDFCC-\uDFDF\uDFF7-\uDFFF]|\uD804[\uDC4E-\uDC51\uDC76-\uDC7E\uDCC3-\uDCCC\uDCCE\uDCCF\uDCE9-\uDCEF\uDCFA-\uDCFF\uDD35\uDD48-\uDD4F\uDD77-\uDD7F\uDDE0\uDDF5-\uDDFF\uDE12\uDE42-\uDE7F\uDE87\uDE89\uDE8E\uDE9E\uDEAA-\uDEAF\uDEEB-\uDEEF\uDEFA-\uDEFF\uDF04\uDF0D\uDF0E\uDF11\uDF12\uDF29\uDF31\uDF34\uDF3A\uDF45\uDF46\uDF49\uDF4A\uDF4E\uDF4F\uDF51-\uDF56\uDF58-\uDF5C\uDF64\uDF65\uDF6D-\uDF6F\uDF75-\uDFFF]|\uD805[\uDC5C\uDC62-\uDC7F\uDCC8-\uDCCF\uDCDA-\uDD7F\uDDB6\uDDB7\uDDDE-\uDDFF\uDE45-\uDE4F\uDE5A-\uDE5F\uDE6D-\uDE7F\uDEBA-\uDEBF\uDECA-\uDEFF\uDF1B\uDF1C\uDF2C-\uDF2F\uDF47-\uDFFF]|\uD806[\uDC3C-\uDC9F\uDCF3-\uDCFE\uDD07\uDD08\uDD0A\uDD0B\uDD14\uDD17\uDD36\uDD39\uDD3A\uDD47-\uDD4F\uDD5A-\uDD9F\uDDA8\uDDA9\uDDD8\uDDD9\uDDE5-\uDDFF\uDE48-\uDE4F\uDEA3-\uDEAF\uDEF9-\uDEFF\uDF0A-\uDFFF]|\uD807[\uDC09\uDC37\uDC46-\uDC4F\uDC6D-\uDC6F\uDC90\uDC91\uDCA8\uDCB7-\uDCFF\uDD07\uDD0A\uDD37-\uDD39\uDD3B\uDD3E\uDD48-\uDD4F\uDD5A-\uDD5F\uDD66\uDD69\uDD8F\uDD92\uDD99-\uDD9F\uDDAA-\uDEDF\uDEF9-\uDEFF\uDF11\uDF3B-\uDF3D\uDF5A-\uDFAF\uDFB1-\uDFBF\uDFF2-\uDFFE]|\uD808[\uDF9A-\uDFFF]|\uD809[\uDC6F\uDC75-\uDC7F\uDD44-\uDFFF]|[\uD80A\uD80E-\uD810\uD812-\uD819\uD824-\uD82A\uD82D\uD82E\uD830-\uD832\uD83F\uD87B-\uD87D\uD87F\uD889-\uDB3F\uDB41-\uDB7F][\uDC00-\uDFFF]|\uD80B[\uDC00-\uDF8F\uDFF3-\uDFFF]|\uD80D[\uDC56-\uDFFF]|\uD811[\uDE47-\uDFFF]|\uD81A[\uDE39-\uDE3F\uDE5F\uDE6A-\uDE6D\uDEBF\uDECA-\uDECF\uDEEE\uDEEF\uDEF6-\uDEFF\uDF46-\uDF4F\uDF5A\uDF62\uDF78-\uDF7C\uDF90-\uDFFF]|\uD81B[\uDC00-\uDE3F\uDE9B-\uDEFF\uDF4B-\uDF4E\uDF88-\uDF8E\uDFA0-\uDFDF\uDFE5-\uDFEF\uDFF2-\uDFFF]|\uD821[\uDFF8-\uDFFF]|\uD823[\uDCD6-\uDCFF\uDD09-\uDFFF]|\uD82B[\uDC00-\uDFEF\uDFF4\uDFFC\uDFFF]|\uD82C[\uDD23-\uDD31\uDD33-\uDD4F\uDD53\uDD54\uDD56-\uDD63\uDD68-\uDD6F\uDEFC-\uDFFF]|\uD82F[\uDC6B-\uDC6F\uDC7D-\uDC7F\uDC89-\uDC8F\uDC9A\uDC9B\uDCA4-\uDFFF]|\uD833[\uDC00-\uDEFF\uDF2E\uDF2F\uDF47-\uDF4F\uDFC4-\uDFFF]|\uD834[\uDCF6-\uDCFF\uDD27\uDD28\uDDEB-\uDDFF\uDE46-\uDEBF\uDED4-\uDEDF\uDEF4-\uDEFF\uDF57-\uDF5F\uDF79-\uDFFF]|\uD835[\uDC55\uDC9D\uDCA0\uDCA1\uDCA3\uDCA4\uDCA7\uDCA8\uDCAD\uDCBA\uDCBC\uDCC4\uDD06\uDD0B\uDD0C\uDD15\uDD1D\uDD3A\uDD3F\uDD45\uDD47-\uDD49\uDD51\uDEA6\uDEA7\uDFCC\uDFCD]|\uD836[\uDE8C-\uDE9A\uDEA0\uDEB0-\uDFFF]|\uD837[\uDC00-\uDEFF\uDF1F-\uDF24\uDF2B-\uDFFF]|\uD838[\uDC07\uDC19\uDC1A\uDC22\uDC25\uDC2B-\uDC2F\uDC6E-\uDC8E\uDC90-\uDCFF\uDD2D-\uDD2F\uDD3E\uDD3F\uDD4A-\uDD4D\uDD50-\uDE8F\uDEAF-\uDEBF\uDEFA-\uDEFE\uDF00-\uDFFF]|\uD839[\uDC00-\uDCCF\uDCFA-\uDFDF\uDFE7\uDFEC\uDFEF\uDFFF]|\uD83A[\uDCC5\uDCC6\uDCD7-\uDCFF\uDD4C-\uDD4F\uDD5A-\uDD5D\uDD60-\uDFFF]|\uD83B[\uDC00-\uDC70\uDCB5-\uDD00\uDD3E-\uDDFF\uDE04\uDE20\uDE23\uDE25\uDE26\uDE28\uDE33\uDE38\uDE3A\uDE3C-\uDE41\uDE43-\uDE46\uDE48\uDE4A\uDE4C\uDE50\uDE53\uDE55\uDE56\uDE58\uDE5A\uDE5C\uDE5E\uDE60\uDE63\uDE65\uDE66\uDE6B\uDE73\uDE78\uDE7D\uDE7F\uDE8A\uDE9C-\uDEA0\uDEA4\uDEAA\uDEBC-\uDEEF\uDEF2-\uDFFF]|\uD83C[\uDC2C-\uDC2F\uDC94-\uDC9F\uDCAF\uDCB0\uDCC0\uDCD0\uDCF6-\uDCFF\uDDAE-\uDDE5\uDE03-\uDE0F\uDE3C-\uDE3F\uDE49-\uDE4F\uDE52-\uDE5F\uDE66-\uDEFF]|\uD83D[\uDED8-\uDEDB\uDEED-\uDEEF\uDEFD-\uDEFF\uDF77-\uDF7A\uDFDA-\uDFDF\uDFEC-\uDFEF\uDFF1-\uDFFF]|\uD83E[\uDC0C-\uDC0F\uDC48-\uDC4F\uDC5A-\uDC5F\uDC88-\uDC8F\uDCAE\uDCAF\uDCB2-\uDCFF\uDE54-\uDE5F\uDE6E\uDE6F\uDE7D-\uDE7F\uDE89-\uDE8F\uDEBE\uDEC6-\uDECD\uDEDC-\uDEDF\uDEE9-\uDEEF\uDEF9-\uDEFF\uDF93\uDFCB-\uDFEF\uDFFA-\uDFFF]|\uD869[\uDEE0-\uDEFF]|\uD86D[\uDF3A-\uDF3F]|\uD86E[\uDC1E\uDC1F]|\uD873[\uDEA2-\uDEAF]|\uD87A[\uDFE1-\uDFFF]|\uD87E[\uDE1E-\uDFFF]|\uD884[\uDF4B-\uDF4F]|\uD888[\uDFB0-\uDFFF]|\uDB40[\uDC00\uDC02-\uDC1F\uDC80-\uDCFF\uDDF0-\uDFFF]|[\uDBBF\uDBFF][\uDFFE\uDFFF]/;
|
|
882
|
+
|
|
883
|
+
// src/scratchpad-native/protocol.ts
|
|
884
|
+
var SUBJECT_PREFIX = "sp.v1.api.v2.";
|
|
885
|
+
var METADATA_HEADER = "ScratchPad-Metadata";
|
|
886
|
+
var REPLY_HEADER = "ScratchPad-Reply";
|
|
887
|
+
var CHUNK_SIZE = 512 * 1024;
|
|
888
|
+
var MAX_NODE_BYTES = 900 * 1024;
|
|
889
|
+
var MAX_CHUNKS = 20480;
|
|
890
|
+
var MAX_TREE_ENTRIES = 8192;
|
|
891
|
+
var MAX_DEPTH = 128;
|
|
892
|
+
var MAX_CONTROL_BYTES = 16 * 1024;
|
|
893
|
+
var MAX_REPLY_BYTES = 128 * 1024;
|
|
894
|
+
var MAX_HEADER_BYTES = 8 * 1024;
|
|
895
|
+
var MAX_READ_BYTES = CHUNK_SIZE;
|
|
896
|
+
var MAX_PREPARE_OBJECTS = 64;
|
|
897
|
+
var MAX_VOLUME_OBJECTS = 65536;
|
|
898
|
+
var MAX_VOLUME_BYTES = 8 * 2 ** 30;
|
|
899
|
+
var WRITER_SLOTS = 8;
|
|
900
|
+
var RETENTION_SLOTS = 8;
|
|
901
|
+
var GRANT_SLOTS = 16;
|
|
902
|
+
var MAX_GRANT_SECONDS = 3600;
|
|
903
|
+
var ZERO_CONTENT_ID = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
|
904
|
+
var CODES = [
|
|
905
|
+
"ok",
|
|
906
|
+
"invalid",
|
|
907
|
+
"unauthenticated",
|
|
908
|
+
"denied",
|
|
909
|
+
"not_found",
|
|
910
|
+
"exists",
|
|
911
|
+
"conflict",
|
|
912
|
+
"fenced",
|
|
913
|
+
"intent_changed",
|
|
914
|
+
"capacity",
|
|
915
|
+
"summary_limit",
|
|
916
|
+
"closed",
|
|
917
|
+
"epoch_changed",
|
|
918
|
+
"expired",
|
|
919
|
+
"corrupt",
|
|
920
|
+
"incomplete",
|
|
921
|
+
"busy",
|
|
922
|
+
"unknown",
|
|
923
|
+
"unavailable",
|
|
924
|
+
"internal"
|
|
925
|
+
];
|
|
926
|
+
var PUBLIC_LIMITS = Object.freeze({
|
|
927
|
+
object_bytes: MAX_NODE_BYTES,
|
|
928
|
+
read_bytes: MAX_READ_BYTES,
|
|
929
|
+
control_bytes: MAX_CONTROL_BYTES,
|
|
930
|
+
reply_bytes: MAX_REPLY_BYTES,
|
|
931
|
+
writers: WRITER_SLOTS,
|
|
932
|
+
retentions: RETENTION_SLOTS,
|
|
933
|
+
grants: GRANT_SLOTS,
|
|
934
|
+
grant_seconds: MAX_GRANT_SECONDS
|
|
935
|
+
});
|
|
936
|
+
function check(condition, message) {
|
|
937
|
+
if (!condition) throw invalid(message);
|
|
938
|
+
}
|
|
939
|
+
function record(value, required, optional = []) {
|
|
940
|
+
check(
|
|
941
|
+
typeof value === "object" && value !== null && !Array.isArray(value),
|
|
942
|
+
"expected JSON object"
|
|
943
|
+
);
|
|
944
|
+
const result = value;
|
|
945
|
+
check(
|
|
946
|
+
required.every((key) => Object.hasOwn(result, key)),
|
|
947
|
+
"required field is absent"
|
|
948
|
+
);
|
|
949
|
+
check(
|
|
950
|
+
Object.keys(result).every((key) => required.includes(key) || optional.includes(key)),
|
|
951
|
+
"unknown field"
|
|
952
|
+
);
|
|
953
|
+
return result;
|
|
954
|
+
}
|
|
955
|
+
function uint(value, max = Number.MAX_SAFE_INTEGER, min = 0) {
|
|
956
|
+
check(
|
|
957
|
+
typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= max,
|
|
958
|
+
"integer outside its bound"
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
function bool(value) {
|
|
962
|
+
check(typeof value === "boolean", "expected boolean");
|
|
963
|
+
}
|
|
964
|
+
function text(value, max, min = 0) {
|
|
965
|
+
check(
|
|
966
|
+
typeof value === "string" && Buffer.from(value).toString("utf8") === value,
|
|
967
|
+
"invalid Unicode text"
|
|
968
|
+
);
|
|
969
|
+
check(
|
|
970
|
+
Buffer.byteLength(value) <= max && Buffer.byteLength(value) >= min,
|
|
971
|
+
"text outside its byte bound"
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
function validateContentID(value) {
|
|
975
|
+
check(typeof value === "string" && /^[A-Za-z0-9_-]{43}$/.test(value), "invalid content identity");
|
|
976
|
+
const bytes = Buffer.from(value, "base64url");
|
|
977
|
+
check(
|
|
978
|
+
bytes.length === 32 && bytes.toString("base64url") === value,
|
|
979
|
+
"noncanonical content identity"
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
function validatePublicKey(value, kind) {
|
|
983
|
+
check(typeof value === "string" && /^[A-Z2-7]{56}$/.test(value), "invalid public NKEY");
|
|
984
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
985
|
+
let bits = 0, pending = 0;
|
|
986
|
+
const data = [];
|
|
987
|
+
for (const character of value) {
|
|
988
|
+
pending = pending << 5 | alphabet.indexOf(character);
|
|
989
|
+
bits += 5;
|
|
990
|
+
if (bits >= 8) {
|
|
991
|
+
bits -= 8;
|
|
992
|
+
data.push(pending >>> bits & 255);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
check(data.length === 35 && data[0] === (kind === "account" ? 0 : 160), "wrong NKEY kind");
|
|
996
|
+
let crc = 0;
|
|
997
|
+
for (const byte of data.slice(0, 33)) {
|
|
998
|
+
crc ^= byte << 8;
|
|
999
|
+
for (let i = 0; i < 8; i++) crc = (crc << 1 ^ (crc & 32768 ? 4129 : 0)) & 65535;
|
|
1000
|
+
}
|
|
1001
|
+
check(data[33] === (crc & 255) && data[34] === crc >>> 8, "NKEY checksum differs");
|
|
1002
|
+
}
|
|
1003
|
+
function validatePrincipal(value) {
|
|
1004
|
+
const v = record(value, ["account", "user"]);
|
|
1005
|
+
validatePublicKey(v.account, "account");
|
|
1006
|
+
validatePublicKey(v.user, "user");
|
|
1007
|
+
}
|
|
1008
|
+
function validateName(value) {
|
|
1009
|
+
text(value, 255, 1);
|
|
1010
|
+
check(value !== "." && value !== ".." && !/[\x00-\x1f\x7f/]/.test(value), "forbidden entry name");
|
|
1011
|
+
check(normalizeUnicode15(value) === value, "entry name is not NFC");
|
|
1012
|
+
}
|
|
1013
|
+
function normalizeUnicode15(value) {
|
|
1014
|
+
let result = "", assigned = "";
|
|
1015
|
+
for (const point of value) {
|
|
1016
|
+
if (regex_default.test(point)) {
|
|
1017
|
+
result += assigned.normalize("NFC") + point;
|
|
1018
|
+
assigned = "";
|
|
1019
|
+
} else assigned += point;
|
|
1020
|
+
}
|
|
1021
|
+
return result + assigned.normalize("NFC");
|
|
1022
|
+
}
|
|
1023
|
+
function validatePath(value, maxBytes = 4096) {
|
|
1024
|
+
text(value, maxBytes, 1);
|
|
1025
|
+
check(value.startsWith("/"), "path must be absolute");
|
|
1026
|
+
if (value === "/") return;
|
|
1027
|
+
const names = value.slice(1).split("/");
|
|
1028
|
+
check(names.length <= MAX_DEPTH, "path exceeds its depth bound");
|
|
1029
|
+
for (const name of names) validateName(name);
|
|
1030
|
+
}
|
|
1031
|
+
function validateVolume(value) {
|
|
1032
|
+
check(typeof value === "string" && /^[0-9A-Za-z]{1,64}$/.test(value), "invalid volume name");
|
|
1033
|
+
}
|
|
1034
|
+
function validateTarget(value) {
|
|
1035
|
+
const v = record(value, ["volume", "creation_id"]);
|
|
1036
|
+
validateVolume(v.volume);
|
|
1037
|
+
check(
|
|
1038
|
+
typeof v.creation_id === "string" && /^[0-9a-f]{32}$/.test(v.creation_id),
|
|
1039
|
+
"invalid volume creation identity"
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
function validateTrace(value) {
|
|
1043
|
+
const v = record(value, [], ["thread_id", "root_id", "tool_call_id"]);
|
|
1044
|
+
for (const field of ["thread_id", "root_id"]) {
|
|
1045
|
+
if (v[field] !== void 0) text(v[field], 32);
|
|
1046
|
+
}
|
|
1047
|
+
if (v.tool_call_id !== void 0) {
|
|
1048
|
+
text(v.tool_call_id, 1024);
|
|
1049
|
+
check([...v.tool_call_id].length <= 256, "tool-call label is too long");
|
|
1050
|
+
}
|
|
1051
|
+
if (!v.thread_id && !v.root_id && !v.tool_call_id) return;
|
|
1052
|
+
check(
|
|
1053
|
+
typeof v.thread_id === "string" && /^[0-9a-f]{32}$/.test(v.thread_id),
|
|
1054
|
+
"invalid trace thread"
|
|
1055
|
+
);
|
|
1056
|
+
check(typeof v.root_id === "string" && /^[0-9a-f]{32}$/.test(v.root_id), "invalid trace root");
|
|
1057
|
+
}
|
|
1058
|
+
function validateAction(value) {
|
|
1059
|
+
const v = record(value, ["operation_id", "trace"]);
|
|
1060
|
+
text(v.operation_id, 256, 1);
|
|
1061
|
+
check([...v.operation_id].length <= 64, "operation identity is too long");
|
|
1062
|
+
validateTrace(v.trace);
|
|
1063
|
+
}
|
|
1064
|
+
function validateHead(value) {
|
|
1065
|
+
const v = record(value, ["generation", "root"]);
|
|
1066
|
+
uint(v.generation, void 0, 1);
|
|
1067
|
+
validateContentID(v.root);
|
|
1068
|
+
}
|
|
1069
|
+
function validateWriter(value) {
|
|
1070
|
+
const v = record(value, ["slot", "generation"]);
|
|
1071
|
+
uint(v.slot, WRITER_SLOTS - 1);
|
|
1072
|
+
uint(v.generation, void 0, 1);
|
|
1073
|
+
}
|
|
1074
|
+
function validateRetentionHandle(value) {
|
|
1075
|
+
const v = record(value, ["slot", "generation"]);
|
|
1076
|
+
uint(v.slot, RETENTION_SLOTS - 1);
|
|
1077
|
+
uint(v.generation, void 0, 1);
|
|
1078
|
+
}
|
|
1079
|
+
function validateGrantHandle(value) {
|
|
1080
|
+
const v = record(value, ["slot", "generation"]);
|
|
1081
|
+
uint(v.slot, GRANT_SLOTS - 1);
|
|
1082
|
+
uint(v.generation, void 0, 1);
|
|
1083
|
+
}
|
|
1084
|
+
function validateCommand(value) {
|
|
1085
|
+
const v = record(value, [
|
|
1086
|
+
"creation_id",
|
|
1087
|
+
"writer",
|
|
1088
|
+
"number",
|
|
1089
|
+
"operation_id",
|
|
1090
|
+
"base",
|
|
1091
|
+
"root",
|
|
1092
|
+
"trace",
|
|
1093
|
+
"retention"
|
|
1094
|
+
]);
|
|
1095
|
+
validateTarget({ volume: "v", creation_id: v.creation_id });
|
|
1096
|
+
validateWriter(v.writer);
|
|
1097
|
+
uint(v.number, void 0, 1);
|
|
1098
|
+
validateAction({ operation_id: v.operation_id, trace: v.trace });
|
|
1099
|
+
validateHead(v.base);
|
|
1100
|
+
validateContentID(v.root);
|
|
1101
|
+
const r = record(v.retention, ["enabled", "slot", "generation", "until"]);
|
|
1102
|
+
bool(r.enabled);
|
|
1103
|
+
uint(r.slot, RETENTION_SLOTS - 1);
|
|
1104
|
+
uint(r.generation);
|
|
1105
|
+
uint(r.until);
|
|
1106
|
+
check(
|
|
1107
|
+
r.enabled || r.slot === 0 && r.generation === 0 && r.until === 0,
|
|
1108
|
+
"disabled retention must be zero"
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
function validateReceipt(value) {
|
|
1112
|
+
const v = record(value, ["volume", "principal", "command", "head", "retention"]);
|
|
1113
|
+
validateVolume(v.volume);
|
|
1114
|
+
validatePrincipal(v.principal);
|
|
1115
|
+
validateCommand(v.command);
|
|
1116
|
+
validateHead(v.head);
|
|
1117
|
+
check(
|
|
1118
|
+
v.head.root === v.command.root && v.head.generation - v.command.base.generation === 1,
|
|
1119
|
+
"receipt head differs from command"
|
|
1120
|
+
);
|
|
1121
|
+
if (v.command.retention.enabled) {
|
|
1122
|
+
validateRetentionHandle(v.retention);
|
|
1123
|
+
check(
|
|
1124
|
+
v.retention.slot === v.command.retention.slot && v.retention.generation > v.command.retention.generation,
|
|
1125
|
+
"receipt retention differs from command"
|
|
1126
|
+
);
|
|
1127
|
+
} else {
|
|
1128
|
+
const retention = record(v.retention, ["slot", "generation"]);
|
|
1129
|
+
check(retention.slot === 0 && retention.generation === 0, "unexpected receipt retention");
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
function sameValue(a, b) {
|
|
1133
|
+
if (a === b) return true;
|
|
1134
|
+
if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
|
|
1135
|
+
const left = a, right = b;
|
|
1136
|
+
return Object.keys(left).length === Object.keys(right).length && Object.keys(left).every((key) => Object.hasOwn(right, key) && sameValue(left[key], right[key]));
|
|
1137
|
+
}
|
|
1138
|
+
function validateVolumeView(value) {
|
|
1139
|
+
const v = record(value, [
|
|
1140
|
+
"target",
|
|
1141
|
+
"owner",
|
|
1142
|
+
"head",
|
|
1143
|
+
"revision",
|
|
1144
|
+
"closed",
|
|
1145
|
+
"epoch",
|
|
1146
|
+
"objects",
|
|
1147
|
+
"bytes",
|
|
1148
|
+
"writers",
|
|
1149
|
+
"retentions",
|
|
1150
|
+
"grants"
|
|
1151
|
+
]);
|
|
1152
|
+
validateTarget(v.target);
|
|
1153
|
+
validatePrincipal(v.owner);
|
|
1154
|
+
validateHead(v.head);
|
|
1155
|
+
uint(v.revision, void 0, 1);
|
|
1156
|
+
bool(v.closed);
|
|
1157
|
+
uint(v.epoch, v.revision);
|
|
1158
|
+
uint(v.objects, MAX_VOLUME_OBJECTS);
|
|
1159
|
+
uint(v.bytes, MAX_VOLUME_BYTES);
|
|
1160
|
+
check(
|
|
1161
|
+
v.head.generation <= v.revision && (!v.closed || v.epoch === v.revision),
|
|
1162
|
+
"inconsistent volume revision"
|
|
1163
|
+
);
|
|
1164
|
+
check(Array.isArray(v.writers) && v.writers.length === WRITER_SLOTS, "wrong writer slot count");
|
|
1165
|
+
for (const [index, raw] of v.writers.entries()) {
|
|
1166
|
+
const slot = record(raw, ["generation", "number", "write"], ["principal", "receipt"]);
|
|
1167
|
+
uint(slot.generation, v.revision, 1);
|
|
1168
|
+
uint(slot.number);
|
|
1169
|
+
bool(slot.write);
|
|
1170
|
+
if (slot.principal !== void 0) {
|
|
1171
|
+
validatePrincipal(slot.principal);
|
|
1172
|
+
check(slot.principal.account === v.owner.account, "writer belongs to another account");
|
|
1173
|
+
} else check(!slot.write && slot.number === 0, "writer identity absent");
|
|
1174
|
+
if (slot.number === 0) check(slot.receipt === void 0, "unused writer has a receipt");
|
|
1175
|
+
else {
|
|
1176
|
+
validateReceipt(slot.receipt);
|
|
1177
|
+
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,
|
|
1179
|
+
"writer receipt differs from slot"
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
check(
|
|
1184
|
+
Array.isArray(v.retentions) && v.retentions.length === RETENTION_SLOTS,
|
|
1185
|
+
"wrong retention slot count"
|
|
1186
|
+
);
|
|
1187
|
+
for (const raw of v.retentions) {
|
|
1188
|
+
const slot = record(raw, ["generation"], ["pin"]);
|
|
1189
|
+
uint(slot.generation, v.revision);
|
|
1190
|
+
if (slot.pin !== void 0) {
|
|
1191
|
+
check(slot.generation > 0, "retention pin has zero generation");
|
|
1192
|
+
const pin = record(slot.pin, ["root", "until", "origin"]);
|
|
1193
|
+
validateContentID(pin.root);
|
|
1194
|
+
uint(pin.until);
|
|
1195
|
+
const origin = record(pin.origin, ["writer", "number"]);
|
|
1196
|
+
uint(origin.number);
|
|
1197
|
+
if (origin.number === 0) {
|
|
1198
|
+
const writer = record(origin.writer, ["slot", "generation"]);
|
|
1199
|
+
check(writer.slot === 0 && writer.generation === 0, "empty retention origin is not zero");
|
|
1200
|
+
} else {
|
|
1201
|
+
validateWriter(origin.writer);
|
|
1202
|
+
check(origin.writer.generation < slot.generation, "retention origin generation is invalid");
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
check(Array.isArray(v.grants) && v.grants.length === GRANT_SLOTS, "wrong grant slot count");
|
|
1207
|
+
for (const raw of v.grants) {
|
|
1208
|
+
const slot = record(raw, ["generation"], ["grant"]);
|
|
1209
|
+
uint(slot.generation, v.revision);
|
|
1210
|
+
if (slot.grant !== void 0) {
|
|
1211
|
+
check(slot.generation > 0, "grant has zero generation");
|
|
1212
|
+
const grant = record(slot.grant, [
|
|
1213
|
+
"principal",
|
|
1214
|
+
"root",
|
|
1215
|
+
"path",
|
|
1216
|
+
"directory",
|
|
1217
|
+
"retention",
|
|
1218
|
+
"until"
|
|
1219
|
+
]);
|
|
1220
|
+
validatePrincipal(grant.principal);
|
|
1221
|
+
validateContentID(grant.root);
|
|
1222
|
+
validatePath(grant.path, 256);
|
|
1223
|
+
bool(grant.directory);
|
|
1224
|
+
validateRetentionHandle(grant.retention);
|
|
1225
|
+
uint(grant.until, void 0, 1);
|
|
1226
|
+
check(
|
|
1227
|
+
grant.principal.account === v.owner.account && grant.retention.generation < slot.generation,
|
|
1228
|
+
"invalid grant principal or retention generation"
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
function validateInfo(value) {
|
|
1234
|
+
const v = record(value, ["principal", "scopes", "limits"]);
|
|
1235
|
+
validatePrincipal(v.principal);
|
|
1236
|
+
check(sameValue(v.limits, PUBLIC_LIMITS), "service limits differ from this client contract");
|
|
1237
|
+
check(Array.isArray(v.scopes) && v.scopes.length <= 16, "scope count exceeds bound");
|
|
1238
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1239
|
+
for (const raw of v.scopes) {
|
|
1240
|
+
const scope = record(raw, ["volume", "objects", "bytes"]);
|
|
1241
|
+
validateVolume(scope.volume);
|
|
1242
|
+
uint(scope.objects, MAX_VOLUME_OBJECTS, 1);
|
|
1243
|
+
uint(scope.bytes, MAX_VOLUME_BYTES, 4);
|
|
1244
|
+
check(!seen.has(scope.volume), "duplicate scope");
|
|
1245
|
+
seen.add(scope.volume);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
function validateContentObject(value) {
|
|
1249
|
+
const v = record(value, ["kind", "id"]);
|
|
1250
|
+
validateContentID(v.id);
|
|
1251
|
+
check(v.kind === "chnk" || v.kind === "fobj" || v.kind === "tree", "unsupported object kind");
|
|
1252
|
+
}
|
|
1253
|
+
function validateObjectSpec(value) {
|
|
1254
|
+
const v = record(value, ["object", "length"]);
|
|
1255
|
+
validateContentObject(v.object);
|
|
1256
|
+
uint(v.length, v.object.kind === "chnk" ? CHUNK_SIZE : MAX_NODE_BYTES, 1);
|
|
1257
|
+
}
|
|
1258
|
+
function validateProgress(value, preparation) {
|
|
1259
|
+
const v = record(value, ["object", "length", "state", "next", "code"]);
|
|
1260
|
+
validateObjectSpec({ object: v.object, length: v.length });
|
|
1261
|
+
validateContentObject(v.object);
|
|
1262
|
+
uint(v.next);
|
|
1263
|
+
if (v.state === "refused") {
|
|
1264
|
+
check(preparation && v.next === 0 && v.code === "capacity", "invalid refusal state");
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
check(v.code === "ok", "invalid progress code");
|
|
1268
|
+
if (v.state === "reserved" || v.state === "verified")
|
|
1269
|
+
check(v.next === 0, "unexpected progress cursor");
|
|
1270
|
+
else {
|
|
1271
|
+
check(v.state === "verifying" && v.object.kind !== "chnk", "invalid progress state");
|
|
1272
|
+
uint(v.next, (v.object.kind === "tree" ? MAX_TREE_ENTRIES : MAX_CHUNKS) - 1, 1);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
function validateProgressResult(value, preparation) {
|
|
1276
|
+
const v = record(value, ["epoch", "objects"]);
|
|
1277
|
+
uint(v.epoch);
|
|
1278
|
+
check(
|
|
1279
|
+
Array.isArray(v.objects) && v.objects.length > 0 && v.objects.length <= MAX_PREPARE_OBJECTS,
|
|
1280
|
+
"invalid progress count"
|
|
1281
|
+
);
|
|
1282
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1283
|
+
for (const raw of v.objects) {
|
|
1284
|
+
validateProgress(raw, preparation);
|
|
1285
|
+
const key = raw.object.kind + ":" + raw.object.id;
|
|
1286
|
+
check(!seen.has(key), "duplicate object progress");
|
|
1287
|
+
seen.add(key);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function validatePrepareResult(value) {
|
|
1291
|
+
validateProgressResult(value, true);
|
|
1292
|
+
}
|
|
1293
|
+
function validateVerifyResult(value) {
|
|
1294
|
+
validateProgressResult(value, false);
|
|
1295
|
+
}
|
|
1296
|
+
function validateEntryBody(v) {
|
|
1297
|
+
validateContentID(v.object);
|
|
1298
|
+
bool(v.executable);
|
|
1299
|
+
if (v.type === "symlink") {
|
|
1300
|
+
text(v.link_target, 4096, 1);
|
|
1301
|
+
check(
|
|
1302
|
+
!v.link_target.includes("\0") && !v.executable && v.object === ZERO_CONTENT_ID,
|
|
1303
|
+
"invalid symlink metadata"
|
|
1304
|
+
);
|
|
1305
|
+
} else {
|
|
1306
|
+
check(v.type === "file" || v.type === "directory", "invalid entry type");
|
|
1307
|
+
check(v.link_target === void 0 || v.link_target === "", "unexpected symlink target");
|
|
1308
|
+
check(v.type === "file" || !v.executable, "directory is executable");
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
function validateStat(value) {
|
|
1312
|
+
const v = record(value, ["type", "object", "size", "sha256", "executable"], ["link_target"]);
|
|
1313
|
+
validateEntryBody(v);
|
|
1314
|
+
uint(v.size, MAX_VOLUME_BYTES);
|
|
1315
|
+
if (v.type === "file")
|
|
1316
|
+
check(typeof v.sha256 === "string" && /^[0-9a-f]{64}$/.test(v.sha256), "invalid file digest");
|
|
1317
|
+
else check(v.size === 0 && v.sha256 === "", "non-file metadata contains file bytes");
|
|
1318
|
+
}
|
|
1319
|
+
function validateListResult(value) {
|
|
1320
|
+
const v = record(value, ["entries", "next", "done"]);
|
|
1321
|
+
uint(v.next, MAX_TREE_ENTRIES);
|
|
1322
|
+
bool(v.done);
|
|
1323
|
+
check(
|
|
1324
|
+
Array.isArray(v.entries) && v.entries.length <= 64 && v.entries.length <= v.next && (v.done || v.entries.length > 0),
|
|
1325
|
+
"invalid list window"
|
|
1326
|
+
);
|
|
1327
|
+
let last = Buffer.alloc(0), bytes = 0;
|
|
1328
|
+
for (const raw of v.entries) {
|
|
1329
|
+
const entry = record(raw, ["name", "type", "object", "executable"], ["link_target"]);
|
|
1330
|
+
validateName(entry.name);
|
|
1331
|
+
validateEntryBody(entry);
|
|
1332
|
+
const name = Buffer.from(entry.name);
|
|
1333
|
+
check(Buffer.compare(last, name) < 0, "list names are not strictly ordered");
|
|
1334
|
+
last = name;
|
|
1335
|
+
bytes += Buffer.byteLength(goJSON(entry)) + 1;
|
|
1336
|
+
}
|
|
1337
|
+
check(bytes <= 64 * 1024, "list bytes exceed bound");
|
|
1338
|
+
}
|
|
1339
|
+
function validateReplyMetadata(value) {
|
|
1340
|
+
const v = record(value, ["code", "message"], ["range"]);
|
|
1341
|
+
check(
|
|
1342
|
+
typeof v.code === "string" && CODES.includes(v.code),
|
|
1343
|
+
"unknown reply code"
|
|
1344
|
+
);
|
|
1345
|
+
text(v.message, 256);
|
|
1346
|
+
if (v.code !== "ok") {
|
|
1347
|
+
check(v.message !== "" && v.range === void 0, "invalid error reply");
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
check(v.message === "", "success reply has an error message");
|
|
1351
|
+
if (v.range !== void 0) {
|
|
1352
|
+
const range = record(v.range, ["stat", "offset", "length"]);
|
|
1353
|
+
validateStat(range.stat);
|
|
1354
|
+
check(range.stat.type === "file", "range is not a file");
|
|
1355
|
+
uint(range.offset, range.stat.size);
|
|
1356
|
+
uint(range.length, MAX_READ_BYTES);
|
|
1357
|
+
check(range.length <= range.stat.size - range.offset, "range exceeds file");
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
function validateCollectionReport(value) {
|
|
1361
|
+
const v = record(value, [
|
|
1362
|
+
"inventory_objects",
|
|
1363
|
+
"inventory_bytes",
|
|
1364
|
+
"live_objects",
|
|
1365
|
+
"live_bytes",
|
|
1366
|
+
"candidate_objects",
|
|
1367
|
+
"candidate_bytes",
|
|
1368
|
+
"delete_objects"
|
|
1369
|
+
]);
|
|
1370
|
+
uint(v.inventory_objects, MAX_VOLUME_OBJECTS);
|
|
1371
|
+
uint(v.inventory_bytes, MAX_VOLUME_BYTES);
|
|
1372
|
+
uint(v.live_objects, v.inventory_objects);
|
|
1373
|
+
uint(v.live_bytes, v.inventory_bytes);
|
|
1374
|
+
uint(v.candidate_objects);
|
|
1375
|
+
uint(v.candidate_bytes);
|
|
1376
|
+
uint(v.delete_objects);
|
|
1377
|
+
check(
|
|
1378
|
+
v.candidate_objects === v.inventory_objects - v.live_objects && v.candidate_bytes === v.inventory_bytes - v.live_bytes && v.delete_objects <= v.candidate_objects,
|
|
1379
|
+
"collection counters differ"
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1382
|
+
function validateCollectionResult(value) {
|
|
1383
|
+
const v = record(value, ["volume", "collection"]);
|
|
1384
|
+
validateVolumeView(v.volume);
|
|
1385
|
+
validateCollectionReport(v.collection);
|
|
1386
|
+
check(
|
|
1387
|
+
!v.volume.closed && v.volume.objects === v.collection.live_objects && v.volume.bytes === v.collection.live_bytes,
|
|
1388
|
+
"collection does not match reopened volume"
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
function validateEmpty(value) {
|
|
1392
|
+
record(value, []);
|
|
1393
|
+
}
|
|
1394
|
+
function goJSON(value, escapeHTML = true) {
|
|
1395
|
+
let encoded = JSON.stringify(value);
|
|
1396
|
+
check(encoded !== void 0, "value cannot be encoded as JSON");
|
|
1397
|
+
encoded = encoded.replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
1398
|
+
return escapeHTML ? encoded.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026") : encoded;
|
|
1399
|
+
}
|
|
1400
|
+
function parseStrictJSON(data, limit) {
|
|
1401
|
+
const bytes = typeof data === "string" ? Buffer.from(data) : Buffer.from(data);
|
|
1402
|
+
check(bytes.length <= limit, "JSON exceeds its byte bound");
|
|
1403
|
+
const source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1404
|
+
let cursor = 0;
|
|
1405
|
+
const whitespace = () => {
|
|
1406
|
+
while (/[\t\n\r ]/.test(source[cursor] ?? "!")) cursor++;
|
|
1407
|
+
};
|
|
1408
|
+
const string = () => {
|
|
1409
|
+
const start = cursor++;
|
|
1410
|
+
for (; ; ) {
|
|
1411
|
+
check(cursor < source.length, "unterminated JSON string");
|
|
1412
|
+
const char = source[cursor++];
|
|
1413
|
+
if (char === "\\") {
|
|
1414
|
+
cursor++;
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
if (char === '"') break;
|
|
1418
|
+
}
|
|
1419
|
+
const value2 = JSON.parse(source.slice(start, cursor));
|
|
1420
|
+
text(value2, limit);
|
|
1421
|
+
return value2;
|
|
1422
|
+
};
|
|
1423
|
+
const value = (depth) => {
|
|
1424
|
+
check(depth <= 64, "JSON nesting exceeds bound");
|
|
1425
|
+
whitespace();
|
|
1426
|
+
const first = source[cursor];
|
|
1427
|
+
if (first === '"') return string();
|
|
1428
|
+
if (first === "{") {
|
|
1429
|
+
cursor++;
|
|
1430
|
+
whitespace();
|
|
1431
|
+
const result2 = /* @__PURE__ */ Object.create(null);
|
|
1432
|
+
if (source[cursor] === "}") {
|
|
1433
|
+
cursor++;
|
|
1434
|
+
return result2;
|
|
1435
|
+
}
|
|
1436
|
+
for (; ; ) {
|
|
1437
|
+
check(source[cursor] === '"', "JSON key must be a string");
|
|
1438
|
+
const key = string();
|
|
1439
|
+
whitespace();
|
|
1440
|
+
check(!Object.hasOwn(result2, key), "duplicate JSON key");
|
|
1441
|
+
check(source[cursor++] === ":", "missing JSON colon");
|
|
1442
|
+
result2[key] = value(depth + 1);
|
|
1443
|
+
whitespace();
|
|
1444
|
+
const delimiter = source[cursor++];
|
|
1445
|
+
if (delimiter === "}") return result2;
|
|
1446
|
+
check(delimiter === ",", "invalid JSON object separator");
|
|
1447
|
+
whitespace();
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
if (first === "[") {
|
|
1451
|
+
cursor++;
|
|
1452
|
+
whitespace();
|
|
1453
|
+
const result2 = [];
|
|
1454
|
+
if (source[cursor] === "]") {
|
|
1455
|
+
cursor++;
|
|
1456
|
+
return result2;
|
|
1457
|
+
}
|
|
1458
|
+
for (; ; ) {
|
|
1459
|
+
result2.push(value(depth + 1));
|
|
1460
|
+
whitespace();
|
|
1461
|
+
const delimiter = source[cursor++];
|
|
1462
|
+
if (delimiter === "]") return result2;
|
|
1463
|
+
check(delimiter === ",", "invalid JSON array separator");
|
|
1464
|
+
whitespace();
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
for (const [token, decoded] of [
|
|
1468
|
+
["true", true],
|
|
1469
|
+
["false", false],
|
|
1470
|
+
["null", null]
|
|
1471
|
+
]) {
|
|
1472
|
+
if (source.startsWith(token, cursor)) {
|
|
1473
|
+
cursor += token.length;
|
|
1474
|
+
return decoded;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
const match = /^-?(?:0|[1-9][0-9]*)/.exec(source.slice(cursor));
|
|
1478
|
+
check(match, "invalid JSON value");
|
|
1479
|
+
cursor += match[0].length;
|
|
1480
|
+
const number = Number(match[0]);
|
|
1481
|
+
check(
|
|
1482
|
+
Number.isSafeInteger(number) && !Object.is(number, -0),
|
|
1483
|
+
"unsafe or noncanonical JSON integer"
|
|
1484
|
+
);
|
|
1485
|
+
return number;
|
|
1486
|
+
};
|
|
1487
|
+
const result = value(0);
|
|
1488
|
+
whitespace();
|
|
1489
|
+
check(cursor === source.length, "trailing JSON data");
|
|
1490
|
+
return result;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// src/scratchpad-native/codec.ts
|
|
1494
|
+
var CodecError = class extends Error {
|
|
1495
|
+
constructor(reason, message) {
|
|
1496
|
+
super(message);
|
|
1497
|
+
this.reason = reason;
|
|
1498
|
+
this.name = "CodecError";
|
|
1499
|
+
}
|
|
1500
|
+
reason;
|
|
1501
|
+
};
|
|
1502
|
+
function reject(reason, message) {
|
|
1503
|
+
throw new CodecError(reason, message);
|
|
1504
|
+
}
|
|
1505
|
+
function hash(tag, data) {
|
|
1506
|
+
return node_crypto.createHash("sha256").update(tag).update("\0").update(data).digest("base64url");
|
|
1507
|
+
}
|
|
1508
|
+
function chunkID(data) {
|
|
1509
|
+
return hash("scratchpad:v1:chunk", data);
|
|
1510
|
+
}
|
|
1511
|
+
function fileObjectID(data) {
|
|
1512
|
+
return hash("scratchpad:v1:file", data);
|
|
1513
|
+
}
|
|
1514
|
+
function treeNodeID(data) {
|
|
1515
|
+
return hash("scratchpad:v1:tree", data);
|
|
1516
|
+
}
|
|
1517
|
+
async function chunkStream(source, emit) {
|
|
1518
|
+
const file = { size: 0, digest: "", chunks: [] };
|
|
1519
|
+
const digest = node_crypto.createHash("sha256"), buffer = Buffer.alloc(CHUNK_SIZE);
|
|
1520
|
+
let used = 0;
|
|
1521
|
+
const flush = async () => {
|
|
1522
|
+
if (file.chunks.length >= MAX_CHUNKS) reject("chunk-shape", "file exceeds chunk count bound");
|
|
1523
|
+
const bytes = buffer.subarray(0, used), id = chunkID(bytes);
|
|
1524
|
+
digest.update(bytes);
|
|
1525
|
+
file.size += used;
|
|
1526
|
+
file.chunks.push({ id, length: used });
|
|
1527
|
+
await emit(file.chunks.length - 1, { kind: "chnk", id }, bytes);
|
|
1528
|
+
used = 0;
|
|
1529
|
+
};
|
|
1530
|
+
for await (const part of source) {
|
|
1531
|
+
check(part instanceof Uint8Array, "stream must yield bytes");
|
|
1532
|
+
let offset = 0;
|
|
1533
|
+
while (offset < part.length) {
|
|
1534
|
+
const length = Math.min(CHUNK_SIZE - used, part.length - offset);
|
|
1535
|
+
buffer.set(part.subarray(offset, offset + length), used);
|
|
1536
|
+
used += length;
|
|
1537
|
+
offset += length;
|
|
1538
|
+
if (used === CHUNK_SIZE) await flush();
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
if (used !== 0) await flush();
|
|
1542
|
+
file.digest = digest.digest("hex");
|
|
1543
|
+
return file;
|
|
1544
|
+
}
|
|
1545
|
+
function head(major, value) {
|
|
1546
|
+
uint(value);
|
|
1547
|
+
if (value < 24) return Buffer.from([major << 5 | value]);
|
|
1548
|
+
if (value <= 255) return Buffer.from([major << 5 | 24, value]);
|
|
1549
|
+
if (value <= 65535) {
|
|
1550
|
+
const bytes2 = Buffer.alloc(3);
|
|
1551
|
+
bytes2[0] = major << 5 | 25;
|
|
1552
|
+
bytes2.writeUInt16BE(value, 1);
|
|
1553
|
+
return bytes2;
|
|
1554
|
+
}
|
|
1555
|
+
if (value <= 4294967295) {
|
|
1556
|
+
const bytes2 = Buffer.alloc(5);
|
|
1557
|
+
bytes2[0] = major << 5 | 26;
|
|
1558
|
+
bytes2.writeUInt32BE(value, 1);
|
|
1559
|
+
return bytes2;
|
|
1560
|
+
}
|
|
1561
|
+
const bytes = Buffer.alloc(9);
|
|
1562
|
+
bytes[0] = major << 5 | 27;
|
|
1563
|
+
bytes.writeBigUInt64BE(BigInt(value), 1);
|
|
1564
|
+
return bytes;
|
|
1565
|
+
}
|
|
1566
|
+
function byteString(bytes) {
|
|
1567
|
+
return Buffer.concat([head(2, bytes.length), bytes]);
|
|
1568
|
+
}
|
|
1569
|
+
function textString(value) {
|
|
1570
|
+
const bytes = Buffer.from(value);
|
|
1571
|
+
return Buffer.concat([head(3, bytes.length), bytes]);
|
|
1572
|
+
}
|
|
1573
|
+
function validateFile(file) {
|
|
1574
|
+
uint(file.size);
|
|
1575
|
+
check(
|
|
1576
|
+
typeof file.digest === "string" && /^[0-9a-f]{64}$/.test(file.digest),
|
|
1577
|
+
"invalid logical file digest"
|
|
1578
|
+
);
|
|
1579
|
+
if (!Array.isArray(file.chunks) || file.chunks.length > MAX_CHUNKS)
|
|
1580
|
+
reject("chunk-shape", "chunk count exceeds bound");
|
|
1581
|
+
let total = 0;
|
|
1582
|
+
for (const [index, chunk] of file.chunks.entries()) {
|
|
1583
|
+
validateContentID(chunk.id);
|
|
1584
|
+
uint(chunk.length);
|
|
1585
|
+
if (chunk.length === 0 || chunk.length > CHUNK_SIZE || index !== file.chunks.length - 1 && chunk.length !== CHUNK_SIZE)
|
|
1586
|
+
reject("chunk-shape", "invalid fixed chunk length");
|
|
1587
|
+
total += chunk.length;
|
|
1588
|
+
}
|
|
1589
|
+
if (total !== file.size) reject("chunk-shape", "chunk lengths differ from file size");
|
|
1590
|
+
}
|
|
1591
|
+
function encodeFileObject(file) {
|
|
1592
|
+
validateFile(file);
|
|
1593
|
+
const parts = [head(5, 3), textString("c"), head(4, file.chunks.length)];
|
|
1594
|
+
for (const chunk of file.chunks)
|
|
1595
|
+
parts.push(head(4, 2), byteString(Buffer.from(chunk.id, "base64url")), head(0, chunk.length));
|
|
1596
|
+
parts.push(
|
|
1597
|
+
textString("d"),
|
|
1598
|
+
byteString(Buffer.from(file.digest, "hex")),
|
|
1599
|
+
textString("s"),
|
|
1600
|
+
head(0, file.size)
|
|
1601
|
+
);
|
|
1602
|
+
return Buffer.concat(parts);
|
|
1603
|
+
}
|
|
1604
|
+
function entryName(name) {
|
|
1605
|
+
if (typeof name !== "string" || Buffer.from(name).toString("utf8") !== name)
|
|
1606
|
+
reject("utf8", "invalid entry name UTF-8");
|
|
1607
|
+
if (Buffer.byteLength(name) === 0 || Buffer.byteLength(name) > 255)
|
|
1608
|
+
reject("name-length", "entry name length exceeds bound");
|
|
1609
|
+
if (name === "." || name === ".." || /[\x00-\x1f\x7f/]/.test(name))
|
|
1610
|
+
reject("name-forbidden", "forbidden entry name");
|
|
1611
|
+
try {
|
|
1612
|
+
validateName(name);
|
|
1613
|
+
} catch {
|
|
1614
|
+
reject("not-nfc", "entry name is not NFC");
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
function validateEntry(entry) {
|
|
1618
|
+
entryName(entry.name);
|
|
1619
|
+
if (entry.type === "symlink") {
|
|
1620
|
+
if (entry.executable || entry.object !== void 0 && entry.object !== ZERO_CONTENT_ID)
|
|
1621
|
+
reject("type-invalid", "symlink has file fields");
|
|
1622
|
+
const target = entry.linkTarget ?? "";
|
|
1623
|
+
if (typeof target !== "string" || Buffer.from(target).toString("utf8") !== target)
|
|
1624
|
+
reject("utf8", "invalid symlink UTF-8");
|
|
1625
|
+
if (Buffer.byteLength(target) === 0 || Buffer.byteLength(target) > 4096 || target.includes("\0"))
|
|
1626
|
+
reject("target-invalid", "invalid symlink target");
|
|
1627
|
+
} else {
|
|
1628
|
+
if (entry.type !== "file" && entry.type !== "directory")
|
|
1629
|
+
reject("type-invalid", "unknown entry type");
|
|
1630
|
+
validateContentID(entry.object);
|
|
1631
|
+
if (entry.linkTarget || entry.type === "directory" && entry.executable)
|
|
1632
|
+
reject("type-invalid", "entry has fields of another type");
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
function encodeTreeNode(entries) {
|
|
1636
|
+
for (const entry of entries) validateEntry(entry);
|
|
1637
|
+
const sorted = [...entries].sort(
|
|
1638
|
+
(a, b) => Buffer.compare(Buffer.from(a.name), Buffer.from(b.name))
|
|
1639
|
+
);
|
|
1640
|
+
for (let index = 1; index < sorted.length; index++)
|
|
1641
|
+
if (sorted[index].name === sorted[index - 1].name)
|
|
1642
|
+
reject("duplicate-name", "duplicate tree entry");
|
|
1643
|
+
if (entries.length > MAX_TREE_ENTRIES) reject("entry-count", "tree entry count exceeds bound");
|
|
1644
|
+
const parts = [head(5, 1), textString("e"), head(4, entries.length)];
|
|
1645
|
+
let length = parts.reduce((sum, part) => sum + part.length, 0);
|
|
1646
|
+
for (const entry of sorted) {
|
|
1647
|
+
const fields = [head(5, entry.executable ? 4 : 3)];
|
|
1648
|
+
if (entry.type === "symlink") fields.push(textString("l"), textString(entry.linkTarget));
|
|
1649
|
+
else fields.push(textString("i"), byteString(Buffer.from(entry.object, "base64url")));
|
|
1650
|
+
fields.push(
|
|
1651
|
+
textString("n"),
|
|
1652
|
+
textString(entry.name),
|
|
1653
|
+
textString("t"),
|
|
1654
|
+
head(0, entry.type === "file" ? 0 : entry.type === "directory" ? 1 : 2)
|
|
1655
|
+
);
|
|
1656
|
+
if (entry.executable) fields.push(textString("x"), head(0, 1));
|
|
1657
|
+
length += fields.reduce((sum, part) => sum + part.length, 0);
|
|
1658
|
+
if (length > MAX_NODE_BYTES) reject("node-size", "tree node byte count exceeds bound");
|
|
1659
|
+
parts.push(...fields);
|
|
1660
|
+
}
|
|
1661
|
+
return Buffer.concat(parts);
|
|
1662
|
+
}
|
|
1663
|
+
treeNodeID(encodeTreeNode([]));
|
|
1664
|
+
function decodeCBOR(data) {
|
|
1665
|
+
let cursor = 0;
|
|
1666
|
+
const take = (length) => {
|
|
1667
|
+
if (!Number.isSafeInteger(length) || length > data.length - cursor)
|
|
1668
|
+
reject("type-mismatch", "truncated CBOR");
|
|
1669
|
+
const bytes = Buffer.from(data.buffer, data.byteOffset + cursor, length);
|
|
1670
|
+
cursor += length;
|
|
1671
|
+
return bytes;
|
|
1672
|
+
};
|
|
1673
|
+
const item = (depth) => {
|
|
1674
|
+
if (depth > 8) reject("type-mismatch", "CBOR nesting exceeds object shape");
|
|
1675
|
+
const first = take(1)[0], major = first >>> 5, additional = first & 31;
|
|
1676
|
+
if (additional === 31) reject("indefinite-length", "indefinite CBOR length");
|
|
1677
|
+
if (![0, 2, 3, 4, 5].includes(major)) reject("forbidden-item", "forbidden CBOR item");
|
|
1678
|
+
let value = additional;
|
|
1679
|
+
if (additional >= 24) {
|
|
1680
|
+
if (additional > 27) reject("forbidden-item", "reserved CBOR length");
|
|
1681
|
+
const bytes = take(2 ** (additional - 24));
|
|
1682
|
+
const large = additional === 27 ? bytes.readBigUInt64BE() : BigInt(bytes.readUIntBE(0, bytes.length));
|
|
1683
|
+
const minimum = [24n, 256n, 65536n, 4294967296n][additional - 24];
|
|
1684
|
+
if (large < minimum) reject("non-shortest", "CBOR integer is not shortest");
|
|
1685
|
+
if (large > BigInt(Number.MAX_SAFE_INTEGER))
|
|
1686
|
+
reject("type-mismatch", "CBOR integer exceeds exact range");
|
|
1687
|
+
value = Number(large);
|
|
1688
|
+
}
|
|
1689
|
+
if (major === 0) return value;
|
|
1690
|
+
if (major === 2) return take(value);
|
|
1691
|
+
if (major === 3) {
|
|
1692
|
+
try {
|
|
1693
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(take(value));
|
|
1694
|
+
} catch {
|
|
1695
|
+
return reject("utf8", "CBOR text is not UTF-8");
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
if (value > data.length - cursor) reject("type-mismatch", "CBOR item count exceeds input");
|
|
1699
|
+
if (major === 4) {
|
|
1700
|
+
const values = [];
|
|
1701
|
+
for (let index = 0; index < value; index++) values.push(item(depth + 1));
|
|
1702
|
+
return values;
|
|
1703
|
+
}
|
|
1704
|
+
const result2 = /* @__PURE__ */ new Map();
|
|
1705
|
+
let last;
|
|
1706
|
+
for (let index = 0; index < value; index++) {
|
|
1707
|
+
const start = cursor;
|
|
1708
|
+
let key;
|
|
1709
|
+
try {
|
|
1710
|
+
key = item(depth + 1);
|
|
1711
|
+
} catch (error) {
|
|
1712
|
+
if (error instanceof CodecError && error.reason === "utf8")
|
|
1713
|
+
reject("key-unknown", "invalid UTF-8 map key cannot be a schema key");
|
|
1714
|
+
throw error;
|
|
1715
|
+
}
|
|
1716
|
+
const encoded = Buffer.from(data.subarray(start, cursor));
|
|
1717
|
+
if (typeof key !== "string") reject("type-mismatch", "CBOR map key must be text");
|
|
1718
|
+
if (last && Buffer.compare(last, encoded) >= 0)
|
|
1719
|
+
reject("key-order", "CBOR keys are not strictly ordered");
|
|
1720
|
+
last = encoded;
|
|
1721
|
+
result2.set(key, item(depth + 1));
|
|
1722
|
+
}
|
|
1723
|
+
return result2;
|
|
1724
|
+
};
|
|
1725
|
+
const result = item(0);
|
|
1726
|
+
if (cursor !== data.length) reject("trailing-bytes", "trailing CBOR bytes");
|
|
1727
|
+
return result;
|
|
1728
|
+
}
|
|
1729
|
+
function map(value, keys) {
|
|
1730
|
+
if (!(value instanceof Map)) reject("type-mismatch", "expected CBOR map");
|
|
1731
|
+
for (const key of value.keys())
|
|
1732
|
+
if (!keys.includes(key)) reject("key-unknown", "unknown object key");
|
|
1733
|
+
for (const key of keys) if (!value.has(key)) reject("key-missing", "missing object key");
|
|
1734
|
+
return value;
|
|
1735
|
+
}
|
|
1736
|
+
function bytes32(value) {
|
|
1737
|
+
if (!(value instanceof Uint8Array) || value.length !== 32)
|
|
1738
|
+
reject("type-mismatch", "expected 32-byte digest");
|
|
1739
|
+
return Buffer.from(value);
|
|
1740
|
+
}
|
|
1741
|
+
function integer(value) {
|
|
1742
|
+
if (typeof value !== "number") reject("type-mismatch", "expected unsigned integer");
|
|
1743
|
+
return value;
|
|
1744
|
+
}
|
|
1745
|
+
function decodeFileObject(data) {
|
|
1746
|
+
const fields = map(decodeCBOR(data), ["c", "d", "s"]), refs = fields.get("c");
|
|
1747
|
+
if (!Array.isArray(refs)) reject("type-mismatch", "chunks must be array");
|
|
1748
|
+
const file = {
|
|
1749
|
+
size: integer(fields.get("s")),
|
|
1750
|
+
digest: bytes32(fields.get("d")).toString("hex"),
|
|
1751
|
+
chunks: []
|
|
1752
|
+
};
|
|
1753
|
+
for (const ref of refs) {
|
|
1754
|
+
if (!Array.isArray(ref) || ref.length !== 2)
|
|
1755
|
+
reject("type-mismatch", "chunk reference must have two fields");
|
|
1756
|
+
file.chunks.push({ id: bytes32(ref[0]).toString("base64url"), length: integer(ref[1]) });
|
|
1757
|
+
}
|
|
1758
|
+
validateFile(file);
|
|
1759
|
+
return file;
|
|
1760
|
+
}
|
|
1761
|
+
function decodeTreeNode(data) {
|
|
1762
|
+
if (data.length > MAX_NODE_BYTES) reject("node-size", "tree node exceeds byte bound");
|
|
1763
|
+
const fields = map(decodeCBOR(data), ["e"]), raw = fields.get("e");
|
|
1764
|
+
if (!Array.isArray(raw)) reject("type-mismatch", "entries must be an array");
|
|
1765
|
+
if (raw.length > MAX_TREE_ENTRIES) reject("entry-count", "tree entry count exceeds bound");
|
|
1766
|
+
const entries = [];
|
|
1767
|
+
let previous;
|
|
1768
|
+
for (const item of raw) {
|
|
1769
|
+
if (!(item instanceof Map)) reject("type-mismatch", "entry must be a map");
|
|
1770
|
+
for (const key of item.keys())
|
|
1771
|
+
if (!["i", "l", "n", "t", "x"].includes(key)) reject("key-unknown", "unknown entry key");
|
|
1772
|
+
if (!item.has("n") || !item.has("t")) reject("key-missing", "missing entry name or type");
|
|
1773
|
+
const name = item.get("n"), type = integer(item.get("t"));
|
|
1774
|
+
if (typeof name !== "string") reject("type-mismatch", "entry name must be text");
|
|
1775
|
+
if (type > 2) reject("type-invalid", "unknown entry type");
|
|
1776
|
+
if (!item.has(type === 2 ? "l" : "i")) reject("key-missing", "missing entry target");
|
|
1777
|
+
if (item.has(type === 2 ? "i" : "l")) reject("type-invalid", "entry has other target type");
|
|
1778
|
+
if (item.has("x") && integer(item.get("x")) !== 1)
|
|
1779
|
+
reject("type-invalid", "executable flag must be one");
|
|
1780
|
+
const entry = {
|
|
1781
|
+
name,
|
|
1782
|
+
type: type === 0 ? "file" : type === 1 ? "directory" : "symlink"
|
|
1783
|
+
};
|
|
1784
|
+
if (item.has("x")) entry.executable = true;
|
|
1785
|
+
if (type === 2) {
|
|
1786
|
+
const target = item.get("l");
|
|
1787
|
+
if (typeof target !== "string") reject("type-mismatch", "symlink target must be text");
|
|
1788
|
+
entry.linkTarget = target;
|
|
1789
|
+
} else entry.object = bytes32(item.get("i")).toString("base64url");
|
|
1790
|
+
validateEntry(entry);
|
|
1791
|
+
if (previous !== void 0) {
|
|
1792
|
+
const order = Buffer.compare(Buffer.from(previous), Buffer.from(name));
|
|
1793
|
+
if (order === 0) reject("duplicate-name", "duplicate tree entry");
|
|
1794
|
+
if (order > 0) reject("entry-order", "entries are not sorted");
|
|
1795
|
+
}
|
|
1796
|
+
previous = name;
|
|
1797
|
+
entries.push(entry);
|
|
1798
|
+
}
|
|
1799
|
+
return entries;
|
|
1800
|
+
}
|
|
1801
|
+
function verifyObject(object, data) {
|
|
1802
|
+
validateContentID(object.id);
|
|
1803
|
+
if (object.kind === "chnk") {
|
|
1804
|
+
if (data.length === 0 || data.length > CHUNK_SIZE)
|
|
1805
|
+
reject("chunk-shape", "chunk length outside bound");
|
|
1806
|
+
if (chunkID(data) !== object.id) reject("id-mismatch", "chunk identity differs");
|
|
1807
|
+
} else if (object.kind === "fobj") {
|
|
1808
|
+
const file = decodeFileObject(data);
|
|
1809
|
+
if (!Buffer.from(encodeFileObject(file)).equals(data))
|
|
1810
|
+
reject("type-invalid", "file bytes are not canonical");
|
|
1811
|
+
if (fileObjectID(data) !== object.id) reject("id-mismatch", "file identity differs");
|
|
1812
|
+
} else if (object.kind === "tree") {
|
|
1813
|
+
const entries = decodeTreeNode(data);
|
|
1814
|
+
if (!Buffer.from(encodeTreeNode(entries)).equals(data))
|
|
1815
|
+
reject("type-invalid", "tree bytes are not canonical");
|
|
1816
|
+
if (treeNodeID(data) !== object.id) reject("id-mismatch", "tree identity differs");
|
|
1817
|
+
} else reject("type-invalid", "unsupported object kind");
|
|
1818
|
+
}
|
|
1819
|
+
function validateReference(value) {
|
|
1820
|
+
const v = record(value, [
|
|
1821
|
+
"version",
|
|
1822
|
+
"creator",
|
|
1823
|
+
"account",
|
|
1824
|
+
"target",
|
|
1825
|
+
"root",
|
|
1826
|
+
"generation",
|
|
1827
|
+
"path",
|
|
1828
|
+
"retention",
|
|
1829
|
+
"expires_at"
|
|
1830
|
+
]);
|
|
1831
|
+
check(v.version === 2, "reference version must be two");
|
|
1832
|
+
text(v.creator, 512, 1);
|
|
1833
|
+
validatePublicKey(v.account, "account");
|
|
1834
|
+
validateTarget(v.target);
|
|
1835
|
+
validateContentID(v.root);
|
|
1836
|
+
uint(v.generation, void 0, 1);
|
|
1837
|
+
validatePath(v.path, 256);
|
|
1838
|
+
validateRetentionHandle(v.retention);
|
|
1839
|
+
uint(v.expires_at, void 0, 1);
|
|
1840
|
+
}
|
|
1841
|
+
function encodeReference(reference) {
|
|
1842
|
+
validateReference(reference);
|
|
1843
|
+
const ordered = {
|
|
1844
|
+
version: 2,
|
|
1845
|
+
creator: reference.creator,
|
|
1846
|
+
account: reference.account,
|
|
1847
|
+
target: { volume: reference.target.volume, creation_id: reference.target.creation_id },
|
|
1848
|
+
root: reference.root,
|
|
1849
|
+
generation: reference.generation,
|
|
1850
|
+
path: reference.path,
|
|
1851
|
+
retention: { slot: reference.retention.slot, generation: reference.retention.generation },
|
|
1852
|
+
expires_at: reference.expires_at
|
|
1853
|
+
};
|
|
1854
|
+
const token = "sp-ref:" + Buffer.from(goJSON(ordered, false)).toString("base64url");
|
|
1855
|
+
check(Buffer.byteLength(token) <= 16 * 1024, "reference exceeds byte bound");
|
|
1856
|
+
return token;
|
|
1857
|
+
}
|
|
1858
|
+
function decodeReference(token) {
|
|
1859
|
+
text(token, 16 * 1024);
|
|
1860
|
+
check(token.startsWith("sp-ref:"), "reference prefix is absent");
|
|
1861
|
+
const encoded = token.slice(7);
|
|
1862
|
+
check(/^[A-Za-z0-9_-]+$/.test(encoded), "reference is not unpadded base64url");
|
|
1863
|
+
const data = Buffer.from(encoded, "base64url");
|
|
1864
|
+
check(data.toString("base64url") === encoded, "reference base64url has stray bits");
|
|
1865
|
+
const reference = parseStrictJSON(data, 16 * 1024);
|
|
1866
|
+
validateReference(reference);
|
|
1867
|
+
check(encodeReference(reference) === token, "reference JSON is not canonical");
|
|
1868
|
+
return reference;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// src/scratchpad-native/references.ts
|
|
1872
|
+
var SHORT_REFERENCE_PREFIX2 = "sp-r1:";
|
|
1873
|
+
var SHORT_REFERENCE_BYTES = 66;
|
|
1874
|
+
var REFERENCE_PATHS = 8;
|
|
1875
|
+
var MAX_REFERENCE_INTENT_BYTES = 3072;
|
|
1876
|
+
var MAX_REFERENCE_METADATA_BYTES = 32768;
|
|
1877
|
+
function decodeShortReference(token) {
|
|
1878
|
+
text(token, SHORT_REFERENCE_BYTES, SHORT_REFERENCE_BYTES);
|
|
1879
|
+
check(/^sp-r1:[A-Za-z0-9_-]{60}$/.test(token), "malformed short reference");
|
|
1880
|
+
const data = Buffer.from(token.slice(SHORT_REFERENCE_PREFIX2.length), "base64url");
|
|
1881
|
+
check(data.length === 45 && data[32] < 64, "invalid short reference slots");
|
|
1882
|
+
const generation = Number(data.readBigUInt64BE(24));
|
|
1883
|
+
uint(generation, void 0, 1);
|
|
1884
|
+
const result = {
|
|
1885
|
+
route: data.subarray(0, 8).toString("hex"),
|
|
1886
|
+
creation_id: data.subarray(8, 24).toString("hex"),
|
|
1887
|
+
retention: { slot: data[32] >> 3, generation },
|
|
1888
|
+
path_index: data[32] & 7
|
|
1889
|
+
};
|
|
1890
|
+
validateTarget({ volume: "Reference", creation_id: result.creation_id });
|
|
1891
|
+
return result;
|
|
1892
|
+
}
|
|
1893
|
+
function referenceRoute(account, volume) {
|
|
1894
|
+
return node_crypto.createHash("sha256").update(`scratchpad-reference-volume-v1\0${account}\0${volume}`).digest().subarray(0, 8).toString("hex");
|
|
1895
|
+
}
|
|
1896
|
+
function validateReferenceSpec(value) {
|
|
1897
|
+
const v = record(value, ["creator", "paths"]);
|
|
1898
|
+
text(v.creator, 512, 1);
|
|
1899
|
+
check(
|
|
1900
|
+
Array.isArray(v.paths) && v.paths.length > 0 && v.paths.length <= REFERENCE_PATHS,
|
|
1901
|
+
"reference paths exceed bound"
|
|
1902
|
+
);
|
|
1903
|
+
let previous = "";
|
|
1904
|
+
for (const path of v.paths) {
|
|
1905
|
+
validatePath(path, 256);
|
|
1906
|
+
check(
|
|
1907
|
+
Buffer.compare(Buffer.from(previous), Buffer.from(path)) < 0,
|
|
1908
|
+
"reference paths must be distinct and sorted"
|
|
1909
|
+
);
|
|
1910
|
+
previous = path;
|
|
1911
|
+
}
|
|
1912
|
+
check(
|
|
1913
|
+
Buffer.byteLength(goJSON({ creator: v.creator, paths: v.paths })) <= MAX_REFERENCE_INTENT_BYTES,
|
|
1914
|
+
"reference intent exceeds byte bound"
|
|
1915
|
+
);
|
|
1916
|
+
}
|
|
1917
|
+
function validateResolvedReference(value) {
|
|
1918
|
+
const v = record(value, ["short", "expanded"]);
|
|
1919
|
+
text(v.short, SHORT_REFERENCE_BYTES, SHORT_REFERENCE_BYTES);
|
|
1920
|
+
const locator = decodeShortReference(v.short);
|
|
1921
|
+
validateReference(v.expanded);
|
|
1922
|
+
check(
|
|
1923
|
+
locator.route === referenceRoute(v.expanded.account, v.expanded.target.volume) && locator.creation_id === v.expanded.target.creation_id && sameValue(locator.retention, v.expanded.retention),
|
|
1924
|
+
"short reference binding differs"
|
|
1925
|
+
);
|
|
1926
|
+
}
|
|
1927
|
+
function validateReferenceCapabilities(value) {
|
|
1928
|
+
const v = record(value, [
|
|
1929
|
+
"enabled",
|
|
1930
|
+
"version",
|
|
1931
|
+
"prefix",
|
|
1932
|
+
"token_bytes",
|
|
1933
|
+
"paths_per_retention",
|
|
1934
|
+
"max_intent_bytes",
|
|
1935
|
+
"max_metadata_bytes"
|
|
1936
|
+
]);
|
|
1937
|
+
check(
|
|
1938
|
+
typeof v.enabled === "boolean" && v.version === 1 && v.prefix === SHORT_REFERENCE_PREFIX2 && v.token_bytes === SHORT_REFERENCE_BYTES && v.paths_per_retention === REFERENCE_PATHS && v.max_intent_bytes === MAX_REFERENCE_INTENT_BYTES && v.max_metadata_bytes === MAX_REFERENCE_METADATA_BYTES,
|
|
1939
|
+
"unsupported reference capability"
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1942
|
+
function validateReferencePublishRequest(value) {
|
|
1943
|
+
const v = record(value, ["target", "command", "reference"]);
|
|
1944
|
+
validateTarget(v.target);
|
|
1945
|
+
validateCommand(v.command);
|
|
1946
|
+
check(
|
|
1947
|
+
v.target.creation_id === v.command.creation_id && v.command.retention.enabled && v.command.retention.until > 0,
|
|
1948
|
+
"referenced publication requires finite retention and matching target"
|
|
1949
|
+
);
|
|
1950
|
+
validateReferenceSpec(v.reference);
|
|
1951
|
+
}
|
|
1952
|
+
function validateReferencePublishResult(value) {
|
|
1953
|
+
const v = record(value, ["receipt", "references"]);
|
|
1954
|
+
validateReceipt(v.receipt);
|
|
1955
|
+
check(
|
|
1956
|
+
v.receipt.command.retention.enabled && Array.isArray(v.references) && v.references.length > 0 && v.references.length <= REFERENCE_PATHS,
|
|
1957
|
+
"invalid referenced receipt"
|
|
1958
|
+
);
|
|
1959
|
+
let previous = "", creator;
|
|
1960
|
+
for (const [index, pair] of v.references.entries()) {
|
|
1961
|
+
validateResolvedReference(pair);
|
|
1962
|
+
const ref = pair.expanded;
|
|
1963
|
+
creator ??= ref.creator;
|
|
1964
|
+
check(
|
|
1965
|
+
decodeShortReference(pair.short).path_index === index && ref.creator === creator && Buffer.compare(Buffer.from(previous), Buffer.from(ref.path)) < 0 && ref.account === v.receipt.principal.account && ref.target.volume === v.receipt.volume && ref.target.creation_id === v.receipt.command.creation_id && ref.root === v.receipt.head.root && ref.generation === v.receipt.head.generation && sameValue(ref.retention, v.receipt.retention) && ref.expires_at === v.receipt.command.retention.until,
|
|
1966
|
+
"reference differs from receipt"
|
|
1967
|
+
);
|
|
1968
|
+
previous = ref.path;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
function validateReferenceStatResult(value) {
|
|
1972
|
+
const v = record(value, ["reference", "stat"]);
|
|
1973
|
+
validateResolvedReference(v.reference);
|
|
1974
|
+
validateStat(v.stat);
|
|
1975
|
+
}
|
|
1976
|
+
function validateReferenceRetainRequest(value) {
|
|
1977
|
+
const v = record(value, ["retain", "reference"]);
|
|
1978
|
+
const retain = record(v.retain, ["target", "slot", "generation", "root", "until", "action"]);
|
|
1979
|
+
validateTarget(retain.target);
|
|
1980
|
+
uint(retain.slot, RETENTION_SLOTS - 1);
|
|
1981
|
+
uint(retain.generation);
|
|
1982
|
+
validateContentID(retain.root);
|
|
1983
|
+
uint(retain.until, void 0, 1);
|
|
1984
|
+
validateAction(retain.action);
|
|
1985
|
+
validateReferenceSpec(v.reference);
|
|
1986
|
+
}
|
|
1987
|
+
function validateReferenceRetainResult(value) {
|
|
1988
|
+
const v = record(value, ["volume", "references"]);
|
|
1989
|
+
validateVolumeView(v.volume);
|
|
1990
|
+
check(
|
|
1991
|
+
Array.isArray(v.references) && v.references.length > 0 && v.references.length <= REFERENCE_PATHS,
|
|
1992
|
+
"invalid retained references"
|
|
1993
|
+
);
|
|
1994
|
+
let first, previous = "";
|
|
1995
|
+
for (const [index, pair] of v.references.entries()) {
|
|
1996
|
+
validateResolvedReference(pair);
|
|
1997
|
+
const ref = pair.expanded;
|
|
1998
|
+
first ??= ref;
|
|
1999
|
+
const retained = v.volume.retentions[ref.retention.slot];
|
|
2000
|
+
check(
|
|
2001
|
+
decodeShortReference(pair.short).path_index === index && Buffer.compare(Buffer.from(previous), Buffer.from(ref.path)) < 0 && ref.creator === first.creator && ref.root === first.root && ref.generation === first.generation && sameValue(ref.retention, first.retention) && ref.expires_at === first.expires_at && sameValue(ref.target, v.volume.target) && ref.account === v.volume.owner.account && retained.generation === ref.retention.generation && retained.pin?.root === ref.root && retained.pin.until === ref.expires_at,
|
|
2002
|
+
"reference differs from retained root"
|
|
2003
|
+
);
|
|
2004
|
+
previous = ref.path;
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// src/scratchpad-cli.ts
|
|
2009
|
+
var DEFAULT_SP_BIN = "sp";
|
|
2010
|
+
var HANDOFF_TTL_S = 600;
|
|
2011
|
+
var DEFAULT_GRANT_TTL_S = 600;
|
|
2012
|
+
var EXPIRY_MARGIN_S = 5;
|
|
2013
|
+
var MAX_LEDGER = 16;
|
|
2014
|
+
var MAX_REFERENCE_BYTES = 16 * 1024;
|
|
2015
|
+
var MAX_READ_BYTES2 = 16 * 1024 * 1024;
|
|
2016
|
+
var MAX_READ_STDOUT = Math.ceil(MAX_READ_BYTES2 / 3) * 4 + 64 * 1024;
|
|
2017
|
+
var MAX_STDOUT = 256 * 1024;
|
|
2018
|
+
var MAX_STDERR = 64 * 1024;
|
|
2019
|
+
var ACCOUNT_RE = /^A[A-Z2-7]{55}$/;
|
|
2020
|
+
var VOLUME_RE = /^[0-9A-Za-z]{1,64}$/;
|
|
2021
|
+
var CREATION_RE = /^[0-9a-f]{32}$/;
|
|
2022
|
+
var ROOT_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
2023
|
+
var BASE64URL_RE = /^[A-Za-z0-9_-]+$/;
|
|
2024
|
+
var BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
2025
|
+
var ScratchPadError = class extends Error {
|
|
2026
|
+
constructor(message, code, unknown2) {
|
|
2027
|
+
super(message);
|
|
2028
|
+
this.code = code;
|
|
2029
|
+
this.unknown = unknown2;
|
|
2030
|
+
this.name = "ScratchPadError";
|
|
2031
|
+
}
|
|
2032
|
+
code;
|
|
2033
|
+
unknown;
|
|
2034
|
+
};
|
|
2035
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2036
|
+
var hasKeys = (v, keys) => Object.keys(v).length === keys.length && keys.every((k) => k in v);
|
|
2037
|
+
var positive = (v) => Number.isSafeInteger(v) && v > 0;
|
|
2038
|
+
var utf8Bytes = (s) => Buffer.byteLength(s, "utf8");
|
|
2039
|
+
function validTarget(v) {
|
|
2040
|
+
return isObject(v) && hasKeys(v, ["volume", "creation_id"]) && typeof v["volume"] === "string" && VOLUME_RE.test(v["volume"]) && typeof v["creation_id"] === "string" && CREATION_RE.test(v["creation_id"]);
|
|
2041
|
+
}
|
|
2042
|
+
function validPath(p) {
|
|
2043
|
+
if (typeof p !== "string" || !p.startsWith("/") || utf8Bytes(p) > 256) return false;
|
|
2044
|
+
if (p === "/") return true;
|
|
2045
|
+
return p.slice(1).split("/").every((name) => name !== "" && name !== "." && name !== "..");
|
|
2046
|
+
}
|
|
2047
|
+
function decodeReference2(reference) {
|
|
2048
|
+
const refuse2 = (why) => {
|
|
2049
|
+
throw new Error(`malformed ScratchPad reference: ${why}`);
|
|
2050
|
+
};
|
|
2051
|
+
if (typeof reference !== "string" || !reference.startsWith(REFERENCE_PREFIX)) {
|
|
2052
|
+
refuse2("no sp-ref: prefix");
|
|
2053
|
+
}
|
|
2054
|
+
if (reference.length > MAX_REFERENCE_BYTES) refuse2("longer than 16 KiB");
|
|
2055
|
+
const body = reference.slice(REFERENCE_PREFIX.length);
|
|
2056
|
+
if (!BASE64URL_RE.test(body) || body.length % 4 === 1) refuse2("not unpadded base64url");
|
|
2057
|
+
let v;
|
|
2058
|
+
try {
|
|
2059
|
+
v = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
|
|
2060
|
+
} catch {
|
|
2061
|
+
refuse2("not JSON");
|
|
2062
|
+
}
|
|
2063
|
+
const keys = [
|
|
2064
|
+
"version",
|
|
2065
|
+
"creator",
|
|
2066
|
+
"account",
|
|
2067
|
+
"target",
|
|
2068
|
+
"root",
|
|
2069
|
+
"generation",
|
|
2070
|
+
"path",
|
|
2071
|
+
"retention",
|
|
2072
|
+
"expires_at"
|
|
2073
|
+
];
|
|
2074
|
+
if (!isObject(v) || !hasKeys(v, keys)) return refuse2("not the version 2 fields");
|
|
2075
|
+
const r = v.retention;
|
|
2076
|
+
if (v["version"] !== 2 || typeof v["creator"] !== "string" || v["creator"].length === 0 || utf8Bytes(v["creator"]) > 512 || typeof v["account"] !== "string" || !ACCOUNT_RE.test(v["account"]) || !validTarget(v["target"]) || typeof v["root"] !== "string" || !ROOT_RE.test(v["root"]) || !positive(v["generation"]) || !validPath(v["path"]) || !isObject(r) || !hasKeys(r, ["slot", "generation"]) || !Number.isSafeInteger(r["slot"]) || r["slot"] < 0 || r["slot"] > 7 || !positive(r["generation"]) || !positive(v["expires_at"])) {
|
|
2077
|
+
refuse2("a field is out of shape");
|
|
2078
|
+
}
|
|
2079
|
+
return v;
|
|
2080
|
+
}
|
|
2081
|
+
var now = () => Math.floor(Date.now() / 1e3);
|
|
2082
|
+
var RETENTION_SLOTS2 = 8;
|
|
2083
|
+
var GRANT_SLOTS2 = 16;
|
|
2084
|
+
var CONTROL_KINDS = ["close", "apply", "release", "grant"];
|
|
2085
|
+
var NO_VOLUME_CODES = ["workspace", "not_found"];
|
|
2086
|
+
var count = (v) => Number.isSafeInteger(v) && v >= 0;
|
|
2087
|
+
function decodeDescribe(reply) {
|
|
2088
|
+
const refuse2 = (why) => {
|
|
2089
|
+
throw new ScratchPadError(`sp describe: ${why}`, "malformed_reply", false);
|
|
2090
|
+
};
|
|
2091
|
+
if (!isObject(reply) || !isObject(reply["volume"])) return refuse2("the reply names no volume");
|
|
2092
|
+
const volume = reply["volume"];
|
|
2093
|
+
const control = reply["control"];
|
|
2094
|
+
let kind;
|
|
2095
|
+
if (control !== null && control !== void 0) {
|
|
2096
|
+
if (!isObject(control) || typeof control["kind"] !== "string" || !CONTROL_KINDS.includes(control["kind"])) {
|
|
2097
|
+
return refuse2("the saved control request is out of shape");
|
|
2098
|
+
}
|
|
2099
|
+
kind = control["kind"];
|
|
2100
|
+
}
|
|
2101
|
+
if (typeof volume["closed"] !== "boolean") return refuse2("the volume names no closed state");
|
|
2102
|
+
const retentions = volume["retentions"];
|
|
2103
|
+
const grants = volume["grants"];
|
|
2104
|
+
if (!Array.isArray(retentions) || retentions.length !== RETENTION_SLOTS2)
|
|
2105
|
+
return refuse2("not eight retention slots");
|
|
2106
|
+
if (!Array.isArray(grants) || grants.length !== GRANT_SLOTS2)
|
|
2107
|
+
return refuse2("not sixteen grant slots");
|
|
2108
|
+
const pins = retentions.map((slot) => {
|
|
2109
|
+
if (!isObject(slot) || !count(slot["generation"]))
|
|
2110
|
+
return refuse2("a retention slot is out of shape");
|
|
2111
|
+
const pin = slot["pin"];
|
|
2112
|
+
if (pin === void 0 || pin === null)
|
|
2113
|
+
return { generation: slot["generation"], until: void 0 };
|
|
2114
|
+
if (!isObject(pin) || !count(pin["until"]) || typeof pin["root"] !== "string")
|
|
2115
|
+
return refuse2("a retention pin is out of shape");
|
|
2116
|
+
return { generation: slot["generation"], until: pin["until"] };
|
|
2117
|
+
});
|
|
2118
|
+
const live = [];
|
|
2119
|
+
for (const slot of grants) {
|
|
2120
|
+
if (!isObject(slot) || !count(slot["generation"]))
|
|
2121
|
+
return refuse2("a grant slot is out of shape");
|
|
2122
|
+
const grant = slot["grant"];
|
|
2123
|
+
if (grant === void 0 || grant === null) continue;
|
|
2124
|
+
const handle = isObject(grant) ? grant["retention"] : void 0;
|
|
2125
|
+
if (!isObject(grant) || !count(grant["until"]) || !isObject(handle) || !count(handle["slot"]) || handle["slot"] >= RETENTION_SLOTS2 || !positive(handle["generation"])) {
|
|
2126
|
+
return refuse2("a grant is out of shape");
|
|
2127
|
+
}
|
|
2128
|
+
live.push({
|
|
2129
|
+
slot: handle["slot"],
|
|
2130
|
+
generation: handle["generation"],
|
|
2131
|
+
until: grant["until"]
|
|
2132
|
+
});
|
|
2133
|
+
}
|
|
2134
|
+
return { closed: volume["closed"], control: kind, retentions: pins, grants: live };
|
|
2135
|
+
}
|
|
2136
|
+
function decodeSpool(reply) {
|
|
2137
|
+
const refuse2 = (why) => {
|
|
2138
|
+
throw new ScratchPadError(`sp spool-remove: ${why}`, "malformed_reply", false);
|
|
2139
|
+
};
|
|
2140
|
+
if (!isObject(reply) || typeof reply["present"] !== "boolean")
|
|
2141
|
+
return refuse2("the reply says no spool state");
|
|
2142
|
+
const present = reply["present"];
|
|
2143
|
+
const keys = ["present", "files", "bytes", "removable", "applied"];
|
|
2144
|
+
if (present) keys.push("phase");
|
|
2145
|
+
if (!hasKeys(reply, keys) || present && (typeof reply["phase"] !== "string" || reply["phase"] === "") || !count(reply["files"]) || !count(reply["bytes"]) || typeof reply["removable"] !== "boolean" || typeof reply["applied"] !== "boolean") {
|
|
2146
|
+
return refuse2("the spool state is out of shape");
|
|
2147
|
+
}
|
|
2148
|
+
return { present, removable: reply["removable"], applied: reply["applied"] };
|
|
2149
|
+
}
|
|
2150
|
+
function cleanupCannotHelp(view, at = now()) {
|
|
2151
|
+
const maintenance = view.control === "close" || view.control === "apply";
|
|
2152
|
+
if (view.closed && !maintenance)
|
|
2153
|
+
return "the volume is closed, and no cleanup of this agent's is saved";
|
|
2154
|
+
if (maintenance) return void 0;
|
|
2155
|
+
const room = view.retentions.some(
|
|
2156
|
+
(pin, slot) => pin.until === void 0 || // free
|
|
2157
|
+
pin.until !== 0 && // expired, and no live grant names this exact handle
|
|
2158
|
+
pin.until <= at && !view.grants.some(
|
|
2159
|
+
(g) => g.slot === slot && g.generation === pin.generation && g.until > at
|
|
2160
|
+
)
|
|
2161
|
+
);
|
|
2162
|
+
return room ? void 0 : "every retention slot holds a live pin";
|
|
2163
|
+
}
|
|
2164
|
+
function spCliScratchPad(options) {
|
|
2165
|
+
if (!("creds" in options.nats) || options.nats.creds === void 0) {
|
|
2166
|
+
throw new Error(
|
|
2167
|
+
"ScratchPad: the sp CLI needs NATS_URL and NATS_CREDS; a NATS context or an nkey seed alone is not supported"
|
|
2168
|
+
);
|
|
2169
|
+
}
|
|
2170
|
+
const connection = ["--url", options.nats.url, "--creds", options.nats.creds];
|
|
2171
|
+
const bin = options.bin ?? DEFAULT_SP_BIN;
|
|
2172
|
+
const configuredDir = options.dir ?? node_path.join(node_os.tmpdir(), `scratchpad-${options.volume}`);
|
|
2173
|
+
const grantTtlS = Math.min(options.grantTtlS ?? DEFAULT_GRANT_TTL_S, DEFAULT_GRANT_TTL_S);
|
|
2174
|
+
let dir;
|
|
2175
|
+
let binding;
|
|
2176
|
+
let mutations = Promise.resolve();
|
|
2177
|
+
let mayHavePending = true;
|
|
2178
|
+
let pendingGrant;
|
|
2179
|
+
const ledger = /* @__PURE__ */ new Map();
|
|
2180
|
+
function serialize(work) {
|
|
2181
|
+
const next = mutations.then(work);
|
|
2182
|
+
mutations = next.catch(() => void 0);
|
|
2183
|
+
return next;
|
|
2184
|
+
}
|
|
2185
|
+
const expansions = /* @__PURE__ */ new Map();
|
|
2186
|
+
async function resolveReference(token, trace) {
|
|
2187
|
+
if (!token.startsWith(SHORT_REFERENCE_PREFIX2)) return decodeReference2(token);
|
|
2188
|
+
decodeShortReference(token);
|
|
2189
|
+
const cached = expansions.get(token);
|
|
2190
|
+
if (cached && cached.expires_at > now()) return structuredClone(cached);
|
|
2191
|
+
const reply = await sp(
|
|
2192
|
+
"resolve",
|
|
2193
|
+
["--ref", token],
|
|
2194
|
+
{ trace }
|
|
2195
|
+
);
|
|
2196
|
+
const pair = { short: token, expanded: reply?.reference };
|
|
2197
|
+
validateResolvedReference(pair);
|
|
2198
|
+
if (reply.full_reference !== encodeReference(pair.expanded))
|
|
2199
|
+
throw new ScratchPadError(
|
|
2200
|
+
"sp resolve: expanded token differs from descriptor",
|
|
2201
|
+
"malformed_reply",
|
|
2202
|
+
false
|
|
2203
|
+
);
|
|
2204
|
+
const ref = decodeReference2(reply.full_reference);
|
|
2205
|
+
if (ref.expires_at <= now()) throw new Error("the reference has expired");
|
|
2206
|
+
expansions.delete(token);
|
|
2207
|
+
expansions.set(token, ref);
|
|
2208
|
+
if (expansions.size > 64) expansions.delete(expansions.keys().next().value);
|
|
2209
|
+
return structuredClone(ref);
|
|
2210
|
+
}
|
|
2211
|
+
async function workspace() {
|
|
2212
|
+
dir ??= (async () => {
|
|
2213
|
+
await promises.mkdir(configuredDir, { recursive: true, mode: 448 });
|
|
2214
|
+
return promises.realpath(configuredDir);
|
|
2215
|
+
})();
|
|
2216
|
+
return dir;
|
|
2217
|
+
}
|
|
2218
|
+
async function sp(command, args, opts = {}) {
|
|
2219
|
+
const trace = opts.trace ? ["--thread-id", opts.trace.threadId, "--root-id", opts.trace.rootId] : [];
|
|
2220
|
+
if (opts.trace && opts.toolCallId !== void 0) trace.push("--tool-call-id", opts.toolCallId);
|
|
2221
|
+
const volume = ["read", "resolve"].includes(command) ? [] : ["--volume", options.volume];
|
|
2222
|
+
const argv = [command, ...connection, "--dir", await workspace(), ...volume, ...args, ...trace];
|
|
2223
|
+
const maxStdout = opts.maxStdout ?? MAX_STDOUT;
|
|
2224
|
+
const { code, stdout, stderr } = await new Promise((resolve3) => {
|
|
2225
|
+
const child = node_child_process.execFile(
|
|
2226
|
+
bin,
|
|
2227
|
+
argv,
|
|
2228
|
+
{ maxBuffer: Math.max(maxStdout, MAX_STDERR), encoding: "utf8" },
|
|
2229
|
+
(err, stdout2, stderr2) => {
|
|
2230
|
+
const e = err;
|
|
2231
|
+
resolve3({
|
|
2232
|
+
code: e === null ? 0 : typeof e.code === "number" ? e.code : null,
|
|
2233
|
+
stdout: stdout2 ?? "",
|
|
2234
|
+
stderr: stderr2 || (e !== null && typeof e.code === "string" ? e.message : "")
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
);
|
|
2238
|
+
child.stdin?.end(opts.stdin ?? "");
|
|
2239
|
+
});
|
|
2240
|
+
if (code === null && /^spawn .*(ENOENT|EACCES)/m.test(stderr)) {
|
|
2241
|
+
throw new ScratchPadError(`sp ${command}: ${stderr.trim()}`, "spawn_failed", false);
|
|
2242
|
+
}
|
|
2243
|
+
if (code === 0 && stdout.length <= maxStdout) {
|
|
2244
|
+
try {
|
|
2245
|
+
return JSON.parse(stdout);
|
|
2246
|
+
} catch {
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
let failure = {
|
|
2250
|
+
code: code === 0 ? "malformed_reply" : "failed",
|
|
2251
|
+
message: stderr.trim().slice(0, 512) || `exit ${code ?? "by signal"}`,
|
|
2252
|
+
unknown: opts.mutation === true
|
|
2253
|
+
};
|
|
2254
|
+
if (code !== 0) {
|
|
2255
|
+
try {
|
|
2256
|
+
const e = JSON.parse(stderr).error;
|
|
2257
|
+
if (isObject(e) && typeof e["code"] === "string") {
|
|
2258
|
+
failure = {
|
|
2259
|
+
code: e["code"],
|
|
2260
|
+
message: typeof e["message"] === "string" ? e["message"] : "",
|
|
2261
|
+
unknown: e["unknown"] === true || opts.mutation === true && typeof e["unknown"] !== "boolean"
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
2264
|
+
} catch {
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
throw new ScratchPadError(
|
|
2268
|
+
`sp ${command}: ${failure.code}${failure.unknown ? " (outcome unknown)" : ""}: ${failure.message}`,
|
|
2269
|
+
failure.code,
|
|
2270
|
+
failure.unknown
|
|
2271
|
+
);
|
|
2272
|
+
}
|
|
2273
|
+
function bind(reply, command) {
|
|
2274
|
+
const r = reply;
|
|
2275
|
+
const account = r?.principal?.account;
|
|
2276
|
+
if (typeof account !== "string" || !ACCOUNT_RE.test(account) || !validTarget(r.target)) {
|
|
2277
|
+
throw new ScratchPadError(
|
|
2278
|
+
`sp ${command}: the reply names no principal and target`,
|
|
2279
|
+
"malformed_reply",
|
|
2280
|
+
false
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
if (r.target.volume !== options.volume) {
|
|
2284
|
+
throw new ScratchPadError(
|
|
2285
|
+
`sp ${command}: the workspace is bound to another volume`,
|
|
2286
|
+
"invalid",
|
|
2287
|
+
false
|
|
2288
|
+
);
|
|
2289
|
+
}
|
|
2290
|
+
return { account, target: { volume: r.target.volume, creation_id: r.target.creation_id } };
|
|
2291
|
+
}
|
|
2292
|
+
async function init(trace) {
|
|
2293
|
+
binding ??= bind(await sp("init", [], { trace, mutation: true }), "init");
|
|
2294
|
+
return binding;
|
|
2295
|
+
}
|
|
2296
|
+
async function describe3(trace) {
|
|
2297
|
+
if (binding !== void 0) return binding;
|
|
2298
|
+
try {
|
|
2299
|
+
binding = bind(await sp("describe", [], { trace }), "describe");
|
|
2300
|
+
} catch (err) {
|
|
2301
|
+
if (err instanceof ScratchPadError && NO_VOLUME_CODES.includes(err.code)) return void 0;
|
|
2302
|
+
throw err;
|
|
2303
|
+
}
|
|
2304
|
+
return binding;
|
|
2305
|
+
}
|
|
2306
|
+
const ownedBy = (ref, b) => b !== void 0 && ref.account === b.account && ref.target.volume === b.target.volume && ref.target.creation_id === b.target.creation_id;
|
|
2307
|
+
const ledgerKey = (ref, grantee) => JSON.stringify([
|
|
2308
|
+
ref.account,
|
|
2309
|
+
ref.target.volume,
|
|
2310
|
+
ref.target.creation_id,
|
|
2311
|
+
ref.root,
|
|
2312
|
+
ref.path,
|
|
2313
|
+
grantee.account,
|
|
2314
|
+
grantee.user
|
|
2315
|
+
]);
|
|
2316
|
+
function prune() {
|
|
2317
|
+
const t = now();
|
|
2318
|
+
for (const [key, until] of ledger) {
|
|
2319
|
+
if (Math.max(until["first-hand"] ?? 0, until["on-request"] ?? 0) <= t) ledger.delete(key);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
function record2(key, kind, expiresAt) {
|
|
2323
|
+
ledger.set(key, { ...ledger.get(key), [kind]: expiresAt });
|
|
2324
|
+
}
|
|
2325
|
+
function grantExpiry(reply, ref) {
|
|
2326
|
+
const r = reply;
|
|
2327
|
+
if (!isObject(r) || !isObject(r.handle) || !positive(r.expires_at)) {
|
|
2328
|
+
throw new ScratchPadError(
|
|
2329
|
+
"sp grant: the reply names no handle and expiry",
|
|
2330
|
+
"malformed_reply",
|
|
2331
|
+
true
|
|
2332
|
+
);
|
|
2333
|
+
}
|
|
2334
|
+
return Math.min(r.expires_at, ref.expires_at);
|
|
2335
|
+
}
|
|
2336
|
+
async function publish(bytes, path, operationId, trace) {
|
|
2337
|
+
return sp(
|
|
2338
|
+
"publish-file",
|
|
2339
|
+
[
|
|
2340
|
+
"--source",
|
|
2341
|
+
"-",
|
|
2342
|
+
"--path",
|
|
2343
|
+
path,
|
|
2344
|
+
"--operation-id",
|
|
2345
|
+
operationId,
|
|
2346
|
+
"--creator",
|
|
2347
|
+
options.creator,
|
|
2348
|
+
"--ttl-seconds",
|
|
2349
|
+
String(HANDOFF_TTL_S)
|
|
2350
|
+
],
|
|
2351
|
+
{ trace, stdin: bytes, mutation: true }
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
2354
|
+
async function recover(trace) {
|
|
2355
|
+
if (!mayHavePending) return;
|
|
2356
|
+
try {
|
|
2357
|
+
await sp("recover", [], { trace, mutation: true });
|
|
2358
|
+
} catch (err) {
|
|
2359
|
+
if (!(err instanceof ScratchPadError) || err.unknown) throw err;
|
|
2360
|
+
}
|
|
2361
|
+
mayHavePending = false;
|
|
2362
|
+
}
|
|
2363
|
+
async function removeRefusedSpool(refusal2, trace) {
|
|
2364
|
+
let why;
|
|
2365
|
+
try {
|
|
2366
|
+
const spool = decodeSpool(await sp("spool-remove", ["--apply"], { trace, mutation: true }));
|
|
2367
|
+
if (!spool.present || spool.applied) return;
|
|
2368
|
+
why = "the refused publication's spool is protected";
|
|
2369
|
+
} catch (err) {
|
|
2370
|
+
if (err instanceof ScratchPadError && err.unknown) throw err;
|
|
2371
|
+
why = `its spool was not removed: ${err.message}`;
|
|
2372
|
+
}
|
|
2373
|
+
mayHavePending = true;
|
|
2374
|
+
throw new ScratchPadError(
|
|
2375
|
+
`${refusal2.message}; cleanup not started: ${why}`,
|
|
2376
|
+
refusal2.code,
|
|
2377
|
+
false
|
|
2378
|
+
);
|
|
2379
|
+
}
|
|
2380
|
+
async function volumeView(trace) {
|
|
2381
|
+
return decodeDescribe(await sp("describe", [], { trace }));
|
|
2382
|
+
}
|
|
2383
|
+
async function commit(text2, path, trace) {
|
|
2384
|
+
if (options.creator === void 0) {
|
|
2385
|
+
throw new Error(
|
|
2386
|
+
"ScratchPad: a reference names its creator; this ScratchPad was made without one"
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
const own = await init(trace);
|
|
2390
|
+
await recover(trace);
|
|
2391
|
+
const operationId = node_crypto.randomUUID();
|
|
2392
|
+
let reply;
|
|
2393
|
+
try {
|
|
2394
|
+
reply = await publish(text2, path, operationId, trace);
|
|
2395
|
+
} catch (err) {
|
|
2396
|
+
if (err instanceof ScratchPadError && err.unknown) mayHavePending = true;
|
|
2397
|
+
if (!(err instanceof ScratchPadError) || err.code !== "capacity" || err.unknown) throw err;
|
|
2398
|
+
await removeRefusedSpool(err, trace);
|
|
2399
|
+
let reason;
|
|
2400
|
+
try {
|
|
2401
|
+
reason = cleanupCannotHelp(await volumeView(trace));
|
|
2402
|
+
} catch (inspect) {
|
|
2403
|
+
throw new ScratchPadError(
|
|
2404
|
+
`${err.message}; cleanup not started: ${inspect.message}`,
|
|
2405
|
+
err.code,
|
|
2406
|
+
false
|
|
2407
|
+
);
|
|
2408
|
+
}
|
|
2409
|
+
if (reason !== void 0)
|
|
2410
|
+
throw new ScratchPadError(
|
|
2411
|
+
`${err.message}; cleanup not started: ${reason}`,
|
|
2412
|
+
err.code,
|
|
2413
|
+
false
|
|
2414
|
+
);
|
|
2415
|
+
try {
|
|
2416
|
+
await sp("maintain", ["--apply"], { trace, mutation: true });
|
|
2417
|
+
} catch (cleanup) {
|
|
2418
|
+
mayHavePending = true;
|
|
2419
|
+
const unknown2 = !(cleanup instanceof ScratchPadError) || cleanup.unknown;
|
|
2420
|
+
throw new ScratchPadError(
|
|
2421
|
+
`${err.message}; cleanup: ${cleanup.message}`,
|
|
2422
|
+
unknown2 && cleanup instanceof ScratchPadError ? cleanup.code : unknown2 ? "failed" : err.code,
|
|
2423
|
+
unknown2
|
|
2424
|
+
);
|
|
2425
|
+
}
|
|
2426
|
+
try {
|
|
2427
|
+
reply = await publish(text2, path, operationId, trace);
|
|
2428
|
+
} catch (again) {
|
|
2429
|
+
if (again instanceof ScratchPadError && again.unknown) mayHavePending = true;
|
|
2430
|
+
throw again;
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
const token = reply?.reference;
|
|
2434
|
+
if (typeof token !== "string" || !isObject(reply.receipt)) {
|
|
2435
|
+
mayHavePending = true;
|
|
2436
|
+
throw new ScratchPadError(
|
|
2437
|
+
"sp publish-file: the reply names no receipt and reference",
|
|
2438
|
+
"malformed_reply",
|
|
2439
|
+
true
|
|
2440
|
+
);
|
|
2441
|
+
}
|
|
2442
|
+
const ref = await resolveReference(token, trace);
|
|
2443
|
+
if (!ownedBy(ref, own) || ref.path !== path || ref.creator !== options.creator) {
|
|
2444
|
+
throw new ScratchPadError(
|
|
2445
|
+
"sp publish-file: the reference names another volume, path or creator",
|
|
2446
|
+
"malformed_reply",
|
|
2447
|
+
false
|
|
2448
|
+
);
|
|
2449
|
+
}
|
|
2450
|
+
return token;
|
|
2451
|
+
}
|
|
2452
|
+
async function runGrant(args, trace, toolCallId, ref) {
|
|
2453
|
+
return grantExpiry(await sp("grant", args, { trace, toolCallId, mutation: true }), ref);
|
|
2454
|
+
}
|
|
2455
|
+
async function grantRead(reference, grantee, trace, toolCallId, kind) {
|
|
2456
|
+
const ref = await resolveReference(reference, trace);
|
|
2457
|
+
if (!ownedBy(ref, await describe3(trace))) {
|
|
2458
|
+
throw new Error(
|
|
2459
|
+
"the reference points into another agent's volume; only its creator can grant read on it"
|
|
2460
|
+
);
|
|
2461
|
+
}
|
|
2462
|
+
await recover(trace);
|
|
2463
|
+
if (pendingGrant !== void 0) {
|
|
2464
|
+
const earlier = pendingGrant;
|
|
2465
|
+
try {
|
|
2466
|
+
record2(
|
|
2467
|
+
earlier.key,
|
|
2468
|
+
earlier.kind,
|
|
2469
|
+
await runGrant(earlier.args, void 0, void 0, earlier.ref)
|
|
2470
|
+
);
|
|
2471
|
+
pendingGrant = void 0;
|
|
2472
|
+
} catch (err) {
|
|
2473
|
+
if (!(err instanceof ScratchPadError) || err.unknown) throw err;
|
|
2474
|
+
pendingGrant = void 0;
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
const remaining = ref.expires_at - now() - EXPIRY_MARGIN_S;
|
|
2478
|
+
const ttl = Math.floor(Math.min(grantTtlS, remaining));
|
|
2479
|
+
if (ttl < 1)
|
|
2480
|
+
throw new Error(
|
|
2481
|
+
"the reference has expired, or is about to; there is nothing left to grant on"
|
|
2482
|
+
);
|
|
2483
|
+
const key = ledgerKey(ref, grantee);
|
|
2484
|
+
prune();
|
|
2485
|
+
if (!ledger.has(key) && ledger.size >= MAX_LEDGER) {
|
|
2486
|
+
throw new Error(
|
|
2487
|
+
`this agent already holds ${MAX_LEDGER} live grants; try again when one expires`
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
const trail = trace ? ["--thread-id", trace.threadId, "--root-id", trace.rootId] : [];
|
|
2491
|
+
if (trace && toolCallId !== void 0) trail.push("--tool-call-id", toolCallId);
|
|
2492
|
+
const args = [
|
|
2493
|
+
"--ref",
|
|
2494
|
+
reference,
|
|
2495
|
+
"--grantee-account",
|
|
2496
|
+
grantee.account,
|
|
2497
|
+
"--grantee-user",
|
|
2498
|
+
grantee.user,
|
|
2499
|
+
"--operation-id",
|
|
2500
|
+
node_crypto.randomUUID(),
|
|
2501
|
+
"--ttl-seconds",
|
|
2502
|
+
String(ttl)
|
|
2503
|
+
];
|
|
2504
|
+
try {
|
|
2505
|
+
record2(key, kind, await runGrant(args, trace, toolCallId, ref));
|
|
2506
|
+
} catch (err) {
|
|
2507
|
+
if (err instanceof ScratchPadError && err.unknown)
|
|
2508
|
+
pendingGrant = { args: [...args, ...trail], key, kind, ref };
|
|
2509
|
+
throw err;
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
const pad = {
|
|
2513
|
+
commit(text2, path, trace) {
|
|
2514
|
+
return serialize(() => commit(text2, path, trace));
|
|
2515
|
+
},
|
|
2516
|
+
async read(reference, trace) {
|
|
2517
|
+
const ref = await resolveReference(reference, trace);
|
|
2518
|
+
if (ref.expires_at <= now()) throw new Error("the reference has expired");
|
|
2519
|
+
const got = await sp(
|
|
2520
|
+
"read",
|
|
2521
|
+
["--ref", reference],
|
|
2522
|
+
{
|
|
2523
|
+
trace,
|
|
2524
|
+
maxStdout: MAX_READ_STDOUT
|
|
2525
|
+
}
|
|
2526
|
+
);
|
|
2527
|
+
const { data_base64: data, bytes } = got ?? {};
|
|
2528
|
+
if (typeof data !== "string" || !BASE64_RE.test(data) || !Number.isSafeInteger(bytes) || bytes > MAX_READ_BYTES2) {
|
|
2529
|
+
throw new ScratchPadError("sp read: the reply carries no data", "malformed_reply", false);
|
|
2530
|
+
}
|
|
2531
|
+
const raw = Buffer.from(data, "base64");
|
|
2532
|
+
if (raw.length !== bytes || raw.toString("base64") !== data) {
|
|
2533
|
+
throw new ScratchPadError(
|
|
2534
|
+
"sp read: the data does not match its length",
|
|
2535
|
+
"malformed_reply",
|
|
2536
|
+
false
|
|
2537
|
+
);
|
|
2538
|
+
}
|
|
2539
|
+
try {
|
|
2540
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(raw);
|
|
2541
|
+
} catch {
|
|
2542
|
+
throw new Error("the referenced file is not UTF-8 text");
|
|
2543
|
+
}
|
|
2544
|
+
},
|
|
2545
|
+
resolveReference,
|
|
2546
|
+
async owns(reference) {
|
|
2547
|
+
let ref;
|
|
2548
|
+
try {
|
|
2549
|
+
ref = await resolveReference(reference);
|
|
2550
|
+
} catch {
|
|
2551
|
+
return false;
|
|
2552
|
+
}
|
|
2553
|
+
return ownedBy(ref, await describe3(activeTrace()));
|
|
2554
|
+
},
|
|
2555
|
+
grantRead(reference, grantee, trace, toolCallId, kind = "first-hand") {
|
|
2556
|
+
return serialize(() => grantRead(reference, grantee, trace, toolCallId, kind));
|
|
2557
|
+
},
|
|
2558
|
+
granted(reference, grantee) {
|
|
2559
|
+
const find = (ref) => {
|
|
2560
|
+
if (!ownedBy(ref, binding)) return void 0;
|
|
2561
|
+
const until = ledger.get(ledgerKey(ref, grantee)), t = now();
|
|
2562
|
+
if ((until?.["first-hand"] ?? 0) > t) return "first-hand";
|
|
2563
|
+
if ((until?.["on-request"] ?? 0) > t) return "on-request";
|
|
2564
|
+
return void 0;
|
|
2565
|
+
};
|
|
2566
|
+
if (reference.startsWith(SHORT_REFERENCE_PREFIX2))
|
|
2567
|
+
return resolveReference(reference).then(find, () => void 0);
|
|
2568
|
+
try {
|
|
2569
|
+
return find(decodeReference2(reference));
|
|
2570
|
+
} catch {
|
|
2571
|
+
return void 0;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
};
|
|
2575
|
+
return pad;
|
|
2576
|
+
}
|
|
2577
|
+
var SCRATCHPAD_VOLUME_VAR = "SCRATCHPAD_VOLUME";
|
|
2578
|
+
var SCRATCHPAD_BIN_VAR = "SCRATCHPAD_BIN";
|
|
2579
|
+
var SCRATCHPAD_DIR_VAR = "SCRATCHPAD_DIR";
|
|
2580
|
+
var SCRATCHPAD_GRANT_TTL_S_VAR = "SCRATCHPAD_GRANT_TTL_S";
|
|
2581
|
+
function scratchPadFromEnv(env = process.env, options = {}) {
|
|
2582
|
+
const optional = (name) => {
|
|
2583
|
+
const value = env[name];
|
|
2584
|
+
return value === void 0 || value === "" ? void 0 : value;
|
|
2585
|
+
};
|
|
2586
|
+
const volume = optional(SCRATCHPAD_VOLUME_VAR);
|
|
2587
|
+
if (volume === void 0) return void 0;
|
|
2588
|
+
const ttl = optional(SCRATCHPAD_GRANT_TTL_S_VAR);
|
|
2589
|
+
let grantTtlS;
|
|
2590
|
+
if (ttl !== void 0) {
|
|
2591
|
+
grantTtlS = Number(ttl);
|
|
2592
|
+
if (!Number.isFinite(grantTtlS) || grantTtlS <= 0) {
|
|
2593
|
+
throw new Error(`${SCRATCHPAD_GRANT_TTL_S_VAR} must be a positive number, got "${ttl}"`);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
const context = optional("NATS_CONTEXT");
|
|
2597
|
+
const creds = optional("NATS_CREDS");
|
|
2598
|
+
const url = optional("NATS_URL") ?? "nats://127.0.0.1:4222";
|
|
2599
|
+
const nats = options.nats ?? (creds !== void 0 ? { url, creds } : context !== void 0 ? { context } : { url });
|
|
2600
|
+
const bin = optional(SCRATCHPAD_BIN_VAR);
|
|
2601
|
+
const dir = optional(SCRATCHPAD_DIR_VAR);
|
|
2602
|
+
return spCliScratchPad({
|
|
2603
|
+
volume,
|
|
2604
|
+
nats,
|
|
2605
|
+
...bin !== void 0 ? { bin } : {},
|
|
2606
|
+
...dir !== void 0 ? { dir } : {},
|
|
2607
|
+
...grantTtlS !== void 0 ? { grantTtlS } : {},
|
|
2608
|
+
...options.creator !== void 0 ? { creator: options.creator } : {}
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
var LOOP_GUARD_CODE = 409;
|
|
2612
|
+
function loopGuardDescription(rootId) {
|
|
2613
|
+
return `the agent's model is waiting inside a prompt of this tree (root ${rootId}); a prompt of the same tree cannot be served until that call returns`;
|
|
2614
|
+
}
|
|
2615
|
+
function openCalls(result) {
|
|
2616
|
+
if (typeof result !== "object" || result === null || Array.isArray(result)) return void 0;
|
|
2617
|
+
const open = result.open_calls;
|
|
2618
|
+
if (open === void 0) return 0;
|
|
2619
|
+
return typeof open === "number" && Number.isInteger(open) && open >= 0 ? open : void 0;
|
|
2620
|
+
}
|
|
2621
|
+
function isThenable(value) {
|
|
2622
|
+
return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
|
|
2623
|
+
}
|
|
2624
|
+
function loopGuard() {
|
|
2625
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
2626
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
2627
|
+
const waiting = (rootId) => {
|
|
2628
|
+
const entries = byRoot.get(rootId);
|
|
2629
|
+
if (entries === void 0) return false;
|
|
2630
|
+
for (const entry of entries) if (entry.waiting) return true;
|
|
2631
|
+
return false;
|
|
2632
|
+
};
|
|
2633
|
+
const toolCallEnded = (entry, returned, result) => {
|
|
2634
|
+
entry.running -= 1;
|
|
2635
|
+
const open = returned ? openCalls(result) : void 0;
|
|
2636
|
+
if (open === void 0) return;
|
|
2637
|
+
entry.open = open;
|
|
2638
|
+
if (entry.running === 0 && entry.open === 0) entry.waiting = false;
|
|
2639
|
+
};
|
|
2640
|
+
return {
|
|
2641
|
+
requestInterceptor: {
|
|
2642
|
+
async aroundRequest(_ctx, next) {
|
|
2643
|
+
const scope = activeTrace();
|
|
2644
|
+
if (scope === void 0) {
|
|
2645
|
+
await next();
|
|
2646
|
+
return;
|
|
2647
|
+
}
|
|
2648
|
+
const { rootId } = scope;
|
|
2649
|
+
if (waiting(rootId)) {
|
|
2650
|
+
throw new agentService.RequestRejectedError(LOOP_GUARD_CODE, loopGuardDescription(rootId));
|
|
2651
|
+
}
|
|
2652
|
+
const entry = { rootId, waiting: false, running: 0, open: 0 };
|
|
2653
|
+
byScope.set(scope, entry);
|
|
2654
|
+
let ofRoot = byRoot.get(rootId);
|
|
2655
|
+
if (ofRoot === void 0) byRoot.set(rootId, ofRoot = /* @__PURE__ */ new Set());
|
|
2656
|
+
ofRoot.add(entry);
|
|
2657
|
+
try {
|
|
2658
|
+
await next();
|
|
2659
|
+
} finally {
|
|
2660
|
+
byScope.delete(scope);
|
|
2661
|
+
ofRoot.delete(entry);
|
|
2662
|
+
if (ofRoot.size === 0) byRoot.delete(rootId);
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
},
|
|
2666
|
+
promptInterceptor: {
|
|
2667
|
+
beforePrompt() {
|
|
2668
|
+
const scope = activeTrace();
|
|
2669
|
+
const entry = scope === void 0 ? void 0 : byScope.get(scope);
|
|
2670
|
+
if (entry !== void 0) entry.waiting = true;
|
|
2671
|
+
}
|
|
2672
|
+
},
|
|
2673
|
+
aroundToolCall(scope, run) {
|
|
2674
|
+
const entry = scope === void 0 ? void 0 : byScope.get(scope);
|
|
2675
|
+
if (entry === void 0) return run();
|
|
2676
|
+
entry.running += 1;
|
|
2677
|
+
let value;
|
|
2678
|
+
try {
|
|
2679
|
+
value = run();
|
|
2680
|
+
} catch (err) {
|
|
2681
|
+
toolCallEnded(entry, false);
|
|
2682
|
+
throw err;
|
|
2683
|
+
}
|
|
2684
|
+
if (isThenable(value)) {
|
|
2685
|
+
void value.then(
|
|
2686
|
+
(result) => toolCallEnded(entry, true, result),
|
|
2687
|
+
() => toolCallEnded(entry, false)
|
|
2688
|
+
);
|
|
2689
|
+
} else {
|
|
2690
|
+
toolCallEnded(entry, true, value);
|
|
2691
|
+
}
|
|
2692
|
+
return value;
|
|
2693
|
+
},
|
|
2694
|
+
serving: (rootId) => byRoot.has(rootId),
|
|
2695
|
+
waiting
|
|
2696
|
+
};
|
|
2697
|
+
}
|
|
2698
|
+
function pathRequest(v) {
|
|
2699
|
+
validateTarget(v.target);
|
|
2700
|
+
validateContentID(v.root);
|
|
2701
|
+
validatePath(v.path);
|
|
2702
|
+
validateTrace(v.trace);
|
|
2703
|
+
}
|
|
2704
|
+
function validateRequest(verb, value) {
|
|
2705
|
+
switch (verb) {
|
|
2706
|
+
case "reference.info":
|
|
2707
|
+
case "info":
|
|
2708
|
+
validateEmpty(value);
|
|
2709
|
+
return;
|
|
2710
|
+
case "volume.create": {
|
|
2711
|
+
const v = record(value, ["volume", "action"]);
|
|
2712
|
+
validateVolume(v.volume);
|
|
2713
|
+
validateAction(v.action);
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
case "volume.get": {
|
|
2717
|
+
const v = record(value, ["target"]);
|
|
2718
|
+
validateTarget(v.target);
|
|
2719
|
+
return;
|
|
2720
|
+
}
|
|
2721
|
+
case "object.prepare": {
|
|
2722
|
+
const v = record(value, ["target", "objects"]);
|
|
2723
|
+
validateTarget(v.target);
|
|
2724
|
+
check(
|
|
2725
|
+
Array.isArray(v.objects) && v.objects.length > 0 && v.objects.length <= MAX_PREPARE_OBJECTS,
|
|
2726
|
+
"invalid preparation count"
|
|
2727
|
+
);
|
|
2728
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2729
|
+
for (const raw of v.objects) {
|
|
2730
|
+
validateObjectSpec(raw);
|
|
2731
|
+
const key = raw.object.kind + ":" + raw.object.id;
|
|
2732
|
+
check(!seen.has(key), "duplicate preparation object");
|
|
2733
|
+
seen.add(key);
|
|
2734
|
+
}
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
case "object.verify": {
|
|
2738
|
+
const v = record(value, ["target", "epoch", "objects"]);
|
|
2739
|
+
validateTarget(v.target);
|
|
2740
|
+
uint(v.epoch);
|
|
2741
|
+
check(
|
|
2742
|
+
Array.isArray(v.objects) && v.objects.length > 0 && v.objects.length <= MAX_PREPARE_OBJECTS,
|
|
2743
|
+
"invalid verification count"
|
|
2744
|
+
);
|
|
2745
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2746
|
+
for (const raw of v.objects) {
|
|
2747
|
+
validateContentObject(raw);
|
|
2748
|
+
const key = raw.kind + ":" + raw.id;
|
|
2749
|
+
check(!seen.has(key), "duplicate verification object");
|
|
2750
|
+
seen.add(key);
|
|
2751
|
+
}
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
case "object.put": {
|
|
2755
|
+
const v = record(value, ["target", "object", "epoch"]);
|
|
2756
|
+
validateTarget(v.target);
|
|
2757
|
+
validateContentObject(v.object);
|
|
2758
|
+
uint(v.epoch);
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
case "reference.retain":
|
|
2762
|
+
validateReferenceRetainRequest(value);
|
|
2763
|
+
return;
|
|
2764
|
+
case "reference.publish":
|
|
2765
|
+
case "reference.receipt":
|
|
2766
|
+
validateReferencePublishRequest(value);
|
|
2767
|
+
return;
|
|
2768
|
+
case "reference.resolve":
|
|
2769
|
+
case "reference.stat": {
|
|
2770
|
+
const v = record(value, ["reference", "trace"]);
|
|
2771
|
+
text(v.reference, SHORT_REFERENCE_BYTES, SHORT_REFERENCE_BYTES);
|
|
2772
|
+
decodeShortReference(v.reference);
|
|
2773
|
+
validateTrace(v.trace);
|
|
2774
|
+
return;
|
|
2775
|
+
}
|
|
2776
|
+
case "checkpoint.publish": {
|
|
2777
|
+
const v = record(value, ["target", "command"]);
|
|
2778
|
+
validateTarget(v.target);
|
|
2779
|
+
validateCommand(v.command);
|
|
2780
|
+
check(
|
|
2781
|
+
v.target.creation_id === v.command.creation_id,
|
|
2782
|
+
"command names another creation identity"
|
|
2783
|
+
);
|
|
2784
|
+
return;
|
|
2785
|
+
}
|
|
2786
|
+
case "writer.reset": {
|
|
2787
|
+
const v = record(value, ["target", "writer"]);
|
|
2788
|
+
validateTarget(v.target);
|
|
2789
|
+
validateWriter(v.writer);
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2792
|
+
case "root.retain": {
|
|
2793
|
+
const v = record(value, ["target", "slot", "generation", "root", "until", "action"]);
|
|
2794
|
+
validateTarget(v.target);
|
|
2795
|
+
uint(v.slot, RETENTION_SLOTS - 1);
|
|
2796
|
+
uint(v.generation);
|
|
2797
|
+
validateContentID(v.root);
|
|
2798
|
+
uint(v.until);
|
|
2799
|
+
validateAction(v.action);
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
case "root.release": {
|
|
2803
|
+
const v = record(value, ["target", "handle"]);
|
|
2804
|
+
validateTarget(v.target);
|
|
2805
|
+
validateRetentionHandle(v.handle);
|
|
2806
|
+
return;
|
|
2807
|
+
}
|
|
2808
|
+
case "grant.issue": {
|
|
2809
|
+
const v = record(value, [
|
|
2810
|
+
"target",
|
|
2811
|
+
"slot",
|
|
2812
|
+
"generation",
|
|
2813
|
+
"principal",
|
|
2814
|
+
"root",
|
|
2815
|
+
"path",
|
|
2816
|
+
"retention",
|
|
2817
|
+
"until",
|
|
2818
|
+
"action"
|
|
2819
|
+
]);
|
|
2820
|
+
validateTarget(v.target);
|
|
2821
|
+
uint(v.slot, GRANT_SLOTS - 1);
|
|
2822
|
+
uint(v.generation);
|
|
2823
|
+
validatePrincipal(v.principal);
|
|
2824
|
+
validateContentID(v.root);
|
|
2825
|
+
validatePath(v.path, 256);
|
|
2826
|
+
validateRetentionHandle(v.retention);
|
|
2827
|
+
uint(v.until, void 0, 1);
|
|
2828
|
+
validateAction(v.action);
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
case "grant.revoke": {
|
|
2832
|
+
const v = record(value, ["target", "handle", "action"]);
|
|
2833
|
+
validateTarget(v.target);
|
|
2834
|
+
validateGrantHandle(v.handle);
|
|
2835
|
+
validateAction(v.action);
|
|
2836
|
+
return;
|
|
2837
|
+
}
|
|
2838
|
+
case "content.stat": {
|
|
2839
|
+
const v = record(value, ["target", "root", "path", "trace"]);
|
|
2840
|
+
pathRequest(v);
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2843
|
+
case "content.read": {
|
|
2844
|
+
const v = record(value, ["target", "root", "path", "trace", "offset", "length"]);
|
|
2845
|
+
pathRequest(v);
|
|
2846
|
+
uint(v.offset);
|
|
2847
|
+
uint(v.length, MAX_READ_BYTES, 1);
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
case "content.list": {
|
|
2851
|
+
const v = record(value, ["target", "root", "path", "trace", "offset", "limit"]);
|
|
2852
|
+
pathRequest(v);
|
|
2853
|
+
uint(v.offset, 4294967295);
|
|
2854
|
+
uint(v.limit, 64, 1);
|
|
2855
|
+
return;
|
|
2856
|
+
}
|
|
2857
|
+
case "maintenance.close":
|
|
2858
|
+
case "maintenance.plan":
|
|
2859
|
+
case "maintenance.apply": {
|
|
2860
|
+
const v = record(value, ["target", "revision"]);
|
|
2861
|
+
validateTarget(v.target);
|
|
2862
|
+
uint(v.revision, void 0, 1);
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
default:
|
|
2866
|
+
throw invalid("unknown ScratchPad verb");
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
var ScratchPadTransport = class {
|
|
2870
|
+
constructor(nc, options = {}) {
|
|
2871
|
+
this.nc = nc;
|
|
2872
|
+
check(nc && !nc.isClosed() && !nc.isDraining(), "an open NATS connection is required");
|
|
2873
|
+
check(nc.info?.headers === true, "NATS header support is required");
|
|
2874
|
+
check(
|
|
2875
|
+
(nc.info?.max_payload ?? 0) >= MAX_NODE_BYTES + MAX_HEADER_BYTES,
|
|
2876
|
+
"NATS max_payload cannot carry the largest canonical object"
|
|
2877
|
+
);
|
|
2878
|
+
this.timeoutMs = options.timeoutMs ?? 1e4;
|
|
2879
|
+
uint(this.timeoutMs, 5 * 60 * 1e3, 1);
|
|
2880
|
+
}
|
|
2881
|
+
nc;
|
|
2882
|
+
timeoutMs;
|
|
2883
|
+
async within(work, deadline, signal) {
|
|
2884
|
+
void work.catch(() => {
|
|
2885
|
+
});
|
|
2886
|
+
if (signal?.aborted) throw signal.reason ?? new Error("operation cancelled");
|
|
2887
|
+
const remaining = deadline - performance.now();
|
|
2888
|
+
if (remaining <= 0) throw new Error("request deadline exceeded");
|
|
2889
|
+
return new Promise((resolve3, reject2) => {
|
|
2890
|
+
const timer = setTimeout(() => {
|
|
2891
|
+
cleanup();
|
|
2892
|
+
reject2(new Error("request deadline exceeded"));
|
|
2893
|
+
}, remaining);
|
|
2894
|
+
const cancel = () => {
|
|
2895
|
+
cleanup();
|
|
2896
|
+
const cause = signal?.reason;
|
|
2897
|
+
reject2(cause instanceof Error ? cause : new Error("operation cancelled", { cause }));
|
|
2898
|
+
};
|
|
2899
|
+
const cleanup = () => {
|
|
2900
|
+
clearTimeout(timer);
|
|
2901
|
+
signal?.removeEventListener("abort", cancel);
|
|
2902
|
+
};
|
|
2903
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
2904
|
+
void work.then(
|
|
2905
|
+
(value) => {
|
|
2906
|
+
cleanup();
|
|
2907
|
+
resolve3(value);
|
|
2908
|
+
},
|
|
2909
|
+
(error) => {
|
|
2910
|
+
cleanup();
|
|
2911
|
+
reject2(error instanceof Error ? error : new Error("request failed", { cause: error }));
|
|
2912
|
+
}
|
|
2913
|
+
);
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
async exchange(verb, body, mutation, ranged, options, metadata) {
|
|
2917
|
+
let headerBytes = 0;
|
|
2918
|
+
const requestHeaders = metadata === void 0 ? void 0 : natsCore.headers();
|
|
2919
|
+
if (metadata !== void 0) {
|
|
2920
|
+
headerBytes = 12 + Buffer.byteLength(METADATA_HEADER) + 4 + Buffer.byteLength(metadata);
|
|
2921
|
+
check(
|
|
2922
|
+
headerBytes <= MAX_HEADER_BYTES && !/[\r\n\0]/.test(metadata),
|
|
2923
|
+
"invalid request header"
|
|
2924
|
+
);
|
|
2925
|
+
requestHeaders.set(METADATA_HEADER, metadata);
|
|
2926
|
+
}
|
|
2927
|
+
check(
|
|
2928
|
+
body.length + headerBytes <= (this.nc.info?.max_payload ?? 0),
|
|
2929
|
+
"request exceeds NATS payload bound"
|
|
2930
|
+
);
|
|
2931
|
+
const deadline = performance.now() + this.timeoutMs;
|
|
2932
|
+
try {
|
|
2933
|
+
if (options.signal?.aborted || this.nc.isClosed() || this.nc.isDraining())
|
|
2934
|
+
throw new Error("connection or signal closed before transmission");
|
|
2935
|
+
await this.within(this.nc.flush(), deadline, options.signal);
|
|
2936
|
+
if (options.signal?.aborted || this.nc.isClosed() || this.nc.isDraining())
|
|
2937
|
+
throw new Error("connection or signal closed before transmission");
|
|
2938
|
+
if (deadline <= performance.now()) throw new Error("deadline ended before transmission");
|
|
2939
|
+
check(this.nc.info?.headers === true, "NATS header support is required");
|
|
2940
|
+
check(
|
|
2941
|
+
body.length + headerBytes <= (this.nc.info?.max_payload ?? 0),
|
|
2942
|
+
"request exceeds current NATS payload bound"
|
|
2943
|
+
);
|
|
2944
|
+
} catch (cause) {
|
|
2945
|
+
throw new NativeScratchPadError("unavailable", "request not transmitted", {
|
|
2946
|
+
local: true,
|
|
2947
|
+
cause
|
|
2948
|
+
});
|
|
2949
|
+
}
|
|
2950
|
+
let reply;
|
|
2951
|
+
try {
|
|
2952
|
+
const request = this.nc.request(SUBJECT_PREFIX + verb, body, {
|
|
2953
|
+
timeout: Math.max(1, Math.ceil(deadline - performance.now())),
|
|
2954
|
+
...requestHeaders === void 0 ? {} : { headers: requestHeaders }
|
|
2955
|
+
});
|
|
2956
|
+
reply = await this.within(request, deadline, options.signal);
|
|
2957
|
+
} catch (cause) {
|
|
2958
|
+
throw new NativeScratchPadError(
|
|
2959
|
+
mutation ? "unknown" : "unavailable",
|
|
2960
|
+
"request returned no trustworthy reply",
|
|
2961
|
+
{ unknown: mutation, cause }
|
|
2962
|
+
);
|
|
2963
|
+
}
|
|
2964
|
+
try {
|
|
2965
|
+
let count2 = 0, size = 12, value = "";
|
|
2966
|
+
for (const [key, values] of reply.headers ?? []) {
|
|
2967
|
+
for (const item of values) size += Buffer.byteLength(key) + 4 + Buffer.byteLength(item);
|
|
2968
|
+
if (key.toLowerCase() === REPLY_HEADER.toLowerCase()) {
|
|
2969
|
+
count2 += values.length;
|
|
2970
|
+
value = values[0] ?? "";
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
check(
|
|
2974
|
+
size <= MAX_HEADER_BYTES && count2 === 1,
|
|
2975
|
+
"reply must have one bounded ScratchPad-Reply header"
|
|
2976
|
+
);
|
|
2977
|
+
const meta = parseStrictJSON(value, MAX_HEADER_BYTES);
|
|
2978
|
+
validateReplyMetadata(meta);
|
|
2979
|
+
if (meta.code !== "ok") {
|
|
2980
|
+
check(reply.data.length === 0, "error reply has a body");
|
|
2981
|
+
throw new NativeScratchPadError(meta.code, meta.message, {
|
|
2982
|
+
unknown: mutation && ["unknown", "unavailable", "internal"].includes(meta.code)
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2985
|
+
check(ranged === (meta.range !== void 0), "reply range does not match verb");
|
|
2986
|
+
check(
|
|
2987
|
+
reply.data.length <= (ranged ? MAX_READ_BYTES : MAX_REPLY_BYTES),
|
|
2988
|
+
"reply body exceeds bound"
|
|
2989
|
+
);
|
|
2990
|
+
return { metadata: meta, body: reply.data };
|
|
2991
|
+
} catch (cause) {
|
|
2992
|
+
if (cause instanceof NativeScratchPadError && !cause.local) throw cause;
|
|
2993
|
+
throw failed(mutation, "reply metadata is malformed", cause);
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
async control(verb, request, validate, mutation, options) {
|
|
2997
|
+
validateRequest(verb, request);
|
|
2998
|
+
const body = Buffer.from(goJSON(request));
|
|
2999
|
+
check(body.length <= MAX_CONTROL_BYTES, "control request exceeds bound");
|
|
3000
|
+
const reply = await this.exchange(verb, body, mutation, false, options);
|
|
3001
|
+
try {
|
|
3002
|
+
const result = parseStrictJSON(reply.body, MAX_REPLY_BYTES);
|
|
3003
|
+
validate(result);
|
|
3004
|
+
return result;
|
|
3005
|
+
} catch (cause) {
|
|
3006
|
+
throw failed(mutation, "reply body is malformed", cause);
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
matching(condition, mutation, detail) {
|
|
3010
|
+
if (!condition) throw failed(mutation, detail);
|
|
3011
|
+
}
|
|
3012
|
+
async view(verb, target, request, mutation, options) {
|
|
3013
|
+
const result = await this.control(verb, request, validateVolumeView, mutation, options);
|
|
3014
|
+
this.matching(
|
|
3015
|
+
sameValue(result.target, target),
|
|
3016
|
+
mutation,
|
|
3017
|
+
"reply names another volume incarnation"
|
|
3018
|
+
);
|
|
3019
|
+
return result;
|
|
3020
|
+
}
|
|
3021
|
+
info(options = {}) {
|
|
3022
|
+
return this.control("info", {}, validateInfo, false, options);
|
|
3023
|
+
}
|
|
3024
|
+
async createVolume(request, options = {}) {
|
|
3025
|
+
const expected = structuredClone(request);
|
|
3026
|
+
const result = await this.control(
|
|
3027
|
+
"volume.create",
|
|
3028
|
+
expected,
|
|
3029
|
+
validateVolumeView,
|
|
3030
|
+
true,
|
|
3031
|
+
options
|
|
3032
|
+
);
|
|
3033
|
+
this.matching(result.target.volume === expected.volume, true, "reply names another volume");
|
|
3034
|
+
return result;
|
|
3035
|
+
}
|
|
3036
|
+
getVolume(target, options = {}) {
|
|
3037
|
+
const expected = structuredClone(target);
|
|
3038
|
+
return this.view("volume.get", expected, { target: expected }, false, options);
|
|
3039
|
+
}
|
|
3040
|
+
async prepare(request, options = {}) {
|
|
3041
|
+
const expected = structuredClone(request);
|
|
3042
|
+
const result = await this.control(
|
|
3043
|
+
"object.prepare",
|
|
3044
|
+
expected,
|
|
3045
|
+
validatePrepareResult,
|
|
3046
|
+
true,
|
|
3047
|
+
options
|
|
3048
|
+
);
|
|
3049
|
+
this.matching(
|
|
3050
|
+
result.objects.length === expected.objects.length && result.objects.every(
|
|
3051
|
+
(progress, index) => sameValue(progress.object, expected.objects[index].object) && progress.length === expected.objects[index].length
|
|
3052
|
+
),
|
|
3053
|
+
true,
|
|
3054
|
+
"preparation reply names other objects"
|
|
3055
|
+
);
|
|
3056
|
+
return result;
|
|
3057
|
+
}
|
|
3058
|
+
async putPrepared(request, data, options = {}) {
|
|
3059
|
+
const expected = structuredClone(request), bytes = Uint8Array.from(data);
|
|
3060
|
+
validateRequest("object.put", expected);
|
|
3061
|
+
check(bytes.length > 0 && bytes.length <= MAX_NODE_BYTES, "object bytes exceed bound");
|
|
3062
|
+
try {
|
|
3063
|
+
verifyObject(expected.object, bytes);
|
|
3064
|
+
} catch (cause) {
|
|
3065
|
+
throw invalid("object bytes differ from canonical identity", cause);
|
|
3066
|
+
}
|
|
3067
|
+
const reply = await this.exchange(
|
|
3068
|
+
"object.put",
|
|
3069
|
+
bytes,
|
|
3070
|
+
true,
|
|
3071
|
+
false,
|
|
3072
|
+
options,
|
|
3073
|
+
goJSON(expected)
|
|
3074
|
+
);
|
|
3075
|
+
try {
|
|
3076
|
+
validateEmpty(parseStrictJSON(reply.body, MAX_REPLY_BYTES));
|
|
3077
|
+
} catch (cause) {
|
|
3078
|
+
throw failed(true, "put reply body is malformed", cause);
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
async verify(request, options = {}) {
|
|
3082
|
+
const expected = structuredClone(request);
|
|
3083
|
+
const result = await this.control(
|
|
3084
|
+
"object.verify",
|
|
3085
|
+
expected,
|
|
3086
|
+
validateVerifyResult,
|
|
3087
|
+
true,
|
|
3088
|
+
options
|
|
3089
|
+
);
|
|
3090
|
+
this.matching(
|
|
3091
|
+
result.epoch === expected.epoch && result.objects.length === expected.objects.length && result.objects.every(
|
|
3092
|
+
(progress, index) => sameValue(progress.object, expected.objects[index])
|
|
3093
|
+
),
|
|
3094
|
+
true,
|
|
3095
|
+
"verification reply names other objects or epoch"
|
|
3096
|
+
);
|
|
3097
|
+
return result;
|
|
3098
|
+
}
|
|
3099
|
+
async publish(request, options = {}) {
|
|
3100
|
+
const expected = structuredClone(request);
|
|
3101
|
+
const result = await this.control(
|
|
3102
|
+
"checkpoint.publish",
|
|
3103
|
+
expected,
|
|
3104
|
+
validateReceipt,
|
|
3105
|
+
true,
|
|
3106
|
+
options
|
|
3107
|
+
);
|
|
3108
|
+
const normalize = (command) => ({
|
|
3109
|
+
...command,
|
|
3110
|
+
trace: Object.fromEntries(Object.entries(command.trace).filter(([, value]) => value !== ""))
|
|
3111
|
+
});
|
|
3112
|
+
this.matching(
|
|
3113
|
+
result.volume === expected.target.volume && sameValue(normalize(result.command), normalize(expected.command)),
|
|
3114
|
+
true,
|
|
3115
|
+
"receipt names another command"
|
|
3116
|
+
);
|
|
3117
|
+
return result;
|
|
3118
|
+
}
|
|
3119
|
+
/** Unknown reference verbs on older services return invalid. Other failures propagate. */
|
|
3120
|
+
async referenceInfo(options = {}) {
|
|
3121
|
+
try {
|
|
3122
|
+
return await this.control(
|
|
3123
|
+
"reference.info",
|
|
3124
|
+
{},
|
|
3125
|
+
validateReferenceCapabilities,
|
|
3126
|
+
false,
|
|
3127
|
+
options
|
|
3128
|
+
);
|
|
3129
|
+
} catch (error) {
|
|
3130
|
+
if (error instanceof NativeScratchPadError && error.code === "invalid" && !error.local && !error.unknown)
|
|
3131
|
+
return void 0;
|
|
3132
|
+
throw error;
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
async referencePublication(verb, request, options) {
|
|
3136
|
+
const expected = structuredClone(request), mutation = verb === "reference.publish";
|
|
3137
|
+
const result = await this.control(
|
|
3138
|
+
verb,
|
|
3139
|
+
expected,
|
|
3140
|
+
validateReferencePublishResult,
|
|
3141
|
+
mutation,
|
|
3142
|
+
options
|
|
3143
|
+
);
|
|
3144
|
+
this.matching(
|
|
3145
|
+
result.receipt.volume === expected.target.volume && sameValue(
|
|
3146
|
+
{
|
|
3147
|
+
...result.receipt.command,
|
|
3148
|
+
trace: Object.fromEntries(
|
|
3149
|
+
Object.entries(result.receipt.command.trace).filter(([, value]) => value !== "")
|
|
3150
|
+
)
|
|
3151
|
+
},
|
|
3152
|
+
{
|
|
3153
|
+
...expected.command,
|
|
3154
|
+
trace: Object.fromEntries(
|
|
3155
|
+
Object.entries(expected.command.trace).filter(([, value]) => value !== "")
|
|
3156
|
+
)
|
|
3157
|
+
}
|
|
3158
|
+
) && result.references.length === expected.reference.paths.length && result.references.every(
|
|
3159
|
+
(pair, index) => pair.expanded.creator === expected.reference.creator && pair.expanded.path === expected.reference.paths[index]
|
|
3160
|
+
),
|
|
3161
|
+
mutation,
|
|
3162
|
+
"referenced receipt names another intent"
|
|
3163
|
+
);
|
|
3164
|
+
return result;
|
|
3165
|
+
}
|
|
3166
|
+
referencePublish(request, options = {}) {
|
|
3167
|
+
return this.referencePublication("reference.publish", request, options);
|
|
3168
|
+
}
|
|
3169
|
+
referenceReceipt(request, options = {}) {
|
|
3170
|
+
return this.referencePublication("reference.receipt", request, options);
|
|
3171
|
+
}
|
|
3172
|
+
async referenceResolve(request, options = {}) {
|
|
3173
|
+
const expected = structuredClone(request);
|
|
3174
|
+
const result = await this.control(
|
|
3175
|
+
"reference.resolve",
|
|
3176
|
+
expected,
|
|
3177
|
+
validateResolvedReference,
|
|
3178
|
+
false,
|
|
3179
|
+
options
|
|
3180
|
+
);
|
|
3181
|
+
this.matching(result.short === expected.reference, false, "resolution names another reference");
|
|
3182
|
+
return result;
|
|
3183
|
+
}
|
|
3184
|
+
async referenceStat(request, options = {}) {
|
|
3185
|
+
const expected = structuredClone(request);
|
|
3186
|
+
const result = await this.control(
|
|
3187
|
+
"reference.stat",
|
|
3188
|
+
expected,
|
|
3189
|
+
validateReferenceStatResult,
|
|
3190
|
+
false,
|
|
3191
|
+
options
|
|
3192
|
+
);
|
|
3193
|
+
this.matching(
|
|
3194
|
+
result.reference.short === expected.reference,
|
|
3195
|
+
false,
|
|
3196
|
+
"stat names another reference"
|
|
3197
|
+
);
|
|
3198
|
+
return result;
|
|
3199
|
+
}
|
|
3200
|
+
async referenceRetain(request, options = {}) {
|
|
3201
|
+
const expected = structuredClone(request), retain = expected.retain;
|
|
3202
|
+
const result = await this.control(
|
|
3203
|
+
"reference.retain",
|
|
3204
|
+
expected,
|
|
3205
|
+
validateReferenceRetainResult,
|
|
3206
|
+
true,
|
|
3207
|
+
options
|
|
3208
|
+
);
|
|
3209
|
+
this.matching(
|
|
3210
|
+
sameValue(result.volume.target, retain.target) && result.references.length === expected.reference.paths.length && result.references.every(
|
|
3211
|
+
(pair, index) => pair.expanded.creator === expected.reference.creator && pair.expanded.path === expected.reference.paths[index] && pair.expanded.root === retain.root && pair.expanded.expires_at === retain.until && pair.expanded.retention.slot === retain.slot && pair.expanded.retention.generation === retain.generation + 1
|
|
3212
|
+
),
|
|
3213
|
+
true,
|
|
3214
|
+
"retained references name another intent"
|
|
3215
|
+
);
|
|
3216
|
+
return result;
|
|
3217
|
+
}
|
|
3218
|
+
async resetWriter(target, writer, options = {}) {
|
|
3219
|
+
const expected = structuredClone({ target, writer });
|
|
3220
|
+
const result = await this.control("writer.reset", expected, validateWriter, true, options);
|
|
3221
|
+
this.matching(
|
|
3222
|
+
result.slot === expected.writer.slot && result.generation > expected.writer.generation,
|
|
3223
|
+
true,
|
|
3224
|
+
"reset reply names another writer generation"
|
|
3225
|
+
);
|
|
3226
|
+
return result;
|
|
3227
|
+
}
|
|
3228
|
+
retain(request, options = {}) {
|
|
3229
|
+
const expected = structuredClone(request);
|
|
3230
|
+
return this.view("root.retain", expected.target, expected, true, options);
|
|
3231
|
+
}
|
|
3232
|
+
release(request, options = {}) {
|
|
3233
|
+
const expected = structuredClone(request);
|
|
3234
|
+
return this.view("root.release", expected.target, expected, true, options);
|
|
3235
|
+
}
|
|
3236
|
+
async issueGrant(request, options = {}) {
|
|
3237
|
+
const expected = structuredClone(request);
|
|
3238
|
+
const result = await this.control(
|
|
3239
|
+
"grant.issue",
|
|
3240
|
+
expected,
|
|
3241
|
+
validateGrantHandle,
|
|
3242
|
+
true,
|
|
3243
|
+
options
|
|
3244
|
+
);
|
|
3245
|
+
this.matching(
|
|
3246
|
+
result.slot === expected.slot && result.generation > expected.generation,
|
|
3247
|
+
true,
|
|
3248
|
+
"grant reply names another slot generation"
|
|
3249
|
+
);
|
|
3250
|
+
return result;
|
|
3251
|
+
}
|
|
3252
|
+
revokeGrant(request, options = {}) {
|
|
3253
|
+
const expected = structuredClone(request);
|
|
3254
|
+
return this.view("grant.revoke", expected.target, expected, true, options);
|
|
3255
|
+
}
|
|
3256
|
+
stat(request, options = {}) {
|
|
3257
|
+
return this.control("content.stat", structuredClone(request), validateStat, false, options);
|
|
3258
|
+
}
|
|
3259
|
+
async list(request, options = {}) {
|
|
3260
|
+
const expected = structuredClone(request);
|
|
3261
|
+
const result = await this.control(
|
|
3262
|
+
"content.list",
|
|
3263
|
+
expected,
|
|
3264
|
+
validateListResult,
|
|
3265
|
+
false,
|
|
3266
|
+
options
|
|
3267
|
+
);
|
|
3268
|
+
this.matching(
|
|
3269
|
+
result.entries.length <= expected.limit && result.next === expected.offset + result.entries.length,
|
|
3270
|
+
false,
|
|
3271
|
+
"list result differs from requested window"
|
|
3272
|
+
);
|
|
3273
|
+
return result;
|
|
3274
|
+
}
|
|
3275
|
+
async read(request, options = {}) {
|
|
3276
|
+
const expected = structuredClone(request);
|
|
3277
|
+
validateRequest("content.read", expected);
|
|
3278
|
+
const data = Buffer.from(goJSON(expected));
|
|
3279
|
+
check(data.length <= MAX_CONTROL_BYTES, "read request exceeds bound");
|
|
3280
|
+
const reply = await this.exchange("content.read", data, false, true, options), range = reply.metadata.range;
|
|
3281
|
+
this.matching(
|
|
3282
|
+
range.offset === expected.offset && range.length === Math.min(expected.length, range.stat.size - range.offset) && reply.body.length === range.length,
|
|
3283
|
+
false,
|
|
3284
|
+
"range result differs from request"
|
|
3285
|
+
);
|
|
3286
|
+
return { stat: range.stat, offset: range.offset, data: reply.body };
|
|
3287
|
+
}
|
|
3288
|
+
closeVolume(request, options = {}) {
|
|
3289
|
+
const expected = structuredClone(request);
|
|
3290
|
+
return this.view("maintenance.close", expected.target, expected, true, options);
|
|
3291
|
+
}
|
|
3292
|
+
planCollection(request, options = {}) {
|
|
3293
|
+
return this.control(
|
|
3294
|
+
"maintenance.plan",
|
|
3295
|
+
structuredClone(request),
|
|
3296
|
+
validateCollectionReport,
|
|
3297
|
+
false,
|
|
3298
|
+
options
|
|
3299
|
+
);
|
|
3300
|
+
}
|
|
3301
|
+
async applyCollection(request, options = {}) {
|
|
3302
|
+
const expected = structuredClone(request);
|
|
3303
|
+
const result = await this.control(
|
|
3304
|
+
"maintenance.apply",
|
|
3305
|
+
expected,
|
|
3306
|
+
validateCollectionResult,
|
|
3307
|
+
true,
|
|
3308
|
+
options
|
|
3309
|
+
);
|
|
3310
|
+
this.matching(
|
|
3311
|
+
sameValue(result.volume.target, expected.target),
|
|
3312
|
+
true,
|
|
3313
|
+
"collection names another volume incarnation"
|
|
3314
|
+
);
|
|
3315
|
+
return result;
|
|
3316
|
+
}
|
|
3317
|
+
};
|
|
3318
|
+
|
|
3319
|
+
// src/scratchpad-native/reader.ts
|
|
3320
|
+
var DEFAULT_READ_LIMIT_BYTES = 16 * 1024 * 1024;
|
|
3321
|
+
function wireTrace(trace, toolCallId) {
|
|
3322
|
+
const result = trace ? { thread_id: trace.threadId, root_id: trace.rootId } : {};
|
|
3323
|
+
if (toolCallId) result.tool_call_id = toolCallId;
|
|
3324
|
+
validateTrace(result);
|
|
3325
|
+
return Object.fromEntries(Object.entries(result).filter(([, value]) => value !== ""));
|
|
3326
|
+
}
|
|
3327
|
+
var ScratchPadReader = class {
|
|
3328
|
+
constructor(transport, principal) {
|
|
3329
|
+
this.transport = transport;
|
|
3330
|
+
this.principal = principal;
|
|
3331
|
+
}
|
|
3332
|
+
transport;
|
|
3333
|
+
principal;
|
|
3334
|
+
expansions = /* @__PURE__ */ new Map();
|
|
3335
|
+
reference(token) {
|
|
3336
|
+
return this.checkedReference(decodeReference(token));
|
|
3337
|
+
}
|
|
3338
|
+
checkedReference(reference) {
|
|
3339
|
+
if (reference.account !== this.principal.account)
|
|
3340
|
+
throw new NativeScratchPadError("denied", "reference belongs to another account", {
|
|
3341
|
+
local: true
|
|
3342
|
+
});
|
|
3343
|
+
this.checkDeadline(reference.expires_at);
|
|
3344
|
+
return reference;
|
|
3345
|
+
}
|
|
3346
|
+
checkDeadline(expiresAt) {
|
|
3347
|
+
if (Date.now() >= expiresAt * 1e3)
|
|
3348
|
+
throw new NativeScratchPadError("expired", "reference has expired", { local: true });
|
|
3349
|
+
}
|
|
3350
|
+
cacheReference(pair) {
|
|
3351
|
+
this.expansions.delete(pair.short);
|
|
3352
|
+
this.expansions.set(pair.short, structuredClone(pair.expanded));
|
|
3353
|
+
if (this.expansions.size > 64) this.expansions.delete(this.expansions.keys().next().value);
|
|
3354
|
+
}
|
|
3355
|
+
/** Expand either form for programmatic use. Metadata supplies no read authority. */
|
|
3356
|
+
async resolveReference(token, trace) {
|
|
3357
|
+
if (!token.startsWith(SHORT_REFERENCE_PREFIX2)) return this.reference(token);
|
|
3358
|
+
decodeShortReference(token);
|
|
3359
|
+
const cached = this.expansions.get(token);
|
|
3360
|
+
if (cached) return this.checkedReference(structuredClone(cached));
|
|
3361
|
+
const pair = await this.transport.referenceResolve({
|
|
3362
|
+
reference: token,
|
|
3363
|
+
trace: wireTrace(trace)
|
|
3364
|
+
});
|
|
3365
|
+
this.checkedReference(pair.expanded);
|
|
3366
|
+
this.cacheReference(pair);
|
|
3367
|
+
return structuredClone(pair.expanded);
|
|
3368
|
+
}
|
|
3369
|
+
async openFile(token, options) {
|
|
3370
|
+
const limit = options.limitBytes ?? DEFAULT_READ_LIMIT_BYTES;
|
|
3371
|
+
uint(limit, MAX_VOLUME_BYTES);
|
|
3372
|
+
const resolved = token.startsWith(SHORT_REFERENCE_PREFIX2) ? await this.transport.referenceStat({ reference: token, trace: wireTrace(options.trace) }) : void 0;
|
|
3373
|
+
const reference = resolved ? this.checkedReference(resolved.reference.expanded) : this.reference(token);
|
|
3374
|
+
if (resolved) this.cacheReference(resolved.reference);
|
|
3375
|
+
const request = {
|
|
3376
|
+
target: reference.target,
|
|
3377
|
+
root: reference.root,
|
|
3378
|
+
path: reference.path,
|
|
3379
|
+
trace: wireTrace(options.trace)
|
|
3380
|
+
};
|
|
3381
|
+
const stat = resolved?.stat ?? await this.transport.stat(request);
|
|
3382
|
+
this.checkDeadline(reference.expires_at);
|
|
3383
|
+
check(stat.type === "file", "reference does not name a file");
|
|
3384
|
+
if (stat.size > limit)
|
|
3385
|
+
throw new NativeScratchPadError(
|
|
3386
|
+
"capacity",
|
|
3387
|
+
`file has ${stat.size} bytes; read limit is ${limit}`,
|
|
3388
|
+
{ local: true }
|
|
3389
|
+
);
|
|
3390
|
+
return { request, stat, expiresAt: reference.expires_at };
|
|
3391
|
+
}
|
|
3392
|
+
async *chunks(request, stat, expiresAt) {
|
|
3393
|
+
this.checkDeadline(expiresAt);
|
|
3394
|
+
const chunks = [], digest = node_crypto.createHash("sha256");
|
|
3395
|
+
for (let offset = 0; offset < stat.size; offset += CHUNK_SIZE) {
|
|
3396
|
+
const length = Math.min(CHUNK_SIZE, stat.size - offset);
|
|
3397
|
+
this.checkDeadline(expiresAt);
|
|
3398
|
+
const result = await this.transport.read({ ...request, offset, length });
|
|
3399
|
+
this.checkDeadline(expiresAt);
|
|
3400
|
+
if (!sameValue(stat, result.stat) || result.offset !== offset || result.data.length !== length) {
|
|
3401
|
+
throw new NativeScratchPadError("corrupt", "read range differs from the immutable file");
|
|
3402
|
+
}
|
|
3403
|
+
digest.update(result.data);
|
|
3404
|
+
chunks.push({ id: chunkID(result.data), length });
|
|
3405
|
+
yield result.data;
|
|
3406
|
+
this.checkDeadline(expiresAt);
|
|
3407
|
+
}
|
|
3408
|
+
const sha256 = digest.digest("hex");
|
|
3409
|
+
const object = fileObjectID(encodeFileObject({ size: stat.size, digest: sha256, chunks }));
|
|
3410
|
+
this.checkDeadline(expiresAt);
|
|
3411
|
+
if (sha256 !== stat.sha256 || object !== stat.object)
|
|
3412
|
+
throw new NativeScratchPadError("corrupt", "file bytes differ from their digest or identity");
|
|
3413
|
+
}
|
|
3414
|
+
/** Returns bytes only after size, logical digest and canonical file identity agree. */
|
|
3415
|
+
async readBytes(token, options = {}) {
|
|
3416
|
+
const { request, stat, expiresAt } = await this.openFile(token, options);
|
|
3417
|
+
const output = new Uint8Array(stat.size);
|
|
3418
|
+
let offset = 0;
|
|
3419
|
+
for await (const bytes of this.chunks(request, stat, expiresAt)) {
|
|
3420
|
+
output.set(bytes, offset);
|
|
3421
|
+
offset += bytes.length;
|
|
3422
|
+
}
|
|
3423
|
+
this.checkDeadline(expiresAt);
|
|
3424
|
+
return output;
|
|
3425
|
+
}
|
|
3426
|
+
/** Bytes are provisional until iteration completes without error. Consume to EOF
|
|
3427
|
+
* and publish a downloaded file only after success; early cancellation is unverified.
|
|
3428
|
+
* The caller must set an explicit whole-file size allowance.
|
|
3429
|
+
*/
|
|
3430
|
+
async *readStream(token, options) {
|
|
3431
|
+
uint(options?.limitBytes, MAX_VOLUME_BYTES);
|
|
3432
|
+
const { request, stat, expiresAt } = await this.openFile(token, options);
|
|
3433
|
+
yield* this.chunks(request, stat, expiresAt);
|
|
3434
|
+
this.checkDeadline(expiresAt);
|
|
3435
|
+
}
|
|
3436
|
+
async read(reference, trace) {
|
|
3437
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(
|
|
3438
|
+
await this.readBytes(reference, trace ? { trace } : {})
|
|
3439
|
+
);
|
|
3440
|
+
}
|
|
3441
|
+
};
|
|
3442
|
+
async function scratchPadReader(options) {
|
|
3443
|
+
const transport = new ScratchPadTransport(options.nc, options);
|
|
3444
|
+
const info = await transport.info();
|
|
3445
|
+
return new ScratchPadReader(transport, info.principal);
|
|
3446
|
+
}
|
|
3447
|
+
var STATE_LIMIT = 128 * 1024;
|
|
3448
|
+
var OWNERS = /* @__PURE__ */ Symbol.for("synadia.fabric.scratchpad.workspace-owners.v1");
|
|
3449
|
+
var globals = globalThis;
|
|
3450
|
+
var owners = globals[OWNERS] ??= /* @__PURE__ */ new Set();
|
|
3451
|
+
var WorkspaceStore = class _WorkspaceStore {
|
|
3452
|
+
constructor(db, directory) {
|
|
3453
|
+
this.db = db;
|
|
3454
|
+
this.directory = directory;
|
|
3455
|
+
}
|
|
3456
|
+
db;
|
|
3457
|
+
directory;
|
|
3458
|
+
poisoned = false;
|
|
3459
|
+
closed = false;
|
|
3460
|
+
static async open(directory) {
|
|
3461
|
+
check(
|
|
3462
|
+
typeof directory === "string" && directory.length > 0,
|
|
3463
|
+
"a persistent workspace directory is required"
|
|
3464
|
+
);
|
|
3465
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
3466
|
+
check(
|
|
3467
|
+
major > 22 || major === 22 && minor >= 13,
|
|
3468
|
+
"native ScratchPad requires Node 22.13 or later"
|
|
3469
|
+
);
|
|
3470
|
+
const { DatabaseSync } = await import('node:sqlite');
|
|
3471
|
+
const requested = node_path.resolve(directory);
|
|
3472
|
+
node_fs.mkdirSync(requested, { recursive: true, mode: 448 });
|
|
3473
|
+
const directoryStat = node_fs.lstatSync(requested);
|
|
3474
|
+
check(
|
|
3475
|
+
directoryStat.isDirectory() && !directoryStat.isSymbolicLink(),
|
|
3476
|
+
"workspace must be a real directory"
|
|
3477
|
+
);
|
|
3478
|
+
check(
|
|
3479
|
+
process.platform === "win32" || (directoryStat.mode & 63) === 0,
|
|
3480
|
+
"workspace directory must have mode 0700"
|
|
3481
|
+
);
|
|
3482
|
+
const canonical = node_fs.realpathSync(requested);
|
|
3483
|
+
if (owners.has(canonical))
|
|
3484
|
+
throw new NativeScratchPadError("busy", "workspace is already open in this process", {
|
|
3485
|
+
local: true
|
|
3486
|
+
});
|
|
3487
|
+
owners.add(canonical);
|
|
3488
|
+
let db;
|
|
3489
|
+
try {
|
|
3490
|
+
const file = node_path.join(canonical, "scratchpad.sqlite");
|
|
3491
|
+
try {
|
|
3492
|
+
const fd = node_fs.openSync(
|
|
3493
|
+
file,
|
|
3494
|
+
node_fs.constants.O_CREAT | node_fs.constants.O_EXCL | node_fs.constants.O_WRONLY | (node_fs.constants.O_NOFOLLOW ?? 0),
|
|
3495
|
+
384
|
|
3496
|
+
);
|
|
3497
|
+
try {
|
|
3498
|
+
node_fs.fsyncSync(fd);
|
|
3499
|
+
} finally {
|
|
3500
|
+
node_fs.closeSync(fd);
|
|
3501
|
+
}
|
|
3502
|
+
if (process.platform !== "win32") {
|
|
3503
|
+
const parent = node_fs.openSync(canonical, node_fs.constants.O_RDONLY);
|
|
3504
|
+
try {
|
|
3505
|
+
node_fs.fsyncSync(parent);
|
|
3506
|
+
} finally {
|
|
3507
|
+
node_fs.closeSync(parent);
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
} catch (error) {
|
|
3511
|
+
if (error.code !== "EEXIST") throw error;
|
|
3512
|
+
}
|
|
3513
|
+
const stat = node_fs.lstatSync(file);
|
|
3514
|
+
check(
|
|
3515
|
+
stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1,
|
|
3516
|
+
"workspace database must be a private regular file"
|
|
3517
|
+
);
|
|
3518
|
+
check(
|
|
3519
|
+
process.platform === "win32" || (stat.mode & 63) === 0,
|
|
3520
|
+
"workspace database must have mode 0600"
|
|
3521
|
+
);
|
|
3522
|
+
db = new DatabaseSync(file);
|
|
3523
|
+
db.exec(
|
|
3524
|
+
"PRAGMA busy_timeout=0; PRAGMA locking_mode=EXCLUSIVE; PRAGMA journal_mode=DELETE; PRAGMA synchronous=EXTRA; PRAGMA fullfsync=ON; BEGIN EXCLUSIVE; COMMIT;"
|
|
3525
|
+
);
|
|
3526
|
+
db.exec(
|
|
3527
|
+
"CREATE TABLE IF NOT EXISTS state (id INTEGER PRIMARY KEY CHECK(id=1), value TEXT NOT NULL); CREATE TABLE IF NOT EXISTS objects (kind TEXT NOT NULL, id TEXT NOT NULL, ordinal INTEGER NOT NULL, body BLOB NOT NULL, PRIMARY KEY(kind,id)); CREATE INDEX IF NOT EXISTS object_order ON objects(ordinal);"
|
|
3528
|
+
);
|
|
3529
|
+
return new _WorkspaceStore(db, canonical);
|
|
3530
|
+
} catch (cause) {
|
|
3531
|
+
db?.close();
|
|
3532
|
+
owners.delete(canonical);
|
|
3533
|
+
if (cause instanceof NativeScratchPadError) throw cause;
|
|
3534
|
+
throw new NativeScratchPadError("busy", "cannot exclusively open the workspace database", {
|
|
3535
|
+
local: true,
|
|
3536
|
+
cause
|
|
3537
|
+
});
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
usable() {
|
|
3541
|
+
check(
|
|
3542
|
+
!this.closed && !this.poisoned,
|
|
3543
|
+
"workspace is closed or had a write failure; close and reopen it"
|
|
3544
|
+
);
|
|
3545
|
+
}
|
|
3546
|
+
load() {
|
|
3547
|
+
this.usable();
|
|
3548
|
+
const row = this.db.prepare("SELECT value FROM state WHERE id=1").get();
|
|
3549
|
+
return row ? parseStrictJSON(String(row.value), STATE_LIMIT) : void 0;
|
|
3550
|
+
}
|
|
3551
|
+
save(state, afterRemote = false, clearObjects = false) {
|
|
3552
|
+
this.usable();
|
|
3553
|
+
try {
|
|
3554
|
+
const json = JSON.stringify(state);
|
|
3555
|
+
check(Buffer.byteLength(json) <= STATE_LIMIT, "workspace metadata exceeds its byte bound");
|
|
3556
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
3557
|
+
this.db.prepare(
|
|
3558
|
+
"INSERT INTO state(id,value) VALUES(1,?) ON CONFLICT(id) DO UPDATE SET value=excluded.value"
|
|
3559
|
+
).run(json);
|
|
3560
|
+
if (clearObjects) this.db.exec("DELETE FROM objects");
|
|
3561
|
+
this.db.exec("COMMIT");
|
|
3562
|
+
} catch (cause) {
|
|
3563
|
+
this.poisoned = true;
|
|
3564
|
+
if (this.db.isTransaction) this.db.exec("ROLLBACK");
|
|
3565
|
+
throw new NativeScratchPadError(
|
|
3566
|
+
"unavailable",
|
|
3567
|
+
"workspace write failed; close and reopen before recovery",
|
|
3568
|
+
{ local: true, unknown: afterRemote, cause }
|
|
3569
|
+
);
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
clearObjects() {
|
|
3573
|
+
this.usable();
|
|
3574
|
+
this.db.exec("DELETE FROM objects");
|
|
3575
|
+
}
|
|
3576
|
+
putObject(ordinal, object, data) {
|
|
3577
|
+
this.usable();
|
|
3578
|
+
uint(ordinal);
|
|
3579
|
+
check(data.length > 0 && data.length <= MAX_NODE_BYTES, "spool object exceeds bound");
|
|
3580
|
+
verifyObject(object, data);
|
|
3581
|
+
this.db.prepare(
|
|
3582
|
+
"INSERT INTO objects(kind,id,ordinal,body) VALUES(?,?,?,?) ON CONFLICT(kind,id) DO NOTHING"
|
|
3583
|
+
).run(object.kind, object.id, ordinal, data);
|
|
3584
|
+
}
|
|
3585
|
+
objects() {
|
|
3586
|
+
this.usable();
|
|
3587
|
+
return this.db.prepare("SELECT kind,id,length(body) AS length FROM objects ORDER BY ordinal").all().map((row) => ({
|
|
3588
|
+
object: { kind: row.kind, id: String(row.id) },
|
|
3589
|
+
length: Number(row.length)
|
|
3590
|
+
}));
|
|
3591
|
+
}
|
|
3592
|
+
bytes(object) {
|
|
3593
|
+
this.usable();
|
|
3594
|
+
const row = this.db.prepare("SELECT body FROM objects WHERE kind=? AND id=?").get(object.kind, object.id);
|
|
3595
|
+
check(row?.body instanceof Uint8Array, "saved publication payload is missing");
|
|
3596
|
+
verifyObject(object, row.body);
|
|
3597
|
+
return row.body;
|
|
3598
|
+
}
|
|
3599
|
+
close() {
|
|
3600
|
+
if (this.closed) return;
|
|
3601
|
+
this.db.close();
|
|
3602
|
+
this.closed = true;
|
|
3603
|
+
owners.delete(this.directory);
|
|
3604
|
+
}
|
|
3605
|
+
};
|
|
3606
|
+
|
|
3607
|
+
// src/scratchpad-native/client.ts
|
|
3608
|
+
function now2() {
|
|
3609
|
+
return Math.floor(Date.now() / 1e3);
|
|
3610
|
+
}
|
|
3611
|
+
function refuse(code, message) {
|
|
3612
|
+
throw new NativeScratchPadError(code, message, { local: true });
|
|
3613
|
+
}
|
|
3614
|
+
function unknown(error, message) {
|
|
3615
|
+
return new NativeScratchPadError(
|
|
3616
|
+
error instanceof NativeScratchPadError ? error.code : "unknown",
|
|
3617
|
+
message,
|
|
3618
|
+
{ unknown: true, cause: error }
|
|
3619
|
+
);
|
|
3620
|
+
}
|
|
3621
|
+
function validateState(value) {
|
|
3622
|
+
const s = record(
|
|
3623
|
+
value,
|
|
3624
|
+
["version", "principal", "volume", "creator", "creation", "next", "grants"],
|
|
3625
|
+
["target", "writer", "pending", "last"]
|
|
3626
|
+
);
|
|
3627
|
+
check(s.version === 1, "unsupported workspace version");
|
|
3628
|
+
validatePrincipal(s.principal);
|
|
3629
|
+
validateVolume(s.volume);
|
|
3630
|
+
text(s.creator, 512, 1);
|
|
3631
|
+
validateAction(s.creation);
|
|
3632
|
+
uint(s.next, void 0, 1);
|
|
3633
|
+
if (s.target !== void 0) {
|
|
3634
|
+
validateTarget(s.target);
|
|
3635
|
+
check(s.target.volume === s.volume, "workspace target differs from volume");
|
|
3636
|
+
}
|
|
3637
|
+
if (s.writer !== void 0) {
|
|
3638
|
+
validateWriter(s.writer);
|
|
3639
|
+
check(s.writer.slot === 0 && s.target, "workspace writer binding is invalid");
|
|
3640
|
+
}
|
|
3641
|
+
check(Array.isArray(s.grants) && s.grants.length <= 16, "workspace grant ledger exceeds bound");
|
|
3642
|
+
for (const raw of s.grants) {
|
|
3643
|
+
const g = record(raw, ["request", "handle", "kind"]);
|
|
3644
|
+
check(g.kind === "first-hand" || g.kind === "on-request", "invalid grant provenance");
|
|
3645
|
+
validateGrant(g.request);
|
|
3646
|
+
const h = record(g.handle, ["slot", "generation"]);
|
|
3647
|
+
uint(h.slot, 15);
|
|
3648
|
+
uint(h.generation, void 0, 1);
|
|
3649
|
+
}
|
|
3650
|
+
if (s.last !== void 0) {
|
|
3651
|
+
const last = record(s.last, ["reference", "receipt", "path", "ttl"], ["resolved"]);
|
|
3652
|
+
validateReceipt(last.receipt);
|
|
3653
|
+
text(last.reference, 8192, 1);
|
|
3654
|
+
let reference;
|
|
3655
|
+
if (last.reference.startsWith(SHORT_REFERENCE_PREFIX2)) {
|
|
3656
|
+
validateResolvedReference(last.resolved);
|
|
3657
|
+
validateReferencePublishResult({ receipt: last.receipt, references: [last.resolved] });
|
|
3658
|
+
check(last.resolved.short === last.reference, "saved short reference differs");
|
|
3659
|
+
reference = last.resolved.expanded;
|
|
3660
|
+
} else {
|
|
3661
|
+
check(last.resolved === void 0, "full reference has unexpected expansion");
|
|
3662
|
+
reference = decodeReference(last.reference);
|
|
3663
|
+
}
|
|
3664
|
+
validatePath(last.path, 256);
|
|
3665
|
+
uint(last.ttl, MAX_GRANT_SECONDS, 1);
|
|
3666
|
+
check(
|
|
3667
|
+
sameValue(last.receipt.principal, s.principal) && sameValue(reference.target, s.target) && reference.root === last.receipt.head.root && reference.path === last.path && sameValue(reference.retention, last.receipt.retention),
|
|
3668
|
+
"saved receipt differs from workspace"
|
|
3669
|
+
);
|
|
3670
|
+
}
|
|
3671
|
+
if (s.pending !== void 0) {
|
|
3672
|
+
check(s.target, "pending operation has no target");
|
|
3673
|
+
const p = s.pending;
|
|
3674
|
+
switch (p.kind) {
|
|
3675
|
+
case "publish":
|
|
3676
|
+
record(p, ["kind", "command", "path", "ttl"], ["expiry_revision", "reference"]);
|
|
3677
|
+
if (p.reference !== void 0) {
|
|
3678
|
+
validateReferenceSpec(p.reference);
|
|
3679
|
+
check(
|
|
3680
|
+
p.reference.creator === s.creator && sameValue(p.reference.paths, [p.path]),
|
|
3681
|
+
"saved reference intent differs from publication"
|
|
3682
|
+
);
|
|
3683
|
+
}
|
|
3684
|
+
validateCommand(p.command);
|
|
3685
|
+
validatePath(p.path, 256);
|
|
3686
|
+
uint(p.ttl, MAX_GRANT_SECONDS, 1);
|
|
3687
|
+
if (p.expiry_revision !== void 0) uint(p.expiry_revision, void 0, 1);
|
|
3688
|
+
check(
|
|
3689
|
+
p.command.creation_id === s.target.creation_id && sameValue(p.command.writer, s.writer) && p.command.number === s.next,
|
|
3690
|
+
"pending writer position differs"
|
|
3691
|
+
);
|
|
3692
|
+
break;
|
|
3693
|
+
case "reset":
|
|
3694
|
+
record(p, ["kind", "writer"]);
|
|
3695
|
+
validateWriter(p.writer);
|
|
3696
|
+
break;
|
|
3697
|
+
case "grant":
|
|
3698
|
+
record(p, ["kind", "request", "provenance"]);
|
|
3699
|
+
validateGrant(p.request);
|
|
3700
|
+
check(
|
|
3701
|
+
p.provenance === "first-hand" || p.provenance === "on-request",
|
|
3702
|
+
"invalid pending grant provenance"
|
|
3703
|
+
);
|
|
3704
|
+
break;
|
|
3705
|
+
case "release":
|
|
3706
|
+
record(p, ["kind", "request"]);
|
|
3707
|
+
record(p.request, ["target", "handle"]);
|
|
3708
|
+
validateTarget(p.request.target);
|
|
3709
|
+
validateRetentionHandle(p.request.handle);
|
|
3710
|
+
break;
|
|
3711
|
+
case "close":
|
|
3712
|
+
case "apply":
|
|
3713
|
+
record(p, ["kind", "request"]);
|
|
3714
|
+
record(p.request, ["target", "revision"]);
|
|
3715
|
+
validateTarget(p.request.target);
|
|
3716
|
+
uint(p.request.revision, void 0, 1);
|
|
3717
|
+
break;
|
|
3718
|
+
default:
|
|
3719
|
+
check(false, "unknown workspace pending operation");
|
|
3720
|
+
}
|
|
3721
|
+
if ("request" in p)
|
|
3722
|
+
check(sameValue(p.request.target, s.target), "pending operation names another target");
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
function validateGrant(value) {
|
|
3726
|
+
const g = record(value, [
|
|
3727
|
+
"target",
|
|
3728
|
+
"slot",
|
|
3729
|
+
"generation",
|
|
3730
|
+
"principal",
|
|
3731
|
+
"root",
|
|
3732
|
+
"path",
|
|
3733
|
+
"retention",
|
|
3734
|
+
"until",
|
|
3735
|
+
"action"
|
|
3736
|
+
]);
|
|
3737
|
+
validateTarget(g.target);
|
|
3738
|
+
uint(g.slot, 15);
|
|
3739
|
+
uint(g.generation);
|
|
3740
|
+
validatePrincipal(g.principal);
|
|
3741
|
+
validatePath(g.path, 256);
|
|
3742
|
+
validateRetentionHandle(g.retention);
|
|
3743
|
+
uint(g.until, void 0, 1);
|
|
3744
|
+
validateAction(g.action);
|
|
3745
|
+
text(g.root, 43, 43);
|
|
3746
|
+
}
|
|
3747
|
+
var NativeScratchPad = class extends ScratchPadReader {
|
|
3748
|
+
constructor(transport, store, state, scope, ttl) {
|
|
3749
|
+
super(transport, state.principal);
|
|
3750
|
+
this.store = store;
|
|
3751
|
+
this.state = state;
|
|
3752
|
+
this.scope = scope;
|
|
3753
|
+
this.ttl = ttl;
|
|
3754
|
+
}
|
|
3755
|
+
store;
|
|
3756
|
+
state;
|
|
3757
|
+
scope;
|
|
3758
|
+
ttl;
|
|
3759
|
+
tail = Promise.resolve();
|
|
3760
|
+
closing = false;
|
|
3761
|
+
closePromise;
|
|
3762
|
+
run(work) {
|
|
3763
|
+
if (this.closing)
|
|
3764
|
+
return Promise.reject(
|
|
3765
|
+
new NativeScratchPadError("closed", "native client is closed", { local: true })
|
|
3766
|
+
);
|
|
3767
|
+
const result = this.tail.then(work);
|
|
3768
|
+
this.tail = result.then(
|
|
3769
|
+
() => {
|
|
3770
|
+
},
|
|
3771
|
+
() => {
|
|
3772
|
+
}
|
|
3773
|
+
);
|
|
3774
|
+
return result;
|
|
3775
|
+
}
|
|
3776
|
+
save(state, remote = false, clearObjects = false) {
|
|
3777
|
+
try {
|
|
3778
|
+
validateState(state);
|
|
3779
|
+
this.store.save(state, remote, clearObjects);
|
|
3780
|
+
this.state = state;
|
|
3781
|
+
} catch (error) {
|
|
3782
|
+
if (remote)
|
|
3783
|
+
throw unknown(
|
|
3784
|
+
error,
|
|
3785
|
+
"remote effect may be accepted; close and reopen the workspace before recovery"
|
|
3786
|
+
);
|
|
3787
|
+
throw error;
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
change(pending) {
|
|
3791
|
+
this.save({ ...this.state, pending });
|
|
3792
|
+
}
|
|
3793
|
+
target() {
|
|
3794
|
+
check(this.state.target, "workspace has no volume");
|
|
3795
|
+
return this.state.target;
|
|
3796
|
+
}
|
|
3797
|
+
async ensureTarget() {
|
|
3798
|
+
if (!this.state.target) {
|
|
3799
|
+
const view2 = await this.transport.createVolume({
|
|
3800
|
+
volume: this.state.volume,
|
|
3801
|
+
action: this.state.creation
|
|
3802
|
+
});
|
|
3803
|
+
if (!sameValue(view2.owner, this.principal))
|
|
3804
|
+
throw unknown(void 0, "volume creation names another owner");
|
|
3805
|
+
this.save({ ...this.state, target: view2.target }, true);
|
|
3806
|
+
return view2;
|
|
3807
|
+
}
|
|
3808
|
+
const view = await this.transport.getVolume(this.state.target);
|
|
3809
|
+
check(
|
|
3810
|
+
sameValue(view.owner, this.principal),
|
|
3811
|
+
"workspace principal is no longer the volume owner"
|
|
3812
|
+
);
|
|
3813
|
+
return view;
|
|
3814
|
+
}
|
|
3815
|
+
async initialize() {
|
|
3816
|
+
if (this.state.writer) return;
|
|
3817
|
+
const view = await this.ensureTarget();
|
|
3818
|
+
if (this.state.pending)
|
|
3819
|
+
refuse("fenced", "writer reset needs an explicit resetWriter with the observed generation");
|
|
3820
|
+
const slot = view.writers[0];
|
|
3821
|
+
if (slot.number !== 0)
|
|
3822
|
+
refuse(
|
|
3823
|
+
"fenced",
|
|
3824
|
+
"writer slot 0 has accepted commands but this workspace has no journal; inspect describe and reset explicitly"
|
|
3825
|
+
);
|
|
3826
|
+
await this.reset({ slot: 0, generation: slot.generation });
|
|
3827
|
+
}
|
|
3828
|
+
async reset(writer) {
|
|
3829
|
+
const previous = this.state;
|
|
3830
|
+
this.change({ kind: "reset", writer });
|
|
3831
|
+
let result;
|
|
3832
|
+
try {
|
|
3833
|
+
result = await this.transport.resetWriter(this.target(), writer);
|
|
3834
|
+
} catch (error) {
|
|
3835
|
+
if (error instanceof NativeScratchPadError && !error.unknown) this.save(previous, true);
|
|
3836
|
+
throw error;
|
|
3837
|
+
}
|
|
3838
|
+
const state = { ...this.state, writer: result, next: 1 };
|
|
3839
|
+
delete state.pending;
|
|
3840
|
+
delete state.last;
|
|
3841
|
+
this.save(state, true, true);
|
|
3842
|
+
return result;
|
|
3843
|
+
}
|
|
3844
|
+
/** Explicitly abandon writer recovery, fencing the exact generation observed by describe(). */
|
|
3845
|
+
resetWriter(options) {
|
|
3846
|
+
return this.run(async () => {
|
|
3847
|
+
uint(options.expectedGeneration, void 0, 1);
|
|
3848
|
+
if (options.target) {
|
|
3849
|
+
validateTarget(options.target);
|
|
3850
|
+
check(
|
|
3851
|
+
options.target.volume === this.state.volume,
|
|
3852
|
+
"explicit reset target names another volume"
|
|
3853
|
+
);
|
|
3854
|
+
if (this.state.target)
|
|
3855
|
+
check(
|
|
3856
|
+
sameValue(options.target, this.state.target),
|
|
3857
|
+
"explicit reset target differs from workspace"
|
|
3858
|
+
);
|
|
3859
|
+
else {
|
|
3860
|
+
const view = await this.transport.getVolume(options.target);
|
|
3861
|
+
check(sameValue(view.owner, this.principal), "explicit reset target has another owner");
|
|
3862
|
+
this.save({ ...this.state, target: view.target });
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
await this.ensureTarget();
|
|
3866
|
+
if (this.state.pending && !["reset", "publish"].includes(this.state.pending.kind))
|
|
3867
|
+
refuse("busy", "recover the pending control request before resetting the writer");
|
|
3868
|
+
return this.reset({ slot: 0, generation: options.expectedGeneration });
|
|
3869
|
+
});
|
|
3870
|
+
}
|
|
3871
|
+
describe() {
|
|
3872
|
+
return this.run(async () => {
|
|
3873
|
+
const volume = await this.ensureTarget();
|
|
3874
|
+
return this.state.pending ? { volume, pending: this.state.pending.kind } : { volume };
|
|
3875
|
+
});
|
|
3876
|
+
}
|
|
3877
|
+
recover() {
|
|
3878
|
+
return this.run(() => this.recoverPending());
|
|
3879
|
+
}
|
|
3880
|
+
async recoverPending() {
|
|
3881
|
+
if (this.state.pending?.kind === "reset")
|
|
3882
|
+
refuse("fenced", "reset outcome needs an explicit resetWriter with the observed generation");
|
|
3883
|
+
await this.initialize();
|
|
3884
|
+
const pending = this.state.pending;
|
|
3885
|
+
if (pending?.kind === "publish") {
|
|
3886
|
+
try {
|
|
3887
|
+
return { kind: "publish", ...await this.resumePublish(pending) };
|
|
3888
|
+
} catch (error) {
|
|
3889
|
+
if (error instanceof NativeScratchPadError && error.code === "expired" && !error.unknown && !this.state.pending)
|
|
3890
|
+
return { kind: "expired" };
|
|
3891
|
+
throw error;
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
if (pending) {
|
|
3895
|
+
await this.control(pending, false);
|
|
3896
|
+
return { kind: pending.kind };
|
|
3897
|
+
}
|
|
3898
|
+
return this.state.last ? { kind: "ready", reference: this.state.last.reference, receipt: this.state.last.receipt } : { kind: "ready" };
|
|
3899
|
+
}
|
|
3900
|
+
publishBytes(bytes, path, options) {
|
|
3901
|
+
const stable = Uint8Array.from(bytes);
|
|
3902
|
+
return this.publishStream([stable], path, options);
|
|
3903
|
+
}
|
|
3904
|
+
/** A publication replaces the head with one file at path. Older references keep their retained roots. */
|
|
3905
|
+
publishStream(source, path, options) {
|
|
3906
|
+
return this.publish(source, path, options, false);
|
|
3907
|
+
}
|
|
3908
|
+
publish(source, path, options, recover) {
|
|
3909
|
+
options = structuredClone(options);
|
|
3910
|
+
return this.run(async () => {
|
|
3911
|
+
validatePath(path, 256);
|
|
3912
|
+
check(path !== "/", "a file path is required");
|
|
3913
|
+
const trace = wireTrace(options.trace), ttl = options.ttlSeconds ?? this.ttl;
|
|
3914
|
+
uint(ttl, MAX_GRANT_SECONDS, 1);
|
|
3915
|
+
validateAction({ operation_id: options.operationId, trace });
|
|
3916
|
+
if (recover) await this.recoverPending();
|
|
3917
|
+
await this.initialize();
|
|
3918
|
+
const pending = this.state.pending;
|
|
3919
|
+
if (pending) {
|
|
3920
|
+
if (pending.kind !== "publish" || pending.command.operation_id !== options.operationId)
|
|
3921
|
+
refuse("busy", "recover the pending operation before publishing another one");
|
|
3922
|
+
this.samePublication(pending, path, ttl, trace);
|
|
3923
|
+
return this.resumePublish(pending);
|
|
3924
|
+
}
|
|
3925
|
+
if (this.state.last?.receipt.command.operation_id === options.operationId) {
|
|
3926
|
+
const last = this.state.last;
|
|
3927
|
+
this.samePublication(
|
|
3928
|
+
{ kind: "publish", command: last.receipt.command, path: last.path, ttl: last.ttl },
|
|
3929
|
+
path,
|
|
3930
|
+
ttl,
|
|
3931
|
+
trace
|
|
3932
|
+
);
|
|
3933
|
+
return { reference: last.reference, receipt: last.receipt };
|
|
3934
|
+
}
|
|
3935
|
+
const capabilities = await this.transport.referenceInfo();
|
|
3936
|
+
const reference = capabilities?.enabled ? { creator: this.state.creator, paths: [path] } : void 0;
|
|
3937
|
+
if (reference) validateReferenceSpec(reference);
|
|
3938
|
+
this.store.clearObjects();
|
|
3939
|
+
let ordinal = 0, totalBytes = 0;
|
|
3940
|
+
const put = (object, data) => {
|
|
3941
|
+
totalBytes += data.length;
|
|
3942
|
+
if (totalBytes > this.scope.bytes || ordinal + 1 > this.scope.objects)
|
|
3943
|
+
refuse("capacity", "artifact exceeds this volume's configured capacity");
|
|
3944
|
+
this.store.putObject(ordinal++, object, data);
|
|
3945
|
+
};
|
|
3946
|
+
try {
|
|
3947
|
+
const file = await chunkStream(source, (_index, object, data) => put(object, data));
|
|
3948
|
+
const fileBytes = encodeFileObject(file);
|
|
3949
|
+
let root = fileObjectID(fileBytes);
|
|
3950
|
+
put({ kind: "fobj", id: root }, fileBytes);
|
|
3951
|
+
let type = "file";
|
|
3952
|
+
for (const name of path.slice(1).split("/").reverse()) {
|
|
3953
|
+
const data = encodeTreeNode([{ name, type, object: root }]);
|
|
3954
|
+
root = treeNodeID(data);
|
|
3955
|
+
put({ kind: "tree", id: root }, data);
|
|
3956
|
+
type = "directory";
|
|
3957
|
+
}
|
|
3958
|
+
const objects = this.store.objects();
|
|
3959
|
+
let view = await this.transport.getVolume(this.target());
|
|
3960
|
+
const clock = now2();
|
|
3961
|
+
const fullRetention = view.retentions.every((slot2) => slot2.pin);
|
|
3962
|
+
const canRelease = view.retentions.some(
|
|
3963
|
+
(slot2, index) => slot2.pin && slot2.pin.until !== 0 && slot2.pin.until <= clock && !view.grants.some(
|
|
3964
|
+
(grant) => grant.grant && grant.grant.until > clock && sameValue(grant.grant.retention, { slot: index, generation: slot2.generation })
|
|
3965
|
+
)
|
|
3966
|
+
);
|
|
3967
|
+
if (fullRetention && !canRelease)
|
|
3968
|
+
refuse(
|
|
3969
|
+
"capacity",
|
|
3970
|
+
"all retention slots hold live or protected handoffs; cleanup cannot free a slot"
|
|
3971
|
+
);
|
|
3972
|
+
const needsCleanup = fullRetention || view.objects + objects.length > this.scope.objects || view.bytes + objects.reduce((n, object) => n + object.length, 0) > this.scope.bytes;
|
|
3973
|
+
if (needsCleanup) view = await this.maintainInternal();
|
|
3974
|
+
const slot = view.retentions.findIndex((entry) => !entry.pin);
|
|
3975
|
+
if (slot < 0)
|
|
3976
|
+
refuse(
|
|
3977
|
+
"capacity",
|
|
3978
|
+
"all retention slots are occupied; release finished handoffs or wait for their expiry"
|
|
3979
|
+
);
|
|
3980
|
+
if (view.objects + objects.length > this.scope.objects || view.bytes + objects.reduce((n, object) => n + object.length, 0) > this.scope.bytes)
|
|
3981
|
+
refuse("capacity", "live roots and this artifact exceed the volume's capacity");
|
|
3982
|
+
const writer = this.state.writer;
|
|
3983
|
+
const actual = view.writers[writer.slot];
|
|
3984
|
+
if (actual.generation !== writer.generation || actual.number + 1 !== this.state.next)
|
|
3985
|
+
refuse("fenced", "remote writer position differs from the durable workspace");
|
|
3986
|
+
if (view.closed) refuse("closed", "volume is closed by another maintenance interval");
|
|
3987
|
+
const command = {
|
|
3988
|
+
creation_id: this.target().creation_id,
|
|
3989
|
+
writer,
|
|
3990
|
+
number: this.state.next,
|
|
3991
|
+
operation_id: options.operationId,
|
|
3992
|
+
base: view.head,
|
|
3993
|
+
root,
|
|
3994
|
+
trace,
|
|
3995
|
+
retention: {
|
|
3996
|
+
enabled: true,
|
|
3997
|
+
slot,
|
|
3998
|
+
generation: view.retentions[slot].generation,
|
|
3999
|
+
until: now2() + ttl
|
|
4000
|
+
}
|
|
4001
|
+
};
|
|
4002
|
+
const publication = {
|
|
4003
|
+
kind: "publish",
|
|
4004
|
+
command,
|
|
4005
|
+
path,
|
|
4006
|
+
ttl,
|
|
4007
|
+
...reference ? { reference } : {}
|
|
4008
|
+
};
|
|
4009
|
+
this.change(publication);
|
|
4010
|
+
return await this.resumePublish(publication);
|
|
4011
|
+
} catch (error) {
|
|
4012
|
+
if (!this.state.pending) this.store.clearObjects();
|
|
4013
|
+
throw error;
|
|
4014
|
+
}
|
|
4015
|
+
});
|
|
4016
|
+
}
|
|
4017
|
+
samePublication(publication, path, ttl, trace) {
|
|
4018
|
+
if (publication.path !== path || publication.ttl !== ttl || !sameValue(publication.command.trace, trace))
|
|
4019
|
+
refuse(
|
|
4020
|
+
"intent_changed",
|
|
4021
|
+
"operation id is already bound to another publication; recover uses its saved payload"
|
|
4022
|
+
);
|
|
4023
|
+
}
|
|
4024
|
+
async resumePublish(publication) {
|
|
4025
|
+
if (publication.reference) {
|
|
4026
|
+
try {
|
|
4027
|
+
const result = await this.transport.referenceReceipt({
|
|
4028
|
+
target: this.target(),
|
|
4029
|
+
command: publication.command,
|
|
4030
|
+
reference: publication.reference
|
|
4031
|
+
});
|
|
4032
|
+
if (!sameValue(result.receipt.principal, this.principal))
|
|
4033
|
+
throw unknown(void 0, "referenced receipt names another principal");
|
|
4034
|
+
return this.accept(publication, result.receipt, result.references[0]);
|
|
4035
|
+
} catch (error) {
|
|
4036
|
+
if (!(error instanceof NativeScratchPadError && error.code === "not_found" && !error.unknown))
|
|
4037
|
+
throw error;
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
const view = await this.transport.getVolume(this.target());
|
|
4041
|
+
const slot = view.writers[publication.command.writer.slot];
|
|
4042
|
+
if (!publication.reference && slot.receipt && sameValue(slot.receipt.command, publication.command) && sameValue(slot.receipt.principal, this.principal))
|
|
4043
|
+
return this.accept(publication, slot.receipt);
|
|
4044
|
+
if (slot.generation !== publication.command.writer.generation || slot.number >= publication.command.number)
|
|
4045
|
+
refuse("fenced", "saved writer position has been fenced; inspect and reset explicitly");
|
|
4046
|
+
if (publication.expiry_revision !== void 0) {
|
|
4047
|
+
await this.fenceExpired(publication, view);
|
|
4048
|
+
throw new NativeScratchPadError("expired", "saved publication expired without acceptance");
|
|
4049
|
+
}
|
|
4050
|
+
if (publication.command.retention.until > now2()) await this.upload();
|
|
4051
|
+
let receipt, resolved;
|
|
4052
|
+
try {
|
|
4053
|
+
if (publication.reference) {
|
|
4054
|
+
const result = await this.transport.referencePublish({
|
|
4055
|
+
target: this.target(),
|
|
4056
|
+
command: publication.command,
|
|
4057
|
+
reference: publication.reference
|
|
4058
|
+
});
|
|
4059
|
+
receipt = result.receipt;
|
|
4060
|
+
resolved = result.references[0];
|
|
4061
|
+
} else {
|
|
4062
|
+
receipt = await this.transport.publish({
|
|
4063
|
+
target: this.target(),
|
|
4064
|
+
command: publication.command
|
|
4065
|
+
});
|
|
4066
|
+
}
|
|
4067
|
+
} catch (error) {
|
|
4068
|
+
if (error instanceof NativeScratchPadError && error.code === "expired" && !error.unknown) {
|
|
4069
|
+
await this.fenceExpired(publication, view);
|
|
4070
|
+
}
|
|
4071
|
+
throw error;
|
|
4072
|
+
}
|
|
4073
|
+
if (!sameValue(receipt.command, publication.command) || !sameValue(receipt.principal, this.principal))
|
|
4074
|
+
throw unknown(void 0, "publish receipt differs from the saved command");
|
|
4075
|
+
return this.accept(publication, receipt, resolved);
|
|
4076
|
+
}
|
|
4077
|
+
async fenceExpired(publication, view) {
|
|
4078
|
+
let revision = publication.expiry_revision;
|
|
4079
|
+
if (revision === void 0) {
|
|
4080
|
+
if (view.closed) refuse("closed", "another maintenance interval owns this closed volume");
|
|
4081
|
+
revision = view.revision;
|
|
4082
|
+
publication = { ...publication, expiry_revision: revision };
|
|
4083
|
+
this.change(publication);
|
|
4084
|
+
}
|
|
4085
|
+
if (!(view.closed && view.revision === revision + 1)) {
|
|
4086
|
+
if (view.revision !== revision) {
|
|
4087
|
+
const pending = { ...publication };
|
|
4088
|
+
delete pending.expiry_revision;
|
|
4089
|
+
this.change(pending);
|
|
4090
|
+
throw new NativeScratchPadError(
|
|
4091
|
+
"conflict",
|
|
4092
|
+
"expiry fence changed; recover the saved publication again"
|
|
4093
|
+
);
|
|
4094
|
+
}
|
|
4095
|
+
view = await this.transport.closeVolume({ target: this.target(), revision });
|
|
4096
|
+
}
|
|
4097
|
+
const slot = view.writers[publication.command.writer.slot];
|
|
4098
|
+
if (!view.closed || view.revision !== revision + 1 || slot.generation !== publication.command.writer.generation || slot.number !== publication.command.number - 1)
|
|
4099
|
+
throw unknown(void 0, "expiry fence has not established publication absence");
|
|
4100
|
+
const continuation = {
|
|
4101
|
+
kind: "apply",
|
|
4102
|
+
request: { target: this.target(), revision: view.revision }
|
|
4103
|
+
};
|
|
4104
|
+
this.save({ ...this.state, pending: continuation }, true, true);
|
|
4105
|
+
await this.control(continuation, true);
|
|
4106
|
+
}
|
|
4107
|
+
accept(publication, receipt, resolved) {
|
|
4108
|
+
check(
|
|
4109
|
+
Boolean(publication.reference) === Boolean(resolved),
|
|
4110
|
+
"referenced publication lost its descriptor"
|
|
4111
|
+
);
|
|
4112
|
+
const reference = resolved?.short ?? encodeReference({
|
|
4113
|
+
version: 2,
|
|
4114
|
+
creator: this.state.creator,
|
|
4115
|
+
account: this.principal.account,
|
|
4116
|
+
target: this.target(),
|
|
4117
|
+
root: receipt.head.root,
|
|
4118
|
+
generation: receipt.head.generation,
|
|
4119
|
+
path: publication.path,
|
|
4120
|
+
retention: receipt.retention,
|
|
4121
|
+
expires_at: publication.command.retention.until
|
|
4122
|
+
});
|
|
4123
|
+
const state = {
|
|
4124
|
+
...this.state,
|
|
4125
|
+
next: receipt.command.number + 1,
|
|
4126
|
+
last: {
|
|
4127
|
+
reference,
|
|
4128
|
+
receipt,
|
|
4129
|
+
path: publication.path,
|
|
4130
|
+
ttl: publication.ttl,
|
|
4131
|
+
...resolved ? { resolved } : {}
|
|
4132
|
+
}
|
|
4133
|
+
};
|
|
4134
|
+
delete state.pending;
|
|
4135
|
+
this.save(state, true, true);
|
|
4136
|
+
if (resolved) this.cacheReference(resolved);
|
|
4137
|
+
return { reference, receipt };
|
|
4138
|
+
}
|
|
4139
|
+
async upload() {
|
|
4140
|
+
const objects = this.store.objects();
|
|
4141
|
+
check(objects.length > 0, "saved publication payload is missing");
|
|
4142
|
+
const sorted = [...objects].sort(
|
|
4143
|
+
(a, b) => Buffer.compare(
|
|
4144
|
+
Buffer.from(`${a.object.kind}:${a.object.id}`),
|
|
4145
|
+
Buffer.from(`${b.object.kind}:${b.object.id}`)
|
|
4146
|
+
)
|
|
4147
|
+
);
|
|
4148
|
+
const verified = /* @__PURE__ */ new Set();
|
|
4149
|
+
let epoch;
|
|
4150
|
+
const key = (o) => o.kind + ":" + o.id;
|
|
4151
|
+
for (let start = 0; start < sorted.length; start += MAX_PREPARE_OBJECTS) {
|
|
4152
|
+
const result = await this.transport.prepare({
|
|
4153
|
+
target: this.target(),
|
|
4154
|
+
objects: sorted.slice(start, start + MAX_PREPARE_OBJECTS)
|
|
4155
|
+
});
|
|
4156
|
+
if (epoch !== void 0 && epoch !== result.epoch)
|
|
4157
|
+
throw unknown(void 0, "upload preparation changed epoch; recover the saved publication");
|
|
4158
|
+
epoch = result.epoch;
|
|
4159
|
+
for (const object of result.objects) {
|
|
4160
|
+
if (object.state === "refused")
|
|
4161
|
+
refuse(
|
|
4162
|
+
"capacity",
|
|
4163
|
+
"object reservation refused; saved publication stays pending for recovery or explicit reset"
|
|
4164
|
+
);
|
|
4165
|
+
if (object.state === "verified") verified.add(key(object.object));
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
check(epoch !== void 0, "publication contains no objects");
|
|
4169
|
+
for (const spec of objects) {
|
|
4170
|
+
if (verified.has(key(spec.object))) continue;
|
|
4171
|
+
await this.transport.putPrepared(
|
|
4172
|
+
{ target: this.target(), object: spec.object, epoch },
|
|
4173
|
+
this.store.bytes(spec.object)
|
|
4174
|
+
);
|
|
4175
|
+
let next = -1;
|
|
4176
|
+
for (; ; ) {
|
|
4177
|
+
const result = await this.transport.verify({
|
|
4178
|
+
target: this.target(),
|
|
4179
|
+
epoch,
|
|
4180
|
+
objects: [spec.object]
|
|
4181
|
+
});
|
|
4182
|
+
const progress = result.objects[0];
|
|
4183
|
+
if (progress.state === "verified") break;
|
|
4184
|
+
if (progress.state !== "verifying" || progress.next <= next)
|
|
4185
|
+
throw unknown(void 0, "object verification did not advance");
|
|
4186
|
+
next = progress.next;
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
async commit(value, path, trace) {
|
|
4191
|
+
const result = await this.publish(
|
|
4192
|
+
[Buffer.from(value)],
|
|
4193
|
+
path.startsWith("/") ? path : "/" + path,
|
|
4194
|
+
{ operationId: node_crypto.randomUUID(), ...trace ? { trace } : {} },
|
|
4195
|
+
true
|
|
4196
|
+
);
|
|
4197
|
+
return result.reference;
|
|
4198
|
+
}
|
|
4199
|
+
owns(token) {
|
|
4200
|
+
const matches = (ref) => ref.creator === this.state.creator && sameValue(ref.target, this.state.target);
|
|
4201
|
+
if (token.startsWith(SHORT_REFERENCE_PREFIX2))
|
|
4202
|
+
return this.resolveReference(token).then(matches, () => false);
|
|
4203
|
+
try {
|
|
4204
|
+
return matches(this.reference(token));
|
|
4205
|
+
} catch {
|
|
4206
|
+
return false;
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
granted(token, grantee) {
|
|
4210
|
+
const find = (ref) => {
|
|
4211
|
+
if (ref.creator !== this.state.creator || !sameValue(ref.target, this.state.target))
|
|
4212
|
+
return void 0;
|
|
4213
|
+
let kind;
|
|
4214
|
+
for (const grant of this.state.grants) {
|
|
4215
|
+
const q = grant.request;
|
|
4216
|
+
if (q.until > now2() && sameValue(q.principal, grantee) && q.root === ref.root && q.path === ref.path && sameValue(q.retention, ref.retention)) {
|
|
4217
|
+
if (grant.kind === "first-hand") return grant.kind;
|
|
4218
|
+
kind = grant.kind;
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
return kind;
|
|
4222
|
+
};
|
|
4223
|
+
if (token.startsWith(SHORT_REFERENCE_PREFIX2))
|
|
4224
|
+
return this.resolveReference(token).then(find, () => void 0);
|
|
4225
|
+
try {
|
|
4226
|
+
return find(this.reference(token));
|
|
4227
|
+
} catch {
|
|
4228
|
+
return void 0;
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
grantRead(token, grantee, trace, toolCallId, kind = "first-hand") {
|
|
4232
|
+
return this.run(async () => {
|
|
4233
|
+
validatePrincipal(grantee);
|
|
4234
|
+
const labels = wireTrace(trace, toolCallId);
|
|
4235
|
+
if (!await this.owns(token) || grantee.account !== this.principal.account)
|
|
4236
|
+
refuse("denied", "only the owner can grant this same-account reference");
|
|
4237
|
+
const ref = await this.resolveReference(token, trace);
|
|
4238
|
+
if (this.state.pending) await this.recoverPending();
|
|
4239
|
+
const view = await this.transport.getVolume(this.target()), clock = now2();
|
|
4240
|
+
const pin = view.retentions[ref.retention.slot];
|
|
4241
|
+
if (pin.generation !== ref.retention.generation || pin.pin?.root !== ref.root)
|
|
4242
|
+
refuse("expired", "reference retention is no longer held");
|
|
4243
|
+
const until = Math.min(
|
|
4244
|
+
clock + this.ttl,
|
|
4245
|
+
ref.expires_at,
|
|
4246
|
+
pin.pin.until || Number.MAX_SAFE_INTEGER
|
|
4247
|
+
);
|
|
4248
|
+
if (until <= clock) refuse("expired", "reference retention has expired");
|
|
4249
|
+
const old = this.state.grants.find(
|
|
4250
|
+
(g) => g.request.until >= until && sameValue(g.request.principal, grantee) && g.request.root === ref.root && g.request.path === ref.path && sameValue(g.request.retention, ref.retention) && view.grants[g.handle.slot]?.generation === g.handle.generation
|
|
4251
|
+
);
|
|
4252
|
+
if (old) {
|
|
4253
|
+
if (kind === "first-hand" && old.kind !== kind) {
|
|
4254
|
+
const grants = this.state.grants.map((g) => g === old ? { ...g, kind } : g);
|
|
4255
|
+
this.save({ ...this.state, grants });
|
|
4256
|
+
}
|
|
4257
|
+
return;
|
|
4258
|
+
}
|
|
4259
|
+
const slot = view.grants.findIndex((g) => !g.grant || g.grant.until <= clock);
|
|
4260
|
+
if (slot < 0) refuse("capacity", "all read-grant slots hold live grants");
|
|
4261
|
+
const control = {
|
|
4262
|
+
kind: "grant",
|
|
4263
|
+
provenance: kind,
|
|
4264
|
+
request: {
|
|
4265
|
+
target: this.target(),
|
|
4266
|
+
slot,
|
|
4267
|
+
generation: view.grants[slot].generation,
|
|
4268
|
+
principal: { ...grantee },
|
|
4269
|
+
root: ref.root,
|
|
4270
|
+
path: ref.path,
|
|
4271
|
+
retention: ref.retention,
|
|
4272
|
+
until,
|
|
4273
|
+
action: { operation_id: node_crypto.randomUUID(), trace: labels }
|
|
4274
|
+
}
|
|
4275
|
+
};
|
|
4276
|
+
this.change(control);
|
|
4277
|
+
await this.control(control, true);
|
|
4278
|
+
});
|
|
4279
|
+
}
|
|
4280
|
+
/** Release a completed handoff. Refuses while any live grant names its retention. */
|
|
4281
|
+
release(token) {
|
|
4282
|
+
return this.run(async () => {
|
|
4283
|
+
if (this.state.pending) refuse("busy", "recover the pending operation before release");
|
|
4284
|
+
const ref = token.startsWith(SHORT_REFERENCE_PREFIX2) ? await this.resolveReference(token) : decodeReference(token);
|
|
4285
|
+
if (ref.account !== this.principal.account || !sameValue(ref.target, this.state.target))
|
|
4286
|
+
refuse("denied", "reference belongs to another workspace");
|
|
4287
|
+
const view = await this.transport.getVolume(this.target());
|
|
4288
|
+
if (view.grants.some(
|
|
4289
|
+
(g) => g.grant && g.grant.until > now2() && sameValue(g.grant.retention, ref.retention)
|
|
4290
|
+
))
|
|
4291
|
+
refuse("busy", "a live read grant still uses this retention");
|
|
4292
|
+
const control = {
|
|
4293
|
+
kind: "release",
|
|
4294
|
+
request: { target: this.target(), handle: ref.retention }
|
|
4295
|
+
};
|
|
4296
|
+
this.change(control);
|
|
4297
|
+
await this.control(control, true);
|
|
4298
|
+
});
|
|
4299
|
+
}
|
|
4300
|
+
/** Collect unreferenced content in this client's own maintenance interval. */
|
|
4301
|
+
maintain() {
|
|
4302
|
+
return this.run(() => this.maintainInternal());
|
|
4303
|
+
}
|
|
4304
|
+
async maintainInternal() {
|
|
4305
|
+
if (this.state.pending) refuse("busy", "recover the pending operation before collection");
|
|
4306
|
+
let view = await this.ensureTarget();
|
|
4307
|
+
if (view.closed) refuse("closed", "another maintenance interval owns this closed volume");
|
|
4308
|
+
const clock = now2();
|
|
4309
|
+
for (const [slot, held] of view.retentions.entries()) {
|
|
4310
|
+
if (!held.pin || held.pin.until === 0 || held.pin.until > clock) continue;
|
|
4311
|
+
const handle = { slot, generation: held.generation };
|
|
4312
|
+
if (view.grants.some(
|
|
4313
|
+
(g) => g.grant && g.grant.until > clock && sameValue(g.grant.retention, handle)
|
|
4314
|
+
))
|
|
4315
|
+
continue;
|
|
4316
|
+
const control2 = { kind: "release", request: { target: this.target(), handle } };
|
|
4317
|
+
this.change(control2);
|
|
4318
|
+
await this.control(control2, true);
|
|
4319
|
+
}
|
|
4320
|
+
view = await this.transport.getVolume(this.target());
|
|
4321
|
+
const control = {
|
|
4322
|
+
kind: "close",
|
|
4323
|
+
request: { target: this.target(), revision: view.revision }
|
|
4324
|
+
};
|
|
4325
|
+
this.change(control);
|
|
4326
|
+
await this.control(control, true);
|
|
4327
|
+
return this.transport.getVolume(this.target());
|
|
4328
|
+
}
|
|
4329
|
+
async control(control, fresh) {
|
|
4330
|
+
let failure, view, handle;
|
|
4331
|
+
let settled = false;
|
|
4332
|
+
try {
|
|
4333
|
+
switch (control.kind) {
|
|
4334
|
+
case "grant":
|
|
4335
|
+
handle = await this.transport.issueGrant(control.request);
|
|
4336
|
+
break;
|
|
4337
|
+
case "release":
|
|
4338
|
+
view = await this.transport.release(control.request);
|
|
4339
|
+
break;
|
|
4340
|
+
case "close":
|
|
4341
|
+
view = await this.transport.closeVolume(control.request);
|
|
4342
|
+
break;
|
|
4343
|
+
case "apply":
|
|
4344
|
+
view = (await this.transport.applyCollection(control.request)).volume;
|
|
4345
|
+
break;
|
|
4346
|
+
}
|
|
4347
|
+
settled = true;
|
|
4348
|
+
} catch (error) {
|
|
4349
|
+
failure = error;
|
|
4350
|
+
if (control.kind !== "release") {
|
|
4351
|
+
try {
|
|
4352
|
+
view = await this.transport.getVolume(this.target());
|
|
4353
|
+
if (control.kind === "grant") {
|
|
4354
|
+
const q = control.request, observed = view.grants[q.slot], g = observed.grant;
|
|
4355
|
+
if (observed.generation > q.generation && g && sameValue(g.principal, q.principal) && g.root === q.root && g.path === q.path && sameValue(g.retention, q.retention) && g.until === q.until) {
|
|
4356
|
+
handle = { slot: q.slot, generation: observed.generation };
|
|
4357
|
+
failure = void 0;
|
|
4358
|
+
settled = true;
|
|
4359
|
+
}
|
|
4360
|
+
} else if (control.kind === "close") {
|
|
4361
|
+
if (view.closed && view.revision === control.request.revision + 1) {
|
|
4362
|
+
failure = void 0;
|
|
4363
|
+
settled = true;
|
|
4364
|
+
} else if (view.revision !== control.request.revision) {
|
|
4365
|
+
settled = true;
|
|
4366
|
+
failure = new NativeScratchPadError(
|
|
4367
|
+
"conflict",
|
|
4368
|
+
"saved close is fenced by another revision"
|
|
4369
|
+
);
|
|
4370
|
+
}
|
|
4371
|
+
} else if (!view.closed) {
|
|
4372
|
+
failure = void 0;
|
|
4373
|
+
settled = true;
|
|
4374
|
+
}
|
|
4375
|
+
} catch {
|
|
4376
|
+
}
|
|
4377
|
+
}
|
|
4378
|
+
if (error instanceof NativeScratchPadError && !error.unknown && (fresh || control.kind === "release" || control.kind === "grant" && error.code === "expired"))
|
|
4379
|
+
settled = true;
|
|
4380
|
+
}
|
|
4381
|
+
if (!settled)
|
|
4382
|
+
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))
|
|
4384
|
+
throw unknown(void 0, "close returned another maintenance interval");
|
|
4385
|
+
const state = { ...this.state };
|
|
4386
|
+
delete state.pending;
|
|
4387
|
+
if (!failure && control.kind === "grant") {
|
|
4388
|
+
check(handle, "grant acceptance has no handle");
|
|
4389
|
+
state.grants = state.grants.filter(
|
|
4390
|
+
(g) => g.request.until > now2() && g.handle.slot !== handle.slot
|
|
4391
|
+
);
|
|
4392
|
+
state.grants.push({ request: control.request, handle, kind: control.provenance });
|
|
4393
|
+
}
|
|
4394
|
+
if (!failure && control.kind === "close")
|
|
4395
|
+
state.pending = {
|
|
4396
|
+
kind: "apply",
|
|
4397
|
+
request: { target: this.target(), revision: view.revision }
|
|
4398
|
+
};
|
|
4399
|
+
this.save(state, true);
|
|
4400
|
+
if (failure)
|
|
4401
|
+
throw failure instanceof Error ? failure : new Error("control request failed", { cause: failure });
|
|
4402
|
+
if (state.pending?.kind === "apply") await this.control(state.pending, true);
|
|
4403
|
+
}
|
|
4404
|
+
close() {
|
|
4405
|
+
if (this.closePromise) return this.closePromise;
|
|
4406
|
+
this.closing = true;
|
|
4407
|
+
return this.closePromise = this.tail.then(() => this.store.close());
|
|
4408
|
+
}
|
|
4409
|
+
};
|
|
4410
|
+
async function nativeScratchPad(options) {
|
|
4411
|
+
validateVolume(options.volume);
|
|
4412
|
+
text(options.creator, 512, 1);
|
|
4413
|
+
const ttl = options.ttlSeconds ?? 600;
|
|
4414
|
+
uint(ttl, MAX_GRANT_SECONDS, 1);
|
|
4415
|
+
const transport = new ScratchPadTransport(options.nc, options);
|
|
4416
|
+
const info = await transport.info();
|
|
4417
|
+
const scope = info.scopes.find((scope2) => scope2.volume === options.volume);
|
|
4418
|
+
if (!scope) refuse("denied", "authenticated principal owns no provisioned scope for this volume");
|
|
4419
|
+
const store = await WorkspaceStore.open(options.dir);
|
|
4420
|
+
try {
|
|
4421
|
+
let state = store.load();
|
|
4422
|
+
if (state === void 0) {
|
|
4423
|
+
state = {
|
|
4424
|
+
version: 1,
|
|
4425
|
+
principal: info.principal,
|
|
4426
|
+
volume: options.volume,
|
|
4427
|
+
creator: options.creator,
|
|
4428
|
+
creation: { operation_id: node_crypto.randomUUID(), trace: {} },
|
|
4429
|
+
next: 1,
|
|
4430
|
+
grants: []
|
|
4431
|
+
};
|
|
4432
|
+
store.save(state);
|
|
4433
|
+
}
|
|
4434
|
+
validateState(state);
|
|
4435
|
+
check(
|
|
4436
|
+
sameValue(state.principal, info.principal) && state.volume === options.volume && state.creator === options.creator,
|
|
4437
|
+
"workspace is bound to another principal, volume, or creator"
|
|
4438
|
+
);
|
|
4439
|
+
return new NativeScratchPad(transport, store, state, scope, ttl);
|
|
4440
|
+
} catch (error) {
|
|
4441
|
+
store.close();
|
|
4442
|
+
throw error;
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4445
|
+
async function nativeScratchPadFromEnv(env, options) {
|
|
4446
|
+
const volume = env["SCRATCHPAD_VOLUME"];
|
|
4447
|
+
if (!volume) return void 0;
|
|
4448
|
+
const dir = env["SCRATCHPAD_DIR"];
|
|
4449
|
+
check(dir, "SCRATCHPAD_DIR must name a persistent private directory for the native client");
|
|
4450
|
+
const value = env["SCRATCHPAD_GRANT_TTL_S"];
|
|
4451
|
+
check(value === void 0 || /^[0-9]+$/.test(value), "SCRATCHPAD_GRANT_TTL_S must be an integer");
|
|
4452
|
+
return nativeScratchPad({
|
|
4453
|
+
...options,
|
|
4454
|
+
volume,
|
|
4455
|
+
dir,
|
|
4456
|
+
...value === void 0 ? {} : { ttlSeconds: Number(value) }
|
|
4457
|
+
});
|
|
4458
|
+
}
|
|
4459
|
+
|
|
4460
|
+
// src/plugin-settings.ts
|
|
4461
|
+
var EDGE_SUBJECT_OPTION = "edgeSubject";
|
|
4462
|
+
var EDGE_SUBJECT_VAR = "SYNADIA_FABRIC_EDGE_SUBJECT";
|
|
4463
|
+
function resolveEdgeSubject(options, env = process.env) {
|
|
4464
|
+
if (Object.hasOwn(options, EDGE_SUBJECT_OPTION)) {
|
|
4465
|
+
return options[EDGE_SUBJECT_OPTION];
|
|
4466
|
+
}
|
|
4467
|
+
const fromEnv = env[EDGE_SUBJECT_VAR];
|
|
4468
|
+
if (fromEnv !== void 0) return fromEnv;
|
|
4469
|
+
return DEFAULT_EDGE_SUBJECT;
|
|
4470
|
+
}
|
|
4471
|
+
var SCRATCHPAD_OPTION = "scratchpad";
|
|
4472
|
+
var SP_VARS = {
|
|
4473
|
+
url: "SP_NATS_URL",
|
|
4474
|
+
creds: "SP_NATS_CREDS",
|
|
4475
|
+
dir: SCRATCHPAD_DIR_VAR,
|
|
4476
|
+
volume: SCRATCHPAD_VOLUME_VAR,
|
|
4477
|
+
creator: "SP_CREATOR",
|
|
4478
|
+
threadId: "SP_THREAD_ID",
|
|
4479
|
+
rootId: "SP_ROOT_ID",
|
|
4480
|
+
callerAccount: "SP_CALLER_ACCOUNT",
|
|
4481
|
+
callerUser: "SP_CALLER_USER",
|
|
4482
|
+
grants: "SP_GRANTS_FILE"
|
|
4483
|
+
};
|
|
4484
|
+
async function resolveScratchPad(ctx, env) {
|
|
4485
|
+
const log = ctx.logger;
|
|
4486
|
+
const raw = ctx.options[SCRATCHPAD_OPTION];
|
|
4487
|
+
const options = raw !== null && typeof raw === "object" ? raw : {};
|
|
4488
|
+
const pick = (option, variable) => {
|
|
4489
|
+
const fromOptions = options[option];
|
|
4490
|
+
if (typeof fromOptions === "string" && fromOptions !== "") return fromOptions;
|
|
4491
|
+
const fromEnv = env[variable];
|
|
4492
|
+
return fromEnv === void 0 || fromEnv === "" ? void 0 : fromEnv;
|
|
4493
|
+
};
|
|
4494
|
+
const volume = pick("volume", SP_VARS.volume);
|
|
4495
|
+
if (volume === void 0) return void 0;
|
|
4496
|
+
let url = pick("url", "NATS_URL");
|
|
4497
|
+
let creds = pick("creds", "NATS_CREDS");
|
|
4498
|
+
if (url === void 0 || creds === void 0) {
|
|
4499
|
+
const context = env["NATS_CONTEXT"] || stringOf(ctx.settings.config["context"]);
|
|
4500
|
+
if (context !== void 0) {
|
|
4501
|
+
try {
|
|
4502
|
+
const { fields } = await agents.readContextFile(context);
|
|
4503
|
+
url ??= stringOf(fields["url"]);
|
|
4504
|
+
creds ??= stringOf(fields["creds"]);
|
|
4505
|
+
} catch (err) {
|
|
4506
|
+
log.warn(
|
|
4507
|
+
`agent-fabric: ScratchPad is off: the NATS context "${context}" could not be read (${String(err)})`
|
|
4508
|
+
);
|
|
4509
|
+
return void 0;
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
url ??= stringOf(ctx.settings.config["url"]);
|
|
4514
|
+
creds ??= stringOf(ctx.settings.config["credentials"]);
|
|
4515
|
+
if (url === void 0 || creds === void 0) {
|
|
4516
|
+
log.warn(
|
|
4517
|
+
"agent-fabric: ScratchPad is off: sp needs a NATS URL and a credentials file (options.scratchpad, NATS_URL and NATS_CREDS, a NATS context with both, or url and credentials in the plugin's configuration)"
|
|
4518
|
+
);
|
|
4519
|
+
return void 0;
|
|
4520
|
+
}
|
|
4521
|
+
let dir = pick("dir", SP_VARS.dir);
|
|
4522
|
+
if (dir === void 0 && ctx.settings.stateDir !== void 0) {
|
|
4523
|
+
dir = node_path.join(ctx.settings.stateDir, "scratchpad");
|
|
4524
|
+
}
|
|
4525
|
+
if (dir !== void 0) {
|
|
4526
|
+
dir = node_path.resolve(expandHome(dir));
|
|
4527
|
+
try {
|
|
4528
|
+
node_fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
4529
|
+
} catch (err) {
|
|
4530
|
+
log.warn(`agent-fabric: ScratchPad workspace ${dir} could not be created (${String(err)})`);
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4533
|
+
const grants = dir !== void 0 ? `${dir}.grants` : void 0;
|
|
4534
|
+
if (grants !== void 0) pruneRecordedGrants(grants, log);
|
|
4535
|
+
return {
|
|
4536
|
+
url,
|
|
4537
|
+
creds: expandHome(creds),
|
|
4538
|
+
volume,
|
|
4539
|
+
...dir !== void 0 ? { dir } : {},
|
|
4540
|
+
...grants !== void 0 ? { grants } : {}
|
|
4541
|
+
};
|
|
4542
|
+
}
|
|
4543
|
+
function exportScratchPad(settings, env) {
|
|
4544
|
+
env[SP_VARS.url] = settings.url;
|
|
4545
|
+
env[SP_VARS.creds] = settings.creds;
|
|
4546
|
+
env[SP_VARS.volume] = settings.volume;
|
|
4547
|
+
if (settings.dir !== void 0) env[SP_VARS.dir] = settings.dir;
|
|
4548
|
+
if (settings.grants !== void 0) env[SP_VARS.grants] = settings.grants;
|
|
4549
|
+
}
|
|
4550
|
+
var ServedPromptEnv = class {
|
|
4551
|
+
constructor(env) {
|
|
4552
|
+
this.env = env;
|
|
4553
|
+
}
|
|
4554
|
+
env;
|
|
4555
|
+
#holder;
|
|
4556
|
+
enter(request, scope) {
|
|
4557
|
+
this.#clear();
|
|
4558
|
+
this.#holder = request.id;
|
|
4559
|
+
if (scope !== void 0) {
|
|
4560
|
+
this.env[SP_VARS.threadId] = scope.threadId;
|
|
4561
|
+
this.env[SP_VARS.rootId] = scope.rootId;
|
|
4562
|
+
}
|
|
4563
|
+
if (request.caller !== void 0) {
|
|
4564
|
+
try {
|
|
4565
|
+
const grantee = granteeFromIdentity(request.caller);
|
|
4566
|
+
this.env[SP_VARS.callerAccount] = grantee.account;
|
|
4567
|
+
this.env[SP_VARS.callerUser] = grantee.user;
|
|
4568
|
+
} catch {
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
leave(request) {
|
|
4573
|
+
if (this.#holder === request.id) {
|
|
4574
|
+
this.#clear();
|
|
4575
|
+
this.#holder = void 0;
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
#clear() {
|
|
4579
|
+
delete this.env[SP_VARS.threadId];
|
|
4580
|
+
delete this.env[SP_VARS.rootId];
|
|
4581
|
+
delete this.env[SP_VARS.callerAccount];
|
|
4582
|
+
delete this.env[SP_VARS.callerUser];
|
|
4583
|
+
}
|
|
4584
|
+
};
|
|
4585
|
+
function spBinary(env, skillDir) {
|
|
4586
|
+
const own = env[SCRATCHPAD_BIN_VAR];
|
|
4587
|
+
if (own !== void 0 && own !== "") return own;
|
|
4588
|
+
const os = node_os.platform() === "darwin" ? "darwin" : node_os.platform() === "linux" ? "linux" : void 0;
|
|
4589
|
+
const cpu = node_os.arch() === "arm64" ? "arm64" : node_os.arch() === "x64" ? "amd64" : void 0;
|
|
4590
|
+
if (os === void 0 || cpu === void 0) return void 0;
|
|
4591
|
+
return node_path.join(skillDir, "bin", `sp-${os}-${cpu}`);
|
|
4592
|
+
}
|
|
4593
|
+
var ScratchPadHandOn = class {
|
|
4594
|
+
#inner;
|
|
4595
|
+
#scratchPad;
|
|
4596
|
+
#early = [];
|
|
4597
|
+
requestInterceptor = {
|
|
4598
|
+
aroundRequest: (ctx, next) => {
|
|
4599
|
+
if (this.#inner === void 0) this.#early.push(ctx.envelope.prompt);
|
|
4600
|
+
else this.#inner.remember(ctx.envelope.prompt);
|
|
4601
|
+
return next();
|
|
4602
|
+
}
|
|
4603
|
+
};
|
|
4604
|
+
/**
|
|
4605
|
+
* Builds the extension, once the plugin's client is on the bus. Given the
|
|
4606
|
+
* plugin's service, also answers grant requests for what this agent
|
|
4607
|
+
* published, on the endpoint an SDK-built agent serves them on
|
|
4608
|
+
* (`grantRequestEndpoint`), added through the service's own
|
|
4609
|
+
* `addEndpoint`, since the plugin builds its service before the add-on
|
|
4610
|
+
* could name an extra endpoint. Several instances of one agent share the
|
|
4611
|
+
* work, as an SDK-built agent's do.
|
|
4612
|
+
*/
|
|
4613
|
+
start(options) {
|
|
4614
|
+
const { scratchPad, agents: agents$1, service } = options;
|
|
4615
|
+
this.#scratchPad = scratchPad;
|
|
4616
|
+
this.#inner = scratchPadExtension({ scratchPad, agents: agents$1 });
|
|
4617
|
+
for (const text2 of this.#early.splice(0)) this.#inner.remember(text2);
|
|
4618
|
+
if (service === void 0) return;
|
|
4619
|
+
const logger = options.logger ?? agents.SILENT_LOGGER;
|
|
4620
|
+
const ep = grantRequestEndpoint({
|
|
4621
|
+
promptAddress: service.subject.prompt,
|
|
4622
|
+
scratchPad,
|
|
4623
|
+
logger
|
|
4624
|
+
});
|
|
4625
|
+
try {
|
|
4626
|
+
service.service.addEndpoint(ep.name, {
|
|
4627
|
+
subject: ep.subject,
|
|
4628
|
+
...ep.queue !== void 0 ? { queue: ep.queue } : {},
|
|
4629
|
+
handler: ep.handler
|
|
4630
|
+
});
|
|
4631
|
+
} catch (err) {
|
|
4632
|
+
logger.warn(`agent-fabric: grant requests are not answered: ${String(err)}`);
|
|
4633
|
+
}
|
|
4634
|
+
}
|
|
4635
|
+
discoveryFields(agent) {
|
|
4636
|
+
return this.#inner?.discoveryFields(agent) ?? {
|
|
4637
|
+
[READS_SCRATCHPAD_REFERENCES_FIELD]: readsReferences(agent.metadata)
|
|
4638
|
+
};
|
|
4639
|
+
}
|
|
4640
|
+
async beforePrompt(ctx) {
|
|
4641
|
+
const inner = this.#inner;
|
|
4642
|
+
const scratchPad = this.#scratchPad;
|
|
4643
|
+
if (inner === void 0 || scratchPad === void 0) {
|
|
4644
|
+
if (findReferences(ctx.prompt).length === 0) return;
|
|
4645
|
+
return {
|
|
4646
|
+
error: "the prompt was not sent: ScratchPad is not ready yet; try again in a moment"
|
|
4647
|
+
};
|
|
4648
|
+
}
|
|
4649
|
+
for (const reference of new Set(findReferences(ctx.prompt))) {
|
|
4650
|
+
if (await scratchPad.owns(reference)) inner.remember(reference);
|
|
4651
|
+
}
|
|
4652
|
+
return inner.beforePrompt(ctx);
|
|
4653
|
+
}
|
|
4654
|
+
afterReply(ctx) {
|
|
4655
|
+
this.#inner?.afterReply(ctx);
|
|
4656
|
+
}
|
|
4657
|
+
};
|
|
4658
|
+
function spPort(settings, creator, env, skillDir) {
|
|
4659
|
+
const bin = spBinary(env, skillDir);
|
|
4660
|
+
if (bin === void 0) return void 0;
|
|
4661
|
+
const port = spCliScratchPad({
|
|
4662
|
+
volume: settings.volume,
|
|
4663
|
+
nats: { url: settings.url, creds: settings.creds },
|
|
4664
|
+
bin,
|
|
4665
|
+
creator,
|
|
4666
|
+
...settings.dir !== void 0 ? { dir: settings.dir } : {}
|
|
4667
|
+
});
|
|
4668
|
+
return settings.grants !== void 0 ? withRecordedGrants(port, settings.grants) : port;
|
|
4669
|
+
}
|
|
4670
|
+
var RECORDED_LINE = /^(\d+) (\S+) (\S+) (sp-(?:ref|r1):[A-Za-z0-9_-]+)$/;
|
|
4671
|
+
function readRecordedGrants(file) {
|
|
4672
|
+
let text2;
|
|
4673
|
+
try {
|
|
4674
|
+
text2 = node_fs.readFileSync(file, "utf8");
|
|
4675
|
+
} catch {
|
|
4676
|
+
return [];
|
|
4677
|
+
}
|
|
4678
|
+
const grants = [];
|
|
4679
|
+
for (const line of text2.split("\n")) {
|
|
4680
|
+
const m = RECORDED_LINE.exec(line);
|
|
4681
|
+
if (m === null) continue;
|
|
4682
|
+
grants.push({
|
|
4683
|
+
expiresAt: Number(m[1]),
|
|
4684
|
+
grantee: { account: m[2], user: m[3] },
|
|
4685
|
+
reference: m[4]
|
|
4686
|
+
});
|
|
4687
|
+
}
|
|
4688
|
+
return grants;
|
|
4689
|
+
}
|
|
4690
|
+
function pruneRecordedGrants(file, log) {
|
|
4691
|
+
const t = Math.floor(Date.now() / 1e3);
|
|
4692
|
+
const live = readRecordedGrants(file).filter((g) => g.expiresAt > t);
|
|
4693
|
+
try {
|
|
4694
|
+
const staging = `${file}.${process.pid}.tmp`;
|
|
4695
|
+
node_fs.writeFileSync(
|
|
4696
|
+
staging,
|
|
4697
|
+
live.map((g) => `${g.expiresAt} ${g.grantee.account} ${g.grantee.user} ${g.reference}
|
|
4698
|
+
`).join(""),
|
|
4699
|
+
{ mode: 384 }
|
|
4700
|
+
);
|
|
4701
|
+
node_fs.renameSync(staging, file);
|
|
4702
|
+
} catch (err) {
|
|
4703
|
+
log.warn(`agent-fabric: ScratchPad grants file ${file} could not be written (${String(err)})`);
|
|
4704
|
+
}
|
|
4705
|
+
}
|
|
4706
|
+
function withRecordedGrants(port, file, now3 = () => Date.now() / 1e3) {
|
|
4707
|
+
return {
|
|
4708
|
+
...port.resolveReference ? {
|
|
4709
|
+
resolveReference: (reference, trace) => port.resolveReference(reference, trace)
|
|
4710
|
+
} : {},
|
|
4711
|
+
owns: (reference) => port.owns(reference),
|
|
4712
|
+
grantRead: (reference, grantee, trace, toolCallId, kind) => port.grantRead(reference, grantee, trace, toolCallId, kind),
|
|
4713
|
+
async granted(reference, grantee) {
|
|
4714
|
+
const own = await port.granted(reference, grantee);
|
|
4715
|
+
if (own === "first-hand") return own;
|
|
4716
|
+
const t = now3();
|
|
4717
|
+
const recorded = readRecordedGrants(file).some(
|
|
4718
|
+
(g) => g.reference === reference && g.grantee.account === grantee.account && g.grantee.user === grantee.user && g.expiresAt > t
|
|
4719
|
+
);
|
|
4720
|
+
return recorded ? "first-hand" : own;
|
|
4721
|
+
}
|
|
4722
|
+
};
|
|
4723
|
+
}
|
|
4724
|
+
function stringOf(value) {
|
|
4725
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
4726
|
+
}
|
|
4727
|
+
function expandHome(path) {
|
|
4728
|
+
if (path === "~") return node_os.homedir();
|
|
4729
|
+
if (path.startsWith("~/")) return node_path.join(node_os.homedir(), path.slice(2));
|
|
4730
|
+
return path;
|
|
4731
|
+
}
|
|
4732
|
+
|
|
4733
|
+
exports.DEFAULT_EDGE_SUBJECT = DEFAULT_EDGE_SUBJECT;
|
|
4734
|
+
exports.DEFAULT_GRANT_REQUEST_TIMEOUT_MS = DEFAULT_GRANT_REQUEST_TIMEOUT_MS;
|
|
4735
|
+
exports.DEFAULT_GRANT_TTL_S = DEFAULT_GRANT_TTL_S;
|
|
4736
|
+
exports.DEFAULT_READ_LIMIT_BYTES = DEFAULT_READ_LIMIT_BYTES;
|
|
4737
|
+
exports.DEFAULT_SP_BIN = DEFAULT_SP_BIN;
|
|
4738
|
+
exports.EDGE_RECORD_VERSION = EDGE_RECORD_VERSION;
|
|
4739
|
+
exports.EDGE_SUBJECT_OPTION = EDGE_SUBJECT_OPTION;
|
|
4740
|
+
exports.EDGE_SUBJECT_VAR = EDGE_SUBJECT_VAR;
|
|
4741
|
+
exports.GRANT_REQUEST_ENDPOINT = GRANT_REQUEST_ENDPOINT;
|
|
4742
|
+
exports.HARNESS_THREAD_ID_MAX = HARNESS_THREAD_ID_MAX;
|
|
4743
|
+
exports.LOOP_GUARD_CODE = LOOP_GUARD_CODE;
|
|
4744
|
+
exports.NativeScratchPad = NativeScratchPad;
|
|
4745
|
+
exports.NativeScratchPadError = NativeScratchPadError;
|
|
4746
|
+
exports.READS_REFERENCES_KEY = READS_REFERENCES_KEY;
|
|
4747
|
+
exports.READS_SCRATCHPAD_REFERENCES_FIELD = READS_SCRATCHPAD_REFERENCES_FIELD;
|
|
4748
|
+
exports.REFERENCE_PREFIX = REFERENCE_PREFIX;
|
|
4749
|
+
exports.ROOT_ID_FIELD = ROOT_ID_FIELD;
|
|
4750
|
+
exports.ROOT_ID_HEADER = ROOT_ID_HEADER;
|
|
4751
|
+
exports.SCRATCHPAD_BIN_VAR = SCRATCHPAD_BIN_VAR;
|
|
4752
|
+
exports.SCRATCHPAD_DIR_VAR = SCRATCHPAD_DIR_VAR;
|
|
4753
|
+
exports.SCRATCHPAD_GRANT_TTL_S_VAR = SCRATCHPAD_GRANT_TTL_S_VAR;
|
|
4754
|
+
exports.SCRATCHPAD_OPTION = SCRATCHPAD_OPTION;
|
|
4755
|
+
exports.SCRATCHPAD_VOLUME_VAR = SCRATCHPAD_VOLUME_VAR;
|
|
4756
|
+
exports.SERVED_RECORD_REASON_VERSION = SERVED_RECORD_REASON_VERSION;
|
|
4757
|
+
exports.SERVED_RECORD_VERSION = SERVED_RECORD_VERSION;
|
|
4758
|
+
exports.SHORT_REFERENCE_PREFIX = SHORT_REFERENCE_PREFIX;
|
|
4759
|
+
exports.SP_VARS = SP_VARS;
|
|
4760
|
+
exports.ScratchPadError = ScratchPadError;
|
|
4761
|
+
exports.ScratchPadExtension = ScratchPadExtension;
|
|
4762
|
+
exports.ScratchPadHandOn = ScratchPadHandOn;
|
|
4763
|
+
exports.ScratchPadReader = ScratchPadReader;
|
|
4764
|
+
exports.ServedPromptEnv = ServedPromptEnv;
|
|
4765
|
+
exports.ServedPublisher = ServedPublisher;
|
|
4766
|
+
exports.THREAD_ID_FIELD = THREAD_ID_FIELD;
|
|
4767
|
+
exports.THREAD_ID_HEADER = THREAD_ID_HEADER;
|
|
4768
|
+
exports.THREAD_ID_HEX_LEN = THREAD_ID_HEX_LEN;
|
|
4769
|
+
exports.TOOL_CALL_ID_CONTEXT_KEY = TOOL_CALL_ID_CONTEXT_KEY;
|
|
4770
|
+
exports.TOOL_CALL_ID_MAX = TOOL_CALL_ID_MAX;
|
|
4771
|
+
exports.activeTrace = activeTrace;
|
|
4772
|
+
exports.answerGrantRequest = answerGrantRequest;
|
|
4773
|
+
exports.assertValidTraceOptions = assertValidTraceOptions;
|
|
4774
|
+
exports.bindActiveTrace = bindActiveTrace;
|
|
4775
|
+
exports.buildEdgeRecord = buildEdgeRecord;
|
|
4776
|
+
exports.buildServedRecord = buildServedRecord;
|
|
4777
|
+
exports.countTraceRecordDropped = countTraceRecordDropped;
|
|
4778
|
+
exports.countTraceRecordPublished = countTraceRecordPublished;
|
|
4779
|
+
exports.decodeReference = decodeReference;
|
|
4780
|
+
exports.decodeShortReference = decodeShortReference;
|
|
4781
|
+
exports.durableHeaders = durableHeaders;
|
|
4782
|
+
exports.durableRunId = durableRunId;
|
|
4783
|
+
exports.durableTrace = durableTrace;
|
|
4784
|
+
exports.encodeGrantReply = encodeGrantReply;
|
|
4785
|
+
exports.encodeGrantRequest = encodeGrantRequest;
|
|
4786
|
+
exports.encodeReference = encodeReference;
|
|
4787
|
+
exports.exportScratchPad = exportScratchPad;
|
|
4788
|
+
exports.fabricTracing = fabricTracing;
|
|
4789
|
+
exports.findReferences = findReferences;
|
|
4790
|
+
exports.grantRequestEndpoint = grantRequestEndpoint;
|
|
4791
|
+
exports.grantSubject = grantSubject;
|
|
4792
|
+
exports.granteeFromIdentity = granteeFromIdentity;
|
|
4793
|
+
exports.isThreadId = isThreadId;
|
|
4794
|
+
exports.loopGuard = loopGuard;
|
|
4795
|
+
exports.loopGuardDescription = loopGuardDescription;
|
|
4796
|
+
exports.nativeScratchPad = nativeScratchPad;
|
|
4797
|
+
exports.nativeScratchPadFromEnv = nativeScratchPadFromEnv;
|
|
4798
|
+
exports.randomThreadId = randomThreadId;
|
|
4799
|
+
exports.readRecordedGrants = readRecordedGrants;
|
|
4800
|
+
exports.readsReferences = readsReferences;
|
|
4801
|
+
exports.referenceCreator = referenceCreator;
|
|
4802
|
+
exports.removeReferences = removeReferences;
|
|
4803
|
+
exports.requestGrant = requestGrant;
|
|
4804
|
+
exports.resolveEdgeSubject = resolveEdgeSubject;
|
|
4805
|
+
exports.resolveScratchPad = resolveScratchPad;
|
|
4806
|
+
exports.scratchPadExtension = scratchPadExtension;
|
|
4807
|
+
exports.scratchPadFromEnv = scratchPadFromEnv;
|
|
4808
|
+
exports.scratchPadReader = scratchPadReader;
|
|
4809
|
+
exports.servedPublisher = servedPublisher;
|
|
4810
|
+
exports.spBinary = spBinary;
|
|
4811
|
+
exports.spCliScratchPad = spCliScratchPad;
|
|
4812
|
+
exports.spPort = spPort;
|
|
4813
|
+
exports.traceHeaders = traceHeaders;
|
|
4814
|
+
exports.traceRecordCounts = traceRecordCounts;
|
|
4815
|
+
exports.unixSeconds = unixSeconds;
|
|
4816
|
+
exports.validHarnessThreadId = validHarnessThreadId;
|
|
4817
|
+
exports.validToolCallId = validToolCallId;
|
|
4818
|
+
exports.withDurableTrace = withDurableTrace;
|
|
4819
|
+
exports.withRecordedGrants = withRecordedGrants;
|
|
4820
|
+
//# sourceMappingURL=index.cjs.map
|
|
4821
|
+
//# sourceMappingURL=index.cjs.map
|