Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e94c514
fix(ci): align managed MCP supervisor version
prekshivyas Aug 20, 2026
e769c75
ci(mcp): classify OpenShell revision failure
prekshivyas Aug 20, 2026
e950965
ci(mcp): require preset failure evidence
prekshivyas Aug 20, 2026
52b23ee
fix(ci): bind MCP waiver to candidate commit
senthilr-nv Aug 20, 2026
290799f
ci(mcp): remove OpenShell revision waiver
prekshivyas Aug 20, 2026
c6e8280
test(ci): require managed MCP supervisor pins
prekshivyas Aug 20, 2026
168cff5
fix(ci): preserve managed MCP qualification environment
prekshivyas Aug 20, 2026
9b0725d
fix(ci): activate exact managed MCP image
prekshivyas Aug 20, 2026
fcfad8e
test(mcp): classify credential sync failures
prekshivyas Aug 21, 2026
838e177
fix(mcp): classify gateway inference credentials
prekshivyas Aug 21, 2026
636409e
fix(mcp): resolve revisioned discovery credentials
prekshivyas Aug 21, 2026
e4c5537
fix(mcp): project live credential revision to mcporter
prekshivyas Aug 21, 2026
5df42c1
ci(mcp): qualify bridge changes in managed images
prekshivyas Aug 21, 2026
00aea74
Merge remote-tracking branch 'origin/main' into codex/fix-mcp-live-cr…
prekshivyas Aug 21, 2026
6d1108f
test(mcp): scope managed image discovery topology
prekshivyas Aug 21, 2026
f250e90
fix(mcp): use live revision in rewrite probe
prekshivyas Aug 21, 2026
47a3a18
merge(main): refresh PR branch
senthilr-nv Aug 21, 2026
2432b0d
test(mcp): track full-scope raw policy proof
senthilr-nv Aug 21, 2026
52b2db1
merge(main): include latest base update
senthilr-nv Aug 21, 2026
5f5266c
fix(rebuild): restore MCP policies through owned lifecycle
prekshivyas Aug 21, 2026
75aefaf
fix(onboard): preserve journaled policy selection
prekshivyas Aug 21, 2026
a51adb1
fix(rebuild): carry filtered MCP policy presets
prekshivyas Aug 21, 2026
6c05be2
fix(mcp): verify exact adapter credential revision
prekshivyas Aug 21, 2026
bccdcf7
fix(rebuild): reseed policies after owned delete
cjagwani Aug 21, 2026
83e992c
fix(ci): wait for public managed digest
cjagwani Aug 21, 2026
4136947
test(e2e): expose MCP rebuild diagnostics
cjagwani Aug 21, 2026
eea65ac
fix(rebuild): persist filtered policy resume intent
jyaunches Aug 21, 2026
d79472d
fix(rebuild): preserve journaled MCP policy ownership
cjagwani Aug 21, 2026
992f81d
Merge branch 'main' into codex/fix-mcp-live-credential-projection
cjagwani Aug 21, 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
1 change: 1 addition & 0 deletions .github/workflows/base-image.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ on:
- "nemoclaw-blueprint/**"
- "scripts/**"
- "test/e2e/live/managed-image-activation-e2e*.ts"
- "src/lib/actions/sandbox/mcp-bridge-*.ts"
- "src/lib/actions/sandbox/openshell-child-visible-credentials.v*.json"
- "src/lib/core/json-types.ts"
- "src/lib/core/ports.ts"
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/managed-images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ on:
- "nemoclaw-blueprint/**"
- "scripts/**"
- "src/lib/actions/sandbox/openshell-child-visible-credentials.v*.json"
- "src/lib/actions/sandbox/mcp-bridge-*.ts"
- "src/lib/core/json-types.ts"
- "src/lib/core/ports.ts"
- "src/lib/messaging/**"
Expand Down Expand Up @@ -832,12 +833,12 @@ jobs:
}
reference="${IMAGE}@${DIGEST}"
raw="$RUNNER_TEMP/${AGENT}-published-manifest.raw"
scripts/checks/pull-public-exact-digest.sh "$reference" linux/amd64
docker buildx imagetools inspect "$reference" --raw > "$raw"
[[ "sha256:$(sha256sum "$raw" | awk '{print $1}')" == "$DIGEST" ]] || {
echo "ERROR: published PR manifest bytes do not match the build digest" >&2
exit 1
}
scripts/checks/pull-public-exact-digest.sh "$reference" linux/amd64
release="v$(node -p 'require("./package.json").version')"
contract_dir="$RUNNER_TEMP/managed-pr-contract"
mkdir -p "$contract_dir"
Expand Down Expand Up @@ -957,7 +958,7 @@ jobs:
retention-days: 1

pr-openclaw-mcp-discovery:
name: PR exact OpenClaw trusted-private MCP discovery (pass ${{ matrix.pass }})
name: PR exact OpenClaw managed-image MCP discovery (pass ${{ matrix.pass }})
needs: pr-build-and-entrypoint
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-24.04
Expand All @@ -979,11 +980,12 @@ jobs:
NEMOCLAW_E2E_REQUIRE_EXECUTED_TEST: "1"
NEMOCLAW_E2E_SHARD: openclaw
NEMOCLAW_MCP_BRIDGE_AGENT: openclaw
NEMOCLAW_MCP_BRIDGE_E2E_SCOPE: managed-image-discovery
NEMOCLAW_NON_INTERACTIVE: "1"
NEMOCLAW_OPENSHELL_EXACT_MAIN_PROOF: "1"
NEMOCLAW_RUN_LIVE_E2E: "1"
NEMOCLAW_SANDBOX_NAME: e2e-pr-exact-mcp-${{ matrix.pass }}
OPENSHELL_DOCKER_SUPERVISOR_IMAGE: ghcr.io/nvidia/openshell/supervisor@sha256:b58be5e40c788977ffa0e8305a8cad9c656efdf1a3fe182582a00ca870bb0edb
OPENSHELL_DOCKER_SUPERVISOR_IMAGE: ghcr.io/nvidia/openshell/supervisor@sha256:722f44669722961b7f432b0b81de25b91a58f34a61d6403bef967acaf2b3af01
steps:
- name: Checkout exact PR head
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -1056,7 +1058,7 @@ jobs:
NEMOCLAW_OPENSHELL_FORCE_INSTALL: "1"
run: bash scripts/install-openshell.sh

- name: Run exact OpenClaw trusted-private MCP discovery
- name: Run exact OpenClaw managed-image MCP discovery
shell: bash
run: |
set -euo pipefail
Expand Down
2 changes: 1 addition & 1 deletion ci/onboard-entry-composition-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"policy": {
"createOnboardPolicyApplication.getRecordedPolicyTier": 1,
"preflightAuthoritativeRebuildTarget": 1,
"runOnboard": 6,
"runOnboard": 5,
"sandboxCreateIntentResolver.getAgentPolicyPath": 1
},
"provider": {
Expand Down
11 changes: 11 additions & 0 deletions nemoclaw-blueprint/provider-profiles/openai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

id: openai
display_name: OpenAI-compatible gateway route
description: Endpointless profile for credentials consumed by the OpenShell inference gateway
category: inference
credentials: []
endpoints: []
binaries: []
inference_capable: true
55 changes: 55 additions & 0 deletions src/lib/actions/sandbox/mcp-bridge-adapter-openclaw.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
OPENCLAW_MCPORTER_ROOT,
} from "./mcp-bridge-adapter-openclaw";
import {
entryHeaders,
buildOpenClawMcporterInspectCommand,
mcporterHeadersMatchExpected,
openClawMcporterRoot,
Expand Down Expand Up @@ -55,6 +56,15 @@ describe("OpenClaw mcporter MCP adapter", testTimeoutOptions(20_000), () => {
),
).toBe(true);
expect(mcporterHeadersMatchExpected(expected, expected)).toBe(true);
expect(
mcporterHeadersMatchExpected(
{
Authorization: "Bearer openshell:resolve:env:v1442987827285932589_GITHUB_TOKEN",
accept: "application/json, text/event-stream",
},
expected,
),
).toBe(true);
expect(
mcporterHeadersMatchExpected(
{
Expand Down Expand Up @@ -85,6 +95,51 @@ describe("OpenClaw mcporter MCP adapter", testTimeoutOptions(20_000), () => {
).toBe(false);
});

it.each([
"Bearer openshell:resolve:env:v_GITHUB_TOKEN",
"Bearer openshell:resolve:env:v42_OTHER_TOKEN",
"Bearer openshell:resolve:env:v42x_GITHUB_TOKEN",
`Bearer openshell:resolve:env:v${"1".repeat(21)}_GITHUB_TOKEN`,
])("rejects an unsafe revisioned mcporter Authorization header: %s", (authorization) => {
expect(
mcporterHeadersMatchExpected(
{ Authorization: authorization },
{ Authorization: "Bearer openshell:resolve:env:GITHUB_TOKEN" },
),
).toBe(false);
});

it("projects the live OpenShell credential revision into mcporter config", () => {
const command = buildOpenClawMcporterRegisterCommand(
baseEntry,
false,
OPENCLAW_MCPORTER_ROOT,
"v1442987827285932589",
);

expect(command).toContain(
"Authorization=Bearer openshell:resolve:env:v1442987827285932589_GITHUB_TOKEN",
);
expect(command).not.toContain("Authorization=Bearer openshell:resolve:env:GITHUB_TOKEN'");
});

it("matches the exact readiness-proven revision during post-write inspection", () => {
const expectedV12 = entryHeaders(baseEntry, "v12");

expect(
mcporterHeadersMatchExpected(
{ Authorization: "Bearer openshell:resolve:env:v12_GITHUB_TOKEN" },
expectedV12,
),
).toBe(true);
expect(
mcporterHeadersMatchExpected(
{ Authorization: "Bearer openshell:resolve:env:v11_GITHUB_TOKEN" },
expectedV12,
),
).toBe(false);
});

it("registers, inspects, and removes the OpenClaw workspace project config", () => {
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-mcporter-owner-"));
try {
Expand Down
9 changes: 6 additions & 3 deletions src/lib/actions/sandbox/mcp-bridge-adapter-openclaw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from "./mcp-bridge-adapter-status";
import { McpBridgeError } from "./mcp-bridge-contracts";
import { redactBridgeSecretsForDisplay } from "./mcp-bridge-output";
import type { McpAttachedCredentialRevision } from "./mcp-bridge-provider-readiness";
import { getAgentConfigDir } from "./mcp-bridge-state";
import { executeSandboxCommand } from "./process-recovery";

Expand Down Expand Up @@ -50,9 +51,10 @@ export function buildOpenClawMcporterRegisterCommand(
entry: McpBridgeEntry,
replaceExisting = false,
root = OPENCLAW_MCPORTER_ROOT,
credentialRevision?: McpAttachedCredentialRevision,
): string {
const args = mcporterArgs(root, "config", "add", entry.server, "--url", entry.url);
const authorization = authorizationValue(entry);
const authorization = authorizationValue(entry, credentialRevision);
if (authorization) args.push("--header", `Authorization=${authorization}`);
args.push("--scope", "project");
const addCommand = args.map(shellQuote).join(" ");
Expand Down Expand Up @@ -142,12 +144,13 @@ export function registerOpenClawAdapter(
entry: McpBridgeEntry,
envValues: Record<string, string> = {},
replaceExisting = false,
credentialRevision?: McpAttachedCredentialRevision,
): void {
ensureMcporter(sandboxName);
const root = mcporterRootForEntry(entry);
const result = executeSandboxCommand(
sandboxName,
buildOpenClawMcporterRegisterCommand(entry, replaceExisting, root),
buildOpenClawMcporterRegisterCommand(entry, replaceExisting, root, credentialRevision),
);
const output = redactBridgeSecretsForDisplay(
[result?.stdout, result?.stderr].filter(Boolean).join("\n").trim(),
Expand All @@ -164,7 +167,7 @@ export function registerOpenClawAdapter(
// from the URL and opaque OpenShell placeholder NemoClaw intended.
const verification = executeSandboxCommand(
sandboxName,
buildOpenClawMcporterInspectCommand(entry, true, root),
buildOpenClawMcporterInspectCommand(entry, true, root, credentialRevision),
);
const verificationOutput = redactBridgeSecretsForDisplay(
[verification?.stdout, verification?.stderr].filter(Boolean).join("\n").trim(),
Expand Down
40 changes: 40 additions & 0 deletions src/lib/actions/sandbox/mcp-bridge-adapter-registration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ import {
buildHermesMcpStatusCommand,
registerAgentAdapter,
} from "./mcp-bridge-adapters";
import { registerOpenClawAdapter } from "./mcp-bridge-adapter-openclaw";
import {
entryHeaders,
mcporterHeadersMatchExpected,
} from "./mcp-bridge-adapter-status";

const baseEntry: McpBridgeEntry = {
server: "github",
Expand Down Expand Up @@ -114,3 +119,38 @@ describe.each(adapterCases)("$name MCP adapter registration", (adapterCase) => {
).toThrow(`${adapterCase.adapter} config verification failed after adding 'github': mismatch.`);
});
});

describe("OpenClaw MCP adapter registration", () => {
beforeEach(() => {
mocks.executeSandboxCommand.mockReset();
});

it("rejects a v11 post-write observation after registering the readiness-proven v12", () => {
const entry: McpBridgeEntry = {
...baseEntry,
agent: "openclaw",
adapter: "mcporter",
};
const actualV11Headers = {
Authorization: "Bearer openshell:resolve:env:v11_GITHUB_TOKEN",
};
const verification = mcporterHeadersMatchExpected(actualV11Headers, entryHeaders(entry, "v12"))
? registered
: mismatch;
mocks.executeSandboxCommand
.mockReturnValueOnce({ status: 0, stdout: "/usr/bin/mcporter\n", stderr: "" })
.mockReturnValueOnce(commandSuccess)
.mockReturnValueOnce(verification);

expect(() =>
registerOpenClawAdapter("alpha", entry, { GITHUB_TOKEN: "host-only-secret" }, false, "v12"),
).toThrow("mcporter config verification failed after adding 'github': mismatch");

expect(mocks.executeSandboxCommand.mock.calls[1]?.[1]).toContain(
"Authorization=Bearer openshell:resolve:env:v12_GITHUB_TOKEN",
);
expect(mocks.executeSandboxCommand.mock.calls[2]?.[1]).toContain(
"Bearer openshell:resolve:env:v12_GITHUB_TOKEN",
);
});
});
50 changes: 41 additions & 9 deletions src/lib/actions/sandbox/mcp-bridge-adapter-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import type { McpBridgeEntry } from "../../state/registry";
import type { McpAttachedCredentialRevision } from "./mcp-bridge-provider-readiness";
import {
DEEPAGENTS_MANAGED_PROJECTION_READ_HELPERS,
DEEPAGENTS_STRICT_JSON_HELPERS,
Expand All @@ -21,18 +22,30 @@ export const OPENCLAW_MCPORTER_ROOT = openClawMcporterRoot();
const DEFAULT_AUTH_HEADER = "Authorization";
const DEFAULT_AUTH_SCHEME = "Bearer";

function authPlaceholder(entry: Pick<McpBridgeEntry, "env">): string | null {
function authPlaceholder(
entry: Pick<McpBridgeEntry, "env">,
credentialRevision?: McpAttachedCredentialRevision,
): string | null {
const envName = entry.env[0];
return envName ? `openshell:resolve:env:${envName}` : null;
if (!envName) return null;
const revision =
credentialRevision && credentialRevision !== "canonical" ? `${credentialRevision}_` : "";
return `openshell:resolve:env:${revision}${envName}`;
}

export function authorizationValue(entry: Pick<McpBridgeEntry, "env">): string | null {
const placeholder = authPlaceholder(entry);
export function authorizationValue(
entry: Pick<McpBridgeEntry, "env">,
credentialRevision?: McpAttachedCredentialRevision,
): string | null {
const placeholder = authPlaceholder(entry, credentialRevision);
return placeholder ? `${DEFAULT_AUTH_SCHEME} ${placeholder}` : null;
}

export function entryHeaders(entry: Pick<McpBridgeEntry, "env">): Record<string, string> {
const authorization = authorizationValue(entry);
export function entryHeaders(
entry: Pick<McpBridgeEntry, "env">,
credentialRevision?: McpAttachedCredentialRevision,
): Record<string, string> {
const authorization = authorizationValue(entry, credentialRevision);
return authorization ? { [DEFAULT_AUTH_HEADER]: authorization } : {};
}

Expand All @@ -45,7 +58,9 @@ export function pythonJsonLiteral(value: unknown): string {
* `config get --json` with an `accept: application/json, text/event-stream`
* header, even when that header is absent from the persisted config. Treat
* only that synthesized header as equivalent; every persisted/other header
* remains part of the ownership fingerprint.
* remains part of the ownership fingerprint. When the expected placeholder is
* canonical, a strictly bounded revisioned form of the same credential is also
* equivalent. A revisioned expectation remains exact.
*
* This function is also serialized into the in-sandbox inspection commands,
* so keep it self-contained (no references to module-scope values).
Expand All @@ -59,7 +74,23 @@ export function mcporterHeadersMatchExpected(
}
const actualHeaders = actual as Record<string, unknown>;
for (const [name, value] of Object.entries(expected)) {
if (actualHeaders[name] !== value) return false;
const actualValue = actualHeaders[name];
if (actualValue === value) continue;
if (name.toLowerCase() !== "authorization") return false;
const prefix = "Bearer openshell:resolve:env:";
if (
typeof actualValue !== "string" ||
!value.startsWith(prefix) ||
!actualValue.startsWith(prefix)
) {
return false;
}
const envName = value.slice(prefix.length);
const versioned = actualValue.slice(prefix.length);
const suffix = `_${envName}`;
if (!versioned.startsWith("v") || !versioned.endsWith(suffix)) return false;
const revision = versioned.slice(1, -suffix.length);
if (!/^[0-9]{1,20}$/u.test(revision)) return false;
}
const extraNames = Object.keys(actualHeaders).filter((name) => !Object.hasOwn(expected, name));
if (extraNames.length === 0) return true;
Expand Down Expand Up @@ -164,11 +195,12 @@ export function buildOpenClawMcporterInspectCommand(
entry: McpBridgeEntry,
failOnMismatch: boolean,
root = OPENCLAW_MCPORTER_ROOT,
credentialRevision?: McpAttachedCredentialRevision,
): string {
const payload = {
server: entry.server,
url: entry.url,
headers: entryHeaders(entry),
headers: entryHeaders(entry, credentialRevision),
failOnMismatch,
root,
};
Expand Down
15 changes: 13 additions & 2 deletions src/lib/actions/sandbox/mcp-bridge-adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
registerOpenClawAdapter,
unregisterOpenClawAdapter,
} from "./mcp-bridge-adapter-openclaw";
import type { McpAttachedCredentialRevision } from "./mcp-bridge-provider-readiness";

export {
buildDeepAgentsMcpRegisterCommand,
Expand Down Expand Up @@ -125,11 +126,21 @@ export function registerAgentAdapter(
adapter: AgentMcpAdapter,
entry: McpBridgeEntry,
envValues: Record<string, string> = {},
options: { replaceExisting?: boolean; teardownRollback?: boolean } = {},
options: {
replaceExisting?: boolean;
teardownRollback?: boolean;
credentialRevision?: McpAttachedCredentialRevision;
} = {},
): void {
switch (adapter) {
case "mcporter":
registerOpenClawAdapter(sandboxName, entry, envValues, options.replaceExisting === true);
registerOpenClawAdapter(
sandboxName,
entry,
envValues,
options.replaceExisting === true,
options.credentialRevision,
);
return;
case "hermes-config":
registerHermesAdapter(sandboxName, entry, envValues, options.replaceExisting === true);
Expand Down
Loading
Loading