summaryrefslogtreecommitdiff
path: root/src/Agent.Core/Tools
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-08-10 19:38:55 +0900
committertanaka takashi <isazaka@gmail.com>2026-08-10 19:38:55 +0900
commite8b625e22f343d75e1b50d1634b4ab06899d060b (patch)
tree80b8d5d045f9402884a75cf2c74d1e15cb3ba9aa /src/Agent.Core/Tools
parentf2001b0c5a0b35e052ba62ef4f2e19bcc64fd870 (diff)
feat(tools): add dir internal tool for directory operations
新規Internal Tool `dir` を追加し、ディレクトリの作成・削除・一覧取得を 可能にした。file_editの拡張ではなく独立toolとして実装し、LLMが 「ディレクトリ操作はdir tool」と直感的に判断できるようにした。 追加した機能: - dir tool (6番目のInternal Tool、MCP非公開) - mkdir: Write capability要求、Directory.CreateDirectory()で親ディレクトリも含めて作成 - rmdir: Delete capability要求、空ディレクトリのみ削除(非空はToolValidationExceptionで拒否) - list: Read capability要求、直下エントリ一覧を返す 設計判断: - file_edit拡張ではなく新規toolとした理由: ツール名から機能が推測しやすく、 LLMが直感的に選択できるため - rmdirを空ディレクトリのみに制限した理由: rollback時の内容復元の複雑さを排除、 MemoryBackupLimit超過リスクなし、安全側 - mkdir/rmdirは既存のPathPolicy/ACL/Journal/Recovery枠組みを利用 - root継承ACE方式のため新規作成ディレクトリは自動的に親のACEを継承、追加ACL操作不要 変更対象: - InternalToolCatalog.cs: dir定義追加 (5→6 tool) - InternalToolDispatcher.cs: FakeInternalToolBackendの効果判定にdir追加 - InternalToolValidator.cs: dir用コメント追記 - WindowsToolBackend.cs: DirectoryOp()メソッド実装 - TransactionalFileEditor.cs: Mkdir()/Rmdir() transaction + DirectoryMkdirTransaction/DirectoryRmdirTransaction クラス追加 - RecoveryManager.cs: dir.mkdir/dir.rmdir crash recovery追加 - Specifications.md: FS-06追加 - Design.md: 6群表更新、§7.6 dir追加、5群→6群参照更新 テスト: - InternalToolTests.cs: dir単体テスト、catalog 6群検証、malformed args拒否 - SecurityPolicyTests.cs: mkdir/rmdir/list/許可外deny テスト追加 - PromptAndRuntimeTests.cs: tool count 5→6 - OllamaClientTests.cs: tool catalog配列長 5→6 検証結果: - Release build: 警告0、エラー0 - Planner tests: 10/10 PASS、skip 0 - Core tests: 56/56 PASS、skip 0 - MCP経由での動作確認: mkdir/list/rmdir/file_edit すべて正常 - Python実行確認: dir mkdir + file_edit + process_run でfib(1)〜fib(10)が正常出力 - Release Bundle publish済み 既知の運用上の知見: - uv venvのpyvenv.cfgのhomeがjunctionを指す場合、CanonicalPath.Resolveが reparse pointとして拒否する。実体パスへ書き換えるか、実体ディレクトリを agent.yamlのread/executeに追加する必要がある - venvのpython.exeはuv trampolineで子プロセスを生成するため、 sandboxのchild-process制限で拒否される。pyvenv.cfgが指す実体python.exeを 直接起動する必要がある
Diffstat (limited to 'src/Agent.Core/Tools')
-rw-r--r--src/Agent.Core/Tools/InternalToolCatalog.cs3
-rw-r--r--src/Agent.Core/Tools/InternalToolDispatcher.cs2
-rw-r--r--src/Agent.Core/Tools/InternalToolValidator.cs2
3 files changed, 5 insertions, 2 deletions
diff --git a/src/Agent.Core/Tools/InternalToolCatalog.cs b/src/Agent.Core/Tools/InternalToolCatalog.cs
index 1563c16..3ab17b1 100644
--- a/src/Agent.Core/Tools/InternalToolCatalog.cs
+++ b/src/Agent.Core/Tools/InternalToolCatalog.cs
@@ -19,7 +19,8 @@ public static class InternalToolCatalog
Define("file_read", "Read an approved file.", """{"type":"object","additionalProperties":false,"required":["path","mode"],"properties":{"path":{"type":"string"},"mode":{"type":"string","enum":["whole","range","metadata"]},"start":{"type":"integer","minimum":0},"length":{"type":"integer","minimum":0}}}"""),
Define("file_edit", "Edit an approved file transactionally.", """{"type":"object","additionalProperties":false,"required":["path","operation"],"properties":{"path":{"type":"string"},"operation":{"type":"string","enum":["overwrite","patch","atomic_replace","delete"]},"content":{"type":["string","null"]},"patch":{"type":["string","null"]},"expected_sha256":{"type":["string","null"]}}}"""),
Define("process_run", "Run one approved executable with separated arguments.", """{"type":"object","additionalProperties":false,"required":["executable","args","cwd"],"properties":{"executable":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"cwd":{"type":"string"},"timeout_seconds":{"type":"integer","minimum":1},"stdin":{"type":["string","null"]}}}"""),
- Define("task_update", "Update the deterministic task graph.", """{"type":"object","additionalProperties":false,"required":["operation"],"properties":{"operation":{"type":"string","enum":["add","add_dependency","set_state","set_estimate"]},"task_id":{"type":["string","null"]},"description":{"type":["string","null"]},"depends_on":{"type":"array","items":{"type":"string"}},"budget_class":{"type":["string","null"],"enum":["1K","2K","4K","8K","16K","32K","64K","128K",null]},"optimistic_tokens":{"type":"integer","minimum":0},"most_likely_tokens":{"type":"integer","minimum":0},"pessimistic_tokens":{"type":"integer","minimum":0},"state":{"type":["string","null"],"enum":["queued","planning","running","completed","failed","cancelled",null]}}}""")
+ Define("task_update", "Update the deterministic task graph.", """{"type":"object","additionalProperties":false,"required":["operation"],"properties":{"operation":{"type":"string","enum":["add","add_dependency","set_state","set_estimate"]},"task_id":{"type":["string","null"]},"description":{"type":["string","null"]},"depends_on":{"type":"array","items":{"type":"string"}},"budget_class":{"type":["string","null"],"enum":["1K","2K","4K","8K","16K","32K","64K","128K",null]},"optimistic_tokens":{"type":"integer","minimum":0},"most_likely_tokens":{"type":"integer","minimum":0},"pessimistic_tokens":{"type":"integer","minimum":0},"state":{"type":["string","null"],"enum":["queued","planning","running","completed","failed","cancelled",null]}}}"""),
+ Define("dir", "Create, remove, or list directories under an approved root. Only empty directories can be removed.", """{"type":"object","additionalProperties":false,"required":["path","operation"],"properties":{"path":{"type":"string"},"operation":{"type":"string","enum":["mkdir","rmdir","list"]}}}""")
];
return definitions.ToDictionary(static definition => definition.Name, StringComparer.Ordinal);
}
diff --git a/src/Agent.Core/Tools/InternalToolDispatcher.cs b/src/Agent.Core/Tools/InternalToolDispatcher.cs
index 0dca5df..c888705 100644
--- a/src/Agent.Core/Tools/InternalToolDispatcher.cs
+++ b/src/Agent.Core/Tools/InternalToolDispatcher.cs
@@ -23,7 +23,7 @@ public sealed class FakeInternalToolBackend : IInternalToolBackend
public Task<ToolExecutionResult> ExecuteAsync(Guid taskId, string toolName, JsonElement arguments, CancellationToken cancellationToken)
{
_calls.Add((toolName, arguments.Clone()));
- bool effect = toolName is "file_edit" or "process_run";
+ bool effect = toolName is "file_edit" or "process_run" or "dir";
return Task.FromResult(new ToolExecutionResult(true, "fake-ok", effect));
}
}
diff --git a/src/Agent.Core/Tools/InternalToolValidator.cs b/src/Agent.Core/Tools/InternalToolValidator.cs
index c48719e..091b790 100644
--- a/src/Agent.Core/Tools/InternalToolValidator.cs
+++ b/src/Agent.Core/Tools/InternalToolValidator.cs
@@ -75,6 +75,8 @@ public static class InternalToolValidator
if (operation == "set_state") Require(arguments, "task_id", "state");
if (operation == "set_estimate") Require(arguments, "task_id", "budget_class", "optimistic_tokens", "most_likely_tokens", "pessimistic_tokens");
}
+ // dir: path and operation are already required by the schema; no operation-specific
+ // additional arguments are needed since mkdir/rmdir/list only use the path field.
static void Require(JsonElement value, params string[] properties)
{