From 8f7ba6615b5416b725e65cb9cc3782f9bded4d57 Mon Sep 17 00:00:00 2001 From: Codev contributors Date: Fri, 9 Oct 2026 14:30:38 -0400 Subject: [PATCH] Normalize scoped agent read paths --- Codev.Core/AgentProfileCatalog.cs | 29 ++++++++++++++++++++++++- Codev.Tests/AgentProfileCatalogTests.cs | 12 +++++++++- DESIGN.md | 2 +- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/Codev.Core/AgentProfileCatalog.cs b/Codev.Core/AgentProfileCatalog.cs index 9e9d5380..ebc2c44b 100644 --- a/Codev.Core/AgentProfileCatalog.cs +++ b/Codev.Core/AgentProfileCatalog.cs @@ -454,11 +454,38 @@ internal static bool CommandPatternMatches(string pattern, string command) internal static bool OpenCodePathPatternMatches(string pattern, string relativePath) { + if (!TryNormalizePermissionPath(relativePath, out var normalizedPath)) return false; var normalizedPattern = pattern.Replace('\\', '/'); - var normalizedPath = relativePath.Replace('\\', '/'); return OpenCodeResourcePatternMatches(normalizedPattern, normalizedPath); } + private static bool TryNormalizePermissionPath(string path, out string normalizedPath) + { + normalizedPath = ""; + if (string.IsNullOrWhiteSpace(path)) return false; + var portablePath = path.Replace('\\', '/'); + if (portablePath[0] == '/' || + Regex.IsMatch(portablePath, "^[A-Za-z]:", RegexOptions.CultureInvariant, TimeSpan.FromMilliseconds(100))) + return false; + + var segments = new List(); + foreach (var segment in portablePath.Split('/', StringSplitOptions.RemoveEmptyEntries)) + { + if (segment == ".") continue; + if (segment == "..") + { + if (segments.Count == 0) return false; + segments.RemoveAt(segments.Count - 1); + continue; + } + segments.Add(segment); + } + + if (segments.Count == 0) return false; + normalizedPath = string.Join('/', segments); + return true; + } + internal static bool OpenCodeResourcePatternMatches(string pattern, string resource) { var regex = "^" + Regex.Escape(pattern).Replace("\\*", ".*").Replace("\\?", ".") + "$"; diff --git a/Codev.Tests/AgentProfileCatalogTests.cs b/Codev.Tests/AgentProfileCatalogTests.cs index b0b88a76..fb76c33e 100644 --- a/Codev.Tests/AgentProfileCatalogTests.cs +++ b/Codev.Tests/AgentProfileCatalogTests.cs @@ -310,13 +310,23 @@ await File.WriteAllTextAsync(Path.Combine(directory, "writer.md"), """ Assert.Equal(AgentToolPermission.Allow, AgentProfilePolicy.PermissionFor(writer, "read_file", resource: "docs/guide.md")); Assert.Equal(AgentToolPermission.Deny, AgentProfilePolicy.PermissionFor(writer, "read_file", resource: "docs/private/key.md")); Assert.Equal(AgentToolPermission.Ask, AgentProfilePolicy.PermissionFor(writer, "read_file", resource: "README.md")); + Assert.Equal(AgentToolPermission.Ask, AgentProfilePolicy.PermissionFor(writer, "read_file", resource: "docs/../README.md")); + Assert.Equal(AgentToolPermission.Ask, AgentProfilePolicy.PermissionFor(writer, "read_file", resource: "../docs/guide.md")); + Assert.Equal(AgentToolPermission.Ask, AgentProfilePolicy.PermissionFor(writer, "read_file", resource: "/docs/guide.md")); + Assert.Equal(AgentToolPermission.Ask, AgentProfilePolicy.PermissionFor(writer, "read_file", resource: "C:/docs/guide.md")); + Assert.Equal(AgentToolPermission.Allow, AgentProfilePolicy.PermissionFor(writer, "read_file", resource: "docs/../docs/guide.md")); Assert.Equal(AgentToolPermission.Allow, AgentProfilePolicy.PermissionFor(writer, "list_files")); Assert.Equal(AgentToolPermission.Allow, AgentProfilePolicy.PermissionFor(writer, "search_files")); Assert.True(AgentProfilePolicy.CanExposeReadPath(writer, "docs/guide.md")); Assert.False(AgentProfilePolicy.CanExposeReadPath(writer, "docs/private/key.md")); Assert.False(AgentProfilePolicy.CanExposeReadPath(writer, "README.md")); + Assert.False(AgentProfilePolicy.CanExposeReadPath(writer, "docs/../README.md")); + Assert.False(AgentProfilePolicy.CanExposeReadPath(writer, "../docs/guide.md")); + Assert.False(AgentProfilePolicy.CanExposeReadPath(writer, "/docs/guide.md")); + Assert.False(AgentProfilePolicy.CanExposeReadPath(writer, "C:/docs/guide.md")); + Assert.True(AgentProfilePolicy.CanExposeReadPath(writer, "docs/../docs/guide.md")); Assert.Equal(["docs/guide.md"], AgentProfilePolicy.FilterReadablePaths(writer, - ["docs/guide.md", "docs/private/key.md", "README.md"])); + ["docs/guide.md", "docs/private/key.md", "README.md", "docs/../README.md", "../docs/guide.md"])); Assert.Empty(loaded.Warnings); } diff --git a/DESIGN.md b/DESIGN.md index e4241de5..86540eac 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -353,7 +353,7 @@ X-items are from section 7. Items in phase 1 to 3 do not depend on the Avalonia |---|---|---|---|---| | D1 | Slash commands and file-based commands | S | none | Core · Avalonia implemented; project commands require trust; manual QA remains; The shared-core implementation remains in place | | D2 | Skills | M | D1 | Later · Avalonia discovers Codev and OpenCode/Claude/Agents-compatible user and trusted-project skill folders. It accepts OpenCode `name`, `license`, `compatibility`, and nested `metadata` frontmatter, validates the declared name against the directory, and keeps Codev's bounded prompt-only behavior. Project Codev skills win same-name conflicts, followed by OpenCode, Claude, and Agents folders; Codev user skills win over shared user folders. Code task loaders re-read the original file and recheck trust. The portable suite now passes 822 tests with one opt-in live-model test skipped; manual UI checks remain | -| D3 | Agent definition files | M | B4 | Core · Avalonia supports built-in Build/Plan primary agents switchable during Code tasks, Markdown profiles, a validated user profile editor, prompt/tool policies with wildcard matching, edit-path and command allow/deny globs, and model/temperature/step settings. It imports legacy and current V2 OpenCode Markdown profiles from the standard user and trusted-project folders, maps ordered wildcard, shell-command, read-path, and edit-path permissions without overriding Codev's project policy, and warns when a model preference is unsupported. Scoped read rules are checked for direct reads and filter list/search results before they reach the model; they also constrain automatically attached source excerpts, relevance-preflight candidate paths, and optional repo-map entries. An explicit empty filtered selection stays empty instead of falling back to unrestricted automatic context. Resource-scoped V2 `glob`/`grep` rules now map to bounded path-glob and regex-search tools, with requested patterns checked against ordered permission rules and read-path scope still filtering returned files. Path-scoped edit `ask` rules now reach per-proposal approval outside Auto while matching denies remain hard blocks; the shared Auto/project policy still governs asks in Auto. Resource-scoped V2 `task`/`subagent` rules now target specific installed profiles through isolated child worktrees; target denies are hard blocks, asks follow the Auto/project policy, and only root profiles with explicit task permission may delegate. Resource-scoped V2 skill rules filter each discovered skill by OpenCode skill-ID patterns, with deny hiding skills and ask checked when loading; resource-scoped custom-tool rules remain unsupported and are rejected with a warning. Headless UI tests reject invalid profile Markdown, save valid Markdown through the real store and verify reload, select a saved profile in the real picker while verifying active-conversation persistence and label updates, and switch Build/Plan from the picker and Ctrl+Shift+A in both directions. Persisted profile references use validated display names (including spaces); profile filenames remain short slugs. Portable tests assert Plan schemas omit edit, shell, MCP, delegation, and background operations for both Ollama and OpenAI; executor tests reject Plan edits and commands before project review or command approval. A Windows x64 preview smoke verified that a multi-word `Smoke QA` selection remains visible after restart. Current-source Windows/.NET 10 recheck on PR head `3ce4814` passed all 30 `AgentProfileCatalogTests` and `Code_task_profile_picker_selects_a_saved_profile_on_the_active_conversation` (1/1), including OpenCode-format frontmatter import and picker selection/persistence. Other native profile-editor checks remain; The shared policy engine remains in place | +| D3 | Agent definition files | M | B4 | Core · Avalonia supports built-in Build/Plan primary agents switchable during Code tasks, Markdown profiles, a validated user profile editor, prompt/tool policies with wildcard matching, edit-path and command allow/deny globs, and model/temperature/step settings. It imports legacy and current V2 OpenCode Markdown profiles from the standard user and trusted-project folders, maps ordered wildcard, shell-command, read-path, and edit-path permissions without overriding Codev's project policy, and warns when a model preference is unsupported. Scoped read rules are checked for direct reads and filter list/search results before they reach the model; relative file candidates are normalized lexically before matching, and rooted or escaping paths cannot inherit a narrower directory allow; they also constrain automatically attached source excerpts, relevance-preflight candidate paths, and optional repo-map entries. An explicit empty filtered selection stays empty instead of falling back to unrestricted automatic context. Resource-scoped V2 `glob`/`grep` rules now map to bounded path-glob and regex-search tools, with requested patterns checked against ordered permission rules and read-path scope still filtering returned files. Path-scoped edit `ask` rules now reach per-proposal approval outside Auto while matching denies remain hard blocks; the shared Auto/project policy still governs asks in Auto. Resource-scoped V2 `task`/`subagent` rules now target specific installed profiles through isolated child worktrees; target denies are hard blocks, asks follow the Auto/project policy, and only root profiles with explicit task permission may delegate. Resource-scoped V2 skill rules filter each discovered skill by OpenCode skill-ID patterns, with deny hiding skills and ask checked when loading; resource-scoped custom-tool rules remain unsupported and are rejected with a warning. Headless UI tests reject invalid profile Markdown, save valid Markdown through the real store and verify reload, select a saved profile in the real picker while verifying active-conversation persistence and label updates, and switch Build/Plan from the picker and Ctrl+Shift+A in both directions. Persisted profile references use validated display names (including spaces); profile filenames remain short slugs. Portable tests assert Plan schemas omit edit, shell, MCP, delegation, and background operations for both Ollama and OpenAI; executor tests reject Plan edits and commands before project review or command approval. A Windows x64 preview smoke verified that a multi-word `Smoke QA` selection remains visible after restart. Current-source Windows/.NET 10 recheck on PR head `3ce4814` passed all 30 `AgentProfileCatalogTests` and `Code_task_profile_picker_selects_a_saved_profile_on_the_active_conversation` (1/1), including OpenCode-format frontmatter import and picker selection/persistence. Other native profile-editor checks remain; The shared policy engine remains in place | | D4 | Hooks (formatter first) | M | B1 | Avalonia formatter hook implemented · general hooks remain Later; manual UI/security review pending | | D5 | Subagents with child sessions | L | D3, section 3 worktrees | Later · Avalonia implements isolated child conversations, Orchestrator delegation, child diff review/merge, and missing-worktree recovery. Codev-managed file operations and Git history are isolated in per-child worktrees; shell commands still run with the user's account permissions and can affect paths or services outside the worktree. Child UI/status and delegation output disclose this boundary. Parallel worktree mutation is serialized, and creation/recovery disable checkout hooks, fsmonitor, and configured smudge/process filters; child merge suppresses hooks and refuses external merge drivers or checkout filters only when they apply to reviewed files (or when a changed `.gitattributes` could affect them). Git LFS/custom-filter files are consequently unexpanded during child checkout and this is reported to the user. Smudge filters are disabled with an empty command so checkout does not launch a replacement executable from PATH. Regression tests cover helper suppression, concurrency, atomic permission-policy inheritance, recovery, parent-stop scoping, and stopping one selected child without canceling siblings. Child mode and eligible rules are persisted in one update, so interruption cannot leave Auto active without inherited exact denies. **Current-checkout validation (2026-10-07, code head `415a171`):** the live Qwen3.6 `Live_orchestrator_starts_two_read_only_children_in_parallel_worktrees` test passed in 49 seconds, verifying two overlapping Ask children in distinct managed worktrees, exact fixture reads/results, and a clean parent Git tree. The two stop-scope regression tests passed 2/2, and the full Avalonia Release suite passed 56 tests with 12 opt-in live tests skipped. At PR #10 head `af41fc9` on 2026-10-08, focused local Windows .NET 10 Release runs passed all 11 `GitChildWorktreeManagerTests` and four `ChildDelegationTests`; the opt-in live Orchestrator/Qwen3.6 test was skipped because `CODEV_OLLAMA_LIVE_TESTS` was unset. Full OS-level shell confinement remains dependent on B7; packaged UI stop/cancel controls, native interaction with review/merge/recovery, and independent security review remain release gaps. D5's workflow is implemented ahead of phase 5; the remaining safety and manual-verification work is not complete. | | D6 | MCP client | L | B1 | Core · Avalonia implemented for Ollama and OpenAI Code tasks (stdio and auto-detected Streamable HTTP with legacy SSE fallback, user settings, tool/prompt/resource/resource-template discovery and calls, untrusted results, per-project permissions); diagnostics persist per conversation. On 2026-10-01, the focused MCP suite passed 49/49 on Windows Release, including a cross-origin redirect regression proving custom environment-backed secret headers reach only the configured endpoint. The mock OAuth/PKCE integration covers browser callback, token exchange, authenticated MCP call, persistence, and refresh. Opt-in native-vault OAuth smoke now passes with Windows Credential Manager, Linux Secret Service, and macOS Keychain (CI [#36849825001](https://github.com/jay23606/codev/actions/runs/36849825001)); four-RID package/launch validation passed in [#36849824882](https://github.com/jay23606/codev/actions/runs/36849824882). Latest Avalonia UI plus portable CI passed on all three OSes in [#36853446126](https://github.com/jay23606/codev/actions/runs/36853446126), with four-RID packaging and launch in [#36853446114](https://github.com/jay23606/codev/actions/runs/36853446114). Live provider sign-in, native interactive macOS/Linux UI smoke, and independent security review remain; external server configuration and an optional global connection-history view remain future work; The shared-core implementation remains in place |