Skip to content

Wire force_unsafe retry-on-403 to chunk edit and upload SPA paths #790

@memtomem

Description

@memtomem

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:

  1. Issue the write without force_unsafe.
  2. 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.
  3. On confirm, retry with force_unsafe: true (request body for chunk edits; ?force_unsafe=true query param for upload).
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions