Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1e7f3ae
fix(dcode): improve managed first-run sessions
jyaunches Jul 11, 2026
0b3c56a
fix(dcode): document signal forwarding race
jyaunches Jul 11, 2026
6554547
test(dcode): cover repeated session cleanup
jyaunches Jul 11, 2026
d162269
fix(dcode): refresh curl security pin
jyaunches Jul 11, 2026
cdf55f2
fix(dcode): close supervisor signal race
jyaunches Jul 11, 2026
6b18844
test(dcode): exercise repeated cleanup check
jyaunches Jul 11, 2026
89fde82
fix(dcode): bypass supervisor for identity
jyaunches Jul 11, 2026
0edc466
fix(dcode): bypass supervisor for version
jyaunches Jul 11, 2026
344df05
fix(dcode): preserve empty prompt validation
jyaunches Jul 11, 2026
0c7dccc
test(dcode): preserve warning policy on re-onboard
jyaunches Jul 11, 2026
1d3fca4
fix(dcode): supervise interactive sessions only
jyaunches Jul 11, 2026
7fd5cfd
fix(dcode): install supervisor in runtime path
jyaunches Jul 11, 2026
8c33f70
docs(dcode): clarify interactive supervision
jyaunches Jul 11, 2026
b0e8db1
test(dcode): wait for TUI composer readiness
jyaunches Jul 11, 2026
db5061f
Merge remote-tracking branch 'origin/main' into codex/fix-managed-dee…
jyaunches Jul 11, 2026
1446e9e
test(e2e): align shared credential-free inventory
jyaunches Jul 11, 2026
4512915
fix(dcode): harden supervisor platform boundary
jyaunches Jul 11, 2026
cb82e2b
fix(dcode): bound disconnect cleanup
jyaunches Jul 11, 2026
f67d366
merge(main): sync latest changes
jyaunches Jul 11, 2026
d05bdce
Merge branch 'main' into codex/fix-managed-deep-code-ux-6678
cv Jul 11, 2026
dc6e8b5
fix(dcode): observe late disconnect signals
jyaunches Jul 11, 2026
bc8c962
Merge remote-tracking branch 'origin/codex/fix-managed-deep-code-ux-6…
jyaunches Jul 11, 2026
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
4 changes: 3 additions & 1 deletion agents/langchain-deepagents-code/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ COPY agents/langchain-deepagents-code/validate-progressive-tool-disclosure.py /o
COPY agents/langchain-deepagents-code/validate-observability.py /opt/nemoclaw-deepagents-code/validate-observability.py
COPY agents/langchain-deepagents-code/dcode-wrapper.sh /usr/local/lib/nemoclaw/dcode-wrapper.sh
COPY agents/langchain-deepagents-code/dcode-launcher.sh /usr/local/lib/nemoclaw/dcode-launcher.sh
COPY agents/langchain-deepagents-code/dcode-session-supervisor.py /usr/local/lib/nemoclaw/dcode-session-supervisor.py
COPY agents/langchain-deepagents-code/start.sh /usr/local/bin/nemoclaw-start
COPY nemoclaw-blueprint/ /opt/nemoclaw-blueprint/
# The first-party profile plugin uses Deep Agents' supported entry-point hook to
Expand All @@ -49,7 +50,8 @@ COPY nemoclaw-blueprint/ /opt/nemoclaw-blueprint/
# removalCondition: remove when installation validates dependencies atomically.
# hadolint ignore=DL4006
RUN chmod 444 /opt/nemoclaw-deepagents-code/generate-config.ts /opt/nemoclaw-deepagents-code/managed-dcode-runtime.py /opt/nemoclaw-deepagents-code/patch-managed-deepagents-code.py /opt/nemoclaw-deepagents-code/validate-nemotron-ultra-profile.py /opt/nemoclaw-deepagents-code/progressive_tool_disclosure.py /opt/nemoclaw-deepagents-code/nemoclaw_observability.py /opt/nemoclaw-deepagents-code/validate-progressive-tool-disclosure.py /opt/nemoclaw-deepagents-code/validate-observability.py \
&& chmod 755 /usr/local/bin/nemoclaw-start /usr/local/lib/nemoclaw/dcode-wrapper.sh /usr/local/lib/nemoclaw/dcode-launcher.sh \
&& chmod 755 /usr/local/bin/nemoclaw-start /usr/local/lib/nemoclaw/dcode-wrapper.sh /usr/local/lib/nemoclaw/dcode-launcher.sh /usr/local/lib/nemoclaw/dcode-session-supervisor.py \
&& test "$(stat -c '%u:%g:%a' /usr/local/lib/nemoclaw/dcode-session-supervisor.py)" = "0:0:755" \
&& install -o root -g root -m 0755 /usr/local/lib/nemoclaw/dcode-launcher.sh /usr/local/lib/nemoclaw/dcode-managed-exec \
&& test -f /usr/local/lib/nemoclaw/dcode-managed-exec \
&& test ! -L /usr/local/lib/nemoclaw/dcode-managed-exec \
Expand Down
9 changes: 8 additions & 1 deletion agents/langchain-deepagents-code/Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3=3.13.5-1 \
python3-pip=25.1.1+dfsg-1 \
python3-venv=3.13.5-1 \
curl=8.14.1-2+deb13u3 \
curl=8.14.1-2+deb13u4 \
git=1:2.47.3-0+deb13u1 \
ca-certificates=20250419 \
iproute2=6.15.0-1 \
Expand All @@ -27,6 +27,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
e2fsprogs=1.47.2-3+b11 \
openssh-sftp-server=1:10.0p1-7+deb13u4 \
jq=1.7.1-6+deb13u2 \
ripgrep=14.1.1-1+b4 \
vim-tiny=2:9.1.1230-2 \
&& rm -rf /var/lib/apt/lists/*

Expand All @@ -36,6 +37,12 @@ RUN groupadd -r sandbox \
&& mkdir -p /sandbox/.nemoclaw \
/sandbox/.deepagents/.state \
/sandbox/.deepagents/skills \
# Deep Agents Code owns the optional-name onboarding state, but managed
# terminals cannot answer that upstream first-run prompt before becoming
# usable. Preseed it here; the TUI startup E2E rejects pending onboarding
# and unexpected name prompts. Remove this when upstream supports a
# documented non-interactive managed-onboarding mode.
&& printf '1\n' > /sandbox/.deepagents/.state/onboarding_complete \
&& chown -R sandbox:sandbox /sandbox \
&& chmod 2770 /sandbox/.deepagents \
&& chmod 770 /sandbox/.deepagents/.state /sandbox/.deepagents/skills
Expand Down
22 changes: 21 additions & 1 deletion agents/langchain-deepagents-code/dcode-launcher.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ unset _nemoclaw_auto_approval_env
readonly MANAGED_DCODE_WRAPPER="/usr/local/lib/nemoclaw/dcode-wrapper.sh"
readonly MANAGED_EXEC_LAUNCHER="/usr/local/lib/nemoclaw/dcode-managed-exec"
readonly MANAGED_OBSERVABILITY_MARKER="/sandbox/.deepagents/.nemoclaw-observability-enabled"
readonly MANAGED_SESSION_SUPERVISOR="/usr/local/lib/nemoclaw/dcode-session-supervisor.py"
export HOME=/sandbox
export PATH="/usr/local/bin:/opt/venv/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin"

Expand Down Expand Up @@ -159,4 +160,23 @@ if [ "$0" = "$MANAGED_EXEC_LAUNCHER" ]; then
exec "$@"
fi

exec "$MANAGED_DCODE_WRAPPER" "$@"
# Read-only managed identity commands never start DCode or LangGraph children.
# Keep onboard's live-route validation on the established wrapper path while
# supervising every command that can create a terminal-agent process tree.
case "${1:-}" in
status | whoami | identity | --version | -v | -V) exec "$MANAGED_DCODE_WRAPPER" "$@" ;;
esac

# DCode's one-shot mode owns and cleans up its server lifecycle before exiting.
# Keep that established automation path outside the interactive-session
# supervisor; this also preserves the wrapper's exact parser diagnostics.
_nemoclaw_dcode_args=("$@")
for ((_nemoclaw_arg_index = 0; _nemoclaw_arg_index < ${#_nemoclaw_dcode_args[@]}; _nemoclaw_arg_index++)); do
_nemoclaw_arg="${_nemoclaw_dcode_args[_nemoclaw_arg_index]}"
case "$_nemoclaw_arg" in
-n | -n?* | --non-interactive | --non-interactive=*) exec "$MANAGED_DCODE_WRAPPER" "$@" ;;
esac
done
unset _nemoclaw_dcode_args _nemoclaw_arg_index _nemoclaw_arg

exec /opt/venv/bin/python3 -I "$MANAGED_SESSION_SUPERVISOR" "$MANAGED_DCODE_WRAPPER" "$@"
185 changes: 185 additions & 0 deletions agents/langchain-deepagents-code/dcode-session-supervisor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Reap processes started by one managed Deep Agents Code terminal session."""

from __future__ import annotations

import ctypes
import errno
import os
import signal
import subprocess
import sys
import time
from collections.abc import Sequence
from pathlib import Path

_PR_SET_CHILD_SUBREAPER = 36
_TERM_GRACE_SECONDS = 3.0
_KILL_GRACE_SECONDS = 1.0
_POLL_SECONDS = 0.05


def _enable_child_subreaper() -> None:
"""Adopt orphaned LangGraph descendants when the DCode process exits."""
libc = ctypes.CDLL(None, use_errno=True)
if libc.prctl(_PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) != 0:
error = ctypes.get_errno()
raise OSError(error, os.strerror(error))


def _direct_children() -> set[int]:
children: set[int] = set()
try:
entries = os.scandir("/proc")
except OSError:
return children
with entries:
for entry in entries:
if not entry.name.isdecimal():
continue
try:
stat = Path(f"/proc/{entry.name}/stat").read_text(encoding="utf-8")
closing = stat.rfind(")")
fields = stat[closing + 2 :].split()
if closing != -1 and len(fields) >= 2 and int(fields[1]) == os.getpid():
children.add(int(entry.name))
except (FileNotFoundError, PermissionError, ValueError, OSError):
continue
return children


def _reap_exited_children() -> None:
while True:
try:
pid, _status = os.waitpid(-1, os.WNOHANG)
except ChildProcessError:
return
except InterruptedError:
continue
if pid == 0:
return


def _signal_children(children: set[int], sig: signal.Signals) -> None:
for pid in children:
try:
os.kill(pid, sig)
except ProcessLookupError:
continue
except PermissionError:
print(
f"dcode: cannot signal managed session descendant pid={pid}",
file=sys.stderr,
)


def _cleanup_adopted_descendants() -> None:
"""Terminate and reap every descendant associated with this launch."""
deadline = time.monotonic() + _TERM_GRACE_SECONDS
signaled: set[int] = set()
while True:
_reap_exited_children()
children = _direct_children()
if not children:
return
new_children = children - signaled
if new_children:
_signal_children(new_children, signal.SIGTERM)
signaled.update(new_children)
if time.monotonic() >= deadline:
_signal_children(children, signal.SIGKILL)
break
time.sleep(_POLL_SECONDS)

kill_deadline = time.monotonic() + 1.0
while time.monotonic() < kill_deadline:
_reap_exited_children()
children = _direct_children()
if not children:
return
_signal_children(children, signal.SIGKILL)
time.sleep(_POLL_SECONDS)
_reap_exited_children()


def _exit_code(returncode: int) -> int:
return returncode if returncode >= 0 else 128 + abs(returncode)


def _wait_after_disconnect(child: subprocess.Popen[bytes]) -> int:
"""Bound shutdown even when the direct DCode child ignores disconnect."""
try:
return child.wait(timeout=_TERM_GRACE_SECONDS)
except subprocess.TimeoutExpired:
child.terminate()
try:
return child.wait(timeout=_KILL_GRACE_SECONDS)
except subprocess.TimeoutExpired:
child.kill()
return child.wait()


def run(argv: Sequence[str]) -> int:
if not argv:
print("dcode session supervisor requires a command.", file=sys.stderr)
return 64
if sys.platform != "linux":
print(
"dcode: session supervision requires a Linux OpenShell sandbox.",
file=sys.stderr,
)
return 1

_enable_child_subreaper()
child: subprocess.Popen[bytes] | None = None
pending_signals: list[int] = []
disconnect_received = False

def forward(sig: int, _frame: object) -> None:
nonlocal disconnect_received
disconnect_received = True
if child is None:
pending_signals.append(sig)
return
try:
os.kill(child.pid, sig)
except (ProcessLookupError, PermissionError):
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
# The child may exit between signal delivery and this forwarding
# attempt; cleanup below still reaps any adopted descendants.
pass

# Terminal-generated SIGINT already reaches every member of the foreground
# process group. Keep the supervisor alive to reap descendants without
# delivering a second Ctrl-C to DCode. OpenShell may target only the direct
# launcher for disconnect/termination signals, so those are forwarded.
signal.signal(signal.SIGINT, lambda _sig, _frame: None)
for sig in (signal.SIGHUP, signal.SIGTERM):
signal.signal(sig, forward)

try:
child = subprocess.Popen(list(argv))
for pending_signal in pending_signals:
forward(pending_signal, None)
while True:
try:
returncode = child.wait(timeout=_POLL_SECONDS)
break
except subprocess.TimeoutExpired:
if disconnect_received:
returncode = _wait_after_disconnect(child)
break
finally:
_cleanup_adopted_descendants()
return _exit_code(returncode)
Comment thread
coderabbitai[bot] marked this conversation as resolved.


if __name__ == "__main__":
try:
raise SystemExit(run(sys.argv[1:]))
except OSError as error:
if error.errno == errno.ENOSYS:
print("dcode: Linux child-subreaper support is unavailable.", file=sys.stderr)
else:
print(f"dcode: session supervisor failed: {error}", file=sys.stderr)
raise SystemExit(1) from error
4 changes: 4 additions & 0 deletions agents/langchain-deepagents-code/dcode-wrapper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -802,8 +802,12 @@ print_identity() {
model="$(terminal_safe_identity_value "$(toml_section_scalar models default)")"
[ -n "$model" ] || model="$(terminal_safe_identity_value "$(toml_section_scalar models recent)")"
endpoint="$(toml_section_scalar models.providers.openai base_url)"
[ -n "$endpoint" ] || endpoint="$(toml_section_scalar models.providers.openrouter base_url)"
route="$(terminal_safe_identity_value "$(toml_provider_metadata route)")"
provider="$(terminal_safe_identity_value "$(toml_provider_metadata provider)")"
case "$model" in
openrouter:*) provider="openrouter" ;;
esac
[ -n "$endpoint" ] || endpoint="${OPENAI_BASE_URL:-}"
endpoint="$(safe_endpoint_identity_value "$endpoint")"
printf 'Sandbox: %s\n' "$sandbox_name"
Expand Down
4 changes: 4 additions & 0 deletions agents/langchain-deepagents-code/generate-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,10 @@ function buildConfig(settings: Settings): ManagedDeepAgentsConfig {
"check = false",
"auto_update = false",
"",
"[warnings]",
"# Tavily is optional in managed sandboxes; surface errors only when web search is invoked.",
'suppress = ["tavily"]',
"",
].join("\n");
return { text, provider, model, defaultModel };
}
Expand Down
7 changes: 5 additions & 2 deletions docs/get-started/quickstart-langchain-deepagents-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ This agent-specific default does not change the shared Nemotron 3 Super default
NemoClaw/OpenShell keeps real provider credentials in credential handling and does not write them into the Deep Agents config file.
Deep Agents Code reaches `inference.local` through the managed OpenShell L7 proxy rather than direct sandbox DNS.
The image launcher normalizes the runtime proxy environment for interactive, login-shell, and direct-exec paths and removes inherited proxy credentials and bypass entries before `dcode` starts.
Managed interactive sessions keep Deep Agents Code's optional first-run name prompt, skip its dependency and model selection screens, then open the TUI with the model selected during NemoClaw onboarding.
Press Enter at the name prompt to continue without setting a name.
Managed interactive sessions pre-complete Deep Agents Code's optional first-run onboarding, skip its dependency and model selection screens, then open the TUI with the model selected during NemoClaw onboarding.
The image includes `ripgrep`, and ordinary sessions suppress the optional Tavily warning unless web search is configured or invoked.

</Accordion>

Expand Down Expand Up @@ -151,6 +151,9 @@ dcode -n "Summarize this repository"
```

The managed `dcode`, `dcode.real`, and `deepagents-code` launchers use `/opt/venv/bin/python3 -I` to run the pinned package with an isolated import path and `HOME=/sandbox`.
For interactive sessions, each launcher supervises its own process descendants so terminal exit or disconnect terminates the associated LangGraph server tree without affecting another session.
After a disconnect, the supervisor uses bounded grace periods before it kills unresponsive processes from that session.
The supervisor runs inside the Linux OpenShell sandbox and fails closed if invoked outside Linux; the host operating system does not change this sandbox guarantee.
They disable Deep Agents Code package update checks and the LangGraph server version check, block CLI and TUI update/install commands, and disable nested remote sandbox providers, remote async subagents, MCP commands and project auto-loading, startup commands, executable hooks, ACP mode, interpreter tool calling, shell allow-list overrides, native LangSmith tracing, and ambient OpenTelemetry exporter configuration.
The managed model constructor accepts only Deep Agents Code's `openai` provider path and reads its endpoint from a root-owned image file.
It supplies the non-secret gateway placeholder key and ignores mutable provider classes, credentials, endpoints, and constructor parameters in Deep Agents Code config.
Expand Down
Loading
Loading