Skip to content
9 changes: 6 additions & 3 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/

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 @@ -180,8 +180,11 @@
test -x /usr/local/bin/dcode; \
/usr/local/bin/dcode --version

# Copy config generator, wrapper, startup script, and shared blueprint files.
COPY agents/langchain-deepagents-code/generate-config.ts /opt/nemoclaw-deepagents-code/generate-config.ts
# Copy the managed-startup entrypoint, config generator, its shared identity contract,
# wrapper, startup script, and shared blueprint files.
COPY agents/langchain-deepagents-code/generate-config-entrypoint.ts /opt/nemoclaw-deepagents-code/generate-config.ts
COPY agents/langchain-deepagents-code/generate-config.ts /opt/nemoclaw-deepagents-code/agents/langchain-deepagents-code/generate-config.ts
COPY src/lib/inference/managed-dcode/identity.ts /opt/nemoclaw-deepagents-code/src/lib/inference/managed-dcode/identity.ts
COPY agents/langchain-deepagents-code/managed-dcode-runtime.py /opt/nemoclaw-deepagents-code/managed-dcode-runtime.py
COPY agents/langchain-deepagents-code/patch-managed-deepagents-code.py /opt/nemoclaw-deepagents-code/patch-managed-deepagents-code.py
# SECURITY: copy only the two hash-verified plugin inputs, never the source directory.
Expand Down Expand Up @@ -219,7 +222,7 @@
&& test -f /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh \
&& test ! -L /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh \
&& test "$(stat -c '%u:%g:%a' /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh)" = '0:0:444' \
&& chmod 444 /opt/nemoclaw-deepagents-code/generate-config.ts /opt/nemoclaw-deepagents-code/managed-dcode-runtime.py /opt/nemoclaw-deepagents-code/patch-managed-deepagents-code.py /opt/nemoclaw-deepagents-code/validate-nemotron-ultra-profile.py /opt/nemoclaw-deepagents-code/progressive_tool_disclosure.py /opt/nemoclaw-deepagents-code/nemoclaw_observability.py /opt/nemoclaw-deepagents-code/validate-progressive-tool-disclosure.py /opt/nemoclaw-deepagents-code/validate-observability.py /usr/local/lib/nemoclaw/entrypoint-env-wrapper.sh /usr/local/lib/nemoclaw/dcode-login-profile.sh \
&& chmod 444 /opt/nemoclaw-deepagents-code/generate-config.ts /opt/nemoclaw-deepagents-code/agents/langchain-deepagents-code/generate-config.ts /opt/nemoclaw-deepagents-code/src/lib/inference/managed-dcode/identity.ts /opt/nemoclaw-deepagents-code/managed-dcode-runtime.py /opt/nemoclaw-deepagents-code/patch-managed-deepagents-code.py /opt/nemoclaw-deepagents-code/validate-nemotron-ultra-profile.py /opt/nemoclaw-deepagents-code/progressive_tool_disclosure.py /opt/nemoclaw-deepagents-code/nemoclaw_observability.py /opt/nemoclaw-deepagents-code/validate-progressive-tool-disclosure.py /opt/nemoclaw-deepagents-code/validate-observability.py /usr/local/lib/nemoclaw/entrypoint-env-wrapper.sh /usr/local/lib/nemoclaw/dcode-login-profile.sh \
&& chmod 755 /usr/local/bin/nemoclaw-start /usr/local/bin/nemoclaw-managed-startup-hold /usr/local/bin/nemoclaw-managed-bootstrap /usr/local/lib/nemoclaw/dcode-wrapper.sh /usr/local/lib/nemoclaw/dcode-launcher.sh /usr/local/lib/nemoclaw/dcode-session-supervisor.py \
&& test "$(stat -c '%u:%g:%a' /usr/local/lib/nemoclaw/dcode-session-supervisor.py)" = "0:0:755" \
&& install -o root -g root -m 0755 /usr/local/lib/nemoclaw/dcode-launcher.sh /usr/local/lib/nemoclaw/dcode-managed-exec \
Expand Down Expand Up @@ -312,7 +315,7 @@
&& env -i /usr/local/bin/dcode.real --version \
&& env -i /usr/local/bin/deepagents-code --version

ENV HOME=/sandbox \

Check warning on line 318 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/

Check warning on line 318 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

// Preserve the managed-startup path while the generator keeps its repository-relative imports.
import "/opt/nemoclaw-deepagents-code/agents/langchain-deepagents-code/generate-config.ts";
79 changes: 14 additions & 65 deletions agents/langchain-deepagents-code/generate-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";

import {
type ManagedDcodeProvider,
normalizeManagedDcodeEndpointUrl,
resolveManagedDcodeIdentity,
} from "../../src/lib/inference/managed-dcode/identity.ts";

type ReasoningEffort = "low" | "medium" | "high";

type Settings = {
Expand All @@ -22,11 +28,9 @@ type Settings = {
reasoningEffort: ReasoningEffort | null;
};

type ManagedDeepAgentsProvider = "openai" | "openrouter";

type ManagedDeepAgentsConfig = {
text: string;
provider: ManagedDeepAgentsProvider;
provider: ManagedDcodeProvider;
model: string;
defaultModel: string;
};
Expand All @@ -36,10 +40,6 @@ const NEMOTRON_ULTRA_MODEL_IDS = new Set([
"nvidia/nvidia/nemotron-3-ultra",
]);

const OPENROUTER_UPSTREAM_PROVIDERS = new Set(["openrouter", "openrouter-api"]);
const OPENROUTER_ENDPOINT_HOST = "openrouter.ai";
const OPENROUTER_ENDPOINT_PATH = "/api/v1";

function readSettings(env: NodeJS.ProcessEnv): Settings {
const providerKey = normalizeCommentMetadata(
env.NEMOCLAW_INFERENCE_PROVIDER_ID || env.NEMOCLAW_PROVIDER_KEY || "inference",
Expand All @@ -58,7 +58,7 @@ function readSettings(env: NodeJS.ProcessEnv): Settings {
"inference",
"NEMOCLAW_UPSTREAM_PROVIDER",
),
upstreamEndpointUrl: normalizeOptionalEndpointUrl(
upstreamEndpointUrl: normalizeManagedDcodeEndpointUrl(
env.NEMOCLAW_UPSTREAM_ENDPOINT_URL,
"NEMOCLAW_UPSTREAM_ENDPOINT_URL",
),
Expand Down Expand Up @@ -92,30 +92,6 @@ function normalizeCommentMetadata(value: string, name: string): string {
return value.trim();
}

function normalizeOptionalEndpointUrl(value: string | undefined, name: string): string | null {
if (value === undefined || value.trim() === "") return null;
if (/[\p{Cc}\p{Cf}]/u.test(value)) {
throw new Error(`${name} must not contain control characters.`);
}
const text = value.trim();
let url: URL;
try {
url = new URL(text);
} catch {
throw new Error(`${name} must be a valid URL.`);
}
if (url.protocol !== "http:" && url.protocol !== "https:") {
throw new Error(`${name} must use HTTP or HTTPS.`);
}
if (url.username || url.password) {
throw new Error(`${name} must not include credentials.`);
}
if (url.search || url.hash) {
throw new Error(`${name} must not include query strings or fragments.`);
}
return url.href;
}

function normalizeInferenceBaseUrl(value: string): string {
if (/[\r\n]/.test(value)) {
throw new Error("NEMOCLAW_INFERENCE_BASE_URL must not contain line breaks.");
Expand Down Expand Up @@ -149,35 +125,6 @@ function tomlArray(values: readonly string[]): string {
return `[${values.map(tomlString).join(", ")}]`;
}

function managedDeepAgentsProviderFor(settings: Settings): ManagedDeepAgentsProvider {
if (OPENROUTER_UPSTREAM_PROVIDERS.has(settings.upstreamProvider)) return "openrouter";
if (
settings.upstreamProvider === "compatible-endpoint" &&
isOpenRouterEndpointUrl(settings.upstreamEndpointUrl)
) {
return "openrouter";
}
return "openai";
}

function isOpenRouterEndpointUrl(value: string | null): boolean {
if (!value) return false;
const url = new URL(value);
return (
url.protocol === "https:" &&
url.hostname.toLowerCase() === OPENROUTER_ENDPOINT_HOST &&
url.pathname.replace(/\/+$/, "") === OPENROUTER_ENDPOINT_PATH
);
}

function modelNameForManagedProvider(model: string): string {
const trimmed = model.trim();
for (const prefix of ["openai:", "openrouter:"]) {
if (trimmed.startsWith(prefix)) return trimmed.slice(prefix.length);
}
return trimmed;
}

function openAiModelRequestParamLines(
model: string,
reasoningEffort: ReasoningEffort | null,
Expand Down Expand Up @@ -206,7 +153,7 @@ function openAiModelRequestParamLines(
}

function providerConfigLines(
provider: ManagedDeepAgentsProvider,
provider: ManagedDcodeProvider,
model: string,
baseUrl: string,
reasoningEffort: ReasoningEffort | null,
Expand All @@ -231,9 +178,11 @@ function providerConfigLines(
}

function buildConfig(settings: Settings): ManagedDeepAgentsConfig {
const provider = managedDeepAgentsProviderFor(settings);
const model = modelNameForManagedProvider(settings.model);
const defaultModel = `${provider}:${model}`;
const { provider, model, defaultModel } = resolveManagedDcodeIdentity(
settings.upstreamProvider,
settings.model,
settings.upstreamEndpointUrl,
);
const text = [
"# Generated by NemoClaw. This file contains no provider secrets.",
`# NemoClaw provider route: ${settings.providerKey}; upstream provider: ${settings.upstreamProvider}; API: ${settings.inferenceApi}.`,
Expand Down
83 changes: 83 additions & 0 deletions src/lib/inference/managed-dcode/identity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

export const OPENROUTER_ENDPOINT_URL = "https://openrouter.ai/api/v1";
export const OPENROUTER_PROVIDER_NAME = "openrouter-api";

export type ManagedDcodeProvider = "openai" | "openrouter";

export type ManagedDcodeIdentity = {
provider: ManagedDcodeProvider;
model: string;
defaultModel: string;
};

export function normalizeManagedDcodeEndpointUrl(
value: string | null | undefined,
name: string,
): string | null {
if (value === undefined || value === null || value.trim() === "") return null;
if (/[\p{Cc}\p{Cf}]/u.test(value)) {
throw new Error(`${name} must not contain control characters.`);
}
const text = value.trim();
let url: URL;
try {
url = new URL(text);
} catch {
throw new Error(`${name} must be a valid URL.`);
}
if (url.protocol !== "http:" && url.protocol !== "https:") {
throw new Error(`${name} must use HTTP or HTTPS.`);
}
if (url.username || url.password) {
throw new Error(`${name} must not include credentials.`);
}
if (url.search || url.hash) {
throw new Error(`${name} must not include query strings or fragments.`);
}
return url.href;
}

export function normalizeManagedDcodeModelName(model: string): string {
const trimmed = model.trim();
for (const prefix of ["openai:", "openrouter:"]) {
if (trimmed.startsWith(prefix)) return trimmed.slice(prefix.length);
}
return trimmed;
}

function isOpenRouterEndpointUrl(value: string | null | undefined): boolean {
try {
const normalized = normalizeManagedDcodeEndpointUrl(value, "endpoint URL");
if (!normalized) return false;
const url = new URL(normalized);
const openRouterUrl = new URL(OPENROUTER_ENDPOINT_URL);
return (
url.origin === openRouterUrl.origin &&
url.pathname.replace(/\/+$/, "") === openRouterUrl.pathname.replace(/\/+$/, "")
);
} catch {
return false;
}
}

export function resolveManagedDcodeIdentity(
upstreamProvider: string | null | undefined,
model: string,
upstreamEndpointUrl: string | null | undefined,
): ManagedDcodeIdentity {
const providerName = upstreamProvider?.trim();
const provider =
providerName === "openrouter" ||
providerName === OPENROUTER_PROVIDER_NAME ||
(providerName === "compatible-endpoint" && isOpenRouterEndpointUrl(upstreamEndpointUrl))
? "openrouter"
: "openai";
const normalizedModel = normalizeManagedDcodeModelName(model);
return {
provider,
model: normalizedModel,
defaultModel: `${provider}:${normalizedModel}`,
};
}
6 changes: 4 additions & 2 deletions src/lib/inference/openrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@

import { OPENROUTER_RUNTIME_ADAPTER_PORT } from "../core/ports";

export const OPENROUTER_ENDPOINT_URL = "https://openrouter.ai/api/v1";
export {
OPENROUTER_ENDPOINT_URL,
OPENROUTER_PROVIDER_NAME,
} from "./managed-dcode/identity";
export const OPENROUTER_HELP_URL = "https://openrouter.ai/workspaces/default/keys";
export const OPENROUTER_CREDENTIAL_ENV = "OPENROUTER_API_KEY";
export const OPENROUTER_PROVIDER_NAME = "openrouter-api";
// OpenShell does not expose a native OpenRouter provider profile yet. Register
// OpenRouter through the OpenAI-compatible provider profile while keeping a
// distinct provider name and credential binding in NemoClaw.
Expand Down
12 changes: 6 additions & 6 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ const {
getSelectionDrift,
}: typeof import("./onboard/selection-drift") = require("./onboard/selection-drift");
const {
getDcodeSelectionDrift,
createDcodeSelectionDriftReader,
requiresSelectionRecreate,
usesManagedDcodeIdentity,
}: typeof import("./onboard/dcode-selection-drift") = require("./onboard/dcode-selection-drift");
Expand Down Expand Up @@ -1582,7 +1582,7 @@ const sandboxCreateOrchestrationRuntime = {
get getDashboardForwardPort() {
return getDashboardForwardPort;
},
getDcodeSelectionDrift,
readDcodeSelectionDrift: createDcodeSelectionDriftReader(runCaptureOpenshell),
getDefaultSandboxNameForAgent,
getDockerDriverGatewayStateDir,
getHermesToolGatewayBroker,
Expand Down Expand Up @@ -1668,6 +1668,7 @@ const createSandboxWithBaseImageResolution =
sandboxCreateOrchestrationRuntime,
);


const { createSandbox, createSandboxWithTemporaryManagedRuntime } =
agentOnboard.createHermesApiPortScopedSandboxEntryPoints({
createBaseImageResolutionContext: () =>
Expand Down Expand Up @@ -3231,6 +3232,7 @@ async function runOnboard(opts: OnboardOptions = {}): Promise<void> {
},
},
},

sandbox: {
gatewayName: GATEWAY_NAME,
hermesPortableLifecycle:
Expand Down Expand Up @@ -3263,10 +3265,7 @@ async function runOnboard(opts: OnboardOptions = {}): Promise<void> {
messagingChannelConfigsEqual,
getSandboxReuseState,
getSandboxRecreateObservation,
getDcodeSelectionDrift: (name, selectedProvider, selectedModel, selectedApi) =>
getDcodeSelectionDrift(name, selectedProvider, selectedModel, selectedApi, {
runCaptureOpenshell,
}),
getDcodeSelectionDrift: createDcodeSelectionDriftReader(runCaptureOpenshell),
hasSandboxGpuDrift,
getSandboxHermesToolGateways: (name) => registry.getSandbox(name)?.hermesToolGateways,
getSandboxRegistryEntry: registry.getSandbox,
Expand Down Expand Up @@ -3315,6 +3314,7 @@ async function runOnboard(opts: OnboardOptions = {}): Promise<void> {
hermesApiPortReservationScope,
...createArgs,
),

),
),
updateSandboxRegistry: (name, updates) => registry.updateSandbox(name, updates),
Expand Down
Loading
Loading