[Sprint 6] Phase-2 docs + release notes + final soak#199
Conversation
CLAUDE.md: drop ghost references to removed hook-template files (`src/hook_substitute.rs`, `src/hook_templates_defaults.rs`, `src/hook_client.rs`, `hook-templates/defaults.toml`) and the removed `--template` / `--param` flag, `[[hook_template]]` block, and the `validate_hook_templates` / `derive_template_hook_name` helpers. Rewrite the `hooks.rs` and `hook_handler.rs` entries to describe the post-template-removal raw-cmd-only model with owner-gated UDS preferred + root-only direct-write fallback, point at the actual home of `submit_hook_create_via_daemon` / `submit_hook_delete_via_daemon` (in `src/mcp.rs`), add an entry for `hook_list_handler.rs`, and update the MCP server description so it reflects the post-Phase-2 reality (every tool routes through the daemon UDS; `AimxMcpServer::load_config` exists only behind `#[cfg(test)]`). book/security.md: replace the obsolete two-tier "template hooks vs raw-cmd hooks" section with the actual ownership-as-authorization model. Update the UDS verb description for `HOOK-CREATE` to match what the daemon now accepts, add `HOOK-LIST` to the verb summary, and rewrite the MCP-trust subsection so it acknowledges that MCP is owner-gated, not unrestricted. Drop the duplicate "Origin protection" subheading the section restructure left behind. book/setup.md: drop a stale "template-bound hooks" reference. book/cli.md: add a UDS protocol verb table covering every verb the daemon accepts (including the new `HOOK-LIST`) so operators have one source of truth for the wire surface. agents/common/references/mcp-tools.md: note that `hook_list` is backed by the new `HOOK-LIST` UDS verb so the MCP process never reads `/etc/aimx/config.toml` directly. book/release-notes.md: add a Phase-2 release-notes entry covering what was broken (9 of 12 MCP tools failed with EACCES on production-perm installs), why it shipped that way (production-perm scenario was untested), what's now guarded (every email + hook tool routes through the daemon UDS, `AimxMcpServer::load_config` is gone from non-test code, end-to-end CI smoke covers all twelve tools), and user-visible behavior changes (`hook_delete` is now a thin pass-through to the daemon's `HOOK-DELETE`; no tool caches the `MAILBOX-LIST` response across calls). Mention the new `HOOK-LIST` UDS verb and the test-only env-var rename. src/mcp.rs: reword two doc comments that mentioned the literal `self.load_config()` text so the structural cross-check grep returns zero matches outside `#[cfg(test)]` blocks.
uzyn
left a comment
There was a problem hiding this comment.
Sprint 6 Review — Phase-2 docs + release notes + final soak
Sprint Goal Assessment
The sprint goal — make CLAUDE.md describe the actual code (no ghost references to removed hook-template files), get book/ and agents/common/ audited and accurate, and ship a clear Phase-2 release-notes entry — is substantially met. Both targeted greps (hook_substitute|hook_templates_defaults|hook_client\.rs|hook-templates/defaults\.toml|--template|--param|hook_template|validate_hook_templates|derive_template_hook_name against CLAUDE.md, and hook_template|--template|--param|\[\[hook_template\]\] against book/ agents/) return zero matches locally. The new HOOK-LIST UDS verb is documented in both book/cli.md's new verb table and agents/common/references/mcp-tools.md. The Phase-2 release-notes entry hits each PRD R42 point in plain English.
The shortfall: the implementer rewrote three sections of book/security.md (verb summary, the "Hooks: ownership = authorization" section, the MCP-trust subsection) but left other sections of the same file describing the old template/run_as model. The same page now contains the literal claim "There is no per-hook run_as knob" alongside an operator-hardening tip telling operators to "Set run_as explicitly on raw-cmd hooks you hand-edit." See Non-blockers below.
Acceptance Criteria Checklist
S6-1 — Purge hook-template ghost references from CLAUDE.md
- Removed references to
src/hook_substitute.rs,src/hook_templates_defaults.rs,src/hook_client.rs,hook-templates/defaults.toml. - Removed
--template/--param/[[hook_template]]/validate_hook_templates/derive_template_hook_namereferences. -
hooks.rsline rewritten to reflect owner-gated UDS-preferred + root-only direct-write fallback model. - CLAUDE.md points at
src/mcp.rsas the home ofsubmit_hook_create_via_daemon/submit_hook_delete_via_daemon. - Verification grep returns zero matches.
S6-2 — Audit book/ + agents/common/; surface HOOK-LIST verb
-
book/hooks.md,book/cli.md,book/mcp.md,book/agent-integration.mdaudited — verification grep clean. -
agents/common/aimx-primer.mdandagents/common/references/audited — only allowed "no template indirection" disclaimers remain. -
book/cli.mdincludes the new "UDS protocol verbs" table withHOOK-LISTrow alongsideMAILBOX-LISTrow. -
agents/common/references/mcp-tools.md::hook_listnotes theHOOK-LISTUDS-verb backing. - Verification grep returns zero matches.
S6-3 — Phase-2 release-notes entry
- New entry titled "MCP email and hook tools now work for non-root operators" at the top of the unreleased section.
- Body covers the four PRD R42 points (what was broken, why, what's now guarded, user-visible behavior changes).
- Mentions the new
HOOK-LISTUDS verb with a sentence on its frame shape and a cross-link tocli.mdandmcp.md. - Notes the
AIMX_TEST_SKIP_AUTHZ_CHECKrename (one-liner under "Tooling note").
S6-4 — Final pass + soak
- PR description asserts root-crate
cargo testis green (1035 + 106 + 1 default-lane suites; 8 / 21 ignored gated by sudo / two-uid host). - PR description asserts
services/verifieris 43/43 green. - PR description asserts
cargo clippy --all-targets -- -D warningsclean on both crates. - PR description asserts
cargo fmt -- --checkclean on both crates (independently verified locally on the root crate). - PR description asserts
aimx doctorruns cleanly post-change. - Live MCP smoke transcript on the integration host (12 MCP tools against
0600 root:rootconfig + runningaimx serve) — explicitly out of scope for the dev environment; flagged in the PR description for the maintainer to capture before / after merge. Reasonable concession: the integration-host setup truly cannot run inside the dev container. Maintainer must capture it before declaring Phase 2 shipped. -
grep -nE 'self\.load_config\(\)' src/mcp.rsoutside#[cfg(test)]— zero matches (verified). -
grep -rn 'AIMX_TEST_SKIP_ROOT_CHECK' .outsidetarget/and.git/— zero matches (verified).
CI status at review time: core-tests and CodeQL Analyze (rust) still IN_PROGRESS; everything else (build, verifier-tests, mailbox-dir-perms-isolation, docs-build, the other CodeQL analyses) is green. Maintainer should confirm a clean run before merging.
Test Coverage
This PR is doc-only. No new code paths to cover. The implementer added no test changes — that is correct.
Potential Bugs
None. This is a documentation PR plus two src/mcp.rs doc-comment rewordings (replacing the literal self.load_config() text with "direct config-load path" so the structural cross-check grep stays clean). The two reworded comments are at src/mcp.rs:272-275 and src/mcp.rs:632-635; the prose still accurately describes what each method does.
Code Quality
The two src/mcp.rs doc comment edits read fine. No drift from intent.
Alignment with PRD
Aligned. R40 (CLAUDE.md points at the actual src/mcp.rs home of the helpers, not the nonexistent src/hook_client.rs), R41 (broader book/ + agents/common/ audit), and R42 (Phase-2 release-notes entry) are all addressed. PRD §P2.10 (the cosmetic helper-relocation refactor) is explicitly deferred and the deferred-list entry remains in Sprint Plan §P2 Deferred to v2.
Issues Surfaced
Non-blocker 1 — book/security.md internal contradiction on per-hook run_as
The PR rewrote three sections of book/security.md to match the new owner-gated model:
- The "AIMX/1 verbs" summary (line 215+) now lists
HOOK-CREATEas a raw-argv verb. - The new "Hooks: ownership = authorization" section (line 233+) explicitly states "There is no per-hook
run_asknob: a hook onalice's mailbox runs asalice, full stop." - The "MCP server" trust-model bullets (line 285+) say MCP cannot override
run_as.
But the surrounding sections of the same page are still describing the old template/run_as model:
- Line 30 — "…cannot sign mail as a wildcard sender, forge DKIM, or escape the hook template sandbox." The hook template sandbox no longer exists.
- Line 48 — Process boundary table row says "Hook subprocess | mailbox owner (
run_as)". The parenthetical implies a per-hookrun_asknob that line 241 explicitly denies. - Line 117 — Filesystem table description: "Mailboxes, trust policy, hooks, templates".
templatesno longer applies. - Line 132 — "…the template definitions" listed as part of
/etc/aimx/. - Line 213 — "the daemon's own checks on
From:+ template shapes decide what actually happens" — there are no template shapes anymore. - Line 317 — "Keep the template list minimal. Every installed template is an argv shape you have authorised for agents to invoke."
- Line 318 — "Review hook-fire logs after a new template lands".
- Line 320 — "Set
run_asexplicitly on raw-cmd hooks you hand-edit, even to the mailbox's owner." This directly contradicts the new line 241 statement that there is no per-hookrun_asknob.
S6-2's first bullet enumerates book/hooks.md, book/cli.md, book/mcp.md, book/agent-integration.md — book/security.md is not in the explicit AC list, and the AC's regex grep does not match these residuals. So this is not a strict AC violation. However: an operator reading the security page now sees mutually contradictory advice on whether per-hook run_as exists. That's a real reader-confusion bug worth fixing in this sprint since the page was already touched. Fix is mechanical: rewrite or strike the eight callouts above.
Classification: Non-blocker. Doc consistency, not behavior; the merged code is correct and the new sections describe it correctly.
Non-blocker 2 — CLAUDE.md socket-authz line is stale
Line 124 of CLAUDE.md still reads "aimx.sock sits here as a world-writable UDS (0o666); authorization on the socket is out of scope and the DKIM secret isolation (root-only 0600 key) is the security boundary instead." After Sprint 1 (and confirmed by Sprint 4), the daemon does run authorization on the socket via SO_PEERCRED + auth::authorize. The DKIM key is no longer the only boundary — it's the boundary for forging mail as a wildcard sender, but per-mailbox actions are owner-gated.
This is in unchanged CLAUDE.md territory (S6-1's scope was hook-template ghost references), so it is not an AC violation here. Worth flagging because the PR is the natural place to catch it (CLAUDE.md was open in this PR's diff already) and the Phase-2 release notes explicitly highlight that owner-gating is the new boundary.
Classification: Non-blocker. Doc drift, no behavior impact.
Pre-existing Triage
The implementer's PR notes flagged a pre-existing Sprint 1 + Sprint 2 banned-token mention at book/release-notes.md:64, in the unchanged "Mailbox create/delete no longer requires root" entry. Confirmed pre-existing and not in this PR's scope. The phrasing should be rewritten per CLAUDE.local.md's rule (drop the cross-reference, keep the technical content) — e.g. "the daemon binary that includes Phase-1 of the user-mailbox track" — but this is a separate cleanup, not a Sprint 6 blocker.
There are also several Sprint/S\d-\d/User Story matches in src/, tests/, and other .md files that pre-date this PR. Per the brief and per CLAUDE.local.md's "do NOT do a sweeping pre-existing-hits cleanup" guidance, these are out of scope for Sprint 6 and should be triaged into a separate cleanup ticket.
Summary and Recommended Actions
Overall verdict: Needs minor fixes (doc consistency in book/security.md).
Blockers: None.
Non-blockers:
book/security.mdlines 30 / 48 / 117 / 132 / 213 / 317 / 318 / 320 — strike or rewrite the residual template /run_ascallouts so the page is internally consistent with its own new "Hooks: ownership = authorization" section. Highest priority: line 320's "Setrun_asexplicitly" tip directly contradicts line 241's "There is no per-hookrun_asknob."CLAUDE.mdline 124 — drop or rewrite the "authorization on the socket is out of scope" clause; that statement is outdated after Sprints 1 + 4.
Nice-to-have / triage:
book/release-notes.md:64— rewrite the pre-existingSprint 1 + Sprint 2mention perCLAUDE.local.md's no-cross-reference rule. Separate cleanup ticket.- The Sprint 6 exit gate's "Live MCP smoke transcript on the integration host" AC remains unchecked. The implementer's "out of scope for the dev environment" note is reasonable, but this AC is explicitly part of the Phase-2 ship gate. Capture the transcript on the integration host (non-root caller, all 12 MCP tools,
0600 root:rootconfig + runningaimx serve) before merge — or, if capturing post-merge, surface a follow-up issue so the AC isn't silently lost. - Several pre-existing
Sprint/S\d-\d/User Storymatches insrc/,tests/, and book files predate this PR. Triage into a separateCLAUDE.local.md-banned-token cleanup ticket; not Sprint 6's job.
The CI runs that were still in progress at review time (core-tests, CodeQL Analyze (rust)) need to be confirmed green by the maintainer before merge.
Once the two Non-blockers above are resolved (or explicitly waived for a follow-up PR), this is ready to merge.
book/security.md: rewrite the residual template / run_as / 'world-readable mail' / 'socket auth out of scope' callouts so the page is internally consistent with its 'Hooks: ownership = authorization' and 'Per-action authorization' sections. Reframe the one-sentence summary as two boundaries (DKIM key externally, server-side per-verb auth::authorize + SO_PEERCRED internally), strike 'escape the hook template sandbox', drop the 'Storage root world-readable by design' phrasing now that mailboxes are per-owner 0700, and replace the 'Set run_as explicitly' hardening tip (which directly contradicted the new no-run_as section) with 'Pick the mailbox owner deliberately'. CLAUDE.md: rewrite the runtime-dir line that called UDS authorization 'out of scope' and named the DKIM key as the only boundary. Both auth boundaries now coexist: the daemon binds the socket world-writable but runs auth::authorize on every verb keyed on SO_PEERCRED; DKIM remains 0600 root:root and is consumed only by the root-running daemon.
Cycle 2 — both Cycle-1 non-blockers addressedPushed Non-blocker 1 —
|
uzyn
left a comment
There was a problem hiding this comment.
Cycle 3 Re-review — both cycle-2 non-blockers resolved; one residual landing on a third file
Walked the cycle-2 diff (5fcbf53..0df2834) line by line and re-grepped both files plus the rest of book/. Verdict: both flagged issues are genuinely fixed, but a fresh-reader audit of the broader S6-2 surface turned up the same contradiction now landing on book/getting-started.md. Posting as a comment, not a change request, since the cycle-1 issues themselves are cleanly resolved and CI is still settling.
Resolved
Non-blocker 1 — book/security.md internal contradiction. Verified clean.
$ rg -n 'run_as' book/security.md
220: …no per-hook `run_as` knob, full stop.
235: …no template-hook layer and no per-hook `run_as` override…
241: There is no per-hook `run_as` knob…
292: …no `run_as` knob anywhere…
320: Hooks always run as `mailbox.owner_uid` — there is no per-hook `run_as` override.
All five surviving mentions are explicit denials, mutually consistent with each other and with the new line-5 two-boundaries framing. The template mention at line 235 is also an explicit denial. The eight callouts the cycle-1 review enumerated (lines 30 / 48 / 117 / 132 / 213 / 317 / 318 / 320) are all rewritten correctly, and the implementer caught five additional residuals on a fresh-reader pass (lines 5 / 7 / 13 / 16 / 37 / 68 / 124 / 203 / 210 / 220 / 292 / 305) that the cycle-1 review had missed. The broken anchor at line 68 (#raw-cmd-hooks-operator-only → #mailbox-ownership--hook-authorization) was fixed and cross-checked: the target heading exists at book/hooks.md:12 (## Mailbox ownership = hook authorization).
The line-48 process-boundary table now reads (setuid to mailbox.owner_uid) instead of (run_as), matching the daemon's actual mechanism.
Non-blocker 2 — CLAUDE.md:124 stale claim. Verified clean. The new line names both boundaries (auth::authorize keyed on SO_PEERCRED for the internal surface; 0600 root:root DKIM key for the external one) in one sentence, in the same line slot. It no longer reads as "DKIM is the only boundary."
New issue surfaced during the re-review
Non-blocker 3 — book/getting-started.md still carries the pre-cycle-2 stale framing
The cycle-2 work rewrote book/security.md's one-sentence summary (line 5) and CLAUDE.md's runtime-dir line (line 124) so both frame the design as two boundaries — external DKIM key + internal server-side auth::authorize. But book/getting-started.md lines 57–59 still read:
AIMX is a single-operator mail server designed for AI agents on a domain you own. It stores mail under
/var/lib/aimx/(world-readable). Any local user or agent can read email files. This is by design: AIMX assumes a single-admin server where all agents are trusted to read each other's mail.All mutations […] go through the
aimxMCP server or CLI. Never write to the data directory directly. The UDS send socket at/run/aimx/aimx.sockis world-writable. Any local user can submit outbound mail throughaimx send. The authorisation boundary is the root-only DKIM private key, not filesystem ACLs on the mailbox tree.
Two specific problems:
- "It stores mail under
/var/lib/aimx/(world-readable). Any local user or agent can read email files." — false.book/security.md's filesystem table at lines 117–124 (andCLAUDE.md's setup description) document that per-mailbox dirs are0700chowned to the owner, andbook/getting-started.mditself, eight lines later, contradicts this implicitly. A fresh reader hitting the getting-started page first will form a wrong mental model. - "The authorisation boundary is the root-only DKIM private key, not filesystem ACLs" — same single-boundary framing the cycle-2 work just struck from
security.mdline 5 andCLAUDE.mdline 124. After cycle 2, this is now the only place inbook/that still asserts the old framing.
This isn't a regression introduced in cycle 2 — it pre-existed cycle 1 — but the cycle-1 review's S6-2 AC (book/ audit clean) and cycle-2's security.md reconciliation make it a fresh inconsistency on the page that contradicts the rewritten security.md directly. S6-2 explicitly listed book/hooks.md, book/cli.md, book/mcp.md, book/agent-integration.md and checked them with the template / --template / --param / [[hook_template]] regex — book/getting-started.md and the broader world-readable / single-boundary framing weren't in that regex. So this is technically not an AC violation, but the same reasoning that justified the cycle-2 reconciliation pass on security.md applies cleanly here.
Mechanical fix sketch: rewrite the two paragraphs to describe per-owner mailbox isolation (0700 per mailbox; non-owners cannot read another mailbox's contents) and the two-boundaries authorization model, then cross-link to security.md for the full discussion (the existing cross-link at line 63 stays).
Classification: Non-blocker. Doc consistency, not behavior. Same severity as cycle-1's NB-1 — and arguably should have been caught alongside it.
CI status at re-review time
build ✓ · verifier-tests ✓ · docs-build ✓ · Analyze (actions) ✓ · Analyze (javascript-typescript) ✓ · core-tests pending · mailbox-dir-perms-isolation pending · Analyze (rust) pending · CodeQL skipping · deploy skipping. Maintainer to confirm the three pending jobs land green before merging.
Summary
Overall verdict: Comments only — both cycle-2 non-blockers are cleanly resolved. The new finding in book/getting-started.md is the same severity class as cycle-1's NB-1 and shouldn't gate the merge: it's pre-existing copy that pre-dates the Phase-2 rewrite, the security.md cross-link from getting-started.md still funnels readers to the now-correct page, and the technical content of Sprint 6 is delivered. Maintainer's call whether to fix in this PR (mechanical, ~3 lines), defer to a tiny follow-up, or merge as-is.
Blockers: None.
Non-blockers (carry forward):
book/getting-started.md:57–59— rewrite the "world-readable" / "DKIM is the boundary" paragraphs to match the new two-boundaries + per-owner-isolation framing insecurity.md. Optional for this PR; safe to defer to a small follow-up.
Nice-to-have / triage (unchanged from cycle 1):
book/release-notes.md:64pre-existing banned-token mention.- Live MCP smoke transcript on the integration host (out of scope for the dev environment; capture before declaring Phase 2 shipped).
- Pre-existing banned-token hits in
src/,tests/, otherbook/files — separate cleanup ticket.
Once any of (a) the residual book/getting-started.md paragraphs are fixed in this PR, (b) a follow-up issue is filed for it, or (c) the maintainer waives it, this is ready to merge along with the three pending CI jobs landing green.
Sprint 6: Phase-2 docs + release notes + final soak
CLAUDE.md: drop ghost references to removed hook-template files
(`src/hook_substitute.rs`, `src/hook_templates_defaults.rs`,
`src/hook_client.rs`, `hook-templates/defaults.toml`) and the removed
`--template` / `--param` flag, `[[hook_template]]` block, and the
`validate_hook_templates` / `derive_template_hook_name` helpers. Rewrite
the `hooks.rs` and `hook_handler.rs` entries to describe the
post-template-removal raw-cmd-only model with owner-gated UDS preferred
+ root-only direct-write fallback. Add an entry for `hook_list_handler.rs`
and update the MCP server description so it reflects the post-Phase-2
reality. Reframe the runtime-dir line so it names both auth boundaries
(server-side `auth::authorize` keyed on `SO_PEERCRED` + DKIM key isolation),
not just DKIM.
book/security.md: end-to-end reconciliation pass — replace the obsolete
two-tier "template hooks vs raw-cmd hooks" model with the actual
ownership-as-authorization model. Reframe the one-sentence summary as
two boundaries, drop residual template / `run_as` callouts in the
threat-model, filesystem table, UDS rationale, hardening tips, and
out-of-scope sections. Add `HOOK-LIST` to the verb summary. Repair the
`#raw-cmd-hooks-operator-only` anchor.
book/cli.md: add a UDS protocol verb table covering every verb the
daemon accepts (including the new `HOOK-LIST`).
book/setup.md: drop a stale "template-bound hooks" reference.
agents/common/references/mcp-tools.md: note `hook_list` is backed by
the `HOOK-LIST` UDS verb so the MCP process never reads
`/etc/aimx/config.toml` directly.
book/release-notes.md: add a Phase-2 release-notes entry covering what
was broken (9 of 12 MCP tools failed with EACCES on production-perm
installs), why it shipped that way, what's now guarded (every email +
hook tool routes through the daemon UDS, end-to-end CI smoke covers
all twelve tools), and user-visible behavior changes (`hook_delete`
is now a thin pass-through to the daemon's `HOOK-DELETE`; no tool
caches the `MAILBOX-LIST` response across calls). Mention the new
`HOOK-LIST` UDS verb and the `AIMX_TEST_SKIP_AUTHZ_CHECK` rename.
src/mcp.rs: reword two doc comments that mentioned the literal
`self.load_config()` text so the structural cross-check grep returns
zero matches outside `#[cfg(test)]` blocks.
The Security model section had two stale claims left over from before the
user-mailbox track shipped per-owner mailbox isolation and server-side
per-verb authorization:
1. "It stores mail under /var/lib/aimx/ (world-readable). Any local
user or agent can read email files." — false post-Phase-1: each
mailbox's inbox/<name>/ and sent/<name>/ are chowned to the
configured Linux owner at mode 0700, so non-owners (and non-root)
cannot read another mailbox.
2. "The authorisation boundary is the root-only DKIM private key, not
filesystem ACLs on the mailbox tree." — stale single-boundary
framing post-Phase-2: AIMX now has two boundaries — the DKIM key on
the SMTP-out external surface, and per-verb auth::authorize keyed
on SO_PEERCRED on the UDS internal surface.
Rewrites both paragraphs to match the canonical wording shipped to
book/security.md and CLAUDE.md in 0df2834. Also reframes the
"per-user mailbox isolation" callout: per-owner 0700 mailboxes do
prevent non-owners from reading another mailbox, so the page now says
the trust boundary is the host (not each user on the host) — the same
wording security.md uses for the matching FAQ pointer.
Docs-only change. No code touched.
|
Addressed the stale Security model wording in What was fixed (lines 57-61):
Verification:
No new concerns. |
uzyn
left a comment
There was a problem hiding this comment.
Cycle 4 Re-review — cycle-3 non-blocker resolved; Sprint 6 ready to merge
Walked the cycle-3 diff (0df2834..02c76ba) line by line against the canonical wording in book/security.md:5,7,37 and CLAUDE.md:124. The fix is clean and consistent across all three files. CI status (the three jobs that were pending at cycle 3) needs maintainer confirmation before merge.
Resolved
Cycle-3 Non-blocker — book/getting-started.md:57-61 stale framing. Verified clean.
The diff rewrites three contiguous paragraphs of the "Security model" section. Cross-checked against the canonical wording shipped in cycle 2:
- Line 57 — "world-readable" framing replaced with the explicit per-owner
0700chown story. Matchesbook/security.md:37("Each mailbox is0700and chowned to its configured Linux owner") andCLAUDE.md:122(storage description) verbatim in technical content. - Line 59 — single-boundary "DKIM private key, not filesystem ACLs" framing replaced with the two-boundary statement (root-only DKIM key on the SMTP-out external surface + per-verb
auth::authorizekeyed onSO_PEERCREDon the UDS internal surface). Matches the canonical one-sentence summary atbook/security.md:5andCLAUDE.md:124. - Line 61 — "per-user mailbox isolation" → Postfix/Stalwart pointer reworded to "two users on one host genuinely cannot trust each other to operate the daemon" with the trust-boundary-is-the-host clarification. Matches
book/security.md:37's exact framing word-for-word.
Spot-checks on residual single-boundary / world-readable claims across book/ and CLAUDE.md:
$ rg -n 'world-readable|filesystem ACLs|all agents are trusted to read' book/ CLAUDE.md
(no matches in book/ or CLAUDE.md)
$ rg -n 'authorisation boundary is the|authorization boundary is the' book/ CLAUDE.md
(only the corrected "external boundary … internal boundary" framings)
Both queries are clean across book/ and CLAUDE.md. The previously-flagged contradiction is fully reconciled.
New issue surfaced during the re-review
agents/common/references/workflows.md:275 carries the same stale framing. The "Direct filesystem reads" subsection ends with the literal sentence "The filesystem is world-readable and the format is stable. Use direct reads for scanning, grep-based filtering, or when processing hundreds of messages." Post-Phase-1, the filesystem is not world-readable: each mailbox dir is 0700 chowned to its owner. An agent that follows this advice and tries to scan a mailbox it does not own will get EACCES.
Severity assessment per the cycle brief. Sprint 6 has been through three cycles, the technical deliverables are settled, and the cycle-3 brief asks specifically whether residuals in book/ should block — this hit is in agents/, not book/, and agents/ was not in the explicit S6-2 audit AC list (which enumerated agents/common/aimx-primer.md and agents/common/references/ against the template / --template / --param / [[hook_template]] regex; the world-readable / single-boundary regex was not run against agents/). The surrounding example block (cat /var/lib/aimx/inbox/agent/...) still works correctly for the mailbox the agent owns — the misleading sentence is the closing summary, not the procedure itself. Not severe enough to block this PR. Recommend filing a small follow-up cleanup ticket: rewrite the line as "The filesystem is readable to the mailbox owner and the format is stable — direct reads work for scanning the agent's own mailbox; cross-mailbox reads are blocked by the per-owner 0700 mode."
CI status at re-review time
build ✓ · verifier-tests ✓ · docs-build ✓ · Analyze (actions) ✓ · Analyze (javascript-typescript) ✓ · core-tests pending · mailbox-dir-perms-isolation pending · Analyze (rust) pending · CodeQL skipping · deploy skipping. The three pending jobs are the same set that were pending at cycle 3 — they tend to run long but consistently land green on doc-only changes. Maintainer to confirm the three pending jobs land green before merging.
Pre-existing triage (unchanged from cycle 1)
book/release-notes.md:64pre-existing banned-token mention.- Live MCP smoke transcript on the integration host (out of scope for the dev environment; capture before declaring Phase 2 shipped).
- Pre-existing banned-token hits in
src/,tests/, otherbook/files — separate cleanup ticket. - New:
agents/common/references/workflows.md:275"filesystem is world-readable" line — small follow-up cleanup ticket.
Summary
Overall verdict: Ready to merge.
Blockers: None.
Non-blockers: None for this PR. The new finding in agents/common/references/workflows.md:275 is the same severity class as the cycle-3 finding but lands on a separate file and outside the S6-2 audit scope. Per the cycle-3 brief's guidance to "land it without ratcheting unless there's a real new bug", deferring to a small follow-up.
Nice-to-have / triage:
agents/common/references/workflows.md:275— rewrite the "filesystem is world-readable" closing sentence to match the per-owner0700reality. Mechanical, ~1 line. Small follow-up.book/release-notes.md:64pre-existing banned-token mention — separate cleanup ticket.- Live MCP smoke transcript on the integration host — capture before declaring Phase 2 shipped.
- Pre-existing banned-token hits in
src/,tests/, otherbook/files — separate cleanup ticket.
Once the three pending CI jobs land green, this is good to merge.
Sprint 6: Phase-2 docs + release notes + final soak
CLAUDE.md: drop ghost references to removed hook-template files
(`src/hook_substitute.rs`, `src/hook_templates_defaults.rs`,
`src/hook_client.rs`, `hook-templates/defaults.toml`) and the removed
`--template` / `--param` flag, `[[hook_template]]` block, and the
`validate_hook_templates` / `derive_template_hook_name` helpers. Rewrite
the `hooks.rs` and `hook_handler.rs` entries to describe the
post-template-removal raw-cmd-only model with owner-gated UDS preferred
+ root-only direct-write fallback. Add an entry for `hook_list_handler.rs`
and update the MCP server description so it reflects the post-Phase-2
reality. Reframe the runtime-dir line so it names both auth boundaries
(server-side `auth::authorize` keyed on `SO_PEERCRED` + DKIM key isolation),
not just DKIM.
book/security.md: end-to-end reconciliation pass — replace the obsolete
two-tier "template hooks vs raw-cmd hooks" model with the actual
ownership-as-authorization model. Reframe the one-sentence summary as
two boundaries; drop residual template / `run_as` callouts in the
threat-model, filesystem table, UDS rationale, hardening tips, and
out-of-scope sections. Add `HOOK-LIST` to the verb summary. Repair the
`#raw-cmd-hooks-operator-only` anchor.
book/getting-started.md: reconcile the "Security model" section with
the canonical two-boundaries + per-owner `0700` framing shipped to
security.md and CLAUDE.md.
book/cli.md: add a UDS protocol verb table covering every verb the
daemon accepts (including the new `HOOK-LIST`).
book/setup.md: drop a stale "template-bound hooks" reference.
agents/common/references/mcp-tools.md: note `hook_list` is backed by
the `HOOK-LIST` UDS verb so the MCP process never reads
`/etc/aimx/config.toml` directly.
book/release-notes.md: add a Phase-2 release-notes entry covering what
was broken (9 of 12 MCP tools failed with EACCES on production-perm
installs), why it shipped that way, what's now guarded (every email +
hook tool routes through the daemon UDS, end-to-end CI smoke covers
all twelve tools), and user-visible behavior changes. Mention the new
`HOOK-LIST` UDS verb and the `AIMX_TEST_SKIP_AUTHZ_CHECK` rename.
src/mcp.rs: reword two doc comments that mentioned the literal
`self.load_config()` text so the structural cross-check grep returns
zero matches outside `#[cfg(test)]` blocks.
Summary
src/hook_substitute.rs,src/hook_templates_defaults.rs,src/hook_client.rs,hook-templates/defaults.toml) and the removed--template/--paramflag,[[hook_template]]block, and helpers (validate_hook_templates/derive_template_hook_name). Rewrite thehooks.rsline to reflect the post-template-removal raw-cmd-only model with owner-gated UDS preferred + root-only direct-write fallback. Point at the actual home ofsubmit_hook_create_via_daemon/submit_hook_delete_via_daemon(src/mcp.rs). Add an entry forhook_list_handler.rs. Update the MCP server section so it reflects post-Phase-2 reality (every tool routes through the daemon UDS;AimxMcpServer::load_configexists only behind#[cfg(test)]).book/security.mdwith the actual ownership-as-authorization model. Update the UDS verb summary forHOOK-CREATEto match what the daemon now accepts. AddHOOK-LISTto the verb summary inbook/security.mdand add a full UDS protocol verb table tobook/cli.mdso operators have one source of truth for the wire surface (includingHOOK-LISTalongsideMAILBOX-LIST). Rewrite the MCP-trust subsection so it acknowledges MCP is owner-gated, not unrestricted. Drop a stale "template-bound hooks" reference inbook/setup.md. Note inagents/common/references/mcp-tools.mdthathook_listis backed by theHOOK-LISTUDS verb.AimxMcpServer::load_configis gone from non-test code, end-to-end CI smoke covers all twelve tools), and user-visible behavior changes (hook_deleteis now a thin pass-through to the daemon'sHOOK-DELETE; no tool caches theMAILBOX-LISTresponse across calls). Mentions the newHOOK-LISTUDS verb and theAIMX_TEST_SKIP_AUTHZ_CHECKrename.self.load_config()text so the structural cross-check grep returns zero matches outside#[cfg(test)]blocks. No behavior change.Test plan
cargo fmt -- --checkclean on root cratecargo clippy --all-targets -- -D warningsclean on root cratecargo testgreen on root crate (1035 + 106 + 1 default-lane suites; 8 / 21 ignored gated by sudo or two-uid host)cargo fmt -- --checkclean onservices/verifier/cargo clippy --all-targets -- -D warningsclean onservices/verifier/cargo testgreen onservices/verifier/(43 / 43)aimx doctorruns cleanly post-change against a stub configgrep -nE 'hook_substitute|hook_templates_defaults|hook_client\.rs|hook-templates/defaults\.toml|--template|--param|hook_template|validate_hook_templates|derive_template_hook_name' CLAUDE.mdreturns zero matchesbook/+agents/template grep clean:grep -rnE 'hook_template|--template|--param|\[\[hook_template\]\]' book/ agents/returns zero matchesgrep -nE 'self\.load_config\(\)' src/mcp.rsreturns zero matches outside#[cfg(test)]blocksgrep -rn 'AIMX_TEST_SKIP_ROOT_CHECK' .returns zero matches outsidetarget/and.git/0600 root:rootconfig + runningaimx serve, exercising every one of the 12 MCP tools). Flag for the maintainer to capture on the integration host before/after merge.