Skip to content

feat(evolution): pin invariant rules outside the agent-writable tree — increment of #3518 - #3560

Merged
Lexus2016 merged 4 commits into
mainfrom
evolution/issue-3518-invariant-core
Oct 31, 2026
Merged

Lexus2016 merged 4 commits into
mainfrom
evolution/issue-3518-invariant-core

Conversation

@Lexus2016

Copy link
Copy Markdown
Owner

Slice 2 of the replay-verified self-edits roadmap (#3515), child issue #3518.

What this slice ships

scripts/evolution_invariant_filter.py judged every proposal against scripts/evolution_invariant_rules.json — a file inside the tree the agent commits to, so a self-edit could rewrite the rules it is judged against. The default load now resolves a pinned copy outside the checkout (~/.hermes/evolution/pinned/, override $EVOLUTION_PINNED_RULES), installed once at deploy time by copying the in-tree seed there. The filter refuses to run when that surface is missing or shadowed by an in-tree copy. An explicit path argument still reads as-is, so existing callers are unchanged.

Tests

  • A rewritten in-tree rules file cannot override the pinned one (pinned rule fires, rewritten rule does not).
  • A missing pinned surface fails closed.
  • A shadow file beside the pinned surface fails closed.
  • Hermetic fixture keeps the whole suite off the machine's real pinned file. 31 passed.

Checks

lint (blocking PLW1514) ✓, format ✓, pre-PR shard ✓, 198 changed lines.

Deferred (next increment):

  • wire the deploy-time copy into the installer so fresh installs seed the pinned surface automatically
  • extend the same pinning to the evidence gate and the evolution stage contracts

Hermes Evolution added 2 commits October 30, 2026 22:36
…3518)

The invariant filter now judges proposals against a pinned rules copy under
~/.hermes/evolution/pinned (override: $EVOLUTION_PINNED_RULES), installed once
at deploy time by copying the in-tree seed there. The default load refuses to
run when that surface is missing or shadowed by an in-tree copy, so a self-edit
that rewrites scripts/evolution_invariant_rules.json cannot weaken the checks
that govern it. An explicit path argument still reads as-is.

Deferred (next increment):
- wire the deploy-time copy into the installer so fresh installs seed the
  pinned surface automatically
- extend the same pinning to the evidence gate and evolution stage contracts

Co-Authored-By: Hermes Evolution <evolution@hermes.ai>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T21:44:14.104554Z b5c4c2c PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on 96bfa84 — fix(evolution): seed the pinned invariant surface on deploy

❌ Job failures

No production imports of the tools.lazy_deps stub / No production imports of the tools.lazy_deps stub · View job

Job No production imports of the tools.lazy_deps stub / No production imports of the tools.lazy_deps stub failed.


Python lints / Windows footguns (blocking) · View job

Job Python lints / Windows footguns (blocking) failed.


Python lints / ruff enforcement (blocking) · View job

Job Python lints / ruff enforcement (blocking) failed.


Python tests / Run tests slice 1/8 · View job

Job Python tests / Run tests slice 1/8 failed.


Python tests / Run tests slice 2/8 · View job

Job Python tests / Run tests slice 2/8 failed.


Python tests / Run tests slice 3/8 · View job

Job Python tests / Run tests slice 3/8 failed.


Python tests / Run tests slice 4/8 · View job

Job Python tests / Run tests slice 4/8 failed.


Python tests / Run tests slice 5/8 · View job

Job Python tests / Run tests slice 5/8 failed.


Python tests / Run tests slice 6/8 · View job

Job Python tests / Run tests slice 6/8 failed.


Python tests / Run tests slice 7/8 · View job

Job Python tests / Run tests slice 7/8 failed.


Python tests / Run tests slice 8/8 · View job

Job Python tests / Run tests slice 8/8 failed.


debug info

CI timings

CI timings · View report · View job

Wall time 17m24s vs 17m1s (+2.3%). 13 job(s) slower, 8 faster, 1 unchanged.

  • Python tests / Run tests slice 4/8: +329.0s
  • Python tests / Run tests slice 2/8: -207.0s
  • Python tests / Run tests slice 7/8: -204.0s
  • Python tests / Run tests slice 3/8: +165.0s
  • Python tests / Run tests slice 5/8: +140.0s

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b5c4c2c875

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/evolution_invariant_filter.py Outdated

# Slice 5 (#3518): the rules the filter judges against live OUTSIDE the tree
# the agent commits to, so a self-edit cannot weaken the checks that govern it.
_PINNED_RULES_DIR = Path.home() / ".hermes" / "evolution" / "pinned"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve the pinned directory from the active Hermes home

When HERMES_HOME points to an isolated installation or named profile, this import-time constant still reads rules from the user's default ~/.hermes directory. Consequently, a profile with its own pinned rules either rejects every proposal because the default file is absent or silently evaluates proposals against another profile's policy. Resolve the directory through get_hermes_home() at call time instead of capturing Path.home() at import. This violates the profile-aware path requirement in AGENTS.md.

AGENTS.md reference: AGENTS.md:L140-L145

Useful? React with 👍 / 👎.

Comment on lines +100 to +102
pinned = pinned_rules_path()
_assert_not_shadowed(pinned)
if not pinned.is_file():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject pinned paths that resolve inside the checkout

If EVOLUTION_PINNED_RULES points to scripts/evolution_invariant_rules.json, or to an external symlink targeting that file, the default loader accepts the agent-writable rules unchanged. _assert_not_shadowed() only checks two sibling filenames; it never verifies the location of the file actually being loaded. Both configurations therefore defeat this change's outside-the-tree invariant and allow subsequent in-tree edits to change the judging policy. Resolve the selected path and reject checkout-contained targets before loading it, while retaining the documented explicit-path audit API.

Useful? React with 👍 / 👎.

Comment on lines +296 to +297
@pytest.fixture(autouse=True)
def _hermetic_pinned_surface(tmp_path, monkeypatch):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Share the pinned-rules fixture with the existing gate tests

On a clean test machine without an operator-installed pinned file, this module-local fixture only provisions rules for test_evolution_invariant_filter.py. Existing callers in tests/scripts/test_evolution_harness_gate.py remain unprovisioned: for example, test_run_gate_verdicts expects validated, but run_gate(PROPOSAL, gate_runner=_green) now returns invalid with rules-unavailable before invoking the runner. Move the provisioning into a shared fixture or provision the other consumer tests explicitly so the suite remains hermetic and preserves its existing gate coverage.

Useful? React with 👍 / 👎.

Lexus2016 and others added 2 commits October 30, 2026 23:41
The default load now reads the rules from outside the checkout, so a fresh
host failed closed with 'rules-unavailable' and the harness gate rejected
every proposal: tests/scripts/test_evolution_harness_gate.py went 12/12 ->
2/12 on this branch.

- install_pinned_rules() + '--install-pinned': idempotent deploy-time copy of
  the in-tree seed; refuses to overwrite a diverged surface without force.
- The pinned dir resolves from $HERMES_HOME (host-level, not per-profile), so
  the surface sits where the rest of the host state does.
- The fail-closed error names the exact command to run.
- Harness-gate tests simulate a deployed host; installer + resolution have
  their own tests.

tests/scripts -k 'evolution or harness or invariant': 26 failed on main and 26
on this branch, identical ids (pre-existing debt); 46/46 in the two touched
modules.
@Lexus2016

Copy link
Copy Markdown
Owner Author

Reviewed, amended, and merged.

Blocker found on the branch: the default load now resolves the pinned surface, but nothing seeded it. On a host without ~/.hermes/evolution/pinned/evolution_invariant_rules.json the load failed closed with rules-unavailable, so evolution_harness_gate rejected every proposal: tests/scripts/test_evolution_harness_gate.py was 2/12 on the branch vs 12/12 on main. The filter's own module stayed green, which hid the integration break.

Pushed 96bfa845 to the branch:

  • install_pinned_rules() + --install-pinned: idempotent deploy-time copy of the in-tree seed; refuses to overwrite a diverged surface without force.
  • The pinned dir resolves from $HERMES_HOME (host-level: $EVOLUTION_PROFILE_DIR is deliberately not honored, one host judges every profile by one rulebook), default ~/.hermes/evolution/pinned/.
  • The fail-closed error now names the exact command to run instead of "copy the file there once".
  • Harness-gate tests simulate a deployed host; the installer and the path resolution have their own tests.

Verification (local, Python 3.14.7):

  • tests/scripts/test_evolution_invariant_filter.py + test_evolution_harness_gate.py: 46 passed.
  • pytest tests/scripts -k "evolution or harness or invariant": 26 failed on main and 26 on this branch with identical ids (pre-existing debt), 1764 passed.
  • Code-health ratchet vs f1000232: 0 blocking, 0 advisory.
  • --install-pinned smoke test: creates the surface, second run is a no-op.

Merged as an admin squash: the required context All required checks pass (v2) is red on main itself (pre-existing slice debt), so CI cannot gate this PR.

Deploy note: each host needs python scripts/evolution_invariant_filter.py --install-pinned once before the next cycle. Done on the maintainer host; wiring the copy into the installer stays the next increment, as stated in the PR body.

@Lexus2016
Lexus2016 merged commit a6772da into main Oct 31, 2026
34 of 45 checks passed
@Lexus2016
Lexus2016 deleted the evolution/issue-3518-invariant-core branch October 31, 2026 00:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant