{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://iteratingself.com/actum/spec/preflight-failure.schema.json",
  "title": "Preflight Failure",
  "description": "0.10.0 (preflight+unwind-policy epic, plan D2). Returned when execution create (or resume) refuses a request before any execution resource exists, because a dependency the pinned composition needs is unavailable. Distinct from failure-contract.schema.json (whose error is the const compose_step_failed): that contract describes a composition that started and failed mid-flight, with completed steps to account for and an unwind plan to run. Preflight runs strictly before executor.create_execution, so its refusal guarantees \"nothing started, nothing to unwind\" -- there is no execution_id, no failed_step, no completed_steps, and never anything to compensate. HTTP mapping: 503 with a Retry-After header -- REQUIRED on every preflight_failed 503 without exception, including a policy refusal that will not resolve on its own (dependency.kind: egress_blocked) and the undecryptable-vault-entry case below, both of which get a substantially longer value than an ordinary transient-unavailability refusal since retrying sooner cannot help either. The one exception to the availability framing (not to the header requirement) is an undecryptable vault entry from key mismatch/corruption, which is server misconfiguration and still maps to 503 kind: vault (not a 4xx) because the caller cannot fix it by retrying. Configuration failures that are the caller's/operator's to fix keep their own existing 4xx contracts and MUST NEVER use this schema: a missing connection grant stays the existing 403, a missing connection stays its existing error shape. 0.17.0 rollup: dependency.kind gains egress_blocked, closing what was previously a same-kind (\"provider\") wire-collision between an ordinary unreachable Tier-2 probe and one refused by the egress policy. Audit: a refusal emits action execution.preflight with outcome: \"denied\".",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "spec_version",
    "error",
    "dependency"
  ],
  "properties": {
    "spec_version": {
      "type": "string",
      "description": "Version of this contract schema (semver).",
      "examples": [
        "0.10.0"
      ]
    },
    "error": {
      "const": "preflight_failed"
    },
    "dependency": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "kind",
        "name"
      ],
      "description": "The dependency that was unavailable and caused the refusal.",
      "properties": {
        "kind": {
          "enum": [
            "store",
            "vault",
            "provider",
            "egress_blocked"
          ],
          "description": "store: the persistence backend round-trip (the same probe /v1/readyz performs) failed. vault: a connection binding the pinned composition references could not be decrypted (unavailable, or -- the one non-availability exception this schema still covers -- an undecryptable entry from key mismatch/corruption, which is server misconfiguration). provider: the named connection's remote API was unavailable -- at preflight, a Tier-2 probe (see composition.schema.json's preflight_probes) failed or could not be delivered. egress_blocked: a Tier-2 probe's target address resolved to a private/loopback/link-local/CGNAT/NAT64 host the operator has not explicitly allowlisted (spec/README.md's \"Egress is restricted by default\") -- a POLICY refusal, never a transient outage: retrying before the operator changes the allowlist cannot succeed, which is why implementations should give this kind a substantially longer Retry-After than the ordinary few-second default (the reference implementation uses one hour). This is a closed four-value vocabulary; store/vault/provider are additionally shared with execution.schema.json's compensation.skipped[].dependency.kind (where provider likewise means a connect-phase failure against the binding connection's remote API), so a caller correlates a preflight 503 and a later unwind skip on identical kind/name values for those three -- egress_blocked has no unwind-time analogue (a send-time egress refusal on an already-created execution surfaces as an ordinary step failure, not an unwind skip entry) and is Tier-2-probe-only: it is never raised by the Tier-1 store/vault sweep. Missing-connection/missing-grant configuration errors keep their existing 4xx contracts and never reach this schema."
        },
        "name": {
          "type": "string",
          "description": "Identifies the specific dependency instance: the connection name for vault/provider kinds; a store identifier for kind store (the field is always required — the reference implementation sends the literal \"store\").",
          "examples": [
            "tracker"
          ]
        }
      }
    },
    "retryable": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "retry_after"
      ],
      "description": "Optional machine-readable duplicate of the Retry-After response header. The header itself is REQUIRED on every preflight_failed 503 -- including the vault-corruption case, where it reflects when an operator-side fix is worth re-probing -- and when this body field is present its value MUST equal the header's. Absence of the body field does not imply immediate retry is safe; the header still governs.",
      "properties": {
        "retry_after": {
          "type": "integer",
          "minimum": 0,
          "description": "Seconds the caller should wait before retrying.",
          "examples": [
            30
          ]
        }
      }
    }
  }
}
