proteworks 0.1.9__tar.gz → 0.1.11__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 (85) hide show
  1. {proteworks-0.1.9 → proteworks-0.1.11}/PKG-INFO +29 -8
  2. {proteworks-0.1.9 → proteworks-0.1.11}/README.md +28 -7
  3. {proteworks-0.1.9 → proteworks-0.1.11}/pyproject.toml +4 -1
  4. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/__init__.py +2 -2
  5. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/api.py +96 -3
  6. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/auth.py +63 -18
  7. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/cli.py +154 -3
  8. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/config.py +67 -15
  9. proteworks-0.1.11/src/proteworks/executions.py +105 -0
  10. proteworks-0.1.11/src/proteworks/mcp/profile.py +78 -0
  11. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/mcp/server.py +16 -14
  12. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/mcp/tools_files.py +64 -9
  13. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/mcp/tools_orgs.py +23 -3
  14. proteworks-0.1.11/src/proteworks/mcp/tools_skills.py +60 -0
  15. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/mcp/tools_studio.py +3 -0
  16. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/mcp/tools_workflows.py +101 -4
  17. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/mcp_host.py +14 -1
  18. proteworks-0.1.11/src/proteworks/recycle.py +270 -0
  19. proteworks-0.1.11/src/proteworks/redact.py +97 -0
  20. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/session.py +5 -0
  21. proteworks-0.1.11/src/proteworks/skills/protein-studio-molx/SKILL.md +155 -0
  22. proteworks-0.1.11/src/proteworks/skills/protein-studio-molx/kernel.py +163 -0
  23. proteworks-0.1.11/src/proteworks/skills/protein-studio-molx/scripts/validate_molx.js +42 -0
  24. proteworks-0.1.11/src/proteworks/skills/proteworks-files/SKILL.md +52 -0
  25. proteworks-0.1.11/src/proteworks/skills/proteworks-studio/SKILL.md +35 -0
  26. proteworks-0.1.11/src/proteworks/skills/proteworks-workflow-msa-generation/SKILL.md +28 -0
  27. proteworks-0.1.11/src/proteworks/skills/proteworks-workflow-proteinmpnn/SKILL.md +28 -0
  28. proteworks-0.1.11/src/proteworks/skills/proteworks-workflow-refolding/SKILL.md +58 -0
  29. proteworks-0.1.11/src/proteworks/skills/proteworks-workflow-rf-diffusion3/SKILL.md +31 -0
  30. proteworks-0.1.11/src/proteworks/skills/proteworks-workflow-rfd4-proteina/SKILL.md +86 -0
  31. proteworks-0.1.11/src/proteworks/skills/proteworks-workflow-structure-prediction/SKILL.md +51 -0
  32. proteworks-0.1.11/src/proteworks/skills/proteworks-workflows/SKILL.md +64 -0
  33. proteworks-0.1.11/src/proteworks/skills_catalog.py +201 -0
  34. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/studio/kinds/rfd4_proteina.py +14 -0
  35. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/workflows.py +353 -17
  36. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks.egg-info/PKG-INFO +29 -8
  37. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks.egg-info/SOURCES.txt +20 -0
  38. {proteworks-0.1.9 → proteworks-0.1.11}/tests/test_cli.py +300 -23
  39. proteworks-0.1.11/tests/test_executions.py +176 -0
  40. {proteworks-0.1.9 → proteworks-0.1.11}/tests/test_mcp_adversarial.py +2 -0
  41. {proteworks-0.1.9 → proteworks-0.1.11}/tests/test_mcp_files.py +70 -1
  42. {proteworks-0.1.9 → proteworks-0.1.11}/tests/test_mcp_host.py +2 -0
  43. {proteworks-0.1.9 → proteworks-0.1.11}/tests/test_mcp_studio.py +29 -0
  44. {proteworks-0.1.9 → proteworks-0.1.11}/tests/test_mcp_workflows.py +179 -3
  45. proteworks-0.1.11/tests/test_recycle.py +123 -0
  46. proteworks-0.1.11/tests/test_redact.py +31 -0
  47. proteworks-0.1.11/tests/test_rfd4_proteina_workflow.py +231 -0
  48. {proteworks-0.1.9 → proteworks-0.1.11}/tests/test_session.py +5 -1
  49. proteworks-0.1.11/tests/test_skills_catalog.py +120 -0
  50. {proteworks-0.1.9 → proteworks-0.1.11}/tests/test_token_store.py +31 -5
  51. proteworks-0.1.9/src/proteworks/mcp/profile.py +0 -30
  52. proteworks-0.1.9/src/proteworks/redact.py +0 -47
  53. proteworks-0.1.9/tests/test_rfd4_proteina_workflow.py +0 -57
  54. {proteworks-0.1.9 → proteworks-0.1.11}/setup.cfg +0 -0
  55. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/browse.py +0 -0
  56. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/folders.py +0 -0
  57. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/index.py +0 -0
  58. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/mcp/__init__.py +0 -0
  59. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/mcp/errors.py +0 -0
  60. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/mcp/paths.py +0 -0
  61. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/mcp/runtime.py +0 -0
  62. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/orgs.py +0 -0
  63. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/pull.py +0 -0
  64. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/push.py +0 -0
  65. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/results.py +0 -0
  66. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/studio/__init__.py +0 -0
  67. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/studio/common.py +0 -0
  68. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/studio/kinds/__init__.py +0 -0
  69. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/studio/kinds/base.py +0 -0
  70. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/studio/kinds/proteinmpnn.py +0 -0
  71. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/studio/kinds/refolding.py +0 -0
  72. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/studio/kinds/rfd3.py +0 -0
  73. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/studio/kinds/structure_prediction.py +0 -0
  74. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/studio/registry.py +0 -0
  75. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks/studio_proteinmpnn.py +0 -0
  76. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks.egg-info/dependency_links.txt +0 -0
  77. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks.egg-info/entry_points.txt +0 -0
  78. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks.egg-info/requires.txt +0 -0
  79. {proteworks-0.1.9 → proteworks-0.1.11}/src/proteworks.egg-info/top_level.txt +0 -0
  80. {proteworks-0.1.9 → proteworks-0.1.11}/tests/test_browse.py +0 -0
  81. {proteworks-0.1.9 → proteworks-0.1.11}/tests/test_config_dir.py +0 -0
  82. {proteworks-0.1.9 → proteworks-0.1.11}/tests/test_folders.py +0 -0
  83. {proteworks-0.1.9 → proteworks-0.1.11}/tests/test_mcp_errors.py +0 -0
  84. {proteworks-0.1.9 → proteworks-0.1.11}/tests/test_mcp_paths.py +0 -0
  85. {proteworks-0.1.9 → proteworks-0.1.11}/tests/test_transfer_result.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proteworks
3
- Version: 0.1.9
3
+ Version: 0.1.11
4
4
  Summary: Sync files with Proteworks using file-browser paths.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -52,6 +52,8 @@ proteworks mcp setup-claude-science
52
52
  proteworks mcp doctor
53
53
  ```
54
54
 
55
+ To switch accounts: `proteworks login --force` (clears the local token and the browser Auth0 session, then opens the sign-in UI). `proteworks logout` only forgets the local token.
56
+
55
57
  After any upgrade: run step 2 again (same `install-system` command), then `doctor`. Do not `uv tool install` again unless `/usr/local/bin/proteworks` is missing.
56
58
 
57
59
  ### Claude Science connector
@@ -99,20 +101,37 @@ proteworks push ./1UBQ.pdb '~/My Files/test/'
99
101
  proteworks push '*.pdb' '~/My Files/pdbs/'
100
102
  proteworks push designs.zip '~/My Files/dest/' # auto-extracts
101
103
  proteworks mkdir '~/My Files/campaigns/round6/'
104
+ proteworks recycle '~/My Files/campaigns/old-run/' # soft delete → recycle bin
102
105
  ```
103
106
 
104
- MCP also exposes `create_folder` for the same empty-directory create (returns `folder_id` for `output_folder_id` / push).
107
+ MCP also exposes `create_folder` for the same empty-directory create (returns `folder_id` for `output_folder_id` / push), and `move_to_recycle_bin` for soft-deleting a file, folder/, or wildcard (dry_run by default; set `confirm=true` to execute).
105
108
 
106
109
  Login returns to `http://127.0.0.1:8765/callback`. Add that exact URL to the Auth0 application's Allowed Callback URLs.
107
110
 
108
111
  Example prompts: list `~/My Files/`, dry-run pull a folder, pull it, push results back.
109
112
 
110
- Optional coaching skills (not required for the server):
113
+ ### Agent skills (MCP)
114
+
115
+ Playbooks ship with the server. Agents should:
116
+
117
+ 1. `list_skills` — catalog (name, description, files)
118
+ 2. `get_skill(name)` — `SKILL.md` instructions (default)
119
+ 3. `get_skill(name, path="…")` — helpers such as `kernel.py`
120
+
121
+ Bundled today (canonical under `src/proteworks/skills/`; `skills/` holds symlinks for host import):
111
122
 
112
- - [`skills/proteworks-files/SKILL.md`](skills/proteworks-files/SKILL.md)
113
- - [`skills/proteworks-workflows/SKILL.md`](skills/proteworks-workflows/SKILL.md)
123
+ - [`protein-studio-molx`](src/proteworks/skills/protein-studio-molx/) — build Protein Studio `.molx` sessions (`SKILL.md` + `kernel.py`)
124
+ - [`proteworks-workflows`](src/proteworks/skills/proteworks-workflows/) — shared prepare/submit/monitor hub
125
+ - [`proteworks-workflow-rfd4-proteina`](src/proteworks/skills/proteworks-workflow-rfd4-proteina/) — RFD4 modes including binder rescue
126
+ - [`proteworks-workflow-rf-diffusion3`](src/proteworks/skills/proteworks-workflow-rf-diffusion3/)
127
+ - [`proteworks-workflow-proteinmpnn`](src/proteworks/skills/proteworks-workflow-proteinmpnn/)
128
+ - [`proteworks-workflow-refolding`](src/proteworks/skills/proteworks-workflow-refolding/)
129
+ - [`proteworks-workflow-structure-prediction`](src/proteworks/skills/proteworks-workflow-structure-prediction/)
130
+ - [`proteworks-workflow-msa-generation`](src/proteworks/skills/proteworks-workflow-msa-generation/)
131
+ - [`proteworks-studio`](src/proteworks/skills/proteworks-studio/) — `.studio` inspect / save-selected
132
+ - [`proteworks-files`](src/proteworks/skills/proteworks-files/) — list/pull/push/recycle
114
133
 
115
- Import via Settings → Skills → Import from GitHub (or upload the folder). Hosts without a Skills UI still get conventions from MCP tool descriptions and this README.
134
+ Add another skill by creating `src/proteworks/skills/<name>/SKILL.md` (plus optional helpers), then symlink `skills/<name>/SKILL.md` → the packaged file for GitHub Skills import. No new MCP tool names are required for playbook-only adds.
116
135
 
117
136
  ### Tools
118
137
 
@@ -120,16 +139,18 @@ Import via Settings → Skills → Import from GitHub (or upload the folder). Ho
120
139
  | --- | --- |
121
140
  | `auth_status` | Login state, API URL, active org (never the token) |
122
141
  | `organization_list` / `organization_set` | Choose organization (`organization_set` needs `confirm=true`) |
142
+ | `list_skills` / `get_skill` | Bundled playbooks (e.g. `.molx`); no login required |
123
143
  | `list_files` | Browse a remote path; returns `file_id` + `relative_path` |
124
144
  | `pull` / `push` | Transfer with `dry_run` (default true); real transfers need `confirm_transfer=true` |
125
- | `list_workflows` | Allowlisted agent workflows (`rf-diffusion3`, `proteinmpnn`, `refolding`, `structure-prediction`, `msa-generation`) |
145
+ | `list_workflows` | Allowlisted agent workflows (`rf-diffusion3`, `rfd4-proteina`, `proteinmpnn`, `refolding`, `structure-prediction`, `msa-generation`) |
126
146
  | `get_workflow_schema` | Full schema with UI-matching defaults |
127
147
  | `prepare_workflow_submission` | Merge defaults + overrides; **no submit** (draft body) |
128
148
  | `submit_workflow` | Same merge, then submit |
129
- | `inspect_studio` | Inspect supported `.studio` (proteinmpnn / refolding / rf-diffusion3 / structure_prediction); normalized rows + scores |
149
+ | `inspect_studio` | Inspect supported `.studio` (proteinmpnn / refolding / rf-diffusion3 / rfd4-proteina / structure_prediction); normalized rows + scores |
130
150
  | `save_selected_studio` | Same as Studio Save selected → subset `.studio` uploaded + retention finalized (not yet for structure_prediction) |
131
151
  | `inspect_proteinmpnn_studio` | Legacy ProteinMPNN flat-row inspect (prefer `inspect_studio`) |
132
152
  | `prepare_refolding_from_proteinmpnn_studio` | Build refolding payload + SA handoff from MPNN studio selection (**no submit**) |
153
+ | `list_executions` | List/filter past runs (`workflow_name`, `status`, `output_folder`); includes `design_count` totals |
133
154
  | `get_execution` / `list_execution_results` / `cancel_execution` | Status / results / cancel (`cancel_execution` needs `confirm=true`) |
134
155
  | `estimate_workflow_cost` | Cost estimate from merged params (same validation as prepare) |
135
156
 
@@ -38,6 +38,8 @@ proteworks mcp setup-claude-science
38
38
  proteworks mcp doctor
39
39
  ```
40
40
 
41
+ To switch accounts: `proteworks login --force` (clears the local token and the browser Auth0 session, then opens the sign-in UI). `proteworks logout` only forgets the local token.
42
+
41
43
  After any upgrade: run step 2 again (same `install-system` command), then `doctor`. Do not `uv tool install` again unless `/usr/local/bin/proteworks` is missing.
42
44
 
43
45
  ### Claude Science connector
@@ -85,20 +87,37 @@ proteworks push ./1UBQ.pdb '~/My Files/test/'
85
87
  proteworks push '*.pdb' '~/My Files/pdbs/'
86
88
  proteworks push designs.zip '~/My Files/dest/' # auto-extracts
87
89
  proteworks mkdir '~/My Files/campaigns/round6/'
90
+ proteworks recycle '~/My Files/campaigns/old-run/' # soft delete → recycle bin
88
91
  ```
89
92
 
90
- MCP also exposes `create_folder` for the same empty-directory create (returns `folder_id` for `output_folder_id` / push).
93
+ MCP also exposes `create_folder` for the same empty-directory create (returns `folder_id` for `output_folder_id` / push), and `move_to_recycle_bin` for soft-deleting a file, folder/, or wildcard (dry_run by default; set `confirm=true` to execute).
91
94
 
92
95
  Login returns to `http://127.0.0.1:8765/callback`. Add that exact URL to the Auth0 application's Allowed Callback URLs.
93
96
 
94
97
  Example prompts: list `~/My Files/`, dry-run pull a folder, pull it, push results back.
95
98
 
96
- Optional coaching skills (not required for the server):
99
+ ### Agent skills (MCP)
100
+
101
+ Playbooks ship with the server. Agents should:
102
+
103
+ 1. `list_skills` — catalog (name, description, files)
104
+ 2. `get_skill(name)` — `SKILL.md` instructions (default)
105
+ 3. `get_skill(name, path="…")` — helpers such as `kernel.py`
106
+
107
+ Bundled today (canonical under `src/proteworks/skills/`; `skills/` holds symlinks for host import):
97
108
 
98
- - [`skills/proteworks-files/SKILL.md`](skills/proteworks-files/SKILL.md)
99
- - [`skills/proteworks-workflows/SKILL.md`](skills/proteworks-workflows/SKILL.md)
109
+ - [`protein-studio-molx`](src/proteworks/skills/protein-studio-molx/) — build Protein Studio `.molx` sessions (`SKILL.md` + `kernel.py`)
110
+ - [`proteworks-workflows`](src/proteworks/skills/proteworks-workflows/) — shared prepare/submit/monitor hub
111
+ - [`proteworks-workflow-rfd4-proteina`](src/proteworks/skills/proteworks-workflow-rfd4-proteina/) — RFD4 modes including binder rescue
112
+ - [`proteworks-workflow-rf-diffusion3`](src/proteworks/skills/proteworks-workflow-rf-diffusion3/)
113
+ - [`proteworks-workflow-proteinmpnn`](src/proteworks/skills/proteworks-workflow-proteinmpnn/)
114
+ - [`proteworks-workflow-refolding`](src/proteworks/skills/proteworks-workflow-refolding/)
115
+ - [`proteworks-workflow-structure-prediction`](src/proteworks/skills/proteworks-workflow-structure-prediction/)
116
+ - [`proteworks-workflow-msa-generation`](src/proteworks/skills/proteworks-workflow-msa-generation/)
117
+ - [`proteworks-studio`](src/proteworks/skills/proteworks-studio/) — `.studio` inspect / save-selected
118
+ - [`proteworks-files`](src/proteworks/skills/proteworks-files/) — list/pull/push/recycle
100
119
 
101
- Import via Settings → Skills → Import from GitHub (or upload the folder). Hosts without a Skills UI still get conventions from MCP tool descriptions and this README.
120
+ Add another skill by creating `src/proteworks/skills/<name>/SKILL.md` (plus optional helpers), then symlink `skills/<name>/SKILL.md` → the packaged file for GitHub Skills import. No new MCP tool names are required for playbook-only adds.
102
121
 
103
122
  ### Tools
104
123
 
@@ -106,16 +125,18 @@ Import via Settings → Skills → Import from GitHub (or upload the folder). Ho
106
125
  | --- | --- |
107
126
  | `auth_status` | Login state, API URL, active org (never the token) |
108
127
  | `organization_list` / `organization_set` | Choose organization (`organization_set` needs `confirm=true`) |
128
+ | `list_skills` / `get_skill` | Bundled playbooks (e.g. `.molx`); no login required |
109
129
  | `list_files` | Browse a remote path; returns `file_id` + `relative_path` |
110
130
  | `pull` / `push` | Transfer with `dry_run` (default true); real transfers need `confirm_transfer=true` |
111
- | `list_workflows` | Allowlisted agent workflows (`rf-diffusion3`, `proteinmpnn`, `refolding`, `structure-prediction`, `msa-generation`) |
131
+ | `list_workflows` | Allowlisted agent workflows (`rf-diffusion3`, `rfd4-proteina`, `proteinmpnn`, `refolding`, `structure-prediction`, `msa-generation`) |
112
132
  | `get_workflow_schema` | Full schema with UI-matching defaults |
113
133
  | `prepare_workflow_submission` | Merge defaults + overrides; **no submit** (draft body) |
114
134
  | `submit_workflow` | Same merge, then submit |
115
- | `inspect_studio` | Inspect supported `.studio` (proteinmpnn / refolding / rf-diffusion3 / structure_prediction); normalized rows + scores |
135
+ | `inspect_studio` | Inspect supported `.studio` (proteinmpnn / refolding / rf-diffusion3 / rfd4-proteina / structure_prediction); normalized rows + scores |
116
136
  | `save_selected_studio` | Same as Studio Save selected → subset `.studio` uploaded + retention finalized (not yet for structure_prediction) |
117
137
  | `inspect_proteinmpnn_studio` | Legacy ProteinMPNN flat-row inspect (prefer `inspect_studio`) |
118
138
  | `prepare_refolding_from_proteinmpnn_studio` | Build refolding payload + SA handoff from MPNN studio selection (**no submit**) |
139
+ | `list_executions` | List/filter past runs (`workflow_name`, `status`, `output_folder`); includes `design_count` totals |
119
140
  | `get_execution` / `list_execution_results` / `cancel_execution` | Status / results / cancel (`cancel_execution` needs `confirm=true`) |
120
141
  | `estimate_workflow_cost` | Cost estimate from merged params (same validation as prepare) |
121
142
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "proteworks"
3
- version = "0.1.9"
3
+ version = "0.1.11"
4
4
  description = "Sync files with Proteworks using file-browser paths."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -25,5 +25,8 @@ build-backend = "setuptools.build_meta"
25
25
  [tool.setuptools.packages.find]
26
26
  where = ["src"]
27
27
 
28
+ [tool.setuptools.package-data]
29
+ proteworks = ["skills/**/*"]
30
+
28
31
  [tool.pytest.ini_options]
29
32
  testpaths = ["tests"]
@@ -8,6 +8,6 @@ try:
8
8
  try:
9
9
  __version__ = version("proteworks")
10
10
  except PackageNotFoundError:
11
- __version__ = "0.1.9"
11
+ __version__ = "0.1.11"
12
12
  except Exception: # pragma: no cover
13
- __version__ = "0.1.9"
13
+ __version__ = "0.1.11"
@@ -37,13 +37,21 @@ class ApiError(Exception):
37
37
 
38
38
 
39
39
  def assert_allowed_storage_url(url: str) -> None:
40
- """Reject signed URLs whose host is outside the storage allowlist (SSRF defense)."""
40
+ """Reject signed URLs whose host is outside the storage allowlist (SSRF defense).
41
+
42
+ Production GCS signed URLs must be HTTPS. Plain HTTP is allowed only for
43
+ localhost / 127.0.0.1 (FakeApi and local integration tests).
44
+ """
41
45
  parsed = urlparse(str(url or ""))
42
- if parsed.scheme not in {"https", "http"}:
43
- raise ApiError("Storage URL must use http(s)", 400)
44
46
  host = (parsed.hostname or "").lower()
45
47
  if not host:
46
48
  raise ApiError("Storage URL missing host", 400)
49
+ if parsed.scheme == "https":
50
+ pass
51
+ elif parsed.scheme == "http" and host in {"localhost", "127.0.0.1"}:
52
+ pass
53
+ else:
54
+ raise ApiError("Storage URL must use https", 400)
47
55
  suffixes = allowed_storage_host_suffixes()
48
56
  if any(host == suffix or host.endswith("." + suffix) for suffix in suffixes):
49
57
  return
@@ -201,6 +209,47 @@ class ApiClient:
201
209
  data={"name": name, "parent_id": parent_id},
202
210
  )
203
211
 
212
+ def get_recycle_bin_folder(self) -> dict:
213
+ """GET /api/v1/folders/recycle-bin (current user's recycle bin)."""
214
+ return self.request("GET", "/api/v1/folders/recycle-bin")
215
+
216
+ def get_directory(
217
+ self,
218
+ folder_id: str,
219
+ *,
220
+ files_limit: int = 1,
221
+ folders_limit: int = 1,
222
+ ) -> dict:
223
+ """GET /api/v1/directory/{folder_id} (includes view_in_recycle_bin)."""
224
+ return self.request(
225
+ "GET",
226
+ f"/api/v1/directory/{folder_id}",
227
+ params={
228
+ "files_limit": files_limit,
229
+ "folders_limit": folders_limit,
230
+ "fields": "summary",
231
+ },
232
+ )
233
+
234
+ def move_files_to_recycle_bin(self, file_ids: list[str]) -> dict:
235
+ """POST /api/v1/files/move-to-recycle-bin (soft-delete; not permanent)."""
236
+ return self.request(
237
+ "POST",
238
+ "/api/v1/files/move-to-recycle-bin",
239
+ json_body={"file_ids": list(file_ids)},
240
+ )
241
+
242
+ def move_folder_to_recycle_bin(self, folder_id: str) -> dict:
243
+ """POST /api/v1/folders/{id}/move-to-recycle-bin (async job for trees)."""
244
+ return self.request(
245
+ "POST",
246
+ f"/api/v1/folders/{folder_id}/move-to-recycle-bin",
247
+ )
248
+
249
+ def get_folder_recycle_move_job(self, job_id: str) -> dict:
250
+ """GET /api/v1/folders/recycle-move-jobs/{job_id}."""
251
+ return self.request("GET", f"/api/v1/folders/recycle-move-jobs/{job_id}")
252
+
204
253
  def upload_init(
205
254
  self,
206
255
  filename: str,
@@ -406,9 +455,53 @@ class ApiClient:
406
455
  def get_agent_schema(self, workflow_name: str) -> dict:
407
456
  return self.request("GET", f"/api/v1/workflows/agent-schemas/{workflow_name}")
408
457
 
458
+ def resolve_structure_prediction_methods(
459
+ self,
460
+ *,
461
+ parameters: Optional[dict] = None,
462
+ complex_ir: Optional[dict] = None,
463
+ enable_all_capable: bool = False,
464
+ ) -> dict:
465
+ """POST capable-predictor resolution for structure-prediction prepare."""
466
+ body: dict[str, Any] = {"enable_all_capable": bool(enable_all_capable)}
467
+ if parameters is not None:
468
+ body["parameters"] = parameters
469
+ if complex_ir is not None:
470
+ body["complex_ir"] = complex_ir
471
+ return self.request(
472
+ "POST",
473
+ "/api/v1/workflows/agent-schemas/structure-prediction/resolve-methods",
474
+ json_body=body,
475
+ )
476
+
409
477
  def submit_workflow(self, body: dict) -> dict:
410
478
  return self.request("POST", "/api/v1/workflows/submit", json_body=body)
411
479
 
480
+ def list_executions(
481
+ self,
482
+ *,
483
+ limit: int = 50,
484
+ since: Optional[str] = None,
485
+ workflow_name: Optional[str] = None,
486
+ status: Optional[str] = None,
487
+ output_folder: Optional[str] = None,
488
+ user_id: Optional[str] = None,
489
+ ) -> list:
490
+ """GET /api/v1/workflows/executions with optional filters."""
491
+ params: dict[str, Any] = {"limit": int(limit)}
492
+ if since:
493
+ params["since"] = since
494
+ if workflow_name:
495
+ params["workflow_name"] = workflow_name
496
+ if status:
497
+ params["status"] = status
498
+ if output_folder:
499
+ params["output_folder"] = output_folder
500
+ if user_id:
501
+ params["user_id"] = user_id
502
+ payload = self.request("GET", "/api/v1/workflows/executions", params=params)
503
+ return list(payload or [])
504
+
412
505
  def get_execution(self, execution_id: str) -> dict:
413
506
  return self.request("GET", f"/api/v1/workflows/executions/{execution_id}")
414
507
 
@@ -12,7 +12,13 @@ from urllib.parse import parse_qs, urlencode, urlparse
12
12
 
13
13
  import httpx
14
14
 
15
- from proteworks.config import LOOPBACK_PORT, LOOPBACK_REDIRECT_PATH, Settings, TokenStore
15
+ from proteworks.config import (
16
+ LOOPBACK_PORT,
17
+ LOOPBACK_REDIRECT_PATH,
18
+ ConfigStore,
19
+ Settings,
20
+ TokenStore,
21
+ )
16
22
 
17
23
 
18
24
  def pkce_pair() -> tuple[str, str]:
@@ -22,20 +28,33 @@ def pkce_pair() -> tuple[str, str]:
22
28
  return verifier, challenge
23
29
 
24
30
 
25
- def authorize_url(settings: Settings, redirect_uri: str, state: str, challenge: str) -> str:
26
- query = urlencode(
27
- {
28
- "response_type": "code",
29
- "client_id": settings.auth0_client_id,
30
- "redirect_uri": redirect_uri,
31
- "scope": "openid profile email",
32
- "audience": settings.auth0_audience,
33
- "state": state,
34
- "code_challenge": challenge,
35
- "code_challenge_method": "S256",
36
- }
37
- )
38
- return f"https://{settings.auth0_domain}/authorize?{query}"
31
+ def authorize_url(
32
+ settings: Settings,
33
+ redirect_uri: str,
34
+ state: str,
35
+ challenge: str,
36
+ *,
37
+ prompt: str | None = "login",
38
+ ) -> str:
39
+ """Build the Auth0 authorize URL.
40
+
41
+ ``prompt=login`` (the default) forces the credentials UI even when the browser
42
+ already has an Auth0 SSO cookie, so ``proteworks login`` can switch accounts.
43
+ Pass ``prompt=None`` only in tests that need the silent SSO path.
44
+ """
45
+ params: dict[str, str] = {
46
+ "response_type": "code",
47
+ "client_id": settings.auth0_client_id,
48
+ "redirect_uri": redirect_uri,
49
+ "scope": "openid profile email",
50
+ "audience": settings.auth0_audience,
51
+ "state": state,
52
+ "code_challenge": challenge,
53
+ "code_challenge_method": "S256",
54
+ }
55
+ if prompt:
56
+ params["prompt"] = prompt
57
+ return f"https://{settings.auth0_domain}/authorize?{urlencode(params)}"
39
58
 
40
59
 
41
60
  class _CallbackHandler(BaseHTTPRequestHandler):
@@ -78,8 +97,29 @@ def open_browser(url: str) -> bool:
78
97
  os.close(saved_stderr)
79
98
 
80
99
 
81
- def login(settings: Settings, token_store: TokenStore, stderr) -> None:
82
- """Open a browser, receive the loopback redirect, and save the access token."""
100
+ def logout(token_store: TokenStore, stderr) -> None:
101
+ """Forget the local access token (keychain + credentials file)."""
102
+ token_store.clear()
103
+ print("Signed out.", file=stderr)
104
+
105
+
106
+ def login(settings: Settings, token_store: TokenStore, stderr, *, force: bool = False) -> None:
107
+ """Open a browser, receive the loopback redirect, and save the access token.
108
+
109
+ Always requests ``prompt=login`` so Auth0 does not silently reuse a browser SSO
110
+ session. With ``force=True``, also clear any local token first and open Auth0's
111
+ logout endpoint so federated IdP cookies (e.g. Google) do not auto-pick the
112
+ previous account.
113
+ """
114
+ if force:
115
+ token_store.clear()
116
+ logout_url = (
117
+ f"https://{settings.auth0_domain}/v2/logout?"
118
+ + urlencode({"client_id": settings.auth0_client_id})
119
+ )
120
+ print("Clearing the browser Auth0 session…", file=stderr)
121
+ open_browser(logout_url)
122
+
83
123
  verifier, challenge = pkce_pair()
84
124
  state = secrets.token_urlsafe(24)
85
125
  try:
@@ -89,8 +129,10 @@ def login(settings: Settings, token_store: TokenStore, stderr) -> None:
89
129
  f"Port {LOOPBACK_PORT} is in use, so login cannot listen on {loopback_redirect_uri()}. ({exc})"
90
130
  ) from exc
91
131
  redirect_uri = loopback_redirect_uri()
92
- url = authorize_url(settings, redirect_uri, state, challenge)
132
+ url = authorize_url(settings, redirect_uri, state, challenge, prompt="login")
93
133
  print("Opening a browser to sign in.", file=stderr)
134
+ if force:
135
+ print("Choose the account you want, then continue.", file=stderr)
94
136
  opened = open_browser(url)
95
137
  if not opened:
96
138
  print("Open this URL to sign in:", file=stderr)
@@ -118,4 +160,7 @@ def login(settings: Settings, token_store: TokenStore, stderr) -> None:
118
160
  if not access_token:
119
161
  raise SystemExit("Login failed.")
120
162
  token_store.set(access_token)
163
+ # Bind credentials to this API URL so a later PROTEWORKS_API_URL change cannot
164
+ # silently send the bearer token to a different host.
165
+ ConfigStore(settings.config_dir / "config.json").bind_api_url(settings.api_url)
121
166
  print("Signed in.", file=stderr)
@@ -3,12 +3,13 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import argparse
6
+ import json
6
7
  import sys
7
8
  from pathlib import Path
8
9
 
9
10
  from proteworks import __version__
10
11
  from proteworks.api import ApiClient, ApiError
11
- from proteworks.auth import login
12
+ from proteworks.auth import login, logout
12
13
  from proteworks.config import ConfigStore, Settings, TokenStore, load_settings
13
14
  from proteworks.mcp_host import (
14
15
  SYSTEM_BIN_DIR_DEFAULT,
@@ -24,9 +25,11 @@ from proteworks.orgs import (
24
25
  regular_organizations,
25
26
  select_organization,
26
27
  )
28
+ from proteworks.executions import summarize_execution_list
27
29
  from proteworks.folders import mkdir_remote
28
30
  from proteworks.pull import export_organization, pull_path
29
31
  from proteworks.push import push_paths
32
+ from proteworks.recycle import RecycleError, recycle_remote
30
33
  from proteworks.redact import install_redacting_logs
31
34
  from proteworks.session import SessionError, authenticated_client, open_session
32
35
 
@@ -50,6 +53,13 @@ examples:
50
53
  proteworks mkdir '~/My Files/campaigns/round6/'
51
54
  """
52
55
 
56
+ _RECYCLE_EXAMPLES = """\
57
+ examples:
58
+ proteworks recycle '~/My Files/campaigns/run/design.cif'
59
+ proteworks recycle '~/My Files/campaigns/old-run/'
60
+ proteworks recycle '~/My Files/msa/*.cif' --dry-run
61
+ """
62
+
53
63
  _EXPORT_EXAMPLES = """\
54
64
  examples:
55
65
  proteworks export ./backup
@@ -61,6 +71,13 @@ examples:
61
71
  proteworks mcp doctor
62
72
  """
63
73
 
74
+ _EXECUTIONS_EXAMPLES = """\
75
+ examples:
76
+ proteworks executions list --workflow-name rf-diffusion3 --status Succeeded
77
+ proteworks executions list --workflow-name rfd4-proteina,rf-diffusion3 --output-folder 'TFR/'
78
+ proteworks executions list --output-folder '~/My Files/TFR/' --status Succeeded --limit 200
79
+ """
80
+
64
81
 
65
82
  def build_parser() -> argparse.ArgumentParser:
66
83
  """Build the parser, then describe commands by walking what was registered."""
@@ -76,7 +93,16 @@ def build_parser() -> argparse.ArgumentParser:
76
93
  )
77
94
  sub = parser.add_subparsers(dest="command", required=True)
78
95
 
79
- sub.add_parser("login", help="Sign in and save an access token in the OS keychain")
96
+ login_parser = sub.add_parser(
97
+ "login",
98
+ help="Sign in and save an access token in the OS keychain",
99
+ )
100
+ login_parser.add_argument(
101
+ "--force",
102
+ action="store_true",
103
+ help="Clear local credentials and the browser Auth0 session, then sign in (use to switch accounts)",
104
+ )
105
+ sub.add_parser("logout", help="Forget the local access token")
80
106
 
81
107
  org = sub.add_parser("organization", help="List organizations or choose the active one")
82
108
  org_sub = org.add_subparsers(dest="org_command", required=True)
@@ -129,6 +155,22 @@ def build_parser() -> argparse.ArgumentParser:
129
155
  )
130
156
  mkdir.add_argument("remote", help="Remote folder path ending with /")
131
157
 
158
+ recycle = sub.add_parser(
159
+ "recycle",
160
+ help="Move a remote file, folder/, or wildcard into the recycle bin (soft delete)",
161
+ formatter_class=argparse.RawDescriptionHelpFormatter,
162
+ epilog=_RECYCLE_EXAMPLES,
163
+ )
164
+ recycle.add_argument(
165
+ "remote",
166
+ help="File-browser path such as '~/My Files/run/design.cif' or '~/My Files/run/'. Quote wildcards.",
167
+ )
168
+ recycle.add_argument(
169
+ "--dry-run",
170
+ action="store_true",
171
+ help="Print what would be moved without calling the recycle-bin API",
172
+ )
173
+
132
174
  export = sub.add_parser(
133
175
  "export",
134
176
  help="Download the active organization's files (organization admins)",
@@ -138,6 +180,50 @@ def build_parser() -> argparse.ArgumentParser:
138
180
  export.add_argument("dest", help="Local directory")
139
181
  export.add_argument("--dry-run", action="store_true", help="Print the files that would be downloaded")
140
182
 
183
+ executions = sub.add_parser(
184
+ "executions",
185
+ help="List past workflow executions",
186
+ formatter_class=argparse.RawDescriptionHelpFormatter,
187
+ epilog=_EXECUTIONS_EXAMPLES,
188
+ )
189
+ executions_sub = executions.add_subparsers(dest="executions_command", required=True)
190
+ executions_list = executions_sub.add_parser(
191
+ "list",
192
+ help="List executions (filter by workflow name, status, output folder)",
193
+ formatter_class=argparse.RawDescriptionHelpFormatter,
194
+ epilog=_EXECUTIONS_EXAMPLES,
195
+ )
196
+ executions_list.add_argument(
197
+ "--workflow-name",
198
+ help=(
199
+ "Workflow name prefix (comma-separated). "
200
+ "rf-diffusion3 matches rf-diffusion3 and rf-diffusion3-modal."
201
+ ),
202
+ )
203
+ executions_list.add_argument(
204
+ "--status",
205
+ help="Status filter (comma-separated), e.g. Succeeded",
206
+ )
207
+ executions_list.add_argument(
208
+ "--output-folder",
209
+ help="Output folder path prefix, e.g. TFR/ or '~/My Files/TFR/'",
210
+ )
211
+ executions_list.add_argument(
212
+ "--since",
213
+ help="Only executions updated since this ISO 8601 timestamp",
214
+ )
215
+ executions_list.add_argument(
216
+ "--limit",
217
+ type=int,
218
+ default=50,
219
+ help="Max executions to return (default 50, max 200)",
220
+ )
221
+ executions_list.add_argument(
222
+ "--json",
223
+ action="store_true",
224
+ help="Print full JSON summary instead of a table",
225
+ )
226
+
141
227
  mcp = sub.add_parser(
142
228
  "mcp",
143
229
  help="System install and local MCP helpers (doctor, optional Science setup)",
@@ -200,7 +286,9 @@ def build_parser() -> argparse.ArgumentParser:
200
286
  " proteworks push '*.pdb' '~/My Files/pdbs/'\n"
201
287
  " proteworks push designs.zip '~/My Files/dest/'\n"
202
288
  " proteworks mkdir '~/My Files/campaigns/round6/'\n"
289
+ " proteworks recycle '~/My Files/campaigns/old-run/'\n"
203
290
  " proteworks export ./backup\n"
291
+ " proteworks executions list --workflow-name rf-diffusion3 --status Succeeded\n"
204
292
  " sudo proteworks mcp install-system\n"
205
293
  " proteworks mcp doctor\n"
206
294
  )
@@ -230,12 +318,15 @@ def main(argv: list[str] | None = None, *, settings: Settings | None = None, tok
230
318
  if settings.api_url_overridden:
231
319
  print(f"using {settings.api_url}", file=sys.stderr)
232
320
  if args.command == "login":
233
- login(settings, token_store, sys.stderr)
321
+ login(settings, token_store, sys.stderr, force=bool(args.force))
234
322
  config = ConfigStore(settings.config_dir / "config.json")
235
323
  data = config.read()
236
324
  data["api_url"] = settings.api_url
237
325
  config.write(data)
238
326
  return 0
327
+ if args.command == "logout":
328
+ logout(token_store, sys.stderr)
329
+ return 0
239
330
  if args.command == "organization" and args.org_command == "list":
240
331
  auth = authenticated_client(settings=settings, token_store=token_store)
241
332
  if isinstance(auth, SessionError):
@@ -305,6 +396,20 @@ def main(argv: list[str] | None = None, *, settings: Settings | None = None, tok
305
396
  action = "created" if result.get("created") else "exists"
306
397
  print(f"{action} {result.get('path')} folder_id={result.get('folder_id')}", file=sys.stderr)
307
398
  return 0
399
+ if args.command == "recycle":
400
+ try:
401
+ recycle_remote(
402
+ session.api,
403
+ args.remote,
404
+ dry_run=bool(args.dry_run),
405
+ stderr=sys.stderr,
406
+ )
407
+ except (ApiError, RecycleError, ValueError) as exc:
408
+ print(str(exc), file=sys.stderr)
409
+ return 1
410
+ return 0
411
+ if args.command == "executions" and args.executions_command == "list":
412
+ return _executions_list(session.api, args, sys.stdout, sys.stderr)
308
413
  parser.print_help(sys.stderr)
309
414
  return 2
310
415
  finally:
@@ -359,5 +464,51 @@ def _organization_set(api: ApiClient, config: ConfigStore, settings: Settings, n
359
464
  return 0
360
465
 
361
466
 
467
+ def _executions_list(api: ApiClient, args: argparse.Namespace, stdout, stderr) -> int:
468
+ limit = max(1, min(int(args.limit), 200))
469
+ try:
470
+ executions = api.list_executions(
471
+ limit=limit,
472
+ since=args.since,
473
+ workflow_name=args.workflow_name,
474
+ status=args.status,
475
+ output_folder=args.output_folder,
476
+ )
477
+ except ApiError as exc:
478
+ print(str(exc), file=stderr)
479
+ return 1
480
+ summary = summarize_execution_list(
481
+ [row for row in executions if isinstance(row, dict)]
482
+ )
483
+ if args.json:
484
+ print(json.dumps(summary, indent=2, default=str), file=stdout)
485
+ return 0
486
+ rows = summary.get("executions") or []
487
+ if not rows:
488
+ print("No executions matched.", file=stderr)
489
+ return 0
490
+ for row in rows:
491
+ designs = row.get("design_count")
492
+ designs_s = str(designs) if designs is not None else "?"
493
+ folder = row.get("output_folder") or "-"
494
+ print(
495
+ f"{row.get('created_at') or '-'} {row.get('status') or '-':12} "
496
+ f"{row.get('workflow_name') or '-':22} designs={designs_s:<6} "
497
+ f"id={row.get('id')} out={folder}",
498
+ file=stdout,
499
+ )
500
+ by_wf = summary.get("by_workflow") or {}
501
+ total = summary.get("total_designs")
502
+ print("", file=stderr)
503
+ for name, bucket in sorted(by_wf.items()):
504
+ print(
505
+ f"{name}: {bucket.get('runs', 0)} runs, {bucket.get('designs', 0)} designs",
506
+ file=stderr,
507
+ )
508
+ if total is not None:
509
+ print(f"total designs: {total}", file=stderr)
510
+ return 0
511
+
512
+
362
513
  if __name__ == "__main__":
363
514
  raise SystemExit(main())