Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
5 changes: 3 additions & 2 deletions agents/langchain-deepagents-code/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
strings "$binary" | grep -Fq '/usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh'

# hadolint ignore=DL3006
FROM ${BASE_IMAGE}

Check warning on line 68 in agents/langchain-deepagents-code/Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (Deep Agents Code)

Default value for global ARG results in an empty or invalid base image name

InvalidDefaultArgInFrom: Default value for ARG ${BASE_IMAGE} results in empty or invalid base image name More info: https://docs.docker.com/go/dockerfile/rule/invalid-default-arg-in-from/

# The supplied base may end as a non-root runtime user. Reset the build user
# explicitly before installing the root-owned managed-startup handoff.
Expand Down Expand Up @@ -289,10 +289,11 @@
&& printf '%s\n' "$NEMOCLAW_PROXY_HOST" > /usr/local/share/nemoclaw/dcode-proxy-host \
&& printf '%s\n' "$NEMOCLAW_PROXY_PORT" > /usr/local/share/nemoclaw/dcode-proxy-port \
&& printf '%s\n' "$NEMOCLAW_INFERENCE_BASE_URL" > /usr/local/share/nemoclaw/dcode-inference-base-url \
&& printf '%s\n' "$NEMOCLAW_UPSTREAM_PROVIDER" > /usr/local/share/nemoclaw/dcode-upstream-provider \
&& printf '%s\n' "$NEMOCLAW_DCODE_AUTO_APPROVAL" > /usr/local/share/nemoclaw/dcode-auto-approval \
&& printf '%s\n' "$NEMOCLAW_REASONING_EFFORT" > /usr/local/share/nemoclaw/dcode-reasoning-effort \
&& chown root:root /usr/local/share/nemoclaw/dcode-proxy-host /usr/local/share/nemoclaw/dcode-proxy-port /usr/local/share/nemoclaw/dcode-inference-base-url /usr/local/share/nemoclaw/dcode-auto-approval /usr/local/share/nemoclaw/dcode-reasoning-effort \
&& chmod 0444 /usr/local/share/nemoclaw/dcode-proxy-host /usr/local/share/nemoclaw/dcode-proxy-port /usr/local/share/nemoclaw/dcode-inference-base-url /usr/local/share/nemoclaw/dcode-auto-approval /usr/local/share/nemoclaw/dcode-reasoning-effort \
&& chown root:root /usr/local/share/nemoclaw/dcode-proxy-host /usr/local/share/nemoclaw/dcode-proxy-port /usr/local/share/nemoclaw/dcode-inference-base-url /usr/local/share/nemoclaw/dcode-upstream-provider /usr/local/share/nemoclaw/dcode-auto-approval /usr/local/share/nemoclaw/dcode-reasoning-effort \
&& chmod 0444 /usr/local/share/nemoclaw/dcode-proxy-host /usr/local/share/nemoclaw/dcode-proxy-port /usr/local/share/nemoclaw/dcode-inference-base-url /usr/local/share/nemoclaw/dcode-upstream-provider /usr/local/share/nemoclaw/dcode-auto-approval /usr/local/share/nemoclaw/dcode-reasoning-effort \
&& unset OTEL_EXPORTER_OTLP_TRACES_ENDPOINT \
&& empty_prompt_log="$(mktemp)" \
&& if timeout 10 env -i /usr/local/lib/nemoclaw/dcode-wrapper.sh -n "" >"$empty_prompt_log" 2>&1; then empty_prompt_status=0; else empty_prompt_status=$?; fi \
Expand All @@ -310,7 +311,7 @@
&& env -i /usr/local/bin/dcode.real --version \
&& env -i /usr/local/bin/deepagents-code --version

ENV HOME=/sandbox \

Check warning on line 314 in agents/langchain-deepagents-code/Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (Deep Agents Code)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "DEEPAGENTS_CODE_OPENAI_API_KEY") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
VIRTUAL_ENV=/opt/venv \
PATH="/usr/local/bin:/opt/venv/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin" \
NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \
Expand Down
30 changes: 22 additions & 8 deletions agents/langchain-deepagents-code/managed-dcode-runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
_INFERENCE_BASE_URL_FILE = Path(
"/usr/local/share/nemoclaw/dcode-inference-base-url"
)
_UPSTREAM_PROVIDER_FILE = Path(
"/usr/local/share/nemoclaw/dcode-upstream-provider"
)
_MANAGED_PROXY_HOST_FILE = Path(
"/usr/local/share/nemoclaw/dcode-proxy-host"
)
Expand Down Expand Up @@ -1115,21 +1118,21 @@ def managed_fetch_proxy_url() -> str | None:
return value


def _read_managed_proxy_value(path: Path, label: str) -> str:
"""Read one immutable proxy component from the managed image."""
def _read_managed_file_value(path: Path, label: str) -> str:
"""Read one root-owned, read-only value from the managed image."""
if not path.is_file() or path.is_symlink():
raise RuntimeError(f"managed proxy {label} file is missing or unsafe")
raise RuntimeError(f"managed {label} file is missing or unsafe")
try:
metadata = path.stat()
raw = path.read_text(encoding="utf-8")
except OSError as exc:
raise RuntimeError(f"managed proxy {label} file is unreadable") from exc
raise RuntimeError(f"managed {label} file is unreadable") from exc
if (
metadata.st_uid != _MANAGED_FILE_OWNER_UID
or stat.S_IMODE(metadata.st_mode) != 0o444
):
raise RuntimeError(
f"managed proxy {label} file has unsafe ownership or mode"
f"managed {label} file has unsafe ownership or mode"
)
value = raw.rstrip("\n")
if (
Expand All @@ -1139,14 +1142,14 @@ def _read_managed_proxy_value(path: Path, label: str) -> str:
or value != value.strip()
or any(ord(character) < 32 for character in value)
):
raise RuntimeError(f"managed proxy {label} file has invalid contents")
raise RuntimeError(f"managed {label} file has invalid contents")
return value


def _managed_fetch_proxy_url_from_files() -> str:
"""Derive the trusted proxy URL independently from root-owned files."""
host = _read_managed_proxy_value(_MANAGED_PROXY_HOST_FILE, "host")
port = _read_managed_proxy_value(_MANAGED_PROXY_PORT_FILE, "port")
host = _read_managed_file_value(_MANAGED_PROXY_HOST_FILE, "proxy host")
port = _read_managed_file_value(_MANAGED_PROXY_PORT_FILE, "proxy port")
if _MANAGED_PROXY_HOST.fullmatch(host) is None:
raise RuntimeError("managed proxy host file has invalid contents")
if (
Expand All @@ -1157,6 +1160,16 @@ def _managed_fetch_proxy_url_from_files() -> str:
return f"http://{host}:{port}"


def _managed_upstream_provider() -> str:
"""Return the root-owned upstream provider."""
value = _read_managed_file_value(
_UPSTREAM_PROVIDER_FILE, "upstream provider"
)
if _DISPLAY_PROVIDER_NAME.fullmatch(value) is None:
raise RuntimeError("managed upstream provider file has invalid contents")
return value


def _managed_fetch_ca_bundle() -> tuple[int, str]:
"""Open and validate fixed OpenShell TLS trust without a pathname race."""
path = _MANAGED_FETCH_CA_BUNDLE_FILE
Expand Down Expand Up @@ -1472,6 +1485,7 @@ def assert_safe_runtime() -> None:
"""Reject unmanaged runtime credentials before dcode bootstraps settings."""
_assert_safe_environment()
_assert_safe_auth_state()
os.environ[_UPSTREAM_PROVIDER_ENV] = _managed_upstream_provider()
managed_fetch_proxy_url()
base_url = managed_inference_base_url()
os.environ["OPENAI_BASE_URL"] = base_url
Expand Down
9 changes: 9 additions & 0 deletions src/lib/onboard/dockerfile-patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
type DcodeAutoApprovalMode,
isDcodeAutoApprovalMode,
} from "./dcode-auto-approval";
import { isValidDcodeUpstreamProvider } from "./managed-startup/dcode-upstream-provider";
import * as remoteDashboardBindContract from "./dockerfile-remote-dashboard-bind-contract";
import {
type DockerfileInstruction,
Expand Down Expand Up @@ -322,6 +323,14 @@ export function patchStagedDockerfile(
// etc.) rather than the proxy-routing key. The replace is a silent no-op
// when the staged Dockerfile predates this ARG (e.g. OpenClaw).
const upstreamProvider = provider && provider.trim() ? provider : providerKey;
if (
options.agentName === "langchain-deepagents-code" &&
!isValidDcodeUpstreamProvider(upstreamProvider)
) {
throw new Error(
"NEMOCLAW_UPSTREAM_PROVIDER must start with an ASCII letter or digit and contain 1-64 ASCII letters, digits, dots, underscores, or hyphens for DCode.",
);
}
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_UPSTREAM_PROVIDER=.*$/m,
`ARG NEMOCLAW_UPSTREAM_PROVIDER=${sanitizeDockerArg(upstreamProvider)}`,
Expand Down
123 changes: 82 additions & 41 deletions src/lib/onboard/managed-startup-agent-environment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,25 +400,26 @@ describe("managed startup agent environment", () => {
).toThrow(message);
});

it.each(
MANAGED_STARTUP_AGENTS,
)("derives every unsupported $0 runtime unset from the closed contract", (agent) => {
const result = mapManagedStartupProfileToAgentEnvironment(PROFILES[agent](), {
NEMOCLAW_AUTO_PAIR_DEADLINE_SECS: "30",
NEMOCLAW_AUTO_PAIR_FAST_DEADLINE_SECS: "3",
NEMOCLAW_AUTO_PAIR_FAST_REENTRY_INTERVAL_SECS: "0.25",
NEMOCLAW_AUTO_PAIR_FAST_REENTRY_POLLS: "3",
NEMOCLAW_AUTO_PAIR_RUN_TIMEOUT_SECS: "10",
NEMOCLAW_AUTO_PAIR_SLOW_INTERVAL_SECS: "600",
});
const unsets = new Set(result.applicationRuntime.unsetEnvironment);
for (const obligation of MANAGED_STARTUP_RUNTIME_CLEANUP_OBLIGATIONS) {
expect(unsets.has(obligation.input)).toBe(!obligation.supportedFor.includes(agent));
}
for (const name of OPENCLAW_APPLICATION_RUNTIME_NAMES) {
expect(unsets.has(name)).toBe(agent !== "openclaw");
}
});
it.each(MANAGED_STARTUP_AGENTS)(
"derives every unsupported $0 runtime unset from the closed contract",
(agent) => {
const result = mapManagedStartupProfileToAgentEnvironment(PROFILES[agent](), {
NEMOCLAW_AUTO_PAIR_DEADLINE_SECS: "30",
NEMOCLAW_AUTO_PAIR_FAST_DEADLINE_SECS: "3",
NEMOCLAW_AUTO_PAIR_FAST_REENTRY_INTERVAL_SECS: "0.25",
NEMOCLAW_AUTO_PAIR_FAST_REENTRY_POLLS: "3",
NEMOCLAW_AUTO_PAIR_RUN_TIMEOUT_SECS: "10",
NEMOCLAW_AUTO_PAIR_SLOW_INTERVAL_SECS: "600",
});
const unsets = new Set(result.applicationRuntime.unsetEnvironment);
for (const obligation of MANAGED_STARTUP_RUNTIME_CLEANUP_OBLIGATIONS) {
expect(unsets.has(obligation.input)).toBe(!obligation.supportedFor.includes(agent));
}
for (const name of OPENCLAW_APPLICATION_RUNTIME_NAMES) {
expect(unsets.has(name)).toBe(agent !== "openclaw");
}
},
);

it("keeps the profile mapper independent from mutable process-global runtime input", () => {
const name = "NEMOCLAW_AUTO_PAIR_FAST_REENTRY_POLLS";
Expand Down Expand Up @@ -514,7 +515,7 @@ describe("managed startup agent environment", () => {
});
});

it("keeps DCode routing and auto-approval in root-owned files instead of ambient runtime env", () => {
it("keeps DCode routing, provider identity, and auto-approval in root-owned files", () => {
const result = mapManagedStartupProfileToAgentEnvironment(dcodeProfile(), {
NEMOCLAW_AUTO_PAIR_FAST_REENTRY_INTERVAL_SECS: "not-a-number",
});
Expand All @@ -538,6 +539,7 @@ describe("managed startup agent environment", () => {
const expectedDcodeRuntime = { ...result.configurationEnvironment };
delete expectedDcodeRuntime.NEMOCLAW_INFERENCE_BASE_URL;
delete expectedDcodeRuntime.NEMOCLAW_REASONING_EFFORT;
delete expectedDcodeRuntime.NEMOCLAW_UPSTREAM_PROVIDER;
for (const name of [
"HTTP_PROXY",
"HTTPS_PROXY",
Expand Down Expand Up @@ -566,6 +568,7 @@ describe("managed startup agent environment", () => {
expect(result.runtimeEnvironment).not.toHaveProperty("HTTPS_PROXY");
expect(result.runtimeEnvironment).not.toHaveProperty("NEMOCLAW_INFERENCE_BASE_URL");
expect(result.runtimeEnvironment).not.toHaveProperty("NEMOCLAW_REASONING_EFFORT");
expect(result.runtimeEnvironment).not.toHaveProperty("NEMOCLAW_UPSTREAM_PROVIDER");

expect(result.materials).toEqual([
{
Expand All @@ -591,6 +594,15 @@ describe("managed startup agent environment", () => {
group: "root",
mode: 0o444,
},
{
kind: "root-owned-file",
legacyInput: "NEMOCLAW_UPSTREAM_PROVIDER",
path: "/usr/local/share/nemoclaw/dcode-upstream-provider",
contents: "openrouter\n",
owner: "root",
group: "root",
mode: 0o444,
},
{
kind: "root-owned-file",
legacyInput: "NEMOCLAW_PROXY_HOST",
Expand Down Expand Up @@ -678,29 +690,43 @@ describe("managed startup agent environment", () => {
});
});

it.each(
MANAGED_STARTUP_AGENTS,
)("represents the complete $0 Docker/start affordance inventory", (agent) => {
const result = mapManagedStartupProfileToAgentEnvironment(PROFILES[agent]());
expect(representedLegacyInputs(result)).toEqual(
MANAGED_STARTUP_PROFILE_AFFORDANCE_INVENTORY[agent]
.map((affordance) => affordance.input)
.sort(),
);
const messagingActions = result.actions.filter(
(action) => action.kind === "apply-messaging-plan",
);
expect(messagingActions.map(({ phase, runAs }) => [phase, runAs])).toEqual(
agent === "langchain-deepagents-code"
? []
: [
["runtime-setup", "root"],
["post-agent-install", "sandbox"],
],
);
expect(messagingActions.map((action) => String(action.phase))).not.toContain("agent-install");
it("materializes the longest DCode upstream provider accepted by its runtime (#7112)", () => {
const profile = dcodeProfile();
const upstreamProvider = "a".repeat(64);
const result = mapManagedStartupProfileToAgentEnvironment({
...profile,
inference: { ...profile.inference, upstreamProvider },
});

expect(
result.materials.find((material) => material.legacyInput === "NEMOCLAW_UPSTREAM_PROVIDER"),
).toMatchObject({ contents: `${upstreamProvider}\n` });
});

it.each(MANAGED_STARTUP_AGENTS)(
"represents the complete $0 Docker/start affordance inventory",
(agent) => {
const result = mapManagedStartupProfileToAgentEnvironment(PROFILES[agent]());
expect(representedLegacyInputs(result)).toEqual(
MANAGED_STARTUP_PROFILE_AFFORDANCE_INVENTORY[agent]
.map((affordance) => affordance.input)
.sort(),
);
const messagingActions = result.actions.filter(
(action) => action.kind === "apply-messaging-plan",
);
expect(messagingActions.map(({ phase, runAs }) => [phase, runAs])).toEqual(
agent === "langchain-deepagents-code"
? []
: [
["runtime-setup", "root"],
["post-agent-install", "sandbox"],
],
);
expect(messagingActions.map((action) => String(action.phase))).not.toContain("agent-install");
},
);

it("uses explicit clear states without erasing launch-only ambient proxy credentials", () => {
const openclawBase = openClawProfile();
assert(openclawBase.agentConfig.agent === "openclaw", "fixture mismatch");
Expand Down Expand Up @@ -871,6 +897,21 @@ describe("managed startup agent environment", () => {
);
});

it.each(["provider-π", `p${"x".repeat(64)}`, "-ollama-local"])(
"rejects unsupported DCode provider identifier %s before materialization (#7112)",
(upstreamProvider) => {
const base = dcodeProfile();
const profile: ManagedStartupProfile = {
...base,
inference: { ...base.inference, upstreamProvider },
};

expect(() => mapManagedStartupProfileToAgentEnvironment(profile)).toThrow(
/must start with an ASCII letter or digit and contain 1-64 ASCII letters, digits, dots, underscores, or hyphens for DCode/u,
);
},
);

it("revalidates typed input while keeping DCode host proxy intent outside its pinned runtime", () => {
const dcodeBase = dcodeProfile();
const profile: ManagedStartupProfile = {
Expand Down
18 changes: 12 additions & 6 deletions src/lib/onboard/managed-startup/agent-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@ export interface ManagedStartupRootOwnedFileMaterial {
| "NEMOCLAW_INFERENCE_BASE_URL"
| "NEMOCLAW_PROXY_HOST"
| "NEMOCLAW_PROXY_PORT"
| "NEMOCLAW_REASONING_EFFORT";
| "NEMOCLAW_REASONING_EFFORT"
| "NEMOCLAW_UPSTREAM_PROVIDER";
readonly path:
| "/usr/local/share/nemoclaw/dcode-auto-approval"
| "/usr/local/share/nemoclaw/dcode-inference-base-url"
| "/usr/local/share/nemoclaw/dcode-proxy-host"
| "/usr/local/share/nemoclaw/dcode-proxy-port"
| "/usr/local/share/nemoclaw/dcode-reasoning-effort";
| "/usr/local/share/nemoclaw/dcode-reasoning-effort"
| "/usr/local/share/nemoclaw/dcode-upstream-provider";
readonly contents: string;
readonly owner: "root";
readonly group: "root";
Expand Down Expand Up @@ -76,15 +78,13 @@ interface ManagedStartupApplyMessagingActionBase {
readonly phase: "runtime-setup" | "post-agent-install";
}

export interface ManagedStartupApplyMessagingRuntimeAction
extends ManagedStartupApplyMessagingActionBase {
export interface ManagedStartupApplyMessagingRuntimeAction extends ManagedStartupApplyMessagingActionBase {
readonly phase: "runtime-setup";
/** Writes the reduced, root-owned messaging runtime-plan artifact. */
readonly runAs: "root";
}

export interface ManagedStartupApplyMessagingConfigAction
extends ManagedStartupApplyMessagingActionBase {
export interface ManagedStartupApplyMessagingConfigAction extends ManagedStartupApplyMessagingActionBase {
readonly phase: "post-agent-install";
/** Renders only sandbox-owned agent configuration from preinstalled assets. */
readonly runAs: "sandbox";
Expand Down Expand Up @@ -495,6 +495,7 @@ function mapDcodeProfile(
// consumed by managed-dcode-runtime.py.
delete runtimeEnvironment.NEMOCLAW_INFERENCE_BASE_URL;
delete runtimeEnvironment.NEMOCLAW_REASONING_EFFORT;
delete runtimeEnvironment.NEMOCLAW_UPSTREAM_PROVIDER;
for (const name of [
"HTTP_PROXY",
"HTTPS_PROXY",
Expand All @@ -517,6 +518,11 @@ function mapDcodeProfile(
"/usr/local/share/nemoclaw/dcode-inference-base-url",
profile.inference.routedBaseUrl,
),
rootOwnedFile(
"NEMOCLAW_UPSTREAM_PROVIDER",
"/usr/local/share/nemoclaw/dcode-upstream-provider",
profile.inference.upstreamProvider,
),
rootOwnedFile(
"NEMOCLAW_PROXY_HOST",
"/usr/local/share/nemoclaw/dcode-proxy-host",
Expand Down
8 changes: 8 additions & 0 deletions src/lib/onboard/managed-startup/dcode-upstream-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

const DCODE_UPSTREAM_PROVIDER_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;

export function isValidDcodeUpstreamProvider(value: string): boolean {
return DCODE_UPSTREAM_PROVIDER_RE.test(value);
}
Loading
Loading