Skip to content

[Sprint 6] Phase-2 docs + release notes + final soak#199

Merged
uzyn merged 3 commits into
mainfrom
user-mailbox-sprint-6
May 3, 2026
Merged

[Sprint 6] Phase-2 docs + release notes + final soak#199
uzyn merged 3 commits into
mainfrom
user-mailbox-sprint-6

Conversation

@uzyn

@uzyn uzyn commented May 3, 2026

Copy link
Copy Markdown
Owner

Summary

  • 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 helpers (validate_hook_templates / derive_template_hook_name). Rewrite the hooks.rs line 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 of submit_hook_create_via_daemon / submit_hook_delete_via_daemon (src/mcp.rs). Add an entry for hook_list_handler.rs. Update the MCP server section so it reflects post-Phase-2 reality (every tool routes through the daemon UDS; AimxMcpServer::load_config exists only behind #[cfg(test)]).
  • book/ + agents/common/ — replace the obsolete two-tier "template hooks vs raw-cmd hooks" section in book/security.md with the actual ownership-as-authorization model. Update the UDS verb summary for HOOK-CREATE to match what the daemon now accepts. Add HOOK-LIST to the verb summary in book/security.md and add a full UDS protocol verb table to book/cli.md so operators have one source of truth for the wire surface (including HOOK-LIST alongside MAILBOX-LIST). Rewrite the MCP-trust subsection so it acknowledges MCP is owner-gated, not unrestricted. Drop a stale "template-bound hooks" reference in book/setup.md. Note in agents/common/references/mcp-tools.md that hook_list is backed by the HOOK-LIST UDS verb.
  • book/release-notes.md — Phase-2 release-notes entry titled "MCP email and hook tools now work for non-root operators". Covers 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 is 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). Mentions 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. No behavior change.

Test plan

  • cargo fmt -- --check clean on root crate
  • cargo clippy --all-targets -- -D warnings clean on root crate
  • cargo test green on root crate (1035 + 106 + 1 default-lane suites; 8 / 21 ignored gated by sudo or two-uid host)
  • cargo fmt -- --check clean on services/verifier/
  • cargo clippy --all-targets -- -D warnings clean on services/verifier/
  • cargo test green on services/verifier/ (43 / 43)
  • aimx doctor runs cleanly post-change against a stub config
  • CLAUDE.md ghost-reference grep clean: grep -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.md returns zero matches
  • book/ + agents/ template grep clean: grep -rnE 'hook_template|--template|--param|\[\[hook_template\]\]' book/ agents/ returns zero matches
  • grep -nE 'self\.load_config\(\)' src/mcp.rs returns zero matches outside #[cfg(test)] blocks
  • grep -rn 'AIMX_TEST_SKIP_ROOT_CHECK' . returns zero matches outside target/ and .git/
  • Out of scope for this dev environment: the live MCP smoke transcript on the integration host (non-root MCP client against a 0600 root:root config + running aimx serve, exercising every one of the 12 MCP tools). Flag for the maintainer to capture on the integration host before/after merge.

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 uzyn left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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_name references.
  • hooks.rs line rewritten to reflect owner-gated UDS-preferred + root-only direct-write fallback model.
  • CLAUDE.md points at src/mcp.rs as the home of submit_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.md audited — verification grep clean.
  • agents/common/aimx-primer.md and agents/common/references/ audited — only allowed "no template indirection" disclaimers remain.
  • book/cli.md includes the new "UDS protocol verbs" table with HOOK-LIST row alongside MAILBOX-LIST row.
  • agents/common/references/mcp-tools.md::hook_list notes the HOOK-LIST UDS-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-LIST UDS verb with a sentence on its frame shape and a cross-link to cli.md and mcp.md.
  • Notes the AIMX_TEST_SKIP_AUTHZ_CHECK rename (one-liner under "Tooling note").

S6-4 — Final pass + soak

  • PR description asserts root-crate cargo test is green (1035 + 106 + 1 default-lane suites; 8 / 21 ignored gated by sudo / two-uid host).
  • PR description asserts services/verifier is 43/43 green.
  • PR description asserts cargo clippy --all-targets -- -D warnings clean on both crates.
  • PR description asserts cargo fmt -- --check clean on both crates (independently verified locally on the root crate).
  • PR description asserts aimx doctor runs cleanly post-change.
  • Live MCP smoke transcript on the integration host (12 MCP tools against 0600 root:root config + running aimx 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.rs outside #[cfg(test)] — zero matches (verified).
  • grep -rn 'AIMX_TEST_SKIP_ROOT_CHECK' . outside target/ 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-CREATE as a raw-argv verb.
  • The new "Hooks: ownership = authorization" section (line 233+) explicitly states "There is no per-hook run_as knob: a hook on alice's mailbox runs as alice, 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-hook run_as knob that line 241 explicitly denies.
  • Line 117 — Filesystem table description: "Mailboxes, trust policy, hooks, templates". templates no 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_as explicitly 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-hook run_as knob.

S6-2's first bullet enumerates book/hooks.md, book/cli.md, book/mcp.md, book/agent-integration.mdbook/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:

  1. book/security.md lines 30 / 48 / 117 / 132 / 213 / 317 / 318 / 320 — strike or rewrite the residual template / run_as callouts so the page is internally consistent with its own new "Hooks: ownership = authorization" section. Highest priority: line 320's "Set run_as explicitly" tip directly contradicts line 241's "There is no per-hook run_as knob."
  2. CLAUDE.md line 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:

  1. book/release-notes.md:64 — rewrite the pre-existing Sprint 1 + Sprint 2 mention per CLAUDE.local.md's no-cross-reference rule. Separate cleanup ticket.
  2. 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:root config + running aimx serve) before merge — or, if capturing post-merge, surface a follow-up issue so the AC isn't silently lost.
  3. Several pre-existing Sprint/S\d-\d/User Story matches in src/, tests/, and book files predate this PR. Triage into a separate CLAUDE.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.
@uzyn

uzyn commented May 3, 2026

Copy link
Copy Markdown
Owner Author

Cycle 2 — both Cycle-1 non-blockers addressed

Pushed 0df2834 on top of 5fcbf53.

Non-blocker 1 — book/security.md internal contradiction

Did a full end-to-end pass on the page with the eye of a fresh reader who only knows the post-Phase-1/2 reality (no template-bound hooks, no run_as knob anywhere, every UDS verb owner-gated). Specific fixes:

  • Line 5 (one-sentence summary) — reframed from "DKIM key is the boundary, period" to two boundaries: external (root-only DKIM key) + internal (server-side per-verb auth::authorize keyed on SO_PEERCRED). The whole page reads as a consequence of that split.
  • Line 7 (reasoning paragraph) — replaced "world-readable mail for LLM parsing" (no longer true; mailboxes are per-owner 0700) with "per-owner mailbox isolation for agent privacy"; added "with server-side auth::authorize on every verb" to the world-writable UDS phrase so the design intent matches the implementation.
  • Line 13 (At a glance, UDS bullet) — added the auth::authorize server-side gate; described the socket as "signing oracle + owner-bound CRUD surface", not just "signing oracle".
  • Line 16 (At a glance, out-of-scope bullet) — dropped "per-user mailbox isolation" from the not-implemented list; per-owner isolation is implemented now (lines 121–122).
  • Line 30 (curious local user threat) — struck "escape the hook template sandbox" (no such sandbox exists); added owner-binding consequences (cannot mutate / hook another uid's mailbox; cannot run hooks as anyone other than the mailbox owner).
  • Line 37 (mutually-distrustful humans) — rewrote: each mailbox is 0700, but any local uid can submit to the UDS and the owner-binding only stops cross-uid action — not from creating their own mailbox or sending under one they own. So genuinely-mutually-distrustful humans should still use Postfix / Stalwart.
  • Line 48 (trust-boundary table, Hook subprocess row) — replaced "(run_as)" parenthetical with "(setuid to mailbox.owner_uid)".
  • Line 68 (per-action authz table, HookCrud row) — rewrote: "Template-bound hooks via UDS / MCP. Raw-shell …operator-only" was stale on both halves. Now lists the aimx hooks CLI surface, the three UDS verbs (HOOK-CREATE / HOOK-LIST / HOOK-DELETE), and the three MCP tools, plus a working anchor (hooks.md#mailbox-ownership--hook-authorization); the previous #raw-cmd-hooks-operator-only anchor didn't exist.
  • Line 117 (filesystem table) — dropped "templates" from the config.toml purpose ("Mailboxes, trust policy, hooks, templates" → "Mailboxes, trust policy, hooks").
  • Line 120 (/var/lib/aimx/ purpose) — replaced "world-readable by design" with "traversable; per-mailbox dirs are 0700" — accurate to what the modes actually mean.
  • Line 124 (UDS row) — added "owner-gated CRUD" + "server-side auth::authorize per verb" to the purpose column.
  • Line 132 (/etc/aimx/ paragraph) — dropped "the template definitions" from the secrets+policy enumeration.
  • Line 203 (UDS deliberate-mode rationale) — added the second boundary (server-side auth::authorize keyed on SO_PEERCRED) so the rationale is no longer "DKIM is the only thing protecting you here".
  • Line 210 (rejected alternative — local auth handshake) — updated the parenthetical that claimed "AIMX has no use for [peer credentials] that the DKIM boundary isn't already doing better"; AIMX does use them now, exactly to drive auth::authorize.
  • Line 213 (combined result paragraph) — replaced "the daemon's own checks on From: + template shapes decide what actually happens" with the actual mechanism (per-verb SO_PEERCRED + auth::authorize); listed the new attacker-can'ts (mutate / hook another uid's mailbox, run hooks as anyone other than the owner).
  • Line 220 (HOOK-CREATE verb) — dropped the "no run_as override over MCP" qualifier; there's no run_as knob anywhere now.
  • Line 292 (MCP cannot do) — replaced "override run_as" with "change which uid a hook runs as (hooks always run as the mailbox owner — there is no run_as knob anywhere)".
  • Line 305 (Explicitly out of scope, item 5) — was "UDS socket authorisation. The socket is 0666; the DKIM key is the boundary instead." Rewrote as "Socket-mode-based UDS gating" — i.e. we don't tighten the mode; we run auth::authorize per verb instead. Authorization is in scope; mode-based gating isn't.
  • Lines 317 / 318 / 320 (Hardening tips) — "Keep the template list minimal" → "Keep the per-mailbox hook list minimal"; "Review hook-fire logs after a new template lands" → "after a new hook lands"; "Set run_as explicitly on raw-cmd hooks" (which directly contradicted line 241) → "Pick the mailbox owner deliberately".

Final-state spot-checks against the contradiction the reviewer called out:

$ rg -n 'run_as' book/security.md
220:…validates the hook (no `dangerously_support_untrusted` over the wire)…
     The hook always runs as the mailbox owner — there is 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: …change which uid a hook runs as (hooks always run as the mailbox owner — there is 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 of the knob, every one mutually consistent.

$ rg -n 'template' book/security.md
235: …there is no template-hook layer…

One mention, an explicit denial.

Non-blocker 2 — CLAUDE.md:124 stale claim

Replaced the line that called socket authorization "out of scope" and named DKIM as the boundary with a sentence that explicitly names both boundaries — server-side auth::authorize keyed on the kernel-validated SO_PEERCRED uid, plus the 0600 root:root DKIM key consumed only by the root-running daemon. Kept it tight (one paragraph, same line slot in the bullet list).

Out of scope (per brief)

  • book/release-notes.md:64 "Sprint 1 + Sprint 2" banned-token — left for the maintainer's separate cleanup ticket.
  • "Live MCP smoke transcript on the integration host" AC — out of scope for the dev environment; maintainer captures it.
  • Pre-existing banned-token hits in src/, tests/, other book/ files — separate cleanup ticket.

Local soak (post-fix)

  • cargo fmt -- --check clean on root crate ✓
  • cargo fmt -- --check clean on services/verifier
  • cargo clippy --all-targets -- -D warnings clean on root crate ✓
  • cargo clippy --all-targets -- -D warnings clean on services/verifier
  • cargo test on root crate: 1035 + 106 + 1 passed, 8 ignored (sudo / two-uid gated) ✓
  • cargo test on services/verifier: 43 / 43 passed ✓
  • Banned-token grep on the diffed files: zero hits ✓

@uzyn uzyn left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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 aimx MCP server or CLI. Never write to the data directory directly. The UDS send socket at /run/aimx/aimx.sock is world-writable. Any local user can submit outbound mail through aimx send. The authorisation boundary is the root-only DKIM private key, not filesystem ACLs on the mailbox tree.

Two specific problems:

  1. "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 (and CLAUDE.md's setup description) document that per-mailbox dirs are 0700 chowned to the owner, and book/getting-started.md itself, eight lines later, contradicts this implicitly. A fresh reader hitting the getting-started page first will form a wrong mental model.
  2. "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.md line 5 and CLAUDE.md line 124. After cycle 2, this is now the only place in book/ 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):

  1. 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 in security.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:64 pre-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/, other book/ 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.
@uzyn

uzyn commented May 3, 2026

Copy link
Copy Markdown
Owner Author

Addressed the stale Security model wording in book/getting-started.md. Pushed as 02c76ba.

What was fixed (lines 57-61):

  1. "world-readable mail" claim (false) — rewrote to state that mail is stored under /var/lib/aimx/inbox/<mailbox>/ and /var/lib/aimx/sent/<mailbox>/ with each mailbox's directories chowned to its configured Linux owner at mode 0700, so non-owners (and non-root) cannot read another mailbox.

  2. single-boundary "DKIM key, not filesystem ACLs" framing (stale) — rewrote to name AIMX's two authorization boundaries: the root-only DKIM private key on the SMTP-out external surface, and per-verb auth::authorize keyed on the caller's SO_PEERCRED uid on the UDS internal surface. Wording matches the canonical rewrite shipped to book/security.md and CLAUDE.md in 0df2834.

  3. "per-user mailbox isolation" pointer to Postfix/Stalwart — swept up while in the file. Per-owner 0700 mailboxes do prevent non-owners from reading another mailbox, so the framing was misleading. Reworded to "two users on one host genuinely cannot trust each other to operate the daemon" with the explicit clarification that the trust boundary AIMX defends is the host, not each user on the host — same framing book/security.md uses.

Verification:

  • cargo fmt -- --check: clean
  • cargo clippy --all-targets -- -D warnings: clean
  • cargo test: 1142 passed, 0 failed (no regressions; docs-only change)
  • Banned-token grep on the diff: zero new hits

No new concerns.

@uzyn uzyn left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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 0700 chown story. Matches book/security.md:37 ("Each mailbox is 0700 and chowned to its configured Linux owner") and CLAUDE.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::authorize keyed on SO_PEERCRED on the UDS internal surface). Matches the canonical one-sentence summary at book/security.md:5 and CLAUDE.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:64 pre-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/, other book/ 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:

  1. agents/common/references/workflows.md:275 — rewrite the "filesystem is world-readable" closing sentence to match the per-owner 0700 reality. Mechanical, ~1 line. Small follow-up.
  2. book/release-notes.md:64 pre-existing banned-token mention — separate cleanup ticket.
  3. Live MCP smoke transcript on the integration host — capture before declaring Phase 2 shipped.
  4. Pre-existing banned-token hits in src/, tests/, other book/ 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.

@uzyn uzyn merged commit 8be0f6a into main May 3, 2026
10 checks passed
@uzyn uzyn deleted the user-mailbox-sprint-6 branch May 3, 2026 08:31
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