@playcraft/cli 0.1.71 → 0.1.73
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/persistence/journal.js +3 -0
- package/dist/workspace-runtime/serve.js +3 -1
- package/dist/workspace-runtime/server/directory-access.js +10 -0
- package/dist/workspace-runtime/server/http.js +335 -119
- package/dist/workspace-runtime/workspaces/composer-context.js +92 -41
- package/dist/workspace-runtime/workspaces/files.js +39 -9
- package/dist/workspace-runtime/workspaces/platform-export.js +33 -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,29 +20,29 @@ 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';
|
|
42
42
|
import { WorkspaceArtifactStore } from '../workspaces/artifacts.js';
|
|
43
43
|
import { OperationLogStore } from '../workspaces/operation-log.js';
|
|
44
44
|
import { acceptOperationCore, observeLevelEditorTransaction } from './accept-operation.js';
|
|
45
|
-
import { workspaceArtifactContentQuerySchema } from '@playcraft/common/cloud-workspace/runtime';
|
|
45
|
+
import { workspaceArtifactContentQuerySchema, workspaceBindingIdentity } from '@playcraft/common/cloud-workspace/runtime';
|
|
46
46
|
import { searchWorkspaceFiles } from '../workspaces/files-search.js';
|
|
47
47
|
import { serveWorkspaceArchive } from '../workspaces/archive.js';
|
|
48
48
|
import { verifyRuntimeAssetProvenance } from '../workspaces/asset-provenance-verifier.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 } : {}),
|
|
@@ -210,10 +251,14 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
210
251
|
...(state.native?.startPrediction ? { predictionVersion: 1 } : {}),
|
|
211
252
|
sourceReadToken: state.journal.sourceReadToken(),
|
|
212
253
|
workspaceMode: state.config.workspaceMode,
|
|
254
|
+
// Runtime compares these with Business and registers again a binding this sandbox missed.
|
|
255
|
+
workspaceBindings: state.journal.listWorkspaces().map(workspaceBindingIdentity),
|
|
213
256
|
executionGroup: {
|
|
214
257
|
replaced: state.journal.executionGroup().replaced,
|
|
215
258
|
stopConfirmed: state.journal.executionGroup().stopConfirmed,
|
|
216
|
-
...(state.journal.stoppedRuntimeEpochs().length
|
|
259
|
+
...(state.journal.stoppedRuntimeEpochs().length
|
|
260
|
+
? { stoppedRuntimeEpochs: state.journal.stoppedRuntimeEpochs() }
|
|
261
|
+
: {}),
|
|
217
262
|
},
|
|
218
263
|
});
|
|
219
264
|
return;
|
|
@@ -230,8 +275,12 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
230
275
|
writeJson(response, 409, { error: 'multi_workspace_unavailable' });
|
|
231
276
|
return;
|
|
232
277
|
}
|
|
233
|
-
const scope = workspaceSessionRestoreResponseSchema
|
|
234
|
-
sessionId:
|
|
278
|
+
const scope = workspaceSessionRestoreResponseSchema
|
|
279
|
+
.pick({ sessionId: true, workspaceId: true, directoryName: true })
|
|
280
|
+
.safeParse({
|
|
281
|
+
sessionId: decodeURIComponent(restoreRoute[1]),
|
|
282
|
+
workspaceId: url.searchParams.get('workspaceId'),
|
|
283
|
+
directoryName,
|
|
235
284
|
});
|
|
236
285
|
const parsed = workspaceSessionRestoreRequestSchema.safeParse(await readJson(request, 4096));
|
|
237
286
|
if (!scope.success || !parsed.success) {
|
|
@@ -245,8 +294,11 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
245
294
|
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
295
|
});
|
|
247
296
|
writeJson(response, 200, workspaceSessionRestoreResponseSchema.parse({
|
|
248
|
-
...scope.data,
|
|
249
|
-
|
|
297
|
+
...scope.data,
|
|
298
|
+
threadId: restored.threadId,
|
|
299
|
+
generation: parsed.data.generation,
|
|
300
|
+
runtimeEpoch: parsed.data.runtimeEpoch,
|
|
301
|
+
recoverability: 'native',
|
|
250
302
|
}));
|
|
251
303
|
}
|
|
252
304
|
catch (error) {
|
|
@@ -267,8 +319,10 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
267
319
|
return;
|
|
268
320
|
}
|
|
269
321
|
const session = state.journal.getSession(sessionId);
|
|
270
|
-
if (!session ||
|
|
271
|
-
|
|
322
|
+
if (!session ||
|
|
323
|
+
productionSessionAttemptId(sessionId) ||
|
|
324
|
+
session.workspaceId !== url.searchParams.get('workspaceId') ||
|
|
325
|
+
(session.directoryName ?? 'basic') !== directoryName) {
|
|
272
326
|
writeJson(response, 404, { error: 'session_not_found' });
|
|
273
327
|
return;
|
|
274
328
|
}
|
|
@@ -286,8 +340,10 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
286
340
|
return;
|
|
287
341
|
const sessionId = decodeURIComponent(predictionRoute[1]);
|
|
288
342
|
const session = state.journal.getSession(sessionId);
|
|
289
|
-
if (!session ||
|
|
290
|
-
|
|
343
|
+
if (!session ||
|
|
344
|
+
productionSessionAttemptId(sessionId) ||
|
|
345
|
+
session.workspaceId !== url.searchParams.get('workspaceId') ||
|
|
346
|
+
(session.directoryName ?? 'basic') !== directoryName) {
|
|
291
347
|
writeJson(response, 404, { error: 'session_not_found' });
|
|
292
348
|
return;
|
|
293
349
|
}
|
|
@@ -305,7 +361,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
305
361
|
if (writeDestroyed(state, response))
|
|
306
362
|
return;
|
|
307
363
|
const parsed = workspaceRegistrationRequestSchema.safeParse(await readJson(request));
|
|
308
|
-
if (!parsed.success ||
|
|
364
|
+
if (!parsed.success ||
|
|
365
|
+
parsed.data.generation !== state.config.generation ||
|
|
366
|
+
parsed.data.runtimeEpoch !== state.runtimeEpoch) {
|
|
309
367
|
writeJson(response, 409, { error: 'invalid_workspace_registration' });
|
|
310
368
|
return;
|
|
311
369
|
}
|
|
@@ -319,7 +377,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
319
377
|
if (writeDestroyed(state, response))
|
|
320
378
|
return;
|
|
321
379
|
const body = asRecord(await readJson(request));
|
|
322
|
-
if (body.generation !== state.config.generation ||
|
|
380
|
+
if (body.generation !== state.config.generation ||
|
|
381
|
+
body.runtimeEpoch !== state.runtimeEpoch ||
|
|
382
|
+
!Number.isSafeInteger(body.leaseFence)) {
|
|
323
383
|
writeJson(response, 409, { error: 'invalid_workspace_registration' });
|
|
324
384
|
return;
|
|
325
385
|
}
|
|
@@ -331,7 +391,12 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
331
391
|
return;
|
|
332
392
|
}
|
|
333
393
|
const directory = workspaceDirectoryRegistrationSchema.parse(body.directory);
|
|
334
|
-
writeJson(response, 200, await withDirectoryWrite(state, { ...directory, mode: 'exclusive', wait: false }, () => withDirectoryWrite(state, {
|
|
394
|
+
writeJson(response, 200, await withDirectoryWrite(state, { ...directory, mode: 'exclusive', wait: false }, () => withDirectoryWrite(state, {
|
|
395
|
+
workspaceId: directory.workspaceId,
|
|
396
|
+
directoryName: source.data,
|
|
397
|
+
mode: 'exclusive',
|
|
398
|
+
wait: false,
|
|
399
|
+
}, () => registerChatWorktree(state, directory, Number(body.leaseFence), source.data, body.sourceBranchName, attachments.data))));
|
|
335
400
|
}
|
|
336
401
|
else {
|
|
337
402
|
const directory = workspaceDirectoryRegistrationSchema.parse(body.directory);
|
|
@@ -339,17 +404,27 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
339
404
|
}
|
|
340
405
|
return;
|
|
341
406
|
}
|
|
342
|
-
if (request.method === 'POST' &&
|
|
407
|
+
if (request.method === 'POST' &&
|
|
408
|
+
url.pathname === '/runtime/workspaces/directories/worktree-source') {
|
|
343
409
|
if (writeDestroyed(state, response))
|
|
344
410
|
return;
|
|
345
411
|
const body = asRecord(await readJson(request));
|
|
346
412
|
const source = workspaceDirectoryNameSchema.safeParse(body.sourceDirectoryName);
|
|
347
|
-
if (body.generation !== state.config.generation ||
|
|
348
|
-
|
|
413
|
+
if (body.generation !== state.config.generation ||
|
|
414
|
+
body.runtimeEpoch !== state.runtimeEpoch ||
|
|
415
|
+
!Number.isSafeInteger(body.leaseFence) ||
|
|
416
|
+
!source.success ||
|
|
417
|
+
typeof body.workspaceId !== 'string' ||
|
|
418
|
+
typeof body.sourceBranchName !== 'string') {
|
|
349
419
|
writeJson(response, 409, { error: 'invalid_workspace_registration' });
|
|
350
420
|
return;
|
|
351
421
|
}
|
|
352
|
-
writeJson(response, 200, await withDirectoryWrite(state, {
|
|
422
|
+
writeJson(response, 200, await withDirectoryWrite(state, {
|
|
423
|
+
workspaceId: body.workspaceId,
|
|
424
|
+
directoryName: source.data,
|
|
425
|
+
mode: 'exclusive',
|
|
426
|
+
wait: false,
|
|
427
|
+
}, () => readWorktreeSource(state, body.workspaceId, source.data, Number(body.leaseFence), body.sourceBranchName)));
|
|
353
428
|
return;
|
|
354
429
|
}
|
|
355
430
|
const maintenance = /^\/runtime\/workspaces\/([a-f0-9-]{36})\/directories\/(cleanup|sync|stats)$/.exec(url.pathname);
|
|
@@ -387,11 +462,15 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
387
462
|
await handleSessionHistory(state, response, historyPath.workspaceId, historyPath.sessionId);
|
|
388
463
|
return;
|
|
389
464
|
}
|
|
390
|
-
if (['GET', 'HEAD', 'POST'].includes(request.method ?? '') &&
|
|
465
|
+
if (['GET', 'HEAD', 'POST'].includes(request.method ?? '') &&
|
|
466
|
+
url.pathname === '/runtime/session-image') {
|
|
391
467
|
let recordedPath;
|
|
392
468
|
if (request.method === 'POST') {
|
|
393
469
|
const body = asRecord(await readJson(request));
|
|
394
|
-
if (Object.keys(body).length !== 1 ||
|
|
470
|
+
if (Object.keys(body).length !== 1 ||
|
|
471
|
+
typeof body.path !== 'string' ||
|
|
472
|
+
!body.path ||
|
|
473
|
+
body.path.length > 4096) {
|
|
395
474
|
writeJson(response, 404, { error: 'image_not_found' });
|
|
396
475
|
return;
|
|
397
476
|
}
|
|
@@ -410,15 +489,18 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
410
489
|
await handleSourceEvents(state, request, response, url);
|
|
411
490
|
return;
|
|
412
491
|
}
|
|
413
|
-
if (url.pathname === '/runtime/attachments/library' &&
|
|
492
|
+
if (url.pathname === '/runtime/attachments/library' &&
|
|
493
|
+
['GET', 'POST'].includes(request.method ?? '')) {
|
|
414
494
|
if (writeDestroyed(state, response))
|
|
415
495
|
return;
|
|
416
496
|
const workspaceId = url.searchParams.get('workspaceId') ?? '';
|
|
417
497
|
const root = await workspaceRoot(state, workspaceId, directoryName);
|
|
418
498
|
if (request.method === 'GET') {
|
|
419
499
|
const sessionId = url.searchParams.get('sessionId') ?? undefined;
|
|
420
|
-
writeJson(response, 200, {
|
|
421
|
-
|
|
500
|
+
writeJson(response, 200, {
|
|
501
|
+
attachments: await listWorkspaceLibraryAttachments(root, sessionId),
|
|
502
|
+
importReceipts: await listWorkspaceLibraryImportReceipts(root, sessionId),
|
|
503
|
+
});
|
|
422
504
|
}
|
|
423
505
|
else {
|
|
424
506
|
if (await batchDirectoryHeld(state, workspaceId, directoryName)) {
|
|
@@ -428,15 +510,25 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
428
510
|
try {
|
|
429
511
|
const attachment = await importWorkspaceLibraryAttachment(root, await readLibraryAttachmentUpload(request));
|
|
430
512
|
await state.journal.allocateSourceEvent({
|
|
431
|
-
runtimeId: state.config.runtimeId,
|
|
432
|
-
|
|
513
|
+
runtimeId: state.config.runtimeId,
|
|
514
|
+
generation: state.config.generation,
|
|
515
|
+
runtimeEpoch: state.runtimeEpoch,
|
|
516
|
+
workspaceId,
|
|
517
|
+
type: 'attachments.imported',
|
|
518
|
+
occurredAt: new Date().toISOString(),
|
|
433
519
|
payload: { workspaceId, directoryName, attachmentId: attachment.id },
|
|
434
520
|
});
|
|
435
521
|
writeJson(response, 200, attachment);
|
|
436
522
|
}
|
|
437
523
|
catch (error) {
|
|
438
|
-
const code = error instanceof Error &&
|
|
439
|
-
|
|
524
|
+
const code = error instanceof Error &&
|
|
525
|
+
[
|
|
526
|
+
'library_asset_too_large',
|
|
527
|
+
'library_asset_changed',
|
|
528
|
+
'library_attachment_limit',
|
|
529
|
+
].includes(error.message)
|
|
530
|
+
? error.message
|
|
531
|
+
: 'invalid_library_attachment';
|
|
440
532
|
writeJson(response, 400, { error: code });
|
|
441
533
|
}
|
|
442
534
|
}
|
|
@@ -461,8 +553,12 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
461
553
|
else if (uploadId && action === 'complete' && request.method === 'POST') {
|
|
462
554
|
const attachment = await completeWorkspaceDirectoryAttachment(root, uploadId);
|
|
463
555
|
await state.journal.allocateSourceEvent({
|
|
464
|
-
runtimeId: state.config.runtimeId,
|
|
465
|
-
|
|
556
|
+
runtimeId: state.config.runtimeId,
|
|
557
|
+
generation: state.config.generation,
|
|
558
|
+
runtimeEpoch: state.runtimeEpoch,
|
|
559
|
+
workspaceId,
|
|
560
|
+
type: 'attachments.imported',
|
|
561
|
+
occurredAt: new Date().toISOString(),
|
|
466
562
|
payload: { workspaceId, directoryName, attachmentId: attachment.id },
|
|
467
563
|
});
|
|
468
564
|
writeJson(response, 200, attachment);
|
|
@@ -473,7 +569,11 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
473
569
|
writeJson(response, 400, { error: 'invalid_attachment' });
|
|
474
570
|
}
|
|
475
571
|
catch (error) {
|
|
476
|
-
writeJson(response, 400, {
|
|
572
|
+
writeJson(response, 400, {
|
|
573
|
+
error: error instanceof Error && error.message === 'video_probe_unavailable'
|
|
574
|
+
? 'video_probe_unavailable'
|
|
575
|
+
: 'invalid_attachment',
|
|
576
|
+
});
|
|
477
577
|
}
|
|
478
578
|
return;
|
|
479
579
|
}
|
|
@@ -487,10 +587,16 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
487
587
|
}
|
|
488
588
|
const root = await workspaceRoot(state, workspaceId, directoryName);
|
|
489
589
|
try {
|
|
490
|
-
writeJson(response, 200, await uploadWorkspaceVideoAttachment(root, request, {
|
|
590
|
+
writeJson(response, 200, await uploadWorkspaceVideoAttachment(root, request, {
|
|
591
|
+
name: url.searchParams.get('name'),
|
|
592
|
+
}));
|
|
491
593
|
}
|
|
492
594
|
catch (error) {
|
|
493
|
-
writeJson(response, 400, {
|
|
595
|
+
writeJson(response, 400, {
|
|
596
|
+
error: error instanceof Error && error.message === 'video_probe_unavailable'
|
|
597
|
+
? 'video_probe_unavailable'
|
|
598
|
+
: 'invalid_attachment',
|
|
599
|
+
});
|
|
494
600
|
}
|
|
495
601
|
return;
|
|
496
602
|
}
|
|
@@ -511,7 +617,8 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
511
617
|
}
|
|
512
618
|
return;
|
|
513
619
|
}
|
|
514
|
-
if (['GET', 'HEAD'].includes(request.method ?? '') &&
|
|
620
|
+
if (['GET', 'HEAD'].includes(request.method ?? '') &&
|
|
621
|
+
url.pathname === '/runtime/attachment-content') {
|
|
515
622
|
if (writeDestroyed(state, response))
|
|
516
623
|
return;
|
|
517
624
|
const root = await workspaceRoot(state, url.searchParams.get('workspaceId') ?? '', directoryName);
|
|
@@ -558,8 +665,7 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
558
665
|
return;
|
|
559
666
|
}
|
|
560
667
|
await workspaceRoot(state, workspaceId, directoryName);
|
|
561
|
-
const items = await new DeliveryStore(state.config.stateDir, workspaceId)
|
|
562
|
-
.list(directoryName, parsed.data.channel, parsed.data.limit);
|
|
668
|
+
const items = await new DeliveryStore(state.config.stateDir, workspaceId).list(directoryName, parsed.data.channel, parsed.data.limit);
|
|
563
669
|
writeJson(response, 200, deliveryBuildListSchema.parse({ items, scannedAt: new Date().toISOString() }));
|
|
564
670
|
return;
|
|
565
671
|
}
|
|
@@ -594,7 +700,8 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
594
700
|
writeJson(response, 200, deliveryBuildDetailSchema.parse({ record, ...(report ? { report } : {}) }));
|
|
595
701
|
return;
|
|
596
702
|
}
|
|
597
|
-
if (['GET', 'HEAD'].includes(request.method ?? '') &&
|
|
703
|
+
if (['GET', 'HEAD'].includes(request.method ?? '') &&
|
|
704
|
+
url.pathname === '/runtime/model-preview-content') {
|
|
598
705
|
if (writeDestroyed(state, response))
|
|
599
706
|
return;
|
|
600
707
|
const workspaceId = url.searchParams.get('workspaceId');
|
|
@@ -605,10 +712,15 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
605
712
|
const root = await workspaceRoot(state, workspaceId, directoryName);
|
|
606
713
|
try {
|
|
607
714
|
const output = await new ModelPreviewStore(state.config.stateDir, root).content(url.searchParams.get('previewId') ?? '');
|
|
608
|
-
response.writeHead(200, {
|
|
609
|
-
'
|
|
715
|
+
response.writeHead(200, {
|
|
716
|
+
'content-type': 'text/html; charset=utf-8',
|
|
717
|
+
'content-length': output.bytes.length,
|
|
718
|
+
'cache-control': 'private, no-store',
|
|
719
|
+
'x-content-type-options': 'nosniff',
|
|
720
|
+
'x-content-sha256': output.record.contentHash,
|
|
610
721
|
'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'
|
|
722
|
+
'referrer-policy': 'no-referrer',
|
|
723
|
+
});
|
|
612
724
|
response.end(request.method === 'HEAD' ? undefined : output.bytes);
|
|
613
725
|
}
|
|
614
726
|
catch {
|
|
@@ -663,10 +775,15 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
663
775
|
}
|
|
664
776
|
await workspaceRoot(state, workspaceId, directoryName);
|
|
665
777
|
const parsed = workspaceArtifactContentQuerySchema.safeParse({
|
|
666
|
-
artifactId: url.searchParams.get('artifactId'),
|
|
778
|
+
artifactId: url.searchParams.get('artifactId'),
|
|
779
|
+
versionId: url.searchParams.get('versionId'),
|
|
667
780
|
...(url.searchParams.has('path') ? { path: url.searchParams.get('path') } : {}),
|
|
668
|
-
...(url.searchParams.has('offset')
|
|
669
|
-
|
|
781
|
+
...(url.searchParams.has('offset')
|
|
782
|
+
? { offset: Number(url.searchParams.get('offset')) }
|
|
783
|
+
: {}),
|
|
784
|
+
...(url.searchParams.has('length')
|
|
785
|
+
? { length: Number(url.searchParams.get('length')) }
|
|
786
|
+
: {}),
|
|
670
787
|
});
|
|
671
788
|
if (!parsed.success) {
|
|
672
789
|
writeJson(response, 400, { error: 'invalid_artifact_query' });
|
|
@@ -692,17 +809,27 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
692
809
|
return;
|
|
693
810
|
const archiveWorkspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
694
811
|
const root = await sessionWorkspaceRoot(state, archiveWorkspaceId, directoryName);
|
|
695
|
-
const expectedModelPaths = state.journal
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
.
|
|
699
|
-
|
|
700
|
-
|
|
812
|
+
const expectedModelPaths = state.journal
|
|
813
|
+
.listOperations()
|
|
814
|
+
.map(({ operation }) => operation)
|
|
815
|
+
.filter((operation) => operation.kind === 'asset.library.import' &&
|
|
816
|
+
(state.config.workspaceMode !== 'multi' ||
|
|
817
|
+
operation.workspaceId === archiveWorkspaceId) &&
|
|
818
|
+
(operation.directoryName ?? 'basic') === directoryName)
|
|
819
|
+
.flatMap((operation) => operation.result && 'bundleReceipt' in operation.result
|
|
820
|
+
? (operation.result.bundleReceipt?.files ?? [])
|
|
821
|
+
: [])
|
|
822
|
+
.map((file) => file.path)
|
|
823
|
+
.filter((path) => path.toLowerCase().endsWith('.glb'));
|
|
824
|
+
await serveWorkspaceArchive(root, request, response, {
|
|
825
|
+
excludedRoots: [state.config.stateDir],
|
|
701
826
|
expectedModelPaths: [...new Set(expectedModelPaths)],
|
|
702
|
-
verifyProvenance: provenance => verifyRuntimeAssetProvenance(state, provenance)
|
|
827
|
+
verifyProvenance: (provenance) => verifyRuntimeAssetProvenance(state, provenance),
|
|
828
|
+
});
|
|
703
829
|
return;
|
|
704
830
|
}
|
|
705
|
-
if (['GET', 'HEAD'].includes(request.method ?? '') &&
|
|
831
|
+
if (['GET', 'HEAD'].includes(request.method ?? '') &&
|
|
832
|
+
url.pathname === '/runtime/file-content') {
|
|
706
833
|
if (writeDestroyed(state, response))
|
|
707
834
|
return;
|
|
708
835
|
const root = await workspaceRoot(state, url.searchParams.get('workspaceId') ?? undefined, directoryName);
|
|
@@ -738,8 +865,10 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
738
865
|
}
|
|
739
866
|
const controller = new AbortController();
|
|
740
867
|
const abort = () => controller.abort();
|
|
741
|
-
const closedRequest = () => {
|
|
742
|
-
|
|
868
|
+
const closedRequest = () => {
|
|
869
|
+
if (request.aborted || !request.complete)
|
|
870
|
+
abort();
|
|
871
|
+
};
|
|
743
872
|
request.once('aborted', abort);
|
|
744
873
|
request.once('close', closedRequest);
|
|
745
874
|
response.once('close', abort);
|
|
@@ -752,7 +881,8 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
752
881
|
catch (error) {
|
|
753
882
|
if (error instanceof Error && error.name === 'AbortError')
|
|
754
883
|
return;
|
|
755
|
-
if (error instanceof Error &&
|
|
884
|
+
if (error instanceof Error &&
|
|
885
|
+
['invalid_workspace_search', 'invalid_workspace_search_cursor'].includes(error.message))
|
|
756
886
|
writeJson(response, 400, { error: error.message });
|
|
757
887
|
else
|
|
758
888
|
throw error;
|
|
@@ -820,7 +950,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
820
950
|
catch (error) {
|
|
821
951
|
if (error instanceof WorkspaceContextError)
|
|
822
952
|
throw error;
|
|
823
|
-
writeJson(response, 409, {
|
|
953
|
+
writeJson(response, 409, {
|
|
954
|
+
error: error instanceof RemixReferenceError ? error.message : 'remix_reference_unavailable',
|
|
955
|
+
});
|
|
824
956
|
}
|
|
825
957
|
return;
|
|
826
958
|
}
|
|
@@ -843,13 +975,17 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
843
975
|
return;
|
|
844
976
|
const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
845
977
|
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)));
|
|
978
|
+
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
979
|
writeJson(response, 200, view);
|
|
848
980
|
}
|
|
849
981
|
catch (error) {
|
|
850
982
|
if (error instanceof WorkspaceContextError)
|
|
851
983
|
throw error;
|
|
852
|
-
writeJson(response, 409, {
|
|
984
|
+
writeJson(response, 409, {
|
|
985
|
+
error: error instanceof Error && error.message.startsWith('workspace_skills_')
|
|
986
|
+
? error.message
|
|
987
|
+
: 'workspace_skills_unavailable',
|
|
988
|
+
});
|
|
853
989
|
}
|
|
854
990
|
return;
|
|
855
991
|
}
|
|
@@ -866,10 +1002,18 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
866
1002
|
}
|
|
867
1003
|
const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
868
1004
|
const root = await workspaceRoot(state, workspaceId, directoryName), scope = revisionScope(state, workspaceId, directoryName);
|
|
869
|
-
const first = await readWorkspaceLevelEditor(root, state.journal.fileRevisions(scope), {
|
|
1005
|
+
const first = await readWorkspaceLevelEditor(root, state.journal.fileRevisions(scope), {
|
|
1006
|
+
state,
|
|
1007
|
+
workspaceId,
|
|
1008
|
+
directoryName,
|
|
1009
|
+
});
|
|
870
1010
|
for (const document of Object.values(first.definition?.documents ?? {}))
|
|
871
1011
|
await withObservedFile(state, workspaceId, document.path, async () => undefined, directoryName);
|
|
872
|
-
writeJson(response, 200, await readWorkspaceLevelEditor(root, state.journal.fileRevisions(scope), {
|
|
1012
|
+
writeJson(response, 200, await readWorkspaceLevelEditor(root, state.journal.fileRevisions(scope), {
|
|
1013
|
+
state,
|
|
1014
|
+
workspaceId,
|
|
1015
|
+
directoryName,
|
|
1016
|
+
}));
|
|
873
1017
|
return;
|
|
874
1018
|
}
|
|
875
1019
|
if (request.method === 'GET' && url.pathname === '/runtime/level-editor/tasks') {
|
|
@@ -880,7 +1024,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
880
1024
|
return;
|
|
881
1025
|
}
|
|
882
1026
|
const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
883
|
-
writeJson(response, 200, {
|
|
1027
|
+
writeJson(response, 200, {
|
|
1028
|
+
tasks: await listLevelEditorTasks(state, workspaceId, directoryName, await workspaceRoot(state, workspaceId, directoryName)),
|
|
1029
|
+
});
|
|
884
1030
|
return;
|
|
885
1031
|
}
|
|
886
1032
|
if (request.method === 'GET' && url.pathname === '/runtime/skill-activation-requests') {
|
|
@@ -892,7 +1038,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
892
1038
|
}
|
|
893
1039
|
const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
894
1040
|
await workspaceRoot(state, workspaceId, directoryName);
|
|
895
|
-
writeJson(response, 200, {
|
|
1041
|
+
writeJson(response, 200, {
|
|
1042
|
+
requests: await listSkillActivationRequests(state, workspaceId, directoryName),
|
|
1043
|
+
});
|
|
896
1044
|
return;
|
|
897
1045
|
}
|
|
898
1046
|
if (request.method === 'GET' && url.pathname === '/runtime/level-editor/requests') {
|
|
@@ -904,7 +1052,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
904
1052
|
}
|
|
905
1053
|
const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
906
1054
|
await workspaceRoot(state, workspaceId, directoryName);
|
|
907
|
-
writeJson(response, 200, {
|
|
1055
|
+
writeJson(response, 200, {
|
|
1056
|
+
requests: await listLevelEditorRequests(state, workspaceId, directoryName),
|
|
1057
|
+
});
|
|
908
1058
|
return;
|
|
909
1059
|
}
|
|
910
1060
|
if (request.method === 'GET' && url.pathname === '/runtime/parameters') {
|
|
@@ -922,14 +1072,18 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
922
1072
|
}
|
|
923
1073
|
const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
924
1074
|
const root = await workspaceRoot(state, workspaceId, directoryName);
|
|
925
|
-
writeJson(response, 200, {
|
|
1075
|
+
writeJson(response, 200, {
|
|
1076
|
+
tasks: await listParameterAgentTasks(state, workspaceId, directoryName, root),
|
|
1077
|
+
});
|
|
926
1078
|
return;
|
|
927
1079
|
}
|
|
928
1080
|
if (request.method === 'GET' && url.pathname === '/runtime/terminals') {
|
|
929
1081
|
if (writeDestroyed(state, response))
|
|
930
1082
|
return;
|
|
931
1083
|
writeJson(response, 200, {
|
|
932
|
-
terminals: state.journal
|
|
1084
|
+
terminals: state.journal
|
|
1085
|
+
.listTerminals(url.searchParams.get('workspaceId') ?? undefined)
|
|
1086
|
+
.filter((terminal) => (terminal.directoryName ?? 'basic') === directoryName),
|
|
933
1087
|
});
|
|
934
1088
|
return;
|
|
935
1089
|
}
|
|
@@ -957,7 +1111,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
957
1111
|
directoryName,
|
|
958
1112
|
sessionId: url.searchParams.get('sessionId') ?? undefined,
|
|
959
1113
|
}));
|
|
960
|
-
const observed = operation
|
|
1114
|
+
const observed = operation
|
|
1115
|
+
? await observeLevelEditorTransaction(state, operation)
|
|
1116
|
+
: undefined;
|
|
961
1117
|
writeJson(response, observed ? 200 : 404, observed ?? { error: 'operation_not_found' });
|
|
962
1118
|
return;
|
|
963
1119
|
}
|
|
@@ -971,8 +1127,9 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
971
1127
|
return;
|
|
972
1128
|
}
|
|
973
1129
|
const operation = state.journal.getOperationById(operationLog[1]);
|
|
974
|
-
if (!operation ||
|
|
975
|
-
|
|
1130
|
+
if (!operation ||
|
|
1131
|
+
operation.workspaceId !== workspaceId ||
|
|
1132
|
+
(operation.directoryName ?? 'basic') !== directoryName) {
|
|
976
1133
|
writeJson(response, 404, { error: 'operation_not_found' });
|
|
977
1134
|
return;
|
|
978
1135
|
}
|
|
@@ -996,13 +1153,16 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
996
1153
|
return;
|
|
997
1154
|
}
|
|
998
1155
|
if (request.method === 'POST' && url.pathname === '/runtime/scheduled-task-proposals/source') {
|
|
999
|
-
const body = await readJson(request);
|
|
1000
|
-
if (!body ||
|
|
1001
|
-
|
|
1156
|
+
const body = (await readJson(request));
|
|
1157
|
+
if (!body ||
|
|
1158
|
+
Object.keys(body).some((key) => !['bridge', 'threadId', 'turnId', 'toolCallId'].includes(key)) ||
|
|
1159
|
+
!['bridge', 'threadId', 'turnId', 'toolCallId'].every((key) => typeof body[key] === 'string' && Boolean(body[key]))) {
|
|
1002
1160
|
writeJson(response, 400, { valid: false });
|
|
1003
1161
|
return;
|
|
1004
1162
|
}
|
|
1005
|
-
writeJson(response, 200, {
|
|
1163
|
+
writeJson(response, 200, {
|
|
1164
|
+
valid: state.native?.scheduledTaskProposalSource?.(body) === true,
|
|
1165
|
+
});
|
|
1006
1166
|
return;
|
|
1007
1167
|
}
|
|
1008
1168
|
if (request.method === 'POST' && url.pathname === '/runtime/operations') {
|
|
@@ -1018,13 +1178,17 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
1018
1178
|
writeJson(response, 409, { error: 'directory_busy' });
|
|
1019
1179
|
return;
|
|
1020
1180
|
}
|
|
1021
|
-
if (request.method === 'GET' &&
|
|
1181
|
+
if (request.method === 'GET' &&
|
|
1182
|
+
['/runtime/connectivity', '/runtime/files'].includes(url.pathname)) {
|
|
1022
1183
|
const cause = error instanceof Error && error.cause ? error.cause : error;
|
|
1023
1184
|
if (['EACCES', 'EPERM'].includes(String(cause?.code))) {
|
|
1024
1185
|
writeJson(response, 409, { error: 'workspace_permission_denied' });
|
|
1025
1186
|
return;
|
|
1026
1187
|
}
|
|
1027
|
-
if (url.pathname === '/runtime/connectivity' &&
|
|
1188
|
+
if (url.pathname === '/runtime/connectivity' &&
|
|
1189
|
+
error instanceof WorkspaceContextError &&
|
|
1190
|
+
error.cause &&
|
|
1191
|
+
!['ENOENT', 'ENOTDIR'].includes(String(cause?.code))) {
|
|
1028
1192
|
writeJson(response, 500, { error: 'workspace_access_failed' });
|
|
1029
1193
|
return;
|
|
1030
1194
|
}
|
|
@@ -1058,7 +1222,7 @@ function handshake(state) {
|
|
|
1058
1222
|
if (item.name === 'conversation') {
|
|
1059
1223
|
if (conversationReady)
|
|
1060
1224
|
return capability('conversation', true);
|
|
1061
|
-
return capability('conversation', false, destroyed ? 'runtime_destroyed' : state.nativeError ?? 'adapter_not_started');
|
|
1225
|
+
return capability('conversation', false, destroyed ? 'runtime_destroyed' : (state.nativeError ?? 'adapter_not_started'));
|
|
1062
1226
|
}
|
|
1063
1227
|
if (item.name === 'files') {
|
|
1064
1228
|
return filesReady
|
|
@@ -1083,7 +1247,9 @@ function handshake(state) {
|
|
|
1083
1247
|
if (destroyed)
|
|
1084
1248
|
return capability('desktop', false, 'runtime_destroyed');
|
|
1085
1249
|
const status = desktopHost(state).snapshot().status;
|
|
1086
|
-
return status === 'ready'
|
|
1250
|
+
return status === 'ready'
|
|
1251
|
+
? capability('desktop', true)
|
|
1252
|
+
: capability('desktop', false, status);
|
|
1087
1253
|
}
|
|
1088
1254
|
if (item.name === 'parameters') {
|
|
1089
1255
|
return destroyed
|
|
@@ -1142,8 +1308,15 @@ async function acceptOperation(state, request, response) {
|
|
|
1142
1308
|
if (parsed.data.kind === 'workspace.references.import') {
|
|
1143
1309
|
const source = workspaceRemixReferenceImportRequestSchema.safeParse(parsed.data.payload);
|
|
1144
1310
|
const snapshot = parsed.data.remixReferenceSnapshot;
|
|
1145
|
-
if (!source.success ||
|
|
1146
|
-
snapshot
|
|
1311
|
+
if (!source.success ||
|
|
1312
|
+
(snapshot &&
|
|
1313
|
+
(snapshot.source.environment !== state.config.environment ||
|
|
1314
|
+
('builtinTemplate' in source.data.source
|
|
1315
|
+
? !('builtinTemplate' in snapshot.source) ||
|
|
1316
|
+
snapshot.source.builtinTemplate !== source.data.source.builtinTemplate
|
|
1317
|
+
: !('projectUuid' in snapshot.source) ||
|
|
1318
|
+
snapshot.source.projectUuid !== source.data.source.projectUuid ||
|
|
1319
|
+
snapshot.source.branchName !== source.data.source.branchName)))) {
|
|
1147
1320
|
writeJson(response, 400, { error: 'invalid_remix_reference' });
|
|
1148
1321
|
return;
|
|
1149
1322
|
}
|
|
@@ -1152,13 +1325,20 @@ async function acceptOperation(state, request, response) {
|
|
|
1152
1325
|
writeJson(response, 400, { error: 'invalid_operation', reason: 'sessionId_required' });
|
|
1153
1326
|
return;
|
|
1154
1327
|
}
|
|
1155
|
-
if (parsed.data.directoryName === 'root' &&
|
|
1328
|
+
if (parsed.data.directoryName === 'root' &&
|
|
1329
|
+
!parsed.data.kind.startsWith('session.') &&
|
|
1330
|
+
!['runtime.ensure', 'terminal.create'].includes(parsed.data.kind)) {
|
|
1156
1331
|
writeJson(response, 400, { error: 'invalid_workspace_directory' });
|
|
1157
1332
|
return;
|
|
1158
1333
|
}
|
|
1159
|
-
if (state.config.workspaceMode === 'multi' &&
|
|
1334
|
+
if (state.config.workspaceMode === 'multi' &&
|
|
1335
|
+
parsed.data.kind === 'runtime.ensure' &&
|
|
1336
|
+
parsed.data.workspaceId &&
|
|
1337
|
+
parsed.data.directoryName !== 'root') {
|
|
1160
1338
|
const workspaceId = parsed.data.workspaceId, directoryName = parsed.data.directoryName ?? 'basic';
|
|
1161
|
-
const restoring = state.journal
|
|
1339
|
+
const restoring = state.journal
|
|
1340
|
+
.getWorkspace(workspaceId)
|
|
1341
|
+
?.directories?.some((directory) => directory.directoryName === directoryName && directory.status === 'reclaimed') ?? false;
|
|
1162
1342
|
// Opening a resident checkout only refreshes instructions and can share its background install.
|
|
1163
1343
|
// Rebuilding a reclaimed checkout must still exclude every existing writer.
|
|
1164
1344
|
await withDirectoryWrite(state, { workspaceId, directoryName, mode: restoring ? 'exclusive' : 'shared', wait: false }, async () => {
|
|
@@ -1169,7 +1349,8 @@ async function acceptOperation(state, request, response) {
|
|
|
1169
1349
|
await sessionWorkspaceRoot(state, parsed.data.workspaceId, parsed.data.directoryName);
|
|
1170
1350
|
// Shared classification: a new conversation writes no files, so a batch hold must not refuse it.
|
|
1171
1351
|
const occupiesDirectory = takesWorkspaceDirectory(parsed.data.kind);
|
|
1172
|
-
if (occupiesDirectory &&
|
|
1352
|
+
if (occupiesDirectory &&
|
|
1353
|
+
(await batchDirectoryHeld(state, parsed.data.workspaceId, parsed.data.directoryName))) {
|
|
1173
1354
|
writeJson(response, 409, { error: 'batch_workspace_readonly' });
|
|
1174
1355
|
return;
|
|
1175
1356
|
}
|
|
@@ -1178,15 +1359,23 @@ async function acceptOperation(state, request, response) {
|
|
|
1178
1359
|
: parsed.data.kind === 'asset.library.import'
|
|
1179
1360
|
? { selection: parsed.data.payload, snapshot: parsed.data.libraryAssetImports }
|
|
1180
1361
|
: parsed.data.kind === 'session.turn' && parsed.data.scheduledTaskProposalBridge
|
|
1181
|
-
? {
|
|
1362
|
+
? {
|
|
1363
|
+
...asRecord(parsed.data.payload),
|
|
1364
|
+
scheduledTaskProposalBridge: parsed.data.scheduledTaskProposalBridge,
|
|
1365
|
+
}
|
|
1182
1366
|
: parsed.data.payload;
|
|
1183
1367
|
const accept = async () => {
|
|
1184
1368
|
const result = await acceptOperationCore(state, parsed.data, {
|
|
1185
1369
|
leaseFence: incomingFence,
|
|
1186
1370
|
executionPayload,
|
|
1187
1371
|
validate: async (scopeKey) => {
|
|
1188
|
-
if (parsed.data.kind.startsWith('session.') &&
|
|
1189
|
-
|
|
1372
|
+
if (parsed.data.kind.startsWith('session.') &&
|
|
1373
|
+
parsed.data.sessionId &&
|
|
1374
|
+
productionSessionAttemptId(parsed.data.sessionId)) {
|
|
1375
|
+
return {
|
|
1376
|
+
status: 409,
|
|
1377
|
+
body: runtimeError('capability_unavailable', 'production_session_readonly'),
|
|
1378
|
+
};
|
|
1190
1379
|
}
|
|
1191
1380
|
if (parsed.data.kind === 'session.create') {
|
|
1192
1381
|
const creation = state.journal.findSessionCreation(parsed.data.sessionId);
|
|
@@ -1198,7 +1387,10 @@ async function acceptOperation(state, request, response) {
|
|
|
1198
1387
|
const payload = asRecord(parsed.data.payload);
|
|
1199
1388
|
const terminalId = typeof payload.terminalId === 'string' ? payload.terminalId.trim() : '';
|
|
1200
1389
|
if (!terminalId)
|
|
1201
|
-
return {
|
|
1390
|
+
return {
|
|
1391
|
+
status: 400,
|
|
1392
|
+
body: { error: 'invalid_operation', reason: 'terminalId_required' },
|
|
1393
|
+
};
|
|
1202
1394
|
const creation = state.journal.findTerminalCreation(terminalId);
|
|
1203
1395
|
const terminal = state.journal.getTerminal(terminalId);
|
|
1204
1396
|
if (creation && operationScopeKey(creation) !== scopeKey) {
|
|
@@ -1207,16 +1399,20 @@ async function acceptOperation(state, request, response) {
|
|
|
1207
1399
|
if (terminal && !creation)
|
|
1208
1400
|
return { status: 409, body: { error: 'terminal_already_bound' } };
|
|
1209
1401
|
}
|
|
1210
|
-
else if (parsed.data.kind.startsWith('session.') &&
|
|
1402
|
+
else if (parsed.data.kind.startsWith('session.') &&
|
|
1403
|
+
state.config.workspaceMode === 'multi') {
|
|
1211
1404
|
const session = state.journal.getSession(parsed.data.sessionId);
|
|
1212
1405
|
// A replaced sandbox has no binding yet; the recovering turn creates it.
|
|
1213
1406
|
const rebinding = !session && parsed.data.kind === 'session.turn' && parsed.data.sessionRecovery;
|
|
1214
|
-
if (!rebinding &&
|
|
1215
|
-
|
|
1407
|
+
if (!rebinding &&
|
|
1408
|
+
(!session ||
|
|
1409
|
+
session.workspaceId !== parsed.data.workspaceId ||
|
|
1410
|
+
(session.directoryName ?? 'basic') !== (parsed.data.directoryName ?? 'basic'))) {
|
|
1216
1411
|
return { status: 409, body: { error: 'session_workspace_mismatch' } };
|
|
1217
1412
|
}
|
|
1218
1413
|
}
|
|
1219
|
-
if (parsed.data.kind === 'workspace.references.import' &&
|
|
1414
|
+
if (parsed.data.kind === 'workspace.references.import' &&
|
|
1415
|
+
!parsed.data.remixReferenceSnapshot) {
|
|
1220
1416
|
return { status: 400, body: { error: 'remix_reference_snapshot_required' } };
|
|
1221
1417
|
}
|
|
1222
1418
|
if (parsed.data.kind === 'asset.library.import' && !parsed.data.libraryAssetImports) {
|
|
@@ -1243,7 +1439,8 @@ async function acceptOperation(state, request, response) {
|
|
|
1243
1439
|
writeJson(response, executed.status === 'failed' ? 409 : 202, executed);
|
|
1244
1440
|
};
|
|
1245
1441
|
const acceptBound = async () => {
|
|
1246
|
-
if (occupiesDirectory &&
|
|
1442
|
+
if (occupiesDirectory &&
|
|
1443
|
+
(await batchDirectoryHeld(state, parsed.data.workspaceId, parsed.data.directoryName))) {
|
|
1247
1444
|
writeJson(response, 409, { error: 'batch_workspace_readonly' });
|
|
1248
1445
|
return;
|
|
1249
1446
|
}
|
|
@@ -1319,7 +1516,7 @@ async function handleTerminalIdentity(state, request, response, remainder) {
|
|
|
1319
1516
|
if (writeDestroyed(state, response))
|
|
1320
1517
|
return;
|
|
1321
1518
|
const [terminalId, action] = remainder.split('/');
|
|
1322
|
-
if (!terminalId || action && action !== 'connect') {
|
|
1519
|
+
if (!terminalId || (action && action !== 'connect')) {
|
|
1323
1520
|
writeJson(response, 404, { error: 'not_found' });
|
|
1324
1521
|
return;
|
|
1325
1522
|
}
|
|
@@ -1424,7 +1621,12 @@ async function readObservedParameters(state, workspaceId, directoryName = 'basic
|
|
|
1424
1621
|
const scope = revisionScope(state, workspaceId, directoryName);
|
|
1425
1622
|
return state.journal.exclusive(`workspace:${scope ?? 'single'}`, async () => {
|
|
1426
1623
|
const first = await readWorkspaceParameters(root, state.journal.fileRevisions(scope));
|
|
1427
|
-
for (const path of [
|
|
1624
|
+
for (const path of [
|
|
1625
|
+
first.schemaPath,
|
|
1626
|
+
first.dataPath,
|
|
1627
|
+
first.runtimeI18n?.globalPath,
|
|
1628
|
+
first.runtimeI18n?.themePath,
|
|
1629
|
+
]) {
|
|
1428
1630
|
if (path)
|
|
1429
1631
|
await withObservedFile(state, workspaceId, path, async () => undefined, directoryName);
|
|
1430
1632
|
}
|
|
@@ -1502,7 +1704,10 @@ async function handleSourceEvents(state, request, response, url) {
|
|
|
1502
1704
|
const runtimeEpoch = epochQuery === null ? state.runtimeEpoch : Number(epochQuery);
|
|
1503
1705
|
const runtimeId = url.searchParams.get('runtimeId') ?? state.config.runtimeId;
|
|
1504
1706
|
const generation = Number(url.searchParams.get('generation') ?? state.config.generation);
|
|
1505
|
-
if (runtimeId !== state.config.runtimeId ||
|
|
1707
|
+
if (runtimeId !== state.config.runtimeId ||
|
|
1708
|
+
generation !== state.config.generation ||
|
|
1709
|
+
!Number.isInteger(runtimeEpoch) ||
|
|
1710
|
+
runtimeEpoch < 0) {
|
|
1506
1711
|
writeJson(response, 403, { error: 'source_identity_mismatch' });
|
|
1507
1712
|
return;
|
|
1508
1713
|
}
|
|
@@ -1512,15 +1717,26 @@ async function handleSourceEvents(state, request, response, url) {
|
|
|
1512
1717
|
runtimeEpoch,
|
|
1513
1718
|
after: safeAfter,
|
|
1514
1719
|
};
|
|
1515
|
-
if (!state.journal
|
|
1516
|
-
|
|
1720
|
+
if (!state.journal
|
|
1721
|
+
.epochsSnapshot()
|
|
1722
|
+
.some((epoch) => epoch.runtime_id === runtimeId &&
|
|
1723
|
+
epoch.generation === generation &&
|
|
1724
|
+
epoch.runtime_epoch === runtimeEpoch)) {
|
|
1725
|
+
writeJson(response, 409, {
|
|
1726
|
+
code: 'source_gap',
|
|
1727
|
+
retainedFrom: 1,
|
|
1728
|
+
highWater: 0,
|
|
1729
|
+
coveredThrough: safeAfter,
|
|
1730
|
+
});
|
|
1517
1731
|
return;
|
|
1518
1732
|
}
|
|
1519
1733
|
if (String(request.headers.accept ?? '').includes('text/event-stream')) {
|
|
1520
1734
|
writeSourceStream(state, request, response, query);
|
|
1521
1735
|
return;
|
|
1522
1736
|
}
|
|
1523
|
-
if (epochQuery === null &&
|
|
1737
|
+
if (epochQuery === null &&
|
|
1738
|
+
!url.searchParams.has('runtimeId') &&
|
|
1739
|
+
!url.searchParams.has('generation')) {
|
|
1524
1740
|
const events = state.journal.listEvents(safeAfter);
|
|
1525
1741
|
const page = state.journal.readSourcePage({ ...query, limit: 1 });
|
|
1526
1742
|
writeJson(response, 200, {
|