Skip to content

Chat agent: pin language to user's messages, not data#47

Merged
felixtosh merged 4 commits into
mainfrom
casa-tier2-prep
Jun 24, 2026
Merged

Chat agent: pin language to user's messages, not data#47
felixtosh merged 4 commits into
mainfrom
casa-tier2-prep

Conversation

@felixtosh

Copy link
Copy Markdown
Owner

Stefan reported the chat assistant drifts to German even when he writes in English. Cause: the previous "match the user's language" instruction was too weak; the agent reads German invoice text, partner names, and email subjects in the user's data and drifts to the dominant language in its context window.

This PR strengthens the system-prompt rule to:

  • Explicitly anchor on the USER's messages (latest takes precedence)
  • Call out that transactions/invoices/emails being in another language must NOT cause a language switch
  • Provide an example for mixed-language messages

Single-line change in lib/chat/system-prompt.ts — no API surface changes.

🤖 Generated with Claude Code

felixtosh and others added 4 commits June 24, 2026 00:09
Admin impersonation (the headline feature):
- New `impersonateUser` callable in `functions/src/admin/impersonateUser.ts`:
  admin-only, mints a Firebase custom token signing the admin in as the
  target user, refuses to impersonate other admins (privilege confusion),
  logs the action server-side.
- `/admin/users` UserDetailPanel: "Impersonate (opens new tab)" button.
- New `/impersonate` route: consumes the token from the URL fragment,
  signs in via `signInWithCustomToken`, sets a per-tab marker, redirects
  to `/transactions`. Token never lands in URL/history.
- `ImpersonationBanner` in dashboard layout: persistent amber banner
  showing "Impersonating X · as admin Y", with an Exit button that
  signs out and clears the marker. sessionStorage scoping keeps the
  admin's main tab untouched.

CSP runtime fixes (CASA Tier 2 prod CSP was too tight):
- `frame-src` now allows `blob:` (PDF/file previews fetched as blobs)
  and `https://firebasestorage.googleapis.com` (issued invoice PDFs).
- Dev mode additionally relaxes connect-src/img-src/frame-src for
  `127.0.0.1:*` + `localhost:*` so Firebase emulators are reachable,
  and skips `upgrade-insecure-requests` + the `/__/auth/` rewrite that
  would otherwise route emulator OAuth callbacks to prod.
- PDF.js worker self-hosted via `new URL(..., import.meta.url)` instead
  of loading from `unpkg.com` (CSP would block it; CDN dep removed too).

Auth flow simplification:
- Switched OAuth from `signInWithRedirect` to `signInWithPopup`
  everywhere. Firebase's 2026 guidance: redirect needs a custom
  authDomain or reverse proxy since browsers (Chrome M115+, Firefox
  109+, Safari 16.1+) blocked third-party cookies in cross-site
  iframes. Popup sidesteps that entirely and works with the local
  Auth Emulator. Dropped the dead `getRedirectResult` effect, the
  `pendingRedirect` SSR state, and the `fibuki_oauth_provider`
  sessionStorage dance.
- Wrapped `firebaseUser.getIdTokenResult` in try/catch so a transient
  network failure no longer crashes the dev server with an unhandled
  rejection.
- Emulator host pinned to `127.0.0.1` (was `localhost`) to avoid the
  macOS IPv6 resolution that emulators don't bind.

Settings/identity:
- On save, mirror the top-level tax residence country onto the personal
  entity's address so invoice sender snapshots carry a country.

Test plan:
- `npm run lint` → 0 errors, 40 warnings (unchanged)
- `npx tsc --noEmit` → clean (both root and functions)
- `npm run build` → ✓ Compiled successfully
- `(cd functions && npm test)` → 469/469 pass

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- New `bulkRetryExtraction` callable (admin or self): processes up to 50
  errored files per call in waves of 5 concurrent extractions, returns
  { processed, succeeded, failed, hasMore, sampleErrors }. Mirrors
  `retryFileExtraction`'s reset semantics so partner/transaction
  matching also re-runs.
- UserDetailPanel: "Rescan all errored files" button — handler polls
  the callable up to 10 batches (500 files max) until hasMore is false,
  then surfaces a summary toast.
- Resolves the friend's pain point of having to click the spinner on
  each errored file individually after a Gemini model deprecation.

Test plan:
- `npm run lint` → 0 errors / 40 warnings
- `npx tsc --noEmit` (root + functions) → clean

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…phan route

Background: Google rejected our OAuth verification for the restricted
`gmail.readonly` scope on 2026-06-09 (no response within 90 days). For
unverified apps, Google silently strips restricted scopes from consent
grants for non-test users, leaving users with tokens that 403 on every
Gmail API call.

Our code was treating these stripped-scope tokens as valid:
- `/api/gmail/callback` stored whatever Google returned, no scope check
- Refresh paths in `gmailSyncQueue.ts` and `searchGmailCallable.ts`
  silently accepted refreshed tokens without `gmail.readonly`
- Gmail 403 `insufficientPermissions` did not flip `needsReauth`, so the
  UI showed "Scanning for invoices…" forever instead of a Reconnect CTA

Fixes:
- Callback: parse `tokens.scope`, redirect with `?error=missing_gmail_scope`
  if `gmail.readonly` is absent; no integration is created.
- Refresh paths (both call sites): parse `scope` on the refresh response,
  set `needsReauth: true` + `lastError` and abort if `gmail.readonly` is
  missing, so future calls don't keep using a useless token.
- Queue-processor error handler: detect Gmail 403 with reason
  `insufficientPermissions` (or the equivalent error string) and flip
  `needsReauth: true` so the integration's UI prompts a reconnect.
- Gmail integration card: derive `isStuckInitialSync` when
  `initialSyncComplete === false`, `lastSyncError` is set, and
  `initialSyncStartedAt` is >24h old — treat as needsReauth visually
  (Reconnect Required badge instead of indefinite Syncing spinner).
- Delete `/api/gmail/auth/route.ts` — legacy POST endpoint that stored
  pre-acquired access tokens with an empty refresh_token; nothing in the
  app or extensions called it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User feedback (Stefan): the assistant drifts into German half the time
even when he writes in English. Cause: the previous "match the user's
language" instruction was too weak, and the agent reads German invoice
text, partner names, and email subjects in the database — drifting to
the dominant language in its context window.

Strengthen the rule: explicitly anchor on the USER'S messages (latest
takes precedence), and call out that transactions/invoices/emails being
in another language must NOT cause a language switch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@@ -0,0 +1,57 @@
import admin from 'firebase-admin';
import { execSync } from 'node:child_process';
import { decrypt } from '../lib/utils/encryption.js';
const auth = admin.auth();
const db = admin.firestore();

const ENCRYPTION_KEY = execSync(
@felixtosh felixtosh merged commit 1b03a81 into main Jun 24, 2026
7 checks passed
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