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
Found while re-validating PR #495 (#461 point 4). Minor, but it makes a shipped line of code read as a security control when it is not one.
The asymmetry
PR #495 added two consumers of the launch-time config_digest. Only one of them has a baseline an agent cannot reach.
The auto-sweep gate is sound._sweep_factory's trusted_digest is threaded in memory from launch — cli.pycmd_run computes it once and passes it in, and _resume_paused_run passes the freshly computed new_digest. It is never read back from disk, so a driven session has nothing to rewrite. That is the refusal path, and it holds.
The resume warning is advisory. It compares against state.trusted_config_digest:
security_config_changed=bool(state.trusted_config_digest) and (
new_digest!=state.trusted_config_digest
)
RunState round-trips through <project>/.bmad-loop/runs/<id>/state.json (runs.RUNS_DIR = .bmad-loop/runs, run_dir_for) — inside the same agent-writable tree config_digest's own docstring says driven sessions can write anywhere in. A session that rewrites policy.toml can also rewrite that field and the operator is never told. Blanking it is enough: the bool(...) guard reads "" as "no prior pin, no warning".
So the warning catches an incidental or accidental config change. It does not catch a deliberate one, which is the case it is worded for ("re-read .bmad-loop/policy.toml … first if you did not make that edit").
Scope
This is not specific to one field. Every RunState field has the property — policy_snapshot, the budget counters, paused_reason. trusted_config_digest is just the first one that reads like a security control, which is why it is worth either fixing or saying out loud.
Fixed for now by saying it out loud: the comment at the comparison site now states that it is advisory and why, so the next reader does not mistake it for the gate.
Options
Leave it advisory, documented. What is shipped. Defensible: the human-present path only warns by design, and an adversary who can rewrite state.json has other levers anyway.
Hold the resume baseline outside the project tree — e.g. alongside other user-scoped state rather than in the run dir. Closes it for this field only, and puts one RunState field somewhere different from the rest, which is a coherence cost.
Found while re-validating PR #495 (#461 point 4). Minor, but it makes a shipped line of code read as a security control when it is not one.
The asymmetry
PR #495 added two consumers of the launch-time
config_digest. Only one of them has a baseline an agent cannot reach.The auto-sweep gate is sound.
_sweep_factory'strusted_digestis threaded in memory from launch —cli.pycmd_runcomputes it once and passes it in, and_resume_paused_runpasses the freshly computednew_digest. It is never read back from disk, so a driven session has nothing to rewrite. That is the refusal path, and it holds.The resume warning is advisory. It compares against
state.trusted_config_digest:RunStateround-trips through<project>/.bmad-loop/runs/<id>/state.json(runs.RUNS_DIR = .bmad-loop/runs,run_dir_for) — inside the same agent-writable treeconfig_digest's own docstring says driven sessions can write anywhere in. A session that rewritespolicy.tomlcan also rewrite that field and the operator is never told. Blanking it is enough: thebool(...)guard reads""as "no prior pin, no warning".So the warning catches an incidental or accidental config change. It does not catch a deliberate one, which is the case it is worded for ("re-read
.bmad-loop/policy.toml… first if you did not make that edit").Scope
This is not specific to one field. Every
RunStatefield has the property —policy_snapshot, the budget counters,paused_reason.trusted_config_digestis just the first one that reads like a security control, which is why it is worth either fixing or saying out loud.Fixed for now by saying it out loud: the comment at the comparison site now states that it is advisory and why, so the next reader does not mistake it for the gate.
Options
state.jsonhas other levers anyway.RunStatefield somewhere different from the rest, which is a coherence cost.state.jsonintegrity as its own concern — the honest general fix, and much larger thaninitvendors the hook relay into the agent-writable workspace, turning a file-write primitive into unattended persistent code execution #461 point 4. Would subsume this.No action needed for #495 to land; the gate it adds is unaffected.
Related: #461, PR #495.