@opengeni/core 0.4.9 → 0.8.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));
@@ -2417,6 +2701,7 @@ function withDefaultEnabledCapabilityMcpTools(tools, settings, runtimeSettings)
2417
2701
  function normalizeResources(resources) {
2418
2702
  const mountPaths = /* @__PURE__ */ new Map();
2419
2703
  const identities = /* @__PURE__ */ new Map();
2704
+ const credentialBindingProviders = /* @__PURE__ */ new Map();
2420
2705
  const seenResources = /* @__PURE__ */ new Set();
2421
2706
  const out = [];
2422
2707
  for (const resource of resources) {
@@ -2439,14 +2724,35 @@ function normalizeResources(resources) {
2439
2724
  throw new HTTPException8(422, { message: "repository URL must include owner and repo" });
2440
2725
  }
2441
2726
  const repo = parts.join("/");
2442
- const mountPath = normalizeMountPath(resource.mountPath ?? `repos/${repo}`);
2727
+ const normalizedUri = `https://${url.host.toLowerCase()}/${repo}.git`;
2728
+ const mountPath = normalizeMountPath(
2729
+ resource.mountPath ?? defaultRepositoryMountPath(normalizedUri)
2730
+ );
2731
+ const credentialProvider = gitCredentialProviderForRepository(resource);
2732
+ const credentialBindingId = gitCredentialBindingIdForRepository(resource, credentialProvider);
2733
+ if ((resource.credentialBindingId || resource.connectionId || resource.access) && !credentialProvider) {
2734
+ throw new HTTPException8(422, {
2735
+ message: "repository credential bindings and access intent require a Git provider"
2736
+ });
2737
+ }
2738
+ if (credentialProvider && credentialBindingId) {
2739
+ const boundProvider = credentialBindingProviders.get(credentialBindingId);
2740
+ if (boundProvider && boundProvider !== credentialProvider) {
2741
+ throw new HTTPException8(422, {
2742
+ message: `credential binding ${credentialBindingId} is assigned to multiple Git providers`
2743
+ });
2744
+ }
2745
+ credentialBindingProviders.set(credentialBindingId, credentialProvider);
2746
+ }
2443
2747
  normalized = {
2444
2748
  kind: "repository",
2445
- uri: `https://${url.hostname.toLowerCase()}/${repo}.git`,
2749
+ uri: normalizedUri,
2446
2750
  ref: resource.ref.trim(),
2447
2751
  mountPath,
2448
- ...resource.subpath ? { subpath: normalizeMountPath(resource.subpath) } : {},
2752
+ ...resource.subpath ? { subpath: normalizeRepositorySubpath(resource.subpath) } : {},
2449
2753
  ...resource.provider ? { provider: resource.provider } : {},
2754
+ ...resource.credentialBindingId ? { credentialBindingId: resource.credentialBindingId } : {},
2755
+ ...resource.access ? { access: resource.access } : {},
2450
2756
  ...resource.repositoryId !== void 0 ? { repositoryId: resource.repositoryId } : {},
2451
2757
  ...resource.installationId !== void 0 ? { installationId: resource.installationId } : {},
2452
2758
  ...resource.projectId !== void 0 ? { projectId: resource.projectId } : {},
@@ -2456,14 +2762,15 @@ function normalizeResources(resources) {
2456
2762
  };
2457
2763
  }
2458
2764
  const key = stableJson(normalized);
2459
- const mounted = normalized.mountPath ? mountPaths.get(normalized.mountPath) : void 0;
2765
+ const mountCollisionKey = normalized.mountPath ? resourceMountPathCollisionKey(normalized.mountPath) : void 0;
2766
+ const mounted = mountCollisionKey ? mountPaths.get(mountCollisionKey) : void 0;
2460
2767
  if (mounted && mounted !== key) {
2461
2768
  throw new HTTPException8(422, {
2462
2769
  message: `duplicate resource mount path: ${normalized.mountPath}`
2463
2770
  });
2464
2771
  }
2465
2772
  if (normalized.mountPath) {
2466
- mountPaths.set(normalized.mountPath, key);
2773
+ mountPaths.set(mountCollisionKey, key);
2467
2774
  }
2468
2775
  const identity = resourceIdentityKey(normalized);
2469
2776
  const seenIdentity = identities.get(identity);
@@ -2490,8 +2797,24 @@ function mergeResourceRefs(existing, additions) {
2490
2797
  throw error;
2491
2798
  }
2492
2799
  }
2800
+ function validateGitHubRepositorySelectionShapes(resources) {
2801
+ const selected = gitHubRepositorySelections(resources);
2802
+ if (selected.length === 0) {
2803
+ return [];
2804
+ }
2805
+ return [...new Set(selected.map((item) => item.installationId))];
2806
+ }
2493
2807
  function validateGitHubRepositorySelectionShape(resources) {
2494
- const selected = resources.flatMap((resource) => {
2808
+ const installationIds = validateGitHubRepositorySelectionShapes(resources);
2809
+ if (installationIds.length > 1) {
2810
+ throw new HTTPException8(422, {
2811
+ message: "GitHub App repository resources must belong to one installation"
2812
+ });
2813
+ }
2814
+ return installationIds[0] ?? null;
2815
+ }
2816
+ function gitHubRepositorySelections(resources) {
2817
+ return resources.flatMap((resource) => {
2495
2818
  if (resource.kind !== "repository") {
2496
2819
  return [];
2497
2820
  }
@@ -2503,38 +2826,34 @@ function validateGitHubRepositorySelectionShape(resources) {
2503
2826
  if (installationRaw === void 0 && repositoryRaw === void 0) {
2504
2827
  return [];
2505
2828
  }
2506
- const installationId2 = positiveInteger(installationRaw);
2829
+ const installationId = positiveInteger(installationRaw);
2507
2830
  const repositoryId = positiveInteger(repositoryRaw);
2508
- if (!installationId2 || !repositoryId) {
2831
+ if (!installationId || !repositoryId) {
2509
2832
  throw new HTTPException8(422, {
2510
2833
  message: "GitHub App repository resources require positive github_installation_id and github_repository_id"
2511
2834
  });
2512
2835
  }
2513
- return [{ installationId: installationId2, repositoryId }];
2836
+ return [{ installationId, repositoryId }];
2514
2837
  });
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
2838
  }
2526
2839
  async function validateGitHubRepositorySelection(db, workspaceId, resources) {
2527
- const installationId = validateGitHubRepositorySelectionShape(resources);
2528
- if (installationId === null) {
2840
+ const installationIds = validateGitHubRepositorySelectionShapes(resources);
2841
+ if (installationIds.length === 0) {
2529
2842
  return;
2530
2843
  }
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
- });
2844
+ const selections = gitHubRepositorySelections(resources);
2845
+ for (const installationId of installationIds) {
2846
+ const repositoryIds = selections.filter((selection) => selection.installationId === installationId).map((selection) => selection.repositoryId);
2847
+ if (!await areGitHubRepositoriesAllowedForWorkspace(
2848
+ db,
2849
+ workspaceId,
2850
+ installationId,
2851
+ repositoryIds
2852
+ )) {
2853
+ throw new HTTPException8(422, {
2854
+ message: "GitHub App repository resources must be authorized for a GitHub App installation linked to this workspace"
2855
+ });
2856
+ }
2538
2857
  }
2539
2858
  }
2540
2859
  async function validateFileResources(db, workspaceId, resources) {
@@ -2559,11 +2878,12 @@ async function validateFileResources(db, workspaceId, resources) {
2559
2878
  }
2560
2879
  }
2561
2880
  function normalizeMountPath(path) {
2562
- const normalized = path.trim().replace(/^\/+|\/+$/g, "");
2563
- if (!normalized || normalized.includes("..")) {
2881
+ try {
2882
+ return normalizeResourceMountPath(path);
2883
+ } catch (error) {
2884
+ if (!(error instanceof ResourceMountPathError)) throw error;
2564
2885
  throw new HTTPException8(422, { message: `invalid resource mount path: ${path}` });
2565
2886
  }
2566
- return normalized;
2567
2887
  }
2568
2888
  function parseResourceUrl(uri) {
2569
2889
  try {
@@ -2597,6 +2917,8 @@ import { CODEX_MODEL_ID_PREFIX } from "@opengeni/codex";
2597
2917
  import { configuredAllowedModels, policyProviderIdForModel } from "@opengeni/config";
2598
2918
  import {
2599
2919
  CreateSessionRequest,
2920
+ ServiceTurnInitiator,
2921
+ ServiceTurnInitiatorContext,
2600
2922
  evaluateWorkspaceModelPolicy,
2601
2923
  reasoningEffortForMetadata
2602
2924
  } from "@opengeni/contracts";
@@ -2611,7 +2933,8 @@ import {
2611
2933
  listDistinctVariableSetIdsInGroup,
2612
2934
  listDistinctRigVersionIdsInGroup,
2613
2935
  getSandbox as getSandbox3,
2614
- getSession,
2936
+ getSession as getSession2,
2937
+ SessionIdConflictError,
2615
2938
  getSessionByCreateIdempotencyKey,
2616
2939
  getSessionEvent,
2617
2940
  getWorkspaceControlEvent,
@@ -2619,11 +2942,14 @@ import {
2619
2942
  getSessionTurn,
2620
2943
  getWorkspaceModelPolicy,
2621
2944
  initializeSessionStartAtomically,
2945
+ listSessionTurns,
2946
+ listSessionMcpServersForChildInheritance,
2622
2947
  requireSession as requireSession2,
2623
2948
  submitHumanPromptInTransaction,
2624
2949
  updateSessionTitle as updateSessionTitleRow,
2625
2950
  withWorkspaceSubjectRls,
2626
2951
  QueueCommandConflictError,
2952
+ AgentCommandAuthorityError,
2627
2953
  SessionControlConflictError
2628
2954
  } from "@opengeni/db";
2629
2955
  import {
@@ -2636,6 +2962,72 @@ var reservedSessionMcpServerIds = /* @__PURE__ */ new Set(["opengeni", "files",
2636
2962
  var maxSessionMcpCredentialHeaders = 16;
2637
2963
  var maxSessionMcpCredentialHeaderValueLength = 4096;
2638
2964
  var sessionMcpCredentialHeaderName = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
2965
+ function serviceInitiatorForGrant(grant) {
2966
+ if (!grant.serviceInitiator) {
2967
+ if (grant.serviceInitiatorContext) {
2968
+ throw new HTTPException9(403, {
2969
+ message: "service initiator context requires a signed service initiator"
2970
+ });
2971
+ }
2972
+ return null;
2973
+ }
2974
+ const initiator = ServiceTurnInitiator.safeParse(grant.serviceInitiator);
2975
+ if (!initiator.success) {
2976
+ throw new HTTPException9(403, {
2977
+ message: "a delegated command initiator must be a bounded service principal"
2978
+ });
2979
+ }
2980
+ const context = ServiceTurnInitiatorContext.safeParse(grant.serviceInitiatorContext ?? {});
2981
+ if (!context.success) {
2982
+ throw new HTTPException9(403, {
2983
+ message: "delegated service initiator context is invalid or reserved"
2984
+ });
2985
+ }
2986
+ const callerTurnId = grant.metadata?.["turnId"];
2987
+ const callerAttemptId = grant.metadata?.["attemptId"];
2988
+ const callerExecutionGeneration = grant.metadata?.["executionGeneration"];
2989
+ if (callerTurnId !== void 0 || callerAttemptId !== void 0 || callerExecutionGeneration !== void 0) {
2990
+ throw new HTTPException9(403, {
2991
+ message: "a service initiator cannot replace an exact agent-attempt initiator"
2992
+ });
2993
+ }
2994
+ return {
2995
+ initiator: initiator.data,
2996
+ context: context.data
2997
+ };
2998
+ }
2999
+ function creationInitiatorForGrant(grant) {
3000
+ const serviceInitiator = serviceInitiatorForGrant(grant);
3001
+ const callerSessionId = grant.metadata?.["sessionId"];
3002
+ const callerTurnId = grant.metadata?.["turnId"];
3003
+ const callerAttemptId = grant.metadata?.["attemptId"];
3004
+ const callerExecutionGeneration = grant.metadata?.["executionGeneration"];
3005
+ const hasCallerTurnClaim = callerTurnId !== void 0 || callerAttemptId !== void 0 || callerExecutionGeneration !== void 0;
3006
+ if (hasCallerTurnClaim) {
3007
+ if (typeof callerSessionId !== "string" || typeof callerTurnId !== "string" || typeof callerAttemptId !== "string" || typeof callerExecutionGeneration !== "number" || !Number.isSafeInteger(callerExecutionGeneration) || callerExecutionGeneration < 1) {
3008
+ throw new HTTPException9(403, { message: "caller attempt claims are incomplete" });
3009
+ }
3010
+ const actor = {
3011
+ type: "agent_attempt",
3012
+ sessionId: callerSessionId,
3013
+ turnId: callerTurnId,
3014
+ attemptId: callerAttemptId,
3015
+ executionGeneration: callerExecutionGeneration
3016
+ };
3017
+ return { actor };
3018
+ }
3019
+ if (serviceInitiator) {
3020
+ return serviceInitiator;
3021
+ }
3022
+ return {
3023
+ initiator: {
3024
+ kind: "subject",
3025
+ subjectId: grant.subjectId,
3026
+ ...grant.subjectLabel ? { label: grant.subjectLabel } : {}
3027
+ },
3028
+ context: {}
3029
+ };
3030
+ }
2639
3031
  function normalizedSessionMcpCredentialHeaders(headers) {
2640
3032
  if (!headers) {
2641
3033
  return {};
@@ -2677,7 +3069,20 @@ function mcpServerConfigFromInput(server) {
2677
3069
  ...server.allowedTools ? { allowedTools: server.allowedTools } : {},
2678
3070
  ...server.timeoutMs ? { timeoutMs: server.timeoutMs } : {},
2679
3071
  cacheToolsList: server.cacheToolsList ?? false,
2680
- ...server.requireApproval !== void 0 ? { requireApproval: server.requireApproval } : {}
3072
+ ...server.requireApproval !== void 0 ? { requireApproval: server.requireApproval } : {},
3073
+ ...server.connectionRef ? { connectionRef: server.connectionRef } : {}
3074
+ };
3075
+ }
3076
+ function mcpServerConfigFromStoredInput(server) {
3077
+ return {
3078
+ id: server.id,
3079
+ ...server.name ? { name: server.name } : {},
3080
+ url: server.url,
3081
+ ...server.allowedTools ? { allowedTools: server.allowedTools } : {},
3082
+ ...server.timeoutMs ? { timeoutMs: server.timeoutMs } : {},
3083
+ cacheToolsList: server.cacheToolsList ?? false,
3084
+ ...server.requireApproval != null ? { requireApproval: server.requireApproval } : {},
3085
+ ...server.connectionRef ? { connectionRef: server.connectionRef } : {}
2681
3086
  };
2682
3087
  }
2683
3088
  function mcpServerConfigFromMetadata(server) {
@@ -2685,7 +3090,8 @@ function mcpServerConfigFromMetadata(server) {
2685
3090
  id: server.id,
2686
3091
  ...server.name ? { name: server.name } : {},
2687
3092
  url: server.url,
2688
- cacheToolsList: false
3093
+ cacheToolsList: false,
3094
+ ...server.connectionRef ? { connectionRef: server.connectionRef } : {}
2689
3095
  };
2690
3096
  }
2691
3097
  function settingsWithSessionMcpServerConfigs(settings, servers) {
@@ -2706,7 +3112,7 @@ function validateSessionMcpServersForCreate(settings, grant, servers) {
2706
3112
  return { runtimeServers: [], dbServers: [], metadata: [] };
2707
3113
  }
2708
3114
  requirePermission(grant, "mcp_servers:attach");
2709
- const encryptionKey = requireVariableSetEncryption(settings);
3115
+ const encryptionKey = servers.some((server) => Object.keys(server.headers ?? {}).length > 0) ? requireVariableSetEncryption(settings) : null;
2710
3116
  const existingIds = new Set(settings.mcpServers.map((server) => server.id));
2711
3117
  const seenIds = /* @__PURE__ */ new Set();
2712
3118
  const runtimeServers = [];
@@ -2736,6 +3142,7 @@ function validateSessionMcpServersForCreate(settings, grant, servers) {
2736
3142
  timeoutMs: server.timeoutMs ?? null,
2737
3143
  cacheToolsList: server.cacheToolsList ?? false,
2738
3144
  requireApproval: server.requireApproval ?? null,
3145
+ connectionRef: server.connectionRef ?? null,
2739
3146
  headersEncrypted
2740
3147
  });
2741
3148
  metadata.push({
@@ -2743,11 +3150,46 @@ function validateSessionMcpServersForCreate(settings, grant, servers) {
2743
3150
  name: server.name ?? null,
2744
3151
  url: server.url,
2745
3152
  headerNames: Object.keys(headersEncrypted).sort(),
2746
- credentialVersion: 1
3153
+ credentialVersion: 1,
3154
+ connectionRef: server.connectionRef ?? null
2747
3155
  });
2748
3156
  }
2749
3157
  return { runtimeServers, dbServers, metadata };
2750
3158
  }
3159
+ function validateInheritedSessionMcpServersForCreate(servers) {
3160
+ if (servers.length === 0) {
3161
+ return { runtimeServers: [], dbServers: [], metadata: [] };
3162
+ }
3163
+ const seenIds = /* @__PURE__ */ new Set();
3164
+ for (const server of servers) {
3165
+ if (seenIds.has(server.id)) {
3166
+ throw new HTTPException9(422, {
3167
+ message: `duplicate inherited session MCP server id: ${server.id}`
3168
+ });
3169
+ }
3170
+ seenIds.add(server.id);
3171
+ if (reservedSessionMcpServerIds.has(server.id)) {
3172
+ throw new HTTPException9(422, {
3173
+ message: `reserved inherited session MCP server id: ${server.id}`
3174
+ });
3175
+ }
3176
+ }
3177
+ return {
3178
+ runtimeServers: servers.map(mcpServerConfigFromStoredInput),
3179
+ dbServers: servers.map((server) => ({
3180
+ ...server,
3181
+ headersEncrypted: { ...server.headersEncrypted ?? {} }
3182
+ })),
3183
+ metadata: servers.map((server) => ({
3184
+ id: server.id,
3185
+ name: server.name ?? null,
3186
+ url: server.url,
3187
+ headerNames: Object.keys(server.headersEncrypted ?? {}).sort(),
3188
+ credentialVersion: 1,
3189
+ connectionRef: server.connectionRef ?? null
3190
+ }))
3191
+ };
3192
+ }
2751
3193
  function validateSessionMcpCredentialUpdates(input) {
2752
3194
  if (input.updates.length === 0) {
2753
3195
  return [];
@@ -2792,18 +3234,26 @@ async function createAndStartSession(input) {
2792
3234
  input.createIdempotencyKey
2793
3235
  );
2794
3236
  if (existing) {
3237
+ if (input.requestedSessionId && existing.id !== input.requestedSessionId) {
3238
+ throw new SessionIdConflictError(input.requestedSessionId);
3239
+ }
2795
3240
  return await finishStartSession(
2796
3241
  existing.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
2797
3242
  existing
2798
3243
  );
2799
3244
  }
2800
3245
  const { session: keyed, created } = await createSessionWithIdempotencyKey(input.db, {
3246
+ ...input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {},
2801
3247
  accountId: input.accountId,
2802
3248
  workspaceId: input.workspaceId,
2803
3249
  initialMessage: input.initialMessage,
3250
+ initialTurnInstructions: input.turnInstructions ?? null,
2804
3251
  resources: input.resources,
2805
3252
  tools: input.tools,
2806
3253
  metadata: sessionMetadata,
3254
+ ...input.createdBy ? { createdBy: input.createdBy } : {},
3255
+ ...input.createdByContext ? { createdByContext: input.createdByContext } : {},
3256
+ createdByActor: input.createdByActor ?? null,
2807
3257
  model: input.model,
2808
3258
  sandboxBackend: input.sandboxBackend,
2809
3259
  variableSetId: input.variableSet?.id ?? null,
@@ -2826,12 +3276,17 @@ async function createAndStartSession(input) {
2826
3276
  return await finishStartSession(input, keyed);
2827
3277
  }
2828
3278
  const session = await createSession(input.db, {
3279
+ ...input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {},
2829
3280
  accountId: input.accountId,
2830
3281
  workspaceId: input.workspaceId,
2831
3282
  initialMessage: input.initialMessage,
3283
+ initialTurnInstructions: input.turnInstructions ?? null,
2832
3284
  resources: input.resources,
2833
3285
  tools: input.tools,
2834
3286
  metadata: sessionMetadata,
3287
+ ...input.createdBy ? { createdBy: input.createdBy } : {},
3288
+ ...input.createdByContext ? { createdByContext: input.createdByContext } : {},
3289
+ createdByActor: input.createdByActor ?? null,
2835
3290
  model: input.model,
2836
3291
  sandboxBackend: input.sandboxBackend,
2837
3292
  variableSetId: input.variableSet?.id ?? null,
@@ -2900,7 +3355,9 @@ async function finishStartSession(input, session) {
2900
3355
  wakeRevision: started.workflowWakeRevision
2901
3356
  });
2902
3357
  }
2903
- return await requireSession2(input.db, session.workspaceId, session.id);
3358
+ const persisted = await requireSession2(input.db, session.workspaceId, session.id);
3359
+ const initialTurnId = started.turn?.id ?? (await listSessionTurns(input.db, session.workspaceId, session.id, 1))[0]?.id ?? null;
3360
+ return { ...persisted, initialTurnId };
2904
3361
  }
2905
3362
  function workflowIdForSession(sessionId) {
2906
3363
  return `session-${sessionId}`;
@@ -2967,12 +3424,17 @@ async function postUserMessageTurn(input) {
2967
3424
  workspaceId,
2968
3425
  sessionId,
2969
3426
  subjectId: input.actor ?? accountId,
2970
- actor: { type: "human", subjectId: input.actor ?? accountId },
3427
+ ...input.actorLabel ? { subjectLabel: input.actorLabel } : {},
3428
+ actor: input.commandActor ?? {
3429
+ type: "human",
3430
+ subjectId: input.actor ?? accountId
3431
+ },
2971
3432
  operationKey,
2972
3433
  delivery: input.delivery ?? "send",
2973
3434
  controlEtag: input.controlEtag ?? null,
2974
3435
  expectedDraftRevision: input.expectedDraftRevision ?? null,
2975
3436
  text: input.text,
3437
+ turnInstructions: input.turnInstructions ?? null,
2976
3438
  resources: input.resources,
2977
3439
  tools: input.tools,
2978
3440
  model: requestedModel,
@@ -3044,23 +3506,40 @@ async function postUserMessageTurn(input) {
3044
3506
  async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
3045
3507
  const { settings, db, bus, workflowClient, objectStorage } = deps;
3046
3508
  const payload = CreateSessionRequest.parse(rawPayload);
3509
+ const parentSessionId = typeof grant.metadata?.["sessionId"] === "string" ? grant.metadata["sessionId"] : null;
3510
+ if (parentSessionId) {
3511
+ await requireSessionAuthorization(deps, grant, {
3512
+ sessionId: parentSessionId,
3513
+ operation: "session.child.create",
3514
+ surface: "core"
3515
+ });
3516
+ }
3517
+ const parentSession = parentSessionId ? await getSession2(db, workspaceId, parentSessionId) : null;
3518
+ if (parentSessionId && !parentSession) {
3519
+ throw new HTTPException9(404, {
3520
+ message: `parent session not found in workspace: ${parentSessionId}`
3521
+ });
3522
+ }
3047
3523
  const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
3048
3524
  db,
3049
3525
  workspaceId,
3050
3526
  settings
3051
3527
  );
3052
- const sessionMcpServers = validateSessionMcpServersForCreate(
3053
- capabilityRuntimeSettings,
3054
- grant,
3055
- payload.mcpServers
3056
- );
3528
+ const sessionMcpServers = hasOwnProperty(rawPayload, "mcpServers") ? validateSessionMcpServersForCreate(capabilityRuntimeSettings, grant, payload.mcpServers) : parentSession ? validateInheritedSessionMcpServersForCreate(
3529
+ await listSessionMcpServersForChildInheritance(db, workspaceId, parentSession.id)
3530
+ ) : validateSessionMcpServersForCreate(capabilityRuntimeSettings, grant, payload.mcpServers);
3057
3531
  const runtimeSettings = settingsWithSessionMcpServerConfigs(
3058
3532
  capabilityRuntimeSettings,
3059
3533
  sessionMcpServers.runtimeServers
3060
3534
  );
3061
- const resources = normalizeResources(payload.resources);
3062
- const requestedTools = validateToolRefs(payload.tools, runtimeSettings);
3063
- const defaultedTools = hasOwnProperty(rawPayload, "tools") ? requestedTools : withDefaultEnabledCapabilityMcpTools(requestedTools, settings, capabilityRuntimeSettings);
3535
+ const resources = normalizeResources(
3536
+ hasOwnProperty(rawPayload, "resources") ? payload.resources : parentSession?.resources ?? payload.resources
3537
+ );
3538
+ const requestedTools = validateToolRefs(
3539
+ hasOwnProperty(rawPayload, "tools") ? payload.tools : parentSession?.tools ?? payload.tools,
3540
+ runtimeSettings
3541
+ );
3542
+ const defaultedTools = hasOwnProperty(rawPayload, "tools") || parentSession ? requestedTools : withDefaultEnabledCapabilityMcpTools(requestedTools, settings, capabilityRuntimeSettings);
3064
3543
  const tools = withFirstPartyTools(defaultedTools, runtimeSettings);
3065
3544
  await validateGitHubRepositorySelection(db, workspaceId, resources);
3066
3545
  if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
@@ -3098,7 +3577,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
3098
3577
  );
3099
3578
  const model = payload.model ?? settings.openaiModel;
3100
3579
  const reasoningEffort = payload.reasoningEffort ?? settings.openaiReasoningEffort;
3101
- let firstPartyMcpPermissions = payload.firstPartyMcpPermissions ?? null;
3580
+ let firstPartyMcpPermissions = payload.firstPartyMcpPermissions ?? (parentSessionId ? [...new Set(grant.permissions)] : null);
3102
3581
  if (firstPartyMcpPermissions && firstPartyMcpPermissions.length === 0) {
3103
3582
  throw new HTTPException9(422, {
3104
3583
  message: "firstPartyMcpPermissions must not be empty; omit it for the default worker permission set"
@@ -3112,9 +3591,10 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
3112
3591
  }
3113
3592
  }
3114
3593
  if (payload.goal && firstPartyMcpPermissions && !firstPartyMcpPermissions.includes("goals:manage")) {
3115
- firstPartyMcpPermissions = [...firstPartyMcpPermissions, "goals:manage"];
3594
+ throw new HTTPException9(422, {
3595
+ message: "goal-bearing sessions require goals:manage in the resulting first-party MCP permission set"
3596
+ });
3116
3597
  }
3117
- const parentSessionId = typeof grant.metadata?.["sessionId"] === "string" ? grant.metadata["sessionId"] : null;
3118
3598
  const sandboxChoice = payload.sandbox ?? (parentSessionId ? "shared" : "new");
3119
3599
  let sandboxGroupId = null;
3120
3600
  let inheritedBackend;
@@ -3127,12 +3607,10 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
3127
3607
  message: "sandbox:'shared' requires a parent session (spawn from inside a session); use 'new' for a top-level create."
3128
3608
  });
3129
3609
  }
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
- });
3610
+ if (!parentSession) {
3611
+ throw new Error("trusted parent session was not resolved");
3135
3612
  }
3613
+ const parent = parentSession;
3136
3614
  const parentBoxed = parent.sandboxBackend !== "none";
3137
3615
  const variableSetMismatch = parentBoxed && !variableSetMatchesGroup(parent.variableSetId ?? null);
3138
3616
  let rigMismatch = parentBoxed && !rigVersionMatchesGroup(parent.rigVersionId ?? null);
@@ -3218,50 +3696,69 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
3218
3696
  quantity: 1,
3219
3697
  model
3220
3698
  });
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
- });
3699
+ const creationInitiator = creationInitiatorForGrant(grant);
3700
+ let session;
3701
+ try {
3702
+ session = await createAndStartSession({
3703
+ ...payload.requestedSessionId ? { requestedSessionId: payload.requestedSessionId } : {},
3704
+ db,
3705
+ bus,
3706
+ workflowClient,
3707
+ accountId: grant.accountId,
3708
+ workspaceId,
3709
+ initialMessage: payload.initialMessage,
3710
+ turnInstructions: payload.turnInstructions ?? null,
3711
+ resources,
3712
+ tools,
3713
+ ...payload.clientEventId ? { clientEventId: payload.clientEventId } : {},
3714
+ model,
3715
+ reasoningEffort,
3716
+ // A shared spawn inherits the box's backend; a caller-supplied
3717
+ // sandboxBackend on a shared spawn is ignored (it is the same box). A
3718
+ // machine-targeted top-level create labels the home "selfhosted"
3719
+ // (machineHomeBackend), overriding the caller/deployment default so the row
3720
+ // matches where the session actually runs.
3721
+ sandboxBackend: inheritedBackend ?? machineHomeBackend ?? payload.sandboxBackend ?? settings.sandboxBackend,
3722
+ // Mirror the backend relabel on the OS axis: only a machine-targeted
3723
+ // top-level create carries a derived OS; everything else is omitted and the
3724
+ // "linux" default holds (shared spawns keep the parent-box behavior).
3725
+ ...machineHomeOs ? { sandboxOs: machineHomeOs } : {},
3726
+ sandboxGroupId,
3727
+ metadata: payload.metadata,
3728
+ ...creationInitiator.initiator ? { createdBy: creationInitiator.initiator } : {},
3729
+ ...creationInitiator.context ? { createdByContext: creationInitiator.context } : {},
3730
+ createdByActor: creationInitiator.actor ?? null,
3731
+ variableSet: variableSet ? { id: variableSet.id, name: variableSet.name } : null,
3732
+ // Frozen rig binding (M3): both null for a rig-less session (today's path).
3733
+ rigId: frozenRigId,
3734
+ rigVersionId: frozenRigVersionId,
3735
+ goal: payload.goal ?? null,
3736
+ // Per-session persona instructions (already trimmed/validated by the
3737
+ // contracts schema). Persisted on the row; composed system-level at turn
3738
+ // time. Not surfaced as an event.
3739
+ instructions: payload.instructions ?? null,
3740
+ firstPartyMcpPermissions,
3741
+ mcpServers: sessionMcpServers.dbServers,
3742
+ sessionMcpServers: sessionMcpServers.metadata,
3743
+ parentSessionId,
3744
+ createIdempotencyKey: payload.idempotencyKey ?? null,
3745
+ // Create-time machine targeting (A-2a): when a target sandbox is named, the
3746
+ // active-sandbox pointer is seeded race-free inside createAndStartSession
3747
+ // (after the row exists, before the first turn dispatches). Validation
3748
+ // (ownership/liveness) lives in swapActiveSandbox; an invalid target 422s.
3749
+ seedTargetSandbox: payload.targetSandboxId ? { sandboxId: payload.targetSandboxId, settings, workingDir: payload.workingDir ?? null } : null
3750
+ });
3751
+ } catch (error) {
3752
+ if (error instanceof AgentCommandAuthorityError) {
3753
+ throw new HTTPException9(403, { message: error.message });
3754
+ }
3755
+ if (error instanceof SessionIdConflictError) {
3756
+ throw new HTTPException9(409, {
3757
+ message: "requested session id is already in use"
3758
+ });
3759
+ }
3760
+ throw error;
3761
+ }
3265
3762
  await recordWorkspaceUsage(deps, {
3266
3763
  accountId: grant.accountId,
3267
3764
  workspaceId,
@@ -3271,12 +3768,21 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
3271
3768
  unit: "run",
3272
3769
  sourceResourceType: "session",
3273
3770
  sourceResourceId: session.id,
3771
+ sessionId: session.id,
3772
+ initiator: session.createdBy,
3773
+ initiatorContext: session.createdByContext,
3774
+ origin: creationInitiator.actor ? "system" : "user",
3274
3775
  idempotencyKey: `agent_run.created:${workspaceId}:${session.id}`
3275
3776
  });
3276
3777
  return session;
3277
3778
  }
3278
3779
  async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, input) {
3279
3780
  const { settings, db, bus, workflowClient, objectStorage } = deps;
3781
+ await requireSessionAuthorization(deps, grant, {
3782
+ sessionId,
3783
+ operation: input.delivery === "steer" ? "session.steer" : "session.append",
3784
+ surface: "core"
3785
+ });
3280
3786
  const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
3281
3787
  db,
3282
3788
  workspaceId,
@@ -3311,6 +3817,7 @@ async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, inp
3311
3817
  session: existingSession,
3312
3818
  updates: input.mcpCredentialUpdates ?? []
3313
3819
  });
3820
+ const delegatedServiceInitiator = serviceInitiatorForGrant(grant);
3314
3821
  const { accepted, turn } = await postUserMessageTurn({
3315
3822
  db,
3316
3823
  bus,
@@ -3320,14 +3827,24 @@ async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, inp
3320
3827
  workspaceId,
3321
3828
  sessionId,
3322
3829
  text: input.text,
3830
+ turnInstructions: input.turnInstructions ?? null,
3323
3831
  resources: requestedResources,
3324
3832
  tools: requestedTools,
3325
3833
  model: input.model ?? null,
3326
3834
  reasoningEffort: input.reasoningEffort ?? null,
3327
3835
  mcpCredentialUpdates,
3328
3836
  delivery: input.delivery ?? "send",
3329
- origin: input.origin ?? "human",
3837
+ origin: delegatedServiceInitiator ? "operator" : input.origin ?? "human",
3330
3838
  actor: grant.subjectId,
3839
+ ...grant.subjectLabel ? { actorLabel: grant.subjectLabel } : {},
3840
+ ...delegatedServiceInitiator ? {
3841
+ commandActor: {
3842
+ type: "service",
3843
+ subjectId: delegatedServiceInitiator.initiator.subjectId,
3844
+ ...delegatedServiceInitiator.initiator.label ? { subjectLabel: delegatedServiceInitiator.initiator.label } : {},
3845
+ context: delegatedServiceInitiator.context
3846
+ }
3847
+ } : {},
3331
3848
  ...input.controlEtag !== void 0 ? { controlEtag: input.controlEtag } : {},
3332
3849
  ...input.expectedDraftRevision !== void 0 ? { expectedDraftRevision: input.expectedDraftRevision } : {},
3333
3850
  ...input.clientEventId ? { clientEventId: input.clientEventId } : {}
@@ -3341,12 +3858,23 @@ async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, inp
3341
3858
  unit: "run",
3342
3859
  sourceResourceType: "session_turn",
3343
3860
  sourceResourceId: turn.id,
3861
+ sessionId,
3862
+ turnId: turn.id,
3863
+ initiator: turn.initiator,
3864
+ initiatorContext: turn.initiatorContext,
3865
+ origin: turn.source,
3344
3866
  idempotencyKey: `agent_run.created:${workspaceId}:${turn.id}`
3345
3867
  });
3346
3868
  return { accepted, turn };
3347
3869
  }
3348
- async function updateSessionTitle(deps, workspaceId, sessionId, title, source) {
3870
+ async function updateSessionTitle(deps, grant, sessionId, title, source) {
3349
3871
  const { db, bus } = deps;
3872
+ const authorization = await requireSessionAuthorization(deps, grant, {
3873
+ sessionId,
3874
+ operation: "session.title.write",
3875
+ surface: "core"
3876
+ });
3877
+ const workspaceId = grant.workspaceId;
3350
3878
  const result = await updateSessionTitleRow(db, { workspaceId, sessionId, title, source });
3351
3879
  if (result.updated) {
3352
3880
  await appendAndPublishEvents(db, bus, workspaceId, sessionId, [
@@ -3359,10 +3887,25 @@ async function updateSessionTitle(deps, workspaceId, sessionId, title, source) {
3359
3887
  }
3360
3888
  ]);
3361
3889
  }
3362
- return result;
3890
+ return {
3891
+ ...result,
3892
+ relatedSessionAccess: authorization?.relatedSessionAccess ?? "root"
3893
+ };
3363
3894
  }
3364
- async function readSessionLineage(db, workspaceId, sessionId) {
3365
- const lineage = await getSessionLineage(db, workspaceId, sessionId);
3895
+ async function readSessionLineage(deps, grant, sessionId) {
3896
+ const authorization = await requireSessionAuthorization(deps, grant, {
3897
+ sessionId,
3898
+ operation: "session.lineage.read",
3899
+ surface: "core"
3900
+ });
3901
+ if (authorization?.relatedSessionAccess === "target") {
3902
+ const session = await getSession2(deps.db, grant.workspaceId, sessionId);
3903
+ if (!session) {
3904
+ throw new HTTPException9(404, { message: "session not found" });
3905
+ }
3906
+ return { ancestors: [], children: [], truncated: false };
3907
+ }
3908
+ const lineage = await getSessionLineage(deps.db, grant.workspaceId, sessionId);
3366
3909
  if (!lineage) {
3367
3910
  throw new HTTPException9(404, { message: "session not found" });
3368
3911
  }
@@ -3651,13 +4194,15 @@ import {
3651
4194
  deleteSessionQueueItemInTransaction,
3652
4195
  editQueuedTurnInTransaction,
3653
4196
  getComposerDraftInTransaction,
3654
- getSession as getSession2,
4197
+ getSession as getSession3,
3655
4198
  getSessionEvent as getSessionEvent2,
3656
4199
  getWorkspaceControlEvent as getWorkspaceControlEvent2,
3657
4200
  getSessionQueueSnapshot,
3658
4201
  moveQueuedTurnInTransaction,
3659
4202
  mutateSessionControlInTransaction,
3660
4203
  mutateWorkspaceControlInTransaction,
4204
+ projectEffectiveControlForRelatedAccess,
4205
+ runIdempotentPersistenceTransaction,
3661
4206
  saveComposerDraftInTransaction,
3662
4207
  sendAgentMessageInTransaction,
3663
4208
  serializeEffectiveSessionControl,
@@ -3670,6 +4215,42 @@ import {
3670
4215
  publishDurableSessionEvents as publishDurableSessionEvents2,
3671
4216
  publishDurableWorkspaceControlEvent as publishDurableWorkspaceControlEvent2
3672
4217
  } from "@opengeni/events";
4218
+ function humanAccessGrant(context) {
4219
+ return {
4220
+ accountId: context.accountId,
4221
+ workspaceId: context.workspaceId,
4222
+ subjectId: context.subjectId,
4223
+ permissions: []
4224
+ };
4225
+ }
4226
+ function agentAccessGrant(context) {
4227
+ return {
4228
+ accountId: context.accountId,
4229
+ workspaceId: context.workspaceId,
4230
+ subjectId: context.subjectId,
4231
+ permissions: [],
4232
+ metadata: {
4233
+ sessionId: context.callerSessionId,
4234
+ turnId: context.callerTurnId,
4235
+ attemptId: context.callerAttemptId,
4236
+ executionGeneration: context.callerExecutionGeneration
4237
+ }
4238
+ };
4239
+ }
4240
+ async function authorizeHumanSessionCommand(deps, context, operation) {
4241
+ return await requireSessionAuthorization(deps, humanAccessGrant(context), {
4242
+ sessionId: context.sessionId,
4243
+ operation,
4244
+ surface: "core"
4245
+ });
4246
+ }
4247
+ async function authorizeAgentSessionCommand(deps, context, targetSessionId, operation) {
4248
+ return await requireSessionAuthorization(deps, agentAccessGrant(context), {
4249
+ sessionId: targetSessionId,
4250
+ operation,
4251
+ surface: "core"
4252
+ });
4253
+ }
3673
4254
  function agentActor(context) {
3674
4255
  return {
3675
4256
  type: "agent_attempt",
@@ -3679,6 +4260,20 @@ function agentActor(context) {
3679
4260
  executionGeneration: context.callerExecutionGeneration
3680
4261
  };
3681
4262
  }
4263
+ async function runAgentCommandPersistenceTransaction(deps, context, input) {
4264
+ return await runIdempotentPersistenceTransaction(
4265
+ {
4266
+ stage: input.stage,
4267
+ eventTypes: input.eventTypes,
4268
+ maxAttempts: 3
4269
+ },
4270
+ async () => await withWorkspaceRls(
4271
+ deps.db,
4272
+ context.workspaceId,
4273
+ async (scoped) => scoped.transaction(async (tx) => await input.transaction(tx))
4274
+ )
4275
+ );
4276
+ }
3682
4277
  async function publishAndWakeAgentCommand(deps, input) {
3683
4278
  await publishSessionEventIds(deps, input.workspaceId, input.sessionId, input.eventIds);
3684
4279
  if (!input.shouldSignal || input.wakeRevision === null) return;
@@ -3730,20 +4325,19 @@ async function publishWorkspaceControlEvent(deps, workspaceId, eventId) {
3730
4325
  await publishDurableWorkspaceControlEvent2(deps.bus, workspaceId, event);
3731
4326
  }
3732
4327
  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
- );
4328
+ await authorizeAgentSessionCommand(deps, context, input.targetSessionId, "session.append");
4329
+ const result = await runAgentCommandPersistenceTransaction(deps, context, {
4330
+ stage: "session_commands.agent_message",
4331
+ eventTypes: ["system.update.pending"],
4332
+ transaction: async (tx) => await sendAgentMessageInTransaction(tx, {
4333
+ accountId: context.accountId,
4334
+ workspaceId: context.workspaceId,
4335
+ targetSessionId: input.targetSessionId,
4336
+ actor: agentActor(context),
4337
+ operationKey: input.idempotencyKey,
4338
+ text: input.text
4339
+ })
4340
+ });
3747
4341
  await publishAndWakeAgentCommand(deps, {
3748
4342
  accountId: context.accountId,
3749
4343
  workspaceId: context.workspaceId,
@@ -3758,20 +4352,19 @@ async function sendAgentSessionMessage(deps, context, input) {
3758
4352
  return result;
3759
4353
  }
3760
4354
  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
- );
4355
+ await authorizeAgentSessionCommand(deps, context, input.targetSessionId, "session.steer");
4356
+ const result = await runAgentCommandPersistenceTransaction(deps, context, {
4357
+ stage: "session_commands.agent_steer",
4358
+ eventTypes: ["session.control.steer_requested", "system.update.pending", "turn.superseded"],
4359
+ transaction: async (tx) => await steerAgentSessionInTransaction(tx, {
4360
+ accountId: context.accountId,
4361
+ workspaceId: context.workspaceId,
4362
+ targetSessionId: input.targetSessionId,
4363
+ actor: agentActor(context),
4364
+ operationKey: input.idempotencyKey,
4365
+ instruction: input.instruction
4366
+ })
4367
+ });
3775
4368
  await publishAndWakeAgentCommand(deps, {
3776
4369
  accountId: context.accountId,
3777
4370
  workspaceId: context.workspaceId,
@@ -3786,6 +4379,7 @@ async function steerAgentSession(deps, context, input) {
3786
4379
  return result;
3787
4380
  }
3788
4381
  async function controlAgentSessionWorkstream(deps, context, input) {
4382
+ await authorizeAgentSessionCommand(deps, context, input.targetSessionId, "session.control");
3789
4383
  const result = await withWorkspaceRls(
3790
4384
  deps.db,
3791
4385
  context.workspaceId,
@@ -3836,12 +4430,20 @@ function composerDraft(row) {
3836
4430
  updatedAt: row.updatedAt.toISOString()
3837
4431
  };
3838
4432
  }
3839
- async function authoritativeQueue(db, workspaceId, sessionId) {
4433
+ async function authoritativeQueue(db, workspaceId, sessionId, relatedSessionAccess) {
3840
4434
  const snapshot = await getSessionQueueSnapshot(db, workspaceId, sessionId);
3841
4435
  if (!snapshot) throw new Error(`Session not found: ${sessionId}`);
3842
- return snapshot;
4436
+ return {
4437
+ ...snapshot,
4438
+ effectiveControl: projectEffectiveControlForRelatedAccess(
4439
+ snapshot.effectiveControl,
4440
+ sessionId,
4441
+ relatedSessionAccess
4442
+ )
4443
+ };
3843
4444
  }
3844
4445
  async function moveHumanQueuePrompt(deps, context, turnId, input) {
4446
+ const authorization = await authorizeHumanSessionCommand(deps, context, "session.queue.control");
3845
4447
  const result = await withWorkspaceRls(
3846
4448
  deps.db,
3847
4449
  context.workspaceId,
@@ -3858,12 +4460,18 @@ async function moveHumanQueuePrompt(deps, context, turnId, input) {
3858
4460
  );
3859
4461
  const response = {
3860
4462
  receipt: receipt(result.receipt),
3861
- snapshot: await authoritativeQueue(deps.db, context.workspaceId, context.sessionId)
4463
+ snapshot: await authoritativeQueue(
4464
+ deps.db,
4465
+ context.workspaceId,
4466
+ context.sessionId,
4467
+ authorization?.relatedSessionAccess ?? "root"
4468
+ )
3862
4469
  };
3863
4470
  await publishSessionEventIds(deps, context.workspaceId, context.sessionId, result.eventIds);
3864
4471
  return response;
3865
4472
  }
3866
4473
  async function deleteHumanQueuePrompt(deps, context, turnId, input) {
4474
+ const authorization = await authorizeHumanSessionCommand(deps, context, "session.queue.control");
3867
4475
  const result = await withWorkspaceRls(
3868
4476
  deps.db,
3869
4477
  context.workspaceId,
@@ -3880,12 +4488,18 @@ async function deleteHumanQueuePrompt(deps, context, turnId, input) {
3880
4488
  );
3881
4489
  const response = {
3882
4490
  receipt: receipt(result.receipt),
3883
- snapshot: await authoritativeQueue(deps.db, context.workspaceId, context.sessionId)
4491
+ snapshot: await authoritativeQueue(
4492
+ deps.db,
4493
+ context.workspaceId,
4494
+ context.sessionId,
4495
+ authorization?.relatedSessionAccess ?? "root"
4496
+ )
3884
4497
  };
3885
4498
  await publishSessionEventIds(deps, context.workspaceId, context.sessionId, result.eventIds);
3886
4499
  return response;
3887
4500
  }
3888
4501
  async function editHumanQueuePrompt(deps, context, turnId, input) {
4502
+ const authorization = await authorizeHumanSessionCommand(deps, context, "session.queue.control");
3889
4503
  const result = await withWorkspaceSubjectRls2(
3890
4504
  deps.db,
3891
4505
  context.workspaceId,
@@ -3904,13 +4518,19 @@ async function editHumanQueuePrompt(deps, context, turnId, input) {
3904
4518
  );
3905
4519
  const response = {
3906
4520
  receipt: receipt(result.receipt),
3907
- snapshot: await authoritativeQueue(deps.db, context.workspaceId, context.sessionId),
4521
+ snapshot: await authoritativeQueue(
4522
+ deps.db,
4523
+ context.workspaceId,
4524
+ context.sessionId,
4525
+ authorization?.relatedSessionAccess ?? "root"
4526
+ ),
3908
4527
  draft: composerDraft(result.draft)
3909
4528
  };
3910
4529
  await publishSessionEventIds(deps, context.workspaceId, context.sessionId, result.eventIds);
3911
4530
  return response;
3912
4531
  }
3913
4532
  async function steerHumanQueuePrompt(deps, context, turnId, input) {
4533
+ const authorization = await authorizeHumanSessionCommand(deps, context, "session.queue.control");
3914
4534
  const result = await withWorkspaceRls(
3915
4535
  deps.db,
3916
4536
  context.workspaceId,
@@ -3927,13 +4547,19 @@ async function steerHumanQueuePrompt(deps, context, turnId, input) {
3927
4547
  );
3928
4548
  const response = {
3929
4549
  receipt: receipt(result.receipt),
3930
- snapshot: await authoritativeQueue(deps.db, context.workspaceId, context.sessionId)
4550
+ snapshot: await authoritativeQueue(
4551
+ deps.db,
4552
+ context.workspaceId,
4553
+ context.sessionId,
4554
+ authorization?.relatedSessionAccess ?? "root"
4555
+ )
3931
4556
  };
3932
4557
  await publishSessionEventIds(deps, context.workspaceId, context.sessionId, result.eventIds);
3933
4558
  await publishWorkspaceControlEvent(deps, context.workspaceId, result.workspaceControlEventId);
3934
4559
  return response;
3935
4560
  }
3936
4561
  async function controlHumanSessionWorkstream(deps, context, input) {
4562
+ const authorization = await authorizeHumanSessionCommand(deps, context, "session.control");
3937
4563
  const result = await withWorkspaceRls(
3938
4564
  deps.db,
3939
4565
  context.workspaceId,
@@ -3952,7 +4578,11 @@ async function controlHumanSessionWorkstream(deps, context, input) {
3952
4578
  );
3953
4579
  const response = {
3954
4580
  receipt: receipt(result.receipt),
3955
- effectiveControl: serializeEffectiveSessionControl(result.control),
4581
+ effectiveControl: projectEffectiveControlForRelatedAccess(
4582
+ serializeEffectiveSessionControl(result.control),
4583
+ context.sessionId,
4584
+ authorization?.relatedSessionAccess ?? "root"
4585
+ ),
3956
4586
  interruptionCount: result.interruptionCount,
3957
4587
  wakeCount: result.wakeCount
3958
4588
  };
@@ -3990,9 +4620,10 @@ async function controlHumanWorkspace(deps, context, input) {
3990
4620
  await requestControlWakeDispatch(deps, result.wakeCount);
3991
4621
  return response;
3992
4622
  }
3993
- async function getHumanComposerDraft(db, context) {
4623
+ async function getHumanComposerDraft(deps, context) {
4624
+ await authorizeHumanSessionCommand(deps, context, "session.composer.read");
3994
4625
  const row = await withWorkspaceSubjectRls2(
3995
- db,
4626
+ deps.db,
3996
4627
  context.workspaceId,
3997
4628
  context.subjectId,
3998
4629
  (scoped) => getComposerDraftInTransaction(scoped, {
@@ -4003,7 +4634,7 @@ async function getHumanComposerDraft(db, context) {
4003
4634
  );
4004
4635
  const mapped = composerDraft(row);
4005
4636
  if (mapped) return mapped;
4006
- const session = await getSession2(db, context.workspaceId, context.sessionId);
4637
+ const session = await getSession3(deps.db, context.workspaceId, context.sessionId);
4007
4638
  if (!session) throw new Error(`Session not found: ${context.sessionId}`);
4008
4639
  return {
4009
4640
  revision: 0,
@@ -4017,9 +4648,10 @@ async function getHumanComposerDraft(db, context) {
4017
4648
  updatedAt: null
4018
4649
  };
4019
4650
  }
4020
- async function saveHumanComposerDraft(db, context, input) {
4651
+ async function saveHumanComposerDraft(deps, context, input) {
4652
+ await authorizeHumanSessionCommand(deps, context, "session.composer.write");
4021
4653
  const row = await withWorkspaceSubjectRls2(
4022
- db,
4654
+ deps.db,
4023
4655
  context.workspaceId,
4024
4656
  context.subjectId,
4025
4657
  (scoped) => scoped.transaction(
@@ -4040,9 +4672,12 @@ export {
4040
4672
  MAX_ENVIRONMENTS_PER_WORKSPACE,
4041
4673
  MAX_RIGS_PER_WORKSPACE,
4042
4674
  MAX_VARIABLES_PER_ENVIRONMENT,
4675
+ SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS,
4043
4676
  SESSION_WORKFLOW_WAKE_DISPATCHER_PERIOD_MS,
4044
4677
  SESSION_WORKFLOW_WAKE_DISPATCHER_SCHEDULE_ID,
4045
4678
  SESSION_WORKFLOW_WAKE_DISPATCHER_WORKFLOW_TYPE,
4679
+ SessionAuthorizationDeniedError,
4680
+ SessionAuthorizationUnavailableError,
4046
4681
  acceptSessionUserMessage,
4047
4682
  activateRigVersionForApi,
4048
4683
  appendRigSetupCommand,
@@ -4114,6 +4749,8 @@ export {
4114
4749
  requireRigChangeForApi,
4115
4750
  requireRigForApi,
4116
4751
  requireScheduledTaskForApi,
4752
+ requireSessionAuthorization,
4753
+ requireSessionAuthorizationListScope,
4117
4754
  requireVariableSetEncryption,
4118
4755
  requireVariableSetForApi,
4119
4756
  resolveCapabilityPack,
@@ -4141,6 +4778,7 @@ export {
4141
4778
  validateFileResources,
4142
4779
  validateGitHubRepositorySelection,
4143
4780
  validateGitHubRepositorySelectionShape,
4781
+ validateGitHubRepositorySelectionShapes,
4144
4782
  validateMcpCapabilityConnection,
4145
4783
  validateToolRefs,
4146
4784
  validateVariableSetAttachment,