@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.
Files changed (39) hide show
  1. package/dist/access/index.d.ts +24 -0
  2. package/dist/canonical-human-identities.js +2 -2
  3. package/dist/{chunk-ZVZJTMSV.js → chunk-OF65T3PM.js} +2 -2
  4. package/dist/{chunk-YGOMUGYS.js → chunk-QO5GVFFO.js} +17 -8
  5. package/dist/{chunk-YGOMUGYS.js.map → chunk-QO5GVFFO.js.map} +1 -1
  6. package/dist/domain/company-brain-governed-writes.d.ts +15 -6
  7. package/dist/domain/company-profile-agent-admin.d.ts +3 -2
  8. package/dist/domain/environments.d.ts +1 -1
  9. package/dist/domain/memory-slack-delivery.d.ts +4 -1
  10. package/dist/domain/personal-connection-delegations.d.ts +1 -0
  11. package/dist/domain/pr-review.d.ts +1 -1
  12. package/dist/domain/scheduled-tasks.d.ts +12 -0
  13. package/dist/domain/sessions.d.ts +14 -1
  14. package/dist/domain/workspace-members.d.ts +8 -1
  15. package/dist/index.js +686 -380
  16. package/dist/index.js.map +1 -1
  17. package/dist/managed-auth-session-sets.d.ts +18 -0
  18. package/dist/managed-auth-session-sets.js +3 -1
  19. package/dist/sandbox/fleet.d.ts +6 -4
  20. package/dist/sandbox/routing.d.ts +7 -2
  21. package/dist/sandbox/runtime-settings.d.ts +17 -1
  22. package/package.json +10 -10
  23. package/src/access/index.ts +140 -5
  24. package/src/application/user-resource-grants.ts +31 -2
  25. package/src/billing/limits.ts +5 -2
  26. package/src/domain/company-brain-governed-writes.ts +29 -13
  27. package/src/domain/company-profile-agent-admin.ts +3 -2
  28. package/src/domain/environments.ts +6 -34
  29. package/src/domain/memory-slack-delivery.ts +30 -0
  30. package/src/domain/personal-connection-delegations.ts +40 -7
  31. package/src/domain/remember.ts +5 -6
  32. package/src/domain/scheduled-tasks.ts +94 -0
  33. package/src/domain/sessions.ts +117 -9
  34. package/src/domain/workspace-members.ts +34 -2
  35. package/src/managed-auth-session-sets.ts +38 -11
  36. package/src/sandbox/fleet.ts +20 -17
  37. package/src/sandbox/routing.ts +18 -4
  38. package/src/sandbox/runtime-settings.ts +32 -0
  39. /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
- projected.some((delegation) => delegation.userDelegation)
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 projected;
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 }
@@ -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
- try {
552
- await notifyActivation({
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
- } catch {
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" ||
@@ -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
- ...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
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
- ...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
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 keep the parent-box behavior.
2273
- ...(machineHomeOs ? { sandboxOs: machineHomeOs } : {}),
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: payload.targetSandboxId
2418
+ seedTargetSandbox: effectiveSeedTarget
2311
2419
  ? {
2312
- sandboxId: payload.targetSandboxId,
2420
+ sandboxId: effectiveSeedTarget.sandboxId,
2313
2421
  settings,
2314
- workingDir: payload.workingDir ?? null,
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, { message: "you cannot remove your own membership" });
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, { message: "cannot delete the account's only workspace" });
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: managedAuthSha256(input.authority),
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: managedAuthSha256(input.transactionSecret),
292
- authSessionId: created.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: managedAuthSha256(input.authority),
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, created.authSessionId);
330
+ await reconcileCreatedManagedAuthSession(input, input.authSessionId);
308
331
  return receipt;
309
332
  }
310
- await input.adapter.revokeSession(created).catch(() => undefined);
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, created.authSessionId);
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: Pick<
323
- Parameters<typeof authenticateAndAdoptManagedAuthSession>[0],
324
- "db" | "adapter" | "authority" | "mode"
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, {