@ai-sdk/harness-opencode 1.0.11 → 1.0.13

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';
@@ -56,7 +55,15 @@ type RuntimeState = {
56
55
  toolNames: Set<string>;
57
56
  };
58
57
 
59
- 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>> = {
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);
@@ -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 writeSkills({
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 writeSkills({
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 sandbox.run({
481
- command: `mkdir -p ${shellQuote(skillsDir)}`,
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