Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
29c18e7
fix(codex): use resume-compatible flags
jbetala7 Apr 29, 2026
45c802e
fix: V-001 security vulnerability
orbisai0security Apr 29, 2026
4890dac
docs: align prompt-injection thresholds to security.ts (v1.6.4.0 catc…
brycealan May 1, 2026
4bd6359
fix: Korean/CJK IME input and rendering in Sidebar Terminal
realcarsonterry May 2, 2026
5038900
fix(ship): tighten Plan Completion gate (VAS-449 remediation)
vaskockorovski May 3, 2026
468e94d
fix(browse): bash.exe wrap for telemetry on Windows
scarson May 3, 2026
b0c138c
fix(make-pdf): Bun.which-based binary resolution for browse + pdftote…
scarson May 3, 2026
dd8402c
fix(browse): NTFS ACL hardening for Windows state files via icacls
scarson May 3, 2026
9433790
fix(browse): declare lastConsoleFlushed to restore console-log persis…
yashkot007 May 4, 2026
a1eb6c3
fix(browse): per-process state-file temp path to fix concurrent-write…
yashkot007 May 4, 2026
369c7f2
fix(browse): clear refs when iframe auto-detaches in getActiveFrameOr…
yashkot007 May 4, 2026
c1200b8
fix(codex): resolve python for JSON parser
jbetala7 May 4, 2026
28709c5
fix: add fail-fast probe for base branch in ship step 12
Jasperc2024 May 5, 2026
f68381f
fix(plan-devex-review): remove contradictory plan-mode handshake
Jasperc2024 May 5, 2026
4bdb020
fix(design): honor Retry-After header in variants 429 handler
stedfn May 6, 2026
530987e
fix(docs): correct per-skill symlink removal snippet in README uninstall
stedfn May 6, 2026
e6172f8
fix: reject partial browse client env integers
hiSandog May 6, 2026
c6e1de3
fix(gemini-adapter): detect new ~/.gemini/oauth_creds.json auth path
May 7, 2026
014a51b
fix(browser): add --no-sandbox for root user on Linux/WSL2
furkankoykiran May 7, 2026
d173a65
security: pass cwd to git via execFileSync, not interpolation through…
garagon May 8, 2026
01e5842
security: gate domain-skill auto-promote on classifier_score > 0
garagon May 8, 2026
9756fb8
Merge PR #1309: declare lastConsoleFlushed to restore console-log per…
garrytan May 9, 2026
c7438e0
Merge PR #1310: per-process state-file tempfile path to fix concurren…
garrytan May 9, 2026
9f5c913
Merge PR #1311: clear refs when iframe auto-detaches in getActiveFram…
garrytan May 9, 2026
f4b77d3
Merge PR #1339: reject partial browse client env integers
garrytan May 9, 2026
bc67b93
Merge PR #1366: --no-sandbox for root user on Linux/WSL2 only
garrytan May 9, 2026
7877f28
Merge PR #1306: bash.exe wrap for telemetry on Windows
garrytan May 9, 2026
0292950
Merge PR #1307: Bun.which-based binary resolution for browse + pdftot…
garrytan May 9, 2026
4b11d5a
Merge PR #1308: NTFS ACL hardening for Windows state files via icacls
garrytan May 9, 2026
458b173
Merge PR #1316: resolve Python before JSON parsing in codex skill
garrytan May 9, 2026
9cffb15
Merge PR #1270: codex exec resume drops -C/-s flags, uses sandbox_mod…
garrytan May 9, 2026
9940235
Merge PR #1368: pass cwd to git via execFileSync, not interpolation t…
garrytan May 9, 2026
8529aee
Merge PR #1369: gate domain-skill auto-promote on classifier_score > 0
garrytan May 9, 2026
e36bf7e
Merge PR #1273: remove ~/.gstack/openai.json fallback in design/proto…
garrytan May 9, 2026
8e6008e
Merge PR #1337: honor Retry-After header in design variants 429 handler
garrytan May 9, 2026
c49035c
Merge PR #1362: detect new ~/.gemini/oauth_creds.json auth path
garrytan May 9, 2026
632529c
Merge PR #1332: fail-fast probe for base branch in /ship step 12
garrytan May 9, 2026
1722f65
Merge PR #1302: tighten /ship Plan Completion gate
garrytan May 9, 2026
d45e124
fix(ship): port #1302 SKILL.md edits to .tmpl + resolver source
garrytan May 9, 2026
b02fd97
Merge PR #1333: remove contradictory plan-mode handshake from /plan-d…
garrytan May 9, 2026
9f17e9b
Merge PR #1297: Korean/CJK IME input and rendering in Sidebar Terminal
garrytan May 9, 2026
1875696
Merge PR #1338: correct per-skill symlink removal snippet in README u…
garrytan May 9, 2026
4fa7704
Merge PR #1290: align prompt-injection thresholds in CLAUDE.md and AR…
garrytan May 9, 2026
e259ec1
ci(windows): extend free-tests lane to cover icacls + Bun.which resol…
garrytan May 9, 2026
08c6e07
test(codex): live flag-semantics smoke for codex exec resume
garrytan May 9, 2026
31243d6
chore: regen SKILL.md after fix wave
garrytan May 9, 2026
6591b02
fix(server.ts): keep fs.writeFileSync for state-file writes
garrytan May 9, 2026
2944c31
v1.30.0.0: fix wave — 21 community PRs + 2 closing fixes for Windows …
garrytan May 9, 2026
4b89406
test(domain-skills): cover #1369 classifier_score=0 quarantine + scor…
garrytan May 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions .github/workflows/windows-free-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,17 @@ jobs:
continue-on-error: true

- name: Verify new portability work on Windows
# 31 tests targeting the new code paths added by v1.20.0.0. These
# MUST pass for the release-note headline ("curated Windows lane added")
# to be truthful.
run: bun test test/gstack-paths.test.ts browse/test/claude-bin.test.ts test/test-free-shards.test.ts
# Tests targeting the v1.20.0.0 lane plus v1.30.0.0 fix-wave additions.
# v1.30.0.0 extension covers icacls hardening (#1308), bash.exe telemetry
# wrap (#1306), and Bun.which-based binary resolvers (#1307). These must
# pass on Windows for the wave's "Windows hardening" framing to be honest.
run: |
bun test \
test/gstack-paths.test.ts \
browse/test/claude-bin.test.ts \
test/test-free-shards.test.ts \
browse/test/file-permissions.test.ts \
browse/test/security.test.ts \
make-pdf/test/browseClient.test.ts \
make-pdf/test/pdftotext.test.ts
shell: bash
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ The Chrome sidebar agent has tools (Bash, Read, Glob, Grep, WebFetch) and reads

4. **L5 canary token (`browse/src/security.ts`).** A random token injected into the system prompt at session start. Rolling-buffer detection across `text_delta` and `input_json_delta` streams catches the token if it shows up anywhere in Claude's output, tool arguments, URLs, or file writes. Deterministic BLOCK — if the token leaks, the attacker convinced Claude to reveal the system prompt, and the session ends.

5. **L6 ensemble combiner (`combineVerdict`).** BLOCK requires agreement from two ML classifiers at >= `WARN` (0.60), not a single confident hit. This is the Stack Overflow instruction-writing false-positive mitigation. On tool-output scans, single-layer high confidence BLOCKs directly — the content wasn't user-authored, so the FP concern doesn't apply.
5. **L6 ensemble combiner (`combineVerdict`).** BLOCK requires agreement from two ML classifiers at >= `WARN` (0.75), not a single confident hit. This is the Stack Overflow instruction-writing false-positive mitigation. On tool-output scans, single-layer high confidence BLOCKs directly — the content wasn't user-authored, so the FP concern doesn't apply.

**Critical constraint:** `security-classifier.ts` runs only in the sidebar-agent process, never in the compiled browse binary. `@huggingface/transformers` v4 requires `onnxruntime-node`, which fails `dlopen` from Bun compile's temp extract directory. Only the pure-string pieces (canary inject/check, verdict combiner, attack log, status) are in `security.ts`, which is safe to import from `server.ts`.

Expand Down
74 changes: 74 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,79 @@
# Changelog

## [1.30.0.0] - 2026-05-09

## **Twenty-one community fixes land in one wave, plus closing fixes that put the Windows + codex surfaces under CI for the first time.**

Browse stops silently dropping `browse-console.log` writes (a regression from a missing variable declaration), the cold-start race that ENOENT'd one of every fifteen parallel daemons gets a per-process tempfile, and concurrent iframe detach finally clears refs symmetrically with main-frame nav. `codex exec resume` works on machines that ship `python` without the `python3` alias, and stops passing the `-C` and `-s` flags that the resume subcommand rejects. Windows users get bash.exe wrap for telemetry spawn, `Bun.which` binary resolution that finds `.exe`/`.cmd`/`.bat` instead of bare paths, and NTFS ACL hardening on every file written to `~/.gstack/`. Two closing fixes land alongside: `windows-free-tests.yml` now exercises the icacls + Bun.which test files (closing the gap codex's outside-voice review flagged in the plan), and a live `codex exec resume --help` smoke catches CLI flag-semantics drift that the existing regex-only test would have missed.

### The numbers that matter

End-to-end verified via `bun test` (free tier, 452 tests pass) and gate-tier E2E:

| Surface | Before | After | Δ |
|---|---|---|---|
| Browse `console.log` persistence | swallowed every 1s flush due to `lastConsoleFlushed` ReferenceError | declared, persisted to disk | regression closed |
| Concurrent daemon cold-start | shared `state.tmp` raced rename, killed 1 in N spawns | per-process `tmpStatePath()` (pid + 4 random bytes) | no more ENOENT |
| Iframe detach handling | refs leaked when iframe auto-detached (asymmetric with main-frame nav) | refs cleared symmetrically | parity fix |
| `codex exec resume` flag set | `-C "$_REPO_ROOT" -s read-only` (rejected by the resume subcommand) | `-c 'sandbox_mode="read-only"'` + `cd "$_REPO_ROOT"` | works without warnings |
| Codex JSON parsing | hardcoded `python3`; broke on machines with only `python` | probes `python3` then `python`, errors clearly if neither | works on more machines |
| Windows browse / make-pdf binary resolution | bare-path probe missed `.exe`/`.cmd`/`.bat` | `Bun.which` + `GSTACK_*_BIN` override + extension probing | works on Windows installs |
| Windows state-file hardening | POSIX `0o600` mode bits no-op'd on NTFS | icacls inheritance break + grant-only ACL on every `~/.gstack/` write | actual hardening, not silent no-op |
| Windows telemetry spawn | `spawn(bash-script)` ENOENT'd silently on Windows (`CreateProcess` rejects shebangs) | bash.exe wrap with PATH / `GSTACK_BASH_BIN` override | telemetry events captured on Windows |
| Domain-skill auto-promote | promoted regardless of classifier_score | gated on `classifier_score > 0` | adversarially-flagged domains stay quarantined |
| Shell-injection surface in memory ingest | git cwd interpolated through `/bin/sh` | `execFileSync` with cwd as a parameter | one less injection path |
| Windows free-tests CI coverage | 3 test files (claude-bin, gstack-paths, test-shards) | 7 test files (+ icacls, security telemetry, browseClient, pdftotext) | 4 new surfaces under CI |
| Codex CLI flag-semantics test | regex-only on SKILL.md text | live `codex exec resume --help` smoke (skips when codex absent) | catches upstream flag drift |

PR count: 21 community merges + 4 in-house follow-up commits (#1302 template port, CL-1 Windows CI extension, CL-2 codex flag smoke, server.ts conflict-resolution fix). Contributors credited: 13 unique authors. Test count went from 452 → 459 (4 new tests from the merged PRs + 3 from CL-1/CL-2 invariants).

### What this means for builders

If you're on a Windows install, this is the release where `~/.gstack/` is actually access-restricted (icacls grants), browse and make-pdf find the right `.exe`, and bash-shebang telemetry stops dropping on the floor. Set `GSTACK_BROWSE_BIN` / `GSTACK_PDFTOTEXT_BIN` / `GSTACK_BASH_BIN` to override. If you use the `/codex` skill, resume sessions work on machines with only `python` and no `python3`, and the rejected `-C/-s` flags are gone. If you spawn multiple browse daemons in parallel (CI shards, cold-start races, multi-tab Conductor), the per-process tempfile fix means rename no longer steals the file out from under a sibling. Run `gbrain autopilot --install` once and forget about it.

### Itemized changes

#### Added

- **#1306** Windows bash.exe wrap for telemetry spawn (`browse/src/security.ts`). Honors `GSTACK_BASH_BIN` / `BASH_BIN` env override, falls back to `Bun.which('bash')` (finds Git Bash on standard Windows installs). Returns null when bash is unresolvable so caller skips the spawn cleanly. Contributed by @scarson.
- **#1307** `Bun.which`-based binary resolution for `make-pdf/src/browseClient.ts` and `make-pdf/src/pdftotext.ts`. Probes `.exe`/`.cmd`/`.bat` after a bare-path miss on Windows; honors `GSTACK_BROWSE_BIN` / `GSTACK_PDFTOTEXT_BIN` overrides. Extends the v1.24 pattern from `claude-bin.ts` to the other two binary resolvers. Contributed by @scarson.
- **#1308** NTFS ACL hardening for `~/.gstack/` state files (`browse/src/file-permissions.ts` is the new helper). `writeSecureFile` and `mkdirSecure` invoke `icacls /inheritance:r /grant:r <user>:(F)` on Windows; POSIX `chmod 0o600` continues working unchanged. First icacls failure per process is logged once with the advice line "sensitive files may be readable by other accounts on this machine"; later failures stay silent to avoid spam. Contributed by @scarson.
- **#1316** Python3-or-python probe in `codex/SKILL.md.tmpl`. Resolves `python3` then `python`, errors clearly if neither is on PATH. Contributed by @jbetala7.
- **#1339** Strict integer validation in `browse/src/browse-client.ts` env handling. Partial integers now throw rather than silently truncating. Contributed by @hiSandog.
- **#1369** `classifier_score > 0` gate on domain-skill auto-promote (`browse/src/domain-skills.ts:248-320`). Quarantined domains stay quarantined even if every other heuristic says promote. Contributed by @garagon.
- **CL-1** Windows free-tests CI lane now runs `browse/test/file-permissions.test.ts`, `browse/test/security.test.ts`, `make-pdf/test/browseClient.test.ts`, and `make-pdf/test/pdftotext.test.ts`. The four test files already platform-gate their assertions via `process.platform`, so the same files run on POSIX and Windows lanes and exercise only the relevant branch.
- **CL-2** Live codex CLI flag-semantics smoke (`test/codex-resume-flag-semantics.test.ts`). Probes `codex exec resume --help` for `-c`/`sandbox_mode` presence and top-level `-C` absence; skips when codex isn't on PATH so dev machines without codex installed never see it fail.

#### Changed

- **#1270** `codex exec resume` invocation in `codex/SKILL.md.tmpl` drops `-C "$_REPO_ROOT"` and `-s read-only` (the resume subcommand rejects both), uses `-c 'sandbox_mode="read-only"'` config and `cd "$_REPO_ROOT"` instead. Adds the regression test `codex/SKILL.md resume command only uses resume-supported flags`. Contributed by @jbetala7.
- **#1273** `design/prototype.ts` (the prototype script only — the main design CLI is unchanged) reads the OpenAI key only from `OPENAI_API_KEY`. Output filenames sanitized to `[a-zA-Z0-9_-]` only. The `~/.gstack/openai.json` file fallback is removed from the prototype script; `design/src/auth.ts` and `design/src/cli.ts` still support it for the main CLI flow. Contributed by @orbisai0security.
- **#1302** /ship Plan Completion gate (`ship/SKILL.md.tmpl` + `scripts/resolvers/review.ts`) adds Verification Mode classification (DIFF-VERIFIABLE / CROSS-REPO / EXTERNAL-STATE / CONTENT-SHAPE), the UNVERIFIABLE classification, per-item confirmation gate (no blanket-confirm AskUserQuestion), and explicit fail-closed behavior on subagent failure. Forbids the silent-fail-open path that produced the VAS-449 incident shape. Contributed by @vaskockorovski.
- **#1332** /ship step 12 fail-fast probe for the base branch in `ship/SKILL.md.tmpl`. Prevents step 12 from running against an unresolvable base. Contributed by @Jasperc2024.
- **#1337** `design/src/variants.ts` honors the `Retry-After` header on 429 responses. Prevents thundering-herd retries against rate-limited endpoints. Contributed by @stedfn.
- **#1362** `test/helpers/providers/gemini.ts` detects the new `~/.gemini/oauth_creds.json` auth path alongside the legacy location. Contributed by @abigail-atheryon.
- **#1366** `browse/src/browser-manager.ts` adds `--no-sandbox` only when running as root (Linux/WSL2), not unconditionally. Contributed by @furkankoykiran.
- **#1368** `bin/gstack-memory-ingest.ts` passes git cwd via `execFileSync` parameter rather than interpolating into a `/bin/sh` invocation. One less shell-injection class. Contributed by @garagon.

#### Fixed

- **#1309** Missing `let lastConsoleFlushed = 0;` declaration in `browse/src/server.ts`. Every 1-second `flushBuffers` tick was throwing a swallowed ReferenceError; `browse-console.log` was never written in any production deployment since this regressed. Contributed by @yashkot007.
- **#1310** Per-process `tmpStatePath()` for state-file writes in `browse/src/server.ts`. The shared `state.tmp` literal raced on rename when concurrent daemons spawned (15-parallel cold-start reproducer). pid + 4 random bytes of suffix gives each writer a unique path; atomic rename still gives last-writer-wins on the final state. Contributed by @yashkot007.
- **#1311** `getActiveFrameOrPage` in `browse/src/tab-session.ts` clears refs symmetrically when an iframe auto-detaches, matching the existing main-frame nav path. Contributed by @yashkot007.
- **#1297** Korean / CJK IME input rendering in the Sidebar Terminal (`extension/sidepanel-terminal.js`, `browse/src/terminal-agent.ts`, `extension/sidepanel.css`). Composition state preserved, character widths corrected. Contributed by @realcarsonterry.
- **#1333** Removed the contradictory plan-mode handshake from `plan-devex-review/SKILL.md.tmpl` (the skill was simultaneously claiming plan-mode is active and asking the user to confirm entering plan-mode). Contributed by @Jasperc2024.

#### Documentation

- **#1290** `CLAUDE.md` and `ARCHITECTURE.md` prompt-injection thresholds aligned to the actual values in `browse/src/security.ts` (BLOCK 0.85, WARN 0.60, LOG_ONLY 0.40 — the docs had drifted to older numbers). Contributed by @brycealan.
- **#1338** README per-skill symlink uninstall snippet corrected (the previous wording would `rm` the global skills directory rather than the project-local symlink). Contributed by @stedfn.

#### For contributors

- The wave was triaged by `/plan-ceo-review` (single-wave + bisect-discipline merge ordering), `/plan-eng-review` (mapped 5 cross-PR conflict pairs with explicit resolution rules + tightened the `gh pr checkout N -b pr-N` syntax), and `/codex` outside-voice review (caught 6 factual errors and 2 process improvements that both internal reviews missed; cross-model agreement was 14%). All review findings were incorporated before merge; the two CI gaps codex flagged became the CL-1 and CL-2 closing fixes that ship in this same release.
- The five cross-PR conflict pairs documented in the plan (#1316↔#1270 codex resume line, #1309→#1310→#1308 server.ts state writes, #1366↔#1308 browser-manager, #1306↔#1308 security.ts, #1332↔#1302 ship template) all surfaced as predicted; resolutions kept both intents on each. The lone exception was the #1310/#1308 state-file write site, where `fs.writeFileSync(tmpStatePath(), ..., { mode: 0o600 })` is preserved (locks #1310's race-fix invariant exercised by `browse/test/server-tmp-state-path.test.ts`); icacls hardening still applies to every other `writeSecureFile` call site #1308 introduced (`auth.json`, the `mkdirSecure` paths, etc.).
- PR #1302 only edited the generated `ship/SKILL.md`, not the source `ship/SKILL.md.tmpl` or `scripts/resolvers/review.ts`. The next `bun run gen:skill-docs` would have wiped its changes; the wave includes `fix(ship): port #1302 SKILL.md edits to .tmpl + resolver source` to keep the changes alive across regen.

## [1.29.0.0] - 2026-05-08

## **Code search beats Grep across every Conductor worktree now, not just the last one you synced.**
Expand Down
6 changes: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,12 @@ for `server.ts`. See `~/.gstack/projects/garrytan-gstack/ceo-plans/2026-04-19-pr

**Thresholds** (in `security.ts`):
- `BLOCK: 0.85` — single-layer score that would cause BLOCK if cross-confirmed
- `WARN: 0.60` — cross-confirm threshold. When L4 AND L4b both >= 0.60 → BLOCK
- `WARN: 0.75` — cross-confirm threshold. When L4 AND L4b both >= 0.75 → BLOCK
- `LOG_ONLY: 0.40` — gates transcript classifier (skip Haiku when all layers < 0.40)
- `SOLO_CONTENT_BLOCK: 0.92` — single-layer threshold for label-less content classifiers
(testsavant, deberta). Intentionally higher than `BLOCK` because these layers can't
distinguish "this is an injection" from "this looks like phishing aimed at the user."
The transcript classifier keeps a separate, label-gated solo path at `BLOCK` (0.85).

**Ensemble rule:** BLOCK only when the ML content classifier AND the transcript
classifier both report >= WARN. Single-layer high confidence degrades to WARN —
Expand Down
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,9 +327,18 @@ If you don't have the repo cloned (e.g. you installed via a Claude Code paste an
# 1. Stop browse daemons
pkill -f "gstack.*browse" 2>/dev/null || true

# 2. Remove per-skill symlinks pointing into gstack/
find ~/.claude/skills -maxdepth 1 -type l 2>/dev/null | while read -r link; do
case "$(readlink "$link" 2>/dev/null)" in gstack/*|*/gstack/*) rm -f "$link" ;; esac
# 2. Remove per-skill directories whose SKILL.md points into gstack/
find ~/.claude/skills -mindepth 1 -maxdepth 1 -type d ! -name gstack 2>/dev/null |
while IFS= read -r dir; do
link="$dir/SKILL.md"
[ -L "$link" ] || continue
target=$(readlink "$link" 2>/dev/null) || continue
case "$target" in
gstack/*|*/gstack/*)
rm -f "$link"
rmdir "$dir" 2>/dev/null || true
;;
esac
done

# 3. Remove gstack
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.29.0.0
1.30.0.0
9 changes: 8 additions & 1 deletion bin/gstack-memory-ingest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,9 +632,16 @@ function extractContentText(rec: any): string {
function resolveGitRemote(cwd: string): string {
if (!cwd) return "";
try {
const out = execSync(`git -C ${JSON.stringify(cwd)} remote get-url origin 2>/dev/null`, {
// execFileSync (no shell) so `cwd` cannot trigger command substitution.
// Transcript JSONL records are an untrusted surface (a poisoned `.cwd`
// value containing `"$(...)"` survived `JSON.stringify` interpolation
// into a `/bin/sh -c` context, since JSON quoting does not escape `$`
// or backticks). Mirrors the execFileSync pattern this module already
// uses for `gbrainAvailable()` (line 762) and `gbrainPutPage()` (line 816).
const out = execFileSync("git", ["-C", cwd, "remote", "get-url", "origin"], {
encoding: "utf-8",
timeout: 2000,
stdio: ["ignore", "pipe", "ignore"],
});
return canonicalizeRemote(out.trim());
} catch {
Expand Down
13 changes: 10 additions & 3 deletions browse/src/browse-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ interface ResolvedAuth {
source: 'env' | 'state-file';
}

function parseIntegerEnvValue(value: string | undefined): number | undefined {
const trimmed = value?.trim();
if (!trimmed || !/^-?\d+$/.test(trimmed)) return undefined;
const parsed = parseInt(trimmed, 10);
return Number.isFinite(parsed) ? parsed : undefined;
}

/** Resolve the daemon port + token. Throws a clear error if neither path works. */
export function resolveBrowseAuth(opts: BrowseClientOptions = {}): ResolvedAuth {
if (opts.port !== undefined && opts.token !== undefined) {
Expand All @@ -64,8 +71,8 @@ export function resolveBrowseAuth(opts: BrowseClientOptions = {}): ResolvedAuth
const envPort = process.env.GSTACK_PORT;
const envToken = process.env.GSTACK_SKILL_TOKEN;
if (envPort && envToken) {
const port = opts.port ?? parseInt(envPort, 10);
if (!isNaN(port)) {
const port = opts.port ?? parseIntegerEnvValue(envPort);
if (port !== undefined) {
return { port, token: opts.token ?? envToken, source: 'env' };
}
}
Expand Down Expand Up @@ -132,7 +139,7 @@ export class BrowseClient {
const auth = resolveBrowseAuth(opts);
this.port = auth.port;
this.token = auth.token;
this.tabId = opts.tabId ?? (process.env.BROWSE_TAB ? parseInt(process.env.BROWSE_TAB, 10) : undefined);
this.tabId = opts.tabId ?? parseIntegerEnvValue(process.env.BROWSE_TAB);
this.timeoutMs = opts.timeoutMs ?? 30_000;
}

Expand Down
Loading
Loading