@playcraft/cli 0.1.70 → 0.1.72
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/README.md +20 -1
- package/dist/cli-root-help.js +1 -0
- package/dist/commands/references.js +3 -0
- package/dist/commands/skills.js +68 -4
- package/dist/commands/tools-batch.js +2 -0
- package/dist/project-skills/discovery.js +248 -0
- package/dist/project-skills/messages.js +80 -0
- package/dist/project-skills/remote-activation.js +500 -0
- package/dist/project-skills/remote-client.js +117 -0
- package/dist/project-skills/remote-commands.js +87 -0
- package/dist/project-skills/remote-prepare.js +223 -0
- package/dist/project-skills/search-command.js +47 -0
- package/dist/project-skills/search.js +96 -0
- package/dist/recording/project-digest.js +1 -1
- package/dist/references/messages.js +2 -0
- package/dist/references/protocol.js +39 -12
- package/dist/workspace-runtime/batch/freeze.js +94 -24
- package/dist/workspace-runtime/batch/records.js +12 -6
- package/dist/workspace-runtime/batch/routes.js +31 -11
- package/dist/workspace-runtime/batch/source-candidate.js +29 -16
- package/dist/workspace-runtime/batch/source-capture.js +251 -20
- package/dist/workspace-runtime/codex/app-server.js +30 -1
- package/dist/workspace-runtime/codex/model-broker.js +93 -6
- package/dist/workspace-runtime/codex/remix-references-broker.js +208 -67
- package/dist/workspace-runtime/codex/skill-discovery-broker.js +174 -0
- package/dist/workspace-runtime/serve.js +5 -0
- package/dist/workspace-runtime/server/directory-access.js +10 -0
- package/dist/workspace-runtime/server/dispatch.js +121 -28
- package/dist/workspace-runtime/server/http.js +345 -117
- package/dist/workspace-runtime/server/operation-record.js +3 -1
- package/dist/workspace-runtime/workspaces/composer-context.js +92 -41
- package/dist/workspace-runtime/workspaces/files.js +59 -23
- package/dist/workspace-runtime/workspaces/remix-references.js +162 -53
- package/dist/workspace-runtime/workspaces/skill-activation-recovery.js +54 -0
- package/dist/workspace-runtime/workspaces/skill-activation-requests.js +512 -0
- package/dist/workspace-runtime/workspaces/skills.js +13 -176
- package/dist/workspace-runtime/workspaces/starter-template.js +74 -0
- package/package.json +4 -3
- package/project-template/.claude/skills/playcraft-skill-recommender/SKILL.md +28 -6
- package/project-template-v2/.claude/skills/playcraft-skill-recommender/SKILL.md +28 -6
- package/project-templates/empty/gitignore +1 -0
- package/project-templates/phaser/.claude/skills/playcraft-skill-recommender/SKILL.md +28 -6
- package/project-templates/threejs/.claude/skills/playcraft-skill-recommender/SKILL.md +28 -6
|
@@ -1,40 +1,41 @@
|
|
|
1
|
-
import { hasPendingLevelEditorTransaction, assertNoPendingLevelEditorTransaction } from '../workspaces/level-editor-atomic.js';
|
|
1
|
+
import { hasPendingLevelEditorTransaction, assertNoPendingLevelEditorTransaction, } from '../workspaces/level-editor-atomic.js';
|
|
2
2
|
import { readObservedFile, withObservedFile } from '../workspaces/revisions.js';
|
|
3
3
|
import { observeWorkspaceDirectories } from '../workspaces/directory-revisions.js';
|
|
4
|
-
import { registerWorkspace, revisionScope, sessionWorkspaceRoot, workspaceRoot, WorkspaceContextError } from '../workspaces/registry.js';
|
|
4
|
+
import { registerWorkspace, revisionScope, sessionWorkspaceRoot, workspaceRoot, WorkspaceContextError, } from '../workspaces/registry.js';
|
|
5
5
|
import { constants } from 'node:fs';
|
|
6
6
|
import { access as accessDirectory, stat } from 'node:fs/promises';
|
|
7
7
|
import { workspaceRegistrationRequestSchema, workspaceFileSearchQuerySchema, HTTP_STATUS_BY_ERROR, assertLeaseFence, buildHandshakeDocument, capability, findUntrustedRuntimeFields, gitFileRefSchema, sourceAckSchema, operationAcceptRequestSchema, operationLogSchema, operationKindSchema, operationScopeKey, productionSessionAttemptId, takesWorkspaceDirectory, nativeHistoryReadSchema, runtimeError, runtimeConnectivityDocumentSchema, unavailableRuntimeCapabilities, workspaceUiResultSchema, workspaceRemixReferenceImportRequestSchema, WORKSPACE_REMIX_REFERENCE_BODY_MAX_BYTES, WORKSPACE_ASSET_LIBRARY_IMPORT_BODY_MAX_BYTES, workspaceSessionRestoreRequestSchema, workspaceSessionRestoreResponseSchema, deliveryValidatePayloadSchema, deliveryBuildListQuerySchema, deliveryBuildListSchema, deliveryBuildDetailSchema, deliveryBuildLogSchema, workspaceDirectoryNameSchema, workspaceDirectoryRegistrationSchema, attachmentIdsSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
8
8
|
import { controlTokenMatches, extractControlToken } from './auth.js';
|
|
9
9
|
import { applyRuntimeEndpointPatch } from './endpoints.js';
|
|
10
10
|
import { wakeSourceEventUploader } from './source-uploader.js';
|
|
11
|
-
import { InvalidWorkspacePathError
|
|
11
|
+
import { InvalidWorkspacePathError } from '../workspaces/files.js';
|
|
12
12
|
import { GitWorkspaceError, readWorkspaceGitDiff, readWorkspaceGitFile, readWorkspaceGitGraph, readWorkspaceGitStatus, } from '../workspaces/git.js';
|
|
13
13
|
import { readWorkspaceParameters } from '../workspaces/parameters.js';
|
|
14
14
|
import { listParameterAgentTasks } from '../workspaces/parameter-agent.js';
|
|
15
15
|
import { readWorkspaceLevelEditor } from '../workspaces/level-editor.js';
|
|
16
16
|
import { listLevelEditorTasks } from '../workspaces/level-editor-agent.js';
|
|
17
17
|
import { listLevelEditorRequests } from '../workspaces/level-editor-requests.js';
|
|
18
|
+
import { listSkillActivationRequests } from '../workspaces/skill-activation-requests.js';
|
|
18
19
|
import { readDependencies } from '../workspaces/dependencies.js';
|
|
19
20
|
import { permissionDriftFromError } from '../workspaces/workspace-shares.js';
|
|
20
21
|
import { desktopHost, desktopRoute } from './desktop-host.js';
|
|
21
22
|
import { ensureCliCredentials } from '../workspaces/cli-credentials.js';
|
|
22
|
-
import { readAttachmentUpload, uploadWorkspaceAttachment, uploadWorkspaceVideoAttachment } from '../workspaces/attachments.js';
|
|
23
|
+
import { readAttachmentUpload, uploadWorkspaceAttachment, uploadWorkspaceVideoAttachment, } from '../workspaces/attachments.js';
|
|
23
24
|
import { serveWorkspaceAttachment } from '../workspaces/attachment-content.js';
|
|
24
25
|
import { serveWorkspaceFileContent } from '../workspaces/file-content.js';
|
|
25
26
|
import { validateDeliveryArtifact, DeliveryValidationError } from '../workspaces/delivery.js';
|
|
26
27
|
import { DeliveryStore } from '../workspaces/delivery-store.js';
|
|
27
|
-
import { importWorkspaceLibraryAttachment, listWorkspaceLibraryAttachments, listWorkspaceLibraryImportReceipts, readLibraryAttachmentUpload } from '../workspaces/library-attachments.js';
|
|
28
|
-
import { beginWorkspaceDirectoryAttachment, uploadWorkspaceDirectoryFile, completeWorkspaceDirectoryAttachment, cancelWorkspaceDirectoryAttachment } from '../workspaces/directory-attachments.js';
|
|
28
|
+
import { importWorkspaceLibraryAttachment, listWorkspaceLibraryAttachments, listWorkspaceLibraryImportReceipts, readLibraryAttachmentUpload, } from '../workspaces/library-attachments.js';
|
|
29
|
+
import { beginWorkspaceDirectoryAttachment, uploadWorkspaceDirectoryFile, completeWorkspaceDirectoryAttachment, cancelWorkspaceDirectoryAttachment, } from '../workspaces/directory-attachments.js';
|
|
29
30
|
import { readWorkspaceSkills, readWorkspaceSkillSnapshot } from '../workspaces/skills.js';
|
|
30
|
-
import { listWorkspaceRemixReferences, RemixReferenceError } from '../workspaces/remix-references.js';
|
|
31
|
+
import { listWorkspaceRemixReferences, RemixReferenceError, } from '../workspaces/remix-references.js';
|
|
31
32
|
import { loadWorkspaceSkillArtifact } from '../workspaces/skill-artifact-loader.js';
|
|
32
33
|
import { join } from 'node:path';
|
|
33
34
|
import { terminalHost } from './terminal-host.js';
|
|
34
35
|
import { batchDirectoryHeld } from '../batch/occupancy.js';
|
|
35
36
|
import { dispatchBatchRoute } from '../batch/routes.js';
|
|
36
|
-
import { cleanupWorkspaceDirectories, syncWorkspaceDirectories } from '../workspaces/directory-maintenance.js';
|
|
37
|
-
import { registerWorkspaceDirectory, restoreWorkspaceDirectory } from '../workspaces/directories.js';
|
|
37
|
+
import { cleanupWorkspaceDirectories, syncWorkspaceDirectories, } from '../workspaces/directory-maintenance.js';
|
|
38
|
+
import { registerWorkspaceDirectory, restoreWorkspaceDirectory, } from '../workspaces/directories.js';
|
|
38
39
|
import { readWorkspaceDirectoryStats } from '../workspaces/directory-stats.js';
|
|
39
40
|
import { probeRuntimeConnectivity } from './connectivity.js';
|
|
40
41
|
import { refreshWorkspaceArtifacts } from '../workspaces/artifact-events.js';
|
|
@@ -51,7 +52,7 @@ import { serveSessionImage } from '../workspaces/session-image.js';
|
|
|
51
52
|
import { restoreNativeSession, SessionRestoreError } from './session-restore.js';
|
|
52
53
|
import { handleAssetTransferRequest } from './asset-transfers.js';
|
|
53
54
|
import { readWorktreeSource, registerChatWorktree } from '../workspaces/worktrees.js';
|
|
54
|
-
import { withDirectoryWrite } from './directory-access.js';
|
|
55
|
+
import { directoryBusyCode, withDirectoryWrite } from './directory-access.js';
|
|
55
56
|
import { readProceduralModelCandidates } from '../workspaces/procedural-3d/read-candidates.js';
|
|
56
57
|
import { ModelPreviewStore } from '../workspaces/procedural-3d/model-preview-store.js';
|
|
57
58
|
import { readModelPreparation } from '../workspaces/model-preparation.js';
|
|
@@ -60,23 +61,40 @@ import { handleAudioAssetRequest } from './audio-assets.js';
|
|
|
60
61
|
export async function handleRuntimeRequest(state, request, response) {
|
|
61
62
|
const url = new URL(request.url ?? '/', 'http://127.0.0.1');
|
|
62
63
|
const workspaceId = url.searchParams.get('workspaceId');
|
|
63
|
-
const protectedRead = [
|
|
64
|
+
const protectedRead = [
|
|
65
|
+
'/runtime/level-editor',
|
|
66
|
+
'/runtime/parameters',
|
|
67
|
+
'/runtime/file',
|
|
68
|
+
'/runtime/files',
|
|
69
|
+
'/runtime/files/search',
|
|
70
|
+
'/runtime/file-content',
|
|
71
|
+
'/runtime/archive',
|
|
72
|
+
'/runtime/git-bundle',
|
|
73
|
+
'/runtime/git/status',
|
|
74
|
+
'/runtime/git/diff',
|
|
75
|
+
'/runtime/audio/assets',
|
|
76
|
+
'/runtime/audio/content',
|
|
77
|
+
].includes(url.pathname) && ['GET', 'HEAD'].includes(request.method ?? 'GET');
|
|
64
78
|
if (authorizeRuntime(state, request) === 'control' && protectedRead) {
|
|
79
|
+
const directoryName = url.searchParams.get('directoryName') ?? 'basic';
|
|
65
80
|
try {
|
|
66
|
-
const directoryName = url.searchParams.get('directoryName') ?? 'basic';
|
|
67
81
|
const readContext = { state, workspaceId: workspaceId ?? undefined, directoryName };
|
|
68
82
|
const root = await workspaceRoot(state, readContext.workspaceId, directoryName);
|
|
69
83
|
if (await hasPendingLevelEditorTransaction(root, readContext)) {
|
|
70
84
|
// A retained writer owns the exclusive lease. Reconcile that exact operation before admitting a reader.
|
|
71
85
|
for (const { operation } of state.journal.listOperations())
|
|
72
|
-
if (operation.kind === 'level-editor.transaction' &&
|
|
86
|
+
if (operation.kind === 'level-editor.transaction' &&
|
|
87
|
+
(state.config.workspaceMode !== 'multi' ||
|
|
88
|
+
operation.workspaceId === readContext.workspaceId) &&
|
|
89
|
+
(operation.directoryName ?? 'basic') === directoryName)
|
|
73
90
|
await observeLevelEditorTransaction(state, operation);
|
|
74
91
|
}
|
|
75
92
|
const recovering = await hasPendingLevelEditorTransaction(root, readContext);
|
|
76
93
|
return await withDirectoryWrite(state, { ...readContext, mode: recovering ? 'exclusive' : 'shared', wait: false }, async () => {
|
|
77
94
|
if (recovering) {
|
|
78
95
|
const snapshot = await readWorkspaceLevelEditor(root, state.journal.fileRevisions(revisionScope(state, readContext.workspaceId, directoryName)), readContext);
|
|
79
|
-
if (snapshot.status === 'invalid' &&
|
|
96
|
+
if (snapshot.status === 'invalid' &&
|
|
97
|
+
snapshot.diagnostics.some((item) => /recovery|history/.test(item.code)))
|
|
80
98
|
throw new Error(snapshot.diagnostics[0].code);
|
|
81
99
|
}
|
|
82
100
|
await assertNoPendingLevelEditorTransaction(root, readContext);
|
|
@@ -84,26 +102,49 @@ export async function handleRuntimeRequest(state, request, response) {
|
|
|
84
102
|
});
|
|
85
103
|
}
|
|
86
104
|
catch (error) {
|
|
87
|
-
if (!response.headersSent)
|
|
88
|
-
|
|
105
|
+
if (!response.headersSent) {
|
|
106
|
+
const busy = error instanceof Error && error.message === 'directory_busy';
|
|
107
|
+
writeJson(response, 409, {
|
|
108
|
+
error: busy
|
|
109
|
+
? await directoryBusyCode(state, workspaceId ?? undefined, directoryName)
|
|
110
|
+
: error instanceof Error
|
|
111
|
+
? error.message
|
|
112
|
+
: 'level_editor_recovery_required',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
89
115
|
return;
|
|
90
116
|
}
|
|
91
117
|
}
|
|
92
118
|
// Body-scoped operations, members and terminal control take their own lifetime-aware leases.
|
|
93
|
-
const ownLease = url.pathname.startsWith('/runtime/operations') ||
|
|
119
|
+
const ownLease = url.pathname.startsWith('/runtime/operations') ||
|
|
120
|
+
url.pathname.startsWith('/runtime/terminals/') ||
|
|
94
121
|
url.pathname === '/runtime/audio/export' ||
|
|
95
|
-
url.pathname.startsWith('/runtime/batch-') ||
|
|
122
|
+
url.pathname.startsWith('/runtime/batch-') ||
|
|
123
|
+
/^\/runtime\/sessions\/[^/]+\/ui-results$/.test(url.pathname) ||
|
|
96
124
|
/^\/runtime\/sessions\/[^/]+\/prediction$/.test(url.pathname);
|
|
97
|
-
if (authorizeRuntime(state, request) === 'control' &&
|
|
98
|
-
|
|
125
|
+
if (authorizeRuntime(state, request) === 'control' &&
|
|
126
|
+
workspaceId &&
|
|
127
|
+
(!['GET', 'HEAD', 'OPTIONS'].includes(request.method ?? 'GET') ||
|
|
128
|
+
url.pathname === '/runtime/git-bundle') &&
|
|
129
|
+
!ownLease) {
|
|
99
130
|
try {
|
|
100
|
-
return await withDirectoryWrite(state, {
|
|
131
|
+
return await withDirectoryWrite(state, {
|
|
132
|
+
workspaceId,
|
|
133
|
+
directoryName: url.searchParams.get('directoryName') ?? 'basic',
|
|
134
|
+
wait: false,
|
|
101
135
|
sessionId: url.searchParams.get('sessionId') ?? undefined,
|
|
102
|
-
...(url.pathname === '/runtime/git-bundle' ? { mode: 'exclusive' } : {})
|
|
136
|
+
...(url.pathname === '/runtime/git-bundle' ? { mode: 'exclusive' } : {}),
|
|
137
|
+
}, () => dispatchRuntimeRequest(state, request, response));
|
|
103
138
|
}
|
|
104
139
|
catch (error) {
|
|
105
|
-
if (!response.headersSent)
|
|
106
|
-
|
|
140
|
+
if (!response.headersSent) {
|
|
141
|
+
const busy = error instanceof Error && error.message === 'directory_busy';
|
|
142
|
+
writeJson(response, busy ? 409 : 500, {
|
|
143
|
+
error: busy
|
|
144
|
+
? await directoryBusyCode(state, workspaceId, url.searchParams.get('directoryName') ?? 'basic')
|
|
145
|
+
: 'runtime_request_failed',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
107
148
|
else
|
|
108
149
|
response.destroy(error instanceof Error ? error : undefined);
|
|
109
150
|
return;
|
|
@@ -159,7 +200,7 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
159
200
|
}
|
|
160
201
|
const workspaceId = parsed.data;
|
|
161
202
|
const root = await sessionWorkspaceRoot(state, workspaceId, directoryName);
|
|
162
|
-
const rootStat = await stat(root).catch(error => {
|
|
203
|
+
const rootStat = await stat(root).catch((error) => {
|
|
163
204
|
if (['ENOENT', 'ENOTDIR'].includes(error.code))
|
|
164
205
|
throw new WorkspaceContextError('workspace_not_prepared', { cause: error });
|
|
165
206
|
throw error;
|
|
@@ -191,6 +232,7 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
191
232
|
parameterAgentVersion: 1,
|
|
192
233
|
directoryConcurrencyVersion: 1,
|
|
193
234
|
skillExecutionPolicyVersion: 1,
|
|
235
|
+
skillActivationVersion: 1,
|
|
194
236
|
parameterInitializationVersion: 1,
|
|
195
237
|
levelEditorVersion: 1,
|
|
196
238
|
levelEditorTaskVersion: 1,
|
|
@@ -200,6 +242,7 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
200
242
|
levelEditorRetryVersion: 1,
|
|
201
243
|
remixReferencesVersion: 1,
|
|
202
244
|
batchSourceFreezeRevision: 2,
|
|
245
|
+
batchStarterTemplateVersion: 1,
|
|
203
246
|
batchSourcePublicationVersion: 1,
|
|
204
247
|
batchReferenceContextVersion: 1,
|
|
205
248
|
...(state.native?.workspaceUi ? { workspaceUiVersion: 1 } : {}),
|
|
@@ -211,7 +254,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
211
254
|
executionGroup: {
|
|
212
255
|
replaced: state.journal.executionGroup().replaced,
|
|
213
256
|
stopConfirmed: state.journal.executionGroup().stopConfirmed,
|
|
214
|
-
...(state.journal.stoppedRuntimeEpochs().length
|
|
257
|
+
...(state.journal.stoppedRuntimeEpochs().length
|
|
258
|
+
? { stoppedRuntimeEpochs: state.journal.stoppedRuntimeEpochs() }
|
|
259
|
+
: {}),
|
|
215
260
|
},
|
|
216
261
|
});
|
|
217
262
|
return;
|
|
@@ -228,8 +273,12 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
228
273
|
writeJson(response, 409, { error: 'multi_workspace_unavailable' });
|
|
229
274
|
return;
|
|
230
275
|
}
|
|
231
|
-
const scope = workspaceSessionRestoreResponseSchema
|
|
232
|
-
sessionId:
|
|
276
|
+
const scope = workspaceSessionRestoreResponseSchema
|
|
277
|
+
.pick({ sessionId: true, workspaceId: true, directoryName: true })
|
|
278
|
+
.safeParse({
|
|
279
|
+
sessionId: decodeURIComponent(restoreRoute[1]),
|
|
280
|
+
workspaceId: url.searchParams.get('workspaceId'),
|
|
281
|
+
directoryName,
|
|
233
282
|
});
|
|
234
283
|
const parsed = workspaceSessionRestoreRequestSchema.safeParse(await readJson(request, 4096));
|
|
235
284
|
if (!scope.success || !parsed.success) {
|
|
@@ -243,8 +292,11 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
243
292
|
return state.journal.exclusive(`session:${scope.data.sessionId}`, () => state.journal.exclusive(`workspace:${revisionScope(state, scope.data.workspaceId, directoryName) ?? 'single'}`, () => restoreNativeSession(state, scope.data, parsed.data)));
|
|
244
293
|
});
|
|
245
294
|
writeJson(response, 200, workspaceSessionRestoreResponseSchema.parse({
|
|
246
|
-
...scope.data,
|
|
247
|
-
|
|
295
|
+
...scope.data,
|
|
296
|
+
threadId: restored.threadId,
|
|
297
|
+
generation: parsed.data.generation,
|
|
298
|
+
runtimeEpoch: parsed.data.runtimeEpoch,
|
|
299
|
+
recoverability: 'native',
|
|
248
300
|
}));
|
|
249
301
|
}
|
|
250
302
|
catch (error) {
|
|
@@ -265,8 +317,10 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
265
317
|
return;
|
|
266
318
|
}
|
|
267
319
|
const session = state.journal.getSession(sessionId);
|
|
268
|
-
if (!session ||
|
|
269
|
-
|
|
320
|
+
if (!session ||
|
|
321
|
+
productionSessionAttemptId(sessionId) ||
|
|
322
|
+
session.workspaceId !== url.searchParams.get('workspaceId') ||
|
|
323
|
+
(session.directoryName ?? 'basic') !== directoryName) {
|
|
270
324
|
writeJson(response, 404, { error: 'session_not_found' });
|
|
271
325
|
return;
|
|
272
326
|
}
|
|
@@ -284,8 +338,10 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
284
338
|
return;
|
|
285
339
|
const sessionId = decodeURIComponent(predictionRoute[1]);
|
|
286
340
|
const session = state.journal.getSession(sessionId);
|
|
287
|
-
if (!session ||
|
|
288
|
-
|
|
341
|
+
if (!session ||
|
|
342
|
+
productionSessionAttemptId(sessionId) ||
|
|
343
|
+
session.workspaceId !== url.searchParams.get('workspaceId') ||
|
|
344
|
+
(session.directoryName ?? 'basic') !== directoryName) {
|
|
289
345
|
writeJson(response, 404, { error: 'session_not_found' });
|
|
290
346
|
return;
|
|
291
347
|
}
|
|
@@ -303,7 +359,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
303
359
|
if (writeDestroyed(state, response))
|
|
304
360
|
return;
|
|
305
361
|
const parsed = workspaceRegistrationRequestSchema.safeParse(await readJson(request));
|
|
306
|
-
if (!parsed.success ||
|
|
362
|
+
if (!parsed.success ||
|
|
363
|
+
parsed.data.generation !== state.config.generation ||
|
|
364
|
+
parsed.data.runtimeEpoch !== state.runtimeEpoch) {
|
|
307
365
|
writeJson(response, 409, { error: 'invalid_workspace_registration' });
|
|
308
366
|
return;
|
|
309
367
|
}
|
|
@@ -317,7 +375,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
317
375
|
if (writeDestroyed(state, response))
|
|
318
376
|
return;
|
|
319
377
|
const body = asRecord(await readJson(request));
|
|
320
|
-
if (body.generation !== state.config.generation ||
|
|
378
|
+
if (body.generation !== state.config.generation ||
|
|
379
|
+
body.runtimeEpoch !== state.runtimeEpoch ||
|
|
380
|
+
!Number.isSafeInteger(body.leaseFence)) {
|
|
321
381
|
writeJson(response, 409, { error: 'invalid_workspace_registration' });
|
|
322
382
|
return;
|
|
323
383
|
}
|
|
@@ -329,7 +389,12 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
329
389
|
return;
|
|
330
390
|
}
|
|
331
391
|
const directory = workspaceDirectoryRegistrationSchema.parse(body.directory);
|
|
332
|
-
writeJson(response, 200, await withDirectoryWrite(state, { ...directory, mode: 'exclusive', wait: false }, () => withDirectoryWrite(state, {
|
|
392
|
+
writeJson(response, 200, await withDirectoryWrite(state, { ...directory, mode: 'exclusive', wait: false }, () => withDirectoryWrite(state, {
|
|
393
|
+
workspaceId: directory.workspaceId,
|
|
394
|
+
directoryName: source.data,
|
|
395
|
+
mode: 'exclusive',
|
|
396
|
+
wait: false,
|
|
397
|
+
}, () => registerChatWorktree(state, directory, Number(body.leaseFence), source.data, body.sourceBranchName, attachments.data))));
|
|
333
398
|
}
|
|
334
399
|
else {
|
|
335
400
|
const directory = workspaceDirectoryRegistrationSchema.parse(body.directory);
|
|
@@ -337,17 +402,27 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
337
402
|
}
|
|
338
403
|
return;
|
|
339
404
|
}
|
|
340
|
-
if (request.method === 'POST' &&
|
|
405
|
+
if (request.method === 'POST' &&
|
|
406
|
+
url.pathname === '/runtime/workspaces/directories/worktree-source') {
|
|
341
407
|
if (writeDestroyed(state, response))
|
|
342
408
|
return;
|
|
343
409
|
const body = asRecord(await readJson(request));
|
|
344
410
|
const source = workspaceDirectoryNameSchema.safeParse(body.sourceDirectoryName);
|
|
345
|
-
if (body.generation !== state.config.generation ||
|
|
346
|
-
|
|
411
|
+
if (body.generation !== state.config.generation ||
|
|
412
|
+
body.runtimeEpoch !== state.runtimeEpoch ||
|
|
413
|
+
!Number.isSafeInteger(body.leaseFence) ||
|
|
414
|
+
!source.success ||
|
|
415
|
+
typeof body.workspaceId !== 'string' ||
|
|
416
|
+
typeof body.sourceBranchName !== 'string') {
|
|
347
417
|
writeJson(response, 409, { error: 'invalid_workspace_registration' });
|
|
348
418
|
return;
|
|
349
419
|
}
|
|
350
|
-
writeJson(response, 200, await withDirectoryWrite(state, {
|
|
420
|
+
writeJson(response, 200, await withDirectoryWrite(state, {
|
|
421
|
+
workspaceId: body.workspaceId,
|
|
422
|
+
directoryName: source.data,
|
|
423
|
+
mode: 'exclusive',
|
|
424
|
+
wait: false,
|
|
425
|
+
}, () => readWorktreeSource(state, body.workspaceId, source.data, Number(body.leaseFence), body.sourceBranchName)));
|
|
351
426
|
return;
|
|
352
427
|
}
|
|
353
428
|
const maintenance = /^\/runtime\/workspaces\/([a-f0-9-]{36})\/directories\/(cleanup|sync|stats)$/.exec(url.pathname);
|
|
@@ -385,11 +460,15 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
385
460
|
await handleSessionHistory(state, response, historyPath.workspaceId, historyPath.sessionId);
|
|
386
461
|
return;
|
|
387
462
|
}
|
|
388
|
-
if (['GET', 'HEAD', 'POST'].includes(request.method ?? '') &&
|
|
463
|
+
if (['GET', 'HEAD', 'POST'].includes(request.method ?? '') &&
|
|
464
|
+
url.pathname === '/runtime/session-image') {
|
|
389
465
|
let recordedPath;
|
|
390
466
|
if (request.method === 'POST') {
|
|
391
467
|
const body = asRecord(await readJson(request));
|
|
392
|
-
if (Object.keys(body).length !== 1 ||
|
|
468
|
+
if (Object.keys(body).length !== 1 ||
|
|
469
|
+
typeof body.path !== 'string' ||
|
|
470
|
+
!body.path ||
|
|
471
|
+
body.path.length > 4096) {
|
|
393
472
|
writeJson(response, 404, { error: 'image_not_found' });
|
|
394
473
|
return;
|
|
395
474
|
}
|
|
@@ -408,15 +487,18 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
408
487
|
await handleSourceEvents(state, request, response, url);
|
|
409
488
|
return;
|
|
410
489
|
}
|
|
411
|
-
if (url.pathname === '/runtime/attachments/library' &&
|
|
490
|
+
if (url.pathname === '/runtime/attachments/library' &&
|
|
491
|
+
['GET', 'POST'].includes(request.method ?? '')) {
|
|
412
492
|
if (writeDestroyed(state, response))
|
|
413
493
|
return;
|
|
414
494
|
const workspaceId = url.searchParams.get('workspaceId') ?? '';
|
|
415
495
|
const root = await workspaceRoot(state, workspaceId, directoryName);
|
|
416
496
|
if (request.method === 'GET') {
|
|
417
497
|
const sessionId = url.searchParams.get('sessionId') ?? undefined;
|
|
418
|
-
writeJson(response, 200, {
|
|
419
|
-
|
|
498
|
+
writeJson(response, 200, {
|
|
499
|
+
attachments: await listWorkspaceLibraryAttachments(root, sessionId),
|
|
500
|
+
importReceipts: await listWorkspaceLibraryImportReceipts(root, sessionId),
|
|
501
|
+
});
|
|
420
502
|
}
|
|
421
503
|
else {
|
|
422
504
|
if (await batchDirectoryHeld(state, workspaceId, directoryName)) {
|
|
@@ -426,15 +508,25 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
426
508
|
try {
|
|
427
509
|
const attachment = await importWorkspaceLibraryAttachment(root, await readLibraryAttachmentUpload(request));
|
|
428
510
|
await state.journal.allocateSourceEvent({
|
|
429
|
-
runtimeId: state.config.runtimeId,
|
|
430
|
-
|
|
511
|
+
runtimeId: state.config.runtimeId,
|
|
512
|
+
generation: state.config.generation,
|
|
513
|
+
runtimeEpoch: state.runtimeEpoch,
|
|
514
|
+
workspaceId,
|
|
515
|
+
type: 'attachments.imported',
|
|
516
|
+
occurredAt: new Date().toISOString(),
|
|
431
517
|
payload: { workspaceId, directoryName, attachmentId: attachment.id },
|
|
432
518
|
});
|
|
433
519
|
writeJson(response, 200, attachment);
|
|
434
520
|
}
|
|
435
521
|
catch (error) {
|
|
436
|
-
const code = error instanceof Error &&
|
|
437
|
-
|
|
522
|
+
const code = error instanceof Error &&
|
|
523
|
+
[
|
|
524
|
+
'library_asset_too_large',
|
|
525
|
+
'library_asset_changed',
|
|
526
|
+
'library_attachment_limit',
|
|
527
|
+
].includes(error.message)
|
|
528
|
+
? error.message
|
|
529
|
+
: 'invalid_library_attachment';
|
|
438
530
|
writeJson(response, 400, { error: code });
|
|
439
531
|
}
|
|
440
532
|
}
|
|
@@ -459,8 +551,12 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
459
551
|
else if (uploadId && action === 'complete' && request.method === 'POST') {
|
|
460
552
|
const attachment = await completeWorkspaceDirectoryAttachment(root, uploadId);
|
|
461
553
|
await state.journal.allocateSourceEvent({
|
|
462
|
-
runtimeId: state.config.runtimeId,
|
|
463
|
-
|
|
554
|
+
runtimeId: state.config.runtimeId,
|
|
555
|
+
generation: state.config.generation,
|
|
556
|
+
runtimeEpoch: state.runtimeEpoch,
|
|
557
|
+
workspaceId,
|
|
558
|
+
type: 'attachments.imported',
|
|
559
|
+
occurredAt: new Date().toISOString(),
|
|
464
560
|
payload: { workspaceId, directoryName, attachmentId: attachment.id },
|
|
465
561
|
});
|
|
466
562
|
writeJson(response, 200, attachment);
|
|
@@ -471,7 +567,11 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
471
567
|
writeJson(response, 400, { error: 'invalid_attachment' });
|
|
472
568
|
}
|
|
473
569
|
catch (error) {
|
|
474
|
-
writeJson(response, 400, {
|
|
570
|
+
writeJson(response, 400, {
|
|
571
|
+
error: error instanceof Error && error.message === 'video_probe_unavailable'
|
|
572
|
+
? 'video_probe_unavailable'
|
|
573
|
+
: 'invalid_attachment',
|
|
574
|
+
});
|
|
475
575
|
}
|
|
476
576
|
return;
|
|
477
577
|
}
|
|
@@ -485,10 +585,16 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
485
585
|
}
|
|
486
586
|
const root = await workspaceRoot(state, workspaceId, directoryName);
|
|
487
587
|
try {
|
|
488
|
-
writeJson(response, 200, await uploadWorkspaceVideoAttachment(root, request, {
|
|
588
|
+
writeJson(response, 200, await uploadWorkspaceVideoAttachment(root, request, {
|
|
589
|
+
name: url.searchParams.get('name'),
|
|
590
|
+
}));
|
|
489
591
|
}
|
|
490
592
|
catch (error) {
|
|
491
|
-
writeJson(response, 400, {
|
|
593
|
+
writeJson(response, 400, {
|
|
594
|
+
error: error instanceof Error && error.message === 'video_probe_unavailable'
|
|
595
|
+
? 'video_probe_unavailable'
|
|
596
|
+
: 'invalid_attachment',
|
|
597
|
+
});
|
|
492
598
|
}
|
|
493
599
|
return;
|
|
494
600
|
}
|
|
@@ -509,7 +615,8 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
509
615
|
}
|
|
510
616
|
return;
|
|
511
617
|
}
|
|
512
|
-
if (['GET', 'HEAD'].includes(request.method ?? '') &&
|
|
618
|
+
if (['GET', 'HEAD'].includes(request.method ?? '') &&
|
|
619
|
+
url.pathname === '/runtime/attachment-content') {
|
|
513
620
|
if (writeDestroyed(state, response))
|
|
514
621
|
return;
|
|
515
622
|
const root = await workspaceRoot(state, url.searchParams.get('workspaceId') ?? '', directoryName);
|
|
@@ -556,8 +663,7 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
556
663
|
return;
|
|
557
664
|
}
|
|
558
665
|
await workspaceRoot(state, workspaceId, directoryName);
|
|
559
|
-
const items = await new DeliveryStore(state.config.stateDir, workspaceId)
|
|
560
|
-
.list(directoryName, parsed.data.channel, parsed.data.limit);
|
|
666
|
+
const items = await new DeliveryStore(state.config.stateDir, workspaceId).list(directoryName, parsed.data.channel, parsed.data.limit);
|
|
561
667
|
writeJson(response, 200, deliveryBuildListSchema.parse({ items, scannedAt: new Date().toISOString() }));
|
|
562
668
|
return;
|
|
563
669
|
}
|
|
@@ -592,7 +698,8 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
592
698
|
writeJson(response, 200, deliveryBuildDetailSchema.parse({ record, ...(report ? { report } : {}) }));
|
|
593
699
|
return;
|
|
594
700
|
}
|
|
595
|
-
if (['GET', 'HEAD'].includes(request.method ?? '') &&
|
|
701
|
+
if (['GET', 'HEAD'].includes(request.method ?? '') &&
|
|
702
|
+
url.pathname === '/runtime/model-preview-content') {
|
|
596
703
|
if (writeDestroyed(state, response))
|
|
597
704
|
return;
|
|
598
705
|
const workspaceId = url.searchParams.get('workspaceId');
|
|
@@ -603,10 +710,15 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
603
710
|
const root = await workspaceRoot(state, workspaceId, directoryName);
|
|
604
711
|
try {
|
|
605
712
|
const output = await new ModelPreviewStore(state.config.stateDir, root).content(url.searchParams.get('previewId') ?? '');
|
|
606
|
-
response.writeHead(200, {
|
|
607
|
-
'
|
|
713
|
+
response.writeHead(200, {
|
|
714
|
+
'content-type': 'text/html; charset=utf-8',
|
|
715
|
+
'content-length': output.bytes.length,
|
|
716
|
+
'cache-control': 'private, no-store',
|
|
717
|
+
'x-content-type-options': 'nosniff',
|
|
718
|
+
'x-content-sha256': output.record.contentHash,
|
|
608
719
|
'content-security-policy': "sandbox allow-scripts; default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; connect-src 'none'",
|
|
609
|
-
'referrer-policy': 'no-referrer'
|
|
720
|
+
'referrer-policy': 'no-referrer',
|
|
721
|
+
});
|
|
610
722
|
response.end(request.method === 'HEAD' ? undefined : output.bytes);
|
|
611
723
|
}
|
|
612
724
|
catch {
|
|
@@ -661,10 +773,15 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
661
773
|
}
|
|
662
774
|
await workspaceRoot(state, workspaceId, directoryName);
|
|
663
775
|
const parsed = workspaceArtifactContentQuerySchema.safeParse({
|
|
664
|
-
artifactId: url.searchParams.get('artifactId'),
|
|
776
|
+
artifactId: url.searchParams.get('artifactId'),
|
|
777
|
+
versionId: url.searchParams.get('versionId'),
|
|
665
778
|
...(url.searchParams.has('path') ? { path: url.searchParams.get('path') } : {}),
|
|
666
|
-
...(url.searchParams.has('offset')
|
|
667
|
-
|
|
779
|
+
...(url.searchParams.has('offset')
|
|
780
|
+
? { offset: Number(url.searchParams.get('offset')) }
|
|
781
|
+
: {}),
|
|
782
|
+
...(url.searchParams.has('length')
|
|
783
|
+
? { length: Number(url.searchParams.get('length')) }
|
|
784
|
+
: {}),
|
|
668
785
|
});
|
|
669
786
|
if (!parsed.success) {
|
|
670
787
|
writeJson(response, 400, { error: 'invalid_artifact_query' });
|
|
@@ -690,17 +807,27 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
690
807
|
return;
|
|
691
808
|
const archiveWorkspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
692
809
|
const root = await sessionWorkspaceRoot(state, archiveWorkspaceId, directoryName);
|
|
693
|
-
const expectedModelPaths = state.journal
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
.
|
|
697
|
-
|
|
698
|
-
|
|
810
|
+
const expectedModelPaths = state.journal
|
|
811
|
+
.listOperations()
|
|
812
|
+
.map(({ operation }) => operation)
|
|
813
|
+
.filter((operation) => operation.kind === 'asset.library.import' &&
|
|
814
|
+
(state.config.workspaceMode !== 'multi' ||
|
|
815
|
+
operation.workspaceId === archiveWorkspaceId) &&
|
|
816
|
+
(operation.directoryName ?? 'basic') === directoryName)
|
|
817
|
+
.flatMap((operation) => operation.result && 'bundleReceipt' in operation.result
|
|
818
|
+
? (operation.result.bundleReceipt?.files ?? [])
|
|
819
|
+
: [])
|
|
820
|
+
.map((file) => file.path)
|
|
821
|
+
.filter((path) => path.toLowerCase().endsWith('.glb'));
|
|
822
|
+
await serveWorkspaceArchive(root, request, response, {
|
|
823
|
+
excludedRoots: [state.config.stateDir],
|
|
699
824
|
expectedModelPaths: [...new Set(expectedModelPaths)],
|
|
700
|
-
verifyProvenance: provenance => verifyRuntimeAssetProvenance(state, provenance)
|
|
825
|
+
verifyProvenance: (provenance) => verifyRuntimeAssetProvenance(state, provenance),
|
|
826
|
+
});
|
|
701
827
|
return;
|
|
702
828
|
}
|
|
703
|
-
if (['GET', 'HEAD'].includes(request.method ?? '') &&
|
|
829
|
+
if (['GET', 'HEAD'].includes(request.method ?? '') &&
|
|
830
|
+
url.pathname === '/runtime/file-content') {
|
|
704
831
|
if (writeDestroyed(state, response))
|
|
705
832
|
return;
|
|
706
833
|
const root = await workspaceRoot(state, url.searchParams.get('workspaceId') ?? undefined, directoryName);
|
|
@@ -736,8 +863,10 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
736
863
|
}
|
|
737
864
|
const controller = new AbortController();
|
|
738
865
|
const abort = () => controller.abort();
|
|
739
|
-
const closedRequest = () => {
|
|
740
|
-
|
|
866
|
+
const closedRequest = () => {
|
|
867
|
+
if (request.aborted || !request.complete)
|
|
868
|
+
abort();
|
|
869
|
+
};
|
|
741
870
|
request.once('aborted', abort);
|
|
742
871
|
request.once('close', closedRequest);
|
|
743
872
|
response.once('close', abort);
|
|
@@ -750,7 +879,8 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
750
879
|
catch (error) {
|
|
751
880
|
if (error instanceof Error && error.name === 'AbortError')
|
|
752
881
|
return;
|
|
753
|
-
if (error instanceof Error &&
|
|
882
|
+
if (error instanceof Error &&
|
|
883
|
+
['invalid_workspace_search', 'invalid_workspace_search_cursor'].includes(error.message))
|
|
754
884
|
writeJson(response, 400, { error: error.message });
|
|
755
885
|
else
|
|
756
886
|
throw error;
|
|
@@ -818,7 +948,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
818
948
|
catch (error) {
|
|
819
949
|
if (error instanceof WorkspaceContextError)
|
|
820
950
|
throw error;
|
|
821
|
-
writeJson(response, 409, {
|
|
951
|
+
writeJson(response, 409, {
|
|
952
|
+
error: error instanceof RemixReferenceError ? error.message : 'remix_reference_unavailable',
|
|
953
|
+
});
|
|
822
954
|
}
|
|
823
955
|
return;
|
|
824
956
|
}
|
|
@@ -841,13 +973,17 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
841
973
|
return;
|
|
842
974
|
const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
843
975
|
try {
|
|
844
|
-
const view = await state.journal.exclusive(`workspace:${revisionScope(state, workspaceId, directoryName) ?? 'single'}`, async () => readWorkspaceSkills(await workspaceRoot(state, workspaceId, directoryName), join(state.config.stateDir, 'skill-artifacts'), entry => loadWorkspaceSkillArtifact(state, workspaceId ?? 'workspace', entry, directoryName)));
|
|
976
|
+
const view = await state.journal.exclusive(`workspace:${revisionScope(state, workspaceId, directoryName) ?? 'single'}`, async () => readWorkspaceSkills(await workspaceRoot(state, workspaceId, directoryName), join(state.config.stateDir, 'skill-artifacts'), (entry) => loadWorkspaceSkillArtifact(state, workspaceId ?? 'workspace', entry, directoryName)));
|
|
845
977
|
writeJson(response, 200, view);
|
|
846
978
|
}
|
|
847
979
|
catch (error) {
|
|
848
980
|
if (error instanceof WorkspaceContextError)
|
|
849
981
|
throw error;
|
|
850
|
-
writeJson(response, 409, {
|
|
982
|
+
writeJson(response, 409, {
|
|
983
|
+
error: error instanceof Error && error.message.startsWith('workspace_skills_')
|
|
984
|
+
? error.message
|
|
985
|
+
: 'workspace_skills_unavailable',
|
|
986
|
+
});
|
|
851
987
|
}
|
|
852
988
|
return;
|
|
853
989
|
}
|
|
@@ -864,10 +1000,18 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
864
1000
|
}
|
|
865
1001
|
const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
866
1002
|
const root = await workspaceRoot(state, workspaceId, directoryName), scope = revisionScope(state, workspaceId, directoryName);
|
|
867
|
-
const first = await readWorkspaceLevelEditor(root, state.journal.fileRevisions(scope), {
|
|
1003
|
+
const first = await readWorkspaceLevelEditor(root, state.journal.fileRevisions(scope), {
|
|
1004
|
+
state,
|
|
1005
|
+
workspaceId,
|
|
1006
|
+
directoryName,
|
|
1007
|
+
});
|
|
868
1008
|
for (const document of Object.values(first.definition?.documents ?? {}))
|
|
869
1009
|
await withObservedFile(state, workspaceId, document.path, async () => undefined, directoryName);
|
|
870
|
-
writeJson(response, 200, await readWorkspaceLevelEditor(root, state.journal.fileRevisions(scope), {
|
|
1010
|
+
writeJson(response, 200, await readWorkspaceLevelEditor(root, state.journal.fileRevisions(scope), {
|
|
1011
|
+
state,
|
|
1012
|
+
workspaceId,
|
|
1013
|
+
directoryName,
|
|
1014
|
+
}));
|
|
871
1015
|
return;
|
|
872
1016
|
}
|
|
873
1017
|
if (request.method === 'GET' && url.pathname === '/runtime/level-editor/tasks') {
|
|
@@ -878,7 +1022,23 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
878
1022
|
return;
|
|
879
1023
|
}
|
|
880
1024
|
const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
881
|
-
writeJson(response, 200, {
|
|
1025
|
+
writeJson(response, 200, {
|
|
1026
|
+
tasks: await listLevelEditorTasks(state, workspaceId, directoryName, await workspaceRoot(state, workspaceId, directoryName)),
|
|
1027
|
+
});
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
if (request.method === 'GET' && url.pathname === '/runtime/skill-activation-requests') {
|
|
1031
|
+
if (writeDestroyed(state, response))
|
|
1032
|
+
return;
|
|
1033
|
+
if (directoryName === 'root') {
|
|
1034
|
+
writeJson(response, 400, { error: 'invalid_workspace_directory' });
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
1038
|
+
await workspaceRoot(state, workspaceId, directoryName);
|
|
1039
|
+
writeJson(response, 200, {
|
|
1040
|
+
requests: await listSkillActivationRequests(state, workspaceId, directoryName),
|
|
1041
|
+
});
|
|
882
1042
|
return;
|
|
883
1043
|
}
|
|
884
1044
|
if (request.method === 'GET' && url.pathname === '/runtime/level-editor/requests') {
|
|
@@ -890,7 +1050,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
890
1050
|
}
|
|
891
1051
|
const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
892
1052
|
await workspaceRoot(state, workspaceId, directoryName);
|
|
893
|
-
writeJson(response, 200, {
|
|
1053
|
+
writeJson(response, 200, {
|
|
1054
|
+
requests: await listLevelEditorRequests(state, workspaceId, directoryName),
|
|
1055
|
+
});
|
|
894
1056
|
return;
|
|
895
1057
|
}
|
|
896
1058
|
if (request.method === 'GET' && url.pathname === '/runtime/parameters') {
|
|
@@ -908,14 +1070,18 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
908
1070
|
}
|
|
909
1071
|
const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
910
1072
|
const root = await workspaceRoot(state, workspaceId, directoryName);
|
|
911
|
-
writeJson(response, 200, {
|
|
1073
|
+
writeJson(response, 200, {
|
|
1074
|
+
tasks: await listParameterAgentTasks(state, workspaceId, directoryName, root),
|
|
1075
|
+
});
|
|
912
1076
|
return;
|
|
913
1077
|
}
|
|
914
1078
|
if (request.method === 'GET' && url.pathname === '/runtime/terminals') {
|
|
915
1079
|
if (writeDestroyed(state, response))
|
|
916
1080
|
return;
|
|
917
1081
|
writeJson(response, 200, {
|
|
918
|
-
terminals: state.journal
|
|
1082
|
+
terminals: state.journal
|
|
1083
|
+
.listTerminals(url.searchParams.get('workspaceId') ?? undefined)
|
|
1084
|
+
.filter((terminal) => (terminal.directoryName ?? 'basic') === directoryName),
|
|
919
1085
|
});
|
|
920
1086
|
return;
|
|
921
1087
|
}
|
|
@@ -943,7 +1109,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
943
1109
|
directoryName,
|
|
944
1110
|
sessionId: url.searchParams.get('sessionId') ?? undefined,
|
|
945
1111
|
}));
|
|
946
|
-
const observed = operation
|
|
1112
|
+
const observed = operation
|
|
1113
|
+
? await observeLevelEditorTransaction(state, operation)
|
|
1114
|
+
: undefined;
|
|
947
1115
|
writeJson(response, observed ? 200 : 404, observed ?? { error: 'operation_not_found' });
|
|
948
1116
|
return;
|
|
949
1117
|
}
|
|
@@ -957,8 +1125,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
957
1125
|
return;
|
|
958
1126
|
}
|
|
959
1127
|
const operation = state.journal.getOperationById(operationLog[1]);
|
|
960
|
-
if (!operation ||
|
|
961
|
-
|
|
1128
|
+
if (!operation ||
|
|
1129
|
+
operation.workspaceId !== workspaceId ||
|
|
1130
|
+
(operation.directoryName ?? 'basic') !== directoryName) {
|
|
962
1131
|
writeJson(response, 404, { error: 'operation_not_found' });
|
|
963
1132
|
return;
|
|
964
1133
|
}
|
|
@@ -982,13 +1151,16 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
982
1151
|
return;
|
|
983
1152
|
}
|
|
984
1153
|
if (request.method === 'POST' && url.pathname === '/runtime/scheduled-task-proposals/source') {
|
|
985
|
-
const body = await readJson(request);
|
|
986
|
-
if (!body ||
|
|
987
|
-
|
|
1154
|
+
const body = (await readJson(request));
|
|
1155
|
+
if (!body ||
|
|
1156
|
+
Object.keys(body).some((key) => !['bridge', 'threadId', 'turnId', 'toolCallId'].includes(key)) ||
|
|
1157
|
+
!['bridge', 'threadId', 'turnId', 'toolCallId'].every((key) => typeof body[key] === 'string' && Boolean(body[key]))) {
|
|
988
1158
|
writeJson(response, 400, { valid: false });
|
|
989
1159
|
return;
|
|
990
1160
|
}
|
|
991
|
-
writeJson(response, 200, {
|
|
1161
|
+
writeJson(response, 200, {
|
|
1162
|
+
valid: state.native?.scheduledTaskProposalSource?.(body) === true,
|
|
1163
|
+
});
|
|
992
1164
|
return;
|
|
993
1165
|
}
|
|
994
1166
|
if (request.method === 'POST' && url.pathname === '/runtime/operations') {
|
|
@@ -1004,13 +1176,17 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
1004
1176
|
writeJson(response, 409, { error: 'directory_busy' });
|
|
1005
1177
|
return;
|
|
1006
1178
|
}
|
|
1007
|
-
if (request.method === 'GET' &&
|
|
1179
|
+
if (request.method === 'GET' &&
|
|
1180
|
+
['/runtime/connectivity', '/runtime/files'].includes(url.pathname)) {
|
|
1008
1181
|
const cause = error instanceof Error && error.cause ? error.cause : error;
|
|
1009
1182
|
if (['EACCES', 'EPERM'].includes(String(cause?.code))) {
|
|
1010
1183
|
writeJson(response, 409, { error: 'workspace_permission_denied' });
|
|
1011
1184
|
return;
|
|
1012
1185
|
}
|
|
1013
|
-
if (url.pathname === '/runtime/connectivity' &&
|
|
1186
|
+
if (url.pathname === '/runtime/connectivity' &&
|
|
1187
|
+
error instanceof WorkspaceContextError &&
|
|
1188
|
+
error.cause &&
|
|
1189
|
+
!['ENOENT', 'ENOTDIR'].includes(String(cause?.code))) {
|
|
1014
1190
|
writeJson(response, 500, { error: 'workspace_access_failed' });
|
|
1015
1191
|
return;
|
|
1016
1192
|
}
|
|
@@ -1044,7 +1220,7 @@ function handshake(state) {
|
|
|
1044
1220
|
if (item.name === 'conversation') {
|
|
1045
1221
|
if (conversationReady)
|
|
1046
1222
|
return capability('conversation', true);
|
|
1047
|
-
return capability('conversation', false, destroyed ? 'runtime_destroyed' : state.nativeError ?? 'adapter_not_started');
|
|
1223
|
+
return capability('conversation', false, destroyed ? 'runtime_destroyed' : (state.nativeError ?? 'adapter_not_started'));
|
|
1048
1224
|
}
|
|
1049
1225
|
if (item.name === 'files') {
|
|
1050
1226
|
return filesReady
|
|
@@ -1069,7 +1245,9 @@ function handshake(state) {
|
|
|
1069
1245
|
if (destroyed)
|
|
1070
1246
|
return capability('desktop', false, 'runtime_destroyed');
|
|
1071
1247
|
const status = desktopHost(state).snapshot().status;
|
|
1072
|
-
return status === 'ready'
|
|
1248
|
+
return status === 'ready'
|
|
1249
|
+
? capability('desktop', true)
|
|
1250
|
+
: capability('desktop', false, status);
|
|
1073
1251
|
}
|
|
1074
1252
|
if (item.name === 'parameters') {
|
|
1075
1253
|
return destroyed
|
|
@@ -1128,8 +1306,15 @@ async function acceptOperation(state, request, response) {
|
|
|
1128
1306
|
if (parsed.data.kind === 'workspace.references.import') {
|
|
1129
1307
|
const source = workspaceRemixReferenceImportRequestSchema.safeParse(parsed.data.payload);
|
|
1130
1308
|
const snapshot = parsed.data.remixReferenceSnapshot;
|
|
1131
|
-
if (!source.success ||
|
|
1132
|
-
snapshot
|
|
1309
|
+
if (!source.success ||
|
|
1310
|
+
(snapshot &&
|
|
1311
|
+
(snapshot.source.environment !== state.config.environment ||
|
|
1312
|
+
('builtinTemplate' in source.data.source
|
|
1313
|
+
? !('builtinTemplate' in snapshot.source) ||
|
|
1314
|
+
snapshot.source.builtinTemplate !== source.data.source.builtinTemplate
|
|
1315
|
+
: !('projectUuid' in snapshot.source) ||
|
|
1316
|
+
snapshot.source.projectUuid !== source.data.source.projectUuid ||
|
|
1317
|
+
snapshot.source.branchName !== source.data.source.branchName)))) {
|
|
1133
1318
|
writeJson(response, 400, { error: 'invalid_remix_reference' });
|
|
1134
1319
|
return;
|
|
1135
1320
|
}
|
|
@@ -1138,13 +1323,20 @@ async function acceptOperation(state, request, response) {
|
|
|
1138
1323
|
writeJson(response, 400, { error: 'invalid_operation', reason: 'sessionId_required' });
|
|
1139
1324
|
return;
|
|
1140
1325
|
}
|
|
1141
|
-
if (parsed.data.directoryName === 'root' &&
|
|
1326
|
+
if (parsed.data.directoryName === 'root' &&
|
|
1327
|
+
!parsed.data.kind.startsWith('session.') &&
|
|
1328
|
+
!['runtime.ensure', 'terminal.create'].includes(parsed.data.kind)) {
|
|
1142
1329
|
writeJson(response, 400, { error: 'invalid_workspace_directory' });
|
|
1143
1330
|
return;
|
|
1144
1331
|
}
|
|
1145
|
-
if (state.config.workspaceMode === 'multi' &&
|
|
1332
|
+
if (state.config.workspaceMode === 'multi' &&
|
|
1333
|
+
parsed.data.kind === 'runtime.ensure' &&
|
|
1334
|
+
parsed.data.workspaceId &&
|
|
1335
|
+
parsed.data.directoryName !== 'root') {
|
|
1146
1336
|
const workspaceId = parsed.data.workspaceId, directoryName = parsed.data.directoryName ?? 'basic';
|
|
1147
|
-
const restoring = state.journal
|
|
1337
|
+
const restoring = state.journal
|
|
1338
|
+
.getWorkspace(workspaceId)
|
|
1339
|
+
?.directories?.some((directory) => directory.directoryName === directoryName && directory.status === 'reclaimed') ?? false;
|
|
1148
1340
|
// Opening a resident checkout only refreshes instructions and can share its background install.
|
|
1149
1341
|
// Rebuilding a reclaimed checkout must still exclude every existing writer.
|
|
1150
1342
|
await withDirectoryWrite(state, { workspaceId, directoryName, mode: restoring ? 'exclusive' : 'shared', wait: false }, async () => {
|
|
@@ -1155,7 +1347,8 @@ async function acceptOperation(state, request, response) {
|
|
|
1155
1347
|
await sessionWorkspaceRoot(state, parsed.data.workspaceId, parsed.data.directoryName);
|
|
1156
1348
|
// Shared classification: a new conversation writes no files, so a batch hold must not refuse it.
|
|
1157
1349
|
const occupiesDirectory = takesWorkspaceDirectory(parsed.data.kind);
|
|
1158
|
-
if (occupiesDirectory &&
|
|
1350
|
+
if (occupiesDirectory &&
|
|
1351
|
+
(await batchDirectoryHeld(state, parsed.data.workspaceId, parsed.data.directoryName))) {
|
|
1159
1352
|
writeJson(response, 409, { error: 'batch_workspace_readonly' });
|
|
1160
1353
|
return;
|
|
1161
1354
|
}
|
|
@@ -1164,15 +1357,23 @@ async function acceptOperation(state, request, response) {
|
|
|
1164
1357
|
: parsed.data.kind === 'asset.library.import'
|
|
1165
1358
|
? { selection: parsed.data.payload, snapshot: parsed.data.libraryAssetImports }
|
|
1166
1359
|
: parsed.data.kind === 'session.turn' && parsed.data.scheduledTaskProposalBridge
|
|
1167
|
-
? {
|
|
1360
|
+
? {
|
|
1361
|
+
...asRecord(parsed.data.payload),
|
|
1362
|
+
scheduledTaskProposalBridge: parsed.data.scheduledTaskProposalBridge,
|
|
1363
|
+
}
|
|
1168
1364
|
: parsed.data.payload;
|
|
1169
1365
|
const accept = async () => {
|
|
1170
1366
|
const result = await acceptOperationCore(state, parsed.data, {
|
|
1171
1367
|
leaseFence: incomingFence,
|
|
1172
1368
|
executionPayload,
|
|
1173
1369
|
validate: async (scopeKey) => {
|
|
1174
|
-
if (parsed.data.kind.startsWith('session.') &&
|
|
1175
|
-
|
|
1370
|
+
if (parsed.data.kind.startsWith('session.') &&
|
|
1371
|
+
parsed.data.sessionId &&
|
|
1372
|
+
productionSessionAttemptId(parsed.data.sessionId)) {
|
|
1373
|
+
return {
|
|
1374
|
+
status: 409,
|
|
1375
|
+
body: runtimeError('capability_unavailable', 'production_session_readonly'),
|
|
1376
|
+
};
|
|
1176
1377
|
}
|
|
1177
1378
|
if (parsed.data.kind === 'session.create') {
|
|
1178
1379
|
const creation = state.journal.findSessionCreation(parsed.data.sessionId);
|
|
@@ -1184,7 +1385,10 @@ async function acceptOperation(state, request, response) {
|
|
|
1184
1385
|
const payload = asRecord(parsed.data.payload);
|
|
1185
1386
|
const terminalId = typeof payload.terminalId === 'string' ? payload.terminalId.trim() : '';
|
|
1186
1387
|
if (!terminalId)
|
|
1187
|
-
return {
|
|
1388
|
+
return {
|
|
1389
|
+
status: 400,
|
|
1390
|
+
body: { error: 'invalid_operation', reason: 'terminalId_required' },
|
|
1391
|
+
};
|
|
1188
1392
|
const creation = state.journal.findTerminalCreation(terminalId);
|
|
1189
1393
|
const terminal = state.journal.getTerminal(terminalId);
|
|
1190
1394
|
if (creation && operationScopeKey(creation) !== scopeKey) {
|
|
@@ -1193,16 +1397,20 @@ async function acceptOperation(state, request, response) {
|
|
|
1193
1397
|
if (terminal && !creation)
|
|
1194
1398
|
return { status: 409, body: { error: 'terminal_already_bound' } };
|
|
1195
1399
|
}
|
|
1196
|
-
else if (parsed.data.kind.startsWith('session.') &&
|
|
1400
|
+
else if (parsed.data.kind.startsWith('session.') &&
|
|
1401
|
+
state.config.workspaceMode === 'multi') {
|
|
1197
1402
|
const session = state.journal.getSession(parsed.data.sessionId);
|
|
1198
1403
|
// A replaced sandbox has no binding yet; the recovering turn creates it.
|
|
1199
1404
|
const rebinding = !session && parsed.data.kind === 'session.turn' && parsed.data.sessionRecovery;
|
|
1200
|
-
if (!rebinding &&
|
|
1201
|
-
|
|
1405
|
+
if (!rebinding &&
|
|
1406
|
+
(!session ||
|
|
1407
|
+
session.workspaceId !== parsed.data.workspaceId ||
|
|
1408
|
+
(session.directoryName ?? 'basic') !== (parsed.data.directoryName ?? 'basic'))) {
|
|
1202
1409
|
return { status: 409, body: { error: 'session_workspace_mismatch' } };
|
|
1203
1410
|
}
|
|
1204
1411
|
}
|
|
1205
|
-
if (parsed.data.kind === 'workspace.references.import' &&
|
|
1412
|
+
if (parsed.data.kind === 'workspace.references.import' &&
|
|
1413
|
+
!parsed.data.remixReferenceSnapshot) {
|
|
1206
1414
|
return { status: 400, body: { error: 'remix_reference_snapshot_required' } };
|
|
1207
1415
|
}
|
|
1208
1416
|
if (parsed.data.kind === 'asset.library.import' && !parsed.data.libraryAssetImports) {
|
|
@@ -1229,7 +1437,8 @@ async function acceptOperation(state, request, response) {
|
|
|
1229
1437
|
writeJson(response, executed.status === 'failed' ? 409 : 202, executed);
|
|
1230
1438
|
};
|
|
1231
1439
|
const acceptBound = async () => {
|
|
1232
|
-
if (occupiesDirectory &&
|
|
1440
|
+
if (occupiesDirectory &&
|
|
1441
|
+
(await batchDirectoryHeld(state, parsed.data.workspaceId, parsed.data.directoryName))) {
|
|
1233
1442
|
writeJson(response, 409, { error: 'batch_workspace_readonly' });
|
|
1234
1443
|
return;
|
|
1235
1444
|
}
|
|
@@ -1305,7 +1514,7 @@ async function handleTerminalIdentity(state, request, response, remainder) {
|
|
|
1305
1514
|
if (writeDestroyed(state, response))
|
|
1306
1515
|
return;
|
|
1307
1516
|
const [terminalId, action] = remainder.split('/');
|
|
1308
|
-
if (!terminalId || action && action !== 'connect') {
|
|
1517
|
+
if (!terminalId || (action && action !== 'connect')) {
|
|
1309
1518
|
writeJson(response, 404, { error: 'not_found' });
|
|
1310
1519
|
return;
|
|
1311
1520
|
}
|
|
@@ -1410,7 +1619,12 @@ async function readObservedParameters(state, workspaceId, directoryName = 'basic
|
|
|
1410
1619
|
const scope = revisionScope(state, workspaceId, directoryName);
|
|
1411
1620
|
return state.journal.exclusive(`workspace:${scope ?? 'single'}`, async () => {
|
|
1412
1621
|
const first = await readWorkspaceParameters(root, state.journal.fileRevisions(scope));
|
|
1413
|
-
for (const path of [
|
|
1622
|
+
for (const path of [
|
|
1623
|
+
first.schemaPath,
|
|
1624
|
+
first.dataPath,
|
|
1625
|
+
first.runtimeI18n?.globalPath,
|
|
1626
|
+
first.runtimeI18n?.themePath,
|
|
1627
|
+
]) {
|
|
1414
1628
|
if (path)
|
|
1415
1629
|
await withObservedFile(state, workspaceId, path, async () => undefined, directoryName);
|
|
1416
1630
|
}
|
|
@@ -1488,7 +1702,10 @@ async function handleSourceEvents(state, request, response, url) {
|
|
|
1488
1702
|
const runtimeEpoch = epochQuery === null ? state.runtimeEpoch : Number(epochQuery);
|
|
1489
1703
|
const runtimeId = url.searchParams.get('runtimeId') ?? state.config.runtimeId;
|
|
1490
1704
|
const generation = Number(url.searchParams.get('generation') ?? state.config.generation);
|
|
1491
|
-
if (runtimeId !== state.config.runtimeId ||
|
|
1705
|
+
if (runtimeId !== state.config.runtimeId ||
|
|
1706
|
+
generation !== state.config.generation ||
|
|
1707
|
+
!Number.isInteger(runtimeEpoch) ||
|
|
1708
|
+
runtimeEpoch < 0) {
|
|
1492
1709
|
writeJson(response, 403, { error: 'source_identity_mismatch' });
|
|
1493
1710
|
return;
|
|
1494
1711
|
}
|
|
@@ -1498,15 +1715,26 @@ async function handleSourceEvents(state, request, response, url) {
|
|
|
1498
1715
|
runtimeEpoch,
|
|
1499
1716
|
after: safeAfter,
|
|
1500
1717
|
};
|
|
1501
|
-
if (!state.journal
|
|
1502
|
-
|
|
1718
|
+
if (!state.journal
|
|
1719
|
+
.epochsSnapshot()
|
|
1720
|
+
.some((epoch) => epoch.runtime_id === runtimeId &&
|
|
1721
|
+
epoch.generation === generation &&
|
|
1722
|
+
epoch.runtime_epoch === runtimeEpoch)) {
|
|
1723
|
+
writeJson(response, 409, {
|
|
1724
|
+
code: 'source_gap',
|
|
1725
|
+
retainedFrom: 1,
|
|
1726
|
+
highWater: 0,
|
|
1727
|
+
coveredThrough: safeAfter,
|
|
1728
|
+
});
|
|
1503
1729
|
return;
|
|
1504
1730
|
}
|
|
1505
1731
|
if (String(request.headers.accept ?? '').includes('text/event-stream')) {
|
|
1506
1732
|
writeSourceStream(state, request, response, query);
|
|
1507
1733
|
return;
|
|
1508
1734
|
}
|
|
1509
|
-
if (epochQuery === null &&
|
|
1735
|
+
if (epochQuery === null &&
|
|
1736
|
+
!url.searchParams.has('runtimeId') &&
|
|
1737
|
+
!url.searchParams.has('generation')) {
|
|
1510
1738
|
const events = state.journal.listEvents(safeAfter);
|
|
1511
1739
|
const page = state.journal.readSourcePage({ ...query, limit: 1 });
|
|
1512
1740
|
writeJson(response, 200, {
|