Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions app/admin/memory/proposals/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { AppShell } from "@/components/layout/app-shell";
import { PageHeader } from "@/components/ui/page-header";
import { SectionCard } from "@/components/ui/section-card";
import { resolvePandoraRuntimeSafetyConfig } from "@/lib/config/pandora-runtime-safety-config";
import { resolvePandoraServerSession } from "@/lib/auth/pandora-server-session-resolver";
import { createSupabaseServerClient } from "@/lib/supabase/server";
import { getMemoryProposal } from "@/lib/services/memory-proposal-service";
import { approveProposalAction, persistProposalAction, rejectProposalAction, reviseProposalAction } from "../actions";
export const dynamic="force-dynamic";
type Props={params:Promise<{id:string}>};
export default async function Page({params}:Props){const {id}=await params; const runtime=resolvePandoraRuntimeSafetyConfig(); const session=await resolvePandoraServerSession(); const supabase=await createSupabaseServerClient() as never; const result=await getMemoryProposal(supabase,id,session,runtime); const p=result.ok?result.data:null; const gate=runtime.config.approvedReviewPersistenceEnabled; return <AppShell><PageHeader eyebrow="Internal admin" title="Memory proposal detail" description="Proposal review is admin-only, namespace-scoped, POST/server-action based, audited, and gated. Public writes remain disabled."/><SectionCard title="Safety copy" description="This does not write active memory until approved."><p>Approval is audited.</p><p>Public writes remain disabled.</p><p>Model calls, embeddings, retrieval, GPT Actions, and MCP remain disabled.</p><p>Reviewed write gate enabled: {String(gate)}</p></SectionCard>{!p?<SectionCard title="Unavailable" description={result.ok?"No proposal found.":result.error}><p>No proposal rows are exposed.</p></SectionCard>:<><SectionCard title={p.title??p.id} description={`Status: ${p.status}`}><dl><dt>Proposal id</dt><dd>{p.id}</dd><dt>Namespace</dt><dd>{p.namespace}</dd><dt>Memory text</dt><dd>{p.memory_text}</dd><dt>Source</dt><dd>{p.source_type} {p.source_ref}</dd><dt>Proposed by</dt><dd>{p.proposed_by}</dd><dt>Reviewed by</dt><dd>{p.reviewed_by??"—"}</dd><dt>Reviewed at</dt><dd>{p.reviewed_at??"—"}</dd><dt>Persisted memory id</dt><dd>{p.persisted_memory_id??"—"}</dd></dl></SectionCard><SectionCard title="Review actions" description="All actions are blocked while the reviewed persistence gate is disabled."><form action={approveProposalAction}><input type="hidden" name="id" value={p.id}/><button disabled={!gate||p.status!=="pending"}>Approve</button></form><form action={rejectProposalAction}><input type="hidden" name="id" value={p.id}/><input name="reason" placeholder="Rejection reason"/><button disabled={!gate||p.status!=="pending"}>Reject</button></form><form action={reviseProposalAction}><input type="hidden" name="id" value={p.id}/><input name="note" placeholder="Revision note"/><button disabled={!gate||p.status!=="pending"}>Request revision</button></form><form action={persistProposalAction}><input type="hidden" name="id" value={p.id}/><button disabled={!gate||p.status!=="approved"}>Persist approved proposal</button></form></SectionCard><SectionCard title="Audit timeline" description="Audit events are stored in audit_logs for proposal_created, proposal_approved, proposal_rejected, proposal_needs_revision, and proposal_persisted."><p>Use /admin/memory/audit with namespace {p.namespace} to inspect append-only audit proof.</p></SectionCard></>}</AppShell>}
25 changes: 25 additions & 0 deletions app/admin/memory/proposals/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use server";

import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { resolvePandoraRuntimeSafetyConfig } from "@/lib/config/pandora-runtime-safety-config";
import { resolvePandoraServerSession } from "@/lib/auth/pandora-server-session-resolver";
import { createSupabaseServerClient } from "@/lib/supabase/server";
import { approveMemoryProposal, createMemoryProposal, persistApprovedMemoryProposal, rejectMemoryProposal, requestMemoryProposalRevision } from "@/lib/services/memory-proposal-service";

function text(formData: FormData, key: string) { const value = formData.get(key); return typeof value === "string" && value.trim() ? value.trim() : undefined; }
function num(formData: FormData, key: string) { const value = text(formData, key); return value ? Number(value) : undefined; }
async function deps() { return { supabase: await createSupabaseServerClient() as never, session: await resolvePandoraServerSession(), runtime: resolvePandoraRuntimeSafetyConfig() }; }
function fail(message: string): never { throw new Error(message); }

export async function createProposalAction(formData: FormData) {
const { supabase, session, runtime } = await deps();
const result = await createMemoryProposal(supabase, { namespace: (text(formData, "namespace") ?? "real_life") as "real_life" | "au", title: text(formData, "title"), memory_text: text(formData, "memory_text") ?? "", memory_type: text(formData, "memory_type"), source_type: text(formData, "source_type"), source_ref: text(formData, "source_ref"), confidence: num(formData, "confidence") }, session, runtime);
if (!result.ok) fail(result.error);
revalidatePath("/admin/memory/proposals");
redirect(`/admin/memory/proposals/${result.data.id}`);
}
export async function approveProposalAction(formData: FormData) { const { supabase, session, runtime } = await deps(); const id = text(formData, "id") ?? fail("id required"); const r = await approveMemoryProposal(supabase, id, "server-action", session, runtime); if (!r.ok) fail(r.error); revalidatePath(`/admin/memory/proposals/${id}`); }
export async function rejectProposalAction(formData: FormData) { const { supabase, session, runtime } = await deps(); const id = text(formData, "id") ?? fail("id required"); const r = await rejectMemoryProposal(supabase, id, text(formData, "reason") ?? "Rejected by operator.", "server-action", session, runtime); if (!r.ok) fail(r.error); revalidatePath(`/admin/memory/proposals/${id}`); }
export async function reviseProposalAction(formData: FormData) { const { supabase, session, runtime } = await deps(); const id = text(formData, "id") ?? fail("id required"); const r = await requestMemoryProposalRevision(supabase, id, text(formData, "note") ?? "Revision requested by operator.", "server-action", session, runtime); if (!r.ok) fail(r.error); revalidatePath(`/admin/memory/proposals/${id}`); }
export async function persistProposalAction(formData: FormData) { const { supabase, session, runtime } = await deps(); const id = text(formData, "id") ?? fail("id required"); const r = await persistApprovedMemoryProposal(supabase, id, "server-action", session, runtime); if (!r.ok) fail(r.error); revalidatePath(`/admin/memory/proposals/${id}`); revalidatePath("/admin/memory/browser"); }
2 changes: 2 additions & 0 deletions app/admin/memory/proposals/new/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { AppShell } from "@/components/layout/app-shell";import { PageHeader } from "@/components/ui/page-header";import { SectionCard } from "@/components/ui/section-card";import { resolvePandoraRuntimeSafetyConfig } from "@/lib/config/pandora-runtime-safety-config";import { createProposalAction } from "../actions";
export const dynamic="force-dynamic";export default function Page(){const runtime=resolvePandoraRuntimeSafetyConfig();return <AppShell><PageHeader eyebrow="Internal admin" title="New memory proposal" description="This does not write active memory until approved. Approval is audited. Public writes remain disabled. Model calls, embeddings, retrieval, GPT Actions, and MCP remain disabled."/><SectionCard title="Proposal form" description={runtime.config.approvedReviewPersistenceEnabled?"Reviewed write gate is enabled.":"Reviewed write gate is disabled; creation is blocked."}><form action={createProposalAction} className="memory-browser-filter-form"><label>Namespace<select name="namespace" defaultValue="real_life"><option value="real_life">real_life</option><option value="au">au</option></select></label><label>Title<input name="title"/></label><label>Memory text<textarea name="memory_text" required/></label><label>Memory type<input name="memory_type" placeholder="observation"/></label><label>Source type<input name="source_type" placeholder="manual_admin_entry"/></label><label>Source ref<input name="source_ref"/></label><label>Confidence<input name="confidence" type="number" min="0" max="1" step="0.0001"/></label><button disabled={!runtime.config.approvedReviewPersistenceEnabled}>Create pending proposal</button></form></SectionCard></AppShell>}
11 changes: 11 additions & 0 deletions app/admin/memory/proposals/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Link from "next/link";
import { AppShell } from "@/components/layout/app-shell";
import { PageHeader } from "@/components/ui/page-header";
import { SectionCard } from "@/components/ui/section-card";
import { resolvePandoraServerSession } from "@/lib/auth/pandora-server-session-resolver";
import { resolvePandoraRuntimeSafetyConfig } from "@/lib/config/pandora-runtime-safety-config";
import { createSupabaseServerClient } from "@/lib/supabase/server";
import { listMemoryProposals } from "@/lib/services/memory-proposal-service";
export const dynamic = "force-dynamic";
type Props = { searchParams?: Promise<Record<string,string|undefined>> };
export default async function Page({ searchParams }: Props) { const p = await searchParams; const namespace = p?.namespace ?? "real_life"; const status = p?.status; const session = await resolvePandoraServerSession(); const runtime = resolvePandoraRuntimeSafetyConfig(); const supabase = await createSupabaseServerClient() as never; const proposals = await listMemoryProposals(supabase, namespace, status, session, runtime); return <AppShell><PageHeader eyebrow="Internal admin" title="Memory proposals" description="Controlled proposal → review → approve → persist → audit workflow. Public writes remain disabled. Model calls, embeddings, retrieval, GPT Actions, and MCP remain disabled."/><SectionCard title="Phase 4A gate" description="This does not write active memory until approved. Approval is audited."><p>Reviewed persistence gate enabled: {String(runtime.config.approvedReviewPersistenceEnabled)}</p><p>Public writes remain disabled: {String(!runtime.config.publicMemoryPersistenceEnabled)}</p><Link className="button-link button-link--primary" href="/admin/memory/proposals/new">New proposal</Link></SectionCard><SectionCard title="Filters" description="Namespace-scoped proposal list."><form><select name="namespace" defaultValue={namespace}><option value="real_life">real_life</option><option value="au">au</option></select><select name="status" defaultValue={status ?? ""}><option value="">all</option>{["pending","approved","rejected","needs_revision","persisted","disabled"].map(s=><option key={s} value={s}>{s}</option>)}</select><button>Apply</button></form>{!proposals.ok ? <p>{proposals.error}</p> : <ul>{proposals.data.map(x => <li key={x.id}><Link href={`/admin/memory/proposals/${x.id}`}>{x.title ?? x.id}</Link> — {x.status} — {x.namespace}</li>)}</ul>}</SectionCard></AppShell> }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== app/admin layouts with auth/redirect logic =="
fd -a 'layout.(ts|tsx)' app/admin | xargs -r rg -n -C3 'resolvePandoraServerSession|redirect\(|forbidden\(|unauthorized\(|notFound\('

echo "== middleware touching /admin routes =="
fd -a 'middleware.(ts|tsx|js|mjs)' . | xargs -r rg -n -C3 '/admin|redirect\(|forbidden\(|unauthorized\('

echo "== local page body =="
sed -n '1,160p' app/admin/memory/proposals/page.tsx

Repository: besfeng23/Memory

Length of output: 247


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Checking directory structure..."
ls -la app/admin/ 2>/dev/null || echo "app/admin not found or empty"
echo "Checking for layout files..."
ls -la app/admin/layout.* 2>/dev/null || echo "No layout files in app/admin"
echo "Checking for middleware..."
ls -la middleware.* 2>/dev/null || echo "No middleware file in root"

Repository: besfeng23/Memory

Length of output: 501


🏁 Script executed:

cat middleware.ts

Repository: besfeng23/Memory

Length of output: 1520


Missing authentication guard exposes admin UI to unauthenticated users.

middleware.ts calls await supabase.auth.getUser() but unconditionally returns NextResponse.next() without checking the result or redirecting unauthorized users. Additionally, app/admin lacks a layout.tsx guard. Consequently, the Page component renders the admin shell (headers, CTAs, filters) before verifying session.ok.

Wrap the return value in if (!session.ok) return <ForbiddenOrRedirect />; or add a layout guard that redirects if session.error is present.

// app/admin/memory/proposals/page.tsx
if (!session.ok) {
  return <NotAuthorizedOrRedirect />; // or redirect('/login')
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/admin/memory/proposals/page.tsx` at line 11, The admin memory proposals
page renders the full admin shell before verifying authentication, so add an
authorization guard in Page after resolvePandoraServerSession() and before any
UI is returned. Use the session result from resolvePandoraServerSession() to
short-circuit unauthenticated users with a redirect or forbidden/unauthorized
component, or move the check into an app/admin layout guard so Page only runs
for authorized sessions. Keep the guard tied to the existing Page and
session.ok/session.error handling so the admin UI, filters, and proposal list
are never exposed to unauthenticated users.

19 changes: 19 additions & 0 deletions app/admin/memory/verification/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,25 @@ export default async function AdminMemoryVerificationPage() {
<Badge line={dto.recommendation} />
</ul>
</SectionCard>

<SectionCard
title="Phase 4A Controlled Persistence Readiness"
description="Read-only foundation remains closed. Controlled persistence is disabled until reviewed write gate is enabled. Public writes remain forbidden."
>
<ul>
<li><strong>proposal schema available:</strong> available via memory_proposals migration</li>
<li><strong>proposal RLS available:</strong> authenticated user/namespace scoped, no anon policy</li>
<li><strong>proposal route guard status:</strong> admin proposal routes require Supabase session and server-derived identity</li>
<li><strong>reviewed persistence gate status:</strong> {runtime.config.approvedReviewPersistenceEnabled ? "enabled" : "disabled safe default"}</li>
<li><strong>mutation endpoint status:</strong> POST/server-action only; blocked when gate is disabled</li>
<li><strong>audit write proof status:</strong> proposal lifecycle writes audit_logs events</li>
<li><strong>public write status:</strong> disabled</li>
<li><strong>direct write status:</strong> disabled; browser remains read-only</li>
<li><strong>proposal workflow status:</strong> {runtime.config.approvedReviewPersistenceEnabled ? "enabled" : "ready but disabled"}</li>
<li><strong>Phase 3F read-only closure:</strong> closed</li>
<li><strong>Phase 4A write feature:</strong> {runtime.config.approvedReviewPersistenceEnabled ? "enabled" : "disabled"}</li>
</ul>
</SectionCard>
<SectionCard
title="Runtime gate matrix"
description="Every Pandora runtime gate, expected closure state, and closure impact."
Expand Down
57 changes: 57 additions & 0 deletions docs/phase-4a-controlled-memory-persistence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Phase 4A: Controlled operator-approved memory persistence

Phase 4A exists to make Pandora Memory able to create memory only through a reviewed operator workflow. Phase 3F closed the read-only foundation: authenticated admin readback, admin browser, audit route, public-read blocking, and dangerous runtime gates disabled.

## What Phase 4A adds

- Pending `memory_proposals` records.
- Operator review actions: approve, reject, request revision, persist approved proposal.
- Append-only writes to active memory only after approval.
- Audit events for proposal lifecycle actions.
- Admin UI under `/admin/memory/proposals`.

## Proposal lifecycle

1. Authenticated operator creates a pending proposal.
2. Proposal is not active memory.
3. Authenticated operator approves, rejects, or requests revision.
4. Only an approved proposal can be persisted.
5. Persistence links the proposal to a memory item and writes source, patch, and audit proof.

## Env gates

`PANDORA_ENABLE_APPROVED_REVIEW_MEMORY_PERSISTENCE=true` is required for all mutations. The default is disabled. Public reads and public writes remain disabled.

## RLS expectations

`memory_proposals` is RLS-enabled. Authenticated users can access only their own namespace-scoped proposal rows. App routes must use server-derived Supabase Auth identity and must not use service-role clients.

## Audit requirements

Lifecycle events include `proposal_created`, `proposal_approved`, `proposal_rejected`, `proposal_needs_revision`, `proposal_persisted`, and `proposal_disabled` if disabling is used later.

## Manual verification checklist

- Confirm `/admin/memory/verification` says read-only foundation remains closed.
- Confirm Phase 4A is disabled when the reviewed write gate is absent.
- Enable the reviewed write gate in a reviewed environment.
- Create a pending proposal as an authenticated operator.
- Approve it.
- Persist it.
- Confirm the row appears in `/admin/memory/browser?namespace=real_life`.
- Confirm audit events exist.
- Confirm `/memory/browser` redirects and never renders public rows.

## Rollback/no-close rules

Do not close Phase 4A if proposal mutation is public, unauthenticated, unaudited, not namespace-scoped, or bypasses RLS. Roll back by disabling `PANDORA_ENABLE_APPROVED_REVIEW_MEMORY_PERSISTENCE`.

## Must remain disabled

- Public reads
- Public writes
- Model calls
- Embeddings
- Semantic retrieval
- GPT Actions
- MCP
14 changes: 14 additions & 0 deletions docs/templates/memory-proposal-approval-proof.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Memory proposal approval proof

- Proposal id:
- Namespace:
- Proposed by:
- Reviewed by:
- Reviewed at:
- Status:
- Persisted memory id:
- Audit event ids:
- Source proof:
- Final decision:
- Blockers:
- Reviewer sign-off:
2 changes: 1 addition & 1 deletion lib/auth/pandora-server-session-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export async function resolvePandoraServerSession(input: { request?: NextRequest
if (input.testSession) return { ok: true, session: { ...input.testSession, serverDerivedOnly: true, clientUserIdAccepted: false, serviceRoleUsed: false }, blockers: [] };
const user = await getCurrentUser();
if (!user?.id) return { ok: false, session: null, blockers: [{ code: "auth_required", message: "Authenticated server session is required." }, { code: "session_unavailable", message: "No real auth provider session was resolved; no production user is faked." }] };
const session: PandoraServerSession = { userId: user.id, email: user.email, displayName: user.user_metadata?.name ?? user.user_metadata?.full_name, authProvider: user.app_metadata?.provider, authenticated: true, allowedNamespaces: ["real_life", "au"], adminCapabilities: [], isInternalOperator: false, isPersistenceOperator: false, sessionSource: "supabase", serverDerivedOnly: true, clientUserIdAccepted: false, publicReadAllowed: false, publicPersistenceAllowed: false, serviceRoleUsed: false };
const session: PandoraServerSession = { userId: user.id, email: user.email, displayName: user.user_metadata?.name ?? user.user_metadata?.full_name, authProvider: user.app_metadata?.provider, authenticated: true, allowedNamespaces: ["real_life", "au"], adminCapabilities: Array.isArray(user.app_metadata?.adminCapabilities) ? user.app_metadata.adminCapabilities : Array.isArray(user.app_metadata?.roles) ? user.app_metadata.roles : [], isInternalOperator: user.app_metadata?.role === "admin" || user.app_metadata?.role === "operator" || user.app_metadata?.isInternalOperator === true, isPersistenceOperator: user.app_metadata?.isPersistenceOperator === true || user.app_metadata?.role === "persistence_operator", sessionSource: "supabase", serverDerivedOnly: true, clientUserIdAccepted: false, publicReadAllowed: false, publicPersistenceAllowed: false, serviceRoleUsed: false };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Include array-based roles when deriving operator flags.

adminCapabilities already falls back to app_metadata.roles, but isInternalOperator / isPersistenceOperator ignore that same array. A session shaped like { roles: ["admin"] } or { roles: ["persistence_operator"] } will still resolve to false here, so the new proposal preflights reject legitimate operators.

Suggested fix
+  const roles = Array.isArray(user.app_metadata?.roles)
+    ? user.app_metadata.roles.filter((role): role is string => typeof role === "string")
+    : [];
+  const adminCapabilities = Array.isArray(user.app_metadata?.adminCapabilities)
+    ? user.app_metadata.adminCapabilities.filter((cap): cap is string => typeof cap === "string")
+    : roles;
-  const session: PandoraServerSession = { userId: user.id, email: user.email, displayName: user.user_metadata?.name ?? user.user_metadata?.full_name, authProvider: user.app_metadata?.provider, authenticated: true, allowedNamespaces: ["real_life", "au"], adminCapabilities: Array.isArray(user.app_metadata?.adminCapabilities) ? user.app_metadata.adminCapabilities : Array.isArray(user.app_metadata?.roles) ? user.app_metadata.roles : [], isInternalOperator: user.app_metadata?.role === "admin" || user.app_metadata?.role === "operator" || user.app_metadata?.isInternalOperator === true, isPersistenceOperator: user.app_metadata?.isPersistenceOperator === true || user.app_metadata?.role === "persistence_operator", sessionSource: "supabase", serverDerivedOnly: true, clientUserIdAccepted: false, publicReadAllowed: false, publicPersistenceAllowed: false, serviceRoleUsed: false };
+  const session: PandoraServerSession = { userId: user.id, email: user.email, displayName: user.user_metadata?.name ?? user.user_metadata?.full_name, authProvider: user.app_metadata?.provider, authenticated: true, allowedNamespaces: ["real_life", "au"], adminCapabilities, isInternalOperator: roles.includes("admin") || roles.includes("operator") || user.app_metadata?.role === "admin" || user.app_metadata?.role === "operator" || user.app_metadata?.isInternalOperator === true, isPersistenceOperator: roles.includes("persistence_operator") || user.app_metadata?.isPersistenceOperator === true || user.app_metadata?.role === "persistence_operator", sessionSource: "supabase", serverDerivedOnly: true, clientUserIdAccepted: false, publicReadAllowed: false, publicPersistenceAllowed: false, serviceRoleUsed: false };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const session: PandoraServerSession = { userId: user.id, email: user.email, displayName: user.user_metadata?.name ?? user.user_metadata?.full_name, authProvider: user.app_metadata?.provider, authenticated: true, allowedNamespaces: ["real_life", "au"], adminCapabilities: Array.isArray(user.app_metadata?.adminCapabilities) ? user.app_metadata.adminCapabilities : Array.isArray(user.app_metadata?.roles) ? user.app_metadata.roles : [], isInternalOperator: user.app_metadata?.role === "admin" || user.app_metadata?.role === "operator" || user.app_metadata?.isInternalOperator === true, isPersistenceOperator: user.app_metadata?.isPersistenceOperator === true || user.app_metadata?.role === "persistence_operator", sessionSource: "supabase", serverDerivedOnly: true, clientUserIdAccepted: false, publicReadAllowed: false, publicPersistenceAllowed: false, serviceRoleUsed: false };
const roles = Array.isArray(user.app_metadata?.roles)
? user.app_metadata.roles.filter((role): role is string => typeof role === "string")
: [];
const adminCapabilities = Array.isArray(user.app_metadata?.adminCapabilities)
? user.app_metadata.adminCapabilities.filter((cap): cap is string => typeof cap === "string")
: roles;
const session: PandoraServerSession = { userId: user.id, email: user.email, displayName: user.user_metadata?.name ?? user.user_metadata?.full_name, authProvider: user.app_metadata?.provider, authenticated: true, allowedNamespaces: ["real_life", "au"], adminCapabilities, isInternalOperator: roles.includes("admin") || roles.includes("operator") || user.app_metadata?.role === "admin" || user.app_metadata?.role === "operator" || user.app_metadata?.isInternalOperator === true, isPersistenceOperator: roles.includes("persistence_operator") || user.app_metadata?.isPersistenceOperator === true || user.app_metadata?.role === "persistence_operator", sessionSource: "supabase", serverDerivedOnly: true, clientUserIdAccepted: false, publicReadAllowed: false, publicPersistenceAllowed: false, serviceRoleUsed: false };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/auth/pandora-server-session-resolver.ts` at line 21, The session
derivation in PandoraServerSessionResolver currently checks adminCapabilities
against app_metadata.roles but does not apply the same array fallback when
computing isInternalOperator and isPersistenceOperator. Update the session
object construction so the operator flags in the PandoraServerSession derived
from user.app_metadata also consider roles arrays (for example alongside role
and explicit booleans), keeping the logic consistent with the existing
adminCapabilities fallback.

return { ok: true, session, blockers: [] };
}
export function createRepositoryContextFromPandoraSession(input: PandoraRepositoryContextFromSessionInput): PandoraRepositoryContextFromSessionResult {
Expand Down
Loading
Loading