Skip to content

fix(web): wire force_unsafe to Add Memory privacy-confirm dialog#789

Merged
memtomem merged 1 commit intomainfrom
fix/web-add-force-unsafe-on-confirm
May 5, 2026
Merged

fix(web): wire force_unsafe to Add Memory privacy-confirm dialog#789
memtomem merged 1 commit intomainfrom
fix/web-add-force-unsafe-on-confirm

Conversation

@memtomem
Copy link
Copy Markdown
Owner

@memtomem memtomem commented May 5, 2026

Summary

Fix a regression introduced by ca483c5. When the trust-boundary privacy.enforce_write_guard was wired to POST /api/add with force_unsafe: bool = False defaulting to block, the SPA's existing privacy-warning flow in app.js was not updated to thread the bypass flag through the resulting submit.

Result: confirmed Add Memory submissions of secret-shaped content (or false positives) now get a server-side 403 instead of being saved. The confirm dialog became a no-op trap.

This PR threads forceUnsafe = true from a confirmed dialog into the POST body so the user's "I know, store it anyway" choice actually reaches the bypass branch the server already supports.

What changed

web/static/app.js — Add Memory submit handler:

+  let forceUnsafe = false;
   const patterns = STATE.privacyPatterns;
   if (patterns && patterns.some(re => re.test(content))) {
     const ok = await showConfirm({...});
     if (!ok) return;
+    forceUnsafe = true;
   }
   ...
-    const data = await api('POST', '/api/add', { content, title, tags, file, namespace });
+    const body = { content, title, tags, file, namespace };
+    if (forceUnsafe) body.force_unsafe = true;
+    const data = await api('POST', '/api/add', body);

web/static/index.html?v=99?v=100 per feedback_static_asset_cache_bust.md.

tests-js/add-memory-force-unsafe.test.mjs — three pinned branches:

  1. clean content → POST without force_unsafe
  2. flagged content + confirm → POST with force_unsafe: true
  3. flagged content + cancel → no POST

Mutation-validated per feedback_pin_test_mutation_validation.md: reverting body.force_unsafe = true makes case (2) fail with expected undefined to be true, so the regression cannot silently re-land.

Out of scope (follow-up)

PATCH /api/chunks/{id} and POST /api/upload accept force_unsafe server-side but the SPA has no UI for it (lines 2151 / 3621 / 3875 / 5405 in app.js). Those are new gaps, not regressions — they never had a confirm flow — and need a different pattern (detect 403 redaction_blocked, show retry-confirm dialog) plus likely a shared helper. Will track in a separate issue so this PR stays a single regression fix per feedback_one_change_per_pr.md.

Test plan

  • npx vitest run — 14/14 pass (5 test files)
  • Mutation-validated: revert prod fix → case (2) fails as expected → restore → all green
  • Static-asset cache-bust bumped (app.js v99 → v100)
  • CI green (lint / js-unit / typecheck / 3-OS python tests / Playwright / golden-path)
  • Manual smoke (after merge): paste a fake sk- token into Add Memory → confirm dialog appears → confirm → save succeeds → row appears in results

References

🤖 Generated with Claude Code

When ``ca483c5`` applied the trust-boundary ``privacy.enforce_write_guard``
to ``POST /api/add`` with ``force_unsafe: bool = False`` defaulting to
block, the SPA's existing privacy-warning flow in ``app.js`` was not
updated to thread the bypass flag through the resulting submit. Effect:
when the user pasted secret-shaped content (or tripped a false positive),
the SPA showed the confirm dialog as before, but the confirmed POST went
to the server *without* ``force_unsafe`` — and the server matched the
same patterns the client just surfaced and returned 403. The dialog
became a no-op trap.

Wire ``forceUnsafe = true`` from a confirmed dialog through to the
request body so the SPA's "I know, store it anyway" path actually
reaches the bypass branch the server expects:

  let forceUnsafe = false;
  if (patterns.some(...)) {
      if (!await showConfirm(...)) return;
      forceUnsafe = true;
  }
  ...
  const body = { content, ..., namespace };
  if (forceUnsafe) body.force_unsafe = true;
  await api('POST', '/api/add', body);

Clean content still posts without the field (server default ``False``
keeps blocking semantics intact). Cancel still aborts. The new comment
block records *why* the wiring exists so a future cleanup pass does not
silently drop it.

Pin three branches in ``tests-js/add-memory-force-unsafe.test.mjs``:

1. clean content → POST without ``force_unsafe``
2. flagged content + confirm → POST with ``force_unsafe: true``
3. flagged content + cancel → no POST

Mutation-validated per ``feedback_pin_test_mutation_validation.md``:
reverting the ``body.force_unsafe = true`` line makes case (2) fail with
``expected undefined to be true`` so the regression cannot land silently.

Bump ``app.js?v=99`` → ``?v=100`` per ``feedback_static_asset_cache_bust.md``;
disk-cached SPAs would otherwise miss the wiring on first load.

Out of scope (separate follow-up):

- ``PATCH /api/chunks/{id}`` (lines 2151, 3621) and ``POST /api/upload``
  (lines 3875, 5405) accept ``force_unsafe`` server-side but the SPA
  has no UI for it. Those are *new gaps* — never had a confirm flow —
  so they need a different pattern (detect 403 ``redaction_blocked``,
  show retry-confirm dialog) and warrant a shared helper. Tracked
  separately so this PR stays a single regression fix.

Co-Authored-By: Claude <[email protected]>
@memtomem memtomem merged commit 75a9f4d into main May 5, 2026
11 checks passed
@memtomem memtomem deleted the fix/web-add-force-unsafe-on-confirm branch May 5, 2026 00:50
@github-actions github-actions Bot locked and limited conversation to collaborators May 5, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants