@opengeni/core 1.0.1 → 2.2.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 +34 -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 +22 -1
  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 +40 -6
  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 +4827 -1164
  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 +157 -31
  51. package/src/application/new-session-drafts.ts +24 -2
  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 +532 -51
  77. package/src/domain/pr-review.ts +671 -0
  78. package/src/domain/remember.ts +549 -0
  79. package/src/domain/resources.ts +131 -4
  80. package/src/domain/scheduled-tasks.ts +497 -76
  81. package/src/domain/session-tool-policy.ts +12 -9
  82. package/src/domain/sessions.ts +613 -166
  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": "1.0.1",
3
+ "version": "2.2.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.4",
51
- "@opengeni/contracts": "^1.0.1",
52
- "@opengeni/db": "^1.0.1",
53
- "@opengeni/documents": "^0.5.41",
54
- "@opengeni/events": "^0.3.112",
55
- "@opengeni/observability": "^0.7.7",
56
- "@opengeni/runtime": "^1.0.1",
57
- "@opengeni/storage": "^0.2.96",
58
- "hono": "^4.12.18"
53
+ "@opengeni/codex": "^0.2.19-canary.0",
54
+ "@opengeni/config": "^0.19.1-canary.0",
55
+ "@opengeni/contracts": "^2.3.0-canary.0",
56
+ "@opengeni/db": "^3.2.0-canary.0",
57
+ "@opengeni/documents": "^0.7.0-canary.0",
58
+ "@opengeni/events": "^0.3.124-canary.0",
59
+ "@opengeni/observability": "^0.8.4-canary.0",
60
+ "@opengeni/runtime": "^1.3.1-canary.0",
61
+ "@opengeni/storage": "^0.2.106-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,43 @@ 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 carries no `workspace_memberships` row,
230
+ * so seams that fence on one must consult the
231
+ * `organization_memberships.personal_workspace_id` pointer instead. That is a
232
+ * runtime property, not a schema one: migration 0219's `42501` is a
233
+ * precondition inside the provisioning function rather than a constraint, and
234
+ * the parity report records `personal_workspace_has_no_membership_row` as
235
+ * `basis: "runtime"` - its own term for something nothing in the schema
236
+ * prevents. It holds because every membership writer requires `members:manage`
237
+ * on the target, which a personal-workspace grant deliberately omits. `AGENTS.md`
238
+ * scopes that exception tightly: it "derives an owner-only personal-workspace
239
+ * grant only for the canonical managed-cookie (Better Auth) session", and
240
+ * "Bearer/delegated principals, API keys, and account or organization
241
+ * administrators receive no personal-workspace access through that exception."
242
+ *
243
+ * This asks about PROVENANCE, not shape. Inspecting a grant's `principalKind`,
244
+ * `metadata.delegated`, or `serviceInitiator` is not sufficient: a delegated
245
+ * bearer chooses every one of those claims inside its own host-signed token and
246
+ * can name any subject, including the owner's. So the answer comes from
247
+ * {@link canonicalManagedCookieContexts}, stamped only where a Better Auth
248
+ * cookie was actually verified.
249
+ *
250
+ * The subject equality is the second half: the exception is OWNER-only, so the
251
+ * grant must belong to the authenticated human itself, not to some other
252
+ * subject reached from an authenticated session. `user:` excludes machine
253
+ * principals, which can never own an organization membership.
254
+ */
255
+ function isCanonicalManagedHumanSession(context: AccessContext, grant: AccessGrant): boolean {
256
+ return (
257
+ canonicalManagedCookieContexts.has(context) &&
258
+ grant.subjectId === context.subjectId &&
259
+ grant.subjectId.startsWith("user:")
260
+ );
261
+ }
262
+
121
263
  function hostedHumanSessionPrincipalKind(context: AccessContext): "human_session" | undefined {
122
264
  if (context.mode !== "managed" || context.workspaceGrants.length === 0) {
123
265
  return undefined;
@@ -165,42 +307,19 @@ export function requireLiteralPermission(grant: AccessGrant, permission: Permiss
165
307
  }
166
308
 
167
309
  export function hasLiteralPermission(permissions: Permission[], permission: Permission): boolean {
310
+ if (!Array.isArray(permissions)) return false;
168
311
  return permissions.includes(permission);
169
312
  }
170
313
 
171
314
  export function hasPermission(permissions: Permission[], permission: Permission): boolean {
315
+ if (!Array.isArray(permissions)) return false;
172
316
  if (permission === "secrets:read") {
173
317
  return permissions.includes("secrets:read");
174
318
  }
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
- );
319
+ // Variable-set metadata, plaintext read, write/rotation, attachment, and
320
+ // runtime use are independent capabilities. Deprecated broad permissions
321
+ // remain parseable but do not imply any of the exact permissions.
322
+ return permissions.includes(permission) || permissions.includes("workspace:admin");
204
323
  }
205
324
 
206
325
  async function resolveAccessContext(c: Context, deps: AccessDeps): Promise<AccessContext | null> {
@@ -260,11 +379,18 @@ async function resolveAccessContext(c: Context, deps: AccessDeps): Promise<Acces
260
379
  if (deps.managedAuth) {
261
380
  const session = await getManagedSession(c, deps.managedAuth, { db: deps.db });
262
381
  if (session?.user) {
263
- return await ensureManagedAccessForUser(deps.db, {
382
+ // THE canonical managed-cookie (Better Auth) branch, and the only place
383
+ // that may stamp a context as such. Every `return` above this point leaves
384
+ // its context unstamped, so the owner-only personal-workspace exception
385
+ // fails closed for them without depending on an exclusion list.
386
+ const context = await ensureManagedAccessForUser(deps.db, {
264
387
  userId: session.user.id,
265
388
  email: session.user.email,
266
389
  name: session.user.name,
390
+ emailVerified: session.user.emailVerified,
267
391
  });
392
+ canonicalManagedCookieContexts.add(context);
393
+ return context;
268
394
  }
269
395
  }
270
396
 
@@ -12,6 +12,7 @@ import {
12
12
  getVariableSet,
13
13
  NewSessionDraftAccessError,
14
14
  newSessionDraftToolsProvided,
15
+ newSessionSelectionHistory,
15
16
  publicNewSessionDraftOptions,
16
17
  requireFileForSubject,
17
18
  saveNewSessionDraftInTransaction,
@@ -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
  }
@@ -103,8 +105,13 @@ async function hydrateNewSessionDraft(
103
105
  const options = { ...mapped.options };
104
106
  if (options.variableSetId) {
105
107
  if (
108
+ !hasPermission(grant.permissions, "variable-sets:attach") ||
106
109
  !hasPermission(grant.permissions, "variable-sets:use") ||
107
- !(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
+ ))
108
115
  ) {
109
116
  delete options.variableSetId;
110
117
  }
@@ -178,6 +185,7 @@ export async function getActorNewSessionDraft(
178
185
  reasoningEffort: deps.settings.openaiReasoningEffort,
179
186
  latencyMode: "standard",
180
187
  options: {},
188
+ selectionHistory: { projects: [] },
181
189
  updatedAt: null,
182
190
  }
183
191
  );
@@ -195,6 +203,13 @@ export async function saveActorNewSessionDraft(
195
203
  grant: AccessGrant,
196
204
  workspaceId: string,
197
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,
198
213
  ): Promise<NewSessionDraftValue> {
199
214
  const input = SaveNewSessionDraftRequest.parse(rawInput);
200
215
  // The pre-marker client contract required `tools` and had no
@@ -212,7 +227,9 @@ export async function saveActorNewSessionDraft(
212
227
  const tools = toolsProvided ? validateToolRefs(input.tools, runtimeSettings) : [];
213
228
  await validateGitHubRepositorySelection(deps.db, workspaceId, resources);
214
229
  if (resources.some((resource) => resource.kind === "file") && !deps.objectStorage) {
215
- throw new HTTPException(503, { message: "object storage is not configured" });
230
+ throw new HTTPException(503, {
231
+ message: "object storage is not configured",
232
+ });
216
233
  }
217
234
  await validateFileResources(deps.db, grant.accountId, workspaceId, grant.subjectId, resources);
218
235
  assertConfiguredModel(deps.settings, input.model);
@@ -239,6 +256,11 @@ export async function saveActorNewSessionDraft(
239
256
  // worker MCP principal) legitimately have no workspace_memberships
240
257
  // row, so they must not be rejected by the human-removal fence.
241
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,
242
264
  }),
243
265
  ),
244
266
  );