@opengeni/core 2.7.2-canary.0 → 2.7.5-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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/core",
3
- "version": "2.7.2-canary.0",
3
+ "version": "2.7.5-canary.1",
4
4
  "description": "OpenGeni framework-agnostic core: the domain, access, and billing layers (neutral access, off-HTTP V2 surface). Behavior-preserving extraction from apps/api — keeps Hono's HTTPException for error throwing (typed-errors cleanup deferred).",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -54,15 +54,17 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@modelcontextprotocol/sdk": "^1.29.0",
57
- "@opengeni/codex": "^0.2.20-canary.2",
58
- "@opengeni/config": "^0.23.2-canary.0",
59
- "@opengeni/contracts": "^2.11.1-canary.0",
60
- "@opengeni/db": "^3.8.2-canary.0",
61
- "@opengeni/documents": "^0.8.17-canary.0",
62
- "@opengeni/events": "^0.4.15-canary.0",
63
- "@opengeni/observability": "^0.8.17-canary.0",
64
- "@opengeni/runtime": "^2.1.2-canary.0",
65
- "@opengeni/storage": "^0.2.118-canary.0",
57
+ "@opengeni/capabilities": "^0.3.2-canary.1",
58
+ "@opengeni/codex": "^0.2.21-canary.1",
59
+ "@opengeni/config": "^1.0.0-canary.1",
60
+ "@opengeni/contracts": "^2.13.0-canary.1",
61
+ "@opengeni/db": "^4.0.0-canary.1",
62
+ "@opengeni/documents": "^0.8.19-canary.1",
63
+ "@opengeni/events": "^0.4.17-canary.1",
64
+ "@opengeni/network": "^0.3.0-canary.1",
65
+ "@opengeni/observability": "^0.8.19-canary.1",
66
+ "@opengeni/runtime": "^2.3.0-canary.1",
67
+ "@opengeni/storage": "^0.2.120-canary.1",
66
68
  "hono": "^4.12.18",
67
69
  "zod": "^4.2.1"
68
70
  },
@@ -243,6 +243,31 @@ export function requireAccountAdminAuthorizationStamp(
243
243
  });
244
244
  }
245
245
 
246
+ /**
247
+ * Verify that an access authorization was minted by the canonical request
248
+ * resolver for this exact subject and workspace.
249
+ *
250
+ * This is the protocol-neutral boundary for request-local services that need
251
+ * the authenticated grant rather than a caller-supplied grant-shaped object.
252
+ * Object identity is intentional: matching fields alone are not proof that the
253
+ * request authenticated the named subject.
254
+ */
255
+ export function requireResolvedAccessGrantAuthorization(
256
+ authorization: AccessGrantAuthorization,
257
+ workspaceId: string,
258
+ ): AccessGrant {
259
+ const { grant } = authorization;
260
+ if (
261
+ !resolvedAccessGrantAuthorizations.has(authorization) ||
262
+ !authorization.contextIntegrity ||
263
+ authorization.authenticatedSubjectId !== grant.subjectId ||
264
+ grant.workspaceId !== workspaceId
265
+ ) {
266
+ throw new HTTPException(403, { message: "workspace access authorization is invalid" });
267
+ }
268
+ return grant;
269
+ }
270
+
246
271
  /**
247
272
  * Resolve the exact built-in single-user local administrator for an account.
248
273
  *
@@ -0,0 +1,215 @@
1
+ import {
2
+ createGraphqlMcpServer,
3
+ createOpenApiMcpServer,
4
+ createPinnedIntegrationTransport,
5
+ directIntegrationTransport,
6
+ IntegrationInvocationError,
7
+ type IntegrationCredentialResolver,
8
+ type IntegrationInvocationAuthority,
9
+ type IntegrationTransport,
10
+ } from "@opengeni/capabilities";
11
+ import type { Settings } from "@opengeni/config";
12
+ import type { ToolAuthNeededPayload } from "@opengeni/contracts";
13
+ import type {
14
+ ApiIntegrationRuntime,
15
+ ResolveConnectionCredentialInput,
16
+ ResolveConnectionCredentialResult,
17
+ } from "@opengeni/db";
18
+ import type { FetchLike } from "@opengeni/network";
19
+ import type { LocalMcpServerRegistration } from "@opengeni/runtime";
20
+
21
+ export type BuildApiIntegrationServersInput = {
22
+ settings: Settings;
23
+ integrations: readonly ApiIntegrationRuntime[];
24
+ authority: Omit<IntegrationInvocationAuthority, "connectionRef">;
25
+ resolveCredential: (
26
+ input: ResolveConnectionCredentialInput,
27
+ ) => Promise<ResolveConnectionCredentialResult>;
28
+ onAuthNeeded?: (payload: ToolAuthNeededPayload) => Promise<void> | void;
29
+ fetchImpl?: FetchLike;
30
+ };
31
+
32
+ /**
33
+ * Compile active persisted API facets into ordinary in-process MCP providers.
34
+ * The caller supplies its authority resolver, so agent attempts and current
35
+ * humans use the same provider assembly without sharing credentials or policy.
36
+ */
37
+ export function buildApiIntegrationMcpServers(
38
+ input: BuildApiIntegrationServersInput,
39
+ ): LocalMcpServerRegistration[] {
40
+ const transport = createPinnedIntegrationTransport({
41
+ network: input.settings,
42
+ ...(input.fetchImpl ? { fetchImpl: input.fetchImpl } : {}),
43
+ });
44
+ return input.integrations.map((integration) => {
45
+ const authority: IntegrationInvocationAuthority = {
46
+ ...input.authority,
47
+ ...(integration.connectionRef?.connectionId
48
+ ? { connectionRef: integration.connectionRef.connectionId }
49
+ : {}),
50
+ };
51
+ const credentialResolver = integration.connectionRef
52
+ ? integrationCredentialResolver(input, integration, "execution")
53
+ : undefined;
54
+ const buildServer = (
55
+ serverTransport: IntegrationTransport,
56
+ serverCredentialResolver = credentialResolver,
57
+ ) =>
58
+ integration.revision.protocol === "openapi"
59
+ ? createOpenApiMcpServer({
60
+ revision: integration.revision,
61
+ transport: serverTransport,
62
+ authority,
63
+ ...(serverCredentialResolver ? { credentialResolver: serverCredentialResolver } : {}),
64
+ })
65
+ : createGraphqlMcpServer({
66
+ revision: integration.revision,
67
+ endpoint: integration.baseUrl,
68
+ transport: serverTransport,
69
+ authority,
70
+ ...(serverCredentialResolver ? { credentialResolver: serverCredentialResolver } : {}),
71
+ });
72
+ const server = buildServer(transport);
73
+ const preflightCredentialResolver = integration.connectionRef
74
+ ? integrationCredentialResolver(input, integration, "preflight")
75
+ : undefined;
76
+ const preflightServer = preflightCredentialResolver
77
+ ? buildServer(providerBlockingPreflightTransport, preflightCredentialResolver)
78
+ : null;
79
+ return {
80
+ id: integration.serverId,
81
+ server,
82
+ approvalAuthority: {
83
+ kind: "api_integration",
84
+ capabilityId: integration.capabilityId,
85
+ pluginKey: integration.pluginKey,
86
+ pluginInstallationId: integration.pluginInstallationId,
87
+ installationVersion: integration.installationVersion,
88
+ instanceId: integration.instanceId,
89
+ instanceKey: integration.instanceKey,
90
+ instanceVersion: integration.instanceVersion,
91
+ definitionId: integration.definitionId,
92
+ definitionProvenance: integration.definitionProvenance,
93
+ revisionId: integration.revision.id,
94
+ baseUrl: integration.baseUrl,
95
+ providerDomain: integration.providerDomain,
96
+ connectionRef: integration.connectionRef,
97
+ connectionAuthorityGeneration: integration.connectionAuthorityGeneration,
98
+ },
99
+ ...(integration.connectionRef?.connectionId
100
+ ? { resolvedConnectionId: integration.connectionRef.connectionId }
101
+ : {}),
102
+ ...(credentialResolver
103
+ ? {
104
+ preflightCall: async (
105
+ toolName: string,
106
+ args: Record<string, unknown>,
107
+ options?: { signal?: AbortSignal },
108
+ ) => await preflightApiIntegrationCall(preflightServer!, toolName, args, options),
109
+ }
110
+ : {}),
111
+ };
112
+ });
113
+ }
114
+
115
+ const providerBlockingPreflightTransport = directIntegrationTransport(async () => {
116
+ throw new Error("integration provider request blocked by preflight");
117
+ });
118
+
119
+ async function preflightApiIntegrationCall(
120
+ server: LocalMcpServerRegistration["server"],
121
+ toolName: string,
122
+ args: Record<string, unknown>,
123
+ options?: { signal?: AbortSignal },
124
+ ): Promise<void> {
125
+ try {
126
+ await server.callTool(toolName, args, null, options);
127
+ } catch (error) {
128
+ if (error instanceof IntegrationInvocationError && error.code === "request_failed") return;
129
+ throw error;
130
+ }
131
+ throw new Error("integration preflight unexpectedly crossed the provider request boundary");
132
+ }
133
+
134
+ function integrationCredentialResolver(
135
+ input: BuildApiIntegrationServersInput,
136
+ integration: ApiIntegrationRuntime,
137
+ credentialResolutionMode: "execution" | "preflight",
138
+ ): IntegrationCredentialResolver {
139
+ const connectionRef = integration.connectionRef;
140
+ if (!connectionRef) throw new Error("Integration credential resolver requires a connection");
141
+ const expectedAuthorityGeneration = integration.connectionAuthorityGeneration;
142
+ if (expectedAuthorityGeneration === null) {
143
+ throw new Error("Integration credential resolver requires a connection authority generation");
144
+ }
145
+ return {
146
+ resolve: async (request) => {
147
+ const result = await input.resolveCredential({
148
+ workspaceId: input.authority.workspaceId,
149
+ serverId: integration.serverId,
150
+ toolName: request.operationKey,
151
+ connectionRef,
152
+ destinationUrl: request.destinationUrl,
153
+ credentialTarget: "http_api",
154
+ forceRefresh: request.forceRefresh === true,
155
+ credentialResolutionMode,
156
+ expectedAuthorityGeneration,
157
+ });
158
+ if (result.status === "auth_needed") {
159
+ await publishAuthNeeded(
160
+ input,
161
+ integration.serverId,
162
+ request.operationKey,
163
+ result,
164
+ connectionRef,
165
+ );
166
+ return null;
167
+ }
168
+ const destination = new URL(request.destinationUrl);
169
+ return {
170
+ audience: { origin: destination.origin, pathPrefix: "/" },
171
+ placements:
172
+ result.placements ??
173
+ Object.entries(result.headers).map(([name, value]) => ({
174
+ carrier: "header" as const,
175
+ name,
176
+ value,
177
+ })),
178
+ ...(result.authorizeProviderRequest
179
+ ? { authorizeProviderRequest: result.authorizeProviderRequest }
180
+ : {}),
181
+ ...(result.expiresAt ? { expiresAt: result.expiresAt.toISOString() } : {}),
182
+ ...(connectionRef.scopes ? { scope: [...connectionRef.scopes] } : {}),
183
+ };
184
+ },
185
+ };
186
+ }
187
+
188
+ async function publishAuthNeeded(
189
+ input: BuildApiIntegrationServersInput,
190
+ serverId: string,
191
+ toolName: string,
192
+ result: Extract<ResolveConnectionCredentialResult, { status: "auth_needed" }>,
193
+ connectionRef: NonNullable<ApiIntegrationRuntime["connectionRef"]>,
194
+ ): Promise<void> {
195
+ try {
196
+ await input.onAuthNeeded?.({
197
+ serverId,
198
+ toolName,
199
+ providerDomain: result.providerDomain,
200
+ ...(result.provider ? { provider: result.provider } : {}),
201
+ reason: result.reason,
202
+ ...(result.connectionId ? { connectionId: result.connectionId } : {}),
203
+ ...(result.authoritySource === "host" || connectionRef.authoritySource === "host"
204
+ ? { authoritySource: "host" as const }
205
+ : {}),
206
+ ...(result.scopes ? { scopes: result.scopes } : {}),
207
+ ...(result.resource ? { resource: result.resource } : {}),
208
+ ...(result.selectedResources ? { selectedResources: result.selectedResources } : {}),
209
+ ...(result.authorizationUrl ? { authorizationUrl: result.authorizationUrl } : {}),
210
+ });
211
+ } catch {
212
+ // Authentication notices are advisory UI/audit signals. The local tool
213
+ // still returns the fixed connection-required result when publication fails.
214
+ }
215
+ }
@@ -11,6 +11,7 @@ import {
11
11
  getSandbox,
12
12
  getVariableSet,
13
13
  NewSessionDraftAccessError,
14
+ newSessionDraftSelectedProjectChannelId,
14
15
  newSessionDraftToolsProvided,
15
16
  newSessionSelectionHistory,
16
17
  publicNewSessionDraftOptions,
@@ -41,6 +42,7 @@ function mapNewSessionDraft(
41
42
  row: Awaited<ReturnType<typeof getNewSessionDraftInTransaction>>,
42
43
  ): NewSessionDraftValue | null {
43
44
  if (!row) return null;
45
+ const selectedProjectChannelId = newSessionDraftSelectedProjectChannelId(row);
44
46
  return NewSessionDraft.parse({
45
47
  revision: row.revision,
46
48
  text: row.text,
@@ -50,6 +52,7 @@ function mapNewSessionDraft(
50
52
  model: row.model,
51
53
  reasoningEffort: row.reasoningEffort,
52
54
  latencyMode: row.latencyMode,
55
+ ...(selectedProjectChannelId !== undefined ? { selectedProjectChannelId } : {}),
53
56
  options: publicNewSessionDraftOptions(row),
54
57
  selectionHistory: newSessionSelectionHistory(row),
55
58
  updatedAt: row.updatedAt.toISOString(),
@@ -264,6 +267,9 @@ export async function saveActorNewSessionDraft(
264
267
  model: input.model,
265
268
  reasoningEffort: input.reasoningEffort,
266
269
  latencyMode: input.latencyMode,
270
+ ...(input.selectedProjectChannelId !== undefined
271
+ ? { selectedProjectChannelId: input.selectedProjectChannelId }
272
+ : {}),
267
273
  options: input.options,
268
274
  // Only managed people are removed through removeWorkspaceMember().
269
275
  // API keys and delegated service actors (for example the first-party
@@ -555,7 +555,7 @@ export async function controlAgentSessionWorkstream(
555
555
  wakeRevision: result.workflowWake.wakeRevision,
556
556
  shouldSignal: true,
557
557
  interruptionCount: result.interruptionCount,
558
- controlRequested: true,
558
+ controlRequested: input.action === "pause",
559
559
  });
560
560
  },
561
561
  },
@@ -928,7 +928,7 @@ export async function controlHumanSessionWorkstreamWithOutcome(
928
928
  wakeRevision: result.workflowWake.wakeRevision,
929
929
  shouldSignal: true,
930
930
  interruptionCount: result.interruptionCount,
931
- controlRequested: true,
931
+ controlRequested: input.action === "pause",
932
932
  });
933
933
  },
934
934
  },
@@ -0,0 +1,169 @@
1
+ import type {
2
+ GitHubActionPolicyActor,
3
+ GitHubActionPolicyActorState,
4
+ GitHubActionPolicyDecision,
5
+ GitHubActionPolicyEffectiveDecision,
6
+ GitHubActionPolicyGroup,
7
+ } from "@opengeni/contracts";
8
+ import {
9
+ listConnectorActionPolicies,
10
+ resolveConnectorActionPolicy,
11
+ upsertConnectorActionPolicies,
12
+ type ConnectorActionPolicySnapshotEntry,
13
+ type Database,
14
+ } from "@opengeni/db";
15
+ import {
16
+ GITHUB_REST_MCP_APP_SERVER_ID,
17
+ GITHUB_REST_MCP_PERSONAL_SERVER_ID,
18
+ GITHUB_REST_WRITE_TOOL_NAMES,
19
+ } from "@opengeni/runtime/github-rest-mcp";
20
+
21
+ export const GITHUB_ACTION_POLICY_GROUP_TOOL_NAMES = {
22
+ routine: GITHUB_REST_WRITE_TOOL_NAMES.filter(
23
+ (toolName) => toolName !== "pull_request_review_submit" && toolName !== "pull_request_merge",
24
+ ),
25
+ review: ["pull_request_review_submit"],
26
+ merge: ["pull_request_merge"],
27
+ } as const satisfies Record<GitHubActionPolicyGroup, readonly string[]>;
28
+
29
+ const groupedToolNames = Object.values(GITHUB_ACTION_POLICY_GROUP_TOOL_NAMES).flat();
30
+ if (
31
+ groupedToolNames.length !== new Set(groupedToolNames).size ||
32
+ GITHUB_REST_WRITE_TOOL_NAMES.some((toolName) => !groupedToolNames.includes(toolName))
33
+ ) {
34
+ throw new Error("GitHub action policy groups must cover every write tool exactly once");
35
+ }
36
+
37
+ export type GitHubActionPolicyActorBinding = {
38
+ actor: GitHubActionPolicyActor;
39
+ label: string;
40
+ connectionId: string;
41
+ serverId: typeof GITHUB_REST_MCP_APP_SERVER_ID | typeof GITHUB_REST_MCP_PERSONAL_SERVER_ID;
42
+ };
43
+
44
+ function effectiveToolDecision(
45
+ policies: readonly ConnectorActionPolicySnapshotEntry[],
46
+ actor: GitHubActionPolicyActorBinding,
47
+ toolName: string,
48
+ ): GitHubActionPolicyDecision {
49
+ const resolved = resolveConnectorActionPolicy(policies, {
50
+ connectionId: actor.connectionId,
51
+ serverId: actor.serverId,
52
+ toolName,
53
+ actionName: toolName,
54
+ });
55
+ // Match connector_write admission: absent policy inherits repository capability.
56
+ // This is a policy projection, not a grant of repository or provider access.
57
+ if (!resolved.managed) return "allow";
58
+ if (resolved.entry) return resolved.entry.policy;
59
+ return resolved.decision;
60
+ }
61
+
62
+ function effectiveGroupDecision(
63
+ policies: readonly ConnectorActionPolicySnapshotEntry[],
64
+ actor: GitHubActionPolicyActorBinding,
65
+ group: GitHubActionPolicyGroup,
66
+ ): GitHubActionPolicyEffectiveDecision {
67
+ const decisions = new Set(
68
+ GITHUB_ACTION_POLICY_GROUP_TOOL_NAMES[group].map((toolName) =>
69
+ effectiveToolDecision(policies, actor, toolName),
70
+ ),
71
+ );
72
+ return decisions.size === 1 ? decisions.values().next().value! : "mixed";
73
+ }
74
+
75
+ export function projectGitHubActionPolicyActor(
76
+ policies: readonly ConnectorActionPolicySnapshotEntry[],
77
+ binding: GitHubActionPolicyActorBinding,
78
+ ): GitHubActionPolicyActorState {
79
+ const groups = {
80
+ routine: effectiveGroupDecision(policies, binding, "routine"),
81
+ review: effectiveGroupDecision(policies, binding, "review"),
82
+ merge: effectiveGroupDecision(policies, binding, "merge"),
83
+ };
84
+ return binding.actor.kind === "workspace_app"
85
+ ? {
86
+ kind: "workspace_app",
87
+ installationId: binding.actor.installationId,
88
+ label: binding.label,
89
+ groups,
90
+ }
91
+ : {
92
+ kind: "personal",
93
+ connectionId: binding.actor.connectionId,
94
+ label: binding.label,
95
+ groups,
96
+ };
97
+ }
98
+
99
+ export async function listGitHubActionPolicyActors(
100
+ db: Database,
101
+ input: {
102
+ accountId: string;
103
+ workspaceId: string;
104
+ actors: readonly GitHubActionPolicyActorBinding[];
105
+ },
106
+ ): Promise<GitHubActionPolicyActorState[]> {
107
+ const policies = await listConnectorActionPolicies(db, {
108
+ accountId: input.accountId,
109
+ workspaceId: input.workspaceId,
110
+ connectionIds: input.actors.map((actor) => actor.connectionId),
111
+ });
112
+ return input.actors.map((actor) => projectGitHubActionPolicyActor(policies, actor));
113
+ }
114
+
115
+ export async function updateGitHubActionPolicyGroup(
116
+ db: Database,
117
+ input: {
118
+ accountId: string;
119
+ workspaceId: string;
120
+ subjectId: string;
121
+ actor: GitHubActionPolicyActorBinding;
122
+ group: GitHubActionPolicyGroup;
123
+ decision: GitHubActionPolicyDecision;
124
+ },
125
+ ): Promise<GitHubActionPolicyActorState> {
126
+ await upsertConnectorActionPolicies(
127
+ db,
128
+ GITHUB_ACTION_POLICY_GROUP_TOOL_NAMES[input.group].map((toolName) => ({
129
+ accountId: input.accountId,
130
+ workspaceId: input.workspaceId,
131
+ subjectId: input.subjectId,
132
+ connectionId: input.actor.connectionId,
133
+ serverId: input.actor.serverId,
134
+ toolName,
135
+ actionName: toolName,
136
+ policy: input.decision,
137
+ })),
138
+ );
139
+ const policies = await listConnectorActionPolicies(db, {
140
+ accountId: input.accountId,
141
+ workspaceId: input.workspaceId,
142
+ connectionIds: [input.actor.connectionId],
143
+ });
144
+ return projectGitHubActionPolicyActor(policies, input.actor);
145
+ }
146
+
147
+ export function githubAppActionPolicyActor(input: {
148
+ installationId: number;
149
+ accountLogin: string | null;
150
+ }): GitHubActionPolicyActorBinding {
151
+ return {
152
+ actor: { kind: "workspace_app", installationId: input.installationId },
153
+ label: input.accountLogin ? `OpenGeni bot on ${input.accountLogin}` : "OpenGeni bot",
154
+ connectionId: `github-app:${input.installationId}`,
155
+ serverId: GITHUB_REST_MCP_APP_SERVER_ID,
156
+ };
157
+ }
158
+
159
+ export function personalGitHubActionPolicyActor(input: {
160
+ connectionId: string;
161
+ githubLogin: string;
162
+ }): GitHubActionPolicyActorBinding {
163
+ return {
164
+ actor: { kind: "personal", connectionId: input.connectionId },
165
+ label: `@${input.githubLogin}`,
166
+ connectionId: input.connectionId,
167
+ serverId: GITHUB_REST_MCP_PERSONAL_SERVER_ID,
168
+ };
169
+ }
package/src/index.ts CHANGED
@@ -80,6 +80,7 @@ export * from "./domain/product-integration-pack";
80
80
  export * from "./domain/personal-connection-delegations";
81
81
  export * from "./domain/resources";
82
82
  export * from "./domain/github-repository-bindings";
83
+ export * from "./domain/github-action-policies";
83
84
  export * from "./domain/session-tool-policy";
84
85
  export * from "./domain/scheduled-tasks";
85
86
  export * from "./domain/sessions";
@@ -106,6 +107,7 @@ export * from "./application/composer-submit";
106
107
  export * from "./application/session-commands";
107
108
  export * from "./application/session-tenancy";
108
109
  export * from "./application/user-resource-grants";
110
+ export * from "./application/api-integration-servers";
109
111
 
110
112
  // Durable editable-artifact live broker, ticket, ports, and projection types.
111
113
  export * from "./editable-artifact-live";
@@ -29,7 +29,6 @@ import {
29
29
  type SandboxRetainedProcess,
30
30
  type SandboxWorkspaceMutationAdmission,
31
31
  } from "@opengeni/db";
32
- import { settleSessionBackgroundCommandForRetainedProcess } from "@opengeni/db/session-background-commands";
33
32
  import { appendAndPublishEvents, type EventBus } from "@opengeni/events";
34
33
  import {
35
34
  isProviderSandboxGoneDuringRoutedOperation,
@@ -449,14 +448,11 @@ export function wrapChannelABoxWithRouting(
449
448
  reason: proof.reason,
450
449
  idleGraceMs: settings.sandboxIdleGraceMs,
451
450
  });
452
- const backgroundSettlement = retainedProcessBackgroundSettlement(settlement.process, proof);
453
- await settleSessionBackgroundCommandForRetainedProcess(db, {
454
- accountId: ids.accountId,
455
- workspaceId: ids.workspaceId,
456
- sessionId: ids.sessionId,
457
- retainedProcessId: process.id,
458
- ...backgroundSettlement,
459
- });
451
+ if (settlement.backgroundCommandEvents.length > 0 && bus) {
452
+ await bus
453
+ .publish(ids.workspaceId, ids.sessionId, settlement.backgroundCommandEvents)
454
+ .catch(() => undefined);
455
+ }
460
456
  }
461
457
  : undefined;
462
458
  const resolver = makeActiveBackendResolver({
@@ -520,6 +516,7 @@ export function wrapChannelABoxWithRouting(
520
516
  });
521
517
 
522
518
  const proxy = new RoutingSandboxSession({
519
+ bindActiveRouteOnFirstResolve: true,
523
520
  defaultResolved: {
524
521
  session: established.session as RoutableBackendSession,
525
522
  sandboxId: null,