@opengeni/core 0.20.16 → 0.21.10
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 +8 -0
- package/dist/application/session-commands.d.ts +7 -2
- package/dist/dependencies.d.ts +3 -1
- package/dist/domain/capabilities.d.ts +7 -0
- package/dist/domain/memory-slack-publication.d.ts +0 -2
- package/dist/domain/scheduled-tasks.d.ts +25 -1
- package/dist/domain/sessions.d.ts +30 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +453 -102
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +53 -0
- package/dist/sandbox/fleet.d.ts +9 -1
- package/dist/session-authorization.d.ts +9 -0
- package/dist/transcription.d.ts +35 -0
- package/package.json +10 -10
- package/src/access/index.ts +43 -3
- package/src/application/session-commands.ts +29 -9
- package/src/dependencies.ts +3 -1
- package/src/domain/capabilities.ts +99 -29
- package/src/domain/memory-slack-publication.ts +5 -17
- package/src/domain/resources.ts +10 -20
- package/src/domain/scheduled-tasks.ts +221 -7
- package/src/domain/sessions.ts +127 -37
- package/src/index.ts +1 -0
- package/src/managed-auth-type.ts +4 -3
- package/src/managed-session.ts +36 -0
- package/src/sandbox/fleet.ts +56 -1
- package/src/sandbox/routing.ts +26 -0
- package/src/session-authorization.ts +17 -0
- package/src/transcription.ts +40 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import type { ManagedAuth } from "./managed-auth-type";
|
|
3
|
+
/**
|
|
4
|
+
* Read a Better Auth session without bypassing its sliding-cookie renewal.
|
|
5
|
+
*
|
|
6
|
+
* Better Auth can refresh the durable session while resolving `getSession`.
|
|
7
|
+
* Programmatic callers must explicitly request and forward the returned cookie
|
|
8
|
+
* headers; the HTTP handler does this automatically, but direct API calls do not.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getManagedSession(c: Context, auth: ManagedAuth): Promise<{
|
|
11
|
+
session: {
|
|
12
|
+
id: string;
|
|
13
|
+
createdAt: Date;
|
|
14
|
+
updatedAt: Date;
|
|
15
|
+
userId: string;
|
|
16
|
+
expiresAt: Date;
|
|
17
|
+
token: string;
|
|
18
|
+
ipAddress?: string | null | undefined;
|
|
19
|
+
userAgent?: string | null | undefined;
|
|
20
|
+
} | {
|
|
21
|
+
[x: string]: any;
|
|
22
|
+
[x: number]: any;
|
|
23
|
+
[x: symbol]: any;
|
|
24
|
+
id: string;
|
|
25
|
+
createdAt: Date;
|
|
26
|
+
updatedAt: Date;
|
|
27
|
+
userId: string;
|
|
28
|
+
expiresAt: Date;
|
|
29
|
+
token: string;
|
|
30
|
+
ipAddress?: string | null | undefined;
|
|
31
|
+
userAgent?: string | null | undefined;
|
|
32
|
+
};
|
|
33
|
+
user: {
|
|
34
|
+
id: string;
|
|
35
|
+
createdAt: Date;
|
|
36
|
+
updatedAt: Date;
|
|
37
|
+
email: string;
|
|
38
|
+
emailVerified: boolean;
|
|
39
|
+
name: string;
|
|
40
|
+
image?: string | null | undefined;
|
|
41
|
+
} | {
|
|
42
|
+
[x: string]: any;
|
|
43
|
+
[x: number]: any;
|
|
44
|
+
[x: symbol]: any;
|
|
45
|
+
id: string;
|
|
46
|
+
createdAt: Date;
|
|
47
|
+
updatedAt: Date;
|
|
48
|
+
email: string;
|
|
49
|
+
emailVerified: boolean;
|
|
50
|
+
name: string;
|
|
51
|
+
image?: string | null | undefined;
|
|
52
|
+
};
|
|
53
|
+
} | null>;
|
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 } from "@opengeni/runtime/sandbox";
|
|
4
|
+
import { type BackendUnresolvableCode, type ControlRpc, type SelfhostedRelayConfig, type SelfhostedOpStreamDeps } from "@opengeni/runtime/sandbox";
|
|
5
5
|
export type FleetServices = {
|
|
6
6
|
db: Database;
|
|
7
7
|
settings: Settings;
|
|
@@ -44,6 +44,7 @@ export declare function buildFleetContextForSession(deps: {
|
|
|
44
44
|
}): Promise<FleetContext>;
|
|
45
45
|
/** The dominant liveness of a fleet member, surfaced to the dock + the agent. */
|
|
46
46
|
export type FleetLiveness = "online" | "reconnecting" | "offline";
|
|
47
|
+
export type FleetOperationAvailability = "ready" | "wakeable" | "recovering" | "unavailable";
|
|
47
48
|
/**
|
|
48
49
|
* A fleet member as the agent + the dock see it (the M8b/M9 UI seam — the
|
|
49
50
|
* `sandboxes_list` response entry the dock renders). STABLE shape: the dock keys
|
|
@@ -65,6 +66,11 @@ export type FleetSandboxEntry = {
|
|
|
65
66
|
enrollmentId: string | null;
|
|
66
67
|
/** Whether this target can be attached/swapped to right now (live + addressable). */
|
|
67
68
|
attachable: boolean;
|
|
69
|
+
/** Whether an ordinary shell/files operation can use this target. This is
|
|
70
|
+
* deliberately separate from `attachable`: an idle managed home sandbox can
|
|
71
|
+
* be wakeable even while its holderless lease is cold/draining and therefore
|
|
72
|
+
* not an already-live swap target. */
|
|
73
|
+
operationAvailability: FleetOperationAvailability;
|
|
68
74
|
/** Selfhosted only: whether whole-machine + screen-control consent is acked. */
|
|
69
75
|
consented?: boolean;
|
|
70
76
|
/** Selfhosted only: whether a display (real/Xvfb) is present. */
|
|
@@ -154,6 +160,8 @@ export type RunOnSelfhostedMachine = {
|
|
|
154
160
|
controlTimeoutMs: number;
|
|
155
161
|
/** Longer agent-side process deadline for exec. */
|
|
156
162
|
execTimeoutMs: number;
|
|
163
|
+
/** Streaming transport required when execTimeoutMs is 0 (unbounded). */
|
|
164
|
+
opStream?: SelfhostedOpStreamDeps;
|
|
157
165
|
};
|
|
158
166
|
/**
|
|
159
167
|
* Execute the one-off machine operation once the workspace/enrollment lookup has
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SessionAuthorizationActor, SessionAuthorizationListScope, type AccessGrant, type SessionAuthorizationOperation, type SessionAuthorizationSurface, type SessionAuthorizationTarget } from "@opengeni/contracts";
|
|
2
|
+
import { type Database } from "@opengeni/db";
|
|
2
3
|
import type { AppDependencies } from "./dependencies";
|
|
3
4
|
export type SessionAuthorizationDependencies = Pick<AppDependencies, "db" | "sessionAuthorization">;
|
|
4
5
|
/** Maximum time an omitted host hint leaves a live session stream unchecked. */
|
|
@@ -18,6 +19,14 @@ export type ResolvedSessionAuthorization = {
|
|
|
18
19
|
relatedSessionAccess: "target" | "root";
|
|
19
20
|
reauthorizeAfterMs: number | null;
|
|
20
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* Prove that a first-party request belongs to the exact currently active
|
|
24
|
+
* attempt of the named caller session. Unlike the optional embedding-host ACL
|
|
25
|
+
* port, this database fence is mandatory for high-trust operations.
|
|
26
|
+
*/
|
|
27
|
+
export declare function requireLiveAgentAttemptAuthorization(db: Database, grant: AccessGrant, callerSessionId: string): Promise<Extract<SessionAuthorizationActor, {
|
|
28
|
+
kind: "agent_attempt";
|
|
29
|
+
}>>;
|
|
21
30
|
/**
|
|
22
31
|
* Resolve and enforce the host ACL for one session. The target and agent actor
|
|
23
32
|
* are reconstructed from workspace-scoped durable state. A request can supply
|
package/dist/transcription.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import type { TranscribeAudioResponse, VoiceInputErrorCode } from "@opengeni/contracts";
|
|
2
|
+
/**
|
|
3
|
+
* Server-owned upstream budget for one provider attempt. Resumable recording
|
|
4
|
+
* claims remain fenced for longer than this budget before another worker may
|
|
5
|
+
* reclaim them. Provider adapters must honor the supplied AbortSignal and must
|
|
6
|
+
* not return while their upstream request is still live; OpenGeni does not
|
|
7
|
+
* claim remote-side idempotency or cancellation for vendors that cannot meet
|
|
8
|
+
* that adapter contract.
|
|
9
|
+
*/
|
|
10
|
+
export declare const TRANSCRIPTION_PROVIDER_REQUEST_TIMEOUT_MILLISECONDS: number;
|
|
2
11
|
export type TranscriptionLimits = {
|
|
3
12
|
maxDurationSeconds: number;
|
|
4
13
|
maxSizeBytes: number;
|
|
@@ -13,6 +22,10 @@ export type TranscriptionRequest = {
|
|
|
13
22
|
durationSeconds?: number | undefined;
|
|
14
23
|
signal?: AbortSignal | undefined;
|
|
15
24
|
requestId: string;
|
|
25
|
+
/** Absolute server-owned provider deadline persisted for resumable attempts. */
|
|
26
|
+
providerDeadlineAt?: Date | undefined;
|
|
27
|
+
/** Exact provider selected before a resumable segment is first sent upstream. */
|
|
28
|
+
providerId?: string | undefined;
|
|
16
29
|
};
|
|
17
30
|
export type TranscriptionResult = TranscribeAudioResponse & {
|
|
18
31
|
/** Server-private provider id for operational metrics only. Never returned to clients. */
|
|
@@ -43,6 +56,8 @@ export type TranscriptionAvailabilityContext = {
|
|
|
43
56
|
*/
|
|
44
57
|
export type TranscriptionProvider = {
|
|
45
58
|
readonly id: string;
|
|
59
|
+
/** The adapter guarantees that its upstream transport honors AbortSignal. */
|
|
60
|
+
readonly supportsServerDeadline: true;
|
|
46
61
|
readonly experimental?: boolean | undefined;
|
|
47
62
|
/**
|
|
48
63
|
* Deployment readiness when called without a workspace. When `workspaceId` is
|
|
@@ -54,6 +69,7 @@ export type TranscriptionProvider = {
|
|
|
54
69
|
mimeType: string;
|
|
55
70
|
filename: string;
|
|
56
71
|
workspaceId: string;
|
|
72
|
+
requestId: string;
|
|
57
73
|
signal?: AbortSignal | undefined;
|
|
58
74
|
}): Promise<{
|
|
59
75
|
text: string;
|
|
@@ -64,8 +80,27 @@ export type TranscriptionService = {
|
|
|
64
80
|
limits(): TranscriptionLimits;
|
|
65
81
|
/** True when at least one ready provider can serve requests. */
|
|
66
82
|
available(context?: TranscriptionAvailabilityContext): boolean | Promise<boolean>;
|
|
83
|
+
/** Select one provider before a durable segment attempt; retries pin this id. */
|
|
84
|
+
selectProvider?(context: TranscriptionAvailabilityContext): string | null | Promise<string | null>;
|
|
67
85
|
transcribe(request: TranscriptionRequest): Promise<TranscriptionResult>;
|
|
68
86
|
};
|
|
87
|
+
export type PreparedTranscriptionSegment = {
|
|
88
|
+
segmentNumber: number;
|
|
89
|
+
startMilliseconds: number;
|
|
90
|
+
durationMilliseconds: number;
|
|
91
|
+
mimeType: "audio/wav";
|
|
92
|
+
bytes: Uint8Array;
|
|
93
|
+
};
|
|
94
|
+
export type TranscriptionSegmenter = {
|
|
95
|
+
available(): boolean | Promise<boolean>;
|
|
96
|
+
segment(input: {
|
|
97
|
+
sourceMimeType: string;
|
|
98
|
+
totalDurationMilliseconds: number;
|
|
99
|
+
providerSegmentSeconds: number;
|
|
100
|
+
chunks: AsyncIterable<Uint8Array>;
|
|
101
|
+
signal?: AbortSignal | undefined;
|
|
102
|
+
}): AsyncIterable<PreparedTranscriptionSegment>;
|
|
103
|
+
};
|
|
69
104
|
export declare function normalizeMimeType(mimeType: string): string;
|
|
70
105
|
export declare function isAcceptedMimeType(mimeType: string, accepted: readonly string[]): boolean;
|
|
71
106
|
export declare function filenameForMimeType(mimeType: string): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.10",
|
|
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": {
|
|
@@ -34,15 +34,15 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37
|
-
"@opengeni/codex": "^0.2.
|
|
38
|
-
"@opengeni/config": "^0.
|
|
39
|
-
"@opengeni/contracts": "^0.
|
|
40
|
-
"@opengeni/db": "^0.
|
|
41
|
-
"@opengeni/documents": "^0.5.
|
|
42
|
-
"@opengeni/events": "^0.3.
|
|
43
|
-
"@opengeni/observability": "^0.
|
|
44
|
-
"@opengeni/runtime": "^0.18.
|
|
45
|
-
"@opengeni/storage": "^0.2.
|
|
37
|
+
"@opengeni/codex": "^0.2.13",
|
|
38
|
+
"@opengeni/config": "^0.12.1",
|
|
39
|
+
"@opengeni/contracts": "^0.40.0",
|
|
40
|
+
"@opengeni/db": "^0.28.9",
|
|
41
|
+
"@opengeni/documents": "^0.5.18",
|
|
42
|
+
"@opengeni/events": "^0.3.88",
|
|
43
|
+
"@opengeni/observability": "^0.5.6",
|
|
44
|
+
"@opengeni/runtime": "^0.18.24",
|
|
45
|
+
"@opengeni/storage": "^0.2.75",
|
|
46
46
|
"hono": "^4.12.18"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
package/src/access/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import type { Context } from "hono";
|
|
18
18
|
import { HTTPException } from "hono/http-exception";
|
|
19
19
|
import type { ManagedAuth } from "../managed-auth-type";
|
|
20
|
+
import { getManagedSession } from "../managed-session";
|
|
20
21
|
|
|
21
22
|
const bearerPrefix = "Bearer ";
|
|
22
23
|
|
|
@@ -143,10 +144,51 @@ export function requirePermission(grant: AccessGrant, permission: Permission): v
|
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Require a permission to be present literally on the grant. This deliberately
|
|
149
|
+
* does not expand workspace:admin or deprecated aliases and is reserved for
|
|
150
|
+
* authorities, such as plaintext secret reads, that old broad grants must not
|
|
151
|
+
* acquire implicitly.
|
|
152
|
+
*/
|
|
153
|
+
export function requireLiteralPermission(grant: AccessGrant, permission: Permission): void {
|
|
154
|
+
if (!hasLiteralPermission(grant.permissions, permission)) {
|
|
155
|
+
throw new HTTPException(403, {
|
|
156
|
+
message: `missing literal permission: ${permission}`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function hasLiteralPermission(permissions: Permission[], permission: Permission): boolean {
|
|
162
|
+
return permissions.includes(permission);
|
|
163
|
+
}
|
|
164
|
+
|
|
146
165
|
export function hasPermission(permissions: Permission[], permission: Permission): boolean {
|
|
166
|
+
if (permission === "secrets:read") {
|
|
167
|
+
return permissions.includes("secrets:read");
|
|
168
|
+
}
|
|
147
169
|
const aliases: Partial<Record<Permission, Permission[]>> = {
|
|
148
170
|
"variable-sets:use": ["environments:use" as Permission],
|
|
149
171
|
"variable-sets:manage": ["environments:manage" as Permission],
|
|
172
|
+
"variable-sets:list": [
|
|
173
|
+
"variable-sets:use",
|
|
174
|
+
"variable-sets:manage",
|
|
175
|
+
"environments:use" as Permission,
|
|
176
|
+
"environments:manage" as Permission,
|
|
177
|
+
],
|
|
178
|
+
"variable-sets:read": [
|
|
179
|
+
"variable-sets:use",
|
|
180
|
+
"variable-sets:manage",
|
|
181
|
+
"environments:use" as Permission,
|
|
182
|
+
"environments:manage" as Permission,
|
|
183
|
+
],
|
|
184
|
+
"variable-sets:write": ["variable-sets:manage", "environments:manage" as Permission],
|
|
185
|
+
"secrets:list": [
|
|
186
|
+
"variable-sets:use",
|
|
187
|
+
"variable-sets:manage",
|
|
188
|
+
"environments:use" as Permission,
|
|
189
|
+
"environments:manage" as Permission,
|
|
190
|
+
],
|
|
191
|
+
"secrets:write": ["variable-sets:manage", "environments:manage" as Permission],
|
|
150
192
|
};
|
|
151
193
|
return (
|
|
152
194
|
permissions.includes(permission) ||
|
|
@@ -210,9 +252,7 @@ async function resolveAccessContext(c: Context, deps: AccessDeps): Promise<Acces
|
|
|
210
252
|
}
|
|
211
253
|
|
|
212
254
|
if (deps.managedAuth) {
|
|
213
|
-
const session = await deps.managedAuth
|
|
214
|
-
headers: c.req.raw.headers,
|
|
215
|
-
});
|
|
255
|
+
const session = await getManagedSession(c, deps.managedAuth);
|
|
216
256
|
if (session?.user) {
|
|
217
257
|
return await ensureManagedAccessForUser(deps.db, {
|
|
218
258
|
userId: session.user.id,
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
type EventBus,
|
|
47
47
|
} from "@opengeni/events";
|
|
48
48
|
import type { SessionWorkflowClient } from "../dependencies";
|
|
49
|
+
import { normalizeResources } from "../domain/resources";
|
|
49
50
|
import {
|
|
50
51
|
requireSessionAuthorization,
|
|
51
52
|
type ResolvedSessionAuthorization,
|
|
@@ -200,10 +201,14 @@ async function publishAndWakeAgentCommand(
|
|
|
200
201
|
? { interruptionRequested: true }
|
|
201
202
|
: {}),
|
|
202
203
|
});
|
|
203
|
-
} catch
|
|
204
|
+
} catch {
|
|
204
205
|
console.warn(
|
|
205
|
-
|
|
206
|
-
|
|
206
|
+
"[session-commands] immediate Agent command wake failed; durable outbox will retry",
|
|
207
|
+
{
|
|
208
|
+
errorClass: "WorkflowWakeOperationError",
|
|
209
|
+
errorCode: "agent_command_wake_failed",
|
|
210
|
+
origin: "core",
|
|
211
|
+
},
|
|
207
212
|
);
|
|
208
213
|
}
|
|
209
214
|
}
|
|
@@ -222,10 +227,15 @@ async function requestControlWakeDispatch(
|
|
|
222
227
|
if (wakeCount === 0) return;
|
|
223
228
|
try {
|
|
224
229
|
await deps.workflowClient.requestSessionWorkflowWakeDispatch();
|
|
225
|
-
} catch
|
|
230
|
+
} catch {
|
|
226
231
|
console.warn(
|
|
227
|
-
|
|
228
|
-
|
|
232
|
+
"[session-commands] immediate control wake dispatch failed; durable outbox will retry",
|
|
233
|
+
{
|
|
234
|
+
errorClass: "WorkflowWakeOperationError",
|
|
235
|
+
errorCode: "control_wake_dispatch_failed",
|
|
236
|
+
origin: "core",
|
|
237
|
+
wakeCount,
|
|
238
|
+
},
|
|
229
239
|
);
|
|
230
240
|
}
|
|
231
241
|
}
|
|
@@ -565,7 +575,7 @@ export async function steerHumanQueuePrompt(
|
|
|
565
575
|
return response;
|
|
566
576
|
}
|
|
567
577
|
|
|
568
|
-
export async function
|
|
578
|
+
export async function controlHumanSessionWorkstreamWithOutcome(
|
|
569
579
|
deps: {
|
|
570
580
|
db: Database;
|
|
571
581
|
bus: EventBus;
|
|
@@ -574,7 +584,7 @@ export async function controlHumanSessionWorkstream(
|
|
|
574
584
|
},
|
|
575
585
|
context: HumanSessionCommandContext,
|
|
576
586
|
input: SessionControlRequest,
|
|
577
|
-
): Promise<SessionControlResponse> {
|
|
587
|
+
): Promise<{ response: SessionControlResponse; replay: boolean }> {
|
|
578
588
|
const authorization = await authorizeHumanSessionCommand(deps, context, "session.control");
|
|
579
589
|
const result = await withWorkspaceRls(deps.db, context.workspaceId, (scoped) =>
|
|
580
590
|
scoped.transaction((tx) =>
|
|
@@ -607,7 +617,16 @@ export async function controlHumanSessionWorkstream(
|
|
|
607
617
|
}
|
|
608
618
|
await publishWorkspaceControlEvent(deps, context.workspaceId, result.workspaceControlEventId);
|
|
609
619
|
await requestControlWakeDispatch(deps, result.wakeCount);
|
|
610
|
-
return response;
|
|
620
|
+
return { response, replay: result.replay };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** Backward-compatible response path used by the REST control route. */
|
|
624
|
+
export async function controlHumanSessionWorkstream(
|
|
625
|
+
deps: Parameters<typeof controlHumanSessionWorkstreamWithOutcome>[0],
|
|
626
|
+
context: Parameters<typeof controlHumanSessionWorkstreamWithOutcome>[1],
|
|
627
|
+
input: Parameters<typeof controlHumanSessionWorkstreamWithOutcome>[2],
|
|
628
|
+
): Promise<SessionControlResponse> {
|
|
629
|
+
return (await controlHumanSessionWorkstreamWithOutcome(deps, context, input)).response;
|
|
611
630
|
}
|
|
612
631
|
|
|
613
632
|
export async function controlHumanWorkspace(
|
|
@@ -692,6 +711,7 @@ export async function saveHumanComposerDraft(
|
|
|
692
711
|
saveComposerDraftInTransaction(tx as unknown as Database, {
|
|
693
712
|
...context,
|
|
694
713
|
...input,
|
|
714
|
+
resources: normalizeResources(input.resources),
|
|
695
715
|
subjectId: context.subjectId,
|
|
696
716
|
}),
|
|
697
717
|
),
|
package/src/dependencies.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { Observability } from "@opengeni/observability";
|
|
|
15
15
|
import type { createObjectStorage } from "@opengeni/storage";
|
|
16
16
|
import type { ManagedAuth } from "./managed-auth-type";
|
|
17
17
|
import type { ApiSandboxClient, ResumeBoxByIdInput, ResumedSandboxSession } from "./sandbox-types";
|
|
18
|
-
import type { TranscriptionService } from "./transcription";
|
|
18
|
+
import type { TranscriptionSegmenter, TranscriptionService } from "./transcription";
|
|
19
19
|
|
|
20
20
|
export type SessionWorkflowClient = {
|
|
21
21
|
signalUserMessage: (input: {
|
|
@@ -123,6 +123,8 @@ export type AppDependencies = {
|
|
|
123
123
|
oauthCallbackDeadlineMs?: number;
|
|
124
124
|
/** Optional host-owned voice-input transcription service. */
|
|
125
125
|
transcription?: TranscriptionService | null;
|
|
126
|
+
/** Optional host-owned long-form audio normalization/segmentation service. */
|
|
127
|
+
transcriptionSegmenter?: TranscriptionSegmenter | null;
|
|
126
128
|
// The API process's OWN agent-loop-free sandbox client (constructed from
|
|
127
129
|
// settings via @opengeni/runtime/sandbox). Undefined when sandboxBackend=none.
|
|
128
130
|
// This is the foundation of the API-direct control plane: the API resumes
|
|
@@ -85,6 +85,7 @@ export async function buildCapabilityCatalog(input: {
|
|
|
85
85
|
socialConnections,
|
|
86
86
|
bundledSkills,
|
|
87
87
|
curatedLibrarySkills,
|
|
88
|
+
codexAppsCredentialId,
|
|
88
89
|
] = await Promise.all([
|
|
89
90
|
listCapabilityCatalogItems(input.db, input.workspaceId),
|
|
90
91
|
listCapabilityInstallations(input.db, input.workspaceId),
|
|
@@ -93,6 +94,9 @@ export async function buildCapabilityCatalog(input: {
|
|
|
93
94
|
listSocialConnections(input.db, input.workspaceId, 500, input.subjectId),
|
|
94
95
|
discoverBundledSkills(),
|
|
95
96
|
discoverCuratedSkillLibraryItems(),
|
|
97
|
+
input.settings.codexConnectedAppsEnabled
|
|
98
|
+
? resolveCodexAppsCredentialIdForRun(input.db, input.workspaceId)
|
|
99
|
+
: Promise.resolve(null),
|
|
96
100
|
]);
|
|
97
101
|
const capabilityInstallationById = new Map(
|
|
98
102
|
capabilityInstallations.map((installation) => [installation.capabilityId, installation]),
|
|
@@ -112,7 +116,16 @@ export async function buildCapabilityCatalog(input: {
|
|
|
112
116
|
...bundledSkills,
|
|
113
117
|
...curatedLibrarySkills,
|
|
114
118
|
];
|
|
115
|
-
const
|
|
119
|
+
const codexApps = input.settings.codexConnectedAppsEnabled
|
|
120
|
+
? codexAppsCatalogItem(codexAppsCredentialId !== null)
|
|
121
|
+
: null;
|
|
122
|
+
const items = dedupeCatalogItems([
|
|
123
|
+
...builtIns,
|
|
124
|
+
...persistedItems.filter((item) => !isReservedCodexAppsCatalogItem(item)),
|
|
125
|
+
// Keep the reserved, server-derived item authoritative over any stale
|
|
126
|
+
// legacy catalog row with the same id.
|
|
127
|
+
...(codexApps ? [codexApps] : []),
|
|
128
|
+
])
|
|
116
129
|
.map((item) =>
|
|
117
130
|
applyCapabilityEnablement(item, capabilityInstallationById.get(item.id), activePackIds),
|
|
118
131
|
)
|
|
@@ -142,8 +155,9 @@ export async function createCatalogItem(input: {
|
|
|
142
155
|
}
|
|
143
156
|
if (
|
|
144
157
|
input.payload.kind === "mcp" &&
|
|
145
|
-
|
|
146
|
-
|
|
158
|
+
(id === `mcp:${CODEX_APPS_MCP_SERVER_ID}` ||
|
|
159
|
+
(typeof input.payload.metadata.mcpServerId === "string" &&
|
|
160
|
+
input.payload.metadata.mcpServerId.trim() === CODEX_APPS_MCP_SERVER_ID))
|
|
147
161
|
) {
|
|
148
162
|
throw new HTTPException(422, {
|
|
149
163
|
message: `${CODEX_APPS_MCP_SERVER_ID} is reserved for the canonical Codex Apps service`,
|
|
@@ -997,34 +1011,84 @@ function packCatalogItem(
|
|
|
997
1011
|
}
|
|
998
1012
|
|
|
999
1013
|
function configuredMcpCatalogItems(settings: Settings): CapabilityCatalogItem[] {
|
|
1000
|
-
return settings.mcpServers
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
:
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1014
|
+
return settings.mcpServers
|
|
1015
|
+
.filter((server) => server.id !== CODEX_APPS_MCP_SERVER_ID)
|
|
1016
|
+
.map((server) =>
|
|
1017
|
+
CapabilityCatalogItem.parse({
|
|
1018
|
+
id: `mcp:${server.id}`,
|
|
1019
|
+
kind: "mcp",
|
|
1020
|
+
source: firstPartyMcpServerIds.has(server.id) ? "built_in" : "configured",
|
|
1021
|
+
name: server.name ?? server.id,
|
|
1022
|
+
description: firstPartyMcpDescription(server.id),
|
|
1023
|
+
category: firstPartyMcpServerIds.has(server.id) ? "platform" : "configured",
|
|
1024
|
+
tags: ["mcp", ...(server.allowedTools?.length ? ["limited-tools"] : [])],
|
|
1025
|
+
endpointUrl: server.url,
|
|
1026
|
+
tools: [{ kind: "mcp", id: server.id }],
|
|
1027
|
+
runtime: {
|
|
1028
|
+
available: true,
|
|
1029
|
+
mcpServerId: server.id,
|
|
1030
|
+
transport: "streamable-http",
|
|
1031
|
+
notes: firstPartyMcpServerIds.has(server.id)
|
|
1032
|
+
? "Available from OpenGeni runtime configuration."
|
|
1033
|
+
: "Configured through OPENGENI_MCP_SERVERS.",
|
|
1034
|
+
},
|
|
1035
|
+
metadata: {
|
|
1036
|
+
mcpServerId: server.id,
|
|
1037
|
+
allowedTools: server.allowedTools ?? [],
|
|
1038
|
+
cacheToolsList: server.cacheToolsList,
|
|
1039
|
+
},
|
|
1040
|
+
}),
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function isReservedCodexAppsCatalogItem(item: CapabilityCatalogItem): boolean {
|
|
1045
|
+
return (
|
|
1046
|
+
item.id === `mcp:${CODEX_APPS_MCP_SERVER_ID}` ||
|
|
1047
|
+
(item.kind === "mcp" &&
|
|
1048
|
+
(item.runtime.mcpServerId === CODEX_APPS_MCP_SERVER_ID ||
|
|
1049
|
+
item.metadata.mcpServerId === CODEX_APPS_MCP_SERVER_ID))
|
|
1025
1050
|
);
|
|
1026
1051
|
}
|
|
1027
1052
|
|
|
1053
|
+
/**
|
|
1054
|
+
* The Apps MCP is not a configured server and is not a user-installable
|
|
1055
|
+
* capability. It is a deployment-gated, workspace-designated runtime surface,
|
|
1056
|
+
* so project it into the same catalog the web picker already consumes while
|
|
1057
|
+
* keeping its authorization state server-derived and explicit.
|
|
1058
|
+
*/
|
|
1059
|
+
export function codexAppsCatalogItem(available: boolean): CapabilityCatalogItem {
|
|
1060
|
+
return CapabilityCatalogItem.parse({
|
|
1061
|
+
id: `mcp:${CODEX_APPS_MCP_SERVER_ID}`,
|
|
1062
|
+
kind: "mcp",
|
|
1063
|
+
source: "built_in",
|
|
1064
|
+
name: "Codex Apps",
|
|
1065
|
+
description:
|
|
1066
|
+
"Use the ChatGPT Apps designated for this workspace. Sessions include this surface by default when it is authorized; explicit policies can opt out.",
|
|
1067
|
+
category: "productivity",
|
|
1068
|
+
tags: ["mcp", "codex", "connected-apps"],
|
|
1069
|
+
providerDomain: "chatgpt.com",
|
|
1070
|
+
surfaceType: "codex_apps",
|
|
1071
|
+
transport: "streamable-http",
|
|
1072
|
+
mcpUrl: CODEX_APPS_MCP_URL,
|
|
1073
|
+
authKind: "none",
|
|
1074
|
+
tools: [{ kind: "mcp", id: CODEX_APPS_MCP_SERVER_ID }],
|
|
1075
|
+
runtime: {
|
|
1076
|
+
available,
|
|
1077
|
+
...(available ? { mcpServerId: CODEX_APPS_MCP_SERVER_ID } : {}),
|
|
1078
|
+
transport: "streamable-http",
|
|
1079
|
+
notes: available
|
|
1080
|
+
? "Available through the active workspace Apps designation."
|
|
1081
|
+
: "Unavailable until an active Codex Apps credential is designated for this workspace.",
|
|
1082
|
+
},
|
|
1083
|
+
enabled: available,
|
|
1084
|
+
enabledReason: available ? "designated Apps credential" : "no active Apps designation",
|
|
1085
|
+
metadata: {
|
|
1086
|
+
mcpServerId: CODEX_APPS_MCP_SERVER_ID,
|
|
1087
|
+
authorization: "workspace_designation",
|
|
1088
|
+
},
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1028
1092
|
function platformApiCatalogItems(socialConnections: SocialConnection[]): CapabilityCatalogItem[] {
|
|
1029
1093
|
const xConnection = preferredSocialConnection(socialConnections, "x");
|
|
1030
1094
|
const xEnabled = xConnection?.status === "connected" || xConnection?.status === "needs_reauth";
|
|
@@ -1276,6 +1340,12 @@ export function applyCapabilityEnablement(
|
|
|
1276
1340
|
// connector is browseable, not that an account is already connected.
|
|
1277
1341
|
return { ...item, connectionRef: null };
|
|
1278
1342
|
}
|
|
1343
|
+
if (item.surfaceType === "codex_apps") {
|
|
1344
|
+
// Unlike ordinary built-ins, Apps availability is derived from the exact
|
|
1345
|
+
// active workspace designation above. Do not overwrite it with the
|
|
1346
|
+
// generic source-based "built in" enablement rule.
|
|
1347
|
+
return { ...item, connectionRef: null };
|
|
1348
|
+
}
|
|
1279
1349
|
if (item.source === "built_in" || item.source === "configured") {
|
|
1280
1350
|
return {
|
|
1281
1351
|
...item,
|
|
@@ -1606,7 +1676,7 @@ function capabilityInstallationRuntimeReady(
|
|
|
1606
1676
|
}
|
|
1607
1677
|
|
|
1608
1678
|
/**
|
|
1609
|
-
* Checks the
|
|
1679
|
+
* Checks the generic installation projection (header names only) against the
|
|
1610
1680
|
* capability's declared credential requirements.
|
|
1611
1681
|
*/
|
|
1612
1682
|
function storedCredentialHeadersSatisfy(
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import {
|
|
3
|
-
redactSensitiveText,
|
|
4
3
|
stableJson,
|
|
5
4
|
type KnowledgeMemoryKind,
|
|
6
5
|
type KnowledgeMemoryStatus,
|
|
@@ -109,13 +108,11 @@ export type MemorySlackPublicationProjection = {
|
|
|
109
108
|
importance: MemorySlackImportance;
|
|
110
109
|
deliveryMode: MemorySlackDeliveryMode;
|
|
111
110
|
summary: string;
|
|
112
|
-
summaryRedacted: boolean;
|
|
113
111
|
summaryTruncated: boolean;
|
|
114
112
|
namespace: string;
|
|
115
113
|
labels: string[];
|
|
116
114
|
labelsTruncated: boolean;
|
|
117
115
|
ownerLabel: string | null;
|
|
118
|
-
ownerLabelRedacted: boolean;
|
|
119
116
|
ownerLabelTruncated: boolean;
|
|
120
117
|
authoritativeRecord: {
|
|
121
118
|
workspaceId: string;
|
|
@@ -201,9 +198,8 @@ export function evaluateMemorySlackPublication(
|
|
|
201
198
|
if (!deliveryMode) return denied("below_noise_policy");
|
|
202
199
|
|
|
203
200
|
const collapsedSummary = collapseText(input.distribution.shareSummary);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const summary = truncateUtf8(redactedSummary, MEMORY_SLACK_SUMMARY_MAX_UTF8_BYTES);
|
|
201
|
+
if (!collapsedSummary) return denied("missing_summary");
|
|
202
|
+
const summary = truncateUtf8(collapsedSummary, MEMORY_SLACK_SUMMARY_MAX_UTF8_BYTES);
|
|
207
203
|
|
|
208
204
|
const namespace = normalizeNamespace(input.memory.namespace);
|
|
209
205
|
const labels = normalizeLabels(input.memory.labels);
|
|
@@ -221,13 +217,11 @@ export function evaluateMemorySlackPublication(
|
|
|
221
217
|
importance: input.distribution.importance,
|
|
222
218
|
deliveryMode,
|
|
223
219
|
summary: summary.value,
|
|
224
|
-
summaryRedacted: redactedSummary !== collapsedSummary,
|
|
225
220
|
summaryTruncated: summary.truncated,
|
|
226
221
|
namespace,
|
|
227
222
|
labels: labels.values,
|
|
228
223
|
labelsTruncated: labels.truncated,
|
|
229
224
|
ownerLabel: owner.value,
|
|
230
|
-
ownerLabelRedacted: owner.redacted,
|
|
231
225
|
ownerLabelTruncated: owner.truncated,
|
|
232
226
|
authoritativeRecord: {
|
|
233
227
|
workspaceId: input.context.workspaceId,
|
|
@@ -368,10 +362,8 @@ function effectiveDeliveryMode(
|
|
|
368
362
|
|
|
369
363
|
function normalizeNamespace(value: string): string | null {
|
|
370
364
|
const trimmed = value.trim();
|
|
371
|
-
if (redactSensitiveText(trimmed) !== trimmed) return null;
|
|
372
365
|
const namespace = trimmed.toLowerCase();
|
|
373
366
|
if (!namespace || utf8Bytes(namespace) > MEMORY_SLACK_NAMESPACE_MAX_UTF8_BYTES) return null;
|
|
374
|
-
if (redactSensitiveText(namespace) !== namespace) return null;
|
|
375
367
|
const segments = namespace.split("/");
|
|
376
368
|
if (segments.some((segment) => !SELECTOR_SEGMENT_PATTERN.test(segment))) return null;
|
|
377
369
|
return segments.join("/");
|
|
@@ -385,10 +377,8 @@ function normalizeLabels(
|
|
|
385
377
|
for (const value of values) {
|
|
386
378
|
if (typeof value !== "string") return null;
|
|
387
379
|
const trimmed = value.trim();
|
|
388
|
-
if (redactSensitiveText(trimmed) !== trimmed) return null;
|
|
389
380
|
const label = trimmed.toLowerCase();
|
|
390
381
|
if (
|
|
391
|
-
redactSensitiveText(label) !== label ||
|
|
392
382
|
!SELECTOR_SEGMENT_PATTERN.test(label) ||
|
|
393
383
|
utf8Bytes(label) > MEMORY_SLACK_LABEL_MAX_UTF8_BYTES
|
|
394
384
|
) {
|
|
@@ -406,14 +396,12 @@ function normalizeLabels(
|
|
|
406
396
|
function boundedOptionalText(
|
|
407
397
|
value: string | null | undefined,
|
|
408
398
|
maxBytes: number,
|
|
409
|
-
): { value: string | null;
|
|
399
|
+
): { value: string | null; truncated: boolean } {
|
|
410
400
|
const collapsed = collapseText(value ?? "");
|
|
411
|
-
if (!collapsed) return { value: null,
|
|
412
|
-
const
|
|
413
|
-
const bounded = truncateUtf8(redacted, maxBytes);
|
|
401
|
+
if (!collapsed) return { value: null, truncated: false };
|
|
402
|
+
const bounded = truncateUtf8(collapsed, maxBytes);
|
|
414
403
|
return {
|
|
415
404
|
value: bounded.value || null,
|
|
416
|
-
redacted: redacted !== collapsed,
|
|
417
405
|
truncated: bounded.truncated,
|
|
418
406
|
};
|
|
419
407
|
}
|