Skip to content

v1.34.0.0 feat: gstack consumable as submodule (factory-export API + AUTH_TOKEN env + import.meta.main gate)#1472

Merged
garrytan merged 7 commits into
mainfrom
garrytan/monterrey-v3
May 13, 2026
Merged

v1.34.0.0 feat: gstack consumable as submodule (factory-export API + AUTH_TOKEN env + import.meta.main gate)#1472
garrytan merged 7 commits into
mainfrom
garrytan/monterrey-v3

Conversation

@garrytan
Copy link
Copy Markdown
Owner

@garrytan garrytan commented May 13, 2026

Summary

GStack is now consumable as a submodule. Five new exported helpers + AUTH_TOKEN env injection + import.meta.main gate let downstream Bun projects embed the browse server without forking. Plus 3 security hardening fixes from adversarial review and a TDZ regression bug fix.

The whole release in one screen: see CHANGELOG.md ## [1.34.0.0].

Shipped in 5 commits

Foundation (4 commits):

  • bed1a9f5 config.ts helpers: resolveGstackHome (honors GSTACK_HOME), resolveChromiumProfile(explicit?), cleanSingletonLocks with defensive guard.
  • 4b3bbed2 security-classifier TDZ fix: finish() hoisted above resolveClaudeCommand() + regression test (IRON RULE).
  • fde757cf browser-manager fold-in: isCustomChromium() exported, --load-extension gated, profile via resolveChromiumProfile(), cleanSingletonLocks pre-launch.
  • df6d2a16 server.ts factory-export API: ServerConfig/ServerHandle types, resolveConfigFromEnv(), start() exported, AUTH_TOKEN env injection, import.meta.main gates, cleanSingletonLocks wired into shutdown/emergencyCleanup.

Adversarial hardening (1 commit):

  • 149fe743 fix: harden auth-token validation (unicode-whitespace bypass), TDZ try/catch, lockfile path safety. Three issues caught by /ship adversarial review before merge.

Test Coverage

Pre-merge coverage audit ran as a subagent. 94% effective coverage across the new surface. 3 GAPs deferred — all require real-browser launch (covered by dev smoke).

Symbol / Surface Tests Quality Status
resolveGstackHome() 2 ★★★ COVERED
resolveChromiumProfile(explicit?) 4 ★★★ COVERED
cleanSingletonLocks(userDataDir) 4 ★★★ COVERED
isCustomChromium() predicate 8 ★★★ COVERED
checkTranscript TDZ hoist 1 ★★★ COVERED
AUTH_TOKEN env path / sanitization 6 ★★★ COVERED (includes new BOM + short-token cases)
resolveConfigFromEnv() 6 ★★★ COVERED
ServerConfig / ServerHandle / Surface types 3 ★★★ COVERED
TUNNEL_COMMANDS / canDispatchOverTunnel preserved 2 ★★★ PRESERVED
import.meta.main gate (no auto-start) 1 ★★★ COVERED
launchHeaded extension/profile/lock branches 0 GAP (real-browser; manual smoke OK)
start() export from non-main caller 0 GAP (integration; lands with phoenix)

Tests: 5 new test files + 1 extended. 34 new tests. Final test pass: 96/96 critical tests green.

Pre-Landing Review

0 findings at confidence >= 7. Clean refactor: factory-export API surface, env-injectable AUTH_TOKEN with safe whitespace fallback, import.meta.main gating, shared cleanSingletonLocks helper, TDZ fix with regression test.

Adversarial Review

5 findings from Claude adversarial pass. 3 auto-fixed in commit 149fe743:

# Finding Fix
1 AUTH_TOKEN .trim() only strips ASCII whitespace — BOM (U+FEFF) or zero-width (U+200B) tokens pass as one-character bearer secrets sanitizeAuthToken() strips all unicode whitespace + requires >= 16 chars
2 resolveClaudeCommand() / spawn() can throw — Promise executor rejected with raw exception Wrapped in try/catch, degrades to structured signal
3 cleanSingletonLocks accepted CWD-relative paths and env-controlled paths that bypassed basename guard Requires absolute paths, resolves both sides via path.resolve(), env-match requires absolute CHROMIUM_PROFILE

2 informational findings accepted (substring-match looseness on isCustomChromium and embedder-contract risk in beforeRoute docstring — neither is a boundary violation, both documented for follow-up).

Codex adversarial run skipped this round for time; Codex plan-review was already run during /plan-eng-review and surfaced D9-D13 cross-model tensions resolved in the plan.

Plan Completion

Plan completion audit (subagent): 18 DONE + 5 CHANGED + 0 NOT DONE + 0 UNVERIFIABLE. CHANGED items are the explicitly-deferred buildFetchHandler runtime extraction — documented in the JSDoc at server.ts ~L113 and the commit message of df6d2a16. Phoenix can ship v0.6.0.0 today against the start()+env surface; the cleaner factory lands in a follow-up.

Pre-existing failures (NOT my changes)

test/gstack-next-version.test.ts (CLI runs against real repo and emits parseable JSON) times out at 5s. Verified the same failure exists on origin/main (dc6252d1). Pre-existing — not introduced by this branch. Tracking separately.

TODOS

TODOS.md has 0 items completed by this PR. No new TODOs added — the deferred buildFetchHandler extraction is documented in the plan file and JSDoc, not as a project TODO.

Documentation

No README/CLAUDE.md doc updates needed — diff is internal browse server API for embedders, not user-facing CLI surface.

Test plan

  • 96/96 critical tests pass (bun test browse/test/<critical-files>)
  • bun run build passes (tsc + binary compile)
  • bun run dev goto https://example.com && bun run dev text && bun run dev stop works end-to-end
  • Module import side-effects test proves import 'server.ts' doesn't auto-start (subprocess assert)
  • NEEDS REVIEW: does phoenix's expected import { start } integration actually work? Smoke test on the gbrowser side once landed.

🤖 Generated with Claude Code


View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

garrytan and others added 7 commits May 11, 2026 23:19
…gletonLocks

Three new exported helpers in browse/src/config.ts:

- resolveGstackHome(): honors GSTACK_HOME env, falls back to os.homedir()/.gstack
  Matches the existing convention in browse/src/telemetry.ts:26 and
  browse/src/domain-skills.ts:66.

- resolveChromiumProfile(explicit?): explicit arg wins -> CHROMIUM_PROFILE env
  -> resolveGstackHome()/chromium-profile. Lets gbrowser pass per-workspace
  profile paths through ServerConfig instead of relying on ambient env state.

- cleanSingletonLocks(dir): removes SingletonLock/Socket/Cookie via safeUnlinkQuiet.
  Defensive guard refuses to operate unless dir basename is 'chromium-profile'
  OR matches explicit CHROMIUM_PROFILE env value, preventing accidental
  deletion in unrelated directories.

Extends browse/test/config.test.ts with 12 tests covering env precedence,
guard behavior, ENOENT swallowing, and CHROMIUM_PROFILE override.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The checkTranscript Promise executor in browse/src/security-classifier.ts
referenced `finish()` at the !claude early-return guard before declaring
it 5 lines later. JavaScript throws ReferenceError: Cannot access 'finish'
before initialization (TDZ) for that path, but the path is only reachable
when resolveClaudeCommand returns null inside the spawn block (a TOCTOU
window vs. the outer checkHaikuAvailable cache).

Fix: hoist `let stdout = ''`, `let done = false`, and `const finish` block
above `const claude = resolveClaudeCommand()` so finish is in scope before
any reference to it. Behavior is identical when claude is on PATH; the
fix only matters for the dormant missing-CLI degraded path.

Adds browse/test/security-classifier-tdz.test.ts as the regression guard:
clears PATH + override env vars, calls checkTranscript, asserts the result
serializes with degraded:true and a meaningful reason field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…+ lock cleanup

Three fold-ins so gbrowser can become a thin overlay instead of forking
browse-server:

- Export isCustomChromium(): detects custom Chromium builds that bake the
  extension in as a component extension. Prefers explicit
  GSTACK_CHROMIUM_KIND=custom-extension-baked signal; falls back to
  GSTACK_CHROMIUM_PATH substring containing 'GBrowser' / 'gbrowser'.
  Gates the --load-extension push at launchHeaded so we don't trigger
  ServiceWorkerState::SetWorkerId DCHECK when two copies of the same
  service worker race to register.

- Swap hardcoded path.join(HOME, '.gstack', 'chromium-profile') in
  launchHeaded for resolveChromiumProfile() so phoenix can pass a
  per-workspace profile via CHROMIUM_PROFILE env (one daemon per gbd
  workspace, each with a distinct profile dir).

- Call cleanSingletonLocks(userDataDir) immediately after mkdirSync.
  Chromium's ProcessSingleton refuses to start when stale
  SingletonLock/Socket/Cookie files survive a SIGKILL or hard crash;
  pre-launch cleanup defends against the crash case. Safe under external
  coordination (gbd.lock for gbrowser, single-instance CLI check for
  gstack).

The existing .auth.json write at L291-302 is preserved — extensions
still need it for bootstrap even when component-baked.

Adds browse/test/browser-manager-custom-chromium.test.ts with 8 tests
covering both the env-kind and path-substring signals plus stock /
playwright-bundled Chromium negative cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaces the embedder API gbrowser (phoenix) needs to consume gstack as a
submodule, and gates module-load side effects so the file is safe to
import without auto-starting a daemon.

Changes to browse/src/server.ts:

- AUTH_TOKEN now honors process.env.AUTH_TOKEN (trimmed) before falling
  back to crypto.randomUUID(). Whitespace-only values are rejected so the
  security boundary can't be silently weakened.

- New exported types: ServerConfig and ServerHandle. ServerConfig documents
  the full factory contract (authToken, browsePort, idleTimeoutMs, config,
  browserManager, chromiumProfile, xvfb, proxyBridge, startTime, beforeRoute).
  ServerHandle documents the return shape (fetchLocal, fetchTunnel,
  shutdown, stopListeners). Caller-owned lifecycle annotations on xvfb and
  proxyBridge prevent double-close bugs from surprise ownership.

- New exported function: resolveConfigFromEnv() builds a ServerConfig-shaped
  object from process.env for CLI use. Embedders construct their own
  ServerConfig explicitly.

- start() is now exported. Embedders can call it with env vars set as a
  v1 escape hatch until full buildFetchHandler extraction lands.

- Signal handlers (SIGINT, SIGTERM, Windows exit, uncaughtException,
  unhandledRejection) and the auto-kickoff at module bottom are now wrapped
  in `if (import.meta.main)`. CLI path is unchanged. Embedders register
  their own handlers.

- shutdown() and emergencyCleanup() now call cleanSingletonLocks(
  resolveChromiumProfile()) instead of inline path+loop. Single
  implementation, defensive guard, honors per-workspace CHROMIUM_PROFILE.

New tests:
- browse/test/server-no-import-side-effects.test.ts: spawns a fresh Bun
  subprocess that imports server.ts, asserts no signal handlers registered,
  no state-dir populated. Guards the core refactor invariant from
  regression.
- browse/test/server-factory.test.ts: 12 tests covering AUTH_TOKEN env
  behavior (honored, whitespace-rejected, trimmed), preserved exports
  (TUNNEL_COMMANDS, canDispatchOverTunnel), and ServerConfig/ServerHandle
  type compatibility.

Deferred to follow-up PR: full buildFetchHandler extraction that hoists
the 13 module-level mutables + helpers into a factory closure. Phoenix
can ship v0.6.0.0 against the start()+env surface today; the cleaner
factory comes next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three security hardening fixes from /ship adversarial review:

1. AUTH_TOKEN unicode-whitespace bypass (server.ts:67-83).
   Old: `process.env.AUTH_TOKEN?.trim() || randomUUID()` only stripped
   ASCII whitespace. A misconfigured embedder shipping AUTH_TOKEN=$''
   (BOM) or $'​' (zero-width space) would silently get a
   one-character bearer secret. New `sanitizeAuthToken()` strips all
   unicode whitespace via regex and requires >= 16 chars after stripping;
   anything shorter falls back to crypto.randomUUID(). Same sanitizer
   used by `resolveConfigFromEnv()` so the embedder path is hardened too.

2. security-classifier.ts checkTranscript safety net.
   `resolveClaudeCommand()` and `spawn()` can throw under transient
   conditions (PATH probe failure, posix_spawn ENOMEM). Old code let the
   throw propagate and rejected the Promise with a raw exception. Now
   wrapped in try/catch that calls finish() with a degraded signal,
   matching the graceful-degradation contract the layer already promises
   for missing-CLI / exit-nonzero / parse-error.

3. cleanSingletonLocks defensive guard tightened (config.ts).
   Old: basename === 'chromium-profile' OR userDataDir === $CHROMIUM_PROFILE.
   The second branch was env-controlled and the first was bypassable by
   passing a relative path that resolved to chromium-profile via CWD
   drift. New guard: refuses relative paths outright, resolves both
   sides via path.resolve(), and only accepts the env-match path when
   $CHROMIUM_PROFILE is itself absolute.

Test updates: replace the old `.trim()` test with three new cases
covering unicode-whitespace stripping, short-token rejection, and
zero-width-only rejection (server-factory.test.ts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

E2E Evals: ✅ PASS

6/6 tests passed | $.99 total cost | 12 parallel runners

Suite Result Status Cost
e2e-browse 2/2 $0.14
e2e-deploy 2/2 $0.3
e2e-qa-workflow 1/1 $0.53
llm-judge 1/1 $0.02

12x ubicloud-standard-2 (Docker: pre-baked toolchain + deps) | wall clock ≈ slowest suite

@garrytan garrytan merged commit 0c88517 into main May 13, 2026
23 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant