Skip to main content

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.

Since v1.2

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 | null
  • enum, const
  • properties, required, additionalProperties
  • items (single schema; tuple form is not supported)
  • minimum / maximum / exclusiveMinimum / exclusiveMaximum
  • minLength / maxLength / pattern
  • minItems / maxItems / uniqueItems
  • oneOf / anyOf / allOf / not
  • $ref (local only — #/definitions/<name>)
  • format: uuid | email | uri | uri-reference | date-time
  • title, 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> — read agents/<id>/capabilities.yaml under 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 (default generated/).
  • --json — machine-readable summary.

Runtime validation

When a skill invokes RequestAgent, the runtime:

  1. Looks up the target peer's capability in the caller's loaded capability table.

  2. If the capability declares inputSchema, validates payload against 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.

  3. If the request passes, publishes and awaits the response.

  4. On return, if the capability declares outputSchema, validates response. A failure surfaces the same schema-violation shape with schemaSide: "response" and the raw response preserved 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 / outputSchema behave 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 + validators options on createRequestAgentTool is 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 + minSchemaVersion pair analogous to x-fleet-version.
  • Cross-language codegen. Only TypeScript today. Python and Go emitters are planned when the first non-Node consumer ships.