@opengeni/db 0.9.3 → 0.10.7

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 (33) hide show
  1. package/dist/{chunk-4LG5NBTC.js → chunk-P6PKXY5W.js} +93 -1
  2. package/dist/chunk-P6PKXY5W.js.map +1 -0
  3. package/dist/index.d.ts +3 -2
  4. package/dist/index.js +1332 -178
  5. package/dist/index.js.map +1 -1
  6. package/dist/provision-roles.d.ts +406 -32
  7. package/dist/{schema-CdPGTHlD.d.ts → schema-CqkzrBRS.d.ts} +513 -2
  8. package/dist/schema.d.ts +1 -1
  9. package/dist/schema.js +3 -1
  10. package/drizzle/0053_codex_credential_leases.sql +2 -2
  11. package/drizzle/0057_durable_queue_control.sql +1 -1
  12. package/drizzle/0061_session_workflow_wake_outbox.sql +1 -1
  13. package/drizzle/0062_session_list_snapshot_reaper.sql +1 -1
  14. package/drizzle/0063_session_control_mega_foundation.sql +1 -1
  15. package/drizzle/0064_rotation_strategy_sharded_backfill.sql +1 -1
  16. package/drizzle/0065_codex_subscription_overview.sql +168 -0
  17. package/drizzle/0065_session_tool_policy.sql +38 -0
  18. package/drizzle/0067_session_event_payload_bounds.sql +2 -2
  19. package/drizzle/0068_workspace_control_event_bounds.sql +2 -2
  20. package/drizzle/0069_session_event_history_backfill.sql +2 -2
  21. package/drizzle/0074_session_activity_revisions.sql +2 -2
  22. package/drizzle/0106_session_attempt_mcp_approval_policies.sql +29 -0
  23. package/drizzle/0107_host_export_lineage_contract.sql +381 -0
  24. package/drizzle/0108_fence_invalidated_warming_epochs.sql +76 -0
  25. package/package.json +5 -4
  26. package/src/codex-token-resolver.ts +175 -14
  27. package/src/connection-token-resolver.ts +143 -120
  28. package/src/event-payload-sanitizer.ts +32 -2
  29. package/src/index.ts +1888 -205
  30. package/src/schema.ts +107 -1
  31. package/src/session-control.ts +2 -0
  32. package/src/session-queue-commands.ts +94 -21
  33. package/dist/chunk-4LG5NBTC.js.map +0 -1
package/src/schema.ts CHANGED
@@ -1,5 +1,6 @@
1
- import type { McpServerConnectionRef } from "@opengeni/contracts";
1
+ import type { McpServerConnectionRef, SessionMcpApprovalPolicy } from "@opengeni/contracts";
2
2
  import { sql } from "drizzle-orm";
3
+ import type { SessionToolPolicy } from "@opengeni/contracts";
3
4
  import type { HumanInputQuestion, HumanInputResponse } from "@opengeni/contracts";
4
5
  import {
5
6
  bigint,
@@ -320,6 +321,19 @@ export const codexSubscriptionCredentials = pgTable(
320
321
  // refresh, encrypted material, and already-frozen/in-flight turns are
321
322
  // intentionally independent. account eligibility policy owns toggle OCC/audit and product UI.
322
323
  allocatorEnabled: boolean("allocator_enabled").notNull().default(true),
324
+ // Independent OCC/audit sequence for the allocator toggle. Token refresh
325
+ // continues to own `version`; quota/cache writes own neither counter.
326
+ allocatorVersion: integer("allocator_version").notNull().default(1),
327
+ allocatorUpdatedBySubjectId: text("allocator_updated_by_subject_id"),
328
+ allocatorUpdatedAt: timestamp("allocator_updated_at", { withTimezone: true }),
329
+ // Authoritative count-only summary cached from /wham/usage. Detailed rows
330
+ // are never persisted as redemption authority; every first POST preflights
331
+ // the provider's fresh detail endpoint.
332
+ resetCreditAvailableCount: integer("reset_credit_available_count"),
333
+ resetCreditsCheckedAt: timestamp("reset_credits_checked_at", { withTimezone: true }),
334
+ // Set only by a direct Better Auth cookie connection/reconnection. Legacy,
335
+ // configured, delegated, API-key, and agent-created rows remain view-only.
336
+ connectedBySubjectId: text("connected_by_subject_id"),
323
337
  selectionCount: integer("selection_count").notNull().default(0),
324
338
  lastSelectedAt: timestamp("last_selected_at", { withTimezone: true }),
325
339
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
@@ -343,6 +357,82 @@ export const codexSubscriptionCredentials = pgTable(
343
357
  }),
344
358
  );
345
359
 
360
+ // One durable logical human redemption. `processing` means the fresh provider
361
+ // detail preflight is still owed; `provider_started` means the POST may have
362
+ // reached upstream and every retry must reuse upstreamIdempotencyKey without
363
+ // requiring the credit to remain visible as available.
364
+ export const codexResetRedemptionAttempts = pgTable(
365
+ "codex_reset_redemption_attempts",
366
+ {
367
+ id: uuid("id").primaryKey(),
368
+ accountId: uuid("account_id")
369
+ .notNull()
370
+ .references(() => managedAccounts.id, { onDelete: "cascade" }),
371
+ workspaceId: uuid("workspace_id")
372
+ .notNull()
373
+ .references(() => workspaces.id, { onDelete: "cascade" }),
374
+ credentialId: uuid("credential_id").notNull(),
375
+ subjectId: text("subject_id").notNull(),
376
+ browserSessionHash: text("browser_session_hash").notNull(),
377
+ creditId: text("credit_id").notNull(),
378
+ upstreamIdempotencyKey: uuid("upstream_idempotency_key").notNull().defaultRandom(),
379
+ status: text("status").notNull().default("processing"),
380
+ outcome: text("outcome"),
381
+ claimHolderId: uuid("claim_holder_id"),
382
+ claimExpiresAt: timestamp("claim_expires_at", { withTimezone: true }),
383
+ confirmationExpiresAt: timestamp("confirmation_expires_at", { withTimezone: true }).notNull(),
384
+ providerStartedAt: timestamp("provider_started_at", { withTimezone: true }),
385
+ completedAt: timestamp("completed_at", { withTimezone: true }),
386
+ lastFailureKind: text("last_failure_kind"),
387
+ retryCount: integer("retry_count").notNull().default(0),
388
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
389
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
390
+ },
391
+ (table) => ({
392
+ workspaceAccount: foreignKey({
393
+ name: "codex_reset_redemption_workspace_account_fk",
394
+ columns: [table.workspaceId, table.accountId],
395
+ foreignColumns: [workspaces.id, workspaces.accountId],
396
+ }).onDelete("cascade"),
397
+ upstreamKey: uniqueIndex("codex_reset_redemption_upstream_key_idx").on(
398
+ table.upstreamIdempotencyKey,
399
+ ),
400
+ credentialCredit: uniqueIndex("codex_reset_redemption_credential_credit_idx")
401
+ .on(table.workspaceId, table.credentialId, table.creditId)
402
+ .where(
403
+ sql`${table.status} <> 'completed' or ${table.outcome} in ('reset', 'alreadyRedeemed')`,
404
+ ),
405
+ workspaceCredential: index("codex_reset_redemption_workspace_credential_idx").on(
406
+ table.workspaceId,
407
+ table.credentialId,
408
+ table.createdAt,
409
+ ),
410
+ claimExpiry: index("codex_reset_redemption_claim_expiry_idx")
411
+ .on(table.claimExpiresAt)
412
+ .where(sql`${table.status} <> 'completed'`),
413
+ statusValid: check(
414
+ "codex_reset_redemption_status_check",
415
+ sql`${table.status} in ('processing', 'provider_started', 'completed')`,
416
+ ),
417
+ outcomeValid: check(
418
+ "codex_reset_redemption_outcome_check",
419
+ sql`${table.outcome} is null or ${table.outcome} in ('reset', 'nothingToReset', 'noCredit', 'alreadyRedeemed')`,
420
+ ),
421
+ completionConsistent: check(
422
+ "codex_reset_redemption_completed_check",
423
+ sql`(${table.status} = 'completed') = (${table.outcome} is not null and ${table.completedAt} is not null)`,
424
+ ),
425
+ retryCountValid: check(
426
+ "codex_reset_redemption_retry_count_check",
427
+ sql`${table.retryCount} >= 0`,
428
+ ),
429
+ humanSubjectValid: check(
430
+ "codex_reset_redemption_human_subject_check",
431
+ sql`${table.subjectId} like 'user:_%'`,
432
+ ),
433
+ }),
434
+ );
435
+
346
436
  // Generic external-service credential spine. credential_encrypted is the ONLY
347
437
  // secret-bearing column; normal API reads use metadata-only helpers below the DB
348
438
  // layer. Runtime token material is decrypted only by the broker accessor.
@@ -634,6 +724,10 @@ export const sessions = pgTable(
634
724
  // Non-default first-party MCP token permissions (manager-style sessions);
635
725
  // null means the fixed worker default set in @opengeni/runtime.
636
726
  firstPartyMcpPermissions: jsonb("first_party_mcp_permissions").$type<string[]>(),
727
+ // Durable tool-policy origin. NULL is retained for pre-migration rows;
728
+ // mapSession exposes those rows as `legacy` instead of guessing omitted vs
729
+ // explicit [].
730
+ toolPolicy: jsonb("tool_policy").$type<SessionToolPolicy>(),
637
731
  // The manager session that spawned this one via session_create. Set only
638
732
  // when the creating grant carried a worker-signed sessionId claim (a session
639
733
  // spawning a worker); null for direct API creates and scheduled-task runs.
@@ -1156,6 +1250,9 @@ export const sessionTurns = pgTable(
1156
1250
  turnInstructions: text("turn_instructions"),
1157
1251
  resources: jsonb("resources").$type<unknown[]>().notNull().default([]),
1158
1252
  tools: jsonb("tools").$type<unknown[]>().notNull().default([]),
1253
+ // false = inherit the durable session policy; true = this turn explicitly
1254
+ // replaces it with `tools` after the core subset fence.
1255
+ toolsProvided: boolean("tools_provided").notNull().default(false),
1159
1256
  model: text("model").notNull(),
1160
1257
  reasoningEffort: text("reasoning_effort").notNull(),
1161
1258
  sandboxBackend: text("sandbox_backend").notNull(),
@@ -1230,6 +1327,10 @@ export const sessionTurnAttempts = pgTable(
1230
1327
  verifiedControlRevision: bigint("verified_control_revision", {
1231
1328
  mode: "number",
1232
1329
  }).notNull(),
1330
+ // Immutable policy snapshot captured under the session lock at claim.
1331
+ mcpApprovalPolicies: jsonb("mcp_approval_policies")
1332
+ .$type<Record<string, SessionMcpApprovalPolicy>>()
1333
+ .notNull(),
1233
1334
  startedAt: timestamp("started_at", { withTimezone: true }).notNull().defaultNow(),
1234
1335
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1235
1336
  closedAt: timestamp("closed_at", { withTimezone: true }),
@@ -1502,6 +1603,7 @@ export const composerDrafts = pgTable(
1502
1603
  text: text("text").notNull().default(""),
1503
1604
  resources: jsonb("resources").$type<unknown[]>().notNull().default([]),
1504
1605
  tools: jsonb("tools").$type<unknown[]>().notNull().default([]),
1606
+ toolsProvided: boolean("tools_provided").notNull().default(false),
1505
1607
  model: text("model").notNull(),
1506
1608
  reasoningEffort: text("reasoning_effort").notNull(),
1507
1609
  sourceTurnId: uuid("source_turn_id"),
@@ -2999,6 +3101,10 @@ export const hostExportOutbox = pgTable(
2999
3101
  "host_export_outbox_kind_check",
3000
3102
  sql`${table.exportKind} in ('session_event', 'usage_event')`,
3001
3103
  ),
3104
+ rootSessionCaptured: check(
3105
+ "host_export_outbox_root_session_check",
3106
+ sql`${table.sessionId} is null or ${table.rootSessionId} is not null`,
3107
+ ),
3002
3108
  sourceUnique: uniqueIndex("host_export_outbox_source_uq").on(table.exportKind, table.sourceId),
3003
3109
  cursorUnique: uniqueIndex("host_export_outbox_cursor_uq")
3004
3110
  .on(table.exportKind, table.exportCursor)
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import {
3
3
  boundWorkspaceControlEvent,
4
4
  workspaceControlUtf8Bytes,
5
+ type SessionMcpApprovalPolicy,
5
6
  type TurnInitiatorContext,
6
7
  } from "@opengeni/contracts";
7
8
  import { and, eq, inArray, sql } from "drizzle-orm";
@@ -512,6 +513,7 @@ export async function registerSessionTurnAttemptClaim(
512
513
  temporalWorkflowRunId: string;
513
514
  temporalActivityId: string;
514
515
  verifiedControlRevision: number;
516
+ mcpApprovalPolicies: Record<string, SessionMcpApprovalPolicy>;
515
517
  },
516
518
  ): Promise<typeof schema.sessionTurnAttempts.$inferSelect> {
517
519
  const [inserted] = await db
@@ -1,9 +1,13 @@
1
1
  import {
2
+ metadataWithTurnExecutionPolicyV1,
2
3
  mergeResourceRefs,
3
4
  mergeToolRefs,
5
+ turnExecutionPolicyAuditMetadata,
4
6
  type ReasoningEffort,
5
7
  type ResourceRef,
8
+ type SessionToolPolicy,
6
9
  type ToolRef,
10
+ type TurnExecutionPolicyV1,
7
11
  } from "@opengeni/contracts";
8
12
  import { and, asc, eq, inArray, sql } from "drizzle-orm";
9
13
  import type { Database } from "./index";
@@ -39,7 +43,8 @@ export type QueueCommandConflictCode =
39
43
  | "QUEUE_ANCHOR_CHANGED"
40
44
  | "PROMPT_CHANGED"
41
45
  | "DRAFT_CHANGED"
42
- | "DRAFT_NOT_EMPTY";
46
+ | "DRAFT_NOT_EMPTY"
47
+ | "EDIT_SOURCE_CHANGED";
43
48
 
44
49
  export class QueueCommandConflictError extends Error {
45
50
  readonly name = "QueueCommandConflictError";
@@ -377,6 +382,7 @@ function draftIsNonEmpty(draft: ComposerDraftRow): boolean {
377
382
  draft.text.length > 0 ||
378
383
  draft.resources.length > 0 ||
379
384
  draft.tools.length > 0 ||
385
+ draft.toolsProvided ||
380
386
  draft.sourceTurnId !== null
381
387
  );
382
388
  }
@@ -416,6 +422,7 @@ export async function saveComposerDraftInTransaction(
416
422
  text: string;
417
423
  resources: ResourceRef[];
418
424
  tools: ToolRef[];
425
+ toolsProvided: boolean;
419
426
  model: string;
420
427
  reasoningEffort: ReasoningEffort;
421
428
  },
@@ -447,6 +454,7 @@ export async function saveComposerDraftInTransaction(
447
454
  text: input.text,
448
455
  resources: input.resources,
449
456
  tools: input.tools,
457
+ toolsProvided: input.toolsProvided,
450
458
  model: input.model,
451
459
  reasoningEffort: input.reasoningEffort,
452
460
  // A queue edit is still the same accepted work item. Preserve its frozen
@@ -818,6 +826,7 @@ export async function editQueuedTurnInTransaction(
818
826
  text: turn.prompt,
819
827
  resources: turn.resources,
820
828
  tools: turn.tools,
829
+ toolsProvided: turn.toolsProvided,
821
830
  model: turn.model,
822
831
  reasoningEffort: turn.reasoningEffort,
823
832
  sourceTurnId: turn.id,
@@ -1134,9 +1143,12 @@ export async function submitHumanPromptInTransaction(
1134
1143
  turnInstructions?: string | null;
1135
1144
  resources: ResourceRef[];
1136
1145
  tools: ToolRef[];
1146
+ toolsProvided?: boolean;
1137
1147
  model?: string | null;
1138
1148
  reasoningEffort?: ReasoningEffort | null;
1139
1149
  reasoningEffortFallback: ReasoningEffort;
1150
+ /** Trusted API/core admission snapshot. Omitted only by legacy low-level callers. */
1151
+ turnExecutionPolicy?: TurnExecutionPolicyV1;
1140
1152
  source: "user" | "api";
1141
1153
  mcpCredentialUpdates?: Array<{
1142
1154
  id: string;
@@ -1163,6 +1175,7 @@ export async function submitHumanPromptInTransaction(
1163
1175
  turnInstructions: input.turnInstructions ?? null,
1164
1176
  resources: input.resources,
1165
1177
  tools: input.tools,
1178
+ toolsProvided: input.toolsProvided === true,
1166
1179
  model: input.model ?? null,
1167
1180
  reasoningEffort: input.reasoningEffort ?? null,
1168
1181
  source: input.source,
@@ -1271,6 +1284,7 @@ export async function submitHumanPromptInTransaction(
1271
1284
  text: draft.text,
1272
1285
  resources: draft.resources,
1273
1286
  tools: draft.tools,
1287
+ toolsProvided: draft.toolsProvided,
1274
1288
  model: draft.model,
1275
1289
  reasoningEffort: draft.reasoningEffort,
1276
1290
  }) !==
@@ -1278,6 +1292,7 @@ export async function submitHumanPromptInTransaction(
1278
1292
  text: input.text,
1279
1293
  resources: input.resources,
1280
1294
  tools: input.tools,
1295
+ toolsProvided: input.toolsProvided === true,
1281
1296
  model: input.model ?? session.model,
1282
1297
  reasoningEffort: input.reasoningEffort ?? input.reasoningEffortFallback,
1283
1298
  })
@@ -1293,6 +1308,52 @@ export async function submitHumanPromptInTransaction(
1293
1308
  }
1294
1309
  }
1295
1310
 
1311
+ let editedSourceTurn: QueuedTurnRow | undefined;
1312
+ let editedSourceTurnInstructions: string | null | undefined;
1313
+ if (draft?.sourceTurnId) {
1314
+ const sourceLocks = await lockSessionEventWriteRows(db, {
1315
+ workspaceId: input.workspaceId,
1316
+ controlLock: "already_locked",
1317
+ workspaceLock: "already_locked",
1318
+ turnIds: [draft.sourceTurnId],
1319
+ });
1320
+ const sourceTurn = sourceLocks.turns[0];
1321
+ const sourceTurnVersion = draft.sourceTurnVersion;
1322
+ const sourceMetadata = sourceTurn?.metadata ?? {};
1323
+ const sourceIsExactWithdrawnRevision =
1324
+ sourceTurn !== undefined &&
1325
+ sourceTurn.accountId === input.accountId &&
1326
+ sourceTurn.workspaceId === input.workspaceId &&
1327
+ sourceTurn.sessionId === input.sessionId &&
1328
+ (sourceTurn.source === "user" || sourceTurn.source === "api") &&
1329
+ sourceTurn.status === "withdrawn_for_edit" &&
1330
+ sourceTurnVersion !== null &&
1331
+ sourceTurn.version === sourceTurnVersion + 1 &&
1332
+ sourceTurn.cancelledBy === input.subjectId &&
1333
+ sourceTurn.cancelReason === "withdrawn_for_edit" &&
1334
+ sourceTurn.activeAttemptId === null &&
1335
+ sourceMetadata.delivery !== "steer";
1336
+ if (!sourceIsExactWithdrawnRevision) {
1337
+ throw new QueueCommandConflictError(
1338
+ "EDIT_SOURCE_CHANGED",
1339
+ "Edited prompt source changed or is no longer withdrawn for edit",
1340
+ {
1341
+ queueVersion: session.queueVersion,
1342
+ draftRevision: draft.revision,
1343
+ ...(sourceTurn ? { turnVersion: sourceTurn.version } : {}),
1344
+ },
1345
+ );
1346
+ }
1347
+ // This is the sole private-instruction source for an edited replacement.
1348
+ // It is deliberately held separately from the public draft and event
1349
+ // projections below. The public draft is expected to differ after editing;
1350
+ // source identity is fenced by its exact withdrawn row version, not by
1351
+ // comparing the replacement content with the old prompt. A client-supplied
1352
+ // instruction value must never override the private source value.
1353
+ editedSourceTurn = sourceTurn;
1354
+ editedSourceTurnInstructions = sourceTurn.turnInstructions ?? null;
1355
+ }
1356
+
1296
1357
  for (const update of input.mcpCredentialUpdates ?? []) {
1297
1358
  const [server] = await db
1298
1359
  .update(schema.sessionMcpServers)
@@ -1315,22 +1376,7 @@ export async function submitHumanPromptInTransaction(
1315
1376
  const now = new Date();
1316
1377
  let frozenInitiator: FrozenTurnInitiator;
1317
1378
  if (input.delivery === "send" && draft?.sourceTurnId) {
1318
- const [sourceTurn] = await db
1319
- .select({
1320
- sessionId: schema.sessionTurns.sessionId,
1321
- initiatorKind: schema.sessionTurns.initiatorKind,
1322
- initiatorSubjectId: schema.sessionTurns.initiatorSubjectId,
1323
- initiatorContext: schema.sessionTurns.initiatorContext,
1324
- })
1325
- .from(schema.sessionTurns)
1326
- .where(
1327
- and(
1328
- eq(schema.sessionTurns.workspaceId, input.workspaceId),
1329
- eq(schema.sessionTurns.sessionId, input.sessionId),
1330
- eq(schema.sessionTurns.id, draft.sourceTurnId),
1331
- ),
1332
- )
1333
- .limit(1);
1379
+ const sourceTurn = editedSourceTurn;
1334
1380
  if (!sourceTurn) {
1335
1381
  throw new SessionControlInvariantError("Edited prompt source turn is missing");
1336
1382
  }
@@ -1366,7 +1412,11 @@ export async function submitHumanPromptInTransaction(
1366
1412
  payload: sanitizeEventPayload({
1367
1413
  text: input.text,
1368
1414
  ...(input.resources.length ? { resources: input.resources } : {}),
1369
- ...(input.tools.length ? { tools: input.tools } : {}),
1415
+ ...(input.toolsProvided === true
1416
+ ? { tools: input.tools }
1417
+ : input.tools.length
1418
+ ? { tools: input.tools }
1419
+ : {}),
1370
1420
  ...(input.model ? { model: input.model } : {}),
1371
1421
  ...(input.reasoningEffort ? { reasoningEffort: input.reasoningEffort } : {}),
1372
1422
  delivery: input.delivery,
@@ -1389,13 +1439,19 @@ export async function submitHumanPromptInTransaction(
1389
1439
  source: input.source,
1390
1440
  position: input.delivery === "steer" ? 0 : existingQueued.length + 1,
1391
1441
  prompt: input.text,
1392
- turnInstructions: input.turnInstructions ?? null,
1442
+ turnInstructions:
1443
+ editedSourceTurnInstructions !== undefined
1444
+ ? editedSourceTurnInstructions
1445
+ : (input.turnInstructions ?? null),
1393
1446
  resources: input.resources,
1394
1447
  tools: input.tools,
1448
+ toolsProvided: input.toolsProvided === true,
1395
1449
  model: input.model ?? session.model,
1396
1450
  reasoningEffort: input.reasoningEffort ?? input.reasoningEffortFallback,
1397
1451
  sandboxBackend: session.sandboxBackend,
1398
- metadata: {},
1452
+ metadata: input.turnExecutionPolicy
1453
+ ? metadataWithTurnExecutionPolicyV1({}, input.turnExecutionPolicy)
1454
+ : {},
1399
1455
  lineage: { actor: input.actor.type },
1400
1456
  ...initiatorColumns(frozenInitiator),
1401
1457
  })
@@ -1455,6 +1511,7 @@ export async function submitHumanPromptInTransaction(
1455
1511
  .update(schema.sessionTurns)
1456
1512
  .set({
1457
1513
  metadata: {
1514
+ ...turn.metadata,
1458
1515
  delivery: "steer",
1459
1516
  replacedTurnId,
1460
1517
  replacedAttemptId,
@@ -1532,7 +1589,9 @@ export async function submitHumanPromptInTransaction(
1532
1589
  .update(schema.sessions)
1533
1590
  .set({
1534
1591
  resources: mergeResourceRefs(session.resources as ResourceRef[], input.resources),
1535
- tools: mergeToolRefs(session.tools as ToolRef[], input.tools),
1592
+ tools: sessionToolPolicyIsFixed(session.toolPolicy)
1593
+ ? session.tools
1594
+ : mergeToolRefs(session.tools as ToolRef[], input.tools),
1536
1595
  activeTurnId: input.delivery === "steer" ? liveCurrentTurnId : session.activeTurnId,
1537
1596
  status: nextStatus,
1538
1597
  queueVersion,
@@ -1566,6 +1625,9 @@ export async function submitHumanPromptInTransaction(
1566
1625
  operationId: reserved.receipt.id,
1567
1626
  replacedTurnId,
1568
1627
  interruptionCount,
1628
+ ...(input.turnExecutionPolicy
1629
+ ? turnExecutionPolicyAuditMetadata(input.turnExecutionPolicy, turnId)
1630
+ : {}),
1569
1631
  },
1570
1632
  });
1571
1633
  const eventIds = eventRows.map((event) => event.id);
@@ -1582,6 +1644,11 @@ export async function submitHumanPromptInTransaction(
1582
1644
  interruptionCount,
1583
1645
  replacedTurnId,
1584
1646
  workspaceControlEventId: resumed.workspaceControlEventId,
1647
+ ...(input.turnExecutionPolicy
1648
+ ? {
1649
+ executionPolicy: turnExecutionPolicyAuditMetadata(input.turnExecutionPolicy, turnId),
1650
+ }
1651
+ : {}),
1585
1652
  },
1586
1653
  });
1587
1654
  return {
@@ -1597,6 +1664,12 @@ export async function submitHumanPromptInTransaction(
1597
1664
  };
1598
1665
  }
1599
1666
 
1667
+ function sessionToolPolicyIsFixed(value: unknown): boolean {
1668
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1669
+ const mode = (value as Partial<SessionToolPolicy>).mode;
1670
+ return mode === "workspace_default" || mode === "explicit" || mode === "inherited";
1671
+ }
1672
+
1600
1673
  export async function sendAgentMessageInTransaction(
1601
1674
  db: Database,
1602
1675
  input: {