/*
RFC-0288: the closed capability catalog record schema — one YAML file per
AI-agent-invocable action (packages/werkstatt-shared/src/ontology/capabilities/*.yaml).
The input/output shape is a deliberately closed JSON-Schema subset chosen for
lossless projection to both OpenAPI (RFC-0289) and MCP tool schemas (RFC-0290).
RFC-1169 widened the OUTPUT subset with nested `object`/`array` properties
(preview envelopes); the INPUT subset stays flat — the runtime validator
interprets flat shapes only.
- Do not resolve entitlements or active-capability logic here — that is
@warpgogol/werkstatt-shared/agent/capability.ts (resolveActiveCapabilities).
- Do not widen the JSON-Schema subset beyond what OpenAPI + MCP both
accept verbatim — widening requires an RFC amending RFC-0288.
- RFC-1186: capabilityRequiresSchema gains optional formBinding flag (activate only where a bound send-message block exists); eventKind enum gains recommendation and referral-application.
- RFC-1186: steps 1-2 — capability contracts, catalog records, resolver, propsSchema
- RFC-1186: step 5a — IntegrationEvent kind union + delivery formId parity
- RFC-1187: phases 1-3 — contracts, Redis registry, AS core + Bearer pipeline
- RFC-1211: optional closed `ui` block (capabilityUiSchema from agent/capability-schema.ts) — opts a capability into deterministic ui:// resource projection.
RFC-0288, RFC-1112, RFC-1115, RFC-1169
*/
import { z } from "zod";
import { INTEGRATION_EVENT_KINDS } from "../../integration/port.ts";
import { capabilityMeteringSchema, capabilityUiSchema } from "../../agent/capability-schema.ts";
import { verifiablePresentationRequirementSchema } from "../../agent/presentation-verdict.ts";
/** Localized string map: language code → string. At least one entry required. */
const localizedStringSchema = z
.record(z.string().min(1), z.string().min(1))
.refine((v) => Object.keys(v).length > 0, { message: "must declare at least one language" });
const capabilityPropertySchema = z
.object({
type: z.enum(["string", "boolean", "integer"]),
format: z.enum(["email", "uuid", "uri", "date"]).optional(),
minLength: z.number().int().nonnegative().optional(),
maxLength: z.number().int().positive().optional(),
})
.strict();
/**
* RFC-0288: the closed JSON-Schema subset. Deliberately small — every field
* here must be losslessly projectable to an OpenAPI 3.1 schema object AND an
* MCP tool inputSchema verbatim, with no transformation.
*/
export const capabilityInputOutputSchema = z
.object({
type: z.literal("object"),
required: z.array(z.string().min(1)).optional(),
additionalProperties: z.literal(false),
properties: z.record(z.string().min(1), capabilityPropertySchema),
})
.strict()
.refine((schema) => (schema.required ?? []).every((key) => key in schema.properties), {
message: "required[] entries must all exist in properties",
});
export type CapabilityInputOutputSchema = z.infer;
/**
* RFC-1169: nested property schema for OUTPUT shapes only. The flat
* `capabilityPropertySchema` above stays the input contract — the runtime
* validator (`validateAgainstCapabilitySchema`) interprets flat shapes only.
* Output schemas may nest one level via `object`/`array` so preview envelopes
* (missing[], problems[], summary{}) are declarable while staying verbatim-
* projectable to OpenAPI 3.1 and MCP tool schemas.
*/
export const capabilityNestedPropertySchema: z.ZodType = z.lazy(() =>
z
.object({
type: z.enum(["string", "boolean", "integer", "array", "object"]),
format: z.enum(["email", "uuid", "uri", "date"]).optional(),
minLength: z.number().int().nonnegative().optional(),
maxLength: z.number().int().positive().optional(),
items: capabilityNestedPropertySchema.optional(),
properties: z.record(z.string().min(1), capabilityNestedPropertySchema).optional(),
required: z.array(z.string().min(1)).optional(),
additionalProperties: z.boolean().optional(),
description: z.string().min(1).optional(),
})
.strict(),
);
/** Recursive output property shape — the TS mirror of capabilityNestedPropertySchema. */
export interface CapabilityNestedProperty {
type: "string" | "boolean" | "integer" | "array" | "object";
format?: "email" | "uuid" | "uri" | "date";
minLength?: number;
maxLength?: number;
items?: CapabilityNestedProperty;
properties?: Record;
required?: string[];
additionalProperties?: boolean;
description?: string;
}
/**
* RFC-1169: output schema subset — same object wrapper as the input subset,
* but `properties` accepts nested shapes. Conditional presence (e.g. `draftId`
* present only when `ready: true`) is NOT expressible — outputs declare only
* flat `required` sets; conditional invariants are enforced by
* `agent.surface.contract.check`, not by this schema.
*/
export const capabilityOutputSchema = z
.object({
type: z.literal("object"),
required: z.array(z.string().min(1)).optional(),
additionalProperties: z.literal(false),
properties: z.record(z.string().min(1), capabilityNestedPropertySchema),
})
.strict()
.refine((schema) => (schema.required ?? []).every((key) => key in schema.properties), {
message: "required[] entries must all exist in properties",
});
export type CapabilityOutputSchema = z.infer;
/**
* RFC-1169: the submit-sibling linkage declared on `sideEffect: "none"`
* capabilities. `capability` must resolve against the closed catalog
* (enforced by agent.capability.validate, AGC-06 — the record schema cannot
* see sibling records); `sideEffect` is a human+model readable consequence
* statement of that next action, not the record-level effect class.
*/
const capabilityNextActionSchema = z
.object({
capability: z
.string()
.regex(/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/, "capability must be a dot-separated id"),
sideEffect: z.string().min(1),
})
.strict();
const capabilityIntegrationSchema = z
.object({
// Single vocabulary with IntegrationEvent.kind — the delivery substrate
// owns the closed kind catalog (RFC-1186).
eventKind: z.enum(INTEGRATION_EVENT_KINDS),
source: z.literal("agent"),
})
.strict();
const capabilityRequiresSchema = z
.object({
entitlements: z.array(z.string().min(1)).default([]),
sections: z.array(z.string().min(1)).default([]),
/**
* RFC-1186: when true, the capability is active only where at least one
* `send-message` block declares `agentCapability: ` — in addition to
* the entitlement/section gates. Absent/false means binding does not
* affect activation.
*/
formBinding: z.boolean().optional(),
/**
* RFC-1213: opt-in verifier-side presentation requirement — the caller
* must attach a W3C verifiable presentation satisfying `accept` and bound
* to its verified agent identity (bindTo: "verified-agent" only in v1).
* Closed shape; the canonical schema lives in agent/presentation-verdict.ts.
*/
verifiablePresentation: verifiablePresentationRequirementSchema.optional(),
})
.strict();
const capabilityHumanEquivalentSchema = z
.object({
sectionType: z.string().min(1),
})
.strict();
const capabilityLimitsSchema = z
.object({
perMinutePerIp: z.number().int().positive(),
maxPayloadBytes: z.number().int().positive(),
/**
* RFC-1187: elevated-tier limit for verified-Bearer callers, keyed
* `${capabilityId}:agent:${agentId}` instead of the anonymous `:ip:` bucket.
* Optional — when absent, authenticated callers keep the anonymous limit.
*/
perMinutePerAgent: z.number().int().positive().optional(),
})
.strict();
/**
* RFC-1115: model-oriented guidance rendered into MCP tool descriptions and
* OpenAPI operation docs. `sideEffect` restates the record-level sideEffect so
* the guidance block is self-contained — a superRefine keeps them consistent.
* `retrySemantics` describes the RFC-1112 Idempotency-Key contract
* descriptively; the mechanism stays owned by RFC-1112.
*/
const capabilityAgentGuidanceSchema = z
.object({
sideEffect: z.enum(["write", "none"]),
whenToUse: z.string().min(1),
whenNotToUse: z.string().min(1).optional(),
retrySemantics: z.string().min(1).optional(),
})
.strict();
/** RFC-0288: one capability catalog record (packages/werkstatt-site/src/domain/ontology/capabilities/.yaml). */
export const capabilityRecordSchema = z
.object({
/** Dot-separated, lowercase; the file stem MUST equal this id. */
id: z
.string()
.regex(/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/, "id must be dot-separated lowercase"),
/** Integer; bump on breaking input/output change (by RFC). */
version: z.number().int().positive(),
kind: z.literal("action"),
/**
* RFC-1112: "write" (default when absent) dispatches an IntegrationEvent;
* "none" marks a side-effect-free preview that validates input and returns
* a draftId without dispatching.
*/
sideEffect: z.enum(["write", "none"]).optional(),
title: localizedStringSchema,
description: localizedStringSchema,
input: capabilityInputOutputSchema,
output: capabilityOutputSchema,
/** RFC-1169: preview → submit sibling linkage; meaningful only on sideEffect "none" records (AGC-06 enforces). */
nextAction: capabilityNextActionSchema.optional(),
integration: capabilityIntegrationSchema.optional(),
requires: capabilityRequiresSchema,
humanEquivalent: capabilityHumanEquivalentSchema.optional(),
limits: capabilityLimitsSchema,
agentGuidance: capabilityAgentGuidanceSchema.optional(),
/**
* RFC-1211: opt-in MCP-UI resource projection — `resource: true` gives the
* capability a deterministic self-contained `ui://` document committed
* under public/.well-known/ui/ and served via MCP resources. Closed shape;
* the canonical schema lives in agent/capability-schema.ts.
*/
ui: capabilityUiSchema.optional(),
/**
* RFC-1212: opt-in metered invocation — closed `metering` block declaring
* per-call pricing (PBP usage-Charge product ref + unit price + free
* window). Metered capabilities emit exactly one journaled AgentUsageRecord
* per dispatched call and project `pricing` into the agent manifest.
*/
metering: capabilityMeteringSchema.optional(),
})
.strict()
.superRefine((record, ctx) => {
// RFC-1115: agentGuidance.sideEffect must restate the effective record-level
// sideEffect (absent means "write") — contradictory declarations are drift.
const effectiveSideEffect = record.sideEffect ?? "write";
if (record.agentGuidance && record.agentGuidance.sideEffect !== effectiveSideEffect) {
ctx.addIssue({
code: "custom",
path: ["agentGuidance", "sideEffect"],
message: `must match the record-level sideEffect ("${effectiveSideEffect}")`,
});
}
if (record.sideEffect === "none") return;
if (!record.integration) {
ctx.addIssue({
code: "custom",
path: ["integration"],
message: 'required unless sideEffect is "none"',
});
}
if (!record.humanEquivalent) {
ctx.addIssue({
code: "custom",
path: ["humanEquivalent"],
message: 'required unless sideEffect is "none"',
});
}
});
export type CapabilityRecord = z.infer;