@kodelyth/codex 2026.5.42 → 2026.6.2

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 (138) hide show
  1. package/package.json +17 -2
  2. package/doctor-contract-api.test.ts +0 -44
  3. package/doctor-contract-api.ts +0 -68
  4. package/harness.ts +0 -72
  5. package/index.test.ts +0 -230
  6. package/index.ts +0 -66
  7. package/media-understanding-provider.test.ts +0 -486
  8. package/media-understanding-provider.ts +0 -521
  9. package/prompt-overlay-runtime-contract.test.ts +0 -48
  10. package/prompt-overlay.ts +0 -21
  11. package/provider-catalog.ts +0 -83
  12. package/provider-discovery.ts +0 -45
  13. package/provider.test.ts +0 -384
  14. package/provider.ts +0 -243
  15. package/src/app-server/app-inventory-cache.test.ts +0 -176
  16. package/src/app-server/app-inventory-cache.ts +0 -324
  17. package/src/app-server/approval-bridge.test.ts +0 -1471
  18. package/src/app-server/approval-bridge.ts +0 -1211
  19. package/src/app-server/auth-bridge.test.ts +0 -1449
  20. package/src/app-server/auth-bridge.ts +0 -614
  21. package/src/app-server/auth-profile-runtime-contract.test.ts +0 -239
  22. package/src/app-server/capabilities.ts +0 -27
  23. package/src/app-server/client-factory.ts +0 -24
  24. package/src/app-server/client.test.ts +0 -563
  25. package/src/app-server/client.ts +0 -715
  26. package/src/app-server/compact.test.ts +0 -710
  27. package/src/app-server/compact.ts +0 -500
  28. package/src/app-server/computer-use.test.ts +0 -788
  29. package/src/app-server/computer-use.ts +0 -683
  30. package/src/app-server/config.test.ts +0 -879
  31. package/src/app-server/config.ts +0 -1038
  32. package/src/app-server/context-engine-projection.test.ts +0 -252
  33. package/src/app-server/context-engine-projection.ts +0 -403
  34. package/src/app-server/delivery-no-reply-runtime-contract.test.ts +0 -80
  35. package/src/app-server/dynamic-tool-diagnostics.ts +0 -73
  36. package/src/app-server/dynamic-tool-profile.ts +0 -69
  37. package/src/app-server/dynamic-tools.test.ts +0 -1302
  38. package/src/app-server/dynamic-tools.ts +0 -623
  39. package/src/app-server/elicitation-bridge.test.ts +0 -1056
  40. package/src/app-server/elicitation-bridge.ts +0 -783
  41. package/src/app-server/event-projector.test.ts +0 -2668
  42. package/src/app-server/event-projector.ts +0 -2057
  43. package/src/app-server/image-payload-sanitizer.test.ts +0 -49
  44. package/src/app-server/image-payload-sanitizer.ts +0 -167
  45. package/src/app-server/klaw-owned-tool-runtime-contract.test.ts +0 -456
  46. package/src/app-server/local-runtime-attribution.ts +0 -39
  47. package/src/app-server/managed-binary.test.ts +0 -139
  48. package/src/app-server/managed-binary.ts +0 -193
  49. package/src/app-server/models.test.ts +0 -246
  50. package/src/app-server/models.ts +0 -172
  51. package/src/app-server/native-hook-relay.test.ts +0 -271
  52. package/src/app-server/native-hook-relay.ts +0 -150
  53. package/src/app-server/native-subagent-task-mirror.test.ts +0 -573
  54. package/src/app-server/native-subagent-task-mirror.ts +0 -497
  55. package/src/app-server/outcome-fallback-runtime-contract.test.ts +0 -404
  56. package/src/app-server/plugin-activation.test.ts +0 -336
  57. package/src/app-server/plugin-activation.ts +0 -283
  58. package/src/app-server/plugin-app-cache-key.ts +0 -74
  59. package/src/app-server/plugin-approval-roundtrip.ts +0 -122
  60. package/src/app-server/plugin-inventory.test.ts +0 -355
  61. package/src/app-server/plugin-inventory.ts +0 -357
  62. package/src/app-server/plugin-thread-config.test.ts +0 -865
  63. package/src/app-server/plugin-thread-config.ts +0 -455
  64. package/src/app-server/protocol-generated/json/DynamicToolCallParams.json +0 -33
  65. package/src/app-server/protocol-generated/json/v2/ErrorNotification.json +0 -199
  66. package/src/app-server/protocol-generated/json/v2/GetAccountResponse.json +0 -102
  67. package/src/app-server/protocol-generated/json/v2/ModelListResponse.json +0 -227
  68. package/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json +0 -2630
  69. package/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json +0 -2630
  70. package/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json +0 -1659
  71. package/src/app-server/protocol-generated/json/v2/TurnStartResponse.json +0 -1655
  72. package/src/app-server/protocol-validators.test.ts +0 -75
  73. package/src/app-server/protocol-validators.ts +0 -203
  74. package/src/app-server/protocol.ts +0 -520
  75. package/src/app-server/rate-limit-cache.ts +0 -48
  76. package/src/app-server/rate-limits.test.ts +0 -202
  77. package/src/app-server/rate-limits.ts +0 -583
  78. package/src/app-server/request.ts +0 -73
  79. package/src/app-server/run-attempt.context-engine.test.ts +0 -1004
  80. package/src/app-server/run-attempt.test.ts +0 -9477
  81. package/src/app-server/run-attempt.ts +0 -4683
  82. package/src/app-server/run-attempt.vision-tools.test.ts +0 -35
  83. package/src/app-server/schema-normalization-runtime-contract.test.ts +0 -206
  84. package/src/app-server/session-binding.test.ts +0 -303
  85. package/src/app-server/session-binding.ts +0 -398
  86. package/src/app-server/session-history.ts +0 -44
  87. package/src/app-server/shared-client.test.ts +0 -589
  88. package/src/app-server/shared-client.ts +0 -289
  89. package/src/app-server/side-question.test.ts +0 -1175
  90. package/src/app-server/side-question.ts +0 -1007
  91. package/src/app-server/test-support.ts +0 -48
  92. package/src/app-server/thread-lifecycle.test.ts +0 -447
  93. package/src/app-server/thread-lifecycle.ts +0 -939
  94. package/src/app-server/thread-lifecycle.user-mcp-servers.test.ts +0 -442
  95. package/src/app-server/timeout.ts +0 -9
  96. package/src/app-server/tool-progress-normalization.ts +0 -77
  97. package/src/app-server/trajectory.test.ts +0 -205
  98. package/src/app-server/trajectory.ts +0 -365
  99. package/src/app-server/transcript-mirror.test.ts +0 -524
  100. package/src/app-server/transcript-mirror.ts +0 -208
  101. package/src/app-server/transcript-repair-runtime-contract.test.ts +0 -44
  102. package/src/app-server/transport-stdio.test.ts +0 -171
  103. package/src/app-server/transport-stdio.ts +0 -107
  104. package/src/app-server/transport-websocket.test.ts +0 -69
  105. package/src/app-server/transport-websocket.ts +0 -90
  106. package/src/app-server/transport.ts +0 -117
  107. package/src/app-server/user-input-bridge.test.ts +0 -249
  108. package/src/app-server/user-input-bridge.ts +0 -316
  109. package/src/app-server/version.ts +0 -4
  110. package/src/app-server/vision-tools.ts +0 -12
  111. package/src/command-account.ts +0 -544
  112. package/src/command-formatters.ts +0 -425
  113. package/src/command-handlers.ts +0 -2004
  114. package/src/command-rpc.test.ts +0 -16
  115. package/src/command-rpc.ts +0 -142
  116. package/src/commands.test.ts +0 -3312
  117. package/src/commands.ts +0 -65
  118. package/src/conversation-binding-data.ts +0 -124
  119. package/src/conversation-binding.test.ts +0 -599
  120. package/src/conversation-binding.ts +0 -561
  121. package/src/conversation-control.test.ts +0 -126
  122. package/src/conversation-control.ts +0 -303
  123. package/src/conversation-turn-collector.test.ts +0 -191
  124. package/src/conversation-turn-collector.ts +0 -186
  125. package/src/conversation-turn-input.test.ts +0 -141
  126. package/src/conversation-turn-input.ts +0 -106
  127. package/src/manifest.test.ts +0 -20
  128. package/src/migration/apply.ts +0 -501
  129. package/src/migration/helpers.ts +0 -55
  130. package/src/migration/plan.ts +0 -461
  131. package/src/migration/provider.test.ts +0 -1741
  132. package/src/migration/provider.ts +0 -41
  133. package/src/migration/source.ts +0 -643
  134. package/src/migration/targets.ts +0 -25
  135. package/src/node-cli-sessions.test.ts +0 -180
  136. package/src/node-cli-sessions.ts +0 -711
  137. package/test-api.ts +0 -82
  138. package/tsconfig.json +0 -16
@@ -1,208 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import fs from "node:fs/promises";
3
- import {
4
- acquireSessionWriteLock,
5
- appendSessionTranscriptMessage,
6
- emitSessionTranscriptUpdate,
7
- resolveSessionWriteLockOptions,
8
- runAgentHarnessBeforeMessageWriteHook,
9
- type AgentMessage,
10
- type EmbeddedRunAttemptParams,
11
- type SessionWriteLockAcquireTimeoutConfig,
12
- } from "klaw/plugin-sdk/agent-harness-runtime";
13
-
14
- type MirroredAgentMessage = Extract<AgentMessage, { role: "user" | "assistant" | "toolResult" }>;
15
-
16
- const MIRROR_IDENTITY_META_KEY = "mirrorIdentity" as const;
17
-
18
- function normalizeOptionalString(value: string | null | undefined): string | undefined {
19
- const normalized = value?.trim();
20
- return normalized ? normalized : undefined;
21
- }
22
-
23
- function buildSenderLabel(params: {
24
- senderId?: string;
25
- senderName?: string;
26
- senderUsername?: string;
27
- senderE164?: string;
28
- }): string | undefined {
29
- const label = params.senderName ?? params.senderUsername ?? params.senderE164 ?? params.senderId;
30
- if (!label) {
31
- return undefined;
32
- }
33
- if (!params.senderId || label.includes(params.senderId)) {
34
- return label;
35
- }
36
- return `${label} (${params.senderId})`;
37
- }
38
-
39
- export function buildCodexUserPromptMessage(params: EmbeddedRunAttemptParams): AgentMessage {
40
- const senderId = normalizeOptionalString(params.senderId);
41
- const senderName = normalizeOptionalString(params.senderName);
42
- const senderUsername = normalizeOptionalString(params.senderUsername);
43
- const senderE164 = normalizeOptionalString(params.senderE164);
44
- const senderLabel = buildSenderLabel({ senderId, senderName, senderUsername, senderE164 });
45
- const sourceChannel = normalizeOptionalString(
46
- params.inputProvenance?.sourceChannel ?? params.messageChannel ?? params.messageProvider,
47
- );
48
- return {
49
- role: "user",
50
- content: params.prompt,
51
- timestamp: Date.now(),
52
- ...(params.inputProvenance ? { provenance: params.inputProvenance } : {}),
53
- ...(sourceChannel ? { sourceChannel } : {}),
54
- ...(senderId ? { senderId } : {}),
55
- ...(senderName ? { senderName } : {}),
56
- ...(senderUsername ? { senderUsername } : {}),
57
- ...(senderE164 ? { senderE164 } : {}),
58
- ...(senderLabel ? { senderLabel } : {}),
59
- } as AgentMessage;
60
- }
61
-
62
- /**
63
- * Tag a message with a stable logical identity for mirror dedupe. Callers
64
- * should use a value that is invariant for the same logical message across
65
- * re-emits (e.g. `${turnId}:prompt`, `${turnId}:assistant`) but distinct
66
- * for genuinely-distinct messages (different turns, different kinds). When
67
- * present this identity replaces the role/content fingerprint in the
68
- * idempotency key, so the dedupe survives caller-scope rotation without
69
- * collapsing distinct same-content turns.
70
- */
71
- export function attachCodexMirrorIdentity<T extends AgentMessage>(message: T, identity: string): T {
72
- const record = message as unknown as Record<string, unknown>;
73
- const existing = record["__klaw"];
74
- const baseMeta =
75
- existing && typeof existing === "object" && !Array.isArray(existing)
76
- ? (existing as Record<string, unknown>)
77
- : {};
78
- return {
79
- ...record,
80
- __klaw: { ...baseMeta, [MIRROR_IDENTITY_META_KEY]: identity },
81
- } as unknown as T;
82
- }
83
-
84
- function readMirrorIdentity(message: MirroredAgentMessage): string | undefined {
85
- const record = message as unknown as { __klaw?: unknown };
86
- const meta = record["__klaw"];
87
- if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
88
- return undefined;
89
- }
90
- const id = (meta as Record<string, unknown>)[MIRROR_IDENTITY_META_KEY];
91
- return typeof id === "string" && id.length > 0 ? id : undefined;
92
- }
93
-
94
- // Fallback content fingerprint for callers that did not tag the message
95
- // with a stable mirror identity. Only role and content participate; volatile
96
- // metadata (timestamps, usage, etc.) is intentionally excluded so the
97
- // fingerprint survives snapshot reordering inside a fixed scope. Distinct
98
- // same-content turns are still distinguished by the caller's idempotency
99
- // scope when callers route through this fallback.
100
- function fingerprintMirrorMessageContent(message: MirroredAgentMessage): string {
101
- const payload = JSON.stringify({ role: message.role, content: message.content });
102
- return createHash("sha256").update(payload).digest("hex").slice(0, 16);
103
- }
104
-
105
- function buildMirrorDedupeIdentity(message: MirroredAgentMessage): string {
106
- const explicit = readMirrorIdentity(message);
107
- if (explicit) {
108
- return explicit;
109
- }
110
- return `${message.role}:${fingerprintMirrorMessageContent(message)}`;
111
- }
112
-
113
- export async function mirrorCodexAppServerTranscript(params: {
114
- sessionFile: string;
115
- sessionKey?: string;
116
- agentId?: string;
117
- messages: AgentMessage[];
118
- idempotencyScope?: string;
119
- config?: SessionWriteLockAcquireTimeoutConfig;
120
- }): Promise<void> {
121
- const messages = params.messages.filter(
122
- (message): message is MirroredAgentMessage =>
123
- message.role === "user" || message.role === "assistant" || message.role === "toolResult",
124
- );
125
- if (messages.length === 0) {
126
- return;
127
- }
128
-
129
- const lock = await acquireSessionWriteLock({
130
- sessionFile: params.sessionFile,
131
- ...resolveSessionWriteLockOptions(params.config),
132
- });
133
- try {
134
- const existingIdempotencyKeys = await readTranscriptIdempotencyKeys(params.sessionFile);
135
- for (const message of messages) {
136
- const dedupeIdentity = buildMirrorDedupeIdentity(message);
137
- const idempotencyKey = params.idempotencyScope
138
- ? `${params.idempotencyScope}:${dedupeIdentity}`
139
- : undefined;
140
- if (idempotencyKey && existingIdempotencyKeys.has(idempotencyKey)) {
141
- continue;
142
- }
143
- const transcriptMessage = {
144
- ...message,
145
- ...(idempotencyKey ? { idempotencyKey } : {}),
146
- } as AgentMessage;
147
- const nextMessage = runAgentHarnessBeforeMessageWriteHook({
148
- message: transcriptMessage,
149
- agentId: params.agentId,
150
- sessionKey: params.sessionKey,
151
- });
152
- if (!nextMessage) {
153
- continue;
154
- }
155
- const messageToAppend = (
156
- idempotencyKey
157
- ? {
158
- ...(nextMessage as unknown as Record<string, unknown>),
159
- idempotencyKey,
160
- }
161
- : nextMessage
162
- ) as AgentMessage;
163
- await appendSessionTranscriptMessage({
164
- transcriptPath: params.sessionFile,
165
- message: messageToAppend,
166
- config: params.config,
167
- });
168
- if (idempotencyKey) {
169
- existingIdempotencyKeys.add(idempotencyKey);
170
- }
171
- }
172
- } finally {
173
- await lock.release();
174
- }
175
-
176
- if (params.sessionKey) {
177
- emitSessionTranscriptUpdate({ sessionFile: params.sessionFile, sessionKey: params.sessionKey });
178
- } else {
179
- emitSessionTranscriptUpdate(params.sessionFile);
180
- }
181
- }
182
-
183
- async function readTranscriptIdempotencyKeys(sessionFile: string): Promise<Set<string>> {
184
- const keys = new Set<string>();
185
- let raw: string;
186
- try {
187
- raw = await fs.readFile(sessionFile, "utf8");
188
- } catch (error) {
189
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
190
- throw error;
191
- }
192
- return keys;
193
- }
194
- for (const line of raw.split(/\r?\n/)) {
195
- if (!line.trim()) {
196
- continue;
197
- }
198
- try {
199
- const parsed = JSON.parse(line) as { message?: { idempotencyKey?: unknown } };
200
- if (typeof parsed.message?.idempotencyKey === "string") {
201
- keys.add(parsed.message.idempotencyKey);
202
- }
203
- } catch {
204
- continue;
205
- }
206
- }
207
- return keys;
208
- }
@@ -1,44 +0,0 @@
1
- import {
2
- assistantHistoryMessage,
3
- currentPromptHistoryMessage,
4
- mediaOnlyHistoryMessage,
5
- structuredHistoryMessage,
6
- } from "klaw/plugin-sdk/agent-runtime-test-contracts";
7
- import { describe, expect, it } from "vitest";
8
- import { projectContextEngineAssemblyForCodex } from "./context-engine-projection.js";
9
-
10
- describe("Codex transcript projection runtime contract", () => {
11
- it("drops only the duplicate trailing current prompt while preserving prior structured context", () => {
12
- const prompt = "newest inbound message";
13
-
14
- const result = projectContextEngineAssemblyForCodex({
15
- prompt,
16
- originalHistoryMessages: [structuredHistoryMessage()],
17
- assembledMessages: [
18
- structuredHistoryMessage(),
19
- assistantHistoryMessage(),
20
- currentPromptHistoryMessage(prompt),
21
- ],
22
- });
23
-
24
- expect(result.promptText).toContain("Current user request:\nnewest inbound message");
25
- expect(result.promptText).toContain("[user]\nolder structured context\n[image omitted]");
26
- expect(result.promptText).toContain("[assistant]\nack");
27
- expect(result.promptText).not.toContain("[user]\nnewest inbound message");
28
- });
29
-
30
- it("keeps media-only user history visible as omitted media instead of dropping the turn", () => {
31
- const result = projectContextEngineAssemblyForCodex({
32
- prompt: "newest inbound message",
33
- originalHistoryMessages: [mediaOnlyHistoryMessage()],
34
- assembledMessages: [
35
- mediaOnlyHistoryMessage(),
36
- currentPromptHistoryMessage("newest inbound message"),
37
- ],
38
- });
39
-
40
- expect(result.promptText).toContain("[user]\n[image omitted]");
41
- expect(result.promptText).not.toContain("data:image/png");
42
- expect(result.promptText).not.toContain("bbbb");
43
- });
44
- });
@@ -1,171 +0,0 @@
1
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { afterEach, describe, expect, it } from "vitest";
5
- import type { CodexAppServerStartOptions } from "./config.js";
6
- import {
7
- resolveCodexAppServerSpawnEnv,
8
- resolveCodexAppServerSpawnInvocation,
9
- } from "./transport-stdio.js";
10
-
11
- const tempDirs: string[] = [];
12
-
13
- async function createTempDir(): Promise<string> {
14
- const dir = await mkdtemp(path.join(os.tmpdir(), "klaw-codex-spawn-"));
15
- tempDirs.push(dir);
16
- return dir;
17
- }
18
-
19
- afterEach(async () => {
20
- for (const dir of tempDirs.splice(0)) {
21
- await rm(dir, { recursive: true, force: true });
22
- }
23
- });
24
-
25
- function startOptions(command: string): CodexAppServerStartOptions {
26
- return {
27
- transport: "stdio",
28
- command,
29
- args: ["app-server", "--listen", "stdio://"],
30
- headers: {},
31
- };
32
- }
33
-
34
- describe("resolveCodexAppServerSpawnInvocation", () => {
35
- it("keeps non-Windows Codex app-server invocation unchanged", () => {
36
- const resolved = resolveCodexAppServerSpawnInvocation(startOptions("codex"), {
37
- platform: "darwin",
38
- env: {},
39
- execPath: "/usr/local/bin/node",
40
- });
41
-
42
- expect(resolved).toEqual({
43
- command: "codex",
44
- args: ["app-server", "--listen", "stdio://"],
45
- shell: undefined,
46
- windowsHide: undefined,
47
- });
48
- });
49
-
50
- it("requires managed Codex commands to be resolved before spawn", () => {
51
- expect(() =>
52
- resolveCodexAppServerSpawnInvocation(
53
- {
54
- ...startOptions("codex"),
55
- commandSource: "managed",
56
- },
57
- {
58
- platform: "darwin",
59
- env: {},
60
- execPath: "/usr/local/bin/node",
61
- },
62
- ),
63
- ).toThrow("must be resolved before spawn");
64
- });
65
-
66
- it("resolves Windows npm .cmd Codex shims through Node instead of raw spawn", async () => {
67
- const binDir = await createTempDir();
68
- const entryPath = path.join(binDir, "node_modules", "@openai", "codex", "bin", "codex.js");
69
- const shimPath = path.join(binDir, "codex.cmd");
70
- await mkdir(path.dirname(entryPath), { recursive: true });
71
- await writeFile(entryPath, "console.log('codex')\n", "utf8");
72
- await writeFile(
73
- shimPath,
74
- '@ECHO off\r\n"%~dp0\\node_modules\\@openai\\codex\\bin\\codex.js" %*\r\n',
75
- "utf8",
76
- );
77
-
78
- const resolved = resolveCodexAppServerSpawnInvocation(startOptions("codex"), {
79
- platform: "win32",
80
- env: { PATH: binDir, PATHEXT: ".CMD;.EXE;.BAT" },
81
- execPath: "C:\\node\\node.exe",
82
- });
83
-
84
- expect(resolved).toEqual({
85
- command: "C:\\node\\node.exe",
86
- args: [entryPath, "app-server", "--listen", "stdio://"],
87
- shell: undefined,
88
- windowsHide: true,
89
- });
90
- });
91
- });
92
-
93
- describe("resolveCodexAppServerSpawnEnv", () => {
94
- it("applies configured env overrides before clearing denied env vars", () => {
95
- expect({
96
- ...resolveCodexAppServerSpawnEnv(
97
- {
98
- env: {
99
- OPENAI_API_KEY: "configured-openai-key",
100
- KEEP: "override",
101
- },
102
- clearEnv: ["OPENAI_API_KEY", "CODEX_API_KEY", "MISSING"],
103
- },
104
- {
105
- OPENAI_API_KEY: "parent-openai-key",
106
- CODEX_API_KEY: "parent-codex-key",
107
- KEEP: "parent",
108
- },
109
- ),
110
- }).toEqual({
111
- KEEP: "override",
112
- });
113
- });
114
-
115
- it("clears denied env vars case-insensitively on Windows", () => {
116
- expect({
117
- ...resolveCodexAppServerSpawnEnv(
118
- {
119
- env: {
120
- OpenAI_Api_Key: "configured-openai-key",
121
- Other: "configured",
122
- },
123
- clearEnv: ["OPENAI_API_KEY", " CODEX_API_KEY ", ""],
124
- },
125
- {
126
- Codex_Api_Key: "parent-codex-key",
127
- KEEP: "parent",
128
- },
129
- "win32",
130
- ),
131
- }).toEqual({
132
- KEEP: "parent",
133
- Other: "configured",
134
- });
135
- });
136
-
137
- it("uses a null-prototype env map and ignores prototype-polluting keys", () => {
138
- const overrides = Object.create(null) as Record<string, string | undefined>;
139
- Object.defineProperty(overrides, "__proto__", {
140
- value: "polluted",
141
- enumerable: true,
142
- });
143
- Object.defineProperty(overrides, "constructor", {
144
- value: "polluted",
145
- enumerable: true,
146
- });
147
- Object.defineProperty(overrides, "prototype", {
148
- value: "polluted",
149
- enumerable: true,
150
- });
151
- overrides.SAFE = "1";
152
-
153
- const env = resolveCodexAppServerSpawnEnv(
154
- {
155
- env: overrides as Record<string, string>,
156
- },
157
- {
158
- BASE: "1",
159
- },
160
- );
161
-
162
- expect(Object.getPrototypeOf(env)).toBeNull();
163
- expect({ ...env }).toEqual({
164
- BASE: "1",
165
- SAFE: "1",
166
- });
167
- expect(Object.hasOwn(env, "__proto__")).toBe(false);
168
- expect(Object.hasOwn(env, "constructor")).toBe(false);
169
- expect(Object.hasOwn(env, "prototype")).toBe(false);
170
- });
171
- });
@@ -1,107 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import {
3
- materializeWindowsSpawnProgram,
4
- resolveWindowsSpawnProgram,
5
- } from "klaw/plugin-sdk/windows-spawn";
6
- import type { CodexAppServerStartOptions } from "./config.js";
7
- import type { CodexAppServerTransport } from "./transport.js";
8
-
9
- const UNSAFE_ENVIRONMENT_KEYS = new Set(["__proto__", "constructor", "prototype"]);
10
-
11
- type CodexAppServerSpawnRuntime = {
12
- platform: NodeJS.Platform;
13
- env: NodeJS.ProcessEnv;
14
- execPath: string;
15
- };
16
-
17
- const DEFAULT_SPAWN_RUNTIME: CodexAppServerSpawnRuntime = {
18
- platform: process.platform,
19
- env: process.env,
20
- execPath: process.execPath,
21
- };
22
-
23
- export function resolveCodexAppServerSpawnInvocation(
24
- options: CodexAppServerStartOptions,
25
- runtime: CodexAppServerSpawnRuntime = DEFAULT_SPAWN_RUNTIME,
26
- ): { command: string; args: string[]; shell?: boolean; windowsHide?: boolean } {
27
- if (options.commandSource === "managed") {
28
- throw new Error("Managed Codex app-server start options must be resolved before spawn.");
29
- }
30
- const program = resolveWindowsSpawnProgram({
31
- command: options.command,
32
- platform: runtime.platform,
33
- env: runtime.env,
34
- execPath: runtime.execPath,
35
- packageName: "@openai/codex",
36
- });
37
- const resolved = materializeWindowsSpawnProgram(program, options.args);
38
- return {
39
- command: resolved.command,
40
- args: resolved.argv,
41
- shell: resolved.shell,
42
- windowsHide: resolved.windowsHide,
43
- };
44
- }
45
-
46
- export function resolveCodexAppServerSpawnEnv(
47
- options: Pick<CodexAppServerStartOptions, "env" | "clearEnv">,
48
- baseEnv: NodeJS.ProcessEnv = process.env,
49
- platform: NodeJS.Platform = process.platform,
50
- ): NodeJS.ProcessEnv {
51
- const env = Object.create(null) as NodeJS.ProcessEnv;
52
- copySafeEnvironmentEntries(env, baseEnv);
53
- copySafeEnvironmentEntries(env, options.env ?? {});
54
- const keysToClear = normalizedEnvironmentKeys(options.clearEnv ?? []);
55
- if (platform === "win32") {
56
- const lowerCaseKeysToClear = new Set(keysToClear.map((key) => key.toLowerCase()));
57
- for (const candidate of Object.keys(env)) {
58
- if (lowerCaseKeysToClear.has(candidate.toLowerCase())) {
59
- delete env[candidate];
60
- }
61
- }
62
- } else {
63
- for (const key of keysToClear) {
64
- delete env[key];
65
- }
66
- }
67
- return env;
68
- }
69
-
70
- function normalizedEnvironmentKeys(rawKeys: readonly string[]): string[] {
71
- const keys: string[] = [];
72
- for (const rawKey of rawKeys) {
73
- const key = rawKey.trim();
74
- if (key.length > 0) {
75
- keys.push(key);
76
- }
77
- }
78
- return keys;
79
- }
80
-
81
- function copySafeEnvironmentEntries(
82
- target: NodeJS.ProcessEnv,
83
- source: NodeJS.ProcessEnv | Record<string, string | undefined>,
84
- ): void {
85
- for (const [key, value] of Object.entries(source)) {
86
- if (UNSAFE_ENVIRONMENT_KEYS.has(key)) {
87
- continue;
88
- }
89
- target[key] = value;
90
- }
91
- }
92
-
93
- export function createStdioTransport(options: CodexAppServerStartOptions): CodexAppServerTransport {
94
- const env = resolveCodexAppServerSpawnEnv(options);
95
- const invocation = resolveCodexAppServerSpawnInvocation(options, {
96
- platform: process.platform,
97
- env,
98
- execPath: process.execPath,
99
- });
100
- return spawn(invocation.command, invocation.args, {
101
- env,
102
- detached: process.platform !== "win32",
103
- shell: invocation.shell,
104
- stdio: ["pipe", "pipe", "pipe"],
105
- windowsHide: invocation.windowsHide,
106
- });
107
- }
@@ -1,69 +0,0 @@
1
- import { afterEach, describe, expect, it } from "vitest";
2
- import { WebSocketServer, type RawData } from "ws";
3
- import { CodexAppServerClient } from "./client.js";
4
-
5
- describe("Codex app-server websocket transport", () => {
6
- const clients: CodexAppServerClient[] = [];
7
- const servers: WebSocketServer[] = [];
8
-
9
- afterEach(async () => {
10
- for (const client of clients) {
11
- client.close();
12
- }
13
- clients.length = 0;
14
- await Promise.all(
15
- servers
16
- .splice(0)
17
- .map(
18
- (server) =>
19
- new Promise<void>((resolve, reject) =>
20
- server.close((error) => (error ? reject(error) : resolve())),
21
- ),
22
- ),
23
- );
24
- });
25
-
26
- it("can speak JSON-RPC over websocket transport", async () => {
27
- const server = new WebSocketServer({ host: "127.0.0.1", port: 0 });
28
- servers.push(server);
29
- const authHeaders: Array<string | undefined> = [];
30
- server.on("connection", (socket, request) => {
31
- authHeaders.push(request.headers.authorization);
32
- socket.on("message", (data) => {
33
- const message = JSON.parse(rawDataToText(data)) as { id?: number; method?: string };
34
- if (message.method === "initialize") {
35
- socket.send(JSON.stringify({ id: message.id, result: { userAgent: "klaw/0.125.0" } }));
36
- return;
37
- }
38
- if (message.method === "model/list") {
39
- socket.send(JSON.stringify({ id: message.id, result: { data: [] } }));
40
- }
41
- });
42
- });
43
- await new Promise<void>((resolve) => server.once("listening", resolve));
44
- const address = server.address();
45
- if (!address || typeof address === "string") {
46
- throw new Error("expected websocket test server port");
47
- }
48
- const client = CodexAppServerClient.start({
49
- transport: "websocket",
50
- url: `ws://127.0.0.1:${address.port}`,
51
- authToken: "secret",
52
- });
53
- clients.push(client);
54
-
55
- await expect(client.initialize()).resolves.toBeUndefined();
56
- await expect(client.request("model/list", {})).resolves.toEqual({ data: [] });
57
- expect(authHeaders).toEqual(["Bearer secret"]);
58
- });
59
- });
60
-
61
- function rawDataToText(data: RawData): string {
62
- if (Array.isArray(data)) {
63
- return Buffer.concat(data).toString("utf8");
64
- }
65
- if (data instanceof ArrayBuffer) {
66
- return Buffer.from(new Uint8Array(data)).toString("utf8");
67
- }
68
- return Buffer.from(data).toString("utf8");
69
- }
@@ -1,90 +0,0 @@
1
- import { EventEmitter } from "node:events";
2
- import { PassThrough, Writable } from "node:stream";
3
- import WebSocket, { type RawData } from "ws";
4
- import type { CodexAppServerStartOptions } from "./config.js";
5
- import type { CodexAppServerTransport } from "./transport.js";
6
-
7
- export function createWebSocketTransport(
8
- options: CodexAppServerStartOptions,
9
- ): CodexAppServerTransport {
10
- if (!options.url) {
11
- throw new Error(
12
- "codex app-server websocket transport requires plugins.entries.codex.config.appServer.url",
13
- );
14
- }
15
- const events = new EventEmitter();
16
- const stdout = new PassThrough();
17
- const stderr = new PassThrough();
18
- const headers = {
19
- ...options.headers,
20
- ...(options.authToken ? { Authorization: `Bearer ${options.authToken}` } : {}),
21
- };
22
- const socket = new WebSocket(options.url, { headers });
23
- const pendingFrames: string[] = [];
24
- let killed = false;
25
-
26
- const sendFrame = (frame: string) => {
27
- const trimmed = frame.trim();
28
- if (!trimmed) {
29
- return;
30
- }
31
- if (socket.readyState === WebSocket.OPEN) {
32
- socket.send(trimmed);
33
- return;
34
- }
35
- pendingFrames.push(trimmed);
36
- };
37
-
38
- // `initialize` can be written before the WebSocket open event fires. Buffer
39
- // whole JSON-RPC frames so stdio and websocket transports share call timing.
40
- socket.once("open", () => {
41
- for (const frame of pendingFrames.splice(0)) {
42
- socket.send(frame);
43
- }
44
- });
45
- socket.once("error", (error) => events.emit("error", error));
46
- socket.once("close", (code, reason) => {
47
- killed = true;
48
- events.emit("exit", code, reason.toString("utf8"));
49
- });
50
- socket.on("message", (data) => {
51
- const text = websocketFrameToText(data);
52
- stdout.write(text.endsWith("\n") ? text : `${text}\n`);
53
- });
54
-
55
- const stdin = new Writable({
56
- write(chunk, _encoding, callback) {
57
- for (const frame of chunk.toString("utf8").split("\n")) {
58
- sendFrame(frame);
59
- }
60
- callback();
61
- },
62
- });
63
-
64
- return {
65
- stdin,
66
- stdout,
67
- stderr,
68
- get killed() {
69
- return killed;
70
- },
71
- kill: () => {
72
- killed = true;
73
- socket.close();
74
- },
75
- once: (event, listener) => events.once(event, listener),
76
- };
77
- }
78
-
79
- function websocketFrameToText(data: RawData): string {
80
- if (typeof data === "string") {
81
- return data;
82
- }
83
- if (Buffer.isBuffer(data)) {
84
- return data.toString("utf8");
85
- }
86
- if (Array.isArray(data)) {
87
- return Buffer.concat(data).toString("utf8");
88
- }
89
- return Buffer.from(data).toString("utf8");
90
- }