narraeon 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/node/protocol/appPreferences.js +25 -0
- package/dist/node/protocol/conversationObservation.js +67 -0
- package/dist/node/protocol/v1.js +188 -34
- package/dist/node/protocol/worldExtensions.js +16 -0
- package/dist/node/protocol/worldMaintenance.js +77 -0
- package/dist/node/protocol/worldPromptDiagnostics.js +1 -0
- package/dist/node/runtime/ConversationChanges.js +24 -0
- package/dist/node/runtime/V1Runtime.js +447 -121
- package/dist/node/runtime/artifact/FileNativeArtifactStore.js +326 -12
- package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +63 -0
- package/dist/node/runtime/authoring/ModelToolConversation.js +218 -0
- package/dist/node/runtime/config/AppPreferencesStore.js +46 -17
- package/dist/node/runtime/content/ContentTreeFingerprint.js +13 -0
- package/dist/node/runtime/content/ContentWorkspace.js +43 -13
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +128 -46
- package/dist/node/runtime/content/FileNativeContentTree.js +28 -10
- package/dist/node/runtime/content/PackageFollowups.js +80 -0
- package/dist/node/runtime/extension/FrontendExtensionBundle.js +19 -14
- package/dist/node/runtime/extension/PackageScriptPermissions.js +70 -0
- package/dist/node/runtime/extension/WorldExtensionControls.js +205 -0
- package/dist/node/runtime/extension/WorldExtensionRequests.js +64 -0
- package/dist/node/runtime/model/FileNativeModelAdapters.js +21 -466
- package/dist/node/runtime/model/ModelConnectionStore.js +44 -10
- package/dist/node/runtime/model/ModelHost.js +31 -1
- package/dist/node/runtime/model/ModelHostStream.js +51 -5
- package/dist/node/runtime/play/FileNativeContinuityCorrection.js +55 -45
- package/dist/node/runtime/play/FileNativePlayPresetStore.js +256 -238
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +118 -9
- package/dist/node/runtime/play/OrderedFollowups.js +71 -0
- package/dist/node/runtime/play/OrderedPlayPrompts.js +180 -0
- package/dist/node/runtime/play/PlayCallChain.js +814 -61
- package/dist/node/runtime/play/PlayContinuity.js +88 -0
- package/dist/node/runtime/play/PlayDocumentTools.js +205 -287
- package/dist/node/runtime/play/PlayFollowupRequests.js +34 -3
- package/dist/node/runtime/play/PlayPresetWorkbench.js +45 -54
- package/dist/node/runtime/play/PlayPromptRun.js +86 -0
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +670 -93
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +37 -21
- package/dist/node/runtime/prompt/PromptCompilationCodec.js +322 -0
- package/dist/node/runtime/prompt/WorldMaintenanceReport.js +251 -0
- package/dist/node/runtime/prompt/WorldMaterialCoverage.js +83 -0
- package/dist/node/runtime/prompt/WorldPromptDiagnostics.js +185 -0
- package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +1027 -0
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +1339 -0
- package/dist/node/runtime/setting/SettingImprovementSession.js +714 -0
- package/dist/node/runtime/world/FileNativeAuthorityV3.js +88 -1
- package/dist/node/runtime/world/FileNativeWorldStore.js +292 -27
- package/dist/node/runtime/world/PlayerViewBindings.js +30 -0
- package/dist/node/runtime/world/PlayerViewRenderer.js +27 -34
- package/dist/node/runtime/world/WorldDocumentMaintenance.js +174 -0
- package/dist/node/runtime/world/WorldDocumentStore.js +166 -96
- package/dist/node/runtime/world/WorldOperationCoordinator.js +103 -2
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +696 -0
- package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +380 -0
- package/dist/node/runtime/world-revision/WorldRevisionSession.js +709 -0
- package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +494 -0
- package/dist/node/server/ConversationStream.js +109 -0
- package/dist/node/server/createServer.js +5 -1
- package/dist/node/shared/artifact-payload-contract.js +123 -0
- package/dist/node/shared/default-play-prompts.js +25 -45
- package/dist/node/shared/default-preset-host.js +114 -138
- package/dist/node/shared/default-setting-improvement-prompt.js +52 -38
- package/dist/node/shared/display-regex.js +114 -0
- package/dist/node/shared/first-party-action-choices.js +1 -6
- package/dist/node/shared/first-party-player-view.js +1 -6
- package/dist/node/shared/ordered-author-prompts.js +64 -0
- package/dist/node/shared/ordered-followups.js +42 -0
- package/dist/node/shared/ordered-play-prompts.js +73 -0
- package/dist/web/assets/index-BXfelCi9.js +1207 -0
- package/dist/web/assets/index-BYnhUxlk.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/node/runtime/model/SettingAuthorStream.js +0 -21
- package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +0 -1731
- package/dist/web/assets/index-D5VqDvjY.js +0 -1192
- package/dist/web/assets/index-Xa0s35CJ.css +0 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { renderDocumentWritePosition } from "../prompt/WorldMaintenanceReport.js";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { stringify } from "yaml";
|
|
4
|
+
import { renderPlayerViewBindings } from "../world/PlayerViewBindings.js";
|
|
3
5
|
import { PromptCompilationError, } from "../prompt/FileNativePromptCompiler.js";
|
|
4
6
|
import {} from "../world/WorldDocumentStore.js";
|
|
5
7
|
import { acceptWorldStateRevision, beginWorldStateRevision, fileNativeStateChanges, worldDocumentRevisionFailureMessage, } from "../world/WorldStateRevision.js";
|
|
@@ -18,20 +20,54 @@ class PlayDocumentToolFailure extends Error {
|
|
|
18
20
|
*/
|
|
19
21
|
export class FileNativePlayDocuments {
|
|
20
22
|
#candidate;
|
|
23
|
+
#committedSnapshot;
|
|
21
24
|
#reads;
|
|
25
|
+
#declaredDirectories = [];
|
|
26
|
+
#maintenanceWarning;
|
|
27
|
+
get maintenanceWarning() {
|
|
28
|
+
return this.#maintenanceWarning;
|
|
29
|
+
}
|
|
30
|
+
bindMaintenanceUnavailable(reason, locale) {
|
|
31
|
+
this.#maintenance = {};
|
|
32
|
+
this.#maintenanceLocale = locale;
|
|
33
|
+
this.#maintenanceWarning = reason;
|
|
34
|
+
}
|
|
35
|
+
#maintenanceLocale = "en";
|
|
36
|
+
#maintenance = {};
|
|
37
|
+
bindMaintenance(facts, locale = "en") {
|
|
38
|
+
this.#maintenanceWarning = undefined;
|
|
39
|
+
this.#maintenance = structuredClone(facts);
|
|
40
|
+
this.#maintenanceLocale = locale;
|
|
41
|
+
}
|
|
22
42
|
constructor(files) {
|
|
23
43
|
this.#candidate = openPlayCandidate({ ...files });
|
|
44
|
+
this.#committedSnapshot = this.#candidate.snapshot;
|
|
24
45
|
this.#reads = {
|
|
25
46
|
snapshotId: this.#candidate.snapshot.id,
|
|
26
47
|
documents: new Map(),
|
|
27
|
-
pendingReads: new Map(),
|
|
28
48
|
};
|
|
29
49
|
}
|
|
30
50
|
get snapshot() {
|
|
31
51
|
return this.#candidate.snapshot;
|
|
32
52
|
}
|
|
33
|
-
|
|
34
|
-
|
|
53
|
+
get committedSnapshot() {
|
|
54
|
+
return this.#committedSnapshot;
|
|
55
|
+
}
|
|
56
|
+
bindBootstrap(bootstrap, previousAuthorization) {
|
|
57
|
+
const injected = bootstrapAuthorizations(bootstrap, this.#candidate.snapshot);
|
|
58
|
+
this.#reads = injected;
|
|
59
|
+
// Refreshing a prefix retains the native conversation. Its exact reads
|
|
60
|
+
// remain valid only on the identical state tree, including our own writes.
|
|
61
|
+
if (previousAuthorization?.stateFingerprint ===
|
|
62
|
+
fingerprintStateFiles(this.#candidate.files)) {
|
|
63
|
+
this.restoreAuthorizationCheckpoint(previousAuthorization);
|
|
64
|
+
for (const [ref, locators] of injected.documents) {
|
|
65
|
+
for (const locator of locators ?? [null])
|
|
66
|
+
authorizeRead(this.#reads, this.#candidate.snapshot.id, ref, locator);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Creation capabilities come from this prefix, not past directory slots.
|
|
70
|
+
this.#declaredDirectories = declaredStateDirectories(bootstrap);
|
|
35
71
|
}
|
|
36
72
|
authorizationCheckpoint() {
|
|
37
73
|
return {
|
|
@@ -75,62 +111,33 @@ export class FileNativePlayDocuments {
|
|
|
75
111
|
this.#reads = {
|
|
76
112
|
snapshotId: this.#candidate.snapshot.id,
|
|
77
113
|
documents,
|
|
78
|
-
// Provider cursors are deliberately snapshot-local. A partial read has
|
|
79
|
-
// granted no write authority yet, so it restarts after cold recovery.
|
|
80
|
-
pendingReads: new Map(),
|
|
81
114
|
};
|
|
82
115
|
}
|
|
83
|
-
contextToolBudgetRequest(call, history) {
|
|
84
|
-
const preview = previewContextToolResult(this.#candidate.snapshot, history, call);
|
|
85
|
-
if (preview === null)
|
|
86
|
-
return null;
|
|
87
|
-
const retryParameter = call.name === "context_read" ? "max_bytes" : "limit";
|
|
88
|
-
return {
|
|
89
|
-
requestedResultBytes: Buffer.byteLength(preview.markdown, "utf8"),
|
|
90
|
-
previewMarkdown: preview.markdown,
|
|
91
|
-
retryParameter,
|
|
92
|
-
retryMinimum: call.name === "context_read" ? 4 : 1,
|
|
93
|
-
retryMaximum: contextToolRetryMaximum(call),
|
|
94
|
-
restartWithoutCursor: record(call.arguments) && typeof call.arguments.cursor === "string",
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
contextToolRetryPreview(call, history, retryValue) {
|
|
98
|
-
const argumentsWithoutCursor = record(call.arguments)
|
|
99
|
-
? Object.fromEntries(Object.entries(call.arguments).filter(([key]) => key !== "cursor"))
|
|
100
|
-
: {};
|
|
101
|
-
const retryCall = {
|
|
102
|
-
...structuredClone(call),
|
|
103
|
-
id: `${call.id}-budget-retry`,
|
|
104
|
-
arguments: {
|
|
105
|
-
...argumentsWithoutCursor,
|
|
106
|
-
[call.name === "context_read" ? "maxBytes" : "limit"]: retryValue,
|
|
107
|
-
},
|
|
108
|
-
};
|
|
109
|
-
const preview = previewContextToolResult(this.#candidate.snapshot, history, retryCall);
|
|
110
|
-
if (preview === null)
|
|
111
|
-
throw new TypeError("context tool retry preview requires a read tool");
|
|
112
|
-
return { call: retryCall, markdown: preview.markdown };
|
|
113
|
-
}
|
|
114
116
|
execute(call, history) {
|
|
115
117
|
if (call.name === "context_search")
|
|
116
118
|
return executeContextSearch(this.#candidate.snapshot, history, call.arguments);
|
|
117
119
|
if (call.name === "state_list")
|
|
118
|
-
return executeStateList(this.#candidate.snapshot, history, call.arguments);
|
|
120
|
+
return executeStateList(this.#candidate.snapshot, history, call.arguments, this.#declaredDirectories);
|
|
119
121
|
if (call.name === "history_list")
|
|
120
122
|
return executeHistoryList(this.#candidate.snapshot, history, call.arguments);
|
|
121
123
|
// Frozen contexts created before runtime-tools-v5 retain this exact name
|
|
122
124
|
// and argument shape. New contexts never advertise it.
|
|
123
125
|
if (call.name === "context_list")
|
|
124
|
-
return executeContextList(this.#candidate.snapshot, history, call.arguments);
|
|
126
|
+
return executeContextList(this.#candidate.snapshot, history, call.arguments, this.#declaredDirectories);
|
|
125
127
|
if (call.name === "context_read") {
|
|
126
128
|
const result = executeContextRead(this.#candidate.snapshot, history, call.arguments);
|
|
127
129
|
authorizeToolRead(this.#reads, this.#candidate.snapshot, result);
|
|
130
|
+
const ref = result.readAuthorization?.shortRef;
|
|
131
|
+
if (ref !== undefined && result.markdown !== undefined)
|
|
132
|
+
result.markdown += `\n\n${this.#maintenanceWarning === undefined ? renderDocumentWritePosition(ref, this.#maintenance[ref], this.#maintenanceLocale) : `${this.#maintenanceLocale === "zh-CN" ? "最近写入位置暂时不可用" : "Last committed write unavailable"}: ${this.#maintenanceWarning}`}`;
|
|
128
133
|
return result;
|
|
129
134
|
}
|
|
130
135
|
if (call.name === "world_patch")
|
|
131
136
|
return applyPlayCandidatePatch(this.#candidate, this.#reads, call.arguments);
|
|
132
137
|
if (call.name === "world_create")
|
|
133
|
-
return applyPlayCandidateCreate(this.#candidate, this.#reads, call.arguments);
|
|
138
|
+
return applyPlayCandidateCreate(this.#candidate, this.#reads, call.arguments, this.#declaredDirectories);
|
|
139
|
+
if (call.name === "world_retire")
|
|
140
|
+
return applyPlayCandidateRetirement(this.#candidate, this.#reads, call.arguments);
|
|
134
141
|
return toolFailure(`The current document context does not accept ${call.name}`);
|
|
135
142
|
}
|
|
136
143
|
stateChanges() {
|
|
@@ -142,15 +149,14 @@ export class FileNativePlayDocuments {
|
|
|
142
149
|
* this new committed baseline instead of replaying the old diff.
|
|
143
150
|
*/
|
|
144
151
|
acceptCommittedState() {
|
|
152
|
+
this.#committedSnapshot = this.#candidate.snapshot;
|
|
145
153
|
this.#candidate.changes.clear();
|
|
146
|
-
this.#candidate.suppliedBytes = 0;
|
|
147
154
|
}
|
|
148
155
|
}
|
|
149
156
|
function bootstrapAuthorizations(bootstrap, snapshot) {
|
|
150
157
|
const result = {
|
|
151
158
|
snapshotId: snapshot.id,
|
|
152
159
|
documents: new Map(),
|
|
153
|
-
pendingReads: new Map(),
|
|
154
160
|
};
|
|
155
161
|
for (const { status, complete, readAuthorization } of bootstrap.coverage) {
|
|
156
162
|
if (status !== "resolved" || !complete || readAuthorization === undefined)
|
|
@@ -159,7 +165,6 @@ function bootstrapAuthorizations(bootstrap, snapshot) {
|
|
|
159
165
|
const resolved = snapshot.query({
|
|
160
166
|
kind: "read_document",
|
|
161
167
|
document: { shortRef: readAuthorization.shortRef },
|
|
162
|
-
maxBytes: 4,
|
|
163
168
|
});
|
|
164
169
|
if (resolved.kind === "read_document" && resolved.ok)
|
|
165
170
|
authorizeRead(result, snapshot.id, resolved.document.shortRef, null);
|
|
@@ -176,6 +181,16 @@ function bootstrapAuthorizations(bootstrap, snapshot) {
|
|
|
176
181
|
}
|
|
177
182
|
return result;
|
|
178
183
|
}
|
|
184
|
+
function declaredStateDirectories(bootstrap) {
|
|
185
|
+
// Catalog coverage is the durable compiler projection shared by current
|
|
186
|
+
// state_list contexts and legacy context_list contexts.
|
|
187
|
+
return [
|
|
188
|
+
...new Set(bootstrap.coverage
|
|
189
|
+
.filter(({ slot }) => slot === "catalog")
|
|
190
|
+
.map(({ source }) => source)
|
|
191
|
+
.filter(validStateDirectory)),
|
|
192
|
+
].sort((left, right) => left.localeCompare(right));
|
|
193
|
+
}
|
|
179
194
|
function authorizeRead(authorizations, snapshotId, ref, locator) {
|
|
180
195
|
if (authorizations.snapshotId !== snapshotId)
|
|
181
196
|
return;
|
|
@@ -192,31 +207,7 @@ function authorizeToolRead(authorizations, snapshot, result) {
|
|
|
192
207
|
if (authorization?.snapshotId !== snapshot.id ||
|
|
193
208
|
authorizations.snapshotId !== snapshot.id)
|
|
194
209
|
return;
|
|
195
|
-
|
|
196
|
-
let progress = authorizations.pendingReads.get(authorization.readKey);
|
|
197
|
-
if (page.start === 0) {
|
|
198
|
-
progress = {
|
|
199
|
-
shortRef: authorization.shortRef,
|
|
200
|
-
locator: authorization.locator,
|
|
201
|
-
nextOffset: page.end,
|
|
202
|
-
totalBytes: page.total,
|
|
203
|
-
};
|
|
204
|
-
authorizations.pendingReads.set(authorization.readKey, progress);
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
207
|
-
if (progress === undefined)
|
|
208
|
-
return;
|
|
209
|
-
if (progress.shortRef !== authorization.shortRef ||
|
|
210
|
-
!sameLocator(progress.locator, authorization.locator) ||
|
|
211
|
-
progress.nextOffset !== page.start ||
|
|
212
|
-
progress.totalBytes !== page.total)
|
|
213
|
-
return;
|
|
214
|
-
progress.nextOffset = page.end;
|
|
215
|
-
}
|
|
216
|
-
if (progress.nextOffset !== progress.totalBytes)
|
|
217
|
-
return;
|
|
218
|
-
authorizeRead(authorizations, snapshot.id, progress.shortRef, progress.locator);
|
|
219
|
-
authorizations.pendingReads.delete(authorization.readKey);
|
|
210
|
+
authorizeRead(authorizations, snapshot.id, authorization.shortRef, authorization.locator);
|
|
220
211
|
}
|
|
221
212
|
function sameLocator(left, right) {
|
|
222
213
|
if (left === null || right === null)
|
|
@@ -231,16 +222,12 @@ function parseYamlHandleSegment(segment) {
|
|
|
231
222
|
return /^(?:0|[1-9][0-9]*)$/u.test(segment) ? Number(segment) : segment;
|
|
232
223
|
}
|
|
233
224
|
function openPlayCandidate(files) {
|
|
234
|
-
return
|
|
235
|
-
...beginWorldStateRevision(files),
|
|
236
|
-
suppliedBytes: 0,
|
|
237
|
-
};
|
|
225
|
+
return beginWorldStateRevision(files);
|
|
238
226
|
}
|
|
239
227
|
function documentDescriptorByRef(snapshot, ref) {
|
|
240
228
|
const result = snapshot.query({
|
|
241
229
|
kind: "read_document",
|
|
242
230
|
document: { shortRef: ref },
|
|
243
|
-
maxBytes: 4,
|
|
244
231
|
});
|
|
245
232
|
return result.kind === "read_document" && result.ok ? result.document : null;
|
|
246
233
|
}
|
|
@@ -255,17 +242,11 @@ function applyPlayCandidatePatch(candidate, reads, args) {
|
|
|
255
242
|
const target = documentDescriptorByRef(candidate.snapshot, ref);
|
|
256
243
|
if (target === null)
|
|
257
244
|
return toolFailure(`Target @${ref} does not exist.`);
|
|
258
|
-
const retainedAuthorization = reads.documents.get(ref);
|
|
245
|
+
const retainedAuthorization = authorizationsAfterEdits(reads.documents.get(ref), args.edits);
|
|
259
246
|
if (reads.snapshotId !== candidate.snapshot.id ||
|
|
260
|
-
retainedAuthorization === undefined
|
|
261
|
-
!editsAreAuthorized(retainedAuthorization, args.edits))
|
|
247
|
+
retainedAuthorization === undefined)
|
|
262
248
|
return toolFailure(`Read @${ref} exactly, or receive the complete document in bootstrap, before writing it.`);
|
|
263
249
|
try {
|
|
264
|
-
const suppliedBytes = Buffer.byteLength(JSON.stringify(args.edits), "utf8");
|
|
265
|
-
if (suppliedBytes > 64 * 1024)
|
|
266
|
-
throw new PlayDocumentToolFailure("A single patch body exceeds 64 KiB");
|
|
267
|
-
if (candidate.suppliedBytes + suppliedBytes > 256 * 1024)
|
|
268
|
-
throw new PlayDocumentToolFailure("New body content for this model operation exceeds 256 KiB in total");
|
|
269
250
|
assertToolReferenceHandles(args.edits);
|
|
270
251
|
const revised = candidate.snapshot.revise({
|
|
271
252
|
commands: [
|
|
@@ -282,13 +263,19 @@ function applyPlayCandidatePatch(candidate, reads, args) {
|
|
|
282
263
|
const receipt = formatToolRevisionReceipt(target.shortRef, revised.changes);
|
|
283
264
|
const previousSnapshot = candidate.snapshot;
|
|
284
265
|
acceptWorldStateRevision(candidate, revised);
|
|
285
|
-
candidate.suppliedBytes += suppliedBytes;
|
|
286
266
|
carryWriteAuthorizations(previousSnapshot, candidate, reads, revised.changes);
|
|
287
267
|
if (retainedAuthorization === null)
|
|
288
268
|
authorizeRead(reads, candidate.snapshot.id, ref, null);
|
|
289
269
|
else
|
|
290
|
-
for (const locator of retainedAuthorization)
|
|
291
|
-
|
|
270
|
+
for (const locator of retainedAuthorization) {
|
|
271
|
+
const selected = candidate.snapshot.query({
|
|
272
|
+
kind: "select_node",
|
|
273
|
+
document: { shortRef: ref },
|
|
274
|
+
locator,
|
|
275
|
+
});
|
|
276
|
+
if (selected.kind === "select_node" && selected.ok)
|
|
277
|
+
authorizeRead(reads, candidate.snapshot.id, ref, selected.node.locator);
|
|
278
|
+
}
|
|
292
279
|
return {
|
|
293
280
|
ok: true,
|
|
294
281
|
markdown: receipt,
|
|
@@ -301,26 +288,53 @@ function applyPlayCandidatePatch(candidate, reads, args) {
|
|
|
301
288
|
throw error;
|
|
302
289
|
}
|
|
303
290
|
}
|
|
304
|
-
function
|
|
305
|
-
if (authorization === null)
|
|
306
|
-
return
|
|
307
|
-
|
|
308
|
-
return false;
|
|
309
|
-
return edits.every((edit) => {
|
|
291
|
+
function authorizationsAfterEdits(authorization, edits) {
|
|
292
|
+
if (authorization === null || authorization === undefined)
|
|
293
|
+
return authorization;
|
|
294
|
+
for (const edit of edits) {
|
|
310
295
|
if (!record(edit) || edit.op === "set_metadata" || !record(edit.locator))
|
|
311
|
-
return
|
|
296
|
+
return undefined;
|
|
312
297
|
const requested = Array.isArray(edit.locator.yaml)
|
|
313
298
|
? { codec: "yaml", path: edit.locator.yaml }
|
|
314
299
|
: Array.isArray(edit.locator.markdown)
|
|
315
300
|
? { codec: "markdown", path: edit.locator.markdown }
|
|
316
301
|
: null;
|
|
317
|
-
|
|
318
|
-
authorization.some((allowed) => {
|
|
302
|
+
if (requested === null ||
|
|
303
|
+
!authorization.some((allowed) => {
|
|
319
304
|
const path = "yaml" in allowed ? allowed.yaml : allowed.markdown;
|
|
320
305
|
return (requested.codec in allowed &&
|
|
321
306
|
path.every((segment, index) => requested.path[index] === segment));
|
|
322
|
-
}))
|
|
323
|
-
|
|
307
|
+
}))
|
|
308
|
+
return undefined;
|
|
309
|
+
// A removed/replaced subtree or shifted array address is no longer proof
|
|
310
|
+
// about its old descendants. Check each edit before moving to the next so
|
|
311
|
+
// a batch cannot use a deleted array item to overwrite its unread successor.
|
|
312
|
+
authorization = authorization.filter((allowed) => {
|
|
313
|
+
if (!(requested.codec in allowed))
|
|
314
|
+
return true;
|
|
315
|
+
const path = "yaml" in allowed ? allowed.yaml : allowed.markdown;
|
|
316
|
+
const withinEdit = requested.path.every((segment, index) => path[index] === segment);
|
|
317
|
+
if (withinEdit &&
|
|
318
|
+
["remove", "remove_section", "rename_section"].includes(String(edit.op)))
|
|
319
|
+
return false;
|
|
320
|
+
if (withinEdit &&
|
|
321
|
+
path.length > requested.path.length &&
|
|
322
|
+
["replace", "replace_section"].includes(String(edit.op)))
|
|
323
|
+
return false;
|
|
324
|
+
if (edit.op === "remove" && requested.codec === "yaml") {
|
|
325
|
+
const index = requested.path.length - 1;
|
|
326
|
+
const removed = requested.path[index];
|
|
327
|
+
const current = path[index];
|
|
328
|
+
if (typeof removed === "number" &&
|
|
329
|
+
typeof current === "number" &&
|
|
330
|
+
current >= removed &&
|
|
331
|
+
requested.path.slice(0, -1).every((segment, i) => path[i] === segment))
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
return true;
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return authorization;
|
|
324
338
|
}
|
|
325
339
|
function formatToolRevisionReceipt(shortRef, changes) {
|
|
326
340
|
if (changes.length > 1)
|
|
@@ -329,6 +343,43 @@ function formatToolRevisionReceipt(shortRef, changes) {
|
|
|
329
343
|
? `@${shortRef} pending write`
|
|
330
344
|
: `@${shortRef} unchanged`;
|
|
331
345
|
}
|
|
346
|
+
function applyPlayCandidateRetirement(candidate, reads, args) {
|
|
347
|
+
if (!record(args) ||
|
|
348
|
+
!hasOnlyToolKeys(args, ["target", "retired"]) ||
|
|
349
|
+
typeof args.target !== "string" ||
|
|
350
|
+
typeof args.retired !== "boolean")
|
|
351
|
+
return toolFailure("world_retire requires exactly a target and boolean retired value.");
|
|
352
|
+
const ref = args.target.replace(/^@/u, "");
|
|
353
|
+
const target = documentDescriptorByRef(candidate.snapshot, ref);
|
|
354
|
+
if (target === null)
|
|
355
|
+
return toolFailure(`Target @${ref} does not exist.`);
|
|
356
|
+
if (reads.snapshotId !== candidate.snapshot.id ||
|
|
357
|
+
reads.documents.get(ref) !== null)
|
|
358
|
+
return toolFailure(`Read @${ref} exactly, or receive the complete document in bootstrap, before retiring or restoring it.`);
|
|
359
|
+
const revised = candidate.snapshot.revise({
|
|
360
|
+
commands: [
|
|
361
|
+
{
|
|
362
|
+
kind: "retire",
|
|
363
|
+
document: { documentId: target.documentId },
|
|
364
|
+
retired: args.retired,
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
});
|
|
368
|
+
if (!revised.ok || revised.snapshotStatus !== "usable")
|
|
369
|
+
return toolFailure(worldDocumentRevisionFailureMessage(revised.diagnostics), "candidate");
|
|
370
|
+
const changed = revised.changes.length > 0;
|
|
371
|
+
const previousSnapshot = candidate.snapshot;
|
|
372
|
+
acceptWorldStateRevision(candidate, revised);
|
|
373
|
+
carryWriteAuthorizations(previousSnapshot, candidate, reads, revised.changes);
|
|
374
|
+
authorizeRead(reads, candidate.snapshot.id, ref, null);
|
|
375
|
+
return {
|
|
376
|
+
ok: true,
|
|
377
|
+
markdown: changed
|
|
378
|
+
? `@${target.shortRef} pending ${args.retired ? "retirement" : "restoration"}`
|
|
379
|
+
: `@${target.shortRef} unchanged`,
|
|
380
|
+
candidateWrite: { shortRef: target.shortRef, changed },
|
|
381
|
+
};
|
|
382
|
+
}
|
|
332
383
|
function nextAvailableStatePath(candidate, parentDirectory, refHint, codec) {
|
|
333
384
|
const directory = parentDirectory === "" ? "" : `${parentDirectory}/`;
|
|
334
385
|
const extension = codec === "yaml" ? "yaml" : "md";
|
|
@@ -363,7 +414,7 @@ function assertToolReferenceHandles(value, seen = new WeakSet()) {
|
|
|
363
414
|
assertToolReferenceHandles(item, seen);
|
|
364
415
|
seen.delete(value);
|
|
365
416
|
}
|
|
366
|
-
function applyPlayCandidateCreate(candidate, authorizations, args) {
|
|
417
|
+
function applyPlayCandidateCreate(candidate, authorizations, args, declaredDirectories) {
|
|
367
418
|
try {
|
|
368
419
|
const parentDirectory = record(args) && typeof args.parent === "string"
|
|
369
420
|
? parseStateDirectoryHandle(args.parent)
|
|
@@ -380,11 +431,8 @@ function applyPlayCandidateCreate(candidate, authorizations, args) {
|
|
|
380
431
|
!args.aliases.every((alias) => typeof alias === "string") ||
|
|
381
432
|
typeof args.body !== "string")
|
|
382
433
|
throw new PlayDocumentToolFailure("world_create arguments are invalid");
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
throw new PlayDocumentToolFailure("The world_create body exceeds 64 KiB");
|
|
386
|
-
if (candidate.suppliedBytes + suppliedBytes > 256 * 1024)
|
|
387
|
-
throw new PlayDocumentToolFailure("New body content for this model operation exceeds 256 KiB in total");
|
|
434
|
+
if (!knownStateDirectory(candidate.snapshot, parentDirectory, declaredDirectories))
|
|
435
|
+
throw new PlayDocumentToolFailure("world_create parent is not a Runtime-known state directory; call state_list and use one of its directory handles");
|
|
388
436
|
const path = nextAvailableStatePath(candidate, parentDirectory, args.refHint, args.codec);
|
|
389
437
|
const command = args.codec === "yaml"
|
|
390
438
|
? {
|
|
@@ -423,7 +471,6 @@ function applyPlayCandidateCreate(candidate, authorizations, args) {
|
|
|
423
471
|
acceptWorldStateRevision(candidate, revised);
|
|
424
472
|
carryWriteAuthorizations(previousSnapshot, candidate, authorizations, revised.changes);
|
|
425
473
|
authorizeRead(authorizations, candidate.snapshot.id, created.shortRef, null);
|
|
426
|
-
candidate.suppliedBytes += suppliedBytes;
|
|
427
474
|
return {
|
|
428
475
|
ok: true,
|
|
429
476
|
markdown: `@${created.shortRef} pending write`,
|
|
@@ -453,9 +500,6 @@ function carryWriteAuthorizations(previousSnapshot, candidate, authorizations, c
|
|
|
453
500
|
authorizations.snapshotId = candidate.snapshot.id;
|
|
454
501
|
for (const ref of stale)
|
|
455
502
|
authorizations.documents.delete(ref);
|
|
456
|
-
for (const [readKey, progress] of authorizations.pendingReads)
|
|
457
|
-
if (stale.has(progress.shortRef))
|
|
458
|
-
authorizations.pendingReads.delete(readKey);
|
|
459
503
|
}
|
|
460
504
|
function playCandidateStateChanges(candidate) {
|
|
461
505
|
return fileNativeStateChanges(candidate);
|
|
@@ -464,7 +508,6 @@ function documentById(snapshot, id) {
|
|
|
464
508
|
const result = snapshot.query({
|
|
465
509
|
kind: "read_document",
|
|
466
510
|
document: { documentId: id },
|
|
467
|
-
maxBytes: 4,
|
|
468
511
|
});
|
|
469
512
|
return result.kind === "read_document" && result.ok ? result.document : null;
|
|
470
513
|
}
|
|
@@ -475,36 +518,6 @@ function toolFailure(message, failureKind = "protocol") {
|
|
|
475
518
|
failureKind,
|
|
476
519
|
};
|
|
477
520
|
}
|
|
478
|
-
function previewContextToolResult(snapshot, history, call) {
|
|
479
|
-
if (call.name === "state_list")
|
|
480
|
-
return executeStateList(snapshot, history, call.arguments);
|
|
481
|
-
if (call.name === "history_list")
|
|
482
|
-
return executeHistoryList(snapshot, history, call.arguments);
|
|
483
|
-
if (call.name === "context_list")
|
|
484
|
-
return executeContextList(snapshot, history, call.arguments);
|
|
485
|
-
if (call.name === "context_search")
|
|
486
|
-
return executeContextSearch(snapshot, history, call.arguments);
|
|
487
|
-
if (call.name === "context_read")
|
|
488
|
-
return executeContextRead(snapshot, history, call.arguments);
|
|
489
|
-
return null;
|
|
490
|
-
}
|
|
491
|
-
function contextToolRetryMaximum(call) {
|
|
492
|
-
if (!record(call.arguments))
|
|
493
|
-
return call.name === "context_read" ? 8_192 : 1;
|
|
494
|
-
if (call.name === "context_read")
|
|
495
|
-
return Number.isInteger(call.arguments.maxBytes) &&
|
|
496
|
-
Number(call.arguments.maxBytes) >= 4 &&
|
|
497
|
-
Number(call.arguments.maxBytes) <= 8_192
|
|
498
|
-
? Number(call.arguments.maxBytes)
|
|
499
|
-
: 8_192;
|
|
500
|
-
const defaultLimit = call.name === "context_search" ? 10 : 20;
|
|
501
|
-
const maximumLimit = call.name === "context_search" ? 50 : 100;
|
|
502
|
-
return Number.isInteger(call.arguments.limit) &&
|
|
503
|
-
Number(call.arguments.limit) >= 1 &&
|
|
504
|
-
Number(call.arguments.limit) <= maximumLimit
|
|
505
|
-
? Number(call.arguments.limit)
|
|
506
|
-
: defaultLimit;
|
|
507
|
-
}
|
|
508
521
|
function executeContextSearch(snapshot, history, args) {
|
|
509
522
|
if (!record(args) ||
|
|
510
523
|
!hasOnlyToolKeys(args, [
|
|
@@ -650,7 +663,26 @@ function parseStateDirectoryHandle(handle) {
|
|
|
650
663
|
return null;
|
|
651
664
|
}
|
|
652
665
|
}
|
|
653
|
-
function
|
|
666
|
+
function validStateDirectory(directory) {
|
|
667
|
+
if (directory.length === 0)
|
|
668
|
+
return false;
|
|
669
|
+
try {
|
|
670
|
+
return (parseStateDirectoryHandle(stateDirectoryHandle(directory)) === directory);
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function knownStateDirectory(snapshot, directory, declaredDirectories) {
|
|
677
|
+
if (directory === "")
|
|
678
|
+
return true;
|
|
679
|
+
const relativePrefix = `${directory}/`;
|
|
680
|
+
if (declaredDirectories.some((declared) => declared === directory || declared.startsWith(relativePrefix)))
|
|
681
|
+
return true;
|
|
682
|
+
const logicalPrefix = `${snapshot.logicalRoot}/${relativePrefix}`;
|
|
683
|
+
return snapshot.files.some(({ path }) => path.startsWith(logicalPrefix));
|
|
684
|
+
}
|
|
685
|
+
function executeStateList(snapshot, history, args, declaredDirectories) {
|
|
654
686
|
if (!record(args) ||
|
|
655
687
|
!hasOnlyToolKeys(args, ["parent", "cursor", "limit"]) ||
|
|
656
688
|
typeof args.parent !== "string" ||
|
|
@@ -661,7 +693,7 @@ function executeStateList(snapshot, history, args) {
|
|
|
661
693
|
ok: false,
|
|
662
694
|
markdown: "# Runtime argument error\n\nstate_list requires an @dir-* parent returned by Runtime and accepts only cursor and limit in addition; use history_list for committed history.",
|
|
663
695
|
};
|
|
664
|
-
return executeContextList(snapshot, history, { ...args, source: "state" });
|
|
696
|
+
return executeContextList(snapshot, history, { ...args, source: "state" }, declaredDirectories);
|
|
665
697
|
}
|
|
666
698
|
function executeHistoryList(snapshot, history, args) {
|
|
667
699
|
if (!record(args) ||
|
|
@@ -675,7 +707,7 @@ function executeHistoryList(snapshot, history, args) {
|
|
|
675
707
|
};
|
|
676
708
|
return executeContextList(snapshot, history, { ...args, source: "history" });
|
|
677
709
|
}
|
|
678
|
-
function executeContextList(snapshot, history, args) {
|
|
710
|
+
function executeContextList(snapshot, history, args, declaredDirectories = []) {
|
|
679
711
|
if (!record(args) || (args.source !== "state" && args.source !== "history"))
|
|
680
712
|
return {
|
|
681
713
|
ok: false,
|
|
@@ -709,9 +741,16 @@ function executeContextList(snapshot, history, args) {
|
|
|
709
741
|
const directory = parseStateDirectoryHandle(parent);
|
|
710
742
|
if (directory === null)
|
|
711
743
|
return unexpectedStateQueryResult();
|
|
744
|
+
if (!knownStateDirectory(snapshot, directory, declaredDirectories))
|
|
745
|
+
return {
|
|
746
|
+
ok: false,
|
|
747
|
+
markdown: "# Runtime argument error\n\nThe parent is not a Runtime-known state directory; call state_list and descend only through its returned directory handles.",
|
|
748
|
+
};
|
|
712
749
|
const result = snapshot.query({
|
|
713
750
|
kind: "catalog",
|
|
714
751
|
directory,
|
|
752
|
+
includeRetired: true,
|
|
753
|
+
declaredDirectories,
|
|
715
754
|
limit,
|
|
716
755
|
...(typeof args.cursor === "string" || args.cursor === null
|
|
717
756
|
? { cursor: args.cursor }
|
|
@@ -728,7 +767,7 @@ function executeContextList(snapshot, history, args) {
|
|
|
728
767
|
}
|
|
729
768
|
if (entry.document === undefined)
|
|
730
769
|
return "- Damaged document (currently not addressable)";
|
|
731
|
-
return `- Document @${entry.document.shortRef} · ${entry.document.title} · ${entry.document.codec.toUpperCase()} · ${entry.document.summary}${entry.status === "damaged" ? " · needs repair" : ""}`;
|
|
770
|
+
return `- Document @${entry.document.shortRef} · ${entry.document.title} · ${entry.document.codec.toUpperCase()} · ${entry.document.summary}${entry.document.retired ? " · retired" : ""}${entry.status === "damaged" ? " · needs repair" : ""}`;
|
|
732
771
|
});
|
|
733
772
|
return {
|
|
734
773
|
ok: true,
|
|
@@ -761,24 +800,22 @@ function executeContextList(snapshot, history, args) {
|
|
|
761
800
|
}
|
|
762
801
|
function executeContextRead(snapshot, history, args) {
|
|
763
802
|
if (!record(args) ||
|
|
803
|
+
// Frozen contexts created before complete exact reads still advertise
|
|
804
|
+
// cursor/maxBytes. Accept and ignore those obsolete arguments so an
|
|
805
|
+
// existing Provider continuation can cross the upgrade boundary.
|
|
764
806
|
!hasOnlyToolKeys(args, ["ref", "cursor", "maxBytes"]) ||
|
|
765
|
-
typeof args.ref !== "string"
|
|
766
|
-
(args.maxBytes !== undefined && typeof args.maxBytes !== "number") ||
|
|
767
|
-
!validOptionalCursor(args.cursor))
|
|
807
|
+
typeof args.ref !== "string")
|
|
768
808
|
return {
|
|
769
809
|
ok: false,
|
|
770
810
|
markdown: "# Runtime argument error\n\nA stable ref returned by list or search is required.",
|
|
771
811
|
};
|
|
772
|
-
const maxBytes = typeof args.maxBytes === "number" ? args.maxBytes : 8192;
|
|
773
|
-
if (!Number.isInteger(maxBytes) || maxBytes < 4 || maxBytes > 8192)
|
|
774
|
-
return {
|
|
775
|
-
ok: false,
|
|
776
|
-
markdown: "# Runtime argument error\n\nmaxBytes must be between 4 and 8192.",
|
|
777
|
-
};
|
|
778
812
|
const historyRefValue = args.ref.replace(/^@/u, "");
|
|
779
813
|
const historyEntry = history.find(({ path }) => historyRef(path) === historyRefValue);
|
|
780
814
|
if (historyEntry !== undefined)
|
|
781
|
-
return
|
|
815
|
+
return {
|
|
816
|
+
ok: true,
|
|
817
|
+
markdown: `# Exact read @${historyRefValue}\n\n${historyEntry.contents}\n\n---\nSource: @${historyRefValue}\nComplete: yes`,
|
|
818
|
+
};
|
|
782
819
|
const handle = parseStateReadHandle(args.ref);
|
|
783
820
|
if (handle === null)
|
|
784
821
|
return {
|
|
@@ -788,7 +825,6 @@ function executeContextRead(snapshot, history, args) {
|
|
|
788
825
|
const resolved = snapshot.query({
|
|
789
826
|
kind: "read_document",
|
|
790
827
|
document: { shortRef: handle.shortRef },
|
|
791
|
-
maxBytes: 4,
|
|
792
828
|
});
|
|
793
829
|
if (resolved.kind === "error")
|
|
794
830
|
return renderStateQueryFailure(resolved);
|
|
@@ -796,19 +832,17 @@ function executeContextRead(snapshot, history, args) {
|
|
|
796
832
|
return unexpectedStateQueryResult();
|
|
797
833
|
if (handle.path === null)
|
|
798
834
|
return resolved.codec === "markdown"
|
|
799
|
-
? readMarkdownDocument(snapshot, resolved.document
|
|
800
|
-
: readProjectedNode(snapshot, resolved.document, { yaml: [] }, args.ref,
|
|
835
|
+
? readMarkdownDocument(snapshot, resolved.document)
|
|
836
|
+
: readProjectedNode(snapshot, resolved.document, { yaml: [] }, args.ref, true);
|
|
801
837
|
const locator = resolved.codec === "yaml"
|
|
802
838
|
? { yaml: handle.path.map(parseYamlHandleSegment) }
|
|
803
839
|
: { markdown: handle.path };
|
|
804
|
-
return readProjectedNode(snapshot, resolved.document, locator, args.ref,
|
|
840
|
+
return readProjectedNode(snapshot, resolved.document, locator, args.ref, false);
|
|
805
841
|
}
|
|
806
|
-
function readMarkdownDocument(snapshot, document
|
|
842
|
+
function readMarkdownDocument(snapshot, document) {
|
|
807
843
|
const result = snapshot.query({
|
|
808
844
|
kind: "read_document",
|
|
809
845
|
document: { shortRef: document.shortRef },
|
|
810
|
-
maxBytes,
|
|
811
|
-
...(typeof cursor === "string" || cursor === null ? { cursor } : {}),
|
|
812
846
|
});
|
|
813
847
|
if (result.kind === "error")
|
|
814
848
|
return renderStateQueryFailure(result);
|
|
@@ -816,21 +850,15 @@ function readMarkdownDocument(snapshot, document, maxBytes, cursor) {
|
|
|
816
850
|
return unexpectedStateQueryResult();
|
|
817
851
|
return {
|
|
818
852
|
ok: true,
|
|
819
|
-
markdown: `# Exact read @${document.shortRef}\n\n${renderDocumentMetadata(document)}\n[Writable body starts; locators are relative to this point]\n${result.body.trimEnd()}\n[Writable body ${
|
|
853
|
+
markdown: `# Exact read @${document.shortRef}\n\n${renderDocumentMetadata(document)}\n[Writable body starts; locators are relative to this point]\n${result.body.trimEnd()}\n[Writable body ends]\n\n${renderPlayerViewBindings(snapshot, document.shortRef)}\n\n---\nScope: state · @${document.shortRef}\nComplete: yes`,
|
|
820
854
|
readAuthorization: {
|
|
821
855
|
snapshotId: snapshot.id,
|
|
822
|
-
readKey: readAuthorizationKey(document.shortRef, null, maxBytes),
|
|
823
856
|
shortRef: document.shortRef,
|
|
824
857
|
locator: null,
|
|
825
|
-
page: {
|
|
826
|
-
start: result.page.start,
|
|
827
|
-
end: result.page.end,
|
|
828
|
-
total: result.page.total,
|
|
829
|
-
},
|
|
830
858
|
},
|
|
831
859
|
};
|
|
832
860
|
}
|
|
833
|
-
function readProjectedNode(snapshot, document, locator, requestedHandle,
|
|
861
|
+
function readProjectedNode(snapshot, document, locator, requestedHandle, wholeDocument) {
|
|
834
862
|
const result = snapshot.query({
|
|
835
863
|
kind: "select_node",
|
|
836
864
|
document: { shortRef: document.shortRef },
|
|
@@ -849,15 +877,24 @@ function readProjectedNode(snapshot, document, locator, requestedHandle, maxByte
|
|
|
849
877
|
const text = wholeDocument
|
|
850
878
|
? `${renderDocumentMetadata(document)}\n[Writable body starts; locators are relative to this point]\n${body}\n[Writable body ends]\n`
|
|
851
879
|
: `# ${document.title} · ${renderLocator(result.node.locator)} [${requestedHandle}]\n\n${body}\n`;
|
|
852
|
-
return
|
|
880
|
+
return {
|
|
881
|
+
ok: true,
|
|
882
|
+
markdown: `# Exact read ${requestedHandle}\n\n${text.trimEnd()}\n\n${renderPlayerViewBindings(snapshot, document.shortRef)}\n\n---\nScope: state · ${requestedHandle}\nComplete: yes`,
|
|
883
|
+
readAuthorization: {
|
|
884
|
+
snapshotId: snapshot.id,
|
|
885
|
+
shortRef: document.shortRef,
|
|
886
|
+
locator: wholeDocument ? null : result.node.locator,
|
|
887
|
+
},
|
|
888
|
+
};
|
|
853
889
|
}
|
|
854
890
|
function renderDocumentMetadata(document) {
|
|
855
891
|
const metadata = stringify({
|
|
856
892
|
title: document.title,
|
|
857
893
|
summary: document.summary,
|
|
858
894
|
aliases: document.aliases,
|
|
895
|
+
...(document.retired ? { retired: true } : {}),
|
|
859
896
|
}, { indent: 2, lineWidth: 0 }).trimEnd();
|
|
860
|
-
return `[Document metadata: not body content;
|
|
897
|
+
return `[Document metadata: not body content; set_metadata updates one or more fields and preserves omitted fields; retired is changed only by world_retire]\n${metadata}`;
|
|
861
898
|
}
|
|
862
899
|
function toolWorldDocumentValue(value) {
|
|
863
900
|
if (Array.isArray(value))
|
|
@@ -873,98 +910,6 @@ function toolWorldDocumentValue(value) {
|
|
|
873
910
|
toolWorldDocumentValue(child),
|
|
874
911
|
]));
|
|
875
912
|
}
|
|
876
|
-
function pagedStateProjection(snapshot, handle, text, maxBytes, cursor, shortRef, locator) {
|
|
877
|
-
const scope = JSON.stringify({
|
|
878
|
-
kind: "state_projected_read",
|
|
879
|
-
snapshotId: snapshot.id,
|
|
880
|
-
handle,
|
|
881
|
-
maxBytes,
|
|
882
|
-
});
|
|
883
|
-
const offset = parseStateProjectionCursor(snapshot, cursor, scope);
|
|
884
|
-
const bytes = Buffer.from(text, "utf8");
|
|
885
|
-
if (offset === null || offset > bytes.length)
|
|
886
|
-
return {
|
|
887
|
-
ok: false,
|
|
888
|
-
markdown: "# Runtime argument error\n\nThe cursor does not match the current snapshot or complete query criteria.",
|
|
889
|
-
};
|
|
890
|
-
const end = safeUtf8ReadEnd(bytes, offset, maxBytes);
|
|
891
|
-
const complete = end === bytes.length;
|
|
892
|
-
const page = bytes.subarray(offset, end).toString("utf8");
|
|
893
|
-
return {
|
|
894
|
-
ok: true,
|
|
895
|
-
markdown: `# Exact read ${handle}\n\n${page}\n\n---\nScope: state · ${handle}\nThis page: ${offset}..${end} / ${bytes.length} bytes\nComplete: ${complete ? "yes" : "no"}${complete ? "" : `\nNext-page cursor: ${stateProjectionCursorFor(snapshot, scope, end)}`}`,
|
|
896
|
-
readAuthorization: {
|
|
897
|
-
snapshotId: snapshot.id,
|
|
898
|
-
readKey: readAuthorizationKey(shortRef, locator, maxBytes),
|
|
899
|
-
shortRef,
|
|
900
|
-
locator,
|
|
901
|
-
page: { start: offset, end, total: bytes.length },
|
|
902
|
-
},
|
|
903
|
-
};
|
|
904
|
-
}
|
|
905
|
-
function readAuthorizationKey(shortRef, locator, maxBytes) {
|
|
906
|
-
return JSON.stringify({ shortRef, locator, maxBytes });
|
|
907
|
-
}
|
|
908
|
-
const stateProjectionCursorSecrets = new WeakMap();
|
|
909
|
-
function stateProjectionCursorFor(snapshot, scope, offset) {
|
|
910
|
-
const payload = Buffer.from(JSON.stringify({ offset }), "utf8").toString("base64url");
|
|
911
|
-
const signature = createHmac("sha256", stateProjectionCursorSecret(snapshot))
|
|
912
|
-
.update(snapshot.id)
|
|
913
|
-
.update("\0")
|
|
914
|
-
.update(scope)
|
|
915
|
-
.update("\0")
|
|
916
|
-
.update(payload)
|
|
917
|
-
.digest("base64url");
|
|
918
|
-
return `${payload}.${signature}`;
|
|
919
|
-
}
|
|
920
|
-
function parseStateProjectionCursor(snapshot, cursor, scope) {
|
|
921
|
-
if (cursor === undefined || cursor === null)
|
|
922
|
-
return 0;
|
|
923
|
-
if (typeof cursor !== "string")
|
|
924
|
-
return null;
|
|
925
|
-
const parts = cursor.split(".");
|
|
926
|
-
if (parts.length !== 2)
|
|
927
|
-
return null;
|
|
928
|
-
const [payload, suppliedSignature] = parts;
|
|
929
|
-
if (payload === undefined || suppliedSignature === undefined)
|
|
930
|
-
return null;
|
|
931
|
-
try {
|
|
932
|
-
const supplied = Buffer.from(suppliedSignature, "base64url");
|
|
933
|
-
if (supplied.toString("base64url") !== suppliedSignature)
|
|
934
|
-
return null;
|
|
935
|
-
const expected = createHmac("sha256", stateProjectionCursorSecret(snapshot))
|
|
936
|
-
.update(snapshot.id)
|
|
937
|
-
.update("\0")
|
|
938
|
-
.update(scope)
|
|
939
|
-
.update("\0")
|
|
940
|
-
.update(payload)
|
|
941
|
-
.digest();
|
|
942
|
-
if (supplied.length !== expected.length ||
|
|
943
|
-
!timingSafeEqual(supplied, expected))
|
|
944
|
-
return null;
|
|
945
|
-
const decoded = Buffer.from(payload, "base64url");
|
|
946
|
-
if (decoded.toString("base64url") !== payload)
|
|
947
|
-
return null;
|
|
948
|
-
const value = JSON.parse(decoded.toString("utf8"));
|
|
949
|
-
return record(value) &&
|
|
950
|
-
Object.keys(value).length === 1 &&
|
|
951
|
-
Number.isSafeInteger(value.offset) &&
|
|
952
|
-
Number(value.offset) >= 0
|
|
953
|
-
? Number(value.offset)
|
|
954
|
-
: null;
|
|
955
|
-
}
|
|
956
|
-
catch {
|
|
957
|
-
return null;
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
function stateProjectionCursorSecret(snapshot) {
|
|
961
|
-
const existing = stateProjectionCursorSecrets.get(snapshot);
|
|
962
|
-
if (existing !== undefined)
|
|
963
|
-
return existing;
|
|
964
|
-
const created = randomBytes(32);
|
|
965
|
-
stateProjectionCursorSecrets.set(snapshot, created);
|
|
966
|
-
return created;
|
|
967
|
-
}
|
|
968
913
|
function parseStateReadHandle(value) {
|
|
969
914
|
const match = /^@([a-z][a-z0-9-]*)(?:#\/(.+))?$/u.exec(value);
|
|
970
915
|
if (match === null)
|
|
@@ -1026,33 +971,6 @@ function snippet(text, query) {
|
|
|
1026
971
|
function historyRef(path) {
|
|
1027
972
|
return `history-message-${path.replace(/\.md$/u, "")}`;
|
|
1028
973
|
}
|
|
1029
|
-
function pagedRead(ref, text, maxBytes, cursor) {
|
|
1030
|
-
const scope = JSON.stringify({
|
|
1031
|
-
kind: "read",
|
|
1032
|
-
ref,
|
|
1033
|
-
sha256: createHash("sha256").update(text).digest("hex"),
|
|
1034
|
-
maxBytes,
|
|
1035
|
-
});
|
|
1036
|
-
const offset = parseCursor(cursor, scope);
|
|
1037
|
-
if (offset === null)
|
|
1038
|
-
return {
|
|
1039
|
-
ok: false,
|
|
1040
|
-
markdown: "# Runtime argument error\n\nThe cursor does not match the current read.",
|
|
1041
|
-
};
|
|
1042
|
-
const bytes = Buffer.from(text, "utf8");
|
|
1043
|
-
const end = safeUtf8ReadEnd(bytes, offset, maxBytes);
|
|
1044
|
-
const page = bytes.subarray(offset, end).toString("utf8");
|
|
1045
|
-
return {
|
|
1046
|
-
ok: true,
|
|
1047
|
-
markdown: `# Exact read @${ref}\n\n${page}\n\n---\nSource: @${ref}\nThis page: ${offset}..${end} bytes\nComplete: ${end === bytes.length ? "yes" : "no"}\n${end === bytes.length ? "" : `Next-page cursor: ${cursorFor(scope, end)}`}`,
|
|
1048
|
-
};
|
|
1049
|
-
}
|
|
1050
|
-
function safeUtf8ReadEnd(bytes, offset, maxBytes) {
|
|
1051
|
-
let end = Math.min(bytes.length, offset + maxBytes);
|
|
1052
|
-
while (end > offset && (bytes[end] ?? 0) >= 0x80 && (bytes[end] ?? 0) < 0xc0)
|
|
1053
|
-
end -= 1;
|
|
1054
|
-
return end;
|
|
1055
|
-
}
|
|
1056
974
|
function cursorFor(scope, offset) {
|
|
1057
975
|
return Buffer.from(JSON.stringify({
|
|
1058
976
|
scope: createHash("sha256").update(scope).digest("hex"),
|