Files
gstack/scripts/host-config-export.ts
T
Garry Tanandcapy-ai[bot] 4c6fa89cdc v1.91.159.0 security: gstack's Bun entry points ignore the audited repo's .env and bunfig.toml (S1) (#3312)
* security: isolate every gstack Bun entry point from the audited checkout's .env and bunfig.toml

Bun auto-loads .env, .env.local and .env.<NODE_ENV> from its cwd and runs a
cwd bunfig.toml preload before the program starts. gstack runs inside the repo
it audits, so a committed .env could steer state roots, the claude binary or
spend caps, and a bunfig preload ran arbitrary code in every bin and hook.

Every bun shebang, shell and skill caller, TypeScript spawn and compiled build
now disables both (--no-env-file --config=/dev/null, bunIsolationArgs(),
--no-compile-autoload-dotenv/bunfig), with a ratchet and hostile-fixture
regressions.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: name the directly executed scripts in the isolation ratchet

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: follow the isolation flags in touchfiles, copy lists, recipe assertions and the harness pin

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: keep browse/src/bun-child-config.ts unchanged so the native cookie qualification still binds

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(windows): the empty bunfig outlives its process so the timeline hook's detached repair child can read it

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* v1.91.159.0 security: gstack's Bun entry points ignore the audited repo's .env and bunfig.toml (S1)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: plan-mode render pattern accepts the isolation flags

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-20 00:24:28 +00:00

117 lines
3.4 KiB
TypeScript

#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* Standalone query CLI for host configs (list / get / detect / validate).
*
* NOT yet wired into ./setup — setup still hand-rolls its host lists (a
* known drift source; driving setup from this CLI is queued follow-up work).
* Behavior is pinned by test/host-config.test.ts.
*
* Usage: bun run scripts/host-config-export.ts <command> [args]
*
* Commands:
* list Print all host names, one per line
* get <host> <field> Print a single config field value
* detect Print names of hosts whose CLI binary is on PATH
* validate Validate all configs, exit 1 on error
*
* All output is shell-safe (single-quoted values, no eval needed).
*/
import { ALL_HOST_CONFIGS, getHostConfig, ALL_HOST_NAMES } from '../hosts/index';
import { validateAllConfigs } from './host-config';
import { RESOLVERS } from './resolvers';
import { execSync } from 'child_process';
function shellEscape(s: string): string {
return "'" + s.replace(/'/g, "'\\''") + "'";
}
const [command, ...args] = process.argv.slice(2);
switch (command) {
case 'list':
for (const name of ALL_HOST_NAMES) {
console.log(name);
}
break;
case 'get': {
const [hostName, field] = args;
if (!hostName || !field) {
console.error('Usage: host-config-export.ts get <host> <field>');
process.exit(1);
}
const config = getHostConfig(hostName);
const value = (config as any)[field];
if (value === undefined) {
console.error(`Unknown field: ${field}`);
process.exit(1);
}
if (typeof value === 'string') {
console.log(value);
} else if (typeof value === 'boolean') {
console.log(value ? '1' : '0');
} else if (Array.isArray(value)) {
for (const item of value) {
console.log(typeof item === 'string' ? item : JSON.stringify(item));
}
} else {
console.log(JSON.stringify(value));
}
break;
}
case 'detect': {
for (const config of ALL_HOST_CONFIGS) {
const commands = [config.cliCommand, ...(config.cliAliases || [])];
for (const cmd of commands) {
try {
execSync(`command -v ${shellEscape(cmd)}`, { stdio: 'pipe' });
console.log(config.name);
break; // Found this host, move to next
} catch {
// Binary not found, try next alias
}
}
}
break;
}
case 'validate': {
const errors = validateAllConfigs(ALL_HOST_CONFIGS, new Set(Object.keys(RESOLVERS)));
if (errors.length > 0) {
for (const error of errors) {
console.error(`ERROR: ${error}`);
}
process.exit(1);
}
console.log(`All ${ALL_HOST_CONFIGS.length} configs valid`);
break;
}
case 'symlinks': {
const [hostName] = args;
if (!hostName) {
console.error('Usage: host-config-export.ts symlinks <host>');
process.exit(1);
}
const config = getHostConfig(hostName);
for (const link of config.runtimeRoot.globalSymlinks) {
console.log(link);
}
if (config.runtimeRoot.globalFiles) {
for (const [dir, files] of Object.entries(config.runtimeRoot.globalFiles)) {
for (const file of files) {
console.log(`${dir}/${file}`);
}
}
}
break;
}
default:
console.error('Usage: host-config-export.ts <list|get|detect|validate|symlinks> [args]');
process.exit(1);
}