narraeon 0.4.0 → 0.5.1
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 +186 -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 +445 -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 +13 -0
- 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-DG85hpWC.css +1 -0
- package/dist/web/assets/index-DfxvCjb6.js +1207 -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-D8YKfmNG.js +0 -1192
- package/dist/web/assets/index-Xa0s35CJ.css +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PackageScriptPermissions } from "../extension/PackageScriptPermissions.js";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { readFileSync, unlinkSync } from "node:fs";
|
|
3
4
|
import { link, open, mkdir, readFile, readdir, rename, rm, stat, unlink, } from "node:fs/promises";
|
|
@@ -54,18 +55,20 @@ export class CurrentTreeContentLibrary {
|
|
|
54
55
|
this.#beforePackagePublish = options.beforePackagePublish;
|
|
55
56
|
this.#beforeCurrentTreeReplace = options.beforeCurrentTreeReplace;
|
|
56
57
|
}
|
|
57
|
-
createPackage(
|
|
58
|
+
createPackage(input) {
|
|
58
59
|
return this.#mutate(async () => {
|
|
59
|
-
const
|
|
60
|
-
const
|
|
60
|
+
const title = normalizePackageTitle(input.title);
|
|
61
|
+
const normalized = normalizeCurrentContentTreeFiles(input.files ?? [], this.#limits);
|
|
62
|
+
const package_ = await this.#publishNewPackage(normalized, title);
|
|
61
63
|
return packageSummary(package_);
|
|
62
64
|
});
|
|
63
65
|
}
|
|
64
|
-
importPackage(
|
|
66
|
+
importPackage(input, options = {}) {
|
|
65
67
|
return this.#mutate(async () => {
|
|
66
68
|
options.signal?.throwIfAborted();
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
+
const title = normalizePackageTitle(input.title);
|
|
70
|
+
const normalized = normalizeCurrentContentTreeFiles(input.files, this.#limits);
|
|
71
|
+
return this.#publishNewPackage(normalized, title, options.signal);
|
|
69
72
|
});
|
|
70
73
|
}
|
|
71
74
|
copyPackage(localId, options = {}) {
|
|
@@ -73,7 +76,7 @@ export class CurrentTreeContentLibrary {
|
|
|
73
76
|
options.signal?.throwIfAborted();
|
|
74
77
|
const source = await this.readPackage(localId);
|
|
75
78
|
options.signal?.throwIfAborted();
|
|
76
|
-
return this.#publishNewPackage(source.files, options.signal);
|
|
79
|
+
return this.#publishNewPackage(source.files, source.title, options.signal);
|
|
77
80
|
});
|
|
78
81
|
}
|
|
79
82
|
deletePackage(localId, options = {}) {
|
|
@@ -127,7 +130,38 @@ export class CurrentTreeContentLibrary {
|
|
|
127
130
|
}
|
|
128
131
|
return packages;
|
|
129
132
|
}
|
|
133
|
+
async readScriptGrants(localId) {
|
|
134
|
+
await this.readPackage(localId);
|
|
135
|
+
return PackageScriptPermissions.read(this.#packageRoot(localId));
|
|
136
|
+
}
|
|
137
|
+
async scriptPermissions(localId, enabled) {
|
|
138
|
+
const lease = await this.acquireOperationLease(localId, { wait: true });
|
|
139
|
+
try {
|
|
140
|
+
const root = this.#packageRoot(localId);
|
|
141
|
+
if (enabled !== undefined)
|
|
142
|
+
await PackageScriptPermissions.write(root, enabled
|
|
143
|
+
? PackageScriptPermissions.currentGrants(lease.package.files)
|
|
144
|
+
: []);
|
|
145
|
+
return PackageScriptPermissions.status(lease.package.files, await PackageScriptPermissions.read(root));
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
lease.release();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
130
151
|
async readPackage(localId) {
|
|
152
|
+
const snapshot = await this.#readPackageSnapshot(localId);
|
|
153
|
+
if (snapshot.metadata.schemaVersion === 2)
|
|
154
|
+
return snapshot.package;
|
|
155
|
+
const lease = await this.acquireOperationLease(localId, { wait: true });
|
|
156
|
+
try {
|
|
157
|
+
await ensureStoredPackageTitle(this.#packageRoot(localId), localId, lease.package.title);
|
|
158
|
+
return structuredClone(lease.package);
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
lease.release();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async #readPackageSnapshot(localId) {
|
|
131
165
|
const packageRoot = this.#packageRoot(localId);
|
|
132
166
|
const metadata = await readJsonFile(join(packageRoot, localMetadataFileName), () => new CurrentTreeContentPackageNotFoundError(), "The local content-package envelope is damaged");
|
|
133
167
|
if (!isStoredMetadata(metadata) || metadata.localId !== localId) {
|
|
@@ -146,18 +180,20 @@ export class CurrentTreeContentLibrary {
|
|
|
146
180
|
cause: error,
|
|
147
181
|
});
|
|
148
182
|
}
|
|
149
|
-
|
|
183
|
+
const inspection = inspectContentPackageCurrentTree(files);
|
|
184
|
+
return {
|
|
185
|
+
metadata,
|
|
186
|
+
package: detail(localId, effectivePackageTitle(metadata, inspection), files, inspection),
|
|
187
|
+
};
|
|
150
188
|
}
|
|
151
189
|
/**
|
|
152
|
-
*
|
|
190
|
+
* Title a package in the local library. The title lives in the package's own
|
|
153
191
|
* local shell, not in its content, so renaming never touches a document the
|
|
154
192
|
* runtime reads during play.
|
|
155
193
|
*/
|
|
156
|
-
renamePackage(localId,
|
|
194
|
+
renamePackage(localId, title, options = {}) {
|
|
157
195
|
return this.#mutate(async () => {
|
|
158
|
-
const
|
|
159
|
-
if (!validPackageName(trimmed))
|
|
160
|
-
throw new CurrentTreeContentLibraryError("A content-package name must contain 1 to 160 characters and no line breaks");
|
|
196
|
+
const normalizedTitle = normalizePackageTitle(title);
|
|
161
197
|
const lease = await this.acquireOperationLease(localId, {
|
|
162
198
|
wait: options.waitForLease !== false,
|
|
163
199
|
});
|
|
@@ -167,20 +203,11 @@ export class CurrentTreeContentLibrary {
|
|
|
167
203
|
const metadata = await readJsonFile(join(packageRoot, localMetadataFileName), () => new CurrentTreeContentPackageNotFoundError(), "The local content-package envelope is damaged");
|
|
168
204
|
if (!isStoredMetadata(metadata) || metadata.localId !== localId)
|
|
169
205
|
throw new CurrentTreeContentLibraryError("The local content-package envelope is damaged");
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
name: trimmed,
|
|
176
|
-
}));
|
|
177
|
-
await rename(temporaryPath, join(packageRoot, localMetadataFileName));
|
|
178
|
-
await syncDirectory(packageRoot);
|
|
179
|
-
}
|
|
180
|
-
catch (error) {
|
|
181
|
-
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
182
|
-
throw error;
|
|
183
|
-
}
|
|
206
|
+
await writeStoredLocalMetadata(packageRoot, {
|
|
207
|
+
schemaVersion: 2,
|
|
208
|
+
localId,
|
|
209
|
+
title: normalizedTitle,
|
|
210
|
+
});
|
|
184
211
|
return packageSummary(await this.readPackage(localId));
|
|
185
212
|
}
|
|
186
213
|
finally {
|
|
@@ -246,7 +273,7 @@ export class CurrentTreeContentLibrary {
|
|
|
246
273
|
}
|
|
247
274
|
let released = false;
|
|
248
275
|
try {
|
|
249
|
-
const package_ = await this
|
|
276
|
+
const package_ = (await this.#readPackageSnapshot(localId)).package;
|
|
250
277
|
return {
|
|
251
278
|
package: structuredClone(package_),
|
|
252
279
|
replace: async (files, options = {}) => {
|
|
@@ -254,7 +281,7 @@ export class CurrentTreeContentLibrary {
|
|
|
254
281
|
throw new CurrentTreeContentPackageBusyError();
|
|
255
282
|
options.signal?.throwIfAborted();
|
|
256
283
|
const normalized = normalizeCurrentContentTreeFiles(files, this.#limits);
|
|
257
|
-
return this.#replaceNormalizedCurrentTree(localId, normalized, options.signal);
|
|
284
|
+
return this.#replaceNormalizedCurrentTree(localId, package_.title, normalized, options.signal);
|
|
258
285
|
},
|
|
259
286
|
release: () => {
|
|
260
287
|
if (released)
|
|
@@ -269,12 +296,13 @@ export class CurrentTreeContentLibrary {
|
|
|
269
296
|
throw error;
|
|
270
297
|
}
|
|
271
298
|
}
|
|
272
|
-
async #replaceNormalizedCurrentTree(localId, normalized, signal) {
|
|
299
|
+
async #replaceNormalizedCurrentTree(localId, title, normalized, signal) {
|
|
273
300
|
const packageRoot = this.#packageRoot(localId);
|
|
274
301
|
const temporaryPath = join(packageRoot, `.current-tree-${randomUUID()}.tmp`);
|
|
275
302
|
const previousPath = join(packageRoot, currentTreePreviousFileName);
|
|
276
303
|
try {
|
|
277
304
|
signal?.throwIfAborted();
|
|
305
|
+
await ensureStoredPackageTitle(packageRoot, localId, title);
|
|
278
306
|
await writeDurableNewFile(temporaryPath, serialize({
|
|
279
307
|
schemaVersion: 1,
|
|
280
308
|
files: normalized,
|
|
@@ -319,9 +347,9 @@ export class CurrentTreeContentLibrary {
|
|
|
319
347
|
? error.message
|
|
320
348
|
: "Unable to replace the content package's current tree atomically", { cause: error });
|
|
321
349
|
}
|
|
322
|
-
return detail(localId, normalized, inspectContentPackageCurrentTree(normalized));
|
|
350
|
+
return detail(localId, title, normalized, inspectContentPackageCurrentTree(normalized));
|
|
323
351
|
}
|
|
324
|
-
async #publishNewPackage(normalized, signal) {
|
|
352
|
+
async #publishNewPackage(normalized, title, signal) {
|
|
325
353
|
const localId = `package-${randomUUID()}`;
|
|
326
354
|
const stagingPath = join(this.#stagingRoot, localId);
|
|
327
355
|
const finalPath = join(this.#packagesRoot, localId);
|
|
@@ -333,8 +361,9 @@ export class CurrentTreeContentLibrary {
|
|
|
333
361
|
signal?.throwIfAborted();
|
|
334
362
|
await mkdir(stagingPath, { mode: 0o700 });
|
|
335
363
|
await writeDurableNewFile(join(stagingPath, localMetadataFileName), serialize({
|
|
336
|
-
schemaVersion:
|
|
364
|
+
schemaVersion: 2,
|
|
337
365
|
localId,
|
|
366
|
+
title,
|
|
338
367
|
}));
|
|
339
368
|
await writeDurableNewFile(join(stagingPath, currentTreeFileName), serialize({
|
|
340
369
|
schemaVersion: 1,
|
|
@@ -357,7 +386,7 @@ export class CurrentTreeContentLibrary {
|
|
|
357
386
|
cause: error,
|
|
358
387
|
});
|
|
359
388
|
}
|
|
360
|
-
return detail(localId, normalized, inspectContentPackageCurrentTree(normalized));
|
|
389
|
+
return detail(localId, title, normalized, inspectContentPackageCurrentTree(normalized));
|
|
361
390
|
}
|
|
362
391
|
#packageRoot(localId) {
|
|
363
392
|
if (!localIdPattern.test(localId)) {
|
|
@@ -445,9 +474,9 @@ export class CurrentTreeContentLibrary {
|
|
|
445
474
|
}
|
|
446
475
|
}
|
|
447
476
|
}
|
|
448
|
-
function detail(localId, files, inspection
|
|
477
|
+
function detail(localId, title, files, inspection) {
|
|
449
478
|
return {
|
|
450
|
-
...summary(localId,
|
|
479
|
+
...summary(localId, title, inspection),
|
|
451
480
|
files: files.map((file) => ({ ...file })),
|
|
452
481
|
documents: structuredClone(inspection.documents),
|
|
453
482
|
issues: structuredClone(inspection.issues),
|
|
@@ -455,12 +484,10 @@ function detail(localId, files, inspection, name) {
|
|
|
455
484
|
control: structuredClone(inspection.control),
|
|
456
485
|
};
|
|
457
486
|
}
|
|
458
|
-
function summary(localId,
|
|
487
|
+
function summary(localId, title, inspection) {
|
|
459
488
|
return {
|
|
460
489
|
localId,
|
|
461
|
-
|
|
462
|
-
// the package currently contains.
|
|
463
|
-
displayName: name ?? inspection.displayName,
|
|
490
|
+
title,
|
|
464
491
|
status: inspection.status,
|
|
465
492
|
documentCount: inspection.documents.length,
|
|
466
493
|
issueCount: inspection.issues.length,
|
|
@@ -469,7 +496,7 @@ function summary(localId, inspection, name) {
|
|
|
469
496
|
function packageSummary(package_) {
|
|
470
497
|
return {
|
|
471
498
|
localId: package_.localId,
|
|
472
|
-
|
|
499
|
+
title: package_.title,
|
|
473
500
|
status: package_.status,
|
|
474
501
|
documentCount: package_.documentCount,
|
|
475
502
|
issueCount: package_.issueCount,
|
|
@@ -575,6 +602,33 @@ function normalizeLimits(input) {
|
|
|
575
602
|
}
|
|
576
603
|
return limits;
|
|
577
604
|
}
|
|
605
|
+
async function ensureStoredPackageTitle(packageRoot, localId, title) {
|
|
606
|
+
const metadata = await readJsonFile(join(packageRoot, localMetadataFileName), () => new CurrentTreeContentPackageNotFoundError(), "The local content-package envelope is damaged");
|
|
607
|
+
if (!isStoredMetadata(metadata) || metadata.localId !== localId)
|
|
608
|
+
throw new CurrentTreeContentLibraryError("The local content-package envelope is damaged");
|
|
609
|
+
if (metadata.schemaVersion === 2) {
|
|
610
|
+
if (metadata.title !== title)
|
|
611
|
+
throw new CurrentTreeContentLibraryError("The content-package title changed while its current tree was locked");
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
await writeStoredLocalMetadata(packageRoot, {
|
|
615
|
+
schemaVersion: 2,
|
|
616
|
+
localId,
|
|
617
|
+
title,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
async function writeStoredLocalMetadata(packageRoot, metadata) {
|
|
621
|
+
const temporaryPath = join(packageRoot, `.local-${randomUUID()}.tmp`);
|
|
622
|
+
try {
|
|
623
|
+
await writeDurableNewFile(temporaryPath, serialize(metadata));
|
|
624
|
+
await rename(temporaryPath, join(packageRoot, localMetadataFileName));
|
|
625
|
+
await syncDirectory(packageRoot);
|
|
626
|
+
}
|
|
627
|
+
catch (error) {
|
|
628
|
+
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
629
|
+
throw error;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
578
632
|
async function writeDurableNewFile(path, contents) {
|
|
579
633
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
580
634
|
const handle = await open(path, "wx", 0o600);
|
|
@@ -607,17 +661,45 @@ async function readJsonFile(path, missing, invalidMessage) {
|
|
|
607
661
|
}
|
|
608
662
|
}
|
|
609
663
|
function isStoredMetadata(value) {
|
|
664
|
+
return isStoredLocalMetadata(value) || isLegacyStoredLocalMetadata(value);
|
|
665
|
+
}
|
|
666
|
+
function isStoredLocalMetadata(value) {
|
|
667
|
+
return (isPlainObject(value) &&
|
|
668
|
+
Object.keys(value).length === 3 &&
|
|
669
|
+
value.schemaVersion === 2 &&
|
|
670
|
+
typeof value.localId === "string" &&
|
|
671
|
+
localIdPattern.test(value.localId) &&
|
|
672
|
+
typeof value.title === "string" &&
|
|
673
|
+
validPackageTitle(value.title));
|
|
674
|
+
}
|
|
675
|
+
function isLegacyStoredLocalMetadata(value) {
|
|
610
676
|
return (isPlainObject(value) &&
|
|
611
677
|
Object.keys(value).length === (value.name === undefined ? 2 : 3) &&
|
|
612
678
|
value.schemaVersion === 1 &&
|
|
613
679
|
typeof value.localId === "string" &&
|
|
614
680
|
localIdPattern.test(value.localId) &&
|
|
615
681
|
(value.name === undefined ||
|
|
616
|
-
(typeof value.name === "string" &&
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
682
|
+
(typeof value.name === "string" && validPackageTitle(value.name))));
|
|
683
|
+
}
|
|
684
|
+
function effectivePackageTitle(metadata, inspection) {
|
|
685
|
+
if (metadata.schemaVersion === 2)
|
|
686
|
+
return metadata.title;
|
|
687
|
+
return metadata.name ?? legacyContentDerivedTitle(inspection);
|
|
688
|
+
}
|
|
689
|
+
function legacyContentDerivedTitle(inspection) {
|
|
690
|
+
return (inspection.documents.find(({ id }) => id === "situation.current")?.title ??
|
|
691
|
+
inspection.documents[0]?.title ??
|
|
692
|
+
"Untitled content package");
|
|
693
|
+
}
|
|
694
|
+
const packageTitlePattern = /^[^\r\n]{1,160}$/u;
|
|
695
|
+
function validPackageTitle(title) {
|
|
696
|
+
return packageTitlePattern.test(title);
|
|
697
|
+
}
|
|
698
|
+
function normalizePackageTitle(title) {
|
|
699
|
+
const normalized = title.trim();
|
|
700
|
+
if (!validPackageTitle(normalized))
|
|
701
|
+
throw new CurrentTreeContentLibraryError("A content-package title must contain 1 to 160 characters and no line breaks");
|
|
702
|
+
return normalized;
|
|
621
703
|
}
|
|
622
704
|
function isStoredCurrentTree(value) {
|
|
623
705
|
return (isPlainObject(value) &&
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { readPackageFollowups } from "./PackageFollowups.js";
|
|
1
2
|
import { parseDocument } from "yaml";
|
|
2
3
|
import { WorldDocumentStore, } from "../world/WorldDocumentStore.js";
|
|
3
|
-
const maxOpeningBytes = 64 * 1024;
|
|
4
4
|
export function isContentPackageWorldDocumentPath(path) {
|
|
5
5
|
return path.startsWith("world/");
|
|
6
6
|
}
|
|
@@ -35,11 +35,10 @@ export function inspectContentPackageCurrentTree(files, options = {}) {
|
|
|
35
35
|
const openingFile = openingFiles[0];
|
|
36
36
|
if (openingFile.encoding !== undefined ||
|
|
37
37
|
openingFile.contents.trim().length === 0 ||
|
|
38
|
-
Buffer.byteLength(openingFile.contents, "utf8") > maxOpeningBytes ||
|
|
39
38
|
Buffer.from(openingFile.contents, "utf8").toString("utf8") !==
|
|
40
39
|
openingFile.contents) {
|
|
41
40
|
opening = "invalid";
|
|
42
|
-
issue(issues, "invalid_opening", "opening.md", "The introduction must be non-empty, well-formed UTF-8 text
|
|
41
|
+
issue(issues, "invalid_opening", "opening.md", "The introduction must be non-empty, well-formed UTF-8 text");
|
|
43
42
|
}
|
|
44
43
|
else
|
|
45
44
|
opening = "valid";
|
|
@@ -66,9 +65,16 @@ export function inspectContentPackageCurrentTree(files, options = {}) {
|
|
|
66
65
|
const frame = parseRestrictedYaml("control/frame.yaml", frameSource, issues);
|
|
67
66
|
if (isRecord(frame) &&
|
|
68
67
|
frame.format === "narraeon.world-frame/v1" &&
|
|
69
|
-
Object.keys(frame).every((key) => [
|
|
68
|
+
Object.keys(frame).every((key) => [
|
|
69
|
+
"format",
|
|
70
|
+
"bindings",
|
|
71
|
+
"instructions",
|
|
72
|
+
"context",
|
|
73
|
+
"maintenance",
|
|
74
|
+
].includes(key)) &&
|
|
70
75
|
isRecord(frame.bindings) &&
|
|
71
|
-
Object.keys(frame.bindings).length === 1
|
|
76
|
+
Object.keys(frame.bindings).length === 1 &&
|
|
77
|
+
validMaintenance(frame)) {
|
|
72
78
|
worldFrame = "valid";
|
|
73
79
|
const binding = isRecord(frame.bindings)
|
|
74
80
|
? frame.bindings.currentSituation
|
|
@@ -110,12 +116,14 @@ export function inspectContentPackageCurrentTree(files, options = {}) {
|
|
|
110
116
|
issue(issues, "invalid_player_view", "control/player-views.yaml", `${formatProblems.length - maxReportedPlayerViewProblems} more player-view issues were omitted`);
|
|
111
117
|
}
|
|
112
118
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
try {
|
|
120
|
+
readPackageFollowups(snapshotFiles);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
issue(issues, "invalid_package_followups", "control/followups.yaml", error instanceof Error ? error.message : "Invalid package followups");
|
|
124
|
+
}
|
|
116
125
|
return {
|
|
117
126
|
status: issues.length === 0 ? "usable" : "needs_repair",
|
|
118
|
-
displayName,
|
|
119
127
|
documents: documents.sort((a, b) => a.path.localeCompare(b.path)),
|
|
120
128
|
issues,
|
|
121
129
|
opening,
|
|
@@ -123,6 +131,15 @@ export function inspectContentPackageCurrentTree(files, options = {}) {
|
|
|
123
131
|
worldDocumentSnapshot,
|
|
124
132
|
};
|
|
125
133
|
}
|
|
134
|
+
function validMaintenance(frame) {
|
|
135
|
+
try {
|
|
136
|
+
worldMaintenanceSettings(frame);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
126
143
|
function worldDocumentSnapshotDocuments(snapshot) {
|
|
127
144
|
const documents = [];
|
|
128
145
|
const pendingDirectories = [""];
|
|
@@ -387,7 +404,7 @@ function validateFrameContext(value, index, snapshot, issues) {
|
|
|
387
404
|
}
|
|
388
405
|
// Naming the offending key matters: reporting the kind reads as "this slot
|
|
389
406
|
// does not exist" and sends the author off deleting a valid slot.
|
|
390
|
-
const unknownKeys = Object.keys(entry.slot).filter((key) => !allowed.includes(key));
|
|
407
|
+
const unknownKeys = Object.keys(entry.slot).filter((key) => !allowed.includes(key) && key !== "id" && key !== "advisoryBytes");
|
|
391
408
|
if (unknownKeys.length > 0) {
|
|
392
409
|
issue(issues, "invalid_world_frame", "control/frame.yaml", `Slot ${entry.slot.kind} does not accept parameters ${unknownKeys.join(", ")}; it accepts only ${allowed.join(", ")}`);
|
|
393
410
|
continue;
|
|
@@ -510,3 +527,4 @@ function issue(issues, code, path, message, documentId) {
|
|
|
510
527
|
function isRecord(value) {
|
|
511
528
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
512
529
|
}
|
|
530
|
+
import { worldMaintenanceSettings } from "../prompt/WorldMaintenanceReport.js";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { isAlias, isNode, parseDocument, visit } from "yaml";
|
|
2
|
+
import { parseFollowups, parsePlayPresetRegexAsset, } from "../play/FileNativePlayPresetStore.js";
|
|
3
|
+
/** The world owns the copied control tree; source-package metadata is never consulted. */
|
|
4
|
+
export function readPackageFollowups(files) {
|
|
5
|
+
const source = files.find(({ path }) => path === "control/followups.yaml");
|
|
6
|
+
const resources = Object.fromEntries(files
|
|
7
|
+
.filter(({ path, encoding }) => path.startsWith("control/") && encoding === undefined)
|
|
8
|
+
.map(({ path, contents }) => [path.slice("control/".length), contents]));
|
|
9
|
+
if (source === undefined)
|
|
10
|
+
return { followups: [], files: resources };
|
|
11
|
+
if (source.encoding !== undefined)
|
|
12
|
+
throw new Error("Package followups must be UTF-8 YAML");
|
|
13
|
+
const document = parseDocument(source.contents, { uniqueKeys: true });
|
|
14
|
+
let unsafe = false;
|
|
15
|
+
visit(document, (_, node) => {
|
|
16
|
+
if (isAlias(node) ||
|
|
17
|
+
(isNode(node) &&
|
|
18
|
+
(node.tag !== undefined || ("anchor" in node && Boolean(node.anchor)))))
|
|
19
|
+
unsafe = true;
|
|
20
|
+
});
|
|
21
|
+
if (unsafe || document.errors.length > 0 || document.warnings.length > 0)
|
|
22
|
+
throw new Error("Invalid package followups YAML: aliases, tags and duplicate keys are not allowed");
|
|
23
|
+
const value = document.toJS({ maxAliasCount: 0 });
|
|
24
|
+
if (!record(value) ||
|
|
25
|
+
value.format !== "narraeon.package-followups/v1" ||
|
|
26
|
+
Object.keys(value).some((key) => !["format", "followups"].includes(key)) ||
|
|
27
|
+
!Array.isArray(value.followups))
|
|
28
|
+
throw new Error("Expected narraeon.package-followups/v1 with a followups array");
|
|
29
|
+
const controls = value.followups.map((raw) => {
|
|
30
|
+
if (!record(raw) ||
|
|
31
|
+
typeof raw.enabled !== "boolean" ||
|
|
32
|
+
typeof raw.mount !== "string" ||
|
|
33
|
+
![
|
|
34
|
+
"story",
|
|
35
|
+
"sidebar",
|
|
36
|
+
"composer_above",
|
|
37
|
+
"composer_below",
|
|
38
|
+
"overlay",
|
|
39
|
+
"debug",
|
|
40
|
+
].includes(raw.mount))
|
|
41
|
+
throw new Error("Every package followup requires a boolean enabled and valid mount");
|
|
42
|
+
const { enabled, mount, ...definition } = raw;
|
|
43
|
+
return { enabled, mount: mount, definition };
|
|
44
|
+
});
|
|
45
|
+
const definitions = parseFollowups(controls.map((item) => item.definition), resources);
|
|
46
|
+
for (const definition of definitions)
|
|
47
|
+
for (const artifact of definition.artifacts) {
|
|
48
|
+
if (artifact.regex !== undefined)
|
|
49
|
+
parsePlayPresetRegexAsset(resources[artifact.regex], artifact.regex);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
followups: definitions.map((definition, index) => ({
|
|
53
|
+
...controls[index],
|
|
54
|
+
definition,
|
|
55
|
+
})),
|
|
56
|
+
files: resources,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function record(value) {
|
|
60
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
61
|
+
}
|
|
62
|
+
export function isPackageFollowupControlPath(path) {
|
|
63
|
+
return (path === "control/followups.yaml" ||
|
|
64
|
+
/^control\/(?:prompts\/[A-Za-z0-9._-]+\.md|renderers\/[A-Za-z0-9._-]+\.html|scripts\/[A-Za-z0-9._-]+\.js|regex\/[A-Za-z0-9._-]+\.yaml|assets\/[A-Za-z0-9][A-Za-z0-9._/-]*)$/u.test(path));
|
|
65
|
+
}
|
|
66
|
+
/** Authorization and compilation must freeze the exact same request resource closure. */
|
|
67
|
+
export function packageFollowupResources(definition, files) {
|
|
68
|
+
const paths = new Set(definition.artifacts.flatMap((artifact) => [
|
|
69
|
+
artifact.renderer,
|
|
70
|
+
artifact.regex,
|
|
71
|
+
...(artifact.scripts ?? []),
|
|
72
|
+
...(artifact.assets ?? []),
|
|
73
|
+
].filter((path) => path !== undefined)));
|
|
74
|
+
return Object.fromEntries([...paths].map((path) => {
|
|
75
|
+
const source = files[path];
|
|
76
|
+
if (source === undefined)
|
|
77
|
+
throw new Error(`Missing package followup resource: ${path}`);
|
|
78
|
+
return [path, source];
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { parsePlayPresetRegexAsset, } from "../play/FileNativePlayPresetStore.js";
|
|
2
|
-
export function projectArtifactForFrontend(artifact, binding, failure = "missing_revision") {
|
|
2
|
+
export function projectArtifactForFrontend(artifact, binding, failure = "missing_revision", packageScriptsEnabled = false) {
|
|
3
3
|
const preset = {
|
|
4
4
|
id: artifact.playPresetId,
|
|
5
5
|
revision: artifact.playPresetRevision,
|
|
6
6
|
};
|
|
7
|
-
if (binding === null)
|
|
7
|
+
if (binding === null && artifact.frozenPresentation === undefined)
|
|
8
8
|
return missingBundle(preset, "The frozen play-preset revision is unavailable and cannot be replaced with the current revision", failure);
|
|
9
|
-
if (binding
|
|
10
|
-
binding.
|
|
9
|
+
if (binding !== null &&
|
|
10
|
+
(binding.id !== artifact.playPresetId ||
|
|
11
|
+
binding.revision !== artifact.playPresetRevision))
|
|
11
12
|
return missingBundle(preset, "The play-preset binding does not match the artifact's frozen identity", "invalid_revision");
|
|
12
|
-
const followup = binding
|
|
13
|
-
const declaration =
|
|
13
|
+
const followup = binding?.definition.followups.find(({ id }) => id === artifact.requestId);
|
|
14
|
+
const declaration = artifact.frozenPresentation?.declaration ??
|
|
15
|
+
followup?.artifacts.find(({ name }) => name === artifact.output);
|
|
14
16
|
if (declaration === undefined)
|
|
15
17
|
return {
|
|
16
18
|
...missingBundle(preset, "The artifact declaration is missing from the frozen revision"),
|
|
@@ -23,8 +25,10 @@ export function projectArtifactForFrontend(artifact, binding, failure = "missing
|
|
|
23
25
|
};
|
|
24
26
|
try {
|
|
25
27
|
const frozenBinding = {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
scriptsEnabled: artifact.requestId.startsWith("package:")
|
|
29
|
+
? packageScriptsEnabled && declaration.rendererMode === "app"
|
|
30
|
+
: artifact.playPresetScriptsEnabled,
|
|
31
|
+
files: artifact.frozenPresentation?.files ?? binding?.files ?? {},
|
|
28
32
|
};
|
|
29
33
|
const regex = declaration.regex
|
|
30
34
|
? parsePlayPresetRegexAsset(frozenBinding.files[declaration.regex] ??
|
|
@@ -32,8 +36,9 @@ export function projectArtifactForFrontend(artifact, binding, failure = "missing
|
|
|
32
36
|
throw new Error("The frozen regex resource does not exist");
|
|
33
37
|
})(), declaration.regex)
|
|
34
38
|
: [];
|
|
35
|
-
const renderer =
|
|
36
|
-
const mount =
|
|
39
|
+
const renderer = resolveArtifactRenderer(declaration, frozenBinding);
|
|
40
|
+
const mount = artifact.frozenPresentation?.mount ??
|
|
41
|
+
binding?.definition.mounts.find(({ channel }) => channel === declaration.channel)?.mount;
|
|
37
42
|
return {
|
|
38
43
|
status: "ready",
|
|
39
44
|
preset,
|
|
@@ -44,7 +49,7 @@ export function projectArtifactForFrontend(artifact, binding, failure = "missing
|
|
|
44
49
|
declaration: declarationView(declaration),
|
|
45
50
|
regex,
|
|
46
51
|
...(renderer === undefined ? {} : { renderer }),
|
|
47
|
-
trustedLocalCode:
|
|
52
|
+
trustedLocalCode: frozenBinding.scriptsEnabled,
|
|
48
53
|
fallback: "none",
|
|
49
54
|
};
|
|
50
55
|
}
|
|
@@ -125,8 +130,8 @@ function artifactMatchesDeclaration(artifact, declaration) {
|
|
|
125
130
|
(artifact.rendererRevision ?? undefined) ===
|
|
126
131
|
(declaration.rendererRevision ?? undefined));
|
|
127
132
|
}
|
|
128
|
-
export function projectDebugArtifactForFrontend(artifact, binding, failure = "missing_revision") {
|
|
129
|
-
return projectArtifactForFrontend(artifact, binding, failure);
|
|
133
|
+
export function projectDebugArtifactForFrontend(artifact, binding, failure = "missing_revision", packageScriptsEnabled = false) {
|
|
134
|
+
return projectArtifactForFrontend(artifact, binding, failure, packageScriptsEnabled);
|
|
130
135
|
}
|
|
131
136
|
function declarationView(declaration) {
|
|
132
137
|
return {
|
|
@@ -145,7 +150,7 @@ function declarationView(declaration) {
|
|
|
145
150
|
rendererMode: declaration.rendererMode ?? "document",
|
|
146
151
|
};
|
|
147
152
|
}
|
|
148
|
-
function
|
|
153
|
+
export function resolveArtifactRenderer(declaration, binding) {
|
|
149
154
|
const mode = declaration.rendererMode ?? "document";
|
|
150
155
|
const rendererSource = declaration.renderer
|
|
151
156
|
? binding.files[declaration.renderer]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { open, readFile, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { readPackageFollowups, packageFollowupResources, } from "../content/PackageFollowups.js";
|
|
5
|
+
const fileName = "package-script-permissions.json";
|
|
6
|
+
/** Local grants authorize exact resource bytes. They never travel in a portable content tree. */
|
|
7
|
+
export class PackageScriptPermissions {
|
|
8
|
+
static async read(ownerRoot) {
|
|
9
|
+
try {
|
|
10
|
+
const value = JSON.parse(await readFile(join(ownerRoot, fileName), "utf8"));
|
|
11
|
+
if (typeof value !== "object" ||
|
|
12
|
+
value === null ||
|
|
13
|
+
!("schemaVersion" in value) ||
|
|
14
|
+
value.schemaVersion !== 1 ||
|
|
15
|
+
!("grants" in value) ||
|
|
16
|
+
Object.keys(value).length !== 2 ||
|
|
17
|
+
!Array.isArray(value.grants) ||
|
|
18
|
+
!value.grants.every((entry) => typeof entry === "string" && /^[a-f0-9]{64}$/u.test(entry)))
|
|
19
|
+
throw new Error("Invalid local package script permissions");
|
|
20
|
+
return value.grants;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
24
|
+
return [];
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
static async write(ownerRoot, grants) {
|
|
29
|
+
const path = join(ownerRoot, fileName);
|
|
30
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
31
|
+
try {
|
|
32
|
+
const file = await open(temporary, "wx", 0o600);
|
|
33
|
+
try {
|
|
34
|
+
await file.writeFile(JSON.stringify({ schemaVersion: 1, grants }));
|
|
35
|
+
await file.sync();
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
await file.close();
|
|
39
|
+
}
|
|
40
|
+
await rename(temporary, path);
|
|
41
|
+
const directory = await open(ownerRoot, "r");
|
|
42
|
+
try {
|
|
43
|
+
await directory.sync();
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
await directory.close();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
await rm(temporary, { force: true });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
static fingerprint(files) {
|
|
54
|
+
return createHash("sha256")
|
|
55
|
+
.update(JSON.stringify(Object.entries(files).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)))
|
|
56
|
+
.digest("hex");
|
|
57
|
+
}
|
|
58
|
+
static currentGrants(files) {
|
|
59
|
+
const source = readPackageFollowups(files);
|
|
60
|
+
return [
|
|
61
|
+
...new Set(source.followups.map(({ definition }) => this.fingerprint(packageFollowupResources(definition, source.files)))),
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
static status(files, grants) {
|
|
65
|
+
const current = this.currentGrants(files);
|
|
66
|
+
return {
|
|
67
|
+
enabled: current.length > 0 && current.every((grant) => grants.includes(grant)),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|