diff --git a/.aiox-core/install-manifest.yaml b/.aiox-core/install-manifest.yaml index 872939507..2cb196a33 100644 --- a/.aiox-core/install-manifest.yaml +++ b/.aiox-core/install-manifest.yaml @@ -8,7 +8,7 @@ # - File types for categorization # version: 5.3.0 -generated_at: "2026-07-11T16:43:33.419Z" +generated_at: "2026-07-11T17:07:50.052Z" generator: scripts/generate-install-manifest.js file_count: 1164 files: @@ -4553,9 +4553,9 @@ files: type: script size: 9488 - path: scripts/pm.sh - hash: sha256:5e066c431a20e8ff1be7c1d17ef04f0a76e9bcd82efa9cdd8e8887662a34d1e1 + hash: sha256:dae90035d05e3756bc450526e6b7336625d017552d6809bb00012a2f525cd4b1 type: script - size: 15321 + size: 15958 - path: scripts/README.md hash: sha256:4b0aa7d4664919c7d7b241b6451bd97bf07ca66e7e5f6d4b455e12e094d0e6ae type: script diff --git a/.aiox-core/scripts/pm.sh b/.aiox-core/scripts/pm.sh index 6a415aa0e..f593e4da9 100755 --- a/.aiox-core/scripts/pm.sh +++ b/.aiox-core/scripts/pm.sh @@ -98,6 +98,9 @@ Environment Variables: AIOX_DEBUG Enable debug mode (default: false) AIOX_TIMEOUT Timeout in seconds (default: 300) AIOX_INLINE_MODE Run without a visual terminal (default: false) + AIOX_NO_VISUAL_TERMINAL + Never open a visual terminal, force inline (default: false; + also implied by JEST_WORKER_ID, NODE_ENV=test, or CI=true) CLAUDE_CMD Claude CLI command (default: claude) AIOX_MODEL_BUDGET_CEILING_USD Required positive ceiling for automated model dispatch @@ -482,7 +485,11 @@ spawn_terminal() { log_debug "Created lock file: $LOCK_FILE" # Check for inline mode (Story 12.10 - fallback for non-visual environments) - if [[ "$INLINE_MODE" == "true" ]]; then + # Shell-level defense in depth (#802 review follow-up): direct `bash pm.sh …` + # invocations bypass the Node spawner's detectEnvironment(), so mirror its + # guards here — test runners (JEST_WORKER_ID / NODE_ENV=test), CI, and the + # explicit AIOX_NO_VISUAL_TERMINAL opt-out must never open a real terminal. + if [[ "$INLINE_MODE" == "true" || -n "${JEST_WORKER_ID:-}" || "${NODE_ENV:-}" == "test" || "${CI:-false}" == "true" || "${AIOX_NO_VISUAL_TERMINAL:-false}" == "true" ]]; then local inline_result=0 spawn_inline || inline_result=$? if [[ $inline_result -ne 0 ]]; then diff --git a/tests/unit/terminal-spawner-shell-safety.test.js b/tests/unit/terminal-spawner-shell-safety.test.js index 702c253f9..b200679fc 100644 --- a/tests/unit/terminal-spawner-shell-safety.test.js +++ b/tests/unit/terminal-spawner-shell-safety.test.js @@ -23,4 +23,22 @@ describe('terminal dispatch shell safety', () => { expect(source).not.toContain('full_cmd+=" ${PARAMS}"'); expect(source).toContain('osascript - "$cmd"'); }); + + it('guards spawn_terminal at the shell level against test/CI/no-visual environments (#802 follow-up)', () => { + // Direct `bash pm.sh …` invocations bypass the Node spawner's + // detectEnvironment(); the shell guard must mirror it so a test runner, + // CI, or an explicit opt-out can never open a real terminal window. + const source = fs.readFileSync( + path.resolve(__dirname, '../../.aiox-core/scripts/pm.sh'), + 'utf8', + ); + const guard = source + .split('\n') + .find((line) => line.includes('if [[') && line.includes('$INLINE_MODE')); + expect(guard).toBeDefined(); + expect(guard).toContain('JEST_WORKER_ID'); + expect(guard).toContain('NODE_ENV'); + expect(guard).toContain('${CI:-false}'); + expect(guard).toContain('AIOX_NO_VISUAL_TERMINAL'); + }); });