diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index f1d1bc91f35..c1761a91a3e 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -348,6 +348,23 @@ jobs: - name: Prepare E2E workspace uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@50281ee84c4a6fc759da95ea28fc0b7d9c378a28 + # invalidState: a profile plugin installed with --no-deps can import even + # when an incomplete base image omitted its required upstream packages. + # sourceBoundary: this trusted workflow scopes the repo-owned stripped-base + # build to the exact DCode target; the production Dockerfile must reject it + # at the isolated import gate before its later dependency-consistency check. + # whyNotSourceFix: dependency completeness belongs to the hash-locked base; + # resolving dependencies during local plugin install would duplicate that + # trust boundary, so the regression constructs the invalid input instead. + # regressionTest: workflow-boundary tests pin the target, script, and + # ordering; build-gate tests pin the base build and failure contract. + # removalCondition: remove only if package installation no longer uses + # --no-deps or an equivalent earlier build gate proves both imports. + - name: Verify DCode profile import gate rejects missing base dependencies + if: ${{ matrix.id == 'ubuntu-repo-cloud-langchain-deepagents-code' }} + shell: bash + run: bash scripts/check-dcode-profile-import-gate.sh + - name: Run live E2E tests env: NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} diff --git a/agents/langchain-deepagents-code/Dockerfile b/agents/langchain-deepagents-code/Dockerfile index 9c0e68eb9f0..eb1fd60b983 100644 --- a/agents/langchain-deepagents-code/Dockerfile +++ b/agents/langchain-deepagents-code/Dockerfile @@ -10,6 +10,8 @@ ARG BASE_IMAGE # hadolint ignore=DL3006 FROM ${BASE_IMAGE} +USER root + RUN set -eu; \ dcode_path="$(command -v dcode 2>/dev/null || true)"; \ if [ "$dcode_path" != "/usr/local/bin/dcode" ]; then \ @@ -23,7 +25,9 @@ RUN set -eu; \ COPY agents/langchain-deepagents-code/generate-config.ts /opt/nemoclaw-deepagents-code/generate-config.ts COPY agents/langchain-deepagents-code/managed-dcode-runtime.py /opt/nemoclaw-deepagents-code/managed-dcode-runtime.py COPY agents/langchain-deepagents-code/patch-managed-deepagents-code.py /opt/nemoclaw-deepagents-code/patch-managed-deepagents-code.py -COPY agents/langchain-deepagents-code/patch-nemotron-ultra-profile.py /opt/nemoclaw-deepagents-code/patch-nemotron-ultra-profile.py +# SECURITY: copy only the two hash-verified plugin inputs, never the source directory. +COPY agents/langchain-deepagents-code/profile-plugin/pyproject.toml /opt/nemoclaw-deepagents-profile-plugin/ +COPY agents/langchain-deepagents-code/profile-plugin/src/nemoclaw_deepagents_profile/__init__.py /opt/nemoclaw-deepagents-profile-plugin/src/nemoclaw_deepagents_profile/ COPY agents/langchain-deepagents-code/validate-nemotron-ultra-profile.py /opt/nemoclaw-deepagents-code/validate-nemotron-ultra-profile.py COPY agents/langchain-deepagents-code/progressive_tool_disclosure.py /opt/nemoclaw-deepagents-code/progressive_tool_disclosure.py COPY agents/langchain-deepagents-code/nemoclaw_observability.py /opt/nemoclaw-deepagents-code/nemoclaw_observability.py @@ -33,19 +37,30 @@ COPY agents/langchain-deepagents-code/dcode-wrapper.sh /usr/local/lib/nemoclaw/d COPY agents/langchain-deepagents-code/dcode-launcher.sh /usr/local/lib/nemoclaw/dcode-launcher.sh COPY agents/langchain-deepagents-code/start.sh /usr/local/bin/nemoclaw-start COPY nemoclaw-blueprint/ /opt/nemoclaw-blueprint/ -# The Nemotron bridge registers managed aliases for the released SDK profile; the -# managed-runtime patch independently hardens Deep Agents Code entrypoints and -# installs the reviewed Relay observability boundary. -# Both are exact-version, fail-closed build steps validated in one layer so no -# reusable image layer can contain only one of the required managed patches. -RUN chmod 444 /opt/nemoclaw-deepagents-code/generate-config.ts /opt/nemoclaw-deepagents-code/managed-dcode-runtime.py /opt/nemoclaw-deepagents-code/patch-managed-deepagents-code.py /opt/nemoclaw-deepagents-code/patch-nemotron-ultra-profile.py /opt/nemoclaw-deepagents-code/validate-nemotron-ultra-profile.py /opt/nemoclaw-deepagents-code/progressive_tool_disclosure.py /opt/nemoclaw-deepagents-code/nemoclaw_observability.py /opt/nemoclaw-deepagents-code/validate-progressive-tool-disclosure.py /opt/nemoclaw-deepagents-code/validate-observability.py \ +# The first-party profile plugin uses Deep Agents' supported entry-point hook to +# register managed aliases without modifying third-party package source. The +# managed-runtime patch independently hardens DCode entrypoints and installs the +# reviewed Relay observability boundary. Build validation keeps both exact and +# fail closed in one layer. +# invalidState: a no-deps plugin install can precede missing base dependencies. +# sourceBoundary: Dockerfile.base owns dependencies; this layer only proves them. +# whyNotSourceFix: dependency completeness is a NemoClaw image-build contract. +# regressionTest: the stripped-base gate must reach this marker, then fail import. +# removalCondition: remove when installation validates dependencies atomically. +# hadolint ignore=DL4006 +RUN chmod 444 /opt/nemoclaw-deepagents-code/generate-config.ts /opt/nemoclaw-deepagents-code/managed-dcode-runtime.py /opt/nemoclaw-deepagents-code/patch-managed-deepagents-code.py /opt/nemoclaw-deepagents-code/validate-nemotron-ultra-profile.py /opt/nemoclaw-deepagents-code/progressive_tool_disclosure.py /opt/nemoclaw-deepagents-code/nemoclaw_observability.py /opt/nemoclaw-deepagents-code/validate-progressive-tool-disclosure.py /opt/nemoclaw-deepagents-code/validate-observability.py \ && chmod 755 /usr/local/bin/nemoclaw-start /usr/local/lib/nemoclaw/dcode-wrapper.sh /usr/local/lib/nemoclaw/dcode-launcher.sh \ && chmod -R a+rX /opt/nemoclaw-blueprint \ - && python3 /opt/nemoclaw-deepagents-code/patch-nemotron-ultra-profile.py \ + && test "$(find /opt/nemoclaw-deepagents-profile-plugin -type f -print | LC_ALL=C sort)" = "$(printf '%s\n' '/opt/nemoclaw-deepagents-profile-plugin/pyproject.toml' '/opt/nemoclaw-deepagents-profile-plugin/src/nemoclaw_deepagents_profile/__init__.py')" \ + && printf '%s %s\n' '75ff7e7a5142cad4305126ccb1b8fc756306e82d4c559ddbc624012fb54ebfc4' '/opt/nemoclaw-deepagents-profile-plugin/src/nemoclaw_deepagents_profile/__init__.py' '7ba7b77bd6f889cc861eddbe3e38fc1f4433a85b7bc2a9b516e19a19a37a7686' '/opt/nemoclaw-deepagents-profile-plugin/pyproject.toml' | sha256sum -c - \ + && /opt/venv/bin/pip3 install --no-index --no-cache-dir --no-deps --no-build-isolation /opt/nemoclaw-deepagents-profile-plugin \ + && /opt/venv/bin/python3 -I -c 'import nemoclaw_deepagents_profile; print("NEMOCLAW_DCODE_PROFILE_" + "IMPORT_GATE", flush=True); import deepagents; import deepagents_code' \ + && /opt/venv/bin/pip3 check \ + && rm -rf /opt/nemoclaw-deepagents-profile-plugin \ && python3 /opt/nemoclaw-deepagents-code/patch-managed-deepagents-code.py \ && install -d -m 0700 /tmp/nemoclaw-progressive-validation \ && TMPDIR=/tmp/nemoclaw-progressive-validation python3 /opt/nemoclaw-deepagents-code/validate-progressive-tool-disclosure.py \ - && TMPDIR=/tmp/nemoclaw-progressive-validation python3 /opt/nemoclaw-deepagents-code/validate-nemotron-ultra-profile.py \ + && TMPDIR=/tmp/nemoclaw-progressive-validation /opt/venv/bin/python3 -I /opt/nemoclaw-deepagents-code/validate-nemotron-ultra-profile.py \ && rm -rf /tmp/nemoclaw-progressive-validation \ && /opt/venv/bin/python3 -I /opt/nemoclaw-deepagents-code/validate-observability.py \ && rm -f /opt/nemoclaw-deepagents-code/validate-progressive-tool-disclosure.py \ diff --git a/agents/langchain-deepagents-code/dcode-wrapper.sh b/agents/langchain-deepagents-code/dcode-wrapper.sh index 7689533d0b8..70c99dbb2a6 100755 --- a/agents/langchain-deepagents-code/dcode-wrapper.sh +++ b/agents/langchain-deepagents-code/dcode-wrapper.sh @@ -113,7 +113,7 @@ run_dcode() { # raw or escaped bodies before mutable metadata can reach status output. # * Name-context rejection fires case-insensitively when the variable name # ends in a credential keyword (_KEY, _TOKEN, _SECRET, _PASSWORD, -# _CREDENTIAL, _PASS) and the value is at least 10 chars (mirroring +# _PASSWD, _PASS, _CREDENTIAL) and the value is at least 10 chars (mirroring # CONTEXT_PATTERNS minimum length). # * Managed messaging values (SLACK_BOT_TOKEN, SLACK_APP_TOKEN, # TELEGRAM_BOT_TOKEN, DISCORD_BOT_TOKEN) are allowed only when the value @@ -145,9 +145,11 @@ run_dcode() { has_context_secret_shape() { local upper upper="$(printf '%s' "$1" | tr '[:lower:]' '[:upper:]')" - # The outer class accepts '=', ':', or whitespace; [:space:] is the nested - # POSIX character class understood by Bash's [[ string =~ regex ]] operator. - [[ "$upper" =~ (_KEY|API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)[=:[:space:]][\'\"]?[A-Z0-9_.+/=-]{10,} ]] + # Keep horizontal separator whitespace bounded to mirror the canonical + # lookbehind and avoid an attacker-controlled scan over arbitrarily long runs. + [[ "$upper" =~ (^|[^A-Z0-9])([A-Z0-9]{1,128}_(KEY|TOKEN|SECRET|CREDENTIAL|PASSWORD|PASSWD|PASS)|(X[-_])?API[-_]KEY|TOKEN|SECRET|CREDENTIAL|PASSWORD|PASSWD|PASS)[\'\"]?([[:blank:]]{0,32}[=:][[:blank:]]{0,32}|[[:blank:]]{1,32})[\'\"]?[^[:space:]\'\"]{10,} ]] \ + || [[ "$1" =~ (^|[^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))[\'\"]?([[:blank:]]{0,32}[=:][[:blank:]]{0,32}|[[:blank:]]{1,32})[\'\"]?[^[:space:]\'\"]{10,} ]] \ + || [[ "$1" =~ (^|[^A-Za-z0-9])KEY[\'\"]?([[:blank:]]{0,32}[=:][[:blank:]]{0,32}|[[:blank:]]{1,32})[\'\"]?[^[:space:]\'\"]{10,} ]] } has_bearer_secret_shape() { @@ -166,23 +168,11 @@ has_bearer_secret_shape() { has_private_key_block_shape() { local value="$1" + local required_separator="${2-}" local begin_marker="-----BEGIN " local end_marker="-----END " case "$value" in - *"$begin_marker"*"PRIVATE KEY-----"*"$end_marker"*"PRIVATE KEY-----"*) - return 0 - ;; - esac - return 1 -} - -has_multiline_private_key_block_shape() { - local value="$1" - local begin_marker="-----BEGIN " - local end_marker="-----END " - local newline=$'\n' - case "$value" in - *"$begin_marker"*"PRIVATE KEY-----"*"$newline"*"$end_marker"*"PRIVATE KEY-----"*) + *"$begin_marker"*"PRIVATE KEY-----"*"$required_separator"*"$end_marker"*"PRIVATE KEY-----"*) return 0 ;; esac @@ -331,7 +321,7 @@ has_credential_name_context() { local upper upper="$(printf '%s' "$1" | tr '[:lower:]' '[:upper:]')" case "$upper" in - KEY | API_KEY | TOKEN | SECRET | PASSWORD | PASS | CREDENTIAL) + KEY | API_KEY | TOKEN | SECRET | PASSWORD | PASSWD | PASS | CREDENTIAL) return 0 ;; LANGSMITH_RUNS_ENDPOINTS | LANGCHAIN_RUNS_ENDPOINTS) @@ -340,10 +330,14 @@ has_credential_name_context() { OTEL_EXPORTER_OTLP_ENDPOINT | OTEL_EXPORTER_OTLP_TRACES_ENDPOINT | OTEL_EXPORTER_OTLP_HEADERS | OTEL_EXPORTER_OTLP_TRACES_HEADERS) return 0 ;; - *_API_KEY | *_KEY | *_TOKEN | *_SECRET | *_PASSWORD | *_PASS | *_CREDENTIAL) + *_API_KEY | *_KEY | *_TOKEN | *_SECRET | *_PASSWORD | *_PASSWD | *_PASS | *_CREDENTIAL | *-API-KEY | *-KEY | *-TOKEN | *-SECRET | *-PASSWORD | *-PASSWD | *-PASS | *-CREDENTIAL) return 0 ;; esac + if [[ "$1" =~ [A-Za-z0-9](Token|Secret|Credential|Password|Passwd|Pass)$ ]] \ + || [[ "$1" =~ ([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$ ]]; then + return 0 + fi return 1 } @@ -465,7 +459,7 @@ assert_no_secret_env_file() { # Scan the whole file before line parsing so raw multiline blocks cannot put # their begin and end markers on different physical dotenv lines. env_file_content="$(<"$env_file")" - if has_multiline_private_key_block_shape "$env_file_content"; then + if has_private_key_block_shape "$env_file_content" $'\n'; then refuse_secret_env "$env_file" "private-key block" fi while IFS= read -r line || [ -n "$line" ]; do diff --git a/agents/langchain-deepagents-code/dependency-review.md b/agents/langchain-deepagents-code/dependency-review.md index e74f6574565..668374dcbf2 100644 --- a/agents/langchain-deepagents-code/dependency-review.md +++ b/agents/langchain-deepagents-code/dependency-review.md @@ -22,32 +22,96 @@ NemoClaw no longer vendors or overlays that source. - Native profile SHA-256: `c8e8dd2b0182334b54be4f46ff0c7b45fbb95dc13bd9a92c249eb47a14fa13d7` - Unmodified built-in bootstrap SHA-256: `005a91e7fc4ca6b21220673dd9d02d6686bf63e1e4f1102d124b01f96886efcf` -- Managed-alias bootstrap SHA-256: `9d9e817143b330fd45345fcfa8276ea6fe5d6bc5a396f0438b0899a450e4744b` - -The build patch verifies those official artifacts, then registers the native -profile under the two `openai:` model keys used by NemoClaw's managed -OpenAI-compatible `ChatOpenAI` route. It is atomic, idempotent, and fails closed -on version, source, bootstrap, or partial-state drift. The image build applies -the patch and runs the complete profile and dispatch validator against the -installed hash-locked wheels, while focused fixtures cover failure states. -This build-time site-packages mutation is the deliberate managed-image adapter; -the released package is never changed at runtime. The deleted source-backport -license path, `LICENSE.langchain-deepagents`, is not staged into the image, and -the image regression tests enforce that absence. +- First-party adapter: `nemoclaw-deepagents-profile==0.1.0` +- Adapter module SHA-256: `75ff7e7a5142cad4305126ccb1b8fc756306e82d4c559ddbc624012fb54ebfc4` +- Adapter project metadata SHA-256: `7ba7b77bd6f889cc861eddbe3e38fc1f4433a85b7bc2a9b516e19a19a37a7686` +- Adapter wheel license expression: `Apache-2.0` +- Adapter dependency audit result: `No known vulnerabilities found`. Its only + requirements are the exact `deepagents-code==0.1.34` and + `deepagents==0.7.0a6` entries covered by the lockfile audit command above; no + additional third-party distribution is introduced. + +### Test-only legacy license fixture limitation + +> **Removal condition:** Delete the test-only legacy license-table conversion in +> `test/langchain-deepagents-code-nemotron-profile-plugin.test.ts` as soon as the +> runner's system setuptools accepts PEP 639 license strings. Production never +> uses this conversion. + +The adapter metadata intentionally uses the PEP 639 SPDX expression +`license = "Apache-2.0"`, supported by its pinned production build backend. +The real-wheel test substitutes the equivalent legacy table only for its +offline, no-isolation wrong-version fixture with the runner's older system +setuptools; this is a known fixture limitation, not production metadata. The +production image builds the unchanged project with lock-pinned +`setuptools==82.0.1`, and its isolated validator fails closed unless the +installed wheel exposes `License-Expression: Apache-2.0`. + +The adapter is a private, first-party build-context package: NemoClaw does not +publish it to a registry or resolve it from an index. The image verifies its +reviewed source and project-metadata hashes, then builds it offline with +`--no-index --no-deps --no-build-isolation`. There is therefore no separate +published distribution for a registry audit to resolve. If that packaging +boundary ever changes, the publishing workflow must build and audit the wheel +before upload; index publication is not permitted without that release gate. + +The adapter project remains recoverable from the image's `COPY` layer after the +later `RUN` removes its duplicate build tree; a failed build may likewise retain +that layer in the trusted local cache. This is accepted because the project +contains only non-secret, first-party Apache-2.0 source and metadata, and the +installed Python module necessarily ships the same source in `site-packages`. +A multi-stage build or secret mount would not make the shipped module +confidential. Revisit this boundary if an adapter build input becomes +secret-bearing or non-public. + +Before local build and installation, the managed image verifies that the build +tree contains exactly the two individually copied adapter inputs, then checks +both against the module and project-metadata hashes recorded above. Extra files +cannot enter the wheel through the Docker build context. It then installs the +first-party `nemoclaw-deepagents-profile` package +without consulting an index. Its `deepagents.harness_profiles` entry +point runs after built-in profiles are registered, reads the reviewed canonical +profile through one exact-version/hash-gated private registry lookup, and uses +Deep Agents' public registration API to map it to the two exact `openai:` model +keys used by NemoClaw's managed OpenAI-compatible `ChatOpenAI` route. The +released SDK has no public profile getter or alias API. The adapter does not add +a provider-wide OpenAI profile. + +The adapter verifies the exact DCode and Deep Agents versions plus the official +native-profile and bootstrap source hashes. It also binds the imported Deep +Agents package to the distribution that supplied the reviewed version. +Registration is atomic, idempotent, and rejects missing canonical, partial, or +conflicting alias state. The image validator runs under isolated Python, +verifies the installed entry-point metadata and adapter source hash before the +upstream source checks, checks both upstream files again after profile loading, +resolves the complete native middleware for both aliases, compiles a graph, +proves parser/native dispatch parity, and confirms an unrelated OpenAI model +receives no Ultra behavior. The Docker build separately imports the adapter, +Deep Agents, and DCode under isolated Python immediately after installation; +the validator then binds the installed module to its distribution and rechecks +the module hash. A DCode-only CI regression builds the current, hash-locked +`Dockerfile.base` instead of consuming a mutable registry tag, strips both +upstream distributions, and proves the production build stops at that import +gate before the later dependency-consistency check. The targeted E2E job invokes +`scripts/check-dcode-profile-import-gate.sh` with real Docker before live tests; +the fake-Docker unit suite separately pins its diagnostic failure branches. + +The reviewed native-profile and bootstrap files stay byte-for-byte unchanged. +Focused fixtures cover the reviewed version/hash, missing-source, +missing-canonical, partial/conflicting, rollback, and idempotence states. The +deleted source-backport license path, `LICENSE.langchain-deepagents`, is not +staged into the image, and image regression tests enforce that absence. Deep Agents Code `0.1.34` is the released consumer; prerelease risk is limited to its exact `deepagents==0.7.0a6` SDK pin. That risk is accepted because the consumer and SDK are hash locked, the dependency audit is clean, and all source, -version, middleware, graph, and dispatch checks fail closed. - -The exact version and source-hash gates are also the executable lifecycle -tracker for the alias bridge: any dependency change stops the image build with -an explicit instruction to check for native managed-alias support, and requires -this review to be updated. The admin-maintainer override for this -source-of-truth decision records that the review is satisfied on the ancestor -containing this policy -([PR review](https://github.com/NVIDIA/NemoClaw/pull/6416#pullrequestreview-4649633900)). -That approval accepts this mandatory dependency-review gate as sufficient -removal accountability, so no standalone removal issue is used. When Deep -Agents natively recognizes both managed keys, the dependency review removes the -bridge instead of updating its versions or hashes. +version, middleware, graph, and dispatch contracts are enforced by the isolated +image-build validator. That validator is the fail-closed gate because Deep +Agents deliberately isolates and logs third-party plugin callback failures. + +The exact version and source-hash gates remain the executable lifecycle check +for the alias adapter: any dependency change stops the image build and requires +this review to revalidate the managed adapter. Remove it instead of refreshing +its hashes only if a future reviewed dependency already provides both exact +mappings; no external contribution is required. Issue #6424 records the +NemoClaw-owned replacement of the previous installed-bootstrap mutation. diff --git a/agents/langchain-deepagents-code/managed-dcode-runtime.py b/agents/langchain-deepagents-code/managed-dcode-runtime.py index 22a16c125a2..c6760db1fe6 100644 --- a/agents/langchain-deepagents-code/managed-dcode-runtime.py +++ b/agents/langchain-deepagents-code/managed-dcode-runtime.py @@ -35,9 +35,14 @@ } _MANAGED_FILE_OWNER_UID = 0 _CREDENTIAL_NAME = re.compile( - r"(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASS|CREDENTIAL)$", + r"(?:^|[_-])(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|PASS|CREDENTIAL)$", re.IGNORECASE, ) +_CREDENTIAL_CAMEL_NAME = re.compile( + r"(?:[A-Za-z0-9](?:Token|Secret|Credential|Password|Passwd|Pass)|" + r"(?:[Aa]ccess|[Rr]efresh|[Cc]lient|[Bb]earer|[Aa]uth|[Aa][Pp][Ii]|" + r"[Pp]rivate|[Ss]igning|[Ss]ession|[Bb]ot|[Aa]pp|[Rr]esolved)Key)$" +) _CREDENTIAL_ENV_NAMES = { "LANGSMITH_RUNS_ENDPOINTS", "LANGCHAIN_RUNS_ENDPOINTS", @@ -46,6 +51,12 @@ "OTEL_EXPORTER_OTLP_HEADERS", "OTEL_EXPORTER_OTLP_TRACES_HEADERS", } +# 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 = ( + r"[^\t\n\v\f\r \u00a0\u1680\u2000-\u200a\u2028\u2029" + r"\u202f\u205f\u3000\ufeff'\"]" +) _OPENSHELL_ENV_PLACEHOLDER_PREFIX = "openshell:resolve:env:" _UPSTREAM_PROVIDER_ENV = "NEMOCLAW_UPSTREAM_PROVIDER" _MANAGED_ADAPTER_PROVIDER = "openai" @@ -140,7 +151,21 @@ r"Bearer[\t\n\v\f\r \u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+[A-Za-z0-9_.+/=-]{10,}", re.IGNORECASE, ), - (None, r"(?:_KEY|API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)[=:\s]['\"]?[A-Za-z0-9_.+/=-]{10,}", re.IGNORECASE), + ( + None, + rf"(?:^|[^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)['\"]?(?:[ \t]{{0,32}}[=:][ \t]{{0,32}}|[ \t]{{1,32}})['\"]?{_ECMASCRIPT_NON_WHITESPACE_SECRET_CHAR}{{10,}}", + re.IGNORECASE, + ), + ( + None, + rf"(?:^|[^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))['\"]?(?:[ \t]{{0,32}}[=:][ \t]{{0,32}}|[ \t]{{1,32}})['\"]?{_ECMASCRIPT_NON_WHITESPACE_SECRET_CHAR}{{10,}}", + 0, + ), + ( + None, + rf"(?:^|[^A-Za-z0-9])KEY['\"]?(?:[ \t]{{0,32}}[=:][ \t]{{0,32}}|[ \t]{{1,32}})['\"]?{_ECMASCRIPT_NON_WHITESPACE_SECRET_CHAR}{{10,}}", + 0, + ), (None, r"lsv2_(?:pt|sk)_[A-Za-z0-9]{10,}(?:_[A-Za-z0-9]+)*", 0), (None, r"-----BEGIN [^-\r\n]*PRIVATE KEY-----[\s\S]*-----END [^-\r\n]*PRIVATE KEY-----", 0), ) @@ -200,7 +225,11 @@ 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) + len(value) >= 10 + and ( + _CREDENTIAL_NAME.search(name) + or _CREDENTIAL_CAMEL_NAME.search(name) + ) ) or ( bool(value) and name.upper() in _CREDENTIAL_ENV_NAMES ): diff --git a/agents/langchain-deepagents-code/nemoclaw_observability.py b/agents/langchain-deepagents-code/nemoclaw_observability.py index 957cc69ba41..29d01e0e9a7 100644 --- a/agents/langchain-deepagents-code/nemoclaw_observability.py +++ b/agents/langchain-deepagents-code/nemoclaw_observability.py @@ -131,6 +131,12 @@ def _safe_identifier(value: Any, fallback: str) -> str: _REDACTED_SECRET_VALUE = "" +# 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 = ( + r"[^\t\n\v\f\r \u00a0\u1680\u2000-\u200a\u2028\u2029" + r"\u202f\u205f\u3000\ufeff'\"]" +) # SECURITY -- Invalid state: Relay legitimately carries raw model and tool # content, but NemoClaw's managed exporter must not emit recognized credential # shapes from that content. This isolated Python package cannot import the @@ -172,10 +178,29 @@ def _safe_identifier(value: Any, fallback: str) -> str: re.IGNORECASE, ), re.compile( - r"((?:_KEY|API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)[=: ]['\"]?)" - r"[A-Za-z0-9_.+/=-]{10,}", + r"((?:^|[^A-Za-z0-9])(?:[A-Za-z0-9]{1,128}_" + r"(?:KEY|TOKEN|SECRET|CREDENTIAL|PASSWORD|PASSWD|PASS)|" + r"(?:X[-_])?API[-_]KEY|" + r"TOKEN|SECRET|CREDENTIAL|PASSWORD|PASSWD|PASS)" + r"['\"]?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})['\"]?)" + rf"{_ECMASCRIPT_NON_WHITESPACE_SECRET_CHAR}{{10,}}", re.IGNORECASE, ), + re.compile( + r"((?:^|[^A-Za-z0-9])" + r"(?:[A-Za-z0-9]{1,128}(?:Token|Secret|Credential)|" + r"[A-Za-z0-9]{0,128}(?:[Aa]ccess|[Rr]efresh|[Cc]lient|[Bb]earer|" + r"[Aa]uth|[Aa][Pp][Ii]|[Pp]rivate|[Ss]igning|[Ss]ession|[Bb]ot|" + r"[Aa]pp|[Rr]esolved)Key|" + r"[A-Za-z0-9]{1,128}(?:Password|Passwd|Pass))" + r"['\"]?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})['\"]?)" + rf"{_ECMASCRIPT_NON_WHITESPACE_SECRET_CHAR}{{10,}}", + ), + re.compile( + r"((?:^|[^A-Za-z0-9])KEY['\"]?" + r"(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})['\"]?)" + rf"{_ECMASCRIPT_NON_WHITESPACE_SECRET_CHAR}{{10,}}", + ), ) _ANCHORED_SECRET_REPLACEMENT = rf"\g<1>{_REDACTED_SECRET_VALUE}" _UNTERMINATED_PRIVATE_KEY_PATTERN = re.compile( @@ -199,11 +224,36 @@ def _safe_identifier(value: Any, fallback: str) -> str: ), ( r"(?:Bearer[\t\n\v\f\r \u00a0\u1680\u2000-\u200a\u2028\u2029" - r"\u202f\u205f\u3000\ufeff]+|" - r"(?:_KEY|API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)[=: ]['\"]?)" + r"\u202f\u205f\u3000\ufeff]+)" r"[A-Za-z0-9_.+/=-]*\Z", re.IGNORECASE, ), + ( + r"(?:^|[^A-Za-z0-9])(?:[A-Za-z0-9]{1,128}_" + r"(?:KEY|TOKEN|SECRET|CREDENTIAL|PASSWORD|PASSWD|PASS)|" + r"(?:X[-_])?API[-_]KEY|" + r"TOKEN|SECRET|CREDENTIAL|PASSWORD|PASSWD|PASS)" + r"['\"]?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})['\"]?" + rf"{_ECMASCRIPT_NON_WHITESPACE_SECRET_CHAR}*\Z", + re.IGNORECASE, + ), + ( + r"(?:^|[^A-Za-z0-9])" + r"(?:[A-Za-z0-9]{1,128}(?:Token|Secret|Credential)|" + r"[A-Za-z0-9]{0,128}(?:[Aa]ccess|[Rr]efresh|[Cc]lient|" + r"[Bb]earer|[Aa]uth|[Aa][Pp][Ii]|[Pp]rivate|[Ss]igning|" + r"[Ss]ession|[Bb]ot|[Aa]pp|[Rr]esolved)Key|" + r"[A-Za-z0-9]{1,128}(?:Password|Passwd|Pass))" + r"['\"]?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})['\"]?" + rf"{_ECMASCRIPT_NON_WHITESPACE_SECRET_CHAR}*\Z", + 0, + ), + ( + r"(?:^|[^A-Za-z0-9])KEY['\"]?" + r"(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})['\"]?" + rf"{_ECMASCRIPT_NON_WHITESPACE_SECRET_CHAR}*\Z", + 0, + ), ) ) @@ -277,7 +327,6 @@ def _redact_capture_key(key: Any) -> bool: "credentials", "header", "password", - "passwd", "secret", "token", } @@ -286,6 +335,9 @@ def _redact_capture_key(key: Any) -> bool: or normalized.endswith("_api_key") or normalized.endswith("_access_key") or normalized.endswith("_headers") + or normalized in {"pass", "passwd"} + or normalized.endswith("_pass") + or normalized.endswith("_passwd") or normalized.endswith("_password") or normalized.endswith("_private_key") or normalized.endswith("_secret") diff --git a/agents/langchain-deepagents-code/patch-nemotron-ultra-profile.py b/agents/langchain-deepagents-code/patch-nemotron-ultra-profile.py deleted file mode 100644 index 7e57e22b844..00000000000 --- a/agents/langchain-deepagents-code/patch-nemotron-ultra-profile.py +++ /dev/null @@ -1,179 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -"""Register the native Nemotron 3 Ultra profile for NemoClaw model aliases. - -Deep Agents 0.7.0a6 ships the profile from langchain-ai/deepagents PR #4192. -NemoClaw patches only the built-in bootstrap so its two managed OpenAI-compatible -model identities resolve that official profile too. - -Remove this alias bridge when Deep Agents natively recognizes the managed -OpenAI-compatible aliases. -""" - -# invalidState: the released native profile recognizes NVIDIA and hosted-provider -# identities, but NemoClaw's managed ChatOpenAI identities use openai: keys. -# sourceBoundary: Deep Agents owns the native profile and bootstrap; NemoClaw owns -# only the two openai: aliases required by its inference.local route. -# whyNotSourceFix: the aliases describe NemoClaw's managed model identity and the -# released SDK has no supported configuration hook for bootstrap-time aliases. -# regressionTest: exact wheel source/bootstrap hashes, failure-state tests, -# build-time graph/dispatch validation, and the typed DCode E2E target cover it. -# removalCondition: both managed ChatOpenAI aliases resolve the native Ultra -# profile without this patch; any DCode, Deep Agents, or source drift fails build. - -from __future__ import annotations - -import hashlib -import importlib.metadata -import importlib.util -import os -from pathlib import Path -from stat import S_IMODE - -EXPECTED_DCODE_VERSION = "0.1.34" -EXPECTED_DEEPAGENTS_VERSION = "0.7.0a6" -EXPECTED_NATIVE_PROFILE_SHA256 = ( - "c8e8dd2b0182334b54be4f46ff0c7b45fbb95dc13bd9a92c249eb47a14fa13d7" -) -EXPECTED_BOOTSTRAP_SHA256 = ( - "005a91e7fc4ca6b21220673dd9d02d6686bf63e1e4f1102d124b01f96886efcf" -) -EXPECTED_PATCHED_BOOTSTRAP_SHA256 = ( - "9d9e817143b330fd45345fcfa8276ea6fe5d6bc5a396f0438b0899a450e4744b" -) - -PATCH_MARKER = "# NemoClaw managed OpenAI-compatible Nemotron 3 Ultra aliases." -CANONICAL_PROFILE_KEY = "nvidia:nvidia/nemotron-3-ultra-550b-a55b" -MANAGED_PROFILE_KEYS = ( - "openai:nvidia/nemotron-3-ultra-550b-a55b", - "openai:nvidia/nvidia/nemotron-3-ultra", -) - -REGISTRY_IMPORT_ANCHOR = ( - "from deepagents.profiles.harness.harness_profiles import _HARNESS_PROFILES\n" -) -REGISTRY_IMPORT_PATCH = ( - "from deepagents.profiles.harness.harness_profiles import (\n" - " _HARNESS_PROFILES,\n" - " _register_harness_profile_impl,\n" - ")\n" -) -REGISTER_ANCHOR = " _nvidia_nemotron_3_ultra.register()\n" -REGISTER_PATCH = f''' _nvidia_nemotron_3_ultra.register()\n {PATCH_MARKER}\n _nemotron_ultra_profile = _HARNESS_PROFILES[\n "{CANONICAL_PROFILE_KEY}"\n ]\n _register_harness_profile_impl(\n "{MANAGED_PROFILE_KEYS[0]}", _nemotron_ultra_profile\n )\n _register_harness_profile_impl(\n "{MANAGED_PROFILE_KEYS[1]}", _nemotron_ultra_profile\n )\n''' - - -def fail(message: str) -> SystemExit: - """Build a consistent fail-closed error.""" - return SystemExit(f"ERROR: {message}") - - -def sha256(data: bytes) -> str: - return hashlib.sha256(data).hexdigest() - - -def require_version(distribution: str, expected: str) -> None: - try: - actual = importlib.metadata.version(distribution) - except importlib.metadata.PackageNotFoundError as exc: - raise fail(f"required distribution {distribution!r} is not installed") from exc - if actual != expected: - raise fail( - f"expected {distribution}=={expected}, found {actual}; dependency drift " - "requires reviewing whether upstream now recognizes both managed aliases " - "and removing this bridge when it does" - ) - - -def deepagents_root() -> Path: - spec = importlib.util.find_spec("deepagents") - if spec is None or spec.submodule_search_locations is None: - raise fail("could not locate the installed deepagents package") - roots = tuple(Path(entry) for entry in spec.submodule_search_locations) - if len(roots) != 1: - raise fail(f"expected one deepagents package root, found {len(roots)}") - root = roots[0] - if root.is_symlink() or not root.is_dir(): - raise fail(f"deepagents package root is not a trusted directory: {root}") - return root - - -def require_regular_file(path: Path, label: str) -> bytes: - if path.is_symlink() or not path.is_file(): - raise fail(f"{label} is not a trusted regular file: {path}") - return path.read_bytes() - - -def patched_bootstrap(original: bytes) -> bytes: - if sha256(original) != EXPECTED_BOOTSTRAP_SHA256: - raise fail( - "deepagents built-in profile bootstrap does not match the reviewed 0.7.0a6 source" - ) - text = original.decode("utf-8") - for label, anchor in ( - ("harness registry import", REGISTRY_IMPORT_ANCHOR), - ("harness registration", REGISTER_ANCHOR), - ): - if text.count(anchor) != 1: - raise fail(f"expected exactly one {label} anchor") - text = text.replace(REGISTRY_IMPORT_ANCHOR, REGISTRY_IMPORT_PATCH) - text = text.replace(REGISTER_ANCHOR, REGISTER_PATCH) - compile(text, "deepagents/profiles/_builtin_profiles.py", "exec") - return text.encode("utf-8") - - -def atomic_write(path: Path, data: bytes) -> None: - temporary = path.with_name(f".{path.name}.nemoclaw-tmp") - if temporary.exists() or temporary.is_symlink(): - raise fail(f"temporary patch path already exists: {temporary}") - try: - previous_umask = os.umask(0o022) - try: - temporary.write_bytes(data) - finally: - os.umask(previous_umask) - if S_IMODE(temporary.stat().st_mode) != 0o644: - raise fail(f"unexpected temporary patch mode: {temporary}") - temporary.replace(path) - finally: - if temporary.exists() and not temporary.is_symlink(): - temporary.unlink() - - -def main() -> None: - require_version("deepagents-code", EXPECTED_DCODE_VERSION) - require_version("deepagents", EXPECTED_DEEPAGENTS_VERSION) - - package_root = deepagents_root() - bootstrap_path = package_root / "profiles" / "_builtin_profiles.py" - native_profile_path = ( - package_root / "profiles" / "harness" / "_nvidia_nemotron_3_ultra.py" - ) - native_profile = require_regular_file( - native_profile_path, "native Nemotron profile source" - ) - if sha256(native_profile) != EXPECTED_NATIVE_PROFILE_SHA256: - raise fail("native Nemotron profile source does not match Deep Agents 0.7.0a6") - compile(native_profile, str(native_profile_path), "exec") - - bootstrap = require_regular_file( - bootstrap_path, "deepagents built-in profile bootstrap" - ) - bootstrap_hash = sha256(bootstrap) - - if bootstrap_hash == EXPECTED_BOOTSTRAP_SHA256: - updated_bootstrap = patched_bootstrap(bootstrap) - if sha256(updated_bootstrap) != EXPECTED_PATCHED_BOOTSTRAP_SHA256: - raise fail("internal patched-bootstrap digest is inconsistent") - atomic_write(bootstrap_path, updated_bootstrap) - print("Registered the native Nemotron 3 Ultra profile for managed aliases.") - return - - if bootstrap_hash == EXPECTED_PATCHED_BOOTSTRAP_SHA256: - print("Nemotron 3 Ultra managed-alias bridge is already applied.") - return - - raise fail("partial, conflicting, or drifted Nemotron profile alias patch state") - - -if __name__ == "__main__": - main() diff --git a/agents/langchain-deepagents-code/profile-plugin/.gitignore b/agents/langchain-deepagents-code/profile-plugin/.gitignore new file mode 100644 index 00000000000..8739192a9fe --- /dev/null +++ b/agents/langchain-deepagents-code/profile-plugin/.gitignore @@ -0,0 +1,2 @@ +build/ +src/*.egg-info/ diff --git a/agents/langchain-deepagents-code/profile-plugin/pyproject.toml b/agents/langchain-deepagents-code/profile-plugin/pyproject.toml new file mode 100644 index 00000000000..6b1736745b5 --- /dev/null +++ b/agents/langchain-deepagents-code/profile-plugin/pyproject.toml @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[build-system] +requires = ["setuptools==82.0.1"] +build-backend = "setuptools.build_meta" + +[project] +name = "nemoclaw-deepagents-profile" +version = "0.1.0" +description = "NemoClaw-managed Deep Agents harness profile aliases" +requires-python = ">=3.11" +license = "Apache-2.0" +dependencies = [ + "deepagents-code==0.1.34", + "deepagents==0.7.0a6", +] + +[project.entry-points."deepagents.harness_profiles"] +nemoclaw-managed-aliases = "nemoclaw_deepagents_profile:register" + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/agents/langchain-deepagents-code/profile-plugin/src/nemoclaw_deepagents_profile/__init__.py b/agents/langchain-deepagents-code/profile-plugin/src/nemoclaw_deepagents_profile/__init__.py new file mode 100644 index 00000000000..04835992f4a --- /dev/null +++ b/agents/langchain-deepagents-code/profile-plugin/src/nemoclaw_deepagents_profile/__init__.py @@ -0,0 +1,160 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Register released Deep Agents profiles for NemoClaw-managed model keys.""" + +from __future__ import annotations + +import hashlib +import importlib.metadata +import importlib.util +from collections.abc import Callable, MutableMapping +from pathlib import Path +from typing import Any + +EXPECTED_DCODE_VERSION = "0.1.34" +EXPECTED_DEEPAGENTS_VERSION = "0.7.0a6" +EXPECTED_NATIVE_PROFILE_SHA256 = ( + "c8e8dd2b0182334b54be4f46ff0c7b45fbb95dc13bd9a92c249eb47a14fa13d7" +) +EXPECTED_BOOTSTRAP_SHA256 = ( + "005a91e7fc4ca6b21220673dd9d02d6686bf63e1e4f1102d124b01f96886efcf" +) + +CANONICAL_PROFILE_KEY = "nvidia:nvidia/nemotron-3-ultra-550b-a55b" +MANAGED_PROFILE_KEYS = ( + "openai:nvidia/nemotron-3-ultra-550b-a55b", + "openai:nvidia/nvidia/nemotron-3-ultra", +) + +# invalidState: Deep Agents resolves pre-built ChatOpenAI models under `openai:` +# keys, while its native Ultra profile is registered under an NVIDIA key. +# sourceBoundary: NemoClaw owns only these two managed inference aliases; the +# prompt, tool overrides, middleware, bootstrap, and canonical profile remain +# byte-identical Deep Agents artifacts. +# whyPrivateRead: Deep Agents exposes public profile registration and plugin +# hooks but no public getter/alias API. The exact version/source gates constrain +# this single registry read; all writes use the public registration function. +# regressionTest: focused fixtures cover discovery, hashes, canonical identity, +# rollback, partial/conflicting state, and idempotence; the isolated real-wheel +# validator covers middleware, graph, dispatch, and unrelated-model behavior. +# removalCondition: remove this package only if a future reviewed dependency +# already provides both exact mappings; no external contribution is required. + + +def _fail(message: str) -> RuntimeError: + return RuntimeError(f"NemoClaw Deep Agents profile plugin: {message}") + + +def _sha256(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def _require_version(distribution: str, expected: str) -> None: + try: + actual = importlib.metadata.version(distribution) + except importlib.metadata.PackageNotFoundError as exc: + raise _fail(f"required distribution {distribution!r} is not installed") from exc + if actual != expected: + raise _fail( + f"expected {distribution}=={expected}, found {actual}; dependency drift " + "requires revalidating the managed profile adapter" + ) + + +def _deepagents_root() -> Path: + _require_version("deepagents", EXPECTED_DEEPAGENTS_VERSION) + try: + distribution = importlib.metadata.distribution("deepagents") + except importlib.metadata.PackageNotFoundError as exc: + raise _fail("required distribution 'deepagents' is not installed") from exc + + spec = importlib.util.find_spec("deepagents") + if spec is None or spec.submodule_search_locations is None: + raise _fail("could not locate the installed deepagents package") + roots = tuple(Path(entry) for entry in spec.submodule_search_locations) + if len(roots) != 1: + raise _fail(f"expected one deepagents package root, found {len(roots)}") + root = roots[0] + if root.is_symlink() or not root.is_dir(): + raise _fail(f"deepagents package root is not a trusted directory: {root}") + distribution_root = Path(distribution.locate_file("deepagents")) + if distribution_root.is_symlink() or not distribution_root.is_dir(): + raise _fail( + "deepagents distribution root is not a trusted directory: " + f"{distribution_root}" + ) + # Bind the import to its reviewed distribution so sys.path shadows fail closed. + try: + matches_distribution = root.samefile(distribution_root) + except OSError as exc: + raise _fail("could not verify the imported deepagents package root") from exc + if not matches_distribution: + raise _fail( + "imported deepagents package does not match the reviewed distribution" + ) + return root + + +def _require_source(path: Path, label: str, expected_sha256: str) -> None: + if path.is_symlink() or not path.is_file(): + raise _fail(f"{label} is not a trusted regular file: {path}") + source = path.read_bytes() + if _sha256(source) != expected_sha256: + raise _fail(f"{label} does not match the reviewed Deep Agents 0.7.0a6 wheel") + compile(source, str(path), "exec") + + +def _register_aliases( + registry: MutableMapping[str, Any], + register_profile: Callable[[str, Any], None], +) -> None: + native_profile = registry.get(CANONICAL_PROFILE_KEY) + if native_profile is None: + raise _fail(f"canonical profile {CANONICAL_PROFILE_KEY!r} is not registered") + + existing = tuple(key in registry for key in MANAGED_PROFILE_KEYS) + if all(existing): + if all(registry[key] is native_profile for key in MANAGED_PROFILE_KEYS): + return + raise _fail("managed aliases conflict with the reviewed canonical profile") + if any(existing): + raise _fail("managed aliases are in a partial registration state") + + try: + for key in MANAGED_PROFILE_KEYS: + register_profile(key, native_profile) + if not all(registry.get(key) is native_profile for key in MANAGED_PROFILE_KEYS): + raise _fail( + "managed alias registration did not preserve canonical identity" + ) + except Exception: + for key in MANAGED_PROFILE_KEYS: + registry.pop(key, None) + raise + + +def register() -> None: + """Register NemoClaw model aliases through Deep Agents' plugin hook.""" + _require_version("deepagents-code", EXPECTED_DCODE_VERSION) + + package_root = _deepagents_root() + _require_source( + package_root / "profiles" / "harness" / "_nvidia_nemotron_3_ultra.py", + "native Nemotron profile source", + EXPECTED_NATIVE_PROFILE_SHA256, + ) + _require_source( + package_root / "profiles" / "_builtin_profiles.py", + "built-in profile bootstrap", + EXPECTED_BOOTSTRAP_SHA256, + ) + + from deepagents.profiles import register_harness_profile # noqa: PLC0415 + from deepagents.profiles.harness.harness_profiles import ( # noqa: PLC0415 + _HARNESS_PROFILES, + ) + + _register_aliases(_HARNESS_PROFILES, register_harness_profile) + + +__all__ = ["register"] diff --git a/agents/langchain-deepagents-code/validate-nemotron-ultra-profile.py b/agents/langchain-deepagents-code/validate-nemotron-ultra-profile.py index 8226eb3762a..697f4556405 100644 --- a/agents/langchain-deepagents-code/validate-nemotron-ultra-profile.py +++ b/agents/langchain-deepagents-code/validate-nemotron-ultra-profile.py @@ -4,7 +4,10 @@ from __future__ import annotations +import hashlib import importlib.metadata +import importlib.util +import json import tempfile from collections.abc import Callable, Sequence from pathlib import Path @@ -16,7 +19,10 @@ from deepagents.profiles.harness._nvidia_nemotron_3_ultra import ( NemotronTextToolCallParser, ) -from deepagents.profiles.harness.harness_profiles import _harness_profile_for_model +from deepagents.profiles.harness.harness_profiles import ( + HarnessProfile, + _harness_profile_for_model, +) from deepagents_code.agent import create_cli_agent from langchain.agents.middleware.types import AgentMiddleware from langchain_core.language_models.fake_chat_models import FakeMessagesListChatModel @@ -24,6 +30,7 @@ from langchain_openai import ChatOpenAI EXPECTED_VERSIONS = { + "nemoclaw-deepagents-profile": "0.1.0", "deepagents-code": "0.1.34", "deepagents": "0.7.0a6", "langchain": "1.3.11", @@ -31,6 +38,21 @@ "langgraph": "1.2.6", "langchain-openai": "1.3.3", } +EXPECTED_PROFILE_ENTRY_POINT = ( + "deepagents.harness_profiles", + "nemoclaw-managed-aliases", + "nemoclaw_deepagents_profile:register", +) +EXPECTED_PLUGIN_LICENSE_EXPRESSION = "Apache-2.0" +EXPECTED_PLUGIN_SOURCE_SHA256 = ( + "75ff7e7a5142cad4305126ccb1b8fc756306e82d4c559ddbc624012fb54ebfc4" +) +EXPECTED_NATIVE_PROFILE_SHA256 = ( + "c8e8dd2b0182334b54be4f46ff0c7b45fbb95dc13bd9a92c249eb47a14fa13d7" +) +EXPECTED_BOOTSTRAP_SHA256 = ( + "005a91e7fc4ca6b21220673dd9d02d6686bf63e1e4f1102d124b01f96886efcf" +) MANAGED_MODEL_IDS = ( "nvidia/nemotron-3-ultra-550b-a55b", "nvidia/nvidia/nemotron-3-ultra", @@ -50,6 +72,7 @@ "FinalAnswerGuardMiddleware", ) DISPATCH_COMMAND = "printf NEMOCLAW_DISPATCH_OK" +DENIED_DISPATCH_COMMAND = "uname -a" def require(condition: bool, message: str) -> None: @@ -58,14 +81,118 @@ def require(condition: bool, message: str) -> None: raise RuntimeError(message) +def deepagents_root() -> Path: + spec = importlib.util.find_spec("deepagents") + require( + spec is not None and spec.submodule_search_locations is not None, + "could not locate the installed deepagents package", + ) + roots = tuple(Path(entry) for entry in spec.submodule_search_locations) + require( + len(roots) == 1, f"expected one deepagents package root, found {len(roots)}" + ) + root = roots[0] + require( + not root.is_symlink() and root.is_dir(), + f"deepagents package root is not a trusted directory: {root}", + ) + return root + + +def validate_official_sources() -> None: + root = deepagents_root() + for relative_path, label, expected_hash in ( + ( + Path("profiles/harness/_nvidia_nemotron_3_ultra.py"), + "native Nemotron profile source", + EXPECTED_NATIVE_PROFILE_SHA256, + ), + ( + Path("profiles/_builtin_profiles.py"), + "built-in profile bootstrap", + EXPECTED_BOOTSTRAP_SHA256, + ), + ): + path = root / relative_path + require( + not path.is_symlink() and path.is_file(), + f"{label} is not a trusted regular file: {path}", + ) + source = path.read_bytes() + require( + hashlib.sha256(source).hexdigest() == expected_hash, + f"{label} does not match the reviewed official wheel", + ) + compile(source, str(path), "exec") + + +def validate_profile_entry_point() -> None: + group, name, value = EXPECTED_PROFILE_ENTRY_POINT + group_entries = tuple(importlib.metadata.entry_points().select(group=group)) + require(group_entries, f"profile entry point group {group!r} was not found") + matches = [ + entry_point + for entry_point in group_entries + if entry_point.name == name + ] + require(len(matches) == 1, f"expected exactly one {name!r} profile entry point") + entry_point = matches[0] + require( + entry_point.value == value, + f"profile entry point target is {entry_point.value!r}, expected {value!r}", + ) + distribution = entry_point.dist + require(distribution is not None, "profile entry point has no source distribution") + require( + distribution.metadata["Name"] == "nemoclaw-deepagents-profile", + "profile entry point comes from an unexpected distribution", + ) + require( + distribution.version == EXPECTED_VERSIONS["nemoclaw-deepagents-profile"], + "profile entry point comes from an unexpected distribution version", + ) + require( + distribution.metadata.get("License-Expression") + == EXPECTED_PLUGIN_LICENSE_EXPRESSION, + "profile plugin license metadata does not match the reviewed package", + ) + module_spec = importlib.util.find_spec("nemoclaw_deepagents_profile") + require( + module_spec is not None and module_spec.origin is not None, + "could not locate the installed profile plugin", + ) + module_path = Path(module_spec.origin) + distribution_path = Path( + distribution.locate_file("nemoclaw_deepagents_profile/__init__.py") + ) + # Plugin registration separately binds the imported Deep Agents package to + # its distribution; this check binds the plugin module to its distribution. + for path, label in ( + (module_path, "imported profile plugin"), + (distribution_path, "distributed profile plugin"), + ): + require( + not path.is_symlink() and path.is_file(), + f"{label} is not a trusted regular file: {path}", + ) + require( + module_path.samefile(distribution_path), + "imported profile plugin does not match its reviewed distribution", + ) + source = module_path.read_bytes() + require( + hashlib.sha256(source).hexdigest() == EXPECTED_PLUGIN_SOURCE_SHA256, + "profile plugin source does not match the reviewed first-party package", + ) + compile(source, str(module_path), "exec") + + class ScriptedManagedModel(FakeMessagesListChatModel): """Expose the managed ChatOpenAI identity while returning fixed messages.""" model_name: str = MANAGED_MODEL_IDS[0] - def bind_tools( - self, tools: Any, **kwargs: Any - ) -> ScriptedManagedModel: + def bind_tools(self, tools: Any, **kwargs: Any) -> ScriptedManagedModel: del tools, kwargs return self @@ -81,9 +208,7 @@ def __init__(self, root_dir: Path) -> None: super().__init__(root_dir=root_dir, virtual_mode=False) self.dispatched_commands: list[tuple[str, int | None]] = [] - def execute( - self, command: str, *, timeout: int | None = None - ) -> ExecuteResponse: + def execute(self, command: str, *, timeout: int | None = None) -> ExecuteResponse: if "__DETECT_CONTEXT_EOF__" not in command: self.dispatched_commands.append((command, timeout)) return ExecuteResponse( @@ -101,8 +226,8 @@ def make_model(model_id: str) -> ChatOpenAI: ) -def middleware_names(profile: object) -> tuple[str, ...]: - middleware = getattr(profile, "extra_middleware") +def middleware_names(profile: HarnessProfile) -> tuple[str, ...]: + middleware = profile.extra_middleware if callable(middleware): factory = cast(Callable[[], Sequence[AgentMiddleware]], middleware) middleware = factory() @@ -167,7 +292,7 @@ def validate_parser_tool_visibility() -> None: def dispatch_execute_once( first_response: AIMessage, -) -> tuple[tuple[str, int | None], tuple[str, str | None]]: +) -> tuple[tuple[tuple[str, int | None], ...], tuple[str, str | None]]: """Run one model-produced execute call through DCode's managed allow-list.""" with tempfile.TemporaryDirectory(prefix="nemoclaw-profile-dispatch-") as tmp: backend = RecordingManagedShell(Path(tmp)) @@ -201,29 +326,20 @@ def dispatch_execute_once( for message in result["messages"] if isinstance(message, ToolMessage) and message.name == "execute" ] - require( - len(backend.dispatched_commands) == 1, - "execute validation did not dispatch exactly one shell command", - ) require( len(execute_results) == 1, "execute validation did not produce exactly one tool result", ) tool_result = execute_results[0] require(isinstance(tool_result.content, str), "execute result content is not text") - return backend.dispatched_commands[0], (tool_result.content, tool_result.status) + return tuple(backend.dispatched_commands), (tool_result.content, tool_result.status) -def validate_parser_dispatch_parity() -> None: - """Prove repaired and native execute calls share the managed dispatcher.""" +def validate_dispatch_case( + command: str, +) -> tuple[tuple[tuple[str, int | None], ...], tuple[str, str | None]]: repaired = dispatch_execute_once( - AIMessage( - content=( - '{"tool":"bash","cmd":"' - f"{DISPATCH_COMMAND}" - '"}' - ) - ) + AIMessage(content=json.dumps({"tool": "bash", "cmd": command})) ) native = dispatch_execute_once( AIMessage( @@ -231,7 +347,7 @@ def validate_parser_dispatch_parity() -> None: tool_calls=[ { "name": "execute", - "args": {"command": DISPATCH_COMMAND}, + "args": {"command": command}, "id": "native-execute", "type": "tool_call", } @@ -239,11 +355,25 @@ def validate_parser_dispatch_parity() -> None: ) ) require(repaired == native, "repaired and native execute dispatch results differ") + return repaired + + +def validate_parser_dispatch_parity() -> None: + """Prove repaired and native execute calls share the managed dispatcher.""" + allowed = validate_dispatch_case(DISPATCH_COMMAND) require( - repaired[0] == (DISPATCH_COMMAND, None), + allowed[0] == ((DISPATCH_COMMAND, None),), "execute dispatch arguments do not match the managed command", ) - require(repaired[1][1] == "success", "managed execute dispatch was not successful") + require(allowed[1][1] == "success", "managed execute dispatch was not successful") + + denied = validate_dispatch_case(DENIED_DISPATCH_COMMAND) + require(denied[0] == (), "denied execute command reached the shell backend") + require(denied[1][1] == "error", "denied execute command did not return an error") + require( + "Shell command rejected" in denied[1][0], + "denied execute command did not preserve the managed rejection result", + ) def main() -> None: @@ -254,6 +384,8 @@ def main() -> None: f"expected {distribution}=={expected}, found {actual}", ) + validate_profile_entry_point() + validate_official_sources() managed_models = [validate_profile(model_id) for model_id in MANAGED_MODEL_IDS] validate_parser_tool_visibility() validate_parser_dispatch_parity() @@ -272,6 +404,11 @@ def main() -> None: middleware_names(unrelated) == (), "unrelated OpenAI model received Ultra middleware", ) + # Final source re-verification matches Dockerfile's import-gate marker: + # re-bind and re-hash the plugin plus both official files after graph and + # dispatch checks to close the install/import-to-validation window. + validate_profile_entry_point() + validate_official_sources() print("Nemotron 3 Ultra managed harness profile validation passed.") diff --git a/agents/langchain-deepagents-code/validate-observability.py b/agents/langchain-deepagents-code/validate-observability.py index c82d6dc952b..d3ff23633a1 100644 --- a/agents/langchain-deepagents-code/validate-observability.py +++ b/agents/langchain-deepagents-code/validate-observability.py @@ -579,6 +579,21 @@ def _assert_secret_value_redaction(observability: ModuleType) -> None: "Api_Key=opaqueCredentialPayloadZ1234567890", "opaqueCredentialPayloadZ1234567890", ), + ( + "case-insensitive passwd assignment", + "Custom_Passwd=opaqueCredentialPayloadZ1234567890", + "opaqueCredentialPayloadZ1234567890", + ), + ( + "password-leading punctuation", + "Custom_Pass=!OpaquePassword123", + "!OpaquePassword123", + ), + ( + "password-tail punctuation", + "Custom_Pass=abcdefghij!tail-secret", + "tail-secret", + ), ( "private key block", private_key_probe, @@ -595,6 +610,8 @@ def _assert_secret_value_redaction(observability: ModuleType) -> None: benign_values = ( "sk-too-short", "Bearer short", + "COMPASS=opaqueNonSecretPayload123", + "BYPASS=allowedValue123", "-----BEGIN PUBLIC KEY-----\nnot-private\n-----END PUBLIC KEY-----", ) for value in benign_values: @@ -647,7 +664,9 @@ def recording_scrubber( "ABCDEFGHIJKLMNOPQRSTUVWX.Abcdef.ZZZZZZZZZZZZZZZZZZZZZZZZZZZ", ), ("Bearer token", "Bearer ABCDEFGHIJ"), - ("key assignment", "Api_" + "Key" + "=" + "ABCDEFGHIJ"), + # Context patterns require a real non-identifier boundary. Without the + # space this is an oversized x...Api_Key identifier, not an assignment. + ("key assignment", " " + "Api_" + "Key" + "=" + "ABCDEFGHIJ"), ) for label, credential in boundary_probes: boundary_prefix = credential[:-3] diff --git a/scripts/check-dcode-profile-import-gate.sh b/scripts/check-dcode-profile-import-gate.sh new file mode 100755 index 00000000000..bef43852ec3 --- /dev/null +++ b/scripts/check-dcode-profile-import-gate.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +readonly repo_root +readonly image_suffix="${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-0}-$$" +readonly source_base_image="nemoclaw-dcode-profile-source-base:${image_suffix}" +readonly stripped_image="nemoclaw-dcode-profile-missing-dependencies:${image_suffix}" +readonly failed_image="nemoclaw-dcode-profile-import-gate-failure:${image_suffix}" +build_log="$(mktemp "${TMPDIR:-/tmp}/nemoclaw-dcode-profile-import-gate.XXXXXX.log")" +readonly build_log + +cleanup() { + docker image rm --force \ + "${failed_image}" \ + "${stripped_image}" \ + "${source_base_image}" >/dev/null 2>&1 || true + rm -f "${build_log}" +} +trap cleanup EXIT + +cd "${repo_root}" + +# --progress=plain is required to prove the exact import failure marker. The +# primary security boundary is the exact ARG-name allowlist below, which covers +# agents/langchain-deepagents-code/Dockerfile.base, +# test/Dockerfile.dcode-profile-missing-dependencies, and +# agents/langchain-deepagents-code/Dockerfile. Those three reviewed Dockerfiles +# contain no secret-bearing ARGs. Only BASE_IMAGE is passed via --build-arg, +# always as a public, non-secret image reference. +for dockerfile in \ + agents/langchain-deepagents-code/Dockerfile.base \ + test/Dockerfile.dcode-profile-missing-dependencies \ + agents/langchain-deepagents-code/Dockerfile; do + while IFS= read -r arg_name; do + case "${arg_name}" in + BASE_IMAGE | NEMOCLAW_MODEL | NEMOCLAW_PROVIDER_KEY | NEMOCLAW_UPSTREAM_PROVIDER | NEMOCLAW_INFERENCE_BASE_URL | NEMOCLAW_INFERENCE_API | NEMOCLAW_TOOL_DISCLOSURE | NEMOCLAW_DCODE_AUTO_APPROVAL | NEMOCLAW_BUILD_ID | NEMOCLAW_DARWIN_VM_COMPAT | NEMOCLAW_PROXY_HOST | NEMOCLAW_PROXY_PORT) ;; + *) + echo "ERROR: plain-progress build refuses unreviewed ARG ${arg_name} in ${dockerfile}" >&2 + exit 1 + ;; + esac + done < <( + awk ' + toupper($1) == "ARG" { + name = $2 + while (name == "\\") { + if ((getline) <= 0) { + print "" + next + } + name = $1 + } + sub(/=.*/, "", name) + print name + } + ' "${dockerfile}" + ) +done + +# Build the reviewed repository base directly so this trusted negative gate has +# no mutable registry input. Docker layers remain reusable by the live target. +# Plain progress and the captured production log are safe after the ARG-name +# gate above; no build gets secret-bearing input. +docker build \ + --progress=plain \ + --file agents/langchain-deepagents-code/Dockerfile.base \ + --tag "${source_base_image}" \ + . + +docker build \ + --progress=plain \ + --file test/Dockerfile.dcode-profile-missing-dependencies \ + --build-arg "BASE_IMAGE=${source_base_image}" \ + --tag "${stripped_image}" \ + . + +if docker build \ + --progress=plain \ + --file agents/langchain-deepagents-code/Dockerfile \ + --build-arg "BASE_IMAGE=${stripped_image}" \ + --tag "${failed_image}" \ + . 2>&1 | tee "${build_log}"; then + echo "ERROR: DCode production image unexpectedly built without deepagents dependencies" >&2 + exit 1 +fi + +if ! grep -Fq "NEMOCLAW_DCODE_PROFILE_IMPORT_GATE" "${build_log}"; then + echo "ERROR: DCode build failed before reaching the profile import gate" >&2 + exit 1 +fi + +if ! grep -Fq "ModuleNotFoundError: No module named 'deepagents'" "${build_log}"; then + echo "ERROR: DCode build did not fail on the expected missing Deep Agents import" >&2 + exit 1 +fi + +echo "DCode profile import gate rejected a base missing deepagents and deepagents-code" diff --git a/src/lib/security/credential-filter-secret-patterns.test.ts b/src/lib/security/credential-filter-secret-patterns.test.ts new file mode 100644 index 00000000000..314999e474c --- /dev/null +++ b/src/lib/security/credential-filter-secret-patterns.test.ts @@ -0,0 +1,251 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; + +import { isCredentialField, stripCredentials, valueLooksLikeSecret } from "./credential-filter.js"; + +describe("isCredentialField", () => { + it("matches explicit field names", () => { + expect(isCredentialField("apiKey")).toBe(true); + expect(isCredentialField("api_key")).toBe(true); + expect(isCredentialField("token")).toBe(true); + expect(isCredentialField("secret")).toBe(true); + expect(isCredentialField("password")).toBe(true); + expect(isCredentialField("resolvedKey")).toBe(true); + }); + + it("matches pattern-based names", () => { + expect(isCredentialField("accessToken")).toBe(true); + expect(isCredentialField("refreshToken")).toBe(true); + expect(isCredentialField("clientSecret")).toBe(true); + expect(isCredentialField("bearerToken")).toBe(true); + expect(isCredentialField("privateKey")).toBe(true); + expect(isCredentialField("sessionToken")).toBe(true); + // OpenClaw channel token fields (#5027). + expect(isCredentialField("botToken")).toBe(true); + expect(isCredentialField("appToken")).toBe(true); + }); + + it("matches terminal pass aliases without treating pass substrings as credentials", () => { + for (const field of [ + "pass", + "passwd", + "customPass", + "customPasswd", + "DBPass", + "db_pass", + "db_passwd", + "db-pass", + "db-passwd", + ]) { + expect(isCredentialField(field), field).toBe(true); + } + for (const field of [ + "COMPASS", + "BYPASS", + "passengerCount", + "passed", + "passRate", + "passCount", + "passThrough", + ]) { + expect(isCredentialField(field), field).toBe(false); + } + }); + + it("matches env-variable-style secret names (#5027)", () => { + expect(isCredentialField("GITHUB_TOKEN")).toBe(true); + expect(isCredentialField("BRAVE_API_KEY")).toBe(true); + expect(isCredentialField("OPENAI_API_KEY")).toBe(true); + expect(isCredentialField("DB_PASSWORD")).toBe(true); + expect(isCredentialField("DB_PASSWD")).toBe(true); + expect(isCredentialField("DB_PASS")).toBe(true); + expect(isCredentialField("SLACK_APP_TOKEN")).toBe(true); + // Bare uppercase secret words must also be scrubbed. + expect(isCredentialField("TOKEN")).toBe(true); + expect(isCredentialField("PASSWORD")).toBe(true); + expect(isCredentialField("PASSWD")).toBe(true); + expect(isCredentialField("PASS")).toBe(true); + expect(isCredentialField("SECRET")).toBe(true); + expect(isCredentialField("CREDENTIALS")).toBe(true); + }); + + it("matches well-known HTTP auth header names (#5027)", () => { + expect(isCredentialField("Authorization")).toBe(true); + expect(isCredentialField("authorization")).toBe(true); + expect(isCredentialField("Proxy-Authorization")).toBe(true); + expect(isCredentialField("X-API-Key")).toBe(true); + expect(isCredentialField("X-API-Token")).toBe(true); + expect(isCredentialField("x-auth-token")).toBe(true); + expect(isCredentialField("Private-Token")).toBe(true); + expect(isCredentialField("X-Custom-Auth")).toBe(true); + expect(isCredentialField("Cookie")).toBe(true); + }); + + it("does not match safe field names", () => { + expect(isCredentialField("name")).toBe(false); + expect(isCredentialField("model")).toBe(false); + expect(isCredentialField("provider")).toBe(false); + expect(isCredentialField("endpoint")).toBe(false); + expect(isCredentialField("version")).toBe(false); + // Benign env/setting names must not be scrubbed. + expect(isCredentialField("NODE_ENV")).toBe(false); + expect(isCredentialField("LOG_LEVEL")).toBe(false); + expect(isCredentialField("PATH")).toBe(false); + expect(isCredentialField("tokenizer")).toBe(false); + expect(isCredentialField("maxTokens")).toBe(false); + expect(isCredentialField("X-Request-Id")).toBe(false); + }); + + it("does not strip public keys (verification material, not secrets)", () => { + expect(isCredentialField("publicKey")).toBe(false); + expect(isCredentialField("PUBLIC_KEY")).toBe(false); + expect(isCredentialField("public-key")).toBe(false); + expect(isCredentialField("X-Public-Key")).toBe(false); + expect(isCredentialField("GITHUB_PUBLIC_KEY")).toBe(false); + // But private keys and other secret fields still match. + expect(isCredentialField("privateKey")).toBe(true); + expect(isCredentialField("PRIVATE_KEY")).toBe(true); + expect(isCredentialField("apiKey")).toBe(true); + }); +}); + +describe("valueLooksLikeSecret", () => { + it("matches recognizable secret formats", () => { + expect(valueLooksLikeSecret("ghp_0123456789abcdef")).toBe(true); + expect(valueLooksLikeSecret("sk-proj-0123456789abcdefghij")).toBe(true); + expect(valueLooksLikeSecret("xoxb-123456789-abcdefghij")).toBe(true); + expect(valueLooksLikeSecret("Bearer abcdef0123456789")).toBe(true); + }); + + it("does not match benign values", () => { + expect(valueLooksLikeSecret("npx")).toBe(false); + expect(valueLooksLikeSecret("https://integrate.api.nvidia.com/v1")).toBe(false); + expect(valueLooksLikeSecret("moonshotai/kimi-k2")).toBe(false); + expect(valueLooksLikeSecret("production")).toBe(false); + }); +}); + +describe("stripCredentials", () => { + it("strips terminal pass aliases while preserving benign pass substrings", () => { + const payload = "opaqueCredentialPayloadZ1234567890"; + const result = stripCredentials({ + customPass: payload, + customPasswd: payload, + DBPass: payload, + db_pass: payload, + db_passwd: payload, + "db-pass": payload, + COMPASS: "north", + BYPASS: "allowed", + passRate: 0.9, + passCount: 4, + passThrough: true, + }); + + for (const field of [ + "customPass", + "customPasswd", + "DBPass", + "db_pass", + "db_passwd", + "db-pass", + ]) { + expect((result as Record)[field], field).toBe("[STRIPPED_BY_MIGRATION]"); + } + expect(result).toMatchObject({ + COMPASS: "north", + BYPASS: "allowed", + passRate: 0.9, + passCount: 4, + passThrough: true, + }); + }); + + it("strips raw channel tokens and MCP env secrets from openclaw.json (#5027)", () => { + const input = { + channels: { + slack: { + accounts: { default: { botToken: "xoxb-123-realsecret", appToken: "xapp-1-realsecret" } }, + }, + }, + mcpServers: { + github: { + command: "npx", + env: { + GITHUB_TOKEN: "ghp_realsecret", + TOKEN: "raw", + PASSWORD: "pw", + NODE_ENV: "production", + }, + }, + }, + }; + const result = stripCredentials(input); + expect(result.channels.slack.accounts.default.botToken).toBe("[STRIPPED_BY_MIGRATION]"); + expect(result.channels.slack.accounts.default.appToken).toBe("[STRIPPED_BY_MIGRATION]"); + expect(result.mcpServers.github.env.GITHUB_TOKEN).toBe("[STRIPPED_BY_MIGRATION]"); + expect(result.mcpServers.github.env.TOKEN).toBe("[STRIPPED_BY_MIGRATION]"); + expect(result.mcpServers.github.env.PASSWORD).toBe("[STRIPPED_BY_MIGRATION]"); + // Non-secret env vars and command survive. + expect(result.mcpServers.github.env.NODE_ENV).toBe("production"); + expect(result.mcpServers.github.command).toBe("npx"); + }); + + it("strips MCP HTTP auth headers by name and value backstop (#5027)", () => { + const input = { + mcpServers: { + remote: { + url: "https://mcp.example.com", + headers: { + Authorization: "Bearer ghp_0123456789abcdef", + "X-API-Key": "sk-0123456789abcdefghij", // gitleaks:allow + // Opaque value (no recognizable prefix) caught by header name. + "X-API-Token": "plain-opaque-value-12345", + // Opaque value under a custom -auth header, caught by header name. + "X-Custom-Auth": "plain-opaque-value-67890", + // Bearer resolve reference must survive (only a reference, no secret). + "X-Auth-Token": "Bearer openshell:resolve:env:REMOTE_MCP_TOKEN", + "X-Request-Id": "req-12345", + }, + }, + }, + }; + const result = stripCredentials(input); + const headers = result.mcpServers.remote.headers; + expect(headers.Authorization).toBe("[STRIPPED_BY_MIGRATION]"); + expect(headers["X-API-Key"]).toBe("[STRIPPED_BY_MIGRATION]"); + expect(headers["X-API-Token"]).toBe("[STRIPPED_BY_MIGRATION]"); + expect(headers["X-Custom-Auth"]).toBe("[STRIPPED_BY_MIGRATION]"); + expect(headers["X-Auth-Token"]).toBe("Bearer openshell:resolve:env:REMOTE_MCP_TOKEN"); + expect(headers["X-Request-Id"]).toBe("req-12345"); + expect(result.mcpServers.remote.url).toBe("https://mcp.example.com"); + }); + + it("scrubs secret strings and flag values inside array args (#5027)", () => { + const input = { + mcpServers: { + cli: { + command: "some-mcp", + args: [ + "--api-key", + "opaqueOpaqueSecret123", // opaque value after a credential flag + "--verbose", // value-less flag must not be swallowed + "--token=plainOpaque", // inline credential flag form + "--name=server", // benign inline flag survives + "ghp_0123456789abcdef", // shape-based catch + ], + }, + }, + }; + const result = stripCredentials(input); + const args = result.mcpServers.cli.args; + expect(args[0]).toBe("--api-key"); + expect(args[1]).toBe("[STRIPPED_BY_MIGRATION]"); + expect(args[2]).toBe("--verbose"); + expect(args[3]).toBe("--token=[STRIPPED_BY_MIGRATION]"); + expect(args[4]).toBe("--name=server"); + expect(args[5]).toBe("[STRIPPED_BY_MIGRATION]"); + }); +}); diff --git a/src/lib/security/credential-filter.test.ts b/src/lib/security/credential-filter.test.ts index 22f64914bff..45b2696fd7f 100644 --- a/src/lib/security/credential-filter.test.ts +++ b/src/lib/security/credential-filter.test.ts @@ -8,106 +8,13 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { isConfigValue, - isCredentialField, isSafeCredentialPlaceholder, isSensitiveFile, sanitizeConfigFile, shouldScanSnapshotFileForCredentials, stripCredentials, - valueLooksLikeSecret, } from "./credential-filter.js"; -describe("isCredentialField", () => { - it("matches explicit field names", () => { - expect(isCredentialField("apiKey")).toBe(true); - expect(isCredentialField("api_key")).toBe(true); - expect(isCredentialField("token")).toBe(true); - expect(isCredentialField("secret")).toBe(true); - expect(isCredentialField("password")).toBe(true); - expect(isCredentialField("resolvedKey")).toBe(true); - }); - - it("matches pattern-based names", () => { - expect(isCredentialField("accessToken")).toBe(true); - expect(isCredentialField("refreshToken")).toBe(true); - expect(isCredentialField("clientSecret")).toBe(true); - expect(isCredentialField("bearerToken")).toBe(true); - expect(isCredentialField("privateKey")).toBe(true); - expect(isCredentialField("sessionToken")).toBe(true); - // OpenClaw channel token fields (#5027). - expect(isCredentialField("botToken")).toBe(true); - expect(isCredentialField("appToken")).toBe(true); - }); - - it("matches env-variable-style secret names (#5027)", () => { - expect(isCredentialField("GITHUB_TOKEN")).toBe(true); - expect(isCredentialField("BRAVE_API_KEY")).toBe(true); - expect(isCredentialField("OPENAI_API_KEY")).toBe(true); - expect(isCredentialField("DB_PASSWORD")).toBe(true); - expect(isCredentialField("SLACK_APP_TOKEN")).toBe(true); - // Bare uppercase secret words must also be scrubbed. - expect(isCredentialField("TOKEN")).toBe(true); - expect(isCredentialField("PASSWORD")).toBe(true); - expect(isCredentialField("SECRET")).toBe(true); - expect(isCredentialField("CREDENTIALS")).toBe(true); - }); - - it("matches well-known HTTP auth header names (#5027)", () => { - expect(isCredentialField("Authorization")).toBe(true); - expect(isCredentialField("authorization")).toBe(true); - expect(isCredentialField("Proxy-Authorization")).toBe(true); - expect(isCredentialField("X-API-Key")).toBe(true); - expect(isCredentialField("X-API-Token")).toBe(true); - expect(isCredentialField("x-auth-token")).toBe(true); - expect(isCredentialField("Private-Token")).toBe(true); - expect(isCredentialField("X-Custom-Auth")).toBe(true); - expect(isCredentialField("Cookie")).toBe(true); - }); - - it("does not match safe field names", () => { - expect(isCredentialField("name")).toBe(false); - expect(isCredentialField("model")).toBe(false); - expect(isCredentialField("provider")).toBe(false); - expect(isCredentialField("endpoint")).toBe(false); - expect(isCredentialField("version")).toBe(false); - // Benign env/setting names must not be scrubbed. - expect(isCredentialField("NODE_ENV")).toBe(false); - expect(isCredentialField("LOG_LEVEL")).toBe(false); - expect(isCredentialField("PATH")).toBe(false); - expect(isCredentialField("tokenizer")).toBe(false); - expect(isCredentialField("maxTokens")).toBe(false); - expect(isCredentialField("X-Request-Id")).toBe(false); - }); - - it("does not strip public keys (verification material, not secrets)", () => { - expect(isCredentialField("publicKey")).toBe(false); - expect(isCredentialField("PUBLIC_KEY")).toBe(false); - expect(isCredentialField("public-key")).toBe(false); - expect(isCredentialField("X-Public-Key")).toBe(false); - expect(isCredentialField("GITHUB_PUBLIC_KEY")).toBe(false); - // But private keys and other secret fields still match. - expect(isCredentialField("privateKey")).toBe(true); - expect(isCredentialField("PRIVATE_KEY")).toBe(true); - expect(isCredentialField("apiKey")).toBe(true); - }); -}); - -describe("valueLooksLikeSecret", () => { - it("matches recognizable secret formats", () => { - expect(valueLooksLikeSecret("ghp_0123456789abcdef")).toBe(true); - expect(valueLooksLikeSecret("sk-proj-0123456789abcdefghij")).toBe(true); - expect(valueLooksLikeSecret("xoxb-123456789-abcdefghij")).toBe(true); - expect(valueLooksLikeSecret("Bearer abcdef0123456789")).toBe(true); - }); - - it("does not match benign values", () => { - expect(valueLooksLikeSecret("npx")).toBe(false); - expect(valueLooksLikeSecret("https://integrate.api.nvidia.com/v1")).toBe(false); - expect(valueLooksLikeSecret("moonshotai/kimi-k2")).toBe(false); - expect(valueLooksLikeSecret("production")).toBe(false); - }); -}); - describe("isSafeCredentialPlaceholder", () => { it("recognizes OpenShell resolve placeholders and the unused sentinel", () => { expect(isSafeCredentialPlaceholder("openshell:resolve:env:DISCORD_BOT_TOKEN")).toBe(true); @@ -197,92 +104,6 @@ describe("stripCredentials", () => { ); }); - it("strips raw channel tokens and MCP env secrets from openclaw.json (#5027)", () => { - const input = { - channels: { - slack: { - accounts: { default: { botToken: "xoxb-123-realsecret", appToken: "xapp-1-realsecret" } }, - }, - }, - mcpServers: { - github: { - command: "npx", - env: { - GITHUB_TOKEN: "ghp_realsecret", - TOKEN: "raw", - PASSWORD: "pw", - NODE_ENV: "production", - }, - }, - }, - }; - const result = stripCredentials(input); - expect(result.channels.slack.accounts.default.botToken).toBe("[STRIPPED_BY_MIGRATION]"); - expect(result.channels.slack.accounts.default.appToken).toBe("[STRIPPED_BY_MIGRATION]"); - expect(result.mcpServers.github.env.GITHUB_TOKEN).toBe("[STRIPPED_BY_MIGRATION]"); - expect(result.mcpServers.github.env.TOKEN).toBe("[STRIPPED_BY_MIGRATION]"); - expect(result.mcpServers.github.env.PASSWORD).toBe("[STRIPPED_BY_MIGRATION]"); - // Non-secret env vars and command survive. - expect(result.mcpServers.github.env.NODE_ENV).toBe("production"); - expect(result.mcpServers.github.command).toBe("npx"); - }); - - it("strips MCP HTTP auth headers by name and value backstop (#5027)", () => { - const input = { - mcpServers: { - remote: { - url: "https://mcp.example.com", - headers: { - Authorization: "Bearer ghp_0123456789abcdef", - "X-API-Key": "sk-0123456789abcdefghij", // gitleaks:allow - // Opaque value (no recognizable prefix) caught by header name. - "X-API-Token": "plain-opaque-value-12345", - // Opaque value under a custom -auth header, caught by header name. - "X-Custom-Auth": "plain-opaque-value-67890", - // Bearer resolve reference must survive (only a reference, no secret). - "X-Auth-Token": "Bearer openshell:resolve:env:REMOTE_MCP_TOKEN", - "X-Request-Id": "req-12345", - }, - }, - }, - }; - const result = stripCredentials(input); - const headers = result.mcpServers.remote.headers; - expect(headers.Authorization).toBe("[STRIPPED_BY_MIGRATION]"); - expect(headers["X-API-Key"]).toBe("[STRIPPED_BY_MIGRATION]"); - expect(headers["X-API-Token"]).toBe("[STRIPPED_BY_MIGRATION]"); - expect(headers["X-Custom-Auth"]).toBe("[STRIPPED_BY_MIGRATION]"); - expect(headers["X-Auth-Token"]).toBe("Bearer openshell:resolve:env:REMOTE_MCP_TOKEN"); - expect(headers["X-Request-Id"]).toBe("req-12345"); - expect(result.mcpServers.remote.url).toBe("https://mcp.example.com"); - }); - - it("scrubs secret strings and flag values inside array args (#5027)", () => { - const input = { - mcpServers: { - cli: { - command: "some-mcp", - args: [ - "--api-key", - "opaqueOpaqueSecret123", // opaque value after a credential flag - "--verbose", // value-less flag must not be swallowed - "--token=plainOpaque", // inline credential flag form - "--name=server", // benign inline flag survives - "ghp_0123456789abcdef", // shape-based catch - ], - }, - }, - }; - const result = stripCredentials(input); - const args = result.mcpServers.cli.args; - expect(args[0]).toBe("--api-key"); - expect(args[1]).toBe("[STRIPPED_BY_MIGRATION]"); - expect(args[2]).toBe("--verbose"); - expect(args[3]).toBe("--token=[STRIPPED_BY_MIGRATION]"); - expect(args[4]).toBe("--name=server"); - expect(args[5]).toBe("[STRIPPED_BY_MIGRATION]"); - }); - it("still strips raw secrets even under preserved-style sibling fields", () => { const input = { good: { apiKey: "openshell:resolve:env:GOOD_KEY" }, diff --git a/src/lib/security/credential-filter.ts b/src/lib/security/credential-filter.ts index 8f10df5874b..3776531fc2a 100644 --- a/src/lib/security/credential-filter.ts +++ b/src/lib/security/credential-filter.ts @@ -23,7 +23,7 @@ import { } from "node:fs"; import { basename, dirname, join } from "node:path"; -import { SECRET_PATTERNS } from "./secret-patterns"; +import { hasPassCredentialSegment, SECRET_PATTERNS } from "./secret-patterns"; function parseJson(text: string): T { return JSON.parse(text); @@ -106,6 +106,8 @@ const CREDENTIAL_FIELDS = new Set([ "token", "secret", "password", + "pass", + "passwd", "resolvedKey", ]); @@ -123,12 +125,12 @@ const CREDENTIAL_FIELD_PATTERN = * server's `env: { GITHUB_TOKEN, BRAVE_API_KEY, TOKEN }` block. These are not * camelCase, so the suffix pattern above misses them. Matches an all-uppercase * name that is, or ends in, a secret word (`TOKEN`, `KEY`, `SECRET`, - * `PASSWORD`, `PASSPHRASE`, `CREDENTIAL`, optionally pluralized) — covering both + * `PASSWORD`, `PASSWD`, `PASS`, `PASSPHRASE`, `CREDENTIAL`, optionally pluralized) — covering both * the prefixed (`GITHUB_TOKEN`) and bare (`TOKEN`) forms — while leaving benign * env vars like `NODE_ENV`, `LOG_LEVEL`, or `PATH` untouched. */ const ENV_SECRET_FIELD_PATTERN = - /^(?:[A-Z0-9]+_)*(?:TOKEN|KEY|SECRET|PASSWORD|PASSPHRASE|CREDENTIAL)S?$/; + /^(?:[A-Z0-9]+_)*(?:TOKEN|KEY|SECRET|PASSWORD|PASSWD|PASS|PASSPHRASE|CREDENTIAL)S?$/; /** * Well-known HTTP auth header names (matched case-insensitively) whose entire @@ -166,6 +168,7 @@ export function isCredentialField(key: string): boolean { return ( CREDENTIAL_FIELDS.has(key) || CREDENTIAL_FIELD_PATTERN.test(key) || + hasPassCredentialSegment(key) || ENV_SECRET_FIELD_PATTERN.test(key) || HEADER_CREDENTIAL_PATTERN.test(key) || CREDENTIAL_HEADER_NAMES.has(key.toLowerCase()) diff --git a/src/lib/security/redact.test.ts b/src/lib/security/redact.test.ts index d2b54dbf7a9..86fbfacd677 100644 --- a/src/lib/security/redact.test.ts +++ b/src/lib/security/redact.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest"; -import { redact, redactForLog, redactUrl } from "./redact.js"; +import { redact, redactForLog, redactFull, redactSensitiveText, redactUrl } from "./redact.js"; describe("URL redaction", () => { it.each([ @@ -93,10 +93,91 @@ describe("URL redaction", () => { }); describe("redactForLog", () => { + it("redacts pass aliases in structured keys and canonical text assignments", () => { + const payload = "opaqueCredentialPayloadZ1234567890"; + + expect( + redactForLog({ + pass: payload, + passwd: payload, + customPass: payload, + customPasswd: payload, + DBPass: payload, + db_pass: payload, + "db-pass": payload, + replyToken: payload, + }), + ).toEqual({ + pass: "", + passwd: "", + customPass: "", + customPasswd: "", + DBPass: "", + db_pass: "", + "db-pass": "", + replyToken: "", + }); + for (const [assignment, expected] of [ + [`CUSTOM_PASS=${payload}`, "CUSTOM_PASS="], + [`CUSTOM_PASSWD=${payload}`, "CUSTOM_PASSWD="], + [`CUSTOM_PASS ${payload}`, "CUSTOM_PASS "], + ["CUSTOM_PASS=!OpaquePassword123", "CUSTOM_PASS="], + ["CUSTOM_PASS=abcdefghij!tail-secret", "CUSTOM_PASS="], + ["CUSTOM_PASS=,OpaquePassword123", "CUSTOM_PASS="], + ["CUSTOM_PASS=OpaquePassword123,", "CUSTOM_PASS="], + [`PASS: ${payload}`, "PASS: "], + [`PASS = ${payload}`, "PASS = "], + [`{"PASS":"${payload}"}`, '{"PASS":""}'], + [`api-key=${payload}`, "api-key="], + [`X-Api-Key=${payload}`, "X-Api-Key="], + [`clientSecret=${payload}`, "clientSecret="], + [`replyToken=${payload}`, "replyToken="], + [`{"replyToken":"${payload}"}`, '{"replyToken":""}'], + [`githubToken=${payload}`, "githubToken="], + [`webhookSecret=${payload}`, "webhookSecret="], + [`databaseCredential=${payload}`, "databaseCredential="], + [`customPass=${payload}`, "customPass="], + [`DBPass=${payload}`, "DBPass="], + ]) { + expect(redactSensitiveText(assignment)).toBe(expected); + expect(redactFull(assignment)).toBe(expected); + expect(redactForLog(assignment)).toBe(expected); + } + }); + + it("preserves benign structured keys and assignments containing pass", () => { + const benign = { + compass: "north", + bypass: false, + passengerCount: 2, + passed: true, + passRate: 0.9, + passCount: 4, + passThrough: "enabled", + correlationMarker: "reply-correlation-marker-123", + }; + + expect(redactForLog(benign)).toEqual(benign); + for (const text of [ + "COMPASS=opaqueNonSecretPayload123 BYPASS=allowedValue123", + "TOPSECRET=opaqueNonSecretPayload123 SUBTOKEN=opaqueNonSecretPayload123", + "publicKey=opaqueVerificationMaterial123 customKey=opaqueNonSecretPayload123", + "public-key=opaqueVerificationMaterial123 custom-key=opaqueNonSecretPayload123", + "passRate=opaqueNonSecretPayload123", + '{"key":"agent:main:main"}', + '{"correlationMarker":"reply-correlation-marker-123"}', + ]) { + expect(redactSensitiveText(text), text).toBe(text); + expect(redactFull(text), text).toBe(text); + expect(redactForLog(text), text).toBe(text); + } + }); + it("redacts sensitive object keys recursively while preserving safe fields", () => { const result = redactForLog({ provider: "openai", apiKey: "sk-" + "a".repeat(24), + replyToken: "opaqueCredentialPayloadZ1234567890", nested: { model: "gpt-4o", refreshToken: "refresh-token-value", @@ -107,6 +188,7 @@ describe("redactForLog", () => { expect(result).toEqual({ provider: "openai", apiKey: "", + replyToken: "", nested: { model: "gpt-4o", refreshToken: "", diff --git a/src/lib/security/redact.ts b/src/lib/security/redact.ts index 595c612f1c7..211f48c8fc8 100644 --- a/src/lib/security/redact.ts +++ b/src/lib/security/redact.ts @@ -19,7 +19,13 @@ import type { StdioOptions } from "node:child_process"; */ import { listMessagingCredentialMetadata } from "../messaging/channels"; -import { SECRET_BLOCK_PATTERNS, SECRET_PATTERNS, TOKEN_PREFIX_PATTERNS } from "./secret-patterns"; +import { + CONTEXT_PATTERNS, + hasPassCredentialSegment, + SECRET_BLOCK_PATTERNS, + SECRET_PATTERNS, + TOKEN_PREFIX_PATTERNS, +} from "./secret-patterns"; const SENSITIVE_ENV_ASSIGNMENT_KEYS = [ "NVIDIA_INFERENCE_API_KEY", @@ -170,22 +176,26 @@ export function writeRedactedResult( // ── Full redaction (debug.ts style) ───────────────────────────── const FULL_REDACT_PATTERNS: [RegExp, string][] = [ + ...SECRET_BLOCK_PATTERNS.map((p): [RegExp, string] => [ + new RegExp(p.source, p.flags), + "", + ]), [ - /(NVIDIA_INFERENCE_API_KEY|NVIDIA_API_KEY|API_KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|_KEY)=\S+/gi, - "$1=", + /((?:^|[^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)["']?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["']?)[^\s'"]+((?:"|')?)/gi, + "$1$2", ], [ - /((?:"|')?(?:api[_-]?key|token|secret|password|credential)(?:"|')?\s*[:=]\s*(?:"|')?)[^"',}\s]+((?:"|')?)/gi, + /((?:^|[^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))["']?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["']?)[^\s'"]+((?:"|')?)/g, + "$1$2", + ], + [ + /((?:^|[^A-Za-z0-9])KEY["']?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["']?)[^\s'"]+((?:"|')?)/g, "$1$2", ], ...TOKEN_PREFIX_PATTERNS.map((p): [RegExp, string] => [ new RegExp(p.source, p.flags), "", ]), - ...SECRET_BLOCK_PATTERNS.map((p): [RegExp, string] => [ - new RegExp(p.source, p.flags), - "", - ]), [/(Bearer )\S+/gi, "$1"], [/\/bot[^/\s]+\//g, "/bot/"], ]; @@ -216,7 +226,7 @@ export function redactSensitiveText(value: unknown): string | null { let result = value .replace(SENSITIVE_ENV_ASSIGNMENT_PATTERN, "$1=") .replace(/Bearer\s+\S+/gi, "Bearer "); - for (const pattern of [...TOKEN_PREFIX_PATTERNS, ...SECRET_BLOCK_PATTERNS]) { + for (const pattern of [...SECRET_BLOCK_PATTERNS, ...CONTEXT_PATTERNS, ...TOKEN_PREFIX_PATTERNS]) { pattern.lastIndex = 0; result = result.replace(pattern, ""); } @@ -245,7 +255,10 @@ export function redactUrl(value: unknown): string | null { } function isSensitiveKey(key: string): boolean { - return /(?:api[_-]?key|token|secret|password|credential|authorization|bearer)/i.test(key); + return ( + /(?:api[_-]?key|token|secret|password|credential|authorization|bearer)/i.test(key) || + hasPassCredentialSegment(key) + ); } export function redactForLog(value: unknown, seen: WeakSet = new WeakSet()): unknown { diff --git a/src/lib/security/secret-patterns.ts b/src/lib/security/secret-patterns.ts index e3c4da67469..e254677c79d 100644 --- a/src/lib/security/secret-patterns.ts +++ b/src/lib/security/secret-patterns.ts @@ -51,9 +51,27 @@ export const TOKEN_PREFIX_PATTERNS: RegExp[] = [ /** Context-anchored patterns (require a prefix like KEY=, Bearer, etc.). */ export const CONTEXT_PATTERNS: RegExp[] = [ /(?<=Bearer\s+)[A-Za-z0-9_.+/=-]{10,}/gi, - /(?<=(?:_KEY|API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)[=: ]['"]?)[A-Za-z0-9_.+/=-]{10,}/gi, + /(?<=(?:^|[^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)["']?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["']?)[^\s'"]{10,}/gi, + /(?<=(?:^|[^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))["']?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["']?)[^\s'"]{10,}/g, + /(?<=(?:^|[^A-Za-z0-9])KEY["']?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["']?)[^\s'"]{10,}/g, ]; +/** Match pass/passwd only as a complete or terminal credential-name segment. */ +export function hasPassCredentialSegment(key: string): boolean { + const normalized = key + .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2") + .replace(/([a-z0-9])([A-Z])/g, "$1_$2") + .replace(/[^A-Za-z0-9]+/g, "_") + .replace(/^_+|_+$/g, "") + .toLowerCase(); + return ( + normalized === "pass" || + normalized === "passwd" || + normalized.endsWith("_pass") || + normalized.endsWith("_passwd") + ); +} + /** Multi-line or JSON-escaped secret blocks that do not have a token prefix. */ export const SECRET_BLOCK_PATTERNS: RegExp[] = [ /-----BEGIN (?:[A-Z0-9]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z0-9]+ )?PRIVATE KEY-----/g, @@ -62,8 +80,8 @@ export const SECRET_BLOCK_PATTERNS: RegExp[] = [ /** All secret patterns combined. */ export const SECRET_PATTERNS: RegExp[] = [ ...TOKEN_PREFIX_PATTERNS, - ...CONTEXT_PATTERNS, ...SECRET_BLOCK_PATTERNS, + ...CONTEXT_PATTERNS, ]; /** diff --git a/test/Dockerfile.dcode-profile-missing-dependencies b/test/Dockerfile.dcode-profile-missing-dependencies new file mode 100644 index 00000000000..1d57f7c76e3 --- /dev/null +++ b/test/Dockerfile.dcode-profile-missing-dependencies @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# This fixture starts from the repository-built DCode base but removes both +# reviewed upstream distributions. The tiny dcode stub lets the production +# Dockerfile's executable-path preflight complete so the build must reach its +# isolated Python import gate. +ARG BASE_IMAGE + +# hadolint ignore=DL3006 +FROM ${BASE_IMAGE} + +USER root + +RUN /opt/venv/bin/pip3 uninstall --yes deepagents-code deepagents \ + && /opt/venv/bin/python3 -I -c 'import importlib.util; assert importlib.util.find_spec("deepagents") is None; assert importlib.util.find_spec("deepagents_code") is None' \ + && rm -f /usr/local/bin/dcode \ + && printf '%s\n' '#!/bin/sh' 'printf "%s\\n" "deepagents-code 0.1.34"' > /usr/local/bin/dcode \ + && chmod 0755 /usr/local/bin/dcode + +USER sandbox diff --git a/test/deepagents-code-tui-startup-check.test.ts b/test/deepagents-code-tui-startup-check.test.ts index e6beee35da1..9c2c05ac48b 100644 --- a/test/deepagents-code-tui-startup-check.test.ts +++ b/test/deepagents-code-tui-startup-check.test.ts @@ -7,7 +7,11 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; -import { CONTEXT_PATTERNS, TOKEN_PREFIX_PATTERNS } from "../src/lib/security/secret-patterns.ts"; +import { + CONTEXT_PATTERNS, + SECRET_BLOCK_PATTERNS, + TOKEN_PREFIX_PATTERNS, +} from "../src/lib/security/secret-patterns.ts"; const tuiStartupCheckPath = path.join( process.cwd(), @@ -537,6 +541,30 @@ describe("Deep Agents Code TUI startup check helpers", () => { rawSecret: "abcdefghijklmnopqrst", }, ], + [ + fingerprint(CONTEXT_PATTERNS[2]), + { + name: "camel_secret_context", + sample: "clientSecret=opaqueCredentialPayloadZ1234567890", + rawSecret: "opaqueCredentialPayloadZ1234567890", + }, + ], + [ + fingerprint(CONTEXT_PATTERNS[3]), + { + name: "uppercase_key_context", + sample: "KEY=opaqueCredentialPayloadZ1234567890", + rawSecret: "opaqueCredentialPayloadZ1234567890", + }, + ], + [ + fingerprint(SECRET_BLOCK_PATTERNS[0]), + { + name: "private_key_block", + sample: + "-----BEGIN TEST PRIVATE KEY-----\nopaque-test-body\n-----END TEST PRIVATE KEY-----", + }, + ], ]); const extraSamples = [ { name: "akia", sample: secretFixture("AK", "IA", "ABCDEFGHIJKLMNOP") }, @@ -576,9 +604,53 @@ describe("Deep Agents Code TUI startup check helpers", () => { sample: "SERVICE_KEY=abcdefghijklmnopqrst", rawSecret: "abcdefghijklmnopqrst", }, + { + name: "pass_punctuation_context", + sample: "CUSTOM_PASS=!OpaquePassword123", + rawSecret: "!OpaquePassword123", + }, + { + name: "quoted_json_pass_context", + sample: '{"PASS":"opaqueCredentialPayloadZ1234567890"}', + rawSecret: "opaqueCredentialPayloadZ1234567890", + }, + { + name: "spaced_pass_context", + sample: "PASS = opaqueCredentialPayloadZ1234567890", + rawSecret: "opaqueCredentialPayloadZ1234567890", + }, + { + name: "generic_punctuation_context", + sample: "API_KEY=,OpaqueCredentialPayloadZ1234567890", + rawSecret: ",OpaqueCredentialPayloadZ1234567890", + }, + { + name: "hyphenated_api_key_context", + sample: "X-Api-Key=opaqueCredentialPayloadZ1234567890", + rawSecret: "opaqueCredentialPayloadZ1234567890", + }, + { + name: "reply_token_context", + sample: '{"replyToken":"opaqueCredentialPayloadZ1234567890"}', + rawSecret: "opaqueCredentialPayloadZ1234567890", + }, + { + name: "python_extra_next_line_context", + sample: "API_KEY=12345\u00856789012345", + rawSecret: "12345\u00856789012345", + }, + { + name: "python_extra_file_separator_context", + sample: "API_KEY=12345\u001c6789012345", + rawSecret: "12345\u001c6789012345", + }, ]; - const canonicalFingerprints = [...TOKEN_PREFIX_PATTERNS, ...CONTEXT_PATTERNS].map(fingerprint); + const canonicalFingerprints = [ + ...TOKEN_PREFIX_PATTERNS, + ...CONTEXT_PATTERNS, + ...SECRET_BLOCK_PATTERNS, + ].map(fingerprint); expect([...canonicalSamples.keys()]).toEqual(canonicalFingerprints); for (const { name, sample, rawSecret } of [...canonicalSamples.values(), ...extraSamples]) { @@ -591,7 +663,22 @@ describe("Deep Agents Code TUI startup check helpers", () => { } expect(redactsSecret(langsmithPt)).toBe("[REDACTED_SECRET]"); expect(redactsSecret(langsmithSk)).toBe("[REDACTED_SECRET]"); - expect(detectsSecret("plain startup text")).toBe("clean"); + for (const benign of [ + "plain startup text", + "COMPASS=opaqueNonSecretPayload123", + "BYPASS=allowedValue123", + "TOPSECRET=opaqueNonSecretPayload123", + "SUBTOKEN=opaqueNonSecretPayload123", + "publicKey=opaqueVerificationMaterial123", + "customKey=opaqueNonSecretPayload123", + "public-key=opaqueVerificationMaterial123", + "custom-key=opaqueNonSecretPayload123", + '{"key":"agent:main:main"}', + '{"correlationMarker":"reply-correlation-marker-123"}', + ]) { + expect(detectsSecret(benign), benign).toBe("clean"); + expect(redactsSecret(benign), benign).toBe(benign); + } }); it("removes raw TUI startup artifacts after writing the sanitized capture", () => { diff --git a/test/e2e/e2e-cloud-experimental/checks/03-deepagents-code-nemotron-ultra-profile.sh b/test/e2e/e2e-cloud-experimental/checks/03-deepagents-code-nemotron-ultra-profile.sh index 06ba2530c92..7800cba9fdf 100755 --- a/test/e2e/e2e-cloud-experimental/checks/03-deepagents-code-nemotron-ultra-profile.sh +++ b/test/e2e/e2e-cloud-experimental/checks/03-deepagents-code-nemotron-ultra-profile.sh @@ -38,12 +38,14 @@ import importlib.metadata from pathlib import Path import tomllib +from deepagents.profiles import _builtin_profiles from deepagents.profiles.harness import _nvidia_nemotron_3_ultra from deepagents.profiles.harness.harness_profiles import _harness_profile_for_model from langchain_openai import ChatOpenAI CONFIG_PATH = Path("/sandbox/.deepagents/config.toml") EXPECTED_VERSIONS = { + "nemoclaw-deepagents-profile": "0.1.0", "deepagents-code": "0.1.34", "deepagents": "0.7.0a6", } @@ -54,6 +56,9 @@ MANAGED_MODEL_IDS = ( EXPECTED_NATIVE_PROFILE_SHA256 = ( "c8e8dd2b0182334b54be4f46ff0c7b45fbb95dc13bd9a92c249eb47a14fa13d7" ) +EXPECTED_BOOTSTRAP_SHA256 = ( + "005a91e7fc4ca6b21220673dd9d02d6686bf63e1e4f1102d124b01f96886efcf" +) EXPECTED_MIDDLEWARE = [ "NemotronProgressBudgetMiddleware", "NemotronPolicyNudgeMiddleware", @@ -73,9 +78,25 @@ for distribution, expected in EXPECTED_VERSIONS.items(): actual = importlib.metadata.version(distribution) assert actual == expected, (distribution, actual) +profile_entry_points = [ + entry_point + for entry_point in importlib.metadata.entry_points( + group="deepagents.harness_profiles" + ) + if entry_point.name == "nemoclaw-managed-aliases" +] +assert len(profile_entry_points) == 1, profile_entry_points +profile_entry_point = profile_entry_points[0] +assert profile_entry_point.value == "nemoclaw_deepagents_profile:register" +assert profile_entry_point.dist is not None +assert profile_entry_point.dist.metadata["Name"] == "nemoclaw-deepagents-profile" + native_profile_path = Path(_nvidia_nemotron_3_ultra.__file__) native_profile_hash = hashlib.sha256(native_profile_path.read_bytes()).hexdigest() assert native_profile_hash == EXPECTED_NATIVE_PROFILE_SHA256, native_profile_hash +bootstrap_path = Path(_builtin_profiles.__file__) +bootstrap_hash = hashlib.sha256(bootstrap_path.read_bytes()).hexdigest() +assert bootstrap_hash == EXPECTED_BOOTSTRAP_SHA256, bootstrap_hash config = tomllib.loads(CONFIG_PATH.read_text(encoding="utf-8")) default_model = config["models"]["default"] @@ -89,8 +110,24 @@ assert provider["enabled"] is True assert provider["params"] == {"use_responses_api": False} +class ProfileOnlyChatOpenAI(ChatOpenAI): + """Fail closed if local profile resolution ever attempts inference.""" + + def _generate(self, *args, **kwargs): + raise AssertionError("profile contract attempted synchronous inference") + + async def _agenerate(self, *args, **kwargs): + raise AssertionError("profile contract attempted asynchronous inference") + + def _stream(self, *args, **kwargs): + raise AssertionError("profile contract attempted synchronous streaming") + + async def _astream(self, *args, **kwargs): + raise AssertionError("profile contract attempted asynchronous streaming") + + def make_model(model_id): - return ChatOpenAI( + return ProfileOnlyChatOpenAI( model=model_id, api_key="nemoclaw-managed-placeholder", base_url=provider["base_url"], @@ -122,11 +159,14 @@ for model_id in MANAGED_MODEL_IDS: unrelated = _harness_profile_for_model(make_model("gpt-4.1-mini"), None) assert unrelated.system_prompt_suffix is None assert middleware_names(unrelated) == [] +assert hashlib.sha256(native_profile_path.read_bytes()).hexdigest() == native_profile_hash +assert hashlib.sha256(bootstrap_path.read_bytes()).hexdigest() == bootstrap_hash print( "NEMOCLAW_NEMOTRON_ULTRA_PROFILE_OK:" f"{default_model}:dcode={EXPECTED_VERSIONS['deepagents-code']}:" - f"deepagents={EXPECTED_VERSIONS['deepagents']}" + f"deepagents={EXPECTED_VERSIONS['deepagents']}:" + f"plugin={EXPECTED_VERSIONS['nemoclaw-deepagents-profile']}" ) PY } diff --git a/test/e2e/e2e-cloud-experimental/checks/10-deepagents-code-tui-startup.sh b/test/e2e/e2e-cloud-experimental/checks/10-deepagents-code-tui-startup.sh index c14eec51296..14592deb042 100755 --- a/test/e2e/e2e-cloud-experimental/checks/10-deepagents-code-tui-startup.sh +++ b/test/e2e/e2e-cloud-experimental/checks/10-deepagents-code-tui-startup.sh @@ -66,9 +66,12 @@ contains_secret() { $context_value_pattern = $ENV{"NEMOCLAW_CONTEXT_SECRET_VALUE_PATTERN"}; } if ( + /-----BEGIN (?:[A-Z0-9]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z0-9]+ )?PRIVATE KEY-----/ || /$token_pattern/ || /Bearer\s+$context_value_pattern/i || - /(?:_KEY|API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)[=: ]["'\''"]?$context_value_pattern/i + /(?:^|[^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)["'\''"]?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["'\''"]?[^\s"'\''"]{10,}/i || + /(?:^|[^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))["'\''"]?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["'\''"]?[^\s"'\''"]{10,}/ || + /(?:^|[^A-Za-z0-9])KEY["'\''"]?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["'\''"]?[^\s"'\''"]{10,}/ ) { $found = 1; } @@ -84,9 +87,12 @@ redact_secrets() { $token_pattern = $ENV{"NEMOCLAW_TOKEN_SECRET_PATTERN"}; $context_value_pattern = $ENV{"NEMOCLAW_CONTEXT_SECRET_VALUE_PATTERN"}; } + s/-----BEGIN (?:[A-Z0-9]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z0-9]+ )?PRIVATE KEY-----/[REDACTED_SECRET]/g; s/$token_pattern/[REDACTED_SECRET]/g; s/(Bearer\s+)$context_value_pattern/${1}[REDACTED_SECRET]/gi; - s/((?:_KEY|API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)[=: ]["'\''"]?)$context_value_pattern/${1}[REDACTED_SECRET]/gi; + s/((?:^|[^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)["'\''"]?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["'\''"]?)[^\s"'\''"]{10,}/${1}[REDACTED_SECRET]/gim; + s/((?:^|[^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))["'\''"]?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["'\''"]?)[^\s"'\''"]{10,}/${1}[REDACTED_SECRET]/gm; + s/((?:^|[^A-Za-z0-9])KEY["'\''"]?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["'\''"]?)[^\s"'\''"]{10,}/${1}[REDACTED_SECRET]/gm; ' } diff --git a/test/e2e/fixtures/redaction.ts b/test/e2e/fixtures/redaction.ts index fad529357c9..656844eac21 100644 --- a/test/e2e/fixtures/redaction.ts +++ b/test/e2e/fixtures/redaction.ts @@ -29,10 +29,22 @@ * - child-env allowlist filtering for fixture probes */ +import { randomUUID } from "node:crypto"; import type { Readable, Writable } from "node:stream"; const REDACTED = ""; const EXPLICIT_REDACTED = "[REDACTED]"; +const MANAGED_CREDENTIAL_REFERENCE_SOURCE = String.raw`(?:(?:Bearer[ \t]+)?openshell:resolve:env:(?:v[0-9]{1,20}_)?[A-Z][A-Z0-9_]{0,127}|(?:xoxb|xapp)-OPENSHELL-RESOLVE-ENV-(?:v[0-9]{1,20}_)?[A-Z][A-Z0-9_]{0,127})`; +const SAFE_QUOTED_CREDENTIAL_REFERENCE_PATTERN = new RegExp( + `(["'])${MANAGED_CREDENTIAL_REFERENCE_SOURCE}\\1`, + "g", +); +const SAFE_STANDALONE_CREDENTIAL_REFERENCE_PATTERN = new RegExp( + `(^|[ \\t\\r\\n])${MANAGED_CREDENTIAL_REFERENCE_SOURCE}(?=$|[ \\t\\r\\n])`, + "g", +); +const SAFE_ENV_ASSIGNMENT_PATTERN = + /^[ \t]*(?:export[ \t]+)?([A-Z][A-Z0-9_]{0,127})[ \t]*=[ \t]*(?:(?:Bearer[ \t]+)?openshell:resolve:env:(?:v[0-9]{1,20}_)?\1|(?:xoxb|xapp)-OPENSHELL-RESOLVE-ENV-(?:v[0-9]{1,20}_)?\1)[ \t]*$/gm; // Fixture-local mirror of src/lib/security/secret-patterns.ts. The // fixture layer deliberately does not import from src/lib/security/ so it @@ -73,7 +85,9 @@ export const TOKEN_PREFIX_PATTERNS: RegExp[] = [ export const CONTEXT_PATTERNS: RegExp[] = [ /(?<=Bearer\s+)[A-Za-z0-9_.+/=-]{10,}/gi, - /(?<=(?:_KEY|API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)[=: ]['"]?)[A-Za-z0-9_.+/=-]{10,}/gi, + /(?<=(?:^|[^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)["']?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["']?)[^\s'"]{10,}/gi, + /(?<=(?:^|[^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))["']?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["']?)[^\s'"]{10,}/g, + /(?<=(?:^|[^A-Za-z0-9])KEY["']?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["']?)[^\s'"]{10,}/g, ]; export const SECRET_BLOCK_PATTERNS: RegExp[] = [ @@ -96,6 +110,47 @@ export const SECRET_BLOCK_PATTERNS: RegExp[] = [ * env allowlist (buildChildEnv); pattern redaction catches what slips * through (e.g. error messages that echo a secret value). */ +function redactCanonicalShapes(text: string): string { + let out = text; + for (const p of TOKEN_PREFIX_PATTERNS) { + p.lastIndex = 0; + out = out.replace(p, REDACTED); + } + for (const p of SECRET_BLOCK_PATTERNS) { + p.lastIndex = 0; + out = out.replace(p, REDACTED); + } + for (const p of CONTEXT_PATTERNS) { + p.lastIndex = 0; + out = out.replace(p, REDACTED); + } + return out; +} + +function protectManagedCredentialReferences(text: string): { + protectedText: string; + references: Array<{ marker: string; value: string }>; +} { + let protectedText = text; + const references: Array<{ marker: string; value: string }> = []; + let markerPrefix: string; + do { + markerPrefix = `\uE000 ${randomUUID()} `; + } while (text.includes(markerPrefix)); + const protect = (pattern: RegExp): void => { + pattern.lastIndex = 0; + protectedText = protectedText.replace(pattern, (value) => { + const marker = `${markerPrefix}${references.length} \uE001`; + references.push({ marker, value }); + return marker; + }); + }; + protect(SAFE_ENV_ASSIGNMENT_PATTERN); + protect(SAFE_QUOTED_CREDENTIAL_REFERENCE_PATTERN); + protect(SAFE_STANDALONE_CREDENTIAL_REFERENCE_PATTERN); + return { protectedText, references }; +} + export function redactString(text: string, explicitValues?: Iterable): string { if (!text) return text; let out = text; @@ -108,19 +163,12 @@ export function redactString(text: string, explicitValues?: Iterable): s out = out.split(value).join(EXPLICIT_REDACTED); } } - for (const p of TOKEN_PREFIX_PATTERNS) { - p.lastIndex = 0; - out = out.replace(p, REDACTED); - } - for (const p of CONTEXT_PATTERNS) { - p.lastIndex = 0; - out = out.replace(p, REDACTED); - } - for (const p of SECRET_BLOCK_PATTERNS) { - p.lastIndex = 0; - out = out.replace(p, REDACTED); + const { protectedText, references } = protectManagedCredentialReferences(out); + let redacted = redactCanonicalShapes(protectedText); + for (const { marker, value } of references) { + redacted = redacted.replace(marker, value); } - return out; + return redacted; } // Env keys the fixture layer guarantees children may always see. Anything @@ -157,7 +205,7 @@ const FIXTURE_ENV_PREFIXES: readonly string[] = ["E2E_", "NEMOCLAW_LOG_"]; // non-secret values via the secretEnv channel and keeps the // "fixture-allowlist vs declared-secret" distinction honest. const SECRET_ENV_KEY_SHAPE = - /^[A-Z][A-Z0-9_]*(?:API[_]?KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|PASSPHRASE|PRIVATE[_]?KEY|ACCESS[_]?KEY)$/; + /^(?:[A-Z][A-Z0-9_]*_)?(?:API[_]?KEY|TOKEN|SECRET|PASSWORD|PASSWD|PASS|CREDENTIAL|PASSPHRASE|PRIVATE[_]?KEY|ACCESS[_]?KEY)$/; export function isValidSecretEnvKey(key: string): boolean { return SECRET_ENV_KEY_SHAPE.test(key); @@ -216,7 +264,7 @@ export function buildChildEnv( if (!isValidSecretEnvKey(key)) { throw new Error( `secretEnv entry '${key}' does not match the secret-key shape ` + - `(must end with API_KEY, TOKEN, SECRET, PASSWORD, CREDENTIAL, ` + + `(must end with API_KEY, TOKEN, SECRET, PASSWORD, PASSWD, PASS, CREDENTIAL, ` + `PASSPHRASE, PRIVATE_KEY, or ACCESS_KEY). Refusing to allowlist.`, ); } diff --git a/test/e2e/live/openclaw-tui-chat-correlation.test.ts b/test/e2e/live/openclaw-tui-chat-correlation.test.ts index cf4fbdd44bc..264948da0f2 100644 --- a/test/e2e/live/openclaw-tui-chat-correlation.test.ts +++ b/test/e2e/live/openclaw-tui-chat-correlation.test.ts @@ -60,7 +60,7 @@ type ChatEventPayload = { type GatewayEvent = { event?: string; payload?: ChatEventPayload; ts?: number }; type SentRun = { promptToken: string; - replyToken: string; + replyMarker: string; runId: string; message: string; }; @@ -76,7 +76,7 @@ type CompactChatEvent = { errorMessage?: string; }; type UncorrelatedReply = { - replyToken: string; + replyMarker: string; expectedRunId: string; actualRunId?: string; state?: string; @@ -86,7 +86,7 @@ type Issue2603Analysis = { chatEvents: CompactChatEvent[]; emptyFinalsForSubmittedRuns: CompactChatEvent[]; missingReplies: string[]; - duplicateReplies: { replyToken: string; count: number }[]; + duplicateReplies: { replyMarker: string; count: number }[]; uncorrelatedReplies: UncorrelatedReply[]; finalReplyOrder: string[]; userTurnOrder: string[]; @@ -141,7 +141,9 @@ function analyzeIssue2603Trace({ historyMessages, }: Issue2603Trace): Issue2603Analysis { const submittedRunIds = new Set(sentRuns.map((entry) => entry.runId)); - const expectedRunByReplyToken = new Map(sentRuns.map((entry) => [entry.replyToken, entry.runId])); + const expectedRunByReplyMarker = new Map( + sentRuns.map((entry) => [entry.replyMarker, entry.runId]), + ); const chatEvents = compactChatEvents(events); const emptyFinalsForSubmittedRuns = chatEvents.filter( @@ -155,16 +157,16 @@ function analyzeIssue2603Trace({ const uncorrelatedReplies: UncorrelatedReply[] = []; const visibleReplyCounts = new Map(); const finalReplyCounts = new Map(); - for (const [replyToken, expectedRunId] of expectedRunByReplyToken) { + for (const [replyMarker, expectedRunId] of expectedRunByReplyMarker) { for (const event of chatEvents) { - if (!containsReplyTokenAllowingWhitespace(event.text, replyToken)) continue; - visibleReplyCounts.set(replyToken, (visibleReplyCounts.get(replyToken) ?? 0) + 1); + if (!containsReplyTokenAllowingWhitespace(event.text, replyMarker)) continue; + visibleReplyCounts.set(replyMarker, (visibleReplyCounts.get(replyMarker) ?? 0) + 1); if (event.state === "final") { - finalReplyCounts.set(replyToken, (finalReplyCounts.get(replyToken) ?? 0) + 1); + finalReplyCounts.set(replyMarker, (finalReplyCounts.get(replyMarker) ?? 0) + 1); } if (event.runId !== expectedRunId) { uncorrelatedReplies.push({ - replyToken, + replyMarker, expectedRunId, actualRunId: event.runId, state: event.state, @@ -173,20 +175,20 @@ function analyzeIssue2603Trace({ } } const missingReplies = sentRuns - .map((entry) => entry.replyToken) - .filter((replyToken) => !visibleReplyCounts.has(replyToken)); + .map((entry) => entry.replyMarker) + .filter((replyMarker) => !visibleReplyCounts.has(replyMarker)); const duplicateReplies = sentRuns .map((entry) => ({ - replyToken: entry.replyToken, - count: finalReplyCounts.get(entry.replyToken) ?? 0, + replyMarker: entry.replyMarker, + count: finalReplyCounts.get(entry.replyMarker) ?? 0, })) .filter((entry) => entry.count > 1); const finalReplyOrder = chatEvents .filter((event) => event.state === "final") .flatMap((event) => sentRuns - .filter((entry) => containsReplyTokenAllowingWhitespace(event.text, entry.replyToken)) - .map((entry) => entry.replyToken), + .filter((entry) => containsReplyTokenAllowingWhitespace(event.text, entry.replyMarker)) + .map((entry) => entry.replyMarker), ); const userMessages = historyMessages @@ -289,8 +291,8 @@ function compactReplyTokenText(value) { return String(value || "").replace(/\s+/g, ""); } -function sawAllReplies(replyTokens) { - return replyTokens.every((token) => events.some((event) => event.event === "chat" && compactReplyTokenText(textFromMessage(event.payload?.message)).includes(compactReplyTokenText(token)))); +function sawAllReplies(replyMarkers) { + return replyMarkers.every((marker) => events.some((event) => event.event === "chat" && compactReplyTokenText(textFromMessage(event.payload?.message)).includes(compactReplyTokenText(marker)))); } ws.on("message", (data) => { @@ -353,10 +355,10 @@ ws.on("open", async () => { ], ]; - for (const [promptToken, replyToken, message] of messages) { + for (const [promptToken, replyMarker, message] of messages) { const idempotencyKey = randomUUID(); const response = await request("chat.send", { sessionKey, message, deliver: false, timeoutMs: 90_000, idempotencyKey }); - sentRuns.push({ promptToken, replyToken, message, runId: response.runId ?? idempotencyKey }); + sentRuns.push({ promptToken, replyMarker, message, runId: response.runId ?? idempotencyKey }); await new Promise((resolve) => setTimeout(resolve, 1_000)); } @@ -569,7 +571,7 @@ test( expect(analysis.missingReplies, failureSummary).toEqual([]); expect(analysis.duplicateReplies, failureSummary).toEqual([]); expect(analysis.finalReplyOrder, failureSummary).toEqual( - repro.sentRuns.map((entry) => entry.replyToken), + repro.sentRuns.map((entry) => entry.replyMarker), ); expect(analysis.missingUserTurns, failureSummary).toEqual([]); expect(analysis.duplicateUserTurns, failureSummary).toEqual([]); diff --git a/test/e2e/support/dcode-profile-import-gate-workflow-boundary.test.ts b/test/e2e/support/dcode-profile-import-gate-workflow-boundary.test.ts new file mode 100644 index 00000000000..b5c30b9fa79 --- /dev/null +++ b/test/e2e/support/dcode-profile-import-gate-workflow-boundary.test.ts @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { describe, expect, it } from "vitest"; +import YAML from "yaml"; + +import { validateE2eWorkflowBoundary } from "../../../tools/e2e/workflow-boundary.mts"; + +const WORKFLOW_PATH = path.join(process.cwd(), ".github/workflows/e2e.yaml"); +const GATE_STEP_NAME = "Verify DCode profile import gate rejects missing base dependencies"; +const CLEANUP_STEP_NAME = "Clean up Docker auth"; + +type WorkflowStep = { + env?: Record; + if?: string; + name?: string; + run?: string; + shell?: string; +}; + +type Workflow = { + jobs: Record; +}; + +function readWorkflow(): Workflow { + return YAML.parse(fs.readFileSync(WORKFLOW_PATH, "utf8")) as Workflow; +} + +function validateMutation(mutate: (workflow: Workflow) => void): string[] { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "dcode-profile-import-gate-workflow-")); + const workflowPath = path.join(tmp, "workflow.yaml"); + const workflow = readWorkflow(); + mutate(workflow); + fs.writeFileSync(workflowPath, YAML.stringify(workflow)); + try { + return validateE2eWorkflowBoundary(workflowPath); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } +} + +function liveGateStep(workflow: Workflow): WorkflowStep { + return workflow.jobs.live.steps.find((step) => step.name === GATE_STEP_NAME)!; +} + +describe("DCode missing-dependency profile import gate workflow boundary", () => { + it("rejects replacing the reviewed gate with a mutable registry base", () => { + const errors = validateMutation((workflow) => { + liveGateStep(workflow).run = + "docker pull ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base:latest && docker tag ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base:latest nemoclaw-dcode-profile-source-base:mutable"; + }); + + expect(errors).toContain( + "live DCode profile import gate must run the reviewed negative-build script", + ); + }); + + it("rejects widening the negative build beyond the typed DCode target", () => { + const errors = validateMutation((workflow) => { + liveGateStep(workflow).if = "${{ always() }}"; + }); + + expect(errors).toContain( + "live DCode profile import gate must be scoped to the typed DCode target", + ); + }); + + it("rejects a mutable registry base override", () => { + const errors = validateMutation((workflow) => { + liveGateStep(workflow).env = { + NEMOCLAW_DCODE_PROFILE_GATE_BASE_IMAGE: + "ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base:latest", + }; + }); + + expect(errors).toContain( + "live DCode profile import gate must build the reviewed repository base without an override", + ); + }); + + it("rejects moving the import gate after live inference", () => { + const errors = validateMutation((workflow) => { + const steps = workflow.jobs.live.steps; + const gate = liveGateStep(workflow); + steps.splice(steps.indexOf(gate), 1); + steps.push(gate); + }); + + expect(errors).toContain("live DCode profile import gate must run before live E2E tests"); + }); + + it("rejects moving the import gate before workspace prep", () => { + const errors = validateMutation((workflow) => { + const steps = workflow.jobs.live.steps; + const gate = liveGateStep(workflow); + steps.splice(steps.indexOf(gate), 1); + steps.unshift(gate); + }); + + expect(errors).toContain("live DCode profile import gate must run after workspace prep"); + }); + + it("rejects moving Docker auth cleanup before the import gate", () => { + const errors = validateMutation((workflow) => { + const steps = workflow.jobs.live.steps; + const cleanup = steps.find((step) => step.name === CLEANUP_STEP_NAME)!; + steps.splice(steps.indexOf(cleanup), 1); + steps.splice(steps.indexOf(liveGateStep(workflow)), 0, cleanup); + }); + + expect(errors).toContain("live Docker Hub cleanup must be the final job step"); + }); + + it("rejects running the import gate with a non-bash shell", () => { + const errors = validateMutation((workflow) => { + liveGateStep(workflow).shell = "sh"; + }); + + expect(errors).toContain("live DCode profile import gate must use bash"); + }); +}); diff --git a/test/e2e/support/e2e-redaction-entry.test.ts b/test/e2e/support/e2e-redaction-entry.test.ts index c7f90e8817c..266c032b45e 100644 --- a/test/e2e/support/e2e-redaction-entry.test.ts +++ b/test/e2e/support/e2e-redaction-entry.test.ts @@ -22,11 +22,27 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; import { ArtifactSink } from "../fixtures/artifacts.ts"; -import { buildChildEnv, redactString } from "../fixtures/redaction.ts"; +import { buildChildEnv, isValidSecretEnvKey, redactString } from "../fixtures/redaction.ts"; import { SecretStore } from "../fixtures/secrets.ts"; import { ShellProbe, trustedShellCommand } from "../fixtures/shell-probe.ts"; describe("fixture redaction entry point", () => { + it("recognizes pass env names only at exact or underscore-delimited boundaries", () => { + for (const key of ["PASS", "PASSWD", "CUSTOM_PASS", "CUSTOM_PASSWD"]) { + expect(isValidSecretEnvKey(key), key).toBe(true); + } + for (const key of ["COMPASS", "BYPASS", "PASSENGER_COUNT", "PASSED"]) { + expect(isValidSecretEnvKey(key), key).toBe(false); + } + + expect( + buildChildEnv( + { COMPASS: "north", BYPASS: "allowed" }, + { fixtureOverlay: {}, additionalAllowedEnv: ["COMPASS", "BYPASS"] }, + ), + ).toMatchObject({ COMPASS: "north", BYPASS: "allowed" }); + }); + it("passes only the workflow-owned trace directory through child env", () => { const childEnv = buildChildEnv( { @@ -119,6 +135,58 @@ describe("fixture redaction entry point", () => { expect(redactString("nothing sensitive here", [])).toBe("nothing sensitive here"); }); + it("preserves managed credential references and non-credential JSON identifiers", () => { + const discordReference = "openshell:resolve:env:DISCORD_BOT_TOKEN"; + const versionedReference = "openshell:resolve:env:v2237303833964223913_WECHAT_BOT_TOKEN"; + const slackReference = "xoxb-OPENSHELL-RESOLVE-ENV-SLACK_BOT_TOKEN"; + const discordAssignment = `DISCORD_BOT_TOKEN=${discordReference}`; + const text = JSON.stringify({ + key: "agent:main:main", + replyMarker: "A2603-REPLY", + token: discordReference, + versionedToken: versionedReference, + botToken: slackReference, + }); + + expect(redactString(text)).toBe(text); + expect(redactString(discordAssignment)).toBe(discordAssignment); + const collision = `\uE000 NEMOCLAW_SAFE_CREDENTIAL_REFERENCE_0 \uE001 ${text}`; + expect(redactString(collision)).toBe(collision); + expect(redactString(text, [discordReference])).not.toContain(discordReference); + expect(redactString('{"replyToken":"opaqueCredentialPayloadZ1234567890"}')).toBe( + '{"replyToken":""}', + ); + + const privateKey = [ + ["-----BEGIN", "PRIVATE KEY-----"].join(" "), + `opaquePrivateMaterial123 ${discordReference} morePrivateMaterial456`, + ["-----END", "PRIVATE KEY-----"].join(" "), + ].join("\n"); + expect(redactString(privateKey)).toBe(""); + }); + + it.each([ + ["attached suffix", "TOKEN=openshell:resolve:env:FOO-opaqueCredentialPayloadZ1234567890"], + ["dot suffix", "TOKEN=openshell:resolve:env:FOO.opaqueCredentialPayloadZ1234567890"], + ["slash suffix", "TOKEN=openshell:resolve:env:FOO/opaqueCredentialPayloadZ1234567890"], + ["colon suffix", "TOKEN=openshell:resolve:env:FOO:opaqueCredentialPayloadZ1234567890"], + ["semicolon suffix", "TOKEN=openshell:resolve:env:FOO;opaqueCredentialPayloadZ1234567890"], + ["hash suffix", "TOKEN=openshell:resolve:env:FOO#opaqueCredentialPayloadZ1234567890"], + ["comma suffix", "TOKEN=openshell:resolve:env:FOO,opaqueCredentialPayloadZ1234567890"], + ["brace suffix", "TOKEN=openshell:resolve:env:FOO}opaqueCredentialPayloadZ1234567890"], + ["bracket suffix", "TOKEN=openshell:resolve:env:FOO]opaqueCredentialPayloadZ1234567890"], + ["nested assignment", "TOKEN=foo=openshell:resolve:env:FOO"], + ["short prefix", "TOKEN=short:openshell:resolve:env:FOO"], + ["oversized revision", `TOKEN=openshell:resolve:env:v${"1".repeat(21)}_FOO`], + ["oversized identifier", `TOKEN=openshell:resolve:env:${"A".repeat(129)}`], + ["mixed case", "TOKEN=OpenShell:Resolve:Env:FOO"], + ["lowercase Slack", "TOKEN=xoxb-openshell-resolve-env-SLACK_BOT_TOKEN"], + ])("redacts a managed-reference lookalike with $label", (_label, value) => { + const out = redactString(value); + expect(out).toContain(""); + expect(out).not.toContain(value.slice("TOKEN=".length)); + }); + it("returns empty input verbatim", () => { expect(redactString("")).toBe(""); expect(redactString("", ["anything"])).toBe(""); diff --git a/test/e2e/support/platform-parity-cloud-experimental.test.ts b/test/e2e/support/platform-parity-cloud-experimental.test.ts index 4d2e79c5d1b..1499b85c65c 100644 --- a/test/e2e/support/platform-parity-cloud-experimental.test.ts +++ b/test/e2e/support/platform-parity-cloud-experimental.test.ts @@ -308,7 +308,7 @@ describe("P0-E cloud-experimental parity guardrails", () => { expect(result.status, result.stderr).toBe(0); }); - it("registers executable Deep Agents cloud-experimental checks", () => { + it("registers executable Deep Agents cloud-experimental checks in execution order", () => { expect(DEEPAGENTS_CLOUD_EXPERIMENTAL_CHECKS).toEqual([ "test/e2e/e2e-cloud-experimental/checks/03-deepagents-code-nemotron-ultra-profile.sh", "test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh", @@ -328,27 +328,6 @@ describe("P0-E cloud-experimental parity guardrails", () => { } }); - it("checks the stock Nemotron Ultra profile before destructive re-onboarding", () => { - const profileCheckPath = DEEPAGENTS_CLOUD_EXPERIMENTAL_CHECKS[0]; - const profileCheck = fs.readFileSync(path.join(process.cwd(), profileCheckPath), "utf8"); - - expect(profileCheckPath).toBe( - "test/e2e/e2e-cloud-experimental/checks/03-deepagents-code-nemotron-ultra-profile.sh", - ); - expect(profileCheck).toContain("/opt/venv/bin/python3 -I -"); - expect(profileCheck).toContain("from langchain_openai import ChatOpenAI"); - expect(profileCheck).toContain("_harness_profile_for_model(make_model(model_id), None)"); - expect(profileCheck).toContain('"nvidia/nemotron-3-ultra-550b-a55b"'); - expect(profileCheck).toContain('"nvidia/nvidia/nemotron-3-ultra"'); - expect(profileCheck).toContain('"deepagents-code": "0.1.34"'); - expect(profileCheck).toContain('"deepagents": "0.7.0a6"'); - expect(profileCheck).toContain("_nvidia_nemotron_3_ultra.__file__"); - expect(profileCheck).toContain('description_overrides["read_file"]'); - expect(profileCheck).toContain("middleware_names(profile) == EXPECTED_MIDDLEWARE"); - expect(profileCheck).toContain('make_model("gpt-4.1-mini")'); - expect(profileCheck).not.toMatch(/\.(?:invoke|ainvoke|stream|astream)\(/); - }); - it("gives the destructive fresh re-onboard check its onboarding budget", () => { expect( cloudExperimentalCheckTimeoutMs( diff --git a/test/fixtures/deepagents-observability-harness.py b/test/fixtures/deepagents-observability-harness.py index 65bba7bf52c..2ed7ac11d8e 100644 --- a/test/fixtures/deepagents-observability-harness.py +++ b/test/fixtures/deepagents-observability-harness.py @@ -1056,12 +1056,22 @@ def _privacy_scenario(path: Path) -> dict[str, Any]: "bearer": SECRET, "clientSecret": SECRET, "credential": SECRET, + "customPasswd": SECRET, + "DBPass": SECRET, "header": SECRET, "nested": {"checkpoint_id": SECRET, "command": "allowed"}, "opaque": _HostileCaptureObject(), "oversized": "x" * 9000, + "pass": SECRET, "passwd": SECRET, + "passCount": 4, + "passRate": 0.9, + "passThrough": "allowed", "privateKey": SECRET, + "correlationMarker": "reply-correlation-marker-123", + "replyToken": "opaqueCredentialPayloadZ1234567890", + "ReplyToken": SECRET, + "reply_token": SECRET, "token": SECRET, }, ) diff --git a/test/helpers/langchain-deepagents-code-secret-patterns.ts b/test/helpers/langchain-deepagents-code-secret-patterns.ts index 1ab3b954c81..14320ae4c40 100644 --- a/test/helpers/langchain-deepagents-code-secret-patterns.ts +++ b/test/helpers/langchain-deepagents-code-secret-patterns.ts @@ -164,6 +164,138 @@ export const CANONICAL_SECRET_POSITIVE_VECTORS: readonly CanonicalSecretPositive patternGroup: "context", patternIndex: 1, }, + { + label: "credential_json_context", + value: '{"API_KEY":"opaqueCredentialPayloadZ1234567890"}', + patternGroup: "context", + patternIndex: 1, + }, + { + label: "credential_spaced_context", + value: "API_KEY = opaqueCredentialPayloadZ1234567890", + patternGroup: "context", + patternIndex: 1, + }, + { + label: "credential_punctuation_leading", + value: "API_KEY=,OpaqueCredentialPayloadZ1234567890", + patternGroup: "context", + patternIndex: 1, + }, + { + label: "hyphenated_api_key_context", + value: "api-key=opaqueCredentialPayloadZ1234567890", + patternGroup: "context", + patternIndex: 1, + }, + { + label: "mixed_case_x_api_key_context", + value: "X-Api-Key=opaqueCredentialPayloadZ1234567890", + patternGroup: "context", + patternIndex: 1, + }, + { + label: "python_extra_next_line_context", + value: "API_KEY=12345\u00856789012345", + patternGroup: "context", + patternIndex: 1, + }, + { + label: "python_extra_file_separator_context", + value: "API_KEY=12345\u001c6789012345", + patternGroup: "context", + patternIndex: 1, + }, + { + label: "uppercase_key_context", + value: "KEY=opaqueCredentialPayloadZ1234567890", + patternGroup: "context", + patternIndex: 3, + }, + { + label: "pass_context", + value: "CUSTOM_PASS=opaqueCredentialPayloadZ1234567890", + patternGroup: "context", + patternIndex: 1, + }, + { + label: "passwd_context", + value: "CUSTOM_PASSWD=opaqueCredentialPayloadZ1234567890", + patternGroup: "context", + patternIndex: 1, + }, + { + label: "pass_punctuation_leading", + value: "CUSTOM_PASS=!OpaquePassword123", + patternGroup: "context", + patternIndex: 1, + }, + { + label: "pass_punctuation_tail", + value: "CUSTOM_PASS=abcdefghij!tail-secret", + patternGroup: "context", + patternIndex: 1, + }, + { + label: "pass_json_context", + value: '{"PASS":"opaqueCredentialPayloadZ1234567890"}', + patternGroup: "context", + patternIndex: 1, + }, + { + label: "pass_spaced_context", + value: "PASS = opaqueCredentialPayloadZ1234567890", + patternGroup: "context", + patternIndex: 1, + }, + { + label: "pass_colon_context", + value: "PASS: opaqueCredentialPayloadZ1234567890", + patternGroup: "context", + patternIndex: 1, + }, + { + label: "client_secret_context", + value: "clientSecret=opaqueCredentialPayloadZ1234567890", + patternGroup: "context", + patternIndex: 2, + }, + { + label: "github_token_context", + value: "githubToken=opaqueCredentialPayloadZ1234567890", + patternGroup: "context", + patternIndex: 2, + }, + { + label: "webhook_secret_context", + value: "webhookSecret=opaqueCredentialPayloadZ1234567890", + patternGroup: "context", + patternIndex: 2, + }, + { + label: "database_credential_context", + value: "databaseCredential=opaqueCredentialPayloadZ1234567890", + patternGroup: "context", + patternIndex: 2, + }, + { + label: "custom_pass_context", + value: "customPass=opaqueCredentialPayloadZ1234567890", + patternGroup: "context", + patternIndex: 2, + }, + { + label: "acronym_pass_context", + value: "DBPass=opaqueCredentialPayloadZ1234567890", + patternGroup: "context", + patternIndex: 2, + }, + { + label: "camel_api_key_context", + value: "apiKey=opaqueCredentialPayloadZ1234567890", + patternGroup: "context", + patternIndex: 2, + }, { label: "private_key_block", value: "-----BEGIN TEST PRIVATE KEY-----\nopaque-test-body\n-----END TEST PRIVATE KEY-----", diff --git a/test/langchain-deepagents-code-image-credentials.test.ts b/test/langchain-deepagents-code-image-credentials.test.ts index 015f4437687..5754d0bde7b 100644 --- a/test/langchain-deepagents-code-image-credentials.test.ts +++ b/test/langchain-deepagents-code-image-credentials.test.ts @@ -127,6 +127,24 @@ describe("LangChain Deep Agents Code image credential boundary", () => { } }); + it("accepts the mounted OpenShell TLS key path only from runtime provenance", () => { + const name = "OPENSHELL_TLS_KEY"; + const value = "/etc/openshell/tls/client/tls.key"; + const runtimeDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-tls-runtime-")); + const runtimeFixture = makeWrapperFixture(runtimeDir); + const runtimeResult = runWrapper(runtimeFixture.wrapperPath, ["-n", "hi"], { [name]: value }); + expect(runtimeResult.status, runtimeResult.stderr).toBe(0); + expect(fs.existsSync(runtimeFixture.ranMarker)).toBe(true); + + const dotenvDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-tls-dotenv-")); + const dotenvFixture = makeWrapperFixture(dotenvDir); + fs.writeFileSync(dotenvFixture.envFile, `${name}=${value}\n`, "utf8"); + const dotenvResult = runWrapper(dotenvFixture.wrapperPath, ["-n", "hi"], {}); + expect(dotenvResult.status).not.toBe(0); + expect(dotenvResult.stderr).toContain(name); + expect(dotenvResult.stderr).not.toContain(value); + expect(fs.existsSync(dotenvFixture.ranMarker)).toBe(false); + }); it("allows nemoclaw-managed messaging tokens whose values are intentionally credential-shaped", () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-wrapper-")); const { wrapperPath, ranMarker } = makeWrapperFixture(tempDir); @@ -344,23 +362,37 @@ describe("LangChain Deep Agents Code image credential boundary", () => { expect(fs.existsSync(ranMarker)).toBe(false); }); - it.each([ - { label: "malformed JSON", content: "{not valid json at all" }, - { label: "present but unreadable", content: '{"credentials": null}', unreadable: true }, - ])("refuses to launch when auth.json is $label (fail-closed)", ({ content, unreadable }) => { + it("refuses to launch when auth.json is malformed JSON (fail-closed)", () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-auth-edge-")); const { wrapperPath, ranMarker, authFile } = makeWrapperFixture(tempDir); - fs.writeFileSync(authFile, content, "utf8"); - fs.chmodSync(authFile, unreadable ? 0o000 : 0o644); + fs.writeFileSync(authFile, "{not valid json at all", "utf8"); const result = runWrapper(wrapperPath, ["-n", "hi"], {}); expect(result.status).not.toBe(0); expect(result.stderr).toContain("auth.json"); expect(result.stderr).toContain("stored Deep Agents Code credentials"); expect(result.stdout).not.toContain("dcode-stub-ran"); expect(fs.existsSync(ranMarker)).toBe(false); - fs.chmodSync(authFile, 0o644); }); + it.skipIf(process.getuid?.() === 0)( + "refuses to launch when auth.json is present but unreadable (fail-closed)", + () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-auth-unreadable-")); + const { wrapperPath, ranMarker, authFile } = makeWrapperFixture(tempDir); + fs.writeFileSync(authFile, JSON.stringify({ version: 1, credentials: {} }), "utf8"); + fs.chmodSync(authFile, 0o000); + try { + const result = runWrapper(wrapperPath, ["-n", "hi"], {}); + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("auth.json"); + expect(result.stderr).toContain("stored Deep Agents Code credentials"); + expect(result.stdout).not.toContain("dcode-stub-ran"); + expect(fs.existsSync(ranMarker)).toBe(false); + } finally { + fs.chmodSync(authFile, 0o644); + } + }, + ); it("allows launch when auth.json is absent (fresh sandbox)", () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-auth-absent-")); const { wrapperPath, ranMarker, authFile } = makeWrapperFixture(tempDir); @@ -590,8 +622,33 @@ describe("LangChain Deep Agents Code image credential boundary", () => { expect(fs.existsSync(ranMarker)).toBe(false); }); - it("rejects exact canonical credential names KEY/TOKEN/SECRET/PASSWORD/CREDENTIAL with opaque payloads", () => { - const cases: string[] = ["KEY", "TOKEN", "SECRET", "PASSWORD", "CREDENTIAL", "API_KEY"]; + it("rejects the wrapper credential-name policy with opaque payloads", () => { + const cases = [ + "KEY", + "TOKEN", + "SECRET", + "PASSWORD", + "PASSWD", + "PASS", + "CREDENTIAL", + "API_KEY", + "CUSTOM_PASSWD", + "CUSTOM_PASS", + "customPass", + "customPasswd", + "DBPass", + "db_pass", + "db_passwd", + "db-pass", + "db-passwd", + "apiKey", + "accessToken", + "replyToken", + "clientSecret", + "myCredential", + "customPassword", + "privateKey", + ]; const opaque = "opaqueCredentialPayloadZ1234567890"; for (const name of cases) { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `nemoclaw-dcode-exactctx-${name}-`)); @@ -604,6 +661,26 @@ describe("LangChain Deep Agents Code image credential boundary", () => { } }); + it("allows benign runtime names containing pass as a substring", () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-dcode-pass-near-miss-")); + const { wrapperPath, ranMarker } = makeWrapperFixture(tempDir); + const result = runWrapper(wrapperPath, ["-n", "hi"], { + BYPASS: "allowedValue123", + COMPASS: "opaqueNonSecretPayload123", + passengerCount: "opaqueNonSecretPayload123", + passed: "opaqueNonSecretPayload123", + passRate: "opaqueNonSecretPayload123", + passCount: "opaqueNonSecretPayload123", + passThrough: "opaqueNonSecretPayload123", + correlationMarker: "reply-correlation-marker-123", + tokenizer: "opaqueNonSecretPayload123", + publicKey: "opaqueVerificationMaterial123", + customKey: "opaqueNonSecretPayload123", + }); + + expect(result.status, result.stderr).toBe(0); + expect(fs.existsSync(ranMarker)).toBe(true); + }); it.each([ { label: "variable expansion", content: "MY_CRED=$OTHER_SECRET" }, { label: "command substitution", content: "MY_CRED=$(whoami)" }, @@ -658,20 +735,20 @@ describe("LangChain Deep Agents Code image credential boundary", () => { expect(fs.existsSync(ranMarker)).toBe(false); }); - it("rejects the canonical positive secret corpus before dcode starts (#6195)", () => { - for (const { label, value } of CANONICAL_SECRET_POSITIVE_VECTORS) { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `nemoclaw-dcode-parity-${label}-`)); - try { - const { wrapperPath, ranMarker } = makeWrapperFixture(tempDir); - const varName = `NEMOCLAW_PARITY_${label.toUpperCase()}`; - const result = runWrapper(wrapperPath, ["-n", "hi"], { [varName]: value }); - expect(result.status, `${label} via runtime env not rejected`).not.toBe(0); - expect(result.stderr).toContain(varName); - expect(result.stderr).not.toContain(value); - expect(fs.existsSync(ranMarker)).toBe(false); - } finally { - fs.rmSync(tempDir, { force: true, recursive: true }); - } + it.each( + CANONICAL_SECRET_POSITIVE_VECTORS, + )("rejects canonical $label secrets before dcode starts (#6195)", ({ label, value }) => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `nemoclaw-dcode-parity-${label}-`)); + try { + const { wrapperPath, ranMarker } = makeWrapperFixture(tempDir); + const varName = `NEMOCLAW_PARITY_${label.toUpperCase()}`; + const result = runWrapper(wrapperPath, ["-n", "hi"], { [varName]: value }); + expect(result.status, `${label} via runtime env not rejected`).not.toBe(0); + expect(result.stderr).toContain(varName); + expect(result.stderr).not.toContain(value); + expect(fs.existsSync(ranMarker)).toBe(false); + } finally { + fs.rmSync(tempDir, { force: true, recursive: true }); } }); }); diff --git a/test/langchain-deepagents-code-image.test.ts b/test/langchain-deepagents-code-image.test.ts index 79bf33de8f9..f1a8134b1b2 100644 --- a/test/langchain-deepagents-code-image.test.ts +++ b/test/langchain-deepagents-code-image.test.ts @@ -250,7 +250,6 @@ describe("LangChain Deep Agents Code image contracts", () => { "managed-dcode-runtime.py", "nemoclaw_observability.py", "patch-managed-deepagents-code.py", - "patch-nemotron-ultra-profile.py", "validate-nemotron-ultra-profile.py", "DEEPAGENTS_CODE_LANGSMITH_TRACING=false", "LANGSMITH_TRACING=false", @@ -258,9 +257,21 @@ describe("LangChain Deep Agents Code image contracts", () => { "DEEPAGENTS_CODE_RIPGREP_INSTALLER=system", "install -m 0755 /usr/local/lib/nemoclaw/dcode-launcher.sh /usr/local/bin/dcode.real", "install -m 0755 /usr/local/lib/nemoclaw/dcode-launcher.sh /usr/local/bin/deepagents-code", + "/opt/venv/bin/pip3 install --no-index --no-cache-dir --no-deps --no-build-isolation /opt/nemoclaw-deepagents-profile-plugin", + "find /opt/nemoclaw-deepagents-profile-plugin -type f -print | LC_ALL=C sort", + "/opt/venv/bin/pip3 check", + "/opt/venv/bin/python3 -I /opt/nemoclaw-deepagents-code/validate-nemotron-ultra-profile.py", ]) { expect(dockerfile).toContain(s); } + expect( + dockerfile + .split("\n") + .filter((line) => line.startsWith("COPY agents/langchain-deepagents-code/profile-plugin")), + ).toEqual([ + "COPY agents/langchain-deepagents-code/profile-plugin/pyproject.toml /opt/nemoclaw-deepagents-profile-plugin/", + "COPY agents/langchain-deepagents-code/profile-plugin/src/nemoclaw_deepagents_profile/__init__.py /opt/nemoclaw-deepagents-profile-plugin/src/nemoclaw_deepagents_profile/", + ]); expect(dockerfile).toContain( "rm -f /usr/local/bin/dcode /usr/local/bin/deepagents-code /opt/venv/bin/dcode /opt/venv/bin/deepagents-code", ); @@ -273,15 +284,10 @@ describe("LangChain Deep Agents Code image contracts", () => { expect(dockerfile).toContain( "rm -f /opt/nemoclaw-deepagents-code/validate-progressive-tool-disclosure.py", ); - expect(dockerfile).toContain( - "python3 /opt/nemoclaw-deepagents-code/patch-nemotron-ultra-profile.py", - ); - expect(dockerfile).toContain( - "python3 /opt/nemoclaw-deepagents-code/validate-nemotron-ultra-profile.py", - ); expect(dockerfile).toContain( "rm -f /opt/nemoclaw-deepagents-code/validate-nemotron-ultra-profile.py", ); + expect(dockerfile).not.toContain("patch-nemotron-ultra-profile.py"); expect(dockerfile).not.toContain("nemotron-ultra-harness-profile.py"); expect(dockerfile).not.toContain("LICENSE.langchain-deepagents"); expect(dockerfile).not.toContain("langchain-deepagents-MIT.txt"); @@ -748,6 +754,9 @@ describe("LangChain Deep Agents Code image contracts", () => { "uv tool run --python 3.13 pip-audit -r agents/langchain-deepagents-code/requirements.lock --progress-spinner off --disable-pip", ); expect(review).toContain("No known vulnerabilities found"); + expect(review).toContain("75ff7e7a5142cad4305126ccb1b8fc756306e82d4c559ddbc624012fb54ebfc4"); + expect(review).toContain("7ba7b77bd6f889cc861eddbe3e38fc1f4433a85b7bc2a9b516e19a19a37a7686"); + expect(review).toContain("Adapter dependency audit result: `No known vulnerabilities found`"); expect(review).toContain("Deep Agents Code `0.1.34` pins `deepagents==0.7.0a6`"); expect(review).toContain("NemoClaw no longer vendors or overlays that source"); }); diff --git a/test/langchain-deepagents-code-nemotron-profile-patch.test.ts b/test/langchain-deepagents-code-nemotron-profile-patch.test.ts deleted file mode 100644 index 009053c423e..00000000000 --- a/test/langchain-deepagents-code-nemotron-profile-patch.test.ts +++ /dev/null @@ -1,423 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { spawnSync } from "node:child_process"; -import { createHash } from "node:crypto"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; - -const repoRoot = path.resolve(import.meta.dirname, ".."); -const agentDir = path.join(repoRoot, "agents", "langchain-deepagents-code"); -const patcherPath = path.join(agentDir, "patch-nemotron-ultra-profile.py"); -const validatorPath = path.join(agentDir, "validate-nemotron-ultra-profile.py"); - -const EXPECTED_DCODE_VERSION = "0.1.34"; -const EXPECTED_DEEPAGENTS_VERSION = "0.7.0a6"; -const NATIVE_PROFILE_SHA256 = "c8e8dd2b0182334b54be4f46ff0c7b45fbb95dc13bd9a92c249eb47a14fa13d7"; -const PINNED_BUILTIN_SHA256 = "005a91e7fc4ca6b21220673dd9d02d6686bf63e1e4f1102d124b01f96886efcf"; -const PATCHED_BUILTIN_SHA256 = "9d9e817143b330fd45345fcfa8276ea6fe5d6bc5a396f0438b0899a450e4744b"; -const CANONICAL_MODEL_SPEC = "nvidia:nvidia/nemotron-3-ultra-550b-a55b"; -const MANAGED_MODEL_ALIASES = [ - "openai:nvidia/nemotron-3-ultra-550b-a55b", - "openai:nvidia/nvidia/nemotron-3-ultra", -] as const; - -// This compact fixture preserves the exact 0.7.0a6 alias-patch anchors. The -// production constants above remain tied to the official wheel; private test -// patcher copies use fixture digests so drift cases stay focused and fast. -const BUILTIN_SOURCE = `"""Focused Deep Agents 0.7.0a6 bootstrap fixture.""" - -from deepagents.profiles.harness import ( - _anthropic_haiku_4_5, - _anthropic_opus_4_7, - _anthropic_sonnet_4_6, - _nvidia_nemotron_3_ultra, - _openai_codex, -) -from deepagents.profiles.harness.harness_profiles import _HARNESS_PROFILES -from deepagents.profiles.provider import _nvidia, _openai, _openrouter - - -def _invoke_profile_plugins(group: str) -> None: - del group - - -def _ensure_builtin_profiles_loaded() -> None: - try: - _nvidia.register() - _openai.register() - _openrouter.register() - _anthropic_opus_4_7.register() - _anthropic_sonnet_4_6.register() - _anthropic_haiku_4_5.register() - _nvidia_nemotron_3_ultra.register() - _openai_codex.register() - _invoke_profile_plugins("deepagents.provider_profiles") - _invoke_profile_plugins("deepagents.harness_profiles") - frozenset(_HARNESS_PROFILES) - except Exception: - raise -`; - -const NATIVE_PROFILE_SOURCE = `"""Focused native Nemotron profile fixture.""" - -from deepagents.profiles.harness.harness_profiles import _register_harness_profile_impl - - -def register() -> None: - _register_harness_profile_impl( - "${CANONICAL_MODEL_SPEC}", object() - ) -`; - -const REGISTRY_IMPORT_ANCHOR = - "from deepagents.profiles.harness.harness_profiles import _HARNESS_PROFILES\n"; -const REGISTRY_IMPORT_PATCH = `from deepagents.profiles.harness.harness_profiles import ( - _HARNESS_PROFILES, - _register_harness_profile_impl, -) -`; -const REGISTER_ANCHOR = " _nvidia_nemotron_3_ultra.register()\n"; -const REGISTER_PATCH = ` _nvidia_nemotron_3_ultra.register() - # NemoClaw managed OpenAI-compatible Nemotron 3 Ultra aliases. - _nemotron_ultra_profile = _HARNESS_PROFILES[ - "${CANONICAL_MODEL_SPEC}" - ] - _register_harness_profile_impl( - "${MANAGED_MODEL_ALIASES[0]}", _nemotron_ultra_profile - ) - _register_harness_profile_impl( - "${MANAGED_MODEL_ALIASES[1]}", _nemotron_ultra_profile - ) -`; - -const tempRoots: string[] = []; - -type PatchFixture = { - root: string; - builtinPath: string; - nativeProfilePath: string; -}; - -function sha256(value: string | Buffer): string { - return createHash("sha256").update(value).digest("hex"); -} - -function writeFixtureFile(root: string, relativePath: string, content: string): string { - const target = path.join(root, relativePath); - fs.mkdirSync(path.dirname(target), { recursive: true }); - fs.writeFileSync(target, content, "utf8"); - return target; -} - -function countOccurrences(source: string, needle: string): number { - return source.split(needle).length - 1; -} - -function patchedBuiltinFixture(source: string): string { - return source - .replace(REGISTRY_IMPORT_ANCHOR, REGISTRY_IMPORT_PATCH) - .replace(REGISTER_ANCHOR, REGISTER_PATCH); -} - -function makePatchFixture( - options: { - dcode?: string; - deepagents?: string; - builtinSource?: string; - nativeProfileSource?: string; - } = {}, -): PatchFixture { - const dcodeVersion = options.dcode ?? EXPECTED_DCODE_VERSION; - const deepagentsVersion = options.deepagents ?? EXPECTED_DEEPAGENTS_VERSION; - const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-nemotron-alias-")); - tempRoots.push(root); - - writeFixtureFile(root, "deepagents_code/__init__.py", '"""DCode fixture."""\n'); - writeFixtureFile(root, "deepagents/__init__.py", '"""Deep Agents fixture."""\n'); - writeFixtureFile(root, "deepagents/profiles/__init__.py", '"""Profiles fixture."""\n'); - writeFixtureFile(root, "deepagents/profiles/harness/__init__.py", '"""Harness fixture."""\n'); - writeFixtureFile( - root, - "deepagents/profiles/harness/harness_profiles.py", - `_HARNESS_PROFILES = {} - -def _register_harness_profile_impl(key, profile): - _HARNESS_PROFILES[key] = profile -`, - ); - writeFixtureFile(root, "deepagents/profiles/provider/__init__.py", '"""Provider fixture."""\n'); - const builtinPath = writeFixtureFile( - root, - "deepagents/profiles/_builtin_profiles.py", - options.builtinSource ?? BUILTIN_SOURCE, - ); - const nativeProfilePath = writeFixtureFile( - root, - "deepagents/profiles/harness/_nvidia_nemotron_3_ultra.py", - options.nativeProfileSource ?? NATIVE_PROFILE_SOURCE, - ); - writeFixtureFile( - root, - `deepagents_code-${dcodeVersion}.dist-info/METADATA`, - `Metadata-Version: 2.1\nName: deepagents-code\nVersion: ${dcodeVersion}\n`, - ); - writeFixtureFile( - root, - `deepagents-${deepagentsVersion}.dist-info/METADATA`, - `Metadata-Version: 2.1\nName: deepagents\nVersion: ${deepagentsVersion}\n`, - ); - - return { root, builtinPath, nativeProfilePath }; -} - -function prepareFixturePatcher(expectedBootstrap = BUILTIN_SOURCE): string { - const scriptRoot = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-nemotron-patcher-")); - tempRoots.push(scriptRoot); - const source = fs.readFileSync(patcherPath, "utf8"); - const testSource = source - .replaceAll(NATIVE_PROFILE_SHA256, sha256(NATIVE_PROFILE_SOURCE)) - .replaceAll(PINNED_BUILTIN_SHA256, sha256(expectedBootstrap)) - .replace( - /EXPECTED_PATCHED_BOOTSTRAP_SHA256\s*=\s*(?:\(\s*)?"[^"]+"(?:\s*\))?/, - `EXPECTED_PATCHED_BOOTSTRAP_SHA256 = "${sha256(patchedBuiltinFixture(expectedBootstrap))}"`, - ); - const testPatcher = path.join(scriptRoot, path.basename(patcherPath)); - fs.writeFileSync(testPatcher, testSource, "utf8"); - return testPatcher; -} - -function runPatcher(fixture: PatchFixture, script = prepareFixturePatcher()) { - return spawnSync("python3", [script], { - encoding: "utf8", - env: { PATH: process.env.PATH, PYTHONPATH: fixture.root }, - }); -} - -function runBootstrapProbe(fixture: PatchFixture) { - const script = `import importlib -import json -import sys - -sys.path.insert(0, ${JSON.stringify(fixture.root)}) -harness = importlib.import_module("deepagents.profiles.harness") -registry = importlib.import_module("deepagents.profiles.harness.harness_profiles") -provider = importlib.import_module("deepagents.profiles.provider") -events = [] -canonical_profile = object() - -class RegistrationModule: - def __init__(self, name): - self.name = name - - def register(self): - events.append(self.name) - if self.name == "nemotron": - registry._HARNESS_PROFILES[${JSON.stringify(CANONICAL_MODEL_SPEC)}] = canonical_profile - -for name in ( - "_anthropic_haiku_4_5", - "_anthropic_opus_4_7", - "_anthropic_sonnet_4_6", - "_openai_codex", -): - setattr(harness, name, RegistrationModule(name)) -harness._nvidia_nemotron_3_ultra = RegistrationModule("nemotron") -provider._nvidia = RegistrationModule("nvidia") -provider._openai = RegistrationModule("openai") -provider._openrouter = RegistrationModule("openrouter") - -bootstrap = importlib.import_module("deepagents.profiles._builtin_profiles") -bootstrap._ensure_builtin_profiles_loaded() -print(json.dumps({ - "events": events, - "aliases_share_profile": all( - registry._HARNESS_PROFILES[key] is canonical_profile - for key in ${JSON.stringify(MANAGED_MODEL_ALIASES)} - ), -})) -`; - return spawnSync("python3", ["-c", script], { - encoding: "utf8", - env: { PATH: process.env.PATH }, - }); -} - -function assertFixtureUnchanged( - fixture: PatchFixture, - expectedBootstrap: string, - expectedProfile = NATIVE_PROFILE_SOURCE, -): void { - expect(fs.readFileSync(fixture.builtinPath, "utf8")).toBe(expectedBootstrap); - expect(fs.readFileSync(fixture.nativeProfilePath, "utf8")).toBe(expectedProfile); -} - -afterEach(() => { - for (const root of tempRoots.splice(0)) { - fs.rmSync(root, { recursive: true, force: true }); - } -}); - -describe("LangChain Deep Agents Code Nemotron Ultra managed aliases", () => { - it("pins the released package versions and official wheel source digests", () => { - const patcher = fs.readFileSync(patcherPath, "utf8"); - - for (const expected of [ - EXPECTED_DCODE_VERSION, - EXPECTED_DEEPAGENTS_VERSION, - NATIVE_PROFILE_SHA256, - PINNED_BUILTIN_SHA256, - PATCHED_BUILTIN_SHA256, - ]) { - expect(patcher).toContain(expected); - } - expect(patcher).toContain("native Nemotron profile source"); - expect(patcher).not.toContain("nemotron-ultra-harness-profile.py"); - }); - - it("validates released-wheel graphs and parser/native managed dispatch parity", () => { - const validator = fs.readFileSync(validatorPath, "utf8"); - - for (const expected of [ - '"deepagents-code": "0.1.34"', - '"deepagents": "0.7.0a6"', - ...MANAGED_MODEL_ALIASES.map((alias) => `"${alias.replace(/^openai:/, "")}"`), - "create_deep_agent(model=managed_models[0])", - "validate_parser_tool_visibility()", - "validate_parser_dispatch_parity()", - "create_cli_agent(", - "shell_allow_list", - "graph.invoke(", - "DISPATCH_COMMAND", - '"NemotronProgressBudgetMiddleware"', - '"FinalAnswerGuardMiddleware"', - ]) { - expect(validator).toContain(expected); - } - expect(validator).toContain("def require(condition: bool, message: str)"); - expect(validator).not.toMatch(/^\s*assert\b/m); - }); - - it("registers both aliases against the native profile atomically and idempotently", () => { - const fixture = makePatchFixture(); - const script = prepareFixturePatcher(); - const originalProfile = fs.readFileSync(fixture.nativeProfilePath, "utf8"); - - const first = runPatcher(fixture, script); - expect(first.status, first.stderr).toBe(0); - const patchedBootstrap = fs.readFileSync(fixture.builtinPath, "utf8"); - expect(fs.readFileSync(fixture.nativeProfilePath, "utf8")).toBe(originalProfile); - expect(patchedBootstrap).toContain(" _register_harness_profile_impl,\n"); - expect(countOccurrences(patchedBootstrap, REGISTER_ANCHOR)).toBe(1); - for (const alias of MANAGED_MODEL_ALIASES) { - expect(countOccurrences(patchedBootstrap, alias)).toBe(1); - } - - const probe = runBootstrapProbe(fixture); - expect(probe.status, probe.stderr).toBe(0); - const wiring = JSON.parse(probe.stdout) as { - events: string[]; - aliases_share_profile: boolean; - }; - expect(wiring.aliases_share_profile).toBe(true); - expect(wiring.events.indexOf("nemotron")).toBeLessThan(wiring.events.indexOf("_openai_codex")); - - const second = runPatcher(fixture, script); - expect(second.status, second.stderr).toBe(0); - expect(second.stdout).toContain("managed-alias bridge is already applied"); - expect(fs.readFileSync(fixture.builtinPath, "utf8")).toBe(patchedBootstrap); - expect(fs.readFileSync(fixture.nativeProfilePath, "utf8")).toBe(originalProfile); - expect(fs.statSync(fixture.builtinPath).mode & 0o777).toBe(0o644); - }); - - it.each([ - ["Deep Agents Code", { dcode: "0.1.35" }, "deepagents-code==0.1.34"], - ["Deep Agents", { deepagents: "0.7.0a7" }, "deepagents==0.7.0a6"], - ] as const)("fails closed on %s version drift", (_label, versions, message) => { - const fixture = makePatchFixture(versions); - const result = runPatcher(fixture); - - expect(result.status).not.toBe(0); - expect(result.stderr).toContain(message); - expect(result.stderr).toContain( - "dependency drift requires reviewing whether upstream now recognizes both managed aliases", - ); - assertFixtureUnchanged(fixture, BUILTIN_SOURCE); - }); - - it.each([ - ["registry import", REGISTRY_IMPORT_ANCHOR], - ["native registration", REGISTER_ANCHOR], - ] as const)("rejects a missing or duplicated %s anchor", (_label, anchor) => { - for (const mode of ["missing", "duplicate"] as const) { - const source = - mode === "missing" - ? BUILTIN_SOURCE.replace(anchor, "") - : BUILTIN_SOURCE.replace(anchor, anchor + anchor); - const fixture = makePatchFixture({ builtinSource: source }); - const result = runPatcher(fixture, prepareFixturePatcher(source)); - - expect(result.status).not.toBe(0); - expect(result.stderr).toMatch(/exactly one .* anchor/i); - assertFixtureUnchanged(fixture, source); - } - }); - - it.each([ - ["missing", (profilePath: string) => fs.rmSync(profilePath)], - [ - "linked", - (profilePath: string) => { - fs.rmSync(profilePath); - fs.symlinkSync("/dev/null", profilePath); - }, - ], - ["drifted", (profilePath: string) => fs.appendFileSync(profilePath, "# drift\n", "utf8")], - ] as const)("rejects %s native profile source", (_label, mutateProfile) => { - const fixture = makePatchFixture(); - mutateProfile(fixture.nativeProfilePath); - const originalBootstrap = fs.readFileSync(fixture.builtinPath, "utf8"); - const result = runPatcher(fixture); - - expect(result.status).not.toBe(0); - expect(result.stderr).toMatch(/native Nemotron profile|trusted regular file/i); - expect(fs.readFileSync(fixture.builtinPath, "utf8")).toBe(originalBootstrap); - }); - - it("rejects drifted and partial bootstrap states without touching native source", () => { - const driftedSource = `${BUILTIN_SOURCE}\n# bootstrap drift\n`; - const drifted = makePatchFixture({ builtinSource: driftedSource }); - const driftResult = runPatcher(drifted); - expect(driftResult.status).not.toBe(0); - expect(driftResult.stderr).toMatch(/partial|conflicting|drifted/i); - assertFixtureUnchanged(drifted, driftedSource); - - const partial = makePatchFixture(); - const script = prepareFixturePatcher(); - const first = runPatcher(partial, script); - expect(first.status, first.stderr).toBe(0); - const partialSource = `${fs.readFileSync(partial.builtinPath, "utf8")}# partial drift\n`; - fs.writeFileSync(partial.builtinPath, partialSource, "utf8"); - const partialResult = runPatcher(partial, script); - expect(partialResult.status).not.toBe(0); - expect(partialResult.stderr).toMatch(/partial|conflicting|drifted/i); - assertFixtureUnchanged(partial, partialSource); - }); - - it("leaves the bootstrap unchanged when the atomic temporary path is occupied", () => { - const fixture = makePatchFixture(); - const temporary = path.join( - path.dirname(fixture.builtinPath), - "._builtin_profiles.py.nemoclaw-tmp", - ); - fs.writeFileSync(temporary, "occupied\n", "utf8"); - - const result = runPatcher(fixture); - expect(result.status).not.toBe(0); - expect(result.stderr).toContain("temporary patch path already exists"); - assertFixtureUnchanged(fixture, BUILTIN_SOURCE); - expect(fs.readFileSync(temporary, "utf8")).toBe("occupied\n"); - }); -}); diff --git a/test/langchain-deepagents-code-nemotron-profile-plugin.test.ts b/test/langchain-deepagents-code-nemotron-profile-plugin.test.ts new file mode 100644 index 00000000000..25a5cf36ccf --- /dev/null +++ b/test/langchain-deepagents-code-nemotron-profile-plugin.test.ts @@ -0,0 +1,593 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import assert from "node:assert/strict"; +import { execFileSync, spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; + +const repoRoot = path.resolve(import.meta.dirname, ".."); +const agentDir = path.join(repoRoot, "agents", "langchain-deepagents-code"); +const pluginProjectDir = path.join(agentDir, "profile-plugin"); +const pluginSourcePath = path.join( + pluginProjectDir, + "src", + "nemoclaw_deepagents_profile", + "__init__.py", +); +const pluginProjectPath = path.join(pluginProjectDir, "pyproject.toml"); +const validatorPath = path.join(agentDir, "validate-nemotron-ultra-profile.py"); +const pythonBin = execFileSync("python3", ["-c", "import sys; print(sys.executable)"], { + encoding: "utf8", +}).trim(); + +const EXPECTED_DCODE_VERSION = "0.1.34"; +const EXPECTED_DEEPAGENTS_VERSION = "0.7.0a6"; +const NATIVE_PROFILE_SHA256 = "c8e8dd2b0182334b54be4f46ff0c7b45fbb95dc13bd9a92c249eb47a14fa13d7"; +const UNMODIFIED_BOOTSTRAP_SHA256 = + "005a91e7fc4ca6b21220673dd9d02d6686bf63e1e4f1102d124b01f96886efcf"; +const CANONICAL_MODEL_SPEC = "nvidia:nvidia/nemotron-3-ultra-550b-a55b"; +const MANAGED_MODEL_ALIASES = [ + "openai:nvidia/nemotron-3-ultra-550b-a55b", + "openai:nvidia/nvidia/nemotron-3-ultra", +] as const; + +const NATIVE_PROFILE_SOURCE = `"""Focused native Nemotron profile fixture.""" + +NATIVE_PROFILE_MARKER = "reviewed" +`; + +const BOOTSTRAP_SOURCE = `"""Focused unmodified Deep Agents bootstrap fixture.""" + +BOOTSTRAP_MARKER = "unmodified" +`; + +const tempRoots: string[] = []; + +type PluginFixture = { + root: string; + nativeProfilePath: string; + bootstrapPath: string; +}; + +type ProbeResult = { + aliases: boolean[]; + canonicalPresent: boolean; + error: string | null; + registryKeys: string[]; +}; + +function sha256(value: string | Buffer): string { + return createHash("sha256").update(value).digest("hex"); +} + +function replaceHashDefinitions( + source: string, + replacements: readonly (readonly [name: string, currentHash: string, fixtureHash: string])[], +): string { + // This test-only parser deliberately fails on source-shape drift or duplicate + // definitions; replace it with an AST transform if the two-constant scope grows. + return replacements.reduce((current, [name, currentHash, fixtureHash]) => { + const definition = new RegExp(`(${name}\\s*=\\s*\\(\\s*)(["'])${currentHash}\\2(\\s*\\))`, "g"); + assert.equal(current.match(definition)?.length, 1, `expected exactly one ${name} definition`); + return current.replace(definition, `$1$2${fixtureHash}$2$3`); + }, source); +} + +function writeFixtureFile(root: string, relativePath: string, content: string): string { + const target = path.join(root, relativePath); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, content, "utf8"); + return target; +} + +function makePluginFixture( + options: { + dcode?: string; + deepagents?: string; + nativeProfileSource?: string; + bootstrapSource?: string; + } = {}, +): PluginFixture { + const dcodeVersion = options.dcode ?? EXPECTED_DCODE_VERSION; + const deepagentsVersion = options.deepagents ?? EXPECTED_DEEPAGENTS_VERSION; + const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-profile-plugin-fixture-")); + tempRoots.push(root); + + writeFixtureFile(root, "deepagents_code/__init__.py", '"""DCode fixture."""\n'); + writeFixtureFile(root, "deepagents/__init__.py", '"""Deep Agents fixture."""\n'); + writeFixtureFile( + root, + "deepagents/profiles/__init__.py", + "from deepagents.profiles.harness.harness_profiles import register_harness_profile\n", + ); + writeFixtureFile(root, "deepagents/profiles/harness/__init__.py", '"""Harness fixture."""\n'); + writeFixtureFile( + root, + "deepagents/profiles/harness/harness_profiles.py", + `import os + +_HARNESS_PROFILES = {} + + +def register_harness_profile(key, profile): + _HARNESS_PROFILES[key] = profile + if os.environ.get("NEMOCLAW_TEST_FAIL_KEY") == key: + raise RuntimeError(f"injected registration failure for {key}") +`, + ); + const nativeProfilePath = writeFixtureFile( + root, + "deepagents/profiles/harness/_nvidia_nemotron_3_ultra.py", + options.nativeProfileSource ?? NATIVE_PROFILE_SOURCE, + ); + const bootstrapPath = writeFixtureFile( + root, + "deepagents/profiles/_builtin_profiles.py", + options.bootstrapSource ?? BOOTSTRAP_SOURCE, + ); + writeFixtureFile( + root, + `deepagents_code-${dcodeVersion}.dist-info/METADATA`, + `Metadata-Version: 2.1\nName: deepagents-code\nVersion: ${dcodeVersion}\n`, + ); + writeFixtureFile( + root, + `deepagents-${deepagentsVersion}.dist-info/METADATA`, + `Metadata-Version: 2.1\nName: deepagents\nVersion: ${deepagentsVersion}\n`, + ); + + return { root, nativeProfilePath, bootstrapPath }; +} + +function prepareFixturePlugin( + nativeSource = NATIVE_PROFILE_SOURCE, + bootstrapSource = BOOTSTRAP_SOURCE, +): string { + const pluginRoot = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-profile-plugin-source-")); + tempRoots.push(pluginRoot); + const source = replaceHashDefinitions(fs.readFileSync(pluginSourcePath, "utf8"), [ + ["EXPECTED_NATIVE_PROFILE_SHA256", NATIVE_PROFILE_SHA256, sha256(nativeSource)], + ["EXPECTED_BOOTSTRAP_SHA256", UNMODIFIED_BOOTSTRAP_SHA256, sha256(bootstrapSource)], + ]); + return writeFixtureFile(pluginRoot, "nemoclaw_deepagents_profile/__init__.py", source); +} + +function makeValidatorDependencyStubRoot(): string { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-profile-validator-stubs-")); + tempRoots.push(root); + const stubs = { + "deepagents/__init__.py": "def create_deep_agent(*args, **kwargs): return object()\n", + "deepagents/backends/__init__.py": + "class LocalShellBackend:\n def __init__(self, *args, **kwargs): pass\n", + "deepagents/backends/protocol.py": "class ExecuteResponse: pass\n", + "deepagents/profiles/__init__.py": "", + "deepagents/profiles/harness/__init__.py": "", + "deepagents/profiles/harness/_nvidia_nemotron_3_ultra.py": + "class NemotronTextToolCallParser: pass\n", + "deepagents/profiles/harness/harness_profiles.py": + "class HarnessProfile: pass\ndef _harness_profile_for_model(*args, **kwargs): return HarnessProfile()\n", + "deepagents_code/__init__.py": "", + "deepagents_code/agent.py": "def create_cli_agent(*args, **kwargs): return None\n", + "langchain/agents/middleware/types.py": "class AgentMiddleware: pass\n", + "langchain_core/language_models/fake_chat_models.py": "class FakeMessagesListChatModel: pass\n", + "langchain_core/messages.py": + "class AIMessage: pass\nclass HumanMessage: pass\nclass ToolMessage: pass\n", + "langchain_openai/__init__.py": "class ChatOpenAI: pass\n", + }; + for (const [relativePath, content] of Object.entries(stubs)) { + writeFixtureFile(root, relativePath, content); + } + return root; +} + +function makeValidatorStubRoot( + entryPointName: string, + entryPointGroup = "deepagents.harness_profiles", + licenseExpression = "Apache-2.0", +): string { + const root = makeValidatorDependencyStubRoot(); + writeFixtureFile( + root, + "nemoclaw_deepagents_profile/__init__.py", + fs.readFileSync(pluginSourcePath, "utf8"), + ); + writeFixtureFile( + root, + "nemoclaw_deepagents_profile-0.1.0.dist-info/METADATA", + `Metadata-Version: 2.4\nName: nemoclaw-deepagents-profile\nVersion: 0.1.0\nLicense-Expression: ${licenseExpression}\n`, + ); + writeFixtureFile( + root, + "nemoclaw_deepagents_profile-0.1.0.dist-info/entry_points.txt", + `[${entryPointGroup}]\n${entryPointName} = nemoclaw_deepagents_profile:register\n`, + ); + return root; +} + +function buildAndInstallPluginWheel(version: string): string { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-profile-wheel-")); + tempRoots.push(root); + const projectRoot = path.join(root, "project"); + const wheelDir = path.join(root, "wheel"); + const installRoot = path.join(root, "installed"); + fs.cpSync(pluginProjectDir, projectRoot, { recursive: true }); + fs.mkdirSync(wheelDir); + const projectPath = path.join(projectRoot, "pyproject.toml"); + const project = fs.readFileSync(projectPath, "utf8"); + // invalidState: the runner's system setuptools predates PEP 639 strings. + // sourceBoundary: this portable wheel exists only to test wrong-version + // entry-point binding; production builds the unchanged project metadata. + // whyNotSourceFix: the PEP 639 string is the standards-current source form. + // regressionTest: the production validator requires License-Expression. + // removalCondition: remove this conversion when runner setuptools supports it. + const versionedProject = project + .replace('version = "0.1.0"', `version = "${version}"`) + .replace('license = "Apache-2.0"', 'license = { text = "Apache-2.0" }'); + assert.ok( + versionedProject.includes(`version = "${version}"`), + "fixture version was not replaced", + ); + assert.ok( + versionedProject.includes('license = { text = "Apache-2.0" }'), + "fixture license was not replaced", + ); + fs.writeFileSync(projectPath, versionedProject, "utf8"); + const pipEnv = { + ...process.env, + PIP_DISABLE_PIP_VERSION_CHECK: "1", + PIP_NO_INPUT: "1", + }; + execFileSync( + pythonBin, + [ + "-m", + "pip", + "wheel", + "--no-cache-dir", + "--no-deps", + "--no-index", + "--no-build-isolation", + "--wheel-dir", + wheelDir, + projectRoot, + ], + { env: pipEnv, stdio: "pipe" }, + ); + const wheelPath = path.join(wheelDir, `nemoclaw_deepagents_profile-${version}-py3-none-any.whl`); + execFileSync( + pythonBin, + [ + "-m", + "pip", + "install", + "--no-cache-dir", + "--no-deps", + "--no-index", + "--target", + installRoot, + wheelPath, + ], + { env: pipEnv, stdio: "pipe" }, + ); + return installRoot; +} + +function runEntryPointValidationWithRoots(pythonRoots: string[]) { + const script = `import importlib.util + +spec = importlib.util.spec_from_file_location("nemoclaw_profile_validator", ${JSON.stringify(validatorPath)}) +validator = importlib.util.module_from_spec(spec) +spec.loader.exec_module(validator) +validator.validate_profile_entry_point() +`; + return spawnSync(pythonBin, ["-S", "-c", script], { + encoding: "utf8", + env: { + PATH: "/usr/bin:/bin", + PYTHONPATH: pythonRoots.join(":"), + }, + }); +} + +function runEntryPointValidation( + entryPointName: string, + entryPointGroup = "deepagents.harness_profiles", +) { + return runEntryPointValidationWithRoots([makeValidatorStubRoot(entryPointName, entryPointGroup)]); +} + +function runPlugin( + fixture: PluginFixture, + options: { + additionalPythonRoots?: string[]; + aliasState?: "complete" | "conflict" | "partial"; + failKey?: string; + registerCalls?: number; + withCanonical?: boolean; + } = {}, +) { + const pluginPath = prepareFixturePlugin(); + const pluginRoot = path.dirname(path.dirname(pluginPath)); + const script = `import json +from deepagents.profiles.harness.harness_profiles import _HARNESS_PROFILES + +canonical = object() +if ${(options.withCanonical ?? true) ? "True" : "False"}: + _HARNESS_PROFILES[${JSON.stringify(CANONICAL_MODEL_SPEC)}] = canonical + +state = ${JSON.stringify(options.aliasState ?? "")} +aliases = ${JSON.stringify(MANAGED_MODEL_ALIASES)} +if state == "complete": + for key in aliases: + _HARNESS_PROFILES[key] = canonical +elif state == "partial": + _HARNESS_PROFILES[aliases[0]] = canonical +elif state == "conflict": + for key in aliases: + _HARNESS_PROFILES[key] = object() + +from nemoclaw_deepagents_profile import register + +error = None +try: + for _ in range(${options.registerCalls ?? 1}): + register() +except Exception as exc: + error = str(exc) + +print(json.dumps({ + "aliases": [_HARNESS_PROFILES.get(key) is canonical for key in aliases], + "canonicalPresent": _HARNESS_PROFILES.get(${JSON.stringify(CANONICAL_MODEL_SPEC)}) is canonical, + "error": error, + "registryKeys": sorted(_HARNESS_PROFILES), +})) +raise SystemExit(1 if error else 0) +`; + const result = spawnSync(pythonBin, ["-c", script], { + encoding: "utf8", + env: { + PATH: "/usr/bin:/bin", + PYTHONPATH: [...(options.additionalPythonRoots ?? []), fixture.root, pluginRoot].join(":"), + ...(options.failKey ? { NEMOCLAW_TEST_FAIL_KEY: options.failKey } : {}), + }, + }); + return { + ...result, + probe: JSON.parse(result.stdout) as ProbeResult, + }; +} + +function expectOfficialSourcesUnchanged( + fixture: PluginFixture, + nativeSource = NATIVE_PROFILE_SOURCE, + bootstrapSource = BOOTSTRAP_SOURCE, +): void { + expect(fs.readFileSync(fixture.nativeProfilePath, "utf8")).toBe(nativeSource); + expect(fs.readFileSync(fixture.bootstrapPath, "utf8")).toBe(bootstrapSource); +} + +const replaceProfileSource = { + missing(sourcePath: string): void { + fs.rmSync(sourcePath); + }, + linked(sourcePath: string): void { + fs.rmSync(sourcePath); + fs.symlinkSync("/dev/null", sourcePath); + }, +}; + +afterEach(() => { + for (const root of tempRoots.splice(0)) { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +describe("LangChain Deep Agents Code managed Nemotron profile plugin (#6424)", () => { + it("rewrites one exact hash definition across reviewed formatting variants", () => { + const original = "a".repeat(64); + const replacement = "b".repeat(64); + const source = `EXPECTED_ONE = (\n "${original}"\n)\nEXPECTED_TWO=( '${original}' )\n`; + const result = replaceHashDefinitions(source, [ + ["EXPECTED_ONE", original, replacement], + ["EXPECTED_TWO", original, replacement], + ]); + + expect(result).not.toContain(original); + expect(result.match(new RegExp(replacement, "g"))).toHaveLength(2); + expect(() => + replaceHashDefinitions(`${source}EXPECTED_ONE = ("${original}")\n`, [ + ["EXPECTED_ONE", original, replacement], + ]), + ).toThrow(/expected exactly one EXPECTED_ONE definition/); + }); + + it("declares the supported Deep Agents harness-profile entry point", () => { + const project = fs.readFileSync(pluginProjectPath, "utf8"); + + expect(project).toContain('name = "nemoclaw-deepagents-profile"'); + expect(project).toContain('version = "0.1.0"'); + expect(project).toContain('requires = ["setuptools==82.0.1"]'); + expect(project).toContain('license = "Apache-2.0"'); + expect(project).toContain('[project.entry-points."deepagents.harness_profiles"]'); + expect(project).toContain('nemoclaw-managed-aliases = "nemoclaw_deepagents_profile:register"'); + expect(project).toContain('"deepagents-code==0.1.34"'); + expect(project).toContain('"deepagents==0.7.0a6"'); + }); + + it("accepts the exact plugin, then rejects source substitution", () => { + const root = makeValidatorStubRoot("nemoclaw-managed-aliases"); + expect(runEntryPointValidationWithRoots([root]).status).toBe(0); + fs.appendFileSync(path.join(root, "nemoclaw_deepagents_profile", "__init__.py"), "# drift\n"); + + const result = runEntryPointValidationWithRoots([root]); + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + "profile plugin source does not match the reviewed first-party package", + ); + }); + + it("rejects a malicious wrong harness-profile entry point", () => { + const result = runEntryPointValidation("wrong-managed-aliases"); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + "expected exactly one 'nemoclaw-managed-aliases' profile entry point", + ); + }); + + it("rejects unreviewed profile plugin license metadata", () => { + const root = makeValidatorStubRoot( + "nemoclaw-managed-aliases", + "deepagents.harness_profiles", + "MIT", + ); + const result = runEntryPointValidationWithRoots([root]); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + "profile plugin license metadata does not match the reviewed package", + ); + }); + + it("rejects a plugin distribution missing the harness-profile entry-point group", () => { + const result = runEntryPointValidation("nemoclaw-managed-aliases", "unrelated.entry_points"); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + "profile entry point group 'deepagents.harness_profiles' was not found", + ); + }); + + it("rejects an installed real plugin wheel with an unreviewed version", () => { + const dependencyRoot = makeValidatorDependencyStubRoot(); + // A real wheel exercises entry-point metadata and locate_file binding that + // a hand-written dist-info stub cannot prove. + const installedPluginRoot = buildAndInstallPluginWheel("0.1.1"); + const result = runEntryPointValidationWithRoots([dependencyRoot, installedPluginRoot]); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + "profile entry point comes from an unexpected distribution version", + ); + }); + + it("idempotently registers both aliases without changing wheel sources", () => { + const fixture = makePluginFixture(); + const result = runPlugin(fixture, { registerCalls: 2 }); + + expect(result.status, result.stderr).toBe(0); + expect(result.probe.aliases).toEqual([true, true]); + expect(result.probe.canonicalPresent).toBe(true); + expect(result.probe.registryKeys).toEqual( + [...MANAGED_MODEL_ALIASES, CANONICAL_MODEL_SPEC].sort(), + ); + expectOfficialSourcesUnchanged(fixture); + }); + + it.each([ + ["Deep Agents Code", { dcode: "0.1.35" }, "deepagents-code==0.1.34"], + ["Deep Agents", { deepagents: "0.7.0a7" }, "deepagents==0.7.0a6"], + ] as const)("fails closed on %s version drift", (_label, versions, message) => { + const fixture = makePluginFixture(versions); + const result = runPlugin(fixture); + + expect(result.status).not.toBe(0); + expect(result.probe.error).toContain(message); + expect(result.probe.aliases).toEqual([false, false]); + expectOfficialSourcesUnchanged(fixture); + }); + + it("rejects a prepended shadow Deep Agents package before reading official sources", () => { + const fixture = makePluginFixture(); + const shadowRoot = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-shadow-deepagents-")); + tempRoots.push(shadowRoot); + writeFixtureFile( + shadowRoot, + "deepagents/__init__.py", + "from pkgutil import extend_path\n__path__ = extend_path(__path__, __name__)\n", + ); + const result = runPlugin(fixture, { additionalPythonRoots: [shadowRoot] }); + + expect(result.status).not.toBe(0); + expect(result.probe.error).toContain( + "imported deepagents package does not match the reviewed distribution", + ); + expect(result.probe.aliases).toEqual([false, false]); + expectOfficialSourcesUnchanged(fixture); + }); + + it.each([ + ["native profile", "native"], + ["bootstrap", "bootstrap"], + ] as const)("rejects drifted %s source without changing either wheel file", (_label, target) => { + const drift = "# drift\n"; + const nativeSource = + target === "native" ? NATIVE_PROFILE_SOURCE + drift : NATIVE_PROFILE_SOURCE; + const bootstrapSource = target === "bootstrap" ? BOOTSTRAP_SOURCE + drift : BOOTSTRAP_SOURCE; + const fixture = makePluginFixture({ + nativeProfileSource: nativeSource, + bootstrapSource, + }); + const result = runPlugin(fixture); + + expect(result.status).not.toBe(0); + expect(result.probe.error).toMatch(/does not match the reviewed Deep Agents/i); + expect(result.probe.aliases).toEqual([false, false]); + expectOfficialSourcesUnchanged(fixture, nativeSource, bootstrapSource); + }); + + it.each([ + ["missing", "native profile", "nativeProfilePath"], + ["linked", "native profile", "nativeProfilePath"], + ["missing", "bootstrap", "bootstrapPath"], + ["linked", "bootstrap", "bootstrapPath"], + ] as const)("rejects a %s %s source file", (mode, _label, sourceKey) => { + const fixture = makePluginFixture(); + replaceProfileSource[mode](fixture[sourceKey]); + + const result = runPlugin(fixture); + + expect(result.status).not.toBe(0); + expect(result.probe.error).toMatch(/not a trusted regular file/i); + expect(result.probe.aliases).toEqual([false, false]); + }); + + it("rejects a missing canonical profile without creating aliases", () => { + const fixture = makePluginFixture(); + const result = runPlugin(fixture, { withCanonical: false }); + + expect(result.status).not.toBe(0); + expect(result.probe.error).toContain("canonical profile"); + expect(result.probe.aliases).toEqual([false, false]); + expect(result.probe.registryKeys).toEqual([]); + expectOfficialSourcesUnchanged(fixture); + }); + + it.each([ + "partial", + "conflict", + ] as const)("rejects %s managed alias state without further registry changes", (aliasState) => { + const fixture = makePluginFixture(); + const result = runPlugin(fixture, { aliasState }); + + expect(result.status).not.toBe(0); + expect(result.probe.error).toMatch(/partial|conflict/i); + expect(result.probe.registryKeys).toHaveLength(aliasState === "partial" ? 2 : 3); + expectOfficialSourcesUnchanged(fixture); + }); + + it("rolls back the first alias when the second registration fails", () => { + const fixture = makePluginFixture(); + const result = runPlugin(fixture, { failKey: MANAGED_MODEL_ALIASES[1] }); + + expect(result.status).not.toBe(0); + expect(result.probe.error).toContain("injected registration failure"); + expect(result.probe.aliases).toEqual([false, false]); + expect(result.probe.registryKeys).toEqual([CANONICAL_MODEL_SPEC]); + expectOfficialSourcesUnchanged(fixture); + }); +}); diff --git a/test/langchain-deepagents-code-observability.test.ts b/test/langchain-deepagents-code-observability.test.ts index 9bc1ec808e1..33641d7a6a0 100644 --- a/test/langchain-deepagents-code-observability.test.ts +++ b/test/langchain-deepagents-code-observability.test.ts @@ -79,11 +79,21 @@ describe("managed Deep Agents Code observability", () => { bearer: "", clientSecret: "", credential: "", + customPasswd: "", + DBPass: "", header: "", nested: { checkpoint_id: "", command: "allowed" }, opaque: { _omitted_type: "opaque" }, + pass: "", passwd: "", + passCount: 4, + passRate: 0.9, + passThrough: "allowed", privateKey: "", + correlationMarker: "reply-correlation-marker-123", + replyToken: "", + ReplyToken: "", + reply_token: "", token: "", }, oversized_capture: { diff --git a/test/langchain-deepagents-code-profile-build-gate.test.ts b/test/langchain-deepagents-code-profile-build-gate.test.ts new file mode 100644 index 00000000000..a1067c52538 --- /dev/null +++ b/test/langchain-deepagents-code-profile-build-gate.test.ts @@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { describe, expect, it } from "vitest"; + +const repoRoot = path.resolve(import.meta.dirname, ".."); +const checkPath = path.join(repoRoot, "scripts", "check-dcode-profile-import-gate.sh"); +const reviewedDockerfiles = [ + "agents/langchain-deepagents-code/Dockerfile.base", + "test/Dockerfile.dcode-profile-missing-dependencies", + "agents/langchain-deepagents-code/Dockerfile", +] as const; +const unreviewedArgCases = [ + ...reviewedDockerfiles.map((dockerfile) => ({ + declaration: "ARG UNREVIEWED_SECRET", + dockerfile, + label: "uppercase directive", + })), + ...reviewedDockerfiles.map((dockerfile) => ({ + declaration: "arg UNREVIEWED_SECRET", + dockerfile, + label: "lowercase directive", + })), + { + declaration: "ArG \\\n UNREVIEWED_SECRET", + dockerfile: reviewedDockerfiles[2], + label: "mixed-case continued directive", + }, +] as const; + +function runGateWithFakeDocker( + mode: "expected-failure-with-marker" | "early-failure" | "success", + mutateFixture: (fixtureRoot: string) => void = () => undefined, +) { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "dcode-profile-import-gate-")); + const fixtureRoot = path.join(tmp, "repo"); + const fixtureCheckPath = path.join(fixtureRoot, "scripts", path.basename(checkPath)); + const dockerPath = path.join(tmp, "docker"); + const callLog = path.join(tmp, "docker.log"); + fs.mkdirSync(path.dirname(fixtureCheckPath), { recursive: true }); + fs.copyFileSync(checkPath, fixtureCheckPath); + for (const dockerfile of reviewedDockerfiles) { + const fixturePath = path.join(fixtureRoot, dockerfile); + fs.mkdirSync(path.dirname(fixturePath), { recursive: true }); + fs.copyFileSync(path.join(repoRoot, dockerfile), fixturePath); + } + mutateFixture(fixtureRoot); + fs.writeFileSync( + dockerPath, + `#!/usr/bin/env bash +set -eu +printf '%s\\n' "$*" >> "\${FAKE_DOCKER_LOG:?}" +case " $* " in + *" --file agents/langchain-deepagents-code/Dockerfile "*) + case "\${FAKE_DOCKER_MODE:?}" in + expected-failure-with-marker) + printf '%s\\n' NEMOCLAW_DCODE_PROFILE_IMPORT_GATE "ModuleNotFoundError: No module named 'deepagents'" + exit 1 + ;; + early-failure) + printf '%s\\n' "production build failed before import gate" + exit 1 + ;; + success) exit 0 ;; + esac + ;; +esac +exit 0 +`, + "utf8", + ); + fs.chmodSync(dockerPath, 0o755); + try { + const result = spawnSync("bash", [fixtureCheckPath], { + cwd: fixtureRoot, + encoding: "utf8", + env: { + ...process.env, + FAKE_DOCKER_LOG: callLog, + FAKE_DOCKER_MODE: mode, + PATH: `${tmp}${path.delimiter}${process.env.PATH ?? "/usr/bin:/bin"}`, + }, + }); + return { ...result, calls: fs.existsSync(callLog) ? fs.readFileSync(callLog, "utf8") : "" }; + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } +} + +describe("LangChain Deep Agents Code profile build gate", () => { + it.each( + unreviewedArgCases, + )("rejects an unreviewed ARG with $label in $dockerfile", (testCase) => { + const result = runGateWithFakeDocker("expected-failure-with-marker", (fixtureRoot) => + fs.appendFileSync(path.join(fixtureRoot, testCase.dockerfile), `\n${testCase.declaration}\n`), + ); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain(`unreviewed ARG UNREVIEWED_SECRET in ${testCase.dockerfile}`); + expect(result.calls).not.toContain("--file"); + }); + + it("accepts only the expected production-build failure at the runtime marker", () => { + const result = runGateWithFakeDocker("expected-failure-with-marker"); + + expect(result.status, result.stderr).toBe(0); + const markerIndex = result.stdout.indexOf("NEMOCLAW_DCODE_PROFILE_IMPORT_GATE"); + expect(markerIndex).toBeGreaterThanOrEqual(0); + expect( + result.stdout.indexOf("ModuleNotFoundError: No module named 'deepagents'"), + ).toBeGreaterThan(markerIndex); + expect(result.stdout).toContain( + "DCode profile import gate rejected a base missing deepagents and deepagents-code", + ); + expect(result.calls).toContain("--file agents/langchain-deepagents-code/Dockerfile.base"); + expect(result.calls).toContain("--file test/Dockerfile.dcode-profile-missing-dependencies"); + expect(result.calls).toContain("--file agents/langchain-deepagents-code/Dockerfile"); + expect(result.calls).not.toContain(":latest"); + expect([...result.calls.matchAll(/--build-arg ([^ =]+)=/g)].map((match) => match[1])).toEqual([ + "BASE_IMAGE", + "BASE_IMAGE", + ]); + const script = fs.readFileSync(checkPath, "utf8"); + const argGuard = "plain-progress build refuses unreviewed ARG"; + expect(script).toContain(argGuard); + expect(script).toContain("docker build"); + expect(script.indexOf(argGuard)).toBeLessThan(script.indexOf("docker build")); + }); + + it("rejects a production build that unexpectedly succeeds", () => { + const result = runGateWithFakeDocker("success"); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + "DCode production image unexpectedly built without deepagents dependencies", + ); + }); + + it("rejects a failure before the runtime import marker", () => { + const result = runGateWithFakeDocker("early-failure"); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("DCode build failed before reaching the profile import gate"); + }); +}); diff --git a/test/langchain-deepagents-code-secret-pattern-parity.test.ts b/test/langchain-deepagents-code-secret-pattern-parity.test.ts index f38bc3200c5..7557275b6ef 100644 --- a/test/langchain-deepagents-code-secret-pattern-parity.test.ts +++ b/test/langchain-deepagents-code-secret-pattern-parity.test.ts @@ -75,7 +75,9 @@ describe("Deep Agents Code secret-pattern parity", () => { ], context: [ "(?<=Bearer\\s+)[A-Za-z0-9_.+/=-]{10,}::gi", - "(?<=(?:_KEY|API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)[=: ]['\"]?)[A-Za-z0-9_.+/=-]{10,}::gi", + "(?<=(?:^|[^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)[\"']?(?:[ \\t]{0,32}[=:][ \\t]{0,32}|[ \\t]{1,32})[\"']?)[^\\s'\"]{10,}::gi", + "(?<=(?:^|[^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))[\"']?(?:[ \\t]{0,32}[=:][ \\t]{0,32}|[ \\t]{1,32})[\"']?)[^\\s'\"]{10,}::g", + "(?<=(?:^|[^A-Za-z0-9])KEY[\"']?(?:[ \\t]{0,32}[=:][ \\t]{0,32}|[ \\t]{1,32})[\"']?)[^\\s'\"]{10,}::g", ], block: [ "-----BEGIN (?:[A-Z0-9]+ )?PRIVATE KEY-----[\\s\\S]*?-----END (?:[A-Z0-9]+ )?PRIVATE KEY-----::g", @@ -104,27 +106,117 @@ describe("Deep Agents Code secret-pattern parity", () => { } }); + it("bounds assignment separators and rejects credential-word substrings (#6452)", () => { + const assignmentPattern = CONTEXT_PATTERNS[1]; + for (const value of [ + "COMPASS=opaqueNonSecretPayload123", + "BYPASS=allowedValue123", + "TOPSECRET=opaqueNonSecretPayload123", + "SUBTOKEN=opaqueNonSecretPayload123", + "public-key=opaqueVerificationMaterial123", + "custom-key=opaqueNonSecretPayload123", + '{"key":"agent:main:main"}', + `TOKEN${" ".repeat(33)}opaqueCredentialPayloadZ1234567890`, + `TOKEN${" ".repeat(100_000)}opaqueCredentialPayloadZ1234567890`, + ]) { + expect(matches(assignmentPattern, value), value.slice(0, 80)).toBe(false); + } + expect( + matches(assignmentPattern, `TOKEN${" ".repeat(32)}opaqueCredentialPayloadZ1234567890`), + ).toBe(true); + + const camelPattern = CONTEXT_PATTERNS[2]; + for (const value of [ + "COMPASS=opaqueNonSecretPayload123", + "BYPASS=allowedValue123", + "passRate=opaqueNonSecretPayload123", + "passCount=opaqueNonSecretPayload123", + "passThrough=opaqueNonSecretPayload123", + "publicKey=opaqueVerificationMaterial123", + "customKey=opaqueNonSecretPayload123", + '{"correlationMarker":"reply-correlation-marker-123"}', + `${"a".repeat(129)}Secret=opaqueCredentialPayloadZ1234567890`, + ]) { + expect(matches(camelPattern, value), value.slice(0, 80)).toBe(false); + } + }); + it("detects every shared positive vector in the managed Python runtime (#6195)", () => { const probe = ` import importlib.util +import fcntl import json +import os import sys sys.dont_write_bytecode = True +for name, value in { + "F_SEAL_WRITE": 1, + "F_SEAL_GROW": 2, + "F_SEAL_SHRINK": 4, + "F_SEAL_SEAL": 8, +}.items(): + setattr(fcntl, name, getattr(fcntl, name, value)) spec = importlib.util.spec_from_file_location("_nemoclaw_managed_parity", sys.argv[1]) if spec is None or spec.loader is None: raise RuntimeError("managed runtime module could not be loaded") managed = importlib.util.module_from_spec(spec) spec.loader.exec_module(managed) values = json.load(sys.stdin) -json.dump([managed._contains_secret_shape(value) for value in values], sys.stdout) +credential_names = [ + "pass", "passwd", "customPass", "customPasswd", "DBPass", "db_pass", + "db_passwd", "db-pass", "db-passwd", "apiKey", "accessToken", + "clientSecret", "myCredential", "customPassword", "privateKey", + "foo\\nclientSecret", "replyToken", +] +benign_names = [ + "COMPASS", "BYPASS", "passengerCount", "passed", "passRate", + "passCount", "passThrough", "publicKey", "customKey", "correlationMarker", +] +is_credential_name = lambda name: bool( + managed._CREDENTIAL_NAME.search(name) or managed._CREDENTIAL_CAMEL_NAME.search(name) +) +original_environment = os.environ.copy() +def environment_is_safe(name, value): + os.environ.clear() + os.environ[name] = value + try: + managed._assert_safe_environment() + return True + except RuntimeError: + return False +try: + runtime_name_safety = [ + environment_is_safe("correlationMarker", "reply-correlation-marker-123"), + environment_is_safe("replyToken", "opaqueCredentialPayloadZ1234567890"), + environment_is_safe("replyToken", "sk-abcdefghijklmnopqrstuvwx"), + environment_is_safe("ReplyToken", "opaqueCredentialPayloadZ1234567890"), + environment_is_safe("foo\\nclientSecret", "opaqueCredentialPayloadZ1234567890"), + ] +finally: + os.environ.clear() + os.environ.update(original_environment) +json.dump( + { + "values": [managed._contains_secret_shape(value) for value in values], + "credential_names": [is_credential_name(name) for name in credential_names], + "benign_names": [is_credential_name(name) for name in benign_names], + "runtime_name_safety": runtime_name_safety, + }, + sys.stdout, +) `; const output = execFileSync("python3", ["-I", "-c", probe, managedRuntimePath], { encoding: "utf8", input: JSON.stringify(CANONICAL_SECRET_POSITIVE_VECTORS.map((vector) => vector.value)), }); - expect(JSON.parse(output)).toEqual(CANONICAL_SECRET_POSITIVE_VECTORS.map(() => true)); + expect(JSON.parse(output)).toEqual({ + values: CANONICAL_SECRET_POSITIVE_VECTORS.map(() => true), + credential_names: Array.from({ length: 17 }, () => true), + benign_names: Array.from({ length: 10 }, () => false), + runtime_name_safety: [true, false, false, false, false], + }); }); it("scrubs every shared positive vector in managed observability (#6452)", () => { @@ -140,21 +232,43 @@ if spec is None or spec.loader is None: observability = importlib.util.module_from_spec(spec) spec.loader.exec_module(observability) values = json.load(sys.stdin) -json.dump([observability._scrub_secret_values(value) for value in values], sys.stdout) +credential = "Api_" + "Key" + "=" + "ABCDEFGHIJ" +boundary_prefix = credential[:-3] +boundary_value = ( + "x" * (observability._MAX_CAPTURE_STRING_CHARS - len(boundary_prefix) - 1) + + " " + + credential +) +json.dump({ + "values": [observability._scrub_secret_values(value) for value in values], + "boundary": observability._bounded_capture(boundary_value), + "reply_token": observability._scrub_secret_values( + 'replyToken="opaqueCredentialPayloadZ1234567890"' + ), +}, sys.stdout) `; const values = CANONICAL_SECRET_POSITIVE_VECTORS.map((vector) => vector.value); const output = execFileSync("python3", ["-I", "-c", probe, observabilityPath], { encoding: "utf8", input: JSON.stringify(values), }); - const scrubbed = JSON.parse(output) as string[]; + const scrubbed = JSON.parse(output) as { + values: string[]; + boundary: string; + reply_token: string; + }; for (const [index, value] of values.entries()) { - expect(scrubbed[index], CANONICAL_SECRET_POSITIVE_VECTORS[index].label).toContain( + expect(scrubbed.values[index], CANONICAL_SECRET_POSITIVE_VECTORS[index].label).toContain( "", ); - expect(scrubbed[index], CANONICAL_SECRET_POSITIVE_VECTORS[index].label).not.toContain(value); + expect(scrubbed.values[index], CANONICAL_SECRET_POSITIVE_VECTORS[index].label).not.toContain( + value, + ); } + expect(scrubbed.boundary).toContain(""); + expect(scrubbed.boundary).not.toContain("Api_Key=ABCDEFG"); + expect(scrubbed.reply_token).toBe('replyToken=""'); }); it("preserves benign near-misses in managed observability (#6452)", () => { @@ -175,6 +289,14 @@ json.dump([observability._scrub_secret_values(value) for value in values], sys.s const values = [ "sk-too-short", "Bearer short", + "COMPASS=opaqueNonSecretPayload123", + "BYPASS=allowedValue123", + "TOPSECRET=opaqueNonSecretPayload123", + "SUBTOKEN=opaqueNonSecretPayload123", + "publicKey=opaqueVerificationMaterial123", + "customKey=opaqueNonSecretPayload123", + '{"key":"agent:main:main"}', + '{"correlationMarker":"reply-correlation-marker-123"}', "-----BEGIN PUBLIC KEY-----\\nnot-private\\n-----END PUBLIC KEY-----", ]; const output = execFileSync("python3", ["-I", "-c", probe, observabilityPath], { diff --git a/test/openclaw-tui-chat-correlation.test.ts b/test/openclaw-tui-chat-correlation.test.ts index 1c3270c3299..fda60955ded 100644 --- a/test/openclaw-tui-chat-correlation.test.ts +++ b/test/openclaw-tui-chat-correlation.test.ts @@ -44,7 +44,7 @@ type GatewayEvent = { type SentRun = { promptToken: string; - replyToken: string; + replyMarker: string; runId: string; message: string; }; @@ -65,7 +65,7 @@ type CompactChatEvent = { }; type UncorrelatedReply = { - replyToken: string; + replyMarker: string; expectedRunId: string; actualRunId?: string; state?: string; @@ -82,7 +82,7 @@ type Issue2603Analysis = { conflictingSessionRunEvents: CompactChatEvent[]; emptyFinalsForSubmittedRuns: CompactChatEvent[]; missingReplies: string[]; - duplicateReplies: { replyToken: string; count: number }[]; + duplicateReplies: { replyMarker: string; count: number }[]; uncorrelatedReplies: UncorrelatedReply[]; missingUserTurns: DuplicateUserTurn[]; duplicateUserTurns: DuplicateUserTurn[]; @@ -157,7 +157,9 @@ function analyzeIssue2603Trace({ historyMessages, }: Issue2603Trace): Issue2603Analysis { const submittedRunIds = new Set(sentRuns.map((entry) => entry.runId)); - const expectedRunByReplyToken = new Map(sentRuns.map((entry) => [entry.replyToken, entry.runId])); + const expectedRunByReplyMarker = new Map( + sentRuns.map((entry) => [entry.replyMarker, entry.runId]), + ); const chatEvents = compactChatEvents( events.filter((event) => isOwnSessionChatEvent(event, sessionKey)), ); @@ -179,16 +181,16 @@ function analyzeIssue2603Trace({ const uncorrelatedReplies: UncorrelatedReply[] = []; const visibleReplyCounts = new Map(); const finalReplyCounts = new Map(); - for (const [replyToken, expectedRunId] of expectedRunByReplyToken) { + for (const [replyMarker, expectedRunId] of expectedRunByReplyMarker) { for (const event of chatEvents) { - if (!containsReplyTokenAllowingWhitespace(event.text, replyToken)) continue; - visibleReplyCounts.set(replyToken, (visibleReplyCounts.get(replyToken) ?? 0) + 1); + if (!containsReplyTokenAllowingWhitespace(event.text, replyMarker)) continue; + visibleReplyCounts.set(replyMarker, (visibleReplyCounts.get(replyMarker) ?? 0) + 1); if (event.state === "final") { - finalReplyCounts.set(replyToken, (finalReplyCounts.get(replyToken) ?? 0) + 1); + finalReplyCounts.set(replyMarker, (finalReplyCounts.get(replyMarker) ?? 0) + 1); } if (event.runId !== expectedRunId) { uncorrelatedReplies.push({ - replyToken, + replyMarker, expectedRunId, actualRunId: event.runId, state: event.state, @@ -197,12 +199,12 @@ function analyzeIssue2603Trace({ } } const missingReplies = sentRuns - .map((entry) => entry.replyToken) - .filter((replyToken) => !visibleReplyCounts.has(replyToken)); + .map((entry) => entry.replyMarker) + .filter((replyMarker) => !visibleReplyCounts.has(replyMarker)); const duplicateReplies = sentRuns .map((entry) => ({ - replyToken: entry.replyToken, - count: finalReplyCounts.get(entry.replyToken) ?? 0, + replyMarker: entry.replyMarker, + count: finalReplyCounts.get(entry.replyMarker) ?? 0, })) .filter((entry) => entry.count > 1); @@ -302,20 +304,20 @@ const capturedIssue2603Trace: Issue2603Trace = { sentRuns: [ { promptToken: "A2603", - replyToken: "A2603-REPLY", + replyMarker: "A2603-REPLY", runId: "18f73be1-3410-46cb-8098-e881bf92c510", message: "A2603: First task. Wait 8 seconds, then reply exactly A2603-REPLY and nothing else.", }, { promptToken: "B2603", - replyToken: "B2603-REPLY", + replyMarker: "B2603-REPLY", runId: "a32dc5a4-9b45-4109-9b17-2fcd35787d0c", message: "B2603: Second task. Reply exactly B2603-REPLY and nothing else.", }, { promptToken: "C2603", - replyToken: "C2603-REPLY", + replyMarker: "C2603-REPLY", runId: "32e608a6-aeb4-4615-8416-d656f2bfa92f", message: "C2603: Third task. Reply exactly C2603-REPLY and nothing else.", }, @@ -480,8 +482,8 @@ function isOwnSessionChatEvent(event) { return typeof eventSessionKey !== "string" || eventSessionKey === sessionKey; } -function sawAllReplies(replyTokens) { - return replyTokens.every((token) => events.some((event) => isOwnSessionChatEvent(event) && compactReplyTokenText(textFromMessage(event.payload?.message)).includes(compactReplyTokenText(token)))); +function sawAllReplies(replyMarkers) { + return replyMarkers.every((marker) => events.some((event) => isOwnSessionChatEvent(event) && compactReplyTokenText(textFromMessage(event.payload?.message)).includes(compactReplyTokenText(marker)))); } ws.on("message", (data) => { @@ -536,10 +538,10 @@ ws.on("open", async () => { ["C2603", "C2603-REPLY", "C2603: Third task. Reply exactly C2603-REPLY and nothing else. Do not use tools."], ]; - for (const [promptToken, replyToken, message] of messages) { + for (const [promptToken, replyMarker, message] of messages) { const idempotencyKey = randomUUID(); const response = await request("chat.send", { sessionKey, message, deliver: false, timeoutMs: 90_000, idempotencyKey }); - sentRuns.push({ promptToken, replyToken, message, runId: response.runId ?? idempotencyKey }); + sentRuns.push({ promptToken, replyMarker, message, runId: response.runId ?? idempotencyKey }); await new Promise((resolve) => setTimeout(resolve, 1_000)); } @@ -611,7 +613,7 @@ function looksLikeEventCaptureFailure(repro: LiveIssue2603Trace): boolean { ); const hasReplyTokenEvent = repro.sentRuns.some((entry) => analysis.chatEvents.some((event) => - containsReplyTokenAllowingWhitespace(event.text, entry.replyToken), + containsReplyTokenAllowingWhitespace(event.text, entry.replyMarker), ), ); return ( @@ -673,13 +675,13 @@ describe("OpenClaw TUI chat correlation regression (#2603)", () => { ]); expect(analysis.uncorrelatedReplies).toEqual([ { - replyToken: "B2603-REPLY", + replyMarker: "B2603-REPLY", expectedRunId: "a32dc5a4-9b45-4109-9b17-2fcd35787d0c", actualRunId: "507730cf-8055-424d-87fe-ee9221c34d74", state: "final", }, { - replyToken: "C2603-REPLY", + replyMarker: "C2603-REPLY", expectedRunId: "32e608a6-aeb4-4615-8416-d656f2bfa92f", actualRunId: "5487775f-8d5e-4080-ae91-dcce701868a6", state: "final", @@ -697,7 +699,7 @@ describe("OpenClaw TUI chat correlation regression (#2603)", () => { sentRuns: [ { promptToken: "A2603", - replyToken: "A2603-REPLY", + replyMarker: "A2603-REPLY", runId: "split-reply-run", message: "A2603: Reply exactly A2603-REPLY and nothing else.", }, @@ -729,7 +731,7 @@ describe("OpenClaw TUI chat correlation regression (#2603)", () => { sentRuns: [ { promptToken: "B2603", - replyToken: "B2603-REPLY", + replyMarker: "B2603-REPLY", runId: "a32dc5a4-9b45-4109-9b17-2fcd35787d0c", message: "B2603: Second task. Reply exactly B2603-REPLY and nothing else.", }, @@ -775,7 +777,7 @@ describe("OpenClaw TUI chat correlation regression (#2603)", () => { sentRuns: [ { promptToken: "A2603", - replyToken: "A2603-REPLY", + replyMarker: "A2603-REPLY", runId: "run-a", message: "A2603: First task. Wait 8 seconds, then reply exactly A2603-REPLY and nothing else.", @@ -878,7 +880,7 @@ describe("OpenClaw TUI chat correlation regression (#2603)", () => { expect(analysis.chatEvents).toHaveLength(1); expect(analysis.foreignSessionChatEvents).toHaveLength(2); expect(analysis.uncorrelatedReplies).toEqual([]); - expect(analysis.missingReplies).toEqual([runB.replyToken, runC.replyToken]); + expect(analysis.missingReplies).toEqual([runB.replyMarker, runC.replyMarker]); }); it("keeps chat events without a sessionKey in correlation analysis (fail-open)", () => { @@ -975,7 +977,7 @@ describe("OpenClaw TUI chat correlation regression (#2603)", () => { historyMessages: [], }; - expect(runB.replyToken).toBe("B2603-REPLY"); + expect(runB.replyMarker).toBe("B2603-REPLY"); expect(analyzeIssue2603Trace(repro).chatEvents).toHaveLength(1); expect(looksLikeEventCaptureFailure(repro)).toBe(false); }); diff --git a/tools/e2e/workflow-boundary.mts b/tools/e2e/workflow-boundary.mts index 87f9d86c6bb..909c27938da 100644 --- a/tools/e2e/workflow-boundary.mts +++ b/tools/e2e/workflow-boundary.mts @@ -3607,7 +3607,49 @@ export function validateE2eWorkflowBoundary(workflowPath = DEFAULT_E2E_WORKFLOW_ errors.push("live DCode TUI host dependencies must be installed before workspace prep"); } + const dcodeProfileImportGate = requireStep( + errors, + steps, + "Verify DCode profile import gate rejects missing base dependencies", + ); + if ( + Object.hasOwn(asRecord(dcodeProfileImportGate?.env), "NEMOCLAW_DCODE_PROFILE_GATE_BASE_IMAGE") + ) { + errors.push( + "live DCode profile import gate must build the reviewed repository base without an override", + ); + } + if ( + dcodeProfileImportGate?.["if"] !== + "${{ matrix.id == 'ubuntu-repo-cloud-langchain-deepagents-code' }}" + ) { + errors.push("live DCode profile import gate must be scoped to the typed DCode target"); + } + if (dcodeProfileImportGate?.shell !== "bash") { + errors.push("live DCode profile import gate must use bash"); + } + if ( + stringValue(dcodeProfileImportGate?.run).trim() !== + "bash scripts/check-dcode-profile-import-gate.sh" + ) { + errors.push("live DCode profile import gate must run the reviewed negative-build script"); + } + const runVitest = requireStep(errors, steps, "Run live E2E tests"); + if ( + prepareWorkspace && + dcodeProfileImportGate && + steps.indexOf(prepareWorkspace) >= steps.indexOf(dcodeProfileImportGate) + ) { + errors.push("live DCode profile import gate must run after workspace prep"); + } + if ( + dcodeProfileImportGate && + runVitest && + steps.indexOf(dcodeProfileImportGate) >= steps.indexOf(runVitest) + ) { + errors.push("live DCode profile import gate must run before live E2E tests"); + } const runVitestEnv = asRecord(runVitest?.env); if (runVitestEnv.TARGET_ID !== "${{ matrix.id }}") { errors.push("live E2E step must pass matrix.id through TARGET_ID env");