@opengeni/core 0.8.0 → 0.11.1
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 +148 -6
- package/dist/index.js +827 -127
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/application/new-session-drafts.ts +126 -0
- package/src/application/session-commands.ts +2 -0
- package/src/dependencies.ts +2 -0
- package/src/domain/resources.ts +32 -1
- package/src/domain/scheduled-tasks.ts +27 -9
- package/src/domain/session-tool-policy.ts +211 -0
- package/src/domain/sessions.ts +369 -84
- package/src/index.ts +2 -0
- package/src/sandbox/fleet.ts +149 -39
- package/src/sandbox/routing.ts +261 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "OpenGeni framework-agnostic core: the domain, access, and billing layers (neutral access, off-HTTP V2 surface). Behavior-preserving extraction from apps/api — keeps Hono's HTTPException for error throwing (typed-errors cleanup deferred).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -34,15 +34,15 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37
|
-
"@opengeni/codex": "^0.2.
|
|
38
|
-
"@opengeni/config": "^0.
|
|
39
|
-
"@opengeni/contracts": "^0.
|
|
40
|
-
"@opengeni/db": "^0.
|
|
41
|
-
"@opengeni/documents": "^0.2.
|
|
42
|
-
"@opengeni/events": "^0.3.
|
|
37
|
+
"@opengeni/codex": "^0.2.7",
|
|
38
|
+
"@opengeni/config": "^0.7.0",
|
|
39
|
+
"@opengeni/contracts": "^0.19.0",
|
|
40
|
+
"@opengeni/db": "^0.12.0",
|
|
41
|
+
"@opengeni/documents": "^0.2.30",
|
|
42
|
+
"@opengeni/events": "^0.3.21",
|
|
43
43
|
"@opengeni/observability": "^0.3.0",
|
|
44
|
-
"@opengeni/runtime": "^0.
|
|
45
|
-
"@opengeni/storage": "^0.2.
|
|
44
|
+
"@opengeni/runtime": "^0.13.3",
|
|
45
|
+
"@opengeni/storage": "^0.2.24",
|
|
46
46
|
"hono": "^4.12.18"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NewSessionDraft,
|
|
3
|
+
SaveNewSessionDraftRequest,
|
|
4
|
+
type AccessGrant,
|
|
5
|
+
type NewSessionDraft as NewSessionDraftValue,
|
|
6
|
+
} from "@opengeni/contracts";
|
|
7
|
+
import {
|
|
8
|
+
getNewSessionDraftInTransaction,
|
|
9
|
+
NewSessionDraftAccessError,
|
|
10
|
+
saveNewSessionDraftInTransaction,
|
|
11
|
+
withWorkspaceSubjectRls,
|
|
12
|
+
} from "@opengeni/db";
|
|
13
|
+
import { HTTPException } from "hono/http-exception";
|
|
14
|
+
import type { AppDependencies } from "../dependencies";
|
|
15
|
+
import { settingsWithEnabledCapabilityMcpServers } from "../domain/capabilities";
|
|
16
|
+
import {
|
|
17
|
+
normalizeResources,
|
|
18
|
+
validateFileResources,
|
|
19
|
+
validateGitHubRepositorySelection,
|
|
20
|
+
validateToolRefs,
|
|
21
|
+
} from "../domain/resources";
|
|
22
|
+
import { assertConfiguredModel, assertWorkspaceModelPolicyAllows } from "../domain/sessions";
|
|
23
|
+
|
|
24
|
+
type NewSessionDraftDependencies = Pick<AppDependencies, "settings" | "db" | "objectStorage">;
|
|
25
|
+
|
|
26
|
+
function mapNewSessionDraft(
|
|
27
|
+
row: Awaited<ReturnType<typeof getNewSessionDraftInTransaction>>,
|
|
28
|
+
): NewSessionDraftValue | null {
|
|
29
|
+
if (!row) return null;
|
|
30
|
+
return NewSessionDraft.parse({
|
|
31
|
+
revision: row.revision,
|
|
32
|
+
text: row.text,
|
|
33
|
+
resources: row.resources,
|
|
34
|
+
tools: row.tools,
|
|
35
|
+
model: row.model,
|
|
36
|
+
reasoningEffort: row.reasoningEffort,
|
|
37
|
+
options: row.sessionOptions,
|
|
38
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Read the authenticated actor's server-authoritative pre-session composer state. */
|
|
43
|
+
export async function getActorNewSessionDraft(
|
|
44
|
+
deps: Pick<NewSessionDraftDependencies, "settings" | "db">,
|
|
45
|
+
grant: AccessGrant,
|
|
46
|
+
workspaceId: string,
|
|
47
|
+
): Promise<NewSessionDraftValue> {
|
|
48
|
+
const row = await withWorkspaceSubjectRls(deps.db, workspaceId, grant.subjectId, (scoped) =>
|
|
49
|
+
getNewSessionDraftInTransaction(scoped, {
|
|
50
|
+
workspaceId,
|
|
51
|
+
subjectId: grant.subjectId,
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
return (
|
|
55
|
+
mapNewSessionDraft(row) ?? {
|
|
56
|
+
revision: 0,
|
|
57
|
+
text: "",
|
|
58
|
+
resources: [],
|
|
59
|
+
tools: [],
|
|
60
|
+
model: deps.settings.openaiModel,
|
|
61
|
+
reasoningEffort: deps.settings.openaiReasoningEffort,
|
|
62
|
+
options: {},
|
|
63
|
+
updatedAt: null,
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validate and save one exact actor-private draft revision. Create-time-only
|
|
70
|
+
* checks (live machine target, rig/variable-set state, and permission
|
|
71
|
+
* delegation) intentionally remain in createSessionForRequest: a recoverable
|
|
72
|
+
* draft may represent incomplete options, while no invalid option can become a
|
|
73
|
+
* session without passing that single canonical create boundary.
|
|
74
|
+
*/
|
|
75
|
+
export async function saveActorNewSessionDraft(
|
|
76
|
+
deps: NewSessionDraftDependencies,
|
|
77
|
+
grant: AccessGrant,
|
|
78
|
+
workspaceId: string,
|
|
79
|
+
rawInput: unknown,
|
|
80
|
+
): Promise<NewSessionDraftValue> {
|
|
81
|
+
const input = SaveNewSessionDraftRequest.parse(rawInput);
|
|
82
|
+
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
83
|
+
deps.db,
|
|
84
|
+
workspaceId,
|
|
85
|
+
deps.settings,
|
|
86
|
+
);
|
|
87
|
+
const resources = normalizeResources(input.resources);
|
|
88
|
+
const tools = validateToolRefs(input.tools, runtimeSettings);
|
|
89
|
+
await validateGitHubRepositorySelection(deps.db, workspaceId, resources);
|
|
90
|
+
if (resources.some((resource) => resource.kind === "file") && !deps.objectStorage) {
|
|
91
|
+
throw new HTTPException(503, { message: "object storage is not configured" });
|
|
92
|
+
}
|
|
93
|
+
await validateFileResources(deps.db, workspaceId, resources);
|
|
94
|
+
assertConfiguredModel(deps.settings, input.model);
|
|
95
|
+
await assertWorkspaceModelPolicyAllows(deps.db, deps.settings, workspaceId, input.model);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const saved = await withWorkspaceSubjectRls(deps.db, workspaceId, grant.subjectId, (scoped) =>
|
|
99
|
+
scoped.transaction((tx) =>
|
|
100
|
+
saveNewSessionDraftInTransaction(tx as unknown as typeof scoped, {
|
|
101
|
+
accountId: grant.accountId,
|
|
102
|
+
workspaceId,
|
|
103
|
+
subjectId: grant.subjectId,
|
|
104
|
+
expectedRevision: input.expectedRevision,
|
|
105
|
+
text: input.text,
|
|
106
|
+
resources,
|
|
107
|
+
tools,
|
|
108
|
+
model: input.model,
|
|
109
|
+
reasoningEffort: input.reasoningEffort,
|
|
110
|
+
options: input.options,
|
|
111
|
+
// Only managed people are removed through removeWorkspaceMember().
|
|
112
|
+
// API keys and delegated service actors (for example the first-party
|
|
113
|
+
// worker MCP principal) legitimately have no workspace_memberships
|
|
114
|
+
// row, so they must not be rejected by the human-removal fence.
|
|
115
|
+
requireWorkspaceMembership: grant.subjectId.startsWith("user:"),
|
|
116
|
+
}),
|
|
117
|
+
),
|
|
118
|
+
);
|
|
119
|
+
return mapNewSessionDraft(saved)!;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error instanceof NewSessionDraftAccessError) {
|
|
122
|
+
throw new HTTPException(403, { message: error.message });
|
|
123
|
+
}
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -385,6 +385,7 @@ function composerDraft(
|
|
|
385
385
|
text: row.text,
|
|
386
386
|
resources: row.resources as ComposerDraft["resources"],
|
|
387
387
|
tools: row.tools as ComposerDraft["tools"],
|
|
388
|
+
toolsProvided: row.toolsProvided,
|
|
388
389
|
model: row.model,
|
|
389
390
|
reasoningEffort: row.reasoningEffort as ComposerDraft["reasoningEffort"],
|
|
390
391
|
sourceTurnId: row.sourceTurnId,
|
|
@@ -648,6 +649,7 @@ export async function getHumanComposerDraft(
|
|
|
648
649
|
text: "",
|
|
649
650
|
resources: [],
|
|
650
651
|
tools: [],
|
|
652
|
+
toolsProvided: false,
|
|
651
653
|
model: session.model,
|
|
652
654
|
reasoningEffort: reasoningEffortForMetadata(session.metadata, "medium"),
|
|
653
655
|
sourceTurnId: null,
|
package/src/dependencies.ts
CHANGED
|
@@ -104,6 +104,8 @@ export type AppDependencies = {
|
|
|
104
104
|
*/
|
|
105
105
|
sessionAuthorization?: SessionAuthorizationPort | null;
|
|
106
106
|
managedAuth?: ManagedAuth | null;
|
|
107
|
+
/** Injectable Codex HTTP transport for deterministic API/provider tests. */
|
|
108
|
+
codexFetch?: typeof fetch;
|
|
107
109
|
// The API process's OWN agent-loop-free sandbox client (constructed from
|
|
108
110
|
// settings via @opengeni/runtime/sandbox). Undefined when sandboxBackend=none.
|
|
109
111
|
// This is the foundation of the API-direct control plane: the API resumes
|
package/src/domain/resources.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import { areGitHubRepositoriesAllowedForWorkspace, requireFile, type Database } from "@opengeni/db";
|
|
19
19
|
import { HTTPException } from "hono/http-exception";
|
|
20
20
|
|
|
21
|
-
export function validateToolRefs(tools: ToolRef[], settings:
|
|
21
|
+
export function validateToolRefs(tools: ToolRef[], settings: McpSettings): ToolRef[] {
|
|
22
22
|
const mcpServerIds = new Set(settings.mcpServers.map((server) => server.id));
|
|
23
23
|
const out: ToolRef[] = [];
|
|
24
24
|
for (const tool of tools) {
|
|
@@ -75,6 +75,37 @@ export function withDefaultEnabledCapabilityMcpTools(
|
|
|
75
75
|
return mergeToolRefs(tools, enabledCapabilityMcpToolRefs(settings, runtimeSettings));
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/** Drop stored refs that are no longer present in the current runtime registry. */
|
|
79
|
+
export function availableToolRefs(tools: ToolRef[], settings: McpSettings): ToolRef[] {
|
|
80
|
+
const available = new Set(settings.mcpServers.map((server) => server.id));
|
|
81
|
+
return tools.filter((tool) => available.has(tool.id));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A child or fixed-policy follow-up may narrow its allow-list, never widen it. */
|
|
85
|
+
export function assertToolRefsSubset(
|
|
86
|
+
requested: ToolRef[],
|
|
87
|
+
allowed: ToolRef[],
|
|
88
|
+
message = "requested tools exceed the session tool policy",
|
|
89
|
+
): void {
|
|
90
|
+
const allowedIds = new Set(allowed.map((tool) => `${tool.kind}:${tool.id}`));
|
|
91
|
+
const widened = requested.find((tool) => !allowedIds.has(`${tool.kind}:${tool.id}`));
|
|
92
|
+
if (widened) {
|
|
93
|
+
throw new HTTPException(403, { message: `${message}: ${widened.id}` });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Validate runtime availability and then enforce the durable policy fence. */
|
|
98
|
+
export function validateToolRefsForSessionPolicy(input: {
|
|
99
|
+
requested: ToolRef[];
|
|
100
|
+
settings: McpSettings;
|
|
101
|
+
allowedTools: ToolRef[];
|
|
102
|
+
message: string;
|
|
103
|
+
}): ToolRef[] {
|
|
104
|
+
const validated = validateToolRefs(input.requested, input.settings);
|
|
105
|
+
assertToolRefsSubset(validated, input.allowedTools, input.message);
|
|
106
|
+
return validated;
|
|
107
|
+
}
|
|
108
|
+
|
|
78
109
|
export function normalizeResources(resources: ResourceRef[]): ResourceRef[] {
|
|
79
110
|
const mountPaths = new Map<string, string>();
|
|
80
111
|
const identities = new Map<string, string>();
|
|
@@ -9,19 +9,21 @@ import type {
|
|
|
9
9
|
import {
|
|
10
10
|
createScheduledTask,
|
|
11
11
|
deleteScheduledTask,
|
|
12
|
+
getNestedAgentDepthDeploymentPolicy,
|
|
12
13
|
getRig,
|
|
13
14
|
getScheduledTask,
|
|
15
|
+
requireWorkspace,
|
|
14
16
|
updateScheduledTask,
|
|
15
17
|
type Database,
|
|
16
18
|
type UpdateScheduledTaskInput,
|
|
17
19
|
} from "@opengeni/db";
|
|
18
20
|
import { HTTPException } from "hono/http-exception";
|
|
19
|
-
import { requirePermission } from "../access";
|
|
21
|
+
import { hasPermission, requirePermission } from "../access";
|
|
20
22
|
import type { SessionWorkflowClient } from "../dependencies";
|
|
21
23
|
import type { ObjectStorageDependency } from "../dependencies";
|
|
22
24
|
import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
|
|
23
25
|
import { validateVariableSetAttachment } from "./environments";
|
|
24
|
-
import {
|
|
26
|
+
import { assertWorkspaceModelPolicyAllows, canonicalConfiguredModel } from "./sessions";
|
|
25
27
|
import {
|
|
26
28
|
normalizeResources,
|
|
27
29
|
validateFileResources,
|
|
@@ -199,6 +201,7 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
199
201
|
settings: input.settings,
|
|
200
202
|
db: input.db,
|
|
201
203
|
objectStorage: input.objectStorage,
|
|
204
|
+
grant: input.grant,
|
|
202
205
|
workspaceId: input.existing.workspaceId,
|
|
203
206
|
payload: { agentConfig: input.payload.agentConfig },
|
|
204
207
|
...(input.toolsProvided !== undefined ? { toolsProvided: input.toolsProvided } : {}),
|
|
@@ -318,6 +321,7 @@ async function validateScheduledTaskAgentConfig(input: {
|
|
|
318
321
|
settings: Settings;
|
|
319
322
|
db: Database;
|
|
320
323
|
objectStorage: ObjectStorageDependency;
|
|
324
|
+
grant: AccessGrant;
|
|
321
325
|
payload: { agentConfig: ScheduledTaskAgentConfig };
|
|
322
326
|
workspaceId: string;
|
|
323
327
|
toolsProvided?: boolean;
|
|
@@ -327,15 +331,10 @@ async function validateScheduledTaskAgentConfig(input: {
|
|
|
327
331
|
// session choke points (a `scheduled_tasks:manage` holder could otherwise set
|
|
328
332
|
// a model the host does not expose). An omitted model inherits the host
|
|
329
333
|
// default downstream, which is always configured.
|
|
330
|
-
|
|
334
|
+
const model = canonicalConfiguredModel(input.settings, input.payload.agentConfig.model);
|
|
331
335
|
// Same policy vetting as the session choke points; an omitted model flows
|
|
332
336
|
// through session creation later, where the effective default is vetted.
|
|
333
|
-
await assertWorkspaceModelPolicyAllows(
|
|
334
|
-
input.db,
|
|
335
|
-
input.settings,
|
|
336
|
-
input.workspaceId,
|
|
337
|
-
input.payload.agentConfig.model,
|
|
338
|
-
);
|
|
337
|
+
await assertWorkspaceModelPolicyAllows(input.db, input.settings, input.workspaceId, model);
|
|
339
338
|
const resources = normalizeResources(input.payload.agentConfig.resources ?? []);
|
|
340
339
|
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
341
340
|
input.db,
|
|
@@ -361,8 +360,27 @@ async function validateScheduledTaskAgentConfig(input: {
|
|
|
361
360
|
throw new HTTPException(503, { message: "object storage is not configured" });
|
|
362
361
|
}
|
|
363
362
|
await validateFileResources(input.db, input.workspaceId, resources);
|
|
363
|
+
const requestedMaxDepth = input.payload.agentConfig.maxNestedAgentDepth;
|
|
364
|
+
if (requestedMaxDepth !== undefined) {
|
|
365
|
+
const workspace = await requireWorkspace(input.db, input.workspaceId);
|
|
366
|
+
const workspaceMaxDepth = workspace.settings.maxNestedAgentDepth;
|
|
367
|
+
const deploymentPolicy = await getNestedAgentDepthDeploymentPolicy(input.db);
|
|
368
|
+
const inheritedMaxDepth =
|
|
369
|
+
typeof workspaceMaxDepth === "number"
|
|
370
|
+
? workspaceMaxDepth
|
|
371
|
+
: deploymentPolicy.maxNestedAgentDepth;
|
|
372
|
+
if (
|
|
373
|
+
requestedMaxDepth > inheritedMaxDepth &&
|
|
374
|
+
!hasPermission(input.grant.permissions, "workspace:admin")
|
|
375
|
+
) {
|
|
376
|
+
throw new HTTPException(403, {
|
|
377
|
+
message: `scheduled task maxNestedAgentDepth ${requestedMaxDepth} exceeds inherited limit ${inheritedMaxDepth}; workspace:admin is required to increase it`,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
364
381
|
return {
|
|
365
382
|
...input.payload.agentConfig,
|
|
383
|
+
...(model === undefined || model === null ? {} : { model }),
|
|
366
384
|
prompt,
|
|
367
385
|
resources,
|
|
368
386
|
tools,
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { Settings } from "@opengeni/config";
|
|
2
|
+
import {
|
|
3
|
+
SESSION_EFFECTIVE_TOOL_POLICY_ID_LIMIT,
|
|
4
|
+
SESSION_EFFECTIVE_TOOL_POLICY_ID_MAX_LENGTH,
|
|
5
|
+
mergeToolRefs,
|
|
6
|
+
type Session,
|
|
7
|
+
type SessionEffectiveToolPolicy,
|
|
8
|
+
type SessionToolPolicy,
|
|
9
|
+
type ToolRef,
|
|
10
|
+
} from "@opengeni/contracts";
|
|
11
|
+
import type { Database } from "@opengeni/db";
|
|
12
|
+
import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
|
|
13
|
+
import { enabledCapabilityMcpToolRefs } from "./resources";
|
|
14
|
+
|
|
15
|
+
const MANDATORY_SESSION_MCP_SERVER_IDS = ["opengeni"] as const;
|
|
16
|
+
const PROJECTABLE_REGISTRY_ID = /^[A-Za-z0-9_-]+$/;
|
|
17
|
+
|
|
18
|
+
export type ResolvedSessionToolPolicy = {
|
|
19
|
+
toolRefs: ToolRef[];
|
|
20
|
+
effectivePolicy: SessionEffectiveToolPolicy;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type SessionToolPolicyInput = {
|
|
24
|
+
toolPolicy?: SessionToolPolicy | null;
|
|
25
|
+
sessionTools: ToolRef[];
|
|
26
|
+
turnTools?: ToolRef[];
|
|
27
|
+
/** Undefined preserves the legacy merge path for pre-provenance callers. */
|
|
28
|
+
turnToolsProvided?: boolean;
|
|
29
|
+
availableMcpServerIds: Iterable<string>;
|
|
30
|
+
/** Current omitted-tools defaults, intentionally narrower than all servers. */
|
|
31
|
+
defaultMcpServerIds?: Iterable<string>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function sortedIds(ids: Iterable<string>): string[] {
|
|
35
|
+
return [...new Set(ids)].sort();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function projectIds(ids: readonly string[]): { ids: string[]; truncated: boolean } {
|
|
39
|
+
const projectable = ids.filter(
|
|
40
|
+
(id) =>
|
|
41
|
+
id.length <= SESSION_EFFECTIVE_TOOL_POLICY_ID_MAX_LENGTH && PROJECTABLE_REGISTRY_ID.test(id),
|
|
42
|
+
);
|
|
43
|
+
return {
|
|
44
|
+
ids: projectable.slice(0, SESSION_EFFECTIVE_TOOL_POLICY_ID_LIMIT),
|
|
45
|
+
truncated:
|
|
46
|
+
projectable.length !== ids.length ||
|
|
47
|
+
projectable.length > SESSION_EFFECTIVE_TOOL_POLICY_ID_LIMIT,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the same ID-only policy used by API projections and worker turns.
|
|
53
|
+
* This function never receives endpoint URLs, credentials, schemas, or live
|
|
54
|
+
* probe results. `availableMcpServerIds` is the resolved runtime registry;
|
|
55
|
+
* `defaultMcpServerIds` is the capability-only omitted-tools set.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveSessionToolPolicy(input: SessionToolPolicyInput): ResolvedSessionToolPolicy {
|
|
58
|
+
const policy = input.toolPolicy ?? { mode: "legacy" as const, inheritedFromSessionId: null };
|
|
59
|
+
const availableIds = new Set(input.availableMcpServerIds);
|
|
60
|
+
// Never infer omitted-tools defaults from the full runtime registry: static
|
|
61
|
+
// MCPs are explicit-only unless they are capability-derived defaults.
|
|
62
|
+
const defaultIds = new Set(input.defaultMcpServerIds ?? []);
|
|
63
|
+
const mandatoryIds: string[] = MANDATORY_SESSION_MCP_SERVER_IDS.filter((id) =>
|
|
64
|
+
availableIds.has(id),
|
|
65
|
+
);
|
|
66
|
+
const mandatoryIdSet = new Set<string>(mandatoryIds);
|
|
67
|
+
const selectedRefs =
|
|
68
|
+
input.turnToolsProvided === true
|
|
69
|
+
? mergeToolRefs([], input.turnTools ?? [])
|
|
70
|
+
: input.turnToolsProvided === false
|
|
71
|
+
? mergeToolRefs([], input.sessionTools)
|
|
72
|
+
: mergeToolRefs(input.sessionTools, input.turnTools ?? []);
|
|
73
|
+
const tracksWorkspaceDefaults =
|
|
74
|
+
policy.mode === "workspace_default" && input.turnToolsProvided !== true;
|
|
75
|
+
|
|
76
|
+
// Optional capability refs are a historical materialization of a
|
|
77
|
+
// workspace-default selection. They may outlive an installation or its
|
|
78
|
+
// credentials; do not hand an unavailable optional ref to runtime, where it
|
|
79
|
+
// would otherwise be an unknown MCP id. Strict historical refs intentionally
|
|
80
|
+
// remain so their fail-loud compatibility contract is preserved.
|
|
81
|
+
let toolRefs = selectedRefs.filter((tool) => tool.optional !== true || availableIds.has(tool.id));
|
|
82
|
+
if (tracksWorkspaceDefaults) {
|
|
83
|
+
toolRefs = mergeToolRefs(
|
|
84
|
+
toolRefs,
|
|
85
|
+
sortedIds(defaultIds)
|
|
86
|
+
.filter((id) => availableIds.has(id))
|
|
87
|
+
.map((id) => ({ kind: "mcp" as const, id, optional: true as const })),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
toolRefs = mergeToolRefs(
|
|
91
|
+
toolRefs,
|
|
92
|
+
mandatoryIds.map((id) => ({ kind: "mcp" as const, id })),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// `effectiveIds` is the requested policy truth, including unavailable
|
|
96
|
+
// optional refs retained in the persisted selection. `toolRefs` above is
|
|
97
|
+
// the runtime-safe materialization, so projections can distinguish dropped
|
|
98
|
+
// history from what is actually handed to the MCP router.
|
|
99
|
+
const requestedEffectiveRefs = mergeToolRefs(
|
|
100
|
+
selectedRefs,
|
|
101
|
+
tracksWorkspaceDefaults
|
|
102
|
+
? sortedIds(defaultIds)
|
|
103
|
+
.filter((id) => availableIds.has(id))
|
|
104
|
+
.map((id) => ({ kind: "mcp" as const, id, optional: true as const }))
|
|
105
|
+
: [],
|
|
106
|
+
);
|
|
107
|
+
const effectiveIds = sortedIds(
|
|
108
|
+
mergeToolRefs(
|
|
109
|
+
requestedEffectiveRefs,
|
|
110
|
+
mandatoryIds.map((id) => ({ kind: "mcp" as const, id })),
|
|
111
|
+
).map((tool) => tool.id),
|
|
112
|
+
);
|
|
113
|
+
const configuredIds = effectiveIds.filter((id) => availableIds.has(id));
|
|
114
|
+
const configuredIdSet = new Set(configuredIds);
|
|
115
|
+
const droppedIds = effectiveIds.filter((id) => !configuredIdSet.has(id));
|
|
116
|
+
const deferredIds = tracksWorkspaceDefaults
|
|
117
|
+
? sortedIds(
|
|
118
|
+
toolRefs
|
|
119
|
+
.filter(
|
|
120
|
+
(tool) =>
|
|
121
|
+
tool.optional === true &&
|
|
122
|
+
configuredIdSet.has(tool.id) &&
|
|
123
|
+
!mandatoryIdSet.has(tool.id),
|
|
124
|
+
)
|
|
125
|
+
.map((tool) => tool.id),
|
|
126
|
+
)
|
|
127
|
+
: [];
|
|
128
|
+
const selectedIds = sortedIds(
|
|
129
|
+
selectedRefs
|
|
130
|
+
.filter(
|
|
131
|
+
(tool) =>
|
|
132
|
+
!mandatoryIdSet.has(tool.id) && !(tracksWorkspaceDefaults && tool.optional === true),
|
|
133
|
+
)
|
|
134
|
+
.map((tool) => tool.id),
|
|
135
|
+
);
|
|
136
|
+
const projections = {
|
|
137
|
+
selected: projectIds(selectedIds),
|
|
138
|
+
effective: projectIds(effectiveIds),
|
|
139
|
+
mandatory: projectIds(sortedIds(mandatoryIds)),
|
|
140
|
+
deferred: projectIds(deferredIds),
|
|
141
|
+
configured: projectIds(configuredIds),
|
|
142
|
+
dropped: projectIds(droppedIds),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
toolRefs,
|
|
147
|
+
effectivePolicy: {
|
|
148
|
+
mode: policy.mode,
|
|
149
|
+
inheritedFromSessionId: policy.inheritedFromSessionId,
|
|
150
|
+
selectedIds: projections.selected.ids,
|
|
151
|
+
effectiveIds: projections.effective.ids,
|
|
152
|
+
mandatoryIds: projections.mandatory.ids,
|
|
153
|
+
lazyRouter: {
|
|
154
|
+
state: tracksWorkspaceDefaults ? "required" : "disabled",
|
|
155
|
+
deferredIds: projections.deferred.ids,
|
|
156
|
+
},
|
|
157
|
+
configuredIds: projections.configured.ids,
|
|
158
|
+
droppedIds: projections.dropped.ids,
|
|
159
|
+
counts: {
|
|
160
|
+
selected: selectedIds.length,
|
|
161
|
+
effective: effectiveIds.length,
|
|
162
|
+
mandatory: mandatoryIds.length,
|
|
163
|
+
deferred: deferredIds.length,
|
|
164
|
+
configured: configuredIds.length,
|
|
165
|
+
dropped: droppedIds.length,
|
|
166
|
+
},
|
|
167
|
+
idsTruncated: Object.values(projections).some((projection) => projection.truncated),
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Current full runtime registry IDs, including configured static servers. */
|
|
173
|
+
export async function workspaceSessionToolPolicyServerIds(
|
|
174
|
+
db: Database,
|
|
175
|
+
workspaceId: string,
|
|
176
|
+
settings: Settings,
|
|
177
|
+
): Promise<string[]> {
|
|
178
|
+
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
|
|
179
|
+
return sortedIds(runtimeSettings.mcpServers.map((server) => server.id));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Current omitted-tools defaults; this preserves capability-first behavior. */
|
|
183
|
+
export async function workspaceSessionToolPolicyDefaultServerIds(
|
|
184
|
+
db: Database,
|
|
185
|
+
workspaceId: string,
|
|
186
|
+
settings: Settings,
|
|
187
|
+
): Promise<string[]> {
|
|
188
|
+
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
|
|
189
|
+
return sortedIds(enabledCapabilityMcpToolRefs(settings, runtimeSettings).map((tool) => tool.id));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Add a bounded, secret-safe effective projection to a session response. */
|
|
193
|
+
export function sessionWithEffectiveToolPolicy(
|
|
194
|
+
session: Session,
|
|
195
|
+
workspaceServerIds: Iterable<string>,
|
|
196
|
+
workspaceDefaultServerIds: Iterable<string> = [],
|
|
197
|
+
): Session {
|
|
198
|
+
const availableIds = new Set(workspaceServerIds);
|
|
199
|
+
for (const server of session.mcpServers) {
|
|
200
|
+
availableIds.add(server.id);
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
...session,
|
|
204
|
+
effectiveToolPolicy: resolveSessionToolPolicy({
|
|
205
|
+
...(session.toolPolicy ? { toolPolicy: session.toolPolicy } : {}),
|
|
206
|
+
sessionTools: session.tools,
|
|
207
|
+
availableMcpServerIds: availableIds,
|
|
208
|
+
defaultMcpServerIds: workspaceDefaultServerIds,
|
|
209
|
+
}).effectivePolicy,
|
|
210
|
+
};
|
|
211
|
+
}
|