Files
gstack/test/fixtures/fake-impeccable.ts
Garry TanandClaude Fable 5.1 60758c5ddf fix(design-detect): never execute a repository-controlled engine; allow-list --changed targets; sanitize engine text
Pre-landing review findings (security + checklist), all reproduced before the fix:

- A checked-out branch could commit `.claude/skills/impeccable/scripts/bin/<os>-<arch>/impeccable`
  and the probe would report READY and `scan` would run it, with the agent's full
  environment. Launchers and sibling engines under the repo or cwd now count as
  "skill present" only (IMPECCABLE_NOT_CACHED: repository-local install, and the
  hint never names a repository-local launcher to run); only HOME-rooted installs,
  IMPECCABLE_BIN, the cache, and PATH entries outside the repo qualify, all by
  realpath. The engine now sees a minimal environment (PATH, HOME, TMPDIR, locale,
  IMPECCABLE_*), never the agent's tokens.
- `scan --changed <base>` pushed git-derived paths without the allow-list, so a
  committed symlink with a frontend extension handed a file outside the repo to
  the engine. Derived targets now go through the same allow-list as explicit ones
  and symlinks named by git are refused outright.
- A repo-controlled `scripts/VERSION` with embedded newlines forged probe lines;
  the version is trusted only when it is semver, and every printed version is
  sanitized. Engine text containing the untrusted-content fence or a
  `SENTINEL:` prefix is neutralized with a zero-width space
  (neutralizeSentinels in the contract), so page text cannot close the envelope
  or forge a probe line.
- A failing `git diff <base>...HEAD` (unknown or unfetched base) was swallowed
  and read as "no frontend changes"; it is now DETECT_REFUSED with exit 1.
- The scan allow-list root follows `${GSTACK_HOME:-$HOME/.gstack}` like the
  templates and gstack-slug (config.yaml keeps gstack-config's STATE_ROOT
  precedence); a quoted or commented design_detector value reads correctly.

Smaller: raw engine chunks are kept only in --format raw; diagnostics are
capped (200 kept, 20 echoed); the engine identity hash reads size + 4 MB, not
the whole binary; PROBE_STEP and ENGINE_STDERR are contract sentinels; the
--verbose gate covers every probe step; analytics use one sentinel vocabulary;
bare limits live in DETECT_LIMITS. The fake engine's knobs are IMPECCABLE_FAKE_*
(so they pass the minimal env) and a shared test helper installs it. New tests
cover each item above plus clean runs, `{}` parse errors, missing paths, and the
50 MB stdout cap.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 17:31:30 +00:00

45 lines
2.1 KiB
TypeScript
Executable File

#!/usr/bin/env bun
/**
* fake-impeccable — a stand-in for the impeccable engine binary in tests.
*
* Behaves like `impeccable detect --json <targets>`: prints a findings JSON
* array on stdout and exits with the engine's code. Everything is driven by env
* so tests never edit this file:
* IMPECCABLE_FAKE_OUTPUT path of the JSON (default: impeccable-detect-sample.json beside this file)
* IMPECCABLE_FAKE_EXIT exit code (default 2 = findings)
* IMPECCABLE_FAKE_LOG append one JSON line per invocation: {argv, cwd, stdinIsTTY}
* IMPECCABLE_FAKE_SLEEP_MS sleep before printing (timeout tests)
* IMPECCABLE_FAKE_STDERR text to print on stderr (diagnostics tests)
* IMPECCABLE_FAKE_RAW print this exact text instead of the JSON file (parse-error tests)
* IMPECCABLE_FAKE_REPEAT repeat the sample findings N times (display-cap tests)
* Spawned directly (shebang), so the spawn-based tests are POSIX-only.
*/
import * as fs from 'fs';
import * as path from 'path';
const env = process.env;
if (env.IMPECCABLE_FAKE_LOG) {
fs.appendFileSync(env.IMPECCABLE_FAKE_LOG, JSON.stringify({ argv: process.argv.slice(2), cwd: process.cwd(), stdinIsTTY: Boolean(process.stdin.isTTY) }) + '\n');
}
const sleep = Number(env.IMPECCABLE_FAKE_SLEEP_MS ?? 0);
if (sleep > 0) Bun.sleepSync(sleep);
if (env.IMPECCABLE_FAKE_STDERR) process.stderr.write(env.IMPECCABLE_FAKE_STDERR + '\n');
if (env.IMPECCABLE_FAKE_RAW !== undefined) {
process.stdout.write(env.IMPECCABLE_FAKE_RAW);
} else {
const file = env.IMPECCABLE_FAKE_OUTPUT ?? path.join(import.meta.dir, 'impeccable-detect-sample.json');
const text = fs.readFileSync(file, 'utf-8');
const repeat = Number(env.IMPECCABLE_FAKE_REPEAT ?? 1);
if (repeat > 1) {
const arr = JSON.parse(text) as unknown[];
const out: unknown[] = [];
for (let i = 0; i < repeat; i++) for (const f of arr) out.push({ ...(f as object), line: i });
process.stdout.write(JSON.stringify(out, null, 2) + '\n');
} else {
process.stdout.write(text);
}
}
// exitCode, not process.exit(): large outputs must flush through the pipe first.
process.exitCode = Number(env.IMPECCABLE_FAKE_EXIT ?? 2);