diff --git a/agents/langchain-deepagents-code/dcode-launcher.sh b/agents/langchain-deepagents-code/dcode-launcher.sh index bc37d25ff8c..85c87aa3ed4 100755 --- a/agents/langchain-deepagents-code/dcode-launcher.sh +++ b/agents/langchain-deepagents-code/dcode-launcher.sh @@ -211,16 +211,4 @@ 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" "$@" diff --git a/docs/get-started/quickstart-langchain-deepagents-code.mdx b/docs/get-started/quickstart-langchain-deepagents-code.mdx index bde02ee2bc7..489011e0001 100644 --- a/docs/get-started/quickstart-langchain-deepagents-code.mdx +++ b/docs/get-started/quickstart-langchain-deepagents-code.mdx @@ -151,7 +151,7 @@ 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. +For interactive and headless sessions, each launcher supervises its own process descendants so completion, 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. diff --git a/test/deepagents-code-tui-startup-check.test.ts b/test/deepagents-code-tui-startup-check.test.ts index bf7333ea3da..8cc33b53414 100644 --- a/test/deepagents-code-tui-startup-check.test.ts +++ b/test/deepagents-code-tui-startup-check.test.ts @@ -418,13 +418,15 @@ describe("Deep Agents Code TUI startup check helpers", () => { "10-deepagents-code-tui-startup.repeat.sanitized.log", ); const processCounts = path.join(captureDir, "process-counts.txt"); - fs.writeFileSync(processCounts, "0\n1\n0\n1\n0\n"); + fs.writeFileSync(processCounts, "0\n1\n0\n1\n0\n1\n0\n"); try { const result = runTuiStartupCheckHelperResult( [ "sandbox_exec() { printf 'NEMOCLAW_DCODE_PROBE:deepagents\\nNEMOCLAW_DCODE_ONBOARDING:complete\\n'; }", "ensure_expect_available() { return 0; }", + 'run_headless_session() { printf "PONG\\n"; }', + "sandbox_is_ready() { return 0; }", "dcode_process_count() {", ' value="$(sed -n "1p" "$COUNT_FILE")"', ' sed "1d" "$COUNT_FILE" >"$COUNT_FILE.next"', @@ -444,6 +446,11 @@ describe("Deep Agents Code TUI startup check helpers", () => { const sanitizedText = fs.readFileSync(sanitizedCapture, "utf8"); const repeatedSanitizedText = fs.readFileSync(repeatedSanitizedCapture, "utf8"); expect(result.status).toBe(0); + expect(result.stdout).toContain("headless dcode request returned PONG"); + expect(result.stdout).toContain( + "headless completion returned the DCode/LangGraph process count to baseline", + ); + expect(result.stdout).toContain("sandbox remained Ready after headless completion"); expect(result.stdout).toContain( "session 1: finite expect harness reached startup and observed exit", ); @@ -454,6 +461,7 @@ describe("Deep Agents Code TUI startup check helpers", () => { expect(sanitizedText).toContain("NEMOCLAW_TUI_READY"); expect(sanitizedText).toContain("NEMOCLAW_TUI_EXIT_CAPTURED:130"); expect(repeatedSanitizedText).toContain("NEMOCLAW_TUI_READY"); + expect(repeatedSanitizedText).toContain("NEMOCLAW_TUI_EXIT_CAPTURED:130"); expect(result.stdout).toContain( "session 2: DCode/LangGraph process count returned to baseline", ); @@ -471,6 +479,8 @@ describe("Deep Agents Code TUI startup check helpers", () => { [ "sandbox_exec() { printf 'NEMOCLAW_DCODE_PROBE:deepagents\\nNEMOCLAW_DCODE_ONBOARDING:complete\\n'; }", "ensure_expect_available() { return 0; }", + 'run_headless_session() { printf "PONG\\n"; }', + "sandbox_is_ready() { return 0; }", "dcode_process_count() { printf 'NEMOCLAW_DCODE_PROCESS_COUNT:0\\n'; }", "wait_for_dcode_process_baseline() { return 0; }", "run_tui_expect() {", diff --git a/test/e2e/e2e-cloud-experimental/checks/10-deepagents-code-tui-startup.sh b/test/e2e/e2e-cloud-experimental/checks/10-deepagents-code-tui-startup.sh index e8b5a64fe7c..a6c773b260a 100755 --- a/test/e2e/e2e-cloud-experimental/checks/10-deepagents-code-tui-startup.sh +++ b/test/e2e/e2e-cloud-experimental/checks/10-deepagents-code-tui-startup.sh @@ -4,10 +4,10 @@ # # Case: Deep Agents Code interactive TUI startup (#5620). # -# This live check runs twice against a real Deep Agents Code sandbox. It proves -# the interactive `dcode` TUI starts in a PTY, opens and closes the `/agents` -# modal, exits after Ctrl-C without leaking DCode/LangGraph processes, and -# leaves only sanitized, secret-free capture artifacts. +# This live check runs a headless request and two interactive sessions against a +# real Deep Agents Code sandbox. It proves completed sessions do not leak +# DCode/LangGraph processes and leaves only sanitized, secret-free capture +# artifacts. # # shellcheck disable=SC2016 # expect(1) Tcl: $env(...) and {...} are Tcl/sh expansion, not bash expansion. @@ -17,6 +17,7 @@ set -euo pipefail SANDBOX_NAME="${SANDBOX_NAME:-${NEMOCLAW_SANDBOX_NAME:-e2e-cloud-onboard}}" PREFIX="10-deepagents-code-tui-startup" TUI_TIMEOUT="${DEEPAGENTS_TUI_TIMEOUT:-90}" +HEADLESS_TIMEOUT="${DEEPAGENTS_HEADLESS_TIMEOUT:-120}" PROCESS_CLEANUP_TIMEOUT=20 # Shell-only live check fallback for remote e2e hosts; Vitest parity coverage in # test/deepagents-code-tui-startup-check.test.ts pins this to secret-patterns.ts. @@ -50,6 +51,26 @@ sandbox_exec() { openshell sandbox exec --name "$SANDBOX_NAME" -- bash -c "$1" 2>&1 } +run_headless_session() { + openshell sandbox exec --name "$SANDBOX_NAME" -- \ + dcode -n 'Reply with exactly one word: PONG' \ + --json --max-turns 1 --timeout "$HEADLESS_TIMEOUT" 2>&1 +} + +sandbox_is_ready() { + openshell sandbox list 2>&1 \ + | awk -v name="$SANDBOX_NAME" '$1 == name && /Ready/ { found = 1 } END { exit(found ? 0 : 1) }' +} + +wait_for_sandbox_ready() { + local deadline=$((SECONDS + PROCESS_CLEANUP_TIMEOUT)) + while :; do + sandbox_is_ready && return 0 + [ "$SECONDS" -ge "$deadline" ] && return 1 + sleep 1 + done +} + is_positive_integer() { [[ "$1" =~ ^[1-9][0-9]*$ ]] } @@ -149,9 +170,10 @@ strip_terminal_control_sequences() { cleanup_sensitive_captures() { local artifact - for artifact in "${SENSITIVE_CAPTURE_FILES[@]}"; do + for artifact in "${SENSITIVE_CAPTURE_FILES[@]:-}"; do [ -n "$artifact" ] && rm -f -- "$artifact" done + return 0 } make_capture_dir() { @@ -384,6 +406,12 @@ main() { exit 1 fi + if ! is_positive_integer "$HEADLESS_TIMEOUT"; then + fail_test "DEEPAGENTS_HEADLESS_TIMEOUT must be a positive integer" + printf '%s\n' "${PREFIX}: $PASSED passed, $FAILED failed" + exit 1 + fi + if ! command -v perl >/dev/null 2>&1; then fail_test "perl is required to sanitize and redact Deep Agents Code TUI captures" printf '%s\n' "${PREFIX}: $PASSED passed, $FAILED failed" @@ -439,6 +467,27 @@ main() { exit 1 fi + local headless_output headless_rc + set +e + headless_output="$(run_headless_session)" + headless_rc=$? + set -e + if [ "$headless_rc" -eq 0 ] && grep -Eiq '(^|[^A-Za-z])PONG([^A-Za-z]|$)' <<<"$headless_output"; then + pass "headless dcode request returned PONG" + else + fail_test "headless dcode request did not return exit-zero PONG" + fi + if wait_for_dcode_process_baseline "$baseline_process_count"; then + pass "headless completion returned the DCode/LangGraph process count to baseline" + else + fail_test "headless completion left the DCode/LangGraph process count above baseline after ${PROCESS_CLEANUP_TIMEOUT}s" + fi + if wait_for_sandbox_ready; then + pass "sandbox remained Ready after headless completion" + else + fail_test "sandbox was not Ready after headless completion" + fi + info "Running two Deep Agents Code TUI sessions in sandbox: $SANDBOX_NAME" info "Capture directory: $capture_dir" diff --git a/test/langchain-deepagents-code-proxy-launcher.test.ts b/test/langchain-deepagents-code-proxy-launcher.test.ts index 29f35f361f9..1061c8c0d6e 100644 --- a/test/langchain-deepagents-code-proxy-launcher.test.ts +++ b/test/langchain-deepagents-code-proxy-launcher.test.ts @@ -178,17 +178,14 @@ describe("Deep Agents Code direct-exec proxy launcher", () => { expect(launcher.indexOf(directIdentity)).toBeLessThan(launcher.indexOf(supervisedSession)); }); - it("keeps one-shot non-interactive sessions outside the interactive supervisor", () => { + it("routes one-shot non-interactive sessions through the session supervisor (#6720)", () => { const launcher = readAgentFile("dcode-launcher.sh"); - const nonInteractiveBypass = - '-n | -n?* | --non-interactive | --non-interactive=*) exec "$MANAGED_DCODE_WRAPPER" "$@" ;;'; const supervisedSession = 'exec /opt/venv/bin/python3 -I "$MANAGED_SESSION_SUPERVISOR" "$MANAGED_DCODE_WRAPPER" "$@"'; - expect(launcher).toContain(nonInteractiveBypass); - expect(launcher.indexOf(nonInteractiveBypass)).toBeLessThan( - launcher.indexOf(supervisedSession), - ); + expect(launcher).not.toMatch(/-n\s*\|[^\n]*exec "\$MANAGED_DCODE_WRAPPER"/u); + expect(launcher).not.toMatch(/--non-interactive[^\n]*exec "\$MANAGED_DCODE_WRAPPER"/u); + expect(launcher).toContain(supervisedSession); }); it("preserves the empty-prompt failure through the installed launcher chain (#6440)", () => {