@opengeni/core 0.10.0 → 0.11.2
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 +86 -3
- package/dist/index.js +485 -93
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/application/new-session-drafts.ts +126 -0
- package/src/domain/scheduled-tasks.ts +23 -1
- package/src/domain/sessions.ts +115 -50
- package/src/index.ts +1 -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.2",
|
|
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.
|
|
39
|
-
"@opengeni/contracts": "^0.
|
|
40
|
-
"@opengeni/db": "^0.
|
|
41
|
-
"@opengeni/documents": "^0.2.
|
|
42
|
-
"@opengeni/events": "^0.3.
|
|
38
|
+
"@opengeni/config": "^0.7.1",
|
|
39
|
+
"@opengeni/contracts": "^0.19.0",
|
|
40
|
+
"@opengeni/db": "^0.12.1",
|
|
41
|
+
"@opengeni/documents": "^0.2.31",
|
|
42
|
+
"@opengeni/events": "^0.3.22",
|
|
43
43
|
"@opengeni/observability": "^0.3.0",
|
|
44
|
-
"@opengeni/runtime": "^0.13.
|
|
45
|
-
"@opengeni/storage": "^0.2.
|
|
44
|
+
"@opengeni/runtime": "^0.13.4",
|
|
45
|
+
"@opengeni/storage": "^0.2.25",
|
|
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
|
+
}
|
|
@@ -9,14 +9,16 @@ 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";
|
|
@@ -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;
|
|
@@ -356,6 +360,24 @@ async function validateScheduledTaskAgentConfig(input: {
|
|
|
356
360
|
throw new HTTPException(503, { message: "object storage is not configured" });
|
|
357
361
|
}
|
|
358
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
|
+
}
|
|
359
381
|
return {
|
|
360
382
|
...input.payload.agentConfig,
|
|
361
383
|
...(model === undefined || model === null ? {} : { model }),
|
package/src/domain/sessions.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
CreateSessionRequest,
|
|
11
11
|
DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
|
|
12
|
+
SessionSpawnDenial,
|
|
12
13
|
ServiceTurnInitiator,
|
|
13
14
|
ServiceTurnInitiatorContext,
|
|
14
15
|
evaluateWorkspaceModelPolicy,
|
|
@@ -36,7 +37,7 @@ import {
|
|
|
36
37
|
} from "@opengeni/contracts";
|
|
37
38
|
import {
|
|
38
39
|
createSession,
|
|
39
|
-
|
|
40
|
+
createSessionWithIdempotencyKeyResult,
|
|
40
41
|
encryptVariableSetValue,
|
|
41
42
|
getAnySessionInGroup,
|
|
42
43
|
getEnrollment,
|
|
@@ -47,7 +48,7 @@ import {
|
|
|
47
48
|
getSandbox,
|
|
48
49
|
getSession,
|
|
49
50
|
SessionIdConflictError,
|
|
50
|
-
|
|
51
|
+
getSessionSpawnDenialByIdempotencyKey,
|
|
51
52
|
getSessionEvent,
|
|
52
53
|
getWorkspaceControlEvent,
|
|
53
54
|
getSessionLineage,
|
|
@@ -66,6 +67,7 @@ import {
|
|
|
66
67
|
type UpdateSessionMcpServerCredentialsInput,
|
|
67
68
|
QueueCommandConflictError,
|
|
68
69
|
AgentCommandAuthorityError,
|
|
70
|
+
SessionSpawnDeniedDbError,
|
|
69
71
|
SessionControlConflictError,
|
|
70
72
|
type SessionCommandActor,
|
|
71
73
|
} from "@opengeni/db";
|
|
@@ -105,6 +107,34 @@ const maxSessionMcpCredentialHeaderValueLength = 4096;
|
|
|
105
107
|
// RFC 9110 field-name token characters.
|
|
106
108
|
const sessionMcpCredentialHeaderName = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
107
109
|
|
|
110
|
+
/** Transport-neutral typed denial raised only after its audit row committed. */
|
|
111
|
+
export class SessionSpawnDeniedError extends Error {
|
|
112
|
+
readonly denial: SessionSpawnDenial;
|
|
113
|
+
|
|
114
|
+
constructor(denial: SessionSpawnDenial) {
|
|
115
|
+
super(sessionSpawnDeniedMessage(denial));
|
|
116
|
+
this.name = "SessionSpawnDeniedError";
|
|
117
|
+
this.denial = denial;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function sessionSpawnDeniedMessage(denial: SessionSpawnDenial): string {
|
|
122
|
+
if (denial.code === "nested_agent_depth_override_forbidden") {
|
|
123
|
+
return `requested nested-agent depth limit ${denial.requestedMaxNestedAgentDepthOverride ?? "unknown"} exceeds inherited limit ${denial.effectiveMaxNestedAgentDepth}; workspace:admin is required to increase it`;
|
|
124
|
+
}
|
|
125
|
+
return `nested-agent depth ${denial.attemptedDepth} exceeds effective limit ${denial.effectiveMaxNestedAgentDepth} (current parent depth ${denial.currentDepth})`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function sessionSpawnDenialEnvelope(error: SessionSpawnDeniedError) {
|
|
129
|
+
return {
|
|
130
|
+
error: {
|
|
131
|
+
code: error.denial.code,
|
|
132
|
+
message: error.message,
|
|
133
|
+
details: { denial: error.denial },
|
|
134
|
+
},
|
|
135
|
+
} as const;
|
|
136
|
+
}
|
|
137
|
+
|
|
108
138
|
type ValidatedSessionMcpServers = {
|
|
109
139
|
runtimeServers: Settings["mcpServers"];
|
|
110
140
|
dbServers: CreateSessionMcpServerInput[];
|
|
@@ -513,36 +543,27 @@ export async function createAndStartSession(input: {
|
|
|
513
543
|
// `workingDir` (optional) is the path/cwd base the chosen machine runs under,
|
|
514
544
|
// seeded alongside the pointer through the epoch-fenced CAS.
|
|
515
545
|
seedTargetSandbox?: { sandboxId: string; settings: Settings; workingDir?: string | null } | null;
|
|
546
|
+
// Exact actor-private pre-session draft represented by this create. The
|
|
547
|
+
// initializer consumes it only after the first durable runnable unit commits.
|
|
548
|
+
consumeNewSessionDraft?: { subjectId: string; expectedRevision: number } | null;
|
|
549
|
+
// A child may lower its inherited nested-agent depth limit freely; increases
|
|
550
|
+
// are authorized by the caller's workspace:admin grant and checked again by
|
|
551
|
+
// the database admission transaction.
|
|
552
|
+
maxNestedAgentDepthOverride?: number | null;
|
|
553
|
+
allowNestedAgentDepthIncrease?: boolean;
|
|
554
|
+
subjectId?: string | null;
|
|
516
555
|
}): Promise<CreateSessionResponse> {
|
|
517
556
|
const sessionMetadata = {
|
|
518
557
|
...input.metadata,
|
|
519
558
|
model: input.model,
|
|
520
559
|
reasoningEffort: input.reasoningEffort,
|
|
521
560
|
};
|
|
522
|
-
//
|
|
523
|
-
//
|
|
561
|
+
// Keyed creation is intentionally handled only by the database admission
|
|
562
|
+
// transaction below. Its workspace/key lock replays either the successful
|
|
563
|
+
// session or the committed denial atomically; an application-side lookup
|
|
564
|
+
// cannot serialize those two source tables against an older writer.
|
|
524
565
|
if (input.createIdempotencyKey) {
|
|
525
|
-
const
|
|
526
|
-
input.db,
|
|
527
|
-
input.workspaceId,
|
|
528
|
-
input.createIdempotencyKey,
|
|
529
|
-
);
|
|
530
|
-
if (existing) {
|
|
531
|
-
if (input.requestedSessionId && existing.id !== input.requestedSessionId) {
|
|
532
|
-
throw new SessionIdConflictError(input.requestedSessionId);
|
|
533
|
-
}
|
|
534
|
-
return await finishStartSession(
|
|
535
|
-
existing.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
|
|
536
|
-
existing,
|
|
537
|
-
);
|
|
538
|
-
}
|
|
539
|
-
// No prior session: insert under the key, racing concurrent creates. The
|
|
540
|
-
// partial unique index lets exactly one insert win; a loser gets back the
|
|
541
|
-
// winner's row with created=false. Both callers may enter the idempotent
|
|
542
|
-
// initializer; exactly one creates the first events/turn. Each retry
|
|
543
|
-
// advances the coalesced wake revision so an in-flight stale delivery can
|
|
544
|
-
// never acknowledge work committed by the other caller.
|
|
545
|
-
const { session: keyed, created } = await createSessionWithIdempotencyKey(input.db, {
|
|
566
|
+
const keyedResult = await createSessionWithIdempotencyKeyResult(input.db, {
|
|
546
567
|
...(input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {}),
|
|
547
568
|
accountId: input.accountId,
|
|
548
569
|
workspaceId: input.workspaceId,
|
|
@@ -567,7 +588,14 @@ export async function createAndStartSession(input: {
|
|
|
567
588
|
sandboxGroupId: input.sandboxGroupId ?? null,
|
|
568
589
|
...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
|
|
569
590
|
mcpServers: input.mcpServers ?? [],
|
|
591
|
+
maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
|
|
592
|
+
allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
|
|
593
|
+
subjectId: input.subjectId ?? null,
|
|
570
594
|
});
|
|
595
|
+
if (keyedResult.denied) {
|
|
596
|
+
throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(keyedResult.denial));
|
|
597
|
+
}
|
|
598
|
+
const { session: keyed, created } = keyedResult;
|
|
571
599
|
if (!created) {
|
|
572
600
|
return await finishStartSession(
|
|
573
601
|
keyed.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
|
|
@@ -576,31 +604,42 @@ export async function createAndStartSession(input: {
|
|
|
576
604
|
}
|
|
577
605
|
return await finishStartSession(input, keyed);
|
|
578
606
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
607
|
+
let session: Session;
|
|
608
|
+
try {
|
|
609
|
+
session = await createSession(input.db, {
|
|
610
|
+
...(input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {}),
|
|
611
|
+
accountId: input.accountId,
|
|
612
|
+
workspaceId: input.workspaceId,
|
|
613
|
+
initialMessage: input.initialMessage,
|
|
614
|
+
initialTurnInstructions: input.turnInstructions ?? null,
|
|
615
|
+
resources: input.resources,
|
|
616
|
+
tools: input.tools,
|
|
617
|
+
...(input.toolPolicy ? { toolPolicy: input.toolPolicy } : {}),
|
|
618
|
+
metadata: sessionMetadata,
|
|
619
|
+
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
620
|
+
...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
|
|
621
|
+
createdByActor: input.createdByActor ?? null,
|
|
622
|
+
model: input.model,
|
|
623
|
+
sandboxBackend: input.sandboxBackend,
|
|
624
|
+
variableSetId: input.variableSet?.id ?? null,
|
|
625
|
+
rigId: input.rigId ?? null,
|
|
626
|
+
rigVersionId: input.rigVersionId ?? null,
|
|
627
|
+
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
628
|
+
instructions: input.instructions ?? null,
|
|
629
|
+
parentSessionId: input.parentSessionId ?? null,
|
|
630
|
+
sandboxGroupId: input.sandboxGroupId ?? null,
|
|
631
|
+
...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
|
|
632
|
+
mcpServers: input.mcpServers ?? [],
|
|
633
|
+
maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
|
|
634
|
+
allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
|
|
635
|
+
subjectId: input.subjectId ?? null,
|
|
636
|
+
});
|
|
637
|
+
} catch (error) {
|
|
638
|
+
if (error instanceof SessionSpawnDeniedDbError) {
|
|
639
|
+
throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(error.denial));
|
|
640
|
+
}
|
|
641
|
+
throw error;
|
|
642
|
+
}
|
|
604
643
|
return await finishStartSession(input, session);
|
|
605
644
|
}
|
|
606
645
|
|
|
@@ -633,6 +672,7 @@ async function finishStartSession(
|
|
|
633
672
|
settings: Settings;
|
|
634
673
|
workingDir?: string | null;
|
|
635
674
|
} | null;
|
|
675
|
+
consumeNewSessionDraft?: { subjectId: string; expectedRevision: number } | null;
|
|
636
676
|
},
|
|
637
677
|
session: Session,
|
|
638
678
|
): Promise<CreateSessionResponse> {
|
|
@@ -693,6 +733,7 @@ async function finishStartSession(
|
|
|
693
733
|
: {}),
|
|
694
734
|
}
|
|
695
735
|
: null,
|
|
736
|
+
consumeNewSessionDraft: input.consumeNewSessionDraft ?? null,
|
|
696
737
|
});
|
|
697
738
|
await publishDurableSessionEvents(input.bus, session.workspaceId, session.id, started.events);
|
|
698
739
|
if (started.workflowWakeRevision !== null) {
|
|
@@ -980,6 +1021,20 @@ export async function createSessionForRequest(
|
|
|
980
1021
|
): Promise<Session> {
|
|
981
1022
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
982
1023
|
const payload = CreateSessionRequest.parse(rawPayload);
|
|
1024
|
+
// A committed keyed denial is the idempotent outcome even if mutable
|
|
1025
|
+
// resources, policy, authorization, or budget have changed since the first
|
|
1026
|
+
// attempt. Replay it before any of those checks, just as a keyed successful
|
|
1027
|
+
// session is returned rather than recreated later in createAndStartSession.
|
|
1028
|
+
if (payload.idempotencyKey) {
|
|
1029
|
+
const denial = await getSessionSpawnDenialByIdempotencyKey(
|
|
1030
|
+
db,
|
|
1031
|
+
workspaceId,
|
|
1032
|
+
payload.idempotencyKey,
|
|
1033
|
+
);
|
|
1034
|
+
if (denial) {
|
|
1035
|
+
throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(denial));
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
983
1038
|
// Parent linkage and execution-context inheritance come ONLY from the
|
|
984
1039
|
// worker-signed sessionId claim. A caller cannot nominate a parent in the
|
|
985
1040
|
// payload, so inheriting an existing repository/tool/credential snapshot does
|
|
@@ -1469,6 +1524,9 @@ export async function createSessionForRequest(
|
|
|
1469
1524
|
sessionMcpServers: sessionMcpServers.metadata,
|
|
1470
1525
|
parentSessionId,
|
|
1471
1526
|
createIdempotencyKey: payload.idempotencyKey ?? null,
|
|
1527
|
+
maxNestedAgentDepthOverride: payload.maxNestedAgentDepth ?? null,
|
|
1528
|
+
allowNestedAgentDepthIncrease: hasPermission(grant.permissions, "workspace:admin"),
|
|
1529
|
+
subjectId: grant.subjectId,
|
|
1472
1530
|
// Create-time machine targeting (A-2a): when a target sandbox is named, the
|
|
1473
1531
|
// active-sandbox pointer is seeded race-free inside createAndStartSession
|
|
1474
1532
|
// (after the row exists, before the first turn dispatches). Validation
|
|
@@ -1476,6 +1534,13 @@ export async function createSessionForRequest(
|
|
|
1476
1534
|
seedTargetSandbox: payload.targetSandboxId
|
|
1477
1535
|
? { sandboxId: payload.targetSandboxId, settings, workingDir: payload.workingDir ?? null }
|
|
1478
1536
|
: null,
|
|
1537
|
+
consumeNewSessionDraft:
|
|
1538
|
+
payload.expectedNewSessionDraftRevision !== undefined
|
|
1539
|
+
? {
|
|
1540
|
+
subjectId: grant.subjectId,
|
|
1541
|
+
expectedRevision: payload.expectedNewSessionDraftRevision,
|
|
1542
|
+
}
|
|
1543
|
+
: null,
|
|
1479
1544
|
});
|
|
1480
1545
|
} catch (error) {
|
|
1481
1546
|
if (error instanceof AgentCommandAuthorityError) {
|
package/src/index.ts
CHANGED
|
@@ -61,4 +61,5 @@ export * from "./domain/session-tool-policy";
|
|
|
61
61
|
export * from "./domain/scheduled-tasks";
|
|
62
62
|
export * from "./domain/sessions";
|
|
63
63
|
export * from "./domain/workspace-members";
|
|
64
|
+
export * from "./application/new-session-drafts";
|
|
64
65
|
export * from "./application/session-commands";
|