@ai-sdk/harness-opencode 1.0.27 → 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/dist/index.js CHANGED
@@ -168,7 +168,7 @@ var inboundMessageSchema = z.discriminatedUnion("type", [
168
168
  ]);
169
169
 
170
170
  // src/version.ts
171
- var VERSION = true ? "1.0.27" : "0.0.0-test";
171
+ var VERSION = true ? "1.0.28" : "0.0.0-test";
172
172
 
173
173
  // src/opencode-harness.ts
174
174
  var OPENCODE_CLIENT_APP = `ai-sdk/harness-opencode/${VERSION}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness-opencode",
3
- "version": "1.0.27",
3
+ "version": "1.0.28",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -5,7 +5,6 @@ import {
5
5
  } from '@ai-sdk/harness/bridge';
6
6
  import { randomUUID } from 'node:crypto';
7
7
  import { mkdirSync } from 'node:fs';
8
- import { createServer, type Server } from 'node:http';
9
8
  import path from 'node:path';
10
9
  import { argv, env as procEnv } from 'node:process';
11
10
  import type { StartMessage } from '../opencode-bridge-protocol';
@@ -35,21 +34,12 @@ import {
35
34
  type HarnessUsage,
36
35
  type OpenCodeTokenUsage,
37
36
  } from './opencode-usage';
38
- import {
39
- ToolRelayAuthorizer,
40
- isToolRelayRequestFromAllowedProcess,
41
- type ToolRelayCall,
42
- } from './tool-relay-auth';
37
+ import { startAuthorizedToolRelay, type ToolRelay } from './tool-relay';
43
38
 
44
39
  type Emit = (msg: Record<string, unknown>) => void;
45
40
 
46
41
  type OpenCodeClient = ReturnType<typeof createOpencodeClient>;
47
42
  type OpenCodeServer = Awaited<ReturnType<typeof createOpencodeServer>>;
48
- type ToolRelay = {
49
- port: number;
50
- close(): void;
51
- authorizeToolCall(call: ToolRelayCall): void;
52
- };
53
43
 
54
44
  type RuntimeState = {
55
45
  server?: OpenCodeServer;
@@ -172,7 +162,6 @@ async function ensureRuntime({
172
162
  if (start.tools && start.tools.length > 0) {
173
163
  runtime.toolNames = new Set(start.tools.map(tool => tool.name));
174
164
  runtime.relay = await startToolRelay({
175
- allowedScriptPaths: [`${bootstrapDir}/host-tool-mcp.mjs`],
176
165
  tools: start.tools,
177
166
  emit,
178
167
  requestToolResult: turn.requestToolResult,
@@ -1814,105 +1803,17 @@ function emitAssistantContentPart(part: unknown, emit: Emit): void {
1814
1803
  }
1815
1804
 
1816
1805
  async function startToolRelay({
1817
- allowedScriptPaths,
1818
1806
  tools,
1819
1807
  emit,
1820
1808
  requestToolResult,
1821
1809
  }: {
1822
- allowedScriptPaths: ReadonlyArray<string>;
1823
1810
  tools: ReadonlyArray<{ name: string }>;
1824
1811
  emit: Emit;
1825
1812
  requestToolResult: (
1826
1813
  toolCallId: string,
1827
1814
  ) => Promise<{ output: unknown; isError?: boolean }>;
1828
1815
  }): Promise<ToolRelay> {
1829
- const toolNames = new Set(tools.map(t => t.name));
1830
- const allowedScriptPathSet = new Set(allowedScriptPaths);
1831
- const authorizer = new ToolRelayAuthorizer();
1832
- const server = createServer(async (req, res) => {
1833
- try {
1834
- if (req.method !== 'POST' || req.url !== '/') {
1835
- res.writeHead(401, { 'Content-Type': 'application/json' });
1836
- res.end(JSON.stringify({ error: 'unauthorized tool relay request' }));
1837
- return;
1838
- }
1839
- const chunks: Buffer[] = [];
1840
- for await (const chunk of req) {
1841
- chunks.push(chunk as Buffer);
1842
- }
1843
- const body = Buffer.concat(chunks).toString('utf8');
1844
- const { requestId, toolName, input } = JSON.parse(body) as {
1845
- requestId: string;
1846
- toolName: string;
1847
- input: unknown;
1848
- };
1849
-
1850
- if (!toolNames.has(toolName)) {
1851
- res.writeHead(403, { 'Content-Type': 'application/json' });
1852
- res.end(
1853
- JSON.stringify({ error: `Tool "${toolName}" is not available` }),
1854
- );
1855
- return;
1856
- }
1857
- const authorized =
1858
- authorizer.consumeToolCall({ toolName, input }) ||
1859
- (await isToolRelayRequestFromAllowedProcess({
1860
- socket: req.socket,
1861
- allowedScriptPaths: allowedScriptPathSet,
1862
- }));
1863
- if (!authorized) {
1864
- res.writeHead(401, { 'Content-Type': 'application/json' });
1865
- res.end(JSON.stringify({ error: 'unauthorized tool relay request' }));
1866
- return;
1867
- }
1868
-
1869
- emit({
1870
- type: 'tool-call',
1871
- toolCallId: requestId,
1872
- toolName,
1873
- input: JSON.stringify(input ?? {}),
1874
- providerExecuted: false,
1875
- });
1876
-
1877
- const { output, isError } = await requestToolResult(requestId);
1878
- emit({
1879
- type: 'tool-result',
1880
- toolCallId: requestId,
1881
- toolName,
1882
- result: output ?? null,
1883
- isError: !!isError,
1884
- });
1885
-
1886
- res.writeHead(200, { 'Content-Type': 'application/json' });
1887
- res.end(JSON.stringify({ result: output }));
1888
- } catch (error) {
1889
- res.writeHead(500, { 'Content-Type': 'application/json' });
1890
- res.end(
1891
- JSON.stringify({
1892
- error: error instanceof Error ? error.message : String(error),
1893
- }),
1894
- );
1895
- }
1896
- });
1897
-
1898
- await new Promise<void>(resolve =>
1899
- server.listen(0, '127.0.0.1', () => resolve()),
1900
- );
1901
- const address = server.address();
1902
- if (!address || typeof address === 'string') {
1903
- throw new Error('tool relay did not expose a numeric port');
1904
- }
1905
- return {
1906
- port: address.port,
1907
- close: () => closeServer(server),
1908
- authorizeToolCall: call => authorizer.authorizeToolCall(call),
1909
- };
1910
- }
1911
-
1912
- function closeServer(server: Server): void {
1913
- try {
1914
- server.close();
1915
- } catch {}
1816
+ return startAuthorizedToolRelay({ tools, emit, requestToolResult });
1916
1817
  }
1917
1818
 
1918
1819
  function createDeferred<T>(): {
@@ -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;
@@ -11,6 +8,12 @@ export class ToolRelayAuthorizer {
11
8
  private readonly now: () => number;
12
9
  private readonly authorizations: Array<{ key: string; expiresAt: number }> =
13
10
  [];
11
+ private readonly pendingRequests: Array<{
12
+ key: string;
13
+ expiresAt: number;
14
+ timeout: ReturnType<typeof setTimeout>;
15
+ resolve: (authorized: boolean) => void;
16
+ }> = [];
14
17
 
15
18
  constructor({
16
19
  ttlMs = 10_000,
@@ -25,19 +28,58 @@ export class ToolRelayAuthorizer {
25
28
 
26
29
  authorizeToolCall(call: ToolRelayCall): void {
27
30
  this.pruneExpired();
31
+ const key = toolRelayCallKey(call);
32
+ const pendingRequestIndex = this.pendingRequests.findIndex(
33
+ request => request.key === key,
34
+ );
35
+ if (pendingRequestIndex !== -1) {
36
+ const [pendingRequest] = this.pendingRequests.splice(
37
+ pendingRequestIndex,
38
+ 1,
39
+ );
40
+ clearTimeout(pendingRequest.timeout);
41
+ pendingRequest.resolve(true);
42
+ return;
43
+ }
28
44
  this.authorizations.push({
29
- key: toolRelayCallKey(call),
45
+ key,
30
46
  expiresAt: this.now() + this.ttlMs,
31
47
  });
32
48
  }
33
49
 
34
- consumeToolCall(call: ToolRelayCall): boolean {
50
+ waitForToolCallAuthorization(call: ToolRelayCall): Promise<boolean> {
35
51
  this.pruneExpired();
36
52
  const key = toolRelayCallKey(call);
37
- const index = this.authorizations.findIndex(auth => auth.key === key);
38
- if (index === -1) return false;
39
- this.authorizations.splice(index, 1);
40
- return true;
53
+ const authorizationIndex = this.authorizations.findIndex(
54
+ authorization => authorization.key === key,
55
+ );
56
+ if (authorizationIndex !== -1) {
57
+ this.authorizations.splice(authorizationIndex, 1);
58
+ return Promise.resolve(true);
59
+ }
60
+
61
+ const expiresAt = this.now() + this.ttlMs;
62
+ return new Promise(resolve => {
63
+ const pendingRequest = {
64
+ key,
65
+ expiresAt,
66
+ timeout: setTimeout(() => {
67
+ const index = this.pendingRequests.indexOf(pendingRequest);
68
+ if (index !== -1) this.pendingRequests.splice(index, 1);
69
+ resolve(false);
70
+ }, this.ttlMs),
71
+ resolve,
72
+ };
73
+ this.pendingRequests.push(pendingRequest);
74
+ });
75
+ }
76
+
77
+ close(): void {
78
+ this.authorizations.length = 0;
79
+ for (const pendingRequest of this.pendingRequests.splice(0)) {
80
+ clearTimeout(pendingRequest.timeout);
81
+ pendingRequest.resolve(false);
82
+ }
41
83
  }
42
84
 
43
85
  private pruneExpired(): void {
@@ -47,6 +89,14 @@ export class ToolRelayAuthorizer {
47
89
  this.authorizations.splice(i, 1);
48
90
  }
49
91
  }
92
+ for (let i = this.pendingRequests.length - 1; i >= 0; i--) {
93
+ const pendingRequest = this.pendingRequests[i];
94
+ if (pendingRequest.expiresAt <= now) {
95
+ this.pendingRequests.splice(i, 1);
96
+ clearTimeout(pendingRequest.timeout);
97
+ pendingRequest.resolve(false);
98
+ }
99
+ }
50
100
  }
51
101
  }
52
102
 
@@ -72,80 +122,3 @@ function normalizeJsonValue(value: unknown): unknown {
72
122
  }
73
123
  return value;
74
124
  }
75
-
76
- export async function isToolRelayRequestFromAllowedProcess({
77
- socket,
78
- allowedScriptPaths,
79
- }: {
80
- socket: Socket;
81
- allowedScriptPaths: ReadonlySet<string>;
82
- }): Promise<boolean> {
83
- if (process.platform !== 'linux') return false;
84
- if (!socket.remotePort || !socket.localPort) return false;
85
-
86
- const inode = await findTcpSocketInode({
87
- clientPort: socket.remotePort,
88
- serverPort: socket.localPort,
89
- });
90
- if (!inode) return false;
91
-
92
- const cmdline = await findProcessCmdlineForSocketInode({ inode });
93
- return cmdline?.some(arg => allowedScriptPaths.has(arg)) ?? false;
94
- }
95
-
96
- async function findTcpSocketInode({
97
- clientPort,
98
- serverPort,
99
- }: {
100
- clientPort: number;
101
- serverPort: number;
102
- }): Promise<string | undefined> {
103
- for (const tablePath of ['/proc/net/tcp', '/proc/net/tcp6']) {
104
- const table = await readFile(tablePath, 'utf8').catch(() => undefined);
105
- if (!table) continue;
106
- for (const line of table.split('\n').slice(1)) {
107
- const columns = line.trim().split(/\s+/);
108
- if (columns.length < 10) continue;
109
- const local = parseProcNetAddress(columns[1]);
110
- const remote = parseProcNetAddress(columns[2]);
111
- if (
112
- local?.port === clientPort &&
113
- remote?.port === serverPort &&
114
- columns[9] !== '0'
115
- ) {
116
- return columns[9];
117
- }
118
- }
119
- }
120
- return undefined;
121
- }
122
-
123
- function parseProcNetAddress(value: string): { port: number } | undefined {
124
- const [, portHex] = value.split(':');
125
- if (!portHex) return undefined;
126
- return { port: Number.parseInt(portHex, 16) };
127
- }
128
-
129
- async function findProcessCmdlineForSocketInode({
130
- inode,
131
- }: {
132
- inode: string;
133
- }): Promise<string[] | undefined> {
134
- const procEntries = await readdir('/proc', { withFileTypes: true }).catch(
135
- () => [],
136
- );
137
- for (const entry of procEntries) {
138
- if (!entry.isDirectory() || !/^\d+$/.test(entry.name)) continue;
139
- const fdDir = `/proc/${entry.name}/fd`;
140
- const fds = await readdir(fdDir).catch(() => []);
141
- for (const fd of fds) {
142
- const target = await readlink(`${fdDir}/${fd}`).catch(() => undefined);
143
- if (target !== `socket:[${inode}]`) continue;
144
- const cmdline = await readFile(`/proc/${entry.name}/cmdline`, 'utf8')
145
- .then(value => value.split('\0').filter(Boolean))
146
- .catch(() => undefined);
147
- if (cmdline) return cmdline;
148
- }
149
- }
150
- return undefined;
151
- }
@@ -0,0 +1,107 @@
1
+ import { createServer, type Server } from 'node:http';
2
+ import { ToolRelayAuthorizer, type ToolRelayCall } from './tool-relay-auth';
3
+
4
+ export type ToolRelay = {
5
+ port: number;
6
+ close(): void;
7
+ authorizeToolCall(call: ToolRelayCall): void;
8
+ };
9
+
10
+ export async function startAuthorizedToolRelay({
11
+ tools,
12
+ emit,
13
+ requestToolResult,
14
+ authorizer = new ToolRelayAuthorizer(),
15
+ }: {
16
+ tools: ReadonlyArray<{ name: string }>;
17
+ emit: (message: Record<string, unknown>) => void;
18
+ requestToolResult: (
19
+ toolCallId: string,
20
+ ) => Promise<{ output: unknown; isError?: boolean }>;
21
+ authorizer?: ToolRelayAuthorizer;
22
+ }): Promise<ToolRelay> {
23
+ const toolNames = new Set(tools.map(tool => tool.name));
24
+ const server = createServer(async (req, res) => {
25
+ try {
26
+ if (req.method !== 'POST' || req.url !== '/') {
27
+ res.writeHead(401, { 'Content-Type': 'application/json' });
28
+ res.end(JSON.stringify({ error: 'unauthorized tool relay request' }));
29
+ return;
30
+ }
31
+ const chunks: Buffer[] = [];
32
+ for await (const chunk of req) {
33
+ chunks.push(chunk as Buffer);
34
+ }
35
+ const body = Buffer.concat(chunks).toString('utf8');
36
+ const { requestId, toolName, input } = JSON.parse(body) as {
37
+ requestId: string;
38
+ toolName: string;
39
+ input: unknown;
40
+ };
41
+
42
+ if (!toolNames.has(toolName)) {
43
+ res.writeHead(403, { 'Content-Type': 'application/json' });
44
+ res.end(
45
+ JSON.stringify({ error: `Tool "${toolName}" is not available` }),
46
+ );
47
+ return;
48
+ }
49
+ if (
50
+ !(await authorizer.waitForToolCallAuthorization({ toolName, input }))
51
+ ) {
52
+ res.writeHead(401, { 'Content-Type': 'application/json' });
53
+ res.end(JSON.stringify({ error: 'unauthorized tool relay request' }));
54
+ return;
55
+ }
56
+
57
+ emit({
58
+ type: 'tool-call',
59
+ toolCallId: requestId,
60
+ toolName,
61
+ input: JSON.stringify(input ?? {}),
62
+ providerExecuted: false,
63
+ });
64
+
65
+ const { output, isError } = await requestToolResult(requestId);
66
+ emit({
67
+ type: 'tool-result',
68
+ toolCallId: requestId,
69
+ toolName,
70
+ result: output ?? null,
71
+ isError: !!isError,
72
+ });
73
+
74
+ res.writeHead(200, { 'Content-Type': 'application/json' });
75
+ res.end(JSON.stringify({ result: output }));
76
+ } catch (error) {
77
+ res.writeHead(500, { 'Content-Type': 'application/json' });
78
+ res.end(
79
+ JSON.stringify({
80
+ error: error instanceof Error ? error.message : String(error),
81
+ }),
82
+ );
83
+ }
84
+ });
85
+
86
+ await new Promise<void>(resolve =>
87
+ server.listen(0, '127.0.0.1', () => resolve()),
88
+ );
89
+ const address = server.address();
90
+ if (!address || typeof address === 'string') {
91
+ throw new Error('tool relay did not expose a numeric port');
92
+ }
93
+ return {
94
+ port: address.port,
95
+ close: () => {
96
+ authorizer.close();
97
+ closeServer(server);
98
+ },
99
+ authorizeToolCall: call => authorizer.authorizeToolCall(call),
100
+ };
101
+ }
102
+
103
+ function closeServer(server: Server): void {
104
+ try {
105
+ server.close();
106
+ } catch {}
107
+ }