@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/dist/index.js CHANGED
@@ -566,7 +566,9 @@ async function delegatedAccessContext(c, deps, mode, token = bearerToken(c)) {
566
566
  ...payload.turnId ? { turnId: payload.turnId } : {},
567
567
  ...payload.attemptId ? { attemptId: payload.attemptId } : {},
568
568
  ...payload.executionGeneration ? { executionGeneration: payload.executionGeneration } : {}
569
- }
569
+ },
570
+ ...payload.serviceInitiator ? { serviceInitiator: payload.serviceInitiator } : {},
571
+ ...payload.serviceInitiatorContext ? { serviceInitiatorContext: payload.serviceInitiatorContext } : {}
570
572
  }
571
573
  ],
572
574
  defaultAccountId: payload.accountId,
@@ -586,6 +588,146 @@ async function sha256Hex(value) {
586
588
  return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
587
589
  }
588
590
 
591
+ // src/session-authorization.ts
592
+ import {
593
+ SessionAuthorizationActor,
594
+ SessionAuthorizationDecision,
595
+ SessionAuthorizationListScope
596
+ } from "@opengeni/contracts";
597
+ import {
598
+ getSession,
599
+ getSessionRootId,
600
+ getSessionTurnForAttempt
601
+ } from "@opengeni/db";
602
+ var SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS = 15e3;
603
+ var SessionAuthorizationDeniedError = class extends Error {
604
+ constructor(reason) {
605
+ super("Session not found or access denied");
606
+ this.reason = reason;
607
+ this.name = "SessionAuthorizationDeniedError";
608
+ }
609
+ code = "SESSION_NOT_FOUND_OR_DENIED";
610
+ };
611
+ var SessionAuthorizationUnavailableError = class extends Error {
612
+ code = "SESSION_AUTHORIZATION_UNAVAILABLE";
613
+ constructor(options) {
614
+ super("Session authorization is unavailable", options);
615
+ this.name = "SessionAuthorizationUnavailableError";
616
+ }
617
+ };
618
+ async function requireSessionAuthorization(deps, grant, input) {
619
+ const port = deps.sessionAuthorization;
620
+ if (!port) return null;
621
+ const [actor, target] = await Promise.all([
622
+ resolveSessionAuthorizationActor(deps.db, grant),
623
+ resolveSessionAuthorizationTarget(deps.db, grant, input.sessionId)
624
+ ]);
625
+ let rawDecision;
626
+ try {
627
+ rawDecision = await port.authorizeSession({
628
+ accountId: grant.accountId,
629
+ workspaceId: grant.workspaceId,
630
+ actor,
631
+ target,
632
+ operation: input.operation,
633
+ surface: input.surface
634
+ });
635
+ } catch (error) {
636
+ throw new SessionAuthorizationUnavailableError({ cause: error });
637
+ }
638
+ const parsed = SessionAuthorizationDecision.safeParse(rawDecision);
639
+ if (!parsed.success) {
640
+ throw new SessionAuthorizationUnavailableError({ cause: parsed.error });
641
+ }
642
+ if (!parsed.data.allowed) {
643
+ throw new SessionAuthorizationDeniedError(parsed.data.reason);
644
+ }
645
+ return {
646
+ actor,
647
+ target,
648
+ relatedSessionAccess: parsed.data.relatedSessionAccess ?? "target",
649
+ reauthorizeAfterMs: parsed.data.reauthorizeAfterMs ?? null
650
+ };
651
+ }
652
+ async function requireSessionAuthorizationListScope(deps, grant, surface) {
653
+ const port = deps.sessionAuthorization;
654
+ if (!port) return null;
655
+ const actor = await resolveSessionAuthorizationActor(deps.db, grant);
656
+ let rawScope;
657
+ try {
658
+ rawScope = await port.resolveListScope({
659
+ accountId: grant.accountId,
660
+ workspaceId: grant.workspaceId,
661
+ actor,
662
+ surface
663
+ });
664
+ } catch (error) {
665
+ throw new SessionAuthorizationUnavailableError({ cause: error });
666
+ }
667
+ const parsed = SessionAuthorizationListScope.safeParse(rawScope);
668
+ if (!parsed.success) {
669
+ throw new SessionAuthorizationUnavailableError({ cause: parsed.error });
670
+ }
671
+ if (parsed.data.kind === "all") return parsed.data;
672
+ return {
673
+ kind: "scoped",
674
+ rootSessionIds: [...new Set(parsed.data.rootSessionIds)],
675
+ sessionIds: [...new Set(parsed.data.sessionIds)]
676
+ };
677
+ }
678
+ async function resolveSessionAuthorizationTarget(db, grant, sessionId) {
679
+ const session = await getSession(db, grant.workspaceId, sessionId);
680
+ if (!session || session.accountId !== grant.accountId) {
681
+ throw new SessionAuthorizationDeniedError("not_found");
682
+ }
683
+ let rootSessionId;
684
+ try {
685
+ rootSessionId = await getSessionRootId(db, grant.workspaceId, session.id);
686
+ } catch (error) {
687
+ throw new SessionAuthorizationUnavailableError({ cause: error });
688
+ }
689
+ if (!rootSessionId) {
690
+ throw new SessionAuthorizationDeniedError("not_found");
691
+ }
692
+ return { sessionId: session.id, rootSessionId };
693
+ }
694
+ async function resolveSessionAuthorizationActor(db, grant) {
695
+ const callerSessionId = grant.metadata?.["sessionId"];
696
+ const turnId = grant.metadata?.["turnId"];
697
+ const attemptId = grant.metadata?.["attemptId"];
698
+ const executionGeneration = grant.metadata?.["executionGeneration"];
699
+ const hasAttemptClaim = turnId !== void 0 || attemptId !== void 0 || executionGeneration !== void 0;
700
+ if (!hasAttemptClaim) {
701
+ return SessionAuthorizationActor.parse({
702
+ kind: "subject",
703
+ subjectId: grant.subjectId,
704
+ ...grant.subjectLabel ? { subjectLabel: grant.subjectLabel } : {}
705
+ });
706
+ }
707
+ if (typeof callerSessionId !== "string" || typeof turnId !== "string" || typeof attemptId !== "string" || typeof executionGeneration !== "number" || !Number.isSafeInteger(executionGeneration) || executionGeneration < 1) {
708
+ throw new SessionAuthorizationDeniedError("caller_stale");
709
+ }
710
+ const [callerSession, turn, callerRootSessionId] = await Promise.all([
711
+ getSession(db, grant.workspaceId, callerSessionId),
712
+ getSessionTurnForAttempt(db, grant.workspaceId, callerSessionId, attemptId),
713
+ getSessionRootId(db, grant.workspaceId, callerSessionId).catch(() => null)
714
+ ]);
715
+ if (!callerSession || callerSession.accountId !== grant.accountId || !turn || turn.id !== turnId || turn.executionGeneration !== executionGeneration || callerSession.activeTurnId !== turn.id || !callerRootSessionId) {
716
+ throw new SessionAuthorizationDeniedError("caller_stale");
717
+ }
718
+ return SessionAuthorizationActor.parse({
719
+ kind: "agent_attempt",
720
+ subjectId: grant.subjectId,
721
+ callerSessionId,
722
+ callerRootSessionId,
723
+ turnId,
724
+ attemptId,
725
+ executionGeneration,
726
+ initiator: turn.initiator,
727
+ initiatorContext: turn.initiatorContext
728
+ });
729
+ }
730
+
589
731
  // src/billing/limits.ts
590
732
  import { configuredStaticUsageLimits } from "@opengeni/config";
591
733
  import {
@@ -748,6 +890,12 @@ async function recordWorkspaceUsage(deps, input) {
748
890
  unit: input.unit,
749
891
  sourceResourceType: input.sourceResourceType,
750
892
  sourceResourceId: input.sourceResourceId,
893
+ sessionId: input.sessionId ?? null,
894
+ turnId: input.turnId ?? null,
895
+ turnAttemptId: input.turnAttemptId ?? null,
896
+ initiator: input.initiator ?? (input.subjectId ? { kind: "subject", subjectId: input.subjectId } : null),
897
+ ...input.initiatorContext ? { initiatorContext: input.initiatorContext } : {},
898
+ origin: input.origin ?? null,
751
899
  idempotencyKey: input.idempotencyKey
752
900
  });
753
901
  }
@@ -795,6 +943,10 @@ import {
795
943
  upsertCapabilityCatalogItem
796
944
  } from "@opengeni/db";
797
945
  import { HTTPException as HTTPException6 } from "hono/http-exception";
946
+ import {
947
+ getSkillLibraryEntry,
948
+ listSkillLibraryEntries
949
+ } from "@opengeni/runtime/skill-library";
798
950
 
799
951
  // src/domain/environments.ts
800
952
  import { environmentsEncryptionKeyBytes } from "@opengeni/config";
@@ -1117,13 +1269,15 @@ async function buildCapabilityCatalog(input) {
1117
1269
  capabilityInstallations,
1118
1270
  packInstallations,
1119
1271
  workspacePacks,
1120
- bundledSkills
1272
+ bundledSkills,
1273
+ curatedLibrarySkills
1121
1274
  ] = await Promise.all([
1122
1275
  listCapabilityCatalogItems(input.db, input.workspaceId),
1123
1276
  listCapabilityInstallations(input.db, input.workspaceId),
1124
1277
  listPackInstallations2(input.db, input.workspaceId),
1125
1278
  listWorkspaceCapabilityPacks(input.db, input.workspaceId),
1126
- discoverBundledSkills()
1279
+ discoverBundledSkills(),
1280
+ discoverCuratedSkillLibraryItems()
1127
1281
  ]);
1128
1282
  const capabilityInstallationById = new Map(
1129
1283
  capabilityInstallations.map((installation) => [installation.capabilityId, installation])
@@ -1138,7 +1292,8 @@ async function buildCapabilityCatalog(input) {
1138
1292
  ),
1139
1293
  ...configuredMcpCatalogItems(input.settings),
1140
1294
  ...platformApiCatalogItems(),
1141
- ...bundledSkills
1295
+ ...bundledSkills,
1296
+ ...curatedLibrarySkills
1142
1297
  ];
1143
1298
  const items = dedupeCatalogItems([...builtIns, ...persistedItems]).map(
1144
1299
  (item) => applyCapabilityEnablement(item, capabilityInstallationById.get(item.id), activePackIds)
@@ -1155,7 +1310,12 @@ async function createCatalogItem(input) {
1155
1310
  message: "packs are managed by OpenGeni and cannot be manually created"
1156
1311
  });
1157
1312
  }
1158
- const source = input.payload.source === "built_in" || input.payload.source === "configured" || input.payload.source === "registry" ? "manual" : input.payload.source;
1313
+ if (id.startsWith("skill:")) {
1314
+ throw new HTTPException6(422, {
1315
+ message: "skill ids are managed by the OpenGeni skill library or runtime adapters"
1316
+ });
1317
+ }
1318
+ const source = input.payload.source === "built_in" || input.payload.source === "library" || input.payload.source === "configured" || input.payload.source === "registry" ? "manual" : input.payload.source;
1159
1319
  const metadata = {
1160
1320
  ...input.payload.metadata,
1161
1321
  ...input.payload.kind === "mcp" && input.payload.endpointUrl && !input.payload.metadata.mcpServerId ? { mcpServerId: mcpServerIdForCapability(id, input.payload.metadata) } : {}
@@ -1190,11 +1350,46 @@ async function enableCapability(input) {
1190
1350
  });
1191
1351
  }
1192
1352
  let installationMetadata = input.payload.metadata;
1193
- const installationConfig = { ...input.payload.config };
1353
+ let installationConfig = { ...input.payload.config };
1194
1354
  delete installationConfig.headers;
1195
1355
  delete installationConfig.headersEncrypted;
1196
1356
  delete installationConfig.headerNames;
1197
1357
  delete installationConfig.connectionRef;
1358
+ if (item.kind === "skill" && item.source === "library") {
1359
+ const libraryId = stringMetadata(item.metadata.libraryId);
1360
+ const catalogVersion = stringMetadata(item.metadata.version);
1361
+ if (!libraryId || !catalogVersion) {
1362
+ throw new HTTPException6(422, {
1363
+ message: `skill library metadata is incomplete for ${item.id}`
1364
+ });
1365
+ }
1366
+ const requestedVersion = input.payload.config.version;
1367
+ if (requestedVersion !== void 0 && typeof requestedVersion !== "string") {
1368
+ throw new HTTPException6(422, {
1369
+ message: "skill activation config.version must be a string"
1370
+ });
1371
+ }
1372
+ const normalizedVersion = requestedVersion?.trim() || catalogVersion;
1373
+ if (normalizedVersion !== catalogVersion) {
1374
+ throw new HTTPException6(422, {
1375
+ message: `skill ${libraryId} only supports immutable version ${catalogVersion}`
1376
+ });
1377
+ }
1378
+ const entry = getSkillLibraryEntry(libraryId, normalizedVersion);
1379
+ if (!entry) {
1380
+ throw new HTTPException6(422, {
1381
+ message: `skill library entry is unavailable: ${libraryId}@${normalizedVersion}`
1382
+ });
1383
+ }
1384
+ installationConfig = { version: entry.version };
1385
+ installationMetadata = {
1386
+ libraryId: entry.id,
1387
+ libraryVersion: entry.version,
1388
+ contentSha256: entry.contentSha256,
1389
+ sourceCommit: entry.sourceCommit,
1390
+ provenance: entry.provenance
1391
+ };
1392
+ }
1198
1393
  if (item.kind === "mcp") {
1199
1394
  const headers = await resolveMcpCredentialHeaders(input, item);
1200
1395
  const connectionRef = input.payload.connectionRef ? await validateMcpCapabilityConnectionRef(input, item, input.payload.connectionRef) : null;
@@ -1353,9 +1548,11 @@ async function validateMcpCapabilityConnectionRef(input, item, ref) {
1353
1548
  providerDomain: ref.providerDomain.trim(),
1354
1549
  subjectScope: "workspace",
1355
1550
  ...ref.connectionId ? { connectionId: ref.connectionId } : {},
1551
+ ...ref.provider ? { provider: ref.provider.trim() } : {},
1356
1552
  ...ref.kind ? { kind: ref.kind } : {},
1357
1553
  ...ref.scopes ? { scopes: uniqueStrings(ref.scopes) } : {},
1358
- ...ref.resource ? { resource: ref.resource } : {}
1554
+ ...ref.resource ? { resource: ref.resource } : {},
1555
+ ...ref.selectedResources ? { selectedResources: ref.selectedResources.map((resource) => ({ ...resource })) } : {}
1359
1556
  };
1360
1557
  if (!normalized.providerDomain) {
1361
1558
  throw new HTTPException6(422, { message: "connectionRef.providerDomain is required" });
@@ -1504,12 +1701,20 @@ async function probeStreamableHttpMcpServer(input) {
1504
1701
  function mcpProbeErrorMessage(error, endpointUrl) {
1505
1702
  const message = error instanceof Error ? error.message : String(error);
1506
1703
  const normalized = message.replace(/\s+/g, " ").trim();
1704
+ const endpoint = safeEndpointLabel(endpointUrl);
1507
1705
  if (/404|405|not found|unexpected token|not valid json|invalid json|failed to parse|streamable http error|unable to connect|fetch failed|econnrefused|enotfound|timeout|aborted/i.test(
1508
1706
  normalized
1509
1707
  )) {
1510
- return `OpenGeni could not reach a valid Streamable HTTP MCP server at ${endpointUrl}. Check the endpoint URL or choose a different catalog entry.`;
1708
+ return `OpenGeni could not reach a valid Streamable HTTP MCP server at ${endpoint}. Check the endpoint URL or choose a different catalog entry.`;
1709
+ }
1710
+ return `OpenGeni could not initialize ${endpoint}. Check the endpoint configuration or try again.`;
1711
+ }
1712
+ function safeEndpointLabel(endpointUrl) {
1713
+ try {
1714
+ return new URL(endpointUrl).hostname || "the configured endpoint";
1715
+ } catch {
1716
+ return "the configured endpoint";
1511
1717
  }
1512
- return `OpenGeni could not initialize ${endpointUrl}: ${normalized.slice(0, 500) || "unknown error"}`;
1513
1718
  }
1514
1719
  async function disableCapability(input) {
1515
1720
  const item = await requireCatalogItem(
@@ -1799,6 +2004,43 @@ async function discoverBundledSkills() {
1799
2004
  return [];
1800
2005
  }
1801
2006
  }
2007
+ async function discoverCuratedSkillLibraryItems() {
2008
+ return listSkillLibraryEntries().map((entry) => curatedSkillCatalogItem(entry));
2009
+ }
2010
+ function curatedSkillCatalogItem(entry) {
2011
+ return CapabilityCatalogItem.parse({
2012
+ id: `skill:${entry.id}`,
2013
+ kind: "skill",
2014
+ source: "library",
2015
+ name: entry.name,
2016
+ description: entry.description,
2017
+ category: entry.category,
2018
+ tags: [...entry.tags],
2019
+ homepageUrl: entry.sourceUrl,
2020
+ provenance: entry.provenance,
2021
+ tier: "verified",
2022
+ runtime: {
2023
+ available: true,
2024
+ notes: "Available as an explicit immutable opt-in skill selection."
2025
+ },
2026
+ metadata: {
2027
+ libraryId: entry.id,
2028
+ version: entry.version,
2029
+ contentSha256: entry.contentSha256,
2030
+ sourceCommit: entry.sourceCommit,
2031
+ sourceUrl: entry.sourceUrl,
2032
+ provenance: entry.provenance,
2033
+ license: entry.license,
2034
+ documentationUrl: entry.documentationUrl,
2035
+ compatibility: entry.compatibility,
2036
+ upgrade: entry.upgrade,
2037
+ artifactPath: entry.relativePath
2038
+ }
2039
+ });
2040
+ }
2041
+ function stringMetadata(value) {
2042
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
2043
+ }
1802
2044
  async function readSkillMetadata(url, fallbackName) {
1803
2045
  const content = await readFile(url, "utf8");
1804
2046
  const frontMatter = content.match(/^---\n([\s\S]*?)\n---/);
@@ -1827,6 +2069,15 @@ function applyCapabilityEnablement(item, installation, activePackIds) {
1827
2069
  enabledReason: item.source === "configured" ? "configured" : "built in"
1828
2070
  };
1829
2071
  }
2072
+ if (item.source === "library") {
2073
+ const enabled2 = installation?.status === "active" && skillLibraryInstallationRuntimeReady(item, installation);
2074
+ return {
2075
+ ...item,
2076
+ enabled: enabled2,
2077
+ enabledReason: enabled2 ? "explicitly selected" : null,
2078
+ connectionRef: null
2079
+ };
2080
+ }
1830
2081
  const activeInstallation = installation?.status === "active";
1831
2082
  const enabled = !!activeInstallation && capabilityInstallationRuntimeReady(item, installation);
1832
2083
  return {
@@ -1836,6 +2087,24 @@ function applyCapabilityEnablement(item, installation, activePackIds) {
1836
2087
  connectionRef: enabled && installation ? installationConnectionRef(installation.config) : null
1837
2088
  };
1838
2089
  }
2090
+ function skillLibraryInstallationRuntimeReady(item, installation) {
2091
+ if (item.kind !== "skill" || item.source !== "library" || installation.kind !== "skill") {
2092
+ return false;
2093
+ }
2094
+ const libraryId = stringMetadata(item.metadata.libraryId);
2095
+ const version = stringMetadata(item.metadata.version);
2096
+ const contentSha256 = stringMetadata(item.metadata.contentSha256);
2097
+ const sourceCommit = stringMetadata(item.metadata.sourceCommit);
2098
+ const provenance = stringMetadata(item.metadata.provenance);
2099
+ if (!libraryId || !version || !contentSha256 || !sourceCommit || !provenance) {
2100
+ return false;
2101
+ }
2102
+ const entry = getSkillLibraryEntry(libraryId, version);
2103
+ if (!entry || item.id !== `skill:${entry.id}` || installation.capabilityId !== `skill:${entry.id}` || contentSha256 !== entry.contentSha256 || sourceCommit !== entry.sourceCommit || provenance !== entry.provenance) {
2104
+ return false;
2105
+ }
2106
+ return installation.config.version === entry.version && stringMetadata(installation.metadata.libraryId) === entry.id && stringMetadata(installation.metadata.libraryVersion) === entry.version && stringMetadata(installation.metadata.contentSha256) === entry.contentSha256 && stringMetadata(installation.metadata.sourceCommit) === entry.sourceCommit && stringMetadata(installation.metadata.provenance) === entry.provenance;
2107
+ }
1839
2108
  function installationConnectionRef(config) {
1840
2109
  const ref = config.connectionRef;
1841
2110
  if (!ref || typeof ref !== "object") {
@@ -1850,6 +2119,14 @@ function installationConnectionRef(config) {
1850
2119
  function dedupeCatalogItems(items) {
1851
2120
  const byId = /* @__PURE__ */ new Map();
1852
2121
  for (const item of items) {
2122
+ if (item.kind === "skill" && item.source === "library" && byId.has(item.id)) {
2123
+ byId.set(item.id, item);
2124
+ continue;
2125
+ }
2126
+ const existing = byId.get(item.id);
2127
+ if (existing?.kind === "skill" && existing.source === "library") {
2128
+ continue;
2129
+ }
1853
2130
  byId.set(item.id, item);
1854
2131
  }
1855
2132
  return [...byId.values()];
@@ -2377,13 +2654,20 @@ async function listRigChangesForApi(deps, workspaceId, rigId, limit) {
2377
2654
 
2378
2655
  // src/domain/resources.ts
2379
2656
  import {
2657
+ gitCredentialBindingIdForRepository,
2658
+ gitCredentialProviderForRepository,
2659
+ defaultRepositoryMountPath,
2380
2660
  mergeResourceRefs as mergeContractResourceRefs,
2381
2661
  mergeToolRefs,
2662
+ normalizeRepositorySubpath,
2663
+ normalizeResourceMountPath,
2382
2664
  resourceIdentityKey,
2665
+ resourceMountPathCollisionKey,
2383
2666
  ResourceRefConflictError,
2667
+ ResourceMountPathError,
2384
2668
  stableJson
2385
2669
  } from "@opengeni/contracts";
2386
- import { listGitHubInstallationIdsForWorkspace, requireFile } from "@opengeni/db";
2670
+ import { areGitHubRepositoriesAllowedForWorkspace, requireFile } from "@opengeni/db";
2387
2671
  import { HTTPException as HTTPException8 } from "hono/http-exception";
2388
2672
  function validateToolRefs(tools, settings) {
2389
2673
  const mcpServerIds = new Set(settings.mcpServers.map((server) => server.id));
@@ -2414,9 +2698,26 @@ function enabledCapabilityMcpToolRefs(settings, runtimeSettings) {
2414
2698
  function withDefaultEnabledCapabilityMcpTools(tools, settings, runtimeSettings) {
2415
2699
  return mergeToolRefs(tools, enabledCapabilityMcpToolRefs(settings, runtimeSettings));
2416
2700
  }
2701
+ function availableToolRefs(tools, settings) {
2702
+ const available = new Set(settings.mcpServers.map((server) => server.id));
2703
+ return tools.filter((tool) => available.has(tool.id));
2704
+ }
2705
+ function assertToolRefsSubset(requested, allowed, message = "requested tools exceed the session tool policy") {
2706
+ const allowedIds = new Set(allowed.map((tool) => `${tool.kind}:${tool.id}`));
2707
+ const widened = requested.find((tool) => !allowedIds.has(`${tool.kind}:${tool.id}`));
2708
+ if (widened) {
2709
+ throw new HTTPException8(403, { message: `${message}: ${widened.id}` });
2710
+ }
2711
+ }
2712
+ function validateToolRefsForSessionPolicy(input) {
2713
+ const validated = validateToolRefs(input.requested, input.settings);
2714
+ assertToolRefsSubset(validated, input.allowedTools, input.message);
2715
+ return validated;
2716
+ }
2417
2717
  function normalizeResources(resources) {
2418
2718
  const mountPaths = /* @__PURE__ */ new Map();
2419
2719
  const identities = /* @__PURE__ */ new Map();
2720
+ const credentialBindingProviders = /* @__PURE__ */ new Map();
2420
2721
  const seenResources = /* @__PURE__ */ new Set();
2421
2722
  const out = [];
2422
2723
  for (const resource of resources) {
@@ -2439,14 +2740,35 @@ function normalizeResources(resources) {
2439
2740
  throw new HTTPException8(422, { message: "repository URL must include owner and repo" });
2440
2741
  }
2441
2742
  const repo = parts.join("/");
2442
- const mountPath = normalizeMountPath(resource.mountPath ?? `repos/${repo}`);
2743
+ const normalizedUri = `https://${url.host.toLowerCase()}/${repo}.git`;
2744
+ const mountPath = normalizeMountPath(
2745
+ resource.mountPath ?? defaultRepositoryMountPath(normalizedUri)
2746
+ );
2747
+ const credentialProvider = gitCredentialProviderForRepository(resource);
2748
+ const credentialBindingId = gitCredentialBindingIdForRepository(resource, credentialProvider);
2749
+ if ((resource.credentialBindingId || resource.connectionId || resource.access) && !credentialProvider) {
2750
+ throw new HTTPException8(422, {
2751
+ message: "repository credential bindings and access intent require a Git provider"
2752
+ });
2753
+ }
2754
+ if (credentialProvider && credentialBindingId) {
2755
+ const boundProvider = credentialBindingProviders.get(credentialBindingId);
2756
+ if (boundProvider && boundProvider !== credentialProvider) {
2757
+ throw new HTTPException8(422, {
2758
+ message: `credential binding ${credentialBindingId} is assigned to multiple Git providers`
2759
+ });
2760
+ }
2761
+ credentialBindingProviders.set(credentialBindingId, credentialProvider);
2762
+ }
2443
2763
  normalized = {
2444
2764
  kind: "repository",
2445
- uri: `https://${url.hostname.toLowerCase()}/${repo}.git`,
2765
+ uri: normalizedUri,
2446
2766
  ref: resource.ref.trim(),
2447
2767
  mountPath,
2448
- ...resource.subpath ? { subpath: normalizeMountPath(resource.subpath) } : {},
2768
+ ...resource.subpath ? { subpath: normalizeRepositorySubpath(resource.subpath) } : {},
2449
2769
  ...resource.provider ? { provider: resource.provider } : {},
2770
+ ...resource.credentialBindingId ? { credentialBindingId: resource.credentialBindingId } : {},
2771
+ ...resource.access ? { access: resource.access } : {},
2450
2772
  ...resource.repositoryId !== void 0 ? { repositoryId: resource.repositoryId } : {},
2451
2773
  ...resource.installationId !== void 0 ? { installationId: resource.installationId } : {},
2452
2774
  ...resource.projectId !== void 0 ? { projectId: resource.projectId } : {},
@@ -2456,14 +2778,15 @@ function normalizeResources(resources) {
2456
2778
  };
2457
2779
  }
2458
2780
  const key = stableJson(normalized);
2459
- const mounted = normalized.mountPath ? mountPaths.get(normalized.mountPath) : void 0;
2781
+ const mountCollisionKey = normalized.mountPath ? resourceMountPathCollisionKey(normalized.mountPath) : void 0;
2782
+ const mounted = mountCollisionKey ? mountPaths.get(mountCollisionKey) : void 0;
2460
2783
  if (mounted && mounted !== key) {
2461
2784
  throw new HTTPException8(422, {
2462
2785
  message: `duplicate resource mount path: ${normalized.mountPath}`
2463
2786
  });
2464
2787
  }
2465
2788
  if (normalized.mountPath) {
2466
- mountPaths.set(normalized.mountPath, key);
2789
+ mountPaths.set(mountCollisionKey, key);
2467
2790
  }
2468
2791
  const identity = resourceIdentityKey(normalized);
2469
2792
  const seenIdentity = identities.get(identity);
@@ -2490,8 +2813,24 @@ function mergeResourceRefs(existing, additions) {
2490
2813
  throw error;
2491
2814
  }
2492
2815
  }
2816
+ function validateGitHubRepositorySelectionShapes(resources) {
2817
+ const selected = gitHubRepositorySelections(resources);
2818
+ if (selected.length === 0) {
2819
+ return [];
2820
+ }
2821
+ return [...new Set(selected.map((item) => item.installationId))];
2822
+ }
2493
2823
  function validateGitHubRepositorySelectionShape(resources) {
2494
- const selected = resources.flatMap((resource) => {
2824
+ const installationIds = validateGitHubRepositorySelectionShapes(resources);
2825
+ if (installationIds.length > 1) {
2826
+ throw new HTTPException8(422, {
2827
+ message: "GitHub App repository resources must belong to one installation"
2828
+ });
2829
+ }
2830
+ return installationIds[0] ?? null;
2831
+ }
2832
+ function gitHubRepositorySelections(resources) {
2833
+ return resources.flatMap((resource) => {
2495
2834
  if (resource.kind !== "repository") {
2496
2835
  return [];
2497
2836
  }
@@ -2503,38 +2842,34 @@ function validateGitHubRepositorySelectionShape(resources) {
2503
2842
  if (installationRaw === void 0 && repositoryRaw === void 0) {
2504
2843
  return [];
2505
2844
  }
2506
- const installationId2 = positiveInteger(installationRaw);
2845
+ const installationId = positiveInteger(installationRaw);
2507
2846
  const repositoryId = positiveInteger(repositoryRaw);
2508
- if (!installationId2 || !repositoryId) {
2847
+ if (!installationId || !repositoryId) {
2509
2848
  throw new HTTPException8(422, {
2510
2849
  message: "GitHub App repository resources require positive github_installation_id and github_repository_id"
2511
2850
  });
2512
2851
  }
2513
- return [{ installationId: installationId2, repositoryId }];
2852
+ return [{ installationId, repositoryId }];
2514
2853
  });
2515
- if (selected.length === 0) {
2516
- return null;
2517
- }
2518
- const installationId = selected[0].installationId;
2519
- if (selected.some((item) => item.installationId !== installationId)) {
2520
- throw new HTTPException8(422, {
2521
- message: "GitHub App repository resources must belong to one installation"
2522
- });
2523
- }
2524
- return installationId;
2525
2854
  }
2526
2855
  async function validateGitHubRepositorySelection(db, workspaceId, resources) {
2527
- const installationId = validateGitHubRepositorySelectionShape(resources);
2528
- if (installationId === null) {
2856
+ const installationIds = validateGitHubRepositorySelectionShapes(resources);
2857
+ if (installationIds.length === 0) {
2529
2858
  return;
2530
2859
  }
2531
- const linkedInstallationIds = new Set(
2532
- await listGitHubInstallationIdsForWorkspace(db, workspaceId)
2533
- );
2534
- if (!linkedInstallationIds.has(installationId)) {
2535
- throw new HTTPException8(422, {
2536
- message: "GitHub App repository resources must belong to a GitHub App installation linked to this workspace"
2537
- });
2860
+ const selections = gitHubRepositorySelections(resources);
2861
+ for (const installationId of installationIds) {
2862
+ const repositoryIds = selections.filter((selection) => selection.installationId === installationId).map((selection) => selection.repositoryId);
2863
+ if (!await areGitHubRepositoriesAllowedForWorkspace(
2864
+ db,
2865
+ workspaceId,
2866
+ installationId,
2867
+ repositoryIds
2868
+ )) {
2869
+ throw new HTTPException8(422, {
2870
+ message: "GitHub App repository resources must be authorized for a GitHub App installation linked to this workspace"
2871
+ });
2872
+ }
2538
2873
  }
2539
2874
  }
2540
2875
  async function validateFileResources(db, workspaceId, resources) {
@@ -2559,11 +2894,12 @@ async function validateFileResources(db, workspaceId, resources) {
2559
2894
  }
2560
2895
  }
2561
2896
  function normalizeMountPath(path) {
2562
- const normalized = path.trim().replace(/^\/+|\/+$/g, "");
2563
- if (!normalized || normalized.includes("..")) {
2897
+ try {
2898
+ return normalizeResourceMountPath(path);
2899
+ } catch (error) {
2900
+ if (!(error instanceof ResourceMountPathError)) throw error;
2564
2901
  throw new HTTPException8(422, { message: `invalid resource mount path: ${path}` });
2565
2902
  }
2566
- return normalized;
2567
2903
  }
2568
2904
  function parseResourceUrl(uri) {
2569
2905
  try {
@@ -2582,6 +2918,128 @@ function positiveInteger(value) {
2582
2918
  return null;
2583
2919
  }
2584
2920
 
2921
+ // src/domain/session-tool-policy.ts
2922
+ import {
2923
+ SESSION_EFFECTIVE_TOOL_POLICY_ID_LIMIT,
2924
+ SESSION_EFFECTIVE_TOOL_POLICY_ID_MAX_LENGTH,
2925
+ mergeToolRefs as mergeToolRefs2
2926
+ } from "@opengeni/contracts";
2927
+ var MANDATORY_SESSION_MCP_SERVER_IDS = ["opengeni"];
2928
+ var PROJECTABLE_REGISTRY_ID = /^[A-Za-z0-9_-]+$/;
2929
+ function sortedIds(ids) {
2930
+ return [...new Set(ids)].sort();
2931
+ }
2932
+ function projectIds(ids) {
2933
+ const projectable = ids.filter(
2934
+ (id) => id.length <= SESSION_EFFECTIVE_TOOL_POLICY_ID_MAX_LENGTH && PROJECTABLE_REGISTRY_ID.test(id)
2935
+ );
2936
+ return {
2937
+ ids: projectable.slice(0, SESSION_EFFECTIVE_TOOL_POLICY_ID_LIMIT),
2938
+ truncated: projectable.length !== ids.length || projectable.length > SESSION_EFFECTIVE_TOOL_POLICY_ID_LIMIT
2939
+ };
2940
+ }
2941
+ function resolveSessionToolPolicy(input) {
2942
+ const policy = input.toolPolicy ?? { mode: "legacy", inheritedFromSessionId: null };
2943
+ const availableIds = new Set(input.availableMcpServerIds);
2944
+ const defaultIds = new Set(input.defaultMcpServerIds ?? []);
2945
+ const mandatoryIds = MANDATORY_SESSION_MCP_SERVER_IDS.filter(
2946
+ (id) => availableIds.has(id)
2947
+ );
2948
+ const mandatoryIdSet = new Set(mandatoryIds);
2949
+ const selectedRefs = input.turnToolsProvided === true ? mergeToolRefs2([], input.turnTools ?? []) : input.turnToolsProvided === false ? mergeToolRefs2([], input.sessionTools) : mergeToolRefs2(input.sessionTools, input.turnTools ?? []);
2950
+ const tracksWorkspaceDefaults = policy.mode === "workspace_default" && input.turnToolsProvided !== true;
2951
+ let toolRefs = selectedRefs.filter((tool) => tool.optional !== true || availableIds.has(tool.id));
2952
+ if (tracksWorkspaceDefaults) {
2953
+ toolRefs = mergeToolRefs2(
2954
+ toolRefs,
2955
+ sortedIds(defaultIds).filter((id) => availableIds.has(id)).map((id) => ({ kind: "mcp", id, optional: true }))
2956
+ );
2957
+ }
2958
+ toolRefs = mergeToolRefs2(
2959
+ toolRefs,
2960
+ mandatoryIds.map((id) => ({ kind: "mcp", id }))
2961
+ );
2962
+ const requestedEffectiveRefs = mergeToolRefs2(
2963
+ selectedRefs,
2964
+ tracksWorkspaceDefaults ? sortedIds(defaultIds).filter((id) => availableIds.has(id)).map((id) => ({ kind: "mcp", id, optional: true })) : []
2965
+ );
2966
+ const effectiveIds = sortedIds(
2967
+ mergeToolRefs2(
2968
+ requestedEffectiveRefs,
2969
+ mandatoryIds.map((id) => ({ kind: "mcp", id }))
2970
+ ).map((tool) => tool.id)
2971
+ );
2972
+ const configuredIds = effectiveIds.filter((id) => availableIds.has(id));
2973
+ const configuredIdSet = new Set(configuredIds);
2974
+ const droppedIds = effectiveIds.filter((id) => !configuredIdSet.has(id));
2975
+ const deferredIds = tracksWorkspaceDefaults ? sortedIds(
2976
+ toolRefs.filter(
2977
+ (tool) => tool.optional === true && configuredIdSet.has(tool.id) && !mandatoryIdSet.has(tool.id)
2978
+ ).map((tool) => tool.id)
2979
+ ) : [];
2980
+ const selectedIds = sortedIds(
2981
+ selectedRefs.filter(
2982
+ (tool) => !mandatoryIdSet.has(tool.id) && !(tracksWorkspaceDefaults && tool.optional === true)
2983
+ ).map((tool) => tool.id)
2984
+ );
2985
+ const projections = {
2986
+ selected: projectIds(selectedIds),
2987
+ effective: projectIds(effectiveIds),
2988
+ mandatory: projectIds(sortedIds(mandatoryIds)),
2989
+ deferred: projectIds(deferredIds),
2990
+ configured: projectIds(configuredIds),
2991
+ dropped: projectIds(droppedIds)
2992
+ };
2993
+ return {
2994
+ toolRefs,
2995
+ effectivePolicy: {
2996
+ mode: policy.mode,
2997
+ inheritedFromSessionId: policy.inheritedFromSessionId,
2998
+ selectedIds: projections.selected.ids,
2999
+ effectiveIds: projections.effective.ids,
3000
+ mandatoryIds: projections.mandatory.ids,
3001
+ lazyRouter: {
3002
+ state: tracksWorkspaceDefaults ? "required" : "disabled",
3003
+ deferredIds: projections.deferred.ids
3004
+ },
3005
+ configuredIds: projections.configured.ids,
3006
+ droppedIds: projections.dropped.ids,
3007
+ counts: {
3008
+ selected: selectedIds.length,
3009
+ effective: effectiveIds.length,
3010
+ mandatory: mandatoryIds.length,
3011
+ deferred: deferredIds.length,
3012
+ configured: configuredIds.length,
3013
+ dropped: droppedIds.length
3014
+ },
3015
+ idsTruncated: Object.values(projections).some((projection) => projection.truncated)
3016
+ }
3017
+ };
3018
+ }
3019
+ async function workspaceSessionToolPolicyServerIds(db, workspaceId, settings) {
3020
+ const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
3021
+ return sortedIds(runtimeSettings.mcpServers.map((server) => server.id));
3022
+ }
3023
+ async function workspaceSessionToolPolicyDefaultServerIds(db, workspaceId, settings) {
3024
+ const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
3025
+ return sortedIds(enabledCapabilityMcpToolRefs(settings, runtimeSettings).map((tool) => tool.id));
3026
+ }
3027
+ function sessionWithEffectiveToolPolicy(session, workspaceServerIds, workspaceDefaultServerIds = []) {
3028
+ const availableIds = new Set(workspaceServerIds);
3029
+ for (const server of session.mcpServers) {
3030
+ availableIds.add(server.id);
3031
+ }
3032
+ return {
3033
+ ...session,
3034
+ effectiveToolPolicy: resolveSessionToolPolicy({
3035
+ ...session.toolPolicy ? { toolPolicy: session.toolPolicy } : {},
3036
+ sessionTools: session.tools,
3037
+ availableMcpServerIds: availableIds,
3038
+ defaultMcpServerIds: workspaceDefaultServerIds
3039
+ }).effectivePolicy
3040
+ };
3041
+ }
3042
+
2585
3043
  // src/domain/scheduled-tasks.ts
2586
3044
  import {
2587
3045
  createScheduledTask,
@@ -2594,11 +3052,20 @@ import { HTTPException as HTTPException10 } from "hono/http-exception";
2594
3052
 
2595
3053
  // src/domain/sessions.ts
2596
3054
  import { CODEX_MODEL_ID_PREFIX } from "@opengeni/codex";
2597
- import { configuredAllowedModels, policyProviderIdForModel } from "@opengeni/config";
3055
+ import {
3056
+ canonicalizeConfiguredModelId,
3057
+ configuredAllowedModels,
3058
+ policyProviderIdForModel,
3059
+ resolveTurnExecutionPolicyV1
3060
+ } from "@opengeni/config";
2598
3061
  import {
2599
3062
  CreateSessionRequest,
3063
+ DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
3064
+ ServiceTurnInitiator,
3065
+ ServiceTurnInitiatorContext,
2600
3066
  evaluateWorkspaceModelPolicy,
2601
- reasoningEffortForMetadata
3067
+ reasoningEffortForMetadata,
3068
+ SessionMcpApprovalPolicy
2602
3069
  } from "@opengeni/contracts";
2603
3070
  import {
2604
3071
  createSession,
@@ -2611,7 +3078,8 @@ import {
2611
3078
  listDistinctVariableSetIdsInGroup,
2612
3079
  listDistinctRigVersionIdsInGroup,
2613
3080
  getSandbox as getSandbox3,
2614
- getSession,
3081
+ getSession as getSession2,
3082
+ SessionIdConflictError,
2615
3083
  getSessionByCreateIdempotencyKey,
2616
3084
  getSessionEvent,
2617
3085
  getWorkspaceControlEvent,
@@ -2619,11 +3087,15 @@ import {
2619
3087
  getSessionTurn,
2620
3088
  getWorkspaceModelPolicy,
2621
3089
  initializeSessionStartAtomically,
3090
+ listSessionTurns,
3091
+ listSessionMcpServersForChildInheritance,
2622
3092
  requireSession as requireSession2,
2623
3093
  submitHumanPromptInTransaction,
3094
+ appendSessionEventsWithLockedSessionUpdate,
2624
3095
  updateSessionTitle as updateSessionTitleRow,
2625
3096
  withWorkspaceSubjectRls,
2626
3097
  QueueCommandConflictError,
3098
+ AgentCommandAuthorityError,
2627
3099
  SessionControlConflictError
2628
3100
  } from "@opengeni/db";
2629
3101
  import {
@@ -2636,6 +3108,72 @@ var reservedSessionMcpServerIds = /* @__PURE__ */ new Set(["opengeni", "files",
2636
3108
  var maxSessionMcpCredentialHeaders = 16;
2637
3109
  var maxSessionMcpCredentialHeaderValueLength = 4096;
2638
3110
  var sessionMcpCredentialHeaderName = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
3111
+ function serviceInitiatorForGrant(grant) {
3112
+ if (!grant.serviceInitiator) {
3113
+ if (grant.serviceInitiatorContext) {
3114
+ throw new HTTPException9(403, {
3115
+ message: "service initiator context requires a signed service initiator"
3116
+ });
3117
+ }
3118
+ return null;
3119
+ }
3120
+ const initiator = ServiceTurnInitiator.safeParse(grant.serviceInitiator);
3121
+ if (!initiator.success) {
3122
+ throw new HTTPException9(403, {
3123
+ message: "a delegated command initiator must be a bounded service principal"
3124
+ });
3125
+ }
3126
+ const context = ServiceTurnInitiatorContext.safeParse(grant.serviceInitiatorContext ?? {});
3127
+ if (!context.success) {
3128
+ throw new HTTPException9(403, {
3129
+ message: "delegated service initiator context is invalid or reserved"
3130
+ });
3131
+ }
3132
+ const callerTurnId = grant.metadata?.["turnId"];
3133
+ const callerAttemptId = grant.metadata?.["attemptId"];
3134
+ const callerExecutionGeneration = grant.metadata?.["executionGeneration"];
3135
+ if (callerTurnId !== void 0 || callerAttemptId !== void 0 || callerExecutionGeneration !== void 0) {
3136
+ throw new HTTPException9(403, {
3137
+ message: "a service initiator cannot replace an exact agent-attempt initiator"
3138
+ });
3139
+ }
3140
+ return {
3141
+ initiator: initiator.data,
3142
+ context: context.data
3143
+ };
3144
+ }
3145
+ function creationInitiatorForGrant(grant) {
3146
+ const serviceInitiator = serviceInitiatorForGrant(grant);
3147
+ const callerSessionId = grant.metadata?.["sessionId"];
3148
+ const callerTurnId = grant.metadata?.["turnId"];
3149
+ const callerAttemptId = grant.metadata?.["attemptId"];
3150
+ const callerExecutionGeneration = grant.metadata?.["executionGeneration"];
3151
+ const hasCallerTurnClaim = callerTurnId !== void 0 || callerAttemptId !== void 0 || callerExecutionGeneration !== void 0;
3152
+ if (hasCallerTurnClaim) {
3153
+ if (typeof callerSessionId !== "string" || typeof callerTurnId !== "string" || typeof callerAttemptId !== "string" || typeof callerExecutionGeneration !== "number" || !Number.isSafeInteger(callerExecutionGeneration) || callerExecutionGeneration < 1) {
3154
+ throw new HTTPException9(403, { message: "caller attempt claims are incomplete" });
3155
+ }
3156
+ const actor = {
3157
+ type: "agent_attempt",
3158
+ sessionId: callerSessionId,
3159
+ turnId: callerTurnId,
3160
+ attemptId: callerAttemptId,
3161
+ executionGeneration: callerExecutionGeneration
3162
+ };
3163
+ return { actor };
3164
+ }
3165
+ if (serviceInitiator) {
3166
+ return serviceInitiator;
3167
+ }
3168
+ return {
3169
+ initiator: {
3170
+ kind: "subject",
3171
+ subjectId: grant.subjectId,
3172
+ ...grant.subjectLabel ? { label: grant.subjectLabel } : {}
3173
+ },
3174
+ context: {}
3175
+ };
3176
+ }
2639
3177
  function normalizedSessionMcpCredentialHeaders(headers) {
2640
3178
  if (!headers) {
2641
3179
  return {};
@@ -2677,7 +3215,20 @@ function mcpServerConfigFromInput(server) {
2677
3215
  ...server.allowedTools ? { allowedTools: server.allowedTools } : {},
2678
3216
  ...server.timeoutMs ? { timeoutMs: server.timeoutMs } : {},
2679
3217
  cacheToolsList: server.cacheToolsList ?? false,
2680
- ...server.requireApproval !== void 0 ? { requireApproval: server.requireApproval } : {}
3218
+ ...server.requireApproval !== void 0 ? { requireApproval: server.requireApproval } : {},
3219
+ ...server.connectionRef ? { connectionRef: server.connectionRef } : {}
3220
+ };
3221
+ }
3222
+ function mcpServerConfigFromStoredInput(server) {
3223
+ return {
3224
+ id: server.id,
3225
+ ...server.name ? { name: server.name } : {},
3226
+ url: server.url,
3227
+ ...server.allowedTools ? { allowedTools: server.allowedTools } : {},
3228
+ ...server.timeoutMs ? { timeoutMs: server.timeoutMs } : {},
3229
+ cacheToolsList: server.cacheToolsList ?? false,
3230
+ ...server.requireApproval != null ? { requireApproval: server.requireApproval } : {},
3231
+ ...server.connectionRef ? { connectionRef: server.connectionRef } : {}
2681
3232
  };
2682
3233
  }
2683
3234
  function mcpServerConfigFromMetadata(server) {
@@ -2685,7 +3236,9 @@ function mcpServerConfigFromMetadata(server) {
2685
3236
  id: server.id,
2686
3237
  ...server.name ? { name: server.name } : {},
2687
3238
  url: server.url,
2688
- cacheToolsList: false
3239
+ cacheToolsList: false,
3240
+ requireApproval: server.requireApproval,
3241
+ ...server.connectionRef ? { connectionRef: server.connectionRef } : {}
2689
3242
  };
2690
3243
  }
2691
3244
  function settingsWithSessionMcpServerConfigs(settings, servers) {
@@ -2706,7 +3259,7 @@ function validateSessionMcpServersForCreate(settings, grant, servers) {
2706
3259
  return { runtimeServers: [], dbServers: [], metadata: [] };
2707
3260
  }
2708
3261
  requirePermission(grant, "mcp_servers:attach");
2709
- const encryptionKey = requireVariableSetEncryption(settings);
3262
+ const encryptionKey = servers.some((server) => Object.keys(server.headers ?? {}).length > 0) ? requireVariableSetEncryption(settings) : null;
2710
3263
  const existingIds = new Set(settings.mcpServers.map((server) => server.id));
2711
3264
  const seenIds = /* @__PURE__ */ new Set();
2712
3265
  const runtimeServers = [];
@@ -2736,6 +3289,7 @@ function validateSessionMcpServersForCreate(settings, grant, servers) {
2736
3289
  timeoutMs: server.timeoutMs ?? null,
2737
3290
  cacheToolsList: server.cacheToolsList ?? false,
2738
3291
  requireApproval: server.requireApproval ?? null,
3292
+ connectionRef: server.connectionRef ?? null,
2739
3293
  headersEncrypted
2740
3294
  });
2741
3295
  metadata.push({
@@ -2743,11 +3297,48 @@ function validateSessionMcpServersForCreate(settings, grant, servers) {
2743
3297
  name: server.name ?? null,
2744
3298
  url: server.url,
2745
3299
  headerNames: Object.keys(headersEncrypted).sort(),
2746
- credentialVersion: 1
3300
+ credentialVersion: 1,
3301
+ requireApproval: server.requireApproval ?? false,
3302
+ connectionRef: server.connectionRef ?? null
2747
3303
  });
2748
3304
  }
2749
3305
  return { runtimeServers, dbServers, metadata };
2750
3306
  }
3307
+ function validateInheritedSessionMcpServersForCreate(servers) {
3308
+ if (servers.length === 0) {
3309
+ return { runtimeServers: [], dbServers: [], metadata: [] };
3310
+ }
3311
+ const seenIds = /* @__PURE__ */ new Set();
3312
+ for (const server of servers) {
3313
+ if (seenIds.has(server.id)) {
3314
+ throw new HTTPException9(422, {
3315
+ message: `duplicate inherited session MCP server id: ${server.id}`
3316
+ });
3317
+ }
3318
+ seenIds.add(server.id);
3319
+ if (reservedSessionMcpServerIds.has(server.id)) {
3320
+ throw new HTTPException9(422, {
3321
+ message: `reserved inherited session MCP server id: ${server.id}`
3322
+ });
3323
+ }
3324
+ }
3325
+ return {
3326
+ runtimeServers: servers.map(mcpServerConfigFromStoredInput),
3327
+ dbServers: servers.map((server) => ({
3328
+ ...server,
3329
+ headersEncrypted: { ...server.headersEncrypted ?? {} }
3330
+ })),
3331
+ metadata: servers.map((server) => ({
3332
+ id: server.id,
3333
+ name: server.name ?? null,
3334
+ url: server.url,
3335
+ headerNames: Object.keys(server.headersEncrypted ?? {}).sort(),
3336
+ credentialVersion: 1,
3337
+ requireApproval: server.requireApproval ?? false,
3338
+ connectionRef: server.connectionRef ?? null
3339
+ }))
3340
+ };
3341
+ }
2751
3342
  function validateSessionMcpCredentialUpdates(input) {
2752
3343
  if (input.updates.length === 0) {
2753
3344
  return [];
@@ -2792,18 +3383,27 @@ async function createAndStartSession(input) {
2792
3383
  input.createIdempotencyKey
2793
3384
  );
2794
3385
  if (existing) {
3386
+ if (input.requestedSessionId && existing.id !== input.requestedSessionId) {
3387
+ throw new SessionIdConflictError(input.requestedSessionId);
3388
+ }
2795
3389
  return await finishStartSession(
2796
3390
  existing.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
2797
3391
  existing
2798
3392
  );
2799
3393
  }
2800
3394
  const { session: keyed, created } = await createSessionWithIdempotencyKey(input.db, {
3395
+ ...input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {},
2801
3396
  accountId: input.accountId,
2802
3397
  workspaceId: input.workspaceId,
2803
3398
  initialMessage: input.initialMessage,
3399
+ initialTurnInstructions: input.turnInstructions ?? null,
2804
3400
  resources: input.resources,
2805
3401
  tools: input.tools,
3402
+ ...input.toolPolicy ? { toolPolicy: input.toolPolicy } : {},
2806
3403
  metadata: sessionMetadata,
3404
+ ...input.createdBy ? { createdBy: input.createdBy } : {},
3405
+ ...input.createdByContext ? { createdByContext: input.createdByContext } : {},
3406
+ createdByActor: input.createdByActor ?? null,
2807
3407
  model: input.model,
2808
3408
  sandboxBackend: input.sandboxBackend,
2809
3409
  variableSetId: input.variableSet?.id ?? null,
@@ -2826,12 +3426,18 @@ async function createAndStartSession(input) {
2826
3426
  return await finishStartSession(input, keyed);
2827
3427
  }
2828
3428
  const session = await createSession(input.db, {
3429
+ ...input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {},
2829
3430
  accountId: input.accountId,
2830
3431
  workspaceId: input.workspaceId,
2831
3432
  initialMessage: input.initialMessage,
3433
+ initialTurnInstructions: input.turnInstructions ?? null,
2832
3434
  resources: input.resources,
2833
3435
  tools: input.tools,
3436
+ ...input.toolPolicy ? { toolPolicy: input.toolPolicy } : {},
2834
3437
  metadata: sessionMetadata,
3438
+ ...input.createdBy ? { createdBy: input.createdBy } : {},
3439
+ ...input.createdByContext ? { createdByContext: input.createdByContext } : {},
3440
+ createdByActor: input.createdByActor ?? null,
2835
3441
  model: input.model,
2836
3442
  sandboxBackend: input.sandboxBackend,
2837
3443
  variableSetId: input.variableSet?.id ?? null,
@@ -2880,7 +3486,9 @@ async function finishStartSession(input, session) {
2880
3486
  sessionId: session.id,
2881
3487
  ...input.clientEventId ? { clientEventId: input.clientEventId } : {},
2882
3488
  reasoningEffortFallback: input.reasoningEffort,
3489
+ turnExecutionPolicy: input.turnExecutionPolicy,
2883
3490
  createdEventPayload: {
3491
+ ...input.toolPolicy ? { toolPolicy: input.toolPolicy } : {},
2884
3492
  ...input.variableSet ? { variableSetId: input.variableSet.id, variableSetName: input.variableSet.name } : {},
2885
3493
  ...input.sessionMcpServers?.length ? { mcpServers: input.sessionMcpServers } : {}
2886
3494
  },
@@ -2900,36 +3508,49 @@ async function finishStartSession(input, session) {
2900
3508
  wakeRevision: started.workflowWakeRevision
2901
3509
  });
2902
3510
  }
2903
- return await requireSession2(input.db, session.workspaceId, session.id);
3511
+ const persisted = await requireSession2(input.db, session.workspaceId, session.id);
3512
+ const initialTurnId = started.turn?.id ?? (await listSessionTurns(input.db, session.workspaceId, session.id, 1))[0]?.id ?? null;
3513
+ return { ...persisted, initialTurnId };
2904
3514
  }
2905
3515
  function workflowIdForSession(sessionId) {
2906
3516
  return `session-${sessionId}`;
2907
3517
  }
2908
- function assertConfiguredModel(settings, model) {
3518
+ function canonicalConfiguredModel(settings, model) {
2909
3519
  if (model === null || model === void 0) {
2910
- return;
3520
+ return model;
2911
3521
  }
2912
- if (configuredAllowedModels(settings).includes(model)) {
2913
- return;
3522
+ const canonicalModel = canonicalizeConfiguredModelId(settings, model);
3523
+ if (configuredAllowedModels(settings).includes(canonicalModel)) {
3524
+ return canonicalModel;
2914
3525
  }
2915
- if (settings.codexSubscriptionEnabled && model.startsWith(CODEX_MODEL_ID_PREFIX)) {
2916
- return;
3526
+ if (settings.codexSubscriptionEnabled && canonicalModel.startsWith(CODEX_MODEL_ID_PREFIX)) {
3527
+ return canonicalModel;
2917
3528
  }
2918
3529
  throw new HTTPException9(422, { message: `model is not available: ${model}` });
2919
3530
  }
3531
+ function assertConfiguredModel(settings, model) {
3532
+ canonicalConfiguredModel(settings, model);
3533
+ }
2920
3534
  async function assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model) {
2921
3535
  if (model === null || model === void 0) {
2922
3536
  return;
2923
3537
  }
3538
+ const canonicalModel = canonicalConfiguredModel(settings, model);
3539
+ if (canonicalModel === null || canonicalModel === void 0) {
3540
+ return;
3541
+ }
2924
3542
  const policy = await getWorkspaceModelPolicy(db, workspaceId);
2925
3543
  if (!policy) {
2926
3544
  return;
2927
3545
  }
2928
- const providerId = policyProviderIdForModel(settings, model);
2929
- const verdict = evaluateWorkspaceModelPolicy(policy, { providerId, modelId: model });
3546
+ const providerId = policyProviderIdForModel(settings, canonicalModel);
3547
+ const verdict = evaluateWorkspaceModelPolicy(policy, {
3548
+ providerId,
3549
+ modelId: canonicalModel
3550
+ });
2930
3551
  if (!verdict.allowed) {
2931
3552
  throw new HTTPException9(422, {
2932
- message: verdict.reason === "provider" ? `model "${model}" is not allowed by this workspace's model policy: provider "${providerId}" is not in the allowed providers` : `model "${model}" is not allowed by this workspace's model policy`
3553
+ message: verdict.reason === "provider" ? `model "${canonicalModel}" is not allowed by this workspace's model policy: provider "${providerId}" is not in the allowed providers` : `model "${canonicalModel}" is not allowed by this workspace's model policy`
2933
3554
  });
2934
3555
  }
2935
3556
  }
@@ -2950,7 +3571,7 @@ function reasoningEffortForSession(metadata, fallback) {
2950
3571
  }
2951
3572
  async function postUserMessageTurn(input) {
2952
3573
  const { db, bus, workflowClient, settings, accountId, workspaceId, sessionId } = input;
2953
- const requestedModel = input.model ?? null;
3574
+ const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
2954
3575
  const requestedReasoningEffort = input.reasoningEffort ?? null;
2955
3576
  assertConfiguredModel(settings, requestedModel);
2956
3577
  await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, requestedModel);
@@ -2967,17 +3588,24 @@ async function postUserMessageTurn(input) {
2967
3588
  workspaceId,
2968
3589
  sessionId,
2969
3590
  subjectId: input.actor ?? accountId,
2970
- actor: { type: "human", subjectId: input.actor ?? accountId },
3591
+ ...input.actorLabel ? { subjectLabel: input.actorLabel } : {},
3592
+ actor: input.commandActor ?? {
3593
+ type: "human",
3594
+ subjectId: input.actor ?? accountId
3595
+ },
2971
3596
  operationKey,
2972
3597
  delivery: input.delivery ?? "send",
2973
3598
  controlEtag: input.controlEtag ?? null,
2974
3599
  expectedDraftRevision: input.expectedDraftRevision ?? null,
2975
3600
  text: input.text,
3601
+ turnInstructions: input.turnInstructions ?? null,
2976
3602
  resources: input.resources,
2977
3603
  tools: input.tools,
3604
+ toolsProvided: input.toolsProvided,
2978
3605
  model: requestedModel,
2979
3606
  reasoningEffort: requestedReasoningEffort,
2980
- reasoningEffortFallback: settings.openaiReasoningEffort,
3607
+ reasoningEffortFallback: input.reasoningEffortFallback ?? settings.openaiReasoningEffort,
3608
+ turnExecutionPolicy: input.turnExecutionPolicy,
2981
3609
  source: input.origin === "operator" ? "api" : "user",
2982
3610
  mcpCredentialUpdates: input.mcpCredentialUpdates ?? []
2983
3611
  })
@@ -3044,24 +3672,79 @@ async function postUserMessageTurn(input) {
3044
3672
  async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
3045
3673
  const { settings, db, bus, workflowClient, objectStorage } = deps;
3046
3674
  const payload = CreateSessionRequest.parse(rawPayload);
3675
+ const parentSessionId = typeof grant.metadata?.["sessionId"] === "string" ? grant.metadata["sessionId"] : null;
3676
+ if (parentSessionId) {
3677
+ await requireSessionAuthorization(deps, grant, {
3678
+ sessionId: parentSessionId,
3679
+ operation: "session.child.create",
3680
+ surface: "core"
3681
+ });
3682
+ }
3683
+ const parentSession = parentSessionId ? await getSession2(db, workspaceId, parentSessionId) : null;
3684
+ if (parentSessionId && !parentSession) {
3685
+ throw new HTTPException9(404, {
3686
+ message: `parent session not found in workspace: ${parentSessionId}`
3687
+ });
3688
+ }
3047
3689
  const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
3048
3690
  db,
3049
3691
  workspaceId,
3050
3692
  settings
3051
3693
  );
3052
- const sessionMcpServers = validateSessionMcpServersForCreate(
3053
- capabilityRuntimeSettings,
3054
- grant,
3055
- payload.mcpServers
3056
- );
3694
+ const sessionMcpServers = hasOwnProperty(rawPayload, "mcpServers") ? validateSessionMcpServersForCreate(capabilityRuntimeSettings, grant, payload.mcpServers) : parentSession ? validateInheritedSessionMcpServersForCreate(
3695
+ await listSessionMcpServersForChildInheritance(db, workspaceId, parentSession.id)
3696
+ ) : validateSessionMcpServersForCreate(capabilityRuntimeSettings, grant, payload.mcpServers);
3057
3697
  const runtimeSettings = settingsWithSessionMcpServerConfigs(
3058
3698
  capabilityRuntimeSettings,
3059
3699
  sessionMcpServers.runtimeServers
3060
3700
  );
3061
- const resources = normalizeResources(payload.resources);
3062
- const requestedTools = validateToolRefs(payload.tools, runtimeSettings);
3063
- const defaultedTools = hasOwnProperty(rawPayload, "tools") ? requestedTools : withDefaultEnabledCapabilityMcpTools(requestedTools, settings, capabilityRuntimeSettings);
3064
- const tools = withFirstPartyTools(defaultedTools, runtimeSettings);
3701
+ const resources = normalizeResources(
3702
+ hasOwnProperty(rawPayload, "resources") ? payload.resources : parentSession?.resources ?? payload.resources
3703
+ );
3704
+ const toolsProvided = hasOwnProperty(rawPayload, "tools");
3705
+ const requestedTools = validateToolRefs(
3706
+ toolsProvided ? payload.tools : parentSession?.tools ?? payload.tools,
3707
+ runtimeSettings
3708
+ );
3709
+ let selectedTools;
3710
+ let toolPolicy;
3711
+ if (parentSession) {
3712
+ const parentTracksWorkspaceDefaults = parentSession.toolPolicy?.mode === "workspace_default";
3713
+ const parentEffective = withFirstPartyTools(
3714
+ parentTracksWorkspaceDefaults ? withDefaultEnabledCapabilityMcpTools(
3715
+ availableToolRefs(parentSession.tools, runtimeSettings),
3716
+ settings,
3717
+ runtimeSettings
3718
+ ) : parentSession.tools,
3719
+ runtimeSettings
3720
+ );
3721
+ if (toolsProvided) {
3722
+ assertToolRefsSubset(
3723
+ requestedTools,
3724
+ parentEffective,
3725
+ "child tools may only narrow the parent session tool policy"
3726
+ );
3727
+ selectedTools = requestedTools;
3728
+ toolPolicy = { mode: "explicit", inheritedFromSessionId: parentSession.id };
3729
+ } else {
3730
+ selectedTools = parentEffective;
3731
+ toolPolicy = {
3732
+ mode: parentTracksWorkspaceDefaults ? "workspace_default" : "inherited",
3733
+ inheritedFromSessionId: parentSession.id
3734
+ };
3735
+ }
3736
+ } else if (toolsProvided) {
3737
+ selectedTools = requestedTools;
3738
+ toolPolicy = { mode: "explicit", inheritedFromSessionId: null };
3739
+ } else {
3740
+ selectedTools = withDefaultEnabledCapabilityMcpTools(
3741
+ requestedTools,
3742
+ settings,
3743
+ capabilityRuntimeSettings
3744
+ );
3745
+ toolPolicy = { mode: "workspace_default", inheritedFromSessionId: null };
3746
+ }
3747
+ const tools = withFirstPartyTools(selectedTools, runtimeSettings);
3065
3748
  await validateGitHubRepositorySelection(db, workspaceId, resources);
3066
3749
  if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
3067
3750
  throw new HTTPException9(503, { message: "object storage is not configured" });
@@ -3089,16 +3772,30 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
3089
3772
  frozenRigVersionId = rig.activeVersion.id;
3090
3773
  }
3091
3774
  }
3092
- assertConfiguredModel(settings, payload.model);
3093
- await assertWorkspaceModelPolicyAllows(
3094
- db,
3095
- settings,
3096
- workspaceId,
3097
- payload.model ?? settings.openaiModel
3098
- );
3099
- const model = payload.model ?? settings.openaiModel;
3775
+ const model = canonicalConfiguredModel(settings, payload.model ?? settings.openaiModel);
3776
+ if (model === null || model === void 0) {
3777
+ throw new Error("effective session model unexpectedly resolved to null");
3778
+ }
3779
+ await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model);
3100
3780
  const reasoningEffort = payload.reasoningEffort ?? settings.openaiReasoningEffort;
3101
- let firstPartyMcpPermissions = payload.firstPartyMcpPermissions ?? null;
3781
+ const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
3782
+ modelId: model,
3783
+ requestedModelId: payload.model ?? null,
3784
+ modelSource: payload.model === void 0 ? "deployment" : "explicit",
3785
+ reasoningEffort,
3786
+ reasoningSource: payload.reasoningEffort === void 0 ? "deployment" : "explicit"
3787
+ });
3788
+ const parentFirstPartyMcpPermissions = parentSession ? [...parentSession.firstPartyMcpPermissions ?? DEFAULT_FIRST_PARTY_MCP_PERMISSIONS] : null;
3789
+ if (parentFirstPartyMcpPermissions && payload.firstPartyMcpPermissions?.some(
3790
+ (permission) => !hasPermission(parentFirstPartyMcpPermissions, permission)
3791
+ )) {
3792
+ throw new HTTPException9(403, {
3793
+ message: "child first-party MCP permissions may only narrow the parent session grant"
3794
+ });
3795
+ }
3796
+ let firstPartyMcpPermissions = payload.firstPartyMcpPermissions ?? (parentFirstPartyMcpPermissions ? parentFirstPartyMcpPermissions.filter(
3797
+ (permission) => hasPermission(grant.permissions, permission)
3798
+ ) : null);
3102
3799
  if (firstPartyMcpPermissions && firstPartyMcpPermissions.length === 0) {
3103
3800
  throw new HTTPException9(422, {
3104
3801
  message: "firstPartyMcpPermissions must not be empty; omit it for the default worker permission set"
@@ -3112,9 +3809,10 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
3112
3809
  }
3113
3810
  }
3114
3811
  if (payload.goal && firstPartyMcpPermissions && !firstPartyMcpPermissions.includes("goals:manage")) {
3115
- firstPartyMcpPermissions = [...firstPartyMcpPermissions, "goals:manage"];
3812
+ throw new HTTPException9(422, {
3813
+ message: "goal-bearing sessions require goals:manage in the resulting first-party MCP permission set"
3814
+ });
3116
3815
  }
3117
- const parentSessionId = typeof grant.metadata?.["sessionId"] === "string" ? grant.metadata["sessionId"] : null;
3118
3816
  const sandboxChoice = payload.sandbox ?? (parentSessionId ? "shared" : "new");
3119
3817
  let sandboxGroupId = null;
3120
3818
  let inheritedBackend;
@@ -3127,12 +3825,10 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
3127
3825
  message: "sandbox:'shared' requires a parent session (spawn from inside a session); use 'new' for a top-level create."
3128
3826
  });
3129
3827
  }
3130
- const parent = await getSession(db, workspaceId, parentSessionId);
3131
- if (!parent) {
3132
- throw new HTTPException9(404, {
3133
- message: `parent session not found in workspace: ${parentSessionId}`
3134
- });
3828
+ if (!parentSession) {
3829
+ throw new Error("trusted parent session was not resolved");
3135
3830
  }
3831
+ const parent = parentSession;
3136
3832
  const parentBoxed = parent.sandboxBackend !== "none";
3137
3833
  const variableSetMismatch = parentBoxed && !variableSetMatchesGroup(parent.variableSetId ?? null);
3138
3834
  let rigMismatch = parentBoxed && !rigVersionMatchesGroup(parent.rigVersionId ?? null);
@@ -3218,50 +3914,71 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
3218
3914
  quantity: 1,
3219
3915
  model
3220
3916
  });
3221
- const session = await createAndStartSession({
3222
- db,
3223
- bus,
3224
- workflowClient,
3225
- accountId: grant.accountId,
3226
- workspaceId,
3227
- initialMessage: payload.initialMessage,
3228
- resources,
3229
- tools,
3230
- ...payload.clientEventId ? { clientEventId: payload.clientEventId } : {},
3231
- model,
3232
- reasoningEffort,
3233
- // A shared spawn inherits the box's backend; a caller-supplied
3234
- // sandboxBackend on a shared spawn is ignored (it is the same box). A
3235
- // machine-targeted top-level create labels the home "selfhosted"
3236
- // (machineHomeBackend), overriding the caller/deployment default so the row
3237
- // matches where the session actually runs.
3238
- sandboxBackend: inheritedBackend ?? machineHomeBackend ?? payload.sandboxBackend ?? settings.sandboxBackend,
3239
- // Mirror the backend relabel on the OS axis: only a machine-targeted
3240
- // top-level create carries a derived OS; everything else is omitted and the
3241
- // "linux" default holds (shared spawns keep the parent-box behavior).
3242
- ...machineHomeOs ? { sandboxOs: machineHomeOs } : {},
3243
- sandboxGroupId,
3244
- metadata: payload.metadata,
3245
- variableSet: variableSet ? { id: variableSet.id, name: variableSet.name } : null,
3246
- // Frozen rig binding (M3): both null for a rig-less session (today's path).
3247
- rigId: frozenRigId,
3248
- rigVersionId: frozenRigVersionId,
3249
- goal: payload.goal ?? null,
3250
- // Per-session persona instructions (already trimmed/validated by the
3251
- // contracts schema). Persisted on the row; composed system-level at turn
3252
- // time. Not surfaced as an event.
3253
- instructions: payload.instructions ?? null,
3254
- firstPartyMcpPermissions,
3255
- mcpServers: sessionMcpServers.dbServers,
3256
- sessionMcpServers: sessionMcpServers.metadata,
3257
- parentSessionId,
3258
- createIdempotencyKey: payload.idempotencyKey ?? null,
3259
- // Create-time machine targeting (A-2a): when a target sandbox is named, the
3260
- // active-sandbox pointer is seeded race-free inside createAndStartSession
3261
- // (after the row exists, before the first turn dispatches). Validation
3262
- // (ownership/liveness) lives in swapActiveSandbox; an invalid target 422s.
3263
- seedTargetSandbox: payload.targetSandboxId ? { sandboxId: payload.targetSandboxId, settings, workingDir: payload.workingDir ?? null } : null
3264
- });
3917
+ const creationInitiator = creationInitiatorForGrant(grant);
3918
+ let session;
3919
+ try {
3920
+ session = await createAndStartSession({
3921
+ ...payload.requestedSessionId ? { requestedSessionId: payload.requestedSessionId } : {},
3922
+ db,
3923
+ bus,
3924
+ workflowClient,
3925
+ accountId: grant.accountId,
3926
+ workspaceId,
3927
+ initialMessage: payload.initialMessage,
3928
+ turnInstructions: payload.turnInstructions ?? null,
3929
+ resources,
3930
+ tools,
3931
+ toolPolicy,
3932
+ ...payload.clientEventId ? { clientEventId: payload.clientEventId } : {},
3933
+ model,
3934
+ reasoningEffort,
3935
+ turnExecutionPolicy,
3936
+ // A shared spawn inherits the box's backend; a caller-supplied
3937
+ // sandboxBackend on a shared spawn is ignored (it is the same box). A
3938
+ // machine-targeted top-level create labels the home "selfhosted"
3939
+ // (machineHomeBackend), overriding the caller/deployment default so the row
3940
+ // matches where the session actually runs.
3941
+ sandboxBackend: inheritedBackend ?? machineHomeBackend ?? payload.sandboxBackend ?? settings.sandboxBackend,
3942
+ // Mirror the backend relabel on the OS axis: only a machine-targeted
3943
+ // top-level create carries a derived OS; everything else is omitted and the
3944
+ // "linux" default holds (shared spawns keep the parent-box behavior).
3945
+ ...machineHomeOs ? { sandboxOs: machineHomeOs } : {},
3946
+ sandboxGroupId,
3947
+ metadata: payload.metadata,
3948
+ ...creationInitiator.initiator ? { createdBy: creationInitiator.initiator } : {},
3949
+ ...creationInitiator.context ? { createdByContext: creationInitiator.context } : {},
3950
+ createdByActor: creationInitiator.actor ?? null,
3951
+ variableSet: variableSet ? { id: variableSet.id, name: variableSet.name } : null,
3952
+ // Frozen rig binding (M3): both null for a rig-less session (today's path).
3953
+ rigId: frozenRigId,
3954
+ rigVersionId: frozenRigVersionId,
3955
+ goal: payload.goal ?? null,
3956
+ // Per-session persona instructions (already trimmed/validated by the
3957
+ // contracts schema). Persisted on the row; composed system-level at turn
3958
+ // time. Not surfaced as an event.
3959
+ instructions: payload.instructions ?? null,
3960
+ firstPartyMcpPermissions,
3961
+ mcpServers: sessionMcpServers.dbServers,
3962
+ sessionMcpServers: sessionMcpServers.metadata,
3963
+ parentSessionId,
3964
+ createIdempotencyKey: payload.idempotencyKey ?? null,
3965
+ // Create-time machine targeting (A-2a): when a target sandbox is named, the
3966
+ // active-sandbox pointer is seeded race-free inside createAndStartSession
3967
+ // (after the row exists, before the first turn dispatches). Validation
3968
+ // (ownership/liveness) lives in swapActiveSandbox; an invalid target 422s.
3969
+ seedTargetSandbox: payload.targetSandboxId ? { sandboxId: payload.targetSandboxId, settings, workingDir: payload.workingDir ?? null } : null
3970
+ });
3971
+ } catch (error) {
3972
+ if (error instanceof AgentCommandAuthorityError) {
3973
+ throw new HTTPException9(403, { message: error.message });
3974
+ }
3975
+ if (error instanceof SessionIdConflictError) {
3976
+ throw new HTTPException9(409, {
3977
+ message: "requested session id is already in use"
3978
+ });
3979
+ }
3980
+ throw error;
3981
+ }
3265
3982
  await recordWorkspaceUsage(deps, {
3266
3983
  accountId: grant.accountId,
3267
3984
  workspaceId,
@@ -3271,31 +3988,76 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
3271
3988
  unit: "run",
3272
3989
  sourceResourceType: "session",
3273
3990
  sourceResourceId: session.id,
3991
+ sessionId: session.id,
3992
+ initiator: session.createdBy,
3993
+ initiatorContext: session.createdByContext,
3994
+ origin: creationInitiator.actor ? "system" : "user",
3274
3995
  idempotencyKey: `agent_run.created:${workspaceId}:${session.id}`
3275
3996
  });
3276
3997
  return session;
3277
3998
  }
3278
3999
  async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, input) {
4000
+ if (input.toolsProvided && !deps.settings.sessionTurnToolReplacementEnabled) {
4001
+ throw new HTTPException9(503, {
4002
+ message: "explicit follow-up tool replacement is temporarily unavailable until provenance-aware turn workers finish rolling out; omit tools to inherit the session policy and retry"
4003
+ });
4004
+ }
3279
4005
  const { settings, db, bus, workflowClient, objectStorage } = deps;
4006
+ await requireSessionAuthorization(deps, grant, {
4007
+ sessionId,
4008
+ operation: input.delivery === "steer" ? "session.steer" : "session.append",
4009
+ surface: "core"
4010
+ });
3280
4011
  const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
3281
4012
  db,
3282
4013
  workspaceId,
3283
4014
  settings
3284
4015
  );
3285
4016
  const existingSession = await requireSession2(db, workspaceId, sessionId);
4017
+ const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
4018
+ const effectiveModel = canonicalConfiguredModel(settings, requestedModel ?? existingSession.model) ?? null;
4019
+ if (effectiveModel === null) {
4020
+ throw new Error("effective follow-up model unexpectedly resolved to null");
4021
+ }
4022
+ const sessionReasoningEffort = reasoningEffortForSession(
4023
+ existingSession.metadata,
4024
+ settings.openaiReasoningEffort
4025
+ );
4026
+ const effectiveReasoningEffort = input.reasoningEffort ?? sessionReasoningEffort;
4027
+ const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
4028
+ modelId: effectiveModel,
4029
+ requestedModelId: input.model ?? null,
4030
+ modelSource: input.model == null ? "session" : "explicit",
4031
+ reasoningEffort: effectiveReasoningEffort,
4032
+ reasoningSource: input.reasoningEffort == null ? "session" : "explicit"
4033
+ });
3286
4034
  const runtimeSettings = settingsWithSessionMcpServerMetadata(
3287
4035
  capabilityRuntimeSettings,
3288
4036
  existingSession.mcpServers
3289
4037
  );
3290
4038
  const requestedResources = normalizeResources(input.resources ?? []);
3291
- const validatedTools = validateToolRefs(input.tools ?? [], runtimeSettings);
3292
- const requestedTools = input.toolsProvided ? validatedTools : withDefaultEnabledCapabilityMcpTools(validatedTools, settings, capabilityRuntimeSettings);
4039
+ const tracksWorkspaceDefaults = existingSession.toolPolicy?.mode === "workspace_default";
4040
+ const sessionPolicyTools = withFirstPartyTools(
4041
+ tracksWorkspaceDefaults ? withDefaultEnabledCapabilityMcpTools(
4042
+ availableToolRefs(existingSession.tools, runtimeSettings),
4043
+ settings,
4044
+ capabilityRuntimeSettings
4045
+ ) : existingSession.tools,
4046
+ runtimeSettings
4047
+ );
4048
+ const validatedTools = input.toolsProvided ? validateToolRefsForSessionPolicy({
4049
+ requested: input.tools ?? [],
4050
+ settings: runtimeSettings,
4051
+ allowedTools: sessionPolicyTools,
4052
+ message: "message tools may only narrow the session tool policy"
4053
+ }) : [];
4054
+ const requestedTools = input.toolsProvided ? validatedTools : [];
3293
4055
  await requireLimit(deps, {
3294
4056
  accountId: grant.accountId,
3295
4057
  workspaceId,
3296
4058
  action: "agent_run:create",
3297
4059
  quantity: 1,
3298
- model: input.model ?? existingSession.model
4060
+ model: effectiveModel
3299
4061
  });
3300
4062
  if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
3301
4063
  throw new HTTPException9(503, { message: "object storage is not configured" });
@@ -3311,6 +4073,7 @@ async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, inp
3311
4073
  session: existingSession,
3312
4074
  updates: input.mcpCredentialUpdates ?? []
3313
4075
  });
4076
+ const delegatedServiceInitiator = serviceInitiatorForGrant(grant);
3314
4077
  const { accepted, turn } = await postUserMessageTurn({
3315
4078
  db,
3316
4079
  bus,
@@ -3320,14 +4083,27 @@ async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, inp
3320
4083
  workspaceId,
3321
4084
  sessionId,
3322
4085
  text: input.text,
4086
+ turnInstructions: input.turnInstructions ?? null,
3323
4087
  resources: requestedResources,
3324
4088
  tools: requestedTools,
4089
+ toolsProvided: input.toolsProvided,
3325
4090
  model: input.model ?? null,
3326
4091
  reasoningEffort: input.reasoningEffort ?? null,
4092
+ reasoningEffortFallback: sessionReasoningEffort,
4093
+ turnExecutionPolicy,
3327
4094
  mcpCredentialUpdates,
3328
4095
  delivery: input.delivery ?? "send",
3329
- origin: input.origin ?? "human",
4096
+ origin: delegatedServiceInitiator ? "operator" : input.origin ?? "human",
3330
4097
  actor: grant.subjectId,
4098
+ ...grant.subjectLabel ? { actorLabel: grant.subjectLabel } : {},
4099
+ ...delegatedServiceInitiator ? {
4100
+ commandActor: {
4101
+ type: "service",
4102
+ subjectId: delegatedServiceInitiator.initiator.subjectId,
4103
+ ...delegatedServiceInitiator.initiator.label ? { subjectLabel: delegatedServiceInitiator.initiator.label } : {},
4104
+ context: delegatedServiceInitiator.context
4105
+ }
4106
+ } : {},
3331
4107
  ...input.controlEtag !== void 0 ? { controlEtag: input.controlEtag } : {},
3332
4108
  ...input.expectedDraftRevision !== void 0 ? { expectedDraftRevision: input.expectedDraftRevision } : {},
3333
4109
  ...input.clientEventId ? { clientEventId: input.clientEventId } : {}
@@ -3341,12 +4117,23 @@ async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, inp
3341
4117
  unit: "run",
3342
4118
  sourceResourceType: "session_turn",
3343
4119
  sourceResourceId: turn.id,
4120
+ sessionId,
4121
+ turnId: turn.id,
4122
+ initiator: turn.initiator,
4123
+ initiatorContext: turn.initiatorContext,
4124
+ origin: turn.source,
3344
4125
  idempotencyKey: `agent_run.created:${workspaceId}:${turn.id}`
3345
4126
  });
3346
4127
  return { accepted, turn };
3347
4128
  }
3348
- async function updateSessionTitle(deps, workspaceId, sessionId, title, source) {
4129
+ async function updateSessionTitle(deps, grant, sessionId, title, source) {
3349
4130
  const { db, bus } = deps;
4131
+ const authorization = await requireSessionAuthorization(deps, grant, {
4132
+ sessionId,
4133
+ operation: "session.title.write",
4134
+ surface: "core"
4135
+ });
4136
+ const workspaceId = grant.workspaceId;
3350
4137
  const result = await updateSessionTitleRow(db, { workspaceId, sessionId, title, source });
3351
4138
  if (result.updated) {
3352
4139
  await appendAndPublishEvents(db, bus, workspaceId, sessionId, [
@@ -3359,10 +4146,67 @@ async function updateSessionTitle(deps, workspaceId, sessionId, title, source) {
3359
4146
  }
3360
4147
  ]);
3361
4148
  }
3362
- return result;
4149
+ return {
4150
+ ...result,
4151
+ relatedSessionAccess: authorization?.relatedSessionAccess ?? "root"
4152
+ };
4153
+ }
4154
+ async function updateSessionMcpApprovalPolicy(deps, grant, sessionId, serverId, requireApproval) {
4155
+ const normalizedPolicy = SessionMcpApprovalPolicy.parse(requireApproval);
4156
+ await requireSessionAuthorization(deps, grant, {
4157
+ sessionId,
4158
+ operation: "session.mcp.approval_policy.write",
4159
+ surface: "core"
4160
+ });
4161
+ requirePermission(grant, "sessions:control");
4162
+ const outcome = {};
4163
+ const events = await appendSessionEventsWithLockedSessionUpdate(
4164
+ deps.db,
4165
+ grant.workspaceId,
4166
+ sessionId,
4167
+ async (_session, context) => {
4168
+ const result = await context.updateSessionMcpApprovalPolicy(serverId, normalizedPolicy);
4169
+ if (!result.server) {
4170
+ throw new HTTPException9(404, { message: "session MCP server not found" });
4171
+ }
4172
+ outcome.server = result.server;
4173
+ return {
4174
+ events: result.changed ? [
4175
+ {
4176
+ type: "session.mcp.approval_policy.updated",
4177
+ payload: {
4178
+ serverId,
4179
+ effectiveFrom: "next_attempt"
4180
+ }
4181
+ }
4182
+ ] : []
4183
+ };
4184
+ }
4185
+ );
4186
+ const updatedServer = outcome.server;
4187
+ if (!updatedServer) {
4188
+ throw new Error("session MCP approval policy update returned no server");
4189
+ }
4190
+ await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, events);
4191
+ return {
4192
+ server: updatedServer,
4193
+ effectiveFrom: "next_attempt"
4194
+ };
3363
4195
  }
3364
- async function readSessionLineage(db, workspaceId, sessionId) {
3365
- const lineage = await getSessionLineage(db, workspaceId, sessionId);
4196
+ async function readSessionLineage(deps, grant, sessionId) {
4197
+ const authorization = await requireSessionAuthorization(deps, grant, {
4198
+ sessionId,
4199
+ operation: "session.lineage.read",
4200
+ surface: "core"
4201
+ });
4202
+ if (authorization?.relatedSessionAccess === "target") {
4203
+ const session = await getSession2(deps.db, grant.workspaceId, sessionId);
4204
+ if (!session) {
4205
+ throw new HTTPException9(404, { message: "session not found" });
4206
+ }
4207
+ return { ancestors: [], children: [], truncated: false };
4208
+ }
4209
+ const lineage = await getSessionLineage(deps.db, grant.workspaceId, sessionId);
3366
4210
  if (!lineage) {
3367
4211
  throw new HTTPException9(404, { message: "session not found" });
3368
4212
  }
@@ -3552,13 +4396,8 @@ function manualScheduledTaskTriggerUsageKey(workspaceId, taskId, triggerToken) {
3552
4396
  return `agent_run.created:scheduled-trigger:${workspaceId}:${taskId}:${triggerToken}`;
3553
4397
  }
3554
4398
  async function validateScheduledTaskAgentConfig(input) {
3555
- assertConfiguredModel(input.settings, input.payload.agentConfig.model);
3556
- await assertWorkspaceModelPolicyAllows(
3557
- input.db,
3558
- input.settings,
3559
- input.workspaceId,
3560
- input.payload.agentConfig.model
3561
- );
4399
+ const model = canonicalConfiguredModel(input.settings, input.payload.agentConfig.model);
4400
+ await assertWorkspaceModelPolicyAllows(input.db, input.settings, input.workspaceId, model);
3562
4401
  const resources = normalizeResources(input.payload.agentConfig.resources ?? []);
3563
4402
  const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
3564
4403
  input.db,
@@ -3578,6 +4417,7 @@ async function validateScheduledTaskAgentConfig(input) {
3578
4417
  await validateFileResources(input.db, input.workspaceId, resources);
3579
4418
  return {
3580
4419
  ...input.payload.agentConfig,
4420
+ ...model === void 0 || model === null ? {} : { model },
3581
4421
  prompt,
3582
4422
  resources,
3583
4423
  tools
@@ -3651,13 +4491,15 @@ import {
3651
4491
  deleteSessionQueueItemInTransaction,
3652
4492
  editQueuedTurnInTransaction,
3653
4493
  getComposerDraftInTransaction,
3654
- getSession as getSession2,
4494
+ getSession as getSession3,
3655
4495
  getSessionEvent as getSessionEvent2,
3656
4496
  getWorkspaceControlEvent as getWorkspaceControlEvent2,
3657
4497
  getSessionQueueSnapshot,
3658
4498
  moveQueuedTurnInTransaction,
3659
4499
  mutateSessionControlInTransaction,
3660
4500
  mutateWorkspaceControlInTransaction,
4501
+ projectEffectiveControlForRelatedAccess,
4502
+ runIdempotentPersistenceTransaction,
3661
4503
  saveComposerDraftInTransaction,
3662
4504
  sendAgentMessageInTransaction,
3663
4505
  serializeEffectiveSessionControl,
@@ -3670,6 +4512,42 @@ import {
3670
4512
  publishDurableSessionEvents as publishDurableSessionEvents2,
3671
4513
  publishDurableWorkspaceControlEvent as publishDurableWorkspaceControlEvent2
3672
4514
  } from "@opengeni/events";
4515
+ function humanAccessGrant(context) {
4516
+ return {
4517
+ accountId: context.accountId,
4518
+ workspaceId: context.workspaceId,
4519
+ subjectId: context.subjectId,
4520
+ permissions: []
4521
+ };
4522
+ }
4523
+ function agentAccessGrant(context) {
4524
+ return {
4525
+ accountId: context.accountId,
4526
+ workspaceId: context.workspaceId,
4527
+ subjectId: context.subjectId,
4528
+ permissions: [],
4529
+ metadata: {
4530
+ sessionId: context.callerSessionId,
4531
+ turnId: context.callerTurnId,
4532
+ attemptId: context.callerAttemptId,
4533
+ executionGeneration: context.callerExecutionGeneration
4534
+ }
4535
+ };
4536
+ }
4537
+ async function authorizeHumanSessionCommand(deps, context, operation) {
4538
+ return await requireSessionAuthorization(deps, humanAccessGrant(context), {
4539
+ sessionId: context.sessionId,
4540
+ operation,
4541
+ surface: "core"
4542
+ });
4543
+ }
4544
+ async function authorizeAgentSessionCommand(deps, context, targetSessionId, operation) {
4545
+ return await requireSessionAuthorization(deps, agentAccessGrant(context), {
4546
+ sessionId: targetSessionId,
4547
+ operation,
4548
+ surface: "core"
4549
+ });
4550
+ }
3673
4551
  function agentActor(context) {
3674
4552
  return {
3675
4553
  type: "agent_attempt",
@@ -3679,6 +4557,20 @@ function agentActor(context) {
3679
4557
  executionGeneration: context.callerExecutionGeneration
3680
4558
  };
3681
4559
  }
4560
+ async function runAgentCommandPersistenceTransaction(deps, context, input) {
4561
+ return await runIdempotentPersistenceTransaction(
4562
+ {
4563
+ stage: input.stage,
4564
+ eventTypes: input.eventTypes,
4565
+ maxAttempts: 3
4566
+ },
4567
+ async () => await withWorkspaceRls(
4568
+ deps.db,
4569
+ context.workspaceId,
4570
+ async (scoped) => scoped.transaction(async (tx) => await input.transaction(tx))
4571
+ )
4572
+ );
4573
+ }
3682
4574
  async function publishAndWakeAgentCommand(deps, input) {
3683
4575
  await publishSessionEventIds(deps, input.workspaceId, input.sessionId, input.eventIds);
3684
4576
  if (!input.shouldSignal || input.wakeRevision === null) return;
@@ -3730,20 +4622,19 @@ async function publishWorkspaceControlEvent(deps, workspaceId, eventId) {
3730
4622
  await publishDurableWorkspaceControlEvent2(deps.bus, workspaceId, event);
3731
4623
  }
3732
4624
  async function sendAgentSessionMessage(deps, context, input) {
3733
- const result = await withWorkspaceRls(
3734
- deps.db,
3735
- context.workspaceId,
3736
- (scoped) => scoped.transaction(
3737
- (tx) => sendAgentMessageInTransaction(tx, {
3738
- accountId: context.accountId,
3739
- workspaceId: context.workspaceId,
3740
- targetSessionId: input.targetSessionId,
3741
- actor: agentActor(context),
3742
- operationKey: input.idempotencyKey,
3743
- text: input.text
3744
- })
3745
- )
3746
- );
4625
+ await authorizeAgentSessionCommand(deps, context, input.targetSessionId, "session.append");
4626
+ const result = await runAgentCommandPersistenceTransaction(deps, context, {
4627
+ stage: "session_commands.agent_message",
4628
+ eventTypes: ["system.update.pending"],
4629
+ transaction: async (tx) => await sendAgentMessageInTransaction(tx, {
4630
+ accountId: context.accountId,
4631
+ workspaceId: context.workspaceId,
4632
+ targetSessionId: input.targetSessionId,
4633
+ actor: agentActor(context),
4634
+ operationKey: input.idempotencyKey,
4635
+ text: input.text
4636
+ })
4637
+ });
3747
4638
  await publishAndWakeAgentCommand(deps, {
3748
4639
  accountId: context.accountId,
3749
4640
  workspaceId: context.workspaceId,
@@ -3758,20 +4649,19 @@ async function sendAgentSessionMessage(deps, context, input) {
3758
4649
  return result;
3759
4650
  }
3760
4651
  async function steerAgentSession(deps, context, input) {
3761
- const result = await withWorkspaceRls(
3762
- deps.db,
3763
- context.workspaceId,
3764
- (scoped) => scoped.transaction(
3765
- (tx) => steerAgentSessionInTransaction(tx, {
3766
- accountId: context.accountId,
3767
- workspaceId: context.workspaceId,
3768
- targetSessionId: input.targetSessionId,
3769
- actor: agentActor(context),
3770
- operationKey: input.idempotencyKey,
3771
- instruction: input.instruction
3772
- })
3773
- )
3774
- );
4652
+ await authorizeAgentSessionCommand(deps, context, input.targetSessionId, "session.steer");
4653
+ const result = await runAgentCommandPersistenceTransaction(deps, context, {
4654
+ stage: "session_commands.agent_steer",
4655
+ eventTypes: ["session.control.steer_requested", "system.update.pending", "turn.superseded"],
4656
+ transaction: async (tx) => await steerAgentSessionInTransaction(tx, {
4657
+ accountId: context.accountId,
4658
+ workspaceId: context.workspaceId,
4659
+ targetSessionId: input.targetSessionId,
4660
+ actor: agentActor(context),
4661
+ operationKey: input.idempotencyKey,
4662
+ instruction: input.instruction
4663
+ })
4664
+ });
3775
4665
  await publishAndWakeAgentCommand(deps, {
3776
4666
  accountId: context.accountId,
3777
4667
  workspaceId: context.workspaceId,
@@ -3786,6 +4676,7 @@ async function steerAgentSession(deps, context, input) {
3786
4676
  return result;
3787
4677
  }
3788
4678
  async function controlAgentSessionWorkstream(deps, context, input) {
4679
+ await authorizeAgentSessionCommand(deps, context, input.targetSessionId, "session.control");
3789
4680
  const result = await withWorkspaceRls(
3790
4681
  deps.db,
3791
4682
  context.workspaceId,
@@ -3829,6 +4720,7 @@ function composerDraft(row) {
3829
4720
  text: row.text,
3830
4721
  resources: row.resources,
3831
4722
  tools: row.tools,
4723
+ toolsProvided: row.toolsProvided,
3832
4724
  model: row.model,
3833
4725
  reasoningEffort: row.reasoningEffort,
3834
4726
  sourceTurnId: row.sourceTurnId,
@@ -3836,12 +4728,20 @@ function composerDraft(row) {
3836
4728
  updatedAt: row.updatedAt.toISOString()
3837
4729
  };
3838
4730
  }
3839
- async function authoritativeQueue(db, workspaceId, sessionId) {
4731
+ async function authoritativeQueue(db, workspaceId, sessionId, relatedSessionAccess) {
3840
4732
  const snapshot = await getSessionQueueSnapshot(db, workspaceId, sessionId);
3841
4733
  if (!snapshot) throw new Error(`Session not found: ${sessionId}`);
3842
- return snapshot;
4734
+ return {
4735
+ ...snapshot,
4736
+ effectiveControl: projectEffectiveControlForRelatedAccess(
4737
+ snapshot.effectiveControl,
4738
+ sessionId,
4739
+ relatedSessionAccess
4740
+ )
4741
+ };
3843
4742
  }
3844
4743
  async function moveHumanQueuePrompt(deps, context, turnId, input) {
4744
+ const authorization = await authorizeHumanSessionCommand(deps, context, "session.queue.control");
3845
4745
  const result = await withWorkspaceRls(
3846
4746
  deps.db,
3847
4747
  context.workspaceId,
@@ -3858,12 +4758,18 @@ async function moveHumanQueuePrompt(deps, context, turnId, input) {
3858
4758
  );
3859
4759
  const response = {
3860
4760
  receipt: receipt(result.receipt),
3861
- snapshot: await authoritativeQueue(deps.db, context.workspaceId, context.sessionId)
4761
+ snapshot: await authoritativeQueue(
4762
+ deps.db,
4763
+ context.workspaceId,
4764
+ context.sessionId,
4765
+ authorization?.relatedSessionAccess ?? "root"
4766
+ )
3862
4767
  };
3863
4768
  await publishSessionEventIds(deps, context.workspaceId, context.sessionId, result.eventIds);
3864
4769
  return response;
3865
4770
  }
3866
4771
  async function deleteHumanQueuePrompt(deps, context, turnId, input) {
4772
+ const authorization = await authorizeHumanSessionCommand(deps, context, "session.queue.control");
3867
4773
  const result = await withWorkspaceRls(
3868
4774
  deps.db,
3869
4775
  context.workspaceId,
@@ -3880,12 +4786,18 @@ async function deleteHumanQueuePrompt(deps, context, turnId, input) {
3880
4786
  );
3881
4787
  const response = {
3882
4788
  receipt: receipt(result.receipt),
3883
- snapshot: await authoritativeQueue(deps.db, context.workspaceId, context.sessionId)
4789
+ snapshot: await authoritativeQueue(
4790
+ deps.db,
4791
+ context.workspaceId,
4792
+ context.sessionId,
4793
+ authorization?.relatedSessionAccess ?? "root"
4794
+ )
3884
4795
  };
3885
4796
  await publishSessionEventIds(deps, context.workspaceId, context.sessionId, result.eventIds);
3886
4797
  return response;
3887
4798
  }
3888
4799
  async function editHumanQueuePrompt(deps, context, turnId, input) {
4800
+ const authorization = await authorizeHumanSessionCommand(deps, context, "session.queue.control");
3889
4801
  const result = await withWorkspaceSubjectRls2(
3890
4802
  deps.db,
3891
4803
  context.workspaceId,
@@ -3904,13 +4816,19 @@ async function editHumanQueuePrompt(deps, context, turnId, input) {
3904
4816
  );
3905
4817
  const response = {
3906
4818
  receipt: receipt(result.receipt),
3907
- snapshot: await authoritativeQueue(deps.db, context.workspaceId, context.sessionId),
4819
+ snapshot: await authoritativeQueue(
4820
+ deps.db,
4821
+ context.workspaceId,
4822
+ context.sessionId,
4823
+ authorization?.relatedSessionAccess ?? "root"
4824
+ ),
3908
4825
  draft: composerDraft(result.draft)
3909
4826
  };
3910
4827
  await publishSessionEventIds(deps, context.workspaceId, context.sessionId, result.eventIds);
3911
4828
  return response;
3912
4829
  }
3913
4830
  async function steerHumanQueuePrompt(deps, context, turnId, input) {
4831
+ const authorization = await authorizeHumanSessionCommand(deps, context, "session.queue.control");
3914
4832
  const result = await withWorkspaceRls(
3915
4833
  deps.db,
3916
4834
  context.workspaceId,
@@ -3927,13 +4845,19 @@ async function steerHumanQueuePrompt(deps, context, turnId, input) {
3927
4845
  );
3928
4846
  const response = {
3929
4847
  receipt: receipt(result.receipt),
3930
- snapshot: await authoritativeQueue(deps.db, context.workspaceId, context.sessionId)
4848
+ snapshot: await authoritativeQueue(
4849
+ deps.db,
4850
+ context.workspaceId,
4851
+ context.sessionId,
4852
+ authorization?.relatedSessionAccess ?? "root"
4853
+ )
3931
4854
  };
3932
4855
  await publishSessionEventIds(deps, context.workspaceId, context.sessionId, result.eventIds);
3933
4856
  await publishWorkspaceControlEvent(deps, context.workspaceId, result.workspaceControlEventId);
3934
4857
  return response;
3935
4858
  }
3936
4859
  async function controlHumanSessionWorkstream(deps, context, input) {
4860
+ const authorization = await authorizeHumanSessionCommand(deps, context, "session.control");
3937
4861
  const result = await withWorkspaceRls(
3938
4862
  deps.db,
3939
4863
  context.workspaceId,
@@ -3952,7 +4876,11 @@ async function controlHumanSessionWorkstream(deps, context, input) {
3952
4876
  );
3953
4877
  const response = {
3954
4878
  receipt: receipt(result.receipt),
3955
- effectiveControl: serializeEffectiveSessionControl(result.control),
4879
+ effectiveControl: projectEffectiveControlForRelatedAccess(
4880
+ serializeEffectiveSessionControl(result.control),
4881
+ context.sessionId,
4882
+ authorization?.relatedSessionAccess ?? "root"
4883
+ ),
3956
4884
  interruptionCount: result.interruptionCount,
3957
4885
  wakeCount: result.wakeCount
3958
4886
  };
@@ -3990,9 +4918,10 @@ async function controlHumanWorkspace(deps, context, input) {
3990
4918
  await requestControlWakeDispatch(deps, result.wakeCount);
3991
4919
  return response;
3992
4920
  }
3993
- async function getHumanComposerDraft(db, context) {
4921
+ async function getHumanComposerDraft(deps, context) {
4922
+ await authorizeHumanSessionCommand(deps, context, "session.composer.read");
3994
4923
  const row = await withWorkspaceSubjectRls2(
3995
- db,
4924
+ deps.db,
3996
4925
  context.workspaceId,
3997
4926
  context.subjectId,
3998
4927
  (scoped) => getComposerDraftInTransaction(scoped, {
@@ -4003,13 +4932,14 @@ async function getHumanComposerDraft(db, context) {
4003
4932
  );
4004
4933
  const mapped = composerDraft(row);
4005
4934
  if (mapped) return mapped;
4006
- const session = await getSession2(db, context.workspaceId, context.sessionId);
4935
+ const session = await getSession3(deps.db, context.workspaceId, context.sessionId);
4007
4936
  if (!session) throw new Error(`Session not found: ${context.sessionId}`);
4008
4937
  return {
4009
4938
  revision: 0,
4010
4939
  text: "",
4011
4940
  resources: [],
4012
4941
  tools: [],
4942
+ toolsProvided: false,
4013
4943
  model: session.model,
4014
4944
  reasoningEffort: reasoningEffortForMetadata2(session.metadata, "medium"),
4015
4945
  sourceTurnId: null,
@@ -4017,9 +4947,10 @@ async function getHumanComposerDraft(db, context) {
4017
4947
  updatedAt: null
4018
4948
  };
4019
4949
  }
4020
- async function saveHumanComposerDraft(db, context, input) {
4950
+ async function saveHumanComposerDraft(deps, context, input) {
4951
+ await authorizeHumanSessionCommand(deps, context, "session.composer.write");
4021
4952
  const row = await withWorkspaceSubjectRls2(
4022
- db,
4953
+ deps.db,
4023
4954
  context.workspaceId,
4024
4955
  context.subjectId,
4025
4956
  (scoped) => scoped.transaction(
@@ -4040,9 +4971,12 @@ export {
4040
4971
  MAX_ENVIRONMENTS_PER_WORKSPACE,
4041
4972
  MAX_RIGS_PER_WORKSPACE,
4042
4973
  MAX_VARIABLES_PER_ENVIRONMENT,
4974
+ SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS,
4043
4975
  SESSION_WORKFLOW_WAKE_DISPATCHER_PERIOD_MS,
4044
4976
  SESSION_WORKFLOW_WAKE_DISPATCHER_SCHEDULE_ID,
4045
4977
  SESSION_WORKFLOW_WAKE_DISPATCHER_WORKFLOW_TYPE,
4978
+ SessionAuthorizationDeniedError,
4979
+ SessionAuthorizationUnavailableError,
4046
4980
  acceptSessionUserMessage,
4047
4981
  activateRigVersionForApi,
4048
4982
  appendRigSetupCommand,
@@ -4051,12 +4985,15 @@ export {
4051
4985
  assertAllowedVariableSetVariableName,
4052
4986
  assertConfiguredModel,
4053
4987
  assertPackSandboxImageCompatible,
4988
+ assertToolRefsSubset,
4054
4989
  assertWorkspaceDeletable,
4055
4990
  assertWorkspaceMemberRemovable,
4056
4991
  assertWorkspaceModelPolicyAllows,
4992
+ availableToolRefs,
4057
4993
  buildCapabilityCatalog,
4058
4994
  buildFleetContextForSession,
4059
4995
  buildMarketingDailyAnalysisAgentConfig,
4996
+ canonicalConfiguredModel,
4060
4997
  checkLimit,
4061
4998
  classifyRigVerificationOutcome,
4062
4999
  controlAgentSessionWorkstream,
@@ -4114,10 +5051,13 @@ export {
4114
5051
  requireRigChangeForApi,
4115
5052
  requireRigForApi,
4116
5053
  requireScheduledTaskForApi,
5054
+ requireSessionAuthorization,
5055
+ requireSessionAuthorizationListScope,
4117
5056
  requireVariableSetEncryption,
4118
5057
  requireVariableSetForApi,
4119
5058
  resolveCapabilityPack,
4120
5059
  resolveMemberSubjectId,
5060
+ resolveSessionToolPolicy,
4121
5061
  restoreScheduledTask,
4122
5062
  rigActorForGrant,
4123
5063
  routingEnabled,
@@ -4127,6 +5067,7 @@ export {
4127
5067
  scheduledTaskToolsProvided,
4128
5068
  scheduledTaskTriggerToken,
4129
5069
  sendAgentSessionMessage,
5070
+ sessionWithEffectiveToolPolicy,
4130
5071
  settingsWithEnabledCapabilityMcpServers,
4131
5072
  settingsWithMcpCapabilityServers,
4132
5073
  settingsWithSessionMcpServerMetadata,
@@ -4137,16 +5078,21 @@ export {
4137
5078
  syncCreatedScheduledTask,
4138
5079
  syncUpdatedScheduledTask,
4139
5080
  updateRigForApi,
5081
+ updateSessionMcpApprovalPolicy,
4140
5082
  updateSessionTitle,
4141
5083
  validateFileResources,
4142
5084
  validateGitHubRepositorySelection,
4143
5085
  validateGitHubRepositorySelectionShape,
5086
+ validateGitHubRepositorySelectionShapes,
4144
5087
  validateMcpCapabilityConnection,
4145
5088
  validateToolRefs,
5089
+ validateToolRefsForSessionPolicy,
4146
5090
  validateVariableSetAttachment,
4147
5091
  validatedScheduledTaskUpdate,
4148
5092
  withDefaultEnabledCapabilityMcpTools,
4149
5093
  workflowIdForSession,
5094
+ workspaceSessionToolPolicyDefaultServerIds,
5095
+ workspaceSessionToolPolicyServerIds,
4150
5096
  wrapChannelABoxWithRouting
4151
5097
  };
4152
5098
  //# sourceMappingURL=index.js.map