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 emptydenied_read_signals: Vec<String>— default emptyallowed_user_attributes: Vec<String>— default emptydenied_user_attributes: Vec<String>— default emptyallowed_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:
- For each policy, check all names in
allowed_read_signalsanddenied_read_signalsexist in the signal registry. ReturnSchemaError::InvalidSignalName(or a newSchemaError::UnknownSignalInPolicy) if not. - Reject policies where the same signal name appears in both
allowed_read_signalsanddenied_read_signals. - For each name in
allowed_profile_overrides, if it is not the sentinel"*", verify it exists in the registered profiles (built-in + declared). ReturnSchemaErrorif not. - Expand
"*"sentinel inallowed_profile_overridesto 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_signalscontains an unknown signal. - Schema build fails when
allowed_read_signalsanddenied_read_signalsshare a signal. - Schema build fails when
allowed_profile_overridescontains 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_overridewith empty list: any profile → Err.check_profile_overridewith 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
AuditKindenum (or extend existing audit entry type) with variants:ReadDenied,AttributeReadDenied,ProfileOverrideRejected. The existing write-denial entries keep their current representation. - Ensure
AuditLog::recordaccepts the new kinds and records the signal type / attribute key / profile name in the entry.
state.rs:
- Add
overrides_rejected: AtomicU64toSessionState. - Update
SessionStateconstruction sites (inTidalDb::start_session) to initializeoverrides_rejectedto zero.
snapshot.rs:
- Add
overrides_rejected: u64toSessionSnapshot. - Populate it from
state.overrides_rejected.load(Relaxed)in bothbuild_snapshotandbuild_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:
- Looks up the session in
self.sessions. ReturnsTidalError::NotFoundif absent. - Retrieves the
AgentPolicyfrom the schema bypolicy_name. - Calls the appropriate
PolicyEvaluator::check_*method. - On violation: records in audit log, increments
signals_rejected(oroverrides_rejected), returnsErr(TidalError::PolicyViolation(violation)). - 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):
- Look up the session.
- If the query's
profilefield is set and differs from the session's default profile (stored inSessionState.metadata["_default_profile"]or a new dedicated field): callPolicyEvaluator::check_profile_override(profile_name). - On violation: record audit entry with
AuditKind::ProfileOverrideRejected, incrementstate.overrides_rejected, returnErr(TidalError::PolicyViolation(...)). - 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:
- Schema setup: signals
view,like,hide; policyread-restrictedwithallowed_read_signals: ["view"]anddenied_read_signals: ["hide"]; policyprofile-restrictedwithallowed_profile_overrides: ["search"]. - Signal read allow list:
read_decay_score_for_sessionforview→ Ok; forlike→ Err(ReadNotAllowed). - Signal read deny list:
read_decay_score_for_sessionforhide→ Err(ReadDenied). - Non-session read:
read_decay_scoreforhide→ Ok (unrestricted). - Attribute read: session with
allowed_user_attributes: ["locale"]; readlocale→ Ok; readage_range→ Err(AttributeReadNotAllowed). - Profile override allowed: retrieve with profile
searchandprofile-restrictedsession → Ok. - Profile override disallowed: retrieve with profile
for_youandprofile-restrictedsession → Err(ProfileOverrideNotAllowed). - Snapshot check:
overrides_rejected == 1after one disallowed override. - Audit log check: audit entries for read denials are present.
- Empty-policy regression: policy with all new fields empty behaves identically to the existing
AgentPolicy. - Schema build failure: policy with unknown signal in
allowed_read_signalsreturnsSchemaError. - Schema build failure: policy with same signal in allow and deny lists returns
SchemaError. - Schema build failure: policy with unknown profile in
allowed_profile_overridesreturnsSchemaError. - 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:
AgentPolicyextended with 5 new fields;Defaultimpl added - T2: Schema build-time validation for new policy fields
- T3:
PolicyEvaluator::check_read,check_attribute_read,check_profile_overrideimplemented and tested - T4:
AuditKindextended;SessionState.overrides_rejectedadded;SessionSnapshot.overrides_rejectedpopulated - 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.tomlcleancargo clippy --manifest-path tidal/Cargo.toml -D warningscleancargo test --manifest-path tidal/Cargo.toml --liball passingcargo test --manifest-path tidal/Cargo.toml --test m10_agent_capabilityall passing