diff --git a/.agents/feature-map/components/craft-sandboxes.md b/.agents/feature-map/components/craft-sandboxes.md index b2c50c316c..e408071697 100644 --- a/.agents/feature-map/components/craft-sandboxes.md +++ b/.agents/feature-map/components/craft-sandboxes.md @@ -19,7 +19,7 @@ `backend/onyx/server/features/build/sandbox/docker/` (`docker_sandbox_manager.py`, `dev_mode_serve.py`, `internal/exec_helpers.py`), `backend/onyx/server/features/build/sandbox/image/sandbox_daemon/` (`server.py`, -`snapshot.py`, `extract.py`, `filesystem.py`, `manifest.py`, `contract.py`, +`snapshot.py`, `extract.py`, `filesystem.py`, `manifest.py`, `models.py`, `opencode_history.py`), `backend/onyx/server/features/build/sandbox/util/` (`agent_instructions.py`, `opencode_config.py`, `mcp_config.py`, `api_url_check.py`), @@ -289,7 +289,7 @@ entry): it starts before the main container, keeps running for the pod's whole life, and can be restarted independently by the kubelet without restarting the main container. It runs `sandbox_daemon/server.py` (`image/sandbox_daemon/server.py`), a FastAPI app on port 8731 -(`PUSH_DAEMON_PORT`, `contract.py:16`) that owns nearly all filesystem access: +(`PUSH_DAEMON_PORT`, `models.py`) that owns nearly all filesystem access: `/push` (file bundle install), `/filesystem/list`, `/filesystem/outputs-manifest`, `/snapshot/create`, `/snapshot/restore/{session_id}`, `/opencode-history/{create,restore,mark-restored}`, `/health`, `/ready`. Every @@ -558,7 +558,7 @@ re-provisions from `SLEEPING` (§4.2) and restores each session on demand. | changes the sandbox image (`image/Dockerfile`, `initial-requirements.txt`) | the app-image/sandbox-image tag coupling (`docs/craft/infra/image-architecture.md`); the prepuller DaemonSet's pinned tag (`sandbox-image-prepuller.yaml`); `ENABLE_SKILLS` build-arg gating; re-run the spinup benchmark (`kubernetes/scripts/bench-sandbox-spinup.sh`) | | changes the pod template (`sandbox-podtemplate.yaml`) | `_overlay_dynamic_fields`/`_require_container` (version-skew handling); the dynamic fields Python still owns; resource requests vs the CI/localdev values overlays; the Service's port range staying in sync with the template's container ports | | changes the snapshot format (what's included/excluded, archive layout) | `sandbox_daemon/snapshot.py`'s `_SNAPSHOT_ROOTS`/`_SNAPSHOT_GENERATED_*` sets; `restore_snapshot`'s webapp-restore path; existing snapshots in FileStore become unreadable by a format change unless restore stays backward-compatible | -| changes the sidecar contract (`contract.py`, `server.py` routes) | both `sidecar_client.py` (api-server side) and every route in `sandbox_daemon/server.py`; the image must ship the updated daemon in the same release as the api-server that calls it (version skew is a real risk, no negotiation exists) | +| changes the sidecar contract (`models.py`, `server.py` routes) | both `sidecar_client.py` (api-server side) and every route in `sandbox_daemon/server.py`; the image must ship the updated daemon in the same release as the api-server that calls it (version skew is a real risk, no negotiation exists) | | changes RBAC (`sandbox-rbac.yaml`) | `_wait_for_pod_ready`'s `watch.Watch()` usage needs `pods:watch`; removing it makes every provision fail with an `ApiException`, and no unit test catches it (see §9); `pods/exec` is still needed for the many residual exec call sites; `_create_sandbox_pod`'s `podtemplates:get` | | changes network policy (`network-policy-sandbox-*.yaml`) | the in-pod iptables lockdown (`firewall-init.sh`) stays authoritative regardless of CNI enforcement; keep the allow-list in sync with whichever worker consumes the `sandbox` Celery queue (currently `celery-worker-heavy`) | | changes idle timeout or snapshot cadence | the `SNAPSHOT_INTERVAL_DIVISOR` relationship between idle timeout and background-snapshot freshness bound; opencode-history durability exposure scales with this gap | diff --git a/backend/onyx/server/features/build/sandbox/base.py b/backend/onyx/server/features/build/sandbox/base.py index 29067f7446..918540156e 100644 --- a/backend/onyx/server/features/build/sandbox/base.py +++ b/backend/onyx/server/features/build/sandbox/base.py @@ -32,7 +32,7 @@ from onyx.server.features.build.sandbox.event_schema import ( ToolCallProgress, ToolCallStart, ) -from onyx.server.features.build.sandbox.image.sandbox_daemon.contract import ( +from onyx.server.features.build.sandbox.image.sandbox_daemon.models import ( OutputsManifestResponse, ) from onyx.server.features.build.sandbox.models import ( diff --git a/backend/onyx/server/features/build/sandbox/docker/docker_sandbox_manager.py b/backend/onyx/server/features/build/sandbox/docker/docker_sandbox_manager.py index 7a9e09bd3f..b8793a2dc3 100644 --- a/backend/onyx/server/features/build/sandbox/docker/docker_sandbox_manager.py +++ b/backend/onyx/server/features/build/sandbox/docker/docker_sandbox_manager.py @@ -113,7 +113,7 @@ from onyx.server.features.build.sandbox.docker.internal.exec_helpers import ( stream_stdin_to_container, stream_stdout_from_container, ) -from onyx.server.features.build.sandbox.image.sandbox_daemon.contract import ( +from onyx.server.features.build.sandbox.image.sandbox_daemon.models import ( OutputsManifestResponse, ) from onyx.server.features.build.sandbox.labels import ( diff --git a/backend/onyx/server/features/build/sandbox/image/sandbox_daemon/filesystem.py b/backend/onyx/server/features/build/sandbox/image/sandbox_daemon/filesystem.py index 8b93760470..9e4e86200f 100644 --- a/backend/onyx/server/features/build/sandbox/image/sandbox_daemon/filesystem.py +++ b/backend/onyx/server/features/build/sandbox/image/sandbox_daemon/filesystem.py @@ -6,7 +6,7 @@ import mimetypes from pathlib import Path from uuid import UUID -from sandbox_daemon.contract import FilesystemListResponse, SidecarFilesystemEntry +from sandbox_daemon.models import FilesystemListResponse, SidecarFilesystemEntry from sandbox_daemon.snapshot import SESSIONS_ROOT _USER_LIBRARY_LINK_TARGET = Path("/workspace/managed/user_library") diff --git a/backend/onyx/server/features/build/sandbox/image/sandbox_daemon/manifest.py b/backend/onyx/server/features/build/sandbox/image/sandbox_daemon/manifest.py index 33e85fb4b2..d061c9ae19 100644 --- a/backend/onyx/server/features/build/sandbox/image/sandbox_daemon/manifest.py +++ b/backend/onyx/server/features/build/sandbox/image/sandbox_daemon/manifest.py @@ -28,7 +28,7 @@ from pathlib import Path from typing import NamedTuple from uuid import UUID -from sandbox_daemon.contract import OutputsManifestEntry, OutputsManifestResponse +from sandbox_daemon.models import OutputsManifestEntry, OutputsManifestResponse from sandbox_daemon.snapshot import SESSIONS_ROOT # Ceilings so a pathological outputs tree cannot stall the daemon or exhaust diff --git a/backend/onyx/server/features/build/sandbox/image/sandbox_daemon/contract.py b/backend/onyx/server/features/build/sandbox/image/sandbox_daemon/models.py similarity index 96% rename from backend/onyx/server/features/build/sandbox/image/sandbox_daemon/contract.py rename to backend/onyx/server/features/build/sandbox/image/sandbox_daemon/models.py index 392a14ae7a..cc5eca0236 100644 --- a/backend/onyx/server/features/build/sandbox/image/sandbox_daemon/contract.py +++ b/backend/onyx/server/features/build/sandbox/image/sandbox_daemon/models.py @@ -1,7 +1,7 @@ """HTTP contract shared between the sandbox daemon and the api-server. Both sides import these constants and request models to keep the sidecar wire -contract in sync. The daemon imports this as ``sandbox_daemon.contract`` (the +contract in sync. The daemon imports this as ``sandbox_daemon.models`` (the Dockerfile copies ``sandbox_daemon/`` to ``/workspace/sandbox_daemon/``); the api-server imports the full module path. """ diff --git a/backend/onyx/server/features/build/sandbox/image/sandbox_daemon/server.py b/backend/onyx/server/features/build/sandbox/image/sandbox_daemon/server.py index c0b6cc05bc..7ada6cd71a 100644 --- a/backend/onyx/server/features/build/sandbox/image/sandbox_daemon/server.py +++ b/backend/onyx/server/features/build/sandbox/image/sandbox_daemon/server.py @@ -15,7 +15,10 @@ from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from fastapi import FastAPI, Header, HTTPException, Query, Request, Response from fastapi.responses import StreamingResponse -from sandbox_daemon.contract import ( +from sandbox_daemon.extract import MAX_BUNDLE_BYTES, safe_extract_then_atomic_swap +from sandbox_daemon.filesystem import FilesystemPathError, list_session_directory +from sandbox_daemon.manifest import build_outputs_manifest +from sandbox_daemon.models import ( PUSH_DAEMON_PORT, SIDECAR_FILESYSTEM_LIST_PATH, SIDECAR_HEALTH_PATH, @@ -33,9 +36,6 @@ from sandbox_daemon.contract import ( SnapshotCreateRequest, sidecar_snapshot_restore_path, ) -from sandbox_daemon.extract import MAX_BUNDLE_BYTES, safe_extract_then_atomic_swap -from sandbox_daemon.filesystem import FilesystemPathError, list_session_directory -from sandbox_daemon.manifest import build_outputs_manifest from sandbox_daemon.opencode_history import ( create_opencode_history_archive_file, mark_opencode_history_restored, diff --git a/backend/onyx/server/features/build/sandbox/kubernetes/kubernetes_sandbox_manager.py b/backend/onyx/server/features/build/sandbox/kubernetes/kubernetes_sandbox_manager.py index 01cfd04eef..049d7e1084 100644 --- a/backend/onyx/server/features/build/sandbox/kubernetes/kubernetes_sandbox_manager.py +++ b/backend/onyx/server/features/build/sandbox/kubernetes/kubernetes_sandbox_manager.py @@ -78,7 +78,7 @@ from onyx.server.features.build.configs import ( from onyx.server.features.build.sandbox.base import ( SandboxManager, ) -from onyx.server.features.build.sandbox.image.sandbox_daemon.contract import ( +from onyx.server.features.build.sandbox.image.sandbox_daemon.models import ( PUSH_DAEMON_PORT, SIDECAR_OPENCODE_HISTORY_CREATE_PATH, SIDECAR_OPENCODE_HISTORY_MARK_RESTORED_PATH, diff --git a/backend/onyx/server/features/build/sandbox/kubernetes/sidecar_client.py b/backend/onyx/server/features/build/sandbox/kubernetes/sidecar_client.py index b2092aec2d..4a8c9cd8b1 100644 --- a/backend/onyx/server/features/build/sandbox/kubernetes/sidecar_client.py +++ b/backend/onyx/server/features/build/sandbox/kubernetes/sidecar_client.py @@ -16,7 +16,7 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from onyx.server.features.build.configs import SANDBOX_PUSH_PRIVATE_KEY -from onyx.server.features.build.sandbox.image.sandbox_daemon.contract import ( +from onyx.server.features.build.sandbox.image.sandbox_daemon.models import ( PUSH_DAEMON_PORT, SIDECAR_FILESYSTEM_LIST_PATH, SIDECAR_HEALTH_PATH, diff --git a/backend/tests/common/craft/stubs.py b/backend/tests/common/craft/stubs.py index 66b7e892d1..dce1af874b 100644 --- a/backend/tests/common/craft/stubs.py +++ b/backend/tests/common/craft/stubs.py @@ -57,7 +57,7 @@ from typing import Any, cast from uuid import UUID from onyx.server.features.build.sandbox.base import SandboxEvent, SandboxManager -from onyx.server.features.build.sandbox.image.sandbox_daemon.contract import ( +from onyx.server.features.build.sandbox.image.sandbox_daemon.models import ( OutputsManifestResponse, ) from onyx.server.features.build.sandbox.models import ( diff --git a/backend/tests/external_dependency_unit/craft_helm/test_pod_spec.py b/backend/tests/external_dependency_unit/craft_helm/test_pod_spec.py index b8984caea5..ccdcd0a272 100644 --- a/backend/tests/external_dependency_unit/craft_helm/test_pod_spec.py +++ b/backend/tests/external_dependency_unit/craft_helm/test_pod_spec.py @@ -43,7 +43,7 @@ from kubernetes import client import onyx.server.features.build.sandbox.kubernetes.kubernetes_sandbox_manager as ksm import onyx.server.features.build.sandbox.kubernetes.sidecar_client as sidecar from onyx.server.features.build.configs import SANDBOX_PROXY_INJECTED_PLACEHOLDER -from onyx.server.features.build.sandbox.image.sandbox_daemon.contract import ( +from onyx.server.features.build.sandbox.image.sandbox_daemon.models import ( PUSH_DAEMON_PORT, SIDECAR_HEALTH_PATH, SIDECAR_READY_PATH, diff --git a/backend/tests/unit/onyx/server/features/craft/sandbox/sandbox_daemon/test_sandbox_daemon.py b/backend/tests/unit/onyx/server/features/craft/sandbox/sandbox_daemon/test_sandbox_daemon.py index e5fe3a0396..7c17b171b2 100644 --- a/backend/tests/unit/onyx/server/features/craft/sandbox/sandbox_daemon/test_sandbox_daemon.py +++ b/backend/tests/unit/onyx/server/features/craft/sandbox/sandbox_daemon/test_sandbox_daemon.py @@ -30,7 +30,7 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from fastapi.testclient import TestClient -from onyx.server.features.build.sandbox.image.sandbox_daemon.contract import ( +from onyx.server.features.build.sandbox.image.sandbox_daemon.models import ( SIDECAR_FILESYSTEM_LIST_PATH, SIDECAR_HEALTH_PATH, SIDECAR_OPENCODE_HISTORY_CREATE_PATH, @@ -60,7 +60,7 @@ def _load_sandbox_daemon_modules() -> tuple[ModuleType, ModuleType]: """ if ( "sandbox_daemon.server" in sys.modules - and "sandbox_daemon.contract" in sys.modules + and "sandbox_daemon.models" in sys.modules and "sandbox_daemon.extract" in sys.modules and "sandbox_daemon.filesystem" in sys.modules ): @@ -72,7 +72,7 @@ def _load_sandbox_daemon_modules() -> tuple[ModuleType, ModuleType]: sys.modules["sandbox_daemon"] = types.ModuleType("sandbox_daemon") for name in ( - "contract", + "models", "extract", "snapshot", "opencode_history", diff --git a/backend/tests/unit/onyx/server/features/craft/sandbox/test_k8s_push_error_mapping.py b/backend/tests/unit/onyx/server/features/craft/sandbox/test_k8s_push_error_mapping.py index 9278897537..60fb4de26e 100644 --- a/backend/tests/unit/onyx/server/features/craft/sandbox/test_k8s_push_error_mapping.py +++ b/backend/tests/unit/onyx/server/features/craft/sandbox/test_k8s_push_error_mapping.py @@ -32,7 +32,7 @@ from cryptography.hazmat.primitives.serialization import ( from kubernetes import client from kubernetes.client.rest import ApiException -from onyx.server.features.build.sandbox.image.sandbox_daemon.contract import ( +from onyx.server.features.build.sandbox.image.sandbox_daemon.models import ( SIDECAR_OPENCODE_HISTORY_MARK_RESTORED_PATH, SIDECAR_OPENCODE_HISTORY_RESTORE_PATH, SIDECAR_PUSH_PATH, diff --git a/backend/tests/unit/onyx/server/features/craft/sandbox/test_outputs_manifest.py b/backend/tests/unit/onyx/server/features/craft/sandbox/test_outputs_manifest.py index 8b707d5f07..2cf5808bbd 100644 --- a/backend/tests/unit/onyx/server/features/craft/sandbox/test_outputs_manifest.py +++ b/backend/tests/unit/onyx/server/features/craft/sandbox/test_outputs_manifest.py @@ -37,7 +37,7 @@ _DAEMON_DIR = ( # runs first the other's cache guard sees every module and reuses it, and no # model class gets two identities in one session. _DAEMON_MODULES = ( - "contract", + "models", "extract", "snapshot", "opencode_history", diff --git a/deployment/helm/charts/onyx/templates/sandbox-podtemplate.yaml b/deployment/helm/charts/onyx/templates/sandbox-podtemplate.yaml index 181f5784f8..56897cac89 100644 --- a/deployment/helm/charts/onyx/templates/sandbox-podtemplate.yaml +++ b/deployment/helm/charts/onyx/templates/sandbox-podtemplate.yaml @@ -21,7 +21,7 @@ kubelet evicts sandbox pods first under node disk pressure. */}} {{- $nextjsStart := (index $cm "SANDBOX_NEXTJS_PORT_START") | default "3010" | int }} {{- $nextjsEnd := (index $cm "SANDBOX_NEXTJS_PORT_END") | default "3100" | int }} {{- $opencodePort := (index $cm "OPENCODE_SERVE_PORT") | default "4096" | int }} -{{/* Keep in sync with PUSH_DAEMON_PORT in sandbox_daemon/contract.py. */}} +{{/* Keep in sync with PUSH_DAEMON_PORT in sandbox_daemon/models.py. */}} {{- $pushDaemonPort := include "onyx.sandboxPushDaemonPort" . }} {{- $proxyPort := include "onyx.sandboxProxyPort" . }} {{- $proxyHost := include "onyx.sandboxProxyHost" . }} diff --git a/docs/craft/sandbox/sandbox-exec-sidecar.md b/docs/craft/sandbox/sandbox-exec-sidecar.md index 1d37cd5573..14e9c1b7c7 100644 --- a/docs/craft/sandbox/sandbox-exec-sidecar.md +++ b/docs/craft/sandbox/sandbox-exec-sidecar.md @@ -65,9 +65,9 @@ retained only for in-sandbox-container process control. long as any `k8s_stream` exists). Both shrink to serving only 3 call sites. A later sandbox-container control endpoint could remove them entirely if desired. - **Wire-schema location:** request/response models go in - `sandbox_daemon/contract.py` (the daemon imports `sandbox_daemon.contract`; the + `sandbox_daemon/models.py` (the daemon imports `sandbox_daemon.models`; the api-server imports the full - `onyx.server.features.build.sandbox.image.sandbox_daemon.contract` path). This is + `onyx.server.features.build.sandbox.image.sandbox_daemon.models` path). This is the existing shared-contract pattern — both ends stay in sync. - **Reuse the existing client plumbing** on the api-server side: `_signed_sidecar_headers` + `_sandbox_pod_hosts` (Service FQDN, then pod-IP @@ -80,7 +80,7 @@ retained only for in-sandbox-container process control. ## Implementation Strategy -1. **Add typed models** to `sandbox_daemon/contract.py` for each new operation +1. **Add typed models** to `sandbox_daemon/models.py` for each new operation (session setup, cleanup, list-sessions, exists, list-directory, read-file, delete-file, write-file, upload-stats, ensure-attachments, regenerate-config, pptx-preview). Structured request bodies + JSON responses replace every