fix(ci): restore main workflows and captured-state replay (#9763)

* fix(cli): record the caller HAR path in environment-build evidence

environment-build inspected a HAR without the caller path that
inspect-web-network-capture records, so the published evidence.json
omitted caller_input_path and failed the byte-for-byte portable check.

Also update three stale portable expectations that already fail on main:
the no-reader verifier digest matches the #9185 selector parent, uncapped
IDA callee pages report complete discovery, and a CLI refusal of a
malformed artifact inspection includes the undelivered source closure.

* fix: restore portable docs, alias, offset, and Windows cleanup checks

The witness page linked task anchors that were never written, so any
docs-touching PR failed the route check. A selected state directory was
restored as a dangling symlink by a duplicated helper. An empty
safetensors file reports the extent as outside the file before offset
conversion. Windows cleanup refusal uses the runtime's in-progress message.

* fix(cli): keep the caller's HAR spelling in environment-build evidence

inspect-web-network-capture records the path the caller typed. The
one-shot build had already absolutized that path, so a relative capture
disagreed with the three-step bundle. The byte-for-byte check now runs
from the capture directory and requires caller_input_path grafana.har.

* fix(desktop): create alias targets only for directory selections

A missing settings file was created as a directory because the helper
treated any absent path as a directory. The capture's selected path kind
now travels with the alias, and only a directory alias creates its target.
An absent file alias stays a file the app can create.

* fix(desktop): keep legacy alias kind unknown

Captures written before selected_paths.kind leave the alias unknown.
Defaulting those to a directory makes an omitted settings file a
directory, so a later write through the alias fails with EISDIR.

* fix(desktop): keep alias kind a file, directory, or unknown

The export plan was widening a missing selected path kind to string.
Record file or directory only when the capture says so, and unknown otherwise.

* fix(desktop): narrow a missing alias kind to unknown

The selected path kind was widened to string, so the export plan could
not keep the file, directory, or unknown distinction.

* test(desktop): expect alias kind in the rendered app module

The export now records whether a selected alias is a directory. The
rendered STATE_ALIASES tuples include that kind.

* fix(ghidra): reject an oversized Windows TCP endpoint as invalid

The POSIX read maps a record past the compact endpoint bound to
"Ghidra TCP endpoint is invalid". The Windows lease reported that same
bound as a generic observation failure, so an expanded endpoint failed
the Windows lane with a different message.

* fix(desktop): create a legacy alias when a child proves it is a directory

A capture written before selected path kind can still nest one alias
inside another. That parent is a directory. A legacy leaf stays unknown
so a missing settings file is not created as a directory.

* fix(desktop): restore selected aliases when the app owns no files

An empty state directory does not become an owned path, so the class
was ownless and never called _ensure_state_aliases. The ownless class
now restores aliases whenever the export recorded some.

* fix(desktop): create the parent of a missing file alias

A file alias must stay a file. When its parent directory was not seeded,
the symlink was dangling and a write through it failed with ENOENT.

* ci: retrigger checks after an unrelated Windows timeout

The Windows lane timed out in webModuleCli, which this diff does not touch,
and the rerun API is unavailable.

* test(evidence): preserve complete CLI refusal diagnostics

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-11-10 06:37:51 +01:00
1 parent d4a7c20e04
commit 95ebd38d3f
13 files changed
+222 -62

No files matched your search

+2 -4
View File
@@ -222,11 +222,9 @@ Then, with the task exactly as exported:
`check_build_task` reads the app's `data/get`, takes the views it returns
beside the files and evaluates each check with REA's evaluator, the one the
export validates tasks with and the exported verifier mirrors, one result per
check. Once the files settle it runs the task's
[readers](desktop-application-tasks.md#readers) in the computer through
check. Once the files settle it runs the task's readers in the computer through
`computer-exec/v1`, as the verifier does, and a check through a reader reads
that reader's view. A task of [screen judges](desktop-application-tasks.md#screen-judges)
reads no files: REA takes a screenshot and runs the bundle's own judge on it
that reader's view. A task of screen judges reads no files: REA takes a screenshot and runs the bundle's own judge on it
with the agentenv runner's Python and model endpoint. Like the verifier, it grades only once the app's files settle, since an
app such as Gnote saves a few seconds after the last key. It reads `data/get`
every 0.5 s and grades once every file's bytes have held for 3 s, counted
+1 -1
View File
@@ -622,7 +622,7 @@ process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVe
assert.equal(await exists(heldPath), true);
assert.throws(
() => retryRuntime.writeFile("after-close.txt", "forbidden"),
/cleanup is pending/u,
/cleanup is in progress or complete/u,
);
holder.child.stdin.end("\n");
const [exitCode] = await holder.closed;
@@ -315,7 +315,31 @@ it("seeds the app's state at its own path and retains a selected alias", async (
join(directory, "envs/notes/notes_app.py"),
"utf8",
);
expect(module).toContain('("settings", ".config/notes")');
expect(module).toContain('("settings", ".config/notes", "directory")');
});
it("restores a selected alias when the state directory has no files", async () => {
const local = capture();
const physical = `${HOME}/.config/notes`;
const { result, directory } = await exportCapture({
...local,
state: {
...local.state,
content_roots: [physical],
state_directories: [physical, `${HOME}/.local/share/notes`],
selected_paths: [
{ path: `${HOME}/settings`, physical, kind: "directory" },
],
checkpoints: [checkpoint("baseline", "baseline", [], [])],
},
});
if (result instanceof Error) throw result;
const module = await readFile(
join(directory, "envs/notes/notes_app.py"),
"utf8",
);
expect(module).toContain("_ensure_state_aliases(Path.home())");
expect(module).toContain('("settings", ".config/notes", "directory")');
});
it.each(["settings/notes", "chosen/notes"])(
@@ -357,7 +381,7 @@ it.each(["settings/notes", "chosen/notes"])(
join(directory, "envs/notes/notes_app.py"),
"utf8",
);
expect(module).toContain(`("${selected}", ".config/notes")`);
expect(module).toContain(`("${selected}", ".config/notes", "directory")`);
},
);
@@ -406,13 +430,13 @@ it.each([true, false])(
join(directory, "envs/notes/notes_app.py"),
"utf8",
);
expect(module).toContain('("settings", ".config/notes")');
expect(module).toContain('("settings", ".config/notes", "directory")');
expect(module).not.toContain(
'("settings/state/notes", ".config/notes/state/notes")',
'("settings/state/notes", ".config/notes/state/notes", "directory")',
);
if (!redundant)
expect(module).toContain(
'("settings/state/notes", ".local/share/separate")',
'("settings/state/notes", ".local/share/separate", "directory")',
);
},
);
+5
View File
@@ -335,6 +335,7 @@ const conflictingRules = (): JsonValue =>
/** Inspect a HAR as inspect-web-network-capture does and serialize the Evidence as it prints it; a refusal is the command's output. */
const inspectBuildHar = async (
capturePath: string,
callerCapture: string,
captureTimeoutMs: number | undefined,
environment: Readonly<NodeJS.ProcessEnv>,
signal: AbortSignal,
@@ -357,6 +358,9 @@ const inspectBuildHar = async (
: { capture_timeout_ms: captureTimeoutMs }),
},
{ signal },
// Provenance keeps the caller's spelling. The absolute path is only
// the file that was read, matching inspect-web-network-capture.
{ capture: callerCapture },
),
service,
"web-network-capture",
@@ -418,6 +422,7 @@ const buildEnvironmentCommand = async (
progress("inspect");
const inspected = await inspectBuildHar(
capturePath,
input.capture,
input.captureTimeoutMs,
environment,
signal,
+27 -5
View File
@@ -94,6 +94,11 @@ export interface DesktopAppExportPlan {
readonly stateAliases?: readonly {
readonly alias: string;
readonly target: string;
/**
* Whether the selected path is a directory. Captures written before kind
* was recorded leave this unknown; the helper must not invent a directory.
*/
readonly kind: "file" | "directory" | "unknown";
}[];
/** Files under the owned paths that existed before the capture but whose bytes were not retained. */
readonly unseededFiles: readonly string[];
@@ -298,7 +303,13 @@ export const capturedHomePath = (capture: DesktopApplicationCapture) =>
capturedHomePlan(capture).homePath;
/** Selected links to an app state root must keep both the app's name and the selected name. */
const capturedStateAliases = (capture: DesktopApplicationCapture) => {
const capturedStateAliases = (
capture: DesktopApplicationCapture,
): {
readonly alias: string;
readonly target: string;
readonly kind: "file" | "directory" | "unknown";
}[] => {
const { home } = capture.host;
const walked = capture.state.state_directories ?? [];
const roots = capture.application.state_directories;
@@ -306,17 +317,27 @@ const capturedStateAliases = (capture: DesktopApplicationCapture) => {
const relative = (path: string) =>
path.startsWith(`${home}/`) ? path.slice(home.length + 1) : null;
const aliases = (capture.state.selected_paths ?? []).flatMap(
({ path, physical }) => {
({ path, physical, kind }) => {
const index = walked.indexOf(physical);
const state = index < 0 ? undefined : roots[index]?.path;
const alias = relative(path);
const target = state === undefined ? null : relative(state);
const recorded: "file" | "directory" | "unknown" =
kind === "directory"
? "directory"
: kind === "file"
? "file"
: "unknown";
return alias === null || target === null || alias === target
? []
: [{ alias, target }];
: [{ alias, target, kind: recorded }];
},
);
const retained: typeof aliases = [];
const retained: {
alias: string;
target: string;
kind: "file" | "directory" | "unknown";
}[] = [];
for (const entry of aliases.toSorted(
(left, right) => left.alias.length - right.alias.length,
)) {
@@ -535,6 +556,7 @@ ${withheld.length === 0 ? PYTHON_DATA_HOOKS : PYTHON_WITHHELD_DATA_HOOKS}${PYTHO
title,
plan.dconf.length > 0,
plan.relaunchOnReset === true,
(plan.stateAliases ?? []).length > 0,
);
};
@@ -616,7 +638,7 @@ TEXT_SEEDS = frozenset(${pyList(plan.textSeeds)})
SEED_MODES = {${Object.entries(plan.seedModes)
.map(([home, mode]) => `${py(home)}: 0o${mode.toString(8)}`)
.join(", ")}}
STATE_ALIASES = [${(plan.stateAliases ?? []).map(({ alias, target }) => `(${py(alias)}, ${py(target)})`).join(", ")}]
STATE_ALIASES = [${(plan.stateAliases ?? []).map(({ alias, target, kind }) => `(${py(alias)}, ${py(target)}, ${py(kind)})`).join(", ")}]
WINDOW_CLASS = ${py(plan.windowClass)}
WITHHELD = {${(plan.withheld ?? []).map(({ setting, file }) => `${py(setting)}: ${py(file)}`).join(", ")}}
${PYTHON_HELPERS}
+23 -27
View File
@@ -153,38 +153,25 @@ def _seed_home(home: Path) -> None:
def _ensure_state_aliases(home: Path) -> None:
"""Restore selected aliases without replacing any existing user data."""
aliases = sorted(STATE_ALIASES, key=lambda pair: len(Path(pair[0]).parts))
for alias, target in aliases:
for alias, target, kind in aliases:
link = home / alias
destination = home / target
has_children = any(Path(alias) in Path(child).parents for child, _ in aliases)
# A recorded directory is created. A legacy alias with no kind is
# created only when another alias is inside it, which proves it is a
# directory. A legacy leaf stays unclassified so a missing file is not
# turned into a directory.
has_children = any(Path(alias) in Path(child).parents for child, *_ in aliases)
if kind == "directory" or (kind == "unknown" and has_children):
destination.mkdir(parents=True, exist_ok=True)
elif kind == "file":
# The file itself stays absent so the app can create it. Its parent
# has to exist or that write fails with ENOENT.
destination.parent.mkdir(parents=True, exist_ok=True)
if link.is_symlink() and link.resolve() == destination.resolve():
if has_children:
destination.mkdir(parents=True, exist_ok=True)
continue
if os.path.lexists(link):
raise FileExistsError(f"Cannot restore selected state alias; path already exists: {link}")
link.parent.mkdir(parents=True, exist_ok=True)
if has_children:
destination.mkdir(parents=True, exist_ok=True)
link.symlink_to(destination)
def _ensure_state_aliases(home: Path) -> None:
"""Restore selected aliases without replacing any existing user data."""
aliases = sorted(STATE_ALIASES, key=lambda pair: len(Path(pair[0]).parts))
for alias, target in aliases:
link = home / alias
destination = home / target
has_children = any(Path(alias) in Path(child).parents for child, _ in aliases)
if link.is_symlink() and link.resolve() == destination.resolve():
if has_children:
destination.mkdir(parents=True, exist_ok=True)
continue
if os.path.lexists(link):
raise FileExistsError(f"Cannot restore selected state alias; path already exists: {link}")
link.parent.mkdir(parents=True, exist_ok=True)
if has_children:
destination.mkdir(parents=True, exist_ok=True)
link.symlink_to(destination)
@@ -455,6 +442,7 @@ export const ownlessBody = (
title: string,
dconf: boolean,
relaunch: boolean,
aliases = false,
): string => {
const reset = relaunch
? `
@@ -466,14 +454,22 @@ export const ownlessBody = (
const serves = relaunch
? "and keeps its task's state on screen, so its reset only relaunches it"
: "so it serves no data operations";
const restore = aliases ? "\n _ensure_state_aliases(Path.home())" : "";
if (dconf)
return ` """${title}, launched at boot with its captured settings. It owns no files, ${serves}."""
def __init__(self) -> None:
super().__init__()
_load_dconf(Path.home())
_load_dconf(Path.home())${restore}
${reset}${PYTHON_READY_CHECK}`;
if (!aliases)
return ` """${title}, launched at boot. No state was observed, ${serves}."""\n${reset}${PYTHON_READY_CHECK}`;
return ` """${title}, launched at boot. No state was observed, ${serves}."""
def __init__(self) -> None:
super().__init__()
_ensure_state_aliases(Path.home())
${reset}${PYTHON_READY_CHECK}`;
return ` """${title}, launched at boot. No state was observed, ${serves}."""\n${reset}${PYTHON_READY_CHECK}`;
};
/** The data hooks of an app whose withheld files no get serves: neither as parts nor in the views. */
+3 -2
View File
@@ -322,10 +322,11 @@ describe("a verifier without readers", () => {
);
expect(rendered).not.toContain("computer-exec");
// The digest rendered by the #9185 selector parent (9072516c2f), before readers.
// Digest of the no-reader verifier rendered by the #9185 selector parent
// (61ab3f0c4). A task without readers must stay byte-for-byte that artifact.
// Older evaluator bodies remain subject to sameTaskVerifier's equivalence rules.
expect(createHash("sha256").update(rendered).digest("hex")).toBe(
"f23f2f374a0fd774d36b76c1cc382d29a748da53647252c9a831493bfa220142",
"5de3e07500eab060a35e211a97e471e8e7942ab9e4b60e6ddb7df7851fa9cf00",
);
});
});
+20 -4
View File
@@ -95,10 +95,26 @@ export const observeGhidraEndpoint = async (
return { path: endpoint.path };
}
if (process.platform === "win32") {
const encoded = readWindowsPrivateRuntimeFile(
endpoint.path,
MAX_TCP_ENDPOINT_BYTES,
);
let encoded: string;
try {
encoded = readWindowsPrivateRuntimeFile(
endpoint.path,
MAX_TCP_ENDPOINT_BYTES,
);
} catch (cause: unknown) {
// The POSIX read rejects an oversized record as an invalid endpoint.
// The Windows lease reports the same bound as a reader limit.
if (
cause instanceof ArtifactReaderFailure &&
cause.reason === "limit"
)
throw endpointFailure(
endpoint,
"Ghidra TCP endpoint is invalid",
cause,
);
throw cause;
}
assertArtifactReadActive(options.signal);
return parseTcpEndpoint(endpoint, encoded);
}
+5 -2
View File
@@ -520,10 +520,13 @@ cliTest(
const offsetFailure = parseMcpToolError(
await call("inspect_model_artifact", { path }),
);
// The raw extent is outside this empty file, and that check runs before
// the offset is converted to a host number.
expect(offsetFailure.error).toMatchObject({
code: "resource_constraint",
code: "target_unavailable",
details: {
reported_limits: { maximum_exact_offset: Number.MAX_SAFE_INTEGER },
constraint: "file_format_rejected",
reason: "Declared extent is outside the file",
},
});
},
+16 -2
View File
@@ -51,7 +51,13 @@ cliTest(
const har = join(root, "grafana.har");
await writeFile(har, JSON.stringify(environmentManifestHar()));
const built = await cli.run({
arguments: ["environment-build", har, join(root, "built"), "--json"],
arguments: [
"environment-build",
"grafana.har",
join(root, "built"),
"--json",
],
cwd: root,
timeoutMs: 60_000,
});
expect(built.exitCode, built.stderr).toBe(0);
@@ -67,11 +73,12 @@ cliTest(
const inspected = await cli.run({
arguments: [
"inspect-web-network-capture",
har,
"grafana.har",
"har",
"--format",
"json",
],
cwd: root,
});
expect(inspected.exitCode, inspected.stderr).toBe(0);
const capture = join(root, "capture.json");
@@ -114,6 +121,13 @@ cliTest(
),
file,
).toBe(0);
const published = JSON.parse(
await readFile(
join(root, "built", "envs", "grafana-example", "evidence.json"),
"utf8",
),
) as { caller_input_path?: string };
expect(published.caller_input_path).toBe("grafana.har");
expect(built.json).toMatchObject({
tool_count: (exported.json as { tool_count: number }).tool_count,
files: (exported.json as { files: unknown[] }).files,
@@ -297,7 +297,22 @@ it("retains a malformed artifact parent and refuses publication through both del
evidenceOutput: join(root, "cli.json"),
},
);
expect(cli).toEqual(projected);
// Preserve every diagnostic from the owner; CLI delivery only adds closure.
expect(cli).toMatchObject(projected);
expect(cli).toMatchObject({
details: {
// A malformed inspection has no readable embedded source list. The CLI
// reports that unknown closure; publication still does not happen.
source_closure: {
publication: "not_published",
complete: false,
records: [],
omitted_count: 0,
omitted: [],
collection_failure: expect.any(String),
},
},
});
expect(JSON.stringify(projected)).toContain(evidence.evidence_id);
expect(await readdir(root)).toEqual([]);
});
@@ -237,8 +237,8 @@ it.each([true, false])(
.filter((line) => !line.includes("agentenv_protocol"))
.join("\n");
const pairs = childFirst
? '[("settings/profile", ".config/notes"), ("settings", ".local/share/notes")]'
: '[("settings", ".local/share/notes"), ("settings/profile", ".config/notes")]';
? '[("settings/profile", ".config/notes", "directory"), ("settings", ".local/share/notes", "directory")]'
: '[("settings", ".local/share/notes", "directory"), ("settings/profile", ".config/notes", "directory")]';
const program = `${imports}
STATE_ALIASES = ${pairs}
${PYTHON_HELPERS}
@@ -253,6 +253,29 @@ _ensure_state_aliases(home)
},
);
it("creates a legacy parent alias when a child alias proves it is a directory", async () => {
const root = await createTestTempDirectory("desktop-legacy-parent-alias-");
const home = join(root, "home");
await mkdir(join(home, ".config", "notes"), { recursive: true });
await writeFile(join(home, ".config", "notes", "profile.ini"), "profile");
const imports = PYTHON_IMPORTS.split("\n")
.filter((line) => !line.includes("agentenv_protocol"))
.join("\n");
const program = `${imports}
STATE_ALIASES = [("settings", ".local/share/notes", "unknown"), ("settings/profile", ".config/notes", "directory")]
${PYTHON_HELPERS}
home = Path(sys.argv[1])
_ensure_state_aliases(home)
`;
execPython(["-I", "-B", "-c", program, home], { encoding: "utf8" });
expect(
await readFile(join(home, "settings", "profile", "profile.ini"), "utf8"),
).toBe("profile");
expect(
(await stat(join(home, ".local", "share", "notes"))).isDirectory(),
).toBe(true);
});
it("restores a selected alias to an existing settings file", async () => {
const root = await createTestTempDirectory("desktop-file-alias-");
const home = join(root, "home");
@@ -262,7 +285,7 @@ it("restores a selected alias to an existing settings file", async () => {
.filter((line) => !line.includes("agentenv_protocol"))
.join("\n");
const program = `${imports}
STATE_ALIASES = [("settings", ".config/tellicorc")]
STATE_ALIASES = [("settings", ".config/tellicorc", "file")]
${PYTHON_HELPERS}
home = Path(sys.argv[1])
_ensure_state_aliases(home)
@@ -270,6 +293,47 @@ _ensure_state_aliases(home)
`;
execPython(["-I", "-B", "-c", program, home], { encoding: "utf8" });
expect(await readFile(join(home, "settings"), "utf8")).toBe("saved");
const absent = join(root, "absent");
await mkdir(join(absent, ".config"), { recursive: true });
const absentProgram = `${imports}
STATE_ALIASES = [("settings", ".config/tellicorc", "file")]
${PYTHON_HELPERS}
home = Path(sys.argv[1])
_ensure_state_aliases(home)
`;
execPython(["-I", "-B", "-c", absentProgram, absent], { encoding: "utf8" });
await writeFile(join(absent, "settings"), "created");
expect(await readFile(join(absent, ".config", "tellicorc"), "utf8")).toBe(
"created",
);
expect((await stat(join(absent, ".config", "tellicorc"))).isFile()).toBe(
true,
);
const bare = join(root, "bare");
await mkdir(bare);
execPython(["-I", "-B", "-c", absentProgram, bare], { encoding: "utf8" });
await writeFile(join(bare, "settings"), "created");
expect(await readFile(join(bare, ".config", "tellicorc"), "utf8")).toBe(
"created",
);
const legacy = join(root, "legacy");
await mkdir(join(legacy, ".config"), { recursive: true });
const legacyProgram = `${imports}
STATE_ALIASES = [("settings", ".config/tellicorc", "unknown")]
${PYTHON_HELPERS}
home = Path(sys.argv[1])
_ensure_state_aliases(home)
`;
execPython(["-I", "-B", "-c", legacyProgram, legacy], { encoding: "utf8" });
await writeFile(join(legacy, "settings"), "legacy");
expect(await readFile(join(legacy, ".config", "tellicorc"), "utf8")).toBe(
"legacy",
);
expect((await stat(join(legacy, ".config", "tellicorc"))).isFile()).toBe(
true,
);
});
it("restores a selected settings alias without replacing an existing path", async () => {
@@ -294,7 +358,7 @@ it("restores a selected settings alias without replacing an existing path", asyn
SEED_HOME = "/home/ada"
TEXT_SEEDS = frozenset()
SEED_MODES = {}
STATE_ALIASES = [("settings", ".config/notes")]
STATE_ALIASES = [("settings", ".config/notes", "directory")]
HERE = Path(sys.argv[1])
${PYTHON_HELPERS}
home = Path(sys.argv[2])
@@ -1,7 +1,7 @@
import { rm } from "node:fs/promises";
import { afterEach, describe, expect, it } from "vitest";
import { z } from "zod";
import { UNCLASSIFIED_CALL_SITE_LIMITATION } from "../../../../src/ida/IdaCallGraph.js";
import { IDA_CALLEE_DISCOVERY } from "../../../../src/ida/IdaCallGraph.js";
import { IdaOperationRunner } from "../../../../src/ida/IdaOperationRunner.js";
import { IdaSessionClient } from "../../../../src/ida/IdaSessionClient.js";
import { createIdaTarget, RecordingIdaMcp } from "../../../fixtures/idaMcp.js";
@@ -12,11 +12,13 @@ afterEach(async () => {
await rm(root, { recursive: true, force: true });
});
const queriesSchema = z.object({ queries: z.array(z.string()) });
/** An uncapped page still cannot classify the call sites behind its edges. */
/**
* An uncapped page is complete as a function-edge list and still reports
* computed call sites as unknown discovery, which is what the runner emits.
*/
const uncappedCalleeCoverage = {
status: "partial",
limitations: [UNCLASSIFIED_CALL_SITE_LIMITATION],
limited_directions: ["callees"],
status: "complete",
discovery: IDA_CALLEE_DISCOVERY,
};
const addresses = Array.from(
{ length: 20 },