Skip to content

bug(github-pat): a duplicated GITHUB_PAT line survives rotation and wins — the agent keeps the revoked token while the rotation reports success #2016

Description

@dolho

Summary

_patch_env_github_pat replaces the first GITHUB_PAT= line only (count=1), while the agent's own .env parser is last-wins. An .env carrying two GITHUB_PAT lines therefore ends a rotation with the new token on line 1 and the revoked token still on line 2 — and the agent reads the revoked one. The rotation reports the agent as updated.

Found by /edge-cases round-trip analysis of #1979 (issue #1967). Same failure class that issue exists to fix — "the rotation reports success and the agent keeps using the old token" — via a different route.

Mechanism

src/backend/services/github_pat_propagation_service.py:

line_re = re.compile(rf'(?m)^[ \t]*{key}=.*$')
if line_re.search(out):
    out = line_re.sub(new_line, out, count=1)      # first match only

Agent side (agent_server/services/execution_env.parse_env_file, and the credential-inject mirror before it) walks every line and assigns parsed[key] = value — the last occurrence wins.

Reproduction

Executed against the merged code, not inferred:

in:  GITHUB_PAT="old-token"
     FOO=1
     GITHUB_PAT="old-token"

out: GITHUB_PAT="ghp_NEWTOKEN"
     FOO=1
     GITHUB_PAT="old-token"      <-- survives
     GH_TOKEN="ghp_NEWTOKEN"
     GITHUB_TOKEN="ghp_NEWTOKEN"

agent reads GITHUB_PAT = 'old-token'

tests/unit/test_pat_propagation_properties.py::TestKnownGaps::test_a_duplicated_pat_line_still_rotates (xfail(strict=True)).

The same file states the general contract as a Hypothesis property — after a rotation the agent reads back exactly the new token under all three key names — which holds for every realistic single-line .env; this is the input where it does not.

How a duplicate line gets there

Not from this function: when the key is absent it appends, and when present it substitutes. The routes are:

So: uncommon, but silent and credential-shaped when it happens, and the fix is small.

Severity

P3 rather than P2 because it needs a pre-existing duplicate line. Raising it if that turns out to be reachable from a supported flow.

The old token also survives as text in the file regardless of which line wins, so anything sourcing or grepping .env still finds a revoked credential. The property test_no_token_is_left_behind_anywhere_in_the_file covers that direction.

Suggested fix

Drop count=1 (replace every occurrence), or normalize to a single line per key. Either makes the last-wins reader agree with the writer. A test asserting what the agent reads back, rather than what the file contains, is the one that would have caught this.

Acceptance criteria

  • After a rotation, the agent's parser reads the new token even when .env contained duplicate lines
  • The revoked token does not survive anywhere in the file
  • Regression test asserted through the agent's read semantics, not the file text (the xfail above flips to passing)

Related

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions