tidaldb/.sdlc/features/m10-agent-capability-boundaries/tasks.md

182 lines
9.2 KiB
Markdown

# 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<String>` — default empty
- `denied_read_signals: Vec<String>` — default empty
- `allowed_user_attributes: Vec<String>` — default empty
- `denied_user_attributes: Vec<String>` — default empty
- `allowed_profile_overrides: Vec<String>` — 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<Option<f64>>
pub fn read_windowed_count_for_session(
&self, session_id: SessionId, entity_id: EntityId,
signal_type: &str, window: Window,
) -> crate::Result<u64>
pub fn read_velocity_for_session(
&self, session_id: SessionId, entity_id: EntityId,
signal_type: &str, window: Window,
) -> crate::Result<f64>
pub fn read_user_attribute_for_session(
&self, session_id: SessionId, user_id: EntityId, key: &str,
) -> crate::Result<Option<String>>
```
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<String>` 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