Skip to content

feat(harness): unified env var delivery (ADR 0055)#2582

Open
ralphbean wants to merge 11 commits into
mainfrom
feat-unified-env-delivery
Open

feat(harness): unified env var delivery (ADR 0055)#2582
ralphbean wants to merge 11 commits into
mainfrom
feat-unified-env-delivery

Conversation

@ralphbean

Copy link
Copy Markdown
Member

Summary

  • Adds env: top-level field to harness YAML with runner and sandbox sub-maps, per ADR 0055
  • Wires EnvConfig into forge resolution, base composition, validation, and lint diagnostics
  • runner_env emits deprecation warnings whenever present; env.runner takes precedence
  • Runner generates sandbox export lines from env.sandbox at bootstrap (before .env.d sourcing so manual .env files win during migration)
  • Validates env var key names against POSIX shell identifier regex to prevent injection

Closes #2575

Changes

  • ADR 0055 — documents rationale, schema, merge semantics, deprecation model, and migration phases (amends ADRs 0024 and 0049)
  • internal/harness/harness.goEnvConfig struct, Env field on Harness, extended ValidateRunnerEnvWith
  • internal/harness/forge.goEnv field on ForgeConfig, merge in mergeForgeConfig
  • internal/harness/compose.go — env merge in mergeBaseIntoChild and mergeForgeConfigInto
  • internal/harness/lint.goLint() deprecation diagnostics for runner_env
  • internal/cli/run.go — env expansion, deprecation stderr warnings, env.runner precedence, buildSandboxEnvLines

Test plan

  • Unit tests for YAML parsing, forge merge, base composition, lint diagnostics, validation, sandbox line generation
  • Integration test: full LoadWithBase pipeline with env: + base: + forge: together
  • go vet clean, make lint clean
  • All harness tests pass; 2 pre-existing CLI test failures unrelated to this change

🤖 Generated with Claude Code

Add ADR 0055 introducing env: field with runner/sandbox sub-maps to the
harness schema, deprecating runner_env and manual .env file convention.
Includes cross-reference annotations on ADRs 0024 and 0049, architecture.md
update, and implementation plan.

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
Add the env: field to Harness and ForgeConfig per ADR 0055. This is the
schema-only change — merge, resolution, and runtime behavior follow in
subsequent commits.

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
Wire EnvConfig into mergeForgeConfig so forge.<platform>.env sub-maps
merge with top-level env following the same per-variable additive merge
semantics as runner_env (ADR 0045).

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
Wire EnvConfig into mergeBaseIntoChild and mergeForgeConfigInto so
env.runner and env.sandbox sub-maps merge correctly through base: chains
following the same per-variable additive rules as runner_env.

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
Lint() now emits a warning whenever runner_env is present, regardless of
whether env: also exists. When both are present, the warning notes that
env.runner takes precedence. Per ADR 0055.

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
Extend ValidateRunnerEnvWith to also check env.runner and env.sandbox
var refs. The runner expands ${VAR} references in both sub-maps, then
merges env.runner over runner_env (env.runner wins on collision).
Deprecation warnings are emitted to stderr whenever runner_env is
present. Per ADR 0055.

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
The runner now exports env.sandbox key-value pairs into the sandbox's
.env file at bootstrap. These are placed before the .env.d sourcing
loop so that manual .env files (if still present during migration)
take precedence per ADR 0055's last-writer-wins guarantee.

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
Add a regex guard to skip env var keys that are not valid POSIX shell
identifiers. Keys are interpolated into shell export lines, so invalid
keys (with spaces or special characters) could produce malformed or
injectable shell. Also adds tests for invalid keys and empty values.

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
Exercises base composition + forge resolution together to verify
env.runner and env.sandbox merge correctly end-to-end.

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

feat(harness): unify env var delivery via env.runner/env.sandbox (ADR 0055)
✨ Enhancement 📝 Documentation 🧪 Tests 🕐 40+ Minutes

Grey Divider

Description

• Add env: to harness YAML with runner/sandbox maps, per ADR 0055.
• Merge env through base composition and forge resolution with additive semantics.
• Deprecate runner_env via lint + runtime warnings; generate sandbox exports safely.
Diagram

graph TD
A["harness.yaml"] --> B["Parse Harness"] --> C["Compose base:"] --> D["Resolve forge.<platform>"] --> E["Merged EnvConfig"] --> F["Runner (run.go)"]
F --> G["Host env: env.runner"]
F --> H["Sandbox .env exports"] --> I["Source .env.d/*.env"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Write env.sandbox into a generated .env.d file (KEY=value)
  • ➕ Avoids embedding export statements into a bootstrap script
  • ➕ Matches existing .env.d sourcing convention more directly
  • ➕ Further reduces shell-syntax footguns (no need for export quoting rules)
  • ➖ Requires ensuring the file is created/copied into the sandbox at the right time
  • ➖ May require changes to existing bootstrap/copy logic and tests
  • ➖ Still needs key validation to avoid loader edge-cases
2. Keep runner_env as the single source and auto-project into sandbox
  • ➕ Minimizes schema surface area and migration burden
  • ➕ Preserves existing behavior with fewer YAML changes
  • ➖ Doesn't solve the core problem: runner vs sandbox need different delivery points
  • ➖ Encourages continuing a deprecated mechanism and perpetuates ambiguity
  • ➖ Harder to reason about override precedence vs manual .env files

Recommendation: The PR’s approach (explicit env.runner + env.sandbox with merge semantics and deprecation path) is the most maintainable long-term because it makes delivery targets explicit and composes cleanly through base/forge. The main improvement worth considering later is emitting env.sandbox into a dedicated generated .env.d file instead of inline exports; the current key-regex validation and single-quote escaping largely mitigate injection risk, so this is not a blocker.

Files changed (16) +1717 / -5

Enhancement (5) +186 / -1
run.goApply env.runner precedence and generate sandbox exports from env.sandbox +70/-0

Apply env.runner precedence and generate sandbox exports from env.sandbox

• Expands '${VAR}' references in 'env.runner' and 'env.sandbox', emits stderr deprecation warnings for 'runner_env', and overlays 'env.runner' over 'runner_env'. Adds sandbox export line generation with POSIX key validation and single-quote escaping, inserted before '.env.d' sourcing to allow manual overrides during migration.

internal/cli/run.go

compose.goMerge EnvConfig through base harness and base forge composition +56/-0

Merge EnvConfig through base harness and base forge composition

• Extends base composition ('mergeBaseIntoChild') and forge-config composition ('mergeForgeConfigInto') to merge 'env.runner' and 'env.sandbox' independently, with child keys winning and sub-map inheritance when omitted.

internal/harness/compose.go

forge.goAdd env to ForgeConfig and merge it during forge resolution +24/-0

Add env to ForgeConfig and merge it during forge resolution

• Adds 'Env *EnvConfig' to 'ForgeConfig' and extends 'mergeForgeConfig' so forge env overlays the top-level env with per-submap merging and forge precedence.

internal/harness/forge.go

harness.goIntroduce EnvConfig and validate var references in env.* +21/-0

Introduce EnvConfig and validate var references in env.*

• Adds the 'EnvConfig' struct and 'Env' field to 'Harness'. Extends 'ValidateRunnerEnvWith' to validate '${VAR}' references within both 'env.runner' and 'env.sandbox' values.

internal/harness/harness.go

lint.goEmit lint diagnostics for runner_env deprecation +15/-1

Emit lint diagnostics for runner_env deprecation

• Implements 'Harness.Lint()' warnings when 'runner_env' is present. Message varies depending on whether 'env.runner' is also set, clarifying precedence and migration guidance.

internal/harness/lint.go

Tests (6) +403 / -0
run_test.goAdd unit tests for sandbox export line generation +93/-0

Add unit tests for sandbox export line generation

• Adds tests validating export formatting, deterministic sorting, quote escaping, empty handling, and invalid-key skipping for 'env.sandbox' bootstrap lines.

internal/cli/run_test.go

compose_test.goAdd base composition tests for env runner/sandbox merging +53/-0

Add base composition tests for env runner/sandbox merging

• Adds unit tests ensuring env sub-maps merge correctly through base composition, including inheritance and child override behavior.

internal/harness/compose_test.go

forge_test.goAdd tests for forge env parsing and resolution merging +87/-0

Add tests for forge env parsing and resolution merging

• Adds YAML parsing coverage for 'forge.<platform>.env' plus resolution tests verifying additive merges, override precedence, and inheritance when forge env is nil.

internal/harness/forge_test.go

harness_test.goAdd tests for env parsing and ValidateRunnerEnvWith coverage +70/-0

Add tests for env parsing and ValidateRunnerEnvWith coverage

• Adds parsing tests for the new 'env:' YAML shape and validates that missing host vars referenced in 'env.runner'/'env.sandbox' are detected by 'ValidateRunnerEnvWith'.

internal/harness/harness_test.go

integration_test.goAdd integration test for env merges across base + forge pipeline +59/-0

Add integration test for env merges across base + forge pipeline

• Adds an end-to-end load test exercising 'LoadWithBase' with 'base:', top-level 'env:', and 'forge.<platform>.env', verifying correct precedence rules (child-over-base; forge-over-top).

internal/harness/integration_test.go

lint_test.goAdd lint tests for runner_env deprecation warnings +41/-0

Add lint tests for runner_env deprecation warnings

• Adds tests asserting that 'runner_env' always produces a warning, that warnings mention precedence when 'env.runner' is present, and that no warning occurs without 'runner_env'.

internal/harness/lint_test.go

Documentation (5) +1128 / -4
0024-harness-definitions.mdAnnotate ADR 0024 as amended by unified env delivery +4/-0

Annotate ADR 0024 as amended by unified env delivery

• Adds an amendment note pointing readers to ADR 0055. Documents that 'env:' with 'runner'/'sandbox' replaces 'runner_env' and manual '.env' conventions.

docs/ADRs/0024-harness-definitions.md

0049-agent-configuration-env-var-convention.mdMark ADR 0049 delivery mechanism as deprecated in favor of env: +6/-1

Mark ADR 0049 delivery mechanism as deprecated in favor of env:

• Adds a note that ADR 0055 introduces 'env.runner'/'env.sandbox' and deprecates 'runner_env' and manual '.env' files. Keeps the naming convention intact while updating the recommended delivery path.

docs/ADRs/0049-agent-configuration-env-var-convention.md

0055-unified-env-var-delivery.mdAdd ADR 0055 defining env.runner/env.sandbox schema and migration +158/-0

Add ADR 0055 defining env.runner/env.sandbox schema and migration

• Introduces a new accepted ADR documenting the unified 'env:' schema, merge semantics across base/forge, runner behavior, and a phased deprecation plan for 'runner_env' and manual '.env' files.

docs/ADRs/0055-unified-env-var-delivery.md

architecture.mdUpdate architecture docs for unified env var delivery +8/-3

Update architecture docs for unified env var delivery

• Documents the new 'env:' key and runner-generated sandbox env behavior. Updates references to env var delivery mechanisms to prefer 'env.runner'/'env.sandbox'.

docs/architecture.md

2026-06-23-unified-env-var-delivery.mdAdd implementation plan for ADR 0055 rollout +952/-0

Add implementation plan for ADR 0055 rollout

• Adds a detailed execution plan covering schema, merging, linting, runtime behavior, and tests. Serves as step-by-step guidance for implementing and validating the ADR changes.

docs/superpowers/plans/2026-06-23-unified-env-var-delivery.md

@fullsend-ai-review

fullsend-ai-review Bot commented Jun 23, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 7:35 PM UTC · Completed 7:50 PM UTC
Commit: 9942ac0 · View workflow run →

@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.82474% with 38 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/cli/run.go 52.77% 12 Missing and 5 partials ⚠️
internal/harness/compose.go 50.00% 14 Missing and 1 partial ⚠️
internal/harness/forge.go 53.84% 3 Missing and 3 partials ⚠️

📢 Thoughts on this report? Let us know!

@qodo-code-review

qodo-code-review Bot commented Jun 23, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (2)

Context used
✅ Compliance rules (platform): 51 rules

Grey Divider


Action required

1. False runner_env linting 🐞 Bug ≡ Correctness
Description
runAgent overwrites h.RunnerEnv with the merged env.runner values and then calls h.Lint(),
so Lint()’s len(h.RunnerEnv) > 0 check can emit a runner_env deprecation warning even when the
YAML never had runner_env. This also prevents reliably warning on runner_env: {}
(present-but-empty) and makes deprecation reporting inconsistent with the intended migration
behavior.
Code

internal/cli/run.go[R371-382]

+	// Build effective runner env: start with runner_env, overlay env.runner.
+	effectiveRunnerEnv := make(map[string]string)
+	for k, v := range h.RunnerEnv {
+		effectiveRunnerEnv[k] = v
+	}
+	if h.Env != nil {
+		for k, v := range h.Env.Runner {
+			effectiveRunnerEnv[k] = v
+		}
+	}
+	h.RunnerEnv = effectiveRunnerEnv
+
Relevance

⭐⭐⭐ High

Team recently wired Harness.Lint into run/lock; likely to fix false-positive runner_env deprecation
warning logic.

PR-#2362
PR-#2446
PR-#279

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
runAgent replaces h.RunnerEnv with a newly built merged map (which may contain only env.runner
keys) and then calls h.Lint(). Lint() determines whether to warn about runner_env purely by
checking len(h.RunnerEnv) > 0, which becomes an invalid proxy after the overwrite.

internal/cli/run.go[347-403]
internal/harness/lint.go[40-56]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`runAgent` currently merges `env.runner` into `h.RunnerEnv` (overwriting the original meaning of `RunnerEnv`) before running `h.Lint()`. Since `Lint()` decides whether to warn about deprecated `runner_env` using `len(h.RunnerEnv) > 0`, this can produce false deprecation warnings for harnesses that only use `env.runner`.

## Issue Context
The runner needs an *effective* runner env map (runner_env overlaid by env.runner), but `Harness.Lint()` needs to know whether deprecated `runner_env` was actually provided in the harness YAML.

## Fix Focus Areas
- internal/cli/run.go[347-403]
- internal/harness/lint.go[40-56]

### Suggested approach
- In `runAgent`, compute `effectiveRunnerEnv` as a local variable and use it for `envToList(...)` / downstream execution without overwriting `h.RunnerEnv`; OR capture a `hadRunnerEnv := (h.RunnerEnv != nil)` (or equivalent “present in YAML” signal) *before* any merging and ensure `Lint()` uses that signal.
- Ensure lint/deprecation warnings are evaluated before `RunnerEnv` is repurposed, and update the logic so `runner_env: {}` (present-but-empty) still emits a deprecation warning (per ADR intent).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. ADR 0055 Context too long 📜 Skill insight ⚙ Maintainability
Description
docs/ADRs/0055-unified-env-var-delivery.md has more than 3 paragraphs in its ## Context section,
exceeding the required 1–3 short paragraphs. This reduces ADR scannability and violates the required
ADR format constraints.
Code

docs/ADRs/0055-unified-env-var-delivery.md[R23-46]

+## Context
+
+Setting an environment variable that needs to reach both the runner (pre/post
+scripts) and the sandbox (agent inference) requires specifying it in two
+independent mechanisms with different formats:
+
+1. `runner_env:` in the harness YAML — a key-value map for host-side scripts.
+2. A `.env` file under `env/` — shell `export` syntax, delivered via
+   `host_files` with `expand: true`.
+
+ADR 0049 acknowledges this explicitly: "A config var needed by both must
+appear in both places."
+
+The `.env` file is especially painful to customize. It contains all
+passthrough context vars (`GITHUB_PR_URL`, `GH_TOKEN`, `PR_NUMBER`, etc.).
+Adding a single custom var like `REVIEW_FINDING_SEVERITY_THRESHOLD` forces
+forking the entire file and maintaining all those passthroughs — see
+[fullsend-ai/.fullsend#84](https://github.com/fullsend-ai/.fullsend/pull/84).
+
+This separation was not an intentional design choice. It fell out of the
+original `fullsend run` implementation (PR #231), which solved two different
+runtime problems at different execution points and was later codified into
+ADR 0024 without anyone asking whether a user should have to specify the same
+var in two places.
Relevance

⭐⭐ Medium

No historical enforcement of Context paragraph-count rules; ADR feedback typically about
clarity/accuracy, not section length.

PR-#1814
PR-#2473
PR-#279

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1062090 requires the ADR ## Context section to be 1–3 short paragraphs. In ADR
0055, the Context contains multiple separate paragraphs (e.g., the opening paragraph, the ADR 0049
paragraph, the .env pain paragraph, and the historical explanation paragraph) plus a numbered
list, exceeding the limit.

docs/ADRs/0055-unified-env-var-delivery.md[23-46]
Skill: writing-adrs

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `## Context` section in ADR 0055 exceeds the allowed length (must be 1–3 short paragraphs).

## Issue Context
Compliance requires ADR Context sections to be concise. ADR 0055 currently contains multiple paragraphs plus a numbered list, pushing it beyond the allowed format.

## Fix Focus Areas
- docs/ADRs/0055-unified-env-var-delivery.md[23-46]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. ADR 0055 exceeds 100 lines 📜 Skill insight ⚙ Maintainability
Description
ADR docs/ADRs/0055-unified-env-var-delivery.md exceeds the 100-line maximum content limit
(excluding frontmatter), indicating it is too long for the required ADR format. This increases
maintenance burden and makes the decision record harder to review.
Code

docs/ADRs/0055-unified-env-var-delivery.md[R13-158]

+# 55. Unified environment variable delivery for harness runner and sandbox
+
+Date: 2026-06-23
+
+Amends: [ADR 0024](0024-harness-definitions.md), [ADR 0049](0049-agent-configuration-env-var-convention.md)
+
+## Status
+
+Accepted
+
+## Context
+
+Setting an environment variable that needs to reach both the runner (pre/post
+scripts) and the sandbox (agent inference) requires specifying it in two
+independent mechanisms with different formats:
+
+1. `runner_env:` in the harness YAML — a key-value map for host-side scripts.
+2. A `.env` file under `env/` — shell `export` syntax, delivered via
+   `host_files` with `expand: true`.
+
+ADR 0049 acknowledges this explicitly: "A config var needed by both must
+appear in both places."
+
+The `.env` file is especially painful to customize. It contains all
+passthrough context vars (`GITHUB_PR_URL`, `GH_TOKEN`, `PR_NUMBER`, etc.).
+Adding a single custom var like `REVIEW_FINDING_SEVERITY_THRESHOLD` forces
+forking the entire file and maintaining all those passthroughs — see
+[fullsend-ai/.fullsend#84](https://github.com/fullsend-ai/.fullsend/pull/84).
+
+This separation was not an intentional design choice. It fell out of the
+original `fullsend run` implementation (PR #231), which solved two different
+runtime problems at different execution points and was later codified into
+ADR 0024 without anyone asking whether a user should have to specify the same
+var in two places.
+
+## Decision
+
+Add a new `env:` top-level field to the harness schema with `runner` and
+`sandbox` sub-maps. Deprecate `runner_env` and the manual `.env` file
+convention.
+
+### Schema
+
+```yaml
+env:
+  runner:
+    FULLSEND_OUTPUT_SCHEMA: "${FULLSEND_DIR}/schemas/review-result.schema.json"
+  sandbox:
+    GITHUB_PR_URL: "${GITHUB_PR_URL}"
+    GH_TOKEN: "${GH_TOKEN}"
+    REVIEW_FINDING_SEVERITY_THRESHOLD: "medium"
+```
+
+- `env.runner` — key-value pairs set in the host process environment for
+  pre/post scripts and the validation loop. Replaces `runner_env`.
+- `env.sandbox` — key-value pairs the runner writes into a generated `.env`
+  file and copies into the sandbox at bootstrap. Replaces manual `.env` files
+  delivered via `host_files` with `expand: true`.
+- Values in both sub-maps support `${VAR}` expansion from the host
+  environment, same as `runner_env` and `expand: true` host_files today.
+
+The `env:` field can appear at the top level and inside `forge.<platform>`
+blocks, replacing `runner_env` at both levels
+([ADR 0045](0045-forge-portable-harness-schema.md)).
+
+Go struct:
+
+```go
+type EnvConfig struct {
+    Runner  map[string]string `yaml:"runner,omitempty"`
+    Sandbox map[string]string `yaml:"sandbox,omitempty"`
+}
+```
+
+Added to both `Harness` and `ForgeConfig`:
+
+```go
+Env *EnvConfig `yaml:"env,omitempty"`
+```
+
+### Merge semantics
+
+`env:` follows the same per-variable additive merge rules established by
+ADR 0045 for `runner_env`:
+
+- **`base:` composition** — parent map merged with child map; child keys win
+  on collision. Each sub-map (`runner`, `sandbox`) merges independently. A
+  child that declares only one sub-map inherits the other from the parent.
+- **`forge.<platform>` resolution** — identical rules. Forge sub-maps merge
+  with top-level sub-maps; forge keys win.
+
+### Runner behavior
+
+When `env.sandbox` is present (after all merges), the runner:
+
+1. Expands `${VAR}` references from the host environment.
+2. Writes the result as `KEY=value` lines to a generated `.env` file inside
+   the sandbox (e.g. `/sandbox/workspace/.env.d/generated.env`).
+3. The sandbox's `envfile.Load` picks it up normally.
+
+`env.runner` sets key-value pairs in the host process environment before
+executing pre/post scripts and the validation loop — identical to current
+`runner_env` behavior.
+
+### Deprecation
+
+`runner_env` **always** emits a deprecation warning when present, regardless
+of whether `env:` also exists:
+
+- When `env:` is also present: `env.runner` wins; warning says so.
+- When `env:` is absent: `runner_env` still works; warning says
+  "migrate to env.runner."
+- Same rules apply to `forge.<platform>.runner_env`.
+
+Manually-authored `.env` files delivered via `host_files` are not
+automatically removed or skipped. Users migrate those entries into
+`env.sandbox` at their own pace and remove the `host_files` entries
+themselves. Both mechanisms coexist safely during migration.
+
+### Migration phases
+
+**Phase 1 — Schema extension (this ADR):** Add `env:` to `Harness` and
+`ForgeConfig`. `runner_env` emits deprecation warnings whenever present. When
+both exist, `env.runner` wins. Runner generates `.env` from `env.sandbox`.
+
+**Phase 2 — Migrate scaffold harnesses:** Update all scaffold harnesses to
+use `env:` instead of `runner_env`. Move vars from manual `.env` files into
+`env.sandbox`. Remove redundant `.env` host_files entries and `.env` files
+from the scaffold.
+
+**Phase 3 — Remove `runner_env`:** Remove `runner_env` from the Go structs.
+`yaml.Unmarshal` silently ignores it in old files. `Lint()` emits an error
+for harnesses that still reference it.
+
+## Consequences
+
+- Adding a config var that both runner and sandbox need is a change to one
+  file (the harness YAML), not a fork of an entire `.env` file.
+- `base:` composition works naturally — adding one config knob to a
+  customized harness is a few lines, not a full env file fork.
+- No runner changes are needed for Phase 1 beyond generating the `.env` file
+  from `env.sandbox` and emitting deprecation warnings for `runner_env`.
+- Existing harnesses continue to work unchanged; they just get noisier about
+  `runner_env` deprecation.
+- ADR 0049's env var naming convention applies unchanged — the delivery
+  mechanism changes but the `{AGENT}_{SETTING_NAME}` convention does not.
Relevance

⭐⭐ Medium

No evidence team enforces ADR length caps; prior ADR reviews focus accuracy/wording, not line
limits.

PR-#1814
PR-#1578
PR-#1489

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1062092 requires ADR content (excluding frontmatter) to be ≤100 lines. ADR 0055 as
added in this PR spans through line 158, with substantial content continuing well past the 100-line
threshold after the frontmatter ends at line 11.

docs/ADRs/0055-unified-env-var-delivery.md[1-158]
Skill: writing-adrs

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
ADR 0055 is longer than the allowed maximum of 100 content lines (excluding frontmatter).

## Issue Context
Compliance requires ADRs to stay concise; longer material should be moved to other docs (e.g., architecture docs) or shortened.

## Fix Focus Areas
- docs/ADRs/0055-unified-env-var-delivery.md[13-158]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Silent env.sandbox key drop ✓ Resolved 🐞 Bug ☼ Reliability
Description
buildSandboxEnvLines silently skips any env.sandbox keys that don’t match the shell-identifier
regex, but there is no harness validation or lint diagnostic to tell users their configured variable
was ignored. This can cause missing sandbox env vars with no actionable error message.
Code

internal/cli/run.go[R1251-1271]

+// validEnvKeyRe matches POSIX-portable environment variable names.
+// Keys that don't match are skipped to prevent shell injection.
+var validEnvKeyRe = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
+
+// buildSandboxEnvLines generates export lines for env.sandbox values (ADR 0055).
+// Values have already been expanded by the caller. Each value is single-quoted
+// with internal single quotes escaped. Keys that are not valid shell identifiers
+// are silently skipped.
+func buildSandboxEnvLines(h *harness.Harness) []string {
+	if h.Env == nil || len(h.Env.Sandbox) == 0 {
+		return nil
+	}
+	keys := make([]string, 0, len(h.Env.Sandbox))
+	for k := range h.Env.Sandbox {
+		if validEnvKeyRe.MatchString(k) {
+			keys = append(keys, k)
+		}
+	}
+	if len(keys) == 0 {
+		return nil
+	}
Relevance

⭐⭐ Medium

Precedent mixed: team often adds warnings for silent skips, but rejected similar “warn on invalid
parse” logging in run.go.

PR-#1040
PR-#815
PR-#816

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The CLI-side sandbox export generator explicitly filters invalid keys and returns lines only for
matching identifiers, and it does not emit any warning. Harness validation (Validate and
ValidateRunnerEnvWith) does not validate env key names, so the drop is not surfaced anywhere.

internal/cli/run.go[1251-1281]
internal/harness/harness.go[308-389]
internal/harness/harness.go[499-540]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`buildSandboxEnvLines` filters out invalid env var keys and **silently** drops them. The harness validation pipeline does not validate `env.runner` / `env.sandbox` key names, so users can misconfigure keys and get missing variables with no warning/error.

## Issue Context
Shell injection prevention is good, but “skip silently” turns configuration mistakes into invisible runtime behavior changes.

## Fix Focus Areas
- internal/cli/run.go[1251-1281]
- internal/harness/harness.go[308-389]
- internal/harness/harness.go[499-540]

### Suggested approach
- Add key-name validation in `(*Harness).Validate()` (or a dedicated helper) for both `env.runner` and `env.sandbox` (and optionally `runner_env` too), rejecting keys that don’t match `^[A-Za-z_][A-Za-z0-9_]*$`.
- Once validation exists, change `buildSandboxEnvLines` to assume keys are valid (or return an error instead of silently skipping), so misconfigurations fail fast with a clear message.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +23 to +46
## Context

Setting an environment variable that needs to reach both the runner (pre/post
scripts) and the sandbox (agent inference) requires specifying it in two
independent mechanisms with different formats:

1. `runner_env:` in the harness YAML — a key-value map for host-side scripts.
2. A `.env` file under `env/` — shell `export` syntax, delivered via
`host_files` with `expand: true`.

ADR 0049 acknowledges this explicitly: "A config var needed by both must
appear in both places."

The `.env` file is especially painful to customize. It contains all
passthrough context vars (`GITHUB_PR_URL`, `GH_TOKEN`, `PR_NUMBER`, etc.).
Adding a single custom var like `REVIEW_FINDING_SEVERITY_THRESHOLD` forces
forking the entire file and maintaining all those passthroughs — see
[fullsend-ai/.fullsend#84](https://github.com/fullsend-ai/.fullsend/pull/84).

This separation was not an intentional design choice. It fell out of the
original `fullsend run` implementation (PR #231), which solved two different
runtime problems at different execution points and was later codified into
ADR 0024 without anyone asking whether a user should have to specify the same
var in two places.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Adr 0055 context too long 📜 Skill insight ⚙ Maintainability

docs/ADRs/0055-unified-env-var-delivery.md has more than 3 paragraphs in its ## Context section,
exceeding the required 1–3 short paragraphs. This reduces ADR scannability and violates the required
ADR format constraints.
Agent Prompt
## Issue description
The `## Context` section in ADR 0055 exceeds the allowed length (must be 1–3 short paragraphs).

## Issue Context
Compliance requires ADR Context sections to be concise. ADR 0055 currently contains multiple paragraphs plus a numbered list, pushing it beyond the allowed format.

## Fix Focus Areas
- docs/ADRs/0055-unified-env-var-delivery.md[23-46]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +13 to +158
# 55. Unified environment variable delivery for harness runner and sandbox

Date: 2026-06-23

Amends: [ADR 0024](0024-harness-definitions.md), [ADR 0049](0049-agent-configuration-env-var-convention.md)

## Status

Accepted

## Context

Setting an environment variable that needs to reach both the runner (pre/post
scripts) and the sandbox (agent inference) requires specifying it in two
independent mechanisms with different formats:

1. `runner_env:` in the harness YAML — a key-value map for host-side scripts.
2. A `.env` file under `env/` — shell `export` syntax, delivered via
`host_files` with `expand: true`.

ADR 0049 acknowledges this explicitly: "A config var needed by both must
appear in both places."

The `.env` file is especially painful to customize. It contains all
passthrough context vars (`GITHUB_PR_URL`, `GH_TOKEN`, `PR_NUMBER`, etc.).
Adding a single custom var like `REVIEW_FINDING_SEVERITY_THRESHOLD` forces
forking the entire file and maintaining all those passthroughs — see
[fullsend-ai/.fullsend#84](https://github.com/fullsend-ai/.fullsend/pull/84).

This separation was not an intentional design choice. It fell out of the
original `fullsend run` implementation (PR #231), which solved two different
runtime problems at different execution points and was later codified into
ADR 0024 without anyone asking whether a user should have to specify the same
var in two places.

## Decision

Add a new `env:` top-level field to the harness schema with `runner` and
`sandbox` sub-maps. Deprecate `runner_env` and the manual `.env` file
convention.

### Schema

```yaml
env:
runner:
FULLSEND_OUTPUT_SCHEMA: "${FULLSEND_DIR}/schemas/review-result.schema.json"
sandbox:
GITHUB_PR_URL: "${GITHUB_PR_URL}"
GH_TOKEN: "${GH_TOKEN}"
REVIEW_FINDING_SEVERITY_THRESHOLD: "medium"
```

- `env.runner` — key-value pairs set in the host process environment for
pre/post scripts and the validation loop. Replaces `runner_env`.
- `env.sandbox` — key-value pairs the runner writes into a generated `.env`
file and copies into the sandbox at bootstrap. Replaces manual `.env` files
delivered via `host_files` with `expand: true`.
- Values in both sub-maps support `${VAR}` expansion from the host
environment, same as `runner_env` and `expand: true` host_files today.

The `env:` field can appear at the top level and inside `forge.<platform>`
blocks, replacing `runner_env` at both levels
([ADR 0045](0045-forge-portable-harness-schema.md)).

Go struct:

```go
type EnvConfig struct {
Runner map[string]string `yaml:"runner,omitempty"`
Sandbox map[string]string `yaml:"sandbox,omitempty"`
}
```

Added to both `Harness` and `ForgeConfig`:

```go
Env *EnvConfig `yaml:"env,omitempty"`
```

### Merge semantics

`env:` follows the same per-variable additive merge rules established by
ADR 0045 for `runner_env`:

- **`base:` composition** — parent map merged with child map; child keys win
on collision. Each sub-map (`runner`, `sandbox`) merges independently. A
child that declares only one sub-map inherits the other from the parent.
- **`forge.<platform>` resolution** — identical rules. Forge sub-maps merge
with top-level sub-maps; forge keys win.

### Runner behavior

When `env.sandbox` is present (after all merges), the runner:

1. Expands `${VAR}` references from the host environment.
2. Writes the result as `KEY=value` lines to a generated `.env` file inside
the sandbox (e.g. `/sandbox/workspace/.env.d/generated.env`).
3. The sandbox's `envfile.Load` picks it up normally.

`env.runner` sets key-value pairs in the host process environment before
executing pre/post scripts and the validation loop — identical to current
`runner_env` behavior.

### Deprecation

`runner_env` **always** emits a deprecation warning when present, regardless
of whether `env:` also exists:

- When `env:` is also present: `env.runner` wins; warning says so.
- When `env:` is absent: `runner_env` still works; warning says
"migrate to env.runner."
- Same rules apply to `forge.<platform>.runner_env`.

Manually-authored `.env` files delivered via `host_files` are not
automatically removed or skipped. Users migrate those entries into
`env.sandbox` at their own pace and remove the `host_files` entries
themselves. Both mechanisms coexist safely during migration.

### Migration phases

**Phase 1 — Schema extension (this ADR):** Add `env:` to `Harness` and
`ForgeConfig`. `runner_env` emits deprecation warnings whenever present. When
both exist, `env.runner` wins. Runner generates `.env` from `env.sandbox`.

**Phase 2 — Migrate scaffold harnesses:** Update all scaffold harnesses to
use `env:` instead of `runner_env`. Move vars from manual `.env` files into
`env.sandbox`. Remove redundant `.env` host_files entries and `.env` files
from the scaffold.

**Phase 3 — Remove `runner_env`:** Remove `runner_env` from the Go structs.
`yaml.Unmarshal` silently ignores it in old files. `Lint()` emits an error
for harnesses that still reference it.

## Consequences

- Adding a config var that both runner and sandbox need is a change to one
file (the harness YAML), not a fork of an entire `.env` file.
- `base:` composition works naturally — adding one config knob to a
customized harness is a few lines, not a full env file fork.
- No runner changes are needed for Phase 1 beyond generating the `.env` file
from `env.sandbox` and emitting deprecation warnings for `runner_env`.
- Existing harnesses continue to work unchanged; they just get noisier about
`runner_env` deprecation.
- ADR 0049's env var naming convention applies unchanged — the delivery
mechanism changes but the `{AGENT}_{SETTING_NAME}` convention does not.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Adr 0055 exceeds 100 lines 📜 Skill insight ⚙ Maintainability

ADR docs/ADRs/0055-unified-env-var-delivery.md exceeds the 100-line maximum content limit
(excluding frontmatter), indicating it is too long for the required ADR format. This increases
maintenance burden and makes the decision record harder to review.
Agent Prompt
## Issue description
ADR 0055 is longer than the allowed maximum of 100 content lines (excluding frontmatter).

## Issue Context
Compliance requires ADRs to stay concise; longer material should be moved to other docs (e.g., architecture docs) or shortened.

## Fix Focus Areas
- docs/ADRs/0055-unified-env-var-delivery.md[13-158]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread internal/cli/run.go
Comment on lines +371 to +382
// Build effective runner env: start with runner_env, overlay env.runner.
effectiveRunnerEnv := make(map[string]string)
for k, v := range h.RunnerEnv {
effectiveRunnerEnv[k] = v
}
if h.Env != nil {
for k, v := range h.Env.Runner {
effectiveRunnerEnv[k] = v
}
}
h.RunnerEnv = effectiveRunnerEnv

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. False runner_env linting 🐞 Bug ≡ Correctness

runAgent overwrites h.RunnerEnv with the merged env.runner values and then calls h.Lint(),
so Lint()’s len(h.RunnerEnv) > 0 check can emit a runner_env deprecation warning even when the
YAML never had runner_env. This also prevents reliably warning on runner_env: {}
(present-but-empty) and makes deprecation reporting inconsistent with the intended migration
behavior.
Agent Prompt
## Issue description
`runAgent` currently merges `env.runner` into `h.RunnerEnv` (overwriting the original meaning of `RunnerEnv`) before running `h.Lint()`. Since `Lint()` decides whether to warn about deprecated `runner_env` using `len(h.RunnerEnv) > 0`, this can produce false deprecation warnings for harnesses that only use `env.runner`.

## Issue Context
The runner needs an *effective* runner env map (runner_env overlaid by env.runner), but `Harness.Lint()` needs to know whether deprecated `runner_env` was actually provided in the harness YAML.

## Fix Focus Areas
- internal/cli/run.go[347-403]
- internal/harness/lint.go[40-56]

### Suggested approach
- In `runAgent`, compute `effectiveRunnerEnv` as a local variable and use it for `envToList(...)` / downstream execution without overwriting `h.RunnerEnv`; OR capture a `hadRunnerEnv := (h.RunnerEnv != nil)` (or equivalent “present in YAML” signal) *before* any merging and ensure `Lint()` uses that signal.
- Ensure lint/deprecation warnings are evaluated before `RunnerEnv` is repurposed, and update the logic so `runner_env: {}` (present-but-empty) still emits a deprecation warning (per ADR intent).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@fullsend-ai-review

fullsend-ai-review Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review

Findings

High

  • [logic-error] internal/cli/run.go — False positive deprecation warning from Lint(). The effectiveRunnerEnv merge copies env.runner entries into h.RunnerEnv before Lint() is called. A harness that declares only env.runner (the new recommended path, never using runner_env) will have h.RunnerEnv populated via the merge, and Lint() will see len(h.RunnerEnv) > 0, emitting a spurious "runner_env is deprecated" warning. This was flagged in the prior review and remains unaddressed.
    Remediation: Either (a) move the Lint() call before the effectiveRunnerEnv merge, (b) save the original RunnerEnv length before the merge and use that in Lint(), or (c) have Lint check a separate flag that records whether the original YAML contained runner_env.

Medium

  • [logic-error] internal/cli/run.go — Duplicate deprecation warning for runner_env. When runner_env is present, the inline fmt.Fprintln emits a deprecation warning to stderr, and then Lint() also emits a deprecation diagnostic via emitDiagnostic. Both fire in the same run, producing two nearly identical warnings for the same issue.
    Remediation: Remove the inline fmt.Fprintln deprecation warning and rely solely on Lint() (after fixing the false-positive ordering issue), or suppress the Lint diagnostic when the runtime warning has already been emitted.

  • [privilege-escalation] internal/cli/run.go:1329env.sandbox values can shadow security-critical infrastructure env vars (PATH, FULLSEND_FETCH_TOKEN, FULLSEND_FETCH_URL, FULLSEND_OUTPUT_DIR, FULLSEND_TARGET_REPO_DIR) in the generated sandbox .env file. In bootstrapEnv(), buildSandboxEnvLines output is appended after the infrastructure exports, so last-writer-wins shell semantics let a harness with env.sandbox.FULLSEND_FETCH_TOKEN override the runner-generated fetch token. The validEnvKeyRe regex validates key format but does not check for collisions with reserved names.
    Remediation: Add a deny-list of infrastructure-reserved variable names and either skip them with a warning or reject during validation.

  • [stale-references] docs/ADRs/0053-agent-driven-branch-targeting.md:133 — Instructs adding environment variables to runner_env without mentioning the env.runner alternative. ADR 0053 is recent and actively guides new implementation. The PR amended ADR 0024 and ADR 0049 but missed this one.
    Remediation: Add an amendment note referencing ADR 0055, consistent with the amendments already applied to ADR 0024 and ADR 0049.

  • [stale-examples] docs/guides/user/building-custom-agents.md:147 — Example harness configurations use runner_env without mentioning deprecation. These are user-facing guides that actively direct new harness authors toward the deprecated pattern.
    Remediation: Update examples to use env: with runner:/sandbox: sub-maps, or add a deprecation note directing users to ADR 0055.

  • [stale-examples] docs/guides/user/customizing-agents.md:43 — Example harness uses runner_env without mentioning deprecation. Same concern as above — user-facing guides should reflect the current recommended approach.
    Remediation: Update examples to use env: with runner:/sandbox: sub-maps, or add a deprecation note.

Previous run

Review

Findings

High

  • [logic-error] internal/cli/run.go — False positive deprecation warning from Lint(). After the effectiveRunnerEnv merge (around line 382), h.RunnerEnv is replaced with a merged map containing both old runner_env and env.runner entries. Lint() is called after this merge and checks len(h.RunnerEnv) > 0 to decide whether to emit a runner_env deprecation warning. A harness using only env.runner (the new recommended path, never declaring runner_env) will have h.RunnerEnv populated via the merge and receive a spurious "runner_env is deprecated" warning.
    Remediation: Move the Lint() call before the effectiveRunnerEnv merge, or save the original runner_env length before the merge and use that in Lint().

Medium

  • [injection] internal/cli/run.go:1258env.sandbox values can shadow security-critical infrastructure env vars (PATH, FULLSEND_FETCH_TOKEN, FULLSEND_FETCH_URL, FULLSEND_OUTPUT_DIR, FULLSEND_TARGET_REPO_DIR) in the generated sandbox .env file. In bootstrapEnv(), buildSandboxEnvLines output is appended after the infrastructure exports, so last-writer-wins shell semantics let a harness with env.sandbox.FULLSEND_FETCH_TOKEN override the runner-generated fetch token.
    Remediation: Add a deny-list of infrastructure-reserved variable names (FULLSEND_FETCH_*, FULLSEND_OUTPUT_DIR, FULLSEND_TARGET_REPO_DIR, PATH) and either skip them with a warning or return a validation error.

  • [stale-references] docs/ADRs/0053-agent-driven-branch-targeting.md:133 — Instructs adding environment variables to runner_env without mentioning the env.runner alternative. ADR 0053 is recent and actively guides new implementation. The PR amended ADR 0024 and ADR 0049 but missed this one.
    Remediation: Add an amendment note referencing ADR 0055, consistent with the amendments already applied to ADR 0024 and ADR 0049.

  • [stale-examples] docs/guides/user/building-custom-agents.md:147 — Example harness configurations use runner_env without mentioning deprecation. These are user-facing guides that actively direct new harness authors toward the deprecated pattern.
    Remediation: Update examples to use env: with runner:/sandbox: sub-maps, or add a deprecation note directing users to ADR 0055.

  • [stale-examples] docs/guides/user/customizing-agents.md:43 — Example harness uses runner_env without mentioning deprecation. Same concern as above — user-facing guides should reflect the current recommended approach.
    Remediation: Update examples to use env: with runner:/sandbox: sub-maps, or add a deprecation note.


Labels: PR modifies harness schema, runner bootstrap logic, and multiple ADRs/docs

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the review comment for full details.

Comment thread internal/cli/run.go
// buildSandboxEnvLines generates export lines for env.sandbox values (ADR 0055).
// Values have already been expanded by the caller. Each value is single-quoted
// with internal single quotes escaped. Keys that are not valid shell identifiers
// are silently skipped.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] injection

env.sandbox values can shadow security-critical infrastructure env vars (PATH, FULLSEND_FETCH_TOKEN, FULLSEND_FETCH_URL, FULLSEND_OUTPUT_DIR, FULLSEND_TARGET_REPO_DIR) in the generated sandbox .env file. buildSandboxEnvLines output is appended after infrastructure exports, so last-writer-wins shell semantics allow overriding runner-generated values.

Suggested fix: Add a deny-list of infrastructure-reserved variable names and either skip them with a warning or return a validation error.

@fullsend-ai-review fullsend-ai-review Bot added component/harness Agent harness, config, and skills loading component/runner Agent runner behavior and lifecycle component/docs User-facing documentation feature Feature-category issue awaiting human prioritization labels Jun 23, 2026
When child.Env is nil during base composition, the previous code
assigned child.Env = base.Env, sharing the pointer and underlying
maps. This creates aliasing where downstream mutations (e.g., forge
resolution) would mutate the base's maps. Clone defensively instead,
matching the pattern used for RunnerEnv.

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
buildSandboxEnvLines silently dropped keys that failed the POSIX
identifier regex. Add a stderr warning so harness authors get feedback
when a key like MY-VAR is ignored instead of wondering why it didn't
take effect.

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>

@ralphbean ralphbean left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

LGTM. I pushed two small fixes:

  • 56fc33f — clone EnvConfig in the nil-child merge path instead of aliasing base.Env directly (matches how RunnerEnv is handled)
  • 343194e — emit a stderr warning when buildSandboxEnvLines skips an invalid key

@fullsend-ai-review

fullsend-ai-review Bot commented Jun 23, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 8:02 PM UTC · Completed 8:15 PM UTC
Commit: 343194e · View workflow run →

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the review comment for full details.

Comment thread internal/cli/run.go
}
}
h.RunnerEnv = effectiveRunnerEnv

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[high] logic-error

False positive deprecation warning from Lint(). The effectiveRunnerEnv merge copies env.runner entries into h.RunnerEnv before Lint() is called. A harness that declares only env.runner (the new recommended path, never using runner_env) will have h.RunnerEnv populated via the merge, and Lint() will see len(h.RunnerEnv) > 0, emitting a spurious 'runner_env is deprecated' warning. This was flagged in the prior review and remains unaddressed.

Suggested fix: Either (a) move the Lint() call before the effectiveRunnerEnv merge, (b) save the original RunnerEnv length before the merge and use that in Lint(), or (c) have Lint check a separate flag that records whether the original YAML contained runner_env.

Comment thread internal/cli/run.go
}

// ADR 0055: env.runner takes precedence over runner_env.
// Emit deprecation warning when runner_env is present.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] logic-error

Duplicate deprecation warning for runner_env. When runner_env is present, the inline fmt.Fprintln emits a deprecation warning to stderr, and then Lint() also emits a deprecation diagnostic via emitDiagnostic. Both fire in the same run, producing two nearly identical warnings for the same issue.

Suggested fix: Remove the inline fmt.Fprintln deprecation warning and rely solely on Lint() (after fixing the false-positive ordering issue), or suppress the Lint diagnostic when the runtime warning has already been emitted.

Comment thread internal/cli/run.go
@@ -1261,6 +1329,10 @@ func bootstrapEnv(sandboxName, remoteRepositoryDir string, h *harness.Harness, r
lines = append(lines, fmt.Sprintf("export FULLSEND_FETCH_TOKEN='%s'", escToken))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] privilege-escalation

env.sandbox values can shadow security-critical infrastructure env vars (PATH, FULLSEND_FETCH_TOKEN, FULLSEND_FETCH_URL, FULLSEND_OUTPUT_DIR, FULLSEND_TARGET_REPO_DIR) in the generated sandbox .env file. In bootstrapEnv(), buildSandboxEnvLines output is appended after the infrastructure exports, so last-writer-wins shell semantics let a harness with env.sandbox.FULLSEND_FETCH_TOKEN override the runner-generated fetch token. The validEnvKeyRe regex validates key format but does not check for collisions with reserved names.

Suggested fix: Add a deny-list of infrastructure-reserved variable names and either skip them with a warning or reject during validation.


When `env.sandbox` is present (after all merges), the runner:

1. Expands `${VAR}` references from the host environment.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We need to define what that means, which kinds of expansion syntax do we support? only plain references? What about e.g. value prefix/suffix removal?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/docs User-facing documentation component/harness Agent harness, config, and skills loading component/runner Agent runner behavior and lifecycle feature Feature-category issue awaiting human prioritization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unify runner and sandbox env var delivery under a single harness env: key

2 participants