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
+4
View File
@@ -52,6 +52,9 @@ const defaults = Object.freeze({
redact_repo_visibility: "",
redact_prepush_hook: false,
salience_allowlist: "",
// Remote pair-agent (ngrok tunnel) is opt-in: OFF exposes nothing to the
// internet. Set "on" to allow the tunnel to start.
pair_agent: "off",
});
const [command, ...args] = process.argv.slice(2);
@@ -133,6 +136,7 @@ function validateClosedValue(key, value) {
[/^redact_repo_visibility$/, ["public", "private", "unknown"], "unknown"],
[/^redact_prepush_hook$/, ["true", "false"], "false"],
[/^plan_tune_hooks$/, ["prompt", "yes", "no"], "prompt"],
[/^pair_agent$/, ["off", "on"], "off"],
];
if (key === "codex_reviews" && !["enabled", "disabled"].includes(value)) {
throw new Error(`codex_reviews '${value}' not recognized. Valid values: enabled, disabled. Existing value left unchanged.`);