From bee6cee4187d7ca7449afe7c4fa9e12e42ce1382 Mon Sep 17 00:00:00 2001 From: Tim O'Farrell Date: Thu, 11 Jun 2026 09:23:45 -0600 Subject: [PATCH] fix: set bundled skill source to real filesystem path (#1310) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: set bundled skill source to real filesystem path Bundled public skills were sent to the Python agent-server with `source: "public"`, which the SDK uses as the skill's `location` in SkillKnowledge. Any skill whose SKILL.md references bundled resources (scripts/, references/) is therefore unable to resolve those files — the agent sees 'Skill location: public' instead of an actual path. Fix: compute the absolute path to the skills directory inside the `@openhands/extensions` node_modules package at Vite build/serve time and inject it as `__EXTENSIONS_SKILLS_DIR__` via Vite's `define`. `buildBundledSkills()` now sets `source` to: `${__EXTENSIONS_SKILLS_DIR__}/${name}/SKILL.md` Library builds receive an empty string so consumers are not bound to this machine's node_modules path; the adapter falls back to 'public' when the value is falsy. Co-authored-by: openhands * fix: declare __EXTENSIONS_SKILLS_DIR__ global for TypeScript Add missing 'declare const __EXTENSIONS_SKILLS_DIR__: string' to src/react-app-env.d.ts so tsc can resolve the Vite-injected constant used in buildBundledSkills(). Co-authored-by: openhands * test: assert skill source is absolute path ending in //SKILL.md Replace the loose 'typeof === string' / toBeTruthy checks with two specific assertions that directly verify the intent of buildBundledSkills(): - source starts with '/' (absolute path the agent-server can resolve) - source ends with '//SKILL.md' (points to the right file) Co-authored-by: openhands --------- Co-authored-by: openhands --- __tests__/api/agent-server-adapter.test.ts | 8 +++++++- src/api/agent-server-adapter.ts | 11 +++++++++-- src/react-app-env.d.ts | 5 +++++ vite-env.d.ts | 7 +++++++ vite.config.ts | 20 ++++++++++++++++++++ 5 files changed, 48 insertions(+), 3 deletions(-) diff --git a/__tests__/api/agent-server-adapter.test.ts b/__tests__/api/agent-server-adapter.test.ts index cb12c1030c..1a00b19e40 100644 --- a/__tests__/api/agent-server-adapter.test.ts +++ b/__tests__/api/agent-server-adapter.test.ts @@ -135,7 +135,13 @@ describe("buildStartConversationRequest", () => { for (const skill of skills) { expect(skill).toHaveProperty("name"); expect(skill).toHaveProperty("content"); - expect(skill).toHaveProperty("source", "public"); + // source must be an absolute path to the skill's SKILL.md so the + // Python agent-server can resolve bundled resources (scripts/, references/). + const source = skill.source as string; + expect(source).toMatch(/^\//); + expect(source).toMatch( + new RegExp(`/${skill.name as string}/SKILL\\.md$`), + ); expect(skill).toHaveProperty("is_agentskills_format", true); // trigger is either null (always-active) or { type, keywords } if (skill.trigger !== null) { diff --git a/src/api/agent-server-adapter.ts b/src/api/agent-server-adapter.ts index bf18f81adf..ed79923ac2 100644 --- a/src/api/agent-server-adapter.ts +++ b/src/api/agent-server-adapter.ts @@ -544,7 +544,7 @@ interface BundledSkill { name: string; content: string; trigger: { type: "keyword"; keywords: string[] } | null; - source: "public"; + source: string; description: string | null; is_agentskills_format: true; license?: string; @@ -566,11 +566,18 @@ function buildBundledSkills(): BundledSkill[] { ? { type: "keyword", keywords: entry.triggers } : null; + // Use the absolute path to the skill's SKILL.md so the Python + // agent-server can resolve bundled resources (scripts/, references/). + // Falls back to "public" in library builds where the path isn't known. + const source = __EXTENSIONS_SKILLS_DIR__ + ? `${__EXTENSIONS_SKILLS_DIR__}/${entry.name}/SKILL.md` + : "public"; + return { name: entry.name, content: entry.content, trigger, - source: "public" as const, + source, description: entry.description ?? null, is_agentskills_format: true as const, ...(entry.license ? { license: entry.license } : {}), diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts index c9376e9c0f..e76f8116ff 100644 --- a/src/react-app-env.d.ts +++ b/src/react-app-env.d.ts @@ -1,5 +1,10 @@ /// +// Injected by vite.config.ts `define` — absolute path to the +// @openhands/extensions skills directory in node_modules, or an +// empty string in library builds. +declare const __EXTENSIONS_SKILLS_DIR__: string; + interface Window { posthog?: { capture: (event: string, properties?: Record) => void; diff --git a/vite-env.d.ts b/vite-env.d.ts index 11f02fe2a0..e77fd9f48c 100644 --- a/vite-env.d.ts +++ b/vite-env.d.ts @@ -1 +1,8 @@ /// + +/** + * Absolute filesystem path to the bundled extensions skills directory, + * injected by Vite at build time via `define` in vite.config.ts. + * Empty string in library builds; always a real path in app/dev builds. + */ +declare const __EXTENSIONS_SKILLS_DIR__: string; diff --git a/vite.config.ts b/vite.config.ts index df27b3e6ae..31d83e086f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,8 @@ /// /// import { fileURLToPath } from "node:url"; +import { createRequire } from "node:module"; +import { resolve, dirname } from "node:path"; import { defineConfig, loadEnv } from "vite"; import svgr from "vite-plugin-svgr"; import { reactRouter } from "@react-router/dev/vite"; @@ -22,6 +24,16 @@ const LIB_EXTERNALS = [ ]; const APP_CHUNK_MAX_BYTES = 450 * 1024; +// Absolute path to the bundled extensions skills directory in node_modules. +// Injected as __EXTENSIONS_SKILLS_DIR__ so agent-server-adapter.ts can pass +// real filesystem paths to the Python agent-server (which uses them to +// resolve bundled skill resources like scripts/ and references/). +const _require = createRequire(import.meta.url); +const EXTENSIONS_SKILLS_DIR = resolve( + dirname(_require.resolve("@openhands/extensions/package.json")), + "skills", +); + const appBuildConfig = { rolldownOptions: { output: { @@ -59,6 +71,14 @@ export default defineConfig(({ mode }) => { const FE_PORT = Number.parseInt(VITE_FRONTEND_PORT, 10); return { + define: { + // Empty string for library builds so consumers aren't bound to this + // machine's node_modules path; agent-server-adapter falls back to + // "public" when the value is falsy. + __EXTENSIONS_SKILLS_DIR__: JSON.stringify( + isLibraryBuild ? "" : EXTENSIONS_SKILLS_DIR, + ), + }, plugins: [ { name: "suppress-chrome-devtools-well-known",