@kodelyth/codex 2026.5.40 → 2026.5.42

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 (178) hide show
  1. package/dist/client-ChMX13_o.js +642 -0
  2. package/dist/client-factory-D3dIsp4Y.js +9 -0
  3. package/dist/command-formatters-BRW7_Nu7.js +519 -0
  4. package/dist/command-handlers-P2IqtXaZ.js +1462 -0
  5. package/dist/compact-baos5flR.js +329 -0
  6. package/dist/computer-use-VfLvTMaa.js +367 -0
  7. package/dist/config-CezENx_E.js +510 -0
  8. package/dist/doctor-contract-api.js +53 -0
  9. package/dist/harness.js +51 -0
  10. package/dist/index.js +1133 -0
  11. package/dist/media-understanding-provider.js +335 -0
  12. package/dist/models-B9DhrIwD.js +110 -0
  13. package/dist/node-cli-sessions-De4_DuFw.js +1216 -0
  14. package/dist/plugin-activation-BlMuJeXz.js +452 -0
  15. package/dist/prompt-overlay.js +12 -0
  16. package/dist/protocol-C9UWI98H.js +9 -0
  17. package/dist/protocol-validators-BGBspNmF.js +5988 -0
  18. package/dist/provider-catalog.js +84 -0
  19. package/dist/provider-discovery.js +33 -0
  20. package/dist/provider.js +150 -0
  21. package/dist/rate-limit-cache-CHuacE27.js +24 -0
  22. package/dist/request-CTQKUxaa.js +89 -0
  23. package/dist/rolldown-runtime-DUslC3ob.js +14 -0
  24. package/dist/run-attempt-DqV2OU1R.js +5366 -0
  25. package/dist/session-binding-3PzU7ZTW.js +222 -0
  26. package/dist/shared-client-Cnyr9dyT.js +631 -0
  27. package/dist/side-question-CP5XlA0U.js +667 -0
  28. package/dist/test-api.js +45 -0
  29. package/dist/thread-lifecycle-DBJetBuV.js +1561 -0
  30. package/dist/vision-tools-Cl_5a93K.js +1379 -0
  31. package/doctor-contract-api.test.ts +44 -0
  32. package/doctor-contract-api.ts +68 -0
  33. package/harness.ts +72 -0
  34. package/index.test.ts +230 -0
  35. package/index.ts +66 -0
  36. package/klaw.plugin.json +24 -85
  37. package/media-understanding-provider.test.ts +486 -0
  38. package/media-understanding-provider.ts +521 -0
  39. package/package.json +3 -3
  40. package/prompt-overlay-runtime-contract.test.ts +48 -0
  41. package/prompt-overlay.ts +21 -0
  42. package/provider-catalog.ts +83 -0
  43. package/provider-discovery.ts +45 -0
  44. package/provider.test.ts +384 -0
  45. package/provider.ts +243 -0
  46. package/src/app-server/app-inventory-cache.test.ts +176 -0
  47. package/src/app-server/app-inventory-cache.ts +324 -0
  48. package/src/app-server/approval-bridge.test.ts +1471 -0
  49. package/src/app-server/approval-bridge.ts +1211 -0
  50. package/src/app-server/auth-bridge.test.ts +1449 -0
  51. package/src/app-server/auth-bridge.ts +614 -0
  52. package/src/app-server/auth-profile-runtime-contract.test.ts +239 -0
  53. package/src/app-server/capabilities.ts +27 -0
  54. package/src/app-server/client-factory.ts +24 -0
  55. package/src/app-server/client.test.ts +563 -0
  56. package/src/app-server/client.ts +715 -0
  57. package/src/app-server/compact.test.ts +710 -0
  58. package/src/app-server/compact.ts +500 -0
  59. package/src/app-server/computer-use.test.ts +788 -0
  60. package/src/app-server/computer-use.ts +683 -0
  61. package/src/app-server/config.test.ts +879 -0
  62. package/src/app-server/config.ts +1038 -0
  63. package/src/app-server/context-engine-projection.test.ts +252 -0
  64. package/src/app-server/context-engine-projection.ts +403 -0
  65. package/src/app-server/delivery-no-reply-runtime-contract.test.ts +80 -0
  66. package/src/app-server/dynamic-tool-diagnostics.ts +73 -0
  67. package/src/app-server/dynamic-tool-profile.ts +69 -0
  68. package/src/app-server/dynamic-tools.test.ts +1302 -0
  69. package/src/app-server/dynamic-tools.ts +623 -0
  70. package/src/app-server/elicitation-bridge.test.ts +1056 -0
  71. package/src/app-server/elicitation-bridge.ts +783 -0
  72. package/src/app-server/event-projector.test.ts +2668 -0
  73. package/src/app-server/event-projector.ts +2057 -0
  74. package/src/app-server/image-payload-sanitizer.test.ts +49 -0
  75. package/src/app-server/image-payload-sanitizer.ts +167 -0
  76. package/src/app-server/klaw-owned-tool-runtime-contract.test.ts +456 -0
  77. package/src/app-server/local-runtime-attribution.ts +39 -0
  78. package/src/app-server/managed-binary.test.ts +139 -0
  79. package/src/app-server/managed-binary.ts +193 -0
  80. package/src/app-server/models.test.ts +246 -0
  81. package/src/app-server/models.ts +172 -0
  82. package/src/app-server/native-hook-relay.test.ts +271 -0
  83. package/src/app-server/native-hook-relay.ts +150 -0
  84. package/src/app-server/native-subagent-task-mirror.test.ts +573 -0
  85. package/src/app-server/native-subagent-task-mirror.ts +497 -0
  86. package/src/app-server/outcome-fallback-runtime-contract.test.ts +404 -0
  87. package/src/app-server/plugin-activation.test.ts +336 -0
  88. package/src/app-server/plugin-activation.ts +283 -0
  89. package/src/app-server/plugin-app-cache-key.ts +74 -0
  90. package/src/app-server/plugin-approval-roundtrip.ts +122 -0
  91. package/src/app-server/plugin-inventory.test.ts +355 -0
  92. package/src/app-server/plugin-inventory.ts +357 -0
  93. package/src/app-server/plugin-thread-config.test.ts +865 -0
  94. package/src/app-server/plugin-thread-config.ts +455 -0
  95. package/src/app-server/protocol-generated/json/DynamicToolCallParams.json +33 -0
  96. package/src/app-server/protocol-generated/json/v2/ErrorNotification.json +199 -0
  97. package/src/app-server/protocol-generated/json/v2/GetAccountResponse.json +102 -0
  98. package/src/app-server/protocol-generated/json/v2/ModelListResponse.json +227 -0
  99. package/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json +2630 -0
  100. package/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json +2630 -0
  101. package/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json +1659 -0
  102. package/src/app-server/protocol-generated/json/v2/TurnStartResponse.json +1655 -0
  103. package/src/app-server/protocol-validators.test.ts +75 -0
  104. package/src/app-server/protocol-validators.ts +203 -0
  105. package/src/app-server/protocol.ts +520 -0
  106. package/src/app-server/rate-limit-cache.ts +48 -0
  107. package/src/app-server/rate-limits.test.ts +202 -0
  108. package/src/app-server/rate-limits.ts +583 -0
  109. package/src/app-server/request.ts +73 -0
  110. package/src/app-server/run-attempt.context-engine.test.ts +1004 -0
  111. package/src/app-server/run-attempt.test.ts +9477 -0
  112. package/src/app-server/run-attempt.ts +4683 -0
  113. package/src/app-server/run-attempt.vision-tools.test.ts +35 -0
  114. package/src/app-server/schema-normalization-runtime-contract.test.ts +206 -0
  115. package/src/app-server/session-binding.test.ts +303 -0
  116. package/src/app-server/session-binding.ts +398 -0
  117. package/src/app-server/session-history.ts +44 -0
  118. package/src/app-server/shared-client.test.ts +589 -0
  119. package/src/app-server/shared-client.ts +289 -0
  120. package/src/app-server/side-question.test.ts +1175 -0
  121. package/src/app-server/side-question.ts +1007 -0
  122. package/src/app-server/test-support.ts +48 -0
  123. package/src/app-server/thread-lifecycle.test.ts +447 -0
  124. package/src/app-server/thread-lifecycle.ts +939 -0
  125. package/src/app-server/thread-lifecycle.user-mcp-servers.test.ts +442 -0
  126. package/src/app-server/timeout.ts +9 -0
  127. package/src/app-server/tool-progress-normalization.ts +77 -0
  128. package/src/app-server/trajectory.test.ts +205 -0
  129. package/src/app-server/trajectory.ts +365 -0
  130. package/src/app-server/transcript-mirror.test.ts +524 -0
  131. package/src/app-server/transcript-mirror.ts +208 -0
  132. package/src/app-server/transcript-repair-runtime-contract.test.ts +44 -0
  133. package/src/app-server/transport-stdio.test.ts +171 -0
  134. package/src/app-server/transport-stdio.ts +107 -0
  135. package/src/app-server/transport-websocket.test.ts +69 -0
  136. package/src/app-server/transport-websocket.ts +90 -0
  137. package/src/app-server/transport.ts +117 -0
  138. package/src/app-server/user-input-bridge.test.ts +249 -0
  139. package/src/app-server/user-input-bridge.ts +316 -0
  140. package/src/app-server/version.ts +4 -0
  141. package/src/app-server/vision-tools.ts +12 -0
  142. package/src/command-account.ts +544 -0
  143. package/src/command-formatters.ts +425 -0
  144. package/src/command-handlers.ts +2004 -0
  145. package/src/command-rpc.test.ts +16 -0
  146. package/src/command-rpc.ts +142 -0
  147. package/src/commands.test.ts +3312 -0
  148. package/src/commands.ts +65 -0
  149. package/src/conversation-binding-data.ts +124 -0
  150. package/src/conversation-binding.test.ts +599 -0
  151. package/src/conversation-binding.ts +561 -0
  152. package/src/conversation-control.test.ts +126 -0
  153. package/src/conversation-control.ts +303 -0
  154. package/src/conversation-turn-collector.test.ts +191 -0
  155. package/src/conversation-turn-collector.ts +186 -0
  156. package/src/conversation-turn-input.test.ts +141 -0
  157. package/src/conversation-turn-input.ts +106 -0
  158. package/src/manifest.test.ts +20 -0
  159. package/src/migration/apply.ts +501 -0
  160. package/src/migration/helpers.ts +55 -0
  161. package/src/migration/plan.ts +461 -0
  162. package/src/migration/provider.test.ts +1741 -0
  163. package/src/migration/provider.ts +41 -0
  164. package/src/migration/source.ts +643 -0
  165. package/src/migration/targets.ts +25 -0
  166. package/src/node-cli-sessions.test.ts +180 -0
  167. package/src/node-cli-sessions.ts +711 -0
  168. package/test-api.ts +82 -0
  169. package/tsconfig.json +16 -0
  170. package/doctor-contract-api.js +0 -7
  171. package/harness.js +0 -7
  172. package/index.js +0 -7
  173. package/media-understanding-provider.js +0 -7
  174. package/prompt-overlay.js +0 -7
  175. package/provider-catalog.js +0 -7
  176. package/provider-discovery.js +0 -7
  177. package/provider.js +0 -7
  178. package/test-api.js +0 -7
@@ -0,0 +1,122 @@
1
+ import {
2
+ callGatewayTool,
3
+ type EmbeddedRunAttemptParams,
4
+ } from "klaw/plugin-sdk/agent-harness-runtime";
5
+
6
+ const DEFAULT_CODEX_APPROVAL_TIMEOUT_MS = 120_000;
7
+ const MAX_PLUGIN_APPROVAL_TITLE_LENGTH = 80;
8
+ const MAX_PLUGIN_APPROVAL_DESCRIPTION_LENGTH = 256;
9
+
10
+ type ExecApprovalDecision = "allow-once" | "allow-always" | "deny";
11
+
12
+ export type AppServerApprovalOutcome =
13
+ | "approved-once"
14
+ | "approved-session"
15
+ | "denied"
16
+ | "unavailable"
17
+ | "cancelled";
18
+
19
+ type ApprovalRequestResult = {
20
+ id?: string;
21
+ decision?: ExecApprovalDecision | null;
22
+ };
23
+
24
+ type ApprovalWaitResult = {
25
+ id?: string;
26
+ decision?: ExecApprovalDecision | null;
27
+ };
28
+
29
+ export async function requestPluginApproval(params: {
30
+ paramsForRun: EmbeddedRunAttemptParams;
31
+ title: string;
32
+ description: string;
33
+ severity: "info" | "warning";
34
+ toolName: string;
35
+ toolCallId?: string;
36
+ }): Promise<ApprovalRequestResult | undefined> {
37
+ const timeoutMs = DEFAULT_CODEX_APPROVAL_TIMEOUT_MS;
38
+ return callGatewayTool(
39
+ "plugin.approval.request",
40
+ { timeoutMs: timeoutMs + 10_000 },
41
+ {
42
+ pluginId: "klaw-codex-app-server",
43
+ title: truncateForGateway(params.title, MAX_PLUGIN_APPROVAL_TITLE_LENGTH),
44
+ description: truncateForGateway(params.description, MAX_PLUGIN_APPROVAL_DESCRIPTION_LENGTH),
45
+ severity: params.severity,
46
+ toolName: params.toolName,
47
+ toolCallId: params.toolCallId,
48
+ agentId: params.paramsForRun.agentId,
49
+ sessionKey: params.paramsForRun.sessionKey,
50
+ turnSourceChannel: params.paramsForRun.messageChannel ?? params.paramsForRun.messageProvider,
51
+ turnSourceTo: params.paramsForRun.currentChannelId,
52
+ turnSourceAccountId: params.paramsForRun.agentAccountId,
53
+ turnSourceThreadId: params.paramsForRun.currentThreadTs,
54
+ timeoutMs,
55
+ twoPhase: true,
56
+ },
57
+ { expectFinal: false },
58
+ ) as Promise<ApprovalRequestResult | undefined>;
59
+ }
60
+
61
+ export function approvalRequestExplicitlyUnavailable(result: unknown): boolean {
62
+ if (result === null || result === undefined || typeof result !== "object") {
63
+ return false;
64
+ }
65
+ let descriptor: PropertyDescriptor | undefined;
66
+ try {
67
+ descriptor = Object.getOwnPropertyDescriptor(result, "decision");
68
+ } catch {
69
+ return false;
70
+ }
71
+ return descriptor !== undefined && "value" in descriptor && descriptor.value === null;
72
+ }
73
+
74
+ export async function waitForPluginApprovalDecision(params: {
75
+ approvalId: string;
76
+ signal?: AbortSignal;
77
+ }): Promise<ExecApprovalDecision | null | undefined> {
78
+ const timeoutMs = DEFAULT_CODEX_APPROVAL_TIMEOUT_MS;
79
+ const waitPromise: Promise<ApprovalWaitResult | undefined> = callGatewayTool(
80
+ "plugin.approval.waitDecision",
81
+ { timeoutMs: timeoutMs + 10_000 },
82
+ { id: params.approvalId },
83
+ );
84
+ if (!params.signal) {
85
+ return (await waitPromise)?.decision;
86
+ }
87
+ let onAbort: (() => void) | undefined;
88
+ const abortPromise = new Promise<never>((_, reject) => {
89
+ if (params.signal!.aborted) {
90
+ reject(params.signal!.reason);
91
+ return;
92
+ }
93
+ onAbort = () => reject(params.signal!.reason);
94
+ params.signal!.addEventListener("abort", onAbort, { once: true });
95
+ });
96
+ try {
97
+ return (await Promise.race([waitPromise, abortPromise]))?.decision;
98
+ } finally {
99
+ if (onAbort) {
100
+ params.signal.removeEventListener("abort", onAbort);
101
+ }
102
+ }
103
+ }
104
+
105
+ export function mapExecDecisionToOutcome(
106
+ decision: ExecApprovalDecision | null | undefined,
107
+ ): AppServerApprovalOutcome {
108
+ if (decision === "allow-once") {
109
+ return "approved-once";
110
+ }
111
+ if (decision === "allow-always") {
112
+ return "approved-session";
113
+ }
114
+ if (decision === null || decision === undefined) {
115
+ return "unavailable";
116
+ }
117
+ return "denied";
118
+ }
119
+
120
+ function truncateForGateway(value: string, maxLength: number): string {
121
+ return value.length <= maxLength ? value : `${value.slice(0, Math.max(0, maxLength - 3))}...`;
122
+ }
@@ -0,0 +1,355 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { CodexAppInventoryCache } from "./app-inventory-cache.js";
3
+ import { CODEX_PLUGINS_MARKETPLACE_NAME } from "./config.js";
4
+ import { findOpenAiCuratedPluginSummary, readCodexPluginInventory } from "./plugin-inventory.js";
5
+ import type { v2 } from "./protocol.js";
6
+
7
+ describe("Codex plugin inventory", () => {
8
+ it("returns enabled migrated curated plugins with stable owned app ids", async () => {
9
+ const appCache = new CodexAppInventoryCache();
10
+ await appCache.refreshNow({
11
+ key: "runtime",
12
+ nowMs: 0,
13
+ request: async () => ({
14
+ data: [appInfo("google-calendar-app", true)],
15
+ nextCursor: null,
16
+ }),
17
+ });
18
+ const calls: string[] = [];
19
+ const inventory = await readCodexPluginInventory({
20
+ pluginConfig: {
21
+ codexPlugins: {
22
+ enabled: true,
23
+ plugins: {
24
+ "google-calendar": {
25
+ marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
26
+ pluginName: "google-calendar",
27
+ },
28
+ slack: {
29
+ enabled: false,
30
+ marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
31
+ pluginName: "slack",
32
+ },
33
+ },
34
+ },
35
+ },
36
+ appCache,
37
+ appCacheKey: "runtime",
38
+ nowMs: 1,
39
+ request: async (method, params) => {
40
+ calls.push(method);
41
+ if (method === "plugin/list") {
42
+ return pluginList([
43
+ pluginSummary("google-calendar", { installed: true, enabled: true }),
44
+ pluginSummary("slack", { installed: true, enabled: true }),
45
+ ]);
46
+ }
47
+ if (method === "plugin/read") {
48
+ expect(params).toEqual({
49
+ marketplacePath: "/marketplaces/openai-curated",
50
+ pluginName: "google-calendar",
51
+ });
52
+ return pluginDetail("google-calendar", [appSummary("google-calendar-app")]);
53
+ }
54
+ throw new Error(`unexpected request ${method}`);
55
+ },
56
+ });
57
+
58
+ expect(inventory.records).toHaveLength(1);
59
+ const record = inventory.records[0];
60
+ expect(record?.policy.pluginName).toBe("google-calendar");
61
+ expect(record?.summary.installed).toBe(true);
62
+ expect(record?.summary.enabled).toBe(true);
63
+ expect(record?.appOwnership).toBe("proven");
64
+ expect(record?.ownedAppIds).toStrictEqual(["google-calendar-app"]);
65
+ expect(record?.apps).toStrictEqual([
66
+ {
67
+ id: "google-calendar-app",
68
+ name: "google-calendar-app",
69
+ accessible: true,
70
+ enabled: true,
71
+ needsAuth: false,
72
+ },
73
+ ]);
74
+ expect(calls).toEqual(["plugin/list", "plugin/read"]);
75
+ });
76
+
77
+ it("matches namespaced curated plugin ids by normalized path segment", async () => {
78
+ const appCache = new CodexAppInventoryCache();
79
+ await appCache.refreshNow({
80
+ key: "runtime",
81
+ nowMs: 0,
82
+ request: async () => ({
83
+ data: [appInfo("github-app", true)],
84
+ nextCursor: null,
85
+ }),
86
+ });
87
+
88
+ const listed = pluginList([
89
+ pluginSummary("openai-curated/github", {
90
+ name: "GitHub",
91
+ installed: true,
92
+ enabled: true,
93
+ }),
94
+ ]);
95
+ expect(findOpenAiCuratedPluginSummary(listed, "github")?.summary.id).toBe(
96
+ "openai-curated/github",
97
+ );
98
+
99
+ const inventory = await readCodexPluginInventory({
100
+ pluginConfig: {
101
+ codexPlugins: {
102
+ enabled: true,
103
+ plugins: {
104
+ github: {
105
+ marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
106
+ pluginName: "github",
107
+ },
108
+ },
109
+ },
110
+ },
111
+ appCache,
112
+ appCacheKey: "runtime",
113
+ nowMs: 1,
114
+ request: async (method, params) => {
115
+ if (method === "plugin/list") {
116
+ return listed;
117
+ }
118
+ if (method === "plugin/read") {
119
+ expect(params).toEqual({
120
+ marketplacePath: "/marketplaces/openai-curated",
121
+ pluginName: "github",
122
+ });
123
+ return pluginDetail("github", [appSummary("github-app")]);
124
+ }
125
+ throw new Error(`unexpected request ${method}`);
126
+ },
127
+ });
128
+
129
+ expect(inventory.records).toHaveLength(1);
130
+ const record = inventory.records[0];
131
+ expect(record?.policy.pluginName).toBe("github");
132
+ expect(record?.summary.id).toBe("openai-curated/github");
133
+ expect(record?.summary.installed).toBe(true);
134
+ expect(record?.summary.enabled).toBe(true);
135
+ expect(record?.appOwnership).toBe("proven");
136
+ expect(record?.ownedAppIds).toStrictEqual(["github-app"]);
137
+ expect(inventory.diagnostics.map((diagnostic) => diagnostic.code)).not.toContain(
138
+ "plugin_missing",
139
+ );
140
+ });
141
+
142
+ it("fails closed when plugin detail apps are absent from app inventory", async () => {
143
+ const appCache = new CodexAppInventoryCache();
144
+ await appCache.refreshNow({
145
+ key: "runtime",
146
+ nowMs: 0,
147
+ request: async () => ({
148
+ data: [],
149
+ nextCursor: null,
150
+ }),
151
+ });
152
+ const inventory = await readCodexPluginInventory({
153
+ pluginConfig: {
154
+ codexPlugins: {
155
+ enabled: true,
156
+ plugins: {
157
+ "google-calendar": {
158
+ marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
159
+ pluginName: "google-calendar",
160
+ },
161
+ },
162
+ },
163
+ },
164
+ appCache,
165
+ appCacheKey: "runtime",
166
+ nowMs: 1,
167
+ request: async (method) => {
168
+ if (method === "plugin/list") {
169
+ return pluginList([pluginSummary("google-calendar", { installed: true, enabled: true })]);
170
+ }
171
+ if (method === "plugin/read") {
172
+ return pluginDetail("google-calendar", [appSummary("google-calendar-app")]);
173
+ }
174
+ throw new Error(`unexpected request ${method}`);
175
+ },
176
+ });
177
+
178
+ const record = inventory.records[0];
179
+ expect(record?.appOwnership).toBe("proven");
180
+ expect(record?.authRequired).toBe(true);
181
+ expect(record?.ownedAppIds).toStrictEqual(["google-calendar-app"]);
182
+ expect(record?.apps).toStrictEqual([
183
+ {
184
+ id: "google-calendar-app",
185
+ name: "google-calendar-app",
186
+ accessible: false,
187
+ enabled: false,
188
+ needsAuth: true,
189
+ },
190
+ ]);
191
+ });
192
+
193
+ it("marks display-name-only app matches ambiguous instead of exposing app ids", async () => {
194
+ const appCache = new CodexAppInventoryCache();
195
+ await appCache.refreshNow({
196
+ key: "runtime",
197
+ nowMs: 0,
198
+ request: async () => ({
199
+ data: [
200
+ {
201
+ ...appInfo("calendar-app", true),
202
+ pluginDisplayNames: ["Google Calendar"],
203
+ },
204
+ ],
205
+ nextCursor: null,
206
+ }),
207
+ });
208
+
209
+ const inventory = await readCodexPluginInventory({
210
+ pluginConfig: {
211
+ codexPlugins: {
212
+ enabled: true,
213
+ plugins: {
214
+ "google-calendar": {
215
+ marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
216
+ pluginName: "google-calendar",
217
+ },
218
+ },
219
+ },
220
+ },
221
+ appCache,
222
+ appCacheKey: "runtime",
223
+ nowMs: 1,
224
+ readPluginDetails: false,
225
+ request: async (method) => {
226
+ if (method === "plugin/list") {
227
+ return pluginList([
228
+ pluginSummary("google-calendar", {
229
+ name: "Google Calendar",
230
+ installed: true,
231
+ enabled: true,
232
+ }),
233
+ ]);
234
+ }
235
+ throw new Error(`unexpected request ${method}`);
236
+ },
237
+ });
238
+
239
+ expect(inventory.records[0]?.appOwnership).toBe("ambiguous");
240
+ expect(inventory.records[0]?.ownedAppIds).toStrictEqual([]);
241
+ expect(inventory.diagnostics.map((diagnostic) => diagnostic.code)).toStrictEqual([
242
+ "app_ownership_ambiguous",
243
+ ]);
244
+ });
245
+
246
+ it("fails closed when the app inventory cache is missing", async () => {
247
+ const appCache = new CodexAppInventoryCache();
248
+ const inventory = await readCodexPluginInventory({
249
+ pluginConfig: {
250
+ codexPlugins: {
251
+ enabled: true,
252
+ plugins: {
253
+ "google-calendar": {
254
+ marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
255
+ pluginName: "google-calendar",
256
+ },
257
+ },
258
+ },
259
+ },
260
+ appCache,
261
+ appCacheKey: "runtime",
262
+ request: async (method) => {
263
+ if (method === "app/list") {
264
+ return { data: [], nextCursor: null };
265
+ }
266
+ if (method === "plugin/list") {
267
+ return pluginList([pluginSummary("google-calendar", { installed: true, enabled: true })]);
268
+ }
269
+ if (method === "plugin/read") {
270
+ return pluginDetail("google-calendar", [appSummary("google-calendar-app")]);
271
+ }
272
+ throw new Error(`unexpected request ${method}`);
273
+ },
274
+ });
275
+
276
+ expect(inventory.appInventory?.state).toBe("missing");
277
+ expect(inventory.records[0]?.ownedAppIds).toEqual(["google-calendar-app"]);
278
+ expect(inventory.records[0]?.apps).toStrictEqual([]);
279
+ expect(inventory.diagnostics.map((diagnostic) => diagnostic.code)).toStrictEqual([
280
+ "app_inventory_missing",
281
+ ]);
282
+ });
283
+ });
284
+
285
+ function pluginList(plugins: v2.PluginSummary[]): v2.PluginListResponse {
286
+ return {
287
+ marketplaces: [
288
+ {
289
+ name: CODEX_PLUGINS_MARKETPLACE_NAME,
290
+ path: "/marketplaces/openai-curated",
291
+ interface: null,
292
+ plugins,
293
+ },
294
+ ],
295
+ marketplaceLoadErrors: [],
296
+ featuredPluginIds: [],
297
+ };
298
+ }
299
+
300
+ function pluginSummary(id: string, overrides: Partial<v2.PluginSummary> = {}): v2.PluginSummary {
301
+ return {
302
+ id,
303
+ name: id,
304
+ source: { type: "remote" },
305
+ installed: false,
306
+ enabled: false,
307
+ installPolicy: "AVAILABLE",
308
+ authPolicy: "ON_USE",
309
+ availability: "AVAILABLE",
310
+ interface: null,
311
+ ...overrides,
312
+ };
313
+ }
314
+
315
+ function pluginDetail(pluginName: string, apps: v2.AppSummary[]): v2.PluginReadResponse {
316
+ return {
317
+ plugin: {
318
+ marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
319
+ marketplacePath: "/marketplaces/openai-curated",
320
+ summary: pluginSummary(pluginName, { installed: true, enabled: true }),
321
+ description: null,
322
+ skills: [],
323
+ apps,
324
+ mcpServers: [],
325
+ },
326
+ };
327
+ }
328
+
329
+ function appSummary(id: string): v2.AppSummary {
330
+ return {
331
+ id,
332
+ name: id,
333
+ description: null,
334
+ installUrl: null,
335
+ needsAuth: false,
336
+ };
337
+ }
338
+
339
+ function appInfo(id: string, accessible: boolean): v2.AppInfo {
340
+ return {
341
+ id,
342
+ name: id,
343
+ description: null,
344
+ logoUrl: null,
345
+ logoUrlDark: null,
346
+ distributionChannel: null,
347
+ branding: null,
348
+ appMetadata: null,
349
+ labels: null,
350
+ installUrl: null,
351
+ isAccessible: accessible,
352
+ isEnabled: true,
353
+ pluginDisplayNames: [],
354
+ };
355
+ }