mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-13 00:19:03 +02:00
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:
co-authored by
Claude Fable 5.1
parent
59258850ba
commit
3b7a2775ee
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user