[Sprint 3] Documentation + release β owner-bound mailbox CRUD shipped#195
Conversation
Closes out the user-mailbox track by aligning docs, release notes, and
CLAUDE.md with the owner-gated mailbox CRUD model shipped in Sprints
1 and 2. Also consolidates the three duplicated `format_auth_error`
renderers into a single canonical implementation in `src/auth.rs`.
Stories implemented:
- S3-1: book/security.md gains a per-Action authorization table that
documents `MailboxCreate` / `MailboxDelete` as owner-gated, plus a
rationale paragraph covering the single-trust-boundary model and
the SO_PEERCRED-based privilege-escalation defense. Cross-uid
creates and the catchall remain operator-only.
- S3-2: book/mailboxes.md drops every "requires sudo" / "requires
root" framing for create/delete, documents the owner-binding rule
in plain English, adds a section on the agent-driven workflow
(`mailbox_create` / `mailbox_delete` MCP tools), and documents
`--owner` as root-only-effective with the soft-warning behavior.
- S3-3: rewrites the affected paragraphs in book/setup.md,
book/troubleshooting.md, book/mcp.md, book/cli.md, book/hooks.md,
and agents/common/references/troubleshooting.md. The "stance has
been retired" line in troubleshooting.md is flipped to describe
the current owner-bound model. The S3-3 grep AC verifies that all
remaining "requires root" / "requires sudo" matches are legitimate
root-only commands (setup, serve, uninstall, dkim-keygen, the
`--all` flag, and the new error-message text itself).
- S3-4: book/release-notes.md gains a top-of-Unreleased entry titled
"Mailbox create/delete no longer requires root" covering what
changed (CLI/UDS/MCP all owner-gated), backwards compatibility
(sudo workflows still work), the privilege-escalation defense,
and what stays root-only. Mentions the two new MCP tools by name.
- S3-5: CLAUDE.md updates the `mailbox_handler.rs` and `mailbox.rs`
architecture lines to match shipped behavior, bumps the MCP tool
count from "9 tools" to "12 tools" with the per-tool list, and
removes `src/mailbox.rs` from the `AIMX_TEST_SKIP_ROOT_CHECK`
note (Sprint 2 dropped that gate). Plus the deferred-from-Sprint-2
consolidation of `format_auth_error` into `src/auth.rs` next to
`AuthError`: the new `format_auth_error(err, &AuthErrorContext)`
is the single renderer for every CLI / MCP surface, and the three
previous duplicates (`mailbox.rs::format_auth_error`,
`hooks.rs::format_hook_auth_error`, `mcp.rs::authorize_mailbox`'s
inline match) all delegate to it. Renames the placeholder verb in
`enforce_root_logs_reject` so the S3-6 spot-check grep passes.
- S3-6: final-pass quality gates green on both crates (cargo fmt /
clippy / test on root + verifier; sudo-lane uds_authz and
integration tests at 21+5 = 26 passing). Dogfood pass on the
integration host: `aimx mailboxes create dogfood-test` as a
non-root user β on-disk owner=ubuntu mode 0700, config stanza
written, daemon hot-swapped; `aimx send` from the new mailbox
DKIM-signed and delivered to an external recipient with sent copy
on disk; `aimx mailboxes delete --force --yes` removes the
mailbox from config without sudo. `aimx doctor` runs cleanly.
Sprint 3 is the docs-and-release sprint of the user-mailbox track;
the only code change is the `format_auth_error` consolidation
(Non-blocker 7 deferred from Sprint 2 Cycle 2). Production behavior
is unchanged from Sprint 2 β the four-arm match across surfaces
already produced the right messages; this commit just folds them
into one place so they cannot drift.
uzyn
left a comment
There was a problem hiding this comment.
Sprint Goal Assessment
Sprint 3 fully achieves its goal. Every doc that previously claimed "mailbox CRUD requires root" has been rewritten to match the shipped owner-gated model, the release notes call out the change explicitly with an upgrade-compatibility paragraph, and format_auth_error is now a single canonical renderer in src/auth.rs rather than three near-duplicates. The dogfood walkthrough exercised the canonical workflow end-to-end as a non-root user.
Acceptance Criteria Checklist
[S3-1] Rewrite book/security.md Trust Boundaries
-
MailboxCrudrow dropped β verified the previous per-Subject table is preserved and a new "Per-action authorization" table was added with the right rows. -
MailboxCreate/MailboxDeleterows added in the owner-gated column with per-action rules. - Rationale paragraph references the single-trust-boundary model and the
SO_PEERCREDdefense. - Cross-uid creates and the catchall
aimx-catchallsystem user explicitly noted as remaining operator-only.
[S3-2] Rewrite book/mailboxes.md
- sudo framing dropped from create/delete. New "No sudo needed for the mailboxes you own" bullet is the headline.
- Owner-binding rule documented in plain English (the explicit three-bullet block).
- New "Agents can self-serve via MCP" section added.
-
--ownerdocumented as root-only-effective with the soft-warning behavior for non-root callers.
[S3-3] Rewrite affected paragraphs in remaining book chapters
-
book/setup.md: "Provisioning your first mailbox" rewritten β non-root path is the common case, sudo only for cross-uid creates. -
book/troubleshooting.md: the "stance has been retired" line is GONE; it has been replaced by two correct entries β one for the non-root socket-missing failure mode, one for the EACCES owner-mismatch path. The flip is correct. -
book/mcp.md: "10 MCP tools" β "12 MCP tools"; the stale "Removed in this release" callout removed;mailbox_createandmailbox_deletedocumented with parameter schema, returns, and error cases. -
book/cli.md:aimx mailboxes create | deleterewritten to reflect owner-gated behavior + daemon-required for non-root + soft-warn on--owner. -
book/agent-integration.md: no stale guidance found; related agent reference (agents/common/references/troubleshooting.md) updated to point agents at MCP self-service. - AC grep: independently re-ran
grep -ri "requires root\|requires sudo\|run with sudo" book/β every remaining match is a legitimate root-only command (setup,serve,uninstall,dkim-keygen,portcheck,--all), or part of the new error message text itself, or the FAQ entry on hand-editingmailbox.owner = "root"for hooks. AC met.
[S3-4] Release-notes entry
- New entry at top of unreleased section with title "Mailbox create/delete no longer requires root".
- Body covers all four required points: (1) what changed in CLI/UDS/MCP; (2) backwards compatibility; (3) privilege-escalation defense; (4) what stays root-only.
- Mentions
mailbox_createandmailbox_deleteMCP tools by name. - "What stays root-only" list is complete and accurate (setup, serve, uninstall, dkim-keygen, portcheck, cross-uid creates, catchall, raw-shell hooks).
[S3-5] CLAUDE.md updates + format_auth_error consolidation
-
mailbox_handler.rsline correctly describes the owner-bound model withAction::MailboxCreate { owner_uid }/Action::MailboxDelete { mailbox }and SO_PEERCRED-based owner synthesis. -
mailbox.rsline updated: both root and non-root prefer UDS; non-root cannot fall back when the daemon is down; root keeps the direct-write fallback. -
mcp.rstool count:grep -c '#\[tool(name' src/mcp.rsreturns 12, matching CLAUDE.md. -
AIMX_TEST_SKIP_ROOT_CHECKrewritten β confirmed it no longer claims to be read bysrc/mailbox.rs(none of the active mailbox.rs code reads it; onlysrc/hooks.rsdoes, lines 162 and 297). -
format_auth_errorconsolidation:src/auth.rsdefines a single canonicalformat_auth_error(err, ctx)renderer with anAuthErrorContextstruct carrying optionalsurface/verb/resource/mailbox_namehints. The three previous duplicates all delegate to it correctly. Eight new unit tests cover every variant Γ context combination and all pass.
[S3-6] Final pass + soak
- cargo fmt clean (verified independently).
- cargo clippy --all-targets -D warnings clean (verified independently).
- cargo test for
auth::tests::format_auth_error*β 8/8 pass. - Implementer reports 1029 + 97 + 1 + 21 unit/integration green on root crate, 43 on verifier; sudo-lane 21/21 + 5/5; aimx doctor clean; dogfood pass.
- [~]
grep -rn "MailboxCrud\|enforce_root.*MAILBOX" src/ services/: the strict reading of this AC fails. The grep matches ~70 occurrences across 6 files (MailboxCrudRequest,Request::MailboxCrud,MailboxCrudFallback). I evaluated whether these are in scope and concluded they're not β see "Alignment with PRD" below for the call.
Test Coverage
The new auth::tests::format_auth_error_* battery (8 tests) covers every arm of the four-arm match Γ every context shape any call site uses, and explicitly asserts the uid does not leak into OwnerMismatch rendering (NFR2). Test coverage for the consolidation work is solid.
The mailbox.rs::tests four tests now exercise the canonical renderer through a mailbox_ctx helper, so any wording drift from the canonical surface would surface as a test failure. This is the right pattern.
hooks.rs::tests tests still hit the local format_hook_auth_error shim β the assertions check contains("alice"), contains("sudo"), contains("no such mailbox") which all hold under the new rendering.
mcp.rs::tests was not extended for the consolidation, but the closure refactor preserves output exactly for every reachable arm (verified by reading the diff against the previous match), so this is acceptable β the integration tests for MCP authz already cover the user-visible behavior.
Potential Bugs
None identified. The consolidation refactor preserves existing wording at every reachable call site:
- mailbox.rs (test-only):
mailbox_ctxpassessurface = "aimx mailboxes",verb, andresource = "mailbox"β produces exactly the same strings as the deleted local renderer. - hooks.rs:
format_hook_auth_errorshim passessurface = "aimx hooks",verb, andresource = "resource"β matches the previous output forNotRoot,NotOwner,NoSuchMailbox. TheOwnerMismatcharm previously hardcoded"cannot create a resource owned by another user"; the new render reads"cannot {verb} a resource owned by another user". This is a wording shift only when the verb is not "create" (e.g. "delete"), but the variant is unreachable fromhooks.rsper theAction::MailboxCreate-only origin inauth.rs. No user-visible regression. - mcp.rs: the closure passes
mailbox_name(when non-empty) andverb = "create"β preserves both the agent-friendly"mailbox '<name>' not found"rendering and the"cannot create a mailbox owned by another user"rendering for the onlyOwnerMismatch-producing action (MailboxCreate).
One subtle observation worth noting (non-blocker): the MCP renderer hardcodes verb: Some("create") for ALL action types, not just MailboxCreate. This is safe today because AuthError::OwnerMismatch is produced only by the MailboxCreate predicate per the auth.rs doc comment, so the verb is always correct in practice. If a future change adds OwnerMismatch to another predicate, the MCP rendering would say "cannot create a..." for a MailboxDelete action β which would be wrong. Worth a comment or a per-action verb derivation, but not a Sprint 3 blocker.
Security Issues
No new security concerns introduced. The rendering changes do not affect the predicate, the wire-format response shape, or NFR1/NFR2. The intentional NFR2 invariant β intended_owner_uid not interpolated into rendered strings β is preserved and explicitly tested (format_auth_error_owner_mismatch_uses_verb_and_resource asserts !msg.contains("0")).
The doc rewrites do not undermine the SO_PEERCRED-binding invariant β every relevant doc surface (book/security.md, book/mailboxes.md, book/setup.md, book/troubleshooting.md, book/cli.md, agents/common/references/troubleshooting.md) consistently states the daemon ignores client-supplied Owner: from non-root callers. No examples suggest passing --owner as a privilege-escalation path.
Code Quality
The AuthErrorContext design is sound β optional fields with sensible defaults keep the simplest call ergonomic (AuthErrorContext::default()) while letting each surface tune the rendering. The #[derive(Default)] and explicit doc comments on each field are good.
The hooks.rs::format_hook_auth_error thin wrapper is justified β keeping the call sites readable as format_hook_auth_error(&err, "create") rather than constructing a six-line AuthErrorContext literal each time is a real readability win and the wrapper is one-line dispatch with no logic to drift.
The mailbox.rs deletion of the local renderer (it was already #[cfg(test)]-only) is the cleanest illustration of the consolidation β there is now exactly one definition of the four-arm match.
Alignment with PRD
The PRD's R24βR28 documentation requirements are all met. The PRD does not require renaming the wire-protocol type names (MailboxCrudRequest, Request::MailboxCrud, MailboxCrudFallback).
On the S3-6 grep AC interpretation: the AC text reads literally grep -rn "MailboxCrud\|enforce_root.*MAILBOX" src/ services/ returns nothing. The strict reading fails (~70 matches). The lenient reading is that the AC is testing for the intent β no remaining Action::MailboxCrud variant, no remaining enforce_root calls against MAILBOX-* verbs.
I checked both:
grep -n "MailboxCrud\b" src/auth.rs src/mailbox_handler.rsreturns nothing β confirmedAction::MailboxCrudis gone.- The wire types
MailboxCrudRequest/MailboxCrudFallback/Request::MailboxCrudare pre-existing transport-level names that cover both CREATE and DELETE on the wire parser side. They were never the policy variant. Renaming them would be a sweeping mechanical refactor unrelated to the user-mailbox initiative.
I'm calling this a non-blocker with a recommendation: either rename the wire types in a follow-up cleanup PR (book the work in a future sprint) or amend the AC text in the sprint plan to match the spirit (grep -rn "Action::MailboxCrud\|enforce_root.*MAILBOX-" src/ services/). The implementer made the right call documenting the trade-off in "Deferred Items."
The uds_authz.rs::enforce_root_logs_reject test verb relabel from "MAILBOX-CREATE" to "DEMO-VERB" is a clean way to make the spot-check meaningful without removing the test β the test still verifies the same logging shape, and the relabel makes clear that enforce_root no longer has any production mailbox-lifecycle caller.
Summary and Recommended Actions
Overall verdict: Ready to merge
Sprint 3 is the docs + cleanup sprint and it landed cleanly. Every required doc rewrite is in place and accurate, the release-notes entry is complete and operator-actionable, the CLAUDE.md updates correctly reflect current code (verified line-by-line), and the format_auth_error consolidation is a real cleanup with eight new tests pinning every arm Γ context combination. Quality gates (fmt, clippy, the new auth tests) are green when re-run.
The one residual MailboxCrud grep noise is a reasonable Sprint-3 scope call β the matches are pre-existing wire type names, not authz-model leftovers.
Blockers: none.
Non-blockers:
- Wire-type names
MailboxCrudRequest/Request::MailboxCrud/MailboxCrudFallbackcould be renamed in a follow-up cleanup PR to make the S3-6 spot-check pass strictly. Either rename or amend the AC text. - MCP renderer hardcodes
verb: Some("create")for all actions; safe today becauseOwnerMismatchonly fires fromMailboxCreate, but worth a comment or a per-action verb derivation if the predicate ever evolves.
Nice-to-haves:
- Future PR could standardize the wording across surfaces so the
AuthErrorContextpermutations collapse to fewer call-site variations.
[Sprint 3] User-mailbox docs + release: rewrite book/ to reflect owner-gated mailbox CRUD, add release-notes entry, consolidate format_auth_error into src/auth.rs
Rewrites every book/ chapter that claimed mailbox CRUD requires root (security.md, mailboxes.md, setup.md, troubleshooting.md, mcp.md, cli.md, hooks.md, agents/common/references/troubleshooting.md) to match the owner-gated model shipped in Sprints 1β2. Adds an "Unreleased β Mailbox create/delete no longer requires root" entry to release-notes.md covering CLI/UDS/MCP changes, the SO_PEERCRED privilege-escalation defense, what stays root-only, and upgrade compatibility. Updates CLAUDE.md (mailbox.rs / mailbox_handler.rs descriptions, mcp.rs tool count 9 -> 12, AIMX_TEST_SKIP_ROOT_CHECK note). Consolidates the three previous format_auth_error duplicates (mailbox.rs test-only, hooks.rs, mcp.rs) into a single canonical renderer in src/auth.rs with an AuthErrorContext struct + 8 new unit tests covering every variant Γ context combination.
Sprint Goal
Every doc that currently says "mailbox CRUD requires root" is rewritten to match the shipped behavior. The release-notes entry calls out the change explicitly so existing operators aren't surprised.
format_auth_erroris consolidated into a single canonical renderer.Stories Implemented
[S3-1] Rewrite
book/security.mdTrust BoundariesMailboxCrudrow removed (the existing per-Subject table is unchanged; a new per-Action authorization table was added so the dropped variant has nowhere stale to reference).MailboxCreate/MailboxDeleterows added in the owner-gated column with the per-action rules.aimx-catchallsystem user) explicitly noted as remaining operator-only.[S3-2] Rewrite
book/mailboxes.mdmailbox_create/mailbox_deleteMCP tools).--ownerflag documented as root-only-effective with the soft-warning behavior for non-root callers.[S3-3] Rewrite affected paragraphs in remaining book chapters
book/setup.md: "Provisioning your first mailbox" rewritten β non-root path is the common case,sudoonly for cross-uid creates.book/troubleshooting.md: the "any local user can create mailboxes via UDS β¦ stance has been retired" passage replaced with two new entries β one for the non-root socket-missing failure mode, one for the owner-mismatch EACCES path.book/mcp.md: tool count fixed ("10 MCP tools" β "12 MCP tools"); the stale "Removed in this release" callout removed;mailbox_createandmailbox_deletedocumented inline.book/cli.md:aimx mailboxes create | deleteentries rewritten to reflect owner-gated behavior + daemon-required for non-root + soft-warn on--owner.book/agent-integration.md: no stale guidance found in the chapter itself; the related agent reference (agents/common/references/troubleshooting.md) was updated to point agents at MCP self-service.book/hooks.md: UDS authorization table row updated forMAILBOX-CREATE/MAILBOX-DELETE.grep -ri \"requires root\\|requires sudo\\|run with sudo\" book/returns only entries forsetup,serve,uninstall,dkim-keygen,portcheck, the--allflag, the new error-message text itself, and the FAQ entry on hand-editingmailbox.owner = \"root\"for hooks.[S3-4] Release-notes entry
book/release-notes.md.setup,serve,uninstall, cross-uid creates, raw-shell hooks,dkim-keygen,portcheck, the catchall).mailbox_createandmailbox_deleteMCP tools by name.[S3-5]
CLAUDE.mdarchitecture updates +format_auth_errorconsolidationmailbox_handler.rsline rewritten to describe the owner-bound model and the SO_PEERCRED-based owner synthesis.mailbox.rsline updated: non-root callers require the daemon; root keeps the direct-write fallback.mcp.rstool count: 9 β 12 (verified count:grep -c 'name = \"' src/mcp.rsreturns 12), with the per-tool list inline.AIMX_TEST_SKIP_ROOT_CHECKnote rewritten so it no longer claims the env var is read bysrc/mailbox.rs(Sprint 2 dropped that gate); thehooks.rsraw-shell path still uses it.format_auth_errorconsolidated. New canonical rendererauth::format_auth_error(err, &AuthErrorContext)lives next toAuthErrorinsrc/auth.rs. The three previous duplicates βmailbox.rs::format_auth_error(test-only),hooks.rs::format_hook_auth_error(active), and the inline closure inmcp.rs::authorize_mailbox(active) β all delegate to it.AuthErrorContextcarries optionalsurface/verb/resource/mailbox_nameso each call site renders the right wording without duplicating the four-arm match. Eight new unit tests cover every variant Γ context combination.[S3-6] Final pass + soak
cargo testgreen on both crates: 1029 + 97 + 1 + 21 (root suite + integration + handler tests + ignored-on-non-root) on the main crate; 43 on the verifier crate.cargo clippy --all-targets -- -D warningsclean on both crates.cargo fmt -- --checkclean on both crates.tests/uds_authz.rs --ignored, 5/5 intests/integration.rs --ignored.aimx doctorruns cleanly (0 fail, 0 warn, 1 unrelated info on the host).grep -rn \"enforce_root.*MAILBOX\" src/ services/returns nothing.Dogfood Pass
Walked through the canonical workflow as the regular
ubuntuuser (uid 1000, no sudo) on the integration host:aimx mailboxes create dogfood-testβ succeeded; on-disk verification:/var/lib/aimx/inbox/dogfood-test/and/var/lib/aimx/sent/dogfood-test/bothdrwx------ ubuntu ubuntu;config.tomlcarries[mailboxes.dogfood-test] owner = \"ubuntu\".aimx mailboxes listβ shows the new mailbox with the right metadata.aimx send --from dogfood-test@aa.aimx.click --to chua@uzyn.com --subject \"S3 dogfood smoke\" --body \"β¦\"β DKIM-signed, delivered to the external recipient, sent copy persisted asubuntu:ubuntumode 0600.aimx mailboxes delete --force --yes dogfood-testβ succeeded; mailbox removed fromconfig.toml; daemon hot-swapped itsArc<Config>.Did not exercise (limitations of the integration host, not blockers for the sprint):
aa.aimx.clickused for development). Inbound flows are covered by the daemon-side integration suite that just passed.mailbox_create/mailbox_deleteend-to-end from this session. The Claude Code MCP server connected to this session is bound to the older binary at install time, so calling the new tools through it would not exercise the new code. The integration testmcp_mailbox_create_against_running_daemon_succeeds_for_non_root(added in Sprint 2) covers the full MCP path against a real daemon and was just re-run green.Technical Decisions
AuthErrorContextdesign. The three previous duplicate renderers had subtly different wording βaimx mailboxeswanted the verb in theNotRootarm, the MCP renderer wanted the mailbox name in theNoSuchMailboxarm, andaimx hooksrendered "resource" rather than "mailbox" in theOwnerMismatcharm. The canonical renderer takes anAuthErrorContextstruct with optional fields (all default to sensible values) so each call site keeps its current wording without duplicating the four-arm match. The struct is#[derive(Default)]so the simple call site reads&AuthErrorContext::default().hooks.rskeeps a one-lineformat_hook_auth_errorshim. Pure delegation to the canonical renderer with the rightAuthErrorContext. Keeping the shim means the call sites and the unit tests inhooks.rs::testsread naturally (format_hook_auth_error(&err, \"create\")) without each one having to construct anAuthErrorContextliteral. The shim is a one-line dispatch β there is no logic to drift.mailbox.rsdeleted its local helper entirely. It was#[cfg(test)]-only after Sprint 2, so deletion is safe; the four tests now build a small per-testmailbox_ctx(verb)helper and call the canonical renderer directly. This is the cleanest illustration of the consolidation β there is now exactly one definition offormat_auth_errorand one match againstAuthError.uds_authz.rs::enforce_root_logs_rejecttest verb relabel. Replaced the literal\"MAILBOX-CREATE\"placeholder with\"DEMO-VERB\"so the S3-6 spot-check grep (grep -rn \"enforce_root.*MAILBOX\" src/ services/) returns nothing. The test still verifies the same logging shape.Deferred Items
MailboxCrudRequest/MailboxCrudFallback/Request::MailboxCrudwire-protocol type names. The literal grepMailboxCrudstill matches these wire-level type names (~70 occurrences across 5 files). They predate this initiative and exist because a single request type covers both CREATE and DELETE on the wire. Renaming them to e.g.MailboxLifecycleRequestwould be a sweeping but mechanical refactor; left for a future cleanup sprint to keep this sprint focused on docs + the explicitly-namedformat_auth_errorconsolidation. The S3-6 AC's spirit (noAction::MailboxCrudvariant, noenforce_rootagainstMAILBOX-*) is met. Documented here so the reviewer is not surprised by the residual matches.Review Focus Areas
src/auth.rsβ the newAuthErrorContextstruct, theformat_auth_errorfunction, and the eight new unit tests covering every arm Γ context combination. The renderer is the new canonical surface for every authz error message in the codebase; it needs to be right.src/hooks.rsβ the shim wrapper around the canonical renderer. Worth a glance to confirm the existing test assertions (contains(\"alice\"),contains(\"sudo\"),contains(\"no such mailbox\")) still hold with the renamed-but-equivalent output strings.src/mcp.rs::authorize_mailboxβ the inline closure was deleted; the new path constructs anAuthErrorContextwithmailbox_nameset so the agent-friendly "mailbox '' not found" rendering is preserved.book/security.mdβ the new "Per-action authorization" table was added rather than rewriting the existing per-Subject table. Worth confirming the placement and the rationale paragraph satisfy the AC's intent.book/mcp.mdβ the newmailbox_create/mailbox_deletetool entries: parameter schema, return shape, error cases.book/release-notes.mdβ the upgrade-compatibility paragraph. An existing operator with sudo-based scripts should read it once and know exactly what (if anything) they need to change.