Skip to content

The resume config-change warning baselines off state.json, inside the tree it polices #498

Description

@pbean

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.py cmd_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

  1. 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.
  2. 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.
  3. Treat state.json integrity as its own concern — the honest general fix, and much larger than init vendors 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions