pi-cursor-sdk 0.1.56 → 0.1.57
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/CHANGELOG.md +45 -1
- package/README.md +100 -28
- package/docs/cursor-live-smoke-checklist.md +7 -7
- package/docs/cursor-model-ux-spec.md +45 -38
- package/docs/cursor-native-tool-replay.md +2 -2
- package/docs/cursor-native-tool-visual-audit.md +1 -1
- package/docs/cursor-testing-lessons.md +20 -7
- package/docs/cursor-tool-surfaces.md +13 -2
- package/docs/platform-smoke-implementation.md +220 -0
- package/docs/platform-smoke.md +183 -247
- package/package.json +38 -6
- package/platform-smoke.config.mjs +5 -1
- package/scripts/cloud-runtime-smoke.d.mts +3 -0
- package/scripts/cloud-runtime-smoke.mjs +502 -0
- package/scripts/debug-provider-events.mjs +7 -2
- package/scripts/isolated-cursor-smoke.sh +4 -6
- package/scripts/lib/cursor-child-process.d.mts +1 -0
- package/scripts/lib/cursor-child-process.mjs +137 -7
- package/scripts/lib/local-resume-smoke-harness.mjs +543 -0
- package/scripts/local-resume-cleanup-smoke.mjs +108 -0
- package/scripts/local-resume-smoke.d.mts +1 -0
- package/scripts/local-resume-smoke.mjs +642 -0
- package/scripts/platform-smoke/artifact-anchored-extract.d.mts +10 -0
- package/scripts/platform-smoke/artifact-anchored-extract.mjs +111 -0
- package/scripts/platform-smoke/artifact-bundle-chunk.mjs +57 -0
- package/scripts/platform-smoke/artifact-bundle-contract.mjs +34 -0
- package/scripts/platform-smoke/artifact-fs-safety.mjs +311 -0
- package/scripts/platform-smoke/artifact-openat-extract.c +335 -0
- package/scripts/platform-smoke/artifact-secrets.mjs +155 -0
- package/scripts/platform-smoke/artifacts.mjs +293 -65
- package/scripts/platform-smoke/card-detect.mjs +16 -4
- package/scripts/platform-smoke/crabbox-runner.mjs +45 -3
- package/scripts/platform-smoke/doctor.mjs +20 -10
- package/scripts/platform-smoke/live-suite-runner.mjs +18 -57
- package/scripts/platform-smoke/local-resume-runner.mjs +252 -0
- package/scripts/platform-smoke/local-resume-suites.d.mts +15 -0
- package/scripts/platform-smoke/local-resume-suites.mjs +104 -0
- package/scripts/platform-smoke/scenarios.mjs +16 -2
- package/scripts/platform-smoke/target-runtime.mjs +206 -0
- package/scripts/platform-smoke/targets.mjs +33 -141
- package/scripts/platform-smoke/visual-evidence.mjs +6 -7
- package/scripts/platform-smoke/wrapped-line-match.mjs +9 -0
- package/scripts/platform-smoke.mjs +40 -27
- package/scripts/refresh-cursor-model-snapshots.mjs +18 -6
- package/scripts/steering-rpc-smoke.mjs +12 -2
- package/scripts/tmux-live-smoke.sh +3 -5
- package/shared/cursor-cloud-lifecycle-constants.d.mts +3 -0
- package/shared/cursor-cloud-lifecycle-constants.mjs +7 -0
- package/shared/cursor-sensitive-text.mjs +7 -1
- package/src/context.ts +5 -2
- package/src/cursor-agents-context-registration.ts +7 -0
- package/src/cursor-agents-context.ts +3 -1
- package/src/cursor-api-key.ts +15 -1
- package/src/cursor-bridge-contract.ts +3 -0
- package/src/cursor-cloud-lifecycle.ts +733 -0
- package/src/cursor-cloud-options.ts +206 -0
- package/src/cursor-cloud-reporting.ts +246 -0
- package/src/cursor-config.ts +659 -0
- package/src/cursor-display-only-trace.ts +14 -0
- package/src/cursor-display-text.ts +8 -2
- package/src/cursor-durable-fs.ts +49 -0
- package/src/cursor-fallback-models.generated.ts +2045 -485
- package/src/cursor-live-run-accounting.ts +7 -1
- package/src/cursor-live-run-coordinator.ts +1 -0
- package/src/cursor-pi-tool-bridge-run.ts +14 -4
- package/src/cursor-provider-live-run-drain.ts +5 -0
- package/src/cursor-provider-run-finalizer.ts +36 -28
- package/src/cursor-provider-turn-finalize.ts +72 -6
- package/src/cursor-provider-turn-prepare.ts +228 -12
- package/src/cursor-provider-turn-runner.ts +41 -13
- package/src/cursor-provider-turn-send.ts +59 -16
- package/src/cursor-provider-turn-types.ts +44 -10
- package/src/cursor-runtime-state.ts +478 -0
- package/src/cursor-sdk-event-debug.ts +46 -6
- package/src/cursor-sdk-process-error-guard.ts +101 -30
- package/src/cursor-session-agent-cleanup.ts +328 -0
- package/src/cursor-session-agent-resume.ts +439 -0
- package/src/cursor-session-agent.ts +109 -13
- package/src/cursor-session-scope.ts +35 -2
- package/src/cursor-session-send-policy.ts +1 -1
- package/src/cursor-skill-tool.ts +30 -11
- package/src/cursor-state.ts +112 -69
- package/src/cursor-usage-accounting.ts +14 -4
- package/src/index.ts +11 -2
- package/src/model-discovery.ts +10 -56
|
@@ -2,13 +2,48 @@
|
|
|
2
2
|
* Artifact management — directory layout, manifest, redaction scanning, packaging.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { resolve, relative, dirname, isAbsolute } from "node:path";
|
|
8
|
+
import { gunzipSync, gzipSync } from "node:zlib";
|
|
9
|
+
import {
|
|
10
|
+
MAX_BUNDLE_AGGREGATE_BYTES, MAX_BUNDLE_FILE_BYTES, MAX_BUNDLE_FILE_COUNT,
|
|
11
|
+
MAX_BUNDLE_PATH_BYTES, MAX_BUNDLE_PATH_COMPONENTS,
|
|
12
|
+
MAX_COMPRESSED_BUNDLE_BYTES, MAX_INFLATED_BUNDLE_JSON_BYTES,
|
|
13
|
+
PLATFORM_ARTIFACT_BUNDLE_END, PLATFORM_ARTIFACT_BUNDLE_FILE_MARKER,
|
|
14
|
+
PLATFORM_ARTIFACT_BUNDLE_PATH, PLATFORM_ARTIFACT_BUNDLE_START,
|
|
15
|
+
decodeCanonicalBase64, isCanonicalPlatformBundlePath,
|
|
16
|
+
} from "./artifact-bundle-contract.mjs";
|
|
17
|
+
import {
|
|
18
|
+
boundedFileSnapshot, openRegularFileNoFollow, walkArtifactTree,
|
|
19
|
+
writeBundleSpillFile, writeExtractedFiles,
|
|
20
|
+
} from "./artifact-fs-safety.mjs";
|
|
21
|
+
import { isBinaryArtifactContent, redactSecrets, scanForSecrets } from "./artifact-secrets.mjs";
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
MAX_BUNDLE_AGGREGATE_BYTES, MAX_BUNDLE_FILE_BYTES, MAX_BUNDLE_FILE_COUNT,
|
|
25
|
+
MAX_BUNDLE_PATH_BYTES, MAX_BUNDLE_PATH_COMPONENTS,
|
|
26
|
+
MAX_COMPRESSED_BUNDLE_BYTES, MAX_INFLATED_BUNDLE_JSON_BYTES,
|
|
27
|
+
PLATFORM_ARTIFACT_BUNDLE_END, PLATFORM_ARTIFACT_BUNDLE_FILE_MARKER,
|
|
28
|
+
PLATFORM_ARTIFACT_BUNDLE_PATH, PLATFORM_ARTIFACT_BUNDLE_START,
|
|
29
|
+
isCanonicalPlatformBundlePath,
|
|
30
|
+
};
|
|
31
|
+
export { openRegularFileNoFollow };
|
|
32
|
+
export { isBinaryArtifactContent, redactSecrets, scanForSecrets };
|
|
7
33
|
|
|
8
34
|
const PLATFORM_SMOKE_RUN_DIR_PATTERN = /^run-(\d+)-[a-z0-9]+$/i;
|
|
9
35
|
const HOURS_TO_MS = 60 * 60 * 1000;
|
|
10
36
|
const DAYS_TO_MS = 24 * HOURS_TO_MS;
|
|
11
37
|
const LATEST_INDEX_NAME = "latest.json";
|
|
38
|
+
const INLINE_BUNDLE_MAX_BYTES = 48 * 1024;
|
|
39
|
+
const PLATFORM_ARTIFACT_TEXT_EXTENSIONS = new Set([
|
|
40
|
+
"ansi", "html", "js", "json", "jsonl", "log", "md", "mjs", "ts", "txt", "yaml", "yml",
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
function isTransportableTextPath(path) {
|
|
44
|
+
const extension = path.toLowerCase().match(/\.([^.\/]+)$/)?.[1];
|
|
45
|
+
return extension !== undefined && PLATFORM_ARTIFACT_TEXT_EXTENSIONS.has(extension);
|
|
46
|
+
}
|
|
12
47
|
|
|
13
48
|
function finiteNonNegativeNumber(value) {
|
|
14
49
|
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
@@ -113,62 +148,44 @@ export function writeManifest(dir, expectedFiles) {
|
|
|
113
148
|
return manifest;
|
|
114
149
|
}
|
|
115
150
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
[/(bearer\s+)[A-Za-z0-9\-._~+/]{20,}=*/gi, "bearer token", "$1[REDACTED_BEARER_TOKEN]"],
|
|
119
|
-
[/connect\.sid=[A-Za-z0-9%]+/gi, "session cookie", "connect.sid=[REDACTED_SESSION_COOKIE]"],
|
|
120
|
-
[/https?:\/\/[^/\s]*\/cursor-pi-tool-bridge\/[A-Za-z0-9_.:-]+\/mcp/gi, "bridge endpoint URL", "[REDACTED_BRIDGE_ENDPOINT_URL]"],
|
|
121
|
-
[/"(apiKey|accessToken|refreshToken|session|cookie)"\s*:\s*"[^"\s]{12,}"/gi, "auth/token JSON field", '"$1":"[REDACTED_SECRET]"'],
|
|
122
|
-
];
|
|
123
|
-
|
|
124
|
-
/** Redact known secret material before writing logs/artifacts. */
|
|
125
|
-
export function redactSecrets(text) {
|
|
126
|
-
let redacted = String(text ?? "");
|
|
127
|
-
const cursorKey = process.env.CURSOR_API_KEY;
|
|
128
|
-
if (cursorKey && cursorKey.length > 10) {
|
|
129
|
-
redacted = redacted.split(cursorKey).join("[REDACTED_CURSOR_API_KEY]");
|
|
130
|
-
}
|
|
131
|
-
for (const [pattern, , replacement] of SECRET_PATTERNS) {
|
|
132
|
-
redacted = redacted.replace(pattern, replacement);
|
|
133
|
-
}
|
|
134
|
-
return redacted;
|
|
151
|
+
function artifactRelativePath(root, path) {
|
|
152
|
+
return relative(root, path).replace(/\\/g, "/");
|
|
135
153
|
}
|
|
136
154
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
for (const [pattern, label] of SECRET_PATTERNS) {
|
|
145
|
-
pattern.lastIndex = 0;
|
|
146
|
-
if (pattern.test(String(text ?? ""))) violations.push(`potential ${label}`);
|
|
147
|
-
}
|
|
148
|
-
return violations;
|
|
155
|
+
function safeArtifactPath(path) {
|
|
156
|
+
return redactSecrets(path || ".");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isSensitiveTransportPath(root, path) {
|
|
160
|
+
return artifactRelativePath(root, path).split("/").some((name) =>
|
|
161
|
+
/^\.env/i.test(name) || /^auth\.json$/i.test(name) || /^(?:id_rsa|id_ed25519|.*\.pem|.*\.key)$/i.test(name));
|
|
149
162
|
}
|
|
150
163
|
|
|
151
|
-
/**
|
|
164
|
+
/** Binary-safe scan of every bounded artifact file except non-artifact infrastructure. */
|
|
152
165
|
export function scanArtifacts(dir) {
|
|
153
166
|
const findings = [];
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (!["txt", "json", "jsonl", "md", "log", "ansi", "html", "yml", "yaml", "js", "mjs", "ts"].includes(ext)) continue;
|
|
161
|
-
try {
|
|
162
|
-
const content = readFileSync(fp, "utf-8");
|
|
163
|
-
const violations = scanForSecrets(content);
|
|
164
|
-
for (const v of violations) {
|
|
165
|
-
findings.push({ file: relative(dir, fp), violation: v });
|
|
166
|
-
}
|
|
167
|
-
} catch { /* binary or unreadable */ }
|
|
168
|
-
}
|
|
167
|
+
const failures = [];
|
|
168
|
+
function relativeDetails(root, path) {
|
|
169
|
+
const relativePath = artifactRelativePath(root, path) || ".";
|
|
170
|
+
const file = safeArtifactPath(relativePath);
|
|
171
|
+
for (const violation of scanForSecrets(relativePath)) findings.push({ file, violation });
|
|
172
|
+
return { relativePath, file };
|
|
169
173
|
}
|
|
170
|
-
|
|
171
|
-
|
|
174
|
+
walkArtifactTree(dir, {
|
|
175
|
+
directoryRead: (path, _parents, root) => failures.push({ file: relativeDetails(root, path).file, violation: "artifact scan directory-read" }),
|
|
176
|
+
fileRead: (path, _parents, root) => failures.push({ file: relativeDetails(root, path).file, violation: "artifact scan file-read" }),
|
|
177
|
+
nonRegular: (path, _parents, root) => failures.push({ file: relativeDetails(root, path).file, violation: "artifact scan non-regular-entry" }),
|
|
178
|
+
file: (path, parents, root) => {
|
|
179
|
+
const { file } = relativeDetails(root, path);
|
|
180
|
+
const snapshot = boundedFileSnapshot(path, MAX_BUNDLE_FILE_BYTES, parents);
|
|
181
|
+
if (!snapshot.ok) {
|
|
182
|
+
failures.push({ file, violation: `artifact scan ${snapshot.reason}` });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
for (const violation of scanForSecrets(snapshot.content)) findings.push({ file, violation });
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
return failures.length > 0 ? failures : findings;
|
|
172
189
|
}
|
|
173
190
|
|
|
174
191
|
/** Write summary.json for a suite. */
|
|
@@ -180,13 +197,233 @@ export function writeSummary(dir, data) {
|
|
|
180
197
|
}
|
|
181
198
|
|
|
182
199
|
function readJsonFile(path) {
|
|
200
|
+
const snapshot = boundedFileSnapshot(path);
|
|
201
|
+
if (!snapshot.ok) return undefined;
|
|
183
202
|
try {
|
|
184
|
-
return JSON.parse(
|
|
203
|
+
return JSON.parse(snapshot.content.toString("utf8"));
|
|
185
204
|
} catch {
|
|
186
205
|
return undefined;
|
|
187
206
|
}
|
|
188
207
|
}
|
|
189
208
|
|
|
209
|
+
function shouldTransportBundleFile(root, path, pathPrefix) {
|
|
210
|
+
if (isSensitiveTransportPath(root, path) || !isTransportableTextPath(path)) return false;
|
|
211
|
+
if (pathPrefix) return true;
|
|
212
|
+
const rel = relative(root, path).replace(/\\/g, "/");
|
|
213
|
+
return /^(?:artifacts\/(?:terminal\.(?:ansi|txt)|session\.jsonl|live-status\.json|pi-command\.json|bridge-diagnostics\.jsonl|abort-started\.txt)|logs\/(?:process-|leftover-process-check)[^/]*|cursor-sdk-events\/.*\/(?:session|metadata)\.json)$/i.test(rel);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function singleFilePlatformArtifactBundle(pathPrefix, name, value) {
|
|
217
|
+
const content = `${JSON.stringify(value, null, 2)}\n`;
|
|
218
|
+
return {
|
|
219
|
+
files: [{
|
|
220
|
+
path: [pathPrefix || "artifacts", name].join("/"),
|
|
221
|
+
contentBase64: Buffer.from(content).toString("base64"),
|
|
222
|
+
size: Buffer.byteLength(content),
|
|
223
|
+
}],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function bundleLimitArtifact(pathPrefix, reasons) {
|
|
228
|
+
return singleFilePlatformArtifactBundle(pathPrefix, "bundle-limit-exceeded.json", {
|
|
229
|
+
status: "failed",
|
|
230
|
+
reasons: [...new Set(reasons)],
|
|
231
|
+
limits: {
|
|
232
|
+
maxFileBytes: MAX_BUNDLE_FILE_BYTES,
|
|
233
|
+
maxFileCount: MAX_BUNDLE_FILE_COUNT,
|
|
234
|
+
maxAggregateBytes: MAX_BUNDLE_AGGREGATE_BYTES,
|
|
235
|
+
maxPathBytes: MAX_BUNDLE_PATH_BYTES,
|
|
236
|
+
maxPathComponents: MAX_BUNDLE_PATH_COMPONENTS,
|
|
237
|
+
maxInflatedJsonBytes: MAX_INFLATED_BUNDLE_JSON_BYTES,
|
|
238
|
+
maxCompressedBytes: MAX_COMPRESSED_BUNDLE_BYTES,
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function buildPlatformArtifactBundle(root, pathPrefix = "") {
|
|
244
|
+
const prefixViolations = scanForSecrets(pathPrefix);
|
|
245
|
+
if (prefixViolations.length > 0) {
|
|
246
|
+
return singleFilePlatformArtifactBundle("artifacts", "bundle-redaction-violations.json",
|
|
247
|
+
prefixViolations.map((violation) => ({ file: "[artifact-path-prefix]", violation })));
|
|
248
|
+
}
|
|
249
|
+
const files = [];
|
|
250
|
+
const findings = [];
|
|
251
|
+
const limitReasons = [];
|
|
252
|
+
let aggregateBytes = 0;
|
|
253
|
+
walkArtifactTree(root, {
|
|
254
|
+
directoryRead: () => limitReasons.push("directory-read"),
|
|
255
|
+
fileRead: () => limitReasons.push("file-read"),
|
|
256
|
+
nonRegular: () => limitReasons.push("non-regular-entry"),
|
|
257
|
+
file: (path, parents, canonicalRoot) => {
|
|
258
|
+
const rel = artifactRelativePath(canonicalRoot, path);
|
|
259
|
+
const file = safeArtifactPath(rel);
|
|
260
|
+
const pathViolations = scanForSecrets(rel);
|
|
261
|
+
for (const violation of pathViolations) findings.push({ file, violation });
|
|
262
|
+
const snapshot = boundedFileSnapshot(path, MAX_BUNDLE_FILE_BYTES, parents);
|
|
263
|
+
if (!snapshot.ok) {
|
|
264
|
+
limitReasons.push(snapshot.reason);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const { content } = snapshot;
|
|
268
|
+
for (const violation of scanForSecrets(content)) findings.push({ file, violation });
|
|
269
|
+
if (pathViolations.length > 0 || !shouldTransportBundleFile(canonicalRoot, path, pathPrefix)) return;
|
|
270
|
+
if (isBinaryArtifactContent(content)) {
|
|
271
|
+
limitReasons.push("binary-content");
|
|
272
|
+
} else if (files.length >= MAX_BUNDLE_FILE_COUNT) {
|
|
273
|
+
limitReasons.push("file-count");
|
|
274
|
+
} else if (aggregateBytes + content.length > MAX_BUNDLE_AGGREGATE_BYTES) {
|
|
275
|
+
limitReasons.push("aggregate-bytes");
|
|
276
|
+
} else {
|
|
277
|
+
files.push({ path: [pathPrefix, rel].filter(Boolean).join("/"), contentBase64: content.toString("base64"), size: content.length });
|
|
278
|
+
aggregateBytes += content.length;
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
if (limitReasons.length > 0) return bundleLimitArtifact(pathPrefix, limitReasons);
|
|
283
|
+
if (findings.length > 0) {
|
|
284
|
+
return singleFilePlatformArtifactBundle(pathPrefix, "bundle-redaction-violations.json", findings);
|
|
285
|
+
}
|
|
286
|
+
return { files };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function validatePlatformArtifactBundle(bundle) {
|
|
290
|
+
if (!bundle || typeof bundle !== "object" || Array.isArray(bundle) ||
|
|
291
|
+
Object.keys(bundle).length !== 1 || !Object.hasOwn(bundle, "files") ||
|
|
292
|
+
!Array.isArray(bundle.files) || bundle.files.length > MAX_BUNDLE_FILE_COUNT) return undefined;
|
|
293
|
+
const files = [];
|
|
294
|
+
const paths = new Set();
|
|
295
|
+
let aggregateBytes = 0;
|
|
296
|
+
let pathComponents = 0;
|
|
297
|
+
for (const file of bundle.files) {
|
|
298
|
+
if (!file || typeof file !== "object" || Array.isArray(file) ||
|
|
299
|
+
Object.keys(file).length !== 3 || !Object.hasOwn(file, "path") ||
|
|
300
|
+
!Object.hasOwn(file, "contentBase64") || !Object.hasOwn(file, "size") ||
|
|
301
|
+
!isCanonicalPlatformBundlePath(file.path) || scanForSecrets(file.path).length > 0 ||
|
|
302
|
+
!isTransportableTextPath(file.path) || !Number.isSafeInteger(file.size) || file.size < 0 || file.size > MAX_BUNDLE_FILE_BYTES) return undefined;
|
|
303
|
+
const comparisonPath = process.platform === "win32" ? file.path.toLowerCase() : file.path;
|
|
304
|
+
if (paths.has(comparisonPath)) return undefined;
|
|
305
|
+
paths.add(comparisonPath);
|
|
306
|
+
pathComponents += file.path.split("/").length;
|
|
307
|
+
if (pathComponents > MAX_BUNDLE_PATH_COMPONENTS) return undefined;
|
|
308
|
+
aggregateBytes += file.size;
|
|
309
|
+
if (aggregateBytes > MAX_BUNDLE_AGGREGATE_BYTES) return undefined;
|
|
310
|
+
const content = decodeCanonicalBase64(file.contentBase64, file.size);
|
|
311
|
+
if (!content || content.length !== file.size || isBinaryArtifactContent(content) || scanForSecrets(content).length > 0) return undefined;
|
|
312
|
+
files.push({ path: file.path, content });
|
|
313
|
+
}
|
|
314
|
+
for (const path of paths) {
|
|
315
|
+
const segments = path.split("/");
|
|
316
|
+
for (let index = 1; index < segments.length; index++) {
|
|
317
|
+
if (paths.has(segments.slice(0, index).join("/"))) return undefined;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return files;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function compressPlatformArtifactBundle(bundle) {
|
|
324
|
+
if (!validatePlatformArtifactBundle(bundle)) throw new Error("platform artifact bundle exceeds decoded limits");
|
|
325
|
+
const serialized = Buffer.from(JSON.stringify(bundle));
|
|
326
|
+
if (serialized.length > MAX_INFLATED_BUNDLE_JSON_BYTES) throw new Error("platform artifact bundle exceeds inflated JSON limit");
|
|
327
|
+
const compressed = gzipSync(serialized, { level: 9 });
|
|
328
|
+
if (compressed.length > MAX_COMPRESSED_BUNDLE_BYTES) throw new Error("platform artifact bundle exceeds compressed limit");
|
|
329
|
+
return compressed;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function platformArtifactBundleEnvelope(compressed) {
|
|
333
|
+
return {
|
|
334
|
+
encoding: "gzip-base64",
|
|
335
|
+
size: compressed.length,
|
|
336
|
+
sha256: createHash("sha256").update(compressed).digest("hex"),
|
|
337
|
+
contentBase64: compressed.toString("base64"),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function formatCompressedPlatformArtifactBundle(compressed) {
|
|
342
|
+
return `${PLATFORM_ARTIFACT_BUNDLE_START}\n${JSON.stringify(platformArtifactBundleEnvelope(compressed))}\n${PLATFORM_ARTIFACT_BUNDLE_END}\n`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function formatPlatformArtifactBundle(bundle) {
|
|
346
|
+
return formatCompressedPlatformArtifactBundle(compressPlatformArtifactBundle(bundle));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function writePlatformArtifactBundle(root, pathPrefix = "") {
|
|
350
|
+
rmSync(PLATFORM_ARTIFACT_BUNDLE_PATH, { force: true });
|
|
351
|
+
const validPrefix = pathPrefix === "" || (isCanonicalPlatformBundlePath(pathPrefix) && scanForSecrets(pathPrefix).length === 0);
|
|
352
|
+
let bundle = validPrefix ? buildPlatformArtifactBundle(root, pathPrefix) : undefined;
|
|
353
|
+
let compressed;
|
|
354
|
+
if (!bundle || !validatePlatformArtifactBundle(bundle)) {
|
|
355
|
+
bundle = bundleLimitArtifact("artifacts", [validPrefix ? "invalid-bundle" : "invalid-path-prefix"]);
|
|
356
|
+
compressed = compressPlatformArtifactBundle(bundle);
|
|
357
|
+
} else {
|
|
358
|
+
try {
|
|
359
|
+
compressed = compressPlatformArtifactBundle(bundle);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
bundle = bundleLimitArtifact(pathPrefix, [error instanceof Error ? error.message : "bundle-encoding"]);
|
|
362
|
+
compressed = compressPlatformArtifactBundle(bundle);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const inline = formatCompressedPlatformArtifactBundle(compressed);
|
|
366
|
+
if (Buffer.byteLength(inline) <= INLINE_BUNDLE_MAX_BYTES) {
|
|
367
|
+
process.stdout.write(inline);
|
|
368
|
+
return bundle;
|
|
369
|
+
}
|
|
370
|
+
writeBundleSpillFile(PLATFORM_ARTIFACT_BUNDLE_PATH, compressed);
|
|
371
|
+
process.stdout.write(`${PLATFORM_ARTIFACT_BUNDLE_FILE_MARKER}${JSON.stringify({
|
|
372
|
+
path: PLATFORM_ARTIFACT_BUNDLE_PATH,
|
|
373
|
+
...platformArtifactBundleEnvelope(compressed),
|
|
374
|
+
encoding: "gzip",
|
|
375
|
+
contentBase64: undefined,
|
|
376
|
+
})}\n`);
|
|
377
|
+
return bundle;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function isSafePlatformBundlePath(outputDir, bundlePath) {
|
|
381
|
+
if (!isCanonicalPlatformBundlePath(bundlePath)) return false;
|
|
382
|
+
const outPath = resolve(outputDir, bundlePath);
|
|
383
|
+
const rel = relative(outputDir, outPath).replace(/\\/g, "/");
|
|
384
|
+
return rel === bundlePath && !isAbsolute(rel);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function extractPlatformArtifactBundle(outputDir, stdout) {
|
|
388
|
+
const failed = { ok: false, violations: [] };
|
|
389
|
+
const start = stdout.indexOf(PLATFORM_ARTIFACT_BUNDLE_START);
|
|
390
|
+
const end = stdout.indexOf(PLATFORM_ARTIFACT_BUNDLE_END, start);
|
|
391
|
+
if (start === -1 || end === -1) return failed;
|
|
392
|
+
let bundle;
|
|
393
|
+
const payload = stdout.slice(start + PLATFORM_ARTIFACT_BUNDLE_START.length, end).trim();
|
|
394
|
+
if (Buffer.byteLength(payload) > MAX_INFLATED_BUNDLE_JSON_BYTES) return failed;
|
|
395
|
+
try {
|
|
396
|
+
const parsed = JSON.parse(payload);
|
|
397
|
+
if (parsed?.encoding !== "gzip-base64" || typeof parsed.contentBase64 !== "string" ||
|
|
398
|
+
!Number.isSafeInteger(parsed.size) || parsed.size < 1 || parsed.size > MAX_COMPRESSED_BUNDLE_BYTES ||
|
|
399
|
+
typeof parsed.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(parsed.sha256)) return failed;
|
|
400
|
+
const compressed = decodeCanonicalBase64(parsed.contentBase64, parsed.size);
|
|
401
|
+
if (!compressed || compressed.length !== parsed.size || createHash("sha256").update(compressed).digest("hex") !== parsed.sha256) return failed;
|
|
402
|
+
bundle = JSON.parse(gunzipSync(compressed, { maxOutputLength: MAX_INFLATED_BUNDLE_JSON_BYTES }).toString("utf8"));
|
|
403
|
+
} catch { return failed; }
|
|
404
|
+
const decodedFiles = validatePlatformArtifactBundle(bundle);
|
|
405
|
+
if (!decodedFiles || decodedFiles.some((file) => !isSafePlatformBundlePath(outputDir, file.path))) return failed;
|
|
406
|
+
|
|
407
|
+
const files = [];
|
|
408
|
+
const violations = [];
|
|
409
|
+
for (const file of decodedFiles) {
|
|
410
|
+
const text = file.content.toString("utf8");
|
|
411
|
+
violations.push(...scanForSecrets(file.content).map((violation) => ({ file: file.path, violation })));
|
|
412
|
+
if (file.path.endsWith("redaction-violations.json")) {
|
|
413
|
+
try {
|
|
414
|
+
const parsed = JSON.parse(text);
|
|
415
|
+
if (Array.isArray(parsed)) violations.push(...parsed.filter((item) => typeof item?.violation === "string").map((item) => ({
|
|
416
|
+
file: typeof item.file === "string" ? item.file : file.path, violation: item.violation,
|
|
417
|
+
})));
|
|
418
|
+
} catch {}
|
|
419
|
+
}
|
|
420
|
+
files.push({ path: file.path, content: Buffer.from(redactSecrets(text)) });
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const succeeded = writeExtractedFiles(outputDir, files);
|
|
424
|
+
return succeeded ? { ok: true, violations } : failed;
|
|
425
|
+
}
|
|
426
|
+
|
|
190
427
|
function collectFiles(root) {
|
|
191
428
|
const files = [];
|
|
192
429
|
function walk(dir) {
|
|
@@ -227,6 +464,7 @@ function providerDebugPathFields(debugRoot) {
|
|
|
227
464
|
function pathFields(suiteDir) {
|
|
228
465
|
const artifactsDir = resolve(suiteDir, "artifacts");
|
|
229
466
|
const debugRoot = resolve(suiteDir, "cursor-sdk-events");
|
|
467
|
+
const localResumeEvidenceRoot = resolve(suiteDir, "local-resume-evidence");
|
|
230
468
|
const paths = {
|
|
231
469
|
artifactManifest: existingPath(resolve(suiteDir, "artifact-manifest.json")),
|
|
232
470
|
summary: existingPath(resolve(suiteDir, "summary.json")),
|
|
@@ -238,6 +476,9 @@ function pathFields(suiteDir) {
|
|
|
238
476
|
visualEvidence: existingPath(resolve(artifactsDir, "visual-evidence.json")),
|
|
239
477
|
sessionJsonl: existingPath(resolve(artifactsDir, "session.jsonl")),
|
|
240
478
|
jsonlToolResults: existingPath(resolve(artifactsDir, "jsonl-tool-results.json")),
|
|
479
|
+
localResumeEvidence: existingPath(resolve(suiteDir, "local-resume-evidence.json")),
|
|
480
|
+
localResumeEvidenceRoot: existingPath(localResumeEvidenceRoot),
|
|
481
|
+
localResumeRuntimeLaunches: existingPath(resolve(localResumeEvidenceRoot, "runtime-launches.jsonl")),
|
|
241
482
|
...providerDebugPathFields(debugRoot),
|
|
242
483
|
};
|
|
243
484
|
for (const [key, value] of Object.entries(paths)) {
|
|
@@ -332,16 +573,3 @@ export function writeCommand(dir, cmd) {
|
|
|
332
573
|
export function writeExitCode(dir, code, signal) {
|
|
333
574
|
writeFileSync(resolve(dir, "exit-code.txt"), `code=${code}\nsignal=${signal ?? "none"}\n`);
|
|
334
575
|
}
|
|
335
|
-
|
|
336
|
-
/** Package a directory as tar.gz (posix) or zip (powershell). */
|
|
337
|
-
export async function packageArtifacts(dir, archivePath) {
|
|
338
|
-
const { execSync } = await import("node:child_process");
|
|
339
|
-
const dirName = basename(dir);
|
|
340
|
-
const parentDir = resolve(dir, "..");
|
|
341
|
-
if (archivePath.endsWith(".tar.gz")) {
|
|
342
|
-
execSync(`tar -czf "${archivePath}" -C "${parentDir}" "${dirName}"`, { stdio: "pipe" });
|
|
343
|
-
} else if (archivePath.endsWith(".zip")) {
|
|
344
|
-
execSync(`cd "${parentDir}" && zip -r "${archivePath}" "${dirName}"`, { stdio: "pipe" });
|
|
345
|
-
}
|
|
346
|
-
return archivePath;
|
|
347
|
-
}
|
|
@@ -8,9 +8,14 @@
|
|
|
8
8
|
|
|
9
9
|
import { writeFileSync, mkdirSync } from "node:fs";
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
|
+
import { matchesWrappedLineAt } from "./wrapped-line-match.mjs";
|
|
11
12
|
|
|
12
13
|
const CARD_PATTERNS = [
|
|
13
|
-
{
|
|
14
|
+
{
|
|
15
|
+
id: "read",
|
|
16
|
+
pattern: /^\s*read\s+(?:(?:\.\/)?package\.json|.*[\\/]package\.json)\s*$/i,
|
|
17
|
+
wrappedPattern: /^\s*read\s+.*[\\/]package\.(?:json|js\s+on|j\s*son)\s*$/i,
|
|
18
|
+
},
|
|
14
19
|
{ id: "grep", pattern: /^\s*grep \/pi-cursor-sdk\/ in\s+(?:(?:\S+[\\/])?README\.md)\s*$/i },
|
|
15
20
|
{ id: "find", pattern: /^\s*find README\.md in\s+\S+/i },
|
|
16
21
|
{ id: "list", pattern: /^\s*(?:find \* in src|find src\/\* in \.|Get-ChildItem -Name \.\/src)\s*/i },
|
|
@@ -18,7 +23,11 @@ const CARD_PATTERNS = [
|
|
|
18
23
|
{ id: "write", pattern: /^\s*\+.*beta\s*$/i },
|
|
19
24
|
{ id: "edit-diff", pattern: /^\s*\+.*gamma\s*$/i },
|
|
20
25
|
{ id: "shell-failure", pattern: /^\s*(?:native shell failure|Command exited with code 7)\s*$/i },
|
|
21
|
-
{
|
|
26
|
+
{
|
|
27
|
+
id: "bridge-read-success",
|
|
28
|
+
pattern: /^\s*read\s+(?:\.\/package\.json|.*[\\/]package\.j(?:son|s))\s*$/i,
|
|
29
|
+
wrappedPattern: /^\s*read\s+.*[\\/]package\.(?:json|js\s+on|j\s*son)\s*$/i,
|
|
30
|
+
},
|
|
22
31
|
{ id: "bridge-read-failure", pattern: /^\s*(?:read \.\/definitely-missing-platform-smoke-file\.txt|ENOENT: no such file)\s*/i },
|
|
23
32
|
{ id: "bridge-shell-success", pattern: /^\s*bridge visual smoke\s*$/i },
|
|
24
33
|
{ id: "footer-status", pattern: /\bcomposer-2-5\b|\bcomposer-2\.5\b/i },
|
|
@@ -31,6 +40,10 @@ function cleanLine(line) {
|
|
|
31
40
|
.replace(/\r/g, "");
|
|
32
41
|
}
|
|
33
42
|
|
|
43
|
+
function matchesCardAt(lines, index, card) {
|
|
44
|
+
return matchesWrappedLineAt(lines, index, card.pattern, card.wrappedPattern);
|
|
45
|
+
}
|
|
46
|
+
|
|
34
47
|
/**
|
|
35
48
|
* Detect stable rendered evidence regions in terminal text.
|
|
36
49
|
*
|
|
@@ -44,8 +57,7 @@ export function detectCards(txtContent) {
|
|
|
44
57
|
for (let i = 0; i < lines.length; i++) {
|
|
45
58
|
for (const card of CARD_PATTERNS) {
|
|
46
59
|
if (seen.has(card.id)) continue;
|
|
47
|
-
card
|
|
48
|
-
if (!card.pattern.test(lines[i])) continue;
|
|
60
|
+
if (!matchesCardAt(lines, i, card)) continue;
|
|
49
61
|
seen.add(card.id);
|
|
50
62
|
cards.push({
|
|
51
63
|
id: card.id,
|
|
@@ -5,12 +5,30 @@
|
|
|
5
5
|
* Never prints CURSOR_API_KEY.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { spawn } from "node:child_process";
|
|
8
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
9
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
9
10
|
|
|
10
11
|
const CRABBOX_BIN = process.env.PLATFORM_SMOKE_CRABBOX || "crabbox";
|
|
11
12
|
|
|
12
13
|
function env(name) { return process.env[name] ?? ""; }
|
|
13
14
|
|
|
15
|
+
export function ensureUbuntuContainerImage(config = {}) {
|
|
16
|
+
const override = env("PLATFORM_SMOKE_UBUNTU_IMAGE");
|
|
17
|
+
if (override) return override;
|
|
18
|
+
const image = config.ubuntuContainerImage;
|
|
19
|
+
const baseImage = config.ubuntuContainerBaseImage;
|
|
20
|
+
if (!image || !baseImage) throw new Error("platform smoke config requires ubuntuContainerImage and ubuntuContainerBaseImage");
|
|
21
|
+
const childEnv = { ...process.env };
|
|
22
|
+
delete childEnv.CURSOR_API_KEY;
|
|
23
|
+
execFileSync("docker", ["build", "--quiet", "--tag", image, "-"], {
|
|
24
|
+
env: childEnv,
|
|
25
|
+
input: `FROM ${baseImage}\nUSER root\n`,
|
|
26
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
27
|
+
timeout: 300_000,
|
|
28
|
+
});
|
|
29
|
+
return image;
|
|
30
|
+
}
|
|
31
|
+
|
|
14
32
|
function buildCrabboxEnv(opts = {}) {
|
|
15
33
|
const env = { ...process.env, CRABBOX_SYNC_GIT_SEED: "false", ...opts.env };
|
|
16
34
|
const allowed = new Set(opts.allowEnv ?? []);
|
|
@@ -90,7 +108,8 @@ export function buildTargetBaseArgs(targetName, config = {}) {
|
|
|
90
108
|
];
|
|
91
109
|
}
|
|
92
110
|
case "ubuntu": {
|
|
93
|
-
const image = env("PLATFORM_SMOKE_UBUNTU_IMAGE") || config.ubuntuContainerImage
|
|
111
|
+
const image = env("PLATFORM_SMOKE_UBUNTU_IMAGE") || config.ubuntuContainerImage;
|
|
112
|
+
if (!image) throw new Error("platform smoke config requires ubuntuContainerImage");
|
|
94
113
|
return [
|
|
95
114
|
"--provider", "local-container",
|
|
96
115
|
"--target", "linux",
|
|
@@ -144,6 +163,21 @@ function parseLeaseId(output) {
|
|
|
144
163
|
* The lease will be kept until explicitly stopped.
|
|
145
164
|
*/
|
|
146
165
|
export async function warmupLease(targetName, slug, config = {}) {
|
|
166
|
+
if (targetName === "ubuntu") {
|
|
167
|
+
try {
|
|
168
|
+
const image = ensureUbuntuContainerImage(config);
|
|
169
|
+
console.log(` prepared Ubuntu container image ${image}`);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
stdout: "",
|
|
174
|
+
stderr: `failed to prepare Ubuntu container image: ${error instanceof Error ? error.message : String(error)}`,
|
|
175
|
+
code: 1,
|
|
176
|
+
signal: null,
|
|
177
|
+
leaseId: leaseIdFor(targetName),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
147
181
|
const fullArgs = ["warmup", ...buildTargetBaseArgs(targetName, config), "--slug", slug, "--keep"];
|
|
148
182
|
if (targetName === "macos") fullArgs.push("--reclaim");
|
|
149
183
|
console.log(` [crabbox] ${fullArgs.join(" ")}`);
|
|
@@ -174,17 +208,24 @@ export async function runOnLease(targetName, leaseId, command, opts = {}) {
|
|
|
174
208
|
} else if (opts.freshSync !== false) {
|
|
175
209
|
args.push("--fresh-sync");
|
|
176
210
|
}
|
|
211
|
+
if (opts.captureStdoutPath) args.push("--capture-stdout", opts.captureStdoutPath);
|
|
177
212
|
if (opts.shell) {
|
|
178
213
|
args.push("--shell", command);
|
|
179
214
|
} else {
|
|
180
215
|
args.push("--", ...(Array.isArray(command) ? command : command.split(" ")));
|
|
181
216
|
}
|
|
182
217
|
console.log(` [crabbox] run ${args.slice(1, 6).join(" ")} ...`);
|
|
183
|
-
|
|
218
|
+
const result = await execCrabbox(args, {
|
|
184
219
|
timeout: opts.timeout ?? 600_000,
|
|
185
220
|
env: opts.env,
|
|
186
221
|
allowEnv: opts.allowEnv,
|
|
187
222
|
});
|
|
223
|
+
if (!opts.captureStdoutPath || !existsSync(opts.captureStdoutPath)) return result;
|
|
224
|
+
try {
|
|
225
|
+
return { ...result, stdout: readFileSync(opts.captureStdoutPath, "utf8") };
|
|
226
|
+
} finally {
|
|
227
|
+
rmSync(opts.captureStdoutPath, { force: true });
|
|
228
|
+
}
|
|
188
229
|
}
|
|
189
230
|
|
|
190
231
|
/**
|
|
@@ -201,6 +242,7 @@ export async function stopLease(targetName, leaseId, config = {}) {
|
|
|
201
242
|
* Crabbox syncs the checkout, executes, and releases the lease.
|
|
202
243
|
*/
|
|
203
244
|
export async function runOneShot(targetName, command, opts = {}) {
|
|
245
|
+
if (targetName === "ubuntu") ensureUbuntuContainerImage(opts.config);
|
|
204
246
|
const args = ["run", ...buildTargetBaseArgs(targetName, opts.config)];
|
|
205
247
|
if (opts.shell) {
|
|
206
248
|
args.push("--shell", command);
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { execSync, execFileSync } from "node:child_process";
|
|
10
10
|
import { accessSync, constants, existsSync, mkdirSync, writeFileSync, unlinkSync, rmSync, statSync } from "node:fs";
|
|
11
11
|
import { dirname, resolve } from "node:path";
|
|
12
|
+
import { ensureUbuntuContainerImage } from "./crabbox-runner.mjs";
|
|
12
13
|
import { renderAll } from "./render-ansi.mjs";
|
|
13
14
|
|
|
14
15
|
let failures = 0;
|
|
@@ -259,17 +260,25 @@ async function runChecks(config) {
|
|
|
259
260
|
} else {
|
|
260
261
|
fail("crabbox providers failed");
|
|
261
262
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
263
|
+
let ubuntuImage;
|
|
264
|
+
try {
|
|
265
|
+
ubuntuImage = ensureUbuntuContainerImage(config);
|
|
266
|
+
ok(`Ubuntu container image ready: ${ubuntuImage}`);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
fail(`could not prepare Ubuntu container image: ${error instanceof Error ? error.message : String(error)}`);
|
|
269
|
+
}
|
|
270
|
+
if (ubuntuImage) {
|
|
271
|
+
const lcDoc = silent(cbox, ["doctor", "--provider", "local-container", "--target", "linux", "--local-container-image", ubuntuImage, "--json"]);
|
|
272
|
+
if (lcDoc) {
|
|
273
|
+
try {
|
|
274
|
+
const d = JSON.parse(lcDoc);
|
|
275
|
+
d.ok ? ok("local-container provider OK") : fail(`local-container: ${d.error ?? "not ok"}`);
|
|
276
|
+
} catch {
|
|
277
|
+
fail("could not parse crabbox doctor --json for local-container");
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
fail("crabbox doctor --provider local-container --json failed");
|
|
270
281
|
}
|
|
271
|
-
} else {
|
|
272
|
-
fail("crabbox doctor --provider local-container --json failed");
|
|
273
282
|
}
|
|
274
283
|
const sshHost = env("PLATFORM_SMOKE_MAC_HOST") || "localhost";
|
|
275
284
|
const sshUser = env("PLATFORM_SMOKE_MAC_USER") || env("USER");
|
|
@@ -414,6 +423,7 @@ async function runChecks(config) {
|
|
|
414
423
|
["git", "git --version"],
|
|
415
424
|
["rsync", "rsync --version"],
|
|
416
425
|
["tar", "tar --version"],
|
|
426
|
+
...(process.platform === "win32" ? [] : [["C compiler", "cc --version"]]),
|
|
417
427
|
]) {
|
|
418
428
|
const out = shell(command);
|
|
419
429
|
out ? ok(`${name}: ${out.split("\n")[0]}`) : fail(`${name} not found`);
|