Skip to content
Merged
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
54 changes: 54 additions & 0 deletions app/api/cron/capability-sync/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Capability discovery→registry sync cron — zero-code onboarding PR-3 (gap G2).
*
* DEFAULT-OFF: inert unless CAP_DISCOVERY_SYNC_ENABLED=1, so merging this route
* changes nothing in production until deliberately enabled. When on, it fetches
* each orchestrator /discovery in CAP_DISCOVERY_URLS (comma-separated), extracts
* + validates Capability Descriptors (PR-1), and registers new identities into
* the universal overlay via the shipped lifecycle (dedup-guarded). Best-effort:
* a bad URL or descriptor is skipped, never fatal.
*
* Protected by CRON_SECRET when set (Vercel Cron sends it as a bearer).
*/
import { NextResponse } from "next/server";
import {
fetchDiscoveryDescriptors,
applyDiscoverySync,
} from "@/lib/capabilities/discovery-sync";
import type { CapabilityDescriptor } from "@/lib/capabilities/descriptor";

export const dynamic = "force-dynamic";

function authorized(req: Request): boolean {
const secret = process.env.CRON_SECRET;
if (!secret) return true; // no secret configured → allow (staging convenience)
const got = req.headers.get("authorization") ?? "";
return got === `Bearer ${secret}`;
}

export async function GET(req: Request) {
if (process.env.CAP_DISCOVERY_SYNC_ENABLED !== "1") {
return NextResponse.json({ enabled: false });
}
if (!authorized(req)) {
return NextResponse.json({ error: "unauthorized" }, { status: 401 });
}

const urls = (process.env.CAP_DISCOVERY_URLS ?? "")
.split(",")
.map((u) => u.trim())
.filter(Boolean);

const all: CapabilityDescriptor[] = [];
let invalid = 0;
const perUrl: Array<{ url: string; valid: number; invalid: number }> = [];
for (const url of urls) {
const { valid, invalid: bad } = await fetchDiscoveryDescriptors(url);
all.push(...valid);
invalid += bad.length;
perUrl.push({ url, valid: valid.length, invalid: bad.length });
}

const report = await applyDiscoverySync(all, invalid);
return NextResponse.json({ enabled: true, sources: perUrl, ...report });
}
232 changes: 232 additions & 0 deletions lib/capabilities/discovery-sync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
/**
* Discovery → registry sync — zero-code onboarding PR-3 (plan
* LIVE-RUNNER-CAP-ADOPTION-PROPOSAL.html §4, gap G2).
*
* SERVER-ONLY (transitively imports @vercel/blob via registry-overlay). Reads
* Capability Descriptors (PR-1) a provider publishes in their orchestrator's
* `/discovery` advertisement and runs them through the shipped cap-lifecycle:
*
* validate (PR-1) → dedup by semantic_key → NEW identity ⇒ register it into the
* universal overlay (so the agent can describe/list it); EXISTING identity same
* name ⇒ idempotent refresh; DIFFERENT name ⇒ synonym, SKIP + report.
*
* This is what turns "provider does work" into "it just works": no hand-run
* cap_add, no hardcoded per-cap map. The projection + planner are PURE (no I/O),
* so they unit-test without a network or Blob. The apply/fetch wrappers are thin.
*
* SAFETY: additive (new module + a default-OFF cron route); fail-open (a bad
* descriptor is skipped, never thrown); writes only the operator-governed
* universal overlay. Merging changes nothing until CAP_DISCOVERY_SYNC_ENABLED=1.
* NOTE: promoting a discovered cap to UNIVERSAL scope is governed by the sync
* being operator-triggered; per-descriptor scope governance is a later PR.
*/
import {
validateDescriptor,
computeSemanticKey,
type CapabilityDescriptor,
type Capability,
} from "./descriptor";
import type { RegistryEntry, OutputKind } from "./generate-registry";
import type { UsageBlock, UsageInput } from "./usage";
import { readRegistry, upsertOverlayEntry } from "./registry-overlay";

const MEDIA_KINDS = new Set(["image", "video", "audio", "3d"]);

const ACTIONS_BY_OUTPUT: Record<string, string[]> = {
image: ["generate", "restyle", "edit"],
video: ["animate", "generate"],
audio: ["tts", "music"],
"3d": ["generate"],
text: ["generate"],
tool: ["run"],
};

/** Canonical name from identity (mirrors cap_add §4.1): media ⇒ family[-variant]-modality; tool/mcp ⇒ family-modality. A descriptor may pin an explicit name (an existing identity). */
export function canonicalName(cap: Capability): string {
if (cap.name) return cap.name;
const medialess = cap.kind === "tool" || cap.kind === "mcp";
const parts = medialess
? [cap.family, cap.modality]
: [cap.family, cap.variant, cap.modality];
return parts.filter(Boolean).join("-").toLowerCase();
}

/** Synthesize the agent-usage block (describe_capability card) from the io contract. */
export function synthesizeUsage(cap: Capability, name: string): UsageBlock {
const invokeVia = MEDIA_KINDS.has(cap.output_kind) ? "create_media" : "run_capability";
const actions = ACTIONS_BY_OUTPUT[cap.output_kind] ?? ["generate"];

const inputs: Record<string, UsageInput> = {};
const exampleArgs: Record<string, unknown> = {};
for (const [param, spec] of Object.entries(cap.io.inputs)) {
const agentKey = spec.from ?? param;
inputs[agentKey] = {
type: spec.type ?? "string",
required: spec.required,
...(spec.values ? { enum: [...spec.values] } : {}),
...(spec.default !== undefined ? { default: spec.default } : {}),
note: `wire=${spec.wire}`,
};
if (spec.required) exampleArgs[agentKey] = spec.default ?? `<${param}>`;
}

const example =
invokeVia === "create_media"
? { tool: "create_media", args: { action: actions[0], model_override: name, ...exampleArgs } }
: { tool: "run_capability", args: { capability: name, ...exampleArgs } };

return {
invoke_via: invokeVia,
actions,
inputs,
output: { kind: cap.io.output.kind },
good_for: cap.good_for ?? `${cap.family} ${cap.modality}`,
...(cap.avoid_for ? { avoid_for: cap.avoid_for } : {}),
example,
};
}

/** Project a validated descriptor into a universal RegistryEntry (active, live). */
export function descriptorToEntry(cap: Capability): RegistryEntry {
const name = canonicalName(cap);
return {
name,
// Descriptor kinds are a superset (adds "live-runner"); the core union
// widening is a separate PR, so cast here — the overlay stores JSON and
// consumers that switch on kind fall back gracefully.
kind: cap.kind as RegistryEntry["kind"],
modality: cap.modality,
family: cap.family,
variant: cap.variant ?? null,
semantic_key: cap.semantic_key ?? computeSemanticKey(cap),
aliases: cap.aliases ?? [],
output_kind: cap.output_kind as OutputKind,
fallback_chain: null,
sla: null,
unit_kind: cap.offering.price.display_unit ?? null,
display_unit: cap.offering.price.display_unit ?? null,
display_price_usd: cap.offering.price.display_usd ?? null,
status: "active",
owner_scope: "universal",
usage: synthesizeUsage(cap, name),
};
}

export interface SyncPlan {
/** New/refreshed identities to upsert into the universal overlay. */
register: RegistryEntry[];
/** Descriptors refused because their key already belongs to another name. */
synonymSkipped: Array<{ name: string; key: string; owner: string }>;
}

/**
* PURE planner. Dedups descriptors against the existing registry view AND within
* the batch (mirrors cap_seed): a key owned by a DIFFERENT name is a synonym and
* is skipped; same name (or unseen) registers.
*/
export function planDiscoverySync(
descriptors: CapabilityDescriptor[],
existing: RegistryEntry[],
): SyncPlan {
const seen = new Map<string, string>(
existing.map((e) => [
e.semantic_key ??
computeSemanticKey({
kind: e.kind,
modality: e.modality ?? "",
family: e.family ?? "",
variant: e.variant ?? null,
}),
e.name,
]),
);

const register: RegistryEntry[] = [];
const synonymSkipped: SyncPlan["synonymSkipped"] = [];

for (const d of descriptors) {
const entry = descriptorToEntry(d.capability);
const owner = seen.get(entry.semantic_key);
if (owner && owner !== entry.name) {
synonymSkipped.push({ name: entry.name, key: entry.semantic_key, owner });
continue;
}
register.push(entry);
seen.set(entry.semantic_key, entry.name);
}
return { register, synonymSkipped };
}

export interface SyncReport {
registered: string[];
synonymSkipped: SyncPlan["synonymSkipped"];
invalid: number;
}

/** Apply a sync: read the universal view, plan, upsert. Best-effort (per-entry). */
export async function applyDiscoverySync(
descriptors: CapabilityDescriptor[],
invalidCount = 0,
): Promise<SyncReport> {
const existing = await readRegistry(); // universal view
const plan = planDiscoverySync(descriptors, existing);
const registered: string[] = [];
for (const entry of plan.register) {
try {
await upsertOverlayEntry(entry, { scope: "universal" });
registered.push(entry.name);
} catch {
// best-effort: one Blob write failing must not abort the rest
}
}
return { registered, synonymSkipped: plan.synonymSkipped, invalid: invalidCount };
}

/**
* PURE extraction: pull + validate Capability Descriptors out of a parsed
* `/discovery` payload. Tolerant of shapes — a bare array, `{runners:[…]}`, or
* `{orchestrators:[{runners:[…]}]}`. Entries without a `capability` block (legacy
* app-examples advertisements) are simply ignored (back-compat).
*/
export function extractDescriptors(discovery: unknown): {
valid: CapabilityDescriptor[];
invalid: Array<{ errors: string[] }>;
} {
const entries: unknown[] = Array.isArray(discovery)
? discovery
: isRecord(discovery) && Array.isArray(discovery.runners)
? discovery.runners
: isRecord(discovery) && Array.isArray(discovery.orchestrators)
? discovery.orchestrators.flatMap((o) =>
isRecord(o) && Array.isArray(o.runners) ? o.runners : [],
)
: [];

const valid: CapabilityDescriptor[] = [];
const invalid: Array<{ errors: string[] }> = [];
for (const e of entries) {
if (!isRecord(e) || e.capability === undefined) continue; // legacy / no descriptor
const res = validateDescriptor({ capability: e.capability });
if (res.ok) valid.push(res.value);
else invalid.push({ errors: res.errors });
}
return { valid, invalid };
}

/** Fetch one orchestrator's /discovery and extract descriptors. Fail-open. */
export async function fetchDiscoveryDescriptors(url: string): Promise<{
valid: CapabilityDescriptor[];
invalid: Array<{ errors: string[] }>;
}> {
try {
const res = await fetch(url, { cache: "no-store" });
if (!res.ok) return { valid: [], invalid: [] };
return extractDescriptors(await res.json());
} catch {
return { valid: [], invalid: [] };
}
}

function isRecord(v: unknown): v is Record<string, unknown> {
return typeof v === "object" && v !== null;
}
Loading
Loading