agent-folder-workspace 0.1.1__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/PKG-INFO +35 -10
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/README.md +34 -9
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/docs/architecture.md +19 -7
- agent_folder_workspace-0.1.2/docs/superpowers/plans/2026-07-23-runtime-workspace-selection.md +384 -0
- agent_folder_workspace-0.1.2/docs/superpowers/specs/2026-07-23-runtime-workspace-selection-design.md +141 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/docs/windows.md +21 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/pyproject.toml +1 -1
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/scripts/check_wheel.py +1 -1
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/__init__.py +7 -1
- agent_folder_workspace-0.1.2/src/agent_folder_workspace/mcp_server.py +304 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/models.py +20 -0
- agent_folder_workspace-0.1.2/src/agent_folder_workspace/opencode/skill/SKILL.md +30 -0
- agent_folder_workspace-0.1.2/src/agent_folder_workspace/schemas/WorkspaceCloseV1.schema.json +27 -0
- agent_folder_workspace-0.1.2/src/agent_folder_workspace/schemas/WorkspaceHandleV1.schema.json +509 -0
- agent_folder_workspace-0.1.2/src/agent_folder_workspace/schemas/WorkspaceListV1.schema.json +538 -0
- agent_folder_workspace-0.1.2/src/agent_folder_workspace/workspace_manager.py +234 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_contracts.py +50 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_distribution.py +12 -2
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_mcp_cli_installer.py +145 -4
- agent_folder_workspace-0.1.2/tests/test_mcp_pdf_survival.py +195 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_parser_workers.py +1 -0
- agent_folder_workspace-0.1.2/tests/test_workspace_manager.py +165 -0
- agent_folder_workspace-0.1.1/src/agent_folder_workspace/mcp_server.py +0 -179
- agent_folder_workspace-0.1.1/src/agent_folder_workspace/opencode/skill/SKILL.md +0 -24
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/.github/workflows/ci.yml +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/.gitignore +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/CONTRIBUTING.md +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/LICENSE +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/SECURITY.md +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/docs/acceptance.md +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/docs/formats.md +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/docs/superpowers/plans/2026-07-22-console-logging.md +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/docs/superpowers/specs/2026-07-22-console-logging-design.md +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/scripts/generate_json_schemas.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/scripts/generate_probe_fixtures.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/__main__.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/__init__.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/base.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/legacy_office.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/manager.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/odf.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/ooxml.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/pdf.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/sqlite.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/structured.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/text.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/alternative_backends.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/backend_conversion_worker.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/backend_probe_worker.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/backends.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/cache.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/cfb.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/cli.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/config.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/console_logging.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/cursors.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/errors.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/ids.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/installer.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/legacy.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/opencode/plugin/agent-folder-workspace.ts.tmpl +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/parser_workers.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/path_safety.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/probe_fixtures/probe.doc +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/probe_fixtures/probe.ppt +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/probe_fixtures/probe.xls +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/py.typed +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/registry.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/schemas/CapabilityReportV1.schema.json +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/schemas/ContentPageV1.schema.json +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/schemas/DiagnosticV1.schema.json +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/schemas/NodePageV1.schema.json +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/schemas/NodeV1.schema.json +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/schemas/SearchPageV1.schema.json +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/workspace.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/__init__.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/cfb_fixture.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_alternative_backends.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_backends.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_cfb.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_console_logging.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_content_adapters.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_legacy_adapters.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_legacy_fuzz.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_legacy_parsers.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_odf_pdf_adapters.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_ooxml_adapters.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_package.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_search_cache.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_structured_text.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_windows_reparse_security.py +0 -0
- {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_workspace_registry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-folder-workspace
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Offline, lazy, agent-oriented exploration of heterogeneous folders
|
|
5
5
|
Author-email: Dark Light <darklight@noreply.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -32,12 +32,12 @@ Description-Content-Type: text/markdown
|
|
|
32
32
|
|
|
33
33
|
# agent-folder-workspace
|
|
34
34
|
|
|
35
|
-
`agent-folder-workspace` presents
|
|
36
|
-
|
|
35
|
+
`agent-folder-workspace` presents local directories as stable, paginated node
|
|
36
|
+
hierarchies for agents. It discovers file metadata first and opens document
|
|
37
37
|
bodies only when a caller expands or reads a node. The default parser path is
|
|
38
38
|
local Python code: Microsoft Office and LibreOffice are not required.
|
|
39
39
|
|
|
40
|
-
> **Project status:** `0.1.
|
|
40
|
+
> **Project status:** `0.1.2` is an alpha contract. Read-only exploration and
|
|
41
41
|
> byte access are implemented; editing, rendering, OCR, and complete semantic
|
|
42
42
|
> coverage of every supported container are not.
|
|
43
43
|
|
|
@@ -92,7 +92,9 @@ adapters, and caching:
|
|
|
92
92
|
adapters and enforce one deadline across startup, queueing, IPC, and parsing.
|
|
93
93
|
5. `SizedLru` retains bounded decoded pages in memory. `ContentIndex` stores an
|
|
94
94
|
optional per-workspace SQLite search index in the application cache.
|
|
95
|
-
6.
|
|
95
|
+
6. `WorkspaceManager` gives one FastMCP process a bounded registry of active and
|
|
96
|
+
explicitly addressed workspace roots.
|
|
97
|
+
7. The Python API, Typer CLI, and FastMCP stdio server share the same workspace
|
|
96
98
|
methods and versioned response contracts.
|
|
97
99
|
|
|
98
100
|
Node IDs are deterministic hashes of relative and logical paths; they do not
|
|
@@ -131,7 +133,7 @@ archive, XML, and structural limits. It is frozen and rejects unknown fields.
|
|
|
131
133
|
The package exports a stable
|
|
132
134
|
error hierarchy (`FolderWorkspaceError`, `InputValidationError`,
|
|
133
135
|
`NodeNotFoundError`, `StaleCursorError`, and `ResourceLimitError`) and six
|
|
134
|
-
|
|
136
|
+
document wire models plus three workspace-selection models:
|
|
135
137
|
|
|
136
138
|
- `CapabilityReportV1`
|
|
137
139
|
- `ContentPageV1`
|
|
@@ -139,6 +141,9 @@ versioned wire models:
|
|
|
139
141
|
- `NodePageV1`
|
|
140
142
|
- `NodeV1`
|
|
141
143
|
- `SearchPageV1`
|
|
144
|
+
- `WorkspaceCloseV1`
|
|
145
|
+
- `WorkspaceHandleV1`
|
|
146
|
+
- `WorkspaceListV1`
|
|
142
147
|
|
|
143
148
|
Committed JSON Schemas live in
|
|
144
149
|
`src/agent_folder_workspace/schemas/`. Regenerate them with
|
|
@@ -147,7 +152,7 @@ Pydantic's `model_json_schema()` output.
|
|
|
147
152
|
|
|
148
153
|
## MCP server
|
|
149
154
|
|
|
150
|
-
Run one stdio server
|
|
155
|
+
Run one stdio server with an initial root:
|
|
151
156
|
|
|
152
157
|
```console
|
|
153
158
|
folderws mcp --root ./documents
|
|
@@ -155,6 +160,10 @@ folderws mcp --root ./documents
|
|
|
155
160
|
|
|
156
161
|
The server exposes exactly these tools:
|
|
157
162
|
|
|
163
|
+
- `open_workspace`
|
|
164
|
+
- `list_workspaces`
|
|
165
|
+
- `activate_workspace`
|
|
166
|
+
- `close_workspace`
|
|
158
167
|
- `workspace_info`
|
|
159
168
|
- `list_children`
|
|
160
169
|
- `get_node`
|
|
@@ -165,13 +174,27 @@ The server exposes exactly these tools:
|
|
|
165
174
|
- `refresh_workspace`
|
|
166
175
|
- `probe_backends`
|
|
167
176
|
|
|
177
|
+
`open_workspace(path)` accepts an explicit absolute directory, returns an opaque,
|
|
178
|
+
process-lifetime `workspace_id`, and makes that workspace active. Existing tools
|
|
179
|
+
accept an optional `workspace_id`; omission uses the active workspace. Opening
|
|
180
|
+
the same canonical root again reuses its ID. `list_workspaces`,
|
|
181
|
+
`activate_workspace`, and `close_workspace` manage the registry. One process
|
|
182
|
+
holds at most eight workspaces and never evicts one silently.
|
|
183
|
+
|
|
184
|
+
On Windows, runtime selection accepts `C:\Documents`, `C:/Documents`, UNC paths,
|
|
185
|
+
and Git Bash paths such as `/c/Documents`. Relative external paths are rejected.
|
|
186
|
+
The initial CLI root remains compatible with relative paths such as `.`.
|
|
187
|
+
|
|
168
188
|
Results use the same versioned models as the Python API. A method that is absent
|
|
169
189
|
from an injected workspace implementation returns a structured
|
|
170
190
|
`capability_unavailable` response instead of failing server construction.
|
|
191
|
+
Expected workspace/parser failures and unexpected tool exceptions become
|
|
192
|
+
structured results; a later MCP request remains available. PDF decoding remains
|
|
193
|
+
lazy and isolated in bounded parser workers.
|
|
171
194
|
|
|
172
195
|
The transport is stdio. There is no HTTP listener, authentication layer, or
|
|
173
|
-
|
|
174
|
-
permissions of the account that launched `folderws`;
|
|
196
|
+
network broker. The process and its MCP client share the operating-system
|
|
197
|
+
permissions of the account that launched `folderws`; open roots narrowly.
|
|
175
198
|
|
|
176
199
|
## CLI
|
|
177
200
|
|
|
@@ -299,6 +322,8 @@ The normal Python parser path is offline and read-only. It does not upload
|
|
|
299
322
|
documents and does not execute document macros. Important controls include:
|
|
300
323
|
|
|
301
324
|
- roots that are symbolic links or reparse points are rejected;
|
|
325
|
+
- external roots are opened only by an explicit `open_workspace` call, are
|
|
326
|
+
bounded to eight per process, and receive process-lifetime opaque IDs;
|
|
302
327
|
- child links are not followed by default, and resolved sources must remain
|
|
303
328
|
under the selected root;
|
|
304
329
|
- SQLite files are opened with `mode=ro` and `PRAGMA query_only=ON`;
|
|
@@ -385,7 +410,7 @@ The detailed coverage and per-format caveats are in
|
|
|
385
410
|
rendering. A hard parent timeout cannot guarantee cleanup of a vendor process
|
|
386
411
|
that is itself stuck below COM or LibreOffice process control.
|
|
387
412
|
- The stated 100,000-node and warm p95 performance figures are design targets,
|
|
388
|
-
not certified guarantees in `0.1.
|
|
413
|
+
not certified guarantees in `0.1.2`; CI covers correctness across Windows,
|
|
389
414
|
Linux, and macOS but does not emulate a specific Windows-x64 SSD workload.
|
|
390
415
|
|
|
391
416
|
## Development
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# agent-folder-workspace
|
|
2
2
|
|
|
3
|
-
`agent-folder-workspace` presents
|
|
4
|
-
|
|
3
|
+
`agent-folder-workspace` presents local directories as stable, paginated node
|
|
4
|
+
hierarchies for agents. It discovers file metadata first and opens document
|
|
5
5
|
bodies only when a caller expands or reads a node. The default parser path is
|
|
6
6
|
local Python code: Microsoft Office and LibreOffice are not required.
|
|
7
7
|
|
|
8
|
-
> **Project status:** `0.1.
|
|
8
|
+
> **Project status:** `0.1.2` is an alpha contract. Read-only exploration and
|
|
9
9
|
> byte access are implemented; editing, rendering, OCR, and complete semantic
|
|
10
10
|
> coverage of every supported container are not.
|
|
11
11
|
|
|
@@ -60,7 +60,9 @@ adapters, and caching:
|
|
|
60
60
|
adapters and enforce one deadline across startup, queueing, IPC, and parsing.
|
|
61
61
|
5. `SizedLru` retains bounded decoded pages in memory. `ContentIndex` stores an
|
|
62
62
|
optional per-workspace SQLite search index in the application cache.
|
|
63
|
-
6.
|
|
63
|
+
6. `WorkspaceManager` gives one FastMCP process a bounded registry of active and
|
|
64
|
+
explicitly addressed workspace roots.
|
|
65
|
+
7. The Python API, Typer CLI, and FastMCP stdio server share the same workspace
|
|
64
66
|
methods and versioned response contracts.
|
|
65
67
|
|
|
66
68
|
Node IDs are deterministic hashes of relative and logical paths; they do not
|
|
@@ -99,7 +101,7 @@ archive, XML, and structural limits. It is frozen and rejects unknown fields.
|
|
|
99
101
|
The package exports a stable
|
|
100
102
|
error hierarchy (`FolderWorkspaceError`, `InputValidationError`,
|
|
101
103
|
`NodeNotFoundError`, `StaleCursorError`, and `ResourceLimitError`) and six
|
|
102
|
-
|
|
104
|
+
document wire models plus three workspace-selection models:
|
|
103
105
|
|
|
104
106
|
- `CapabilityReportV1`
|
|
105
107
|
- `ContentPageV1`
|
|
@@ -107,6 +109,9 @@ versioned wire models:
|
|
|
107
109
|
- `NodePageV1`
|
|
108
110
|
- `NodeV1`
|
|
109
111
|
- `SearchPageV1`
|
|
112
|
+
- `WorkspaceCloseV1`
|
|
113
|
+
- `WorkspaceHandleV1`
|
|
114
|
+
- `WorkspaceListV1`
|
|
110
115
|
|
|
111
116
|
Committed JSON Schemas live in
|
|
112
117
|
`src/agent_folder_workspace/schemas/`. Regenerate them with
|
|
@@ -115,7 +120,7 @@ Pydantic's `model_json_schema()` output.
|
|
|
115
120
|
|
|
116
121
|
## MCP server
|
|
117
122
|
|
|
118
|
-
Run one stdio server
|
|
123
|
+
Run one stdio server with an initial root:
|
|
119
124
|
|
|
120
125
|
```console
|
|
121
126
|
folderws mcp --root ./documents
|
|
@@ -123,6 +128,10 @@ folderws mcp --root ./documents
|
|
|
123
128
|
|
|
124
129
|
The server exposes exactly these tools:
|
|
125
130
|
|
|
131
|
+
- `open_workspace`
|
|
132
|
+
- `list_workspaces`
|
|
133
|
+
- `activate_workspace`
|
|
134
|
+
- `close_workspace`
|
|
126
135
|
- `workspace_info`
|
|
127
136
|
- `list_children`
|
|
128
137
|
- `get_node`
|
|
@@ -133,13 +142,27 @@ The server exposes exactly these tools:
|
|
|
133
142
|
- `refresh_workspace`
|
|
134
143
|
- `probe_backends`
|
|
135
144
|
|
|
145
|
+
`open_workspace(path)` accepts an explicit absolute directory, returns an opaque,
|
|
146
|
+
process-lifetime `workspace_id`, and makes that workspace active. Existing tools
|
|
147
|
+
accept an optional `workspace_id`; omission uses the active workspace. Opening
|
|
148
|
+
the same canonical root again reuses its ID. `list_workspaces`,
|
|
149
|
+
`activate_workspace`, and `close_workspace` manage the registry. One process
|
|
150
|
+
holds at most eight workspaces and never evicts one silently.
|
|
151
|
+
|
|
152
|
+
On Windows, runtime selection accepts `C:\Documents`, `C:/Documents`, UNC paths,
|
|
153
|
+
and Git Bash paths such as `/c/Documents`. Relative external paths are rejected.
|
|
154
|
+
The initial CLI root remains compatible with relative paths such as `.`.
|
|
155
|
+
|
|
136
156
|
Results use the same versioned models as the Python API. A method that is absent
|
|
137
157
|
from an injected workspace implementation returns a structured
|
|
138
158
|
`capability_unavailable` response instead of failing server construction.
|
|
159
|
+
Expected workspace/parser failures and unexpected tool exceptions become
|
|
160
|
+
structured results; a later MCP request remains available. PDF decoding remains
|
|
161
|
+
lazy and isolated in bounded parser workers.
|
|
139
162
|
|
|
140
163
|
The transport is stdio. There is no HTTP listener, authentication layer, or
|
|
141
|
-
|
|
142
|
-
permissions of the account that launched `folderws`;
|
|
164
|
+
network broker. The process and its MCP client share the operating-system
|
|
165
|
+
permissions of the account that launched `folderws`; open roots narrowly.
|
|
143
166
|
|
|
144
167
|
## CLI
|
|
145
168
|
|
|
@@ -267,6 +290,8 @@ The normal Python parser path is offline and read-only. It does not upload
|
|
|
267
290
|
documents and does not execute document macros. Important controls include:
|
|
268
291
|
|
|
269
292
|
- roots that are symbolic links or reparse points are rejected;
|
|
293
|
+
- external roots are opened only by an explicit `open_workspace` call, are
|
|
294
|
+
bounded to eight per process, and receive process-lifetime opaque IDs;
|
|
270
295
|
- child links are not followed by default, and resolved sources must remain
|
|
271
296
|
under the selected root;
|
|
272
297
|
- SQLite files are opened with `mode=ro` and `PRAGMA query_only=ON`;
|
|
@@ -353,7 +378,7 @@ The detailed coverage and per-format caveats are in
|
|
|
353
378
|
rendering. A hard parent timeout cannot guarantee cleanup of a vendor process
|
|
354
379
|
that is itself stuck below COM or LibreOffice process control.
|
|
355
380
|
- The stated 100,000-node and warm p95 performance figures are design targets,
|
|
356
|
-
not certified guarantees in `0.1.
|
|
381
|
+
not certified guarantees in `0.1.2`; CI covers correctness across Windows,
|
|
357
382
|
Linux, and macOS but does not emulate a specific Windows-x64 SSD workload.
|
|
358
383
|
|
|
359
384
|
## Development
|
|
@@ -7,7 +7,8 @@ tests, not this prose alone, define the wire contract.
|
|
|
7
7
|
|
|
8
8
|
```mermaid
|
|
9
9
|
flowchart LR
|
|
10
|
-
Client["Python, CLI, or MCP client"] -->
|
|
10
|
+
Client["Python, CLI, or MCP client"] --> Session["WorkspaceManager"]
|
|
11
|
+
Session --> Workspace["FolderWorkspace"]
|
|
11
12
|
Workspace --> Registry["NodeRegistry metadata tree"]
|
|
12
13
|
Workspace --> Manager["AdapterManager"]
|
|
13
14
|
Manager --> Direct["Direct text, CSV, and binary paths"]
|
|
@@ -31,7 +32,7 @@ is an explicit diagnostic path.
|
|
|
31
32
|
|
|
32
33
|
`models.py` contains strict Pydantic models. Wire-level models carry
|
|
33
34
|
`schema_version="1.0"`; unknown fields are rejected and non-finite numbers are
|
|
34
|
-
not accepted. The
|
|
35
|
+
not accepted. The nine top-level exported models have committed JSON Schemas in
|
|
35
36
|
`src/agent_folder_workspace/schemas/`.
|
|
36
37
|
|
|
37
38
|
`errors.py` provides stable exception categories, and `ids.py` creates node IDs
|
|
@@ -192,9 +193,18 @@ copies.
|
|
|
192
193
|
|
|
193
194
|
The Python API returns model instances and raises the public error hierarchy.
|
|
194
195
|
The FastMCP server serializes model responses with `model_dump(mode="json")`
|
|
195
|
-
and exposes a fixed
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
and exposes a fixed thirteen-tool stdio surface. `WorkspaceManager` owns at
|
|
197
|
+
most eight roots, opaque process-lifetime IDs, canonical-root deduplication,
|
|
198
|
+
active selection, and deterministic cleanup. Existing tools accept an optional
|
|
199
|
+
workspace ID and otherwise route to the active root. Its capability dispatcher
|
|
200
|
+
resolves workspace methods at call time so reduced injected implementations
|
|
201
|
+
return a structured `capability_unavailable` error.
|
|
202
|
+
|
|
203
|
+
MCP tool boundaries translate public workspace errors to stable error codes and
|
|
204
|
+
sanitize unexpected failures behind a correlation ID. This leaves the stdio
|
|
205
|
+
transport usable for a later request. Risky PDF and container semantics remain
|
|
206
|
+
inside parser workers; opening or listing a filesystem root does not decode
|
|
207
|
+
their payloads.
|
|
198
208
|
|
|
199
209
|
The Typer CLI provides MCP serving, backend diagnostics, indexing, cache
|
|
200
210
|
clearing, and managed OpenCode integration installation. OpenCode resources are
|
|
@@ -218,8 +228,10 @@ enterprise policy, add-in, crash, or vendor defect.
|
|
|
218
228
|
|
|
219
229
|
## Security boundaries
|
|
220
230
|
|
|
221
|
-
-
|
|
222
|
-
implement per-node authorization.
|
|
231
|
+
- Every selected root is a separate filesystem authority boundary; the library
|
|
232
|
+
does not implement per-node authorization.
|
|
233
|
+
- Runtime roots require an explicit absolute path. Windows native, slash-style,
|
|
234
|
+
UNC, and Git Bash drive paths normalize before registry deduplication.
|
|
223
235
|
- The default MCP transport is local stdio; operating-system permissions govern
|
|
224
236
|
the process and client.
|
|
225
237
|
- `read_binary` intentionally reveals file bytes inside the root.
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# Runtime Workspace Selection Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Let one OpenCode MCP process open, identify, activate, route to, and close multiple local folder workspaces while containing PDF/tool failures without terminating the transport.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Add a focused `WorkspaceManager` between FastMCP handlers and existing `FolderWorkspace` objects. The manager owns opaque session IDs, canonical-root deduplication, the active workspace, bounded lifecycle, and Windows/Git-Bash path normalization; document parsing remains unchanged behind existing parser workers. FastMCP tools route through the manager and convert expected or unexpected operation failures into stable JSON payloads so a later request remains usable.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Python 3.11-3.14, Pydantic v2 contracts, FastMCP stdio, pytest, pypdf/pdfplumber, Ruff, strict Mypy.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
### Task 1: Versioned workspace-selection contracts
|
|
14
|
+
|
|
15
|
+
**Files:**
|
|
16
|
+
- Modify: `src/agent_folder_workspace/models.py`
|
|
17
|
+
- Modify: `src/agent_folder_workspace/__init__.py`
|
|
18
|
+
- Create: `src/agent_folder_workspace/schemas/WorkspaceHandleV1.schema.json`
|
|
19
|
+
- Create: `src/agent_folder_workspace/schemas/WorkspaceListV1.schema.json`
|
|
20
|
+
- Create: `src/agent_folder_workspace/schemas/WorkspaceCloseV1.schema.json`
|
|
21
|
+
- Modify: `tests/test_contracts.py`
|
|
22
|
+
- Modify: `tests/test_distribution.py`
|
|
23
|
+
|
|
24
|
+
- [ ] **Step 1: Write failing model-contract tests**
|
|
25
|
+
|
|
26
|
+
Add tests that validate an opaque workspace handle, a list response with one active ID, and a close
|
|
27
|
+
response:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
handle = afw.WorkspaceHandleV1(
|
|
31
|
+
workspace_id="ws_0123456789abcdef0123456789abcdef",
|
|
32
|
+
root="/documents",
|
|
33
|
+
active=True,
|
|
34
|
+
workspace_info=workspace_info,
|
|
35
|
+
)
|
|
36
|
+
listing = afw.WorkspaceListV1(
|
|
37
|
+
active_workspace_id=handle.workspace_id,
|
|
38
|
+
workspaces=[handle],
|
|
39
|
+
)
|
|
40
|
+
closed = afw.WorkspaceCloseV1(
|
|
41
|
+
closed_workspace_id="ws_abcdefabcdefabcdefabcdefabcdefab",
|
|
42
|
+
active_workspace_id=handle.workspace_id,
|
|
43
|
+
)
|
|
44
|
+
assert listing.workspaces[0].workspace_info == workspace_info
|
|
45
|
+
assert closed.active_workspace_id == handle.workspace_id
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Extend `PUBLIC_SCHEMAS` with all three models.
|
|
49
|
+
|
|
50
|
+
- [ ] **Step 2: Run the focused tests and confirm missing models**
|
|
51
|
+
|
|
52
|
+
Run: `python3 -m pytest tests/test_contracts.py tests/test_distribution.py::test_committed_json_schemas_match_the_public_v1_contracts -q`
|
|
53
|
+
|
|
54
|
+
Expected: failure because `WorkspaceHandleV1`, `WorkspaceListV1`, and `WorkspaceCloseV1` do not exist.
|
|
55
|
+
|
|
56
|
+
- [ ] **Step 3: Add strict Pydantic contracts and public exports**
|
|
57
|
+
|
|
58
|
+
Add:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
class WorkspaceHandleV1(ContractModel):
|
|
62
|
+
schema_version: Literal["1.0"] = "1.0"
|
|
63
|
+
workspace_id: str = Field(pattern=r"^ws_[0-9a-f]{32}$")
|
|
64
|
+
root: str = Field(min_length=1, max_length=32_768)
|
|
65
|
+
active: bool
|
|
66
|
+
workspace_info: WorkspaceInfoV1
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class WorkspaceListV1(ContractModel):
|
|
70
|
+
schema_version: Literal["1.0"] = "1.0"
|
|
71
|
+
active_workspace_id: str = Field(pattern=r"^ws_[0-9a-f]{32}$")
|
|
72
|
+
workspaces: list[WorkspaceHandleV1]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class WorkspaceCloseV1(ContractModel):
|
|
76
|
+
schema_version: Literal["1.0"] = "1.0"
|
|
77
|
+
closed_workspace_id: str = Field(pattern=r"^ws_[0-9a-f]{32}$")
|
|
78
|
+
active_workspace_id: str = Field(pattern=r"^ws_[0-9a-f]{32}$")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Export the types from `agent_folder_workspace.__init__`.
|
|
82
|
+
|
|
83
|
+
- [ ] **Step 4: Generate schemas and run focused tests**
|
|
84
|
+
|
|
85
|
+
Run: `python3 scripts/generate_json_schemas.py`
|
|
86
|
+
|
|
87
|
+
Run: `python3 -m pytest tests/test_contracts.py tests/test_distribution.py::test_committed_json_schemas_match_the_public_v1_contracts -q`
|
|
88
|
+
|
|
89
|
+
Expected: all selected tests pass.
|
|
90
|
+
|
|
91
|
+
- [ ] **Step 5: Commit the contract slice**
|
|
92
|
+
|
|
93
|
+
Stage only the model, export, schema, and selected test files. Commit through the repository commit
|
|
94
|
+
policy with title `Add workspace selection contracts`.
|
|
95
|
+
|
|
96
|
+
### Task 2: Bounded workspace manager and Windows path normalization
|
|
97
|
+
|
|
98
|
+
**Files:**
|
|
99
|
+
- Create: `src/agent_folder_workspace/workspace_manager.py`
|
|
100
|
+
- Create: `tests/test_workspace_manager.py`
|
|
101
|
+
|
|
102
|
+
- [ ] **Step 1: Write failing manager lifecycle and routing tests**
|
|
103
|
+
|
|
104
|
+
Cover initial registration, same-root deduplication, active selection, explicit lookup, close
|
|
105
|
+
fallback, final-workspace refusal, eight-workspace limit, partial-open cleanup, and idempotent
|
|
106
|
+
manager close. Use a fake factory whose workspace records `close()` calls.
|
|
107
|
+
|
|
108
|
+
The public manager interface used by tests is:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
manager = WorkspaceManager(initial_root, initial_workspace=fake, factory=factory)
|
|
112
|
+
opened = manager.open_workspace(other_root)
|
|
113
|
+
assert manager.active_workspace_id == opened.workspace_id
|
|
114
|
+
assert manager.workspace(opened.workspace_id) is factory.created[other_root]
|
|
115
|
+
assert manager.workspace() is factory.created[other_root]
|
|
116
|
+
assert manager.close_workspace(opened.workspace_id).active_workspace_id != opened.workspace_id
|
|
117
|
+
manager.close()
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
- [ ] **Step 2: Write failing Windows/Git-Bash path tests**
|
|
121
|
+
|
|
122
|
+
Test the pure helper without changing the host OS:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
assert normalize_workspace_path_text(r"C:\Docs\PDF", windows=True) == r"C:\Docs\PDF"
|
|
126
|
+
assert normalize_workspace_path_text("C:/Docs/PDF", windows=True) == r"C:\Docs\PDF"
|
|
127
|
+
assert normalize_workspace_path_text("/c/Docs/PDF", windows=True) == r"C:\Docs\PDF"
|
|
128
|
+
assert normalize_workspace_path_text("//server/share/PDF", windows=True) == r"\\server\share\PDF"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Also prove that relative paths are rejected before the factory is called. Node IDs remain
|
|
132
|
+
deterministic per relative/logical path; explicit workspace IDs disambiguate equivalent trees,
|
|
133
|
+
while signed cursors fail when routed to another workspace.
|
|
134
|
+
|
|
135
|
+
- [ ] **Step 3: Run tests and confirm the module is missing**
|
|
136
|
+
|
|
137
|
+
Run: `python3 -m pytest tests/test_workspace_manager.py -q`
|
|
138
|
+
|
|
139
|
+
Expected: collection failure because `workspace_manager` does not exist.
|
|
140
|
+
|
|
141
|
+
- [ ] **Step 4: Implement `WorkspaceManager`**
|
|
142
|
+
|
|
143
|
+
Use a private entry dataclass and two indexes:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
@dataclass(slots=True)
|
|
147
|
+
class _WorkspaceEntry:
|
|
148
|
+
workspace_id: str
|
|
149
|
+
root: Path
|
|
150
|
+
workspace: object
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class WorkspaceManager:
|
|
154
|
+
def __init__(
|
|
155
|
+
self,
|
|
156
|
+
root: str | Path,
|
|
157
|
+
*,
|
|
158
|
+
initial_workspace: object | None = None,
|
|
159
|
+
factory: Callable[[str | Path], object] = FolderWorkspace.open,
|
|
160
|
+
max_workspaces: int = 8,
|
|
161
|
+
) -> None: ...
|
|
162
|
+
|
|
163
|
+
def open_workspace(self, path: str | Path) -> WorkspaceHandleV1: ...
|
|
164
|
+
def list_workspaces(self) -> WorkspaceListV1: ...
|
|
165
|
+
def activate_workspace(self, workspace_id: str) -> WorkspaceHandleV1: ...
|
|
166
|
+
def close_workspace(self, workspace_id: str) -> WorkspaceCloseV1: ...
|
|
167
|
+
def workspace(self, workspace_id: str | None = None) -> object: ...
|
|
168
|
+
def close(self) -> None: ...
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Generate IDs with `f"ws_{uuid.uuid4().hex}"`. Canonicalize through the successfully opened
|
|
172
|
+
workspace's `.root` when available, otherwise through the validated absolute request. Update
|
|
173
|
+
recency only after successful operations, never evict silently, and preserve the current active ID
|
|
174
|
+
when an open attempt fails.
|
|
175
|
+
|
|
176
|
+
- [ ] **Step 5: Run manager tests**
|
|
177
|
+
|
|
178
|
+
Run: `python3 -m pytest tests/test_workspace_manager.py -q`
|
|
179
|
+
|
|
180
|
+
Expected: all manager tests pass.
|
|
181
|
+
|
|
182
|
+
- [ ] **Step 6: Commit the manager slice**
|
|
183
|
+
|
|
184
|
+
Stage `workspace_manager.py` and `test_workspace_manager.py`. Commit through the policy with title
|
|
185
|
+
`Manage multiple folder workspaces`.
|
|
186
|
+
|
|
187
|
+
### Task 3: MCP routing and failure containment
|
|
188
|
+
|
|
189
|
+
**Files:**
|
|
190
|
+
- Modify: `src/agent_folder_workspace/mcp_server.py`
|
|
191
|
+
- Modify: `tests/test_mcp_cli_installer.py`
|
|
192
|
+
|
|
193
|
+
- [ ] **Step 1: Write failing public-tool and routing tests**
|
|
194
|
+
|
|
195
|
+
Update the expected tool set with:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
{
|
|
199
|
+
"open_workspace",
|
|
200
|
+
"list_workspaces",
|
|
201
|
+
"activate_workspace",
|
|
202
|
+
"close_workspace",
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Call `open_workspace` on a second temporary directory, capture its ID, verify omitted IDs use it,
|
|
207
|
+
then pass the initial ID explicitly and verify the call reaches the initial fake workspace.
|
|
208
|
+
Exercise optional `workspace_id` on `workspace_info`, `list_children`, `get_node`, `read_content`,
|
|
209
|
+
`read_binary`, `search_content`, `index_content`, and `refresh_workspace`.
|
|
210
|
+
|
|
211
|
+
- [ ] **Step 2: Write failing structured-error keepalive tests**
|
|
212
|
+
|
|
213
|
+
Use a fake workspace that raises `InputValidationError("bad PDF")`, then one that raises
|
|
214
|
+
`RuntimeError("parser exploded")`. Assert:
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
assert result["ok"] is False
|
|
218
|
+
assert result["error"]["code"] in {"input_validation_error", "internal_error"}
|
|
219
|
+
assert _call_tool(server, "workspace_info", {})["scan_state"] == "ready"
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Verify messages are sanitized and no traceback or document content enters the result.
|
|
223
|
+
|
|
224
|
+
- [ ] **Step 3: Run focused MCP tests and confirm failures**
|
|
225
|
+
|
|
226
|
+
Run: `python3 -m pytest tests/test_mcp_cli_installer.py -q`
|
|
227
|
+
|
|
228
|
+
Expected: failures for the missing tools, missing optional IDs, and uncontained errors.
|
|
229
|
+
|
|
230
|
+
- [ ] **Step 4: Route FastMCP handlers through `WorkspaceManager`**
|
|
231
|
+
|
|
232
|
+
Construct one manager in `create_mcp_server`, register four management tools, and add
|
|
233
|
+
`workspace_id: str | None = None` as the final parameter of every workspace-specific existing
|
|
234
|
+
tool. Resolve with:
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
selected = manager.workspace(workspace_id)
|
|
238
|
+
return call_workspace_capability(selected, capability, *args, **kwargs)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Keep `probe_backends` global. Attach the manager to the server for deterministic ownership tests,
|
|
242
|
+
and make `run_mcp` close the manager exactly once in `finally`.
|
|
243
|
+
|
|
244
|
+
- [ ] **Step 5: Add stable error payloads**
|
|
245
|
+
|
|
246
|
+
Add a deterministic helper and wrap capability invocation:
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
def _error_code(error: FolderWorkspaceError) -> str:
|
|
250
|
+
name = type(error).__name__.removesuffix("Error")
|
|
251
|
+
return re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower() + "_error"
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
return _jsonable(method(*args, **kwargs))
|
|
256
|
+
except FolderWorkspaceError as error:
|
|
257
|
+
return {
|
|
258
|
+
"ok": False,
|
|
259
|
+
"error": {
|
|
260
|
+
"code": _error_code(error),
|
|
261
|
+
"message": redact_text(error),
|
|
262
|
+
},
|
|
263
|
+
}
|
|
264
|
+
except Exception as error:
|
|
265
|
+
correlation_id = uuid.uuid4().hex
|
|
266
|
+
logger.exception("mcp tool failed correlation_id=%s", correlation_id)
|
|
267
|
+
return {
|
|
268
|
+
"ok": False,
|
|
269
|
+
"error": {
|
|
270
|
+
"code": "internal_error",
|
|
271
|
+
"message": "Unexpected workspace failure",
|
|
272
|
+
"correlation_id": correlation_id,
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Apply the same boundary to manager tools. Never catch `BaseException`, so normal process
|
|
278
|
+
termination and cancellation semantics remain intact.
|
|
279
|
+
|
|
280
|
+
- [ ] **Step 6: Run focused MCP and manager tests**
|
|
281
|
+
|
|
282
|
+
Run: `python3 -m pytest tests/test_mcp_cli_installer.py tests/test_workspace_manager.py -q`
|
|
283
|
+
|
|
284
|
+
Expected: all selected tests pass.
|
|
285
|
+
|
|
286
|
+
- [ ] **Step 7: Commit the MCP slice**
|
|
287
|
+
|
|
288
|
+
Stage `mcp_server.py` and `test_mcp_cli_installer.py`. Commit through the policy with title
|
|
289
|
+
`Route MCP tools by workspace ID`.
|
|
290
|
+
|
|
291
|
+
### Task 4: Single-PDF MCP regression and parser survival
|
|
292
|
+
|
|
293
|
+
**Files:**
|
|
294
|
+
- Create: `tests/test_mcp_pdf_survival.py`
|
|
295
|
+
- Modify: `tests/test_parser_workers.py`
|
|
296
|
+
- Modify: `src/agent_folder_workspace/adapters/pdf.py` only if a new unhandled parser exception is proven
|
|
297
|
+
- Modify: `src/agent_folder_workspace/parser_workers.py` only if a worker-restart test proves a lifecycle defect
|
|
298
|
+
|
|
299
|
+
- [ ] **Step 1: Add a valid single-PDF MCP test**
|
|
300
|
+
|
|
301
|
+
Create a one-page PDF with `PdfWriter`, open its directory through `open_workspace`, list the root,
|
|
302
|
+
expand the PDF, read a bounded page, and then call `workspace_info` again. Assert every response is
|
|
303
|
+
successful and the server remains usable.
|
|
304
|
+
|
|
305
|
+
- [ ] **Step 2: Add malformed/encrypted/oversized PDF keepalive tests**
|
|
306
|
+
|
|
307
|
+
Parameterize fixtures for `b""`, a truncated `%PDF-` body, encrypted `PdfWriter` output, and a PDF
|
|
308
|
+
larger than a deliberately small `max_content_cache_bytes`. For each fixture, trigger the PDF
|
|
309
|
+
hierarchy read, assert a structured error or locked diagnostic, and make a successful
|
|
310
|
+
`workspace_info` call on the same server afterward.
|
|
311
|
+
|
|
312
|
+
- [ ] **Step 3: Add parser exception and abnormal-exit survival tests**
|
|
313
|
+
|
|
314
|
+
Inject a worker target that returns an unexpected remote exception and one that exits before
|
|
315
|
+
replying. Assert `ParserWorkerFailure`, verify a replacement worker can complete a later request,
|
|
316
|
+
and confirm closing the pool is bounded and idempotent.
|
|
317
|
+
|
|
318
|
+
- [ ] **Step 4: Run the regression tests**
|
|
319
|
+
|
|
320
|
+
Run: `python3 -m pytest tests/test_mcp_pdf_survival.py tests/test_parser_workers.py -q`
|
|
321
|
+
|
|
322
|
+
Expected: all survival tests pass. If a failure proves an adapter or worker defect, make the
|
|
323
|
+
smallest production change in the listed file and rerun this command.
|
|
324
|
+
|
|
325
|
+
- [ ] **Step 5: Commit the PDF survival slice**
|
|
326
|
+
|
|
327
|
+
Stage the new tests and only production files changed by a proven failing test. Commit through the
|
|
328
|
+
policy with title `Keep MCP alive after PDF failures`.
|
|
329
|
+
|
|
330
|
+
### Task 5: OpenCode guidance, documentation, and release gates
|
|
331
|
+
|
|
332
|
+
**Files:**
|
|
333
|
+
- Modify: `src/agent_folder_workspace/opencode/skill/SKILL.md`
|
|
334
|
+
- Modify: `README.md`
|
|
335
|
+
- Modify: `docs/architecture.md`
|
|
336
|
+
- Modify: `docs/windows.md`
|
|
337
|
+
- Modify: `tests/test_mcp_cli_installer.py`
|
|
338
|
+
- Modify: `tests/test_distribution.py`
|
|
339
|
+
|
|
340
|
+
- [ ] **Step 1: Add failing installed-resource assertions**
|
|
341
|
+
|
|
342
|
+
Assert the generated skill directs the model to call `open_workspace` for a user-approved external
|
|
343
|
+
absolute path, retain `workspace_id`, use the active default for concise calls, and close unused
|
|
344
|
+
workspaces. Assert README/distribution payloads contain the runtime-selection documentation.
|
|
345
|
+
|
|
346
|
+
- [ ] **Step 2: Update the bundled skill and documentation**
|
|
347
|
+
|
|
348
|
+
Document the four management tools, optional workspace IDs, eight-workspace bound, ID lifetime,
|
|
349
|
+
Git Bash path form, root security, and PDF error behavior. Retain the instruction to call
|
|
350
|
+
`workspace_info` first when no external root is requested.
|
|
351
|
+
|
|
352
|
+
- [ ] **Step 3: Run documentation/resource tests**
|
|
353
|
+
|
|
354
|
+
Run: `python3 -m pytest tests/test_mcp_cli_installer.py tests/test_distribution.py -q`
|
|
355
|
+
|
|
356
|
+
Expected: all selected tests pass.
|
|
357
|
+
|
|
358
|
+
- [ ] **Step 4: Run all quality gates**
|
|
359
|
+
|
|
360
|
+
Run:
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
python3 scripts/generate_json_schemas.py --check
|
|
364
|
+
python3 -m ruff check .
|
|
365
|
+
python3 -m mypy
|
|
366
|
+
python3 -m pytest
|
|
367
|
+
SOURCE_DATE_EPOCH=315532800 python3 -m build --no-isolation
|
|
368
|
+
python3 scripts/check_wheel.py dist --install-smoke
|
|
369
|
+
python3 scripts/check_wheel.py --reproducible
|
|
370
|
+
git diff --check
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Expected: every command exits zero.
|
|
374
|
+
|
|
375
|
+
- [ ] **Step 5: Record the Windows acceptance requirement**
|
|
376
|
+
|
|
377
|
+
Update the Beads child issue with the exact Git Bash/OpenCode harness command and mark the remote
|
|
378
|
+
Windows observation as pending until it runs on Windows. Do not claim the original field failure is
|
|
379
|
+
reproduced or fixed solely from macOS tests.
|
|
380
|
+
|
|
381
|
+
- [ ] **Step 6: Commit the documentation and verified release slice**
|
|
382
|
+
|
|
383
|
+
Stage only the skill, documentation, test, schema, and necessary metadata files. Commit through the
|
|
384
|
+
policy with title `Document runtime workspace selection`.
|