@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.d.ts +103 -138
- package/dist/index.js +792 -154
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/access/index.ts +4 -0
- package/src/application/session-commands.ts +190 -38
- package/src/billing/limits.ts +20 -1
- package/src/dependencies.ts +26 -2
- package/src/domain/capabilities.ts +171 -3
- package/src/domain/resources.ts +89 -30
- package/src/domain/sessions.ts +409 -93
- package/src/index.ts +1 -0
- package/src/sandbox/fleet.ts +2 -2
- package/src/sandbox/routing.ts +1 -1
- package/src/session-authorization.ts +208 -0
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
|
-
|
|
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
|
-
|
|
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 ${
|
|
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 {
|
|
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
|
|
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:
|
|
2749
|
+
uri: normalizedUri,
|
|
2446
2750
|
ref: resource.ref.trim(),
|
|
2447
2751
|
mountPath,
|
|
2448
|
-
...resource.subpath ? { 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
|
|
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(
|
|
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
|
|
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
|
|
2829
|
+
const installationId = positiveInteger(installationRaw);
|
|
2507
2830
|
const repositoryId = positiveInteger(repositoryRaw);
|
|
2508
|
-
if (!
|
|
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
|
|
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
|
|
2528
|
-
if (
|
|
2840
|
+
const installationIds = validateGitHubRepositorySelectionShapes(resources);
|
|
2841
|
+
if (installationIds.length === 0) {
|
|
2529
2842
|
return;
|
|
2530
2843
|
}
|
|
2531
|
-
const
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
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
|
-
|
|
2563
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3054
|
-
|
|
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(
|
|
3062
|
-
|
|
3063
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3131
|
-
|
|
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
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
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,
|
|
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
|
|
3890
|
+
return {
|
|
3891
|
+
...result,
|
|
3892
|
+
relatedSessionAccess: authorization?.relatedSessionAccess ?? "root"
|
|
3893
|
+
};
|
|
3363
3894
|
}
|
|
3364
|
-
async function readSessionLineage(
|
|
3365
|
-
const
|
|
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
|
|
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
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
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
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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(
|
|
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
|
|
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(
|
|
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,
|