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
41 changes: 41 additions & 0 deletions agents/langchain-deepagents-code/dcode-launcher.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ unset _nemoclaw_auto_approval_env
readonly MANAGED_DCODE_WRAPPER="/usr/local/lib/nemoclaw/dcode-wrapper.sh"
readonly MANAGED_EXEC_LAUNCHER="/usr/local/lib/nemoclaw/dcode-managed-exec"
readonly MANAGED_OBSERVABILITY_MARKER="/sandbox/.deepagents/.nemoclaw-observability-enabled"
readonly MANAGED_FETCH_CA_BUNDLE_FILE="/etc/openshell-tls/ca-bundle.pem"
readonly MANAGED_SESSION_SUPERVISOR="/usr/local/lib/nemoclaw/dcode-session-supervisor.py"
export HOME=/sandbox
export PATH="/usr/local/bin:/opt/venv/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin"
Expand Down Expand Up @@ -98,10 +99,50 @@ read_managed_proxy_value() {
printf '%s' "$value"
}

managed_fetch_ca_bundle_metadata() {
local file="$1"
local metadata
if metadata="$(stat -c '%u:%a:%s' "$file" 2>/dev/null)"; then
printf '%s' "$metadata"
else
stat -f '%u:%Lp:%z' "$file" 2>/dev/null
fi
}

validate_managed_fetch_ca_bundle() {
local file="$MANAGED_FETCH_CA_BUNDLE_FILE"
local metadata owner mode size extra
if [ -L "$file" ]; then
printf '%s\n' 'Missing or unsafe managed fetch CA bundle file.' >&2
return 1
fi
[ -e "$file" ] || return 0
if [ ! -f "$file" ] || [ ! -r "$file" ]; then
printf '%s\n' 'Missing or unsafe managed fetch CA bundle file.' >&2
return 1
fi
metadata="$(managed_fetch_ca_bundle_metadata "$file")" || {
printf '%s\n' 'Cannot inspect managed fetch CA bundle file.' >&2
return 1
}
IFS=: read -r owner mode size extra <<<"$metadata"
if [ -n "${extra:-}" ] \
|| [[ ! "$owner" =~ ^[0-9]+$ ]] \
|| [[ ! "$mode" =~ ^[0-7]{3,4}$ ]] \
|| [[ ! "$size" =~ ^[0-9]+$ ]] \
|| [ "$owner" != "$MANAGED_PROXY_OWNER_UID" ] \
|| [ "$size" -le 0 ] \
|| (((8#$mode & 0022) != 0)); then
printf '%s\n' 'Unsafe ownership or mode on managed fetch CA bundle file.' >&2
return 1
fi
}

# Onboard validates the build args and the Dockerfile stores them in root-owned
# files. Runtime env is untrusted and cannot override those image-baked values.
PROXY_HOST="$(read_managed_proxy_value "$MANAGED_PROXY_HOST_FILE" "host")"
PROXY_PORT="$(read_managed_proxy_value "$MANAGED_PROXY_PORT_FILE" "port")"
validate_managed_fetch_ca_bundle
unset NEMOCLAW_PROXY_HOST NEMOCLAW_PROXY_PORT
# Generic proxy fallbacks are outside the managed dcode contract and may carry
# host credentials even after the scheme-specific proxy values are normalized.
Expand Down
81 changes: 81 additions & 0 deletions agents/langchain-deepagents-code/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ unset _NEMOCLAW_SANDBOX_RLIMITS
# or when dcode no longer uses inference.local.
readonly MANAGED_PROXY_HOST_FILE="/usr/local/share/nemoclaw/dcode-proxy-host"
readonly MANAGED_PROXY_PORT_FILE="/usr/local/share/nemoclaw/dcode-proxy-port"
readonly MANAGED_FETCH_CA_BUNDLE_FILE="/etc/openshell-tls/ca-bundle.pem"
readonly MANAGED_PROXY_OWNER_UID=0

managed_proxy_file_metadata() {
Expand Down Expand Up @@ -104,10 +105,50 @@ read_managed_proxy_value() {
printf '%s' "$value"
}

managed_fetch_ca_bundle_metadata() {
local file="$1"
local metadata
if metadata="$(stat -c '%u:%a:%s' "$file" 2>/dev/null)"; then
printf '%s' "$metadata"
else
stat -f '%u:%Lp:%z' "$file" 2>/dev/null
fi
}

validate_managed_fetch_ca_bundle() {
local file="$MANAGED_FETCH_CA_BUNDLE_FILE"
local metadata owner mode size extra
if [ -L "$file" ]; then
printf '%s\n' 'Missing or unsafe managed fetch CA bundle file.' >&2
return 1
fi
[ -e "$file" ] || return 0
if [ ! -f "$file" ] || [ ! -r "$file" ]; then
printf '%s\n' 'Missing or unsafe managed fetch CA bundle file.' >&2
return 1
fi
metadata="$(managed_fetch_ca_bundle_metadata "$file")" || {
printf '%s\n' 'Cannot inspect managed fetch CA bundle file.' >&2
return 1
}
IFS=: read -r owner mode size extra <<<"$metadata"
if [ -n "${extra:-}" ] \
|| [[ ! "$owner" =~ ^[0-9]+$ ]] \
|| [[ ! "$mode" =~ ^[0-7]{3,4}$ ]] \
|| [[ ! "$size" =~ ^[0-9]+$ ]] \
|| [ "$owner" != "$MANAGED_PROXY_OWNER_UID" ] \
|| [ "$size" -le 0 ] \
|| (((8#$mode & 0022) != 0)); then
printf '%s\n' 'Unsafe ownership or mode on managed fetch CA bundle file.' >&2
return 1
fi
}

# Fail closed if the root-owned image contract is missing. Process-level
# NEMOCLAW_PROXY_* values are not a trusted runtime routing source.
PROXY_HOST="$(read_managed_proxy_value "$MANAGED_PROXY_HOST_FILE" "host")"
PROXY_PORT="$(read_managed_proxy_value "$MANAGED_PROXY_PORT_FILE" "port")"
validate_managed_fetch_ca_bundle
unset NEMOCLAW_PROXY_HOST NEMOCLAW_PROXY_PORT
# Generic proxy fallbacks are outside the managed dcode contract and may carry
# host credentials even after the scheme-specific proxy values are normalized.
Expand Down Expand Up @@ -200,6 +241,46 @@ prepare_runtime_env() {
write_export_if_set http_proxy
write_export_if_set https_proxy
write_export_if_set no_proxy
printf '_nemoclaw_dcode_ca_bundle=%q\n' "$MANAGED_FETCH_CA_BUNDLE_FILE"
printf '_nemoclaw_dcode_ca_owner_uid=%q\n' "$MANAGED_PROXY_OWNER_UID"
cat <<'NEMOCLAW_DCODE_CA_GUARD'
if [ -L "$_nemoclaw_dcode_ca_bundle" ]; then
printf "%s\n" "Missing or unsafe managed fetch CA bundle file." >&2
return 1 2>/dev/null || exit 1
elif [ -e "$_nemoclaw_dcode_ca_bundle" ]; then
if [ ! -f "$_nemoclaw_dcode_ca_bundle" ] || [ ! -r "$_nemoclaw_dcode_ca_bundle" ]; then
printf "%s\n" "Missing or unsafe managed fetch CA bundle file." >&2
return 1 2>/dev/null || exit 1
fi
_nemoclaw_dcode_ca_meta="$(stat -c "%u:%a:%s" "$_nemoclaw_dcode_ca_bundle" 2>/dev/null || stat -f "%u:%Lp:%z" "$_nemoclaw_dcode_ca_bundle" 2>/dev/null || true)"
IFS=: read -r _nemoclaw_dcode_ca_owner _nemoclaw_dcode_ca_mode _nemoclaw_dcode_ca_size _nemoclaw_dcode_ca_extra <<NEMOCLAW_DCODE_CA_METADATA
$_nemoclaw_dcode_ca_meta
NEMOCLAW_DCODE_CA_METADATA
_nemoclaw_dcode_ca_valid=1
case "$_nemoclaw_dcode_ca_owner" in
'' | *[!0-9]*) _nemoclaw_dcode_ca_valid=0 ;;
esac
case "$_nemoclaw_dcode_ca_owner_uid" in
'' | *[!0-9]*) _nemoclaw_dcode_ca_valid=0 ;;
esac
case "$_nemoclaw_dcode_ca_mode" in
[0-7][0-7][0-7] | [0-7][0-7][0-7][0-7]) ;;
*) _nemoclaw_dcode_ca_valid=0 ;;
esac
case "$_nemoclaw_dcode_ca_size" in
'' | *[!0-9]*) _nemoclaw_dcode_ca_valid=0 ;;
esac
if [ "$_nemoclaw_dcode_ca_valid" -ne 1 ] \
|| [ -n "${_nemoclaw_dcode_ca_extra:-}" ] \
|| [ "$_nemoclaw_dcode_ca_owner" != "$_nemoclaw_dcode_ca_owner_uid" ] \
|| [ "$_nemoclaw_dcode_ca_size" -le 0 ] \
|| [ "$((0$_nemoclaw_dcode_ca_mode & 022))" -ne 0 ]; then
printf "%s\n" "Unsafe ownership or mode on managed fetch CA bundle file." >&2
return 1 2>/dev/null || exit 1
fi
fi
unset _nemoclaw_dcode_ca_bundle _nemoclaw_dcode_ca_owner_uid _nemoclaw_dcode_ca_meta _nemoclaw_dcode_ca_owner _nemoclaw_dcode_ca_mode _nemoclaw_dcode_ca_size _nemoclaw_dcode_ca_extra _nemoclaw_dcode_ca_valid
NEMOCLAW_DCODE_CA_GUARD
write_export_if_set SSL_CERT_FILE
write_export_if_set REQUESTS_CA_BUNDLE
write_export_if_set NODE_EXTRA_CA_CERTS
Expand Down
7 changes: 7 additions & 0 deletions test/helpers/langchain-deepagents-code-headless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export function makeStartScriptFixture(
const rlimitLib = path.join(tempDir, "sandbox-rlimits.sh");
const hostFile = path.join(tempDir, "trusted-proxy-host");
const portFile = path.join(tempDir, "trusted-proxy-port");
const caFile = path.join(tempDir, "trusted-ca-bundle.pem");
const markerDir = path.join(tempDir, "persistent-dcode-state");
expect(original).toContain("local target=/tmp/nemoclaw-proxy-env.sh");
expect(original).toContain('tmp="$(mktemp /tmp/nemoclaw-proxy-env.XXXXXX)"');
Expand All @@ -67,6 +68,10 @@ export function makeStartScriptFixture(
'readonly MANAGED_PROXY_PORT_FILE="/usr/local/share/nemoclaw/dcode-proxy-port"',
`readonly MANAGED_PROXY_PORT_FILE="${portFile}"`,
)
.replace(
'readonly MANAGED_FETCH_CA_BUNDLE_FILE="/etc/openshell-tls/ca-bundle.pem"',
`readonly MANAGED_FETCH_CA_BUNDLE_FILE="${caFile}"`,
)
.replace(
"readonly MANAGED_PROXY_OWNER_UID=0",
`readonly MANAGED_PROXY_OWNER_UID=${process.getuid?.() ?? 0}`,
Expand All @@ -85,13 +90,15 @@ export function makeStartScriptFixture(
expect(fixture).not.toContain("local marker_dir=/sandbox/.deepagents");
fs.writeFileSync(hostFile, "10.200.0.1\n", "utf8");
fs.writeFileSync(portFile, "3128\n", "utf8");
fs.writeFileSync(caFile, "trusted CA bundle\n", "utf8");
fs.writeFileSync(
rlimitLib,
"harden_resource_limits() { :; }\nverify_resource_limits_exact() { :; }\n",
"utf8",
);
fs.chmodSync(hostFile, 0o444);
fs.chmodSync(portFile, 0o444);
fs.chmodSync(caFile, 0o444);
fs.writeFileSync(scriptPath, fixture, "utf8");
fs.chmodSync(scriptPath, 0o755);
return { envFile, scriptPath };
Expand Down
110 changes: 110 additions & 0 deletions test/langchain-deepagents-code-proxy-launcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,16 @@ function writeManagedProxyFiles(
): void {
const hostFile = path.join(tempDir, "trusted-proxy-host");
const portFile = path.join(tempDir, "trusted-proxy-port");
const caFile = path.join(tempDir, "trusted-ca-bundle.pem");
fs.rmSync(hostFile, { force: true });
fs.rmSync(portFile, { force: true });
fs.rmSync(caFile, { force: true });
fs.writeFileSync(hostFile, `${managedProxy.host}\n`);
fs.writeFileSync(portFile, `${managedProxy.port}\n`);
fs.writeFileSync(caFile, "trusted CA bundle\n");
fs.chmodSync(hostFile, 0o444);
fs.chmodSync(portFile, 0o444);
fs.chmodSync(caFile, 0o444);
}

function replaceManagedProxyFileConstants(source: string, tempDir: string): string {
Expand All @@ -69,6 +73,10 @@ function replaceManagedProxyFileConstants(source: string, tempDir: string): stri
'readonly MANAGED_PROXY_PORT_FILE="/usr/local/share/nemoclaw/dcode-proxy-port"',
`readonly MANAGED_PROXY_PORT_FILE="${path.join(tempDir, "trusted-proxy-port")}"`,
)
.replace(
'readonly MANAGED_FETCH_CA_BUNDLE_FILE="/etc/openshell-tls/ca-bundle.pem"',
`readonly MANAGED_FETCH_CA_BUNDLE_FILE="${path.join(tempDir, "trusted-ca-bundle.pem")}"`,
)
.replace(
"readonly MANAGED_PROXY_OWNER_UID=0",
`readonly MANAGED_PROXY_OWNER_UID=${TEST_OWNER_UID}`,
Expand Down Expand Up @@ -439,6 +447,11 @@ describe("Deep Agents Code direct-exec proxy launcher", () => {
expect(launcherResult.status, launcherResult.stderr).toBe(0);
expect(startResult.status, startResult.stderr).toBe(0);
const envFileText = fs.readFileSync(envFile, "utf8");
const posixSourceResult = spawnSync("sh", ["-c", '. "$1"', "sh", envFile], {
env: { PATH: process.env.PATH ?? "/usr/bin:/bin" },
encoding: "utf8",
});
expect(posixSourceResult.status, posixSourceResult.stderr).toBe(0);
const launcherNoProxy = launcherResult.stdout.match(/^LAUNCHER_NO_PROXY=(.*)$/m)?.[1];
const startNoProxy = startResult.stdout.match(/^START_PROXY=[^|]*\|([^|]*)\|/m)?.[1];
expect(fs.statSync(envFile).mode & 0o777).toBe(0o444);
Expand Down Expand Up @@ -517,6 +530,103 @@ describe("Deep Agents Code direct-exec proxy launcher", () => {
);
});

const expectManagedCaBundleRejection = ({
expected,
mutate,
}: {
expected: string;
mutate: (caFile: string) => void;
}): void => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-ca-bundle-"));
const launcherPath = makeLauncherProxyProbeFixture(tempDir);
const { envFile, scriptPath } = makeStartProxyProbeFixture(tempDir);
const caFile = path.join(tempDir, "trusted-ca-bundle.pem");

const safeStart = spawnSync("bash", [scriptPath, "true"], {
env: { PATH: process.env.PATH ?? "/usr/bin:/bin" },
encoding: "utf8",
});
expect(safeStart.status, safeStart.stderr).toBe(0);
expect(fs.existsSync(envFile)).toBe(true);

mutate(caFile);
const launcherResult = runLauncher(launcherPath, ["-n", "PONG"], {});
const startResult = spawnSync("bash", [scriptPath, "true"], {
env: { PATH: process.env.PATH ?? "/usr/bin:/bin" },
encoding: "utf8",
});
const connectSourceResult = spawnSync("sh", ["-c", '. "$1"', "sh", envFile], {
env: { PATH: process.env.PATH ?? "/usr/bin:/bin" },
encoding: "utf8",
});

expect(launcherResult.status).not.toBe(0);
expect(startResult.status).not.toBe(0);
expect(connectSourceResult.status).not.toBe(0);
expect(launcherResult.stderr).toContain(expected);
expect(startResult.stderr).toContain(expected);
expect(connectSourceResult.stderr).toContain(expected);
const combined = `${launcherResult.stderr}\n${startResult.stderr}\n${connectSourceResult.stderr}`;
expect(combined).not.toContain(caFile);
};

it.each([
{
condition: "writable",
expected: "Unsafe ownership or mode on managed fetch CA bundle file",
mutate: (caFile: string) => fs.chmodSync(caFile, 0o666),
},
{
condition: "empty",
expected: "Unsafe ownership or mode on managed fetch CA bundle file",
mutate: (caFile: string) => {
fs.chmodSync(caFile, 0o600);
fs.truncateSync(caFile, 0);
fs.chmodSync(caFile, 0o444);
},
},
{
condition: "non-regular",
expected: "Missing or unsafe managed fetch CA bundle file",
mutate: (caFile: string) => {
fs.rmSync(caFile);
fs.mkdirSync(caFile);
},
},
{
condition: "regular-file symlink",
expected: "Missing or unsafe managed fetch CA bundle file",
mutate: (caFile: string) => {
const target = `${caFile}.target`;
fs.renameSync(caFile, target);
fs.symlinkSync(target, caFile);
},
},
{
condition: "dangling symlink",
expected: "Missing or unsafe managed fetch CA bundle file",
mutate: (caFile: string) => {
fs.rmSync(caFile);
fs.symlinkSync(`${caFile}.missing`, caFile);
},
},
])("rejects $condition managed fetch CA bundles in start, connect, and direct dcode paths (#6636)", ({
expected,
mutate,
}) => {
expectManagedCaBundleRejection({ expected, mutate });
});

it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"rejects an unreadable managed fetch CA bundle in start, connect, and direct dcode paths (#6636)",
() => {
expectManagedCaBundleRejection({
expected: "Missing or unsafe managed fetch CA bundle file",
mutate: (caFile: string) => fs.chmodSync(caFile, 0o000),
});
},
);

it("keeps dcode shell proxy validators aligned with onboard validation (#6191)", () => {
const start = readAgentFile("start.sh");
const launcher = readAgentFile("dcode-launcher.sh");
Expand Down
7 changes: 7 additions & 0 deletions test/support/dcode-start-script-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function makeStartScriptFixture(tempDir: string): {
const rlimitLib = path.join(tempDir, "sandbox-rlimits.sh");
const hostFile = path.join(tempDir, "trusted-proxy-host");
const portFile = path.join(tempDir, "trusted-proxy-port");
const caFile = path.join(tempDir, "trusted-ca-bundle.pem");
const original = fs.readFileSync(START_SCRIPT, "utf8");
assert.ok(original.includes("local target=/tmp/nemoclaw-proxy-env.sh"));
assert.ok(original.includes('tmp="$(mktemp /tmp/nemoclaw-proxy-env.XXXXXX)"'));
Expand All @@ -36,6 +37,10 @@ export function makeStartScriptFixture(tempDir: string): {
'readonly MANAGED_PROXY_PORT_FILE="/usr/local/share/nemoclaw/dcode-proxy-port"',
`readonly MANAGED_PROXY_PORT_FILE="${portFile}"`,
)
.replace(
'readonly MANAGED_FETCH_CA_BUNDLE_FILE="/etc/openshell-tls/ca-bundle.pem"',
`readonly MANAGED_FETCH_CA_BUNDLE_FILE="${caFile}"`,
)
.replace(
"readonly MANAGED_PROXY_OWNER_UID=0",
`readonly MANAGED_PROXY_OWNER_UID=${process.getuid?.() ?? 0}`,
Expand All @@ -51,13 +56,15 @@ export function makeStartScriptFixture(tempDir: string): {
assert.ok(!fixture.includes('tmp="$(mktemp /tmp/nemoclaw-proxy-env.XXXXXX)"'));
fs.writeFileSync(hostFile, "10.200.0.1\n", "utf8");
fs.writeFileSync(portFile, "3128\n", "utf8");
fs.writeFileSync(caFile, "trusted CA bundle\n", "utf8");
fs.writeFileSync(
rlimitLib,
"harden_resource_limits() { :; }\nverify_resource_limits_exact() { :; }\n",
"utf8",
);
fs.chmodSync(hostFile, 0o444);
fs.chmodSync(portFile, 0o444);
fs.chmodSync(caFile, 0o444);
fs.writeFileSync(scriptPath, fixture, "utf8");
fs.chmodSync(scriptPath, 0o755);
return { envFile, scriptPath };
Expand Down
Loading