Typed capabilities
Every agent in a fleet declares its callable surface in
capabilities.yaml. Starting with v1.2, each capability can attach
a JSON Schema on both sides — the runtime validates outbound payloads
before the envelope hits the wire, and validates inbound responses
before handing them back to the caller's skill.
Introduced by Enterprise Production Plan #11. Capabilities without schemas fall through to legacy loose-JSON semantics — no regression for fleets that predate v1.2.
Shape
capabilities.yaml (one per agent, alongside agent.yaml):
version: 1
agent: agent://pr-reviewer
transports:
- kind: memory
topics:
requests: agents.pr-reviewer.requests
capabilities:
- name: review-pr
description: Review a GitHub pull request.
timeoutMs: 60000
idempotent: true
since: "1.1.0"
inputSchema:
type: object
properties:
prUrl: { type: string, description: "GitHub PR URL." }
severity: { enum: [low, med, high] }
required: [prUrl, severity]
additionalProperties: false
outputSchema:
type: object
properties:
verdict: { enum: [approve, request-changes, comment] }
summary: { type: string }
required: [verdict, summary]
Both inputSchema and outputSchema are optional. When a
capability omits them, the runtime skips validation for that side —
this is the v1.1-compatible "loose JSON" path.
Supported JSON Schema keywords
The runtime ships a draft-07 subset tuned for capability payloads. Anything outside this list is rejected at load time so the agent never starts with a schema it can't enforce:
type:object | array | string | integer | number | boolean | nullenum,constproperties,required,additionalPropertiesitems(single schema; tuple form is not supported)minimum/maximum/exclusiveMinimum/exclusiveMaximumminLength/maxLength/patternminItems/maxItems/uniqueItemsoneOf/anyOf/allOf/not$ref(local only —#/definitions/<name>)format:uuid | email | uri | uri-reference | date-timetitle,description,default,examples,definitions
If your schema grows beyond this subset, open an issue before adopting
AJV directly — the CapabilityValidator contract is the seam we'll
widen.
Codegen
Callers generate TypeScript types from the callee's declaration:
declaragent capabilities gen --peer pr-reviewer
# ✓ wrote generated/pr-reviewer.ts (1 capability)
The emitted file contains per-capability Request / Response types
plus an aggregate Capabilities interface:
// AUTO-GENERATED by `declaragent capabilities gen`.
// Source: agent://pr-reviewer
// Capabilities: review-pr
// DO NOT EDIT — re-run the command to refresh.
export type ReviewPrRequest = {
prUrl: string;
severity: "low" | "med" | "high";
};
export type ReviewPrResponse = {
verdict: "approve" | "request-changes" | "comment";
summary: string;
/* … */
};
export interface Capabilities {
"review-pr": { request: ReviewPrRequest; response: ReviewPrResponse };
}
The emitter is deterministic — the same schema produces
byte-identical output across runs. That makes generated/ safe to
check into the caller's repo and review via git diff when the callee
bumps its schema.
Flags:
--peer <id>— readagents/<id>/capabilities.yamlunder the current fleet root.--capabilities <path>— read a capability file at an arbitrary path (useful for out-of-fleet peers).--out <dir>— override the output directory (defaultgenerated/).--json— machine-readable summary.
Runtime validation
When a skill invokes RequestAgent, the runtime:
-
Looks up the target peer's capability in the caller's loaded capability table.
-
If the capability declares
inputSchema, validatespayloadagainst it. On failure, the tool result is:{ status: "schema-violation",schemaSide: "request",violations: [{ path: "/severity", message: "value not in enum […]" }],error: { code: "EAGENTRPC_SCHEMA_VIOLATION", … } }No envelope goes on the wire — the caller sees the violation before the broker hop.
-
If the request passes, publishes and awaits the response.
-
On return, if the capability declares
outputSchema, validatesresponse. A failure surfaces the sameschema-violationshape withschemaSide: "response"and the rawresponsepreserved for debugging.
Auditing
Every violation emits a capability_schema_violation record on the
local SQLite audit chain:
interface CapabilitySchemaViolationAuditRecord {
kind: "capability_schema_violation";
ts: number;
tenantId: string;
capabilityName: string;
peerId: string; // agent://...
side: "request" | "response";
violations: Array<{ path: string; message: string }>;
correlationId: string;
sessionId?: string;
}
Cardinality: one record per envelope, carrying every violation
for that payload. Downstream consumers (SIEM export loop, audit query --kind capability_schema_violation) can drill into
violations[] without scaling linearly with the number of bad
fields.
Back-compat
- Capabilities without
inputSchema/outputSchemabehave exactly like v1.1 — the runtime never invokes the validator registry on either side. - Callers built against v1.1 continue to run; wiring the new
peerCapabilities+validatorsoptions oncreateRequestAgentToolis opt-in. parseCapabilitiesConfig(raw, { validateSchemas: false })skips the load-time compile check — useful when loading a peer-published schema for inspection only.
Open questions (v1.3)
- Schema versioning. When a callee bumps a capability's schema in
a breaking way, callers currently hard-fail on the first request.
The v1.3 plan is a
since+minSchemaVersionpair analogous tox-fleet-version. - Cross-language codegen. Only TypeScript today. Python and Go emitters are planned when the first non-Node consumer ships.