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",