@opengeni/worker-bundle 0.28.3 → 0.28.4-canary.1
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/activities/agent-turn/codex-selection-diagnostics.d.ts +1 -0
- package/dist/activities/agent-turn/mcp-operation-read-tool.d.ts +16 -0
- package/dist/activities/codex-fleet-shadow.d.ts +1 -1
- package/dist/activities/mcp-operation-authority.d.ts +16 -0
- package/dist/activities/mcp-operation-observer.d.ts +14 -0
- package/dist/activities/mcp-operation-reader.d.ts +45 -0
- package/dist/activities/mcp-operation-store.d.ts +6 -0
- package/dist/activities/sandbox-lease.d.ts +12 -4
- package/dist/{activities-control-UXGPMDL3.js → activities-control-MKGEOBXT.js} +90 -71
- package/dist/activities-control-MKGEOBXT.js.map +1 -0
- package/dist/{activities-turn-NOYDE7PX.js → activities-turn-5MZZ6HQA.js} +15691 -815
- package/dist/activities-turn-5MZZ6HQA.js.map +1 -0
- package/dist/artifact-materializer-entry.js +2 -0
- package/dist/artifact-materializer-entry.js.map +1 -1
- package/dist/artifact-outbox-entry.js +2 -0
- package/dist/artifact-outbox-entry.js.map +1 -1
- package/dist/{chunk-BARTLVBK.js → chunk-A4XQWVZK.js} +25 -25
- package/dist/chunk-A4XQWVZK.js.map +1 -0
- package/dist/{chunk-NNXLW6V7.js → chunk-OA5CSY5N.js} +147 -112
- package/dist/chunk-OA5CSY5N.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/host-export.js +1 -0
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/observability-metrics.d.ts +12 -4
- package/dist/sandbox-archive-storage.d.ts +15 -3
- package/dist/sandbox-resume.d.ts +1 -1
- package/dist/sandbox-routing.d.ts +6 -1
- package/package.json +18 -17
- package/src/activities/agent-turn/claim.ts +3 -3
- package/src/activities/agent-turn/codex-capacity.ts +31 -53
- package/src/activities/agent-turn/codex-selection-diagnostics.ts +1 -0
- package/src/activities/agent-turn/finalization.ts +10 -5
- package/src/activities/agent-turn/mcp-operation-read-tool.ts +61 -0
- package/src/activities/agent-turn/sandbox-establish.ts +2 -0
- package/src/activities/agent-turn/skill-search.ts +2 -16
- package/src/activities/agent-turn/stream-attempt.ts +1 -1
- package/src/activities/agent-turn/tool-environment.ts +65 -0
- package/src/activities/codex-fleet-shadow.ts +3 -0
- package/src/activities/mcp-credentials.ts +94 -40
- package/src/activities/mcp-operation-authority.ts +76 -0
- package/src/activities/mcp-operation-observer.ts +86 -0
- package/src/activities/mcp-operation-reader.ts +108 -0
- package/src/activities/mcp-operation-store.ts +98 -0
- package/src/activities/sandbox-lease.ts +111 -73
- package/src/observability-metrics.ts +28 -24
- package/src/sandbox-archive-storage.ts +82 -18
- package/src/sandbox-resume.ts +79 -63
- package/src/sandbox-routing.ts +5 -1
- package/dist/activities-control-UXGPMDL3.js.map +0 -1
- package/dist/activities-turn-NOYDE7PX.js.map +0 -1
- package/dist/chunk-BARTLVBK.js.map +0 -1
- package/dist/chunk-NNXLW6V7.js.map +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Settings } from "@opengeni/config";
|
|
2
2
|
import type { ConnectionCredentialsPort } from "@opengeni/contracts";
|
|
3
|
+
import type { HostMcpAcceptedAuthority } from "@opengeni/contracts/host-mcp-bindings";
|
|
3
4
|
import {
|
|
4
5
|
buildConnectionTokenResolver,
|
|
5
6
|
buildHostConnectionTokenResolver,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
type SessionTurnForExecution,
|
|
14
15
|
} from "@opengeni/db";
|
|
15
16
|
import { recordTenancyCompatibilityLaneUse, type Observability } from "@opengeni/observability";
|
|
17
|
+
import { mcpOperationAuthorityDigest } from "./mcp-operation-authority";
|
|
16
18
|
|
|
17
19
|
const OPENGENI_CONNECTION_ID_PATTERN =
|
|
18
20
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
@@ -39,46 +41,78 @@ export function connectionTokenResolverForTurn(input: {
|
|
|
39
41
|
const readHostTurn = input.getHostTurnForAttempt ?? getSessionTurnForAttempt;
|
|
40
42
|
const hostDb = input.db;
|
|
41
43
|
const nativeResolver = buildConnectionTokenResolver(input.db, input.settings);
|
|
44
|
+
const recoveryEnabled = (serverId: string) =>
|
|
45
|
+
input.settings.mcpServers.some(
|
|
46
|
+
(server) =>
|
|
47
|
+
server.id === serverId &&
|
|
48
|
+
"operationRecovery" in server &&
|
|
49
|
+
server.operationRecovery !== undefined &&
|
|
50
|
+
Object.keys(server.operationRecovery ?? {}).length > 0,
|
|
51
|
+
);
|
|
42
52
|
const configuredResolver = hostResolver
|
|
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
|
-
|
|
53
|
+
? async (
|
|
54
|
+
credentialRequest: ResolveConnectionCredentialInput,
|
|
55
|
+
): Promise<ResolveConnectionCredentialResult> => {
|
|
56
|
+
let authorizedSnapshot: HostMcpAcceptedAuthority | undefined;
|
|
57
|
+
const wantsDigest = recoveryEnabled(credentialRequest.serverId);
|
|
58
|
+
const resolver = buildHostConnectionTokenResolver(hostResolver, {
|
|
59
|
+
accountId: input.accountId,
|
|
60
|
+
workspaceId: input.workspaceId,
|
|
61
|
+
sessionId: input.sessionId,
|
|
62
|
+
rootSessionId: input.rootSessionId,
|
|
63
|
+
turnId: input.turn.id,
|
|
64
|
+
attemptId: input.attemptId,
|
|
65
|
+
executionGeneration: input.turn.executionGeneration,
|
|
66
|
+
initiator: input.turn.initiator,
|
|
67
|
+
initiatorContext: input.turn.initiatorContext,
|
|
68
|
+
surface: "model",
|
|
69
|
+
// Only immutable captured direct-turn authority is currently supported.
|
|
70
|
+
// Direct, scheduled and exact causal/child snapshots are validated;
|
|
71
|
+
// no authority follows merely from a binding or creator identity.
|
|
72
|
+
authorizeDurableBinding: (request) =>
|
|
73
|
+
authorizeDirectHostMcpUse(
|
|
74
|
+
hostDb,
|
|
75
|
+
request,
|
|
76
|
+
wantsDigest
|
|
77
|
+
? (snapshot) => {
|
|
78
|
+
authorizedSnapshot = snapshot;
|
|
79
|
+
}
|
|
80
|
+
: undefined,
|
|
81
|
+
),
|
|
82
|
+
authorizeExecution: async (request) => {
|
|
83
|
+
// Native UUID references already pass the accepted-use boundary below.
|
|
84
|
+
// Legacy opaque host IDs retain their compatibility path, but not a
|
|
85
|
+
// right to execute after their accepted attempt has stopped.
|
|
86
|
+
if (
|
|
87
|
+
request.connectionRef.authoritySource !== "host" &&
|
|
88
|
+
(!request.connectionRef.connectionId ||
|
|
89
|
+
OPENGENI_CONNECTION_ID_PATTERN.test(request.connectionRef.connectionId))
|
|
90
|
+
)
|
|
91
|
+
return true;
|
|
92
|
+
if (!request.attemptId) return false;
|
|
93
|
+
const current = await readHostTurn(
|
|
94
|
+
hostDb,
|
|
95
|
+
request.workspaceId,
|
|
96
|
+
request.sessionId,
|
|
97
|
+
request.attemptId,
|
|
98
|
+
);
|
|
99
|
+
return (
|
|
100
|
+
current !== null &&
|
|
101
|
+
current.id === request.turnId &&
|
|
102
|
+
current.executionGeneration === request.executionGeneration
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
const result = await resolver(credentialRequest);
|
|
107
|
+
return result.status === "ok" && authorizedSnapshot
|
|
108
|
+
? {
|
|
109
|
+
...result,
|
|
110
|
+
operationAuthorityDigest: mcpOperationAuthorityDigest(credentialRequest, {
|
|
111
|
+
host: authorizedSnapshot,
|
|
112
|
+
}),
|
|
113
|
+
}
|
|
114
|
+
: result;
|
|
115
|
+
}
|
|
82
116
|
: nativeResolver;
|
|
83
117
|
const baseResolver = (request: ResolveConnectionCredentialInput) =>
|
|
84
118
|
input.connectionCredentials?.mcpAuthoritySource === "host" &&
|
|
@@ -229,10 +263,30 @@ export function connectionTokenResolverForTurn(input: {
|
|
|
229
263
|
});
|
|
230
264
|
const withProviderRequestAuthorization = (
|
|
231
265
|
result: ResolveConnectionCredentialResult,
|
|
266
|
+
attribution = result.status === "ok" ? result.connectionUseAttribution : undefined,
|
|
232
267
|
): ResolveConnectionCredentialResult => {
|
|
233
268
|
if (result.status !== "ok") return result;
|
|
234
269
|
return {
|
|
235
270
|
...result,
|
|
271
|
+
...(attribution && recoveryEnabled(request.serverId)
|
|
272
|
+
? {
|
|
273
|
+
operationAuthorityDigest: mcpOperationAuthorityDigest(request, {
|
|
274
|
+
native: attribution,
|
|
275
|
+
// Match the canonical DB recovery principal: a causal human
|
|
276
|
+
// survives goal/service continuations. These fields come from
|
|
277
|
+
// the accepted turn, never request.subjectId or caller JSON.
|
|
278
|
+
principal: input.turn.initiatingHumanSubjectId
|
|
279
|
+
? { kind: "subject", subjectId: input.turn.initiatingHumanSubjectId }
|
|
280
|
+
: {
|
|
281
|
+
kind: input.turn.initiator.kind,
|
|
282
|
+
...(input.turn.initiator.kind === "subject" ||
|
|
283
|
+
input.turn.initiator.kind === "service"
|
|
284
|
+
? { subjectId: input.turn.initiator.subjectId }
|
|
285
|
+
: {}),
|
|
286
|
+
},
|
|
287
|
+
}),
|
|
288
|
+
}
|
|
289
|
+
: {}),
|
|
236
290
|
authorizeProviderRequest: async () => {
|
|
237
291
|
const authorization = await authorize({
|
|
238
292
|
...credentialUseContext,
|
|
@@ -286,7 +340,7 @@ export function connectionTokenResolverForTurn(input: {
|
|
|
286
340
|
? { subjectId: authorization.attribution.ownerSubjectId }
|
|
287
341
|
: {}),
|
|
288
342
|
});
|
|
289
|
-
return withProviderRequestAuthorization(result);
|
|
343
|
+
return withProviderRequestAuthorization(result, authorization.attribution);
|
|
290
344
|
};
|
|
291
345
|
}
|
|
292
346
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { stableJson } from "@opengeni/contracts";
|
|
3
|
+
import type { ConnectionUseAttribution } from "@opengeni/contracts/connection-authority";
|
|
4
|
+
import type { HostMcpAcceptedAuthority } from "@opengeni/contracts/host-mcp-bindings";
|
|
5
|
+
import type { ResolveConnectionCredentialInput } from "@opengeni/db";
|
|
6
|
+
|
|
7
|
+
/** Metadata only. Callers must obtain authority from a current live fence.
|
|
8
|
+
* This equality key is never authorization and never replaces physical-use checks. */
|
|
9
|
+
export function mcpOperationAuthorityDigest(
|
|
10
|
+
request: ResolveConnectionCredentialInput,
|
|
11
|
+
authority:
|
|
12
|
+
| {
|
|
13
|
+
native: ConnectionUseAttribution;
|
|
14
|
+
host?: never;
|
|
15
|
+
principal?: { kind: string; subjectId?: string };
|
|
16
|
+
}
|
|
17
|
+
| { host: HostMcpAcceptedAuthority; native?: never },
|
|
18
|
+
): string {
|
|
19
|
+
const n = authority.native;
|
|
20
|
+
const h = authority.host;
|
|
21
|
+
const ref = request.connectionRef;
|
|
22
|
+
const metadata = {
|
|
23
|
+
version: 1,
|
|
24
|
+
serverId: request.serverId,
|
|
25
|
+
destinationUrl: request.destinationUrl,
|
|
26
|
+
credentialTarget: request.credentialTarget ?? "mcp",
|
|
27
|
+
connectionRef: {
|
|
28
|
+
authoritySource: ref.authoritySource,
|
|
29
|
+
connectionId: ref.connectionId,
|
|
30
|
+
provider: ref.provider,
|
|
31
|
+
providerDomain: ref.providerDomain,
|
|
32
|
+
kind: ref.kind,
|
|
33
|
+
subjectScope: ref.subjectScope,
|
|
34
|
+
hostBinding: ref.hostBinding,
|
|
35
|
+
scopes: ref.scopes ? [...ref.scopes].sort() : undefined,
|
|
36
|
+
resource: ref.resource,
|
|
37
|
+
selectedResources: ref.selectedResources
|
|
38
|
+
? [...ref.selectedResources].sort((a, b) =>
|
|
39
|
+
stableJson(a) < stableJson(b) ? -1 : stableJson(a) > stableJson(b) ? 1 : 0,
|
|
40
|
+
)
|
|
41
|
+
: undefined,
|
|
42
|
+
},
|
|
43
|
+
native: n
|
|
44
|
+
? {
|
|
45
|
+
organizationId: n.organizationId,
|
|
46
|
+
workspaceId: n.workspaceId,
|
|
47
|
+
sessionId: n.sessionId,
|
|
48
|
+
connectionId: n.connectionId,
|
|
49
|
+
connectionGeneration: n.connectionGeneration,
|
|
50
|
+
scope: n.scope,
|
|
51
|
+
ownerSubjectId: n.ownerSubjectId,
|
|
52
|
+
authorityId: n.authorityId,
|
|
53
|
+
grantId: n.grantId,
|
|
54
|
+
principal: "principal" in authority ? authority.principal : undefined,
|
|
55
|
+
}
|
|
56
|
+
: undefined,
|
|
57
|
+
host: h
|
|
58
|
+
? {
|
|
59
|
+
accountId: h.accountId,
|
|
60
|
+
workspaceId: h.workspaceId,
|
|
61
|
+
targetSessionId: h.targetSessionId,
|
|
62
|
+
targetSessionVisibility: h.targetSessionVisibility,
|
|
63
|
+
targetSessionAuthorityEpoch: h.targetSessionAuthorityEpoch,
|
|
64
|
+
ownerSubjectId: h.ownerSubjectId,
|
|
65
|
+
ownerOrganizationMembershipId: h.ownerOrganizationMembershipId,
|
|
66
|
+
ownerMembershipAuthorizationRevision: h.ownerMembershipAuthorizationRevision,
|
|
67
|
+
bindingId: h.bindingId,
|
|
68
|
+
bindingGeneration: h.bindingGeneration,
|
|
69
|
+
definition: h.definition,
|
|
70
|
+
delegationId: h.delegationId,
|
|
71
|
+
delegationGeneration: h.delegationGeneration,
|
|
72
|
+
}
|
|
73
|
+
: undefined,
|
|
74
|
+
};
|
|
75
|
+
return createHash("sha256").update(stableJson(metadata)).digest("hex");
|
|
76
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Settings } from "@opengeni/config";
|
|
2
|
+
import type { AttemptToolEnvironment } from "@opengeni/codemode";
|
|
3
|
+
import { digestCanonicalJson } from "@opengeni/tool-gateway";
|
|
4
|
+
import {
|
|
5
|
+
runMcpOperationObservationWithAuthority,
|
|
6
|
+
type ResolveConnectionCredentialInput,
|
|
7
|
+
type ResolveConnectionCredentialResult,
|
|
8
|
+
} from "@opengeni/runtime";
|
|
9
|
+
import type { McpOperationReaderDependencies } from "./mcp-operation-reader";
|
|
10
|
+
|
|
11
|
+
/** Current-attempt adapter: no credential renewal authority survives this
|
|
12
|
+
* closure. The stored digest constrains fresh authority, never establishes it. */
|
|
13
|
+
export function createMcpOperationObserverResolver(input: {
|
|
14
|
+
settings: Pick<Settings, "mcpServers">;
|
|
15
|
+
workspaceId: string;
|
|
16
|
+
credentialSubjectId?: string;
|
|
17
|
+
assertAttempt: () => Promise<void>;
|
|
18
|
+
resolveCredential: (
|
|
19
|
+
request: ResolveConnectionCredentialInput,
|
|
20
|
+
) => Promise<ResolveConnectionCredentialResult>;
|
|
21
|
+
getEnvironment: () => Promise<AttemptToolEnvironment | null>;
|
|
22
|
+
}): McpOperationReaderDependencies["resolveObserver"] {
|
|
23
|
+
return async (operation) => {
|
|
24
|
+
await input.assertAttempt();
|
|
25
|
+
const configured = input.settings.mcpServers.find(
|
|
26
|
+
(candidate) => candidate.id === operation.serverId,
|
|
27
|
+
);
|
|
28
|
+
const recovery = configured?.operationRecovery?.[operation.originalTool];
|
|
29
|
+
if (!configured?.connectionRef || !recovery) return { status: "unsupported" };
|
|
30
|
+
const destinationUrl = new URL(configured.url).toString();
|
|
31
|
+
if (
|
|
32
|
+
recovery.observerTool !== operation.observerTool ||
|
|
33
|
+
digestCanonicalJson(destinationUrl) !== operation.destinationDigest
|
|
34
|
+
) {
|
|
35
|
+
return { status: "binding_changed" };
|
|
36
|
+
}
|
|
37
|
+
const connectionRef = structuredClone(configured.connectionRef);
|
|
38
|
+
const environment = await input.getEnvironment();
|
|
39
|
+
const observerEntry = environment?.catalog.entries.find(
|
|
40
|
+
(entry) =>
|
|
41
|
+
entry.identity.serverId === operation.serverId &&
|
|
42
|
+
entry.identity.toolName === operation.observerTool,
|
|
43
|
+
);
|
|
44
|
+
if (!environment || !observerEntry) return { status: "unsupported" };
|
|
45
|
+
|
|
46
|
+
const authorize = async () => {
|
|
47
|
+
await input.assertAttempt();
|
|
48
|
+
const resolved = await input.resolveCredential({
|
|
49
|
+
workspaceId: input.workspaceId,
|
|
50
|
+
serverId: operation.serverId,
|
|
51
|
+
toolName: operation.observerTool,
|
|
52
|
+
connectionRef,
|
|
53
|
+
destinationUrl,
|
|
54
|
+
forceRefresh: false,
|
|
55
|
+
...(input.credentialSubjectId ? { subjectId: input.credentialSubjectId } : {}),
|
|
56
|
+
});
|
|
57
|
+
// This is credential-resolution authorization. The actual broker keeps
|
|
58
|
+
// ownership of the physical provider-request authorization/audit fact.
|
|
59
|
+
return (
|
|
60
|
+
resolved.status === "ok" && resolved.operationAuthorityDigest === operation.authorityDigest
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
if (!(await authorize())) return { status: "auth_needed" };
|
|
64
|
+
return {
|
|
65
|
+
status: "ready",
|
|
66
|
+
authorize,
|
|
67
|
+
callObserver: async (tool, args) => {
|
|
68
|
+
if (tool !== operation.observerTool) throw new Error("MCP observer binding changed");
|
|
69
|
+
return await runMcpOperationObservationWithAuthority(
|
|
70
|
+
{
|
|
71
|
+
serverId: operation.serverId,
|
|
72
|
+
observerTool: operation.observerTool,
|
|
73
|
+
destinationDigest: operation.destinationDigest,
|
|
74
|
+
authorityDigest: operation.authorityDigest,
|
|
75
|
+
},
|
|
76
|
+
async () =>
|
|
77
|
+
await environment.callModel({
|
|
78
|
+
modelName: observerEntry.modelName,
|
|
79
|
+
arguments: args,
|
|
80
|
+
subjectId: "worker:first-party-mcp",
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { AttemptToolResult } from "@opengeni/contracts";
|
|
2
|
+
import { observeMcpOperation, type McpObservationReceipt } from "@opengeni/runtime";
|
|
3
|
+
import type { McpOperationReadSelector } from "./agent-turn/mcp-operation-read-tool";
|
|
4
|
+
|
|
5
|
+
type TerminalReceipt = Extract<McpObservationReceipt, { status: "completed" }>;
|
|
6
|
+
|
|
7
|
+
/** Internal, credential-free projection loaded through canonical current
|
|
8
|
+
* attempt/session authorization. Never return this object directly to tools. */
|
|
9
|
+
export type McpOperationReadRecord = {
|
|
10
|
+
operationId: string;
|
|
11
|
+
sourceTurnId: string;
|
|
12
|
+
sourceCallId: string | null;
|
|
13
|
+
serverId: string;
|
|
14
|
+
originalTool: string;
|
|
15
|
+
observerTool: string;
|
|
16
|
+
argumentDigest: string;
|
|
17
|
+
destinationDigest: string;
|
|
18
|
+
authorityDigest: string;
|
|
19
|
+
originalOutcome: "captured" | "completed" | "outcome_unknown";
|
|
20
|
+
originalResult: AttemptToolResult | null;
|
|
21
|
+
receipt: TerminalReceipt | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type ObserverResolution =
|
|
25
|
+
| { status: "unsupported" | "auth_needed" | "binding_changed" }
|
|
26
|
+
| {
|
|
27
|
+
status: "ready";
|
|
28
|
+
authorize: (phase: "before_request" | "before_delivery") => Promise<boolean>;
|
|
29
|
+
callObserver: (tool: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type McpOperationReaderDependencies = {
|
|
33
|
+
/** Includes live attempt and original-principal/session disclosure checks. */
|
|
34
|
+
load: (selector: McpOperationReadSelector) => Promise<McpOperationReadRecord | null>;
|
|
35
|
+
claim: (operationId: string) => Promise<string | null>;
|
|
36
|
+
release: (operationId: string, claimId: string) => Promise<void>;
|
|
37
|
+
/** Claim-fenced transaction storing one immutable receipt. Must recheck
|
|
38
|
+
* canonical current authority and never rewrite the original outcome.
|
|
39
|
+
* Delivery uses the ordinary operation_read tool-result lifecycle. */
|
|
40
|
+
settle: (
|
|
41
|
+
operationId: string,
|
|
42
|
+
claimId: string,
|
|
43
|
+
receipt: TerminalReceipt,
|
|
44
|
+
) => Promise<McpOperationReadRecord>;
|
|
45
|
+
/** Binds the current trusted config/connection to the persisted original
|
|
46
|
+
* digests. Model arguments may not supply or replace any of that authority. */
|
|
47
|
+
resolveObserver: (operation: McpOperationReadRecord) => Promise<ObserverResolution>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** One explicit read; no timer, inference wake, or mutation replay. */
|
|
51
|
+
export async function readMcpOperation(
|
|
52
|
+
selector: McpOperationReadSelector,
|
|
53
|
+
dependencies: McpOperationReaderDependencies,
|
|
54
|
+
): Promise<Record<string, unknown>> {
|
|
55
|
+
const operation = await dependencies.load(selector);
|
|
56
|
+
if (!operation) throw new Error("MCP operation is unavailable");
|
|
57
|
+
if (operation.receipt || operation.originalOutcome === "completed") return project(operation);
|
|
58
|
+
|
|
59
|
+
const observer = await dependencies.resolveObserver(operation);
|
|
60
|
+
if (observer.status !== "ready") return project(operation, { status: observer.status });
|
|
61
|
+
const claimId = await dependencies.claim(operation.operationId);
|
|
62
|
+
if (!claimId) {
|
|
63
|
+
// A competing reader may have committed after our initial load. Re-read
|
|
64
|
+
// under current authority instead of returning an old cached projection.
|
|
65
|
+
const fresh = await dependencies.load({ operationId: operation.operationId });
|
|
66
|
+
if (!fresh) throw new Error("MCP operation is unavailable");
|
|
67
|
+
return project(
|
|
68
|
+
fresh,
|
|
69
|
+
fresh.receipt || fresh.originalOutcome === "completed"
|
|
70
|
+
? undefined
|
|
71
|
+
: { status: "observation_in_progress" },
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
let settled = false;
|
|
75
|
+
try {
|
|
76
|
+
const receipt = await observeMcpOperation({
|
|
77
|
+
binding: {
|
|
78
|
+
operationId: operation.operationId,
|
|
79
|
+
serverId: operation.serverId,
|
|
80
|
+
originalTool: operation.originalTool,
|
|
81
|
+
observerTool: operation.observerTool,
|
|
82
|
+
argumentDigest: operation.argumentDigest,
|
|
83
|
+
},
|
|
84
|
+
authorize: observer.authorize,
|
|
85
|
+
callObserver: observer.callObserver,
|
|
86
|
+
});
|
|
87
|
+
if (receipt.status !== "completed") return project(operation, receipt);
|
|
88
|
+
const recorded = await dependencies.settle(operation.operationId, claimId, receipt);
|
|
89
|
+
settled = true;
|
|
90
|
+
return project(recorded);
|
|
91
|
+
} finally {
|
|
92
|
+
if (!settled) await dependencies.release(operation.operationId, claimId);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function project(
|
|
97
|
+
operation: McpOperationReadRecord,
|
|
98
|
+
observation?: Record<string, unknown>,
|
|
99
|
+
): Record<string, unknown> {
|
|
100
|
+
return {
|
|
101
|
+
operationId: operation.operationId,
|
|
102
|
+
original: { turnId: operation.sourceTurnId, sourceCallId: operation.sourceCallId },
|
|
103
|
+
invocationOutcome:
|
|
104
|
+
operation.originalOutcome === "captured" ? "outcome_unknown" : operation.originalOutcome,
|
|
105
|
+
...(operation.originalOutcome === "completed" ? { result: operation.originalResult } : {}),
|
|
106
|
+
observation: observation ?? operation.receipt ?? { status: "unknown" },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { AttemptToolResult } from "@opengeni/contracts";
|
|
2
|
+
import type { Database } from "@opengeni/db";
|
|
3
|
+
import {
|
|
4
|
+
readMcpOperation as readStoredMcpOperation,
|
|
5
|
+
claimMcpOperationObservation,
|
|
6
|
+
releaseMcpOperationObservation,
|
|
7
|
+
settleMcpOperationObservation,
|
|
8
|
+
type McpOperationAttempt,
|
|
9
|
+
type McpOperationRecord,
|
|
10
|
+
} from "@opengeni/db/mcp-operations";
|
|
11
|
+
import type {
|
|
12
|
+
McpOperationReaderDependencies,
|
|
13
|
+
McpOperationReadRecord,
|
|
14
|
+
} from "./mcp-operation-reader";
|
|
15
|
+
|
|
16
|
+
/** All DB calls remain bound to the actual accepted worker attempt. Locators
|
|
17
|
+
* supplied to operation_read cannot replace this scope or its principal. */
|
|
18
|
+
export function createMcpOperationReadStore(
|
|
19
|
+
db: Database,
|
|
20
|
+
attempt: McpOperationAttempt,
|
|
21
|
+
): Pick<McpOperationReaderDependencies, "load" | "claim" | "release" | "settle"> {
|
|
22
|
+
const current = { ...attempt };
|
|
23
|
+
const load: McpOperationReaderDependencies["load"] = async (selector) => {
|
|
24
|
+
const found = await readStoredMcpOperation(db, current, selector);
|
|
25
|
+
if (found.status === "ambiguous") {
|
|
26
|
+
throw new Error("MCP source call matches multiple operations; select an exact operationId");
|
|
27
|
+
}
|
|
28
|
+
return found.status === "found" ? projectRecord(found.operation) : null;
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
load,
|
|
32
|
+
claim: async (operationId) => {
|
|
33
|
+
const result = await claimMcpOperationObservation(db, current, operationId);
|
|
34
|
+
return result.status === "claimed" ? result.claimId : null;
|
|
35
|
+
},
|
|
36
|
+
release: async (operationId, claimId) => {
|
|
37
|
+
await releaseMcpOperationObservation(db, current, { operationId, claimId });
|
|
38
|
+
},
|
|
39
|
+
settle: async (operationId, claimId, receipt) => {
|
|
40
|
+
const outcome = await settleMcpOperationObservation(db, current, {
|
|
41
|
+
operationId,
|
|
42
|
+
claimId,
|
|
43
|
+
receiptRevision: receipt.receiptRevision,
|
|
44
|
+
result: receipt.result,
|
|
45
|
+
});
|
|
46
|
+
if (outcome.status === "conflict") {
|
|
47
|
+
throw new Error(
|
|
48
|
+
"MCP provider returned conflicting terminal receipts; the first receipt is preserved",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (
|
|
52
|
+
outcome.status !== "settled" &&
|
|
53
|
+
outcome.status !== "existing" &&
|
|
54
|
+
outcome.status !== "original_completed"
|
|
55
|
+
) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"MCP observation claim is no longer current; read the existing operation again",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const recorded = await load({ operationId });
|
|
61
|
+
if (!recorded) throw new Error("MCP operation is unavailable");
|
|
62
|
+
return recorded;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function projectRecord(record: McpOperationRecord): McpOperationReadRecord {
|
|
68
|
+
if (record.observationResult !== null && record.receiptRevision === null) {
|
|
69
|
+
throw new Error("Stored MCP operation receipt is incomplete");
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
operationId: record.operationId,
|
|
73
|
+
sourceTurnId: record.sourceTurnId,
|
|
74
|
+
sourceCallId: record.sourceCallId,
|
|
75
|
+
serverId: record.serverId,
|
|
76
|
+
originalTool: record.originalTool,
|
|
77
|
+
observerTool: record.observerTool,
|
|
78
|
+
argumentDigest: record.argumentDigest,
|
|
79
|
+
destinationDigest: record.destinationDigest,
|
|
80
|
+
authorityDigest: record.authorityDigest,
|
|
81
|
+
// Capture precedes the physical request; after a crash it is not proof
|
|
82
|
+
// that the mutation was or was not sent. It never authorizes a replay.
|
|
83
|
+
originalOutcome: record.originalOutcome,
|
|
84
|
+
originalResult:
|
|
85
|
+
record.originalResult === null ? null : AttemptToolResult.parse(record.originalResult),
|
|
86
|
+
receipt:
|
|
87
|
+
record.observationResult === null
|
|
88
|
+
? null
|
|
89
|
+
: {
|
|
90
|
+
version: 1,
|
|
91
|
+
operationRef: record.operationId,
|
|
92
|
+
fingerprint: { version: 1, algorithm: "sha256", value: record.argumentDigest },
|
|
93
|
+
status: "completed",
|
|
94
|
+
receiptRevision: record.receiptRevision!,
|
|
95
|
+
result: AttemptToolResult.parse(record.observationResult),
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|