-
Notifications
You must be signed in to change notification settings - Fork 3k
Expand file tree
/
Copy pathmcp-bridge-rebuild-exec-unavailable.ts
More file actions
273 lines (256 loc) · 9.94 KB
/
Copy pathmcp-bridge-rebuild-exec-unavailable.ts
File metadata and controls
273 lines (256 loc) · 9.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
import type { AgentMcpAdapter } from "../../agent/defs";
import { resolveSandboxGatewayName } from "../../onboard/gateway-binding";
import type { McpBridgeEntry, SandboxEntry } from "../../state/registry";
import { McpBridgeError } from "./mcp-bridge-contracts";
import {
assertMcpDestroySnapshotCurrent,
cloneMcpBridgeEntry,
inspectExactMcpDestroyProvider,
} from "./mcp-bridge-destroy-preflight";
import { assertGeneratedPolicyExactReadOnly } from "./mcp-bridge-policy";
import {
assertNoAttachedProviderCredentialCollisions,
preflightMcpEntryTargets,
} from "./mcp-bridge-provider";
import {
assertMcpDestroyNotPending,
bridgeState,
ensureSandboxGatewaySelected,
getBridgeAdapter,
getSandboxAgent,
getSandboxOrThrow,
} from "./mcp-bridge-state";
import type { McpBridgeTargetValidation } from "./mcp-bridge-url-validation";
import { assertAuthenticatedBridgeEntry, validateSandboxName } from "./mcp-bridge-validation";
type ReadOnlyValidationSnapshot = {
policyByServer: Map<string, string>;
providerByServer: Map<string, string>;
targetsByServer: Map<string, string>;
};
type ExplicitAdapterMcpBridgeEntry = McpBridgeEntry & { adapter: AgentMcpAdapter };
type McpOwnershipField = {
label: string;
value: (entry: McpBridgeEntry) => string | undefined;
};
export interface ExecUnavailableMcpRebuildPreparation {
entries: McpBridgeEntry[];
detachedProviderEntries: McpBridgeEntry[];
scrubbedAdapterEntries: McpBridgeEntry[];
revalidateBeforeDelete: () => Promise<void>;
assertDeleteEdgeUnchanged: () => void;
}
function assertUniqueMcpOwnership(entries: readonly McpBridgeEntry[]): void {
for (const entry of entries) assertAuthenticatedBridgeEntry(entry);
const ownershipFields: readonly McpOwnershipField[] = [
{ label: "credential key", value: (entry) => entry.env[0] },
{ label: "provider name", value: (entry) => entry.providerName },
{ label: "provider ID", value: (entry) => entry.providerId },
{ label: "generated policy name", value: (entry) => entry.policyName },
];
for (const field of ownershipFields) {
const ownerByValue = new Map<string, string>();
for (const entry of entries) {
const value = field.value(entry);
if (!value) continue;
if (ownerByValue.has(value)) {
const priorOwner = ownerByValue.get(value) ?? "<unknown>";
throw new McpBridgeError(
`MCP servers '${priorOwner}' and '${entry.server}' reuse the same ${field.label} '${value}'. Refusing read-only host-side rebuild recovery.`,
);
}
ownerByValue.set(value, entry.server);
}
}
}
function snapshotCompleteEntries(sandboxName: string): {
entries: ExplicitAdapterMcpBridgeEntry[];
gatewayName: string;
agentName: string;
adapter: AgentMcpAdapter;
} {
validateSandboxName(sandboxName);
const sandbox = getSandboxOrThrow(sandboxName);
assertMcpDestroyNotPending(sandbox);
const entries = Object.values(bridgeState(sandbox)).map(cloneMcpBridgeEntry);
const incomplete = entries.find((entry) => entry.addState !== undefined);
if (incomplete) {
throw new McpBridgeError(
`MCP server '${incomplete.server}' has an incomplete add transaction (${incomplete.addState}). Read-only host-side rebuild recovery cannot discard or adopt it; re-run the original mcp add command or remove it with --force before rebuilding the sandbox.`,
);
}
const agent = getSandboxAgent(sandbox);
const adapter = getBridgeAdapter(agent);
const incompatible = entries.find(
(entry) => entry.agent !== agent.name || entry.adapter !== adapter,
);
if (incompatible) {
throw new McpBridgeError(
"Managed MCP adapter identity is missing or incompatible with the sandbox's recorded agent. Refusing read-only host-side rebuild recovery.",
);
}
assertUniqueMcpOwnership(entries);
return {
entries: entries.map((entry) => ({ ...entry, adapter })),
gatewayName: resolveSandboxGatewayName(sandbox),
agentName: agent.name,
adapter,
};
}
function policyFingerprint(policy: ReturnType<typeof assertGeneratedPolicyExactReadOnly>): string {
return JSON.stringify({
name: policy.name,
content: policy.content,
pendingContent: policy.pendingContent,
sourcePath: policy.sourcePath,
appliedAt: policy.appliedAt,
});
}
function providerFingerprint(provider: ReturnType<typeof inspectExactMcpDestroyProvider>): string {
return JSON.stringify({
exists: provider.exists,
id: provider.id,
resourceVersion: provider.resourceVersion,
type: provider.type,
credentialKeys: provider.credentialKeys,
});
}
function targetFingerprint(target: McpBridgeTargetValidation | undefined): string {
if (!target || target.addresses.length === 0) {
throw new McpBridgeError(
"Resolved MCP target validation returned no exact address pins. Refusing host-side rebuild recovery.",
);
}
return JSON.stringify([...target.addresses].sort());
}
async function inspectReadOnlyRecoveryState(
sandboxName: string,
entries: readonly McpBridgeEntry[],
adapter: AgentMcpAdapter,
): Promise<ReadOnlyValidationSnapshot> {
const resolvedTargets = await preflightMcpEntryTargets(entries);
// This may start or recover the sandbox's recorded host gateway and select
// it in CLI context. It does not mutate MCP ownership or sandbox contents;
// the provider, policy, and target checks below remain inspection-only.
if (entries.length > 0) await ensureSandboxGatewaySelected(sandboxName);
const policyByServer = new Map<string, string>();
const providerByServer = new Map<string, string>();
const targetsByServer = new Map<string, string>();
for (const entry of entries) {
const target = resolvedTargets.get(entry.server);
const policy = assertGeneratedPolicyExactReadOnly(
sandboxName,
entry,
adapter,
target ?? {
addresses: [],
},
);
policyByServer.set(entry.server, policyFingerprint(policy));
const provider = inspectExactMcpDestroyProvider(entry, { allowMissing: false });
providerByServer.set(entry.server, providerFingerprint(provider));
targetsByServer.set(entry.server, targetFingerprint(target));
}
assertNoAttachedProviderCredentialCollisions(sandboxName, entries);
return { policyByServer, providerByServer, targetsByServer };
}
function assertValidationSnapshotCurrent(
entries: readonly McpBridgeEntry[],
expected: ReadOnlyValidationSnapshot,
current: ReadOnlyValidationSnapshot,
): void {
const drifted = entries.find(
(entry) =>
current.policyByServer.get(entry.server) !== expected.policyByServer.get(entry.server) ||
current.providerByServer.get(entry.server) !== expected.providerByServer.get(entry.server) ||
current.targetsByServer.get(entry.server) !== expected.targetsByServer.get(entry.server),
);
if (drifted) {
throw new McpBridgeError(
`MCP server '${drifted.server}' changed after host-side rebuild preflight. Refusing to delete the still-live sandbox; retry after its target, policy, and provider state is stable.`,
);
}
}
function assertDeleteEdgeUnchanged(
sandboxName: string,
expectedEntries: readonly McpBridgeEntry[],
expectedGatewayName: string,
expectedAgentName: string,
expectedAdapter: AgentMcpAdapter,
): void {
const sandbox: SandboxEntry = assertMcpDestroySnapshotCurrent(sandboxName, expectedEntries);
assertMcpDestroyNotPending(sandbox);
try {
const agent = getSandboxAgent(sandbox);
if (agent.name !== expectedAgentName || getBridgeAdapter(agent) !== expectedAdapter) {
throw new Error("adapter binding changed");
}
} catch {
throw new McpBridgeError(
`Sandbox '${sandboxName}' changed its recorded agent or MCP adapter after host-side rebuild preflight. Refusing to delete it.`,
);
}
if (resolveSandboxGatewayName(sandbox) !== expectedGatewayName) {
throw new McpBridgeError(
`Sandbox '${sandboxName}' changed its recorded gateway after host-side rebuild preflight. Refusing to delete it.`,
);
}
}
async function revalidateBeforeDelete(
sandboxName: string,
expectedEntries: readonly McpBridgeEntry[],
expectedGatewayName: string,
expectedAgentName: string,
expectedAdapter: AgentMcpAdapter,
expectedValidation: ReadOnlyValidationSnapshot,
): Promise<void> {
assertDeleteEdgeUnchanged(
sandboxName,
expectedEntries,
expectedGatewayName,
expectedAgentName,
expectedAdapter,
);
const currentValidation = await inspectReadOnlyRecoveryState(
sandboxName,
expectedEntries,
expectedAdapter,
);
assertValidationSnapshotCurrent(expectedEntries, expectedValidation, currentValidation);
}
/**
* Preserve complete MCP intent when sandbox exec is unavailable but OpenShell
* still reports the sandbox live. Unlike absent-sandbox recovery, this path is
* read-only with respect to MCP ownership and sandbox contents: it may recover
* and select the recorded host gateway for inspection, but it never discards
* add markers, scrubs adapters, detaches providers, reconciles policy records,
* or otherwise mutates MCP ownership before delete.
*/
export async function prepareMcpBridgesForExecUnavailableRebuild(
sandboxName: string,
): Promise<ExecUnavailableMcpRebuildPreparation> {
const { entries, gatewayName, agentName, adapter } = snapshotCompleteEntries(sandboxName);
const expectedEntries = entries.map(cloneMcpBridgeEntry);
const expectedValidation = await inspectReadOnlyRecoveryState(
sandboxName,
expectedEntries,
adapter,
);
return {
entries: entries.map(cloneMcpBridgeEntry),
detachedProviderEntries: [],
scrubbedAdapterEntries: [],
revalidateBeforeDelete: () =>
revalidateBeforeDelete(
sandboxName,
expectedEntries,
gatewayName,
agentName,
adapter,
expectedValidation,
),
assertDeleteEdgeUnchanged: () =>
assertDeleteEdgeUnchanged(sandboxName, expectedEntries, gatewayName, agentName, adapter),
};
}