@opengeni/core 1.0.1 → 2.2.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access/index.d.ts +37 -0
- package/dist/application/new-session-drafts.d.ts +8 -1
- package/dist/application/session-commands.d.ts +18 -11
- package/dist/application/session-tenancy.d.ts +18 -0
- package/dist/application/user-resource-grants.d.ts +20 -0
- package/dist/{chunk-6WKPWE5S.js → chunk-H7X52RI2.js} +27 -18
- package/dist/chunk-H7X52RI2.js.map +1 -0
- package/dist/{chunk-JJU6I5XD.js → chunk-VCZFFEPB.js} +21 -9
- package/dist/chunk-VCZFFEPB.js.map +1 -0
- package/dist/{chunk-NYPMQ7NO.js → chunk-ZYCNLYGH.js} +29 -27
- package/dist/chunk-ZYCNLYGH.js.map +1 -0
- package/dist/dependencies.d.ts +12 -1
- package/dist/domain/automations.d.ts +51 -0
- package/dist/domain/company-brain-governed-writes.d.ts +70 -0
- package/dist/domain/company-profile-agent-admin.d.ts +24 -0
- package/dist/domain/connection-authority.d.ts +97 -0
- package/dist/domain/connection-authority.js +169 -0
- package/dist/domain/connection-authority.js.map +1 -0
- package/dist/domain/editable-artifacts/service.d.ts +1 -2
- package/dist/domain/environments.d.ts +1 -1
- package/dist/domain/github-repository-bindings.d.ts +117 -0
- package/dist/domain/governed-learning-activation.d.ts +16 -0
- package/dist/domain/governed-learning-evaluator.d.ts +6 -0
- package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
- package/dist/domain/memory-slack-delivery.d.ts +1 -4
- package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
- package/dist/domain/packs.d.ts +5 -1
- package/dist/domain/personal-connection-delegations.d.ts +34 -2
- package/dist/domain/pr-review.d.ts +149 -0
- package/dist/domain/remember.d.ts +69 -0
- package/dist/domain/resources.d.ts +22 -1
- package/dist/domain/scheduled-tasks.d.ts +43 -2
- package/dist/domain/session-tool-policy.d.ts +1 -1
- package/dist/domain/sessions.d.ts +40 -6
- package/dist/editable-artifact-live/application.d.ts +10 -2
- package/dist/editable-artifact-live/ports.d.ts +0 -1
- package/dist/editable-artifact-live/server.d.ts +0 -1
- package/dist/editable-artifact-live/ticket.d.ts +0 -1
- package/dist/editable-artifact-live/types.d.ts +5 -2
- package/dist/editable-artifact-live.js +2 -2
- package/dist/editable-artifacts.js +2 -4
- package/dist/index.d.ts +13 -1
- package/dist/index.js +4827 -1164
- package/dist/index.js.map +1 -1
- package/dist/rigs/index.d.ts +22 -5
- package/dist/sandbox/fleet.d.ts +34 -6
- package/dist/sandbox/routing.d.ts +15 -1
- package/dist/sandbox/runtime-settings.d.ts +39 -0
- package/package.json +16 -11
- package/src/access/index.ts +157 -31
- package/src/application/new-session-drafts.ts +24 -2
- package/src/application/session-commands.ts +305 -71
- package/src/application/session-tenancy.ts +321 -0
- package/src/application/user-resource-grants.ts +140 -0
- package/src/billing/limits.ts +25 -15
- package/src/dependencies.ts +12 -1
- package/src/domain/automations.ts +196 -0
- package/src/domain/capabilities.ts +7 -1
- package/src/domain/company-brain-governed-writes.ts +330 -0
- package/src/domain/company-profile-agent-admin.ts +55 -0
- package/src/domain/connection-authority.ts +329 -0
- package/src/domain/editable-artifacts/agent-application.ts +17 -9
- package/src/domain/editable-artifacts/service.ts +12 -1
- package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
- package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
- package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
- package/src/domain/environments.ts +16 -3
- package/src/domain/github-repository-bindings.ts +295 -0
- package/src/domain/governed-learning-activation.ts +44 -0
- package/src/domain/governed-learning-evaluator.ts +49 -0
- package/src/domain/governed-learning-slack-publication.ts +157 -0
- package/src/domain/insights.ts +9 -0
- package/src/domain/memory-slack-delivery.ts +0 -30
- package/src/domain/organization-membership-lifecycle.ts +9 -0
- package/src/domain/packs.ts +106 -3
- package/src/domain/personal-connection-delegations.ts +532 -51
- package/src/domain/pr-review.ts +671 -0
- package/src/domain/remember.ts +549 -0
- package/src/domain/resources.ts +131 -4
- package/src/domain/scheduled-tasks.ts +497 -76
- package/src/domain/session-tool-policy.ts +12 -9
- package/src/domain/sessions.ts +613 -166
- package/src/editable-artifact-live/application.ts +21 -3
- package/src/editable-artifact-live/ports.ts +0 -1
- package/src/editable-artifact-live/server.ts +13 -7
- package/src/editable-artifact-live/ticket.ts +1 -5
- package/src/editable-artifact-live/types.ts +6 -2
- package/src/editable-artifact-live/wire.ts +0 -10
- package/src/index.ts +13 -1
- package/src/rigs/index.ts +165 -48
- package/src/sandbox/fleet.ts +376 -30
- package/src/sandbox/routing.ts +91 -29
- package/src/sandbox/runtime-settings.ts +274 -0
- package/src/session-authorization.ts +17 -49
- package/dist/chunk-6WKPWE5S.js.map +0 -1
- package/dist/chunk-JJU6I5XD.js.map +0 -1
- package/dist/chunk-NYPMQ7NO.js.map +0 -1
- package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
- package/src/domain/durable-learning-slack-publication.ts +0 -202
package/dist/rigs/index.d.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import type { AccessGrant, CreateRigRequest, RigDefinitionEditPayload, ProposeRigChangeRequest, Rig, RigChange, RigVersion, UpdateRigRequest } from "@opengeni/contracts";
|
|
1
|
+
import type { AccessGrant, CreateRigRequest, RigDefinitionEditPayload, ProposeRigChangeRequest, Rig, RigChange, ResourceAuthorityScope, RigVersion, UpdateRigRequest } from "@opengeni/contracts";
|
|
2
2
|
import { type Database } from "@opengeni/db";
|
|
3
3
|
export * from "./provider-images.js";
|
|
4
4
|
export declare const MAX_RIGS_PER_WORKSPACE = 50;
|
|
5
5
|
export declare const MAX_CHECKS_PER_RIG = 100;
|
|
6
6
|
export declare const MAX_CREDENTIAL_HOOKS_PER_RIG = 50;
|
|
7
7
|
export declare const MAX_DEFAULT_VARIABLE_SETS_PER_RIG = 25;
|
|
8
|
+
export declare const RIG_DEFAULT_VARIABLE_SET_LOAD_CONCURRENCY = 4;
|
|
9
|
+
type VariableSetEnvironment = {
|
|
10
|
+
values: Record<string, string>;
|
|
11
|
+
} | null;
|
|
12
|
+
/** Load complete rig defaults concurrently while preserving listed precedence. */
|
|
13
|
+
export declare function loadRigDefaultVariableSetEnvironment(variableSetIds: readonly string[], load: (variableSetId: string) => Promise<VariableSetEnvironment>): Promise<Record<string, string>>;
|
|
14
|
+
/** Rig defaults layer below the session's explicitly selected variable set. */
|
|
15
|
+
export declare function mergeRigDefaultVariableSetEnvironment(rigDefaultValues: Record<string, string>, sessionValues: Record<string, string>): Record<string, string>;
|
|
8
16
|
export type RigServices = {
|
|
9
17
|
db: Database;
|
|
10
18
|
};
|
|
@@ -16,11 +24,20 @@ export declare function recordRigAuditEvent(db: Database, input: {
|
|
|
16
24
|
metadata?: Record<string, unknown>;
|
|
17
25
|
}): Promise<void>;
|
|
18
26
|
export declare function rigActorForGrant(grant: AccessGrant): string;
|
|
19
|
-
export declare function requireRigForApi(db: Database,
|
|
27
|
+
export declare function requireRigForApi(db: Database, grant: AccessGrant, rigId: string): Promise<Rig>;
|
|
20
28
|
export declare function requireRigChangeForApi(db: Database, workspaceId: string, rigId: string, changeId: string): Promise<RigChange>;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
export declare function
|
|
29
|
+
/** A resource may depend only on Variable Sets whose audience is no narrower
|
|
30
|
+
* than its own. User-owned rigs may use anything visible to their owner. */
|
|
31
|
+
export declare function variableSetScopeAllowedForRig(rigScope: ResourceAuthorityScope, variableSetScope: ResourceAuthorityScope): boolean;
|
|
32
|
+
export declare function createRigForApi(deps: RigServices, grant: AccessGrant, payload: CreateRigRequest, options?: {
|
|
33
|
+
allowOrganization?: boolean;
|
|
34
|
+
}): Promise<Rig>;
|
|
35
|
+
export declare function updateRigForApi(deps: RigServices, grant: AccessGrant, rig: Rig, payload: UpdateRigRequest, options?: {
|
|
36
|
+
allowOrganization?: boolean;
|
|
37
|
+
}): Promise<Rig>;
|
|
38
|
+
export declare function deleteRigForApi(deps: RigServices, grant: AccessGrant, rig: Rig, options?: {
|
|
39
|
+
allowOrganization?: boolean;
|
|
40
|
+
}): Promise<void>;
|
|
24
41
|
export declare function proposeRigChangeForApi(deps: RigServices, grant: AccessGrant, rig: Rig, request: ProposeRigChangeRequest, options?: {
|
|
25
42
|
proposedBy?: string;
|
|
26
43
|
}): Promise<RigChange>;
|
package/dist/sandbox/fleet.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Settings } from "@opengeni/config";
|
|
2
2
|
import { type Database, type SandboxRecord } from "@opengeni/db";
|
|
3
3
|
import type { EventBus } from "@opengeni/events";
|
|
4
|
-
import { type BackendUnresolvableCode, type ControlRpc, type SelfhostedRelayConfig, type SelfhostedOpStreamDeps } from "@opengeni/runtime/sandbox";
|
|
4
|
+
import { type BackendUnresolvableCode, type ControlRpc, type SelfhostedRelayConfig, type SelfhostedOpStreamDeps, type SelfhostedOperationAdmission, type SelfhostedOperationResourcePolicy } from "@opengeni/runtime/sandbox";
|
|
5
5
|
export type FleetServices = {
|
|
6
6
|
db: Database;
|
|
7
7
|
settings: Settings;
|
|
@@ -18,6 +18,13 @@ export type FleetReadinessHold = {
|
|
|
18
18
|
export type FleetContext = {
|
|
19
19
|
accountId: string;
|
|
20
20
|
workspaceId: string;
|
|
21
|
+
subjectId?: string;
|
|
22
|
+
attemptAuthority?: {
|
|
23
|
+
turnId: string;
|
|
24
|
+
attemptId: string;
|
|
25
|
+
executionGeneration: number;
|
|
26
|
+
initiatingHumanSubjectId: string;
|
|
27
|
+
};
|
|
21
28
|
/** The calling session (the pointer the attach/swap mutates + whose group box
|
|
22
29
|
* is the default fleet member). */
|
|
23
30
|
sessionId: string;
|
|
@@ -42,6 +49,8 @@ export declare function buildFleetContextForSession(deps: {
|
|
|
42
49
|
accountId: string;
|
|
43
50
|
workspaceId: string;
|
|
44
51
|
sessionId: string;
|
|
52
|
+
subjectId?: string;
|
|
53
|
+
attemptAuthority?: FleetContext["attemptAuthority"];
|
|
45
54
|
}): Promise<FleetContext>;
|
|
46
55
|
/** The dominant liveness of a fleet member, surfaced to the dock + the agent. */
|
|
47
56
|
export type FleetLiveness = "online" | "reconnecting" | "offline";
|
|
@@ -50,14 +59,15 @@ export type FleetOperationAvailability = "ready" | "wakeable" | "recovering" | "
|
|
|
50
59
|
* A fleet member as the agent + the dock see it (the M8b/M9 UI seam — the
|
|
51
60
|
* `sandboxes_list` response entry the dock renders). STABLE shape: the dock keys
|
|
52
61
|
* on `id`, renders `name`/`kind`/`liveness`, and marks `active`. The session's own
|
|
53
|
-
*
|
|
54
|
-
* null `enrollmentId`; an enrolled machine
|
|
62
|
+
* managed group box is a synthetic entry with `id: groupId`, its actual
|
|
63
|
+
* provisioned backend kind, and a null `enrollmentId`; an enrolled machine
|
|
64
|
+
* carries its sandbox + enrollment ids.
|
|
55
65
|
*/
|
|
56
66
|
export type FleetSandboxEntry = {
|
|
57
67
|
/** The sandbox id used as the attach/swap/run_on `target`. For the session's
|
|
58
68
|
* own group box this is the group id (a null active pointer == this box). */
|
|
59
69
|
id: string;
|
|
60
|
-
kind: "modal" | "selfhosted";
|
|
70
|
+
kind: "modal" | "selfhosted" | "opensandbox";
|
|
61
71
|
name: string;
|
|
62
72
|
liveness: FleetLiveness;
|
|
63
73
|
/** True for the session's currently-active sandbox (the routing target). */
|
|
@@ -106,7 +116,7 @@ export type FleetSwapResult = {
|
|
|
106
116
|
activeSandboxId: string | null;
|
|
107
117
|
activeEpoch: number;
|
|
108
118
|
reason?: string;
|
|
109
|
-
code?: BackendUnresolvableCode | "concurrent_swap" | "recovery_in_progress" | "recovery_degraded" | "recovery_unrecoverable";
|
|
119
|
+
code?: BackendUnresolvableCode | "invalid_working_directory" | "concurrent_swap" | "recovery_in_progress" | "recovery_degraded" | "recovery_unrecoverable";
|
|
110
120
|
};
|
|
111
121
|
/**
|
|
112
122
|
* List the fleet: the session's own group box when it has one (a synthetic
|
|
@@ -157,6 +167,9 @@ export type RunOnResult = {
|
|
|
157
167
|
export type RunOnSelfhostedMachine = {
|
|
158
168
|
workspaceId: string;
|
|
159
169
|
agentId: string;
|
|
170
|
+
/** Exact live daemon pinned for this operation. */
|
|
171
|
+
connectionInstanceId: string;
|
|
172
|
+
workspaceRoot: string;
|
|
160
173
|
controlRpc: ControlRpc;
|
|
161
174
|
relay: SelfhostedRelayConfig;
|
|
162
175
|
/** Short request/reply deadline for read/write and other control operations. */
|
|
@@ -165,6 +178,21 @@ export type RunOnSelfhostedMachine = {
|
|
|
165
178
|
execTimeoutMs: number;
|
|
166
179
|
/** Streaming transport required when execTimeoutMs is 0 (unbounded). */
|
|
167
180
|
opStream?: SelfhostedOpStreamDeps;
|
|
181
|
+
operationResourcePolicy?: SelfhostedOperationResourcePolicy;
|
|
182
|
+
operationResourcePolicySupported?: boolean;
|
|
183
|
+
operationCpuQuotaSupported?: boolean;
|
|
184
|
+
/** Last-boundary authority/connection resolver invoked before every physical
|
|
185
|
+
* provider operation. */
|
|
186
|
+
resolveOperationAdmission?: () => Promise<SelfhostedOperationAdmission | null>;
|
|
187
|
+
/**
|
|
188
|
+
* Exact-attempt values for this one child process. Never persisted on the
|
|
189
|
+
* enrollment or machine. The caller may supply these only after authenticating
|
|
190
|
+
* the active attempt that owns the one-off operation.
|
|
191
|
+
*/
|
|
192
|
+
transientExecEnvironment?: Readonly<Record<string, string>>;
|
|
193
|
+
};
|
|
194
|
+
export type RunOnOptions = {
|
|
195
|
+
transientExecEnvironment?: Readonly<Record<string, string>>;
|
|
168
196
|
};
|
|
169
197
|
/**
|
|
170
198
|
* Execute the one-off machine operation once the workspace/enrollment lookup has
|
|
@@ -182,7 +210,7 @@ export declare function executeRunOnSelfhostedMachine(machine: RunOnSelfhostedMa
|
|
|
182
210
|
* agent subject; an offline machine surfaces a clear reason, never a wrong-box
|
|
183
211
|
* landing.
|
|
184
212
|
*/
|
|
185
|
-
export declare function runOnSandbox(services: FleetServices, ctx: FleetContext, target: string, op: RunOnOp): Promise<RunOnResult>;
|
|
213
|
+
export declare function runOnSandbox(services: FleetServices, ctx: FleetContext, target: string, op: RunOnOp, options?: RunOnOptions): Promise<RunOnResult>;
|
|
186
214
|
export type ProvisionResult = {
|
|
187
215
|
kind: "selfhosted";
|
|
188
216
|
instructions: string;
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { type Settings } from "@opengeni/config";
|
|
2
2
|
import { type Database } from "@opengeni/db";
|
|
3
3
|
import { type EventBus } from "@opengeni/events";
|
|
4
|
-
import { type EstablishedSandboxSession, type RoutingSandboxOperationObserver, type SelfhostedRelayConfig } from "@opengeni/runtime/sandbox";
|
|
4
|
+
import { type EstablishedSandboxSession, type ResolvedActiveBackend, type RoutingRetainedProcess, type RoutingSandboxOperationObserver, type SelfhostedRelayConfig } from "@opengeni/runtime/sandbox";
|
|
5
|
+
type DirectRetainedProcessRoute = {
|
|
6
|
+
providerSessionId: number;
|
|
7
|
+
providerBackend: string;
|
|
8
|
+
providerInstanceId: string;
|
|
9
|
+
leaseEpoch: number;
|
|
10
|
+
routeKind: "home" | "active";
|
|
11
|
+
routeTargetId: string | null;
|
|
12
|
+
routeEpoch: number;
|
|
13
|
+
};
|
|
14
|
+
/** API-direct requests always use active-route authority. The default active
|
|
15
|
+
* pointer is represented by a null target id; it is not a home-route write. */
|
|
16
|
+
export declare function directRetainedProcessMatchesBackend(durable: DirectRetainedProcessRoute, process: RoutingRetainedProcess, backend: ResolvedActiveBackend): boolean;
|
|
5
17
|
export type ChannelARoutingServices = {
|
|
6
18
|
db: Database;
|
|
7
19
|
settings: Settings;
|
|
@@ -37,6 +49,7 @@ export declare function wrapChannelABoxWithRouting(services: ChannelARoutingServ
|
|
|
37
49
|
accountId: string;
|
|
38
50
|
workspaceId: string;
|
|
39
51
|
sessionId: string;
|
|
52
|
+
resourceSubjectId: string;
|
|
40
53
|
homeLease?: {
|
|
41
54
|
sandboxGroupId: string;
|
|
42
55
|
leaseEpoch: number;
|
|
@@ -55,3 +68,4 @@ export declare function wrapChannelABoxWithRouting(services: ChannelARoutingServ
|
|
|
55
68
|
holderId: string;
|
|
56
69
|
};
|
|
57
70
|
}, established: EstablishedSandboxSession): EstablishedSandboxSession;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Settings } from "@opengeni/config";
|
|
2
|
+
import { CapabilityPack, type RigVersion, type Session, type SandboxBackend } from "@opengeni/contracts";
|
|
3
|
+
import { type Database } from "@opengeni/db";
|
|
4
|
+
import { resolveModalCheckpointProviderBinding } from "@opengeni/runtime/sandbox";
|
|
5
|
+
/** Pre-V2 Packs are the sole compatibility path where a Pack can still own a
|
|
6
|
+
* sandbox image. V2 installations select a Rig instead. Keep this predicate at
|
|
7
|
+
* the shared runtime boundary so turns, API-direct tools, viewers, Browser and
|
|
8
|
+
* Computer cannot resolve different physical machines for the same session. */
|
|
9
|
+
export declare function packInstallationUsesLegacyRuntime(input: {
|
|
10
|
+
manifestSnapshot: unknown | null;
|
|
11
|
+
manifestDigest: string | null;
|
|
12
|
+
}): boolean;
|
|
13
|
+
export declare function resolveWorkspaceLegacyRuntimePacks(db: Database, workspaceId: string): Promise<CapabilityPack[]>;
|
|
14
|
+
export declare function legacySandboxRuntimeFromPacks(packs: readonly CapabilityPack[]): {
|
|
15
|
+
sandboxImage: string | null;
|
|
16
|
+
sandboxProviderImages: CapabilityPack["sandboxProviderImages"] | null;
|
|
17
|
+
};
|
|
18
|
+
export declare function settingsWithPackSandboxImage(settings: Settings, sandboxImage: string | null, sandboxProviderImages?: CapabilityPack["sandboxProviderImages"] | null): Settings;
|
|
19
|
+
export declare function settingsWithRigImage(settings: Settings, rigImage: string | null): Settings;
|
|
20
|
+
export declare function rigProviderImageSourceImage(settings: Settings, backend: SandboxBackend): string | null;
|
|
21
|
+
export type RigProviderImageSelectionReason = "selected" | "missing" | "provider_unsupported" | "not_ready" | "not_cold_boot_validated" | "content_mismatch" | "provider_binding_unavailable" | "provider_binding_mismatch";
|
|
22
|
+
export declare function resolveRigProviderImageSelection(settings: Settings, version: RigVersion | null, backend: SandboxBackend, currentProviderBindingKeyHash: string | null): {
|
|
23
|
+
settings: Settings;
|
|
24
|
+
reason: RigProviderImageSelectionReason;
|
|
25
|
+
contentHash: string | null;
|
|
26
|
+
imageId: string | null;
|
|
27
|
+
};
|
|
28
|
+
export declare function resolveRigProviderImageForRun(settings: Settings, version: RigVersion | null, backend: SandboxBackend, resolveBinding?: typeof resolveModalCheckpointProviderBinding): Promise<ReturnType<typeof resolveRigProviderImageSelection>>;
|
|
29
|
+
export declare function settingsWithRigProviderImage(settings: Settings, version: RigVersion | null, backend: SandboxBackend, resolveBinding?: typeof resolveModalCheckpointProviderBinding): Promise<Settings>;
|
|
30
|
+
export type SessionSandboxRuntime = {
|
|
31
|
+
/** Exact logical image/rig settings used to fence the durable group lease. */
|
|
32
|
+
settings: Settings;
|
|
33
|
+
image: string | null;
|
|
34
|
+
rigVersion: RigVersion | null;
|
|
35
|
+
};
|
|
36
|
+
export declare function resolveSessionSandboxRuntime(db: Database, settings: Settings, session: Pick<Session, "workspaceId" | "sandboxBackend" | "rigId" | "rigVersionId">): Promise<SessionSandboxRuntime>;
|
|
37
|
+
/** Resolve provider-native cold-create optimization only for the single CAS
|
|
38
|
+
* winner. Ordinary API calls never perform provider identity I/O. */
|
|
39
|
+
export declare function providerSettingsForSessionSandboxRuntime(runtime: SessionSandboxRuntime, backend: SandboxBackend): Promise<Settings>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.2.0-canary.0",
|
|
4
4
|
"description": "OpenGeni framework-agnostic core: the domain, access, and billing layers (neutral access, off-HTTP V2 surface). Behavior-preserving extraction from apps/api — keeps Hono's HTTPException for error throwing (typed-errors cleanup deferred).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
"./canonical-human-identities": {
|
|
34
34
|
"types": "./dist/canonical-human-identities.d.ts",
|
|
35
35
|
"import": "./dist/canonical-human-identities.js"
|
|
36
|
+
},
|
|
37
|
+
"./connection-authority": {
|
|
38
|
+
"types": "./dist/domain/connection-authority.d.ts",
|
|
39
|
+
"import": "./dist/domain/connection-authority.js"
|
|
36
40
|
}
|
|
37
41
|
},
|
|
38
42
|
"publishConfig": {
|
|
@@ -46,16 +50,17 @@
|
|
|
46
50
|
},
|
|
47
51
|
"dependencies": {
|
|
48
52
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
|
-
"@opengeni/codex": "^0.2.
|
|
50
|
-
"@opengeni/config": "^0.
|
|
51
|
-
"@opengeni/contracts": "^
|
|
52
|
-
"@opengeni/db": "^
|
|
53
|
-
"@opengeni/documents": "^0.
|
|
54
|
-
"@opengeni/events": "^0.3.
|
|
55
|
-
"@opengeni/observability": "^0.
|
|
56
|
-
"@opengeni/runtime": "^1.
|
|
57
|
-
"@opengeni/storage": "^0.2.
|
|
58
|
-
"hono": "^4.12.18"
|
|
53
|
+
"@opengeni/codex": "^0.2.19-canary.0",
|
|
54
|
+
"@opengeni/config": "^0.19.1-canary.0",
|
|
55
|
+
"@opengeni/contracts": "^2.3.0-canary.0",
|
|
56
|
+
"@opengeni/db": "^3.2.0-canary.0",
|
|
57
|
+
"@opengeni/documents": "^0.7.0-canary.0",
|
|
58
|
+
"@opengeni/events": "^0.3.124-canary.0",
|
|
59
|
+
"@opengeni/observability": "^0.8.4-canary.0",
|
|
60
|
+
"@opengeni/runtime": "^1.3.1-canary.0",
|
|
61
|
+
"@opengeni/storage": "^0.2.106-canary.0",
|
|
62
|
+
"hono": "^4.12.18",
|
|
63
|
+
"zod": "^4.2.1"
|
|
59
64
|
},
|
|
60
65
|
"engines": {
|
|
61
66
|
"node": ">=18"
|
package/src/access/index.ts
CHANGED
|
@@ -22,6 +22,41 @@ import { getManagedSession } from "../managed-session";
|
|
|
22
22
|
const bearerPrefix = "Bearer ";
|
|
23
23
|
const accessContextByRequest = new WeakMap<Request, Promise<AccessContext | null>>();
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Contexts that were authenticated by a verified canonical managed cookie
|
|
27
|
+
* (Better Auth session), stamped at the single branch of
|
|
28
|
+
* {@link resolveAccessContext} that performs that verification.
|
|
29
|
+
*
|
|
30
|
+
* This is provenance — HOW the request authenticated — not a shape check on a
|
|
31
|
+
* value. It cannot be forged by a token claim, and it FAILS CLOSED by default:
|
|
32
|
+
* membership is only ever added at that one branch, so every other path
|
|
33
|
+
* (delegated bearer, API key, local bootstrap, configured key, and any path
|
|
34
|
+
* added later) is absent from the set without anyone maintaining a list. A
|
|
35
|
+
* request that never resolves an access context has no context to test at all.
|
|
36
|
+
*
|
|
37
|
+
* Keyed on the resolved `AccessContext` object identity, deliberately NOT on
|
|
38
|
+
* the `Request`: a reused, wrapped, or retried request object can therefore
|
|
39
|
+
* never carry a stale positive into a differently-authenticated resolution, and
|
|
40
|
+
* the non-cached `requireFreshAccessGrant` path is stamped correctly too. Each
|
|
41
|
+
* resolution produces a fresh context object, so the mark travels with exactly
|
|
42
|
+
* the value the cookie branch produced.
|
|
43
|
+
*/
|
|
44
|
+
const canonicalManagedCookieContexts = new WeakSet<AccessContext>();
|
|
45
|
+
const resolvedAccessGrantAuthorizations = new WeakSet<object>();
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Opaque, request-local proof that the canonical access resolver authorized the
|
|
49
|
+
* exact account administrator named by the stamp. The random id is audit and
|
|
50
|
+
* replay provenance only; object identity is what prevents a caller from
|
|
51
|
+
* manufacturing this proof inside the API process.
|
|
52
|
+
*/
|
|
53
|
+
export type AccountAdminAuthorizationStamp = Readonly<{
|
|
54
|
+
authorizationId: string;
|
|
55
|
+
accountId: string;
|
|
56
|
+
actorSubjectId: string;
|
|
57
|
+
permission: "account:admin";
|
|
58
|
+
}>;
|
|
59
|
+
|
|
25
60
|
export type AccessDeps = {
|
|
26
61
|
db: Database;
|
|
27
62
|
settings: Settings;
|
|
@@ -50,11 +85,38 @@ export async function requireAccessGrant(
|
|
|
50
85
|
return (await requireAccessGrantAuthorization(c, deps, workspaceId, permission)).grant;
|
|
51
86
|
}
|
|
52
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Re-resolve the authenticated principal and its current grants without using
|
|
90
|
+
* the request-local access cache. Long-lived responses use this to ensure a
|
|
91
|
+
* membership suspension or revocation takes effect while the connection is
|
|
92
|
+
* still open.
|
|
93
|
+
*/
|
|
94
|
+
export async function requireFreshAccessGrant(
|
|
95
|
+
c: Context,
|
|
96
|
+
deps: AccessDeps,
|
|
97
|
+
workspaceId: string,
|
|
98
|
+
permission?: Permission,
|
|
99
|
+
): Promise<AccessGrant> {
|
|
100
|
+
const context = await resolveAccessContext(c, deps);
|
|
101
|
+
if (!context) {
|
|
102
|
+
throw new HTTPException(401, { message: "authentication required" });
|
|
103
|
+
}
|
|
104
|
+
return (await accessGrantAuthorization(context, deps, workspaceId, permission)).grant;
|
|
105
|
+
}
|
|
106
|
+
|
|
53
107
|
export type AccessGrantAuthorization = {
|
|
54
108
|
grant: AccessGrant;
|
|
55
109
|
accountGrant: AccountGrant | null;
|
|
56
110
|
authenticatedSubjectId: string;
|
|
57
111
|
contextIntegrity: boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Did this request authenticate as the canonical managed-cookie (Better Auth)
|
|
114
|
+
* session that OWNS this grant's subject? The single input to the owner-only
|
|
115
|
+
* managed personal-workspace exception; see `isCanonicalManagedHumanSession`.
|
|
116
|
+
* False for every bearer, API-key, delegated, service, local, and configured
|
|
117
|
+
* principal, and for any future path that does not verify a cookie.
|
|
118
|
+
*/
|
|
119
|
+
canonicalManagedHumanSession: boolean;
|
|
58
120
|
};
|
|
59
121
|
|
|
60
122
|
export function accessGrantAuthorizationFromContext(
|
|
@@ -81,12 +143,46 @@ export function accessGrantAuthorizationFromContext(
|
|
|
81
143
|
) &&
|
|
82
144
|
matchingAccountGrants.length === 1 &&
|
|
83
145
|
matchingAccountGrants[0]?.subjectId === context.subjectId;
|
|
84
|
-
|
|
146
|
+
const authorization: AccessGrantAuthorization = {
|
|
85
147
|
grant,
|
|
86
148
|
accountGrant: contextIntegrity ? matchingAccountGrants[0]! : null,
|
|
87
149
|
authenticatedSubjectId: context.subjectId,
|
|
88
150
|
contextIntegrity,
|
|
151
|
+
canonicalManagedHumanSession: isCanonicalManagedHumanSession(context, grant),
|
|
89
152
|
};
|
|
153
|
+
resolvedAccessGrantAuthorizations.add(authorization);
|
|
154
|
+
return authorization;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Mint the capability passed to an account-admin database lifecycle.
|
|
159
|
+
*
|
|
160
|
+
* This deliberately accepts every canonical access mode (managed,
|
|
161
|
+
* local/configured, and signed delegation). Organization-membership rows are
|
|
162
|
+
* one possible source of account authority, not the definition of it. A plain
|
|
163
|
+
* object with matching fields is rejected because only values returned by the
|
|
164
|
+
* access resolver are present in `resolvedAccessGrantAuthorizations`.
|
|
165
|
+
*/
|
|
166
|
+
export function requireAccountAdminAuthorizationStamp(
|
|
167
|
+
authorization: AccessGrantAuthorization,
|
|
168
|
+
): AccountAdminAuthorizationStamp {
|
|
169
|
+
const { grant, accountGrant } = authorization;
|
|
170
|
+
if (
|
|
171
|
+
!resolvedAccessGrantAuthorizations.has(authorization) ||
|
|
172
|
+
!authorization.contextIntegrity ||
|
|
173
|
+
authorization.authenticatedSubjectId !== grant.subjectId ||
|
|
174
|
+
accountGrant?.accountId !== grant.accountId ||
|
|
175
|
+
accountGrant.subjectId !== grant.subjectId ||
|
|
176
|
+
!accountGrant.permissions.includes("account:admin")
|
|
177
|
+
) {
|
|
178
|
+
throw new HTTPException(403, { message: "missing permission: account:admin" });
|
|
179
|
+
}
|
|
180
|
+
return Object.freeze({
|
|
181
|
+
authorizationId: crypto.randomUUID(),
|
|
182
|
+
accountId: grant.accountId,
|
|
183
|
+
actorSubjectId: grant.subjectId,
|
|
184
|
+
permission: "account:admin" as const,
|
|
185
|
+
});
|
|
90
186
|
}
|
|
91
187
|
|
|
92
188
|
export async function requireAccessGrantAuthorization(
|
|
@@ -96,6 +192,15 @@ export async function requireAccessGrantAuthorization(
|
|
|
96
192
|
permission?: Permission,
|
|
97
193
|
): Promise<AccessGrantAuthorization> {
|
|
98
194
|
const context = await requireAccessContext(c, deps);
|
|
195
|
+
return await accessGrantAuthorization(context, deps, workspaceId, permission);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function accessGrantAuthorization(
|
|
199
|
+
context: AccessContext,
|
|
200
|
+
deps: AccessDeps,
|
|
201
|
+
workspaceId: string,
|
|
202
|
+
permission?: Permission,
|
|
203
|
+
): Promise<AccessGrantAuthorization> {
|
|
99
204
|
const principalKind = hostedHumanSessionPrincipalKind(context);
|
|
100
205
|
const grant =
|
|
101
206
|
context.workspaceGrants.find((candidate) => candidate.workspaceId === workspaceId) ??
|
|
@@ -118,6 +223,43 @@ export async function requireAccessGrantAuthorization(
|
|
|
118
223
|
return accessGrantAuthorizationFromContext(context, grant);
|
|
119
224
|
}
|
|
120
225
|
|
|
226
|
+
/**
|
|
227
|
+
* May this grant use the owner-only managed personal-workspace exception?
|
|
228
|
+
*
|
|
229
|
+
* A managed human's personal workspace carries no `workspace_memberships` row,
|
|
230
|
+
* so seams that fence on one must consult the
|
|
231
|
+
* `organization_memberships.personal_workspace_id` pointer instead. That is a
|
|
232
|
+
* runtime property, not a schema one: migration 0219's `42501` is a
|
|
233
|
+
* precondition inside the provisioning function rather than a constraint, and
|
|
234
|
+
* the parity report records `personal_workspace_has_no_membership_row` as
|
|
235
|
+
* `basis: "runtime"` - its own term for something nothing in the schema
|
|
236
|
+
* prevents. It holds because every membership writer requires `members:manage`
|
|
237
|
+
* on the target, which a personal-workspace grant deliberately omits. `AGENTS.md`
|
|
238
|
+
* scopes that exception tightly: it "derives an owner-only personal-workspace
|
|
239
|
+
* grant only for the canonical managed-cookie (Better Auth) session", and
|
|
240
|
+
* "Bearer/delegated principals, API keys, and account or organization
|
|
241
|
+
* administrators receive no personal-workspace access through that exception."
|
|
242
|
+
*
|
|
243
|
+
* This asks about PROVENANCE, not shape. Inspecting a grant's `principalKind`,
|
|
244
|
+
* `metadata.delegated`, or `serviceInitiator` is not sufficient: a delegated
|
|
245
|
+
* bearer chooses every one of those claims inside its own host-signed token and
|
|
246
|
+
* can name any subject, including the owner's. So the answer comes from
|
|
247
|
+
* {@link canonicalManagedCookieContexts}, stamped only where a Better Auth
|
|
248
|
+
* cookie was actually verified.
|
|
249
|
+
*
|
|
250
|
+
* The subject equality is the second half: the exception is OWNER-only, so the
|
|
251
|
+
* grant must belong to the authenticated human itself, not to some other
|
|
252
|
+
* subject reached from an authenticated session. `user:` excludes machine
|
|
253
|
+
* principals, which can never own an organization membership.
|
|
254
|
+
*/
|
|
255
|
+
function isCanonicalManagedHumanSession(context: AccessContext, grant: AccessGrant): boolean {
|
|
256
|
+
return (
|
|
257
|
+
canonicalManagedCookieContexts.has(context) &&
|
|
258
|
+
grant.subjectId === context.subjectId &&
|
|
259
|
+
grant.subjectId.startsWith("user:")
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
121
263
|
function hostedHumanSessionPrincipalKind(context: AccessContext): "human_session" | undefined {
|
|
122
264
|
if (context.mode !== "managed" || context.workspaceGrants.length === 0) {
|
|
123
265
|
return undefined;
|
|
@@ -165,42 +307,19 @@ export function requireLiteralPermission(grant: AccessGrant, permission: Permiss
|
|
|
165
307
|
}
|
|
166
308
|
|
|
167
309
|
export function hasLiteralPermission(permissions: Permission[], permission: Permission): boolean {
|
|
310
|
+
if (!Array.isArray(permissions)) return false;
|
|
168
311
|
return permissions.includes(permission);
|
|
169
312
|
}
|
|
170
313
|
|
|
171
314
|
export function hasPermission(permissions: Permission[], permission: Permission): boolean {
|
|
315
|
+
if (!Array.isArray(permissions)) return false;
|
|
172
316
|
if (permission === "secrets:read") {
|
|
173
317
|
return permissions.includes("secrets:read");
|
|
174
318
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
"variable-sets:use",
|
|
180
|
-
"variable-sets:manage",
|
|
181
|
-
"environments:use" as Permission,
|
|
182
|
-
"environments:manage" as Permission,
|
|
183
|
-
],
|
|
184
|
-
"variable-sets:read": [
|
|
185
|
-
"variable-sets:use",
|
|
186
|
-
"variable-sets:manage",
|
|
187
|
-
"environments:use" as Permission,
|
|
188
|
-
"environments:manage" as Permission,
|
|
189
|
-
],
|
|
190
|
-
"variable-sets:write": ["variable-sets:manage", "environments:manage" as Permission],
|
|
191
|
-
"secrets:list": [
|
|
192
|
-
"variable-sets:use",
|
|
193
|
-
"variable-sets:manage",
|
|
194
|
-
"environments:use" as Permission,
|
|
195
|
-
"environments:manage" as Permission,
|
|
196
|
-
],
|
|
197
|
-
"secrets:write": ["variable-sets:manage", "environments:manage" as Permission],
|
|
198
|
-
};
|
|
199
|
-
return (
|
|
200
|
-
permissions.includes(permission) ||
|
|
201
|
-
(aliases[permission]?.some((alias) => permissions.includes(alias)) ?? false) ||
|
|
202
|
-
permissions.includes("workspace:admin")
|
|
203
|
-
);
|
|
319
|
+
// Variable-set metadata, plaintext read, write/rotation, attachment, and
|
|
320
|
+
// runtime use are independent capabilities. Deprecated broad permissions
|
|
321
|
+
// remain parseable but do not imply any of the exact permissions.
|
|
322
|
+
return permissions.includes(permission) || permissions.includes("workspace:admin");
|
|
204
323
|
}
|
|
205
324
|
|
|
206
325
|
async function resolveAccessContext(c: Context, deps: AccessDeps): Promise<AccessContext | null> {
|
|
@@ -260,11 +379,18 @@ async function resolveAccessContext(c: Context, deps: AccessDeps): Promise<Acces
|
|
|
260
379
|
if (deps.managedAuth) {
|
|
261
380
|
const session = await getManagedSession(c, deps.managedAuth, { db: deps.db });
|
|
262
381
|
if (session?.user) {
|
|
263
|
-
|
|
382
|
+
// THE canonical managed-cookie (Better Auth) branch, and the only place
|
|
383
|
+
// that may stamp a context as such. Every `return` above this point leaves
|
|
384
|
+
// its context unstamped, so the owner-only personal-workspace exception
|
|
385
|
+
// fails closed for them without depending on an exclusion list.
|
|
386
|
+
const context = await ensureManagedAccessForUser(deps.db, {
|
|
264
387
|
userId: session.user.id,
|
|
265
388
|
email: session.user.email,
|
|
266
389
|
name: session.user.name,
|
|
390
|
+
emailVerified: session.user.emailVerified,
|
|
267
391
|
});
|
|
392
|
+
canonicalManagedCookieContexts.add(context);
|
|
393
|
+
return context;
|
|
268
394
|
}
|
|
269
395
|
}
|
|
270
396
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getVariableSet,
|
|
13
13
|
NewSessionDraftAccessError,
|
|
14
14
|
newSessionDraftToolsProvided,
|
|
15
|
+
newSessionSelectionHistory,
|
|
15
16
|
publicNewSessionDraftOptions,
|
|
16
17
|
requireFileForSubject,
|
|
17
18
|
saveNewSessionDraftInTransaction,
|
|
@@ -50,6 +51,7 @@ function mapNewSessionDraft(
|
|
|
50
51
|
reasoningEffort: row.reasoningEffort,
|
|
51
52
|
latencyMode: row.latencyMode,
|
|
52
53
|
options: publicNewSessionDraftOptions(row),
|
|
54
|
+
selectionHistory: newSessionSelectionHistory(row),
|
|
53
55
|
updatedAt: row.updatedAt.toISOString(),
|
|
54
56
|
});
|
|
55
57
|
}
|
|
@@ -103,8 +105,13 @@ async function hydrateNewSessionDraft(
|
|
|
103
105
|
const options = { ...mapped.options };
|
|
104
106
|
if (options.variableSetId) {
|
|
105
107
|
if (
|
|
108
|
+
!hasPermission(grant.permissions, "variable-sets:attach") ||
|
|
106
109
|
!hasPermission(grant.permissions, "variable-sets:use") ||
|
|
107
|
-
!(await getVariableSet(
|
|
110
|
+
!(await getVariableSet(
|
|
111
|
+
deps.db,
|
|
112
|
+
{ accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
|
|
113
|
+
options.variableSetId,
|
|
114
|
+
))
|
|
108
115
|
) {
|
|
109
116
|
delete options.variableSetId;
|
|
110
117
|
}
|
|
@@ -178,6 +185,7 @@ export async function getActorNewSessionDraft(
|
|
|
178
185
|
reasoningEffort: deps.settings.openaiReasoningEffort,
|
|
179
186
|
latencyMode: "standard",
|
|
180
187
|
options: {},
|
|
188
|
+
selectionHistory: { projects: [] },
|
|
181
189
|
updatedAt: null,
|
|
182
190
|
}
|
|
183
191
|
);
|
|
@@ -195,6 +203,13 @@ export async function saveActorNewSessionDraft(
|
|
|
195
203
|
grant: AccessGrant,
|
|
196
204
|
workspaceId: string,
|
|
197
205
|
rawInput: unknown,
|
|
206
|
+
/**
|
|
207
|
+
* `AccessGrantAuthorization.canonicalManagedHumanSession` — did this request
|
|
208
|
+
* authenticate as the canonical managed cookie that owns `grant.subjectId`?
|
|
209
|
+
* Defaults to false so every caller that has not proven it fails closed onto
|
|
210
|
+
* the historical bare-membership fence.
|
|
211
|
+
*/
|
|
212
|
+
canonicalManagedHumanSession = false,
|
|
198
213
|
): Promise<NewSessionDraftValue> {
|
|
199
214
|
const input = SaveNewSessionDraftRequest.parse(rawInput);
|
|
200
215
|
// The pre-marker client contract required `tools` and had no
|
|
@@ -212,7 +227,9 @@ export async function saveActorNewSessionDraft(
|
|
|
212
227
|
const tools = toolsProvided ? validateToolRefs(input.tools, runtimeSettings) : [];
|
|
213
228
|
await validateGitHubRepositorySelection(deps.db, workspaceId, resources);
|
|
214
229
|
if (resources.some((resource) => resource.kind === "file") && !deps.objectStorage) {
|
|
215
|
-
throw new HTTPException(503, {
|
|
230
|
+
throw new HTTPException(503, {
|
|
231
|
+
message: "object storage is not configured",
|
|
232
|
+
});
|
|
216
233
|
}
|
|
217
234
|
await validateFileResources(deps.db, grant.accountId, workspaceId, grant.subjectId, resources);
|
|
218
235
|
assertConfiguredModel(deps.settings, input.model);
|
|
@@ -239,6 +256,11 @@ export async function saveActorNewSessionDraft(
|
|
|
239
256
|
// worker MCP principal) legitimately have no workspace_memberships
|
|
240
257
|
// row, so they must not be rejected by the human-removal fence.
|
|
241
258
|
requireWorkspaceMembership: grant.subjectId.startsWith("user:"),
|
|
259
|
+
// A managed human's own personal workspace has no membership row at
|
|
260
|
+
// all, so the human-removal fence above must fall back to the
|
|
261
|
+
// organization-membership pointer for them — and only for the
|
|
262
|
+
// canonical managed-cookie session that owns it.
|
|
263
|
+
personalWorkspaceOwnerException: canonicalManagedHumanSession,
|
|
242
264
|
}),
|
|
243
265
|
),
|
|
244
266
|
);
|