@opengeni/worker-bundle 0.27.3-canary.0 → 0.28.1-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -781,7 +781,7 @@ function temporalWorkflowExecutionNotFound(error) {
781
781
  return isGrpcServiceError(error) && error.code === 5;
782
782
  }
783
783
  async function createDefaultWorkerActivities(role, dependencies = {}) {
784
- return role === "control" ? (await import("./activities-control-WZRFEIJH.js")).createControlActivities(dependencies) : (await import("./activities-turn-V7J34ZOF.js")).createTurnActivities(dependencies);
784
+ return role === "control" ? (await import("./activities-control-VVK7RA7F.js")).createControlActivities(dependencies) : (await import("./activities-turn-FOIZYY56.js")).createTurnActivities(dependencies);
785
785
  }
786
786
  function resolveOpenGeniWorkflowDefinition(moduleUrl = import.meta.url) {
787
787
  const modulePath = fileURLToPath(moduleUrl).replaceAll("\\", "/");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/worker-bundle",
3
- "version": "0.27.3-canary.0",
3
+ "version": "0.28.1-canary.0",
4
4
  "description": "OpenGeni worker entry and reusable embedded lifecycle, shipped with a release-coherent pre-bundled Temporal workflow artifact.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -54,22 +54,22 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@llamaindex/liteparse": "2.14.2",
57
- "@opengeni/agent-proto": "^0.5.1-canary.23",
58
- "@opengeni/capabilities": "^0.3.3-canary.5",
59
- "@opengeni/codemode": "^0.5.3-canary.0",
60
- "@opengeni/codex": "^0.2.22-canary.5",
61
- "@opengeni/config": "^1.0.4-canary.0",
62
- "@opengeni/contracts": "^2.15.2-canary.0",
63
- "@opengeni/core": "^2.8.3-canary.0",
64
- "@opengeni/db": "^4.2.2-canary.0",
65
- "@opengeni/documents": "^0.8.23-canary.0",
66
- "@opengeni/events": "^0.4.21-canary.0",
67
- "@opengeni/github": "^0.7.7-canary.0",
68
- "@opengeni/network": "^0.3.1-canary.5",
69
- "@opengeni/observability": "^0.8.23-canary.0",
70
- "@opengeni/runtime": "^2.4.3-canary.0",
71
- "@opengeni/storage": "^0.2.124-canary.0",
72
- "@opengeni/xai-subscription": "^0.1.4-canary.5",
57
+ "@opengeni/agent-proto": "^0.5.1-canary.24",
58
+ "@opengeni/capabilities": "^0.3.3-canary.6",
59
+ "@opengeni/codemode": "^0.5.4-canary.0",
60
+ "@opengeni/codex": "^0.2.22-canary.6",
61
+ "@opengeni/config": "^1.1.0-canary.0",
62
+ "@opengeni/contracts": "^3.0.0-canary.0",
63
+ "@opengeni/core": "^2.9.1-canary.0",
64
+ "@opengeni/db": "^4.3.0-canary.0",
65
+ "@opengeni/documents": "^0.8.24-canary.0",
66
+ "@opengeni/events": "^0.4.22-canary.0",
67
+ "@opengeni/github": "^0.7.8-canary.0",
68
+ "@opengeni/network": "^0.3.1-canary.6",
69
+ "@opengeni/observability": "^0.8.24-canary.0",
70
+ "@opengeni/runtime": "^2.5.1-canary.0",
71
+ "@opengeni/storage": "^0.2.125-canary.0",
72
+ "@opengeni/xai-subscription": "^0.1.4-canary.6",
73
73
  "@temporalio/activity": "^1.17.0",
74
74
  "@temporalio/client": "^1.17.0",
75
75
  "@temporalio/worker": "^1.17.0",
@@ -6,6 +6,8 @@ import {
6
6
  setXaiSessionAccountPin,
7
7
  getWorkspaceVideoGenerationPolicy,
8
8
  loadWorkspaceVercelAiGatewayCredentialLease,
9
+ getExternalLinkTurnAuthorization,
10
+ getSessionTurnForAttempt,
9
11
  } from "@opengeni/db";
10
12
  import {
11
13
  formatSkillCatalog,
@@ -569,11 +571,39 @@ export async function buildTurnAgent(deps: BuildTurnAgentDeps) {
569
571
  outcome: "completed",
570
572
  durationSeconds: (performance.now() - postToolPreparationStartedAt) / 1_000,
571
573
  });
574
+ const linkedToolAuthority = await getExternalLinkTurnAuthorization(
575
+ db,
576
+ {
577
+ accountId: input.accountId,
578
+ workspaceId: input.workspaceId,
579
+ },
580
+ turn.id,
581
+ );
582
+ if (linkedToolAuthority && !linkedToolAuthority.authorized)
583
+ throw new Error("Native identity link was revoked");
572
584
  const agent = (() => {
573
585
  const agentConstructionStartedAt = performance.now();
574
586
  let agentConstructionOutcome: "completed" | "failed" = "completed";
575
587
  try {
576
588
  return runtime.buildAgent(eventing.modelRunSettings, runtimeResources, {
589
+ ...(linkedToolAuthority
590
+ ? {
591
+ authorizeAttemptExecution: async () => {
592
+ const current = await getSessionTurnForAttempt(
593
+ db,
594
+ input.workspaceId,
595
+ input.sessionId,
596
+ input.attemptId,
597
+ );
598
+ if (
599
+ !current ||
600
+ current.id !== turn.id ||
601
+ current.executionGeneration !== turn.executionGeneration
602
+ )
603
+ throw new Error("The linked agent attempt is no longer authorized");
604
+ },
605
+ }
606
+ : {}),
577
607
  ...(preparedTools.inputWaitYield ? { inputWaitYield: preparedTools.inputWaitYield } : {}),
578
608
  reasoningEffort: turn.reasoningEffort,
579
609
  latencyMode: turnExecutionPolicy.latencyMode,
@@ -677,7 +707,6 @@ export async function buildTurnAgent(deps: BuildTurnAgentDeps) {
677
707
  }),
678
708
  onRetainableSessionImageOutput: media.retainSessionImageAtToolBoundary,
679
709
  skillCatalog: deps.skillCatalog,
680
- serverSkillReading: true,
681
710
  ...(!structuredWorkspacePolicyActive && workspaceAgentInstructions
682
711
  ? { instructionsTemplate: workspaceAgentInstructions }
683
712
  : {}),
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  commitSessionAttemptQuiescence,
3
+ listPendingSessionTurns,
3
4
  recordCodexAccountUsageWithWakeTargets,
4
5
  releaseCodexCredentialLease,
5
6
  releaseXaiCredentialLease,
@@ -20,7 +21,11 @@ import type {
20
21
  SessionAttemptQuiescenceProof,
21
22
  TurnActivityServices as ActivityServices,
22
23
  } from "../types";
23
- import { captureWorkspaceRevision, openFreshWorkspaceCaptureSession } from "../workspace-capture";
24
+ import {
25
+ captureWhileIdle,
26
+ captureWorkspaceRevision,
27
+ openFreshWorkspaceCaptureSession,
28
+ } from "../workspace-capture";
24
29
  import { type ChannelASession } from "@opengeni/runtime/sandbox";
25
30
  import { createModelCheckpointMemoryCollector } from "../../model-checkpoint-memory-collector";
26
31
  import {
@@ -488,49 +493,60 @@ export async function finalizeTurnAttempt(deps: TurnFinalizationDeps): Promise<v
488
493
  deadlineRotationRequested: sandboxState.deadlineRotationRequested,
489
494
  });
490
495
  if (
496
+ settings.workspaceCaptureEnabled &&
491
497
  runTurnEndPersistence &&
492
498
  attempt.turnId &&
493
499
  sandboxState.resolvedSandbox &&
494
500
  sandboxState.setupBoxSession &&
495
501
  sandboxState.sandboxGroupId
496
502
  ) {
497
- // Block new periodic snapshots, then drain any one already in flight.
498
- // Keep the lease heartbeat itself running: capture may legitimately
499
- // exceed the 90s holder TTL, and the reaper must remain unable to drain
500
- // the exact box while this holder still reads it.
503
+ // Review reads yield to queued work. Recovery snapshots have their own
504
+ // physical-settlement requirement and are drained later in finalization.
501
505
  sandboxState.turnEndCaptureInProgress = true;
502
- if (sandboxState.snapshotInFlight) {
503
- await waitForWarmSnapshot(
504
- sandboxState.snapshotInFlight,
505
- settings.sandboxSnapshotTimeoutMs,
506
- finalizerSignal,
507
- );
508
- }
509
- const captureEstablished = sandboxState.resolvedSandbox.established;
510
- await captureWorkspaceRevision({
511
- db,
512
- objectStorage,
513
- settings,
514
- publish: async (events) => {
515
- await publishDurableSessionEvents(bus, input.workspaceId, input.sessionId, events);
516
- },
517
- session: sandboxState.setupBoxSession as ChannelASession,
518
- openReadSession: async () =>
519
- await openFreshWorkspaceCaptureSession({
520
- backendId: captureEstablished.backendId,
521
- client: captureEstablished.client,
522
- session: sandboxState.setupBoxSession as ChannelASession,
523
- expectedInstanceId: captureEstablished.instanceId,
524
- }),
525
- leaseEpoch: sandboxState.resolvedSandbox.leaseEpoch,
526
- sandboxGroupId: sandboxState.sandboxGroupId,
527
- accountId: input.accountId,
528
- workspaceId: input.workspaceId,
529
- sessionId: input.sessionId,
530
- turnId: attempt.turnId,
531
- attemptId: input.attemptId,
532
- observability,
506
+ const captureSandbox = sandboxState.resolvedSandbox;
507
+ const captureSession = sandboxState.setupBoxSession as ChannelASession;
508
+ const captureGroupId = sandboxState.sandboxGroupId;
509
+ const captureTurnId = attempt.turnId;
510
+ await captureWhileIdle({
533
511
  ...(finalizerSignal ? { signal: finalizerSignal } : {}),
512
+ hasPendingWork: async () =>
513
+ (await listPendingSessionTurns(db, input.workspaceId, input.sessionId)).some(
514
+ (turn) => turn.id !== captureTurnId,
515
+ ),
516
+ capture: async (signal) => {
517
+ if (sandboxState.snapshotInFlight) {
518
+ await waitForWarmSnapshot(
519
+ sandboxState.snapshotInFlight,
520
+ settings.sandboxSnapshotTimeoutMs,
521
+ signal,
522
+ );
523
+ }
524
+ if (signal.aborted) return;
525
+ await captureWorkspaceRevision({
526
+ db,
527
+ objectStorage,
528
+ settings,
529
+ publish: async (events) =>
530
+ publishDurableSessionEvents(bus, input.workspaceId, input.sessionId, events),
531
+ session: captureSession,
532
+ openReadSession: async () =>
533
+ openFreshWorkspaceCaptureSession({
534
+ backendId: captureSandbox.established.backendId,
535
+ client: captureSandbox.established.client,
536
+ session: captureSession,
537
+ expectedInstanceId: captureSandbox.established.instanceId,
538
+ }),
539
+ leaseEpoch: captureSandbox.leaseEpoch,
540
+ sandboxGroupId: captureGroupId,
541
+ accountId: input.accountId,
542
+ workspaceId: input.workspaceId,
543
+ sessionId: input.sessionId,
544
+ turnId: captureTurnId,
545
+ attemptId: input.attemptId,
546
+ observability,
547
+ signal,
548
+ });
549
+ },
534
550
  });
535
551
  }
536
552
  eventing.toolPreparationClosing = true;
@@ -1,9 +1,8 @@
1
1
  import type { AttemptToolDefinition } from "@opengeni/codemode";
2
2
  import {
3
- assertSkillRelativePath,
3
+ listSkillPaths,
4
4
  PORTABLE_SKILL_MAX_FILES,
5
5
  readSkillFiles,
6
- SKILL_READ_MAX_OUTPUT_BYTES,
7
6
  SKILL_READ_MAX_PATHS,
8
7
  type SkillTextFile,
9
8
  } from "@opengeni/runtime/skill-library";
@@ -74,7 +73,7 @@ export function createSkillReadAttemptToolDefinition(input: {
74
73
  const files = metadata ? metadata.files : (loaded as readonly SkillTextFile[]);
75
74
  const selected =
76
75
  args.listFiles === true
77
- ? listSkillPaths(files)
76
+ ? listSkillPaths(files, PORTABLE_SKILL_MAX_FILES)
78
77
  : readSkillFiles(files, args.paths as string[] | undefined);
79
78
  const output = {
80
79
  ...(metadata
@@ -97,21 +96,3 @@ export function createSkillReadAttemptToolDefinition(input: {
97
96
  },
98
97
  };
99
98
  }
100
-
101
- /** Inventory never reads or serializes file bodies, including the entry point. */
102
- function listSkillPaths(files: readonly SkillTextFile[]): { paths: string[] } {
103
- if (files.length > PORTABLE_SKILL_MAX_FILES) {
104
- throw new Error(`Skill inventory exceeds ${PORTABLE_SKILL_MAX_FILES} files.`);
105
- }
106
- const seen = new Set<string>();
107
- for (const { path } of files) {
108
- assertSkillRelativePath(path);
109
- if (seen.has(path)) throw new Error(`Duplicate stored Skill path: ${path}`);
110
- seen.add(path);
111
- }
112
- const result = { paths: [...seen].sort() };
113
- if (Buffer.byteLength(JSON.stringify(result), "utf8") > SKILL_READ_MAX_OUTPUT_BYTES) {
114
- throw new Error("Skill inventory exceeds the read output limit.");
115
- }
116
- return result;
117
- }
@@ -1,6 +1,8 @@
1
1
  import { getWorkspaceConnectionModelRestrictions } from "@opengeni/db";
2
2
  import {
3
3
  beginConnectorActionExecution,
4
+ getExternalLinkTurnAuthorization,
5
+ getSessionTurnForAttempt,
4
6
  getWorkspaceVideoGenerationPolicy,
5
7
  listSkillDescriptors,
6
8
  completeConnectorActionExecution,
@@ -51,6 +53,7 @@ import {
51
53
  resolveWorkspaceModelSelection,
52
54
  withFrozenPersonalConnectionDelegations,
53
55
  resolveSessionToolPolicy,
56
+ hasPermission,
54
57
  } from "@opengeni/core";
55
58
  import { loadWorkspaceEnvironmentForRunWithCredentials } from "../environment";
56
59
  import { withFirstPartyTools } from "../goals";
@@ -60,7 +63,10 @@ import { ToolResultSpill } from "./tool-result-spill";
60
63
  import { createTurnMediaArtifacts } from "./media-artifacts";
61
64
  import { SandboxChannelAService } from "@opengeni/runtime/sandbox";
62
65
  import { sandboxRunAs } from "@opengeni/runtime";
63
- import { type ToolAuthNeededPayload } from "@opengeni/contracts";
66
+ import {
67
+ DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
68
+ type ToolAuthNeededPayload,
69
+ } from "@opengeni/contracts";
64
70
 
65
71
  import {
66
72
  rollingSafeToolAuthNeededPayload,
@@ -508,6 +514,21 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
508
514
  };
509
515
  })()
510
516
  : undefined;
517
+ const linkedAuthority = await getExternalLinkTurnAuthorization(
518
+ db,
519
+ {
520
+ accountId: input.accountId,
521
+ workspaceId: input.workspaceId,
522
+ },
523
+ turn.id,
524
+ );
525
+ if (linkedAuthority && !linkedAuthority.authorized)
526
+ throw new Error("Native identity link was revoked");
527
+ const effectiveFirstPartyPermissions = linkedAuthority
528
+ ? (session.firstPartyMcpPermissions ?? DEFAULT_FIRST_PARTY_MCP_PERMISSIONS).filter(
529
+ (permission) => hasPermission(linkedAuthority.permissions, permission),
530
+ )
531
+ : session.firstPartyMcpPermissions;
511
532
  const selectedFirstPartyMcpTools = allowedFirstPartyMcpToolsForSession(
512
533
  runSettings,
513
534
  session.firstPartyMcpTools,
@@ -519,16 +540,21 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
519
540
  title: session.title,
520
541
  titleSource: session.titleSource,
521
542
  firstPartyMcpTools: selectedFirstPartyMcpTools,
522
- firstPartyMcpPermissions: session.firstPartyMcpPermissions,
543
+ firstPartyMcpPermissions: effectiveFirstPartyPermissions,
523
544
  }),
524
545
  parallelGenerationAvailable: typeof runtime.generateSessionTitle === "function",
525
546
  });
526
547
  const googleDrivePublicationAllowed =
527
548
  selectedFirstPartyMcpTools.includes("editable_artifact_export") &&
528
549
  selectedFirstPartyMcpTools.includes("editable_artifact_export_status") &&
529
- (!session.firstPartyMcpPermissions?.length ||
530
- (session.firstPartyMcpPermissions.includes("artifacts:read") &&
531
- session.firstPartyMcpPermissions.includes("artifacts:publish")));
550
+ hasPermission(
551
+ [...(effectiveFirstPartyPermissions ?? DEFAULT_FIRST_PARTY_MCP_PERMISSIONS)],
552
+ "artifacts:read",
553
+ ) &&
554
+ hasPermission(
555
+ [...(effectiveFirstPartyPermissions ?? DEFAULT_FIRST_PARTY_MCP_PERMISSIONS)],
556
+ "artifacts:publish",
557
+ );
532
558
  const googleDriveConnectorBindings: readonly AttemptConnectorActionBinding[] =
533
559
  googleDrivePublicationTool && googleDrivePublicationTarget && googleDrivePublicationAllowed
534
560
  ? [
@@ -760,9 +786,7 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
760
786
  attemptId: input.attemptId,
761
787
  executionGeneration: attempt.executionGeneration,
762
788
  },
763
- ...(session.firstPartyMcpPermissions?.length
764
- ? { permissions: session.firstPartyMcpPermissions }
765
- : {}),
789
+ ...(effectiveFirstPartyPermissions ? { permissions: effectiveFirstPartyPermissions } : {}),
766
790
  selectedTools: selectedFirstPartyMcpTools,
767
791
  subjectId: "worker:first-party-mcp",
768
792
  subjectLabel: "OpenGeni worker",
@@ -852,6 +876,24 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
852
876
  ...(credentialSubjectId ? { credentialSubjectId } : {}),
853
877
  ...(codexAppsAuth ? { codexAppsAuth } : {}),
854
878
  resolveCredential,
879
+ ...(linkedAuthority
880
+ ? {
881
+ authorizeAttemptExecution: async () => {
882
+ const current = await getSessionTurnForAttempt(
883
+ db,
884
+ input.workspaceId,
885
+ input.sessionId,
886
+ input.attemptId,
887
+ );
888
+ if (
889
+ !current ||
890
+ current.id !== turn.id ||
891
+ current.executionGeneration !== attempt.executionGeneration
892
+ )
893
+ throw new Error("The linked agent attempt is no longer authorized");
894
+ },
895
+ }
896
+ : {}),
855
897
  onAuthNeeded: publishToolAuthNeeded,
856
898
  materializeConnectorAttachments,
857
899
  refreshOwnedCommand: async (commandId) => {
@@ -885,8 +927,8 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
885
927
  },
886
928
  // Manager-style sessions carry a creation-validated permission set
887
929
  // for their first-party MCP token; null keeps the fixed default.
888
- ...(session.firstPartyMcpPermissions?.length
889
- ? { firstPartyPermissions: session.firstPartyMcpPermissions }
930
+ ...(effectiveFirstPartyPermissions
931
+ ? { firstPartyPermissions: effectiveFirstPartyPermissions }
890
932
  : {}),
891
933
  firstPartyTools: titleToolPlan.remoteFirstPartyMcpTools,
892
934
  nestedAgentDepth: session.nestedAgentDepth,
@@ -3,6 +3,8 @@ import type { ConnectionCredentialsPort } from "@opengeni/contracts";
3
3
  import {
4
4
  buildConnectionTokenResolver,
5
5
  buildHostConnectionTokenResolver,
6
+ getSessionTurnForAttempt,
7
+ authorizeDirectHostMcpUse,
6
8
  resolveAcceptedConnectionUse,
7
9
  sessionTenancyProductActivated,
8
10
  type Database,
@@ -26,13 +28,18 @@ export function connectionTokenResolverForTurn(input: {
26
28
  attemptId: string;
27
29
  turn: SessionTurnForExecution;
28
30
  authorizeAcceptedUse?: typeof resolveAcceptedConnectionUse;
31
+ /** Test seam; production always reads the canonical active-attempt projection. */
32
+ getHostTurnForAttempt?: typeof getSessionTurnForAttempt;
29
33
  /** Test seam for the activation fence on pre-snapshot workspace refs. */
30
34
  isSessionTenancyProductActivated?: typeof sessionTenancyProductActivated;
31
35
  /** Optional; used only for content-free compatibility-lane counters. */
32
36
  observability?: Observability | null | undefined;
33
37
  }): (request: ResolveConnectionCredentialInput) => Promise<ResolveConnectionCredentialResult> {
34
38
  const hostResolver = input.connectionCredentials?.mcpCredentials;
35
- const baseResolver = hostResolver
39
+ const readHostTurn = input.getHostTurnForAttempt ?? getSessionTurnForAttempt;
40
+ const hostDb = input.db;
41
+ const nativeResolver = buildConnectionTokenResolver(input.db, input.settings);
42
+ const configuredResolver = hostResolver
36
43
  ? buildHostConnectionTokenResolver(hostResolver, {
37
44
  accountId: input.accountId,
38
45
  workspaceId: input.workspaceId,
@@ -44,8 +51,40 @@ export function connectionTokenResolverForTurn(input: {
44
51
  initiator: input.turn.initiator,
45
52
  initiatorContext: input.turn.initiatorContext,
46
53
  surface: "model",
54
+ // Only immutable captured direct-turn authority is currently supported.
55
+ // Direct, scheduled and exact causal/child snapshots are validated;
56
+ // no authority follows merely from a binding or creator identity.
57
+ authorizeDurableBinding: (request) => authorizeDirectHostMcpUse(hostDb, request),
58
+ authorizeExecution: async (request) => {
59
+ // Native UUID references already pass the accepted-use boundary below.
60
+ // Legacy opaque host IDs retain their compatibility path, but not a
61
+ // right to execute after their accepted attempt has stopped.
62
+ if (
63
+ request.connectionRef.authoritySource !== "host" &&
64
+ (!request.connectionRef.connectionId ||
65
+ OPENGENI_CONNECTION_ID_PATTERN.test(request.connectionRef.connectionId))
66
+ )
67
+ return true;
68
+ if (!request.attemptId) return false;
69
+ const current = await readHostTurn(
70
+ hostDb,
71
+ request.workspaceId,
72
+ request.sessionId,
73
+ request.attemptId,
74
+ );
75
+ return (
76
+ current !== null &&
77
+ current.id === request.turnId &&
78
+ current.executionGeneration === request.executionGeneration
79
+ );
80
+ },
47
81
  })
48
- : buildConnectionTokenResolver(input.db, input.settings);
82
+ : nativeResolver;
83
+ const baseResolver = (request: ResolveConnectionCredentialInput) =>
84
+ input.connectionCredentials?.mcpAuthoritySource === "host" &&
85
+ request.connectionRef.authoritySource !== "host"
86
+ ? nativeResolver(request)
87
+ : configuredResolver(request);
49
88
  return async (request) => {
50
89
  // Host-owned refs carry explicit provenance because their opaque ids may
51
90
  // themselves be valid UUIDs. They never enter OpenGeni's native connection
@@ -76,6 +115,7 @@ export function connectionTokenResolverForTurn(input: {
76
115
  // so UUID-shaped host ids are unambiguous.
77
116
  if (
78
117
  hostResolver &&
118
+ input.connectionCredentials?.mcpAuthoritySource !== "host" &&
79
119
  request.connectionRef.connectionId &&
80
120
  !OPENGENI_CONNECTION_ID_PATTERN.test(request.connectionRef.connectionId)
81
121
  ) {
@@ -8,7 +8,6 @@ import {
8
8
  SCHEDULED_TASK_OCCURRENCE_PAYLOAD_MAX_BYTES,
9
9
  ScheduledTaskRunAcceptedExecution,
10
10
  SessionAgentAccess,
11
- SessionEndUser,
12
11
  SessionMemoryScope,
13
12
  normalizeAutomaticSessionTitle,
14
13
  scheduledOccurrencePayloadUtf8Bytes,
@@ -2578,26 +2577,29 @@ async function replayScheduledTaskDispatch(input: {
2578
2577
  * facts the creating session projection exposed at task creation are set;
2579
2578
  * a null (or no longer valid) key leaves the generated session on its own
2580
2579
  * default. The field names are the session access-scope contract
2581
- * (`agentAccess`, `endUser`, `memoryScope`) consumed by session create.
2580
+ * (`agentAccess`, `scopeSubjectId`, `memoryScope`) consumed by session create.
2582
2581
  */
2583
2582
  function scheduledCreatorSessionPolicyInput(
2584
2583
  policy: {
2585
2584
  agentAccess: string | null;
2586
- endUser: { source: string; id: string } | null;
2585
+ scopeSubjectId: string | null;
2587
2586
  memoryScope: string | null;
2588
2587
  } | null,
2589
2588
  ): {
2590
2589
  agentAccess?: SessionAgentAccess;
2591
- endUser?: SessionEndUser;
2590
+ scopeSubjectId?: string;
2592
2591
  memoryScope?: SessionMemoryScope;
2593
2592
  } {
2594
2593
  if (!policy) return {};
2595
2594
  const agentAccess = SessionAgentAccess.safeParse(policy.agentAccess);
2596
- const endUser = SessionEndUser.safeParse(policy.endUser);
2597
2595
  const memoryScope = SessionMemoryScope.safeParse(policy.memoryScope);
2598
2596
  return {
2599
2597
  ...(agentAccess.success ? { agentAccess: agentAccess.data } : {}),
2600
- ...(endUser.success ? { endUser: endUser.data } : {}),
2598
+ // This is a trusted DB admission input, not the public create payload.
2599
+ // Preserve the immutable creator policy through scheduled materialization.
2600
+ ...(policy.scopeSubjectId && /^(?:user:|external_user:).+/u.test(policy.scopeSubjectId)
2601
+ ? { scopeSubjectId: policy.scopeSubjectId }
2602
+ : {}),
2601
2603
  ...(memoryScope.success ? { memoryScope: memoryScope.data } : {}),
2602
2604
  };
2603
2605
  }