feat(design-detect): consent-gated engine install, checksum-pinned and receipted

`gstack-design-detect.ts install` is the one download gstack makes, and only
after a design skill's one-time question got a yes. It fetches the engine
version gstack has tested (0.1.3) for this platform from impeccable's own
GitHub release, verifies it against the checksum pinned in
lib/design-detect-contract.ts (all five platforms, captured from the
release's .sha256 sidecars; linux-x64 equals the fixture engine), writes an
egress receipt before the fetch and refuses to download when the receipt
cannot be written (fail-closed; the sink is registered in the wiring test's
polarity table), caps the download at 32 MB, streams with the cap enforced,
writes the file only after the hash matches, and places it under
~/.impeccable/bin/<version>/ (a trusted IMPECCABLE_HOME is honored; never
inside a project). No skill, no hook, no launcher, no npx. --sha256 accepts a
sidecar checksum for a version gstack has not pinned; --base allows a mirror
(https, or http on loopback for tests). After a successful install the probe
runs and its lines follow, so the skill sees READY at once.

The probe ends with DESIGN_DETECTOR_INSTALL_OFFER (version, platform, bytes,
destination) whenever it found no engine and the user has not answered the
question; once design_detector_install_prompted is true it prints neither the
offer nor the NOT_CACHED hint, which used to repeat on every run. The hint's
npx wording is corrected: `npx impeccable detect --help` caches the engine
for npx only, not where the probe looks.

gstack-config gains design_detector_install_prompted (true|false, typo
rejected, enumerated in list and defaults). Tests: a loopback mirror (async
spawn, so the in-process server can answer) covers install, re-install as a
verified no-op, checksum mismatch, 404, unpinned version, non-https base,
design_detector off, and IMPECCABLE_HOME inside the repo; the offer and the
silenced hint; pin completeness per platform.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-09-09 04:51:14 +00:00
co-authored by Claude Fable 5.1
parent 59258850ba
commit 3b7a2775ee
7 changed files with 449 additions and 36 deletions
+14 -2
View File
@@ -122,6 +122,10 @@ CONFIG_HEADER='# gstack configuration — edit freely, changes take effect on ne
# # /document-release skip the outside-voice step entirely.
# # An invalid value is REJECTED (existing value preserved) so
# # a typo cannot silently turn paid Codex calls on or off.
# design_detector_install_prompted: false
# # true once you answered the one-time offer from the
# # design skills to download the impeccable engine with
# # "never ask again"; flip back to false to be asked again.
# design_detector: auto # Deterministic design pre-pass through a user-installed
# # impeccable engine (/design-review, /review, /ship,
# # /design-html). auto = use the engine when the probe
@@ -157,6 +161,7 @@ lookup_default() {
explain_level) echo "default" ;;
codex_reviews) echo "enabled" ;;
design_detector) echo "auto" ;; # auto | off — impeccable engine pre-pass in the design skills
design_detector_install_prompted) echo "false" ;; # true once the user answered the one-time engine install offer with "never ask again"
gstack_contributor) echo "false" ;;
skip_eng_review) echo "false" ;;
workspace_root) echo "$HOME/conductor/workspaces" ;;
@@ -424,6 +429,13 @@ case "${1:-}" in
echo "Error: codex_reviews '$VALUE' not recognized. Valid values: enabled, disabled. Existing value left unchanged." >&2
exit 1
fi
# design_detector_install_prompted records "never ask again" for the one-time
# engine install offer. Rejecting a typo keeps the offer from silently coming
# back (or never coming back) because of a mistyped value.
if [ "$KEY" = "design_detector_install_prompted" ] && [ "$VALUE" != "true" ] && [ "$VALUE" != "false" ]; then
echo "Error: design_detector_install_prompted '$VALUE' not recognized. Valid values: true, false. Existing value left unchanged." >&2
exit 1
fi
# design_detector gates a third-party binary the user installed. Reject a typo
# rather than coerce it: "of" must not silently re-enable or disable the scan.
if [ "$KEY" = "design_detector" ] && [ "$VALUE" != "auto" ] && [ "$VALUE" != "off" ]; then
@@ -469,7 +481,7 @@ case "${1:-}" in
skill_prefix checkpoint_mode checkpoint_push explain_level \
codex_reviews gstack_contributor skip_eng_review workspace_root \
artifacts_sync_mode artifacts_sync_mode_prompted plan_tune_hooks \
timeline_stop_hook design_detector; do
timeline_stop_hook design_detector design_detector_install_prompted; do
VALUE=$(read_config_value "$KEY" || true)
SOURCE="default"
if [ -n "$VALUE" ]; then
@@ -486,7 +498,7 @@ case "${1:-}" in
skill_prefix checkpoint_mode checkpoint_push explain_level \
codex_reviews gstack_contributor skip_eng_review workspace_root \
artifacts_sync_mode artifacts_sync_mode_prompted plan_tune_hooks \
timeline_stop_hook design_detector; do
timeline_stop_hook design_detector design_detector_install_prompted; do
printf ' %-24s %s\n' "$KEY:" "$(lookup_default "$KEY")"
done
;;
+181 -23
View File
@@ -5,12 +5,18 @@
* bun --no-env-file run ~/.claude/skills/gstack/bin/gstack-design-detect.ts probe [--host <h>] [--verbose]
* bun --no-env-file run ~/.claude/skills/gstack/bin/gstack-design-detect.ts scan [--format gstack|raw] [--changed <base>] [--host <h>] <paths...>
* bun --no-env-file run ~/.claude/skills/gstack/bin/gstack-design-detect.ts rules
* bun --no-env-file run ~/.claude/skills/gstack/bin/gstack-design-detect.ts install [--version <v>] [--sha256 <hex>] [--base <url>]
*
* Rule zero: gstack never installs, downloads, or executes anything that could
* download. The probe touches the filesystem and the environment only: file
* existence, a first-bytes sniff, JSON parsing. It never runs impeccable's
* launcher (`scripts/impeccable`) or its npm shim, because both fall through to a
* GitHub download when the engine is not cached.
* Rule zero: gstack never runs impeccable's installer, its launcher
* (`scripts/impeccable`), or its npm shim, because all three fall through to a
* GitHub download and the installer also writes hooks. The probe touches the
* filesystem and the environment only: file existence, a first-bytes sniff,
* JSON parsing. The one download gstack itself can make is `install`: the engine
* binary for a version gstack has tested, fetched only after the user said yes
* to the skill's one-time offer (DESIGN_DETECTOR_INSTALL_OFFER), verified
* against the checksum pinned in lib/design-detect-contract.ts, placed under
* ~/.impeccable/bin/<version>/ (never inside a project), and recorded in the
* egress ledger before the fetch (fail-closed). No skill, no hook, no launcher.
*
* Probe order (first hit wins; every step is a read):
*
@@ -83,7 +89,9 @@ import {
SENTINEL, TESTED_ENGINE_VERSIONS, ADVISORY_RULE_IDS, DETECT_LIMITS,
UNTRUSTED_BEGIN, UNTRUSTED_END, neutralizeSentinels,
type NormalizedFinding, type ScanResult, SCAN_UNTRUSTED_PATHS,
ENGINE_RELEASE_BASE, ENGINE_ASSETS, ENGINE_PINS,
} from '../lib/design-detect-contract';
import { writeReceipt, writeOutcome } from '../lib/egress-receipt';
import { DESIGN_SLOP_CATALOG, entryForImpeccableId } from '../lib/design-catalog';
import { isFrontendPath } from '../lib/frontend-scope';
@@ -119,24 +127,30 @@ function gitTopLevel(cwd: string): string | null {
return top ? realpathOrNull(top) : null;
}
/** design_detector, read the way bin/gstack-config resolves it (same STATE_DIR precedence, same default). */
function configDesignDetector(): 'auto' | 'off' {
/** One flat key from config.yaml, read the way bin/gstack-config resolves it (same STATE_DIR precedence); '' when unset. */
function configValue(key: string): string {
const file = path.join(gstackStateDir(), 'config.yaml');
try {
const text = fs.readFileSync(file, 'utf-8');
let value = '';
const re = new RegExp(`^${key}:\\s*(.*?)\\s*$`);
for (const line of text.split('\n')) {
const m = line.match(/^design_detector:\s*(.*?)\s*$/);
const m = line.match(re);
if (!m) continue;
// flat YAML: drop a trailing comment and surrounding quotes
value = m[1].replace(/\s+#.*$/, '').trim().replace(/^["'](.*)["']$/, '$1');
}
return value.toLowerCase() === 'off' ? 'off' : 'auto'; // `Off` by hand must not silently re-enable a third-party binary
return value;
} catch {
return 'auto';
return '';
}
}
/** design_detector: `Off` by hand must not silently re-enable a third-party binary. */
function configDesignDetector(): 'auto' | 'off' {
return configValue('design_detector').toLowerCase() === 'off' ? 'off' : 'auto';
}
// ── Probe ────────────────────────────────────────────────────────────────────
const HOSTS_WITH_HOOKS: Record<string, string[]> = {
@@ -442,16 +456,155 @@ function probe(host: string, verbose = false): Probe {
const how = p.launcher
? `run \`${p.launcher} detect --help\` once; it fetches the engine version pinned by your install`
: repoLocalLauncher && !launcherOnPath
? 'the skill is installed inside this repository, and gstack never runs a repository-local launcher; install it under your home directory (`npx impeccable install` outside the repo) if you want the engine here'
: 'run `npx impeccable detect --help` once; it fetches the engine';
p.notes.push(`${SENTINEL.HINT}: impeccable is installed but its engine is not cached; ${how} (gstack never downloads it). Silence this: \`gstack-config set design_detector off\`.`);
return p;
? 'the skill is installed inside this repository, and gstack never runs a repository-local launcher; install it under your home directory (`npx impeccable install --scope global` outside the repo) if you want the engine here'
: 'run `npx impeccable install --scope global` yourself (the engine lands beside the skill under your home directory; `npx impeccable detect --help` alone caches it only for npx)';
p.notes.push(`${SENTINEL.HINT}: impeccable is installed but its engine is not cached; ${how}, or accept the install offer. Silence this: \`gstack-config set design_detector off\`.`);
return withInstallOffer(p);
}
p.sentinel = SENTINEL.NOT_AVAILABLE;
return withInstallOffer(p);
}
/** `${platform}-${arch}` → release asset suffix, or null when impeccable ships no engine for this machine. */
function enginePlatform(): string | null {
return ENGINE_ASSETS[`${process.platform}-${process.arch}`] ?? null;
}
function enginePin(version: string, platform: string): { sha256: string; bytes: number } | null {
return ENGINE_PINS[version]?.[platform] ?? null;
}
/** ~/.impeccable (or a trusted IMPECCABLE_HOME): where `install` puts the engine and where the probe's cache step looks. */
function engineCacheRoot(repoRoot: string, cwd: string): string {
return trustedEnvPath('IMPECCABLE_HOME', repoRoot, cwd, [], () => {}) ?? path.join(HOME, '.impeccable');
}
/**
* A probe that found no engine ends with the one-time install offer: the skill
* asks the user, and only a yes runs `install`. Once the user has answered
* "never ask again" (design_detector_install_prompted=true) the probe prints
* neither the offer nor the NOT_CACHED hint, or the hint would be the nag the
* docs promise does not exist. Machines impeccable ships no pinned engine for get
* no offer.
*/
function withInstallOffer(p: Probe): Probe {
if (configValue('design_detector_install_prompted').toLowerCase() === 'true') {
p.notes = p.notes.filter(n => !n.startsWith(`${SENTINEL.HINT}:`));
return p;
}
const platform = enginePlatform();
const version = TESTED_ENGINE_VERSIONS[TESTED_ENGINE_VERSIONS.length - 1];
const pin = platform ? enginePin(version, platform) : null;
if (!platform || !pin) return p;
const dest = path.join(engineCacheRoot(p.repoRoot, p.cwd), 'bin', version, WIN ? 'impeccable.exe' : 'impeccable');
p.notes.push(`${SENTINEL.INSTALL_OFFER}: version=${version} platform=${platform} bytes=${pin.bytes} dest=${dest}`);
return p;
}
// ── Install (the one download gstack makes, after consent) ───────────────────
interface InstallArgs { version?: string; sha256?: string; base?: string; host: string }
function installRefused(reason: string): number {
process.stdout.write(`${SENTINEL.INSTALL_REFUSED}: ${reason}\n`);
analytics({ verb: 'install', sentinel: 'INSTALL_REFUSED', exit: 1 });
return 1;
}
function sha256File(file: string): string {
return createHash('sha256').update(fs.readFileSync(file)).digest('hex');
}
/**
* Download the pinned engine for this machine into the user's cache. Runs only
* after the skill's AskUserQuestion got a yes (the prose never runs it otherwise).
* Fail-closed on the egress receipt, the checksum, and the size cap: on any
* refusal nothing is written. --sha256 accepts a checksum from the release's
* .sha256 sidecar for a version gstack has not pinned; --base allows a mirror
* (https, or http on loopback for tests).
*/
async function install(args: InstallArgs): Promise<number> {
if (configDesignDetector() === 'off') return installRefused('design_detector is off; `gstack-config set design_detector auto` first');
const platform = enginePlatform();
if (!platform) return installRefused(`impeccable ships no engine for ${process.platform}-${process.arch}`);
const version = (args.version ?? TESTED_ENGINE_VERSIONS[TESTED_ENGINE_VERSIONS.length - 1]).replace(/^v/, '');
if (!semverKey(version)) return installRefused(`"${version}" is not a version`);
const pin = enginePin(version, platform);
const expected = (args.sha256 ?? pin?.sha256 ?? '').toLowerCase();
if (!expected) return installRefused(`gstack pins no checksum for engine ${version} on ${platform}; pass --sha256 <hex> from the release's .sha256 sidecar to accept an unpinned download`);
if (!/^[0-9a-f]{64}$/.test(expected)) return installRefused('--sha256 must be 64 hex characters');
let baseUrl: URL;
try { baseUrl = new URL(args.base ?? ENGINE_RELEASE_BASE); } catch { return installRefused(`--base is not a URL: ${args.base}`); }
const loopback = ['127.0.0.1', 'localhost', '[::1]'].includes(baseUrl.hostname);
if (baseUrl.protocol !== 'https:' && !(baseUrl.protocol === 'http:' && loopback)) return installRefused('--base must be https (http only for a loopback mirror)');
const cwd = realpathOrNull(process.cwd()) ?? process.cwd();
const repoRoot = gitTopLevel(cwd) ?? cwd;
const destDir = path.join(engineCacheRoot(repoRoot, cwd), 'bin', version);
if (underProject(destDir, repoRoot, cwd)) return installRefused(`${destDir} lies inside the project; the engine lives under your home directory only`);
const dest = path.join(destDir, WIN ? 'impeccable.exe' : 'impeccable');
const asset = `impeccable-${platform}${platform.startsWith('windows') ? '.exe' : ''}`;
const url = `${baseUrl.toString().replace(/\/$/, '')}/engine-v${version}/${asset}`;
const cap = DETECT_LIMITS.engineDownloadBytes;
let present = false;
try { present = fs.existsSync(dest) && sha256File(dest) === expected; } catch { present = false; }
if (present) {
process.stdout.write(`${SENTINEL.INSTALLED}: ${dest} version=${version} sha256=${expected} (already present, checksum verified)\n`);
const p = probe(args.host);
process.stdout.write(probeLines(p).join('\n') + '\n');
analytics({ verb: 'install', sentinel: 'INSTALLED', engine: version, exit: 0 });
return 0;
}
// The receipt is written BEFORE the fetch and the install is fail-closed on it:
// an executable arriving on the machine unrecorded is worse than no install.
let receipt: string;
try {
receipt = writeReceipt({
sink: 'design-detect-engine-download', host: baseUrl.host, payloadClass: 'engine-binary-fetch', bytes: 0, sha256: null,
consent: `design_detector=auto design_detector_install_prompted=false; user accepted the install offer for impeccable engine ${version} (${platform}); checksum ${args.sha256 ? 'from --sha256' : 'pinned in gstack'}`,
}).id;
} catch (err) {
return installRefused(`egress receipt could not be written, nothing downloaded (${clip(stripControl(String((err as Error)?.message ?? err)), 200)})`);
}
const outcome = (status: string | number) => { try { writeOutcome({ receipt, status }); } catch { /* bookkeeping only */ } };
let res: Response;
try {
res = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(DETECT_LIMITS.engineDownloadTimeoutMs) });
} catch (err) {
outcome('network-error');
return installRefused(`download failed: ${clip(stripControl(String((err as Error)?.message ?? err)), 200)}`);
}
if (!res.ok || !res.body) { outcome(res.status); return installRefused(`download failed: HTTP ${res.status} for ${url}`); }
const declared = Number(res.headers.get('content-length') ?? '0');
if (declared > cap) { outcome(`${res.status} oversize`); return installRefused(`asset declares ${declared} bytes, above the ${cap}-byte cap`); }
const chunks: Uint8Array[] = [];
let total = 0;
const reader = res.body.getReader();
for (;;) {
const { done, value } = await reader.read();
if (done) break;
total += value.byteLength;
if (total > cap) { await reader.cancel(); outcome(`${res.status} oversize`); return installRefused(`asset exceeds the ${cap}-byte cap; nothing written`); }
chunks.push(value);
}
outcome(`${res.status} ${total}B`);
const buf = Buffer.concat(chunks);
const actual = createHash('sha256').update(buf).digest('hex');
if (actual !== expected) return installRefused(`checksum mismatch: expected ${expected}, got ${actual}; nothing written`);
fs.mkdirSync(destDir, { recursive: true, mode: 0o755 });
const tmp = `${dest}.tmp-${process.pid}`;
fs.writeFileSync(tmp, buf, { mode: 0o755 });
fs.chmodSync(tmp, 0o755);
fs.renameSync(tmp, dest);
process.stdout.write(`${SENTINEL.INSTALLED}: ${dest} version=${version} sha256=${actual} bytes=${total}\n`);
analytics({ verb: 'install', sentinel: 'INSTALLED', engine: version, exit: 0 });
const p = probe(args.host);
process.stdout.write(probeLines(p).join('\n') + '\n');
return 0;
}
/** Identity label for an engine with no version source: size + the first few MB hashed (a whole-binary read per probe is wasted work). */
function engineIdentity(file: string): string {
try {
@@ -797,12 +950,13 @@ function analytics(rec: Record<string, unknown>) {
// ── CLI ──────────────────────────────────────────────────────────────────────
function parse(argv: string[]): { verb: string; host: string; verbose: boolean; scan: ScanArgs } {
function parse(argv: string[]): { verb: string; host: string; verbose: boolean; scan: ScanArgs; install: InstallArgs } {
const verb = argv[0] ?? '';
let host = 'claude';
let verbose = false;
let format: 'gstack' | 'raw' = 'gstack';
let changed: string | undefined;
const install: InstallArgs = { host };
const targets: string[] = [];
for (let i = 1; i < argv.length; i++) {
const a = argv[i];
@@ -810,15 +964,19 @@ function parse(argv: string[]): { verb: string; host: string; verbose: boolean;
else if (a === '--verbose') verbose = true;
else if (a === '--format') { const v = argv[++i]; format = v === 'raw' ? 'raw' : 'gstack'; }
else if (a === '--changed') changed = argv[++i] ?? ''; // an empty base is refused in resolveTargets, never defaulted
else if (a === '--version') install.version = argv[++i] ?? '';
else if (a === '--sha256') install.sha256 = argv[++i] ?? '';
else if (a === '--base') install.base = argv[++i] ?? '';
else if (a === '--') { targets.push(...argv.slice(i + 1)); break; }
else if (a.startsWith('--')) process.stderr.write(`ignoring unknown flag ${a}\n`);
else targets.push(a);
}
return { verb, host, verbose, scan: { format, changed, targets, host } };
install.host = host;
return { verb, host, verbose, scan: { format, changed, targets, host }, install };
}
export function main(argv = process.argv.slice(2)): number {
const { verb, host, verbose, scan: scanArgs } = parse(argv);
export async function main(argv = process.argv.slice(2)): Promise<number> {
const { verb, host, verbose, scan: scanArgs, install: installArgs } = parse(argv);
switch (verb) {
case 'probe': {
const p = probe(host, verbose);
@@ -830,8 +988,10 @@ export function main(argv = process.argv.slice(2)): number {
return scan(scanArgs);
case 'rules':
return rules();
case 'install':
return install(installArgs);
default:
process.stderr.write('usage: gstack-design-detect.ts probe [--host <h>] [--verbose] | scan [--format gstack|raw] [--changed <base>] [--host <h>] <paths...> | rules\n');
process.stderr.write('usage: gstack-design-detect.ts probe [--host <h>] [--verbose] | scan [--format gstack|raw] [--changed <base>] [--host <h>] <paths...> | rules | install [--version <v>] [--sha256 <hex>] [--base <url>]\n');
return 2;
}
}
@@ -839,12 +999,10 @@ export function main(argv = process.argv.slice(2)): number {
if (import.meta.main) {
// exitCode, not process.exit(): a pipe write over 64 KB (a big scan) is still
// in flight when process.exit() runs and would be truncated mid-JSON.
try {
process.exitCode = main();
} catch (err) {
main().then((code) => { process.exitCode = code; }, (err) => {
const e = err as Error;
process.stderr.write(`${SENTINEL.INTERNAL_ERROR}: ${e?.name ?? 'Error'}: ${clip(stripControl(String(e?.message ?? e)), DETECT_LIMITS.field.internalError)}\n`);
analytics({ verb: process.argv[2] ?? '', sentinel: 'INTERNAL_ERROR', exit: 3 });
process.exitCode = 3;
}
});
}
+43 -2
View File
@@ -11,7 +11,8 @@
// ──► always: IMPECCABLE_SKILL, IMPECCABLE_HOOK, IMPECCABLE_IGNORED_RULES, IMPECCABLE_IGNORED_FILES,
// IMPECCABLE_IGNORED_VALUES
// ──► maybe: IMPECCABLE_HOOK_OTHER, IMPECCABLE_CONFIG_UNREADABLE, IMPECCABLE_ENV_IGNORED,
// IMPECCABLE_ENGINE_UNTESTED, DESIGN_DETECTOR_HINT
// IMPECCABLE_ENGINE_UNTESTED, DESIGN_DETECTOR_HINT, DESIGN_DETECTOR_INSTALL_OFFER
// install ──► IMPECCABLE_INSTALLED: <path> ... | IMPECCABLE_INSTALL_REFUSED: <reason> (then the probe lines)
// scan ──► stdout: one JSON document (--format gstack) or engine bytes (--format raw)
// ──► stderr: DETECT_TOP block, DETECT_SUMMARY, DETECT_EXIT, DETECT_REFUSED / DETECT_NO_TARGETS /
// DETECT_TIMEOUT / DETECT_PARSE_ERROR / DETECT_OUTPUT_TOO_LARGE
@@ -31,6 +32,12 @@ export const SENTINEL = {
CONFIG_UNREADABLE: 'IMPECCABLE_CONFIG_UNREADABLE',
ENV_IGNORED: 'IMPECCABLE_ENV_IGNORED',
ENGINE_UNTESTED: 'IMPECCABLE_ENGINE_UNTESTED',
/** the probe found no engine and the user has not answered the install offer yet: the skill asks once */
INSTALL_OFFER: 'DESIGN_DETECTOR_INSTALL_OFFER',
/** `install` placed a checksum-verified engine under the user's home */
INSTALLED: 'IMPECCABLE_INSTALLED',
/** `install` did not write anything, reason after the colon */
INSTALL_REFUSED: 'IMPECCABLE_INSTALL_REFUSED',
HINT: 'DESIGN_DETECTOR_HINT',
DETECT_EXIT: 'DETECT_EXIT',
DETECT_EXIT_CODE: 'DETECT_EXIT_CODE',
@@ -76,12 +83,43 @@ export const SELF_DESCRIBING_SENTINELS: readonly string[] = [
SENTINEL.ENGINE_UNTESTED, SENTINEL.DETECT_EXIT, SENTINEL.DETECT_REFUSED, SENTINEL.DETECT_NO_TARGETS,
SENTINEL.DETECT_TIMEOUT, SENTINEL.DETECT_PARSE_ERROR, SENTINEL.DETECT_OUTPUT_TOO_LARGE,
SENTINEL.DESIGN_MD_TOKEN_REF_INVALID, SENTINEL.DESIGN_MD_WRITTEN, SENTINEL.DESIGN_MD_BACKUP, SENTINEL.DESIGN_MD_EDIT_REFUSED,
SENTINEL.PROBE_STEP, SENTINEL.ENGINE_STDERR, SENTINEL.DOM_DUMP_MISSING,
SENTINEL.PROBE_STEP, SENTINEL.ENGINE_STDERR, SENTINEL.DOM_DUMP_MISSING, SENTINEL.INSTALLED, SENTINEL.INSTALL_REFUSED,
];
/** Engine versions the committed fixtures were captured from. */
export const TESTED_ENGINE_VERSIONS: readonly string[] = ['0.1.3'];
/** Where impeccable publishes its engine binaries (GitHub Releases of pbakaus/impeccable, tag engine-v<version>). */
export const ENGINE_RELEASE_BASE = 'https://github.com/pbakaus/impeccable/releases/download';
/** `${process.platform}-${process.arch}` → the release asset's platform suffix (`impeccable-<suffix>`, `.exe` on Windows). */
export const ENGINE_ASSETS: Readonly<Record<string, string>> = {
'darwin-arm64': 'darwin-arm64',
'darwin-x64': 'darwin-x64',
'linux-x64': 'linux-x64',
'linux-arm64': 'linux-arm64',
'win32-x64': 'windows-x64',
};
/**
* Checksums gstack pins for the engine versions it has tested, per platform:
* the `install` verb refuses a download whose bytes do not hash to the pin.
* Captured 2026-09-09 from the release's own .sha256 sidecars
* (https://github.com/pbakaus/impeccable/releases/tag/engine-v0.1.3); the
* linux-x64 hash also matches the engine gstack's fixtures were captured with.
* A pin recorded in this repo defends against a swapped release asset, which a
* same-origin sidecar cannot; adding a version means re-capturing the fixtures.
*/
export const ENGINE_PINS: Readonly<Record<string, Readonly<Record<string, { sha256: string; bytes: number }>>>> = {
'0.1.3': {
'darwin-arm64': { sha256: '23821135d4c62f1428fd15ddb9e91d695402727f43b13a6eb3e9f31fc01b4072', bytes: 12677904 },
'darwin-x64': { sha256: 'a5bb0ae15d1bd8f61ebd2a6a21d39c2b357a211c39b4b95cc2a947cdb10a4db4', bytes: 14300496 },
'linux-x64': { sha256: 'afc7a424e0bd6c606b7be4c773c70e87284afbdb41d748eb9a34f8a4478e57da', bytes: 15991120 },
'linux-arm64': { sha256: '523c0a223ac0c1522489759a9f56dccb0b458b42d6a5c66e74e6fe2255af60ce', bytes: 13262480 },
'windows-x64': { sha256: '50846da00b48f7df5a82adc6c1ef1c82da0a890ac95e65cdd5da12aab2de6c1d', bytes: 14638984 },
},
};
/** Rules the engine reports but never counts (they never change its exit code). */
export const ADVISORY_RULE_IDS: readonly string[] = ['em-dash-overuse'];
@@ -111,6 +149,9 @@ export const DETECT_LIMITS = {
gitTimeoutMs: 30_000,
/** whole-scan wall clock, as a multiple of the per-batch timeout: a huge target set stops, it never grinds for hours */
totalTimeoutFactor: 5,
/** the engine download the user consented to: twice the largest pinned asset, and a hard wall clock */
engineDownloadBytes: 32 * 1024 * 1024,
engineDownloadTimeoutMs: 120_000,
gitMaxBuffer: 64 * 1024 * 1024,
field: { id: 64, engineVersion: 64, message: 120, snippet: 120, value: 200, file: 4096, diagnostic: 400, refusedTarget: 200, parseErrorPreview: 80, internalError: 300 },
} as const;
+21 -1
View File
@@ -12,7 +12,7 @@ import { describe, test, expect } from 'bun:test';
import * as fs from 'fs';
import * as path from 'path';
import { spawnSync } from 'child_process';
import { SENTINEL, TESTED_ENGINE_VERSIONS, ADVISORY_RULE_IDS, DETECT_LIMITS, DETECT_EXIT_ECHO, SELF_DESCRIBING_SENTINELS, UNTRUSTED_BEGIN, UNTRUSTED_END, neutralizeSentinels } from '../lib/design-detect-contract';
import { SENTINEL, TESTED_ENGINE_VERSIONS, ADVISORY_RULE_IDS, DETECT_LIMITS, DETECT_EXIT_ECHO, SELF_DESCRIBING_SENTINELS, UNTRUSTED_BEGIN, UNTRUSTED_END, neutralizeSentinels, ENGINE_PINS, ENGINE_ASSETS, ENGINE_RELEASE_BASE } from '../lib/design-detect-contract';
import { catalogEntry } from '../lib/design-catalog';
const ROOT = path.join(import.meta.dir, '..');
@@ -120,3 +120,23 @@ describe('every sentinel-shaped token the agent can read exists in the contract'
expect(offenders).toEqual([]);
});
});
describe('engine pins: every tested version is pinned for every platform impeccable ships', () => {
test('pins are complete and well-formed, and the release base is impeccable\'s own GitHub over https', () => {
expect(ENGINE_RELEASE_BASE).toBe('https://github.com/pbakaus/impeccable/releases/download');
const platforms = [...new Set(Object.values(ENGINE_ASSETS))].sort();
expect(platforms).toEqual(['darwin-arm64', 'darwin-x64', 'linux-arm64', 'linux-x64', 'windows-x64']);
for (const v of TESTED_ENGINE_VERSIONS) {
const pins = ENGINE_PINS[v];
expect(pins, `no pins for tested engine ${v}`).toBeDefined();
expect(Object.keys(pins).sort()).toEqual(platforms);
for (const [platform, pin] of Object.entries(pins)) {
expect(pin.sha256, `${v} ${platform}`).toMatch(/^[0-9a-f]{64}$/);
expect(pin.bytes, `${v} ${platform}`).toBeGreaterThan(1_000_000);
expect(pin.bytes).toBeLessThan(DETECT_LIMITS.engineDownloadBytes);
}
}
// the fixture engine (test/fixtures/impeccable-captures.meta.json: engine 0.1.3, linux-x64) is the pinned one
expect(ENGINE_PINS['0.1.3']['linux-x64'].sha256).toBe('afc7a424e0bd6c606b7be4c773c70e87284afbdb41d748eb9a34f8a4478e57da');
});
});
+14 -6
View File
@@ -52,6 +52,9 @@ const POLARITY: Record<string, 'fail-closed' | 'fail-open'> = {
'browse-tunnel (ngrok)': 'fail-closed',
'gbrain-mcp-verify': 'fail-closed',
'supabase-provision': 'fail-closed',
// the engine binary the user consented to download: an executable arriving
// on the machine unrecorded is worse than the install failing
'design-detect-engine-download': 'fail-closed',
// fail-open: user-facing operations that must not die over an audit-log
// hiccup; they warn on stderr and proceed.
'design-openai': 'fail-open',
@@ -81,6 +84,8 @@ const MODULE_SINKS = [
// supabase-provision engine (bin/gstack-gbrain-supabase-provision is a thin
// bun-shebang entry over this module; the receipt lives at the api-call layer).
'lib/gbrain-supabase-provision.ts',
// consent-gated engine download (install verb): receipt before the fetch, fail-closed
'bin/gstack-design-detect.ts',
];
/** Shell sinks: must source the shared lib; every network op receipted. */
@@ -144,12 +149,14 @@ const SCANNER_EXEMPT: Record<string, string> = {
};
// Documented non-sink (not an exemption; nothing here matches the scanner):
// bin/gstack-design-detect.ts spawns a third-party engine binary the USER
// installed (impeccable) over local file paths under the repo root or the
// design-report allow-list. URL targets are refused, so gstack never asks the
// engine to touch the network; the engine's own network behavior is not audited
// by gstack (NOTICE.md says so). This is a class the tripwire cannot see —
// a spawned binary, not curl/fetch/git — recorded here so the posture is explicit.
// bin/gstack-design-detect.ts `scan` spawns a third-party engine binary
// (impeccable) over local file paths under the repo root or the design-report
// allow-list. URL targets are refused, so gstack never asks the engine to touch
// the network; the engine's own network behavior is not audited by gstack
// (NOTICE.md says so). This is a class the tripwire cannot see — a spawned
// binary, not curl/fetch/git — recorded here so the posture is explicit. The
// same file's `install` verb IS a sink (the consented engine download) and is
// registered in MODULE_SINKS above with fail-closed polarity.
function isExempt(rel: string): string | undefined {
for (const [key, reason] of Object.entries(SCANNER_EXEMPT)) {
@@ -323,6 +330,7 @@ describe('egress receipt wiring tripwire', () => {
expect(closed.sort()).toEqual([
'brain-sync',
'browse-tunnel (ngrok)',
'design-detect-engine-download',
'gbrain-mcp-verify',
'gbrain-sync',
'memory-ingest',
+21
View File
@@ -173,3 +173,24 @@ describe('design_detector (auto|off, rejecting validator)', () => {
expect(get('design_detector').out).toBe('auto');
});
});
describe('design_detector_install_prompted (true|false, rejecting validator)', () => {
const env = { ...process.env, GSTACK_STATE_ROOT: STATE };
test('defaults to false, rejects a typo with the file unchanged, round-trips true/false, and is enumerated', () => {
expect(get('design_detector_install_prompted')).toEqual({ out: 'false', code: 0 });
const file = path.join(STATE, 'config.yaml');
const before = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : null;
const bad = spawnSync('bash', [CONFIG_BIN, 'set', 'design_detector_install_prompted', 'yes'], { encoding: 'utf-8', timeout: 30_000, env });
expect(bad.status).toBe(1);
expect(bad.stderr).toContain("design_detector_install_prompted 'yes' not recognized");
expect(fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : null).toBe(before);
spawnSync('bash', [CONFIG_BIN, 'set', 'design_detector_install_prompted', 'true'], { encoding: 'utf-8', timeout: 30_000, env });
expect(get('design_detector_install_prompted').out).toBe('true');
spawnSync('bash', [CONFIG_BIN, 'set', 'design_detector_install_prompted', 'false'], { encoding: 'utf-8', timeout: 30_000, env });
expect(get('design_detector_install_prompted').out).toBe('false');
for (const verb of ['list', 'defaults']) {
const r = spawnSync('bash', [CONFIG_BIN, verb], { encoding: 'utf-8', timeout: 30_000, env });
expect(r.stdout).toMatch(/design_detector_install_prompted:\s+false/);
}
});
});
+155 -2
View File
@@ -11,8 +11,9 @@ import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { spawnSync } from 'child_process';
import { SENTINEL, DETECT_LIMITS, UNTRUSTED_BEGIN, UNTRUSTED_END } from '../lib/design-detect-contract';
import { createHash } from 'crypto';
import { spawnSync, spawn } from 'child_process';
import { SENTINEL, DETECT_LIMITS, UNTRUSTED_BEGIN, UNTRUSTED_END, ENGINE_ASSETS, ENGINE_PINS, TESTED_ENGINE_VERSIONS } from '../lib/design-detect-contract';
import { installFakeImpeccable, DETECT_SAMPLE as SAMPLE } from './helpers/fake-impeccable';
const ROOT = path.join(import.meta.dir, '..');
@@ -78,6 +79,33 @@ function run(args: string[], opts: RunOpts = {}) {
function lines(s: string) { return s.split('\n').filter(Boolean); }
/**
* Same as run(), but asynchronous: a test that serves a loopback mirror with
* Bun.serve in THIS process must not block its own event loop with spawnSync,
* or the child's fetch waits forever for a server that can never answer.
*/
function runAsync(args: string[], opts: RunOpts = {}): Promise<{ code: number; out: string; err: string }> {
const env: Record<string, string> = {
PATH: [BUN_DIR, '/usr/bin', '/bin', '/usr/local/bin'].join(path.delimiter),
HOME: path.join(SANDBOX, 'fake-home'),
GSTACK_HOME,
IMPECCABLE_HOME,
IMPECCABLE_FAKE_OUTPUT: SAMPLE,
};
for (const [k, v] of Object.entries(opts.env ?? {})) {
if (v === undefined) delete env[k]; else env[k] = v;
}
return new Promise((resolve) => {
const child = spawn(process.execPath, ['--no-env-file', 'run', BIN, ...args], { cwd: opts.cwd ?? REPO, env, stdio: ['ignore', 'pipe', 'pipe'] });
let out = '', err = '';
child.stdout.setEncoding('utf-8'); child.stderr.setEncoding('utf-8');
child.stdout.on('data', (d: string) => { out += d; });
child.stderr.on('data', (d: string) => { err += d; });
const timer = setTimeout(() => child.kill('SIGKILL'), 60_000);
child.on('close', (code) => { clearTimeout(timer); resolve({ code: code ?? -1, out, err }); });
});
}
describe('probe', () => {
test('empty environment → NOT_AVAILABLE, skill/hook absent, no hint', () => {
const r = run(['probe', '--host', 'claude']);
@@ -1045,3 +1073,128 @@ describe('adversarial round: audit directories, config case, refused base with e
expect(r.code).toBe(1);
});
});
describe('install: the one download gstack makes, after consent', () => {
const PLATFORM = ENGINE_ASSETS[`${process.platform}-${process.arch}`];
const VERSION = TESTED_ENGINE_VERSIONS[TESTED_ENGINE_VERSIONS.length - 1];
const ASSET = PLATFORM ? `impeccable-${PLATFORM}${PLATFORM.startsWith('windows') ? '.exe' : ''}` : '';
const freshHome = () => fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-impeccable-home-'));
const mirror = (body: Uint8Array, hits: string[]) => Bun.serve({
port: 0, hostname: '127.0.0.1',
fetch(req) {
const u = new URL(req.url);
hits.push(u.pathname);
if (u.pathname === `/engine-v${VERSION}/${ASSET}`) return new Response(body);
return new Response('nope', { status: 404 });
},
});
const ledgerPath = () => path.join(GSTACK_HOME, 'security', 'egress.jsonl');
const ledger = () => (fs.existsSync(ledgerPath()) ? fs.readFileSync(ledgerPath(), 'utf-8') : '');
test('the probe offers the pinned engine for this machine once, and stays silent (no offer, no hint) after "never ask again"', () => {
const r = run(['probe']);
expect(lines(r.out)[0]).toBe(SENTINEL.NOT_AVAILABLE);
if (PLATFORM && ENGINE_PINS[VERSION]?.[PLATFORM]) {
expect(r.out).toContain(`${SENTINEL.INSTALL_OFFER}: version=${VERSION} platform=${PLATFORM} bytes=${ENGINE_PINS[VERSION][PLATFORM].bytes} dest=`);
} else {
expect(r.out).not.toContain(SENTINEL.INSTALL_OFFER);
}
fs.writeFileSync(path.join(GSTACK_HOME, 'config.yaml'), 'design_detector_install_prompted: true\n');
const home = freshHome();
try {
const quiet = run(['probe']);
expect(quiet.out).not.toContain(SENTINEL.INSTALL_OFFER);
// NOT_CACHED with a HOME-rooted launcher and no engine: the hint is gone too, or it would nag every run.
const scripts = path.join(home, '.claude', 'skills', 'impeccable', 'scripts');
fs.mkdirSync(scripts, { recursive: true });
fs.writeFileSync(path.join(scripts, 'impeccable'), '#!/bin/sh\necho would download\n');
fs.chmodSync(path.join(scripts, 'impeccable'), 0o755);
const nc = run(['probe'], { env: { HOME: home } });
expect(lines(nc.out)[0]).toBe(`${SENTINEL.NOT_CACHED}: ${path.join(scripts, 'impeccable')}`);
expect(nc.out).not.toContain(SENTINEL.HINT);
expect(nc.out).not.toContain(SENTINEL.INSTALL_OFFER);
} finally {
fs.rmSync(path.join(GSTACK_HOME, 'config.yaml'), { force: true });
fs.rmSync(home, { recursive: true, force: true });
}
});
test.skipIf(!POSIX || !PLATFORM)('downloads from a mirror, verifies the checksum, installs under IMPECCABLE_HOME, receipts the fetch first, and the probe finds it', async () => {
const body = fs.readFileSync(FAKE);
const hash = createHash('sha256').update(body).digest('hex');
const hits: string[] = [];
const server = mirror(body, hits);
const home = freshHome();
try {
const r = await runAsync(['install', '--base', `http://127.0.0.1:${server.port}`, '--sha256', hash], { env: { IMPECCABLE_HOME: home } });
const installed = path.join(home, 'bin', VERSION, 'impeccable');
expect(lines(r.out)[0]).toBe(`${SENTINEL.INSTALLED}: ${installed} version=${VERSION} sha256=${hash} bytes=${body.byteLength}`);
expect(r.code).toBe(0);
expect(fs.readFileSync(installed)).toEqual(body);
expect(fs.statSync(installed).mode & 0o111).not.toBe(0);
expect(hits).toEqual([`/engine-v${VERSION}/${ASSET}`]);
expect(r.out).toContain(`${SENTINEL.READY}: ${fs.realpathSync(installed)}`); // the fresh probe after install
const l = ledger();
expect(l).toContain('"sink":"design-detect-engine-download"');
expect(l).toContain(`"host":"127.0.0.1:${server.port}"`);
expect(l).toContain('"type":"outcome"');
expect(l.indexOf('"type":"egress"')).toBeLessThan(l.indexOf('"type":"outcome"'));
const again = await runAsync(['install', '--base', `http://127.0.0.1:${server.port}`, '--sha256', hash], { env: { IMPECCABLE_HOME: home } });
expect(lines(again.out)[0]).toContain('(already present, checksum verified)');
expect(hits).toHaveLength(1); // nothing fetched the second time
} finally {
server.stop(true);
fs.rmSync(home, { recursive: true, force: true });
}
});
test.skipIf(!POSIX || !PLATFORM)('a checksum mismatch, a 404, an unpinned version, a non-https base, and design_detector off each write nothing', async () => {
const body = fs.readFileSync(FAKE);
const hits: string[] = [];
const server = mirror(body, hits);
const home = freshHome();
const base = `http://127.0.0.1:${server.port}`;
try {
const bad = await runAsync(['install', '--base', base, '--sha256', '0'.repeat(64)], { env: { IMPECCABLE_HOME: home } });
expect(lines(bad.out)[0]).toMatch(new RegExp(`^${SENTINEL.INSTALL_REFUSED}: checksum mismatch: expected 0{64}, got [0-9a-f]{64}; nothing written`));
expect(bad.code).toBe(1);
expect(fs.existsSync(path.join(home, 'bin'))).toBe(false);
const missing = await runAsync(['install', '--base', base, '--version', '9.9.9', '--sha256', 'a'.repeat(64)], { env: { IMPECCABLE_HOME: home } });
expect(lines(missing.out)[0]).toContain(`${SENTINEL.INSTALL_REFUSED}: download failed: HTTP 404`);
const unpinned = await runAsync(['install', '--base', base, '--version', '9.9.9'], { env: { IMPECCABLE_HOME: home } });
expect(lines(unpinned.out)[0]).toContain(`${SENTINEL.INSTALL_REFUSED}: gstack pins no checksum for engine 9.9.9`);
expect(hits.filter(h => h.includes('9.9.9'))).toHaveLength(1); // only the --sha256 attempt reached the mirror
const plain = await runAsync(['install', '--base', 'http://example.com'], { env: { IMPECCABLE_HOME: home } });
expect(lines(plain.out)[0]).toContain(`${SENTINEL.INSTALL_REFUSED}: --base must be https`);
fs.writeFileSync(path.join(GSTACK_HOME, 'config.yaml'), 'design_detector: off\n');
try {
const off = await runAsync(['install', '--base', base, '--sha256', 'a'.repeat(64)], { env: { IMPECCABLE_HOME: home } });
expect(lines(off.out)[0]).toContain(`${SENTINEL.INSTALL_REFUSED}: design_detector is off`);
} finally {
fs.rmSync(path.join(GSTACK_HOME, 'config.yaml'), { force: true });
}
expect(fs.existsSync(path.join(home, 'bin'))).toBe(false);
} finally {
server.stop(true);
fs.rmSync(home, { recursive: true, force: true });
}
});
test.skipIf(!POSIX || !PLATFORM)('IMPECCABLE_HOME inside the project is ignored: the engine lands under the real home, never in the repo', async () => {
const body = fs.readFileSync(FAKE);
const hash = createHash('sha256').update(body).digest('hex');
const server = mirror(body, []);
const fakeUserHome = freshHome();
try {
const inRepo = path.join(REPO, '.impeccable');
const r = await runAsync(['install', '--base', `http://127.0.0.1:${server.port}`, '--sha256', hash], { env: { IMPECCABLE_HOME: inRepo, HOME: fakeUserHome } });
expect(r.code).toBe(0);
expect(fs.existsSync(path.join(inRepo, 'bin'))).toBe(false);
expect(fs.existsSync(path.join(fakeUserHome, '.impeccable', 'bin', VERSION, 'impeccable'))).toBe(true);
} finally {
server.stop(true);
fs.rmSync(fakeUserHome, { recursive: true, force: true });
fs.rmSync(path.join(REPO, '.impeccable'), { recursive: true, force: true });
}
});
});