Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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