Per-workspace usage metering and quota caps for shared self-hosted AI assistant deployments (Odysseus and compatible backends).
Project description
workspaceguard-cli (Python)
Per-workspace usage metering and quota caps for one shared self-hosted AI
assistant deployment -- a genuine, independent Python port of the
workspaceguard-cli npm
package, not a wrapper around a Node binary.
Why this exists
Run Odysseus (or a compatible self-hosted assistant) for your whole household or small team, and there is no way to see who sent how many messages this month, or to stop one person's usage from burning through everyone else's API budget. WorkspaceGuard is a thin sidecar that adds that layer: per-workspace message counts, optional monthly caps that fail closed, and a CLI report an admin (or another agent) can read.
This project originally set out to add per-user workspace isolation (separate chat history, memory, API keys) to a self-hosted AI chat platform. A feasibility spike found that premise was false for the target platform's current default configuration: per-user ownership on chat history, memory, and API tokens is already enforced by default. Rather than ship a competing reimplementation of something the target platform already does correctly, this project keeps its tested isolation engine (namespace separation, an AES-256-GCM vault with real key rotation, fail-closed identity resolution, a self-healing circuit breaker) as the identity-resolution substrate, and builds the layer that platform genuinely does not have: usage metering and quota enforcement per workspace. See the project README for the full story.
Install
pip install workspaceguard-cli
Current status: this Python port is fully built, tested (32/32 pytest
tests passing), and packaged -- the wheel and sdist are built and verified,
installing and running correctly end to end in a fresh virtual environment.
The first PyPI publish is pending: PyPI's own account-level throttle on
registering brand-new project names ("429 Too many new projects created")
is currently blocking the upload, unrelated to any 2FA requirement (this
account's PyPI publishing needs no human 2FA). It will publish as soon as
that throttle window clears. Separately, the npm package (workspaceguard-cli)
is built with CI green but not yet published to the npm registry either
(blocked on a manual 2FA-gated publish step). Neither pip install workspaceguard-cli nor npm install -g workspaceguard-cli is live yet as
of this writing -- check PyPI
for current availability. This package is a genuine, independent port --
not a wrapper around the npm package -- so it will work regardless of npm's
status once it publishes.
Quickstart
# Register the workspaces sharing one deployment (identity = the header
# value your reverse proxy sets after authenticating, e.g. Cloudflare
# Access).
workspaceguard add-workspace alex --identity alex@example.com
workspaceguard add-workspace jordan --identity jordan@example.com
# Optional: cap alex at 1000 messages/month. Omit for unlimited (the default).
workspaceguard set-cap alex 1000
# See usage for every workspace.
workspaceguard usage
$ workspaceguard usage --json
{"ok": true, "usage": [{"workspaceId": "alex", "identity": "alex@example.com", "monthlyMessageCap": 1000, "percentUsed": 0, "period": "2026-07", "messageCount": 0, "estimatedBytes": 0}]}
CLI reference
Every command accepts --json for a structured, agent-native output shape
instead of the human-readable text shown below. Identical command surface
to the npm CLI.
| Command | What it does |
|---|---|
workspaceguard init |
Initializes the data directory and vault for this deployment. |
workspaceguard add-workspace <id> --identity <value> |
Registers a workspace, idempotent on repeat calls for the same id. |
workspaceguard status [--json] |
Lists configured workspaces. |
workspaceguard usage [--json] |
Per-workspace message count, cap, and percent-used for the current month. |
workspaceguard set-cap <id> <count|none> |
Sets or clears a workspace's monthly message cap. |
workspaceguard rotate-key <id> |
Rotates a workspace's vault encryption key (invalidates the old ciphertext). |
workspaceguard scan [--json] |
Isolation config scan (scaffold stub, carried over from the original build). |
Library API
import asyncio
from workspaceguard import create_workspace_guard, MockAdapter, QuotaExceededError
async def main():
guard = await create_workspace_guard(data_dir="./data", backend=MockAdapter())
await guard.add_workspace("alex", "alex@example.com")
await guard.set_cap("alex", 1000)
try:
await guard.chat("alex@example.com", "hello")
except QuotaExceededError:
pass # alex is over their monthly cap
report = await guard.usage_report()
asyncio.run(main())
The library API is async (asyncio), matching the async architecture of
the original TypeScript source rather than flattening it to synchronous
calls.
How it works
target: an inbound chat request with an identity header value
-> resolve_workspace() -- fail closed on any miss, never a default workspace
-> check_quota() -- QuotaExceededError if the workspace is at its cap
-> circuit breaker -- calls the BackendAdapter, opens after 3 consecutive
failures, self-heals via a half-open probe after a cooldown
-> record usage -- per-workspace, per-month counters with automatic
period rollover
workspaceguard/isolation_guard.py-- the single choke point (chat()) every request flows through: resolve workspace -> check quota -> call backend -> record usage.workspaceguard/usage.py-- the usage-metering engine this project adds: per-workspace, per-month counters with automatic period rollover, andQuotaExceededErrorenforcement.workspaceguard/vault.py,workspaceguard/namespace.py,workspaceguard/circuit_breaker.py-- the original isolation-engine code, kept as the identity/workspace-boundary substrate the metering layer reads from, not shipped as a competing isolation product.workspaceguard/adapters/--BackendAdapterabstract base class; a real Odysseus HTTP adapter is the next step (currentlyMockAdapteronly, same as the TypeScript original).
Backend-specific behavior never enters the core modules directly --
everything goes through BackendAdapter.
Trust boundary
WorkspaceGuard trusts an upstream identity header (default:
Cf-Access-Authenticated-User-Email) to resolve the workspace. It must
never be directly reachable from the network -- only from behind whatever
trusted proxy sets that header (Cloudflare Access, Tailscale, etc.). This
is documented, not code-enforced.
What's real vs. not yet built
- Real, tested (28/28 pytest tests passing): usage metering, quota
enforcement, the original isolation engine (vault, namespace separation,
circuit breaker), CLI with
--jsonmode. - Not yet built: a real Odysseus HTTP adapter (only
MockAdapterexists so far, same as the TypeScript original), a hosted managed billing dashboard (deliberately out of scope for this MIT project).
Security
The vault uses AES-256-GCM (via the cryptography package) with one
derived key per workspace per generation -- rotating a workspace's key
increments its generation, so old ciphertext genuinely can no longer be
decrypted, not a no-op. See
SECURITY.md
for the disclosure process and what's in/out of scope.
Contributing
See CONTRIBUTING.md.
cd python
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest
License
MIT, see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file workspaceguard_cli-0.1.1.tar.gz.
File metadata
- Download URL: workspaceguard_cli-0.1.1.tar.gz
- Upload date:
- Size: 21.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2cefea63cb8368e45163672b57437c3e4de1ade4e51ee891bfa234c823ffd621
|
|
| MD5 |
da157f64a1824cd699041e1b1825f7d6
|
|
| BLAKE2b-256 |
a5cb02655d18573047174afd97aecced1582f86b73b56fceebf1086f711f3755
|
Provenance
The following attestation bundles were made for workspaceguard_cli-0.1.1.tar.gz:
Publisher:
publish-pypi.yml on RudrenduPaul/workspaceguard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
workspaceguard_cli-0.1.1.tar.gz -
Subject digest:
2cefea63cb8368e45163672b57437c3e4de1ade4e51ee891bfa234c823ffd621 - Sigstore transparency entry: 2194926400
- Sigstore integration time:
-
Permalink:
RudrenduPaul/workspaceguard@644c4bb2fb355b3aebec7d1ab4b93aa2ed316298 -
Branch / Tag:
refs/tags/python-v0.1.1 - Owner: https://github.com/RudrenduPaul
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@644c4bb2fb355b3aebec7d1ab4b93aa2ed316298 -
Trigger Event:
release
-
Statement type:
File details
Details for the file workspaceguard_cli-0.1.1-py3-none-any.whl.
File metadata
- Download URL: workspaceguard_cli-0.1.1-py3-none-any.whl
- Upload date:
- Size: 23.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0859d254622229e8d4d6f314aab9b70ae5852dca06180b32c18e9f61c709a923
|
|
| MD5 |
d1969b65b111920b8ebff0b986092af4
|
|
| BLAKE2b-256 |
b6f9d8ad2aad4742cb01db6da9285c34d5c62e89cb18c4ac7114cedf6f43abf8
|
Provenance
The following attestation bundles were made for workspaceguard_cli-0.1.1-py3-none-any.whl:
Publisher:
publish-pypi.yml on RudrenduPaul/workspaceguard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
workspaceguard_cli-0.1.1-py3-none-any.whl -
Subject digest:
0859d254622229e8d4d6f314aab9b70ae5852dca06180b32c18e9f61c709a923 - Sigstore transparency entry: 2194926402
- Sigstore integration time:
-
Permalink:
RudrenduPaul/workspaceguard@644c4bb2fb355b3aebec7d1ab4b93aa2ed316298 -
Branch / Tag:
refs/tags/python-v0.1.1 - Owner: https://github.com/RudrenduPaul
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@644c4bb2fb355b3aebec7d1ab4b93aa2ed316298 -
Trigger Event:
release
-
Statement type: