Wire force_unsafe retry-on-403 to chunk edit and upload SPA paths
Background
ca483c5 applied privacy.enforce_write_guard to every user-driven write surface, with each surface accepting an opt-in force_unsafe to bypass after explicit confirmation. The Add Memory flow already had a client-side privacy-warning dialog and was the most visible regression — fixed in #789 by threading forceUnsafe from the dialog into the POST body.
Three other write surfaces accept force_unsafe server-side but the SPA still has no UI for it. Unlike Add Memory these never had a confirm flow at all, so they are new gaps rather than regressions: a user pasting secret-shaped content into a chunk editor or dropping a secret-bearing file onto Index → Upload now gets a 403 surfaced as a generic toast.save_failed / toast.upload_failed with no actionable retry path.
Affected sites
All in packages/memtomem/src/memtomem/web/static/app.js:
| Line |
Caller |
Endpoint |
Server schema |
| 2151 |
d-save-btn (detail-panel chunk save) |
PATCH /api/chunks/{id} |
web/schemas/sources.py:51 |
| 3621 |
inline chunk-card edit save |
PATCH /api/chunks/{id} |
web/schemas/sources.py:51 |
| 3875 |
Index → Upload submit |
POST /api/upload |
query param, web/routes/system.py:1066 |
| 5405 |
Search-tab drag-and-drop upload |
POST /api/upload |
query param, web/routes/system.py:1066 |
(Add Memory in #789 — line 3789 — is not in this issue.)
Contract
Different from Add Memory's pre-scan-and-confirm pattern. These four sites have no client-side regex pre-scan, so the natural shape is detect-then-confirm:
- Issue the write without
force_unsafe.
- If the response is
403 with body {detail: "redaction_blocked", hits: N, surface: "..."} (the RedactionBlockedResponse schema in web/schemas/memory.py:21), surface a confirm dialog naming the hit count and the surface label.
- On confirm, retry with
force_unsafe: true (request body for chunk edits; ?force_unsafe=true query param for upload).
- On cancel, the original 403 stands; surface the existing failure toast unchanged.
Two interface shapes (body field vs. query param) and four call sites — once the helper exists the four sites collapse to one try/catch/retry line each.
Proposal
Add a small helper in app.js — working name apiWithRedactionRetry(method, path, body, opts) — that wraps api(...):
- Catches the 403 response shape from
enforce_write_guard.
- Calls a localized confirm dialog (
compose.privacy_warning_* keys are reusable for the Add path; the chunk/upload paths likely want their own keys for clarity — chunk.privacy_warning_*, upload.privacy_warning_*).
- On confirm, retries with the bypass flag set in the appropriate place (body vs query param).
- On cancel, re-throws the original error so the existing toast paths fire unchanged.
Open question: whether to fold the Add Memory flow (#789) into the same helper afterwards. Add already has client-side pre-scan, so its dialog fires before the request — different timing. Probably keep Add as-is and have the helper only cover the four detect-then-confirm sites.
Test plan
tests-js/:
- One vitest module per call site (or one module with three describe-blocks) pinning:
- clean content → POST/PATCH succeeds with no extra round-trip
- flagged content + confirm → second request carries the bypass flag (body or query, depending on site)
- flagged content + cancel → no retry, original
toast.*_failed fires
Pin the helper itself with a focused unit test that does not depend on the DOM — pass in a stub api and assert the retry/confirm sequencing.
Mutation-validate (feedback_pin_test_mutation_validation.md): drop the retry branch in the helper and confirm the "confirm" cases fail.
Priority
P3. Realistic blast radius is low — chunk content edits rarely contain raw secrets (the edited chunk was already indexed past the same guard at index time), and uploads of secret-bearing files are an unusual workflow. The user-visible symptom is a generic failure toast on what should be a recoverable confirmation, which is annoying but not a data-integrity bug.
References
Wire
force_unsaferetry-on-403 to chunk edit and upload SPA pathsBackground
ca483c5appliedprivacy.enforce_write_guardto every user-driven write surface, with each surface accepting an opt-inforce_unsafeto bypass after explicit confirmation. The Add Memory flow already had a client-side privacy-warning dialog and was the most visible regression — fixed in #789 by threadingforceUnsafefrom the dialog into the POST body.Three other write surfaces accept
force_unsafeserver-side but the SPA still has no UI for it. Unlike Add Memory these never had a confirm flow at all, so they are new gaps rather than regressions: a user pasting secret-shaped content into a chunk editor or dropping a secret-bearing file onto Index → Upload now gets a 403 surfaced as a generictoast.save_failed/toast.upload_failedwith no actionable retry path.Affected sites
All in
packages/memtomem/src/memtomem/web/static/app.js:d-save-btn(detail-panel chunk save)PATCH /api/chunks/{id}web/schemas/sources.py:51PATCH /api/chunks/{id}web/schemas/sources.py:51POST /api/uploadweb/routes/system.py:1066POST /api/uploadweb/routes/system.py:1066(Add Memory in #789 — line 3789 — is not in this issue.)
Contract
Different from Add Memory's pre-scan-and-confirm pattern. These four sites have no client-side regex pre-scan, so the natural shape is detect-then-confirm:
force_unsafe.403with body{detail: "redaction_blocked", hits: N, surface: "..."}(theRedactionBlockedResponseschema inweb/schemas/memory.py:21), surface a confirm dialog naming the hit count and the surface label.force_unsafe: true(request body for chunk edits;?force_unsafe=truequery param for upload).Two interface shapes (body field vs. query param) and four call sites — once the helper exists the four sites collapse to one
try/catch/retryline each.Proposal
Add a small helper in
app.js— working nameapiWithRedactionRetry(method, path, body, opts)— that wrapsapi(...):enforce_write_guard.compose.privacy_warning_*keys are reusable for the Add path; the chunk/upload paths likely want their own keys for clarity —chunk.privacy_warning_*,upload.privacy_warning_*).Open question: whether to fold the Add Memory flow (#789) into the same helper afterwards. Add already has client-side pre-scan, so its dialog fires before the request — different timing. Probably keep Add as-is and have the helper only cover the four detect-then-confirm sites.
Test plan
tests-js/:toast.*_failedfiresPin the helper itself with a focused unit test that does not depend on the DOM — pass in a stub
apiand assert the retry/confirm sequencing.Mutation-validate (
feedback_pin_test_mutation_validation.md): drop the retry branch in the helper and confirm the "confirm" cases fail.Priority
P3. Realistic blast radius is low — chunk content edits rarely contain raw secrets (the edited chunk was already indexed past the same guard at index time), and uploads of secret-bearing files are an unusual workflow. The user-visible symptom is a generic failure toast on what should be a recoverable confirmation, which is annoying but not a data-integrity bug.
References
web/routes/chunks.py:64-80,web/routes/system.py:1063-1158,web/schemas/sources.py:51,web/schemas/memory.py:21-29(RedactionBlockedResponse).feedback_bug_shape_full_tree_grep.md— 4 identical-shape sites, helper preferred over per-site duplication.