diff --git a/packages/api/internal/api/api.gen.go b/packages/api/internal/api/api.gen.go index ceab825b5e..0513b3de6e 100644 --- a/packages/api/internal/api/api.gen.go +++ b/packages/api/internal/api/api.gen.go @@ -11741,54 +11741,54 @@ var swaggerSpec = []string{ "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", - "wNXD/w8AAP//wduPstEWAQA=", + "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", + "F0LA1cP/DwAA//8aKX0Z0RYBAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/packages/envd/internal/api/api.gen.go b/packages/envd/internal/api/api.gen.go index 512747b05f..a1720172d9 100644 --- a/packages/envd/internal/api/api.gen.go +++ b/packages/envd/internal/api/api.gen.go @@ -67,17 +67,26 @@ type Metrics struct { // MemTotal Total virtual memory in bytes MemTotal *int `json:"mem_total,omitempty"` + // MemTotalMib Total virtual memory in MiB + MemTotalMib *int `json:"mem_total_mib,omitempty"` + // MemUsed Used virtual memory in bytes MemUsed *int `json:"mem_used,omitempty"` + // MemUsedMib Used virtual memory in MiB + MemUsedMib *int `json:"mem_used_mib,omitempty"` + // Ts Unix timestamp in UTC for current sandbox time Ts *int64 `json:"ts,omitempty"` } -// VolumeMount Volume +// VolumeMount NFS volume mount configuration type VolumeMount struct { + // NfsTarget NFS server target address NfsTarget string `json:"nfs_target"` - Path string `json:"path"` + + // Path Mount path inside the sandbox + Path string `json:"path"` } // FilePath defines model for FilePath. @@ -104,6 +113,9 @@ type InvalidPath = Error // InvalidUser defines model for InvalidUser. type InvalidUser = Error +// NotAcceptable defines model for NotAcceptable. +type NotAcceptable = Error + // NotEnoughDiskSpace defines model for NotEnoughDiskSpace. type NotEnoughDiskSpace = Error @@ -112,16 +124,16 @@ type UploadSuccess = []EntryInfo // GetFilesParams defines parameters for GetFiles. type GetFilesParams struct { - // Path Path to the file, URL encoded. Can be relative to user's home directory. + // Path Path to the file, URL encoded. Can be relative to the user's home directory (e.g. "file.txt" resolves to ~/file.txt). Path *FilePath `form:"path,omitempty" json:"path,omitempty"` - // Username User used for setting the owner, or resolving relative paths. + // Username User for setting file ownership and resolving relative paths. Defaults to the sandbox's default user. Username *User `form:"username,omitempty" json:"username,omitempty"` - // Signature Signature used for file access permission verification. + // Signature HMAC signature for access verification. Required when no X-Access-Token header is provided. Format is "v1_". Signature *Signature `form:"signature,omitempty" json:"signature,omitempty"` - // SignatureExpiration Signature expiration used for defining the expiration time of the signature. + // SignatureExpiration Unix timestamp (seconds) after which the signature expires. Only used with the signature parameter. SignatureExpiration *SignatureExpiration `form:"signature_expiration,omitempty" json:"signature_expiration,omitempty"` } @@ -132,16 +144,16 @@ type PostFilesMultipartBody struct { // PostFilesParams defines parameters for PostFiles. type PostFilesParams struct { - // Path Path to the file, URL encoded. Can be relative to user's home directory. + // Path Path to the file, URL encoded. Can be relative to the user's home directory (e.g. "file.txt" resolves to ~/file.txt). Path *FilePath `form:"path,omitempty" json:"path,omitempty"` - // Username User used for setting the owner, or resolving relative paths. + // Username User for setting file ownership and resolving relative paths. Defaults to the sandbox's default user. Username *User `form:"username,omitempty" json:"username,omitempty"` - // Signature Signature used for file access permission verification. + // Signature HMAC signature for access verification. Required when no X-Access-Token header is provided. Format is "v1_". Signature *Signature `form:"signature,omitempty" json:"signature,omitempty"` - // SignatureExpiration Signature expiration used for defining the expiration time of the signature. + // SignatureExpiration Unix timestamp (seconds) after which the signature expires. Only used with the signature parameter. SignatureExpiration *SignatureExpiration `form:"signature_expiration,omitempty" json:"signature_expiration,omitempty"` } diff --git a/packages/envd/spec/envd.yaml b/packages/envd/spec/envd.yaml index 83091f1ab5..fb1997b18f 100644 --- a/packages/envd/spec/envd.yaml +++ b/packages/envd/spec/envd.yaml @@ -97,12 +97,14 @@ paths: responses: "200": $ref: "#/components/responses/DownloadSuccess" - "401": - $ref: "#/components/responses/InvalidUser" "400": $ref: "#/components/responses/InvalidPath" + "401": + $ref: "#/components/responses/InvalidUser" "404": $ref: "#/components/responses/FileNotFound" + "406": + $ref: "#/components/responses/NotAcceptable" "500": $ref: "#/components/responses/InternalServerError" post: @@ -142,28 +144,28 @@ components: name: path in: query required: false - description: Path to the file, URL encoded. Can be relative to user's home directory. + description: Path to the file, URL encoded. Can be relative to the user's home directory (e.g. "file.txt" resolves to ~/file.txt). schema: type: string User: name: username in: query required: false - description: User used for setting the owner, or resolving relative paths. + description: User for setting file ownership and resolving relative paths. Defaults to the sandbox's default user. schema: type: string Signature: name: signature in: query required: false - description: Signature used for file access permission verification. + description: HMAC signature for access verification. Required when no X-Access-Token header is provided. Format is "v1_". schema: type: string SignatureExpiration: name: signature_expiration in: query required: false - description: Signature expiration used for defining the expiration time of the signature. + description: Unix timestamp (seconds) after which the signature expires. Only used with the signature parameter. schema: type: integer @@ -188,45 +190,73 @@ components: type: array items: $ref: "#/components/schemas/EntryInfo" + example: + - path: "/home/user/hello.txt" + name: "hello.txt" + type: "file" DownloadSuccess: - description: Entire file downloaded successfully. + description: File content. Content-Type is detected from the file extension (defaults to application/octet-stream). Content-Disposition header contains the filename. content: application/octet-stream: schema: type: string format: binary - description: The file content + description: The raw file content + NotAcceptable: + description: Requested encoding is not supported + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + message: "no acceptable encoding found, supported: [identity, gzip]" + code: 406 InvalidPath: description: Invalid path content: application/json: schema: $ref: "#/components/schemas/Error" + example: + message: "path '/home/user/docs' is a directory" + code: 400 InternalServerError: description: Internal server error content: application/json: schema: $ref: "#/components/schemas/Error" + example: + message: "error opening file '/home/user/file.txt': permission denied" + code: 500 FileNotFound: description: File not found content: application/json: schema: $ref: "#/components/schemas/Error" + example: + message: "path '/home/user/missing.txt' does not exist" + code: 404 InvalidUser: description: Invalid user content: application/json: schema: $ref: "#/components/schemas/Error" + example: + message: "error looking up user 'nonexistent': user: unknown user nonexistent" + code: 401 NotEnoughDiskSpace: description: Not enough disk space content: application/json: schema: $ref: "#/components/schemas/Error" + example: + message: "not enough disk space available" + code: 507 schemas: Error: @@ -283,6 +313,12 @@ components: mem_used: type: integer description: Used virtual memory in bytes + mem_total_mib: + type: integer + description: Total virtual memory in MiB + mem_used_mib: + type: integer + description: Used virtual memory in MiB disk_used: type: integer description: Used disk space in bytes @@ -291,13 +327,15 @@ components: description: Total disk space in bytes VolumeMount: type: object - description: Volume + description: NFS volume mount configuration additionalProperties: false properties: nfs_target: type: string + description: NFS server target address path: type: string + description: Mount path inside the sandbox required: - nfs_target - path diff --git a/packages/orchestrator/internal/sandbox/envd/envd.gen.go b/packages/orchestrator/internal/sandbox/envd/envd.gen.go index 73d03e999b..12060f0922 100644 --- a/packages/orchestrator/internal/sandbox/envd/envd.gen.go +++ b/packages/orchestrator/internal/sandbox/envd/envd.gen.go @@ -62,17 +62,26 @@ type Metrics struct { // MemTotal Total virtual memory in bytes MemTotal int `json:"mem_total,omitempty"` + // MemTotalMib Total virtual memory in MiB + MemTotalMib int `json:"mem_total_mib,omitempty"` + // MemUsed Used virtual memory in bytes MemUsed int `json:"mem_used,omitempty"` + // MemUsedMib Used virtual memory in MiB + MemUsedMib int `json:"mem_used_mib,omitempty"` + // Ts Unix timestamp in UTC for current sandbox time Ts int64 `json:"ts,omitempty"` } -// VolumeMount Volume +// VolumeMount NFS volume mount configuration type VolumeMount struct { + // NfsTarget NFS server target address NfsTarget string `json:"nfs_target"` - Path string `json:"path"` + + // Path Mount path inside the sandbox + Path string `json:"path"` } // FilePath defines model for FilePath. @@ -99,6 +108,9 @@ type InvalidPath = Error // InvalidUser defines model for InvalidUser. type InvalidUser = Error +// NotAcceptable defines model for NotAcceptable. +type NotAcceptable = Error + // NotEnoughDiskSpace defines model for NotEnoughDiskSpace. type NotEnoughDiskSpace = Error @@ -107,16 +119,16 @@ type UploadSuccess = []EntryInfo // GetFilesParams defines parameters for GetFiles. type GetFilesParams struct { - // Path Path to the file, URL encoded. Can be relative to user's home directory. + // Path Path to the file, URL encoded. Can be relative to the user's home directory (e.g. "file.txt" resolves to ~/file.txt). Path FilePath `form:"path,omitempty" json:"path,omitempty"` - // Username User used for setting the owner, or resolving relative paths. + // Username User for setting file ownership and resolving relative paths. Defaults to the sandbox's default user. Username User `form:"username,omitempty" json:"username,omitempty"` - // Signature Signature used for file access permission verification. + // Signature HMAC signature for access verification. Required when no X-Access-Token header is provided. Format is "v1_". Signature Signature `form:"signature,omitempty" json:"signature,omitempty"` - // SignatureExpiration Signature expiration used for defining the expiration time of the signature. + // SignatureExpiration Unix timestamp (seconds) after which the signature expires. Only used with the signature parameter. SignatureExpiration SignatureExpiration `form:"signature_expiration,omitempty" json:"signature_expiration,omitempty"` } @@ -127,16 +139,16 @@ type PostFilesMultipartBody struct { // PostFilesParams defines parameters for PostFiles. type PostFilesParams struct { - // Path Path to the file, URL encoded. Can be relative to user's home directory. + // Path Path to the file, URL encoded. Can be relative to the user's home directory (e.g. "file.txt" resolves to ~/file.txt). Path FilePath `form:"path,omitempty" json:"path,omitempty"` - // Username User used for setting the owner, or resolving relative paths. + // Username User for setting file ownership and resolving relative paths. Defaults to the sandbox's default user. Username User `form:"username,omitempty" json:"username,omitempty"` - // Signature Signature used for file access permission verification. + // Signature HMAC signature for access verification. Required when no X-Access-Token header is provided. Format is "v1_". Signature Signature `form:"signature,omitempty" json:"signature,omitempty"` - // SignatureExpiration Signature expiration used for defining the expiration time of the signature. + // SignatureExpiration Unix timestamp (seconds) after which the signature expires. Only used with the signature parameter. SignatureExpiration SignatureExpiration `form:"signature_expiration,omitempty" json:"signature_expiration,omitempty"` } diff --git a/scripts/generate_openapi.py b/scripts/generate_openapi.py new file mode 100644 index 0000000000..d18554fcaa --- /dev/null +++ b/scripts/generate_openapi.py @@ -0,0 +1,758 @@ +#!/usr/bin/env python3 +"""Generate a merged OpenAPI spec for the full E2B developer-facing API. + +Combines multiple sources into a single e2b-openapi.yml: + + Sandbox API (served on -.e2b.app): + - Proto-generated OpenAPI for process/filesystem Connect RPC + - Hand-written REST spec (packages/envd/spec/envd.yaml) + - Auto-generated stubs for streaming RPCs (parsed from .proto files) + + Platform API (served on api.e2b.app): + - Main E2B API spec (spec/openapi.yml) + +Usage: + python3 scripts/generate-openapi/envd.py + +Outputs e2b-openapi.yml in the current working directory. +Requires: Docker, PyYAML (pip install pyyaml). +""" + +from __future__ import annotations + +import os +import re +import subprocess +import sys +import tempfile +from dataclasses import dataclass +from glob import glob +from typing import Any + +import yaml + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +REPO_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, "..")) + +# Sandbox (envd) specs +ENVD_SPEC_DIR = os.path.join(REPO_ROOT, "packages/envd/spec") +ENVD_REST_SPEC = os.path.join(ENVD_SPEC_DIR, "envd.yaml") + +# Platform API specs +API_SPEC = os.path.join(REPO_ROOT, "spec/openapi.yml") + +DOCKER_IMAGE = "protoc-gen-connect-openapi" + +DOCKERFILE = """\ +FROM golang:1.25-alpine +RUN apk add --no-cache git +RUN go install github.com/bufbuild/buf/cmd/buf@v1.50.0 +RUN go install github.com/sudorandom/protoc-gen-connect-openapi@v0.25.3 +ENV PATH="/go/bin:${PATH}" +""" + +BUF_GEN_YAML = """\ +version: v1 +plugins: + - plugin: connect-openapi + out: /output + opt: + - format=yaml +""" + +# Server definitions for the two API surfaces +SANDBOX_SERVER = { + "url": "https://{port}-{sandboxID}.e2b.app", + "description": "Sandbox API (envd) — runs inside each sandbox", + "variables": { + "port": {"default": "49983", "description": "Port number"}, + "sandboxID": {"default": "{sandbox-id}", "description": "Sandbox identifier"}, + }, +} + +PLATFORM_SERVER = { + "url": "https://api.e2b.app", + "description": "E2B Platform API", +} + +# Tag used to mark sandbox-specific paths so we can attach the right server +SANDBOX_TAG = "x-e2b-server" + +# Security scheme name for envd endpoints (must not collide with platform's AccessTokenAuth) +SANDBOX_AUTH_SCHEME = "SandboxAccessTokenAuth" + +# --------------------------------------------------------------------------- +# Proto parsing — auto-detect streaming RPCs +# --------------------------------------------------------------------------- + +@dataclass +class RpcMethod: + """An RPC method parsed from a .proto file.""" + + package: str + service: str + method: str + request_type: str + response_type: str + client_streaming: bool + server_streaming: bool + comment: str + + @property + def path(self) -> str: + return f"/{self.package}.{self.service}/{self.method}" + + @property + def tag(self) -> str: + return f"{self.package}.{self.service}" + + @property + def operation_id(self) -> str: + return f"{self.package}.{self.service}.{self.method}" + + @property + def request_schema_ref(self) -> str: + return f"#/components/schemas/{self.package}.{self.request_type}" + + @property + def response_schema_ref(self) -> str: + return f"#/components/schemas/{self.package}.{self.response_type}" + + @property + def is_streaming(self) -> bool: + return self.client_streaming or self.server_streaming + + @property + def streaming_label(self) -> str: + if self.client_streaming and self.server_streaming: + return "Bidirectional-streaming" + if self.client_streaming: + return "Client-streaming" + if self.server_streaming: + return "Server-streaming" + return "Unary" + + +_PACKAGE_RE = re.compile(r"^package\s+(\w+)\s*;", re.MULTILINE) +_SERVICE_RE = re.compile(r"service\s+(\w+)\s*\{", re.MULTILINE) +_RPC_RE = re.compile( + r"rpc\s+(\w+)\s*\(\s*(stream\s+)?(\w+)\s*\)\s*returns\s*\(\s*(stream\s+)?(\w+)\s*\)" +) + + +def parse_proto_file(path: str) -> list[RpcMethod]: + """Parse a .proto file and return all RPC methods found.""" + with open(path) as f: + content = f.read() + + pkg_match = _PACKAGE_RE.search(content) + if not pkg_match: + return [] + package = pkg_match.group(1) + + methods: list[RpcMethod] = [] + + for svc_match in _SERVICE_RE.finditer(content): + service_name = svc_match.group(1) + brace_start = content.index("{", svc_match.start()) + depth, pos = 1, brace_start + 1 + while depth > 0 and pos < len(content): + if content[pos] == "{": + depth += 1 + elif content[pos] == "}": + depth -= 1 + pos += 1 + service_body = content[brace_start:pos] + + for rpc_match in _RPC_RE.finditer(service_body): + rpc_start = service_body.rfind("\n", 0, rpc_match.start()) + comment = _extract_comment(service_body, rpc_start) + + methods.append(RpcMethod( + package=package, + service=service_name, + method=rpc_match.group(1), + request_type=rpc_match.group(3), + response_type=rpc_match.group(5), + client_streaming=bool(rpc_match.group(2)), + server_streaming=bool(rpc_match.group(4)), + comment=comment, + )) + + return methods + + +def _extract_comment(text: str, before_pos: int) -> str: + """Extract // comment lines immediately above a position in text.""" + lines = text[:before_pos].rstrip().split("\n") + comment_lines: list[str] = [] + for line in reversed(lines): + stripped = line.strip() + if stripped.startswith("//"): + comment_lines.append(stripped.lstrip("/ ")) + elif stripped == "": + continue + else: + break + comment_lines.reverse() + return " ".join(comment_lines) + + +def find_streaming_rpcs(spec_dir: str) -> list[RpcMethod]: + """Scan all .proto files under spec_dir and return streaming RPCs.""" + streaming: list[RpcMethod] = [] + for proto_path in sorted(glob(os.path.join(spec_dir, "**/*.proto"), recursive=True)): + for rpc in parse_proto_file(proto_path): + if rpc.is_streaming: + streaming.append(rpc) + return streaming + + +def build_streaming_path(rpc: RpcMethod) -> dict[str, Any]: + """Build an OpenAPI path item for a streaming RPC.""" + description = ( + f"{rpc.streaming_label} RPC. " + f"{rpc.comment + '. ' if rpc.comment else ''}" + f"Use the Connect protocol with streaming support." + ) + return { + "post": { + "tags": [rpc.tag], + "summary": rpc.method, + "description": description, + "operationId": rpc.operation_id, + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": rpc.request_schema_ref} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": f"Stream of {rpc.response_type} events", + "content": { + "application/json": { + "schema": {"$ref": rpc.response_schema_ref} + } + }, + }, + }, + } + } + + +# --------------------------------------------------------------------------- +# Docker build & proto generation +# --------------------------------------------------------------------------- + +def docker_build_image() -> None: + """Build the Docker image with buf + protoc-gen-connect-openapi.""" + print("==> Building Docker image") + with tempfile.NamedTemporaryFile(mode="w", suffix=".Dockerfile", delete=False) as f: + f.write(DOCKERFILE) + dockerfile_path = f.name + try: + subprocess.run( + ["docker", "build", "-t", DOCKER_IMAGE, "-f", dockerfile_path, "."], + check=True, + cwd=REPO_ROOT, + ) + finally: + os.unlink(dockerfile_path) + + +def docker_generate_specs() -> list[str]: + """Run buf generate inside Docker, return list of generated YAML strings.""" + print("==> Generating OpenAPI specs from proto files") + with tempfile.TemporaryDirectory() as tmpdir: + buf_gen_path = os.path.join(tmpdir, "buf.gen.yaml") + with open(buf_gen_path, "w") as f: + f.write(BUF_GEN_YAML) + + output_dir = os.path.join(tmpdir, "output") + os.makedirs(output_dir) + + subprocess.run( + [ + "docker", "run", "--rm", + "-v", f"{ENVD_SPEC_DIR}:/spec:ro", + "-v", f"{buf_gen_path}:/config/buf.gen.yaml:ro", + "-v", f"{output_dir}:/output", + DOCKER_IMAGE, + "sh", "-c", + "cd /spec && buf generate --template /config/buf.gen.yaml", + ], + check=True, + ) + + generated: list[str] = [] + for root, _, files in os.walk(output_dir): + for name in sorted(files): + if name.endswith((".yaml", ".yml")): + path = os.path.join(root, name) + rel = os.path.relpath(path, output_dir) + print(f" Generated: {rel}") + with open(path) as f: + generated.append(f.read()) + + if not generated: + print("ERROR: No files were generated", file=sys.stderr) + sys.exit(1) + + return generated + + +# --------------------------------------------------------------------------- +# OpenAPI merging & post-processing +# --------------------------------------------------------------------------- + +def load_yaml_file(path: str) -> str: + """Load a YAML file and return its raw content.""" + print(f"==> Loading spec: {os.path.relpath(path, REPO_ROOT)}") + with open(path) as f: + return f.read() + + +def merge_specs(raw_docs: list[str], protected_paths: set[str] | None = None) -> dict[str, Any]: + """Merge multiple raw YAML OpenAPI docs into a single spec. + + Args: + raw_docs: Raw YAML strings to merge (order matters — later docs + overwrite earlier ones for paths and component entries). + protected_paths: Paths that should not be overwritten once set. + Used to prevent the platform API from overwriting + envd paths that share the same name (e.g. /health). + """ + merged: dict[str, Any] = { + "openapi": "3.1.0", + "info": { + "title": "E2B API", + "version": "0.1.0", + "description": ( + "Complete E2B developer API. " + "Platform endpoints are served on api.e2b.app. " + "Sandbox endpoints (envd) are served on {port}-{sandboxID}.e2b.app." + ), + }, + "servers": [PLATFORM_SERVER], + "paths": {}, + "components": {}, + } + + for raw in raw_docs: + doc = yaml.safe_load(raw) + if not doc: + continue + + for path, methods in doc.get("paths", {}).items(): + if protected_paths and path in protected_paths and path in merged["paths"]: + continue + merged["paths"][path] = methods + + for section, entries in doc.get("components", {}).items(): + if isinstance(entries, dict): + merged["components"].setdefault(section, {}).update(entries) + + if "tags" in doc: + merged.setdefault("tags", []).extend(doc["tags"]) + + if "security" in doc: + existing = merged.setdefault("security", []) + for entry in doc["security"]: + if entry not in existing: + existing.append(entry) + + return merged + + +def tag_paths_with_server( + spec: dict[str, Any], + paths: set[str], + server: dict[str, Any], +) -> None: + """Attach a specific server override to a set of paths. + + OpenAPI 3.1 allows per-path server overrides so clients know which + base URL to use for each endpoint. + """ + for path, path_item in spec["paths"].items(): + if path in paths: + path_item["servers"] = [server] + + +def fill_streaming_endpoints(spec: dict[str, Any], streaming_rpcs: list[RpcMethod]) -> None: + """Replace empty {} streaming path items with proper OpenAPI definitions. + + protoc-gen-connect-openapi emits {} for streaming RPCs because OpenAPI + has no native streaming representation. We detect these from the proto + files and fill them in with proper request/response schemas. + """ + for rpc in streaming_rpcs: + if rpc.path in spec["paths"]: + print(f" Filling streaming endpoint: {rpc.path} ({rpc.streaming_label})") + spec["paths"][rpc.path] = build_streaming_path(rpc) + + +def apply_sandbox_auth(spec: dict[str, Any], envd_paths: set[str]) -> None: + """Ensure all envd/sandbox endpoints declare the SandboxAccessTokenAuth security. + + The hand-written envd.yaml already has security declarations, but the + proto-generated Connect RPC endpoints don't. Add optional auth + (SandboxAccessTokenAuth or anonymous) to any envd endpoint missing it. + """ + auth_security = [{SANDBOX_AUTH_SCHEME: []}, {}] + for path in envd_paths: + path_item = spec["paths"].get(path) + if not path_item: + continue + for method in ("get", "post", "put", "patch", "delete"): + op = path_item.get(method) + if op and "security" not in op: + op["security"] = auth_security + + +def fix_security_schemes(spec: dict[str, Any]) -> None: + """Fix invalid apiKey securityScheme syntax. + + The source envd.yaml uses `scheme: header` which is wrong for + type: apiKey — OpenAPI requires `in: header` instead. + """ + for scheme in spec.get("components", {}).get("securitySchemes", {}).values(): + if scheme.get("type") == "apiKey" and "scheme" in scheme: + scheme["in"] = scheme.pop("scheme") + + +def rename_envd_auth_scheme(spec: dict[str, Any]) -> None: + """Rename AccessTokenAuth → SandboxAccessTokenAuth in the merged spec. + + The source envd.yaml uses AccessTokenAuth for code generation compatibility, + but the public docs need SandboxAccessTokenAuth to avoid collisions with + the platform API's AccessTokenAuth scheme. + """ + old_name = "AccessTokenAuth" + new_name = SANDBOX_AUTH_SCHEME + schemes = spec.get("components", {}).get("securitySchemes", {}) + if old_name in schemes: + schemes[new_name] = schemes.pop(old_name) + # Update all security references in operations + for path_item in spec.get("paths", {}).values(): + for method in ("get", "post", "put", "patch", "delete", "head", "options"): + op = path_item.get(method) + if not op or "security" not in op: + continue + for sec_req in op["security"]: + if old_name in sec_req: + sec_req[new_name] = sec_req.pop(old_name) + # Update top-level security + for sec_req in spec.get("security", []): + if old_name in sec_req: + sec_req[new_name] = sec_req.pop(old_name) + + +# Mapping of (path, method) to desired operationId for the public docs. +# These are added at post-processing time to avoid breaking Go code generation +# (oapi-codegen derives type names from operationIds). +ENVD_OPERATION_IDS: dict[tuple[str, str], str] = { + ("/health", "get"): "getHealth", + ("/metrics", "get"): "getMetrics", + ("/init", "post"): "initSandbox", + ("/envs", "get"): "getEnvVars", + ("/files", "get"): "downloadFile", + ("/files", "post"): "uploadFile", +} + + +def add_operation_ids(spec: dict[str, Any]) -> None: + """Add operationIds to envd endpoints for clean documentation. + + These are added at post-processing time (not in the source spec) to + avoid changing generated Go type names. + """ + count = 0 + for (path, method), op_id in ENVD_OPERATION_IDS.items(): + path_item = spec.get("paths", {}).get(path) + if not path_item: + continue + op = path_item.get(method) + if op and "operationId" not in op: + op["operationId"] = op_id + count += 1 + if count: + print(f"==> Added {count} operationIds to envd endpoints") + + +def _strip_supabase_security(path_item: dict[str, Any]) -> None: + """Remove Supabase security entries from all operations in a path item. + + Each operation's security list is an OR of auth options. We remove + any option that references a Supabase scheme, keeping the rest. + """ + for method in ("get", "post", "put", "patch", "delete", "head", "options"): + op = path_item.get(method) + if not op or "security" not in op: + continue + op["security"] = [ + sec_req for sec_req in op["security"] + if not any("supabase" in key.lower() for key in sec_req) + ] + + +def _has_admin_token_security(path_item: dict[str, Any]) -> bool: + """Check if any operation in a path item references AdminToken auth.""" + for method in ("get", "post", "put", "patch", "delete", "head", "options"): + op = path_item.get(method) + if not op: + continue + for sec_req in op.get("security", []): + if any("admin" in key.lower() for key in sec_req): + return True + return False + + +def filter_paths(spec: dict[str, Any]) -> None: + """Clean up paths that should not appear in the public spec. + + - Removes access-token and api-key endpoints + - Removes endpoints using AdminToken auth + - Strips Supabase auth entries from all operations + - Removes Supabase and AdminToken securityScheme definitions + """ + # Remove excluded paths + excluded_prefixes = ("/access-tokens", "/api-keys") + excluded_exact = {"/v2/sandboxes/{sandboxID}/logs", "/init"} + to_remove = [ + p for p in spec["paths"] + if p.startswith(excluded_prefixes) or p in excluded_exact + ] + + # Remove admin-only paths + for path, path_item in spec["paths"].items(): + if path not in to_remove and _has_admin_token_security(path_item): + to_remove.append(path) + + for path in to_remove: + del spec["paths"][path] + if to_remove: + print(f"==> Removed {len(to_remove)} paths (volumes + admin)") + + # Strip supabase security entries from all operations + for path_item in spec["paths"].values(): + _strip_supabase_security(path_item) + + # Remove supabase and admin security scheme definitions + schemes = spec.get("components", {}).get("securitySchemes", {}) + remove_keys = [k for k in schemes if "supabase" in k.lower() or "admin" in k.lower()] + for key in remove_keys: + del schemes[key] + if remove_keys: + print(f"==> Removed {len(remove_keys)} internal security schemes") + + +def remove_orphaned_schemas(spec: dict[str, Any]) -> None: + """Remove component schemas that are not referenced anywhere in the spec. + Runs iteratively since removing schemas may orphan others.""" + all_orphaned: list[str] = [] + + while True: + spec_text = "" + # Serialize paths + top-level refs (excluding components.schemas itself) + for section in ("paths", "security"): + if section in spec: + spec_text += yaml.dump(spec[section], default_flow_style=False) + for section, entries in spec.get("components", {}).items(): + if section != "schemas": + spec_text += yaml.dump(entries, default_flow_style=False) + # Also check cross-references within schemas + schemas = spec.get("components", {}).get("schemas", {}) + schema_text = yaml.dump(schemas, default_flow_style=False) + + orphaned = [] + for name in list(schemas.keys()): + # Use exact ref pattern to avoid substring collisions + # (e.g. "schemas/Foo" matching inside "schemas/FooBar") + ref_pattern = f"schemas/{name}'" + # Referenced from paths/responses/params + if ref_pattern in spec_text: + continue + # Referenced from other schemas (exclude self-definition) + used = False + for other_name, other_schema in schemas.items(): + if other_name == name: + continue + if ref_pattern in yaml.dump(other_schema, default_flow_style=False): + used = True + break + if not used: + orphaned.append(name) + + if not orphaned: + break + + for name in orphaned: + del schemas[name] + all_orphaned.extend(orphaned) + + if all_orphaned: + print(f"==> Removed {len(all_orphaned)} orphaned schemas: {', '.join(sorted(all_orphaned))}") + + +SANDBOX_NOT_FOUND_RESPONSE = { + "description": "Sandbox not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["sandboxId", "message", "code"], + "properties": { + "sandboxId": { + "type": "string", + "description": "Identifier of the sandbox", + "example": "i1234abcd5678efgh90jk", + }, + "message": { + "type": "string", + "description": "Error message", + "example": "The sandbox was not found", + }, + "code": { + "type": "integer", + "description": "Error code", + "example": 502, + }, + }, + } + } + }, +} + + +EMPTY_RESPONSE_CONTENT = { + "application/json": { + "schema": {"type": "object", "description": "Empty response"} + } +} + + +def add_sandbox_not_found(spec: dict[str, Any], envd_paths: set[str]) -> None: + """Add a 502 response to all sandbox/envd endpoints. + + The load balancer returns 502 when a sandbox is not found. + """ + count = 0 + for path in envd_paths: + path_item = spec["paths"].get(path) + if not path_item: + continue + for method in ("get", "post", "put", "patch", "delete"): + op = path_item.get(method) + if op and "502" not in op.get("responses", {}): + op.setdefault("responses", {})["502"] = SANDBOX_NOT_FOUND_RESPONSE + count += 1 + if count: + print(f"==> Added 502 sandbox-not-found response to {count} operations") + + +def fill_empty_responses(spec: dict[str, Any]) -> None: + """Add an empty content block to any 2xx response that lacks one. + + Mintlify requires a content block on every response to render correctly. + """ + filled = 0 + stripped = 0 + for path, path_item in spec.get("paths", {}).items(): + for method in ("get", "post", "put", "patch", "delete", "head", "options"): + op = path_item.get(method) + if not op: + continue + responses = op.get("responses", {}) + # Remove "default" responses (generic Connect error envelopes) + if "default" in responses: + del responses["default"] + stripped += 1 + for status, resp in responses.items(): + if isinstance(resp, dict) and str(status).startswith("2") and "content" not in resp: + resp["content"] = EMPTY_RESPONSE_CONTENT + filled += 1 + if filled: + print(f"==> Added empty content block to {filled} responses") + if stripped: + print(f"==> Removed {stripped} default error responses") + + +# --------------------------------------------------------------------------- +# Entrypoint +# --------------------------------------------------------------------------- + +def main() -> None: + docker_build_image() + + # --- Sandbox API (envd) --- + proto_docs = docker_generate_specs() + envd_rest_doc = load_yaml_file(ENVD_REST_SPEC) + + # Track which paths come from envd so we can set their server + envd_raw_docs = [envd_rest_doc] + proto_docs + envd_paths: set[str] = set() + for raw in envd_raw_docs: + doc = yaml.safe_load(raw) + if doc and "paths" in doc: + envd_paths.update(doc["paths"].keys()) + + # --- Platform API --- + api_doc = load_yaml_file(API_SPEC) + + # --- Merge everything --- + # Order: envd first, then platform API (platform schemas take precedence + # for shared names like Error since they're more complete). + # Protect envd paths so the platform API doesn't overwrite them + # (e.g. /health exists in both but the envd version is authoritative). + merged = merge_specs(envd_raw_docs + [api_doc], protected_paths=envd_paths) + + # Auto-detect and fill streaming RPC endpoints + streaming_rpcs = find_streaming_rpcs(ENVD_SPEC_DIR) + print(f"==> Found {len(streaming_rpcs)} streaming RPCs in proto files") + fill_streaming_endpoints(merged, streaming_rpcs) + for rpc in streaming_rpcs: + envd_paths.add(rpc.path) + + # Attach per-path server overrides so each path has exactly one server + tag_paths_with_server(merged, envd_paths, SANDBOX_SERVER) + platform_paths = set(merged["paths"].keys()) - envd_paths + tag_paths_with_server(merged, platform_paths, PLATFORM_SERVER) + + # Ensure all sandbox endpoints declare auth + apply_sandbox_auth(merged, envd_paths) + + # Add 502 sandbox-not-found to all envd endpoints + add_sandbox_not_found(merged, envd_paths) + + # Fix known issues + fix_security_schemes(merged) + rename_envd_auth_scheme(merged) + add_operation_ids(merged) + + # Remove internal/unwanted paths + filter_paths(merged) + + # Ensure all 2xx responses have a content block (required by Mintlify) + fill_empty_responses(merged) + + # Clean up unreferenced schemas left over from filtered paths + remove_orphaned_schemas(merged) + + # Write output + output_path = os.path.join(os.getcwd(), "e2b-openapi.yml") + with open(output_path, "w") as f: + yaml.dump(merged, f, default_flow_style=False, sort_keys=False, allow_unicode=True) + + print(f"==> Written to {output_path}") + + +if __name__ == "__main__": + main() diff --git a/spec/openapi.yml b/spec/openapi.yml index 07b0bda0ef..168a1b269d 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -205,13 +205,13 @@ components: CPUCount: type: integer format: int32 - minimum: 1 + minimum: 0 description: CPU cores for the sandbox MemoryMB: type: integer format: int32 - minimum: 128 + minimum: 0 description: Memory for the sandbox in MiB DiskSizeMB: @@ -467,7 +467,6 @@ components: - endAt - state - envdVersion - - volumeMounts properties: templateID: type: string @@ -526,7 +525,6 @@ components: - endAt - state - envdVersion - - volumeMounts properties: templateID: type: string @@ -870,6 +868,13 @@ components: description: Number of times the template was built envdVersion: $ref: "#/components/schemas/EnvdVersion" + buildStatus: + $ref: "#/components/schemas/TemplateBuildStatus" + names: + type: array + nullable: true + items: + type: string TemplateBuild: required: @@ -1169,6 +1174,7 @@ components: - info - warn - error + - "" BuildLogEntry: required: @@ -1214,6 +1220,9 @@ components: - waiting - ready - error + - uploaded + - failed + - "" TemplateBuildInfo: required: @@ -1739,8 +1748,8 @@ paths: get: description: Health check responses: - "200": - description: Request was successful + "204": + description: The service is healthy "401": $ref: "#/components/responses/401" diff --git a/tests/integration/internal/envd/generated.go b/tests/integration/internal/envd/generated.go index c0461908a3..a40aec0440 100644 --- a/tests/integration/internal/envd/generated.go +++ b/tests/integration/internal/envd/generated.go @@ -71,17 +71,26 @@ type Metrics struct { // MemTotal Total virtual memory in bytes MemTotal *int `json:"mem_total,omitempty"` + // MemTotalMib Total virtual memory in MiB + MemTotalMib *int `json:"mem_total_mib,omitempty"` + // MemUsed Used virtual memory in bytes MemUsed *int `json:"mem_used,omitempty"` + // MemUsedMib Used virtual memory in MiB + MemUsedMib *int `json:"mem_used_mib,omitempty"` + // Ts Unix timestamp in UTC for current sandbox time Ts *int64 `json:"ts,omitempty"` } -// VolumeMount Volume +// VolumeMount NFS volume mount configuration type VolumeMount struct { + // NfsTarget NFS server target address NfsTarget string `json:"nfs_target"` - Path string `json:"path"` + + // Path Mount path inside the sandbox + Path string `json:"path"` } // FilePath defines model for FilePath. @@ -108,6 +117,9 @@ type InvalidPath = Error // InvalidUser defines model for InvalidUser. type InvalidUser = Error +// NotAcceptable defines model for NotAcceptable. +type NotAcceptable = Error + // NotEnoughDiskSpace defines model for NotEnoughDiskSpace. type NotEnoughDiskSpace = Error @@ -116,16 +128,16 @@ type UploadSuccess = []EntryInfo // GetFilesParams defines parameters for GetFiles. type GetFilesParams struct { - // Path Path to the file, URL encoded. Can be relative to user's home directory. + // Path Path to the file, URL encoded. Can be relative to the user's home directory (e.g. "file.txt" resolves to ~/file.txt). Path *FilePath `form:"path,omitempty" json:"path,omitempty"` - // Username User used for setting the owner, or resolving relative paths. + // Username User for setting file ownership and resolving relative paths. Defaults to the sandbox's default user. Username *User `form:"username,omitempty" json:"username,omitempty"` - // Signature Signature used for file access permission verification. + // Signature HMAC signature for access verification. Required when no X-Access-Token header is provided. Format is "v1_". Signature *Signature `form:"signature,omitempty" json:"signature,omitempty"` - // SignatureExpiration Signature expiration used for defining the expiration time of the signature. + // SignatureExpiration Unix timestamp (seconds) after which the signature expires. Only used with the signature parameter. SignatureExpiration *SignatureExpiration `form:"signature_expiration,omitempty" json:"signature_expiration,omitempty"` } @@ -136,16 +148,16 @@ type PostFilesMultipartBody struct { // PostFilesParams defines parameters for PostFiles. type PostFilesParams struct { - // Path Path to the file, URL encoded. Can be relative to user's home directory. + // Path Path to the file, URL encoded. Can be relative to the user's home directory (e.g. "file.txt" resolves to ~/file.txt). Path *FilePath `form:"path,omitempty" json:"path,omitempty"` - // Username User used for setting the owner, or resolving relative paths. + // Username User for setting file ownership and resolving relative paths. Defaults to the sandbox's default user. Username *User `form:"username,omitempty" json:"username,omitempty"` - // Signature Signature used for file access permission verification. + // Signature HMAC signature for access verification. Required when no X-Access-Token header is provided. Format is "v1_". Signature *Signature `form:"signature,omitempty" json:"signature,omitempty"` - // SignatureExpiration Signature expiration used for defining the expiration time of the signature. + // SignatureExpiration Unix timestamp (seconds) after which the signature expires. Only used with the signature parameter. SignatureExpiration *SignatureExpiration `form:"signature_expiration,omitempty" json:"signature_expiration,omitempty"` } @@ -764,6 +776,7 @@ type GetFilesResponse struct { JSON400 *InvalidPath JSON401 *InvalidUser JSON404 *FileNotFound + JSON406 *NotAcceptable JSON500 *InternalServerError } @@ -996,6 +1009,13 @@ func ParseGetFilesResponse(rsp *http.Response) (*GetFilesResponse, error) { } response.JSON404 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 406: + var dest NotAcceptable + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON406 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalServerError if err := json.Unmarshal(bodyBytes, &dest); err != nil {