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
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,10 @@ public final class SkillPromptBuilder {
1. After loading a skill, look at its <files-root> in <available_skills>
2. List its files: ls <files-root>/
3. Run scripts: python3 <files-root>/scripts/<script-name>
4. Always use absolute paths derived from <files-root>; never invent paths
5. If a script exists for the task, run it directly — do not rewrite its logic inline
4. In the command, always use absolute paths derived from <files-root>; never invent paths

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same policy now lives in four places that must move together: this bullet list, the working_directory description in ShellExecuteTool, the runtime error string, and docs/v2/{en,zh}/docs/harness/skill.md. The two new tests pin the prompt lines and the schema description independently, so any future rewording needs several coordinated edits. Would extracting the rule into one shared constant (consumed by both the prompt and the tool description) work here, or is the duplication deliberate because this prompt text is intentionally frozen?

5. The working_directory parameter accepts only workspace-relative paths; never pass <files-root> to it

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bullet says working_directory accepts "only workspace-relative paths", but the tool also rejects a leading ~ and any path containing ... A model that reads only the prompt will still emit working_directory="skills/../shared" and burn a tool call on the error. Since the stated goal of the change is to stop wasted calls, could this line state the same three rejections as the tool message (or just defer to it), so prompt and @ToolParam description express one rule instead of two partial ones?

6. Omit working_directory unless the command needs a workspace-relative directory
7. If a script exists for the task, run it directly — do not rewrite its logic inline
</code_execution>
""";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ public String execute(
@ToolParam(
name = "working_directory",
description =
"Working directory (relative to workspace root, optional)",
"Optional working directory relative to the workspace root."
+ " Omit it when invoking an absolute path, such as a skill"
+ " script.",
required = false)
String workingDirectory,
@ToolParam(
Expand All @@ -66,7 +68,8 @@ public String execute(
String wd = workingDirectory.strip();
if (wd.startsWith("/") || wd.startsWith("~") || wd.contains("..")) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message now promises that "absolute paths, '~', and '..' are not allowed", but the guard only catches POSIX-shaped absolute paths. A Windows-shaped one (C:\workspace\skills\alpha, \\server\share\x, or /d/workspace/x) passes this check and reaches commandWithWorkingDirectory, which emits cd /d "C:\workspace\skills\alpha" && ... resolved against the sandbox process cwd — exactly the confusing failure this PR is removing, and the repo does build on windows-latest and already has a Windows branch here. Could the check also reject a drive-letter / leading-\ form? Then the new rejection test would have a Windows counterpart.

return "Error: working_directory must be a relative path within the workspace"
+ " (absolute paths, '~', and '..' are not allowed).";
+ " (absolute paths, '~', and '..' are not allowed). Put absolute paths in"
+ " the command instead, or omit working_directory.";
}
effectiveCommand =
commandWithWorkingDirectory(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,18 @@ void rendersFilesRootAndCodeExecutionWhenAvailable() {
assertTrue(out.contains("<files-root>/workspace/skills/alpha</files-root>"));
assertTrue(out.contains("## Code Execution"));
assertTrue(out.contains("<files-root>"));
assertTrue(out.contains("access to the execute tool"));
assertTrue(out.contains("You have access to the execute tool"));
assertTrue(
out.contains(
"In the command, always use absolute paths derived from <files-root>"));
assertTrue(
out.contains(
"The working_directory parameter accepts only workspace-relative paths;"
+ " never pass <files-root> to it"));
assertTrue(
out.contains(
"Omit working_directory unless the command needs a workspace-relative"
+ " directory"));
assertFalse(out.contains("execute_shell_command"));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,18 @@
package io.agentscope.harness.agent.tool;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.tool.AgentTool;
import io.agentscope.core.tool.Toolkit;
import io.agentscope.harness.agent.filesystem.local.LocalFilesystemWithShell;
import io.agentscope.harness.agent.filesystem.model.ExecuteResponse;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -67,6 +73,45 @@ void execute_withWorkingDirectory_prefixesCd() {
assertEquals(30, sandbox.timeoutSeconds);
}

@Test
void execute_absoluteWorkingDirectory_isRejectedWithRecoveryGuidance() {
String result =
tool.execute(RT, "python3 /workspace/skills/alpha/run.py", "/workspace", null);

assertTrue(result.contains("working_directory must be a relative path"));
assertTrue(result.contains("Put absolute paths in the command instead"));
assertNull(sandbox.command);
assertNull(sandbox.timeoutSeconds);
}

@Test
@SuppressWarnings("unchecked")
void execute_schemaClarifiesWorkingDirectoryUsage() {
Toolkit toolkit = new Toolkit();
toolkit.registerTool(tool);

AgentTool registered = toolkit.getTool(ShellExecuteTool.NAME);
assertEquals("execute", registered.getName());

Map<String, Object> parameters = registered.getParameters();
Map<String, Object> properties = (Map<String, Object>) parameters.get("properties");
Map<String, Object> workingDirectory =
(Map<String, Object>) properties.get("working_directory");
List<String> required = (List<String>) parameters.get("required");

assertFalse(required.contains("working_directory"));
assertTrue(
workingDirectory
.get("description")
.toString()
.contains("relative to the workspace root"));
assertTrue(
workingDirectory
.get("description")
.toString()
.contains("Omit it when invoking an absolute path"));
}

@Test
void commandWithWorkingDirectory_usesCmdCompatibleQuotingOnWindows() {
assertEquals(
Expand Down
10 changes: 5 additions & 5 deletions docs/v2/en/docs/harness/skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,15 +307,15 @@ The agent doesn't see this difference — `load_skill_through_path` always works

### `<files-root>` and shell execution

When a skill ships scripts (e.g. `scripts/run-checks.sh`), the agent needs an absolute path to invoke them via `execute_shell_command`. That path comes from the `<files-root>` element on each skill entry. Resolution depends on the filesystem mode:
When a skill ships scripts (e.g. `scripts/run-checks.sh`), the agent needs an absolute path to invoke them with the `execute` tool. That path comes from the `<files-root>` element on each skill entry. Resolution depends on the filesystem mode:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch renaming the tool here, but the rename is incomplete in the same doc set: docs/v2/en/docs/harness/workspace.md:391 and docs/v2/zh/docs/harness/workspace.md:386 still say the agent invokes skill scripts "via execute_shell_command". On the harness path the registered shell tool is execute (ShellExecuteTool.NAME), so a reader following those harness docs ends up calling a tool that the harness does not register — the same class of mistake this PR exists to prevent. (execute_shell_command is still legitimate for users who register core ShellCommandTool themselves, and AgentSkillPromptProvider keeps that wording for the SkillBox path, so a scope note would be enough if that split is intentional.) Could you update the two workspace.md lines here, or file a follow-up so the harness docs name one tool consistently?


| FS mode (shell available?) | Workspace skill `<files-root>` | Marketplace skill `<files-root>` |
|----------------------------|--------------------------------|-----------------------------------|
| Sandbox | `/workspace/skills/<name>` | `/workspace/.skills-cache/<source>/<name>` |
| Local-with-shell | `<wsRoot>/skills/<name>` | `<wsRoot>/.skills-cache/<source>/<name>` |
| Local without shell / Composite | (not rendered — no shell tool registered) | (not rendered) |

So the agent's shell call is always `execute_shell_command("python3 <files-root>/scripts/foo.py")` — no path guessing, no per-source variations to remember.
So the agent always puts that path in the command, for example `execute(command="python3 <files-root>/scripts/foo.py")` — no path guessing, no per-source variations to remember. The optional `working_directory` parameter accepts only a path relative to the workspace root. Do not pass `<files-root>` to it; omit it when running skill scripts unless the command needs a workspace-relative working directory.

### Where marketplace files actually live

Expand Down Expand Up @@ -380,11 +380,11 @@ In sandbox mode, each skill's `<files-root>` in the `<available_skills>` block i

So the agent simply issues:

```
execute_shell_command("python3 /workspace/skills/code-reviewer/scripts/run-checks.sh <target>")
```text
execute(command="python3 /workspace/skills/code-reviewer/scripts/run-checks.sh <target>")
```

That command runs in the container and reads exactly the file that was projected in. The agent doesn't need to know which layer a skill came from — the framework computes the prefix.
That command runs in the container and reads exactly the file that was projected in. The absolute path stays in `command`; `working_directory` is omitted. The agent doesn't need to know which layer a skill came from — the framework computes the prefix.

> If a sandbox backend mounts the workspace at a non-default location (e.g. AgentRun uses `/home/agentscope/workspace`), the `<files-root>` prefix changes accordingly, and the agent still gets a correct absolute path.

Expand Down
10 changes: 5 additions & 5 deletions docs/v2/zh/docs/harness/skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,15 +307,15 @@ agent 感知不到这种差异,`load_skill_through_path` 调起来都一样。

### `<files-root>` 和 shell 执行

当一个 skill 自带脚本(例如 `scripts/run-checks.sh`),agent 需要绝对路径才能通过 `execute_shell_command` 调用它。这个绝对路径就是 skill 条目里的 `<files-root>`。它怎么算出来取决于文件系统模式:
当一个 skill 自带脚本(例如 `scripts/run-checks.sh`),agent 需要绝对路径才能通过 `execute` 工具调用它。这个绝对路径就是 skill 条目里的 `<files-root>`。它怎么算出来取决于文件系统模式:

| 文件系统模式(是否有 shell) | 工作区 skill 的 `<files-root>` | 市场 skill 的 `<files-root>` |
|---------------------------|----------------------------|---------------------------|
| Sandbox | `/workspace/skills/<name>` | `/workspace/.skills-cache/<source>/<name>` |
| Local-with-shell | `<wsRoot>/skills/<name>` | `<wsRoot>/.skills-cache/<source>/<name>` |
| Local 不带 shell / Composite | (不渲染——没注册 shell 工具) | (不渲染) |

所以 agent 发出来的 shell 命令永远是 `execute_shell_command("python3 <files-root>/scripts/foo.py")`——不用猜路径,不用记每种来源对应哪个前缀。
所以 agent 始终把这个路径放进命令,例如 `execute(command="python3 <files-root>/scripts/foo.py")`——不用猜路径,不用记每种来源对应哪个前缀。可选参数 `working_directory` 只接受相对于工作区根目录的路径。不要把 `<files-root>` 传给它;运行 skill 脚本时应省略该参数,除非命令确实需要一个相对于工作区的工作目录

### 市场 skill 文件实际落在哪儿

Expand Down Expand Up @@ -380,11 +380,11 @@ AGENTS.md skills/ subagents/ knowledge/ .skills-cache/

于是 agent 直接发:

```
execute_shell_command("python3 /workspace/skills/code-reviewer/scripts/run-checks.sh <目标路径>")
```text
execute(command="python3 /workspace/skills/code-reviewer/scripts/run-checks.sh <目标路径>")
```

这条命令在容器里跑,读到的就是投影进来的那份文件。agent 不用知道 skill 来自哪一层,前缀由框架算好。
这条命令在容器里跑,读到的就是投影进来的那份文件。绝对路径保留在 `command` 中,`working_directory` 省略。agent 不用知道 skill 来自哪一层,前缀由框架算好。

> 如果沙箱后端把工作区挂在非默认位置(比如 AgentRun 是 `/home/agentscope/workspace`),`<files-root>` 前缀会跟着换,agent 拿到的依然是正确的绝对路径。

Expand Down
Loading