mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-12 23:43:05 +08:00
Merge origin/main into garrytan/browserharness
Resolves three conflicts: - VERSION: kept 1.19.0.0 (this branch's consolidated Phase 1 + 2a ship); main brought 1.17.0.0 (setup-gbrain wireup) on top of 1.16.0.0 (tunnel allowlist), this branch leaps past with a gap (CLAUDE.md allows version gaps). - package.json: synced to 1.19.0.0 to match VERSION (HEAD had 1.16.0.0 stale from the prior release commit; this fixes the drift). - CHANGELOG.md: kept the v1.19.0.0 entry on top, then main's v1.17.0.0 + v1.16.0.0 entries below. browse/src/server.ts auto-merged: main's TUNNEL_COMMANDS expanded from 17→26 (export const) with the canDispatchOverTunnel pure gate is intact alongside this branch's LOCAL_LISTEN_PORT addition for $B skill run. Verification: 858/0 pass across browser-skill-write + browser-skill-commands + browser-skills-storage + skill-token + dual-listener + tunnel-gate-unit + skill-validation + gen-skill-docs (covers both this branch's Phase 2a and main's tunnel allowlist work). Regenerated all SKILL.md files (--host all) to absorb main's setup-gbrain template changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -116,13 +116,31 @@ const TUNNEL_PATHS = new Set<string>([
|
||||
* extension-inspector state. This allowlist maps to the eng-review decision
|
||||
* logged in the CEO plan for sec-wave v1.6.0.0.
|
||||
*/
|
||||
const TUNNEL_COMMANDS = new Set<string>([
|
||||
export const TUNNEL_COMMANDS = new Set<string>([
|
||||
// Original 17
|
||||
'goto', 'click', 'text', 'screenshot',
|
||||
'html', 'links', 'forms', 'accessibility',
|
||||
'attrs', 'media', 'data',
|
||||
'scroll', 'press', 'type', 'select', 'wait', 'eval',
|
||||
// Tab + navigation primitives operator docs and CLI hints already promised
|
||||
'newtab', 'tabs', 'back', 'forward', 'reload',
|
||||
// Read/inspect/write operators paired agents need to be useful
|
||||
'snapshot', 'fill', 'url', 'closetab',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Pure gate: returns true iff the command is reachable over the tunnel surface.
|
||||
* Extracted from the inline /command handler so the gate logic is unit-testable
|
||||
* without standing up an HTTP listener. Behavior is identical to the inline
|
||||
* check; the function canonicalizes the command (so aliases hit the same set)
|
||||
* and returns false for null/undefined input.
|
||||
*/
|
||||
export function canDispatchOverTunnel(command: string | undefined | null): boolean {
|
||||
if (typeof command !== 'string' || command.length === 0) return false;
|
||||
const cmd = canonicalizeCommand(command);
|
||||
return TUNNEL_COMMANDS.has(cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read ngrok authtoken from env var, ~/.gstack/ngrok.env, or ngrok's native
|
||||
* config files. Returns null if nothing found. Shared between the
|
||||
@@ -1782,8 +1800,7 @@ async function start() {
|
||||
// Paired remote agents drive the browser but cannot configure the
|
||||
// daemon, launch new browsers, import cookies, or rotate tokens.
|
||||
if (surface === 'tunnel') {
|
||||
const cmd = canonicalizeCommand(body?.command);
|
||||
if (!cmd || !TUNNEL_COMMANDS.has(cmd)) {
|
||||
if (!canDispatchOverTunnel(body?.command)) {
|
||||
logTunnelDenial(req, url, `disallowed_command:${body?.command}`);
|
||||
return new Response(JSON.stringify({
|
||||
error: `Command '${body?.command}' is not allowed over the tunnel surface`,
|
||||
@@ -2070,6 +2087,29 @@ async function start() {
|
||||
tunnelListener = null;
|
||||
}
|
||||
}
|
||||
} else if (process.env.BROWSE_TUNNEL_LOCAL_ONLY === '1') {
|
||||
// Test-only: bind the dual-listener tunnel surface on 127.0.0.1 with NO
|
||||
// ngrok forwarding. Lets paid evals exercise the surface==='tunnel' gate
|
||||
// without an ngrok authtoken or live network. Production tunneling still
|
||||
// requires BROWSE_TUNNEL=1 + a valid authtoken above.
|
||||
try {
|
||||
const boundTunnel = Bun.serve({
|
||||
port: 0,
|
||||
hostname: '127.0.0.1',
|
||||
fetch: makeFetchHandler('tunnel'),
|
||||
});
|
||||
tunnelServer = boundTunnel;
|
||||
tunnelActive = true;
|
||||
const tunnelPort = boundTunnel.port;
|
||||
console.log(`[browse] Tunnel listener bound (local-only test mode) on 127.0.0.1:${tunnelPort}`);
|
||||
const stateContent = JSON.parse(fs.readFileSync(config.stateFile, 'utf-8'));
|
||||
stateContent.tunnelLocalPort = tunnelPort;
|
||||
const tmpState = config.stateFile + '.tmp';
|
||||
fs.writeFileSync(tmpState, JSON.stringify(stateContent, null, 2), { mode: 0o600 });
|
||||
fs.renameSync(tmpState, config.stateFile);
|
||||
} catch (err: any) {
|
||||
console.error(`[browse] BROWSE_TUNNEL_LOCAL_ONLY=1 listener bind failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user