mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-22 21:00:51 +02:00
feat(hooks): memorable-user-prompt-hook.ts — consent gate, deny veto, HIGH-tier pre-scan, fail-closed receipt, trust envelope; runExternal in spawn-bin
The PR's hook exec'd the vendor binary with the full environment and passed its stdout to Claude verbatim. It is now the house pattern: a fail-open bash shim over a .ts twin that (1) gates on the memorable_recall consent key, (2) skips repos whose trust policy is deny or read-only, (3) scans the prompt (raw bytes and decoded string leaves) and refuses to hand over a HIGH-tier credential shape, (4) writes a fail-closed egress receipt naming the local executable it ran, (5) spawns the vendor in its own process group with an allowlisted environment and group-kills it on timeout, (6) accepts only a string additionalContext back, caps it at 8 KiB on a UTF-8 boundary and wraps it in the trust envelope, and (7) records an `output-written` outcome after the stdout write completes. One deadline clock (4.5 s) undercuts Claude Code's 5 s kill and bounds both ledger writes through the new lockBudgetMs option on writeReceipt/writeOutcome (default unchanged). spawn-bin gains runExternal for external executables (detached group, stderr drained, stdin EPIPE handled, stdout capped, win32 refused). The wiring test pins the sink fail-closed and sweeps hosts/. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
3034769813
commit
d4dbeb6d42
@@ -52,6 +52,11 @@ const POLARITY: Record<string, 'fail-closed' | 'fail-open'> = {
|
||||
'browse-tunnel (ngrok)': 'fail-closed',
|
||||
'gbrain-mcp-verify': 'fail-closed',
|
||||
'supabase-provision': 'fail-closed',
|
||||
// memorable-recall: a Claude Code hook hands the user's prompt JSON to a
|
||||
// third-party binary on every prompt. Skipping one recall costs nothing;
|
||||
// an unrecorded hand-off of user content is the thing the ledger exists to
|
||||
// prevent, so it fails closed ("no receipt, no send").
|
||||
'memorable-recall': '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 +86,10 @@ 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',
|
||||
// The Memorable bridge hook: gstack-owned code that hands each prompt to a
|
||||
// vendor CLI. hosts/ has no curl/fetch for the scanner to see, so the
|
||||
// receipt wiring is pinned here explicitly.
|
||||
'hosts/claude/hooks/memorable-user-prompt-hook.ts',
|
||||
];
|
||||
|
||||
/** Shell sinks: must source the shared lib; every network op receipted. */
|
||||
@@ -317,6 +326,7 @@ describe('egress receipt wiring tripwire', () => {
|
||||
'browse-tunnel (ngrok)',
|
||||
'gbrain-mcp-verify',
|
||||
'gbrain-sync',
|
||||
'memorable-recall',
|
||||
'memory-ingest',
|
||||
'supabase-provision',
|
||||
'telemetry-sync',
|
||||
@@ -350,6 +360,15 @@ describe('egress receipt wiring tripwire', () => {
|
||||
expect(provision).toContain('fail-closed');
|
||||
expect(provision.indexOf('writeReceipt(')).toBeGreaterThan(0);
|
||||
expect(provision.indexOf('writeReceipt(')).toBeLessThan(provision.indexOf('ctx.fetchImpl('));
|
||||
// memorable-recall (closed): the hook's receipt precedes the vendor spawn
|
||||
// (marker-based: the policy lookup spawns git earlier, so plain
|
||||
// `runExternal(` order would be the wrong thing to pin) and a receipt
|
||||
// failure skips the vendor. The behavioural proof lives in
|
||||
// test/memorable-user-prompt-hook.test.ts.
|
||||
const memo = read('hosts/claude/hooks/memorable-user-prompt-hook.ts');
|
||||
expect(memo).toContain('fail-closed');
|
||||
expect(memo.indexOf('writeReceipt(')).toBeGreaterThan(0);
|
||||
expect(memo.indexOf('writeReceipt(')).toBeLessThan(memo.indexOf('// VENDOR SPAWN'));
|
||||
// design (open): the wrapper catches receipt errors and proceeds.
|
||||
const rf = read('design/src/receipted-fetch.ts');
|
||||
expect(rf).toContain('fail-open');
|
||||
@@ -357,7 +376,7 @@ describe('egress receipt wiring tripwire', () => {
|
||||
});
|
||||
|
||||
test('NEW-SINK SCANNER: every outbound network op in the tree is wired or reasoned-exempt', () => {
|
||||
const SWEEP = ['bin', 'lib', 'scripts', 'design/src', 'browse/src'];
|
||||
const SWEEP = ['bin', 'lib', 'scripts', 'design/src', 'browse/src', 'hosts'];
|
||||
const offenders: string[] = [];
|
||||
for (const dirRel of SWEEP) {
|
||||
const dir = path.join(ROOT, dirRel);
|
||||
|
||||
Reference in New Issue
Block a user