@ai-sdk/harness-codex 1.0.27 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @ai-sdk/harness-codex
2
2
 
3
+ ## 1.0.29
4
+
5
+ ### Patch Changes
6
+
7
+ - c93803d: fix(harness): improve CLI relay tool invocation logic
8
+
9
+ ## 1.0.28
10
+
11
+ ### Patch Changes
12
+
13
+ - @ai-sdk/harness@1.0.27
14
+
3
15
  ## 1.0.27
4
16
 
5
17
  ### Patch Changes
@@ -480,7 +480,6 @@ function serialiseError(err) {
480
480
  // src/bridge/index.ts
481
481
  import { randomUUID } from "crypto";
482
482
  import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
483
- import { createServer } from "http";
484
483
 
485
484
  // src/bridge/cli-relay.ts
486
485
  var CLI_SHIM_FILENAME = "harness-tool.mjs";
@@ -520,24 +519,39 @@ if (!response.ok || payload.error) {
520
519
  console.log(JSON.stringify(payload.result ?? payload, null, 2));
521
520
  `;
522
521
  }
523
- function parseToolRelayCommand({
522
+ function parseToolRelayCommands({
524
523
  command,
525
524
  cliShimPath
526
525
  }) {
527
- return parseToolRelayCommandInternal({ command, cliShimPath, depth: 0 });
526
+ return parseToolRelayCommandsInternal({ command, cliShimPath, depth: 0 });
528
527
  }
529
- function parseToolRelayCommandInternal({
528
+ function parseToolRelayCommandsInternal({
530
529
  command,
531
530
  cliShimPath,
532
531
  depth
533
532
  }) {
533
+ const commands = splitShellAndCommands(command);
534
+ if (!commands) return void 0;
535
+ if (commands.length > 1) {
536
+ const relayCalls = [];
537
+ for (const nestedCommand of commands) {
538
+ const nestedCalls = parseToolRelayCommandsInternal({
539
+ command: nestedCommand,
540
+ cliShimPath,
541
+ depth
542
+ });
543
+ if (!nestedCalls) return void 0;
544
+ relayCalls.push(...nestedCalls);
545
+ }
546
+ return relayCalls;
547
+ }
534
548
  const argv2 = parseShellWords(command);
535
549
  if (!argv2) return void 0;
536
550
  const relayCall = parseDirectToolRelayArgv({ argv: argv2, cliShimPath });
537
- if (relayCall) return relayCall;
551
+ if (relayCall) return [relayCall];
538
552
  const innerCommand = extractShellEvalCommand(argv2);
539
553
  if (!innerCommand || depth >= 2) return void 0;
540
- return parseToolRelayCommandInternal({
554
+ return parseToolRelayCommandsInternal({
541
555
  command: innerCommand,
542
556
  cliShimPath,
543
557
  depth: depth + 1
@@ -566,6 +580,46 @@ function extractShellEvalCommand(argv2) {
566
580
  if (argv2[1] !== "-c" && argv2[1] !== "-lc") return void 0;
567
581
  return argv2[2];
568
582
  }
583
+ function splitShellAndCommands(command) {
584
+ const commands = [];
585
+ let start = 0;
586
+ let quote;
587
+ for (let i = 0; i < command.length; i++) {
588
+ const char = command[i];
589
+ if (quote === "'") {
590
+ if (char === "'") quote = void 0;
591
+ continue;
592
+ }
593
+ if (quote === '"') {
594
+ if (char === '"') {
595
+ quote = void 0;
596
+ } else if (char === "\\") {
597
+ i++;
598
+ }
599
+ continue;
600
+ }
601
+ if (char === '"' || char === "'") {
602
+ quote = char;
603
+ continue;
604
+ }
605
+ if (char === "\\") {
606
+ i++;
607
+ continue;
608
+ }
609
+ if (char === "&" && command[i + 1] === "&") {
610
+ const nestedCommand2 = command.slice(start, i).trim();
611
+ if (!nestedCommand2) return void 0;
612
+ commands.push(nestedCommand2);
613
+ start = i + 2;
614
+ i++;
615
+ }
616
+ }
617
+ if (quote) return void 0;
618
+ const nestedCommand = command.slice(start).trim();
619
+ if (!nestedCommand) return void 0;
620
+ commands.push(nestedCommand);
621
+ return commands;
622
+ }
569
623
  function parseShellWords(command) {
570
624
  const words = [];
571
625
  let current = "";
@@ -670,40 +724,71 @@ function defaultUsage() {
670
724
  };
671
725
  }
672
726
 
727
+ // src/bridge/tool-relay.ts
728
+ import { createServer } from "http";
729
+
673
730
  // src/bridge/tool-relay-auth.ts
674
- import { readdir, readFile, readlink } from "fs/promises";
675
731
  var ToolRelayAuthorizer = class {
676
732
  constructor({
677
733
  ttlMs = 1e4,
678
734
  now = Date.now
679
735
  } = {}) {
680
736
  this.authorizations = [];
737
+ this.pendingRequests = [];
681
738
  this.ttlMs = ttlMs;
682
739
  this.now = now;
683
740
  }
684
741
  authorizeToolCall(call) {
685
742
  this.pruneExpired();
743
+ const key = toolRelayCallKey(call);
744
+ const pendingRequestIndex = this.pendingRequests.findIndex(
745
+ (request) => request.key === key
746
+ );
747
+ if (pendingRequestIndex !== -1) {
748
+ const [pendingRequest] = this.pendingRequests.splice(
749
+ pendingRequestIndex,
750
+ 1
751
+ );
752
+ clearTimeout(pendingRequest.timeout);
753
+ pendingRequest.resolve(true);
754
+ return;
755
+ }
686
756
  this.authorizations.push({
687
- key: toolRelayCallKey(call),
757
+ key,
688
758
  expiresAt: this.now() + this.ttlMs
689
759
  });
690
760
  }
691
- authorizeAnyToolCall() {
761
+ waitForToolCallAuthorization(call) {
692
762
  this.pruneExpired();
693
- this.authorizations.push({
694
- expiresAt: this.now() + this.ttlMs
763
+ const key = toolRelayCallKey(call);
764
+ const authorizationIndex = this.authorizations.findIndex(
765
+ (authorization) => authorization.key === key
766
+ );
767
+ if (authorizationIndex !== -1) {
768
+ this.authorizations.splice(authorizationIndex, 1);
769
+ return Promise.resolve(true);
770
+ }
771
+ const expiresAt = this.now() + this.ttlMs;
772
+ return new Promise((resolve) => {
773
+ const pendingRequest = {
774
+ key,
775
+ expiresAt,
776
+ timeout: setTimeout(() => {
777
+ const index = this.pendingRequests.indexOf(pendingRequest);
778
+ if (index !== -1) this.pendingRequests.splice(index, 1);
779
+ resolve(false);
780
+ }, this.ttlMs),
781
+ resolve
782
+ };
783
+ this.pendingRequests.push(pendingRequest);
695
784
  });
696
785
  }
697
- consumeToolCall(call) {
698
- this.pruneExpired();
699
- const key = toolRelayCallKey(call);
700
- let index = this.authorizations.findIndex((auth) => auth.key === key);
701
- if (index === -1) {
702
- index = this.authorizations.findIndex((auth) => auth.key === void 0);
786
+ close() {
787
+ this.authorizations.length = 0;
788
+ for (const pendingRequest of this.pendingRequests.splice(0)) {
789
+ clearTimeout(pendingRequest.timeout);
790
+ pendingRequest.resolve(false);
703
791
  }
704
- if (index === -1) return false;
705
- this.authorizations.splice(index, 1);
706
- return true;
707
792
  }
708
793
  pruneExpired() {
709
794
  const now = this.now();
@@ -712,6 +797,14 @@ var ToolRelayAuthorizer = class {
712
797
  this.authorizations.splice(i, 1);
713
798
  }
714
799
  }
800
+ for (let i = this.pendingRequests.length - 1; i >= 0; i--) {
801
+ const pendingRequest = this.pendingRequests[i];
802
+ if (pendingRequest.expiresAt <= now) {
803
+ this.pendingRequests.splice(i, 1);
804
+ clearTimeout(pendingRequest.timeout);
805
+ pendingRequest.resolve(false);
806
+ }
807
+ }
715
808
  }
716
809
  };
717
810
  var ToolRelayPendingCalls = class {
@@ -753,62 +846,96 @@ function normalizeJsonValue(value) {
753
846
  }
754
847
  return value;
755
848
  }
756
- async function isToolRelayRequestFromAllowedProcess({
757
- socket,
758
- allowedScriptPaths
759
- }) {
760
- if (process.platform !== "linux") return false;
761
- if (!socket.remotePort || !socket.localPort) return false;
762
- const inode = await findTcpSocketInode({
763
- clientPort: socket.remotePort,
764
- serverPort: socket.localPort
765
- });
766
- if (!inode) return false;
767
- const cmdline = await findProcessCmdlineForSocketInode({ inode });
768
- return cmdline?.some((arg) => allowedScriptPaths.has(arg)) ?? false;
769
- }
770
- async function findTcpSocketInode({
771
- clientPort,
772
- serverPort
849
+
850
+ // src/bridge/tool-relay.ts
851
+ async function startAuthorizedToolRelay({
852
+ tools,
853
+ emit,
854
+ requestToolResult,
855
+ authorizer = new ToolRelayAuthorizer()
773
856
  }) {
774
- for (const tablePath of ["/proc/net/tcp", "/proc/net/tcp6"]) {
775
- const table = await readFile(tablePath, "utf8").catch(() => void 0);
776
- if (!table) continue;
777
- for (const line of table.split("\n").slice(1)) {
778
- const columns = line.trim().split(/\s+/);
779
- if (columns.length < 10) continue;
780
- const local = parseProcNetAddress(columns[1]);
781
- const remote = parseProcNetAddress(columns[2]);
782
- if (local?.port === clientPort && remote?.port === serverPort && columns[9] !== "0") {
783
- return columns[9];
857
+ const toolNames = new Set(tools.map((tool) => tool.name));
858
+ const pendingCalls = new ToolRelayPendingCalls();
859
+ const server = createServer(async (req, res) => {
860
+ try {
861
+ if (req.method !== "POST" || req.url !== "/") {
862
+ res.writeHead(401, { "Content-Type": "application/json" });
863
+ res.end(JSON.stringify({ error: "unauthorized tool relay request" }));
864
+ return;
784
865
  }
866
+ const chunks = [];
867
+ for await (const chunk of req) {
868
+ chunks.push(chunk);
869
+ }
870
+ const body = Buffer.concat(chunks).toString("utf8");
871
+ const { requestId, toolName, input } = JSON.parse(body);
872
+ if (!toolNames.has(toolName)) {
873
+ res.writeHead(403, { "Content-Type": "application/json" });
874
+ res.end(
875
+ JSON.stringify({ error: `Tool "${toolName}" is not available` })
876
+ );
877
+ return;
878
+ }
879
+ const relayCall = { toolName, input };
880
+ if (!await authorizer.waitForToolCallAuthorization(relayCall)) {
881
+ res.writeHead(401, { "Content-Type": "application/json" });
882
+ res.end(JSON.stringify({ error: "unauthorized tool relay request" }));
883
+ return;
884
+ }
885
+ const { result } = pendingCalls.begin({
886
+ call: relayCall,
887
+ run: async () => {
888
+ emit({
889
+ type: "tool-call",
890
+ toolCallId: requestId,
891
+ toolName,
892
+ input: JSON.stringify(input ?? {}),
893
+ providerExecuted: false
894
+ });
895
+ const toolResult = await requestToolResult(requestId);
896
+ emit({
897
+ type: "tool-result",
898
+ toolCallId: requestId,
899
+ toolName,
900
+ result: toolResult.output ?? null,
901
+ isError: !!toolResult.isError
902
+ });
903
+ return toolResult;
904
+ }
905
+ });
906
+ const { output } = await result;
907
+ res.writeHead(200, { "Content-Type": "application/json" });
908
+ res.end(JSON.stringify({ result: output }));
909
+ } catch (error) {
910
+ res.writeHead(500, { "Content-Type": "application/json" });
911
+ res.end(
912
+ JSON.stringify({
913
+ error: error instanceof Error ? error.message : String(error)
914
+ })
915
+ );
785
916
  }
917
+ });
918
+ await new Promise(
919
+ (resolve) => server.listen(0, "127.0.0.1", () => resolve())
920
+ );
921
+ const address = server.address();
922
+ if (!address || typeof address === "string") {
923
+ throw new Error("tool relay did not expose a numeric port");
786
924
  }
787
- return void 0;
788
- }
789
- function parseProcNetAddress(value) {
790
- const [, portHex] = value.split(":");
791
- if (!portHex) return void 0;
792
- return { port: Number.parseInt(portHex, 16) };
925
+ return {
926
+ port: address.port,
927
+ close: () => {
928
+ authorizer.close();
929
+ closeServer(server);
930
+ },
931
+ authorizeToolCall: (call) => authorizer.authorizeToolCall(call)
932
+ };
793
933
  }
794
- async function findProcessCmdlineForSocketInode({
795
- inode
796
- }) {
797
- const procEntries = await readdir("/proc", { withFileTypes: true }).catch(
798
- () => []
799
- );
800
- for (const entry of procEntries) {
801
- if (!entry.isDirectory() || !/^\d+$/.test(entry.name)) continue;
802
- const fdDir = `/proc/${entry.name}/fd`;
803
- const fds = await readdir(fdDir).catch(() => []);
804
- for (const fd of fds) {
805
- const target = await readlink(`${fdDir}/${fd}`).catch(() => void 0);
806
- if (target !== `socket:[${inode}]`) continue;
807
- const cmdline = await readFile(`/proc/${entry.name}/cmdline`, "utf8").then((value) => value.split("\0").filter(Boolean)).catch(() => void 0);
808
- if (cmdline) return cmdline;
809
- }
934
+ function closeServer(server) {
935
+ try {
936
+ server.close();
937
+ } catch {
810
938
  }
811
- return void 0;
812
939
  }
813
940
 
814
941
  // src/bridge/index.ts
@@ -852,7 +979,6 @@ async function runTurn(start, turn) {
852
979
  if (start.tools && start.tools.length > 0) {
853
980
  cliShimPath = `${cliShimDir}/${CLI_SHIM_FILENAME}`;
854
981
  relay = await startToolRelay({
855
- allowedScriptPaths: [cliShimPath, `${bootstrapDir}/host-tool-mcp.mjs`],
856
982
  tools: start.tools,
857
983
  emit,
858
984
  requestToolResult: turn.requestToolResult
@@ -946,18 +1072,16 @@ async function runTurn(start, turn) {
946
1072
  emit({ type: "bridge-thread", threadId: event.thread_id });
947
1073
  }
948
1074
  if (cliShimPath && event.item?.type === "command_execution") {
949
- const relayCall = typeof event.item.command === "string" ? parseToolRelayCommand({
1075
+ const relayCalls = typeof event.item.command === "string" ? parseToolRelayCommands({
950
1076
  command: event.item.command,
951
1077
  cliShimPath
952
1078
  }) : void 0;
953
- if (event.type === "item.started" && relay) {
954
- if (relayCall) {
1079
+ if (event.type === "item.started" && relay && relayCalls) {
1080
+ for (const relayCall of relayCalls) {
955
1081
  relay.authorizeToolCall(relayCall);
956
- } else if (typeof event.item.command !== "string") {
957
- relay.authorizeAnyToolCall();
958
1082
  }
959
1083
  }
960
- if (relayCall) {
1084
+ if (relayCalls) {
961
1085
  stepTracker.observeEvent({ event, itemId: event.item.id });
962
1086
  continue;
963
1087
  }
@@ -1174,97 +1298,11 @@ function mapUsage(usage) {
1174
1298
  };
1175
1299
  }
1176
1300
  async function startToolRelay({
1177
- allowedScriptPaths,
1178
1301
  tools,
1179
1302
  emit,
1180
1303
  requestToolResult
1181
1304
  }) {
1182
- const toolNames = new Set(tools.map((t) => t.name));
1183
- const allowedScriptPathSet = new Set(allowedScriptPaths);
1184
- const authorizer = new ToolRelayAuthorizer();
1185
- const pendingCalls = new ToolRelayPendingCalls();
1186
- const server = createServer(async (req, res) => {
1187
- try {
1188
- if (req.method !== "POST" || req.url !== "/") {
1189
- res.writeHead(401, { "Content-Type": "application/json" });
1190
- res.end(JSON.stringify({ error: "unauthorized tool relay request" }));
1191
- return;
1192
- }
1193
- const chunks = [];
1194
- for await (const chunk of req) {
1195
- chunks.push(chunk);
1196
- }
1197
- const body = Buffer.concat(chunks).toString("utf8");
1198
- const { requestId, toolName, input } = JSON.parse(body);
1199
- if (!toolNames.has(toolName)) {
1200
- res.writeHead(403, { "Content-Type": "application/json" });
1201
- res.end(
1202
- JSON.stringify({ error: `Tool "${toolName}" is not available` })
1203
- );
1204
- return;
1205
- }
1206
- const relayCall = { toolName, input };
1207
- const authorized = authorizer.consumeToolCall(relayCall) || await isToolRelayRequestFromAllowedProcess({
1208
- socket: req.socket,
1209
- allowedScriptPaths: allowedScriptPathSet
1210
- });
1211
- if (!authorized) {
1212
- res.writeHead(401, { "Content-Type": "application/json" });
1213
- res.end(JSON.stringify({ error: "unauthorized tool relay request" }));
1214
- return;
1215
- }
1216
- const { result } = pendingCalls.begin({
1217
- call: relayCall,
1218
- run: async () => {
1219
- emit({
1220
- type: "tool-call",
1221
- toolCallId: requestId,
1222
- toolName,
1223
- input: JSON.stringify(input ?? {}),
1224
- providerExecuted: false
1225
- });
1226
- const toolResult = await requestToolResult(requestId);
1227
- emit({
1228
- type: "tool-result",
1229
- toolCallId: requestId,
1230
- toolName,
1231
- result: toolResult.output ?? null,
1232
- isError: !!toolResult.isError
1233
- });
1234
- return toolResult;
1235
- }
1236
- });
1237
- const { output } = await result;
1238
- res.writeHead(200, { "Content-Type": "application/json" });
1239
- res.end(JSON.stringify({ result: output }));
1240
- } catch (error) {
1241
- res.writeHead(500, { "Content-Type": "application/json" });
1242
- res.end(
1243
- JSON.stringify({
1244
- error: error instanceof Error ? error.message : String(error)
1245
- })
1246
- );
1247
- }
1248
- });
1249
- await new Promise(
1250
- (resolve) => server.listen(0, "127.0.0.1", () => resolve())
1251
- );
1252
- const address = server.address();
1253
- if (!address || typeof address === "string") {
1254
- throw new Error("tool relay did not expose a numeric port");
1255
- }
1256
- return {
1257
- port: address.port,
1258
- close: () => closeServer(server),
1259
- authorizeToolCall: (call) => authorizer.authorizeToolCall(call),
1260
- authorizeAnyToolCall: () => authorizer.authorizeAnyToolCall()
1261
- };
1262
- }
1263
- function closeServer(server) {
1264
- try {
1265
- server.close();
1266
- } catch {
1267
- }
1305
+ return startAuthorizedToolRelay({ tools, emit, requestToolResult });
1268
1306
  }
1269
1307
  function parseArgs(args2) {
1270
1308
  const out = {};