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
28 changes: 26 additions & 2 deletions docs/get-started/quickstart-langchain-deepagents-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,32 @@ NemoClaw intentionally does not preserve `.env` or `.mcp.json` because users may
## Optional Web Search

Deep Agents Code can use Tavily web search when you provide a Tavily credential in the runtime environment.
NemoClaw does not enable Tavily or LangSmith by default for this harness.
Before you provide those credentials, [add the required egress endpoints](../network-policy/customize-network-policy) to the sandbox policy so optional integrations stay explicit.
NemoClaw does not enable Tavily or LangSmith by default for this harness. The sandbox policy denies `api.tavily.com` and `api.smith.langchain.com` until you opt in.

To enable Tavily, apply the maintained `tavily` policy preset so the sandbox may reach the Tavily API, then supply the credential.

```bash
# Preview the endpoints the preset opens:
nemoclaw <sandbox-name> policy-add tavily --dry-run
# Apply it:
nemoclaw <sandbox-name> policy-add tavily --yes
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```

The `tavily` preset only opens egress to `api.tavily.com:443`.
Provide the Tavily API key to the agent at runtime; NemoClaw does not bake `TAVILY_API_KEY` into the managed config or image.
Because OpenShell attributes the harness's calls to the sandbox `python3` interpreter, this egress is process-wide for sandbox Python rather than a `dcode`-only boundary.

Remove the access again when it is no longer needed.

```bash
nemoclaw <sandbox-name> policy-remove tavily --yes
```

### Optional Tracing (LangSmith)

LangSmith tracing is **not a supported integration** for this managed harness yet.
`start.sh` forwards the non-secret `LANGSMITH_TRACING`/`LANGSMITH_PROJECT` toggles if set, but no policy preset opens `api.smith.langchain.com` and no supported mechanism injects `LANGSMITH_API_KEY`.
If you need tracing, [add the egress endpoints manually](../network-policy/customize-network-policy); treat it as unsupported until NemoClaw ships a maintained `langsmith` preset.

## Troubleshooting

Expand Down
25 changes: 25 additions & 0 deletions nemoclaw-blueprint/policies/presets/tavily.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

preset:
name: tavily
description: "Tavily web search API access (opt-in)"

network_policies:
tavily:
name: tavily
endpoints:
- host: api.tavily.com
port: 443
protocol: rest
enforcement: enforce
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
binaries:
- { path: /opt/venv/bin/python3* }
- { path: /usr/bin/python3* }
- { path: /usr/local/bin/python3* }
- { path: /usr/local/bin/node }
- { path: /usr/bin/node }
- { path: /usr/bin/curl }
1 change: 1 addition & 0 deletions test/e2e-scenario/live/cloud-experimental-check-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const DEEPAGENTS_CLOUD_EXPERIMENTAL_CHECKS = [
"test/e2e/e2e-cloud-experimental/checks/05-deepagents-code-landlock-readonly.sh",
"test/e2e/e2e-cloud-experimental/checks/06-deepagents-code-python-egress.sh",
"test/e2e/e2e-cloud-experimental/checks/08-deepagents-code-secret-boundary.sh",
"test/e2e/e2e-cloud-experimental/checks/09-deepagents-code-tavily-opt-in.sh",
] as const;

export function cloudExperimentalChecksForOnboarding(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/bin/bash
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Case: Deep Agents Code Tavily opt-in policy (#5739).

set -euo pipefail

SANDBOX_NAME="${SANDBOX_NAME:-${NEMOCLAW_SANDBOX_NAME:-e2e-cloud-onboard}}"
PREFIX="09-deepagents-code-tavily-opt-in"
REPO="${REPO:-$(pwd)}"
CLI="${NEMOCLAW_E2E_CLI:-${REPO}/bin/nemoclaw.js}"

ok() { printf '%s\n' "${PREFIX}: OK ($*)"; }
info() { printf '%s\n' "${PREFIX}: $*"; }
fail_test() {
printf '%s\n' "${PREFIX}: FAIL: $1" >&2
FAILED=$((FAILED + 1))
}
pass() {
ok "$1"
PASSED=$((PASSED + 1))
}

sandbox_exec() {
openshell sandbox exec --name "$SANDBOX_NAME" -- bash -c "$1" 2>&1
}

nemoclaw_cli() {
if [ -f "$CLI" ]; then
node "$CLI" "$@"
else
nemoclaw "$@"
fi
}

python_probe() {
local url="$1"
sandbox_exec "python3 - ${url@Q} <<'PY'
import sys
import urllib.error
import urllib.request

DENIAL_MARKERS = (
'access denied',
'blocked by',
'connection forbidden',
'egress denied',
'network is unreachable',
'network policy',
'operation not permitted',
'permission denied',
'policy denied',
'tunnel connection failed',
)


def is_policy_denial(text):
lowered = text.lower()
return any(marker in lowered for marker in DENIAL_MARKERS)


url = sys.argv[1]
try:
with urllib.request.urlopen(url, timeout=8) as response:
print(f'REACHED:{response.status}')
except urllib.error.HTTPError as exc:
body = ''
try:
body = exc.read(512).decode('utf-8', 'replace')
except Exception:
body = ''
details = f'{exc} {body}'.strip()
if is_policy_denial(details):
print(f'BLOCKED:HTTPError:{details}')
else:
print(f'REACHED:{exc.code}')
except urllib.error.URLError as exc:
details = str(exc.reason if getattr(exc, 'reason', None) is not None else exc)
if is_policy_denial(details):
print(f'BLOCKED:URLError:{details}')
else:
print(f'ERROR:URLError:{details}')
except OSError as exc:
details = str(exc)
if is_policy_denial(details):
print(f'BLOCKED:{type(exc).__name__}:{details}')
else:
print(f'ERROR:{type(exc).__name__}:{details}')
except Exception as exc:
print(f'ERROR:{type(exc).__name__}:{exc}')
PY
"
}

PASSED=0
FAILED=0

if ! sandbox_exec "test -d /sandbox/.deepagents && command -v dcode >/dev/null 2>&1" >/dev/null; then
info "SKIP: sandbox '${SANDBOX_NAME}' is not a Deep Agents Code sandbox"
exit 0
fi

info "Running Deep Agents Code Tavily opt-in check in sandbox: $SANDBOX_NAME"

# shellcheck disable=SC2016 # command substitution must run inside the sandbox.
PYTHON_REAL="$(sandbox_exec 'readlink -f "$(command -v python3)"' || true)"
if [[ "$PYTHON_REAL" == /opt/venv/* ]]; then
pass "sandbox python resolves through the managed Deep Agents Code venv"
else
fail_test "sandbox python does not resolve through /opt/venv: $PYTHON_REAL"
fi

DRY_RUN_OUTPUT="$(nemoclaw_cli "$SANDBOX_NAME" policy-add tavily --dry-run 2>&1)" || {
fail_test "policy-add tavily --dry-run failed: $DRY_RUN_OUTPUT"
printf '%s\n' "${PREFIX}: $PASSED passed, $FAILED failed"
exit 1
}
if echo "$DRY_RUN_OUTPUT" | grep -q "api.tavily.com"; then
pass "tavily dry-run shows api.tavily.com"
else
fail_test "tavily dry-run did not show api.tavily.com: $DRY_RUN_OUTPUT"
fi

APPLY_OUTPUT="$(nemoclaw_cli "$SANDBOX_NAME" policy-add tavily --yes 2>&1)" || {
fail_test "policy-add tavily failed: $APPLY_OUTPUT"
printf '%s\n' "${PREFIX}: $PASSED passed, $FAILED failed"
exit 1
}
pass "tavily policy preset applies"

sleep "${NEMOCLAW_E2E_POLICY_SETTLE_SECONDS:-5}"

PROBE_OUTPUT="$(python_probe "https://api.tavily.com/")"
if echo "$PROBE_OUTPUT" | grep -q "REACHED:"; then
pass "managed Deep Agents Code python can reach Tavily after policy-add"
elif echo "$PROBE_OUTPUT" | grep -q "BLOCKED:"; then
fail_test "managed Deep Agents Code python is still policy-blocked after policy-add: $PROBE_OUTPUT"
else
fail_test "Tavily probe lacked reachability evidence after policy-add: $PROBE_OUTPUT"
fi

printf '%s\n' "${PREFIX}: $PASSED passed, $FAILED failed"
[ "$FAILED" -eq 0 ] || exit 1
17 changes: 17 additions & 0 deletions test/langchain-deepagents-code-image.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,10 +467,27 @@ describe("LangChain Deep Agents Code image contracts", () => {
expect(secretBoundaryCheck).toContain("assert_no_rejected_interval_audit_logs");
expect(secretBoundaryCheck).toContain("assert_no_rejected_interval_network_logs");
expect(secretBoundaryCheck).toContain("sha256sum ${DEEPAGENTS_ENV_FILE@Q}");
const tavilyOptInCheck = fs.readFileSync(
path.join(
process.cwd(),
"test",
"e2e",
"e2e-cloud-experimental",
"checks",
"09-deepagents-code-tavily-opt-in.sh",
),
"utf8",
);
expect(tavilyOptInCheck).toContain("policy-add tavily --dry-run");
expect(tavilyOptInCheck).toContain("policy-add tavily --yes");
expect(tavilyOptInCheck).toContain("https://api.tavily.com/");
expect(tavilyOptInCheck).toContain("/opt/venv/");
expect(tavilyOptInCheck).toContain("managed Deep Agents Code python can reach Tavily");
expect(cloudExperimentalChecksForOnboarding("cloud-langchain-deepagents-code")).toEqual([
"test/e2e/e2e-cloud-experimental/checks/05-deepagents-code-landlock-readonly.sh",
"test/e2e/e2e-cloud-experimental/checks/06-deepagents-code-python-egress.sh",
"test/e2e/e2e-cloud-experimental/checks/08-deepagents-code-secret-boundary.sh",
"test/e2e/e2e-cloud-experimental/checks/09-deepagents-code-tavily-opt-in.sh",
]);
});

Expand Down
2 changes: 1 addition & 1 deletion test/policies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ describe("policies", () => {
"public-reference",
"pypi",
"slack",
"tavily",
"teams",
"telegram",
"weather",
Expand Down Expand Up @@ -175,7 +176,6 @@ describe("policies", () => {
expect(content).toContain("/usr/bin/node");
}
});

it("whatsapp preset routes web.whatsapp.com as a raw L4 tunnel with TLS pass-through", () => {
// The /ws/chat upgrade is HTTP/1.1-only; if the proxy terminates TLS it
// negotiates h2 ALPN with Meta's edge and the WS upgrade fails (Meta
Expand Down
58 changes: 58 additions & 0 deletions test/tavily-preset.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from "vitest";
import YAML from "yaml";
import * as policies from "../dist/lib/policy";

type TavilyEndpoint = {
host: string;
port: number;
protocol: string;
enforcement: string;
rules: Array<{ allow: { method: string; path: string } }>;
tls?: string;
};

type TavilyPolicy = {
endpoints?: TavilyEndpoint[];
binaries?: Array<{ path: string }>;
access?: string;
};

describe("tavily opt-in preset", () => {
it("declares narrow api.tavily.com egress for the interpreter binaries it allows", () => {
const tavily = policies.loadPreset("tavily");
expect(tavily).not.toBeNull();
const content = String(tavily);
const parsed = YAML.parse(content) as {
network_policies?: {
tavily?: TavilyPolicy;
};
};
const policy = parsed.network_policies?.tavily;

expect(policy?.endpoints).toEqual([
{
host: "api.tavily.com",
port: 443,
protocol: "rest",
enforcement: "enforce",
rules: [
{ allow: { method: "GET", path: "/**" } },
{ allow: { method: "POST", path: "/**" } },
],
},
]);
expect(policy?.binaries).toEqual([
{ path: "/opt/venv/bin/python3*" },
{ path: "/usr/bin/python3*" },
{ path: "/usr/local/bin/python3*" },
{ path: "/usr/local/bin/node" },
{ path: "/usr/bin/node" },
{ path: "/usr/bin/curl" },
]);
expect(policy).not.toHaveProperty("access", "full");
expect(policy?.endpoints?.[0]).not.toHaveProperty("tls", "skip");
});
});
Loading