Skip to content

feat: add --env-file support for injecting env vars from a file#1457

Merged
lpcox merged 4 commits intomainfrom
copilot/fix-safe-outputs-env-vars
Mar 26, 2026
Merged

feat: add --env-file support for injecting env vars from a file#1457
lpcox merged 4 commits intomainfrom
copilot/fix-safe-outputs-env-vars

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 26, 2026

Variables written only to $GITHUB_OUTPUT (e.g. GH_AW_SAFE_OUTPUTS_CONFIG_PATH) are never in process.env, so --env-all cannot forward them to the agent container. Adding --env-file provides a file-based injection path that works regardless of how variables were set.

Changes

  • src/types.tsenvFile?: string added to WrapperConfig
  • src/cli.ts--env-file <path> option; validates file existence at startup
  • src/docker-manager.tsreadEnvFile() parses KEY=VALUE files (skips comments/blank lines, trims whitespace, rejects invalid keys); injected after --env-all but before --env so explicit flags always win; excluded system vars (PATH, HOME, etc.) are never injected
  • src/docker-manager.test.ts — 10 unit tests for readEnvFile() + 5 integration tests for generateDockerCompose with envFile
  • docs/environment.md — documents format, precedence, and a Safe Outputs MCP example

Usage

# Write step outputs to a file, then pass via --env-file
echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=/tmp/config.json" >> /tmp/runtime-paths.env

awf --env-file /tmp/runtime-paths.env --allow-domains github.com -- agent-command

# --env flags still override file values
awf --env-file /tmp/runtime-paths.env -e MY_VAR=override --allow-domains github.com -- agent-command

Precedence (low → high): built-in framework vars → --env-all--env-file--env


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI changed the title [WIP] Fix safe outputs env vars not reaching agent container feat: add --env-file support for injecting env vars from a file Mar 26, 2026
Copilot AI requested a review from lpcox March 26, 2026 19:59
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 26, 2026

Documentation Preview

Documentation build failed for this PR. View logs.

Built from commit 38e4996

@github-actions
Copy link
Copy Markdown
Contributor

⚠️ Coverage Regression Detected

This PR decreases test coverage. Please add tests to maintain coverage levels.

Overall Coverage

Metric Base PR Delta
Lines 82.73% 82.75% 📈 +0.02%
Statements 82.39% 82.42% 📈 +0.03%
Functions 81.44% 81.50% 📈 +0.06%
Branches 76.06% 76.02% 📉 -0.04%
📁 Per-file Coverage Changes (2 files)
File Lines (Before → After) Statements (Before → After)
src/cli.ts 61.4% → 60.9% (-0.59%) 61.9% → 61.3% (-0.57%)
src/docker-manager.ts 85.7% → 86.4% (+0.73%) 85.2% → 85.9% (+0.73%)

Coverage comparison generated by scripts/ci/compare-coverage.ts

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

* Initial plan

* fix: add tests to resolve branch coverage regression in cli.ts

Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/41835a48-8b69-46e0-9485-8fb41265babf

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

✅ Coverage Check Passed

Overall Coverage

Metric Base PR Delta
Lines 82.73% 82.82% 📈 +0.09%
Statements 82.39% 82.48% 📈 +0.09%
Functions 81.44% 81.50% 📈 +0.06%
Branches 76.06% 76.25% 📈 +0.19%
📁 Per-file Coverage Changes (2 files)
File Lines (Before → After) Statements (Before → After)
src/cli.ts 61.4% → 61.2% (-0.27%) 61.9% → 61.6% (-0.26%)
src/docker-manager.ts 85.7% → 86.4% (+0.73%) 85.2% → 85.9% (+0.73%)

Coverage comparison generated by scripts/ci/compare-coverage.ts

@github-actions
Copy link
Copy Markdown
Contributor

Smoke Test Results — PASS

💥 [THE END] — Illustrated by Smoke Claude for issue #1457

@github-actions
Copy link
Copy Markdown
Contributor

Smoke Test Results

GitHub MCP — Last 2 merged PRs:

Playwright — github.com title contains "GitHub"
File Write/tmp/gh-aw/agent/smoke-test-copilot-23616504488.txt created
Bash — File verified via cat

Overall: PASS | PR author: @Copilot | Assignees: @lpcox, @Copilot

📰 BREAKING: Report filed by Smoke Copilot for issue #1457

@github-actions
Copy link
Copy Markdown
Contributor

Chroot Version Comparison Results

Runtime Host Version Chroot Version Match?
Python Python 3.12.13 Python 3.12.3 ❌ NO
Node.js v24.14.0 v20.20.1 ❌ NO
Go go1.22.12 go1.22.12 ✅ YES

Result: ❌ Not all tests passed — Python and Node.js versions differ between host and chroot.

Tested by Smoke Chroot for issue #1457

@github-actions github-actions Bot mentioned this pull request Mar 26, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🏗️ Build Test Suite Results

Ecosystem Project Build/Install Tests Status
Bun elysia 1/1 passed ✅ PASS
Bun hono 1/1 passed ✅ PASS
C++ fmt N/A ✅ PASS
C++ json N/A ✅ PASS
Deno oak N/A 1/1 passed ✅ PASS
Deno std N/A 1/1 passed ✅ PASS
.NET hello-world N/A ✅ PASS
.NET json-parse N/A ✅ PASS
Go color 1/1 passed ✅ PASS
Go env 1/1 passed ✅ PASS
Go uuid 1/1 passed ✅ PASS
Java gson 1/1 passed ✅ PASS
Java caffeine 1/1 passed ✅ PASS
Node.js clsx All passed ✅ PASS
Node.js execa All passed ✅ PASS
Node.js p-limit All passed ✅ PASS
Rust fd 1/1 passed ✅ PASS
Rust zoxide 1/1 passed ✅ PASS

Overall: 8/8 ecosystems passed — ✅ PASS

Note: Java Maven required maven.repo.local to be set to /tmp/gh-aw/agent/.m2/repository because the default ~/.m2/repository directory was owned by root (not writable by the runner user).

Generated by Build Test Suite for issue #1457 ·

@github-actions
Copy link
Copy Markdown
Contributor

Smoke Test Report (Codex)
PR titles: "fix: add tests to recover branch coverage regression in cli.ts"; "fix: auto-inject GH_HOST from GITHUB_SERVER_URL when --env-all is used"

  1. GitHub MCP merged PR review: ✅
  2. safeinputs-gh PR query: ❌ (tool unavailable in this run)
  3. Playwright github.com title contains "GitHub": ✅
  4. Tavily web search: ❌ (Tavily MCP tool unavailable)
  5. File write /tmp/gh-aw/agent/smoke-test-codex-23616504557.txt: ✅
  6. Bash cat verification: ✅
  7. Discussion query + oracle comment: ❌ (github-discussion-query tool unavailable)
  8. npm ci && npm run build: ✅
    Overall status: FAIL

🔮 The oracle has spoken through Smoke Codex

Warning

⚠️ Firewall blocked 2 domains

The following domains were blocked by the firewall during workflow execution:

  • ab.chatgpt.com
  • registry.npmjs.org

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "ab.chatgpt.com"
    - "registry.npmjs.org"

See Network Configuration for more information.

@lpcox lpcox marked this pull request as ready for review March 26, 2026 20:54
@lpcox lpcox requested a review from Mossaka as a code owner March 26, 2026 20:54
Copilot AI review requested due to automatic review settings March 26, 2026 20:54
@lpcox lpcox merged commit df4c2fe into main Mar 26, 2026
60 checks passed
@lpcox lpcox deleted the copilot/fix-safe-outputs-env-vars branch March 26, 2026 20:54
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds --env-file support so AWF can inject environment variables into the agent container from a file (useful for values written to $GITHUB_OUTPUT and other file-based sources), extending the existing --env-all / --env mechanisms.

Changes:

  • Extend WrapperConfig with envFile?: string.
  • Add --env-file <path> CLI option with startup existence validation.
  • Implement readEnvFile() + integrate env-file injection into generateDockerCompose, plus unit/integration tests and documentation.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/types.ts Adds envFile?: string to wrapper config and describes intended behavior/precedence.
src/cli.ts Adds --env-file flag, validates path existence, and threads option into WrapperConfig.
src/docker-manager.ts Implements env-file parser and injects parsed vars into container environment.
src/docker-manager.test.ts Adds tests for readEnvFile() and compose env injection behavior.
docs/environment.md Documents env-file format and precedence expectations.
Comments suppressed due to low confidence (2)

src/docker-manager.test.ts:3513

  • These readEnvFile test names say “should reject …”, but the function currently ignores invalid keys/lines (it doesn’t throw or report). Renaming these tests (or changing the function to actually reject with an error) would make the behavior clearer and keep wording consistent across code/docs.
    it('should reject keys starting with a digit', () => {
      const envFile = path.join(tmpDir, '.env');
      fs.writeFileSync(envFile, '123KEY=value\nFOO=bar\n');
      expect(readEnvFile(envFile)).toEqual({ FOO: 'bar' });
    });

    it('should reject keys containing hyphens', () => {
      const envFile = path.join(tmpDir, '.env');
      fs.writeFileSync(envFile, 'KEY-NAME=value\nFOO=bar\n');
      expect(readEnvFile(envFile)).toEqual({ FOO: 'bar' });
    });

src/docker-manager.ts:235

  • readEnvFile() trims each whole line (raw.trim()), but the docstring says “Values are taken literally”. With the current implementation, trailing spaces in values (and leading spaces before the key) are stripped, so the parsed VALUE isn’t actually literal. Consider only trimming for comment/blank detection (e.g., trimStart for #) and splitting on the first = while preserving the value bytes (also handle \r\n without relying on trim).
  for (const raw of content.split('\n')) {
    const line = raw.trim();
    // Skip comments and blank lines
    if (line === '' || line.startsWith('#')) continue;
    const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
    if (match) {
      result[match[1]] = match[2];
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/docker-manager.ts
Comment on lines +223 to +235
* @throws {Error} If the file cannot be read
*/
export function readEnvFile(filePath: string): Record<string, string> {
const content = fs.readFileSync(filePath, 'utf-8');
const result: Record<string, string> = {};
for (const raw of content.split('\n')) {
const line = raw.trim();
// Skip comments and blank lines
if (line === '' || line.startsWith('#')) continue;
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
if (match) {
result[match[1]] = match[2];
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The readEnvFile() JSDoc says each non-comment line “must match” KEY=VALUE, but the implementation silently ignores non-matching lines (including invalid keys) instead of throwing or surfacing an error. Either update the docs to say invalid lines are ignored, or change the function to fail fast with a helpful error including the line number/content.

This issue also appears on line 228 of the same file.

Suggested change
* @throws {Error} If the file cannot be read
*/
export function readEnvFile(filePath: string): Record<string, string> {
const content = fs.readFileSync(filePath, 'utf-8');
const result: Record<string, string> = {};
for (const raw of content.split('\n')) {
const line = raw.trim();
// Skip comments and blank lines
if (line === '' || line.startsWith('#')) continue;
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
if (match) {
result[match[1]] = match[2];
}
* @throws {Error} If the file cannot be read or contains an invalid line
*/
export function readEnvFile(filePath: string): Record<string, string> {
const content = fs.readFileSync(filePath, 'utf-8');
const result: Record<string, string> = {};
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
const raw = lines[i];
const line = raw.trim();
// Skip comments and blank lines
if (line === '' || line.startsWith('#')) continue;
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
if (!match) {
throw new Error(
`Invalid env file line in ${filePath} at line ${i + 1}: "${raw}" ` +
'(expected KEY=VALUE with KEY matching /^[A-Za-z_][A-Za-z0-9_]*$/)'
);
}
result[match[1]] = match[2];

Copilot uses AI. Check for mistakes.
Comment thread src/docker-manager.ts
Comment on lines +644 to +648
// Environment variables from --env-file (injected before --env flags so explicit flags win)
if (config.envFile) {
const fileEnv = readEnvFile(config.envFile);
for (const [key, value] of Object.entries(fileEnv)) {
if (!EXCLUDED_ENV_VARS.has(key) && !Object.prototype.hasOwnProperty.call(environment, key)) {
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--env-file is documented as having higher precedence than --env-all, but this block only sets values when environment does not already have the key. Since --env-all runs earlier and also populates environment, env-file entries won’t override env-all entries. Either adjust the precedence (allow env-file to override env-all while still protecting framework/excluded vars) or update the docs/comments/tests to reflect the actual “first writer wins” behavior.

Suggested change
// Environment variables from --env-file (injected before --env flags so explicit flags win)
if (config.envFile) {
const fileEnv = readEnvFile(config.envFile);
for (const [key, value] of Object.entries(fileEnv)) {
if (!EXCLUDED_ENV_VARS.has(key) && !Object.prototype.hasOwnProperty.call(environment, key)) {
// Environment variables from --env-file (applied after --env-all but before --env flags;
// env-file entries override env-all for non-excluded vars, while explicit --env flags still win)
if (config.envFile) {
const fileEnv = readEnvFile(config.envFile);
for (const [key, value] of Object.entries(fileEnv)) {
if (!EXCLUDED_ENV_VARS.has(key)) {

Copilot uses AI. Check for mistakes.
Comment thread src/cli.ts
Comment on lines +1579 to +1580
if (!fs.existsSync(options.envFile)) {
logger.error(`--env-file: file not found: ${options.envFile}`);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The --env-file validation only checks existsSync. If the path exists but is a directory or is unreadable, the error will surface later (likely as an uncaught read error) and the CLI message won’t be as clear. Consider validating that the path is a readable regular file (e.g., statSync().isFile() + accessSync(R_OK)) and exiting with a consistent error message.

Suggested change
if (!fs.existsSync(options.envFile)) {
logger.error(`--env-file: file not found: ${options.envFile}`);
try {
if (!fs.existsSync(options.envFile)) {
logger.error(`--env-file: file not found: ${options.envFile}`);
process.exit(1);
}
const envFileStat = fs.statSync(options.envFile);
if (!envFileStat.isFile()) {
logger.error(`--env-file: not a regular file: ${options.envFile}`);
process.exit(1);
}
fs.accessSync(options.envFile, fs.constants.R_OK);
} catch (error) {
logger.error(`--env-file: cannot read file: ${options.envFile}`);

Copilot uses AI. Check for mistakes.
Comment thread docs/environment.md
Comment on lines +53 to +59
- Values are taken literally (no quote stripping, no variable expansion)

**Precedence (lowest → highest):**
1. Built-in framework variables (proxy, DNS, etc.)
2. `--env-all` host variables
3. `--env-file` variables
4. `--env` / `-e` explicit variables (highest priority)
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The --env-file docs claim (1) env-file values are “taken literally” and (2) precedence is built-in → env-all → env-file → env. In the current implementation, lines are trimmed during parsing, framework vars are protected from being overwritten by env-all/env-file, and env-file does not override env-all. Please adjust this section to match actual behavior (or update the implementation to match the documented precedence).

Suggested change
- Values are taken literally (no quote stripping, no variable expansion)
**Precedence (lowest → highest):**
1. Built-in framework variables (proxy, DNS, etc.)
2. `--env-all` host variables
3. `--env-file` variables
4. `--env` / `-e` explicit variables (highest priority)
- Values are read as raw text (no variable expansion). Leading and trailing whitespace on each line is trimmed by the parser, and quotes inside the value are preserved (no quote stripping).
**Precedence (lowest → highest):**
1. Built-in framework variables (proxy, DNS, etc.). For these keys, framework-provided values are always used and are **not** overridden by `--env-all` or `--env-file`.
2. `--env-file` variables
3. `--env-all` host variables
4. `--env` / `-e` explicit variables (highest priority for non-framework variables)

Copilot uses AI. Check for mistakes.
Comment on lines +1373 to +1381
// AWF_DNS_SERVERS is set before envFile processing; file should not clobber it
fs.writeFileSync(envFile, 'AWF_DNS_SERVERS=1.1.1.1\n');

const config = { ...mockConfig, envFile };
const result = generateDockerCompose(config, mockNetworkConfig);
const env = result.services.agent.environment as Record<string, string>;

// AWF_DNS_SERVERS is set by the framework; file should NOT override it
expect(env.AWF_DNS_SERVERS).not.toBe('1.1.1.1');
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test’s comment/intent doesn’t match the actual ordering in generateDockerCompose: AWF_DNS_SERVERS is assigned after the env-file injection block, so the env-file value would be overwritten later regardless of the “don’t clobber existing vars” guard. If the goal is to verify env-file doesn’t override already-set framework vars, use a variable that is definitely set before env-file processing (e.g., one of the proxy vars or AWF_HOST_PATH) or assert the exact precedence rules you want.

This issue also appears on line 3503 of the same file.

Suggested change
// AWF_DNS_SERVERS is set before envFile processing; file should not clobber it
fs.writeFileSync(envFile, 'AWF_DNS_SERVERS=1.1.1.1\n');
const config = { ...mockConfig, envFile };
const result = generateDockerCompose(config, mockNetworkConfig);
const env = result.services.agent.environment as Record<string, string>;
// AWF_DNS_SERVERS is set by the framework; file should NOT override it
expect(env.AWF_DNS_SERVERS).not.toBe('1.1.1.1');
// AWF_HOST_PATH is set before envFile processing; file should not clobber it
fs.writeFileSync(envFile, 'AWF_HOST_PATH=/malicious/path\n');
const config = { ...mockConfig, envFile };
const result = generateDockerCompose(config, mockNetworkConfig);
const env = result.services.agent.environment as Record<string, string>;
// AWF_HOST_PATH is set by the framework; file should NOT override it
expect(env.AWF_HOST_PATH).not.toBe('/malicious/path');

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Safe outputs env vars not reaching agent container when set via GITHUB_OUTPUT only

3 participants