Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -1513,6 +1513,13 @@
"$nemoclaw_command_assertion" "$nemoclaw_command_status" >&2; \
exit 1; \
}; \
if find -P /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime -exec chown -h root:root '{}' + \
&& find -P /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime -type d -exec chmod 0555 '{}' + \
&& find -P /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime -type f -exec chmod 0444 '{}' +; then \
:; \
else \
managed_image_command_failed mcp-tool-discovery-tree-permission-replay "$?"; \
fi; \
discovery_contract="$(node /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime/mcp-tool-discovery.mjs)" \
|| managed_image_command_failed mcp-tool-discovery-bundle-execution "$?"; \
node -e 'const expected = { protocol: 1, ok: false, detail: "tool discovery received invalid runtime arguments" }; const standaloneSecretPatterns = [/(?:nvapi-|nvcf-|gh[pousr]_|sk-proj-|sk-ant-|hf_|glpat-|gsk_|pypi-|tvly-)[A-Za-z0-9_-]{10,}/gu, /github_pat_[A-Za-z0-9_]{30,}/gu, /sk-[A-Za-z0-9_-]{20,}/gu, /(?:xox[bpas]|xapp)-[A-Za-z0-9-]{10,}/gu, /A(?:K|S)IA[A-Z0-9]{16}/gu, /\bbot\d{8,10}:[A-Za-z0-9_-]{35}\b/gu, /\b\d{8,10}:[A-Za-z0-9_-]{35}\b/gu, /\b[A-Za-z0-9]{24}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,}\b/gu, /lsv2_(?:pt|sk)_[A-Za-z0-9]{10,}(?:_[A-Za-z0-9]+)*/gu, /\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{10,}\b/gu, /\b[A-Za-z0-9_=-]{32,}\b/gu]; const redactContextSecrets = (value) => value.replace(/\b(?:Bearer|Basic)\s+\S+/giu, "<REDACTED>").replace(/((?:^|[^A-Za-z0-9])(?:[A-Za-z0-9]{1,128}_(?:KEY|TOKEN|SECRET|CREDENTIAL|PASSWORD|PASSWD|PASS)|(?:X[-_])?API[-_]KEY|TOKEN|SECRET|CREDENTIAL|PASSWORD|PASSWD|PASS)["\x27]?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["\x27]?)[^\s"\x27]{10,}/giu, (_match, prefix) => prefix + "<REDACTED>").replace(/((?:^|[^A-Za-z0-9])(?:[A-Za-z0-9]{1,128}(?:Token|Secret|Credential)|[A-Za-z0-9]{0,128}(?:[Aa]ccess|[Rr]efresh|[Cc]lient|[Bb]earer|[Aa]uth|[Aa][Pp][Ii]|[Pp]rivate|[Ss]igning|[Ss]ession|[Bb]ot|[Aa]pp|[Rr]esolved)Key|[A-Za-z0-9]{1,128}(?:Password|Passwd|Pass))["\x27]?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["\x27]?)[^\s"\x27]{10,}/gu, (_match, prefix) => prefix + "<REDACTED>").replace(/((?:^|[^A-Za-z0-9])KEY["\x27]?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["\x27]?)[^\s"\x27]{10,}/gu, (_match, prefix) => prefix + "<REDACTED>"); const sanitize = (value) => { if (value === undefined) return "<missing>"; if (value === null || typeof value === "boolean" || typeof value === "number") return value; if (typeof value !== "string") return "<" + (Array.isArray(value) ? "array" : typeof value) + ">"; let printable = value.replace(/-----BEGIN (?:[A-Z0-9]+ )?PRIVATE KEY-----[\s\S]*/gu, "<REDACTED>").replace(/[^\x20-\x7e]/gu, "?"); for (const pattern of standaloneSecretPatterns) printable = printable.replace(pattern, "<REDACTED>"); printable = redactContextSecrets(printable); return printable.length <= 240 ? printable : printable.slice(0, 237) + "..."; }; let result; let parsed = true; try { result = JSON.parse(process.argv[1]); } catch { parsed = false; } const record = parsed && result !== null && typeof result === "object" && !Array.isArray(result) ? result : undefined; if (record && record.protocol === expected.protocol && record.ok === expected.ok && record.detail === expected.detail) process.exit(0); const actual = record ? { protocol: sanitize(record.protocol), ok: sanitize(record.ok), detail: sanitize(record.detail) } : parsed ? { type: result === null ? "null" : Array.isArray(result) ? "array" : typeof result, value: sanitize(result) } : { type: "invalid-json", preview: sanitize(process.argv[1]) }; console.error("ERROR: managed image assertion failed: mcp-tool-discovery-json-contract actual=%s expected=%s", JSON.stringify(actual), JSON.stringify(expected)); process.exit(1);' "$discovery_contract" \
Expand Down Expand Up @@ -1636,10 +1643,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 1646 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/

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

Check warning on line 1649 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 @@ -1673,7 +1680,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 1683 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/

Check warning on line 1683 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 1683 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 1683 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
7 changes: 7 additions & 0 deletions agents/hermes/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,13 @@ RUN managed_runtime_assertion_failed() { \
"$nemoclaw_command_assertion" "$nemoclaw_command_status" >&2; \
exit 1; \
}; \
if find -P /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime -exec chown -h root:root '{}' + \
&& find -P /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime -type d -exec chmod 0555 '{}' + \
&& find -P /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime -type f -exec chmod 0444 '{}' +; then \
:; \
else \
managed_image_command_failed mcp-tool-discovery-tree-permission-replay "$?"; \
fi; \
discovery_contract="$(node /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime/mcp-tool-discovery.mjs)" \
|| managed_image_command_failed mcp-tool-discovery-bundle-execution "$?"; \
node -e 'const expected = { protocol: 1, ok: false, detail: "tool discovery received invalid runtime arguments" }; const standaloneSecretPatterns = [/(?:nvapi-|nvcf-|gh[pousr]_|sk-proj-|sk-ant-|hf_|glpat-|gsk_|pypi-|tvly-)[A-Za-z0-9_-]{10,}/gu, /github_pat_[A-Za-z0-9_]{30,}/gu, /sk-[A-Za-z0-9_-]{20,}/gu, /(?:xox[bpas]|xapp)-[A-Za-z0-9-]{10,}/gu, /A(?:K|S)IA[A-Z0-9]{16}/gu, /\bbot\d{8,10}:[A-Za-z0-9_-]{35}\b/gu, /\b\d{8,10}:[A-Za-z0-9_-]{35}\b/gu, /\b[A-Za-z0-9]{24}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,}\b/gu, /lsv2_(?:pt|sk)_[A-Za-z0-9]{10,}(?:_[A-Za-z0-9]+)*/gu, /\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{10,}\b/gu, /\b[A-Za-z0-9_=-]{32,}\b/gu]; const redactContextSecrets = (value) => value.replace(/\b(?:Bearer|Basic)\s+\S+/giu, "<REDACTED>").replace(/((?:^|[^A-Za-z0-9])(?:[A-Za-z0-9]{1,128}_(?:KEY|TOKEN|SECRET|CREDENTIAL|PASSWORD|PASSWD|PASS)|(?:X[-_])?API[-_]KEY|TOKEN|SECRET|CREDENTIAL|PASSWORD|PASSWD|PASS)["\x27]?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["\x27]?)[^\s"\x27]{10,}/giu, (_match, prefix) => prefix + "<REDACTED>").replace(/((?:^|[^A-Za-z0-9])(?:[A-Za-z0-9]{1,128}(?:Token|Secret|Credential)|[A-Za-z0-9]{0,128}(?:[Aa]ccess|[Rr]efresh|[Cc]lient|[Bb]earer|[Aa]uth|[Aa][Pp][Ii]|[Pp]rivate|[Ss]igning|[Ss]ession|[Bb]ot|[Aa]pp|[Rr]esolved)Key|[A-Za-z0-9]{1,128}(?:Password|Passwd|Pass))["\x27]?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["\x27]?)[^\s"\x27]{10,}/gu, (_match, prefix) => prefix + "<REDACTED>").replace(/((?:^|[^A-Za-z0-9])KEY["\x27]?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["\x27]?)[^\s"\x27]{10,}/gu, (_match, prefix) => prefix + "<REDACTED>"); const sanitize = (value) => { if (value === undefined) return "<missing>"; if (value === null || typeof value === "boolean" || typeof value === "number") return value; if (typeof value !== "string") return "<" + (Array.isArray(value) ? "array" : typeof value) + ">"; let printable = value.replace(/-----BEGIN (?:[A-Z0-9]+ )?PRIVATE KEY-----[\s\S]*/gu, "<REDACTED>").replace(/[^\x20-\x7e]/gu, "?"); for (const pattern of standaloneSecretPatterns) printable = printable.replace(pattern, "<REDACTED>"); printable = redactContextSecrets(printable); return printable.length <= 240 ? printable : printable.slice(0, 237) + "..."; }; let result; let parsed = true; try { result = JSON.parse(process.argv[1]); } catch { parsed = false; } const record = parsed && result !== null && typeof result === "object" && !Array.isArray(result) ? result : undefined; if (record && record.protocol === expected.protocol && record.ok === expected.ok && record.detail === expected.detail) process.exit(0); const actual = record ? { protocol: sanitize(record.protocol), ok: sanitize(record.ok), detail: sanitize(record.detail) } : parsed ? { type: result === null ? "null" : Array.isArray(result) ? "array" : typeof result, value: sanitize(result) } : { type: "invalid-json", preview: sanitize(process.argv[1]) }; console.error("ERROR: managed image assertion failed: mcp-tool-discovery-json-contract actual=%s expected=%s", JSON.stringify(actual), JSON.stringify(expected)); process.exit(1);' "$discovery_contract" \
Expand Down
7 changes: 7 additions & 0 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 @@ -125,6 +125,13 @@
"$nemoclaw_command_assertion" "$nemoclaw_command_status" >&2; \
exit 1; \
}; \
if find -P /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime -exec chown -h root:root '{}' + \
&& find -P /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime -type d -exec chmod 0555 '{}' + \
&& find -P /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime -type f -exec chmod 0444 '{}' +; then \
:; \
else \
managed_image_command_failed mcp-tool-discovery-tree-permission-replay "$?"; \
fi; \
discovery_contract="$(node /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime/mcp-tool-discovery.mjs)" \
|| managed_image_command_failed mcp-tool-discovery-bundle-execution "$?"; \
node -e 'const expected = { protocol: 1, ok: false, detail: "tool discovery received invalid runtime arguments" }; const standaloneSecretPatterns = [/(?:nvapi-|nvcf-|gh[pousr]_|sk-proj-|sk-ant-|hf_|glpat-|gsk_|pypi-|tvly-)[A-Za-z0-9_-]{10,}/gu, /github_pat_[A-Za-z0-9_]{30,}/gu, /sk-[A-Za-z0-9_-]{20,}/gu, /(?:xox[bpas]|xapp)-[A-Za-z0-9-]{10,}/gu, /A(?:K|S)IA[A-Z0-9]{16}/gu, /\bbot\d{8,10}:[A-Za-z0-9_-]{35}\b/gu, /\b\d{8,10}:[A-Za-z0-9_-]{35}\b/gu, /\b[A-Za-z0-9]{24}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,}\b/gu, /lsv2_(?:pt|sk)_[A-Za-z0-9]{10,}(?:_[A-Za-z0-9]+)*/gu, /\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{10,}\b/gu, /\b[A-Za-z0-9_=-]{32,}\b/gu]; const redactContextSecrets = (value) => value.replace(/\b(?:Bearer|Basic)\s+\S+/giu, "<REDACTED>").replace(/((?:^|[^A-Za-z0-9])(?:[A-Za-z0-9]{1,128}_(?:KEY|TOKEN|SECRET|CREDENTIAL|PASSWORD|PASSWD|PASS)|(?:X[-_])?API[-_]KEY|TOKEN|SECRET|CREDENTIAL|PASSWORD|PASSWD|PASS)["\x27]?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["\x27]?)[^\s"\x27]{10,}/giu, (_match, prefix) => prefix + "<REDACTED>").replace(/((?:^|[^A-Za-z0-9])(?:[A-Za-z0-9]{1,128}(?:Token|Secret|Credential)|[A-Za-z0-9]{0,128}(?:[Aa]ccess|[Rr]efresh|[Cc]lient|[Bb]earer|[Aa]uth|[Aa][Pp][Ii]|[Pp]rivate|[Ss]igning|[Ss]ession|[Bb]ot|[Aa]pp|[Rr]esolved)Key|[A-Za-z0-9]{1,128}(?:Password|Passwd|Pass))["\x27]?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["\x27]?)[^\s"\x27]{10,}/gu, (_match, prefix) => prefix + "<REDACTED>").replace(/((?:^|[^A-Za-z0-9])KEY["\x27]?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["\x27]?)[^\s"\x27]{10,}/gu, (_match, prefix) => prefix + "<REDACTED>"); const sanitize = (value) => { if (value === undefined) return "<missing>"; if (value === null || typeof value === "boolean" || typeof value === "number") return value; if (typeof value !== "string") return "<" + (Array.isArray(value) ? "array" : typeof value) + ">"; let printable = value.replace(/-----BEGIN (?:[A-Z0-9]+ )?PRIVATE KEY-----[\s\S]*/gu, "<REDACTED>").replace(/[^\x20-\x7e]/gu, "?"); for (const pattern of standaloneSecretPatterns) printable = printable.replace(pattern, "<REDACTED>"); printable = redactContextSecrets(printable); return printable.length <= 240 ? printable : printable.slice(0, 237) + "..."; }; let result; let parsed = true; try { result = JSON.parse(process.argv[1]); } catch { parsed = false; } const record = parsed && result !== null && typeof result === "object" && !Array.isArray(result) ? result : undefined; if (record && record.protocol === expected.protocol && record.ok === expected.ok && record.detail === expected.detail) process.exit(0); const actual = record ? { protocol: sanitize(record.protocol), ok: sanitize(record.ok), detail: sanitize(record.detail) } : parsed ? { type: result === null ? "null" : Array.isArray(result) ? "array" : typeof result, value: sanitize(result) } : { type: "invalid-json", preview: sanitize(process.argv[1]) }; console.error("ERROR: managed image assertion failed: mcp-tool-discovery-json-contract actual=%s expected=%s", JSON.stringify(actual), JSON.stringify(expected)); process.exit(1);' "$discovery_contract" \
Expand Down Expand Up @@ -302,7 +309,7 @@
&& env -i /usr/local/bin/dcode.real --version \
&& env -i /usr/local/bin/deepagents-code --version

ENV HOME=/sandbox \

Check warning on line 312 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 312 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
110 changes: 109 additions & 1 deletion test/support/managed-bootstrap-image-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@
const DISCOVERY_RUNTIME_PATH = `${DISCOVERY_RUNTIME_ROOT}/mcp-tool-discovery.mjs`;
const DISCOVERY_EXPECTED_CONTRACT =
'{"protocol":1,"ok":false,"detail":"tool discovery received invalid runtime arguments"}';
const REVIEWED_DISCOVERY_RUNTIME_ROOT = path.join(
import.meta.dirname,
"..",
"..",
"tools",
"mcp-tool-discovery-runtime",
"reviewed-runtime-bundle",
"mcp-tool-discovery",
);
const REVIEWED_DISCOVERY_RUNTIME_FILES = [
["BUNDLED_PACKAGES.json", "BUNDLED_PACKAGES.json"],
["THIRD_PARTY_LICENSES.txt", "THIRD_PARTY_LICENSES.txt"],
["mcp-tool-discovery.bundle", "mcp-tool-discovery.mjs"],
] as const;
const MANAGED_STARTUP_RUNTIME_PATH = "/usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs";

function expectManagedRuntimeDiagnostic(dockerfile: string): void {
Expand All @@ -54,7 +68,17 @@
discoveryStart,
);
expect(managedRuntimeStart).toBeGreaterThan(discoveryStart);
const functionSource = logicalInstruction.slice(0, discoveryStart).trim();
const permissionReplayStart = logicalInstruction.indexOf(
`if find -P ${DISCOVERY_RUNTIME_ROOT} -exec chown -h root:root '{}' +`,
);
expect(permissionReplayStart).toBeGreaterThan(0);
expect(permissionReplayStart).toBeLessThan(discoveryStart);
const functionSource = logicalInstruction.slice(0, permissionReplayStart).trim();
const permissionReplaySource = logicalInstruction
.slice(permissionReplayStart, discoveryStart)
.trim();
const treeSafetyStart = logicalInstruction.indexOf("discovery_unsafe=", discoveryStart);
expect(treeSafetyStart).toBeGreaterThan(discoveryStart);
const discoverySource = logicalInstruction.slice(discoveryStart, managedRuntimeStart).trim();

for (const fragment of [
Expand All @@ -68,6 +92,10 @@
}

for (const assertion of [
`find -P ${DISCOVERY_RUNTIME_ROOT} -exec chown -h root:root '{}' +`,
`find -P ${DISCOVERY_RUNTIME_ROOT} -type d -exec chmod 0555 '{}' +`,
`find -P ${DISCOVERY_RUNTIME_ROOT} -type f -exec chmod 0444 '{}' +`,
'managed_image_command_failed mcp-tool-discovery-tree-permission-replay "$?"',
`discovery_contract="$(node ${DISCOVERY_RUNTIME_PATH})" || managed_image_command_failed mcp-tool-discovery-bundle-execution "$?"`,
"ERROR: managed image assertion failed: mcp-tool-discovery-json-contract actual=%s expected=%s",
`discovery_unsafe="$(find -L ${DISCOVERY_RUNTIME_ROOT} \\( ! -user root -o -perm /022 \\) -print -quit)" || managed_image_command_failed mcp-tool-discovery-tree-find-execution "$?"`,
Expand All @@ -85,8 +113,36 @@
const missingPath = path.join(tmp, "missing-runtime.cjs");
const targetPath = path.join(tmp, "runtime-target.cjs");
const linkPath = path.join(tmp, "runtime-link.cjs");
const permissionReplayRoot = path.join(tmp, "mcp-tool-discovery-runtime");
const permissionReplayPaths = REVIEWED_DISCOVERY_RUNTIME_FILES.map(([, installedName]) =>
path.join(permissionReplayRoot, installedName),
);
const permissionReplayBundlePath = path.join(permissionReplayRoot, "mcp-tool-discovery.mjs");
const externalPermissionTarget = path.join(tmp, "external-reviewed-artifact.json");
const externalPermissionLink = path.join(permissionReplayRoot, "external-reviewed-artifact.json");
fs.writeFileSync(targetPath, "fixture\n", { mode: 0o444 });
fs.symlinkSync(targetPath, linkPath);
fs.mkdirSync(permissionReplayRoot, { mode: 0o775 });
for (const [reviewedName, installedName] of REVIEWED_DISCOVERY_RUNTIME_FILES) {
const installedPath = path.join(permissionReplayRoot, installedName);
fs.copyFileSync(path.join(REVIEWED_DISCOVERY_RUNTIME_ROOT, reviewedName), installedPath);
fs.chmodSync(installedPath, 0o664);
}
fs.chmodSync(permissionReplayRoot, 0o775);
fs.copyFileSync(
path.join(REVIEWED_DISCOVERY_RUNTIME_ROOT, "BUNDLED_PACKAGES.json"),
externalPermissionTarget,
);
fs.chmodSync(externalPermissionTarget, 0o664);
fs.symlinkSync(externalPermissionTarget, externalPermissionLink);
const permissionReplayContents = new Map(
permissionReplayPaths.map((artifactPath) => [artifactPath, fs.readFileSync(artifactPath)]),
);
const permissionReplayForHost = permissionReplaySource
.replaceAll(DISCOVERY_RUNTIME_ROOT, permissionReplayRoot)
// The source contract pins root:root. Use the current identity so this
// extracted production command can also run on an unprivileged test host.
.replaceAll("root:root", '"$(id -u):$(id -g)"');
const runDiscoveryChecks = ({
discoveryOutput = DISCOVERY_EXPECTED_CONTRACT,
discoveryStatus = 0,
Expand Down Expand Up @@ -166,8 +222,59 @@
},
},
);
const runPermissionReplay = (findStatus?: number) =>
spawnSync(
"sh",
[
"-c",
[
...(findStatus === undefined ? [] : [`find() { return ${findStatus}; }`]),
functionSource,
permissionReplayForHost,
`${JSON.stringify(process.execPath)} ${JSON.stringify(permissionReplayBundlePath)}`,
].join("\n"),
],
{
encoding: "utf-8",
env: { PATH: process.env.PATH ?? "" },
},
);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

try {
const permissionReplayFailure = runPermissionReplay(43);
expect(permissionReplayFailure.status).toBe(1);
expect(permissionReplayFailure.stdout).toBe("");
expect(permissionReplayFailure.stderr).toBe(
"ERROR: managed image assertion failed: mcp-tool-discovery-tree-permission-replay exit-status=43\n",
);
expect(fs.statSync(path.join(permissionReplayRoot, "BUNDLED_PACKAGES.json")).mode & 0o777).toBe(
0o664,
);

const permissionReplay = runPermissionReplay();
expect(permissionReplay.status, permissionReplay.stderr).toBe(0);
expect(permissionReplay.stderr).toBe("");
expect(JSON.parse(permissionReplay.stdout)).toMatchObject({
protocol: 1,
ok: false,
detail: "tool discovery received invalid runtime arguments",
});
expect(fs.statSync(permissionReplayRoot).mode & 0o777).toBe(0o555);
const expectedUid = process.getuid?.() ?? 0;
const expectedGid = process.getgid?.() ?? 0;
for (const artifactPath of [permissionReplayRoot, ...permissionReplayPaths]) {
const artifact = fs.statSync(artifactPath);
expect(artifact.uid).toBe(expectedUid);
expect(artifact.gid).toBe(expectedGid);
expect(artifact.mode & 0o022).toBe(0);
}
for (const artifactPath of permissionReplayPaths) {
expect(fs.statSync(artifactPath).mode & 0o777).toBe(0o444);
expect(fs.readFileSync(artifactPath)).toEqual(permissionReplayContents.get(artifactPath));
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
}
expect(fs.lstatSync(externalPermissionLink).isSymbolicLink()).toBe(true);
expect(fs.statSync(externalPermissionTarget).mode & 0o777).toBe(0o664);

const bundleFailure = runDiscoveryChecks({
discoveryOutput: "output must remain private",
discoveryStatus: 23,
Expand Down Expand Up @@ -312,6 +419,7 @@
`ERROR: managed image assertion failed: non-symlink path=${linkPath} uid=0 gid=0 type=symbolic link mode=777 symlink=yes\n`,
);
} finally {
fs.chmodSync(permissionReplayRoot, 0o755);
fs.rmSync(tmp, { force: true, recursive: true });
}
}
Expand Down
Loading