Skip to content
Closed
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
60 changes: 60 additions & 0 deletions agents/langchain-deepagents-code/dcode-wrapper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ run_dcode() {
# TELEGRAM_BOT_TOKEN, DISCORD_BOT_TOKEN) are allowed only when the value
# matches the platform-specific token shape AND does not embed a
# non-platform canonical secret prefix.
# * Local OTLP collector endpoints are allowed only for OTLP endpoint names,
# only as canonical HTTP URLs to loopback/OpenShell host aliases on
# standard OTLP ports, and never for OTLP headers or other credential
# names.
# * The env-file parser strips a leading `export ` keyword (mirroring
# python-dotenv) and rejects values containing dotenv expansion ($VAR,
# ${VAR}), command substitution ($(...) or backticks), because upstream
Expand Down Expand Up @@ -351,6 +355,59 @@ is_allowed_openshell_runtime_value() {
[ "$name" = "OPENSHELL_TLS_KEY" ] && [ "$value" = "$OPENSHELL_TLS_KEY_PATH" ]
}

is_local_otlp_endpoint_name() {
case "$1" in
OTEL_EXPORTER_OTLP_ENDPOINT | OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)
return 0
;;
esac
return 1
}

is_allowed_local_otlp_endpoint_value() {
local value="$1"
local rest authority host port path
case "$value" in
http://*) rest="${value#http://}" ;;
*) return 1 ;;
esac
case "$rest" in
"" | *[[:space:]]* | *\'* | *\"* | *\\* | *\?* | *\#* | *%* | *@*)
return 1
;;
esac
authority="${rest%%/*}"
[ -n "$authority" ] || return 1
path="${rest#"$authority"}"
if [[ "$authority" == \[::1\]:* ]]; then
host="::1"
port="${authority##*:}"
else
case "$authority" in
*:*)
host="${authority%%:*}"
port="${authority#*:}"
;;
*)
return 1
;;
esac
fi
[ "$port" = "4317" ] || [ "$port" = "4318" ] || return 1
case "$host" in
localhost | 127.0.0.1 | ::1 | host.openshell.internal | host.docker.internal | host.containers.internal) ;;
*)
return 1
;;
esac
case "$path" in
"" | / | /v1/traces)
return 0
;;
esac
return 1
}

is_dynamic_dotenv_value() {
local value="$1"
case "$value" in
Expand Down Expand Up @@ -445,6 +502,9 @@ assert_no_secret_runtime_env() {
if is_secret_shaped_value "$value"; then
refuse_secret_env "runtime environment variable" "$name"
fi
if is_local_otlp_endpoint_name "$name" && is_allowed_local_otlp_endpoint_value "$value"; then
continue
fi
if has_credential_name_context "$name" && [ ${#value} -ge 10 ] && ! is_allowed_openshell_runtime_value "$name" "$value"; then
refuse_secret_env "runtime environment variable" "$name"
fi
Expand Down
74 changes: 67 additions & 7 deletions agents/langchain-deepagents-code/managed-dcode-runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@
"OTEL_EXPORTER_OTLP_HEADERS",
"OTEL_EXPORTER_OTLP_TRACES_HEADERS",
}
_LOCAL_OTLP_ENDPOINT_ENV_NAMES = {
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
}
_LOCAL_OTLP_ENDPOINT_HOSTS = {
"localhost",
"127.0.0.1",
"::1",
"host.openshell.internal",
"host.docker.internal",
"host.containers.internal",
}
_LOCAL_OTLP_ENDPOINT_PORTS = {4317, 4318}
_LOCAL_OTLP_ENDPOINT_PATHS = {"", "/", "/v1/traces"}
# Python's \s also includes control separators that ECMAScript excludes, so
# spell out the canonical whitespace set for cross-runtime parity.
_ECMASCRIPT_NON_WHITESPACE_SECRET_CHAR = (
Expand Down Expand Up @@ -213,6 +227,44 @@ def _is_managed_value(name: str, value: str) -> bool:
return False


def _is_allowed_local_otlp_endpoint(name: str, value: str) -> bool:
if name.upper() not in _LOCAL_OTLP_ENDPOINT_ENV_NAMES:
return False
if not value.isascii() or any(
character.isspace()
or ord(character) < 32
or ord(character) == 127
for character in value
):
return False
if any(character in value for character in ("%", "\\", "'", '"')):
return False
parsed = urlsplit(value)
if (
parsed.scheme != "http"
or not value.startswith("http://")
or not parsed.netloc
or parsed.hostname is None
or parsed.username is not None
or parsed.password is not None
or parsed.query
or parsed.fragment
or parsed.path not in _LOCAL_OTLP_ENDPOINT_PATHS
):
return False
try:
port = parsed.port
except ValueError:
return False
if (
port not in _LOCAL_OTLP_ENDPOINT_PORTS
or parsed.hostname not in _LOCAL_OTLP_ENDPOINT_HOSTS
):
return False
host = f"[{parsed.hostname}]" if ":" in parsed.hostname else parsed.hostname
return value == f"http://{host}:{port}{parsed.path}"


def _assert_safe_environment() -> None:
for name, value in os.environ.items():
if _OPENSHELL_ENV_PLACEHOLDER_PREFIX in value:
Expand All @@ -224,14 +276,22 @@ def _assert_safe_environment() -> None:
)
if _is_managed_value(name, value):
continue
if _contains_secret_shape(value) or (
len(value) >= 10
and (
_CREDENTIAL_NAME.search(name)
or _CREDENTIAL_CAMEL_NAME.search(name)
if _contains_secret_shape(value):
raise RuntimeError(
f"runtime environment variable {name} contains a credential; "
"use NemoClaw credential handling"
)
if _is_allowed_local_otlp_endpoint(name, value):
continue
if (
(
len(value) >= 10
and (
_CREDENTIAL_NAME.search(name)
or _CREDENTIAL_CAMEL_NAME.search(name)
)
)
) or (
bool(value) and name.upper() in _CREDENTIAL_ENV_NAMES
or (bool(value) and name.upper() in _CREDENTIAL_ENV_NAMES)
):
raise RuntimeError(
f"runtime environment variable {name} contains a credential; "
Expand Down
29 changes: 29 additions & 0 deletions test/langchain-deepagents-code-direct-module-patch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,35 @@ check("disabled", False)
}
});

it("allows direct-module local OTLP endpoint env vars only for endpoint names", () => {
const tempDir = createPackageFixture();
patchFixture(tempDir);
const allowed = spawnSync("python3", ["-m", "deepagents_code"], {
env: {
PATH: process.env.PATH,
PYTHONPATH: tempDir,
OTEL_EXPORTER_OTLP_ENDPOINT: "http://host.openshell.internal:4318",
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "http://127.0.0.1:4318/v1/traces",
},
encoding: "utf8",
});

expect(allowed.status, allowed.stderr).toBe(0);
expect(allowed.stdout).toContain("managed-posture-ok");

const rejected = spawnSync("python3", ["-m", "deepagents_code"], {
env: {
PATH: process.env.PATH,
PYTHONPATH: tempDir,
CUSTOM_API_KEY: "http://host.openshell.internal:4318",
},
encoding: "utf8",
});

expect(rejected.status).not.toBe(0);
expect(rejected.stderr).toContain("runtime environment variable CUSTOM_API_KEY");
});

it("allows only scoped managed credential-shaped runtime values", () => {
const tempDir = createPackageFixture();
patchFixture(tempDir);
Expand Down
48 changes: 47 additions & 1 deletion test/langchain-deepagents-code-image-credentials.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,60 @@ describe("LangChain Deep Agents Code image credential boundary", () => {
const fakeSecret = "sk-TEST-FAKE-DO-NOT-USE-0000000000000000000000";
const result = runWrapper(wrapperPath, ["-n", "hi"], { OPENAI_API_KEY: fakeSecret });

expect(result.status).not.toBe(0);
expect(result.status).toBe(2);
expect(result.stderr).toContain("OPENAI_API_KEY");
expect(result.stderr).not.toContain(fakeSecret);
expect(result.stderr).toContain("nemoclaw credentials");
expect(result.stdout).not.toContain("dcode-stub-ran");
expect(fs.existsSync(ranMarker)).toBe(false);
});

it.each([
{ name: "OTEL_EXPORTER_OTLP_ENDPOINT", value: "http://host.openshell.internal:4318" },
{ name: "OTEL_EXPORTER_OTLP_ENDPOINT", value: "http://localhost:4317/" },
{
name: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
value: "http://127.0.0.1:4318/v1/traces",
},
{
name: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
value: "http://[::1]:4318/v1/traces",
},
])("allows local OTLP endpoint env var $name=$value", ({ name, value }) => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-local-otel-"));
const { wrapperPath, ranMarker } = makeWrapperFixture(tempDir);
const result = runWrapper(wrapperPath, ["-n", "hi"], { [name]: value });

expect(result.status, `${name}=${value}`).toBe(0);
expect(result.stdout).toContain("dcode-stub-ran");
expect(result.stderr).not.toContain("refusing to start");
expect(fs.existsSync(ranMarker)).toBe(true);
});

it.each([
{ name: "OTEL_EXPORTER_OTLP_ENDPOINT", value: "https://collector.example/v1/traces" },
{ name: "OTEL_EXPORTER_OTLP_ENDPOINT", value: "http://host.openshell.internal:9999" },
{
name: "OTEL_EXPORTER_OTLP_ENDPOINT",
value: "http://host.openshell.internal:4318/v1/traces?api_key=opaque",
},
{
name: "OTEL_EXPORTER_OTLP_ENDPOINT",
value: "http://token@host.openshell.internal:4318",
},
{ name: "OTEL_EXPORTER_OTLP_HEADERS", value: "authorization=opaque-value" },
{ name: "CUSTOM_API_KEY", value: "http://host.openshell.internal:4318" },
])("rejects non-local or non-endpoint OTLP env var $name=$value", ({ name, value }) => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-otel-reject-"));
const { wrapperPath, ranMarker } = makeWrapperFixture(tempDir);
const result = runWrapper(wrapperPath, ["-n", "hi"], { [name]: value });

expect(result.status, `${name}=${value}`).toBe(2);
expect(result.stderr).toContain(name);
expect(result.stderr).not.toContain(value);
expect(fs.existsSync(ranMarker)).toBe(false);
});

it("rejects secret-shaped values written to the deepagents env file", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-wrapper-"));
const { wrapperPath, ranMarker, envFile } = makeWrapperFixture(tempDir);
Expand Down