{
  "$id": "https://escouade.dev/schemas/plugin-protocol-1.json",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "The protocol of Escouade's plugins, version 1",
  "description": "JSON-RPC 2.0 over the standard input and output of a plugin's program, one message per line. The one source of the protocol's types: the TypeScript of @escouade-dev/plugin is generated from it (sdk/plugin/scripts/generate.mjs), and the Rust tests of the core hold it to what the core sends (plugins_protocol_schema).",
  "x-protocolVersion": 1,
  "x-methods": {
    "initialize": {
      "description": "The first message: the version of the protocol Escouade speaks and what it can do. Answer with your own.",
      "direction": "toPlugin",
      "params": {
        "$ref": "#/definitions/InitializeParams"
      },
      "result": {
        "$ref": "#/definitions/InitializeResult"
      },
      "timeoutMs": 10000
    },
    "shutdown": {
      "description": "Escouade closes: answer, then end.",
      "direction": "toPlugin",
      "params": {
        "type": "null"
      },
      "result": {
        "type": "null"
      },
      "timeoutMs": 1000
    },
    "events/fact": {
      "description": "A fact of permissions.events, from a project where the plugin is on. A notification: no answer.",
      "direction": "toPlugin",
      "notification": true,
      "params": {
        "$ref": "#/definitions/Fact"
      }
    },
    "permissions/decide": {
      "description": "A permission an agent asks for, before the user is asked (permissions.decide). Past the delay, the user is asked.",
      "direction": "toPlugin",
      "params": {
        "$ref": "#/definitions/DecideParams"
      },
      "result": {
        "$ref": "#/definitions/Decision"
      },
      "timeoutMs": 500
    },
    "proc/out": {
      "description": "What a program wrote, as text cut between two characters. Escouade stops reading it past 256 KiB not yet written on the input of the module. A notification: no answer.",
      "direction": "toPlugin",
      "notification": true,
      "params": {
        "$ref": "#/definitions/ProcOutParams"
      }
    },
    "proc/exit": {
      "description": "A program ended, after all it wrote; code is null when it was killed. A notification: no answer.",
      "direction": "toPlugin",
      "notification": true,
      "params": {
        "$ref": "#/definitions/ProcExitParams"
      }
    },
    "secrets/get": {
      "description": "One of its secrets (a key of permissions.secrets), or null when none is kept.",
      "direction": "toEscouade",
      "params": {
        "$ref": "#/definitions/SecretsGetParams"
      },
      "result": {
        "type": ["string", "null"]
      }
    },
    "settings/get": {
      "description": "The values of its settings (contributes.settings), as the user set them; with a project, those its .escouade.yml gives the plugin (plugins.<id>) over them, where the plugin is on.",
      "direction": "toEscouade",
      "params": {
        "$ref": "#/definitions/SettingsGetParams"
      },
      "result": {
        "type": "object",
        "additionalProperties": {
          "type": ["string", "number", "boolean"]
        }
      }
    },
    "files/read": {
      "description": "A file of the plugin's own folder, as text (UTF-8): a path relative to the folder, with / between its parts. Refused (refused) for a path that leaves the folder (absolute, .., a link that goes out of it), a hidden file or folder (.env, .git) and the manifest; notFound for a file that is not there; failed for a file over 256 KiB or not UTF-8. Both transports: the SDK's p.file(path).",
      "direction": "toEscouade",
      "params": {
        "$ref": "#/definitions/FilesReadParams"
      },
      "result": {
        "type": "string"
      },
      "errors": ["invalidParams", "refused", "notFound", "failed"],
      "timeoutMs": 5000
    },
    "net/fetch": {
      "description": "An HTTP request, for a plugin in WASM (run.js, run.wasm): the SDK's standard fetch goes by it. Escouade sends it in https only, to a host of permissions.network or one of its subdomains, never to an address of the machine or of the local network (judged on the addresses the name resolves to). `{account}` in permissions.network opens the host the user typed for one of the plugin's accounts, on that port and that host alone, in http or on the local network; `local:shared` opens http to 127.0.0.1 on a port the core read in the output of the process it started for the plugin (Launch.shared.listens), while it lives. The proxy and the TLS of Réglages › Réseau are followed: through a proxy only the name is judged, and the journal says so. No redirection is followed (the plugin gets the 3xx). The answer comes within 10 s and holds 1 MiB at most (failed past either), unless the manifest's limits ask for more (120 s, 32 MiB at most). A plugin run by a command uses its own network, and is answered methodNotFound; the same for net/stream, ws/* and timer/*.",
      "direction": "toEscouade",
      "params": {
        "$ref": "#/definitions/NetFetchParams"
      },
      "result": {
        "$ref": "#/definitions/NetFetchResult"
      },
      "errors": ["invalidParams", "refused", "failed"],
      "timeoutMs": 10000
    },
    "net/stream": {
      "description": "A response in pieces, for a plugin in WASM: the rules of net/fetch at its opening, the answer once its headers are read. Then net/chunk, and net/end. The plugin holds 256 KiB it has not consumed (net/credit): past it Escouade does not read the socket. 8 streams at once. 120 s without a byte end a stream that is not declared sse.",
      "direction": "toEscouade",
      "params": {
        "$ref": "#/definitions/NetStreamParams"
      },
      "result": {
        "$ref": "#/definitions/NetStreamResult"
      },
      "errors": ["invalidParams", "refused", "failed"],
      "timeoutMs": 10000
    },
    "net/chunk": {
      "description": "A piece of a stream.",
      "direction": "toPlugin",
      "notification": true,
      "params": {
        "$ref": "#/definitions/NetChunkParams"
      }
    },
    "net/end": {
      "description": "The end of a stream; none after a net/cancel.",
      "direction": "toPlugin",
      "notification": true,
      "params": {
        "$ref": "#/definitions/NetEndParams"
      }
    },
    "net/credit": {
      "description": "The plugin consumed these bytes of a stream: as many more may come, never past 256 KiB waiting.",
      "direction": "toEscouade",
      "notification": true,
      "params": {
        "$ref": "#/definitions/NetCreditParams"
      }
    },
    "net/cancel": {
      "description": "The plugin lets go of a stream: its connection closes.",
      "direction": "toEscouade",
      "notification": true,
      "params": {
        "$ref": "#/definitions/NetCancelParams"
      }
    },
    "ws/open": {
      "description": "A WebSocket, for a plugin in WASM: wss only, to the hosts of net/fetch, the proxy and the TLS of Réglages › Réseau. 4 sockets at once, 1 MiB a message. Its headers never go to the journal.",
      "direction": "toEscouade",
      "params": {
        "$ref": "#/definitions/WsOpenParams"
      },
      "result": {
        "$ref": "#/definitions/WsOpenResult"
      },
      "errors": ["invalidParams", "refused", "failed"],
      "timeoutMs": 10000
    },
    "ws/send": {
      "description": "A message on a socket: refused past 1 MiB, or when what waits to be sent would pass 256 KiB (a long message goes when nothing waits).",
      "direction": "toEscouade",
      "params": {
        "$ref": "#/definitions/WsSendParams"
      },
      "result": {
        "type": "null"
      },
      "errors": ["invalidParams", "refused"]
    },
    "ws/close": {
      "description": "Closes a socket: ws/closed follows.",
      "direction": "toEscouade",
      "params": {
        "$ref": "#/definitions/WsCloseParams"
      },
      "result": {
        "type": "null"
      },
      "errors": ["invalidParams", "refused"]
    },
    "ws/message": {
      "description": "A message received on a socket.",
      "direction": "toPlugin",
      "notification": true,
      "params": {
        "$ref": "#/definitions/WsMessageParams"
      }
    },
    "ws/closed": {
      "description": "A socket ended, by either side.",
      "direction": "toPlugin",
      "notification": true,
      "params": {
        "$ref": "#/definitions/WsClosedParams"
      }
    },
    "timer/set": {
      "description": "A timer, for a plugin in WASM: 10 ms at least, 64 at once. The prelude makes setTimeout of it. It goes with the instance.",
      "direction": "toEscouade",
      "params": {
        "$ref": "#/definitions/TimerSetParams"
      },
      "result": {
        "$ref": "#/definitions/TimerSetResult"
      },
      "errors": ["invalidParams", "refused"]
    },
    "timer/cancel": {
      "description": "Takes a timer back.",
      "direction": "toEscouade",
      "notification": true,
      "params": {
        "$ref": "#/definitions/TimerParams"
      }
    },
    "timer/fired": {
      "description": "A timer came due.",
      "direction": "toPlugin",
      "notification": true,
      "params": {
        "$ref": "#/definitions/TimerParams"
      }
    },
    "fs/read": {
      "description": "A file under a root: { text }, or { dataBase64 } when it is not UTF-8. For a plugin in WASM, by escouade.call (run.wasm) or by a line (run.js), on permissions.files: refused for a root it does not declare, a .., a ~, or a path that, links followed, is not under a root of that agent or that its fence refuses (« Projet privé »). failed past 256 KiB, or limits.fileBytes (4 MiB at most).",
      "direction": "toEscouade",
      "params": {
        "$ref": "#/definitions/FsPath"
      },
      "result": {
        "$ref": "#/definitions/FsReadResult"
      },
      "errors": ["invalidParams", "refused", "notFound", "failed"]
    },
    "fs/write": {
      "description": "Writes a file under a root in write access, the folders it needs made; each write goes to the plugin's journal. For a plugin in WASM, by escouade.call (run.wasm) or by a line (run.js), on permissions.files: refused for a root it does not declare, a .., a ~, or a path that, links followed, is not under a root of that agent or that its fence refuses (« Projet privé »). 4 MiB at most.",
      "direction": "toEscouade",
      "params": {
        "$ref": "#/definitions/FsWriteParams"
      },
      "result": {
        "$ref": "#/definitions/FsWriteResult"
      },
      "errors": ["invalidParams", "refused", "failed"]
    },
    "fs/stat": {
      "description": "What a path is. For a plugin in WASM, by escouade.call (run.wasm) or by a line (run.js), on permissions.files: refused for a root it does not declare, a .., a ~, or a path that, links followed, is not under a root of that agent or that its fence refuses (« Projet privé »).",
      "direction": "toEscouade",
      "params": {
        "$ref": "#/definitions/FsPath"
      },
      "result": {
        "$ref": "#/definitions/FsStatResult"
      },
      "errors": ["invalidParams", "refused", "notFound"]
    },
    "fs/list": {
      "description": "The entries of a folder, by name, 4096 at most; a link is a link, not followed. For a plugin in WASM, by escouade.call (run.wasm) or by a line (run.js), on permissions.files: refused for a root it does not declare, a .., a ~, or a path that, links followed, is not under a root of that agent or that its fence refuses (« Projet privé »).",
      "direction": "toEscouade",
      "params": {
        "$ref": "#/definitions/FsPath"
      },
      "result": {
        "$ref": "#/definitions/FsListResult"
      },
      "errors": ["invalidParams", "refused", "notFound"]
    },
    "fs/canon": {
      "description": "The real path of a path, links followed. For a plugin in WASM, by escouade.call (run.wasm) or by a line (run.js), on permissions.files: refused for a root it does not declare, a .., a ~, or a path that, links followed, is not under a root of that agent or that its fence refuses (« Projet privé »).",
      "direction": "toEscouade",
      "params": {
        "$ref": "#/definitions/FsPath"
      },
      "result": {
        "$ref": "#/definitions/FsCanonResult"
      },
      "errors": ["invalidParams", "refused"]
    },
    "proc/spawn": {
      "description": "Runs a program of permissions.commands, for a plugin in WASM: the command and its arguments must be an entry word for word, more following only an entry that ends in *. The program is found on the PATH, never a path; no shell; 4 at once; it ends with the plugin. Its environment is an agent's, without Escouade's variables, plus env. Its output comes by proc/out, its end by proc/exit; proc/out may come before this answer. Each start goes to the journal.",
      "direction": "toEscouade",
      "params": {
        "$ref": "#/definitions/ProcSpawnParams"
      },
      "result": {
        "$ref": "#/definitions/ProcSpawnResult"
      },
      "errors": ["invalidParams", "refused", "failed"]
    },
    "proc/write": {
      "description": "Writes on the input of a program; close ends that input. failed past 1 MiB it has not read.",
      "direction": "toEscouade",
      "params": {
        "$ref": "#/definitions/ProcWriteParams"
      },
      "result": {
        "type": "null"
      },
      "errors": ["invalidParams", "failed"]
    },
    "proc/kill": {
      "description": "Ends a program and what it started; proc/exit follows.",
      "direction": "toEscouade",
      "params": {
        "$ref": "#/definitions/ProcRef"
      },
      "result": {
        "type": "null"
      },
      "errors": ["invalidParams", "failed"]
    },
    "log": {
      "description": "A line of its journal.",
      "direction": "toEscouade",
      "notification": true,
      "params": {
        "$ref": "#/definitions/LogParams"
      }
    }
  },
  "x-defaultTimeoutMs": 5000,
  "x-errors": {
    "parse": {
      "code": -32700,
      "description": "A line that is not JSON."
    },
    "invalidRequest": {
      "code": -32600,
      "description": "A message that is not JSON-RPC 2.0."
    },
    "methodNotFound": {
      "code": -32601,
      "description": "A method the other side does not serve."
    },
    "invalidParams": {
      "code": -32602,
      "description": "Parameters that do not hold."
    },
    "internal": {
      "code": -32603,
      "description": "The handler failed."
    },
    "notYours": {
      "code": -32001,
      "description": "A secret that is not one of the plugin's own."
    },
    "keychain": {
      "code": -32002,
      "description": "The system's keychain cannot be reached."
    },
    "refused": {
      "code": -32003,
      "description": "What the plugin may not reach: a host outside permissions.network, http, an address of the machine or of the local network (net/fetch); a path outside its folder, hidden, or its manifest (files/read); a path out of its roots (fs/*); a program, an argument, a folder or a variable it may not name (proc/spawn). A capability it does not declare says the same."
    },
    "failed": {
      "code": -32004,
      "description": "A request that failed on the way: no answer within its delay, an answer or a file too big, a file that is not UTF-8."
    },
    "notFound": {
      "code": -32005,
      "description": "No such file in the plugin's folder (files/read), or under a root (fs/*)."
    }
  },
  "definitions": {
    "FactName": {
      "description": "The facts of Escouade.",
      "enum": [
        "agent.created",
        "agent.renamed",
        "agent.status",
        "agent.stopped",
        "agent.handover",
        "turn.ended",
        "request.permission",
        "request.question",
        "request.answered",
        "ticket.created",
        "ticket.column",
        "ticket.report",
        "ticket.approved",
        "ticket.rejected",
        "ticket.merged",
        "ticket.conflict",
        "project.added",
        "project.closed",
        "worktree.created",
        "worktree.removed",
        "launch.started",
        "launch.ready",
        "launch.stopped",
        "tests.result",
        "app.started",
        "docs.written",
        "docs.filed",
        "docs.gaps",
        "memory.written",
        "memory.forgotten",
        "decision.proposed",
        "decision.accepted",
        "pr.opened",
        "account.switched",
        "workflow.step_entered",
        "workflow.step_left",
        "workflow.looped_back",
        "workflow.left",
        "workflow.changed"
      ]
    },
    "Project": {
      "description": "A project, by its id and its name.",
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "name": {
          "type": "string"
        },
        "private": {
          "description": "A private project: nothing of it goes to a plugin that is not on for it.",
          "type": "boolean"
        }
      },
      "required": ["id", "name", "private"]
    },
    "Ticket": {
      "description": "A ticket, by its id, its key and its title.",
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "key": {
          "description": "Like ESC-12.",
          "type": "string"
        },
        "title": {
          "type": "string"
        }
      },
      "required": ["id", "key", "title"]
    },
    "Agent": {
      "description": "An agent, by its id and its name.",
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "name": {
          "type": "string"
        }
      },
      "required": ["id", "name"]
    },
    "Fact": {
      "description": "A fact: what changed, and the project, the ticket and the agent it is about, never the conversation. Ignore the fields you do not know.",
      "type": "object",
      "properties": {
        "name": {
          "$ref": "#/definitions/FactName"
        },
        "version": {
          "description": "The version of this fact's fields.",
          "type": "integer"
        },
        "seq": {
          "description": "Its number since Escouade started: two facts come in the order of their numbers.",
          "type": "integer"
        },
        "at": {
          "description": "When, in RFC 3339.",
          "type": "string"
        },
        "project": {
          "$ref": "#/definitions/Project"
        },
        "ticket": {
          "$ref": "#/definitions/Ticket"
        },
        "agent": {
          "$ref": "#/definitions/Agent"
        },
        "data": {
          "description": "What it says, fact by fact (agent.status: status and was; ticket.column: from and to; turn.ended: end…): see the page Events of the plugins’ documentation.",
          "type": "object",
          "additionalProperties": true
        }
      },
      "required": ["name", "version", "seq", "at", "data"],
      "additionalProperties": true
    },
    "EscouadeCapabilities": {
      "description": "What Escouade can do for the plugin.",
      "type": "object",
      "properties": {
        "batch": {
          "description": "Batches, both ways.",
          "type": "boolean"
        },
        "secrets": {
          "description": "secrets/get.",
          "type": "boolean"
        },
        "log": {
          "description": "The log notification.",
          "type": "boolean"
        },
        "attach": {
          "description": "What each agent attached is given again after a restart.",
          "type": "boolean"
        }
      }
    },
    "InitializeParams": {
      "type": "object",
      "properties": {
        "protocolVersion": {
          "description": "The version Escouade speaks.",
          "type": "integer"
        },
        "escouade": {
          "type": "object",
          "properties": {
            "version": {
              "type": "string"
            }
          },
          "required": ["version"]
        },
        "plugin": {
          "description": "The plugin, as its manifest says.",
          "type": "object",
          "properties": {
            "id": {
              "type": "string"
            },
            "version": {
              "type": "string"
            }
          },
          "required": ["id", "version"]
        },
        "lang": {
          "description": "The language of the interface (fr, en), for what the plugin writes for the user. A plugin in WASM has no other way to know it.",
          "type": "string"
        },
        "capabilities": {
          "$ref": "#/definitions/EscouadeCapabilities"
        }
      },
      "required": ["protocolVersion", "escouade", "plugin", "capabilities"]
    },
    "InitializeResult": {
      "type": "object",
      "properties": {
        "protocolVersion": {
          "description": "The version the plugin speaks; another than Escouade's stops it.",
          "type": "integer"
        },
        "capabilities": {
          "description": "What the plugin can do.",
          "type": "object",
          "additionalProperties": true
        }
      },
      "required": ["protocolVersion", "capabilities"]
    },
    "PermissionRequest": {
      "description": "What the agent asks to do.",
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "kind": {
          "description": "Like command, edit, fetch.",
          "type": "string"
        },
        "tool": {
          "description": "The harness's tool, like Bash.",
          "type": "string"
        },
        "input": {
          "description": "What the tool was given, like { command: 'npm test' }.",
          "type": "object",
          "additionalProperties": true
        }
      },
      "required": ["id", "kind"]
    },
    "DecideParams": {
      "type": "object",
      "properties": {
        "request": {
          "$ref": "#/definitions/PermissionRequest"
        },
        "project": {
          "$ref": "#/definitions/Project"
        },
        "ticket": {
          "$ref": "#/definitions/Ticket"
        },
        "agent": {
          "$ref": "#/definitions/Agent"
        }
      },
      "required": ["request", "project"]
    },
    "Decision": {
      "description": "A refusal wins over an allowance; ask leaves it to the user.",
      "type": "object",
      "properties": {
        "decision": {
          "enum": ["allow", "deny", "ask"]
        },
        "message": {
          "description": "Why: told to the agent with a refusal.",
          "type": "string"
        }
      },
      "required": ["decision"]
    },
    "SecretsGetParams": {
      "type": "object",
      "properties": {
        "key": {
          "description": "A key of permissions.secrets.",
          "type": "string"
        }
      },
      "required": ["key"]
    },
    "SettingsGetParams": {
      "type": "object",
      "properties": {
        "project": {
          "description": "The id of a project (project.id of a fact): its .escouade.yml's settings for the plugin go over the user's. None: the user's alone.",
          "type": "string"
        }
      }
    },
    "FilesReadParams": {
      "type": "object",
      "properties": {
        "path": {
          "description": "The file's path in the plugin's folder: rules.json, data/words.txt.",
          "type": "string"
        }
      },
      "required": ["path"]
    },
    "NetFetchParams": {
      "type": "object",
      "properties": {
        "url": {
          "description": "An https URL to a host of permissions.network; http too for a host of `{account}` or `local:shared`.",
          "type": "string"
        },
        "method": {
          "description": "GET when left out.",
          "type": "string"
        },
        "headers": {
          "description": "The headers of the request (Host is set by Escouade).",
          "type": "object",
          "additionalProperties": {
            "type": "string"
          }
        },
        "body": {
          "description": "The body of the request, as text.",
          "type": ["string", "null"]
        },
        "bodyBase64": {
          "description": "The body of the request, as base64, for bytes: body or bodyBase64, not both.",
          "type": "string"
        }
      },
      "required": ["url"]
    },
    "NetFetchResult": {
      "type": "object",
      "properties": {
        "status": {
          "description": "The HTTP status; a 3xx is given as it is, never followed.",
          "type": "integer"
        },
        "headers": {
          "description": "Its headers, by lower case name; a repeated one joined by \", \".",
          "type": "object",
          "additionalProperties": {
            "type": "string"
          }
        },
        "body": {
          "description": "Its body, when it is UTF-8.",
          "type": "string"
        },
        "bodyBase64": {
          "description": "Its body as base64, when it is not UTF-8.",
          "type": "string"
        }
      },
      "required": ["status", "headers"]
    },
    "NetStreamParams": {
      "type": "object",
      "properties": {
        "url": {
          "description": "An https URL to a host of permissions.network; http too for a host of `{account}` or `local:shared`.",
          "type": "string"
        },
        "method": {
          "description": "GET when left out.",
          "type": "string"
        },
        "headers": {
          "description": "The headers of the request (Host is set by Escouade).",
          "type": "object",
          "additionalProperties": {
            "type": "string"
          }
        },
        "body": {
          "description": "The body of the request, as text.",
          "type": ["string", "null"]
        },
        "bodyBase64": {
          "description": "The body of the request, as base64, for bytes: body or bodyBase64, not both.",
          "type": "string"
        },
        "sse": {
          "description": "A stream of events, that may stay silent: it is never closed for that.",
          "type": "boolean"
        }
      },
      "required": ["url"]
    },
    "NetStreamResult": {
      "type": "object",
      "properties": {
        "stream": {
          "description": "What names it.",
          "type": "integer"
        },
        "status": {
          "description": "The HTTP status; a 3xx is given as it is, never followed.",
          "type": "integer"
        },
        "headers": {
          "description": "Its headers, by lower case name; a repeated one joined by \", \".",
          "type": "object",
          "additionalProperties": {
            "type": "string"
          }
        }
      },
      "required": ["stream", "status", "headers"]
    },
    "NetChunkParams": {
      "type": "object",
      "properties": {
        "stream": {
          "description": "The stream.",
          "type": "integer"
        },
        "data": {
          "description": "The piece as text, when it is UTF-8: a character cut by a read waits for the next piece.",
          "type": "string"
        },
        "dataBase64": {
          "description": "The piece as base64, when it is not UTF-8.",
          "type": "string"
        }
      },
      "required": ["stream"]
    },
    "NetEndParams": {
      "type": "object",
      "properties": {
        "stream": {
          "description": "The stream.",
          "type": "integer"
        },
        "error": {
          "description": "Why it broke; none for a stream read to its end.",
          "type": "string"
        }
      },
      "required": ["stream"]
    },
    "NetCreditParams": {
      "type": "object",
      "properties": {
        "stream": {
          "description": "The stream.",
          "type": "integer"
        },
        "bytes": {
          "description": "How many bytes of it were consumed.",
          "type": "integer"
        }
      },
      "required": ["stream", "bytes"]
    },
    "NetCancelParams": {
      "type": "object",
      "properties": {
        "stream": {
          "description": "The stream.",
          "type": "integer"
        }
      },
      "required": ["stream"]
    },
    "WsOpenParams": {
      "type": "object",
      "properties": {
        "url": {
          "description": "A wss URL to a host of net/fetch.",
          "type": "string"
        },
        "headers": {
          "description": "The headers of the handshake (Host is set by Escouade); never in the journal.",
          "type": "object",
          "additionalProperties": {
            "type": "string"
          }
        }
      },
      "required": ["url"]
    },
    "WsOpenResult": {
      "type": "object",
      "properties": {
        "socket": {
          "description": "What names it.",
          "type": "integer"
        }
      },
      "required": ["socket"]
    },
    "WsSendParams": {
      "type": "object",
      "properties": {
        "socket": {
          "description": "The socket.",
          "type": "integer"
        },
        "text": {
          "description": "A text message.",
          "type": "string"
        },
        "dataBase64": {
          "description": "A binary message, as base64: text or dataBase64, not both.",
          "type": "string"
        }
      },
      "required": ["socket"]
    },
    "WsCloseParams": {
      "type": "object",
      "properties": {
        "socket": {
          "description": "The socket.",
          "type": "integer"
        }
      },
      "required": ["socket"]
    },
    "WsMessageParams": {
      "type": "object",
      "properties": {
        "socket": {
          "description": "The socket.",
          "type": "integer"
        },
        "text": {
          "description": "A text message.",
          "type": "string"
        },
        "dataBase64": {
          "description": "A binary message, as base64.",
          "type": "string"
        }
      },
      "required": ["socket"]
    },
    "WsClosedParams": {
      "type": "object",
      "properties": {
        "socket": {
          "description": "The socket.",
          "type": "integer"
        },
        "code": {
          "description": "The close code; 1006 for a connection that broke, 1009 for a message over 1 MiB.",
          "type": "integer"
        },
        "reason": {
          "description": "Why, when it is said.",
          "type": "string"
        }
      },
      "required": ["socket", "code", "reason"]
    },
    "TimerSetParams": {
      "type": "object",
      "properties": {
        "ms": {
          "description": "In how long, in milliseconds; less than 10 is 10.",
          "type": "number"
        }
      },
      "required": ["ms"]
    },
    "TimerSetResult": {
      "type": "object",
      "properties": {
        "timer": {
          "description": "What names it.",
          "type": "integer"
        }
      },
      "required": ["timer"]
    },
    "TimerParams": {
      "type": "object",
      "properties": {
        "timer": {
          "description": "The timer.",
          "type": "integer"
        }
      },
      "required": ["timer"]
    },
    "LogParams": {
      "type": "object",
      "properties": {
        "level": {
          "enum": ["debug", "info", "warn", "error"]
        },
        "message": {
          "type": "string"
        }
      },
      "required": ["level", "message"]
    },
    "FsPath": {
      "type": "object",
      "description": "What fs/* name: a path relative to root, or a path of the host (C:\\wt\\a.txt) under a root of the agent. root plugin and temp need no agent.",
      "properties": {
        "agent": {
          "type": "string",
          "description": "The key of the agent, as agent/attach gave it."
        },
        "root": {
          "description": "A root of permissions.files.",
          "enum": ["plugin", "agent", "agentRepo", "agentHome", "temp"]
        },
        "path": {
          "type": "string",
          "description": "Relative to root when there is one, absolute otherwise; never a .. nor a ~."
        }
      },
      "required": ["path"]
    },
    "FsReadResult": {
      "type": "object",
      "properties": {
        "text": {
          "type": "string",
          "description": "The file, when it is UTF-8."
        },
        "dataBase64": {
          "type": "string",
          "description": "The file, when it is not."
        }
      }
    },
    "FsWriteParams": {
      "type": "object",
      "properties": {
        "agent": {
          "type": "string",
          "description": "As FsPath."
        },
        "root": {
          "description": "As FsPath.",
          "enum": ["plugin", "agent", "agentRepo", "agentHome", "temp"]
        },
        "path": {
          "type": "string",
          "description": "As FsPath."
        },
        "text": {
          "type": "string",
          "description": "What to write, as text."
        },
        "dataBase64": {
          "type": "string",
          "description": "What to write, in base64, in place of text."
        },
        "append": {
          "type": "boolean",
          "description": "At the end of the file."
        }
      },
      "required": ["path"]
    },
    "FsWriteResult": {
      "type": "object",
      "properties": {
        "path": {
          "type": "string",
          "description": "Its real path."
        },
        "bytes": {
          "type": "integer"
        }
      },
      "required": ["path", "bytes"]
    },
    "FsStatResult": {
      "type": "object",
      "properties": {
        "kind": {
          "enum": ["file", "dir", "other"]
        },
        "bytes": {
          "type": "integer"
        },
        "modified": {
          "description": "In milliseconds since 1970.",
          "type": ["integer", "null"]
        }
      },
      "required": ["kind", "bytes"]
    },
    "FsEntry": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        },
        "kind": {
          "enum": ["file", "dir", "link", "other"]
        },
        "bytes": {
          "type": "integer"
        }
      },
      "required": ["name", "kind", "bytes"]
    },
    "FsListResult": {
      "type": "object",
      "properties": {
        "entries": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/FsEntry"
          }
        },
        "truncated": {
          "type": "boolean",
          "description": "Past 4096 entries."
        }
      },
      "required": ["entries", "truncated"]
    },
    "FsCanonResult": {
      "type": "object",
      "properties": {
        "path": {
          "type": "string"
        }
      },
      "required": ["path"]
    },
    "ProcSpawnParams": {
      "type": "object",
      "properties": {
        "command": {
          "type": "string",
          "description": "The program's name, the first word of an entry of permissions.commands."
        },
        "args": {
          "description": "Its arguments.",
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "dir": {
          "description": "Where it runs: a root, never a path. A private project refuses it unless the plugin was turned on for it. Left out: the plugin's temporary folder.",
          "enum": ["agent", "project"]
        },
        "agent": {
          "type": "string",
          "description": "The key of the agent, for dir."
        },
        "project": {
          "type": "string",
          "description": "The id of a project, for dir project; the agent's when left out."
        },
        "env": {
          "description": "The variables it names, over those of an agent; never ESCOUADE_*, PATH, LD_*, DYLD_*.",
          "type": "object",
          "additionalProperties": {
            "type": "string"
          }
        }
      },
      "required": ["command"]
    },
    "ProcSpawnResult": {
      "type": "object",
      "properties": {
        "proc": {
          "type": "integer"
        }
      },
      "required": ["proc"]
    },
    "ProcRef": {
      "type": "object",
      "properties": {
        "proc": {
          "type": "integer"
        }
      },
      "required": ["proc"]
    },
    "ProcWriteParams": {
      "type": "object",
      "properties": {
        "proc": {
          "type": "integer"
        },
        "data": {
          "type": "string",
          "description": "What to write on its input."
        },
        "close": {
          "type": "boolean",
          "description": "Ends its input, after data."
        }
      },
      "required": ["proc"]
    },
    "ProcOutParams": {
      "type": "object",
      "properties": {
        "proc": {
          "type": "integer"
        },
        "stream": {
          "enum": ["stdout", "stderr"]
        },
        "data": {
          "type": "string"
        }
      },
      "required": ["proc", "stream", "data"]
    },
    "ProcExitParams": {
      "type": "object",
      "properties": {
        "proc": {
          "type": "integer"
        },
        "code": {
          "type": ["integer", "null"]
        }
      },
      "required": ["proc", "code"]
    }
  }
}
