@playcraft/cli 0.1.56-beta → 0.1.58-beta
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/assets/client.js +6 -4
- package/dist/commands/tools-3d.js +15 -0
- package/dist/commands/tools-generation.js +3 -2
- package/dist/commands/workspace.js +43 -0
- package/dist/utils/agent-api-client.js +31 -8
- package/dist/utils/agent-generation-provenance.js +45 -0
- package/dist/utils/generation-workspace-scope.js +50 -0
- package/dist/utils/image-generation-retry.js +7 -0
- package/dist/utils/tool-operation-journal.js +34 -4
- package/dist/workspace/messages.js +10 -0
- package/dist/workspace-runtime/batch/deliver.js +140 -80
- package/dist/workspace-runtime/batch/freeze.js +18 -6
- package/dist/workspace-runtime/batch/generate.js +68 -64
- package/dist/workspace-runtime/batch/prepare.js +109 -137
- package/dist/workspace-runtime/batch/records.js +4 -0
- package/dist/workspace-runtime/batch/routes.js +10 -2
- package/dist/workspace-runtime/batch/source-candidate.js +2 -0
- package/dist/workspace-runtime/batch/source-capture.js +1 -0
- package/dist/workspace-runtime/batch/source-failure.js +2 -0
- package/dist/workspace-runtime/batch/source-publish.js +3 -0
- package/dist/workspace-runtime/batch/writer-proof.js +37 -0
- package/dist/workspace-runtime/codex/app-server.js +1 -0
- package/dist/workspace-runtime/codex/config-toml.js +4 -0
- package/dist/workspace-runtime/codex/delivery-tools.js +7 -5
- package/dist/workspace-runtime/codex/generation-provenance-broker.js +80 -0
- package/dist/workspace-runtime/codex/launcher-client.js +18 -0
- package/dist/workspace-runtime/codex/model-broker.js +23 -4
- package/dist/workspace-runtime/persistence/journal.js +34 -0
- package/dist/workspace-runtime/persistence/sqlite-worker.js +68 -6
- package/dist/workspace-runtime/previews/output.js +2 -0
- package/dist/workspace-runtime/previews/publish.js +20 -5
- package/dist/workspace-runtime/previews/runtime-preview.js +4 -0
- package/dist/workspace-runtime/processes/managed-writes.js +47 -5
- package/dist/workspace-runtime/serve.js +19 -32
- package/dist/workspace-runtime/server/accept-operation.js +22 -2
- package/dist/workspace-runtime/server/directory-access.js +414 -0
- package/dist/workspace-runtime/server/dispatch.js +61 -23
- package/dist/workspace-runtime/server/endpoints.js +1 -0
- package/dist/workspace-runtime/server/http.js +52 -10
- package/dist/workspace-runtime/server/operation-record.js +2 -0
- package/dist/workspace-runtime/server/terminal-host.js +43 -8
- package/dist/workspace-runtime/server/workspace-transfer.js +3 -1
- package/dist/workspace-runtime/workspaces/append-only-log.js +3 -0
- package/dist/workspace-runtime/workspaces/artifact-events.js +5 -2
- package/dist/workspace-runtime/workspaces/artifact-files.js +14 -8
- package/dist/workspace-runtime/workspaces/artifact-receipts.js +7 -25
- package/dist/workspace-runtime/workspaces/artifacts.js +4 -1
- package/dist/workspace-runtime/workspaces/asset-library-import.js +9 -4
- package/dist/workspace-runtime/workspaces/asset-publication-access.js +7 -9
- package/dist/workspace-runtime/workspaces/asset-transfer-publish.js +3 -0
- package/dist/workspace-runtime/workspaces/asset-transfer-storage.js +17 -2
- package/dist/workspace-runtime/workspaces/attachments.js +9 -0
- package/dist/workspace-runtime/workspaces/batch-commit.js +5 -3
- package/dist/workspace-runtime/workspaces/binary-write.js +12 -7
- package/dist/workspace-runtime/workspaces/cli-context.js +2 -0
- package/dist/workspace-runtime/workspaces/cli-credentials.js +1 -0
- package/dist/workspace-runtime/workspaces/delivery-build.js +30 -11
- package/dist/workspace-runtime/workspaces/delivery-store.js +2 -0
- package/dist/workspace-runtime/workspaces/dependencies.js +105 -71
- package/dist/workspace-runtime/workspaces/developer-command.js +12 -1
- package/dist/workspace-runtime/workspaces/directory-access.js +13 -18
- package/dist/workspace-runtime/workspaces/directory-attachments.js +8 -0
- package/dist/workspace-runtime/workspaces/directory-create.js +18 -5
- package/dist/workspace-runtime/workspaces/directory-maintenance.js +46 -35
- package/dist/workspace-runtime/workspaces/directory-mutations.js +8 -3
- package/dist/workspace-runtime/workspaces/files.js +4 -0
- package/dist/workspace-runtime/workspaces/git-credential-broker.js +3 -0
- package/dist/workspace-runtime/workspaces/git-direct-store.js +2 -0
- package/dist/workspace-runtime/workspaces/json5-edit.js +36 -0
- package/dist/workspace-runtime/workspaces/library-attachments.js +4 -0
- package/dist/workspace-runtime/workspaces/parameter-agent-protocol.js +2 -0
- package/dist/workspace-runtime/workspaces/parameter-agent-store.js +2 -0
- package/dist/workspace-runtime/workspaces/parameter-agent.js +40 -11
- package/dist/workspace-runtime/workspaces/parameter-assets.js +1 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization-skill-transaction.js +141 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +329 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization.js +63 -0
- package/dist/workspace-runtime/workspaces/parameter-production.js +124 -0
- package/dist/workspace-runtime/workspaces/parameters.js +95 -3
- package/dist/workspace-runtime/workspaces/platform-export.js +190 -0
- package/dist/workspace-runtime/workspaces/prepare.js +4 -0
- package/dist/workspace-runtime/workspaces/registry.js +11 -0
- package/dist/workspace-runtime/workspaces/remix-references.js +5 -0
- package/dist/workspace-runtime/workspaces/shared-write.js +187 -0
- package/dist/workspace-runtime/workspaces/skills.js +14 -15
- package/dist/workspace-runtime/workspaces/transfer.js +4 -1
- package/dist/workspace-runtime/workspaces/workspace-shares.js +235 -0
- package/dist/workspace-runtime/workspaces/worktree-attachments.js +2 -0
- package/dist/workspace-runtime/workspaces/worktree-checkout.js +11 -6
- package/package.json +5 -4
- package/project-template/.claude/skills/playcraft-runtime-parameters/SKILL.md +32 -0
- package/project-template/.claude/skills/playcraft-runtime-parameters/references/platform-protocol.md +21 -0
- package/project-template/.claude/skills/playcraft-runtime-parameters/references/preview-integration.md +57 -0
- package/project-template-v2/.playcraft/skills.lock.json +15 -8
- package/templates/preview-runtime/v1/README.md +4 -0
- package/templates/preview-runtime/v1/manifest.json +6 -6
- package/templates/preview-runtime/v1/runtime.mjs +368 -22
- package/templates/preview-runtime/v1/types/adapters.d.ts +4 -1
- package/templates/preview-runtime/v1/types/preview.d.ts +1 -0
- package/templates/preview-runtime/v1/types/protocol.d.ts +19 -0
- package/templates/preview-runtime/v1/types/runtime.d.ts +2 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { chmod, chown, lstat, readdir } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { SHARED_DIRECTORY_MODE, SHARED_FILE_MODE, sharePathWithGroup, } from './shared-write.js';
|
|
3
4
|
/** Grant development-group traversal of the workspace container without touching source checkouts. */
|
|
4
5
|
export async function grantWorkspaceTraversal(root, groupId) {
|
|
5
6
|
const stat = await lstat(root);
|
|
@@ -18,9 +19,7 @@ export async function openScratchToDevelopment(path, groupId) {
|
|
|
18
19
|
const stat = await lstat(path);
|
|
19
20
|
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
20
21
|
throw new Error('invalid_scratch_directory');
|
|
21
|
-
|
|
22
|
-
await chown(path, stat.uid, groupId);
|
|
23
|
-
await chmod(path, 0o2770);
|
|
22
|
+
await sharePathWithGroup(path, groupId, SHARED_DIRECTORY_MODE);
|
|
24
23
|
}
|
|
25
24
|
/** The sandbox development group can edit engine directories. Symlinks never grant access outside the tree. */
|
|
26
25
|
export async function openDirectoryToDevelopment(root, groupId) {
|
|
@@ -29,22 +28,18 @@ export async function openDirectoryToDevelopment(root, groupId) {
|
|
|
29
28
|
const stat = await lstat(path);
|
|
30
29
|
if (stat.isSymbolicLink())
|
|
31
30
|
return;
|
|
31
|
+
// Entries owned by the other identity cannot be re-shaped from here; the owner repairs them.
|
|
32
32
|
const canModify = currentUid === undefined || currentUid === 0 || stat.uid === currentUid;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
for (const entry of entries)
|
|
44
|
-
await visit(join(path, entry));
|
|
45
|
-
}
|
|
46
|
-
else if (stat.isFile() && canModify)
|
|
47
|
-
await chmod(path, (stat.mode & 0o777) | 0o660);
|
|
33
|
+
await sharePathWithGroup(path, groupId, stat.isDirectory() ? SHARED_DIRECTORY_MODE : SHARED_FILE_MODE, stat.isFile());
|
|
34
|
+
if (!stat.isDirectory())
|
|
35
|
+
return;
|
|
36
|
+
const entries = await readdir(path).catch(error => {
|
|
37
|
+
if (!canModify && error.code === 'EACCES')
|
|
38
|
+
return [];
|
|
39
|
+
throw error;
|
|
40
|
+
});
|
|
41
|
+
for (const entry of entries)
|
|
42
|
+
await visit(join(path, entry));
|
|
48
43
|
};
|
|
49
44
|
await visit(root);
|
|
50
45
|
}
|
|
@@ -25,6 +25,7 @@ async function safeDirectory(root, relative, create = false) {
|
|
|
25
25
|
let directory = root;
|
|
26
26
|
for (const part of relative.split('/')) {
|
|
27
27
|
directory = join(directory, part);
|
|
28
|
+
// shared
|
|
28
29
|
if (create)
|
|
29
30
|
await mkdir(directory, { mode: 0o755 }).catch((error) => {
|
|
30
31
|
if (error.code !== 'EEXIST')
|
|
@@ -59,10 +60,14 @@ export async function beginWorkspaceDirectoryAttachment(root, body) {
|
|
|
59
60
|
const staging = await safeDirectory(root, stagingPath, true);
|
|
60
61
|
const id = randomUUID();
|
|
61
62
|
const stage = join(staging, id);
|
|
63
|
+
// shared
|
|
62
64
|
await mkdir(stage, { mode: 0o755 });
|
|
63
65
|
try {
|
|
66
|
+
// shared
|
|
64
67
|
await mkdir(join(stage, 'data'), { mode: 0o755 });
|
|
68
|
+
// shared
|
|
65
69
|
await mkdir(join(stage, 'data', manifest.name), { mode: 0o755 });
|
|
70
|
+
// shared
|
|
66
71
|
await writeFile(join(stage, 'manifest.json'), JSON.stringify(manifest), { flag: 'wx', mode: 0o644 });
|
|
67
72
|
return { uploadId: id };
|
|
68
73
|
}
|
|
@@ -84,6 +89,7 @@ export async function uploadWorkspaceDirectoryFile(root, id, path, request) {
|
|
|
84
89
|
throw new Error('invalid_attachment');
|
|
85
90
|
const relative = `${stagingPath}/${id}/data/${manifest.name}/${path}`;
|
|
86
91
|
await safeDirectory(root, dirname(relative), true);
|
|
92
|
+
// shared
|
|
87
93
|
const handle = await open(join(root, relative), constants.O_CREAT | constants.O_EXCL | constants.O_RDWR | constants.O_NOFOLLOW, 0o644);
|
|
88
94
|
try {
|
|
89
95
|
let size = 0;
|
|
@@ -164,9 +170,11 @@ export async function completeWorkspaceDirectoryAttachment(root, id) {
|
|
|
164
170
|
path: `${WORKSPACE_USER_UPLOADS_DIRECTORY}/${id}/${manifest.name}`, files };
|
|
165
171
|
workspaceAttachmentSchema.parse(attachment);
|
|
166
172
|
const target = join(uploads, id);
|
|
173
|
+
// shared
|
|
167
174
|
await mkdir(target, { mode: 0o755 });
|
|
168
175
|
try {
|
|
169
176
|
await rename(join(stage, 'data', manifest.name), join(target, manifest.name));
|
|
177
|
+
// shared
|
|
170
178
|
await writeFile(join(metadata, `${id}.json`), JSON.stringify(attachment), { flag: 'wx', mode: 0o644 });
|
|
171
179
|
}
|
|
172
180
|
catch (error) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { constants } from 'node:fs';
|
|
2
|
-
import { lstat, mkdir,
|
|
2
|
+
import { lstat, mkdir, realpath, rmdir } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { directoryCreatePayloadSchema } from '@playcraft/common/cloud-workspace/runtime';
|
|
5
5
|
import { ensureWorkspaceRoot, InvalidWorkspacePathError } from './files.js';
|
|
6
|
+
import { ensureSharedDirectory, openSharedFile } from './shared-write.js';
|
|
6
7
|
const RESERVED_DIRECTORIES = new Set(['.git', 'node_modules', '.playcraft-temp']);
|
|
7
8
|
export class WorkspaceDirectoryCreateError extends Error {
|
|
8
9
|
code = 'directory_exists';
|
|
@@ -18,18 +19,29 @@ export async function createWorkspaceDirectory(root, input) {
|
|
|
18
19
|
if (parts.some(part => RESERVED_DIRECTORIES.has(part)))
|
|
19
20
|
throw new InvalidWorkspacePathError(path);
|
|
20
21
|
const rootReal = await ensureWorkspaceRoot(root);
|
|
22
|
+
const share = async (path) => {
|
|
23
|
+
try {
|
|
24
|
+
await ensureSharedDirectory(rootReal, path);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error.message === 'invalid_shared_directory')
|
|
28
|
+
throw new InvalidWorkspacePathError(path);
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
21
32
|
let parent = rootReal;
|
|
22
33
|
for (const part of parts.slice(0, -1)) {
|
|
23
34
|
parent = join(parent, part);
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
// Created directories are visible to the development identity that runs agents and builds.
|
|
36
|
+
await share(parent);
|
|
26
37
|
const stat = await lstat(parent);
|
|
27
38
|
if (!stat.isDirectory() || stat.isSymbolicLink() || await realpath(parent) !== parent)
|
|
28
39
|
throw new InvalidWorkspacePathError(path);
|
|
29
40
|
}
|
|
30
41
|
const target = join(parent, parts.at(-1));
|
|
42
|
+
// shared
|
|
31
43
|
try {
|
|
32
|
-
await mkdir(target, { mode:
|
|
44
|
+
await mkdir(target, { mode: 0o2770 });
|
|
33
45
|
}
|
|
34
46
|
catch (error) {
|
|
35
47
|
if (error.code !== 'EEXIST')
|
|
@@ -41,7 +53,8 @@ export async function createWorkspaceDirectory(root, input) {
|
|
|
41
53
|
try {
|
|
42
54
|
if (!(await lstat(target)).isDirectory() || await realpath(target) !== target)
|
|
43
55
|
throw new InvalidWorkspacePathError(path);
|
|
44
|
-
|
|
56
|
+
await share(target);
|
|
57
|
+
const marker = await openSharedFile(rootReal, join(target, '.gitkeep'), constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW);
|
|
45
58
|
try {
|
|
46
59
|
await marker.sync();
|
|
47
60
|
}
|
|
@@ -6,6 +6,7 @@ import { promisify } from 'node:util';
|
|
|
6
6
|
import { workspaceDirectoryNameSchema } from '@playcraft/common/cloud-workspace/runtime';
|
|
7
7
|
import { batchDirectoryHeld } from '../batch/occupancy.js';
|
|
8
8
|
import { workspaceRoot, workspaceSessions } from './registry.js';
|
|
9
|
+
import { withDirectoryWrite } from '../server/directory-access.js';
|
|
9
10
|
const execute = promisify(execFile);
|
|
10
11
|
const git = async (state, root, ...args) => (await execute('git', ['-c', 'core.hooksPath=/dev/null', '-c', 'core.fsmonitor=false', '-C', root, ...args], {
|
|
11
12
|
timeout: 30_000, maxBuffer: 1024 * 1024, env: { PATH: process.env.PATH, HOME: state.config.stateDir, GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_NOSYSTEM: '1', GIT_TERMINAL_PROMPT: '0' },
|
|
@@ -39,7 +40,7 @@ export async function cleanupWorkspaceDirectories(state, workspaceId, directoryN
|
|
|
39
40
|
const blocked = [];
|
|
40
41
|
for (const directoryName of names) {
|
|
41
42
|
try {
|
|
42
|
-
await state.journal.exclusive(`directory:${workspaceId}:${directoryName}`, async () => {
|
|
43
|
+
await withDirectoryWrite(state, { workspaceId, directoryName, mode: 'exclusive', wait: false }, () => state.journal.exclusive(`directory:${workspaceId}:${directoryName}`, async () => {
|
|
43
44
|
if (await workspaceDirectoryOccupied(state, workspaceId, directoryName))
|
|
44
45
|
throw new Error('workspace_directory_occupied');
|
|
45
46
|
const root = await workspaceRoot(state, workspaceId, directoryName);
|
|
@@ -51,7 +52,7 @@ export async function cleanupWorkspaceDirectories(state, workspaceId, directoryN
|
|
|
51
52
|
await state.journal.putWorkspace({ ...binding, directories: [...(binding.directories ?? []).filter(item => item.directoryName !== directoryName), { ...directory, status: 'reclaimed' }] });
|
|
52
53
|
await rm(root, { recursive: true });
|
|
53
54
|
cleaned.push(directoryName);
|
|
54
|
-
});
|
|
55
|
+
}));
|
|
55
56
|
}
|
|
56
57
|
catch (error) {
|
|
57
58
|
blocked.push({ directoryName, reason: error instanceof Error && /^[a-z0-9_]+$/.test(error.message) ? error.message : 'workspace_cleanup_failed' });
|
|
@@ -70,42 +71,52 @@ export async function syncWorkspaceDirectories(state, workspaceId, commitHash, t
|
|
|
70
71
|
for (const directoryName of [...new Set(targetDirectoryNames.map(name => workspaceDirectoryNameSchema.parse(name)))]) {
|
|
71
72
|
if (directoryName === 'basic')
|
|
72
73
|
throw new Error('workspace_sync_target_invalid');
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const root = await workspaceRoot(state, workspaceId, directoryName);
|
|
79
|
-
const ledger = join(state.config.stateDir, 'directory-sync', workspaceId, `${directoryName}.json`);
|
|
80
|
-
const applied = await readFile(ledger, 'utf8').then(value => JSON.parse(value), () => undefined);
|
|
81
|
-
await assertCleanAndPushed(state, root, applied?.sourceCommit === commitHash ? applied.targetCommit : undefined);
|
|
82
|
-
if (applied?.sourceCommit === commitHash && await git(state, root, 'rev-parse', 'HEAD') === applied.targetCommit) {
|
|
83
|
-
synced.push(directoryName);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
const containing = await git(state, root, 'merge-base', '--is-ancestor', commitHash, 'HEAD').then(() => true, () => false);
|
|
87
|
-
if (containing) {
|
|
88
|
-
synced.push(directoryName);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
// Import the exact preserved basic commit, without fetching a moving remote branch.
|
|
92
|
-
await execute('git', ['-c', 'core.hooksPath=/dev/null', '-C', root, 'fetch', '--no-tags', '--', basic, commitHash], { timeout: 30_000, maxBuffer: 1024 * 1024 });
|
|
93
|
-
const equivalent = (await git(state, root, 'cherry', 'HEAD', commitHash)).split('\n').some(line => line === `- ${commitHash}`);
|
|
94
|
-
if (!equivalent) {
|
|
95
|
-
try {
|
|
96
|
-
await git(state, root, '-c', 'user.name=PlayCraft', '-c', 'user.email=playcraft@localhost', 'cherry-pick', commitHash);
|
|
74
|
+
try {
|
|
75
|
+
await withDirectoryWrite(state, { workspaceId, directoryName, mode: 'exclusive', wait: false }, () => state.journal.exclusive(`directory:${workspaceId}:${directoryName}`, async () => {
|
|
76
|
+
if (await workspaceDirectoryOccupied(state, workspaceId, directoryName)) {
|
|
77
|
+
skipped.push({ directoryName, reason: 'directory_busy' });
|
|
78
|
+
return;
|
|
97
79
|
}
|
|
98
|
-
|
|
99
|
-
|
|
80
|
+
const root = await workspaceRoot(state, workspaceId, directoryName);
|
|
81
|
+
const ledger = join(state.config.stateDir, 'directory-sync', workspaceId, `${directoryName}.json`);
|
|
82
|
+
const applied = await readFile(ledger, 'utf8').then(value => JSON.parse(value), () => undefined);
|
|
83
|
+
await assertCleanAndPushed(state, root, applied?.sourceCommit === commitHash ? applied.targetCommit : undefined);
|
|
84
|
+
if (applied?.sourceCommit === commitHash && await git(state, root, 'rev-parse', 'HEAD') === applied.targetCommit) {
|
|
85
|
+
synced.push(directoryName);
|
|
100
86
|
return;
|
|
101
87
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
88
|
+
const containing = await git(state, root, 'merge-base', '--is-ancestor', commitHash, 'HEAD').then(() => true, () => false);
|
|
89
|
+
if (containing) {
|
|
90
|
+
synced.push(directoryName);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Import the exact preserved basic commit, without fetching a moving remote branch.
|
|
94
|
+
await execute('git', ['-c', 'core.hooksPath=/dev/null', '-C', root, 'fetch', '--no-tags', '--', basic, commitHash], { timeout: 30_000, maxBuffer: 1024 * 1024 });
|
|
95
|
+
const equivalent = (await git(state, root, 'cherry', 'HEAD', commitHash)).split('\n').some(line => line === `- ${commitHash}`);
|
|
96
|
+
if (!equivalent) {
|
|
97
|
+
try {
|
|
98
|
+
await git(state, root, '-c', 'user.name=PlayCraft', '-c', 'user.email=playcraft@localhost', 'cherry-pick', commitHash);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
conflicts.push(directoryName);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// private
|
|
106
|
+
await mkdir(join(state.config.stateDir, 'directory-sync', workspaceId), { recursive: true, mode: 0o700 });
|
|
107
|
+
const temporary = `${ledger}.${randomUUID()}.tmp`;
|
|
108
|
+
// private
|
|
109
|
+
await writeFile(temporary, JSON.stringify({ sourceCommit: commitHash, targetCommit: await git(state, root, 'rev-parse', 'HEAD') }), { mode: 0o600 });
|
|
110
|
+
await rename(temporary, ledger);
|
|
111
|
+
synced.push(directoryName);
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error instanceof Error && error.message === 'directory_busy')
|
|
116
|
+
skipped.push({ directoryName, reason: 'directory_busy' });
|
|
117
|
+
else
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
109
120
|
}
|
|
110
121
|
return { synced, skipped, conflicts, previewStale: true };
|
|
111
122
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { constants } from 'node:fs';
|
|
3
|
-
import { lstat,
|
|
3
|
+
import { lstat, open, readdir, realpath, rename, rm } from 'node:fs/promises';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { directoryDeletePayloadSchema, directoryRenamePayloadSchema, hashOperationPayload, isMutableWorkspaceDirectoryPath, } from '@playcraft/common/cloud-workspace/runtime';
|
|
7
7
|
import { safeAssetTransferDirectory, syncAssetTransferDirectory } from './asset-transfer-storage.js';
|
|
8
|
+
import { ensureSharedDirectory } from './shared-write.js';
|
|
8
9
|
export class DirectoryMutationError extends Error {
|
|
9
10
|
code;
|
|
10
11
|
constructor(code) {
|
|
@@ -60,6 +61,7 @@ async function validateTree(root, path) {
|
|
|
60
61
|
async function saveRecord(directory, record) {
|
|
61
62
|
await safeAssetTransferDirectory(directory);
|
|
62
63
|
const temporary = join(directory, `${randomUUID()}.tmp`);
|
|
64
|
+
// private
|
|
63
65
|
const file = await open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
|
|
64
66
|
try {
|
|
65
67
|
await file.writeFile(JSON.stringify(record));
|
|
@@ -183,8 +185,11 @@ export async function mutateWorkspaceDirectory(root, kind, input, options) {
|
|
|
183
185
|
if (newPath) {
|
|
184
186
|
// Reserve an absent destination atomically; only our recorded empty placeholder may be replaced.
|
|
185
187
|
if (!destinationIdentity) {
|
|
186
|
-
await
|
|
187
|
-
return
|
|
188
|
+
if (await lstat(destination).catch(error => { if (missing(error))
|
|
189
|
+
return undefined; throw error; }))
|
|
190
|
+
return fail('directory_exists');
|
|
191
|
+
// A renamed directory is a workspace entry both identities must reach.
|
|
192
|
+
await ensureSharedDirectory(root, destination);
|
|
188
193
|
record.reservation = await identity(destination);
|
|
189
194
|
await saveRecord(directory, record);
|
|
190
195
|
}
|
|
@@ -13,6 +13,7 @@ export class InvalidWorkspacePathError extends Error {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
export async function ensureWorkspaceRoot(root) {
|
|
16
|
+
// private
|
|
16
17
|
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
17
18
|
return realpath(root);
|
|
18
19
|
}
|
|
@@ -37,8 +38,10 @@ export async function resolveWorkspaceFile(root, relativePath) {
|
|
|
37
38
|
}
|
|
38
39
|
export async function writeWorkspaceFile(root, relativePath, content) {
|
|
39
40
|
const target = await resolveWorkspaceFile(root, relativePath);
|
|
41
|
+
// private
|
|
40
42
|
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
41
43
|
const bytes = Buffer.from(content, 'utf8');
|
|
44
|
+
// private
|
|
42
45
|
await writeFile(target, bytes, { mode: 0o600 });
|
|
43
46
|
return { path: relativePath, contentHash: hashWorkspaceFileContent(content), size: bytes.byteLength };
|
|
44
47
|
}
|
|
@@ -134,6 +137,7 @@ async function assertParentInsideRoot(rootReal, parent) {
|
|
|
134
137
|
catch (error) {
|
|
135
138
|
if (error.code !== 'ENOENT')
|
|
136
139
|
throw error;
|
|
140
|
+
// private
|
|
137
141
|
await mkdir(parent, { recursive: true, mode: 0o700 });
|
|
138
142
|
const parentReal = await realpath(parent);
|
|
139
143
|
assertInsideRoot(rootReal, parentReal);
|
|
@@ -240,6 +240,7 @@ async function configureGitFile(root, stateDir, commands, repositoryUrl) {
|
|
|
240
240
|
gitDirectory = await open(join(pinnedRoot, '.git'), directoryFlags);
|
|
241
241
|
const gitPath = process.platform === 'linux' ? `/proc/self/fd/${gitDirectory.fd}` : join(root, '.git');
|
|
242
242
|
const configPath = join(gitPath, 'config');
|
|
243
|
+
// private
|
|
243
244
|
lock = await open(join(gitPath, 'config.lock'), constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o640);
|
|
244
245
|
lockPath = join(gitPath, 'config.lock');
|
|
245
246
|
const source = await open(configPath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
@@ -254,9 +255,11 @@ async function configureGitFile(root, stateDir, commands, repositoryUrl) {
|
|
|
254
255
|
finally {
|
|
255
256
|
await source.close();
|
|
256
257
|
}
|
|
258
|
+
// private
|
|
257
259
|
await mkdir(stateDir, { recursive: true, mode: 0o700 });
|
|
258
260
|
temporary = await mkdtemp(join(stateDir, 'git-config-'));
|
|
259
261
|
const staging = join(temporary, 'config');
|
|
262
|
+
// private
|
|
260
263
|
await writeFile(staging, content, { mode: 0o600 });
|
|
261
264
|
await clearManagedGitConfig(staging, repositoryUrl);
|
|
262
265
|
for (const args of commands)
|
|
@@ -39,6 +39,7 @@ export class WorkspaceGitDirectStore {
|
|
|
39
39
|
throw new Error('git_control_directory_rejected');
|
|
40
40
|
}
|
|
41
41
|
for (const path of [stateDir, directory]) {
|
|
42
|
+
// private
|
|
42
43
|
if (path === directory && create)
|
|
43
44
|
await mkdir(path, { mode: 0o700 }).catch(error => {
|
|
44
45
|
if (error.code !== 'EEXIST')
|
|
@@ -105,6 +106,7 @@ export class WorkspaceGitDirectStore {
|
|
|
105
106
|
if (rejectedPassword !== undefined && old?.access?.password !== rejectedPassword)
|
|
106
107
|
return;
|
|
107
108
|
const temporary = join(path, `${randomUUID()}.tmp`);
|
|
109
|
+
// private
|
|
108
110
|
const handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
109
111
|
try {
|
|
110
112
|
await handle.writeFile(JSON.stringify({ identity: this.identity(), binding, access }));
|
|
@@ -33,6 +33,42 @@ export function editJson5Paths(source, changes) {
|
|
|
33
33
|
JSON5.parse(next);
|
|
34
34
|
return next;
|
|
35
35
|
}
|
|
36
|
+
/** Replace complete language records while preserving every unrelated authored member. */
|
|
37
|
+
export function editJson5StringRecords(source, changes) {
|
|
38
|
+
const parsed = JSON5.parse(source);
|
|
39
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
40
|
+
throw new ParameterEditError('parameter_document_invalid');
|
|
41
|
+
if (new Set(changes.map(change => change.key)).size !== changes.length)
|
|
42
|
+
throw new ParameterEditError('parameter_changes_overlap');
|
|
43
|
+
let next = source;
|
|
44
|
+
for (const change of changes) {
|
|
45
|
+
if (!change.key || ['__proto__', 'prototype', 'constructor'].includes(change.key))
|
|
46
|
+
throw new ParameterEditError('parameter_value_invalid');
|
|
47
|
+
const object = readJson5Object(next, skipTrivia(next, 0, next.length));
|
|
48
|
+
const memberIndex = object.members.findIndex(member => member.name === change.key);
|
|
49
|
+
const member = object.members[memberIndex];
|
|
50
|
+
if ('op' in change) {
|
|
51
|
+
if (!member)
|
|
52
|
+
continue;
|
|
53
|
+
const comma = member.comma ?? object.members[memberIndex - 1]?.comma;
|
|
54
|
+
const spans = [{ start: member.keyStart, end: member.value.end }, ...(comma === undefined ? [] : [{ start: comma, end: comma + 1 }])].sort((left, right) => right.start - left.start);
|
|
55
|
+
for (const span of spans)
|
|
56
|
+
next = next.slice(0, span.start) + next.slice(span.end);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
if (!change.value || typeof change.value !== 'object' || Array.isArray(change.value) || Object.values(change.value).some(value => typeof value !== 'string'))
|
|
60
|
+
throw new ParameterEditError('parameter_value_invalid');
|
|
61
|
+
if (!member)
|
|
62
|
+
next = editJson5Paths(next, [{ fieldPath: [change.key], value: '' }]);
|
|
63
|
+
const span = findJson5ValueSpan(next, [change.key]);
|
|
64
|
+
if (!span)
|
|
65
|
+
throw new ParameterEditError('parameter_field_missing');
|
|
66
|
+
next = next.slice(0, span.start) + JSON.stringify(change.value) + next.slice(span.end);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
JSON5.parse(next);
|
|
70
|
+
return next;
|
|
71
|
+
}
|
|
36
72
|
function editJson5Object(source, braceIndex, change, depth) {
|
|
37
73
|
const object = readJson5Object(source, braceIndex);
|
|
38
74
|
const memberIndex = object.members.findIndex((member) => member.name === change.fieldPath[depth]);
|
|
@@ -11,6 +11,7 @@ async function safeDirectory(root, path, create = false) {
|
|
|
11
11
|
let directory = root;
|
|
12
12
|
for (const part of path.split('/').filter(Boolean)) {
|
|
13
13
|
directory = join(directory, part);
|
|
14
|
+
// shared
|
|
14
15
|
if (create)
|
|
15
16
|
await mkdir(directory, { mode: 0o755 }).catch((error) => {
|
|
16
17
|
if (error.code !== 'EEXIST')
|
|
@@ -173,12 +174,14 @@ async function importAttachment(root, body) {
|
|
|
173
174
|
if (!record) {
|
|
174
175
|
const temporary = join(uploads, `.import-${randomUUID()}`);
|
|
175
176
|
const target = join(uploads, id);
|
|
177
|
+
// shared
|
|
176
178
|
await mkdir(temporary, { mode: 0o755 });
|
|
177
179
|
try {
|
|
178
180
|
for (const file of files) {
|
|
179
181
|
const parent = dirname(file.path);
|
|
180
182
|
if (parent !== '.')
|
|
181
183
|
await safeDirectory(temporary, parent, true);
|
|
184
|
+
// shared
|
|
182
185
|
await writeFile(join(temporary, file.path), file.bytes, { flag: 'wx', mode: 0o644 });
|
|
183
186
|
}
|
|
184
187
|
const prefix = `${WORKSPACE_USER_UPLOADS_DIRECTORY}/${id}`;
|
|
@@ -236,6 +239,7 @@ async function importAttachment(root, body) {
|
|
|
236
239
|
record.clientRequestIds = [...new Set([...record.clientRequestIds, clientRequestId])];
|
|
237
240
|
record.unbound ||= !sessionId;
|
|
238
241
|
const tempMetadata = join(metadata, `.import-${randomUUID()}.json`);
|
|
242
|
+
// shared
|
|
239
243
|
await writeFile(tempMetadata, JSON.stringify(record), { flag: 'wx', mode: 0o644 });
|
|
240
244
|
await rename(tempMetadata, join(metadata, `${id}.json`));
|
|
241
245
|
await chmod(join(uploads, id), 0o755);
|
|
@@ -38,3 +38,5 @@ Report JSON (JavaScript Object Notation, JavaScript 对象表示法) contract (n
|
|
|
38
38
|
- No passed/ready/live/checks fields are accepted in the report. All implementation and validation statements remain claims until platform verification.
|
|
39
39
|
- If blocked or partial, preserve reviewable changes and list concrete missing work. Do not overwrite later user edits or claim multi-file changes were atomic.
|
|
40
40
|
`;
|
|
41
|
+
/** Initialization strategy comes from the locked skill; report validation stays platform-owned. */
|
|
42
|
+
export const PARAMETER_AGENT_REPORT_PROTOCOL = PARAMETER_AGENT_PROTOCOL.slice(PARAMETER_AGENT_PROTOCOL.indexOf('Report JSON'));
|
|
@@ -17,6 +17,7 @@ async function taskDirectory(state, workspaceId, directoryName, root) {
|
|
|
17
17
|
}
|
|
18
18
|
const scope = [state.config.environment, state.config.userId, state.config.runtimeId, workspaceId, directoryName];
|
|
19
19
|
const directory = join(parent, parameterDigest(JSON.stringify(scope)));
|
|
20
|
+
// private
|
|
20
21
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
21
22
|
if ((await lstat(parent)).isSymbolicLink() || (await lstat(directory)).isSymbolicLink())
|
|
22
23
|
throw new ParameterAgentError('parameter_agent_state_invalid');
|
|
@@ -47,6 +48,7 @@ export async function writePrivateParameterTask(state, task, root) {
|
|
|
47
48
|
const directory = await taskDirectory(state, task.record.workspaceId, task.record.directoryName, root);
|
|
48
49
|
const record = parameterAgentTaskRecordSchema.parse(task.record);
|
|
49
50
|
const temporary = join(directory, `.${record.taskId}.${randomUUID()}.tmp`);
|
|
51
|
+
// private
|
|
50
52
|
await writeFile(temporary, JSON.stringify({ ...task, record }), { mode: 0o600 });
|
|
51
53
|
await rename(temporary, join(directory, `${record.taskId}.json`));
|
|
52
54
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { parameterAgentReportSchema, parameterAgentTaskRequestSchema, parameterAgentVerifyPayloadSchema,
|
|
2
|
+
import { parameterAgentReportSchema, parameterAgentTaskRequestSchema, parameterAgentVerifyPayloadSchema, parameterI18nCatalog, stableStringify, validateParameterChange, validateParameterI18nChange, } from '@playcraft/common/cloud-workspace/runtime';
|
|
3
3
|
import { readWorkspaceParameters } from './parameters.js';
|
|
4
4
|
import { revisionScope } from './registry.js';
|
|
5
5
|
import { runDeveloperCommand } from './developer-command.js';
|
|
6
|
-
import { PARAMETER_AGENT_PROTOCOL } from './parameter-agent-protocol.js';
|
|
6
|
+
import { PARAMETER_AGENT_PROTOCOL, PARAMETER_AGENT_REPORT_PROTOCOL } from './parameter-agent-protocol.js';
|
|
7
|
+
import { inspectParameterInitialization } from './parameter-initialization.js';
|
|
8
|
+
import { inspectParameterProductionCheck, prepareParameterProductionCheck } from './parameter-production.js';
|
|
7
9
|
import { ParameterAgentError, parameterDigest, parameterInputSnapshot, readParameterAgentFile } from './parameter-agent-inputs.js';
|
|
8
10
|
import { listPrivateParameterTaskIds, parameterTaskWorkspace, readPrivateParameterTask, writePrivateParameterTask } from './parameter-agent-store.js';
|
|
9
11
|
export { ParameterAgentError } from './parameter-agent-inputs.js';
|
|
@@ -58,14 +60,19 @@ async function inspectReport(root, task, snapshot) {
|
|
|
58
60
|
}
|
|
59
61
|
return report;
|
|
60
62
|
}
|
|
61
|
-
export async function prepareParameterAgentTask(state, operation, value, root) {
|
|
63
|
+
export async function prepareParameterAgentTask(state, operation, value, root, options = {}) {
|
|
62
64
|
const request = parameterAgentTaskRequestSchema.parse(value);
|
|
63
65
|
if (operation.kind !== 'session.turn' || operation.clientRequestId !== request.taskId)
|
|
64
66
|
throw new ParameterAgentError('parameter_agent_task_identity_invalid');
|
|
65
67
|
const owner = scope(state, operation);
|
|
68
|
+
const skill = request.action === 'initialize' ? options.initializationSkill : undefined;
|
|
69
|
+
if (request.action === 'initialize' && (!skill || !skill.entryKey || !/^[a-f0-9]{64}$/.test(skill.lockHash) || !skill.path))
|
|
70
|
+
throw new ParameterAgentError('parameter_initialization_skill_unavailable');
|
|
66
71
|
const existing = await readPrivateParameterTask(state, owner.workspaceId, owner.directoryName, request.taskId, root);
|
|
67
72
|
if (existing && (existing.record.sessionId !== owner.sessionId || !current(state, existing) || stableStringify(existing.request) !== stableStringify(request)))
|
|
68
73
|
throw new ParameterAgentError('parameter_agent_task_conflict');
|
|
74
|
+
if (existing && skill && (existing.record.initializationSkill?.entryKey !== skill.entryKey || existing.record.initializationSkill.lockHash !== skill.lockHash))
|
|
75
|
+
throw new ParameterAgentError('parameter_initialization_skill_changed');
|
|
69
76
|
const document = await readWorkspaceParameters(root, state.journal.fileRevisions(revisionScope(state, operation.workspaceId, owner.directoryName)));
|
|
70
77
|
if (request.sourceFingerprint && request.sourceFingerprint !== document.sourceFingerprint)
|
|
71
78
|
throw new ParameterAgentError('parameter_source_conflict');
|
|
@@ -84,7 +91,7 @@ export async function prepareParameterAgentTask(state, operation, value, root) {
|
|
|
84
91
|
const timestamp = now();
|
|
85
92
|
const task = existing ?? {
|
|
86
93
|
request, initialDigest: initial.digest, instanceToken: state.instanceToken,
|
|
87
|
-
record: { version: 1, taskId: request.taskId, action: request.action, ...(request.goal ? { goal: request.goal } : {}), ...owner, generation: state.config.generation, runtimeEpoch: state.runtimeEpoch, status: 'running', reportPath: reportPath(request.taskId), checks: [], createdAt: timestamp, updatedAt: timestamp },
|
|
94
|
+
record: { version: 1, taskId: request.taskId, action: request.action, ...(request.goal ? { goal: request.goal } : {}), ...(skill ? { initializationSkill: { entryKey: skill.entryKey, lockHash: skill.lockHash } } : {}), ...owner, generation: state.config.generation, runtimeEpoch: state.runtimeEpoch, status: 'running', reportPath: reportPath(request.taskId), checks: [], createdAt: timestamp, updatedAt: timestamp },
|
|
88
95
|
};
|
|
89
96
|
if (!existing) {
|
|
90
97
|
if ((await listPrivateParameterTaskIds(state, owner.workspaceId, owner.directoryName, root)).length >= 256)
|
|
@@ -92,9 +99,12 @@ export async function prepareParameterAgentTask(state, operation, value, root) {
|
|
|
92
99
|
await writePrivateParameterTask(state, task, root);
|
|
93
100
|
}
|
|
94
101
|
const instructions = [
|
|
95
|
-
|
|
102
|
+
...(skill ? [
|
|
103
|
+
`This initialization task explicitly selects the enabled, locked parameter skill ${JSON.stringify({ entryKey: skill.entryKey, lockHash: skill.lockHash, path: skill.path })}. Read that frozen SKILL.md and its referenced files before applying its integration workflow. Preserve the current execution permissions.`,
|
|
104
|
+
PARAMETER_AGENT_REPORT_PROTOCOL,
|
|
105
|
+
] : [PARAMETER_AGENT_PROTOCOL]),
|
|
96
106
|
`Platform task identity: ${JSON.stringify({ version: 1, taskId: request.taskId, action: request.action, reportPath: task.record.reportPath })}. Write only this exact report identity.`,
|
|
97
|
-
request.action === 'discover' ? 'Discovery is read-only for business source/schema/data/manifest. Write only the requested development report. Discover actual useful candidates and consumer evidence; do not implement them yet.' : 'Complete the requested integration using existing engineering tools: reuse an existing equivalent field or implement declaration, valid defaults, sparse data semantics, labels, real production consumers and meaningful parameter tests. Leave precise blocked work if incomplete.',
|
|
107
|
+
request.action === 'initialize' ? 'Apply the selected initialization skill to this current game and write its exact admission report. The platform will verify files and checks, then prepare the first preview separately. Do not self-declare a connected or live state.' : request.action === 'discover' ? 'Discovery is read-only for business source/schema/data/manifest. Write only the requested development report. Discover actual useful candidates and consumer evidence; do not implement them yet.' : 'Complete the requested integration using existing engineering tools: reuse an existing equivalent field or implement declaration, valid defaults, sparse data semantics, labels, real production consumers and meaningful parameter tests. Leave precise blocked work if incomplete.',
|
|
98
108
|
`User goal (task data): ${JSON.stringify(request.goal ?? '')}`,
|
|
99
109
|
...(selected ? [`Selected current candidate (reference data): ${JSON.stringify(selected)}`] : []),
|
|
100
110
|
'After finishing files, calculate current raw-file hashes and write the report. The platform will execute independent checks after this turn. Do not claim those checks already passed.',
|
|
@@ -207,7 +217,7 @@ export async function verifyParameterAgentTask(state, operation, payload, root,
|
|
|
207
217
|
const document = await readWorkspaceParameters(root, state.journal.fileRevisions(revisionScope(state, operation.workspaceId, owner.directoryName)));
|
|
208
218
|
if (document.status !== 'ready')
|
|
209
219
|
throw new ParameterAgentError('parameter_agent_parameters_not_ready');
|
|
210
|
-
const catalog =
|
|
220
|
+
const catalog = parameterI18nCatalog(document, 'en');
|
|
211
221
|
const implemented = report.candidates.filter(candidate => candidate.status === 'implemented');
|
|
212
222
|
if (!implemented.length || report.candidates.some(candidate => candidate.status !== 'implemented' || candidate.missing.length))
|
|
213
223
|
throw new ParameterAgentError('parameter_agent_integration_incomplete');
|
|
@@ -220,8 +230,14 @@ export async function verifyParameterAgentTask(state, operation, payload, root,
|
|
|
220
230
|
const field = catalog.find(entry => candidate.fieldPath && entry.key === candidate.fieldPath.join('.'));
|
|
221
231
|
if (!field || !field.editable || !field.canReset)
|
|
222
232
|
throw new ParameterAgentError('parameter_agent_field_not_ready');
|
|
223
|
-
|
|
233
|
+
const invalidValue = field.kind === 'i18n'
|
|
234
|
+
? validateParameterI18nChange(document, { fieldPath: field.fieldPath, locale: 'en', value: String(field.value ?? '') })
|
|
235
|
+
: field.hasOverride && validateParameterChange(document.schema, document.data, { fieldPath: field.fieldPath, value: field.value });
|
|
236
|
+
if (invalidValue)
|
|
224
237
|
throw new ParameterAgentError('parameter_agent_field_not_ready');
|
|
238
|
+
if (field.kind === 'i18n' && document.runtimeI18n &&
|
|
239
|
+
(!evidence.has(document.runtimeI18n.globalPath) || (document.runtimeI18n.themeHash && !evidence.has(document.runtimeI18n.themePath))))
|
|
240
|
+
throw new ParameterAgentError('parameter_agent_evidence_missing');
|
|
225
241
|
if (!candidate.consumers.length)
|
|
226
242
|
throw new ParameterAgentError('parameter_agent_consumer_missing');
|
|
227
243
|
}
|
|
@@ -230,11 +246,17 @@ export async function verifyParameterAgentTask(state, operation, payload, root,
|
|
|
230
246
|
if (typeof packageData.scripts?.[script] !== 'string' || !packageData.scripts[script].trim())
|
|
231
247
|
throw new ParameterAgentError('parameter_agent_check_script_missing');
|
|
232
248
|
}
|
|
249
|
+
const initialization = task.request.action === 'initialize'
|
|
250
|
+
? await inspectParameterInitialization(root, document, report) : undefined;
|
|
233
251
|
task.instanceToken = state.instanceToken;
|
|
234
|
-
await update(state, task, { status: 'checking', checks: [], error: undefined, receiptId: undefined, sourceDigest: before.digest }, root);
|
|
252
|
+
await update(state, task, { status: 'checking', checks: [], error: undefined, receiptId: undefined, productionCheck: undefined, sourceDigest: before.digest }, root);
|
|
235
253
|
const scripts = ['test:parameters', 'build', ...(typeof packageData.scripts?.typecheck === 'string' ? ['typecheck'] : [])];
|
|
254
|
+
let productionPlan;
|
|
255
|
+
let productionEvidence;
|
|
236
256
|
for (const script of scripts) {
|
|
237
257
|
assertActive();
|
|
258
|
+
if (initialization && script === 'build')
|
|
259
|
+
productionPlan = await prepareParameterProductionCheck(root);
|
|
238
260
|
const controller = new AbortController();
|
|
239
261
|
const abort = () => controller.abort();
|
|
240
262
|
options.signal?.addEventListener('abort', abort, { once: true });
|
|
@@ -269,16 +291,23 @@ export async function verifyParameterAgentTask(state, operation, payload, root,
|
|
|
269
291
|
await writePrivateParameterTask(state, task, root);
|
|
270
292
|
if (outcome.code !== 0 || outcome.timedOut || outcome.canceled)
|
|
271
293
|
throw new ParameterAgentError(outcome.timedOut ? 'parameter_agent_check_timeout' : outcome.canceled ? 'parameter_agent_check_canceled' : 'parameter_agent_check_failed');
|
|
294
|
+
if (productionPlan && script === 'build')
|
|
295
|
+
productionEvidence = await inspectParameterProductionCheck(productionPlan, assertActive);
|
|
272
296
|
}
|
|
273
297
|
assertActive();
|
|
274
298
|
const after = await parameterInputSnapshot(root);
|
|
275
299
|
if (after.digest !== before.digest || parameterDigest(await readParameterAgentFile(root, task.record.reportPath, 1024 * 1024)) !== task.reportHash)
|
|
276
300
|
throw new ParameterAgentError('parameter_agent_inputs_changed');
|
|
277
|
-
|
|
301
|
+
if (initialization && initialization !== await inspectParameterInitialization(root, document, report))
|
|
302
|
+
throw new ParameterAgentError('parameter_agent_inputs_changed');
|
|
303
|
+
if (productionPlan && stableStringify(productionEvidence) !== stableStringify(await inspectParameterProductionCheck(productionPlan, assertActive)))
|
|
304
|
+
throw new ParameterAgentError('parameter_agent_production_outputs_not_ready');
|
|
305
|
+
return update(state, task, { status: 'verified', sourceDigest: after.digest, receiptId: randomUUID(), error: undefined,
|
|
306
|
+
...(productionEvidence ? { productionCheck: { ...productionEvidence, checkedAt: now() } } : {}) }, root);
|
|
278
307
|
}
|
|
279
308
|
catch (cause) {
|
|
280
309
|
const code = errorCode(cause);
|
|
281
310
|
const status = code === 'ENOENT' ? 'missing' : /stale|changed|expired/.test(code) ? 'stale' : /(?:not_ready|incomplete|consumer_missing|script_missing|evidence_missing)$/.test(code) ? 'needs-verification' : /report_|evidence_invalid|path_invalid/.test(code) ? 'invalid' : 'failed';
|
|
282
|
-
return update(state, task, { status, error: code === 'ENOENT' ? 'parameter_agent_report_missing' : code, receiptId: undefined }, root);
|
|
311
|
+
return update(state, task, { status, error: code === 'ENOENT' ? 'parameter_agent_report_missing' : code, receiptId: undefined, productionCheck: undefined }, root);
|
|
283
312
|
}
|
|
284
313
|
}
|
|
@@ -159,6 +159,7 @@ export async function writeParameterAssetRegistration(root, plan) {
|
|
|
159
159
|
throw new ParameterAssetError('parameter_asset_index_conflict');
|
|
160
160
|
const parent = await lstat(dirname(target));
|
|
161
161
|
const bytes = Buffer.from(plan.content);
|
|
162
|
+
// private
|
|
162
163
|
staged = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
163
164
|
// Metadata restoration must succeed before publication; never downgrade to a partial text write.
|
|
164
165
|
await staged.chown(before.uid, before.gid);
|