@opengeni/core 0.4.10 → 0.10.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.10.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.7",
38
+ "@opengeni/config": "^0.6.9",
39
+ "@opengeni/contracts": "^0.18.0",
40
+ "@opengeni/db": "^0.10.7",
41
+ "@opengeni/documents": "^0.2.28",
42
+ "@opengeni/events": "^0.3.19",
43
43
  "@opengeni/observability": "^0.3.0",
44
- "@opengeni/runtime": "^0.7.1",
45
- "@opengeni/storage": "^0.2.10",
44
+ "@opengeni/runtime": "^0.13.0",
45
+ "@opengeni/storage": "^0.2.22",
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, {
@@ -285,6 +385,7 @@ function composerDraft(
285
385
  text: row.text,
286
386
  resources: row.resources as ComposerDraft["resources"],
287
387
  tools: row.tools as ComposerDraft["tools"],
388
+ toolsProvided: row.toolsProvided,
288
389
  model: row.model,
289
390
  reasoningEffort: row.reasoningEffort as ComposerDraft["reasoningEffort"],
290
391
  sourceTurnId: row.sourceTurnId,
@@ -293,18 +394,31 @@ function composerDraft(
293
394
  };
294
395
  }
295
396
 
296
- async function authoritativeQueue(db: Database, workspaceId: string, sessionId: string) {
397
+ async function authoritativeQueue(
398
+ db: Database,
399
+ workspaceId: string,
400
+ sessionId: string,
401
+ relatedSessionAccess: "target" | "root",
402
+ ) {
297
403
  const snapshot = await getSessionQueueSnapshot(db, workspaceId, sessionId);
298
404
  if (!snapshot) throw new Error(`Session not found: ${sessionId}`);
299
- return snapshot;
405
+ return {
406
+ ...snapshot,
407
+ effectiveControl: projectEffectiveControlForRelatedAccess(
408
+ snapshot.effectiveControl,
409
+ sessionId,
410
+ relatedSessionAccess,
411
+ ),
412
+ };
300
413
  }
301
414
 
302
415
  export async function moveHumanQueuePrompt(
303
- deps: { db: Database; bus: EventBus },
416
+ deps: { db: Database; bus: EventBus; sessionAuthorization?: SessionAuthorizationPort | null },
304
417
  context: HumanSessionCommandContext,
305
418
  turnId: string,
306
419
  input: MoveSessionQueueItemRequest,
307
420
  ): Promise<SessionQueueMutationResponse> {
421
+ const authorization = await authorizeHumanSessionCommand(deps, context, "session.queue.control");
308
422
  const result = await withWorkspaceRls(deps.db, context.workspaceId, (scoped) =>
309
423
  scoped.transaction((tx) =>
310
424
  moveQueuedTurnInTransaction(tx as unknown as Database, {
@@ -319,18 +433,24 @@ export async function moveHumanQueuePrompt(
319
433
  );
320
434
  const response = {
321
435
  receipt: receipt(result.receipt),
322
- snapshot: await authoritativeQueue(deps.db, context.workspaceId, context.sessionId),
436
+ snapshot: await authoritativeQueue(
437
+ deps.db,
438
+ context.workspaceId,
439
+ context.sessionId,
440
+ authorization?.relatedSessionAccess ?? "root",
441
+ ),
323
442
  };
324
443
  await publishSessionEventIds(deps, context.workspaceId, context.sessionId, result.eventIds);
325
444
  return response;
326
445
  }
327
446
 
328
447
  export async function deleteHumanQueuePrompt(
329
- deps: { db: Database; bus: EventBus },
448
+ deps: { db: Database; bus: EventBus; sessionAuthorization?: SessionAuthorizationPort | null },
330
449
  context: HumanSessionCommandContext,
331
450
  turnId: string,
332
451
  input: DeleteSessionQueueItemRequest,
333
452
  ): Promise<SessionQueueMutationResponse> {
453
+ const authorization = await authorizeHumanSessionCommand(deps, context, "session.queue.control");
334
454
  const result = await withWorkspaceRls(deps.db, context.workspaceId, (scoped) =>
335
455
  scoped.transaction((tx) =>
336
456
  deleteSessionQueueItemInTransaction(tx as unknown as Database, {
@@ -345,18 +465,24 @@ export async function deleteHumanQueuePrompt(
345
465
  );
346
466
  const response = {
347
467
  receipt: receipt(result.receipt),
348
- snapshot: await authoritativeQueue(deps.db, context.workspaceId, context.sessionId),
468
+ snapshot: await authoritativeQueue(
469
+ deps.db,
470
+ context.workspaceId,
471
+ context.sessionId,
472
+ authorization?.relatedSessionAccess ?? "root",
473
+ ),
349
474
  };
350
475
  await publishSessionEventIds(deps, context.workspaceId, context.sessionId, result.eventIds);
351
476
  return response;
352
477
  }
353
478
 
354
479
  export async function editHumanQueuePrompt(
355
- deps: { db: Database; bus: EventBus },
480
+ deps: { db: Database; bus: EventBus; sessionAuthorization?: SessionAuthorizationPort | null },
356
481
  context: HumanSessionCommandContext,
357
482
  turnId: string,
358
483
  input: EditSessionQueueItemRequest,
359
484
  ): Promise<SessionQueueMutationResponse> {
485
+ const authorization = await authorizeHumanSessionCommand(deps, context, "session.queue.control");
360
486
  const result = await withWorkspaceSubjectRls(
361
487
  deps.db,
362
488
  context.workspaceId,
@@ -376,7 +502,12 @@ export async function editHumanQueuePrompt(
376
502
  );
377
503
  const response = {
378
504
  receipt: receipt(result.receipt),
379
- snapshot: await authoritativeQueue(deps.db, context.workspaceId, context.sessionId),
505
+ snapshot: await authoritativeQueue(
506
+ deps.db,
507
+ context.workspaceId,
508
+ context.sessionId,
509
+ authorization?.relatedSessionAccess ?? "root",
510
+ ),
380
511
  draft: composerDraft(result.draft)!,
381
512
  };
382
513
  await publishSessionEventIds(deps, context.workspaceId, context.sessionId, result.eventIds);
@@ -384,11 +515,12 @@ export async function editHumanQueuePrompt(
384
515
  }
385
516
 
386
517
  export async function steerHumanQueuePrompt(
387
- deps: { db: Database; bus: EventBus },
518
+ deps: { db: Database; bus: EventBus; sessionAuthorization?: SessionAuthorizationPort | null },
388
519
  context: HumanSessionCommandContext,
389
520
  turnId: string,
390
521
  input: SteerSessionQueueItemRequest,
391
522
  ): Promise<SessionQueueMutationResponse> {
523
+ const authorization = await authorizeHumanSessionCommand(deps, context, "session.queue.control");
392
524
  const result = await withWorkspaceRls(deps.db, context.workspaceId, (scoped) =>
393
525
  scoped.transaction((tx) =>
394
526
  steerQueuedTurnInTransaction(tx as unknown as Database, {
@@ -403,7 +535,12 @@ export async function steerHumanQueuePrompt(
403
535
  );
404
536
  const response = {
405
537
  receipt: receipt(result.receipt),
406
- snapshot: await authoritativeQueue(deps.db, context.workspaceId, context.sessionId),
538
+ snapshot: await authoritativeQueue(
539
+ deps.db,
540
+ context.workspaceId,
541
+ context.sessionId,
542
+ authorization?.relatedSessionAccess ?? "root",
543
+ ),
407
544
  };
408
545
  await publishSessionEventIds(deps, context.workspaceId, context.sessionId, result.eventIds);
409
546
  await publishWorkspaceControlEvent(deps, context.workspaceId, result.workspaceControlEventId);
@@ -415,10 +552,12 @@ export async function controlHumanSessionWorkstream(
415
552
  db: Database;
416
553
  bus: EventBus;
417
554
  workflowClient: Pick<SessionWorkflowClient, "requestSessionWorkflowWakeDispatch">;
555
+ sessionAuthorization?: SessionAuthorizationPort | null;
418
556
  },
419
557
  context: HumanSessionCommandContext,
420
558
  input: SessionControlRequest,
421
559
  ): Promise<SessionControlResponse> {
560
+ const authorization = await authorizeHumanSessionCommand(deps, context, "session.control");
422
561
  const result = await withWorkspaceRls(deps.db, context.workspaceId, (scoped) =>
423
562
  scoped.transaction((tx) =>
424
563
  mutateSessionControlInTransaction(tx as unknown as Database, {
@@ -435,7 +574,11 @@ export async function controlHumanSessionWorkstream(
435
574
  );
436
575
  const response = {
437
576
  receipt: receipt(result.receipt),
438
- effectiveControl: serializeEffectiveSessionControl(result.control),
577
+ effectiveControl: projectEffectiveControlForRelatedAccess(
578
+ serializeEffectiveSessionControl(result.control),
579
+ context.sessionId,
580
+ authorization?.relatedSessionAccess ?? "root",
581
+ ),
439
582
  interruptionCount: result.interruptionCount,
440
583
  wakeCount: result.wakeCount,
441
584
  };
@@ -482,25 +625,31 @@ export async function controlHumanWorkspace(
482
625
  }
483
626
 
484
627
  export async function getHumanComposerDraft(
485
- db: Database,
628
+ deps: SessionAuthorizationCommandDeps,
486
629
  context: HumanSessionCommandContext,
487
630
  ): 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
- }),
631
+ await authorizeHumanSessionCommand(deps, context, "session.composer.read");
632
+ const row = await withWorkspaceSubjectRls(
633
+ deps.db,
634
+ context.workspaceId,
635
+ context.subjectId,
636
+ (scoped) =>
637
+ getComposerDraftInTransaction(scoped, {
638
+ workspaceId: context.workspaceId,
639
+ sessionId: context.sessionId,
640
+ subjectId: context.subjectId,
641
+ }),
494
642
  );
495
643
  const mapped = composerDraft(row);
496
644
  if (mapped) return mapped;
497
- const session = await getSession(db, context.workspaceId, context.sessionId);
645
+ const session = await getSession(deps.db, context.workspaceId, context.sessionId);
498
646
  if (!session) throw new Error(`Session not found: ${context.sessionId}`);
499
647
  return {
500
648
  revision: 0,
501
649
  text: "",
502
650
  resources: [],
503
651
  tools: [],
652
+ toolsProvided: false,
504
653
  model: session.model,
505
654
  reasoningEffort: reasoningEffortForMetadata(session.metadata, "medium"),
506
655
  sourceTurnId: null,
@@ -510,18 +659,23 @@ export async function getHumanComposerDraft(
510
659
  }
511
660
 
512
661
  export async function saveHumanComposerDraft(
513
- db: Database,
662
+ deps: SessionAuthorizationCommandDeps,
514
663
  context: HumanSessionCommandContext,
515
664
  input: SaveComposerDraftRequest,
516
665
  ): 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
- ),
666
+ await authorizeHumanSessionCommand(deps, context, "session.composer.write");
667
+ const row = await withWorkspaceSubjectRls(
668
+ deps.db,
669
+ context.workspaceId,
670
+ context.subjectId,
671
+ (scoped) =>
672
+ scoped.transaction((tx) =>
673
+ saveComposerDraftInTransaction(tx as unknown as Database, {
674
+ ...context,
675
+ ...input,
676
+ subjectId: context.subjectId,
677
+ }),
678
+ ),
525
679
  );
526
680
  return composerDraft(row)!;
527
681
  }
@@ -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,7 +85,27 @@ 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;
107
+ /** Injectable Codex HTTP transport for deterministic API/provider tests. */
108
+ codexFetch?: typeof fetch;
83
109
  // The API process's OWN agent-loop-free sandbox client (constructed from
84
110
  // settings via @opengeni/runtime/sandbox). Undefined when sandboxBackend=none.
85
111
  // This is the foundation of the API-direct control plane: the API resumes
@@ -118,7 +144,7 @@ export type ApiRouteDeps = AppDependencies & {
118
144
  */
119
145
  export type AcceptSessionUserMessageDependencies = Pick<
120
146
  AppDependencies,
121
- "settings" | "db" | "bus"
147
+ "settings" | "db" | "bus" | "sessionAuthorization"
122
148
  > & {
123
149
  workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
124
150
  objectStorage: ObjectStorageDependency;