refactor(consents): data-source consents only; drop per-agent consent gate #127
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
Manifest
required_consentslists today mix two unrelated dimensions:data:<source>— what data the agent reads.agent:<id>— whether the user opted into this specific agent.In practice nothing ever auto-grants either:
data:coreis auto-granted on signup (services/api/src/routes/auth.ts:108,services/api/src/routes/user.ts:31).services/api/src/routes/integrations.ts(TodoistL113, Google HealthL201) write only tointegration_tokens— they never insert adata:<provider>consent row.agent:<id>consents.Result: the eligibility filter at
services/api/src/profile/eligibility.ts:72drops every agent for every realistic user, even when snippets are freshly pre-computed.Symptom: MLflow trace
tr-591449ea8a72af8e81b6a585234a86ab— userODGp4Gkr7JWemMsqcMLMnhas 5 valid (non-expired) rows inagent_outputsfrom the scheduler, butrecommendercalled/recommendwithagent_ids: []and the orchestrator fell back to its no-context prompt. The user has Todoist connected and Google Health connected previously, but onlydata:coreinuser_consents.Decision
Collapse to a single consent dimension: data source. Consent is implicit in connecting the source.
data:<provider>. Disconnecting revokes it.data:corecontinues to be auto-granted on signup (no integration needed).agent:<id>consent concept is removed. Per-agent control becomes a preference, not a consent:user_preferences[scope='agent:<id>', key='enabled'].eligibility.ts:74already honours this pref — it just stops being load-bearing onceagent:*is removed from manifests.data:*it declares is granted, no active context is insilenced_in_contexts, and itsenabledpreference is notfalse.Plan
1. Auto-grant
data:<provider>on connect, revoke on disconnectIn
services/api/src/routes/integrations.ts:L120insert(integrationTokens)): upsertuser_consentsrow withconsentKey='data:todoist',revokedAt=null,grantedAt=now. Use the sameonConflict(target=[userId,consentKey])shape asservices/api/src/routes/user.ts:30-34.L210): same, withconsentKey='data:google-health'.DELETE /:providerhandler (L216): setrevokedAt=nowon the matching consent row. Do not hard-delete — preserve audit trail.grantDataSourceConsent(userId, provider)/revokeDataSourceConsent(userId, provider)so future providers call one function andprovider → consentKeylives in one place.2. Backfill existing tokens
Migration in
services/api/src/db/migrations.ts: for every row inintegration_tokenswithtokenStatus='active', insert adata:<provider>consent row if missing. Idempotent. Required so users who connected before this change aren't stuck.3. Drop
agent:<id>from manifestsEdit
required_consentsin every file underml/agents/*.py:overdue_task.py:73focus_area.py:38momentum.py:124time_of_day.py:129recent_patterns.py:134health_vitals.py:43Remove only the trailing
agent:<id>entry from each list.data:coreanddata:<source>entries stay.Update
ml/agents/tests/test_manifest.py:46-47: assert every entry inrequired_consentsstarts withdata:(noagent:allowed) anddata:coreis present.(Optional, separate follow-up): rename the field
required_consents→required_data_sourceson the manifest type (ml/agents/manifest.py:42) and the TS wire shape (services/api/src/profile/eligibility.ts:20) once the consent type is collapsed. Skip if it adds churn.4. Eligibility filter
No behaviour change once manifests are clean. Update the docblock at
services/api/src/profile/eligibility.ts:1-12to describe the new model: data-source consents + active-context silencing + per-agentenabledpreference.5. Admin / Web UI
/connectpage already lists integrations and statuses; that page is the consent surface (no new UI required for the consent half).user_preferences[scope='agent:<id>', key='enabled']. This replaces the user-facing role the oldagent:*consents pretended to play.6. Tests
services/api/src/profile/__tests__/eligibility.test.tscases that includeagent:*consents — drop them (most cases only ever includeddata:*).services/api/src/routes/__tests__/integrations.test.ts(or wherever this lives): Todoist callback insertsdata:todoistintouser_consents;DELETE /:providersetsrevokedAt.data:todoistconsent gets one inserted after backfill runs; idempotent on a second run.7. ADR
Add
docs/adr/0015-data-source-consents.mdsuperseding the relevant section of ADR-0014. State the new rule: agent manifests express data dependencies asdata:*only; per-agent control is a preference, not a consent. Reference this issue and tracetr-591449ea8a72af8e81b6a585234a86ab.Out of scope
Verification
After this lands, user
ODGp4Gkr7JWemMsqcMLMn(Todoist + Google Health both connected) should pass eligibility for all five (six, incl. health-vitals) currently registered agents on the next/recommendrequest. The orchestrator should receive a non-emptyagent_ids. The pending followup ("don't schedule non-consented agents") becomes safe to land after this.