Files
PaperClipAI/tests/runner-e2e/native-completion-git-source.mjs
DottaandPaperclip dd868ed125 fix(runner): share native completion tool guidance (#14961)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Native Runner agents report completion through finish and block
tools.
> - The providers receive different descriptions for those tools.
> - Completion guidance belongs with the tools that enforce the result.
> - This pull request shares the descriptions and refreshes retained
catalogs.
> - A separate native suite checks completion and blocking on production
defaults.
> - Legacy agents retain their separate skill and API paths.

## Linked Issues or Issue Description

Refs: #14920, #14948, #14985.

**Current behavior**

Native Codex and MCP bridges describe finish and block differently.
Retained provider sessions can keep old descriptions.

**Proposed behavior**

Native providers receive the same finish and block descriptions. The
descriptions cover report selection, validation feedback, returned
outcomes, approval gates and final-answer timing. Retained native
sessions refresh from v13 to v14.

**Reason and benefit**

Put the completion procedure next to its native tool. Preserve stock
base instructions, schemas, permissions and terminal semantics. This PR
now stands alone on master. It contains no reduced manual, shared prompt
or operational-skill changes from #14948.

## What Changed

- Add canonical native finish and block descriptions. Use them in direct
Codex and both native MCP bridges.
- Advance the native tool contract to v14. Cover old-v13 refresh without
replacing task identity or prior history.
- Check authenticated tool catalogs, provider start/resume frames and
serialized daemon catalogs.
- Add an independent, explicit-only native completion suite. Preserve
the original assigned-skill durable-document journey. Pair it with a
concrete whole-task blocker across Codex, ACPX Claude and OpenCode.
- Verify the actual public production default bundle and budgets before
execution. Require independent durable disposition, native
result/terminal receipts and observable provider-final ordering.
- Correct the blocker browser oracle to accept the requested
explanation. Keep exact owner/action/scope checks. Calibrate positive,
missing and contradictory replies.
- Preserve only actual `tool_call` terminal names (`paperclip_finish` /
`paperclip_block`) in the native compatibility run-log projection.
Require the same named call ID through its finishing result; retain all
other redaction boundaries.
- Admit verified hosted shallow checkout/build hydration and bind the
selected runnerd to exact source/archive/binary provenance. Hosted cells
truthfully reuse the existing trusted build; local admission executes
Rust calibration. Forward only public source/run identifiers through
both launcher preflight subprocess paths.
- Enforce single attempts in the launcher for opted-in fixtures. Keep
ordinary retry policy unchanged. Run exact-source, credential-free
admission before credential loading.

## Verification

- Frozen candidate: `d6e59e4712a3158ab4cd7d58deff1389b4578c21`, based on
master `59c07ede72dc08b8aba149a01cc11e0b7a204621`; historical
descriptions: `e74ed61a69fbdd8b3a8f15dd6456bc3140246e33`. Exactly the
five original native production files and six unit tests differ. Both
carry identical corrected fixtures, strict named finishing-call grader,
closed compatibility carrier and admission. Defaults,
profiles/models/auth/permissions and manifest bytes match.
- Actual launcher `prepareNativeCompletionPreflight` →
`verifyNativeCompletionPreflight` admission passes on both exact refs
with zero providers: candidate 132 / historical 127 selected TypeScript
assertions, 128 Node calibrations and one Rust normalization calibration
each; E2E typecheck, manifest checks, selected binary provenance and
six-cell discovery pass. Each has 257 explicitly skipped unrelated
assertions, not coverage. The credential-free environment calibration
exercises both real prepare/verify subprocess options with public hosted
identifiers and rejects credential/ambient overrides. Complete actual
launcher prepare→verify also passes on both frozen refs with explicitly
synthetic hosted metadata/verified archives, separately labeled as
calibration rather than a trusted GitHub run. Exact framed provenance
parsing and mock source identity are calibrated without relaxing the
real verifier.
- [Complete matched qualification
report](https://github.com/paperclipai/paperclip/blob/532066620b88e8731a5211fbe1cbc48ce8c7dd1a/doc/plans/2026-10-02-native-completion-master-qualification.md),
[immutable
manifest](https://github.com/paperclipai/paperclip/blob/532066620b88e8731a5211fbe1cbc48ce8c7dd1a/doc/plans/2026-10-02-native-completion-calibrated-manifest.json)
and [closed retained
audit/hashes](https://github.com/paperclipai/paperclip/blob/532066620b88e8731a5211fbe1cbc48ce8c7dd1a/doc/plans/2026-10-02-native-completion-calibrated-results/comparison.json)
are inspectable. All six candidate cells pass; historical descriptions
pass five. Paired outcomes: **zero new failures, one new pass (Codex
blocker), five unchanged passes, zero pending pairs**. [Candidate
campaign](https://github.com/paperclipai/paperclip/actions/runs/37098728980)
and [historical
campaign](https://github.com/paperclipai/paperclip/actions/runs/37098815696)
each execute six original attempt-1 native runs, with no campaign retry
and successful cleanup. Their trusted workflow revision is
`215586d127e97c9301d86e769a39a15c13298ca2`, separate from measured
source. [Candidate public
HTML](https://d1p6rlowie26tp.cloudfront.net/runner-e2e/campaigns/gha-37098728980-1/index.html)
and [historical public
HTML](https://d1p6rlowie26tp.cloudfront.net/runner-e2e/campaigns/gha-37098815696-1/index.html)
retain declared screenshots.
- Independent candidate evidence agrees with all original grades: 51
strict native checks, 12 served-default/budget checks and 21 original
skill/document checks pass. The historical Codex blocker saves the
correct whole-task blocker but omits the required marker from its actual
provider final and identical saved reply. This is not semantic-summary
fallback. Its original browser/matcher failure stays retained; the
additional native snapshot/grade and workspace before/after digest were
never written and are not fabricated by the separate API/PRP audit.
Historical Codex completion has one failed finish followed by success
within the same native run; the public receipt records no failure
reason. All twelve runs and their usage remain counted. Reported
model-cost subtotals are $0.00421482 historical/$0.00437391 candidate;
Codex/Claude zero entries have unknown billing type, actual invoices are
unverified and hosted execution cost is unmetered. One matched trial
supports no extra failure within these six cases, not broad statistical
or coding-quality equivalence.
- Initial hosted `e18c2cf9` / `459455ac` and subsequent `0a9c5a7` /
`00a761b` cohorts each stopped before providers in all twelve cells. The
latter failed a mocked-receipt unit test under ambient hosted metadata;
all source/build proofs passed. [All twelve later setup
receipts](https://github.com/paperclipai/paperclip/blob/402ee94c52273ad58de355ae9a7d562dd22f8101/doc/plans/2026-10-02-native-completion-qualified-hosted-setup.json)
are retained. [Exact failed setup
receipts](https://github.com/paperclipai/paperclip/blob/27653eb1a8f8ce839776d760f4563f672e5a706c/doc/plans/2026-10-02-native-completion-master-hosted-setup.json)
and the original manifest remain intact. Local sandbox-denied loopback
and stale anchor-expectation attempts are retained separately; unchanged
appropriate assertions were corrected/admitted before paid dispatch. Old
anonymous OpenCode streams are not assigned inferred tool names or
retroactively passed.
- Full provider-free E2E support previously passed 927 tests in 67
files. Exact-head d6 normal CI run `37098409915`, attempt 1 passes full
repository typecheck/build/tests, Runner Rust/static checks, all browser
shards/aggregate and canary: 52 check-runs pass, four intentional skips,
Snyk passes. Fresh Greptile check `111132956342` is 5/5 with zero
unresolved threads. Source-specific deterministic tests do not
substitute for the bounded live comparison.
- Earlier native source `9138f570c341c251a5727c32d6615ce238bc8e03` is
archived. Its [complete reduced-manual-context
report](https://github.com/paperclipai/paperclip/blob/9138f570c341c251a5727c32d6615ce238bc8e03/doc/plans/2026-10-02-native-completion-live-comparison.md)
remains intact, including original failures, grader limits and
provider-free replay. It is not current-master-context qualification.

## Risks

Changed tool text can change model behavior. The completed six-pair
qualification shows no extra failing outcomes in this bounded trial;
other tasks and repeated-run variance remain unmeasured. Observable
final ordering does not prove provider feedback consumption. Public
evidence can fail closed if a provider does not expose the required
result sequence. This slice does not remove native fixed prompts or
measure general coding quality. No database, schema, permission or
legacy completion changes occur.

## Model Used

OpenAI Codex, GPT-6 family, with code inspection, execution and tool
use. The exact deployment ID and context-window size are not exposed in
this session. They are unavailable rather than inferred from the model
menu.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-03 06:20:38 -05:00

167 lines
12 KiB
JavaScript

import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { closeSync, constants, fstatSync, lstatSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, posix } from "node:path";
const hydrationDirectory = "runner-e2e-build";
const archiveName = "runner-e2e-build-bundle.tar.gz";
const checksumName = `${archiveName}.sha256`;
const hydrationPaths = [archiveName, checksumName].map(file => `${hydrationDirectory}/${file}`);
const sha256 = bytes => createHash("sha256").update(bytes).digest("hex");
/** A shallow commit still carries its hash-bound immediate parent in raw bytes. */
export function verifyNativeCompletionParentAnchor({ sha, rawCommit, anchor }) {
if (!/^[a-f0-9]{40}$/.test(sha ?? "") || !/^[a-f0-9]{40}$/.test(anchor ?? "")) return false;
const bytes = Buffer.isBuffer(rawCommit) ? rawCommit : Buffer.from(rawCommit ?? "");
const observed = createHash("sha1").update(`commit ${bytes.length}\0`).update(bytes).digest("hex");
const parents = bytes.toString("utf8").split("\n\n", 1)[0].split("\n").filter(line => line.startsWith("parent "));
return observed === sha && parents.length === 1 && parents[0] === `parent ${anchor}`;
}
function regularFile(path) {
const stat = lstatSync(path);
if (!stat.isFile() || stat.nlink !== 1) throw new Error("entry is not a regular, independently downloaded file");
return stat;
}
function archiveDigest(path) {
const before = regularFile(path), fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
try {
const opened = fstatSync(fd);
if (!opened.isFile() || opened.nlink !== 1 || opened.ino !== before.ino || opened.dev !== before.dev)
throw new Error("archive changed during verification");
const hash = createHash("sha256"), buffer = Buffer.alloc(1024 * 1024);
let count;
while ((count = readSync(fd, buffer, 0, buffer.length, null)) > 0) hash.update(buffer.subarray(0, count));
const after = fstatSync(fd);
if (after.size !== opened.size || after.mtimeMs !== opened.mtimeMs) throw new Error("archive changed during verification");
return hash.digest("hex");
} finally { closeSync(fd); }
}
export function inspectNativeCompletionBuildHydration(repositoryRoot) {
const directory = join(repositoryRoot, hydrationDirectory), errors = [];
let stat;
try { stat = lstatSync(directory); }
catch (error) {
if (error.code === "ENOENT") return { present: false, verified: false, archiveSha256: null, checksumSha256: null, errors };
return { present: true, verified: false, archiveSha256: null, checksumSha256: null, errors: ["build hydration directory is unreadable"] };
}
let archiveSha256 = null, checksumSha256 = null;
try {
if (!stat.isDirectory()) throw new Error("directory must be a real directory, not a symlink or file");
const entries = readdirSync(directory).sort();
if (JSON.stringify(entries) !== JSON.stringify([archiveName, checksumName].sort()))
throw new Error("directory must contain exactly the archive and its checksum, with no extra entries");
const checksumPath = join(directory, checksumName), checksumStat = regularFile(checksumPath);
if (checksumStat.size > 128) throw new Error("checksum record is malformed");
const checksum = readFileSync(checksumPath), match = /^([a-f0-9]{64}) {2}runner-e2e-build-bundle\.tar\.gz\n?$/.exec(checksum.toString("utf8"));
if (!match) throw new Error("checksum must be one exact SHA256 record for the downloaded archive");
checksumSha256 = sha256(checksum); archiveSha256 = archiveDigest(join(directory, archiveName));
if (archiveSha256 !== match[1]) throw new Error("archive checksum verification failed");
} catch (error) { errors.push(`build hydration ${error.message}`); }
return { present: true, verified: errors.length === 0, archiveSha256, checksumSha256, errors };
}
export const NATIVE_COMPLETION_RUNNERD_PATH = "packages/paperclip-runner/runner/target/debug/paperclip-runnerd";
/** Mirror the production default selector, and reject staged binaries that would take precedence. */
export function inspectNativeCompletionRunnerd({ repositoryRoot, sourceSha, sourceFingerprint, environment }) {
const errors = [], hosted = environment.GITHUB_ACTIONS === "true";
const selectedPath = `${NATIVE_COMPLETION_RUNNERD_PATH}${process.platform === "win32" ? ".exe" : ""}`;
const staged = join(repositoryRoot, `packages/paperclip-runner/dist/bin/paperclip-runnerd${process.platform === "win32" ? ".exe" : ""}`);
let binarySha256 = null, binaryBytes = null, archiveSha256 = null, archiveMemberBytes = null;
try {
try { lstatSync(staged); throw new Error("staged dist/bin runnerd would override the admitted debug executable"); }
catch (error) { if (error.code !== "ENOENT") throw error; }
let directory = repositoryRoot;
for (const part of selectedPath.split("/").slice(0, -1)) {
directory = join(directory, part);
if (!lstatSync(directory).isDirectory()) throw new Error("selected runnerd ancestor must be a real directory");
}
const binary = join(repositoryRoot, selectedPath), stat = regularFile(binary);
if (process.platform !== "win32" && !(stat.mode & 0o111)) throw new Error("selected runnerd is not executable");
binaryBytes = stat.size;
binarySha256 = archiveDigest(binary);
if (hosted) {
if (environment.PAPERCLIP_RUNNER_E2E_SOURCE_SHA !== sourceSha || !/^[a-f0-9]{40}$/.test(sourceSha ?? ""))
throw new Error("trusted hosted target SHA does not match admitted Git HEAD");
if (![environment.GITHUB_RUN_ID, environment.GITHUB_RUN_ATTEMPT].every(value => typeof value === "string" && value === value.trim() && /^[1-9]\d*$/.test(value)))
throw new Error("trusted hosted workflow run and attempt are unavailable");
const hydration = inspectNativeCompletionBuildHydration(repositoryRoot);
if (!hydration.verified) throw new Error(`hosted build archive is unverified: ${hydration.errors.join("; ")}`);
archiveSha256 = hydration.archiveSha256;
const archive = join(repositoryRoot, hydrationDirectory, archiveName);
const tar = args => spawnSync("tar", args, { encoding: "utf8", timeout: 120_000, maxBuffer: 16 * 1024 * 1024 });
const names = tar(["-tzf", archive]);
if (names.status !== 0) throw new Error("hosted build archive cannot be listed");
const matching = names.stdout.split(/\r?\n/).filter(name => name && posix.normalize(name.replace(/^\/+/, "")) === selectedPath);
if (matching.length !== 1 || matching[0] !== selectedPath)
throw new Error("hosted archive must contain exactly one canonical selected runnerd member");
const details = tar(["-tvzf", archive, selectedPath]);
const rows = details.stdout.trim().split(/\r?\n/);
if (details.status !== 0 || rows.length !== 1 || !rows[0].startsWith("-") || !rows[0].endsWith(` ${selectedPath}`))
throw new Error("hosted selected runnerd member must be a regular file");
// Keep daemon bytes out of JavaScript memory; the selected executable supplies the exact expected size.
const temporary = mkdtempSync(join(tmpdir(), "native-completion-runnerd-member-")), member = join(temporary, "runnerd");
let fd;
try {
fd = openSync(member, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o600);
const extracted = spawnSync("tar", ["-xOzf", archive, selectedPath], { timeout: 120_000, stdio: ["ignore", fd, "pipe"] });
archiveMemberBytes = fstatSync(fd).size;
closeSync(fd); fd = undefined;
if (extracted.status !== 0 || archiveMemberBytes !== binaryBytes || archiveDigest(member) !== binarySha256)
throw new Error("selected runnerd byte count or hash differs from the verified hosted archive member");
} finally {
if (fd !== undefined) closeSync(fd);
rmSync(temporary, { recursive: true, force: true });
}
if (archiveDigest(archive) !== archiveSha256) throw new Error("hosted build archive changed during runnerd verification");
}
} catch (error) { errors.push(`runnerd provenance ${error.message}`); }
return { schema: "paperclip.native-completion-runnerd-provenance.v1", mode: hosted ? "trusted_hosted_archive" : "fresh_local_build",
passed: errors.length === 0, selectedPath, binarySha256, binaryBytes, archiveMemberBytes, sourceSha, sourceFingerprint, archiveSha256,
workflowRunId: hosted ? environment.GITHUB_RUN_ID ?? null : null,
workflowRunAttempt: hosted ? environment.GITHUB_RUN_ATTEMPT ?? null : null,
artifactName: hosted ? `runner-e2e-build-${sourceSha}-${environment.GITHUB_RUN_ID}-${environment.GITHUB_RUN_ATTEMPT}` : null,
errors };
}
/** Preserve tracked-source cleanliness; allow only the verified workflow download. */
export function inspectNativeCompletionSourceMetadata({ repositoryRoot, sourceFiles, baseSha, variant, shallowParentAnchors }) {
const git = (args, raw = false) => spawnSync("git", ["--no-replace-objects", ...args], {
cwd: repositoryRoot, encoding: raw ? undefined : "utf8", timeout: 30_000,
});
const head = git(["rev-parse", "--verify", "HEAD"]), sha = head.status === 0 ? head.stdout.trim() : null;
const ancestor = git(["merge-base", "--is-ancestor", baseSha, "HEAD"]);
const shallow = git(["rev-parse", "--is-shallow-repository"]), isShallow = shallow.status === 0 && shallow.stdout.trim() === "true";
const errors = [];
if (!/^[a-f0-9]{40}$/.test(sha ?? "")) errors.push("Git HEAD is unavailable or is not an immutable SHA1 commit");
let layering = ancestor.status === 0, strategy = layering ? "full_ancestry" : null;
const anchor = shallowParentAnchors?.[variant] ?? null;
if (!layering && isShallow && anchor && sha) {
const commit = git(["cat-file", "commit", "HEAD"], true);
layering = commit.status === 0 && verifyNativeCompletionParentAnchor({ sha, rawCommit: commit.stdout, anchor });
if (layering) strategy = "shallow_immediate_parent_anchor";
}
if (!layering) errors.push(isShallow
? "shallow Git HEAD does not have the sole hash-verified immediate parent admitted for this source variant"
: "full Git history does not prove the declared master base is an ancestor of HEAD");
const tracked = git(["ls-files", "--error-unmatch", "--", ...sourceFiles]);
const clean = git(["diff", "--quiet", "HEAD", "--"]);
const status = git(["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
const entries = status.status === 0 ? status.stdout.split("\0").filter(Boolean) : [];
const unexpected = entries.filter(entry => entry.slice(0, 3) !== "?? " || !hydrationPaths.includes(entry.slice(3)));
const hydration = inspectNativeCompletionBuildHydration(repositoryRoot);
if (tracked.status !== 0) errors.push("one or more admitted source paths are not tracked at HEAD");
if (clean.status !== 0) errors.push("tracked source differs from HEAD");
if (status.status !== 0) errors.push("Git worktree status is unavailable");
if (unexpected.length) errors.push(`unexpected worktree entries: ${unexpected.join(", ")}`);
errors.push(...hydration.errors);
const immutable = tracked.status === 0 && clean.status === 0 && status.status === 0 && unexpected.length === 0 &&
(!hydration.present || hydration.verified);
const metadata = { schema: "paperclip.native-completion-source-metadata.v1",
lineage: { strategy, shallow: isShallow, sha, baseSha, anchor: strategy === "shallow_immediate_parent_anchor" ? anchor : null },
hydration: { present: hydration.present, verified: hydration.verified, archiveSha256: hydration.archiveSha256, checksumSha256: hydration.checksumSha256 },
worktreeEntries: entries, errors };
return { sha, layering, immutable, sourceMetadata: metadata, sourceMetadataErrors: errors,
sourceMetadataFingerprint: sha256(JSON.stringify(metadata)) };
}