@playdrop/playdrop-cli 0.17.19 → 0.17.21

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.
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.17.19",
2
+ "version": "0.17.21",
3
3
  "build": 1,
4
- "runtimeSdkVersion": "0.17.19",
4
+ "runtimeSdkVersion": "0.17.21",
5
5
  "runtimeSdkBuild": 1,
6
6
  "clients": {
7
7
  "all": {
@@ -1,8 +1,9 @@
1
1
  import type { ApiClient } from "@playdrop/api-client";
2
2
  import { type AppPlaytestTape, type AppPlaytestTapeEvent, type AppSurface, type AppUploadCausalComparison, type AppUploadMachineCausalEvidence, type DevGameServerCredentialResponse, type UserResponse } from "@playdrop/types";
3
- import type { BrowserContextOptions, Frame, Page } from "playwright-core";
3
+ import type { Browser, BrowserContextOptions, Frame, Page } from "playwright-core";
4
4
  import type { AppTask } from "../catalogue";
5
5
  import { type TaskCaptureSession } from "../appUrls";
6
+ import type { GameServerBuild } from "./serverBuild";
6
7
  export { PLAYDROP_SURFACE_CONTEXT_OPTIONS } from "./surfaceProfiles";
7
8
  export type HostedLoadCheckResult = {
8
9
  status: "PASSED" | "FAILED";
@@ -54,7 +55,7 @@ export declare function verifyHostedEditorPhaseRoundTrip(page: Pick<Page, "evalu
54
55
  export declare function formatHostedLoadCheckFailure(taskName: string, result: HostedLoadCheckResult, scope?: "local" | "staged" | "editor" | "final"): string;
55
56
  export declare function redactHostedLoadCheckSecrets(text: string, sourceUrl: string): string;
56
57
  export declare function formatMountedDevRuntimeFailure(error: unknown): Error;
57
- export declare function runLocalHostedLoadCheck(input: {
58
+ export type LocalHostedLoadCheckBaseInput = {
58
59
  client: ApiClient;
59
60
  apiBase: string;
60
61
  webBase: string | null | undefined;
@@ -66,12 +67,20 @@ export declare function runLocalHostedLoadCheck(input: {
66
67
  currentUser?: UserResponse | null;
67
68
  captureSession?: TaskCaptureSession | null;
68
69
  allowUnregisteredViewerLaunch?: boolean;
70
+ gameServerCredential?: DevGameServerCredentialResponse | null;
71
+ gameServerBuild?: GameServerBuild | null;
72
+ };
73
+ export type LocalHostedLoadCheckRunInput = {
69
74
  screenshotPath?: string | null;
70
75
  surface?: AppSurface;
71
76
  playtestTape?: AppPlaytestTape;
72
77
  postReadyWaitMs?: number;
73
- gameServerCredential?: DevGameServerCredentialResponse | null;
74
- }): Promise<HostedLoadCheckResult>;
78
+ };
79
+ export type LocalHostedLoadCheckInput = LocalHostedLoadCheckBaseInput & LocalHostedLoadCheckRunInput;
80
+ export declare function runLocalHostedLoadCheck(input: LocalHostedLoadCheckInput): Promise<HostedLoadCheckResult>;
81
+ export declare function runLocalHostedLoadCheckSequence(input: LocalHostedLoadCheckBaseInput & {
82
+ checks: LocalHostedLoadCheckRunInput[];
83
+ }): Promise<HostedLoadCheckResult[]>;
75
84
  export declare function buildUploadedHostedLoadCheckUrl(rawUrl: string, phase?: "play" | "editor", localDevPort?: number): string;
76
85
  export declare function buildUploadedHostedLoadCheckScreenshotPath(task: Pick<AppTask, "name" | "projectDir">, sessionId: string, phase?: "play" | "editor"): string | null;
77
86
  export declare function runUploadedHostedLoadCheck(input: {
@@ -91,6 +100,7 @@ export declare function runUploadedHostedLoadCheck(input: {
91
100
  postReadyWaitMs?: number;
92
101
  phase?: "play" | "editor";
93
102
  verifyEditorPhaseRoundTrip?: boolean;
103
+ installedChromeBrowser?: Browser;
94
104
  }): Promise<HostedLoadCheckResult>;
95
105
  export declare function comparePlaytestFrames(firstPath: string, secondPath: string, thresholds?: {
96
106
  meanDeltaMin: number;
@@ -21,6 +21,7 @@ exports.formatHostedLoadCheckFailure = formatHostedLoadCheckFailure;
21
21
  exports.redactHostedLoadCheckSecrets = redactHostedLoadCheckSecrets;
22
22
  exports.formatMountedDevRuntimeFailure = formatMountedDevRuntimeFailure;
23
23
  exports.runLocalHostedLoadCheck = runLocalHostedLoadCheck;
24
+ exports.runLocalHostedLoadCheckSequence = runLocalHostedLoadCheckSequence;
24
25
  exports.buildUploadedHostedLoadCheckUrl = buildUploadedHostedLoadCheckUrl;
25
26
  exports.buildUploadedHostedLoadCheckScreenshotPath = buildUploadedHostedLoadCheckScreenshotPath;
26
27
  exports.runUploadedHostedLoadCheck = runUploadedHostedLoadCheck;
@@ -935,7 +936,7 @@ async function runHostedLoadCheck(options) {
935
936
  }, {
936
937
  ...options.contextOptions,
937
938
  automationOrigin: new URL(options.targetUrl).origin,
938
- });
939
+ }, options.installedChromeBrowser);
939
940
  }
940
941
  catch (error) {
941
942
  const launchFailureMessage = redactReportText(error instanceof Error ? error.message : String(error));
@@ -1014,6 +1015,7 @@ async function startOrReuseMountedDevRuntime(input) {
1014
1015
  credential: input.gameServerCredential,
1015
1016
  platformApiUrl: input.apiBase,
1016
1017
  publicAddress: `${roomsUrl.host}${roomsUrl.pathname.replace(/\/$/u, "")}`,
1018
+ gameServerBuild: input.gameServerBuild ?? undefined,
1017
1019
  });
1018
1020
  }
1019
1021
  const handle = await (0, devServer_1.startDevServer)({
@@ -1044,116 +1046,152 @@ function formatMountedDevRuntimeFailure(error) {
1044
1046
  }
1045
1047
  return error instanceof Error ? error : new Error(message);
1046
1048
  }
1047
- async function runLocalHostedLoadCheck(input) {
1048
- let mountedRuntime = null;
1049
- try {
1050
- mountedRuntime = await startOrReuseMountedDevRuntime({
1051
- client: input.client,
1052
- apiBase: input.apiBase,
1053
- creatorUsername: input.creatorUsername,
1054
- task: input.task,
1055
- devRouterPort: input.devRouterPort,
1056
- gameServerCredential: input.gameServerCredential,
1049
+ async function runLocalHostedLoadCheckAgainstMountedRuntime(input, mountedRuntime, installedChromeBrowser) {
1050
+ const localDevPort = input.devRouterPort === undefined || input.devRouterPort === devServer_1.DEV_ROUTER_PORT ? null : input.devRouterPort;
1051
+ const localAppMetadata = mountedRuntime.runtimeAssetManifest.response.localAppMetadata;
1052
+ const declaredSurfaces = normalizeLoadCheckSurfaceTargets(localAppMetadata.surfaceTargets);
1053
+ const captureSurface = input.surface ?? resolveLoadCheckSurface(localAppMetadata.surfaceTargets, input.task.primarySurface);
1054
+ if (!declaredSurfaces.includes(captureSurface)) {
1055
+ return buildFailedLoadCheckResult("surface_unsupported", `Surface ${captureSurface} is not declared in surfaceTargets for "${input.task.name}".`, null);
1056
+ }
1057
+ const contextOptions = cloneLoadCheckContextOptions(captureSurface);
1058
+ const anonymousExpectedState = resolveAnonymousHostedLaunchState({
1059
+ ...localAppMetadata,
1060
+ captureSurface,
1061
+ });
1062
+ if (input.captureSession) {
1063
+ return await runHostedLoadCheck({
1064
+ targetUrl: (0, appUrls_1.buildPlatformDevUrl)(input.webBase, {
1065
+ creatorUsername: input.creatorUsername,
1066
+ appName: input.task.name,
1067
+ appType: input.task.type ?? "GAME",
1068
+ devAuth: "player",
1069
+ player: "1",
1070
+ launchCheck: true,
1071
+ localDevPort,
1072
+ captureSession: input.captureSession,
1073
+ }),
1074
+ timeoutMs: input.timeoutMs,
1075
+ expectedHostedLaunchState: resolvePostAuthHostedLaunchState(localAppMetadata.controllerMode),
1076
+ contextOptions,
1077
+ screenshotPath: input.screenshotPath,
1078
+ playtestTape: input.playtestTape,
1079
+ playtestSurface: captureSurface,
1080
+ postReadyWaitMs: input.postReadyWaitMs,
1081
+ installedChromeBrowser,
1057
1082
  });
1058
- const localDevPort = input.devRouterPort === undefined || input.devRouterPort === devServer_1.DEV_ROUTER_PORT ? null : input.devRouterPort;
1059
- const localAppMetadata = mountedRuntime.runtimeAssetManifest.response.localAppMetadata;
1060
- const declaredSurfaces = normalizeLoadCheckSurfaceTargets(localAppMetadata.surfaceTargets);
1061
- const captureSurface = input.surface ?? resolveLoadCheckSurface(localAppMetadata.surfaceTargets, input.task.primarySurface);
1062
- if (!declaredSurfaces.includes(captureSurface)) {
1063
- return buildFailedLoadCheckResult("surface_unsupported", `Surface ${captureSurface} is not declared in surfaceTargets for "${input.task.name}".`, null);
1083
+ }
1084
+ if (anonymousExpectedState === "login_required") {
1085
+ const registeredApp = await (0, registration_1.fetchRegisteredAppShell)(input.client, input.creatorUsername, input.task.name);
1086
+ const canLaunchUnregisteredViewer = input.allowUnregisteredViewerLaunch === true && Boolean(input.token) && Boolean(input.currentUser);
1087
+ if (!registeredApp?.id && !canLaunchUnregisteredViewer) {
1088
+ return buildFailedLoadCheckResult("app_registration_required_for_auth_validation", `Auth-required hosted app "${input.task.name}" must be registered on PlayDrop before viewer load-check can run. Run "playdrop project create app ${input.task.name}" and try again.`, null);
1064
1089
  }
1065
- const contextOptions = cloneLoadCheckContextOptions(captureSurface);
1066
- const anonymousExpectedState = resolveAnonymousHostedLaunchState({
1067
- ...localAppMetadata,
1068
- captureSurface,
1090
+ const anonymousGateResult = await runHostedLoadCheck({
1091
+ targetUrl: (0, appUrls_1.buildPlatformDevUrl)(input.webBase, {
1092
+ creatorUsername: input.creatorUsername,
1093
+ appName: input.task.name,
1094
+ appType: input.task.type ?? "GAME",
1095
+ devAuth: "anonymous",
1096
+ launchCheck: true,
1097
+ localDevPort,
1098
+ }),
1099
+ timeoutMs: input.timeoutMs,
1100
+ expectedHostedLaunchState: anonymousExpectedState,
1101
+ contextOptions,
1102
+ installedChromeBrowser,
1069
1103
  });
1070
- if (input.captureSession) {
1071
- return await runHostedLoadCheck({
1072
- targetUrl: (0, appUrls_1.buildPlatformDevUrl)(input.webBase, {
1073
- creatorUsername: input.creatorUsername,
1074
- appName: input.task.name,
1075
- appType: input.task.type ?? "GAME",
1076
- devAuth: "player",
1077
- player: "1",
1078
- launchCheck: true,
1079
- localDevPort,
1080
- captureSession: input.captureSession,
1081
- }),
1082
- timeoutMs: input.timeoutMs,
1083
- expectedHostedLaunchState: resolvePostAuthHostedLaunchState(localAppMetadata.controllerMode),
1084
- contextOptions,
1085
- screenshotPath: input.screenshotPath,
1086
- playtestTape: input.playtestTape,
1087
- playtestSurface: captureSurface,
1088
- postReadyWaitMs: input.postReadyWaitMs,
1089
- });
1090
- }
1091
- if (anonymousExpectedState === "login_required") {
1092
- const registeredApp = await (0, registration_1.fetchRegisteredAppShell)(input.client, input.creatorUsername, input.task.name);
1093
- const canLaunchUnregisteredViewer = input.allowUnregisteredViewerLaunch === true && Boolean(input.token) && Boolean(input.currentUser);
1094
- if (!registeredApp?.id && !canLaunchUnregisteredViewer) {
1095
- return buildFailedLoadCheckResult("app_registration_required_for_auth_validation", `Auth-required hosted app "${input.task.name}" must be registered on PlayDrop before viewer load-check can run. Run "playdrop project create app ${input.task.name}" and try again.`, null);
1096
- }
1097
- const anonymousGateResult = await runHostedLoadCheck({
1098
- targetUrl: (0, appUrls_1.buildPlatformDevUrl)(input.webBase, {
1099
- creatorUsername: input.creatorUsername,
1100
- appName: input.task.name,
1101
- appType: input.task.type ?? "GAME",
1102
- devAuth: "anonymous",
1103
- launchCheck: true,
1104
- localDevPort,
1105
- }),
1106
- timeoutMs: input.timeoutMs,
1107
- expectedHostedLaunchState: anonymousExpectedState,
1108
- contextOptions,
1109
- });
1110
- if (anonymousGateResult.status !== "PASSED") {
1111
- return anonymousGateResult;
1112
- }
1113
- return await runHostedLoadCheck({
1114
- targetUrl: (0, appUrls_1.buildPlatformDevUrl)(input.webBase, {
1115
- creatorUsername: input.creatorUsername,
1116
- appName: input.task.name,
1117
- appType: input.task.type ?? "GAME",
1118
- devAuth: "player",
1119
- player: "1",
1120
- launchCheck: true,
1121
- localDevPort,
1122
- }),
1123
- timeoutMs: input.timeoutMs,
1124
- expectedHostedLaunchState: resolvePostAuthHostedLaunchState(localAppMetadata.controllerMode),
1125
- token: input.token ?? null,
1126
- user: input.currentUser ?? null,
1127
- savedSessionBootstrap: true,
1128
- contextOptions,
1129
- screenshotPath: input.screenshotPath,
1130
- playtestTape: input.playtestTape,
1131
- playtestSurface: captureSurface,
1132
- postReadyWaitMs: input.postReadyWaitMs,
1133
- });
1104
+ if (anonymousGateResult.status !== "PASSED") {
1105
+ return anonymousGateResult;
1134
1106
  }
1135
1107
  return await runHostedLoadCheck({
1136
1108
  targetUrl: (0, appUrls_1.buildPlatformDevUrl)(input.webBase, {
1137
1109
  creatorUsername: input.creatorUsername,
1138
1110
  appName: input.task.name,
1139
1111
  appType: input.task.type ?? "GAME",
1140
- devAuth: "anonymous",
1112
+ devAuth: "player",
1113
+ player: "1",
1141
1114
  launchCheck: true,
1142
1115
  localDevPort,
1143
1116
  }),
1144
1117
  timeoutMs: input.timeoutMs,
1145
- expectedHostedLaunchState: anonymousExpectedState,
1118
+ expectedHostedLaunchState: resolvePostAuthHostedLaunchState(localAppMetadata.controllerMode),
1119
+ token: input.token ?? null,
1120
+ user: input.currentUser ?? null,
1121
+ savedSessionBootstrap: true,
1146
1122
  contextOptions,
1147
1123
  screenshotPath: input.screenshotPath,
1148
1124
  playtestTape: input.playtestTape,
1149
1125
  playtestSurface: captureSurface,
1150
1126
  postReadyWaitMs: input.postReadyWaitMs,
1127
+ installedChromeBrowser,
1128
+ });
1129
+ }
1130
+ return await runHostedLoadCheck({
1131
+ targetUrl: (0, appUrls_1.buildPlatformDevUrl)(input.webBase, {
1132
+ creatorUsername: input.creatorUsername,
1133
+ appName: input.task.name,
1134
+ appType: input.task.type ?? "GAME",
1135
+ devAuth: "anonymous",
1136
+ launchCheck: true,
1137
+ localDevPort,
1138
+ }),
1139
+ timeoutMs: input.timeoutMs,
1140
+ expectedHostedLaunchState: anonymousExpectedState,
1141
+ contextOptions,
1142
+ screenshotPath: input.screenshotPath,
1143
+ playtestTape: input.playtestTape,
1144
+ playtestSurface: captureSurface,
1145
+ postReadyWaitMs: input.postReadyWaitMs,
1146
+ installedChromeBrowser,
1147
+ });
1148
+ }
1149
+ async function mountLocalHostedLoadCheckRuntime(input) {
1150
+ return await startOrReuseMountedDevRuntime({
1151
+ client: input.client,
1152
+ apiBase: input.apiBase,
1153
+ creatorUsername: input.creatorUsername,
1154
+ task: input.task,
1155
+ devRouterPort: input.devRouterPort,
1156
+ gameServerCredential: input.gameServerCredential,
1157
+ gameServerBuild: input.gameServerBuild,
1158
+ });
1159
+ }
1160
+ async function runLocalHostedLoadCheck(input) {
1161
+ const mountedRuntime = await mountLocalHostedLoadCheckRuntime(input);
1162
+ try {
1163
+ return await runLocalHostedLoadCheckAgainstMountedRuntime(input, mountedRuntime);
1164
+ }
1165
+ finally {
1166
+ if (mountedRuntime.handle)
1167
+ await mountedRuntime.handle.close().catch(() => { });
1168
+ }
1169
+ }
1170
+ async function runLocalHostedLoadCheckSequence(input) {
1171
+ if (input.checks.length === 0) {
1172
+ throw new Error("local_hosted_load_check_sequence_empty");
1173
+ }
1174
+ const { checks, ...baseInput } = input;
1175
+ const mountStartedAt = Date.now();
1176
+ const mountedRuntime = await mountLocalHostedLoadCheckRuntime(baseInput);
1177
+ console.log(`[check] Local runtime ready in ${Date.now() - mountStartedAt} ms; reusing one mount for ${checks.length} gameplay passes.`);
1178
+ try {
1179
+ const chromeStartedAt = Date.now();
1180
+ return await (0, playwright_1.withInstalledChromeBrowser)(async (installedChromeBrowser) => {
1181
+ console.log(`[check] Chrome ready in ${Date.now() - chromeStartedAt} ms; reusing one process with isolated pages.`);
1182
+ const results = [];
1183
+ for (const check of checks) {
1184
+ const result = await runLocalHostedLoadCheckAgainstMountedRuntime({ ...baseInput, ...check }, mountedRuntime, installedChromeBrowser);
1185
+ results.push(result);
1186
+ if (result.status !== "PASSED")
1187
+ break;
1188
+ }
1189
+ return results;
1151
1190
  });
1152
1191
  }
1153
1192
  finally {
1154
- if (mountedRuntime?.handle) {
1193
+ if (mountedRuntime.handle)
1155
1194
  await mountedRuntime.handle.close().catch(() => { });
1156
- }
1157
1195
  }
1158
1196
  }
1159
1197
  function appendLoadCheckParams(targetUrl, devAuth) {
@@ -1214,6 +1252,7 @@ async function runUploadedHostedLoadCheck(input) {
1214
1252
  playtestSurface: captureSurface,
1215
1253
  postReadyWaitMs: input.postReadyWaitMs,
1216
1254
  verifyEditorPhaseRoundTrip: input.verifyEditorPhaseRoundTrip,
1255
+ installedChromeBrowser: input.installedChromeBrowser,
1217
1256
  });
1218
1257
  }
1219
1258
  else if (anonymousExpectedState === "login_required") {
@@ -1229,6 +1268,7 @@ async function runUploadedHostedLoadCheck(input) {
1229
1268
  contextOptions,
1230
1269
  screenshotPath: input.screenshotPath,
1231
1270
  failureScreenshotPath,
1271
+ installedChromeBrowser: input.installedChromeBrowser,
1232
1272
  });
1233
1273
  if (anonymousGateResult.status !== "PASSED") {
1234
1274
  result = anonymousGateResult;
@@ -1253,6 +1293,7 @@ async function runUploadedHostedLoadCheck(input) {
1253
1293
  playtestSurface: captureSurface,
1254
1294
  postReadyWaitMs: input.postReadyWaitMs,
1255
1295
  verifyEditorPhaseRoundTrip: input.verifyEditorPhaseRoundTrip,
1296
+ installedChromeBrowser: input.installedChromeBrowser,
1256
1297
  });
1257
1298
  }
1258
1299
  }
@@ -1272,6 +1313,7 @@ async function runUploadedHostedLoadCheck(input) {
1272
1313
  playtestSurface: captureSurface,
1273
1314
  postReadyWaitMs: input.postReadyWaitMs,
1274
1315
  verifyEditorPhaseRoundTrip: input.verifyEditorPhaseRoundTrip,
1316
+ installedChromeBrowser: input.installedChromeBrowser,
1275
1317
  });
1276
1318
  }
1277
1319
  const recorded = await input.client.recordAppUploadLaunchCheck(input.creatorUsername, input.task.name, input.sessionId, {
@@ -1336,7 +1378,7 @@ async function runUploadedHostedPlaytestSmokeCheck(input) {
1336
1378
  zeroInput: (0, node_path_1.join)(tempDir, "zero-input.png"),
1337
1379
  fullTape: (0, node_path_1.join)(tempDir, "full-tape.png"),
1338
1380
  };
1339
- const run = async (label, options) => {
1381
+ const run = async (installedChromeBrowser, label, options) => {
1340
1382
  console.log(`[upload] Playtest smoke check: ${label} from sdk.host.ready().`);
1341
1383
  const result = await runUploadedHostedLoadCheck({
1342
1384
  client: input.client,
@@ -1348,6 +1390,7 @@ async function runUploadedHostedPlaytestSmokeCheck(input) {
1348
1390
  token: input.token,
1349
1391
  currentUser: input.currentUser,
1350
1392
  captureSession: input.captureSession,
1393
+ installedChromeBrowser,
1351
1394
  ...options,
1352
1395
  });
1353
1396
  if (result.status !== "PASSED") {
@@ -1360,43 +1403,47 @@ async function runUploadedHostedPlaytestSmokeCheck(input) {
1360
1403
  return result;
1361
1404
  };
1362
1405
  try {
1363
- const zeroInput = await run("zero-input", {
1364
- postReadyWaitMs: fullTape.durationMs,
1365
- gameFrameScreenshotPath: paths.zeroInput,
1366
- });
1367
- const full = await run("full-tape", {
1368
- playtestTape: fullTape,
1369
- gameFrameScreenshotPath: paths.fullTape,
1406
+ const chromeStartedAt = Date.now();
1407
+ return await (0, playwright_1.withInstalledChromeBrowser)(async (browser) => {
1408
+ console.log(`[upload] Chrome ready in ${Date.now() - chromeStartedAt} ms; reusing one process with isolated pages.`);
1409
+ const zeroInput = await run(browser, "zero-input", {
1410
+ postReadyWaitMs: fullTape.durationMs,
1411
+ gameFrameScreenshotPath: paths.zeroInput,
1412
+ });
1413
+ const full = await run(browser, "full-tape", {
1414
+ playtestTape: fullTape,
1415
+ gameFrameScreenshotPath: paths.fullTape,
1416
+ });
1417
+ const artifactFingerprint = full.artifactFingerprint;
1418
+ if (zeroInput.artifactFingerprint !== artifactFingerprint) {
1419
+ throw new Error("agent_task_playtest_smoke_check_artifact_changed_during_check");
1420
+ }
1421
+ const comparison = await comparePlaytestFrames(paths.zeroInput, paths.fullTape);
1422
+ if (!comparison.passed) {
1423
+ throw new Error(`agent_task_playtest_smoke_check_not_interactive: The validator completed all ${fullTape.events.length} tape actions ` +
1424
+ `without a crash, but the result looked the same as running the game for ${fullTape.durationMs} ms without input ` +
1425
+ `(mean pixel change ${comparison.meanDelta.toFixed(4)}, changed pixels ${(comparison.changedPixelRatio * 100).toFixed(2)}%). ` +
1426
+ "Make sure the tape performs the core gameplay action and produces a visible response, then rerun the tape check.");
1427
+ }
1428
+ const evidence = {
1429
+ version: 2,
1430
+ source: "CLI_MACHINE",
1431
+ artifactFingerprint,
1432
+ primarySurface,
1433
+ primaryVerb: fullTape.primaryVerb,
1434
+ runs: {
1435
+ zeroInput: { finalFrameSha256: await hashFile(paths.zeroInput) },
1436
+ fullTape: { finalFrameSha256: await hashFile(paths.fullTape) },
1437
+ },
1438
+ comparison,
1439
+ };
1440
+ const recorded = await input.client.recordAppUploadCausalCheck(input.creatorUsername, input.task.name, input.sessionId, { evidence });
1441
+ if (recorded.session.causalCheck.status !== "PASSED") {
1442
+ throw new Error("agent_task_playtest_smoke_check_not_recorded");
1443
+ }
1444
+ console.log(`[upload] Playtest smoke check passed for ${input.creatorUsername}/${input.task.name}.`);
1445
+ return evidence;
1370
1446
  });
1371
- const artifactFingerprint = full.artifactFingerprint;
1372
- if (zeroInput.artifactFingerprint !== artifactFingerprint) {
1373
- throw new Error("agent_task_playtest_smoke_check_artifact_changed_during_check");
1374
- }
1375
- const comparison = await comparePlaytestFrames(paths.zeroInput, paths.fullTape);
1376
- if (!comparison.passed) {
1377
- throw new Error(`agent_task_playtest_smoke_check_not_interactive: The validator completed all ${fullTape.events.length} tape actions ` +
1378
- `without a crash, but the result looked the same as running the game for ${fullTape.durationMs} ms without input ` +
1379
- `(mean pixel change ${comparison.meanDelta.toFixed(4)}, changed pixels ${(comparison.changedPixelRatio * 100).toFixed(2)}%). ` +
1380
- "Make sure the tape performs the core gameplay action and produces a visible response, then rerun the tape check.");
1381
- }
1382
- const evidence = {
1383
- version: 2,
1384
- source: "CLI_MACHINE",
1385
- artifactFingerprint,
1386
- primarySurface,
1387
- primaryVerb: fullTape.primaryVerb,
1388
- runs: {
1389
- zeroInput: { finalFrameSha256: await hashFile(paths.zeroInput) },
1390
- fullTape: { finalFrameSha256: await hashFile(paths.fullTape) },
1391
- },
1392
- comparison,
1393
- };
1394
- const recorded = await input.client.recordAppUploadCausalCheck(input.creatorUsername, input.task.name, input.sessionId, { evidence });
1395
- if (recorded.session.causalCheck.status !== "PASSED") {
1396
- throw new Error("agent_task_playtest_smoke_check_not_recorded");
1397
- }
1398
- console.log(`[upload] Playtest smoke check passed for ${input.creatorUsername}/${input.task.name}.`);
1399
- return evidence;
1400
1447
  }
1401
1448
  finally {
1402
1449
  await (0, promises_1.rm)(tempDir, { recursive: true, force: true });
@@ -91,12 +91,13 @@ async function check(targetArg, options = {}) {
91
91
  process.exitCode = 1;
92
92
  return;
93
93
  }
94
+ let gameServerBuild = null;
94
95
  if (taskLookup.task.server) {
95
96
  try {
96
- const serverBuild = await (0, serverBuild_1.buildGameServer)(taskLookup.task);
97
- if (!serverBuild)
97
+ gameServerBuild = await (0, serverBuild_1.buildGameServer)(taskLookup.task);
98
+ if (!gameServerBuild)
98
99
  throw new Error("server_build_missing");
99
- console.log(`[check] Server contract passed: ${serverBuild.manifest.rooms.join(", ")} -> ${(0, serverBuild_1.formatServerBuildPath)(taskLookup.task, serverBuild.bundlePath)} (${serverBuild.manifest.size} bytes)`);
100
+ console.log(`[check] Server contract passed: ${gameServerBuild.manifest.rooms.join(", ")} -> ${(0, serverBuild_1.formatServerBuildPath)(taskLookup.task, gameServerBuild.bundlePath)} (${gameServerBuild.manifest.size} bytes)`);
100
101
  }
101
102
  catch (error) {
102
103
  (0, messages_1.printErrorWithHelp)(error instanceof Error ? error.message : "Server validation failed.", ["Fix the server declaration, frozen dependencies, imports, authentication, or bundle output."], { command: "project check" });
@@ -216,7 +217,7 @@ async function check(targetArg, options = {}) {
216
217
  return;
217
218
  }
218
219
  }
219
- const runCheck = async (input) => await (0, loadCheck_1.runLocalHostedLoadCheck)({
220
+ const localLoadCheckInput = {
220
221
  client,
221
222
  apiBase: envConfig.apiBase,
222
223
  webBase: envConfig.webBase,
@@ -227,30 +228,43 @@ async function check(targetArg, options = {}) {
227
228
  currentUser: null,
228
229
  devRouterPort: (0, dev_1.resolveCliDevRouterPort)(),
229
230
  captureSession,
231
+ gameServerCredential,
232
+ gameServerBuild,
233
+ };
234
+ const runCheck = async (input) => await (0, loadCheck_1.runLocalHostedLoadCheck)({
235
+ ...localLoadCheckInput,
230
236
  screenshotPath: input.screenshotPath,
231
237
  surface: input.surface,
232
238
  playtestTape: input.tape,
233
239
  postReadyWaitMs: input.postReadyWaitMs,
234
- gameServerCredential,
235
240
  });
236
241
  if (tapeSurface && playtestTape) {
237
242
  const idleScreenshotPath = buildComparisonScreenshotPath(screenshotPath, "idle");
238
243
  const tapeScreenshotPath = buildComparisonScreenshotPath(screenshotPath, "tape");
239
- const idleResult = await runCheck({
240
- screenshotPath: idleScreenshotPath,
241
- surface: tapeSurface,
242
- postReadyWaitMs: playtestTape.durationMs,
244
+ const [idleResult, tapeResult] = await (0, loadCheck_1.runLocalHostedLoadCheckSequence)({
245
+ ...localLoadCheckInput,
246
+ checks: [
247
+ {
248
+ screenshotPath: idleScreenshotPath,
249
+ surface: tapeSurface,
250
+ postReadyWaitMs: playtestTape.durationMs,
251
+ },
252
+ {
253
+ screenshotPath: tapeScreenshotPath,
254
+ surface: tapeSurface,
255
+ playtestTape,
256
+ },
257
+ ],
243
258
  });
259
+ if (!idleResult)
260
+ throw new Error("local_playtest_idle_result_missing");
244
261
  if (idleResult.status !== "PASSED") {
245
262
  (0, messages_1.printErrorWithHelp)((0, loadCheck_1.formatHostedLoadCheckFailure)(appName, idleResult, "local"), idleResult.screenshotPath ? [`Idle screenshot: ${idleResult.screenshotPath}`] : [], { command: "project check" });
246
263
  process.exitCode = 1;
247
264
  return;
248
265
  }
249
- const tapeResult = await runCheck({
250
- screenshotPath: tapeScreenshotPath,
251
- surface: tapeSurface,
252
- tape: playtestTape,
253
- });
266
+ if (!tapeResult)
267
+ throw new Error("local_playtest_tape_result_missing");
254
268
  if (tapeResult.status !== "PASSED") {
255
269
  (0, messages_1.printErrorWithHelp)((0, loadCheck_1.formatHostedLoadCheckFailure)(appName, tapeResult, "local"), tapeResult.screenshotPath ? [`Tape screenshot: ${tapeResult.screenshotPath}`] : [], { command: "project check" });
256
270
  process.exitCode = 1;
@@ -49,7 +49,7 @@ function parsePositiveInteger(raw, label, fallback) {
49
49
  }
50
50
  return parsed;
51
51
  }
52
- async function resolveCreator(client, rawCreator, command) {
52
+ async function resolveCreator(client, rawCreator, command, workspaceCreatorUsername) {
53
53
  let creator;
54
54
  try {
55
55
  creator = (0, refs_1.parseCreatorOption)(rawCreator);
@@ -64,6 +64,10 @@ async function resolveCreator(client, rawCreator, command) {
64
64
  if (creator !== "me") {
65
65
  return creator;
66
66
  }
67
+ const workspaceCreator = workspaceCreatorUsername?.trim();
68
+ if (workspaceCreator) {
69
+ return workspaceCreator;
70
+ }
67
71
  const response = await client.me();
68
72
  const username = response.user?.username?.trim();
69
73
  if (!username) {
@@ -219,13 +223,14 @@ async function browseCreations(options = {}) {
219
223
  process.exitCode = 1;
220
224
  return;
221
225
  }
222
- await (0, commandContext_1.withEnvironment)("creations browse", "Browsing your creations", async ({ client }) => {
226
+ await (0, commandContext_1.withEnvironment)("creations browse", "Browsing your creations", async ({ client, workspaceAuth }) => {
223
227
  try {
224
- const creator = await resolveCreator(client, options.creator, "creations browse");
228
+ const workspaceCreator = workspaceAuth?.config.ownerUsername ?? null;
229
+ const creator = await resolveCreator(client, options.creator, "creations browse", workspaceCreator);
225
230
  if (!creator) {
226
231
  return;
227
232
  }
228
- const usingCurrentCreator = !options.creator || options.creator.trim().toLowerCase() === "me";
233
+ const usingCurrentCreator = !workspaceCreator && (!options.creator || options.creator.trim().toLowerCase() === "me");
229
234
  const items = [];
230
235
  let pagination;
231
236
  if (kind === "app") {
@@ -1,10 +1,17 @@
1
1
  type WorkspaceArchiveStageInput = {
2
- downloadUrl: string;
2
+ downloadUrl?: string;
3
+ archivePath?: string;
3
4
  expectedSize: number;
4
5
  expectedSha256: string;
5
6
  targetDir: string;
6
7
  timeoutMs: number;
8
+ maxUncompressedBytes?: number;
7
9
  };
8
- export declare function extractZipArchive(zipBuffer: Uint8Array, targetDir: string): string[];
10
+ export declare function extractZipArchive(zipBuffer: Uint8Array, targetDir: string, maxUncompressedBytes?: number): string[];
9
11
  export declare function stageWorkspaceArchiveInChild(input: WorkspaceArchiveStageInput): Promise<void>;
12
+ export declare function stageZipArchiveBufferInChild(input: {
13
+ bytes: Uint8Array;
14
+ targetDir: string;
15
+ timeoutMs: number;
16
+ }): Promise<void>;
10
17
  export {};
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.extractZipArchive = extractZipArchive;
7
7
  exports.stageWorkspaceArchiveInChild = stageWorkspaceArchiveInChild;
8
+ exports.stageZipArchiveBufferInChild = stageZipArchiveBufferInChild;
8
9
  const node_child_process_1 = require("node:child_process");
9
10
  const node_crypto_1 = require("node:crypto");
10
11
  const node_fs_1 = require("node:fs");
@@ -17,7 +18,9 @@ const fflate_1 = require("fflate");
17
18
  const WORKSPACE_ARCHIVE_MAX_ENTRIES = 1000;
18
19
  const WORKSPACE_ARCHIVE_MAX_DOWNLOAD_BYTES = 256 * 1024 * 1024;
19
20
  const WORKSPACE_ARCHIVE_MAX_UNCOMPRESSED_BYTES = 100 * 1024 * 1024;
21
+ const ASSET_SOURCE_ARCHIVE_MAX_UNCOMPRESSED_BYTES = 256 * 1024 * 1024;
20
22
  const WORKSPACE_ARCHIVE_STAGER_MAX_OLD_SPACE_MB = 256;
23
+ const ASSET_SOURCE_ARCHIVE_STAGER_MAX_OLD_SPACE_MB = 512;
21
24
  const WORKSPACE_ARCHIVE_STAGER_EXIT_GRACE_MS = 60000;
22
25
  const ZIP_CENTRAL_DIRECTORY_SIGNATURE = 0x02014b50;
23
26
  const ZIP_END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50;
@@ -27,11 +30,15 @@ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
27
30
  function normalizeStageInput(value) {
28
31
  const input = value && typeof value === "object" && !Array.isArray(value) ? value : null;
29
32
  const downloadUrl = typeof input?.downloadUrl === "string" ? input.downloadUrl.trim() : "";
33
+ const archivePath = typeof input?.archivePath === "string" ? input.archivePath.trim() : "";
30
34
  const expectedSize = Number(input?.expectedSize);
31
35
  const expectedSha256 = typeof input?.expectedSha256 === "string" ? input.expectedSha256.trim().toLowerCase() : "";
32
36
  const targetDir = typeof input?.targetDir === "string" ? input.targetDir.trim() : "";
33
37
  const timeoutMs = Number(input?.timeoutMs);
34
- if (!downloadUrl ||
38
+ const maxUncompressedBytes = input?.maxUncompressedBytes == null
39
+ ? WORKSPACE_ARCHIVE_MAX_UNCOMPRESSED_BYTES
40
+ : Number(input.maxUncompressedBytes);
41
+ if (Boolean(downloadUrl) === Boolean(archivePath) ||
35
42
  !Number.isInteger(expectedSize) ||
36
43
  expectedSize <= 0 ||
37
44
  !/^[0-9a-f]{64}$/.test(expectedSha256) ||
@@ -44,9 +51,24 @@ function normalizeStageInput(value) {
44
51
  if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
45
52
  throw new Error("agent_task_workspace_archive_download_timeout_invalid");
46
53
  }
47
- return { downloadUrl, expectedSize, expectedSha256, targetDir, timeoutMs };
54
+ if (maxUncompressedBytes !== WORKSPACE_ARCHIVE_MAX_UNCOMPRESSED_BYTES &&
55
+ maxUncompressedBytes !== ASSET_SOURCE_ARCHIVE_MAX_UNCOMPRESSED_BYTES) {
56
+ throw new Error("agent_task_workspace_archive_uncompressed_limit_invalid");
57
+ }
58
+ if (archivePath && !node_path_1.default.isAbsolute(archivePath)) {
59
+ throw new Error("agent_task_workspace_archive_stager_input_invalid");
60
+ }
61
+ return {
62
+ ...(downloadUrl ? { downloadUrl } : {}),
63
+ ...(archivePath ? { archivePath } : {}),
64
+ expectedSize,
65
+ expectedSha256,
66
+ targetDir,
67
+ timeoutMs,
68
+ maxUncompressedBytes,
69
+ };
48
70
  }
49
- function assertWorkspaceArchiveExtractionBounds(buffer) {
71
+ function assertWorkspaceArchiveExtractionBounds(buffer, maxUncompressedBytes) {
50
72
  if (buffer.length < ZIP_END_OF_CENTRAL_DIRECTORY_MIN_BYTES) {
51
73
  throw new Error("agent_task_workspace_archive_invalid_zip");
52
74
  }
@@ -104,8 +126,8 @@ function assertWorkspaceArchiveExtractionBounds(buffer) {
104
126
  throw new Error("agent_task_workspace_archive_invalid_zip");
105
127
  }
106
128
  uncompressedBytes += uncompressedSize;
107
- if (uncompressedBytes > WORKSPACE_ARCHIVE_MAX_UNCOMPRESSED_BYTES) {
108
- throw new Error(`agent_task_workspace_archive_too_large:${uncompressedBytes}>${WORKSPACE_ARCHIVE_MAX_UNCOMPRESSED_BYTES}`);
129
+ if (uncompressedBytes > maxUncompressedBytes) {
130
+ throw new Error(`agent_task_workspace_archive_too_large:${uncompressedBytes}>${maxUncompressedBytes}`);
109
131
  }
110
132
  cursor = nextCursor;
111
133
  }
@@ -116,8 +138,8 @@ function assertWorkspaceArchiveExtractionBounds(buffer) {
116
138
  // This synchronous work is intentionally executed only by the disposable archive
117
139
  // child. A malformed ZIP can terminate that child without blocking or terminating
118
140
  // the long-lived worker process that owns presence and task heartbeats.
119
- function extractZipArchive(zipBuffer, targetDir) {
120
- assertWorkspaceArchiveExtractionBounds(Buffer.from(zipBuffer.buffer, zipBuffer.byteOffset, zipBuffer.byteLength));
141
+ function extractZipArchive(zipBuffer, targetDir, maxUncompressedBytes = WORKSPACE_ARCHIVE_MAX_UNCOMPRESSED_BYTES) {
142
+ assertWorkspaceArchiveExtractionBounds(Buffer.from(zipBuffer.buffer, zipBuffer.byteOffset, zipBuffer.byteLength), maxUncompressedBytes);
121
143
  const files = (0, fflate_1.unzipSync)(zipBuffer);
122
144
  const names = Object.keys(files);
123
145
  if (names.length === 0) {
@@ -143,8 +165,8 @@ function extractZipArchive(zipBuffer, targetDir) {
143
165
  continue;
144
166
  }
145
167
  uncompressedBytes += files[originalName].byteLength;
146
- if (uncompressedBytes > WORKSPACE_ARCHIVE_MAX_UNCOMPRESSED_BYTES) {
147
- throw new Error(`agent_task_workspace_archive_too_large:${uncompressedBytes}>${WORKSPACE_ARCHIVE_MAX_UNCOMPRESSED_BYTES}`);
168
+ if (uncompressedBytes > maxUncompressedBytes) {
169
+ throw new Error(`agent_task_workspace_archive_too_large:${uncompressedBytes}>${maxUncompressedBytes}`);
148
170
  }
149
171
  (0, node_fs_1.mkdirSync)(node_path_1.default.dirname(destination), { recursive: true });
150
172
  (0, node_fs_1.writeFileSync)(destination, Buffer.from(files[originalName]));
@@ -159,55 +181,65 @@ async function downloadAndExtractWorkspaceArchive(input) {
159
181
  const archivePath = node_path_1.default.join(stagingRoot, "archive.zip");
160
182
  const extractedDir = node_path_1.default.join(stagingRoot, "contents");
161
183
  try {
162
- const response = await fetch(input.downloadUrl, { signal: controller.signal });
163
- if (!response.ok) {
164
- throw new Error(`agent_task_workspace_archive_http_error:${response.status}`);
165
- }
166
- const contentLength = response.headers.get("content-length");
167
- if (contentLength !== null && Number(contentLength) !== input.expectedSize) {
168
- throw new Error(`agent_task_workspace_archive_size_mismatch:${contentLength}!=${input.expectedSize}`);
169
- }
170
- if (!response.body) {
171
- throw new Error("agent_task_workspace_archive_body_missing");
172
- }
173
- const reader = response.body.getReader();
174
- const file = await (0, promises_1.open)(archivePath, "wx");
175
- const hash = (0, node_crypto_1.createHash)("sha256");
176
184
  let receivedBytes = 0;
177
- try {
178
- while (true) {
179
- const chunk = await reader.read();
180
- if (chunk.done) {
181
- break;
182
- }
183
- const buffer = Buffer.from(chunk.value);
184
- hash.update(buffer);
185
- if (receivedBytes + buffer.length > input.expectedSize) {
186
- throw new Error(`agent_task_workspace_archive_size_mismatch:${receivedBytes + buffer.length}>${input.expectedSize}`);
187
- }
188
- let chunkOffset = 0;
189
- while (chunkOffset < buffer.length) {
190
- const result = await file.write(buffer, chunkOffset, buffer.length - chunkOffset, receivedBytes + chunkOffset);
191
- if (result.bytesWritten <= 0) {
192
- throw new Error("agent_task_workspace_archive_write_failed");
185
+ let receivedSha256 = "";
186
+ if (input.archivePath) {
187
+ const source = await (0, promises_1.readFile)(input.archivePath);
188
+ receivedBytes = source.length;
189
+ receivedSha256 = (0, node_crypto_1.createHash)("sha256").update(source).digest("hex");
190
+ await (0, promises_1.writeFile)(archivePath, source, { flag: "wx" });
191
+ }
192
+ else {
193
+ const response = await fetch(input.downloadUrl, { signal: controller.signal });
194
+ if (!response.ok) {
195
+ throw new Error(`agent_task_workspace_archive_http_error:${response.status}`);
196
+ }
197
+ const contentLength = response.headers.get("content-length");
198
+ if (contentLength !== null && Number(contentLength) !== input.expectedSize) {
199
+ throw new Error(`agent_task_workspace_archive_size_mismatch:${contentLength}!=${input.expectedSize}`);
200
+ }
201
+ if (!response.body) {
202
+ throw new Error("agent_task_workspace_archive_body_missing");
203
+ }
204
+ const reader = response.body.getReader();
205
+ const file = await (0, promises_1.open)(archivePath, "wx");
206
+ const hash = (0, node_crypto_1.createHash)("sha256");
207
+ try {
208
+ while (true) {
209
+ const chunk = await reader.read();
210
+ if (chunk.done) {
211
+ break;
212
+ }
213
+ const buffer = Buffer.from(chunk.value);
214
+ hash.update(buffer);
215
+ if (receivedBytes + buffer.length > input.expectedSize) {
216
+ throw new Error(`agent_task_workspace_archive_size_mismatch:${receivedBytes + buffer.length}>${input.expectedSize}`);
193
217
  }
194
- chunkOffset += result.bytesWritten;
218
+ let chunkOffset = 0;
219
+ while (chunkOffset < buffer.length) {
220
+ const result = await file.write(buffer, chunkOffset, buffer.length - chunkOffset, receivedBytes + chunkOffset);
221
+ if (result.bytesWritten <= 0) {
222
+ throw new Error("agent_task_workspace_archive_write_failed");
223
+ }
224
+ chunkOffset += result.bytesWritten;
225
+ }
226
+ receivedBytes += buffer.length;
195
227
  }
196
- receivedBytes += buffer.length;
197
228
  }
198
- }
199
- finally {
200
- await file.close();
229
+ finally {
230
+ await file.close();
231
+ }
232
+ receivedSha256 = hash.digest("hex");
201
233
  }
202
234
  if (receivedBytes !== input.expectedSize) {
203
235
  throw new Error(`agent_task_workspace_archive_size_mismatch:${receivedBytes}!=${input.expectedSize}`);
204
236
  }
205
- if (hash.digest("hex") !== input.expectedSha256) {
237
+ if (receivedSha256 !== input.expectedSha256) {
206
238
  throw new Error("agent_task_workspace_archive_sha_mismatch");
207
239
  }
208
240
  const buffer = await (0, promises_1.readFile)(archivePath);
209
241
  await (0, promises_1.mkdir)(extractedDir, { recursive: true });
210
- extractZipArchive(new Uint8Array(buffer), extractedDir);
242
+ extractZipArchive(new Uint8Array(buffer), extractedDir, input.maxUncompressedBytes);
211
243
  await (0, promises_1.mkdir)(node_path_1.default.dirname(input.targetDir), { recursive: true });
212
244
  await (0, promises_1.rm)(input.targetDir, { recursive: true, force: true });
213
245
  await (0, promises_1.rename)(extractedDir, input.targetDir);
@@ -239,9 +271,15 @@ function childFailureDetail(error) {
239
271
  }
240
272
  async function stageWorkspaceArchiveInChild(input) {
241
273
  const normalized = normalizeStageInput(input);
274
+ await runArchiveStagerProcess(normalized);
275
+ }
276
+ async function runArchiveStagerProcess(normalized) {
277
+ const maxOldSpaceMb = normalized.maxUncompressedBytes === ASSET_SOURCE_ARCHIVE_MAX_UNCOMPRESSED_BYTES
278
+ ? ASSET_SOURCE_ARCHIVE_STAGER_MAX_OLD_SPACE_MB
279
+ : WORKSPACE_ARCHIVE_STAGER_MAX_OLD_SPACE_MB;
242
280
  try {
243
281
  await execFileAsync(node_process_1.default.execPath, [
244
- `--max-old-space-size=${WORKSPACE_ARCHIVE_STAGER_MAX_OLD_SPACE_MB}`,
282
+ `--max-old-space-size=${maxOldSpaceMb}`,
245
283
  __filename,
246
284
  JSON.stringify(normalized),
247
285
  ], {
@@ -253,6 +291,29 @@ async function stageWorkspaceArchiveInChild(input) {
253
291
  throw new Error(`agent_task_workspace_archive_stager_failed:${childFailureDetail(error)}`);
254
292
  }
255
293
  }
294
+ async function stageZipArchiveBufferInChild(input) {
295
+ const bytes = Buffer.from(input.bytes.buffer, input.bytes.byteOffset, input.bytes.byteLength);
296
+ if (bytes.length <= 0 || bytes.length > WORKSPACE_ARCHIVE_MAX_DOWNLOAD_BYTES) {
297
+ throw new Error(`agent_task_workspace_archive_download_too_large:${bytes.length}>${WORKSPACE_ARCHIVE_MAX_DOWNLOAD_BYTES}`);
298
+ }
299
+ const stagingRoot = await (0, promises_1.mkdtemp)(node_path_1.default.join(node_os_1.default.tmpdir(), "playdrop-local-workspace-archive-"));
300
+ const archivePath = node_path_1.default.join(stagingRoot, "archive.zip");
301
+ try {
302
+ await (0, promises_1.writeFile)(archivePath, bytes, { flag: "wx" });
303
+ const normalized = normalizeStageInput({
304
+ archivePath,
305
+ expectedSize: bytes.length,
306
+ expectedSha256: (0, node_crypto_1.createHash)("sha256").update(bytes).digest("hex"),
307
+ targetDir: input.targetDir,
308
+ timeoutMs: input.timeoutMs,
309
+ maxUncompressedBytes: ASSET_SOURCE_ARCHIVE_MAX_UNCOMPRESSED_BYTES,
310
+ });
311
+ await runArchiveStagerProcess(normalized);
312
+ }
313
+ finally {
314
+ await (0, promises_1.rm)(stagingRoot, { recursive: true, force: true });
315
+ }
316
+ }
256
317
  async function runArchiveStagerChild() {
257
318
  const rawInput = node_process_1.default.argv[2];
258
319
  if (!rawInput) {
@@ -292,6 +292,12 @@ export declare function assertTaskUploadResultMatchesContext(input: {
292
292
  export declare function stageAssignmentWorkspace(workspaceDir: string, workspace: WorkerGameTaskAssignment["workspace"], options?: {
293
293
  timeoutMs?: number;
294
294
  }): Promise<void>;
295
+ export declare function stageReferencedAssetInputs(input: {
296
+ workspaceDir: string;
297
+ taskContext: Omit<WorkerTaskContextFile, "env" | "devPort">;
298
+ client: Pick<ApiClient, "downloadAssetSource">;
299
+ timeoutMs?: number;
300
+ }): Promise<Omit<WorkerTaskContextFile, "env" | "devPort">>;
295
301
  export declare function assertAssignmentPluginBundleExtractionBounds(buffer: Buffer): void;
296
302
  export declare function stageDevPluginWorkingTree(input: {
297
303
  workingTree: string;
@@ -38,6 +38,7 @@ exports.readTaskUploadResultFile = readTaskUploadResultFile;
38
38
  exports.readTaskNextStepsFile = readTaskNextStepsFile;
39
39
  exports.assertTaskUploadResultMatchesContext = assertTaskUploadResultMatchesContext;
40
40
  exports.stageAssignmentWorkspace = stageAssignmentWorkspace;
41
+ exports.stageReferencedAssetInputs = stageReferencedAssetInputs;
41
42
  exports.assertAssignmentPluginBundleExtractionBounds = assertAssignmentPluginBundleExtractionBounds;
42
43
  exports.stageDevPluginWorkingTree = stageDevPluginWorkingTree;
43
44
  exports.stageAssignmentPluginBundle = stageAssignmentPluginBundle;
@@ -320,6 +321,7 @@ const WORKER_CONTEXT_ALLOWED_COMMANDS = [
320
321
  ["project", "check"],
321
322
  ["project", "capture"],
322
323
  ["browse"],
324
+ ["creations", "browse"],
323
325
  ["search"],
324
326
  ["detail"],
325
327
  ["app", "source"],
@@ -1802,6 +1804,170 @@ async function stageAssignmentWorkspace(workspaceDir, workspace, options = {}) {
1802
1804
  await removeStaleWorkerControlFiles(destination);
1803
1805
  }
1804
1806
  }
1807
+ function normalizeReferenceAssetRevision(value, inputId) {
1808
+ const raw = typeof value === "string" ? value.trim() : "";
1809
+ const match = /^r?([1-9]\d*)$/i.exec(raw);
1810
+ const revision = match?.[1] ? Number.parseInt(match[1], 10) : Number.NaN;
1811
+ if (!Number.isSafeInteger(revision) || revision <= 0) {
1812
+ throw new Error(`agent_task_reference_asset_version_missing:${inputId}`);
1813
+ }
1814
+ return revision;
1815
+ }
1816
+ function resolveReferenceAssetInput(value) {
1817
+ if (!value || typeof value !== "object" || Array.isArray(value))
1818
+ return null;
1819
+ const entry = value;
1820
+ if (entry.kind !== "REFERENCE_ASSET")
1821
+ return null;
1822
+ const id = typeof entry.id === "string" ? entry.id.trim() : "";
1823
+ if (!id || !/^[a-zA-Z0-9_-]+$/.test(id)) {
1824
+ throw new Error("agent_task_reference_asset_input_id_invalid");
1825
+ }
1826
+ const referenceValue = entry.reference;
1827
+ if (!referenceValue || typeof referenceValue !== "object" || Array.isArray(referenceValue)) {
1828
+ throw new Error(`agent_task_reference_asset_invalid:${id}`);
1829
+ }
1830
+ const reference = referenceValue;
1831
+ const rawRef = typeof reference.ref === "string" ? reference.ref.trim() : "";
1832
+ const parsed = rawRef ? (0, types_1.parseContentVersionRef)(rawRef) : null;
1833
+ const legacyMatch = /^playdrop:\/\/assets\/([^/?#]+)\/([^/?#]+)$/.exec(rawRef);
1834
+ if ((parsed && parsed.kind !== "asset") || (!parsed && !legacyMatch)) {
1835
+ throw new Error(`agent_task_reference_asset_invalid:${id}`);
1836
+ }
1837
+ let creatorUsername;
1838
+ let name;
1839
+ let revision;
1840
+ if (parsed?.kind === "asset") {
1841
+ creatorUsername = parsed.creatorUsername;
1842
+ name = parsed.name;
1843
+ revision = parsed.revision;
1844
+ const declaredCreator = typeof reference.creatorUsername === "string" ? reference.creatorUsername.trim() : "";
1845
+ const declaredName = typeof reference.name === "string" ? reference.name.trim() : "";
1846
+ if ((declaredCreator && declaredCreator !== creatorUsername) || (declaredName && declaredName !== name)) {
1847
+ throw new Error(`agent_task_reference_asset_mismatch:${id}`);
1848
+ }
1849
+ if (reference.version != null && normalizeReferenceAssetRevision(reference.version, id) !== revision) {
1850
+ throw new Error(`agent_task_reference_asset_mismatch:${id}`);
1851
+ }
1852
+ }
1853
+ else {
1854
+ creatorUsername = typeof reference.creatorUsername === "string" ? reference.creatorUsername.trim() : "";
1855
+ name = typeof reference.name === "string" ? reference.name.trim() : "";
1856
+ if (!creatorUsername || !name) {
1857
+ throw new Error(`agent_task_reference_asset_invalid:${id}`);
1858
+ }
1859
+ if (legacyMatch?.[1] !== creatorUsername || legacyMatch[2] !== name) {
1860
+ throw new Error(`agent_task_reference_asset_mismatch:${id}`);
1861
+ }
1862
+ revision = normalizeReferenceAssetRevision(reference.version, id);
1863
+ }
1864
+ const ref = `asset:${creatorUsername}/${name}@r${revision}`;
1865
+ return {
1866
+ id,
1867
+ ref,
1868
+ creatorUsername,
1869
+ name,
1870
+ revision,
1871
+ workspacePath: `inputs/assets/${id}`,
1872
+ };
1873
+ }
1874
+ async function stageReferencedAssetInputs(input) {
1875
+ const playdropValue = input.taskContext.metadata.playdrop;
1876
+ if (!playdropValue || typeof playdropValue !== "object" || Array.isArray(playdropValue)) {
1877
+ throw new Error("agent_task_context_playdrop_metadata_missing");
1878
+ }
1879
+ const playdrop = playdropValue;
1880
+ const rawInputs = playdrop.inputs;
1881
+ if (rawInputs == null)
1882
+ return input.taskContext;
1883
+ if (!Array.isArray(rawInputs)) {
1884
+ throw new Error("agent_task_reference_asset_inputs_invalid");
1885
+ }
1886
+ const resolvedById = new Map();
1887
+ for (const value of rawInputs) {
1888
+ const resolved = resolveReferenceAssetInput(value);
1889
+ if (!resolved)
1890
+ continue;
1891
+ if (resolvedById.has(resolved.id)) {
1892
+ throw new Error(`agent_task_reference_asset_input_duplicate:${resolved.id}`);
1893
+ }
1894
+ resolvedById.set(resolved.id, resolved);
1895
+ try {
1896
+ const source = await input.client.downloadAssetSource(resolved.creatorUsername, resolved.name, resolved.revision);
1897
+ const bytes = new Uint8Array(await source.blob.arrayBuffer());
1898
+ if (bytes.length !== source.metadata.sizeBytes) {
1899
+ throw new Error(`source_download_incomplete:expected_${source.metadata.sizeBytes}_received_${bytes.length}`);
1900
+ }
1901
+ await (0, archive_staging_1.stageZipArchiveBufferInChild)({
1902
+ bytes,
1903
+ targetDir: resolveWorkspaceFileDestination(input.workspaceDir, resolved.workspacePath),
1904
+ timeoutMs: input.timeoutMs ?? exports.WORKSPACE_ARCHIVE_DOWNLOAD_TIMEOUT_MS,
1905
+ });
1906
+ }
1907
+ catch (error) {
1908
+ const detail = error instanceof Error ? error.message : String(error);
1909
+ throw new Error(`agent_task_reference_asset_stage_failed:${resolved.id}:${detail}`);
1910
+ }
1911
+ }
1912
+ if (resolvedById.size === 0)
1913
+ return input.taskContext;
1914
+ const localizedInputs = rawInputs.map((value) => {
1915
+ if (!value || typeof value !== "object" || Array.isArray(value))
1916
+ return value;
1917
+ const entry = value;
1918
+ const id = typeof entry.id === "string" ? entry.id.trim() : "";
1919
+ const resolved = resolvedById.get(id);
1920
+ if (!resolved)
1921
+ return value;
1922
+ const reference = entry.reference;
1923
+ return {
1924
+ ...entry,
1925
+ workspacePath: resolved.workspacePath,
1926
+ reference: {
1927
+ ...reference,
1928
+ ref: resolved.ref,
1929
+ creatorUsername: resolved.creatorUsername,
1930
+ name: resolved.name,
1931
+ version: `r${resolved.revision}`,
1932
+ },
1933
+ };
1934
+ });
1935
+ return {
1936
+ ...input.taskContext,
1937
+ metadata: {
1938
+ ...input.taskContext.metadata,
1939
+ playdrop: {
1940
+ ...playdrop,
1941
+ inputs: localizedInputs,
1942
+ },
1943
+ },
1944
+ };
1945
+ }
1946
+ function buildStagedReferenceAssetPrompt(taskContext) {
1947
+ const playdropValue = taskContext.metadata.playdrop;
1948
+ if (!playdropValue || typeof playdropValue !== "object" || Array.isArray(playdropValue))
1949
+ return null;
1950
+ const inputs = playdropValue.inputs;
1951
+ if (!Array.isArray(inputs))
1952
+ return null;
1953
+ const staged = inputs.flatMap((value) => {
1954
+ if (!value || typeof value !== "object" || Array.isArray(value))
1955
+ return [];
1956
+ const entry = value;
1957
+ if (entry.kind !== "REFERENCE_ASSET" || typeof entry.workspacePath !== "string")
1958
+ return [];
1959
+ const reference = entry.reference;
1960
+ if (!reference || typeof reference !== "object" || Array.isArray(reference))
1961
+ return [];
1962
+ const ref = reference.ref;
1963
+ if (typeof ref !== "string")
1964
+ return [];
1965
+ return [`- ${ref}: ${entry.workspacePath}`];
1966
+ });
1967
+ if (staged.length === 0)
1968
+ return null;
1969
+ return ["Attached PlayDrop assets are already available in the workspace:", ...staged].join("\n");
1970
+ }
1805
1971
  function isFile(candidatePath) {
1806
1972
  try {
1807
1973
  return (0, node_fs_1.statSync)(candidatePath).isFile();
@@ -5608,7 +5774,7 @@ async function startWorker(options = {}) {
5608
5774
  });
5609
5775
  }, HEARTBEAT_INTERVAL_MS);
5610
5776
  const runClaimedTask = async (assignment, leaseToken, devPort) => {
5611
- const taskContext = buildWorkerTaskContextV2(assignment);
5777
+ let taskContext = buildWorkerTaskContextV2(assignment);
5612
5778
  const task = {
5613
5779
  id: assignment.task.id,
5614
5780
  kind: taskContext.kind,
@@ -6165,14 +6331,6 @@ async function startWorker(options = {}) {
6165
6331
  createdAt: new Date().toISOString(),
6166
6332
  })}\n`, "utf8");
6167
6333
  }
6168
- if (!cleanRoom || assignment.gameSource) {
6169
- await stageWorkerTaskContext({
6170
- workspaceDir,
6171
- env,
6172
- devPort,
6173
- taskContext,
6174
- });
6175
- }
6176
6334
  const binDir = cleanRoom
6177
6335
  ? await createStaticGameCleanToolPath(workspaceDir, assignment.agent.runtime)
6178
6336
  : await createLocalPlaydropShim({
@@ -6231,6 +6389,27 @@ async function startWorker(options = {}) {
6231
6389
  }
6232
6390
  };
6233
6391
  await stageAssignmentWorkspace(agentWorkspaceDir, assignment.workspace);
6392
+ const referenceAssetClient = (0, apiClient_1.createCliApiClient)({
6393
+ baseUrl: envConfig.apiBase,
6394
+ token: taskContext.taskAccessToken?.trim() || ctx.getToken(),
6395
+ onBehalfCreatorUsername: taskContext.creatorUsername,
6396
+ agentTaskToken: assignment.task.token,
6397
+ agentTaskId: task.id,
6398
+ agentTaskAttempt: taskContext.attempt,
6399
+ });
6400
+ taskContext = await stageReferencedAssetInputs({
6401
+ workspaceDir: agentWorkspaceDir,
6402
+ taskContext,
6403
+ client: referenceAssetClient,
6404
+ });
6405
+ if (!cleanRoom || assignment.gameSource) {
6406
+ await stageWorkerTaskContext({
6407
+ workspaceDir,
6408
+ env,
6409
+ devPort,
6410
+ taskContext,
6411
+ });
6412
+ }
6234
6413
  if (fenced) {
6235
6414
  throw new Error("agent_task_lease_invalid");
6236
6415
  }
@@ -6348,7 +6527,8 @@ async function startWorker(options = {}) {
6348
6527
  : task.kind === "GAME_EVAL" && preparedWorkspace.resumed
6349
6528
  ? buildGameEvalRetryPrompt(assignment.request.prompt)
6350
6529
  : assignment.request.prompt;
6351
- const prompt = basePrompt;
6530
+ const stagedAssetPrompt = buildStagedReferenceAssetPrompt(taskContext);
6531
+ const prompt = stagedAssetPrompt ? `${basePrompt}\n\n${stagedAssetPrompt}` : basePrompt;
6352
6532
  const imageAttachments = assignment.request.attachments
6353
6533
  .filter((attachment) => attachment.contentType.startsWith("image/"))
6354
6534
  .map((attachment) => ({
@@ -25,6 +25,7 @@ type PlaydropBrowserContextOptions = BrowserContextOptions & {
25
25
  export declare function setPlaywrightLoader(loader: (() => Promise<PlaywrightModule>) | null): void;
26
26
  export declare function createPlaywrightLaunchError(tool: string, error: unknown): Error;
27
27
  export declare function createInstalledChromeLaunchError(tool: string, error: unknown): Error;
28
+ type BrowserCallback<T> = (browser: Browser) => Promise<T>;
28
29
  type PageCallback<T> = (context: {
29
30
  browser: Browser;
30
31
  context: BrowserContext;
@@ -33,6 +34,7 @@ type PageCallback<T> = (context: {
33
34
  export declare function withChromiumPage<T>(callback: PageCallback<T>, options?: PlaydropBrowserContextOptions): Promise<T>;
34
35
  /** Procedural publication must exercise normal browser CORS and CSP enforcement. */
35
36
  export declare function withBrowserValidationPage<T>(callback: PageCallback<T>, options?: PlaydropBrowserContextOptions): Promise<T>;
36
- export declare function withInstalledChromePage<T>(callback: PageCallback<T>, options?: PlaydropBrowserContextOptions): Promise<T>;
37
+ export declare function withInstalledChromePage<T>(callback: PageCallback<T>, options?: PlaydropBrowserContextOptions, existingBrowser?: Browser): Promise<T>;
38
+ export declare function withInstalledChromeBrowser<T>(callback: BrowserCallback<T>): Promise<T>;
37
39
  export declare function launchPersistentChromiumContext(userDataDir: string, options?: PlaydropBrowserContextOptions): Promise<BrowserContext>;
38
40
  export {};
@@ -40,6 +40,7 @@ exports.createInstalledChromeLaunchError = createInstalledChromeLaunchError;
40
40
  exports.withChromiumPage = withChromiumPage;
41
41
  exports.withBrowserValidationPage = withBrowserValidationPage;
42
42
  exports.withInstalledChromePage = withInstalledChromePage;
43
+ exports.withInstalledChromeBrowser = withInstalledChromeBrowser;
43
44
  exports.launchPersistentChromiumContext = launchPersistentChromiumContext;
44
45
  const node_url_1 = require("node:url");
45
46
  const TOOL_NAME = "playdrop";
@@ -170,7 +171,13 @@ async function withHeadlessChromiumPage(callback, options, args) {
170
171
  }
171
172
  }
172
173
  }
173
- async function withInstalledChromePage(callback, options = {}) {
174
+ async function withInstalledChromePage(callback, options = {}, existingBrowser) {
175
+ if (existingBrowser) {
176
+ return await withPageInBrowser(existingBrowser, callback, options);
177
+ }
178
+ return await withInstalledChromeBrowser(async (browser) => await withPageInBrowser(browser, callback, options));
179
+ }
180
+ async function withInstalledChromeBrowser(callback) {
174
181
  const { chromium } = await loadPlaywright();
175
182
  let browser;
176
183
  try {
@@ -184,6 +191,20 @@ async function withInstalledChromePage(callback, options = {}) {
184
191
  catch (error) {
185
192
  throw createInstalledChromeLaunchError(TOOL_NAME, error);
186
193
  }
194
+ try {
195
+ return await callback(browser);
196
+ }
197
+ finally {
198
+ try {
199
+ await browser.close();
200
+ }
201
+ catch (closeError) {
202
+ const message = closeError instanceof Error ? closeError.message : String(closeError);
203
+ console.warn(`[${TOOL_NAME}] Failed to close Playwright browser: ${message}`);
204
+ }
205
+ }
206
+ }
207
+ async function withPageInBrowser(browser, callback, options) {
187
208
  let context = null;
188
209
  try {
189
210
  const { automationOrigin, ...contextOptions } = options;
@@ -202,13 +223,6 @@ async function withInstalledChromePage(callback, options = {}) {
202
223
  console.warn(`[${TOOL_NAME}] Failed to close Playwright context: ${message}`);
203
224
  }
204
225
  }
205
- try {
206
- await browser.close();
207
- }
208
- catch (closeError) {
209
- const message = closeError instanceof Error ? closeError.message : String(closeError);
210
- console.warn(`[${TOOL_NAME}] Failed to close Playwright browser: ${message}`);
211
- }
212
226
  }
213
227
  }
214
228
  async function launchPersistentChromiumContext(userDataDir, options = {}) {
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.17.19",
2
+ "version": "0.17.21",
3
3
  "build": 1,
4
- "runtimeSdkVersion": "0.17.19",
4
+ "runtimeSdkVersion": "0.17.21",
5
5
  "runtimeSdkBuild": 1,
6
6
  "clients": {
7
7
  "all": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playdrop/playdrop-cli",
3
- "version": "0.17.19",
3
+ "version": "0.17.21",
4
4
  "description": "Official Playdrop CLI for publishing browser games, creator apps, and AI-generated game assets on playdrop.ai",
5
5
  "homepage": "https://www.playdrop.ai",
6
6
  "repository": {