@opengeni/core 2.5.3 → 2.6.4-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 (46) hide show
  1. package/dist/access/index.d.ts +24 -0
  2. package/dist/billing/limits.d.ts +5 -0
  3. package/dist/canonical-human-identities.js +2 -2
  4. package/dist/{chunk-ZVZJTMSV.js → chunk-OF65T3PM.js} +2 -2
  5. package/dist/{chunk-YGOMUGYS.js → chunk-QO5GVFFO.js} +17 -8
  6. package/dist/{chunk-YGOMUGYS.js.map → chunk-QO5GVFFO.js.map} +1 -1
  7. package/dist/dependencies.d.ts +10 -1
  8. package/dist/domain/company-brain-governed-writes.d.ts +15 -6
  9. package/dist/domain/company-profile-agent-admin.d.ts +3 -2
  10. package/dist/domain/environments.d.ts +1 -1
  11. package/dist/domain/memory-slack-delivery.d.ts +4 -1
  12. package/dist/domain/personal-connection-delegations.d.ts +1 -0
  13. package/dist/domain/pr-review.d.ts +1 -1
  14. package/dist/domain/scheduled-tasks.d.ts +12 -0
  15. package/dist/domain/sessions.d.ts +37 -11
  16. package/dist/domain/workspace-members.d.ts +8 -1
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.js +1706 -667
  19. package/dist/index.js.map +1 -1
  20. package/dist/managed-auth-session-sets.d.ts +18 -0
  21. package/dist/managed-auth-session-sets.js +3 -1
  22. package/dist/model-catalog.d.ts +89 -0
  23. package/dist/sandbox/fleet.d.ts +6 -4
  24. package/dist/sandbox/routing.d.ts +7 -2
  25. package/dist/sandbox/runtime-settings.d.ts +17 -1
  26. package/package.json +10 -10
  27. package/src/access/index.ts +140 -5
  28. package/src/application/user-resource-grants.ts +31 -2
  29. package/src/billing/limits.ts +57 -24
  30. package/src/dependencies.ts +15 -1
  31. package/src/domain/company-brain-governed-writes.ts +29 -13
  32. package/src/domain/company-profile-agent-admin.ts +3 -2
  33. package/src/domain/environments.ts +6 -34
  34. package/src/domain/memory-slack-delivery.ts +30 -0
  35. package/src/domain/personal-connection-delegations.ts +40 -7
  36. package/src/domain/remember.ts +5 -6
  37. package/src/domain/scheduled-tasks.ts +146 -1
  38. package/src/domain/sessions.ts +912 -358
  39. package/src/domain/workspace-members.ts +34 -2
  40. package/src/index.ts +1 -0
  41. package/src/managed-auth-session-sets.ts +38 -11
  42. package/src/model-catalog.ts +565 -0
  43. package/src/sandbox/fleet.ts +20 -17
  44. package/src/sandbox/routing.ts +18 -4
  45. package/src/sandbox/runtime-settings.ts +32 -0
  46. /package/dist/{chunk-ZVZJTMSV.js.map → chunk-OF65T3PM.js.map} +0 -0
@@ -110,4 +110,22 @@ export declare function authenticateAndAdoptManagedAuthSession(input: {
110
110
  projection: ManagedAuthDatabaseProjection;
111
111
  returnIntent: string | null;
112
112
  }>;
113
+ export declare function adoptManagedAuthSession(input: {
114
+ db: Database;
115
+ adapter: ManagedAuthSessionAdapter;
116
+ authority: string;
117
+ authorityHash: string;
118
+ csrfHash: string;
119
+ operationId: string;
120
+ requestDigest: string;
121
+ expectedGeneration: string;
122
+ expectedActorEpoch: string;
123
+ transactionId: string;
124
+ transactionSecretHash: string;
125
+ authSessionId: string;
126
+ mode: ManagedAuthSessionSetMode;
127
+ }): Promise<{
128
+ projection: ManagedAuthDatabaseProjection;
129
+ returnIntent: string | null;
130
+ }>;
113
131
  export declare function isolatedManagedAuthHeaders(request: Request): Headers;
@@ -7,6 +7,7 @@ import {
7
7
  ManagedAuthActorChangeError,
8
8
  ManagedAuthCompletionOutcomeUnknownError,
9
9
  ManagedAuthRequestAdmissionError,
10
+ adoptManagedAuthSession,
10
11
  authenticateAndAdoptManagedAuthSession,
11
12
  isolatedManagedAuthHeaders,
12
13
  managedAuthCsrfHash,
@@ -21,7 +22,7 @@ import {
21
22
  requireManagedAuthMutationAdmission,
22
23
  resolveManagedAuthSelectedSession,
23
24
  withManagedAuthCsrfToken
24
- } from "./chunk-YGOMUGYS.js";
25
+ } from "./chunk-QO5GVFFO.js";
25
26
  export {
26
27
  MANAGED_AUTH_ACTOR_EPOCH_HEADER,
27
28
  MANAGED_AUTH_CSRF_HEADER,
@@ -31,6 +32,7 @@ export {
31
32
  ManagedAuthActorChangeError,
32
33
  ManagedAuthCompletionOutcomeUnknownError,
33
34
  ManagedAuthRequestAdmissionError,
35
+ adoptManagedAuthSession,
34
36
  authenticateAndAdoptManagedAuthSession,
35
37
  isolatedManagedAuthHeaders,
36
38
  managedAuthCsrfHash,
@@ -0,0 +1,89 @@
1
+ import { type ConfiguredModel, type Settings } from "@opengeni/config";
2
+ import { type ModelAvailabilityV1, type ModelCredentialReadinessV1, type WorkspaceModelPolicyContract } from "@opengeni/contracts";
3
+ import { type Database } from "@opengeni/db";
4
+ export type ResolvedCatalogSettings = {
5
+ settings: Settings;
6
+ source: "code" | "database";
7
+ version: number | null;
8
+ modelNotes: Record<string, string>;
9
+ };
10
+ /**
11
+ * Curated workspace Gateway products and workspace-owned custom slugs share
12
+ * one public prefix. Only the latter have a mutable catalog row whose active
13
+ * generation must be rechecked at a fresh acceptance commit boundary.
14
+ */
15
+ export declare function isWorkspaceGatewayCustomModelId(settings: Settings, modelId: string): boolean;
16
+ export declare function isWorkspaceOpenRouterCustomModelId(settings: Settings, modelId: string): boolean;
17
+ export type WorkspaceCustomModelReference = {
18
+ providerKind: "vercel_gateway" | "openrouter";
19
+ upstreamModelId: string;
20
+ };
21
+ export declare function workspaceCustomModelReference(settings: Settings, modelId: string): WorkspaceCustomModelReference | null;
22
+ export declare function isWorkspaceCustomModelId(settings: Settings, modelId: string): boolean;
23
+ /**
24
+ * Resolve the deployment catalog without making synchronous env settings read
25
+ * Postgres. Database mode fails closed when the singleton is absent or invalid;
26
+ * code mode preserves the already-validated env catalog.
27
+ */
28
+ export declare function resolveCatalogSettings(db: Database, envSettings: Settings): Promise<ResolvedCatalogSettings>;
29
+ /**
30
+ * Resolve the deployment catalog and add only the custom Gateway slugs owned by
31
+ * one workspace. Use this at model-bearing workspace boundaries; public config
32
+ * and deployment-operator surfaces must continue to use `resolveCatalogSettings`.
33
+ */
34
+ export declare function resolveWorkspaceCatalogSettings(db: Database, envSettings: Settings, input: {
35
+ accountId: string;
36
+ workspaceId: string;
37
+ retainedProductModelId?: string | null;
38
+ retainedProductModelIds?: readonly (string | null | undefined)[];
39
+ }): Promise<ResolvedCatalogSettings>;
40
+ export type ModelAvailabilityObservation = {
41
+ status: "available" | "degraded" | "unavailable";
42
+ reason: "not_entitled" | "provider_unhealthy" | null;
43
+ checkedAt: string;
44
+ };
45
+ export type ModelCredentialReadinessObservation = {
46
+ status: "ready";
47
+ checkedAt: string;
48
+ } | {
49
+ status: "not_ready";
50
+ reason: "prerequisites_missing" | "needs_reauth";
51
+ checkedAt: string;
52
+ } | {
53
+ status: "error";
54
+ reason: "resolver_error";
55
+ checkedAt: string;
56
+ };
57
+ export declare const MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS: number;
58
+ export type WorkspaceModelSelectionInput = {
59
+ settings: Settings;
60
+ policy: WorkspaceModelPolicyContract | null;
61
+ codexSubscriptionActive: boolean;
62
+ xaiSubscriptionActive?: boolean;
63
+ workspaceGatewayConnectionActive?: boolean;
64
+ workspaceOpenRouterConnectionActive?: boolean;
65
+ workspaceGatewayCustomModels?: readonly {
66
+ upstreamModelId: string;
67
+ label?: string | null;
68
+ }[];
69
+ workspaceOpenRouterCustomModels?: readonly {
70
+ upstreamModelId: string;
71
+ label?: string | null;
72
+ }[];
73
+ credentialReadinessObservations?: Readonly<Record<string, ModelCredentialReadinessObservation>> | undefined;
74
+ observations?: Readonly<Record<string, ModelAvailabilityObservation>> | undefined;
75
+ now?: Date | undefined;
76
+ credentialReadinessMaxAgeMs?: number | undefined;
77
+ };
78
+ export type WorkspaceModelSelection = {
79
+ model: ConfiguredModel;
80
+ credentialReadiness: ModelCredentialReadinessV1;
81
+ policyAllowed: boolean;
82
+ availability: ModelAvailabilityV1;
83
+ };
84
+ /**
85
+ * One shared picker/tool decision. Catalog membership, credential readiness,
86
+ * workspace policy, and optional provider-health observations are evaluated in
87
+ * configured catalog order so every consumer exposes the same selectable set.
88
+ */
89
+ export declare function resolveWorkspaceModelSelection(input: WorkspaceModelSelectionInput): WorkspaceModelSelection[];
@@ -1,4 +1,5 @@
1
1
  import type { Settings } from "@opengeni/config";
2
+ import type { SandboxBackend } from "@opengeni/contracts";
2
3
  import { type Database, type SandboxRecord } from "@opengeni/db";
3
4
  import type { EventBus } from "@opengeni/events";
4
5
  import { type BackendUnresolvableCode, type ControlRpc, type SelfhostedRelayConfig, type SelfhostedOpStreamDeps, type SelfhostedOperationAdmission, type SelfhostedOperationResourcePolicy } from "@opengeni/runtime/sandbox";
@@ -28,8 +29,8 @@ export type FleetContext = {
28
29
  /** The calling session (the pointer the attach/swap mutates + whose group box
29
30
  * is the default fleet member). */
30
31
  sessionId: string;
31
- /** The session's own group sandbox backend (modal/selfhosted/…). */
32
- sessionBackend: string;
32
+ /** The session's durable home-compute policy. */
33
+ sessionBackend: SandboxBackend;
33
34
  /** The session's own group sandbox id (the lease group). */
34
35
  sessionGroupId: string;
35
36
  };
@@ -122,8 +123,9 @@ export type FleetSwapResult = {
122
123
  * List the fleet: the session's own group box when it has one (a synthetic
123
124
  * entry) + the workspace's first-class selfhosted sandboxes (each probed for
124
125
  * liveness), each with an `active` marker derived from the session's active
125
- * pointer. A backend:none session has no synthetic home entry; a null pointer
126
- * then means no compute is attached.
126
+ * pointer. A backend:none session and a machine-home session on a deployment
127
+ * without a managed provider have no synthetic group entry; a null pointer then
128
+ * means no compute is attached.
127
129
  */
128
130
  export declare function listFleet(services: FleetServices, ctx: FleetContext): Promise<FleetListResult>;
129
131
  /**
@@ -1,7 +1,7 @@
1
1
  import { type Settings } from "@opengeni/config";
2
- import { type Database } from "@opengeni/db";
2
+ import { type Database, type SandboxRetainedProcess } from "@opengeni/db";
3
3
  import { type EventBus } from "@opengeni/events";
4
- import { type EstablishedSandboxSession, type ResolvedActiveBackend, type RoutingRetainedProcess, type RoutingSandboxOperationObserver, type SelfhostedRelayConfig } from "@opengeni/runtime/sandbox";
4
+ import { type EstablishedSandboxSession, type ResolvedActiveBackend, type RoutingRetainedProcess, type RoutingRetainedProcessTerminalProof, type RoutingSandboxOperationObserver, type SelfhostedRelayConfig } from "@opengeni/runtime/sandbox";
5
5
  type DirectRetainedProcessRoute = {
6
6
  providerSessionId: number;
7
7
  providerBackend: string;
@@ -14,6 +14,11 @@ type DirectRetainedProcessRoute = {
14
14
  /** API-direct requests always use active-route authority. The default active
15
15
  * pointer is represented by a null target id; it is not a home-route write. */
16
16
  export declare function directRetainedProcessMatchesBackend(durable: DirectRetainedProcessRoute, process: RoutingRetainedProcess, backend: ResolvedActiveBackend): boolean;
17
+ export declare function retainedProcessBackgroundSettlement(process: Pick<SandboxRetainedProcess, "state" | "exitCode" | "settlementReason">, fallback: RoutingRetainedProcessTerminalProof): {
18
+ outcome: "exited" | "lost";
19
+ exitCode: number | null;
20
+ reason: string;
21
+ };
17
22
  export type ChannelARoutingServices = {
18
23
  db: Database;
19
24
  settings: Settings;
@@ -1,7 +1,23 @@
1
1
  import type { Settings } from "@opengeni/config";
2
- import { CapabilityPack, type RigVersion, type Session, type SandboxBackend } from "@opengeni/contracts";
2
+ import { CapabilityPack, type RigVersion, type Session, type SandboxBackend, type SandboxOs } from "@opengeni/contracts";
3
3
  import { type Database } from "@opengeni/db";
4
4
  import { resolveModalCheckpointProviderBinding } from "@opengeni/runtime/sandbox";
5
+ export type ManagedSessionGroupBackend = Exclude<SandboxBackend, "none" | "selfhosted">;
6
+ /**
7
+ * Resolve the provider backend behind a session's synthetic managed group.
8
+ *
9
+ * A machine-home session keeps `selfhosted` as its durable policy while a
10
+ * non-null active pointer selects the enrolled machine. Clearing that pointer
11
+ * is an explicit switch to the deployment's managed group, so API readiness,
12
+ * viewer attachment, Channel-A, and worker turns must all select the same
13
+ * provider. Deployments configured with `none` or `selfhosted` have no managed
14
+ * fallback to expose.
15
+ */
16
+ export declare function managedSessionGroupBackend(deploymentBackend: SandboxBackend, sessionBackend: SandboxBackend): ManagedSessionGroupBackend | null;
17
+ /** A machine-home row carries the machine's host OS; its managed group uses the
18
+ * platform's canonical managed-sandbox OS instead. Ordinary sessions preserve
19
+ * their explicitly selected OS. */
20
+ export declare function managedSessionGroupOs(sessionBackend: SandboxBackend, sessionOs: SandboxOs): SandboxOs;
5
21
  /** Pre-V2 Packs are the sole compatibility path where a Pack can still own a
6
22
  * sandbox image. V2 installations select a Rig instead. Keep this predicate at
7
23
  * the shared runtime boundary so turns, API-direct tools, viewers, Browser and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/core",
3
- "version": "2.5.3",
3
+ "version": "2.6.4-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": {
@@ -54,15 +54,15 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@modelcontextprotocol/sdk": "^1.29.0",
57
- "@opengeni/codex": "^0.2.19",
58
- "@opengeni/config": "^0.22.0",
59
- "@opengeni/contracts": "^2.7.0",
60
- "@opengeni/db": "^3.6.0",
61
- "@opengeni/documents": "^0.8.6",
62
- "@opengeni/events": "^0.4.4",
63
- "@opengeni/observability": "^0.8.9",
64
- "@opengeni/runtime": "^1.4.2",
65
- "@opengeni/storage": "^0.2.110",
57
+ "@opengeni/codex": "^0.2.20-canary.1",
58
+ "@opengeni/config": "^0.22.5-canary.1",
59
+ "@opengeni/contracts": "^2.9.2-canary.1",
60
+ "@opengeni/db": "^3.7.4-canary.0",
61
+ "@opengeni/documents": "^0.8.14-canary.0",
62
+ "@opengeni/events": "^0.4.12-canary.0",
63
+ "@opengeni/observability": "^0.8.14-canary.1",
64
+ "@opengeni/runtime": "^2.0.1-canary.1",
65
+ "@opengeni/storage": "^0.2.115-canary.1",
66
66
  "hono": "^4.12.18",
67
67
  "zod": "^4.2.1"
68
68
  },
@@ -43,7 +43,60 @@ const accessContextByRequest = new WeakMap<Request, Promise<AccessContext | null
43
43
  * the value the cookie branch produced.
44
44
  */
45
45
  const canonicalManagedCookieContexts = new WeakSet<AccessContext>();
46
+ const canonicalLocalHumanContexts = new WeakSet<AccessContext>();
46
47
  const resolvedAccessGrantAuthorizations = new WeakSet<object>();
48
+ const accountScopedApiKeyContexts = new WeakMap<
49
+ AccessContext,
50
+ Readonly<{ accountId: string; permissions: readonly Permission[] }>
51
+ >();
52
+
53
+ const accountScopedApiKeyAccountPermissions = new Set<Permission>([
54
+ "account:read",
55
+ "account:admin",
56
+ "workspace:create",
57
+ "billing:read",
58
+ "billing:manage",
59
+ "api_keys:manage",
60
+ ]);
61
+ const accountScopedApiKeyWorkspaceExcludedPermissions = new Set<Permission>([
62
+ "account:read",
63
+ "account:admin",
64
+ "workspace:create",
65
+ "billing:read",
66
+ "billing:manage",
67
+ ]);
68
+
69
+ export type AccountScopedApiKeyWorkspaceAuthority = Readonly<{
70
+ accountId: string;
71
+ permissions: Permission[];
72
+ }>;
73
+
74
+ /**
75
+ * Return account-scoped API-key workspace authority only for the exact
76
+ * AccessContext object stamped by successful API-key authentication. Subject
77
+ * shape alone is deliberately insufficient.
78
+ */
79
+ export function accountScopedApiKeyWorkspaceAuthority(
80
+ context: AccessContext,
81
+ ): AccountScopedApiKeyWorkspaceAuthority | null {
82
+ const authority = accountScopedApiKeyContexts.get(context);
83
+ if (!authority) return null;
84
+ const matchingAccountGrants = context.accountGrants.filter(
85
+ (grant) => grant.accountId === authority.accountId && grant.subjectId === context.subjectId,
86
+ );
87
+ if (
88
+ !context.subjectId.startsWith("api_key:") ||
89
+ matchingAccountGrants.length !== 1 ||
90
+ context.defaultAccountId !== authority.accountId ||
91
+ context.defaultWorkspaceId !== null
92
+ ) {
93
+ return null;
94
+ }
95
+ return {
96
+ accountId: authority.accountId,
97
+ permissions: [...authority.permissions],
98
+ };
99
+ }
47
100
 
48
101
  /**
49
102
  * Opaque, request-local proof that the canonical access resolver authorized the
@@ -119,6 +172,8 @@ export type AccessGrantAuthorization = {
119
172
  * principal, and for any future path that does not verify a cookie.
120
173
  */
121
174
  canonicalManagedHumanSession: boolean;
175
+ /** Exact in-process single-user local bootstrap, never a delegated bearer. */
176
+ canonicalLocalHumanSession: boolean;
122
177
  };
123
178
 
124
179
  export function accessGrantAuthorizationFromContext(
@@ -151,6 +206,7 @@ export function accessGrantAuthorizationFromContext(
151
206
  authenticatedSubjectId: context.subjectId,
152
207
  contextIntegrity,
153
208
  canonicalManagedHumanSession: isCanonicalManagedHumanSession(context, grant),
209
+ canonicalLocalHumanSession: isCanonicalLocalHumanSession(context, grant),
154
210
  };
155
211
  resolvedAccessGrantAuthorizations.add(authorization);
156
212
  return authorization;
@@ -187,6 +243,37 @@ export function requireAccountAdminAuthorizationStamp(
187
243
  });
188
244
  }
189
245
 
246
+ /**
247
+ * Resolve the exact built-in single-user local administrator for an account.
248
+ *
249
+ * This is intentionally narrower than checking `context.mode === "local"` or
250
+ * the `dev` subject name. Only the in-process local bootstrap branch can place
251
+ * the resolved context in `canonicalLocalHumanContexts`, so delegated bearer
252
+ * tokens and caller-constructed contexts cannot borrow this authority.
253
+ */
254
+ export async function requireCanonicalLocalAccountAdministrator(
255
+ c: Context,
256
+ deps: AccessDeps,
257
+ accountId: string,
258
+ ): Promise<{ subjectId: string; authorization: AccessGrantAuthorization }> {
259
+ if (c.req.header("authorization")) {
260
+ throw new HTTPException(401, { message: "organization administrator session required" });
261
+ }
262
+ const context = await requireAccessContext(c, deps);
263
+ const grant = context.workspaceGrants.find((candidate) => candidate.accountId === accountId);
264
+ if (!grant) {
265
+ throw new HTTPException(403, {
266
+ message: "local organization administration is not authorized",
267
+ });
268
+ }
269
+ const authorization = accessGrantAuthorizationFromContext(context, grant);
270
+ if (!authorization.canonicalLocalHumanSession) {
271
+ throw new HTTPException(401, { message: "organization administrator session required" });
272
+ }
273
+ requireAccountAdminAuthorizationStamp(authorization);
274
+ return { subjectId: context.subjectId, authorization };
275
+ }
276
+
190
277
  export async function requireAccessGrantAuthorization(
191
278
  c: Context,
192
279
  deps: AccessDeps,
@@ -204,7 +291,7 @@ async function accessGrantAuthorization(
204
291
  permission?: Permission,
205
292
  ): Promise<AccessGrantAuthorization> {
206
293
  const principalKind = hostedHumanSessionPrincipalKind(context);
207
- const grant =
294
+ let grant =
208
295
  context.workspaceGrants.find((candidate) => candidate.workspaceId === workspaceId) ??
209
296
  (await getWorkspaceGrant(
210
297
  deps.db,
@@ -217,7 +304,25 @@ async function accessGrantAuthorization(
217
304
  if (!workspace) {
218
305
  throw new HTTPException(404, { message: "workspace not found" });
219
306
  }
220
- throw new HTTPException(403, { message: "workspace access denied" });
307
+ const authority = accountScopedApiKeyWorkspaceAuthority(context);
308
+ const requiredWorkspacePermission = permission ?? "workspace:read";
309
+ if (
310
+ authority &&
311
+ workspace.accountId === authority.accountId &&
312
+ workspace.kind === "shared" &&
313
+ hasPermission(authority.permissions, requiredWorkspacePermission)
314
+ ) {
315
+ grant = {
316
+ workspaceId: workspace.id,
317
+ accountId: workspace.accountId,
318
+ subjectId: context.subjectId,
319
+ ...(context.subjectLabel ? { subjectLabel: context.subjectLabel } : {}),
320
+ permissions: authority.permissions,
321
+ principalKind: "api_key",
322
+ };
323
+ } else {
324
+ throw new HTTPException(403, { message: "workspace access denied" });
325
+ }
221
326
  }
222
327
  if (permission) {
223
328
  requirePermission(grant, permission);
@@ -262,6 +367,18 @@ function isCanonicalManagedHumanSession(context: AccessContext, grant: AccessGra
262
367
  );
263
368
  }
264
369
 
370
+ function isCanonicalLocalHumanSession(context: AccessContext, grant: AccessGrant): boolean {
371
+ return (
372
+ canonicalLocalHumanContexts.has(context) &&
373
+ context.mode === "local" &&
374
+ context.subjectId === "dev" &&
375
+ grant.subjectId === context.subjectId &&
376
+ grant.principalKind === "human_session" &&
377
+ grant.metadata?.delegated !== true &&
378
+ !grant.serviceInitiator
379
+ );
380
+ }
381
+
265
382
  function hostedHumanSessionPrincipalKind(context: AccessContext): "human_session" | undefined {
266
383
  if (context.mode !== "managed" || context.workspaceGrants.length === 0) {
267
384
  return undefined;
@@ -330,7 +447,7 @@ async function resolveAccessContext(c: Context, deps: AccessDeps): Promise<Acces
330
447
  if (delegated) {
331
448
  return delegated;
332
449
  }
333
- return await bootstrapWorkspace(deps.db, {
450
+ const context = await bootstrapWorkspace(deps.db, {
334
451
  accountExternalSource: "opengeni:local",
335
452
  accountExternalId: "default",
336
453
  accountName: "Local",
@@ -340,6 +457,8 @@ async function resolveAccessContext(c: Context, deps: AccessDeps): Promise<Acces
340
457
  subjectId: "dev",
341
458
  subjectLabel: "Local dev",
342
459
  });
460
+ canonicalLocalHumanContexts.add(context);
461
+ return context;
343
462
  }
344
463
 
345
464
  if (deps.settings.productAccessMode === "configured") {
@@ -423,8 +542,10 @@ async function apiKeyAccessContext(
423
542
  ? apiKey.permissions.filter(
424
543
  (permission) => permission === "billing:read" || permission === "billing:manage",
425
544
  )
426
- : apiKey.permissions;
427
- return {
545
+ : apiKey.permissions.filter((permission) =>
546
+ accountScopedApiKeyAccountPermissions.has(permission),
547
+ );
548
+ const context = {
428
549
  mode,
429
550
  subjectId,
430
551
  subjectLabel: apiKey.name,
@@ -451,6 +572,20 @@ async function apiKeyAccessContext(
451
572
  defaultAccountId: apiKey.accountId,
452
573
  defaultWorkspaceId: apiKey.workspaceId,
453
574
  } satisfies AccessContext;
575
+ if (apiKey.workspaceId === null && apiKey.credentialKind === "organization") {
576
+ accountScopedApiKeyContexts.set(
577
+ context,
578
+ Object.freeze({
579
+ accountId: apiKey.accountId,
580
+ permissions: Object.freeze(
581
+ apiKey.permissions.filter(
582
+ (permission) => !accountScopedApiKeyWorkspaceExcludedPermissions.has(permission),
583
+ ),
584
+ ),
585
+ }),
586
+ );
587
+ }
588
+ return context;
454
589
  }
455
590
 
456
591
  async function delegatedAccessContext(
@@ -6,6 +6,7 @@ import type {
6
6
  } from "@opengeni/contracts";
7
7
  import {
8
8
  issueSelfUserResourceGrant,
9
+ issueSelfLocalConnectionUseGrant,
9
10
  listSelfUserResourceAuthorities,
10
11
  revokeSelfUserResourceGrant,
11
12
  sessionTenancyProductActivated,
@@ -14,7 +15,10 @@ import {
14
15
  import { requirePermission, type AccessGrantAuthorization } from "../access";
15
16
  import type { AppDependencies } from "../dependencies";
16
17
  import { requireSessionAuthorization } from "../session-authorization";
17
- import { requireCanonicalManagedHuman } from "./session-tenancy";
18
+ import {
19
+ requireCanonicalManagedHuman,
20
+ SessionTenancyManagedHumanRequiredError,
21
+ } from "./session-tenancy";
18
22
 
19
23
  type UserResourceGrantDependencies = Pick<AppDependencies, "db" | "sessionAuthorization">;
20
24
 
@@ -53,7 +57,15 @@ function requireOwnerAuthority(
53
57
  workspaceId: string,
54
58
  permissions: readonly Permission[],
55
59
  ): void {
56
- requireCanonicalManagedHuman(authorization, workspaceId);
60
+ if (!authorization.canonicalLocalHumanSession) {
61
+ requireCanonicalManagedHuman(authorization, workspaceId);
62
+ } else if (
63
+ !authorization.contextIntegrity ||
64
+ authorization.authenticatedSubjectId !== authorization.grant.subjectId ||
65
+ authorization.grant.workspaceId !== workspaceId
66
+ ) {
67
+ throw new SessionTenancyManagedHumanRequiredError();
68
+ }
57
69
  for (const permission of permissions) requirePermission(authorization.grant, permission);
58
70
  }
59
71
 
@@ -90,6 +102,23 @@ export async function issueManagedHumanUserResourceGrant(
90
102
  ) {
91
103
  const modePermissions: Permission[] =
92
104
  request.mode === "session" ? ["sessions:control"] : ["sessions:create"];
105
+ if (authorization.canonicalLocalHumanSession) {
106
+ requireOwnerAuthority(authorization, workspaceId, [
107
+ ...ISSUE_PERMISSIONS[request.resourceKind],
108
+ "sessions:create",
109
+ ]);
110
+ if (request.resourceKind !== "connection" || request.mode !== "always") {
111
+ throw new SessionTenancyManagedHumanRequiredError();
112
+ }
113
+ return await issueSelfLocalConnectionUseGrant(deps.db, {
114
+ accountId: authorization.grant.accountId,
115
+ workspaceId,
116
+ subjectId: authorization.grant.subjectId,
117
+ authorityId,
118
+ context: request.context,
119
+ workspaceSharedAcknowledged: request.workspaceSharedAcknowledged,
120
+ });
121
+ }
93
122
  await requireOwnerProductGate(deps, authorization, workspaceId, [
94
123
  ...ISSUE_PERMISSIONS[request.resourceKind],
95
124
  ...modePermissions,
@@ -1,4 +1,8 @@
1
- import { configuredStaticUsageLimits, resolveModelProvider } from "@opengeni/config";
1
+ import {
2
+ configuredStaticUsageLimits,
3
+ resolveModelProviderForTurn,
4
+ type Settings,
5
+ } from "@opengeni/config";
2
6
  import type {
3
7
  LimitAction,
4
8
  LimitDecision,
@@ -8,6 +12,7 @@ import type {
8
12
  } from "@opengeni/contracts";
9
13
  import {
10
14
  countActiveApiKeysForWorkspace,
15
+ countActiveOrganizationApiKeysForAccount,
11
16
  countScheduledTasksForWorkspace,
12
17
  countWorkspacesForAccount,
13
18
  getBillingBalance,
@@ -25,14 +30,35 @@ export type LimitCheckInput = {
25
30
  workspaceId?: string;
26
31
  action: LimitAction;
27
32
  quantity?: number;
28
- // The turn's model id, when the action represents an agent turn. Externally
29
- // billed models consume ZERO OpenGeni credits, so the credit-balance +
30
- // model-cost + token gates are skipped. Connected subscriptions still require
31
- // live workspace readiness; an explicitly anonymous deployment route is
32
- // statically ready by definition. Non-model infra actions leave this undefined.
33
+ // The turn's model id, when the action represents an agent turn. The model's
34
+ // deployment cost controls credit/cost gates; upstream metering independently
35
+ // controls the token cap. Connected subscriptions still require live workspace
36
+ // readiness. Non-model infra actions leave this undefined.
33
37
  model?: string | null;
34
38
  };
35
39
 
40
+ export function modelFundingForAdmission(
41
+ settings: Settings,
42
+ model: string | null | undefined,
43
+ codexBilled: boolean,
44
+ ): { fundedWithoutCredits: boolean; countsTowardTokenCap: boolean } {
45
+ const resolvedModel = model ? resolveModelProviderForTurn(settings, model)?.model : null;
46
+ const codexSubscriptionModel =
47
+ resolvedModel?.credentialSource.kind === "connected_subscription" &&
48
+ resolvedModel.credentialSource.provider === "codex";
49
+ return {
50
+ // A Codex namespace/definition never bypasses credits by itself: the live
51
+ // workspace credential predicate above remains authoritative. SuperGrok's
52
+ // static overlay is likewise secret-free; its worker/provider admission
53
+ // owns live account selection before any upstream request can occur.
54
+ fundedWithoutCredits:
55
+ codexBilled ||
56
+ (resolvedModel != null && !codexSubscriptionModel && resolvedModel.cost !== "credits"),
57
+ countsTowardTokenCap:
58
+ !codexBilled && resolvedModel != null && resolvedModel.billing.upstreamPayer === "deployment",
59
+ };
60
+ }
61
+
36
62
  export async function requireLimit(deps: LimitDependencies, input: LimitCheckInput): Promise<void> {
37
63
  const decision = await checkLimit(deps, input);
38
64
  if (decision.allowed) {
@@ -58,25 +84,22 @@ export async function checkLimit(
58
84
  model: input.model,
59
85
  })
60
86
  : false;
61
- const credentialFreeExternal = input.model
62
- ? (() => {
63
- const resolved = resolveModelProvider(deps.settings, input.model);
64
- return (
65
- resolved?.model.billing.metering === "external" &&
66
- resolved.model.credentialSource.kind === "deployment" &&
67
- resolved.model.credentialSource.mechanism === "none"
68
- );
69
- })()
70
- : false;
71
- const externallyBilled = codexBilled || credentialFreeExternal;
72
- const creditDecision = await checkCreditBalance(deps, input, externallyBilled);
87
+ const { fundedWithoutCredits, countsTowardTokenCap } = modelFundingForAdmission(
88
+ deps.settings,
89
+ input.model,
90
+ codexBilled,
91
+ );
92
+ const creditDecision = await checkCreditBalance(deps, input, fundedWithoutCredits);
73
93
  if (!creditDecision.allowed) {
74
94
  return creditDecision;
75
95
  }
76
96
  if (deps.settings.usageLimitsMode !== "static" && deps.settings.usageLimitsMode !== "managed") {
77
97
  return { allowed: true };
78
98
  }
79
- return await checkStaticCaps(deps, input, externallyBilled);
99
+ return await checkStaticCaps(deps, input, {
100
+ fundedWithoutCredits,
101
+ countsTowardTokenCap,
102
+ });
80
103
  }
81
104
 
82
105
  async function checkCreditBalance(
@@ -100,10 +123,14 @@ async function checkCreditBalance(
100
123
  async function checkStaticCaps(
101
124
  deps: LimitDependencies,
102
125
  input: LimitCheckInput,
103
- externallyBilled: boolean,
126
+ funding: { fundedWithoutCredits: boolean; countsTowardTokenCap: boolean },
104
127
  ): Promise<LimitDecision> {
105
128
  const limits = configuredStaticUsageLimits(deps.settings);
106
- if (limits.maxMonthlyCostMicrosPerAccount && isCostlyAction(input.action) && !externallyBilled) {
129
+ if (
130
+ limits.maxMonthlyCostMicrosPerAccount &&
131
+ isCostlyAction(input.action) &&
132
+ !funding.fundedWithoutCredits
133
+ ) {
107
134
  const used = await sumUsageQuantity(deps.db, {
108
135
  accountId: input.accountId,
109
136
  eventType: "model.cost",
@@ -130,10 +157,12 @@ async function checkStaticCaps(
130
157
  );
131
158
  }
132
159
  case "api_key:create": {
133
- if (!limits.maxApiKeysPerWorkspace || !input.workspaceId) {
160
+ if (!limits.maxApiKeysPerWorkspace) {
134
161
  return { allowed: true };
135
162
  }
136
- const count = await countActiveApiKeysForWorkspace(deps.db, input.workspaceId);
163
+ const count = input.workspaceId
164
+ ? await countActiveApiKeysForWorkspace(deps.db, input.workspaceId)
165
+ : await countActiveOrganizationApiKeysForAccount(deps.db, input.accountId);
137
166
  return count < limits.maxApiKeysPerWorkspace
138
167
  ? { allowed: true }
139
168
  : blocked(
@@ -182,7 +211,11 @@ async function checkStaticCaps(
182
211
  );
183
212
  }
184
213
  case "tokens:consume": {
185
- if (externallyBilled || !limits.maxMonthlyTokensPerWorkspace || !input.workspaceId) {
214
+ if (
215
+ !funding.countsTowardTokenCap ||
216
+ !limits.maxMonthlyTokensPerWorkspace ||
217
+ !input.workspaceId
218
+ ) {
186
219
  return { allowed: true };
187
220
  }
188
221
  const used = await sumUsageQuantity(deps.db, {