# Tasks: Agent Capability Boundaries ## Task Breakdown ### T1 — Extend `AgentPolicy` with read and profile override fields **File:** `tidal/src/schema/validation/policies.rs` Add five new fields to `AgentPolicy`: - `allowed_read_signals: Vec` — default empty - `denied_read_signals: Vec` — default empty - `allowed_user_attributes: Vec` — default empty - `denied_user_attributes: Vec` — default empty - `allowed_profile_overrides: Vec` — default empty Implement `Default` for `AgentPolicy` with all lists empty and sensible duration/count defaults. Ensure existing callers constructing `AgentPolicy` with struct literals compile with `..Default::default()` for the new fields. **Acceptance:** `cargo test --lib` passes. No existing test broken. `AgentPolicy::default()` compiles and has all new fields set to empty vecs. --- ### T2 — Schema build-time validation for new policy fields **File:** `tidal/src/schema/validation/builders.rs` In `Schema::build()`, after signals and profiles are finalized, add validation loops: 1. For each policy, check all names in `allowed_read_signals` and `denied_read_signals` exist in the signal registry. Return `SchemaError::InvalidSignalName` (or a new `SchemaError::UnknownSignalInPolicy`) if not. 2. Reject policies where the same signal name appears in both `allowed_read_signals` and `denied_read_signals`. 3. For each name in `allowed_profile_overrides`, if it is not the sentinel `"*"`, verify it exists in the registered profiles (built-in + declared). Return `SchemaError` if not. 4. Expand `"*"` sentinel in `allowed_profile_overrides` to the full set of known profile names before storing. **Acceptance:** Unit tests: - Schema builds successfully when new fields reference valid signal/profile names. - Schema build fails with a clear error when `allowed_read_signals` contains an unknown signal. - Schema build fails when `allowed_read_signals` and `denied_read_signals` share a signal. - Schema build fails when `allowed_profile_overrides` contains an unknown non-sentinel profile. - `"*"` sentinel is expanded to all profile names. --- ### T3 — `PolicyEvaluator`: add `check_read`, `check_attribute_read`, `check_profile_override` **File:** `tidal/src/session/policy.rs` Add three new methods to `PolicyEvaluator`: ```rust pub fn check_read(&self, signal_type: &str) -> Result<(), PolicyViolation> pub fn check_attribute_read(&self, attr_key: &str) -> Result<(), PolicyViolation> pub fn check_profile_override(&self, profile_name: &str) -> Result<(), PolicyViolation> ``` Each follows the deny-first, then allow-list pattern. Uses the new `PolicyViolationKind` variants: `ReadDenied`, `ReadNotAllowed`, `AttributeReadDenied`, `AttributeReadNotAllowed`, `ProfileOverrideNotAllowed`. Add unit tests in the existing `#[cfg(test)]` block covering: - Allow list: permitted signal → Ok, not-in-list signal → Err(ReadNotAllowed). - Deny list: denied signal → Err(ReadDenied) even if also in allow list. - Empty lists: any input → Ok (zero-cost fast path). - `check_profile_override` with empty list: any profile → Err. - `check_profile_override` with non-empty list: listed profile → Ok, unlisted → Err. **Acceptance:** All new unit tests pass. `cargo clippy -D warnings` clean. --- ### T4 — Extend `AuditKind` and `SessionState` for new violation tracking **Files:** `tidal/src/session/audit.rs`, `tidal/src/session/state.rs`, `tidal/src/session/snapshot.rs` `audit.rs`: - Add `AuditKind` enum (or extend existing audit entry type) with variants: `ReadDenied`, `AttributeReadDenied`, `ProfileOverrideRejected`. The existing write-denial entries keep their current representation. - Ensure `AuditLog::record` accepts the new kinds and records the signal type / attribute key / profile name in the entry. `state.rs`: - Add `overrides_rejected: AtomicU64` to `SessionState`. - Update `SessionState` construction sites (in `TidalDb::start_session`) to initialize `overrides_rejected` to zero. `snapshot.rs`: - Add `overrides_rejected: u64` to `SessionSnapshot`. - Populate it from `state.overrides_rejected.load(Relaxed)` in both `build_snapshot` and `build_frozen_snapshot`. **Acceptance:** `SessionSnapshot` includes `overrides_rejected`. Audit log entries for new denial kinds are recorded. Existing snapshot tests still pass. --- ### T5 — Session-gated read methods on `TidalDb` **File:** `tidal/src/db/mod.rs` Add four new public methods: ```rust pub fn read_decay_score_for_session( &self, session_id: SessionId, entity_id: EntityId, signal_type: &str, variant: usize, ) -> crate::Result> pub fn read_windowed_count_for_session( &self, session_id: SessionId, entity_id: EntityId, signal_type: &str, window: Window, ) -> crate::Result pub fn read_velocity_for_session( &self, session_id: SessionId, entity_id: EntityId, signal_type: &str, window: Window, ) -> crate::Result pub fn read_user_attribute_for_session( &self, session_id: SessionId, user_id: EntityId, key: &str, ) -> crate::Result> ``` Each method: 1. Looks up the session in `self.sessions`. Returns `TidalError::NotFound` if absent. 2. Retrieves the `AgentPolicy` from the schema by `policy_name`. 3. Calls the appropriate `PolicyEvaluator::check_*` method. 4. On violation: records in audit log, increments `signals_rejected` (or `overrides_rejected`), returns `Err(TidalError::PolicyViolation(violation))`. 5. On success: delegates to the existing underlying signal/user-state read. The existing `read_decay_score`, `read_windowed_count`, `read_velocity` methods are unchanged. **Acceptance:** Methods compile. `cargo test --lib` passes. --- ### T6 — Profile override enforcement in `retrieve` and `search` **File:** `tidal/src/db/mod.rs` In `TidalDb::retrieve` and `TidalDb::search`, when `for_session` is `Some(sid)`: 1. Look up the session. 2. If the query's `profile` field is set and differs from the session's default profile (stored in `SessionState.metadata["_default_profile"]` or a new dedicated field): call `PolicyEvaluator::check_profile_override(profile_name)`. 3. On violation: record audit entry with `AuditKind::ProfileOverrideRejected`, increment `state.overrides_rejected`, return `Err(TidalError::PolicyViolation(...))`. 4. On success: proceed with the query normally. Add `default_profile: Option` to `SessionState` (set from `start_session` params if a profile is provided; `None` means any profile is an override). **Acceptance:** Query with an allowed profile override succeeds. Query with a disallowed profile override returns `Err`. `overrides_rejected` counter is incremented on violation. --- ### T7 — Integration test: `m10_agent_capability` **File:** `tidal/tests/m10_agent_capability.rs` Write an integration test covering all acceptance criteria from the spec: 1. Schema setup: signals `view`, `like`, `hide`; policy `read-restricted` with `allowed_read_signals: ["view"]` and `denied_read_signals: ["hide"]`; policy `profile-restricted` with `allowed_profile_overrides: ["search"]`. 2. Signal read allow list: `read_decay_score_for_session` for `view` → Ok; for `like` → Err(ReadNotAllowed). 3. Signal read deny list: `read_decay_score_for_session` for `hide` → Err(ReadDenied). 4. Non-session read: `read_decay_score` for `hide` → Ok (unrestricted). 5. Attribute read: session with `allowed_user_attributes: ["locale"]`; read `locale` → Ok; read `age_range` → Err(AttributeReadNotAllowed). 6. Profile override allowed: retrieve with profile `search` and `profile-restricted` session → Ok. 7. Profile override disallowed: retrieve with profile `for_you` and `profile-restricted` session → Err(ProfileOverrideNotAllowed). 8. Snapshot check: `overrides_rejected == 1` after one disallowed override. 9. Audit log check: audit entries for read denials are present. 10. Empty-policy regression: policy with all new fields empty behaves identically to the existing `AgentPolicy`. 11. Schema build failure: policy with unknown signal in `allowed_read_signals` returns `SchemaError`. 12. Schema build failure: policy with same signal in allow and deny lists returns `SchemaError`. 13. Schema build failure: policy with unknown profile in `allowed_profile_overrides` returns `SchemaError`. 14. Sentinel expansion: `allowed_profile_overrides: ["*"]` → all profiles are allowed. **Acceptance:** All integration tests pass under `cargo test --manifest-path tidal/Cargo.toml --test m10_agent_capability`. --- ## Completion Checklist - [ ] T1: `AgentPolicy` extended with 5 new fields; `Default` impl added - [ ] T2: Schema build-time validation for new policy fields - [ ] T3: `PolicyEvaluator::check_read`, `check_attribute_read`, `check_profile_override` implemented and tested - [ ] T4: `AuditKind` extended; `SessionState.overrides_rejected` added; `SessionSnapshot.overrides_rejected` populated - [ ] T5: Four session-gated read methods on `TidalDb` - [ ] T6: Profile override enforcement in `retrieve` / `search` - [ ] T7: Integration test `m10_agent_capability.rs` — all 14 cases pass - [ ] `cargo fmt --manifest-path tidal/Cargo.toml` clean - [ ] `cargo clippy --manifest-path tidal/Cargo.toml -D warnings` clean - [ ] `cargo test --manifest-path tidal/Cargo.toml --lib` all passing - [ ] `cargo test --manifest-path tidal/Cargo.toml --test m10_agent_capability` all passing