/*
RFC-0289: pure, dependency-free formatter that projects a site's Agent Surface
Manifest (+ the capability schemas for its active actions) into a static
OpenAPI 3.1 document. No transformation of capability schemas — they are
copied verbatim (AGO-04 enforces this).
- Do not read files or resolve capabilities — the kernel command does that
and passes the manifest + capability schemas here.
- Do not adopt an OpenAPI library/types package — this is a small, closed
structural subset (AS-5: protocols are disposable).
- RFC-0289: initial OpenAPI projection.
- RFC-0954: add /api/agent/search path with dynamic lang enum from manifest.languages.supported.
- RFC-1112: Idempotency-Key header param + application/problem+json error responses on action paths.
- RFC-1115: agentGuidance renders into operation descriptions — model-oriented guidance for when/when-not/retry.
- RFC-1169: CapabilitySchemaInput gains sideEffect + widened output type; sideEffect "none" actions omit the declared 422 (violations answer 200 ready:false).
*/
import type { AgentSurfaceManifest } from "./manifest.ts";
import type {
CapabilityInputOutputSchema,
CapabilityOutputSchema,
} from "@warpgogol/werkstatt-shared/ontology";
import { renderAgentGuidanceDescription, type CapabilityAgentGuidance } from "./guidance.ts";
export interface OpenApiSchemaObject {
type: string;
required?: string[];
additionalProperties?: boolean;
properties?: Record;
description?: string;
}
export interface OpenApiOperation {
operationId: string;
summary?: string;
description?: string;
/** RFC-1171: OpenAPI 3.1 deprecation flag — set on alias operations. */
deprecated?: boolean;
/** RFC-1212: machine-readable pricing — the agent.json `pricing` projection. */
"x-gogol-pricing"?: unknown;
tags: string[];
parameters?: Array<{
name: string;
in: string;
required?: boolean;
schema: Record;
description?: string;
}>;
requestBody?: {
required: true;
content: { "application/json": { schema: OpenApiSchemaObject } };
};
responses: Record<
string,
{
description: string;
content?: Partial<
Record<"application/json" | "application/problem+json", { schema: unknown }>
>;
}
>;
}
export interface OpenApiDocument {
openapi: "3.1.0";
info: { title: string; version: string; "x-gogol-content-hash": string };
servers: Array<{ url: string }>;
paths: Record>;
components: { schemas: Record };
externalDocs?: { url: string; description: string };
"x-gogol-mcp"?: { url: string; protocolVersion: string };
"x-gogol-proof"?: unknown;
}
/** One active capability's schemas — the exact shapes referenced by its AgentActionRef. */
export interface CapabilitySchemaInput {
id: string;
input: CapabilityInputOutputSchema;
output: CapabilityOutputSchema;
/** RFC-1169: preview capabilities ("none") drop the 422 — violations answer 200. */
sideEffect?: "write" | "none";
/** RFC-1115: localized base description for operation docs. */
description?: Record;
/** RFC-1115: model-oriented guidance rendered into the operation description. */
agentGuidance?: CapabilityAgentGuidance;
}
/** RFC-1112: shared RFC 9457 problem-details schema referenced by every action error response. */
function problemDetailsSchema(): Record {
return {
type: "object",
required: ["type", "title", "status", "code", "retryable"],
properties: {
type: { type: "string" },
title: { type: "string" },
status: { type: "integer" },
detail: { type: "string" },
code: {
type: "string",
enum: [
"invalid-json",
"schema-violation",
"unknown-capability",
"payload-too-large",
"rate-limited",
"dispatch-failed",
"idempotency-key-reuse",
],
},
retryable: { type: "boolean" },
errors: {
type: "array",
items: {
type: "object",
properties: { path: { type: "string" }, message: { type: "string" } },
},
},
schemaRef: { type: "string" },
retryAfterSeconds: { type: "integer" },
},
};
}
/**
* RFC-1112: the standard error responses every action path declares.
* RFC-1169: `sideEffect: "none"` actions omit the 422 — schema violations on
* the preview path answer 200 with the readiness envelope, so declaring the
* response would lie to agents.
*/
function actionErrorResponses(sideEffect?: "write" | "none"): OpenApiOperation["responses"] {
const problem = {
"application/problem+json": {
schema: { $ref: "#/components/schemas/problem-details" },
},
};
const responses: OpenApiOperation["responses"] = {
"400": { description: "Request body is not valid JSON", content: problem },
"409": {
description: "Idempotency-Key was used with a different payload",
content: problem,
},
"413": { description: "Payload exceeds the capability limit", content: problem },
"429": { description: "Rate limit exceeded", content: problem },
"502": { description: "Delivery substrate rejected the event", content: problem },
};
if (sideEffect !== "none") {
responses["422"] = {
description: "Input failed schema validation",
content: problem,
};
}
return responses;
}
/** Generic envelope schema — every knowledge domain shares this wrapper shape (RFC-0287); only `data`'s inner shape varies by domain and is intentionally left untyped here. */
function knowledgeEnvelopeSchema(): OpenApiSchemaObject {
return {
type: "object",
required: ["schema", "site", "baseUrl", "languages", "contentHash", "data"],
properties: {
schema: { type: "string" },
site: { type: "string" },
baseUrl: { type: "string" },
languages: { type: "object" },
contentHash: { type: "string" },
freshness: { type: "object" },
data: { type: "object", description: "Per-language payload; shape varies by domain." },
},
};
}
/**
* Pure: project the manifest (+ active capabilities' schemas) into an OpenAPI
* 3.1 document. Exhaustive per the RFC-0289 projection table — adds nothing beyond it.
*/
export function formatAgentOpenApi(
manifest: AgentSurfaceManifest,
capabilitySchemas: CapabilitySchemaInput[],
): OpenApiDocument {
const schemasById = new Map(capabilitySchemas.map((c) => [c.id, c]));
const paths: OpenApiDocument["paths"] = {};
const componentSchemas: Record = {};
for (const ref of manifest.knowledge) {
componentSchemas[`knowledge-${ref.domain}`] = knowledgeEnvelopeSchema();
paths[ref.url] = {
get: {
operationId: `knowledge.${ref.domain}.get`,
tags: ["knowledge"],
responses: {
"200": {
description: `${ref.domain} knowledge envelope`,
content: {
"application/json": {
schema: { $ref: `#/components/schemas/knowledge-${ref.domain}` },
},
},
},
},
},
};
}
componentSchemas["problem-details"] = problemDetailsSchema();
for (const ref of manifest.actions) {
const schemas = schemasById.get(ref.id);
if (!schemas) continue; // defensive — AGO-04-checked at validate time
componentSchemas[`${ref.id}-input`] = schemas.input;
componentSchemas[`${ref.id}-output`] = schemas.output;
// RFC-1115: agentGuidance composes a model-oriented operation description;
// absent guidance falls back to the base capability description.
const baseDescription =
schemas.description?.[manifest.languages.default] ??
ref.title[manifest.languages.default] ??
ref.id;
paths[ref.url] = {
post: {
operationId: `action.${ref.id}`,
summary: ref.title[manifest.languages.default] ?? ref.id,
description: renderAgentGuidanceDescription(baseDescription, schemas.agentGuidance),
tags: ["actions"],
parameters: [
{
name: "Idempotency-Key",
in: "header",
required: false,
schema: { type: "string", maxLength: 255 },
description:
"Optional dedup key. Repeating a request with the same key and body returns the original receipt; the same key with a different body is rejected with 409.",
},
],
// RFC-1212: machine-readable pricing on metered capabilities — the
// same projection agent.json carries, exposed as an OpenAPI extension.
...(ref.pricing ? { "x-gogol-pricing": ref.pricing } : {}),
requestBody: {
required: true,
content: { "application/json": { schema: schemas.input } },
},
responses: {
"200": {
description: `${ref.id} accepted`,
content: { "application/json": { schema: schemas.output } },
},
...actionErrorResponses(schemas.sideEffect),
},
},
};
}
// RFC-1171: deprecated alias paths are projected as deprecated operations —
// every agent reading the document sees the successor pointer on the alias.
for (const dep of manifest.deprecations ?? []) {
for (const ref of manifest.actions) {
const successorUrl = dep.successor.replaceAll("{id}", ref.id);
if (successorUrl !== ref.url) continue;
const canonical = paths[ref.url]?.post;
if (!canonical) continue;
const aliasPath = dep.path.replaceAll("{id}", ref.id);
paths[aliasPath] = {
post: {
...canonical,
operationId: `action.${ref.id}.deprecated`,
deprecated: true,
description: `Deprecated since ${dep.deprecateAt} — use ${successorUrl} instead (sunset ${dep.sunsetAt}). ${dep.reason}`,
},
};
}
}
if (manifest.interfaces.search) {
const searchSchema: OpenApiSchemaObject = {
type: "object",
required: ["query", "results", "tookMs"],
properties: {
query: { type: "string" },
results: {
type: "array",
items: {
type: "object",
required: ["title", "canonicalUrl", "updatedAt", "lang", "type", "score", "text"],
properties: {
title: { type: ["string", "null"] },
canonicalUrl: { type: "string" },
updatedAt: { type: ["string", "null"] },
lang: { type: "string" },
type: { type: "string", enum: ["page", "knowledge", "faq", "prose"] },
score: { type: "number" },
text: { type: "string" },
},
},
},
tookMs: { type: "number" },
},
};
componentSchemas["search-response"] = searchSchema;
const langEnum = manifest.languages.supported;
paths[manifest.interfaces.search.url] = {
get: {
operationId: "search.query",
summary: "Semantic search over site content",
tags: ["search"],
responses: {
"200": {
description: "Search results",
content: {
"application/json": { schema: { $ref: "#/components/schemas/search-response" } },
},
},
"400": { description: "Invalid query" },
"503": { description: "Search index not available" },
},
parameters: [
{
name: "q",
in: "query",
required: true,
schema: { type: "string", maxLength: 1000 },
description: "Natural language search query",
},
{
name: "lang",
in: "query",
required: false,
schema: { type: "string", enum: langEnum },
description: "Filter by language",
},
{
name: "type",
in: "query",
required: false,
schema: { type: "string", enum: ["page", "knowledge", "faq", "prose"] },
description: "Filter by content type",
},
{
name: "topK",
in: "query",
required: false,
schema: { type: "integer", default: 5, maximum: 20 },
description: "Number of results to return",
},
],
},
};
}
const doc: OpenApiDocument = {
openapi: "3.1.0",
info: {
title: `${manifest.site} agent surface`,
version: manifest.surfaceVersion,
"x-gogol-content-hash": manifest.contentHash,
},
servers: [{ url: manifest.baseUrl }],
paths,
components: { schemas: componentSchemas },
externalDocs: {
url: `${manifest.baseUrl}${manifest.interfaces.llms}`,
description: "LLM-oriented text surface",
},
};
if (manifest.interfaces.mcp) doc["x-gogol-mcp"] = manifest.interfaces.mcp;
if (manifest.proof) doc["x-gogol-proof"] = manifest.proof;
return doc;
}