@ai-sdk/harness-opencode 1.0.11 → 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.
- package/CHANGELOG.md +10 -0
- package/dist/bridge/index.mjs +60 -0
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.js +20 -78
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bridge/index.ts +90 -2
- package/src/opencode-harness.ts +19 -85
package/src/bridge/index.ts
CHANGED
|
@@ -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';
|
|
@@ -56,7 +55,15 @@ type RuntimeState = {
|
|
|
56
55
|
toolNames: Set<string>;
|
|
57
56
|
};
|
|
58
57
|
|
|
59
|
-
|
|
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>> = {
|
|
60
67
|
view: 'read',
|
|
61
68
|
read: 'read',
|
|
62
69
|
write: 'write',
|
|
@@ -75,6 +82,20 @@ const OPENCODE_TO_WIRE: Readonly<Record<string, string>> = {
|
|
|
75
82
|
subtask: 'agent',
|
|
76
83
|
};
|
|
77
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
|
+
|
|
78
99
|
const TOOL_KIND: Readonly<Record<string, 'readonly' | 'edit' | 'bash'>> = {
|
|
79
100
|
read: 'readonly',
|
|
80
101
|
glob: 'readonly',
|
|
@@ -194,6 +215,18 @@ function buildOpenCodeConfig({
|
|
|
194
215
|
};
|
|
195
216
|
if (start.model) config.model = start.model;
|
|
196
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
|
+
}
|
|
197
230
|
const provider = buildProviderConfig(start);
|
|
198
231
|
if (provider) config.provider = provider;
|
|
199
232
|
if (relayPort && start.tools && start.tools.length > 0) {
|
|
@@ -502,6 +535,7 @@ async function runPrompt({
|
|
|
502
535
|
client,
|
|
503
536
|
sessionId,
|
|
504
537
|
permissionMode: start.permissionMode,
|
|
538
|
+
builtinToolFiltering: start.builtinToolFiltering,
|
|
505
539
|
turn,
|
|
506
540
|
emit: msg => {
|
|
507
541
|
if (msg.type === 'text-delta' || msg.type === 'reasoning-delta') {
|
|
@@ -624,6 +658,7 @@ async function runCompaction({
|
|
|
624
658
|
client,
|
|
625
659
|
sessionId,
|
|
626
660
|
permissionMode: start.permissionMode,
|
|
661
|
+
builtinToolFiltering: start.builtinToolFiltering,
|
|
627
662
|
turn,
|
|
628
663
|
emit: msg => {
|
|
629
664
|
if (msg.type === 'compaction') sawCompaction = true;
|
|
@@ -688,6 +723,7 @@ async function consumeEvents({
|
|
|
688
723
|
client,
|
|
689
724
|
sessionId,
|
|
690
725
|
permissionMode,
|
|
726
|
+
builtinToolFiltering,
|
|
691
727
|
turn,
|
|
692
728
|
emit,
|
|
693
729
|
signal,
|
|
@@ -696,6 +732,7 @@ async function consumeEvents({
|
|
|
696
732
|
client: OpenCodeClient;
|
|
697
733
|
sessionId: string;
|
|
698
734
|
permissionMode: StartMessage['permissionMode'];
|
|
735
|
+
builtinToolFiltering: StartMessage['builtinToolFiltering'];
|
|
699
736
|
turn: BridgeTurn;
|
|
700
737
|
emit: Emit;
|
|
701
738
|
signal: AbortSignal;
|
|
@@ -714,6 +751,7 @@ async function consumeEvents({
|
|
|
714
751
|
state,
|
|
715
752
|
sessionId,
|
|
716
753
|
permissionMode,
|
|
754
|
+
builtinToolFiltering,
|
|
717
755
|
client,
|
|
718
756
|
turn,
|
|
719
757
|
emit,
|
|
@@ -759,6 +797,7 @@ async function translateAndEmit({
|
|
|
759
797
|
state,
|
|
760
798
|
sessionId,
|
|
761
799
|
permissionMode,
|
|
800
|
+
builtinToolFiltering,
|
|
762
801
|
client,
|
|
763
802
|
turn,
|
|
764
803
|
emit,
|
|
@@ -767,6 +806,7 @@ async function translateAndEmit({
|
|
|
767
806
|
state: TranslationState;
|
|
768
807
|
sessionId: string;
|
|
769
808
|
permissionMode: StartMessage['permissionMode'];
|
|
809
|
+
builtinToolFiltering: StartMessage['builtinToolFiltering'];
|
|
770
810
|
client: OpenCodeClient;
|
|
771
811
|
turn: BridgeTurn;
|
|
772
812
|
emit: Emit;
|
|
@@ -1022,6 +1062,7 @@ async function translateAndEmit({
|
|
|
1022
1062
|
client,
|
|
1023
1063
|
sessionId,
|
|
1024
1064
|
permissionMode,
|
|
1065
|
+
builtinToolFiltering,
|
|
1025
1066
|
turn,
|
|
1026
1067
|
emit,
|
|
1027
1068
|
event,
|
|
@@ -1033,6 +1074,7 @@ async function translateAndEmit({
|
|
|
1033
1074
|
client,
|
|
1034
1075
|
sessionId,
|
|
1035
1076
|
permissionMode,
|
|
1077
|
+
builtinToolFiltering,
|
|
1036
1078
|
turn,
|
|
1037
1079
|
emit,
|
|
1038
1080
|
event,
|
|
@@ -1268,6 +1310,7 @@ async function handlePermissionV2({
|
|
|
1268
1310
|
client,
|
|
1269
1311
|
sessionId,
|
|
1270
1312
|
permissionMode,
|
|
1313
|
+
builtinToolFiltering,
|
|
1271
1314
|
turn,
|
|
1272
1315
|
emit,
|
|
1273
1316
|
event,
|
|
@@ -1275,6 +1318,7 @@ async function handlePermissionV2({
|
|
|
1275
1318
|
client: OpenCodeClient;
|
|
1276
1319
|
sessionId: string;
|
|
1277
1320
|
permissionMode: StartMessage['permissionMode'];
|
|
1321
|
+
builtinToolFiltering: StartMessage['builtinToolFiltering'];
|
|
1278
1322
|
turn: BridgeTurn;
|
|
1279
1323
|
emit: Emit;
|
|
1280
1324
|
event: OpenCodeEvent;
|
|
@@ -1295,6 +1339,7 @@ async function handlePermissionV2({
|
|
|
1295
1339
|
? String((props.source as { callID?: unknown }).callID)
|
|
1296
1340
|
: requestID,
|
|
1297
1341
|
permissionMode,
|
|
1342
|
+
builtinToolFiltering,
|
|
1298
1343
|
turn,
|
|
1299
1344
|
emit,
|
|
1300
1345
|
});
|
|
@@ -1310,6 +1355,7 @@ async function handlePermission({
|
|
|
1310
1355
|
client,
|
|
1311
1356
|
sessionId,
|
|
1312
1357
|
permissionMode,
|
|
1358
|
+
builtinToolFiltering,
|
|
1313
1359
|
turn,
|
|
1314
1360
|
emit,
|
|
1315
1361
|
event,
|
|
@@ -1317,6 +1363,7 @@ async function handlePermission({
|
|
|
1317
1363
|
client: OpenCodeClient;
|
|
1318
1364
|
sessionId: string;
|
|
1319
1365
|
permissionMode: StartMessage['permissionMode'];
|
|
1366
|
+
builtinToolFiltering: StartMessage['builtinToolFiltering'];
|
|
1320
1367
|
turn: BridgeTurn;
|
|
1321
1368
|
emit: Emit;
|
|
1322
1369
|
event: OpenCodeEvent;
|
|
@@ -1335,6 +1382,7 @@ async function handlePermission({
|
|
|
1335
1382
|
? String((props.tool as { callID?: unknown }).callID)
|
|
1336
1383
|
: requestID,
|
|
1337
1384
|
permissionMode,
|
|
1385
|
+
builtinToolFiltering,
|
|
1338
1386
|
turn,
|
|
1339
1387
|
emit,
|
|
1340
1388
|
});
|
|
@@ -1353,6 +1401,7 @@ async function selectPermissionReply({
|
|
|
1353
1401
|
requestID,
|
|
1354
1402
|
toolCallId,
|
|
1355
1403
|
permissionMode,
|
|
1404
|
+
builtinToolFiltering,
|
|
1356
1405
|
turn,
|
|
1357
1406
|
emit,
|
|
1358
1407
|
}: {
|
|
@@ -1361,6 +1410,7 @@ async function selectPermissionReply({
|
|
|
1361
1410
|
requestID: string;
|
|
1362
1411
|
toolCallId: string;
|
|
1363
1412
|
permissionMode: StartMessage['permissionMode'];
|
|
1413
|
+
builtinToolFiltering: StartMessage['builtinToolFiltering'];
|
|
1364
1414
|
turn: BridgeTurn;
|
|
1365
1415
|
emit: Emit;
|
|
1366
1416
|
}): Promise<{ reply: 'once' | 'always' | 'reject'; message?: string }> {
|
|
@@ -1368,6 +1418,22 @@ async function selectPermissionReply({
|
|
|
1368
1418
|
if (resources.some(resource => isExternalPath(resource))) {
|
|
1369
1419
|
return { reply: 'reject', message: 'External directory access rejected.' };
|
|
1370
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
|
+
}
|
|
1371
1437
|
if (!permissionMode || permissionMode === 'allow-all') {
|
|
1372
1438
|
return { reply: 'always' };
|
|
1373
1439
|
}
|
|
@@ -1408,6 +1474,28 @@ function toPermissionToolName(action: string): string {
|
|
|
1408
1474
|
return toWireToolName(normalized);
|
|
1409
1475
|
}
|
|
1410
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
|
+
|
|
1411
1499
|
function isExternalPath(resource: string): boolean {
|
|
1412
1500
|
if (!path.isAbsolute(resource)) return false;
|
|
1413
1501
|
const normalized = path.resolve(resource);
|
package/src/opencode-harness.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type HarnessV1,
|
|
10
10
|
type HarnessV1Bootstrap,
|
|
11
11
|
type HarnessV1BuiltinTool,
|
|
12
|
+
type HarnessV1BuiltinToolFiltering,
|
|
12
13
|
type HarnessV1ContinueTurnState,
|
|
13
14
|
type HarnessV1DebugConfig,
|
|
14
15
|
type HarnessV1NetworkSandboxSession,
|
|
@@ -23,8 +24,11 @@ import {
|
|
|
23
24
|
import {
|
|
24
25
|
classifyDiskLog,
|
|
25
26
|
markBridgeStarting,
|
|
27
|
+
resolveSandboxHomeDir,
|
|
26
28
|
SandboxChannel,
|
|
29
|
+
shellQuote,
|
|
27
30
|
waitForBridgeReady,
|
|
31
|
+
writeSkills as writeHarnessSkills,
|
|
28
32
|
} from '@ai-sdk/harness/utils';
|
|
29
33
|
import {
|
|
30
34
|
tool,
|
|
@@ -295,6 +299,7 @@ export function createOpenCode(
|
|
|
295
299
|
sandboxId,
|
|
296
300
|
debug: startOpts.observability?.debug,
|
|
297
301
|
permissionMode: startOpts.permissionMode,
|
|
302
|
+
builtinToolFiltering: startOpts.builtinToolFiltering,
|
|
298
303
|
});
|
|
299
304
|
} catch {}
|
|
300
305
|
}
|
|
@@ -326,7 +331,7 @@ export function createOpenCode(
|
|
|
326
331
|
const xdgStateHome = `${sandboxHomeDir}/.local/state`;
|
|
327
332
|
const skillSetup =
|
|
328
333
|
startOpts.skills && startOpts.skills.length > 0
|
|
329
|
-
? await
|
|
334
|
+
? await writeOpenCodeSkills({
|
|
330
335
|
sandbox: session,
|
|
331
336
|
skills: startOpts.skills,
|
|
332
337
|
homeDir: sandboxHomeDir,
|
|
@@ -421,6 +426,7 @@ export function createOpenCode(
|
|
|
421
426
|
sandboxId,
|
|
422
427
|
debug: startOpts.observability?.debug,
|
|
423
428
|
permissionMode: startOpts.permissionMode,
|
|
429
|
+
builtinToolFiltering: startOpts.builtinToolFiltering,
|
|
424
430
|
});
|
|
425
431
|
},
|
|
426
432
|
};
|
|
@@ -458,7 +464,7 @@ async function readBridgeAsset(name: string): Promise<string> {
|
|
|
458
464
|
throw lastErr ?? new Error(`bridge asset not found: ${name}`);
|
|
459
465
|
}
|
|
460
466
|
|
|
461
|
-
async function
|
|
467
|
+
async function writeOpenCodeSkills({
|
|
462
468
|
sandbox,
|
|
463
469
|
skills,
|
|
464
470
|
homeDir,
|
|
@@ -469,96 +475,21 @@ async function writeSkills({
|
|
|
469
475
|
homeDir: string;
|
|
470
476
|
abortSignal?: AbortSignal;
|
|
471
477
|
}): Promise<WriteSkillsResult> {
|
|
472
|
-
for (const skill of skills) {
|
|
473
|
-
safeOpenCodeSkillName(skill.name);
|
|
474
|
-
for (const file of skill.files ?? []) {
|
|
475
|
-
safeOpenCodeSkillFilePath({ skillName: skill.name, filePath: file.path });
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
478
|
const skillsDir = path.posix.join(homeDir, '.agents', 'skills');
|
|
480
|
-
await
|
|
481
|
-
|
|
479
|
+
await writeHarnessSkills({
|
|
480
|
+
sandbox,
|
|
481
|
+
rootDir: skillsDir,
|
|
482
|
+
skills,
|
|
482
483
|
abortSignal,
|
|
484
|
+
invalidSkillNameMessage: ({ name }) =>
|
|
485
|
+
`Invalid OpenCode skill name: ${name}`,
|
|
486
|
+
invalidSkillFilePathMessage: ({ skillName, filePath }) =>
|
|
487
|
+
`Invalid OpenCode skill file path for ${skillName}: ${filePath}`,
|
|
483
488
|
});
|
|
484
489
|
|
|
485
|
-
for (const skill of skills) {
|
|
486
|
-
const name = safeOpenCodeSkillName(skill.name);
|
|
487
|
-
const skillDir = path.posix.join(skillsDir, name);
|
|
488
|
-
const content = `---\nname: ${skill.name}\ndescription: ${skill.description}\n---\n\n${skill.content}`;
|
|
489
|
-
await sandbox.writeTextFile({
|
|
490
|
-
path: path.posix.join(skillDir, 'SKILL.md'),
|
|
491
|
-
content,
|
|
492
|
-
abortSignal,
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
for (const file of skill.files ?? []) {
|
|
496
|
-
const filePath = safeOpenCodeSkillFilePath({
|
|
497
|
-
skillName: skill.name,
|
|
498
|
-
filePath: file.path,
|
|
499
|
-
});
|
|
500
|
-
await sandbox.writeTextFile({
|
|
501
|
-
path: path.posix.join(skillDir, filePath),
|
|
502
|
-
content: file.content,
|
|
503
|
-
abortSignal,
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
490
|
return { skillsDir };
|
|
509
491
|
}
|
|
510
492
|
|
|
511
|
-
async function resolveSandboxHomeDir({
|
|
512
|
-
sandbox,
|
|
513
|
-
abortSignal,
|
|
514
|
-
}: {
|
|
515
|
-
sandbox: Experimental_SandboxSession;
|
|
516
|
-
abortSignal?: AbortSignal;
|
|
517
|
-
}): Promise<string> {
|
|
518
|
-
const result = await sandbox.run({
|
|
519
|
-
command: 'printf "%s" "$HOME"',
|
|
520
|
-
abortSignal,
|
|
521
|
-
});
|
|
522
|
-
const homeDir = result.stdout.trim();
|
|
523
|
-
if (result.exitCode !== 0 || !homeDir || !path.posix.isAbsolute(homeDir)) {
|
|
524
|
-
throw new Error(
|
|
525
|
-
`Unable to resolve sandbox HOME directory: ${result.stderr || result.stdout}`,
|
|
526
|
-
);
|
|
527
|
-
}
|
|
528
|
-
return homeDir;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function safeOpenCodeSkillName(name: string): string {
|
|
532
|
-
if (!/^[A-Za-z0-9._-]+$/.test(name) || name === '.' || name === '..') {
|
|
533
|
-
throw new Error(`Invalid OpenCode skill name: ${name}`);
|
|
534
|
-
}
|
|
535
|
-
return name;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
function safeOpenCodeSkillFilePath({
|
|
539
|
-
skillName,
|
|
540
|
-
filePath,
|
|
541
|
-
}: {
|
|
542
|
-
skillName: string;
|
|
543
|
-
filePath: string;
|
|
544
|
-
}): string {
|
|
545
|
-
const normalized = path.posix.normalize(filePath);
|
|
546
|
-
if (
|
|
547
|
-
normalized === '.' ||
|
|
548
|
-
normalized.startsWith('../') ||
|
|
549
|
-
path.posix.isAbsolute(normalized)
|
|
550
|
-
) {
|
|
551
|
-
throw new Error(
|
|
552
|
-
`Invalid OpenCode skill file path for ${skillName}: ${filePath}`,
|
|
553
|
-
);
|
|
554
|
-
}
|
|
555
|
-
return normalized;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
function shellQuote(value: string): string {
|
|
559
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
493
|
async function forwardBridgeStderr(
|
|
563
494
|
stream: ReadableStream<Uint8Array>,
|
|
564
495
|
): Promise<void> {
|
|
@@ -619,6 +550,7 @@ function createSession({
|
|
|
619
550
|
sandboxId,
|
|
620
551
|
debug,
|
|
621
552
|
permissionMode,
|
|
553
|
+
builtinToolFiltering,
|
|
622
554
|
}: {
|
|
623
555
|
sessionId: string;
|
|
624
556
|
channel: OpenCodeChannel;
|
|
@@ -635,6 +567,7 @@ function createSession({
|
|
|
635
567
|
sandboxId: string;
|
|
636
568
|
debug: HarnessV1DebugConfig | undefined;
|
|
637
569
|
permissionMode: HarnessV1PermissionMode | undefined;
|
|
570
|
+
builtinToolFiltering: HarnessV1BuiltinToolFiltering | undefined;
|
|
638
571
|
}): HarnessV1Session {
|
|
639
572
|
let stopped = false;
|
|
640
573
|
let stopPromise: Promise<void> | undefined;
|
|
@@ -786,6 +719,7 @@ function createSession({
|
|
|
786
719
|
provider,
|
|
787
720
|
...(reasoningVariant ? { variant: reasoningVariant } : {}),
|
|
788
721
|
...(permissionMode ? { permissionMode } : {}),
|
|
722
|
+
...(builtinToolFiltering ? { builtinToolFiltering } : {}),
|
|
789
723
|
...(pendingResumeSessionId
|
|
790
724
|
? { resumeSessionId: pendingResumeSessionId }
|
|
791
725
|
: latestOpenCodeSessionId
|