182 lines
9.2 KiB
Markdown
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
|