Add non-interactive mode for agent execution (#35)

* refactor: extract shared test helpers and utilities

Created centralized test-helpers.ts module to eliminate duplication across test files:

**Extracted Helpers:**
- createMockManifest() - Reusable mock manifest data
- createEmptyManifest() - Empty manifest for edge cases
- createConsoleMocks() - Console spy setup
- createProcessExitMock() - Process exit mock
- restoreMocks() - Mock cleanup utility
- mockSuccessfulFetch() - Simplified successful fetch mock
- mockFailedFetch() - Simplified failed fetch mock
- mockFetchWithStatus() - Fetch mock with custom status
- setupTestEnvironment() - Test directory and env setup
- teardownTestEnvironment() - Cleanup utility

**Deduplication Impact:**
- commands.test.ts: Removed 50+ lines of duplicate mock setup
- manifest.test.ts: Removed 80+ lines of duplicate manifest data and setup code
- integration.test.ts: Removed 40+ lines of duplicate setup/teardown

**Benefits:**
- Single source of truth for test fixtures
- Consistent mock patterns across all tests
- Easier maintenance - changes to test setup in one place
- Improved test readability

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* refactor: Add non-interactive mode for agent execution

Implements --prompt and --prompt-file flags to enable non-interactive
agent execution. This allows users to:

- Execute agents with a prompt and exit automatically
- Use spawn in CI/CD pipelines and automation scripts
- Pass prompts via command line or file

Changes:
- TypeScript CLI: Parse --prompt/-p and --prompt-file flags
- Security: Add validatePrompt() to prevent command injection
- Commands: Pass prompt via SPAWN_PROMPT env var to bash scripts
- Bash scripts: Detect SPAWN_PROMPT and fork interactive/non-interactive
- Help text: Document new flags with examples

Implementation:
- claude.sh: Use 'claude -p' for non-interactive execution
- aider.sh: Use 'aider -m' for non-interactive execution
- shared/common.sh: Add execute_agent_non_interactive() helper

Security:
- Validates prompts for command injection patterns
- Length limit: 10KB max
- Blocks $(), backticks, piping to bash/sh
- Uses printf %q for proper shell escaping

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* docs: Add testing guide for non-interactive mode

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Sprite <noreply@sprite.dev>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
L 2026-02-07 21:20:34 -08:00 committed by GitHub
parent 580424189a
commit c09e714cc7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 394 additions and 83 deletions

View file

@ -74,3 +74,40 @@ export function validateScriptContent(script: string): void {
throw new Error("Script must start with a valid shebang (e.g., #!/bin/bash)");
}
}
/**
* Validates a prompt string for non-interactive agent execution.
* SECURITY-CRITICAL: Prevents command injection via prompt parameter.
*
* @param prompt - The user-provided prompt to validate
* @throws Error if validation fails
*/
export function validatePrompt(prompt: string): void {
if (!prompt || prompt.trim() === "") {
throw new Error("Prompt cannot be empty");
}
// Check length constraints (10KB max to prevent DoS)
const MAX_PROMPT_LENGTH = 10 * 1024;
if (prompt.length > MAX_PROMPT_LENGTH) {
throw new Error(`Prompt exceeds maximum length of ${MAX_PROMPT_LENGTH} characters`);
}
// Check for obvious command injection patterns
// These patterns would break out of the shell quoting used in bash scripts
const dangerousPatterns: Array<{ pattern: RegExp; description: string }> = [
{ pattern: /\$\(.*\)/, description: "command substitution $()" },
{ pattern: /`[^`]*`/, description: "command substitution backticks" },
{ pattern: /;\s*rm\s+-rf/, description: "command chaining with rm -rf" },
{ pattern: /\|\s*bash/, description: "piping to bash" },
{ pattern: /\|\s*sh/, description: "piping to sh" },
];
for (const { pattern, description } of dangerousPatterns) {
if (pattern.test(prompt)) {
throw new Error(
`Prompt blocked: contains potentially dangerous pattern (${description}). If this is a false positive, please use --prompt-file instead.`
);
}
}
}