@opengeni/worker-bundle 0.28.3 → 0.28.4-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/activities/agent-turn/mcp-operation-read-tool.d.ts +16 -0
- 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 +4 -4
- package/dist/{activities-control-UXGPMDL3.js → activities-control-J5QNRWVH.js} +56 -70
- package/dist/activities-control-J5QNRWVH.js.map +1 -0
- package/dist/{activities-turn-NOYDE7PX.js → activities-turn-ZJBY3W5D.js} +15692 -799
- package/dist/activities-turn-ZJBY3W5D.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-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/{chunk-NNXLW6V7.js → chunk-ZNIKZQMN.js} +144 -111
- package/dist/chunk-ZNIKZQMN.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/package.json +18 -17
- package/src/activities/agent-turn/claim.ts +3 -3
- 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/stream-attempt.ts +1 -1
- package/src/activities/agent-turn/tool-environment.ts +65 -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 +64 -71
- package/src/observability-metrics.ts +28 -24
- package/src/sandbox-archive-storage.ts +82 -18
- package/src/sandbox-resume.ts +79 -63
- 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
|
@@ -91,15 +91,23 @@ export declare function recordWorkerDeathRecoveryMetrics(observability: Observab
|
|
|
91
91
|
export declare class TurnLifecycleMetrics {
|
|
92
92
|
private readonly observability;
|
|
93
93
|
private readonly options;
|
|
94
|
-
private readonly
|
|
94
|
+
private readonly attempts;
|
|
95
95
|
private timer;
|
|
96
96
|
constructor(observability: Observability, options?: {
|
|
97
97
|
now?: () => number;
|
|
98
98
|
refreshIntervalMs?: number;
|
|
99
99
|
});
|
|
100
|
-
start(
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
start(input: {
|
|
101
|
+
attemptId: string;
|
|
102
|
+
}): void;
|
|
103
|
+
progress(input: {
|
|
104
|
+
attemptId: string;
|
|
105
|
+
}): void;
|
|
106
|
+
finish(input: {
|
|
107
|
+
attemptId: string;
|
|
108
|
+
outcome: TurnOutcome | null;
|
|
109
|
+
durationSeconds?: number;
|
|
110
|
+
}): void;
|
|
103
111
|
refreshGauges(): void;
|
|
104
112
|
stop(): void;
|
|
105
113
|
private oldestInflightAgeSeconds;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ObjectStorage } from "@opengeni/storage";
|
|
2
|
+
import type { VerifiedHostWorkspaceArchive } from "@opengeni/runtime/sandbox";
|
|
2
3
|
import { type WorkspaceArchiveDescriptor, type WorkspaceArchiveObjectRef } from "@opengeni/contracts";
|
|
3
4
|
export declare class WorkspaceArchiveObjectStorageRequiredError extends Error {
|
|
4
5
|
readonly code: "workspace_archive_object_storage_required";
|
|
@@ -13,15 +14,26 @@ export declare function putTarWorkspaceArchiveObject(input: {
|
|
|
13
14
|
archive: {
|
|
14
15
|
bytes: Uint8Array;
|
|
15
16
|
descriptor: WorkspaceArchiveDescriptor;
|
|
16
|
-
};
|
|
17
|
+
} | VerifiedHostWorkspaceArchive;
|
|
17
18
|
}): Promise<WorkspaceArchiveObjectRef>;
|
|
19
|
+
/** The caller owns a fresh candidate returned by this module. A thrown or lost
|
|
20
|
+
* database response is UNKNOWN: it may have committed, so never delete on catch.
|
|
21
|
+
* Only the transaction's exact candidate disposition can authorize cleanup. */
|
|
22
|
+
export declare function persistWorkspaceArchiveCandidate<T extends {
|
|
23
|
+
wrote: boolean;
|
|
24
|
+
candidateDisposition?: "adopted" | "already_referenced" | "unused";
|
|
25
|
+
}>(input: {
|
|
26
|
+
objectStorage?: ObjectStorage | null;
|
|
27
|
+
ref?: WorkspaceArchiveObjectRef;
|
|
28
|
+
persist: () => Promise<T>;
|
|
29
|
+
}): Promise<T>;
|
|
18
30
|
export declare function putVersion1TarArchiveOrInline(input: {
|
|
19
31
|
backend: string;
|
|
20
32
|
objectStorage?: ObjectStorage | null | undefined;
|
|
21
33
|
accountId: string;
|
|
22
34
|
workspaceId: string;
|
|
23
35
|
sandboxGroupId: string;
|
|
24
|
-
archive: {
|
|
36
|
+
archive: VerifiedHostWorkspaceArchive | {
|
|
25
37
|
bytes: Uint8Array;
|
|
26
38
|
descriptor: WorkspaceArchiveDescriptor;
|
|
27
39
|
base64: string;
|
package/dist/sandbox-resume.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Settings } from "@opengeni/config";
|
|
2
2
|
import { readLease, reinstateTurnLeaseHolder, SandboxLeaseSupersededError, type Database, type LeaseHolderKind } from "@opengeni/db";
|
|
3
3
|
import { establishSandboxSessionFromEnvelope, type EstablishedSandboxSession, type RuntimeMetricsHooks } from "@opengeni/runtime";
|
|
4
|
-
import type
|
|
4
|
+
import { type ObjectStorage } from "@opengeni/storage";
|
|
5
5
|
import type { Observability } from "@opengeni/observability";
|
|
6
6
|
export type ResumeHolderKind = LeaseHolderKind;
|
|
7
7
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/worker-bundle",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.4-canary.0",
|
|
4
4
|
"description": "OpenGeni worker entry and reusable embedded lifecycle, shipped with a release-coherent pre-bundled Temporal workflow artifact.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -54,22 +54,23 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@llamaindex/liteparse": "2.14.2",
|
|
57
|
-
"@opengeni/agent-proto": "^0.5.1",
|
|
58
|
-
"@opengeni/capabilities": "^0.3.3",
|
|
59
|
-
"@opengeni/codemode": "^0.5.
|
|
60
|
-
"@opengeni/codex": "^0.2.22",
|
|
61
|
-
"@opengeni/config": "^1.1.
|
|
62
|
-
"@opengeni/contracts": "^3.0.
|
|
63
|
-
"@opengeni/core": "^2.9.
|
|
64
|
-
"@opengeni/db": "^4.3.
|
|
65
|
-
"@opengeni/documents": "^0.8.
|
|
66
|
-
"@opengeni/events": "^0.4.
|
|
67
|
-
"@opengeni/github": "^0.7.
|
|
68
|
-
"@opengeni/network": "^0.3.1",
|
|
69
|
-
"@opengeni/observability": "^0.8.
|
|
70
|
-
"@opengeni/runtime": "^2.5.
|
|
71
|
-
"@opengeni/storage": "^0.2.
|
|
72
|
-
"@opengeni/
|
|
57
|
+
"@opengeni/agent-proto": "^0.5.1-canary.26",
|
|
58
|
+
"@opengeni/capabilities": "^0.3.3-canary.8",
|
|
59
|
+
"@opengeni/codemode": "^0.5.6-canary.0",
|
|
60
|
+
"@opengeni/codex": "^0.2.22-canary.8",
|
|
61
|
+
"@opengeni/config": "^1.1.2-canary.0",
|
|
62
|
+
"@opengeni/contracts": "^3.0.2-canary.0",
|
|
63
|
+
"@opengeni/core": "^2.9.4-canary.0",
|
|
64
|
+
"@opengeni/db": "^4.3.3-canary.0",
|
|
65
|
+
"@opengeni/documents": "^0.8.27-canary.0",
|
|
66
|
+
"@opengeni/events": "^0.4.25-canary.0",
|
|
67
|
+
"@opengeni/github": "^0.7.10-canary.0",
|
|
68
|
+
"@opengeni/network": "^0.3.1-canary.8",
|
|
69
|
+
"@opengeni/observability": "^0.8.26-canary.0",
|
|
70
|
+
"@opengeni/runtime": "^2.5.3-canary.0",
|
|
71
|
+
"@opengeni/storage": "^0.2.127-canary.0",
|
|
72
|
+
"@opengeni/tool-gateway": "^0.1.7-canary.0",
|
|
73
|
+
"@opengeni/xai-subscription": "^0.1.4-canary.8",
|
|
73
74
|
"@temporalio/activity": "^1.17.0",
|
|
74
75
|
"@temporalio/client": "^1.17.0",
|
|
75
76
|
"@temporalio/worker": "^1.17.0",
|
|
@@ -347,7 +347,7 @@ export async function claimTurnAttempt(deps: ClaimTurnDeps): Promise<ClaimTurnOu
|
|
|
347
347
|
});
|
|
348
348
|
return attached.action === "claimed" && attached.turn.id === turn.id;
|
|
349
349
|
};
|
|
350
|
-
turnLifecycleMetricsFor(observability).start(
|
|
350
|
+
turnLifecycleMetricsFor(observability).start({ attemptId: input.attemptId });
|
|
351
351
|
// §7.5 P3 — pass the accepted billing attribution (externally funded turns
|
|
352
352
|
// bypass OpenGeni credit/token gates)
|
|
353
353
|
// AND the optional host `entitlements` port (when bound, its admitRun replaces
|
|
@@ -451,7 +451,7 @@ export async function claimTurnAttempt(deps: ClaimTurnDeps): Promise<ClaimTurnOu
|
|
|
451
451
|
}
|
|
452
452
|
recordCanonicalStartupMilestones(appended.canonicalStartupMilestones);
|
|
453
453
|
if (inputs.length > 0) {
|
|
454
|
-
turnLifecycleMetricsFor(observability).progress(
|
|
454
|
+
turnLifecycleMetricsFor(observability).progress({ attemptId: input.attemptId });
|
|
455
455
|
}
|
|
456
456
|
activityContext?.heartbeat({
|
|
457
457
|
...heartbeatDetails,
|
|
@@ -515,7 +515,7 @@ export async function claimTurnAttempt(deps: ClaimTurnDeps): Promise<ClaimTurnOu
|
|
|
515
515
|
return false;
|
|
516
516
|
}
|
|
517
517
|
recordCanonicalStartupMilestones(result.canonicalStartupMilestones);
|
|
518
|
-
turnLifecycleMetricsFor(observability).progress(
|
|
518
|
+
turnLifecycleMetricsFor(observability).progress({ attemptId: input.attemptId });
|
|
519
519
|
await publishDurableSessionEvents(bus, input.workspaceId, input.sessionId, result.events);
|
|
520
520
|
if (inputSettlement.turnStatus === "requires_action") {
|
|
521
521
|
// The settlement transaction committed the parent's child_requires_action
|
|
@@ -745,12 +745,17 @@ export async function finalizeTurnAttempt(deps: TurnFinalizationDeps): Promise<v
|
|
|
745
745
|
status: finalizationError ? "cleanup_failed" : control.activityStatus,
|
|
746
746
|
durationSeconds,
|
|
747
747
|
});
|
|
748
|
-
if (attempt.turnId
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
748
|
+
if (attempt.turnId) {
|
|
749
|
+
// This registry describes physical activity ownership, not durable turn
|
|
750
|
+
// settlement. Always clear the exact attempt when its activity unwinds,
|
|
751
|
+
// even when failure settlement could not classify an outcome. Otherwise
|
|
752
|
+
// a recovered successor can make progress while the predecessor leaves a
|
|
753
|
+
// process-local false stuck-turn alert behind.
|
|
754
|
+
turnLifecycleMetricsFor(observability).finish({
|
|
755
|
+
attemptId: input.attemptId,
|
|
756
|
+
outcome: control.turnMetricOutcome,
|
|
752
757
|
durationSeconds,
|
|
753
|
-
);
|
|
758
|
+
});
|
|
754
759
|
}
|
|
755
760
|
activitySpan.end({
|
|
756
761
|
attributes: {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { AttemptToolDefinition } from "@opengeni/codemode";
|
|
2
|
+
import { AttemptToolResult } from "@opengeni/contracts";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
const selectorSchema = z.union([
|
|
6
|
+
z.object({ operationId: z.string().uuid() }).strict(),
|
|
7
|
+
z.object({ sourceTurnId: z.string().uuid(), sourceCallId: z.string().min(1) }).strict(),
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
export type McpOperationReadSelector = z.infer<typeof selectorSchema>;
|
|
11
|
+
|
|
12
|
+
/** Attempt-local adapter. The supplied read closure owns canonical session
|
|
13
|
+
* access, current provider authority, immutable binding checks and settlement.
|
|
14
|
+
* No caller-controlled provider parameters cross this tool boundary. */
|
|
15
|
+
export function createOperationReadAttemptToolDefinition(input: {
|
|
16
|
+
read: (selector: McpOperationReadSelector) => Promise<Record<string, unknown>>;
|
|
17
|
+
}): AttemptToolDefinition {
|
|
18
|
+
return {
|
|
19
|
+
identity: { serverId: "opengeni", toolName: "operation_read" },
|
|
20
|
+
modelName: "operation_read",
|
|
21
|
+
codemodePath: ["opengeni", "operation_read"],
|
|
22
|
+
title: "Read an existing MCP operation outcome",
|
|
23
|
+
description:
|
|
24
|
+
"Read a retained MCP operation and, when supported and currently authorized, observe its provider receipt. " +
|
|
25
|
+
"Use the operationId from the original call, or its exact sourceTurnId and sourceCallId if that response was lost. " +
|
|
26
|
+
"This never repeats the original mutation. Unknown means the outcome is not proven, not that the operation failed. " +
|
|
27
|
+
"Provider support and renewed authorization may be required after reconnecting.",
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
operationId: { type: "string", format: "uuid" },
|
|
32
|
+
sourceTurnId: { type: "string", format: "uuid" },
|
|
33
|
+
sourceCallId: { type: "string", minLength: 1 },
|
|
34
|
+
},
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
oneOf: [
|
|
37
|
+
{
|
|
38
|
+
required: ["operationId"],
|
|
39
|
+
not: { anyOf: [{ required: ["sourceTurnId"] }, { required: ["sourceCallId"] }] },
|
|
40
|
+
},
|
|
41
|
+
{ required: ["sourceTurnId", "sourceCallId"], not: { required: ["operationId"] } },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
annotations: {
|
|
45
|
+
readOnlyHint: true,
|
|
46
|
+
destructiveHint: false,
|
|
47
|
+
idempotentHint: true,
|
|
48
|
+
openWorldHint: true,
|
|
49
|
+
},
|
|
50
|
+
source: "opengeni",
|
|
51
|
+
approval: "none",
|
|
52
|
+
execute: async (args) => {
|
|
53
|
+
const selector = selectorSchema.parse(args);
|
|
54
|
+
const receipt = await input.read(selector);
|
|
55
|
+
return AttemptToolResult.parse({
|
|
56
|
+
content: [{ type: "text", text: JSON.stringify(receipt) }],
|
|
57
|
+
structuredContent: receipt,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -1062,7 +1062,7 @@ export async function runTurnStreamAttempt(
|
|
|
1062
1062
|
currentToolBatchCallIds = new Set<string>();
|
|
1063
1063
|
currentToolBatchCompletedCallIds = new Set<string>();
|
|
1064
1064
|
await historySink.reconcileConversationTruth();
|
|
1065
|
-
turnLifecycleMetricsFor(observability).progress(
|
|
1065
|
+
turnLifecycleMetricsFor(observability).progress({ attemptId: input.attemptId });
|
|
1066
1066
|
modelCheckpointMemoryCollector.schedule(observability);
|
|
1067
1067
|
try {
|
|
1068
1068
|
await ensureRunAllowed(
|
|
@@ -37,6 +37,11 @@ import {
|
|
|
37
37
|
resolveGoogleDrivePublicationTarget,
|
|
38
38
|
} from "../google-drive-publication";
|
|
39
39
|
import { connectionTokenResolverForTurn } from "../mcp-credentials";
|
|
40
|
+
import { createMcpOperationPersistence } from "@opengeni/db/mcp-operations";
|
|
41
|
+
import { createMcpOperationReadStore } from "../mcp-operation-store";
|
|
42
|
+
import { createMcpOperationObserverResolver } from "../mcp-operation-observer";
|
|
43
|
+
import { readMcpOperation } from "../mcp-operation-reader";
|
|
44
|
+
import { createOperationReadAttemptToolDefinition } from "./mcp-operation-read-tool";
|
|
40
45
|
import { buildGitHubRestMcpForTurn } from "../../github-rest-mcp";
|
|
41
46
|
import { materializeConnectorAttachmentsInChannel } from "../connector-attachments";
|
|
42
47
|
import { allowedFirstPartyMcpToolsForSession, type Settings } from "@opengeni/config";
|
|
@@ -668,7 +673,66 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
|
|
|
668
673
|
});
|
|
669
674
|
},
|
|
670
675
|
});
|
|
676
|
+
const operationRecoveryEnabled = githubRestMcp.settings.mcpServers.some(
|
|
677
|
+
(server) =>
|
|
678
|
+
server.operationRecovery &&
|
|
679
|
+
Object.keys(server.operationRecovery).length > 0 &&
|
|
680
|
+
githubRestMcp.tools.some((tool) => tool.kind === "mcp" && tool.id === server.id),
|
|
681
|
+
);
|
|
682
|
+
const operationAttempt = {
|
|
683
|
+
accountId: input.accountId,
|
|
684
|
+
workspaceId: input.workspaceId,
|
|
685
|
+
sessionId: input.sessionId,
|
|
686
|
+
turnId: turn.id,
|
|
687
|
+
attemptId: input.attemptId,
|
|
688
|
+
executionGeneration: attempt.executionGeneration,
|
|
689
|
+
};
|
|
690
|
+
const operationPersistence = operationRecoveryEnabled
|
|
691
|
+
? createMcpOperationPersistence(db, operationAttempt)
|
|
692
|
+
: undefined;
|
|
693
|
+
const operationReadStore = operationRecoveryEnabled
|
|
694
|
+
? createMcpOperationReadStore(db, operationAttempt)
|
|
695
|
+
: undefined;
|
|
696
|
+
const operationObserver = createMcpOperationObserverResolver({
|
|
697
|
+
settings: githubRestMcp.settings,
|
|
698
|
+
workspaceId: input.workspaceId,
|
|
699
|
+
...(credentialSubjectId ? { credentialSubjectId } : {}),
|
|
700
|
+
resolveCredential,
|
|
701
|
+
assertAttempt: async () => {
|
|
702
|
+
throwIfWorkerShuttingDown();
|
|
703
|
+
throwIfTurnCancelled();
|
|
704
|
+
const current = await getSessionTurnForAttempt(
|
|
705
|
+
db,
|
|
706
|
+
input.workspaceId,
|
|
707
|
+
input.sessionId,
|
|
708
|
+
input.attemptId,
|
|
709
|
+
);
|
|
710
|
+
if (
|
|
711
|
+
!current ||
|
|
712
|
+
current.id !== turn.id ||
|
|
713
|
+
current.executionGeneration !== attempt.executionGeneration
|
|
714
|
+
) {
|
|
715
|
+
throw new Error("MCP operation reader no longer owns the executing attempt");
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
getEnvironment: async () => {
|
|
719
|
+
const prepared = eventing.preparedTools;
|
|
720
|
+
if (!prepared) return null;
|
|
721
|
+
return ((await prepared.ready) ?? prepared).attemptToolEnvironment;
|
|
722
|
+
},
|
|
723
|
+
});
|
|
671
724
|
const attemptToolDefinitions = [
|
|
725
|
+
...(operationReadStore
|
|
726
|
+
? [
|
|
727
|
+
createOperationReadAttemptToolDefinition({
|
|
728
|
+
read: async (selector) =>
|
|
729
|
+
await readMcpOperation(selector, {
|
|
730
|
+
...operationReadStore,
|
|
731
|
+
resolveObserver: operationObserver,
|
|
732
|
+
}),
|
|
733
|
+
}),
|
|
734
|
+
]
|
|
735
|
+
: []),
|
|
672
736
|
...skillTools,
|
|
673
737
|
createListModelsAttemptToolDefinition({
|
|
674
738
|
currentModelId: turnExecutionPolicy.productModelId,
|
|
@@ -876,6 +940,7 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
|
|
|
876
940
|
...(credentialSubjectId ? { credentialSubjectId } : {}),
|
|
877
941
|
...(codexAppsAuth ? { codexAppsAuth } : {}),
|
|
878
942
|
resolveCredential,
|
|
943
|
+
...(operationPersistence ? { mcpOperationPersistence: operationPersistence } : {}),
|
|
879
944
|
...(linkedAuthority
|
|
880
945
|
? {
|
|
881
946
|
authorizeAttemptExecution: async () => {
|
|
@@ -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
|
+
}
|