{
 "openapi": "3.1.0",
 "info": {
  "title": "Sandbox Gateway",
  "version": "1",
  "description": "Sandboxes (Kubernetes Pods) as a service. Every request needs Authorization: Bearer <team token>. Errors are JSON {\"detail\", \"code\"}: code is stable (not_found, invalid_token, builds_disabled, sandbox_not_running, sandboxd_unreachable, no_running_build, ...), detail is for people; sandboxd_unreachable (502) says why when the Pod does, e.g. main exited 137 (OOMKilled). Another team's sandbox is 404. A sandbox unused for the team's lease is deleted; one past the team's lifetime cap is deleted whatever it is doing. Responses carry X-Request-ID for log correlation: 1–64 ASCII letters, digits, hyphen or underscore (invalid IDs are replaced); this header is not a tracing span ID."
 },
 "security": [
  {
   "bearer": []
  }
 ],
 "paths": {
  "/sandboxes": {
   "post": {
    "summary": "Create sandbox",
    "description": "Asynchronous; queued within team quota.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/SandboxSpec"
       }
      }
     }
    },
    "responses": {
     "202": {
      "description": "Accepted",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "id": {
           "type": "string"
          }
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "422": {
      "$ref": "#/components/responses/Error"
     }
    }
   },
   "get": {
    "summary": "List team sandboxes",
    "parameters": [
     {
      "name": "label",
      "in": "query",
      "schema": {
       "type": "array",
       "items": {
        "type": "string"
       }
      },
      "style": "form",
      "explode": true,
      "description": "k=v (value must match) or k (any value); repeat to AND"
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "array",
         "items": {
          "$ref": "#/components/schemas/SandboxRow"
         }
        }
       }
      }
     }
    }
   }
  },
  "/sandboxes/{id}": {
   "parameters": [
    {
     "$ref": "#/components/parameters/id"
    }
   ],
   "get": {
    "summary": "Sandbox state",
    "description": "wait=N long-polls up to 60 s until ready or failed.",
    "parameters": [
     {
      "name": "wait",
      "in": "query",
      "schema": {
       "type": "number"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "id": {
           "type": "string"
          },
          "state": {
           "type": "string",
           "enum": [
            "queued",
            "starting",
            "ready",
            "failed"
           ]
          },
          "reason": {
           "type": "string",
           "description": "Where the sandbox stands, for every state but ready: Kueue quota (queued), no node fits (the scheduler's message), image pull (errors keep it starting for 5 min), sandboxd install or start; for failed, the cause."
          },
          "phase": {
           "type": "string",
           "enum": ["queued", "scheduling", "installing", "pulling", "starting", "ready", "failed"],
           "description": "Detailed startup stage; state remains compatible with existing clients. Dockerfile build/push precedes sandbox creation and is tracked through /images."
          },
          "image_source": {
           "type": "string",
           "enum": ["gateway", "external"],
           "description": "gateway means the configured build repository; it does not prove a retained build Job exists for this reference."
          },
          "image_status": {
           "type": "string",
           "enum": ["unknown", "pending", "pulling", "available", "missing", "pull_failed"],
           "description": "Main image status from the Pod informer, not a live registry query. Available includes a node-cached image; missing is the kubelet's not-found error."
          }
         }
        }
       }
      }
     },
     "404": {
      "$ref": "#/components/responses/Error"
     }
    }
   },
   "delete": {
    "summary": "Delete sandbox",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "id": {
           "type": "string"
          },
          "deleted": {
           "type": "boolean"
          }
         }
        }
       }
      }
     },
     "404": {
      "$ref": "#/components/responses/Error"
     }
    }
   }
  },
  "/sandboxes/{id}/result": {
   "parameters": [
    {
     "$ref": "#/components/parameters/id"
    }
   ],
   "post": {
    "summary": "Report the task result",
    "description": "The client's verdict on its own sandbox, kept on the Pod annotation sandbox-result and returned here. The gateway runs argv and sees exit codes; trial success and score are known only to the client. The dashboard does not show it (clients may never report). Report before deleting the sandbox.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/SandboxResult"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/SandboxResult"
        }
       }
      }
     },
     "404": {
      "$ref": "#/components/responses/Error"
     },
     "422": {
      "$ref": "#/components/responses/Error"
     }
    }
   }
  },
  "/sandboxes/{id}/run": {
   "parameters": [
    {
     "$ref": "#/components/parameters/id"
    }
   ],
   "post": {
    "summary": "Run command (JSON)",
    "description": "Text output; max 32 MiB.",
    "parameters": [
     {
      "name": "container",
      "in": "query",
      "schema": {
       "type": "string"
      },
      "description": "A compose service instead of main"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/RunRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/RunResult"
        }
       }
      }
     },
     "409": {
      "$ref": "#/components/responses/Error"
     },
     "422": {
      "$ref": "#/components/responses/Error"
     },
     "507": {
      "$ref": "#/components/responses/Error"
     }
    }
   }
  },
  "/sandboxes/{id}/exec": {
   "parameters": [
    {
     "$ref": "#/components/parameters/id"
    }
   ],
   "post": {
    "summary": "Run command (binary)",
    "description": "Request: JSON line {argv, timeout_sec, env?, cwd?} + newline + stdin. Response: stdout + stderr. Headers X-Return-Code, X-Stdout-Length, X-Stderr-Length, X-Timed-Out. Request and response may be gzipped. For binary data and large output. env and cwd are supported in main and compose services; env values are excluded from gateway command audit/history. Compose env is passed in the native Kubernetes exec wrapper argv.",
    "parameters": [
     {
      "name": "container",
      "in": "query",
      "schema": {
       "type": "string"
      },
      "description": "A compose service instead of main"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/octet-stream": {
       "schema": {
        "type": "string",
        "format": "binary"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "stdout followed by stderr",
      "content": {
       "application/octet-stream": {
        "schema": {
         "type": "string",
         "format": "binary"
        }
       }
      }
     },
     "409": {
      "$ref": "#/components/responses/Error"
     }
    }
   }
  },
  "/sandboxes/{id}/files": {
   "parameters": [
    {
     "$ref": "#/components/parameters/id"
    },
    {
     "name": "path",
     "in": "query",
     "required": true,
     "schema": {
      "type": "string"
     },
     "description": "Absolute path in main"
    },
    {
     "name": "tar",
     "in": "query",
     "schema": {
      "type": "string",
      "enum": [
       "1",
       "auto"
      ]
     },
     "description": "1: directory as tar.gz; auto: detect file or directory on GET (PUT uses 1 only)"
    }
   ],
   "get": {
    "summary": "Download file or directory",
    "description": "tar=1: directory as tar.gz.",
    "responses": {
     "200": {
      "description": "File bytes or tar.gz",
      "content": {
       "application/octet-stream": {
        "schema": {
         "type": "string",
         "format": "binary"
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "403": {
      "$ref": "#/components/responses/Error"
     },
     "404": {
      "$ref": "#/components/responses/Error"
     }
    }
   },
   "put": {
    "summary": "Upload file or directory",
    "description": "tar=1: unpack tar.gz. Creates parent directories.",
    "requestBody": {
     "required": true,
     "content": {
      "application/octet-stream": {
       "schema": {
        "type": "string",
        "format": "binary"
       }
      }
     }
    },
    "responses": {
     "204": {
      "description": "Written"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     }
    }
   }
  },
  "/sandboxes/{id}/sessions": {
   "parameters": [
    {
     "$ref": "#/components/parameters/id"
    }
   ],
   "post": {
    "summary": "Start background process",
    "description": "Runs in main.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "argv"
        ],
        "properties": {
         "argv": {
          "type": "array",
          "items": {
           "type": "string"
          }
         },
         "env": {
          "type": "object",
          "additionalProperties": {
           "type": "string"
          }
         },
         "cwd": {
          "type": "string"
         }
        }
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "Started",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Session"
        }
       }
      }
     }
    }
   },
   "get": {
    "summary": "List background processes",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "array",
         "items": {
          "$ref": "#/components/schemas/Session"
         }
        }
       }
      }
     }
    }
   }
  },
  "/sandboxes/{id}/sessions/{sid}": {
   "parameters": [
    {
     "$ref": "#/components/parameters/id"
    },
    {
     "name": "sid",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     }
    }
   ],
   "get": {
    "summary": "Process state",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Session"
        }
       }
      }
     }
    }
   },
   "delete": {
    "summary": "Kill process and delete logs",
    "description": "Kills the process group.",
    "responses": {
     "200": {
      "description": "Killed"
     }
    }
   }
  },
  "/sandboxes/{id}/sessions/{sid}/logs": {
   "parameters": [
    {
     "$ref": "#/components/parameters/id"
    },
    {
     "name": "sid",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     }
    },
    {
     "name": "stream",
     "in": "query",
     "schema": {
      "type": "string",
      "enum": [
       "stdout",
       "stderr"
      ]
     }
    },
    {
     "name": "offset",
     "in": "query",
     "schema": {
      "type": "integer"
     }
    },
    {
     "name": "follow",
     "in": "query",
     "schema": {
      "type": "string",
      "enum": [
       "1"
      ]
     }
    }
   ],
   "get": {
    "summary": "Process output",
    "description": "Read from byte offset; follow=1 streams until exit.",
    "responses": {
     "200": {
      "description": "Raw output",
      "content": {
       "application/octet-stream": {
        "schema": {
         "type": "string",
         "format": "binary"
        }
       }
      }
     }
    }
   }
  },
  "/sandboxes/{id}/sessions/{sid}/stdin": {
   "parameters": [
    {
     "$ref": "#/components/parameters/id"
    },
    {
     "name": "sid",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     }
    },
    {
     "name": "close",
     "in": "query",
     "schema": {
      "type": "string",
      "enum": [
       "1"
      ]
     }
    }
   ],
   "post": {
    "summary": "Write process stdin",
    "description": "Writes request body; close=1 closes stdin.",
    "requestBody": {
     "content": {
      "application/octet-stream": {
       "schema": {
        "type": "string",
        "format": "binary"
       }
      }
     }
    },
    "responses": {
     "204": {
      "description": "Written"
     }
    }
   }
  },
  "/sandboxes/{id}/history": {
   "parameters": [{"$ref": "#/components/parameters/id"}],
   "get": {
    "summary": "Sandbox history",
    "description": "Lifecycle, downloads, session snapshots and commands across replicas. Own team or admin; no lease renewal. Last 30 entries of each kind retained in replica memory, including for the last 2000 deleted sandboxes. Session exit codes appear only when the gateway observed them; not a task success report. Partial is true when a peer could not answer. Lost on replica restart.",
    "responses": {"200": {"description": "{events, sessions, commands, partial?}"}, "404": {"description": "Unknown sandbox or another team's history"}}
   }
  },
  "/sandboxes/{id}/activity": {
   "parameters": [
    {
     "$ref": "#/components/parameters/id"
    }
   ],
   "get": {
    "summary": "Command history",
    "description": "Running and last 30 commands; no lease renewal. Retained for the last 2000 deleted sandboxes.",
    "responses": {
     "200": {
      "description": "OK"
     }
    }
   }
  },
  "/sandboxes/{id}/events": {
   "parameters": [
    {
     "$ref": "#/components/parameters/id"
    }
   ],
   "get": {
    "summary": "Pod events",
    "description": "Available after deletion while Kubernetes retains them.",
    "responses": {
     "200": {
      "description": "OK"
     }
    }
   }
  },
  "/sandboxes/{id}/logs": {
   "parameters": [
    {
     "$ref": "#/components/parameters/id"
    },
    {
     "name": "container",
     "in": "query",
     "schema": {
      "type": "string"
     }
    },
    {
     "name": "tail",
     "in": "query",
     "schema": {
      "type": "integer"
     }
    }
   ],
   "get": {
    "summary": "Container logs",
    "description": "Tail; max 1 MiB.",
    "responses": {
     "200": {
      "description": "OK"
     }
    }
   }
  },
  "/sandboxes/{id}/diagnostics": {
   "parameters": [
    {
     "$ref": "#/components/parameters/id"
    }
   ],
   "get": {
    "summary": "Sandbox diagnostics",
    "description": "State, Pod, containers, resources and events. Unreadable sections appear in errors. Own team or admin; no lease renewal. Logs: /logs.",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "id": {
           "type": "string"
          },
          "team": {
           "type": "string"
          },
          "state": {
           "type": "string"
          },
          "reason": {
           "type": "string"
          },
          "created": {
           "type": "string",
           "format": "date-time"
          },
          "pod": {
           "type": "object"
          },
          "containers": {
           "type": "array",
           "items": {
            "type": "object"
           }
          },
          "events": {
           "type": "array",
           "items": {
            "type": "object"
           }
          },
          "errors": {
           "type": "object",
           "additionalProperties": {
            "type": "string"
           }
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/images": {
   "post": {
    "summary": "Build image",
    "description": "Tar.gz context with Dockerfile. ID: archive SHA-256; with build secrets, includes the team.",
    "parameters": [
     {
      "name": "X-Build-Secret",
      "in": "header",
      "description": "NAME=value, repeatable (up to 16): a BuildKit secret for RUN --mount=type=secret,id=NAME,env=NAME. Not stored, not in the image, masked in the build log. An image built with secrets is not shared with other teams.",
      "schema": {
       "type": "string"
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/gzip": {
       "schema": {
        "type": "string",
        "format": "binary"
       }
      }
     }
    },
    "responses": {
     "202": {
      "description": "Accepted",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "id": {
           "type": "string"
          }
         }
        }
       }
      }
     },
     "403": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Error"
     }
    }
   }
  },
  "/images/{id}": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     }
    },
    {
     "name": "wait",
     "in": "query",
     "schema": {
      "type": "number"
     }
    }
   ],
   "get": {
    "summary": "Build state",
    "description": "Kubernetes Job; any replica serves it. Use image when ready. wait=N long-polls up to 60 s. log=1 includes logs; failures always include them. retry=true: POST again.",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "state": {
           "type": "string",
           "enum": [
            "building",
            "ready",
            "failed"
           ]
          },
          "image": {
           "type": "string"
          },
          "log": {
           "type": "string"
          },
          "phase": {
           "type": "string",
           "enum": [
            "starting",
            "building"
           ],
           "description": "While building: its Pod is not running yet, or buildctl runs"
          },
          "started": {
           "type": "string",
           "format": "date-time"
          }
         }
        }
       }
      }
     }
    }
   },
   "delete": {
    "summary": "Cancel build",
    "description": "Deletes the Job; subsequent GET returns 404. POST rebuilds. Own team or admin.",
    "responses": {
     "204": {
      "description": "Cancelled"
     },
     "404": {
      "description": "No running build of this image for the team (code no_running_build)"
     }
    }
   }
  },
  "/version": {
   "get": {
    "summary": "Gateway and client versions",
    "description": "Public. Release, API and Harbor major.minor. API changes only for breaking changes.",
    "security": [],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "version": {
           "type": "string"
          },
          "api": {
           "type": "integer"
          },
          "harbor_version": {
           "type": "string",
           "description": "Supported Harbor major.minor derived from the adapter dependency; empty in development builds"
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/healthz": {
   "get": {
    "summary": "Liveness",
    "security": [],
    "responses": {
     "204": {
      "description": "Alive"
     }
    }
   }
  }
 },
 "components": {
  "securitySchemes": {
   "bearer": {
    "type": "http",
    "scheme": "bearer"
   }
  },
  "parameters": {
   "id": {
    "name": "id",
    "in": "path",
    "required": true,
    "schema": {
     "type": "string"
    }
   }
  },
  "responses": {
   "Error": {
    "description": "Error. A 404 for a recently deleted sandbox of the caller's team says when and why it was deleted (its client, the lease, the Pod deleted outside the gateway) and how its containers ended.",
    "content": {
     "application/json": {
      "schema": {
       "type": "object",
       "properties": {
        "detail": {
         "type": "string"
        }
       }
      }
     }
    }
   }
  },
  "schemas": {
   "SandboxSpec": {
    "type": "object",
    "required": [
     "image",
     "cpus",
     "memory_mb"
    ],
    "additionalProperties": false,
    "properties": {
     "image": {
      "type": "string"
     },
     "cpus": {
      "type": "number",
      "description": "CPU request; Kueue admits by it (at least 0.01)"
     },
     "memory_mb": {
      "type": "integer",
      "description": "Memory request and limit"
     },
     "storage_mb": {
      "type": "integer",
      "description": "Disk request and limit; default DEFAULT_STORAGE_MB"
     },
     "env": {
      "type": "object",
      "additionalProperties": {
       "type": "string"
      }
     },
     "compose": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "docker-compose files; services other than main become containers"
     },
     "compose_env": {
      "type": "object",
      "additionalProperties": {
       "type": "string"
      },
      "description": "${VAR} values for the compose files"
     },
     "name": {
      "type": "string",
      "description": "Shown in the dashboard"
     },
     "labels": {
      "type": "object",
      "additionalProperties": {
       "type": "string",
       "maxLength": 256
      },
      "maxProperties": 16,
      "description": "Client tags; keys [a-z0-9_.-], up to 63 characters"
     }
    }
   },
   "SandboxRow": {
    "type": "object",
    "properties": {
     "process_failure": {
      "type": "object",
      "description": "Latest observed failed session from available replicas, retained with deleted history. Own team/admin only; not the client's task verdict. No argv or log bodies.",
      "properties": {
       "session_id": {"type": "string"},
       "exit_code": {"type": "integer"},
       "output_error": {"type": "boolean"},
       "observed_at": {"type": "string", "format": "date-time"}
      }
     },
     "phase": {
      "type": "string",
      "description": "queued, scheduling, installing, pulling, starting, ready or failed. For a deleted sandbox, the last observed phase."
     },
     "image_source": {
      "type": "string",
      "enum": ["gateway", "external"],
      "description": "Configured gateway build repository or an external reference; not a live registry lookup."
     },
     "image_status": {
      "type": "string",
      "enum": ["unknown", "pending", "pulling", "available", "missing", "pull_failed"],
      "description": "Main image status on the node; last observed after deletion."
     },
     "end_state": {
      "type": "string",
      "description": "Last observed Pod state before deletion; deletion does not erase startup failure."
     },
     "end_reason": {
      "type": "string",
      "description": "Last observed Pod-state reason before deletion, also used by reason filters."
     },
     "id": {
      "type": "string"
     },
     "team": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "image": {
      "type": "string"
     },
     "state": {
      "type": "string"
     },
     "reason": {
      "type": "string"
     },
     "labels": {
      "type": "object",
      "additionalProperties": {
       "type": "string"
      }
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     }
    }
   },
   "RunRequest": {
    "type": "object",
    "required": [
     "argv"
    ],
    "additionalProperties": false,
    "properties": {
     "argv": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "minItems": 1
     },
     "stdin": {
      "type": "string"
     },
     "timeout_sec": {
      "type": "number",
      "description": "Kills the process group; exit code 124"
     },
     "env": {
      "type": "object",
      "additionalProperties": {
       "type": "string"
      },
      "description": "main only; put secrets here, not in argv"
     },
     "cwd": {
      "type": "string",
      "description": "main only"
     }
    }
   },
   "SandboxResult": {
    "type": "object",
    "properties": {
     "status": {
      "type": "string",
      "enum": [
       "ok",
       "failed",
       "cancelled"
      ],
      "description": "The trial's verdict"
     },
     "reward": {
      "type": "number",
      "description": "Its score, when it has one"
     },
     "detail": {
      "type": "string",
      "description": "Free text, at most 500 characters"
     },
     "at": {
      "type": "string",
      "format": "date-time",
      "description": "When the client reported it"
     },
     "by": {
      "type": "string",
      "description": "The token that reported it"
     }
    },
    "required": [
     "status"
    ]
   },
   "RunResult": {
    "type": "object",
    "properties": {
     "exit_code": {
      "type": "integer"
     },
     "stdout": {
      "type": "string"
     },
     "stderr": {
      "type": "string"
     },
     "timed_out": {
      "type": "boolean"
     }
    }
   },
   "Session": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "argv": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "running": {
      "type": "boolean"
     },
     "exit_code": {
      "type": [
       "integer",
       "null"
      ]
     },
     "output_error": {
      "type": "string",
      "description": "Output collection failed; logs may be incomplete even when exit_code is zero. Omitted if no output error was observed."
     },
     "started_at": {
      "type": "string"
     }
    }
   }
  }
 }
}
