@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.
- package/dist/access/index.d.ts +24 -0
- package/dist/billing/limits.d.ts +5 -0
- package/dist/canonical-human-identities.js +2 -2
- package/dist/{chunk-ZVZJTMSV.js → chunk-OF65T3PM.js} +2 -2
- package/dist/{chunk-YGOMUGYS.js → chunk-QO5GVFFO.js} +17 -8
- package/dist/{chunk-YGOMUGYS.js.map → chunk-QO5GVFFO.js.map} +1 -1
- package/dist/dependencies.d.ts +10 -1
- package/dist/domain/company-brain-governed-writes.d.ts +15 -6
- package/dist/domain/company-profile-agent-admin.d.ts +3 -2
- package/dist/domain/environments.d.ts +1 -1
- package/dist/domain/memory-slack-delivery.d.ts +4 -1
- package/dist/domain/personal-connection-delegations.d.ts +1 -0
- package/dist/domain/pr-review.d.ts +1 -1
- package/dist/domain/scheduled-tasks.d.ts +12 -0
- package/dist/domain/sessions.d.ts +37 -11
- package/dist/domain/workspace-members.d.ts +8 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1706 -667
- package/dist/index.js.map +1 -1
- package/dist/managed-auth-session-sets.d.ts +18 -0
- package/dist/managed-auth-session-sets.js +3 -1
- package/dist/model-catalog.d.ts +89 -0
- package/dist/sandbox/fleet.d.ts +6 -4
- package/dist/sandbox/routing.d.ts +7 -2
- package/dist/sandbox/runtime-settings.d.ts +17 -1
- package/package.json +10 -10
- package/src/access/index.ts +140 -5
- package/src/application/user-resource-grants.ts +31 -2
- package/src/billing/limits.ts +57 -24
- package/src/dependencies.ts +15 -1
- package/src/domain/company-brain-governed-writes.ts +29 -13
- package/src/domain/company-profile-agent-admin.ts +3 -2
- package/src/domain/environments.ts +6 -34
- package/src/domain/memory-slack-delivery.ts +30 -0
- package/src/domain/personal-connection-delegations.ts +40 -7
- package/src/domain/remember.ts +5 -6
- package/src/domain/scheduled-tasks.ts +146 -1
- package/src/domain/sessions.ts +912 -358
- package/src/domain/workspace-members.ts +34 -2
- package/src/index.ts +1 -0
- package/src/managed-auth-session-sets.ts +38 -11
- package/src/model-catalog.ts +565 -0
- package/src/sandbox/fleet.ts +20 -17
- package/src/sandbox/routing.ts +18 -4
- package/src/sandbox/runtime-settings.ts +32 -0
- /package/dist/{chunk-ZVZJTMSV.js.map → chunk-OF65T3PM.js.map} +0 -0
|
@@ -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-
|
|
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[];
|
package/dist/sandbox/fleet.d.ts
CHANGED
|
@@ -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
|
|
32
|
-
sessionBackend:
|
|
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
|
|
126
|
-
*
|
|
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.
|
|
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.
|
|
58
|
-
"@opengeni/config": "^0.22.
|
|
59
|
-
"@opengeni/contracts": "^2.
|
|
60
|
-
"@opengeni/db": "^3.
|
|
61
|
-
"@opengeni/documents": "^0.8.
|
|
62
|
-
"@opengeni/events": "^0.4.
|
|
63
|
-
"@opengeni/observability": "^0.8.
|
|
64
|
-
"@opengeni/runtime": "^1.
|
|
65
|
-
"@opengeni/storage": "^0.2.
|
|
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
|
},
|
package/src/access/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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,
|
package/src/billing/limits.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
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.
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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,
|
|
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
|
-
|
|
126
|
+
funding: { fundedWithoutCredits: boolean; countsTowardTokenCap: boolean },
|
|
104
127
|
): Promise<LimitDecision> {
|
|
105
128
|
const limits = configuredStaticUsageLimits(deps.settings);
|
|
106
|
-
if (
|
|
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
|
|
160
|
+
if (!limits.maxApiKeysPerWorkspace) {
|
|
134
161
|
return { allowed: true };
|
|
135
162
|
}
|
|
136
|
-
const count =
|
|
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 (
|
|
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, {
|