# Audit: Agent Capability Boundaries ## Scope This audit covers the complete implementation of `m10-agent-capability-boundaries`: the extension of `AgentPolicy` with read-path and profile-override access controls, session-gated read methods, schema build-time validation, and audit integration. ## Files Changed | File | Change Type | Risk | |---|---|---| | `tidal/src/schema/validation/policies.rs` | Additive (5 new fields + Default update) | Low | | `tidal/src/schema/validation/builders/mod.rs` | Additive (new field, new method, new validation) | Low | | `tidal/src/schema/error.rs` | Additive (3 new error variants) | Low | | `tidal/src/session/policy.rs` | Additive (3 new methods, 5 new enum variants, 12 new tests) | Low | | `tidal/src/session/audit.rs` | Additive (3 new AuditKind variants) | Low | | `tidal/src/session/snapshot.rs` | Bug fix (overrides_rejected was hardcoded to 0) | Low | | `tidal/src/db/signals.rs` | Additive (4 new public methods, 2 new private helpers) | Low | | `tidal/src/db/query_ops.rs` | Additive (1 private helper, enforcement calls) | Low | | `tidal/src/lib.rs` | Additive (3 new re-exports) | Low | | `tidal/tests/m10_agent_capability.rs` | New (17 integration tests) | None | No existing public API methods were changed. No WAL changes. No storage changes. No new modules. ## Correctness **Read-path enforcement** correctly follows the deny-before-allow evaluation order specified in the spec. An empty allow list is unrestricted (not a block-all). An empty deny list is a no-op. This matches the spec's zero-cost fast-path requirement. **Profile override enforcement** correctly treats an empty `allowed_profile_overrides` as "no overrides permitted." The query-level profile name supplied by the caller is checked against the policy before the query executor runs. Both `retrieve()` and `search()` are covered. **Snapshot bug fix** (`overrides_rejected` was hardcoded to 0): the fix correctly reads from `state.overrides_rejected.load(Ordering::Relaxed)` in both `build_snapshot` and `build_frozen_snapshot`. This was a latent bug in prior code that the feature exposed and corrected. **`SchemaBuilder::new()` change** from `const fn` to regular `fn` is correct and necessary due to `Vec::new()` not being `const` in the current MSRV (1.91). No callers relied on the `const fn` property. **Sentinel `"*"` expansion** resolves to all names registered via `declare_profile_names()`. If `declare_profile_names()` was not called, the builder's `known_profile_names` is empty, so `"*"` expands to an empty list — effectively disabling the guard, which is the correct behavior (same as empty `allowed_profile_overrides`). ## Backward Compatibility All new `AgentPolicy` fields default to empty `Vec`. The `..AgentPolicy::default()` pattern was added to the one struct literal in `session_tests.rs` that used positional construction. All other existing tests and callers are unaffected. The `Default` impl was updated correctly. ## Security Properties - Read-path enforcement is cooperative at the API boundary (non-session reads remain unrestricted, as documented in the spec non-goals). - Policy evaluation occurs at the call site before any ledger access — violations do not leak data. - Audit log entries are recorded for all violations, enabling forensic review. - `signals_rejected` counter is incremented for read violations (consistent with write violation accounting). - `overrides_rejected` counter is independent (tracked separately from `signals_rejected`). ## Performance All new policy checks short-circuit in O(1) when the relevant lists are empty (the common case for policies that do not restrict reads). Non-empty list checks are O(n) linear scan over `Vec` — acceptable for typical policy sizes (n ≤ 20). The spec notes that HashSet promotion at n > 50 is available as a future optimization. No heap allocation occurs on the fast path for empty-list policies. ## Test Adequacy 17 integration tests cover all 12 acceptance criteria. 12 unit tests in `session/policy.rs` validate the `PolicyEvaluator` methods in isolation. Coverage includes: - Allow-list gate (signal in list → Ok, signal not in list → ReadNotAllowed) - Deny-list gate (signal in deny list → ReadDenied regardless of allow list) - Attribute read allow/deny - Profile override allowed and blocked paths - Audit log recording - Counter increments (signals_rejected, overrides_rejected) - Empty-policy regression (no behavior change) - Schema build-time failure modes (3 cases) - Sentinel `"*"` expansion - Ungated reads unaffected by session policies ## Verdict **APPROVED** — implementation is correct, safe, backward-compatible, and adequately tested. No issues requiring remediation.