@playcraft/cli 0.1.65 → 0.1.67
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 +4 -2
- package/dist/assets/messages.js +2 -0
- package/dist/audio-source/messages.js +35 -0
- package/dist/commands/assets.js +1 -1
- package/dist/commands/audio-source.js +109 -0
- package/dist/commands/audio.js +11 -4
- package/dist/commands/build.js +4 -2
- package/dist/commands/tools-context.js +46 -0
- package/dist/commands/tools.js +2 -0
- package/dist/model-adaptation/project.js +1 -1
- package/dist/project-skills/builtin-import.js +69 -22
- package/dist/project-skills/commands.js +11 -2
- package/dist/project-skills/lifecycle.js +1 -1
- package/dist/project-skills/local-store.js +90 -31
- package/dist/project-skills/messages.js +16 -0
- package/dist/project-skills/migrate-lock.js +166 -0
- package/dist/project-skills/remote-cache.js +36 -5
- package/dist/utils/agent-api-client.js +18 -1
- package/dist/utils/agent-generation-provenance.js +8 -2
- package/dist/utils/audio-export.js +249 -0
- package/dist/utils/audio-processor.js +22 -99
- package/dist/utils/execution-context-bridge.js +28 -0
- package/dist/workspace-runtime/batch/control.js +13 -2
- package/dist/workspace-runtime/batch/generate.js +43 -2
- package/dist/workspace-runtime/batch/prepare.js +4 -0
- package/dist/workspace-runtime/batch/skill-artifacts.js +103 -0
- package/dist/workspace-runtime/batch/source-capture.js +6 -8
- package/dist/workspace-runtime/codex/app-server.js +26 -2
- package/dist/workspace-runtime/codex/execution-context-broker.js +61 -0
- package/dist/workspace-runtime/codex/execution-context-tools.js +37 -0
- package/dist/workspace-runtime/codex/generation-provenance-broker.js +9 -6
- package/dist/workspace-runtime/codex/level-editor-tools.js +18 -0
- package/dist/workspace-runtime/codex/model-broker.js +15 -2
- package/dist/workspace-runtime/codex/scheduled-task-tools.js +1 -1
- package/dist/workspace-runtime/previews/finalize.js +1 -1
- package/dist/workspace-runtime/previews/output.js +4 -1
- package/dist/workspace-runtime/previews/publish.js +5 -2
- package/dist/workspace-runtime/previews/runtime-preview.js +38 -3
- package/dist/workspace-runtime/serve.js +6 -0
- package/dist/workspace-runtime/server/accept-operation.js +85 -13
- package/dist/workspace-runtime/server/audio-assets.js +211 -0
- package/dist/workspace-runtime/server/audio-export.js +136 -0
- package/dist/workspace-runtime/server/directory-access.js +222 -67
- package/dist/workspace-runtime/server/dispatch.js +252 -29
- package/dist/workspace-runtime/server/http.js +97 -3
- package/dist/workspace-runtime/server/operation-record.js +1 -1
- package/dist/workspace-runtime/workspaces/artifact-files.js +10 -3
- package/dist/workspace-runtime/workspaces/artifacts.js +47 -6
- package/dist/workspace-runtime/workspaces/audio-assets/artifact-package.js +104 -0
- package/dist/workspace-runtime/workspaces/audio-assets/binding-usage.js +71 -0
- package/dist/workspace-runtime/workspaces/audio-assets/build-inputs.js +76 -0
- package/dist/workspace-runtime/workspaces/audio-assets/render-worker.js +117 -0
- package/dist/workspace-runtime/workspaces/audio-assets/renderer.js +128 -0
- package/dist/workspace-runtime/workspaces/audio-assets/representations.js +73 -0
- package/dist/workspace-runtime/workspaces/audio-assets/service.js +327 -0
- package/dist/workspace-runtime/workspaces/audio-assets/storage.js +125 -0
- package/dist/workspace-runtime/workspaces/build-isolation.js +2 -0
- package/dist/workspace-runtime/workspaces/composer-context.js +40 -1
- package/dist/workspace-runtime/workspaces/empty-template.js +1 -1
- package/dist/workspace-runtime/workspaces/execution-context.js +24 -0
- package/dist/workspace-runtime/workspaces/files.js +3 -5
- package/dist/workspace-runtime/workspaces/level-editor-agent-store.js +143 -0
- package/dist/workspace-runtime/workspaces/level-editor-agent.js +410 -0
- package/dist/workspace-runtime/workspaces/level-editor-atomic.js +424 -0
- package/dist/workspace-runtime/workspaces/level-editor-extensions.js +140 -0
- package/dist/workspace-runtime/workspaces/level-editor-format.js +75 -0
- package/dist/workspace-runtime/workspaces/level-editor-history-store.js +250 -0
- package/dist/workspace-runtime/workspaces/level-editor-json5.js +132 -0
- package/dist/workspace-runtime/workspaces/level-editor-requests.js +724 -0
- package/dist/workspace-runtime/workspaces/level-editor.js +454 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +62 -22
- package/dist/workspace-runtime/workspaces/parameter-production.js +1 -1
- package/dist/workspace-runtime/workspaces/remix-reference-skill.js +4 -3
- package/dist/workspace-runtime/workspaces/skill-artifact-loader.js +19 -5
- package/dist/workspace-runtime/workspaces/skills.js +48 -13
- package/package.json +6 -4
- package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
- package/project-template/.claude/skills/playcraft-level-editor/SKILL.md +34 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/authoring-contract.json +3475 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/extensions.md +37 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/preview.md +15 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/protocol-v1.md +35 -0
- package/project-template/vite.config.ts +1 -1
- package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
- package/project-template-v2/.playcraft/skills.lock.json +9 -2
- package/project-template-v2/vite.config.ts +1 -1
- package/project-templates/empty/AGENTS.md +8 -4
- package/project-templates/phaser/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
- package/project-templates/phaser/vite.config.ts +1 -1
- package/project-templates/playable-template-phaser/vite.config.ts +1 -1
- package/project-templates/threejs/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
- package/project-templates/threejs/vite.config.ts +1 -1
- package/templates/preview-runtime/v1/README.md +12 -0
- package/templates/preview-runtime/v1/build-preview.mjs +17 -8
- package/templates/preview-runtime/v1/manifest.json +9 -6
- package/templates/preview-runtime/v1/release-guard.mjs +1 -1
- package/templates/preview-runtime/v1/runtime.d.mts +1 -3
- package/templates/preview-runtime/v1/runtime.d.ts +1 -3
- package/templates/preview-runtime/v1/runtime.mjs +1142 -257
- package/templates/preview-runtime/v1/types/host.d.ts +29 -0
- package/templates/preview-runtime/v1/types/index.d.ts +6 -0
- package/templates/preview-runtime/v1/types/level-editor.d.ts +123 -0
|
@@ -57,19 +57,17 @@ export async function readSourceFiles(root) {
|
|
|
57
57
|
}
|
|
58
58
|
return files;
|
|
59
59
|
}
|
|
60
|
-
/**
|
|
61
|
-
* Drops paths whose bytes change between two reads. A managed writer that could not be stopped
|
|
62
|
-
* does not become part of the frozen input.
|
|
63
|
-
*/
|
|
60
|
+
/** A changing source must not silently lose files from the captured input. */
|
|
64
61
|
export async function filterUnstableFiles(root, files) {
|
|
65
62
|
const again = await readSourceFiles(root);
|
|
66
|
-
|
|
63
|
+
if (again.size !== files.size)
|
|
64
|
+
throw new Error('batch_source_changed');
|
|
67
65
|
for (const [path, file] of files) {
|
|
68
66
|
const next = again.get(path);
|
|
69
|
-
if (next
|
|
70
|
-
|
|
67
|
+
if (!next || !next.body.equals(file.body) || next.mode !== file.mode)
|
|
68
|
+
throw new Error('batch_source_changed');
|
|
71
69
|
}
|
|
72
|
-
return
|
|
70
|
+
return files;
|
|
73
71
|
}
|
|
74
72
|
/** Independent root commit. The source branch, index and config are never written. */
|
|
75
73
|
export async function captureIndependentSource(source, destination) {
|
|
@@ -21,6 +21,8 @@ import { WORKSPACE_DELIVERY_TOOLS, WorkspaceDeliveryTools, isWorkspaceDeliveryTo
|
|
|
21
21
|
import { WORKSPACE_UI_INSTRUCTIONS } from './ui-instructions.js';
|
|
22
22
|
import { WORKSPACE_SCHEDULED_TASK_TOOLS, WorkspaceScheduledTaskTools, isWorkspaceScheduledTaskTool } from './scheduled-task-tools.js';
|
|
23
23
|
import { PROCEDURAL_3D_TOOL, ProductionSkillTools } from './production-skill-tools.js';
|
|
24
|
+
import { WORKSPACE_EXECUTION_CONTEXT_TOOL, WorkspaceExecutionContextTools } from './execution-context-tools.js';
|
|
25
|
+
import { WORKSPACE_LEVEL_EDITOR_TOOL, requestLevelEditorTool } from './level-editor-tools.js';
|
|
24
26
|
function withModelShell(config, modelConfigId) {
|
|
25
27
|
if (!modelConfigId || !/^[0-9a-f-]{36}$/i.test(modelConfigId))
|
|
26
28
|
return config;
|
|
@@ -32,7 +34,7 @@ const ENGINEERING_BATCH_INSTRUCTIONS = [
|
|
|
32
34
|
'When the user requests multiple independent engineering variants, create a real platform batch using playcraft tools create-batch --prompt <the complete request, including constraints established in this conversation> --workspace-id <the trusted batch request workspaceId> --source-directory <the trusted batch request sourceDirectoryName> --client-request-id <the trusted batch request clientRequestId> --json.',
|
|
33
35
|
'Distinguish independent variants from multiple assets, levels or themes within one engineering project. Use the current session directory supplied by the runtime as the batch source; do not infer it from a preview or another conversation.',
|
|
34
36
|
'The platform freezes the current source and generates each planned variant in its own Remix directory. Submission authorizes automatic generation without another prompt approval. Keep the source unchanged for this batch; do not replace independent variants with theme switching in the source.',
|
|
35
|
-
'
|
|
37
|
+
'Creation means queued; use playcraft tools get-batch --batch-id <the actual batch identifier> for results. On an uncertain creation, reuse the same clientRequestId. Report creation failures without falling back to editing the source.',
|
|
36
38
|
].join('\n');
|
|
37
39
|
const MEMBER_EXECUTION_INSTRUCTIONS = [
|
|
38
40
|
'You are implementing one assigned variant in an independent Remix directory which the platform has already created.',
|
|
@@ -197,11 +199,14 @@ export function createCodexAppServerAdapter(options) {
|
|
|
197
199
|
proposalId, sessionId: threads.get(source.threadId)?.sessionId, threadId: source.threadId, turnId: source.turnId,
|
|
198
200
|
}))) : undefined;
|
|
199
201
|
const productionSkill = options.productionSkill ? new ProductionSkillTools(options.productionSkill) : undefined;
|
|
202
|
+
const executionContext = options.executionContext ? new WorkspaceExecutionContextTools(options.executionContext) : undefined;
|
|
200
203
|
const dynamicTools = [
|
|
201
204
|
...(options.workspaceUi ? WORKSPACE_UI_TOOLS : []),
|
|
202
205
|
...(deliveryTools ? WORKSPACE_DELIVERY_TOOLS : []),
|
|
203
206
|
...(scheduledTaskTools ? WORKSPACE_SCHEDULED_TASK_TOOLS : []),
|
|
204
207
|
...(productionSkill ? [PROCEDURAL_3D_TOOL] : []),
|
|
208
|
+
...(executionContext ? [WORKSPACE_EXECUTION_CONTEXT_TOOL] : []),
|
|
209
|
+
...(options.levelEditorTasks ? [WORKSPACE_LEVEL_EDITOR_TOOL] : []),
|
|
205
210
|
];
|
|
206
211
|
function emitCollected(event) {
|
|
207
212
|
for (const listener of collected)
|
|
@@ -310,7 +315,6 @@ export function createCodexAppServerAdapter(options) {
|
|
|
310
315
|
'You coordinate PlayCraft batches. The workspace engineering directories are read-only in this conversation.',
|
|
311
316
|
`Workspace identity: ${scope}. Engineering source root: ${JSON.stringify(cwd)}. Default source directory: basic.`,
|
|
312
317
|
'For a request to create multiple independent variants, create a real platform batch using playcraft tools create-batch --prompt <the complete user request> --workspace-id <workspace identity> --source-directory <basic or the explicitly selected remix directory> --client-request-id <the current batch request context identity> --json.',
|
|
313
|
-
'When the batch request context provides teamId, pass it as --team-id; the platform verifies membership before creating project ownership for a standalone workspace.',
|
|
314
318
|
'The platform plans differences, freezes the selected source and generates independent Remix engineering directories. It automatically starts generation; no browser approval is required. Preserve the source files and source Git history.',
|
|
315
319
|
'Do not implement a batch as theme switching inside one engineering directory. Do not generate or repair engineering in this coordinator conversation; the platform production tasks own that work.',
|
|
316
320
|
'If the user asks for status, use playcraft tools get-batch --batch-id <the actual batch identifier>. A successful create-batch response means queued, not completed. Report the actual identifier and current status; never claim generation completed without platform results.',
|
|
@@ -633,6 +637,26 @@ export function createCodexAppServerAdapter(options) {
|
|
|
633
637
|
}
|
|
634
638
|
if (method === 'item/tool/call') {
|
|
635
639
|
const thread = typeof payload.threadId === 'string' ? threads.get(payload.threadId) : undefined;
|
|
640
|
+
if (executionContext && payload.tool === WORKSPACE_EXECUTION_CONTEXT_TOOL.name) {
|
|
641
|
+
const current = () => Boolean(thread && threads.get(String(payload.threadId)) === thread && thread.turnActive
|
|
642
|
+
&& payload.turnId === thread.lastTurnId && !childParents.has(String(payload.threadId)));
|
|
643
|
+
setTimeout(() => {
|
|
644
|
+
void executionContext.request(id, payload, nativeClient, thread?.cwd ? {
|
|
645
|
+
cwd: thread.cwd, sessionId: thread.sessionId, current,
|
|
646
|
+
} : undefined);
|
|
647
|
+
}, 0);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (options.levelEditorTasks && payload.tool === WORKSPACE_LEVEL_EDITOR_TOOL.name) {
|
|
651
|
+
const trusted = thread?.turnActive && thread.cwd && thread.activeMode === 'execute' && !childParents.has(String(payload.threadId))
|
|
652
|
+
&& !options.fixedExecution && !options.trustedExecution?.(thread.sessionId) && thread.permissionMode !== 'read-only'
|
|
653
|
+
&& !thread.readOnlyConfirmed && payload.turnId === thread.lastTurnId && typeof payload.threadId === 'string'
|
|
654
|
+
&& typeof payload.turnId === 'string' && typeof payload.callId === 'string' && payload.callId.length > 0;
|
|
655
|
+
void requestLevelEditorTool(options.levelEditorTasks, id, payload, nativeClient, trusted ? {
|
|
656
|
+
sessionId: thread.sessionId, cwd: thread.cwd, threadId: payload.threadId, turnId: payload.turnId, toolCallId: payload.callId,
|
|
657
|
+
} : undefined);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
636
660
|
if (scheduledTaskTools && isWorkspaceScheduledTaskTool(payload.tool)) {
|
|
637
661
|
const threadId = String(payload.threadId ?? ''), turnId = String(payload.turnId ?? '');
|
|
638
662
|
const current = () => {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { workspaceExecutionContextQuerySchema } from '@playcraft/common/cloud-workspace/runtime';
|
|
2
|
+
import { executionContextToolHandler } from '../workspaces/execution-context.js';
|
|
3
|
+
/** Reads use the existing authenticated per-session broker route, without trusting shell scope fields. */
|
|
4
|
+
export class WorkspaceExecutionContextBroker {
|
|
5
|
+
state;
|
|
6
|
+
constructor(state) {
|
|
7
|
+
this.state = state;
|
|
8
|
+
}
|
|
9
|
+
current(source, valid) {
|
|
10
|
+
const state = this.state, session = state.journal.getSession(source.sessionId);
|
|
11
|
+
const operations = state.journal.listOperations().map(item => item.operation).filter(operation => ['session.turn', 'session.review', 'session.compact'].includes(operation.kind) && operation.sessionId === source.sessionId
|
|
12
|
+
&& operation.clientRequestId === source.clientRequestId
|
|
13
|
+
&& operation.generation === state.config.generation && operation.runtimeEpoch === state.runtimeEpoch
|
|
14
|
+
&& ['dispatched', 'accepted'].includes(operation.status));
|
|
15
|
+
const operation = operations.length === 1 ? operations[0] : undefined;
|
|
16
|
+
return Boolean(valid() && !state.journal.destroyed() && operation?.nativeThreadId && operation.nativeTurnId
|
|
17
|
+
&& session?.threadId === operation.nativeThreadId && session?.activeTurnId === operation.nativeTurnId
|
|
18
|
+
&& session?.cwd === source.cwd && session.workspaceId === operation.workspaceId
|
|
19
|
+
&& (session.directoryName ?? 'basic') === operation.directoryName
|
|
20
|
+
&& !session.turnBlock && ['running', 'waiting_for_user'].includes(session.turnStatus ?? ''));
|
|
21
|
+
}
|
|
22
|
+
async forward(request, response, source, valid) {
|
|
23
|
+
const reply = (status, value) => {
|
|
24
|
+
response.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
25
|
+
response.end(JSON.stringify(value));
|
|
26
|
+
};
|
|
27
|
+
try {
|
|
28
|
+
if (request.socket.remoteAddress !== '127.0.0.1' || !this.current(source, valid))
|
|
29
|
+
return reply(403, { code: 'execution_context_access_denied' });
|
|
30
|
+
let text = '';
|
|
31
|
+
for await (const chunk of request) {
|
|
32
|
+
text += chunk.toString();
|
|
33
|
+
if (Buffer.byteLength(text) > 4096)
|
|
34
|
+
return reply(400, { code: 'execution_context_invalid_input' });
|
|
35
|
+
}
|
|
36
|
+
let raw;
|
|
37
|
+
try {
|
|
38
|
+
raw = JSON.parse(text);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return reply(400, { code: 'execution_context_invalid_input' });
|
|
42
|
+
}
|
|
43
|
+
const parsed = workspaceExecutionContextQuerySchema.safeParse(raw);
|
|
44
|
+
if (!parsed.success)
|
|
45
|
+
return reply(400, { code: 'execution_context_invalid_input' });
|
|
46
|
+
if (!this.current(source, valid))
|
|
47
|
+
return reply(403, { code: 'execution_context_access_denied' });
|
|
48
|
+
const result = await executionContextToolHandler(this.state)(parsed.data, source);
|
|
49
|
+
if (!this.current(source, valid))
|
|
50
|
+
return reply(403, { code: 'execution_context_access_denied' });
|
|
51
|
+
reply(200, result);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const denied = error instanceof Error && ['execution_context_access_denied', 'execution_context_scope_unavailable'].includes(error.message);
|
|
55
|
+
if (!response.headersSent)
|
|
56
|
+
reply(denied ? 403 : 503, { code: denied ? 'execution_context_access_denied' : 'execution_context_unavailable' });
|
|
57
|
+
else
|
|
58
|
+
response.destroy();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { workspaceExecutionContextQuerySchema, workspaceExecutionContextReadSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
2
|
+
import { rejectTool, toolContent } from './tool-response.js';
|
|
3
|
+
export const WORKSPACE_EXECUTION_CONTEXT_TOOL = {
|
|
4
|
+
type: 'function', name: 'playcraft_get_execution_context',
|
|
5
|
+
description: 'Read selected current user, team, workspace or clock information for this execution. Scope and permissions come from the runtime. Request only needed fields. Omit knownRevision when the corresponding information is no longer visible, including after compaction. Clock reads always sample the current time.',
|
|
6
|
+
inputSchema: { type: 'object', properties: {
|
|
7
|
+
fields: { type: 'array', items: { type: 'string', enum: ['user', 'team', 'workspace', 'clock'] },
|
|
8
|
+
minItems: 1, maxItems: 4, uniqueItems: true },
|
|
9
|
+
knownRevision: { type: 'string', pattern: '^[a-f0-9]{64}$',
|
|
10
|
+
description: 'A revision from a previous read of the same fields, only when those values remain visible.' },
|
|
11
|
+
}, required: ['fields'], additionalProperties: false },
|
|
12
|
+
};
|
|
13
|
+
/** Explicit reads always reach the authority again, including conditional and repeated reads. */
|
|
14
|
+
export class WorkspaceExecutionContextTools {
|
|
15
|
+
handler;
|
|
16
|
+
constructor(handler) {
|
|
17
|
+
this.handler = handler;
|
|
18
|
+
}
|
|
19
|
+
async request(requestId, payload, client, context) {
|
|
20
|
+
if (!context?.current() || payload.namespace || payload.tool !== WORKSPACE_EXECUTION_CONTEXT_TOOL.name)
|
|
21
|
+
return rejectTool(client, requestId, 'execution_context_source_unavailable');
|
|
22
|
+
const parsed = workspaceExecutionContextQuerySchema.safeParse(payload.arguments);
|
|
23
|
+
if (!parsed.success)
|
|
24
|
+
return rejectTool(client, requestId, 'execution_context_invalid_input');
|
|
25
|
+
try {
|
|
26
|
+
const value = workspaceExecutionContextReadSchema.parse(await this.handler(parsed.data, { cwd: context.cwd, sessionId: context.sessionId }));
|
|
27
|
+
if (!context.current())
|
|
28
|
+
return rejectTool(client, requestId, 'execution_context_source_unavailable');
|
|
29
|
+
await client.respond(requestId, { success: true, contentItems: toolContent(value) });
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
const allowed = new Set(['execution_context_access_denied', 'execution_context_scope_unavailable']);
|
|
33
|
+
await rejectTool(client, requestId, error instanceof Error && allowed.has(error.message)
|
|
34
|
+
? error.message : 'execution_context_unavailable');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -13,11 +13,11 @@ export class WorkspaceGenerationProvenanceBroker {
|
|
|
13
13
|
const root = this.state.native?.executionProcess?.();
|
|
14
14
|
return socketPath && root ? prepareLauncherOrigin({ socketPath, rootPid: root.pid, runtimeEpoch: this.state.runtimeEpoch }) : undefined;
|
|
15
15
|
}
|
|
16
|
-
operation(context,
|
|
16
|
+
operation(context, scoped = false) {
|
|
17
17
|
let active = this.state.journal.listOperations().map(item => item.operation).filter(operation => ['session.turn', 'session.review', 'session.compact'].includes(operation.kind)
|
|
18
18
|
&& ['dispatched', 'accepted', 'uncertain'].includes(operation.status)
|
|
19
19
|
&& operation.generation === this.state.config.generation && operation.runtimeEpoch === this.state.runtimeEpoch);
|
|
20
|
-
if (
|
|
20
|
+
if (scoped) {
|
|
21
21
|
const owned = active.find(operation => operation.sessionId === context.sessionId && operation.clientRequestId === context.clientRequestId);
|
|
22
22
|
if (!owned)
|
|
23
23
|
return undefined;
|
|
@@ -50,13 +50,16 @@ export class WorkspaceGenerationProvenanceBroker {
|
|
|
50
50
|
const parsed = requestSchema.safeParse(JSON.parse(text));
|
|
51
51
|
if (!parsed.success)
|
|
52
52
|
return deny();
|
|
53
|
-
const
|
|
54
|
-
|
|
53
|
+
const audio = parsed.data.toolPath === '/agent/tools/workspace-audio';
|
|
54
|
+
if (audio && context.readOnly)
|
|
55
|
+
return deny();
|
|
56
|
+
const scoped = audio || parsed.data.toolPath === '/agent/tools/create-batch';
|
|
57
|
+
const operation = this.operation(context, scoped);
|
|
55
58
|
if (!operation || !await verifyLauncherOrigin({ socketPath, boundaryId: context.boundaryId,
|
|
56
59
|
runtimeEpoch: this.state.runtimeEpoch, serverPort: request.socket.localPort, clientPort: request.socket.remotePort, cwd: context.originCwd ?? context.cwd }))
|
|
57
60
|
return deny();
|
|
58
61
|
// The socket lookup may take time. Never attest a turn which ended or changed meanwhile.
|
|
59
|
-
if (!valid() || this.operation(context,
|
|
62
|
+
if (!valid() || this.operation(context, scoped)?.operationId !== operation.operationId)
|
|
60
63
|
return deny();
|
|
61
64
|
const config = this.state.config;
|
|
62
65
|
const input = agentGenerationProvenanceRequestSchema.parse({ ...parsed.data,
|
|
@@ -74,7 +77,7 @@ export class WorkspaceGenerationProvenanceBroker {
|
|
|
74
77
|
return deny();
|
|
75
78
|
}
|
|
76
79
|
const value = z.object({ proof: z.string().min(1).max(8192) }).strict().parse(await upstream.json());
|
|
77
|
-
if (!valid() || this.operation(context,
|
|
80
|
+
if (!valid() || this.operation(context, scoped)?.operationId !== operation.operationId)
|
|
78
81
|
return deny();
|
|
79
82
|
response.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
80
83
|
response.end(JSON.stringify(value));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { levelEditorTaskIntentSchema } from '@playcraft/common/cloud-workspace/runtime';
|
|
2
|
+
import { rejectTool, toolContent } from './tool-response.js';
|
|
3
|
+
export const WORKSPACE_LEVEL_EDITOR_TOOL = { type: 'function', name: 'playcraft_request_level_editor_task',
|
|
4
|
+
description: 'Request creation, evolution or repair of this project’s declarative level editor. This records a request only. Finish this turn normally without doing the same initialization yourself; the platform then runs the locked level-editor skill in a managed follow-up turn in this conversation. A requested result is not a completed editor.',
|
|
5
|
+
inputSchema: { type: 'object', properties: {
|
|
6
|
+
action: { type: 'string', enum: ['initialize', 'evolve', 'repair'] }, goal: { type: 'string', minLength: 1, maxLength: 4000 }, editorId: { type: 'string' },
|
|
7
|
+
}, required: ['action', 'goal'], additionalProperties: false } };
|
|
8
|
+
export async function requestLevelEditorTool(handler, id, payload, client, source) {
|
|
9
|
+
const parsed = levelEditorTaskIntentSchema.safeParse(payload.arguments);
|
|
10
|
+
if (!source || !parsed.success || payload.namespace)
|
|
11
|
+
return rejectTool(client, id, 'level_editor_request_unavailable');
|
|
12
|
+
try {
|
|
13
|
+
await client.respond(id, { success: true, contentItems: toolContent(await handler(source, parsed.data)) });
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
await rejectTool(client, id, error instanceof Error && /^level_editor_/.test(error.message) ? error.message : 'level_editor_request_failed');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -8,6 +8,7 @@ import { workspaceContextFromRoot } from '../workspaces/registry.js';
|
|
|
8
8
|
import { PLATFORM_ROUTES, modelResponsesPath, platformEndpoint } from '@playcraft/common/cloud-workspace/runtime';
|
|
9
9
|
import { WorkspaceGenerationProvenanceBroker } from './generation-provenance-broker.js';
|
|
10
10
|
import { WorkspaceRemixReferencesBroker } from './remix-references-broker.js';
|
|
11
|
+
import { WorkspaceExecutionContextBroker } from './execution-context-broker.js';
|
|
11
12
|
const credentialSchema = z.object({
|
|
12
13
|
token: z.string().min(1), expiresAt: z.string().datetime(),
|
|
13
14
|
configId: z.string().uuid(), model: z.string().min(1).max(128),
|
|
@@ -30,10 +31,12 @@ export class WorkspaceModelBroker {
|
|
|
30
31
|
sessionRequests = new Map();
|
|
31
32
|
generation;
|
|
32
33
|
references;
|
|
34
|
+
executionContext;
|
|
33
35
|
constructor(state) {
|
|
34
36
|
this.state = state;
|
|
35
37
|
this.generation = new WorkspaceGenerationProvenanceBroker(state);
|
|
36
38
|
this.references = new WorkspaceRemixReferencesBroker(state);
|
|
39
|
+
this.executionContext = new WorkspaceExecutionContextBroker(state);
|
|
37
40
|
}
|
|
38
41
|
async start() {
|
|
39
42
|
if (this.server)
|
|
@@ -182,14 +185,15 @@ export class WorkspaceModelBroker {
|
|
|
182
185
|
const cancel = () => abort.abort();
|
|
183
186
|
response.once('close', cancel);
|
|
184
187
|
try {
|
|
185
|
-
const match = /^\/([0-9a-f-]{36}(?:\/[0-9a-f]{32})?)\/(responses|generation-provenance|remix-references)$/.exec(request.url ?? '');
|
|
188
|
+
const match = /^\/([0-9a-f-]{36}(?:\/[0-9a-f]{32})?)\/(responses|generation-provenance|remix-references|execution-context)$/.exec(request.url ?? '');
|
|
186
189
|
const route = match ? this.routes.get(match[1]) : undefined;
|
|
187
190
|
const selected = route?.selected;
|
|
188
191
|
const entry = selected?.entry;
|
|
189
192
|
const presented = Buffer.from(request.headers.authorization ?? '');
|
|
190
193
|
const generationRequest = match?.[2] === 'generation-provenance';
|
|
194
|
+
const contextRequest = match?.[2] === 'execution-context';
|
|
191
195
|
const referencesRequest = match?.[2] === 'remix-references';
|
|
192
|
-
const expected = Buffer.from(route ? `Bearer ${generationRequest ? route.generationCapability
|
|
196
|
+
const expected = Buffer.from(route ? `Bearer ${generationRequest || contextRequest ? route.generationCapability
|
|
193
197
|
: referencesRequest ? route.referencesCapability : route.capability}` : '');
|
|
194
198
|
if (request.method !== 'POST' || !entry || !expected.length || presented.length !== expected.length
|
|
195
199
|
|| !timingSafeEqual(presented, expected) || !this.valid(entry)) {
|
|
@@ -206,6 +210,15 @@ export class WorkspaceModelBroker {
|
|
|
206
210
|
await this.generation.forward(request, response, selected.feedback, () => this.valid(entry) && route?.selected === selected);
|
|
207
211
|
return;
|
|
208
212
|
}
|
|
213
|
+
if (contextRequest) {
|
|
214
|
+
if (!selected?.feedback) {
|
|
215
|
+
response.writeHead(403);
|
|
216
|
+
response.end();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
await this.executionContext.forward(request, response, selected.feedback, () => this.valid(entry) && route?.selected === selected);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
209
222
|
if (referencesRequest) {
|
|
210
223
|
if (!selected?.feedback) {
|
|
211
224
|
response.writeHead(403);
|
|
@@ -3,7 +3,7 @@ import { scheduledTaskProposalDraftSchema, scheduledTaskProposalDraftJsonSchema
|
|
|
3
3
|
import { rejectTool, toolContent } from './tool-response.js';
|
|
4
4
|
export const WORKSPACE_SCHEDULED_TASK_TOOLS = [
|
|
5
5
|
{ type: 'function', name: 'playcraft_get_scheduled_task_context',
|
|
6
|
-
description: 'Read server time, scheduling defaults, and tasks actually related to this conversation.
|
|
6
|
+
description: 'Read current server time, the resolved workspace or user timezone, scheduling defaults, and tasks actually related to this conversation. Use the returned timeZone for local scheduling unless the user explicitly requests another timezone. When several tasks match, ask the user which task to change. This tool never changes a task.',
|
|
7
7
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
|
|
8
8
|
{ type: 'function', name: 'playcraft_propose_scheduled_task',
|
|
9
9
|
description: 'Prepare a scheduled-task creation or change for the user to review in a persistent confirmation card. Read scheduling context first. This does not create or change a task: only the user can confirm the card. A pending_confirmation result must never be reported as an applied task.',
|
|
@@ -59,6 +59,6 @@ export async function finalizeWorkspacePlayable(root, built, tool, options = {})
|
|
|
59
59
|
throw new Error('preview_source_changed');
|
|
60
60
|
const current = await readWorkspacePlayable(root).catch(() => undefined);
|
|
61
61
|
if (!current?.bytes.equals(bytes))
|
|
62
|
-
await saveWorkspacePlayableOutput(root, bytes);
|
|
62
|
+
await saveWorkspacePlayableOutput(root, bytes, options);
|
|
63
63
|
await captureGeneratedArtifact(WORKSPACE_PLAYABLE_OUTPUT_PATH, tool, {}, root, createHash('sha256').update(bytes).digest('hex'), options.source);
|
|
64
64
|
}
|
|
@@ -3,7 +3,7 @@ import { chmod, lstat, mkdir, realpath, rename, rm, writeFile } from 'node:fs/pr
|
|
|
3
3
|
import { dirname, join, relative } from 'node:path';
|
|
4
4
|
import { WORKSPACE_PLAYABLE_OUTPUT_PATH } from '@playcraft/common/cloud-workspace/runtime';
|
|
5
5
|
/** Persist only validated, self-contained build bytes; unrelated delivery files are preserved. */
|
|
6
|
-
export async function saveWorkspacePlayableOutput(root, bytes) {
|
|
6
|
+
export async function saveWorkspacePlayableOutput(root, bytes, options = {}) {
|
|
7
7
|
const workspace = await realpath(root);
|
|
8
8
|
let directory = workspace;
|
|
9
9
|
for (const segment of dirname(WORKSPACE_PLAYABLE_OUTPUT_PATH).split('/')) {
|
|
@@ -30,6 +30,9 @@ export async function saveWorkspacePlayableOutput(root, bytes) {
|
|
|
30
30
|
return undefined; throw error; });
|
|
31
31
|
if (stat && (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1))
|
|
32
32
|
throw new Error('preview_build_failed');
|
|
33
|
+
const signature = stat ? JSON.stringify([stat.dev, stat.ino, stat.size, stat.mtimeMs, stat.ctimeMs]) : undefined;
|
|
34
|
+
if ('expectedSignature' in options && signature !== options.expectedSignature)
|
|
35
|
+
throw new Error('preview_source_changed');
|
|
33
36
|
await rename(temporary, target);
|
|
34
37
|
}
|
|
35
38
|
finally {
|
|
@@ -13,6 +13,7 @@ import { classifyPreviewOutput, findExternalLocalReferences } from './single-fil
|
|
|
13
13
|
import { finalizeWorkspacePlayable, readWorkspacePlayable, workspacePlayableSignature } from './finalize.js';
|
|
14
14
|
import { refreshWorkspaceArtifacts } from '../workspaces/artifact-events.js';
|
|
15
15
|
import { isolateWorkspaceForBuild } from '../workspaces/build-isolation.js';
|
|
16
|
+
import { markAudioBuildInputs, verifyAudioBuildInputs } from '../workspaces/audio-assets/build-inputs.js';
|
|
16
17
|
import { openScratchToDevelopment } from '../workspaces/directory-access.js';
|
|
17
18
|
import { injectPreviewDiagnostics } from './diagnostics.js';
|
|
18
19
|
import { OperationLogStore } from '../workspaces/operation-log.js';
|
|
@@ -103,7 +104,7 @@ export async function runPreviewPublish(state, operation, root) {
|
|
|
103
104
|
// The build runs as the development identity while this copy belongs to the controller.
|
|
104
105
|
await openScratchToDevelopment(buildCopy, (await lstat(root)).gid);
|
|
105
106
|
const project = join(buildCopy, 'project'), staged = join(buildCopy, 'output');
|
|
106
|
-
await isolateWorkspaceForBuild(root, project);
|
|
107
|
+
const audioInputs = await isolateWorkspaceForBuild(root, project);
|
|
107
108
|
if (modelInputs)
|
|
108
109
|
await verifyWorkspaceModelBuildInputs(project, modelInputs);
|
|
109
110
|
await buildPreview(state, project, staged, operation.operationId, output, appendLog);
|
|
@@ -112,7 +113,9 @@ export async function runPreviewPublish(state, operation, root) {
|
|
|
112
113
|
if (modelInputs)
|
|
113
114
|
await verifyWorkspaceModelBuildInputs(root, modelInputs);
|
|
114
115
|
const built = await readSingleHtml(staged);
|
|
115
|
-
|
|
116
|
+
await verifyAudioBuildInputs(root, audioInputs);
|
|
117
|
+
const audioMarked = markAudioBuildInputs(built, audioInputs);
|
|
118
|
+
const finalBytes = modelInputs ? markWorkspaceModelBuild(audioMarked, modelInputs) : audioMarked;
|
|
116
119
|
await finalizeWorkspacePlayable(root, finalBytes, 'preview.publish', {
|
|
117
120
|
expectedSignature: signature, source: { operationId: operation.operationId, ...(operation.sessionId ? { sessionId: operation.sessionId } : {}) },
|
|
118
121
|
});
|
|
@@ -5,6 +5,7 @@ import JSON5 from 'json5';
|
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { embedWorkspaceRuntimeProfile, workspaceRelativePathSchema, workspaceRuntimePreviewProfileSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
7
7
|
import { readWorkspaceParameters } from '../workspaces/parameters.js';
|
|
8
|
+
import { readWorkspaceLevelEditor, levelEditorDigest } from '../workspaces/level-editor.js';
|
|
8
9
|
import { revisionScope } from '../workspaces/registry.js';
|
|
9
10
|
import { PREVIEW_RUNTIME_DIRECTORY, previewRuntimeSdkArtifactSchema, readPreviewRuntimeDistribution, } from '../../preview-runtime/distribution.js';
|
|
10
11
|
const manifestPath = '.playcraft/workspace.json';
|
|
@@ -106,7 +107,21 @@ export async function prepareRuntimePreview(state, operation, root) {
|
|
|
106
107
|
if (typeof packageData.scripts?.[config.buildScript] !== 'string')
|
|
107
108
|
throw new RuntimePreviewBuildError('runtime_preview_script_missing');
|
|
108
109
|
const scope = revisionScope(state, operation.workspaceId, operation.directoryName);
|
|
109
|
-
const
|
|
110
|
+
const readLevel = () => readWorkspaceLevelEditor(root, state.journal.fileRevisions(scope), { state, operation });
|
|
111
|
+
const beforeLevel = await readLevel();
|
|
112
|
+
if (JSON5.parse(manifest.toString()).levelEditor && beforeLevel.status !== 'ready')
|
|
113
|
+
throw new RuntimePreviewBuildError('runtime_preview_level_editor_invalid');
|
|
114
|
+
const read = async () => {
|
|
115
|
+
const parameters = await readWorkspaceParameters(root, state.journal.fileRevisions(scope));
|
|
116
|
+
if (parameters.status === 'ready')
|
|
117
|
+
return parameters;
|
|
118
|
+
const level = await readLevel();
|
|
119
|
+
if (level.status !== 'ready')
|
|
120
|
+
return parameters;
|
|
121
|
+
return { status: 'ready', schemaHash: level.schemaHash, sourceFingerprint: level.sourceFingerprint,
|
|
122
|
+
dataHash: levelEditorDigest(JSON.stringify(level.documentHashes)), dataRevision: Math.max(0, ...Object.values(level.revisions)),
|
|
123
|
+
schemaRevision: 0, themeId: 'default', runtimeI18n: undefined };
|
|
124
|
+
};
|
|
110
125
|
const before = await read();
|
|
111
126
|
if (before.status !== 'ready' ||
|
|
112
127
|
!before.schemaHash ||
|
|
@@ -170,7 +185,14 @@ export async function prepareRuntimePreview(state, operation, root) {
|
|
|
170
185
|
...(before.runtimeI18n.themeHash ? { runtimeI18nHash: before.runtimeI18n.themeHash } : {}) } : {}),
|
|
171
186
|
},
|
|
172
187
|
};
|
|
173
|
-
const
|
|
188
|
+
const levelBinding = beforeLevel.status === 'ready' && beforeLevel.definition ? {
|
|
189
|
+
workspaceId: operation.workspaceId, directoryName: operation.directoryName ?? 'basic', editorId: beforeLevel.definition.editorId,
|
|
190
|
+
dataContractVersion: beforeLevel.definition.dataContractVersion, executionContractHash: beforeLevel.executionContractHash,
|
|
191
|
+
} : undefined;
|
|
192
|
+
const levelInputs = beforeLevel.status === 'ready' && beforeLevel.definition ? await Promise.all([...new Set([beforeLevel.definitionPath, ...Object.values(beforeLevel.definition.documents).flatMap(item => [item.path, item.schema])])]
|
|
193
|
+
.map(async (path) => ({ path, sha256: digest(await insideFile(root, path)) }))) : undefined;
|
|
194
|
+
const context = { buildId, ...profileBase, ...(levelBinding ? { levelEditor: { binding: levelBinding, inputs: levelInputs,
|
|
195
|
+
schemaHash: beforeLevel.schemaHash, dataHash: before.dataHash } } : {}) };
|
|
174
196
|
// Remove stale outputs before the script runs; a stale report cannot satisfy this build's identity.
|
|
175
197
|
for (const path of [config.outputPath, config.reportPath]) {
|
|
176
198
|
await rm(await isolatedPath(root, path), { force: true });
|
|
@@ -183,6 +205,10 @@ export async function prepareRuntimePreview(state, operation, root) {
|
|
|
183
205
|
: `pnpm run ${shellQuote(config.buildScript)}`}`,
|
|
184
206
|
async finish() {
|
|
185
207
|
const after = await read();
|
|
208
|
+
const afterLevel = await readLevel();
|
|
209
|
+
if (levelBinding && (afterLevel.status !== 'ready' || afterLevel.executionContractHash !== beforeLevel.executionContractHash ||
|
|
210
|
+
JSON.stringify(afterLevel.documentHashes) !== JSON.stringify(beforeLevel.documentHashes)))
|
|
211
|
+
throw new RuntimePreviewBuildError('runtime_preview_level_editor_changed');
|
|
186
212
|
if (after.status !== 'ready' ||
|
|
187
213
|
after.sourceFingerprint !== before.sourceFingerprint ||
|
|
188
214
|
after.dataHash !== before.dataHash ||
|
|
@@ -243,8 +269,17 @@ export async function prepareRuntimePreview(state, operation, root) {
|
|
|
243
269
|
...profileBase,
|
|
244
270
|
buildDigest: report.data.htmlDigest,
|
|
245
271
|
});
|
|
272
|
+
let html = embedWorkspaceRuntimeProfile(bytes.toString(), profile);
|
|
273
|
+
if (levelBinding) {
|
|
274
|
+
if (!/<\/head>/i.test(html))
|
|
275
|
+
throw new RuntimePreviewBuildError('runtime_preview_html_invalid');
|
|
276
|
+
if (/id=["']playcraft-level-editor-profile["']/.test(html))
|
|
277
|
+
throw new RuntimePreviewBuildError('runtime_preview_duplicate_level_profile');
|
|
278
|
+
const stamp = `<script id="playcraft-level-editor-profile" type="application/json">${JSON.stringify({ protocolVersion: 1, binding: levelBinding }).replaceAll('<', '\\u003c')}</script>`;
|
|
279
|
+
html = html.replace(/<\/head>/i, `${stamp}</head>`);
|
|
280
|
+
}
|
|
246
281
|
return {
|
|
247
|
-
bytes: Buffer.from(
|
|
282
|
+
bytes: Buffer.from(html),
|
|
248
283
|
profile,
|
|
249
284
|
};
|
|
250
285
|
},
|
|
@@ -34,6 +34,8 @@ import { loadWorkspaceMcpConfig } from './codex/mcp-config.js';
|
|
|
34
34
|
import { startSourceEventUploader, stopSourceEventUploader } from './server/source-uploader.js';
|
|
35
35
|
import { platformExportTarget, readPlatformExport, startPlatformExport } from './workspaces/platform-export.js';
|
|
36
36
|
import { scheduledTaskProposalHandler } from './workspaces/scheduled-task-proposals.js';
|
|
37
|
+
import { executionContextToolHandler } from './workspaces/execution-context.js';
|
|
38
|
+
import { registerLevelEditorRequest, startLevelEditorRequestQueue, stopLevelEditorRequestQueue } from './workspaces/level-editor-requests.js';
|
|
37
39
|
import { proceduralSkillHandler } from './workspaces/procedural-3d/host.js';
|
|
38
40
|
import { recoverModelPreviewOperations } from './workspaces/procedural-3d/model-preview-recovery.js';
|
|
39
41
|
import { recoverModelPreparationOperations, stopModelPreparations } from './workspaces/model-preparation.js';
|
|
@@ -313,11 +315,13 @@ export async function startWorkspaceRuntime(config, options = {}) {
|
|
|
313
315
|
socket.destroy();
|
|
314
316
|
});
|
|
315
317
|
const port = await listenOn(server, config.listen.host, config.listen.port);
|
|
318
|
+
startLevelEditorRequestQueue(state);
|
|
316
319
|
return {
|
|
317
320
|
config: state.config,
|
|
318
321
|
state,
|
|
319
322
|
url: `http://${formatHost(config.listen.host)}:${port}`,
|
|
320
323
|
async close() {
|
|
324
|
+
stopLevelEditorRequestQueue(state);
|
|
321
325
|
const serverClosed = closeServer(server);
|
|
322
326
|
await stopParameterVerifications(state);
|
|
323
327
|
await stopModelPreparations(state);
|
|
@@ -429,7 +433,9 @@ function defaultNativeAdapter(state) {
|
|
|
429
433
|
workspaceUi: true,
|
|
430
434
|
deliveryTools: deliveryToolHandlers(state),
|
|
431
435
|
scheduledTaskProposals: scheduledTaskProposalHandler(state),
|
|
436
|
+
executionContext: executionContextToolHandler(state),
|
|
432
437
|
productionSkill: proceduralSkillHandler(state),
|
|
438
|
+
levelEditorTasks: (source, intent) => registerLevelEditorRequest(state, source, intent),
|
|
433
439
|
...(multi ? { modelBroker: new WorkspaceModelBroker(state) } : {}),
|
|
434
440
|
...(multi ? { loadMcpConfig: async (cwd) => {
|
|
435
441
|
const { workspaceId } = await workspaceContextFromRoot(state, cwd);
|