@opengeni/core 0.12.5 → 0.12.10
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 +43 -17
- package/dist/index.js +113 -58
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/access/index.ts +3 -0
- package/src/application/session-commands.ts +19 -8
- package/src/dependencies.ts +4 -2
- package/src/domain/capabilities.ts +36 -1
- package/src/domain/session-tool-policy.ts +5 -14
- package/src/domain/sessions.ts +102 -66
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/core",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.10",
|
|
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": {
|
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37
37
|
"@opengeni/codex": "^0.2.7",
|
|
38
|
-
"@opengeni/config": "^0.7.
|
|
39
|
-
"@opengeni/contracts": "^0.
|
|
40
|
-
"@opengeni/db": "^0.
|
|
41
|
-
"@opengeni/documents": "^0.2.
|
|
42
|
-
"@opengeni/events": "^0.3.
|
|
38
|
+
"@opengeni/config": "^0.7.13",
|
|
39
|
+
"@opengeni/contracts": "^0.23.0",
|
|
40
|
+
"@opengeni/db": "^0.14.3",
|
|
41
|
+
"@opengeni/documents": "^0.2.45",
|
|
42
|
+
"@opengeni/events": "^0.3.36",
|
|
43
43
|
"@opengeni/observability": "^0.3.0",
|
|
44
|
-
"@opengeni/runtime": "^0.14.
|
|
45
|
-
"@opengeni/storage": "^0.2.
|
|
44
|
+
"@opengeni/runtime": "^0.14.3",
|
|
45
|
+
"@opengeni/storage": "^0.2.37",
|
|
46
46
|
"hono": "^4.12.18"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
package/src/access/index.ts
CHANGED
|
@@ -236,6 +236,9 @@ async function delegatedAccessContext(
|
|
|
236
236
|
metadata: {
|
|
237
237
|
delegated: true,
|
|
238
238
|
...(payload.sessionId ? { sessionId: payload.sessionId } : {}),
|
|
239
|
+
...(payload.firstPartyMcpTools !== undefined
|
|
240
|
+
? { firstPartyMcpTools: payload.firstPartyMcpTools }
|
|
241
|
+
: {}),
|
|
239
242
|
// Caller identity: the turn that minted this token. Tools classify the
|
|
240
243
|
// CALLER from this instead of re-reading the live active pointer.
|
|
241
244
|
...(payload.turnId ? { turnId: payload.turnId } : {}),
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
AccessGrant,
|
|
8
8
|
SessionAuthorizationOperation,
|
|
9
9
|
SessionAuthorizationPort,
|
|
10
|
+
SessionAuthorizationSurface,
|
|
10
11
|
SessionCommandReceipt,
|
|
11
12
|
SessionControlRequest,
|
|
12
13
|
SessionControlResponse,
|
|
@@ -55,6 +56,8 @@ export type HumanSessionCommandContext = {
|
|
|
55
56
|
workspaceId: string;
|
|
56
57
|
sessionId: string;
|
|
57
58
|
subjectId: string;
|
|
59
|
+
/** See AgentSessionCommandContext.authorizationSurface. */
|
|
60
|
+
authorizationSurface?: SessionAuthorizationSurface;
|
|
58
61
|
};
|
|
59
62
|
|
|
60
63
|
export type AgentSessionCommandContext = {
|
|
@@ -65,6 +68,13 @@ export type AgentSessionCommandContext = {
|
|
|
65
68
|
callerTurnId: string;
|
|
66
69
|
callerAttemptId: string;
|
|
67
70
|
callerExecutionGeneration: number;
|
|
71
|
+
/**
|
|
72
|
+
* The trusted adapter surface that owns this command's one authorization
|
|
73
|
+
* decision. Direct core callers omit it and retain the canonical `core`
|
|
74
|
+
* surface; adapters that delegate the complete command set it explicitly so
|
|
75
|
+
* they do not authorize once at the edge and then repeat the host call here.
|
|
76
|
+
*/
|
|
77
|
+
authorizationSurface?: SessionAuthorizationSurface;
|
|
68
78
|
};
|
|
69
79
|
|
|
70
80
|
type SessionAuthorizationCommandDeps = {
|
|
@@ -104,7 +114,7 @@ async function authorizeHumanSessionCommand(
|
|
|
104
114
|
return await requireSessionAuthorization(deps, humanAccessGrant(context), {
|
|
105
115
|
sessionId: context.sessionId,
|
|
106
116
|
operation,
|
|
107
|
-
surface: "core",
|
|
117
|
+
surface: context.authorizationSurface ?? "core",
|
|
108
118
|
});
|
|
109
119
|
}
|
|
110
120
|
|
|
@@ -117,7 +127,7 @@ async function authorizeAgentSessionCommand(
|
|
|
117
127
|
return await requireSessionAuthorization(deps, agentAccessGrant(context), {
|
|
118
128
|
sessionId: targetSessionId,
|
|
119
129
|
operation,
|
|
120
|
-
surface: "core",
|
|
130
|
+
surface: context.authorizationSurface ?? "core",
|
|
121
131
|
});
|
|
122
132
|
}
|
|
123
133
|
|
|
@@ -339,7 +349,12 @@ export async function controlAgentSessionWorkstream(
|
|
|
339
349
|
reason?: string | null;
|
|
340
350
|
},
|
|
341
351
|
) {
|
|
342
|
-
await authorizeAgentSessionCommand(
|
|
352
|
+
const authorization = await authorizeAgentSessionCommand(
|
|
353
|
+
deps,
|
|
354
|
+
context,
|
|
355
|
+
input.targetSessionId,
|
|
356
|
+
"session.control",
|
|
357
|
+
);
|
|
343
358
|
const result = await withWorkspaceRls(deps.db, context.workspaceId, (scoped) =>
|
|
344
359
|
scoped.transaction((tx) =>
|
|
345
360
|
mutateSessionControlInTransaction(tx as unknown as Database, {
|
|
@@ -358,7 +373,7 @@ export async function controlAgentSessionWorkstream(
|
|
|
358
373
|
]);
|
|
359
374
|
await publishWorkspaceControlEvent(deps, context.workspaceId, result.workspaceControlEventId);
|
|
360
375
|
await requestControlWakeDispatch(deps, result.wakeCount);
|
|
361
|
-
return result;
|
|
376
|
+
return { ...result, authorization };
|
|
362
377
|
}
|
|
363
378
|
|
|
364
379
|
function receipt(row: SessionCommandReceiptRow): SessionCommandReceipt {
|
|
@@ -384,8 +399,6 @@ function composerDraft(
|
|
|
384
399
|
revision: row.revision,
|
|
385
400
|
text: row.text,
|
|
386
401
|
resources: row.resources as ComposerDraft["resources"],
|
|
387
|
-
tools: row.tools as ComposerDraft["tools"],
|
|
388
|
-
toolsProvided: row.toolsProvided,
|
|
389
402
|
model: row.model,
|
|
390
403
|
reasoningEffort: row.reasoningEffort as ComposerDraft["reasoningEffort"],
|
|
391
404
|
sourceTurnId: row.sourceTurnId,
|
|
@@ -648,8 +661,6 @@ export async function getHumanComposerDraft(
|
|
|
648
661
|
revision: 0,
|
|
649
662
|
text: "",
|
|
650
663
|
resources: [],
|
|
651
|
-
tools: [],
|
|
652
|
-
toolsProvided: false,
|
|
653
664
|
model: session.model,
|
|
654
665
|
reasoningEffort: reasoningEffortForMetadata(session.metadata, "medium"),
|
|
655
666
|
sourceTurnId: null,
|
package/src/dependencies.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
GitHubAppApiPort,
|
|
6
6
|
ScheduledTask,
|
|
7
7
|
SessionAuthorizationPort,
|
|
8
|
+
TurnInitiator,
|
|
8
9
|
} from "@opengeni/contracts";
|
|
9
10
|
import type { Database } from "@opengeni/db";
|
|
10
11
|
import type { DocumentServices } from "@opengeni/documents";
|
|
@@ -53,8 +54,9 @@ export type SessionWorkflowClient = {
|
|
|
53
54
|
deleteScheduledTaskSchedule: (input: { temporalScheduleId: string }) => Promise<void>;
|
|
54
55
|
triggerScheduledTask: (input: {
|
|
55
56
|
task: ScheduledTask;
|
|
56
|
-
agentRunUsageIdempotencyKey
|
|
57
|
-
triggerWorkflowId
|
|
57
|
+
agentRunUsageIdempotencyKey: string;
|
|
58
|
+
triggerWorkflowId: string;
|
|
59
|
+
initiator: TurnInitiator;
|
|
58
60
|
}) => Promise<void>;
|
|
59
61
|
startRigVerification: (input: {
|
|
60
62
|
workspaceId: string;
|
|
@@ -12,6 +12,12 @@ import {
|
|
|
12
12
|
type EnableCapabilityRequest,
|
|
13
13
|
type McpServerConnectionRef,
|
|
14
14
|
} from "@opengeni/contracts";
|
|
15
|
+
import {
|
|
16
|
+
CODEX_APPS_MCP_SERVER_ID,
|
|
17
|
+
CODEX_APPS_MCP_SERVER_NAME,
|
|
18
|
+
CODEX_APPS_MCP_URL,
|
|
19
|
+
CODEX_APPS_STARTUP_TIMEOUT_MS,
|
|
20
|
+
} from "@opengeni/codex";
|
|
15
21
|
import {
|
|
16
22
|
decryptVariableSetValue,
|
|
17
23
|
decryptedCapabilityHeaders,
|
|
@@ -711,7 +717,36 @@ export async function settingsWithEnabledCapabilityMcpServers(
|
|
|
711
717
|
settings: Settings,
|
|
712
718
|
): Promise<Settings> {
|
|
713
719
|
const enabled = await listEnabledMcpCapabilityServers(db, workspaceId);
|
|
714
|
-
return settingsWithMcpCapabilityServers(settings, enabled);
|
|
720
|
+
return settingsWithCodexAppsMcpServer(settingsWithMcpCapabilityServers(settings, enabled));
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Register Codex Apps as an optional runtime MCP when the deployment enables
|
|
725
|
+
* it. Registration only makes the server selectable; the session tool policy
|
|
726
|
+
* decides whether the model sees it, and Codex credential resolution
|
|
727
|
+
* independently decides whether calls can authenticate.
|
|
728
|
+
*/
|
|
729
|
+
export function settingsWithCodexAppsMcpServer(settings: Settings): Settings {
|
|
730
|
+
if (
|
|
731
|
+
!settings.codexConnectedAppsEnabled ||
|
|
732
|
+
settings.mcpServers.some((server) => server.id === CODEX_APPS_MCP_SERVER_ID)
|
|
733
|
+
) {
|
|
734
|
+
return settings;
|
|
735
|
+
}
|
|
736
|
+
return {
|
|
737
|
+
...settings,
|
|
738
|
+
mcpServers: [
|
|
739
|
+
...settings.mcpServers,
|
|
740
|
+
{
|
|
741
|
+
id: CODEX_APPS_MCP_SERVER_ID,
|
|
742
|
+
name: CODEX_APPS_MCP_SERVER_NAME,
|
|
743
|
+
url: CODEX_APPS_MCP_URL,
|
|
744
|
+
timeoutMs: CODEX_APPS_STARTUP_TIMEOUT_MS,
|
|
745
|
+
// Availability is credential-specific, so discover on every run.
|
|
746
|
+
cacheToolsList: false,
|
|
747
|
+
},
|
|
748
|
+
],
|
|
749
|
+
};
|
|
715
750
|
}
|
|
716
751
|
|
|
717
752
|
export function settingsWithMcpCapabilityServers(
|
|
@@ -21,11 +21,8 @@ export type ResolvedSessionToolPolicy = {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
export type SessionToolPolicyInput = {
|
|
24
|
-
toolPolicy
|
|
24
|
+
toolPolicy: SessionToolPolicy;
|
|
25
25
|
sessionTools: ToolRef[];
|
|
26
|
-
turnTools?: ToolRef[];
|
|
27
|
-
/** Undefined preserves the legacy merge path for pre-provenance callers. */
|
|
28
|
-
turnToolsProvided?: boolean;
|
|
29
26
|
availableMcpServerIds: Iterable<string>;
|
|
30
27
|
/** Current omitted-tools defaults, intentionally narrower than all servers. */
|
|
31
28
|
defaultMcpServerIds?: Iterable<string>;
|
|
@@ -55,7 +52,7 @@ function projectIds(ids: readonly string[]): { ids: string[]; truncated: boolean
|
|
|
55
52
|
* `defaultMcpServerIds` is the capability-only omitted-tools set.
|
|
56
53
|
*/
|
|
57
54
|
export function resolveSessionToolPolicy(input: SessionToolPolicyInput): ResolvedSessionToolPolicy {
|
|
58
|
-
const policy = input.toolPolicy
|
|
55
|
+
const policy = input.toolPolicy;
|
|
59
56
|
const availableIds = new Set(input.availableMcpServerIds);
|
|
60
57
|
// Never infer omitted-tools defaults from the full runtime registry: static
|
|
61
58
|
// MCPs are explicit-only unless they are capability-derived defaults.
|
|
@@ -64,14 +61,8 @@ export function resolveSessionToolPolicy(input: SessionToolPolicyInput): Resolve
|
|
|
64
61
|
availableIds.has(id),
|
|
65
62
|
);
|
|
66
63
|
const mandatoryIdSet = new Set<string>(mandatoryIds);
|
|
67
|
-
const selectedRefs =
|
|
68
|
-
|
|
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;
|
|
64
|
+
const selectedRefs = mergeToolRefs([], input.sessionTools);
|
|
65
|
+
const tracksWorkspaceDefaults = policy.mode === "workspace_default";
|
|
75
66
|
|
|
76
67
|
// Optional capability refs are a historical materialization of a
|
|
77
68
|
// workspace-default selection. They may outlive an installation or its
|
|
@@ -214,7 +205,7 @@ export function sessionWithEffectiveToolPolicy(
|
|
|
214
205
|
return {
|
|
215
206
|
...session,
|
|
216
207
|
effectiveToolPolicy: resolveSessionToolPolicy({
|
|
217
|
-
|
|
208
|
+
toolPolicy: session.toolPolicy,
|
|
218
209
|
sessionTools: session.tools,
|
|
219
210
|
availableMcpServerIds: availableIds,
|
|
220
211
|
defaultMcpServerIds: workspaceDefaultServerIds,
|
package/src/domain/sessions.ts
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
CreateSessionRequest,
|
|
11
11
|
DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
|
|
12
|
+
DEFAULT_FIRST_PARTY_MCP_TOOLS,
|
|
13
|
+
FIRST_PARTY_MCP_TOOL_NAMES,
|
|
12
14
|
OPENGENI_SLACK_BOT_SESSION_METADATA_KEY,
|
|
13
15
|
SessionSpawnDenial,
|
|
14
16
|
ServiceTurnInitiator,
|
|
@@ -19,6 +21,7 @@ import {
|
|
|
19
21
|
type AccessGrant,
|
|
20
22
|
type CreateSessionResponse,
|
|
21
23
|
type GoalSpec,
|
|
24
|
+
type FirstPartyMcpToolName,
|
|
22
25
|
type Permission,
|
|
23
26
|
type ReasoningEffort,
|
|
24
27
|
type ResourceRef,
|
|
@@ -103,7 +106,6 @@ import {
|
|
|
103
106
|
validateFileResources,
|
|
104
107
|
validateGitHubRepositorySelection,
|
|
105
108
|
validateToolRefs,
|
|
106
|
-
validateToolRefsForSessionPolicy,
|
|
107
109
|
withDefaultEnabledCapabilityMcpTools,
|
|
108
110
|
} from "./resources";
|
|
109
111
|
|
|
@@ -127,6 +129,21 @@ export class SessionSpawnDeniedError extends Error {
|
|
|
127
129
|
}
|
|
128
130
|
}
|
|
129
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Resolve per-session first-party tool visibility without consulting
|
|
134
|
+
* authorization. Top-level omission uses the minimal runtime default (stored
|
|
135
|
+
* as null); child omission snapshots the parent's exact effective selection.
|
|
136
|
+
* Explicit [] is authoritative and must never widen.
|
|
137
|
+
*/
|
|
138
|
+
export function resolveFirstPartyMcpToolsForCreate(
|
|
139
|
+
requested: FirstPartyMcpToolName[] | undefined,
|
|
140
|
+
parentStored: FirstPartyMcpToolName[] | null | undefined,
|
|
141
|
+
): FirstPartyMcpToolName[] {
|
|
142
|
+
if (requested !== undefined) return [...requested];
|
|
143
|
+
if (parentStored === undefined) return [...DEFAULT_FIRST_PARTY_MCP_TOOLS];
|
|
144
|
+
return [...(parentStored ?? DEFAULT_FIRST_PARTY_MCP_TOOLS)];
|
|
145
|
+
}
|
|
146
|
+
|
|
130
147
|
function sessionSpawnDeniedMessage(denial: SessionSpawnDenial): string {
|
|
131
148
|
if (denial.code === "nested_agent_depth_override_forbidden") {
|
|
132
149
|
return `requested nested-agent depth limit ${denial.requestedMaxNestedAgentDepthOverride ?? "unknown"} exceeds inherited limit ${denial.effectiveMaxNestedAgentDepth}; workspace:admin is required to increase it`;
|
|
@@ -497,7 +514,7 @@ export async function createAndStartSession(input: {
|
|
|
497
514
|
// Public admission always supplies provenance; optional keeps internal
|
|
498
515
|
// callers that predate durable tool-policy provenance source-compatible
|
|
499
516
|
// during the rolling deploy.
|
|
500
|
-
toolPolicy
|
|
517
|
+
toolPolicy: SessionToolPolicy;
|
|
501
518
|
clientEventId?: string;
|
|
502
519
|
model: string;
|
|
503
520
|
reasoningEffort: Settings["openaiReasoningEffort"];
|
|
@@ -522,6 +539,9 @@ export async function createAndStartSession(input: {
|
|
|
522
539
|
instructions?: string | null;
|
|
523
540
|
// Validated against the creating grant before this is called.
|
|
524
541
|
firstPartyMcpPermissions?: Permission[] | null;
|
|
542
|
+
// Model-visible first-party tool names. Authorization remains controlled by
|
|
543
|
+
// firstPartyMcpPermissions and the target resource checks.
|
|
544
|
+
firstPartyMcpTools: FirstPartyMcpToolName[];
|
|
525
545
|
// Encrypted DB rows plus matching safe metadata for create-time per-session
|
|
526
546
|
// MCP servers. Metadata is the only shape emitted in events/responses.
|
|
527
547
|
mcpServers?: CreateSessionMcpServerInput[];
|
|
@@ -582,7 +602,7 @@ export async function createAndStartSession(input: {
|
|
|
582
602
|
resources: input.resources,
|
|
583
603
|
skills: input.skills ?? [],
|
|
584
604
|
tools: input.tools,
|
|
585
|
-
|
|
605
|
+
toolPolicy: input.toolPolicy,
|
|
586
606
|
metadata: sessionMetadata,
|
|
587
607
|
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
588
608
|
...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
|
|
@@ -593,6 +613,7 @@ export async function createAndStartSession(input: {
|
|
|
593
613
|
rigId: input.rigId ?? null,
|
|
594
614
|
rigVersionId: input.rigVersionId ?? null,
|
|
595
615
|
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
616
|
+
firstPartyMcpTools: input.firstPartyMcpTools,
|
|
596
617
|
instructions: input.instructions ?? null,
|
|
597
618
|
parentSessionId: input.parentSessionId ?? null,
|
|
598
619
|
createIdempotencyKey: input.createIdempotencyKey,
|
|
@@ -626,7 +647,7 @@ export async function createAndStartSession(input: {
|
|
|
626
647
|
resources: input.resources,
|
|
627
648
|
skills: input.skills ?? [],
|
|
628
649
|
tools: input.tools,
|
|
629
|
-
|
|
650
|
+
toolPolicy: input.toolPolicy,
|
|
630
651
|
metadata: sessionMetadata,
|
|
631
652
|
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
632
653
|
...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
|
|
@@ -637,6 +658,7 @@ export async function createAndStartSession(input: {
|
|
|
637
658
|
rigId: input.rigId ?? null,
|
|
638
659
|
rigVersionId: input.rigVersionId ?? null,
|
|
639
660
|
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
661
|
+
firstPartyMcpTools: input.firstPartyMcpTools,
|
|
640
662
|
instructions: input.instructions ?? null,
|
|
641
663
|
parentSessionId: input.parentSessionId ?? null,
|
|
642
664
|
sandboxGroupId: input.sandboxGroupId ?? null,
|
|
@@ -670,7 +692,7 @@ async function finishStartSession(
|
|
|
670
692
|
turnInstructions?: string | null;
|
|
671
693
|
resources: ResourceRef[];
|
|
672
694
|
tools: ToolRef[];
|
|
673
|
-
toolPolicy
|
|
695
|
+
toolPolicy: SessionToolPolicy;
|
|
674
696
|
clientEventId?: string;
|
|
675
697
|
model: string;
|
|
676
698
|
reasoningEffort: Settings["openaiReasoningEffort"];
|
|
@@ -728,7 +750,7 @@ async function finishStartSession(
|
|
|
728
750
|
reasoningEffortFallback: input.reasoningEffort,
|
|
729
751
|
turnExecutionPolicy: input.turnExecutionPolicy,
|
|
730
752
|
createdEventPayload: {
|
|
731
|
-
|
|
753
|
+
toolPolicy: input.toolPolicy,
|
|
732
754
|
...(input.variableSet
|
|
733
755
|
? { variableSetId: input.variableSet.id, variableSetName: input.variableSet.name }
|
|
734
756
|
: {}),
|
|
@@ -898,8 +920,6 @@ export async function postUserMessageTurn(input: {
|
|
|
898
920
|
text: string;
|
|
899
921
|
turnInstructions?: string | null;
|
|
900
922
|
resources: ResourceRef[];
|
|
901
|
-
tools: ToolRef[];
|
|
902
|
-
toolsProvided: boolean;
|
|
903
923
|
model?: string | null;
|
|
904
924
|
reasoningEffort?: Settings["openaiReasoningEffort"] | null;
|
|
905
925
|
clientEventId?: string;
|
|
@@ -943,8 +963,6 @@ export async function postUserMessageTurn(input: {
|
|
|
943
963
|
text: input.text,
|
|
944
964
|
turnInstructions: input.turnInstructions ?? null,
|
|
945
965
|
resources: input.resources,
|
|
946
|
-
tools: input.tools,
|
|
947
|
-
toolsProvided: input.toolsProvided,
|
|
948
966
|
model: requestedModel,
|
|
949
967
|
reasoningEffort: requestedReasoningEffort,
|
|
950
968
|
reasoningEffortFallback: input.reasoningEffortFallback ?? settings.openaiReasoningEffort,
|
|
@@ -1142,12 +1160,10 @@ export async function createSessionForRequest(
|
|
|
1142
1160
|
);
|
|
1143
1161
|
toolPolicy = { mode: "workspace_default", inheritedFromSessionId: null };
|
|
1144
1162
|
}
|
|
1145
|
-
// The first-party MCP server is attached to EVERY session.
|
|
1146
|
-
//
|
|
1147
|
-
//
|
|
1148
|
-
//
|
|
1149
|
-
// the server is attached, so a bare chat still gets titling while the
|
|
1150
|
-
// dangerous tools stay off by default.
|
|
1163
|
+
// The first-party MCP server is attached to EVERY session. Registration is
|
|
1164
|
+
// independently intersected with the exact model-visible selection and the
|
|
1165
|
+
// tool's permission/target authorization predicate, so attachment alone
|
|
1166
|
+
// exposes nothing.
|
|
1151
1167
|
const tools = withFirstPartyTools(selectedTools, runtimeSettings);
|
|
1152
1168
|
await validateGitHubRepositorySelection(db, workspaceId, resources);
|
|
1153
1169
|
if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
@@ -1276,6 +1292,23 @@ export async function createSessionForRequest(
|
|
|
1276
1292
|
"goal-bearing sessions require goals:manage in the resulting first-party MCP permission set",
|
|
1277
1293
|
});
|
|
1278
1294
|
}
|
|
1295
|
+
// Tool visibility is independent from permission authority. A child that
|
|
1296
|
+
// omits the field inherits the parent's exact effective selection; a
|
|
1297
|
+
// top-level omission selects the complete catalog.
|
|
1298
|
+
const firstPartyMcpTools = resolveFirstPartyMcpToolsForCreate(
|
|
1299
|
+
payload.firstPartyMcpTools,
|
|
1300
|
+
parentSession ? parentSession.firstPartyMcpTools : undefined,
|
|
1301
|
+
);
|
|
1302
|
+
if (payload.goal) {
|
|
1303
|
+
const missingGoalTools = ["goal_update", "goal_complete", "goal_pause"].filter(
|
|
1304
|
+
(name) => !firstPartyMcpTools.includes(name as FirstPartyMcpToolName),
|
|
1305
|
+
);
|
|
1306
|
+
if (missingGoalTools.length > 0) {
|
|
1307
|
+
throw new HTTPException(422, {
|
|
1308
|
+
message: `goal-bearing sessions require first-party MCP tools: ${missingGoalTools.join(", ")}`,
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1279
1312
|
// Parent linkage: a worker is linked to its manager ONLY from the
|
|
1280
1313
|
// worker-signed sessionId claim on the creating grant — the manager
|
|
1281
1314
|
// session's own id, signed into the delegated token by the worker and never
|
|
@@ -1541,6 +1574,7 @@ export async function createSessionForRequest(
|
|
|
1541
1574
|
// time. Not surfaced as an event.
|
|
1542
1575
|
instructions: payload.instructions ?? null,
|
|
1543
1576
|
firstPartyMcpPermissions,
|
|
1577
|
+
firstPartyMcpTools,
|
|
1544
1578
|
mcpServers: sessionMcpServers.dbServers,
|
|
1545
1579
|
sessionMcpServers: sessionMcpServers.metadata,
|
|
1546
1580
|
parentSessionId,
|
|
@@ -1597,8 +1631,7 @@ export async function createSessionForRequest(
|
|
|
1597
1631
|
* `POST /sessions/:id/events` and the first-party MCP `session_send_message`
|
|
1598
1632
|
* tool: resource/tool validation, usage limits, the locked append + turn
|
|
1599
1633
|
* enqueue, and usage recording. `toolsProvided: false` durably preserves an
|
|
1600
|
-
*
|
|
1601
|
-
* empty array is a deliberate per-turn narrowing.
|
|
1634
|
+
* Tool selection is durable session state and never rides a follow-up prompt.
|
|
1602
1635
|
*/
|
|
1603
1636
|
export async function acceptSessionUserMessage(
|
|
1604
1637
|
deps: AcceptSessionUserMessageDependencies,
|
|
@@ -1609,8 +1642,6 @@ export async function acceptSessionUserMessage(
|
|
|
1609
1642
|
text: string;
|
|
1610
1643
|
turnInstructions?: string | null;
|
|
1611
1644
|
resources?: ResourceRef[];
|
|
1612
|
-
tools?: ToolRef[];
|
|
1613
|
-
toolsProvided: boolean;
|
|
1614
1645
|
model?: string | null;
|
|
1615
1646
|
reasoningEffort?: ReasoningEffort | null;
|
|
1616
1647
|
clientEventId?: string;
|
|
@@ -1621,23 +1652,12 @@ export async function acceptSessionUserMessage(
|
|
|
1621
1652
|
expectedDraftRevision?: number | null;
|
|
1622
1653
|
},
|
|
1623
1654
|
): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
|
|
1624
|
-
if (input.toolsProvided && !deps.settings.sessionTurnToolReplacementEnabled) {
|
|
1625
|
-
throw new HTTPException(503, {
|
|
1626
|
-
message:
|
|
1627
|
-
"explicit follow-up tool replacement is temporarily unavailable until provenance-aware turn workers finish rolling out; omit tools to inherit the session policy and retry",
|
|
1628
|
-
});
|
|
1629
|
-
}
|
|
1630
1655
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
1631
1656
|
await requireSessionAuthorization(deps, grant, {
|
|
1632
1657
|
sessionId,
|
|
1633
1658
|
operation: input.delivery === "steer" ? "session.steer" : "session.append",
|
|
1634
1659
|
surface: "core",
|
|
1635
1660
|
});
|
|
1636
|
-
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
1637
|
-
db,
|
|
1638
|
-
workspaceId,
|
|
1639
|
-
settings,
|
|
1640
|
-
);
|
|
1641
1661
|
// Hoisted above requireLimit so the codex-billed predicate can resolve the
|
|
1642
1662
|
// turn's effective model (a follow-up turn inherits the session's model). A
|
|
1643
1663
|
// pure read with no side effects.
|
|
@@ -1660,31 +1680,7 @@ export async function acceptSessionUserMessage(
|
|
|
1660
1680
|
reasoningEffort: effectiveReasoningEffort,
|
|
1661
1681
|
reasoningSource: input.reasoningEffort == null ? "session" : "explicit",
|
|
1662
1682
|
});
|
|
1663
|
-
const runtimeSettings = settingsWithSessionMcpServerMetadata(
|
|
1664
|
-
capabilityRuntimeSettings,
|
|
1665
|
-
existingSession.mcpServers,
|
|
1666
|
-
);
|
|
1667
1683
|
const requestedResources = normalizeResources(input.resources ?? []);
|
|
1668
|
-
const tracksWorkspaceDefaults = existingSession.toolPolicy?.mode === "workspace_default";
|
|
1669
|
-
const sessionPolicyTools = withFirstPartyTools(
|
|
1670
|
-
tracksWorkspaceDefaults
|
|
1671
|
-
? withDefaultEnabledCapabilityMcpTools(
|
|
1672
|
-
availableToolRefs(existingSession.tools, runtimeSettings),
|
|
1673
|
-
settings,
|
|
1674
|
-
capabilityRuntimeSettings,
|
|
1675
|
-
)
|
|
1676
|
-
: existingSession.tools,
|
|
1677
|
-
runtimeSettings,
|
|
1678
|
-
);
|
|
1679
|
-
const validatedTools = input.toolsProvided
|
|
1680
|
-
? validateToolRefsForSessionPolicy({
|
|
1681
|
-
requested: input.tools ?? [],
|
|
1682
|
-
settings: runtimeSettings,
|
|
1683
|
-
allowedTools: sessionPolicyTools,
|
|
1684
|
-
message: "message tools may only narrow the session tool policy",
|
|
1685
|
-
})
|
|
1686
|
-
: [];
|
|
1687
|
-
const requestedTools = input.toolsProvided ? validatedTools : [];
|
|
1688
1684
|
await requireLimit(deps, {
|
|
1689
1685
|
accountId: grant.accountId,
|
|
1690
1686
|
workspaceId,
|
|
@@ -1718,8 +1714,6 @@ export async function acceptSessionUserMessage(
|
|
|
1718
1714
|
text: input.text,
|
|
1719
1715
|
turnInstructions: input.turnInstructions ?? null,
|
|
1720
1716
|
resources: requestedResources,
|
|
1721
|
-
tools: requestedTools,
|
|
1722
|
-
toolsProvided: input.toolsProvided,
|
|
1723
1717
|
model: input.model ?? null,
|
|
1724
1718
|
reasoningEffort: input.reasoningEffort ?? null,
|
|
1725
1719
|
reasoningEffortFallback: sessionReasoningEffort,
|
|
@@ -1880,7 +1874,8 @@ export async function updateSessionMcpApprovalPolicy(
|
|
|
1880
1874
|
function toolPolicyAuditSnapshot(
|
|
1881
1875
|
session: Session,
|
|
1882
1876
|
tools: ToolRef[],
|
|
1883
|
-
|
|
1877
|
+
firstPartyMcpTools: FirstPartyMcpToolName[],
|
|
1878
|
+
policy = session.toolPolicy,
|
|
1884
1879
|
) {
|
|
1885
1880
|
// Tool policy refs contain only public server ids and the optional/strict
|
|
1886
1881
|
// execution mode; they never carry URLs, names, headers, credentials,
|
|
@@ -1912,6 +1907,8 @@ function toolPolicyAuditSnapshot(
|
|
|
1912
1907
|
.map((tool) => tool.id),
|
|
1913
1908
|
toolRefs,
|
|
1914
1909
|
toolCount: allToolRefs.length,
|
|
1910
|
+
firstPartyMcpTools: [...firstPartyMcpTools].sort(),
|
|
1911
|
+
firstPartyMcpToolCount: firstPartyMcpTools.length,
|
|
1915
1912
|
truncated: allToolRefs.length > toolRefs.length,
|
|
1916
1913
|
};
|
|
1917
1914
|
}
|
|
@@ -1965,10 +1962,14 @@ export async function updateSessionToolPolicy(
|
|
|
1965
1962
|
return withFirstPartyTools(validatedTools, runtimeSettings);
|
|
1966
1963
|
})()
|
|
1967
1964
|
: null;
|
|
1965
|
+
const explicitRequestedFirstPartyTools = explicitRequest
|
|
1966
|
+
? [...explicitRequest.firstPartyMcpTools]
|
|
1967
|
+
: null;
|
|
1968
1968
|
const workspaceDefaultTools = withFirstPartyTools(
|
|
1969
1969
|
withDefaultEnabledCapabilityMcpTools([], deps.settings, capabilityRuntimeSettings),
|
|
1970
1970
|
runtimeSettings,
|
|
1971
1971
|
);
|
|
1972
|
+
const workspaceDefaultFirstPartyTools = [...FIRST_PARTY_MCP_TOOL_NAMES];
|
|
1972
1973
|
const events = await appendSessionEventsWithLockedSessionUpdate(
|
|
1973
1974
|
deps.db,
|
|
1974
1975
|
grant.workspaceId,
|
|
@@ -1980,6 +1981,7 @@ export async function updateSessionToolPolicy(
|
|
|
1980
1981
|
}
|
|
1981
1982
|
|
|
1982
1983
|
let nextTools: ToolRef[];
|
|
1984
|
+
let nextFirstPartyMcpTools: FirstPartyMcpToolName[];
|
|
1983
1985
|
let nextPolicy: SessionToolPolicy;
|
|
1984
1986
|
if (session.parentSessionId) {
|
|
1985
1987
|
const parent = await context.getLockedSession(session.parentSessionId);
|
|
@@ -1997,6 +1999,9 @@ export async function updateSessionToolPolicy(
|
|
|
1997
1999
|
: parent.tools,
|
|
1998
2000
|
runtimeSettings,
|
|
1999
2001
|
);
|
|
2002
|
+
const parentFirstPartyMcpTools = [
|
|
2003
|
+
...(parent.firstPartyMcpTools ?? DEFAULT_FIRST_PARTY_MCP_TOOLS),
|
|
2004
|
+
];
|
|
2000
2005
|
if (requestedMode === "workspace_default") {
|
|
2001
2006
|
if (!parentTracksWorkspaceDefaults) {
|
|
2002
2007
|
throw new HTTPException(403, {
|
|
@@ -2005,6 +2010,7 @@ export async function updateSessionToolPolicy(
|
|
|
2005
2010
|
});
|
|
2006
2011
|
}
|
|
2007
2012
|
nextTools = parentEffective;
|
|
2013
|
+
nextFirstPartyMcpTools = parentFirstPartyMcpTools;
|
|
2008
2014
|
nextPolicy = {
|
|
2009
2015
|
mode: "workspace_default",
|
|
2010
2016
|
inheritedFromSessionId: parent.id,
|
|
@@ -2016,6 +2022,16 @@ export async function updateSessionToolPolicy(
|
|
|
2016
2022
|
parentEffective,
|
|
2017
2023
|
"session tools may only narrow the parent session tool policy",
|
|
2018
2024
|
);
|
|
2025
|
+
const parentFirstPartySet = new Set(parentFirstPartyMcpTools);
|
|
2026
|
+
const widenedFirstPartyTool = explicitRequestedFirstPartyTools!.find(
|
|
2027
|
+
(tool) => !parentFirstPartySet.has(tool),
|
|
2028
|
+
);
|
|
2029
|
+
if (widenedFirstPartyTool) {
|
|
2030
|
+
throw new HTTPException(403, {
|
|
2031
|
+
message: `session OpenGeni tools may only narrow the parent policy: ${widenedFirstPartyTool}`,
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
nextFirstPartyMcpTools = explicitRequestedFirstPartyTools!;
|
|
2019
2035
|
nextPolicy = {
|
|
2020
2036
|
mode: "explicit",
|
|
2021
2037
|
inheritedFromSessionId: parent.id,
|
|
@@ -2024,20 +2040,29 @@ export async function updateSessionToolPolicy(
|
|
|
2024
2040
|
} else {
|
|
2025
2041
|
nextTools =
|
|
2026
2042
|
requestedMode === "workspace_default" ? workspaceDefaultTools : explicitRequestedTools!;
|
|
2043
|
+
nextFirstPartyMcpTools =
|
|
2044
|
+
requestedMode === "workspace_default"
|
|
2045
|
+
? workspaceDefaultFirstPartyTools
|
|
2046
|
+
: explicitRequestedFirstPartyTools!;
|
|
2027
2047
|
nextPolicy = { mode: requestedMode, inheritedFromSessionId: null };
|
|
2028
2048
|
}
|
|
2029
2049
|
|
|
2030
|
-
const currentPolicy = session.toolPolicy
|
|
2031
|
-
mode: "legacy" as const,
|
|
2032
|
-
inheritedFromSessionId: null,
|
|
2033
|
-
};
|
|
2050
|
+
const currentPolicy = session.toolPolicy;
|
|
2034
2051
|
// JSONB normalizes object-key order on the round trip, so plain
|
|
2035
2052
|
// JSON.stringify would turn an identical retry into a second mutation
|
|
2036
2053
|
// (and version bump) merely because the persisted key order differs from
|
|
2037
2054
|
// the request object. Compare canonical JSON instead.
|
|
2038
2055
|
const unchanged =
|
|
2039
|
-
stableJson({
|
|
2040
|
-
|
|
2056
|
+
stableJson({
|
|
2057
|
+
tools: session.tools,
|
|
2058
|
+
firstPartyMcpTools: session.firstPartyMcpTools ?? DEFAULT_FIRST_PARTY_MCP_TOOLS,
|
|
2059
|
+
policy: currentPolicy,
|
|
2060
|
+
}) ===
|
|
2061
|
+
stableJson({
|
|
2062
|
+
tools: nextTools,
|
|
2063
|
+
firstPartyMcpTools: nextFirstPartyMcpTools,
|
|
2064
|
+
policy: nextPolicy,
|
|
2065
|
+
});
|
|
2041
2066
|
if (unchanged) {
|
|
2042
2067
|
return { events: [] };
|
|
2043
2068
|
}
|
|
@@ -2048,8 +2073,18 @@ export async function updateSessionToolPolicy(
|
|
|
2048
2073
|
{
|
|
2049
2074
|
type: "session.tool_policy.updated" as const,
|
|
2050
2075
|
payload: {
|
|
2051
|
-
before: toolPolicyAuditSnapshot(
|
|
2052
|
-
|
|
2076
|
+
before: toolPolicyAuditSnapshot(
|
|
2077
|
+
session,
|
|
2078
|
+
session.tools,
|
|
2079
|
+
[...(session.firstPartyMcpTools ?? DEFAULT_FIRST_PARTY_MCP_TOOLS)],
|
|
2080
|
+
currentPolicy,
|
|
2081
|
+
),
|
|
2082
|
+
after: toolPolicyAuditSnapshot(
|
|
2083
|
+
session,
|
|
2084
|
+
nextTools,
|
|
2085
|
+
nextFirstPartyMcpTools,
|
|
2086
|
+
nextPolicy,
|
|
2087
|
+
),
|
|
2053
2088
|
version: nextVersion,
|
|
2054
2089
|
effectiveFrom: "next_attempt",
|
|
2055
2090
|
},
|
|
@@ -2057,6 +2092,7 @@ export async function updateSessionToolPolicy(
|
|
|
2057
2092
|
],
|
|
2058
2093
|
update: {
|
|
2059
2094
|
tools: nextTools,
|
|
2095
|
+
firstPartyMcpTools: nextFirstPartyMcpTools,
|
|
2060
2096
|
toolPolicy: nextPolicy,
|
|
2061
2097
|
toolPolicyVersion: nextVersion,
|
|
2062
2098
|
expectedToolPolicyVersion: request.expectedVersion,
|