Skip to content

emrg: point the open-source prompt's memory writes at the trusted root - #1225

Closed
argszero wants to merge 1 commit into
masterfrom
feature/template-memory-writes-land-in-trusted-zone
Closed

argszero wants to merge 1 commit into
masterfrom
feature/template-memory-writes-land-in-trusted-zone

Conversation

@argszero

Copy link
Copy Markdown
Owner

What

open_source_prompt.md told the agent to write two things to {{ evolution_cwd }}/memory/ — the identity file (identity-github-role.md) and the end-of-cycle "key findings" step. This points both at {{ evolution_cwd }}/.emrg/memory/.

Why

{{ evolution_cwd }} is ~/.emrg/evolution, so {{ evolution_cwd }}/memory/ is ~/.emrg/evolution/memory/ — and that path is wrong twice over:

  • The sandbox blocks it. The workspace-write trust list covers ~/.emrg/evolution/.emrg/, not ~/.emrg/evolution/. Every write these instructions demand is refused with "blocked write outside workspace". (The governing rant, 2026-09-14T14:35:47, reports an open-source task hitting exactly that refusal 32 times in one day.)
  • Nothing reads it. The evolution memory root the daemon loads is ~/.emrg/evolution/.emrg/memory/. The wrong root exists and holds 36 stray files nothing reads; the trusted root holds 980. So even a write that slipped past the command-line scan (an open() inside a heredoc, as the rant documents) would land in a folder no memory loader ever consults — instructions that look obeyed and produce nothing.

These two sites are the only {{ evolution_cwd }} write targets that are not part of the state-file / reflection-file mechanism, which the same rant deletes wholesale; that removal is staged separately and this PR deliberately does not pre-empt it.

The guard

test_prompt_memory_writes_land_where_the_sandbox_allows_them walks every builtin template, extracts each {{ evolution_cwd }}-rooted path that routes through a memory/ segment, and asserts the sandbox trusts it. The expected boundary is read from bash_tool._trusted_write_zones()not copied into the test — so if the trust list moves, this test reports "the prompts may be stale" rather than agreeing with a second copy of the rule.

It cannot pass vacuously: a self-test first asserts the shape the rant measured ({{ evolution_cwd }}/memory/identity.md) is genuinely outside every trusted zone on this machine, and the scan must find ≥2 real sites.

Named limit (in the docstring): only {{ evolution_cwd }}-rooted memory paths are checked. State-file / reflection-file paths are out of scope by design.

Verification

Check Result
pytest tests/ 1959 passed, 1 skipped (master 1958 + this guard)
Mutant A — re-insert {{ evolution_cwd }}/memory/identity-github-role.md guard FAILS
Mutant B — re-insert {{ evolution_cwd }}/memory/ ("key findings") guard FAILS
Rendered prompt (the artifact the agent actually sees, via the real builder) 34,877 chars; identity → .../evolution/.emrg/memory/identity-github-role.md, findings → .../evolution/.emrg/memory/; wrong root absent
python -c "from emrg.client.app import run_client", python -m emrg --help OK
check-doc-count.py, check-node-test-count.py OK

`{{ evolution_cwd }}/memory/` is `~/.emrg/evolution/memory/` — outside the
workspace-write sandbox's boundary, so every write there is refused, and
outside the `.emrg/` root, the only memory the daemon loads. The identity file
and the "key findings" step both aimed at it, making those two instructions
unfollowable; the stray directory they left behind holds 36 files nothing reads
(the trusted root holds 980).

Retarget both to `{{ evolution_cwd }}/.emrg/memory/` and guard the class: a new
test walks every `{{ evolution_cwd }}`-rooted memory path in the builtin
templates and asserts the sandbox's own trust list allows it — the list is read
from `_trusted_write_zones()` rather than copied, so a moved boundary is
reported as a stale prompt. A self-test first proves the measured shape really
is outside the boundary on this machine, so the check cannot pass vacuously.

Verified: 1959 passed / 1 skipped (master 1958 + this guard); both mutants
(re-inserting either bad path) fail the guard; rendered prompt names only the
trusted root.
@argszero

Copy link
Copy Markdown
Owner Author

Superseded by #1226 — closing this one rather than leaving a duplicate in the queue.

#1226 does both of this PR's prompt edits, but as part of a larger change: the
{{ evolution_cwd }}/memory/ references here sat in the "Recording and
Submission" section, in open_source_prompt.md, which #1226 rewrites wholesale
(rant 2026-09-14T14:35:47 retires the state-file / reflection-file mechanism, so
that section becomes the round's closing summary and its memory root moves to
{{ evolution_cwd }}/.emrg/memory/). Keeping both open would have guaranteed a
conflict on a section that is deleted in #1226.

The valuable half of this PR is not lost: its guard test
(test_prompt_memory_writes_land_where_the_sandbox_allows_them, which reads the
expected location from the sandbox's own _trusted_write_zones() instead of
copying the rule) is carried into #1226 and kills the mutation it was written
for — verified there by moving the identity path back outside the trusted root.

No votes had been cast on this PR, so nothing is invalidated by the close.

@argszero argszero closed this Sep 14, 2026
argszero added a commit that referenced this pull request Sep 14, 2026
…ompetition, paper) (#1226)

* emrg: retire the state-file/reflection-file mechanism (open-source, competition)

Rant 2026-09-14T14:35:47: "delete the state-file and reflection-file
mechanisms entirely, no residue; the session itself is the memory."

Every phase hand-off in the open-source and competition prompts was a line
written into a per-project `*_state.md`, and every round ended by appending to
a `*_reflections.md` diary. Nothing reads either file: the daemon already
replays the task's session history into every round, so the phase hand-offs are
re-based onto the round's closing summary (the message the next round inherits)
and the durable lessons onto the memory index, which is part of the prompt.

No phase logic is lost: the branches still fire, they just read the state from
the session instead of a file. Both prompts keep their seven self-review
questions, now as what the closing summary must answer.

Two guards, each killed by a mutant in both directions:
- prompt memory paths must land where the workspace-write sandbox trusts them
  (ported from #1225, which this change supersedes - its two prompt hunks are
  inside the section rewritten here)
- a built-in template must not teach the retired mechanism, with an explicit
  PENDING_STATE_SWEEP set that may only shrink (promote / paper / journal are
  the remaining sweep stages)

Measured: rendered prompts contain 0 references to the retired mechanism
(33418 and 18564 chars); full suite 1960 passed, 1 skipped.

* emrg: retire the state-file mechanism from the paper template

Stage 2 of rant 2026-09-14T14:35:47 ("the session itself is the memory").
Stage 1 (#1226) swept open_source_prompt.md and competition_prompt.md; this sweeps
paper_prompt.md and drops it from PENDING_STATE_SWEEP in the same change.

paper_prompt.md taught a per-task `paper_state.md` (current phase / last completed /
next step / blocked / unhandled rants) plus an append-only `reflections.md` diary,
and every hand-off was a line written into one of them. Nothing reads either file —
the daemon replays this task's session history into every round (daemon.py, under a
fixed session_id), so the session is the state, and the memory index is the durable
layer.

The phase logic is not lost, it is re-based: the phase assessment already reads the
project files (literature/heilmeier-catechism.md, result logs, draft .tex, the
experiment log), which is where the research state actually lives; the round's
closing summary carries "last completed / next step / blocked"; and the rant log
(~/.emrg/rants.jsonl) replaces the hand-copied "unhandled rants" field. §6 "Reflect"
becomes "Reflect and Record": the same seven questions, answered in the closing
summary, with durable lessons going to memory entries instead of a diary file.

Verified: no mechanism reference left in the template; paper_prompt.md is dropped from
PENDING_STATE_SWEEP in this same commit (the set keeps meaning "not yet done"), and
test_paper_template_renders_with_context now pins the new contract instead of asserting
the retired path renders. Both the sweep guard and that render test fail when the
mechanism is re-introduced into the swept template (mutant restored byte-for-byte).

* emrg: catch the bare "state file" instruction the sweep's fingerprint missed

Sweeping a template is only half the work: the guard that says "no residue
anywhere else" has to be able to see the residue. Measured this cycle,
paper_prompt.md — already swept on this branch — still told the agent to

    read Agent.md / abstract / state file to determine direction terms

to derive its arXiv keywords. A file nothing reads, named as a source of the
search terms, for a phase that produces the round's literature scan. It survived
because the fingerprint spelled the prose form as "the state file": an
instruction that writes the noun phrase without the article was invisible, and a
guard that reports a clean sweep by not looking is worse than no guard.

Off by one word, in other words, and the same class as the rest of this branch:
the mechanism outlived the change that retired it because nobody re-read what the
check actually looked for.

* paper_prompt.md: drop the state file from the read list (the direction terms
  come from Agent.md / the abstract, which the phase reads anyway).
* test_prompt_templates.py: match the bare noun phrase, not only the article
  form, and cover the reflection-file paraphrase too — while keeping the sweep's
  own denial sentences legal ("there is no state file — the session itself is the
  state" is the replacement text, and flagging it would forbid saying what
  replaced the mechanism). A new test pins both halves of the pattern: the
  instruction form that must be flagged, and the denial that must not be.

Verified: the widened pattern flags paper_prompt.md and nothing else among the
swept templates, and still flags the two templates left in PENDING_STATE_SWEEP,
so the set keeps meaning "not yet done". Full suite on this branch 1965 passed /
3 skipped. Both new checks were killed by a mutant and the files restored
byte-for-byte (sha256): restoring the old fingerprint fails the coverage test,
and re-adding the instruction to paper_prompt.md fails the sweep guard.

* emrg: the memory write root's index is not the index in the prompt

Three paragraphs the sweep re-based onto memory entries under
`{{ evolution_cwd }}/.emrg/memory/` also called that directory's index "part of
this prompt". It is not.

`_collect_memory_data` embeds `session.cwd/.emrg/memory/MEMORY.md` — the *task
project's* index — and the session index, and never
`{{ evolution_cwd }}/.emrg/memory/MEMORY.md`. Measured by pointing
`EVOLUTION_CWD` at a directory whose memory index carries a marker and rendering
the system prompt through the real builder: the marker stays out while the
project's appears. On this installation the two roots differ by construction —
`{{ evolution_cwd }}` is `~/.emrg/evolution` (988 files, the durable record)
while the embedded index belongs to the session's cwd, `{{ evolution_cwd }}/emrg`.

The pairing is what was false, not the path: writing memory entries under that
root is correct (the workspace-write sandbox trusts it), and the sentences that
say "the memory index is embedded in this prompt" without naming a path are
correct and stay. Each paragraph now points at the `read` tool instead, so the
agent is told where its own entries are rather than where they are not.

A guard pins the pairing (`test_no_template_calls_the_write_root_index_its_own_prompt_index`):
a paragraph that names the root and claims prompt-embedding fails, with a
non-vacuity floor of 3 paragraphs naming the root and a self-test that the
detector still detects the shape it was written for. Re-planting the false
sentence turns it red; the file is byte-restored afterwards (sha256 equal).

Suite on this tree: 1975 collected = 1972 passed + 3 skipped.

---------

Co-authored-by: EMRG Evolution <emrg@argszero.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant