@opengeni/runtime 2.5.2 → 2.5.3-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agent-Q2VNH5FV.js → agent-RJQLKFIW.js} +2 -2
- package/dist/{chunk-CD22I76N.js → chunk-FHGLYLBL.js} +2 -2
- package/dist/{chunk-GJC7H2FC.js → chunk-SD6MDONS.js} +350 -67
- package/dist/chunk-SD6MDONS.js.map +1 -0
- package/dist/{chunk-25B3EQRW.js → chunk-UNICCRW5.js} +4 -4
- package/dist/{chunk-LGLJ5PVD.js → chunk-VVEGJPDR.js} +2309 -1345
- package/dist/chunk-VVEGJPDR.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +22 -4
- package/dist/{localSources-Z5IJIIRV.js → localSources-PVFCAPN2.js} +3 -3
- package/dist/mcp-operation-dispatch.d.ts +45 -0
- package/dist/mcp-operation-observation.d.ts +137 -0
- package/dist/sandbox/archive-spool.d.ts +14 -0
- package/dist/sandbox/host-archive-spool.d.ts +19 -0
- package/dist/sandbox/index.d.ts +5 -1
- package/dist/sandbox/index.js +7 -3
- package/dist/sandbox/workspace-archive.d.ts +7 -0
- package/dist/workspace-tool-gateway.js +4 -4
- package/package.json +11 -11
- package/src/index.ts +231 -45
- package/src/mcp-operation-dispatch.ts +144 -0
- package/src/mcp-operation-observation.ts +148 -0
- package/src/sandbox/archive-spool.ts +16 -0
- package/src/sandbox/host-archive-spool.ts +1040 -0
- package/src/sandbox/index.ts +292 -203
- package/src/sandbox/workspace-archive.ts +54 -1
- package/dist/chunk-GJC7H2FC.js.map +0 -1
- package/dist/chunk-LGLJ5PVD.js.map +0 -1
- /package/dist/{agent-Q2VNH5FV.js.map → agent-RJQLKFIW.js.map} +0 -0
- /package/dist/{chunk-CD22I76N.js.map → chunk-FHGLYLBL.js.map} +0 -0
- /package/dist/{chunk-25B3EQRW.js.map → chunk-UNICCRW5.js.map} +0 -0
- /package/dist/{localSources-Z5IJIIRV.js.map → localSources-PVFCAPN2.js.map} +0 -0
package/src/index.ts
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
import type { ModelProviderApi, ResolvedModelProvider, Settings } from "@opengeni/config";
|
|
2
2
|
import { executeCommandReadWithRefresh } from "./command-read-refresh";
|
|
3
|
+
import {
|
|
4
|
+
captureMcpOperationDispatch,
|
|
5
|
+
hasActiveRecoverableMcpOperation,
|
|
6
|
+
runRecoverableMcpOperation,
|
|
7
|
+
type McpOperationPersistence,
|
|
8
|
+
} from "./mcp-operation-dispatch";
|
|
9
|
+
export {
|
|
10
|
+
captureMcpOperationDispatch,
|
|
11
|
+
hasActiveRecoverableMcpOperation,
|
|
12
|
+
McpOperationOutcomeUnknownError,
|
|
13
|
+
runRecoverableMcpOperation,
|
|
14
|
+
type CapturedMcpOperation,
|
|
15
|
+
type McpOperationPersistence,
|
|
16
|
+
type RecoverableMcpOperation,
|
|
17
|
+
} from "./mcp-operation-dispatch";
|
|
18
|
+
import { assertMcpOperationObservationAuthority } from "./mcp-operation-observation";
|
|
19
|
+
export {
|
|
20
|
+
assertMcpOperationObservationAuthority,
|
|
21
|
+
observeMcpOperation,
|
|
22
|
+
runMcpOperationObservationWithAuthority,
|
|
23
|
+
type McpOperationObservationAuthority,
|
|
24
|
+
type McpObservationBinding,
|
|
25
|
+
type McpObservationReceipt,
|
|
26
|
+
} from "./mcp-operation-observation";
|
|
3
27
|
import { formatSkillCatalog, type SkillCatalogDescriptor } from "./skill-catalog";
|
|
4
28
|
export { formatSkillCatalog, type SkillCatalogDescriptor } from "./skill-catalog";
|
|
5
29
|
import {
|
|
@@ -11,6 +35,7 @@ import {
|
|
|
11
35
|
AGENT_INSTRUCTIONS_CORE_PLACEHOLDER,
|
|
12
36
|
collectSandboxEnvironment,
|
|
13
37
|
configuredProviders,
|
|
38
|
+
McpOperationRecoverySchema,
|
|
14
39
|
firstPartyMcpInternalBaseUrl,
|
|
15
40
|
firstPartyMcpInternalWorkspaceUrl,
|
|
16
41
|
resolveFirstPartyDelegationSecret,
|
|
@@ -683,6 +708,8 @@ export type ResolveConnectionCredentialResult =
|
|
|
683
708
|
connectionId: string;
|
|
684
709
|
authoritySource?: "host";
|
|
685
710
|
authorizeProviderRequest?: () => Promise<boolean>;
|
|
711
|
+
/** Metadata-only immutable authority binding supplied by the trusted worker resolver. */
|
|
712
|
+
operationAuthorityDigest?: string;
|
|
686
713
|
expiresAt?: Date | null;
|
|
687
714
|
}
|
|
688
715
|
| {
|
|
@@ -1723,6 +1750,11 @@ type ModelToolInvocation = {
|
|
|
1723
1750
|
};
|
|
1724
1751
|
|
|
1725
1752
|
const modelToolInvocation = new AsyncLocalStorage<ModelToolInvocation>();
|
|
1753
|
+
// Correlation must not activate the separate approval-only invocation guard.
|
|
1754
|
+
const modelMcpCallIdentity = new AsyncLocalStorage<{
|
|
1755
|
+
modelName: string;
|
|
1756
|
+
callId: string;
|
|
1757
|
+
} | null>();
|
|
1726
1758
|
const agentInputWaitYields = new WeakMap<object, InputWaitYield>();
|
|
1727
1759
|
|
|
1728
1760
|
export type BuildAgentOptions = {
|
|
@@ -2657,6 +2689,7 @@ export function buildOpenGeniAgent(
|
|
|
2657
2689
|
agent as unknown as ApprovalCapableAgent,
|
|
2658
2690
|
options.approvedToolCallId,
|
|
2659
2691
|
);
|
|
2692
|
+
installModelMcpCallIdentity(agent as unknown as ApprovalCapableAgent);
|
|
2660
2693
|
return agent;
|
|
2661
2694
|
}
|
|
2662
2695
|
|
|
@@ -2785,6 +2818,7 @@ export function buildOpenGeniAgent(
|
|
|
2785
2818
|
agent as unknown as ApprovalCapableAgent,
|
|
2786
2819
|
options.approvedToolCallId,
|
|
2787
2820
|
);
|
|
2821
|
+
installModelMcpCallIdentity(agent as unknown as ApprovalCapableAgent);
|
|
2788
2822
|
return agent;
|
|
2789
2823
|
}
|
|
2790
2824
|
|
|
@@ -2876,6 +2910,42 @@ type ApprovalCapableAgent = {
|
|
|
2876
2910
|
clone?: (config: unknown) => ApprovalCapableAgent;
|
|
2877
2911
|
};
|
|
2878
2912
|
|
|
2913
|
+
/** Preserve SDK correlation through MCP projection without granting approval.
|
|
2914
|
+
* SandboxAgent clones rebuild instance methods, so reinstall on every clone. */
|
|
2915
|
+
function installModelMcpCallIdentity(agent: ApprovalCapableAgent): void {
|
|
2916
|
+
const listMcpTools = agent.getMcpTools.bind(agent);
|
|
2917
|
+
agent.getMcpTools = async (resolutionContext: unknown) =>
|
|
2918
|
+
(await listMcpTools(resolutionContext)).map((tool) => {
|
|
2919
|
+
if (tool.type !== "function") return tool;
|
|
2920
|
+
const invoke = tool.invoke.bind(tool);
|
|
2921
|
+
return {
|
|
2922
|
+
...tool,
|
|
2923
|
+
invoke: async (runContext, input, details) => {
|
|
2924
|
+
const callId = details?.toolCall?.callId;
|
|
2925
|
+
return await modelMcpCallIdentity.run(
|
|
2926
|
+
typeof callId === "string" && callId.length > 0
|
|
2927
|
+
? { modelName: tool.name, callId }
|
|
2928
|
+
: null,
|
|
2929
|
+
async () => await invoke(runContext, input, details),
|
|
2930
|
+
);
|
|
2931
|
+
},
|
|
2932
|
+
};
|
|
2933
|
+
});
|
|
2934
|
+
const clone = agent.clone?.bind(agent);
|
|
2935
|
+
if (clone) {
|
|
2936
|
+
agent.clone = (config: unknown) => {
|
|
2937
|
+
const cloned = clone(config);
|
|
2938
|
+
installModelMcpCallIdentity(cloned);
|
|
2939
|
+
return cloned;
|
|
2940
|
+
};
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
function modelMcpSourceCallId(modelName: string): string | undefined {
|
|
2945
|
+
const identity = modelMcpCallIdentity.getStore();
|
|
2946
|
+
return identity?.modelName === modelName ? identity.callId : undefined;
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2879
2949
|
/**
|
|
2880
2950
|
* Install the approval wrap on a single agent instance: replace `getMcpTools`
|
|
2881
2951
|
* with one that stamps `needsApproval: () => true` on every MCP tool whose
|
|
@@ -3612,6 +3682,8 @@ export type ToolPreparationPhaseMeasurement = {
|
|
|
3612
3682
|
};
|
|
3613
3683
|
|
|
3614
3684
|
export type PrepareToolsOptions = {
|
|
3685
|
+
/** Opt-in exact-attempt persistence; absence preserves ordinary MCP execution. */
|
|
3686
|
+
mcpOperationPersistence?: McpOperationPersistence;
|
|
3615
3687
|
/** Live exact-owner control refresh; API remains read/observation authority. */
|
|
3616
3688
|
refreshOwnedCommand?: (commandId: string) => Promise<boolean>;
|
|
3617
3689
|
accountId?: string;
|
|
@@ -3986,24 +4058,30 @@ export async function prepareAgentTools(
|
|
|
3986
4058
|
}
|
|
3987
4059
|
const optional = tool.optional === true;
|
|
3988
4060
|
return {
|
|
3989
|
-
server:
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4061
|
+
server: configureMcpOperationRecovery(
|
|
4062
|
+
new PrefixedMcpServer(
|
|
4063
|
+
local.server,
|
|
4064
|
+
config.id,
|
|
4065
|
+
config.allowedTools,
|
|
4066
|
+
optional || Boolean(config.connectionRef),
|
|
4067
|
+
aggregateToolBudget,
|
|
4068
|
+
`${config.id}:${index}`,
|
|
4069
|
+
false,
|
|
4070
|
+
buildConnectorAttachmentAuthority(
|
|
4071
|
+
config,
|
|
4072
|
+
options,
|
|
4073
|
+
resolvedMcpToolConnectionIds,
|
|
4074
|
+
config.url,
|
|
4075
|
+
local.resolvedConnectionId,
|
|
4076
|
+
),
|
|
4077
|
+
tool.eager !== true,
|
|
4078
|
+
local.preflightCall,
|
|
4079
|
+
local.approvalAuthority,
|
|
4003
4080
|
),
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4081
|
+
config,
|
|
4082
|
+
options,
|
|
4083
|
+
config.url,
|
|
4084
|
+
false,
|
|
4007
4085
|
),
|
|
4008
4086
|
bestEffort: optional || Boolean(config.connectionRef),
|
|
4009
4087
|
optional,
|
|
@@ -4121,24 +4199,30 @@ export async function prepareAgentTools(
|
|
|
4121
4199
|
}
|
|
4122
4200
|
: {}),
|
|
4123
4201
|
});
|
|
4124
|
-
const server =
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4202
|
+
const server = configureMcpOperationRecovery(
|
|
4203
|
+
new PrefixedMcpServer(
|
|
4204
|
+
innerServer,
|
|
4205
|
+
config.id,
|
|
4206
|
+
config.allowedTools,
|
|
4207
|
+
bestEffort,
|
|
4208
|
+
aggregateToolBudget,
|
|
4209
|
+
`${config.id}:${index}`,
|
|
4210
|
+
firstParty && !bestEffort,
|
|
4211
|
+
buildConnectorAttachmentAuthority(config, options, resolvedMcpToolConnectionIds, url),
|
|
4212
|
+
tool.eager !== true,
|
|
4213
|
+
undefined,
|
|
4214
|
+
undefined,
|
|
4215
|
+
firstParty && config.id === "opengeni" && !config.connectionRef && !bridge
|
|
4216
|
+
? inputWaitYield
|
|
4217
|
+
: undefined,
|
|
4218
|
+
firstParty && config.id === "opengeni" && !config.connectionRef && !bridge
|
|
4219
|
+
? options.refreshOwnedCommand
|
|
4220
|
+
: undefined,
|
|
4221
|
+
),
|
|
4222
|
+
config,
|
|
4223
|
+
options,
|
|
4224
|
+
url,
|
|
4225
|
+
Boolean(config.connectionRef) && !bridge && !isCodexAppsMcpServer(config),
|
|
4142
4226
|
);
|
|
4143
4227
|
return {
|
|
4144
4228
|
server,
|
|
@@ -4788,18 +4872,42 @@ async function prepareToolGatewayDefinitionsFromServers(
|
|
|
4788
4872
|
}),
|
|
4789
4873
|
}
|
|
4790
4874
|
: {}),
|
|
4791
|
-
execute: async (args, context) =>
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4875
|
+
execute: async (args, context) => {
|
|
4876
|
+
const execute = async () =>
|
|
4877
|
+
await server.executeCatalogTool(
|
|
4878
|
+
toolName,
|
|
4879
|
+
args,
|
|
4880
|
+
{
|
|
4881
|
+
...(context.transportMeta ?? {}),
|
|
4882
|
+
opengeniOperationId: context.operationId,
|
|
4883
|
+
},
|
|
4884
|
+
{
|
|
4885
|
+
...(context.signal ? { signal: context.signal } : {}),
|
|
4886
|
+
},
|
|
4887
|
+
);
|
|
4888
|
+
const recovery = configuredMcpOperationRecovery(server, toolName);
|
|
4889
|
+
if (!recovery) return await execute();
|
|
4890
|
+
if (!recovery.supported || !["model", "codemode"].includes(context.caller.kind)) {
|
|
4891
|
+
throw new Error(
|
|
4892
|
+
"MCP operation recovery requires an exact attempt and remote brokered execution",
|
|
4893
|
+
);
|
|
4894
|
+
}
|
|
4895
|
+
return await runRecoverableMcpOperation(
|
|
4799
4896
|
{
|
|
4800
|
-
|
|
4897
|
+
operationId: context.operationId,
|
|
4898
|
+
...(context.sourceCallId === undefined
|
|
4899
|
+
? {}
|
|
4900
|
+
: { sourceCallId: context.sourceCallId }),
|
|
4901
|
+
serverId: server.registryId,
|
|
4902
|
+
originalTool: toolName,
|
|
4903
|
+
observerTool: recovery.policy[toolName]!.observerTool,
|
|
4904
|
+
destinationDigest: recovery.destinationDigest,
|
|
4905
|
+
argumentDigest: digestCanonicalJson(args),
|
|
4801
4906
|
},
|
|
4802
|
-
|
|
4907
|
+
recovery.persistence,
|
|
4908
|
+
execute,
|
|
4909
|
+
);
|
|
4910
|
+
},
|
|
4803
4911
|
};
|
|
4804
4912
|
});
|
|
4805
4913
|
},
|
|
@@ -4818,6 +4926,38 @@ function attemptToolCodemodePath(serverId: string, toolName: string): readonly s
|
|
|
4818
4926
|
return [serverId, toolName];
|
|
4819
4927
|
}
|
|
4820
4928
|
|
|
4929
|
+
type PreparedMcpOperationRecovery = {
|
|
4930
|
+
policy: NonNullable<Settings["mcpServers"][number]["operationRecovery"]>;
|
|
4931
|
+
persistence: McpOperationPersistence;
|
|
4932
|
+
destinationDigest: string;
|
|
4933
|
+
supported: boolean;
|
|
4934
|
+
};
|
|
4935
|
+
|
|
4936
|
+
const mcpOperationRecoveryByServer = new WeakMap<PrefixedMcpServer, PreparedMcpOperationRecovery>();
|
|
4937
|
+
|
|
4938
|
+
function configureMcpOperationRecovery(
|
|
4939
|
+
server: PrefixedMcpServer,
|
|
4940
|
+
config: Settings["mcpServers"][number],
|
|
4941
|
+
options: PrepareToolsOptions,
|
|
4942
|
+
destinationUrl: string,
|
|
4943
|
+
brokered: boolean,
|
|
4944
|
+
): PrefixedMcpServer {
|
|
4945
|
+
if (config.operationRecovery && options.mcpOperationPersistence) {
|
|
4946
|
+
mcpOperationRecoveryByServer.set(server, {
|
|
4947
|
+
policy: McpOperationRecoverySchema.parse(config.operationRecovery),
|
|
4948
|
+
persistence: options.mcpOperationPersistence,
|
|
4949
|
+
destinationDigest: digestCanonicalJson(new URL(destinationUrl).toString()),
|
|
4950
|
+
supported: brokered && attemptToolScope(options) !== null,
|
|
4951
|
+
});
|
|
4952
|
+
}
|
|
4953
|
+
return server;
|
|
4954
|
+
}
|
|
4955
|
+
|
|
4956
|
+
function configuredMcpOperationRecovery(server: PrefixedMcpServer, toolName: string) {
|
|
4957
|
+
const recovery = mcpOperationRecoveryByServer.get(server);
|
|
4958
|
+
return recovery && Object.hasOwn(recovery.policy, toolName) ? recovery : undefined;
|
|
4959
|
+
}
|
|
4960
|
+
|
|
4821
4961
|
function attemptToolSource(serverId: string): ToolGatewayDefinition["source"] {
|
|
4822
4962
|
if (serverId === "opengeni" || serverId === "files" || serverId === "docs") {
|
|
4823
4963
|
return serverId;
|
|
@@ -4889,6 +5029,35 @@ function connectionBrokerFetch(
|
|
|
4889
5029
|
suppressSetupAuthNeeded,
|
|
4890
5030
|
);
|
|
4891
5031
|
}
|
|
5032
|
+
if (request.method === "tools/call") {
|
|
5033
|
+
assertMcpOperationObservationAuthority({
|
|
5034
|
+
serverId: config.id,
|
|
5035
|
+
...(request.toolName === undefined ? {} : { toolName: request.toolName }),
|
|
5036
|
+
destinationDigest: digestCanonicalJson(destinationUrl),
|
|
5037
|
+
...(first.operationAuthorityDigest === undefined
|
|
5038
|
+
? {}
|
|
5039
|
+
: { authorityDigest: first.operationAuthorityDigest }),
|
|
5040
|
+
});
|
|
5041
|
+
if (
|
|
5042
|
+
hasActiveRecoverableMcpOperation() &&
|
|
5043
|
+
(request.batch ||
|
|
5044
|
+
request.id === undefined ||
|
|
5045
|
+
request.id === null ||
|
|
5046
|
+
!request.operationId ||
|
|
5047
|
+
!request.toolName)
|
|
5048
|
+
) {
|
|
5049
|
+
throw new Error("Recoverable MCP request is missing its exact dispatch identity");
|
|
5050
|
+
}
|
|
5051
|
+
// The helper is a no-op without its private context. Always enter it for
|
|
5052
|
+
// tools/call so a late transport continuation cannot bypass a closed context.
|
|
5053
|
+
await captureMcpOperationDispatch({
|
|
5054
|
+
operationId: request.operationId ?? "",
|
|
5055
|
+
serverId: config.id,
|
|
5056
|
+
toolName: request.toolName ?? "",
|
|
5057
|
+
destinationDigest: digestCanonicalJson(destinationUrl),
|
|
5058
|
+
authorityDigest: first.operationAuthorityDigest ?? "",
|
|
5059
|
+
});
|
|
5060
|
+
}
|
|
4892
5061
|
const response = await baseFetch(
|
|
4893
5062
|
fetchInputForAttempt(input),
|
|
4894
5063
|
withConnectionHeaders(input, init, first.headers),
|
|
@@ -6387,9 +6556,11 @@ class AttemptDefinitionMcpServer implements MCPServer {
|
|
|
6387
6556
|
): Promise<any> {
|
|
6388
6557
|
if (this.closed) throw new Error("local model tool server is closed");
|
|
6389
6558
|
const environment = await this.requiredAttemptToolEnvironment();
|
|
6559
|
+
const sourceCallId = modelMcpSourceCallId(toolName);
|
|
6390
6560
|
return await this.resultCustomDataBridge.captureResult(args, async (cleanArgs) =>
|
|
6391
6561
|
environment.callModel({
|
|
6392
6562
|
modelName: toolName,
|
|
6563
|
+
...(sourceCallId === undefined ? {} : { sourceCallId }),
|
|
6393
6564
|
arguments: cleanArgs ?? {},
|
|
6394
6565
|
subjectId: this.subjectId,
|
|
6395
6566
|
...(meta === undefined ? {} : { transportMeta: meta }),
|
|
@@ -6711,8 +6882,10 @@ export class PrefixedMcpServer implements MCPServer {
|
|
|
6711
6882
|
}
|
|
6712
6883
|
return await this.resultCustomDataBridge.captureResult(args, async (cleanArgs) => {
|
|
6713
6884
|
if (this.attemptToolEnvironment) {
|
|
6885
|
+
const sourceCallId = modelMcpSourceCallId(toolName);
|
|
6714
6886
|
return await this.attemptToolEnvironment.callModel({
|
|
6715
6887
|
modelName: toolName,
|
|
6888
|
+
...(sourceCallId === undefined ? {} : { sourceCallId }),
|
|
6716
6889
|
arguments: cleanArgs ?? {},
|
|
6717
6890
|
subjectId: this.attemptToolSubjectId,
|
|
6718
6891
|
...(meta === undefined ? {} : { transportMeta: meta }),
|
|
@@ -6741,6 +6914,9 @@ export class PrefixedMcpServer implements MCPServer {
|
|
|
6741
6914
|
if (!this.isAllowed(unprefixed)) {
|
|
6742
6915
|
throw new Error(`MCP tool ${unprefixed} is not allowed for server ${this.registryId}`);
|
|
6743
6916
|
}
|
|
6917
|
+
if (configuredMcpOperationRecovery(this, unprefixed) && !hasActiveRecoverableMcpOperation()) {
|
|
6918
|
+
throw new Error("MCP operation recovery requires its trusted attempt execution context");
|
|
6919
|
+
}
|
|
6744
6920
|
// Nested prefix wrappers are a projection seam around one physical
|
|
6745
6921
|
// tools/call. The innermost wrapper owns the single metric observation.
|
|
6746
6922
|
const recordsPhysicalCall = !(this.inner instanceof PrefixedMcpServer);
|
|
@@ -6814,6 +6990,16 @@ export class PrefixedMcpServer implements MCPServer {
|
|
|
6814
6990
|
// state. The broker refreshed credentials for future requests but did not
|
|
6815
6991
|
// replay this call. Preserve that ambiguity as an explicit model-visible
|
|
6816
6992
|
// error for required and best-effort servers alike.
|
|
6993
|
+
if (hasActiveRecoverableMcpOperation()) {
|
|
6994
|
+
recordOutcome(
|
|
6995
|
+
isToolOutcomeUncertainMcpError(error)
|
|
6996
|
+
? "outcome_uncertain"
|
|
6997
|
+
: isAuthNeededMcpError(error)
|
|
6998
|
+
? "auth_needed"
|
|
6999
|
+
: mcpThrownToolCallOutcome(error, options?.signal),
|
|
7000
|
+
);
|
|
7001
|
+
throw error;
|
|
7002
|
+
}
|
|
6817
7003
|
if (isToolOutcomeUncertainMcpError(error)) {
|
|
6818
7004
|
recordOutcome("outcome_uncertain");
|
|
6819
7005
|
return boundedMcpToolResult({
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import type { AttemptToolResult } from "@opengeni/contracts";
|
|
3
|
+
|
|
4
|
+
export type RecoverableMcpOperation = {
|
|
5
|
+
operationId: string;
|
|
6
|
+
sourceCallId?: string;
|
|
7
|
+
serverId: string;
|
|
8
|
+
originalTool: string;
|
|
9
|
+
observerTool: string;
|
|
10
|
+
destinationDigest: string;
|
|
11
|
+
argumentDigest: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type CapturedMcpOperation = RecoverableMcpOperation & { authorityDigest: string };
|
|
15
|
+
|
|
16
|
+
/** Worker-owned persistence. Its implementation must enforce canonical live
|
|
17
|
+
* attempt identity and store no arguments, URLs, headers, or credentials. */
|
|
18
|
+
export type McpOperationPersistence = {
|
|
19
|
+
capture: (operation: CapturedMcpOperation) => Promise<"created" | "existing">;
|
|
20
|
+
settleOriginal: (
|
|
21
|
+
input:
|
|
22
|
+
| { operationId: string; outcome: "completed"; result: AttemptToolResult }
|
|
23
|
+
| { operationId: string; outcome: "outcome_unknown" },
|
|
24
|
+
) => Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type DispatchContext = {
|
|
28
|
+
operation: RecoverableMcpOperation;
|
|
29
|
+
persistence: McpOperationPersistence;
|
|
30
|
+
captureStarted: boolean;
|
|
31
|
+
captured: boolean;
|
|
32
|
+
closed: boolean;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const dispatchContext = new AsyncLocalStorage<DispatchContext>();
|
|
36
|
+
|
|
37
|
+
/** Presence is an execution-routing fact, not authorization. A closed context
|
|
38
|
+
* remains visible to late callbacks so they still hit the dispatch fence. */
|
|
39
|
+
export function hasActiveRecoverableMcpOperation(): boolean {
|
|
40
|
+
return dispatchContext.getStore() !== undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class McpOperationOutcomeUnknownError extends Error {
|
|
44
|
+
readonly code = "mcp_operation_outcome_unknown";
|
|
45
|
+
constructor(
|
|
46
|
+
readonly operationId: string,
|
|
47
|
+
cause: unknown,
|
|
48
|
+
) {
|
|
49
|
+
super(
|
|
50
|
+
`MCP operation ${operationId} has an unknown outcome. Read the existing operation result; do not repeat the mutation.`,
|
|
51
|
+
{ cause },
|
|
52
|
+
);
|
|
53
|
+
this.name = "McpOperationOutcomeUnknownError";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Establish trusted in-process correlation around one gateway execution. */
|
|
58
|
+
export async function runRecoverableMcpOperation(
|
|
59
|
+
operation: RecoverableMcpOperation,
|
|
60
|
+
persistence: McpOperationPersistence,
|
|
61
|
+
execute: () => Promise<AttemptToolResult>,
|
|
62
|
+
): Promise<AttemptToolResult> {
|
|
63
|
+
const context: DispatchContext = {
|
|
64
|
+
operation: { ...operation },
|
|
65
|
+
persistence,
|
|
66
|
+
captureStarted: false,
|
|
67
|
+
captured: false,
|
|
68
|
+
closed: false,
|
|
69
|
+
};
|
|
70
|
+
return await dispatchContext.run(context, async () => {
|
|
71
|
+
let result: AttemptToolResult;
|
|
72
|
+
try {
|
|
73
|
+
try {
|
|
74
|
+
result = await execute();
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (!context.captured) throw error;
|
|
77
|
+
// An uncertain settlement acknowledgment cannot authorize replay or
|
|
78
|
+
// replace the original recovery handle with a database-only error.
|
|
79
|
+
try {
|
|
80
|
+
await persistence.settleOriginal({
|
|
81
|
+
operationId: operation.operationId,
|
|
82
|
+
outcome: "outcome_unknown",
|
|
83
|
+
});
|
|
84
|
+
} catch {
|
|
85
|
+
/* The pre-dispatch record remains the recovery authority. */
|
|
86
|
+
}
|
|
87
|
+
throw new McpOperationOutcomeUnknownError(operation.operationId, error);
|
|
88
|
+
}
|
|
89
|
+
if (!context.captured) {
|
|
90
|
+
throw new Error("Recoverable MCP execution did not cross its durable capture boundary");
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
await persistence.settleOriginal({
|
|
94
|
+
operationId: operation.operationId,
|
|
95
|
+
outcome: "completed",
|
|
96
|
+
result,
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
// Never submit a second, contradictory settlement after an ambiguous
|
|
100
|
+
// acknowledgment of the successful one.
|
|
101
|
+
throw new McpOperationOutcomeUnknownError(operation.operationId, error);
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
} finally {
|
|
105
|
+
context.closed = true;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Called by the authorized broker immediately before the physical request.
|
|
111
|
+
* A second dispatch with the same identity is never an observation. */
|
|
112
|
+
export async function captureMcpOperationDispatch(input: {
|
|
113
|
+
operationId: string;
|
|
114
|
+
serverId: string;
|
|
115
|
+
toolName: string;
|
|
116
|
+
destinationDigest: string;
|
|
117
|
+
authorityDigest: string;
|
|
118
|
+
}): Promise<void> {
|
|
119
|
+
const context = dispatchContext.getStore();
|
|
120
|
+
if (!context) return;
|
|
121
|
+
const operation = context.operation;
|
|
122
|
+
if (
|
|
123
|
+
input.operationId !== operation.operationId ||
|
|
124
|
+
input.serverId !== operation.serverId ||
|
|
125
|
+
input.toolName !== operation.originalTool ||
|
|
126
|
+
input.destinationDigest !== operation.destinationDigest ||
|
|
127
|
+
!/^[a-f0-9]{64}$/u.test(input.authorityDigest)
|
|
128
|
+
)
|
|
129
|
+
throw new Error("Recoverable MCP dispatch identity does not match its trusted binding");
|
|
130
|
+
if (context.closed || context.captureStarted) {
|
|
131
|
+
throw new Error("MCP operation dispatch is already claimed or closed");
|
|
132
|
+
}
|
|
133
|
+
context.captureStarted = true;
|
|
134
|
+
const result = await context.persistence.capture({
|
|
135
|
+
...operation,
|
|
136
|
+
authorityDigest: input.authorityDigest,
|
|
137
|
+
});
|
|
138
|
+
if (result !== "created")
|
|
139
|
+
throw new Error("MCP operation was already dispatched; read its existing result");
|
|
140
|
+
// The SDK may have timed out while capture was committing. Do not start a
|
|
141
|
+
// mutation after its enclosing execution has already returned.
|
|
142
|
+
if (context.closed) throw new Error("MCP operation dispatch closed during capture");
|
|
143
|
+
context.captured = true;
|
|
144
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { AttemptToolResult } from "@opengeni/contracts";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
|
+
|
|
5
|
+
export type McpOperationObservationAuthority = {
|
|
6
|
+
serverId: string;
|
|
7
|
+
observerTool: string;
|
|
8
|
+
destinationDigest: string;
|
|
9
|
+
authorityDigest: string;
|
|
10
|
+
};
|
|
11
|
+
const observationAuthority = new AsyncLocalStorage<{
|
|
12
|
+
binding: McpOperationObservationAuthority;
|
|
13
|
+
closed: boolean;
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
/** Pin the actual broker request, not merely the earlier credential lookup. */
|
|
17
|
+
export async function runMcpOperationObservationWithAuthority<T>(
|
|
18
|
+
binding: McpOperationObservationAuthority,
|
|
19
|
+
execute: () => Promise<T>,
|
|
20
|
+
): Promise<T> {
|
|
21
|
+
const scope = { binding: { ...binding }, closed: false };
|
|
22
|
+
return await observationAuthority.run(scope, async () => {
|
|
23
|
+
try {
|
|
24
|
+
return await execute();
|
|
25
|
+
} finally {
|
|
26
|
+
scope.closed = true;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The caller is the credential broker, after live authorization and before
|
|
32
|
+
* physical tools/call dispatch. Unrelated ordinary calls remain unchanged. */
|
|
33
|
+
export function assertMcpOperationObservationAuthority(actual: {
|
|
34
|
+
serverId: string;
|
|
35
|
+
toolName?: string;
|
|
36
|
+
destinationDigest: string;
|
|
37
|
+
authorityDigest?: string;
|
|
38
|
+
}): void {
|
|
39
|
+
const scope = observationAuthority.getStore();
|
|
40
|
+
if (!scope) return;
|
|
41
|
+
const expected = scope.binding;
|
|
42
|
+
if (
|
|
43
|
+
scope.closed ||
|
|
44
|
+
actual.serverId !== expected.serverId ||
|
|
45
|
+
actual.toolName !== expected.observerTool ||
|
|
46
|
+
actual.destinationDigest !== expected.destinationDigest ||
|
|
47
|
+
actual.authorityDigest !== expected.authorityDigest
|
|
48
|
+
) {
|
|
49
|
+
throw new Error("MCP operation observation authority changed before provider use");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const fingerprint = z
|
|
54
|
+
.object({
|
|
55
|
+
version: z.literal(1),
|
|
56
|
+
algorithm: z.literal("sha256"),
|
|
57
|
+
value: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
58
|
+
})
|
|
59
|
+
.strict();
|
|
60
|
+
|
|
61
|
+
const identity = {
|
|
62
|
+
version: z.literal(1),
|
|
63
|
+
operationRef: z.string().uuid(),
|
|
64
|
+
fingerprint,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const receiptSchema = z.discriminatedUnion("status", [
|
|
68
|
+
z.object({ ...identity, status: z.literal("unknown") }).strict(),
|
|
69
|
+
z
|
|
70
|
+
.object({
|
|
71
|
+
...identity,
|
|
72
|
+
status: z.literal("pending"),
|
|
73
|
+
evidenceRevision: z.string().min(1),
|
|
74
|
+
})
|
|
75
|
+
.strict(),
|
|
76
|
+
z
|
|
77
|
+
.object({
|
|
78
|
+
...identity,
|
|
79
|
+
status: z.literal("completed"),
|
|
80
|
+
receiptRevision: z
|
|
81
|
+
.string()
|
|
82
|
+
.min(1)
|
|
83
|
+
.refine((value) => Buffer.byteLength(value, "utf8") <= 1024, {
|
|
84
|
+
message: "MCP receipt revision must fit the ledger's 1024-byte identity field",
|
|
85
|
+
}),
|
|
86
|
+
result: AttemptToolResult,
|
|
87
|
+
})
|
|
88
|
+
.strict(),
|
|
89
|
+
z.object({ ...identity, status: z.literal("conflict") }).strict(),
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
const bindingSchema = z
|
|
93
|
+
.object({
|
|
94
|
+
operationId: z.string().uuid(),
|
|
95
|
+
serverId: z.string().min(1),
|
|
96
|
+
originalTool: z.string().min(1),
|
|
97
|
+
observerTool: z.string().min(1),
|
|
98
|
+
argumentDigest: fingerprint.shape.value,
|
|
99
|
+
})
|
|
100
|
+
.strict()
|
|
101
|
+
.refine((value) => value.originalTool !== value.observerTool, {
|
|
102
|
+
message: "The observer must not be the original mutation tool",
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export type McpObservationBinding = z.infer<typeof bindingSchema>;
|
|
106
|
+
export type McpObservationReceipt = z.infer<typeof receiptSchema>;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* One observation, never a mutation retry. This helper is deliberately not an
|
|
110
|
+
* authority resolver or a scheduler: its caller must load an immutable trusted
|
|
111
|
+
* binding and bind callObserver to that exact authorized provider destination.
|
|
112
|
+
* Tool annotations and model-supplied arguments cannot establish that trust.
|
|
113
|
+
*
|
|
114
|
+
* Durable settlement must additionally recheck authority under its own locks.
|
|
115
|
+
* A successful return alone is not a durable delivery or commit receipt.
|
|
116
|
+
*/
|
|
117
|
+
export async function observeMcpOperation(input: {
|
|
118
|
+
binding: McpObservationBinding;
|
|
119
|
+
authorize: (phase: "before_request" | "before_delivery") => Promise<boolean>;
|
|
120
|
+
callObserver: (tool: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
121
|
+
}): Promise<McpObservationReceipt> {
|
|
122
|
+
// Parse into a fresh object before callbacks can mutate the caller's input.
|
|
123
|
+
const binding = bindingSchema.parse(input.binding);
|
|
124
|
+
if (!(await input.authorize("before_request"))) {
|
|
125
|
+
throw new Error("MCP operation observation is not authorized");
|
|
126
|
+
}
|
|
127
|
+
const response = await input.callObserver(binding.observerTool, {
|
|
128
|
+
version: 1,
|
|
129
|
+
operationRef: binding.operationId,
|
|
130
|
+
originalTool: binding.originalTool,
|
|
131
|
+
fingerprint: { version: 1, algorithm: "sha256", value: binding.argumentDigest },
|
|
132
|
+
});
|
|
133
|
+
if (!(await input.authorize("before_delivery"))) {
|
|
134
|
+
throw new Error("MCP operation observation is not authorized for delivery");
|
|
135
|
+
}
|
|
136
|
+
const envelope = AttemptToolResult.parse(response);
|
|
137
|
+
if (envelope.isError) {
|
|
138
|
+
throw new Error("MCP operation observer returned an error, not a receipt");
|
|
139
|
+
}
|
|
140
|
+
const receipt = receiptSchema.parse(envelope.structuredContent);
|
|
141
|
+
if (
|
|
142
|
+
receipt.operationRef !== binding.operationId ||
|
|
143
|
+
receipt.fingerprint.value !== binding.argumentDigest
|
|
144
|
+
) {
|
|
145
|
+
throw new Error("MCP operation observation receipt identity does not match");
|
|
146
|
+
}
|
|
147
|
+
return receipt;
|
|
148
|
+
}
|