Repository navigation
security: separate Post Chat read authority from shared persistence mutation #1053
Description
Protected main@83eba56149eb802cd63642c507c324c9976ec78e permits a post_read caller to invoke POST /api/posts/{post_id}/chat; on a cache miss that path runs the LLM and then mutates shared post_chat_result / post_chat_citation state. The protected implementation makes the integrity collision explicit: persist_post_chat normalizes the question, executes DELETE FROM post_chat_result WHERE post_id=$1 AND question_norm=$2, reinserts one answer under the same two-part identity, then inserts its citations. The schema makes citations cascade from that same (post_id, question_norm) result identity. No caller scope or evidence-set identity participates in the durable key.
The exact #1042 Strix artifact (34704740554, artifact digest sha256:bcdc13b76adf4a2b5b9cbbcab2de217db2e25f445f69d2de20a7d9f4cdf19b72) correctly flags the integrity/cost boundary, but remediation must preserve product semantics rather than mechanically declaring every Ask action post_admin.
#1044/#1047 is the prerequisite replay-disclosure repair and remains the single owner of generation-scope receipts, captured-source reauthorization, source-deletion invalidation, and fail-closed replay. This issue owns the remaining write-authority/identity invariant only; do not duplicate #1047 authorization SQL or replay policy.
Chosen invariant
Use scope-owned durable answers, not low-privilege mutation of one canonical shared transcript and not admin-only ephemeral compute.
- A
post_readcaller may obtain a live answer and may persist it only under an immutable normalized authorization-scope identity derived from the fix(security): bind persisted chat replay to reader scope #1047 generation receipt. Persistence identity must distinguish at least corporate scope and restricted/unrestricted process scope; it must not infer privilege from the HTTP verb. - Durable answer identity must also bind normalized question and captured evidence-set identity/digest. A low-scope answer therefore cannot DELETE/replace a broader/different scope's persisted answer merely because focal post + normalized question match.
- Replay still passes through fix(security): bind persisted chat replay to reader scope #1047's current-reader/subsumption and captured-source visibility checks. Scope-owned identity prevents integrity collisions; fix(security): bind persisted chat replay to reader scope #1047 remains disclosure authority.
- If product later needs one promoted/canonical answer across scopes, that promotion is a separate explicit elevated capability/domain event. Do not overload ordinary
post_readpersistence with that authority. - Replace delete-then-insert with deterministic insert/upsert/version semantics whose conflict target includes the scope-owned identity. Preserve prior immutable/provenance evidence rather than deleting another scope's result.
- LLM/retrieval/embedding work runs outside every database transaction/explicit lock. After generation, start only a short write transaction, revalidate the fix(security): bind persisted chat replay to reader scope #1047 authorization/evidence receipt, then persist idempotently. Concurrent source/scope change must fail closed without a partially promoted answer.
- Admission/rate/cost control stays with the product admission owner and contextual-orchestrator contract; no provider/model override or local paid fallback.
Realistic RED required before implementation
On the protected/main-compatible prerequisite tree, prove with authenticated HTTP + PostgreSQL that two callers with different valid scopes asking the same normalized question currently target the same (post_id, question_norm) result row and that the later low-privilege request can replace/delete durable result/citation state belonging to the other scope. Assert the concrete row identity and citation cascade, not only HTTP payload differences; the current main implementation already provides the deterministic delete/reinsert mechanism to exercise.
Also cover concurrent same-scope writes: two identical authorized requests must converge idempotently rather than delete/recreate or produce ambiguous winners. A scope/evidence change between generation and persistence must reject the write after short-transaction revalidation.
Integration order / acceptance
Do not open a competing source writer while #1055 is the active Customer Master repair lane. After #1047 is normally integrated (or a verified successor carries all valid receipt/source-evidence deltas), implement this issue as a separate Post Chat persistence lane and non-force converge it onto the then-protected base.
Acceptance requires the realistic RED above, normalized/versioned schema and rollback, application/repository contract, authenticated reader-vs-reader and any elevated-promotion HTTP/E2E, concurrent-write/idempotency tests, no long-lived transaction around LLM work, exact-head PostgreSQL/full suite, Security/SAST/Required CodeQL/Strix/model review, qualifying independent current-head approval, and normal protected integration.
No broad post_admin substitution, replay-policy copy, cross-service SQL, mutable dependency, self-approval, force push, destructive rebase, no-op validation churn, or gate weakening.
Activity