Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
cb50fa3
test(web): reject a performance fixture with a split session identity
cursoragent Oct 16, 2026
297e581
fix(setup): query Windows process start time without PowerShell
cursoragent Oct 16, 2026
e0d6bcd
test(antigravity): separate the first SSE event from the lifetime dea…
cursoragent Oct 16, 2026
a7c0071
test(web): compare session worktree index identity with git stage
cursoragent Oct 16, 2026
924d651
fix(provenance): identify the OpenPI checkout by realpath
cursoragent Oct 16, 2026
4cfc770
test(antigravity): move the lifetime clock after the prefetched chunk
cursoragent Oct 16, 2026
69daa74
fix: give provenance a declaration file and keep the identity fixture…
cursoragent Oct 16, 2026
2479c85
fix(setup): resolve the Windows start-time script when the query runs
cursoragent Oct 16, 2026
0d693e0
fix(setup): read cscript start times from a UTF-16 pipe
cursoragent Oct 16, 2026
e3df017
test(provenance): compare a foreign OpenPI source by realpath
cursoragent Oct 16, 2026
d00476b
merge main into the test-value audit fixes
cursoragent Oct 16, 2026
b03b1df
fix(setup): fall back to PowerShell when WMI start time is missing
cursoragent Oct 16, 2026
5bf446c
ci: retrigger checks after an unrelated UI timeout
cursoragent Oct 16, 2026
15c67cb
merge main into the test-value audit fixes
cursoragent Oct 16, 2026
ea256c6
fix(setup): read Windows process start time only from WMI
cursoragent Oct 16, 2026
fe57ee4
fix(setup): make the WMI start-time script valid JScript
cursoragent Oct 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 103 additions & 15 deletions extensions/shared/setup-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
writeFile,
} from "node:fs/promises";
import { basename, join } from "node:path";
import { fileURLToPath } from "node:url";
import { isDeepStrictEqual } from "node:util";
import { getAgentDir } from "@earendil-works/pi-coding-agent";
import {
Expand Down Expand Up @@ -1197,8 +1198,68 @@ const sameOwner = (left: LockOwner, right: LockOwner) =>
const sameFile = (left: Stats, right: Stats) =>
left.dev === right.dev && left.ino === right.ino;

const parsePowerShellProcessStartedAt = (stdout: string) => {
const startedAt = Number(stdout.trim());
// CIM datetime is local time plus a UTC offset in minutes. Truncate the
// microsecond field so the integer matches FILETIME / ToUnixTimeMilliseconds.
const CIM_PROCESS_STARTED_AT =
/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.(\d{6})([+-])(\d{3})$/;

// Redirected cscript writes UTF-16 LE, often with a BOM. Reading that
// pipe as UTF-8 leaves NULs or U+FFFD and the CIM parser rejects it, so
// a live PID stays unknown until the 5s lock expires.
export function decodeProcessStartedAtOutput(output: Buffer | string) {
const buffer = Buffer.isBuffer(output) ? output : Buffer.from(output);
const utf16 =
(buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) ||
(buffer.length >= 4 && buffer[1] === 0 && buffer[3] === 0);
const text = utf16 ? buffer.toString("utf16le") : buffer.toString("utf8");
return text
.replace(/^\uFEFF/, "")
.replaceAll("\0", "")
.trim();
}

const parseCimProcessStartedAt = (stdout: string) => {
const match = CIM_PROCESS_STARTED_AT.exec(stdout.trim());
if (!match) return undefined;
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
const hour = Number(match[4]);
const minute = Number(match[5]);
const second = Number(match[6]);
const millisecond = Number(match[7]?.slice(0, 3));
const offsetMinutes = (match[8] === "+" ? 1 : -1) * Number(match[9]);
if (
month < 1 ||
month > 12 ||
day < 1 ||
day > 31 ||
hour > 23 ||
minute > 59 ||
second > 59 ||
!Number.isInteger(offsetMinutes)
) {
return undefined;
}
const localAsUtc = Date.UTC(
year,
month - 1,
day,
hour,
minute,
second,
millisecond,
);
if (!Number.isFinite(localAsUtc)) return undefined;
const parsed = new Date(localAsUtc);
if (
parsed.getUTCFullYear() !== year ||
parsed.getUTCMonth() !== month - 1 ||
parsed.getUTCDate() !== day
) {
return undefined;
}
const startedAt = localAsUtc - offsetMinutes * 60_000;
return isPositiveInteger(startedAt) ? startedAt : undefined;
};

Expand All @@ -1212,16 +1273,18 @@ export function processStartedAtQuery(
platform = process.platform,
) {
if (platform === "win32") {
const script = [
"$ErrorActionPreference='Stop'",
`$p=Get-Process -Id ${pid}`,
"[Console]::Out.Write(([DateTimeOffset]$p.StartTime).ToUnixTimeMilliseconds())",
].join("; ");
// Resolve on use. Vitest loads this module without a file URL, so a
// top-level fileURLToPath throws during import. WMI via cscript avoids
// the PowerShell CLR cold start. The 1s query budget stays; failure is
// still unknown ownership, and the 5s lock does not recover.
const script = fileURLToPath(
new URL("./win32-process-started-at.js", import.meta.url),
);
return {
command: "powershell.exe",
args: ["-NoProfile", "-NonInteractive", "-Command", script],
command: "cscript.exe",
args: ["//Nologo", script, String(pid)],
env: undefined,
parseOutput: parsePowerShellProcessStartedAt,
parseOutput: parseCimProcessStartedAt,
};
}
return {
Expand All @@ -1232,27 +1295,52 @@ export function processStartedAtQuery(
};
}

const queryProcessStartedAt = (pid: number) =>
// One query. A timeout, non-zero exit, or unparseable value is unknown
// ownership. The 5s lock then expires without a second command.
const PROCESS_START_QUERY_TIMEOUT_MS = 1_000;

const runProcessStartedAtQuery = (
query: {
command: string;
args: string[];
env: Record<string, string> | undefined;
parseOutput: (stdout: string) => number | undefined;
},
timeout: number,
) =>
new Promise<number | undefined>((resolve) => {
const query = processStartedAtQuery(pid);
try {
execFile(
query.command,
query.args,
{
encoding: "utf8",
encoding: "buffer",
env: query.env ? { ...process.env, ...query.env } : process.env,
timeout: 1_000,
timeout,
windowsHide: true,
// cscript treats //Nologo as a host switch. Verbatim keeps Node
// from quoting it into an ordinary argument.
windowsVerbatimArguments: process.platform === "win32",
},
(error, stdout) => {
resolve(error ? undefined : query.parseOutput(stdout));
resolve(
error || stdout == null
? undefined
: query.parseOutput(decodeProcessStartedAtOutput(stdout)),
);
},
);
} catch {
resolve(undefined);
}
});

const queryProcessStartedAt = (pid: number) =>
runProcessStartedAtQuery(
processStartedAtQuery(pid),
PROCESS_START_QUERY_TIMEOUT_MS,
);

const makeLockIdentity = async (): Promise<LockIdentity> => {
const processStartedAt = await queryProcessStartedAt(process.pid);
return {
Expand Down
39 changes: 39 additions & 0 deletions extensions/shared/win32-process-started-at.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Windows Script Host JScript. Node does not execute this file.
// setup-config asks cscript.exe for one process CreationDate. A missing
// process or a failed query exits non-zero so the lock stays fail-closed.
// JScript rejects trailing commas, so keep every call list tight.
var raw =
WScript.Arguments.length === 1 ? String(WScript.Arguments.Item(0)) : "";
if (!/^[1-9][0-9]*$/.test(raw)) {
WScript.StdOut.Write("wmi-args");
WScript.Quit(1);
}
var startedAt = "";
var failure = "wmi-error";
try {
var locator = new ActiveXObject("WbemScripting.SWbemLocator");
var service = locator.ConnectServer(".", "root\\cimv2");
var processes = service.ExecQuery(
"SELECT CreationDate FROM Win32_Process WHERE ProcessId=" + raw
);
var enumerator = new Enumerator(processes);
if (enumerator.atEnd()) {
failure = "wmi-empty";
} else {
var value = enumerator.item().CreationDate;
if (value) {
startedAt = String(value);
failure = "";
}
}
} catch (error) {
failure = "wmi-error";
startedAt = "";
}
// Quit outside try. JScript turns WScript.Quit into a catchable exception,
// so a failure exit inside try/catch can fall off the end with status 0.
if (!startedAt) {
WScript.StdOut.Write(failure);
WScript.Quit(1);
}
WScript.Echo(startedAt);
22 changes: 22 additions & 0 deletions scripts/provenance.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const OPENPI_PACKAGE_NAME: "@tt-a1i/openpi";

export function normalizePath(value: string, base?: string): string;

export function collectReportedPaths(
stdout?: string,
stderr?: string,
): string[];

export function evaluateOpenPiProvenance(
reportedPaths: readonly string[],
checkoutPath?: string,
options?: {
normalize?: (value: string) => string;
packageName?: string;
readPackageName?: (directory: string) => string | undefined;
},
): {
checkoutPath: string;
openPiSources: string[];
matches: boolean;
};
138 changes: 92 additions & 46 deletions scripts/provenance.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { execFileSync, spawnSync } from "node:child_process";
import { existsSync, realpathSync } from "node:fs";
import { existsSync, readFileSync, realpathSync } from "node:fs";
import { dirname, isAbsolute, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");

function normalizePath(value, base) {
export const OPENPI_PACKAGE_NAME = "@tt-a1i/openpi";

export function normalizePath(value, base = ROOT) {
const absolute = isAbsolute(value) ? value : resolve(base, value);
try {
return realpathSync.native(absolute);
Expand All @@ -14,54 +16,98 @@ function normalizePath(value, base) {
}
}

const checkoutRevision = execFileSync("git", ["rev-parse", "HEAD"], {
cwd: ROOT,
encoding: "utf8",
}).trim();

const localPi = join(
ROOT,
"node_modules",
".bin",
process.platform === "win32" ? "pi.exe" : "pi",
);
const piCommand = existsSync(localPi)
? localPi
: (process.env.OPENPI_PI_BIN ??
(process.platform === "win32" ? "pi.exe" : "pi"));
const piResult = spawnSync(piCommand, ["list"], {
cwd: ROOT,
encoding: "utf8",
windowsHide: true,
});
if (piResult.error || piResult.status !== 0) {
throw new Error(
`pi list failed: ${piResult.error?.message ?? piResult.stderr ?? "unknown error"}`,
);
function readPackageName(directory) {
try {
const parsed = JSON.parse(
readFileSync(join(directory, "package.json"), "utf8"),
);
return typeof parsed?.name === "string" ? parsed.name : undefined;
} catch {
return undefined;
}
}

const reportedLines = [
...(piResult.stdout ?? "").split(/\r?\n/),
...(piResult.stderr ?? "").split(/\r?\n/),
].map((line) => line.trim());
const absoluteLines = reportedLines.filter((line) => isAbsolute(line));
const reportedPaths = (absoluteLines.length > 0 ? absoluteLines : reportedLines)
.map((line) => line.trim())
.filter(
export function collectReportedPaths(stdout = "", stderr = "") {
const reportedLines = [
...stdout.split(/\r?\n/),
...stderr.split(/\r?\n/),
].map((line) => line.trim());
const absoluteLines = reportedLines.filter((line) => isAbsolute(line));
return (absoluteLines.length > 0 ? absoluteLines : reportedLines).filter(
(line) =>
line.length > 0 &&
!/^(user|project) packages:$/i.test(line) &&
/[\\/]/.test(line),
)
.map((line) => normalizePath(line, ROOT));
const sources = [...new Set(reportedPaths)];
const openPiSources = sources.filter((source) => /openpi/i.test(source));
const checkoutPath = normalizePath(ROOT, ROOT);
const matches = openPiSources.length === 1 && openPiSources[0] === checkoutPath;
);
}

export function evaluateOpenPiProvenance(
reportedPaths,
checkoutPath = ROOT,
options = {},
) {
const normalize = options.normalize ?? normalizePath;
const packageName = options.packageName ?? OPENPI_PACKAGE_NAME;
const readName = options.readPackageName ?? readPackageName;
const checkout = normalize(checkoutPath);
const sources = [];
for (const reported of reportedPaths) {
const source = normalize(reported);
if (!sources.includes(source)) sources.push(source);
}
const openPiSources = sources.filter(
(source) => source === checkout || readName(source) === packageName,
);
return {
checkoutPath: checkout,
openPiSources,
matches: openPiSources.length === 1 && openPiSources[0] === checkout,
};
}

function invokedDirectly() {
const entry = process.argv[1];
if (!entry) return false;
return normalizePath(fileURLToPath(import.meta.url)) === normalizePath(entry);
}

process.stdout.write(`Checkout revision: ${checkoutRevision}\n`);
process.stdout.write(
`OpenPI source reported by pi list: ${openPiSources.join(", ") || "none"}\n`,
);
process.stdout.write(`Provenance match: ${matches ? "yes" : "no"}\n`);
if (!matches) process.exitCode = 1;
if (invokedDirectly()) {
const checkoutRevision = execFileSync("git", ["rev-parse", "HEAD"], {
cwd: ROOT,
encoding: "utf8",
}).trim();

const localPi = join(
ROOT,
"node_modules",
".bin",
process.platform === "win32" ? "pi.exe" : "pi",
);
const piCommand = existsSync(localPi)
? localPi
: (process.env.OPENPI_PI_BIN ??
(process.platform === "win32" ? "pi.exe" : "pi"));
const piResult = spawnSync(piCommand, ["list"], {
cwd: ROOT,
encoding: "utf8",
windowsHide: true,
});
if (piResult.error || piResult.status !== 0) {
throw new Error(
`pi list failed: ${piResult.error?.message ?? piResult.stderr ?? "unknown error"}`,
);
}

const provenance = evaluateOpenPiProvenance(
collectReportedPaths(piResult.stdout ?? "", piResult.stderr ?? ""),
ROOT,
);
process.stdout.write(`Checkout revision: ${checkoutRevision}\n`);
process.stdout.write(
`OpenPI source reported by pi list: ${provenance.openPiSources.join(", ") || "none"}\n`,
);
process.stdout.write(
`Provenance match: ${provenance.matches ? "yes" : "no"}\n`,
);
if (!provenance.matches) process.exitCode = 1;
}
Loading
Loading