@opengeni/core 2.5.3 → 2.6.3-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 (39) hide show
  1. package/dist/access/index.d.ts +24 -0
  2. package/dist/canonical-human-identities.js +2 -2
  3. package/dist/{chunk-ZVZJTMSV.js → chunk-OF65T3PM.js} +2 -2
  4. package/dist/{chunk-YGOMUGYS.js → chunk-QO5GVFFO.js} +17 -8
  5. package/dist/{chunk-YGOMUGYS.js.map → chunk-QO5GVFFO.js.map} +1 -1
  6. package/dist/domain/company-brain-governed-writes.d.ts +15 -6
  7. package/dist/domain/company-profile-agent-admin.d.ts +3 -2
  8. package/dist/domain/environments.d.ts +1 -1
  9. package/dist/domain/memory-slack-delivery.d.ts +4 -1
  10. package/dist/domain/personal-connection-delegations.d.ts +1 -0
  11. package/dist/domain/pr-review.d.ts +1 -1
  12. package/dist/domain/scheduled-tasks.d.ts +12 -0
  13. package/dist/domain/sessions.d.ts +14 -1
  14. package/dist/domain/workspace-members.d.ts +8 -1
  15. package/dist/index.js +686 -380
  16. package/dist/index.js.map +1 -1
  17. package/dist/managed-auth-session-sets.d.ts +18 -0
  18. package/dist/managed-auth-session-sets.js +3 -1
  19. package/dist/sandbox/fleet.d.ts +6 -4
  20. package/dist/sandbox/routing.d.ts +7 -2
  21. package/dist/sandbox/runtime-settings.d.ts +17 -1
  22. package/package.json +10 -10
  23. package/src/access/index.ts +140 -5
  24. package/src/application/user-resource-grants.ts +31 -2
  25. package/src/billing/limits.ts +5 -2
  26. package/src/domain/company-brain-governed-writes.ts +29 -13
  27. package/src/domain/company-profile-agent-admin.ts +3 -2
  28. package/src/domain/environments.ts +6 -34
  29. package/src/domain/memory-slack-delivery.ts +30 -0
  30. package/src/domain/personal-connection-delegations.ts +40 -7
  31. package/src/domain/remember.ts +5 -6
  32. package/src/domain/scheduled-tasks.ts +94 -0
  33. package/src/domain/sessions.ts +117 -9
  34. package/src/domain/workspace-members.ts +34 -2
  35. package/src/managed-auth-session-sets.ts +38 -11
  36. package/src/sandbox/fleet.ts +20 -17
  37. package/src/sandbox/routing.ts +18 -4
  38. package/src/sandbox/runtime-settings.ts +32 -0
  39. /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,
@@ -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.3-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.0",
58
+ "@opengeni/config": "^0.22.5-canary.0",
59
+ "@opengeni/contracts": "^2.9.2-canary.0",
60
+ "@opengeni/db": "^3.7.3-canary.0",
61
+ "@opengeni/documents": "^0.8.13-canary.0",
62
+ "@opengeni/events": "^0.4.11-canary.0",
63
+ "@opengeni/observability": "^0.8.14-canary.0",
64
+ "@opengeni/runtime": "^2.0.1-canary.0",
65
+ "@opengeni/storage": "^0.2.115-canary.0",
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,
@@ -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 || !input.workspaceId) {
134
+ if (!limits.maxApiKeysPerWorkspace) {
134
135
  return { allowed: true };
135
136
  }
136
- const count = await countActiveApiKeysForWorkspace(deps.db, input.workspaceId);
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 preferences only: mandatory instruction policy keeps
49
- * requiring human-authoritative activation even under `automatic`, so its
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. By default only preferences may
147
- * activate automatically; mandatory instruction policy always keeps a human
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
- try {
301
- await notifyActivation({
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
- } catch {
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, canonical human confirmation,
25
- * tenant isolation, CAS, and immutable receipts.
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 type { AccessGrant, VariableSet } from "@opengeni/contracts";
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 (reservedExactNames.has(name) || reservedPrefixes.some((prefix) => name.startsWith(prefix))) {
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,