{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://docs.unoverse.ai/schemas/nodes/interface.schema.json",
  "title": "Unoverse node interface",
  "description": "interface.yaml — the node's WIRING SURFACE, and nothing else. Everything another node, an agent, or a developer needs in order to connect to this one: what goes in, what comes out, what services it offers or consumes, and what credentials it needs.\n\nIt has its own file because this is the question asked most often about a node, and it should never require reading past a node's branding to answer. See docs/architecture/authoring/DECLARATIVE_NODES.md §5.",
  "type": "object",
  "properties": {
    "$schema": {
      "type": "string"
    },
    "inputs": {
      "type": "array",
      "description": "Ports that accept data from upstream nodes. Absent means the node is a trigger or takes no wired input. A streaming or looping node also declares a CONTINUE signal port here, and that port is what makes it a CallbackNode.",
      "items": {
        "$ref": "_defs.schema.json#/definitions/port"
      }
    },
    "outputs": {
      "type": "array",
      "description": "Ports this node emits on. A streaming node emits on several: incremental output during the run, and its settled result at the end (see api.yaml `response.events` and `response.finalize`).\n\nAN ANNOTATION NODE MAY DECLARE NONE. `Note` is a markdown sticky on the canvas: no inputs, no outputs, no api. Having no INPUTS is what keeps it out of the data flow (the graph can never reach it), which is why it needs no outputs either — a dot that could never carry anything. Lint still refuses a node that has inputs but no outputs, because there something reaches it and the data then stops dead.",
      "items": {
        "$ref": "_defs.schema.json#/definitions/port"
      }
    },
    "credentials": {
      "type": "array",
      "description": "Credential TYPES this node needs. Values never appear here. The type is declared once per package in credentials/<name>.yaml, and the executor resolves it at run time.",
      "items": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Must match a credential declared in this package's credentials/ folder. Lint enforces it."
          },
          "type": {
            "type": "string",
            "description": "Credential type id, when it differs from `name`."
          },
          "required": {
            "type": "boolean",
            "default": true
          },
          "displayName": {
            "type": "string"
          },
          "description": {
            "type": "string"
          }
        },
        "additionalProperties": false
      }
    },
    "attaches": {
      "type": "object",
      "description": "ATTACHMENT NODES ONLY (kind: Attachment). What this attachment does to the node it is clipped onto.\n\nAn attachment is configuration that arrives from outside a node and LOCKS what it owns. It never runs, so it has no inputs, no outputs and no api/run.yaml; declaring any of those alongside `attaches` is a lint error. The host reads `config` exactly as it always has and never learns an attachment existed.\n\nCOMPATIBILITY IS THE FIELDS, not a vocabulary. A node accepts this attachment when its configSchema declares every property in `sets`. There is no `accepts:` list on the host to keep in sync and no enum of kinds to widen, which is the whole reason the mechanism generalises past its first two cases (a phone line on a voice node, a guardrail on an LLM node).\n\nSee docs/architecture/authoring/DECLARATIVE_NODES.md §14.",
      "required": [
        "sets"
      ],
      "additionalProperties": false,
      "properties": {
        "sets": {
          "type": "object",
          "minProperties": 1,
          "description": "The host config fields this attachment OWNS while it is clipped on, as literal values.\n\nONE OWNER PER VALUE. These go read-only on the host's Configuration tab, attributed to this attachment, so there is no way to clip a phone line on and then quietly set the sample rate back. Peel the attachment off and they unlock at their own defaults.\n\nEvery field named here must exist in the host's configSchema, which is also what decides whether the two can clip together at all. Values are literals on purpose: anything variable belongs in this attachment's own config.yaml, which reaches the run under `attachments.<type>` and stays out of the lock."
        },
        "credentials": {
          "type": "array",
          "description": "Credential TYPES contributed to the HOST's run, alongside the host's own. Same shape as `credentials` above, and the same rule: the type is declared once per package in credentials/<name>.yaml and values never appear here.\n\nSo a voice node with a phone line clipped on runs holding two credentials, its own vendor key and the carrier key it inherited, and neither node writes into the other. The picker for this one lives on the tab this attachment contributes.",
          "items": {
            "type": "object",
            "required": [
              "name"
            ],
            "properties": {
              "name": {
                "type": "string",
                "description": "Must match a credential declared in this package's credentials/ folder. Lint enforces it."
              },
              "type": {
                "type": "string",
                "description": "Credential type id, when it differs from `name`."
              },
              "required": {
                "type": "boolean",
                "default": true
              },
              "displayName": {
                "type": "string"
              },
              "description": {
                "type": "string"
              }
            },
            "additionalProperties": false
          }
        },
        "capabilities": {
          "type": "object",
          "additionalProperties": false,
          "description": "Engine-level behaviour the HOST takes on while this is clipped to it. Same meaning as a node's own `capabilities`, contributed the way `credentials` are.\n\nIt exists for one case, and the case is the argument. A voice node in a browser emits nothing outside the platform, so it rightly declares no `emitsExternally`. Clip a phone line onto it and the very same node dials a real human being — and a build loop re-runs a workflow after every stage, so that is a real call to a real person once per attempt.\n\n`emitsExternally` is read off the node TYPE's definition, so it cannot vary per instance, and `sets` cannot reach it because it is a capability rather than a config field. This is the channel for it: the host is treated as emitting externally for as long as the attachment is clipped on, and a test run traces the call instead of placing it.\n\nIt only ever TIGHTENS. An attachment cannot turn a host's capability off, because an attachment is a thing someone added and removing a protection is not something adding a thing should be able to do.",
          "properties": {
            "emitsExternally": {
              "const": true,
              "description": "While clipped on, the host EMITS outside the platform and a test run must withhold it. Only ever true: absent says nothing, and false would be a claim this cannot make (the host may emit for its own reasons)."
            }
          }
        }
      }
    },
    "serviceConnectors": {
      "type": "array",
      "description": "Service wiring between nodes in a workflow, drawn as the node's top and bottom handles. `isService: true` means this node PROVIDES a capability to others; false means it CONSUMES one granted by an upstream provider.\n\nA provider node has TWO INDEPENDENT CHANNELS that do not cross: service calls answer an agent and do NOT fire the node's outputs, while a graph trigger fires the outputs and does not answer an agent (mcp-services.md).\n\nUnrelated to api.yaml, which describes this node's own upstream HTTP call.",
      "items": {
        "type": "object",
        "required": [
          "name",
          "serviceType",
          "isService"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "serviceType": {
            "enum": [
              "mcp",
              "embedding"
            ],
            "description": "IMPLEMENTED service types only."
          },
          "isService": {
            "type": "boolean"
          },
          "methods": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Consumers only: the provider methods this node calls."
          },
          "instructions": {
            "type": "string",
            "description": "Providers only: usage protocol handed to the calling agent, for tools whose correct use is not obvious from the schema alone (11-agent-skills.md)."
          },
          "description": {
            "type": "string"
          }
        },
        "additionalProperties": false
      }
    }
  },
  "additionalProperties": false
}
