Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
14e32af
feat(network): support trusted private endpoints
ericksoa Aug 5, 2026
5f90e35
fix(policy): preserve sequential directory apply
ericksoa Aug 5, 2026
c9d7fd2
fix(security): address trusted endpoint review
ericksoa Aug 5, 2026
522c5cb
Merge remote-tracking branch 'origin/main' into feat/trusted-private-mcp
ericksoa Aug 5, 2026
6cc9226
fix(shields): admit recorded private MCP pins
ericksoa Aug 5, 2026
440705d
fix(security): reject mixed trusted policy answers
ericksoa Aug 5, 2026
d1006cb
test(shields): bind restore runner mock
ericksoa Aug 5, 2026
124fffa
merge: resolve conflicts with main
github-actions[bot] Aug 5, 2026
ea10bf2
ci: refresh exact-head validation gates
ericksoa Aug 5, 2026
96a26ac
fix(security): reject mixed inference DNS answers
ericksoa Aug 5, 2026
cb9780b
fix(security): preserve mixed-answer diagnostic
ericksoa Aug 5, 2026
f30d76c
test(e2e): prove trusted-private MCP rebinding
ericksoa Aug 5, 2026
8072597
Merge remote-tracking branch 'origin/main' into feat/trusted-private-mcp
ericksoa Aug 5, 2026
e24a3cf
test(e2e): keep live MCP test linear
ericksoa Aug 5, 2026
39c235b
fix(mcp): trust private endpoint CAs in proxy
ericksoa Aug 5, 2026
a36d289
docs(mcp): explain private endpoint CA trust
ericksoa Aug 5, 2026
de9235e
merge: resolve conflicts with main
github-actions[bot] Aug 5, 2026
c0ecd8b
test(mcp): accept rejected public drift details
ericksoa Aug 5, 2026
aa16bee
merge: resolve conflicts with main
github-actions[bot] Aug 5, 2026
6d319be
chore(ci): validate merged main
ericksoa Aug 5, 2026
affd931
merge: sync main
ericksoa Aug 5, 2026
605fe3f
chore(architecture): lower merged main budgets
ericksoa Aug 5, 2026
2078d41
Merge remote-tracking branch 'origin/main' into feat/trusted-private-mcp
ericksoa Aug 5, 2026
1211a42
chore(ci): retry hosted E2E port collision
ericksoa Aug 5, 2026
565a212
Merge remote-tracking branch 'origin/main' into feat/trusted-private-mcp
ericksoa Aug 5, 2026
714131e
fix(ci): restore complete CLI build artifact
ericksoa Aug 5, 2026
faeb93c
ci(e2e): pin complete CLI artifact restore
ericksoa Aug 5, 2026
8998ef1
test(ci): keep artifact fixtures branchless
ericksoa Aug 5, 2026
f138384
merge: resolve conflicts with main
github-actions[bot] Aug 5, 2026
6f1f960
Merge remote-tracking branch 'origin/main' into feat/trusted-private-mcp
ericksoa Aug 5, 2026
5b2a9bb
test(snapshot): align private restore expectation
ericksoa Aug 5, 2026
dc342e6
merge: resolve conflicts with main
github-actions[bot] Aug 5, 2026
3db5944
Merge remote-tracking branch 'origin/main' into feat/trusted-private-mcp
ericksoa Aug 5, 2026
70a0a0d
merge: resolve conflicts with main
github-actions[bot] Aug 5, 2026
601a856
merge: synchronize trusted private MCP with main
ericksoa Aug 5, 2026
9998eda
fix(hermes): recheck post-restore gateway health
ericksoa Aug 5, 2026
5ee988d
test(hermes): accept flattened cron state
ericksoa Aug 5, 2026
7a7160a
test(hermes): preserve cron receipt redaction
ericksoa Aug 5, 2026
5a5bb70
test(hermes): exercise scheduled cron drain
ericksoa Aug 5, 2026
2b95154
fix(hermes): stabilize scheduler and integrity probes
ericksoa Aug 5, 2026
613f933
test(hermes): tolerate restart pid gap
ericksoa Aug 5, 2026
57f1828
Merge remote-tracking branch 'origin/main' into feat/trusted-private-mcp
ericksoa Aug 5, 2026
9e4c7f3
Merge remote-tracking branch 'origin/main' into feat/trusted-private-mcp
ericksoa Aug 5, 2026
60b058b
Merge remote-tracking branch 'origin/main' into feat/trusted-private-mcp
ericksoa Aug 5, 2026
01e8976
merge: resolve conflicts with main
github-actions[bot] Aug 5, 2026
ac87bce
Merge remote-tracking branch 'origin/main' into feat/trusted-private-mcp
ericksoa Aug 5, 2026
82e5131
fix(hermes): recover raced MCP apply snapshot
ericksoa Aug 5, 2026
f28ac2b
fix(mcp): retry provider detach conflicts
ericksoa Aug 5, 2026
59476e2
fix(ci): scale PR E2E evidence budget
ericksoa Aug 6, 2026
0e4e246
Merge remote-tracking branch 'origin/main' into feat/trusted-private-mcp
ericksoa Aug 6, 2026
c8132c0
Merge remote-tracking branch 'origin/main' into feat/trusted-private-mcp
ericksoa Aug 6, 2026
4c881ae
Merge remote-tracking branch 'origin/main' into feat/trusted-private-mcp
ericksoa Aug 6, 2026
f8cf0f7
fix(ci): compact successful E2E evidence
ericksoa Aug 6, 2026
7d19939
Merge remote-tracking branch 'origin/main' into feat/trusted-private-mcp
ericksoa Aug 6, 2026
56907fc
Merge remote-tracking branch 'origin/main' into feat/trusted-private-mcp
ericksoa Aug 6, 2026
6fe3590
Merge remote-tracking branch 'origin/main' into feat/trusted-private-mcp
ericksoa Aug 6, 2026
442630d
Merge remote-tracking branch 'origin/main' into feat/trusted-private-mcp
ericksoa Aug 6, 2026
6f26510
fix(inference): reconcile serving resolver changes
ericksoa Aug 6, 2026
5daa043
Merge remote-tracking branch 'origin/main' into feat/trusted-private-mcp
ericksoa Aug 6, 2026
00fbb3b
merge: resolve conflicts with main
github-actions[bot] Aug 6, 2026
66bfa77
merge: resolve conflicts with main
github-actions[bot] Aug 6, 2026
1be4214
merge: resolve conflicts with main
github-actions[bot] Aug 6, 2026
cd31190
fix(ci): remove obsolete PR gate evidence code
ericksoa Aug 6, 2026
ec2e203
Merge remote-tracking branch 'origin/main' into feat/trusted-private-mcp
ericksoa Aug 6, 2026
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
10 changes: 7 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -262,15 +262,19 @@
RUN if [ -n "${NEMOCLAW_CORPORATE_CA_B64}" ]; then \
command -v base64 >/dev/null 2>&1 || { echo "[nemoclaw] base64 is required to decode NEMOCLAW_CORPORATE_CA_B64 but is not installed in the build image" >&2; exit 1; }; \
command -v openssl >/dev/null 2>&1 || { echo "[nemoclaw] openssl is required to validate NEMOCLAW_CORPORATE_CA_B64 but is not installed in the build image (#6210)" >&2; exit 1; }; \
mkdir -p /usr/local/share/nemoclaw \
command -v update-ca-certificates >/dev/null 2>&1 || { echo "[nemoclaw] update-ca-certificates is required to anchor NEMOCLAW_CORPORATE_CA_B64 for the OpenShell proxy" >&2; exit 1; }; \
case "${NEMOCLAW_CORPORATE_CA_B64}" in *[!A-Za-z0-9+/=]*) echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 is not valid base64; expected a single-line base64-encoded PEM (#6210)" >&2; exit 1 ;; esac; \
mkdir -p /usr/local/share/nemoclaw /usr/local/share/ca-certificates \
&& { printf '%s' "${NEMOCLAW_CORPORATE_CA_B64}" | base64 --decode > /tmp/nemoclaw-corporate-ca.decoded 2>/dev/null \
|| { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 is not valid base64; expected a single-line base64-encoded PEM (#6210)" >&2; exit 1; }; } \
&& awk '/-----BEGIN CERTIFICATE-----/{f=1} f{print} /-----END CERTIFICATE-----/{f=0}' /tmp/nemoclaw-corporate-ca.decoded > /usr/local/share/nemoclaw/corporate-ca.pem \
&& rm -f /tmp/nemoclaw-corporate-ca.decoded \
&& { grep -qF -- "-----BEGIN CERTIFICATE-----" /usr/local/share/nemoclaw/corporate-ca.pem || { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 did not decode to a bundle of valid X.509 certificates (#6210)" >&2; exit 1; }; } \
&& { openssl crl2pkcs7 -nocrl -certfile /usr/local/share/nemoclaw/corporate-ca.pem >/dev/null 2>&1 || { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 did not decode to a bundle of valid X.509 certificates (#6210)" >&2; exit 1; }; } \
&& chown root:root /usr/local/share/nemoclaw/corporate-ca.pem \
&& chmod 0444 /usr/local/share/nemoclaw/corporate-ca.pem \
&& node -e 'const fs = require("node:fs"); const { X509Certificate } = require("node:crypto"); const pemPath = process.argv[1]; const anchorDir = process.argv[2]; const pem = fs.readFileSync(pemPath, "utf8"); const blocks = pem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g); if (!blocks?.length) process.exit(1); fs.writeFileSync(pemPath, blocks.map((block) => block.trim()).join("\n") + "\n"); blocks.forEach((block, index) => { if (!new X509Certificate(block).ca) process.exit(1); const name = anchorDir + "/nemoclaw-corporate-ca-" + String(index + 1).padStart(2, "0") + ".crt"; fs.writeFileSync(name, block.trim() + "\n"); });' /usr/local/share/nemoclaw/corporate-ca.pem /usr/local/share/ca-certificates \
&& chown root:root /usr/local/share/nemoclaw/corporate-ca.pem /usr/local/share/ca-certificates/nemoclaw-corporate-ca-*.crt \
&& chmod 0444 /usr/local/share/nemoclaw/corporate-ca.pem /usr/local/share/ca-certificates/nemoclaw-corporate-ca-*.crt \
&& update-ca-certificates \
&& echo "[nemoclaw] baked host corporate-proxy CA into image trust (#6210)"; \
fi

Expand Down Expand Up @@ -1198,10 +1202,10 @@
# CHAT_UI_URL is a non-loopback address (Brev Launchable, remote deployments)
# since terminal-based pairing is impossible in those contexts.
# Default: "0" (device auth enabled for local deployments — secure by default).
ARG NEMOCLAW_DISABLE_DEVICE_AUTH=0

Check warning on line 1205 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw)

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

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "NEMOCLAW_DISABLE_DEVICE_AUTH") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
# Internal audit provenance for the opt-out above. Standard onboarding rewrites
# this to managed-onboard; direct image builders retain operator provenance.
ARG NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE=operator

Check warning on line 1208 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw)

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

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
# Compatibility build arg for older custom Dockerfiles and rebuild tooling.
# NemoClaw-managed images intentionally do not consume it; gateway auth tokens
# are generated at container startup and are never baked into image layers.
Expand Down Expand Up @@ -1235,7 +1239,7 @@
# NEMOCLAW_MESSAGING_PLAN_B64 intentionally remains ARG-only: Docker exposes it
# to build RUN processes without retaining the full plan in the final image env.
# Direct ARG interpolation into inline source is a code injection vector (C-2).
ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \

Check warning on line 1242 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw)

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

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "NEMOCLAW_DEVICE_AUTH_OPT_OUT_SOURCE") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 1242 in Dockerfile

View workflow job for this annotation

GitHub Actions / PR build and direct managed startup (OpenClaw)

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

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "NEMOCLAW_DISABLE_DEVICE_AUTH") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
NEMOCLAW_INFERENCE_PROVIDER_ID=${NEMOCLAW_INFERENCE_PROVIDER_ID} \
NEMOCLAW_UPSTREAM_PROVIDER=${NEMOCLAW_UPSTREAM_PROVIDER} \
NEMOCLAW_PRIMARY_MODEL_REF=${NEMOCLAW_PRIMARY_MODEL_REF} \
Expand Down
10 changes: 7 additions & 3 deletions agents/hermes/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -165,15 +165,19 @@ ARG NEMOCLAW_CORPORATE_CA_B64
RUN if [ -n "${NEMOCLAW_CORPORATE_CA_B64}" ]; then \
command -v base64 >/dev/null 2>&1 || { echo "[nemoclaw] base64 is required to decode NEMOCLAW_CORPORATE_CA_B64 but is not installed in the build image" >&2; exit 1; }; \
command -v openssl >/dev/null 2>&1 || { echo "[nemoclaw] openssl is required to validate NEMOCLAW_CORPORATE_CA_B64 but is not installed in the build image (#6210)" >&2; exit 1; }; \
mkdir -p /usr/local/share/nemoclaw \
command -v update-ca-certificates >/dev/null 2>&1 || { echo "[nemoclaw] update-ca-certificates is required to anchor NEMOCLAW_CORPORATE_CA_B64 for the OpenShell proxy" >&2; exit 1; }; \
case "${NEMOCLAW_CORPORATE_CA_B64}" in *[!A-Za-z0-9+/=]*) echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 is not valid base64; expected a single-line base64-encoded PEM (#6210)" >&2; exit 1 ;; esac; \
mkdir -p /usr/local/share/nemoclaw /usr/local/share/ca-certificates \
&& { printf '%s' "${NEMOCLAW_CORPORATE_CA_B64}" | base64 --decode > /tmp/nemoclaw-corporate-ca.decoded 2>/dev/null \
|| { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 is not valid base64; expected a single-line base64-encoded PEM (#6210)" >&2; exit 1; }; } \
&& awk '/-----BEGIN CERTIFICATE-----/{f=1} f{print} /-----END CERTIFICATE-----/{f=0}' /tmp/nemoclaw-corporate-ca.decoded > /usr/local/share/nemoclaw/corporate-ca.pem \
&& rm -f /tmp/nemoclaw-corporate-ca.decoded \
&& { grep -qF -- "-----BEGIN CERTIFICATE-----" /usr/local/share/nemoclaw/corporate-ca.pem || { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 did not decode to a bundle of valid X.509 certificates (#6210)" >&2; exit 1; }; } \
&& { openssl crl2pkcs7 -nocrl -certfile /usr/local/share/nemoclaw/corporate-ca.pem >/dev/null 2>&1 || { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 did not decode to a bundle of valid X.509 certificates (#6210)" >&2; exit 1; }; } \
&& chown root:root /usr/local/share/nemoclaw/corporate-ca.pem \
&& chmod 0444 /usr/local/share/nemoclaw/corporate-ca.pem \
&& node -e 'const fs = require("node:fs"); const { X509Certificate } = require("node:crypto"); const pemPath = process.argv[1]; const anchorDir = process.argv[2]; const pem = fs.readFileSync(pemPath, "utf8"); const blocks = pem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g); if (!blocks?.length) process.exit(1); fs.writeFileSync(pemPath, blocks.map((block) => block.trim()).join("\n") + "\n"); blocks.forEach((block, index) => { if (!new X509Certificate(block).ca) process.exit(1); const name = anchorDir + "/nemoclaw-corporate-ca-" + String(index + 1).padStart(2, "0") + ".crt"; fs.writeFileSync(name, block.trim() + "\n"); });' /usr/local/share/nemoclaw/corporate-ca.pem /usr/local/share/ca-certificates \
&& chown root:root /usr/local/share/nemoclaw/corporate-ca.pem /usr/local/share/ca-certificates/nemoclaw-corporate-ca-*.crt \
&& chmod 0444 /usr/local/share/nemoclaw/corporate-ca.pem /usr/local/share/ca-certificates/nemoclaw-corporate-ca-*.crt \
&& update-ca-certificates \
&& echo "[nemoclaw] baked host corporate-proxy CA into image trust (#6210)"; \
fi

Expand Down
42 changes: 34 additions & 8 deletions agents/hermes/mcp-config-transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
)
MAX_ERROR_MESSAGE_LENGTH = 512
MAX_GATEWAY_PID_RECORD_BYTES = 4096
MCP_RACE_RECOVERY_ATTEMPTS = 3
GATEWAY_INTERNAL_PORT = 18642
GATEWAY_PUBLIC_PORT = 8642
BLOCKED_IPV4_NETWORKS = tuple(
Expand Down Expand Up @@ -591,6 +592,38 @@ def _restore_hash_snapshots(
raise RuntimeError(f"Failed to restore Hermes hash file {path}")


def _is_retryable_mcp_snapshot_race(error: Exception) -> bool:
message = str(error)
return "refusing raced runtime config path:" in message or (
"refusing raced Hermes MCP integrity snapshot" in message
)


def _recover_committed_apply_snapshot(
guard: ModuleType, privileged: bool, expected_text: str
) -> bool:
"""Reopen a bounded number of snapshots across an atomic hash replacement."""
compatibility_hash_path = os.path.join(HERMES_DIR, ".config-hash")
for attempt in range(MCP_RACE_RECOVERY_ATTEMPTS):
try:
integrity = guard.inspect_mcp_integrity_snapshot(
HERMES_DIR,
STRICT_HASH_PATH if privileged else compatibility_hash_path,
compatibility_hash_path if privileged else None,
)
if integrity.config_text != expected_text or integrity.state != "current":
return False
guard.assert_mcp_integrity_snapshot_current(integrity)
return True
except guard.UnsafePathError as recovery_error:
if (
not _is_retryable_mcp_snapshot_race(recovery_error)
or attempt + 1 == MCP_RACE_RECOVERY_ATTEMPTS
):
raise
return False


def apply_transaction(action: str, payload: dict[str, object]) -> bool:
_validate_payload(action, payload)
privileged = os.geteuid() == 0
Expand Down Expand Up @@ -695,14 +728,7 @@ def apply_transaction_and_reload(
# anchors are current, rolling it back would undo a live configuration.
if reload_completed:
try:
compatibility_hash_path = os.path.join(HERMES_DIR, ".config-hash")
integrity = guard.inspect_mcp_integrity_snapshot(
HERMES_DIR,
STRICT_HASH_PATH if privileged else compatibility_hash_path,
compatibility_hash_path if privileged else None,
)
if integrity.config_text == expected_text and integrity.state == "current":
guard.assert_mcp_integrity_snapshot_current(integrity)
if _recover_committed_apply_snapshot(guard, privileged, expected_text):
return {"ok": True, "changed": True, "reloaded": True}
except Exception as recovery_error:
logging.getLogger(__name__).warning(
Expand Down
11 changes: 7 additions & 4 deletions agents/langchain-deepagents-code/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
strings "$binary" | grep -Fq '/usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh'

# hadolint ignore=DL3006
FROM ${BASE_IMAGE}

Check warning on line 95 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 95 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 All @@ -106,15 +106,18 @@
# hadolint ignore=DL3059,DL4006
RUN if [ -n "${NEMOCLAW_CORPORATE_CA_B64}" ]; then \
command -v base64 >/dev/null 2>&1 || { echo "[nemoclaw] base64 is required to decode NEMOCLAW_CORPORATE_CA_B64 but is not installed in the build image" >&2; exit 1; }; \
install -d -o root -g root -m 0755 /usr/local/share/nemoclaw \
command -v update-ca-certificates >/dev/null 2>&1 || { echo "[nemoclaw] update-ca-certificates is required to anchor NEMOCLAW_CORPORATE_CA_B64 for the OpenShell proxy" >&2; exit 1; }; \
case "${NEMOCLAW_CORPORATE_CA_B64}" in *[!A-Za-z0-9+/=]*) echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 is not valid base64; expected a single-line base64-encoded PEM (#6210)" >&2; exit 1 ;; esac; \
install -d -o root -g root -m 0755 /usr/local/share/nemoclaw /usr/local/share/ca-certificates \
&& { printf '%s' "${NEMOCLAW_CORPORATE_CA_B64}" | base64 --decode > /tmp/nemoclaw-corporate-ca.decoded 2>/dev/null \
|| { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 is not valid base64; expected a single-line base64-encoded PEM (#6210)" >&2; exit 1; }; } \
&& awk '/-----BEGIN CERTIFICATE-----/{f=1} f{print} /-----END CERTIFICATE-----/{f=0}' /tmp/nemoclaw-corporate-ca.decoded > /usr/local/share/nemoclaw/corporate-ca.pem \
&& rm -f /tmp/nemoclaw-corporate-ca.decoded \
&& { node -e 'const fs = require("node:fs"); const { X509Certificate } = require("node:crypto"); const pem = fs.readFileSync(process.argv[1], "utf8"); const certificates = pem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g); if (!certificates?.length) process.exit(1); for (const certificate of certificates) if (!new X509Certificate(certificate).ca) process.exit(1);' /usr/local/share/nemoclaw/corporate-ca.pem \
&& { node -e 'const fs = require("node:fs"); const { X509Certificate } = require("node:crypto"); const pemPath = process.argv[1]; const anchorDir = process.argv[2]; const pem = fs.readFileSync(pemPath, "utf8"); const blocks = pem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g); if (!blocks?.length) process.exit(1); fs.writeFileSync(pemPath, blocks.map((block) => block.trim()).join("\n") + "\n"); blocks.forEach((block, index) => { if (!new X509Certificate(block).ca) process.exit(1); const name = anchorDir + "/nemoclaw-corporate-ca-" + String(index + 1).padStart(2, "0") + ".crt"; fs.writeFileSync(name, block.trim() + "\n"); });' /usr/local/share/nemoclaw/corporate-ca.pem /usr/local/share/ca-certificates \
|| { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 did not decode to a bundle of valid X.509 certificates with basicConstraints CA:TRUE (#6210)" >&2; exit 1; }; } \
&& chown root:root /usr/local/share/nemoclaw/corporate-ca.pem \
&& chmod 0444 /usr/local/share/nemoclaw/corporate-ca.pem \
&& chown root:root /usr/local/share/nemoclaw/corporate-ca.pem /usr/local/share/ca-certificates/nemoclaw-corporate-ca-*.crt \
&& chmod 0444 /usr/local/share/nemoclaw/corporate-ca.pem /usr/local/share/ca-certificates/nemoclaw-corporate-ca-*.crt \
&& update-ca-certificates \
&& echo "[nemoclaw] baked host corporate-proxy CA into DCode image trust (#6210)"; \
fi

Expand Down Expand Up @@ -291,7 +294,7 @@
&& env -i /usr/local/bin/dcode.real --version \
&& env -i /usr/local/bin/deepagents-code --version

ENV HOME=/sandbox \

Check warning on line 297 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 297 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
6 changes: 5 additions & 1 deletion docs/inference/custom-endpoint-security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,14 @@ Configure the provider credential separately instead of putting it in the endpoi
Managed provider defaults that do not provide an explicit custom endpoint through these paths are unaffected.

Custom endpoint onboarding has one narrower operator-controlled exception for corporate inference gateways.
Set `NEMOCLAW_TRUSTED_PRIVATE_INFERENCE_HOSTS` to a comma-separated list of exact hostnames or IP literals to admit an endpoint on RFC1918, CGNAT, or IPv6 ULA space.
Set `NEMOCLAW_TRUSTED_PRIVATE_HOSTS` to a comma-separated list of exact hostnames or IP literals to admit an endpoint on RFC1918, carrier-grade network address translation (CGNAT), or IPv6 unique local address space.
NemoClaw still resolves DNS, pins the validation connection, and rejects wildcard or suffix matches, link-local metadata, reserved destinations, and resolver failures.
This allowlist does not relax direct blueprint, `config set`, or unrelated persisted-URL validation.

`NEMOCLAW_TRUSTED_PRIVATE_INFERENCE_HOSTS` remains an inference-only compatibility alias.
Inference onboarding combines exact entries from the generic variable and the compatibility alias.
New configurations should use `NEMOCLAW_TRUSTED_PRIVATE_HOSTS`.

After onboarding records an admitted custom endpoint, `inference set` accepts that same canonical URL for a model change without resolving it again.
The registry must record onboarding as the endpoint source, and the supplied URL must match exactly after normalization.
Legacy entries without a source, endpoints recorded by `inference set`, and different URLs still pass through the full server-side request forgery validation path.
Expand Down
7 changes: 4 additions & 3 deletions docs/inference/set-up-openai-compatible-endpoint.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ NEMOCLAW_PROVIDER=custom \
| `NEMOCLAW_MODEL` | Model ID reported by the server. |
| `NEMOCLAW_COMPATIBLE_AUTH_MODE` | Set to `none` to explicitly select no authentication for an HTTP endpoint using `localhost`, `127.0.0.1`, or `[::1]` and port `8000`, `11434`, or `11435`. |
| `NEMOCLAW_REASONING` | Enables reasoning-only validation with the case-insensitive true values `true`, `1`, `yes`, and `y`. |
| `NEMOCLAW_TRUSTED_PRIVATE_INFERENCE_HOSTS` | Optional comma-separated exact hostnames or IP literals for operator-owned private inference endpoints. Wildcards are not supported. |
| `NEMOCLAW_TRUSTED_PRIVATE_HOSTS` | Optional comma-separated exact hostnames or IP literals for operator-owned private endpoints. Wildcards are not supported. |
| `NEMOCLAW_TRUSTED_PRIVATE_INFERENCE_HOSTS` | Inference-only compatibility alias for `NEMOCLAW_TRUSTED_PRIVATE_HOSTS`. Inference onboarding combines entries from both variables. |
| `COMPATIBLE_API_KEY` | Endpoint API key. Required unless loopback no-auth mode is selected. |

<AgentOnly variant="openclaw">
Expand All @@ -141,7 +142,7 @@ Private and reserved addresses are blocked by default.
To use an inference gateway on a trusted corporate network, list only its exact host and keep the endpoint URL on that host:

```bash
NEMOCLAW_TRUSTED_PRIVATE_INFERENCE_HOSTS=llm.corp.example \
NEMOCLAW_TRUSTED_PRIVATE_HOSTS=llm.corp.example \
NEMOCLAW_PROVIDER=custom \
NEMOCLAW_ENDPOINT_URL=https://llm.corp.example/v1 \
NEMOCLAW_MODEL=your-model \
Expand All @@ -150,7 +151,7 @@ NEMOCLAW_TRUSTED_PRIVATE_INFERENCE_HOSTS=llm.corp.example \
```

NemoClaw still resolves the host before probing and pins the probe to the resolved address.
Only RFC1918, CGNAT, and IPv6 ULA destinations can be admitted; link-local metadata and other reserved ranges remain blocked.
Only RFC1918, carrier-grade network address translation (CGNAT), and IPv6 unique local address (ULA) destinations can be admitted; link-local metadata and other reserved ranges remain blocked.
An unlisted private host, a hostname suffix match, or a DNS failure also remains blocked.

## Related Topics
Expand Down
Loading
Loading