workspaceguard-cli 0.1.1__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.
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ .venv-freshinstall/
3
+ dist/
4
+ build/
5
+ *.egg-info/
6
+ __pycache__/
7
+ *.pyc
8
+ .pytest_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rudrendu Paul
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,226 @@
1
+ Metadata-Version: 2.4
2
+ Name: workspaceguard-cli
3
+ Version: 0.1.1
4
+ Summary: Per-workspace usage metering and quota caps for shared self-hosted AI assistant deployments (Odysseus and compatible backends).
5
+ Project-URL: Homepage, https://github.com/RudrenduPaul/workspaceguard
6
+ Project-URL: Repository, https://github.com/RudrenduPaul/workspaceguard
7
+ Project-URL: Bug Tracker, https://github.com/RudrenduPaul/workspaceguard/issues
8
+ Project-URL: Changelog, https://github.com/RudrenduPaul/workspaceguard/blob/main/CHANGELOG.md
9
+ Project-URL: Documentation, https://github.com/RudrenduPaul/workspaceguard/blob/main/docs/getting-started.md
10
+ Project-URL: Author - Rudrendu Paul, https://github.com/RudrenduPaul
11
+ Project-URL: Author - Sourav Nandy, https://github.com/Sourav-nandy-ai
12
+ Author: Rudrendu Paul, Sourav Nandy
13
+ License-Expression: MIT
14
+ License-File: LICENSE
15
+ Keywords: agent-native,ai-assistant,billing,cli,multi-tenant,odysseus,quota,reverse-proxy,self-hosted,usage-metering
16
+ Classifier: Development Status :: 3 - Alpha
17
+ Classifier: Environment :: Console
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: License :: OSI Approved :: MIT License
20
+ Classifier: Operating System :: OS Independent
21
+ Classifier: Programming Language :: Python :: 3
22
+ Classifier: Programming Language :: Python :: 3.9
23
+ Classifier: Programming Language :: Python :: 3.10
24
+ Classifier: Programming Language :: Python :: 3.11
25
+ Classifier: Programming Language :: Python :: 3.12
26
+ Classifier: Programming Language :: Python :: 3.13
27
+ Classifier: Topic :: Security
28
+ Classifier: Topic :: System :: Systems Administration
29
+ Requires-Python: >=3.9
30
+ Requires-Dist: cryptography<46,>=42
31
+ Requires-Dist: pyyaml<7,>=6.0
32
+ Provides-Extra: dev
33
+ Requires-Dist: build<2,>=1.0; extra == 'dev'
34
+ Requires-Dist: pytest-asyncio<1,>=0.23; extra == 'dev'
35
+ Requires-Dist: pytest<9,>=7.0; extra == 'dev'
36
+ Requires-Dist: twine<7,>=5.0; extra == 'dev'
37
+ Description-Content-Type: text/markdown
38
+
39
+ # workspaceguard-cli (Python)
40
+
41
+ Per-workspace usage metering and quota caps for one shared self-hosted AI
42
+ assistant deployment -- a genuine, independent Python port of the
43
+ [`workspaceguard-cli`](https://github.com/RudrenduPaul/workspaceguard) npm
44
+ package, not a wrapper around a Node binary.
45
+
46
+ [![PyPI version](https://img.shields.io/pypi/v/workspaceguard-cli.svg)](https://pypi.org/project/workspaceguard-cli/)
47
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/RudrenduPaul/workspaceguard/blob/main/LICENSE)
48
+ [![Python versions](https://img.shields.io/pypi/pyversions/workspaceguard-cli.svg)](https://pypi.org/project/workspaceguard-cli/)
49
+
50
+ ## Why this exists
51
+
52
+ Run [Odysseus](https://github.com/pewdiepie-archdaemon/odysseus) (or a
53
+ compatible self-hosted assistant) for your whole household or small team,
54
+ and there is no way to see who sent how many messages this month, or to
55
+ stop one person's usage from burning through everyone else's API budget.
56
+ WorkspaceGuard is a thin sidecar that adds that layer: per-workspace
57
+ message counts, optional monthly caps that fail closed, and a CLI report
58
+ an admin (or another agent) can read.
59
+
60
+ This project originally set out to add per-user workspace isolation
61
+ (separate chat history, memory, API keys) to a self-hosted AI chat
62
+ platform. A feasibility spike found that premise was false for the target
63
+ platform's current default configuration: per-user ownership on chat
64
+ history, memory, and API tokens is already enforced by default. Rather
65
+ than ship a competing reimplementation of something the target platform
66
+ already does correctly, this project keeps its tested isolation engine
67
+ (namespace separation, an AES-256-GCM vault with real key rotation,
68
+ fail-closed identity resolution, a self-healing circuit breaker) as the
69
+ identity-resolution substrate, and builds the layer that platform
70
+ genuinely does not have: usage metering and quota enforcement per
71
+ workspace. See the [project README](https://github.com/RudrenduPaul/workspaceguard#readme)
72
+ for the full story.
73
+
74
+ ## Install
75
+
76
+ ```bash
77
+ pip install workspaceguard-cli
78
+ ```
79
+
80
+ **Current status**: this Python port is fully built, tested (32/32 pytest
81
+ tests passing), and packaged -- the wheel and sdist are built and verified,
82
+ installing and running correctly end to end in a fresh virtual environment.
83
+ The first PyPI publish is pending: PyPI's own account-level throttle on
84
+ registering brand-new project names ("429 Too many new projects created")
85
+ is currently blocking the upload, unrelated to any 2FA requirement (this
86
+ account's PyPI publishing needs no human 2FA). It will publish as soon as
87
+ that throttle window clears. Separately, the npm package (`workspaceguard-cli`)
88
+ is built with CI green but not yet published to the npm registry either
89
+ (blocked on a manual 2FA-gated publish step). Neither `pip install
90
+ workspaceguard-cli` nor `npm install -g workspaceguard-cli` is live yet as
91
+ of this writing -- check [PyPI](https://pypi.org/project/workspaceguard-cli/)
92
+ for current availability. This package is a genuine, independent port --
93
+ not a wrapper around the npm package -- so it will work regardless of npm's
94
+ status once it publishes.
95
+
96
+ ## Quickstart
97
+
98
+ ```bash
99
+ # Register the workspaces sharing one deployment (identity = the header
100
+ # value your reverse proxy sets after authenticating, e.g. Cloudflare
101
+ # Access).
102
+ workspaceguard add-workspace alex --identity alex@example.com
103
+ workspaceguard add-workspace jordan --identity jordan@example.com
104
+
105
+ # Optional: cap alex at 1000 messages/month. Omit for unlimited (the default).
106
+ workspaceguard set-cap alex 1000
107
+
108
+ # See usage for every workspace.
109
+ workspaceguard usage
110
+ ```
111
+
112
+ ```
113
+ $ workspaceguard usage --json
114
+ {"ok": true, "usage": [{"workspaceId": "alex", "identity": "alex@example.com", "monthlyMessageCap": 1000, "percentUsed": 0, "period": "2026-07", "messageCount": 0, "estimatedBytes": 0}]}
115
+ ```
116
+
117
+ ## CLI reference
118
+
119
+ Every command accepts `--json` for a structured, agent-native output shape
120
+ instead of the human-readable text shown below. Identical command surface
121
+ to the npm CLI.
122
+
123
+ | Command | What it does |
124
+ | --- | --- |
125
+ | `workspaceguard init` | Initializes the data directory and vault for this deployment. |
126
+ | `workspaceguard add-workspace <id> --identity <value>` | Registers a workspace, idempotent on repeat calls for the same id. |
127
+ | `workspaceguard status [--json]` | Lists configured workspaces. |
128
+ | `workspaceguard usage [--json]` | Per-workspace message count, cap, and percent-used for the current month. |
129
+ | `workspaceguard set-cap <id> <count\|none>` | Sets or clears a workspace's monthly message cap. |
130
+ | `workspaceguard rotate-key <id>` | Rotates a workspace's vault encryption key (invalidates the old ciphertext). |
131
+ | `workspaceguard scan [--json]` | Isolation config scan (scaffold stub, carried over from the original build). |
132
+
133
+ ## Library API
134
+
135
+ ```python
136
+ import asyncio
137
+ from workspaceguard import create_workspace_guard, MockAdapter, QuotaExceededError
138
+
139
+ async def main():
140
+ guard = await create_workspace_guard(data_dir="./data", backend=MockAdapter())
141
+ await guard.add_workspace("alex", "alex@example.com")
142
+ await guard.set_cap("alex", 1000)
143
+
144
+ try:
145
+ await guard.chat("alex@example.com", "hello")
146
+ except QuotaExceededError:
147
+ pass # alex is over their monthly cap
148
+
149
+ report = await guard.usage_report()
150
+
151
+ asyncio.run(main())
152
+ ```
153
+
154
+ The library API is async (`asyncio`), matching the async architecture of
155
+ the original TypeScript source rather than flattening it to synchronous
156
+ calls.
157
+
158
+ ## How it works
159
+
160
+ ```
161
+ target: an inbound chat request with an identity header value
162
+ -> resolve_workspace() -- fail closed on any miss, never a default workspace
163
+ -> check_quota() -- QuotaExceededError if the workspace is at its cap
164
+ -> circuit breaker -- calls the BackendAdapter, opens after 3 consecutive
165
+ failures, self-heals via a half-open probe after a cooldown
166
+ -> record usage -- per-workspace, per-month counters with automatic
167
+ period rollover
168
+ ```
169
+
170
+ - `workspaceguard/isolation_guard.py` -- the single choke point (`chat()`)
171
+ every request flows through: resolve workspace -> check quota -> call
172
+ backend -> record usage.
173
+ - `workspaceguard/usage.py` -- the usage-metering engine this project adds:
174
+ per-workspace, per-month counters with automatic period rollover, and
175
+ `QuotaExceededError` enforcement.
176
+ - `workspaceguard/vault.py`, `workspaceguard/namespace.py`,
177
+ `workspaceguard/circuit_breaker.py` -- the original isolation-engine
178
+ code, kept as the identity/workspace-boundary substrate the metering
179
+ layer reads from, not shipped as a competing isolation product.
180
+ - `workspaceguard/adapters/` -- `BackendAdapter` abstract base class; a
181
+ real Odysseus HTTP adapter is the next step (currently `MockAdapter`
182
+ only, same as the TypeScript original).
183
+
184
+ Backend-specific behavior never enters the core modules directly --
185
+ everything goes through `BackendAdapter`.
186
+
187
+ ## Trust boundary
188
+
189
+ WorkspaceGuard trusts an upstream identity header (default:
190
+ `Cf-Access-Authenticated-User-Email`) to resolve the workspace. It must
191
+ never be directly reachable from the network -- only from behind whatever
192
+ trusted proxy sets that header (Cloudflare Access, Tailscale, etc.). This
193
+ is documented, not code-enforced.
194
+
195
+ ## What's real vs. not yet built
196
+
197
+ - Real, tested (28/28 pytest tests passing): usage metering, quota
198
+ enforcement, the original isolation engine (vault, namespace separation,
199
+ circuit breaker), CLI with `--json` mode.
200
+ - Not yet built: a real Odysseus HTTP adapter (only `MockAdapter` exists so
201
+ far, same as the TypeScript original), a hosted managed billing
202
+ dashboard (deliberately out of scope for this MIT project).
203
+
204
+ ## Security
205
+
206
+ The vault uses AES-256-GCM (via the `cryptography` package) with one
207
+ derived key per workspace per generation -- rotating a workspace's key
208
+ increments its generation, so old ciphertext genuinely can no longer be
209
+ decrypted, not a no-op. See
210
+ [SECURITY.md](https://github.com/RudrenduPaul/workspaceguard/blob/main/SECURITY.md)
211
+ for the disclosure process and what's in/out of scope.
212
+
213
+ ## Contributing
214
+
215
+ See [CONTRIBUTING.md](https://github.com/RudrenduPaul/workspaceguard/blob/main/CONTRIBUTING.md).
216
+
217
+ ```bash
218
+ cd python
219
+ python3 -m venv .venv && source .venv/bin/activate
220
+ pip install -e ".[dev]"
221
+ pytest
222
+ ```
223
+
224
+ ## License
225
+
226
+ MIT, see [LICENSE](https://github.com/RudrenduPaul/workspaceguard/blob/main/LICENSE).
@@ -0,0 +1,188 @@
1
+ # workspaceguard-cli (Python)
2
+
3
+ Per-workspace usage metering and quota caps for one shared self-hosted AI
4
+ assistant deployment -- a genuine, independent Python port of the
5
+ [`workspaceguard-cli`](https://github.com/RudrenduPaul/workspaceguard) npm
6
+ package, not a wrapper around a Node binary.
7
+
8
+ [![PyPI version](https://img.shields.io/pypi/v/workspaceguard-cli.svg)](https://pypi.org/project/workspaceguard-cli/)
9
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/RudrenduPaul/workspaceguard/blob/main/LICENSE)
10
+ [![Python versions](https://img.shields.io/pypi/pyversions/workspaceguard-cli.svg)](https://pypi.org/project/workspaceguard-cli/)
11
+
12
+ ## Why this exists
13
+
14
+ Run [Odysseus](https://github.com/pewdiepie-archdaemon/odysseus) (or a
15
+ compatible self-hosted assistant) for your whole household or small team,
16
+ and there is no way to see who sent how many messages this month, or to
17
+ stop one person's usage from burning through everyone else's API budget.
18
+ WorkspaceGuard is a thin sidecar that adds that layer: per-workspace
19
+ message counts, optional monthly caps that fail closed, and a CLI report
20
+ an admin (or another agent) can read.
21
+
22
+ This project originally set out to add per-user workspace isolation
23
+ (separate chat history, memory, API keys) to a self-hosted AI chat
24
+ platform. A feasibility spike found that premise was false for the target
25
+ platform's current default configuration: per-user ownership on chat
26
+ history, memory, and API tokens is already enforced by default. Rather
27
+ than ship a competing reimplementation of something the target platform
28
+ already does correctly, this project keeps its tested isolation engine
29
+ (namespace separation, an AES-256-GCM vault with real key rotation,
30
+ fail-closed identity resolution, a self-healing circuit breaker) as the
31
+ identity-resolution substrate, and builds the layer that platform
32
+ genuinely does not have: usage metering and quota enforcement per
33
+ workspace. See the [project README](https://github.com/RudrenduPaul/workspaceguard#readme)
34
+ for the full story.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install workspaceguard-cli
40
+ ```
41
+
42
+ **Current status**: this Python port is fully built, tested (32/32 pytest
43
+ tests passing), and packaged -- the wheel and sdist are built and verified,
44
+ installing and running correctly end to end in a fresh virtual environment.
45
+ The first PyPI publish is pending: PyPI's own account-level throttle on
46
+ registering brand-new project names ("429 Too many new projects created")
47
+ is currently blocking the upload, unrelated to any 2FA requirement (this
48
+ account's PyPI publishing needs no human 2FA). It will publish as soon as
49
+ that throttle window clears. Separately, the npm package (`workspaceguard-cli`)
50
+ is built with CI green but not yet published to the npm registry either
51
+ (blocked on a manual 2FA-gated publish step). Neither `pip install
52
+ workspaceguard-cli` nor `npm install -g workspaceguard-cli` is live yet as
53
+ of this writing -- check [PyPI](https://pypi.org/project/workspaceguard-cli/)
54
+ for current availability. This package is a genuine, independent port --
55
+ not a wrapper around the npm package -- so it will work regardless of npm's
56
+ status once it publishes.
57
+
58
+ ## Quickstart
59
+
60
+ ```bash
61
+ # Register the workspaces sharing one deployment (identity = the header
62
+ # value your reverse proxy sets after authenticating, e.g. Cloudflare
63
+ # Access).
64
+ workspaceguard add-workspace alex --identity alex@example.com
65
+ workspaceguard add-workspace jordan --identity jordan@example.com
66
+
67
+ # Optional: cap alex at 1000 messages/month. Omit for unlimited (the default).
68
+ workspaceguard set-cap alex 1000
69
+
70
+ # See usage for every workspace.
71
+ workspaceguard usage
72
+ ```
73
+
74
+ ```
75
+ $ workspaceguard usage --json
76
+ {"ok": true, "usage": [{"workspaceId": "alex", "identity": "alex@example.com", "monthlyMessageCap": 1000, "percentUsed": 0, "period": "2026-07", "messageCount": 0, "estimatedBytes": 0}]}
77
+ ```
78
+
79
+ ## CLI reference
80
+
81
+ Every command accepts `--json` for a structured, agent-native output shape
82
+ instead of the human-readable text shown below. Identical command surface
83
+ to the npm CLI.
84
+
85
+ | Command | What it does |
86
+ | --- | --- |
87
+ | `workspaceguard init` | Initializes the data directory and vault for this deployment. |
88
+ | `workspaceguard add-workspace <id> --identity <value>` | Registers a workspace, idempotent on repeat calls for the same id. |
89
+ | `workspaceguard status [--json]` | Lists configured workspaces. |
90
+ | `workspaceguard usage [--json]` | Per-workspace message count, cap, and percent-used for the current month. |
91
+ | `workspaceguard set-cap <id> <count\|none>` | Sets or clears a workspace's monthly message cap. |
92
+ | `workspaceguard rotate-key <id>` | Rotates a workspace's vault encryption key (invalidates the old ciphertext). |
93
+ | `workspaceguard scan [--json]` | Isolation config scan (scaffold stub, carried over from the original build). |
94
+
95
+ ## Library API
96
+
97
+ ```python
98
+ import asyncio
99
+ from workspaceguard import create_workspace_guard, MockAdapter, QuotaExceededError
100
+
101
+ async def main():
102
+ guard = await create_workspace_guard(data_dir="./data", backend=MockAdapter())
103
+ await guard.add_workspace("alex", "alex@example.com")
104
+ await guard.set_cap("alex", 1000)
105
+
106
+ try:
107
+ await guard.chat("alex@example.com", "hello")
108
+ except QuotaExceededError:
109
+ pass # alex is over their monthly cap
110
+
111
+ report = await guard.usage_report()
112
+
113
+ asyncio.run(main())
114
+ ```
115
+
116
+ The library API is async (`asyncio`), matching the async architecture of
117
+ the original TypeScript source rather than flattening it to synchronous
118
+ calls.
119
+
120
+ ## How it works
121
+
122
+ ```
123
+ target: an inbound chat request with an identity header value
124
+ -> resolve_workspace() -- fail closed on any miss, never a default workspace
125
+ -> check_quota() -- QuotaExceededError if the workspace is at its cap
126
+ -> circuit breaker -- calls the BackendAdapter, opens after 3 consecutive
127
+ failures, self-heals via a half-open probe after a cooldown
128
+ -> record usage -- per-workspace, per-month counters with automatic
129
+ period rollover
130
+ ```
131
+
132
+ - `workspaceguard/isolation_guard.py` -- the single choke point (`chat()`)
133
+ every request flows through: resolve workspace -> check quota -> call
134
+ backend -> record usage.
135
+ - `workspaceguard/usage.py` -- the usage-metering engine this project adds:
136
+ per-workspace, per-month counters with automatic period rollover, and
137
+ `QuotaExceededError` enforcement.
138
+ - `workspaceguard/vault.py`, `workspaceguard/namespace.py`,
139
+ `workspaceguard/circuit_breaker.py` -- the original isolation-engine
140
+ code, kept as the identity/workspace-boundary substrate the metering
141
+ layer reads from, not shipped as a competing isolation product.
142
+ - `workspaceguard/adapters/` -- `BackendAdapter` abstract base class; a
143
+ real Odysseus HTTP adapter is the next step (currently `MockAdapter`
144
+ only, same as the TypeScript original).
145
+
146
+ Backend-specific behavior never enters the core modules directly --
147
+ everything goes through `BackendAdapter`.
148
+
149
+ ## Trust boundary
150
+
151
+ WorkspaceGuard trusts an upstream identity header (default:
152
+ `Cf-Access-Authenticated-User-Email`) to resolve the workspace. It must
153
+ never be directly reachable from the network -- only from behind whatever
154
+ trusted proxy sets that header (Cloudflare Access, Tailscale, etc.). This
155
+ is documented, not code-enforced.
156
+
157
+ ## What's real vs. not yet built
158
+
159
+ - Real, tested (28/28 pytest tests passing): usage metering, quota
160
+ enforcement, the original isolation engine (vault, namespace separation,
161
+ circuit breaker), CLI with `--json` mode.
162
+ - Not yet built: a real Odysseus HTTP adapter (only `MockAdapter` exists so
163
+ far, same as the TypeScript original), a hosted managed billing
164
+ dashboard (deliberately out of scope for this MIT project).
165
+
166
+ ## Security
167
+
168
+ The vault uses AES-256-GCM (via the `cryptography` package) with one
169
+ derived key per workspace per generation -- rotating a workspace's key
170
+ increments its generation, so old ciphertext genuinely can no longer be
171
+ decrypted, not a no-op. See
172
+ [SECURITY.md](https://github.com/RudrenduPaul/workspaceguard/blob/main/SECURITY.md)
173
+ for the disclosure process and what's in/out of scope.
174
+
175
+ ## Contributing
176
+
177
+ See [CONTRIBUTING.md](https://github.com/RudrenduPaul/workspaceguard/blob/main/CONTRIBUTING.md).
178
+
179
+ ```bash
180
+ cd python
181
+ python3 -m venv .venv && source .venv/bin/activate
182
+ pip install -e ".[dev]"
183
+ pytest
184
+ ```
185
+
186
+ ## License
187
+
188
+ MIT, see [LICENSE](https://github.com/RudrenduPaul/workspaceguard/blob/main/LICENSE).
@@ -0,0 +1,70 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "workspaceguard-cli"
7
+ version = "0.1.1"
8
+ description = "Per-workspace usage metering and quota caps for shared self-hosted AI assistant deployments (Odysseus and compatible backends)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Rudrendu Paul" },
14
+ { name = "Sourav Nandy" },
15
+ ]
16
+ keywords = ["self-hosted", "ai-assistant", "usage-metering", "quota", "billing", "multi-tenant", "reverse-proxy", "odysseus", "cli", "agent-native"]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Environment :: Console",
20
+ "Intended Audience :: Developers",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.9",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Topic :: Security",
30
+ "Topic :: System :: Systems Administration",
31
+ ]
32
+ dependencies = [
33
+ "PyYAML>=6.0,<7",
34
+ "cryptography>=42,<46",
35
+ ]
36
+
37
+ [project.optional-dependencies]
38
+ dev = [
39
+ "pytest>=7.0,<9",
40
+ "pytest-asyncio>=0.23,<1",
41
+ "build>=1.0,<2",
42
+ "twine>=5.0,<7",
43
+ ]
44
+
45
+ [project.urls]
46
+ Homepage = "https://github.com/RudrenduPaul/workspaceguard"
47
+ Repository = "https://github.com/RudrenduPaul/workspaceguard"
48
+ "Bug Tracker" = "https://github.com/RudrenduPaul/workspaceguard/issues"
49
+ Changelog = "https://github.com/RudrenduPaul/workspaceguard/blob/main/CHANGELOG.md"
50
+ Documentation = "https://github.com/RudrenduPaul/workspaceguard/blob/main/docs/getting-started.md"
51
+ "Author - Rudrendu Paul" = "https://github.com/RudrenduPaul"
52
+ "Author - Sourav Nandy" = "https://github.com/Sourav-nandy-ai"
53
+
54
+ [project.scripts]
55
+ workspaceguard = "workspaceguard.cli:main"
56
+
57
+ [tool.hatch.build.targets.wheel]
58
+ packages = ["src/workspaceguard"]
59
+
60
+ [tool.hatch.build.targets.sdist]
61
+ include = [
62
+ "/src/workspaceguard",
63
+ "/tests",
64
+ "/README.md",
65
+ "/LICENSE",
66
+ ]
67
+
68
+ [tool.pytest.ini_options]
69
+ testpaths = ["tests"]
70
+ asyncio_mode = "auto"
@@ -0,0 +1,78 @@
1
+ """
2
+ Programmatic / agent-native entry point.
3
+
4
+ from workspaceguard import create_workspace_guard, MockAdapter
5
+
6
+ guard = await create_workspace_guard(data_dir="./data", backend=MockAdapter())
7
+ await guard.add_workspace("alex", "alex@example.com")
8
+ await guard.set_cap("alex", 1000)
9
+ report = await guard.usage_report()
10
+
11
+ Same core isolation/metering logic as the `workspaceguard` CLI;
12
+ workspaceguard/cli.py is a thin argument-parsing wrapper over this class.
13
+
14
+ This is the Python port of the workspaceguard-cli npm package
15
+ (https://www.npmjs.com/package/workspaceguard-cli, not yet published as of
16
+ this port -- see README for current npm status). See
17
+ https://github.com/RudrenduPaul/workspaceguard for the canonical
18
+ documentation and the original TypeScript source.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ from typing import Optional
23
+
24
+ from .adapters.mock import MockAdapter
25
+ from .config import DuplicateIdentityError, InvalidWorkspaceIdError
26
+ from .isolation_guard import IsolationGuard, WorkspaceUsageReport
27
+ from .types import (
28
+ BackendAdapter,
29
+ BackendCircuitOpenError,
30
+ BackendUnreachableError,
31
+ IdentityNotFoundError,
32
+ VaultDecryptionError,
33
+ WorkspaceNotFoundError,
34
+ )
35
+ from .usage import QuotaExceededError, WorkspaceUsage
36
+
37
+ __version__ = "0.1.1"
38
+
39
+
40
+ async def create_workspace_guard(
41
+ data_dir: str,
42
+ backend: BackendAdapter,
43
+ circuit_open_cooldown_ms: Optional[int] = None,
44
+ ) -> IsolationGuard:
45
+ """
46
+ The library entry point -- the "agent-native" surface. An orchestrator
47
+ process can call this directly instead of shelling out to the CLI. No
48
+ default backend -- a caller must be explicit about which adapter it
49
+ wants (the real Odysseus adapter ships once the feasibility spike
50
+ confirms clean HTTP interception; MockAdapter is exported for tests and
51
+ local experimentation).
52
+ """
53
+ guard = IsolationGuard(
54
+ data_dir=data_dir,
55
+ backend=backend,
56
+ circuit_open_cooldown_ms=circuit_open_cooldown_ms,
57
+ )
58
+ await guard.init()
59
+ return guard
60
+
61
+
62
+ __all__ = [
63
+ "create_workspace_guard",
64
+ "IsolationGuard",
65
+ "WorkspaceUsageReport",
66
+ "BackendAdapter",
67
+ "MockAdapter",
68
+ "IdentityNotFoundError",
69
+ "VaultDecryptionError",
70
+ "BackendUnreachableError",
71
+ "BackendCircuitOpenError",
72
+ "WorkspaceNotFoundError",
73
+ "DuplicateIdentityError",
74
+ "InvalidWorkspaceIdError",
75
+ "QuotaExceededError",
76
+ "WorkspaceUsage",
77
+ "__version__",
78
+ ]
@@ -0,0 +1,3 @@
1
+ from .mock import MockAdapter
2
+
3
+ __all__ = ["MockAdapter"]
@@ -0,0 +1,39 @@
1
+ """
2
+ In-memory mock backend, used for the scaffold-stage isolation tests only.
3
+ Ported from src/adapters/mock.ts. The real Odysseus adapter is blocked on a
4
+ feasibility spike: does Odysseus's HTTP API expose clean interception
5
+ points, or does it read/write some of this directly from disk/DB in a way
6
+ an HTTP-level proxy can't see? See docs/integrations/backends.md.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import Dict, List
11
+
12
+ from ..types import BackendAdapter
13
+
14
+
15
+ class MockAdapter(BackendAdapter):
16
+ name = "mock"
17
+
18
+ def __init__(self) -> None:
19
+ self._messages_by_workspace: Dict[str, List[str]] = {}
20
+ self._failures_remaining = 0
21
+
22
+ async def health_check(self) -> bool:
23
+ return True
24
+
25
+ async def forward_chat(self, workspace_id: str, message: str) -> str:
26
+ if self._failures_remaining > 0:
27
+ self._failures_remaining -= 1
28
+ raise RuntimeError("simulated backend failure")
29
+ existing = self._messages_by_workspace.setdefault(workspace_id, [])
30
+ existing.append(message)
31
+ return f"echo: {message}"
32
+
33
+ def _fail_next_calls(self, n: int) -> None:
34
+ """Test-only: makes the next N forward_chat calls fail, to exercise the circuit breaker."""
35
+ self._failures_remaining = n
36
+
37
+ def _messages_for(self, workspace_id: str) -> List[str]:
38
+ """Test-only inspection point -- never exposed through the public adapter interface."""
39
+ return self._messages_by_workspace.get(workspace_id, [])