Remote /v1/run now emits additive result_v1; Go validates run_id, digest, and paths before materializing, and only accepts missing typed payloads with an explicit legacy handshake. Co-authored-by: Cursor <cursoragent@cursor.com>
83 lines
2.5 KiB
JavaScript
83 lines
2.5 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
|
|
export const RESULT_V1_SCHEMA_VERSION = 1;
|
|
export const MAX_RESULT_MEMBERS = 32;
|
|
export const MAX_RESULT_MEMBER_BYTES = 2 * 1024 * 1024;
|
|
|
|
const RETRYABLE_CODES = new Set(["TIMEOUT", "CANCELLED", "TRANSIENT"]);
|
|
const ALLOWED_CODES = new Set([
|
|
"PROMPT_EMPTY",
|
|
"UNAUTHORIZED",
|
|
"TIMEOUT",
|
|
"CANCELLED",
|
|
"TRANSIENT",
|
|
"MISSING_TARGET",
|
|
"RUN_FAILED",
|
|
]);
|
|
|
|
export function typedErrorCode(payload) {
|
|
const raw = String(payload?.fallback_reason || payload?.error || "");
|
|
const t = raw.toLowerCase();
|
|
if (t.includes("prompt_empty")) return "PROMPT_EMPTY";
|
|
if (t.includes("unauthorized")) return "UNAUTHORIZED";
|
|
if (t.includes("timeout") || t.includes("timed out")) return "TIMEOUT";
|
|
if (t.includes("cancel")) return "CANCELLED";
|
|
if (
|
|
t.includes("connection") ||
|
|
t.includes("terminated") ||
|
|
t.includes("econnreset") ||
|
|
t.includes("socket hang")
|
|
) {
|
|
return "TRANSIENT";
|
|
}
|
|
if (t.includes("missing target")) return "MISSING_TARGET";
|
|
if (payload?.retryable) return "TRANSIENT";
|
|
if (!payload?.ok) return "RUN_FAILED";
|
|
return "";
|
|
}
|
|
|
|
export function membersDigest(members) {
|
|
const rows = (Array.isArray(members) ? members : []).map((m) => ({
|
|
kind: m.kind,
|
|
path: m.path,
|
|
digest: m.digest,
|
|
size: m.size,
|
|
}));
|
|
return createHash("sha256").update(JSON.stringify(rows)).digest("hex");
|
|
}
|
|
|
|
export function withResultV1(payload) {
|
|
const out = { ...payload };
|
|
delete out.stderr;
|
|
const patches = Array.isArray(out.patches) ? out.patches : [];
|
|
const members = [];
|
|
for (const p of patches) {
|
|
const rel = String(p?.rel_path || "");
|
|
if (!rel || rel.includes("..") || rel.startsWith("/")) continue;
|
|
const buf = Buffer.from(String(p.content || ""), "utf8");
|
|
if (buf.length > MAX_RESULT_MEMBER_BYTES) continue;
|
|
if (members.length >= MAX_RESULT_MEMBERS) break;
|
|
members.push({
|
|
kind: "file",
|
|
path: rel,
|
|
digest: createHash("sha256").update(buf).digest("hex"),
|
|
size: buf.length,
|
|
});
|
|
}
|
|
const ok = !!out.ok;
|
|
const errorCode = ok ? "" : typedErrorCode(out);
|
|
const retryable = !ok && RETRYABLE_CODES.has(errorCode) && !!out.retryable;
|
|
out.capabilities = { typed_result: true };
|
|
out.runner_protocol = "typed-v1";
|
|
out.result_v1 = {
|
|
schema_version: RESULT_V1_SCHEMA_VERSION,
|
|
run_id: String(out.run_id || "unknown").slice(0, 128),
|
|
outcome: ok ? "success" : "error",
|
|
retryable,
|
|
error_code: ALLOWED_CODES.has(errorCode) ? errorCode : ok ? "" : "RUN_FAILED",
|
|
members,
|
|
content_digest: membersDigest(members),
|
|
};
|
|
return out;
|
|
}
|