@opengeni/worker-bundle 1.0.1 → 2.0.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 (122) hide show
  1. package/dist/activities/agent-turn/codex.d.ts +1 -1
  2. package/dist/activities/agent-turn/compaction-prep.d.ts +1 -5
  3. package/dist/activities/agent-turn/errors.d.ts +31 -5
  4. package/dist/activities/agent-turn/finalization-monitor.d.ts +17 -0
  5. package/dist/activities/agent-turn/finalization.d.ts +1 -1
  6. package/dist/activities/agent-turn/governance-model.d.ts +0 -2
  7. package/dist/activities/agent-turn/quiescence.d.ts +1 -0
  8. package/dist/activities/agent-turn/recovery-warning.d.ts +8 -0
  9. package/dist/activities/agent-turn/run-credentials.d.ts +0 -1
  10. package/dist/activities/agent-turn/sandbox-establish.d.ts +1 -1
  11. package/dist/activities/agent-turn/sandbox-route.d.ts +0 -20
  12. package/dist/activities/agent-turn/sandbox-runtime.d.ts +1 -1
  13. package/dist/activities/agent-turn/skill-remove.d.ts +14 -0
  14. package/dist/activities/agent-turn/skill-search.d.ts +1 -1
  15. package/dist/activities/agent-turn/stream-attempt.d.ts +1 -0
  16. package/dist/activities/agent-turn/tool-environment.d.ts +0 -4
  17. package/dist/activities/agent-turn/turn-context.d.ts +2 -0
  18. package/dist/activities/agent-turn/turn-sandbox-access.d.ts +3 -0
  19. package/dist/activities/codex-capacity.d.ts +6 -1
  20. package/dist/activities/codex-rotation.d.ts +1 -1
  21. package/dist/activities/context-compaction.d.ts +7 -1
  22. package/dist/activities/mcp-account-routes.d.ts +22 -0
  23. package/dist/activities/mcp-credentials.d.ts +12 -13
  24. package/dist/activities/mcp-operation-authority.d.ts +0 -5
  25. package/dist/activities/rig-verification.d.ts +2 -3
  26. package/dist/activities/sandbox-images.d.ts +1 -1
  27. package/dist/activities/sandbox-lease.d.ts +4 -3
  28. package/dist/activities/session-state.d.ts +57 -2
  29. package/dist/activities/types.d.ts +12 -3
  30. package/dist/{activities-control-HMFGSKC5.js → activities-control-BOLIY3H4.js} +366 -187
  31. package/dist/activities-control-BOLIY3H4.js.map +1 -0
  32. package/dist/activities-control.d.ts +110 -2
  33. package/dist/{activities-turn-AP67JADS.js → activities-turn-2B3WMHBB.js} +767 -471
  34. package/dist/activities-turn-2B3WMHBB.js.map +1 -0
  35. package/dist/activities.d.ts +110 -2
  36. package/dist/artifact-materializer-entry.js +5 -2
  37. package/dist/artifact-materializer-entry.js.map +1 -1
  38. package/dist/artifact-outbox-entry.js +2 -2
  39. package/dist/artifact-outbox-entry.js.map +1 -1
  40. package/dist/{chunk-A4XQWVZK.js → chunk-I7HKKMUJ.js} +77 -1
  41. package/dist/chunk-I7HKKMUJ.js.map +1 -0
  42. package/dist/{chunk-CYITU5UH.js → chunk-TVEE2UUC.js} +275 -146
  43. package/dist/chunk-TVEE2UUC.js.map +1 -0
  44. package/dist/editable-artifact-materializer-service.d.ts +2 -2
  45. package/dist/editable-artifact-outbox-service.d.ts +2 -2
  46. package/dist/index.d.ts +55 -1
  47. package/dist/index.js +4 -3
  48. package/dist/index.js.map +1 -1
  49. package/dist/observability-metrics.d.ts +11 -3
  50. package/dist/retained-process-diagnostics.d.ts +4 -0
  51. package/dist/sandbox-resume.d.ts +2 -12
  52. package/dist/sandbox-routing.d.ts +5 -3
  53. package/dist/sandbox-snapshot-diagnostics.d.ts +20 -0
  54. package/dist/warm-capture-holder.d.ts +3 -0
  55. package/dist/workflow-bundle.js +51 -11
  56. package/package.json +15 -15
  57. package/src/activities/agent-turn/agent-build.ts +42 -5
  58. package/src/activities/agent-turn/claim.ts +12 -0
  59. package/src/activities/agent-turn/codex-capacity.ts +26 -4
  60. package/src/activities/agent-turn/codex.ts +6 -2
  61. package/src/activities/agent-turn/compaction-prep.ts +53 -72
  62. package/src/activities/agent-turn/errors.ts +98 -10
  63. package/src/activities/agent-turn/failure-settlement.ts +49 -28
  64. package/src/activities/agent-turn/finalization-monitor.ts +121 -0
  65. package/src/activities/agent-turn/finalization.ts +79 -21
  66. package/src/activities/agent-turn/governance-model.ts +15 -19
  67. package/src/activities/agent-turn/history.ts +24 -10
  68. package/src/activities/agent-turn/quiescence.ts +3 -0
  69. package/src/activities/agent-turn/recovery-warning.ts +18 -0
  70. package/src/activities/agent-turn/run-credentials.ts +3 -7
  71. package/src/activities/agent-turn/run.ts +68 -52
  72. package/src/activities/agent-turn/sandbox-establish.ts +10 -2
  73. package/src/activities/agent-turn/sandbox-route.ts +0 -20
  74. package/src/activities/agent-turn/sandbox-runtime.ts +40 -7
  75. package/src/activities/agent-turn/skill-remove.ts +46 -0
  76. package/src/activities/agent-turn/skill-save.ts +1 -1
  77. package/src/activities/agent-turn/skill-search.ts +1 -1
  78. package/src/activities/agent-turn/skill-selection.ts +2 -0
  79. package/src/activities/agent-turn/skill-tools.ts +10 -5
  80. package/src/activities/agent-turn/stream-attempt.ts +38 -15
  81. package/src/activities/agent-turn/tool-environment.ts +44 -29
  82. package/src/activities/agent-turn/turn-context.ts +3 -0
  83. package/src/activities/agent-turn/turn-sandbox-access.ts +7 -1
  84. package/src/activities/capabilities.ts +17 -2
  85. package/src/activities/codex-capacity.ts +38 -9
  86. package/src/activities/codex-rotation.ts +18 -5
  87. package/src/activities/context-compaction.ts +26 -4
  88. package/src/activities/goals.ts +1 -0
  89. package/src/activities/google-drive-publication.ts +1 -3
  90. package/src/activities/mcp-account-routes.ts +139 -0
  91. package/src/activities/mcp-credentials.ts +83 -194
  92. package/src/activities/mcp-operation-authority.ts +16 -41
  93. package/src/activities/parent-wake.ts +8 -0
  94. package/src/activities/rig-verification.ts +38 -37
  95. package/src/activities/run-input.ts +25 -16
  96. package/src/activities/sandbox-images.ts +0 -1
  97. package/src/activities/sandbox-lease.ts +177 -44
  98. package/src/activities/scheduled-tasks.ts +118 -93
  99. package/src/activities/session-state.ts +83 -3
  100. package/src/activities/types.ts +18 -2
  101. package/src/activities/workflow-wake.ts +2 -17
  102. package/src/editable-artifact-materializer-service.ts +7 -3
  103. package/src/editable-artifact-outbox-service.ts +4 -4
  104. package/src/github-rest-mcp.ts +0 -1
  105. package/src/index.ts +2 -1
  106. package/src/observability-metrics.ts +105 -2
  107. package/src/personal-github-git-credentials.ts +0 -1
  108. package/src/pr-review-credentials.ts +0 -2
  109. package/src/retained-process-diagnostics.ts +50 -0
  110. package/src/sandbox-resume.ts +58 -73
  111. package/src/sandbox-routing.ts +44 -19
  112. package/src/sandbox-snapshot-diagnostics.ts +112 -0
  113. package/src/warm-capture-holder.ts +15 -0
  114. package/src/workflows/session.ts +64 -2
  115. package/dist/activities/agent-turn/knowledge-sources.d.ts +0 -20
  116. package/dist/activities/packs.d.ts +0 -55
  117. package/dist/activities-control-HMFGSKC5.js.map +0 -1
  118. package/dist/activities-turn-AP67JADS.js.map +0 -1
  119. package/dist/chunk-A4XQWVZK.js.map +0 -1
  120. package/dist/chunk-CYITU5UH.js.map +0 -1
  121. package/src/activities/agent-turn/knowledge-sources.ts +0 -86
  122. package/src/activities/packs.ts +0 -166
@@ -1,3 +1,4 @@
1
+ import { readReasoningConfiguration } from "@opengeni/codex";
1
2
  import {
2
3
  applyContextCompaction,
3
4
  getActiveSessionHistoryItemsPaged,
@@ -21,6 +22,7 @@ import {
21
22
  sanitizeHistoryItemsForModel,
22
23
  summarizeForCompaction,
23
24
  type CompactionItem,
25
+ type CompactionProviderRejection,
24
26
  } from "@opengeni/runtime";
25
27
  import { contextInputBudgetTokens, type Settings } from "@opengeni/config";
26
28
  import type { SessionEvent } from "@opengeni/contracts";
@@ -260,6 +262,12 @@ export async function settleFailedContextCompactionLandmark(
260
262
  options: {
261
263
  clearRequestedCompaction?: boolean;
262
264
  publishLiveEvents?: (events: SessionEvent[]) => Promise<void>;
265
+ /**
266
+ * Closed identifier record of a definitive provider rejection. Carried on
267
+ * the visible `compaction.skipped` landmark so the timeline can name the
268
+ * rejected field instead of offering a retry that cannot succeed.
269
+ */
270
+ providerRejection?: CompactionProviderRejection | null;
263
271
  } = {},
264
272
  ): Promise<Extract<MaybeCompactResult, { compacted: false }>> {
265
273
  const settled = await settleSkippedAfterStart(db, scope, options, "summarization_failed");
@@ -287,6 +295,7 @@ async function settleSkippedAfterStart(
287
295
  },
288
296
  options: {
289
297
  clearRequestedCompaction?: boolean;
298
+ providerRejection?: CompactionProviderRejection | null;
290
299
  },
291
300
  reason:
292
301
  | "no_history"
@@ -304,6 +313,7 @@ async function settleSkippedAfterStart(
304
313
  // an operator `/compact` flag — auto/overflow never set one.
305
314
  requirePendingRequest: false,
306
315
  clearRequestedCompaction,
316
+ ...(options.providerRejection ? { providerRejection: options.providerRejection } : {}),
307
317
  });
308
318
  if (!skipped.recorded) {
309
319
  throw new TurnAttemptFencedError(
@@ -370,7 +380,10 @@ async function compactContextRemoteV2(
370
380
  );
371
381
  const estimatedTokensAfter = estimateTokens(await projectForWire(replacementHistory));
372
382
  const replacementFingerprint = compactionReplacementFingerprint(replacementHistory);
373
- const tailItem = replacementHistory.at(-1);
383
+ const summaryIndex =
384
+ replacementHistory.length -
385
+ (readReasoningConfiguration(replacementHistory.at(-1) ?? {}) ? 2 : 1);
386
+ const tailItem = replacementHistory[summaryIndex];
374
387
  if (!tailItem) {
375
388
  throw new EmptyCompactionSummaryError({
376
389
  stage: "remote_v2_replacement",
@@ -384,7 +397,10 @@ async function compactContextRemoteV2(
384
397
  turnId: scope.turnId,
385
398
  expectedExecutionGeneration: scope.executionGeneration,
386
399
  expectedAttemptId: scope.attemptId,
387
- replacementItems: replacementHistory.slice(0, -1),
400
+ replacementItems: replacementHistory.slice(0, summaryIndex),
401
+ ...(replacementHistory.length > summaryIndex + 1
402
+ ? { trailingItems: replacementHistory.slice(summaryIndex + 1) }
403
+ : {}),
388
404
  summaryItem: tailItem as Record<string, unknown>,
389
405
  ...(options.clearRequestedCompaction ? { clearRequestedCompaction: true } : {}),
390
406
  eventPayload: {
@@ -447,7 +463,10 @@ async function compactContextPortable(
447
463
  const estimatedTokensAfter = estimateTokens(await projectForWire(replacementHistory));
448
464
  const replacementFingerprint = compactionReplacementFingerprint(replacementHistory);
449
465
  const previousReplacementFingerprint = latestCompactionReplacementFingerprint(canonicalItems);
450
- const summaryItem = replacementHistory.at(-1);
466
+ const summaryIndex =
467
+ replacementHistory.length -
468
+ (readReasoningConfiguration(replacementHistory.at(-1) ?? {}) ? 2 : 1);
469
+ const summaryItem = replacementHistory[summaryIndex];
451
470
  if (!summaryItem) {
452
471
  // Started already fanout; settle visibly so the landmark cannot stick on
453
472
  // "Compacting…". This is not an operator-request clear path.
@@ -466,7 +485,10 @@ async function compactContextPortable(
466
485
  turnId: scope.turnId,
467
486
  expectedExecutionGeneration: scope.executionGeneration,
468
487
  expectedAttemptId: scope.attemptId,
469
- replacementItems: replacementHistory.slice(0, -1),
488
+ replacementItems: replacementHistory.slice(0, summaryIndex),
489
+ ...(replacementHistory.length > summaryIndex + 1
490
+ ? { trailingItems: replacementHistory.slice(summaryIndex + 1) }
491
+ : {}),
470
492
  summaryItem: summaryItem as Record<string, unknown>,
471
493
  ...(options.clearRequestedCompaction ? { clearRequestedCompaction: true } : {}),
472
494
  eventPayload: {
@@ -308,6 +308,7 @@ export function goalContinuationPrompt(
308
308
  "- For every explicit requirement, named artifact, command, test, gate, invariant, and deliverable, identify and inspect the authoritative evidence that would prove it.",
309
309
  "- Match verification scope to requirement scope. Treat uncertain, indirect, incomplete, or missing evidence as not achieved and continue working.",
310
310
  "- The audit must prove completion, not merely fail to find obvious remaining work.",
311
+ "- For user-facing report deliverables, including reports produced during another task, follow the Documents Skill: create the durable native document first, inspect its relevant final head after the last edit, and provide the returned artifact reference. Declare report requirements through the available goal tools before authoring and satisfy every persisted report requirement with verified artifact delivery evidence before completion. Sandbox paths and raw file IDs do not prove report delivery. If artifact tooling or access is unavailable, keep that deliverable incomplete and state the blocker; never invent proof or silently substitute a local report. Ordinary chat answers, short progress updates, source-code links, and explicitly requested local-file work remain outside this report contract.",
311
312
  "",
312
313
  "Do not rely on intent, partial progress, memory of earlier work, or a plausible final answer as proof of completion. Call opengeni__goal_complete with concrete evidence only when the full objective is actually achieved and no required work remains.",
313
314
  "",
@@ -90,9 +90,7 @@ export async function resolveGoogleDrivePublicationTarget(
90
90
  );
91
91
  if (frozen.length !== 1) return null;
92
92
  const delegation = frozen[0]!;
93
- const activated = delegation.userDelegation !== undefined;
94
93
  if (
95
- !activated &&
96
94
  !(await ports.getMembership(db, {
97
95
  accountId: workspace.accountId,
98
96
  workspaceId,
@@ -101,7 +99,7 @@ export async function resolveGoogleDrivePublicationTarget(
101
99
  ) {
102
100
  return null;
103
101
  }
104
- const originWorkspaceId = activated ? delegation.originWorkspaceId : workspaceId;
102
+ const originWorkspaceId = delegation.originWorkspaceId;
105
103
  if (!originWorkspaceId) return null;
106
104
  const connection = await ports.getConnection(
107
105
  db,
@@ -0,0 +1,139 @@
1
+ import type { Settings } from "@opengeni/config";
2
+ import type {
3
+ McpConnectionAccountBinding,
4
+ ToolRef,
5
+ ToolAuthNeededPayload,
6
+ } from "@opengeni/contracts";
7
+ import type { ApiIntegrationRuntime } from "@opengeni/db";
8
+
9
+ export function accountRouteAuthNeededPayload(
10
+ payload: ToolAuthNeededPayload,
11
+ bindings: readonly McpConnectionAccountBinding[] | null | undefined,
12
+ ): ToolAuthNeededPayload {
13
+ const binding = bindings?.find((candidate) => candidate.serverId === payload.serverId);
14
+ return binding
15
+ ? {
16
+ ...payload,
17
+ canonicalServerId: binding.canonicalServerId,
18
+ connectionSubjectScope: binding.subjectScope,
19
+ }
20
+ : payload;
21
+ }
22
+
23
+ /** Expand only after canonical tool-policy admission. Aliases are execution
24
+ * identities, never an alternate way to gain admission to a connector. */
25
+ export function expandMcpAccountRoutes(input: {
26
+ settings: Settings;
27
+ tools: ToolRef[];
28
+ bindings: readonly McpConnectionAccountBinding[] | null | undefined;
29
+ }): { settings: Settings; tools: ToolRef[]; accountLabels: ReadonlyMap<string, string> } {
30
+ // Only historical work has no binding snapshot. A present empty snapshot is
31
+ // an explicit lack of account authority, never a request for current defaults.
32
+ if (input.bindings == null) {
33
+ return { settings: input.settings, tools: input.tools, accountLabels: new Map() };
34
+ }
35
+ const configured = new Map(input.settings.mcpServers.map((server) => [server.id, server]));
36
+ const byCanonical = new Map<string, McpConnectionAccountBinding[]>();
37
+ const routes = new Set<string>();
38
+ const accounts = new Set<string>();
39
+ for (const binding of input.bindings) {
40
+ const account = JSON.stringify([binding.canonicalServerId, binding.connectionId]);
41
+ if (
42
+ routes.has(binding.serverId) ||
43
+ accounts.has(account) ||
44
+ configured.has(binding.serverId) ||
45
+ (binding.subjectScope === "subject") !== (binding.ownerSubjectId !== null) ||
46
+ binding.connectionRef.authoritySource === "host" ||
47
+ binding.connectionRef.connectionId !== binding.connectionId ||
48
+ binding.connectionRef.subjectScope !== binding.subjectScope ||
49
+ binding.connectionRef.providerDomain !== binding.providerDomain ||
50
+ binding.connectionRef.kind !== binding.kind
51
+ ) {
52
+ throw new Error("Invalid accepted MCP account route");
53
+ }
54
+ routes.add(binding.serverId);
55
+ accounts.add(account);
56
+ const siblings = byCanonical.get(binding.canonicalServerId) ?? [];
57
+ siblings.push(binding);
58
+ byCanonical.set(binding.canonicalServerId, siblings);
59
+ }
60
+ const servers: Settings["mcpServers"] = [];
61
+ const tools: ToolRef[] = [];
62
+ const accountLabels = new Map<string, string>();
63
+ for (const tool of input.tools) {
64
+ const bindings = byCanonical.get(tool.id);
65
+ if (!bindings) {
66
+ if (!configured.get(tool.id)?.connectionRef) tools.push(tool);
67
+ continue;
68
+ }
69
+ const canonical = configured.get(tool.id);
70
+ if (!canonical?.connectionRef) {
71
+ throw new Error("Accepted MCP account route has no canonical connection configuration");
72
+ }
73
+ for (const binding of bindings) {
74
+ if (
75
+ canonical.connectionRef.authoritySource === "host" ||
76
+ canonical.connectionRef.providerDomain !== binding.providerDomain ||
77
+ (canonical.connectionRef.kind && canonical.connectionRef.kind !== binding.kind)
78
+ ) {
79
+ throw new Error("Accepted MCP account route does not match canonical provider");
80
+ }
81
+ // Static headers can belong to the formerly selected account. Credentials
82
+ // for account-qualified routes come exclusively from the native resolver.
83
+ const { headers: _headers, ...config } = canonical;
84
+ const label = `${canonical.name ?? canonical.id} — ${binding.subjectScope === "subject" ? "Personal" : "Workspace"}: ${binding.accountLabel}`;
85
+ servers.push({
86
+ ...config,
87
+ id: binding.serverId,
88
+ name: label,
89
+ connectionRef: structuredClone(binding.connectionRef),
90
+ });
91
+ tools.push({ ...tool, id: binding.serverId });
92
+ accountLabels.set(binding.serverId, label);
93
+ }
94
+ }
95
+ return {
96
+ settings: {
97
+ ...input.settings,
98
+ mcpServers: [
99
+ ...input.settings.mcpServers.filter((server) => !server.connectionRef),
100
+ ...servers,
101
+ ],
102
+ },
103
+ tools,
104
+ accountLabels,
105
+ };
106
+ }
107
+
108
+ /** Each adapter closes over its own immutable credential ref and authority
109
+ * generation. Sharing a provider transport never shares a credential resolver. */
110
+ export function expandApiIntegrationAccountRoutes(input: {
111
+ integrations: readonly ApiIntegrationRuntime[];
112
+ bindings: readonly McpConnectionAccountBinding[] | null | undefined;
113
+ tools: readonly ToolRef[];
114
+ }): ApiIntegrationRuntime[] {
115
+ const selected = new Set(input.tools.map((tool) => tool.id));
116
+ if (input.bindings == null)
117
+ return input.integrations.filter((item) => selected.has(item.serverId));
118
+ const acceptedBindings = input.bindings;
119
+ return input.integrations.flatMap((integration) => {
120
+ const bindings = acceptedBindings.filter(
121
+ (binding) => binding.canonicalServerId === integration.serverId,
122
+ );
123
+ if (bindings.length === 0)
124
+ return !integration.connectionRef && selected.has(integration.serverId) ? [integration] : [];
125
+ return bindings
126
+ .filter((binding) => selected.has(binding.serverId))
127
+ .map((binding) => {
128
+ if (binding.connectionAuthorityGeneration === undefined) {
129
+ throw new Error("Accepted API integration account route has no authority generation");
130
+ }
131
+ return {
132
+ ...integration,
133
+ serverId: binding.serverId,
134
+ connectionRef: structuredClone(binding.connectionRef),
135
+ connectionAuthorityGeneration: binding.connectionAuthorityGeneration,
136
+ };
137
+ });
138
+ });
139
+ }
@@ -1,47 +1,46 @@
1
1
  import type { Settings } from "@opengeni/config";
2
- import type { ConnectionCredentialsPort } from "@opengeni/contracts";
3
- import type { HostMcpAcceptedAuthority } from "@opengeni/contracts/host-mcp-bindings";
2
+ import { stableJson } from "@opengeni/contracts";
4
3
  import {
5
4
  buildConnectionTokenResolver,
6
- buildHostConnectionTokenResolver,
7
- getSessionTurnForAttempt,
8
- authorizeDirectHostMcpUse,
9
- resolveAcceptedHostMcpBinding,
10
5
  resolveAcceptedConnectionUse,
11
- sessionTenancyProductActivated,
12
6
  type Database,
13
7
  type ResolveConnectionCredentialInput,
14
8
  type ResolveConnectionCredentialResult,
15
9
  type SessionTurnForExecution,
16
10
  } from "@opengeni/db";
17
- import { recordTenancyCompatibilityLaneUse, type Observability } from "@opengeni/observability";
18
11
  import { mcpOperationAuthorityDigest } from "./mcp-operation-authority";
19
12
 
20
- const OPENGENI_CONNECTION_ID_PATTERN =
21
- /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
22
-
23
- export function connectionTokenResolverForTurn(input: {
13
+ type TurnConnectionInput = {
24
14
  db: Database;
25
15
  settings: Settings;
26
- connectionCredentials?: ConnectionCredentialsPort | null;
27
16
  accountId: string;
28
17
  workspaceId: string;
29
18
  sessionId: string;
30
- rootSessionId: string;
31
19
  attemptId: string;
32
20
  turn: SessionTurnForExecution;
21
+ /** Policy identities before account expansion; never used to select credentials. */
22
+ canonicalMcpServerIds?: readonly string[];
33
23
  authorizeAcceptedUse?: typeof resolveAcceptedConnectionUse;
34
- /** Test seam; production always reads the canonical active-attempt projection. */
35
- getHostTurnForAttempt?: typeof getSessionTurnForAttempt;
36
- /** Test seam for the activation fence on pre-snapshot workspace refs. */
37
- isSessionTenancyProductActivated?: typeof sessionTenancyProductActivated;
38
- /** Optional; used only for content-free compatibility-lane counters. */
39
- observability?: Observability | null | undefined;
40
- }): (request: ResolveConnectionCredentialInput) => Promise<ResolveConnectionCredentialResult> {
41
- const hostResolver = input.connectionCredentials?.mcpCredentials;
42
- const readHostTurn = input.getHostTurnForAttempt ?? getSessionTurnForAttempt;
43
- const hostDb = input.db;
44
- const nativeResolver = buildConnectionTokenResolver(input.db, input.settings);
24
+ };
25
+
26
+ type CredentialResolver = (
27
+ request: ResolveConnectionCredentialInput,
28
+ ) => Promise<ResolveConnectionCredentialResult>;
29
+
30
+ export function connectionTokenResolverForTurn(input: TurnConnectionInput): CredentialResolver {
31
+ return bindNativeConnectionCredentialsToTurn(
32
+ input,
33
+ buildConnectionTokenResolver(input.db, input.settings),
34
+ );
35
+ }
36
+
37
+ /** Bind the ordinary credential resolver to one immutable accepted turn.
38
+ * Credential acquisition and refresh stay inside the native connection engine;
39
+ * this layer adds the exact execution context and physical-request checks. */
40
+ export function bindNativeConnectionCredentialsToTurn(
41
+ input: TurnConnectionInput,
42
+ nativeResolver: CredentialResolver,
43
+ ): CredentialResolver {
45
44
  const recoveryEnabled = (serverId: string) =>
46
45
  input.settings.mcpServers.some(
47
46
  (server) =>
@@ -50,84 +49,14 @@ export function connectionTokenResolverForTurn(input: {
50
49
  server.operationRecovery !== undefined &&
51
50
  Object.keys(server.operationRecovery ?? {}).length > 0,
52
51
  );
53
- const configuredResolver = hostResolver
54
- ? async (
55
- credentialRequest: ResolveConnectionCredentialInput,
56
- ): Promise<ResolveConnectionCredentialResult> => {
57
- let authorizedSnapshot: HostMcpAcceptedAuthority | undefined;
58
- const wantsDigest = recoveryEnabled(credentialRequest.serverId);
59
- const resolver = buildHostConnectionTokenResolver(hostResolver, {
60
- accountId: input.accountId,
61
- workspaceId: input.workspaceId,
62
- sessionId: input.sessionId,
63
- rootSessionId: input.rootSessionId,
64
- turnId: input.turn.id,
65
- attemptId: input.attemptId,
66
- executionGeneration: input.turn.executionGeneration,
67
- initiator: input.turn.initiator,
68
- initiatorContext: input.turn.initiatorContext,
69
- surface: "model",
70
- resolveAcceptedBinding: (request) => resolveAcceptedHostMcpBinding(hostDb, request),
71
- // Only immutable captured direct-turn authority is currently supported.
72
- // Direct, scheduled and exact causal/child snapshots are validated;
73
- // no authority follows merely from a binding or creator identity.
74
- authorizeDurableBinding: (request) =>
75
- authorizeDirectHostMcpUse(
76
- hostDb,
77
- request,
78
- wantsDigest
79
- ? (snapshot) => {
80
- authorizedSnapshot = snapshot;
81
- }
82
- : undefined,
83
- ),
84
- authorizeExecution: async (request) => {
85
- // Native UUID references already pass the accepted-use boundary below.
86
- // Legacy opaque host IDs retain their compatibility path, but not a
87
- // right to execute after their accepted attempt has stopped.
88
- if (
89
- request.connectionRef.authoritySource !== "host" &&
90
- (!request.connectionRef.connectionId ||
91
- OPENGENI_CONNECTION_ID_PATTERN.test(request.connectionRef.connectionId))
92
- )
93
- return true;
94
- if (!request.attemptId) return false;
95
- const current = await readHostTurn(
96
- hostDb,
97
- request.workspaceId,
98
- request.sessionId,
99
- request.attemptId,
100
- );
101
- return (
102
- current !== null &&
103
- current.id === request.turnId &&
104
- current.executionGeneration === request.executionGeneration
105
- );
106
- },
107
- });
108
- const result = await resolver(credentialRequest);
109
- return result.status === "ok" && authorizedSnapshot
110
- ? {
111
- ...result,
112
- operationAuthorityDigest: mcpOperationAuthorityDigest(credentialRequest, {
113
- host: authorizedSnapshot,
114
- }),
115
- }
116
- : result;
117
- }
118
- : nativeResolver;
119
- const baseResolver = (request: ResolveConnectionCredentialInput) =>
120
- input.connectionCredentials?.mcpAuthoritySource === "host" &&
121
- request.connectionRef.authoritySource !== "host"
122
- ? nativeResolver(request)
123
- : configuredResolver(request);
52
+ const canonicalMcpServerIds = new Set(
53
+ input.canonicalMcpServerIds ??
54
+ input.settings.mcpServers.filter((server) => server.connectionRef).map((server) => server.id),
55
+ );
124
56
  return async (request) => {
125
- // Host-owned refs carry explicit provenance because their opaque ids may
126
- // themselves be valid UUIDs. They never enter OpenGeni's native connection
127
- // authority or PostgreSQL UUID lookup; the bound host authorizes the exact
128
- // immutable turn context on every credential request.
57
+ // Superseded host references must never fall through to native lookup,
58
+ // even when their opaque identifier happens to be a valid native UUID.
129
59
  if (request.connectionRef.authoritySource === "host") {
130
- if (hostResolver) return await baseResolver(request);
131
60
  return {
132
61
  status: "auth_needed",
133
62
  reason: "unsupported_auth",
@@ -144,34 +73,31 @@ export function connectionTokenResolverForTurn(input: {
144
73
  : {}),
145
74
  };
146
75
  }
147
- // Before explicit provenance existed, the public embedding contract let a
148
- // bound host use any non-UUID opaque id. Preserve those frozen/stored refs
149
- // during upgrades while keeping every UUID-shaped omission on OpenGeni's
150
- // native accepted-use authority. New host refs must carry authoritySource
151
- // so UUID-shaped host ids are unambiguous.
76
+ const accountBindings = input.turn.mcpAccountBindings ?? [];
77
+ const accountBinding = accountBindings.find((binding) => binding.serverId === request.serverId);
78
+ // Canonical policy identity cannot be invoked as a credential fallback once
79
+ // this turn selected explicit account routes for that connector.
152
80
  if (
153
- hostResolver &&
154
- input.connectionCredentials?.mcpAuthoritySource !== "host" &&
155
- request.connectionRef.connectionId &&
156
- !OPENGENI_CONNECTION_ID_PATTERN.test(request.connectionRef.connectionId)
81
+ (input.turn.mcpAccountBindings != null &&
82
+ !accountBinding &&
83
+ canonicalMcpServerIds.has(request.serverId)) ||
84
+ (!accountBinding &&
85
+ accountBindings.some((binding) => binding.canonicalServerId === request.serverId)) ||
86
+ (accountBinding &&
87
+ (request.connectionRef.connectionId !== accountBinding.connectionId ||
88
+ request.connectionRef.providerDomain !== accountBinding.providerDomain ||
89
+ request.connectionRef.kind !== accountBinding.kind ||
90
+ request.connectionRef.subjectScope !== accountBinding.subjectScope ||
91
+ stableJson(request.connectionRef) !== stableJson(accountBinding.connectionRef) ||
92
+ (accountBinding.subjectScope === "subject" &&
93
+ request.subjectId !== accountBinding.ownerSubjectId)))
157
94
  ) {
158
- try {
159
- return await baseResolver(request);
160
- } catch {
161
- return {
162
- status: "auth_needed",
163
- reason: "refresh_failed",
164
- providerDomain: request.connectionRef.providerDomain,
165
- authoritySource: "host",
166
- ...(request.connectionRef.provider ? { provider: request.connectionRef.provider } : {}),
167
- connectionId: request.connectionRef.connectionId,
168
- ...(request.connectionRef.scopes ? { scopes: request.connectionRef.scopes } : {}),
169
- ...(request.connectionRef.resource ? { resource: request.connectionRef.resource } : {}),
170
- ...(request.connectionRef.selectedResources
171
- ? { selectedResources: request.connectionRef.selectedResources }
172
- : {}),
173
- };
174
- }
95
+ return {
96
+ status: "auth_needed",
97
+ reason: "personal_authority_unavailable",
98
+ providerDomain: request.connectionRef.providerDomain,
99
+ ...(request.connectionRef.provider ? { provider: request.connectionRef.provider } : {}),
100
+ };
175
101
  }
176
102
  const acceptedDelegation = input.turn.personalConnectionDelegations.find(
177
103
  (delegation) =>
@@ -191,9 +117,7 @@ export function connectionTokenResolverForTurn(input: {
191
117
  const subjectScope: "subject" | "workspace" =
192
118
  request.connectionRef.subjectScope === "subject" ? "subject" : "workspace";
193
119
  // Every subject-scoped request must match an exact connection frozen on the
194
- // accepted turn. This also hard-fences pre-cutover common-user turns that
195
- // lack a userDelegation: the DB resolver denies those rows because only a
196
- // true legacy_user connection is eligible for bounded compatibility.
120
+ // accepted turn. The database then checks its immutable sender snapshot.
197
121
  if (
198
122
  subjectScope === "subject" &&
199
123
  (!acceptedDelegation || !request.connectionRef.connectionId)
@@ -205,31 +129,15 @@ export function connectionTokenResolverForTurn(input: {
205
129
  ...(request.connectionRef.provider ? { provider: request.connectionRef.provider } : {}),
206
130
  };
207
131
  }
208
- // Workspace-scope requests now run through the same accepted-use authority
209
- // (migration 0279): the exact workspace-owned connection is revalidated
210
- // inside the canonical lifecycle fences and every use leaves an idempotent
211
- // audit fact. A ref with no connection id is the bounded pre-snapshot
212
- // legacy path - it cannot be authorized by exact identity, so it keeps the
213
- // unprivileged resolution the old short-circuit used.
132
+ // Workspace connections also need an exact identity for accepted-use
133
+ // validation and attribution. Never rediscover a credential by domain here.
214
134
  if (subjectScope === "workspace" && !request.connectionRef.connectionId) {
215
- if (
216
- await (input.isSessionTenancyProductActivated ?? sessionTenancyProductActivated)(
217
- input.db,
218
- input.workspaceId,
219
- )
220
- ) {
221
- return {
222
- status: "auth_needed",
223
- reason: "missing_connection",
224
- providerDomain: request.connectionRef.providerDomain,
225
- ...(request.connectionRef.provider ? { provider: request.connectionRef.provider } : {}),
226
- };
227
- }
228
- // This lane writes no `connection_use_audit_facts` row, so this counter is
229
- // the only evidence it was taken. Lane name only - never the server,
230
- // provider domain, connection, or subject.
231
- recordTenancyCompatibilityLaneUse(input.observability, "connection_pre_snapshot_ref");
232
- return await baseResolver(request);
135
+ return {
136
+ status: "auth_needed",
137
+ reason: "missing_connection",
138
+ providerDomain: request.connectionRef.providerDomain,
139
+ ...(request.connectionRef.provider ? { provider: request.connectionRef.provider } : {}),
140
+ };
233
141
  }
234
142
  const credentialUseContext = {
235
143
  accountId: input.accountId,
@@ -299,50 +207,31 @@ export function connectionTokenResolverForTurn(input: {
299
207
  },
300
208
  };
301
209
  };
302
- // One place records the `legacy_user` lane for both resolution paths: the
303
- // scope comes back on the resolver result when the DB resolver authorized
304
- // internally, and on the resolution itself when the host resolver did.
305
- const recordAuthorizedScope = (scope: "workspace" | "user" | "legacy_user" | undefined) => {
306
- if (scope === "legacy_user") {
307
- recordTenancyCompatibilityLaneUse(input.observability, "connection_legacy_user");
308
- }
309
- };
310
- if (!hostResolver) {
311
- const result = await baseResolver({
312
- ...request,
313
- connectionUseContext: credentialUseContext,
314
- });
315
- if (result.status === "ok") recordAuthorizedScope(result.connectionUseAttribution?.scope);
316
- return withProviderRequestAuthorization(result);
317
- }
318
- const authorization = await authorize(credentialUseContext);
319
- if (authorization.status === "denied") {
210
+ const { subjectId: requestSubjectId, ...requestWithoutSubject } = request;
211
+ const result = await nativeResolver({
212
+ ...requestWithoutSubject,
213
+ ...(accountBinding?.connectionAuthorityGeneration !== undefined
214
+ ? { expectedAuthorityGeneration: accountBinding.connectionAuthorityGeneration }
215
+ : {}),
216
+ // Workspace authority has no personal owner, even when the turn has a
217
+ // causal human. Do not pass that human as credential ownership context.
218
+ ...(accountBinding?.subjectScope !== "workspace" && requestSubjectId !== undefined
219
+ ? { subjectId: requestSubjectId }
220
+ : {}),
221
+ connectionUseContext: credentialUseContext,
222
+ });
223
+ if (
224
+ accountBinding &&
225
+ result.status === "ok" &&
226
+ result.connectionId !== accountBinding.connectionId
227
+ ) {
320
228
  return {
321
229
  status: "auth_needed",
322
- reason:
323
- request.connectionRef.subjectScope === "subject"
324
- ? "personal_authority_unavailable"
325
- : "missing_connection",
326
- providerDomain: request.connectionRef.providerDomain,
327
- ...(request.connectionRef.provider ? { provider: request.connectionRef.provider } : {}),
230
+ reason: "personal_authority_unavailable",
231
+ providerDomain: accountBinding.providerDomain,
328
232
  };
329
233
  }
330
- recordAuthorizedScope(authorization.attribution.scope);
331
- const result = await baseResolver({
332
- ...request,
333
- connectionUseContext: credentialUseContext,
334
- connectionUseAuthority: authorization.attribution,
335
- connectionRef: {
336
- ...request.connectionRef,
337
- connectionId: authorization.attribution.connectionId,
338
- kind: authorization.connectionKind,
339
- subjectScope: authorization.attribution.scope === "workspace" ? "workspace" : "subject",
340
- },
341
- ...(authorization.attribution.ownerSubjectId
342
- ? { subjectId: authorization.attribution.ownerSubjectId }
343
- : {}),
344
- });
345
- return withProviderRequestAuthorization(result, authorization.attribution);
234
+ return withProviderRequestAuthorization(result);
346
235
  };
347
236
  }
348
237