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
314 changes: 314 additions & 0 deletions .github/workflows/managed-images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,229 @@ env:
REGISTRY: ghcr.io

jobs:
pr-staging-qa-deep-code:
name: Staging QA base permission regression (Deep Agents Code)
if: github.event_name == 'pull_request'
runs-on: ubuntu-22.04
timeout-minutes: 90
permissions:
contents: read
env:
CANDIDATE_SHA: ${{ github.event.pull_request.head.sha }}
STAGING_PRODUCER_SHA: cd0cb2b89965d57cd3d7d97a30d4bdab26781b61
STAGING_QA_SOURCE_SHA: d097a22145859102c0495b0310de264b7a27624f
STAGING_QA_RECORDED_INDEX_DIGEST: sha256:ceaa94a895ba5cb3f231074b97f9910df3ce9d375802cb1d21c777691e0feb43
STAGING_QA_BASE_IMAGE: nemoclaw-deepagents-code-base:staging-31396519688
STAGING_QA_FINAL_IMAGE: nemoclaw-managed-pr/langchain-deepagents-code-staging-qa
steps:
- name: Checkout latest PR commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
path: candidate
persist-credentials: false

# Run 31396519688 built this source locally and never published it. Keep
# the recorded local-only index digest as provenance. Rebuild the base from
# the exact source with Dockerfile.base and its repository build context.
- name: Checkout exact staging QA base source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.STAGING_QA_SOURCE_SHA }}
path: staging-qa-base-source
persist-credentials: false

- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22.19.0

- name: Validate staging QA source and recorded digest format
shell: bash
run: |
set -euo pipefail
source_root="$GITHUB_WORKSPACE/staging-qa-base-source"
actual_source_sha="$(git -C "$source_root" rev-parse HEAD)"
if [ "$actual_source_sha" != "$STAGING_QA_SOURCE_SHA" ]; then
echo "ERROR: staging QA base source does not match the recorded NemoClaw commit." >&2
exit 1
fi
if [[ ! "$STAGING_QA_RECORDED_INDEX_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "ERROR: staging QA base provenance has an invalid recorded index digest." >&2
exit 1
fi
printf '### Staging run 31396519688 Deep Agents Code QA base\n\n'
printf 'Producer commit: %s\n\n' "$STAGING_PRODUCER_SHA"
printf 'NemoClaw source commit: %s\n\n' "$STAGING_QA_SOURCE_SHA"
printf 'Recorded local-only index digest: %s\n' "$STAGING_QA_RECORDED_INDEX_DIGEST"
{
printf '### Staging run 31396519688 Deep Agents Code QA base\n\n'
printf 'Producer commit: %s\n\n' "$STAGING_PRODUCER_SHA"
printf 'NemoClaw source commit: %s\n\n' "$STAGING_QA_SOURCE_SHA"
printf 'Recorded local-only index digest: %s\n' "$STAGING_QA_RECORDED_INDEX_DIGEST"
} >> "$GITHUB_STEP_SUMMARY"

- name: Reproduce staging discovery permission drift
shell: bash
working-directory: candidate
run: |
set -euo pipefail
reviewed_root="tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery"
for artifact in \
BUNDLED_PACKAGES.json \
THIRD_PARTY_LICENSES.txt \
mcp-tool-discovery.bundle
do
artifact_path="${reviewed_root}/${artifact}"
if [ ! -f "$artifact_path" ] || [ -L "$artifact_path" ]; then
echo "ERROR: reviewed discovery permission fixture must be a regular non-symlink: ${artifact_path}" >&2
exit 1
fi
chmod 0664 "$artifact_path"
done

- name: Rebuild staging QA Deep Agents Code base from exact source
id: staging-base
shell: bash
env:
DOCKER_BUILDKIT: "1"
run: |
set -euo pipefail
source_root="$GITHUB_WORKSPACE/staging-qa-base-source"
docker build --progress=plain \
-f "$source_root/agents/langchain-deepagents-code/Dockerfile.base" \
-t "$STAGING_QA_BASE_IMAGE" \
"$source_root"
base_id="$(docker image inspect --format "{{.Id}}" "$STAGING_QA_BASE_IMAGE")"
if [[ ! "$base_id" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "ERROR: rebuilt staging QA base did not resolve to an immutable local image ID." >&2
exit 1
fi
printf 'id=%s\n' "$base_id" >> "$GITHUB_OUTPUT"
printf '\nRebuilt local image: %s\n' "$base_id" >> "$GITHUB_STEP_SUMMARY"

- name: Build latest PR commit against reproduced staging QA base
id: staging-final
shell: bash
working-directory: candidate
env:
DOCKER_BUILDKIT: "1"
run: |
set -euo pipefail
final_reference="${STAGING_QA_FINAL_IMAGE}:${CANDIDATE_SHA}"
docker build --progress=plain \
-f agents/langchain-deepagents-code/Dockerfile \
--build-arg BASE_IMAGE="$STAGING_QA_BASE_IMAGE" \
--build-arg NEMOCLAW_MODEL=nvidia/nemotron-3-ultra-550b-a55b \
--build-arg NEMOCLAW_INFERENCE_PROVIDER_ID=inference \
--build-arg NEMOCLAW_UPSTREAM_PROVIDER=nvidia-nim \
--build-arg NEMOCLAW_INFERENCE_BASE_URL=https://inference.local/v1 \
--build-arg NEMOCLAW_INFERENCE_API=openai-completions \
--build-arg NEMOCLAW_DCODE_AUTO_APPROVAL=thread-opt-in \
--build-arg NEMOCLAW_BUILD_ID="$STAGING_QA_SOURCE_SHA" \
--build-arg NEMOCLAW_DARWIN_VM_COMPAT=0 \
-t "$final_reference" \
.
final_id="$(docker image inspect --format "{{.Id}}" "$final_reference")"
if [[ ! "$final_id" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "ERROR: staging QA final image did not resolve to an immutable local image ID." >&2
exit 1
fi
printf 'id=%s\n' "$final_id" >> "$GITHUB_OUTPUT"

- name: Validate staging QA final image contract
shell: bash
env:
BASE_ID: ${{ steps.staging-base.outputs.id }}
FINAL_ID: ${{ steps.staging-final.outputs.id }}
run: |
set -euo pipefail
base_layers="$(docker image inspect --format "{{json .RootFS.Layers}}" "$BASE_ID")"
final_layers="$(docker image inspect --format "{{json .RootFS.Layers}}" "$FINAL_ID")"
if ! jq -n -e \
--argjson base "$base_layers" \
--argjson final "$final_layers" '
($base | length) > 0 and
($final | length) >= ($base | length) and
all(range(0; $base | length); $final[.] == $base[.])
' >/dev/null; then
echo "ERROR: staging QA final image does not extend the rebuilt QA base." >&2
exit 1
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if ! actual_discovery_contract="$(
docker run --rm --platform linux/amd64 --entrypoint /bin/sh "$FINAL_ID" -c '
set -eu
discovery_runtime=/usr/local/lib/nemoclaw/mcp-tool-discovery-runtime
if ! entry_owner_unsafe="$(find -P "$discovery_runtime" ! -user root -print -quit)"; then
echo "ERROR: staging QA discovery entry ownership inspection failed." >&2
exit 1
fi
if [ -n "$entry_owner_unsafe" ]; then
echo "ERROR: staging QA discovery entry is not root-owned: $entry_owner_unsafe" >&2
exit 1
fi
if ! owner_or_write_unsafe="$(
find -L "$discovery_runtime" \
\( ! -user root -o -perm /022 \) -print -quit
)"; then
echo "ERROR: staging QA discovery tree inspection failed." >&2
exit 1
fi
if [ -n "$owner_or_write_unsafe" ]; then
echo "ERROR: staging QA discovery tree is not root-owned and read-only: $owner_or_write_unsafe" >&2
exit 1
fi
if ! mode_unsafe="$(
find -P "$discovery_runtime" \
\( \( -type d ! -perm 0555 \) -o \( -type f ! -perm 0444 \) \) \
-print -quit
)"; then
echo "ERROR: staging QA discovery mode inspection failed." >&2
exit 1
fi
if [ -n "$mode_unsafe" ]; then
echo "ERROR: staging QA discovery path has an unexpected mode: $mode_unsafe" >&2
exit 1
fi
if ! actual_discovery_contract="$(node "$discovery_runtime/mcp-tool-discovery.mjs")"; then
echo "ERROR: staging QA discovery bundle execution failed." >&2
exit 1
fi
printf "%s" "$actual_discovery_contract"
'
)"; then
echo "ERROR: staging QA final image discovery contract probe failed." >&2
exit 1
fi
if ! node -e '
const expected = {
protocol: 1,
ok: false,
detail: "tool discovery received invalid runtime arguments",
};
let result;
try {
result = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
} catch {
process.exit(1);
}
const record =
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(1);
}
' <<< "$actual_discovery_contract" >/dev/null; then
echo "ERROR: staging QA discovery bundle contract mismatch." >&2
exit 1
fi

pr-build-and-entrypoint:
name: PR build and direct managed startup (${{ matrix.display_name }})
# External contributor PRs retain the local build/direct-startup lane after
Expand Down Expand Up @@ -94,6 +317,24 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false

- name: Reproduce reviewed discovery permission drift
shell: bash
run: |
set -euo pipefail
reviewed_root="tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery"
for artifact in \
BUNDLED_PACKAGES.json \
THIRD_PARTY_LICENSES.txt \
mcp-tool-discovery.bundle
do
artifact_path="${reviewed_root}/${artifact}"
if [ ! -f "$artifact_path" ] || [ -L "$artifact_path" ]; then
echo "ERROR: reviewed discovery permission fixture must be a regular non-symlink: ${artifact_path}" >&2
exit 1
fi
chmod 0664 "$artifact_path"
done

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0

Expand Down Expand Up @@ -241,6 +482,79 @@ jobs:
exit 1
fi

discovery_runtime="/usr/local/lib/nemoclaw/mcp-tool-discovery-runtime"
if ! actual_discovery_contract="$(
docker run --rm --platform "$PLATFORM" --entrypoint /bin/sh "$image_id" -c '
set -eu
discovery_runtime="$1"
if ! entry_owner_unsafe="$(
find -P "$discovery_runtime" ! -user root -print -quit
)"; then
echo "ERROR: final PR managed image discovery entry ownership inspection failed." >&2
exit 1
fi
if [ -n "$entry_owner_unsafe" ]; then
echo "ERROR: final PR managed image discovery entry is not root-owned: $entry_owner_unsafe" >&2
exit 1
fi
if ! owner_or_write_unsafe="$(
find -L "$discovery_runtime" \
\( ! -user root -o -perm /022 \) -print -quit
)"; then
echo "ERROR: final PR managed image discovery tree inspection failed." >&2
exit 1
fi
if [ -n "$owner_or_write_unsafe" ]; then
echo "ERROR: final PR managed image discovery tree is not root-owned and read-only: $owner_or_write_unsafe" >&2
exit 1
fi
if ! mode_unsafe="$(
find -P "$discovery_runtime" \
\( \( -type d ! -perm 0555 \) -o \( -type f ! -perm 0444 \) \) \
-print -quit
)"; then
echo "ERROR: final PR managed image discovery mode inspection failed." >&2
exit 1
fi
if [ -n "$mode_unsafe" ]; then
echo "ERROR: final PR managed image discovery path has an unexpected mode: $mode_unsafe" >&2
exit 1
fi
node "$discovery_runtime/mcp-tool-discovery.mjs"
' -- "$discovery_runtime"
)"; then
echo "ERROR: final PR managed image discovery contract probe failed." >&2
exit 1
fi
if ! node -e '
const expected = {
protocol: 1,
ok: false,
detail: "tool discovery received invalid runtime arguments",
};
let result;
try {
result = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
} catch {
process.exit(1);
}
const record =
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(1);
}
' <<< "$actual_discovery_contract" >/dev/null; then
echo "ERROR: final PR managed image discovery bundle contract mismatch." >&2
exit 1
fi

printf 'reference=%s\n' "$image_id" >> "$GITHUB_OUTPUT"
printf '### %s exact PR managed image\n\n`%s`\n' "$AGENT" "$image_id" \
>> "$GITHUB_STEP_SUMMARY"
Expand Down
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
Loading
Loading