@ai-sdk/harness-opencode 1.0.10 → 1.0.12

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.
@@ -3,7 +3,6 @@ import {
3
3
  type BridgeEvent,
4
4
  type BridgeTurn,
5
5
  } from '@ai-sdk/harness/bridge';
6
- import type { HarnessV1BuiltinToolName } from '@ai-sdk/harness';
7
6
  import { randomUUID } from 'node:crypto';
8
7
  import { mkdirSync } from 'node:fs';
9
8
  import { createServer, type Server } from 'node:http';
@@ -32,21 +31,39 @@ import {
32
31
  type HarnessUsage,
33
32
  type OpenCodeTokenUsage,
34
33
  } from './opencode-usage';
34
+ import {
35
+ ToolRelayAuthorizer,
36
+ isToolRelayRequestFromAllowedProcess,
37
+ type ToolRelayCall,
38
+ } from './tool-relay-auth';
35
39
 
36
40
  type Emit = (msg: Record<string, unknown>) => void;
37
41
 
38
42
  type OpenCodeClient = ReturnType<typeof createOpencodeClient>;
39
43
  type OpenCodeServer = Awaited<ReturnType<typeof createOpencodeServer>>;
44
+ type ToolRelay = {
45
+ port: number;
46
+ close(): void;
47
+ authorizeToolCall(call: ToolRelayCall): void;
48
+ };
40
49
 
41
50
  type RuntimeState = {
42
51
  server?: OpenCodeServer;
43
52
  client?: OpenCodeClient;
44
53
  sessionId?: string;
45
- relay?: { port: number; close(): void };
54
+ relay?: ToolRelay;
46
55
  toolNames: Set<string>;
47
56
  };
48
57
 
49
- const NATIVE_TO_COMMON: Readonly<Record<string, HarnessV1BuiltinToolName>> = {
58
+ type CommonBuiltinToolName =
59
+ | 'read'
60
+ | 'write'
61
+ | 'edit'
62
+ | 'bash'
63
+ | 'glob'
64
+ | 'grep';
65
+
66
+ const NATIVE_TO_COMMON: Readonly<Record<string, CommonBuiltinToolName>> = {
50
67
  view: 'read',
51
68
  read: 'read',
52
69
  write: 'write',
@@ -65,6 +82,20 @@ const OPENCODE_TO_WIRE: Readonly<Record<string, string>> = {
65
82
  subtask: 'agent',
66
83
  };
67
84
 
85
+ const PUBLIC_TO_NATIVE: Readonly<Record<string, string>> = {
86
+ read: 'view',
87
+ write: 'write',
88
+ edit: 'edit',
89
+ bash: 'bash',
90
+ glob: 'glob',
91
+ grep: 'grep',
92
+ ls: 'list',
93
+ webfetch: 'webfetch',
94
+ skill: 'skill',
95
+ todowrite: 'todowrite',
96
+ agent: 'agent',
97
+ };
98
+
68
99
  const TOOL_KIND: Readonly<Record<string, 'readonly' | 'edit' | 'bash'>> = {
69
100
  read: 'readonly',
70
101
  glob: 'readonly',
@@ -133,12 +164,10 @@ async function ensureRuntime({
133
164
  }): Promise<void> {
134
165
  if (runtime.client) return;
135
166
 
136
- let relayToken: string | undefined;
137
167
  if (start.tools && start.tools.length > 0) {
138
- relayToken = randomUUID();
139
168
  runtime.toolNames = new Set(start.tools.map(tool => tool.name));
140
169
  runtime.relay = await startToolRelay({
141
- relayToken,
170
+ allowedScriptPaths: [`${bootstrapDir}/host-tool-mcp.mjs`],
142
171
  tools: start.tools,
143
172
  emit,
144
173
  requestToolResult: turn.requestToolResult,
@@ -151,7 +180,6 @@ async function ensureRuntime({
151
180
  timeout: 30_000,
152
181
  config: buildOpenCodeConfig({
153
182
  start,
154
- relayToken,
155
183
  relayPort: runtime.relay?.port,
156
184
  }) as never,
157
185
  });
@@ -164,11 +192,9 @@ async function ensureRuntime({
164
192
 
165
193
  function buildOpenCodeConfig({
166
194
  start,
167
- relayToken,
168
195
  relayPort,
169
196
  }: {
170
197
  start: StartMessage;
171
- relayToken: string | undefined;
172
198
  relayPort: number | undefined;
173
199
  }): Record<string, unknown> {
174
200
  const config: Record<string, unknown> = {
@@ -189,9 +215,21 @@ function buildOpenCodeConfig({
189
215
  };
190
216
  if (start.model) config.model = start.model;
191
217
  if (skillsDir) config.skills = { paths: [skillsDir] };
218
+ const inactiveToolNames = resolveInactiveBuiltinToolNames(start);
219
+ const permission = config.permission as Record<string, unknown>;
220
+ for (const toolName of inactiveToolNames) {
221
+ const permissionName = toPermissionToolName(
222
+ PUBLIC_TO_NATIVE[toolName] ?? toolName,
223
+ );
224
+ if (permissionName === 'ls') {
225
+ permission.list = 'ask';
226
+ } else {
227
+ permission[permissionName] = 'ask';
228
+ }
229
+ }
192
230
  const provider = buildProviderConfig(start);
193
231
  if (provider) config.provider = provider;
194
- if (relayToken && relayPort && start.tools && start.tools.length > 0) {
232
+ if (relayPort && start.tools && start.tools.length > 0) {
195
233
  config.mcp = {
196
234
  'harness-tools': {
197
235
  type: 'local',
@@ -206,7 +244,6 @@ function buildOpenCodeConfig({
206
244
  })),
207
245
  ),
208
246
  TOOL_RELAY_URL: `http://127.0.0.1:${relayPort}`,
209
- TOOL_RELAY_TOKEN: relayToken,
210
247
  },
211
248
  },
212
249
  };
@@ -498,6 +535,7 @@ async function runPrompt({
498
535
  client,
499
536
  sessionId,
500
537
  permissionMode: start.permissionMode,
538
+ builtinToolFiltering: start.builtinToolFiltering,
501
539
  turn,
502
540
  emit: msg => {
503
541
  if (msg.type === 'text-delta' || msg.type === 'reasoning-delta') {
@@ -620,6 +658,7 @@ async function runCompaction({
620
658
  client,
621
659
  sessionId,
622
660
  permissionMode: start.permissionMode,
661
+ builtinToolFiltering: start.builtinToolFiltering,
623
662
  turn,
624
663
  emit: msg => {
625
664
  if (msg.type === 'compaction') sawCompaction = true;
@@ -684,6 +723,7 @@ async function consumeEvents({
684
723
  client,
685
724
  sessionId,
686
725
  permissionMode,
726
+ builtinToolFiltering,
687
727
  turn,
688
728
  emit,
689
729
  signal,
@@ -692,6 +732,7 @@ async function consumeEvents({
692
732
  client: OpenCodeClient;
693
733
  sessionId: string;
694
734
  permissionMode: StartMessage['permissionMode'];
735
+ builtinToolFiltering: StartMessage['builtinToolFiltering'];
695
736
  turn: BridgeTurn;
696
737
  emit: Emit;
697
738
  signal: AbortSignal;
@@ -710,6 +751,7 @@ async function consumeEvents({
710
751
  state,
711
752
  sessionId,
712
753
  permissionMode,
754
+ builtinToolFiltering,
713
755
  client,
714
756
  turn,
715
757
  emit,
@@ -725,6 +767,7 @@ type TranslationState = {
725
767
  toolNames: Map<string, { rawToolName: string; toolName: string }>;
726
768
  toolCallsEmitted: Set<string>;
727
769
  toolResultsEmitted: Set<string>;
770
+ hostToolCallsAuthorized: Set<string>;
728
771
  shellCommands: Map<string, string>;
729
772
  messageRoles: Map<string, string>;
730
773
  turnUsage: Record<string, unknown> | undefined;
@@ -740,6 +783,7 @@ function createTranslationState(): TranslationState {
740
783
  toolNames: new Map(),
741
784
  toolCallsEmitted: new Set(),
742
785
  toolResultsEmitted: new Set(),
786
+ hostToolCallsAuthorized: new Set(),
743
787
  shellCommands: new Map(),
744
788
  messageRoles: new Map(),
745
789
  turnUsage: undefined,
@@ -753,6 +797,7 @@ async function translateAndEmit({
753
797
  state,
754
798
  sessionId,
755
799
  permissionMode,
800
+ builtinToolFiltering,
756
801
  client,
757
802
  turn,
758
803
  emit,
@@ -761,6 +806,7 @@ async function translateAndEmit({
761
806
  state: TranslationState;
762
807
  sessionId: string;
763
808
  permissionMode: StartMessage['permissionMode'];
809
+ builtinToolFiltering: StartMessage['builtinToolFiltering'];
764
810
  client: OpenCodeClient;
765
811
  turn: BridgeTurn;
766
812
  emit: Emit;
@@ -922,7 +968,16 @@ async function translateAndEmit({
922
968
  const rawToolName = String(props.tool ?? 'unknown');
923
969
  const toolName = toWireToolName(rawToolName);
924
970
  state.toolNames.set(callID, { rawToolName, toolName });
925
- if (isHostTool(toolName, props.tool)) return;
971
+ const hostToolName = getHostToolName(toolName, props.tool);
972
+ if (hostToolName) {
973
+ authorizeHostToolCall({
974
+ callID,
975
+ toolName: hostToolName,
976
+ input: props.input ?? parseToolInput(state, props),
977
+ state,
978
+ });
979
+ return;
980
+ }
926
981
  emit({
927
982
  type: 'tool-call',
928
983
  toolCallId: callID,
@@ -947,7 +1002,7 @@ async function translateAndEmit({
947
1002
  String((props as { tool?: unknown }).tool ?? '');
948
1003
  const toolName =
949
1004
  cachedTool?.toolName ?? toWireToolName(rawToolName || 'unknown');
950
- if (isHostTool(toolName, rawToolName)) return;
1005
+ if (getHostToolName(toolName, rawToolName)) return;
951
1006
  emit({
952
1007
  type: 'tool-result',
953
1008
  toolCallId: callID,
@@ -1007,6 +1062,7 @@ async function translateAndEmit({
1007
1062
  client,
1008
1063
  sessionId,
1009
1064
  permissionMode,
1065
+ builtinToolFiltering,
1010
1066
  turn,
1011
1067
  emit,
1012
1068
  event,
@@ -1018,6 +1074,7 @@ async function translateAndEmit({
1018
1074
  client,
1019
1075
  sessionId,
1020
1076
  permissionMode,
1077
+ builtinToolFiltering,
1021
1078
  turn,
1022
1079
  emit,
1023
1080
  event,
@@ -1152,7 +1209,18 @@ function emitLegacyToolPart({
1152
1209
  const rawToolName = toolPart.tool;
1153
1210
  const toolName = toWireToolName(rawToolName);
1154
1211
  state.toolNames.set(callID, { rawToolName, toolName });
1155
- if (isHostTool(toolName, rawToolName)) return;
1212
+ const hostToolName = getHostToolName(toolName, rawToolName);
1213
+ if (hostToolName) {
1214
+ if (status === 'running') {
1215
+ authorizeHostToolCall({
1216
+ callID,
1217
+ toolName: hostToolName,
1218
+ input: legacyToolPartInput(toolPart),
1219
+ state,
1220
+ });
1221
+ }
1222
+ return;
1223
+ }
1156
1224
  if (!state.toolCallsEmitted.has(callID)) {
1157
1225
  state.toolCallsEmitted.add(callID);
1158
1226
  emit({
@@ -1242,6 +1310,7 @@ async function handlePermissionV2({
1242
1310
  client,
1243
1311
  sessionId,
1244
1312
  permissionMode,
1313
+ builtinToolFiltering,
1245
1314
  turn,
1246
1315
  emit,
1247
1316
  event,
@@ -1249,6 +1318,7 @@ async function handlePermissionV2({
1249
1318
  client: OpenCodeClient;
1250
1319
  sessionId: string;
1251
1320
  permissionMode: StartMessage['permissionMode'];
1321
+ builtinToolFiltering: StartMessage['builtinToolFiltering'];
1252
1322
  turn: BridgeTurn;
1253
1323
  emit: Emit;
1254
1324
  event: OpenCodeEvent;
@@ -1269,6 +1339,7 @@ async function handlePermissionV2({
1269
1339
  ? String((props.source as { callID?: unknown }).callID)
1270
1340
  : requestID,
1271
1341
  permissionMode,
1342
+ builtinToolFiltering,
1272
1343
  turn,
1273
1344
  emit,
1274
1345
  });
@@ -1284,6 +1355,7 @@ async function handlePermission({
1284
1355
  client,
1285
1356
  sessionId,
1286
1357
  permissionMode,
1358
+ builtinToolFiltering,
1287
1359
  turn,
1288
1360
  emit,
1289
1361
  event,
@@ -1291,6 +1363,7 @@ async function handlePermission({
1291
1363
  client: OpenCodeClient;
1292
1364
  sessionId: string;
1293
1365
  permissionMode: StartMessage['permissionMode'];
1366
+ builtinToolFiltering: StartMessage['builtinToolFiltering'];
1294
1367
  turn: BridgeTurn;
1295
1368
  emit: Emit;
1296
1369
  event: OpenCodeEvent;
@@ -1309,6 +1382,7 @@ async function handlePermission({
1309
1382
  ? String((props.tool as { callID?: unknown }).callID)
1310
1383
  : requestID,
1311
1384
  permissionMode,
1385
+ builtinToolFiltering,
1312
1386
  turn,
1313
1387
  emit,
1314
1388
  });
@@ -1327,6 +1401,7 @@ async function selectPermissionReply({
1327
1401
  requestID,
1328
1402
  toolCallId,
1329
1403
  permissionMode,
1404
+ builtinToolFiltering,
1330
1405
  turn,
1331
1406
  emit,
1332
1407
  }: {
@@ -1335,6 +1410,7 @@ async function selectPermissionReply({
1335
1410
  requestID: string;
1336
1411
  toolCallId: string;
1337
1412
  permissionMode: StartMessage['permissionMode'];
1413
+ builtinToolFiltering: StartMessage['builtinToolFiltering'];
1338
1414
  turn: BridgeTurn;
1339
1415
  emit: Emit;
1340
1416
  }): Promise<{ reply: 'once' | 'always' | 'reject'; message?: string }> {
@@ -1342,6 +1418,22 @@ async function selectPermissionReply({
1342
1418
  if (resources.some(resource => isExternalPath(resource))) {
1343
1419
  return { reply: 'reject', message: 'External directory access rejected.' };
1344
1420
  }
1421
+ if (
1422
+ isBuiltinToolInactive({ toolName, toolFiltering: builtinToolFiltering })
1423
+ ) {
1424
+ emit({
1425
+ type: 'tool-approval-request',
1426
+ approvalId: requestID,
1427
+ toolCallId,
1428
+ });
1429
+ const decision = await turn.requestToolApproval(requestID);
1430
+ return decision.approved
1431
+ ? { reply: 'once' }
1432
+ : {
1433
+ reply: 'reject',
1434
+ ...(decision.reason ? { message: decision.reason } : {}),
1435
+ };
1436
+ }
1345
1437
  if (!permissionMode || permissionMode === 'allow-all') {
1346
1438
  return { reply: 'always' };
1347
1439
  }
@@ -1382,6 +1474,28 @@ function toPermissionToolName(action: string): string {
1382
1474
  return toWireToolName(normalized);
1383
1475
  }
1384
1476
 
1477
+ function resolveInactiveBuiltinToolNames(
1478
+ start: StartMessage,
1479
+ ): ReadonlyArray<string> {
1480
+ const toolFiltering = start.builtinToolFiltering;
1481
+ if (toolFiltering == null) return [];
1482
+ return toolFiltering.mode === 'allow'
1483
+ ? Object.keys(PUBLIC_TO_NATIVE).filter(
1484
+ name => !toolFiltering.toolNames.includes(name),
1485
+ )
1486
+ : toolFiltering.toolNames;
1487
+ }
1488
+
1489
+ function isBuiltinToolInactive(input: {
1490
+ toolName: string;
1491
+ toolFiltering: StartMessage['builtinToolFiltering'];
1492
+ }): boolean {
1493
+ if (input.toolFiltering == null) return false;
1494
+ return input.toolFiltering.mode === 'allow'
1495
+ ? !input.toolFiltering.toolNames.includes(input.toolName)
1496
+ : input.toolFiltering.toolNames.includes(input.toolName);
1497
+ }
1498
+
1385
1499
  function isExternalPath(resource: string): boolean {
1386
1500
  if (!path.isAbsolute(resource)) return false;
1387
1501
  const normalized = path.resolve(resource);
@@ -1413,19 +1527,38 @@ function nativeNameField({
1413
1527
  return { nativeName };
1414
1528
  }
1415
1529
 
1416
- function isHostTool(toolName: string, rawToolName: unknown): boolean {
1417
- if (runtime.toolNames.has(toolName)) return true;
1530
+ function getHostToolName(
1531
+ toolName: string,
1532
+ rawToolName: unknown,
1533
+ ): string | undefined {
1534
+ if (runtime.toolNames.has(toolName)) return toolName;
1418
1535
  if (typeof rawToolName === 'string' && runtime.toolNames.has(rawToolName)) {
1419
- return true;
1536
+ return rawToolName;
1420
1537
  }
1421
1538
  if (
1422
1539
  typeof rawToolName === 'string' &&
1423
1540
  rawToolName.startsWith('harness-tools_') &&
1424
1541
  runtime.toolNames.has(rawToolName.slice('harness-tools_'.length))
1425
1542
  ) {
1426
- return true;
1543
+ return rawToolName.slice('harness-tools_'.length);
1427
1544
  }
1428
- return false;
1545
+ return undefined;
1546
+ }
1547
+
1548
+ function authorizeHostToolCall({
1549
+ callID,
1550
+ toolName,
1551
+ input,
1552
+ state,
1553
+ }: {
1554
+ callID: string;
1555
+ toolName: string;
1556
+ input: unknown;
1557
+ state: TranslationState;
1558
+ }): void {
1559
+ if (state.hostToolCallsAuthorized.has(callID)) return;
1560
+ state.hostToolCallsAuthorized.add(callID);
1561
+ runtime.relay?.authorizeToolCall({ toolName, input });
1429
1562
  }
1430
1563
 
1431
1564
  async function emitContextFallback({
@@ -1601,26 +1734,24 @@ function mapFinishReason(
1601
1734
  }
1602
1735
 
1603
1736
  async function startToolRelay({
1604
- relayToken,
1737
+ allowedScriptPaths,
1605
1738
  tools,
1606
1739
  emit,
1607
1740
  requestToolResult,
1608
1741
  }: {
1609
- relayToken: string;
1742
+ allowedScriptPaths: ReadonlyArray<string>;
1610
1743
  tools: ReadonlyArray<{ name: string }>;
1611
1744
  emit: Emit;
1612
1745
  requestToolResult: (
1613
1746
  toolCallId: string,
1614
1747
  ) => Promise<{ output: unknown; isError?: boolean }>;
1615
- }): Promise<{ port: number; close(): void }> {
1748
+ }): Promise<ToolRelay> {
1616
1749
  const toolNames = new Set(tools.map(t => t.name));
1750
+ const allowedScriptPathSet = new Set(allowedScriptPaths);
1751
+ const authorizer = new ToolRelayAuthorizer();
1617
1752
  const server = createServer(async (req, res) => {
1618
1753
  try {
1619
- if (
1620
- req.method !== 'POST' ||
1621
- req.url !== '/' ||
1622
- req.headers.authorization !== `Bearer ${relayToken}`
1623
- ) {
1754
+ if (req.method !== 'POST' || req.url !== '/') {
1624
1755
  res.writeHead(401, { 'Content-Type': 'application/json' });
1625
1756
  res.end(JSON.stringify({ error: 'unauthorized tool relay request' }));
1626
1757
  return;
@@ -1643,6 +1774,17 @@ async function startToolRelay({
1643
1774
  );
1644
1775
  return;
1645
1776
  }
1777
+ const authorized =
1778
+ authorizer.consumeToolCall({ toolName, input }) ||
1779
+ (await isToolRelayRequestFromAllowedProcess({
1780
+ socket: req.socket,
1781
+ allowedScriptPaths: allowedScriptPathSet,
1782
+ }));
1783
+ if (!authorized) {
1784
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1785
+ res.end(JSON.stringify({ error: 'unauthorized tool relay request' }));
1786
+ return;
1787
+ }
1646
1788
 
1647
1789
  emit({
1648
1790
  type: 'tool-call',
@@ -1683,6 +1825,7 @@ async function startToolRelay({
1683
1825
  return {
1684
1826
  port: address.port,
1685
1827
  close: () => closeServer(server),
1828
+ authorizeToolCall: call => authorizer.authorizeToolCall(call),
1686
1829
  };
1687
1830
  }
1688
1831
 
@@ -0,0 +1,151 @@
1
+ import { readdir, readFile, readlink } from 'node:fs/promises';
2
+ import type { Socket } from 'node:net';
3
+
4
+ export type ToolRelayCall = {
5
+ toolName: string;
6
+ input: unknown;
7
+ };
8
+
9
+ export class ToolRelayAuthorizer {
10
+ private readonly ttlMs: number;
11
+ private readonly now: () => number;
12
+ private readonly authorizations: Array<{ key: string; expiresAt: number }> =
13
+ [];
14
+
15
+ constructor({
16
+ ttlMs = 10_000,
17
+ now = Date.now,
18
+ }: {
19
+ ttlMs?: number;
20
+ now?: () => number;
21
+ } = {}) {
22
+ this.ttlMs = ttlMs;
23
+ this.now = now;
24
+ }
25
+
26
+ authorizeToolCall(call: ToolRelayCall): void {
27
+ this.pruneExpired();
28
+ this.authorizations.push({
29
+ key: toolRelayCallKey(call),
30
+ expiresAt: this.now() + this.ttlMs,
31
+ });
32
+ }
33
+
34
+ consumeToolCall(call: ToolRelayCall): boolean {
35
+ this.pruneExpired();
36
+ 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;
41
+ }
42
+
43
+ private pruneExpired(): void {
44
+ const now = this.now();
45
+ for (let i = this.authorizations.length - 1; i >= 0; i--) {
46
+ if (this.authorizations[i].expiresAt <= now) {
47
+ this.authorizations.splice(i, 1);
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ function toolRelayCallKey({ toolName, input }: ToolRelayCall): string {
54
+ return `${toolName}\0${canonicalJson(input ?? {})}`;
55
+ }
56
+
57
+ function canonicalJson(value: unknown): string {
58
+ return JSON.stringify(normalizeJsonValue(value));
59
+ }
60
+
61
+ function normalizeJsonValue(value: unknown): unknown {
62
+ if (Array.isArray(value)) {
63
+ return value.map(normalizeJsonValue);
64
+ }
65
+ if (value && typeof value === 'object') {
66
+ return Object.fromEntries(
67
+ Object.entries(value as Record<string, unknown>)
68
+ .filter(([, entryValue]) => entryValue !== undefined)
69
+ .sort(([left], [right]) => left.localeCompare(right))
70
+ .map(([key, entryValue]) => [key, normalizeJsonValue(entryValue)]),
71
+ );
72
+ }
73
+ return value;
74
+ }
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
+ }