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
+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 });
}
});
});