Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
d211645
feat(agent): run OpenClaw inside native task sandboxes
yaoyu-33 Sep 22, 2026
f4bef5b
fix(openclaw): isolate cached runtime version checks
yaoyu-33 Sep 22, 2026
e3766aa
fix(openclaw): account for rewritten transcript usage
yaoyu-33 Sep 23, 2026
6c81819
fix(openclaw): preserve unknown optional usage details
yaoyu-33 Sep 23, 2026
44bdd97
fix(openclaw): retain valid native envelope usage subtotals
yaoyu-33 Sep 23, 2026
14b5fbb
fix(openclaw): retain fallback text with malformed usage
yaoyu-33 Sep 23, 2026
bcab742
fix(openclaw): retain unknown cache detail provenance
yaoyu-33 Sep 23, 2026
f4f64fc
fix(openclaw): route native evaluation through environment sessions
yaoyu-33 Sep 23, 2026
41675d8
fix(openclaw): reject expired and malformed session cookies
yaoyu-33 Sep 23, 2026
6637a9b
docs(openclaw): separate server startup from native collection
yaoyu-33 Sep 23, 2026
42d203d
docs(openclaw): remove trailing whitespace
yaoyu-33 Sep 23, 2026
eb95e8d
fix(openclaw): run native sandboxes without PTY sessions
yaoyu-33 Oct 1, 2026
2ec87a2
fix(openclaw): align native sessions with the shared contract
yaoyu-33 Oct 2, 2026
47d0439
docs(openclaw): preserve pairing opt-in across head discovery
yaoyu-33 Oct 2, 2026
75cbc65
fix(openclaw): allow longer model requests before idle timeout
yaoyu-33 Oct 2, 2026
04121ea
fix(openclaw): support agent-owned native sandboxes
yaoyu-33 Oct 2, 2026
5b39930
refactor(openclaw): share sandbox cleanup and runtime contracts
yaoyu-33 Oct 3, 2026
2d92a63
test(openclaw): wait for cancelled descendants to be reaped
yaoyu-33 Oct 3, 2026
8952633
refactor(hermes): decouple default harness configuration
yaoyu-33 Oct 2, 2026
6d443b6
refactor(hermes): share cleanup and runtime validation
yaoyu-33 Oct 3, 2026
7b1b593
fix(hermes): report provider token usage for root model calls
yaoyu-33 Oct 3, 2026
7dd777f
fix(agent): derive usage from shared model capture
yaoyu-33 Oct 3, 2026
55d742f
refactor(hermes): move token usage to its own PR
yaoyu-33 Oct 3, 2026
1f95e8e
refactor(hermes): share sandbox supervision and session transport
yaoyu-33 Oct 5, 2026
ba8a2c6
refactor(sandbox): share harness execution and close lifecycle
yaoyu-33 Oct 5, 2026
c4aabf0
fix(sandbox): validate bootstrap and tolerate diagnostic gaps
yaoyu-33 Oct 6, 2026
1a7dfb1
refactor(agent): move shared execution helpers into agent_utils
yaoyu-33 Oct 6, 2026
29e979f
feat(sandbox): add binary file transfer helpers
yaoyu-33 Oct 6, 2026
e1f36cd
refactor(sandbox): simplify utility imports and ordering
yaoyu-33 Oct 6, 2026
5671efa
refactor(harness): centralize supervised session ownership
yaoyu-33 Oct 6, 2026
2093960
refactor(agent): restore agent_utils package name
yaoyu-33 Oct 6, 2026
a309dfb
fix(hermes): default owned sandbox lifetime
yaoyu-33 Oct 6, 2026
127c21c
Merge remote-tracking branch 'origin/main' into codex/openclaw-agent-…
yaoyu-33 Oct 6, 2026
a67ebd7
refactor(openclaw): adopt shared sandbox session lifecycle
yaoyu-33 Oct 6, 2026
a88a216
test(openclaw): prepare seeded config directory in exec fixture
yaoyu-33 Oct 6, 2026
d5e6ad2
fix(config): preserve native Hermes templates during migration
yaoyu-33 Oct 6, 2026
0db6de0
fix(config): preserve native openclaw templates during migration
yaoyu-33 Oct 6, 2026
2a1a5cb
chore(openclaw): merge complete shared sandbox lifecycle dependency
yaoyu-33 Oct 6, 2026
d07d993
fix(openclaw): reconcile native sessions with current main
yaoyu-33 Oct 6, 2026
e9fff6a
fix(openclaw): align shared model errors and native routing
yaoyu-33 Oct 6, 2026
5f3f95d
Merge commit '0b27f0efa15f0e3a4a64777e85eac1053b8b0929' into codex/op…
yaoyu-33 Oct 8, 2026
c0136ae
fix(openclaw): align native bootstrap and model error policy
yaoyu-33 Oct 8, 2026
f757424
refactor(openclaw): align session and local config names
yaoyu-33 Oct 8, 2026
fa5ae88
test(openclaw): enforce session-based local CLI routing
yaoyu-33 Oct 8, 2026
4e5107b
Merge main into OpenClaw native-session branch
yaoyu-33 Oct 8, 2026
0ecc9b0
refactor(openclaw): remove redundant fixed-model example
yaoyu-33 Oct 8, 2026
e01e9a0
fix(openclaw): preserve setup errors with absent output
yaoyu-33 Oct 8, 2026
9073807
Merge main into OpenClaw sandbox sessions
yaoyu-33 Oct 9, 2026
01218cd
fix(openclaw): align sandbox terminology and request contract
yaoyu-33 Oct 9, 2026
0ba7a55
fix(openclaw): clarify metadata controls and retry failed setup
yaoyu-33 Oct 9, 2026
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
216 changes: 193 additions & 23 deletions responses_api_agents/openclaw_agent/README.md

Large diffs are not rendered by default.

606 changes: 589 additions & 17 deletions responses_api_agents/openclaw_agent/app.py

Large diffs are not rendered by default.

38 changes: 13 additions & 25 deletions responses_api_agents/openclaw_agent/configs/openclaw_agent.yaml
Original file line number Diff line number Diff line change
@@ -1,30 +1,18 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# Agent sessions run in a sandbox; unseeded requests run the local CLI.
openclaw_agent:
responses_api_agents:
openclaw_agent:
entrypoint: app.py
resources_server:
type: resources_servers
name: ???
concurrency: 32
command: openclaw
model: nvinf/nvidia/meta/llama-3.3-70b-instruct
workspace_root: outputs/openclaw_agent/workspaces
openclaw_agent_id: main
thinking: "off"
system_prompt: null
setup_timeout: 900
timeout: 900
extra_args: []
env: {}
openclaw_config:
models:
providers:
nvinf:
api: openai-completions
baseUrl: https://integrate.api.nvidia.com/v1
apiKey: ${policy_api_key}
models:
- id: nvidia/meta/llama-3.3-70b-instruct
name: nvidia/meta/llama-3.3-70b-instruct
api: openai-completions
num_workers: 1
resources_server: null
Comment thread
yaoyu-33 marked this conversation as resolved.
Resolved
model_server: {type: responses_api_models, name: policy_model}
model: ${policy_model_name}
openclaw_version: 2026.6.11
timeout: 900
model_timeout_seconds: 600
sandbox_install_timeout_seconds: 600
session_close_timeout_seconds: 60
session_close_retry_window_seconds: 300
126 changes: 126 additions & 0 deletions responses_api_agents/openclaw_agent/install_openclaw_runtime.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
set -euo pipefail
trap 'status=$?; echo "OpenClaw installer failed (exit $status) at line $LINENO: $BASH_COMMAND" >&2; exit "$status"' ERR

runtime=$1
openclaw_version=$2
node_version=22.19.0
if [ "$(uname -s)" != Linux ]; then
echo 'OpenClaw sandbox execution requires a Linux sandbox.' >&2
exit 1
fi
if ! command -v python3 >/dev/null 2>&1; then
echo 'OpenClaw sandbox execution requires Python >=3.8 in the task image for descendant supervision.' >&2
exit 1
fi
python3 -c 'import sys; assert sys.version_info >= (3, 8), "OpenClaw sandbox execution requires Python >=3.8"'
case "$(uname -m)" in
x86_64) arch=x64 ;;
aarch64) arch=arm64 ;;
*) echo 'OpenClaw sandbox execution supports Linux x86_64/aarch64 only.' >&2; exit 1 ;;
esac
platform="linux-${arch}"
node_dist="https://nodejs.org/dist/v${node_version}"
if getconf GNU_LIBC_VERSION >/dev/null 2>&1; then
:
elif [[ "$(ldd --version 2>&1 || true)" == *musl* ]]; then
if [ "$arch" != x64 ]; then
echo "OpenClaw sandbox execution's pinned Node ${node_version} musl build supports x86_64 only" >&2
exit 1
fi
platform=linux-x64-musl
node_dist="https://unofficial-builds.nodejs.org/download/release/v${node_version}"
else
echo 'OpenClaw sandbox execution requires glibc or musl; could not identify the sandbox libc' >&2
exit 1
fi

install_packages() {
if [ "$(id -u)" -ne 0 ]; then
echo "OpenClaw sandbox execution requires $*: preinstall these packages (automatic installation requires root)." >&2
exit 1
fi
if command -v apk >/dev/null 2>&1; then
apk add --no-cache "$@"
elif command -v apt-get >/dev/null 2>&1; then
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "$@"
else
echo "OpenClaw sandbox execution requires $*: preinstall these packages (automatic installation requires apt-get or apk)." >&2
exit 1
fi
}

# Never change the task repository or its dependency environment.
python3 - "$runtime" "$PWD" <<'PY'
import pathlib, sys
runtime, workdir = (pathlib.Path(p).resolve() for p in sys.argv[1:])
if workdir == runtime or workdir in runtime.parents or runtime in workdir.parents:
raise SystemExit('OpenClaw runtime must be outside the task repository')
PY
# One sandbox can receive overlapping session setup attempts. Keep the ready
# check and all cache writes under a version-scoped advisory lock.
if ! command -v flock >/dev/null 2>&1; then
install_packages util-linux
fi
mkdir -p "$runtime"
exec 9>"$runtime/.install.lock"
flock -x 9
mkdir -p "$runtime/home" "$runtime/cache"
export HOME="$runtime/home"
export npm_config_cache="$runtime/cache/npm"
export XDG_CACHE_HOME="$runtime/cache"
verify_version() {
"$runtime/node/bin/node" -e \
'const p=require(process.argv[1]); if(p.version!==process.argv[2])throw Error("OpenClaw version mismatch")' \
"$runtime/openclaw/node_modules/openclaw/package.json" "$openclaw_version"
}
if [ -f "$runtime/ready" ]; then
verify_version
"$runtime/node/bin/node" "$runtime/openclaw/node_modules/openclaw/openclaw.mjs" --version
exit 0
fi

missing=()
for prerequisite in curl tar gzip sha256sum awk; do
command -v "$prerequisite" >/dev/null 2>&1 || missing+=("$prerequisite")
done
if [ ! -s /etc/ssl/certs/ca-certificates.crt ] && [ ! -s /etc/pki/tls/certs/ca-bundle.crt ]; then
missing+=(ca-certificates)
fi
if [ "${#missing[@]}" -gt 0 ]; then
echo "OpenClaw sandbox execution prerequisites missing: ${missing[*]}. Preinstall curl, ca-certificates, tar, gzip, coreutils, gawk." >&2
install_packages curl ca-certificates tar gzip coreutils gawk
fi
if [ "$platform" = linux-x64-musl ] && ! python3 -c 'import ctypes; ctypes.CDLL("libstdc++.so.6")' 2>/dev/null; then
install_packages libstdc++
fi
cd "$runtime"
archive="node-v${node_version}-${platform}.tar.gz"
curl -fsSL --retry 3 "${node_dist}/${archive}" -o "$archive"
curl -fsSL --retry 3 "${node_dist}/SHASUMS256.txt" -o SHASUMS256.txt
awk -v archive="$archive" '$2 == archive' SHASUMS256.txt | sha256sum -c -
mkdir -p node
tar -xzf "$archive" --strip-components=1 -C node
if [ "$platform" = linux-x64-musl ] && ! "$runtime/node/bin/node" --version; then
# Confine the newer C++ runtime to our Node binary, preserving task libraries.
command -v patchelf >/dev/null 2>&1 || install_packages patchelf
curl -fsSL --retry 3 \
'https://dl-cdn.alpinelinux.org/alpine/v3.19/main/x86_64/libstdc++-13.2.1_git20231014-r0.apk' \
-o libstdc++.apk
echo '3cf66a7164240ef590106496d3c75f486bac46cba9cf2198c0c3b318c53ad027 libstdc++.apk' | sha256sum -c -
mkdir -p libstdcpp
tar -xzf libstdc++.apk -C libstdcpp usr/lib
patchelf --set-rpath '$ORIGIN/../../libstdcpp/usr/lib' "$runtime/node/bin/node"
fi
"$runtime/node/bin/node" --version
PATH="$runtime/node/bin:$PATH" "$runtime/node/bin/node" \
"$runtime/node/lib/node_modules/npm/bin/npm-cli.js" install \
--prefix "$runtime/openclaw" --no-audit --no-fund "openclaw@${openclaw_version}"
"$runtime/node/bin/node" -e \
'const p=require(process.argv[1]); if(p.version!==process.argv[2])throw Error("OpenClaw version mismatch")' \
"$runtime/openclaw/node_modules/openclaw/package.json" "$openclaw_version"
"$runtime/node/bin/node" "$runtime/openclaw/node_modules/openclaw/openclaw.mjs" --version
printf '%s\n' "$openclaw_version" > "$runtime/ready"
132 changes: 132 additions & 0 deletions responses_api_agents/openclaw_agent/sandbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""OpenClaw artifacts and request state over the shared sandbox lifecycle."""

import asyncio
import json
import logging
from dataclasses import dataclass

from pydantic import BaseModel, ConfigDict, JsonValue, ValidationError

from nemo_gym.agent_utils.sandbox_session import SandboxCommand, SandboxSession
from nemo_gym.base_responses_api_agent import AgentSessionState
from nemo_gym.openai_utils import NeMoGymResponse, NeMoGymResponseCreateParamsNonStreaming
from nemo_gym.rollout_observability import AgentObservationBundle
from nemo_gym.sandbox.utils import read_text, upload_text


LOG = logging.getLogger(__name__)


# Executed by sandbox Python before uploading any adapter-owned files. Resolve
# symlinks and '..' on the sandbox filesystem, not on the agent-server host.
_SANDBOX_PATH_CHECK = """
from pathlib import Path
import sys
workdir, session_root, runtime_root = (Path(value).resolve() for value in sys.argv[1:])
if not workdir.is_dir():
raise SystemExit("OpenClaw task workdir is missing or is not a directory: " + str(workdir))
for owned in (session_root, runtime_root):
if workdir == owned or workdir in owned.parents or owned in workdir.parents:
raise SystemExit("OpenClaw task workdir overlaps adapter-owned storage: " + str(owned))
"""


class HarnessProcessInfo(BaseModel):
Comment thread
yaoyu-33 marked this conversation as resolved.
Resolved
"""Optional OpenClaw shim identity, independent of supervisor cleanup."""

model_config = ConfigDict(extra="forbid", strict=True)
hostname: str
pid: int
python: str | None = None


def parse_runtime_info(payload: object) -> HarnessProcessInfo | None:
"""Read OpenClaw diagnostics without failing an otherwise valid episode."""
try:
return HarnessProcessInfo.model_validate(payload)
except ValidationError:
LOG.warning("OpenClaw runtime metadata is missing or malformed")
return None


@dataclass
class OpenClawSandboxSession(AgentSessionState):
"""OpenClaw request/output state; SandboxSession owns execution and teardown."""

session: SandboxSession[dict[str, str]]
runtime: str
task: asyncio.Task[NeMoGymResponse] | None = None
runtime_info: HarnessProcessInfo | None = None
observations: AgentObservationBundle | None = None
activation_request: NeMoGymResponseCreateParamsNonStreaming | None = None

async def upload_json(self, name: str, payload: JsonValue) -> None:
"""Stage adapter input using the shared text-transfer utility."""
await upload_text(self.session.sandbox, path=f"{self.session.session_dir}/{name}", text=json.dumps(payload))

async def read_text(self, name: str) -> str:
"""Read adapter output using file transfer."""
return await read_text(self.session.sandbox, path=f"{self.session.session_dir}/{name}")

async def close(self, timeout: float) -> None:
"""Capture and release before cancelling the HTTP activation."""
await self.session.close(timeout=timeout)
if self.task is not None and not self.task.done() and not self.task.cancelling():
self.task.cancel()
if self.task is not None:
try:
await asyncio.wait_for(asyncio.shield(self.task), timeout=timeout)
except asyncio.CancelledError:
if not self.task.cancelled():
raise
except Exception:
if not self.task.done():
raise
# The agent base replays the activation error independently of close.

async def execute(self, payload: dict[str, JsonValue], *, timeout: float, close_timeout: float) -> dict[str, str]:
"""Run through the common lifecycle; the adapter validates terminal events."""
artifacts = await self.session.execute(
stage_activation=lambda: self.stage_activation(payload),
collect=self.collect_artifacts,
timeout=timeout,
close_timeout=close_timeout,
)
if self.session.closing:
raise asyncio.CancelledError
return artifacts

async def stage_activation(self, payload: dict[str, JsonValue]) -> SandboxCommand:
"""Stage the invocation and return the OpenClaw worker command."""
await self.upload_json("home/.openclaw/openclaw.json", payload["config"])
await self.upload_json("input.json", payload)
return SandboxCommand(
python="python3",
argv=[
"python3",
"-I",
f"{self.session.session_dir}/sandbox_runner.py",
f"{self.session.session_dir}/input.json",
],
)

async def collect_artifacts(self) -> dict[str, str]:
"""Retain transcript and diagnostics before releasing the task sandbox."""
artifacts = {}
session_id = self.session.session_dir.rsplit("/", 1)[-1]
for name in ("stdout.log", "stderr.log", f"home/.openclaw/agents/main/sessions/{session_id}.jsonl"):
try:
artifacts[name] = await self.read_text(name)
except Exception:
LOG.warning("OpenClaw artifact unavailable: %s", name, exc_info=True)
try:
runtime = json.loads(await self.read_text("runtime.json"))
except Exception:
runtime = None
self.runtime_info = parse_runtime_info(runtime)
if "stdout.log" not in artifacts and not any(name.endswith(".jsonl") for name in artifacts):
logs = await self.session.read_output_log()
raise RuntimeError(f"OpenClaw sandbox runner returned no valid result: {logs}")
return artifacts
68 changes: 68 additions & 0 deletions responses_api_agents/openclaw_agent/sandbox_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Capture OpenClaw output; the shared process supervisor owns deadlines and cleanup."""

from __future__ import annotations

import json
import os
import signal
import subprocess
import sys
from contextlib import ExitStack
from pathlib import Path
from typing import TypedDict


class RunnerInput(TypedDict):
"""Harness input, independent of supervisor cleanup and runtime metadata."""

directory: str
prompt: str
command: list[str]
cwd: str
env: dict[str, str]


def run(params: RunnerInput) -> int:
"""Run the harness with isolated files and let the supervisor reap descendants."""
directory = Path(params["directory"])
(directory / "runtime.json").write_text(json.dumps({"hostname": os.uname().nodename, "pid": os.getpid()}))
(directory / "prompt.txt").write_text(params["prompt"])
stopping = False

def interrupt(*_: object) -> None:
nonlocal stopping
# Defer TERM until Popen returns so a signal cannot lose the child handle.
stopping = True

signal.signal(signal.SIGTERM, interrupt)
with ExitStack() as stack:
stdout = stack.enter_context((directory / "stdout.log").open("wb"))
stderr = stack.enter_context((directory / "stderr.log").open("wb"))
stdin = subprocess.DEVNULL
process = subprocess.Popen(
params["command"],
cwd=params["cwd"],
env={**os.environ, **params["env"]},
stdin=stdin,
stdout=stdout,
stderr=stderr,
)
while True:
if stopping:
process.terminate()
stopping = False
try:
return process.wait(timeout=0.05)
except subprocess.TimeoutExpired:
pass


def main() -> None:
params = json.loads(Path(sys.argv[1]).read_text())
raise SystemExit(run(params))


if __name__ == "__main__":
main()
Loading
Loading