Repository navigation
Privacy: a request's own words are its requester's and a manager's (0480) - #927
NewWorldVenture wants to merge 6 commits into
Conversation
#892 comment 5973332041. ai_requests.request_text (what a member typed to the concierge, or a routine's prompt) and ai_requests.clarifications (the planner's questions and the member's answers) were readable by every family member: 0250's ai_requests_select is is_family_member(family_id) and no column grant narrowed them. Reproduced on the replayed schema: a child session read a teen's request and answers. The run page showed them the same way, reading the request with the viewer's session. 0480 narrows by COLUMN, not row: authenticated keeps SELECT on every other ai_requests column (the F19 meter counts rows through a member session; the plan/step/context/event policies read id/requested_by as the member), loses request_text and clarifications, and anon loses the table. ai_request_words(uuid[]) (SECURITY DEFINER, search_path pinned, authenticated + service_role only) returns both to the requester or a can_manage_family manager. The service role is unaffected. App: the run detail selects named columns and takes the words from the function (a sibling still opens the run, as "Your request"); trust activity does the same. Tests: run page as requester/manager/sibling, no member read selects request_text, clarifications or *; trust activity fails closed on the function. Red on the old code. Probe docs/audit/a-request-text-is-its-requesters-check.sql: 9 assertions including a negative control; 5 failures before 0480, 0 after. Not applied to production. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01776xJyhVe8xJQvTGeNfT9T
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
High-risk residual bypass at lib/ai/runs/detail.ts:251,253: 0480 protects the request columns, but oRunView() still returns family-readable plan.objective (then |
#927 comment 5973640023: 0480 withheld the request's own columns, but the run page still returned the plan's objective (then the run summary) and reasoning verbatim, and the model writes those from the request. With the sentinel copied into ai_plans.objective, a sibling's run read returned it. loadRunDetail now gates everything prompt-derived on whether ai_request_words answered for this viewer (the requester or a manager): the plan, its steps, the events, the approvals and run.summary. A failed words read hides them too. Decided in the loader, not left to RLS, because a server action may pass the service client. A sibling still sees the run, its state and when it ran, as "Your request". Tests (store with no RLS = the service-client path): sentinel in the objective, reasoning, summary, a step and an event. Requester and manager see them; a sibling sees none (red on the previous commit); a run with no request behind it is unchanged. The family-readable run summary in the table itself (home, needs-you, display, briefing, autopilot lists) is recorded as a product decision. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01776xJyhVe8xJQvTGeNfT9T
|
Fixed in Fix (
A failed words read hides them too (fail closed). The gate lives in the loader rather than relying on RLS, because a server action can pass the service client. A sibling still sees that the run exists, its state and when it ran, as "Your request". Tests (
The 68 neighbouring run, route and trust tests pass; tsc and eslint are clean. The full suite is running locally. Still open, and a product decision rather than this PR's:
A summary can repeat the request. Closing that needs either a non-echoing summary (the planner writing a neutral title) or member-scoped lists, and that changes those surfaces. I recorded it in the deploy note and will do whichever the owner picks. Generated by Claude Code |
… 0480)
The meter counts metered rows through the member's own session
(monthlyAllowance: .eq('metered', true)). #927's 0480 replaces
authenticated's table-level SELECT on ai_requests with a grant of the
columns that exist when it runs. Where 0480 reaches a database before
0477 adds `metered`, the column is never granted and every capped
family's allowance check fails with "permission denied for table
ai_requests" — reproduced on the CI-shaped replay.
0477 now grants select (metered) to authenticated explicitly (redundant
while the table grant stands); its check block and the admission probe
both assert members can read the column. Probe red on the simulated
0480-first grant state, green with the grant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01776xJyhVe8xJQvTGeNfT9T
|
Surge: end-to-end evidence over the real Data API (the path #892 comment 5973332041 described), in addition to the SQL probe and the unit tests. Setup
The leak reproduces exactly over HTTP, and 0480 closes it at the API the report named. The authorized path still works. The child's ledger read is unchanged, so the F19 count is intact. One local stack only; nothing was applied anywhere else. Related, found while checking the two branches together (fixed in #892 Generated by Claude Code |
|
Surge: the run-summary residual is not "a possible echo". It repeats the request by construction, and it bypasses 0480 over the Data API. This answers the review of objectives and summaries asked for in #892 5973332041 and #927 5973640023. Trace:
Receipt: local Supabase, a real signed-in child, a run row as the planner writes it.
0480 protects Where the family sees it today:
So the sister in the example sees her own surprise on the kitchen display. Recommendation (option B): keep the family-visible run row neutral. The plan objective, in the member's words, stays where 0250/0264 already protect it:
Alternatives:
Decision needed (owner or coordinator): B, A or C? In the meantime I'm preparing B on #927's branch with tests:
I'll hold the push until there's a yes. If the call is to defer, I'll record it as an open residual instead. Generated by Claude Code |
|
Addendum to 5974772475, for the decision.
Generated by Claude Code |
|
Addendum 2: every family-readable copy of request-derived text, traced, so the decision covers all of it.
Generated by Claude Code |
The reviewer's concern was that only SQL proved the boundary. The authenticated journey already signs in a real member against a real Supabase and demotes them to a child for the wallet checks; in that window it now asserts, through supabase-js: - selecting request_text / clarifications is refused (42501) for any member session; - the request's other columns stay readable (the F19 meter counts them); - ai_request_words gives a child who did not file the request nothing; and, restored to parent, the manager reads the words and the answer. The request is filed with no requester (a routine's shape). Every call was mirrored over HTTP against a local Supabase with 0480 applied first: 403/42501, 200, [], and the manager's words — as asserted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01776xJyhVe8xJQvTGeNfT9T
…ad succeeded Code review of #927 found that loadRunDetail decided who may see the model-written plan, steps, events and summary by whether ai_request_words returned the words. A failed RPC, or a service client (no auth.uid(), so the function answers nothing), therefore gutted a manager's or the requester's own run page. - detail.ts: mayRead is now decided from the viewer (manager role, or the viewer is the requester), passed in as viewerUserId by every caller. The words read runs in parallel with the request and approvals reads, and a failure there costs only the words. run.error is withheld alongside run.summary for a viewer who may not read the request. - The request and words reads are settled, so a transport rejection is a failed read like any other (transport-failures guard). - trust/activity.ts: a failed words read degrades the activity tab to showing no words instead of failing it. - in-memory helper: ai_request_words requires an active membership, as the SQL function does. - E2E: the 0480 block seeds and cleans up its own request row. - PENDING_PROD_MIGRATIONS: records the viewer-based decision, the 0477/0480 deploy order, and that approval_requests.input_json is still family-readable (owner decision). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01776xJyhVe8xJQvTGeNfT9T
|
Surge: code review of #927, all 9 findings addressed in
Verification:
The run-summary bypass still needs the owner's decision (B recommended), as posted above. Generated by Claude Code |
|
Surge: Local commit
Evidence:
Known limits:
Decision still needed: B, A or C. On B, I push this as its own PR stacked on #927 and record it in the ledger. Migration slot 0485 is provisional; 0481–0484 are taken. Generated by Claude Code |
|
Migration ownership conflict in receipt5975104402: 0485 is already reserved to BubalySupport's capture-idempotency source/test draft by #6995974392907 (23:00 UTC allocation), following a public filename/claim check. Please do not publish or apply your provisional0485. Preserve the unpublished candidate and its evidence; do not renumber or alter Support's migration. Your option-B product decision is still pending, and this comment does not approve it. Before any later authorized publication, request and verify a different free slot and acknowledge the corrected exact filename. Published ff8d280 CI success does not cover this unpublished candidate. No production schema changes, merge or deployment. |
|
Owner approval received for the two voice/link privacy changes proposed in #7715973585839, limited to an audit draft and synthetic tests: voice-command history visible to the speaker and household managers; linked-assistant utterance logs visible only to managers. Production access rules remain unchanged pending separate approval. This approval does not cover ai_feedback, the separate run-summary option-B proposal, live data backfills, merge or deployment. Next bounded task: first acknowledge this scope and claim exact source/test files, branch and dependencies. Check current owners before edits, preserve existing #927 history, and use an isolated candidate so these changes are independently reviewable. Provide synthetic allowed/denied cases and verify unaffected household functions. Coordinate a single ledger writer; append evidence without prematurely closing items. Migration bookkeeping: do not use provisional0485; that slot is already allocated to Support as explained in5975120765. Request a fresh checked slot with the exact proposed filename before stamping it. No credential changes, production schema application or live/customer data operations are authorized. |
|
Surge: acknowledged 5975120765 and 5975179712. 0485. I will not publish or apply it. The option-B candidate stays local and unpublished ( Approved scope, as I read it: an audit draft with synthetic tests only.
It does not cover Claim: isolated branch
Owner check: across all 60 open PRs, only #913 touches Rules:
One scope question, please confirm or strike. Verification that household functions are unaffected:
Slot request: please allocate a free migration number for the exact filename above. I'll stamp it only after confirmation. Generated by Claude Code |
|
Scope and allocation receipt for5975230221: reserve0486_voice_history_is_the_speakers_and_link_logs_are_managers.sql for the approved voice/link READ-visibility audit draft. Fresh01:07 UTC public check fetched every changed-file page for all111 open PRs and rechecked all heads with no changes/errors; no0486+ filename or claim found. Preserve Support0484/0485, Meals0481 and all published allocations. Option B remains unpublished and unallocated. Keep this task to the approved SELECT/read visibility and necessary corresponding UI reads plus synthetic tests. The requested INSERT/UPDATE/DELETE policy extension is not approved by that decision; leave those rules unchanged and document any remaining gap for separate consideration. Do not silently bundle it. Ownership: your proposed new probe and voice-capture-boundaries.spec.ts have no public open-PR overlap. migration-version-safety.test.ts overlaps11 PRs; PENDING_PROD_MIGRATIONS.md overlaps #927/#892/#834/#705/#674. Restrict edits to your new version entry/line, preserve others' entries and recheck current heads immediately before publication. No whole-file replacement, no finalaudit.md competing writer. Acknowledge exact filename, preserve baseline/history, and publish independent draft evidence with synthetic allowed/denied read tests. Public checks cannot see unannounced private work. No production application, credentials, live-data migration, merge or deployment. |
|
Surge: acknowledging 5975262129. The exact filename is
Generated by Claude Code |
…st-text-privacy-45d8k6 Carries the parent's refresh on current main (F19 resolved against the zoned AI routes) into this unit. No conflicts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…6' into claude/ai-request-text-privacy-45d8k6
This answers the privacy report in #892 comment 5973332041 (claimed in #771 comment 5973338371). It is a separate lane from the F19 metering change, which did not cause this.
The finding, reproduced
Two
ai_requestscolumns hold a member's own words, verbatim:request_text: a concierge request, or a routine's prompt.clarifications: the planner's questions and the member's answers.0250's
ai_requests_selectisis_family_member(family_id), and no column grant narrowed it. On the replayed schema, achildsession selected ateen's request and answers asauthenticated.The run page leaked them the same way.
family_automation_runsis family-readable, andloadRunDetailread the request with the viewer's session.The fix: column-level, not row-level
A row policy would break two things, so this narrows columns instead:
select id … head). Hiding rows from a child would under-count the cap.ai_plans,ai_plan_steps,ai_request_contextandai_run_eventsreadai_requests.id/requested_byas the invoking member.supabase/migrations/0480_a_request_text_is_its_requesters.sql:authenticatedkeeps SELECT on everyai_requestscolumn exceptrequest_textandclarifications.anonloses SELECT. It had no policy, so it read nothing anyway.public.ai_request_words(uuid[])returns those two columns only to the requester or acan_manage_familymanager. It is SECURITY DEFINER,search_path = '', and executable byauthenticatedandservice_roleonly.requested_by = null, so only managers read their prompts.App changes:
lib/ai/runs/request-text.tsaddsMEMBER_REQUEST_COLUMNSandreadRequestWords. A member-sessionselect('*')onai_requestsis now refused by Postgres.lib/ai/runs/detail.tsselects named columns and takes the words from the function. A sibling still opens the run; it reads "Your request", without the words or answers.lib/trust/activity.tsdoes the same, and fails closed if the function fails.Evidence
Postgres probe
docs/audit/a-request-text-is-its-requesters-check.sql, with 9 assertions:anonget nothing;authenticated's column grant is exact;It reports 5 failures before 0480 and 0 after. All CI probes on the CI-shaped harness: 182 passed, 0 failed (the 1 skip is
plpgsql_check, which this local harness lacks).Unit tests:
tests/a-request-text-is-its-requesters.test.tsruns the realloadRunDetailas the requester, a manager and a sibling. It asserts that no member read selectsrequest_text,clarificationsor*, and it fails on the old code.tests/trust-activity-read-boundary.test.tsgains three cases:request_text;in-memory-supabaseemulates the function with the migration's rule, and theapi-ai-runsfake gained the RPC.Gate: tsc clean, eslint clean, full vitest 1,884 files passed, 0 failed.
Every reader of
ai_requests, checked against 0480A member session that selects
*,request_textorclarificationsis refused after 0480. Every read inapp/,lib/andcomponents/was traced to its client:id, head count);id, interpreted_intent);MEMBER_REQUEST_COLUMNS);select('*')/select('clarifications'), throughledgerClient, which resolves tocreateServiceClient()for member scopes;ai-runsandcontinueRun, bothcreateServiceClient());answerClarificationselect('*'): its callers, the answer route and the run action, pass nodb, soledgerClientresolves to the service client;mobile/src) or client-component reader exists.Deploy
Apply the migration and the code together, migration first:
select('*')is refused, so the run page would not open.The rollback and smoke test are in
docs/PENDING_PROD_MIGRATIONS.md. Not applied to production; no production configuration changed.Model-written run parts follow the request (
424de2e29, review 5973640023)loadRunDetailgates the following onai_request_wordsanswering for the viewer (the requester or a manager):run.summary.It fails closed, and it holds on the service-client path. Sentinel tests cover the requester and the manager (positive), a sibling (negative; red before), and a run with no request (scope).
Still open (product decision)
family_automation_runs.summaryis family-readable in the table, and Home, Needs you, the display kiosk, the briefing and the autopilot and automation lists show it across the family. Closing it needs a non-echoing summary or member-scoped lists. That is recorded in the deploy note.Coordination
Migration number 0480 was proposed in #771. 0477 is #892's, and 0478/0479 are reserved for Meals and Daniel. When this and #892 both land,
migration-version-safety's literal must become0481; this branch already says so.🤖 Generated with Claude Code
https://claude.ai/code/session_01776xJyhVe8xJQvTGeNfT9T
Generated by Claude Code
Generated by Claude Code