@ai-sdk/harness-codex 1.0.28 → 1.0.29

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.
@@ -19,24 +19,19 @@ import type { HarnessV1BuiltinToolName } from '@ai-sdk/harness';
19
19
  import type { StartMessage } from '../codex-bridge-protocol';
20
20
  import { randomUUID } from 'node:crypto';
21
21
  import { mkdir, writeFile } from 'node:fs/promises';
22
- import { createServer, type Server } from 'node:http';
23
22
  // Temporary workaround for upstream codex MCP-tool bug — see ./cli-relay.ts
24
23
  import {
25
24
  CLI_SHIM_FILENAME,
26
25
  buildCliShimScript,
27
- parseToolRelayCommand,
26
+ parseToolRelayCommands,
28
27
  } from './cli-relay';
29
28
  import {
30
29
  createCodexStepTracker,
31
30
  defaultUsage,
32
31
  type CodexStepTracker,
33
32
  } from './codex-step-tracker';
34
- import {
35
- ToolRelayAuthorizer,
36
- ToolRelayPendingCalls,
37
- isToolRelayRequestFromAllowedProcess,
38
- type ToolRelayCall,
39
- } from './tool-relay-auth';
33
+ import { startAuthorizedToolRelay, type ToolRelay } from './tool-relay';
34
+ import type { ToolRelayCall } from './tool-relay-auth';
40
35
  import { argv, env as procEnv, stdout } from 'node:process';
41
36
 
42
37
  /*
@@ -100,12 +95,6 @@ await runBridge<StartMessage>({
100
95
  });
101
96
 
102
97
  type Emit = (msg: Record<string, unknown>) => void;
103
- type ToolRelay = {
104
- port: number;
105
- close(): void;
106
- authorizeToolCall(call: ToolRelayCall): void;
107
- authorizeAnyToolCall(): void;
108
- };
109
98
 
110
99
  async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
111
100
  const emit: Emit = msg => turn.emit(msg as BridgeEvent);
@@ -142,7 +131,6 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
142
131
  if (start.tools && start.tools.length > 0) {
143
132
  cliShimPath = `${cliShimDir}/${CLI_SHIM_FILENAME}`;
144
133
  relay = await startToolRelay({
145
- allowedScriptPaths: [cliShimPath, `${bootstrapDir}/host-tool-mcp.mjs`],
146
134
  tools: start.tools,
147
135
  emit,
148
136
  requestToolResult: turn.requestToolResult,
@@ -258,21 +246,19 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
258
246
  }
259
247
  // Temporary workaround for upstream codex MCP-tool bug — see ./cli-relay.ts
260
248
  if (cliShimPath && event.item?.type === 'command_execution') {
261
- const relayCall =
249
+ const relayCalls =
262
250
  typeof event.item.command === 'string'
263
- ? parseToolRelayCommand({
251
+ ? parseToolRelayCommands({
264
252
  command: event.item.command,
265
253
  cliShimPath,
266
254
  })
267
255
  : undefined;
268
- if (event.type === 'item.started' && relay) {
269
- if (relayCall) {
256
+ if (event.type === 'item.started' && relay && relayCalls) {
257
+ for (const relayCall of relayCalls) {
270
258
  relay.authorizeToolCall(relayCall);
271
- } else if (typeof event.item.command !== 'string') {
272
- relay.authorizeAnyToolCall();
273
259
  }
274
260
  }
275
- if (relayCall) {
261
+ if (relayCalls) {
276
262
  stepTracker.observeEvent({ event, itemId: event.item.id });
277
263
  continue;
278
264
  }
@@ -589,116 +575,17 @@ function mapUsage(usage: Record<string, number>): Record<string, unknown> {
589
575
  * (via `requestToolResult`), and responds with `{ result }`.
590
576
  */
591
577
  async function startToolRelay({
592
- allowedScriptPaths,
593
578
  tools,
594
579
  emit,
595
580
  requestToolResult,
596
581
  }: {
597
- allowedScriptPaths: ReadonlyArray<string>;
598
582
  tools: ReadonlyArray<{ name: string }>;
599
583
  emit: Emit;
600
584
  requestToolResult: (
601
585
  toolCallId: string,
602
586
  ) => Promise<{ output: unknown; isError?: boolean }>;
603
587
  }): Promise<ToolRelay> {
604
- const toolNames = new Set(tools.map(t => t.name));
605
- const allowedScriptPathSet = new Set(allowedScriptPaths);
606
- const authorizer = new ToolRelayAuthorizer();
607
- const pendingCalls = new ToolRelayPendingCalls();
608
-
609
- const server = createServer(async (req, res) => {
610
- try {
611
- if (req.method !== 'POST' || req.url !== '/') {
612
- res.writeHead(401, { 'Content-Type': 'application/json' });
613
- res.end(JSON.stringify({ error: 'unauthorized tool relay request' }));
614
- return;
615
- }
616
- const chunks: Buffer[] = [];
617
- for await (const chunk of req) {
618
- chunks.push(chunk as Buffer);
619
- }
620
- const body = Buffer.concat(chunks).toString('utf8');
621
- const { requestId, toolName, input } = JSON.parse(body) as {
622
- requestId: string;
623
- toolName: string;
624
- input: unknown;
625
- };
626
-
627
- if (!toolNames.has(toolName)) {
628
- res.writeHead(403, { 'Content-Type': 'application/json' });
629
- res.end(
630
- JSON.stringify({ error: `Tool "${toolName}" is not available` }),
631
- );
632
- return;
633
- }
634
- const relayCall = { toolName, input };
635
- const authorized =
636
- authorizer.consumeToolCall(relayCall) ||
637
- (await isToolRelayRequestFromAllowedProcess({
638
- socket: req.socket,
639
- allowedScriptPaths: allowedScriptPathSet,
640
- }));
641
- if (!authorized) {
642
- res.writeHead(401, { 'Content-Type': 'application/json' });
643
- res.end(JSON.stringify({ error: 'unauthorized tool relay request' }));
644
- return;
645
- }
646
-
647
- const { result } = pendingCalls.begin({
648
- call: relayCall,
649
- run: async () => {
650
- emit({
651
- type: 'tool-call',
652
- toolCallId: requestId,
653
- toolName,
654
- input: JSON.stringify(input ?? {}),
655
- providerExecuted: false,
656
- });
657
-
658
- const toolResult = await requestToolResult(requestId);
659
- emit({
660
- type: 'tool-result',
661
- toolCallId: requestId,
662
- toolName,
663
- result: toolResult.output ?? null,
664
- isError: !!toolResult.isError,
665
- });
666
- return toolResult;
667
- },
668
- });
669
- const { output } = await result;
670
-
671
- res.writeHead(200, { 'Content-Type': 'application/json' });
672
- res.end(JSON.stringify({ result: output }));
673
- } catch (error) {
674
- res.writeHead(500, { 'Content-Type': 'application/json' });
675
- res.end(
676
- JSON.stringify({
677
- error: error instanceof Error ? error.message : String(error),
678
- }),
679
- );
680
- }
681
- });
682
-
683
- await new Promise<void>(resolve =>
684
- server.listen(0, '127.0.0.1', () => resolve()),
685
- );
686
- const address = server.address();
687
- if (!address || typeof address === 'string') {
688
- throw new Error('tool relay did not expose a numeric port');
689
- }
690
- return {
691
- port: address.port,
692
- close: () => closeServer(server),
693
- authorizeToolCall: call => authorizer.authorizeToolCall(call),
694
- authorizeAnyToolCall: () => authorizer.authorizeAnyToolCall(),
695
- };
696
- }
697
-
698
- function closeServer(server: Server): void {
699
- try {
700
- server.close();
701
- } catch {}
588
+ return startAuthorizedToolRelay({ tools, emit, requestToolResult });
702
589
  }
703
590
 
704
591
  function parseArgs(args: string[]): {
@@ -1,6 +1,3 @@
1
- import { readdir, readFile, readlink } from 'node:fs/promises';
2
- import type { Socket } from 'node:net';
3
-
4
1
  export type ToolRelayCall = {
5
2
  toolName: string;
6
3
  input: unknown;
@@ -14,9 +11,13 @@ export type ToolRelayResult = {
14
11
  export class ToolRelayAuthorizer {
15
12
  private readonly ttlMs: number;
16
13
  private readonly now: () => number;
17
- private readonly authorizations: Array<{
18
- key?: string;
14
+ private readonly authorizations: Array<{ key: string; expiresAt: number }> =
15
+ [];
16
+ private readonly pendingRequests: Array<{
17
+ key: string;
19
18
  expiresAt: number;
19
+ timeout: ReturnType<typeof setTimeout>;
20
+ resolve: (authorized: boolean) => void;
20
21
  }> = [];
21
22
 
22
23
  constructor({
@@ -32,29 +33,58 @@ export class ToolRelayAuthorizer {
32
33
 
33
34
  authorizeToolCall(call: ToolRelayCall): void {
34
35
  this.pruneExpired();
36
+ const key = toolRelayCallKey(call);
37
+ const pendingRequestIndex = this.pendingRequests.findIndex(
38
+ request => request.key === key,
39
+ );
40
+ if (pendingRequestIndex !== -1) {
41
+ const [pendingRequest] = this.pendingRequests.splice(
42
+ pendingRequestIndex,
43
+ 1,
44
+ );
45
+ clearTimeout(pendingRequest.timeout);
46
+ pendingRequest.resolve(true);
47
+ return;
48
+ }
35
49
  this.authorizations.push({
36
- key: toolRelayCallKey(call),
50
+ key,
37
51
  expiresAt: this.now() + this.ttlMs,
38
52
  });
39
53
  }
40
54
 
41
- authorizeAnyToolCall(): void {
55
+ waitForToolCallAuthorization(call: ToolRelayCall): Promise<boolean> {
42
56
  this.pruneExpired();
43
- this.authorizations.push({
44
- expiresAt: this.now() + this.ttlMs,
57
+ const key = toolRelayCallKey(call);
58
+ const authorizationIndex = this.authorizations.findIndex(
59
+ authorization => authorization.key === key,
60
+ );
61
+ if (authorizationIndex !== -1) {
62
+ this.authorizations.splice(authorizationIndex, 1);
63
+ return Promise.resolve(true);
64
+ }
65
+
66
+ const expiresAt = this.now() + this.ttlMs;
67
+ return new Promise(resolve => {
68
+ const pendingRequest = {
69
+ key,
70
+ expiresAt,
71
+ timeout: setTimeout(() => {
72
+ const index = this.pendingRequests.indexOf(pendingRequest);
73
+ if (index !== -1) this.pendingRequests.splice(index, 1);
74
+ resolve(false);
75
+ }, this.ttlMs),
76
+ resolve,
77
+ };
78
+ this.pendingRequests.push(pendingRequest);
45
79
  });
46
80
  }
47
81
 
48
- consumeToolCall(call: ToolRelayCall): boolean {
49
- this.pruneExpired();
50
- const key = toolRelayCallKey(call);
51
- let index = this.authorizations.findIndex(auth => auth.key === key);
52
- if (index === -1) {
53
- index = this.authorizations.findIndex(auth => auth.key === undefined);
82
+ close(): void {
83
+ this.authorizations.length = 0;
84
+ for (const pendingRequest of this.pendingRequests.splice(0)) {
85
+ clearTimeout(pendingRequest.timeout);
86
+ pendingRequest.resolve(false);
54
87
  }
55
- if (index === -1) return false;
56
- this.authorizations.splice(index, 1);
57
- return true;
58
88
  }
59
89
 
60
90
  private pruneExpired(): void {
@@ -64,6 +94,14 @@ export class ToolRelayAuthorizer {
64
94
  this.authorizations.splice(i, 1);
65
95
  }
66
96
  }
97
+ for (let i = this.pendingRequests.length - 1; i >= 0; i--) {
98
+ const pendingRequest = this.pendingRequests[i];
99
+ if (pendingRequest.expiresAt <= now) {
100
+ this.pendingRequests.splice(i, 1);
101
+ clearTimeout(pendingRequest.timeout);
102
+ pendingRequest.resolve(false);
103
+ }
104
+ }
67
105
  }
68
106
  }
69
107
 
@@ -116,80 +154,3 @@ function normalizeJsonValue(value: unknown): unknown {
116
154
  }
117
155
  return value;
118
156
  }
119
-
120
- export async function isToolRelayRequestFromAllowedProcess({
121
- socket,
122
- allowedScriptPaths,
123
- }: {
124
- socket: Socket;
125
- allowedScriptPaths: ReadonlySet<string>;
126
- }): Promise<boolean> {
127
- if (process.platform !== 'linux') return false;
128
- if (!socket.remotePort || !socket.localPort) return false;
129
-
130
- const inode = await findTcpSocketInode({
131
- clientPort: socket.remotePort,
132
- serverPort: socket.localPort,
133
- });
134
- if (!inode) return false;
135
-
136
- const cmdline = await findProcessCmdlineForSocketInode({ inode });
137
- return cmdline?.some(arg => allowedScriptPaths.has(arg)) ?? false;
138
- }
139
-
140
- async function findTcpSocketInode({
141
- clientPort,
142
- serverPort,
143
- }: {
144
- clientPort: number;
145
- serverPort: number;
146
- }): Promise<string | undefined> {
147
- for (const tablePath of ['/proc/net/tcp', '/proc/net/tcp6']) {
148
- const table = await readFile(tablePath, 'utf8').catch(() => undefined);
149
- if (!table) continue;
150
- for (const line of table.split('\n').slice(1)) {
151
- const columns = line.trim().split(/\s+/);
152
- if (columns.length < 10) continue;
153
- const local = parseProcNetAddress(columns[1]);
154
- const remote = parseProcNetAddress(columns[2]);
155
- if (
156
- local?.port === clientPort &&
157
- remote?.port === serverPort &&
158
- columns[9] !== '0'
159
- ) {
160
- return columns[9];
161
- }
162
- }
163
- }
164
- return undefined;
165
- }
166
-
167
- function parseProcNetAddress(value: string): { port: number } | undefined {
168
- const [, portHex] = value.split(':');
169
- if (!portHex) return undefined;
170
- return { port: Number.parseInt(portHex, 16) };
171
- }
172
-
173
- async function findProcessCmdlineForSocketInode({
174
- inode,
175
- }: {
176
- inode: string;
177
- }): Promise<string[] | undefined> {
178
- const procEntries = await readdir('/proc', { withFileTypes: true }).catch(
179
- () => [],
180
- );
181
- for (const entry of procEntries) {
182
- if (!entry.isDirectory() || !/^\d+$/.test(entry.name)) continue;
183
- const fdDir = `/proc/${entry.name}/fd`;
184
- const fds = await readdir(fdDir).catch(() => []);
185
- for (const fd of fds) {
186
- const target = await readlink(`${fdDir}/${fd}`).catch(() => undefined);
187
- if (target !== `socket:[${inode}]`) continue;
188
- const cmdline = await readFile(`/proc/${entry.name}/cmdline`, 'utf8')
189
- .then(value => value.split('\0').filter(Boolean))
190
- .catch(() => undefined);
191
- if (cmdline) return cmdline;
192
- }
193
- }
194
- return undefined;
195
- }
@@ -0,0 +1,119 @@
1
+ import { createServer, type Server } from 'node:http';
2
+ import {
3
+ ToolRelayAuthorizer,
4
+ ToolRelayPendingCalls,
5
+ type ToolRelayCall,
6
+ } from './tool-relay-auth';
7
+
8
+ export type ToolRelay = {
9
+ port: number;
10
+ close(): void;
11
+ authorizeToolCall(call: ToolRelayCall): void;
12
+ };
13
+
14
+ export async function startAuthorizedToolRelay({
15
+ tools,
16
+ emit,
17
+ requestToolResult,
18
+ authorizer = new ToolRelayAuthorizer(),
19
+ }: {
20
+ tools: ReadonlyArray<{ name: string }>;
21
+ emit: (message: Record<string, unknown>) => void;
22
+ requestToolResult: (
23
+ toolCallId: string,
24
+ ) => Promise<{ output: unknown; isError?: boolean }>;
25
+ authorizer?: ToolRelayAuthorizer;
26
+ }): Promise<ToolRelay> {
27
+ const toolNames = new Set(tools.map(tool => tool.name));
28
+ const pendingCalls = new ToolRelayPendingCalls();
29
+
30
+ const server = createServer(async (req, res) => {
31
+ try {
32
+ if (req.method !== 'POST' || req.url !== '/') {
33
+ res.writeHead(401, { 'Content-Type': 'application/json' });
34
+ res.end(JSON.stringify({ error: 'unauthorized tool relay request' }));
35
+ return;
36
+ }
37
+ const chunks: Buffer[] = [];
38
+ for await (const chunk of req) {
39
+ chunks.push(chunk as Buffer);
40
+ }
41
+ const body = Buffer.concat(chunks).toString('utf8');
42
+ const { requestId, toolName, input } = JSON.parse(body) as {
43
+ requestId: string;
44
+ toolName: string;
45
+ input: unknown;
46
+ };
47
+
48
+ if (!toolNames.has(toolName)) {
49
+ res.writeHead(403, { 'Content-Type': 'application/json' });
50
+ res.end(
51
+ JSON.stringify({ error: `Tool "${toolName}" is not available` }),
52
+ );
53
+ return;
54
+ }
55
+ const relayCall = { toolName, input };
56
+ if (!(await authorizer.waitForToolCallAuthorization(relayCall))) {
57
+ res.writeHead(401, { 'Content-Type': 'application/json' });
58
+ res.end(JSON.stringify({ error: 'unauthorized tool relay request' }));
59
+ return;
60
+ }
61
+
62
+ const { result } = pendingCalls.begin({
63
+ call: relayCall,
64
+ run: async () => {
65
+ emit({
66
+ type: 'tool-call',
67
+ toolCallId: requestId,
68
+ toolName,
69
+ input: JSON.stringify(input ?? {}),
70
+ providerExecuted: false,
71
+ });
72
+
73
+ const toolResult = await requestToolResult(requestId);
74
+ emit({
75
+ type: 'tool-result',
76
+ toolCallId: requestId,
77
+ toolName,
78
+ result: toolResult.output ?? null,
79
+ isError: !!toolResult.isError,
80
+ });
81
+ return toolResult;
82
+ },
83
+ });
84
+ const { output } = await result;
85
+
86
+ res.writeHead(200, { 'Content-Type': 'application/json' });
87
+ res.end(JSON.stringify({ result: output }));
88
+ } catch (error) {
89
+ res.writeHead(500, { 'Content-Type': 'application/json' });
90
+ res.end(
91
+ JSON.stringify({
92
+ error: error instanceof Error ? error.message : String(error),
93
+ }),
94
+ );
95
+ }
96
+ });
97
+
98
+ await new Promise<void>(resolve =>
99
+ server.listen(0, '127.0.0.1', () => resolve()),
100
+ );
101
+ const address = server.address();
102
+ if (!address || typeof address === 'string') {
103
+ throw new Error('tool relay did not expose a numeric port');
104
+ }
105
+ return {
106
+ port: address.port,
107
+ close: () => {
108
+ authorizer.close();
109
+ closeServer(server);
110
+ },
111
+ authorizeToolCall: call => authorizer.authorizeToolCall(call),
112
+ };
113
+ }
114
+
115
+ function closeServer(server: Server): void {
116
+ try {
117
+ server.close();
118
+ } catch {}
119
+ }