@opengeni/core 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +64 -5
- package/dist/index.js +343 -35
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- 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 +4 -8
- package/src/domain/session-tool-policy.ts +211 -0
- package/src/domain/sessions.ts +255 -35
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
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.6.
|
|
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.6.9",
|
|
39
|
+
"@opengeni/contracts": "^0.18.0",
|
|
40
|
+
"@opengeni/db": "^0.10.7",
|
|
41
|
+
"@opengeni/documents": "^0.2.28",
|
|
42
|
+
"@opengeni/events": "^0.3.19",
|
|
43
43
|
"@opengeni/observability": "^0.3.0",
|
|
44
|
-
"@opengeni/runtime": "^0.
|
|
45
|
-
"@opengeni/storage": "^0.2.
|
|
44
|
+
"@opengeni/runtime": "^0.13.0",
|
|
45
|
+
"@opengeni/storage": "^0.2.22",
|
|
46
46
|
"hono": "^4.12.18"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
@@ -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>();
|
|
@@ -21,7 +21,7 @@ import type { SessionWorkflowClient } from "../dependencies";
|
|
|
21
21
|
import type { ObjectStorageDependency } from "../dependencies";
|
|
22
22
|
import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
|
|
23
23
|
import { validateVariableSetAttachment } from "./environments";
|
|
24
|
-
import {
|
|
24
|
+
import { assertWorkspaceModelPolicyAllows, canonicalConfiguredModel } from "./sessions";
|
|
25
25
|
import {
|
|
26
26
|
normalizeResources,
|
|
27
27
|
validateFileResources,
|
|
@@ -327,15 +327,10 @@ async function validateScheduledTaskAgentConfig(input: {
|
|
|
327
327
|
// session choke points (a `scheduled_tasks:manage` holder could otherwise set
|
|
328
328
|
// a model the host does not expose). An omitted model inherits the host
|
|
329
329
|
// default downstream, which is always configured.
|
|
330
|
-
|
|
330
|
+
const model = canonicalConfiguredModel(input.settings, input.payload.agentConfig.model);
|
|
331
331
|
// Same policy vetting as the session choke points; an omitted model flows
|
|
332
332
|
// 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
|
-
);
|
|
333
|
+
await assertWorkspaceModelPolicyAllows(input.db, input.settings, input.workspaceId, model);
|
|
339
334
|
const resources = normalizeResources(input.payload.agentConfig.resources ?? []);
|
|
340
335
|
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
341
336
|
input.db,
|
|
@@ -363,6 +358,7 @@ async function validateScheduledTaskAgentConfig(input: {
|
|
|
363
358
|
await validateFileResources(input.db, input.workspaceId, resources);
|
|
364
359
|
return {
|
|
365
360
|
...input.payload.agentConfig,
|
|
361
|
+
...(model === undefined || model === null ? {} : { model }),
|
|
366
362
|
prompt,
|
|
367
363
|
resources,
|
|
368
364
|
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
|
+
}
|