diff --git a/.agents/skills/validate-sas-release/SKILL.md b/.agents/skills/validate-sas-release/SKILL.md index add82047..0b9a6777 100644 --- a/.agents/skills/validate-sas-release/SKILL.md +++ b/.agents/skills/validate-sas-release/SKILL.md @@ -23,6 +23,8 @@ When the owner explicitly authorizes a model migration, retain failed histories Publication plans must reject empty/no-op targets and stale source/archive/run bindings. Native SQL is not an authenticated administrator session: use the existing admin review flow for publication, preserving `enforce_admin_only_publish` and the adversarial trigger. Do not forge claims or alter roles/triggers to publish. Verify actual public versions and model/protocol scope afterward. +The admin review write must retain the verified administrator's authenticated client, pin the evaluated version and confirm the returned row's status, publication flag and reviewer. A closed dialog or successful HTTP transport alone is not publication evidence. Run the real Supabase HTTP transport regressions in `tests/admin-review-update.test.ts`; database denials, missing rows and mismatched versions/actors must reject. Confirm the actual persisted/public subset afterward. + The original business case contract is pinned in `scripts/adversarial-approval.mjs`. Qualification, registry recording and publication planning require all 21 skills and 273 unchanged cases, including inputs, contexts, severities and expectations. Pilots never qualify. Test changes are not an allowed failure fix. Review each distinct raw response against the original domain expectation and record hash-bound decisions; group identical baseline evidence with the existing review-group command without automatically generating verdicts. For interrupted local inference, reuse the worker's explicit `--resume` only when it accepts the original incomplete JSONL prefix and exact plan/runtime/package fingerprints. Preserve all prior rows, including errors. The worker requires a hash-bound attempt journal ending at a completed case, appends only not-yet-started cases and records its actual resumption. An in-flight or unknown attempt blocks recovery. Use `--max-cases N` for an explicit boundary checkpoint; exit 4 remains incomplete evidence. Changed or legacy runtime evidence must remain separate; never edit a raw output or its plan hash to make recovery pass. Run the deterministic recovery regressions with `python3 tests/worker_resume_test.py`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d0b0e81..45c09540 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,14 @@ Component artifacts are versioned and released independently via git tags: - **`super-agent` CLI** — `cli-v*` tags publish cross-platform binaries. - **`@superagentskill/sdk`** — versioned in `packages/sdk-ts/package.json`. +## [0.6.5] — 2026-10-24 — *Genesis* (alpha) — Authenticated publication repair + +### Changed + +- Preserve the verified administrator session through RLS and publication triggers. +- Bind approval writes to the evaluated version and confirm the returned reviewer/status; reject errors and no-op writes. +- Keep failed server responses from being reported as successful review decisions. + ## [0.6.4] — 2026-10-23 — *Genesis* (alpha) — Maintenance release ### Changed diff --git a/package-lock.json b/package-lock.json index 4a046673..16f9d227 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tanstack_start_ts", - "version": "0.6.4", + "version": "0.6.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tanstack_start_ts", - "version": "0.6.4", + "version": "0.6.5", "dependencies": { "@ai-sdk/openai-compatible": "^2.0.47", "@cloudflare/vite-plugin": "1.63.0", diff --git a/package.json b/package.json index 4251d90d..8a8560c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tanstack_start_ts", - "version": "0.6.4", + "version": "0.6.5", "private": true, "sideEffects": false, "type": "module", diff --git a/plugin.json b/plugin.json index 29eecd69..8858b117 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "superagentskill", - "version": "0.6.4", + "version": "0.6.5", "description": "Agent skills and candidate workflows from Super Agent Skill. Published registry packages expose available trust and execution evidence; candidate workflows include offline fixtures.", "author": { "name": "Super Agent Skill", diff --git a/src/lib/admin/review-update.ts b/src/lib/admin/review-update.ts new file mode 100644 index 00000000..732c7d24 --- /dev/null +++ b/src/lib/admin/review-update.ts @@ -0,0 +1,42 @@ +import type { AppDatabaseClient } from "@/integrations/supabase/database"; + +export type ReviewStatus = "draft" | "pending" | "approved" | "paused" | "rejected"; + +/** Keep the verified administrator's JWT so RLS and publication triggers see the real actor. */ +export async function persistReviewDecision( + client: AppDatabaseClient, + input: { + slug: string; + status: ReviewStatus; + userId: string; + version?: string; + notes: string | null; + }, +) { + const published = input.status === "approved"; + let query = client + .from("packages") + .update({ + review_status: input.status, + review_notes: input.notes, + reviewed_by: input.userId, + reviewed_at: new Date().toISOString(), + is_published: published, + }) + .eq("slug", input.slug); + if (input.version) query = query.eq("latest_version", input.version); + const { data, error } = await query + .select("slug,latest_version,review_status,is_published,reviewed_by") + .single(); + if (error) throw new Error(`Review was not saved: ${error.message}`); + if ( + !data || + data.slug !== input.slug || + data.review_status !== input.status || + data.is_published !== published || + data.reviewed_by !== input.userId || + (input.version && data.latest_version !== input.version) + ) + throw new Error("Review was not saved with the expected version, status and administrator"); + return { ok: true as const, status: input.status, is_published: published }; +} diff --git a/src/lib/admin/review.functions.ts b/src/lib/admin/review.functions.ts index c31f93b0..09cdf843 100644 --- a/src/lib/admin/review.functions.ts +++ b/src/lib/admin/review.functions.ts @@ -3,10 +3,10 @@ import type { AppDatabaseClient } from "@/integrations/supabase/database"; import { createServerFn } from "@tanstack/react-start"; import { requireSupabaseAuth } from "@/integrations/supabase/auth-middleware"; import { supabaseAdmin as _supabaseAdmin } from "@/integrations/supabase/client.server"; +import { persistReviewDecision, type ReviewStatus } from "./review-update"; +export type { ReviewStatus } from "./review-update"; const supabaseAdmin = _supabaseAdmin; -export type ReviewStatus = "draft" | "pending" | "approved" | "paused" | "rejected"; - export type ReviewQueueItem = { id: string; slug: string; @@ -116,7 +116,7 @@ export const setReviewStatus = createServerFn({ method: "POST" }) }) .handler(async ({ context, data }) => { await assertAdmin(context.supabase, context.userId); - const isPublished = data.status === "approved"; + let approvedVersion: string | undefined; // Mandatory adversarial gate: a package can only be approved (and thus // become publicly installable / served via MCP) if the CURRENT version @@ -126,25 +126,16 @@ export const setReviewStatus = createServerFn({ method: "POST" }) if (data.status === "approved") { const gate = await assertAdversarialGate(data.slug); if (!gate.ok) { - throw new Response( - JSON.stringify({ error: "adversarial_gate_failed", reason: gate.reason, run: gate.run }), - { status: 422, headers: { "Content-Type": "application/json" } }, - ); + throw new Error(`Adversarial approval gate failed: ${gate.reason}`); } + approvedVersion = gate.version; } - const { error } = await supabaseAdmin - .from("packages") - .update({ - review_status: data.status, - review_notes: data.notes, - reviewed_by: context.userId, - reviewed_at: new Date().toISOString(), - is_published: isPublished, - }) - .eq("slug", data.slug); - if (error) throw new Response(error.message, { status: 500 }); - return { ok: true, status: data.status, is_published: isPublished }; + return persistReviewDecision(context.supabase, { + ...data, + userId: context.userId, + version: approvedVersion, + }); }); // Approval thresholds. Conservative on purpose: zero tolerance for high/ @@ -152,7 +143,8 @@ export const setReviewStatus = createServerFn({ method: "POST" }) const MIN_SEVERITY_WEIGHTED_SCORE = 0.9; const MIN_PASS_RATE = 0.9; -type GateResult = { ok: true; run: unknown } | { ok: false; reason: string; run: unknown }; +type GateResult = + { ok: true; run: unknown; version: string } | { ok: false; reason: string; run: unknown }; async function assertAdversarialGate(slug: string): Promise { const { data: pkg } = await supabaseAdmin @@ -224,5 +216,5 @@ async function assertAdversarialGate(slug: string): Promise { run, }; } - return { ok: true, run }; + return { ok: true, run, version: ver.version }; } diff --git a/src/lib/version.ts b/src/lib/version.ts index b0681f54..45701676 100644 --- a/src/lib/version.ts +++ b/src/lib/version.ts @@ -12,10 +12,10 @@ * We are still pre-1.0: rapid evolution, no stability guarantees. */ -export const PLATFORM_VERSION = "0.6.4" as const; +export const PLATFORM_VERSION = "0.6.5" as const; export const PLATFORM_CODENAME = "Genesis" as const; export const PLATFORM_STAGE: "alpha" | "beta" | "ga" = "alpha"; -export const PLATFORM_BUILD_DATE = "2026-10-23" as const; +export const PLATFORM_BUILD_DATE = "2026-10-24" as const; export type ChangelogEntry = { version: string; @@ -30,6 +30,16 @@ export type ChangelogEntry = { * Every deployed change appends a new entry (or amends the top one before shipping). */ export const CHANGELOG: ChangelogEntry[] = [ + { + version: "0.6.5", + date: "2026-10-24", + kind: "patch", + title: "Authenticated publication repair", + highlights: [ + "Preserve the administrator session through RLS and publication triggers.", + "Confirm the saved version, reviewer and publication state before reporting success.", + ], + }, { version: "0.6.4", date: "2026-10-23", diff --git a/src/routes/admin.review.tsx b/src/routes/admin.review.tsx index 2c7f47ee..432ecad6 100644 --- a/src/routes/admin.review.tsx +++ b/src/routes/admin.review.tsx @@ -202,6 +202,10 @@ function ReviewRow({ item, onAction }: { item: ReviewQueueItem; onAction: () => mutationFn: (input: { status: ReviewStatus; notes: string | null }) => setStatusFn({ data: { slug: item.slug, status: input.status, notes: input.notes } }), onSuccess: (res) => { + if (res?.ok !== true) { + toast.error("Review was not confirmed. Refresh the queue and verify its status."); + return; + } toast.success(`Marked as ${res.status}${res.is_published ? " · live" : ""}`); setOpen(null); setNotes(""); diff --git a/tests/admin-review-update.test.ts b/tests/admin-review-update.test.ts new file mode 100644 index 00000000..10257c61 --- /dev/null +++ b/tests/admin-review-update.test.ts @@ -0,0 +1,86 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createClient } from "@supabase/supabase-js"; +import type { Database } from "../src/integrations/supabase/database.ts"; +import { persistReviewDecision } from "../src/lib/admin/review-update.ts"; + +const input = { + slug: "review-fixture", + status: "approved" as const, + userId: "00000000-0000-4000-8000-000000000001", + version: "0.1.4", + notes: "Offline transport regression only", +}; +const saved = { + slug: input.slug, + latest_version: input.version, + review_status: "approved", + is_published: true, + reviewed_by: input.userId, +}; +function client(response: unknown, status = 200, inspect?: (url: URL, init: RequestInit) => void) { + return createClient("https://review-fixture.invalid", "test-publishable-key", { + auth: { persistSession: false, autoRefreshToken: false }, + global: { + headers: { Authorization: "Bearer synthetic-admin-session" }, + fetch: async (url, init) => { + inspect?.(new URL(String(url)), init ?? {}); + return new Response(JSON.stringify(response), { + status, + headers: { "content-type": "application/json" }, + }); + }, + }, + }); +} + +test("review persistence sends the caller JWT and guards the evaluated version", async () => { + let writes = 0; + const db = client(saved, 200, (url, init) => { + writes++; + assert.equal(init.method, "PATCH"); + assert.equal(new Headers(init.headers).get("authorization"), "Bearer synthetic-admin-session"); + assert.equal(url.searchParams.get("slug"), "eq.review-fixture"); + assert.equal(url.searchParams.get("latest_version"), "eq.0.1.4"); + const body = JSON.parse(String(init.body)); + assert.equal(body.reviewed_by, input.userId); + assert.equal(body.is_published, true); + }); + assert.deepEqual(await persistReviewDecision(db, input), { + ok: true, + status: "approved", + is_published: true, + }); + assert.equal(writes, 1); +}); + +test("a publication trigger/RLS denial rejects instead of reporting approval", async () => { + await assert.rejects( + persistReviewDecision( + client({ code: "42501", message: "marketplace_publish_forbidden" }, 403), + input, + ), + /Review was not saved: marketplace_publish_forbidden/, + ); +}); + +test("no updated row cannot become a successful review", async () => { + await assert.rejects( + persistReviewDecision(client({ code: "PGRST116", message: "No matching version" }, 406), input), + /Review was not saved: No matching version/, + ); +}); + +test("a mismatched persisted version, actor or publication flag rejects", async () => { + for (const change of [ + { latest_version: "0.1.5" }, + { reviewed_by: null }, + { is_published: false }, + { review_status: "draft" }, + ]) { + await assert.rejects( + persistReviewDecision(client({ ...saved, ...change }), input), + /expected version, status and administrator/, + ); + } +});