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
}