feat(browser-skills): \$B skill list/show/run/test/rm subcommands

handleSkillCommand dispatches to per-subcommand handlers; spawnSkill is
the load-bearing function that:

  1. Mints a per-spawn scoped token (read+write only) bound to the
     skill name + spawn-id.
  2. Builds the spawn env:
     - trusted: passes process.env minus GSTACK_TOKEN (defense in depth).
     - untrusted: minimal allowlist (LANG, LC_ALL, TERM, TZ) + locked
       PATH; explicitly drops anything matching TOKEN/KEY/SECRET/etc.
       Also drops AWS_/AZURE_/GCP_/GOOGLE_APPLICATION_/ANTHROPIC_/OPENAI_/
       GITHUB_/GH_/SSH_/GPG_/NPM_TOKEN/PYPI_ patterns.
   3. Always injects GSTACK_PORT + GSTACK_SKILL_TOKEN last (cannot be
     overridden by parent env).
  4. Spawns bun run script.ts -- <args> with cwd=skillDir, captures
     stdout (1MB cap), stderr, and timeout-kills past the deadline.
  5. Revokes the token in finally{}, always.

list output prints the resolved tier inline so "why did it run that
one?" never becomes a debugging mystery (Codex finding #4 mitigation).

server.ts threads the listen port to meta-commands via MetaCommandOpts.daemonPort.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-04-26 05:03:17 -07:00
parent faf663b22c
commit e6a27993fe
5 changed files with 796 additions and 0 deletions

View File

@@ -71,6 +71,14 @@ const AUTH_TOKEN = crypto.randomUUID();
initRegistry(AUTH_TOKEN);
const BROWSE_PORT = parseInt(process.env.BROWSE_PORT || '0', 10);
const IDLE_TIMEOUT_MS = parseInt(process.env.BROWSE_IDLE_TIMEOUT || '1800000', 10); // 30 min
/**
* Port the local listener bound to. Set once the daemon picks a port.
* Used by `$B skill run` to point spawned skill scripts at the daemon over
* loopback. Module-level so handleCommandInternal can read it without threading
* the port through every dispatch.
*/
let LOCAL_LISTEN_PORT: number = 0;
// Sidebar chat is always enabled in headed mode (ungated in v0.12.0)
// ─── Tunnel State ───────────────────────────────────────────────
@@ -1286,6 +1294,7 @@ async function handleCommandInternal(
const chainDepth = (opts?.chainDepth ?? 0);
result = await handleMetaCommand(command, args, browserManager, shutdown, tokenInfo, {
chainDepth,
daemonPort: LOCAL_LISTEN_PORT,
executeCommand: (body, ti) => handleCommandInternal(body, ti, {
skipRateCheck: true, // chain counts as 1 request
skipActivity: true, // chain emits 1 event for all subcommands
@@ -1571,6 +1580,7 @@ async function start() {
safeUnlink(DIALOG_LOG_PATH);
const port = await findPort();
LOCAL_LISTEN_PORT = port;
// Launch browser (headless or headed with extension)
// BROWSE_HEADLESS_SKIP=1 skips browser launch entirely (for HTTP-only testing)