@playcraft/cli 0.1.71 → 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/dist/cli-root-help.js +1 -0
- package/dist/commands/references.js +3 -0
- package/dist/commands/tools-batch.js +2 -0
- 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 +7 -2
- package/dist/workspace-runtime/codex/remix-references-broker.js +208 -67
- package/dist/workspace-runtime/server/directory-access.js +10 -0
- package/dist/workspace-runtime/server/http.js +332 -118
- package/dist/workspace-runtime/workspaces/composer-context.js +92 -41
- package/dist/workspace-runtime/workspaces/files.js +39 -9
- package/dist/workspace-runtime/workspaces/remix-references.js +162 -53
- package/dist/workspace-runtime/workspaces/starter-template.js +74 -0
- package/package.json +4 -3
|
@@ -1,14 +1,14 @@
|
|
|
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';
|
|
@@ -20,22 +20,22 @@ import { readDependencies } from '../workspaces/dependencies.js';
|
|
|
20
20
|
import { permissionDriftFromError } from '../workspaces/workspace-shares.js';
|
|
21
21
|
import { desktopHost, desktopRoute } from './desktop-host.js';
|
|
22
22
|
import { ensureCliCredentials } from '../workspaces/cli-credentials.js';
|
|
23
|
-
import { readAttachmentUpload, uploadWorkspaceAttachment, uploadWorkspaceVideoAttachment } from '../workspaces/attachments.js';
|
|
23
|
+
import { readAttachmentUpload, uploadWorkspaceAttachment, uploadWorkspaceVideoAttachment, } from '../workspaces/attachments.js';
|
|
24
24
|
import { serveWorkspaceAttachment } from '../workspaces/attachment-content.js';
|
|
25
25
|
import { serveWorkspaceFileContent } from '../workspaces/file-content.js';
|
|
26
26
|
import { validateDeliveryArtifact, DeliveryValidationError } from '../workspaces/delivery.js';
|
|
27
27
|
import { DeliveryStore } from '../workspaces/delivery-store.js';
|
|
28
|
-
import { importWorkspaceLibraryAttachment, listWorkspaceLibraryAttachments, listWorkspaceLibraryImportReceipts, readLibraryAttachmentUpload } from '../workspaces/library-attachments.js';
|
|
29
|
-
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';
|
|
30
30
|
import { readWorkspaceSkills, readWorkspaceSkillSnapshot } from '../workspaces/skills.js';
|
|
31
|
-
import { listWorkspaceRemixReferences, RemixReferenceError } from '../workspaces/remix-references.js';
|
|
31
|
+
import { listWorkspaceRemixReferences, RemixReferenceError, } from '../workspaces/remix-references.js';
|
|
32
32
|
import { loadWorkspaceSkillArtifact } from '../workspaces/skill-artifact-loader.js';
|
|
33
33
|
import { join } from 'node:path';
|
|
34
34
|
import { terminalHost } from './terminal-host.js';
|
|
35
35
|
import { batchDirectoryHeld } from '../batch/occupancy.js';
|
|
36
36
|
import { dispatchBatchRoute } from '../batch/routes.js';
|
|
37
|
-
import { cleanupWorkspaceDirectories, syncWorkspaceDirectories } from '../workspaces/directory-maintenance.js';
|
|
38
|
-
import { registerWorkspaceDirectory, restoreWorkspaceDirectory } from '../workspaces/directories.js';
|
|
37
|
+
import { cleanupWorkspaceDirectories, syncWorkspaceDirectories, } from '../workspaces/directory-maintenance.js';
|
|
38
|
+
import { registerWorkspaceDirectory, restoreWorkspaceDirectory, } from '../workspaces/directories.js';
|
|
39
39
|
import { readWorkspaceDirectoryStats } from '../workspaces/directory-stats.js';
|
|
40
40
|
import { probeRuntimeConnectivity } from './connectivity.js';
|
|
41
41
|
import { refreshWorkspaceArtifacts } from '../workspaces/artifact-events.js';
|
|
@@ -52,7 +52,7 @@ import { serveSessionImage } from '../workspaces/session-image.js';
|
|
|
52
52
|
import { restoreNativeSession, SessionRestoreError } from './session-restore.js';
|
|
53
53
|
import { handleAssetTransferRequest } from './asset-transfers.js';
|
|
54
54
|
import { readWorktreeSource, registerChatWorktree } from '../workspaces/worktrees.js';
|
|
55
|
-
import { withDirectoryWrite } from './directory-access.js';
|
|
55
|
+
import { directoryBusyCode, withDirectoryWrite } from './directory-access.js';
|
|
56
56
|
import { readProceduralModelCandidates } from '../workspaces/procedural-3d/read-candidates.js';
|
|
57
57
|
import { ModelPreviewStore } from '../workspaces/procedural-3d/model-preview-store.js';
|
|
58
58
|
import { readModelPreparation } from '../workspaces/model-preparation.js';
|
|
@@ -61,23 +61,40 @@ import { handleAudioAssetRequest } from './audio-assets.js';
|
|
|
61
61
|
export async function handleRuntimeRequest(state, request, response) {
|
|
62
62
|
const url = new URL(request.url ?? '/', 'http://127.0.0.1');
|
|
63
63
|
const workspaceId = url.searchParams.get('workspaceId');
|
|
64
|
-
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');
|
|
65
78
|
if (authorizeRuntime(state, request) === 'control' && protectedRead) {
|
|
79
|
+
const directoryName = url.searchParams.get('directoryName') ?? 'basic';
|
|
66
80
|
try {
|
|
67
|
-
const directoryName = url.searchParams.get('directoryName') ?? 'basic';
|
|
68
81
|
const readContext = { state, workspaceId: workspaceId ?? undefined, directoryName };
|
|
69
82
|
const root = await workspaceRoot(state, readContext.workspaceId, directoryName);
|
|
70
83
|
if (await hasPendingLevelEditorTransaction(root, readContext)) {
|
|
71
84
|
// A retained writer owns the exclusive lease. Reconcile that exact operation before admitting a reader.
|
|
72
85
|
for (const { operation } of state.journal.listOperations())
|
|
73
|
-
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)
|
|
74
90
|
await observeLevelEditorTransaction(state, operation);
|
|
75
91
|
}
|
|
76
92
|
const recovering = await hasPendingLevelEditorTransaction(root, readContext);
|
|
77
93
|
return await withDirectoryWrite(state, { ...readContext, mode: recovering ? 'exclusive' : 'shared', wait: false }, async () => {
|
|
78
94
|
if (recovering) {
|
|
79
95
|
const snapshot = await readWorkspaceLevelEditor(root, state.journal.fileRevisions(revisionScope(state, readContext.workspaceId, directoryName)), readContext);
|
|
80
|
-
if (snapshot.status === 'invalid' &&
|
|
96
|
+
if (snapshot.status === 'invalid' &&
|
|
97
|
+
snapshot.diagnostics.some((item) => /recovery|history/.test(item.code)))
|
|
81
98
|
throw new Error(snapshot.diagnostics[0].code);
|
|
82
99
|
}
|
|
83
100
|
await assertNoPendingLevelEditorTransaction(root, readContext);
|
|
@@ -85,26 +102,49 @@ export async function handleRuntimeRequest(state, request, response) {
|
|
|
85
102
|
});
|
|
86
103
|
}
|
|
87
104
|
catch (error) {
|
|
88
|
-
if (!response.headersSent)
|
|
89
|
-
|
|
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
|
+
}
|
|
90
115
|
return;
|
|
91
116
|
}
|
|
92
117
|
}
|
|
93
118
|
// Body-scoped operations, members and terminal control take their own lifetime-aware leases.
|
|
94
|
-
const ownLease = url.pathname.startsWith('/runtime/operations') ||
|
|
119
|
+
const ownLease = url.pathname.startsWith('/runtime/operations') ||
|
|
120
|
+
url.pathname.startsWith('/runtime/terminals/') ||
|
|
95
121
|
url.pathname === '/runtime/audio/export' ||
|
|
96
|
-
url.pathname.startsWith('/runtime/batch-') ||
|
|
122
|
+
url.pathname.startsWith('/runtime/batch-') ||
|
|
123
|
+
/^\/runtime\/sessions\/[^/]+\/ui-results$/.test(url.pathname) ||
|
|
97
124
|
/^\/runtime\/sessions\/[^/]+\/prediction$/.test(url.pathname);
|
|
98
|
-
if (authorizeRuntime(state, request) === 'control' &&
|
|
99
|
-
|
|
125
|
+
if (authorizeRuntime(state, request) === 'control' &&
|
|
126
|
+
workspaceId &&
|
|
127
|
+
(!['GET', 'HEAD', 'OPTIONS'].includes(request.method ?? 'GET') ||
|
|
128
|
+
url.pathname === '/runtime/git-bundle') &&
|
|
129
|
+
!ownLease) {
|
|
100
130
|
try {
|
|
101
|
-
return await withDirectoryWrite(state, {
|
|
131
|
+
return await withDirectoryWrite(state, {
|
|
132
|
+
workspaceId,
|
|
133
|
+
directoryName: url.searchParams.get('directoryName') ?? 'basic',
|
|
134
|
+
wait: false,
|
|
102
135
|
sessionId: url.searchParams.get('sessionId') ?? undefined,
|
|
103
|
-
...(url.pathname === '/runtime/git-bundle' ? { mode: 'exclusive' } : {})
|
|
136
|
+
...(url.pathname === '/runtime/git-bundle' ? { mode: 'exclusive' } : {}),
|
|
137
|
+
}, () => dispatchRuntimeRequest(state, request, response));
|
|
104
138
|
}
|
|
105
139
|
catch (error) {
|
|
106
|
-
if (!response.headersSent)
|
|
107
|
-
|
|
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
|
+
}
|
|
108
148
|
else
|
|
109
149
|
response.destroy(error instanceof Error ? error : undefined);
|
|
110
150
|
return;
|
|
@@ -160,7 +200,7 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
160
200
|
}
|
|
161
201
|
const workspaceId = parsed.data;
|
|
162
202
|
const root = await sessionWorkspaceRoot(state, workspaceId, directoryName);
|
|
163
|
-
const rootStat = await stat(root).catch(error => {
|
|
203
|
+
const rootStat = await stat(root).catch((error) => {
|
|
164
204
|
if (['ENOENT', 'ENOTDIR'].includes(error.code))
|
|
165
205
|
throw new WorkspaceContextError('workspace_not_prepared', { cause: error });
|
|
166
206
|
throw error;
|
|
@@ -202,6 +242,7 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
202
242
|
levelEditorRetryVersion: 1,
|
|
203
243
|
remixReferencesVersion: 1,
|
|
204
244
|
batchSourceFreezeRevision: 2,
|
|
245
|
+
batchStarterTemplateVersion: 1,
|
|
205
246
|
batchSourcePublicationVersion: 1,
|
|
206
247
|
batchReferenceContextVersion: 1,
|
|
207
248
|
...(state.native?.workspaceUi ? { workspaceUiVersion: 1 } : {}),
|
|
@@ -213,7 +254,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
213
254
|
executionGroup: {
|
|
214
255
|
replaced: state.journal.executionGroup().replaced,
|
|
215
256
|
stopConfirmed: state.journal.executionGroup().stopConfirmed,
|
|
216
|
-
...(state.journal.stoppedRuntimeEpochs().length
|
|
257
|
+
...(state.journal.stoppedRuntimeEpochs().length
|
|
258
|
+
? { stoppedRuntimeEpochs: state.journal.stoppedRuntimeEpochs() }
|
|
259
|
+
: {}),
|
|
217
260
|
},
|
|
218
261
|
});
|
|
219
262
|
return;
|
|
@@ -230,8 +273,12 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
230
273
|
writeJson(response, 409, { error: 'multi_workspace_unavailable' });
|
|
231
274
|
return;
|
|
232
275
|
}
|
|
233
|
-
const scope = workspaceSessionRestoreResponseSchema
|
|
234
|
-
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,
|
|
235
282
|
});
|
|
236
283
|
const parsed = workspaceSessionRestoreRequestSchema.safeParse(await readJson(request, 4096));
|
|
237
284
|
if (!scope.success || !parsed.success) {
|
|
@@ -245,8 +292,11 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
245
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)));
|
|
246
293
|
});
|
|
247
294
|
writeJson(response, 200, workspaceSessionRestoreResponseSchema.parse({
|
|
248
|
-
...scope.data,
|
|
249
|
-
|
|
295
|
+
...scope.data,
|
|
296
|
+
threadId: restored.threadId,
|
|
297
|
+
generation: parsed.data.generation,
|
|
298
|
+
runtimeEpoch: parsed.data.runtimeEpoch,
|
|
299
|
+
recoverability: 'native',
|
|
250
300
|
}));
|
|
251
301
|
}
|
|
252
302
|
catch (error) {
|
|
@@ -267,8 +317,10 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
267
317
|
return;
|
|
268
318
|
}
|
|
269
319
|
const session = state.journal.getSession(sessionId);
|
|
270
|
-
if (!session ||
|
|
271
|
-
|
|
320
|
+
if (!session ||
|
|
321
|
+
productionSessionAttemptId(sessionId) ||
|
|
322
|
+
session.workspaceId !== url.searchParams.get('workspaceId') ||
|
|
323
|
+
(session.directoryName ?? 'basic') !== directoryName) {
|
|
272
324
|
writeJson(response, 404, { error: 'session_not_found' });
|
|
273
325
|
return;
|
|
274
326
|
}
|
|
@@ -286,8 +338,10 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
286
338
|
return;
|
|
287
339
|
const sessionId = decodeURIComponent(predictionRoute[1]);
|
|
288
340
|
const session = state.journal.getSession(sessionId);
|
|
289
|
-
if (!session ||
|
|
290
|
-
|
|
341
|
+
if (!session ||
|
|
342
|
+
productionSessionAttemptId(sessionId) ||
|
|
343
|
+
session.workspaceId !== url.searchParams.get('workspaceId') ||
|
|
344
|
+
(session.directoryName ?? 'basic') !== directoryName) {
|
|
291
345
|
writeJson(response, 404, { error: 'session_not_found' });
|
|
292
346
|
return;
|
|
293
347
|
}
|
|
@@ -305,7 +359,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
305
359
|
if (writeDestroyed(state, response))
|
|
306
360
|
return;
|
|
307
361
|
const parsed = workspaceRegistrationRequestSchema.safeParse(await readJson(request));
|
|
308
|
-
if (!parsed.success ||
|
|
362
|
+
if (!parsed.success ||
|
|
363
|
+
parsed.data.generation !== state.config.generation ||
|
|
364
|
+
parsed.data.runtimeEpoch !== state.runtimeEpoch) {
|
|
309
365
|
writeJson(response, 409, { error: 'invalid_workspace_registration' });
|
|
310
366
|
return;
|
|
311
367
|
}
|
|
@@ -319,7 +375,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
319
375
|
if (writeDestroyed(state, response))
|
|
320
376
|
return;
|
|
321
377
|
const body = asRecord(await readJson(request));
|
|
322
|
-
if (body.generation !== state.config.generation ||
|
|
378
|
+
if (body.generation !== state.config.generation ||
|
|
379
|
+
body.runtimeEpoch !== state.runtimeEpoch ||
|
|
380
|
+
!Number.isSafeInteger(body.leaseFence)) {
|
|
323
381
|
writeJson(response, 409, { error: 'invalid_workspace_registration' });
|
|
324
382
|
return;
|
|
325
383
|
}
|
|
@@ -331,7 +389,12 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
331
389
|
return;
|
|
332
390
|
}
|
|
333
391
|
const directory = workspaceDirectoryRegistrationSchema.parse(body.directory);
|
|
334
|
-
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))));
|
|
335
398
|
}
|
|
336
399
|
else {
|
|
337
400
|
const directory = workspaceDirectoryRegistrationSchema.parse(body.directory);
|
|
@@ -339,17 +402,27 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
339
402
|
}
|
|
340
403
|
return;
|
|
341
404
|
}
|
|
342
|
-
if (request.method === 'POST' &&
|
|
405
|
+
if (request.method === 'POST' &&
|
|
406
|
+
url.pathname === '/runtime/workspaces/directories/worktree-source') {
|
|
343
407
|
if (writeDestroyed(state, response))
|
|
344
408
|
return;
|
|
345
409
|
const body = asRecord(await readJson(request));
|
|
346
410
|
const source = workspaceDirectoryNameSchema.safeParse(body.sourceDirectoryName);
|
|
347
|
-
if (body.generation !== state.config.generation ||
|
|
348
|
-
|
|
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') {
|
|
349
417
|
writeJson(response, 409, { error: 'invalid_workspace_registration' });
|
|
350
418
|
return;
|
|
351
419
|
}
|
|
352
|
-
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)));
|
|
353
426
|
return;
|
|
354
427
|
}
|
|
355
428
|
const maintenance = /^\/runtime\/workspaces\/([a-f0-9-]{36})\/directories\/(cleanup|sync|stats)$/.exec(url.pathname);
|
|
@@ -387,11 +460,15 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
387
460
|
await handleSessionHistory(state, response, historyPath.workspaceId, historyPath.sessionId);
|
|
388
461
|
return;
|
|
389
462
|
}
|
|
390
|
-
if (['GET', 'HEAD', 'POST'].includes(request.method ?? '') &&
|
|
463
|
+
if (['GET', 'HEAD', 'POST'].includes(request.method ?? '') &&
|
|
464
|
+
url.pathname === '/runtime/session-image') {
|
|
391
465
|
let recordedPath;
|
|
392
466
|
if (request.method === 'POST') {
|
|
393
467
|
const body = asRecord(await readJson(request));
|
|
394
|
-
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) {
|
|
395
472
|
writeJson(response, 404, { error: 'image_not_found' });
|
|
396
473
|
return;
|
|
397
474
|
}
|
|
@@ -410,15 +487,18 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
410
487
|
await handleSourceEvents(state, request, response, url);
|
|
411
488
|
return;
|
|
412
489
|
}
|
|
413
|
-
if (url.pathname === '/runtime/attachments/library' &&
|
|
490
|
+
if (url.pathname === '/runtime/attachments/library' &&
|
|
491
|
+
['GET', 'POST'].includes(request.method ?? '')) {
|
|
414
492
|
if (writeDestroyed(state, response))
|
|
415
493
|
return;
|
|
416
494
|
const workspaceId = url.searchParams.get('workspaceId') ?? '';
|
|
417
495
|
const root = await workspaceRoot(state, workspaceId, directoryName);
|
|
418
496
|
if (request.method === 'GET') {
|
|
419
497
|
const sessionId = url.searchParams.get('sessionId') ?? undefined;
|
|
420
|
-
writeJson(response, 200, {
|
|
421
|
-
|
|
498
|
+
writeJson(response, 200, {
|
|
499
|
+
attachments: await listWorkspaceLibraryAttachments(root, sessionId),
|
|
500
|
+
importReceipts: await listWorkspaceLibraryImportReceipts(root, sessionId),
|
|
501
|
+
});
|
|
422
502
|
}
|
|
423
503
|
else {
|
|
424
504
|
if (await batchDirectoryHeld(state, workspaceId, directoryName)) {
|
|
@@ -428,15 +508,25 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
428
508
|
try {
|
|
429
509
|
const attachment = await importWorkspaceLibraryAttachment(root, await readLibraryAttachmentUpload(request));
|
|
430
510
|
await state.journal.allocateSourceEvent({
|
|
431
|
-
runtimeId: state.config.runtimeId,
|
|
432
|
-
|
|
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(),
|
|
433
517
|
payload: { workspaceId, directoryName, attachmentId: attachment.id },
|
|
434
518
|
});
|
|
435
519
|
writeJson(response, 200, attachment);
|
|
436
520
|
}
|
|
437
521
|
catch (error) {
|
|
438
|
-
const code = error instanceof Error &&
|
|
439
|
-
|
|
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';
|
|
440
530
|
writeJson(response, 400, { error: code });
|
|
441
531
|
}
|
|
442
532
|
}
|
|
@@ -461,8 +551,12 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
461
551
|
else if (uploadId && action === 'complete' && request.method === 'POST') {
|
|
462
552
|
const attachment = await completeWorkspaceDirectoryAttachment(root, uploadId);
|
|
463
553
|
await state.journal.allocateSourceEvent({
|
|
464
|
-
runtimeId: state.config.runtimeId,
|
|
465
|
-
|
|
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(),
|
|
466
560
|
payload: { workspaceId, directoryName, attachmentId: attachment.id },
|
|
467
561
|
});
|
|
468
562
|
writeJson(response, 200, attachment);
|
|
@@ -473,7 +567,11 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
473
567
|
writeJson(response, 400, { error: 'invalid_attachment' });
|
|
474
568
|
}
|
|
475
569
|
catch (error) {
|
|
476
|
-
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
|
+
});
|
|
477
575
|
}
|
|
478
576
|
return;
|
|
479
577
|
}
|
|
@@ -487,10 +585,16 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
487
585
|
}
|
|
488
586
|
const root = await workspaceRoot(state, workspaceId, directoryName);
|
|
489
587
|
try {
|
|
490
|
-
writeJson(response, 200, await uploadWorkspaceVideoAttachment(root, request, {
|
|
588
|
+
writeJson(response, 200, await uploadWorkspaceVideoAttachment(root, request, {
|
|
589
|
+
name: url.searchParams.get('name'),
|
|
590
|
+
}));
|
|
491
591
|
}
|
|
492
592
|
catch (error) {
|
|
493
|
-
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
|
+
});
|
|
494
598
|
}
|
|
495
599
|
return;
|
|
496
600
|
}
|
|
@@ -511,7 +615,8 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
511
615
|
}
|
|
512
616
|
return;
|
|
513
617
|
}
|
|
514
|
-
if (['GET', 'HEAD'].includes(request.method ?? '') &&
|
|
618
|
+
if (['GET', 'HEAD'].includes(request.method ?? '') &&
|
|
619
|
+
url.pathname === '/runtime/attachment-content') {
|
|
515
620
|
if (writeDestroyed(state, response))
|
|
516
621
|
return;
|
|
517
622
|
const root = await workspaceRoot(state, url.searchParams.get('workspaceId') ?? '', directoryName);
|
|
@@ -558,8 +663,7 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
558
663
|
return;
|
|
559
664
|
}
|
|
560
665
|
await workspaceRoot(state, workspaceId, directoryName);
|
|
561
|
-
const items = await new DeliveryStore(state.config.stateDir, workspaceId)
|
|
562
|
-
.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);
|
|
563
667
|
writeJson(response, 200, deliveryBuildListSchema.parse({ items, scannedAt: new Date().toISOString() }));
|
|
564
668
|
return;
|
|
565
669
|
}
|
|
@@ -594,7 +698,8 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
594
698
|
writeJson(response, 200, deliveryBuildDetailSchema.parse({ record, ...(report ? { report } : {}) }));
|
|
595
699
|
return;
|
|
596
700
|
}
|
|
597
|
-
if (['GET', 'HEAD'].includes(request.method ?? '') &&
|
|
701
|
+
if (['GET', 'HEAD'].includes(request.method ?? '') &&
|
|
702
|
+
url.pathname === '/runtime/model-preview-content') {
|
|
598
703
|
if (writeDestroyed(state, response))
|
|
599
704
|
return;
|
|
600
705
|
const workspaceId = url.searchParams.get('workspaceId');
|
|
@@ -605,10 +710,15 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
605
710
|
const root = await workspaceRoot(state, workspaceId, directoryName);
|
|
606
711
|
try {
|
|
607
712
|
const output = await new ModelPreviewStore(state.config.stateDir, root).content(url.searchParams.get('previewId') ?? '');
|
|
608
|
-
response.writeHead(200, {
|
|
609
|
-
'
|
|
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,
|
|
610
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'",
|
|
611
|
-
'referrer-policy': 'no-referrer'
|
|
720
|
+
'referrer-policy': 'no-referrer',
|
|
721
|
+
});
|
|
612
722
|
response.end(request.method === 'HEAD' ? undefined : output.bytes);
|
|
613
723
|
}
|
|
614
724
|
catch {
|
|
@@ -663,10 +773,15 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
663
773
|
}
|
|
664
774
|
await workspaceRoot(state, workspaceId, directoryName);
|
|
665
775
|
const parsed = workspaceArtifactContentQuerySchema.safeParse({
|
|
666
|
-
artifactId: url.searchParams.get('artifactId'),
|
|
776
|
+
artifactId: url.searchParams.get('artifactId'),
|
|
777
|
+
versionId: url.searchParams.get('versionId'),
|
|
667
778
|
...(url.searchParams.has('path') ? { path: url.searchParams.get('path') } : {}),
|
|
668
|
-
...(url.searchParams.has('offset')
|
|
669
|
-
|
|
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
|
+
: {}),
|
|
670
785
|
});
|
|
671
786
|
if (!parsed.success) {
|
|
672
787
|
writeJson(response, 400, { error: 'invalid_artifact_query' });
|
|
@@ -692,17 +807,27 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
692
807
|
return;
|
|
693
808
|
const archiveWorkspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
694
809
|
const root = await sessionWorkspaceRoot(state, archiveWorkspaceId, directoryName);
|
|
695
|
-
const expectedModelPaths = state.journal
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
.
|
|
699
|
-
|
|
700
|
-
|
|
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],
|
|
701
824
|
expectedModelPaths: [...new Set(expectedModelPaths)],
|
|
702
|
-
verifyProvenance: provenance => verifyRuntimeAssetProvenance(state, provenance)
|
|
825
|
+
verifyProvenance: (provenance) => verifyRuntimeAssetProvenance(state, provenance),
|
|
826
|
+
});
|
|
703
827
|
return;
|
|
704
828
|
}
|
|
705
|
-
if (['GET', 'HEAD'].includes(request.method ?? '') &&
|
|
829
|
+
if (['GET', 'HEAD'].includes(request.method ?? '') &&
|
|
830
|
+
url.pathname === '/runtime/file-content') {
|
|
706
831
|
if (writeDestroyed(state, response))
|
|
707
832
|
return;
|
|
708
833
|
const root = await workspaceRoot(state, url.searchParams.get('workspaceId') ?? undefined, directoryName);
|
|
@@ -738,8 +863,10 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
738
863
|
}
|
|
739
864
|
const controller = new AbortController();
|
|
740
865
|
const abort = () => controller.abort();
|
|
741
|
-
const closedRequest = () => {
|
|
742
|
-
|
|
866
|
+
const closedRequest = () => {
|
|
867
|
+
if (request.aborted || !request.complete)
|
|
868
|
+
abort();
|
|
869
|
+
};
|
|
743
870
|
request.once('aborted', abort);
|
|
744
871
|
request.once('close', closedRequest);
|
|
745
872
|
response.once('close', abort);
|
|
@@ -752,7 +879,8 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
752
879
|
catch (error) {
|
|
753
880
|
if (error instanceof Error && error.name === 'AbortError')
|
|
754
881
|
return;
|
|
755
|
-
if (error instanceof Error &&
|
|
882
|
+
if (error instanceof Error &&
|
|
883
|
+
['invalid_workspace_search', 'invalid_workspace_search_cursor'].includes(error.message))
|
|
756
884
|
writeJson(response, 400, { error: error.message });
|
|
757
885
|
else
|
|
758
886
|
throw error;
|
|
@@ -820,7 +948,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
820
948
|
catch (error) {
|
|
821
949
|
if (error instanceof WorkspaceContextError)
|
|
822
950
|
throw error;
|
|
823
|
-
writeJson(response, 409, {
|
|
951
|
+
writeJson(response, 409, {
|
|
952
|
+
error: error instanceof RemixReferenceError ? error.message : 'remix_reference_unavailable',
|
|
953
|
+
});
|
|
824
954
|
}
|
|
825
955
|
return;
|
|
826
956
|
}
|
|
@@ -843,13 +973,17 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
843
973
|
return;
|
|
844
974
|
const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
845
975
|
try {
|
|
846
|
-
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)));
|
|
847
977
|
writeJson(response, 200, view);
|
|
848
978
|
}
|
|
849
979
|
catch (error) {
|
|
850
980
|
if (error instanceof WorkspaceContextError)
|
|
851
981
|
throw error;
|
|
852
|
-
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
|
+
});
|
|
853
987
|
}
|
|
854
988
|
return;
|
|
855
989
|
}
|
|
@@ -866,10 +1000,18 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
866
1000
|
}
|
|
867
1001
|
const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
868
1002
|
const root = await workspaceRoot(state, workspaceId, directoryName), scope = revisionScope(state, workspaceId, directoryName);
|
|
869
|
-
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
|
+
});
|
|
870
1008
|
for (const document of Object.values(first.definition?.documents ?? {}))
|
|
871
1009
|
await withObservedFile(state, workspaceId, document.path, async () => undefined, directoryName);
|
|
872
|
-
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
|
+
}));
|
|
873
1015
|
return;
|
|
874
1016
|
}
|
|
875
1017
|
if (request.method === 'GET' && url.pathname === '/runtime/level-editor/tasks') {
|
|
@@ -880,7 +1022,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
880
1022
|
return;
|
|
881
1023
|
}
|
|
882
1024
|
const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
883
|
-
writeJson(response, 200, {
|
|
1025
|
+
writeJson(response, 200, {
|
|
1026
|
+
tasks: await listLevelEditorTasks(state, workspaceId, directoryName, await workspaceRoot(state, workspaceId, directoryName)),
|
|
1027
|
+
});
|
|
884
1028
|
return;
|
|
885
1029
|
}
|
|
886
1030
|
if (request.method === 'GET' && url.pathname === '/runtime/skill-activation-requests') {
|
|
@@ -892,7 +1036,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
892
1036
|
}
|
|
893
1037
|
const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
894
1038
|
await workspaceRoot(state, workspaceId, directoryName);
|
|
895
|
-
writeJson(response, 200, {
|
|
1039
|
+
writeJson(response, 200, {
|
|
1040
|
+
requests: await listSkillActivationRequests(state, workspaceId, directoryName),
|
|
1041
|
+
});
|
|
896
1042
|
return;
|
|
897
1043
|
}
|
|
898
1044
|
if (request.method === 'GET' && url.pathname === '/runtime/level-editor/requests') {
|
|
@@ -904,7 +1050,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
904
1050
|
}
|
|
905
1051
|
const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
906
1052
|
await workspaceRoot(state, workspaceId, directoryName);
|
|
907
|
-
writeJson(response, 200, {
|
|
1053
|
+
writeJson(response, 200, {
|
|
1054
|
+
requests: await listLevelEditorRequests(state, workspaceId, directoryName),
|
|
1055
|
+
});
|
|
908
1056
|
return;
|
|
909
1057
|
}
|
|
910
1058
|
if (request.method === 'GET' && url.pathname === '/runtime/parameters') {
|
|
@@ -922,14 +1070,18 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
922
1070
|
}
|
|
923
1071
|
const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
924
1072
|
const root = await workspaceRoot(state, workspaceId, directoryName);
|
|
925
|
-
writeJson(response, 200, {
|
|
1073
|
+
writeJson(response, 200, {
|
|
1074
|
+
tasks: await listParameterAgentTasks(state, workspaceId, directoryName, root),
|
|
1075
|
+
});
|
|
926
1076
|
return;
|
|
927
1077
|
}
|
|
928
1078
|
if (request.method === 'GET' && url.pathname === '/runtime/terminals') {
|
|
929
1079
|
if (writeDestroyed(state, response))
|
|
930
1080
|
return;
|
|
931
1081
|
writeJson(response, 200, {
|
|
932
|
-
terminals: state.journal
|
|
1082
|
+
terminals: state.journal
|
|
1083
|
+
.listTerminals(url.searchParams.get('workspaceId') ?? undefined)
|
|
1084
|
+
.filter((terminal) => (terminal.directoryName ?? 'basic') === directoryName),
|
|
933
1085
|
});
|
|
934
1086
|
return;
|
|
935
1087
|
}
|
|
@@ -957,7 +1109,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
957
1109
|
directoryName,
|
|
958
1110
|
sessionId: url.searchParams.get('sessionId') ?? undefined,
|
|
959
1111
|
}));
|
|
960
|
-
const observed = operation
|
|
1112
|
+
const observed = operation
|
|
1113
|
+
? await observeLevelEditorTransaction(state, operation)
|
|
1114
|
+
: undefined;
|
|
961
1115
|
writeJson(response, observed ? 200 : 404, observed ?? { error: 'operation_not_found' });
|
|
962
1116
|
return;
|
|
963
1117
|
}
|
|
@@ -971,8 +1125,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
971
1125
|
return;
|
|
972
1126
|
}
|
|
973
1127
|
const operation = state.journal.getOperationById(operationLog[1]);
|
|
974
|
-
if (!operation ||
|
|
975
|
-
|
|
1128
|
+
if (!operation ||
|
|
1129
|
+
operation.workspaceId !== workspaceId ||
|
|
1130
|
+
(operation.directoryName ?? 'basic') !== directoryName) {
|
|
976
1131
|
writeJson(response, 404, { error: 'operation_not_found' });
|
|
977
1132
|
return;
|
|
978
1133
|
}
|
|
@@ -996,13 +1151,16 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
996
1151
|
return;
|
|
997
1152
|
}
|
|
998
1153
|
if (request.method === 'POST' && url.pathname === '/runtime/scheduled-task-proposals/source') {
|
|
999
|
-
const body = await readJson(request);
|
|
1000
|
-
if (!body ||
|
|
1001
|
-
|
|
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]))) {
|
|
1002
1158
|
writeJson(response, 400, { valid: false });
|
|
1003
1159
|
return;
|
|
1004
1160
|
}
|
|
1005
|
-
writeJson(response, 200, {
|
|
1161
|
+
writeJson(response, 200, {
|
|
1162
|
+
valid: state.native?.scheduledTaskProposalSource?.(body) === true,
|
|
1163
|
+
});
|
|
1006
1164
|
return;
|
|
1007
1165
|
}
|
|
1008
1166
|
if (request.method === 'POST' && url.pathname === '/runtime/operations') {
|
|
@@ -1018,13 +1176,17 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
1018
1176
|
writeJson(response, 409, { error: 'directory_busy' });
|
|
1019
1177
|
return;
|
|
1020
1178
|
}
|
|
1021
|
-
if (request.method === 'GET' &&
|
|
1179
|
+
if (request.method === 'GET' &&
|
|
1180
|
+
['/runtime/connectivity', '/runtime/files'].includes(url.pathname)) {
|
|
1022
1181
|
const cause = error instanceof Error && error.cause ? error.cause : error;
|
|
1023
1182
|
if (['EACCES', 'EPERM'].includes(String(cause?.code))) {
|
|
1024
1183
|
writeJson(response, 409, { error: 'workspace_permission_denied' });
|
|
1025
1184
|
return;
|
|
1026
1185
|
}
|
|
1027
|
-
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))) {
|
|
1028
1190
|
writeJson(response, 500, { error: 'workspace_access_failed' });
|
|
1029
1191
|
return;
|
|
1030
1192
|
}
|
|
@@ -1058,7 +1220,7 @@ function handshake(state) {
|
|
|
1058
1220
|
if (item.name === 'conversation') {
|
|
1059
1221
|
if (conversationReady)
|
|
1060
1222
|
return capability('conversation', true);
|
|
1061
|
-
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'));
|
|
1062
1224
|
}
|
|
1063
1225
|
if (item.name === 'files') {
|
|
1064
1226
|
return filesReady
|
|
@@ -1083,7 +1245,9 @@ function handshake(state) {
|
|
|
1083
1245
|
if (destroyed)
|
|
1084
1246
|
return capability('desktop', false, 'runtime_destroyed');
|
|
1085
1247
|
const status = desktopHost(state).snapshot().status;
|
|
1086
|
-
return status === 'ready'
|
|
1248
|
+
return status === 'ready'
|
|
1249
|
+
? capability('desktop', true)
|
|
1250
|
+
: capability('desktop', false, status);
|
|
1087
1251
|
}
|
|
1088
1252
|
if (item.name === 'parameters') {
|
|
1089
1253
|
return destroyed
|
|
@@ -1142,8 +1306,15 @@ async function acceptOperation(state, request, response) {
|
|
|
1142
1306
|
if (parsed.data.kind === 'workspace.references.import') {
|
|
1143
1307
|
const source = workspaceRemixReferenceImportRequestSchema.safeParse(parsed.data.payload);
|
|
1144
1308
|
const snapshot = parsed.data.remixReferenceSnapshot;
|
|
1145
|
-
if (!source.success ||
|
|
1146
|
-
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)))) {
|
|
1147
1318
|
writeJson(response, 400, { error: 'invalid_remix_reference' });
|
|
1148
1319
|
return;
|
|
1149
1320
|
}
|
|
@@ -1152,13 +1323,20 @@ async function acceptOperation(state, request, response) {
|
|
|
1152
1323
|
writeJson(response, 400, { error: 'invalid_operation', reason: 'sessionId_required' });
|
|
1153
1324
|
return;
|
|
1154
1325
|
}
|
|
1155
|
-
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)) {
|
|
1156
1329
|
writeJson(response, 400, { error: 'invalid_workspace_directory' });
|
|
1157
1330
|
return;
|
|
1158
1331
|
}
|
|
1159
|
-
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') {
|
|
1160
1336
|
const workspaceId = parsed.data.workspaceId, directoryName = parsed.data.directoryName ?? 'basic';
|
|
1161
|
-
const restoring = state.journal
|
|
1337
|
+
const restoring = state.journal
|
|
1338
|
+
.getWorkspace(workspaceId)
|
|
1339
|
+
?.directories?.some((directory) => directory.directoryName === directoryName && directory.status === 'reclaimed') ?? false;
|
|
1162
1340
|
// Opening a resident checkout only refreshes instructions and can share its background install.
|
|
1163
1341
|
// Rebuilding a reclaimed checkout must still exclude every existing writer.
|
|
1164
1342
|
await withDirectoryWrite(state, { workspaceId, directoryName, mode: restoring ? 'exclusive' : 'shared', wait: false }, async () => {
|
|
@@ -1169,7 +1347,8 @@ async function acceptOperation(state, request, response) {
|
|
|
1169
1347
|
await sessionWorkspaceRoot(state, parsed.data.workspaceId, parsed.data.directoryName);
|
|
1170
1348
|
// Shared classification: a new conversation writes no files, so a batch hold must not refuse it.
|
|
1171
1349
|
const occupiesDirectory = takesWorkspaceDirectory(parsed.data.kind);
|
|
1172
|
-
if (occupiesDirectory &&
|
|
1350
|
+
if (occupiesDirectory &&
|
|
1351
|
+
(await batchDirectoryHeld(state, parsed.data.workspaceId, parsed.data.directoryName))) {
|
|
1173
1352
|
writeJson(response, 409, { error: 'batch_workspace_readonly' });
|
|
1174
1353
|
return;
|
|
1175
1354
|
}
|
|
@@ -1178,15 +1357,23 @@ async function acceptOperation(state, request, response) {
|
|
|
1178
1357
|
: parsed.data.kind === 'asset.library.import'
|
|
1179
1358
|
? { selection: parsed.data.payload, snapshot: parsed.data.libraryAssetImports }
|
|
1180
1359
|
: parsed.data.kind === 'session.turn' && parsed.data.scheduledTaskProposalBridge
|
|
1181
|
-
? {
|
|
1360
|
+
? {
|
|
1361
|
+
...asRecord(parsed.data.payload),
|
|
1362
|
+
scheduledTaskProposalBridge: parsed.data.scheduledTaskProposalBridge,
|
|
1363
|
+
}
|
|
1182
1364
|
: parsed.data.payload;
|
|
1183
1365
|
const accept = async () => {
|
|
1184
1366
|
const result = await acceptOperationCore(state, parsed.data, {
|
|
1185
1367
|
leaseFence: incomingFence,
|
|
1186
1368
|
executionPayload,
|
|
1187
1369
|
validate: async (scopeKey) => {
|
|
1188
|
-
if (parsed.data.kind.startsWith('session.') &&
|
|
1189
|
-
|
|
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
|
+
};
|
|
1190
1377
|
}
|
|
1191
1378
|
if (parsed.data.kind === 'session.create') {
|
|
1192
1379
|
const creation = state.journal.findSessionCreation(parsed.data.sessionId);
|
|
@@ -1198,7 +1385,10 @@ async function acceptOperation(state, request, response) {
|
|
|
1198
1385
|
const payload = asRecord(parsed.data.payload);
|
|
1199
1386
|
const terminalId = typeof payload.terminalId === 'string' ? payload.terminalId.trim() : '';
|
|
1200
1387
|
if (!terminalId)
|
|
1201
|
-
return {
|
|
1388
|
+
return {
|
|
1389
|
+
status: 400,
|
|
1390
|
+
body: { error: 'invalid_operation', reason: 'terminalId_required' },
|
|
1391
|
+
};
|
|
1202
1392
|
const creation = state.journal.findTerminalCreation(terminalId);
|
|
1203
1393
|
const terminal = state.journal.getTerminal(terminalId);
|
|
1204
1394
|
if (creation && operationScopeKey(creation) !== scopeKey) {
|
|
@@ -1207,16 +1397,20 @@ async function acceptOperation(state, request, response) {
|
|
|
1207
1397
|
if (terminal && !creation)
|
|
1208
1398
|
return { status: 409, body: { error: 'terminal_already_bound' } };
|
|
1209
1399
|
}
|
|
1210
|
-
else if (parsed.data.kind.startsWith('session.') &&
|
|
1400
|
+
else if (parsed.data.kind.startsWith('session.') &&
|
|
1401
|
+
state.config.workspaceMode === 'multi') {
|
|
1211
1402
|
const session = state.journal.getSession(parsed.data.sessionId);
|
|
1212
1403
|
// A replaced sandbox has no binding yet; the recovering turn creates it.
|
|
1213
1404
|
const rebinding = !session && parsed.data.kind === 'session.turn' && parsed.data.sessionRecovery;
|
|
1214
|
-
if (!rebinding &&
|
|
1215
|
-
|
|
1405
|
+
if (!rebinding &&
|
|
1406
|
+
(!session ||
|
|
1407
|
+
session.workspaceId !== parsed.data.workspaceId ||
|
|
1408
|
+
(session.directoryName ?? 'basic') !== (parsed.data.directoryName ?? 'basic'))) {
|
|
1216
1409
|
return { status: 409, body: { error: 'session_workspace_mismatch' } };
|
|
1217
1410
|
}
|
|
1218
1411
|
}
|
|
1219
|
-
if (parsed.data.kind === 'workspace.references.import' &&
|
|
1412
|
+
if (parsed.data.kind === 'workspace.references.import' &&
|
|
1413
|
+
!parsed.data.remixReferenceSnapshot) {
|
|
1220
1414
|
return { status: 400, body: { error: 'remix_reference_snapshot_required' } };
|
|
1221
1415
|
}
|
|
1222
1416
|
if (parsed.data.kind === 'asset.library.import' && !parsed.data.libraryAssetImports) {
|
|
@@ -1243,7 +1437,8 @@ async function acceptOperation(state, request, response) {
|
|
|
1243
1437
|
writeJson(response, executed.status === 'failed' ? 409 : 202, executed);
|
|
1244
1438
|
};
|
|
1245
1439
|
const acceptBound = async () => {
|
|
1246
|
-
if (occupiesDirectory &&
|
|
1440
|
+
if (occupiesDirectory &&
|
|
1441
|
+
(await batchDirectoryHeld(state, parsed.data.workspaceId, parsed.data.directoryName))) {
|
|
1247
1442
|
writeJson(response, 409, { error: 'batch_workspace_readonly' });
|
|
1248
1443
|
return;
|
|
1249
1444
|
}
|
|
@@ -1319,7 +1514,7 @@ async function handleTerminalIdentity(state, request, response, remainder) {
|
|
|
1319
1514
|
if (writeDestroyed(state, response))
|
|
1320
1515
|
return;
|
|
1321
1516
|
const [terminalId, action] = remainder.split('/');
|
|
1322
|
-
if (!terminalId || action && action !== 'connect') {
|
|
1517
|
+
if (!terminalId || (action && action !== 'connect')) {
|
|
1323
1518
|
writeJson(response, 404, { error: 'not_found' });
|
|
1324
1519
|
return;
|
|
1325
1520
|
}
|
|
@@ -1424,7 +1619,12 @@ async function readObservedParameters(state, workspaceId, directoryName = 'basic
|
|
|
1424
1619
|
const scope = revisionScope(state, workspaceId, directoryName);
|
|
1425
1620
|
return state.journal.exclusive(`workspace:${scope ?? 'single'}`, async () => {
|
|
1426
1621
|
const first = await readWorkspaceParameters(root, state.journal.fileRevisions(scope));
|
|
1427
|
-
for (const path of [
|
|
1622
|
+
for (const path of [
|
|
1623
|
+
first.schemaPath,
|
|
1624
|
+
first.dataPath,
|
|
1625
|
+
first.runtimeI18n?.globalPath,
|
|
1626
|
+
first.runtimeI18n?.themePath,
|
|
1627
|
+
]) {
|
|
1428
1628
|
if (path)
|
|
1429
1629
|
await withObservedFile(state, workspaceId, path, async () => undefined, directoryName);
|
|
1430
1630
|
}
|
|
@@ -1502,7 +1702,10 @@ async function handleSourceEvents(state, request, response, url) {
|
|
|
1502
1702
|
const runtimeEpoch = epochQuery === null ? state.runtimeEpoch : Number(epochQuery);
|
|
1503
1703
|
const runtimeId = url.searchParams.get('runtimeId') ?? state.config.runtimeId;
|
|
1504
1704
|
const generation = Number(url.searchParams.get('generation') ?? state.config.generation);
|
|
1505
|
-
if (runtimeId !== state.config.runtimeId ||
|
|
1705
|
+
if (runtimeId !== state.config.runtimeId ||
|
|
1706
|
+
generation !== state.config.generation ||
|
|
1707
|
+
!Number.isInteger(runtimeEpoch) ||
|
|
1708
|
+
runtimeEpoch < 0) {
|
|
1506
1709
|
writeJson(response, 403, { error: 'source_identity_mismatch' });
|
|
1507
1710
|
return;
|
|
1508
1711
|
}
|
|
@@ -1512,15 +1715,26 @@ async function handleSourceEvents(state, request, response, url) {
|
|
|
1512
1715
|
runtimeEpoch,
|
|
1513
1716
|
after: safeAfter,
|
|
1514
1717
|
};
|
|
1515
|
-
if (!state.journal
|
|
1516
|
-
|
|
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
|
+
});
|
|
1517
1729
|
return;
|
|
1518
1730
|
}
|
|
1519
1731
|
if (String(request.headers.accept ?? '').includes('text/event-stream')) {
|
|
1520
1732
|
writeSourceStream(state, request, response, query);
|
|
1521
1733
|
return;
|
|
1522
1734
|
}
|
|
1523
|
-
if (epochQuery === null &&
|
|
1735
|
+
if (epochQuery === null &&
|
|
1736
|
+
!url.searchParams.has('runtimeId') &&
|
|
1737
|
+
!url.searchParams.has('generation')) {
|
|
1524
1738
|
const events = state.journal.listEvents(safeAfter);
|
|
1525
1739
|
const page = state.journal.readSourcePage({ ...query, limit: 1 });
|
|
1526
1740
|
writeJson(response, 200, {
|