@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.
- package/config/client-meta.json +2 -2
- package/dist/apps/loadCheck.d.ts +14 -4
- package/dist/apps/loadCheck.js +173 -126
- package/dist/commands/check.js +28 -14
- package/dist/commands/creations.js +9 -4
- package/dist/commands/worker/archive-staging.d.ts +9 -2
- package/dist/commands/worker/archive-staging.js +108 -47
- package/dist/commands/worker.d.ts +6 -0
- package/dist/commands/worker.js +190 -10
- package/dist/playwright.d.ts +3 -1
- package/dist/playwright.js +22 -8
- package/node_modules/@playdrop/config/client-meta.json +2 -2
- package/package.json +1 -1
package/config/client-meta.json
CHANGED
package/dist/apps/loadCheck.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
74
|
-
|
|
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;
|
package/dist/apps/loadCheck.js
CHANGED
|
@@ -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
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
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
|
-
|
|
1059
|
-
|
|
1060
|
-
const
|
|
1061
|
-
const
|
|
1062
|
-
if (!
|
|
1063
|
-
return buildFailedLoadCheckResult("
|
|
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
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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 (
|
|
1071
|
-
return
|
|
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: "
|
|
1112
|
+
devAuth: "player",
|
|
1113
|
+
player: "1",
|
|
1141
1114
|
launchCheck: true,
|
|
1142
1115
|
localDevPort,
|
|
1143
1116
|
}),
|
|
1144
1117
|
timeoutMs: input.timeoutMs,
|
|
1145
|
-
expectedHostedLaunchState:
|
|
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
|
|
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
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
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 });
|
package/dist/commands/check.js
CHANGED
|
@@ -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
|
-
|
|
97
|
-
if (!
|
|
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: ${
|
|
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
|
|
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
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
250
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 >
|
|
108
|
-
throw new Error(`agent_task_workspace_archive_too_large:${uncompressedBytes}>${
|
|
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 >
|
|
147
|
-
throw new Error(`agent_task_workspace_archive_too_large:${uncompressedBytes}>${
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
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
|
-
|
|
200
|
-
|
|
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 (
|
|
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=${
|
|
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;
|
package/dist/commands/worker.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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) => ({
|
package/dist/playwright.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/playwright.js
CHANGED
|
@@ -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 = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playdrop/playdrop-cli",
|
|
3
|
-
"version": "0.17.
|
|
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": {
|