mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-08 09:48:20 +01:00
fix: the free-text block exits loudly when mktemp fails; spec and redact tests follow the file mechanism
The redact resolver test, the #2679 mktemp guard test, the /spec invariant and the spec secret-sink harness pinned the heredoc transport. They now pin the agent-written file: the harness runs the draft-file block, writes the draft the way the file-write tool would, then runs the real scan fence, with every HIGH/MEDIUM/scanner-failure assertion unchanged. Co-authored-by: dgrant <dgrant@users.noreply.github.com>
This commit is contained in:
1 parent
7d9e37ffd6
commit
e20d59be0d
8 files changed
+34
-23
No files matched your search
@@ -21,9 +21,12 @@ const ctx = {
|
||||
|
||||
|
||||
describe("REDACT_INVOCATION_BLOCK", () => {
|
||||
test("scan-at-sink: temp file → scan that file → exact bytes", () => {
|
||||
test("scan-at-sink: agent-written private file → scan that file → exact bytes", () => {
|
||||
const block = generateRedactInvocationBlock(ctx, ["pre-issue"]);
|
||||
expect(block).toContain("mktemp");
|
||||
// CEO-12: the bytes come from the file the agent wrote (created by the
|
||||
// shared mktemp free-text block), never from a heredoc.
|
||||
expect(block).toContain('/.gstack/tmp/<redact-file-name>"');
|
||||
expect(block).not.toContain("<<");
|
||||
expect(block).toContain("--from-file");
|
||||
expect(block).toMatch(/exact bytes/i);
|
||||
});
|
||||
|
||||
@@ -64,15 +64,13 @@ describe("PR #1169 bug #4: gstack-telemetry-sync mktemp fallback", () => {
|
||||
// must also restore the backup when the swap fails (same failure class:
|
||||
// backup deletion after a failed mv).
|
||||
describe("#2679: skill-content mktemp guards", () => {
|
||||
test("redact-doc resolver guards REDACT_FILE=$(mktemp) with a loud exit", () => {
|
||||
// The guard line contains a ${sink.noun} interpolation in the resolver
|
||||
// source, so match to end-of-line rather than [^}]* (which stops at the
|
||||
// interpolation's closing brace).
|
||||
const body = readScript("scripts/resolvers/redact-doc.ts");
|
||||
expect(body).toMatch(/REDACT_FILE=\$\(mktemp "\\\$\{TMPDIR:-\/tmp\}\/[^"]+"\)\s*\|\|\s*\{.*exit 1/);
|
||||
// And the rendered output (interpolation resolved) carries the guard too.
|
||||
test("the shared free-text block guards each mktemp with a loud exit (it creates /spec's REDACT_FILE)", () => {
|
||||
// CEO-12 moved REDACT_FILE's creation from the redact-doc resolver into the
|
||||
// shared free-text block (scripts/resolvers/free-text-file.ts).
|
||||
const body = readScript("scripts/resolvers/free-text-file.ts");
|
||||
expect(body).toMatch(/=\$\(mktemp "\$_GT\/\$\{f\.stem\}\.XXXXXX"\) \|\| \{ echo "Not sent: [^"]*" >&2; exit 1; \}/);
|
||||
const rendered = readScript("spec/sections/gate-and-file.md");
|
||||
expect(rendered).toMatch(/REDACT_FILE=\$\(mktemp "\$\{TMPDIR:-\/tmp\}\/[^"]+"\)\s*\|\|\s*\{[^}]*exit 1/);
|
||||
expect(rendered).toMatch(/REDACT_FILE=\$\(mktemp "\$_GT\/spec\.XXXXXX"\)\s*\|\|\s*\{[^}]*exit 1/);
|
||||
});
|
||||
|
||||
test("ship pr-body template guards PR_BODY_FILE=$(mktemp ...) with a loud exit", () => {
|
||||
|
||||
@@ -42,6 +42,14 @@ exec '${realMktemp}' "$@"
|
||||
if (scanner === 'broken') writeFileSync(join(bin, 'gstack-redact'), '#!/usr/bin/env bash\nexit 70\n', { mode: 0o755 });
|
||||
writeFileSync(join(bin, 'fake-reviewer'), '#!/usr/bin/env bash\ncat > "$SINK_DIR/reviewer-received.txt"\n', { mode: 0o755 });
|
||||
const full = content(host);
|
||||
// The agent's part: run the draft-file block, then write the draft with its
|
||||
// file-write tool (CEO-12: the text never passes through the shell).
|
||||
const create = full.match(/```bash\n([^`]*?REDACT_FILE=\$\(mktemp[\s\S]*?)\n```/);
|
||||
if (!create) throw new Error(`Missing draft-file block in ${host} spec`);
|
||||
const made = Bun.spawnSync(['bash', '-c', create[1]], { cwd: scratch, env: { ...process.env, TMPDIR: temps }, stdout: 'pipe', stderr: 'pipe', timeout: 10_000 });
|
||||
const draft = /^REDACT_FILE: (\S+) \(name: (\S+)\)$/m.exec(made.stdout.toString());
|
||||
if (made.exitCode !== 0 || !draft) throw new Error(`draft-file block failed: ${made.stderr.toString()}`);
|
||||
writeFileSync(draft[1], body + '\n');
|
||||
const start = full.indexOf('#### Redaction scan — pre-codex');
|
||||
if (start < 0) throw new Error(`Missing pre-codex redaction in ${host} spec`);
|
||||
const match = full.slice(start).match(/```bash\n([\s\S]*?)\n```/);
|
||||
@@ -49,8 +57,8 @@ exec '${realMktemp}' "$@"
|
||||
const fence = match[1]
|
||||
.replaceAll('~/.claude/skills/gstack', runtime)
|
||||
.replaceAll('$HOME/.claude/skills/gstack', runtime)
|
||||
.replace('<the exact the spec body goes here>', body);
|
||||
if (fence.includes('<the exact')) throw new Error('Spec fixture bytes did not replace the scan placeholder');
|
||||
.replace('<redact-file-name>', draft[2]);
|
||||
if (fence.includes('<redact-file-name>')) throw new Error('Spec fixture did not substitute the draft file name');
|
||||
// Deliberately put sinks directly after the real fence. A missing executable
|
||||
// stop (the previous prose-only gate) sends/persists the secret and fails.
|
||||
const script = `${errexit ? 'set -e\n' : ''}${fence}\n"$GSTACK_BIN/fake-reviewer" < "$REDACT_FILE"
|
||||
@@ -68,7 +76,7 @@ rm -f "$REDACT_FILE"
|
||||
const printed = /^REDACT_FILE: (.+)$/m.exec(stdout)?.[1];
|
||||
return { code: result.exitCode, stdout, stderr: result.stderr.toString(),
|
||||
sinks: readdirSync(sinks).map(name => ({ name, body: readFileSync(join(sinks, name), 'utf8') })),
|
||||
pending: [temps, systemTemps].flatMap(dir => readdirSync(dir).map(name => readFileSync(join(dir, name), 'utf8'))),
|
||||
pending: [temps, systemTemps, join(scratch, '.gstack', 'tmp')].flatMap(dir => readdirSync(dir).map(name => readFileSync(join(dir, name), 'utf8'))),
|
||||
printed: printed && existsSync(printed) ? readFileSync(printed, 'utf8') : undefined };
|
||||
} finally { rmSync(scratch, { recursive: true, force: true }); }
|
||||
}
|
||||
|
||||
@@ -184,9 +184,11 @@ describe('/spec redaction at every sink (scan-at-sink, carved: gate-and-file sec
|
||||
expect(fileIdx).toBeGreaterThan(scanIdx);
|
||||
});
|
||||
test('files from the scanned temp file (exact bytes, not a re-render)', () => {
|
||||
expect(SEC_GEN).toMatch(/gh issue create --title "\$ISSUE_TITLE" --body-file "\$REDACT_FILE"/);
|
||||
// #3046: the title reaches the shell only through a quoted heredoc.
|
||||
expect(SEC_GEN).toMatch(/ISSUE_TITLE=\$\(cat <<'GSTACK_TITLE'\n<title>\nGSTACK_TITLE\n\)/);
|
||||
expect(SEC_GEN).toContain('ISSUE_URL=$(gh issue create --title "$(cat "$TITLE_FILE")" --body-file "$REDACT_FILE")');
|
||||
// CEO-12: the title is an agent-written file; no heredoc or quoted placeholder carries it.
|
||||
expect(SEC_GEN).toContain('TITLE_FILE=$(mktemp "$_GT/title.XXXXXX")');
|
||||
expect(SEC_GEN).not.toContain('<<\'GSTACK_TITLE\'');
|
||||
expect(SEC_GEN).not.toMatch(/"<title>"|<<\s*'?REDACT_BODY_EOF/);
|
||||
});
|
||||
test('scan precedes the archive write (pre-archive)', () => {
|
||||
const scanIdx = SEC_GEN.indexOf('Re-scan before archiving');
|
||||
|
||||
Reference in new issue
Block a user