{
  "openapi": "3.1.0",
  "info": {
    "title": "Dashboard HTTP API",
    "version": "1",
    "description": "HTTP interface of the @cotal-ai/web reference implementation. This description covers seven read operations; it does not establish a separate stable product or Cloud management API. Start the dashboard using the web service preset described in the operator guide. The server defaults to 127.0.0.1:7799; use the host and port configured for your dashboard.\n\nOpen the owner-private launch URL once to exchange its k query token for the cotal_web_session cookie. Keep the session on the same host. With the default bind, the permitted browser origins are http://127.0.0.1:<port>, http://localhost:<port> and http://cotal.localhost:<port>. A custom host uses its own HTTP origin. A cross-origin request is refused before session checks. The readiness nonce only opens /api/meta.\n\nAll documented operations use GET and take no request body. Some read handlers also accept other bodyless methods; this description does not promise a 405 response for them. Internal readiness and channel deletion are outside this read reference. The native web service is read-only. Description version 1 identifies this document, not the wire or npm package version.",
    "license": {
      "name": "Apache-2.0"
    }
  },
  "servers": [
    {
      "url": "http://{host}:{port}",
      "description": "Your dashboard listener. Broker TLS does not encrypt this HTTP connection.",
      "variables": {
        "host": {
          "default": "127.0.0.1"
        },
        "port": {
          "default": "7799"
        }
      }
    }
  ],
  "security": [
    {
      "DashboardSession": []
    },
    {
      "LaunchToken": []
    }
  ],
  "externalDocs": {
    "description": "Dashboard setup and access limits",
    "url": "https://docs.cotal.ai/watch-a-mesh/#web-dashboard"
  },
  "paths": {
    "/api/roster": {
      "get": {
        "operationId": "list-roster",
        "summary": "List the roster",
        "description": "Return the dashboard observer's current presence roster. Rows can include offline peers; freshness is reported separately by the presence-view SSE event.",
        "responses": {
          "200": {
            "description": "The current snapshot or history page.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/RosterEntry"
                  }
                }
              }
            }
          },
          "302": {
            "$ref": "#/components/responses/LaunchExchange"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/CrossOrigin"
          },
          "413": {
            "$ref": "#/components/responses/BodyRefused"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/membership": {
      "get": {
        "operationId": "read-membership",
        "summary": "Read channel membership",
        "description": "Return broker-sourced live and durable channel membership. asOf is absent when no membership heartbeat has been written. Use this for display, not authorization.",
        "responses": {
          "200": {
            "description": "The current snapshot or history page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Membership"
                }
              }
            }
          },
          "302": {
            "$ref": "#/components/responses/LaunchExchange"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/CrossOrigin"
          },
          "413": {
            "$ref": "#/components/responses/BodyRefused"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/MembershipUnavailable"
          }
        }
      }
    },
    "/api/channels": {
      "get": {
        "operationId": "list-channels",
        "summary": "List chat channels",
        "description": "List chat channels with message counts and effective replay and delivery policies. Event channels are excluded. description and replayWindow are absent when unset.",
        "responses": {
          "200": {
            "description": "The current snapshot or history page.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Channel"
                  }
                }
              }
            }
          },
          "302": {
            "$ref": "#/components/responses/LaunchExchange"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/CrossOrigin"
          },
          "413": {
            "$ref": "#/components/responses/BodyRefused"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/activity": {
      "get": {
        "operationId": "read-activity",
        "summary": "Read recent activity",
        "description": "Merge the newest chat messages selected by stream sequence with recent direct messages, order by message timestamp and retain the newest limit. partial, missing and reasons identify sources that failed or exceeded the shared 8000 ms deadline. A partial page is still HTTP 200. Failure to obtain the channel list is HTTP 500.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "allowEmptyValue": true,
            "description": "Maximum requested messages. Absent or empty uses 200. Accepts decimal digits naming a safe integer, including 0; negative, fractional, whitespace-padded and unsafe values are refused.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 9007199254740991,
              "default": 200
            },
            "example": 20
          }
        ],
        "responses": {
          "200": {
            "description": "The current snapshot or history page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActivityPage"
                }
              }
            }
          },
          "302": {
            "$ref": "#/components/responses/LaunchExchange"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/CrossOrigin"
          },
          "413": {
            "$ref": "#/components/responses/BodyRefused"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/dms": {
      "get": {
        "operationId": "read-dms",
        "summary": "Read direct-message history",
        "description": "Return recent direct messages visible to the dashboard observer. A failed or late read returns HTTP 503 instead of a partial array.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "allowEmptyValue": true,
            "description": "Maximum requested messages. Absent or empty uses 500. Accepts decimal digits naming a safe integer, including 0; negative, fractional, whitespace-padded and unsafe values are refused.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 9007199254740991,
              "default": 500
            },
            "example": 20
          }
        ],
        "responses": {
          "200": {
            "description": "The current snapshot or history page.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Message"
                  }
                }
              }
            }
          },
          "302": {
            "$ref": "#/components/responses/LaunchExchange"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/CrossOrigin"
          },
          "413": {
            "$ref": "#/components/responses/BodyRefused"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ReadUnavailable"
          }
        }
      }
    },
    "/api/channels/{channel}/history": {
      "get": {
        "operationId": "read-channel-history",
        "summary": "Read channel history",
        "description": "Percent-decode the channel once and validate it using the core channel grammar. Invalid names are refused without rewriting them to another channel. A failed or late read returns HTTP 503.",
        "parameters": [
          {
            "name": "channel",
            "in": "path",
            "required": true,
            "description": "Channel name or pattern accepted by the core channel grammar. Encode it as one URL path component.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 4096
            },
            "example": "general"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "allowEmptyValue": true,
            "description": "Maximum requested messages. Absent or empty uses 200. Accepts decimal digits naming a safe integer, including 0; negative, fractional, whitespace-padded and unsafe values are refused.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 9007199254740991,
              "default": 200
            },
            "example": 20
          }
        ],
        "responses": {
          "200": {
            "description": "The current snapshot or history page.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Message"
                  }
                }
              }
            }
          },
          "302": {
            "$ref": "#/components/responses/LaunchExchange"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/CrossOrigin"
          },
          "413": {
            "$ref": "#/components/responses/BodyRefused"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ReadUnavailable"
          }
        }
      }
    },
    "/feed": {
      "get": {
        "operationId": "watch-feed",
        "summary": "Watch live dashboard events",
        "description": "Open a Server-Sent Events stream. Each frame has event: <name> and data: <JSON>, followed by a blank line. The initial frames contain roster and presence-view, then membership or membership-read-failed. Later frames carry roster, presence-view, membership, membership-read-failed and message updates. The stream provides no event IDs or replay cursor; reconnect and read history for backfill. An unhandled request failure after headers have been sent closes the stream. Membership read failures are sent as events and leave the stream open. The schemas under x-sse-events describe decoded data payloads.\n\n| Event | JSON data |\n|---|---|\n| `roster` | Array of presence records, with optional dashboard progress. |\n| `presence-view` | `state` and `fresh`, plus `staleSince` for a stale observer. |\n| `membership` | Optional `asOf` and an array of `members` with live and durable channels. |\n| `membership-read-failed` | An object with a `reason` string. |\n| `message` | `mode` and `msg`, with subject-derived `senderId` and chat `channel` when available. |\n\nDecoded payload schemas are also included under `x-sse-events` in the [downloadable specification](https://docs.cotal.ai/dashboard.openapi.json).\n\nThe message event forwards publisher JSON without wire-message validation. Treat msg.from as advisory; senderId is derived from the broker subject. A malformed publisher payload can still appear in the feed.",
        "responses": {
          "200": {
            "description": "An open UTF-8 SSE stream. Authentication and origin checks run before streaming begins.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-cache"
                }
              }
            },
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                },
                "example": "event: roster\ndata: []\n\nevent: presence-view\ndata: {\"state\":\"current\",\"fresh\":true}\n\n"
              }
            }
          },
          "302": {
            "$ref": "#/components/responses/LaunchExchange"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/CrossOrigin"
          },
          "413": {
            "$ref": "#/components/responses/BodyRefused"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-sse-events": {
          "roster": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RosterEntry"
            }
          },
          "presence-view": {
            "$ref": "#/components/schemas/PresenceView"
          },
          "membership": {
            "$ref": "#/components/schemas/Membership"
          },
          "membership-read-failed": {
            "type": "object",
            "properties": {
              "reason": {
                "type": "string"
              }
            },
            "required": [
              "reason"
            ]
          },
          "message": {
            "$ref": "#/components/schemas/LiveMessage"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "DashboardSession": {
        "type": "apiKey",
        "in": "cookie",
        "name": "cotal_web_session",
        "description": "Process-local dashboard session issued by the one-use launch URL."
      },
      "LaunchToken": {
        "type": "apiKey",
        "in": "query",
        "name": "k",
        "description": "One-use bootstrap token from the owner-private launch URL. It returns HTTP 302 with a session cookie rather than the requested data."
      }
    },
    "schemas": {
      "Message": {
        "$ref": "#/components/schemas/WireCotalMessage"
      },
      "RosterEntry": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WirePresence"
          },
          {
            "type": "object",
            "properties": {
              "progress": {
                "type": "object",
                "properties": {
                  "kind": {
                    "const": "unknown",
                    "type": "string"
                  }
                },
                "required": [
                  "kind"
                ]
              }
            }
          }
        ],
        "description": "Presence plus an optional progress signal. Working rows currently report progress.kind as unknown because the dashboard has no session-store observer."
      },
      "Membership": {
        "type": "object",
        "properties": {
          "asOf": {
            "type": "number"
          },
          "members": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "live": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "durable": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "observedAt": {
                  "type": "number"
                }
              },
              "required": [
                "id",
                "live",
                "durable",
                "observedAt"
              ]
            }
          }
        },
        "required": [
          "members"
        ]
      },
      "Channel": {
        "type": "object",
        "properties": {
          "channel": {
            "type": "string"
          },
          "messages": {
            "type": "integer",
            "minimum": 0
          },
          "description": {
            "type": "string"
          },
          "replay": {
            "type": "boolean"
          },
          "replayWindow": {
            "type": "string"
          },
          "deliveryClass": {
            "type": "string"
          }
        },
        "required": [
          "channel",
          "messages",
          "replay",
          "deliveryClass"
        ]
      },
      "ActivityEntry": {
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "mode": {
                "const": "chat",
                "type": "string"
              },
              "channel": {
                "type": "string"
              },
              "msg": {
                "$ref": "#/components/schemas/Message"
              }
            },
            "required": [
              "mode",
              "channel",
              "msg"
            ]
          },
          {
            "type": "object",
            "properties": {
              "mode": {
                "const": "unicast",
                "type": "string"
              },
              "msg": {
                "$ref": "#/components/schemas/Message"
              }
            },
            "required": [
              "mode",
              "msg"
            ]
          }
        ]
      },
      "ActivityPage": {
        "type": "object",
        "properties": {
          "entries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ActivityEntry"
            }
          },
          "partial": {
            "type": "boolean"
          },
          "read": {
            "type": "integer",
            "minimum": 0,
            "maximum": 2
          },
          "of": {
            "type": "integer",
            "const": 2
          },
          "missing": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "chat",
                "direct messages"
              ]
            }
          },
          "reasons": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "deadlineMs": {
            "type": "integer",
            "const": 8000
          }
        },
        "required": [
          "entries",
          "partial",
          "read",
          "of",
          "missing",
          "reasons",
          "deadlineMs"
        ]
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "AuthenticationError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "unauthenticated",
              "launch-token-already-used"
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "OriginError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "const": "cross-origin"
          }
        },
        "required": [
          "error"
        ]
      },
      "MembershipError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "const": "membership-read-failed"
          },
          "reason": {
            "type": "string"
          }
        },
        "required": [
          "error",
          "reason"
        ]
      },
      "PresenceView": {
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "state": {
                "const": "current",
                "type": "string"
              },
              "fresh": {
                "const": true,
                "type": "boolean"
              }
            },
            "required": [
              "state",
              "fresh"
            ]
          },
          {
            "type": "object",
            "properties": {
              "state": {
                "const": "unpopulated",
                "type": "string"
              },
              "fresh": {
                "const": false,
                "type": "boolean"
              }
            },
            "required": [
              "state",
              "fresh"
            ]
          },
          {
            "type": "object",
            "properties": {
              "state": {
                "const": "stale",
                "type": "string"
              },
              "fresh": {
                "const": false,
                "type": "boolean"
              },
              "staleSince": {
                "type": "number"
              }
            },
            "required": [
              "state",
              "fresh",
              "staleSince"
            ]
          }
        ]
      },
      "LiveMessage": {
        "type": "object",
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "chat",
              "unicast",
              "anycast"
            ]
          },
          "senderId": {
            "type": "string"
          },
          "channel": {
            "type": "string"
          },
          "msg": {
            "description": "Unvalidated publisher JSON forwarded by the dashboard tap. Well-formed wire messages use the Message schema in this description. Consumers must validate the payload before treating it as a wire message; msg.from is advisory and senderId comes from the broker subject."
          }
        },
        "required": [
          "mode",
          "msg"
        ]
      },
      "WireCotalMessage": {
        "description": "A message on the mesh (chat / direct message for now; extensible to other families).",
        "oneOf": [
          {
            "properties": {
              "channel": {
                "description": "Channel name — multicast (broadcast to everyone on the channel).",
                "type": "string"
              },
              "contextId": {
                "description": "Conversation / thread correlation id.",
                "type": "string"
              },
              "from": {
                "$ref": "#/components/schemas/WireEndpointRef"
              },
              "id": {
                "description": "Unique message id.",
                "type": "string"
              },
              "mentions": {
                "description": "Lowercased peer names called out within a `channel` message — a wake hint that also, on a `live` channel, routes a durable copy to each mentioned target **authorized to read that channel** (SPEC §4/§5). It never carries content outside the target's read ACL and is not a routing substitute for `channel`/`to`; the message still multicasts to the whole channel. Omitted when empty.",
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "parts": {
                "items": {
                  "$ref": "#/components/schemas/WirePart"
                },
                "type": "array"
              },
              "replyTo": {
                "description": "Id of the message being replied to.",
                "type": "string"
              },
              "space": {
                "type": "string"
              },
              "ts": {
                "description": "Epoch ms.",
                "type": "number"
              }
            },
            "required": [
              "channel",
              "from",
              "id",
              "parts",
              "space",
              "ts"
            ],
            "type": "object",
            "not": {
              "anyOf": [
                {
                  "required": [
                    "to"
                  ]
                },
                {
                  "required": [
                    "toService"
                  ]
                }
              ]
            }
          },
          {
            "properties": {
              "contextId": {
                "description": "Conversation / thread correlation id.",
                "type": "string"
              },
              "from": {
                "$ref": "#/components/schemas/WireEndpointRef"
              },
              "id": {
                "description": "Unique message id.",
                "type": "string"
              },
              "mentions": {
                "description": "Lowercased peer names called out within a `channel` message — a wake hint that also, on a `live` channel, routes a durable copy to each mentioned target **authorized to read that channel** (SPEC §4/§5). It never carries content outside the target's read ACL and is not a routing substitute for `channel`/`to`; the message still multicasts to the whole channel. Omitted when empty.",
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "parts": {
                "items": {
                  "$ref": "#/components/schemas/WirePart"
                },
                "type": "array"
              },
              "replyTo": {
                "description": "Id of the message being replied to.",
                "type": "string"
              },
              "space": {
                "type": "string"
              },
              "to": {
                "description": "Instance id — unicast (direct to one specific endpoint).",
                "type": "string"
              },
              "ts": {
                "description": "Epoch ms.",
                "type": "number"
              }
            },
            "required": [
              "from",
              "id",
              "parts",
              "space",
              "to",
              "ts"
            ],
            "type": "object",
            "not": {
              "anyOf": [
                {
                  "required": [
                    "channel"
                  ]
                },
                {
                  "required": [
                    "toService"
                  ]
                }
              ]
            }
          },
          {
            "properties": {
              "contextId": {
                "description": "Conversation / thread correlation id.",
                "type": "string"
              },
              "from": {
                "$ref": "#/components/schemas/WireEndpointRef"
              },
              "id": {
                "description": "Unique message id.",
                "type": "string"
              },
              "mentions": {
                "description": "Lowercased peer names called out within a `channel` message — a wake hint that also, on a `live` channel, routes a durable copy to each mentioned target **authorized to read that channel** (SPEC §4/§5). It never carries content outside the target's read ACL and is not a routing substitute for `channel`/`to`; the message still multicasts to the whole channel. Omitted when empty.",
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "parts": {
                "items": {
                  "$ref": "#/components/schemas/WirePart"
                },
                "type": "array"
              },
              "replyTo": {
                "description": "Id of the message being replied to.",
                "type": "string"
              },
              "space": {
                "type": "string"
              },
              "toService": {
                "description": "Service / role — anycast (any one instance of the service receives it).",
                "type": "string"
              },
              "ts": {
                "description": "Epoch ms.",
                "type": "number"
              }
            },
            "required": [
              "from",
              "id",
              "parts",
              "space",
              "toService",
              "ts"
            ],
            "type": "object",
            "not": {
              "anyOf": [
                {
                  "required": [
                    "channel"
                  ]
                },
                {
                  "required": [
                    "to"
                  ]
                }
              ]
            }
          }
        ]
      },
      "WireEndpointRef": {
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "role": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name"
        ],
        "type": "object"
      },
      "WireExtensionPartKind": {
        "description": "Reverse-DNS extension part kind, e.g. `com.acme.snapshot`.",
        "pattern": "^[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)+$",
        "type": "string"
      },
      "WireJsonValue": {
        "anyOf": [
          {
            "type": "null"
          },
          {
            "type": "boolean"
          },
          {
            "type": "number"
          },
          {
            "type": "string"
          },
          {
            "items": {
              "$ref": "#/components/schemas/WireJsonValue"
            },
            "type": "array"
          },
          {
            "additionalProperties": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/WireJsonValue"
                },
                {
                  "not": {}
                }
              ]
            },
            "type": "object"
          }
        ],
        "description": "Any JSON value (SPEC §5): `null`, a boolean, a number, a string, an array, or an object whose members are JSON values. `undefined` is not a JSON value — `JSON.stringify` omits the key carrying it — so it is excluded at the top level, where it would drop the whole `data` key; an optional member is allowed because serialization drops just that key."
      },
      "WirePart": {
        "oneOf": [
          {
            "properties": {
              "kind": {
                "const": "text",
                "type": "string"
              },
              "text": {
                "type": "string"
              }
            },
            "required": [
              "kind",
              "text"
            ],
            "type": "object"
          },
          {
            "properties": {
              "data": {
                "$ref": "#/components/schemas/WireJsonValue"
              },
              "kind": {
                "const": "data",
                "type": "string"
              }
            },
            "required": [
              "kind",
              "data"
            ],
            "type": "object"
          },
          {
            "description": "Reference to bytes in the space's artifact store — SPEC §5's reserved slot, now defined. Bare (not reverse-DNS) because it is Cotal's own core primitive: extension kinds wrap EXTERNAL vocabularies, core kinds are the standard's own. The full contract, the guard, and the object-store digest boundary live in `artifact.ts`.",
            "properties": {
              "digest": {
                "description": "`sha256:<hex>` over the raw bytes — the artifact's identity. The only self-verifying field.",
                "type": "string",
                "pattern": "^sha256:[0-9a-f]{64}$"
              },
              "kind": {
                "const": "artifact",
                "type": "string"
              },
              "mediaType": {
                "description": "MIME type. A publisher's claim.",
                "type": "string"
              },
              "name": {
                "description": "Human name, e.g. `coverage-report.html`. A publisher's claim, not a checked fact.",
                "type": "string"
              },
              "size": {
                "description": "Size in bytes. A publisher's claim: a receiver must never preallocate from it.",
                "type": "integer",
                "minimum": 0
              }
            },
            "required": [
              "kind",
              "name",
              "mediaType",
              "digest",
              "size"
            ],
            "type": "object"
          },
          {
            "additionalProperties": {},
            "properties": {
              "kind": {
                "$ref": "#/components/schemas/WireExtensionPartKind"
              }
            },
            "required": [
              "kind"
            ],
            "type": "object"
          }
        ]
      },
      "WireAgentCard": {
        "description": "A2A-inspired identity record for an endpoint or agent.",
        "properties": {
          "actor": {
            "description": "The agent-instance actor token under  {@link  owner }  (server-derived from the spawn ledger in user mode; the connection id in the dev default). The other half of  {@link  id } .",
            "type": "string"
          },
          "description": {
            "description": "A2A-style one-line summary of what this agent does (discovery / observability).",
            "type": "string"
          },
          "id": {
            "description": "Unique, stable for the lifetime of this connection. The owner+actor **principal dot-form** `<owner>.<actor>` (= `principalKey().key`) under the owner+actor grammar — the wire identity every `msg.from.id` carries and every sender guard compares against. Peers address each other by this.",
            "type": "string"
          },
          "kind": {
            "$ref": "#/components/schemas/WireEndpointKind",
            "description": "'agent' (participates in coordination) or a plain 'endpoint' (logger, dashboard…)."
          },
          "meta": {
            "additionalProperties": {},
            "description": "Free-form advisory display metadata. Reserved flat string keys are `connector`, `model`, `provider` (the effective provider the connector reported), `host`, `cwd`, `repo`, `branch`, `head`, `sessionKind`, and `sessionId`.",
            "type": "object"
          },
          "name": {
            "description": "Human-readable display name.",
            "type": "string"
          },
          "owner": {
            "description": "The human/account owner token this agent acts under (opaque, per-space; `\"local\"` in the no-login dev default). One of the two halves of  {@link  id } ; travels in presence so peers resolve name→principal.",
            "type": "string"
          },
          "protocolVersion": {
            "description": "Wire-contract version this participant speaks (the SPEC.md version, `\"0.2\"` today). A change signal, not negotiation: v0 has none, but a peer can detect a mismatch instead of silently misreading a future envelope. Omitted ⇒ assume the v0.x line.",
            "type": "string"
          },
          "role": {
            "description": "Cotal addition: the role this participant plays (planner, reviewer, …).",
            "type": "string"
          },
          "skills": {
            "items": {
              "$ref": "#/components/schemas/WireAgentSkill"
            },
            "type": "array"
          },
          "tags": {
            "description": "Cotal: free-form \"what it can do\" tags (A2A skill-tags, flattened) — discovery only.",
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "required": [
          "id",
          "name",
          "kind"
        ],
        "type": "object"
      },
      "WireAgentSkill": {
        "properties": {
          "description": {
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name"
        ],
        "type": "object"
      },
      "WireAttentionMode": {
        "description": "How aggressively peer traffic interrupts an agent — chosen by the agent, orthogonal to  {@link  PresenceStatus } . Defined here (the wire layer) because it is now published in  {@link  Presence } ; the connector imports it. Advisory observability, not a security boundary.",
        "enum": [
          "open",
          "dnd",
          "focus"
        ],
        "type": "string"
      },
      "WireChannelMode": {
        "description": "Per-channel attention override (more specific than the global  {@link  AttentionMode } ).\n- `quiet` — still delivered + buffered, but never wakes; an `@`-mention still wakes (per-channel `dnd`).\n- `muted` — channel messages dropped on receive, incl. `@`-mentions (\"don't receive this channel\").",
        "enum": [
          "quiet",
          "muted"
        ],
        "type": "string"
      },
      "WireEndpointKind": {
        "description": "Cotal wire types (v0.2).\n\nThese are the shapes that travel on the mesh. They are intentionally A2A-inspired (AgentCard / Message / Part) but transport-agnostic. This file IS part of the \"wire contract\" — treat changes here as protocol changes.",
        "enum": [
          "agent",
          "endpoint"
        ],
        "type": "string"
      },
      "WirePresence": {
        "description": "Live presence record. Stored in the space's KV bucket under key = card.id.",
        "properties": {
          "activeAt": {
            "description": "Epoch ms of the last work progress the harness reported (a turn event such as a token or a tool call). `ts` is only the heartbeat: a seat whose turn stopped advancing keeps heartbeating while this stays old. Missing means the connector reports none.",
            "type": "number"
          },
          "activity": {
            "description": "Freeform \"what I'm doing right now\".",
            "type": "string"
          },
          "activitySince": {
            "description": "Epoch ms when the current `activity` was set. A status change does not move it, so an activity left behind while hooks flip the status every turn still reads as old.",
            "type": "number"
          },
          "attention": {
            "$ref": "#/components/schemas/WireAttentionMode",
            "description": "This instance's current global attention mode. Advisory, within-space observability — a peer can see \"they're in focus\" and choose to DM. Published from the connector's authoritative state (presence is a mirror, never the source of truth for delivery). `open`/absent ⇒ receives all."
          },
          "card": {
            "$ref": "#/components/schemas/WireAgentCard"
          },
          "channelModes": {
            "additionalProperties": {
              "$ref": "#/components/schemas/WireChannelMode"
            },
            "description": "Per-channel attention overrides this instance currently has (runtime, reset on restart). Keys are concrete channel names. Advisory: lets a peer see \"locally muted #deploys → DM to reach me\". NOT access control — the broker still authorizes and delivers; this is a receive-side presentation.",
            "type": "object"
          },
          "condition": {
            "$ref": "#/components/schemas/WirePresenceCondition",
            "description": "Structured condition reported by the harness. Missing means nothing was reported."
          },
          "environment": {
            "description": "Opaque reference whose meaning belongs to the provider that issued it.",
            "type": "string"
          },
          "lifecycleUid": {
            "description": "This incarnation's lifecycle UID (SPEC §6/§13.1: MUST in auth mode from v0.4) — the value the alias currently maps to, published so peers/observers can attribute the alias's live occupant. Advisory observability, never authority (authority is the ledger/broker grants).",
            "type": "string"
          },
          "runtime": {
            "$ref": "#/components/schemas/WirePresenceRuntime",
            "description": "The process that publishes this record, with its observed start. Status, activity, heartbeats and reconnects inside the process leave it unchanged; a new process publishes a new one. An offline record keeps it to name the process that went away, so it dates a running duration only while the record is live. Missing means the publisher reports none: unknown, not zero."
          },
          "status": {
            "$ref": "#/components/schemas/WirePresenceStatus"
          },
          "statusSince": {
            "description": "Epoch ms when the instance entered its current `status` and `activity`. A change to either moves it; a heartbeat or a repeated report does not, so an activity that outlived what it described reads as old even while `ts` stays fresh. Never carried by an offline record.",
            "type": "number"
          },
          "ts": {
            "description": "Epoch ms of the last heartbeat.",
            "type": "number"
          }
        },
        "required": [
          "card",
          "status",
          "ts"
        ],
        "type": "object"
      },
      "WirePresenceCondition": {
        "description": "A condition the harness reported. Connectors relay it and never infer one from absence.",
        "properties": {
          "code": {
            "$ref": "#/components/schemas/WirePresenceConditionCode"
          },
          "message": {
            "description": "Free-text detail reported by the harness.",
            "type": "string"
          },
          "since": {
            "description": "Epoch ms when this condition began.",
            "type": "number"
          },
          "source": {
            "description": "The harness's native value, preserved verbatim.",
            "type": "string"
          }
        },
        "required": [
          "code"
        ],
        "type": "object"
      },
      "WirePresenceConditionCode": {
        "description": "Closed, provider-neutral categories a connector may relay from its harness's native signal.",
        "enum": [
          "rate_limit",
          "overloaded",
          "auth",
          "billing",
          "budget",
          "context",
          "model",
          "request",
          "server",
          "retrying",
          "approval",
          "input",
          "failed"
        ],
        "type": "string"
      },
      "WirePresenceRuntime": {
        "description": "The running process behind a presence record, as that process observed itself. Advisory display data, never authority. It carries no pid, host, path, command line or environment.",
        "properties": {
          "id": {
            "description": "Opaque CSPRNG id minted once per OS process and lifecycleUid scope, never reused. A new process or lifecycle scope gets a new id; reconnect within that scope keeps it.",
            "type": "string"
          },
          "source": {
            "$ref": "#/components/schemas/WirePresenceRuntimeSource"
          },
          "startedAt": {
            "description": "Epoch ms when the process started, read in-process from its own clock origin (`performance.timeOrigin`). Never a launch request, registration, heartbeat, status or activity time. Every lifecycle in this process shares this start, including one registered later. It does not date a separate parent, model harness or lifecycle.",
            "type": "number"
          }
        },
        "required": [
          "id",
          "startedAt",
          "source"
        ],
        "type": "object"
      },
      "WirePresenceRuntimeSource": {
        "const": "endpoint-process",
        "description": "Where a  {@link  PresenceRuntime }  start was observed. Closed: a consumer that meets any other value treats the observation as unsupported rather than guessing what it dates.\n- `endpoint-process`: the OS process that holds this presence's mesh connection, observed by   that process itself.",
        "type": "string"
      },
      "WirePresenceStatus": {
        "description": "Lifecycle status of a participant.\n- `idle`: connected, no active task\n- `waiting`: blocked — awaiting input, approval, or a peer\n- `working`: actively executing a task / in a turn\n- `offline`: disconnected or heartbeat lapsed (derived by observers, not self-set while live)",
        "enum": [
          "idle",
          "waiting",
          "working",
          "offline"
        ],
        "type": "string"
      }
    },
    "responses": {
      "LaunchExchange": {
        "description": "A valid one-use k query token is exchanged for a dashboard session. The response redirects to the same path without its query. A valid existing cookie takes precedence over k.",
        "headers": {
          "Location": {
            "schema": {
              "type": "string"
            },
            "description": "The requested path without the query string."
          },
          "Set-Cookie": {
            "schema": {
              "type": "string"
            },
            "description": "cotal_web_session=<session>; Path=/; HttpOnly; SameSite=Strict. The default local listener uses HTTP."
          }
        }
      },
      "Unauthenticated": {
        "description": "No valid session, an invalid launch token, or a spent launch token. The readiness credential does not authorize these routes.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/AuthenticationError"
            },
            "example": {
              "error": "unauthenticated"
            }
          }
        }
      },
      "CrossOrigin": {
        "description": "Origin is checked before session authentication. An origin outside this listener's allowed origins is refused.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/OriginError"
            },
            "example": {
              "error": "cross-origin"
            }
          }
        }
      },
      "BadRequest": {
        "description": "The query or percent-encoded channel name is invalid.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "BodyRefused": {
        "description": "These read routes accept no request body. An announced body is refused before it is read; the connection is closed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "InternalError": {
        "description": "The request failed. The body names the failure; a failed read does not terminate the dashboard.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ReadUnavailable": {
        "description": "The history read failed or did not finish within 8000 ms.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "MembershipUnavailable": {
        "description": "The membership snapshot could not be read. A missing snapshot is not an empty successful one.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/MembershipError"
            }
          }
        }
      }
    }
  }
}
