mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-17 18:32:19 +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:
@@ -49,6 +49,7 @@ import { isProcessAlive } from './error-handling';
|
||||
import { sanitizeBody, stripLoneSurrogateEscapes } from './sanitize';
|
||||
import { startSocksBridge, testUpstream, type BridgeHandle } from './socks-bridge';
|
||||
import { parseProxyConfig, toUpstreamConfig, ProxyConfigError } from './proxy-config';
|
||||
import { writeReceipt } from '../../lib/egress-receipt';
|
||||
import { redactProxyUrl } from './proxy-redact';
|
||||
import { shouldSpawnXvfb, pickFreeDisplay, spawnXvfb, xvfbInstallHint, type XvfbHandle } from './xvfb';
|
||||
import { logTunnelDenial } from './tunnel-denial-log';
|
||||
@@ -2375,6 +2376,19 @@ export function buildFetchHandler(cfg: ServerConfig): ServerHandle {
|
||||
const forwardOpts: any = { addr: tunnelPort, authtoken };
|
||||
if (domain) forwardOpts.domain = domain;
|
||||
|
||||
// Egress receipt BEFORE the tunnel session opens, fail-closed: a
|
||||
// writeReceipt failure lands in this catch, which tears the tunnel
|
||||
// listener back down and refuses the start. One receipt per session
|
||||
// open; browse command behavior over the tunnel is unchanged.
|
||||
writeReceipt({
|
||||
sink: 'browse-tunnel',
|
||||
host: domain || 'connect.ngrok-agent.com',
|
||||
payloadClass: 'tunnel-session-open (scoped-token browser-command surface)',
|
||||
bytes: 0,
|
||||
sha256: null,
|
||||
consent: 'pair_agent=on',
|
||||
});
|
||||
|
||||
tunnelListener = await ngrok.forward(forwardOpts);
|
||||
tunnelUrl = tunnelListener.url();
|
||||
tunnelServer = boundTunnel;
|
||||
@@ -3084,6 +3098,18 @@ export async function start() {
|
||||
const forwardOpts: any = { addr: tunnelPort, authtoken };
|
||||
if (domain) forwardOpts.domain = domain;
|
||||
|
||||
// Egress receipt BEFORE the tunnel session opens, fail-closed: a
|
||||
// writeReceipt failure lands in this catch, which cleans up the
|
||||
// listener and skips the tunnel (same as any other startup failure).
|
||||
writeReceipt({
|
||||
sink: 'browse-tunnel',
|
||||
host: domain || 'connect.ngrok-agent.com',
|
||||
payloadClass: 'tunnel-session-open (scoped-token browser-command surface)',
|
||||
bytes: 0,
|
||||
sha256: null,
|
||||
consent: 'pair_agent=on (BROWSE_TUNNEL=1)',
|
||||
});
|
||||
|
||||
tunnelListener = await ngrok.forward(forwardOpts);
|
||||
tunnelUrl = tunnelListener.url();
|
||||
tunnelServer = boundTunnel;
|
||||
|
||||
Reference in New Issue
Block a user