Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions backend/app/core/harness_capability_invoker.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
new_id,
utc_now,
)
from app.general_skills.package_materialization import (
materialize_general_skill_package,
skill_package_directory_name,
)
from app.harness import (
HarnessArtifactAccessError,
HarnessExecutor,
Expand All @@ -53,18 +57,18 @@
register_command_tools,
snapshot_harness_workspace,
)
from app.harness.execution_context import SANDBOX_WORKSPACE
from app.harness.errors import HarnessExecutionError
from app.harness.execution_context import SANDBOX_WORKSPACE
from app.harness.sandbox import parse_network_policy
from app.knowledge.citations import knowledge_citations_from_results
from app.knowledge.schema import KnowledgeSearchRequest
from app.knowledge.service import KnowledgeService
from app.tools.tool_executor import ToolExecutor
from app.tools.tool_schema import ToolCall


_INLINE_JSON_TOOL_RESULT_MAX_CHARS = 2_000
_INTERNAL_TOOL_RESULT_DIRECTORY = ".harness/tool-results"
_INTERNAL_SKILL_PACKAGE_DIRECTORY = ".harness/skill_packages"
_SANDBOX_JSON_FILE_KIND = "sandbox_json_file"


Expand Down Expand Up @@ -712,6 +716,15 @@ def _read_general_skill_package(
metadata: dict[str, Any],
query: str,
) -> dict[str, Any]:
package = package_from_row(skill)
package_relative_path = (
Path(_INTERNAL_SKILL_PACKAGE_DIRECTORY)
/ skill_package_directory_name(skill.slug, package.digest)
).as_posix()
materialize_general_skill_package(
skill,
self.workspace_root / package_relative_path,
)
return {
"success": True,
"data": {
Expand All @@ -720,11 +733,17 @@ def _read_general_skill_package(
"operation": "read",
"query": query,
"package": _skill_package_preview(skill),
"package_workspace": {
"relative_path": package_relative_path,
"sandbox_path": _sandbox_path(package_relative_path),
"entrypoint": package.entrypoint,
},
"notice": (
"技能包说明已加载到当前隔离 Harness transcript;"
"请由 AgentLoop 直接应用其中的 prompt、规则和示例,并按任务需要调用"
"知识库、原装 Tool、exec_command 或 typed 文件工具;Skill 本身不会"
"生成临时代码或启动第二套 runner。"
"技能包说明已加载到当前隔离 Harness transcript,且真实包文件已物化到"
"当前 TaskFrame。请直接使用 package_workspace.relative_path 下的文件,"
"按任务需要调用知识库、原装 Tool、exec_command 或 typed 文件工具;"
"不要用 write_file 重写技能包中的脚本。技能本身不会生成临时代码,"
"也不会启动第二套 runner。"
),
},
}
Expand Down
66 changes: 66 additions & 0 deletions backend/app/general_skills/package_materialization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from __future__ import annotations

import re
from collections.abc import Mapping, Sequence
from pathlib import Path

from app.capabilities.contracts import GeneralSkillPackage
from app.capabilities.local_general_skill import package_from_row
from app.db.models import GeneralSkill


def materialize_general_skill_package(
skill: GeneralSkill,
target_dir: Path,
) -> GeneralSkillPackage:
"""Restore one stored GeneralSkill package below a trusted workspace path.

The package data is stored in the database rather than as a persistent ZIP
on disk. Both the legacy Runner and Harness-native execution need the same
safe, on-disk view of that snapshot.
"""

package = package_from_row(skill)
target_dir.mkdir(parents=True, exist_ok=True)

metadata = skill.metadata_json if isinstance(skill.metadata_json, Mapping) else {}
directory_values = metadata.get("skill_directories", [])
if isinstance(directory_values, Sequence) and not isinstance(
directory_values, (str, bytes)
):
for value in directory_values:
relative_path = safe_package_path(str(value or ""))
if relative_path:
(target_dir / relative_path).mkdir(parents=True, exist_ok=True)

for file in package.files:
relative_path = safe_package_path(file.path)
if not relative_path:
continue
output_path = target_dir / relative_path
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(file.content, encoding="utf-8")

return package


def safe_package_path(path: str) -> str:
"""Normalize a package-relative path and reject traversal or unsafe parts."""

cleaned = path.replace("\\", "/").strip().strip("/")
parts = [part for part in cleaned.split("/") if part and part != "."]
if not parts or any(part == ".." for part in parts):
return ""
return "/".join(parts)


def skill_package_directory_name(slug: str, digest: str) -> str:
"""Return a short, human-readable immutable package directory name."""

normalized_slug = re.sub(r"[^A-Za-z0-9_-]+", "-", slug).strip("-")
normalized_slug = normalized_slug[:80] or "general-skill"
normalized_digest = digest.removeprefix("sha256:")
short_digest = re.sub(r"[^A-Fa-f0-9]", "", normalized_digest)[:12]
if not short_digest:
raise ValueError("GeneralSkill package digest is invalid.")
return f"{normalized_slug}--{short_digest.lower()}"
15 changes: 2 additions & 13 deletions backend/app/general_skills/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from app import paths
from app.db.models import GeneralSkill, ModelConfig
from app.general_skills.package_materialization import materialize_general_skill_package
from app.general_skills.runtime_env import (
GeneralSkillRuntimeError,
ensure_runtime_python,
Expand Down Expand Up @@ -1033,19 +1034,7 @@ def _skill_package_payload(skill: GeneralSkill, preview_limit: int = 12000) -> d


def _materialize_skill_package(skill: GeneralSkill, target_dir: Path) -> None:
target_dir.mkdir(parents=True, exist_ok=True)
metadata = getattr(skill, "metadata_json", None)
directory_values = metadata.get("skill_directories", []) if isinstance(metadata, Mapping) else []
if isinstance(directory_values, Sequence) and not isinstance(directory_values, (str, bytes)):
for value in directory_values:
relative_path = _safe_package_path(str(value or ""))
if relative_path:
(target_dir / relative_path).mkdir(parents=True, exist_ok=True)
for file in _skill_files(skill):
relative_path = _safe_package_path(str(file["path"]))
output_path = target_dir / relative_path
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(str(file.get("content") or ""), encoding="utf-8")
materialize_general_skill_package(skill, target_dir)


def _safe_package_path(path: str) -> str:
Expand Down
6 changes: 4 additions & 2 deletions backend/app/llm/prompts/harness_agent_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ source_user_message 是创建或最近更新该 TaskFrame 的用户原话,只
- 读取技能包后,直接把 prompt、规范、知识说明和示例作为本 TaskFrame 的执行指导,
再按需要调用知识库、HTTP/MCP/A2A Tool、exec_command 或 typed 文件工具。Skill
不会启动第二套 runner,也不得为了包装答案而生成代码。若任务本身要求创建或编辑
代码,使用 write_file/edit_file 等 typed 文件工具;若包内已有明确脚本,可按
SKILL.md 指令使用 read_file 检查后,通过 exec_command 执行该既有脚本。
代码,使用 write_file/edit_file 等 typed 文件工具;若 GeneralSkill 结果提供
`package_workspace.relative_path`,它是该包真实文件的当前 TaskFrame 相对目录。若包内已有
明确脚本,可按 SKILL.md 指令使用 read_file 检查后,通过 exec_command 执行这个目录中的
既有脚本,不得用 write_file 重写或复制该脚本。
- 如果 GeneralSkill 明确要求返回固定 JSON,Skill 描述的是业务结果契约,不要求 Skill
作者编写 Harness 的 `action` 字段。你仍应使用 `finish`,把业务 JSON 原样放入
`structured_result`,并在 `reply_fragment` 中给出相同 JSON 文本;不得因为对象中包含
Expand Down
15 changes: 14 additions & 1 deletion backend/tests/test_harness_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from sqlmodel import Session, SQLModel, create_engine, select

from app.agents.branching import ensure_open_gallery_binding
from app.capabilities.local_general_skill import package_from_row
from app.core import harness_agent as harness_agent_module
from app.core import turn_planner as turn_planner_module
from app.core.agent_loop import AgentLoop
Expand Down Expand Up @@ -1958,6 +1959,7 @@ def test_general_skill_harness_tool_reads_full_package_when_requested(
{"path": "SKILL.md", "content": "# Runner"},
{"path": "scripts/run.sh", "content": "echo ok"},
],
metadata_json={"skill_directories": ["references/empty"]},
status="published",
)
descriptor = CapabilityDescriptor(
Expand Down Expand Up @@ -1996,7 +1998,18 @@ def test_general_skill_harness_tool_reads_full_package_when_requested(
"scripts/run.sh",
]
assert read_result["data"]["operation"] == "read"
assert "不会生成临时代码" in read_result["data"]["notice"]
assert "真实包文件已物化" in read_result["data"]["notice"]
assert "不会启动第二套 runner" in read_result["data"]["notice"]
package = package_from_row(skill)
package_workspace = read_result["data"]["package_workspace"]
assert package_workspace["relative_path"] == (
f".harness/skill_packages/runner--{package.digest.removeprefix('sha256:')[:12]}"
)
package_root = invoker.workspace_root / package_workspace["relative_path"]
assert (package_root / "SKILL.md").read_text(encoding="utf-8") == "# Runner"
assert (package_root / "scripts/run.sh").read_text(encoding="utf-8") == "echo ok"
assert (package_root / "references/empty").is_dir()
assert invoker.discover_artifacts() == []


def test_general_skill_harness_tool_defaults_to_read_instead_of_generating_code(
Expand Down