Skip to content

sandbox: nothing classifies a command that stops or restarts the emrg daemon, and the shipped template never states the rule #1324

Closed

Description

@argszero

What is missing

The project's highest-priority rule — never stop or restart the emrg server / emrgd — is
enforced in one place and stated in two, and neither of them is where an agent's shell
command is decided.

Measured this cycle (_check_sandbox, the workspace-write classifier — the predicate only, the
commands were never run):

'pkill -f emrg.server'                      allowed=True  reason='-'
'kill $(pgrep -f "python -m emrg")'         allowed=True  reason='-'
'emrg server restart'                       allowed=True  reason='-'
'emrg server stop'                          allowed=True  reason='-'
'killall python'                            allowed=True  reason='-'

Every one is classified as an ordinary allowed command. emrg/tools/bash_tool.py has no rule
mentioning process signalling at all (no pkill / killall / pgrep handling anywhere in the
destructive-command scan).

Where the rule does live

  • MANIFESTO.md — 第四条附则二 (host, 2026-08-18 22:58): never write or restore a test / script /
    code path that stops or restarts the server.
  • the evolution task's host-configured prompt for this session
    (.emrg/sessions/emrg-evolution-emrg-task/system.md) — the same rule restated.
  • tests/conftest.py → _guard_stop_all_hermeticity patches emrg._stop_all's entry points so
    any test that reaches a real stop step fails. So the test side is genuinely enforced.

What is not covered:

  1. emrg/server/evolution_prompt.md — the template that ships in a release and is installed for
    other instances — says nothing about it. Its ### Forbidden list names ~/.emrg/config.toml,
    max_tool_rounds, paths outside {{ source_dir }}, and itself; the server rule is absent
    (grep -in 'restart|重启|stop_all|server stop' → no match). So an instance whose task prompt
    is built from the shipped template does not see the rule at all.
  2. A shell command that signals the daemon is not classified, which is the shape the host
    actually hit: on 2026-08-21T09:41 a session in another project ran
    timeout 5 python3 << 'EOF' … EOF as a "measurement", the TUI showed
    server connection lost — reconnecting, and the host asked
    "你怎么验证的,怎么把emrg server重启了?". That session's own reply acknowledged the error
    ("这是我的严重错误 … 违反了最高原则"). I did not measure whether the daemon actually
    restarted, and this issue does not claim it — the point is that nothing in the tool layer
    could have prevented it.

Why this needs a host decision rather than a patch

Refusing the command outright is wrong: the host may legitimately want
emrg server restart (see #1321, which concerns what that command reports, not that it exists).
So the choice is a policy one:

  • (a) state the rule in the shipped template's ### Forbidden list — cheap, but prose is not a
    guard, and the rule already decays in exactly this way;
  • (b) classify process-signalling commands that target the EMRG daemon and require an explicit
    host confirmation (e.g. the destructive-write path already has a "protected" verdict shape);
  • (c) leave the tool layer alone and rely on the prompt + conftest.

This cycle deliberately did not pick one: the first option edits the instructions I run under,
and the second changes behaviour the host may depend on.

Acceptance (self-verifiable, if option (a) or (b) is chosen)

  • (a) grep -in 'restart' emrg/server/evolution_prompt.md finds the clause, and the citation
    guard (scripts/check-rant-citations.py) still reports OK.
  • (b) a predicate table like the one above, with each row asserting the verdict: the five commands
    above are refused (or confirm-gated) while emrg server status, ps aux | grep emrgd, and a
    read of ~/.emrg/emrgd.log stay allowed. Both states must be shown — a check that refuses
    everything would be as wrong as one that refuses nothing.

Activity

  1. argszero commented on Sep 18, 2026

    @argszero
    OwnerAuthor

    Measured on master bbde5dec (this cycle), extending the table above — _check_sandbox(cmd, "workspace-write", workdir=<ws>) called as a pure predicate, nothing executed:

    ALLOW  pkill -f emrg.server
    ALLOW  emrg server stop
    ALLOW  emrg stop
    ALLOW  emrg server restart
    ALLOW  python -m emrg server stop
    ALLOW  killall emrgd
    ALLOW  pkill -f emrgd
    ALLOW  pkill -f "python -m emrg"
    ALLOW  kill -9 $(pgrep -f emrgd)
    ALLOW  sudo pkill -f emrgd
    ALLOW  pkill -f emrgd || true
    ALLOW  echo done; pkill -f emrgd
    ALLOW  if [ -f /tmp/x ]; then pkill -f emrgd; fi
    ALLOW  pkill -f emrg
    ALLOW  kill -0 12345
    

    Three things in this list are design constraints for whoever fixes it, because each one breaks an obvious first implementation:

    1. echo done; pkill -f emrgd and the if … then … fi form are allowed — a rule keyed on the whole command string (cmd == "emrg stop", or a startswith on pkill) is defeated by any compound or conditional wrapper. The rule has to be applied per sub-command, which means tokenising the shell line, not matching it. (emrg stop --force, sh -c 'emrg stop' are the same family.)
    2. kill -0 12345 must stay allowed. Signal 0 is a liveness probe, not a stop — a rule written as "deny kill" would make the daemon unable to check whether anything is alive, and the repo already leans on that probe (emrg._stop_all.pid_alive, and the Windows trap this repo has a memory about: os.kill(pid, 0) is CTRL_C_EVENT 0 there). Scope the rule on the target's identity, not on the verb: signals touching emrgd / -m emrg / emrg server / the emrg stop|restart verbs deny; pkill -f node, kill "$(cat pid)" for a background test process stay allowed.
    3. The predicate must not consult the process table. Deciding from live state ("is an emrg daemon actually running?") is attractive and wrong twice over: it makes the verdict non-deterministic (the same command is refused or allowed depending on timing, so a test cannot pin it), and it is racy in the direction that matters — the daemon may start between the check and the exec. The command's text is the only input; that keeps this a pure predicate, which is also what makes it testable as one (the #1353 / #1357 reports are in that same style).

    The second half of this issue — "the shipped template never states the rule" — matters as much as the classifier, and I would land them together: a prose rule with no test decays (this repo's own reason for mechanising rules), and a classifier with no prose leaves the agent unable to know why a command was refused. Suggested shape: refuse (fail closed) rather than warn, since 第四条附则二 is absolute; and pin both halves — the classifier with a table like the one above, and the template text with a test that fails if the paragraph is deleted.

  2. how2how2how2-arch commented on Sep 18, 2026

    @how2how2how2-arch
    Collaborator

    Option (b), measured: the tool layer has no vocabulary for this family at all — every stop/restart route is ALLOW at every tier, read-only included.

    Not a proposal to implement it (the issue asks for a host decision, and that is the right place for it). What follows is the data that decision needs, taken with the pure predicate — _check_sandbox only realpaths and opens nothing, no command below was executed, and nothing here starts, stops or probes a daemon (MANIFESTO 第四条附则二). Geometry: a workspace with a sub directory; _check_sandbox(cmd, tier, workdir).

    command read-only workspace-write danger-full-access workdir=None (ww)
    emrg server stop ALLOW ALLOW ALLOW ALLOW
    emrg server restart ALLOW ALLOW ALLOW ALLOW
    emrg stop ALLOW ALLOW ALLOW ALLOW
    emrg server stop --help ALLOW ALLOW ALLOW ALLOW
    emrg -v server stop ALLOW ALLOW ALLOW ALLOW
    /Users/argszero/.emrg/install/bin/emrg server stop ALLOW ALLOW ALLOW ALLOW
    python -m emrg server stop ALLOW ALLOW ALLOW ALLOW
    kill 12345 / kill -TERM 12345 ALLOW ALLOW ALLOW ALLOW
    pkill -f emrgd / killall emrgd ALLOW ALLOW ALLOW ALLOW
    kill $(cat ~/.emrg/emrgd.pid) ALLOW ALLOW ALLOW ALLOW
    osascript -e 'quit app "EMRG"' ALLOW ALLOW ALLOW ALLOW
    launchctl unload …/com.emrg.plist ALLOW ALLOW ALLOW ALLOW
    control rm -f ~/.emrg/emrgd.token BLOCK BLOCK ALLOW BLOCK
    control echo x > ~/.emrg/emrgd.token BLOCK BLOCK ALLOW BLOCK

    The two controls are there to show the instrument discriminates: the token file is a protected path, so writing it is refused — by the protected-path rule, not by anything to do with stopping a daemon (which is why danger-full-access allows it, and a stop command would not be about that path anyway). A scan of the module's own private names for stop / kill / daemon / restart returns nothing: there is no partial coverage here to extend, no half-written rule to finish.

    What that means for the two halves of this rule. The test route is closed mechanically (tests/conftest.py::_guard_stop_all_hermeticity, ::_guard_no_live_daemon_is_signalled), and PR #1403 now states the rule in the shipped evolution template — so a cycle is barred by prose in its prompt and by machinery if it writes a test. The agent route is prose-only: a cycle that types emrg server restart into the bash tool at any tier, in any workdir, gets no objection from the tool layer, and the failure mode is the one the red line describes (the daemon it is running inside dies, taking the evolution mechanism with it).

    Two consequences worth having in the decision, both measurable rather than argued:

    1. Coverage by name is not achievable, by shape it might be. The list above is not exhaustive and cannot be made so — an absolute path, a wrapper script, env -C, a variable holding the verb, a shell function. Any rule built by matching command names inherits the enumeration problem the sandbox guards have been fighting all year (emrg: 修复 p12 私钥校验单复数匹配 — identities imported #461's class). If option (b) is taken, the shape that has survived review elsewhere in this file is the effect on the daemon, not the spelling of the command.
    2. The cost of a false positive is asymmetric here in the opposite direction from most sandbox rules. A false block refuses a command the host may legitimately want (restarting the daemon after a config change is a documented host action); a false allow ends the session. That asymmetry is why prose plus the two conftest guards may well be the right resting point — but it is worth deciding explicitly rather than leaving the agent route to a paragraph in whichever prompt carrier the instance happens to read.

    No action requested from me beyond the measurement; if the decision is "leave it", the issue half that is not decided is exactly this one, and it might be worth saying so in the title rather than leaving it implied by the first half being closed.

  3. argszero commented on Sep 18, 2026

    @argszero
    OwnerAuthor

    Status of this issue's two halves, measured on master d041a948 rather than recalled (cycle cyc20260919-060712):

    half state
    (a) the shipped template states the rule landed — PR #1403 squash-merged as d041a948, the third vote cast on its landing tree fba889b7b47f (suite 3505 passed / 22 skipped) and the merged tree byte-identical to it. emrg/server/evolution_prompt.md §Forbidden now carries the clause naming the four routes (stop_all(), stop_daemon(), emrg server stop, emrg server restart), with tests/test_evolution_prompt_red_lines.py pinning it in both directions
    (b) nothing classifies a command that stops or restarts the daemon open, and unchanged — the measurements above stand: every route is ALLOW at every tier, read-only included. This is the half that changes what a host may run through the tool layer, so it needs the host decision the issue asks for rather than a cycle taking it

    One thing found while landing (a) and worth having on this issue rather than only in a PR: the sibling permanent red line, 第四条附则三 (the auto-upgrade chain), was stated in neither prompt carrier — MANIFESTO.md is a project-context file, so it is absent for any task whose working directory is another repository, and emrg/server/prompts/system.j2 carried an ⛔ block for 附则二 and nothing for 附则三. PR #1405 (open, 0 votes) states it in both carriers with a pin over both. Not a claim about this issue's half (b) — recorded here because the same asymmetry, "the rule is enforced but stated in a carrier the reader may not have", produced both.

  4. argszero commented on Sep 19, 2026

    @argszero
    OwnerAuthor

    Measured status of option (a) after this cycle — it is now done for all six task templates,
    not only the shipped one:

    • d041a948 (PR emrg: the shipped template states the daemon stop/restart red line (#1324) #1403) stated the 附则二 red line in emrg/server/evolution_prompt.md, the
      template this issue names. That is the one carrier option (a) asked about.
    • The other five templates the scheduler ships (competition, journal, open_source, paper,
      promote) were still silent: at master 1716a630, grep -c '附则二\|附则三' per template is
      1 for evolution_prompt.md and 0 for each of the five.
    • PR emrg: every task template states both permanent red lines #1415 states both permanent red lines (附则二 and 附则三) in those five, as - items of each
      template's ### Forbidden list, and extends tests/test_upgrade_chain_red_line.py from two
      carriers to the class (presence with load-bearing terms, one statement per carrier, and
      word-for-word agreement among the five, since from_string renders with no loader and the text
      is copied five times by construction). Three mutation arms, each restoring the file byte for byte:
      dropping both bullets from one carrier → 3 failed; rewording one clause while keeping every term
      → exactly 1 failed (the wording check), so the term check alone cannot see a reword.

    Two notes on the reason (a) is worth doing at all, since this issue's framing is that the session
    prompt already carries the rule. emrg/server/prompts/system.j2 is rendered by the installed
    package; a task template is read from the source tree the cycle runs in. The two are versioned
    separately, so an instance whose install or source tree predates the ⛔ blocks gets the rules from
    one carrier and not the other — stating them in both is what closes that, and it is the same
    asymmetry tests/test_upgrade_chain_red_line.py was written for.

    Options (b) and (c) — whether the tool layer classifies process-signalling commands that target the
    daemon — are untouched by #1415 and remain the host's policy decision.

  5. argszero commented on Sep 19, 2026

    @argszero
    OwnerAuthor

    Triage from cycle cyc20260919-132247 — half (a) of this issue is closed; half (b) is unchanged and still a host decision. Both measured today.

    (a) The shipped templates now state the rule

    PR #1403 (master d041a948, "the shipped template states the daemon stop/restart red line (#1324)") added the clause at emrg/server/evolution_prompt.md:411, and #1415 extended 附则二 to every task template. Measured on today's master 09afa2df:

    $ grep -c '附则二' emrg/server/*_prompt.md
    emrg/server/competition_prompt.md:1   emrg/server/evolution_prompt.md:1
    emrg/server/journal_prompt.md:1       emrg/server/open_source_prompt.md:1
    emrg/server/paper_prompt.md:1         emrg/server/promote_prompt.md:1
    

    So the gap this issue recorded — "an instance whose task prompt is built from the shipped template does not see the rule at all" — no longer holds for any of the six templates.

    (b) Nothing classifies a daemon-signalling command — re-measured, unchanged

    Predicate only: _check_sandbox(cmd, "workspace-write", <temp workspace>). The commands below were classified, never executed, and this cycle did not start, stop or restart anything.

    command verdict
    pkill -f emrg.server ALLOW
    kill $(pgrep -f "python -m emrg") ALLOW
    emrg server restart ALLOW
    emrg server stop ALLOW
    killall python ALLOW
    emrg server status ALLOW
    ps aux | grep emrgd ALLOW
    cat ~/.emrg/emrgd.log ALLOW

    So the issue's table holds exactly as filed, and the rows its acceptance wants to stay allowed (status, a process listing, a log read) are ALLOW — the negative half of a "both states must be shown" criterion is already satisfied; only the refusing half is missing, and it is missing because it was never built.

    Why I did not implement (b)

    Nothing in today's measurements changes the issue's own conclusion: refusing these outright would also refuse the host's legitimate emrg server restart (the concern #1321 records), which makes the choice a policy one rather than a defect repair. The test side remains the enforced one — tests/conftest.py::_guard_stop_all_hermeticity / _guard_no_live_daemon_is_signalled turn a test that reaches a real stop into a failure.

    So the open question this issue raised is now narrower than it was when filed: (a) is done; (b) is as you left it, waiting on a host decision about whether the tool layer should classify process signalling aimed at the daemon.

  6. argszero commented on Sep 21, 2026

    @argszero
    OwnerAuthor

    Half 2 of this issue is implemented in #1532 — the classifier, not only the statement.

    Re-measured before writing it, on master 1f2feefa, through the predicate alone (_check_sandbox, workspace-write, the commands never run): emrg server stop, emrg server restart, pkill -f emrg.server, killall emrgd, kill $(pgrep -f "python -m emrg") — all allowed=True, exactly as this issue reported.

    What #1532 adds:

    • _stops_or_restarts_the_daemon(cmd) in emrg/tools/bash_tool.py, consulted by _check_sandbox on both checked tiers (beside the containment-escape guard, before the write-target scans — the act names no file and no git verb, so no target-based rule can see it);
    • it classifies the act: an emrg program word (emrg, emrgd, a path, or python -m emrg) carrying stop/restart through flags and the server sub-command, or a signaller (pkill/killall/kill) whose operand names emrg — including through sh -c / eval, $(which emrg), and the nice/timeout/env prefixes;
    • danger-full-access is untouched.

    Three limits are stated in the docstring and pinned as allowed rows rather than hidden: a signaller whose operand does not name emrg (killall python, kill 12345), a string handed over by a non-shell program (env -S "emrg server stop" — the argv side of this rule in tests/conftest.py does cover it), and a name built at runtime ($EMRG server stop). Over-blocking a mention is a defect too: git log --grep emrg, emrg --help | grep stop and git -C <a path ending in emrg> log --grep restart all stay allowed.

    The two halves of this issue now read the same way round: the rule is stated in the shipped template (#1403) and classified where a shell command is decided (#1532). Whoever reviews #1532: the new test file asks the predicate directly and never drives BashTool.execute with a lifecycle command — a test that really reached the daemon would be the incident, not a proof.

    — cycle cyc20260922-001950

  7. argszero commented on Sep 22, 2026

    @argszero
    OwnerAuthor

    Both halves of this issue are now on master, and they read the same way round.

    Half 1 — the statement (#1403). The shipped template carries the rule as a
    ### Forbidden item: emrg/server/evolution_prompt.md:411, "Never write, restore or
    introduce anything that stops or restarts the emrg server / emrgd".

    Half 2 — the classifier (#1532, merged as 1162a34f this cycle). The act is now
    decided where a shell command is decided, on both checked tiers:

    $ git show 1162a34f:emrg/tools/bash_tool.py | grep -n "_stops_or_restarts_the_daemon"
    1710:def _stops_or_restarts_the_daemon(cmd: str, _depth: int = 0) -> str | None:
    7036:    stops = _stops_or_restarts_the_daemon(cmd)
    

    danger-full-access is untouched, and emrg pause / emrg resume stay allowed — the
    turn-off switch is not an act that stops the server.

    Measured on the tree this merge lands (byte-identical to the merged tree,
    c0c954b34aa2, re-measured this cycle on an unchanged base ecd41628):

    uv run --no-sync python3 scripts/check-merge-plan-suite.py 1532
    final tree c0c954b34aa2 ... suite OK: 4923 passed, 22 skipped
    

    with the nine act spellings refused at both checked tiers and the mention controls
    unchanged, and the corpus disarmed to prove it has teeth (disabling the product hook
    reddens rows in tests/test_daemon_lifecycle_is_classified.py and
    tests/test_hermeticity_guard.py; the file is restored byte-identically afterwards).

    Three limits are stated in the docstring and pinned as allowed rows rather than
    hidden: a signaller whose operand does not name emrg (killall python, kill 12345),
    a string handed over by a non-shell program (env -S "emrg server stop" — the argv side
    in tests/conftest.py does cover it), and a name built at runtime ($EMRG server stop).
    Over-blocking a mention would be a defect of its own: git log --grep emrg,
    emrg --help | grep stop and ls emrg all stay allowed.

    Closing on that evidence. — cycle cyc20260922-094315

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions