@opengeni/core 2.7.5-canary.6 → 2.8.0-canary.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/domain/pr-review.d.ts +1 -1
- package/dist/domain/sessions.d.ts +45 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +245 -15
- package/dist/index.js.map +1 -1
- package/dist/model-catalog.d.ts +2 -0
- package/dist/session-authorization.d.ts +29 -1
- package/dist/site-session-origin.d.ts +7 -0
- package/package.json +12 -12
- package/src/domain/resources.ts +1 -1
- package/src/domain/scheduled-tasks.ts +71 -1
- package/src/domain/session-tool-policy.ts +1 -1
- package/src/domain/sessions.ts +201 -5
- package/src/index.ts +1 -0
- package/src/model-catalog.ts +7 -4
- package/src/session-authorization.ts +113 -11
- package/src/site-session-origin.ts +17 -0
package/dist/model-catalog.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ConnectionModelRestrictions } from "@opengeni/db";
|
|
1
2
|
import { type ConfiguredModel, type Settings } from "@opengeni/config";
|
|
2
3
|
import { type ModelAvailabilityV1, type ModelCredentialReadinessV1, type WorkspaceModelPolicyContract } from "@opengeni/contracts";
|
|
3
4
|
import { type Database } from "@opengeni/db";
|
|
@@ -62,6 +63,7 @@ export type ModelCredentialReadinessObservation = {
|
|
|
62
63
|
};
|
|
63
64
|
export declare const MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS: number;
|
|
64
65
|
export type WorkspaceModelSelectionInput = {
|
|
66
|
+
connectionModelRestrictions?: ConnectionModelRestrictions;
|
|
65
67
|
settings: Settings;
|
|
66
68
|
policy: WorkspaceModelPolicyContract | null;
|
|
67
69
|
codexSubscriptionActive: boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SessionAuthorizationActor, SessionAuthorizationListScope, type AccessGrant, type SessionAuthorizationOperation, type SessionAuthorizationSurface, type SessionAuthorizationTarget } from "@opengeni/contracts";
|
|
1
|
+
import { SessionAuthorizationActor, SessionAuthorizationListScope, type AccessGrant, type SessionAgentAccess, type SessionAgentAccessViewer, type SessionAuthorizationOperation, type SessionAuthorizationSurface, type SessionAuthorizationTarget, type SessionEndUser } from "@opengeni/contracts";
|
|
2
2
|
import { type Database, type SessionRlsActorContext } from "@opengeni/db";
|
|
3
3
|
import type { AppDependencies } from "./dependencies.js";
|
|
4
4
|
export type SessionAuthorizationDependencies = Pick<AppDependencies, "db" | "sessionAuthorization">;
|
|
@@ -19,6 +19,34 @@ export type ResolvedSessionAuthorization = {
|
|
|
19
19
|
relatedSessionAccess: "target" | "root";
|
|
20
20
|
reauthorizeAfterMs: number | null;
|
|
21
21
|
};
|
|
22
|
+
/** The frozen agent-access facts of one session, as the pairwise rule sees them. */
|
|
23
|
+
export type SessionAgentAccessFacts = {
|
|
24
|
+
agentAccess: SessionAgentAccess;
|
|
25
|
+
endUser: SessionEndUser | null;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* The agent-to-agent reach rule (migration 0427) for one caller/target pair
|
|
29
|
+
* that live in DIFFERENT root trees. A caller always keeps its own tree, so
|
|
30
|
+
* this is never consulted for same-root access. The most restrictive side
|
|
31
|
+
* wins: a `session` side denies everything; a `user` side requires both
|
|
32
|
+
* sessions to carry the same non-null end-user label; two `workspace` sides
|
|
33
|
+
* are today's behaviour. Humans and API keys never pass through here.
|
|
34
|
+
*/
|
|
35
|
+
export declare function agentAccessPermitsCrossTreeAccess(caller: SessionAgentAccessFacts, target: SessionAgentAccessFacts): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* The in-database list scope for one calling attempt. A `session` caller is
|
|
38
|
+
* pinned to its own root tree when the host does not narrow it. Every caller
|
|
39
|
+
* carries the viewer so `sessionAuthorizationScopeFilter` intersects the host
|
|
40
|
+
* scope with the same pairwise rule in SQL, including for session callers.
|
|
41
|
+
*/
|
|
42
|
+
export declare function agentAccessListScopeForViewer(viewer: SessionAgentAccessViewer, hostScope?: SessionAuthorizationListScope | null): SessionAuthorizationListScope;
|
|
43
|
+
/**
|
|
44
|
+
* Whether a grant acts with live agent-attempt authority: an explicit
|
|
45
|
+
* `agent_attempt` principal kind, or (for legacy tokens without one) any
|
|
46
|
+
* worker-signed exact attempt claim. Shared by the session seam and by the
|
|
47
|
+
* narrowing fences that let a human widen a session an agent may only narrow.
|
|
48
|
+
*/
|
|
49
|
+
export declare function grantHasAgentAttemptAuthority(grant: AccessGrant): boolean;
|
|
22
50
|
export declare function sessionRlsActorForAuthorization(authorization: ResolvedSessionAuthorization): SessionRlsActorContext;
|
|
23
51
|
export declare function withResolvedSessionAuthorization<T>(authorization: ResolvedSessionAuthorization, fn: () => Promise<T>): Promise<T>;
|
|
24
52
|
/**
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type SiteSessionOrigin = {
|
|
2
|
+
siteId: string;
|
|
3
|
+
title: string;
|
|
4
|
+
};
|
|
5
|
+
/** Request-local provenance, set only after the API validates a published Site version. */
|
|
6
|
+
export declare function withSiteSessionOrigin<T>(origin: SiteSessionOrigin, run: () => T): T;
|
|
7
|
+
export declare function sessionCreationMetadata(metadata: Record<string, unknown>): Record<string, unknown>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0-canary.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": {
|
|
@@ -54,17 +54,17 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
57
|
-
"@opengeni/capabilities": "^0.3.
|
|
58
|
-
"@opengeni/codex": "^0.2.
|
|
59
|
-
"@opengeni/config": "^1.0.
|
|
60
|
-
"@opengeni/contracts": "^2.
|
|
61
|
-
"@opengeni/db": "^4.
|
|
62
|
-
"@opengeni/documents": "^0.8.
|
|
63
|
-
"@opengeni/events": "^0.4.
|
|
64
|
-
"@opengeni/network": "^0.3.
|
|
65
|
-
"@opengeni/observability": "^0.8.
|
|
66
|
-
"@opengeni/runtime": "^2.
|
|
67
|
-
"@opengeni/storage": "^0.2.
|
|
57
|
+
"@opengeni/capabilities": "^0.3.3-canary.0",
|
|
58
|
+
"@opengeni/codex": "^0.2.22-canary.0",
|
|
59
|
+
"@opengeni/config": "^1.0.1-canary.0",
|
|
60
|
+
"@opengeni/contracts": "^2.14.0-canary.0",
|
|
61
|
+
"@opengeni/db": "^4.1.0-canary.0",
|
|
62
|
+
"@opengeni/documents": "^0.8.20-canary.0",
|
|
63
|
+
"@opengeni/events": "^0.4.18-canary.0",
|
|
64
|
+
"@opengeni/network": "^0.3.1-canary.0",
|
|
65
|
+
"@opengeni/observability": "^0.8.20-canary.0",
|
|
66
|
+
"@opengeni/runtime": "^2.4.0-canary.0",
|
|
67
|
+
"@opengeni/storage": "^0.2.121-canary.0",
|
|
68
68
|
"hono": "^4.12.18",
|
|
69
69
|
"zod": "^4.2.1"
|
|
70
70
|
},
|
package/src/domain/resources.ts
CHANGED
|
@@ -100,7 +100,7 @@ export function withWorkspaceDefaultMcpTools(
|
|
|
100
100
|
runtimeSettings: McpSettings,
|
|
101
101
|
defaults: WorkspaceSessionToolDefaults | null,
|
|
102
102
|
): ToolRef[] {
|
|
103
|
-
if (!defaults) {
|
|
103
|
+
if (!defaults?.mcpServerIds) {
|
|
104
104
|
return withDefaultEnabledCapabilityMcpTools(tools, settings, runtimeSettings);
|
|
105
105
|
}
|
|
106
106
|
return mergeToolRefs(
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
allowedFirstPartyMcpToolsForSession,
|
|
3
|
+
resolveFirstPartyMcpToolPolicy,
|
|
4
|
+
type Settings,
|
|
5
|
+
} from "@opengeni/config";
|
|
2
6
|
import type {
|
|
3
7
|
AccessGrant,
|
|
4
8
|
McpPersonalConnectionDelegation,
|
|
@@ -17,6 +21,9 @@ import {
|
|
|
17
21
|
DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
|
|
18
22
|
OPENGENI_SLACK_BOT_SESSION_METADATA_KEY,
|
|
19
23
|
resolveWorkspaceSessionToolDefaults,
|
|
24
|
+
SessionAgentAccess,
|
|
25
|
+
SessionEndUser,
|
|
26
|
+
SessionMemoryScope,
|
|
20
27
|
} from "@opengeni/contracts";
|
|
21
28
|
import {
|
|
22
29
|
createScheduledTask,
|
|
@@ -41,6 +48,7 @@ import {
|
|
|
41
48
|
withWorkspaceSubjectRls,
|
|
42
49
|
resolveXaiProviderAccountAuthoritySnapshotForAcceptance,
|
|
43
50
|
type Database,
|
|
51
|
+
type ScheduledTaskCreatorPolicy,
|
|
44
52
|
type TemporalScheduleCleanupClaim,
|
|
45
53
|
type UpdateScheduledTaskInput,
|
|
46
54
|
} from "@opengeni/db";
|
|
@@ -246,6 +254,14 @@ export async function createValidatedScheduledTask(input: {
|
|
|
246
254
|
...scheduledConnectionSurfaceEligibility(runtimeSettings, target),
|
|
247
255
|
});
|
|
248
256
|
const creationInitiator = creationInitiatorForGrant(input.grant);
|
|
257
|
+
const creatorPolicy = creationInitiator.actor
|
|
258
|
+
? await frozenScheduledTaskCreatorPolicy({
|
|
259
|
+
db: input.db,
|
|
260
|
+
settings: input.settings,
|
|
261
|
+
grant: input.grant,
|
|
262
|
+
sessionId: creationInitiator.actor.sessionId,
|
|
263
|
+
})
|
|
264
|
+
: null;
|
|
249
265
|
const xaiProviderAccountAuthoritySnapshot: XaiProviderAccountAuthoritySnapshotV1 =
|
|
250
266
|
creationInitiator.actor
|
|
251
267
|
? await getSessionTurnXaiProviderAccountAuthoritySnapshot(
|
|
@@ -285,6 +301,7 @@ export async function createValidatedScheduledTask(input: {
|
|
|
285
301
|
createdByActor: creationInitiator.actor ?? null,
|
|
286
302
|
personalConnectionDelegations,
|
|
287
303
|
xaiProviderAccountAuthoritySnapshot,
|
|
304
|
+
creatorPolicy,
|
|
288
305
|
targetSessionId: target?.id ?? null,
|
|
289
306
|
variableSetId: input.payload.variableSetId ?? null,
|
|
290
307
|
rigId: input.payload.rigId ?? null,
|
|
@@ -294,6 +311,59 @@ export async function createValidatedScheduledTask(input: {
|
|
|
294
311
|
);
|
|
295
312
|
}
|
|
296
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Freeze the creating session's boundary onto an agent-created task so the
|
|
316
|
+
* sessions generated for it inherit exactly what the creator could see and
|
|
317
|
+
* do, never the deployment default. Tools are the session's effective
|
|
318
|
+
* model-visible selection under the deployment ceiling; permissions are the
|
|
319
|
+
* session's effective first-party set intersected with what the calling
|
|
320
|
+
* grant actually holds (a narrowly delegated spawn token cannot hand a
|
|
321
|
+
* schedule more than itself). The session access policy is copied from the
|
|
322
|
+
* projection when it exposes those facts; each absent fact is stored as null
|
|
323
|
+
* so a generated session keeps its own default for that key.
|
|
324
|
+
*/
|
|
325
|
+
async function frozenScheduledTaskCreatorPolicy(input: {
|
|
326
|
+
db: Database;
|
|
327
|
+
settings: Settings;
|
|
328
|
+
grant: AccessGrant;
|
|
329
|
+
sessionId: string;
|
|
330
|
+
}): Promise<ScheduledTaskCreatorPolicy> {
|
|
331
|
+
const session = await getSession(input.db, input.grant.workspaceId, input.sessionId);
|
|
332
|
+
if (!session) {
|
|
333
|
+
throw new HTTPException(403, {
|
|
334
|
+
message: "the calling agent session is not available in this workspace",
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
const firstPartyMcpTools = allowedFirstPartyMcpToolsForSession(
|
|
338
|
+
input.settings,
|
|
339
|
+
session.firstPartyMcpTools,
|
|
340
|
+
);
|
|
341
|
+
const firstPartyMcpPermissions = (
|
|
342
|
+
session.firstPartyMcpPermissions ?? [...DEFAULT_FIRST_PARTY_MCP_PERMISSIONS]
|
|
343
|
+
).filter((permission) => hasPermission(input.grant.permissions, permission));
|
|
344
|
+
if (firstPartyMcpPermissions.length === 0) {
|
|
345
|
+
throw new HTTPException(403, {
|
|
346
|
+
message:
|
|
347
|
+
"the calling agent session holds no first-party MCP permission it could delegate to scheduled runs",
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
// The projection facts are validated through the access-scope contract so
|
|
351
|
+
// only a well-formed value is frozen; anything else stores null for that key.
|
|
352
|
+
const projection = session as unknown as Record<string, unknown>;
|
|
353
|
+
const agentAccess = SessionAgentAccess.safeParse(projection["agentAccess"]);
|
|
354
|
+
const endUser = SessionEndUser.safeParse(projection["endUser"]);
|
|
355
|
+
const memoryScope = SessionMemoryScope.safeParse(projection["memoryScope"]);
|
|
356
|
+
return {
|
|
357
|
+
firstPartyMcpTools,
|
|
358
|
+
firstPartyMcpPermissions,
|
|
359
|
+
sessionPolicy: {
|
|
360
|
+
agentAccess: agentAccess.success ? agentAccess.data : null,
|
|
361
|
+
endUser: endUser.success ? { source: endUser.data.source, id: endUser.data.id } : null,
|
|
362
|
+
memoryScope: memoryScope.success ? memoryScope.data : null,
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
297
367
|
function nestedPostgresMessage(error: unknown): string | null {
|
|
298
368
|
const queue: unknown[] = [error];
|
|
299
369
|
const seen = new Set<unknown>();
|
|
@@ -188,7 +188,7 @@ export async function workspaceSessionToolPolicyDefaultServerIds(
|
|
|
188
188
|
const availableDefaults = defaultSessionMcpServerIds(runtimeSettings.mcpServers);
|
|
189
189
|
const workspace = await requireWorkspace(db, workspaceId);
|
|
190
190
|
const configured = resolveWorkspaceSessionToolDefaults(workspace.settings);
|
|
191
|
-
if (!configured) return availableDefaults;
|
|
191
|
+
if (!configured?.mcpServerIds) return availableDefaults;
|
|
192
192
|
const available = new Set(availableDefaults);
|
|
193
193
|
return sortedIds(configured.mcpServerIds.filter((id) => available.has(id)));
|
|
194
194
|
}
|
package/src/domain/sessions.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CODEX_MODEL_ID_PREFIX, isCodexBilledModel } from "@opengeni/codex";
|
|
2
|
+
import { sessionCreationMetadata } from "../site-session-origin";
|
|
2
3
|
import {
|
|
3
4
|
canonicalizeConfiguredModelId,
|
|
4
5
|
configuredAllowedModels,
|
|
@@ -42,7 +43,10 @@ import {
|
|
|
42
43
|
type ReasoningEffort,
|
|
43
44
|
type ResourceRef,
|
|
44
45
|
type Session,
|
|
46
|
+
type SessionAgentAccess,
|
|
45
47
|
type SessionCommandReceipt,
|
|
48
|
+
type SessionEndUser,
|
|
49
|
+
type SessionMemoryScope,
|
|
46
50
|
type SessionSkill,
|
|
47
51
|
type SessionEvent,
|
|
48
52
|
SessionMcpApprovalPolicy,
|
|
@@ -135,6 +139,7 @@ import type {
|
|
|
135
139
|
SessionWorkflowClient,
|
|
136
140
|
} from "../dependencies";
|
|
137
141
|
import {
|
|
142
|
+
grantHasAgentAttemptAuthority,
|
|
138
143
|
requireLiveAgentAttemptAuthorization,
|
|
139
144
|
requireSessionAuthorization,
|
|
140
145
|
SessionAuthorizationDeniedError,
|
|
@@ -237,11 +242,34 @@ export class SessionSpawnDeniedError extends Error {
|
|
|
237
242
|
}
|
|
238
243
|
}
|
|
239
244
|
|
|
245
|
+
/**
|
|
246
|
+
* A session's effective first-party selection as a ceiling for narrowing:
|
|
247
|
+
* the stored selection (or the deployment default for a legacy null) under
|
|
248
|
+
* the deployment ceiling, with the resume counterpart of pause authority the
|
|
249
|
+
* runtime already grants to existing sessions.
|
|
250
|
+
*/
|
|
251
|
+
export function effectiveFirstPartyMcpToolCeiling(
|
|
252
|
+
stored: readonly FirstPartyMcpToolName[] | null | undefined,
|
|
253
|
+
policy: {
|
|
254
|
+
default: readonly FirstPartyMcpToolName[];
|
|
255
|
+
allowed: readonly FirstPartyMcpToolName[];
|
|
256
|
+
},
|
|
257
|
+
): Set<FirstPartyMcpToolName> {
|
|
258
|
+
const allowed = new Set(policy.allowed);
|
|
259
|
+
const ceiling = new Set([...(stored ?? policy.default)].filter((tool) => allowed.has(tool)));
|
|
260
|
+
if (ceiling.has("goal_pause") && allowed.has("goal_resume")) ceiling.add("goal_resume");
|
|
261
|
+
return ceiling;
|
|
262
|
+
}
|
|
263
|
+
|
|
240
264
|
/**
|
|
241
265
|
* Resolve per-session first-party tool visibility without consulting
|
|
242
266
|
* authorization. Top-level omission snapshots the complete runtime default;
|
|
243
267
|
* child omission snapshots the parent's exact effective selection. Explicit
|
|
244
|
-
* [] is authoritative and must never widen.
|
|
268
|
+
* [] is authoritative and must never widen. An explicit child selection may
|
|
269
|
+
* only narrow the parent's effective selection: a session that was handed a
|
|
270
|
+
* reduced catalog cannot spawn a child that sees more than it does. A
|
|
271
|
+
* top-level explicit selection keeps only the deployment ceiling (checked by
|
|
272
|
+
* the caller) because there is no creator selection to narrow.
|
|
245
273
|
*/
|
|
246
274
|
export function resolveFirstPartyMcpToolsForCreate(
|
|
247
275
|
requested: FirstPartyMcpToolName[] | undefined,
|
|
@@ -254,7 +282,18 @@ export function resolveFirstPartyMcpToolsForCreate(
|
|
|
254
282
|
allowed: FIRST_PARTY_MCP_TOOL_NAMES,
|
|
255
283
|
},
|
|
256
284
|
): FirstPartyMcpToolName[] {
|
|
257
|
-
if (requested !== undefined)
|
|
285
|
+
if (requested !== undefined) {
|
|
286
|
+
if (parentStored !== undefined) {
|
|
287
|
+
const parentCeiling = effectiveFirstPartyMcpToolCeiling(parentStored, policy);
|
|
288
|
+
const widened = requested.find((tool) => !parentCeiling.has(tool));
|
|
289
|
+
if (widened) {
|
|
290
|
+
throw new HTTPException(403, {
|
|
291
|
+
message: `child first-party MCP tools may only narrow the parent session selection: ${widened}`,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return [...requested];
|
|
296
|
+
}
|
|
258
297
|
const allowed = new Set(policy.allowed);
|
|
259
298
|
const inherited = parentStored === undefined ? policy.default : (parentStored ?? policy.default);
|
|
260
299
|
return [...inherited].filter((tool) => allowed.has(tool));
|
|
@@ -756,6 +795,12 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
756
795
|
// Model-visible first-party tool names. Authorization remains controlled by
|
|
757
796
|
// firstPartyMcpPermissions and the target resource checks.
|
|
758
797
|
firstPartyMcpTools: FirstPartyMcpToolName[];
|
|
798
|
+
// Agent-access scope, opaque end-user label, and typed Memory selector
|
|
799
|
+
// (migration 0427), already resolved against the parent by the caller.
|
|
800
|
+
// Omitted keeps the workspace defaults for internal lifecycle callers.
|
|
801
|
+
agentAccess?: SessionAgentAccess;
|
|
802
|
+
endUser?: SessionEndUser | null;
|
|
803
|
+
memoryScope?: SessionMemoryScope;
|
|
759
804
|
// Encrypted DB rows plus matching safe metadata for create-time per-session
|
|
760
805
|
// MCP servers. Metadata is the only shape emitted in events/responses.
|
|
761
806
|
mcpServers?: CreateSessionMcpServerInput[];
|
|
@@ -969,6 +1014,9 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
969
1014
|
firstPartyMcpTools: input.firstPartyMcpTools,
|
|
970
1015
|
instructions: input.instructions ?? null,
|
|
971
1016
|
policyRole: input.policyRole ?? null,
|
|
1017
|
+
...(input.agentAccess ? { agentAccess: input.agentAccess } : {}),
|
|
1018
|
+
...(input.endUser !== undefined ? { endUser: input.endUser } : {}),
|
|
1019
|
+
...(input.memoryScope ? { memoryScope: input.memoryScope } : {}),
|
|
972
1020
|
parentSessionId: input.parentSessionId ?? null,
|
|
973
1021
|
createIdempotencyKey: input.createIdempotencyKey,
|
|
974
1022
|
selectedInstalledSkillIds: input.selectedInstalledSkillIds ?? [],
|
|
@@ -1059,6 +1107,9 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
1059
1107
|
firstPartyMcpTools: input.firstPartyMcpTools,
|
|
1060
1108
|
instructions: input.instructions ?? null,
|
|
1061
1109
|
policyRole: input.policyRole ?? null,
|
|
1110
|
+
...(input.agentAccess ? { agentAccess: input.agentAccess } : {}),
|
|
1111
|
+
...(input.endUser !== undefined ? { endUser: input.endUser } : {}),
|
|
1112
|
+
...(input.memoryScope ? { memoryScope: input.memoryScope } : {}),
|
|
1062
1113
|
parentSessionId: input.parentSessionId ?? null,
|
|
1063
1114
|
sandboxGroupId: input.sandboxGroupId ?? null,
|
|
1064
1115
|
...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
|
|
@@ -1582,7 +1633,10 @@ export async function postUserMessageTurn(
|
|
|
1582
1633
|
// model inherits the session's model downstream (always a configured id).
|
|
1583
1634
|
assertConfiguredModel(settings, requestedModel);
|
|
1584
1635
|
const sessionForModelGate = await requireSession(db, workspaceId, sessionId);
|
|
1585
|
-
|
|
1636
|
+
// Acceptance already froze this policy before resource/credential validation.
|
|
1637
|
+
// A different turn starting meanwhile must not change the model we gate here.
|
|
1638
|
+
const effectiveModelForGate =
|
|
1639
|
+
input.turnExecutionPolicy?.productModelId ?? requestedModel ?? sessionForModelGate.model;
|
|
1586
1640
|
const freshWorkspaceCustomModel =
|
|
1587
1641
|
requestedModel !== null &&
|
|
1588
1642
|
isWorkspaceCustomModelId(settings, requestedModel) &&
|
|
@@ -1760,6 +1814,87 @@ export function resolveSessionCreateVisibility(input: {
|
|
|
1760
1814
|
return input.requestedVisibility === "private" ? "user_private" : "workspace_shared";
|
|
1761
1815
|
}
|
|
1762
1816
|
|
|
1817
|
+
export type SessionCreateScope = {
|
|
1818
|
+
agentAccess: SessionAgentAccess;
|
|
1819
|
+
endUser: SessionEndUser | null;
|
|
1820
|
+
memoryScope: SessionMemoryScope;
|
|
1821
|
+
};
|
|
1822
|
+
|
|
1823
|
+
const AGENT_ACCESS_WIDTH: Record<SessionAgentAccess, number> = {
|
|
1824
|
+
session: 0,
|
|
1825
|
+
user: 1,
|
|
1826
|
+
workspace: 2,
|
|
1827
|
+
};
|
|
1828
|
+
|
|
1829
|
+
const MEMORY_SCOPE_WIDTH: Record<SessionMemoryScope, number> = {
|
|
1830
|
+
off: 0,
|
|
1831
|
+
session: 1,
|
|
1832
|
+
user: 2,
|
|
1833
|
+
workspace: 3,
|
|
1834
|
+
};
|
|
1835
|
+
|
|
1836
|
+
/**
|
|
1837
|
+
* Resolve a new session's agent-access scope, end-user label, and Memory
|
|
1838
|
+
* selector (migration 0427). A top-level request takes its own values. An
|
|
1839
|
+
* agent-created child inherits every omitted value from its trusted parent
|
|
1840
|
+
* and may only NARROW an explicit one: agent access workspace > user >
|
|
1841
|
+
* session, memory workspace > user > session > off, and the label must equal
|
|
1842
|
+
* the parent's. Widening is a 403 because the parent's declared reach is a
|
|
1843
|
+
* security boundary the child's own request cannot cross; a memory `user`
|
|
1844
|
+
* selector without a label is a 422 in either position.
|
|
1845
|
+
*/
|
|
1846
|
+
export function resolveSessionCreateScope(input: {
|
|
1847
|
+
requested: {
|
|
1848
|
+
agentAccess: SessionAgentAccess;
|
|
1849
|
+
agentAccessProvided: boolean;
|
|
1850
|
+
endUser: SessionEndUser | null;
|
|
1851
|
+
endUserProvided: boolean;
|
|
1852
|
+
memoryScope: SessionMemoryScope;
|
|
1853
|
+
memoryScopeProvided: boolean;
|
|
1854
|
+
};
|
|
1855
|
+
parent: SessionCreateScope | null;
|
|
1856
|
+
}): SessionCreateScope {
|
|
1857
|
+
const { requested, parent } = input;
|
|
1858
|
+
let resolved: SessionCreateScope;
|
|
1859
|
+
if (!parent) {
|
|
1860
|
+
resolved = {
|
|
1861
|
+
agentAccess: requested.agentAccess,
|
|
1862
|
+
endUser: requested.endUser,
|
|
1863
|
+
memoryScope: requested.memoryScope,
|
|
1864
|
+
};
|
|
1865
|
+
} else {
|
|
1866
|
+
const agentAccess = requested.agentAccessProvided ? requested.agentAccess : parent.agentAccess;
|
|
1867
|
+
if (AGENT_ACCESS_WIDTH[agentAccess] > AGENT_ACCESS_WIDTH[parent.agentAccess]) {
|
|
1868
|
+
throw new HTTPException(403, {
|
|
1869
|
+
message: "child agent access may only narrow the parent session",
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
if (requested.endUserProvided) {
|
|
1873
|
+
const same =
|
|
1874
|
+
requested.endUser !== null &&
|
|
1875
|
+
parent.endUser !== null &&
|
|
1876
|
+
requested.endUser.source === parent.endUser.source &&
|
|
1877
|
+
requested.endUser.id === parent.endUser.id;
|
|
1878
|
+
if (!same) {
|
|
1879
|
+
throw new HTTPException(403, {
|
|
1880
|
+
message: "child end-user label must equal the parent session label",
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
const memoryScope = requested.memoryScopeProvided ? requested.memoryScope : parent.memoryScope;
|
|
1885
|
+
if (MEMORY_SCOPE_WIDTH[memoryScope] > MEMORY_SCOPE_WIDTH[parent.memoryScope]) {
|
|
1886
|
+
throw new HTTPException(403, {
|
|
1887
|
+
message: "child memory scope may only narrow the parent session",
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
resolved = { agentAccess, endUser: parent.endUser, memoryScope };
|
|
1891
|
+
}
|
|
1892
|
+
if (resolved.memoryScope === "user" && resolved.endUser === null) {
|
|
1893
|
+
throw new HTTPException(422, { message: 'memoryScope "user" requires an endUser label' });
|
|
1894
|
+
}
|
|
1895
|
+
return resolved;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1763
1898
|
async function resolveWorkspaceModelBoundarySettings(
|
|
1764
1899
|
deps: Pick<ApiRouteDeps, "db" | "settings" | "catalogSourceSettings">,
|
|
1765
1900
|
grant: AccessGrant,
|
|
@@ -1832,6 +1967,7 @@ export async function createSessionForRequestWithOutcome(
|
|
|
1832
1967
|
agentChildPresentation?: AgentChildSessionCreatePresentation,
|
|
1833
1968
|
): Promise<CreateSessionRequestOutcome> {
|
|
1834
1969
|
const payload = CreateSessionRequest.parse(rawPayload);
|
|
1970
|
+
payload.metadata = sessionCreationMetadata(payload.metadata);
|
|
1835
1971
|
if (hasReservedOpenGeniSlackBotSessionMetadata(payload.metadata)) {
|
|
1836
1972
|
throw new HTTPException(422, {
|
|
1837
1973
|
message: `${OPENGENI_SLACK_BOT_SESSION_METADATA_KEY} is reserved for scheduler routing`,
|
|
@@ -1902,6 +2038,27 @@ export async function createSessionForRequestWithOutcome(
|
|
|
1902
2038
|
message: error instanceof Error ? error.message : "invalid child visibility",
|
|
1903
2039
|
});
|
|
1904
2040
|
}
|
|
2041
|
+
// Agent-access/end-user/memory scope inherit and narrow exactly like
|
|
2042
|
+
// visibility: presence is read from the raw request because the Zod
|
|
2043
|
+
// defaults erase absent-vs-explicit, and the parent side comes from the
|
|
2044
|
+
// durable row rather than anything the caller sent.
|
|
2045
|
+
const sessionScope = resolveSessionCreateScope({
|
|
2046
|
+
requested: {
|
|
2047
|
+
agentAccess: payload.agentAccess,
|
|
2048
|
+
agentAccessProvided: hasOwnProperty(rawPayload, "agentAccess"),
|
|
2049
|
+
endUser: payload.endUser ?? null,
|
|
2050
|
+
endUserProvided: hasOwnProperty(rawPayload, "endUser"),
|
|
2051
|
+
memoryScope: payload.memoryScope,
|
|
2052
|
+
memoryScopeProvided: hasOwnProperty(rawPayload, "memoryScope"),
|
|
2053
|
+
},
|
|
2054
|
+
parent: parentAuthority
|
|
2055
|
+
? {
|
|
2056
|
+
agentAccess: parentAuthority.agentAccess,
|
|
2057
|
+
endUser: parentAuthority.endUser,
|
|
2058
|
+
memoryScope: parentAuthority.memoryScope,
|
|
2059
|
+
}
|
|
2060
|
+
: null,
|
|
2061
|
+
});
|
|
1905
2062
|
const creationInitiator = creationInitiatorForGrant(grant);
|
|
1906
2063
|
const parentCallingTurn =
|
|
1907
2064
|
parentSession && creationInitiator.actor
|
|
@@ -2434,7 +2591,7 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2434
2591
|
message: `first-party MCP tool is disabled by deployment policy: ${disallowedFirstPartyMcpTool}`,
|
|
2435
2592
|
});
|
|
2436
2593
|
}
|
|
2437
|
-
const workspaceFirstPartyDefaults = workspaceSessionToolDefaults?.firstPartyMcpTools
|
|
2594
|
+
const workspaceFirstPartyDefaults = workspaceSessionToolDefaults?.firstPartyMcpTools?.filter(
|
|
2438
2595
|
(tool) => deploymentFirstPartyMcpToolPolicy.allowed.includes(tool),
|
|
2439
2596
|
);
|
|
2440
2597
|
const firstPartyMcpTools = resolveFirstPartyMcpToolsForCreate(
|
|
@@ -2821,6 +2978,9 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2821
2978
|
// time. Not surfaced as an event.
|
|
2822
2979
|
instructions: payload.instructions ?? null,
|
|
2823
2980
|
policyRole: payload.policyRole ?? null,
|
|
2981
|
+
agentAccess: sessionScope.agentAccess,
|
|
2982
|
+
endUser: sessionScope.endUser,
|
|
2983
|
+
memoryScope: sessionScope.memoryScope,
|
|
2824
2984
|
firstPartyMcpPermissions,
|
|
2825
2985
|
firstPartyMcpTools,
|
|
2826
2986
|
mcpServers: sessionMcpServers.dbServers,
|
|
@@ -3532,6 +3692,7 @@ export async function updateSessionToolPolicy(
|
|
|
3532
3692
|
surface: "core",
|
|
3533
3693
|
});
|
|
3534
3694
|
requirePermission(grant, "sessions:control");
|
|
3695
|
+
const agentAttemptCaller = grantHasAgentAttemptAuthority(grant);
|
|
3535
3696
|
|
|
3536
3697
|
const existingSession = await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
3537
3698
|
const workspace = await requireWorkspace(deps.db, grant.workspaceId);
|
|
@@ -3585,7 +3746,7 @@ export async function updateSessionToolPolicy(
|
|
|
3585
3746
|
runtimeSettings,
|
|
3586
3747
|
);
|
|
3587
3748
|
const workspaceDefaultFirstPartyTools = [
|
|
3588
|
-
...(workspaceSessionToolDefaults?.firstPartyMcpTools
|
|
3749
|
+
...(workspaceSessionToolDefaults?.firstPartyMcpTools?.filter((tool) =>
|
|
3589
3750
|
deploymentFirstPartyMcpToolPolicy.allowed.includes(tool),
|
|
3590
3751
|
) ?? deploymentFirstPartyMcpToolPolicy.default),
|
|
3591
3752
|
];
|
|
@@ -3670,6 +3831,41 @@ export async function updateSessionToolPolicy(
|
|
|
3670
3831
|
? workspaceDefaultFirstPartyTools
|
|
3671
3832
|
: explicitRequestedFirstPartyTools!;
|
|
3672
3833
|
nextPolicy = { mode: requestedMode, inheritedFromSessionId: null };
|
|
3834
|
+
if (agentAttemptCaller) {
|
|
3835
|
+
// A human or API key may widen a top-level session; a live agent
|
|
3836
|
+
// attempt may only narrow relative to the session's CURRENT
|
|
3837
|
+
// effective policy, in either mode. Adopting workspace defaults is a
|
|
3838
|
+
// widen whenever it adds a server or tool the session does not hold.
|
|
3839
|
+
const sessionTracksWorkspaceDefaults = session.toolPolicy?.mode === "workspace_default";
|
|
3840
|
+
const currentEffectiveTools = withFirstPartyTools(
|
|
3841
|
+
sessionTracksWorkspaceDefaults
|
|
3842
|
+
? withWorkspaceDefaultMcpTools(
|
|
3843
|
+
availableToolRefs(session.tools, runtimeSettings),
|
|
3844
|
+
deps.settings,
|
|
3845
|
+
runtimeSettings,
|
|
3846
|
+
workspaceSessionToolDefaults,
|
|
3847
|
+
)
|
|
3848
|
+
: session.tools,
|
|
3849
|
+
runtimeSettings,
|
|
3850
|
+
);
|
|
3851
|
+
assertToolRefsSubset(
|
|
3852
|
+
nextTools,
|
|
3853
|
+
currentEffectiveTools,
|
|
3854
|
+
"an agent may only narrow its session tool policy",
|
|
3855
|
+
);
|
|
3856
|
+
const currentFirstPartyCeiling = effectiveFirstPartyMcpToolCeiling(
|
|
3857
|
+
session.firstPartyMcpTools,
|
|
3858
|
+
deploymentFirstPartyMcpToolPolicy,
|
|
3859
|
+
);
|
|
3860
|
+
const widenedFirstPartyTool = nextFirstPartyMcpTools.find(
|
|
3861
|
+
(tool) => !currentFirstPartyCeiling.has(tool),
|
|
3862
|
+
);
|
|
3863
|
+
if (widenedFirstPartyTool) {
|
|
3864
|
+
throw new HTTPException(403, {
|
|
3865
|
+
message: `an agent may only narrow its session OpenGeni tools: ${widenedFirstPartyTool}`,
|
|
3866
|
+
});
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3673
3869
|
}
|
|
3674
3870
|
|
|
3675
3871
|
const currentPolicy = session.toolPolicy;
|
package/src/index.ts
CHANGED
package/src/model-catalog.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { modelAllowedByConnections, type ConnectionModelRestrictions } from "@opengeni/db";
|
|
1
2
|
import {
|
|
2
3
|
applyModelCatalogDocument,
|
|
3
4
|
configuredGatewayWorkspaceProductModelIds,
|
|
@@ -379,6 +380,7 @@ export type ModelCredentialReadinessObservation =
|
|
|
379
380
|
export const MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS = 5 * 60_000;
|
|
380
381
|
|
|
381
382
|
export type WorkspaceModelSelectionInput = {
|
|
383
|
+
connectionModelRestrictions?: ConnectionModelRestrictions;
|
|
382
384
|
settings: Settings;
|
|
383
385
|
policy: WorkspaceModelPolicyContract | null;
|
|
384
386
|
codexSubscriptionActive: boolean;
|
|
@@ -708,10 +710,11 @@ export function resolveWorkspaceModelSelection(
|
|
|
708
710
|
|
|
709
711
|
return configuredModels(catalogSettings).map((model) => {
|
|
710
712
|
const provider = providers.get(model.providerId);
|
|
711
|
-
const policyAllowed =
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
713
|
+
const policyAllowed =
|
|
714
|
+
evaluateWorkspaceModelPolicy(input.policy, {
|
|
715
|
+
providerId: model.providerId,
|
|
716
|
+
modelId: model.id,
|
|
717
|
+
}).allowed && modelAllowedByConnections(input.connectionModelRestrictions ?? {}, model.id);
|
|
715
718
|
const credentialReadiness = credentialReadinessFor({
|
|
716
719
|
model,
|
|
717
720
|
provider,
|