Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions src/docker-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1295,6 +1295,94 @@ describe('docker-manager', () => {
expect(agent.command).toEqual(['/bin/bash', '-c', 'echo $$HOME && echo $${USER}']);
});

it('should rewrite inline --prompt $(cat ...) to stdin redirect to avoid ARG_MAX expansion', () => {
const configWithInlinePrompt = {
...mockConfig,
agentCommand: 'node /tmp/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" --allow-all-tools',
};
const result = generateDockerCompose(configWithInlinePrompt, mockNetworkConfig);
const agent = result.services.agent;

expect(agent.command).toEqual([
'/bin/bash',
'-c',
'node /tmp/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --prompt - < /tmp/gh-aw/aw-prompts/prompt.txt --allow-all-tools',
]);
});

it('should rewrite inline --prompt $(cat ...) when cat path is single-quoted', () => {
const configWithQuotedPath = {
...mockConfig,
agentCommand: 'node /tmp/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --prompt "$(cat \'/tmp/gh-aw/aw-prompts/prompt with spaces.txt\')" --allow-all-tools',
};
const result = generateDockerCompose(configWithQuotedPath, mockNetworkConfig);
const agent = result.services.agent;

expect(agent.command).toEqual([
'/bin/bash',
'-c',
'node /tmp/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --prompt - < \'/tmp/gh-aw/aw-prompts/prompt with spaces.txt\' --allow-all-tools',
]);
});

it('should rewrite unquoted --prompt $(cat ...) form', () => {
const configWithUnquotedInlinePrompt = {
...mockConfig,
agentCommand: 'node /tmp/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --prompt $(cat /tmp/gh-aw/aw-prompts/prompt.txt) --allow-all-tools',
};
const result = generateDockerCompose(configWithUnquotedInlinePrompt, mockNetworkConfig);
const agent = result.services.agent;

expect(agent.command).toEqual([
'/bin/bash',
'-c',
'node /tmp/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --prompt - < /tmp/gh-aw/aw-prompts/prompt.txt --allow-all-tools',
]);
});

it('should not rewrite regular literal --prompt values', () => {
const configWithLiteralPrompt = {
...mockConfig,
agentCommand: 'copilot --prompt "hello world"',
};
const result = generateDockerCompose(configWithLiteralPrompt, mockNetworkConfig);
const agent = result.services.agent;

expect(agent.command).toEqual(['/bin/bash', '-c', 'copilot --prompt "hello world"']);
});

it('should preserve shell variable expansion in prompt paths', () => {
const configWithVarPath = {
...mockConfig,
agentCommand: 'node driver.cjs copilot --prompt "$(cat "$RUNNER_TEMP/prompt.txt")" --allow-all-tools',
};
const result = generateDockerCompose(configWithVarPath, mockNetworkConfig);
const agent = result.services.agent;

// Env-var path must NOT be single-quoted; $RUNNER_TEMP must expand at runtime
expect(agent.command).toEqual([
'/bin/bash',
'-c',
'node driver.cjs copilot --prompt - < "$$RUNNER_TEMP/prompt.txt" --allow-all-tools',
]);
});

it('should preserve compound command semantics when rewriting prompt', () => {
const configWithPipeline = {
...mockConfig,
agentCommand: 'node driver.cjs copilot --prompt "$(cat /tmp/prompt.txt)" --allow-all-tools 2>&1 | tee -a /tmp/agent-stdio.log',
};
const result = generateDockerCompose(configWithPipeline, mockNetworkConfig);
const agent = result.services.agent;

// Input redirection is in-place; the trailing pipeline is preserved
expect(agent.command).toEqual([
'/bin/bash',
'-c',
'node driver.cjs copilot --prompt - < /tmp/prompt.txt --allow-all-tools 2>&1 | tee -a /tmp/agent-stdio.log',
]);
});

it('should pass through GITHUB_TOKEN when present in environment', () => {
const originalEnv = process.env.GITHUB_TOKEN;
process.env.GITHUB_TOKEN = 'ghp_testtoken123';
Expand Down
85 changes: 81 additions & 4 deletions src/docker-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,78 @@ let agentExternallyKilled = false;
*/
let awfDockerHostOverride: string | undefined;

function trimMatchingOuterQuotes(value: string): string {
const trimmed = value.trim();
if (
(trimmed.startsWith('\'') && trimmed.endsWith('\'')) ||
(trimmed.startsWith('"') && trimmed.endsWith('"'))
) {
return trimmed.slice(1, -1);
}
return trimmed;
}

function findInlinePromptCatRange(command: string): { start: number; end: number; promptPath: string; rawPromptPath: string } | null {
// Variant 1: --prompt "$(cat /path/to/prompt.txt)"
const quotedPrefix = '--prompt "$(cat ';
const quotedStart = command.indexOf(quotedPrefix);
if (quotedStart !== -1) {
const pathStart = quotedStart + quotedPrefix.length;
const pathEnd = command.indexOf(')"', pathStart);
if (pathEnd !== -1) {
return {
start: quotedStart,
end: pathEnd + 2,
promptPath: trimMatchingOuterQuotes(command.slice(pathStart, pathEnd)),
rawPromptPath: command.slice(pathStart, pathEnd).trim(),
};
}
}

// Variant 2: --prompt $(cat /path/to/prompt.txt)
const unquotedPrefix = '--prompt $(cat ';
const unquotedStart = command.indexOf(unquotedPrefix);
if (unquotedStart !== -1) {
const pathStart = unquotedStart + unquotedPrefix.length;
const pathEnd = command.indexOf(')', pathStart);
if (pathEnd !== -1) {
return {
start: unquotedStart,
end: pathEnd + 1,
promptPath: trimMatchingOuterQuotes(command.slice(pathStart, pathEnd)),
rawPromptPath: command.slice(pathStart, pathEnd).trim(),
};
}
}

return null;
}

/**
* Rewrites inline prompt expansions of the form:
* --prompt "$(cat /path/to/prompt.txt)"
* to:
* --prompt - < /path/to/prompt.txt
*
* Uses in-place input redirection (`< file`) instead of prepending `cat |`,
* which preserves shell semantics for compound commands (pipes, `&&`, `;`).
* The original path quoting/expansion is preserved so env-var paths like
* `"$GH_AW_PROMPT"` continue to expand correctly.
*/
function rewriteInlinePromptCatToStdin(agentCommand: string): string {
const inlinePrompt = findInlinePromptCatRange(agentCommand);
if (!inlinePrompt) return agentCommand;

const rawPath = inlinePrompt.rawPromptPath;
if (!rawPath) return agentCommand;

const rewrittenCommand = `${agentCommand.slice(0, inlinePrompt.start)}--prompt - < ${rawPath}${agentCommand.slice(inlinePrompt.end)}`;
if (rewrittenCommand === agentCommand) return agentCommand;

logger.warn(`Rewriting inline prompt expansion to stdin redirect to avoid ARG_MAX/E2BIG: ${rawPath}`);
return rewrittenCommand;
}

/**
* Sets the Docker host to use for AWF's own container operations.
*
Expand Down Expand Up @@ -602,6 +674,7 @@ export function generateDockerCompose(
squidConfigContent?: string
): DockerComposeConfig {
const projectRoot = path.join(__dirname, '..');
const rewrittenAgentCommand = rewriteInlinePromptCatToStdin(config.agentCommand);

// Guard: --build-local requires full repo checkout (not available in standalone bundle)
if (config.buildLocal) {
Expand Down Expand Up @@ -1024,10 +1097,14 @@ export function generateDockerCompose(
if (config.envAll) {
const totalEnvBytes = Object.entries(environment)
.reduce((sum, [k, v]) => sum + k.length + (v?.length ?? 0) + 2, 0); // +2 for '=' and null
if (totalEnvBytes > ENV_SIZE_WARNING_THRESHOLD) {
const argvBytes = ['/bin/bash', '-c', rewrittenAgentCommand]
.reduce((sum, arg) => sum + arg.length + 1, 0); // +1 for each argv null terminator
const totalArgvEnvBytes = totalEnvBytes + argvBytes;
if (totalArgvEnvBytes > ENV_SIZE_WARNING_THRESHOLD) {
logger.warn(
`⚠️ Total container environment size is ${(totalEnvBytes / 1024).toFixed(0)} KB — ` +
'may cause E2BIG (Argument list too long) errors when combined with large command arguments'
`⚠️ Estimated argv+env size is ${(totalArgvEnvBytes / 1024).toFixed(0)} KB ` +
`(env ${(totalEnvBytes / 1024).toFixed(0)} KB, argv ${(argvBytes / 1024).toFixed(0)} KB) — ` +
'may cause E2BIG (Argument list too long) errors'
);
logger.warn(' Consider using --exclude-env to remove unnecessary variables');
}
Expand Down Expand Up @@ -1565,7 +1642,7 @@ export function generateDockerCompose(
start_period: '1s',
},
// Escape $ with $$ for Docker Compose variable interpolation
command: ['/bin/bash', '-c', config.agentCommand.replace(/\$/g, '$$$$')],
command: ['/bin/bash', '-c', rewrittenAgentCommand.replace(/\$/g, '$$$$')],
};

// Set working directory if specified (overrides Dockerfile WORKDIR)
Expand Down
Loading