@opengeni/core 2.5.3 → 2.6.4
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/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/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 +14 -1
- package/dist/domain/workspace-members.d.ts +8 -1
- package/dist/index.js +686 -380
- 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/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 +5 -2
- 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 +94 -0
- package/src/domain/sessions.ts +117 -9
- package/src/domain/workspace-members.ts +34 -2
- package/src/managed-auth-session-sets.ts +38 -11
- 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,
|
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",
|
|
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": "^
|
|
65
|
-
"@opengeni/storage": "^0.2.
|
|
57
|
+
"@opengeni/codex": "^0.2.20",
|
|
58
|
+
"@opengeni/config": "^0.22.5",
|
|
59
|
+
"@opengeni/contracts": "^2.9.2",
|
|
60
|
+
"@opengeni/db": "^3.7.4",
|
|
61
|
+
"@opengeni/documents": "^0.8.14",
|
|
62
|
+
"@opengeni/events": "^0.4.12",
|
|
63
|
+
"@opengeni/observability": "^0.8.14",
|
|
64
|
+
"@opengeni/runtime": "^2.0.1",
|
|
65
|
+
"@opengeni/storage": "^0.2.115",
|
|
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
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
} from "@opengeni/contracts";
|
|
9
9
|
import {
|
|
10
10
|
countActiveApiKeysForWorkspace,
|
|
11
|
+
countActiveOrganizationApiKeysForAccount,
|
|
11
12
|
countScheduledTasksForWorkspace,
|
|
12
13
|
countWorkspacesForAccount,
|
|
13
14
|
getBillingBalance,
|
|
@@ -130,10 +131,12 @@ async function checkStaticCaps(
|
|
|
130
131
|
);
|
|
131
132
|
}
|
|
132
133
|
case "api_key:create": {
|
|
133
|
-
if (!limits.maxApiKeysPerWorkspace
|
|
134
|
+
if (!limits.maxApiKeysPerWorkspace) {
|
|
134
135
|
return { allowed: true };
|
|
135
136
|
}
|
|
136
|
-
const count =
|
|
137
|
+
const count = input.workspaceId
|
|
138
|
+
? await countActiveApiKeysForWorkspace(deps.db, input.workspaceId)
|
|
139
|
+
: await countActiveOrganizationApiKeysForAccount(deps.db, input.accountId);
|
|
137
140
|
return count < limits.maxApiKeysPerWorkspace
|
|
138
141
|
? { allowed: true }
|
|
139
142
|
: blocked(
|
|
@@ -45,9 +45,8 @@ export type CompanyBrainGovernedWriteRouterOptions = {
|
|
|
45
45
|
activate?: typeof activateGovernedLearningDecision;
|
|
46
46
|
/**
|
|
47
47
|
* Destinations the router may activate automatically from a final eligible
|
|
48
|
-
* decision. Defaults to
|
|
49
|
-
*
|
|
50
|
-
* decision receipt is recorded but the inactive draft stays for review.
|
|
48
|
+
* decision. Defaults to Skills and instruction policy. Both still pass
|
|
49
|
+
* through the evaluator and destination-owned activation lifecycle.
|
|
51
50
|
*/
|
|
52
51
|
automaticDestinations?: ReadonlyArray<GovernedLearningActivationDestination>;
|
|
53
52
|
/**
|
|
@@ -64,7 +63,27 @@ export type CompanyBrainGovernedWriteRouterOptions = {
|
|
|
64
63
|
};
|
|
65
64
|
|
|
66
65
|
export const DEFAULT_AUTOMATIC_LEARNING_DESTINATIONS: ReadonlyArray<GovernedLearningActivationDestination> =
|
|
67
|
-
["preference"];
|
|
66
|
+
["preference", "instruction_policy"];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Start a non-authoritative post-activation notification without making the
|
|
70
|
+
* durable activation receipt wait for its settlement. The callback is invoked
|
|
71
|
+
* synchronously so immediate enqueue work begins before the router returns;
|
|
72
|
+
* both synchronous throws and later promise rejections are contained.
|
|
73
|
+
*
|
|
74
|
+
* Exact retries intentionally dispatch again: the publication sink owns an
|
|
75
|
+
* activation-receipt idempotency key, while the activation receipt remains the
|
|
76
|
+
* caller-facing source of truth even if notification delivery stalls forever.
|
|
77
|
+
*/
|
|
78
|
+
export function dispatchBestEffortGovernedLearningNotification(
|
|
79
|
+
notify: () => Promise<unknown>,
|
|
80
|
+
): void {
|
|
81
|
+
try {
|
|
82
|
+
void notify().catch(() => undefined);
|
|
83
|
+
} catch {
|
|
84
|
+
// Notification is best-effort; the durable activation receipt already exists.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
68
87
|
|
|
69
88
|
/**
|
|
70
89
|
* Transport-neutral facade for explicit governed Company Brain proposals.
|
|
@@ -143,9 +162,8 @@ function classifyLearningFailure(
|
|
|
143
162
|
* snapshot. `suggest` records the content-free decision receipt only. Under
|
|
144
163
|
* `automatic`, a final `automaticEligible` receipt is handed to the activation
|
|
145
164
|
* controller, which revalidates current authority and applies the change only
|
|
146
|
-
* through the destination-owned lifecycle.
|
|
147
|
-
* activate automatically
|
|
148
|
-
* activation boundary. Evaluation or activation failure
|
|
165
|
+
* through the destination-owned lifecycle. Eligible Skills and instruction
|
|
166
|
+
* policies may activate automatically. Evaluation or activation failure
|
|
149
167
|
* never rolls back the durable proposal; it is reported as a bounded
|
|
150
168
|
* `learningFailure` and the proposal remains for human review.
|
|
151
169
|
*/
|
|
@@ -297,16 +315,14 @@ export function createCompanyBrainLearningPolicyRouter(
|
|
|
297
315
|
learningFailure: classifyLearningFailure("activation", error),
|
|
298
316
|
});
|
|
299
317
|
}
|
|
300
|
-
|
|
301
|
-
|
|
318
|
+
dispatchBestEffortGovernedLearningNotification(() =>
|
|
319
|
+
notifyActivation({
|
|
302
320
|
db: options.db,
|
|
303
321
|
receipt: activation,
|
|
304
322
|
sessionId: attempt.sessionId,
|
|
305
323
|
attemptId: attempt.attemptId,
|
|
306
|
-
})
|
|
307
|
-
|
|
308
|
-
// Notification is best-effort; the durable receipts already exist.
|
|
309
|
-
}
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
310
326
|
return CompanyBrainLearningPolicyRouteReceipt.parse({
|
|
311
327
|
operationId: request.operationId,
|
|
312
328
|
workspaceId: attempt.workspaceId,
|
|
@@ -21,8 +21,9 @@ export type CompanyProfileAgentAdminRouterOptions = {
|
|
|
21
21
|
/**
|
|
22
22
|
* Explicit organization administration is intentionally separate from derived
|
|
23
23
|
* workspace learning. The database capabilities own exact-attempt admission,
|
|
24
|
-
* current organization-owner authority,
|
|
25
|
-
* tenant isolation, CAS, and immutable
|
|
24
|
+
* current organization-owner authority, the separate organization policy,
|
|
25
|
+
* canonical review when required, tenant isolation, CAS, and immutable
|
|
26
|
+
* receipts.
|
|
26
27
|
*/
|
|
27
28
|
export function createCompanyProfileAgentAdminRouter(
|
|
28
29
|
options: CompanyProfileAgentAdminRouterOptions,
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { environmentsEncryptionKeyBytes, type Settings } from "@opengeni/config";
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
variableSetVariableNameReservation,
|
|
4
|
+
type AccessGrant,
|
|
5
|
+
type VariableSet,
|
|
6
|
+
} from "@opengeni/contracts";
|
|
3
7
|
import { getVariableSet, recordAuditEvent, type Database } from "@opengeni/db";
|
|
4
8
|
import { HTTPException } from "hono/http-exception";
|
|
5
9
|
import { requirePermission } from "../access";
|
|
@@ -11,40 +15,8 @@ export const MAX_VARIABLES_PER_ENVIRONMENT = 100;
|
|
|
11
15
|
// collectGitIdentityEnvironment) plus loader/startup-injection vectors. These
|
|
12
16
|
// can never be set as variable set variables, so the run-scoped
|
|
13
17
|
// git auth block and git identity always win without silent collisions.
|
|
14
|
-
const reservedExactNames = new Set([
|
|
15
|
-
"HOME",
|
|
16
|
-
"PATH",
|
|
17
|
-
"SHELL",
|
|
18
|
-
"USER",
|
|
19
|
-
"LOGNAME",
|
|
20
|
-
"TMPDIR",
|
|
21
|
-
"IFS",
|
|
22
|
-
"ENV",
|
|
23
|
-
"BASH_ENV",
|
|
24
|
-
"NODE_OPTIONS",
|
|
25
|
-
"PYTHONPATH",
|
|
26
|
-
"PYTHONSTARTUP",
|
|
27
|
-
"PERL5OPT",
|
|
28
|
-
"PERL5LIB",
|
|
29
|
-
"GH_TOKEN",
|
|
30
|
-
"GITHUB_TOKEN",
|
|
31
|
-
"GITLAB_TOKEN",
|
|
32
|
-
"AZURE_DEVOPS_EXT_PAT",
|
|
33
|
-
"GIT_ASKPASS",
|
|
34
|
-
"GIT_TERMINAL_PROMPT",
|
|
35
|
-
]);
|
|
36
|
-
|
|
37
|
-
const reservedPrefixes = [
|
|
38
|
-
"OPENGENI_",
|
|
39
|
-
"GIT_CONFIG_",
|
|
40
|
-
"GIT_AUTHOR_",
|
|
41
|
-
"GIT_COMMITTER_",
|
|
42
|
-
"LD_",
|
|
43
|
-
"DYLD_",
|
|
44
|
-
];
|
|
45
|
-
|
|
46
18
|
export function assertAllowedVariableSetVariableName(name: string): void {
|
|
47
|
-
if (
|
|
19
|
+
if (variableSetVariableNameReservation(name)) {
|
|
48
20
|
throw new HTTPException(422, {
|
|
49
21
|
message: `reserved variable set variable name / reserved environment variable name: ${name}`,
|
|
50
22
|
});
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { MemorySlackPublicationDistribution } from "@opengeni/contracts";
|
|
2
2
|
import {
|
|
3
|
+
correctWorkspaceMemory,
|
|
3
4
|
enqueueMemorySlackPublication,
|
|
4
5
|
getCurrentMemorySlackPublicationConfiguration,
|
|
5
6
|
getWorkspaceMemorySlackPublicationSnapshot,
|
|
6
7
|
saveWorkspaceMemory,
|
|
7
8
|
withWorkspaceRls,
|
|
9
|
+
type CorrectWorkspaceMemoryInput,
|
|
10
|
+
type CorrectWorkspaceMemoryResult,
|
|
8
11
|
type Database,
|
|
9
12
|
type EnqueueMemorySlackPublicationResult,
|
|
10
13
|
type MemoryEmbedder,
|
|
@@ -46,6 +49,33 @@ export async function saveWorkspaceMemoryWithSlackPublication(
|
|
|
46
49
|
});
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
export async function correctWorkspaceMemoryWithSlackPublication(
|
|
53
|
+
db: Database,
|
|
54
|
+
input: CorrectWorkspaceMemoryInput,
|
|
55
|
+
publication: MemorySlackPublicationCommitRequest | null,
|
|
56
|
+
embedder?: MemoryEmbedder,
|
|
57
|
+
): Promise<
|
|
58
|
+
CorrectWorkspaceMemoryResult & { slackPublication: MemorySlackPublicationCommitResult }
|
|
59
|
+
> {
|
|
60
|
+
return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
|
|
61
|
+
const result = await correctWorkspaceMemory(scopedDb, input, embedder);
|
|
62
|
+
if (!publication || result.action !== "superseded" || !result.replacement) {
|
|
63
|
+
return { ...result, slackPublication: { decision: null, enqueue: null } };
|
|
64
|
+
}
|
|
65
|
+
const replacement = await requiredSnapshot(scopedDb, input.workspaceId, result.replacement.id);
|
|
66
|
+
const slackPublication = await evaluateAndEnqueue(scopedDb, {
|
|
67
|
+
accountId: input.accountId,
|
|
68
|
+
workspaceId: input.workspaceId,
|
|
69
|
+
snapshot: replacement,
|
|
70
|
+
changeKind: "corrected",
|
|
71
|
+
relatedMemoryId: result.memory.id,
|
|
72
|
+
occurredAt: result.replacement.updatedAt,
|
|
73
|
+
publication,
|
|
74
|
+
});
|
|
75
|
+
return { ...result, slackPublication };
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
49
79
|
async function publishSavedMemoryMutation(
|
|
50
80
|
db: Database,
|
|
51
81
|
input: SaveWorkspaceMemoryInput,
|