mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 07:18:03 +01:00
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
b257eff2ba
commit
83193721d3
8 files changed
+74
-6
No files matched your search
@@ -1,5 +1,17 @@
|
||||
# Changelog
|
||||
|
||||
## [1.93.30.0] - 2026-11-03
|
||||
|
||||
**`browse` starts again when proxy credentials come from `BROWSE_PROXY_USER` and `BROWSE_PROXY_PASS`.** With those two variables set beside `--proxy`, `BROWSE_PROXY_URL` or a standard proxy variable such as `HTTPS_PROXY`, the daemon exited at startup with `proxy creds set in both env (BROWSE_PROXY_USER) and URL — pick one source`, and the CLI reported only `Server failed to start within 15s`. The CLI puts those credentials into the proxy URL it hands the daemon, and the daemon also inherited the two variables, so it saw the same credentials twice and refused them. The daemon now gets the variables blanked whenever the CLI passes it a resolved proxy URL, so it reads the credentials once, from the URL. Setting credentials both in the URL and in the variables is still refused, with the same message, before any daemon starts.
|
||||
|
||||
### Fixed
|
||||
- **`browse/src/cli.ts`**: the daemon environment for a resolved proxy sets `BROWSE_PROXY_USER` and `BROWSE_PROXY_PASS` to empty. This covers a normal start, an auto-restart, a headed `connect` and its supervisor respawn, and the Windows launcher (#3457).
|
||||
|
||||
### Tests
|
||||
- `browse/test/restart-env.test.ts`: for `--proxy`, `BROWSE_PROXY_URL` and `HTTPS_PROXY` with `BROWSE_PROXY_USER`/`PASS`, the restart and headed daemon environments parse the way `server.ts` parses them and yield the credentials once. All three failed before the fix with the "pick one source" error.
|
||||
- `browse/test/proxy-env-daemon.test.ts`: an end-to-end start with `HTTP_PROXY` and `BROWSE_PROXY_USER`/`PASS` against a local proxy that demands Basic auth loads the page through the proxy. It timed out at the 15 s start limit before the fix.
|
||||
- `browse/test/cli-supervisor.test.ts`: the expected headed respawn environment includes the two blanked variables.
|
||||
|
||||
## [1.93.29.0] - 2026-11-03
|
||||
|
||||
**A `gbrain serve --http` on this machine's loopback address now counts as a local brain, so transcripts get imported again.** gbrain recommends this setup for local agents on a Postgres brain (`gbrain bootstrap harness`). gstack treated every http MCP entry as a brain on another machine (Path 4). So `/sync-gbrain` staged transcripts for a brain admin who doesn't exist and never ran `gbrain import` (#3539). Ported from Willy Morin's #3552.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gstack digest v1.93.29.0 — regenerate/re-copy after upgrading gstack
|
||||
# gstack digest v1.93.30.0 — regenerate/re-copy after upgrading gstack
|
||||
|
||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
||||
for agent hosts without a full skill install. The full skills add workflows,
|
||||
|
||||
+5
-1
@@ -523,12 +523,16 @@ export function proxyMismatchLines(flags: GlobalFlags, state: ServerState, env:
|
||||
* The server env for this invocation's resolved proxy (#915); empty when
|
||||
* direct, except BROWSE_PROXY_URL=direct where that bypasses the system proxy
|
||||
* settings (#3440), so a daemon start or auto-restart carries the opt-out the
|
||||
* session's hash was computed from.
|
||||
* session's hash was computed from. The resolved URL already carries any
|
||||
* BROWSE_PROXY_USER/PASS, so those are blanked for the daemon, which inherits
|
||||
* this environment and would otherwise see the creds twice (#3457).
|
||||
*/
|
||||
function proxyServerEnv(flags: Partial<Pick<GlobalFlags, 'proxyUrl' | 'proxySource' | 'proxyBypass' | 'explicitDirect'>> | null | undefined): Record<string, string> {
|
||||
if (!flags?.proxyUrl) return flags?.explicitDirect ? { BROWSE_PROXY_URL: 'direct' } : {};
|
||||
return {
|
||||
BROWSE_PROXY_URL: flags.proxyUrl,
|
||||
BROWSE_PROXY_USER: '',
|
||||
BROWSE_PROXY_PASS: '',
|
||||
...(flags.proxySource ? { BROWSE_PROXY_SOURCE: flags.proxySource } : {}),
|
||||
...(flags.proxyBypass ? { BROWSE_PROXY_BYPASS: flags.proxyBypass } : {}),
|
||||
};
|
||||
|
||||
@@ -124,7 +124,7 @@ describe('runHeadedSupervisor (behavior)', () => {
|
||||
expect(h.calls.startEnv).toHaveLength(1);
|
||||
expect(h.calls.startEnv[0]).toEqual({
|
||||
BROWSE_HEADED: '1', BROWSE_PORT: '34567', BROWSE_PARENT_PID: '0',
|
||||
BROWSE_PROXY_URL: 'socks5://127.0.0.1:9050', BROWSE_CONFIG_HASH: 'abc123',
|
||||
BROWSE_PROXY_URL: 'socks5://127.0.0.1:9050', BROWSE_PROXY_USER: '', BROWSE_PROXY_PASS: '', BROWSE_CONFIG_HASH: 'abc123',
|
||||
});
|
||||
expect(h.calls.startEnv[0]).toBe(h.deps.env);
|
||||
expect(h.calls.agents).toEqual([5000]);
|
||||
|
||||
@@ -14,6 +14,8 @@ import * as path from 'node:path';
|
||||
|
||||
const CLI = path.resolve(import.meta.dir, '../src/cli.ts');
|
||||
const PROXIED_HOST = 'proxied.gstack-915.test';
|
||||
const AUTH_HOST = 'auth.gstack-3457.test';
|
||||
const AUTH_HEADER = `Basic ${Buffer.from('proxy-user:proxy-secret').toString('base64')}`;
|
||||
|
||||
let scratch = '';
|
||||
let stateFile = '';
|
||||
@@ -27,6 +29,11 @@ beforeAll(async () => {
|
||||
stateFile = path.join(scratch, 'state', 'browse.json');
|
||||
proxy = http.createServer((req, res) => {
|
||||
const host = new URL(req.url ?? '/', 'http://unknown').hostname;
|
||||
if (host === AUTH_HOST && req.headers['proxy-authorization'] !== AUTH_HEADER) {
|
||||
res.writeHead(407, { 'Proxy-Authenticate': 'Basic realm="gstack-3457"' });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
proxiedHosts.push(host);
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.end(`<!doctype html><html><body><p>via proxy for ${host}</p></body></html>`);
|
||||
@@ -177,4 +184,15 @@ describe.skipIf(process.platform === 'win32')('#915 proxy from the environment,
|
||||
}
|
||||
expect(daemonPid()).toBe(pid);
|
||||
}, 120_000);
|
||||
|
||||
test('BROWSE_PROXY_USER/PASS with HTTP_PROXY: the daemon starts and Chromium answers the proxy auth challenge (#3457)', async () => {
|
||||
expect((await browse(['stop'], {})).code).toBe(0);
|
||||
const env = { HTTP_PROXY: proxyUrl, BROWSE_PROXY_USER: 'proxy-user', BROWSE_PROXY_PASS: 'proxy-secret' };
|
||||
const r = await browse(['goto', `http://${AUTH_HOST}/`], env);
|
||||
expect(r.code, r.out).toBe(0);
|
||||
expect(r.stderr).toContain(`Starting server with proxy ${proxyUrl.replace('http://', 'http://***:***@')}/ from HTTP_PROXY...`);
|
||||
expect(r.out).not.toContain('proxy-secret');
|
||||
expect(proxiedHosts).toContain(AUTH_HOST);
|
||||
expect((await browse(['text'], env)).stdout).toContain(`via proxy for ${AUTH_HOST}`);
|
||||
}, 120_000);
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { buildRestartEnv } from "../src/cli";
|
||||
import { buildHeadedServerEnv, buildRestartEnv, extractGlobalFlags } from "../src/cli";
|
||||
import { parseProxyConfig, serverProxyEnv } from "../src/proxy-config";
|
||||
|
||||
// #1781: an auto-restart triggered by a plain command (no --headed flag) must
|
||||
// NOT silently downgrade a headed session to headless. buildRestartEnv reapplies
|
||||
@@ -48,3 +49,36 @@ describe("buildRestartEnv (#1781 headed persistence)", () => {
|
||||
expect(env.BROWSE_CONFIG_HASH).toBe("fromstate");
|
||||
});
|
||||
});
|
||||
|
||||
// #3457: the CLI folds BROWSE_PROXY_USER/PASS into the BROWSE_PROXY_URL it hands
|
||||
// the daemon, and the daemon inherits the CLI's environment. The daemon parses
|
||||
// that URL with the env creds exactly as server.ts does, so it must see one source.
|
||||
describe("daemon env carries one proxy credential source (#3457)", () => {
|
||||
const userEnv = { BROWSE_PROXY_USER: "a", BROWSE_PROXY_PASS: "b" };
|
||||
const daemonParse = (env: Record<string, string>) => {
|
||||
const { url, source } = serverProxyEnv(env, "linux");
|
||||
return parseProxyConfig({ proxyUrl: url!, envUser: env.BROWSE_PROXY_USER, envPass: env.BROWSE_PROXY_PASS, source });
|
||||
};
|
||||
const spellings: Array<[string, string[], Record<string, string>]> = [
|
||||
["--proxy", ["--proxy", "http://127.0.0.1:3128", "status"], {}],
|
||||
["BROWSE_PROXY_URL", ["status"], { BROWSE_PROXY_URL: "http://127.0.0.1:3128" }],
|
||||
["HTTPS_PROXY", ["status"], { HTTPS_PROXY: "http://127.0.0.1:3128" }],
|
||||
];
|
||||
|
||||
for (const [name, args, proxyEnv] of spellings) {
|
||||
test(`${name} with BROWSE_PROXY_USER/PASS: start, restart and headed daemons parse the creds once`, () => {
|
||||
const env = { ...userEnv, ...proxyEnv };
|
||||
const flags = extractGlobalFlags(args, env, "linux");
|
||||
for (const childEnv of [buildRestartEnv(flags, null), buildHeadedServerEnv(flags)]) {
|
||||
const parsed = daemonParse({ ...env, ...childEnv });
|
||||
expect(parsed).toMatchObject({ host: "127.0.0.1", port: 3128, userId: "a", password: "b" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test("without a resolved proxy the daemon env leaves BROWSE_PROXY_USER/PASS alone", () => {
|
||||
const env = buildRestartEnv(extractGlobalFlags(["status"], userEnv, "linux"), null);
|
||||
expect(env.BROWSE_PROXY_USER).toBeUndefined();
|
||||
expect(env.BROWSE_PROXY_PASS).toBeUndefined();
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gstack",
|
||||
"version": "1.93.29",
|
||||
"version": "1.93.30",
|
||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
Reference in new issue
Block a user