feat: make remote pair-agent tunnel opt-in (default off)

The ngrok pair-agent tunnel could auto-start whenever ngrok was installed
and shipped active on every install, despite ~0.01% usage. Gate all three
activation points behind a single opt-in config flag so nothing is exposed
to the internet unless the user explicitly enables it.

- New `pair_agent` config key (off | on), default off, read via the shared
  fail-closed `isPairAgentEnabled()` guard in browse/src/config.ts (honors
  `GSTACK_PAIR_AGENT` env override for tests/emergency).
- CLI no longer auto-starts the tunnel when disabled, even if ngrok is
  installed/authed; prints the enable command instead.
- `/tunnel/start` returns 403 with the enable hint when disabled (tunnel
  listener never binds).
- `BROWSE_TUNNEL=1` startup path skips the tunnel bind when disabled.

Local browse/QA (local listener, /command, /browse, /qa, cookie import,
/inspector, /health) is unchanged. When enabled, behavior is identical to
before.

The /pair-agent skill doc is parity-locked GStack 2 legacy (evals/parity/
contracts/pair-agent.json pins the render + blob SHAs). Its up-front
"enable pair_agent first" wording needs a separate parity-aware regen and
is intentionally not touched here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sinabina
2026-07-21 17:37:01 -07:00
co-authored by Claude Opus 4.8
parent d977070f28
commit 7baffc003b
5 changed files with 149 additions and 8 deletions
+22
View File
@@ -165,6 +165,28 @@ export function resolveGstackHome(): string {
return process.env.GSTACK_HOME || path.join(os.homedir(), '.gstack');
}
/**
* Is the remote pair-agent (ngrok tunnel) surface opt-in enabled?
*
* Fail-closed: the tunnel exposes the local browser to the internet, so it
* stays OFF unless the user explicitly ran `gstack-config set pair_agent on`.
* Any read/parse failure (missing config, malformed JSON) also resolves OFF.
*
* Env override `GSTACK_PAIR_AGENT=on|off` wins (used by tests and as an
* emergency knob), mirroring the telemetry env-hint convention.
*/
export function isPairAgentEnabled(): boolean {
const env = process.env.GSTACK_PAIR_AGENT;
if (env === 'on') return true;
if (env === 'off') return false;
try {
const raw = fs.readFileSync(path.join(resolveGstackHome(), 'config.json'), 'utf-8');
return JSON.parse(raw)?.pair_agent === 'on';
} catch {
return false;
}
}
/**
* Resolve the Chromium profile directory.
*