@ai-sdk/harness-claude-code 1.0.0-canary.2 → 1.0.0-canary.3

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.
@@ -1,5 +1,6 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import {
5
6
  commonTool,
@@ -506,6 +507,7 @@ export function createClaudeCode(
506
507
  sandboxId,
507
508
  debug: startOpts.observability?.debug,
508
509
  permissionMode: startOpts.permissionMode,
510
+ skills: startOpts.skills ?? [],
509
511
  });
510
512
  } catch {
511
513
  // Bridge no longer reachable — recover by respawning below.
@@ -536,12 +538,20 @@ export function createClaudeCode(
536
538
  }
537
539
  }
538
540
 
541
+ const sandboxHomeDir =
542
+ startOpts.skills && startOpts.skills.length > 0
543
+ ? await resolveSandboxHomeDir({
544
+ sandbox: session,
545
+ abortSignal: startOpts.abortSignal,
546
+ })
547
+ : undefined;
539
548
  const port = resolveBridgePort(sandboxSession, settings.port);
540
549
  const token = randomBytes(32).toString('hex');
541
550
  const env = {
542
551
  ...resolveClaudeCodeEnv(settings.auth),
543
552
  BRIDGE_CHANNEL_TOKEN: token,
544
553
  BRIDGE_WS_PORT: String(port),
554
+ ...(sandboxHomeDir ? { HOME: sandboxHomeDir } : {}),
545
555
  ...(respawnStrategy === 'replay'
546
556
  ? { BRIDGE_REPLAY_FROM_DISK: '1' }
547
557
  : {}),
@@ -560,9 +570,12 @@ export function createClaudeCode(
560
570
  });
561
571
 
562
572
  if (startOpts.skills && startOpts.skills.length > 0) {
573
+ if (!sandboxHomeDir) {
574
+ throw new Error('Unable to resolve sandbox HOME directory.');
575
+ }
563
576
  await writeSkills({
564
577
  sandbox: session,
565
- workdir: workDir,
578
+ homeDir: sandboxHomeDir,
566
579
  skills: startOpts.skills,
567
580
  abortSignal: startOpts.abortSignal,
568
581
  });
@@ -632,6 +645,7 @@ export function createClaudeCode(
632
645
  sandboxId,
633
646
  debug: startOpts.observability?.debug,
634
647
  permissionMode: startOpts.permissionMode,
648
+ skills: startOpts.skills ?? [],
635
649
  });
636
650
  },
637
651
  };
@@ -652,33 +666,108 @@ function resolveBridgePort(
652
666
  }
653
667
 
654
668
  /**
655
- * Materialise skill files into `${workdir}/.claude/skills/<name>.md`. The
656
- * `claude` CLI auto-discovers skills from that directory on startup, so the
657
- * files have to be in place before the bridge is spawned. Each file uses
658
- * the YAML-frontmatter shape the CLI expects.
669
+ * Materialise skill files into
670
+ * `$HOME/.claude/skills/<name>/SKILL.md`. The `claude` CLI
671
+ * auto-discovers skills from that directory on startup, so the files have to
672
+ * be in place before the bridge is spawned without mutating the session
673
+ * workdir. Each file uses the YAML-frontmatter shape the CLI expects.
659
674
  */
660
675
  async function writeSkills({
661
676
  sandbox,
662
- workdir,
677
+ homeDir,
663
678
  skills,
664
679
  abortSignal,
665
680
  }: {
666
681
  sandbox: Experimental_SandboxSession;
667
- workdir: string;
682
+ homeDir: string;
668
683
  skills: ReadonlyArray<HarnessV1Skill>;
669
684
  abortSignal?: AbortSignal;
670
685
  }): Promise<void> {
686
+ for (const skill of skills) {
687
+ safeClaudeSkillName(skill.name);
688
+ for (const file of skill.files ?? []) {
689
+ safeClaudeSkillFilePath({
690
+ skillName: skill.name,
691
+ filePath: file.path,
692
+ });
693
+ }
694
+ }
695
+
671
696
  await sandbox.run({
672
- command: `mkdir -p ${workdir}/.claude/skills`,
697
+ command: `mkdir -p ${shellQuote(homeDir)}/.claude/skills`,
673
698
  abortSignal,
674
699
  });
675
700
  for (const skill of skills) {
676
- const path = `${workdir}/.claude/skills/${skill.name}.md`;
701
+ const name = safeClaudeSkillName(skill.name);
702
+ const skillDir = `${homeDir}/.claude/skills/${name}`;
703
+ const path = `${skillDir}/SKILL.md`;
677
704
  const content = `---\nname: ${skill.name}\ndescription: ${skill.description}\n---\n\n${skill.content}\n`;
678
705
  await sandbox.writeTextFile({ path, content, abortSignal });
706
+ for (const file of skill.files ?? []) {
707
+ const filePath = safeClaudeSkillFilePath({
708
+ skillName: skill.name,
709
+ filePath: file.path,
710
+ });
711
+ await sandbox.writeTextFile({
712
+ path: `${skillDir}/${filePath}`,
713
+ content: file.content,
714
+ abortSignal,
715
+ });
716
+ }
679
717
  }
680
718
  }
681
719
 
720
+ async function resolveSandboxHomeDir({
721
+ sandbox,
722
+ abortSignal,
723
+ }: {
724
+ sandbox: Experimental_SandboxSession;
725
+ abortSignal?: AbortSignal;
726
+ }): Promise<string> {
727
+ const result = await sandbox.run({
728
+ command: 'printf "%s" "$HOME"',
729
+ abortSignal,
730
+ });
731
+ const homeDir = result.stdout.trim();
732
+ if (result.exitCode !== 0 || !homeDir || !path.posix.isAbsolute(homeDir)) {
733
+ throw new Error(
734
+ `Unable to resolve sandbox HOME directory: ${result.stderr || result.stdout}`,
735
+ );
736
+ }
737
+ return homeDir;
738
+ }
739
+
740
+ function safeClaudeSkillName(name: string): string {
741
+ if (!/^[A-Za-z0-9._-]+$/.test(name) || name === '.' || name === '..') {
742
+ throw new Error(`Invalid Claude Code skill name: ${name}`);
743
+ }
744
+ return name;
745
+ }
746
+
747
+ function safeClaudeSkillFilePath({
748
+ skillName,
749
+ filePath,
750
+ }: {
751
+ skillName: string;
752
+ filePath: string;
753
+ }): string {
754
+ const normalized = path.posix.normalize(filePath);
755
+ if (
756
+ normalized === '.' ||
757
+ normalized.startsWith('../') ||
758
+ path.posix.isAbsolute(normalized)
759
+ ) {
760
+ throw new Error(
761
+ `Invalid Claude Code skill file path for ${skillName}: ${filePath}`,
762
+ );
763
+ }
764
+ return normalized;
765
+ }
766
+
767
+ function shellQuote(value: string): string {
768
+ return `'${value.replace(/'/g, `'\\''`)}'`;
769
+ }
770
+
682
771
  async function readBridgeAsset(name: string): Promise<string> {
683
772
  const candidates = [
684
773
  new URL(`./bridge/${name}`, import.meta.url),
@@ -1063,6 +1152,7 @@ function createSession({
1063
1152
  sandboxId,
1064
1153
  debug,
1065
1154
  permissionMode,
1155
+ skills,
1066
1156
  }: {
1067
1157
  sessionId: string;
1068
1158
  channel: ClaudeCodeChannel;
@@ -1079,6 +1169,7 @@ function createSession({
1079
1169
  sandboxId: string;
1080
1170
  debug: HarnessV1DebugConfig | undefined;
1081
1171
  permissionMode: HarnessV1PermissionMode | undefined;
1172
+ skills: ReadonlyArray<HarnessV1Skill>;
1082
1173
  }): HarnessV1Session {
1083
1174
  let stopped = false;
1084
1175
  let stopPromise: Promise<void> | undefined;
@@ -1259,6 +1350,9 @@ function createSession({
1259
1350
  model,
1260
1351
  maxTurns,
1261
1352
  thinking,
1353
+ ...(skills.length > 0
1354
+ ? { skills: skills.map(skill => skill.name) }
1355
+ : {}),
1262
1356
  ...(permissionMode ? { permissionMode } : {}),
1263
1357
  ...(debug ? { debug } : {}),
1264
1358
  ...(pendingResumeFlag ? { continue: true } : {}),
@@ -1307,6 +1401,9 @@ function createSession({
1307
1401
  model,
1308
1402
  maxTurns,
1309
1403
  thinking,
1404
+ ...(skills.length > 0
1405
+ ? { skills: skills.map(skill => skill.name) }
1406
+ : {}),
1310
1407
  ...(permissionMode ? { permissionMode } : {}),
1311
1408
  ...(debug ? { debug } : {}),
1312
1409
  continue: true,