mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-10-19 01:50:51 -05:00
refactor(craft): rename sandbox daemon contract module to models (#15687)
This commit is contained in:
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
-1
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
+3
-3
@@ -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",
|
||||||
|
|||||||
+1
-1
@@ -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" . }}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user