mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-10-19 01:50:51 -05:00
refactor: consolidate API routing on ONYX_SERVER_URL (#13375)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
aaaa1efd21
commit
2164093ae3
37 files changed
+210
-144
No files matched your search
@@ -51,7 +51,7 @@ env:
|
||||
SANDBOX_CONTAINER_IMAGE: "onyxdotapp/sandbox:ci"
|
||||
# Read by api_server on boot; Sandbox containers call back via this URL on the
|
||||
# compose bridge.
|
||||
SANDBOX_API_SERVER_URL: "http://api_server:8080"
|
||||
ONYX_SERVER_URL: "http://api_server:8080"
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
|
||||
@@ -45,7 +45,7 @@ env:
|
||||
SANDBOX_SERVICE_ACCOUNT_NAME: "sandbox"
|
||||
SANDBOX_CONTAINER_IMAGE: "localhost:5001/onyx-sandbox:ci"
|
||||
# Sandbox pod resource requests live in values-ci.yaml (configMap.SANDBOX_POD_*).
|
||||
SANDBOX_API_SERVER_URL: "http://onyx-api-service.onyx.svc.cluster.local:8080"
|
||||
ONYX_SERVER_URL: "http://onyx-api-service.onyx.svc.cluster.local:8080"
|
||||
|
||||
# Proxy lives in release namespace `onyx`. SANDBOX_PROXY_HOST is set
|
||||
# later to the proxy Service ClusterIP — the runner has no cluster DNS,
|
||||
|
||||
Vendored
+1
-1
@@ -140,7 +140,7 @@ SANDBOX_CONTAINER_IMAGE=onyxdotapp/sandbox:dev
|
||||
|
||||
# How sandboxes reach the api_server: out through the egress proxy to the host
|
||||
# gateway, not internal cluster DNS. Linux-kind devs sub their own host gateway.
|
||||
SANDBOX_API_SERVER_URL=http://host.docker.internal:3000
|
||||
ONYX_SERVER_URL=http://host.docker.internal:3000/api
|
||||
|
||||
SANDBOX_NAMESPACE=onyx-sandboxes
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ from onyx.sandbox_proxy.logging_utils import (
|
||||
from onyx.sandbox_proxy.request_evaluator import RequestEvaluator
|
||||
from onyx.server.features.build.configs import (
|
||||
MCP_SESSION_TAG_HEADER,
|
||||
SANDBOX_API_SERVER_URL,
|
||||
ONYX_SERVER_URL,
|
||||
SANDBOX_APPROVAL_WAIT_TIMEOUT_SECONDS,
|
||||
)
|
||||
from onyx.server.features.build.db import action_approval
|
||||
@@ -91,9 +91,9 @@ PARSER_MAX_BODY_BYTES = 32 * 1024 * 1024
|
||||
# in-cluster name resolving to an internal IP, while still denying every other port
|
||||
# on that host (e.g. a co-located Redis/Postgres reachable at the same hostname).
|
||||
def _parse_api_server() -> tuple[str | None, int | None]:
|
||||
if not SANDBOX_API_SERVER_URL:
|
||||
if not ONYX_SERVER_URL:
|
||||
return None, None
|
||||
parsed = urlparse(SANDBOX_API_SERVER_URL)
|
||||
parsed = urlparse(ONYX_SERVER_URL)
|
||||
host = (parsed.hostname or "").lower() or None
|
||||
port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
||||
return host, port
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Onyx-API PAT credential resolver.
|
||||
|
||||
Claims requests bound for the Onyx API host (the host of
|
||||
``SANDBOX_API_SERVER_URL``) and sets both auth headers to the sandbox's real
|
||||
per-sandbox PAT, read encrypted off ``Sandbox.encrypted_pat``. The tenant is
|
||||
embedded in the PAT itself, so no separate tenant header is injected.
|
||||
Claims requests bound for the Onyx API host (the host of ``ONYX_SERVER_URL``)
|
||||
and sets both auth headers to the sandbox's real per-sandbox PAT, read
|
||||
encrypted off ``Sandbox.encrypted_pat``. The tenant is embedded in the PAT
|
||||
itself, so no separate tenant header is injected.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -24,7 +24,7 @@ from onyx.sandbox_proxy.credential_injection import (
|
||||
InjectionContext,
|
||||
)
|
||||
from onyx.sandbox_proxy.logging_utils import full_log_id, short_log_id
|
||||
from onyx.server.features.build.configs import SANDBOX_API_SERVER_URL
|
||||
from onyx.server.features.build.configs import ONYX_SERVER_URL
|
||||
from onyx.server.features.build.db.sandbox import get_sandbox_by_id
|
||||
from onyx.utils.logger import setup_logger
|
||||
|
||||
@@ -35,7 +35,7 @@ class OnyxPatResolver(CredentialResolver):
|
||||
"""Injects the sandbox's Onyx API PAT on requests to the configured API host."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
parsed = urlparse(SANDBOX_API_SERVER_URL) if SANDBOX_API_SERVER_URL else None
|
||||
parsed = urlparse(ONYX_SERVER_URL) if ONYX_SERVER_URL else None
|
||||
host = parsed.hostname if parsed else None
|
||||
self._api_host = host.lower() if host else None
|
||||
if parsed is not None and host:
|
||||
|
||||
@@ -97,9 +97,9 @@ ENABLE_OPENCODE_DEBUGGING = (
|
||||
os.environ.get("ENABLE_OPENCODE_DEBUGGING", "false").lower() == "true"
|
||||
)
|
||||
|
||||
# Must be set when SANDBOX_BACKEND=kubernetes (no default — varies per
|
||||
# deployment).
|
||||
SANDBOX_API_SERVER_URL = os.environ.get("SANDBOX_API_SERVER_URL", "")
|
||||
# Complete Onyx API base URL reachable from the sandbox, including any path
|
||||
# prefix. Must be set when SANDBOX_BACKEND=kubernetes.
|
||||
ONYX_SERVER_URL = os.environ.get("ONYX_SERVER_URL", "")
|
||||
|
||||
# ==============================================================================
|
||||
# Sandbox egress proxy
|
||||
|
||||
@@ -54,9 +54,13 @@ The Docker backend is intentionally the closest single-VM analogue of the Kubern
|
||||
|
||||
#### Docker mode trust boundary
|
||||
|
||||
`api_server` and `background` mount the host Docker socket so they can drive sandbox containers. Anything that can talk to that socket is effectively root on the host — only enable Craft on hosts you fully control. Sandbox containers themselves run unprivileged: `--security-opt no-new-privileges`, `--cap-drop ALL`, `user=1000:1000`, no Docker socket, and a fixed env allowlist (`ONYX_PAT` + `ONYX_SERVER_URL`).
|
||||
`api_server` and `background` mount the host Docker socket so they can drive sandbox containers. Anything that can talk to that socket is effectively root on the host — only enable Craft on hosts you fully control. Sandbox containers themselves run unprivileged: `--security-opt no-new-privileges`, `--cap-drop ALL`, `user=1000:1000`, no Docker socket, and a fixed env allowlist (`ONYX_PAT` + `ONYX_SERVER_URL` + `ONYX_API_PREFIX`).
|
||||
|
||||
`SANDBOX_API_SERVER_URL` must be the **public** HTTPS URL that the agent reaches Onyx through (same way any onyx-cli client would). Compose hostnames like `http://api_server:8080` do not resolve from inside the sandbox bridge.
|
||||
`ONYX_SERVER_URL` is the complete API base URL used by both the host-side
|
||||
manager and the sandbox client. The Craft Compose overlay defaults it to
|
||||
`http://onyx-craft-api:8080`, a private alias on the sandbox bridge. Public
|
||||
reverse-proxy overrides must include their API path prefix, for example
|
||||
`https://onyx.your-org.example/api`.
|
||||
|
||||
On EC2 the Docker bridge by default routes to `169.254.169.254` (IMDS), which can hand out IAM credentials. `install.sh --include-craft` installs a host-level `DOCKER-USER` iptables rule to drop sandbox→IMDS traffic when it has sudo/iptables access, and prints the manual command otherwise. There is no application-level fallback — fix this at the host firewall.
|
||||
|
||||
@@ -169,10 +173,10 @@ SANDBOX_SERVICE_ACCOUNT_NAME=sandbox # No storage credentials required
|
||||
|
||||
```bash
|
||||
|
||||
# Public URL the sandbox agent uses to reach Onyx (HTTPS, externally resolvable —
|
||||
# compose hostnames like http://api_server:8080 will not resolve from inside the
|
||||
# sandbox bridge).
|
||||
SANDBOX_API_SERVER_URL=https://onyx.your-org.example
|
||||
# Complete API base URL. The Craft Compose overlay defaults to its private
|
||||
# http://onyx-craft-api:8080 alias; override only to route through a public
|
||||
# reverse proxy.
|
||||
ONYX_SERVER_URL=https://onyx.your-org.example/api
|
||||
|
||||
# Host path of the Docker socket mounted into api_server/background
|
||||
SANDBOX_DOCKER_SOCKET=/var/run/docker.sock # Default: /var/run/docker.sock
|
||||
@@ -222,7 +226,8 @@ uv run pytest backend/tests/integration/tests/craft/k8s/test_kubernetes_sandbox.
|
||||
- Confirm `api_server` actually has the Docker socket: `docker compose exec api_server ls -l /var/run/docker.sock`
|
||||
- Confirm the dedicated bridge exists: `docker network inspect onyx_craft_sandbox` (created by `install.sh --include-craft`, or run `docker network create onyx_craft_sandbox` manually)
|
||||
- Check sandbox logs: `docker logs sandbox-<id8>`
|
||||
- Confirm `SANDBOX_API_SERVER_URL` is a publicly resolvable HTTPS URL (the agent cannot reach `http://api_server:8080` from inside the sandbox bridge)
|
||||
- Confirm `ONYX_SERVER_URL` resolves from the sandbox bridge and includes any
|
||||
API path prefix. The Compose default is `http://onyx-craft-api:8080`.
|
||||
|
||||
### Sandbox Stuck in PROVISIONING (Kubernetes)
|
||||
|
||||
@@ -252,7 +257,7 @@ uv run pytest backend/tests/integration/tests/craft/k8s/test_kubernetes_sandbox.
|
||||
- **Sandbox app containers and sidecar init containers** do not receive FileStore, S3, or MinIO credentials
|
||||
- **Network policies** can restrict sandbox egress traffic
|
||||
- **Resource limits** prevent resource exhaustion
|
||||
- **Docker containers** run with `--security-opt no-new-privileges`, `--cap-drop ALL`, `user=1000:1000`, no Docker socket, and a fixed env allowlist (`ONYX_PAT` + `ONYX_SERVER_URL`)
|
||||
- **Docker containers** run with `--security-opt no-new-privileges`, `--cap-drop ALL`, `user=1000:1000`, no Docker socket, and a fixed env allowlist (`ONYX_PAT` + `ONYX_SERVER_URL` + `ONYX_API_PREFIX`)
|
||||
- **Docker network isolation** is enforced by joining only the dedicated `onyx_craft_sandbox` bridge — compose's default network (postgres/redis/minio/model servers) is unreachable by DNS from inside a sandbox
|
||||
- **EC2 IMDS** must be blocked at the host firewall (`install.sh --include-craft` installs a `DOCKER-USER` iptables rule on EC2 when sudo is available) — there is no app-level fallback
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ Sandbox containers run with:
|
||||
- no Docker socket mount
|
||||
- no S3 / MinIO / Postgres / Redis / FileStore credentials in env
|
||||
- a fixed env allowlist (``ONYX_PAT``, ``ONYX_SERVER_URL``,
|
||||
``ONYX_API_PREFIX``,
|
||||
opencode auth/config only)
|
||||
- only the dedicated sandbox bridge network — never compose's default
|
||||
network. As a result api_server / postgres / redis / minio /
|
||||
@@ -47,8 +48,9 @@ Outbound communication is intentionally limited to:
|
||||
1. Public internet over HTTPS (the bridge has default internet egress; block at
|
||||
the host's ``DOCKER-USER`` chain if you need a stricter posture, e.g. for EC2
|
||||
IMDS).
|
||||
2. The Onyx API via ``ONYX_SERVER_URL`` — which must be the *public* HTTPS URL
|
||||
the agent reaches just like any other onyx-cli client.
|
||||
2. The Onyx API via the complete ``ONYX_SERVER_URL`` API base. The Craft
|
||||
overlay uses a private alias on the sandbox bridge by default; deployments
|
||||
may instead provide a public HTTPS API URL.
|
||||
|
||||
Most control-plane traffic from api_server → sandbox uses the Docker
|
||||
Engine API (``docker exec``). Prompt/event transport uses opencode-serve over
|
||||
@@ -82,10 +84,10 @@ from onyx.db.enums import SandboxStatus
|
||||
from onyx.file_store.file_store import get_default_file_store
|
||||
from onyx.server.features.build.configs import (
|
||||
ATTACHMENTS_DIRECTORY,
|
||||
ONYX_SERVER_URL,
|
||||
OPENCODE_DISABLED_TOOLS,
|
||||
OPENCODE_SERVE_PORT,
|
||||
OPENCODE_SERVER_PASSWORD,
|
||||
SANDBOX_API_SERVER_URL,
|
||||
SANDBOX_CONTAINER_IMAGE,
|
||||
SANDBOX_DOCKER_CPU_LIMIT,
|
||||
SANDBOX_DOCKER_MEMORY_LIMIT,
|
||||
@@ -326,9 +328,9 @@ _COMPOSE_INTERNAL_HOSTNAMES = {
|
||||
def _looks_like_internal_compose_host(url: str) -> bool:
|
||||
"""Heuristic: Does ``url`` reference a compose-internal service hostname?
|
||||
|
||||
Used to warn deployers that pointed SANDBOX_API_SERVER_URL at the
|
||||
api_server's compose DNS name. Sandboxes can't resolve that — they only join
|
||||
the craft bridge network — so the URL must be the public Onyx URL.
|
||||
Used to warn deployers that pointed ONYX_SERVER_URL at a service available
|
||||
only on Compose's default network. Sandboxes can resolve the dedicated
|
||||
``onyx-craft-api`` alias, but not these unrelated service names.
|
||||
"""
|
||||
if not url:
|
||||
return False
|
||||
@@ -542,10 +544,10 @@ def build_container_create_kwargs(
|
||||
``firewall-init.sh`` to read ``ca.crt``. That volume also contains
|
||||
root-only ``ca.key``; the agent runs as UID 1000 after init.
|
||||
|
||||
``ONYX_SERVER_URL`` must be the *public* Onyx URL (the one onyx-cli inside
|
||||
the sandbox will hit over HTTPS) — not an internal compose DNS name. We emit
|
||||
a warning if it looks like the latter, since reaching it would require the
|
||||
sandbox to be on the compose default network.
|
||||
``ONYX_SERVER_URL`` is the complete API base used by onyx-cli inside the
|
||||
sandbox. The default ``onyx-craft-api`` alias is attached to the sandbox
|
||||
bridge; we warn about other Compose DNS names because they exist only on the
|
||||
default network.
|
||||
|
||||
``opencode_password`` is generated per-provision by the manager and injected
|
||||
as the env var named by ``OPENCODE_SERVER_PASSWORD``. The api_server reads
|
||||
@@ -556,16 +558,19 @@ def build_container_create_kwargs(
|
||||
"""
|
||||
if _looks_like_internal_compose_host(api_server_url):
|
||||
logger.warning(
|
||||
"SANDBOX_API_SERVER_URL=%s looks like an internal compose hostname. Sandboxes only "
|
||||
"join the craft bridge network and reach the API server like any other public client, "
|
||||
"so this URL must resolve publicly (e.g. https://onyx.your-org.com). Internal DNS will "
|
||||
"fail and the agent will see 'connection refused'.",
|
||||
"ONYX_SERVER_URL=%s looks like an internal compose hostname. Sandboxes only "
|
||||
"join the craft bridge network, so default-network DNS will fail. Use the "
|
||||
"http://onyx-craft-api:8080 bridge alias or a public API base such as "
|
||||
"https://onyx.your-org.com/api.",
|
||||
api_server_url,
|
||||
)
|
||||
|
||||
env: dict[str, str] = {
|
||||
"ONYX_PAT": onyx_pat,
|
||||
"ONYX_SERVER_URL": api_server_url,
|
||||
# The deployment URL is already the exact API base. Disable the CLI's
|
||||
# compatibility prefix for direct services and prefixed ingress URLs.
|
||||
"ONYX_API_PREFIX": "",
|
||||
OPENCODE_SERVER_PASSWORD: opencode_password,
|
||||
"OPENCODE_CONFIG_CONTENT": opencode_config_json,
|
||||
}
|
||||
@@ -832,11 +837,11 @@ class DockerSandboxManager(SandboxManager):
|
||||
) -> SandboxInfo:
|
||||
if not onyx_pat:
|
||||
raise ValueError("onyx_pat is required for Docker sandbox provisioning.")
|
||||
if not SANDBOX_API_SERVER_URL:
|
||||
if not ONYX_SERVER_URL:
|
||||
raise ValueError(
|
||||
"SANDBOX_API_SERVER_URL must be set for Docker sandbox provisioning."
|
||||
"ONYX_SERVER_URL must be set for Docker sandbox provisioning."
|
||||
)
|
||||
validate_sandbox_api_url(SANDBOX_API_SERVER_URL)
|
||||
validate_sandbox_api_url(ONYX_SERVER_URL)
|
||||
|
||||
logger.info(
|
||||
"Provisioning Docker sandbox %s for user %s, tenant %s.",
|
||||
@@ -999,7 +1004,7 @@ class DockerSandboxManager(SandboxManager):
|
||||
tenant_id=tenant_id,
|
||||
image=self._image,
|
||||
onyx_pat=onyx_pat,
|
||||
api_server_url=SANDBOX_API_SERVER_URL,
|
||||
api_server_url=ONYX_SERVER_URL,
|
||||
network=self._network_name,
|
||||
volume_name=volume_name,
|
||||
memory_limit=self._memory_limit,
|
||||
|
||||
@@ -64,10 +64,10 @@ from onyx.cache.interface import CACHE_TRANSIENT_ERRORS
|
||||
from onyx.db.enums import SandboxStatus
|
||||
from onyx.file_store.file_store import get_default_file_store
|
||||
from onyx.server.features.build.configs import (
|
||||
ONYX_SERVER_URL,
|
||||
OPENCODE_DISABLED_TOOLS,
|
||||
OPENCODE_SERVE_PORT,
|
||||
OPENCODE_SERVER_PASSWORD,
|
||||
SANDBOX_API_SERVER_URL,
|
||||
SANDBOX_CONTAINER_IMAGE,
|
||||
SANDBOX_NAMESPACE,
|
||||
SANDBOX_NEXTJS_PORT_END,
|
||||
@@ -1048,11 +1048,11 @@ class KubernetesSandboxManager(SandboxManager):
|
||||
|
||||
if not onyx_pat:
|
||||
raise ValueError("onyx_pat is required for Kubernetes sandbox provisioning")
|
||||
if not SANDBOX_API_SERVER_URL:
|
||||
if not ONYX_SERVER_URL:
|
||||
raise ValueError(
|
||||
"SANDBOX_API_SERVER_URL must be set for Kubernetes sandbox provisioning"
|
||||
"ONYX_SERVER_URL must be set for Kubernetes sandbox provisioning"
|
||||
)
|
||||
validate_sandbox_api_url(SANDBOX_API_SERVER_URL)
|
||||
validate_sandbox_api_url(ONYX_SERVER_URL)
|
||||
if not SANDBOX_PROXY_HOST:
|
||||
raise ValueError(
|
||||
"SANDBOX_PROXY_HOST must be set for Kubernetes sandbox provisioning"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Provision-time validation of SANDBOX_API_SERVER_URL.
|
||||
"""Provision-time validation of ONYX_SERVER_URL.
|
||||
|
||||
SANDBOX_API_SERVER_URL is the full base URL a sandbox client uses, any API
|
||||
path prefix included. The classic misconfiguration is a public URL without
|
||||
its /api prefix, which fails far from the cause: every LLM-gateway and
|
||||
onyx-cli call inside the sandbox 404s. Probe once per process at provision
|
||||
time and fail with the corrected value instead.
|
||||
ONYX_SERVER_URL is the full base URL a client uses, any API path prefix
|
||||
included. The classic misconfiguration is a public URL without its /api
|
||||
prefix, which fails far from the cause: every LLM-gateway and onyx-cli call
|
||||
inside the sandbox 404s. Probe once per process at provision time and fail
|
||||
with the corrected value instead.
|
||||
"""
|
||||
|
||||
import threading
|
||||
@@ -68,7 +68,7 @@ def validate_sandbox_api_url(api_server_url: str) -> None:
|
||||
response = _probe_health(base)
|
||||
if response is None:
|
||||
logger.warning(
|
||||
"Could not probe %s/health to validate SANDBOX_API_SERVER_URL; "
|
||||
"Could not probe %s/health to validate ONYX_SERVER_URL; "
|
||||
"skipping the prefix check",
|
||||
base,
|
||||
)
|
||||
@@ -91,14 +91,14 @@ def validate_sandbox_api_url(api_server_url: str) -> None:
|
||||
if prefixed_response is not None and _is_health_response(prefixed_response):
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.INTERNAL_ERROR,
|
||||
f"SANDBOX_API_SERVER_URL={api_server_url!r} is missing its API path "
|
||||
f"ONYX_SERVER_URL={api_server_url!r} is missing its API path "
|
||||
f"prefix: {base}/health does not serve the API health payload "
|
||||
f"(HTTP {response.status_code}) while {prefixed}/health does. "
|
||||
f"Set SANDBOX_API_SERVER_URL to {prefixed!r} — the full base URL a "
|
||||
f"Set ONYX_SERVER_URL to {prefixed!r} — the full base URL a "
|
||||
"sandbox client uses.",
|
||||
)
|
||||
logger.warning(
|
||||
"SANDBOX_API_SERVER_URL=%s: %s/health did not serve the API health "
|
||||
"ONYX_SERVER_URL=%s: %s/health did not serve the API health "
|
||||
"payload (HTTP %s) and %s/health did not either; sandbox API calls "
|
||||
"will likely fail. Verify the URL is the full base URL including any "
|
||||
"path prefix.",
|
||||
|
||||
@@ -133,7 +133,7 @@ def lock_env(
|
||||
preconditions (URL/proxy host) satisfied."""
|
||||
monkeypatch.setattr(factory, "CACHE_BACKEND", CacheBackendType.REDIS)
|
||||
monkeypatch.setattr(
|
||||
kubernetes_sandbox_manager, "SANDBOX_API_SERVER_URL", "http://api-server"
|
||||
kubernetes_sandbox_manager, "ONYX_SERVER_URL", "http://api-server"
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
kubernetes_sandbox_manager, "SANDBOX_PROXY_HOST", "sandbox-proxy"
|
||||
|
||||
@@ -49,7 +49,7 @@ def test_claims_then_resolve_round_trips_minted_pat(
|
||||
matched_actions=None,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(onyx_pat_mod, "SANDBOX_API_SERVER_URL", f"https://{_API_HOST}")
|
||||
monkeypatch.setattr(onyx_pat_mod, "ONYX_SERVER_URL", f"https://{_API_HOST}")
|
||||
resolver = OnyxPatResolver()
|
||||
assert resolver.claims(MagicMock(host=_API_HOST, port=443), ctx) is True
|
||||
assert resolver.claims(MagicMock(host="slack.com", port=443), ctx) is False
|
||||
|
||||
@@ -101,7 +101,7 @@ def test_reuse_existing_pod_clears_stale_tombstone() -> None:
|
||||
mgr._terminated_sandboxes.add(sandbox_id)
|
||||
|
||||
with (
|
||||
mock.patch.object(k8s_mod, "SANDBOX_API_SERVER_URL", "http://api"),
|
||||
mock.patch.object(k8s_mod, "ONYX_SERVER_URL", "http://api"),
|
||||
mock.patch.object(k8s_mod, "SANDBOX_PROXY_HOST", "proxy.local"),
|
||||
mock.patch.object(mgr, "_pod_exists_and_healthy", return_value=True),
|
||||
mock.patch.object(mgr, "_ensure_service_exists"),
|
||||
|
||||
+11
-7
@@ -250,7 +250,7 @@ def proxy_kwargs() -> ContainerCreateKwargs:
|
||||
tenant_id=TENANT_ID,
|
||||
image="onyxdotapp/sandbox:test",
|
||||
onyx_pat=SANDBOX_PROXY_INJECTED_PLACEHOLDER,
|
||||
api_server_url="https://onyx.example.com",
|
||||
api_server_url="https://onyx.example.com/api",
|
||||
network="onyx_craft_sandbox",
|
||||
volume_name="onyx-craft-sandbox-12345678",
|
||||
memory_limit="2g",
|
||||
@@ -359,6 +359,7 @@ def test_container_kwargs_env_allowlist_excludes_storage_credentials(
|
||||
# Required env
|
||||
assert env["ONYX_PAT"] == "pat-redacted"
|
||||
assert env["ONYX_SERVER_URL"] == "http://api_server:8080"
|
||||
assert env["ONYX_API_PREFIX"] == ""
|
||||
# opencode-serve transport wiring
|
||||
assert env["OPENCODE_SERVER_PASSWORD"] == _OPENCODE_PASSWORD
|
||||
assert env["OPENCODE_CONFIG_CONTENT"] == _OPENCODE_CONFIG_JSON
|
||||
@@ -493,6 +494,7 @@ def test_container_kwargs_env_is_a_minimal_allowlist(
|
||||
assert set(env.keys()) == {
|
||||
"ONYX_PAT",
|
||||
"ONYX_SERVER_URL",
|
||||
"ONYX_API_PREFIX",
|
||||
"OPENCODE_SERVER_PASSWORD",
|
||||
"OPENCODE_CONFIG_CONTENT",
|
||||
}
|
||||
@@ -526,7 +528,7 @@ def test_container_kwargs_mounts_tmp_as_tmpfs(
|
||||
def test_container_kwargs_warns_on_internal_compose_host(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Deployers that point SANDBOX_API_SERVER_URL at compose DNS get warned."""
|
||||
"""Deployers that point ONYX_SERVER_URL at compose DNS get warned."""
|
||||
import logging
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
@@ -563,7 +565,7 @@ def test_container_kwargs_no_warning_for_public_url(
|
||||
tenant_id=TENANT_ID,
|
||||
image="onyxdotapp/sandbox:test",
|
||||
onyx_pat="pat",
|
||||
api_server_url="https://onyx.example.com",
|
||||
api_server_url="https://onyx.example.com/api",
|
||||
network="onyx_craft_sandbox",
|
||||
volume_name="vol",
|
||||
memory_limit="2g",
|
||||
@@ -656,10 +658,11 @@ def test_proxy_kwargs_env_contains_proxy_and_ca_keys(
|
||||
contract vars.
|
||||
"""
|
||||
env = proxy_kwargs["environment"]
|
||||
# The legacy 4-key core is preserved; ONYX_PAT is the proxy placeholder in
|
||||
# this posture (real value lives in Postgres, proxy injects on wire).
|
||||
# The compatibility core is preserved; ONYX_PAT is the proxy placeholder
|
||||
# in this posture (real value lives in Postgres, proxy injects on wire).
|
||||
assert env["ONYX_PAT"] == SANDBOX_PROXY_INJECTED_PLACEHOLDER
|
||||
assert env["ONYX_SERVER_URL"] == "https://onyx.example.com"
|
||||
assert env["ONYX_SERVER_URL"] == "https://onyx.example.com/api"
|
||||
assert env["ONYX_API_PREFIX"] == ""
|
||||
assert env["OPENCODE_SERVER_PASSWORD"] == _OPENCODE_PASSWORD
|
||||
assert env["OPENCODE_CONFIG_CONTENT"] == _OPENCODE_CONFIG_JSON
|
||||
# firewall-init.sh contract.
|
||||
@@ -729,6 +732,7 @@ def test_proxy_kwargs_env_is_a_locked_allowlist(
|
||||
# Legacy core
|
||||
"ONYX_PAT",
|
||||
"ONYX_SERVER_URL",
|
||||
"ONYX_API_PREFIX",
|
||||
"OPENCODE_SERVER_PASSWORD",
|
||||
"OPENCODE_CONFIG_CONTENT",
|
||||
# firewall-init.sh contract
|
||||
@@ -777,7 +781,7 @@ def test_proxy_kwargs_requires_ca_volume() -> None:
|
||||
tenant_id=TENANT_ID,
|
||||
image="onyxdotapp/sandbox:test",
|
||||
onyx_pat="pat",
|
||||
api_server_url="https://onyx.example.com",
|
||||
api_server_url="https://onyx.example.com/api",
|
||||
network="onyx_craft_sandbox",
|
||||
volume_name="vol",
|
||||
memory_limit="2g",
|
||||
|
||||
@@ -46,6 +46,8 @@ def _bare_manager() -> DockerSandboxManager:
|
||||
mgr._agent_instructions_template_path = ( # type: ignore[attr-defined]
|
||||
dsm.Path(dsm.__file__).parent.parent.parent / "AGENTS.template.md"
|
||||
)
|
||||
mgr._image_checked = False # type: ignore[attr-defined]
|
||||
mgr._image_check_lock = dsm.threading.Lock() # type: ignore[attr-defined]
|
||||
mgr._init_serve_state()
|
||||
return mgr
|
||||
|
||||
@@ -316,7 +318,7 @@ def test_provision_generates_fresh_password_and_injects_into_container_env(
|
||||
every later request 401s.
|
||||
"""
|
||||
monkeypatch.setattr(dsm, "DEV_MODE", True)
|
||||
monkeypatch.setattr(dsm, "SANDBOX_API_SERVER_URL", "https://onyx.example.com")
|
||||
monkeypatch.setattr(dsm, "ONYX_SERVER_URL", "https://onyx.example.com/api")
|
||||
monkeypatch.setattr(dsm, "validate_sandbox_api_url", lambda *_: None)
|
||||
# Skip the actual readiness HTTP probe — that needs a real container.
|
||||
monkeypatch.setattr(
|
||||
@@ -376,6 +378,7 @@ def test_provision_generates_fresh_password_and_injects_into_container_env(
|
||||
assert set(env.keys()) == {
|
||||
"ONYX_PAT",
|
||||
"ONYX_SERVER_URL",
|
||||
"ONYX_API_PREFIX",
|
||||
OPENCODE_SERVER_PASSWORD,
|
||||
"OPENCODE_CONFIG_CONTENT",
|
||||
}
|
||||
@@ -475,7 +478,7 @@ def test_provision_removes_container_when_history_restore_fails(
|
||||
home.
|
||||
"""
|
||||
monkeypatch.setattr(dsm, "DEV_MODE", True)
|
||||
monkeypatch.setattr(dsm, "SANDBOX_API_SERVER_URL", "https://onyx.example.com")
|
||||
monkeypatch.setattr(dsm, "ONYX_SERVER_URL", "https://onyx.example.com/api")
|
||||
monkeypatch.setattr(dsm, "validate_sandbox_api_url", lambda *_: None)
|
||||
|
||||
mgr = _bare_manager()
|
||||
|
||||
+8
-7
@@ -41,6 +41,7 @@ from onyx.server.features.build.sandbox.image.sandbox_daemon.contract import (
|
||||
)
|
||||
from onyx.server.features.build.sandbox.kubernetes import sidecar_client
|
||||
from onyx.server.features.build.sandbox.kubernetes.kubernetes_sandbox_manager import (
|
||||
OPENCODE_HISTORY_RESTORE_TIMEOUT_SECONDS,
|
||||
KubernetesSandboxManager,
|
||||
_build_targz,
|
||||
)
|
||||
@@ -703,7 +704,7 @@ def test_restore_opencode_history_posts_archive_to_sidecar(
|
||||
assert archive_file.read() == archive_body
|
||||
assert sha256_hex == hashlib.sha256(archive_body).hexdigest()
|
||||
assert operation_label == "opencode history restore"
|
||||
assert timeout_seconds == 300.0
|
||||
assert timeout_seconds == OPENCODE_HISTORY_RESTORE_TIMEOUT_SECONDS
|
||||
calls.append("restore")
|
||||
|
||||
sidecar_client = MagicMock()
|
||||
@@ -737,7 +738,7 @@ def test_restore_opencode_history_marks_sidecar_ready_when_no_snapshot(
|
||||
assert sandbox_id == expected_sandbox_id
|
||||
assert endpoint_path == SIDECAR_OPENCODE_HISTORY_MARK_RESTORED_PATH
|
||||
assert operation_label == "opencode history restore marker"
|
||||
assert timeout_seconds == 300.0
|
||||
assert timeout_seconds == OPENCODE_HISTORY_RESTORE_TIMEOUT_SECONDS
|
||||
|
||||
sidecar_client = MagicMock()
|
||||
sidecar_client.post_empty.side_effect = fake_mark_restored
|
||||
@@ -753,7 +754,7 @@ def test_provision_cleans_up_pod_when_opencode_history_restore_fails(
|
||||
) -> None:
|
||||
import onyx.server.features.build.sandbox.kubernetes.kubernetes_sandbox_manager as ksm
|
||||
|
||||
monkeypatch.setattr(ksm, "SANDBOX_API_SERVER_URL", "http://api-server")
|
||||
monkeypatch.setattr(ksm, "ONYX_SERVER_URL", "http://api-server")
|
||||
monkeypatch.setattr(ksm, "validate_sandbox_api_url", lambda *_: None)
|
||||
monkeypatch.setattr(ksm, "SANDBOX_PROXY_HOST", "proxy.local")
|
||||
|
||||
@@ -798,7 +799,7 @@ def test_provision_existing_healthy_pod_does_not_restore_opencode_history(
|
||||
) -> None:
|
||||
import onyx.server.features.build.sandbox.kubernetes.kubernetes_sandbox_manager as ksm
|
||||
|
||||
monkeypatch.setattr(ksm, "SANDBOX_API_SERVER_URL", "http://api-server")
|
||||
monkeypatch.setattr(ksm, "ONYX_SERVER_URL", "http://api-server")
|
||||
monkeypatch.setattr(ksm, "validate_sandbox_api_url", lambda *_: None)
|
||||
monkeypatch.setattr(ksm, "SANDBOX_PROXY_HOST", "proxy.local")
|
||||
|
||||
@@ -836,7 +837,7 @@ def test_provision_conflicting_healthy_pod_skips_startup_restore(
|
||||
) -> None:
|
||||
import onyx.server.features.build.sandbox.kubernetes.kubernetes_sandbox_manager as ksm
|
||||
|
||||
monkeypatch.setattr(ksm, "SANDBOX_API_SERVER_URL", "http://api-server")
|
||||
monkeypatch.setattr(ksm, "ONYX_SERVER_URL", "http://api-server")
|
||||
monkeypatch.setattr(ksm, "validate_sandbox_api_url", lambda *_: None)
|
||||
monkeypatch.setattr(ksm, "SANDBOX_PROXY_HOST", "proxy.local")
|
||||
|
||||
@@ -886,7 +887,7 @@ def test_provision_conflicting_not_ready_pod_runs_startup_restore(
|
||||
) -> None:
|
||||
import onyx.server.features.build.sandbox.kubernetes.kubernetes_sandbox_manager as ksm
|
||||
|
||||
monkeypatch.setattr(ksm, "SANDBOX_API_SERVER_URL", "http://api-server")
|
||||
monkeypatch.setattr(ksm, "ONYX_SERVER_URL", "http://api-server")
|
||||
monkeypatch.setattr(ksm, "validate_sandbox_api_url", lambda *_: None)
|
||||
monkeypatch.setattr(ksm, "SANDBOX_PROXY_HOST", "proxy.local")
|
||||
|
||||
@@ -948,7 +949,7 @@ def test_provision_conflicting_not_ready_pod_restore_failure_does_not_cleanup(
|
||||
) -> None:
|
||||
import onyx.server.features.build.sandbox.kubernetes.kubernetes_sandbox_manager as ksm
|
||||
|
||||
monkeypatch.setattr(ksm, "SANDBOX_API_SERVER_URL", "http://api-server")
|
||||
monkeypatch.setattr(ksm, "ONYX_SERVER_URL", "http://api-server")
|
||||
monkeypatch.setattr(ksm, "validate_sandbox_api_url", lambda *_: None)
|
||||
monkeypatch.setattr(ksm, "SANDBOX_PROXY_HOST", "proxy.local")
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Unit tests for `OnyxPatResolver`.
|
||||
|
||||
Coverage: the host claim rule (incl. inert when `SANDBOX_API_SERVER_URL` is
|
||||
Coverage: the host claim rule (incl. inert when `ONYX_SERVER_URL` is
|
||||
unset), header rendering on both auth headers, and the fail-closed cases the
|
||||
dispatcher maps to a 403. The encrypt/store/read round-trip against a real DB
|
||||
is pinned in `tests/external_dependency_unit/sandbox_proxy/test_onyx_pat_resolver.py`.
|
||||
@@ -70,7 +70,7 @@ def _ctx() -> InjectionContext:
|
||||
@pytest.fixture
|
||||
def resolver(monkeypatch: pytest.MonkeyPatch) -> OnyxPatResolver:
|
||||
# __init__ captures the API host, so the patch must precede construction.
|
||||
monkeypatch.setattr(onyx_pat, "SANDBOX_API_SERVER_URL", _API_URL)
|
||||
monkeypatch.setattr(onyx_pat, "ONYX_SERVER_URL", _API_URL)
|
||||
return OnyxPatResolver()
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ def test_claims_requires_matching_host_and_port(resolver: OnyxPatResolver) -> No
|
||||
|
||||
|
||||
def test_claims_matches_explicit_port(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(onyx_pat, "SANDBOX_API_SERVER_URL", "http://internal:8080")
|
||||
monkeypatch.setattr(onyx_pat, "ONYX_SERVER_URL", "http://internal:8080")
|
||||
resolver = OnyxPatResolver()
|
||||
assert (
|
||||
resolver.claims(make_flow(host="internal", port=8080).request, _ctx()) is True
|
||||
@@ -102,7 +102,7 @@ def test_claims_matches_explicit_port(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
|
||||
|
||||
def test_inert_when_api_url_unset(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(onyx_pat, "SANDBOX_API_SERVER_URL", "")
|
||||
monkeypatch.setattr(onyx_pat, "ONYX_SERVER_URL", "")
|
||||
assert (
|
||||
OnyxPatResolver().claims(make_flow(host=_API_HOST, port=443).request, _ctx())
|
||||
is False
|
||||
|
||||
+2
-1
@@ -31,7 +31,8 @@ Environment variables override config file values:
|
||||
|
||||
| Variable | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `ONYX_SERVER_URL` | No | Server URL (default: `https://cloud.onyx.app`) |
|
||||
| `ONYX_SERVER_URL` | No | Server origin or already-prefixed API base (default: `https://cloud.onyx.app`) |
|
||||
| `ONYX_API_PREFIX` | No | API path prefix (default: `/api`); set to empty for direct backend access |
|
||||
| `ONYX_PAT` | No | Personal access token for authentication (required if no config file) |
|
||||
| `ONYX_PERSONA_ID` | No | Default agent/persona ID |
|
||||
| `ONYX_STREAM_MARKDOWN` | No | Enable/disable progressive markdown rendering (true/false) |
|
||||
|
||||
@@ -36,8 +36,8 @@ type Client struct {
|
||||
}
|
||||
|
||||
// NewClient creates a new API client from config.
|
||||
// ServerURL is the server origin (e.g. "https://cloud.onyx.app").
|
||||
// APIURL appends the /api prefix to form the API base URL.
|
||||
// ServerURL may be a server origin or an API base that already includes the
|
||||
// configured API prefix.
|
||||
func NewClient(cfg config.OnyxCliConfig) *Client {
|
||||
var transport *http.Transport
|
||||
if t, ok := http.DefaultTransport.(*http.Transport); ok {
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestSearch_Success(t *testing.T) {
|
||||
if r.Method != "POST" {
|
||||
t.Errorf("method = %s, want POST", r.Method)
|
||||
}
|
||||
if !strings.HasSuffix(r.URL.Path, "/search") {
|
||||
if r.URL.Path != "/api/search" {
|
||||
t.Errorf("path = %s, want /api/search", r.URL.Path)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
const (
|
||||
EnvServerURL = "ONYX_SERVER_URL"
|
||||
EnvAPIPrefix = "ONYX_API_PREFIX"
|
||||
EnvAPIKey = "ONYX_PAT"
|
||||
EnvAgentID = "ONYX_PERSONA_ID"
|
||||
EnvSSHHostKey = "ONYX_SSH_HOST_KEY"
|
||||
@@ -56,18 +57,38 @@ func (c OnyxCliConfig) IsConfigured() bool {
|
||||
return c.APIKey != ""
|
||||
}
|
||||
|
||||
// APIURL appends the API prefix (default "/api") to the server origin.
|
||||
// Set ONYX_API_PREFIX="" for direct backend access without the proxy prefix.
|
||||
func APIURL(serverURL string) string {
|
||||
func apiPrefix() string {
|
||||
prefix := "/api"
|
||||
if v, ok := os.LookupEnv("ONYX_API_PREFIX"); ok {
|
||||
prefix = v
|
||||
if value, ok := os.LookupEnv(EnvAPIPrefix); ok {
|
||||
prefix = value
|
||||
}
|
||||
u := strings.TrimRight(serverURL, "/")
|
||||
return strings.Trim(prefix, "/")
|
||||
}
|
||||
|
||||
// APIURL returns the API base for a server origin or an already-prefixed URL.
|
||||
// Set ONYX_API_PREFIX="" for direct backend access without a proxy path.
|
||||
func APIURL(serverURL string) string {
|
||||
baseURL := strings.TrimRight(serverURL, "/")
|
||||
prefix := apiPrefix()
|
||||
if prefix == "" {
|
||||
return u
|
||||
return baseURL
|
||||
}
|
||||
return u + "/" + strings.Trim(prefix, "/")
|
||||
|
||||
suffix := "/" + prefix
|
||||
if strings.HasSuffix(baseURL, suffix) {
|
||||
return baseURL
|
||||
}
|
||||
return baseURL + suffix
|
||||
}
|
||||
|
||||
// OnyxWebURL removes an API prefix when the configured URL already includes it.
|
||||
func OnyxWebURL(serverURL string) string {
|
||||
baseURL := strings.TrimRight(serverURL, "/")
|
||||
prefix := apiPrefix()
|
||||
if prefix == "" {
|
||||
return baseURL
|
||||
}
|
||||
return strings.TrimSuffix(baseURL, "/"+prefix)
|
||||
}
|
||||
|
||||
// ConfigDir returns ~/.config/onyx-cli
|
||||
|
||||
@@ -254,14 +254,16 @@ func TestSaveCreatesParentDirs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAPIURL(t *testing.T) {
|
||||
t.Setenv(EnvAPIPrefix, "/api")
|
||||
cases := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"https://cloud.onyx.app", "https://cloud.onyx.app/api"},
|
||||
{"https://cloud.onyx.app/", "https://cloud.onyx.app/api"},
|
||||
{"https://cloud.onyx.app/api", "https://cloud.onyx.app/api"},
|
||||
{"https://cloud.onyx.app/api/", "https://cloud.onyx.app/api"},
|
||||
{"http://localhost:8080", "http://localhost:8080/api"},
|
||||
{"http://localhost:3000", "http://localhost:3000/api"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := APIURL(tc.input)
|
||||
@@ -271,20 +273,42 @@ func TestAPIURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIURLCustomPrefix(t *testing.T) {
|
||||
t.Setenv(EnvAPIPrefix, "/onyx/api")
|
||||
for _, input := range []string{
|
||||
"https://onyx.example",
|
||||
"https://onyx.example/onyx/api",
|
||||
} {
|
||||
got := APIURL(input)
|
||||
if got != "https://onyx.example/onyx/api" {
|
||||
t.Errorf("APIURL(%q) = %q", input, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIURLEmptyPrefix(t *testing.T) {
|
||||
t.Setenv("ONYX_API_PREFIX", "")
|
||||
t.Setenv(EnvAPIPrefix, "")
|
||||
if got := APIURL("http://localhost:8080/"); got != "http://localhost:8080" {
|
||||
t.Errorf("APIURL() = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnyxWebURL(t *testing.T) {
|
||||
t.Setenv(EnvAPIPrefix, "/api")
|
||||
cases := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"http://localhost:8080", "http://localhost:8080"},
|
||||
{"http://localhost:8080/", "http://localhost:8080"},
|
||||
{"https://cloud.onyx.app/api", "https://cloud.onyx.app"},
|
||||
{"https://cloud.onyx.app/api/", "https://cloud.onyx.app"},
|
||||
{"https://onyx.example/base/api", "https://onyx.example/base"},
|
||||
{"https://cloud.onyx.app", "https://cloud.onyx.app"},
|
||||
{"http://localhost:8080", "http://localhost:8080"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := APIURL(tc.input)
|
||||
got := OnyxWebURL(tc.input)
|
||||
if got != tc.want {
|
||||
t.Errorf("APIURL(%q) = %q, want %q", tc.input, got, tc.want)
|
||||
t.Errorf("OnyxWebURL(%q) = %q, want %q", tc.input, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,8 @@ export ONYX_PAT="your-pat"
|
||||
|
||||
| Variable | Required | Description |
|
||||
| ----------------- | -------- | -------------------------------------------------------- |
|
||||
| `ONYX_SERVER_URL` | No | Onyx server URL (default: `https://cloud.onyx.app`) |
|
||||
| `ONYX_SERVER_URL` | No | Server origin or already-prefixed API base (default: `https://cloud.onyx.app`) |
|
||||
| `ONYX_API_PREFIX` | No | API path prefix (default: `/api`); empty for direct backend access |
|
||||
| `ONYX_PAT` | Yes | Personal access token for authentication (unless config file exists) |
|
||||
| `ONYX_PERSONA_ID` | No | Default agent/persona ID |
|
||||
| `ONYX_STREAM_MARKDOWN` | No | Enable/disable progressive markdown rendering (true/false) |
|
||||
|
||||
@@ -13,9 +13,11 @@ import (
|
||||
"github.com/onyx-dot-app/onyx/cli/internal/iostreams"
|
||||
)
|
||||
|
||||
// NewClient creates a test API client pointed at the given URL.
|
||||
func NewClient(url string) *api.Client {
|
||||
return api.NewClient(config.OnyxCliConfig{ServerURL: url, APIKey: "test-key"})
|
||||
// NewClient creates a test API client pointed at the given server URL.
|
||||
func NewClient(serverURL string) *api.Client {
|
||||
return api.NewClient(
|
||||
config.OnyxCliConfig{ServerURL: serverURL, APIKey: "test-key"},
|
||||
)
|
||||
}
|
||||
|
||||
// StatusServer returns an httptest.Server that always responds with the given status code.
|
||||
|
||||
@@ -49,7 +49,7 @@ func handleSlashCommand(m Model, text string) (Model, tea.Cmd) {
|
||||
return cmdNew(m)
|
||||
|
||||
case "/connectors":
|
||||
url := strings.TrimRight(m.config.ServerURL, "/") + "/admin/indexing/status"
|
||||
url := config.OnyxWebURL(m.config.ServerURL) + "/admin/indexing/status"
|
||||
if browser.OpenBrowser(url) {
|
||||
m.viewport.addInfo("Opened " + url + " in browser")
|
||||
} else {
|
||||
@@ -58,7 +58,7 @@ func handleSlashCommand(m Model, text string) (Model, tea.Cmd) {
|
||||
return m, nil
|
||||
|
||||
case "/settings":
|
||||
url := strings.TrimRight(m.config.ServerURL, "/") + "/app/settings/general"
|
||||
url := config.OnyxWebURL(m.config.ServerURL) + "/app/settings/general"
|
||||
if browser.OpenBrowser(url) {
|
||||
m.viewport.addInfo("Opened " + url + " in browser")
|
||||
} else {
|
||||
|
||||
@@ -8,9 +8,7 @@ services:
|
||||
api_server:
|
||||
environment:
|
||||
- SANDBOX_BACKEND=${SANDBOX_BACKEND:-docker}
|
||||
# Public Onyx URL; Internal compose hostnames don't resolve from the
|
||||
# sandbox bridge.
|
||||
- SANDBOX_API_SERVER_URL=${SANDBOX_API_SERVER_URL:-}
|
||||
- ONYX_SERVER_URL=${ONYX_SERVER_URL:-http://onyx-craft-api:8080}
|
||||
- SANDBOX_CONTAINER_IMAGE=${SANDBOX_CONTAINER_IMAGE:-onyxdotapp/sandbox:${IMAGE_TAG:-latest}}
|
||||
# Pinned: Must match the compose `networks:` block + install.sh literal.
|
||||
# An env_file leak would point Python at a non-existent network.
|
||||
@@ -30,9 +28,10 @@ services:
|
||||
volumes:
|
||||
- ${SANDBOX_DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
|
||||
networks:
|
||||
# Bridge join lets api_server proxy the Next.js preview iframe.
|
||||
- default
|
||||
- onyx_craft_sandbox
|
||||
default:
|
||||
onyx_craft_sandbox:
|
||||
aliases:
|
||||
- onyx-craft-api
|
||||
# service_healthy, not service_started: The proxy binds its listener only
|
||||
# after initial sync, so service_started would race startup provisions.
|
||||
depends_on:
|
||||
@@ -44,7 +43,7 @@ services:
|
||||
# Must match api_server so the idle-cleanup celery task uses the same
|
||||
# backend to snapshot + terminate sandboxes.
|
||||
- SANDBOX_BACKEND=${SANDBOX_BACKEND:-docker}
|
||||
- SANDBOX_API_SERVER_URL=${SANDBOX_API_SERVER_URL:-}
|
||||
- ONYX_SERVER_URL=${ONYX_SERVER_URL:-http://onyx-craft-api:8080}
|
||||
- SANDBOX_CONTAINER_IMAGE=${SANDBOX_CONTAINER_IMAGE:-onyxdotapp/sandbox:${IMAGE_TAG:-latest}}
|
||||
# See api_server above re: Pinned env_file-override vars.
|
||||
- SANDBOX_DOCKER_NETWORK=onyx_craft_sandbox
|
||||
@@ -85,7 +84,7 @@ services:
|
||||
# OnyxPatResolver reads this at boot to know which host to claim for PAT
|
||||
# injection. Without it, /me requests pass through bare with the
|
||||
# placeholder bearer and api_server 403s.
|
||||
- SANDBOX_API_SERVER_URL=${SANDBOX_API_SERVER_URL:-}
|
||||
- ONYX_SERVER_URL=${ONYX_SERVER_URL:-http://onyx-craft-api:8080}
|
||||
volumes:
|
||||
- sandbox_proxy_ca:/var/lib/sandbox-proxy/ca
|
||||
# RO: the proxy only inspects (events watcher + initial sync).
|
||||
|
||||
@@ -21,15 +21,17 @@ IMAGE_TAG=latest
|
||||
## kubernetes by image tag.
|
||||
# SANDBOX_BACKEND=docker
|
||||
#
|
||||
## SANDBOX_API_SERVER_URL must be a *public* HTTPS URL the sandbox can reach
|
||||
## the same way any other Onyx client does — internal compose hostnames like
|
||||
## "http://api_server:8080" do not resolve from inside the sandbox bridge.
|
||||
# SANDBOX_API_SERVER_URL=https://onyx.your-org.example
|
||||
## ONYX_SERVER_URL is the complete API base URL used by Onyx clients. The Craft
|
||||
## compose overlay defaults it to its private onyx-craft-api network alias,
|
||||
## which works on Docker Desktop and Linux without exposing another host port.
|
||||
## Override only when routing sandbox API traffic through a public reverse
|
||||
## proxy, including the /api path prefix served by that proxy.
|
||||
# ONYX_SERVER_URL=https://onyx.your-org.example/api
|
||||
#
|
||||
## Sandbox bridge network. Created by install.sh (or manually:
|
||||
## docker network create onyx_craft_sandbox
|
||||
## Sandboxes join only this network — they cannot reach postgres, redis,
|
||||
## minio, or any other compose service by name.
|
||||
## Sandboxes join only this network. The firewall forces API and public egress
|
||||
## through sandbox-proxy; postgres, redis, and minio stay off the bridge.
|
||||
# SANDBOX_DOCKER_NETWORK=onyx_craft_sandbox
|
||||
#
|
||||
## Per-sandbox container resource limits. Defaults match K8s pod *requests*
|
||||
|
||||
@@ -5,7 +5,7 @@ home: https://www.onyx.app/
|
||||
sources:
|
||||
- "https://github.com/onyx-dot-app/onyx"
|
||||
type: application
|
||||
version: 0.7.2
|
||||
version: 0.7.3
|
||||
appVersion: latest
|
||||
annotations:
|
||||
category: Productivity
|
||||
@@ -17,12 +17,9 @@ annotations:
|
||||
image: docker.io/onyxdotapp/onyx-backend:latest
|
||||
artifacthub.io/changes: |
|
||||
- kind: changed
|
||||
description: The inference and indexing model servers now run on a distroless Docker
|
||||
Hardened Image (no shell or package manager) as nonroot (the onyx user, UID 1001). Custom CA
|
||||
certificates for the model servers are merged with certifi's public roots in Python
|
||||
at startup instead of via update-ca-certificates. This chart version requires the
|
||||
distroless model-server image — upgrade the chart and the model-server image
|
||||
together (see values.yaml).
|
||||
description: Replace configMap.SANDBOX_API_SERVER_URL with
|
||||
configMap.ONYX_SERVER_URL for Craft sandbox API routing. Set it to the
|
||||
exact API base, including /api when routing through the standard ingress.
|
||||
dependencies:
|
||||
# Helm uses the first condition path that exists: `postgresqlOperator.enabled`
|
||||
# is unset by default, so this falls through to `postgresql.enabled`. Set it
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
{{- if and $sandboxBackend (ne $sandboxBackend "kubernetes") }}
|
||||
{{- fail "configMap.SANDBOX_BACKEND must be \"kubernetes\" when configMap.ENABLE_CRAFT is enabled in the Helm chart" }}
|
||||
{{- end }}
|
||||
{{- if not (index .Values.configMap "SANDBOX_API_SERVER_URL") }}
|
||||
{{- fail "configMap.SANDBOX_API_SERVER_URL must be set when configMap.ENABLE_CRAFT is enabled" }}
|
||||
{{- if not (index .Values.configMap "ONYX_SERVER_URL") }}
|
||||
{{- fail "configMap.ONYX_SERVER_URL must be set when configMap.ENABLE_CRAFT is enabled" }}
|
||||
{{- end }}
|
||||
{{- $auth := .Values.auth | default dict }}
|
||||
{{- $sandboxPushSecret := (index $auth "sandboxPushSecret") | default dict }}
|
||||
|
||||
@@ -25,7 +25,7 @@ kubelet evicts sandbox pods first under node disk pressure. */}}
|
||||
{{- $proxyPort := include "onyx.sandboxProxyPort" . }}
|
||||
{{- $proxyHost := include "onyx.sandboxProxyHost" . }}
|
||||
{{- $caConfigMap := (index $cm "SANDBOX_PROXY_CA_CONFIGMAP") | default "sandbox-proxy-ca-bundle" }}
|
||||
{{- $serverUrl := (index $cm "SANDBOX_API_SERVER_URL") }}
|
||||
{{- $serverUrl := (index $cm "ONYX_SERVER_URL") }}
|
||||
{{- $sandboxPod := .Values.sandboxPod | default dict }}
|
||||
{{- $caBundleFile := "/etc/ssl/sandbox/ca-bundle.crt" }}
|
||||
apiVersion: v1
|
||||
@@ -140,6 +140,8 @@ template:
|
||||
env:
|
||||
- { name: ONYX_PAT, value: "replaced_by_egress_proxy" }
|
||||
- { name: ONYX_SERVER_URL, value: "{{ $serverUrl }}" }
|
||||
{{/* The deployment value is the exact API base; do not append the CLI default. */}}
|
||||
- { name: ONYX_API_PREFIX, value: "" }
|
||||
- { name: GH_TOKEN, value: "replaced_by_egress_proxy" }
|
||||
- { name: GH_NO_UPDATE_NOTIFIER, value: "1" }
|
||||
- { name: OPENCODE_DATA_HOME, value: "/workspace/opencode-data" }
|
||||
|
||||
@@ -43,7 +43,7 @@ configMap:
|
||||
# No model server in this lane; skips the GPU-status probe that would hang API startup.
|
||||
DISABLE_MODEL_SERVER: "true"
|
||||
# Sandbox pods call the chart-managed API service, matching production.
|
||||
SANDBOX_API_SERVER_URL: "http://onyx-api-service.onyx.svc.cluster.local:8080"
|
||||
ONYX_SERVER_URL: "http://onyx-api-service.onyx.svc.cluster.local:8080"
|
||||
# Sandbox image the kind cluster pulls (built+pushed by the CI workflow).
|
||||
SANDBOX_CONTAINER_IMAGE: "localhost:5001/onyx-sandbox:ci"
|
||||
# Tiny requests so the sandbox pod schedules alongside the full Onyx stack on
|
||||
|
||||
@@ -79,7 +79,8 @@ codeInterpreter:
|
||||
configMap:
|
||||
ENABLE_CRAFT: "true"
|
||||
# Host gateway (Docker Desktop), not internal cluster DNS; Linux-kind devs sub their own.
|
||||
SANDBOX_API_SERVER_URL: "http://host.docker.internal:3000"
|
||||
# Full base URL, path prefix included — the Next dev server proxies the API under /api.
|
||||
ONYX_SERVER_URL: "http://host.docker.internal:3000/api"
|
||||
ENABLE_PAID_ENTERPRISE_EDITION_FEATURES: "true"
|
||||
|
||||
sandboxProxy:
|
||||
|
||||
@@ -1648,9 +1648,12 @@ configMap:
|
||||
SANDBOX_BACKEND: "kubernetes"
|
||||
SANDBOX_NAMESPACE: "onyx-sandboxes"
|
||||
SANDBOX_SERVICE_ACCOUNT_NAME: "sandbox"
|
||||
# ONYX_SERVER_URL must be the full API base URL, path prefix included — e.g.
|
||||
# "https://onyx.example/api" through the public proxy, or
|
||||
# "http://<release>-api-service.<ns>.svc.cluster.local:8080" directly at the
|
||||
# API service (no prefix). It is required when ENABLE_CRAFT=true.
|
||||
SANDBOX_PROXY_CA_SECRET: "sandbox-proxy-ca"
|
||||
SANDBOX_PROXY_CA_CONFIGMAP: "sandbox-proxy-ca-bundle"
|
||||
|
||||
# -- Craft sandbox RBAC (rendered when configMap.ENABLE_CRAFT=true).
|
||||
craft:
|
||||
# Extra SAs (release ns) to also grant sandbox RBAC, e.g. a separate worker SA.
|
||||
|
||||
@@ -371,7 +371,7 @@ For Craft development, the required vars (already in the template) are:
|
||||
ENABLE_CRAFT=true
|
||||
SANDBOX_BACKEND=kubernetes
|
||||
SANDBOX_CONTAINER_IMAGE=onyxdotapp/sandbox:dev
|
||||
SANDBOX_API_SERVER_URL=http://onyx-api-service.onyx.svc.cluster.local:8080
|
||||
ONYX_SERVER_URL=http://onyx-api-service.onyx.svc.cluster.local:8080
|
||||
ONYX_SANDBOX_PUSH_PRIVATE_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
```
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ bash "$WT"/deployment/docker_compose/install.sh --local --include-craft
|
||||
cat >> ~/onyx_data/deployment/.env <<'ENV'
|
||||
ENABLE_CRAFT=true
|
||||
SANDBOX_BACKEND=docker
|
||||
SANDBOX_API_SERVER_URL=http://host.docker.internal:3001
|
||||
HOST_PORT=3001
|
||||
ENV
|
||||
|
||||
@@ -49,12 +48,8 @@ ENV
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- macOS with **Docker Desktop** (or OrbStack) — these provide `host.docker.internal`
|
||||
resolution from inside the `onyx_craft_sandbox` bridge network, which the
|
||||
sandbox container needs to reach api_server.
|
||||
- On Linux, replace `http://host.docker.internal:3001` with your machine's
|
||||
reachable address (or use `--add-host` workarounds). Native Linux Docker
|
||||
does *not* resolve `host.docker.internal` by default.
|
||||
- Docker Desktop, OrbStack, or Docker Engine on Linux. The Craft overlay uses
|
||||
the private `onyx-craft-api` bridge alias on every platform.
|
||||
- ~80 GB free Docker disk. Onyx's full stack pulls ~30 GB; local image
|
||||
builds add another 10–15 GB; build cache balloons to 40+ GB if you let
|
||||
it. See [OpenSearch read-only block](#opensearch-flipped-into-read-only-mode-disk-full) below.
|
||||
@@ -70,7 +65,7 @@ These must end up in `~/onyx_data/deployment/.env` after install:
|
||||
|---|---|---|
|
||||
| `ENABLE_CRAFT=true` | yes | `--include-craft` sets this (fresh installs and existing `.env`). |
|
||||
| `SANDBOX_BACKEND=docker` | yes | `--include-craft` sets this alongside `ENABLE_CRAFT`. |
|
||||
| `SANDBOX_API_SERVER_URL=http://host.docker.internal:3001` | yes | Provision raises `ValueError("SANDBOX_API_SERVER_URL must be set")` without it. Must be a URL the sandbox container can reach **from the `onyx_craft_sandbox` bridge** — compose-internal hostnames (`api_server`, `nginx`) won't resolve there. Match the port to `HOST_PORT`. |
|
||||
| `ONYX_SERVER_URL` | optional | Complete API base URL. The Craft overlay defaults to `http://onyx-craft-api:8080` on the private sandbox bridge. Override with a public URL only when desired, including its `/api` path prefix. |
|
||||
| `HOST_PORT=3001` | only if 3000 conflicts | Default is 3000; nginx binds this on the host. Free up 3000 or change here. |
|
||||
| `IMAGE_TAG` | optional | Uses the normal compose default (`latest`) unless set. Craft uses this same tag for the sandbox image, so do not set a separate sandbox image for normal deployments. There are **no** Craft-specific app/backend images — Craft is enabled at runtime via `ENABLE_CRAFT=true` (above). See [image architecture](../infra/image-architecture.md). |
|
||||
| `ONYX_BACKEND_IMAGE` | only when running unreleased PRs | Lets you override just the backend image without forcing model-server / web-server to the same tag. |
|
||||
@@ -122,12 +117,11 @@ adapt the prompts (Standard mode = `2`, keep existing env = blank).
|
||||
### 3. Fix the .env
|
||||
|
||||
On an existing `.env`, `--include-craft` writes `ENABLE_CRAFT=true` and
|
||||
`SANDBOX_BACKEND=docker` for you (on both the update and restart paths). It
|
||||
does **not** set the host-specific values, so append those yourself:
|
||||
`SANDBOX_BACKEND=docker` for you (on both the update and restart paths).
|
||||
Set `HOST_PORT` only when the default port is unavailable:
|
||||
|
||||
```bash
|
||||
cat >> ~/onyx_data/deployment/.env <<'ENV'
|
||||
SANDBOX_API_SERVER_URL=http://host.docker.internal:3001
|
||||
HOST_PORT=3001
|
||||
ENV
|
||||
```
|
||||
|
||||
@@ -491,7 +491,7 @@ The proxy uses first-claim-wins credential dispatch. Resolvers must keep
|
||||
### Onyx PAT Resolver
|
||||
|
||||
`OnyxPatResolver` claims requests whose host and port match
|
||||
`SANDBOX_API_SERVER_URL`.
|
||||
`ONYX_SERVER_URL`.
|
||||
|
||||
It loads the sandbox row and decrypts `Sandbox.encrypted_pat`, then injects both
|
||||
Onyx API key header names as bearer tokens. The tenant is embedded in the PAT,
|
||||
@@ -747,7 +747,8 @@ Relevant config:
|
||||
|
||||
- `ENABLE_CRAFT=true` enables Craft chart resources.
|
||||
- `SANDBOX_BACKEND` is `kubernetes` or `docker`.
|
||||
- `SANDBOX_API_SERVER_URL` must be set for sandbox API calls and PAT injection.
|
||||
- `ONYX_SERVER_URL` must be the complete API base for sandbox API calls and PAT
|
||||
injection.
|
||||
- `SANDBOX_PROXY_HOST` and `SANDBOX_PROXY_PORT` tell sandboxes where to proxy.
|
||||
- `SANDBOX_PROXY_LISTEN_PORT` and `SANDBOX_PROXY_HEALTHZ_PORT` configure the
|
||||
proxy process.
|
||||
|
||||
@@ -210,7 +210,7 @@ sandboxProxy:
|
||||
- ElastiCache: `REDIS_HOST`, `REDIS_SSL=true`, `REDIS_SSL_CERT_REQS=none`, `auth.redis.enabled=false` (no auth token).
|
||||
- OpenSearch (v4.0 search backend): `ONYX_DISABLE_VESPA=true`, `ENABLE_OPENSEARCH_INDEXING/RETRIEVAL_FOR_ONYX=true`, `USING_AWS_MANAGED_OPENSEARCH=true`, `OPENSEARCH_REST_API_PORT=443`, `OPENSEARCH_USE_SSL=true`, `OPENSEARCH_ADMIN_USERNAME=admin`.
|
||||
- S3: `S3_FILE_STORE_BUCKET_NAME`, `S3_ENDPOINT_URL=""`, `AWS_REGION_NAME`.
|
||||
- Craft: `ENABLE_CRAFT=true`, `SANDBOX_API_SERVER_URL=http://onyx-api-service.onyx.svc.cluster.local:8080`, `auth.sandboxPushSecret.enabled=true`. (`SANDBOX_SERVICE_ACCOUNT_NAME`/`SANDBOX_CONTAINER_IMAGE` default correctly.)
|
||||
- Craft: `ENABLE_CRAFT=true`, `ONYX_SERVER_URL=http://onyx-api-service.onyx.svc.cluster.local:8080`, `auth.sandboxPushSecret.enabled=true`. (`SANDBOX_SERVICE_ACCOUNT_NAME`/`SANDBOX_CONTAINER_IMAGE` default correctly.)
|
||||
|
||||
### Images
|
||||
Use an immutable release tag for Kubernetes customer Craft deployments, e.g.
|
||||
|
||||
Reference in new issue
Block a user