{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Request",
  "type": "object",
  "properties": {
    "callback_url": {
      "description": "Terminal-event destination; requires the daemon callbacks.allowlist.",
      "type": [
        "string",
        "null"
      ]
    },
    "content_hash": {
      "type": "string",
      "default": ""
    },
    "created_at": {
      "type": "integer",
      "format": "uint64",
      "default": 0,
      "minimum": 0
    },
    "decision": {
      "anyOf": [
        {
          "$ref": "#/$defs/Decision"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "dedupe_key": {
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "expires_at": {
      "type": [
        "integer",
        "null"
      ],
      "format": "uint64",
      "default": null,
      "minimum": 0
    },
    "id": {
      "type": "string",
      "default": ""
    },
    "input": {
      "anyOf": [
        {
          "$ref": "#/$defs/RequestInput"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "kind": {
      "$ref": "#/$defs/Kind",
      "default": "custom"
    },
    "on_timeout": {
      "$ref": "#/$defs/OnTimeout",
      "default": "deny"
    },
    "options": {
      "type": "array",
      "default": [
        {
          "id": "approve",
          "label": "Approve",
          "outcome": "approve",
          "style": "primary"
        },
        {
          "id": "deny",
          "label": "Deny",
          "outcome": "deny",
          "style": "danger"
        }
      ],
      "items": {
        "$ref": "#/$defs/Option"
      }
    },
    "previews": {
      "type": "array",
      "default": [],
      "items": {
        "$ref": "#/$defs/Preview"
      }
    },
    "risk": {
      "$ref": "#/$defs/Risk",
      "default": "low"
    },
    "run": {
      "description": "Present while the desktop app runs the command (daemon-managed).",
      "anyOf": [
        {
          "$ref": "#/$defs/RunClaim"
        },
        {
          "type": "null"
        }
      ]
    },
    "source": {
      "$ref": "#/$defs/Source",
      "default": {
        "agent": null,
        "branch": null,
        "cwd": null,
        "host": null,
        "integration": null,
        "repo": null,
        "session": null
      }
    },
    "status": {
      "$ref": "#/$defs/Status",
      "default": "pending"
    },
    "summary": {
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "timeout": {
      "description": "Optional positive duration, at most one year. Absent uses config; null/`none` disables expiry.",
      "type": [
        "string",
        "null"
      ]
    },
    "title": {
      "type": "string"
    },
    "tool": {
      "type": [
        "string",
        "null"
      ]
    }
  },
  "additionalProperties": false,
  "required": [
    "title"
  ],
  "$defs": {
    "Answer": {
      "description": "The human's answer to one question.",
      "type": "object",
      "properties": {
        "selected": {
          "description": "Chosen option labels; at most one unless the question is `multi`.",
          "type": "array",
          "default": [],
          "items": {
            "type": "string"
          }
        },
        "text": {
          "description": "Typed answer; only when the question allows free text.",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "additionalProperties": false
    },
    "Command": {
      "type": "object",
      "properties": {
        "argv": {
          "type": [
            "array",
            "null"
          ],
          "default": null,
          "items": {
            "type": "string"
          }
        },
        "cwd": {
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "env_keys": {
          "type": "array",
          "default": [],
          "items": {
            "type": "string"
          }
        },
        "shell": {
          "type": [
            "string",
            "null"
          ],
          "default": null
        }
      },
      "additionalProperties": false
    },
    "Decision": {
      "type": "object",
      "properties": {
        "content_hash": {
          "type": "string"
        },
        "decided_at": {
          "type": [
            "integer",
            "null"
          ],
          "format": "uint64",
          "default": null,
          "minimum": 0
        },
        "decided_by": {
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "feedback": {
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "fields": {
          "anyOf": [
            {
              "$ref": "#/$defs/DecisionFields"
            },
            {
              "type": "null"
            }
          ],
          "default": null
        },
        "option": {
          "type": "string"
        },
        "outcome": {
          "anyOf": [
            {
              "$ref": "#/$defs/Outcome"
            },
            {
              "type": "null"
            }
          ],
          "default": null
        },
        "rule_id": {
          "type": [
            "string",
            "null"
          ]
        },
        "run_result": {
          "description": "The desktop app ran the request's command for the human: this is how\nit ended. Present only on approved command requests decided by Run.",
          "anyOf": [
            {
              "$ref": "#/$defs/RunResult"
            },
            {
              "type": "null"
            }
          ]
        },
        "scope": {
          "type": [
            "string",
            "null"
          ]
        }
      },
      "additionalProperties": false,
      "required": [
        "option",
        "content_hash"
      ]
    },
    "DecisionFields": {
      "description": "Decision `fields`. Question requests carry only `answers`, keyed by question\nid; other requests carry answers to `input.fields` or edited tool input.",
      "type": "object",
      "properties": {
        "answers": {
          "type": [
            "object",
            "null"
          ],
          "additionalProperties": {
            "$ref": "#/$defs/Answer"
          }
        }
      }
    },
    "Email": {
      "description": "An email draft for review. handup never sends it: on approval the agent\nsends the draft (or the edited `decision.fields`) with its own mail tool.",
      "type": "object",
      "properties": {
        "attachments": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/EmailAttachment"
          }
        },
        "bcc": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "body": {
          "description": "Message body as Markdown (plain text renders as-is).",
          "type": "string",
          "default": ""
        },
        "body_html": {
          "description": "Optional HTML alternative; rendered only in the sandboxed HTML preview frame.",
          "type": [
            "string",
            "null"
          ]
        },
        "cc": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "date": {
          "description": "Date shown in the header, as the agent formats it (e.g. RFC 2822).",
          "type": [
            "string",
            "null"
          ]
        },
        "from": {
          "type": [
            "string",
            "null"
          ]
        },
        "in_reply_to": {
          "description": "The message this draft replies to, shown collapsed under the body.",
          "anyOf": [
            {
              "$ref": "#/$defs/EmailQuote"
            },
            {
              "type": "null"
            }
          ]
        },
        "reply_to": {
          "type": [
            "string",
            "null"
          ]
        },
        "subject": {
          "type": "string",
          "default": ""
        },
        "to": {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      },
      "additionalProperties": false
    },
    "EmailAttachment": {
      "type": "object",
      "properties": {
        "blob": {
          "description": "Uploaded blob hash (`POST /v1/blobs`) so the reviewer can open the file.",
          "type": [
            "string",
            "null"
          ]
        },
        "mime": {
          "type": [
            "string",
            "null"
          ]
        },
        "name": {
          "type": "string"
        },
        "size": {
          "type": [
            "integer",
            "null"
          ],
          "format": "uint64",
          "minimum": 0
        }
      },
      "additionalProperties": false,
      "required": [
        "name"
      ]
    },
    "EmailQuote": {
      "type": "object",
      "properties": {
        "body": {
          "description": "Plain text of the earlier message (or a snippet of it).",
          "type": "string",
          "default": ""
        },
        "date": {
          "type": [
            "string",
            "null"
          ]
        },
        "from": {
          "type": [
            "string",
            "null"
          ]
        }
      },
      "additionalProperties": false
    },
    "Kind": {
      "type": "string",
      "enum": [
        "command",
        "edit",
        "review",
        "question",
        "custom"
      ]
    },
    "OnTimeout": {
      "type": "string",
      "enum": [
        "deny",
        "approve",
        "expire"
      ]
    },
    "Option": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "label": {
          "type": "string"
        },
        "outcome": {
          "$ref": "#/$defs/Outcome"
        },
        "style": {
          "type": [
            "string",
            "null"
          ]
        }
      },
      "additionalProperties": false,
      "required": [
        "id",
        "label",
        "outcome"
      ]
    },
    "Outcome": {
      "type": "string",
      "enum": [
        "approve",
        "deny"
      ]
    },
    "Preview": {
      "type": "object",
      "properties": {
        "after_blob": {
          "type": [
            "string",
            "null"
          ]
        },
        "before_blob": {
          "type": [
            "string",
            "null"
          ]
        },
        "blob": {
          "type": [
            "string",
            "null"
          ]
        },
        "command": {
          "anyOf": [
            {
              "$ref": "#/$defs/Command"
            },
            {
              "type": "null"
            }
          ]
        },
        "email": {
          "anyOf": [
            {
              "$ref": "#/$defs/Email"
            },
            {
              "type": "null"
            }
          ]
        },
        "entry": {
          "type": [
            "string",
            "null"
          ]
        },
        "files": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "$ref": "#/$defs/PreviewFile"
          }
        },
        "inline": {
          "type": [
            "string",
            "null"
          ]
        },
        "lang": {
          "type": [
            "string",
            "null"
          ]
        },
        "mime": {
          "type": [
            "string",
            "null"
          ]
        },
        "name": {
          "type": [
            "string",
            "null"
          ]
        },
        "size": {
          "type": [
            "integer",
            "null"
          ],
          "format": "uint64",
          "minimum": 0
        },
        "title": {
          "type": [
            "string",
            "null"
          ]
        },
        "type": {
          "$ref": "#/$defs/PreviewType"
        }
      },
      "additionalProperties": false,
      "required": [
        "type"
      ]
    },
    "PreviewFile": {
      "type": "object",
      "properties": {
        "blob": {
          "type": "string"
        },
        "lang": {
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "mime": {
          "type": "string"
        },
        "path": {
          "type": "string"
        },
        "size": {
          "type": "integer",
          "format": "uint64",
          "minimum": 0
        }
      },
      "additionalProperties": false,
      "required": [
        "path",
        "blob",
        "mime",
        "size"
      ]
    },
    "PreviewType": {
      "type": "string",
      "enum": [
        "text",
        "markdown",
        "code",
        "diff",
        "files",
        "command",
        "json",
        "html",
        "image",
        "video",
        "audio",
        "file",
        "pdf",
        "email"
      ]
    },
    "Question": {
      "description": "A question for the human; answered in `decision.fields.answers[id]`.",
      "type": "object",
      "properties": {
        "allow_free_text": {
          "description": "Allow a typed answer besides (or instead of) the options.",
          "type": "boolean",
          "default": false
        },
        "header": {
          "description": "Short topic chip, e.g. \"Database\".",
          "type": [
            "string",
            "null"
          ]
        },
        "id": {
          "description": "Stable id, unique within the request.",
          "type": "string"
        },
        "multi": {
          "description": "Allow selecting several options.",
          "type": "boolean",
          "default": false
        },
        "options": {
          "type": "array",
          "default": [],
          "items": {
            "$ref": "#/$defs/QuestionOption"
          }
        },
        "question": {
          "description": "The question itself (markdown).",
          "type": "string"
        },
        "recommended": {
          "description": "Index of the suggested option.",
          "type": [
            "integer",
            "null"
          ],
          "format": "uint",
          "minimum": 0
        }
      },
      "additionalProperties": false,
      "required": [
        "id",
        "question"
      ]
    },
    "QuestionOption": {
      "description": "One selectable answer to a [`Question`].",
      "type": "object",
      "properties": {
        "description": {
          "description": "One-line explanation shown under the label.",
          "type": [
            "string",
            "null"
          ]
        },
        "label": {
          "description": "Answer text; unique within its question and returned in `selected`.",
          "type": "string"
        },
        "preview": {
          "description": "Markdown shown while the option is focused (code blocks allowed).",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "additionalProperties": false,
      "required": [
        "label"
      ]
    },
    "RequestInput": {
      "description": "Request `input`. Only `kind: question` requests interpret `questions`;\nother keys (for example raw agent tool input) pass through unchanged.",
      "type": "object",
      "properties": {
        "feedback": {
          "description": "`optional` (default) or `required_on_deny`.",
          "type": [
            "string",
            "null"
          ]
        },
        "fields": {
          "description": "Generic form fields (`text`, `textarea`, `select`, `boolean`) for non-question requests.",
          "type": [
            "array",
            "null"
          ],
          "items": true
        },
        "questions": {
          "description": "Structured questions (1..=10) for `kind: question`; answers arrive in `decision.fields.answers`.",
          "type": [
            "array",
            "null"
          ],
          "items": {
            "$ref": "#/$defs/Question"
          }
        }
      }
    },
    "Risk": {
      "type": "string",
      "enum": [
        "low",
        "medium",
        "high"
      ]
    },
    "RunClaim": {
      "description": "The handup desktop app on this computer is running the request's command\nright now. While the claim holds, the request stays pending but no other\ndecision, expiry, or timeout applies; cancelling it stops the run. Set and\ncleared only by the daemon.",
      "type": "object",
      "properties": {
        "elevated": {
          "description": "Running with administrator rights.",
          "type": "boolean",
          "default": false
        },
        "lease_until": {
          "description": "Unix milliseconds after which an unreported run no longer holds the\nrequest (the app crashed or lost the daemon).",
          "type": "integer",
          "format": "uint64",
          "minimum": 0
        },
        "started_at": {
          "description": "Unix milliseconds when the run was claimed.",
          "type": "integer",
          "format": "uint64",
          "minimum": 0
        }
      },
      "additionalProperties": false,
      "required": [
        "started_at",
        "lease_until"
      ]
    },
    "RunResult": {
      "description": "Outcome of running a command request in the handup desktop app. The\ncommand already ran: an agent must not run it again. Output is the\nredacted tail of each stream.",
      "type": "object",
      "properties": {
        "duration_ms": {
          "description": "Wall time from start to exit, in milliseconds.",
          "type": "integer",
          "format": "uint64",
          "default": 0,
          "minimum": 0
        },
        "elevated": {
          "description": "Ran with administrator rights (pkexec on Linux, osascript on macOS).",
          "type": "boolean",
          "default": false
        },
        "error": {
          "description": "Why it did not finish normally: timeout, cancellation, signal, or a\nstart failure (for example pkexec missing or authentication refused).",
          "type": [
            "string",
            "null"
          ]
        },
        "exit_code": {
          "description": "Process exit status; absent when it never started or a signal ended it.",
          "type": [
            "integer",
            "null"
          ],
          "format": "int32"
        },
        "stderr_tail": {
          "description": "Last bytes of standard error (at most 64 KiB), credentials redacted.",
          "type": "string",
          "default": ""
        },
        "stdout_tail": {
          "description": "Last bytes of standard output (at most 64 KiB), credentials redacted.",
          "type": "string",
          "default": ""
        },
        "truncated": {
          "description": "Earlier output was dropped from either tail.",
          "type": "boolean",
          "default": false
        }
      },
      "additionalProperties": false
    },
    "Source": {
      "type": "object",
      "properties": {
        "agent": {
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "branch": {
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "cwd": {
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "host": {
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "integration": {
          "description": "Server-verified integration token name; caller values are overwritten.",
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "repo": {
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "session": {
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "session_title": {
          "description": "Display-only session name; scoped rules always use the session ID.",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "additionalProperties": false
    },
    "Status": {
      "type": "string",
      "enum": [
        "pending",
        "approved",
        "denied",
        "answered",
        "expired",
        "cancelled"
      ]
    }
  }
}
