{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Agent Card",
  "description": "Agent card authored by developers (agent-card.json).",
  "type": "object",
  "required": ["identity", "capabilities", "skills", "runtime"],
  "additionalProperties": false,
  "$defs": {
    "catalogFormTypes": {
      "$comment": "FORM transport class — JSON-structured formats describable by a JSON Schema. Section A.1 (alphabetized).",
      "enum": [
        "application/geo+json",
        "application/json",
        "application/ld+json",
        "application/problem+json"
      ]
    },
    "catalogTextTypes": {
      "$comment": "TEXT transport class — text-serializable formats pasteable into a Textarea. Section A.2 (alphabetized).",
      "enum": [
        "application/javascript",
        "application/jsonl",
        "application/sql",
        "application/toml",
        "application/typescript",
        "application/vnd.google-earth.kml+xml",
        "application/x-ndjson",
        "application/x-toml",
        "application/x-yaml",
        "application/xml",
        "text/css",
        "text/csv",
        "text/html",
        "text/javascript",
        "text/markdown",
        "text/plain",
        "text/tab-separated-values",
        "text/x-c",
        "text/x-c++",
        "text/x-csharp",
        "text/x-go",
        "text/x-java-source",
        "text/x-python",
        "text/x-rust",
        "text/x-shellscript",
        "text/x-sql",
        "text/xml",
        "text/yaml"
      ]
    },
    "catalogFileTypes": {
      "$comment": "FILE transport class — binary/file-oriented formats plus catalog overrides for textual file types. Section A.3 (alphabetized; includes application/octet-stream).",
      "enum": [
        "application/epub+zip",
        "application/gpx+xml",
        "application/gzip",
        "application/java-archive",
        "application/msword",
        "application/octet-stream",
        "application/pdf",
        "application/rtf",
        "application/vnd.apache.arrow.file",
        "application/vnd.apache.arrow.stream",
        "application/vnd.apple.mpegurl",
        "application/vnd.google.protobuf",
        "application/vnd.ms-excel",
        "application/vnd.ms-powerpoint",
        "application/vnd.oasis.opendocument.presentation",
        "application/vnd.oasis.opendocument.spreadsheet",
        "application/vnd.oasis.opendocument.text",
        "application/vnd.openxmlformats-officedocument.presentationml.presentation",
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        "application/vnd.rar",
        "application/x-7z-compressed",
        "application/x-avro",
        "application/x-bzip2",
        "application/x-gzip",
        "application/x-latex",
        "application/x-msgpack",
        "application/x-parquet",
        "application/x-protobuf",
        "application/x-rar-compressed",
        "application/x-tar",
        "application/x-tex",
        "application/x-xz",
        "application/zip",
        "audio/aac",
        "audio/flac",
        "audio/midi",
        "audio/mp4",
        "audio/mpeg",
        "audio/ogg",
        "audio/opus",
        "audio/vnd.wave",
        "audio/wav",
        "audio/webm",
        "audio/x-midi",
        "audio/x-wav",
        "image/avif",
        "image/bmp",
        "image/gif",
        "image/heic",
        "image/heif",
        "image/jpeg",
        "image/png",
        "image/svg+xml",
        "image/tiff",
        "image/webp",
        "image/x-icon",
        "model/gltf+json",
        "model/gltf-binary",
        "model/obj",
        "model/stl",
        "text/rtf",
        "video/mp2t",
        "video/mp4",
        "video/ogg",
        "video/quicktime",
        "video/webm",
        "video/x-matroska",
        "video/x-msvideo"
      ]
    },
    "contentType": {
      "$comment": "Acceptance = catalog union ∪ family wildcards ∪ suffix patterns. anyOf so catalog entries that also match a family/suffix pattern do not cause rejection.",
      "anyOf": [
        { "$ref": "#/$defs/catalogFormTypes" },
        { "$ref": "#/$defs/catalogTextTypes" },
        { "$ref": "#/$defs/catalogFileTypes" },
        { "type": "string", "pattern": "^text/[a-z0-9!#$&^_.+-]{1,127}$" },
        { "type": "string", "pattern": "^image/[a-z0-9!#$&^_.+-]{1,127}$" },
        { "type": "string", "pattern": "^audio/[a-z0-9!#$&^_.+-]{1,127}$" },
        { "type": "string", "pattern": "^video/[a-z0-9!#$&^_.+-]{1,127}$" },
        { "type": "string", "pattern": "^[a-z][a-z0-9!#$&^_.-]{0,126}/[a-z0-9][a-z0-9!#$&^_.-]{0,126}\\+(json|xml|zip|gzip)$" }
      ]
    },
    "isFormClass": {
      "$comment": "Class=form iff in catalogFormTypes OR (*/*+json AND NOT in text/file catalogs).",
      "anyOf": [
        { "$ref": "#/$defs/catalogFormTypes" },
        {
          "allOf": [
            { "type": "string", "pattern": "^[a-z][a-z0-9!#$&^_.-]{0,126}/[a-z0-9][a-z0-9!#$&^_.-]{0,126}\\+json$" },
            { "not": { "$ref": "#/$defs/catalogTextTypes" } },
            { "not": { "$ref": "#/$defs/catalogFileTypes" } }
          ]
        }
      ]
    },
    "isTextClass": {
      "$comment": "Class=text iff in catalogTextTypes OR (text/* AND NOT in form/file catalogs) OR (*/*+xml AND NOT in form/file catalogs).",
      "anyOf": [
        { "$ref": "#/$defs/catalogTextTypes" },
        {
          "allOf": [
            { "type": "string", "pattern": "^text/" },
            { "not": { "$ref": "#/$defs/catalogFormTypes" } },
            { "not": { "$ref": "#/$defs/catalogFileTypes" } }
          ]
        },
        {
          "allOf": [
            { "type": "string", "pattern": "^[a-z][a-z0-9!#$&^_.-]{0,126}/[a-z0-9][a-z0-9!#$&^_.-]{0,126}\\+xml$" },
            { "not": { "$ref": "#/$defs/catalogFormTypes" } },
            { "not": { "$ref": "#/$defs/catalogFileTypes" } }
          ]
        }
      ]
    },
    "schemaPropertyType": {
      "enum": ["string", "number", "integer", "boolean", "object", "array", "null"]
    },
    "schemaProperty": {
      "$comment": "Closed keyword set matching io-schema-reference.md. Unknown keywords reject; `type` is required at every level.",
      "type": "object",
      "required": ["type"],
      "additionalProperties": false,
      "properties": {
        "type": { "$ref": "#/$defs/schemaPropertyType" },
        "title": { "type": "string" },
        "description": { "type": "string" },
        "default": {},
        "enum": { "type": "array" },
        "const": {},
        "required": { "type": "array", "items": { "type": "string" } },
        "minimum": { "type": "number" },
        "maximum": { "type": "number" },
        "minItems": { "type": "integer", "minimum": 0 },
        "maxItems": { "type": "integer", "minimum": 0 },
        "properties": {
          "type": "object",
          "additionalProperties": { "$ref": "#/$defs/schemaProperty" }
        },
        "items": { "$ref": "#/$defs/schemaProperty" }
      }
    },
    "acceptItem": {
      "anyOf": [
        { "$ref": "#/$defs/contentType" },
        { "enum": ["text/*", "image/*", "audio/*", "video/*"] }
      ]
    }
  },
  "properties": {
    "identity": {
      "type": "object",
      "required": ["agentName", "displayName", "description", "version", "provider"],
      "additionalProperties": false,
      "properties": {
        "agentName": {
          "type": "string",
          "pattern": "^[a-zA-Z0-9_]+$"
        },
        "displayName": { "type": "string" },
        "description": { "type": "string" },
        "version": { "type": "string" },
        "provider": {
          "type": "object",
          "required": ["organization"],
          "additionalProperties": false,
          "properties": {
            "organization": { "type": "string" },
            "url": { "type": "string" }
          }
        },
        "documentationUrl": { "type": "string" },
        "repositoryUrl": { "type": "string" },
        "iconUrl": { "type": "string" }
      }
    },
    "capabilities": {
      "type": "object",
      "required": ["taskKinds"],
      "additionalProperties": false,
      "properties": {
        "taskKinds": {
          "type": "array",
          "minItems": 1,
          "uniqueItems": true,
          "items": {
            "type": "string",
            "enum": ["request", "pipe"]
          }
        }
      }
    },
    "io": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "inputs": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["id", "contentType", "description", "required"],
            "additionalProperties": false,
            "properties": {
              "id": { "type": "string", "minLength": 1 },
              "description": { "type": "string", "minLength": 1 },
              "contentType": { "$ref": "#/$defs/contentType" },
              "required": { "type": "boolean" },
              "example": {},
              "schema": {},
              "accept": {
                "type": "array",
                "items": { "$ref": "#/$defs/acceptItem" }
              },
              "maxSizeBytes": {
                "type": "integer",
                "minimum": 1,
                "maximum": 26214400
              }
            },
            "allOf": [
              {
                "$comment": "FORM class: require schema+example, constrain schema shape, forbid accept+maxSizeBytes. Gated on contentType first passing $defs.contentType so invalid values (e.g. typos, charset parameters) do NOT trigger spurious class-invariant errors alongside the contentType rejection.",
                "if": {
                  "properties": {
                    "contentType": {
                      "allOf": [
                        { "$ref": "#/$defs/contentType" },
                        { "$ref": "#/$defs/isFormClass" }
                      ]
                    }
                  },
                  "required": ["contentType"]
                },
                "then": {
                  "required": ["schema", "example"],
                  "properties": {
                    "schema": {
                      "type": "object",
                      "required": ["type", "properties"],
                      "additionalProperties": false,
                      "properties": {
                        "type": { "const": "object" },
                        "title": { "type": "string" },
                        "description": { "type": "string" },
                        "required": { "type": "array", "items": { "type": "string" } },
                        "properties": {
                          "type": "object",
                          "additionalProperties": { "$ref": "#/$defs/schemaProperty" }
                        }
                      }
                    }
                  },
                  "not": {
                    "anyOf": [
                      { "required": ["accept"] },
                      { "required": ["maxSizeBytes"] }
                    ]
                  }
                }
              },
              {
                "$comment": "TEXT class: forbid schema, accept, maxSizeBytes. Gated on $defs/contentType.",
                "if": {
                  "properties": {
                    "contentType": {
                      "allOf": [
                        { "$ref": "#/$defs/contentType" },
                        { "$ref": "#/$defs/isTextClass" }
                      ]
                    }
                  },
                  "required": ["contentType"]
                },
                "then": {
                  "not": {
                    "anyOf": [
                      { "required": ["schema"] },
                      { "required": ["accept"] },
                      { "required": ["maxSizeBytes"] }
                    ]
                  }
                }
              },
              {
                "$comment": "FILE class (default — neither form nor text): forbid schema only; accept+maxSizeBytes permitted. Gated on $defs/contentType so typos/garbage don't fall into the default bucket.",
                "if": {
                  "properties": {
                    "contentType": {
                      "allOf": [
                        { "$ref": "#/$defs/contentType" },
                        { "not": { "$ref": "#/$defs/isFormClass" } },
                        { "not": { "$ref": "#/$defs/isTextClass" } }
                      ]
                    }
                  },
                  "required": ["contentType"]
                },
                "then": {
                  "not": { "required": ["schema"] }
                }
              }
            ]
          }
        },
        "outputs": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["id", "contentType", "guaranteed"],
            "additionalProperties": false,
            "properties": {
              "id": { "type": "string", "minLength": 1 },
              "description": { "type": "string", "minLength": 1 },
              "contentType": { "$ref": "#/$defs/contentType" },
              "guaranteed": { "type": "boolean" },
              "example": {},
              "schema": {}
            }
          }
        }
      }
    },
    "streams": {
      "type": "object",
      "additionalProperties": {
        "type": "object",
        "required": ["direction", "format"],
        "properties": {
          "direction": {
            "type": "string",
            "enum": ["outbound", "inbound", "bidirectional"]
          },
          "format": {
            "type": "string",
            "enum": ["events", "bytes"]
          },
          "description": { "type": "string" },
          "affinity": {
            "type": "string",
            "enum": ["shared", "dedicated"]
          },
          "schema": {},
          "outboundSchema": {},
          "inboundSchema": {},
          "contentType": { "type": "string" }
        },
        "allOf": [
          {
            "$comment": "Event streams: schema fields allowed, contentType not allowed",
            "if": { "properties": { "format": { "const": "events" } } },
            "then": {
              "not": { "required": ["contentType"] }
            }
          },
          {
            "$comment": "Byte streams: contentType allowed, schema fields not allowed",
            "if": { "properties": { "format": { "const": "bytes" } } },
            "then": {
              "not": {
                "anyOf": [
                  { "required": ["schema"] },
                  { "required": ["outboundSchema"] },
                  { "required": ["inboundSchema"] }
                ]
              }
            }
          },
          {
            "$comment": "Bidirectional event streams must have both directional schemas",
            "if": {
              "properties": {
                "direction": { "const": "bidirectional" },
                "format": { "const": "events" }
              }
            },
            "then": {
              "required": ["outboundSchema", "inboundSchema"],
              "not": { "required": ["schema"] }
            }
          },
          {
            "$comment": "Unidirectional event streams use schema, not directional schemas",
            "if": {
              "properties": {
                "direction": { "enum": ["outbound", "inbound"] },
                "format": { "const": "events" }
              }
            },
            "then": {
              "not": {
                "anyOf": [
                  { "required": ["outboundSchema"] },
                  { "required": ["inboundSchema"] }
                ]
              }
            }
          }
        ]
      },
      "$comment": "_default stream: affinity must not be shared",
      "properties": {
        "_default": {
          "type": "object",
          "not": {
            "properties": { "affinity": { "const": "shared" } },
            "required": ["affinity"]
          }
        }
      }
    },
    "security": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "encryption": {
          "type": "object",
          "required": ["required", "algorithm", "consumerKeyRequired"],
          "additionalProperties": false,
          "properties": {
            "required": { "type": "boolean" },
            "algorithm": { "type": "string" },
            "agentPublicKey": { "type": "string" },
            "agentKeyUrl": { "type": "string" },
            "consumerKeyRequired": { "type": "boolean" },
            "documentationUrl": { "type": "string" }
          }
        }
      }
    },
    "services": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "webhooks": { "type": "boolean" }
      }
    },
    "skills": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": ["id", "name"],
        "additionalProperties": false,
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "description": { "type": "string" },
          "examples": {
            "type": "array",
            "items": { "type": "string" }
          }
        }
      }
    },
    "extensions": {
      "type": "object",
      "additionalProperties": true
    },
    "runtime": {
      "type": "object",
      "required": ["handler"],
      "additionalProperties": false,
      "properties": {
        "handler": { "type": "string" },
        "handlerExport": { "type": "string" },
        "concurrency": { "type": "integer", "minimum": 1 },
        "expectedInstances": { "type": "integer", "minimum": 0 },
        "maxPendingBacklog": { "type": "integer", "minimum": 0 },
        "maxRunningTimeSec": { "type": "integer", "minimum": 1 }
      }
    }
  },
  "allOf": [
    {
      "$comment": "Request-only agents: streams (if present) must contain only _default, and at most one entry. This branch adds composition constraints (propertyNames, maxProperties) to the primary properties.streams definition above; the entry-shape (direction/format/affinity/...) is not redefined here and is inherited from the primary block via JSON Schema allOf composition. The shared-affinity ban for request tasks flows from the primary block's _default-forbidden-shared constraint, which forces _default.affinity to be 'dedicated' for every agent (including request-only) that declares _default.",
      "if": {
        "properties": {
          "capabilities": {
            "properties": {
              "taskKinds": {
                "const": ["request"]
              }
            }
          }
        }
      },
      "then": {
        "properties": {
          "streams": {
            "propertyNames": { "const": "_default" },
            "maxProperties": 1
          }
        }
      }
    }
  ]
}
