Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
dfa9376
fix(policy): enforce external policy authority
apurvvkumaria Aug 24, 2026
ad1c8f7
test(policy): model managed authority preflight
apurvvkumaria Aug 24, 2026
2b18b39
merge: sync main
apurvvkumaria Aug 24, 2026
58d9477
fix(policy): fail closed on empty authority
apurvvkumaria Aug 24, 2026
864145b
fix(policy): guard blueprint and Shields mutations
apurvvkumaria Aug 24, 2026
eae0cc1
test(blueprint): model policy authority preflight
apurvvkumaria Aug 24, 2026
192c23f
fix(policy): retain incomplete blueprint transitions
apurvvkumaria Aug 24, 2026
b06fe62
test(blueprint): keep policy mutations visible
apurvvkumaria Aug 24, 2026
915db84
test(policy): cover authority refusal states
apurvvkumaria Aug 24, 2026
c7bd1f0
fix(shields): report external policy recovery
apurvvkumaria Aug 24, 2026
5078283
fix(policy): reconcile interrupted mutations
apurvvkumaria Aug 24, 2026
548f8fd
test(blueprint): cover policy reconciliation refusals
apurvvkumaria Aug 24, 2026
1246705
Merge branch 'main' into codex/9833-policy-authority-foundation
cv Aug 24, 2026
e4f9bcd
Merge branch 'main' into codex/9833-policy-authority-foundation
cv Aug 24, 2026
6976407
Merge branch 'main' into codex/9833-policy-authority-foundation
cv Aug 24, 2026
3ecd24b
fix(onboard): respect external policy authority
apurvvkumaria Aug 24, 2026
9fa334a
fix(onboard): close policy authority lifecycle gaps
apurvvkumaria Aug 24, 2026
c7743a5
Merge branch 'main' into codex/9833-policy-authority-foundation
cv Aug 24, 2026
f71fbf7
fix(rebuild): preserve external policy authority
apurvvkumaria Aug 24, 2026
2729d14
Merge branch 'main' into codex/9833-policy-authority-foundation
cv Aug 24, 2026
6f71bdb
fix(policy): close authority review gaps
apurvvkumaria Aug 24, 2026
61646cc
merge: update onboarding authority base
apurvvkumaria Aug 24, 2026
fadf3ef
Merge remote-tracking branch 'origin/codex/9833-onboarding-authority'…
apurvvkumaria Aug 24, 2026
6cf16bb
test(e2e): retain blueprint policy call evidence
apurvvkumaria Aug 24, 2026
adcb438
test(rebuild): qualify policy authority fixtures
apurvvkumaria Aug 24, 2026
9e18267
merge: update policy authority foundation
apurvvkumaria Aug 24, 2026
bcd4c6b
merge: refresh onboarding authority base
apurvvkumaria Aug 24, 2026
2e017cc
merge: refresh policy authority foundation from main
apurvvkumaria Aug 24, 2026
9c9c48f
merge: refresh onboarding authority from foundation
apurvvkumaria Aug 24, 2026
722cece
merge: refresh onboarding authority base
apurvvkumaria Aug 24, 2026
5379892
test(e2e): retain blueprint policy call trace
apurvvkumaria Aug 24, 2026
ae0c6e9
test(ci): rebalance policy authority coverage shard
apurvvkumaria Aug 24, 2026
12525cc
merge: update policy authority foundation
apurvvkumaria Aug 24, 2026
4cfc6e5
merge: update onboarding authority base
apurvvkumaria Aug 24, 2026
a8ad43e
Merge remote-tracking branch 'origin/main' into codex/9833-policy-aut…
apurvvkumaria Aug 24, 2026
977c91c
test(ci): rebalance policy coverage shards
apurvvkumaria Aug 24, 2026
173e584
fix(policy): close authority recovery review gaps
apurvvkumaria Aug 24, 2026
c5fde09
Merge branch 'codex/9833-policy-authority-foundation' into codex/9833…
cv Aug 24, 2026
e4c9cc4
Merge branch 'codex/9833-onboarding-authority' into codex/9833-rebuil…
cv Aug 24, 2026
e9df90e
merge: refresh onboarding authority foundation
apurvvkumaria Aug 24, 2026
a098118
test(onboard): return policy authority metadata
apurvvkumaria Aug 24, 2026
1d06701
Merge branch 'main' into codex/9833-policy-authority-foundation
cv Aug 24, 2026
5b22e5a
Merge branch 'codex/9833-policy-authority-foundation' into codex/9833…
cv Aug 24, 2026
0fd0770
Merge branch 'codex/9833-onboarding-authority' into codex/9833-rebuil…
cv Aug 24, 2026
04e9bf0
merge: reconcile onboarding authority updates
apurvvkumaria Aug 24, 2026
009f569
merge: refresh rebuild authority base
apurvvkumaria Aug 24, 2026
90544c5
Merge branch 'main' into codex/9833-policy-authority-foundation
cv Aug 24, 2026
6cdfd4a
Merge branch 'codex/9833-policy-authority-foundation' into codex/9833…
cv Aug 24, 2026
e5ee4a5
test(onboard): repair policy authority fixtures
apurvvkumaria Aug 24, 2026
1d5bdf8
Merge remote-tracking branch 'origin/codex/9833-onboarding-authority'…
apurvvkumaria Aug 24, 2026
a0fa36d
fix(policy): preserve external Shields recovery
apurvvkumaria Aug 24, 2026
b010ce3
Merge branch 'codex/9833-policy-authority-foundation' into codex/9833…
apurvvkumaria Aug 24, 2026
6d7a3b1
Merge branch 'codex/9833-onboarding-authority' into codex/9833-rebuil…
apurvvkumaria Aug 24, 2026
b42ce40
Merge remote-tracking branch 'origin/main' into codex/9833-policy-aut…
apurvvkumaria Aug 24, 2026
302619b
Merge branch 'codex/9833-policy-authority-foundation' into codex/9833…
apurvvkumaria Aug 24, 2026
c0f8276
Merge branch 'codex/9833-onboarding-authority' into codex/9833-rebuil…
apurvvkumaria Aug 24, 2026
ceae05e
fix(shields): retain locked policy recovery
apurvvkumaria Aug 25, 2026
23f42a7
merge: restack policy authority foundation
apurvvkumaria Aug 25, 2026
d7bfc85
merge: restack onboarding authority
apurvvkumaria Aug 25, 2026
c9205ff
fix(policy): preserve recovery receipts
apurvvkumaria Aug 25, 2026
f0f3322
merge: restack policy authority foundation
apurvvkumaria Aug 25, 2026
69fa96a
merge: restack onboarding authority
apurvvkumaria Aug 25, 2026
07346c4
fix(policy): import registry owner directly
apurvvkumaria Aug 25, 2026
1610a3a
merge: resolve conflicts with main
github-actions[bot] Aug 26, 2026
2cd8d10
merge: resolve conflicts with main
github-actions[bot] Aug 26, 2026
1f98558
merge: resolve conflicts with main
github-actions[bot] Aug 26, 2026
6edc2d7
merge: resolve conflicts with main
github-actions[bot] Aug 26, 2026
3040113
merge(rebuild): synchronize rebuild authority with main
apurvvkumaria Aug 26, 2026
102d167
fix(rebuild): restore authority gate evidence
apurvvkumaria Aug 26, 2026
9ac16a8
merge: sync current main
apurvvkumaria Aug 26, 2026
6f4db14
merge(rebuild): synchronize current main
apurvvkumaria Aug 26, 2026
ae3d415
merge(rebuild): synchronize workflow contracts
apurvvkumaria Aug 26, 2026
9209e26
merge(rebuild): synchronize policy command cleanup
apurvvkumaria Aug 26, 2026
9dafe49
merge: resolve conflicts with main
github-actions[bot] Aug 27, 2026
5160abc
merge: resolve conflicts with main
github-actions[bot] Aug 27, 2026
6e2ad4b
test(e2e): bind rebuild fixture policy ownership
cjagwani Aug 27, 2026
99168b0
merge: update rebuild authority from current main
cjagwani Aug 27, 2026
45683b3
merge: sync E2E host command fix
cjagwani Aug 27, 2026
f33a64c
fix(e2e): retain PATH for host commands
cjagwani Aug 27, 2026
0aa109c
merge: resolve conflicts with main
github-actions[bot] Aug 27, 2026
b929197
test(e2e): preserve old runtime during rebuild setup
cjagwani Aug 28, 2026
3c3f748
test(e2e): scope legacy image patch to Docker build
cjagwani Aug 28, 2026
a4411d3
test(e2e): enable legacy image prebuild
cjagwani Aug 28, 2026
cb15ad3
test(e2e): preserve OpenShell companion identity
cjagwani Aug 28, 2026
4749b5c
test(e2e): use explicit legacy rebuild context
cjagwani Aug 28, 2026
7e9f806
Merge remote-tracking branch 'origin/main' into HEAD
cjagwani Aug 28, 2026
1be532a
Merge branch 'main' into codex/9833-rebuild-authority
cv Aug 28, 2026
73aebca
Merge remote-tracking branch 'origin/codex/9833-rebuild-authority' in…
cjagwani Aug 28, 2026
5714c42
test: cover MCP rebuild refusal compensation
cv Aug 28, 2026
7188538
test(e2e): keep rebuild fixture on current base
cjagwani Aug 28, 2026
15b792c
Merge remote-tracking branch 'origin/main' into HEAD
cjagwani Aug 28, 2026
7744ddd
Merge remote-tracking branch 'origin/codex/9833-rebuild-authority' in…
cjagwani Aug 28, 2026
8ddd9e8
fix: preserve rebuild policy authority failures
cv Aug 28, 2026
76cdb13
merge: sync concurrent PR updates
cv Aug 28, 2026
db7e43d
merge: resolve conflicts with main
github-actions[bot] Aug 28, 2026
8015468
ci: retrigger PR review advisor
cv Aug 28, 2026
450887f
ci: retrigger PR review advisor
cv Aug 28, 2026
aeb8bec
merge: resolve conflicts with main
github-actions[bot] Aug 28, 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
2 changes: 1 addition & 1 deletion ci/source-architecture-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"src/lib/security/redact.ts": 54,
"src/lib/state/onboard-session.ts": 36,
"src/lib/state/mcp-lifecycle-lock.ts": 21,
"src/lib/state/registry.ts": 100,
"src/lib/state/registry.ts": 103,
"src/lib/state/state-root.ts": 21,
"src/lib/subprocess-env.ts": 24,
"src/lib/validation.ts": 24
Expand Down
8 changes: 3 additions & 5 deletions src/lib/actions/sandbox/mcp-bridge-contracts.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import type { AgentMcpAdapter } from "../../agent/defs";
import type { AgentMcpAdapter } from "../../agent/definition-types";

export { isAgentMcpAdapter } from "../../agent/definition-types";

export const MCP_BRIDGE_POLICY_SOURCE = "generated:nemoclaw-mcp-bridge";
export type McpBridgeErrorReasonCode = "rejected" | "unresolved";
Expand Down Expand Up @@ -93,7 +95,3 @@ export interface McpBridgeStatus {
addedAt?: string;
updatedAt?: string;
}

export function isAgentMcpAdapter(value: unknown): value is AgentMcpAdapter {
return value === "mcporter" || value === "hermes-config" || value === "deepagents-config";
}
239 changes: 239 additions & 0 deletions src/lib/actions/sandbox/mcp-bridge-rebuild-policy-authority.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { beforeEach, describe, expect, it, vi } from "vitest";
import { PolicyAuthorityRefusalError } from "../../adapters/openshell/policy-authority";
import type { McpBridgeEntry, SandboxEntry } from "../../state/registry";

const mocks = vi.hoisted(() => ({
assertAdapterConfigMutationsAllowed: vi.fn(),
assertAdapterTeardownRuntimeCapabilities: vi.fn(),
assertDestroyNotPending: vi.fn(),
assertGeneratedPolicyMutationSafe: vi.fn(),
assertNoProviderCredentialCollisions: vi.fn(),
assertProviderRecoverable: vi.fn(),
attachProvider: vi.fn(),
bridgeState: vi.fn(),
detachProvider: vi.fn(),
discardSafeIncompleteAdds: vi.fn(),
ensureSandboxGatewaySelected: vi.fn(),
getSandboxOrThrow: vi.fn(),
inspectExactDestroyProvider: vi.fn(),
preflightEntryTargets: vi.fn(),
removeGeneratedPolicy: vi.fn(),
refreshProviderEnvironment: vi.fn(),
restoreRuntime: vi.fn(),
rollbackScrubbedAdapters: vi.fn(),
scrubAdapter: vi.fn(),
setBridgeState: vi.fn(),
waitForDetachedCredential: vi.fn(),
waitForAttachedCredential: vi.fn(),
}));

vi.mock("./mcp-bridge-adapter-teardown", () => ({
rollbackScrubbedMcpAdapters: mocks.rollbackScrubbedAdapters,
scrubManagedMcpAdapterOrThrow: mocks.scrubAdapter,
}));

vi.mock("./mcp-bridge-destroy", () => ({
cloneMcpBridgeEntry: (entry: McpBridgeEntry) => structuredClone(entry),
discardSafeIncompleteMcpAdds: mocks.discardSafeIncompleteAdds,
inspectExactMcpDestroyProvider: mocks.inspectExactDestroyProvider,
}));

vi.mock("./mcp-bridge-policy", () => ({
assertGeneratedPolicyMutationSafe: mocks.assertGeneratedPolicyMutationSafe,
assertGeneratedPolicyRegistrationMutationSafe: vi.fn(),
removeGeneratedPolicy: mocks.removeGeneratedPolicy,
}));

vi.mock("./mcp-bridge-provider", () => ({
assertMcpProviderRecoverable: mocks.assertProviderRecoverable,
assertNoProviderCredentialCollisions: mocks.assertNoProviderCredentialCollisions,
assertNoRegisteredProviderCredentialCollisions: vi.fn(),
attachProvider: mocks.attachProvider,
detachProvider: mocks.detachProvider,
preflightMcpEntryTargets: mocks.preflightEntryTargets,
refreshMcpProviderEnvironment: mocks.refreshProviderEnvironment,
waitForAttachedMcpCredential: mocks.waitForAttachedCredential,
waitForDetachedMcpCredential: mocks.waitForDetachedCredential,
}));

vi.mock("./mcp-bridge-restart", () => ({
restoreExistingMcpBridgeRuntime: mocks.restoreRuntime,
}));

vi.mock("./mcp-bridge-runtime-capabilities", () => ({
assertMcpAdapterConfigMutationsAllowed: mocks.assertAdapterConfigMutationsAllowed,
assertMcpAdapterTeardownRuntimeCapabilities: mocks.assertAdapterTeardownRuntimeCapabilities,
}));

vi.mock("./mcp-bridge-state", () => ({
assertMcpDestroyNotPending: mocks.assertDestroyNotPending,
bridgeState: mocks.bridgeState,
ensureSandboxGatewaySelected: mocks.ensureSandboxGatewaySelected,
getSandboxOrThrow: mocks.getSandboxOrThrow,
setBridgeState: mocks.setBridgeState,
}));

const { prepareMcpBridgesForRebuild, restoreMcpBridgesAfterRebuild } =
await import("./mcp-bridge-rebuild");

let entry: McpBridgeEntry;
let sandbox: SandboxEntry;

describe("MCP rebuild policy authority", () => {
beforeEach(() => {
vi.clearAllMocks();
entry = {
server: "github",
agent: "openclaw",
adapter: "mcporter",
url: "https://mcp.example.test/mcp",
env: ["GITHUB_TOKEN"],
providerName: "alpha-mcp-github",
providerId: "provider-1",
policyName: "mcp-bridge-github",
addedAt: "2026-08-20T00:00:00.000Z",
};
sandbox = {
name: "alpha",
agent: "openclaw",
policyAuthority: "externally-managed",
mcp: { bridges: { github: entry } },
};
mocks.getSandboxOrThrow.mockReturnValue(sandbox);
mocks.bridgeState.mockImplementation((current: SandboxEntry) => current.mcp?.bridges ?? {});
mocks.discardSafeIncompleteAdds.mockResolvedValue(sandbox);
mocks.detachProvider.mockReturnValue("detached");
mocks.rollbackScrubbedAdapters.mockReturnValue([]);
mocks.scrubAdapter.mockImplementation((_name, _sandbox, current: McpBridgeEntry) => ({
...current,
credentialRevision: "v1",
}));
});

it("preserves externally managed policy while preparing adapter and provider state (#9833)", async () => {
const validatePolicyAuthority = vi.fn().mockResolvedValue(undefined);

await expect(
prepareMcpBridgesForRebuild("alpha", validatePolicyAuthority),
).resolves.toMatchObject({
entries: [entry],
detachedProviderEntries: [entry],
scrubbedAdapterEntries: [entry],
});

expect(mocks.removeGeneratedPolicy).not.toHaveBeenCalled();
expect(validatePolicyAuthority).toHaveBeenCalledTimes(3);
expect(validatePolicyAuthority.mock.invocationCallOrder[1]).toBeLessThan(
mocks.scrubAdapter.mock.invocationCallOrder[0],
);
expect(validatePolicyAuthority.mock.invocationCallOrder[2]).toBeLessThan(
mocks.detachProvider.mock.invocationCallOrder[0],
);
});

it("preserves the authority refusal before MCP teardown mutation (#9833)", async () => {
const refusal = new PolicyAuthorityRefusalError("policy authority changed");
const validatePolicyAuthority = vi
.fn()
.mockResolvedValueOnce(undefined)
.mockRejectedValueOnce(refusal);

await expect(prepareMcpBridgesForRebuild("alpha", validatePolicyAuthority)).rejects.toBe(
refusal,
);

expect(mocks.scrubAdapter).not.toHaveBeenCalled();
expect(mocks.removeGeneratedPolicy).not.toHaveBeenCalled();
expect(mocks.detachProvider).not.toHaveBeenCalled();
});

it("rolls back an adapter scrub when authority changes before policy removal (#9833)", async () => {
sandbox.policyAuthority = "nemoclaw-managed";
const refusal = new PolicyAuthorityRefusalError("policy authority changed");
const validatePolicyAuthority = vi
.fn()
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce(undefined)
.mockRejectedValueOnce(refusal);

await expect(prepareMcpBridgesForRebuild("alpha", validatePolicyAuthority)).rejects.toBe(
refusal,
);

expect(mocks.rollbackScrubbedAdapters).toHaveBeenCalledExactlyOnceWith("alpha", sandbox, [
expect.objectContaining({ server: "github", credentialRevision: "v1" }),
]);
expect(mocks.removeGeneratedPolicy).not.toHaveBeenCalled();
expect(mocks.detachProvider).not.toHaveBeenCalled();
expect(mocks.restoreRuntime).not.toHaveBeenCalled();
});

it("reattaches a detached provider when authority changes before the next detach (#9833)", async () => {
const second = {
...entry,
server: "gitlab",
env: ["GITLAB_TOKEN"],
providerName: "alpha-mcp-gitlab",
providerId: "provider-2",
policyName: "mcp-bridge-gitlab",
};
sandbox.mcp = { bridges: { github: entry, gitlab: second } };
const refusal = new PolicyAuthorityRefusalError("policy authority changed");
const validatePolicyAuthority = vi
.fn()
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce(undefined)
.mockRejectedValueOnce(refusal);

await expect(prepareMcpBridgesForRebuild("alpha", validatePolicyAuthority)).rejects.toBe(
refusal,
);

expect(mocks.attachProvider).toHaveBeenCalledExactlyOnceWith("alpha", entry);
expect(mocks.refreshProviderEnvironment).toHaveBeenCalledExactlyOnceWith(entry);
expect(mocks.waitForAttachedCredential).toHaveBeenCalledExactlyOnceWith("alpha", entry);
expect(mocks.rollbackScrubbedAdapters).toHaveBeenCalledWith(
"alpha",
sandbox,
expect.arrayContaining([
expect.objectContaining({ server: "github", credentialRevision: "v1" }),
expect.objectContaining({ server: "gitlab", credentialRevision: "v1" }),
]),
);
expect(mocks.removeGeneratedPolicy).not.toHaveBeenCalled();
expect(mocks.restoreRuntime).not.toHaveBeenCalled();
});

it("revalidates between registry recovery and runtime restoration (#9833)", async () => {
const refusal = new PolicyAuthorityRefusalError("policy authority changed");
const validatePolicyAuthority = vi
.fn()
.mockResolvedValueOnce(undefined)
.mockRejectedValueOnce(refusal);

await expect(
restoreMcpBridgesAfterRebuild("alpha", [entry], validatePolicyAuthority),
).rejects.toBe(refusal);

expect(mocks.setBridgeState).toHaveBeenCalledOnce();
expect(mocks.restoreRuntime).not.toHaveBeenCalled();
});

it("revalidates after MCP runtime restoration before returning success (#9833)", async () => {
const validatePolicyAuthority = vi.fn().mockResolvedValue(undefined);

await expect(
restoreMcpBridgesAfterRebuild("alpha", [entry], validatePolicyAuthority),
).resolves.toBeUndefined();

expect(validatePolicyAuthority).toHaveBeenCalledTimes(3);
expect(mocks.restoreRuntime.mock.invocationCallOrder[0]).toBeLessThan(
validatePolicyAuthority.mock.invocationCallOrder[2],
);
});
});
Loading