@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.
@@ -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>;
@@ -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
@@ -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.20.16",
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.11",
38
- "@opengeni/config": "^0.10.14",
39
- "@opengeni/contracts": "^0.38.3",
40
- "@opengeni/db": "^0.27.11",
41
- "@opengeni/documents": "^0.5.7",
42
- "@opengeni/events": "^0.3.77",
43
- "@opengeni/observability": "^0.4.16",
44
- "@opengeni/runtime": "^0.18.15",
45
- "@opengeni/storage": "^0.2.67",
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": {
@@ -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.api.getSession({
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 (error) {
204
+ } catch {
204
205
  console.warn(
205
- `[session-commands] immediate Agent command wake failed for ${input.workspaceId}/${input.sessionId}; durable outbox will retry`,
206
- error,
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 (error) {
230
+ } catch {
226
231
  console.warn(
227
- `[session-commands] immediate control wake dispatch failed for ${wakeCount} committed revisions; durable outbox will retry`,
228
- error,
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 controlHumanSessionWorkstream(
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
  ),
@@ -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 items = dedupeCatalogItems([...builtIns, ...persistedItems])
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
- typeof input.payload.metadata.mcpServerId === "string" &&
146
- input.payload.metadata.mcpServerId.trim() === CODEX_APPS_MCP_SERVER_ID
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.map((server) =>
1001
- CapabilityCatalogItem.parse({
1002
- id: `mcp:${server.id}`,
1003
- kind: "mcp",
1004
- source: firstPartyMcpServerIds.has(server.id) ? "built_in" : "configured",
1005
- name: server.name ?? server.id,
1006
- description: firstPartyMcpDescription(server.id),
1007
- category: firstPartyMcpServerIds.has(server.id) ? "platform" : "configured",
1008
- tags: ["mcp", ...(server.allowedTools?.length ? ["limited-tools"] : [])],
1009
- endpointUrl: server.url,
1010
- tools: [{ kind: "mcp", id: server.id }],
1011
- runtime: {
1012
- available: true,
1013
- mcpServerId: server.id,
1014
- transport: "streamable-http",
1015
- notes: firstPartyMcpServerIds.has(server.id)
1016
- ? "Available from OpenGeni runtime configuration."
1017
- : "Configured through OPENGENI_MCP_SERVERS.",
1018
- },
1019
- metadata: {
1020
- mcpServerId: server.id,
1021
- allowedTools: server.allowedTools ?? [],
1022
- cacheToolsList: server.cacheToolsList,
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 redacted installation config (header names only) against 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
- const redactedSummary = redactSensitiveText(collapsedSummary);
205
- if (!redactedSummary) return denied("missing_summary");
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; redacted: boolean; truncated: boolean } {
399
+ ): { value: string | null; truncated: boolean } {
410
400
  const collapsed = collapseText(value ?? "");
411
- if (!collapsed) return { value: null, redacted: false, truncated: false };
412
- const redacted = redactSensitiveText(collapsed);
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
  }