@ai-sdk/harness-opencode 1.0.26 → 1.0.28

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-opencode
2
2
 
3
+ ## 1.0.28
4
+
5
+ ### Patch Changes
6
+
7
+ - c93803d: fix(harness): improve CLI relay tool invocation logic
8
+
9
+ ## 1.0.27
10
+
11
+ ### Patch Changes
12
+
13
+ - @ai-sdk/harness@1.0.27
14
+
3
15
  ## 1.0.26
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 { mkdirSync } from "fs";
483
- import { createServer } from "http";
484
483
  import path2 from "path";
485
484
  import { argv, env as procEnv2 } from "process";
486
485
  import {
@@ -703,31 +702,71 @@ function prependOpenCodeBinToPath({
703
702
  ].join(path.delimiter);
704
703
  }
705
704
 
705
+ // src/bridge/tool-relay.ts
706
+ import { createServer } from "http";
707
+
706
708
  // src/bridge/tool-relay-auth.ts
707
- import { readdir, readFile, readlink } from "fs/promises";
708
709
  var ToolRelayAuthorizer = class {
709
710
  constructor({
710
711
  ttlMs = 1e4,
711
712
  now = Date.now
712
713
  } = {}) {
713
714
  this.authorizations = [];
715
+ this.pendingRequests = [];
714
716
  this.ttlMs = ttlMs;
715
717
  this.now = now;
716
718
  }
717
719
  authorizeToolCall(call) {
718
720
  this.pruneExpired();
721
+ const key = toolRelayCallKey(call);
722
+ const pendingRequestIndex = this.pendingRequests.findIndex(
723
+ (request) => request.key === key
724
+ );
725
+ if (pendingRequestIndex !== -1) {
726
+ const [pendingRequest] = this.pendingRequests.splice(
727
+ pendingRequestIndex,
728
+ 1
729
+ );
730
+ clearTimeout(pendingRequest.timeout);
731
+ pendingRequest.resolve(true);
732
+ return;
733
+ }
719
734
  this.authorizations.push({
720
- key: toolRelayCallKey(call),
735
+ key,
721
736
  expiresAt: this.now() + this.ttlMs
722
737
  });
723
738
  }
724
- consumeToolCall(call) {
739
+ waitForToolCallAuthorization(call) {
725
740
  this.pruneExpired();
726
741
  const key = toolRelayCallKey(call);
727
- const index = this.authorizations.findIndex((auth) => auth.key === key);
728
- if (index === -1) return false;
729
- this.authorizations.splice(index, 1);
730
- return true;
742
+ const authorizationIndex = this.authorizations.findIndex(
743
+ (authorization) => authorization.key === key
744
+ );
745
+ if (authorizationIndex !== -1) {
746
+ this.authorizations.splice(authorizationIndex, 1);
747
+ return Promise.resolve(true);
748
+ }
749
+ const expiresAt = this.now() + this.ttlMs;
750
+ return new Promise((resolve) => {
751
+ const pendingRequest = {
752
+ key,
753
+ expiresAt,
754
+ timeout: setTimeout(() => {
755
+ const index = this.pendingRequests.indexOf(pendingRequest);
756
+ if (index !== -1) this.pendingRequests.splice(index, 1);
757
+ resolve(false);
758
+ }, this.ttlMs),
759
+ resolve
760
+ };
761
+ this.pendingRequests.push(pendingRequest);
762
+ });
763
+ }
764
+ close() {
765
+ this.authorizations.length = 0;
766
+ for (const pendingRequest of this.pendingRequests.splice(0)) {
767
+ clearTimeout(pendingRequest.timeout);
768
+ pendingRequest.resolve(false);
769
+ }
731
770
  }
732
771
  pruneExpired() {
733
772
  const now = this.now();
@@ -736,6 +775,14 @@ var ToolRelayAuthorizer = class {
736
775
  this.authorizations.splice(i, 1);
737
776
  }
738
777
  }
778
+ for (let i = this.pendingRequests.length - 1; i >= 0; i--) {
779
+ const pendingRequest = this.pendingRequests[i];
780
+ if (pendingRequest.expiresAt <= now) {
781
+ this.pendingRequests.splice(i, 1);
782
+ clearTimeout(pendingRequest.timeout);
783
+ pendingRequest.resolve(false);
784
+ }
785
+ }
739
786
  }
740
787
  };
741
788
  function toolRelayCallKey({ toolName, input }) {
@@ -755,62 +802,87 @@ function normalizeJsonValue(value) {
755
802
  }
756
803
  return value;
757
804
  }
758
- async function isToolRelayRequestFromAllowedProcess({
759
- socket,
760
- allowedScriptPaths
761
- }) {
762
- if (process.platform !== "linux") return false;
763
- if (!socket.remotePort || !socket.localPort) return false;
764
- const inode = await findTcpSocketInode({
765
- clientPort: socket.remotePort,
766
- serverPort: socket.localPort
767
- });
768
- if (!inode) return false;
769
- const cmdline = await findProcessCmdlineForSocketInode({ inode });
770
- return cmdline?.some((arg) => allowedScriptPaths.has(arg)) ?? false;
771
- }
772
- async function findTcpSocketInode({
773
- clientPort,
774
- serverPort
805
+
806
+ // src/bridge/tool-relay.ts
807
+ async function startAuthorizedToolRelay({
808
+ tools,
809
+ emit,
810
+ requestToolResult,
811
+ authorizer = new ToolRelayAuthorizer()
775
812
  }) {
776
- for (const tablePath of ["/proc/net/tcp", "/proc/net/tcp6"]) {
777
- const table = await readFile(tablePath, "utf8").catch(() => void 0);
778
- if (!table) continue;
779
- for (const line of table.split("\n").slice(1)) {
780
- const columns = line.trim().split(/\s+/);
781
- if (columns.length < 10) continue;
782
- const local = parseProcNetAddress(columns[1]);
783
- const remote = parseProcNetAddress(columns[2]);
784
- if (local?.port === clientPort && remote?.port === serverPort && columns[9] !== "0") {
785
- return columns[9];
813
+ const toolNames = new Set(tools.map((tool) => tool.name));
814
+ const server = createServer(async (req, res) => {
815
+ try {
816
+ if (req.method !== "POST" || req.url !== "/") {
817
+ res.writeHead(401, { "Content-Type": "application/json" });
818
+ res.end(JSON.stringify({ error: "unauthorized tool relay request" }));
819
+ return;
820
+ }
821
+ const chunks = [];
822
+ for await (const chunk of req) {
823
+ chunks.push(chunk);
824
+ }
825
+ const body = Buffer.concat(chunks).toString("utf8");
826
+ const { requestId, toolName, input } = JSON.parse(body);
827
+ if (!toolNames.has(toolName)) {
828
+ res.writeHead(403, { "Content-Type": "application/json" });
829
+ res.end(
830
+ JSON.stringify({ error: `Tool "${toolName}" is not available` })
831
+ );
832
+ return;
833
+ }
834
+ if (!await authorizer.waitForToolCallAuthorization({ toolName, input })) {
835
+ res.writeHead(401, { "Content-Type": "application/json" });
836
+ res.end(JSON.stringify({ error: "unauthorized tool relay request" }));
837
+ return;
786
838
  }
839
+ emit({
840
+ type: "tool-call",
841
+ toolCallId: requestId,
842
+ toolName,
843
+ input: JSON.stringify(input ?? {}),
844
+ providerExecuted: false
845
+ });
846
+ const { output, isError } = await requestToolResult(requestId);
847
+ emit({
848
+ type: "tool-result",
849
+ toolCallId: requestId,
850
+ toolName,
851
+ result: output ?? null,
852
+ isError: !!isError
853
+ });
854
+ res.writeHead(200, { "Content-Type": "application/json" });
855
+ res.end(JSON.stringify({ result: output }));
856
+ } catch (error) {
857
+ res.writeHead(500, { "Content-Type": "application/json" });
858
+ res.end(
859
+ JSON.stringify({
860
+ error: error instanceof Error ? error.message : String(error)
861
+ })
862
+ );
787
863
  }
864
+ });
865
+ await new Promise(
866
+ (resolve) => server.listen(0, "127.0.0.1", () => resolve())
867
+ );
868
+ const address = server.address();
869
+ if (!address || typeof address === "string") {
870
+ throw new Error("tool relay did not expose a numeric port");
788
871
  }
789
- return void 0;
790
- }
791
- function parseProcNetAddress(value) {
792
- const [, portHex] = value.split(":");
793
- if (!portHex) return void 0;
794
- return { port: Number.parseInt(portHex, 16) };
872
+ return {
873
+ port: address.port,
874
+ close: () => {
875
+ authorizer.close();
876
+ closeServer(server);
877
+ },
878
+ authorizeToolCall: (call) => authorizer.authorizeToolCall(call)
879
+ };
795
880
  }
796
- async function findProcessCmdlineForSocketInode({
797
- inode
798
- }) {
799
- const procEntries = await readdir("/proc", { withFileTypes: true }).catch(
800
- () => []
801
- );
802
- for (const entry of procEntries) {
803
- if (!entry.isDirectory() || !/^\d+$/.test(entry.name)) continue;
804
- const fdDir = `/proc/${entry.name}/fd`;
805
- const fds = await readdir(fdDir).catch(() => []);
806
- for (const fd of fds) {
807
- const target = await readlink(`${fdDir}/${fd}`).catch(() => void 0);
808
- if (target !== `socket:[${inode}]`) continue;
809
- const cmdline = await readFile(`/proc/${entry.name}/cmdline`, "utf8").then((value) => value.split("\0").filter(Boolean)).catch(() => void 0);
810
- if (cmdline) return cmdline;
811
- }
881
+ function closeServer(server) {
882
+ try {
883
+ server.close();
884
+ } catch {
812
885
  }
813
- return void 0;
814
886
  }
815
887
 
816
888
  // src/bridge/index.ts
@@ -903,7 +975,6 @@ async function ensureRuntime({
903
975
  if (start.tools && start.tools.length > 0) {
904
976
  runtime.toolNames = new Set(start.tools.map((tool) => tool.name));
905
977
  runtime.relay = await startToolRelay({
906
- allowedScriptPaths: [`${bootstrapDir}/host-tool-mcp.mjs`],
907
978
  tools: start.tools,
908
979
  emit,
909
980
  requestToolResult: turn.requestToolResult
@@ -2153,87 +2224,11 @@ function emitAssistantContentPart(part, emit) {
2153
2224
  emit({ type: "reasoning-end", id });
2154
2225
  }
2155
2226
  async function startToolRelay({
2156
- allowedScriptPaths,
2157
2227
  tools,
2158
2228
  emit,
2159
2229
  requestToolResult
2160
2230
  }) {
2161
- const toolNames = new Set(tools.map((t) => t.name));
2162
- const allowedScriptPathSet = new Set(allowedScriptPaths);
2163
- const authorizer = new ToolRelayAuthorizer();
2164
- const server = createServer(async (req, res) => {
2165
- try {
2166
- if (req.method !== "POST" || req.url !== "/") {
2167
- res.writeHead(401, { "Content-Type": "application/json" });
2168
- res.end(JSON.stringify({ error: "unauthorized tool relay request" }));
2169
- return;
2170
- }
2171
- const chunks = [];
2172
- for await (const chunk of req) {
2173
- chunks.push(chunk);
2174
- }
2175
- const body = Buffer.concat(chunks).toString("utf8");
2176
- const { requestId, toolName, input } = JSON.parse(body);
2177
- if (!toolNames.has(toolName)) {
2178
- res.writeHead(403, { "Content-Type": "application/json" });
2179
- res.end(
2180
- JSON.stringify({ error: `Tool "${toolName}" is not available` })
2181
- );
2182
- return;
2183
- }
2184
- const authorized = authorizer.consumeToolCall({ toolName, input }) || await isToolRelayRequestFromAllowedProcess({
2185
- socket: req.socket,
2186
- allowedScriptPaths: allowedScriptPathSet
2187
- });
2188
- if (!authorized) {
2189
- res.writeHead(401, { "Content-Type": "application/json" });
2190
- res.end(JSON.stringify({ error: "unauthorized tool relay request" }));
2191
- return;
2192
- }
2193
- emit({
2194
- type: "tool-call",
2195
- toolCallId: requestId,
2196
- toolName,
2197
- input: JSON.stringify(input ?? {}),
2198
- providerExecuted: false
2199
- });
2200
- const { output, isError } = await requestToolResult(requestId);
2201
- emit({
2202
- type: "tool-result",
2203
- toolCallId: requestId,
2204
- toolName,
2205
- result: output ?? null,
2206
- isError: !!isError
2207
- });
2208
- res.writeHead(200, { "Content-Type": "application/json" });
2209
- res.end(JSON.stringify({ result: output }));
2210
- } catch (error) {
2211
- res.writeHead(500, { "Content-Type": "application/json" });
2212
- res.end(
2213
- JSON.stringify({
2214
- error: error instanceof Error ? error.message : String(error)
2215
- })
2216
- );
2217
- }
2218
- });
2219
- await new Promise(
2220
- (resolve) => server.listen(0, "127.0.0.1", () => resolve())
2221
- );
2222
- const address = server.address();
2223
- if (!address || typeof address === "string") {
2224
- throw new Error("tool relay did not expose a numeric port");
2225
- }
2226
- return {
2227
- port: address.port,
2228
- close: () => closeServer(server),
2229
- authorizeToolCall: (call) => authorizer.authorizeToolCall(call)
2230
- };
2231
- }
2232
- function closeServer(server) {
2233
- try {
2234
- server.close();
2235
- } catch {
2236
- }
2231
+ return startAuthorizedToolRelay({ tools, emit, requestToolResult });
2237
2232
  }
2238
2233
  function createDeferred() {
2239
2234
  let resolve;