@ai-sdk/harness-opencode 1.0.118 → 1.0.120
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 +21 -0
- package/dist/bridge/index.mjs +22 -7
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.js +38 -5
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/bridge/index.ts +31 -8
- package/src/opencode-harness.ts +49 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness-opencode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.120",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@ai-sdk/harness": "1.0.
|
|
30
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
29
|
+
"@ai-sdk/harness": "1.0.118",
|
|
30
|
+
"@ai-sdk/provider-utils": "5.0.45",
|
|
31
31
|
"ws": "8.21.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
package/src/bridge/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type BridgeTurn,
|
|
5
5
|
} from '@ai-sdk/harness/bridge';
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { realpathSync } from 'node:fs';
|
|
7
8
|
import path from 'node:path';
|
|
8
9
|
import { argv, env as procEnv } from 'node:process';
|
|
9
10
|
import { isDeepStrictEqual } from 'node:util';
|
|
@@ -1304,9 +1305,6 @@ async function selectPermissionReply({
|
|
|
1304
1305
|
emit: Emit;
|
|
1305
1306
|
}): Promise<{ reply: 'once' | 'always' | 'reject'; message?: string }> {
|
|
1306
1307
|
const toolName = toPermissionToolName(action);
|
|
1307
|
-
if (resources.some(resource => isExternalPath(resource))) {
|
|
1308
|
-
return { reply: 'reject', message: 'External directory access rejected.' };
|
|
1309
|
-
}
|
|
1310
1308
|
if (
|
|
1311
1309
|
isBuiltinToolInactive({ toolName, toolFiltering: builtinToolFiltering })
|
|
1312
1310
|
) {
|
|
@@ -1326,6 +1324,9 @@ async function selectPermissionReply({
|
|
|
1326
1324
|
if (!permissionMode || permissionMode === 'allow-all') {
|
|
1327
1325
|
return { reply: 'always' };
|
|
1328
1326
|
}
|
|
1327
|
+
if (resources.some(resource => isExternalPath(resource))) {
|
|
1328
|
+
return { reply: 'reject', message: 'External directory access rejected.' };
|
|
1329
|
+
}
|
|
1329
1330
|
const kind = TOOL_KIND[toolName] ?? 'bash';
|
|
1330
1331
|
const allowed =
|
|
1331
1332
|
permissionMode === 'allow-edits'
|
|
@@ -1387,16 +1388,38 @@ function isBuiltinToolInactive(input: {
|
|
|
1387
1388
|
|
|
1388
1389
|
function isExternalPath(resource: string): boolean {
|
|
1389
1390
|
if (!path.isAbsolute(resource)) return false;
|
|
1390
|
-
const normalized = path.resolve(resource);
|
|
1391
1391
|
return (
|
|
1392
|
-
!isPathInsideOrEqual(
|
|
1393
|
-
(!skillsDir || !isPathInsideOrEqual(
|
|
1392
|
+
!isPathInsideOrEqual(resource, workdir) &&
|
|
1393
|
+
(!skillsDir || !isPathInsideOrEqual(resource, skillsDir))
|
|
1394
1394
|
);
|
|
1395
1395
|
}
|
|
1396
1396
|
|
|
1397
1397
|
function isPathInsideOrEqual(file: string, root: string): boolean {
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1398
|
+
const relative = path.relative(
|
|
1399
|
+
canonicalizeForContainment(root),
|
|
1400
|
+
canonicalizeForContainment(file),
|
|
1401
|
+
);
|
|
1402
|
+
return (
|
|
1403
|
+
relative === '' ||
|
|
1404
|
+
(relative !== '..' &&
|
|
1405
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
1406
|
+
!path.isAbsolute(relative))
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function canonicalizeForContainment(inputPath: string): string {
|
|
1411
|
+
const normalized = path.resolve(inputPath);
|
|
1412
|
+
try {
|
|
1413
|
+
return realpathSync.native(normalized);
|
|
1414
|
+
} catch {
|
|
1415
|
+
const parent = path.dirname(normalized);
|
|
1416
|
+
return parent === normalized
|
|
1417
|
+
? normalized
|
|
1418
|
+
: path.join(
|
|
1419
|
+
canonicalizeForContainment(parent),
|
|
1420
|
+
path.basename(normalized),
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1400
1423
|
}
|
|
1401
1424
|
|
|
1402
1425
|
function toWireToolName(nativeName: string): string {
|
package/src/opencode-harness.ts
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
waitForBridgeReady,
|
|
44
44
|
withBridgeToken,
|
|
45
45
|
writeSkills,
|
|
46
|
+
type SandboxChannelReconnectOptions,
|
|
46
47
|
type WriteSkillsResult,
|
|
47
48
|
} from '@ai-sdk/harness/utils';
|
|
48
49
|
import {
|
|
@@ -120,6 +121,13 @@ export type OpenCodeHarnessSettings = {
|
|
|
120
121
|
*/
|
|
121
122
|
readonly portEndpoint?: HarnessV1PortEndpoint;
|
|
122
123
|
readonly startupTimeoutMs?: number;
|
|
124
|
+
/**
|
|
125
|
+
* Configures reconnection attempts after an established bridge connection
|
|
126
|
+
* drops. The reconnect window includes connection establishment and
|
|
127
|
+
* backoff delays. Defaults to 30 seconds with exponential backoff from 50
|
|
128
|
+
* milliseconds up to 2 seconds.
|
|
129
|
+
*/
|
|
130
|
+
readonly reconnect?: SandboxChannelReconnectOptions;
|
|
123
131
|
/**
|
|
124
132
|
* Creates the authentication token used by the sandbox bridge. Defaults to
|
|
125
133
|
* a random 32-byte hexadecimal token.
|
|
@@ -437,10 +445,11 @@ export function createOpenCode(
|
|
|
437
445
|
});
|
|
438
446
|
let supportsUserMessageResponses = false;
|
|
439
447
|
const attachChannel: OpenCodeChannel = new SandboxChannel({
|
|
440
|
-
connect: () =>
|
|
448
|
+
connect: ({ abortSignal }) =>
|
|
441
449
|
openWebSocket({
|
|
442
450
|
endpoint: attachEndpoint,
|
|
443
451
|
helloTimeoutMs: Math.min(timeoutMs, 5_000),
|
|
452
|
+
abortSignal,
|
|
444
453
|
onHello: supported => {
|
|
445
454
|
supportsUserMessageResponses = supported;
|
|
446
455
|
},
|
|
@@ -449,6 +458,7 @@ export function createOpenCode(
|
|
|
449
458
|
initialLastSeenEventId: coords.lastSeenEventId,
|
|
450
459
|
onDiagnostic,
|
|
451
460
|
onBridgeError,
|
|
461
|
+
reconnect: settings.reconnect,
|
|
452
462
|
});
|
|
453
463
|
await attachChannel.open(isContinue ? { resume: true } : undefined);
|
|
454
464
|
return createSession({
|
|
@@ -610,10 +620,11 @@ export function createOpenCode(
|
|
|
610
620
|
let supportsUserMessageResponses = false;
|
|
611
621
|
|
|
612
622
|
const channel: OpenCodeChannel = new SandboxChannel({
|
|
613
|
-
connect: () =>
|
|
623
|
+
connect: ({ abortSignal }) =>
|
|
614
624
|
openWebSocket({
|
|
615
625
|
endpoint: bridgeEndpoint,
|
|
616
626
|
helloTimeoutMs: Math.min(timeoutMs, 5_000),
|
|
627
|
+
abortSignal,
|
|
617
628
|
onHello: supported => {
|
|
618
629
|
supportsUserMessageResponses = supported;
|
|
619
630
|
},
|
|
@@ -621,6 +632,7 @@ export function createOpenCode(
|
|
|
621
632
|
outboundSchema: outboundMessageSchema,
|
|
622
633
|
onDiagnostic,
|
|
623
634
|
onBridgeError,
|
|
635
|
+
reconnect: settings.reconnect,
|
|
624
636
|
...(respawnStrategy === 'replay'
|
|
625
637
|
? { initialLastSeenEventId: coords?.lastSeenEventId ?? 0 }
|
|
626
638
|
: {}),
|
|
@@ -726,25 +738,39 @@ async function resolveBridgeEndpoint({
|
|
|
726
738
|
function openWebSocket({
|
|
727
739
|
endpoint,
|
|
728
740
|
helloTimeoutMs,
|
|
741
|
+
abortSignal,
|
|
729
742
|
onHello,
|
|
730
743
|
}: {
|
|
731
744
|
endpoint: HarnessV1PortEndpoint;
|
|
732
745
|
helloTimeoutMs: number;
|
|
746
|
+
abortSignal: AbortSignal;
|
|
733
747
|
onHello(supportsUserMessageResponses: boolean): void;
|
|
734
748
|
}): Promise<WebSocket> {
|
|
735
749
|
return new Promise((resolve, reject) => {
|
|
750
|
+
const abortReason = () =>
|
|
751
|
+
abortSignal.reason ?? new Error('WebSocket connection aborted');
|
|
752
|
+
if (abortSignal.aborted) {
|
|
753
|
+
reject(abortReason());
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
736
757
|
const ws = new WebSocket(endpoint.url, {
|
|
737
758
|
headers: endpoint.headers == null ? undefined : { ...endpoint.headers },
|
|
738
759
|
});
|
|
739
760
|
let opened = false;
|
|
740
761
|
let receivedHello = false;
|
|
741
762
|
let settled = false;
|
|
763
|
+
let helloTimer: ReturnType<typeof setTimeout> | undefined;
|
|
764
|
+
let onAbort: (() => void) | undefined;
|
|
742
765
|
const cleanup = () => {
|
|
743
766
|
clearTimeout(helloTimer);
|
|
744
767
|
ws.off('open', onOpen);
|
|
745
768
|
ws.off('message', onMessage);
|
|
746
769
|
ws.off('close', onClose);
|
|
747
770
|
ws.off('error', onError);
|
|
771
|
+
if (onAbort != null) {
|
|
772
|
+
abortSignal.removeEventListener('abort', onAbort);
|
|
773
|
+
}
|
|
748
774
|
};
|
|
749
775
|
const settle = (error?: unknown) => {
|
|
750
776
|
if (settled) return;
|
|
@@ -753,6 +779,20 @@ function openWebSocket({
|
|
|
753
779
|
if (error == null) {
|
|
754
780
|
resolve(ws);
|
|
755
781
|
} else {
|
|
782
|
+
const suppressError = () => {};
|
|
783
|
+
const socketWithOnce = ws as WebSocket & {
|
|
784
|
+
once?: (event: string, listener: () => void) => void;
|
|
785
|
+
};
|
|
786
|
+
socketWithOnce.once?.('error', suppressError);
|
|
787
|
+
try {
|
|
788
|
+
ws.terminate();
|
|
789
|
+
} catch {
|
|
790
|
+
try {
|
|
791
|
+
ws.close();
|
|
792
|
+
} catch {
|
|
793
|
+
// best-effort
|
|
794
|
+
}
|
|
795
|
+
}
|
|
756
796
|
reject(error);
|
|
757
797
|
}
|
|
758
798
|
};
|
|
@@ -793,11 +833,17 @@ function openWebSocket({
|
|
|
793
833
|
const onError = (err: Error) => {
|
|
794
834
|
settle(err);
|
|
795
835
|
};
|
|
836
|
+
onAbort = () => settle(abortReason());
|
|
796
837
|
ws.on('open', onOpen);
|
|
797
838
|
ws.on('message', onMessage);
|
|
798
839
|
ws.on('close', onClose);
|
|
799
840
|
ws.on('error', onError);
|
|
800
|
-
|
|
841
|
+
abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
842
|
+
if (abortSignal.aborted) {
|
|
843
|
+
onAbort();
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
helloTimer = setTimeout(
|
|
801
847
|
() =>
|
|
802
848
|
settle(
|
|
803
849
|
new Error(
|