mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-15 17:35:29 +02:00
feat(security): receipt TS module sinks + tunnel
writeReceipt (fail-closed, sha256:null — a subprocess or SDK owns the wire bytes) before every TS-module network-bearing operation: - bin/gstack-gbrain-sync.ts: before the gbrain code walk that ships repo content to the user's gbrain DB (may be remote Postgres). A refused receipt fails the stage with status refused-egress-receipt. - bin/gstack-memory-ingest.ts: before the gbrain batch import of transcript pages. A refused receipt returns a system_error verdict without spawning the import. - browse/src/server.ts: before both ngrok.forward call sites (start-up BROWSE_TUNNEL=1 path and the /tunnel/start endpoint). A receipt failure lands in the existing catch that tears the tunnel listener back down and refuses the start. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit 5677d618a48fcd0ae2b068bf868781d90f809cb5)
This commit is contained in:
@@ -65,6 +65,7 @@ import {
|
||||
withErrorContext,
|
||||
} from "../lib/gstack-memory-helpers";
|
||||
import { execGbrainText, spawnGbrainAsync } from "../lib/gbrain-exec";
|
||||
import { writeReceipt } from "../lib/egress-receipt";
|
||||
import { checkOwnedStagingDir, STAGING_MARKER } from "../lib/staging-guard";
|
||||
|
||||
// ── Types ──────────────────────────────────────────────────────────────────
|
||||
@@ -1690,6 +1691,36 @@ async function ingestPass(args: CliArgs): Promise<BulkResult> {
|
||||
// spawn, parent termination orphans the gbrain process (observed
|
||||
// during 2026-05-10 cold-run testing — gbrain kept running 15 min
|
||||
// after the orchestrator timed out).
|
||||
//
|
||||
// Egress receipt BEFORE the import (fail-closed): the gbrain DB may be a
|
||||
// remote Postgres, so the ingest is a potential off-machine send. The
|
||||
// gbrain subprocess owns the wire bytes (content-free receipt, sha256
|
||||
// null). The remote-http branch above stages locally only — its egress
|
||||
// happens in gstack-brain-sync, which writes its own receipt at the push.
|
||||
try {
|
||||
writeReceipt({
|
||||
sink: "memory-ingest",
|
||||
host: "gbrain-db (user-configured DATABASE_URL)",
|
||||
payloadClass: `transcript-pages count=${staging.written} (sent by gbrain subprocess)`,
|
||||
bytes: 0,
|
||||
sha256: null,
|
||||
consent: "gbrain setup consent (/setup-gbrain)",
|
||||
});
|
||||
} catch (err) {
|
||||
const msg = `EGRESS_RECEIPT_FAILED: ${(err as Error).message} — ingest refused`;
|
||||
console.error(`[memory-ingest] ERR: ${msg}`);
|
||||
failed += prep.prepared.length;
|
||||
return {
|
||||
written: 0,
|
||||
skipped_secret: prep.skippedSecret,
|
||||
skipped_dedup: prep.skippedDedup,
|
||||
skipped_unattributed: prep.skippedUnattributed,
|
||||
failed,
|
||||
duration_ms: Date.now() - t0,
|
||||
partial_pages: prep.partialPages,
|
||||
system_error: msg,
|
||||
};
|
||||
}
|
||||
const importResult = await runGbrainImport(stagingDir, resolveImportTimeoutMs());
|
||||
|
||||
const stdout = importResult.stdout || "";
|
||||
|
||||
Reference in New Issue
Block a user