mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-06 12:03:07 +08:00
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:
@@ -135,7 +135,13 @@ describe("buildStartConversationRequest", () => {
|
|||||||
for (const skill of skills) {
|
for (const skill of skills) {
|
||||||
expect(skill).toHaveProperty("name");
|
expect(skill).toHaveProperty("name");
|
||||||
expect(skill).toHaveProperty("content");
|
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);
|
expect(skill).toHaveProperty("is_agentskills_format", true);
|
||||||
// trigger is either null (always-active) or { type, keywords }
|
// trigger is either null (always-active) or { type, keywords }
|
||||||
if (skill.trigger !== null) {
|
if (skill.trigger !== null) {
|
||||||
|
|||||||
@@ -544,7 +544,7 @@ interface BundledSkill {
|
|||||||
name: string;
|
name: string;
|
||||||
content: string;
|
content: string;
|
||||||
trigger: { type: "keyword"; keywords: string[] } | null;
|
trigger: { type: "keyword"; keywords: string[] } | null;
|
||||||
source: "public";
|
source: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
is_agentskills_format: true;
|
is_agentskills_format: true;
|
||||||
license?: string;
|
license?: string;
|
||||||
@@ -566,11 +566,18 @@ function buildBundledSkills(): BundledSkill[] {
|
|||||||
? { type: "keyword", keywords: entry.triggers }
|
? { type: "keyword", keywords: entry.triggers }
|
||||||
: null;
|
: 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 {
|
return {
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
content: entry.content,
|
content: entry.content,
|
||||||
trigger,
|
trigger,
|
||||||
source: "public" as const,
|
source,
|
||||||
description: entry.description ?? null,
|
description: entry.description ?? null,
|
||||||
is_agentskills_format: true as const,
|
is_agentskills_format: true as const,
|
||||||
...(entry.license ? { license: entry.license } : {}),
|
...(entry.license ? { license: entry.license } : {}),
|
||||||
|
|||||||
Vendored
+5
@@ -1,5 +1,10 @@
|
|||||||
/// <reference types="react-scripts" />
|
/// <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 {
|
interface Window {
|
||||||
posthog?: {
|
posthog?: {
|
||||||
capture: (event: string, properties?: Record<string, unknown>) => void;
|
capture: (event: string, properties?: Record<string, unknown>) => void;
|
||||||
|
|||||||
Vendored
+7
@@ -1 +1,8 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <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;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
/// <reference types="vitest" />
|
/// <reference types="vitest" />
|
||||||
/// <reference types="vite-plugin-svgr/client" />
|
/// <reference types="vite-plugin-svgr/client" />
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { createRequire } from "node:module";
|
||||||
|
import { resolve, dirname } from "node:path";
|
||||||
import { defineConfig, loadEnv } from "vite";
|
import { defineConfig, loadEnv } from "vite";
|
||||||
import svgr from "vite-plugin-svgr";
|
import svgr from "vite-plugin-svgr";
|
||||||
import { reactRouter } from "@react-router/dev/vite";
|
import { reactRouter } from "@react-router/dev/vite";
|
||||||
@@ -22,6 +24,16 @@ const LIB_EXTERNALS = [
|
|||||||
];
|
];
|
||||||
const APP_CHUNK_MAX_BYTES = 450 * 1024;
|
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 = {
|
const appBuildConfig = {
|
||||||
rolldownOptions: {
|
rolldownOptions: {
|
||||||
output: {
|
output: {
|
||||||
@@ -59,6 +71,14 @@ export default defineConfig(({ mode }) => {
|
|||||||
const FE_PORT = Number.parseInt(VITE_FRONTEND_PORT, 10);
|
const FE_PORT = Number.parseInt(VITE_FRONTEND_PORT, 10);
|
||||||
|
|
||||||
return {
|
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: [
|
plugins: [
|
||||||
{
|
{
|
||||||
name: "suppress-chrome-devtools-well-known",
|
name: "suppress-chrome-devtools-well-known",
|
||||||
|
|||||||
Reference in New Issue
Block a user