Allow file browser to open root Markdown in Editor

Let File Browser-sourced Editor opens register existing Markdown files under the Agent Zero runtime root while preserving the stricter document artifact sandbox for ordinary document operations. Pass the file-browser source through the Editor store and cover the /a0/AGENTS.md-style path with regression tests.

Alessandro committed May 22, 2026 at 15:00 UTC 4f2d996ac8ce31da638660b64052166a0fc235b0
5 files changed +69 -9
plugins/_editor/api/editor_session.py
+10 -1
@@ -58,7 +58,11 @@ class EditorSession(ApiHandler):
58 doc = (
59 document_store.get_document(file_id)
60 if file_id
61 - else document_store.register_document(str(input.get("path") or ""), context_id=context_id)
61 + else document_store.register_document(
62 + str(input.get("path") or ""),
63 + context_id=context_id,
64 + allow_base_dir=self._allow_base_dir_open(input),
65 + )
66 )
67 except Exception as exc:
68 return {"ok": False, "error": str(exc)}
@@ -145,6 +149,11 @@ class EditorSession(ApiHandler):
149 origin = request.headers.get("Origin") or request.host_url.rstrip("/")
150 return origin.rstrip("/")
151
152 + def _allow_base_dir_open(self, input: dict) -> bool:
153 + if str(input.get("source") or "").strip() != "file-browser":
154 + return False
155 + return bool(str(input.get("path") or "").strip())
156 +
157
158 def _public_doc(doc: dict) -> dict:
159 return {
plugins/_editor/webui/editor-store.js
+6 -2
@@ -873,8 +873,12 @@ const model = {
873 await fileBrowserStore.open(workdirPath);
874 },
875
876 - async openPath(path) {
877 - return await this.openSession({ path: String(path || "") });
876 + async openPath(path, options = {}) {
877 + return await this.openSession({
878 + path: String(path || ""),
879 + source: options?.source || "",
880 + refresh: options?.refresh === true,
881 + });
882 },
883
884 async openSession(payload = {}) {
plugins/_office/helpers/document_store.py
+12 -5
@@ -128,7 +128,7 @@ def _path_from_a0(path: str | Path) -> Path:
128 return Path(raw if os.path.isabs(raw) else files.get_abs_path(raw)).expanduser()
129
130
131 -def allowed_roots(context_id: str = "") -> list[Path]:
131 +def allowed_roots(context_id: str = "", allow_base_dir: bool = False) -> list[Path]:
132 project_helpers = _projects()
133 roots = {
134 WORKDIR.resolve(strict=False),
@@ -140,6 +140,8 @@ def allowed_roots(context_id: str = "") -> list[Path]:
140 configured = str(_settings().get_settings().get("workdir_path") or "").strip()
141 if configured:
142 roots.add(_path_from_a0(configured).resolve(strict=False))
143 + if allow_base_dir:
144 + roots.add(Path(files.get_base_dir()).resolve(strict=False))
145 return sorted(roots, key=lambda item: str(item))
146
147
@@ -155,10 +157,10 @@ def _settings() -> Any:
157 return settings
158
159
158 -def normalize_path(path: str | Path, context_id: str = "") -> Path:
160 +def normalize_path(path: str | Path, context_id: str = "", allow_base_dir: bool = False) -> Path:
161 candidate = _path_from_a0(path)
162 resolved = candidate.resolve(strict=False)
161 - roots = allowed_roots(context_id)
163 + roots = allowed_roots(context_id, allow_base_dir=allow_base_dir)
164 if not any(_is_relative_to(resolved, root) for root in roots):
165 raise PermissionError("Document artifacts must stay inside the active project or workdir.")
166 if candidate.exists():
@@ -236,8 +238,13 @@ def init_db(conn: sqlite3.Connection) -> None:
238 )
239
240
239 -def register_document(path: str | Path, owner_id: str = "a0", context_id: str = "") -> dict[str, Any]:
240 - resolved = normalize_path(path, context_id=context_id)
241 +def register_document(
242 + path: str | Path,
243 + owner_id: str = "a0",
244 + context_id: str = "",
245 + allow_base_dir: bool = False,
246 +) -> dict[str, Any]:
247 + resolved = normalize_path(path, context_id=context_id, allow_base_dir=allow_base_dir)
248 if not resolved.exists():
249 raise FileNotFoundError(str(resolved))
250 ext = normalize_extension(resolved.suffix.lstrip("."))
tests/test_office_document_store.py
+40
@@ -25,6 +25,7 @@ from plugins._editor.helpers import (
25 markdown_sessions as editor_markdown_sessions,
26 open_files_context,
27 )
28 +from plugins._editor.api.editor_session import EditorSession
29 from plugins._office.helpers import (
30 artifact_editor,
31 canvas_context,
@@ -94,6 +95,45 @@ def test_text_files_register_as_desktop_documents(office_state):
95 assert "txt" in desktop_session.OFFICIAL_EXTENSIONS
96
97
98 +def test_file_browser_can_register_runtime_root_markdown(office_state, monkeypatch):
99 + runtime_root = office_state.state.parent / "runtime-root"
100 + runtime_root.mkdir()
101 + path = runtime_root / "AGENTS.md"
102 + path.write_text("# Runtime Instructions\n", encoding="utf-8")
103 + monkeypatch.setattr(document_store.files, "get_base_dir", lambda: str(runtime_root))
104 +
105 + with pytest.raises(PermissionError, match="active project or workdir"):
106 + document_store.register_document(path)
107 +
108 + doc = document_store.register_document(path, allow_base_dir=True)
109 +
110 + assert doc["basename"] == "AGENTS.md"
111 + assert doc["path"] == str(path)
112 +
113 +
114 +def test_editor_file_browser_source_opens_runtime_root_markdown(office_state, monkeypatch):
115 + runtime_root = office_state.state.parent / "runtime-root"
116 + runtime_root.mkdir()
117 + path = runtime_root / "AGENTS.md"
118 + path.write_text("# Runtime Instructions\n", encoding="utf-8")
119 + monkeypatch.setattr(document_store.files, "get_base_dir", lambda: str(runtime_root))
120 + handler = EditorSession(app=None, thread_lock=None)
121 + request = types.SimpleNamespace(headers={}, host_url="http://localhost/")
122 +
123 + blocked = asyncio.run(handler.process({"action": "open", "path": str(path)}, request))
124 + opened = asyncio.run(handler.process({
125 + "action": "open",
126 + "path": str(path),
127 + "source": "file-browser",
128 + }, request))
129 +
130 + assert blocked["ok"] is False
131 + assert "active project or workdir" in blocked["error"]
132 + assert opened["ok"] is True
133 + assert opened["title"] == "AGENTS.md"
134 + assert opened["text"] == "# Runtime Instructions\n"
135 +
136 +
137 @pytest.mark.parametrize(
138 ("kind", "title", "fmt", "expected_name"),
139 [
webui/components/modals/file-browser/file-browser-store.js
+1 -1
@@ -838,7 +838,7 @@ const model = {
838 if (target === "editor") {
839 const { store: editorStore } = await import("/plugins/_editor/webui/editor-store.js");
840 if (!this.storeHasPath(editorStore, path)) {
841 - const session = await editorStore.openPath(path);
841 + const session = await editorStore.openPath(path, { source: "file-browser" });
842 if (!session || session.ok === false) {
843 throw new Error(editorStore.error || "Markdown could not be opened.");
844 }