Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7b9ef8f
refactor(openshell): add selected runtime foundation
apurvvkumaria Sep 1, 2026
69d8770
refactor(mcp): bind lifecycle to recorded runtime
apurvvkumaria Sep 1, 2026
69554c4
merge: refresh selected runtime foundation from main
apurvvkumaria Sep 1, 2026
c0699c2
merge: refresh selected runtime foundation from main
apurvvkumaria Sep 1, 2026
c83432d
merge: refresh MCP target stack from foundation
apurvvkumaria Sep 1, 2026
ce876c2
merge: resolve conflicts with main
github-actions[bot] Sep 2, 2026
2581d38
merge: resolve conflicts with main
github-actions[bot] Sep 3, 2026
1b3f817
Merge remote-tracking branch 'origin/main' into repair/pr-10814
prekshivyas Sep 3, 2026
dbcb8c8
fix(openshell): fail closed for selected exec
prekshivyas Sep 3, 2026
98346ab
refactor(mcp): bind lifecycle to recorded runtime
apurvvkumaria Sep 1, 2026
6a48846
merge: refresh selected runtime foundation from main
prekshivyas Sep 3, 2026
bcb228c
fix(openshell): fail closed for selected exec
prekshivyas Sep 3, 2026
a371313
chore(stack): retain PR 10815 branch ancestry
prekshivyas Sep 3, 2026
acb29a7
Merge origin/main into codex/10621-target-foundation
prekshivyas Sep 3, 2026
628e8eb
chore(stack): include refreshed PR 10814 ancestry
prekshivyas Sep 3, 2026
dd66faf
merge: incorporate upstream selected runtime repair
prekshivyas Sep 3, 2026
1cb9f77
fix(mcp): pin snapshot projection repair target
prekshivyas Sep 3, 2026
94305e0
chore(stack): include refreshed PR 10814 ancestry
prekshivyas Sep 3, 2026
1b43043
merge: refresh MCP target binding from parent
prekshivyas Sep 4, 2026
4d4adee
merge: reconcile concurrent MCP target repair
prekshivyas Sep 4, 2026
249534d
Merge branch 'main' into codex/10621-mcp-target
prekshivyas Sep 4, 2026
2ca0dca
merge: integrate latest upstream MCP branch
prekshivyas Sep 4, 2026
95312e6
refactor(destroy): pin recorded runtime target (#10816)
apurvvkumaria Sep 4, 2026
f02613f
refactor(rebuild): preserve recorded runtime target (#10817)
apurvvkumaria Sep 4, 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
15 changes: 15 additions & 0 deletions src/lib/actions/sandbox/gateway-target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { GATEWAY_PORT } from "../../core/ports";
import {
resolveGatewayName,
resolveGatewayPortFromName,
resolveSandboxGatewayName,
type SandboxGatewayBinding,
} from "../../onboard/gateway-binding";
Expand Down Expand Up @@ -31,6 +32,20 @@ export function getPersistedSandboxTargetGatewayName(sandbox: SandboxGatewayBind
return resolveSandboxGatewayName(sandbox);
}

/** Resolve the complete canonical gateway binding from one persisted sandbox row. */
export function getPersistedSandboxTargetGateway(sandbox: SandboxGatewayBinding): {
gatewayName: string;
gatewayPort: number;
selectedInProcess: boolean;
} {
const gatewayName = getPersistedSandboxTargetGatewayName(sandbox);
const gatewayPort = resolveGatewayPortFromName(gatewayName);
if (gatewayPort === null) {
throw new Error(`Invalid persisted OpenShell gateway '${gatewayName}'.`);
}
return { gatewayName, gatewayPort, selectedInProcess: gatewayPort === GATEWAY_PORT };
}

export function gatewayNamePattern(gatewayName: string): RegExp {
return new RegExp(
`Gateway:\\s+${gatewayName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?=\\s|$)`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@
// SPDX-License-Identifier: Apache-2.0

import { McpBridgeError } from "./mcp-bridge-contracts";
import type { McpProviderInspectionRuntimeSelection } from "./mcp-bridge-provider-inspection";
import { executeSandboxCommand } from "./process-recovery";

const DEEPAGENTS_MCP_CAPABILITY_MARKER = "NEMOCLAW_DEEPAGENTS_MCP_CAPABILITY=2";
const DEEPAGENTS_MCP_CAPABILITY_COMMAND =
"/usr/local/bin/deepagents-code --nemoclaw-mcp-capability";

export function assertDeepAgentsMcpMutationRuntimeCapability(sandboxName: string): void {
const result = executeSandboxCommand(sandboxName, DEEPAGENTS_MCP_CAPABILITY_COMMAND);
export function assertDeepAgentsMcpMutationRuntimeCapability(
sandboxName: string,
runtimeSelection: McpProviderInspectionRuntimeSelection,
): void {
const result = executeSandboxCommand(sandboxName, DEEPAGENTS_MCP_CAPABILITY_COMMAND, {
runtimeSelection,
});
if (result?.status !== 0 || result.stdout.trim() !== DEEPAGENTS_MCP_CAPABILITY_MARKER) {
throw new McpBridgeError(
`LangChain Deep Agents Code sandbox '${sandboxName}' does not contain managed MCP capability v2. Rebuild the sandbox before changing authenticated MCP state.`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@ import type { McpBridgeEntry } from "../../state/registry";
import type { AdapterMutationOptions } from "./mcp-bridge-adapter-inspection";
import { McpBridgeError } from "./mcp-bridge-contracts";
import { redactBridgeSecretsForDisplay } from "./mcp-bridge-output";
import type { McpProviderInspectionRuntimeSelection } from "./mcp-bridge-provider-inspection";
import { executeSandboxCommand } from "./process-recovery";

export function runDeepAgentsAdapterCommand(
sandboxName: string,
entry: Pick<McpBridgeEntry, "env">,
command: string,
failureMessage: string,
runtimeSelection: McpProviderInspectionRuntimeSelection,
options: AdapterMutationOptions = {},
): string {
const result = executeSandboxCommand(sandboxName, command);
const result = executeSandboxCommand(sandboxName, command, { runtimeSelection });
const output = redactBridgeSecretsForDisplay(
[result?.stdout, result?.stderr].filter(Boolean).join("\n").trim(),
entry,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import type { McpBridgeEntry } from "../../state/registry";
import type { McpAttachedCredentialRevision } from "./mcp-bridge-provider-readiness";
import type { McpProviderInspectionRuntimeSelection } from "./mcp-bridge-provider-inspection";
import {
type AdapterRegistrationInspection,
inspectAdapterRegistrationCommand,
Expand All @@ -12,11 +13,13 @@ import { buildDeepAgentsMcpStatusCommand } from "./mcp-bridge-adapter-status";
export function inspectDeepAgentsAdapterRegistration(
sandboxName: string,
entry: McpBridgeEntry,
runtimeSelection: McpProviderInspectionRuntimeSelection,
credentialRevision?: McpAttachedCredentialRevision,
): AdapterRegistrationInspection {
return inspectAdapterRegistrationCommand(
sandboxName,
entry,
buildDeepAgentsMcpStatusCommand(entry, credentialRevision),
runtimeSelection,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
pythonJsonLiteral,
} from "./mcp-bridge-adapter-status";
import type { McpAttachedCredentialRevision } from "./mcp-bridge-provider-readiness";
import type { McpProviderInspectionRuntimeSelection } from "./mcp-bridge-provider-inspection";
import { McpBridgeError } from "./mcp-bridge-contracts";

export function buildDeepAgentsMcpRegisterCommand(
Expand Down Expand Up @@ -224,9 +225,15 @@
function verifyDeepAgentsAdapterRegistration(
sandboxName: string,
entry: McpBridgeEntry,
runtimeSelection: McpProviderInspectionRuntimeSelection,
credentialRevision?: McpAttachedCredentialRevision,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
): void {
const inspection = inspectDeepAgentsAdapterRegistration(sandboxName, entry, credentialRevision);
const inspection = inspectDeepAgentsAdapterRegistration(
sandboxName,
entry,
runtimeSelection,
credentialRevision,
);
if (inspection.state === "registered") return;
const detail = inspection.state === "error" ? inspection.detail : inspection.state;
throw new McpBridgeError(
Expand All @@ -237,6 +244,7 @@
export function registerDeepAgentsAdapter(
sandboxName: string,
entry: McpBridgeEntry,
runtimeSelection: McpProviderInspectionRuntimeSelection,
envValues: Record<string, string> = {},
replaceExisting = false,
teardownRollback = false,
Expand All @@ -253,6 +261,7 @@
credentialRevision,
),
`Deep Agents Code MCP config registration failed for '${entry.server}'.`,
runtimeSelection,
{ envValues },
);
if (teardownRollback) {
Expand All @@ -262,7 +271,7 @@
);
}
} else {
verifyDeepAgentsAdapterRegistration(sandboxName, entry, credentialRevision);
verifyDeepAgentsAdapterRegistration(sandboxName, entry, runtimeSelection, credentialRevision);
}
}

Expand All @@ -285,7 +294,7 @@
}
const entry = managedEntries[0];
const commandEntry: Pick<McpBridgeEntry, "env"> = entry ?? { env: [] };
const runtimeKind = runDeepAgentsAdapterCommand(

Check failure on line 297 in src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts

View workflow job for this annotation

GitHub Actions / installer-integration

Expected 5-6 arguments, but got 4.
sandboxName,
commandEntry,
buildDeepAgentsMcpRuntimeKindCommand(),
Expand All @@ -298,8 +307,8 @@
if (runtimeKind !== "v2") {
throw new McpBridgeError("Could not identify the managed Deep Agents MCP runtime.");
}
assertDeepAgentsMcpMutationRuntimeCapability(sandboxName);

Check failure on line 310 in src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts

View workflow job for this annotation

GitHub Actions / installer-integration

Expected 2 arguments, but got 1.
runDeepAgentsAdapterCommand(

Check failure on line 311 in src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts

View workflow job for this annotation

GitHub Actions / installer-integration

Expected 5-6 arguments, but got 4.
sandboxName,
commandEntry,
buildDeepAgentsMcpRegisterCommand(entry, true, managedEntries, false, undefined, {
Expand All @@ -308,6 +317,6 @@
"Deep Agents Code managed MCP projection repair failed.",
);
for (const managedEntry of managedEntries) {
verifyDeepAgentsAdapterRegistration(sandboxName, managedEntry);

Check failure on line 320 in src/lib/actions/sandbox/mcp-bridge-adapter-deepagents-registration.ts

View workflow job for this annotation

GitHub Actions / installer-integration

Expected 3-4 arguments, but got 2.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import type { McpBridgeEntry } from "../../state/registry";
import { runDeepAgentsAdapterCommand } from "./mcp-bridge-adapter-deepagents-command";
import type { McpProviderInspectionRuntimeSelection } from "./mcp-bridge-provider-inspection";
import {
DEEPAGENTS_LEGACY_CONFIG_HELPERS,
DEEPAGENTS_LEGACY_MCP_CONFIG_PATH,
Expand Down Expand Up @@ -167,13 +168,15 @@ export function buildDeepAgentsMcpRemoveCommand(
export function unregisterDeepAgentsAdapter(
sandboxName: string,
entry: McpBridgeEntry,
runtimeSelection: McpProviderInspectionRuntimeSelection,
options: AdapterMutationOptions = {},
): AdapterRemovalOutcome {
const stdout = runDeepAgentsAdapterCommand(
sandboxName,
entry,
buildDeepAgentsMcpRemoveCommand(entry, options.force === true, options.teardown === true),
`Deep Agents Code MCP config removal failed for '${entry.server}'.`,
runtimeSelection,
options,
);
const marker = stdout.match(/NEMOCLAW_DEEPAGENTS_MCP_REMOVAL=(removed|absent|unowned)/);
Expand Down
88 changes: 82 additions & 6 deletions src/lib/actions/sandbox/mcp-bridge-adapter-hermes-branding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,109 @@

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { setProviderCommandRuntimeHooksForTest } from "../../adapters/openshell/provider-command";
import type { McpBridgeEntry } from "../../state/registry";

const mocks = vi.hoisted(() => ({
runOpenshellProviderCommand: vi.fn(),
getSandboxOrThrow: vi.fn(),
runOpenshell: vi.fn(),
}));

vi.mock("../../adapters/openshell/provider-command", () => ({
runOpenshellProviderCommand: mocks.runOpenshellProviderCommand,
vi.mock("./mcp-bridge-state", () => ({
getSandboxOrThrow: mocks.getSandboxOrThrow,
}));

import { assertHermesMcpMutationRuntimeCapability } from "./mcp-bridge-adapter-hermes";
import {
assertHermesMcpMutationRuntimeCapability,
unregisterHermesAdapter,
} from "./mcp-bridge-adapter-hermes";

const entry: McpBridgeEntry = {
server: "github",
agent: "hermes",
adapter: "hermes-config",
url: "https://api.githubcopilot.com/mcp/",
env: ["GITHUB_TOKEN"],
providerName: "alpha-mcp-github",
providerId: "11111111-2222-4333-8444-555555555555",
policyName: "mcp-bridge-github",
addedAt: new Date(0).toISOString(),
};

const runtimeSelection = { gatewayName: "nemoclaw-8091", workspace: "default" };

describe("Hermes MCP recovery guidance", () => {
beforeEach(() => {
vi.stubEnv("NEMOCLAW_INVOKED_AS", "nemohermes");
mocks.runOpenshellProviderCommand.mockReset().mockReturnValue({
mocks.getSandboxOrThrow.mockReset().mockReturnValue({
agent: "hermes",
gatewayName: "nemoclaw-8091",
name: "alpha",
});
mocks.runOpenshell.mockReset().mockReturnValue({
status: 1,
stdout: "",
stderr: "Hermes gateway is not running under the managed service lifecycle",
});
setProviderCommandRuntimeHooksForTest({ runOpenshell: mocks.runOpenshell });
});

afterEach(() => {
setProviderCommandRuntimeHooksForTest({});
vi.unstubAllEnvs();
});

it("uses the invoked CLI name when the managed lifecycle is unavailable", () => {
expect(() => assertHermesMcpMutationRuntimeCapability("alpha")).toThrow(
expect(() => assertHermesMcpMutationRuntimeCapability("alpha", runtimeSelection)).toThrow(
"Run `nemohermes alpha recover` and retry.",
);
});

it("pins Hermes MCP lifecycle commands to the recorded runtime target (#10514)", () => {
vi.stubEnv("OPENSHELL_GATEWAY", "ambient-gateway");
vi.stubEnv("OPENSHELL_GATEWAY_ENDPOINT", "https://ambient.invalid");
vi.stubEnv("OPENSHELL_GATEWAY_INSECURE", "true");
vi.stubEnv("OPENSHELL_LOCAL_TLS_DIR", "/ambient/tls");
vi.stubEnv("OPENSHELL_TOKEN", "ambient-token");
vi.stubEnv("OPENSHELL_WORKSPACE", "ambient-workspace");
mocks.runOpenshell.mockImplementation((_args, options) => {
expect(options).toEqual(
expect.objectContaining({
env: expect.objectContaining({
OPENSHELL_GATEWAY: "nemoclaw-8091",
OPENSHELL_WORKSPACE: "default",
}),
replaceEnv: true,
}),
);
expect(options?.env).not.toHaveProperty("OPENSHELL_GATEWAY_ENDPOINT");
expect(options?.env).not.toHaveProperty("OPENSHELL_GATEWAY_INSECURE");
expect(options?.env).not.toHaveProperty("OPENSHELL_LOCAL_TLS_DIR");
expect(options?.env).not.toHaveProperty("OPENSHELL_TOKEN");
return {
status: 0,
stdout: JSON.stringify({ changed: true, ok: true, reloaded: true }),
stderr: "",
};
});

expect(() =>
assertHermesMcpMutationRuntimeCapability("alpha", runtimeSelection),
).not.toThrow();
expect(() => unregisterHermesAdapter("alpha", entry, runtimeSelection)).not.toThrow();
expect(mocks.runOpenshell).toHaveBeenCalledTimes(2);
});

it("refuses host-local recovery when the selected Hermes gateway is not ready (#10514)", () => {
mocks.runOpenshell.mockReturnValue({
status: 1,
stdout: "",
stderr: "Hermes gateway is not running for managed MCP reload",
});

expect(() => assertHermesMcpMutationRuntimeCapability("alpha", runtimeSelection)).toThrow(
"NemoClaw did not attempt host-local supervisor recovery.",
);
expect(mocks.runOpenshell).toHaveBeenCalledTimes(3);
});
});
Loading
Loading