Skip to content

Warn when a provider env map sets ANTHROPIC_BASE_URL, API_TIMEOUT_MS, HOME or RUNNER_TEMP #1911

Description

@The01Geek

Problem Statement

A maintainer sets up a third-party model provider in .prflow/config.json. That provider entry has dedicated fields for the endpoint (base_url) and the request timeout (timeout_ms), and a free-form env map for anything else. If the maintainer puts ANTHROPIC_BASE_URL or API_TIMEOUT_MS in the env map, it quietly beats the dedicated field, and nothing in the run says so. Two names that are not written by the step at all, HOME and RUNNER_TEMP, are just as unguarded, and setting one of them moves where every later step in the job looks for its files.

This is a config footgun, not a security hole. .prflow/config.json is maintainer-owned and read from a trusted branch, and the two names that would matter for credentials are already refused.

Current Behavior

Triggering input. A provider entry whose env map names any of ANTHROPIC_BASE_URL, API_TIMEOUT_MS, HOME or RUNNER_TEMP.

Observed result. The "Inject provider endpoint" step exits 0 and prints nothing. It writes its own values first and appends the env map last, so the map's value is the one later steps see. On the Bedrock path the run contradicts itself: the step warns that base_url is ignored, while the map puts an endpoint back.

Expected result. A warning naming the keys, so the maintainer can see that the env map decided the value.

Environment. All three cloud workflows, on the GitHub-hosted Actions runner. The defect does not depend on the operating system or the shell.

Desired Behavior

The step keeps working exactly as it does today, and adds one warning. When a provider env map names any of the four watched keys, the step prints a single ::warning:: listing every matched key and carries on. Which value ends up in effect does not change: the env map still wins, deliberately, so no existing setup breaks.

User Impact

A maintainer who sets one of these keys in the env map finds out from the run log instead of from surprising behavior. Everyone else sees no change: a run whose env map names none of the four keys behaves and logs exactly as before.

Technical Context

Scope note: The files and details below are the known starting points, not the full list. Before implementing, trace the change through the codebase to find every affected call site, consumer, and layer — this issue maps the work, it does not bound it.

  • Relevant Classes/Files — the "Inject provider endpoint (provider-routed sections only)" step, which is byte-identical in .github/workflows/devflow.yml, .github/workflows/devflow-implement.yml and .github/workflows/devflow-runner.yml; the #1773 block in lib/test/run.sh.

  • Architecture Alignment — a new warn block beside the existing guards in the same step, mirroring the shape of the deny guard directly above it. No new file, no new helper, no config key.

  • Execution tier — the block runs inside a workflow run: step, executed by the Actions runner rather than by the model action. No tool-permission allowlist and no command-shape matcher sits in front of it, so the change adds no grant and cannot be silently denied. The allowlist the workflow does build is assembled in a different step and is untouched.

  • How consumers get it — the installer already copies these three workflow files, so a consumer picks the change up by re-running the installer. Nothing in install.sh changes.

  • Dependencies — none beyond what the step already uses (jq).

  • Data/Schema Considerations — .prflow/config.schema.json describes the provider env map and lists the names the deny guard refuses; the four watched keys are not there yet and the description gains them. The schema's shape does not change.

  • Cross-layer Impact — three workflow files, one test block, and four documentation surfaces.

  • Verified: .prflow/config.schema.json states the ordering the whole problem rests on: "The env map is written last in the step, so an accepted key takes effect for every subsequent step in the job, not just the action step."

  • Verified: lib/test/modules/coverage-map.json names the covering test run for this surface: "issue The provider env map is exported unfiltered and needs a deny list #1773 provider env-map key-name deny list; run.sh-resident, driven from the R313_INJ_BODY inject-step extraction beside the Add opt-in third-party model provider support to the cloud tier (per-workflow endpoint, auth, and model selection) #313 assertions (the monolith shard covers them)".

  • Third-party behavior, with its source: the Actions runner stores each GITHUB_ENV pair by assigning into a dictionary in SetEnvFileCommand, so a later write for the same key replaces an earlier one. Source: https://raw.githubusercontent.com/actions/runner/main/src/Runner.Worker/FileCommandManager.cs — the published documentation at https://docs.github.com/en/actions/reference/workflows-and-actions/variables does not cover repeated writes.

  • Setting HOME moves where the model action writes its user settings file, and where gh and git read their configuration in the steps that follow — assumption, confirm before implementing, citing the in-repo comment in .github/workflows/devflow-implement.yml that records reading the action's source and concluding it resolves the user settings path from the home directory.

  • RUNNER_TEMP is read by name by later steps in all three workflows, among them the git-env pin resolution, the committer identity resolution, the execution-transcript scrub and the observability backstop.

  • Builds on The provider env map is exported unfiltered and needs a deny list #1773, merged, which added the deny guard this change deliberately leaves alone. The provider env deny list omits the names the inject step writes itself #1892 was recorded there as deferred.

Acceptance Criteria

  • The watched keys are exactly these four — complete by construction: ANTHROPIC_BASE_URL, API_TIMEOUT_MS, HOME, RUNNER_TEMP. When a provider env map names one or more of them, the step prints one ::warning:: naming every key that matched, and exits 0.
  • A key the env map sets ends up with the map's value, for the watched keys and every other key alike, unchanged from before this issue: an existing provider configuration produces the same GITHUB_ENV contents it produces today, plus the new warning line.
  • Key matching is case-folded and whole-name, the same rule the deny guard directly above already applies: a map naming home warns exactly as one naming HOME does, and a map naming HOMEDIR produces no warning.
  • A provider env map naming none of the four watched keys produces no warning, and the step's output is unchanged from before this issue.
  • Every name the deny guard refuses today still refuses the run with the same ::error::, and the deny guard's list of names is unchanged.
  • The "Inject provider endpoint" step body is byte-identical across the three workflow files after the change, the property the existing three-way body-identity check asserts.
  • The test block's per-key refusal loop covers the deny guard's names and none of the four watched keys, so no watched key is asserted to refuse the run.
  • A provider entry with no env key at all, and one whose env is not an object, behave exactly as they do today — the new warn block does not introduce a new way for the step to fail.

Implementation Notes

  • Approach — add one warn block to the "Inject provider endpoint" step, beside the guards that already run there. It reads the env map's keys, upper-cases each one and tests it against a four-name set, collects the matches, and prints a single ::warning:: naming them. It reports and continues rather than refusing the run, and it changes neither what is written nor the order it is written in. Every name outside the four is handled by the path that handles it today: the step's final statement exports it verbatim, with no warning. The deny guard, the name-shape guard and the empty-secret guard are all untouched, and no name moves onto the deny list.

    The same change narrows how the test block finds the denied names. That extraction currently scans the whole step body for a line containing the membership test, and the new warn block adds a second such line, so the extraction is anchored to the deny guard's own assignment. Without that, the watched keys are swept into the loop that asserts a name refuses the run, and the suite fails on names that are meant to warn.

  • Relevant files — the three workflow files carrying the step (.github/workflows/devflow.yml, .github/workflows/devflow-implement.yml, .github/workflows/devflow-runner.yml), which must be edited together to stay byte-identical; the #1773 block in lib/test/run.sh for the new assertions; likely .prflow/config.schema.json and docs/external/docs/configuration/providers.md, and plausibly docs/internal/cloud-setup.md and docs/internal/DEVFLOW_SYSTEM_OVERVIEW.md, for the prose copies; and a new .changeset/*.md file.

  • Code Patterns — copy the deny guard that sits directly above: the same jq shape reading .env // {}, the same ascii_upcase | IN(...) membership test, the same collect-then-report-once form that names every offending key in one message. Keep the command substitution in a bare assignment, exactly as denied_env_keys does and for the same reason the comment beside it gives.

  • Testing Strategy — extend the #1773 block in lib/test/run.sh, which extracts the real step body and runs it under bash -c against a temporary GITHUB_ENV. That single behavioral level is the whole plan; there is no unit boundary below it and no integration boundary above it.

    Start with the bug-reproduction case: feed a map naming ANTHROPIC_BASE_URL and assert no warning is printed. That fails against today's code by exhibiting the reported silence, and passes once the warn block lands.

    The warned names are not covered for free. The existing per-key loop derives its population from the deny guard's own list, so it sweeps up denied names automatically and will not touch a warned one. Every case below is hand-written.

    Cases to add: one per watched key, asserting the warning names it and the step still exits 0; two keys at once, asserting a single warning names both; a lower-case spelling, asserting it matches; a longer name containing a watched key, asserting it does not; a map naming none of the four, asserting silence; and the Bedrock path with ANTHROPIC_BASE_URL in the map, which is the case where the step's existing "base_url is ignored" warning and the map disagree.

    These shape cases guard the new block itself: a provider entry with no env key, and one whose env is not an object. Both must behave as they do today.

    governing conventions consulted: CLAUDE.md (the six-shape adversarial matrix for config-JSON consumers), CONTRIBUTING.md, and docs/internal/. The six-shape matrix is deliberately narrowed here to the two shapes above. The justification: the new block reads the same .env value the deny guard already reads, through the same .env // {} guard, so the array, scalar and valid-falsy shapes reach it exactly as they reach the existing guard and are already covered there. Only the absent and non-object shapes exercise the new block's own reading of that value.

  • Documentation Needed — .prflow/config.schema.json, docs/external/docs/configuration/providers.md, docs/internal/cloud-setup.md, docs/internal/DEVFLOW_SYSTEM_OVERVIEW.md. Each gains the four watched keys and the warn-do-not-refuse rule, kept clearly apart from the deny list so a reader does not take a warned key for a refused one. While editing docs/internal/cloud-setup.md, correct the sentence introducing that bullet: it promises two notes above three bullets, and it says the schema carries none of the content when the schema in fact carries the whole name inventory.

  • Potential Gotchas

    • The command substitution must be a bare assignment. Wrapping it in local, declare, or an if/&&/|| context lets set -e swallow a jq failure, and the empty result then reads as "nothing matched" — the warning silently never fires. The deny guard above carries a comment saying exactly this.
    • The test block derives the denied names by scanning the extracted step body for the membership test and printing every line that matches. A warn block written with the same shape puts the watched keys into that population, and the per-key loop then asserts each one refuses the run with an empty GITHUB_ENV file. They warn and continue, so the suite fails. Anchor the extraction to the deny guard's assignment before adding the block. The extraction already fails closed if it matches nothing at all, because the frozen list of expected names is then reported missing in full.
    • The three step bodies are asserted byte-identical, comparing the run: bodies only. Editing one file and not the others turns the suite red; the differing SECTION value lives in the step's env: block and is outside that comparison.
    • The suite cannot check which of two writes wins. Its GITHUB_ENV helpers emit every occurrence of a repeated key rather than resolving a winner, and every assertion over that output is a membership test. So assert the warning fires; do not try to assert precedence.
    • The four watched names will exist as one executable copy plus several prose copies, and no test reconciles them. This already happened to the deny list, where only the workflow copy is machine-checked, so a name added there keeps the suite green while every prose copy drifts.
    • Editing files under .github/workflows/ needs a workflows-scoped push.
    • The covering run for this surface is lib/test/run-shard.sh monolith; there is no focused module.
    • The change adds a .changeset/*.md file with a patch bump. Nothing starts refusing, so no consumer action is required on upgrade — unlike The provider env map is exported unfiltered and needs a deny list #1773, which shipped as a minor with an upgrade note precisely because it made runs fail.

Generated via /prflow:create-issue (v2.34.2, claude-opus-5, high)

Activity

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

Metadata

Metadata

Assignees

Labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions