Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions backend/agents/create_agent_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
UncertaintyReserveBasisUnknown,
)
from nexent.core.tools.parallel_executor import ParallelExecutorTool
from nexent.core.agents.sandbox import SandboxConfig
from nexent.core.agents.nexent_agent import get_local_python_authorized_imports
from nexent.memory import models as memory_models

Expand Down Expand Up @@ -73,6 +72,9 @@
from utils.redis_utils import get_redis_client
from consts.const import (
AGENT_WORKSPACE_ROOT,
NEXENT_SANDBOX_WORKSPACE_MODE,
NEXENT_SANDBOX_CONTAINER_WORKSPACE_ROOT,
NEXENT_SANDBOX_FAILURE_POLICY,
AIDP_API_KEY,
AIDP_SERVER_URL,
AIDP_TENANT_ID,
Expand Down Expand Up @@ -2914,11 +2916,16 @@ async def create_agent_run_info(
# Resolve sandbox config: DB policy overrides env-var defaults.
# build_sandbox_policy returns None when level=local (backward-compatible).
# Import inside function body to avoid circular dependency.
from agents.sandbox_config import resolve_sandbox_config
from management.services.agent.service import build_sandbox_policy, get_sandbox_minio_client
sandbox_policy = build_sandbox_policy(tenant_id=tenant_id, agent_type="")
agent_db_policy = getattr(agent_config, "sandbox_policy", None)
merged_policy = sandbox_policy if sandbox_policy else agent_db_policy
sandbox_config = SandboxConfig.from_dict(merged_policy) if merged_policy else None
sandbox_config = resolve_sandbox_config(
agent_db_policy, sandbox_policy,
workspace_mode=NEXENT_SANDBOX_WORKSPACE_MODE,
container_workspace_root=NEXENT_SANDBOX_CONTAINER_WORKSPACE_ROOT,
failure_policy=NEXENT_SANDBOX_FAILURE_POLICY,
)
sandbox_minio_client = (
get_sandbox_minio_client()
if sandbox_config and sandbox_config.auto_sync_outputs
Expand All @@ -2934,6 +2941,7 @@ async def create_agent_run_info(
if (
getattr(sandbox_config.level, "value", sandbox_config.level) == "docker"
and getattr(sandbox_config.scope, "value", sandbox_config.scope) == "system"
and sandbox_config.workspace_mode == "legacy"
):
sandbox_config.extra_kwargs.update({
"workspace_volume_name": NEXENT_SANDBOX_WORKSPACE_VOLUME,
Expand Down
25 changes: 25 additions & 0 deletions backend/agents/sandbox_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Resolve per-agent sandbox policy without reading deployment environment."""

from typing import Any

from nexent.core.agents.sandbox import SandboxConfig


def resolve_sandbox_config(
database_policy: dict[str, Any] | None, environment_policy: dict[str, Any] | None, *,
workspace_mode: str, container_workspace_root: str, failure_policy: str,
) -> SandboxConfig | None:
"""Use a complete DB policy first; omitted policy fields retain SDK defaults.

Workspace deployment values are defaults that explicit policy fields override.
Empty DB objects represent an unconfigured policy, not an explicit local level.
"""
if database_policy is not None and not isinstance(database_policy, dict):
raise TypeError('Agent sandbox policy must be a dictionary')
policy = database_policy or environment_policy
return SandboxConfig.from_dict({
'workspace_mode': workspace_mode,
'container_workspace_root': container_workspace_root,
'failure_policy': failure_policy,
**policy,
}) if policy else None
3 changes: 3 additions & 0 deletions backend/consts/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ class VectorDatabaseType(str, Enum):
MAX_CONCURRENT_UPLOADS = 5
UPLOAD_FOLDER = os.getenv('UPLOAD_FOLDER', 'uploads')
AGENT_WORKSPACE_ROOT = os.getenv('AGENT_WORKSPACE_ROOT', '/mnt/nexent/workdir')
NEXENT_SANDBOX_WORKSPACE_MODE = os.getenv('NEXENT_SANDBOX_WORKSPACE_MODE', 'legacy')
NEXENT_SANDBOX_CONTAINER_WORKSPACE_ROOT = os.getenv('NEXENT_SANDBOX_CONTAINER_WORKSPACE_ROOT', '')
NEXENT_SANDBOX_FAILURE_POLICY = os.getenv('NEXENT_SANDBOX_FAILURE_POLICY', 'local')
ROOT_DIR = os.getenv("ROOT_DIR")

PER_WAVE_TIMEOUT = int(os.getenv("DP_SPLIT_WAIT_TIMEOUT_PER_WAVE_S", "30"))
Expand Down
8 changes: 8 additions & 0 deletions deploy/env/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,14 @@ NEXENT_SANDBOX_DOCKER_IMAGE=nexent/nexent-sandbox:latest
# Docker named volume shared by nexent-runtime and the system-scoped sandbox.
NEXENT_SANDBOX_WORKSPACE_VOLUME=nexent-agent-workspace

# Workspace deployment defaults; explicit Agent sandbox_policy fields override them.
# A non-empty DB policy replaces the env policy; its other missing fields use SDK defaults.
# For Windows native runtime with Docker Linux containers, use bind and /mnt/nexent.
NEXENT_SANDBOX_WORKSPACE_MODE=legacy
NEXENT_SANDBOX_CONTAINER_WORKSPACE_ROOT=
# local preserves fallback behavior; error reports Docker failures without local execution.
NEXENT_SANDBOX_FAILURE_POLICY=local

# Sandbox resource limits.
NEXENT_SANDBOX_MEMORY_LIMIT_MB=2048
NEXENT_SANDBOX_CPU_QUOTA=1.0
Expand Down
51 changes: 51 additions & 0 deletions docs/sandbox-jupyter-tls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Sandbox Jupyter TLS

Docker sandbox control traffic uses HTTPS for kernel health, creation and deletion,
and WSS for execution, readiness and reconnection. Each container creates its own
ECDSA certificate before Jupyter starts. The private key stays inside the container
under `/home/sandbox/.nexent-jupyter-tls` (directory `0700`, key `0600`).
The bootstrap checks ancestor ownership and write permissions, rejects symlinks,
and uses directory-relative file descriptors and exclusive creation. Private key
permissions apply before writing. Existing identities must be complete, privately
owned, currently valid and have matching keys; unsafe files are never repaired in
place. These filesystem controls do not isolate processes sharing the same UID.

The runtime reads only the public certificate through its existing trusted Docker
connection. Each container owner has a separate trust file and SSL context, shared
by its kernel leases and cleaned up with the owner. Certificate and hostname checks
remain enabled. HTTPS requests do not use environment proxies or follow redirects.
No global CA installation or manual certificate configuration is required.

The certificate covers the container name, localhost and loopback.
Native runtimes continue to use dynamically allocated
loopback ports; containerized runtimes use Docker networking. Workspace mapping,
kernel isolation, execution and cancellation contracts remain unchanged.

## Deployment and migration

- Images must contain `cryptography >= 42` and Jupyter Kernel Gateway with
`certfile`/`keyfile` support. The SDK now declares the cryptography dependency;
both repository Dockerfiles install the SDK. A custom older image without it
must be rebuilt. The configured user must own the private TLS directory and be
able to create it under a trusted, non-publicly-writable `/home/sandbox` parent.
Both repository images already provide this layout. Startup does not download dependencies.
- Before upgrading an existing system sandbox, drain active runs and explicitly
stop its old HTTP or TLS-version-1 container. Version 2 uses the private identity
directory; it does not read or migrate files from the old public temporary path.
The runtime refuses to recover or automatically
delete a running legacy container. On the next acquisition it can remove the
stopped owned container and create a TLS container.
- Certificates are valid for 365 days and retained on container restart. Drain
and recreate a system container before expiry; merely restarting it does not
renew the certificate. There is no hot certificate rotation in this change.
- If recovery cannot load or verify a running container's certificate, it reports
an error and preserves that container. Investigate the certificate/time/Docker
connection, then drain and explicitly stop the owner before recreating it.

Client contexts and the test HTTPS server explicitly require TLS 1.2 or newer.
The repository's Sonar configuration declares Python 3.11, matching the SDK runtime.

This change addresses the Jupyter TLS findings in PR #3985. The separate host
tool callback bridge is outside this change. SonarCloud and Codecov results still
need to be checked on the pushed commit; no findings are suppressed or accepted
automatically.
Loading
Loading