@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.
Files changed (32) hide show
  1. package/dist/{agent-Q2VNH5FV.js → agent-RJQLKFIW.js} +2 -2
  2. package/dist/{chunk-CD22I76N.js → chunk-FHGLYLBL.js} +2 -2
  3. package/dist/{chunk-GJC7H2FC.js → chunk-SD6MDONS.js} +350 -67
  4. package/dist/chunk-SD6MDONS.js.map +1 -0
  5. package/dist/{chunk-25B3EQRW.js → chunk-UNICCRW5.js} +4 -4
  6. package/dist/{chunk-LGLJ5PVD.js → chunk-VVEGJPDR.js} +2309 -1345
  7. package/dist/chunk-VVEGJPDR.js.map +1 -0
  8. package/dist/index.d.ts +7 -0
  9. package/dist/index.js +22 -4
  10. package/dist/{localSources-Z5IJIIRV.js → localSources-PVFCAPN2.js} +3 -3
  11. package/dist/mcp-operation-dispatch.d.ts +45 -0
  12. package/dist/mcp-operation-observation.d.ts +137 -0
  13. package/dist/sandbox/archive-spool.d.ts +14 -0
  14. package/dist/sandbox/host-archive-spool.d.ts +19 -0
  15. package/dist/sandbox/index.d.ts +5 -1
  16. package/dist/sandbox/index.js +7 -3
  17. package/dist/sandbox/workspace-archive.d.ts +7 -0
  18. package/dist/workspace-tool-gateway.js +4 -4
  19. package/package.json +11 -11
  20. package/src/index.ts +231 -45
  21. package/src/mcp-operation-dispatch.ts +144 -0
  22. package/src/mcp-operation-observation.ts +148 -0
  23. package/src/sandbox/archive-spool.ts +16 -0
  24. package/src/sandbox/host-archive-spool.ts +1040 -0
  25. package/src/sandbox/index.ts +292 -203
  26. package/src/sandbox/workspace-archive.ts +54 -1
  27. package/dist/chunk-GJC7H2FC.js.map +0 -1
  28. package/dist/chunk-LGLJ5PVD.js.map +0 -1
  29. /package/dist/{agent-Q2VNH5FV.js.map → agent-RJQLKFIW.js.map} +0 -0
  30. /package/dist/{chunk-CD22I76N.js.map → chunk-FHGLYLBL.js.map} +0 -0
  31. /package/dist/{chunk-25B3EQRW.js.map → chunk-UNICCRW5.js.map} +0 -0
  32. /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: new PrefixedMcpServer(
3990
- local.server,
3991
- config.id,
3992
- config.allowedTools,
3993
- optional || Boolean(config.connectionRef),
3994
- aggregateToolBudget,
3995
- `${config.id}:${index}`,
3996
- false,
3997
- buildConnectorAttachmentAuthority(
3998
- config,
3999
- options,
4000
- resolvedMcpToolConnectionIds,
4001
- config.url,
4002
- local.resolvedConnectionId,
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
- tool.eager !== true,
4005
- local.preflightCall,
4006
- local.approvalAuthority,
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 = new PrefixedMcpServer(
4125
- innerServer,
4126
- config.id,
4127
- config.allowedTools,
4128
- bestEffort,
4129
- aggregateToolBudget,
4130
- `${config.id}:${index}`,
4131
- firstParty && !bestEffort,
4132
- buildConnectorAttachmentAuthority(config, options, resolvedMcpToolConnectionIds, url),
4133
- tool.eager !== true,
4134
- undefined,
4135
- undefined,
4136
- firstParty && config.id === "opengeni" && !config.connectionRef && !bridge
4137
- ? inputWaitYield
4138
- : undefined,
4139
- firstParty && config.id === "opengeni" && !config.connectionRef && !bridge
4140
- ? options.refreshOwnedCommand
4141
- : undefined,
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
- await server.executeCatalogTool(
4793
- toolName,
4794
- args,
4795
- {
4796
- ...(context.transportMeta ?? {}),
4797
- opengeniOperationId: context.operationId,
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
- ...(context.signal ? { signal: context.signal } : {}),
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
+ }