@playdrop/playdrop-cli 0.12.32 → 0.12.33
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/config/client-meta.json +2 -2
- package/dist/apiClient.js +16 -1
- package/dist/commands/upload.js +2 -0
- package/dist/commands/worker/runtime.d.ts +2 -0
- package/dist/commands/worker/runtime.js +14 -10
- package/dist/commands/worker.d.ts +9 -29
- package/dist/commands/worker.js +604 -660
- package/dist/index.js +6 -0
- package/node_modules/@playdrop/api-client/dist/client.d.ts +2 -1
- package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts +2 -1
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.js +11 -0
- package/node_modules/@playdrop/api-client/dist/index.d.ts +3 -2
- package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/index.js +5 -0
- package/node_modules/@playdrop/config/client-meta.json +2 -2
- package/node_modules/@playdrop/types/dist/agent-task-terminal.d.ts +5 -0
- package/node_modules/@playdrop/types/dist/agent-task-terminal.d.ts.map +1 -0
- package/node_modules/@playdrop/types/dist/agent-task-terminal.js +117 -0
- package/node_modules/@playdrop/types/dist/api.d.ts +97 -103
- package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/api.js +1 -1
- package/node_modules/@playdrop/types/dist/index.d.ts +1 -0
- package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/index.js +3 -1
- package/package.json +1 -1
package/dist/commands/worker.js
CHANGED
|
@@ -7,7 +7,7 @@ exports.WORKER_TASK_WORKSPACE_RETENTION = exports.WORKER_CONTEXT_COMMAND_NOT_ALL
|
|
|
7
7
|
exports.allocateWorkerDevPort = allocateWorkerDevPort;
|
|
8
8
|
exports.isWorkerContextCommandAllowed = isWorkerContextCommandAllowed;
|
|
9
9
|
exports.resolveWorkerExecutionTargetFromRole = resolveWorkerExecutionTargetFromRole;
|
|
10
|
-
exports.
|
|
10
|
+
exports.normalizeWorkerTaskAssignmentV2 = normalizeWorkerTaskAssignmentV2;
|
|
11
11
|
exports.nextClaimBackoffMs = nextClaimBackoffMs;
|
|
12
12
|
exports.claimBackoffDelayMs = claimBackoffDelayMs;
|
|
13
13
|
exports.resolveWorkerHomeDir = resolveWorkerHomeDir;
|
|
@@ -16,9 +16,7 @@ exports.hasAgentTaskUploadedArtifact = hasAgentTaskUploadedArtifact;
|
|
|
16
16
|
exports.appendWorkerTranscriptChunks = appendWorkerTranscriptChunks;
|
|
17
17
|
exports.readTaskNextStepsFile = readTaskNextStepsFile;
|
|
18
18
|
exports.assertTaskUploadResultMatchesContext = assertTaskUploadResultMatchesContext;
|
|
19
|
-
exports.
|
|
20
|
-
exports.stagePlaydropPluginReferences = stagePlaydropPluginReferences;
|
|
21
|
-
exports.stageCodexTaskSkills = stageCodexTaskSkills;
|
|
19
|
+
exports.stageAssignmentWorkspaceV2 = stageAssignmentWorkspaceV2;
|
|
22
20
|
exports.assertAssignmentPluginBundleExtractionBounds = assertAssignmentPluginBundleExtractionBounds;
|
|
23
21
|
exports.stageAssignmentPluginBundle = stageAssignmentPluginBundle;
|
|
24
22
|
exports.resolveDevPluginWorkingTree = resolveDevPluginWorkingTree;
|
|
@@ -35,12 +33,9 @@ exports.resolvePlaywrightBrowsersPathFromExecutable = resolvePlaywrightBrowsersP
|
|
|
35
33
|
exports.loadWorkerEnvFile = loadWorkerEnvFile;
|
|
36
34
|
exports.buildWorkerCapabilities = buildWorkerCapabilities;
|
|
37
35
|
exports.buildWorkerClaimBody = buildWorkerClaimBody;
|
|
38
|
-
exports.resolveWorkerBaseSource = resolveWorkerBaseSource;
|
|
39
36
|
exports.extractZipArchive = extractZipArchive;
|
|
40
|
-
exports.readWorkerProjectVersion = readWorkerProjectVersion;
|
|
41
37
|
exports.buildCataloguePreviewPayload = buildCataloguePreviewPayload;
|
|
42
|
-
exports.
|
|
43
|
-
exports.removeStaleWorkerControlFilesFromBaseSource = removeStaleWorkerControlFilesFromBaseSource;
|
|
38
|
+
exports.removeStaleWorkerControlFiles = removeStaleWorkerControlFiles;
|
|
44
39
|
exports.resolveCodexModel = resolveCodexModel;
|
|
45
40
|
exports.resolveClaudeModel = resolveClaudeModel;
|
|
46
41
|
exports.prepareCodexRunHomes = prepareCodexRunHomes;
|
|
@@ -76,6 +71,7 @@ exports.instrumentTaskChromeTitleMatchers = instrumentTaskChromeTitleMatchers;
|
|
|
76
71
|
exports.enforceInstrumentBrowserHygiene = enforceInstrumentBrowserHygiene;
|
|
77
72
|
exports.startWorker = startWorker;
|
|
78
73
|
exports.reportTask = reportTask;
|
|
74
|
+
exports.showTaskContext = showTaskContext;
|
|
79
75
|
exports.reportCatalogueTask = reportCatalogueTask;
|
|
80
76
|
exports.uploadTask = uploadTask;
|
|
81
77
|
exports.claimSlugTask = claimSlugTask;
|
|
@@ -97,6 +93,7 @@ const node_os_1 = __importDefault(require("node:os"));
|
|
|
97
93
|
const node_path_1 = __importDefault(require("node:path"));
|
|
98
94
|
const node_process_1 = __importDefault(require("node:process"));
|
|
99
95
|
const node_util_1 = require("node:util");
|
|
96
|
+
const apiClient_1 = require("../apiClient");
|
|
100
97
|
const clientInfo_1 = require("../clientInfo");
|
|
101
98
|
const commandContext_1 = require("../commandContext");
|
|
102
99
|
const config_1 = require("../config");
|
|
@@ -136,15 +133,13 @@ const CLAIM_BACKOFF_JITTER_MS = 500;
|
|
|
136
133
|
const FAIL_REPORT_RETRY_DELAY_MS = 2000;
|
|
137
134
|
const WORKER_TRANSCRIPT_CHUNK_BATCH_SIZE = 100;
|
|
138
135
|
const SLACK_API_BASE_URL = 'https://slack.com/api';
|
|
139
|
-
const WORKER_WRAPPER_CONTRACT_VERSION =
|
|
136
|
+
const WORKER_WRAPPER_CONTRACT_VERSION = 2;
|
|
140
137
|
const WORKER_SUPPORTED_KINDS = ['NEW_GAME', 'REMIX_GAME', 'GAME_UPDATE', 'GAME_REVIEW', 'GAME_EVAL'];
|
|
141
|
-
const PLAYDROP_PLUGIN_MANIFEST_REQUIRED_STARTUP_KINDS = WORKER_SUPPORTED_KINDS;
|
|
142
138
|
const requireFromWorker = (0, node_module_1.createRequire)(__filename);
|
|
143
|
-
const
|
|
144
|
-
const STAGED_CODEX_SKILL_ROOT = '.agents/skills';
|
|
145
|
-
const PLAYDROP_PLUGIN_STAGING_MANIFEST_PATH = 'playdrop-worker-staging.json';
|
|
139
|
+
const AGENT_BUNDLE_MANIFEST_PATH = 'agent-bundle.json';
|
|
146
140
|
const ASSIGNMENT_PLUGIN_BUNDLE_MAX_FILES = 1000;
|
|
147
141
|
const ASSIGNMENT_PLUGIN_BUNDLE_MAX_UNCOMPRESSED_BYTES = 20 * 1024 * 1024;
|
|
142
|
+
const ASSIGNMENT_WORKSPACE_ARCHIVE_MAX_UNCOMPRESSED_BYTES = 100 * 1024 * 1024;
|
|
148
143
|
const ZIP_CENTRAL_DIRECTORY_SIGNATURE = 0x02014b50;
|
|
149
144
|
const ZIP_END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50;
|
|
150
145
|
const ZIP_END_OF_CENTRAL_DIRECTORY_MIN_BYTES = 22;
|
|
@@ -152,6 +147,7 @@ const ZIP_MAX_COMMENT_BYTES = 0xffff;
|
|
|
152
147
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
153
148
|
exports.WORKER_SESSION_EXPIRED_MESSAGE = 'worker session expired: run "playdrop auth login" and start the worker again';
|
|
154
149
|
const WORKER_CONTEXT_ALLOWED_COMMANDS = [
|
|
150
|
+
['task', 'context'],
|
|
155
151
|
['task', 'report'],
|
|
156
152
|
['task', 'report-catalogue'],
|
|
157
153
|
['task', 'claim-slug'],
|
|
@@ -300,116 +296,162 @@ function normalizeAssignmentAgent(value) {
|
|
|
300
296
|
}
|
|
301
297
|
return value;
|
|
302
298
|
}
|
|
303
|
-
function normalizeAssignmentKind(value, task) {
|
|
304
|
-
if (value !== 'NEW_GAME' && value !== 'REMIX_GAME' && value !== 'GAME_UPDATE' && value !== 'GAME_REVIEW' && value !== 'GAME_EVAL') {
|
|
305
|
-
throw new Error('agent_task_assignment_kind_missing');
|
|
306
|
-
}
|
|
307
|
-
if (value !== task.kind) {
|
|
308
|
-
throw new Error('agent_task_assignment_kind_mismatch');
|
|
309
|
-
}
|
|
310
|
-
return value;
|
|
311
|
-
}
|
|
312
299
|
function normalizeAssignmentTarget(value) {
|
|
313
300
|
if (value !== 'FIRST_PARTY' && value !== 'PERSONAL') {
|
|
314
301
|
throw new Error('agent_task_assignment_target_missing');
|
|
315
302
|
}
|
|
316
303
|
return value;
|
|
317
304
|
}
|
|
318
|
-
function
|
|
319
|
-
|
|
320
|
-
|
|
305
|
+
function normalizeAssignmentContent(input) {
|
|
306
|
+
const relativePath = normalizeAssignmentWorkspaceRelativePath(input.path);
|
|
307
|
+
const contentBase64 = typeof input.contentBase64 === 'string' ? input.contentBase64.trim() : '';
|
|
308
|
+
const sha256 = typeof input.sha256 === 'string' ? input.sha256.trim() : '';
|
|
309
|
+
const sizeBytes = Number(input.sizeBytes);
|
|
310
|
+
if (!contentBase64 || !/^[A-Za-z0-9+/]+={0,2}$/.test(contentBase64)) {
|
|
311
|
+
throw new Error(`${input.code}_content_invalid`);
|
|
321
312
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
throw new Error('agent_task_claim_missing_task_assignment');
|
|
313
|
+
if (!/^[a-f0-9]{64}$/.test(sha256) || !Number.isInteger(sizeBytes) || sizeBytes <= 0) {
|
|
314
|
+
throw new Error(`${input.code}_metadata_invalid`);
|
|
325
315
|
}
|
|
326
|
-
const
|
|
327
|
-
if (
|
|
328
|
-
throw new Error(
|
|
329
|
-
}
|
|
330
|
-
const taskId = parsePositiveInteger(assignment.taskId, 'agent_task_assignment_task_id_missing');
|
|
331
|
-
if (taskId !== claim.task.id) {
|
|
332
|
-
throw new Error('agent_task_assignment_task_id_mismatch');
|
|
316
|
+
const buffer = Buffer.from(contentBase64, 'base64');
|
|
317
|
+
if (buffer.length !== sizeBytes) {
|
|
318
|
+
throw new Error(`${input.code}_size_mismatch`);
|
|
333
319
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
throw new Error('agent_task_assignment_token_missing');
|
|
320
|
+
if ((0, node_crypto_1.createHash)('sha256').update(buffer).digest('hex') !== sha256) {
|
|
321
|
+
throw new Error(`${input.code}_sha_mismatch`);
|
|
337
322
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
? null
|
|
344
|
-
: typeof assignment.reasoningEffort === 'string'
|
|
345
|
-
? assignment.reasoningEffort.trim()
|
|
346
|
-
: '';
|
|
347
|
-
if (reasoningEffort !== null && (!reasoningEffort || reasoningEffort.length > 40 || /\s/.test(reasoningEffort))) {
|
|
348
|
-
throw new Error('invalid_agent_task_reasoning_effort');
|
|
323
|
+
return { path: relativePath, contentBase64, sha256, sizeBytes, buffer };
|
|
324
|
+
}
|
|
325
|
+
function normalizeWorkerTaskAssignmentV2(claim) {
|
|
326
|
+
if (claim.protocolVersion !== 2) {
|
|
327
|
+
throw new Error('agent_task_claim_protocol_version_mismatch');
|
|
349
328
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
: null;
|
|
353
|
-
if (!workspace) {
|
|
354
|
-
throw new Error('agent_task_assignment_workspace_missing');
|
|
329
|
+
if (claim.action !== 'run') {
|
|
330
|
+
return null;
|
|
355
331
|
}
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
332
|
+
const assignment = claim.assignment;
|
|
333
|
+
const taskId = parsePositiveInteger(assignment.task.id, 'agent_task_assignment_task_id_missing');
|
|
334
|
+
const taskToken = typeof assignment.task.token === 'string' ? assignment.task.token.trim() : '';
|
|
335
|
+
const taskType = typeof assignment.task.type === 'string' ? assignment.task.type.trim() : '';
|
|
336
|
+
const attempt = parsePositiveInteger(assignment.task.attempt, 'agent_task_assignment_attempt_missing');
|
|
337
|
+
const prompt = typeof assignment.request.prompt === 'string' ? assignment.request.prompt : '';
|
|
338
|
+
const model = typeof assignment.agent.model === 'string' ? assignment.agent.model.trim() : '';
|
|
339
|
+
if (!taskToken || !taskType || !prompt.trim() || !model) {
|
|
340
|
+
throw new Error('agent_task_assignment_invalid');
|
|
341
|
+
}
|
|
342
|
+
const workspacePaths = new Set();
|
|
343
|
+
const files = assignment.workspace.files.map((file) => {
|
|
344
|
+
const normalized = normalizeAssignmentContent({
|
|
345
|
+
path: file.path,
|
|
346
|
+
contentBase64: file.contentBase64,
|
|
347
|
+
sha256: file.sha256,
|
|
348
|
+
sizeBytes: file.sizeBytes,
|
|
349
|
+
code: 'agent_task_assignment_workspace_file',
|
|
350
|
+
});
|
|
351
|
+
const contentType = typeof file.contentType === 'string' ? file.contentType.trim() : '';
|
|
352
|
+
if (!contentType) {
|
|
353
|
+
throw new Error('agent_task_assignment_workspace_file_content_type_missing');
|
|
354
|
+
}
|
|
355
|
+
if (workspacePaths.has(normalized.path)) {
|
|
356
|
+
throw new Error('agent_task_assignment_workspace_path_duplicate');
|
|
357
|
+
}
|
|
358
|
+
workspacePaths.add(normalized.path);
|
|
359
|
+
return {
|
|
360
|
+
path: normalized.path,
|
|
361
|
+
sha256: normalized.sha256,
|
|
362
|
+
sizeBytes: normalized.sizeBytes,
|
|
363
|
+
contentType,
|
|
364
|
+
contentBase64: normalized.contentBase64,
|
|
365
|
+
};
|
|
366
|
+
});
|
|
367
|
+
const archives = assignment.workspace.archives.map((archive) => {
|
|
368
|
+
if (archive.format !== 'zip') {
|
|
369
|
+
throw new Error('agent_task_assignment_workspace_archive_format_invalid');
|
|
370
|
+
}
|
|
371
|
+
const normalized = normalizeAssignmentContent({
|
|
372
|
+
path: archive.path,
|
|
373
|
+
contentBase64: archive.contentBase64,
|
|
374
|
+
sha256: archive.sha256,
|
|
375
|
+
sizeBytes: archive.sizeBytes,
|
|
376
|
+
code: 'agent_task_assignment_workspace_archive',
|
|
377
|
+
});
|
|
378
|
+
if (workspacePaths.has(normalized.path)) {
|
|
379
|
+
throw new Error('agent_task_assignment_workspace_path_duplicate');
|
|
380
|
+
}
|
|
381
|
+
workspacePaths.add(normalized.path);
|
|
382
|
+
return {
|
|
383
|
+
path: normalized.path,
|
|
384
|
+
format: 'zip',
|
|
385
|
+
sha256: normalized.sha256,
|
|
386
|
+
sizeBytes: normalized.sizeBytes,
|
|
387
|
+
contentBase64: normalized.contentBase64,
|
|
388
|
+
};
|
|
389
|
+
});
|
|
390
|
+
const fileByPath = new Map(files.map((file) => [file.path, file]));
|
|
391
|
+
const attachmentIds = new Set();
|
|
392
|
+
const attachments = assignment.request.attachments.map((attachment) => {
|
|
393
|
+
const id = typeof attachment.id === 'string' ? attachment.id.trim() : '';
|
|
394
|
+
const attachmentPath = normalizeAssignmentWorkspaceRelativePath(attachment.path);
|
|
395
|
+
const fileName = typeof attachment.fileName === 'string' ? attachment.fileName.trim() : '';
|
|
396
|
+
const contentType = typeof attachment.contentType === 'string' ? attachment.contentType.trim() : '';
|
|
397
|
+
const sha256 = typeof attachment.sha256 === 'string' ? attachment.sha256.trim() : '';
|
|
398
|
+
const sizeBytes = Number(attachment.sizeBytes);
|
|
399
|
+
const file = fileByPath.get(attachmentPath);
|
|
400
|
+
if (!id || attachmentIds.has(id) || !fileName || !contentType || !file) {
|
|
401
|
+
throw new Error('agent_task_assignment_attachment_invalid');
|
|
402
|
+
}
|
|
403
|
+
if (sha256 !== file.sha256 || sizeBytes !== file.sizeBytes || contentType !== file.contentType) {
|
|
404
|
+
throw new Error('agent_task_assignment_attachment_metadata_mismatch');
|
|
405
|
+
}
|
|
406
|
+
attachmentIds.add(id);
|
|
407
|
+
return {
|
|
408
|
+
id,
|
|
409
|
+
path: attachmentPath,
|
|
410
|
+
fileName,
|
|
411
|
+
contentType,
|
|
412
|
+
sizeBytes,
|
|
413
|
+
sha256,
|
|
414
|
+
description: typeof attachment.description === 'string' ? attachment.description : null,
|
|
415
|
+
};
|
|
416
|
+
});
|
|
417
|
+
for (const attachment of attachments) {
|
|
418
|
+
if (!fileByPath.has(attachment.path)) {
|
|
419
|
+
throw new Error('agent_task_assignment_attachment_file_missing');
|
|
420
|
+
}
|
|
361
421
|
}
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
if (!output) {
|
|
366
|
-
throw new Error('agent_task_assignment_output_missing');
|
|
422
|
+
const browser = assignment.agent.browser;
|
|
423
|
+
if (browser !== 'NONE' && browser !== 'CHROME' && browser !== 'PLAYWRIGHT') {
|
|
424
|
+
throw new Error('agent_task_assignment_browser_invalid');
|
|
367
425
|
}
|
|
368
|
-
const
|
|
369
|
-
if (
|
|
370
|
-
throw new Error('
|
|
371
|
-
}
|
|
372
|
-
const pluginBundle = assignment.pluginBundle && typeof assignment.pluginBundle === 'object' && !Array.isArray(assignment.pluginBundle)
|
|
373
|
-
? assignment.pluginBundle
|
|
374
|
-
: null;
|
|
375
|
-
if (!pluginBundle) {
|
|
376
|
-
throw new Error('agent_task_assignment_plugin_bundle_missing');
|
|
426
|
+
const reasoningEffort = assignment.agent.reasoningEffort;
|
|
427
|
+
if (reasoningEffort !== null && typeof reasoningEffort !== 'string') {
|
|
428
|
+
throw new Error('agent_task_assignment_reasoning_effort_invalid');
|
|
377
429
|
}
|
|
378
430
|
return {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
selectedAttempt: assignment.selectedAttempt ?? null,
|
|
431
|
+
task: {
|
|
432
|
+
id: taskId,
|
|
433
|
+
token: taskToken,
|
|
434
|
+
type: taskType,
|
|
435
|
+
executionTarget: normalizeAssignmentTarget(assignment.task.executionTarget),
|
|
436
|
+
attempt,
|
|
437
|
+
metadata: assignment.task.metadata,
|
|
438
|
+
},
|
|
388
439
|
workspace: {
|
|
389
|
-
folderName: normalizeAssignmentWorkspaceFolder(workspace.folderName),
|
|
390
|
-
|
|
440
|
+
folderName: normalizeAssignmentWorkspaceFolder(assignment.workspace.folderName),
|
|
441
|
+
archives,
|
|
442
|
+
files,
|
|
391
443
|
},
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
attachments: Array.isArray(inputs.attachments) ? inputs.attachments : [],
|
|
397
|
-
baseSource: inputs.baseSource ?? null,
|
|
398
|
-
remixSource: inputs.remixSource ?? null,
|
|
399
|
-
reviewTarget: inputs.reviewTarget ?? null,
|
|
400
|
-
evalTarget: inputs.evalTarget ?? null,
|
|
401
|
-
allowedTemplateKeys: Array.isArray(inputs.allowedTemplateKeys)
|
|
402
|
-
? Array.from(new Set(inputs.allowedTemplateKeys.map((value) => (typeof value === 'string' ? value.trim() : '')).filter(Boolean)))
|
|
403
|
-
: [],
|
|
444
|
+
plugin: assignment.plugin,
|
|
445
|
+
request: {
|
|
446
|
+
prompt,
|
|
447
|
+
attachments,
|
|
404
448
|
},
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
releaseNoteRequired: output.releaseNoteRequired !== false,
|
|
449
|
+
agent: {
|
|
450
|
+
runtime: normalizeAssignmentAgent(assignment.agent.runtime),
|
|
451
|
+
model,
|
|
452
|
+
reasoningEffort,
|
|
453
|
+
browser,
|
|
411
454
|
},
|
|
412
|
-
prompt,
|
|
413
455
|
};
|
|
414
456
|
}
|
|
415
457
|
function nextClaimBackoffMs(currentMs) {
|
|
@@ -458,73 +500,55 @@ async function pruneWorkerTaskWorkspaces(incomingWorkspaceDir, retention = expor
|
|
|
458
500
|
}
|
|
459
501
|
return removed;
|
|
460
502
|
}
|
|
461
|
-
function buildWorkerTaskContext(
|
|
462
|
-
const
|
|
463
|
-
if (!
|
|
464
|
-
throw new Error('
|
|
465
|
-
}
|
|
466
|
-
if (task.id !== assignment.taskId) {
|
|
467
|
-
throw new Error('agent_task_context_task_id_mismatch');
|
|
468
|
-
}
|
|
469
|
-
if (task.kind !== assignment.kind) {
|
|
470
|
-
throw new Error('agent_task_context_kind_mismatch');
|
|
503
|
+
function buildWorkerTaskContext(input) {
|
|
504
|
+
const playdrop = input.metadata.playdrop;
|
|
505
|
+
if (!playdrop || typeof playdrop !== 'object' || Array.isArray(playdrop)) {
|
|
506
|
+
throw new Error('agent_task_context_playdrop_metadata_missing');
|
|
471
507
|
}
|
|
472
|
-
|
|
473
|
-
|
|
508
|
+
const metadata = playdrop;
|
|
509
|
+
const kind = metadata.kind;
|
|
510
|
+
if (kind !== 'NEW_GAME' && kind !== 'REMIX_GAME' && kind !== 'GAME_UPDATE' && kind !== 'GAME_REVIEW' && kind !== 'GAME_EVAL') {
|
|
511
|
+
throw new Error('agent_task_context_kind_missing');
|
|
474
512
|
}
|
|
475
|
-
const creatorUsername = typeof
|
|
476
|
-
|
|
513
|
+
const creatorUsername = typeof metadata.creatorUsername === 'string'
|
|
514
|
+
? metadata.creatorUsername.trim()
|
|
515
|
+
: '';
|
|
516
|
+
const output = metadata.output && typeof metadata.output === 'object' && !Array.isArray(metadata.output)
|
|
517
|
+
? metadata.output
|
|
518
|
+
: null;
|
|
519
|
+
const outputVersion = typeof output?.version === 'string' ? output.version.trim() : '';
|
|
520
|
+
const outputAppName = typeof output?.appName === 'string' && output.appName.trim()
|
|
521
|
+
? output.appName.trim()
|
|
522
|
+
: null;
|
|
477
523
|
if (!creatorUsername || !outputVersion) {
|
|
478
524
|
throw new Error('agent_task_context_invalid');
|
|
479
525
|
}
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
throw new Error('agent_task_context_token_missing');
|
|
483
|
-
}
|
|
484
|
-
const attempt = Number(task.attempts);
|
|
485
|
-
if (!Number.isInteger(attempt) || attempt <= 0) {
|
|
486
|
-
throw new Error('agent_task_context_attempt_missing');
|
|
487
|
-
}
|
|
488
|
-
const outputAppName = typeof assignment.output.appName === 'string' && assignment.output.appName.trim()
|
|
489
|
-
? assignment.output.appName.trim()
|
|
526
|
+
const remixSource = metadata.remixSource && typeof metadata.remixSource === 'object' && !Array.isArray(metadata.remixSource)
|
|
527
|
+
? metadata.remixSource
|
|
490
528
|
: null;
|
|
491
|
-
const
|
|
492
|
-
?
|
|
529
|
+
const remixSourceRef = typeof remixSource?.ref === 'string' && remixSource.ref.trim()
|
|
530
|
+
? remixSource.ref.trim()
|
|
493
531
|
: null;
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
const parsedReviewAppVersionId = Number(reviewTarget?.appVersionId);
|
|
497
|
-
if (!Number.isInteger(parsedReviewAppVersionId) || parsedReviewAppVersionId <= 0) {
|
|
498
|
-
throw new Error('agent_task_context_review_target_missing');
|
|
499
|
-
}
|
|
500
|
-
reviewAppVersionId = parsedReviewAppVersionId;
|
|
501
|
-
}
|
|
502
|
-
const remixSource = assignment.inputs.remixSource && typeof assignment.inputs.remixSource === 'object'
|
|
503
|
-
? assignment.inputs.remixSource
|
|
532
|
+
const reviewTarget = metadata.reviewTarget && typeof metadata.reviewTarget === 'object' && !Array.isArray(metadata.reviewTarget)
|
|
533
|
+
? metadata.reviewTarget
|
|
504
534
|
: null;
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
remixSourceRef = parsedRemixSourceRef;
|
|
512
|
-
}
|
|
513
|
-
const allowedTemplateKeys = Array.isArray(assignment.inputs.allowedTemplateKeys)
|
|
514
|
-
? Array.from(new Set(assignment.inputs.allowedTemplateKeys.map((value) => (typeof value === 'string' ? value.trim() : '')).filter(Boolean)))
|
|
535
|
+
const rawReviewAppVersionId = Number(reviewTarget?.appVersionId);
|
|
536
|
+
const reviewAppVersionId = Number.isInteger(rawReviewAppVersionId) && rawReviewAppVersionId > 0
|
|
537
|
+
? rawReviewAppVersionId
|
|
538
|
+
: null;
|
|
539
|
+
const allowedTemplateKeys = Array.isArray(metadata.allowedTemplateKeys)
|
|
540
|
+
? metadata.allowedTemplateKeys.filter((value) => typeof value === 'string' && Boolean(value.trim()))
|
|
515
541
|
: [];
|
|
516
|
-
const creatorRequest = assignment.inputs.request.prompt.trim();
|
|
517
|
-
if (!creatorRequest) {
|
|
518
|
-
throw new Error('agent_task_context_creator_request_missing');
|
|
519
|
-
}
|
|
520
542
|
return {
|
|
521
|
-
taskId:
|
|
522
|
-
attempt,
|
|
523
|
-
taskToken,
|
|
524
|
-
|
|
543
|
+
taskId: input.taskId,
|
|
544
|
+
attempt: input.attempt,
|
|
545
|
+
taskToken: input.taskToken,
|
|
546
|
+
taskType: input.taskType,
|
|
547
|
+
metadata: input.metadata,
|
|
548
|
+
kind,
|
|
525
549
|
creatorUsername,
|
|
526
|
-
creatorRequest,
|
|
527
|
-
target:
|
|
550
|
+
creatorRequest: input.prompt,
|
|
551
|
+
target: input.target,
|
|
528
552
|
outputAppName,
|
|
529
553
|
outputVersion,
|
|
530
554
|
remixSourceRef,
|
|
@@ -532,12 +556,62 @@ function buildWorkerTaskContext(claim, assignment) {
|
|
|
532
556
|
reviewAppVersionId,
|
|
533
557
|
};
|
|
534
558
|
}
|
|
559
|
+
function buildWorkerTaskContextV2(assignment) {
|
|
560
|
+
return buildWorkerTaskContext({
|
|
561
|
+
taskId: assignment.task.id,
|
|
562
|
+
attempt: assignment.task.attempt,
|
|
563
|
+
taskToken: assignment.task.token,
|
|
564
|
+
taskType: assignment.task.type,
|
|
565
|
+
metadata: assignment.task.metadata,
|
|
566
|
+
prompt: assignment.request.prompt,
|
|
567
|
+
target: assignment.task.executionTarget,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
535
570
|
function workerTaskContextPath(workspaceDir) {
|
|
536
571
|
return node_path_1.default.join(workspaceDir, '.playdrop', 'task.json');
|
|
537
572
|
}
|
|
538
573
|
function workerTaskUploadResultPath(workspaceDir) {
|
|
539
574
|
return node_path_1.default.join(workspaceDir, '.playdrop', 'task-upload-result.json');
|
|
540
575
|
}
|
|
576
|
+
function serializeWorkerTaskContext(taskContext) {
|
|
577
|
+
const playdrop = taskContext.metadata.playdrop;
|
|
578
|
+
if (!playdrop || typeof playdrop !== 'object' || Array.isArray(playdrop)) {
|
|
579
|
+
throw new Error('agent_task_context_playdrop_metadata_missing');
|
|
580
|
+
}
|
|
581
|
+
const playdropMetadata = playdrop;
|
|
582
|
+
const output = playdropMetadata.output;
|
|
583
|
+
if (!output || typeof output !== 'object' || Array.isArray(output)) {
|
|
584
|
+
throw new Error('agent_task_context_invalid');
|
|
585
|
+
}
|
|
586
|
+
const metadata = {
|
|
587
|
+
...taskContext.metadata,
|
|
588
|
+
playdrop: {
|
|
589
|
+
...playdropMetadata,
|
|
590
|
+
output: {
|
|
591
|
+
...output,
|
|
592
|
+
appName: taskContext.outputAppName,
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
};
|
|
596
|
+
return {
|
|
597
|
+
protocolVersion: 2,
|
|
598
|
+
task: {
|
|
599
|
+
id: taskContext.taskId,
|
|
600
|
+
attempt: taskContext.attempt,
|
|
601
|
+
token: taskContext.taskToken,
|
|
602
|
+
type: taskContext.taskType,
|
|
603
|
+
executionTarget: taskContext.target,
|
|
604
|
+
metadata,
|
|
605
|
+
},
|
|
606
|
+
request: {
|
|
607
|
+
prompt: taskContext.creatorRequest,
|
|
608
|
+
},
|
|
609
|
+
worker: {
|
|
610
|
+
devPort: taskContext.devPort,
|
|
611
|
+
env: taskContext.env,
|
|
612
|
+
},
|
|
613
|
+
};
|
|
614
|
+
}
|
|
541
615
|
async function stageWorkerTaskContext(input) {
|
|
542
616
|
const playdropDir = node_path_1.default.join(input.workspaceDir, '.playdrop');
|
|
543
617
|
await (0, promises_1.mkdir)(playdropDir, { recursive: true });
|
|
@@ -546,18 +620,10 @@ async function stageWorkerTaskContext(input) {
|
|
|
546
620
|
devPort: input.devPort,
|
|
547
621
|
env: input.env,
|
|
548
622
|
};
|
|
549
|
-
await (0, promises_1.writeFile)(workerTaskContextPath(input.workspaceDir), JSON.stringify(taskFile, null, 2), 'utf8');
|
|
550
|
-
await (0, promises_1.writeFile)(node_path_1.default.join(input.workspaceDir, '.playdrop.json'), JSON.stringify({
|
|
551
|
-
ownerUsername: input.taskContext.creatorUsername,
|
|
552
|
-
env: input.env,
|
|
553
|
-
taskId: input.taskContext.taskId,
|
|
554
|
-
taskAttempt: input.taskContext.attempt,
|
|
555
|
-
devPort: input.devPort,
|
|
556
|
-
taskToken: input.taskContext.taskToken,
|
|
557
|
-
}, null, 2), 'utf8');
|
|
623
|
+
await (0, promises_1.writeFile)(workerTaskContextPath(input.workspaceDir), JSON.stringify(serializeWorkerTaskContext(taskFile), null, 2), 'utf8');
|
|
558
624
|
}
|
|
559
625
|
async function writeWorkerTaskContextFile(workspaceDir, taskContext) {
|
|
560
|
-
await (0, promises_1.writeFile)(workerTaskContextPath(workspaceDir), JSON.stringify(taskContext, null, 2), 'utf8');
|
|
626
|
+
await (0, promises_1.writeFile)(workerTaskContextPath(workspaceDir), JSON.stringify(serializeWorkerTaskContext(taskContext), null, 2), 'utf8');
|
|
561
627
|
}
|
|
562
628
|
function findWorkspaceTaskFile(startDir, basename) {
|
|
563
629
|
let current = node_path_1.default.resolve(startDir);
|
|
@@ -576,67 +642,57 @@ function findWorkspaceTaskFile(startDir, basename) {
|
|
|
576
642
|
function readTaskContextFile(startDir = node_process_1.default.cwd()) {
|
|
577
643
|
const file = findWorkspaceTaskFile(startDir, 'task.json');
|
|
578
644
|
const parsed = JSON.parse((0, node_fs_1.readFileSync)(file, 'utf8'));
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
const taskToken = typeof parsed.taskToken === 'string' ? parsed.taskToken.trim() : '';
|
|
582
|
-
const creatorUsername = typeof parsed.creatorUsername === 'string' ? parsed.creatorUsername.trim() : '';
|
|
583
|
-
const env = typeof parsed.env === 'string' ? parsed.env.trim() : '';
|
|
584
|
-
const creatorRequest = typeof parsed.creatorRequest === 'string' ? parsed.creatorRequest.trim() : '';
|
|
585
|
-
const outputVersion = typeof parsed.outputVersion === 'string' ? parsed.outputVersion.trim() : '';
|
|
586
|
-
const remixSourceRef = typeof parsed.remixSourceRef === 'string' && parsed.remixSourceRef.trim()
|
|
587
|
-
? parsed.remixSourceRef.trim()
|
|
645
|
+
const task = parsed.task && typeof parsed.task === 'object' && !Array.isArray(parsed.task)
|
|
646
|
+
? parsed.task
|
|
588
647
|
: null;
|
|
589
|
-
const
|
|
590
|
-
?
|
|
591
|
-
:
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
const
|
|
600
|
-
|
|
648
|
+
const request = parsed.request && typeof parsed.request === 'object' && !Array.isArray(parsed.request)
|
|
649
|
+
? parsed.request
|
|
650
|
+
: null;
|
|
651
|
+
const worker = parsed.worker && typeof parsed.worker === 'object' && !Array.isArray(parsed.worker)
|
|
652
|
+
? parsed.worker
|
|
653
|
+
: null;
|
|
654
|
+
const taskId = Number(task?.id);
|
|
655
|
+
const attempt = Number(task?.attempt);
|
|
656
|
+
const taskToken = typeof task?.token === 'string' ? task.token.trim() : '';
|
|
657
|
+
const taskType = typeof task?.type === 'string' ? task.type.trim() : '';
|
|
658
|
+
const metadata = task?.metadata && typeof task.metadata === 'object' && !Array.isArray(task.metadata)
|
|
659
|
+
? task.metadata
|
|
660
|
+
: null;
|
|
661
|
+
const prompt = typeof request?.prompt === 'string' ? request.prompt : '';
|
|
662
|
+
const env = typeof worker?.env === 'string' ? worker.env.trim() : '';
|
|
663
|
+
const devPort = typeof worker?.devPort === 'number'
|
|
664
|
+
? worker.devPort
|
|
665
|
+
: Number.parseInt(String(worker?.devPort ?? ''), 10);
|
|
666
|
+
if (parsed.protocolVersion !== 2
|
|
667
|
+
|| !Number.isInteger(taskId) || taskId <= 0
|
|
601
668
|
|| !Number.isInteger(attempt) || attempt <= 0
|
|
602
669
|
|| !taskToken
|
|
603
|
-
||
|
|
604
|
-
||
|
|
605
|
-
||
|
|
606
|
-
|| !
|
|
670
|
+
|| !taskType
|
|
671
|
+
|| !metadata
|
|
672
|
+
|| (task?.executionTarget !== 'FIRST_PARTY' && task?.executionTarget !== 'PERSONAL')
|
|
673
|
+
|| !prompt.trim()
|
|
607
674
|
|| !env
|
|
608
|
-
|| !outputVersion
|
|
609
|
-
|| (parsed.kind === 'REMIX_GAME' && !remixSourceRef)
|
|
610
|
-
|| (parsed.kind === 'GAME_REVIEW' && reviewAppVersionId === null)
|
|
611
675
|
|| !Number.isInteger(devPort) || devPort <= 0 || devPort > 65535) {
|
|
612
676
|
throw new Error(`task_context_invalid:${file}`);
|
|
613
677
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
}
|
|
631
|
-
function normalizeAssignmentRequest(value) {
|
|
632
|
-
const request = value && typeof value === 'object' && !Array.isArray(value)
|
|
633
|
-
? value
|
|
634
|
-
: null;
|
|
635
|
-
const prompt = typeof request?.prompt === 'string' ? request.prompt.trim() : '';
|
|
636
|
-
if (!prompt) {
|
|
637
|
-
throw new Error('agent_task_assignment_request_prompt_missing');
|
|
678
|
+
try {
|
|
679
|
+
return {
|
|
680
|
+
...buildWorkerTaskContext({
|
|
681
|
+
taskId,
|
|
682
|
+
attempt,
|
|
683
|
+
taskToken,
|
|
684
|
+
taskType,
|
|
685
|
+
metadata,
|
|
686
|
+
prompt,
|
|
687
|
+
target: task.executionTarget,
|
|
688
|
+
}),
|
|
689
|
+
devPort,
|
|
690
|
+
env,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
throw new Error(`task_context_invalid:${file}`);
|
|
638
695
|
}
|
|
639
|
-
return prompt;
|
|
640
696
|
}
|
|
641
697
|
function hasAgentTaskUploadedArtifact(task) {
|
|
642
698
|
return ((Number.isInteger(task.appId) && Number(task.appId) > 0)
|
|
@@ -829,41 +885,6 @@ function normalizeAssignmentWorkspaceRelativePath(value) {
|
|
|
829
885
|
}
|
|
830
886
|
return normalizedPath;
|
|
831
887
|
}
|
|
832
|
-
function normalizeAssignmentWorkspaceFile(value) {
|
|
833
|
-
const file = value && typeof value === 'object' && !Array.isArray(value)
|
|
834
|
-
? value
|
|
835
|
-
: null;
|
|
836
|
-
if (!file) {
|
|
837
|
-
throw new Error('agent_task_assignment_workspace_file_invalid');
|
|
838
|
-
}
|
|
839
|
-
const relativePath = normalizeAssignmentWorkspaceRelativePath(file.relativePath);
|
|
840
|
-
const contentBase64 = typeof file.contentBase64 === 'string' ? file.contentBase64.trim() : '';
|
|
841
|
-
if (!contentBase64 || contentBase64.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(contentBase64)) {
|
|
842
|
-
throw new Error('agent_task_assignment_workspace_file_content_invalid');
|
|
843
|
-
}
|
|
844
|
-
const contentType = typeof file.contentType === 'string' && file.contentType.trim()
|
|
845
|
-
? file.contentType.trim()
|
|
846
|
-
: undefined;
|
|
847
|
-
return {
|
|
848
|
-
relativePath,
|
|
849
|
-
contentBase64,
|
|
850
|
-
...(contentType ? { contentType } : {}),
|
|
851
|
-
};
|
|
852
|
-
}
|
|
853
|
-
function normalizeAssignmentWorkspaceFiles(value) {
|
|
854
|
-
if (!Array.isArray(value)) {
|
|
855
|
-
return [];
|
|
856
|
-
}
|
|
857
|
-
const seen = new Set();
|
|
858
|
-
return value.map((entry) => {
|
|
859
|
-
const normalized = normalizeAssignmentWorkspaceFile(entry);
|
|
860
|
-
if (seen.has(normalized.relativePath)) {
|
|
861
|
-
throw new Error('agent_task_assignment_workspace_file_duplicate');
|
|
862
|
-
}
|
|
863
|
-
seen.add(normalized.relativePath);
|
|
864
|
-
return normalized;
|
|
865
|
-
});
|
|
866
|
-
}
|
|
867
888
|
function resolveWorkspaceFileDestination(workspaceDir, relativePath) {
|
|
868
889
|
const root = node_path_1.default.resolve(workspaceDir);
|
|
869
890
|
const destination = node_path_1.default.resolve(root, ...relativePath.split('/'));
|
|
@@ -873,12 +894,31 @@ function resolveWorkspaceFileDestination(workspaceDir, relativePath) {
|
|
|
873
894
|
}
|
|
874
895
|
return destination;
|
|
875
896
|
}
|
|
876
|
-
async function
|
|
877
|
-
const
|
|
878
|
-
|
|
879
|
-
|
|
897
|
+
async function stageAssignmentWorkspaceV2(workspaceDir, workspace) {
|
|
898
|
+
for (const file of workspace.files) {
|
|
899
|
+
const normalized = normalizeAssignmentContent({
|
|
900
|
+
path: file.path,
|
|
901
|
+
contentBase64: file.contentBase64,
|
|
902
|
+
sha256: file.sha256,
|
|
903
|
+
sizeBytes: file.sizeBytes,
|
|
904
|
+
code: 'agent_task_assignment_workspace_file',
|
|
905
|
+
});
|
|
906
|
+
const destination = resolveWorkspaceFileDestination(workspaceDir, normalized.path);
|
|
880
907
|
await (0, promises_1.mkdir)(node_path_1.default.dirname(destination), { recursive: true });
|
|
881
|
-
await (0, promises_1.writeFile)(destination,
|
|
908
|
+
await (0, promises_1.writeFile)(destination, normalized.buffer);
|
|
909
|
+
}
|
|
910
|
+
for (const archive of workspace.archives) {
|
|
911
|
+
const normalized = normalizeAssignmentContent({
|
|
912
|
+
path: archive.path,
|
|
913
|
+
contentBase64: archive.contentBase64,
|
|
914
|
+
sha256: archive.sha256,
|
|
915
|
+
sizeBytes: archive.sizeBytes,
|
|
916
|
+
code: 'agent_task_assignment_workspace_archive',
|
|
917
|
+
});
|
|
918
|
+
const destination = resolveWorkspaceFileDestination(workspaceDir, normalized.path);
|
|
919
|
+
await (0, promises_1.mkdir)(destination, { recursive: true });
|
|
920
|
+
extractZipArchive(new Uint8Array(normalized.buffer), destination);
|
|
921
|
+
await removeStaleWorkerControlFiles(destination);
|
|
882
922
|
}
|
|
883
923
|
}
|
|
884
924
|
function isFile(candidatePath) {
|
|
@@ -889,146 +929,6 @@ function isFile(candidatePath) {
|
|
|
889
929
|
return false;
|
|
890
930
|
}
|
|
891
931
|
}
|
|
892
|
-
function normalizePlaydropPluginManifestPath(input) {
|
|
893
|
-
if (typeof input.value !== 'string' || input.value.trim().length <= 0) {
|
|
894
|
-
throw new Error(`playdrop_plugin_staging_manifest_incomplete:${input.kind}: file paths must be non-empty strings in ${input.manifestPath}`);
|
|
895
|
-
}
|
|
896
|
-
if (input.value.includes('\0') || input.value.includes('\\')) {
|
|
897
|
-
throw new Error(`playdrop_plugin_staging_manifest_invalid_path:${input.kind}:${input.value}`);
|
|
898
|
-
}
|
|
899
|
-
if (node_path_1.default.isAbsolute(input.value) || /^[A-Za-z]:[\\/]/.test(input.value)) {
|
|
900
|
-
throw new Error(`playdrop_plugin_staging_manifest_invalid_path:${input.kind}:${input.value}`);
|
|
901
|
-
}
|
|
902
|
-
const normalized = node_path_1.default.posix.normalize(input.value);
|
|
903
|
-
if (normalized === '.' || normalized === '..' || normalized.startsWith('../')) {
|
|
904
|
-
throw new Error(`playdrop_plugin_staging_manifest_invalid_path:${input.kind}:${input.value}`);
|
|
905
|
-
}
|
|
906
|
-
return normalized;
|
|
907
|
-
}
|
|
908
|
-
function readPlaydropPluginStagingManifest(pluginRoot) {
|
|
909
|
-
const resolvedRoot = node_path_1.default.resolve(pluginRoot);
|
|
910
|
-
const manifestPath = node_path_1.default.join(resolvedRoot, PLAYDROP_PLUGIN_STAGING_MANIFEST_PATH);
|
|
911
|
-
if (!isFile(manifestPath)) {
|
|
912
|
-
throw new Error(`playdrop_plugin_staging_manifest_missing: expected ${PLAYDROP_PLUGIN_STAGING_MANIFEST_PATH} under ${resolvedRoot}`);
|
|
913
|
-
}
|
|
914
|
-
let parsed;
|
|
915
|
-
try {
|
|
916
|
-
parsed = JSON.parse((0, node_fs_1.readFileSync)(manifestPath, 'utf8'));
|
|
917
|
-
}
|
|
918
|
-
catch (error) {
|
|
919
|
-
throw new Error(`playdrop_plugin_staging_manifest_invalid_json:${manifestPath}:${error instanceof Error ? error.message : String(error)}`);
|
|
920
|
-
}
|
|
921
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
922
|
-
throw new Error(`playdrop_plugin_staging_manifest_invalid:${manifestPath}`);
|
|
923
|
-
}
|
|
924
|
-
const rawManifest = parsed;
|
|
925
|
-
if (rawManifest.version !== 1) {
|
|
926
|
-
throw new Error(`playdrop_plugin_staging_manifest_unsupported_version:${manifestPath}`);
|
|
927
|
-
}
|
|
928
|
-
if (!rawManifest.taskFiles || typeof rawManifest.taskFiles !== 'object' || Array.isArray(rawManifest.taskFiles)) {
|
|
929
|
-
throw new Error(`playdrop_plugin_staging_manifest_incomplete: taskFiles missing in ${manifestPath}`);
|
|
930
|
-
}
|
|
931
|
-
const taskFiles = {};
|
|
932
|
-
const rawTaskFiles = rawManifest.taskFiles;
|
|
933
|
-
for (const kind of PLAYDROP_PLUGIN_MANIFEST_REQUIRED_STARTUP_KINDS) {
|
|
934
|
-
const rawFiles = rawTaskFiles[kind];
|
|
935
|
-
if (!Array.isArray(rawFiles) || rawFiles.length <= 0) {
|
|
936
|
-
throw new Error(`playdrop_plugin_staging_manifest_incomplete:${kind}: non-empty taskFiles entry required in ${manifestPath}`);
|
|
937
|
-
}
|
|
938
|
-
const seen = new Set();
|
|
939
|
-
const files = rawFiles.map((value) => normalizePlaydropPluginManifestPath({ manifestPath, kind, value }));
|
|
940
|
-
for (const relativePath of files) {
|
|
941
|
-
if (seen.has(relativePath)) {
|
|
942
|
-
throw new Error(`playdrop_plugin_staging_manifest_duplicate_file:${kind}:${relativePath}`);
|
|
943
|
-
}
|
|
944
|
-
seen.add(relativePath);
|
|
945
|
-
const source = node_path_1.default.resolve(resolvedRoot, ...relativePath.split('/'));
|
|
946
|
-
const relativeToRoot = node_path_1.default.relative(resolvedRoot, source);
|
|
947
|
-
if (relativeToRoot.startsWith('..') || node_path_1.default.isAbsolute(relativeToRoot)) {
|
|
948
|
-
throw new Error(`playdrop_plugin_staging_manifest_invalid_path:${kind}:${relativePath}`);
|
|
949
|
-
}
|
|
950
|
-
if (!isFile(source)) {
|
|
951
|
-
throw new Error(`playdrop_plugin_staging_manifest_file_missing:${kind}:${relativePath}`);
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
taskFiles[kind] = files;
|
|
955
|
-
}
|
|
956
|
-
for (const kind of WORKER_SUPPORTED_KINDS) {
|
|
957
|
-
if (taskFiles[kind]) {
|
|
958
|
-
continue;
|
|
959
|
-
}
|
|
960
|
-
const rawFiles = rawTaskFiles[kind];
|
|
961
|
-
if (rawFiles === undefined) {
|
|
962
|
-
continue;
|
|
963
|
-
}
|
|
964
|
-
if (!Array.isArray(rawFiles) || rawFiles.length <= 0) {
|
|
965
|
-
throw new Error(`playdrop_plugin_staging_manifest_incomplete:${kind}: non-empty taskFiles entry required in ${manifestPath}`);
|
|
966
|
-
}
|
|
967
|
-
const seen = new Set();
|
|
968
|
-
const files = rawFiles.map((value) => normalizePlaydropPluginManifestPath({ manifestPath, kind, value }));
|
|
969
|
-
for (const relativePath of files) {
|
|
970
|
-
if (seen.has(relativePath)) {
|
|
971
|
-
throw new Error(`playdrop_plugin_staging_manifest_duplicate_file:${kind}:${relativePath}`);
|
|
972
|
-
}
|
|
973
|
-
seen.add(relativePath);
|
|
974
|
-
const source = node_path_1.default.resolve(resolvedRoot, ...relativePath.split('/'));
|
|
975
|
-
const relativeToRoot = node_path_1.default.relative(resolvedRoot, source);
|
|
976
|
-
if (relativeToRoot.startsWith('..') || node_path_1.default.isAbsolute(relativeToRoot)) {
|
|
977
|
-
throw new Error(`playdrop_plugin_staging_manifest_invalid_path:${kind}:${relativePath}`);
|
|
978
|
-
}
|
|
979
|
-
if (!isFile(source)) {
|
|
980
|
-
throw new Error(`playdrop_plugin_staging_manifest_file_missing:${kind}:${relativePath}`);
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
taskFiles[kind] = files;
|
|
984
|
-
}
|
|
985
|
-
return {
|
|
986
|
-
version: 1,
|
|
987
|
-
taskFiles: taskFiles,
|
|
988
|
-
};
|
|
989
|
-
}
|
|
990
|
-
async function stagePlaydropPluginReferences(input) {
|
|
991
|
-
const pluginRoot = node_path_1.default.resolve(input.pluginRoot);
|
|
992
|
-
if (!WORKER_SUPPORTED_KINDS.includes(input.kind)) {
|
|
993
|
-
throw new Error(`unsupported_agent_task_kind:${input.kind}`);
|
|
994
|
-
}
|
|
995
|
-
const manifest = readPlaydropPluginStagingManifest(pluginRoot);
|
|
996
|
-
const referenceFiles = manifest.taskFiles[input.kind];
|
|
997
|
-
if (!Array.isArray(referenceFiles) || referenceFiles.length <= 0) {
|
|
998
|
-
throw new Error(`playdrop_plugin_staging_manifest_incomplete:${input.kind}: non-empty taskFiles entry required`);
|
|
999
|
-
}
|
|
1000
|
-
const staged = [];
|
|
1001
|
-
for (const relativePath of referenceFiles) {
|
|
1002
|
-
const destinationRelativePath = node_path_1.default.join(STAGED_PLAYDROP_PLUGIN_ROOT, relativePath);
|
|
1003
|
-
const destination = resolveWorkspaceFileDestination(input.workspaceDir, destinationRelativePath);
|
|
1004
|
-
await (0, promises_1.mkdir)(node_path_1.default.dirname(destination), { recursive: true });
|
|
1005
|
-
await (0, promises_1.copyFile)(node_path_1.default.join(pluginRoot, relativePath), destination);
|
|
1006
|
-
staged.push(destinationRelativePath.replace(/\\/g, '/'));
|
|
1007
|
-
}
|
|
1008
|
-
return staged;
|
|
1009
|
-
}
|
|
1010
|
-
async function stageCodexTaskSkills(input) {
|
|
1011
|
-
const skillRoot = node_path_1.default.join(input.workspaceDir, STAGED_CODEX_SKILL_ROOT);
|
|
1012
|
-
await (0, promises_1.rm)(skillRoot, { recursive: true, force: true });
|
|
1013
|
-
const stagedSkills = [];
|
|
1014
|
-
for (const stagedPluginPath of input.stagedPluginPaths) {
|
|
1015
|
-
const normalized = stagedPluginPath.replace(/\\/g, '/');
|
|
1016
|
-
const match = /^\.playdrop\/plugin\/skills\/([^/]+)\/SKILL\.md$/.exec(normalized);
|
|
1017
|
-
if (!match?.[1]) {
|
|
1018
|
-
continue;
|
|
1019
|
-
}
|
|
1020
|
-
const source = resolveWorkspaceFileDestination(input.workspaceDir, normalized);
|
|
1021
|
-
const destinationRelativePath = `${STAGED_CODEX_SKILL_ROOT}/${match[1]}/SKILL.md`;
|
|
1022
|
-
const destination = resolveWorkspaceFileDestination(input.workspaceDir, destinationRelativePath);
|
|
1023
|
-
await (0, promises_1.mkdir)(node_path_1.default.dirname(destination), { recursive: true });
|
|
1024
|
-
await (0, promises_1.copyFile)(source, destination);
|
|
1025
|
-
stagedSkills.push(destinationRelativePath);
|
|
1026
|
-
}
|
|
1027
|
-
if (stagedSkills.length <= 0) {
|
|
1028
|
-
throw new Error('agent_task_codex_skills_missing');
|
|
1029
|
-
}
|
|
1030
|
-
return stagedSkills.sort();
|
|
1031
|
-
}
|
|
1032
932
|
function assertAssignmentPluginBundleExtractionBounds(buffer) {
|
|
1033
933
|
if (buffer.length < ZIP_END_OF_CENTRAL_DIRECTORY_MIN_BYTES) {
|
|
1034
934
|
throw new Error('agent_task_plugin_bundle_invalid_zip');
|
|
@@ -1094,15 +994,102 @@ function assertAssignmentPluginBundleExtractionBounds(buffer) {
|
|
|
1094
994
|
throw new Error('agent_task_plugin_bundle_invalid_zip');
|
|
1095
995
|
}
|
|
1096
996
|
}
|
|
997
|
+
function normalizeAgentBundlePath(root, value, code) {
|
|
998
|
+
const relativePath = typeof value === 'string' ? value.trim().replace(/\\/g, '/') : '';
|
|
999
|
+
if (!relativePath
|
|
1000
|
+
|| relativePath.includes('\0')
|
|
1001
|
+
|| node_path_1.default.posix.isAbsolute(relativePath)
|
|
1002
|
+
|| relativePath === '..'
|
|
1003
|
+
|| relativePath.startsWith('../')) {
|
|
1004
|
+
throw new Error(code);
|
|
1005
|
+
}
|
|
1006
|
+
const resolved = node_path_1.default.resolve(root, ...relativePath.split('/'));
|
|
1007
|
+
if (!isPathInside(root, resolved)) {
|
|
1008
|
+
throw new Error(code);
|
|
1009
|
+
}
|
|
1010
|
+
return resolved;
|
|
1011
|
+
}
|
|
1012
|
+
function readAgentBundleManifest(extractedRoot) {
|
|
1013
|
+
const manifestPath = node_path_1.default.join(extractedRoot, AGENT_BUNDLE_MANIFEST_PATH);
|
|
1014
|
+
if (!isFile(manifestPath)) {
|
|
1015
|
+
throw new Error('agent_bundle_manifest_missing');
|
|
1016
|
+
}
|
|
1017
|
+
let parsed;
|
|
1018
|
+
try {
|
|
1019
|
+
parsed = JSON.parse((0, node_fs_1.readFileSync)(manifestPath, 'utf8'));
|
|
1020
|
+
}
|
|
1021
|
+
catch {
|
|
1022
|
+
throw new Error('agent_bundle_manifest_invalid_json');
|
|
1023
|
+
}
|
|
1024
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1025
|
+
throw new Error('agent_bundle_manifest_invalid');
|
|
1026
|
+
}
|
|
1027
|
+
const manifest = parsed;
|
|
1028
|
+
if (manifest.version !== 1
|
|
1029
|
+
|| !manifest.plugin
|
|
1030
|
+
|| typeof manifest.plugin.root !== 'string'
|
|
1031
|
+
|| !manifest.workspace
|
|
1032
|
+
|| !Array.isArray(manifest.workspace.files)) {
|
|
1033
|
+
throw new Error('agent_bundle_manifest_invalid');
|
|
1034
|
+
}
|
|
1035
|
+
const pluginRoot = normalizeAgentBundlePath(extractedRoot, manifest.plugin.root, 'agent_bundle_plugin_root_invalid');
|
|
1036
|
+
if (!isFile(node_path_1.default.join(pluginRoot, '.codex-plugin', 'plugin.json'))) {
|
|
1037
|
+
throw new Error('agent_bundle_codex_plugin_manifest_missing');
|
|
1038
|
+
}
|
|
1039
|
+
if (!isFile(node_path_1.default.join(pluginRoot, '.claude-plugin', 'plugin.json'))) {
|
|
1040
|
+
throw new Error('agent_bundle_claude_plugin_manifest_missing');
|
|
1041
|
+
}
|
|
1042
|
+
const destinations = new Set();
|
|
1043
|
+
for (const entry of manifest.workspace.files) {
|
|
1044
|
+
const source = normalizeAgentBundlePath(extractedRoot, entry?.source, 'agent_bundle_workspace_source_invalid');
|
|
1045
|
+
if (!isFile(source)) {
|
|
1046
|
+
throw new Error(`agent_bundle_workspace_source_missing:${entry?.source ?? ''}`);
|
|
1047
|
+
}
|
|
1048
|
+
const destination = typeof entry?.destination === 'string'
|
|
1049
|
+
? entry.destination.trim().replace(/\\/g, '/')
|
|
1050
|
+
: '';
|
|
1051
|
+
if (!destination
|
|
1052
|
+
|| node_path_1.default.posix.isAbsolute(destination)
|
|
1053
|
+
|| destination === '..'
|
|
1054
|
+
|| destination.startsWith('../')) {
|
|
1055
|
+
throw new Error('agent_bundle_workspace_destination_invalid');
|
|
1056
|
+
}
|
|
1057
|
+
destinations.add(destination);
|
|
1058
|
+
}
|
|
1059
|
+
if (!destinations.has('AGENTS.md')) {
|
|
1060
|
+
throw new Error('agent_bundle_agents_file_missing');
|
|
1061
|
+
}
|
|
1062
|
+
if (!destinations.has('CLAUDE.md')) {
|
|
1063
|
+
throw new Error('agent_bundle_claude_file_missing');
|
|
1064
|
+
}
|
|
1065
|
+
return { manifest, pluginRoot };
|
|
1066
|
+
}
|
|
1067
|
+
async function stageAgentBundleWorkspaceFiles(extractedRoot, workspaceDir) {
|
|
1068
|
+
const resolved = readAgentBundleManifest(extractedRoot);
|
|
1069
|
+
for (const entry of resolved.manifest.workspace.files) {
|
|
1070
|
+
const source = normalizeAgentBundlePath(extractedRoot, entry.source, 'agent_bundle_workspace_source_invalid');
|
|
1071
|
+
const destination = resolveWorkspaceFileDestination(workspaceDir, entry.destination);
|
|
1072
|
+
await (0, promises_1.mkdir)(node_path_1.default.dirname(destination), { recursive: true });
|
|
1073
|
+
await (0, promises_1.copyFile)(source, destination);
|
|
1074
|
+
}
|
|
1075
|
+
return resolved.pluginRoot;
|
|
1076
|
+
}
|
|
1097
1077
|
async function stageAssignmentPluginBundle(input) {
|
|
1098
1078
|
const sha256 = typeof input.pluginBundle.sha256 === 'string' ? input.pluginBundle.sha256.trim() : '';
|
|
1079
|
+
const fileName = typeof input.pluginBundle.fileName === 'string' ? input.pluginBundle.fileName.trim() : '';
|
|
1099
1080
|
const contentBase64 = typeof input.pluginBundle.contentBase64 === 'string' ? input.pluginBundle.contentBase64.trim() : '';
|
|
1100
|
-
|
|
1081
|
+
const sizeBytes = Number(input.pluginBundle.sizeBytes);
|
|
1082
|
+
if (!/^[a-f0-9]{64}$/.test(sha256)
|
|
1083
|
+
|| !fileName
|
|
1084
|
+
|| node_path_1.default.basename(fileName) !== fileName
|
|
1085
|
+
|| !Number.isInteger(sizeBytes)
|
|
1086
|
+
|| sizeBytes <= 0
|
|
1087
|
+
|| !contentBase64) {
|
|
1101
1088
|
throw new Error('agent_task_plugin_bundle_invalid');
|
|
1102
1089
|
}
|
|
1103
1090
|
const buffer = Buffer.from(contentBase64, 'base64');
|
|
1104
|
-
if (buffer.length
|
|
1105
|
-
throw new Error('
|
|
1091
|
+
if (buffer.length !== sizeBytes) {
|
|
1092
|
+
throw new Error('agent_task_plugin_bundle_size_mismatch');
|
|
1106
1093
|
}
|
|
1107
1094
|
const actualSha = (0, node_crypto_1.createHash)('sha256').update(buffer).digest('hex');
|
|
1108
1095
|
if (actualSha !== sha256) {
|
|
@@ -1140,8 +1127,7 @@ async function stageAssignmentPluginBundle(input) {
|
|
|
1140
1127
|
await (0, promises_1.mkdir)(node_path_1.default.dirname(destination), { recursive: true });
|
|
1141
1128
|
await (0, promises_1.writeFile)(destination, Buffer.from(content));
|
|
1142
1129
|
}
|
|
1143
|
-
|
|
1144
|
-
return pluginRoot;
|
|
1130
|
+
return await stageAgentBundleWorkspaceFiles(pluginRoot, input.workspaceDir);
|
|
1145
1131
|
}
|
|
1146
1132
|
function resolveDevPluginWorkingTree(input) {
|
|
1147
1133
|
const workingTree = input.workingTree?.trim();
|
|
@@ -1157,7 +1143,7 @@ function resolveDevPluginWorkingTree(input) {
|
|
|
1157
1143
|
throw new Error(`worker_dev_plugin_working_tree_requires_dev_environment:${input.env}`);
|
|
1158
1144
|
}
|
|
1159
1145
|
const resolved = node_path_1.default.resolve(workingTree);
|
|
1160
|
-
|
|
1146
|
+
readAgentBundleManifest(resolved);
|
|
1161
1147
|
return resolved;
|
|
1162
1148
|
}
|
|
1163
1149
|
// A PlayDrop project root is the topmost directory holding a catalogue.json,
|
|
@@ -1570,6 +1556,7 @@ function buildWorkerCapabilities(input) {
|
|
|
1570
1556
|
}
|
|
1571
1557
|
function buildWorkerClaimBody(input) {
|
|
1572
1558
|
return {
|
|
1559
|
+
protocolVersion: 2,
|
|
1573
1560
|
workerKey: input.workerKey,
|
|
1574
1561
|
capabilities: {
|
|
1575
1562
|
...input.capabilities,
|
|
@@ -1577,89 +1564,43 @@ function buildWorkerClaimBody(input) {
|
|
|
1577
1564
|
},
|
|
1578
1565
|
};
|
|
1579
1566
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
throw new Error('agent_task_assignment_missing_base_source');
|
|
1583
|
-
}
|
|
1584
|
-
if (typeof value.appName !== 'string' || !value.appName.trim()) {
|
|
1585
|
-
throw new Error('agent_task_base_app_name_missing');
|
|
1586
|
-
}
|
|
1587
|
-
if (typeof value.version !== 'string' || !value.version.trim()) {
|
|
1588
|
-
throw new Error('agent_task_base_app_version_missing');
|
|
1589
|
-
}
|
|
1590
|
-
if (typeof value.appId !== 'number' || !Number.isInteger(value.appId) || value.appId <= 0) {
|
|
1591
|
-
throw new Error('agent_task_base_app_id_missing');
|
|
1592
|
-
}
|
|
1593
|
-
if (typeof value.appVersionId !== 'number' || !Number.isInteger(value.appVersionId) || value.appVersionId <= 0) {
|
|
1594
|
-
throw new Error('agent_task_base_app_version_id_missing');
|
|
1595
|
-
}
|
|
1596
|
-
return {
|
|
1597
|
-
appName: value.appName.trim(),
|
|
1598
|
-
version: value.version.trim(),
|
|
1599
|
-
appId: value.appId,
|
|
1600
|
-
appVersionId: value.appVersionId,
|
|
1601
|
-
};
|
|
1602
|
-
}
|
|
1603
|
-
// Extracts a zip archive into targetDir using fflate (already a CLI
|
|
1604
|
-
// dependency; see src/commands/create.ts). Entries that resolve outside the
|
|
1605
|
-
// target directory throw instead of being skipped: a base source archive with
|
|
1606
|
-
// traversal entries is corrupt and must fail the task loudly.
|
|
1567
|
+
// Extracts a server-provided workspace ZIP using fflate. Entries that resolve
|
|
1568
|
+
// outside the target directory fail the task immediately.
|
|
1607
1569
|
function extractZipArchive(zipBuffer, targetDir) {
|
|
1608
1570
|
const files = (0, fflate_1.unzipSync)(zipBuffer);
|
|
1609
1571
|
const names = Object.keys(files);
|
|
1610
1572
|
if (names.length === 0) {
|
|
1611
|
-
throw new Error('
|
|
1573
|
+
throw new Error('agent_task_workspace_archive_empty');
|
|
1574
|
+
}
|
|
1575
|
+
if (names.length > ASSIGNMENT_PLUGIN_BUNDLE_MAX_FILES) {
|
|
1576
|
+
throw new Error(`agent_task_workspace_archive_too_many_entries:${names.length}>${ASSIGNMENT_PLUGIN_BUNDLE_MAX_FILES}`);
|
|
1612
1577
|
}
|
|
1613
1578
|
const resolvedTarget = node_path_1.default.resolve(targetDir);
|
|
1614
1579
|
const written = [];
|
|
1580
|
+
let uncompressedBytes = 0;
|
|
1615
1581
|
for (const originalName of names) {
|
|
1616
1582
|
const name = originalName.replace(/\\/g, '/');
|
|
1583
|
+
if (!name || name.startsWith('/') || name.split('/').includes('..')) {
|
|
1584
|
+
throw new Error(`agent_task_workspace_archive_unsafe_entry:${originalName}`);
|
|
1585
|
+
}
|
|
1617
1586
|
const destination = node_path_1.default.resolve(resolvedTarget, name);
|
|
1618
1587
|
if (destination !== resolvedTarget && !destination.startsWith(resolvedTarget + node_path_1.default.sep)) {
|
|
1619
|
-
throw new Error(`
|
|
1588
|
+
throw new Error(`agent_task_workspace_archive_unsafe_entry:${originalName}`);
|
|
1620
1589
|
}
|
|
1621
1590
|
if (name.endsWith('/')) {
|
|
1622
1591
|
(0, node_fs_1.mkdirSync)(destination, { recursive: true });
|
|
1623
1592
|
continue;
|
|
1624
1593
|
}
|
|
1594
|
+
uncompressedBytes += files[originalName].byteLength;
|
|
1595
|
+
if (uncompressedBytes > ASSIGNMENT_WORKSPACE_ARCHIVE_MAX_UNCOMPRESSED_BYTES) {
|
|
1596
|
+
throw new Error(`agent_task_workspace_archive_too_large:${uncompressedBytes}>${ASSIGNMENT_WORKSPACE_ARCHIVE_MAX_UNCOMPRESSED_BYTES}`);
|
|
1597
|
+
}
|
|
1625
1598
|
(0, node_fs_1.mkdirSync)(node_path_1.default.dirname(destination), { recursive: true });
|
|
1626
1599
|
(0, node_fs_1.writeFileSync)(destination, Buffer.from(files[originalName]));
|
|
1627
1600
|
written.push(name);
|
|
1628
1601
|
}
|
|
1629
1602
|
return written;
|
|
1630
1603
|
}
|
|
1631
|
-
// Reads the version string of the named app from the catalogue.json at the
|
|
1632
|
-
// project root. Used for the staged base version and the version-bump guard.
|
|
1633
|
-
function readWorkerProjectVersion(projectDir, appName) {
|
|
1634
|
-
const cataloguePath = node_path_1.default.join(projectDir, 'catalogue.json');
|
|
1635
|
-
let raw;
|
|
1636
|
-
try {
|
|
1637
|
-
raw = (0, node_fs_1.readFileSync)(cataloguePath, 'utf8');
|
|
1638
|
-
}
|
|
1639
|
-
catch (error) {
|
|
1640
|
-
throw new Error(`agent_task_catalogue_unreadable: could not read ${cataloguePath} (${error instanceof Error ? error.message : String(error)}).`);
|
|
1641
|
-
}
|
|
1642
|
-
let parsed;
|
|
1643
|
-
try {
|
|
1644
|
-
parsed = JSON.parse(raw);
|
|
1645
|
-
}
|
|
1646
|
-
catch {
|
|
1647
|
-
throw new Error(`agent_task_catalogue_invalid_json: ${cataloguePath} is not valid JSON.`);
|
|
1648
|
-
}
|
|
1649
|
-
const apps = parsed?.apps;
|
|
1650
|
-
if (!Array.isArray(apps)) {
|
|
1651
|
-
throw new Error(`agent_task_catalogue_apps_missing: ${cataloguePath} has no "apps" array.`);
|
|
1652
|
-
}
|
|
1653
|
-
const entry = apps.find((candidate) => Boolean(candidate) && typeof candidate === 'object' && candidate.name === appName);
|
|
1654
|
-
if (!entry) {
|
|
1655
|
-
throw new Error(`agent_task_catalogue_app_missing: no app named "${appName}" in ${cataloguePath}.`);
|
|
1656
|
-
}
|
|
1657
|
-
const version = typeof entry.version === 'string' ? entry.version.trim() : '';
|
|
1658
|
-
if (!version) {
|
|
1659
|
-
throw new Error(`agent_task_catalogue_version_missing: app "${appName}" in ${cataloguePath} has no version string.`);
|
|
1660
|
-
}
|
|
1661
|
-
return version;
|
|
1662
|
-
}
|
|
1663
1604
|
function normalizePreviewString(value) {
|
|
1664
1605
|
const normalized = typeof value === 'string' ? value.trim() : '';
|
|
1665
1606
|
return normalized || undefined;
|
|
@@ -1842,30 +1783,9 @@ async function drainWorkerEventQueue(input) {
|
|
|
1842
1783
|
(0, node_fs_1.rmSync)(filePath, { force: true });
|
|
1843
1784
|
}
|
|
1844
1785
|
}
|
|
1845
|
-
|
|
1846
|
-
// version anyway, but failing here gives the task a precise error code.
|
|
1847
|
-
function assertWorkerProjectVersionBumped(input) {
|
|
1848
|
-
if (input.projectVersion === input.baseVersion) {
|
|
1849
|
-
throw new Error(`agent_task_version_not_bumped: app "${input.appName}" version is still "${input.baseVersion}"; the update must bump the catalogue.json version before publish.`);
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
async function removeStaleWorkerControlFilesFromBaseSource(stagingDir) {
|
|
1786
|
+
async function removeStaleWorkerControlFiles(stagingDir) {
|
|
1853
1787
|
await (0, promises_1.rm)(node_path_1.default.join(stagingDir, '.playdrop.json'), { force: true });
|
|
1854
1788
|
}
|
|
1855
|
-
// Downloads the base app source archive and unzips it into the workspace so
|
|
1856
|
-
// the project root (catalogue.json) lands inside the workspace before Codex
|
|
1857
|
-
// starts. Returns the staged project root and its catalogue version string.
|
|
1858
|
-
async function stageWorkerBaseAppSource(input) {
|
|
1859
|
-
const { blob } = await input.client.downloadAppSource(input.creatorUsername, input.baseApp.appName, input.baseApp.version);
|
|
1860
|
-
const buffer = Buffer.from(await blob.arrayBuffer());
|
|
1861
|
-
const stagingDir = node_path_1.default.join(input.workspaceDir, input.baseApp.appName);
|
|
1862
|
-
(0, node_fs_1.mkdirSync)(stagingDir, { recursive: true });
|
|
1863
|
-
extractZipArchive(new Uint8Array(buffer), stagingDir);
|
|
1864
|
-
await removeStaleWorkerControlFilesFromBaseSource(stagingDir);
|
|
1865
|
-
const stagedProjectDir = discoverWorkerProjectRoot(stagingDir);
|
|
1866
|
-
const stagedBaseVersion = readWorkerProjectVersion(stagedProjectDir, input.baseApp.appName);
|
|
1867
|
-
return { stagedProjectDir, stagedBaseVersion };
|
|
1868
|
-
}
|
|
1869
1789
|
function resolveCodexModel(model, reasoningEffortOverride) {
|
|
1870
1790
|
const normalized = model.trim();
|
|
1871
1791
|
if (!normalized) {
|
|
@@ -2011,6 +1931,21 @@ async function runCodex(input) {
|
|
|
2011
1931
|
workerUsername: input.workerUsername,
|
|
2012
1932
|
envName: input.envName,
|
|
2013
1933
|
});
|
|
1934
|
+
if (input.taskPluginRoot) {
|
|
1935
|
+
const pluginEnv = {
|
|
1936
|
+
...node_process_1.default.env,
|
|
1937
|
+
HOME: homeDir,
|
|
1938
|
+
CODEX_HOME: codexHomeDir,
|
|
1939
|
+
};
|
|
1940
|
+
await execFileAsync('codex', ['plugin', 'marketplace', 'add', input.taskPluginRoot, '--json'], {
|
|
1941
|
+
cwd: input.workspaceDir,
|
|
1942
|
+
env: pluginEnv,
|
|
1943
|
+
});
|
|
1944
|
+
await execFileAsync('codex', ['plugin', 'add', 'playdrop@playdrop', '--json'], {
|
|
1945
|
+
cwd: input.workspaceDir,
|
|
1946
|
+
env: pluginEnv,
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
2014
1949
|
let observedCodexThreadId = null;
|
|
2015
1950
|
let codexThreadProbe = '';
|
|
2016
1951
|
const result = await (0, runtime_1.runLoggedProcess)({
|
|
@@ -2021,11 +1956,13 @@ async function runCodex(input) {
|
|
|
2021
1956
|
reasoningEffort: input.codexModel.reasoningEffort,
|
|
2022
1957
|
networkAccess: (0, runtime_1.readEnvBoolean)('PLAYDROP_WORKER_AGENT_NETWORK_ACCESS', false),
|
|
2023
1958
|
sandboxMode: (0, runtime_1.readCodexSandboxMode)(),
|
|
1959
|
+
imagePaths: input.imagePaths,
|
|
2024
1960
|
}),
|
|
2025
1961
|
cwd: input.workspaceDir,
|
|
2026
1962
|
env: (0, runtime_1.buildWorkerChildEnv)({
|
|
2027
1963
|
binDir: input.binDir,
|
|
2028
1964
|
taskId: input.taskId,
|
|
1965
|
+
taskToken: input.taskToken,
|
|
2029
1966
|
attempt: input.attempt,
|
|
2030
1967
|
envName: input.envName,
|
|
2031
1968
|
eventDir: input.eventDir,
|
|
@@ -2106,6 +2043,7 @@ async function runClaude(input) {
|
|
|
2106
2043
|
env: (0, runtime_1.buildWorkerChildEnv)({
|
|
2107
2044
|
binDir: input.binDir,
|
|
2108
2045
|
taskId: input.taskId,
|
|
2046
|
+
taskToken: input.taskToken,
|
|
2109
2047
|
attempt: input.attempt,
|
|
2110
2048
|
envName: input.envName,
|
|
2111
2049
|
eventDir: input.eventDir,
|
|
@@ -2113,7 +2051,23 @@ async function runClaude(input) {
|
|
|
2113
2051
|
playdropConfigPath,
|
|
2114
2052
|
playwrightBrowsersPath: resolveWorkerPlaywrightBrowsersPath(),
|
|
2115
2053
|
}),
|
|
2116
|
-
stdin:
|
|
2054
|
+
stdin: `${JSON.stringify({
|
|
2055
|
+
type: 'user',
|
|
2056
|
+
message: {
|
|
2057
|
+
role: 'user',
|
|
2058
|
+
content: [
|
|
2059
|
+
{ type: 'text', text: input.prompt },
|
|
2060
|
+
...input.imageAttachments.map((attachment) => ({
|
|
2061
|
+
type: 'image',
|
|
2062
|
+
source: {
|
|
2063
|
+
type: 'base64',
|
|
2064
|
+
media_type: attachment.contentType,
|
|
2065
|
+
data: (0, node_fs_1.readFileSync)(attachment.path).toString('base64'),
|
|
2066
|
+
},
|
|
2067
|
+
})),
|
|
2068
|
+
],
|
|
2069
|
+
},
|
|
2070
|
+
})}\n`,
|
|
2117
2071
|
timeoutMs: (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_CLAUDE_TIMEOUT_MS', runtime_1.DEFAULT_CODEX_TIMEOUT_MS),
|
|
2118
2072
|
maxOutputChars: runtime_1.DEFAULT_CODEX_LOG_TAIL_CHARS,
|
|
2119
2073
|
onTranscriptChunks: input.onTranscriptChunks,
|
|
@@ -2163,6 +2117,13 @@ function buildAgentRunResult(result) {
|
|
|
2163
2117
|
tokensUsed: result.tokensUsed,
|
|
2164
2118
|
};
|
|
2165
2119
|
}
|
|
2120
|
+
function agentFailureRawError(result) {
|
|
2121
|
+
return [result.stderr, result.stdout, result.outputTail]
|
|
2122
|
+
.map((value) => value.trim())
|
|
2123
|
+
.filter(Boolean)
|
|
2124
|
+
.join('\n')
|
|
2125
|
+
|| 'agent_failure';
|
|
2126
|
+
}
|
|
2166
2127
|
function buildSupervisorFailureRunResult(message) {
|
|
2167
2128
|
return {
|
|
2168
2129
|
exitCode: null,
|
|
@@ -2183,22 +2144,33 @@ function buildSupervisorFailureRunResult(message) {
|
|
|
2183
2144
|
tokensUsed: null,
|
|
2184
2145
|
};
|
|
2185
2146
|
}
|
|
2186
|
-
function
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
if (normalized.startsWith(pluginPrefix)) {
|
|
2190
|
-
return normalized.slice(pluginPrefix.length);
|
|
2147
|
+
function listPluginSkillPaths(pluginRoot) {
|
|
2148
|
+
if (!pluginRoot) {
|
|
2149
|
+
return [];
|
|
2191
2150
|
}
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2151
|
+
const skillsRoot = node_path_1.default.join(pluginRoot, 'skills');
|
|
2152
|
+
if (!(0, node_fs_1.existsSync)(skillsRoot)) {
|
|
2153
|
+
return [];
|
|
2154
|
+
}
|
|
2155
|
+
const paths = [];
|
|
2156
|
+
const visit = (directory) => {
|
|
2157
|
+
for (const entry of (0, node_fs_1.readdirSync)(directory, { withFileTypes: true })) {
|
|
2158
|
+
const entryPath = node_path_1.default.join(directory, entry.name);
|
|
2159
|
+
if (entry.isDirectory()) {
|
|
2160
|
+
visit(entryPath);
|
|
2161
|
+
}
|
|
2162
|
+
else if (entry.isFile() && entry.name === 'SKILL.md') {
|
|
2163
|
+
paths.push(node_path_1.default.relative(pluginRoot, entryPath).replace(/\\/g, '/'));
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
};
|
|
2167
|
+
visit(skillsRoot);
|
|
2168
|
+
return paths.sort();
|
|
2196
2169
|
}
|
|
2197
2170
|
function collectObservedPlaydropSkillPaths(output, availablePaths) {
|
|
2198
2171
|
const observed = new Set();
|
|
2199
2172
|
for (const availablePath of availablePaths) {
|
|
2200
|
-
if (output.includes(availablePath)
|
|
2201
|
-
|| output.includes(`${STAGED_PLAYDROP_PLUGIN_ROOT}/${availablePath}`)) {
|
|
2173
|
+
if (output.includes(availablePath)) {
|
|
2202
2174
|
observed.add(availablePath);
|
|
2203
2175
|
}
|
|
2204
2176
|
}
|
|
@@ -2219,8 +2191,8 @@ async function recordAgentRunTelemetry(input) {
|
|
|
2219
2191
|
workerKey: input.workerKey,
|
|
2220
2192
|
leaseToken: input.leaseToken,
|
|
2221
2193
|
attempt: input.task.attempts,
|
|
2222
|
-
agent: input.assignment.agent,
|
|
2223
|
-
requestedModel: input.assignment.model,
|
|
2194
|
+
agent: input.assignment.agent.runtime,
|
|
2195
|
+
requestedModel: input.assignment.agent.model,
|
|
2224
2196
|
resolvedRuntimeModel: input.resolvedRuntimeModel,
|
|
2225
2197
|
reasoningEffort: input.reasoningEffort,
|
|
2226
2198
|
status: resolveTelemetryStatus(input.status),
|
|
@@ -2240,6 +2212,11 @@ async function recordAgentRunTelemetry(input) {
|
|
|
2240
2212
|
observedSkillPaths: [...observedSkillPaths].sort(),
|
|
2241
2213
|
startedAt: input.startedAt.toISOString(),
|
|
2242
2214
|
completedAt: input.completedAt.toISOString(),
|
|
2215
|
+
...(input.status === 'FAILED'
|
|
2216
|
+
? {
|
|
2217
|
+
terminalReason: (0, types_1.classifyAgentTaskTerminalReason)(agentFailureRawError(input.result), 'agent'),
|
|
2218
|
+
}
|
|
2219
|
+
: {}),
|
|
2243
2220
|
});
|
|
2244
2221
|
}
|
|
2245
2222
|
function providerNameForAgent(agent) {
|
|
@@ -2713,14 +2690,6 @@ function buildWorkerSetupActions(input) {
|
|
|
2713
2690
|
message: 'The PlayDrop CLI package could not load playwright-core from its installed dependencies.',
|
|
2714
2691
|
});
|
|
2715
2692
|
}
|
|
2716
|
-
if (reasons.has('playdrop_plugin_staging_manifest_missing')) {
|
|
2717
|
-
actions.push({
|
|
2718
|
-
reason: 'playdrop_plugin_staging_manifest_missing',
|
|
2719
|
-
label: 'Install or update the PlayDrop agent plugin',
|
|
2720
|
-
command: 'playdrop agents status --json',
|
|
2721
|
-
message: 'Run the CLI-reported nextAction.command for a supported local agent so playdrop-worker-staging.json is available.',
|
|
2722
|
-
});
|
|
2723
|
-
}
|
|
2724
2693
|
if (reasons.has('agent_cli_not_found')) {
|
|
2725
2694
|
actions.push({
|
|
2726
2695
|
reason: 'agent_cli_not_found',
|
|
@@ -3591,8 +3560,14 @@ async function setupWorker(options = {}) {
|
|
|
3591
3560
|
}, { env: options.env });
|
|
3592
3561
|
}
|
|
3593
3562
|
async function failTaskWithRetry(client, taskId, body) {
|
|
3563
|
+
const terminalReason = body.terminalReason ?? (0, types_1.classifyAgentTaskTerminalReason)(body.error, 'worker');
|
|
3564
|
+
const requestBody = {
|
|
3565
|
+
...body,
|
|
3566
|
+
error: terminalReason.code,
|
|
3567
|
+
terminalReason,
|
|
3568
|
+
};
|
|
3594
3569
|
try {
|
|
3595
|
-
await client.workerFailAgentTask(taskId,
|
|
3570
|
+
await client.workerFailAgentTask(taskId, requestBody);
|
|
3596
3571
|
}
|
|
3597
3572
|
catch (firstError) {
|
|
3598
3573
|
if (isAgentTaskNotRunningError(firstError)) {
|
|
@@ -3600,7 +3575,7 @@ async function failTaskWithRetry(client, taskId, body) {
|
|
|
3600
3575
|
}
|
|
3601
3576
|
await sleep(FAIL_REPORT_RETRY_DELAY_MS);
|
|
3602
3577
|
try {
|
|
3603
|
-
await client.workerFailAgentTask(taskId,
|
|
3578
|
+
await client.workerFailAgentTask(taskId, requestBody);
|
|
3604
3579
|
}
|
|
3605
3580
|
catch (secondError) {
|
|
3606
3581
|
if (isAgentTaskNotRunningError(secondError)) {
|
|
@@ -3620,13 +3595,24 @@ async function fetchTaskDetail(client, target, taskId) {
|
|
|
3620
3595
|
return await client.getAgentTask(taskId);
|
|
3621
3596
|
}
|
|
3622
3597
|
function instrumentTaskChromeTitleMatchers(assignment) {
|
|
3623
|
-
const
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3598
|
+
const playdrop = assignment.task.metadata.playdrop;
|
|
3599
|
+
const metadata = playdrop && typeof playdrop === 'object' && !Array.isArray(playdrop)
|
|
3600
|
+
? playdrop
|
|
3601
|
+
: {};
|
|
3602
|
+
const kind = metadata.kind;
|
|
3603
|
+
const evalTarget = metadata.evalTarget && typeof metadata.evalTarget === 'object' && !Array.isArray(metadata.evalTarget)
|
|
3604
|
+
? metadata.evalTarget
|
|
3605
|
+
: null;
|
|
3606
|
+
const targets = kind === 'GAME_REVIEW'
|
|
3607
|
+
? [metadata.reviewTarget]
|
|
3608
|
+
: kind === 'GAME_EVAL' && Array.isArray(evalTarget?.targets)
|
|
3609
|
+
? evalTarget.targets
|
|
3627
3610
|
: [];
|
|
3628
3611
|
const matchers = new Set();
|
|
3629
|
-
for (const
|
|
3612
|
+
for (const rawTarget of targets) {
|
|
3613
|
+
const target = rawTarget && typeof rawTarget === 'object' && !Array.isArray(rawTarget)
|
|
3614
|
+
? rawTarget
|
|
3615
|
+
: null;
|
|
3630
3616
|
if (!target)
|
|
3631
3617
|
continue;
|
|
3632
3618
|
for (const value of [target.appDisplayName, target.appName]) {
|
|
@@ -3719,7 +3705,7 @@ on run matchers
|
|
|
3719
3705
|
end run
|
|
3720
3706
|
`;
|
|
3721
3707
|
async function enforceInstrumentBrowserHygiene(assignment, options = {}) {
|
|
3722
|
-
if (assignment.
|
|
3708
|
+
if (assignment.agent.browser === 'NONE') {
|
|
3723
3709
|
return { remainingTaskTabs: 0, blankWindows: 0 };
|
|
3724
3710
|
}
|
|
3725
3711
|
if ((options.platform ?? node_process_1.default.platform) !== 'darwin') {
|
|
@@ -3927,11 +3913,19 @@ async function startWorker(options = {}) {
|
|
|
3927
3913
|
});
|
|
3928
3914
|
}, HEARTBEAT_INTERVAL_MS);
|
|
3929
3915
|
const runClaimedTask = async (claim, devPort) => {
|
|
3930
|
-
const
|
|
3931
|
-
if (!
|
|
3932
|
-
throw new Error('
|
|
3916
|
+
const claimedAssignment = normalizeWorkerTaskAssignmentV2(claim);
|
|
3917
|
+
if (!claimedAssignment) {
|
|
3918
|
+
throw new Error('agent_task_claim_missing_assignment');
|
|
3933
3919
|
}
|
|
3934
|
-
const
|
|
3920
|
+
const taskContext = buildWorkerTaskContextV2(claimedAssignment);
|
|
3921
|
+
const task = {
|
|
3922
|
+
id: claimedAssignment.task.id,
|
|
3923
|
+
kind: taskContext.kind,
|
|
3924
|
+
model: claimedAssignment.agent.model,
|
|
3925
|
+
executionTarget: claimedAssignment.task.executionTarget,
|
|
3926
|
+
attempts: claimedAssignment.task.attempt,
|
|
3927
|
+
};
|
|
3928
|
+
const leaseToken = claim.action === 'run' ? claim.lease.token.trim() : '';
|
|
3935
3929
|
if (!leaseToken) {
|
|
3936
3930
|
throw new Error('agent_task_claim_missing_lease_token');
|
|
3937
3931
|
}
|
|
@@ -3946,7 +3940,7 @@ async function startWorker(options = {}) {
|
|
|
3946
3940
|
let workspaceDir = null;
|
|
3947
3941
|
let availableSkillPaths = [];
|
|
3948
3942
|
const observedSkillPaths = new Set();
|
|
3949
|
-
|
|
3943
|
+
const assignment = claimedAssignment;
|
|
3950
3944
|
let resolvedRuntimeModel = task.model;
|
|
3951
3945
|
let reasoningEffort = null;
|
|
3952
3946
|
let agentStartedAt = null;
|
|
@@ -3955,9 +3949,6 @@ async function startWorker(options = {}) {
|
|
|
3955
3949
|
let telemetryReported = false;
|
|
3956
3950
|
let heartbeatTransientFailures = 0;
|
|
3957
3951
|
const reportTelemetry = async (status, setupError) => {
|
|
3958
|
-
if (!assignment) {
|
|
3959
|
-
return;
|
|
3960
|
-
}
|
|
3961
3952
|
const completedAt = agentCompletedAt ?? new Date();
|
|
3962
3953
|
await recordAgentRunTelemetry({
|
|
3963
3954
|
client,
|
|
@@ -3977,24 +3968,19 @@ async function startWorker(options = {}) {
|
|
|
3977
3968
|
telemetryReported = true;
|
|
3978
3969
|
};
|
|
3979
3970
|
try {
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
throw new Error('agent_task_claim_missing_task_assignment');
|
|
3983
|
-
}
|
|
3984
|
-
const codexModel = assignment.agent === 'CODEX'
|
|
3985
|
-
? resolveCodexModel(assignment.model, assignment.reasoningEffort)
|
|
3971
|
+
const codexModel = assignment.agent.runtime === 'CODEX'
|
|
3972
|
+
? resolveCodexModel(assignment.agent.model, assignment.agent.reasoningEffort)
|
|
3986
3973
|
: null;
|
|
3987
|
-
const claudeModel = assignment.agent === 'CLAUDE_CODE'
|
|
3988
|
-
? resolveClaudeModel(assignment.model, assignment.reasoningEffort)
|
|
3974
|
+
const claudeModel = assignment.agent.runtime === 'CLAUDE_CODE'
|
|
3975
|
+
? resolveClaudeModel(assignment.agent.model, assignment.agent.reasoningEffort)
|
|
3989
3976
|
: null;
|
|
3990
|
-
resolvedRuntimeModel = codexModel?.model ?? claudeModel?.model ?? assignment.model;
|
|
3977
|
+
resolvedRuntimeModel = codexModel?.model ?? claudeModel?.model ?? assignment.agent.model;
|
|
3991
3978
|
reasoningEffort = codexModel?.reasoningEffort ?? claudeModel?.effort ?? null;
|
|
3992
|
-
const taskContext = buildWorkerTaskContext(claim, assignment);
|
|
3993
3979
|
workspaceDir = workerTaskWorkspaceDirFromAssignment(assignment);
|
|
3994
3980
|
await pruneWorkerTaskWorkspaces(workspaceDir);
|
|
3995
3981
|
await (0, promises_1.rm)(workspaceDir, { recursive: true, force: true });
|
|
3996
3982
|
await (0, promises_1.mkdir)(workspaceDir, { recursive: true });
|
|
3997
|
-
const eventDir = node_path_1.default.join(workspaceDir, '.playdrop
|
|
3983
|
+
const eventDir = node_path_1.default.join(workspaceDir, '.playdrop', 'task-events');
|
|
3998
3984
|
await (0, promises_1.mkdir)(eventDir, { recursive: true });
|
|
3999
3985
|
await stageWorkerTaskContext({
|
|
4000
3986
|
workspaceDir,
|
|
@@ -4039,52 +4025,16 @@ async function startWorker(options = {}) {
|
|
|
4039
4025
|
appendObservedSkillPathsFromTranscript(chunks);
|
|
4040
4026
|
await appendWorkerTranscriptChunks({ client, taskId: task.id, workerKey, leaseToken, chunks });
|
|
4041
4027
|
};
|
|
4042
|
-
await
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
pluginBundle: assignment.pluginBundle,
|
|
4053
|
-
});
|
|
4054
|
-
const stagedPluginPaths = await stagePlaydropPluginReferences({
|
|
4055
|
-
workspaceDir,
|
|
4056
|
-
pluginRoot: assignmentPluginRoot,
|
|
4057
|
-
kind: task.kind,
|
|
4058
|
-
});
|
|
4059
|
-
if (assignment.agent === 'CODEX') {
|
|
4060
|
-
await stageCodexTaskSkills({ workspaceDir, stagedPluginPaths });
|
|
4061
|
-
}
|
|
4062
|
-
availableSkillPaths = normalizeTelemetrySkillPaths(stagedPluginPaths);
|
|
4063
|
-
await client.workerCreateAgentTaskEvent(task.id, {
|
|
4064
|
-
workerKey,
|
|
4065
|
-
leaseToken,
|
|
4066
|
-
kind: 'progress',
|
|
4067
|
-
phase: 'setup',
|
|
4068
|
-
message: devPluginWorkingTree
|
|
4069
|
-
? 'Staged PlayDrop plugin references from explicit dev working tree'
|
|
4070
|
-
: 'Staged PlayDrop plugin references from server task bundle',
|
|
4071
|
-
pct: 3,
|
|
4072
|
-
payload: {
|
|
4073
|
-
pluginSource: devPluginWorkingTree ? 'DEV_WORKING_TREE' : 'SERVER_TASK_BUNDLE',
|
|
4074
|
-
bundleSha: assignment.pluginBundle.sha256,
|
|
4075
|
-
},
|
|
4076
|
-
});
|
|
4077
|
-
if (assignment.workspace.files.length > 0) {
|
|
4078
|
-
await client.workerCreateAgentTaskEvent(task.id, {
|
|
4079
|
-
workerKey,
|
|
4080
|
-
leaseToken,
|
|
4081
|
-
kind: 'progress',
|
|
4082
|
-
phase: 'setup',
|
|
4083
|
-
message: `Staging ${assignment.workspace.files.length} server-provided workspace file${assignment.workspace.files.length === 1 ? '' : 's'}`,
|
|
4084
|
-
pct: 3,
|
|
4085
|
-
});
|
|
4086
|
-
await stageAssignmentWorkspaceFiles(workspaceDir, assignment.workspace.files);
|
|
4087
|
-
}
|
|
4028
|
+
await stageAssignmentWorkspaceV2(workspaceDir, assignment.workspace);
|
|
4029
|
+
const assignmentPluginRoot = devPluginWorkingTree
|
|
4030
|
+
? await stageAgentBundleWorkspaceFiles(devPluginWorkingTree, workspaceDir)
|
|
4031
|
+
: assignment.plugin
|
|
4032
|
+
? await stageAssignmentPluginBundle({
|
|
4033
|
+
workspaceDir,
|
|
4034
|
+
pluginBundle: assignment.plugin,
|
|
4035
|
+
})
|
|
4036
|
+
: null;
|
|
4037
|
+
availableSkillPaths = listPluginSkillPaths(assignmentPluginRoot);
|
|
4088
4038
|
heartbeatTimer = setInterval(() => {
|
|
4089
4039
|
client.workerHeartbeatAgentTask(task.id, { workerKey, leaseToken })
|
|
4090
4040
|
.then((heartbeat) => {
|
|
@@ -4143,57 +4093,18 @@ async function startWorker(options = {}) {
|
|
|
4143
4093
|
handleEventDrainFailure(error);
|
|
4144
4094
|
});
|
|
4145
4095
|
}, 1000);
|
|
4146
|
-
const prompt = assignment.prompt;
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
kind: 'progress',
|
|
4154
|
-
phase: 'setup',
|
|
4155
|
-
message: `Staging base source ${baseSource.appName} version ${baseSource.version} into the workspace`,
|
|
4156
|
-
pct: 4,
|
|
4157
|
-
});
|
|
4158
|
-
await stageWorkerBaseAppSource({
|
|
4159
|
-
client,
|
|
4160
|
-
workspaceDir,
|
|
4161
|
-
creatorUsername: baseSourcePayload.creatorUsername,
|
|
4162
|
-
baseApp: baseSource,
|
|
4163
|
-
});
|
|
4164
|
-
}
|
|
4165
|
-
else if (task.kind === 'NEW_GAME' || task.kind === 'REMIX_GAME') {
|
|
4166
|
-
if (assignment.inputs.baseSource !== null) {
|
|
4167
|
-
throw new Error('agent_task_assignment_unexpected_base_source');
|
|
4168
|
-
}
|
|
4169
|
-
if (task.kind === 'REMIX_GAME' && !assignment.inputs.remixSource) {
|
|
4170
|
-
throw new Error('agent_task_assignment_remix_source_missing');
|
|
4171
|
-
}
|
|
4172
|
-
}
|
|
4173
|
-
else if (task.kind === 'GAME_REVIEW') {
|
|
4174
|
-
if (assignment.inputs.baseSource !== null) {
|
|
4175
|
-
throw new Error('agent_task_assignment_unexpected_base_source');
|
|
4176
|
-
}
|
|
4177
|
-
if (!assignment.inputs.reviewTarget) {
|
|
4178
|
-
throw new Error('agent_task_assignment_review_target_missing');
|
|
4179
|
-
}
|
|
4180
|
-
}
|
|
4181
|
-
else if (task.kind === 'GAME_EVAL') {
|
|
4182
|
-
if (assignment.inputs.baseSource !== null) {
|
|
4183
|
-
throw new Error('agent_task_assignment_unexpected_base_source');
|
|
4184
|
-
}
|
|
4185
|
-
if (!assignment.inputs.evalTarget) {
|
|
4186
|
-
throw new Error('agent_task_assignment_eval_target_missing');
|
|
4187
|
-
}
|
|
4188
|
-
}
|
|
4189
|
-
else {
|
|
4190
|
-
throw new Error(`unsupported_agent_task_kind:${task.kind}`);
|
|
4191
|
-
}
|
|
4096
|
+
const prompt = assignment.request.prompt;
|
|
4097
|
+
const imageAttachments = assignment.request.attachments
|
|
4098
|
+
.filter((attachment) => attachment.contentType.startsWith('image/'))
|
|
4099
|
+
.map((attachment) => ({
|
|
4100
|
+
path: resolveWorkspaceFileDestination(workspaceDir, attachment.path),
|
|
4101
|
+
contentType: attachment.contentType,
|
|
4102
|
+
}));
|
|
4192
4103
|
if (shuttingDown) {
|
|
4193
4104
|
throw new Error('worker_shutdown');
|
|
4194
4105
|
}
|
|
4195
4106
|
agentStartedAt = new Date();
|
|
4196
|
-
if (assignment.agent === 'CODEX') {
|
|
4107
|
+
if (assignment.agent.runtime === 'CODEX') {
|
|
4197
4108
|
agentResult = await runCodex({
|
|
4198
4109
|
workspaceDir,
|
|
4199
4110
|
binDir,
|
|
@@ -4202,9 +4113,12 @@ async function startWorker(options = {}) {
|
|
|
4202
4113
|
envName: env,
|
|
4203
4114
|
workerUsername: username,
|
|
4204
4115
|
taskId: task.id,
|
|
4116
|
+
taskToken: assignment.task.token,
|
|
4205
4117
|
attempt: taskContext.attempt,
|
|
4206
4118
|
devPort,
|
|
4207
|
-
codexModel: codexModel ?? resolveCodexModel(assignment.model),
|
|
4119
|
+
codexModel: codexModel ?? resolveCodexModel(assignment.agent.model),
|
|
4120
|
+
taskPluginRoot: assignmentPluginRoot,
|
|
4121
|
+
imagePaths: imageAttachments.map((attachment) => attachment.path),
|
|
4208
4122
|
onTranscriptChunks: async (chunks) => {
|
|
4209
4123
|
await appendTranscriptChunks(chunks);
|
|
4210
4124
|
},
|
|
@@ -4213,21 +4127,23 @@ async function startWorker(options = {}) {
|
|
|
4213
4127
|
},
|
|
4214
4128
|
});
|
|
4215
4129
|
}
|
|
4216
|
-
else if (assignment.agent === 'CLAUDE_CODE') {
|
|
4130
|
+
else if (assignment.agent.runtime === 'CLAUDE_CODE') {
|
|
4217
4131
|
agentResult = await runClaude({
|
|
4218
4132
|
workspaceDir,
|
|
4219
4133
|
binDir,
|
|
4220
4134
|
eventDir,
|
|
4221
4135
|
prompt,
|
|
4222
|
-
enableChrome:
|
|
4223
|
-
enablePlaywrightMcp:
|
|
4136
|
+
enableChrome: assignment.agent.browser === 'CHROME',
|
|
4137
|
+
enablePlaywrightMcp: assignment.agent.browser === 'PLAYWRIGHT',
|
|
4224
4138
|
envName: env,
|
|
4225
4139
|
workerUsername: username,
|
|
4226
4140
|
taskId: task.id,
|
|
4141
|
+
taskToken: assignment.task.token,
|
|
4227
4142
|
attempt: taskContext.attempt,
|
|
4228
4143
|
devPort,
|
|
4229
|
-
claudeModel: claudeModel ?? resolveClaudeModel(assignment.model),
|
|
4144
|
+
claudeModel: claudeModel ?? resolveClaudeModel(assignment.agent.model),
|
|
4230
4145
|
taskPluginRoot: assignmentPluginRoot,
|
|
4146
|
+
imageAttachments,
|
|
4231
4147
|
onTranscriptChunks: async (chunks) => {
|
|
4232
4148
|
await appendTranscriptChunks(chunks);
|
|
4233
4149
|
},
|
|
@@ -4236,17 +4152,11 @@ async function startWorker(options = {}) {
|
|
|
4236
4152
|
},
|
|
4237
4153
|
});
|
|
4238
4154
|
}
|
|
4239
|
-
else if (assignment.agent === 'CURSOR_COMPOSER') {
|
|
4240
|
-
const assignmentAgent = assignment.agent;
|
|
4241
|
-
const assignmentModel = assignment.model;
|
|
4242
|
-
const plannedAttemptIndex = assignment.attemptPlan?.options.findIndex((option) => option.agent === assignmentAgent && option.model === assignmentModel);
|
|
4243
|
-
const attemptIndex = typeof plannedAttemptIndex === 'number' && plannedAttemptIndex >= 0
|
|
4244
|
-
? plannedAttemptIndex
|
|
4245
|
-
: 0;
|
|
4155
|
+
else if (assignment.agent.runtime === 'CURSOR_COMPOSER') {
|
|
4246
4156
|
await client.workerRecordAgentTaskAttemptUnavailable(task.id, {
|
|
4247
4157
|
workerKey,
|
|
4248
4158
|
leaseToken,
|
|
4249
|
-
attemptIndex,
|
|
4159
|
+
attemptIndex: 0,
|
|
4250
4160
|
reason: 'unavailable_runtime',
|
|
4251
4161
|
message: 'Cursor Composer noninteractive worker execution is not configured on this worker.',
|
|
4252
4162
|
});
|
|
@@ -4254,7 +4164,7 @@ async function startWorker(options = {}) {
|
|
|
4254
4164
|
throw new Error('worker_cursor_composer_runner_not_configured');
|
|
4255
4165
|
}
|
|
4256
4166
|
else {
|
|
4257
|
-
throw new Error(`unsupported_worker_agent:${assignment.agent}`);
|
|
4167
|
+
throw new Error(`unsupported_worker_agent:${assignment.agent.runtime}`);
|
|
4258
4168
|
}
|
|
4259
4169
|
agentCompletedAt = new Date();
|
|
4260
4170
|
await queueEventDrain().catch((error) => {
|
|
@@ -4313,8 +4223,8 @@ async function startWorker(options = {}) {
|
|
|
4313
4223
|
}
|
|
4314
4224
|
else {
|
|
4315
4225
|
retainWorkspace = true;
|
|
4316
|
-
const failureCode = buildAgentFailureCode(assignment.agent, completedAgentResult, {
|
|
4317
|
-
model: assignment.model,
|
|
4226
|
+
const failureCode = buildAgentFailureCode(assignment.agent.runtime, completedAgentResult, {
|
|
4227
|
+
model: assignment.agent.model,
|
|
4318
4228
|
reasoningEffort,
|
|
4319
4229
|
});
|
|
4320
4230
|
await client.workerCreateAgentTaskEvent(task.id, {
|
|
@@ -4330,6 +4240,7 @@ async function startWorker(options = {}) {
|
|
|
4330
4240
|
workerKey,
|
|
4331
4241
|
leaseToken,
|
|
4332
4242
|
error: failureCode,
|
|
4243
|
+
terminalReason: (0, types_1.classifyAgentTaskTerminalReason)(agentFailureRawError(completedAgentResult), 'agent'),
|
|
4333
4244
|
result: agentRunResult,
|
|
4334
4245
|
});
|
|
4335
4246
|
await reportTelemetry('FAILED');
|
|
@@ -4416,6 +4327,7 @@ async function startWorker(options = {}) {
|
|
|
4416
4327
|
workerKey,
|
|
4417
4328
|
leaseToken,
|
|
4418
4329
|
error: normalizeWorkerFailureErrorCode(message),
|
|
4330
|
+
terminalReason: (0, types_1.classifyAgentTaskTerminalReason)(message, 'worker'),
|
|
4419
4331
|
result: { workerError: message },
|
|
4420
4332
|
});
|
|
4421
4333
|
}
|
|
@@ -4438,9 +4350,8 @@ async function startWorker(options = {}) {
|
|
|
4438
4350
|
if (eventDrainTimer) {
|
|
4439
4351
|
clearInterval(eventDrainTimer);
|
|
4440
4352
|
}
|
|
4441
|
-
if (
|
|
4442
|
-
&&
|
|
4443
|
-
&& (assignment.kind === 'GAME_REVIEW' || assignment.kind === 'GAME_EVAL')) {
|
|
4353
|
+
if (agentStartedAt
|
|
4354
|
+
&& assignment.agent.browser !== 'NONE') {
|
|
4444
4355
|
try {
|
|
4445
4356
|
const hygiene = await enforceInstrumentBrowserHygiene(assignment);
|
|
4446
4357
|
console.log(`Agent task ${task.id} browser hygiene passed: ${hygiene.remainingTaskTabs} task tabs, ${hygiene.blankWindows} blank window(s).`);
|
|
@@ -4514,8 +4425,7 @@ async function startWorker(options = {}) {
|
|
|
4514
4425
|
claimBackoffMs = nextClaimBackoffMs(claimBackoffMs);
|
|
4515
4426
|
continue;
|
|
4516
4427
|
}
|
|
4517
|
-
|
|
4518
|
-
if (!task) {
|
|
4428
|
+
if (claim.action !== 'run') {
|
|
4519
4429
|
if (shuttingDown || quiescingForUpdate) {
|
|
4520
4430
|
break;
|
|
4521
4431
|
}
|
|
@@ -4525,18 +4435,19 @@ async function startWorker(options = {}) {
|
|
|
4525
4435
|
await sleep(DEFAULT_POLL_INTERVAL_MS);
|
|
4526
4436
|
continue;
|
|
4527
4437
|
}
|
|
4528
|
-
|
|
4438
|
+
const taskId = claim.assignment.task.id;
|
|
4439
|
+
activeTaskIds.add(taskId);
|
|
4529
4440
|
activeDevPorts.add(devPort);
|
|
4530
4441
|
const taskRun = runClaimedTask(claim, devPort)
|
|
4531
4442
|
.catch((error) => {
|
|
4532
4443
|
recordFatalTaskError(error);
|
|
4533
4444
|
})
|
|
4534
4445
|
.finally(() => {
|
|
4535
|
-
activeTaskRuns.delete(
|
|
4536
|
-
activeTaskIds.delete(
|
|
4446
|
+
activeTaskRuns.delete(taskId);
|
|
4447
|
+
activeTaskIds.delete(taskId);
|
|
4537
4448
|
activeDevPorts.delete(devPort);
|
|
4538
4449
|
});
|
|
4539
|
-
activeTaskRuns.set(
|
|
4450
|
+
activeTaskRuns.set(taskId, taskRun);
|
|
4540
4451
|
if (options.once) {
|
|
4541
4452
|
await taskRun;
|
|
4542
4453
|
if (fatalTaskError) {
|
|
@@ -4633,6 +4544,21 @@ async function reportTask(options) {
|
|
|
4633
4544
|
}
|
|
4634
4545
|
resolveWorkerTaskStateForWrite();
|
|
4635
4546
|
}
|
|
4547
|
+
function showTaskContext() {
|
|
4548
|
+
const taskContext = readTaskContextFile();
|
|
4549
|
+
console.log(JSON.stringify({
|
|
4550
|
+
task: {
|
|
4551
|
+
id: taskContext.taskId,
|
|
4552
|
+
type: taskContext.taskType,
|
|
4553
|
+
attempt: taskContext.attempt,
|
|
4554
|
+
executionTarget: taskContext.target,
|
|
4555
|
+
metadata: taskContext.metadata,
|
|
4556
|
+
},
|
|
4557
|
+
request: {
|
|
4558
|
+
prompt: taskContext.creatorRequest,
|
|
4559
|
+
},
|
|
4560
|
+
}, null, 2));
|
|
4561
|
+
}
|
|
4636
4562
|
async function reportCatalogueTask(options) {
|
|
4637
4563
|
const message = options.message?.trim();
|
|
4638
4564
|
if (!message) {
|
|
@@ -4668,11 +4594,26 @@ async function resolveTaskCommandContext(command, optionsEnv, taskContext) {
|
|
|
4668
4594
|
if (!ctx) {
|
|
4669
4595
|
return null;
|
|
4670
4596
|
}
|
|
4671
|
-
const
|
|
4597
|
+
const taskClientOptions = {
|
|
4598
|
+
token: ctx.token,
|
|
4599
|
+
onBehalfCreatorUsername: taskContext.creatorUsername,
|
|
4600
|
+
agentTaskToken: taskContext.taskToken,
|
|
4601
|
+
agentTaskId: taskContext.taskId,
|
|
4602
|
+
agentTaskAttempt: taskContext.attempt,
|
|
4603
|
+
};
|
|
4604
|
+
const client = (0, apiClient_1.createCliApiClient)({
|
|
4605
|
+
...taskClientOptions,
|
|
4606
|
+
baseUrl: ctx.envConfig.apiBase,
|
|
4607
|
+
});
|
|
4608
|
+
const aiClient = (0, apiClient_1.createCliAiClient)({
|
|
4609
|
+
...taskClientOptions,
|
|
4610
|
+
baseUrl: ctx.envConfig.aiBase,
|
|
4611
|
+
});
|
|
4612
|
+
const me = await client.me();
|
|
4672
4613
|
if (!me.user) {
|
|
4673
4614
|
throw new Error('task_context_user_missing');
|
|
4674
4615
|
}
|
|
4675
|
-
return { ...ctx, user: me.user };
|
|
4616
|
+
return { ...ctx, client, aiClient, user: me.user };
|
|
4676
4617
|
}
|
|
4677
4618
|
async function uploadTask(options = {}) {
|
|
4678
4619
|
const taskContext = readTaskContextFile();
|
|
@@ -4811,6 +4752,7 @@ async function completeTask(options) {
|
|
|
4811
4752
|
await ctx.client.workerFailAgentTask(taskContext.taskId, {
|
|
4812
4753
|
taskToken: taskContext.taskToken,
|
|
4813
4754
|
error: 'remix_relation_missing',
|
|
4755
|
+
terminalReason: (0, types_1.classifyAgentTaskTerminalReason)('upload_validation:remix_relation_missing', 'agent'),
|
|
4814
4756
|
result: {
|
|
4815
4757
|
appId: uploadResult.appId,
|
|
4816
4758
|
appVersionId: uploadResult.appVersionId,
|
|
@@ -5074,6 +5016,7 @@ async function instrumentErrorTask(options) {
|
|
|
5074
5016
|
await ctx.client.workerFailAgentTask(taskContext.taskId, {
|
|
5075
5017
|
taskToken: taskContext.taskToken,
|
|
5076
5018
|
error: `instrument_error:${reason}`,
|
|
5019
|
+
terminalReason: (0, types_1.classifyAgentTaskTerminalReason)(`instrument_error:${reason}`, 'instrument'),
|
|
5077
5020
|
result: {
|
|
5078
5021
|
outcome: 'instrument_error',
|
|
5079
5022
|
reason,
|
|
@@ -5097,6 +5040,7 @@ async function failTask(options) {
|
|
|
5097
5040
|
await ctx.client.workerFailAgentTask(taskContext.taskId, {
|
|
5098
5041
|
taskToken: taskContext.taskToken,
|
|
5099
5042
|
error: message,
|
|
5043
|
+
terminalReason: (0, types_1.classifyAgentTaskTerminalReason)(message, 'agent'),
|
|
5100
5044
|
result: { failedBy: 'agent' },
|
|
5101
5045
|
});
|
|
5102
5046
|
(0, output_1.printSuccess)('Task marked failed. This was the final operation; send one short final status sentence and stop immediately.');
|