Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions agents/langchain-deepagents-code/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ ARG NEMOCLAW_UPSTREAM_PROVIDER=nvidia
ARG NEMOCLAW_INFERENCE_BASE_URL=https://inference.local/v1
ARG NEMOCLAW_INFERENCE_API=openai-completions
ARG NEMOCLAW_TOOL_DISCLOSURE=progressive
ARG NEMOCLAW_DCODE_AUTO_APPROVAL=disabled
ARG NEMOCLAW_BUILD_ID=default
ARG NEMOCLAW_DARWIN_VM_COMPAT=0
ARG NEMOCLAW_PROXY_HOST=10.200.0.1
Expand All @@ -70,6 +71,10 @@ ARG NEMOCLAW_PROXY_PORT=3128
RUN case "$NEMOCLAW_TOOL_DISCLOSURE" in \
progressive|direct) ;; \
*) echo "ERROR: NEMOCLAW_TOOL_DISCLOSURE must be progressive or direct" >&2; exit 1 ;; \
esac \
&& case "$NEMOCLAW_DCODE_AUTO_APPROVAL" in \
disabled|thread-opt-in) ;; \
*) echo "ERROR: NEMOCLAW_DCODE_AUTO_APPROVAL must be disabled or thread-opt-in" >&2; exit 1 ;; \
esac

# The launcher and startup script read these root-owned files instead of
Expand All @@ -81,8 +86,9 @@ RUN install -d -m 0755 /usr/local/share/nemoclaw \
&& printf '%s\n' "$NEMOCLAW_PROXY_HOST" > /usr/local/share/nemoclaw/dcode-proxy-host \
&& printf '%s\n' "$NEMOCLAW_PROXY_PORT" > /usr/local/share/nemoclaw/dcode-proxy-port \
&& printf '%s\n' "$NEMOCLAW_INFERENCE_BASE_URL" > /usr/local/share/nemoclaw/dcode-inference-base-url \
&& chown root:root /usr/local/share/nemoclaw/dcode-proxy-host /usr/local/share/nemoclaw/dcode-proxy-port /usr/local/share/nemoclaw/dcode-inference-base-url \
&& chmod 0444 /usr/local/share/nemoclaw/dcode-proxy-host /usr/local/share/nemoclaw/dcode-proxy-port /usr/local/share/nemoclaw/dcode-inference-base-url \
&& printf '%s\n' "$NEMOCLAW_DCODE_AUTO_APPROVAL" > /usr/local/share/nemoclaw/dcode-auto-approval \
&& chown root:root /usr/local/share/nemoclaw/dcode-proxy-host /usr/local/share/nemoclaw/dcode-proxy-port /usr/local/share/nemoclaw/dcode-inference-base-url /usr/local/share/nemoclaw/dcode-auto-approval \
&& chmod 0444 /usr/local/share/nemoclaw/dcode-proxy-host /usr/local/share/nemoclaw/dcode-proxy-port /usr/local/share/nemoclaw/dcode-inference-base-url /usr/local/share/nemoclaw/dcode-auto-approval \
&& empty_prompt_log="$(mktemp)" \
&& if timeout 10 /usr/local/bin/dcode -n "" >"$empty_prompt_log" 2>&1; then empty_prompt_status=0; else empty_prompt_status=$?; fi \
&& test "$empty_prompt_status" -eq 2 \
Expand Down
4 changes: 4 additions & 0 deletions agents/langchain-deepagents-code/dcode-launcher.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

set -euo pipefail
unset BASH_ENV ENV
while IFS= read -r _nemoclaw_auto_approval_env; do
unset "$_nemoclaw_auto_approval_env"
done < <(compgen -A variable NEMOCLAW_DCODE_AUTO_APPROVAL || true)
unset _nemoclaw_auto_approval_env

readonly MANAGED_DCODE_WRAPPER="/usr/local/lib/nemoclaw/dcode-wrapper.sh"
readonly MANAGED_OBSERVABILITY_MARKER="/tmp/nemoclaw-observability-enabled"
Expand Down
51 changes: 50 additions & 1 deletion agents/langchain-deepagents-code/dcode-wrapper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ if [ "${1:-}" = "--nemoclaw-mcp-capability" ] && [ "$#" -eq 1 ]; then
fi

unset BASH_ENV ENV OPENAI_PROXY
while IFS= read -r _nemoclaw_auto_approval_env; do
unset "$_nemoclaw_auto_approval_env"
done < <(compgen -A variable NEMOCLAW_DCODE_AUTO_APPROVAL || true)
unset _nemoclaw_auto_approval_env

export HOME=/sandbox
export PATH="/usr/local/bin:/opt/venv/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin"
Expand Down Expand Up @@ -40,6 +44,49 @@ 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"
readonly MANAGED_DCODE_AUTO_APPROVAL_OWNER_UID=0

managed_auto_approval_file_metadata() {
local file="$1"
local metadata
if metadata="$(stat -c '%u:%a:%s' "$file" 2>/dev/null)"; then
printf '%s' "$metadata"
else
stat -f '%u:%Lp:%z' "$file" 2>/dev/null
fi
}

read_managed_auto_approval_mode() {
local file="$MANAGED_DCODE_AUTO_APPROVAL_FILE"
local metadata
if [ ! -f "$file" ] || [ -L "$file" ] || [ ! -r "$file" ]; then
printf '%s' 'disabled'
return 0
fi
metadata="$(managed_auto_approval_file_metadata "$file")" || {
printf '%s' 'disabled'
return 0
}
case "$metadata" in
"${MANAGED_DCODE_AUTO_APPROVAL_OWNER_UID}:444:9")
if cmp -s -- "$file" <(printf '%s\n' 'disabled'); then
printf '%s' 'disabled'
return 0
fi
;;
"${MANAGED_DCODE_AUTO_APPROVAL_OWNER_UID}:444:14")
if cmp -s -- "$file" <(printf '%s\n' 'thread-opt-in'); then
printf '%s' 'thread-opt-in'
return 0
fi
;;
esac
printf '%s' 'disabled'
}

MANAGED_DCODE_AUTO_APPROVAL_MODE="$(read_managed_auto_approval_mode)"
readonly MANAGED_DCODE_AUTO_APPROVAL_MODE

run_dcode() {
unset PYTHONHOME PYTHONPATH
Expand Down Expand Up @@ -789,7 +836,9 @@ for arg in "$@"; do
reject_managed_override "interpreter posture" "$arg"
;;
-y | --auto-a | --auto-ap | --auto-app | --auto-appr | --auto-appro | --auto-approv | --auto-approve)
reject_managed_override "tool approval posture" "$arg"
if [ "$MANAGED_DCODE_AUTO_APPROVAL_MODE" != "thread-opt-in" ]; then
reject_managed_override "tool approval posture" "$arg"
fi
;;
--acp)
reject_managed_override "ACP approval posture" "$arg"
Expand Down
75 changes: 75 additions & 0 deletions agents/langchain-deepagents-code/managed-dcode-runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import os
import re
import stat
import sys
from pathlib import Path
from urllib.parse import urlparse, urlsplit

Expand All @@ -23,6 +24,15 @@
_INFERENCE_BASE_URL_FILE = Path(
"/usr/local/share/nemoclaw/dcode-inference-base-url"
)
_AUTO_APPROVAL_FILE = Path(
"/usr/local/share/nemoclaw/dcode-auto-approval"
)
_AUTO_APPROVAL_DISABLED = "disabled"
_AUTO_APPROVAL_THREAD_OPT_IN = "thread-opt-in"
_AUTO_APPROVAL_CONTENTS = {
b"disabled\n": _AUTO_APPROVAL_DISABLED,
b"thread-opt-in\n": _AUTO_APPROVAL_THREAD_OPT_IN,
}
_MANAGED_FILE_OWNER_UID = 0
_CREDENTIAL_NAME = re.compile(
r"(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASS|CREDENTIAL)$",
Expand Down Expand Up @@ -864,6 +874,71 @@ def managed_inference_base_url() -> str:
return value


def _disabled_auto_approval(reason: str) -> str:
if os.environ.get("NEMOCLAW_DEBUG") == "1":
print(
f"NemoClaw managed auto-approval disabled: {reason}",
file=sys.stderr,
)
return _AUTO_APPROVAL_DISABLED


def managed_auto_approval_mode() -> str:
"""Return the trusted managed auto-approval mode, failing closed."""
# The image build owns this file, but runtime must tolerate missing or
# malformed image state and fail closed. Keep this check until sandbox
# images are immutable end to end; direct-module tests pin rejected shapes.
path = _AUTO_APPROVAL_FILE
try:
if path.is_symlink():
return _disabled_auto_approval("capability path is a symlink")
flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0)
flags |= getattr(os, "O_NOFOLLOW", 0)
descriptor = os.open(path, flags)
except OSError:
return _disabled_auto_approval("capability file is missing or unreadable")

try:
metadata = os.fstat(descriptor)
if (
not stat.S_ISREG(metadata.st_mode)
or metadata.st_uid != _MANAGED_FILE_OWNER_UID
or stat.S_IMODE(metadata.st_mode) != 0o444
or metadata.st_size not in {
len(content) for content in _AUTO_APPROVAL_CONTENTS
}
):
return _disabled_auto_approval("capability metadata is unsafe")

chunks: list[bytes] = []
remaining = metadata.st_size
while remaining:
chunk = os.read(descriptor, remaining)
if not chunk:
return _disabled_auto_approval("capability file was truncated")
chunks.append(chunk)
remaining -= len(chunk)
if os.read(descriptor, 1):
return _disabled_auto_approval("capability file changed while reading")
except OSError:
return _disabled_auto_approval("capability file read failed")
finally:
try:
os.close(descriptor)
except OSError:
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
# Cleanup cannot weaken the fail-closed capability result.
pass

return _AUTO_APPROVAL_CONTENTS.get(b"".join(chunks)) or _disabled_auto_approval(
"capability contents are invalid"
)


def managed_auto_approval_enabled() -> bool:
"""Return whether thread-scoped auto-approval may be explicitly enabled."""
return managed_auto_approval_mode() == _AUTO_APPROVAL_THREAD_OPT_IN


def managed_display_provider(adapter_provider: object) -> str:
"""Return the provider label to show for the managed inference adapter.

Expand Down
Loading
Loading