@opengeni/core 0.4.10 → 0.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/core",
3
- "version": "0.4.10",
3
+ "version": "0.8.0",
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": {
@@ -34,15 +34,15 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@modelcontextprotocol/sdk": "^1.29.0",
37
- "@opengeni/codex": "^0.2.3",
38
- "@opengeni/config": "^0.5.1",
39
- "@opengeni/contracts": "^0.10.0",
40
- "@opengeni/db": "^0.7.3",
41
- "@opengeni/documents": "^0.2.12",
42
- "@opengeni/events": "^0.3.3",
37
+ "@opengeni/codex": "^0.2.5",
38
+ "@opengeni/config": "^0.6.2",
39
+ "@opengeni/contracts": "^0.15.0",
40
+ "@opengeni/db": "^0.9.3",
41
+ "@opengeni/documents": "^0.2.19",
42
+ "@opengeni/events": "^0.3.10",
43
43
  "@opengeni/observability": "^0.3.0",
44
- "@opengeni/runtime": "^0.7.1",
45
- "@opengeni/storage": "^0.2.10",
44
+ "@opengeni/runtime": "^0.11.0",
45
+ "@opengeni/storage": "^0.2.15",
46
46
  "hono": "^4.12.18"
47
47
  },
48
48
  "engines": {
@@ -240,6 +240,10 @@ async function delegatedAccessContext(
240
240
  ? { executionGeneration: payload.executionGeneration }
241
241
  : {}),
242
242
  },
243
+ ...(payload.serviceInitiator ? { serviceInitiator: payload.serviceInitiator } : {}),
244
+ ...(payload.serviceInitiatorContext
245
+ ? { serviceInitiatorContext: payload.serviceInitiatorContext }
246
+ : {}),
243
247
  },
244
248
  ],
245
249
  defaultAccountId: payload.accountId,
@@ -4,6 +4,9 @@ import type {
4
4
  EditSessionQueueItemRequest,
5
5
  MoveSessionQueueItemRequest,
6
6
  SaveComposerDraftRequest,
7
+ AccessGrant,
8
+ SessionAuthorizationOperation,
9
+ SessionAuthorizationPort,
7
10
  SessionCommandReceipt,
8
11
  SessionControlRequest,
9
12
  SessionControlResponse,
@@ -24,6 +27,8 @@ import {
24
27
  moveQueuedTurnInTransaction,
25
28
  mutateSessionControlInTransaction,
26
29
  mutateWorkspaceControlInTransaction,
30
+ projectEffectiveControlForRelatedAccess,
31
+ runIdempotentPersistenceTransaction,
27
32
  saveComposerDraftInTransaction,
28
33
  sendAgentMessageInTransaction,
29
34
  serializeEffectiveSessionControl,
@@ -40,6 +45,10 @@ import {
40
45
  type EventBus,
41
46
  } from "@opengeni/events";
42
47
  import type { SessionWorkflowClient } from "../dependencies";
48
+ import {
49
+ requireSessionAuthorization,
50
+ type ResolvedSessionAuthorization,
51
+ } from "../session-authorization";
43
52
 
44
53
  export type HumanSessionCommandContext = {
45
54
  accountId: string;
@@ -51,12 +60,67 @@ export type HumanSessionCommandContext = {
51
60
  export type AgentSessionCommandContext = {
52
61
  accountId: string;
53
62
  workspaceId: string;
63
+ subjectId: string;
54
64
  callerSessionId: string;
55
65
  callerTurnId: string;
56
66
  callerAttemptId: string;
57
67
  callerExecutionGeneration: number;
58
68
  };
59
69
 
70
+ type SessionAuthorizationCommandDeps = {
71
+ db: Database;
72
+ sessionAuthorization?: SessionAuthorizationPort | null;
73
+ };
74
+
75
+ function humanAccessGrant(context: HumanSessionCommandContext): AccessGrant {
76
+ return {
77
+ accountId: context.accountId,
78
+ workspaceId: context.workspaceId,
79
+ subjectId: context.subjectId,
80
+ permissions: [],
81
+ };
82
+ }
83
+
84
+ function agentAccessGrant(context: AgentSessionCommandContext): AccessGrant {
85
+ return {
86
+ accountId: context.accountId,
87
+ workspaceId: context.workspaceId,
88
+ subjectId: context.subjectId,
89
+ permissions: [],
90
+ metadata: {
91
+ sessionId: context.callerSessionId,
92
+ turnId: context.callerTurnId,
93
+ attemptId: context.callerAttemptId,
94
+ executionGeneration: context.callerExecutionGeneration,
95
+ },
96
+ };
97
+ }
98
+
99
+ async function authorizeHumanSessionCommand(
100
+ deps: SessionAuthorizationCommandDeps,
101
+ context: HumanSessionCommandContext,
102
+ operation: SessionAuthorizationOperation,
103
+ ): Promise<ResolvedSessionAuthorization | null> {
104
+ return await requireSessionAuthorization(deps, humanAccessGrant(context), {
105
+ sessionId: context.sessionId,
106
+ operation,
107
+ surface: "core",
108
+ });
109
+ }
110
+
111
+ async function authorizeAgentSessionCommand(
112
+ deps: SessionAuthorizationCommandDeps,
113
+ context: AgentSessionCommandContext,
114
+ targetSessionId: string,
115
+ operation: SessionAuthorizationOperation,
116
+ ): Promise<ResolvedSessionAuthorization | null> {
117
+ return await requireSessionAuthorization(deps, agentAccessGrant(context), {
118
+ sessionId: targetSessionId,
119
+ operation,
120
+ surface: "core",
121
+ });
122
+ }
123
+
60
124
  function agentActor(context: AgentSessionCommandContext) {
61
125
  return {
62
126
  type: "agent_attempt" as const,
@@ -67,11 +131,39 @@ function agentActor(context: AgentSessionCommandContext) {
67
131
  };
68
132
  }
69
133
 
134
+ /**
135
+ * Retry only one operation-keyed Agent command transaction. The caller keeps
136
+ * event publication and Temporal wake delivery after this returns, so a
137
+ * deadlock/serialization retry can never replay an external effect.
138
+ */
139
+ async function runAgentCommandPersistenceTransaction<T>(
140
+ deps: { db: Database },
141
+ context: AgentSessionCommandContext,
142
+ input: {
143
+ stage: string;
144
+ eventTypes: string[];
145
+ transaction: (tx: Database) => Promise<T>;
146
+ },
147
+ ): Promise<T> {
148
+ return await runIdempotentPersistenceTransaction(
149
+ {
150
+ stage: input.stage,
151
+ eventTypes: input.eventTypes,
152
+ maxAttempts: 3,
153
+ },
154
+ async () =>
155
+ await withWorkspaceRls(deps.db, context.workspaceId, async (scoped) =>
156
+ scoped.transaction(async (tx) => await input.transaction(tx as unknown as Database)),
157
+ ),
158
+ );
159
+ }
160
+
70
161
  async function publishAndWakeAgentCommand(
71
162
  deps: {
72
163
  db: Database;
73
164
  bus: EventBus;
74
165
  workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
166
+ sessionAuthorization?: SessionAuthorizationPort | null;
75
167
  },
76
168
  input: {
77
169
  accountId: string;
@@ -161,13 +253,17 @@ export async function sendAgentSessionMessage(
161
253
  db: Database;
162
254
  bus: EventBus;
163
255
  workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
256
+ sessionAuthorization?: SessionAuthorizationPort | null;
164
257
  },
165
258
  context: AgentSessionCommandContext,
166
259
  input: { targetSessionId: string; text: string; idempotencyKey: string },
167
260
  ) {
168
- const result = await withWorkspaceRls(deps.db, context.workspaceId, (scoped) =>
169
- scoped.transaction((tx) =>
170
- sendAgentMessageInTransaction(tx as unknown as Database, {
261
+ await authorizeAgentSessionCommand(deps, context, input.targetSessionId, "session.append");
262
+ const result = await runAgentCommandPersistenceTransaction(deps, context, {
263
+ stage: "session_commands.agent_message",
264
+ eventTypes: ["system.update.pending"],
265
+ transaction: async (tx) =>
266
+ await sendAgentMessageInTransaction(tx, {
171
267
  accountId: context.accountId,
172
268
  workspaceId: context.workspaceId,
173
269
  targetSessionId: input.targetSessionId,
@@ -175,8 +271,7 @@ export async function sendAgentSessionMessage(
175
271
  operationKey: input.idempotencyKey,
176
272
  text: input.text,
177
273
  }),
178
- ),
179
- );
274
+ });
180
275
  await publishAndWakeAgentCommand(deps, {
181
276
  accountId: context.accountId,
182
277
  workspaceId: context.workspaceId,
@@ -196,13 +291,17 @@ export async function steerAgentSession(
196
291
  db: Database;
197
292
  bus: EventBus;
198
293
  workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
294
+ sessionAuthorization?: SessionAuthorizationPort | null;
199
295
  },
200
296
  context: AgentSessionCommandContext,
201
297
  input: { targetSessionId: string; instruction: string; idempotencyKey: string },
202
298
  ) {
203
- const result = await withWorkspaceRls(deps.db, context.workspaceId, (scoped) =>
204
- scoped.transaction((tx) =>
205
- steerAgentSessionInTransaction(tx as unknown as Database, {
299
+ await authorizeAgentSessionCommand(deps, context, input.targetSessionId, "session.steer");
300
+ const result = await runAgentCommandPersistenceTransaction(deps, context, {
301
+ stage: "session_commands.agent_steer",
302
+ eventTypes: ["session.control.steer_requested", "system.update.pending", "turn.superseded"],
303
+ transaction: async (tx) =>
304
+ await steerAgentSessionInTransaction(tx, {
206
305
  accountId: context.accountId,
207
306
  workspaceId: context.workspaceId,
208
307
  targetSessionId: input.targetSessionId,
@@ -210,8 +309,7 @@ export async function steerAgentSession(
210
309
  operationKey: input.idempotencyKey,
211
310
  instruction: input.instruction,
212
311
  }),
213
- ),
214
- );
312
+ });
215
313
  await publishAndWakeAgentCommand(deps, {
216
314
  accountId: context.accountId,
217
315
  workspaceId: context.workspaceId,
@@ -231,6 +329,7 @@ export async function controlAgentSessionWorkstream(
231
329
  db: Database;
232
330
  bus: EventBus;
233
331
  workflowClient: Pick<SessionWorkflowClient, "requestSessionWorkflowWakeDispatch">;
332
+ sessionAuthorization?: SessionAuthorizationPort | null;
234
333
  },
235
334
  context: AgentSessionCommandContext,
236
335
  input: {
@@ -240,6 +339,7 @@ export async function controlAgentSessionWorkstream(
240
339
  reason?: string | null;
241
340
  },
242
341
  ) {
342
+ await authorizeAgentSessionCommand(deps, context, input.targetSessionId, "session.control");
243
343
  const result = await withWorkspaceRls(deps.db, context.workspaceId, (scoped) =>
244
344
  scoped.transaction((tx) =>
245
345
  mutateSessionControlInTransaction(tx as unknown as Database, {
@@ -293,18 +393,31 @@ function composerDraft(
293
393
  };
294
394
  }
295
395
 
296
- async function authoritativeQueue(db: Database, workspaceId: string, sessionId: string) {
396
+ async function authoritativeQueue(
397
+ db: Database,
398
+ workspaceId: string,
399
+ sessionId: string,
400
+ relatedSessionAccess: "target" | "root",
401
+ ) {
297
402
  const snapshot = await getSessionQueueSnapshot(db, workspaceId, sessionId);
298
403
  if (!snapshot) throw new Error(`Session not found: ${sessionId}`);
299
- return snapshot;
404
+ return {
405
+ ...snapshot,
406
+ effectiveControl: projectEffectiveControlForRelatedAccess(
407
+ snapshot.effectiveControl,
408
+ sessionId,
409
+ relatedSessionAccess,
410
+ ),
411
+ };
300
412
  }
301
413
 
302
414
  export async function moveHumanQueuePrompt(
303
- deps: { db: Database; bus: EventBus },
415
+ deps: { db: Database; bus: EventBus; sessionAuthorization?: SessionAuthorizationPort | null },
304
416
  context: HumanSessionCommandContext,
305
417
  turnId: string,
306
418
  input: MoveSessionQueueItemRequest,
307
419
  ): Promise<SessionQueueMutationResponse> {
420
+ const authorization = await authorizeHumanSessionCommand(deps, context, "session.queue.control");
308
421
  const result = await withWorkspaceRls(deps.db, context.workspaceId, (scoped) =>
309
422
  scoped.transaction((tx) =>
310
423
  moveQueuedTurnInTransaction(tx as unknown as Database, {
@@ -319,18 +432,24 @@ export async function moveHumanQueuePrompt(
319
432
  );
320
433
  const response = {
321
434
  receipt: receipt(result.receipt),
322
- snapshot: await authoritativeQueue(deps.db, context.workspaceId, context.sessionId),
435
+ snapshot: await authoritativeQueue(
436
+ deps.db,
437
+ context.workspaceId,
438
+ context.sessionId,
439
+ authorization?.relatedSessionAccess ?? "root",
440
+ ),
323
441
  };
324
442
  await publishSessionEventIds(deps, context.workspaceId, context.sessionId, result.eventIds);
325
443
  return response;
326
444
  }
327
445
 
328
446
  export async function deleteHumanQueuePrompt(
329
- deps: { db: Database; bus: EventBus },
447
+ deps: { db: Database; bus: EventBus; sessionAuthorization?: SessionAuthorizationPort | null },
330
448
  context: HumanSessionCommandContext,
331
449
  turnId: string,
332
450
  input: DeleteSessionQueueItemRequest,
333
451
  ): Promise<SessionQueueMutationResponse> {
452
+ const authorization = await authorizeHumanSessionCommand(deps, context, "session.queue.control");
334
453
  const result = await withWorkspaceRls(deps.db, context.workspaceId, (scoped) =>
335
454
  scoped.transaction((tx) =>
336
455
  deleteSessionQueueItemInTransaction(tx as unknown as Database, {
@@ -345,18 +464,24 @@ export async function deleteHumanQueuePrompt(
345
464
  );
346
465
  const response = {
347
466
  receipt: receipt(result.receipt),
348
- snapshot: await authoritativeQueue(deps.db, context.workspaceId, context.sessionId),
467
+ snapshot: await authoritativeQueue(
468
+ deps.db,
469
+ context.workspaceId,
470
+ context.sessionId,
471
+ authorization?.relatedSessionAccess ?? "root",
472
+ ),
349
473
  };
350
474
  await publishSessionEventIds(deps, context.workspaceId, context.sessionId, result.eventIds);
351
475
  return response;
352
476
  }
353
477
 
354
478
  export async function editHumanQueuePrompt(
355
- deps: { db: Database; bus: EventBus },
479
+ deps: { db: Database; bus: EventBus; sessionAuthorization?: SessionAuthorizationPort | null },
356
480
  context: HumanSessionCommandContext,
357
481
  turnId: string,
358
482
  input: EditSessionQueueItemRequest,
359
483
  ): Promise<SessionQueueMutationResponse> {
484
+ const authorization = await authorizeHumanSessionCommand(deps, context, "session.queue.control");
360
485
  const result = await withWorkspaceSubjectRls(
361
486
  deps.db,
362
487
  context.workspaceId,
@@ -376,7 +501,12 @@ export async function editHumanQueuePrompt(
376
501
  );
377
502
  const response = {
378
503
  receipt: receipt(result.receipt),
379
- snapshot: await authoritativeQueue(deps.db, context.workspaceId, context.sessionId),
504
+ snapshot: await authoritativeQueue(
505
+ deps.db,
506
+ context.workspaceId,
507
+ context.sessionId,
508
+ authorization?.relatedSessionAccess ?? "root",
509
+ ),
380
510
  draft: composerDraft(result.draft)!,
381
511
  };
382
512
  await publishSessionEventIds(deps, context.workspaceId, context.sessionId, result.eventIds);
@@ -384,11 +514,12 @@ export async function editHumanQueuePrompt(
384
514
  }
385
515
 
386
516
  export async function steerHumanQueuePrompt(
387
- deps: { db: Database; bus: EventBus },
517
+ deps: { db: Database; bus: EventBus; sessionAuthorization?: SessionAuthorizationPort | null },
388
518
  context: HumanSessionCommandContext,
389
519
  turnId: string,
390
520
  input: SteerSessionQueueItemRequest,
391
521
  ): Promise<SessionQueueMutationResponse> {
522
+ const authorization = await authorizeHumanSessionCommand(deps, context, "session.queue.control");
392
523
  const result = await withWorkspaceRls(deps.db, context.workspaceId, (scoped) =>
393
524
  scoped.transaction((tx) =>
394
525
  steerQueuedTurnInTransaction(tx as unknown as Database, {
@@ -403,7 +534,12 @@ export async function steerHumanQueuePrompt(
403
534
  );
404
535
  const response = {
405
536
  receipt: receipt(result.receipt),
406
- snapshot: await authoritativeQueue(deps.db, context.workspaceId, context.sessionId),
537
+ snapshot: await authoritativeQueue(
538
+ deps.db,
539
+ context.workspaceId,
540
+ context.sessionId,
541
+ authorization?.relatedSessionAccess ?? "root",
542
+ ),
407
543
  };
408
544
  await publishSessionEventIds(deps, context.workspaceId, context.sessionId, result.eventIds);
409
545
  await publishWorkspaceControlEvent(deps, context.workspaceId, result.workspaceControlEventId);
@@ -415,10 +551,12 @@ export async function controlHumanSessionWorkstream(
415
551
  db: Database;
416
552
  bus: EventBus;
417
553
  workflowClient: Pick<SessionWorkflowClient, "requestSessionWorkflowWakeDispatch">;
554
+ sessionAuthorization?: SessionAuthorizationPort | null;
418
555
  },
419
556
  context: HumanSessionCommandContext,
420
557
  input: SessionControlRequest,
421
558
  ): Promise<SessionControlResponse> {
559
+ const authorization = await authorizeHumanSessionCommand(deps, context, "session.control");
422
560
  const result = await withWorkspaceRls(deps.db, context.workspaceId, (scoped) =>
423
561
  scoped.transaction((tx) =>
424
562
  mutateSessionControlInTransaction(tx as unknown as Database, {
@@ -435,7 +573,11 @@ export async function controlHumanSessionWorkstream(
435
573
  );
436
574
  const response = {
437
575
  receipt: receipt(result.receipt),
438
- effectiveControl: serializeEffectiveSessionControl(result.control),
576
+ effectiveControl: projectEffectiveControlForRelatedAccess(
577
+ serializeEffectiveSessionControl(result.control),
578
+ context.sessionId,
579
+ authorization?.relatedSessionAccess ?? "root",
580
+ ),
439
581
  interruptionCount: result.interruptionCount,
440
582
  wakeCount: result.wakeCount,
441
583
  };
@@ -482,19 +624,24 @@ export async function controlHumanWorkspace(
482
624
  }
483
625
 
484
626
  export async function getHumanComposerDraft(
485
- db: Database,
627
+ deps: SessionAuthorizationCommandDeps,
486
628
  context: HumanSessionCommandContext,
487
629
  ): Promise<ComposerDraft> {
488
- const row = await withWorkspaceSubjectRls(db, context.workspaceId, context.subjectId, (scoped) =>
489
- getComposerDraftInTransaction(scoped, {
490
- workspaceId: context.workspaceId,
491
- sessionId: context.sessionId,
492
- subjectId: context.subjectId,
493
- }),
630
+ await authorizeHumanSessionCommand(deps, context, "session.composer.read");
631
+ const row = await withWorkspaceSubjectRls(
632
+ deps.db,
633
+ context.workspaceId,
634
+ context.subjectId,
635
+ (scoped) =>
636
+ getComposerDraftInTransaction(scoped, {
637
+ workspaceId: context.workspaceId,
638
+ sessionId: context.sessionId,
639
+ subjectId: context.subjectId,
640
+ }),
494
641
  );
495
642
  const mapped = composerDraft(row);
496
643
  if (mapped) return mapped;
497
- const session = await getSession(db, context.workspaceId, context.sessionId);
644
+ const session = await getSession(deps.db, context.workspaceId, context.sessionId);
498
645
  if (!session) throw new Error(`Session not found: ${context.sessionId}`);
499
646
  return {
500
647
  revision: 0,
@@ -510,18 +657,23 @@ export async function getHumanComposerDraft(
510
657
  }
511
658
 
512
659
  export async function saveHumanComposerDraft(
513
- db: Database,
660
+ deps: SessionAuthorizationCommandDeps,
514
661
  context: HumanSessionCommandContext,
515
662
  input: SaveComposerDraftRequest,
516
663
  ): Promise<ComposerDraft> {
517
- const row = await withWorkspaceSubjectRls(db, context.workspaceId, context.subjectId, (scoped) =>
518
- scoped.transaction((tx) =>
519
- saveComposerDraftInTransaction(tx as unknown as Database, {
520
- ...context,
521
- ...input,
522
- subjectId: context.subjectId,
523
- }),
524
- ),
664
+ await authorizeHumanSessionCommand(deps, context, "session.composer.write");
665
+ const row = await withWorkspaceSubjectRls(
666
+ deps.db,
667
+ context.workspaceId,
668
+ context.subjectId,
669
+ (scoped) =>
670
+ scoped.transaction((tx) =>
671
+ saveComposerDraftInTransaction(tx as unknown as Database, {
672
+ ...context,
673
+ ...input,
674
+ subjectId: context.subjectId,
675
+ }),
676
+ ),
525
677
  );
526
678
  return composerDraft(row)!;
527
679
  }
@@ -1,5 +1,11 @@
1
1
  import { configuredStaticUsageLimits } from "@opengeni/config";
2
- import type { LimitAction, LimitDecision } from "@opengeni/contracts";
2
+ import type {
3
+ LimitAction,
4
+ LimitDecision,
5
+ SessionTurnSource,
6
+ TurnInitiator,
7
+ TurnInitiatorContext,
8
+ } from "@opengeni/contracts";
3
9
  import {
4
10
  countActiveApiKeysForWorkspace,
5
11
  countScheduledTasksForWorkspace,
@@ -213,6 +219,12 @@ export async function recordWorkspaceUsage(
213
219
  unit: string;
214
220
  sourceResourceType: string;
215
221
  sourceResourceId: string;
222
+ sessionId?: string | null;
223
+ turnId?: string | null;
224
+ turnAttemptId?: string | null;
225
+ initiator?: TurnInitiator | null;
226
+ initiatorContext?: TurnInitiatorContext;
227
+ origin?: SessionTurnSource | null;
216
228
  idempotencyKey: string;
217
229
  },
218
230
  ): Promise<void> {
@@ -225,6 +237,13 @@ export async function recordWorkspaceUsage(
225
237
  unit: input.unit,
226
238
  sourceResourceType: input.sourceResourceType,
227
239
  sourceResourceId: input.sourceResourceId,
240
+ sessionId: input.sessionId ?? null,
241
+ turnId: input.turnId ?? null,
242
+ turnAttemptId: input.turnAttemptId ?? null,
243
+ initiator:
244
+ input.initiator ?? (input.subjectId ? { kind: "subject", subjectId: input.subjectId } : null),
245
+ ...(input.initiatorContext ? { initiatorContext: input.initiatorContext } : {}),
246
+ origin: input.origin ?? null,
228
247
  idempotencyKey: input.idempotencyKey,
229
248
  });
230
249
  }
@@ -1,5 +1,11 @@
1
1
  import type { Settings } from "@opengeni/config";
2
- import type { Document, ScheduledTask } from "@opengeni/contracts";
2
+ import type {
3
+ ConnectionCredentialsPort,
4
+ Document,
5
+ GitHubAppApiPort,
6
+ ScheduledTask,
7
+ SessionAuthorizationPort,
8
+ } from "@opengeni/contracts";
3
9
  import type { Database } from "@opengeni/db";
4
10
  import type { DocumentServices } from "@opengeni/documents";
5
11
  import type { EventBus } from "@opengeni/events";
@@ -79,6 +85,24 @@ export type AppDependencies = {
79
85
  observability?: Observability;
80
86
  readinessChecks?: Partial<Record<"db" | "nats" | "temporal", () => Promise<void> | void>>;
81
87
  githubStateSecret?: string;
88
+ /**
89
+ * Optional host-provided GitHub App API seam. Embedded hosts can authorize
90
+ * users, inspect installations, and list repositories with their own GitHub
91
+ * App credentials; standalone deployments fall back to @opengeni/github.
92
+ */
93
+ githubAppApi?: GitHubAppApiPort;
94
+ /**
95
+ * Optional host-owned connection credential seam. API-side consumers use
96
+ * the MCP leg for Toolspace/Code Mode; worker consumers bind the same port
97
+ * for model MCP, Git, and sandbox-secret resolution.
98
+ */
99
+ connectionCredentials?: ConnectionCredentialsPort | null;
100
+ /**
101
+ * Optional embedding-host session ACL. Unset preserves standalone workspace
102
+ * authorization; once bound, every session-addressed surface fails closed on
103
+ * an unavailable or invalid host decision.
104
+ */
105
+ sessionAuthorization?: SessionAuthorizationPort | null;
82
106
  managedAuth?: ManagedAuth | null;
83
107
  // The API process's OWN agent-loop-free sandbox client (constructed from
84
108
  // settings via @opengeni/runtime/sandbox). Undefined when sandboxBackend=none.
@@ -118,7 +142,7 @@ export type ApiRouteDeps = AppDependencies & {
118
142
  */
119
143
  export type AcceptSessionUserMessageDependencies = Pick<
120
144
  AppDependencies,
121
- "settings" | "db" | "bus"
145
+ "settings" | "db" | "bus" | "sessionAuthorization"
122
146
  > & {
123
147
  workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
124
148
  objectStorage: ObjectStorageDependency;