{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://golfsga.com/sga-ois/schema/0.1/denial.schema.json",
  "title": "SGA-OIS admission denial",
  "description": "The engine refusing a device admission, in a form the device can act on WITHOUT reading English. The standard requires vendors to send coded values rather than prose and owes them the same in return: a refusal a device cannot parse is a refusal it will retry forever, while one it can parse may be fixed unattended with nobody standing at the bay. A denial is sent before the connection closes -- a silent disconnect is indistinguishable from a crash.",
  "type": "object",
  "required": [
    "type",
    "code",
    "remedy"
  ],
  "additionalProperties": false,
  "properties": {
    "sga_kind": {
      "const": "denial",
      "description": "Document type discriminator. Always denial. Optional on the wire -- a consumer that already knows what it asked for does not need it -- but present in stored and exchanged documents so a file on its own can say what it is."
    },
    "type": {
      "const": "session.denied",
      "description": "Message type discriminator. Always session.denied."
    },
    "code": {
      "$ref": "common.schema.json#/$defs/denialCode",
      "description": "What was refused."
    },
    "remedy": {
      "$ref": "common.schema.json#/$defs/denialRemedy",
      "description": "What would fix it. Deliberately a SEPARATE field from code: the same code carries different remedies as a rule's shape changes, and a device should branch on this one."
    },
    "required_firmware": {
      "type": "string",
      "maxLength": 32,
      "description": "The version to reach. Present with remedy update_firmware, and absent with change_firmware -- where a build is denied by name there may be no later version to point at, and naming one that does not exist yet is worse than naming none."
    },
    "denied_builds": {
      "type": "array",
      "maxItems": 32,
      "items": {
        "type": "string",
        "maxLength": 32
      },
      "description": "The exact builds refused. Present with remedy change_firmware: move to any other supported build, which may legitimately mean an earlier one."
    },
    "advisory": {
      "type": "string",
      "maxLength": 32,
      "description": "The published advisory id, for an operator or a log."
    },
    "retry_after_ms": {
      "type": "integer",
      "minimum": 0,
      "description": "How long to wait before trying again, so a refused device does not hammer the bay. Short where the device can fix itself; long where nothing it does will help."
    },
    "detail": {
      "type": "string",
      "maxLength": 200,
      "description": "Human fallback for a log or an operator screen. MUST NOT be parsed: everything a device acts on is in the coded fields."
    },
    "occupied_by": {
      "type": "object",
      "additionalProperties": false,
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      },
      "description": "Who already holds the role, with code role_occupied. REQUIRED in practice for that code: a refusal that does not name the incumbent tells the second simulator only that something went wrong, and its author goes looking for a bug that is not theirs. With this it can say 'Acme Sim is already running in this bay', which is a sentence an operator can act on.",
      "properties": {
        "role": {
          "$ref": "common.schema.json#/$defs/role",
          "description": "The role that is taken."
        },
        "vendor": {
          "type": "string",
          "maxLength": 64,
          "description": "Vendor of the participant holding it."
        },
        "model": {
          "$ref": "common.schema.json#/$defs/modelName",
          "description": "Model of the participant holding it -- the half a human reads."
        },
        "device_id": {
          "type": "string",
          "maxLength": 128,
          "description": "Opaque id of the incumbent, for logs and support threads."
        }
      }
    },
    "redirect": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "bay_id"
      ],
      "patternProperties": {
        "^x-[a-z0-9_-]+\\.": {}
      },
      "description": "Where the device DOES belong, with code associated_elsewhere. REQUIRED in practice for that code: a redirect that does not say where to go is a dead end, and the device has no other way to find out.\n\nA fixed device -- a ceiling camera, an environment sensor -- cannot scan a placard, so it cannot prove which bay it is in the way a player's phone can. At a multi-bay facility it therefore discovers several engines and has to be TOLD which one is its own. That answer is not published in DNS: doing so would broadcast the whole device-to-bay map to anything that asks, it would mean engines answering queries for names they do not own, and an unauthenticated multicast answer can carry neither authority nor a reason. It is answered here instead, over a connection the device already has, by the engine holding the configuration.\n\nWhat it carries is how to FIND the right engine, never an address. An address in a redirect is stale in exactly the case that produces redirects -- a facility coming back up after a power cut with every lease renewed -- and a device that followed one would have no way to tell it had been sent somewhere else. The device rediscovers, by whichever mechanism brought it here, and confirms it arrived by comparing the new engine's session.hello engine_id against engine_id below.",
      "properties": {
        "bay_id": {
          "type": "string",
          "maxLength": 128,
          "description": "The bay the device belongs to. Always present: it is the binding, and it is what each engine's own advertisement is keyed on, so a device with nothing but this can still browse for its bay."
        },
        "engine_id": {
          "type": "string",
          "maxLength": 128,
          "description": "The engine to expect on arrival, where the redirecting engine knows it. This is what makes a redirect verifiable rather than merely helpful: a device that arrives somewhere else has been misdirected and MUST NOT join."
        },
        "instance": {
          "type": "string",
          "maxLength": 63,
          "description": "The DNS-SD instance name of that engine, where known. A shortcut, not the authority -- resolving one name beats browsing a facility with two hundred bays in it -- and a device that cannot resolve it still has bay_id."
        },
        "alias": {
          "type": "string",
          "maxLength": 64,
          "description": "The operator's name for that bay: \"Bay 12\", \"The Loft\". For a screen or a support thread, and display only -- nothing resolves it, so renaming a bay never breaks a redirect."
        }
      }
    }
  },
  "patternProperties": {
    "^x-[a-z0-9_-]+\\.": {}
  }
}
