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

9.2 KiB

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:

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:

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.


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