diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index a1a19c3827..43b22375db 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -567,7 +567,7 @@ jobs: E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/openshell-gateway-auth-contract NEMOCLAW_RUN_LIVE_E2E: "1" NEMOCLAW_NON_INTERACTIVE: "1" - NEMOCLAW_OPENSHELL_PIN_VERSION: "0.0.72" + NEMOCLAW_OPENSHELL_PIN_VERSION: "0.0.85" DOCKER_GRPC_PROBE_IMAGE: "node:22-trixie-slim@sha256:2d9f5c76c8f4dd36e8f253bee5d828a83a6c09f36188f0b0414325232e0b175d" steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -634,7 +634,9 @@ jobs: NEMOCLAW_E2E_SHARD: ${{ matrix.agent }} NEMOCLAW_MCP_BRIDGE_AGENT: ${{ matrix.agent }} NEMOCLAW_OPENSHELL_CHANNEL: stable + NEMOCLAW_OPENSHELL_EXACT_MAIN_PROOF: "1" NEMOCLAW_RUN_LIVE_E2E: "1" + OPENSHELL_DOCKER_SUPERVISOR_IMAGE: ghcr.io/nvidia/openshell/supervisor@sha256:f4226253a3525c3832adac5b38b419a0f27d1e915effe565b5885e20f93cd5e9 steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: @@ -678,6 +680,20 @@ jobs: run: | set -euo pipefail bash scripts/install-openshell.sh + if [[ "$NEMOCLAW_MCP_BRIDGE_AGENT" == "deepagents" ]]; then + mkdir -p "$E2E_ARTIFACT_DIR/mcp-bridge-deepagents" + jq -n '{ + schemaVersion: 1, + sourceRepository: "NVIDIA/OpenShell", + releaseTag: "v0.0.85", + sourceSha: "3dee5570a46076a57a3b056f35f35ebc0861ac85", + artifacts: { + cli: {binarySha256: "222d9d53a142691d7a7de2c692f38e52d24066f9f633d53746c5fef775861bc8"}, + gateway: {binarySha256: "33bb479d936c3c1b17dd475df05747be9de74564fb67d69a4c33cdd01181d02f"}, + standaloneSandbox: {binarySha256: "863ef21ab7ef623f5e7a8728c4e5532b46bfbae3ace3b800665a1c6353a1f7d2"} + } + }' > "$E2E_ARTIFACT_DIR/mcp-bridge-deepagents/openshell-exact-main-provenance.json" + fi - name: Run MCP OpenShell provider live test run: | @@ -694,6 +710,13 @@ jobs: export OPENSHELL_BIN "$OPENSHELL_BIN" --version npx tsx tools/e2e/live-vitest-invocation.mts run --test-path test/e2e/live/mcp-bridge.test.ts + if [[ "$NEMOCLAW_MCP_BRIDGE_AGENT" == "deepagents" ]]; then + npx vitest run --project e2e-live \ + test/e2e/live/openshell-credential-generation-window.test.ts \ + -t '^openshell-credential-generation-window$' \ + --no-file-parallelism \ + --silent=false --reporter=default --reporter=test/e2e/risk-signal-reporter.ts + fi - id: mcp_artifact_secret_scan name: Scan MCP artifacts for fixture credentials @@ -3575,6 +3598,15 @@ jobs: sandbox_base_image_ref: "ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:10433a8cd2f2b809dd0fdf983514679e04c0f8aa1ff5bbff675029046033b108" openshell_version: 0.0.44 openclaw_version: 2026.5.22 + - id: v0.0.74-x86_64 + runner: ubuntu-latest + shard: v0-0-74-x86-64 + nemoclaw_ref: v0.0.74 + nemoclaw_commit: "3a05b54e8ec3e1d5550ec5c728de54af872bffe3" + installer_sha256: "a0cd3feca488d247e53d59d7d8246d2b86e75e95acb5e7d78504b3c0c60fd7db" + sandbox_base_image_ref: "ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:104151ffadc2ff0b6c815e3c95c2783ced61aee0d0f83fc327cc02be9b7e14e6" + openshell_version: 0.0.72 + openclaw_version: 2026.5.27 env: E2E_JOB: "1" E2E_TARGET_ID: "openshell-gateway-upgrade" @@ -3605,7 +3637,8 @@ jobs: - name: Run OpenShell gateway upgrade live Vitest test # Keep the original v0.0.36 fixture on x86_64 and validate the exact - # v0.0.55/OpenShell 0.0.44 regression shape on x86_64 and arm64. + # v0.0.55/OpenShell 0.0.44 regression shape on x86_64 and arm64, plus + # the immediate v0.0.74/OpenShell 0.0.72 predecessor to this bump. env: GITHUB_TOKEN: ${{ github.token }} run: | diff --git a/agents/hermes/Dockerfile b/agents/hermes/Dockerfile index 54fca1976f..eae7b7ddf6 100644 --- a/agents/hermes/Dockerfile +++ b/agents/hermes/Dockerfile @@ -134,7 +134,7 @@ COPY agents/hermes/runtime-config-guard.py /usr/local/lib/nemoclaw/hermes-runtim COPY agents/hermes/finalize-tirith-marker.py /usr/local/lib/nemoclaw/finalize-tirith-marker.py COPY agents/hermes/build-mcp-digest.py /usr/local/lib/nemoclaw/build-hermes-mcp-digest.py COPY agents/hermes/mcp-config-transaction.py /usr/local/lib/nemoclaw/hermes-mcp-config-transaction.py -COPY src/lib/actions/sandbox/openshell-child-visible-credentials.v0.0.72.json /usr/local/lib/nemoclaw/openshell-child-visible-credentials.v0.0.72.json +COPY src/lib/actions/sandbox/openshell-child-visible-credentials.v0.0.85.json /usr/local/lib/nemoclaw/openshell-child-visible-credentials.v0.0.85.json COPY scripts/state-dir-guard.py /usr/local/lib/nemoclaw/state-dir-guard.py COPY nemoclaw-blueprint/scripts/*.js /usr/local/lib/nemoclaw/preloads/ # Dockerfile.base is the source of truth for rlimit hooks. This Hermes replay @@ -143,11 +143,11 @@ COPY nemoclaw-blueprint/scripts/*.js /usr/local/lib/nemoclaw/preloads/ # minimum supported Hermes sandbox base tag guarantees those artifacts and # test/sandbox-rlimit-hooks.test.ts covers that base. RUN chmod 755 /usr/local/bin/nemoclaw-start /usr/local/lib/nemoclaw/sandbox-init.sh /usr/local/lib/nemoclaw/validate-hermes-env-secret-boundary.py /usr/local/lib/nemoclaw/patch-hermes-session-list-preview.py /usr/local/lib/nemoclaw/seed-hermes-dashboard-config.py /usr/local/lib/nemoclaw/hermes-runtime-config-guard.py /usr/local/lib/nemoclaw/finalize-tirith-marker.py /usr/local/lib/nemoclaw/hermes-mcp-config-transaction.py \ - && chown root:root /usr/local/bin/nemoclaw-gateway-control /usr/local/lib/nemoclaw/gateway-supervisor.sh /usr/local/lib/nemoclaw/state-dir-guard.py /usr/local/lib/nemoclaw/managed-gateway-control.py /usr/local/lib/nemoclaw/build-hermes-mcp-digest.py /usr/local/lib/nemoclaw/openshell-child-visible-credentials.v0.0.72.json \ + && chown root:root /usr/local/bin/nemoclaw-gateway-control /usr/local/lib/nemoclaw/gateway-supervisor.sh /usr/local/lib/nemoclaw/state-dir-guard.py /usr/local/lib/nemoclaw/managed-gateway-control.py /usr/local/lib/nemoclaw/build-hermes-mcp-digest.py /usr/local/lib/nemoclaw/openshell-child-visible-credentials.v0.0.85.json \ && chmod 700 /usr/local/bin/nemoclaw-gateway-control \ && chmod 500 /usr/local/lib/nemoclaw/state-dir-guard.py /usr/local/lib/nemoclaw/managed-gateway-control.py \ && chmod 444 /usr/local/lib/nemoclaw/gateway-supervisor.sh /usr/local/lib/nemoclaw/build-hermes-mcp-digest.py \ - && chmod 444 /usr/local/lib/nemoclaw/openshell-child-visible-credentials.v0.0.72.json \ + && chmod 444 /usr/local/lib/nemoclaw/openshell-child-visible-credentials.v0.0.85.json \ && if [ -d /usr/local/lib/nemoclaw/preloads ]; then \ chown -R 0:0 /usr/local/lib/nemoclaw/preloads \ && find /usr/local/lib/nemoclaw/preloads -type f -exec chmod 444 {} + \ @@ -207,7 +207,7 @@ PY # file has to also rewrite the Dockerfile-committed hash, which reviewers gate). # Regenerate with `sha256sum agents/hermes/{hermes-wrapper.py,validate-env-secret-boundary.py,finalize-tirith-marker.py}`. ARG NEMOCLAW_HERMES_WRAPPER_SHA256=ec8b0e4d6254175929d5a02240acd6af25e94774a30b36ba5c6f5a69c32fb9d7 -ARG NEMOCLAW_HERMES_VALIDATOR_SHA256=970d7ff03bc409ff1d5ca46bfdbd2a42ac28a32a810ccc147a508301bff38496 +ARG NEMOCLAW_HERMES_VALIDATOR_SHA256=d7371a84099b204346e0bfecfacb0086fdab690e5fadcf5e12417b6038927d19 ARG NEMOCLAW_HERMES_TIRITH_FINALIZER_SHA256=a1e6b1c53ab297569abb87c29d15c294d729e46005bfd022136b4c447a791819 # hadolint ignore=DL4006 RUN printf '%s %s\n' \ diff --git a/agents/hermes/mcp-config-transaction.py b/agents/hermes/mcp-config-transaction.py index dc1369c375..4cd644316b 100755 --- a/agents/hermes/mcp-config-transaction.py +++ b/agents/hermes/mcp-config-transaction.py @@ -58,7 +58,8 @@ ENV_PLACEHOLDER_RE = re.compile( r"^Bearer openshell:resolve:env:([A-Za-z_][A-Za-z0-9_]{0,127})$" ) -BOUNDARY_MANIFEST_NAME = "openshell-child-visible-credentials.v0.0.72.json" +OPENSHELL_REVISIONED_CREDENTIAL_NAME_RE = re.compile(r"^v[0-9]+_[A-Za-z0-9_]+$") +BOUNDARY_MANIFEST_NAME = "openshell-child-visible-credentials.v0.0.85.json" ANSI_ESCAPE_RE = re.compile( r"\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\)|[@-_])" ) @@ -116,7 +117,7 @@ def _load_credential_boundary_manifest() -> dict[str, object]: # corrupt, or wrong-version OpenShell boundary manifest. # sourceBoundary: NemoClaw owns one reviewed manifest installed beside this # helper in images; the second path is the deterministic source-checkout layout. - # whyNotSourceFix: OpenShell v0.0.72 has no machine-readable child-env contract. + # whyNotSourceFix: OpenShell v0.0.85 has no machine-readable child-env contract. # It also deliberately hides the supervisor identity mount from workload # children and the Hermes image contains no OpenShell CLI. Executing # ``openshell --version`` here would therefore either fail every real @@ -142,7 +143,10 @@ def _load_credential_boundary_manifest() -> dict[str, object]: if manifest_path is None: raise RuntimeError("Hermes MCP credential boundary manifest is missing") manifest = json.loads(manifest_path.read_text(encoding="utf-8")) - if not isinstance(manifest, dict) or manifest.get("openshellVersion") != "0.0.72": + if ( + not isinstance(manifest, dict) + or manifest.get("openshellVersion") != "0.0.85" + ): raise RuntimeError("Hermes MCP credential boundary manifest is invalid") return manifest @@ -175,7 +179,8 @@ def _manifest_strings(manifest: dict[str, object], key: str) -> frozenset[str]: def _credential_name_is_reserved(name: str) -> bool: return ( - name in _RAW_CHILD_VALUE_KEYS + OPENSHELL_REVISIONED_CREDENTIAL_NAME_RE.fullmatch(name) is not None + or name in _RAW_CHILD_VALUE_KEYS or name in _REWRITTEN_CHILD_VALUE_KEYS or name in _RUNTIME_CONTROL_KEYS or any(name.startswith(prefix) for prefix in _RUNTIME_CONTROL_PREFIXES) @@ -324,7 +329,7 @@ def _validate_payload(action: str, payload: dict[str, object]) -> None: } if action == "add" and hostname in host_aliases: raise ValueError( - "Authenticated MCP OpenShell host aliases are unavailable with OpenShell v0.0.72" + "Authenticated MCP OpenShell host aliases are unavailable with OpenShell v0.0.85" ) if not (action == "remove" and hostname in host_aliases) and ( hostname in {"localhost", "local", "internal", "metadata"} @@ -1068,7 +1073,7 @@ def _assert_non_root_lifecycle_identity() -> None: # topology. # sourceBoundary: OpenShell owns workload topology; NemoClaw owns the # immutable root-lifecycle marker and validates it before mutation. - # whyNotSourceFix: OpenShell 0.0.72 supports both topologies but exposes no + # whyNotSourceFix: OpenShell 0.0.85 supports both topologies but exposes no # attested same-UID capability that this packaged helper can query. # regressionTest: hermes-mcp-config-transaction.test.ts rejects both probe # and add when the root-lifecycle marker identifies the legacy topology. diff --git a/agents/hermes/runtime-config-guard.py b/agents/hermes/runtime-config-guard.py index 2ea3f9be3e..c1e2fb14ec 100755 --- a/agents/hermes/runtime-config-guard.py +++ b/agents/hermes/runtime-config-guard.py @@ -587,7 +587,7 @@ def _pinned_process_matches_supervised_nonroot_start( supervisor_identity: tuple[str, int | None], expected_effective_uid: int, ) -> bool: - # OpenShell 0.0.72 keeps its supervisor at PID 1 and launches the non-root + # OpenShell 0.0.85 keeps its supervisor at PID 1 and launches the non-root # NemoClaw entrypoint as a child, so startup authority must be proved from # pinned procfs identity rather than a PID-1 equality check. Remove this # compatibility proof when #6256 provides authenticated supervisor/runtime diff --git a/agents/hermes/start.sh b/agents/hermes/start.sh index 9da8f4b03c..9bef178dca 100755 --- a/agents/hermes/start.sh +++ b/agents/hermes/start.sh @@ -3062,7 +3062,7 @@ fi # while Hermes actually runs in the legacy root-separated topology. # sourceBoundary: OpenShell owns workload topology; NemoClaw owns the immutable # root-lifecycle marker and stamps it before starting the root-separated gateway. -# whyNotSourceFix: OpenShell 0.0.72 supports both topologies but exposes no +# whyNotSourceFix: OpenShell 0.0.85 supports both topologies but exposes no # attested same-UID capability that this packaged entrypoint can query. # regressionTest: hermes-mcp-config-transaction.test.ts rejects both probe and # add when the root-lifecycle marker identifies the legacy topology. diff --git a/agents/hermes/validate-env-secret-boundary.py b/agents/hermes/validate-env-secret-boundary.py index 998e930caf..fddf0c167c 100755 --- a/agents/hermes/validate-env-secret-boundary.py +++ b/agents/hermes/validate-env-secret-boundary.py @@ -52,12 +52,12 @@ } ) RUNTIME_ALLOWED_RAW_SECRET_KEYS = frozenset({"OPENCLAW_GATEWAY_TOKEN"}) -# OpenShell's Docker/Podman supervisor owns this variable and injects a mounted -# file path, not private-key material. Keep the allowance exact and runtime-only -# so a caller cannot use the secret-shaped name to smuggle an arbitrary value or -# persist it in Hermes' mutable .env file. -RUNTIME_ALLOWED_PLATFORM_PATH_VALUES = frozenset( - {("OPENSHELL_TLS_KEY", "/etc/openshell/tls/client/tls.key")} +# OpenShell 8eacb477 (candidate 0.0.85) makes these supervisor-only identity +# variables and removes them from entrypoint, exec, and connect children. Their +# presence in Hermes is therefore contract drift even when the value is only a +# mounted path. +OPENSHELL_SUPERVISOR_ONLY_ENV_KEYS = frozenset( + {"OPENSHELL_TLS_CA", "OPENSHELL_TLS_CERT", "OPENSHELL_TLS_KEY"} ) ALLOWED_LITERALS = frozenset({"", "[STRIPPED_BY_MIGRATION]"}) MAX_ENV_BYTES = 4 * 1024 * 1024 @@ -451,6 +451,11 @@ def validate_env_file(path: str) -> int: key = key.strip() if not KEY_NAME_RE.fullmatch(key): continue + if key in OPENSHELL_SUPERVISOR_ONLY_ENV_KEYS: + violation_count += 1 + if len(violations) < MAX_VIOLATIONS: + violations.append(f"{key} (line {lineno})") + continue if key in ENV_FILE_ALLOWED_NONSECRET_KEYS: continue if key in ENV_FILE_ALLOWED_RAW_SECRET_KEYS and is_allowed_raw_secret_value( @@ -468,8 +473,9 @@ def validate_env_file(path: str) -> int: return 0 _emit_violations( "[SECURITY] Refusing Hermes startup because /sandbox/.hermes/.env " - "contains raw secret-shaped values. Store credentials in OpenShell " - "providers and keep only openshell resolver placeholders in the sandbox.", + "contains raw secret-shaped values or OpenShell supervisor-only identity " + "variables. Store credentials in OpenShell providers and keep only " + "openshell resolver placeholders in the sandbox.", violations, violation_count - len(violations), ) @@ -481,14 +487,17 @@ def validate_runtime_env(env: dict[str, str] | None = None) -> int: violations: list[str] = [] violation_count = 0 for key, value in sorted(source.items()): + if key in OPENSHELL_SUPERVISOR_ONLY_ENV_KEYS: + violation_count += 1 + if len(violations) < MAX_VIOLATIONS: + violations.append(key) + continue if key in RUNTIME_ALLOWED_NONSECRET_KEYS: continue if key in RUNTIME_ALLOWED_RAW_SECRET_KEYS and is_allowed_raw_secret_value( key, value ): continue - if (key, value) in RUNTIME_ALLOWED_PLATFORM_PATH_VALUES: - continue if not KEY_NAME_RE.fullmatch(key): continue if not SECRET_KEY_RE.search(key): @@ -502,8 +511,9 @@ def validate_runtime_env(env: dict[str, str] | None = None) -> int: return 0 _emit_violations( "[SECURITY] Refusing Hermes startup because the process environment " - "contains raw secret-shaped values. Store credentials in OpenShell " - "providers and keep only openshell resolver placeholders in the sandbox.", + "contains raw secret-shaped values or OpenShell supervisor-only identity " + "variables. Store credentials in OpenShell providers and keep only " + "openshell resolver placeholders in the sandbox.", violations, violation_count - len(violations), ) diff --git a/agents/langchain-deepagents-code/dcode-wrapper.sh b/agents/langchain-deepagents-code/dcode-wrapper.sh index f97f32a7a1..5714db7610 100755 --- a/agents/langchain-deepagents-code/dcode-wrapper.sh +++ b/agents/langchain-deepagents-code/dcode-wrapper.sh @@ -41,7 +41,6 @@ unset PYTHONHOME PYTHONPATH readonly DEEPAGENTS_ENV_FILE="/sandbox/.deepagents/.env" readonly OPENSHELL_ENV_PLACEHOLDER_PREFIX="openshell:resolve:env:" readonly DEEPAGENTS_CONFIG_FILE="/sandbox/.deepagents/config.toml" -readonly OPENSHELL_TLS_KEY_PATH="/etc/openshell/tls/client/tls.key" readonly DEEPAGENTS_AUTH_FILE="/sandbox/.deepagents/.state/auth.json" readonly DEEPAGENTS_CODEX_AUTH_FILE="/sandbox/.deepagents/.state/chatgpt-auth.json" readonly MANAGED_DCODE_AUTO_APPROVAL_FILE="/usr/local/share/nemoclaw/dcode-auto-approval" @@ -351,14 +350,15 @@ has_credential_name_context() { return 1 } -# SECURITY: OpenShell's supervisor injects this mounted TLS key path into the -# runtime environment. Allow only the exact name/value pair after the generic -# value scan. Never allow the name alone, and never apply this exception to the -# mutable Deep Agents Code .env file. -is_allowed_openshell_runtime_value() { - local name="$1" - local value="$2" - [ "$name" = "OPENSHELL_TLS_KEY" ] && [ "$value" = "$OPENSHELL_TLS_KEY_PATH" ] +# OpenShell 8eacb477 (candidate 0.0.85) strips these supervisor identity +# variables from entrypoint, exec, and connect children. Reject their presence +# regardless of value so a runtime regression cannot silently expose mounted +# mTLS identity to dcode. +is_openshell_supervisor_only_env_name() { + case "$1" in + OPENSHELL_TLS_CA | OPENSHELL_TLS_CERT | OPENSHELL_TLS_KEY) return 0 ;; + esac + return 1 } # OTLP endpoint variables carry the collector URL, not a credential. The @@ -440,8 +440,7 @@ is_openshell_env_placeholder_for_name() { local canonical revision_prefix revision_suffix versioned revision # OPENSHELL_TLS_KEY is supervisor infrastructure, not a provider credential. - # Only its exact mounted path is accepted from the runtime environment below; - # never let a provider placeholder bypass that name/value allowlist. + # Never let a provider placeholder bypass that supervisor-only boundary. [ "$name" != "OPENSHELL_TLS_KEY" ] || return 1 # Keep this identifier contract aligned with OpenShell provider env keys. @@ -506,6 +505,9 @@ assert_no_secret_runtime_env() { name="${pair%%=*}" [ "$name" != "$pair" ] || continue value="${pair#*=}" + if is_openshell_supervisor_only_env_name "$name"; then + refuse_secret_env "runtime environment variable" "$name" + fi if [[ "$value" == *"$OPENSHELL_ENV_PLACEHOLDER_PREFIX"* ]]; then if is_openshell_env_placeholder_for_name "$name" "$value"; then continue @@ -526,7 +528,7 @@ assert_no_secret_runtime_env() { fi continue fi - if has_credential_name_context "$name" && [ ${#value} -ge 10 ] && ! is_allowed_openshell_runtime_value "$name" "$value"; then + if has_credential_name_context "$name" && [ ${#value} -ge 10 ]; then refuse_secret_env "runtime environment variable" "$name" fi done < <(env -0) @@ -578,6 +580,9 @@ assert_no_secret_env_file() { ;; esac value="$(trim_whitespace "$value")" + if is_openshell_supervisor_only_env_name "$key"; then + refuse_secret_env "$env_file" "$key" + fi if is_dynamic_dotenv_value "$value"; then refuse_dynamic_env "$env_file" "$key" fi diff --git a/agents/langchain-deepagents-code/managed-dcode-runtime.py b/agents/langchain-deepagents-code/managed-dcode-runtime.py index 33ce036e7a..d4e36ea4d1 100644 --- a/agents/langchain-deepagents-code/managed-dcode-runtime.py +++ b/agents/langchain-deepagents-code/managed-dcode-runtime.py @@ -82,6 +82,11 @@ r"\u202f\u205f\u3000\ufeff'\"]" ) _OPENSHELL_ENV_PLACEHOLDER_PREFIX = "openshell:resolve:env:" +# OpenShell 8eacb477 reserves these for the supervisor and strips them from all +# child process shapes; fail closed if a regressed runtime exposes them here. +_OPENSHELL_SUPERVISOR_ONLY_ENV_NAMES = frozenset( + {"OPENSHELL_TLS_CA", "OPENSHELL_TLS_CERT", "OPENSHELL_TLS_KEY"} +) _UPSTREAM_PROVIDER_ENV = "NEMOCLAW_UPSTREAM_PROVIDER" _FETCH_URL_TRUSTED_PROXY_ENV = ( "DEEPAGENTS_CODE_FETCH_URL_TRUSTED_PROXY_URL" @@ -236,8 +241,6 @@ def _is_openshell_placeholder_for_name(name: str, value: str) -> bool: def _is_managed_value(name: str, value: str) -> bool: if name == "DEEPAGENTS_CODE_OPENAI_API_KEY": return value == "nemoclaw-managed-inference" - if name == "OPENSHELL_TLS_KEY": - return value == "/etc/openshell/tls/client/tls.key" if name == "SLACK_BOT_TOKEN": return bool(re.fullmatch(r"xoxb-[A-Za-z0-9_-]{10,}", value)) and not _contains_other_platform_secret(value, "slack") if name == "SLACK_APP_TOKEN": @@ -283,6 +286,11 @@ def _is_safe_otlp_endpoint_url(value: str) -> bool: def _assert_safe_environment() -> None: for name, value in os.environ.items(): + if name in _OPENSHELL_SUPERVISOR_ONLY_ENV_NAMES: + raise RuntimeError( + f"runtime environment variable {name} is reserved for the " + "OpenShell supervisor and must not reach a child process" + ) if _OPENSHELL_ENV_PLACEHOLDER_PREFIX in value: if _is_openshell_placeholder_for_name(name, value): continue diff --git a/ci/platform-matrix.json b/ci/platform-matrix.json index 6f7eeda445..1ad3b95bdc 100644 --- a/ci/platform-matrix.json +++ b/ci/platform-matrix.json @@ -229,7 +229,7 @@ { "name": "Intel Mac (macOS x86_64)", "status": "unsupported", - "notes": "OpenShell does not publish macOS x86_64 standalone gateway assets. Install hard-fails on x86_64 macOS (`scripts/install-openshell.sh:663`). See issue #954 (closed)." + "notes": "OpenShell does not publish macOS x86_64 standalone gateway assets. Install hard-fails on x86_64 macOS (`scripts/install-openshell.sh:689`). See issue #954 (closed)." }, { "name": "Non-Ubuntu/Debian Linux distros", diff --git a/docs/deployment/set-up-mcp-bridge.mdx b/docs/deployment/set-up-mcp-bridge.mdx index 79c6af7328..cbe5e03e10 100644 --- a/docs/deployment/set-up-mcp-bridge.mdx +++ b/docs/deployment/set-up-mcp-bridge.mdx @@ -31,7 +31,7 @@ The integration has three parts: This integration depends on the OpenShell MCP/JSON-RPC L7 policy support from [NVIDIA/OpenShell#1865](https://github.com/NVIDIA/OpenShell/pull/1865). -NemoClaw v0.0.74 defaults to the pinned stable OpenShell `0.0.72` release, which exposes native `protocol: mcp` policy handling and provider-backed credential replacement. +Current NemoClaw builds default to the pinned stable OpenShell `0.0.85` release, which exposes native `protocol: mcp` policy handling and provider-backed credential replacement. The optional OpenShell development channel is compatibility evidence only and is not a shipping target. NemoClaw accepts Streamable HTTP MCP endpoints only. @@ -71,26 +71,26 @@ NemoClaw accepts canonical HTTPS MCP URLs and writes the credential placeholder ### DNS Pinning Contract -The MCP integration pins its OpenShell DNS enforcement contract to [`NVIDIA/OpenShell@8cb16de9eae4c44d7d31e1493747d8c10abb5963`](https://github.com/NVIDIA/OpenShell/tree/8cb16de9eae4c44d7d31e1493747d8c10abb5963). +The MCP integration pins its OpenShell DNS enforcement contract to [`NVIDIA/OpenShell@3dee5570a46076a57a3b056f35f35ebc0861ac85`](https://github.com/NVIDIA/OpenShell/tree/3dee5570a46076a57a3b056f35f35ebc0861ac85). OpenShell uses one socket-address list throughout resolution, validation, and connection: -- [`crates/openshell-supervisor-network/src/proxy.rs:2476-2502`](https://github.com/NVIDIA/OpenShell/blob/8cb16de9eae4c44d7d31e1493747d8c10abb5963/crates/openshell-supervisor-network/src/proxy.rs#L2476-L2502) produces the list. -- [`crates/openshell-supervisor-network/src/proxy.rs:2527-2567`](https://github.com/NVIDIA/OpenShell/blob/8cb16de9eae4c44d7d31e1493747d8c10abb5963/crates/openshell-supervisor-network/src/proxy.rs#L2527-L2567) validates every address. -- [`crates/openshell-supervisor-network/src/proxy.rs:2622-2630`](https://github.com/NVIDIA/OpenShell/blob/8cb16de9eae4c44d7d31e1493747d8c10abb5963/crates/openshell-supervisor-network/src/proxy.rs#L2622-L2630) returns the validated list unchanged. +- [`crates/openshell-supervisor-network/src/proxy.rs:2648-2674`](https://github.com/NVIDIA/OpenShell/blob/3dee5570a46076a57a3b056f35f35ebc0861ac85/crates/openshell-supervisor-network/src/proxy.rs#L2648-L2674) produces the list. +- [`crates/openshell-supervisor-network/src/proxy.rs:2699-2739`](https://github.com/NVIDIA/OpenShell/blob/3dee5570a46076a57a3b056f35f35ebc0861ac85/crates/openshell-supervisor-network/src/proxy.rs#L2699-L2739) validates every address. +- [`crates/openshell-supervisor-network/src/proxy.rs:2794-2803`](https://github.com/NVIDIA/OpenShell/blob/3dee5570a46076a57a3b056f35f35ebc0861ac85/crates/openshell-supervisor-network/src/proxy.rs#L2794-L2803) returns the validated list unchanged. -The CONNECT path passes that list directly to [`TcpStream::connect` at `crates/openshell-supervisor-network/src/proxy.rs:822-832`](https://github.com/NVIDIA/OpenShell/blob/8cb16de9eae4c44d7d31e1493747d8c10abb5963/crates/openshell-supervisor-network/src/proxy.rs#L822-L832). -The explicit HTTP-forward path carries the same list from [`crates/openshell-supervisor-network/src/proxy.rs:3885-3893`](https://github.com/NVIDIA/OpenShell/blob/8cb16de9eae4c44d7d31e1493747d8c10abb5963/crates/openshell-supervisor-network/src/proxy.rs#L3885-L3893) to [`crates/openshell-supervisor-network/src/proxy.rs:4123-4125`](https://github.com/NVIDIA/OpenShell/blob/8cb16de9eae4c44d7d31e1493747d8c10abb5963/crates/openshell-supervisor-network/src/proxy.rs#L4123-L4125). +The CONNECT path passes that list directly to [`TcpStream::connect` at `crates/openshell-supervisor-network/src/proxy.rs:1079-1081`](https://github.com/NVIDIA/OpenShell/blob/3dee5570a46076a57a3b056f35f35ebc0861ac85/crates/openshell-supervisor-network/src/proxy.rs#L1079-L1081). +The explicit HTTP-forward path carries the same list from [`crates/openshell-supervisor-network/src/proxy.rs:4093-4100`](https://github.com/NVIDIA/OpenShell/blob/3dee5570a46076a57a3b056f35f35ebc0861ac85/crates/openshell-supervisor-network/src/proxy.rs#L4093-L4100) to [`crates/openshell-supervisor-network/src/proxy.rs:4340-4342`](https://github.com/NVIDIA/OpenShell/blob/3dee5570a46076a57a3b056f35f35ebc0861ac85/crates/openshell-supervisor-network/src/proxy.rs#L4340-L4342). There is no second hostname resolution between validation and connection in either path. -### Stable OpenShell 0.0.72 Limitations +### Stable OpenShell 0.0.85 Limitations -OpenShell v0.0.72 attributes network policy with `/proc//exe` and process ancestors, so script-based adapters require Node or Python interpreter grants rather than immutable package-entrypoint identities. +OpenShell v0.0.85 attributes network policy with `/proc//exe` and process ancestors, so script-based adapters require Node or Python interpreter grants rather than immutable package-entrypoint identities. NemoClaw compensates with an exact HTTPS destination, path, MCP method profile, DNS pins, and a unique least-privilege credential for each server. Remove the interpreter grants when OpenShell exposes stable script or package entrypoint attribution. -OpenShell v0.0.72 attaches static provider credentials at sandbox scope rather than reserving a credential key exclusively for one endpoint. +OpenShell v0.0.85 attaches static provider credentials at sandbox scope rather than reserving a credential key exclusively for one endpoint. It also does not expose an immutable provider binding on an attachment, provide a `tls: require` policy mode, bind the HTTP `Host` header to the policy destination, or include query parameters in MCP path matching. NemoClaw rejects credential-key reuse between managed MCP servers, creates a dedicated provider for each definition, reports the residual risk in `status`, and requires a unique least-privilege token and environment key. @@ -98,7 +98,7 @@ Do not grant a broader inspected-HTTP route to the same adapter runtime because The generated configuration uses the canonical HTTPS URL, but malicious code running as an allowed interpreter can deliberately change the scheme, `Host` header, or query string within the supported OpenShell policy contract. -OpenShell v0.0.72 updates, attaches, detaches, and deletes providers by mutable name instead of an atomic immutable identity. +OpenShell v0.0.85 updates, attaches, detaches, and deletes providers by mutable name instead of an atomic immutable identity. NemoClaw compensates with randomized provider names, the per-sandbox lifecycle lock, and immediate ownership checks against the recorded provider ID, type, and credential-key metadata before mutations. NemoClaw fails closed and preserves retryable state when those checks do not match, but the checks do not provide compare-and-swap behavior against another OpenShell client. diff --git a/docs/manage-sandboxes/add-mcp-server.mdx b/docs/manage-sandboxes/add-mcp-server.mdx index ff6a7b8471..77af168a32 100644 --- a/docs/manage-sandboxes/add-mcp-server.mdx +++ b/docs/manage-sandboxes/add-mcp-server.mdx @@ -56,7 +56,11 @@ Do not reuse OpenShell Google Cloud compatibility names such as `GCP_PROJECT_ID` NemoClaw rejects them because OpenShell exposes those non-secret configuration names as child-process values. It also rejects `GCE_METADATA_HOST`, `GCE_METADATA_IP`, and `METADATA_SERVER_DETECTION`, which OpenShell rewrites for its metadata emulator. -The child-visible compatibility list is pinned to OpenShell `v0.0.72` commit `8cb16de9eae4c44d7d31e1493747d8c10abb5963` and must be reviewed with every OpenShell version change. +NemoClaw pins the child-visible compatibility list to OpenShell `v0.0.85` commit `3dee5570a46076a57a3b056f35f35ebc0861ac85`. +Review this list with every OpenShell version change. + +OpenShell reserves credential names matching `v[0-9]+_[A-Za-z0-9_]+` for revisioned placeholders. +NemoClaw rejects names such as `v10_GITHUB_TOKEN` because OpenShell skips them instead of attaching a credential resolver. NemoClaw rejects host subprocess controls such as `PATH`, proxy or TLS variables, and `OPENSHELL_*`, `GRPC_*`, `LC_*`, or `XDG_*` keys. Loader, shell, language, and agent runtime controls such as `LD_PRELOAD`, `BASH_ENV`, `NODE_OPTIONS`, `PYTHONHOME`, `NEMOCLAW_*`, and `OPENCLAW_*` are rejected as well. @@ -86,8 +90,8 @@ OpenShell re-resolves the hostname for each new connection, requires every curre A DNS change to a private, special-use, or otherwise unpinned address fails closed instead of widening the route. -Authenticated MCP rejects `host.openshell.internal`, `host.docker.internal`, and `host.containers.internal` on stable OpenShell v0.0.72. -That release does not expose an attested driver gateway address that NemoClaw can pin. +Authenticated MCP rejects `host.openshell.internal`, `host.docker.internal`, and `host.containers.internal` on stable OpenShell `v0.0.85`. +That release has a trusted-gateway branch for one narrow link-local topology, but it does not expose an attested driver gateway address that NemoClaw can pin. Use a normal HTTPS DNS endpoint with public address records until OpenShell exposes attested gateway state for exact policy pinning. ## Understand the Generated Method Profile diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index ad3c75cef5..303d509e66 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -906,11 +906,16 @@ printf 'hello\n' | $$nemoclaw my-assistant exec --stdin -- cat ssh dgx-spark '$$nemoclaw my-assistant exec --no-stdin -- pwd' ``` -The OpenShell exec endpoint rejects any command argument (the values after `--`) that contains a newline or carriage return, so multi-line commands such as a `bash` heredoc cannot be passed through `exec`. -NemoClaw detects this before dispatch, names the offending argument position, and exits with status `2` instead of surfacing the lower-level OpenShell `InvalidArgument` error. -Join the statements with semicolons (`$$nemoclaw exec -- bash -lc "cmd1; cmd2"`). -Pipe the script into the sandbox shell over stdin (`printf 'cmd1\ncmd2\n' | $$nemoclaw exec --stdin -- bash`). -Or write the script to a file in the sandbox and run it (`$$nemoclaw exec -- bash `). +OpenShell preserves line endings and quote characters inside each command argument, so inline scripts and heredocs can be passed as one argument after `--`. +For example, a shell variable keeps the multi-line script in one argv element: + +```bash +script=$'cat <<\'EOF\'\nline one\nline two\nEOF' +$$nemoclaw exec -- bash -lc "$script" +``` + +NUL bytes are still rejected in command arguments. +Line breaks are accepted only in command argv: `--workdir` remains single-line, and NemoClaw does not expose OpenShell request-environment injection on this command. | Flag | Description | |------|-------------| @@ -1407,11 +1412,16 @@ By default, NemoClaw inherits caller stdin only when it is a terminal. Non-terminal or unavailable stdin is closed so SSH, CI, and other one-shot commands cannot wait on an inherited pipe. Pass `--stdin` to forward an intentional pipe, or `--no-stdin` to close terminal stdin explicitly. -The OpenShell exec endpoint rejects any command argument (the values after `--`) that contains a newline or carriage return, so multi-line commands such as a `bash` heredoc cannot be passed through `exec`. -NemoClaw detects this before dispatch, names the offending argument position, and exits with status `2` instead of surfacing the lower-level OpenShell `InvalidArgument` error. -Join the statements with semicolons (`$$nemoclaw exec -- bash -lc "cmd1; cmd2"`). -Pipe the script into the sandbox shell over stdin (`printf 'cmd1\ncmd2\n' | $$nemoclaw exec --stdin -- bash`). -Or write the script to a file in the sandbox and run it (`$$nemoclaw exec -- bash `). +OpenShell preserves line endings and quote characters inside each command argument, so inline scripts and heredocs can be passed as one argument after `--`. +For example, a shell variable keeps the multi-line script in one argv element: + +```bash +script=$'cat <<\'EOF\'\nline one\nline two\nEOF' +$$nemoclaw exec -- bash -lc "$script" +``` + +NUL bytes are still rejected in command arguments. +Line breaks are accepted only in command argv: `--workdir` remains single-line, and NemoClaw does not expose OpenShell request-environment injection on this command. | Flag | Description | |------|-------------| @@ -1965,7 +1975,7 @@ The full URL and path are persisted and displayed, so URLs cannot contain userin Server names must start with a letter and contain at most 64 letters, digits, hyphens, or underscores, and endpoint hostnames must use canonical lowercase DNS labels. NemoClaw rejects invalid names and endpoints before it writes lifecycle state or changes OpenShell resources. NemoClaw generates a narrow `protocol: mcp` policy for the destination, literal path, adapter binaries, pinned addresses, explicit MCP methods, and a 131,072-byte request-body limit. -OpenShell `0.0.72` evaluates that policy before replacing the attached provider placeholder in the allowed request header. +OpenShell `0.0.85` evaluates that policy before replacing the attached provider placeholder in the allowed request header. Static provider placeholders are sandbox-scoped rather than endpoint-exclusive, so do not grant broader inspected-HTTP routes to the same adapter runtime and credential key. The sandbox client connects directly through OpenShell's existing egress path, and NemoClaw does not run a host-side MCP data-plane bridge, proxy, relay, or listener. After the add commits, NemoClaw freshly verifies the exact generated policy, expected provider attachment, recorded provider ID, generic type, valid resource version, and exactly one credential key matching the recorded key. @@ -2064,7 +2074,7 @@ For a v1 image, NemoClaw removes the exact registry-owned entry from the legacy The command fails closed on observed drift. `--force` may remove a modified same-name agent adapter entry, but provider deletion still requires the exact recorded ID, type, and credential key and policy deletion still requires exact owned content. Residuals preserve registry state. -OpenShell `0.0.72` mutates providers by name, so do not concurrently replace a managed provider through another OpenShell client during this command. +OpenShell `0.0.85` mutates providers by name, so do not concurrently replace a managed provider through another OpenShell client during this command. When an interrupted destroy leaves a prepared-only transaction, deletion is not durably confirmed. If the sandbox is still live, run `$$nemoclaw mcp remove --force` with the affected server name. @@ -3184,7 +3194,7 @@ Passthrough commands do not consume flags intended for the downstream command as | Variable | Default | Service | |----------|---------|---------| | `NEMOCLAW_GATEWAY_PORT` | 8080 | OpenShell gateway port | -| `NEMOCLAW_GATEWAY_BIND_ADDRESS` | 127.0.0.1 | The OpenShell gateway uses this bind address; Docker-driver gateways on OpenShell 0.0.72 keep it on loopback while gateway JWT auth is active. | +| `NEMOCLAW_GATEWAY_BIND_ADDRESS` | 127.0.0.1 | The OpenShell gateway uses this bind address; Docker-driver gateways on OpenShell 0.0.85 keep it on loopback while gateway JWT auth is active. | | `NEMOCLAW_DASHBOARD_PORT` | 18789 (auto-derived from `CHAT_UI_URL` port if set) | Dashboard or API forward | @@ -3206,7 +3216,7 @@ When you run multiple NemoClaw gateways with different `NEMOCLAW_GATEWAY_PORT` v On non-WSL hosts, `NEMOCLAW_OLLAMA_PORT` and `NEMOCLAW_OLLAMA_PROXY_PORT` must be different. If you run Ollama on port 11435, set `NEMOCLAW_OLLAMA_PROXY_PORT` to another free port before onboarding. -`NEMOCLAW_GATEWAY_BIND_ADDRESS` accepts only `127.0.0.1` and `0.0.0.0`, but Docker-driver gateways on OpenShell 0.0.72 reject `0.0.0.0` while gateway JWT auth is active. +`NEMOCLAW_GATEWAY_BIND_ADDRESS` accepts only `127.0.0.1` and `0.0.0.0`, but Docker-driver gateways on OpenShell 0.0.85 reject `0.0.0.0` while gateway JWT auth is active. Keep the OpenShell gateway on loopback and use `NEMOCLAW_DASHBOARD_BIND` when you need remote browser/API access. @@ -3457,7 +3467,7 @@ Set them before running `$$nemoclaw onboard`. | `NEMOCLAW_SANDBOX_BASE_IMAGE_REFRESH` | `1`, `true`, `yes`, or `on` to enable | Bypasses recorded sandbox base-image resolution metadata during onboarding, recreation, and rebuild. NemoClaw reruns candidate resolution but can still use a compatible image from Docker's local image store. Versioned release candidates that exist locally but fail validation are refreshed from the registry once during normal resolution. This setting does not discard onboarding session state. | | `NEMOCLAW_SANDBOX_BASE_LOCAL_BUILD` | unset or `auto` (default); `1`, `true`, `yes`, or `on` to enable; `0`, `false`, `no`, or `off` to disable | Controls whether base-image resolution may build a compatible image locally. The default allows builds during normal CLI runs and disables them when `NODE_ENV=test` or `VITEST=true`. When source inputs or a missing/incompatible release-version base require a fresh build, disabling local builds makes resolution fail instead of using an unproven image. | | `NEMOCLAW_DOCKER_GPU_PATCH` | unset, `auto`, `fallback`, `1`, or `0`; other legacy nonzero values remain accepted through `v0.0.x` and will be removed in `v0.1.0` | Selects Linux Docker-driver GPU routing. Unset, `auto`, or `0` uses native OpenShell GPU injection on ordinary native Linux. `fallback` explicitly opts into one native attempt followed by one bounded compatibility retry when trusted host evidence identifies a GPU-routing failure. `1` and legacy nonzero values select the compatibility patch from the outset. Docker Desktop WSL and Jetson/Tegra use the compatibility path by default; Docker Desktop WSL ignores `0`, while Jetson/Tegra accepts `0` only as a troubleshooting override that bypasses device-group propagation. | -| `NEMOCLAW_OPENSHELL_GATEWAY_CONTAINER_PATCH` | `1` to enable; disabled by default | This setting explicitly opts into the Linux gateway compatibility container for an older host ABI or a diagnostic run; use it only on a trusted local host because it uses host networking and mounts the Docker socket read-only even though the socket still exposes the privileged Docker API; prefer OpenShell 0.0.72's directly supported glibc 2.28+ path; see the [OpenShell 0.0.72 compatibility review](/user-guide/openclaw/security/openshell-0.0.72-compatibility-review#source-of-truth-boundaries) for details. | +| `NEMOCLAW_OPENSHELL_GATEWAY_CONTAINER_PATCH` | `1` to enable; disabled by default | This setting explicitly opts into the Linux gateway compatibility container for an older host ABI or a diagnostic run; use it only on a trusted local host because it uses host networking and mounts the Docker socket read-only even though the socket still exposes the privileged Docker API; prefer OpenShell 0.0.85's directly supported glibc 2.28+ path; see the [OpenShell gateway compatibility review](/user-guide/openclaw/security/openshell-0.0.72-compatibility-review#source-of-truth-boundaries) for the unchanged container boundary. | | `NEMOCLAW_OPENSHELL_GATEWAY_BIN` | path | Advanced override for the `openshell-gateway` binary used by the Linux Docker-driver standalone fallback. Defaults to the binary next to `openshell`, then common install paths. | | `NEMOCLAW_OPENSHELL_SANDBOX_BIN` | path | Advanced override for the `openshell-sandbox` binary used by the Linux Docker-driver standalone fallback. Defaults to the binary next to `openshell`, then common install paths. | | `NEMOCLAW_OPENSHELL_GATEWAY_STATE_DIR` | path | Advanced override for the Linux Docker-driver gateway SQLite state directory and standalone-fallback PID file. Defaults to `~/.local/state/nemoclaw/openshell-docker-gateway`. | diff --git a/docs/reference/platform-support.mdx b/docs/reference/platform-support.mdx index fc80acf9eb..e048a3d222 100644 --- a/docs/reference/platform-support.mdx +++ b/docs/reference/platform-support.mdx @@ -162,7 +162,7 @@ They are listed here so launch material, sales conversations, and support triage | Item | Status | Why | |------|--------|-----| | Podman / other container runtimes | Unsupported | Onboard surfaces an explicit unsupported-runtime error for Podman (`src/lib/onboard/fatal-runtime-preflight.ts` prints the rejection; `src/lib/onboard/preflight.ts` flags the unsupported runtime upstream). Only Docker Engine, Docker Desktop, and Colima are supported. See issue #420 (closed). | -| Intel Mac (macOS x86_64) | Unsupported | OpenShell does not publish macOS x86_64 standalone gateway assets. Install hard-fails on x86_64 macOS (`scripts/install-openshell.sh:663`). See issue #954 (closed). | +| Intel Mac (macOS x86_64) | Unsupported | OpenShell does not publish macOS x86_64 standalone gateway assets. Install hard-fails on x86_64 macOS (`scripts/install-openshell.sh:689`). See issue #954 (closed). | | Non-Ubuntu/Debian Linux distros | Unsupported | Installer assumes `apt-get`. Fedora/Rocky/Alma/Arch/NixOS are not validated and the installer's package-manager probes do not cover them. See open issue #899 (Fedora hang). | | Native Kubernetes or OpenShift deployments | Unsupported | NemoClaw runs the sandbox as a Docker container, not a Kubernetes pod. The default Docker-driver topology does not embed k3s. Operator-managed K8s/OpenShift deployments are out of scope; see issue #407 (community OpenShift through agent-sandbox CRD). | | Air-gapped / offline installs | Unsupported | Onboard assumes network reachability for package fetches, container pulls, and provider validation. See open issues #4872 and #2218 (production-deployment epic covering air-gapped support, China network guidance, multi-host topology). | diff --git a/docs/reference/troubleshoot-mcp-servers.mdx b/docs/reference/troubleshoot-mcp-servers.mdx index 68dfa7634f..5e7c9bf3b5 100644 --- a/docs/reference/troubleshoot-mcp-servers.mdx +++ b/docs/reference/troubleshoot-mcp-servers.mdx @@ -30,6 +30,9 @@ Compare status for the same server on a known-good host. A `credential resolution: unknown` verdict with an endpoint or policy detail means the probe could not reach a judgment. Fix the reported endpoint or policy condition, then rerun status. +A detail containing `CONNECT 503` means OpenShell failed closed before TLS setup because gateway TLS termination state was unavailable. +Inspect the OpenShell gateway's ephemeral CA generation and CA-file initialization, repair the reported condition, and rerun `mcp status `. + ## Provider Is Missing During Restart If restart reports a missing provider and the original credential is not registered in OpenShell, export the same variable name used during add and retry. diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index 32368fe9b6..ad84ce43b2 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -304,7 +304,7 @@ NEMOCLAW_DASHBOARD_BIND=0.0.0.0 NEMOCLAW_GATEWAY_PORT=8990 $$nemoclaw onboard Use `NEMOCLAW_DASHBOARD_BIND=0.0.0.0` again on later `$$nemoclaw connect` calls. If the sandbox was originally created without remote bind, recreate it with the same onboard command plus `--recreate-sandbox` before connecting remotely. -Docker-driver gateways on OpenShell 0.0.72 reject `NEMOCLAW_GATEWAY_BIND_ADDRESS=0.0.0.0` while gateway JWT auth is active. +Docker-driver gateways on OpenShell 0.0.85 reject `NEMOCLAW_GATEWAY_BIND_ADDRESS=0.0.0.0` while gateway JWT auth is active. Use `NEMOCLAW_GATEWAY_BIND_ADDRESS=0.0.0.0` only on supported gateway modes and only when other hosts on the network should be able to reach the gateway. @@ -312,14 +312,14 @@ Use `NEMOCLAW_GATEWAY_BIND_ADDRESS=0.0.0.0` only on supported gateway modes and ### Older-glibc gateway compatibility container -OpenShell 0.0.72 directly supports Linux hosts with glibc 2.28 or newer. +OpenShell 0.0.85 directly supports Linux hosts with glibc 2.28 or newer. On an older trusted host, `NEMOCLAW_OPENSHELL_GATEWAY_CONTAINER_PATCH=1` explicitly opts into NemoClaw's compatibility container. Leave it unset on supported hosts. The compatibility container uses host networking and mounts the host Docker socket read-only. A read-only socket mount still permits privileged Docker API operations and can control the host, so do not enable this mode on an untrusted or shared host. The gateway remains loopback-bound, and startup fails closed unless the configured Unix socket answers as a Docker daemon. -See the [OpenShell 0.0.72 compatibility review](../security/openshell-0.0.72-compatibility-review#source-of-truth-boundaries) for the accepted boundary and removal conditions. +See the [OpenShell gateway compatibility review](../security/openshell-0.0.72-compatibility-review#source-of-truth-boundaries) for the unchanged container boundary and removal conditions. Refer to [Environment Variables](commands#environment-variables) for the full list of port overrides. diff --git a/docs/security/best-practices.mdx b/docs/security/best-practices.mdx index 439acaf65f..8274edd713 100644 --- a/docs/security/best-practices.mdx +++ b/docs/security/best-practices.mdx @@ -656,7 +656,7 @@ NemoClaw binds the OpenShell gateway to loopback by default. |---|---| | Default | `NEMOCLAW_GATEWAY_BIND_ADDRESS=127.0.0.1`. | | What you can change | Keep Docker-driver gateways on loopback. Set `NEMOCLAW_DASHBOARD_BIND=0.0.0.0` during onboarding and on later `connect` calls for remote dashboard/API access. Recreate a local-only sandbox before changing it to a remote bind. | -| Risk if relaxed | Other hosts on the network may be able to reach the OpenShell gateway; Docker-driver gateways on OpenShell 0.0.72 reject wildcard gateway binds while gateway JWT auth is active. | +| Risk if relaxed | Other hosts on the network may be able to reach the OpenShell gateway; Docker-driver gateways on OpenShell 0.0.85 reject wildcard gateway binds while gateway JWT auth is active. | | Recommendation | Keep the gateway loopback default and expose only the dashboard forward when remote access is needed. | ### Gateway Compatibility Container @@ -668,9 +668,9 @@ On Linux hosts whose glibc is older than the OpenShell gateway binary requires, | Default | NemoClaw does not auto-enable the compatibility container on ABI mismatch. If `NEMOCLAW_OPENSHELL_GATEWAY_CONTAINER_PATCH=1` is set, the container keeps the main gateway listener on `127.0.0.1`, uses host networking so OpenShell computes the same Docker bridge callback addresses as a host-side gateway, mounts the Docker socket read-only, drops Linux capabilities, sets `no-new-privileges`, and publishes no extra Docker ports. | | What you can change | Opt in with `NEMOCLAW_OPENSHELL_GATEWAY_CONTAINER_PATCH=1`, keep the path disabled with `NEMOCLAW_OPENSHELL_GATEWAY_CONTAINER_PATCH=0`, or run on a host/OpenShell build combination where the gateway binary launches directly. | | Risk if relaxed | The Docker socket remains a privileged host API even when bind-mounted read-only. Treat this mode as equivalent to trusting the host user that can drive Docker, and do not enable it on untrusted shared hosts. | -| Recommendation | Prefer a host with glibc 2.28 or newer, which OpenShell 0.0.72 supports directly, and use the compatibility container only as an explicit local bridge on an older trusted host. | +| Recommendation | Prefer a host with glibc 2.28 or newer, which OpenShell 0.0.85 supports directly, and use the compatibility container only as an explicit local bridge on an older trusted host. | -See [OpenShell 0.0.72 Compatibility Review](./openshell-0.0.72-compatibility-review) for source-of-truth boundaries and contract coverage. +See [OpenShell Gateway Compatibility Review](./openshell-0.0.72-compatibility-review) for the unchanged compatibility-container source-of-truth boundaries. ### Insecure Auth Derivation diff --git a/docs/security/openshell-0.0.85-migration-review.md b/docs/security/openshell-0.0.85-migration-review.md new file mode 100644 index 0000000000..170ce1302f --- /dev/null +++ b/docs/security/openshell-0.0.85-migration-review.md @@ -0,0 +1,813 @@ + + + +# OpenShell 0.0.72 to 0.0.85 migration review + +## Status and decision + +OpenShell published stable tag `v0.0.85` at verified commit +[`3dee5570a46076a57a3b056f35f35ebc0861ac85`](https://github.com/NVIDIA/OpenShell/commit/3dee5570a46076a57a3b056f35f35ebc0861ac85). +This review binds NemoClaw's `0.0.85` selectors to that exact source and its +published artifacts. It is not blanket approval for every upstream platform or +driver: exact-head NemoClaw CI/advisor review, the supported-platform proofs, and +the physical DGX Spark credential-substitution proof below remain separate merge +and issue-closure gates. + +The source under review includes +[`40194f935ef6e29cb07500b9109314778ab6915c`](https://github.com/NVIDIA/OpenShell/commit/40194f935ef6e29cb07500b9109314778ab6915c), +which prevents a credential placeholder from leaving the proxy unresolved. That +change fails closed when the resolver or TLS-termination state is missing; it does +not prove that the affected DGX Spark host can initialize that state and complete a +real credential-bearing MCP call. NVIDIA/NemoClaw#6379 remains open until the +physical Docker 27 DGX Spark reproducer reports honest status and completes a real +MCP tool call with credential substitution. + +## Audit method and exact boundary + +The previous identity is OpenShell `v0.0.72` at +`8cb16de9eae4c44d7d31e1493747d8c10abb5963`. +The selected identity is the exact stable tag SHA above, not a local branch name +or the moving `dev` tag. +It descends from the published `v0.0.82` and `v0.0.83` releases through source +tag `v0.0.84`, whose release workflow failed before publication, and terminates +at the successfully published `v0.0.85` tag. + +The audit enumerated every stable adjacent tag, then read the release notes, +complete commit list, changed paths, source diffs, and upstream tests for each +range. Release notes were treated as leads rather than proof. The resulting boundary +contains 13 adjacent ranges, 67 commits, and 283 distinct changed paths in the +aggregate `v0.0.72..v0.0.85` comparison. + +The authenticated-MCP network review pins the resolve-validate-connect path to +`crates/openshell-supervisor-network/src/proxy.rs:2648-2674`, +`crates/openshell-supervisor-network/src/proxy.rs:2699-2739`, +`crates/openshell-supervisor-network/src/proxy.rs:2794-2803`, +`crates/openshell-supervisor-network/src/proxy.rs:1079-1081`, +`crates/openshell-supervisor-network/src/proxy.rs:4093-4100`, and +`crates/openshell-supervisor-network/src/proxy.rs:4340-4342` at the verified +stable commit. These citations bind the migration decision to the reviewed +address-resolution, validation, and connection paths. + +The ledger was produced with: + +```bash +collect-release-ledger.py \ + --repo \ + --from v0.0.72 \ + --to v0.0.85 \ + --required-fix 40194f935ef6e29cb07500b9109314778ab6915c +``` + +| Range | Commits | Changed paths | Diff size | +|---|---:|---:|---:| +| `v0.0.72 -> v0.0.73` | 5 | 27 | +1,530 / -531 | +| `v0.0.73 -> v0.0.74` | 6 | 25 | +328 / -163 | +| `v0.0.74 -> v0.0.75` | 2 | 26 | +3,416 / -5 | +| `v0.0.75 -> v0.0.76` | 3 | 28 | +2,267 / -201 | +| `v0.0.76 -> v0.0.77` | 3 | 7 | +452 / -27 | +| `v0.0.77 -> v0.0.78` | 6 | 23 | +198 / -115 | +| `v0.0.78 -> v0.0.79` | 1 | 1 | +1 / -1 | +| `v0.0.79 -> v0.0.80` | 5 | 15 | +1,373 / -96 | +| `v0.0.80 -> v0.0.81` | 4 | 9 | +617 / -24 | +| `v0.0.81 -> v0.0.82` | 12 | 76 | +7,851 / -983 | +| `v0.0.82 -> v0.0.83` | 8 | 20 | +1,322 / -163 | +| `v0.0.83 -> v0.0.84` | 7 | 110 | +19,151 / -857 | +| `v0.0.84 -> v0.0.85` | 5 | 29 | +776 / -160 | + +Release publication is a separate gate from source ancestry: + +- `v0.0.73` through `v0.0.80` have published GitHub releases. +- The `v0.0.79` release notes repeat the `v0.0.78` change list and add only the + `setup-uv` bump. The adjacent Git diff, not that cumulative note body, is the + source of truth for the `v0.0.78 -> v0.0.79` range. +- `v0.0.81` is a source tag at `420a855ddc21a20ac528f902bd2ed7f3fc133dc9`, + but it has no GitHub release. Release Tag run + [29101552146](https://github.com/NVIDIA/OpenShell/actions/runs/29101552146) + failed the Ubuntu 26.04 rootless-Podman E2E job and skipped publication. +- OpenShell `bb72d012` produced a successful Release Dev run + [29215426930](https://github.com/NVIDIA/OpenShell/actions/runs/29215426930), + but it does not expose one interchangeable version string. The released CLI, + gateway, and standalone sandbox binaries report `0.0.82-dev.11+gbb72d012`; + the pipeline Cargo version and supervisor image report + `0.0.82-dev.11+gbb72d0123`; and Python wheel filenames use + `0.0.82.dev11+gbb72d0123`. Development compatibility manifests must record + the observed CLI output separately from producer and component versions. A + moving prerelease was useful for compatibility work, but is not the stable + selector or final provenance record. +- Stable tag `v0.0.82` was published by successful `Release Tag` workflow run + [29260186856](https://github.com/NVIDIA/OpenShell/actions/runs/29260186856) + at `94cdd697`. The release is neither draft nor prerelease, and its CLI and + gateway report `0.0.82`. +- Stable tag `v0.0.83` was published from verified commit `e3d26dd3`. +- `v0.0.84` is a verified source tag at `e6f319c7`, but it has no GitHub + release. Release Tag run + [29424042428](https://github.com/NVIDIA/OpenShell/actions/runs/29424042428) + failed the Linux arm64 snap build and did not publish release assets. +- Stable tag `v0.0.85` was published by successful `Release Tag` workflow run + [29507522595](https://github.com/NVIDIA/OpenShell/actions/runs/29507522595) + at `3dee5570`. The release is neither draft nor prerelease, and its CLI, + gateway, sandbox, package, smoke, E2E, GHCR, and Helm producer jobs passed. + +### Rust dependency and notice delta + +The `v0.0.72..v0.0.82` `Cargo.lock` comparison is 48 additions and 25 removals. Its only +new third-party package identities are `capctl 0.2.4`, checksum +`4a6e71767585f51c2a33fed6d67147ec0343725fc3c03bf4b89fe67fede56aa5`, +and its `bitflags 1.3.2` dependency, checksum +`bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a`. +The exact production path is `bitflags 1.3.2 -> capctl 0.2.4 -> +openshell-supervisor-process -> openshell-sandbox`. `afc06dd2` uses +`capctl::caps::bounding::{clear, probe, clear_unknown}` for the new capability +bounding-set behavior, so this is security-boundary code rather than an +incidental build dependency. + +The retained crates.io `capctl` source identifies upstream Git commit +`6b89ddb3e79493a5e34bb681c00053d6122968bd`, is MIT-licensed, and has no build +script. Its implementation contains unsafe FFI around Linux `prctl`, `capget`, +`capset`, and extended-attribute operations. `bitflags 1.3.2` identifies upstream +Git commit `ed185cfb1c447c1b4bd6ac021c9ec3bb02c9e2f2` and is dual +MIT/Apache-2.0. A read-only RustSec advisory-database search on July 12, 2026 +found no advisory entry naming `capctl`; that absence is inventory evidence, not +proof that the unsafe boundary is vulnerability-free. + +OpenShell's `THIRD-PARTY-NOTICES` object is byte-identical at `v0.0.72` and +`v0.0.85` (`41eda0e0a83429e874544b507977c3a56f5a489f`). It does not name +`capctl` and still records only `bitflags 2.10.0`, while the stable lock has +`bitflags 1.3.2` and `2.11.1`. OpenShell has source-directory CycloneDX tooling, +but its license check is advisory and the release job neither generates nor +publishes that SBOM. The stable release does publish SLSA attestations for the +consumed archives, as recorded below, but not for the supervisor OCI image. The +lock-to-binary dependency inventory, notices/licenses, advisory results, archive +provenance, and image provenance must therefore remain separate claims; the +unchanged notice cannot be treated as evidence that the Rust dependency graph +stayed constant. + +The `v0.0.82..v0.0.85` lock delta adds the first-party +`openshell-gateway-interceptors` and `openshell-sdk` workspace crates plus +third-party `prost-reflect 0.16.5`, checksum +`01b80ea363c31af2de2b92e3c07ed1156628f7838c4afb4df75ee78a37fedbd1`. +Lowering the Python floor to 3.11 also adds `tomli 2.4.1` to `uv.lock`, with +source-distribution checksum +`7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f`. +The unchanged notices file names neither package, and the release still does +not publish a lock-derived SBOM for the consumed binaries. +NemoClaw consumes the published CLI, gateway, sandbox, and supervisor artifacts +rather than linking either new workspace crate, so their behavior is audited +below without adding a downstream package dependency. + +## Artifact baseline and provenance gap + +The previously shipped `0.0.72` supervisor index +`sha256:80ed9cda5bf672fefdb9dcd4604b40a8b09c0891b6eb9d03e10227c7e3dfb49d` +resolves to these exact platform identities: + +| Platform | Child manifest | Config | +|---|---|---| +| Linux amd64 | `sha256:e97174326ee25c896117e854c791945d0c458a26bc9d6eab004ccd6c19d86ee7` | `sha256:b34f500c495871bf92d8a04011a210167e95f3650927b3bd67dde3ddcc021ac2` | +| Linux arm64 | `sha256:0679e02da0bd480a3e2f119dc2d205269336c9c01d7d2c8f18d05400f89d160e` | `sha256:e53f2ac5b7b3667833271f62f053887d2be9f223d2699b7e39f88c78fd9df373` | + +Both child configs set `/openshell-sandbox` as the entrypoint but expose no OCI +source labels. A read-only registry audit on July 12, 2026 found that GHCR +returned no referrers index for the shipped manifest and that all 2,905 +supervisor tags contained no digest-derived signature, attestation, or SBOM tag +for that index. Therefore `0.0.72` has no verifiable source-to-image attestation; +matching release tags or timestamps cannot fill the gap. The immutable index +digest is the strongest enforceable runtime control for the baseline, while the +child manifest and config identities above are audit evidence rather than source +provenance. The stable `0.0.85` audit below found the same missing OCI +attestation and source labels and explicitly retains that provenance gap. +NemoClaw cannot manufacture a missing upstream attestation. + +NemoClaw now validates the shape of every consumed OpenShell archive before any +extraction: the asset must contain exactly one regular file with the expected +CLI, gateway, or sandbox binary name. Absolute paths, parent traversal, extra or +duplicate members, links, and devices fail closed. This structural validation is +independent of release SHA-256 verification and also constrains the explicitly +unverified development-channel path. + +### Stable v0.0.85 artifact evidence + +The successful `Release Tag` run above published three checksum manifests whose +release-asset SHA-256 values are independently trusted by NemoClaw's base-owned +verifier: + +| Manifest | SHA-256 | +|---|---| +| `openshell-checksums-sha256.txt` | `6554b3f96c04006d661519786d40d17e34c7860b7aac8fd35259ef2aea01567f` | +| `openshell-gateway-checksums-sha256.txt` | `cc4f32afed376ebe9b43cccdb4d2a77b2524b57132a6b56bb88d705e02420f86` | +| `openshell-sandbox-checksums-sha256.txt` | `b6ac353c933fa4cf9a3ef11d66cce6635f39ecc2e928d9c8ff1783ca797308b3` | + +Every archive consumed by the installer and Brev path matches both the release +manifest and GitHub's release-asset digest: + +| Consumed archive | SHA-256 | +|---|---| +| `openshell-x86_64-unknown-linux-musl.tar.gz` | `078fa086f506832c3d47d992e6109f26074bdd55916ce268e47c3971423459eb` | +| `openshell-aarch64-unknown-linux-musl.tar.gz` | `3cf353e7994d5835a233fe0641f9a860779190b054d0f90a04c897be782734b8` | +| `openshell-aarch64-apple-darwin.tar.gz` | `522c963f9515c7325b978e89022de76227ac245eefe1371292af1424434e2067` | +| `openshell-gateway-x86_64-unknown-linux-gnu.tar.gz` | `718cc9f942f88565cacb13c39717b128d6acc8d336212d42d26243f36ab19ece` | +| `openshell-gateway-aarch64-unknown-linux-gnu.tar.gz` | `09f2823f6e9c5f70f4482b200206eac455d789618da4ebe4acff042d794e7162` | +| `openshell-gateway-aarch64-apple-darwin.tar.gz` | `5de3e08ad1bdb0cdd01373999f537edca3d8aca22ae1c29bc9926969fe401e45` | +| `openshell-sandbox-x86_64-unknown-linux-gnu.tar.gz` | `94306f057d862cd5c34a0daa7692491733bc5ca528a7b92f9f62f717fb70a9be` | +| `openshell-sandbox-aarch64-unknown-linux-gnu.tar.gz` | `2c52b2971aecf125e41ed160d8d2f2addf04031906ca88f120ae3d436dd6b8f7` | + +GitHub's SLSA verifier accepts the attestations for those archives and binds +their exact digests to source commit `3dee5570`, the release workflow, and run +attempt. Archive-shape inspection found exactly the expected regular binary in +each tarball. Stable Linux amd64 E2E additionally binds the extracted CLI, +gateway, and standalone sandbox binaries to +`222d9d53a142691d7a7de2c692f38e52d24066f9f633d53746c5fef775861bc8`, +`33bb479d936c3c1b17dd475df05747be9de74564fb67d69a4c33cdd01181d02f`, and +`863ef21ab7ef623f5e7a8728c4e5532b46bfbae3ace3b800665a1c6353a1f7d2`, +respectively. The last digest is also the reviewed standalone sandbox identity +used when an older host loader cannot execute `--version`; the Linux arm64 +identity is +`680115dbc2affde0e88261ab09f4044726d1cc9e01de55dc5077d1118f52968d`. + +The stable supervisor image is pinned to immutable multi-architecture index +`sha256:f4226253a3525c3832adac5b38b419a0f27d1e915effe565b5885e20f93cd5e9`: + +| Platform | Child manifest | Config | +|---|---|---| +| Linux amd64 | `sha256:d559116a7b8129b227cbcc984e9625c94101af172a228dfe66a471a210ab5393` | `sha256:e92e84ea67371e5c2814dd3d51bf976887e4fa1ad712b7713c40c8c9bb4b549c` | +| Linux arm64 | `sha256:3d145f2144f78d34af80f98129cb2df8cbb8165389aec80a45092aec31cf8050` | `sha256:fa8c9d9f5f42cd1f7d64cc9b14926ee798e8fe40de32880197ece4cde3dc9096` | + +The stable OCI publication retains the candidate image's upstream provenance +limitation: the index has no OCI referrers, attestations, SBOM, or source labels. +NemoClaw therefore enforces the immutable index and platform identities above +and does not claim source-to-image provenance. This gap remains distinct from +the SLSA-bound release archives. + +### Candidate development artifact evidence + +The successful `bb72d012` Release Dev run provides bounded compatibility inputs, +not release approval. NemoClaw does not download or execute these expiring +artifacts as `0.0.85` release proof; the stable tag and its published artifacts +are the sole production authority. The exact retained Linux amd64 artifacts +inspected on July 12, 2026 are historical ancestry evidence: + +| Role | Actions artifact | Actions ZIP SHA-256 | Inner archive SHA-256 | Extracted binary SHA-256 | +|---|---|---|---|---| +| CLI | `8266446648` (`cli-linux-amd64`) | `78923b27a492204b6e869d9f5f392e57b37d8ddcb9367d746f4ee46cfaf0e5a2` | `d1732c0b87801560afd1b06cfea31c60d6a357100d5b817b4a4fb181b0b71933` | `09083ef8087e5191fc3513a7239b08041b511fdeb7f2fe074bdf8820886cbea1` | +| Gateway | `8266452366` (`gateway-binary-linux-amd64`) | `39504758f07a8bac0a52d958ec56e380ac59824bde8db72a815a9b82c6bbcfd6` | `5e3728564b1f965cb5d320bab4f37d388303723f42a64c308227dbc1ef382043` | `39e75f7a2a96c220e3f2d645067f0623d922385ade07edb2037a27cc07ea81d1` | +| Standalone sandbox | `8266435047` (`supervisor-binary-linux-amd64`) | `7b2e47adbbfc644806b465a4f4c3c7bfaba7117e1f19ec9f151b37695b418bf4` | `6f7040e89ec249df7f3b36ddff609a87f096fcdf62cd5c28e86757f175e40a7a` | `58e5d99261d2b8ea06664d020995830fd3f153ea692f36622b92f9b827ea60c8` | + +Each Actions ZIP contains exactly the expected archive, and each nested archive +contains exactly one regular mode-0755 root-owned binary with no links or extra +paths. The artifacts expire around July 18, 2026; their IDs are evidence only +while retained and must not become long-lived dependency selectors. + +The development supervisor image resolves to immutable index +`sha256:fc441051102b1a16ffcabf59878fa464d3c548f29bfbfa6e4acb232ab67198b7`: + +| Platform | Child manifest | Config | Binary layer | `/openshell-sandbox` SHA-256 | +|---|---|---|---|---| +| Linux amd64 | `sha256:4a54b434decd007d2a966edb5db751adb3ca4cf8ab8ac0b248901f8efe614b71` | `sha256:5432194fa43840c333bc7b166bf6e7c0e15247e9dc195cb9a38c1a85b7415f44` | `sha256:d1baeaebaddef6291e0a94b697f28c3c319ac2ec1a83843026e89553cc7cd27e` | `8e89067afca2d1c02a25fb19906dd27fd8d524ee4eb3b2b36b1210338dae9235` | +| Linux arm64 | `sha256:fab8d5c551991648a19bf7876d2edf19fdcf4e95139ce5f75d638354c0820d51` | `sha256:66a1d121d6386e19297d05a950ba7409c5752f337bacfbc156c7c76513e40136` | `sha256:818c727cb5cbcdb78918a274ca6b9aa85be6a95fdb604e49f523cf2c87f2eba4` | `8ec9b88c49f001d070ada7bb5a98fb6f96498fc446b0f2f614056247d7300b85` | + +Read-only package-database inspection of both development child manifests found +Alpine `3.22.5` and the same 29 installed APK records: + +```text +alpine-baselayout-3.7.0-r0 alpine-baselayout-data-3.7.0-r0 +alpine-keys-2.5-r0 alpine-release-3.22.5-r0 +apk-tools-2.14.10-r0 busybox-1.37.0-r20 +busybox-binsh-1.37.0-r20 ca-certificates-bundle-20260611-r0 +gmp-6.3.0-r3 iptables-1.8.11-r1 +iptables-legacy-1.8.11-r1 jansson-2.14.1-r0 +libapk2-2.14.10-r0 libcrypto3-3.5.7-r0 +libip4tc-1.8.11-r1 libip6tc-1.8.11-r1 +libmnl-1.0.5-r2 libncursesw-6.5_p20250503-r0 +libnftnl-1.2.9-r0 libssl3-3.5.7-r0 +libxtables-1.8.11-r1 musl-1.2.5-r12 +musl-utils-1.2.5-r12 ncurses-terminfo-base-6.5_p20250503-r0 +nftables-1.1.3-r0 readline-8.2.13-r1 +scanelf-1.3.8-r1 ssl_client-1.37.0-r20 +zlib-1.3.2-r0 +``` + +The installed package metadata spans MIT, Apache-2.0, GPL-2.0-only, +GPL-2.0-or-later, GPL-3.0-or-later, LGPL-2.1-or-later, +LGPL-3.0-or-later, MPL-2.0, BSD-2-Clause, X11, and Zlib expressions. The image +contains no path whose name matches `LICENSE`, `COPYING`, or `NOTICE`. On amd64, +the filesystem inventory has 152 mode-executable regular files, including 115 +xtables modules, plus 391 symbolic links and 304 BusyBox applets. Those counts +include executable-mode shared objects; they deliberately describe the whole +distributed filesystem rather than only the three explicitly installed package +names. + +The source Dockerfile uses mutable `alpine:3.22` and resolves unversioned +`nftables`, `iptables`, and `iptables-legacy` packages at build time. The build +wrapper passes `--provenance=false`. The development digest freezes the result +inspected above, but the Dockerfile alone cannot reproduce that result or bind +the resolved base/package identities to source. + +This expanded image is not all executable runtime surface for NemoClaw's Docker +driver. OpenShell pulls the image, creates a non-running extractor container, +downloads only `/openshell-sandbox`, caches that binary by image content ID, and +bind-mounts it into the actual sandbox image. The Alpine packages are therefore +downloaded supply-chain and local-daemon storage surface, but are not executed in +the standard NemoClaw Docker topology. OpenShell's Kubernetes sidecar and Podman +image-volume paths can execute or expose the Alpine filesystem; those drivers +remain outside the current NemoClaw integration. Both boundaries must be stated: +the Docker extraction behavior narrows runtime exposure, but it does not make an +unreproducible, unattributed distributed layer disappear. + +Those image binaries byte-match build artifacts `8266448422` (amd64) and +`8266451406` (arm64), respectively. The match binds registry content to retained +Actions output, but not cryptographically to source: GitHub returned no +attestation for the index or either child manifest, and the OCI configs expose no +source, revision, or version labels. Development proof must therefore record +`attestationStatus: absent`, preserve every digest above, and avoid claiming +source-to-image provenance. The stable release was audited anew in the preceding +section rather than inheriting these development identities. + +## Adjacent release findings + +### v0.0.72 to v0.0.73 + +Commits: `afc06dd2`, `a5161d0b`, `a2268060`, `f27ff150`, `474d2d4a`. + +- `afc06dd2` clears the full Linux capability bounding set for entrypoint, exec, + and connect children. Child launch fails when `CAP_SETPCAP` is unavailable and + the bounding set is nonempty; it succeeds without that capability only when the + runtime already supplied an empty bounding set. This directly intersects + NemoClaw's Docker Desktop, WSL, Colossus, cloud, and DGX capability workarounds. + The implementation also adds the `capctl 0.2.4 -> bitflags 1.3.2` dependency + chain audited above; runtime behavior and dependency provenance are separate + gates. +- `a5161d0b` moves selected-driver configuration acquisition into a normalized + server path. NemoClaw renders an authenticated Docker TOML containing the + selected driver, TLS, mTLS, JWT, supervisor image, and supervisor binary; the + final OpenShell binary must parse that exact rendered file and preserve both + listener paths across restart and legacy-gateway upgrade. +- `f27ff150` reserves credential names matching `v_`, introduces + revision-scoped child placeholders, retains eight resolver generations, and + falls an evicted revision back to the current credential only when that key + still exists. NemoClaw must reject the reserved namespace and prove rotation, + removal, restart, and rebuild behavior rather than assuming the old unversioned + placeholder contract. +- `a2268060` changes only upstream GPU E2E fixture execution and `474d2d4a` + changes contributor documentation. They add no consumed runtime contract. + +### v0.0.73 to v0.0.74 + +Commits: `ed0026aa`, `0a25fdf5`, `5477e2f2`, `914da339`, `450685c7`, `45614a3f`. + +- `0a25fdf5` removes the unused gateway `extra_bind_addresses` configuration. + NemoClaw does not emit that field, but must parse its final Docker TOML and prove + the intended loopback and Docker-bridge reachability instead of relying on that + absence alone. +- `450685c7` rejects leading/trailing whitespace in mount fields. NemoClaw does + not configure production driver mounts; the test-only EXDEV tmpfs mount is the + downstream consumer and remains a required no-impact regression. +- The Helm SAN, MCP documentation, Kubernetes combined-topology, and removed raw + `SandboxTemplate.volume_claim_templates` changes are not consumed by NemoClaw's + Docker gateway or CLI integration. NemoClaw has no raw OpenShell protobuf client + for the removed field. + +### v0.0.74 to v0.0.75 + +Commits: `abcd15d1`, `45060f44`. + +Envoy Gateway TLS termination and the Gator agent manifest are native OpenShell +Kubernetes/agent surfaces. NemoClaw neither deploys that Helm topology nor selects +the Gator manifest, so these are evidence-backed exclusions from the Docker +dependency migration. + +### v0.0.75 to v0.0.76 + +Commits: `43bb0302`, `5f9bf9ce`, `6461677c`. + +- `43bb0302` changes Docker and Podman bind mounts to support SELinux relabeling, + explicit source checks, and Docker's legacy bind representation. Production + NemoClaw supplies no driver mounts; the EXDEV fixture remains the direct test. +- `6461677c` adds numeric UID/GID policy identities and configurable Kubernetes + and VM identities. NemoClaw's supported gateway configuration selects only the + Docker driver; that driver does not inject the new UID/GID environment or + consume the VM fields, so the downstream Docker process remains the named + `sandbox` identity. This is a Docker-only exclusion, not a claim that the + upstream change is merely additive on every driver. +- The VM default remains its legacy hardcoded UID/GID `10001`, and prepared-image + cache identities include the rootfs layout, OpenShell version, and source-image + identity. Upgrading from `0.0.72` therefore misses the old cache. Within one + OpenShell version, however, the cache identity omits resolved `sandbox_uid` and + `sandbox_gid`; changing those settings can reuse a rootfs whose passwd/group + entries were baked for the old identity. Any future NemoClaw VM-driver support + must include those values in the key or purge and rebuild the cache. +- The rootless-Podman host E2E change does not alter NemoClaw's Docker runtime. + +### v0.0.76 to v0.0.77 + +Commits: `f852d07b`, `6252aa17`, `31807d68`. + +This range contains Hermes support documentation, an unimplemented driver-config +passthrough RFC, and a workflow action bump. It adds no shipped contract consumed +by NemoClaw. + +### v0.0.77 to v0.0.78 + +Commits: `5656240c`, `290297ff`, `9c14de7b`, `eba5dd75`, `abe42fb5`, `a7271169`. + +The Podman sandbox-JWT secret delivery fix is outside NemoClaw's Docker path. +The remaining changes are documentation or removal of deprecated `--keep` +references; NemoClaw does not invoke `--keep`. + +### v0.0.78 to v0.0.79 + +Commit: `f7aa3aa3`. + +Only `astral-sh/setup-uv` changed. There is no runtime or packaging selector +consumed by NemoClaw in this adjacent range, despite the cumulative `v0.0.79` +release-note body listing older changes. + +### v0.0.79 to v0.0.80 + +Commits: `2e2b497f`, `ed8ce820`, `5207f118`, `ff9af8e3`, `709aa0fe`. + +- `ff9af8e3` acknowledges the exact initially loaded sandbox policy revision and + reconciles an initial mismatch instead of leaving version zero/pending state. + `run_policy_status_reporter` consumes an unbounded FIFO independently of the + enforcement loop and retries a retryable head item forever, with backoff capped + at 32 seconds. Policy enforcement therefore continues, but an older unavailable + acknowledgement can head-of-line block every later status. The separate + construction-failure path makes five best-effort report attempts and then + preserves the startup error. Upstream unit coverage proves matching, mismatched, + global, version-zero, local-override, provider-composed, unbound, and 128-item + FIFO cases; its live test proves a sparse initial revision becomes loaded. It + does not isolate report-RPC outage/recovery or force initial policy construction + failure. NemoClaw must therefore prove the observable success path and retain + the two fault paths as explicit gates; `policy set --wait` alone is insufficient + evidence. +- The Podman import fix, Docker-version typo, man-page date, and setup action bump + do not change the consumed Docker runtime contract. + +### v0.0.80 to v0.0.81 + +Commits: `83131d7e`, `88710225`, `49701088`, `420a855d`. + +NemoClaw does not call `provider refresh configure +--secret-material-env`. Telemetry documentation and Packit target changes are +not consumed. `420a855d` adds upstream supervisor proxy-hostname regression tests +without changing product source. The failed stable publication is nevertheless a +hard artifact gate for this source tag. + +### v0.0.81 to v0.0.82 + +Commits: `5f38b7c4`, `ccdac9ce`, `caaa5165`, `8c0ecac8`, `233d207e`, +`10702133`, `bebf440b`, `8eacb477`, `614c8c16`, `40194f93`, `bb72d012`, +`94cdd697`. + +- `40194f93` closes two placeholder leak paths: missing resolver state now rejects + reserved credential markers, and missing TLS-termination state returns a + pre-200 CONNECT 503 rather than creating a raw tunnel. Upstream covers the 503 + with a loopback first-byte test and a full `handle_tcp_connection` test. Those + tests do not reproduce the affected Docker 27 DGX Spark resolver/CA startup or + complete a credential-bearing MCP lifecycle, so NemoClaw keeps its wire-level + status probe and still requires the physical #6379 tool-call proof. +- `bb72d012` permits newline and carriage-return bytes in exec command arguments + while retaining strict NUL and non-command-field validation. NemoClaw's active + public guard and internal base64 workarounds must be removed or reclassified, + and byte-exact LF, CRLF, quotes, and heredoc cases must pass without weakening + workdir or environment validation. +- `8eacb477` changes the combined Docker supervisor even though its title names + Kubernetes. The supervisor image changes from `scratch` plus one mode-0550 + binary to Alpine 3.22 with `nftables`, `iptables`, `iptables-legacy`, and a + mode-0555 binary. The exact development result is Alpine 3.22.5 with 29 APK + records, 152 mode-executable regular files, 391 symlinks, and no embedded + license/notice path. This adds an OS package, SBOM, vulnerability, license, and + executable surface that must be reviewed and bound to the final OCI digest. + NemoClaw's Docker driver extracts only the binary, but still downloads the + expanded, non-reproducibly resolved image. +- The same commit changes generic Docker namespace nft installation from an + atomic batch to sequential commands. Required failures can occur after the + policy-accept chain and accept rules exist but before all IPv4/IPv6 TCP/UDP + rejects exist; the outer Docker setup records that failure as nonfatal. The + exact required sequence is table creation, table flush, policy-accept output + chain, proxy accept, loopback accept, IPv4 TCP reject, IPv6 TCP reject, IPv4 UDP + reject, and IPv6 UDP reject; conntrack and log commands interspersed in that + sequence are optional. `install_bypass_rules` stops at a required failure, while + `create_netns_for_proxy` catches that error and returns the namespace as usable. + Upstream tests inspect generated arguments and required flags but do not execute + per-command failures. The final runtime proof must inspect the actual installed + rules and verify direct bypass remains unavailable through restart and teardown; + deterministic partial-failure injection remains an upstream testability gate. +- `10702133` makes each driver's default supervisor tag follow the gateway + version. NemoClaw supplies an explicit image and supervisor binary, so the + downstream invariant remains exact CLI/gateway/sandbox/component equality plus + an immutable multi-architecture image digest. +- Shared child-process construction now strips `OPENSHELL_TLS_CA`, + `OPENSHELL_TLS_CERT`, and `OPENSHELL_TLS_KEY` from entrypoint, exec, and connect + children. Those values remain supervisor identity material; NemoClaw tests and + comments must assert absence rather than describing child injection. +- Network binary identity now hashes the live `/proc//exe` target. The + cache remains keyed by the cleaned display path while its fingerprint and hash + come from the live process inode. Upstream proves that an already-running Bash + resolves to the old hash after unlink/replacement, but does not start the altered + replacement and require its network request to fail. The migration must prove + both halves in one live proxy session: the old process remains allowed and a new + altered process at the same display path receives an exact policy denial. +- `ccdac9ce` adds sanitized MCP tool names to policy logs without logging + arguments. Both the full JSON-RPC message and allowed MCP shorthand now include + `rule_methods=tools/call tools=`; `tool_names_for_log` reads only the + parsed call name and replaces control characters. Upstream allow/deny tests + assert that nested argument values are absent. This is an additive + observability/privacy change and a repository-wide consumer search found no + NemoClaw parser coupled to the old field order. +- Native Kubernetes sidecar/PVC/Helm changes, OpenShift documentation, and the TUI + warning destination are not consumed by NemoClaw's Docker integration. +- `94cdd697` updates only the verified `astral-sh/setup-uv` action reference. It + is the final tag commit and adds no runtime, package, or artifact-layout delta + beyond the already reviewed `bb72d012` source. + +The newline migration was audited beyond the public `exec` guard. Production +gateway RPC now sends its reviewed module source directly, and the live E2E +clients pass trusted shell, Python, Node, heredoc, and positional-argument bytes +directly through OpenShell. The sweep removed newline-only base64/eval/temp-file +transports from endpoint smoke, network policy, MCP, messaging, pairing, rebuild, +recovery, plugin, inference-switch, and Deep Agents checks. Focused tests pin LF, +CRLF, heredoc, and positional-argument preservation at the raw OpenShell argv +boundary. + +Remaining base64 use is independently classified: hostile or secret canaries are +kept inert; rendered file payloads and executable lifecycle fixtures are +deliberately materialized; gateway parameters remain data rather than code; and +oversized messaging-provider and scope-upgrade probes retain bounded chunks +because OpenShell still has a 32 KiB per-argument ceiling. The Hermes validator +wrapper remains the owned secret-boundary exception. None of those retained uses +exists merely to avoid a newline in an OpenShell command argument. + +The stable MCP lane now has a separate credential-generation-window target +instead of adding repeated mutations to each 45-minute agent case. It holds one +OpenClaw child open on its original revision, performs a sequence of nine +distinct rotation updates (one more than OpenShell's eight retained generations), +proves that the evicted placeholder resolves only through the current key, +removes that key while the provider remains attached and proves the old +placeholder fails closed, then separately detaches the provider and proves both +fresh-child absence and old-child fail-closed behavior. A second retained child +proves its revision works before the provider's `--credential-expires-at` +deadline and is denied after that deadline while a newer current revision remains +usable. The target then reattaches through `mcp restart`, rebuilds without the +host MCP secret, and removes the bridge. Every request is identified independently +of its credential, and the upstream ledger is required to contain no literal +resolve placeholder. +The stable workflow keeps the OpenClaw, Hermes, and Deep Agents MCP lifecycles +on three fresh-runner shards with separate artifacts. It runs the +`openshell-credential-generation-window` live target once, after the Deep Agents +case, rather than repeating that proof in every shard. +The workflow first ran this bounded target against the exact reviewed development +artifacts and scanned its artifacts for the whole generated-secret prefix. The +stable-source review retains the upstream +`expired_retained_generation_does_not_resolve` unit. The default stable job now +binds the target to tag commit `3dee5570`, the extracted release binaries, and the +immutable supervisor index; its final exact-head result remains a merge gate. + +### v0.0.82 to v0.0.83 + +Commits: `88f2656f`, `0fe24a4c`, `9ad53b3f`, `4e1ffef8`, `fcc9db30`, +`ee9b4551`, `df062867`, `e3d26dd3`. + +- `4e1ffef8` stages certificate-generation temporary files inside the destination + filesystem before rename. + This removes a cross-device failure without changing NemoClaw's certificate + paths or persisted gateway configuration. +- `fcc9db30` replaces stringly typed JSON-RPC inspection failures with typed + internal errors while preserving the policy-visible rejection text. + NemoClaw does not parse the new Rust type and retains its denial/log privacy + runtime proof. +- `e3d26dd3` makes the pending-to-rejected mechanistic policy transition atomic + and prevents a resubmitted endpoint from changing an approved ledger row to + rejected. + It adds no database schema migration and remains compatible with existing + persisted policy records. +- `df062867` lowers the Python package floor from 3.12 to 3.11. + NemoClaw's supported Python floor already includes 3.11, so no downstream + configuration override changes. +- The TUI, agent-skill, and review-bot commits do not affect the CLI, gateway, + sandbox, release archives, or supervisor runtime consumed by NemoClaw. + +### v0.0.83 to v0.0.84 + +Commits: `97e10513`, `a41cd125`, `96fd31fc`, `e8c16eb1`, `994750e3`, +`83003e80`, `e6f319c7`. + +- `96fd31fc` and `83003e80` add gateway-interceptor policy fields, a middleware + service, and a reference implementation. + Existing protobuf and policy inputs decode with an empty interceptor set, and + NemoClaw does not configure the new field. + The default path therefore remains the reviewed network-policy and credential + relay path. +- `e6f319c7` adds the `openshell-sdk` crate. + NemoClaw invokes published CLI, gateway, and sandbox binaries and imports no + OpenShell SDK package, so the new SDK is not a downstream dependency. +- `994750e3` changes snap packaging by vendoring SSH. + NemoClaw consumes release tarballs and the supervisor OCI image, not the snap. +- `e8c16eb1` changes only the development-release Helm action. + The formatting, documentation, and RFC-only commits do not alter a consumed + runtime contract. +- `v0.0.84` is source ancestry, not a shippable dependency identity, because its + arm64 snap producer failed and GitHub published no release. + +### v0.0.84 to v0.0.85 + +Commits: `80293213`, `392ad639`, `b4be33e5`, `21aaa895`, `3dee5570`. + +- `80293213` initializes sandbox annotations in the new SDK client. + NemoClaw does not use that client, and the change is additive for existing + protobuf callers. +- `392ad639` makes the sandbox supervisor guest PID 1 in the VM driver. + Docker-driver behavior and the extracted supervisor image contract are + unchanged. + VM-driver lifecycle and recovery remain part of the supported-platform runtime + proof rather than being inferred from Docker evidence. +- `21aaa895` adds an admin-authorized gateway-info RPC and additive JSON fields + for gateway version, compute drivers, and remote registration metadata. + NemoClaw reads the retained `Gateway:` identity line and tolerates additional + fields; it does not treat the new RPC as artifact or runtime attestation. +- `b4be33e5` adds merge-queue CI and `3dee5570` prunes development snap assets. + Neither changes stable runtime behavior. +- The child-visible credential sources + `crates/openshell-core/src/google_cloud.rs`, + `crates/openshell-core/src/provider_credentials.rs`, and + `crates/openshell-core/src/secrets.rs` are byte-identical between `v0.0.82` + and `v0.0.85`. + The regenerated `v0.0.85` manifest therefore preserves the reviewed key + classes while binding them to the selected tag. +- Provider mutation still exposes no caller-supplied expected resource version, + gateway metadata still exposes no attested Docker-driver target address, and + the forward lifecycle still lacks a downstream-owned atomic stop/reap API. + The corresponding NemoClaw fail-closed checks remain necessary. + +## Downstream concern ledger + +| ID | Severity | Downstream consumer and failure mode | Required disposition | Current state | +|---|---|---|---|---| +| `OS85-01` | Critical | All stable selectors, archives, checksums, binaries, and the supervisor image could identify different builds. | Pin one published tag; verify producer run, signatures/attestations, release hashes, extracted binaries, component versions, OCI index and child manifests; reject archive traversal, links, devices, duplicates, or unexpected members. | Closed for dependency selection: stable tag `3dee5570`, producer run 29507522595, three manifest digests, eight consumed archive digests and SLSA attestations, extracted Linux binary identities, and the immutable multiarch supervisor index are recorded and enforced. | +| `OS85-02` | Critical | `mcp status` can be honest while the affected Spark still cannot initialize resolver/CA state or perform a credential-bearing request. | Physical Docker 27 DGX Spark: register credential, require status success, load tools, complete a real MCP tool call, and prove the literal placeholder never reaches upstream. | Blocked on assigned hardware proof. | +| `OS85-03` | High | `src/lib/actions/sandbox/exec.ts`, command dispatch, docs, and internal wrappers encode the old newline rejection. | Remove the obsolete public rejection and newline-only wrappers; prove byte-exact LF, CR, CRLF, quotes, and heredoc argv; retain NUL plus multiline workdir/environment rejection. | Source and internal-wrapper migration complete; stable exact-head runtime proof remains a merge gate. | +| `OS85-04` | High | OpenShell child launch now clears the complete capability bounding set. Hosts without `CAP_SETPCAP` may fail if their runtime does not pre-clear it. | Prove entrypoint, exec, and connect launch with `CapBnd=0` on Linux Docker, DGX Spark arm64, macOS Docker Desktop/Colima, WSL, and Colossus; update NemoClaw's #3280 caveat only from runtime evidence. | The stable release proof inspects the actual entrypoint, exec, and forced-TTY connect children for full `CapBnd=0`; exact-head Linux execution and every other platform remain open. | +| `OS85-05` | High | Versioned credential placeholders and the eight-generation window change long-running MCP behavior. | Regenerate the exact-version child-visible manifest; reject reserved `v_` names; test more than eight rotations, removed keys, detach, restart/rebuild, fresh exec revision, expiry, and literal-placeholder scans. | The exact `v0.0.85` child-visible manifest is the production authority and the generation-window proof is workflow-mandatory; stable exact-head live execution remains a merge gate. | +| `OS85-06` | High | Initial policy acknowledgement and ordered retry can make the active gateway status lag enforcement. | Test initial LOADED/FAILED, hot update, retry outage/recovery, restart, exact version/hash re-read, and ordered drain. | The stable release proof covers hot-update LOADED identity plus restart initial acknowledgement and exact version/hash recovery. Initial FAILED and isolated report outage/ordered drain remain an open runtime gate. | +| `OS85-07` | High | Sequential nft setup can leave an incomplete policy-accept ruleset after a required command fails; Docker setup treats the error as nonfatal. | Inject each required failure; inspect IPv4/IPv6 TCP/UDP rules and direct-bypass negatives on Linux x86 and Spark arm64; verify restart and teardown. | The stable release proof inspects the live policy-accept chain and all four required rejects before/after restart, and probes controlled IPv4 TCP/UDP listeners. Required-command fault injection, routed IPv6 behavior, Spark arm64, and physical teardown remain an open security gate. | +| `OS85-08` | High | The supervisor image moves from one scratch binary to a 29-package Alpine filesystem resolved from a mutable base and unpinned APK names. NemoClaw's Docker path downloads it but executes only the extracted binary. | Retain exact per-arch package/version/license and file inventories; scan vulnerabilities; verify modes, multiarch manifests, base/package identities, source labels, OCI provenance, and extraction-only behavior; preserve an explicit digest. | The final stable index, child manifests, configs, and candidate-equivalent package/file inventory are recorded. The upstream image still has no OCI attestation, SBOM, source labels, or reproducible base/package inputs; NemoClaw enforces the immutable index and explicitly retains that provenance limitation. | +| `OS85-09` | Medium-high | Normalized selected-driver config can change the effective Docker gateway even when the TOML text is unchanged. | Parse the final rendered TOML with the final binary; prove loopback/bridge listeners, JWT/mTLS, restart, persisted state, and legacy gateway upgrade. | The stable release proof binds the actual rendered Docker TOML to the running release gateway, loopback and Linux bridge listeners, mTLS/JWT mounts and relay access, host gateway restart, persisted sandbox state, and rebuild. The exact-head result, legacy-gateway upgrade, and non-Linux/host-gateway platforms remain open. | +| `OS85-10` | Medium-high | Supervisor TLS identity variables are no longer child environment. Stale tests/comments can normalize a credential leak. | Assert absence from entrypoint, exec, and connect children and update the source-of-truth rationale. | Hermes and Deep Agents now reject all three variables; the stable entrypoint, exec, and connect probes require their absence, with exact-head execution pending. | +| `OS85-11` | Medium-high | Live `/proc//exe` identity changes replacement-time policy behavior. | Prove old process survives replacement and a new altered process at the same path is denied. | The stable release proof runs both processes against the real proxy and requires old=200 before/after replacement, distinct live/path hashes, and new=403; exact-head runtime result pending. | +| `OS85-12` | Medium | OpenShell declares Docker 28.0+ while #6379 is on Docker 27 and NemoClaw marks DGX Spark tested. | Either validate and document a precise downstream exception from physical proof or raise the supported floor and preflight it. | Open product/platform decision. | +| `OS85-13` | Low | Mount parsing/SELinux changes could affect the test-only tmpfs path. | Rerun the EXDEV tmpfs fixture and retain production no-mount evidence. | The stable release proof injects only the reviewed tmpfs config, requires Docker's structured tmpfs representation plus `noexec`/01777 at runtime, retains it across gateway restart, and requires a fresh remount after rebuild. The wrapper is disabled outside the explicit proof lane and production still supplies no driver mounts. Exact-head, Podman, and enforcing-SELinux results remain open. | +| `OS85-14` | Low | Sanitized MCP tool names are newly present in logs. | Record the additive observability/privacy behavior; ensure no downstream parser assumes the old shape. | The stable release check requires the real `fake_echo` tool name and rejects argument/result canaries or an `arguments` field in JSON-RPC policy logs; exact-head runtime result pending. | +| `OS85-15` | High | The installer-hash workflow executes its checker and parser from the PR base SHA. One PR cannot safely teach that trusted base about a new release and consume the release; using the head checker would let reviewed code define its own trust rules. | First land archive safety, normalized full-script template validation, and multi-release trust while selectors remain `0.0.72`; prove the old base rejects a new release and the new base permits only structured release-data changes; then land the exact `0.0.85` manifest identities before refreshing this selector PR. | Base trust landed in #7069. #6778 and #6779 established base-owned structured manifest and sandbox-map validation; #7069 added only the three exact `0.0.85` release identities while retaining `0.0.72` and `0.0.82`. This selector PR must be based on that trusted state and pass the checker without relying on its head copy. | +| `OS85-16` | High | Capability clearing now depends on `capctl 0.2.4` and `bitflags 1.3.2`, but upstream notices are unchanged and the consumed binaries have no published SBOM or attestation covering this dependency graph. | Bind crate checksums and source identities to the stable lock and binaries; review the unsafe syscall boundary and advisories; update notices/licenses; retain a generated SBOM and provenance for every consumed binary. | The stable lock, crate checksums, source identities, licenses, unsafe boundary, current RustSec absence, and SLSA-bound archives are recorded. Upstream still publishes no binary SBOM and its unchanged notices omit the new graph; that limitation remains explicit rather than being presented as complete attribution. | +| `OS85-17` | Medium | The VM driver bakes configurable UID/GID into prepared rootfs state, but its same-version cache key omits both values and can reuse stale passwd/group entries after configuration drift. | Keep NemoClaw's selected driver Docker-only. Before any VM path is supported, key prepared images by UID/GID or purge them and prove identity/ownership after change and restart. | Source-reviewed exclusion for the current Docker topology; VM configuration-churn compatibility is unproven. | + +An unresolved critical or high concern blocks the version selector change. A green +aggregate test suite does not override an open ledger row. + +## Stable release policy, nft, identity, and log proof boundary + +The stable MCP job invokes the historically named +`openshell-exact-main-runtime-contracts.ts` only when +`NEMOCLAW_OPENSHELL_EXACT_MAIN_PROOF=1`. The proof runs before MCP registration, +uses a temporary base-policy extension, and restores the exact captured base +policy before the managed MCP lifecycle continues. It performs these concrete +checks against the reviewed CLI, gateway, supervisor image, and sandbox binary: + +1. Apply a hot policy revision with `--wait`, then independently require the + effective and stored-revision JSON to agree on sandbox, version, active + version, hash, and LOADED/effective state. +2. Read `nft -j` inside the real `sandbox-*` network namespace. Require the + dangerous policy-accept output chain, proxy and loopback accepts before the + rejects, and exactly one IPv4/IPv6 TCP/UDP port-unreachable reject each. +3. Restart the actual OpenShell-managed Docker container. Require the same exact + policy revision/hash, the initial-revision acknowledgement log for that + version, and the complete nft ruleset again. +4. Bind controlled TCP and UDP echo listeners to the supervisor-side + `10.200.0.1` veth, then require workload direct connections to fail quickly + with `ECONNREFUSED`. This distinguishes nft rejection from an unreachable or + unbound external address. +5. Allow a copied Bash at one exact path to open the real proxy, replace the + path with a different executable inode, and require the old process to remain + allowed before and after replacement while the new altered process at the + same path receives HTTP 403. The evidence records different + `/proc//exe` and replacement-path hashes. +6. After the authenticated real MCP call, read sandbox policy logs and require + `decision=allow rule_methods=tools/call tools=fake_echo`. Argument and result + canaries, plus any `arguments` field, are forbidden from those JSON-RPC lines. + +This boundary deliberately does not fake fault injection. OpenShell exposes no +hook that fails only `report_policy_status` while allowing policy polling and +mutation to continue; stopping the gateway would conflate the reporting outage +with loss of the source being polled. It likewise exposes no downstream control +between individual required nft commands. Editing the installed table after +startup would prove that NemoClaw can damage nft state, not that OpenShell handles +a real command failure. The smallest honest missing proofs are upstream-injected +report transport/construction failures and an nft executor that can fail each +required command, followed by the physical Spark run. The Docker namespace has no +routed non-loopback IPv6 address, so the candidate asserts the installed IPv6 +rejects structurally; routed IPv6 bypass behavior belongs in a platform fixture +that actually configures IPv6. No OpenShell repository mutation is part of this +NemoClaw work. + +## Stable release selected-driver and mount proof boundary + +The same stable MCP job prepares a second bounded proof only +when `NEMOCLAW_OPENSHELL_EXACT_MAIN_PROOF=1`. A PATH wrapper delegates every +operation to the hash-pinned release CLI and changes only `openshell sandbox +create`: it adds one reviewed `--driver-config-json` value containing a tmpfs at +`/tmp/nemoclaw-exact-main-driver-config`. Duplicate driver config is rejected. +The helper is inactive outside that explicit lane, and NemoClaw's production +onboard path still supplies no driver mounts. + +The proof does not treat successful onboarding as evidence by itself. It: + +1. Parses the actual mode-0600 + `~/.local/state/nemoclaw/openshell-docker-gateway/openshell-gateway.toml` + with a TOML parser. It requires `compute_drivers = ["docker"]`, no unselected + driver table, the exact loopback endpoint and Docker network, the reviewed + supervisor image, the stable sandbox binary, TLS with client auth, + mTLS auth, sandbox JWT configuration, and unauthenticated users disabled. +2. Resolves `/proc//exe` and requires its SHA-256, plus the CLI and + standalone sandbox SHA-256 values, to match the exact-main provenance + record. Successful startup therefore proves that the final release gateway + parsed the file; an aggregate install test is not substituted. +3. Reads the release gateway's actual sockets with `ss`. It requires both + `127.0.0.1:` and the selected Docker network's IPv4 bridge + `:`, rejects a wildcard listener owned by that process, + requires stable CLI sandbox listing over host mTLS, and requires a real + sandbox exec through the supervisor relay. The container must mount its + sandbox JWT and all three client-mTLS files read-only. +4. Inspects the running Docker container. The test mount must be one structured + `Type=tmpfs` mount and must not appear in `HostConfig.Binds`, which is the + representation changed for SELinux-labelled bind mounts. Inside the sandbox, + `/proc/mounts` must report `tmpfs,noexec`, mode 01777, and a writable marker. +5. Stops and recovers the actual host OpenShell gateway through NemoClaw. The + gateway PID must change, the rendered-config digest and sandbox container ID + must not, the release binary/listeners/auth path must still match, and both + the tmpfs marker and a Deep Agents durable-state marker must remain. +6. Runs the existing managed MCP rebuild with the same test-only wrapper. A new + Docker container is required, the tmpfs must be mounted again with the same + representation/options but without its old volatile marker, and the backed-up + Deep Agents state marker must be restored. This distinguishes a fresh tmpfs + mount from an accidentally retained container. + +The proof is intentionally Linux amd64 Docker-bridge evidence. It does not +isolate Docker Desktop/Colima, WSL, DGX Spark's Docker 27 host-gateway route, +Podman, IPv6 bridge routing, or an enforcing-SELinux host. The reviewed upstream +SELinux change applies to bind mounts; this fixture neither enables bind mounts +nor requests `selinux_label`, so it proves that the consumed tmpfs path remains +on the unaffected structured-mount branch, not that SELinux relabelling works. +Those platform claims need their own real hosts. + +Legacy upgrade is also separate. This stable release lane starts with a fresh +gateway/config/database so every observed process can be tied to the release +provenance. The existing stable gateway-upgrade test starts an old gateway, +but cannot be cited as fresh-release identity evidence. An honest legacy proof +must seed a supported old gateway and database, replace all three components with +the final release artifacts, then repeat the listener/auth/state checks above. +Mixing that old binary into this stable identity lane would invalidate the +claim it is designed to make. + +## Test-selection and false-green audit + +The moving-development MCP workflow currently classifies an OpenShell version +different from the versioned child-visible credential manifest as an expected +compatibility rejection, records the classification as passed, and does not run +the full managed MCP lifecycle. That is correct fail-closed behavior for an +unreviewed development runtime, but it is not evidence that the candidate is +compatible. + +Before any `0.0.85` selector can be called green, the credential manifest and all +of its imports/image copies must identify the stable tag, the default stable job +must run without compatibility branching, and all three agents must complete +registration, credential rotation, DNS-rebinding denial, policy denial, real tool +invocation, +restart/rebuild, and cleanup without a conditional skip or expected failure. + +## Final acceptance gates + +1. The structured manifest and sandbox-build-map prerequisites landed as #6778 + and #6779 while runtime selectors remained `0.0.72`; their base-owned parser + rejects operational installer drift and permits only validated release-data + and selector changes. Exact `0.0.85` manifest trust landed separately as + #7069. This selector PR must be based on `main` containing that trust and + pass the base-owned checker. +2. Stable tag `v0.0.85` contains the required fail-closed credential fix and all + 13 adjacent ranges above, including the unpublished `v0.0.84` source boundary. +3. The tag has successful release publication run 29507522595. Every consumed + archive is bound to that producer/source through exact release digests and + SLSA attestations, and the OCI index/children are immutable. Missing upstream + image attestation/SBOM/source labels and incomplete notices remain recorded + provenance and attribution limitations rather than false-green evidence. +4. Blueprint bounds, installer tables, Brev defaults, workflow pins, feature-gate + hashes, supervisor digest, credential manifest, tests, and active docs select + one coherent version. +5. Every concern-specific unit/integration proof above passes, followed by normal + repository checks and exact-head CI/advisor review. +6. #7055 landed the controller-compatible legacy gateway evidence matrix. The + exact-head, non-skipped live matrix must still pass on Linux x86 Docker, + macOS Docker Desktop/Colima, WSL, Colossus, and physical DGX Spark arm64; + legacy gateway upgrade, restart, rollback, and teardown remain explicit + phases. +7. The physical #6379 Spark run completes an authenticated real MCP tool call and + reports any failure honestly. Inclusion of `40194f93` alone cannot close the + issue. diff --git a/nemoclaw-blueprint/blueprint.yaml b/nemoclaw-blueprint/blueprint.yaml index 87b5c3ecfd..ea06893b29 100644 --- a/nemoclaw-blueprint/blueprint.yaml +++ b/nemoclaw-blueprint/blueprint.yaml @@ -3,8 +3,8 @@ version: "0.1.0" # Requires OpenShell MCP/JSON-RPC L7 policy support from NVIDIA/OpenShell#1865. -min_openshell_version: "0.0.72" -max_openshell_version: "0.0.72" +min_openshell_version: "0.0.85" +max_openshell_version: "0.0.85" min_openclaw_version: "2026.3.11" # Mirrors the components.sandbox.image manifest digest below. Lets a # downstream consumer (or release tooling) verify the blueprint declares diff --git a/nemoclaw/src/blueprint/runner.ts b/nemoclaw/src/blueprint/runner.ts index ba7b488411..58c85ca775 100644 --- a/nemoclaw/src/blueprint/runner.ts +++ b/nemoclaw/src/blueprint/runner.ts @@ -358,7 +358,7 @@ function mergePolicyAdditions(currentPolicyRaw: string, additions: PolicyAdditio const existingNetworkPolicies = current.network_policies ?? {}; const output: UnknownRecord = {}; - // Stable OpenShell 0.0.72 exposes composable top-level policy sections as + // OpenShell 0.0.72 and later expose composable top-level policy sections as // mappings. Preserve unknown mapping sections for forward compatibility, but // fail closed on a scalar or sequence until its mutation semantics are // reviewed for the next supported OpenShell contract. diff --git a/nemoclaw/src/shared/openshell-policy-boundary.cts b/nemoclaw/src/shared/openshell-policy-boundary.cts index 1fabc0f4fc..5f1c0c107f 100644 --- a/nemoclaw/src/shared/openshell-policy-boundary.cts +++ b/nemoclaw/src/shared/openshell-policy-boundary.cts @@ -100,7 +100,7 @@ export function parseOpenShellPolicy(raw: string): ParsedOpenShellPolicy { // regressionTest: the root policy round-trip and plugin runner policy tests. // removalCondition: OpenShell's supported base-policy contract guarantees that // provider-composed entries are absent from every mutation read. -// tracking: revalidate this guard at every stable OpenShell pin after 0.0.72. +// tracking: revalidated for stable OpenShell 0.0.85; revalidate after 0.0.85. export function withoutProviderComposedPolicies(policies: Record): Record { return Object.fromEntries( Object.entries(policies).filter(([name]) => !name.startsWith("_provider_")), diff --git a/scripts/brev-launchable-ci-cpu.sh b/scripts/brev-launchable-ci-cpu.sh index ef089135e7..6a1b65a57a 100755 --- a/scripts/brev-launchable-ci-cpu.sh +++ b/scripts/brev-launchable-ci-cpu.sh @@ -73,7 +73,7 @@ assert_openshell_version() { if [ -z "$OPENSHELL_VERSION" ]; then case "${NEMOCLAW_OPENSHELL_CHANNEL:-stable}" in dev) OPENSHELL_VERSION="dev" ;; - stable | auto) OPENSHELL_VERSION="v0.0.72" ;; + stable | auto) OPENSHELL_VERSION="v0.0.85" ;; *) fail "NEMOCLAW_OPENSHELL_CHANNEL must be one of: stable, dev, auto" ;; esac fi @@ -148,11 +148,11 @@ openshell_cli_asset_for_arch() { openshell_cli_pinned_sha256() { local release_tag="$1" asset="$2" case "${release_tag}:${asset}" in - v0.0.72:openshell-x86_64-unknown-linux-musl.tar.gz) - printf '%s\n' "37836c3b50383e03249c5e16512c1806e591fba8451408a84fb2f628ddb318c4" + v0.0.85:openshell-x86_64-unknown-linux-musl.tar.gz) + printf '%s\n' "078fa086f506832c3d47d992e6109f26074bdd55916ce268e47c3971423459eb" ;; - v0.0.72:openshell-aarch64-unknown-linux-musl.tar.gz) - printf '%s\n' "a5ff01a3240d73c72ec1700eda6cc6c752a86cf50c5dd1b5bdc459f544d03045" + v0.0.85:openshell-aarch64-unknown-linux-musl.tar.gz) + printf '%s\n' "3cf353e7994d5835a233fe0641f9a860779190b054d0f90a04c897be782734b8" ;; *) return 1 diff --git a/scripts/checks/extract-installer-pins.mts b/scripts/checks/extract-installer-pins.mts index 2e897b4bca..bddc0163d3 100644 --- a/scripts/checks/extract-installer-pins.mts +++ b/scripts/checks/extract-installer-pins.mts @@ -174,7 +174,7 @@ function assertExactAssetSet( // whyNotSourceFix: OpenShell can attest its release but cannot keep NemoClaw's // blueprint, installer selector, Brev selector, and embedded tables coherent. // regressionTest: test/installer-hash-check.test.ts moves all runtime consumers -// to 0.0.82 while leaving both valid pin tables at 0.0.72 and requires failure. +// to 0.0.85 while leaving both valid pin tables at 0.0.72 and requires failure. // removalCondition: remove these comparisons only when one base-trusted, // machine-readable pin manifest directly drives every runtime consumer. function extractSingleVersion( diff --git a/scripts/install-openshell.sh b/scripts/install-openshell.sh index 0c0bf559eb..3e7c6d5f61 100755 --- a/scripts/install-openshell.sh +++ b/scripts/install-openshell.sh @@ -37,16 +37,16 @@ info "Detected $OS_LABEL ($ARCH_LABEL)" # round-trippable base policies: WebSocket text frames, provider-shaped # aliases, REST request bodies, MCP/JSON-RPC L7 enforcement, and # `policy get --base` for MCP/JSON-RPC-safe read-modify-write operations. -MIN_VERSION="0.0.72" +MIN_VERSION="0.0.85" # Maximum version validated for this NemoClaw release. Newer OpenShell builds # may change sandbox semantics; upgrade NemoClaw before upgrading past this. -MAX_VERSION="0.0.72" +MAX_VERSION="0.0.85" # Pin fresh installs to this version. The TS installer normally overrides this # via NEMOCLAW_OPENSHELL_PIN_VERSION after resolving the highest published # OpenShell release that satisfies the blueprint's max_openshell_version # (see #3404). The hardcoded value is the fallback for offline runs. PIN_VERSION="$MAX_VERSION" -DEV_MIN_VERSION="0.0.72" +DEV_MIN_VERSION="0.0.85" CHANNEL="${NEMOCLAW_OPENSHELL_CHANNEL:-auto}" case "$CHANNEL" in @@ -140,29 +140,29 @@ fi openshell_pinned_sha256() { local release_tag="$1" asset="$2" case "${release_tag}:${asset}" in - v0.0.72:openshell-x86_64-unknown-linux-musl.tar.gz) - printf '%s\n' "37836c3b50383e03249c5e16512c1806e591fba8451408a84fb2f628ddb318c4" + v0.0.85:openshell-x86_64-unknown-linux-musl.tar.gz) + printf '%s\n' "078fa086f506832c3d47d992e6109f26074bdd55916ce268e47c3971423459eb" ;; - v0.0.72:openshell-aarch64-unknown-linux-musl.tar.gz) - printf '%s\n' "a5ff01a3240d73c72ec1700eda6cc6c752a86cf50c5dd1b5bdc459f544d03045" + v0.0.85:openshell-aarch64-unknown-linux-musl.tar.gz) + printf '%s\n' "3cf353e7994d5835a233fe0641f9a860779190b054d0f90a04c897be782734b8" ;; - v0.0.72:openshell-aarch64-apple-darwin.tar.gz) - printf '%s\n' "117b5354cc42d80bc4d5e070ea5ac4e341208ff6d3c29b516d8a9c80e2310f8d" + v0.0.85:openshell-aarch64-apple-darwin.tar.gz) + printf '%s\n' "522c963f9515c7325b978e89022de76227ac245eefe1371292af1424434e2067" ;; - v0.0.72:openshell-gateway-x86_64-unknown-linux-gnu.tar.gz) - printf '%s\n' "03225fb9388b682af1a5f1614b26b75f828da6031e3ffc1fd920b6fbe5f70877" + v0.0.85:openshell-gateway-x86_64-unknown-linux-gnu.tar.gz) + printf '%s\n' "718cc9f942f88565cacb13c39717b128d6acc8d336212d42d26243f36ab19ece" ;; - v0.0.72:openshell-gateway-aarch64-unknown-linux-gnu.tar.gz) - printf '%s\n' "a97dcb3acb04fb2d1170c1a2170228990c2337e25bb8c18817e5a6e952204108" + v0.0.85:openshell-gateway-aarch64-unknown-linux-gnu.tar.gz) + printf '%s\n' "09f2823f6e9c5f70f4482b200206eac455d789618da4ebe4acff042d794e7162" ;; - v0.0.72:openshell-gateway-aarch64-apple-darwin.tar.gz) - printf '%s\n' "8c07362107393eb5f4ae4b9ee9f4257fd53862c51ad8dd96f2fe31bb6d8d7ffb" + v0.0.85:openshell-gateway-aarch64-apple-darwin.tar.gz) + printf '%s\n' "5de3e08ad1bdb0cdd01373999f537edca3d8aca22ae1c29bc9926969fe401e45" ;; - v0.0.72:openshell-sandbox-x86_64-unknown-linux-gnu.tar.gz) - printf '%s\n' "811f914b6a6a3a3f4533449ddebebb6422333861a27a5fa848db6cbfdffdd230" + v0.0.85:openshell-sandbox-x86_64-unknown-linux-gnu.tar.gz) + printf '%s\n' "94306f057d862cd5c34a0daa7692491733bc5ca528a7b92f9f62f717fb70a9be" ;; - v0.0.72:openshell-sandbox-aarch64-unknown-linux-gnu.tar.gz) - printf '%s\n' "2cf62cbd651e55d0f8750804e2b4025e0d6c8eea4564c87cda47a2c922941db0" + v0.0.85:openshell-sandbox-aarch64-unknown-linux-gnu.tar.gz) + printf '%s\n' "2c52b2971aecf125e41ed160d8d2f2addf04031906ca88f120ae3d436dd6b8f7" ;; *) return 1 @@ -280,6 +280,16 @@ pinned_sandbox_build_version() { 32ca44fe7d9e6d332f2a753c6b8a1a6117b7388281dad9b5274d23ffc67e216f) printf '%s\n' "0.0.72" ;; + # OpenShell v0.0.82 standalone sandbox binaries. + 145246049bd73c60452ac3c2b4b1801663196c8e2f80575af820289c78c1cf09 | \ + 76bc19b70d9f1e1e9871307045796cd39cc7b8fc4c08ffc90593cc934f36d500) + printf '%s\n' "0.0.82" + ;; + # OpenShell v0.0.85 standalone sandbox binaries. + 863ef21ab7ef623f5e7a8728c4e5532b46bfbae3ace3b800665a1c6353a1f7d2 | \ + 680115dbc2affde0e88261ab09f4044726d1cc9e01de55dc5077d1118f52968d) + printf '%s\n' "0.0.85" + ;; *) return 1 ;; diff --git a/scripts/update-hermes-agent.sh b/scripts/update-hermes-agent.sh index 820bb8b29f..87411c4334 100755 --- a/scripts/update-hermes-agent.sh +++ b/scripts/update-hermes-agent.sh @@ -201,7 +201,7 @@ installed_copy_schema_error() { "COPY agents/hermes/build-mcp-digest.py /usr/local/lib/nemoclaw/build-hermes-mcp-digest.py" \ "/opt/hermes/.venv/bin/python -I /usr/local/lib/nemoclaw/build-hermes-mcp-digest.py --guard /usr/local/lib/nemoclaw/hermes-runtime-config-guard.py" \ "hermes-mcp-config-transaction.py" \ - "openshell-child-visible-credentials.v0.0.72.json" \ + "openshell-child-visible-credentials.v0.0.85.json" \ "HERMES_HOME=/sandbox/.hermes /usr/local/bin/hermes doctor --fix" \ "node --experimental-strip-types /opt/nemoclaw-hermes-config/generate-config.ts" \ "/sandbox/.hermes/dashboard-home"; do diff --git a/src/commands/sandbox/exec.test.ts b/src/commands/sandbox/exec.test.ts index 73b1a536d6..05a0c8522c 100644 --- a/src/commands/sandbox/exec.test.ts +++ b/src/commands/sandbox/exec.test.ts @@ -112,10 +112,9 @@ describe("SandboxExecCommand oclif parse path", () => { }); }); - it("forwards a multi-line heredoc command verbatim to the action guard (#5980)", async () => { - // The command layer forwards argv unchanged; execSandbox() applies the - // newline guard (exit 2 before dispatch), which is asserted directly in the - // action test. Here we pin that the heredoc reaches the action intact. + it("forwards a multi-line heredoc command verbatim to the action", async () => { + // The action dispatches this exact argument through OpenShell. Its + // byte-preserving boundary is asserted directly in the action test. const heredoc = "cat < { }); }); - it("forwards the semicolon workaround to dispatch (#5980)", async () => { - // Mirrors the action-layer "forwards the semicolon workaround to dispatch" - // test: the single-line semicolon-joined command carries no newline, so the - // command layer hands it to execSandbox() unchanged, which then dispatches. + it("forwards a semicolon-joined command unchanged", async () => { await SandboxExecCommand.run(["alpha", "--", "bash", "-lc", "echo line1; echo line2"], rootDir); expect(execSandboxMock).toHaveBeenCalledWith( "alpha", @@ -138,7 +134,7 @@ describe("SandboxExecCommand oclif parse path", () => { ); }); - it("preserves --workdir and forwards a single-line command unchanged (#5980)", async () => { + it("preserves --workdir and forwards a single-line command unchanged", async () => { await SandboxExecCommand.run( ["alpha", "--workdir", "/sandbox", "--", "bash", "-lc", "echo line1; echo line2"], rootDir, diff --git a/src/commands/sandbox/exec.ts b/src/commands/sandbox/exec.ts index f79eb5165e..183062dfdb 100644 --- a/src/commands/sandbox/exec.ts +++ b/src/commands/sandbox/exec.ts @@ -10,7 +10,7 @@ export default class SandboxExecCommand extends NemoClawCommand { static strict = false; static summary = "Run a command non-interactively in a running sandbox"; static description = - "Run a single command inside a running sandbox via the OpenShell exec endpoint. The command runs as the sandbox user (HOME=/sandbox) and exits with the remote command's exit code. Use `--` to separate exec options from the user command. Stdin is inherited by default only when it is a terminal; pass `--stdin` to forward an intentional pipe."; + "Run a single command inside a running sandbox via the OpenShell exec endpoint. The command runs as the sandbox user (HOME=/sandbox) and exits with the remote command's exit code. Use `--` to separate exec options from the user command; arguments after it preserve embedded line endings and quotes. NUL bytes are rejected, and `--workdir` must remain single-line. Stdin is inherited by default only when it is a terminal; pass `--stdin` to forward an intentional pipe."; static usage = [ " [--workdir ] [--tty|--no-tty] [--timeout ] [--stdin|--no-stdin] -- [args...]", ]; diff --git a/src/lib/actions/sandbox/auto-pair-approval.test.ts b/src/lib/actions/sandbox/auto-pair-approval.test.ts index a918ae3b59..2693cf9d43 100644 --- a/src/lib/actions/sandbox/auto-pair-approval.test.ts +++ b/src/lib/actions/sandbox/auto-pair-approval.test.ts @@ -11,7 +11,6 @@ import { AUTO_PAIR_MAX_APPROVALS, buildAutoPairApprovalScript, readAutoPairApprovalPolicyModule, - wrapSandboxShellScript, } from "./auto-pair-approval"; const SUMMARY_MARKER = "__NEMOCLAW_AUTO_PAIR_APPROVED__"; @@ -51,24 +50,6 @@ describe("buildAutoPairApprovalScript (#4263/#4616)", () => { }); }); -describe("wrapSandboxShellScript (#4616)", () => { - it("encodes a multi-line payload onto a single newline-free line", () => { - const wrapped = wrapSandboxShellScript("echo one\necho two\n"); - expect(wrapped).not.toMatch(/[\n\r]/); - expect(wrapped).toContain("base64 -d"); - expect(wrapped).toContain("mktemp"); - }); - - it("round-trips and preserves the inner exit status when run", () => { - const inner = "echo line-one\nprintf 'exit-then\\n'\nexit 3\n"; - const wrapped = wrapSandboxShellScript(inner); - const result = spawnSync("sh", ["-c", wrapped], { encoding: "utf-8", timeout: 10_000 }); - expect(result.stdout).toContain("line-one"); - expect(result.stdout).toContain("exit-then"); - expect(result.status).toBe(3); - }); -}); - describe("auto-pair approval pass behaviour (#4616)", () => { it("approves allowlisted upgrades, skips unknown clients, and reports the count", () => { if (spawnSync("sh", ["-c", "command -v python3"], { stdio: "ignore" }).status !== 0) { diff --git a/src/lib/actions/sandbox/auto-pair-approval.ts b/src/lib/actions/sandbox/auto-pair-approval.ts index f86c907336..7c011b64b3 100644 --- a/src/lib/actions/sandbox/auto-pair-approval.ts +++ b/src/lib/actions/sandbox/auto-pair-approval.ts @@ -82,27 +82,6 @@ export type AutoPairApprovalResult = { approved: number; }; -/** - * Wrap a multi-line shell payload so it survives `openshell sandbox exec`. - * - * OpenShell's exec RPC rejects any argument containing a newline or carriage - * return ("command argument N contains newline or carriage return characters"), - * so a multi-line `sh -c