@opengeni/worker-bundle 0.27.3 → 0.28.3-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.
Files changed (37) hide show
  1. package/dist/activities/agent-turn/model-usage.d.ts +1 -1
  2. package/dist/activities/mcp-credentials.d.ts +3 -1
  3. package/dist/activities/sandbox-lease.d.ts +4 -4
  4. package/dist/activities/workspace-capture.d.ts +12 -1
  5. package/dist/{activities-control-WZRFEIJH.js → activities-control-JUBMDBDZ.js} +58 -73
  6. package/dist/activities-control-JUBMDBDZ.js.map +1 -0
  7. package/dist/{activities-turn-V7J34ZOF.js → activities-turn-I2ZKGZE6.js} +268 -187
  8. package/dist/activities-turn-I2ZKGZE6.js.map +1 -0
  9. package/dist/{chunk-BARTLVBK.js → chunk-A4XQWVZK.js} +25 -25
  10. package/dist/chunk-A4XQWVZK.js.map +1 -0
  11. package/dist/{chunk-C54VDT6I.js → chunk-ZNIKZQMN.js} +148 -111
  12. package/dist/chunk-ZNIKZQMN.js.map +1 -0
  13. package/dist/index.js +2 -2
  14. package/dist/observability-metrics.d.ts +12 -4
  15. package/dist/sandbox-archive-storage.d.ts +15 -3
  16. package/dist/sandbox-resume.d.ts +1 -1
  17. package/package.json +17 -17
  18. package/src/activities/agent-turn/agent-build.ts +30 -1
  19. package/src/activities/agent-turn/claim.ts +3 -3
  20. package/src/activities/agent-turn/finalization.ts +62 -41
  21. package/src/activities/agent-turn/model-usage.ts +10 -1
  22. package/src/activities/agent-turn/skill-read.ts +2 -21
  23. package/src/activities/agent-turn/stream-attempt.ts +3 -2
  24. package/src/activities/agent-turn/tool-environment.ts +52 -10
  25. package/src/activities/goals.ts +2 -0
  26. package/src/activities/mcp-credentials.ts +42 -2
  27. package/src/activities/sandbox-lease.ts +64 -71
  28. package/src/activities/scheduled-tasks.ts +8 -6
  29. package/src/activities/workspace-capture.ts +148 -126
  30. package/src/observability-metrics.ts +28 -24
  31. package/src/pr-review-credentials.ts +2 -0
  32. package/src/sandbox-archive-storage.ts +82 -18
  33. package/src/sandbox-resume.ts +79 -63
  34. package/dist/activities-control-WZRFEIJH.js.map +0 -1
  35. package/dist/activities-turn-V7J34ZOF.js.map +0 -1
  36. package/dist/chunk-BARTLVBK.js.map +0 -1
  37. package/dist/chunk-C54VDT6I.js.map +0 -1
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  startContextCompactionPendingMonitor,
20
20
  startSessionRecoveryMonitor,
21
21
  startTurnCapacityMonitor
22
- } from "./chunk-BARTLVBK.js";
22
+ } from "./chunk-A4XQWVZK.js";
23
23
 
24
24
  // src/index.ts
25
25
  import {
@@ -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-JUBMDBDZ.js")).createControlActivities(dependencies) : (await import("./activities-turn-I2ZKGZE6.js")).createTurnActivities(dependencies);
785
785
  }
786
786
  function resolveOpenGeniWorkflowDefinition(moduleUrl = import.meta.url) {
787
787
  const modulePath = fileURLToPath(moduleUrl).replaceAll("\\", "/");
@@ -91,15 +91,23 @@ export declare function recordWorkerDeathRecoveryMetrics(observability: Observab
91
91
  export declare class TurnLifecycleMetrics {
92
92
  private readonly observability;
93
93
  private readonly options;
94
- private readonly turns;
94
+ private readonly attempts;
95
95
  private timer;
96
96
  constructor(observability: Observability, options?: {
97
97
  now?: () => number;
98
98
  refreshIntervalMs?: number;
99
99
  });
100
- start(turnId: string): void;
101
- progress(turnId: string): void;
102
- finish(turnId: string, outcome: TurnOutcome | null, durationSeconds?: number): void;
100
+ start(input: {
101
+ attemptId: string;
102
+ }): void;
103
+ progress(input: {
104
+ attemptId: string;
105
+ }): void;
106
+ finish(input: {
107
+ attemptId: string;
108
+ outcome: TurnOutcome | null;
109
+ durationSeconds?: number;
110
+ }): void;
103
111
  refreshGauges(): void;
104
112
  stop(): void;
105
113
  private oldestInflightAgeSeconds;
@@ -1,4 +1,5 @@
1
- import type { ObjectStorage } from "@opengeni/storage";
1
+ import { type ObjectStorage } from "@opengeni/storage";
2
+ import type { VerifiedHostWorkspaceArchive } from "@opengeni/runtime/sandbox";
2
3
  import { type WorkspaceArchiveDescriptor, type WorkspaceArchiveObjectRef } from "@opengeni/contracts";
3
4
  export declare class WorkspaceArchiveObjectStorageRequiredError extends Error {
4
5
  readonly code: "workspace_archive_object_storage_required";
@@ -13,15 +14,26 @@ export declare function putTarWorkspaceArchiveObject(input: {
13
14
  archive: {
14
15
  bytes: Uint8Array;
15
16
  descriptor: WorkspaceArchiveDescriptor;
16
- };
17
+ } | VerifiedHostWorkspaceArchive;
17
18
  }): Promise<WorkspaceArchiveObjectRef>;
19
+ /** The caller owns a fresh candidate returned by this module. A thrown or lost
20
+ * database response is UNKNOWN: it may have committed, so never delete on catch.
21
+ * Only the transaction's exact candidate disposition can authorize cleanup. */
22
+ export declare function persistWorkspaceArchiveCandidate<T extends {
23
+ wrote: boolean;
24
+ candidateDisposition?: "adopted" | "already_referenced" | "unused";
25
+ }>(input: {
26
+ objectStorage?: ObjectStorage | null;
27
+ ref?: WorkspaceArchiveObjectRef;
28
+ persist: () => Promise<T>;
29
+ }): Promise<T>;
18
30
  export declare function putVersion1TarArchiveOrInline(input: {
19
31
  backend: string;
20
32
  objectStorage?: ObjectStorage | null | undefined;
21
33
  accountId: string;
22
34
  workspaceId: string;
23
35
  sandboxGroupId: string;
24
- archive: {
36
+ archive: VerifiedHostWorkspaceArchive | {
25
37
  bytes: Uint8Array;
26
38
  descriptor: WorkspaceArchiveDescriptor;
27
39
  base64: string;
@@ -1,7 +1,7 @@
1
1
  import { type Settings } from "@opengeni/config";
2
2
  import { readLease, reinstateTurnLeaseHolder, SandboxLeaseSupersededError, type Database, type LeaseHolderKind } from "@opengeni/db";
3
3
  import { establishSandboxSessionFromEnvelope, type EstablishedSandboxSession, type RuntimeMetricsHooks } from "@opengeni/runtime";
4
- import type { ObjectStorage } from "@opengeni/storage";
4
+ import { type ObjectStorage } from "@opengeni/storage";
5
5
  import type { Observability } from "@opengeni/observability";
6
6
  export type ResumeHolderKind = LeaseHolderKind;
7
7
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/worker-bundle",
3
- "version": "0.27.3",
3
+ "version": "0.28.3-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",
58
- "@opengeni/capabilities": "^0.3.3",
59
- "@opengeni/codemode": "^0.5.3",
60
- "@opengeni/codex": "^0.2.22",
61
- "@opengeni/config": "^1.0.4",
62
- "@opengeni/contracts": "^2.15.2",
63
- "@opengeni/core": "^2.8.3",
64
- "@opengeni/db": "^4.2.2",
65
- "@opengeni/documents": "^0.8.23",
66
- "@opengeni/events": "^0.4.21",
67
- "@opengeni/github": "^0.7.7",
68
- "@opengeni/network": "^0.3.1",
69
- "@opengeni/observability": "^0.8.23",
70
- "@opengeni/runtime": "^2.4.3",
71
- "@opengeni/storage": "^0.2.124",
72
- "@opengeni/xai-subscription": "^0.1.4",
57
+ "@opengeni/agent-proto": "^0.5.1-canary.25",
58
+ "@opengeni/capabilities": "^0.3.3-canary.7",
59
+ "@opengeni/codemode": "^0.5.5-canary.0",
60
+ "@opengeni/codex": "^0.2.22-canary.7",
61
+ "@opengeni/config": "^1.1.1-canary.0",
62
+ "@opengeni/contracts": "^3.0.1-canary.0",
63
+ "@opengeni/core": "^2.9.3-canary.0",
64
+ "@opengeni/db": "^4.3.2-canary.0",
65
+ "@opengeni/documents": "^0.8.26-canary.0",
66
+ "@opengeni/events": "^0.4.24-canary.0",
67
+ "@opengeni/github": "^0.7.9-canary.0",
68
+ "@opengeni/network": "^0.3.1-canary.7",
69
+ "@opengeni/observability": "^0.8.25-canary.0",
70
+ "@opengeni/runtime": "^2.5.2-canary.0",
71
+ "@opengeni/storage": "^0.2.126-canary.0",
72
+ "@opengeni/xai-subscription": "^0.1.4-canary.7",
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
  : {}),
@@ -347,7 +347,7 @@ export async function claimTurnAttempt(deps: ClaimTurnDeps): Promise<ClaimTurnOu
347
347
  });
348
348
  return attached.action === "claimed" && attached.turn.id === turn.id;
349
349
  };
350
- turnLifecycleMetricsFor(observability).start(attempt.turnId);
350
+ turnLifecycleMetricsFor(observability).start({ attemptId: input.attemptId });
351
351
  // §7.5 P3 — pass the accepted billing attribution (externally funded turns
352
352
  // bypass OpenGeni credit/token gates)
353
353
  // AND the optional host `entitlements` port (when bound, its admitRun replaces
@@ -451,7 +451,7 @@ export async function claimTurnAttempt(deps: ClaimTurnDeps): Promise<ClaimTurnOu
451
451
  }
452
452
  recordCanonicalStartupMilestones(appended.canonicalStartupMilestones);
453
453
  if (inputs.length > 0) {
454
- turnLifecycleMetricsFor(observability).progress(attempt.turnId!);
454
+ turnLifecycleMetricsFor(observability).progress({ attemptId: input.attemptId });
455
455
  }
456
456
  activityContext?.heartbeat({
457
457
  ...heartbeatDetails,
@@ -515,7 +515,7 @@ export async function claimTurnAttempt(deps: ClaimTurnDeps): Promise<ClaimTurnOu
515
515
  return false;
516
516
  }
517
517
  recordCanonicalStartupMilestones(result.canonicalStartupMilestones);
518
- turnLifecycleMetricsFor(observability).progress(attempt.turnId!);
518
+ turnLifecycleMetricsFor(observability).progress({ attemptId: input.attemptId });
519
519
  await publishDurableSessionEvents(bus, input.workspaceId, input.sessionId, result.events);
520
520
  if (inputSettlement.turnStatus === "requires_action") {
521
521
  // The settlement transaction committed the parent's child_requires_action
@@ -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;
@@ -729,12 +745,17 @@ export async function finalizeTurnAttempt(deps: TurnFinalizationDeps): Promise<v
729
745
  status: finalizationError ? "cleanup_failed" : control.activityStatus,
730
746
  durationSeconds,
731
747
  });
732
- if (attempt.turnId && control.activityStatus !== "unknown") {
733
- turnLifecycleMetricsFor(observability).finish(
734
- attempt.turnId,
735
- control.turnMetricOutcome,
748
+ if (attempt.turnId) {
749
+ // This registry describes physical activity ownership, not durable turn
750
+ // settlement. Always clear the exact attempt when its activity unwinds,
751
+ // even when failure settlement could not classify an outcome. Otherwise
752
+ // a recovered successor can make progress while the predecessor leaves a
753
+ // process-local false stuck-turn alert behind.
754
+ turnLifecycleMetricsFor(observability).finish({
755
+ attemptId: input.attemptId,
756
+ outcome: control.turnMetricOutcome,
736
757
  durationSeconds,
737
- );
758
+ });
738
759
  }
739
760
  activitySpan.end({
740
761
  attributes: {
@@ -129,8 +129,17 @@ export const createSessionTitleModelUsageEventState = createCompactionModelUsage
129
129
 
130
130
  export function modelResponseContextSignal(
131
131
  state: ModelResponseEventState,
132
+ responseCountBeforeStream = 0,
132
133
  ): { revision: number; totalTokens: number } | null {
133
- return state.contextSignal;
134
+ const signal = state.contextSignal;
135
+ // A compaction retry creates a new SDK request counter, but usage identities
136
+ // remain activity-wide. Never bind a pre-stream report to a reused request
137
+ // ordinal; translate only this stream's reports without mutating usage state.
138
+ if (!signal || signal.revision <= responseCountBeforeStream) return null;
139
+ return {
140
+ revision: signal.revision - responseCountBeforeStream,
141
+ totalTokens: signal.totalTokens,
142
+ };
134
143
  }
135
144
 
136
145
  export function assertModelResponseLatencyMode(input: {
@@ -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
- }
@@ -865,7 +865,8 @@ export async function runTurnStreamAttempt(
865
865
  },
866
866
  }
867
867
  : {}),
868
- contextCompactionSignal: () => modelResponseContextSignal(modelResponseState),
868
+ contextCompactionSignal: () =>
869
+ modelResponseContextSignal(modelResponseState, responseCountBeforeStream),
869
870
  contextCompactionRequested: () =>
870
871
  isSessionCompactionRequested(db, input.workspaceId, input.sessionId),
871
872
  onModelPreparationPhase: (measurement) => {
@@ -1061,7 +1062,7 @@ export async function runTurnStreamAttempt(
1061
1062
  currentToolBatchCallIds = new Set<string>();
1062
1063
  currentToolBatchCompletedCallIds = new Set<string>();
1063
1064
  await historySink.reconcileConversationTruth();
1064
- turnLifecycleMetricsFor(observability).progress(attempt.turnId!);
1065
+ turnLifecycleMetricsFor(observability).progress({ attemptId: input.attemptId });
1065
1066
  modelCheckpointMemoryCollector.schedule(observability);
1066
1067
  try {
1067
1068
  await ensureRunAllowed(
@@ -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,
@@ -286,6 +286,8 @@ export function goalContinuationPrompt(
286
286
  "Continuation behavior:",
287
287
  "- This goal persists across turns. A runtime boundary can end one turn without shrinking the objective; the next continuation resumes the same full objective.",
288
288
  "- Keep working until the requested end state is true and verified. Do not end the turn merely because one useful action completed, and do not redefine success around a smaller or easier task.",
289
+ "- An incomplete-status report is not a substitute for continuing the work. If the remaining problem can be investigated or addressed within your current authority, continue that work in this turn rather than returning another equivalent status-only final.",
290
+ "- Distinguish unfinished work from a blocker that actually requires human input or external change; use the existing waiting and blocked audits only when their conditions hold.",
289
291
  "- Temporary rough edges are acceptable while the work is moving in the right direction. Completion still requires the requested end state to be true and verified.",
290
292
  "",
291
293
  "Work from evidence:",
@@ -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
  ) {