Repository navigation
feat(run): control Bash environment forwarding and cancellation #259
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,8 +20,14 @@ import { Global } from "./global" | |
| import { JsonMigration } from "./storage/json-migration" | ||
| import { Database } from "./storage/db" | ||
| import { Shutdown } from "./cli/shutdown" | ||
| import { BashEnvPolicy } from "./util/bash-env-policy" | ||
|
|
||
| process.on("unhandledRejection", (e) => { | ||
| const formatted = BashEnvPolicy.format(e) | ||
| if (formatted) { | ||
| process.stderr.write(formatted + EOL) | ||
| process.exit(2) | ||
| } | ||
| Log.Default.error("rejection", { | ||
| e: e instanceof Error ? e.message : e, | ||
| stack: e instanceof Error ? e.stack : undefined, | ||
|
|
@@ -30,6 +36,11 @@ process.on("unhandledRejection", (e) => { | |
| }) | ||
|
|
||
| process.on("uncaughtException", (e) => { | ||
| const formatted = BashEnvPolicy.format(e) | ||
| if (formatted) { | ||
| process.stderr.write(formatted + EOL) | ||
| process.exit(2) | ||
| } | ||
| Log.Default.error("exception", { | ||
| e: e instanceof Error ? e.message : e, | ||
| stack: e instanceof Error ? e.stack : undefined, | ||
|
|
@@ -55,6 +66,7 @@ let cli = yargs(hideBin(process.argv)) | |
| choices: ["DEBUG", "INFO", "WARN", "ERROR"], | ||
| }) | ||
| .middleware(async (opts) => { | ||
| BashEnvPolicy.initialize(opts) | ||
| await Log.init({ | ||
| print: process.argv.includes("--print-logs"), | ||
| dev: Installation.isLocal(), | ||
|
|
@@ -70,7 +82,7 @@ let cli = yargs(hideBin(process.argv)) | |
|
|
||
| Log.Default.info("aictrl-headless", { | ||
| version: Installation.VERSION, | ||
| args: process.argv.slice(2), | ||
| args: BashEnvPolicy.explicit(opts) ? { command: "run", bashEnvPolicy: "allowlist" } : process.argv.slice(2), | ||
| }) | ||
|
|
||
| const storageDir = path.join(Global.Path.data, "storage") | ||
|
|
@@ -112,6 +124,12 @@ let cli = yargs(hideBin(process.argv)) | |
|
|
||
| cli = cli | ||
| .fail((msg, err) => { | ||
| const missing = BashEnvPolicy.parserError(cli as typeof cli & BashEnvPolicy.Parser, msg ?? err?.message) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 BashEnvPolicy entry wiring duplicated in both entrypoints. 🤖 Fix with your agentWhy this mattersFive identical behavioral blocks (format guard with exit(2), initialize in middleware, args redaction, .fail preamble, catch restructure) are added to both src/headless.ts and src/index.ts (~25 new lines each). The entrypoints already mirror each other for legacy reasons, but this widens the duplicated behavioral surface and test/cli/exception-handler.test.ts now needs per-file compiler-API extraction to test both copies. Since the logic already lives in BashEnvPolicy, a small shared entry bootstrap would keep the entrypoints from silently diverging on future fixes. cli = cli
.fail((msg, err) => {
const missing = BashEnvPolicy.parserError(cli as typeof cli & BashEnvPolicy.Parser, msg ?? err?.message)
if (missing) throw missing
// Use yargs' actual parse result even when validation fails before middleware.
if (cli.parsed && BashEnvPolicy.explicit(cli.parsed.argv)) { |
||
| if (missing) throw missing | ||
| // Use yargs' actual parse result even when validation fails before middleware. | ||
| if (cli.parsed && BashEnvPolicy.explicit(cli.parsed.argv)) { | ||
| throw new BashEnvPolicy.ConfigError("Invalid run options; check --bash-env-allowlist and other run arguments.") | ||
| } | ||
| if ( | ||
| msg?.startsWith("Unknown argument") || | ||
| msg?.startsWith("Not enough non-option arguments") || | ||
|
|
@@ -128,31 +146,37 @@ cli = cli | |
| try { | ||
| await cli.parse() | ||
| } catch (e) { | ||
| let data: Record<string, any> = {} | ||
| if (e instanceof NamedError) { | ||
| const obj = e.toObject() | ||
| Object.assign(data, { | ||
| ...obj.data, | ||
| }) | ||
| } | ||
| const policyError = BashEnvPolicy.format(e) | ||
| if (policyError) { | ||
| process.stderr.write(policyError + EOL) | ||
| process.exitCode = 2 | ||
| } else { | ||
| let data: Record<string, any> = {} | ||
| if (e instanceof NamedError) { | ||
| const obj = e.toObject() | ||
| Object.assign(data, { | ||
| ...obj.data, | ||
| }) | ||
| } | ||
|
|
||
| if (e instanceof Error) { | ||
| Object.assign(data, { | ||
| name: e.name, | ||
| message: e.message, | ||
| cause: e.cause?.toString(), | ||
| stack: e.stack, | ||
| }) | ||
| } | ||
| if (e instanceof Error) { | ||
| Object.assign(data, { | ||
| name: e.name, | ||
| message: e.message, | ||
| cause: e.cause?.toString(), | ||
| stack: e.stack, | ||
| }) | ||
| } | ||
|
|
||
| Log.Default.error("fatal", data) | ||
| const formatted = FormatError(e) | ||
| if (formatted) UI.error(formatted) | ||
| if (formatted === undefined) { | ||
| UI.error("Unexpected error, check log file at " + Log.file() + " for more details" + EOL) | ||
| process.stderr.write((e instanceof Error ? e.message : String(e)) + EOL) | ||
| Log.Default.error("fatal", data) | ||
| const formatted = FormatError(e) | ||
| if (formatted) UI.error(formatted) | ||
| if (formatted === undefined) { | ||
| UI.error("Unexpected error, check log file at " + Log.file() + " for more details" + EOL) | ||
| process.stderr.write((e instanceof Error ? e.message : String(e)) + EOL) | ||
| } | ||
| process.exitCode = 1 | ||
| } | ||
| process.exitCode = 1 | ||
| } finally { | ||
| await Shutdown.flush() | ||
| process.exit() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Policy-error exit(2) skips log record and stderr flush.
🤖 Fix with your agent
Why this matters
The new unhandledRejection/uncaughtException preamble writes the ConfigError diagnostic with an unawaited process.stderr.write and immediately calls process.exit(2). When stderr is a pipe — the normal headless automation case — the write may not drain before exit, so the documented exit-2 + stderr contract can be truncated. The path also records nothing via Log.Default.error, leaving misconfiguration without a persistent trace, unlike every other fatal path in these files, and skips Shutdown.flush that the main parse catch still performs.