fix: set bundled skill source to real filesystem path (#1310)

* 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 <openhands@all-hands.dev>

* 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 <openhands@all-hands.dev>

* test: assert skill source is absolute path ending in /<name>/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 '/<name>/SKILL.md' (points to the right file)

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Tim O'Farrell
2026-06-11 09:23:45 -06:00
committed by GitHub
parent 5076bb3517
commit bee6cee418
5 changed files with 48 additions and 3 deletions
+7 -1
View File
@@ -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) {
+9 -2
View File
@@ -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 } : {}),
+5
View File
@@ -1,5 +1,10 @@
/// <reference types="react-scripts" />
// 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<string, unknown>) => void;
+7
View File
@@ -1 +1,8 @@
/// <reference types="vite/client" />
/**
* 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;
+20
View File
@@ -1,6 +1,8 @@
/// <reference types="vitest" />
/// <reference types="vite-plugin-svgr/client" />
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",