8000 The door rule on every host and role (M9.31) by adeelahmad · Pull Request #105 · adeelahmad/aloop · GitHub
[go: up one dir, main page]

Skip to content

The door rule on every host and role (M9.31) - #105

Draft
adeelahmad wants to merge 19 commits into
masterfrom
claude/m9-ethics-principles-goals-a6hfmm
Draft

adeelahmad wants to merge 19 commits into
masterfrom
claude/m9-ethics-principles-goals-a6hfmm

Conversation

@adeelahmad
Copy link
Copy Markdown
Owner

Requested by Adeel · project thread

Before: the door gate (M9.30) reached only Claude Code and Codex, which read hooks.json. OpenCode and dsh never ran it. An agent there could push to main, merge or publish unasked, and could even write its own standing authorization, because doors authorize refused only Claude's shell. On every host, a dispatched role started without the core principles. Codex's apply_patch and its request_user_input question tool also slipped past the gate.

After: the gate decides every tool call on all four hosts, and every dispatched role carries the core principles.

How:

  • Gate (doors.py, doors.json):
    • The question tools of all four hosts are mapped.
    • apply_patch paths are judged as writes.
    • dsh's {arguments: "<json>"} MCP input is decoded once, and only in that exact shape.
    • A bound worker's workdir (OpenCode, dsh) sets where its command runs.
    • doors authorize|revoke refuses Codex, dsh and OpenCode agent shells.
    • Door rows name the role that hit the door. A subagent is told to report ASK[door] to its supervisor.
    • One deadline covers the whole decision, and the lock polls, so a held lock fails closed instead of hanging.
    • Codex, OpenCode and dsh settings files and aloop's host adapters are one-way, and OpenCode's <server>_<tool> MCP names match.
  • Roles: mission principles hook returns the core block as SubagentStart context on Claude Code and Codex. The dsh install appends it to each role persona. OpenCode pushes it into child sessions and into a resumed root.
  • Plugins: aloop.js and aloop.mjs run mission doors hook last, after every other gate has admitted the call, with a 15 s fail-closed timeout. They skip read-only tools.
  • _host.py names each host's question tool and the door gate.

Evidence (so far; an adversarial review and the full suite are running):

  • Each part was written test first, and its new tests failed on the old code.
  • mission-doors.test.py: 29 checks (21 before). opencode-plugin.test.mjs: 45. dsh-plugin.test.mjs: 48 (40 before). dsh-install.test.py: 10. codex-hooks.test.py: 17. hook-matchers.test.py: 17. mission-principles.test.py: 9. host-adapter.test.py: 14.
  • On the real pinned dsh (0.2.1-rc.2) with a scripted model, dsh-runtime.test.mjs passes 32 checks. A scratch variant showed a role's git push origin main gets ASK[door] and the core block appears once in each role's system prompt.
  • A human's ! command on Claude Code skips PreToolUse. This is static evidence from the 2.1.304 binary: bash mode calls the Bash tool directly with userTypedShellDispatch. A live TTY measurement is listed as a follow-up.

Generated by Claude Code

The door gate now reads every host's calls: Codex's request_user_input is the
question tool; apply_patch (Codex command, OpenCode patchText) is judged path by
path as a Write; dsh's {"arguments": "<json>"} MCP input is read as its object
(same doors, same once digest); OpenCode's <server>_<tool> MCP names match;
OpenCode and dsh seed the Bash reader from tool_input.workdir. authorize/revoke
refuse DSH_SHELL, CODEX_THREAD_ID and ALOOP_AGENT_SHELL shells as well as
CLAUDECODE. door.denied/door.crossed name the agent (agent_id, agent_type) and a
subagent is told to report ASK[door] to its supervisor. One deadline covers the
whole decision and the lock is polled until it, so a held doors.lock is exit 2,
never a hang. change-agent-settings covers Codex, OpenCode and dsh settings and
aloop's host adapters. mission-doors.test.py: 21 -> 29 checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rsDo9GYzNx8tXucywZHnA
…r gate

`mission principles hook` is a SubagentStart hook: inside an aloop project it
hands the role the core block (the door rule first) as additionalContext,
exactly as `principles show --core` prints it; outside one, or on any error,
it prints nothing and exits 0, so it never blocks a dispatch. hooks.json runs
it third in the SubagentStart "*" group (timeout 10), which covers Claude Code
and Codex. Each host adapter's tool policy now names the door gate, and each
names its ask-the-human tool (QUESTION, shown by `host describe`).

Tests: the hook's output in and out of a project and on bad input; hooks.json
wiring for every role; the Codex runner delivers the block and the door gate
denies a root `git push origin main`; host describe. The OpenCode/dsh parity
check in hook-matchers stays red until their M9.31 parts land.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rsDo9GYzNx8tXucywZHnA
tool.execute.before runs `mission doors hook` as the last PreToolUse check
(after budget; it records door.crossed), skips read-only tools, gives it
15 s and fails closed on a timeout, signal, spawn error or an exit other
than 0/2. `question` maps to AskUserQuestion. shell.env marks the agent's
shell ALOOP_AGENT_SHELL=1. Sub-agents and a resumed root get the core
principles block (read once per process), never twice in one call.
Tests: door denials, the live-run question rule, ordering and fail-closed
via a stub plugin root; the workdir (C2) and authorize-refusal (C3) cases
are expected red until the gate side lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rsDo9GYzNx8tXucywZHnA
    
The dsh plugin runs `mission doors hook` last in PreToolUse, after budget,
because it records the crossing (a once-authorization is used up). Read-only
built-ins skip the spawn. A 15 s timeout, a kill, a crash, a spawn error or any
exit other than 0/2 denies, so the gate fails closed.

ask_user_question maps to AskUserQuestion and copies only its declared fields.
An MCP tool's tool_input is its arguments object, as on Claude Code, so a door
that matches an input field (push_files to main) now hits. A nested
`arguments` key carries the whole object, so it cannot hide a field.

`aloop install dsh` adds the core principles block (`mission principles show
--core`) to the end of every role's persona, once. Children get no
SessionStart context, and the text is the same on every render.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rsDo9GYzNx8tXucywZHnA
The gate read any `arguments` string as an MCP tool's whole input, so on
Claude Code `branch: main` beside `arguments: '{"branch": "x"}'` crossed
no door, and the hook decoded dsh's object shape twice. Only the exact
{arguments, workdir} shape is decoded now, once. The OpenCode test's two
expected-red checks are hard checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rsDo9GYzNx8tXucywZHnA
…h forms, MCP names

- A deadline is a BaseException (Deadline): a step's `except Exception` can no
  longer swallow it and turn a stalled decision into an allow.
- apply_patch is read as the hosts parse it: any spacing or indent around a
  header (doors, host-adapter, both scope gates, the OpenCode worktree
  rewrite), and run through the shell (Codex's heredoc and argument forms,
  applypatch, after a cd, from a pipe).
- A relative OpenCode or dsh workdir is resolved under the payload's cwd; a
  missing one is unknown.
- A path through a symlink then `..` is also read as the kernel walks it, and
  an edit's path relative to this checkout when outside the project.
- dsh: an MCP tool's input is its own arguments (no workdir), so its once
  digest is the same from any directory.
- OpenCode: an MCP tool reaches the gates as mcp__<server>__<tool>, from
  client.mcp.status(), the longest server first; codesearch skips the gate.
- opencode.json is a door only where OpenCode reads it; settings replaced,
  moved, linked or removed by cp/mv/ln/install/rsync/rm/sed -i/perl -i are
  the settings door.

Each fix has a test that failed on the code before it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rsDo9GYzNx8tXucywZHnA
…ch; docs for M9.31

Conflicts: the OpenCode plugin's header comment (gate-dispatch on task from
master, the door gate last from this branch) and codex-hooks.test.py (both
blocks kept). ROADMAP: M9.31 done, new items M9.48, M9.49, M9.54-M9.59, and
the M9 row of section 0 brought up to date; TRACKING, CHANGELOG, OVERVIEW.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rsDo9GYzNx8tXucywZHnA
…e M9.31 branch

ROADMAP: master's M9.54 kept; this branch's new items renumbered M9.55-M9.60.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rsDo9GYzNx8tXucywZHnA
… MCP names, BOM)

- A bare redirect, and a group's, subshell's or compound's own redirect or
  heredoc, reach the commands inside (views() follows "(" ")" "cwd+" "cwd|"
  "cwd-"; simple() emits a write-only view for a Cmd with no words).
- A cd after && or || leaves the directory unknown once its chain ends.
- git -C, cd -P and a dsh workdir walk a symlink before `..` (an Either
  context of the lexical and the kernel path); paths also match through a
  glob and relative to the checkout that holds them.
- copied(): cp/mv/ln/install/rsync write DEST, DIR/NAME and DIR/*; removing
  or moving a directory that holds settings, sed's abbreviated --in-place
  and OpenCode's config.json are the settings door.
- MCP tool names: hyphenated servers and aggregators (zapier, composio)
  reach send-external-message, visibility-or-sharing and push-default-branch;
  a service word inside a read tool's name is no door.
- plugin/bin/_patch.py is the one apply_patch header reader (U+FEFF counts
  as space, as OpenCode's trim does), shared by doors, host-adapter and both
  scope gates; OpenCode's header rewrite takes the whole line.
- ROADMAP: M9.31 text no longer claims the scope gates read the shell form
  (M9.58); M9.57 lists the remaining settings writers; M9.61 computed paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rsDo9GYzNx8tXucywZHnA
…n, MCP names)

- A cd that may not run (after && or ||, before ||, in a group or
  compound) leaves each later command in either directory: one view per
  directory instead of UNKNOWN, so a settings write after it is still
  placed, an ordinary command is no wall, and an absolute cd returns.
- cd -, cd -- -, a bare pushd, pushd/popd -n and ±N, cd X &, an eval's
  cd, cd '', ~+ and ~-, and GIT_DIR / GIT_WORK_TREE (prefix, env, export)
  are read as the shell reads them; a pipe into a { } group or compound
  feeds each command inside; a group's heredoc feeds only commands with
  no stdin of their own (no exponential re-feeding).
- copied() returns plans; place() resolves them where the command runs:
  DEST/NAME only into an existing directory (backups and copies into
  .claude pass), -t with {} or xargs arguments, --parents, rsync -R and
  --remove-source-files, GNU long-option prefixes, options after operands;
  mv's sources are writes; ln and cp -l/-s of a settings file are doors.
- Globs expand only where the shell expands them, through links, keep
  every config-like name; checkout_rel only for this project's checkouts;
  a home-relative form for ~/.claude and friends.
- builtin, setsid, flock, taskset, chrt and script -c are wrappers;
  source /dev/stdin runs its stdin.
- Catalogue: anchored settings directories, Composio VERBS_A_, kebab-case,
  scheduled, conversations_add_message and verb-first sends; drafts pass;
  GitLab/Bitbucket/Composio merges; Drive access grants.
- ROADMAP M9.62 records what is left (while-condition heredoc, recursive
  record copies, ln -sT of a settings dir, git aliases).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rsDo9GYzNx8tXucywZHnA
…d after ||, OLDPWD, wrapped cd)

- copied(): a no-value long option (rsync --checksum, install --strip) is no
  prefix of a value option; only an operand right after an unknown option
  widens the destination (at most 8), so long operand lists stay linear;
  ln -n/--no-dereference replaces a linked DEST.
- copy_writes(): glob sources expand to what they match, contents copies
  list SRC's entries, unseen names are DEST/? (no root settings ASK).
- views(): cd after || unions with the success path, OLDPWD follows a cd
  that may not run and is saved around subshells, `|| { …; exit; }` keeps
  the line in the cd's dir, a group after || starts where the left failed,
  a wrapped cd moves no shell; candidate dirs pass into nested reads.
- _shellread: a closing mark carries the list operator after it (Mark.end).
- placed(): an unplaceable relative path is normalized (./.claude).
- Catalogue: $VAR/ prefix and DEST/? for settings dirs, record-dir contents
  forge the record, draft-sending tools send, Drive grant_batch shares.
- mission-doors.test.py section 21d (3 checks, 92 cases red on 88204c6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rsDo9GYzNx8tXucywZHnA
…oop stop) into the M9.31 branch

Conflicts: both sides' plugin constants and helpers kept (the door gate's
timeout and read-only set beside master's NEEDS-HUMAN release), the dsh
test's question helper renamed (master added a child named `ask`). This
branch's open items move to M9.56–M9.65: master took M9.55 and #114 claims
M9.48.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rsDo9GYzNx8tXucywZHnA
…-execdir, env -C, staged copies, dotglob, cp -T, ${X:?}, drafts)

Seventeen confirmed findings from the fifth adversarial review of the
fourth review's fixes, each with a test that was red before the change
(section 21e of mission-doors.test.py, 45 checks):

- directories: an exit inside `|| { ... }` that may not run keeps the
  group's cd; a failed cd's directory no longer survives `&&` or a
  compound end; a second conditional cd keeps where the list stopped; a
  background list (`cd X && make &`) moves no shell; find -execdir and
  env -C/--chdir move their command
- copies: `--` ends the options (crashed before); glob and contents
  copies see files the same line staged; `*` names dotfiles; cp -T and
  rsync -R SRC/./ copy SRC's entries; `$VAR` directories are settings
  roots only for $HOME; an unseen name into the ledger's directory is a
  forgery; a large directory copied whole is placed once per directory
- catalogue: absolute paths after an unknown cd, `$(pwd)` and `${X:?}`
  paths, and an even draft guard (forward_as_draft and send_mail_draft
  write drafts; send_saved_draft sends one)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rsDo9GYzNx8tXucywZHnA
adeelahmad pushed a commit that referenced this pull request Oct 20, 2026
…rney (M9.68)

The first live mission run stopped safely at the baseline's pre-grant
check (11/24, $0.33). What it found:
- A reply session knows no name: the supervisor wrote --by from the
  account's context. Every reply prompt now names who types the line,
  and the stub host picks a name of its own when none is given.
- The pre-grant checks judged the first journey file, a stray draft,
  instead of the baseline's own journey.
- The rival constraint now says the baseline lists none.
- journeys new wrote three broken drafts the researcher could neither
  fix, delete nor retire, so its stop gate was stuck-released. It now
  refuses a draft that journeys check would block and writes nothing.

ROADMAP: this branch's M9.60-64 become M9.63-67 (#105 uses M9.56-62);
new M9.68 (done) and M9.69 (a grant's by is bound to its typed line).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RxETAY6S1gcmdctBK8a8Z
…sh-clone stop) into the M9.31 branch

ROADMAP: the M9 row keeps this branch's done/open lists and adds M9.42
(done in #114) and M9.48 (open); the M10 row takes master's fresh-clone
stop and says the OpenCode plugin exports ALOOP_AGENT_SHELL since M9.31.
aloop.js: the SubagentStart comment names both the core principles and
the artifact gate's per-task start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rsDo9GYzNx8tXucywZHnA
…listed copy sources, $VAR/.claude, variable last names, draft names)

Eight confirmed regressions from a hunt on the fifth review's fixes, each
with a test that was red before the change (section 21f of
mission-doors.test.py, 48 checks; 45 new assertions red on 2ffdd59):

- the shell that reads a word expands its $PWD and $(pwd): words carry
  a mark that chain() binds, so the text a nested shell reads (sh -c
  "...", eval, script -c, flock -c, <<<, an unquoted heredoc, echo into
  a pipe) keeps the outer directory, and env -C, git -C or find -execdir
  move the command but not its already-expanded words (view["shell"])
- `echo ... && exit 1` and `return 1 || exit 1` always exit (printf only
  without conversions, and no write redirect)
- a contents or glob copy of a directory that is missing now is DEST/*,
  and of one that is empty now adds DEST/? to the names the line wrote:
  tar, curl -o and unzip fill it unseen
- a single-source copy onto "$VAR/.claude" may become that directory
- a variable last name in a guarded directory (.aladdin/logs/$F,
  .claude/${F:?}) is any name there
- globbed() lists a glob's directory once for dot entries instead of
  lstat-ing every name (95,000 files: 1.1 s to 0.4 s)
- the draft-writer exclusion needs a separator before message, mail,
  email or as (send_gmail_draft sends again)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rsDo9GYzNx8tXucywZHnA
…er's classify

The macOS runner failed the held-lock check (4d0b1de): with a 1 s
deadline, the work before the lock took the whole second there, so the
gate failed closed with the generic deadline message instead of naming
doors.lock. The check now warms and times that work in the same project
and gives the decision max(2 s, 4x it), so only the held lock can run the
clock out; the assertions (exit 2, BLOCK naming doors.lock, nothing
recorded, bounded time) are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rsDo9GYzNx8tXucywZHnA
adeelahmad pushed a commit that referenced this pull request Oct 20, 2026
…t once, ids M9.80-87

An adversarial review of the live-run fixes confirmed 9 findings:
- seed_test_kept accepted an added def that rebinds the seed's test
  name (replacing it), an import or class shadowing TodoList, and code
  run at import through a decorator, default, annotation, base or class
  body. Added statements must now rebind none of the seed's names and
  run no code at import (pytest's own decorators with plain arguments).
- No test pinned check_product's wiring: it now has one on the stub
  run's repo (an append passes, a redefinition fails).
- journeys new returned on a runner problem before the other problems
  and the "nothing written" footer; it now names every problem at once.
  Its usage offers --from as the one form.
- ROADMAP ids M9.63-70 clash with #105 and #111: now M9.80-87.
- Docs: 39 checks, the M9.87 block count, run 1's three stop causes,
  and a test that pins the rival constraint's new wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RxETAY6S1gcmdctBK8a8Z
…word, nested cd $PWD keys, multi-dir $PWD, "$PWD" copies, whole substituted names)

Five confirmed regressions from a hunt on the sixth review's fixes, and
one older crash found with them, each with a test that was red before
the change (section 21g of mission-doors.test.py, 49 checks; 39 new
assertions red on 4d0b1de):

- a NUL mark (the call's cwd, the checkout's top) inside a word is no
  path: `bash -c "make PREFIX=$PWD/out"` and, since M9.30,
  `make PREFIX=$(git rev-parse --show-toplevel)/out` crashed the gate
  (ValueError: embedded null), which failed closed and blocked them
- chdir() names the call's cwd as the line's directories do ("" and
  relative), so branches() finds a `git checkout main` before a nested
  `cd $PWD && git push`
- where the line may be in more than one directory, a nested $PWD is
  read in each of them and a nested $(pwd) is unknown (two marks)
- a copy into "$PWD" or "$CLAUDE_PROJECT_DIR" is into the directory it
  is, and a regular file source never becomes "$VAR/.claude"
- only a wholly substituted last name ($F, ${F:?}, $(...)) is any name:
  `settings.json.bak.$(date +%s)` and `mission-cron.log.$(date +%F)` are
  backups and rotations, not the guarded records

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rsDo9GYzNx8tXucywZHnA
…e M9.31 branch

ROADMAP: the M9 row keeps this branch's done/open lists; the M10 row takes
master's M10.3 progress and says the OpenCode plugin exports
ALOOP_AGENT_SHELL since M9.31.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rsDo9GYzNx8tXucywZHnA

This branch has not been deployed

No deployments
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.

2 participants

0