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
89 changes: 87 additions & 2 deletions agents/langchain-deepagents-code/managed-dcode-runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@
_MCP_SEALED_KIND = "sealed-memfd"
_MCP_ANONYMOUS_KIND = "anonymous-otmpfile"
_MCP_ANONYMOUS_DIRECTORY = Path("/tmp")
_MCP_PRIVATE_ANONYMOUS_DIRECTORY = Path("/run/nemoclaw-dcode-mcp")
_MCP_PRIVATE_ANONYMOUS_MAX_BYTES = 1_048_576
_MCP_PRIVATE_ANONYMOUS_MODE = 0o1777
_MCP_PRIVATE_ANONYMOUS_MOUNT_OPTIONS = frozenset(
{"rw", "noexec", "nosuid", "nodev"}
)
_MCP_FALLBACK_ERRNOS = {
errno.EACCES,
errno.EINVAL,
Expand Down Expand Up @@ -838,13 +844,16 @@ def _sealed_managed_mcp_snapshot(payload: bytes) -> int:
raise


def _anonymous_managed_mcp_snapshot(payload: bytes) -> int:
def _anonymous_managed_mcp_snapshot_at(
payload: bytes,
directory: Path,
) -> int:
writer: int | None = None
reader: int | None = None
complete = False
try:
flags = os.O_TMPFILE | os.O_EXCL | os.O_RDWR | os.O_CLOEXEC
writer = os.open(_MCP_ANONYMOUS_DIRECTORY, flags, 0o600)
writer = os.open(directory, flags, 0o600)
remaining = memoryview(payload)
while remaining:
written = os.write(writer, remaining)
Expand Down Expand Up @@ -894,6 +903,82 @@ def _anonymous_managed_mcp_snapshot(payload: bytes) -> int:
pass


def _validate_private_managed_mcp_tmpfs() -> None:
directory = _MCP_PRIVATE_ANONYMOUS_DIRECTORY
try:
metadata = os.lstat(directory)
filesystem = os.statvfs(directory)
mount_lines = Path("/proc/self/mountinfo").read_text(
encoding="utf-8"
).splitlines()
except (OSError, UnicodeError) as exc:
raise RuntimeError(
"managed MCP config requires a private bounded tmpfs"
) from exc

matching_mount_options: list[set[str] | None] = []
for line in mount_lines:
fields = line.split()
if len(fields) <= 4 or fields[4] != str(directory):
continue
try:
separator = fields.index("-")
except ValueError:
matching_mount_options.append(None)
continue
if len(fields) <= separator + 3 or fields[separator + 1] != "tmpfs":
matching_mount_options.append(None)
continue
matching_mount_options.append(set(fields[5].split(",")))

mount_options = (
matching_mount_options[0]
if len(matching_mount_options) == 1
else None
)

total_bytes = filesystem.f_blocks * filesystem.f_frsize
if (
not stat.S_ISDIR(metadata.st_mode)
or stat.S_IMODE(metadata.st_mode) != _MCP_PRIVATE_ANONYMOUS_MODE
or not os.path.ismount(directory)
or mount_options is None
or not _MCP_PRIVATE_ANONYMOUS_MOUNT_OPTIONS.issubset(mount_options)
or total_bytes <= 0
or total_bytes > _MCP_PRIVATE_ANONYMOUS_MAX_BYTES
):
raise RuntimeError(
"managed MCP config requires a private bounded tmpfs"
)


def _managed_mcp_tmpfile_fallback_allowed(exc: BaseException) -> bool:
current: BaseException | None = exc
while current is not None:
if isinstance(current, AttributeError):
return True
if isinstance(current, OSError):
return current.errno == errno.EOPNOTSUPP
current = current.__cause__
return False


def _anonymous_managed_mcp_snapshot(payload: bytes) -> int:
try:
return _anonymous_managed_mcp_snapshot_at(
payload,
_MCP_ANONYMOUS_DIRECTORY,
)
except RuntimeError as exc:
if not _managed_mcp_tmpfile_fallback_allowed(exc):
raise
_validate_private_managed_mcp_tmpfs()
return _anonymous_managed_mcp_snapshot_at(
payload,
_MCP_PRIVATE_ANONYMOUS_DIRECTORY,
)


def _managed_mcp_fallback_allowed(exc: BaseException) -> bool:
current: BaseException | None = exc
while current is not None:
Expand Down
1 change: 1 addition & 0 deletions agents/langchain-deepagents-code/policy-additions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ filesystem_policy:
read_write:
- /sandbox
- /sandbox/.deepagents
- /run/nemoclaw-dcode-mcp
- /tmp
- /dev/null

Expand Down
57 changes: 39 additions & 18 deletions docs/security/openshell-0.0.85-migration-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,9 +358,10 @@ Commits: `ed0026aa`, `0a25fdf5`, `5477e2f2`, `914da339`, `450685c7`, `45614a3f`.
NemoClaw does not emit that field, but must parse its final Docker TOML and prove
the intended loopback and Docker-bridge reachability instead of relying on that
absence alone.
- `450685c7` rejects leading/trailing whitespace in mount fields. NemoClaw does
not configure production driver mounts; the test-only EXDEV tmpfs mount is the
downstream consumer and remains a required no-impact regression.
- `450685c7` rejects leading/trailing whitespace in mount fields.
NemoClaw does not configure production driver mounts for OpenClaw or Hermes.
LangChain Deep Agents Code supplies one bounded tmpfs mount for its managed MCP snapshot fallback.
The test-only EXDEV tmpfs mount remains a separate structured-tmpfs parsing regression.
- The Helm SAN, MCP documentation, Kubernetes combined-topology, and removed raw
`SandboxTemplate.volume_claim_templates` changes are not consumed by NemoClaw's
Docker gateway or CLI integration. NemoClaw has no raw OpenShell protobuf client
Expand All @@ -379,9 +380,9 @@ dependency migration.

Commits: `43bb0302`, `5f9bf9ce`, `6461677c`.

- `43bb0302` changes Docker and Podman bind mounts to support SELinux relabeling,
explicit source checks, and Docker's legacy bind representation. Production
NemoClaw supplies no driver mounts; the EXDEV fixture remains the direct test.
- `43bb0302` changes Docker and Podman bind mounts to support SELinux relabeling, explicit source checks, and Docker's legacy bind representation.
Production Deep Agents Code supplies a structured tmpfs mount, not a bind mount.
The EXDEV fixture remains a separate structured-tmpfs regression and does not exercise bind-mount relabeling.
- `6461677c` adds numeric UID/GID policy identities and configurable Kubernetes
and VM identities. NemoClaw's supported gateway configuration selects only the
Docker driver; that driver does not inject the new UID/GID environment or
Expand Down Expand Up @@ -649,7 +650,7 @@ Commits: `80293213`, `392ad639`, `b4be33e5`, `21aaa895`, `3dee5570`.
| `OS85-10` | Medium-high | Supervisor TLS identity variables are no longer child environment. Stale tests/comments can normalize a credential leak. | Assert absence from entrypoint, exec, and connect children and update the source-of-truth rationale. | Hermes and Deep Agents now reject all three variables; the stable entrypoint, exec, and connect probes require their absence, with execution for the PR SHA pending. |
| `OS85-11` | Medium-high | Live `/proc/<pid>/exe` identity changes replacement-time policy behavior. | Prove old process survives replacement and a new altered process at the same path is denied. | The stable release proof runs both processes against the real proxy and requires old=200 before/after replacement, distinct live/path hashes, and new=403; the runtime result for the PR SHA is pending. |
| `OS85-12` | Medium | OpenShell declares Docker 28.0+ while #6379 is on Docker 27 and NemoClaw marks DGX Spark tested. | Either validate and document a precise downstream exception from physical proof or raise the supported floor and preflight it. | Open product/platform decision. |
| `OS85-13` | Low | Mount parsing/SELinux changes could affect the test-only tmpfs path. | Rerun the EXDEV tmpfs fixture and retain production no-mount evidence. | The stable release proof injects only the reviewed tmpfs config, requires Docker's structured tmpfs representation plus `noexec`/01777 at runtime, requires an empty remount after graceful gateway restart with the same container/config/auth and retained durable state, and requires another fresh remount after rebuild. The wrapper is disabled outside the explicit proof lane and production still supplies no driver mounts. Results for the PR SHA, Podman, and enforcing-SELinux remain open. |
| `OS85-13` | Low | Mount parsing or SELinux changes could alter the production Deep Agents Code tmpfs path or the test-only EXDEV tmpfs path. | Require the exact production Deep Agents Code driver configuration and Landlock path. Verify the structured tmpfs type, mount point, options, mode, size, restart and rebuild lifecycle, anonymous-descriptor fallback, integrity binding, and empty mount residue. Rerun the separate EXDEV fixture. | The Deep Agents Code driver configuration declares one tmpfs at `/run/nemoclaw-dcode-mcp` in its Docker and Podman tables. The request sets a 1,048,576-byte limit, mode `01777`, and only the `noexec` option. The Docker/OpenShell path applies `nosuid` and `nodev` by default and rejects a driver request that supplies them explicitly. The Deep Agents Code policy grants Landlock read-write access to that exact path. The stable release proof consumes this production configuration and verifies Docker's structured tmpfs representation, observed `rw`, `noexec`, `nosuid`, and `nodev` options, mode, size, an empty remount after gateway restart, and a fresh remount after rebuild. Inside the real sandbox, the shipped runtime proof observes seccomp `EPERM` from sealed-memfd creation and injects `EOPNOTSUPP` only for its `O_TMPFILE` attempt in the configured `/tmp` anonymous directory. It then verifies that the shipped runtime validates and opens the actual production private tmpfs, preserves the anonymous unlinked read-only descriptor binding, and rejects same-size tampering. It reruns that platform contract after rebuild. The production mount must remain empty after each real Deep Agents Code tool call: initial, after bridge restart, after credential rotation, and after rebuild. Results for the PR SHA, Podman, and enforcing SELinux remain open. |
| `OS85-14` | Low | Sanitized MCP tool names are newly present in logs. | Record the additive observability/privacy behavior; ensure no downstream parser assumes the old shape. | The stable release check requires the real `fake_echo` tool name and rejects argument/result canaries or an `arguments` field in JSON-RPC policy logs; the runtime result for the PR SHA is pending. |
| `OS85-15` | High | The installer-hash workflow executes its checker and parser from the PR base SHA. One PR cannot safely teach that trusted base about a new release and consume the release; using the head checker would let reviewed code define its own trust rules. | First land archive safety, normalized full-script template validation, and multi-release trust while selectors remain `0.0.72`; prove the old base rejects a new release and the new base permits only structured release-data changes; then land the exact `0.0.85` manifest identities before refreshing this selector PR. | Base trust landed in #7069. #6778 and #6779 established base-owned structured manifest and sandbox-map validation; #7069 added only the three exact `0.0.85` release identities while retaining `0.0.72` and `0.0.82`. This selector PR must be based on that trusted state and pass the checker without relying on its head copy. |
| `OS85-16` | High | Capability clearing now depends on `capctl 0.2.4` and `bitflags 1.3.2`, but upstream notices are unchanged and the consumed binaries have no published SBOM or attestation covering this dependency graph. | Bind crate checksums and source identities to the stable lock and binaries; review the unsafe syscall boundary and advisories; update notices/licenses; retain a generated SBOM and provenance for every consumed binary. | The stable lock, crate checksums, source identities, licenses, unsafe boundary, current RustSec absence, and SLSA-bound archives are recorded. Upstream still publishes no binary SBOM and its unchanged notices omit the new graph; that limitation remains explicit rather than being presented as complete attribution. |
Expand Down Expand Up @@ -705,13 +706,20 @@ NemoClaw work.

## Stable release selected-driver and mount proof boundary

The same stable MCP job prepares a second bounded proof only
when `NEMOCLAW_OPENSHELL_EXACT_MAIN_PROOF=1`. A PATH wrapper delegates every
operation to the hash-pinned release CLI and changes only `openshell sandbox
create`: it adds one reviewed `--driver-config-json` value containing a tmpfs at
`/tmp/nemoclaw-exact-main-driver-config`. Duplicate driver config is rejected.
The helper is inactive outside that explicit lane, and NemoClaw's production
onboard path still supplies no driver mounts.
The same stable MCP job prepares a second bounded proof only when `NEMOCLAW_OPENSHELL_EXACT_MAIN_PROOF=1`.
A PATH wrapper delegates every operation to the hash-pinned release CLI without adding driver configuration.
The production Deep Agents Code onboard plan supplies one reviewed `--driver-config-json` value.
Its Docker and Podman tables each define a tmpfs at `/run/nemoclaw-dcode-mcp` with a 1,048,576-byte limit, mode `01777`, and only the `noexec` option.
The Docker/OpenShell path applies `nosuid` and `nodev` by default and rejects a driver request that supplies them explicitly.
OpenClaw and Hermes do not receive this mount.
The Deep Agents Code policy grants Landlock read-write access to the exact mount path.

The production Deep Agents Code runtime uses this tmpfs only as its final anonymous-snapshot location.
The #8018 live regression proof observes seccomp `EPERM` from sealed memfd creation.
It injects `EOPNOTSUPP` only for the shipped runtime's `O_TMPFILE` attempt in its configured `/tmp` anonymous directory.
Before the runtime opens an anonymous file in the private path, it requires exactly one matching mount entry for the exact mount point, the tmpfs filesystem, mode, size bound, and `rw`, `noexec`, `nosuid`, and `nodev` mount options.
The runtime keeps only an unlinked read-only descriptor.
It binds child reads to the file descriptor, device, inode, size, and SHA-256 digest.

The proof does not treat successful onboarding as evidence by itself. It:

Expand All @@ -731,10 +739,11 @@ The proof does not treat successful onboarding as evidence by itself. It:
requires stable CLI sandbox listing over host mTLS, and requires a real
sandbox exec through the supervisor relay. The container must mount its
sandbox JWT and all three client-mTLS files read-only.
4. Inspects the running Docker container. The test mount must be one structured
`Type=tmpfs` mount and must not appear in `HostConfig.Binds`, which is the
representation changed for SELinux-labelled bind mounts. Inside the sandbox,
`/proc/mounts` must report `tmpfs,noexec`, mode 01777, and a writable marker.
4. Inspects the running Docker container.
The production Deep Agents Code mount must use one structured `Type=tmpfs` mount.
It must not appear in `HostConfig.Binds`, which is the representation changed for SELinux-labelled bind mounts.
Inside the sandbox, `/proc/mounts` must report `tmpfs` with `rw`, `noexec`, `nosuid`, and `nodev`.
The mount must have mode `01777`, expose no more than 1,048,576 bytes, and accept a writable marker.
5. Stops and recovers the actual host OpenShell gateway through NemoClaw. A
graceful gateway shutdown stops the managed Docker sandbox, and startup
resumes that same container. The gateway PID must change, the rendered-config
Expand All @@ -746,6 +755,14 @@ The proof does not treat successful onboarding as evidence by itself. It:
representation/options and no volatile marker, and the backed-up Deep Agents
state marker must be restored. The new container identity plus the fresh tmpfs
mount prove that the driver config was reapplied during rebuild.
7. Executes the shipped Deep Agents Code runtime inside the real sandbox after onboarding and rebuild.
The proof requires seccomp to reject sealed-memfd creation with `EPERM`.
It injects `EOPNOTSUPP` only for the shipped runtime's `O_TMPFILE` attempt in its configured `/tmp` anonymous directory.
It then requires the shipped runtime to validate and open the actual production private tmpfs fallback.
The fallback must create an unlinked read-only descriptor whose binding reads the exact payload.
A same-size descriptor mutation must fail the SHA-256 integrity check.
Each platform-contract execution must leave the production mount empty.
The production mount must remain empty after each real Deep Agents Code tool call: initial, after bridge restart, after credential rotation, and after rebuild.

The proof is intentionally Linux amd64 Docker-bridge evidence. It does not
isolate Docker Desktop/Colima, WSL, DGX Spark's Docker 27 host-gateway route,
Expand All @@ -755,6 +772,10 @@ nor requests `selinux_label`, so it proves that the consumed tmpfs path remains
on the unaffected structured-mount branch, not that SELinux relabelling works.
Those platform claims need their own real hosts.

The live proof binds production mount delivery, fallback selection, descriptor integrity, and lifecycle evidence to the stable sandbox artifacts.
Focused source tests separately reject stacked mount entries, other invalid private tmpfs states, and unrelated memfd failures.
These tests keep fail-closed error classification covered without replacing the live platform result.

Legacy upgrade is also separate. This stable release lane starts with a fresh
gateway/config/database so every observed process can be tied to the release
provenance. The existing stable gateway-upgrade test starts an old gateway,
Expand Down
17 changes: 17 additions & 0 deletions src/lib/onboard/sandbox-create-plan-materialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@ import { prepareSandboxGpuRoutePolicies } from "./sandbox-gpu-route-policy";
type PrepareInitialSandboxCreatePolicy =
typeof import("./initial-policy").prepareInitialSandboxCreatePolicy;

const DCODE_MCP_SNAPSHOT_TMPFS_MOUNT = {
type: "tmpfs",
target: "/run/nemoclaw-dcode-mcp",
// Docker applies nosuid and nodev to tmpfs mounts by default and rejects
// both when they are repeated in structured MountTmpfsOptions.
options: ["noexec"],
size_bytes: 1_048_576,
mode: 0o1777,
} as const;
const DCODE_MCP_SNAPSHOT_TMPFS_CONFIG = JSON.stringify({
docker: { mounts: [DCODE_MCP_SNAPSHOT_TMPFS_MOUNT] },
podman: { mounts: [DCODE_MCP_SNAPSHOT_TMPFS_MOUNT] },
});

export type SandboxCreatePlan = {
activeMessagingChannels: string[];
initialSandboxPolicy: InitialSandboxPolicy;
Expand Down Expand Up @@ -154,6 +168,9 @@ export function materializeSandboxCreatePlan({
intent.sandboxName,
"--policy",
initialSandboxPolicy.policyPath,
...(intent.policy.options.agentName === "langchain-deepagents-code"
? ["--driver-config-json", DCODE_MCP_SNAPSHOT_TMPFS_CONFIG]
: []),
...intent.gpuCreateArgs,
...intent.resourceCreateArgs,
];
Expand Down
25 changes: 25 additions & 0 deletions src/lib/onboard/sandbox-create-plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,31 @@ describe("prepareSandboxCreatePlan", () => {
"sandbox",
"--policy",
"/tmp/policy.yaml",
"--driver-config-json",
JSON.stringify({
docker: {
mounts: [
{
type: "tmpfs",
target: "/run/nemoclaw-dcode-mcp",
options: ["noexec"],
size_bytes: 1_048_576,
mode: 0o1777,
},
],
},
podman: {
mounts: [
{
type: "tmpfs",
target: "/run/nemoclaw-dcode-mcp",
options: ["noexec"],
size_bytes: 1_048_576,
mode: 0o1777,
},
],
},
}),
"--gpu",
"--gpu-device",
"nvidia.com/gpu=0",
Expand Down
Loading
Loading