@opengeni/core 0.12.10 → 0.14.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.
Files changed (39) hide show
  1. package/dist/access/index.d.ts +22 -0
  2. package/dist/application/new-session-drafts.d.ts +14 -0
  3. package/dist/application/session-commands.d.ts +107 -0
  4. package/dist/billing/limits.d.ts +29 -0
  5. package/dist/dependencies.d.ts +137 -0
  6. package/dist/domain/capabilities.d.ts +62 -0
  7. package/dist/domain/environments.d.ts +33 -0
  8. package/dist/domain/insights.d.ts +11 -0
  9. package/dist/domain/packs.d.ts +27 -0
  10. package/dist/domain/resources.d.ts +32 -0
  11. package/dist/domain/scheduled-tasks.d.ts +72 -0
  12. package/dist/domain/session-tool-policy.d.ts +31 -0
  13. package/dist/domain/sessions.d.ts +256 -0
  14. package/dist/domain/slack-bot.d.ts +19 -0
  15. package/dist/domain/workspace-members.d.ts +34 -0
  16. package/dist/index.d.ts +23 -1199
  17. package/dist/index.js +693 -53
  18. package/dist/index.js.map +1 -1
  19. package/dist/managed-auth-type.d.ts +2 -0
  20. package/dist/rigs/index.d.ts +57 -0
  21. package/dist/sandbox/fleet.d.ts +197 -0
  22. package/dist/sandbox/routing.d.ts +55 -0
  23. package/dist/sandbox-types.d.ts +52 -0
  24. package/dist/session-authorization.d.ts +36 -0
  25. package/dist/transcription.d.ts +71 -0
  26. package/dist/workflow-wake-contract.d.ts +4 -0
  27. package/package.json +11 -11
  28. package/src/access/index.ts +73 -2
  29. package/src/application/new-session-drafts.ts +3 -0
  30. package/src/application/session-commands.ts +3 -1
  31. package/src/dependencies.ts +5 -0
  32. package/src/domain/insights.ts +480 -0
  33. package/src/domain/session-tool-policy.ts +17 -25
  34. package/src/domain/sessions.ts +75 -4
  35. package/src/domain/slack-bot.ts +2 -4
  36. package/src/index.ts +2 -0
  37. package/src/sandbox/fleet.ts +96 -33
  38. package/src/sandbox/routing.ts +29 -7
  39. package/src/transcription.ts +142 -0
@@ -0,0 +1,2 @@
1
+ import type { Auth } from "better-auth";
2
+ export type ManagedAuth = Auth<any>;
@@ -0,0 +1,57 @@
1
+ import type { AccessGrant, CreateRigRequest, RigDefinitionEditPayload, ProposeRigChangeRequest, Rig, RigChange, RigVersion, UpdateRigRequest } from "@opengeni/contracts";
2
+ import { type Database } from "@opengeni/db";
3
+ export declare const MAX_RIGS_PER_WORKSPACE = 50;
4
+ export declare const MAX_CHECKS_PER_RIG = 100;
5
+ export declare const MAX_CREDENTIAL_HOOKS_PER_RIG = 50;
6
+ export declare const MAX_DEFAULT_VARIABLE_SETS_PER_RIG = 25;
7
+ export type RigServices = {
8
+ db: Database;
9
+ };
10
+ type RigAuditAction = "rig.created" | "rig.updated" | "rig.deleted" | "rig.change.proposed" | "rig.change.verified" | "rig.change.rejected" | "rig.change.failed" | "rig.change.merged" | "rig.verification.started" | "rig.verification.passed" | "rig.verification.failed" | "rig.version.activated" | "rig.version.promoted";
11
+ export declare function recordRigAuditEvent(db: Database, input: {
12
+ grant: AccessGrant;
13
+ action: RigAuditAction;
14
+ rigId: string;
15
+ metadata?: Record<string, unknown>;
16
+ }): Promise<void>;
17
+ export declare function rigActorForGrant(grant: AccessGrant): string;
18
+ export declare function requireRigForApi(db: Database, workspaceId: string, rigId: string): Promise<Rig>;
19
+ export declare function requireRigChangeForApi(db: Database, workspaceId: string, rigId: string, changeId: string): Promise<RigChange>;
20
+ export declare function createRigForApi(deps: RigServices, grant: AccessGrant, payload: CreateRigRequest): Promise<Rig>;
21
+ export declare function updateRigForApi(deps: RigServices, grant: AccessGrant, rig: Rig, payload: UpdateRigRequest): Promise<Rig>;
22
+ export declare function deleteRigForApi(deps: RigServices, grant: AccessGrant, rig: Rig): Promise<void>;
23
+ export declare function proposeRigChangeForApi(deps: RigServices, grant: AccessGrant, rig: Rig, request: ProposeRigChangeRequest, options?: {
24
+ proposedBy?: string;
25
+ }): Promise<RigChange>;
26
+ export type RigVerificationClassification = {
27
+ status: "merged";
28
+ action: "auto_promote";
29
+ } | {
30
+ status: "proposed";
31
+ action: "await_manage_promote";
32
+ } | {
33
+ status: "rejected";
34
+ action: "reject";
35
+ } | {
36
+ status: "failed";
37
+ action: "retryable_failure";
38
+ };
39
+ export declare function classifyRigVerificationOutcome(input: {
40
+ kind: "setup_append" | "definition_edit";
41
+ passed: boolean;
42
+ infraError?: boolean;
43
+ }): RigVerificationClassification;
44
+ export declare function appendRigSetupCommand(baseSetupScript: string | null | undefined, command: string): string;
45
+ export declare function promoteSetupAppendChange(deps: RigServices, grant: AccessGrant, rig: Rig, change: RigChange): Promise<{
46
+ change: RigChange;
47
+ version: RigVersion;
48
+ }>;
49
+ export declare function promoteVerifiedDefinitionEditChangeForApi(deps: RigServices, grant: AccessGrant, rig: Rig, change: RigChange): Promise<{
50
+ change: RigChange;
51
+ version: RigVersion;
52
+ }>;
53
+ export declare function createRigVersionForApi(deps: RigServices, grant: AccessGrant, rig: Rig, payload: RigDefinitionEditPayload): Promise<RigVersion>;
54
+ export declare function activateRigVersionForApi(deps: RigServices, grant: AccessGrant, rig: Rig, versionId: string): Promise<RigVersion>;
55
+ export declare function listRigVersionsForApi(deps: RigServices, workspaceId: string, rigId: string): Promise<RigVersion[]>;
56
+ export declare function listRigChangesForApi(deps: RigServices, workspaceId: string, rigId: string, limit?: number): Promise<RigChange[]>;
57
+ export {};
@@ -0,0 +1,197 @@
1
+ import type { Settings } from "@opengeni/config";
2
+ import { type Database, type SandboxRecord } from "@opengeni/db";
3
+ import type { EventBus } from "@opengeni/events";
4
+ import { type BackendUnresolvableCode, type ControlRpc, type SelfhostedRelayConfig } from "@opengeni/runtime/sandbox";
5
+ export type FleetServices = {
6
+ db: Database;
7
+ settings: Settings;
8
+ bus?: EventBus;
9
+ /** API-direct readiness owner for the session's home group. Production wires
10
+ * this to the same viewer/provider verification + rematerialization path; core
11
+ * tests may omit it when exercising pointer mechanics only. */
12
+ ensureSessionGroupReady?: (ctx: FleetContext) => Promise<FleetReadinessHold>;
13
+ };
14
+ export type FleetReadinessHold = {
15
+ /** Release target liveness only after route publication settles. */
16
+ release: () => Promise<void>;
17
+ };
18
+ export type FleetContext = {
19
+ accountId: string;
20
+ workspaceId: string;
21
+ /** The calling session (the pointer the attach/swap mutates + whose group box
22
+ * is the default fleet member). */
23
+ sessionId: string;
24
+ /** The session's own group sandbox backend (modal/selfhosted/…). */
25
+ sessionBackend: string;
26
+ /** The session's own group sandbox id (the lease group). */
27
+ sessionGroupId: string;
28
+ };
29
+ /**
30
+ * Build a session-scoped {@link FleetContext}: load the session (workspace-
31
+ * scoped), reject a session with no box (backend:none — the fleet is only
32
+ * meaningful for a sandboxed session), and project its group backend/id. Shared
33
+ * by the worker-signed MCP fleet tools and the user-authenticated swap REST
34
+ * route so both resolve the SAME context (no drift). The `accountId`/`workspaceId`/
35
+ * `sessionId` come from the trusted grant/route; the backend + group id come from
36
+ * the session row.
37
+ */
38
+ export declare function buildFleetContextForSession(deps: {
39
+ db: Database;
40
+ }, ctx: {
41
+ accountId: string;
42
+ workspaceId: string;
43
+ sessionId: string;
44
+ }): Promise<FleetContext>;
45
+ /** The dominant liveness of a fleet member, surfaced to the dock + the agent. */
46
+ export type FleetLiveness = "online" | "reconnecting" | "offline";
47
+ /**
48
+ * A fleet member as the agent + the dock see it (the M8b/M9 UI seam — the
49
+ * `sandboxes_list` response entry the dock renders). STABLE shape: the dock keys
50
+ * on `id`, renders `name`/`kind`/`liveness`, and marks `active`. The session's own
51
+ * Modal group box is a synthetic entry with `id: groupId`, `kind: "modal"`, and a
52
+ * null `enrollmentId`; an enrolled machine carries its sandbox + enrollment ids.
53
+ */
54
+ export type FleetSandboxEntry = {
55
+ /** The sandbox id used as the attach/swap/run_on `target`. For the session's
56
+ * own group box this is the group id (a null active pointer == this box). */
57
+ id: string;
58
+ kind: "modal" | "selfhosted";
59
+ name: string;
60
+ liveness: FleetLiveness;
61
+ /** True for the session's currently-active sandbox (the routing target). */
62
+ active: boolean;
63
+ /** True for the session's own group box (the default/home sandbox). */
64
+ isSessionGroup: boolean;
65
+ enrollmentId: string | null;
66
+ /** Whether this target can be attached/swapped to right now (live + addressable). */
67
+ attachable: boolean;
68
+ /** Selfhosted only: whether whole-machine + screen-control consent is acked. */
69
+ consented?: boolean;
70
+ /** Selfhosted only: whether a display (real/Xvfb) is present. */
71
+ hasDisplay?: boolean;
72
+ lastSeenAt?: string | null;
73
+ /** Orthogonal truth dimensions. `liveness` is only their conservative UI
74
+ * projection and is never evidence for a specific dimension. */
75
+ providerStatus: "not_created" | "creating" | "exists" | "missing" | "unknown";
76
+ leaseLiveness: "cold" | "warming" | "warm" | "draining" | null;
77
+ routeStatus: "attached" | "detached";
78
+ archiveStatus: "none" | "available" | "unverified" | "invalid";
79
+ restoreStatus: "not_required" | "pending" | "restoring" | "verifying" | "ready" | "degraded" | "unrecoverable";
80
+ workspaceStatus: "unknown" | "not_ready" | "ready" | "degraded" | "unrecoverable";
81
+ leaseEpoch: number | null;
82
+ routeEpoch: number;
83
+ /** Numeric/boolean persistence truth only. Archive locations, content hashes,
84
+ * provider identities, and storage handles are intentionally not projected. */
85
+ workspaceGeneration: number | null;
86
+ archiveGeneration: number | null;
87
+ archiveComplete: boolean;
88
+ };
89
+ export type FleetListResult = {
90
+ /** The session's currently-active sandbox id, or null == the group box. */
91
+ activeSandboxId: string | null;
92
+ activeEpoch: number;
93
+ sandboxes: FleetSandboxEntry[];
94
+ };
95
+ /** A swap/attach outcome the tool returns. On a rejection, `code` carries the
96
+ * typed reason (issue #341 typed diagnostics) alongside the human `reason`. */
97
+ export type FleetSwapResult = {
98
+ swapped: boolean;
99
+ activeSandboxId: string | null;
100
+ activeEpoch: number;
101
+ reason?: string;
102
+ code?: BackendUnresolvableCode | "concurrent_swap" | "recovery_in_progress" | "recovery_degraded" | "recovery_unrecoverable";
103
+ };
104
+ /**
105
+ * List the fleet: the session's own Modal group box (a synthetic entry) + the
106
+ * workspace's first-class selfhosted sandboxes (each probed for liveness), each
107
+ * with an `active` marker derived from the session's active pointer.
108
+ */
109
+ export declare function listFleet(services: FleetServices, ctx: FleetContext): Promise<FleetListResult>;
110
+ /**
111
+ * THE SWAP (and attach — identical mechanic). Validate the target's ownership +
112
+ * liveness, then repoint the session via the epoch-fenced CAS `setActiveSandbox`:
113
+ * read the current epoch, then CAS on it. A concurrent double-swap lets exactly
114
+ * one win; the loser re-reads + may retry. The bumped epoch fences any in-flight
115
+ * op cached against the old pointer, which then retries against the new active
116
+ * sandbox (the routing proxy's fenced-retry role).
117
+ */
118
+ export declare function swapActiveSandbox(services: FleetServices, ctx: FleetContext, target: string, workingDir?: string | null): Promise<FleetSwapResult>;
119
+ export type RunOnOp = {
120
+ kind: "exec";
121
+ cmd: string;
122
+ workdir?: string;
123
+ } | {
124
+ kind: "read";
125
+ path: string;
126
+ } | {
127
+ kind: "write";
128
+ path: string;
129
+ content: string;
130
+ };
131
+ export type RunOnResult = {
132
+ target: string;
133
+ kind: string;
134
+ ok: boolean;
135
+ stdout?: string;
136
+ stderr?: string;
137
+ exitCode?: number | null;
138
+ /** Exec only: whether the machine killed the child at its process deadline. */
139
+ timedOut?: boolean;
140
+ /** Exec only: the effective clamped process deadline enforced by the machine. */
141
+ deadlineMs?: number;
142
+ content?: string;
143
+ bytesWritten?: number;
144
+ reason?: string;
145
+ };
146
+ export type RunOnSelfhostedMachine = {
147
+ workspaceId: string;
148
+ agentId: string;
149
+ controlRpc: ControlRpc;
150
+ relay: SelfhostedRelayConfig;
151
+ /** Short request/reply deadline for read/write and other control operations. */
152
+ controlTimeoutMs: number;
153
+ /** Longer agent-side process deadline for exec. */
154
+ execTimeoutMs: number;
155
+ };
156
+ /**
157
+ * Execute the one-off machine operation once the workspace/enrollment lookup has
158
+ * succeeded. Kept separate from {@link runOnSandbox} so the command/deadline
159
+ * contract is deterministic against an in-memory ControlRpc without weakening
160
+ * the production ownership lookup or requiring a real machine.
161
+ */
162
+ export declare function executeRunOnSelfhostedMachine(machine: RunOnSelfhostedMachine, target: string, op: RunOnOp): Promise<RunOnResult>;
163
+ /**
164
+ * Run a ONE-OFF op against a SPECIFIC target WITHOUT changing the active pointer
165
+ * (the design `run_on`). Only selfhosted targets are routable as a one-off here
166
+ * (a Modal target is the session's group box, reached via the normal Channel-A /
167
+ * turn path — `run_on` is for reaching a NON-active enrolled machine without
168
+ * swapping). The op is fenced under the target's enrollment, addressed to its
169
+ * agent subject; an offline machine surfaces a clear reason, never a wrong-box
170
+ * landing.
171
+ */
172
+ export declare function runOnSandbox(services: FleetServices, ctx: FleetContext, target: string, op: RunOnOp): Promise<RunOnResult>;
173
+ export type ProvisionResult = {
174
+ kind: "selfhosted";
175
+ instructions: string;
176
+ installCommandUnix: string;
177
+ installCommandWindows: string;
178
+ verificationUri: string;
179
+ note: string;
180
+ } | {
181
+ kind: "modal";
182
+ sandbox: SandboxRecord;
183
+ note: string;
184
+ };
185
+ /**
186
+ * Provision a new fleet member.
187
+ * - selfhosted → return the device-flow enrollment instructions (the agent
188
+ * surfaces them to a HUMAN, who installs the agent + enrolls — the agent
189
+ * cannot click the loud whole-machine consent itself).
190
+ * - modal → create a first-class named modal `sandboxes` record (a swap target).
191
+ * NOTE: the Modal BOX is materialized lazily when first swapped-to (Modal
192
+ * lifecycle is owned by the lease — unchanged per).
193
+ */
194
+ export declare function provisionSandbox(services: FleetServices, ctx: FleetContext, input: {
195
+ kind: "selfhosted" | "modal";
196
+ name?: string;
197
+ }): Promise<ProvisionResult>;
@@ -0,0 +1,55 @@
1
+ import { type Settings } from "@opengeni/config";
2
+ import { type Database } from "@opengeni/db";
3
+ import { type EventBus } from "@opengeni/events";
4
+ import { type EstablishedSandboxSession, type SelfhostedRelayConfig } from "@opengeni/runtime/sandbox";
5
+ export type ChannelARoutingServices = {
6
+ db: Database;
7
+ settings: Settings;
8
+ bus?: EventBus;
9
+ };
10
+ /** Map the deployment relay URL to the leaf's `SelfhostedRelayConfig` shape. The
11
+ * relay URL (`OPENGENI_SELFHOSTED_RELAY_URL`) may carry a path (the relay's wss
12
+ * route); a path-less URL defaults to the relay's `/stream` route (M8b). */
13
+ export declare function relayConfigFromSettings(settings: Settings): SelfhostedRelayConfig;
14
+ /** The canonical relay dial-BASE URL (`scheme://host[:port]/stream`) handed to the
15
+ * agent PRODUCER. The agent's relay channel appends ONLY its routing query to
16
+ * this base (`channel.rs`: `format!("{relay_url}{sep}{query}")`) and relies on the
17
+ * base ALREADY carrying the relay's `/stream` route. `OPENGENI_SELFHOSTED_RELAY_URL`
18
+ * is frequently pathless (e.g. `wss://relay.<env>.app.opengeni.ai`), which made the
19
+ * producer dial a path-less URL the relay 400s. Derive the base from the SAME parser
20
+ * the CONSUMER uses (`relayConfigFromSettings`) so producer + consumer always agree
21
+ * on `/stream` — even when the configured URL omits it. An unconfigured relay maps to
22
+ * `""` (graceful degrade: the agent reports no-relay rather than dialing a synthetic
23
+ * host). Fixes preview AND managed prod with no agent rebuild. */
24
+ export declare function relayDialBaseFromSettings(settings: Settings): string;
25
+ /** Whether the routing proxy should wrap the Channel-A box: gated by the
26
+ * selfhosted flag (the active pointer + swap are only meaningful then). */
27
+ export declare function routingEnabled(settings: Settings): boolean;
28
+ /**
29
+ * Wrap an established home session in a `RoutingSandboxSession` so a Channel-A
30
+ * op routes to the session's currently-active sandbox. Provider homes supply a
31
+ * lease; machine homes supply a pinned selfhosted identity and no lease. Returns
32
+ * the established handle with its `session` replaced by the stable proxy.
33
+ */
34
+ export declare function wrapChannelABoxWithRouting(services: ChannelARoutingServices, ids: {
35
+ accountId: string;
36
+ workspaceId: string;
37
+ sessionId: string;
38
+ homeLease?: {
39
+ sandboxGroupId: string;
40
+ leaseEpoch: number;
41
+ instanceId: string;
42
+ backend: string;
43
+ };
44
+ /** A machine-home Channel-A request has no cloud/home lease. Its already
45
+ * established SelfhostedSession is pinned to the durable active pointer so
46
+ * the first command uses the exact machine instance and epoch. */
47
+ pinnedSelfhosted?: {
48
+ sandboxId: string;
49
+ epoch: number;
50
+ };
51
+ directRequest: {
52
+ requestId: string;
53
+ holderId: string;
54
+ };
55
+ }, established: EstablishedSandboxSession): EstablishedSandboxSession;
@@ -0,0 +1,52 @@
1
+ export type ApiSandboxSession = {
2
+ state?: Record<string, unknown> & {
3
+ sandboxId?: string;
4
+ };
5
+ running?(): Promise<boolean>;
6
+ exec?(args: {
7
+ cmd: string;
8
+ workdir?: string;
9
+ runAs?: string;
10
+ yieldTimeMs?: number;
11
+ maxOutputTokens?: number;
12
+ }): Promise<unknown>;
13
+ execCommand?(args: {
14
+ cmd: string;
15
+ workdir?: string;
16
+ runAs?: string;
17
+ yieldTimeMs?: number;
18
+ maxOutputTokens?: number;
19
+ }): Promise<string>;
20
+ shutdown?(options?: unknown): Promise<void>;
21
+ delete?(options?: unknown): Promise<void>;
22
+ close?(): Promise<void>;
23
+ };
24
+ export type ApiSandboxClient = {
25
+ backendId: string;
26
+ deserializeSessionState?(state: Record<string, unknown>): Promise<Record<string, unknown>>;
27
+ resume?(state: Record<string, unknown>, options?: unknown): Promise<ApiSandboxSession>;
28
+ delete?(state: Record<string, unknown>): Promise<void>;
29
+ };
30
+ export type ResumeBoxByIdInput = {
31
+ /**
32
+ * The backend the box was created on — the lease's `resume_backend_id`. Must
33
+ * match the API's configured sandbox client backendId, or the resume is
34
+ * rejected (a cross-backend envelope can never deserialize correctly).
35
+ */
36
+ backend: string;
37
+ /**
38
+ * The serialized resume-state envelope — the lease's `resume_state` jsonb
39
+ * (the record produced by `client.serializeSessionState(state)`). This is the
40
+ * box identity + reattach descriptor; resume() reattaches to the live box by
41
+ * id (warm reattach) or cold-restores from its snapshot.
42
+ */
43
+ resumeState: Record<string, unknown>;
44
+ };
45
+ /**
46
+ * A live, resumed sandbox session for a SINGLE in-process op. The caller
47
+ * resumes → uses (exec/readFile/resolvePort) → drops it; lifecycle/refcount is
48
+ * the lease's job (P1.x), NOT this handle's. The session is non-owned by
49
+ * construction (resume-by-id never owns the box), so dropping it does not
50
+ * terminate the box.
51
+ */
52
+ export type ResumedSandboxSession = ApiSandboxSession;
@@ -0,0 +1,36 @@
1
+ import { SessionAuthorizationActor, SessionAuthorizationListScope, type AccessGrant, type SessionAuthorizationOperation, type SessionAuthorizationSurface, type SessionAuthorizationTarget } from "@opengeni/contracts";
2
+ import type { AppDependencies } from "./dependencies";
3
+ export type SessionAuthorizationDependencies = Pick<AppDependencies, "db" | "sessionAuthorization">;
4
+ /** Maximum time an omitted host hint leaves a live session stream unchecked. */
5
+ export declare const SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS = 15000;
6
+ export declare class SessionAuthorizationDeniedError extends Error {
7
+ readonly reason: "not_found" | "forbidden" | "revoked" | "caller_stale";
8
+ readonly code = "SESSION_NOT_FOUND_OR_DENIED";
9
+ constructor(reason: "not_found" | "forbidden" | "revoked" | "caller_stale");
10
+ }
11
+ export declare class SessionAuthorizationUnavailableError extends Error {
12
+ readonly code = "SESSION_AUTHORIZATION_UNAVAILABLE";
13
+ constructor(options?: ErrorOptions);
14
+ }
15
+ export type ResolvedSessionAuthorization = {
16
+ actor: SessionAuthorizationActor;
17
+ target: SessionAuthorizationTarget;
18
+ relatedSessionAccess: "target" | "root";
19
+ reauthorizeAfterMs: number | null;
20
+ };
21
+ /**
22
+ * Resolve and enforce the host ACL for one session. The target and agent actor
23
+ * are reconstructed from workspace-scoped durable state. A request can supply
24
+ * an immediate target id and signed attempt claims, but can never nominate a
25
+ * lineage root or frozen initiator.
26
+ *
27
+ * Returns null when no host port is bound so standalone behavior stays byte-
28
+ * for-byte unchanged and pays no additional lineage lookup.
29
+ */
30
+ export declare function requireSessionAuthorization(deps: SessionAuthorizationDependencies, grant: AccessGrant, input: {
31
+ sessionId: string;
32
+ operation: SessionAuthorizationOperation;
33
+ surface: SessionAuthorizationSurface;
34
+ }): Promise<ResolvedSessionAuthorization | null>;
35
+ /** Resolve the host's complete current list scope for an in-database query. */
36
+ export declare function requireSessionAuthorizationListScope(deps: SessionAuthorizationDependencies, grant: AccessGrant, surface: SessionAuthorizationSurface): Promise<SessionAuthorizationListScope | null>;
@@ -0,0 +1,71 @@
1
+ import type { TranscribeAudioResponse, VoiceInputErrorCode } from "@opengeni/contracts";
2
+ export type TranscriptionLimits = {
3
+ maxDurationSeconds: number;
4
+ maxSizeBytes: number;
5
+ acceptedMimeTypes: readonly string[];
6
+ };
7
+ export type TranscriptionRequest = {
8
+ workspaceId: string;
9
+ accountId: string;
10
+ audio: Uint8Array;
11
+ mimeType: string;
12
+ /** Optional client-reported duration; enforced as a soft ceiling before upstream. */
13
+ durationSeconds?: number | undefined;
14
+ signal?: AbortSignal | undefined;
15
+ requestId: string;
16
+ };
17
+ export type TranscriptionResult = TranscribeAudioResponse & {
18
+ /** Server-private provider id for operational metrics only. Never returned to clients. */
19
+ providerId: string;
20
+ audioSeconds: number;
21
+ latencyMs: number;
22
+ };
23
+ export declare class TranscriptionServiceError extends Error {
24
+ readonly code: VoiceInputErrorCode;
25
+ readonly status: number;
26
+ readonly retryable: boolean;
27
+ constructor(input: {
28
+ code: VoiceInputErrorCode;
29
+ message: string;
30
+ status?: number;
31
+ retryable?: boolean;
32
+ });
33
+ }
34
+ export declare function statusForVoiceInputError(code: VoiceInputErrorCode): number;
35
+ /** Optional workspace scope for readiness checks during provider selection. */
36
+ export type TranscriptionAvailabilityContext = {
37
+ workspaceId?: string | undefined;
38
+ };
39
+ /**
40
+ * Extensible transcription provider port. Implementations own credentials and
41
+ * upstream request shape. Selection happens before audio is sent; providers must
42
+ * not fall back to another vendor after an upstream request may have started.
43
+ */
44
+ export type TranscriptionProvider = {
45
+ readonly id: string;
46
+ readonly experimental?: boolean | undefined;
47
+ /**
48
+ * Deployment readiness when called without a workspace. When `workspaceId` is
49
+ * provided, providers may require a workspace-attached credential (e.g. Codex).
50
+ */
51
+ available(context?: TranscriptionAvailabilityContext): boolean | Promise<boolean>;
52
+ transcribe(input: {
53
+ audio: Uint8Array;
54
+ mimeType: string;
55
+ filename: string;
56
+ workspaceId: string;
57
+ signal?: AbortSignal | undefined;
58
+ }): Promise<{
59
+ text: string;
60
+ languages: string[];
61
+ }>;
62
+ };
63
+ export type TranscriptionService = {
64
+ limits(): TranscriptionLimits;
65
+ /** True when at least one ready provider can serve requests. */
66
+ available(context?: TranscriptionAvailabilityContext): boolean | Promise<boolean>;
67
+ transcribe(request: TranscriptionRequest): Promise<TranscriptionResult>;
68
+ };
69
+ export declare function normalizeMimeType(mimeType: string): string;
70
+ export declare function isAcceptedMimeType(mimeType: string, accepted: readonly string[]): boolean;
71
+ export declare function filenameForMimeType(mimeType: string): string;
@@ -0,0 +1,4 @@
1
+ /** One global Temporal Schedule owns bounded delivery of committed session wakes. */
2
+ export declare const SESSION_WORKFLOW_WAKE_DISPATCHER_SCHEDULE_ID = "opengeni-session-workflow-wake-dispatcher";
3
+ export declare const SESSION_WORKFLOW_WAKE_DISPATCHER_WORKFLOW_TYPE = "sessionWorkflowWakeDispatcherWorkflow";
4
+ export declare const SESSION_WORKFLOW_WAKE_DISPATCHER_PERIOD_MS = 10000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/core",
3
- "version": "0.12.10",
3
+ "version": "0.14.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": {
@@ -28,21 +28,21 @@
28
28
  "provenance": true
29
29
  },
30
30
  "scripts": {
31
- "typecheck": "tsgo --noEmit",
32
- "build": "tsup",
31
+ "typecheck": "tsc --noEmit",
32
+ "build": "bun ../../scripts/build-typescript-package.ts",
33
33
  "prepublishOnly": "bash ../../scripts/prepublish-guard"
34
34
  },
35
35
  "dependencies": {
36
36
  "@modelcontextprotocol/sdk": "^1.29.0",
37
- "@opengeni/codex": "^0.2.7",
38
- "@opengeni/config": "^0.7.13",
39
- "@opengeni/contracts": "^0.23.0",
40
- "@opengeni/db": "^0.14.3",
41
- "@opengeni/documents": "^0.2.45",
42
- "@opengeni/events": "^0.3.36",
37
+ "@opengeni/codex": "^0.2.9",
38
+ "@opengeni/config": "^0.7.22",
39
+ "@opengeni/contracts": "^0.26.1",
40
+ "@opengeni/db": "^0.16.2",
41
+ "@opengeni/documents": "^0.2.59",
42
+ "@opengeni/events": "^0.3.50",
43
43
  "@opengeni/observability": "^0.3.0",
44
- "@opengeni/runtime": "^0.14.3",
45
- "@opengeni/storage": "^0.2.37",
44
+ "@opengeni/runtime": "^0.14.16",
45
+ "@opengeni/storage": "^0.2.46",
46
46
  "hono": "^4.12.18"
47
47
  },
48
48
  "engines": {
@@ -1,6 +1,7 @@
1
1
  import type { Settings } from "@opengeni/config";
2
2
  import {
3
3
  verifyDelegatedAccessToken,
4
+ type AccountGrant,
4
5
  type AccessContext,
5
6
  type AccessGrant,
6
7
  type Permission,
@@ -39,10 +40,64 @@ export async function requireAccessGrant(
39
40
  workspaceId: string,
40
41
  permission?: Permission,
41
42
  ): Promise<AccessGrant> {
43
+ return (await requireAccessGrantAuthorization(c, deps, workspaceId, permission)).grant;
44
+ }
45
+
46
+ export type AccessGrantAuthorization = {
47
+ grant: AccessGrant;
48
+ accountGrant: AccountGrant | null;
49
+ authenticatedSubjectId: string;
50
+ contextIntegrity: boolean;
51
+ };
52
+
53
+ export function accessGrantAuthorizationFromContext(
54
+ context: AccessContext,
55
+ grant: AccessGrant,
56
+ ): AccessGrantAuthorization {
57
+ const matchingAccountGrants = context.accountGrants.filter(
58
+ (candidate) => candidate.accountId === grant.accountId,
59
+ );
60
+ const delegated = grant.metadata?.delegated === true;
61
+ const contextIntegrity =
62
+ context.subjectId === grant.subjectId &&
63
+ context.accountGrants.every((candidate) => candidate.subjectId === context.subjectId) &&
64
+ context.workspaceGrants.every(
65
+ (candidate) =>
66
+ candidate.subjectId === context.subjectId &&
67
+ candidate.principalKind === grant.principalKind &&
68
+ (candidate.metadata?.delegated === true) === delegated &&
69
+ Boolean(candidate.serviceInitiator) === Boolean(grant.serviceInitiator) &&
70
+ Boolean(candidate.serviceInitiatorContext) === Boolean(grant.serviceInitiatorContext) &&
71
+ context.accountGrants.filter(
72
+ (accountGrant) => accountGrant.accountId === candidate.accountId,
73
+ ).length === 1,
74
+ ) &&
75
+ matchingAccountGrants.length === 1 &&
76
+ matchingAccountGrants[0]?.subjectId === context.subjectId;
77
+ return {
78
+ grant,
79
+ accountGrant: contextIntegrity ? matchingAccountGrants[0]! : null,
80
+ authenticatedSubjectId: context.subjectId,
81
+ contextIntegrity,
82
+ };
83
+ }
84
+
85
+ export async function requireAccessGrantAuthorization(
86
+ c: Context,
87
+ deps: AccessDeps,
88
+ workspaceId: string,
89
+ permission?: Permission,
90
+ ): Promise<AccessGrantAuthorization> {
42
91
  const context = await requireAccessContext(c, deps);
92
+ const principalKind = hostedHumanSessionPrincipalKind(context);
43
93
  const grant =
44
94
  context.workspaceGrants.find((candidate) => candidate.workspaceId === workspaceId) ??
45
- (await getWorkspaceGrant(deps.db, context.subjectId, workspaceId));
95
+ (await getWorkspaceGrant(
96
+ deps.db,
97
+ context.subjectId,
98
+ workspaceId,
99
+ principalKind ? { principalKind } : undefined,
100
+ ));
46
101
  if (!grant) {
47
102
  const workspace = await requireWorkspace(deps.db, workspaceId).catch(() => null);
48
103
  if (!workspace) {
@@ -53,7 +108,21 @@ export async function requireAccessGrant(
53
108
  if (permission) {
54
109
  requirePermission(grant, permission);
55
110
  }
56
- return grant;
111
+ return accessGrantAuthorizationFromContext(context, grant);
112
+ }
113
+
114
+ function hostedHumanSessionPrincipalKind(context: AccessContext): "human_session" | undefined {
115
+ if (context.mode !== "managed" || context.workspaceGrants.length === 0) {
116
+ return undefined;
117
+ }
118
+ return context.workspaceGrants.every(
119
+ (grant) =>
120
+ grant.principalKind === "human_session" &&
121
+ grant.metadata?.delegated !== true &&
122
+ !grant.serviceInitiator,
123
+ )
124
+ ? "human_session"
125
+ : undefined;
57
126
  }
58
127
 
59
128
  export function requirePermission(grant: AccessGrant, permission: Permission): void {
@@ -191,6 +260,7 @@ async function apiKeyAccessContext(
191
260
  subjectId,
192
261
  subjectLabel: apiKey.name,
193
262
  permissions: apiKey.permissions,
263
+ principalKind: "api_key",
194
264
  },
195
265
  ]
196
266
  : [],
@@ -231,6 +301,7 @@ async function delegatedAccessContext(
231
301
  subjectId: payload.subjectId,
232
302
  ...(payload.subjectLabel ? { subjectLabel: payload.subjectLabel } : {}),
233
303
  permissions: payload.permissions,
304
+ principalKind: payload.principalKind,
234
305
  // sessionId is worker-asserted (HMAC-signed token claim), not agent
235
306
  // controlled; it scopes session-bound MCP tools such as goal management.
236
307
  metadata: {
@@ -48,6 +48,7 @@ function mapNewSessionDraft(
48
48
  toolsProvided: newSessionDraftToolsProvided(row),
49
49
  model: row.model,
50
50
  reasoningEffort: row.reasoningEffort,
51
+ latencyMode: row.latencyMode,
51
52
  options: publicNewSessionDraftOptions(row),
52
53
  updatedAt: row.updatedAt.toISOString(),
53
54
  });
@@ -169,6 +170,7 @@ export async function getActorNewSessionDraft(
169
170
  toolsProvided: false,
170
171
  model: deps.settings.openaiModel,
171
172
  reasoningEffort: deps.settings.openaiReasoningEffort,
173
+ latencyMode: "standard",
172
174
  options: {},
173
175
  updatedAt: null,
174
176
  }
@@ -223,6 +225,7 @@ export async function saveActorNewSessionDraft(
223
225
  toolsProvided,
224
226
  model: input.model,
225
227
  reasoningEffort: input.reasoningEffort,
228
+ latencyMode: input.latencyMode,
226
229
  options: input.options,
227
230
  // Only managed people are removed through removeWorkspaceMember().
228
231
  // API keys and delegated service actors (for example the first-party