Skip to content

Privacy: a request's own words are its requester's and a manager's (0480) - #927

Closed
NewWorldVenture wants to merge 6 commits into
claude/bubaly-repo-connect-45d8k6from
claude/ai-request-text-privacy-45d8k6
Closed

NewWorldVenture wants to merge 6 commits into
claude/bubaly-repo-connect-45d8k6from
claude/ai-request-text-privacy-45d8k6

Conversation

@NewWorldVenture

@NewWorldVenture NewWorldVenture commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

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_requests columns 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_select is is_family_member(family_id), and no column grant narrowed it. On the replayed schema, a child session selected a teen's request and answers as authenticated.

The run page leaked them the same way. family_automation_runs is family-readable, and loadRunDetail read 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:

  • The F19 meter counts the family's rows through a member's session (select id … head). Hiding rows from a child would under-count the cap.
  • The policies on ai_plans, ai_plan_steps, ai_request_context and ai_run_events read ai_requests.id / requested_by as the invoking member.

supabase/migrations/0480_a_request_text_is_its_requesters.sql:

  • authenticated keeps SELECT on every ai_requests column except request_text and clarifications.
  • anon loses SELECT. It had no policy, so it read nothing anyway.
  • public.ai_request_words(uuid[]) returns those two columns only to the requester or a can_manage_family manager. It is SECURITY DEFINER, search_path = '', and executable by authenticated and service_role only.
  • Routine rows have requested_by = null, so only managers read their prompts.
  • The service role (planner, intake, super-admin activity) is unchanged.

App changes:

  • lib/ai/runs/request-text.ts adds MEMBER_REQUEST_COLUMNS and readRequestWords. A member-session select('*') on ai_requests is now refused by Postgres.
  • lib/ai/runs/detail.ts selects 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.ts does 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:

  • a sibling is refused on the text and on the answers;
  • the ledger and the meter count are intact;
  • the requester and the manager are served both columns;
  • another family's parent and anon get nothing;
  • the requester's plans are still readable;
  • authenticated's column grant is exact;
  • negative control: with the column granted back, the sibling reads it.

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.ts runs the real loadRunDetail as the requester, a manager and a sibling. It asserts that no member read selects request_text, clarifications or *, and it fails on the old code.
  • tests/trust-activity-read-boundary.test.ts gains three cases:
    • it fails closed when the function fails;
    • it never selects request_text;
    • an unanswered request reads without its words.
  • Fakes updated: in-memory-supabase emulates the function with the migration's rule, and the api-ai-runs fake gained the RPC.

Gate: tsc clean, eslint clean, full vitest 1,884 files passed, 0 failed.

Every reader of ai_requests, checked against 0480

A member session that selects *, request_text or clarifications is refused after 0480. Every read in app/, lib/ and components/ was traced to its client:

  • Member session, safe columns only:
    • the F19 meter (id, head count);
    • assistant-turn replay;
    • voice exchange;
    • usage;
    • trust activity (id, interpreted_intent);
    • run detail (MEMBER_REQUEST_COLUMNS);
    • purchases private-result;
    • the executor.
  • Service client (unaffected):
    • planner select('*') / select('clarifications'), through ledgerClient, which resolves to createServiceClient() for member scopes;
    • re-plan port (cron ai-runs and continueRun, both createServiceClient());
    • answerClarification select('*'): its callers, the answer route and the run action, pass no db, so ledgerClient resolves to the service client;
    • the super-admin activity page.
  • No mobile (mobile/src) or client-component reader exists.

Deploy

Apply the migration and the code together, migration first:

  • Code before migration: trust activity fails closed until 0480 is applied.
  • Migration before code: a member-session 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)

loadRunDetail gates the following on ai_request_words answering for the viewer (the requester or a manager):

  • the plan's objective and reasoning;
  • its steps;
  • events;
  • approvals;
  • 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.summary is 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 become 0481; this branch already says so.

🤖 Generated with Claude Code

https://claude.ai/code/session_01776xJyhVe8xJQvTGeNfT9T


Generated by Claude Code


Generated by Claude Code

#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
@vercel

vercel Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
bubaly Ignored Ignored Oct 4, 2026 2:19pm UTC

Request Review

@NewWorldVenture

Copy link
Copy Markdown
Owner Author

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
un.summary) and plan.reasoning_summary verbatim. Synthetic repro: seed �i_requests.request_text = 'SENTINEL-0480-PRIVATE' and copy that sentinel into �i_plans.objective; a sibling's family-scoped run read returns the sentinel at objective even though
equestText is null. The migration note acknowledges these model-written fields can echo the prompt, while the new sibling test uses a null plan and null summary and the SQL probe never exercises the run view. Please redact/sanitize prompt-derived run fields for non-requesters and add a sibling negative control with the sentinel in objective/ summary (plus requester and manager positive controls).

#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

Copy link
Copy Markdown
Owner Author

Fixed in 424de2e29. The finding was correct. I reproduced it with your sentinel: copied into ai_plans.objective, a sibling's run read returned it while requestText was null.

Fix (lib/ai/runs/detail.ts). loadRunDetail now gates everything prompt-derived on whether ai_request_words answered for this viewer, that is, the requester or a manager. That covers:

  • the plan (objective and reasoning_summary);
  • its steps;
  • the run's events;
  • the approvals the plan opened;
  • run.summary, where a clarifying question is also kept.

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 (tests/a-request-text-is-its-requesters.test.ts). The in-memory store applies no RLS, so these exercise the service-client path. The sentinel is placed in the objective, the reasoning, the summary, a step and an event:

  • Positive controls: the requester sees all of it, and a manager sees it.
  • Negative control: a sibling's view and raw detail contain no sentinel, and the plan, steps, events and approvals are empty. This test fails on the previous commit.
  • Scope control: a run with no request behind it is unchanged.

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: family_automation_runs.summary is family-readable in the table. The run lists show it across the family:

  • home/page.tsx:337
  • dashboard/needs-you/page.tsx:59
  • display/page.tsx:78 (the kiosk)
  • api/ai/briefing/route.ts:152
  • components/concierge/autopilot-panel.tsx:78
  • dashboard/family-automation / autonomous-family-management (select('*'))

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

NewWorldVenture pushed a commit that referenced this pull request Oct 3, 2026
… 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

Copy link
Copy Markdown
Owner Author

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

Call, as the child unless noted Before 0480 After 0480
GET /rest/v1/ai_requests?select=request_text,clarifications 200, the parent's text and answer 403 42501 permission denied
GET …?select=* 200 403
GET …?select=id,kind (the ledger the meter counts) 200 200
POST /rest/v1/rpc/ai_request_words n/a 200 []
the same RPC as the parent n/a 200, the text and the answer

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 d0d2ae279). 0480 replaces the table-level SELECT with a grant of the columns that exist when it runs. If 0480 reached a database before #892's 0477 added metered, members could not read that column, and the meter's .eq('metered', true) would fail. Reproduced as "permission denied for table ai_requests". 0477 now grants metered by name, so the two migrations work in either order.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

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:

  • lib/ai/planner/schema.ts:76 defines objective as "What the family asked for, as one sentence in their words".
  • lib/ai/planner/index.ts:368 creates the run with summary: validation.objective.
  • An awaiting-clarification run stores the planner's question as its summary (lib/ai/runs/intake.ts).
  • family_automation_runs_select is is_family_member(family_id).

Receipt: local Supabase, a real signed-in child, a run row as the planner writes it.

  • GET /rest/v1/family_automation_runs?select=summary returns 200 "… Plan a surprise party for my sister".

0480 protects ai_requests.request_text, but the planner copies the request, in the member's words, into a column the whole family reads.

Where the family sees it today:

  • Home "working on" (home/page.tsx:337);
  • Needs you (dashboard/needs-you/page.tsx:59);
  • the display kiosk (display/page.tsx:78);
  • the briefing (api/ai/briefing/route.ts:152);
  • the autopilot panel;
  • the family-automation lists.

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: ai_plans, requester or manager only.

  1. family_automation_runs.summary for a concierge run becomes a neutral label from the classified intent, for example "Planning the week" or "Organising a trip". A clarification run gets "Waiting on an answer". It is never the model's restatement.
  2. The run page keeps showing the objective and question to the requester or a manager (already gated by Privacy: a request's own words are its requester's and a manager's (0480) #927's loadRunDetail).
  3. Existing rows are backfilled to the neutral label in the same migration, because rows already filed hold members' words today.

Alternatives:

  • (A) Withhold summary from members at the column level. That breaks or blanks all the family lists above.
  • (C) Scope those lists to the viewer's own runs. That changes what "the family's assistant is working on" means.

Decision needed (owner or coordinator): B, A or C? In the meantime I'm preparing B on #927's branch with tests:

  • a sentinel in the objective never reaches family_automation_runs.summary;
  • the requester and a manager still see it on the run page;
  • the backfill migration has a probe.

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

Copy link
Copy Markdown
Owner Author

Addendum to 5974772475, for the decision.

  • A third copy exists: routine runs store the routine's prompt as their summary (app/api/cron/family-routines/route.ts:251). 0480 made that prompt manager-only on ai_requests, and this copies it to every member.

  • What option B costs in i18n: the summary is currently the model's sentence in the family's language. A neutral label stored as English text would regress six translated surfaces. Done properly, B stores a label key (intent:plan_week, run:awaiting_answer, routine:<rule name>) and translates it where it is rendered: Home, Needs you, the display, the briefing, autopilot, and family automation. The pieces are:

    • a small runSummaryLabel(summary, t) helper;
    • the 22 intent labels across the 7 catalogues;
    • the three createRun call sites;
    • a migration that backfills existing concierge, clarification and routine rows to keys;
    • the probe and tests.

    It is a contained change, but it touches every listing surface, so I'm asking before pushing it.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Addendum 2: every family-readable copy of request-derived text, traced, so the decision covers all of it.

Copy Who can read it Assessment
family_automation_runs.summary: the objective "in their words", the clarification question, or the routine prompt the whole family The bypass. The decision is pending (B / A / C above).
approval_requests.title, .summary and payload.input (the step's full input_json), filed by the executor for a step that needs approval (lib/ai/runs/executor.ts:1274) the whole family (approval_requests_select is is_family_member) Copies a step that 0264 limits to the requester or a manager. It previews a household action that becomes visible to the family once it is approved, so the marginal leak is smaller. Choosing B could also limit non-manager reads to the approval's status.
agent_activity.title: tool summaries of executed changes the whole family Executed outcomes, family-visible by design ("the family sees every change Bubaly made"). This is not a copy of the request.
notifications from AI: a plan's own notify step the recipient, or the family when user_id is null Intended by the plan.
ai_plans, ai_plan_steps, ai_run_events, ai_request_context the requester or a manager Already protected (0250 / 0264).

Generated by Claude Code

claude added 2 commits October 3, 2026 23:59
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

Copy link
Copy Markdown
Owner Author

Surge: code review of #927, all 9 findings addressed in ff8d28046

# Finding Outcome
1 A failed ai_request_words call hid the plan, steps, events and approvals from a manager Fixed. mayRead is now decided from the viewer: a manager, or the viewer is requested_by. Every caller passes viewerUserId. A failed words read costs only the words (test a failed words read costs only the words).
2 With the service client (no auth.uid()), the RPC returns nothing, which gutted the run page Fixed by the same change (test a client with no signed-in user…).
3 A failed words read failed the whole trust activity tab Fixed. It degrades, logs, and shows no words. It still fails closed on the ai_requests read itself.
4 approval_requests.input_json is family-readable and can carry plan text Owner decision. Recorded in PENDING_PROD_MIGRATIONS.md, next to the run-summary B/A/C decision. The run page itself withholds approvals from a viewer who may not read the request.
5 run.error could quote the request Fixed. It is withheld along with run.summary.
6 The E2E relied on a pre-existing row Fixed. The 0480 block seeds its own request row through the service client and deletes it afterwards.
7 0480 before 0477 hides metered Documented in the deploy order. The explicit grant in #892 (d0d2ae279) makes either order safe.
8 The in-memory ai_request_words emulation was looser than the SQL Fixed. It now requires an active membership and requester-or-manager, matching the function.
9 The words read was serial Fixed. It runs in parallel with the request and approvals reads. Both reads are settled, so a transport rejection degrades like a query error (the transport-failures guard caught the first version).

Verification:

  • a-request-text-is-its-requesters: 11 of 11 pass, and 4 of them fail on the previous detail.ts.
  • trust activity: 20 of 20 pass.
  • tsc and eslint are clean.
  • Full suite: the only failures were 3 test expectations caused by the new viewerUserId argument and the settle guard. All three are fixed and pass, along with every test that touches these modules.

The run-summary bypass still needs the owner's decision (B recommended), as posted above.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Surge: ff8d28046 is green on every hosted job (Web, E2E, Database, Mobile, finance). Option B for the run-summary bypass is built and verified locally, not pushed, until there's a decision.

Local commit 64289ad59 on claude/run-summary-labels-45d8k6, based on ff8d28046:

  • Labels, not words: a request's run stores label:intent.<intent> / label:awaitingAnswer / label:continued / label:routine. The label is translated where it is shown (runLabel.*, 25 keys × 7 catalogues). This covers the planner, all intake paths and the routine cron.
  • What stays private: the words stay on the plan (0264) and the request (0480). The run page and the requester's replay still show them.
  • 0485: a BEFORE trigger rewrites any write that still carries words to its label, so the code and the migration can deploy in either order. It covers request runs plus the concierge/routine types, because request_id is ON DELETE SET NULL. It backfills existing rows through the same trigger and fails if any are left.

Evidence:

  • Data API, signed-in child: before 0485, 200 with the sentinel. After 0485, 200 [], even for a fresh service-role write that carries the words.
  • Probe: 10 assertions with a negative control. It fails before 0485 and with a pass-through trigger (7 failures), and passes after. All 183 harness probes pass; the one skip is the expected plpgsql_check.
  • Unit tests: the full suite passes (1,885 files). The real-planner end-to-end assertion fails when the old summary: validation.objective line is restored.

Known limits:

  • The lib builders (Needs you, Working on, Completed, history) render the English label. Their surrounding text is already English-only.
  • family_automation_runs.error and approval_requests stay family-readable. They are a separate decision.

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

Copy link
Copy Markdown
Owner Author

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.

Copy link
Copy Markdown
Owner Author

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.

Copy link
Copy Markdown
Owner Author

Surge: acknowledged 5975120765 and 5975179712.

0485. I will not publish or apply it. The option-B candidate stays local and unpublished (64289ad59, 38b3e1564), with its evidence. Before any authorized publication I'll request a fresh slot and confirm the exact filename. I won't touch Support's migration.

Approved scope, as I read it: an audit draft with synthetic tests only.

  • (1) Voice-command history is visible to the speaker and household managers.
  • (2) Linked-assistant utterance logs are visible only to managers.

It does not cover ai_feedback, option B, live backfills, production application, merge or deploy.

Claim: isolated branch claude/voice-link-privacy-45d8k6, based on #788 (0edd434df) and independent of #927. Files:

  • supabase/migrations/<SLOT>_voice_history_is_the_speakers_and_link_logs_are_managers.sql
  • docs/audit/voice-history-and-link-log-privacy-check.sql: synthetic allowed and denied cases, plus a negative control
  • tests/e2e/voice-capture-boundaries.spec.ts: Data API cases for child, speaker and manager
  • tests/migration-version-safety.test.ts: the next-version literal only
  • docs/PENDING_PROD_MIGRATIONS.md: one new section

Owner check: across all 60 open PRs, only #913 touches lib/assistant/service.ts (calendar). I don't edit it. The insert path stays service-role. No open PR touches voice-module.tsx, lib/voice/ or voice-capture-boundaries.spec.ts. No app code changes are planned. The voice module's delete already reports a refused row, and the only reader of assistant_link_events is the Data API.

Rules:

  • voice_commands SELECT: can_manage_family(family_id) or the speaker (created_by = auth.uid() or is_self_member(member_id)).
  • assistant_link_events SELECT: can_manage_family(family_id).

One scope question, please confirm or strike. voice_commands UPDATE and DELETE are family-wide today, so any member can edit or delete another member's spoken history. INSERT accepts any member_id and created_by, so a member can file history as someone else. I propose the same speaker-or-manager rule for UPDATE and DELETE, and an INSERT check that the row is your own. It will be a separate commit, dropped if not approved.

Verification that household functions are unaffected:

  • a speaker records, lists and deletes their own commands;
  • a manager lists everyone's;
  • the link-event service insert still lands;
  • the full test suite and every harness probe pass.

Slot request: please allocate a free migration number for the exact filename above. I'll stamp it only after confirmation.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

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.

Copy link
Copy Markdown
Owner Author

Surge: acknowledging 5975262129. The exact filename is 0486_voice_history_is_the_speakers_and_link_logs_are_managers.sql. It is published as the independent draft #941 (based on #788 0edd434df, rechecked immediately before the push).

  • Read visibility only. voice_commands INSERT, UPDATE and DELETE are unchanged. The gap is documented in the new 0486 section of PENDING_PROD_MIGRATIONS.md, and the drafted write rule stays unpublished.
  • Shared files. In migration-version-safety.test.ts I changed only the next-version line and its comment (0487). PENDING_PROD_MIGRATIONS.md gets one appended section. No other entry is touched, and I didn't write to finalaudit.md.
  • Correction to my claim. I did not edit voice-capture-boundaries.spec.ts. It is a component harness with a mocked database, so the synthetic allowed and denied read cases are in the new probe, which runs in CI's Database job.
  • Evidence:
    • the probe fails before the migration (the four leaks) and passes after;
    • a fresh replay passes all 182 probes, plus the expected plpgsql_check skip;
    • a local Data API child read of a sibling's command or the link log went from 200 with the text to 200 [], while parent reads, own filing and the service write are unchanged;
    • the full suite passes (1,883 files).
  • Unchanged: Support's 0484/0485, Meals' 0481 and every published allocation. Option B stays unpublished.

Generated by Claude Code

claude added 2 commits October 4, 2026 13:49
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants