refactor(craft): rename sandbox daemon contract module to models (#15687)

This commit is contained in:
Wenxi authored and GitHub committed 2026-10-07 23:38:59 +00:00
1 parent 41758c1836
commit 89c29f10ae
16 files changed
+25 -25

No files matched your search

@@ -19,7 +19,7 @@
`backend/onyx/server/features/build/sandbox/docker/` (`docker_sandbox_manager.py`, `backend/onyx/server/features/build/sandbox/docker/` (`docker_sandbox_manager.py`,
`dev_mode_serve.py`, `internal/exec_helpers.py`), `dev_mode_serve.py`, `internal/exec_helpers.py`),
`backend/onyx/server/features/build/sandbox/image/sandbox_daemon/` (`server.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`), `opencode_history.py`),
`backend/onyx/server/features/build/sandbox/util/` (`agent_instructions.py`, `backend/onyx/server/features/build/sandbox/util/` (`agent_instructions.py`,
`opencode_config.py`, `mcp_config.py`, `api_url_check.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 life, and can be restarted independently by the kubelet without restarting the
main container. It runs `sandbox_daemon/server.py` main container. It runs `sandbox_daemon/server.py`
(`image/sandbox_daemon/server.py`), a FastAPI app on port 8731 (`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`, `/push` (file bundle install), `/filesystem/list`, `/filesystem/outputs-manifest`,
`/snapshot/create`, `/snapshot/restore/{session_id}`, `/snapshot/create`, `/snapshot/restore/{session_id}`,
`/opencode-history/{create,restore,mark-restored}`, `/health`, `/ready`. Every `/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 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 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 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 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 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 | | 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 |
@@ -32,7 +32,7 @@ from onyx.server.features.build.sandbox.event_schema import (
ToolCallProgress, ToolCallProgress,
ToolCallStart, ToolCallStart,
) )
from onyx.server.features.build.sandbox.image.sandbox_daemon.contract import ( from onyx.server.features.build.sandbox.image.sandbox_daemon.models import (
OutputsManifestResponse, OutputsManifestResponse,
) )
from onyx.server.features.build.sandbox.models import ( from onyx.server.features.build.sandbox.models import (
@@ -113,7 +113,7 @@ from onyx.server.features.build.sandbox.docker.internal.exec_helpers import (
stream_stdin_to_container, stream_stdin_to_container,
stream_stdout_from_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, OutputsManifestResponse,
) )
from onyx.server.features.build.sandbox.labels import ( from onyx.server.features.build.sandbox.labels import (
@@ -6,7 +6,7 @@ import mimetypes
from pathlib import Path from pathlib import Path
from uuid import UUID 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 from sandbox_daemon.snapshot import SESSIONS_ROOT
_USER_LIBRARY_LINK_TARGET = Path("/workspace/managed/user_library") _USER_LIBRARY_LINK_TARGET = Path("/workspace/managed/user_library")
@@ -28,7 +28,7 @@ from pathlib import Path
from typing import NamedTuple from typing import NamedTuple
from uuid import UUID 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 from sandbox_daemon.snapshot import SESSIONS_ROOT
# Ceilings so a pathological outputs tree cannot stall the daemon or exhaust # Ceilings so a pathological outputs tree cannot stall the daemon or exhaust
@@ -1,7 +1,7 @@
"""HTTP contract shared between the sandbox daemon and the api-server. """HTTP contract shared between the sandbox daemon and the api-server.
Both sides import these constants and request models to keep the sidecar wire 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 Dockerfile copies ``sandbox_daemon/`` to ``/workspace/sandbox_daemon/``); the
api-server imports the full module path. api-server imports the full module path.
""" """
@@ -15,7 +15,10 @@ from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from fastapi import FastAPI, Header, HTTPException, Query, Request, Response from fastapi import FastAPI, Header, HTTPException, Query, Request, Response
from fastapi.responses import StreamingResponse 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, PUSH_DAEMON_PORT,
SIDECAR_FILESYSTEM_LIST_PATH, SIDECAR_FILESYSTEM_LIST_PATH,
SIDECAR_HEALTH_PATH, SIDECAR_HEALTH_PATH,
@@ -33,9 +36,6 @@ from sandbox_daemon.contract import (
SnapshotCreateRequest, SnapshotCreateRequest,
sidecar_snapshot_restore_path, 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 ( from sandbox_daemon.opencode_history import (
create_opencode_history_archive_file, create_opencode_history_archive_file,
mark_opencode_history_restored, mark_opencode_history_restored,
@@ -78,7 +78,7 @@ from onyx.server.features.build.configs import (
from onyx.server.features.build.sandbox.base import ( from onyx.server.features.build.sandbox.base import (
SandboxManager, 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, PUSH_DAEMON_PORT,
SIDECAR_OPENCODE_HISTORY_CREATE_PATH, SIDECAR_OPENCODE_HISTORY_CREATE_PATH,
SIDECAR_OPENCODE_HISTORY_MARK_RESTORED_PATH, SIDECAR_OPENCODE_HISTORY_MARK_RESTORED_PATH,
@@ -16,7 +16,7 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from onyx.server.features.build.configs import SANDBOX_PUSH_PRIVATE_KEY 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, PUSH_DAEMON_PORT,
SIDECAR_FILESYSTEM_LIST_PATH, SIDECAR_FILESYSTEM_LIST_PATH,
SIDECAR_HEALTH_PATH, SIDECAR_HEALTH_PATH,
+1 -1
View File
@@ -57,7 +57,7 @@ from typing import Any, cast
from uuid import UUID from uuid import UUID
from onyx.server.features.build.sandbox.base import SandboxEvent, SandboxManager 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, OutputsManifestResponse,
) )
from onyx.server.features.build.sandbox.models import ( from onyx.server.features.build.sandbox.models import (
@@ -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.kubernetes_sandbox_manager as ksm
import onyx.server.features.build.sandbox.kubernetes.sidecar_client as sidecar 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.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, PUSH_DAEMON_PORT,
SIDECAR_HEALTH_PATH, SIDECAR_HEALTH_PATH,
SIDECAR_READY_PATH, SIDECAR_READY_PATH,
@@ -30,7 +30,7 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from fastapi.testclient import TestClient 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_FILESYSTEM_LIST_PATH,
SIDECAR_HEALTH_PATH, SIDECAR_HEALTH_PATH,
SIDECAR_OPENCODE_HISTORY_CREATE_PATH, SIDECAR_OPENCODE_HISTORY_CREATE_PATH,
@@ -60,7 +60,7 @@ def _load_sandbox_daemon_modules() -> tuple[ModuleType, ModuleType]:
""" """
if ( if (
"sandbox_daemon.server" in sys.modules "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.extract" in sys.modules
and "sandbox_daemon.filesystem" 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") sys.modules["sandbox_daemon"] = types.ModuleType("sandbox_daemon")
for name in ( for name in (
"contract", "models",
"extract", "extract",
"snapshot", "snapshot",
"opencode_history", "opencode_history",
@@ -32,7 +32,7 @@ from cryptography.hazmat.primitives.serialization import (
from kubernetes import client from kubernetes import client
from kubernetes.client.rest import ApiException 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_MARK_RESTORED_PATH,
SIDECAR_OPENCODE_HISTORY_RESTORE_PATH, SIDECAR_OPENCODE_HISTORY_RESTORE_PATH,
SIDECAR_PUSH_PATH, SIDECAR_PUSH_PATH,
@@ -37,7 +37,7 @@ _DAEMON_DIR = (
# runs first the other's cache guard sees every module and reuses it, and no # runs first the other's cache guard sees every module and reuses it, and no
# model class gets two identities in one session. # model class gets two identities in one session.
_DAEMON_MODULES = ( _DAEMON_MODULES = (
"contract", "models",
"extract", "extract",
"snapshot", "snapshot",
"opencode_history", "opencode_history",
@@ -21,7 +21,7 @@ kubelet evicts sandbox pods first under node disk pressure. */}}
{{- $nextjsStart := (index $cm "SANDBOX_NEXTJS_PORT_START") | default "3010" | int }} {{- $nextjsStart := (index $cm "SANDBOX_NEXTJS_PORT_START") | default "3010" | int }}
{{- $nextjsEnd := (index $cm "SANDBOX_NEXTJS_PORT_END") | default "3100" | int }} {{- $nextjsEnd := (index $cm "SANDBOX_NEXTJS_PORT_END") | default "3100" | int }}
{{- $opencodePort := (index $cm "OPENCODE_SERVE_PORT") | default "4096" | 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" . }} {{- $pushDaemonPort := include "onyx.sandboxPushDaemonPort" . }}
{{- $proxyPort := include "onyx.sandboxProxyPort" . }} {{- $proxyPort := include "onyx.sandboxProxyPort" . }}
{{- $proxyHost := include "onyx.sandboxProxyHost" . }} {{- $proxyHost := include "onyx.sandboxProxyHost" . }}
+3 -3
View File
@@ -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 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. later sandbox-container control endpoint could remove them entirely if desired.
- **Wire-schema location:** request/response models go in - **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 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. the existing shared-contract pattern — both ends stay in sync.
- **Reuse the existing client plumbing** on the api-server side: - **Reuse the existing client plumbing** on the api-server side:
`_signed_sidecar_headers` + `_sandbox_pod_hosts` (Service FQDN, then pod-IP `_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 ## 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, (session setup, cleanup, list-sessions, exists, list-directory, read-file,
delete-file, write-file, upload-stats, ensure-attachments, regenerate-config, delete-file, write-file, upload-stats, ensure-attachments, regenerate-config,
pptx-preview). Structured request bodies + JSON responses replace every pptx-preview). Structured request bodies + JSON responses replace every