@opengeni/core 0.28.1 → 2.1.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.
Files changed (99) hide show
  1. package/dist/access/index.d.ts +37 -0
  2. package/dist/application/new-session-drafts.d.ts +8 -1
  3. package/dist/application/session-commands.d.ts +18 -11
  4. package/dist/application/session-tenancy.d.ts +18 -0
  5. package/dist/application/user-resource-grants.d.ts +20 -0
  6. package/dist/{chunk-6WKPWE5S.js → chunk-H7X52RI2.js} +27 -18
  7. package/dist/chunk-H7X52RI2.js.map +1 -0
  8. package/dist/{chunk-JJU6I5XD.js → chunk-VCZFFEPB.js} +21 -9
  9. package/dist/chunk-VCZFFEPB.js.map +1 -0
  10. package/dist/{chunk-NYPMQ7NO.js → chunk-ZYCNLYGH.js} +29 -27
  11. package/dist/chunk-ZYCNLYGH.js.map +1 -0
  12. package/dist/dependencies.d.ts +12 -1
  13. package/dist/domain/automations.d.ts +51 -0
  14. package/dist/domain/company-brain-governed-writes.d.ts +70 -0
  15. package/dist/domain/company-profile-agent-admin.d.ts +24 -0
  16. package/dist/domain/connection-authority.d.ts +97 -0
  17. package/dist/domain/connection-authority.js +169 -0
  18. package/dist/domain/connection-authority.js.map +1 -0
  19. package/dist/domain/editable-artifacts/service.d.ts +1 -2
  20. package/dist/domain/environments.d.ts +1 -1
  21. package/dist/domain/github-repository-bindings.d.ts +117 -0
  22. package/dist/domain/governed-learning-activation.d.ts +16 -0
  23. package/dist/domain/governed-learning-evaluator.d.ts +6 -0
  24. package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
  25. package/dist/domain/memory-slack-delivery.d.ts +1 -4
  26. package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
  27. package/dist/domain/packs.d.ts +5 -1
  28. package/dist/domain/personal-connection-delegations.d.ts +44 -2
  29. package/dist/domain/pr-review.d.ts +149 -0
  30. package/dist/domain/remember.d.ts +69 -0
  31. package/dist/domain/resources.d.ts +23 -2
  32. package/dist/domain/scheduled-tasks.d.ts +43 -2
  33. package/dist/domain/session-tool-policy.d.ts +1 -1
  34. package/dist/domain/sessions.d.ts +43 -9
  35. package/dist/editable-artifact-live/application.d.ts +10 -2
  36. package/dist/editable-artifact-live/ports.d.ts +0 -1
  37. package/dist/editable-artifact-live/server.d.ts +0 -1
  38. package/dist/editable-artifact-live/ticket.d.ts +0 -1
  39. package/dist/editable-artifact-live/types.d.ts +5 -2
  40. package/dist/editable-artifact-live.js +2 -2
  41. package/dist/editable-artifacts.js +2 -4
  42. package/dist/index.d.ts +13 -1
  43. package/dist/index.js +4973 -1243
  44. package/dist/index.js.map +1 -1
  45. package/dist/rigs/index.d.ts +22 -5
  46. package/dist/sandbox/fleet.d.ts +34 -6
  47. package/dist/sandbox/routing.d.ts +15 -1
  48. package/dist/sandbox/runtime-settings.d.ts +39 -0
  49. package/package.json +16 -11
  50. package/src/access/index.ts +151 -31
  51. package/src/application/new-session-drafts.ts +32 -5
  52. package/src/application/session-commands.ts +305 -71
  53. package/src/application/session-tenancy.ts +321 -0
  54. package/src/application/user-resource-grants.ts +140 -0
  55. package/src/billing/limits.ts +25 -15
  56. package/src/dependencies.ts +12 -1
  57. package/src/domain/automations.ts +196 -0
  58. package/src/domain/capabilities.ts +7 -1
  59. package/src/domain/company-brain-governed-writes.ts +330 -0
  60. package/src/domain/company-profile-agent-admin.ts +55 -0
  61. package/src/domain/connection-authority.ts +329 -0
  62. package/src/domain/editable-artifacts/agent-application.ts +17 -9
  63. package/src/domain/editable-artifacts/service.ts +12 -1
  64. package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
  65. package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
  66. package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
  67. package/src/domain/environments.ts +16 -3
  68. package/src/domain/github-repository-bindings.ts +295 -0
  69. package/src/domain/governed-learning-activation.ts +44 -0
  70. package/src/domain/governed-learning-evaluator.ts +49 -0
  71. package/src/domain/governed-learning-slack-publication.ts +157 -0
  72. package/src/domain/insights.ts +9 -0
  73. package/src/domain/memory-slack-delivery.ts +0 -30
  74. package/src/domain/organization-membership-lifecycle.ts +9 -0
  75. package/src/domain/packs.ts +106 -3
  76. package/src/domain/personal-connection-delegations.ts +597 -42
  77. package/src/domain/pr-review.ts +671 -0
  78. package/src/domain/remember.ts +549 -0
  79. package/src/domain/resources.ts +144 -6
  80. package/src/domain/scheduled-tasks.ts +504 -77
  81. package/src/domain/session-tool-policy.ts +12 -9
  82. package/src/domain/sessions.ts +629 -176
  83. package/src/editable-artifact-live/application.ts +21 -3
  84. package/src/editable-artifact-live/ports.ts +0 -1
  85. package/src/editable-artifact-live/server.ts +13 -7
  86. package/src/editable-artifact-live/ticket.ts +1 -5
  87. package/src/editable-artifact-live/types.ts +6 -2
  88. package/src/editable-artifact-live/wire.ts +0 -10
  89. package/src/index.ts +13 -1
  90. package/src/rigs/index.ts +165 -48
  91. package/src/sandbox/fleet.ts +376 -30
  92. package/src/sandbox/routing.ts +91 -29
  93. package/src/sandbox/runtime-settings.ts +274 -0
  94. package/src/session-authorization.ts +17 -49
  95. package/dist/chunk-6WKPWE5S.js.map +0 -1
  96. package/dist/chunk-JJU6I5XD.js.map +0 -1
  97. package/dist/chunk-NYPMQ7NO.js.map +0 -1
  98. package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
  99. package/src/domain/durable-learning-slack-publication.ts +0 -202
@@ -1,10 +1,18 @@
1
- import type { AccessGrant, CreateRigRequest, RigDefinitionEditPayload, ProposeRigChangeRequest, Rig, RigChange, RigVersion, UpdateRigRequest } from "@opengeni/contracts";
1
+ import type { AccessGrant, CreateRigRequest, RigDefinitionEditPayload, ProposeRigChangeRequest, Rig, RigChange, ResourceAuthorityScope, RigVersion, UpdateRigRequest } from "@opengeni/contracts";
2
2
  import { type Database } from "@opengeni/db";
3
3
  export * from "./provider-images.js";
4
4
  export declare const MAX_RIGS_PER_WORKSPACE = 50;
5
5
  export declare const MAX_CHECKS_PER_RIG = 100;
6
6
  export declare const MAX_CREDENTIAL_HOOKS_PER_RIG = 50;
7
7
  export declare const MAX_DEFAULT_VARIABLE_SETS_PER_RIG = 25;
8
+ export declare const RIG_DEFAULT_VARIABLE_SET_LOAD_CONCURRENCY = 4;
9
+ type VariableSetEnvironment = {
10
+ values: Record<string, string>;
11
+ } | null;
12
+ /** Load complete rig defaults concurrently while preserving listed precedence. */
13
+ export declare function loadRigDefaultVariableSetEnvironment(variableSetIds: readonly string[], load: (variableSetId: string) => Promise<VariableSetEnvironment>): Promise<Record<string, string>>;
14
+ /** Rig defaults layer below the session's explicitly selected variable set. */
15
+ export declare function mergeRigDefaultVariableSetEnvironment(rigDefaultValues: Record<string, string>, sessionValues: Record<string, string>): Record<string, string>;
8
16
  export type RigServices = {
9
17
  db: Database;
10
18
  };
@@ -16,11 +24,20 @@ export declare function recordRigAuditEvent(db: Database, input: {
16
24
  metadata?: Record<string, unknown>;
17
25
  }): Promise<void>;
18
26
  export declare function rigActorForGrant(grant: AccessGrant): string;
19
- export declare function requireRigForApi(db: Database, workspaceId: string, rigId: string): Promise<Rig>;
27
+ export declare function requireRigForApi(db: Database, grant: AccessGrant, rigId: string): Promise<Rig>;
20
28
  export declare function requireRigChangeForApi(db: Database, workspaceId: string, rigId: string, changeId: string): Promise<RigChange>;
21
- export declare function createRigForApi(deps: RigServices, grant: AccessGrant, payload: CreateRigRequest): Promise<Rig>;
22
- export declare function updateRigForApi(deps: RigServices, grant: AccessGrant, rig: Rig, payload: UpdateRigRequest): Promise<Rig>;
23
- export declare function deleteRigForApi(deps: RigServices, grant: AccessGrant, rig: Rig): Promise<void>;
29
+ /** A resource may depend only on Variable Sets whose audience is no narrower
30
+ * than its own. User-owned rigs may use anything visible to their owner. */
31
+ export declare function variableSetScopeAllowedForRig(rigScope: ResourceAuthorityScope, variableSetScope: ResourceAuthorityScope): boolean;
32
+ export declare function createRigForApi(deps: RigServices, grant: AccessGrant, payload: CreateRigRequest, options?: {
33
+ allowOrganization?: boolean;
34
+ }): Promise<Rig>;
35
+ export declare function updateRigForApi(deps: RigServices, grant: AccessGrant, rig: Rig, payload: UpdateRigRequest, options?: {
36
+ allowOrganization?: boolean;
37
+ }): Promise<Rig>;
38
+ export declare function deleteRigForApi(deps: RigServices, grant: AccessGrant, rig: Rig, options?: {
39
+ allowOrganization?: boolean;
40
+ }): Promise<void>;
24
41
  export declare function proposeRigChangeForApi(deps: RigServices, grant: AccessGrant, rig: Rig, request: ProposeRigChangeRequest, options?: {
25
42
  proposedBy?: string;
26
43
  }): Promise<RigChange>;
@@ -1,7 +1,7 @@
1
1
  import type { Settings } from "@opengeni/config";
2
2
  import { type Database, type SandboxRecord } from "@opengeni/db";
3
3
  import type { EventBus } from "@opengeni/events";
4
- import { type BackendUnresolvableCode, type ControlRpc, type SelfhostedRelayConfig, type SelfhostedOpStreamDeps } from "@opengeni/runtime/sandbox";
4
+ import { type BackendUnresolvableCode, type ControlRpc, type SelfhostedRelayConfig, type SelfhostedOpStreamDeps, type SelfhostedOperationAdmission, type SelfhostedOperationResourcePolicy } from "@opengeni/runtime/sandbox";
5
5
  export type FleetServices = {
6
6
  db: Database;
7
7
  settings: Settings;
@@ -18,6 +18,13 @@ export type FleetReadinessHold = {
18
18
  export type FleetContext = {
19
19
  accountId: string;
20
20
  workspaceId: string;
21
+ subjectId?: string;
22
+ attemptAuthority?: {
23
+ turnId: string;
24
+ attemptId: string;
25
+ executionGeneration: number;
26
+ initiatingHumanSubjectId: string;
27
+ };
21
28
  /** The calling session (the pointer the attach/swap mutates + whose group box
22
29
  * is the default fleet member). */
23
30
  sessionId: string;
@@ -42,6 +49,8 @@ export declare function buildFleetContextForSession(deps: {
42
49
  accountId: string;
43
50
  workspaceId: string;
44
51
  sessionId: string;
52
+ subjectId?: string;
53
+ attemptAuthority?: FleetContext["attemptAuthority"];
45
54
  }): Promise<FleetContext>;
46
55
  /** The dominant liveness of a fleet member, surfaced to the dock + the agent. */
47
56
  export type FleetLiveness = "online" | "reconnecting" | "offline";
@@ -50,14 +59,15 @@ export type FleetOperationAvailability = "ready" | "wakeable" | "recovering" | "
50
59
  * A fleet member as the agent + the dock see it (the M8b/M9 UI seam — the
51
60
  * `sandboxes_list` response entry the dock renders). STABLE shape: the dock keys
52
61
  * on `id`, renders `name`/`kind`/`liveness`, and marks `active`. The session's own
53
- * Modal group box is a synthetic entry with `id: groupId`, `kind: "modal"`, and a
54
- * null `enrollmentId`; an enrolled machine carries its sandbox + enrollment ids.
62
+ * managed group box is a synthetic entry with `id: groupId`, its actual
63
+ * provisioned backend kind, and a null `enrollmentId`; an enrolled machine
64
+ * carries its sandbox + enrollment ids.
55
65
  */
56
66
  export type FleetSandboxEntry = {
57
67
  /** The sandbox id used as the attach/swap/run_on `target`. For the session's
58
68
  * own group box this is the group id (a null active pointer == this box). */
59
69
  id: string;
60
- kind: "modal" | "selfhosted";
70
+ kind: "modal" | "selfhosted" | "opensandbox";
61
71
  name: string;
62
72
  liveness: FleetLiveness;
63
73
  /** True for the session's currently-active sandbox (the routing target). */
@@ -106,7 +116,7 @@ export type FleetSwapResult = {
106
116
  activeSandboxId: string | null;
107
117
  activeEpoch: number;
108
118
  reason?: string;
109
- code?: BackendUnresolvableCode | "concurrent_swap" | "recovery_in_progress" | "recovery_degraded" | "recovery_unrecoverable";
119
+ code?: BackendUnresolvableCode | "invalid_working_directory" | "concurrent_swap" | "recovery_in_progress" | "recovery_degraded" | "recovery_unrecoverable";
110
120
  };
111
121
  /**
112
122
  * List the fleet: the session's own group box when it has one (a synthetic
@@ -157,6 +167,9 @@ export type RunOnResult = {
157
167
  export type RunOnSelfhostedMachine = {
158
168
  workspaceId: string;
159
169
  agentId: string;
170
+ /** Exact live daemon pinned for this operation. */
171
+ connectionInstanceId: string;
172
+ workspaceRoot: string;
160
173
  controlRpc: ControlRpc;
161
174
  relay: SelfhostedRelayConfig;
162
175
  /** Short request/reply deadline for read/write and other control operations. */
@@ -165,6 +178,21 @@ export type RunOnSelfhostedMachine = {
165
178
  execTimeoutMs: number;
166
179
  /** Streaming transport required when execTimeoutMs is 0 (unbounded). */
167
180
  opStream?: SelfhostedOpStreamDeps;
181
+ operationResourcePolicy?: SelfhostedOperationResourcePolicy;
182
+ operationResourcePolicySupported?: boolean;
183
+ operationCpuQuotaSupported?: boolean;
184
+ /** Last-boundary authority/connection resolver invoked before every physical
185
+ * provider operation. */
186
+ resolveOperationAdmission?: () => Promise<SelfhostedOperationAdmission | null>;
187
+ /**
188
+ * Exact-attempt values for this one child process. Never persisted on the
189
+ * enrollment or machine. The caller may supply these only after authenticating
190
+ * the active attempt that owns the one-off operation.
191
+ */
192
+ transientExecEnvironment?: Readonly<Record<string, string>>;
193
+ };
194
+ export type RunOnOptions = {
195
+ transientExecEnvironment?: Readonly<Record<string, string>>;
168
196
  };
169
197
  /**
170
198
  * Execute the one-off machine operation once the workspace/enrollment lookup has
@@ -182,7 +210,7 @@ export declare function executeRunOnSelfhostedMachine(machine: RunOnSelfhostedMa
182
210
  * agent subject; an offline machine surfaces a clear reason, never a wrong-box
183
211
  * landing.
184
212
  */
185
- export declare function runOnSandbox(services: FleetServices, ctx: FleetContext, target: string, op: RunOnOp): Promise<RunOnResult>;
213
+ export declare function runOnSandbox(services: FleetServices, ctx: FleetContext, target: string, op: RunOnOp, options?: RunOnOptions): Promise<RunOnResult>;
186
214
  export type ProvisionResult = {
187
215
  kind: "selfhosted";
188
216
  instructions: string;
@@ -1,7 +1,19 @@
1
1
  import { type Settings } from "@opengeni/config";
2
2
  import { type Database } from "@opengeni/db";
3
3
  import { type EventBus } from "@opengeni/events";
4
- import { type EstablishedSandboxSession, type RoutingSandboxOperationObserver, type SelfhostedRelayConfig } from "@opengeni/runtime/sandbox";
4
+ import { type EstablishedSandboxSession, type ResolvedActiveBackend, type RoutingRetainedProcess, type RoutingSandboxOperationObserver, type SelfhostedRelayConfig } from "@opengeni/runtime/sandbox";
5
+ type DirectRetainedProcessRoute = {
6
+ providerSessionId: number;
7
+ providerBackend: string;
8
+ providerInstanceId: string;
9
+ leaseEpoch: number;
10
+ routeKind: "home" | "active";
11
+ routeTargetId: string | null;
12
+ routeEpoch: number;
13
+ };
14
+ /** API-direct requests always use active-route authority. The default active
15
+ * pointer is represented by a null target id; it is not a home-route write. */
16
+ export declare function directRetainedProcessMatchesBackend(durable: DirectRetainedProcessRoute, process: RoutingRetainedProcess, backend: ResolvedActiveBackend): boolean;
5
17
  export type ChannelARoutingServices = {
6
18
  db: Database;
7
19
  settings: Settings;
@@ -37,6 +49,7 @@ export declare function wrapChannelABoxWithRouting(services: ChannelARoutingServ
37
49
  accountId: string;
38
50
  workspaceId: string;
39
51
  sessionId: string;
52
+ resourceSubjectId: string;
40
53
  homeLease?: {
41
54
  sandboxGroupId: string;
42
55
  leaseEpoch: number;
@@ -55,3 +68,4 @@ export declare function wrapChannelABoxWithRouting(services: ChannelARoutingServ
55
68
  holderId: string;
56
69
  };
57
70
  }, established: EstablishedSandboxSession): EstablishedSandboxSession;
71
+ export {};
@@ -0,0 +1,39 @@
1
+ import type { Settings } from "@opengeni/config";
2
+ import { CapabilityPack, type RigVersion, type Session, type SandboxBackend } from "@opengeni/contracts";
3
+ import { type Database } from "@opengeni/db";
4
+ import { resolveModalCheckpointProviderBinding } from "@opengeni/runtime/sandbox";
5
+ /** Pre-V2 Packs are the sole compatibility path where a Pack can still own a
6
+ * sandbox image. V2 installations select a Rig instead. Keep this predicate at
7
+ * the shared runtime boundary so turns, API-direct tools, viewers, Browser and
8
+ * Computer cannot resolve different physical machines for the same session. */
9
+ export declare function packInstallationUsesLegacyRuntime(input: {
10
+ manifestSnapshot: unknown | null;
11
+ manifestDigest: string | null;
12
+ }): boolean;
13
+ export declare function resolveWorkspaceLegacyRuntimePacks(db: Database, workspaceId: string): Promise<CapabilityPack[]>;
14
+ export declare function legacySandboxRuntimeFromPacks(packs: readonly CapabilityPack[]): {
15
+ sandboxImage: string | null;
16
+ sandboxProviderImages: CapabilityPack["sandboxProviderImages"] | null;
17
+ };
18
+ export declare function settingsWithPackSandboxImage(settings: Settings, sandboxImage: string | null, sandboxProviderImages?: CapabilityPack["sandboxProviderImages"] | null): Settings;
19
+ export declare function settingsWithRigImage(settings: Settings, rigImage: string | null): Settings;
20
+ export declare function rigProviderImageSourceImage(settings: Settings, backend: SandboxBackend): string | null;
21
+ export type RigProviderImageSelectionReason = "selected" | "missing" | "provider_unsupported" | "not_ready" | "not_cold_boot_validated" | "content_mismatch" | "provider_binding_unavailable" | "provider_binding_mismatch";
22
+ export declare function resolveRigProviderImageSelection(settings: Settings, version: RigVersion | null, backend: SandboxBackend, currentProviderBindingKeyHash: string | null): {
23
+ settings: Settings;
24
+ reason: RigProviderImageSelectionReason;
25
+ contentHash: string | null;
26
+ imageId: string | null;
27
+ };
28
+ export declare function resolveRigProviderImageForRun(settings: Settings, version: RigVersion | null, backend: SandboxBackend, resolveBinding?: typeof resolveModalCheckpointProviderBinding): Promise<ReturnType<typeof resolveRigProviderImageSelection>>;
29
+ export declare function settingsWithRigProviderImage(settings: Settings, version: RigVersion | null, backend: SandboxBackend, resolveBinding?: typeof resolveModalCheckpointProviderBinding): Promise<Settings>;
30
+ export type SessionSandboxRuntime = {
31
+ /** Exact logical image/rig settings used to fence the durable group lease. */
32
+ settings: Settings;
33
+ image: string | null;
34
+ rigVersion: RigVersion | null;
35
+ };
36
+ export declare function resolveSessionSandboxRuntime(db: Database, settings: Settings, session: Pick<Session, "workspaceId" | "sandboxBackend" | "rigId" | "rigVersionId">): Promise<SessionSandboxRuntime>;
37
+ /** Resolve provider-native cold-create optimization only for the single CAS
38
+ * winner. Ordinary API calls never perform provider identity I/O. */
39
+ export declare function providerSettingsForSessionSandboxRuntime(runtime: SessionSandboxRuntime, backend: SandboxBackend): Promise<Settings>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/core",
3
- "version": "0.28.1",
3
+ "version": "2.1.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": {
@@ -33,6 +33,10 @@
33
33
  "./canonical-human-identities": {
34
34
  "types": "./dist/canonical-human-identities.d.ts",
35
35
  "import": "./dist/canonical-human-identities.js"
36
+ },
37
+ "./connection-authority": {
38
+ "types": "./dist/domain/connection-authority.d.ts",
39
+ "import": "./dist/domain/connection-authority.js"
36
40
  }
37
41
  },
38
42
  "publishConfig": {
@@ -46,16 +50,17 @@
46
50
  },
47
51
  "dependencies": {
48
52
  "@modelcontextprotocol/sdk": "^1.29.0",
49
- "@opengeni/codex": "^0.2.17",
50
- "@opengeni/config": "^0.16.2",
51
- "@opengeni/contracts": "^0.50.0",
52
- "@opengeni/db": "^0.36.1",
53
- "@opengeni/documents": "^0.5.39",
54
- "@opengeni/events": "^0.3.110",
55
- "@opengeni/observability": "^0.7.5",
56
- "@opengeni/runtime": "^0.23.1",
57
- "@opengeni/storage": "^0.2.94",
58
- "hono": "^4.12.18"
53
+ "@opengeni/codex": "^0.2.18-canary.0",
54
+ "@opengeni/config": "^0.19.0-canary.0",
55
+ "@opengeni/contracts": "^2.2.0-canary.0",
56
+ "@opengeni/db": "^3.1.0-canary.0",
57
+ "@opengeni/documents": "^0.6.9-canary.0",
58
+ "@opengeni/events": "^0.3.123-canary.0",
59
+ "@opengeni/observability": "^0.8.3-canary.0",
60
+ "@opengeni/runtime": "^1.3.0-canary.0",
61
+ "@opengeni/storage": "^0.2.105-canary.0",
62
+ "hono": "^4.12.18",
63
+ "zod": "^4.2.1"
59
64
  },
60
65
  "engines": {
61
66
  "node": ">=18"
@@ -22,6 +22,41 @@ import { getManagedSession } from "../managed-session";
22
22
  const bearerPrefix = "Bearer ";
23
23
  const accessContextByRequest = new WeakMap<Request, Promise<AccessContext | null>>();
24
24
 
25
+ /**
26
+ * Contexts that were authenticated by a verified canonical managed cookie
27
+ * (Better Auth session), stamped at the single branch of
28
+ * {@link resolveAccessContext} that performs that verification.
29
+ *
30
+ * This is provenance — HOW the request authenticated — not a shape check on a
31
+ * value. It cannot be forged by a token claim, and it FAILS CLOSED by default:
32
+ * membership is only ever added at that one branch, so every other path
33
+ * (delegated bearer, API key, local bootstrap, configured key, and any path
34
+ * added later) is absent from the set without anyone maintaining a list. A
35
+ * request that never resolves an access context has no context to test at all.
36
+ *
37
+ * Keyed on the resolved `AccessContext` object identity, deliberately NOT on
38
+ * the `Request`: a reused, wrapped, or retried request object can therefore
39
+ * never carry a stale positive into a differently-authenticated resolution, and
40
+ * the non-cached `requireFreshAccessGrant` path is stamped correctly too. Each
41
+ * resolution produces a fresh context object, so the mark travels with exactly
42
+ * the value the cookie branch produced.
43
+ */
44
+ const canonicalManagedCookieContexts = new WeakSet<AccessContext>();
45
+ const resolvedAccessGrantAuthorizations = new WeakSet<object>();
46
+
47
+ /**
48
+ * Opaque, request-local proof that the canonical access resolver authorized the
49
+ * exact account administrator named by the stamp. The random id is audit and
50
+ * replay provenance only; object identity is what prevents a caller from
51
+ * manufacturing this proof inside the API process.
52
+ */
53
+ export type AccountAdminAuthorizationStamp = Readonly<{
54
+ authorizationId: string;
55
+ accountId: string;
56
+ actorSubjectId: string;
57
+ permission: "account:admin";
58
+ }>;
59
+
25
60
  export type AccessDeps = {
26
61
  db: Database;
27
62
  settings: Settings;
@@ -50,11 +85,38 @@ export async function requireAccessGrant(
50
85
  return (await requireAccessGrantAuthorization(c, deps, workspaceId, permission)).grant;
51
86
  }
52
87
 
88
+ /**
89
+ * Re-resolve the authenticated principal and its current grants without using
90
+ * the request-local access cache. Long-lived responses use this to ensure a
91
+ * membership suspension or revocation takes effect while the connection is
92
+ * still open.
93
+ */
94
+ export async function requireFreshAccessGrant(
95
+ c: Context,
96
+ deps: AccessDeps,
97
+ workspaceId: string,
98
+ permission?: Permission,
99
+ ): Promise<AccessGrant> {
100
+ const context = await resolveAccessContext(c, deps);
101
+ if (!context) {
102
+ throw new HTTPException(401, { message: "authentication required" });
103
+ }
104
+ return (await accessGrantAuthorization(context, deps, workspaceId, permission)).grant;
105
+ }
106
+
53
107
  export type AccessGrantAuthorization = {
54
108
  grant: AccessGrant;
55
109
  accountGrant: AccountGrant | null;
56
110
  authenticatedSubjectId: string;
57
111
  contextIntegrity: boolean;
112
+ /**
113
+ * Did this request authenticate as the canonical managed-cookie (Better Auth)
114
+ * session that OWNS this grant's subject? The single input to the owner-only
115
+ * managed personal-workspace exception; see `isCanonicalManagedHumanSession`.
116
+ * False for every bearer, API-key, delegated, service, local, and configured
117
+ * principal, and for any future path that does not verify a cookie.
118
+ */
119
+ canonicalManagedHumanSession: boolean;
58
120
  };
59
121
 
60
122
  export function accessGrantAuthorizationFromContext(
@@ -81,12 +143,46 @@ export function accessGrantAuthorizationFromContext(
81
143
  ) &&
82
144
  matchingAccountGrants.length === 1 &&
83
145
  matchingAccountGrants[0]?.subjectId === context.subjectId;
84
- return {
146
+ const authorization: AccessGrantAuthorization = {
85
147
  grant,
86
148
  accountGrant: contextIntegrity ? matchingAccountGrants[0]! : null,
87
149
  authenticatedSubjectId: context.subjectId,
88
150
  contextIntegrity,
151
+ canonicalManagedHumanSession: isCanonicalManagedHumanSession(context, grant),
89
152
  };
153
+ resolvedAccessGrantAuthorizations.add(authorization);
154
+ return authorization;
155
+ }
156
+
157
+ /**
158
+ * Mint the capability passed to an account-admin database lifecycle.
159
+ *
160
+ * This deliberately accepts every canonical access mode (managed,
161
+ * local/configured, and signed delegation). Organization-membership rows are
162
+ * one possible source of account authority, not the definition of it. A plain
163
+ * object with matching fields is rejected because only values returned by the
164
+ * access resolver are present in `resolvedAccessGrantAuthorizations`.
165
+ */
166
+ export function requireAccountAdminAuthorizationStamp(
167
+ authorization: AccessGrantAuthorization,
168
+ ): AccountAdminAuthorizationStamp {
169
+ const { grant, accountGrant } = authorization;
170
+ if (
171
+ !resolvedAccessGrantAuthorizations.has(authorization) ||
172
+ !authorization.contextIntegrity ||
173
+ authorization.authenticatedSubjectId !== grant.subjectId ||
174
+ accountGrant?.accountId !== grant.accountId ||
175
+ accountGrant.subjectId !== grant.subjectId ||
176
+ !accountGrant.permissions.includes("account:admin")
177
+ ) {
178
+ throw new HTTPException(403, { message: "missing permission: account:admin" });
179
+ }
180
+ return Object.freeze({
181
+ authorizationId: crypto.randomUUID(),
182
+ accountId: grant.accountId,
183
+ actorSubjectId: grant.subjectId,
184
+ permission: "account:admin" as const,
185
+ });
90
186
  }
91
187
 
92
188
  export async function requireAccessGrantAuthorization(
@@ -96,6 +192,15 @@ export async function requireAccessGrantAuthorization(
96
192
  permission?: Permission,
97
193
  ): Promise<AccessGrantAuthorization> {
98
194
  const context = await requireAccessContext(c, deps);
195
+ return await accessGrantAuthorization(context, deps, workspaceId, permission);
196
+ }
197
+
198
+ async function accessGrantAuthorization(
199
+ context: AccessContext,
200
+ deps: AccessDeps,
201
+ workspaceId: string,
202
+ permission?: Permission,
203
+ ): Promise<AccessGrantAuthorization> {
99
204
  const principalKind = hostedHumanSessionPrincipalKind(context);
100
205
  const grant =
101
206
  context.workspaceGrants.find((candidate) => candidate.workspaceId === workspaceId) ??
@@ -118,6 +223,37 @@ export async function requireAccessGrantAuthorization(
118
223
  return accessGrantAuthorizationFromContext(context, grant);
119
224
  }
120
225
 
226
+ /**
227
+ * May this grant use the owner-only managed personal-workspace exception?
228
+ *
229
+ * A managed human's personal workspace has no `workspace_memberships` row
230
+ * (migration 0219), so seams that fence on one must consult the
231
+ * `organization_memberships.personal_workspace_id` pointer instead. `AGENTS.md`
232
+ * scopes that exception tightly: it "derives an owner-only personal-workspace
233
+ * grant only for the canonical managed-cookie (Better Auth) session", and
234
+ * "Bearer/delegated principals, API keys, and account or organization
235
+ * administrators receive no personal-workspace access through that exception."
236
+ *
237
+ * This asks about PROVENANCE, not shape. Inspecting a grant's `principalKind`,
238
+ * `metadata.delegated`, or `serviceInitiator` is not sufficient: a delegated
239
+ * bearer chooses every one of those claims inside its own host-signed token and
240
+ * can name any subject, including the owner's. So the answer comes from
241
+ * {@link canonicalManagedCookieContexts}, stamped only where a Better Auth
242
+ * cookie was actually verified.
243
+ *
244
+ * The subject equality is the second half: the exception is OWNER-only, so the
245
+ * grant must belong to the authenticated human itself, not to some other
246
+ * subject reached from an authenticated session. `user:` excludes machine
247
+ * principals, which can never own an organization membership.
248
+ */
249
+ function isCanonicalManagedHumanSession(context: AccessContext, grant: AccessGrant): boolean {
250
+ return (
251
+ canonicalManagedCookieContexts.has(context) &&
252
+ grant.subjectId === context.subjectId &&
253
+ grant.subjectId.startsWith("user:")
254
+ );
255
+ }
256
+
121
257
  function hostedHumanSessionPrincipalKind(context: AccessContext): "human_session" | undefined {
122
258
  if (context.mode !== "managed" || context.workspaceGrants.length === 0) {
123
259
  return undefined;
@@ -165,42 +301,19 @@ export function requireLiteralPermission(grant: AccessGrant, permission: Permiss
165
301
  }
166
302
 
167
303
  export function hasLiteralPermission(permissions: Permission[], permission: Permission): boolean {
304
+ if (!Array.isArray(permissions)) return false;
168
305
  return permissions.includes(permission);
169
306
  }
170
307
 
171
308
  export function hasPermission(permissions: Permission[], permission: Permission): boolean {
309
+ if (!Array.isArray(permissions)) return false;
172
310
  if (permission === "secrets:read") {
173
311
  return permissions.includes("secrets:read");
174
312
  }
175
- const aliases: Partial<Record<Permission, Permission[]>> = {
176
- "variable-sets:use": ["environments:use" as Permission],
177
- "variable-sets:manage": ["environments:manage" as Permission],
178
- "variable-sets:list": [
179
- "variable-sets:use",
180
- "variable-sets:manage",
181
- "environments:use" as Permission,
182
- "environments:manage" as Permission,
183
- ],
184
- "variable-sets:read": [
185
- "variable-sets:use",
186
- "variable-sets:manage",
187
- "environments:use" as Permission,
188
- "environments:manage" as Permission,
189
- ],
190
- "variable-sets:write": ["variable-sets:manage", "environments:manage" as Permission],
191
- "secrets:list": [
192
- "variable-sets:use",
193
- "variable-sets:manage",
194
- "environments:use" as Permission,
195
- "environments:manage" as Permission,
196
- ],
197
- "secrets:write": ["variable-sets:manage", "environments:manage" as Permission],
198
- };
199
- return (
200
- permissions.includes(permission) ||
201
- (aliases[permission]?.some((alias) => permissions.includes(alias)) ?? false) ||
202
- permissions.includes("workspace:admin")
203
- );
313
+ // Variable-set metadata, plaintext read, write/rotation, attachment, and
314
+ // runtime use are independent capabilities. Deprecated broad permissions
315
+ // remain parseable but do not imply any of the exact permissions.
316
+ return permissions.includes(permission) || permissions.includes("workspace:admin");
204
317
  }
205
318
 
206
319
  async function resolveAccessContext(c: Context, deps: AccessDeps): Promise<AccessContext | null> {
@@ -260,11 +373,18 @@ async function resolveAccessContext(c: Context, deps: AccessDeps): Promise<Acces
260
373
  if (deps.managedAuth) {
261
374
  const session = await getManagedSession(c, deps.managedAuth, { db: deps.db });
262
375
  if (session?.user) {
263
- return await ensureManagedAccessForUser(deps.db, {
376
+ // THE canonical managed-cookie (Better Auth) branch, and the only place
377
+ // that may stamp a context as such. Every `return` above this point leaves
378
+ // its context unstamped, so the owner-only personal-workspace exception
379
+ // fails closed for them without depending on an exclusion list.
380
+ const context = await ensureManagedAccessForUser(deps.db, {
264
381
  userId: session.user.id,
265
382
  email: session.user.email,
266
383
  name: session.user.name,
384
+ emailVerified: session.user.emailVerified,
267
385
  });
386
+ canonicalManagedCookieContexts.add(context);
387
+ return context;
268
388
  }
269
389
  }
270
390
 
@@ -12,8 +12,9 @@ import {
12
12
  getVariableSet,
13
13
  NewSessionDraftAccessError,
14
14
  newSessionDraftToolsProvided,
15
+ newSessionSelectionHistory,
15
16
  publicNewSessionDraftOptions,
16
- requireFile,
17
+ requireFileForSubject,
17
18
  saveNewSessionDraftInTransaction,
18
19
  withWorkspaceSubjectRls,
19
20
  } from "@opengeni/db";
@@ -50,6 +51,7 @@ function mapNewSessionDraft(
50
51
  reasoningEffort: row.reasoningEffort,
51
52
  latencyMode: row.latencyMode,
52
53
  options: publicNewSessionDraftOptions(row),
54
+ selectionHistory: newSessionSelectionHistory(row),
53
55
  updatedAt: row.updatedAt.toISOString(),
54
56
  });
55
57
  }
@@ -88,7 +90,12 @@ async function hydrateNewSessionDraft(
88
90
  continue;
89
91
  }
90
92
  try {
91
- const file = await requireFile(deps.db, workspaceId, resource.fileId);
93
+ const file = await requireFileForSubject(deps.db, {
94
+ accountId: grant.accountId,
95
+ workspaceId,
96
+ subjectId: grant.subjectId,
97
+ fileId: resource.fileId,
98
+ });
92
99
  if (file.status === "ready") resources.push(resource);
93
100
  } catch {
94
101
  // Missing, foreign, failed, and pending files are stale draft state.
@@ -98,8 +105,13 @@ async function hydrateNewSessionDraft(
98
105
  const options = { ...mapped.options };
99
106
  if (options.variableSetId) {
100
107
  if (
108
+ !hasPermission(grant.permissions, "variable-sets:attach") ||
101
109
  !hasPermission(grant.permissions, "variable-sets:use") ||
102
- !(await getVariableSet(deps.db, workspaceId, options.variableSetId))
110
+ !(await getVariableSet(
111
+ deps.db,
112
+ { accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
113
+ options.variableSetId,
114
+ ))
103
115
  ) {
104
116
  delete options.variableSetId;
105
117
  }
@@ -173,6 +185,7 @@ export async function getActorNewSessionDraft(
173
185
  reasoningEffort: deps.settings.openaiReasoningEffort,
174
186
  latencyMode: "standard",
175
187
  options: {},
188
+ selectionHistory: { projects: [] },
176
189
  updatedAt: null,
177
190
  }
178
191
  );
@@ -190,6 +203,13 @@ export async function saveActorNewSessionDraft(
190
203
  grant: AccessGrant,
191
204
  workspaceId: string,
192
205
  rawInput: unknown,
206
+ /**
207
+ * `AccessGrantAuthorization.canonicalManagedHumanSession` — did this request
208
+ * authenticate as the canonical managed cookie that owns `grant.subjectId`?
209
+ * Defaults to false so every caller that has not proven it fails closed onto
210
+ * the historical bare-membership fence.
211
+ */
212
+ canonicalManagedHumanSession = false,
193
213
  ): Promise<NewSessionDraftValue> {
194
214
  const input = SaveNewSessionDraftRequest.parse(rawInput);
195
215
  // The pre-marker client contract required `tools` and had no
@@ -207,9 +227,11 @@ export async function saveActorNewSessionDraft(
207
227
  const tools = toolsProvided ? validateToolRefs(input.tools, runtimeSettings) : [];
208
228
  await validateGitHubRepositorySelection(deps.db, workspaceId, resources);
209
229
  if (resources.some((resource) => resource.kind === "file") && !deps.objectStorage) {
210
- throw new HTTPException(503, { message: "object storage is not configured" });
230
+ throw new HTTPException(503, {
231
+ message: "object storage is not configured",
232
+ });
211
233
  }
212
- await validateFileResources(deps.db, workspaceId, resources);
234
+ await validateFileResources(deps.db, grant.accountId, workspaceId, grant.subjectId, resources);
213
235
  assertConfiguredModel(deps.settings, input.model);
214
236
  await assertWorkspaceModelPolicyAllows(deps.db, deps.settings, workspaceId, input.model);
215
237
 
@@ -234,6 +256,11 @@ export async function saveActorNewSessionDraft(
234
256
  // worker MCP principal) legitimately have no workspace_memberships
235
257
  // row, so they must not be rejected by the human-removal fence.
236
258
  requireWorkspaceMembership: grant.subjectId.startsWith("user:"),
259
+ // A managed human's own personal workspace has no membership row at
260
+ // all, so the human-removal fence above must fall back to the
261
+ // organization-membership pointer for them — and only for the
262
+ // canonical managed-cookie session that owns it.
263
+ personalWorkspaceOwnerException: canonicalManagedHumanSession,
237
264
  }),
238
265
  ),
239
266
  );