You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Split out of PR #570 (codex round 3). Not introduced by that PR's last commit — verified below — but it is a consequence of the #498 relocation, so it belongs on the record next to #571.
The defect
runs.state_dir_for keys a run's out-of-tree control plane by project_tag(project) = sha256(str(project.resolve())). The key is therefore derived from a mutable property of the project: its path. Moving a project leaves its old subtree behind — docs/FEATURES.md already documents that as unsweepable, because after a move no project can name the old key — and if the project ever returns to that path, read_trusted_config_digest finds the old subtree's config-digest still sitting there and prefers it over the newer baseline that travelled in state.json.
A → B → A, all with a real resume at each stop:
step
out-of-tree pin read
in-tree copy
outcome
bless at A under config C1
—
—
pin(A) = digest(C1)
config → C2, move to B, resume
None (new key)
digest(C1)
warns correctly, pin(B) = digest(C2)
move back to A, resume
digest(C1) — stale
digest(C2)
stale pin wins
Two harms, both reproduced:
False warning. With the config unchanged since B's blessing, the resume warns that the host-exec config changed. It did not.
Missed warning. Revert the config to C1 — the value A's stale pin holds — and the resume is silent, although the config did change relative to the last blessing.
Both self-heal: the resume re-stamps the current key, so the next one is correct. The cost is one wrong answer per return trip, on an advisory whose value is that it is rare.
Not introduced by the dual-write fix
Probed at both commits. At 1436ec4 (out-of-tree only, in-tree field unwritten) the stale pin is preferred and the false warning fires identically; the dual write in 33dc0fd did not cause it, and did fix the unrelated leg where the resume at B failed to warn at all.
Sequence/generation number in the pin. The out-of-tree file can carry one, but deciding "a newer blessing exists elsewhere" needs a counterpart the session cannot forge, and the only candidate is the in-tree copy.
Retire stale copies when re-keying (codex's suggestion). It requires naming the old key. From state.json that is a session-supplied path feeding a delete — a destructive primitive strictly worse than this bug. By enumerating the state root instead, it becomes a cross-project delete keyed on run id, which would silence a live run's pin on any id collision.
The direction that does work is the one #571 already names: the identity and privilege model of the state root. A pin whose location is stable across a project's lifetime — rather than derived from a path that can change and change back — has no stale-sibling problem to arbitrate. Worth deciding together with #571 rather than patched here.
Repro
A → B → A with a resume at each stop; assert security_config_changed on the final resume with the config untouched since B.
Split out of PR #570 (codex round 3). Not introduced by that PR's last commit — verified below — but it is a consequence of the #498 relocation, so it belongs on the record next to #571.
The defect
runs.state_dir_forkeys a run's out-of-tree control plane byproject_tag(project)=sha256(str(project.resolve())). The key is therefore derived from a mutable property of the project: its path. Moving a project leaves its old subtree behind —docs/FEATURES.mdalready documents that as unsweepable, because after a move no project can name the old key — and if the project ever returns to that path,read_trusted_config_digestfinds the old subtree'sconfig-digeststill sitting there and prefers it over the newer baseline that travelled instate.json.A → B → A, all with a real resume at each stop:
None(new key)Two harms, both reproduced:
Both self-heal: the resume re-stamps the current key, so the next one is correct. The cost is one wrong answer per return trip, on an advisory whose value is that it is rare.
Not introduced by the dual-write fix
Probed at both commits. At
1436ec4(out-of-tree only, in-tree field unwritten) the stale pin is preferred and the false warning fires identically; the dual write in33dc0fddid not cause it, and did fix the unrelated leg where the resume at B failed to warn at all.Why it was not fixed in #570
No tiebreak is sound at equal privilege, which is the same wall #571 describes, reached by re-keying rather than by tampering:
state.jsonthat is a session-supplied path feeding a delete — a destructive primitive strictly worse than this bug. By enumerating the state root instead, it becomes a cross-project delete keyed on run id, which would silence a live run's pin on any id collision.The direction that does work is the one #571 already names: the identity and privilege model of the state root. A pin whose location is stable across a project's lifetime — rather than derived from a path that can change and change back — has no stale-sibling problem to arbitrate. Worth deciding together with #571 rather than patched here.
Repro
A → B → Awith a resume at each stop; assertsecurity_config_changedon the final resume with the config untouched since B.