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.
Files changed (92) hide show
  1. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/PKG-INFO +35 -10
  2. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/README.md +34 -9
  3. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/docs/architecture.md +19 -7
  4. agent_folder_workspace-0.1.2/docs/superpowers/plans/2026-07-23-runtime-workspace-selection.md +384 -0
  5. agent_folder_workspace-0.1.2/docs/superpowers/specs/2026-07-23-runtime-workspace-selection-design.md +141 -0
  6. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/docs/windows.md +21 -0
  7. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/pyproject.toml +1 -1
  8. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/scripts/check_wheel.py +1 -1
  9. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/__init__.py +7 -1
  10. agent_folder_workspace-0.1.2/src/agent_folder_workspace/mcp_server.py +304 -0
  11. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/models.py +20 -0
  12. agent_folder_workspace-0.1.2/src/agent_folder_workspace/opencode/skill/SKILL.md +30 -0
  13. agent_folder_workspace-0.1.2/src/agent_folder_workspace/schemas/WorkspaceCloseV1.schema.json +27 -0
  14. agent_folder_workspace-0.1.2/src/agent_folder_workspace/schemas/WorkspaceHandleV1.schema.json +509 -0
  15. agent_folder_workspace-0.1.2/src/agent_folder_workspace/schemas/WorkspaceListV1.schema.json +538 -0
  16. agent_folder_workspace-0.1.2/src/agent_folder_workspace/workspace_manager.py +234 -0
  17. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_contracts.py +50 -0
  18. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_distribution.py +12 -2
  19. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_mcp_cli_installer.py +145 -4
  20. agent_folder_workspace-0.1.2/tests/test_mcp_pdf_survival.py +195 -0
  21. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_parser_workers.py +1 -0
  22. agent_folder_workspace-0.1.2/tests/test_workspace_manager.py +165 -0
  23. agent_folder_workspace-0.1.1/src/agent_folder_workspace/mcp_server.py +0 -179
  24. agent_folder_workspace-0.1.1/src/agent_folder_workspace/opencode/skill/SKILL.md +0 -24
  25. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/.github/workflows/ci.yml +0 -0
  26. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/.gitignore +0 -0
  27. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/CONTRIBUTING.md +0 -0
  28. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/LICENSE +0 -0
  29. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/SECURITY.md +0 -0
  30. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/docs/acceptance.md +0 -0
  31. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/docs/formats.md +0 -0
  32. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/docs/superpowers/plans/2026-07-22-console-logging.md +0 -0
  33. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/docs/superpowers/specs/2026-07-22-console-logging-design.md +0 -0
  34. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/scripts/generate_json_schemas.py +0 -0
  35. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/scripts/generate_probe_fixtures.py +0 -0
  36. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/__main__.py +0 -0
  37. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/__init__.py +0 -0
  38. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/base.py +0 -0
  39. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/legacy_office.py +0 -0
  40. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/manager.py +0 -0
  41. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/odf.py +0 -0
  42. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/ooxml.py +0 -0
  43. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/pdf.py +0 -0
  44. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/sqlite.py +0 -0
  45. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/structured.py +0 -0
  46. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/adapters/text.py +0 -0
  47. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/alternative_backends.py +0 -0
  48. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/backend_conversion_worker.py +0 -0
  49. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/backend_probe_worker.py +0 -0
  50. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/backends.py +0 -0
  51. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/cache.py +0 -0
  52. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/cfb.py +0 -0
  53. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/cli.py +0 -0
  54. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/config.py +0 -0
  55. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/console_logging.py +0 -0
  56. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/cursors.py +0 -0
  57. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/errors.py +0 -0
  58. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/ids.py +0 -0
  59. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/installer.py +0 -0
  60. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/legacy.py +0 -0
  61. {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
  62. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/parser_workers.py +0 -0
  63. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/path_safety.py +0 -0
  64. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/probe_fixtures/probe.doc +0 -0
  65. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/probe_fixtures/probe.ppt +0 -0
  66. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/probe_fixtures/probe.xls +0 -0
  67. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/py.typed +0 -0
  68. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/registry.py +0 -0
  69. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/schemas/CapabilityReportV1.schema.json +0 -0
  70. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/schemas/ContentPageV1.schema.json +0 -0
  71. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/schemas/DiagnosticV1.schema.json +0 -0
  72. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/schemas/NodePageV1.schema.json +0 -0
  73. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/schemas/NodeV1.schema.json +0 -0
  74. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/schemas/SearchPageV1.schema.json +0 -0
  75. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/src/agent_folder_workspace/workspace.py +0 -0
  76. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/__init__.py +0 -0
  77. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/cfb_fixture.py +0 -0
  78. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_alternative_backends.py +0 -0
  79. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_backends.py +0 -0
  80. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_cfb.py +0 -0
  81. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_console_logging.py +0 -0
  82. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_content_adapters.py +0 -0
  83. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_legacy_adapters.py +0 -0
  84. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_legacy_fuzz.py +0 -0
  85. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_legacy_parsers.py +0 -0
  86. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_odf_pdf_adapters.py +0 -0
  87. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_ooxml_adapters.py +0 -0
  88. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_package.py +0 -0
  89. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_search_cache.py +0 -0
  90. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_structured_text.py +0 -0
  91. {agent_folder_workspace-0.1.1 → agent_folder_workspace-0.1.2}/tests/test_windows_reparse_security.py +0 -0
  92. {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.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 one local directory as a stable, paginated
36
- node hierarchy for agents. It discovers file metadata first and opens document
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.1` is an alpha contract. Read-only exploration and
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. The Python API, Typer CLI, and FastMCP stdio server share the same workspace
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
- versioned wire models:
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 for one root:
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
- multi-root broker. The process and its MCP client share the operating-system
174
- permissions of the account that launched `folderws`; choose the root narrowly.
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.1`; CI covers correctness across Windows,
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 one local directory as a stable, paginated
4
- node hierarchy for agents. It discovers file metadata first and opens document
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.1` is an alpha contract. Read-only exploration and
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. The Python API, Typer CLI, and FastMCP stdio server share the same workspace
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
- versioned wire models:
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 for one root:
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
- multi-root broker. The process and its MCP client share the operating-system
142
- permissions of the account that launched `folderws`; choose the root narrowly.
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.1`; CI covers correctness across Windows,
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"] --> Workspace["FolderWorkspace"]
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 six top-level exported models have committed JSON Schemas in
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 nine-tool stdio surface. Its capability dispatcher resolves
196
- workspace methods at call time so reduced injected implementations return a
197
- structured `capability_unavailable` error.
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
- - The selected root is the filesystem authority boundary; the library does not
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`.