@opengeni/core 2.5.3 → 2.6.3-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/access/index.d.ts +24 -0
- package/dist/canonical-human-identities.js +2 -2
- package/dist/{chunk-ZVZJTMSV.js → chunk-OF65T3PM.js} +2 -2
- package/dist/{chunk-YGOMUGYS.js → chunk-QO5GVFFO.js} +17 -8
- package/dist/{chunk-YGOMUGYS.js.map → chunk-QO5GVFFO.js.map} +1 -1
- package/dist/domain/company-brain-governed-writes.d.ts +15 -6
- package/dist/domain/company-profile-agent-admin.d.ts +3 -2
- package/dist/domain/environments.d.ts +1 -1
- package/dist/domain/memory-slack-delivery.d.ts +4 -1
- package/dist/domain/personal-connection-delegations.d.ts +1 -0
- package/dist/domain/pr-review.d.ts +1 -1
- package/dist/domain/scheduled-tasks.d.ts +12 -0
- package/dist/domain/sessions.d.ts +14 -1
- package/dist/domain/workspace-members.d.ts +8 -1
- package/dist/index.js +686 -380
- package/dist/index.js.map +1 -1
- package/dist/managed-auth-session-sets.d.ts +18 -0
- package/dist/managed-auth-session-sets.js +3 -1
- package/dist/sandbox/fleet.d.ts +6 -4
- package/dist/sandbox/routing.d.ts +7 -2
- package/dist/sandbox/runtime-settings.d.ts +17 -1
- package/package.json +10 -10
- package/src/access/index.ts +140 -5
- package/src/application/user-resource-grants.ts +31 -2
- package/src/billing/limits.ts +5 -2
- package/src/domain/company-brain-governed-writes.ts +29 -13
- package/src/domain/company-profile-agent-admin.ts +3 -2
- package/src/domain/environments.ts +6 -34
- package/src/domain/memory-slack-delivery.ts +30 -0
- package/src/domain/personal-connection-delegations.ts +40 -7
- package/src/domain/remember.ts +5 -6
- package/src/domain/scheduled-tasks.ts +94 -0
- package/src/domain/sessions.ts +117 -9
- package/src/domain/workspace-members.ts +34 -2
- package/src/managed-auth-session-sets.ts +38 -11
- package/src/sandbox/fleet.ts +20 -17
- package/src/sandbox/routing.ts +18 -4
- package/src/sandbox/runtime-settings.ts +32 -0
- /package/dist/{chunk-ZVZJTMSV.js.map → chunk-OF65T3PM.js.map} +0 -0
|
@@ -384,6 +384,7 @@ export function personalConnectionDelegationsFromVisibleConnections(input: {
|
|
|
384
384
|
export function personalConnectionDelegationsFromParent(input: {
|
|
385
385
|
servers: McpServerConfig[];
|
|
386
386
|
parentDelegations: McpPersonalConnectionDelegation[];
|
|
387
|
+
personalGitHubResources?: ResourceRef[];
|
|
387
388
|
targetSessionId?: string;
|
|
388
389
|
rejectActivatedConnections?: boolean;
|
|
389
390
|
}): McpPersonalConnectionDelegation[] {
|
|
@@ -416,19 +417,55 @@ export function personalConnectionDelegationsFromParent(input: {
|
|
|
416
417
|
childEligible(item) &&
|
|
417
418
|
(item.connectionType === "social" ||
|
|
418
419
|
item.connectionType === "atlassian" ||
|
|
420
|
+
item.connectionType === "github_personal" ||
|
|
419
421
|
item.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID),
|
|
420
422
|
)
|
|
421
423
|
.map((item) => ({ ...item })),
|
|
422
424
|
];
|
|
425
|
+
const requestedGitHub = personalGitHubRepositoryResources(input.personalGitHubResources ?? []);
|
|
426
|
+
const inherited = projected.flatMap((delegation) => {
|
|
427
|
+
if (delegation.connectionType !== "github_personal") return [delegation];
|
|
428
|
+
if (requestedGitHub.length === 0) return [];
|
|
429
|
+
const snapshot = delegation.personalGitHubRepositorySelection;
|
|
430
|
+
if (!snapshot) return [];
|
|
431
|
+
const repositories = requestedGitHub.map((resource) => {
|
|
432
|
+
const parent = snapshot.repositories.find(
|
|
433
|
+
(candidate) =>
|
|
434
|
+
candidate.repositoryId === resource.repositoryId &&
|
|
435
|
+
candidate.canonicalUrl === resource.uri &&
|
|
436
|
+
candidate.ref === resource.ref,
|
|
437
|
+
);
|
|
438
|
+
if (
|
|
439
|
+
!parent ||
|
|
440
|
+
resource.credentialBindingId !== snapshot.credentialBindingId ||
|
|
441
|
+
(resource.access === "write" && parent.access !== "write")
|
|
442
|
+
) {
|
|
443
|
+
throw new Error("agent-created personal GitHub repository exceeds parent authority");
|
|
444
|
+
}
|
|
445
|
+
return { ...parent, access: resource.access };
|
|
446
|
+
});
|
|
447
|
+
return [
|
|
448
|
+
{
|
|
449
|
+
...delegation,
|
|
450
|
+
personalGitHubRepositorySelection: { ...snapshot, repositories },
|
|
451
|
+
},
|
|
452
|
+
];
|
|
453
|
+
});
|
|
454
|
+
if (
|
|
455
|
+
requestedGitHub.length > 0 &&
|
|
456
|
+
!inherited.some((delegation) => delegation.connectionType === "github_personal")
|
|
457
|
+
) {
|
|
458
|
+
throw new Error("agent-created personal GitHub repository authority is unavailable");
|
|
459
|
+
}
|
|
423
460
|
if (
|
|
424
461
|
input.rejectActivatedConnections &&
|
|
425
|
-
|
|
462
|
+
inherited.some((delegation) => delegation.userDelegation)
|
|
426
463
|
) {
|
|
427
464
|
throw new Error(
|
|
428
465
|
"scheduled connection authority is not available until task occurrence authority is activated",
|
|
429
466
|
);
|
|
430
467
|
}
|
|
431
|
-
return
|
|
468
|
+
return inherited;
|
|
432
469
|
}
|
|
433
470
|
|
|
434
471
|
/**
|
|
@@ -798,11 +835,6 @@ export async function freezePersonalConnectionDelegations(input: {
|
|
|
798
835
|
"agent-created work inherits connection authority from its exact parent turn",
|
|
799
836
|
);
|
|
800
837
|
}
|
|
801
|
-
if (personalGitHubResources.length > 0) {
|
|
802
|
-
throw new Error(
|
|
803
|
-
"agent-created personal GitHub repository authority is not activated in this delivery phase",
|
|
804
|
-
);
|
|
805
|
-
}
|
|
806
838
|
const inherited = personalConnectionDelegationsFromParent({
|
|
807
839
|
servers,
|
|
808
840
|
parentDelegations: await getSessionTurnPersonalConnectionDelegations(
|
|
@@ -811,6 +843,7 @@ export async function freezePersonalConnectionDelegations(input: {
|
|
|
811
843
|
input.source.sessionId,
|
|
812
844
|
input.source.turnId,
|
|
813
845
|
),
|
|
846
|
+
personalGitHubResources,
|
|
814
847
|
...(input.targetSessionId ? { targetSessionId: input.targetSessionId } : {}),
|
|
815
848
|
...(input.rejectUnselectedActivatedConnections !== undefined
|
|
816
849
|
? { rejectActivatedConnections: input.rejectUnselectedActivatedConnections }
|
package/src/domain/remember.ts
CHANGED
|
@@ -35,6 +35,7 @@ import { createHash } from "node:crypto";
|
|
|
35
35
|
import {
|
|
36
36
|
createCompanyBrainLearningPolicyRouter,
|
|
37
37
|
derivedGovernedLearningOperationId,
|
|
38
|
+
dispatchBestEffortGovernedLearningNotification,
|
|
38
39
|
} from "./company-brain-governed-writes";
|
|
39
40
|
import { publishGovernedLearningEventToSlack } from "./governed-learning-slack-publication";
|
|
40
41
|
|
|
@@ -548,16 +549,14 @@ export function createRememberRouter(options: RememberRouterOptions): {
|
|
|
548
549
|
}
|
|
549
550
|
}
|
|
550
551
|
if (!activation) throw asRememberFailure(lastFailure);
|
|
551
|
-
|
|
552
|
-
|
|
552
|
+
dispatchBestEffortGovernedLearningNotification(() =>
|
|
553
|
+
notifyActivation({
|
|
553
554
|
db: options.db,
|
|
554
555
|
receipt: activation,
|
|
555
556
|
sessionId: attempt.sessionId,
|
|
556
557
|
attemptId: attempt.attemptId,
|
|
557
|
-
})
|
|
558
|
-
|
|
559
|
-
// Notification is best-effort; the durable receipts already exist.
|
|
560
|
-
}
|
|
558
|
+
}),
|
|
559
|
+
);
|
|
561
560
|
return RememberConfirmReceipt.parse({
|
|
562
561
|
status: "activated",
|
|
563
562
|
operationId: request.operationId,
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
createScheduledTask,
|
|
23
23
|
deleteScheduledTask,
|
|
24
24
|
getConnectionMetadata,
|
|
25
|
+
getEnrollment,
|
|
26
|
+
getLiveEnrollmentConnection,
|
|
25
27
|
getKnowledgeSourceForSyncAuthority,
|
|
26
28
|
getNestedAgentDepthDeploymentPolicy,
|
|
27
29
|
getRig,
|
|
@@ -29,6 +31,7 @@ import {
|
|
|
29
31
|
getScheduledTaskIncludingDeletedForUpdate,
|
|
30
32
|
getScheduledTaskPersonalConnectionDelegations,
|
|
31
33
|
getScheduledTaskXaiProviderAccountAuthoritySnapshot,
|
|
34
|
+
getSandbox,
|
|
32
35
|
getSessionTurnXaiProviderAccountAuthoritySnapshot,
|
|
33
36
|
getSession,
|
|
34
37
|
nestedPostgresSqlState,
|
|
@@ -164,6 +167,15 @@ export async function createValidatedScheduledTask(input: {
|
|
|
164
167
|
rigId: input.payload.rigId,
|
|
165
168
|
agentConfig,
|
|
166
169
|
});
|
|
170
|
+
if (!knowledgeAction) {
|
|
171
|
+
await validateScheduledTaskMachineTarget({
|
|
172
|
+
settings: input.settings,
|
|
173
|
+
db: input.db,
|
|
174
|
+
grant: input.grant,
|
|
175
|
+
runMode: input.payload.runMode,
|
|
176
|
+
agentConfig,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
167
179
|
if (!knowledgeAction && input.payload.variableSetId) {
|
|
168
180
|
await validateVariableSetAttachment(
|
|
169
181
|
{ settings: input.settings, db: input.db },
|
|
@@ -399,6 +411,81 @@ export async function validateScheduledTaskTarget(input: {
|
|
|
399
411
|
return session;
|
|
400
412
|
}
|
|
401
413
|
|
|
414
|
+
export async function validateScheduledTaskMachineTarget(input: {
|
|
415
|
+
settings: Settings;
|
|
416
|
+
db: Database;
|
|
417
|
+
grant: AccessGrant;
|
|
418
|
+
runMode: ScheduledTask["runMode"];
|
|
419
|
+
agentConfig: ScheduledTaskAgentConfig;
|
|
420
|
+
requireOnline?: boolean;
|
|
421
|
+
}): Promise<{
|
|
422
|
+
sandboxId: string;
|
|
423
|
+
enrollmentId: string;
|
|
424
|
+
sandboxOs: Session["sandboxOs"];
|
|
425
|
+
} | null> {
|
|
426
|
+
const machineTarget = input.agentConfig.machineTarget;
|
|
427
|
+
if (!machineTarget) {
|
|
428
|
+
if (
|
|
429
|
+
input.runMode !== "existing_session" &&
|
|
430
|
+
(input.agentConfig.sandboxBackend ?? input.settings.sandboxBackend) === "selfhosted"
|
|
431
|
+
) {
|
|
432
|
+
throw new HTTPException(422, {
|
|
433
|
+
message:
|
|
434
|
+
"self-hosted scheduled tasks require a Connected Machine; select a machine before saving",
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
if (input.runMode === "existing_session") {
|
|
440
|
+
throw new HTTPException(422, {
|
|
441
|
+
message: "machineTarget cannot be used with an existing-session target",
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
if (!input.settings.sandboxOwnershipEnabled || !input.settings.sandboxSelfhostedEnabled) {
|
|
445
|
+
throw new HTTPException(422, {
|
|
446
|
+
message: "Connected Machines are not enabled for scheduled tasks in this deployment",
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
const access = {
|
|
450
|
+
accountId: input.grant.accountId,
|
|
451
|
+
workspaceId: input.grant.workspaceId,
|
|
452
|
+
subjectId: input.grant.subjectId,
|
|
453
|
+
};
|
|
454
|
+
const sandbox = await getSandbox(input.db, access, machineTarget.targetSandboxId);
|
|
455
|
+
if (!sandbox || sandbox.kind !== "selfhosted" || !sandbox.enrollmentId) {
|
|
456
|
+
throw new HTTPException(422, {
|
|
457
|
+
message: "the selected Connected Machine is unavailable",
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
if (sandbox.scope === "user") {
|
|
461
|
+
throw new HTTPException(422, {
|
|
462
|
+
message:
|
|
463
|
+
"personal Connected Machines cannot run unattended schedules; select a workspace or organization machine",
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
const enrollment = input.requireOnline
|
|
467
|
+
? await getLiveEnrollmentConnection(input.db, access, sandbox.enrollmentId)
|
|
468
|
+
: await getEnrollment(input.db, access, sandbox.enrollmentId);
|
|
469
|
+
if (!enrollment || enrollment.status !== "active") {
|
|
470
|
+
throw new HTTPException(422, {
|
|
471
|
+
message: input.requireOnline
|
|
472
|
+
? "the selected Connected Machine is offline"
|
|
473
|
+
: "the selected Connected Machine is unavailable",
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
if (input.requireOnline && !enrollment.workspaceRoot) {
|
|
477
|
+
throw new HTTPException(422, {
|
|
478
|
+
message:
|
|
479
|
+
"the selected Connected Machine has not reported a workspace root; reconnect it with a current agent",
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
sandboxId: sandbox.id,
|
|
484
|
+
enrollmentId: sandbox.enrollmentId,
|
|
485
|
+
sandboxOs: enrollment.os,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
402
489
|
export function scheduledTaskForGrant(task: ScheduledTask, grant: AccessGrant): ScheduledTask {
|
|
403
490
|
if (hasPermission(grant.permissions, "sessions:control") || task.targetSessionId === null) {
|
|
404
491
|
return task;
|
|
@@ -793,6 +880,13 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
793
880
|
rigId: input.payload.rigId !== undefined ? input.payload.rigId : input.existing.rigId,
|
|
794
881
|
agentConfig: update.agentConfig ?? input.existing.agentConfig,
|
|
795
882
|
});
|
|
883
|
+
await validateScheduledTaskMachineTarget({
|
|
884
|
+
settings: input.settings,
|
|
885
|
+
db: input.db,
|
|
886
|
+
grant: input.grant,
|
|
887
|
+
runMode: nextRunMode,
|
|
888
|
+
agentConfig: update.agentConfig ?? input.existing.agentConfig,
|
|
889
|
+
});
|
|
796
890
|
if (
|
|
797
891
|
input.payload.targetSessionId !== undefined ||
|
|
798
892
|
input.existing.runMode === "existing_session" ||
|
package/src/domain/sessions.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type Settings,
|
|
11
11
|
} from "@opengeni/config";
|
|
12
12
|
import {
|
|
13
|
+
AUTOMATIC_SESSION_TITLE_FALLBACK,
|
|
13
14
|
CreateSessionRequest,
|
|
14
15
|
DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
|
|
15
16
|
DEFAULT_FIRST_PARTY_MCP_TOOLS,
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
ServiceTurnInitiator,
|
|
21
22
|
ServiceTurnInitiatorContext,
|
|
22
23
|
evaluateWorkspaceModelPolicy,
|
|
24
|
+
normalizeAutomaticSessionTitle,
|
|
23
25
|
resolveWorkspaceSessionToolDefaults,
|
|
24
26
|
stableJson,
|
|
25
27
|
type AccessGrant,
|
|
@@ -126,6 +128,7 @@ import {
|
|
|
126
128
|
SessionAuthorizationDeniedError,
|
|
127
129
|
} from "../session-authorization";
|
|
128
130
|
import { swapActiveSandbox, type FleetContext } from "../sandbox/fleet";
|
|
131
|
+
import { managedSessionGroupBackend } from "../sandbox/runtime-settings";
|
|
129
132
|
import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
|
|
130
133
|
import { validateSubmittedTimelineAnnotations } from "./timeline-annotations";
|
|
131
134
|
import { requireVariableSetEncryption, validateVariableSetAttachment } from "./environments";
|
|
@@ -607,6 +610,44 @@ export type CreateSessionRequestOutcome = CreateSessionOutcome & {
|
|
|
607
610
|
usageRecording: "recorded" | "failed";
|
|
608
611
|
};
|
|
609
612
|
|
|
613
|
+
type AgentChildSessionCreatePresentation = {
|
|
614
|
+
/** Model-authored title candidate from the first-party `session_create` tool.
|
|
615
|
+
* This is deliberately separate from the public REST request contract. */
|
|
616
|
+
automaticTitleCandidate?: string | null;
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const AGENT_CHILD_AUTOMATIC_TITLE_CONTEXT_KEY = "agentChildAutomaticTitle" as const;
|
|
620
|
+
|
|
621
|
+
/** @internal Exported for the keyed-create repair regression. */
|
|
622
|
+
export function freezeAgentChildAutomaticTitleInCreatorContext(
|
|
623
|
+
context: TurnInitiatorContext | undefined,
|
|
624
|
+
title: string | null | undefined,
|
|
625
|
+
): TurnInitiatorContext | undefined {
|
|
626
|
+
return title ? { ...(context ?? {}), [AGENT_CHILD_AUTOMATIC_TITLE_CONTEXT_KEY]: title } : context;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** @internal Keyed repair must use the committed winner, not the retry payload. */
|
|
630
|
+
export function initialAutomaticTitleForSessionStart(
|
|
631
|
+
session: Pick<Session, "createdByContext">,
|
|
632
|
+
requestedTitle: string | null | undefined,
|
|
633
|
+
): string | null {
|
|
634
|
+
const frozenTitle = session.createdByContext[AGENT_CHILD_AUTOMATIC_TITLE_CONTEXT_KEY];
|
|
635
|
+
return typeof frozenTitle === "string" ? frozenTitle : (requestedTitle ?? null);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function automaticTitleForAgentChildCreate(
|
|
639
|
+
presentation: AgentChildSessionCreatePresentation,
|
|
640
|
+
goal: GoalSpec | null | undefined,
|
|
641
|
+
initialMessage: string | null | undefined,
|
|
642
|
+
): string | null {
|
|
643
|
+
for (const candidate of [presentation.automaticTitleCandidate, goal?.text, initialMessage]) {
|
|
644
|
+
if (typeof candidate !== "string") continue;
|
|
645
|
+
const normalized = normalizeAutomaticSessionTitle(candidate);
|
|
646
|
+
if (normalized && normalized !== AUTOMATIC_SESSION_TITLE_FALLBACK) return normalized;
|
|
647
|
+
}
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
|
|
610
651
|
export async function createAndStartSessionWithOutcome(input: {
|
|
611
652
|
requestedSessionId?: string;
|
|
612
653
|
db: Database;
|
|
@@ -652,6 +693,9 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
652
693
|
// resolved workspace-scoped by the caller). Null/omitted ⇒ unfiled (inbox).
|
|
653
694
|
channelId?: string | null;
|
|
654
695
|
goal?: GoalSpec | null;
|
|
696
|
+
/** Trusted sensitive-safe automatic title for an agent-created child. The
|
|
697
|
+
* atomic initializer commits the row mutation and `session.title_set`. */
|
|
698
|
+
initialAutomaticTitle?: string | null;
|
|
655
699
|
// Per-session agent persona/system instructions (org-visible metadata, not a
|
|
656
700
|
// secret). Persisted on the session row and composed system-level AFTER the
|
|
657
701
|
// workspace agentInstructions at turn time; never emitted as a timeline event.
|
|
@@ -737,6 +781,10 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
737
781
|
reasoningEffort: input.reasoningEffort,
|
|
738
782
|
...(input.latencyMode !== undefined ? { latencyMode: input.latencyMode } : {}),
|
|
739
783
|
};
|
|
784
|
+
const frozenCreatedByContext = freezeAgentChildAutomaticTitleInCreatorContext(
|
|
785
|
+
input.createdByContext,
|
|
786
|
+
input.initialAutomaticTitle,
|
|
787
|
+
);
|
|
740
788
|
// Keyed creation is intentionally handled only by the database admission
|
|
741
789
|
// transaction below. Its workspace/key lock replays either the successful
|
|
742
790
|
// session or the committed denial atomically; an application-side lookup
|
|
@@ -755,7 +803,7 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
755
803
|
toolPolicy: input.toolPolicy,
|
|
756
804
|
metadata: sessionMetadata,
|
|
757
805
|
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
758
|
-
...(
|
|
806
|
+
...(frozenCreatedByContext ? { createdByContext: frozenCreatedByContext } : {}),
|
|
759
807
|
createdByActor: input.createdByActor ?? null,
|
|
760
808
|
model: input.model,
|
|
761
809
|
reasoningEffort: input.reasoningEffort,
|
|
@@ -827,7 +875,7 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
827
875
|
toolPolicy: input.toolPolicy,
|
|
828
876
|
metadata: sessionMetadata,
|
|
829
877
|
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
830
|
-
...(
|
|
878
|
+
...(frozenCreatedByContext ? { createdByContext: frozenCreatedByContext } : {}),
|
|
831
879
|
createdByActor: input.createdByActor ?? null,
|
|
832
880
|
model: input.model,
|
|
833
881
|
reasoningEffort: input.reasoningEffort,
|
|
@@ -905,6 +953,7 @@ async function finishStartSession(
|
|
|
905
953
|
sandboxBackend: Settings["sandboxBackend"];
|
|
906
954
|
variableSets?: Array<{ id: string; name: string; scope: VariableSet["scope"] }>;
|
|
907
955
|
goal?: GoalSpec | null;
|
|
956
|
+
initialAutomaticTitle?: string | null;
|
|
908
957
|
sessionMcpServers?: SessionMcpServerMetadata[];
|
|
909
958
|
seedTargetSandbox?: {
|
|
910
959
|
sandboxId: string;
|
|
@@ -1009,6 +1058,10 @@ async function finishStartSession(
|
|
|
1009
1058
|
: {}),
|
|
1010
1059
|
}
|
|
1011
1060
|
: null,
|
|
1061
|
+
initialAutomaticTitle: initialAutomaticTitleForSessionStart(
|
|
1062
|
+
session,
|
|
1063
|
+
input.initialAutomaticTitle,
|
|
1064
|
+
),
|
|
1012
1065
|
consumeNewSessionDraft: input.consumeNewSessionDraft ?? null,
|
|
1013
1066
|
rememberNewSessionSelection: input.rememberNewSessionSelection ?? null,
|
|
1014
1067
|
deferInitialTurn: input.deferInitialTurn === true,
|
|
@@ -1485,6 +1538,7 @@ export async function createSessionForRequestWithOutcome(
|
|
|
1485
1538
|
workspaceId: string,
|
|
1486
1539
|
rawPayload: unknown,
|
|
1487
1540
|
authorization?: AccessGrantAuthorization,
|
|
1541
|
+
agentChildPresentation?: AgentChildSessionCreatePresentation,
|
|
1488
1542
|
): Promise<CreateSessionRequestOutcome> {
|
|
1489
1543
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
1490
1544
|
const payload = CreateSessionRequest.parse(rawPayload);
|
|
@@ -1612,6 +1666,14 @@ export async function createSessionForRequestWithOutcome(
|
|
|
1612
1666
|
});
|
|
1613
1667
|
}
|
|
1614
1668
|
}
|
|
1669
|
+
const initialAutomaticTitle =
|
|
1670
|
+
parentSession && agentChildPresentation
|
|
1671
|
+
? automaticTitleForAgentChildCreate(
|
|
1672
|
+
agentChildPresentation,
|
|
1673
|
+
effectiveGoal,
|
|
1674
|
+
payload.initialMessage,
|
|
1675
|
+
)
|
|
1676
|
+
: null;
|
|
1615
1677
|
const personalResourceSubjectId = creationInitiator.actor
|
|
1616
1678
|
? (await requireLiveAgentAttemptAuthorization(db, grant, creationInitiator.actor.sessionId))
|
|
1617
1679
|
.initiatingHumanSubjectId
|
|
@@ -2038,6 +2100,8 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2038
2100
|
payload.sandbox ?? (payload.targetSandboxId ? "new" : parentSessionId ? "shared" : "new");
|
|
2039
2101
|
let sandboxGroupId: string | null = null;
|
|
2040
2102
|
let inheritedBackend: Session["sandboxBackend"] | undefined;
|
|
2103
|
+
let inheritedSandboxOs: Session["sandboxOs"] | undefined;
|
|
2104
|
+
let inheritedActiveTarget: { sandboxId: string; workingDir: string | null } | null = null;
|
|
2041
2105
|
// ENV-AWARE GROUPING: under the CURRENT mechanics the workspace VariableSet is
|
|
2042
2106
|
// creation-time box state — the box's manifest env is fixed when it is cold-
|
|
2043
2107
|
// created, and the SDK's provided-session guard rejects any manifest-env delta
|
|
@@ -2112,6 +2176,17 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2112
2176
|
} else {
|
|
2113
2177
|
sandboxGroupId = parent.sandboxGroupId;
|
|
2114
2178
|
inheritedBackend = parent.sandboxBackend;
|
|
2179
|
+
inheritedSandboxOs = parent.sandboxOs;
|
|
2180
|
+
// A Connected Machine route is session-local even when two sessions share
|
|
2181
|
+
// one logical sandbox group. Copy the trusted parent's exact active route
|
|
2182
|
+
// so an omitted child sandbox really does share the creator's current box
|
|
2183
|
+
// instead of creating a selfhosted row with no bound agent.
|
|
2184
|
+
inheritedActiveTarget = parent.activeSandboxId
|
|
2185
|
+
? {
|
|
2186
|
+
sandboxId: parent.activeSandboxId,
|
|
2187
|
+
workingDir: parent.workingDir,
|
|
2188
|
+
}
|
|
2189
|
+
: null;
|
|
2115
2190
|
}
|
|
2116
2191
|
} else if (typeof sandboxChoice === "object") {
|
|
2117
2192
|
const member = await getAnySessionInGroup(db, workspaceId, sandboxChoice.groupId);
|
|
@@ -2160,6 +2235,7 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2160
2235
|
}
|
|
2161
2236
|
sandboxGroupId = sandboxChoice.groupId;
|
|
2162
2237
|
inheritedBackend = member.sandboxBackend;
|
|
2238
|
+
inheritedSandboxOs = member.sandboxOs;
|
|
2163
2239
|
}
|
|
2164
2240
|
// else "new": leave sandboxGroupId null → own singleton group (group ≡ id).
|
|
2165
2241
|
// A working dir is only meaningful for a TARGETED machine (it is the chosen
|
|
@@ -2172,6 +2248,19 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2172
2248
|
"workingDir requires targetSandboxId (it is the targeted machine's working directory)",
|
|
2173
2249
|
});
|
|
2174
2250
|
}
|
|
2251
|
+
// A registry-built selfhosted client is deliberately inert: it has no live
|
|
2252
|
+
// agent identity until a concrete Connected Machine is named. Reject an own
|
|
2253
|
+
// targetless home before persisting a session whose first turn can only fail.
|
|
2254
|
+
// Shared children retain their already-bound group through inheritedBackend.
|
|
2255
|
+
if (
|
|
2256
|
+
inheritedBackend === undefined &&
|
|
2257
|
+
!payload.targetSandboxId &&
|
|
2258
|
+
(payload.sandboxBackend ?? settings.sandboxBackend) === "selfhosted"
|
|
2259
|
+
) {
|
|
2260
|
+
throw new HTTPException(422, {
|
|
2261
|
+
message: "selfhosted sessions require targetSandboxId; select an online Connected Machine",
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
2175
2264
|
// Honest-label (Stage-D closure): a session TARGETED at a Connected
|
|
2176
2265
|
// Machine (a selfhosted sandbox) runs machine-primary every turn, so its HOME
|
|
2177
2266
|
// sandbox_backend must read "selfhosted" — not the deployment cloud default —
|
|
@@ -2230,6 +2319,25 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2230
2319
|
}
|
|
2231
2320
|
}
|
|
2232
2321
|
}
|
|
2322
|
+
const effectiveSandboxBackend =
|
|
2323
|
+
inheritedBackend ?? machineHomeBackend ?? payload.sandboxBackend ?? settings.sandboxBackend;
|
|
2324
|
+
const effectiveSandboxOs = inheritedSandboxOs ?? machineHomeOs;
|
|
2325
|
+
const effectiveSeedTarget = payload.targetSandboxId
|
|
2326
|
+
? {
|
|
2327
|
+
sandboxId: payload.targetSandboxId,
|
|
2328
|
+
workingDir: payload.workingDir ?? null,
|
|
2329
|
+
}
|
|
2330
|
+
: inheritedActiveTarget;
|
|
2331
|
+
if (
|
|
2332
|
+
effectiveSandboxBackend === "selfhosted" &&
|
|
2333
|
+
effectiveSeedTarget === null &&
|
|
2334
|
+
managedSessionGroupBackend(settings.sandboxBackend, effectiveSandboxBackend) === null
|
|
2335
|
+
) {
|
|
2336
|
+
throw new HTTPException(422, {
|
|
2337
|
+
message:
|
|
2338
|
+
"self-hosted execution runs on a Connected Machine, but no machine was selected or inherited; connect the parent session to a machine or provide machineTarget",
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2233
2341
|
if (payload.startMode !== "realtime") {
|
|
2234
2342
|
await requireLimit(deps, {
|
|
2235
2343
|
accountId: grant.accountId,
|
|
@@ -2266,11 +2374,10 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2266
2374
|
// machine-targeted create (top-level or own-box child) labels the home
|
|
2267
2375
|
// "selfhosted" (machineHomeBackend), overriding the caller/deployment
|
|
2268
2376
|
// default so the row matches where the session actually runs.
|
|
2269
|
-
sandboxBackend:
|
|
2270
|
-
inheritedBackend ?? machineHomeBackend ?? payload.sandboxBackend ?? settings.sandboxBackend,
|
|
2377
|
+
sandboxBackend: effectiveSandboxBackend,
|
|
2271
2378
|
// Mirror the backend relabel on the OS axis: a machine-targeted own-box
|
|
2272
|
-
// create carries a derived OS; shared spawns
|
|
2273
|
-
...(
|
|
2379
|
+
// create carries a derived OS; shared spawns inherit the exact parent box.
|
|
2380
|
+
...(effectiveSandboxOs ? { sandboxOs: effectiveSandboxOs } : {}),
|
|
2274
2381
|
sandboxGroupId,
|
|
2275
2382
|
metadata: payload.metadata,
|
|
2276
2383
|
...(creationInitiator.initiator ? { createdBy: creationInitiator.initiator } : {}),
|
|
@@ -2286,6 +2393,7 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2286
2393
|
rigVersionId: frozenRigVersionId,
|
|
2287
2394
|
channelId,
|
|
2288
2395
|
goal: effectiveGoal ?? null,
|
|
2396
|
+
initialAutomaticTitle,
|
|
2289
2397
|
// Per-session persona instructions (already trimmed/validated by the
|
|
2290
2398
|
// contracts schema). Persisted on the row; composed system-level at turn
|
|
2291
2399
|
// time. Not surfaced as an event.
|
|
@@ -2307,11 +2415,11 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2307
2415
|
// active-sandbox pointer is seeded race-free inside createAndStartSession
|
|
2308
2416
|
// (after the row exists, before the first turn dispatches). Validation
|
|
2309
2417
|
// (ownership/liveness) lives in swapActiveSandbox; an invalid target 422s.
|
|
2310
|
-
seedTargetSandbox:
|
|
2418
|
+
seedTargetSandbox: effectiveSeedTarget
|
|
2311
2419
|
? {
|
|
2312
|
-
sandboxId:
|
|
2420
|
+
sandboxId: effectiveSeedTarget.sandboxId,
|
|
2313
2421
|
settings,
|
|
2314
|
-
workingDir:
|
|
2422
|
+
workingDir: effectiveSeedTarget.workingDir,
|
|
2315
2423
|
resourceSubjectId: personalResourceSubjectId,
|
|
2316
2424
|
}
|
|
2317
2425
|
: null,
|
|
@@ -44,7 +44,9 @@ export function assertWorkspaceMemberRemovable(input: {
|
|
|
44
44
|
}): void {
|
|
45
45
|
const { members, subjectId, callerSubjectId } = input;
|
|
46
46
|
if (subjectId === callerSubjectId) {
|
|
47
|
-
throw new HTTPException(409, {
|
|
47
|
+
throw new HTTPException(409, {
|
|
48
|
+
message: "you cannot remove your own membership",
|
|
49
|
+
});
|
|
48
50
|
}
|
|
49
51
|
const target = members.find((member) => member.subjectId === subjectId);
|
|
50
52
|
if (!target) {
|
|
@@ -62,6 +64,34 @@ export function assertWorkspaceMemberRemovable(input: {
|
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
/** Keep scoped member edits from orphaning the workspace or changing the caller's own grant. */
|
|
68
|
+
export function assertWorkspaceMemberUpdateAllowed(input: {
|
|
69
|
+
members: WorkspaceMember[];
|
|
70
|
+
subjectId: string;
|
|
71
|
+
callerSubjectId: string;
|
|
72
|
+
nextPermissions: Permission[];
|
|
73
|
+
}): void {
|
|
74
|
+
const { members, subjectId, callerSubjectId, nextPermissions } = input;
|
|
75
|
+
if (subjectId === callerSubjectId) {
|
|
76
|
+
throw new HTTPException(409, {
|
|
77
|
+
message: "you cannot change your own workspace access",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const target = members.find((member) => member.subjectId === subjectId);
|
|
81
|
+
if (!target) {
|
|
82
|
+
throw new HTTPException(404, { message: "member not found" });
|
|
83
|
+
}
|
|
84
|
+
if (
|
|
85
|
+
memberCanAdminister(target) &&
|
|
86
|
+
!memberCanAdminister({ permissions: nextPermissions }) &&
|
|
87
|
+
!members.some((member) => member.subjectId !== subjectId && memberCanAdminister(member))
|
|
88
|
+
) {
|
|
89
|
+
throw new HTTPException(409, {
|
|
90
|
+
message: "the workspace must keep at least one administrator",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
65
95
|
/**
|
|
66
96
|
* Guard the workspace-delete path before any external/DB mutation. Refuses
|
|
67
97
|
* (409) to delete the account's last workspace, and refuses while any session
|
|
@@ -74,7 +104,9 @@ export function assertWorkspaceDeletable(input: {
|
|
|
74
104
|
activeSessionCount: number;
|
|
75
105
|
}): void {
|
|
76
106
|
if (input.workspaceCountForAccount <= 1) {
|
|
77
|
-
throw new HTTPException(409, {
|
|
107
|
+
throw new HTTPException(409, {
|
|
108
|
+
message: "cannot delete the account's only workspace",
|
|
109
|
+
});
|
|
78
110
|
}
|
|
79
111
|
if (input.activeSessionCount > 0) {
|
|
80
112
|
throw new HTTPException(409, {
|
|
@@ -278,25 +278,48 @@ export async function authenticateAndAdoptManagedAuthSession(input: {
|
|
|
278
278
|
credentials: { email: input.email, password: input.password },
|
|
279
279
|
headers: input.isolatedHeaders,
|
|
280
280
|
});
|
|
281
|
+
return await adoptManagedAuthSession({
|
|
282
|
+
...input,
|
|
283
|
+
authorityHash: managedAuthSha256(input.authority),
|
|
284
|
+
transactionSecretHash: managedAuthSha256(input.transactionSecret),
|
|
285
|
+
authSessionId: created.authSessionId,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function adoptManagedAuthSession(input: {
|
|
290
|
+
db: Database;
|
|
291
|
+
adapter: ManagedAuthSessionAdapter;
|
|
292
|
+
authority: string;
|
|
293
|
+
authorityHash: string;
|
|
294
|
+
csrfHash: string;
|
|
295
|
+
operationId: string;
|
|
296
|
+
requestDigest: string;
|
|
297
|
+
expectedGeneration: string;
|
|
298
|
+
expectedActorEpoch: string;
|
|
299
|
+
transactionId: string;
|
|
300
|
+
transactionSecretHash: string;
|
|
301
|
+
authSessionId: string;
|
|
302
|
+
mode: ManagedAuthSessionSetMode;
|
|
303
|
+
}): Promise<{ projection: ManagedAuthDatabaseProjection; returnIntent: string | null }> {
|
|
281
304
|
let completed: { projection: ManagedAuthDatabaseProjection; returnIntent: string | null };
|
|
282
305
|
try {
|
|
283
306
|
completed = await completeManagedAuthLoginTransaction(input.db, {
|
|
284
|
-
authorityHash:
|
|
307
|
+
authorityHash: input.authorityHash,
|
|
285
308
|
csrfHash: input.csrfHash,
|
|
286
309
|
operationId: input.operationId,
|
|
287
310
|
requestDigest: input.requestDigest,
|
|
288
311
|
expectedGeneration: input.expectedGeneration,
|
|
289
312
|
expectedActorEpoch: input.expectedActorEpoch,
|
|
290
313
|
transactionId: input.transactionId,
|
|
291
|
-
transactionSecretHash:
|
|
292
|
-
authSessionId:
|
|
314
|
+
transactionSecretHash: input.transactionSecretHash,
|
|
315
|
+
authSessionId: input.authSessionId,
|
|
293
316
|
mode: input.mode,
|
|
294
317
|
});
|
|
295
318
|
} catch (error) {
|
|
296
319
|
let receipt: Awaited<ReturnType<typeof getManagedAuthSessionSetOperationReceipt>>;
|
|
297
320
|
try {
|
|
298
321
|
receipt = await getManagedAuthSessionSetOperationReceipt(input.db, {
|
|
299
|
-
authorityHash:
|
|
322
|
+
authorityHash: input.authorityHash,
|
|
300
323
|
operationId: input.operationId,
|
|
301
324
|
requestDigest: input.requestDigest,
|
|
302
325
|
});
|
|
@@ -304,14 +327,16 @@ export async function authenticateAndAdoptManagedAuthSession(input: {
|
|
|
304
327
|
throw new ManagedAuthCompletionOutcomeUnknownError({ cause: receiptError });
|
|
305
328
|
}
|
|
306
329
|
if (receipt) {
|
|
307
|
-
await reconcileCreatedManagedAuthSession(input,
|
|
330
|
+
await reconcileCreatedManagedAuthSession(input, input.authSessionId);
|
|
308
331
|
return receipt;
|
|
309
332
|
}
|
|
310
|
-
await input.adapter
|
|
333
|
+
await input.adapter
|
|
334
|
+
.revokeSession({ authSessionId: input.authSessionId })
|
|
335
|
+
.catch(() => undefined);
|
|
311
336
|
throw error;
|
|
312
337
|
}
|
|
313
338
|
try {
|
|
314
|
-
await reconcileCreatedManagedAuthSession(input,
|
|
339
|
+
await reconcileCreatedManagedAuthSession(input, input.authSessionId);
|
|
315
340
|
} catch (error) {
|
|
316
341
|
throw new ManagedAuthCompletionOutcomeUnknownError({ cause: error });
|
|
317
342
|
}
|
|
@@ -319,10 +344,12 @@ export async function authenticateAndAdoptManagedAuthSession(input: {
|
|
|
319
344
|
}
|
|
320
345
|
|
|
321
346
|
async function reconcileCreatedManagedAuthSession(
|
|
322
|
-
input:
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
347
|
+
input: {
|
|
348
|
+
db: Database;
|
|
349
|
+
adapter: ManagedAuthSessionAdapter;
|
|
350
|
+
authority: string;
|
|
351
|
+
mode: ManagedAuthSessionSetMode;
|
|
352
|
+
},
|
|
326
353
|
authSessionId: string,
|
|
327
354
|
): Promise<void> {
|
|
328
355
|
const snapshot = await getManagedAuthSessionSetSnapshot(input.db, {
|