Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
dd3d9cc
chore: start main E2E remediation draft
prekshivyas Aug 23, 2026
66ff91c
fix(policy): omit inactive Hermes messaging bindings
prekshivyas Aug 23, 2026
1bb1b1b
test(e2e): use exact Hermes Discord provider
prekshivyas Aug 23, 2026
679f629
fix(channels): reattach providers before policy
prekshivyas Aug 23, 2026
cd6fe08
test(e2e): align upgrade credential boundary
apurvvkumaria Aug 23, 2026
800ad98
fix(deepagents): activate MCP progressive disclosure
prekshivyas Aug 23, 2026
f3ab1d1
test(e2e): exercise Hermes late MCP discovery
prekshivyas Aug 23, 2026
3882c3b
fix(onboard): settle pairing for custom images
prekshivyas Aug 23, 2026
2032df2
fix(onboard): release exact deleting replacement
prekshivyas Aug 23, 2026
e370c93
test(e2e): bind fake messaging credentials
prekshivyas Aug 23, 2026
f21be23
Merge remote-tracking branch 'origin/main' into draft/main-e2e-remedi…
prekshivyas Aug 23, 2026
4103ae0
fix(onboard): produce scoped pairing before observation
prekshivyas Aug 23, 2026
46c0370
test(e2e): send initial Hermes Discord identify
prekshivyas Aug 23, 2026
a0a6d29
test(e2e): bind stock timestamp to quote source
prekshivyas Aug 23, 2026
6525265
merge: update draft remediation from main
prekshivyas Aug 23, 2026
ea8eed3
Merge origin/main into draft/main-e2e-remediation
prekshivyas Aug 23, 2026
9aeecc8
fix(channels): compensate unconfirmed attachments
prekshivyas Aug 23, 2026
5b9b217
test(e2e): clean up Hermes rebuild swap
prekshivyas Aug 23, 2026
874d6e2
fix(images): bind managed builds to target architecture
prekshivyas Aug 23, 2026
030d9b0
fix(e2e): wait for managed image publication
prekshivyas Aug 23, 2026
a171c04
fix(e2e): close remaining main failure gaps
prekshivyas Aug 23, 2026
facce3c
fix(rebuild): preserve gateway-held channels
prekshivyas Aug 23, 2026
ce35626
fix(e2e): bind Jetson to managed publication
prekshivyas Aug 23, 2026
b8462e1
merge: retain concurrent E2E fixes
prekshivyas Aug 23, 2026
c26ca33
refactor(channels): isolate gateway binding check
prekshivyas Aug 23, 2026
5a1fb04
refactor(channels): preserve architecture budgets
prekshivyas Aug 23, 2026
750fe60
refactor(e2e): isolate Hermes swap lifecycle
prekshivyas Aug 23, 2026
4f5ec86
fix(e2e): close latest review and image failures
prekshivyas Aug 23, 2026
83c6af0
Merge concurrent remediation updates
prekshivyas Aug 23, 2026
9a31906
Merge latest concurrent remediation updates
prekshivyas Aug 23, 2026
f142c0e
docs(messaging): document provider restoration order
prekshivyas Aug 23, 2026
af28738
fix(e2e): build generic GPU workload from PR source
prekshivyas Aug 23, 2026
7351a53
fix(e2e): close qualification review findings
prekshivyas Aug 23, 2026
21cec27
merge: resolve conflicts with main
github-actions[bot] Aug 24, 2026
6696291
Revert "fix(onboard): produce scoped pairing before observation"
prekshivyas Aug 24, 2026
6d0d2ec
Revert "fix(images): bind managed builds to target architecture"
prekshivyas Aug 24, 2026
9e9b1fa
merge: update remediation from main
prekshivyas Aug 24, 2026
58610f2
Merge branch 'main' into draft/main-e2e-remediation
prekshivyas Aug 24, 2026
681182d
refactor(e2e): narrow main remediation ownership
prekshivyas Aug 24, 2026
96f4c45
Merge remote-tracking branch 'origin/main' into codex/pr-10031
prekshivyas Aug 24, 2026
c9762e5
docs(e2e): drop removed publication retry
prekshivyas Aug 24, 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
5 changes: 3 additions & 2 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ jobs:
env:
EXPECTED_SHA: ${{ inputs.checkout_sha || github.sha }}
GITHUB_TOKEN: ${{ github.token }}
REQUIRE_MANAGED_IMAGE_PUBLICATION: "1"
shell: bash
run: |
set -euo pipefail
Expand Down Expand Up @@ -5327,8 +5328,8 @@ jobs:
run: bash .github/scripts/docker-auth-cleanup.sh

jetson-nvmap-gpu:
needs: generate-matrix
if: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.allow_jetson_dispatch && (inputs.checkout_repository == '' || inputs.checkout_repository == github.repository) && ((inputs.jobs == '' && inputs.targets == '') || contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'jetson-nvmap-gpu')))) }}
needs: [base-image-publication, generate-matrix]
if: ${{ always() && needs['base-image-publication'].result == 'success' && needs['generate-matrix'].result == 'success' && github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.allow_jetson_dispatch && (inputs.checkout_repository == '' || inputs.checkout_repository == github.repository) && ((inputs.jobs == '' && inputs.targets == '') || contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'jetson-nvmap-gpu')))) }}
concurrency:
group: jetson-nvmap-gpu-dispatch
cancel-in-progress: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -721,11 +721,15 @@ def create_cli_agent(model, assistant_id, *args, **kwargs):
)

assert_unique_callable_tool_names(
kwargs.get("tools"), kwargs.get("mcp_server_info")
)
has_loaded_mcp_tools = any(
getattr(info, "tools", ()) for info in kwargs.get("mcp_server_info") or ()
)
kwargs.get("tools"),
kwargs.get("mcp_server_info"),
kwargs.get("mcp_tools"),
)
# Deep Agents Code 0.1.55 passes the exact loaded MCP tool objects
# separately from the status-oriented server metadata. The metadata can be
# empty or lag the executable catalog, so it must not decide whether the
# progressive middleware is installed.
has_loaded_mcp_tools = bool(kwargs.get("mcp_tools"))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if has_loaded_mcp_tools:
from deepagents_code.progressive_tool_disclosure import (
progressive_tool_disclosure_enabled,
Expand Down
23 changes: 21 additions & 2 deletions agents/langchain-deepagents-code/progressive_tool_disclosure.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,15 +280,17 @@ def _tool_description(tool: BaseTool | dict[str, Any]) -> str:
def assert_unique_callable_tool_names(
tools: Sequence[object] | None,
mcp_server_info: Sequence[object] | None,
mcp_tools: Sequence[object] | None = None,
) -> None:
"""Reject ambiguous or non-managed registrations before graph creation.

The pinned runtime combines middleware and regular tools into one executor
registry keyed by resolved callable name. Its model schema selection and
executor lookup do not share the same duplicate-name rule, so accepting two
implementations can bind one schema and execute another. Keep the executor
registry and MCP metadata as separate views: one loaded MCP tool normally
appears once in each, while duplicates within either view are ambiguous.
registry, loaded MCP tools, and MCP metadata as separate views: one loaded
MCP tool normally appears in each view, while duplicates within one view are
ambiguous.
"""
collisions: set[str] = set()
registered_owners: dict[str, list[str]] = {}
Expand Down Expand Up @@ -334,6 +336,23 @@ def assert_unique_callable_tool_names(
f"({', '.join(owners)})"
)

loaded_mcp_owners: dict[str, list[str]] = {}
for index, tool in enumerate(mcp_tools or ()):
name = _tool_name(tool)
if name is None:
continue
owner = f"loaded MCP tool[{index}]"
loaded_mcp_owners.setdefault(name, []).append(owner)
if name in CORE_TOOL_NAMES:
collisions.add(f"{owner} is a non-managed owner of reserved name {name!r}")

for name, owners in loaded_mcp_owners.items():
if len(owners) > 1:
collisions.add(
f"resolved callable name {name!r} has multiple loaded MCP implementations "
f"({', '.join(owners)})"
)

if collisions:
detail = "; ".join(sorted(collisions))
raise RuntimeError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,7 @@ def probe(value: str = "") -> str:
"progressive",
[regular_a, regular_b],
[],
[],
),
"regular_mcp": (
"progressive",
Expand All @@ -786,6 +787,7 @@ def probe(value: str = "") -> str:
),
)
],
[],
),
"cross_mcp": (
"progressive",
Expand All @@ -803,11 +805,13 @@ def probe(value: str = "") -> str:
)
for server in ("alpha", "alpha_beta")
],
[],
),
"reserved_progressive": (
"progressive",
[reserved_regular],
[],
[],
),
"reserved_mcp": (
"progressive",
Expand All @@ -824,16 +828,34 @@ def probe(value: str = "") -> str:
),
)
],
[],
),
"duplicate_direct": (
"direct",
[regular_a, regular_b],
[],
[],
),
"reserved_direct": (
"direct",
[collision_tool("execute", "reserved-direct")],
[],
[],
),
"duplicate_loaded_mcp": (
"direct",
[],
[],
[
collision_tool("loaded_duplicate", "loaded-a"),
collision_tool("loaded_duplicate", "loaded-b"),
],
),
"reserved_loaded_mcp": (
"direct",
[],
[],
[collision_tool("execute", "reserved-loaded")],
),
}
original_cli_factory = agent_module._nemoclaw_original_create_cli_agent
Expand All @@ -848,14 +870,15 @@ def forbidden_original(*args: Any, **kwargs: Any) -> None:
previous = os.environ.get("NEMOCLAW_TOOL_DISCLOSURE")
try:
errors: dict[str, str] = {}
for label, (mode, tools, info) in collision_cases.items():
for label, (mode, tools, info, mcp_tools) in collision_cases.items():
os.environ["NEMOCLAW_TOOL_DISCLOSURE"] = mode
try:
create_cli_agent(
model=object(),
assistant_id="callable-namespace-validator",
tools=tools,
mcp_server_info=info,
mcp_tools=mcp_tools,
)
except RuntimeError as exc:
errors[label] = str(exc)
Expand All @@ -878,6 +901,9 @@ def forbidden_original(*args: Any, **kwargs: Any) -> None:
assert "reserved name 'search_tools'" in errors["reserved_mcp"]
assert "multiple registered implementations" in errors["duplicate_direct"]
assert "reserved name 'execute'" in errors["reserved_direct"]
assert "multiple loaded MCP implementations" in errors["duplicate_loaded_mcp"]
assert "loaded MCP tool[0]" in errors["reserved_loaded_mcp"]
assert "reserved name 'execute'" in errors["reserved_loaded_mcp"]


def _validate_direct_mode_execution() -> None:
Expand All @@ -889,6 +915,18 @@ def direct_probe(value: str) -> str:
executions.append(value)
return "direct-proof"

# Match the exact metadata shape emitted by the pinned MCP wrapper. Without
# coherent read-only hints, the headless MCP guard correctly rejects this
# fixture before the direct executor can prove the disclosure mode.
direct_probe.metadata = {
"readOnlyHint": True,
"destructiveHint": False,
"idempotentHint": True,
"openWorldHint": False,
"_deepagents_code_mcp": True,
"_deepagents_code_mcp_server": "direct-runtime-validator",
}

info = MCPServerInfo(
name="direct-runtime-validator",
transport="http",
Expand Down Expand Up @@ -916,6 +954,7 @@ def direct_probe(value: str) -> str:
enable_memory=False,
enable_skills=False,
enable_shell=False,
mcp_tools=[direct_probe],
mcp_server_info=[info],
)
agent.invoke(
Expand Down Expand Up @@ -1026,6 +1065,15 @@ def isolated_probe() -> str:
"""Return an isolated probe capability."""
return "isolated-proof"

isolated_probe.metadata = {
"readOnlyHint": True,
"destructiveHint": False,
"idempotentHint": True,
"openWorldHint": False,
"_deepagents_code_mcp": True,
"_deepagents_code_mcp_server": "runtime-validator",
}

model = ScriptedModel(scenario="subagent")
info = MCPServerInfo(
name="runtime-validator",
Expand All @@ -1049,6 +1097,7 @@ def isolated_probe() -> str:
enable_memory=False,
enable_skills=False,
enable_shell=False,
mcp_tools=[isolated_probe],
mcp_server_info=[info],
)
agent.invoke(
Expand Down
9 changes: 6 additions & 3 deletions src/lib/onboard/docker-gpu-patch-finalize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ describe("finalizeDockerGpuPatchBackup", () => {
]);
});

it("accepts Error only when the stopped replacement is the sole labeled container (#9962)", () => {
it.each(["Error", "Deleting"])(
"accepts %s only when the stopped replacement is the sole labeled container (#9962)",
(phase) => {
const replacementContainerId = "a".repeat(64);
const events: string[] = [];
const dockerStop = vi.fn(() => {
Expand All @@ -183,7 +185,7 @@ describe("finalizeDockerGpuPatchBackup", () => {
});
const runOpenshell = vi.fn(() => {
events.push("observe error");
return { status: 0, stdout: "alpha 2026-08-23 01:40:35 Error\n" };
return { status: 0, stdout: `alpha 2026-08-23 01:40:35 ${phase}\n` };
});

const outcome = finalizeDockerGpuPatchBackup(
Expand Down Expand Up @@ -216,7 +218,8 @@ describe("finalizeDockerGpuPatchBackup", () => {
]),
expect.objectContaining({ ignoreError: true }),
);
});
},
);

it("caps Error corroboration to the remaining lifecycle-release budget (#9962)", () => {
const replacementContainerId = "a".repeat(64);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/onboard/docker-gpu-patch-finalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export function finalizeDockerGpuPatchBackup(
? waitForOpenShellSandboxLifecycleRelease(sandboxName, lifecycleReleaseTimeoutSecs, {
runOpenshell: deps.runOpenshell,
sleep: deps.sleep,
soleLabeledReplacementCorroboratesError: (remainingMs) =>
soleLabeledReplacementCorroboratesRetiringPhase: (remainingMs) =>
isSoleLabeledReplacement(
sandboxName,
options.result.newContainerId,
Expand Down
23 changes: 21 additions & 2 deletions src/lib/onboard/docker-gpu-supervisor-reconnect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ describe("Docker GPU final lifecycle release", () => {
["a gateway error", "Error: gateway unavailable\n"],
["a phase-free row", "beta 2026-08-21 05:53:18\n"],
["an unrecognized phase", "beta 2026-08-21 05:53:18 Retiring\n"],
["the selected sandbox in Deleting", "alpha 2026-08-21 05:53:18 Deleting\n"],
["the selected sandbox in Ready", "alpha 2026-08-21 05:53:18 Ready\n"],
["the selected sandbox in Provisioning", "alpha 2026-08-21 05:53:18 Provisioning\n"],
["the selected sandbox in Error", "alpha 2026-08-21 05:53:18 Error\n"],
Expand All @@ -48,6 +47,26 @@ describe("Docker GPU final lifecycle release", () => {
expect(runOpenshell).toHaveBeenCalledTimes(2);
});

it.each(["Error", "Deleting"])(
"accepts a corroborated stopped replacement in %s",
(phase) => {
const corroborate = vi.fn(() => true);
const runOpenshell = vi.fn(() => ({
status: 0,
stdout: `alpha 2026-08-23 01:40:35 ${phase}\n`,
}));

expect(
waitForOpenShellSandboxLifecycleRelease("alpha", 1, {
runOpenshell,
sleep: vi.fn(),
soleLabeledReplacementCorroboratesRetiringPhase: corroborate,
}),
).toBe(true);
expect(corroborate).toHaveBeenCalledOnce();
},
);

it.each([
["a failed probe", { status: 1, stderr: "gateway unavailable" }],
["a probe without an exit status", { status: null, stderr: "timed out" }],
Expand Down Expand Up @@ -80,7 +99,7 @@ describe("Docker GPU final lifecycle release", () => {
waitForOpenShellSandboxLifecycleRelease("alpha", 1, {
runOpenshell,
sleep: vi.fn(),
soleLabeledReplacementCorroboratesError: corroborate,
soleLabeledReplacementCorroboratesRetiringPhase: corroborate,
}),
).toBe(false);
} finally {
Expand Down
32 changes: 19 additions & 13 deletions src/lib/onboard/docker-gpu-supervisor-reconnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,13 @@ type DockerLifecycleReleaseDeps = Pick<
"runOpenshell" | "sleep"
> & {
/**
* Corroborating evidence for an Error row from a Docker query that confirms
* the transaction-owned replacement is the sole labeled sandbox container.
* Corroborating evidence for an Error or Deleting row from a Docker query
* that confirms the transaction-owned replacement is the sole labeled
* sandbox container.
* The callback must fail closed and keep its child within the supplied
* remaining lifecycle-release budget.
*/
soleLabeledReplacementCorroboratesError?: (remainingMs: number) => boolean;
soleLabeledReplacementCorroboratesRetiringPhase?: (remainingMs: number) => boolean;
};

/**
Expand All @@ -97,10 +98,12 @@ type DockerLifecycleReleaseDeps = Pick<
* OpenShell processes the stale deletion before the new registration.
* - The caller enters this wait only after the replacement reached Ready and
* was deliberately stopped. A successful list normally omits the sandbox
* name. An Error row is also sufficient only when a separate bounded Docker
* query confirms that exact stopped replacement is the sole remaining
* labeled container. This corroborates the release condition; the OpenShell
* row alone is not an identity-bound ownership receipt.
* name. An Error or Deleting row is also sufficient only when a separate
* bounded Docker query confirms that exact stopped replacement is the sole
* remaining labeled container. This corroborates the release condition;
* the OpenShell row alone is not an identity-bound ownership receipt. The
* Deleting case breaks the otherwise circular wait where OpenShell retains
* the row until that exact replacement emits its restart event.
* - `waits for the sandbox name to disappear before restarting the
* replacement (#9531)` protects the event order. `rejects final handoff when
* OpenShell never releases the deleting lifecycle record (#9531)` protects
Expand Down Expand Up @@ -132,19 +135,22 @@ export function waitForOpenShellSandboxLifecycleRelease(
const output = String(result.stdout ?? "").trim();
const entries = parseLiveSandboxEntries(output);
const sandboxPresent = entries.some((entry) => entry.name === sandboxName);
const stoppedReplacementError = entries.some(
(entry) => entry.name === sandboxName && entry.phase === "Error",
const stoppedReplacementRetiring = entries.some(
(entry) =>
entry.name === sandboxName && (entry.phase === "Error" || entry.phase === "Deleting"),
);
const hasPhaseBearingEntry = entries.some((entry) => entry.phase !== null);
const explicitEmptyList = output === "No sandboxes found" || output === "No sandboxes found.";
const remainingBeforeCorroborationMs = deadline - Date.now();
const soleLabeledReplacementCorroboratesError =
stoppedReplacementError &&
const soleLabeledReplacementCorroboratesRetiringPhase =
stoppedReplacementRetiring &&
remainingBeforeCorroborationMs > 0 &&
deps.soleLabeledReplacementCorroboratesError?.(remainingBeforeCorroborationMs) === true;
deps.soleLabeledReplacementCorroboratesRetiringPhase?.(
remainingBeforeCorroborationMs,
) === true;
if (
explicitEmptyList ||
soleLabeledReplacementCorroboratesError ||
soleLabeledReplacementCorroboratesRetiringPhase ||
(hasPhaseBearingEntry && !sandboxPresent)
) {
return true;
Expand Down
Loading
Loading