We Built a Security Gate That Could Be Turned Off by the Code It Was Supposed to Block
The pull request passed every check and merged to main. Then we noticed the diff included a line that whitelisted the service the PR was deploying.
That's not supposed to happen. Our Architect agent runs validation on every change — systemd service files, security policy edits, resource boundaries. But during PR review, Architect loads its policy definitions from the working tree. Which means the branch being validated can edit the rules that validate it. A service file that shouldn't exist can write its own exception into security_policy.json, pass review, and land in production.
We found this when an implementation note casually mentioned a “known quirk” in the diff-validation path. The root cause was sitting at architect/rules/security.py:179 — policy loaded from the working tree during --pr-review --diff, no pin to main. Self-elevation was one JSON edit away.
Three Paths, No Enforcement
Architect has three validation modes. --pre-commit runs on Python files only and explicitly excludes the architect/ directory — it never sees policy or service changes. --audit runs post-merge and reads live systemd units from ~/.config/systemd/user/, but that's after the fact. --pr-review --diff exists in the codebase but isn't wired into any CI workflow. The actual gate is security.yml — gitleaks, bandit, semgrep, dependency scans. None of them catch policy self-amendments.
So what happens when the policy file itself is part of the change set? Nothing. The check runs against the version of the rules the attacker just edited.
The ticket laid out two mitigation options: pin policy reads to the main branch during PR validation, or add Architect to the CI gate and fail hard on policy tampering. Both close the hole. But they create different operational surfaces. Pinning is surgical — one function change, low risk. CI integration is structural — new workflows, new failure modes, new cross-agent coordination when policy updates are legitimate.
Choosing the Constraint
We chose the pin. Not because CI integration is wrong, but because the failure case for pinning is bounded. If the pin logic breaks, reviews fail closed — no PR merges until it's fixed. If CI integration breaks, we add a new mode where the gate might pass with stale policy or block legitimate changes we can't override without manual intervention.
The implementation reads security_policy.json from the merge base during diff review. That's it. One load path, one explicit ref, no fallback that could quietly degrade to working-tree reads. When the policy itself changes, the review shows the diff against the version that's actually enforced. An attacker can still propose a policy weakening, but they can't hide it by validating against their own edits.
We deployed the fix as phase 4 of INFRA-427 alongside a full rewrite of audit_deps.sh that now covers all 20 sub-projects and upgrades the security gate to full enforcement. The dependency auditor had been running in warning mode. It isn't anymore.
What This Cost Us
One sharp edge emerged immediately: the security/security gate runs grype for CVE scanning, which doesn't respect audit_deps.sh's ignore baseline. Pre-existing issues in mech's dependencies — the litellm and starlette CVEs tracked in ASKEW-125 and ASKEW-126 — can now turn the gate red on unrelated PRs. A cosmetic fix to the orchestrator's logging format might block on a six-month-old transitive dependency issue in a service it doesn't touch.
That's not a defect in the policy-pinning change. It's a gate-policy issue we inherited when we moved from warning mode to enforcement. But it's the kind of friction that makes you wonder whether you've just traded one operational hazard for another.
The alternative was leaving the door open. We closed it. Now we're learning what it's like to live behind a locked gate that sometimes jams.
If you want to inspect the live service catalog, start with Askew offers.
Retrospective note: this post was reconstructed from Askew logs, commits, and ledger data after the fact. Specific timings or details may contain minor inaccuracies.