Skip to content

guard(deploy): the provision-python playbook path is stated in ansible and Python, and nothing checks they agree #15707

Closed

Description

@mrveiss

Found while closing out #15705. One path is stated in two languages and must stay byte-identical, and nothing checks that it does.

The ansible side — autobot-slm-backend/ansible/playbooks/configure-python-provision-permissions.yml:

provision_playbook: "/opt/autobot/code_source/autobot-slm-backend/ansible/playbooks/provision-local-python.yml"

That value is written into a NOPASSWD sudoers rule.

The Python side — autobot-slm-backend/api/code_sync.py:

_ANSIBLE_DIR = Path(DEFAULT_REPO_PATH) / "autobot-slm-backend" / "ansible"          # :1622
_PROVISION_PYTHON_PLAYBOOK = _ANSIBLE_DIR / "playbooks" / "provision-local-python.yml"  # :1623
cmd = ["sudo", "ansible-playbook", str(_PROVISION_PYTHON_PLAYBOOK)]                  # :1896

Sudoers matches that argv exactly. code_sync.py:1628 already records the constraint in a comment: "changing the command args would break the exact-match NOPASSWD sudoers rule", which is why the runner passes cwd= instead of adding arguments.

Why the failure mode is worse than a wrong path

The two values agree today. If either moves — DEFAULT_REPO_PATH changes, or the install root relocates and the playbook literal is updated to match — the other does not follow, and the symptom is sudo refusing the command on the provisioning path. Not a missing directory, not a clear "path not found": a permission failure whose cause is a string mismatch two files apart, in different languages, neither of which mentions the other.

#15705 deliberately left the ansible literal alone for exactly this reason: deriving it from autobot.base_dir would have made it a third independent source rather than closing the gap. The gap is the coupling itself, and it needs a check rather than a refactor.

Acceptance criteria

  • A guard asserts the two spellings resolve to the same string — reading the literal out of the playbook and the constructed path out of code_sync.py, so it fails when either side moves alone
  • The guard fails for the right reason: prove it by mutation, changing one side and confirming red
  • Both sites carry a pointer to the other, so someone editing either is told the constraint exists before they discover it through sudo
  • If a single source is genuinely reachable — the playbook templating its value from something the backend also reads — that is preferred over a guard, and the guard becomes unnecessary rather than additional

Related

Activity

  1. mrveiss commented on Sep 5, 2026

    @mrveiss
    OwnerAuthor

    Strengthened by review on #15706 — the drift is one env var away, not a relocation.

    I filed this saying the two spellings "agree today by coincidence" and would diverge if someone moved the install root. That understated it. Verified:

    # services/git_tracker.py:33
    DEFAULT_REPO_PATH = os.environ.get("SLM_REPO_PATH", "/opt/autobot/code_source")

    api/code_sync.py:98 imports that symbol. So any deployment that sets SLM_REPO_PATH moves the Python side immediately, while the playbook literal and the NOPASSWD rule it writes stay at /opt/autobot/code_source. No relocation, no edit, no migration — one environment variable, and sudo stops matching the argv on the provisioning path.

    That changes the severity and it changes the criteria. Added:

    • The check covers the env-override case, not only a literal edit — an SLM_REPO_PATH that differs from the playbook's value must fail loudly, at a point where the operator can see it, rather than at the moment sudo refuses a command

    A second defect found on the way, filed separately

    services/code_distributor.py:33 binds the same environment variable to a different default:

    module default
    services/git_tracker.py:33 /opt/autobot/code_source
    services/code_distributor.py:33 /opt/autobot

    Same name (DEFAULT_REPO_PATH), same env var (SLM_REPO_PATH), two different meanings — a repository checkout in one, an install root in the other. Unset they disagree by a path component; set, one of the two is necessarily wrong, because a single value cannot be both. Filed as its own issue since it is a distinct defect from this coupling.

    Related

  2. github-actions commented on Sep 7, 2026

    @github-actions
    Contributor

    PR #15901 (merged to Dev_new_gui) references this issue with a close keyword.

    guard(deploy): four deploy facts stated twice, now stated once and checked (#15707, #15724, #15687, #15604)

    If this issue is fully resolved, close it manually. If work remains, no action is needed.

  3. mrveiss commented on Sep 7, 2026

    @mrveiss
    OwnerAuthor

    Closed by #15901, merged as 539873f75. Verified against merged base (git show origin/Dev_new_gui:<path>).

    AC Evidence
    A guard asserts the two spellings resolve to the same string, reading the literal from the playbook and the constructed path from code_sync.py repo_tests/provision_playbook_path_agrees_15707_test.py
    The guard fails for the right reason, proven by mutation 3/3 mutants caught, reported in the PR
    Both sites carry a pointer to the other Verified in both directions. configure-python-provision-permissions.yml:45 — "api/code_sync.py builds _PROVISION_PYTHON_PLAYBOOK from DEFAULT_REPO_PATH… the NOPASSWD rule written below exact-matches that argv". code_sync.py:1621 — "These paths MUST match the sudoers grant created by ansible/playbooks/configure-python-provision-permissions.yml"
    A single source is preferred over a guard if genuinely reachable Considered and rejected with a stated reason: the ansible side derives from autobot.base_dir, the Python side from DEFAULT_REPO_PATH, and the sudoers grant exact-matches an argv built on the Python side. The playbook's own comment records why deriving from the ansible SSOT does not work here

    Note on what this cost, since the scheduling was the interesting part

    This was held behind #15880 on a predicted file-size ratchet collision: code_sync.py sat exactly at its ceiling, so adding a pointer comment would have forced a ceiling change in two mirror files while another PR was moving the same numbers.

    The collision never arose — the pointer comments already existed, added by #15705, so no code_sync.py edit was needed at all. The ordering was still correct: neither session could have known without looking, and looking was cheaper after #15880 landed than during it.

    A precaution that turns out unnecessary is not a precaution that was wrong. It was decided on the information available, and the information that dissolved it only existed later.

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions