tenuo-claude-code 0.2.2__tar.gz → 0.3.0__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.
- tenuo_claude_code-0.3.0/PKG-INFO +286 -0
- tenuo_claude_code-0.3.0/README.md +256 -0
- tenuo_claude_code-0.3.0/demo/README.md +73 -0
- tenuo_claude_code-0.3.0/docs/DETAILS.md +186 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/pyproject.toml +1 -1
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/src/tenuo_claude_code/__init__.py +1 -1
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/src/tenuo_claude_code/admin.py +59 -30
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/src/tenuo_claude_code/cli.py +502 -57
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/src/tenuo_claude_code/verify.py +10 -8
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/tests/test_admin.py +110 -0
- tenuo_claude_code-0.3.0/tests/test_check.py +178 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/tests/test_constraints.py +34 -1
- tenuo_claude_code-0.3.0/tests/test_dx_improvements.py +110 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/tests/test_routing.py +125 -7
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/tests/test_scaffold.py +70 -0
- tenuo_claude_code-0.2.2/PKG-INFO +0 -598
- tenuo_claude_code-0.2.2/README.md +0 -568
- tenuo_claude_code-0.2.2/demo/README.md +0 -113
- tenuo_claude_code-0.2.2/docs/DETAILS.md +0 -302
- tenuo_claude_code-0.2.2/docs/images/README.md +0 -22
- tenuo_claude_code-0.2.2/tests/test_check.py +0 -74
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/.gitignore +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/CONTRIBUTING.md +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/LICENSE +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/demo/.claude/agents/researcher.md +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/demo/.mcp.json +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/demo/docs/README.md +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/demo/fake-secrets.env +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/demo/ops_server.py +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/demo/sandbox/incident-report.md +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/demo/sandbox/notes.txt +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/demo/tenuo.yaml +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/demo/tenuo_demo.py +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/docs/TROUBLESHOOTING.md +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/docs/images/cloud-audit-stream.png +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/docs/images/cloud-receipt-approval-detail.png +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/docs/images/tenuo_claude_code_architecture.png +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/docs/images/tenuo_claude_code_architecture.svg +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/examples/policies/README.md +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/examples/policies/audit-rollout.yaml +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/examples/policies/enforce-with-mcp.yaml +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/examples/policies/read-only-research.yaml +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/src/tenuo_claude_code/authorizer_runtime.py +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/src/tenuo_claude_code/data/harness_tools.yaml +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/src/tenuo_claude_code/paths.py +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/src/tenuo_claude_code/templates/tenuo.yaml.example +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/templates/README.md +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/templates/admin.env.example +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/templates/cloud.env.example +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/templates/tenuo.yaml.advanced.example +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/templates/tenuo.yaml.cloud.example +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/templates/tenuo.yaml.example +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/tests/conftest.py +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/tests/test_approvals.py +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/tests/test_authorize_call.py +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/tests/test_authorizer_runtime.py +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/tests/test_cloud_bindings.py +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/tests/test_permissions.py +0 -0
- {tenuo_claude_code-0.2.2 → tenuo_claude_code-0.3.0}/tests/test_subagents.py +0 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tenuo-claude-code
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Tenuo governance for Claude Code — warrants, hooks, MCP proxy, and Cloud lifecycle
|
|
5
|
+
Project-URL: Homepage, https://tenuo.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/tenuo-ai/claude-governance
|
|
7
|
+
Project-URL: Documentation, https://github.com/tenuo-ai/claude-governance#readme
|
|
8
|
+
Project-URL: Issues, https://github.com/tenuo-ai/claude-governance/issues
|
|
9
|
+
Author: Tenuo Contributors
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,claude-code,governance,mcp,security,warrants
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: certifi>=2024.0
|
|
26
|
+
Requires-Dist: mcp>=1.0
|
|
27
|
+
Requires-Dist: pyyaml>=6.0
|
|
28
|
+
Requires-Dist: tenuo==0.1.0b24
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Tenuo for Claude Code
|
|
32
|
+
|
|
33
|
+
[](https://pypi.org/project/tenuo-claude-code/)
|
|
34
|
+
[](https://pypi.org/project/tenuo-claude-code/)
|
|
35
|
+
[](https://github.com/tenuo-ai/claude-governance/actions/workflows/ci.yml)
|
|
36
|
+
[](LICENSE)
|
|
37
|
+
|
|
38
|
+
Claude Code can read files, run shell commands, fetch URLs, and call MCP tools.
|
|
39
|
+
Tenuo lets you write a `tenuo.yaml` policy for which of those calls are allowed,
|
|
40
|
+
then checks every model-invoked tool call before it runs.
|
|
41
|
+
|
|
42
|
+
That policy is compiled into a signed, expiring credential called a warrant. A
|
|
43
|
+
local authorizer checks each tool call against it and logs the decision. The
|
|
44
|
+
same policy is applied no matter why the model tried the call: prompt injection,
|
|
45
|
+
hallucination, poisoned tool output, or an unsafe request.
|
|
46
|
+
|
|
47
|
+
Start with the local quickstart below. Optional Cloud control-plane setup is in
|
|
48
|
+
[Cloud mode](#cloud-mode).
|
|
49
|
+
|
|
50
|
+
## Quickstart
|
|
51
|
+
|
|
52
|
+
Requires Python 3.10+. Uses Docker if it's running; otherwise a native authorizer binary, installed automatically. On Windows, run these commands from WSL.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install tenuo-claude-code
|
|
56
|
+
mkdir my-project && cd my-project
|
|
57
|
+
tenuo-claude bootstrap
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`bootstrap` writes a starter `tenuo.yaml`, starts the authorizer, and runs a self-test (`verify`). It runs non-interactively, ideal for a fresh folder. For a guided wizard with prompts, run `tenuo-claude onboard` instead; it's the same flow with a preflight `check`. The starter policy is deliberately strict:
|
|
61
|
+
|
|
62
|
+
```yaml
|
|
63
|
+
name: my-project
|
|
64
|
+
sandbox: ./workspace
|
|
65
|
+
mode: enforce
|
|
66
|
+
enforce:
|
|
67
|
+
Read: "subpath:{sandbox}" # Read only files under ./workspace
|
|
68
|
+
Bash: "shlex:ls,pwd,echo,date" # Bash only these commands
|
|
69
|
+
default: deny # every other tool call is denied
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Now open Claude Code in `my-project/`:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
claude
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The agent is governed: `Read ./workspace/notes.txt` is allowed; `Read /etc/passwd`, `Bash(curl …)`, or any tool not listed is denied and logged. After Claude runs a few tools, run `tenuo-claude audit` to see the decisions. Edit `tenuo.yaml` to fit your project (see [Policy](#policy)), then `tenuo-claude refresh`.
|
|
79
|
+
|
|
80
|
+
For an example with MCP and subagents, see the [reference demo](demo/).
|
|
81
|
+
|
|
82
|
+
| Next | Go to |
|
|
83
|
+
|------|-------|
|
|
84
|
+
| Write the policy | [Policy](#policy) |
|
|
85
|
+
| Day-to-day commands | [Commands](#commands) |
|
|
86
|
+
| Org root, receipts, approvals, revocation | [Cloud mode](#cloud-mode) |
|
|
87
|
+
| Security model and limits | [Security](#security) |
|
|
88
|
+
| Something broke | [Troubleshooting](docs/TROUBLESHOOTING.md) · deep dive [docs/DETAILS.md](docs/DETAILS.md) |
|
|
89
|
+
|
|
90
|
+
## Policy
|
|
91
|
+
|
|
92
|
+
`tenuo.yaml` is the whole configuration: it drives the warrant, the authorizer, the hooks, and the MCP proxy. You list tools under `enforce:` and give each a constraint on its key argument.
|
|
93
|
+
|
|
94
|
+
```yaml
|
|
95
|
+
name: acme-backend
|
|
96
|
+
sandbox: ./workspace # a directory; {sandbox} expands to its absolute path
|
|
97
|
+
mode: enforce # block out-of-scope calls. 'audit' = log only, don't block
|
|
98
|
+
|
|
99
|
+
enforce:
|
|
100
|
+
Read: "subpath:{sandbox}"
|
|
101
|
+
Write: "subpath:{sandbox}"
|
|
102
|
+
Bash: "shlex:ls,pwd,echo,cat,grep"
|
|
103
|
+
WebFetch:
|
|
104
|
+
domains: ["api.github.com", "*.githubusercontent.com"]
|
|
105
|
+
cidrs: ["10.0.0.0/8"] # optional: also allow hosts in these IP ranges
|
|
106
|
+
|
|
107
|
+
default: deny # anything not listed above is denied
|
|
108
|
+
|
|
109
|
+
subagents: # optional: each role runs under a narrower warrant
|
|
110
|
+
analyst:
|
|
111
|
+
tools: [Read, Grep, Glob]
|
|
112
|
+
|
|
113
|
+
mcp: # optional: govern a downstream MCP server's tools
|
|
114
|
+
downstream: ./your_mcp_server.py
|
|
115
|
+
enforce:
|
|
116
|
+
read_file: "subpath:{sandbox}" # bare string constrains the `path` arg
|
|
117
|
+
run_query: # constrain a differently-named arg
|
|
118
|
+
arg: sql
|
|
119
|
+
constraint: "regex:^SELECT "
|
|
120
|
+
http_call: # constrain several args at once
|
|
121
|
+
args:
|
|
122
|
+
url: "urlpattern:https://api.example.com/*"
|
|
123
|
+
method: "oneof:GET,HEAD"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Constraints
|
|
127
|
+
|
|
128
|
+
| Constraint | Applies to | What it checks |
|
|
129
|
+
|------------|------------|----------------|
|
|
130
|
+
| `subpath:DIR` | path tools (Read, Write, Edit, Glob, Grep) | the path argument must resolve to a location **inside `DIR`**. Symlinks are resolved first, so a link planted in the directory can't point outside it. |
|
|
131
|
+
| `shlex:a,b,c` | Bash, Monitor | the command's **executable** must be one of `a,b,c`, and the command must be a single simple command: pipes, `&&`/`;` chaining, subshells, and shell expansion are rejected. (This allowlists the *verb*, not file paths: `cat /etc/passwd` passes if `cat` is allowed. Use `Read`/`Write` to scope files; drop `Bash` for a hard lock.) |
|
|
132
|
+
| `domains` / `cidrs` / `schemes` / `ports` | WebFetch | the URL's host must match an allowed domain (`*` matches one label) or CIDR range, **and** pass SSRF hygiene: https-only by default (override with `schemes`), optional `ports` allowlist, with loopback, cloud-metadata IPs, encoded-IP tricks, and spoofed hosts (`api.github.com.evil.com`) blocked. |
|
|
133
|
+
| `oneof:a,b` · `notoneof:a,b` · `exact:v` · `pattern:glob` · `regex:re` · `range:min,max` · `urlpattern:url` · `cidr:n/m` | any tool argument | the value must be in the set / not in the set / equal / glob-match / regex-match / fall in the numeric range (either bound may be blank) / match the URL glob / fall inside the IP range (for any IP-based tool, not just WebFetch). |
|
|
134
|
+
|
|
135
|
+
The keys above are the `tenuo.yaml` DSL's convenient subset. The underlying tenuo engine supports more (set operations, numeric ranges, negation, boolean composition, and CEL expressions) for programmatic policies — see [tenuo](https://github.com/tenuo-ai/tenuo).
|
|
136
|
+
|
|
137
|
+
`{sandbox}` is a convenience variable for the directory in `sandbox:`; you can point `subpath:` at any path. Tools you don't list aren't governed individually; they're caught by `default`.
|
|
138
|
+
|
|
139
|
+
**Command-execution tools.** `Bash`, `PowerShell`, and `Monitor` all execute commands and are each governed independently (their own constraint on the `command` argument). `Monitor` runs the same shell commands as `Bash` in the background; `PowerShell` is a different dialect, so prefer `oneof`/`pattern`/`regex` over `shlex` (which parses POSIX syntax) for it. If your team enables `PowerShell` or `Monitor` in Claude Code, list them under `enforce:` too. Left unlisted they fall to `default`: `deny` blocks them, but `audit` logs them **unconstrained**, so on `default: audit` give every enabled shell an explicit constraint.
|
|
140
|
+
|
|
141
|
+
**MCP tool arguments.** Under `mcp.enforce:`, a bare constraint string targets the `path` argument. To constrain a differently-named argument use `arg: NAME` + `constraint:`, and to constrain several at once use `args: {NAME: constraint, …}`. This works for any downstream MCP tool and any constraint kind, locally and on Cloud. Tools you don't list are still allowed/denied by `default` and can be human-approval gated (`approval:`); they just aren't argument-constrained.
|
|
142
|
+
|
|
143
|
+
- **`mode: enforce`** blocks denied calls. **`mode: audit`** computes and logs the same decisions but blocks nothing; use it to dry-run a policy, then switch to `enforce`.
|
|
144
|
+
- **`default: deny`** denies any tool not listed (recommended). `default: audit` logs-and-allows unknown tools instead.
|
|
145
|
+
- **`subagents:`** declares roles; spawning is gated to those roles, and each runs under the session warrant **attenuated** to its `tools` (it can only ever do less than the session). [Details](docs/DETAILS.md#subagents).
|
|
146
|
+
|
|
147
|
+
Ready-made policies: [examples/policies/](examples/policies/). After any edit, run `tenuo-claude refresh`.
|
|
148
|
+
|
|
149
|
+
## Commands
|
|
150
|
+
|
|
151
|
+
Day to day, you mostly need `up` (start), `audit` (review), and `refresh` (after editing policy).
|
|
152
|
+
|
|
153
|
+
| Command | What it does |
|
|
154
|
+
|---------|--------------|
|
|
155
|
+
| `onboard` | Interactive first-run wizard (`--local` / `--cloud`); same flow as `bootstrap` but prompts and runs a preflight `check`. Scaffolds an example policy if you don't have one. |
|
|
156
|
+
| `bootstrap` | First-run quickstart (used above): non-interactive scaffold starter policy (if none) → `init` → `up` → `verify`. `--cloud` for Cloud. |
|
|
157
|
+
| `init` | Compile an **existing** `tenuo.yaml`: mint the warrant, wire the PreToolUse hook and MCP proxy. Pass `--scaffold` to write an example if none exists (it no longer does so automatically). |
|
|
158
|
+
| `up` / `down` | Start / stop the authorizer (auto-selects Docker or native; `--native` to force). |
|
|
159
|
+
| `refresh` | Recompile after editing `tenuo.yaml` (restarts the authorizer if running). In Cloud mode, warns if capability rules drifted from the last `tenuo-admin setup`. |
|
|
160
|
+
| `verify [--deep]` | Self-test the live policy against the authorizer (no Claude session needed). `--deep` adds an SSRF / encoded-IP matrix, extra Bash deny cases, and a live PreToolUse exit-code harness: a reproducible artifact for security review. |
|
|
161
|
+
| `audit [--tail N]` | Show the decision log (`.state/receipts.jsonl`). |
|
|
162
|
+
| `check` | Preflight: dependencies, wiring, audit-sink health, leaked admin keys, and (Cloud) control-plane bindings. |
|
|
163
|
+
| `status` | Warrant, mode, audit-sink health, and Cloud summary. |
|
|
164
|
+
| `install-authorizer` | Install the native authorizer to `~/.tenuo/bin` (no Docker, no Cargo). |
|
|
165
|
+
| `bench [--json]` | Measure per-call overhead on your machine (PoP sign, authorizer round-trip, full hook path). |
|
|
166
|
+
| `revoke` | Revoke the current session warrant. |
|
|
167
|
+
|
|
168
|
+
The warrant is short-lived (~1h TTL); `up` refreshes it. The authorizer listens on `127.0.0.1:9090`; change it with `TENUO_AUTHORIZER_PORT` before `bootstrap`. Generated files (don't commit): `.state/` (keys, warrant, credentials), `.claude/settings.json` (hooks), `.mcp.json` (MCP wiring).
|
|
169
|
+
|
|
170
|
+
Working from a git clone instead of PyPI? See [Build from source](#build-from-source).
|
|
171
|
+
|
|
172
|
+
## How enforcement works
|
|
173
|
+
|
|
174
|
+
`init` compiles `tenuo.yaml` into a signed warrant and wires two interception points; both check the **same** warrant against the **same** local authorizer:
|
|
175
|
+
|
|
176
|
+
- **Native tools** (Read, Bash, WebFetch, …) → a Claude Code **PreToolUse hook** intercepts the call, signs a proof-of-possession with the session key, and asks the authorizer.
|
|
177
|
+
- **MCP tools** → Claude is pointed at a **proxy** that stands in for the downstream MCP server; the proxy authorizes, then forwards only if allowed.
|
|
178
|
+
|
|
179
|
+

|
|
180
|
+
|
|
181
|
+
The authorizer (a small local service, ~1–3 ms/call) verifies the warrant's signature, proof-of-possession, and expiry, then checks the call's arguments against the warrant's constraints → allow, deny, or (Cloud) approval-required. Full detail in [docs/DETAILS.md](docs/DETAILS.md).
|
|
182
|
+
|
|
183
|
+
## Cloud mode
|
|
184
|
+
|
|
185
|
+
Local mode is enough to evaluate Tenuo on one project. Connect [cloud.tenuo.ai](https://cloud.tenuo.ai) for organization-scale governance:
|
|
186
|
+
|
|
187
|
+
- **Tenant-root warrants**: sessions chain to your org root, not a key on the laptop.
|
|
188
|
+
- **Signed receipts**: one verifiable allow/deny/approval audit stream (Ed25519 over CBOR).
|
|
189
|
+
- **Fleet revocation**: revoke a warrant id; authorizers pick it up within ~30s.
|
|
190
|
+
- **Human approval gates**: specific calls pause for a person instead of allow/deny ([below](#human-approval-cloud)).
|
|
191
|
+
- **Managed rollout**: push hook/MCP wiring through Claude Code managed settings instead of per-project local settings.
|
|
192
|
+
|
|
193
|
+

|
|
194
|
+
|
|
195
|
+
### Setup
|
|
196
|
+
|
|
197
|
+
Two keys, kept apart; the runtime never sees the admin key:
|
|
198
|
+
|
|
199
|
+
| Key | From | Goes in | Used by |
|
|
200
|
+
|-----|------|---------|---------|
|
|
201
|
+
| **Runtime** (`tenuo_ct_…`) | cloud.tenuo.ai → Agents → Quick Connect → **Authorizer Only** | `.state/cloud.env` | `tenuo-claude up`, hooks |
|
|
202
|
+
| **Tenant-admin** (`tc_…`) | Settings → API Keys → Create (admin role) | `~/.tenuo/admin.env` | `tenuo-admin setup` (once) |
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
mkdir my-project && cd my-project
|
|
206
|
+
tenuo-claude bootstrap --cloud # wizard prompts for the runtime token, then sets up + verifies
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Every session after that:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
tenuo-claude check && tenuo-claude up
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
If `check` reports a **cloud bindings** failure, run `tenuo-admin setup` and retry.
|
|
216
|
+
|
|
217
|
+
> `tenuo-claude up` refuses to start if a tenant-admin key is in the environment; keep it only in `~/.tenuo/admin.env`. And once a project is on Cloud, don't re-run plain `bootstrap`: it reverts the project to local mode and moves your Cloud files aside. Use `check && up`.
|
|
218
|
+
|
|
219
|
+
CI / non-interactive and manual step-by-step setup: [docs/DETAILS.md § Tenuo Cloud](docs/DETAILS.md#tenuo-cloud-extended). After changing `enforce`/`mcp`/`subagents`/approvals on Cloud, re-run `tenuo-admin setup`; for `mode`-only changes, `refresh` suffices.
|
|
220
|
+
|
|
221
|
+
### Human approval (Cloud)
|
|
222
|
+
|
|
223
|
+
A gated capability returns a third outcome, `approval-required`, instead of allow/deny. The hook opens a Cloud approval request, waits for an approver on their notification channel, then re-authorizes with their signed approval. The repo ships two worked examples (off-allowlist `WebFetch`, and `delete_deployment` with `target=production`). Setup and policy shape: [docs/DETAILS.md § Human approval](docs/DETAILS.md#human-approval-cloud).
|
|
224
|
+
|
|
225
|
+

|
|
226
|
+
|
|
227
|
+
## Security
|
|
228
|
+
|
|
229
|
+
Tenuo runs **alongside** Claude Code permissions; it doesn't replace managed settings. The difference is where and how policy is enforced:
|
|
230
|
+
|
|
231
|
+
| | Claude Code permissions | Tenuo warrant |
|
|
232
|
+
|---|---|---|
|
|
233
|
+
| Form | Allow/ask/deny rules in settings | Signed, expiring capability token; Cloud chains to your org root |
|
|
234
|
+
| Enforcement point | Claude's permission UI | PreToolUse hook + MCP proxy, checked by the authorizer |
|
|
235
|
+
| `--dangerously-skip-permissions` | Skips the prompts | Does not disable installed Tenuo hook/proxy checks |
|
|
236
|
+
| Expiry | Until edited | ~1h session TTL; `up` refreshes |
|
|
237
|
+
| Revocation | Edit rules (live sessions may keep allowances) | Revoke warrant id → ~30s fleet sync (Cloud) |
|
|
238
|
+
| Evidence | Optional hook logs | Local JSONL; signed receipt stream in Cloud |
|
|
239
|
+
| Org deployment | Per-user settings, locally editable | Managed-settings hooks + shared policy |
|
|
240
|
+
|
|
241
|
+
Admins can also block the bypass flag entirely in managed settings (`disableBypassPermissionsMode`).
|
|
242
|
+
|
|
243
|
+
**What's in scope.** Tenuo governs **model-invoked tool calls** (Read, Bash, WebFetch, MCP tools, subagent spawns) on the PreToolUse path, including the agent's own Bash. The TUI `!` shell (a command the *operator* types) is not a tool call and is out of scope; the model can't invoke it. ([details](docs/DETAILS.md#agent-tools-vs-operator-shell))
|
|
244
|
+
|
|
245
|
+
**Fail-closed.** A missing or broken `tenuo.yaml` denies every governed call until it's restored. Keys under `.state/` must be owner-only (`0600`).
|
|
246
|
+
|
|
247
|
+
**Receipts.** Every governed call carries a proof-of-possession signature the authorizer verifies. Locally, the hook appends a JSON line to `.state/receipts.jsonl` (read with `tenuo-claude audit`; in `mode: audit`, denials show as `WOULD-DENY`):
|
|
248
|
+
|
|
249
|
+
```json
|
|
250
|
+
{"phase":"pre","decision":"deny","claude_tool":"Read","governed":true,
|
|
251
|
+
"args":{"file_path":"/etc/passwd"},"reason":"Constraint not satisfied"}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Connected to Cloud, the authorizer also emits **signed** receipts to your tenant, the verifiable record for compliance and fleet audit.
|
|
255
|
+
|
|
256
|
+
**Rolling out to a team.** Keep `tenuo.yaml` in version control, push the hook/MCP wiring through Claude Code **managed settings** (not per-developer `settings.local.json`), and use Cloud for org-root warrants, central audit, and revocation. Start in `mode: audit`, review the `WOULD-DENY` rows, then switch to `enforce`. [Talk to us](https://tenuo.ai/early-access.html) about managed-settings rollout. Report issues: [SECURITY.md](SECURITY.md).
|
|
257
|
+
|
|
258
|
+
## Build from source
|
|
259
|
+
|
|
260
|
+
For development, running the demo from a checkout, or using `./bin/tenuo-claude`:
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
git clone https://github.com/tenuo-ai/claude-governance.git
|
|
264
|
+
cd claude-governance
|
|
265
|
+
uv venv && uv sync && source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
266
|
+
chmod +x bin/tenuo-claude
|
|
267
|
+
uv run tenuo-claude install-authorizer # only if you don't use Docker
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Run via `./bin/tenuo-claude --help`, `uv run tenuo-claude --help`, or `pip install -e .`. Re-run `tenuo-claude init` (or `refresh`) after moving the repo or reinstalling. The hooks pin the launcher path at wiring time. Contributors: [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
271
|
+
|
|
272
|
+
## Performance
|
|
273
|
+
|
|
274
|
+
Authorization is ~1–3 ms per call; the command hook adds ~100–200 ms (mostly process startup). Measure on your machine with `tenuo-claude bench` after `up`.
|
|
275
|
+
|
|
276
|
+
## This repo
|
|
277
|
+
|
|
278
|
+
GitHub: [`tenuo-ai/claude-governance`](https://github.com/tenuo-ai/claude-governance) · PyPI: [`tenuo-claude-code`](https://pypi.org/project/tenuo-claude-code/)
|
|
279
|
+
|
|
280
|
+
| Path | Contents |
|
|
281
|
+
|------|----------|
|
|
282
|
+
| `src/tenuo_claude_code/` | Package source |
|
|
283
|
+
| `templates/` | Starter `tenuo.yaml` and credential examples |
|
|
284
|
+
| `examples/policies/` | Ready-made policy templates |
|
|
285
|
+
| `demo/` | Reference project and scripted tour |
|
|
286
|
+
| `docs/` | [Implementation details](docs/DETAILS.md) · [Troubleshooting](docs/TROUBLESHOOTING.md) |
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Tenuo for Claude Code
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/tenuo-claude-code/)
|
|
4
|
+
[](https://pypi.org/project/tenuo-claude-code/)
|
|
5
|
+
[](https://github.com/tenuo-ai/claude-governance/actions/workflows/ci.yml)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Claude Code can read files, run shell commands, fetch URLs, and call MCP tools.
|
|
9
|
+
Tenuo lets you write a `tenuo.yaml` policy for which of those calls are allowed,
|
|
10
|
+
then checks every model-invoked tool call before it runs.
|
|
11
|
+
|
|
12
|
+
That policy is compiled into a signed, expiring credential called a warrant. A
|
|
13
|
+
local authorizer checks each tool call against it and logs the decision. The
|
|
14
|
+
same policy is applied no matter why the model tried the call: prompt injection,
|
|
15
|
+
hallucination, poisoned tool output, or an unsafe request.
|
|
16
|
+
|
|
17
|
+
Start with the local quickstart below. Optional Cloud control-plane setup is in
|
|
18
|
+
[Cloud mode](#cloud-mode).
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
Requires Python 3.10+. Uses Docker if it's running; otherwise a native authorizer binary, installed automatically. On Windows, run these commands from WSL.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install tenuo-claude-code
|
|
26
|
+
mkdir my-project && cd my-project
|
|
27
|
+
tenuo-claude bootstrap
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`bootstrap` writes a starter `tenuo.yaml`, starts the authorizer, and runs a self-test (`verify`). It runs non-interactively, ideal for a fresh folder. For a guided wizard with prompts, run `tenuo-claude onboard` instead; it's the same flow with a preflight `check`. The starter policy is deliberately strict:
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
name: my-project
|
|
34
|
+
sandbox: ./workspace
|
|
35
|
+
mode: enforce
|
|
36
|
+
enforce:
|
|
37
|
+
Read: "subpath:{sandbox}" # Read only files under ./workspace
|
|
38
|
+
Bash: "shlex:ls,pwd,echo,date" # Bash only these commands
|
|
39
|
+
default: deny # every other tool call is denied
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Now open Claude Code in `my-project/`:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
claude
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The agent is governed: `Read ./workspace/notes.txt` is allowed; `Read /etc/passwd`, `Bash(curl …)`, or any tool not listed is denied and logged. After Claude runs a few tools, run `tenuo-claude audit` to see the decisions. Edit `tenuo.yaml` to fit your project (see [Policy](#policy)), then `tenuo-claude refresh`.
|
|
49
|
+
|
|
50
|
+
For an example with MCP and subagents, see the [reference demo](demo/).
|
|
51
|
+
|
|
52
|
+
| Next | Go to |
|
|
53
|
+
|------|-------|
|
|
54
|
+
| Write the policy | [Policy](#policy) |
|
|
55
|
+
| Day-to-day commands | [Commands](#commands) |
|
|
56
|
+
| Org root, receipts, approvals, revocation | [Cloud mode](#cloud-mode) |
|
|
57
|
+
| Security model and limits | [Security](#security) |
|
|
58
|
+
| Something broke | [Troubleshooting](docs/TROUBLESHOOTING.md) · deep dive [docs/DETAILS.md](docs/DETAILS.md) |
|
|
59
|
+
|
|
60
|
+
## Policy
|
|
61
|
+
|
|
62
|
+
`tenuo.yaml` is the whole configuration: it drives the warrant, the authorizer, the hooks, and the MCP proxy. You list tools under `enforce:` and give each a constraint on its key argument.
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
name: acme-backend
|
|
66
|
+
sandbox: ./workspace # a directory; {sandbox} expands to its absolute path
|
|
67
|
+
mode: enforce # block out-of-scope calls. 'audit' = log only, don't block
|
|
68
|
+
|
|
69
|
+
enforce:
|
|
70
|
+
Read: "subpath:{sandbox}"
|
|
71
|
+
Write: "subpath:{sandbox}"
|
|
72
|
+
Bash: "shlex:ls,pwd,echo,cat,grep"
|
|
73
|
+
WebFetch:
|
|
74
|
+
domains: ["api.github.com", "*.githubusercontent.com"]
|
|
75
|
+
cidrs: ["10.0.0.0/8"] # optional: also allow hosts in these IP ranges
|
|
76
|
+
|
|
77
|
+
default: deny # anything not listed above is denied
|
|
78
|
+
|
|
79
|
+
subagents: # optional: each role runs under a narrower warrant
|
|
80
|
+
analyst:
|
|
81
|
+
tools: [Read, Grep, Glob]
|
|
82
|
+
|
|
83
|
+
mcp: # optional: govern a downstream MCP server's tools
|
|
84
|
+
downstream: ./your_mcp_server.py
|
|
85
|
+
enforce:
|
|
86
|
+
read_file: "subpath:{sandbox}" # bare string constrains the `path` arg
|
|
87
|
+
run_query: # constrain a differently-named arg
|
|
88
|
+
arg: sql
|
|
89
|
+
constraint: "regex:^SELECT "
|
|
90
|
+
http_call: # constrain several args at once
|
|
91
|
+
args:
|
|
92
|
+
url: "urlpattern:https://api.example.com/*"
|
|
93
|
+
method: "oneof:GET,HEAD"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Constraints
|
|
97
|
+
|
|
98
|
+
| Constraint | Applies to | What it checks |
|
|
99
|
+
|------------|------------|----------------|
|
|
100
|
+
| `subpath:DIR` | path tools (Read, Write, Edit, Glob, Grep) | the path argument must resolve to a location **inside `DIR`**. Symlinks are resolved first, so a link planted in the directory can't point outside it. |
|
|
101
|
+
| `shlex:a,b,c` | Bash, Monitor | the command's **executable** must be one of `a,b,c`, and the command must be a single simple command: pipes, `&&`/`;` chaining, subshells, and shell expansion are rejected. (This allowlists the *verb*, not file paths: `cat /etc/passwd` passes if `cat` is allowed. Use `Read`/`Write` to scope files; drop `Bash` for a hard lock.) |
|
|
102
|
+
| `domains` / `cidrs` / `schemes` / `ports` | WebFetch | the URL's host must match an allowed domain (`*` matches one label) or CIDR range, **and** pass SSRF hygiene: https-only by default (override with `schemes`), optional `ports` allowlist, with loopback, cloud-metadata IPs, encoded-IP tricks, and spoofed hosts (`api.github.com.evil.com`) blocked. |
|
|
103
|
+
| `oneof:a,b` · `notoneof:a,b` · `exact:v` · `pattern:glob` · `regex:re` · `range:min,max` · `urlpattern:url` · `cidr:n/m` | any tool argument | the value must be in the set / not in the set / equal / glob-match / regex-match / fall in the numeric range (either bound may be blank) / match the URL glob / fall inside the IP range (for any IP-based tool, not just WebFetch). |
|
|
104
|
+
|
|
105
|
+
The keys above are the `tenuo.yaml` DSL's convenient subset. The underlying tenuo engine supports more (set operations, numeric ranges, negation, boolean composition, and CEL expressions) for programmatic policies — see [tenuo](https://github.com/tenuo-ai/tenuo).
|
|
106
|
+
|
|
107
|
+
`{sandbox}` is a convenience variable for the directory in `sandbox:`; you can point `subpath:` at any path. Tools you don't list aren't governed individually; they're caught by `default`.
|
|
108
|
+
|
|
109
|
+
**Command-execution tools.** `Bash`, `PowerShell`, and `Monitor` all execute commands and are each governed independently (their own constraint on the `command` argument). `Monitor` runs the same shell commands as `Bash` in the background; `PowerShell` is a different dialect, so prefer `oneof`/`pattern`/`regex` over `shlex` (which parses POSIX syntax) for it. If your team enables `PowerShell` or `Monitor` in Claude Code, list them under `enforce:` too. Left unlisted they fall to `default`: `deny` blocks them, but `audit` logs them **unconstrained**, so on `default: audit` give every enabled shell an explicit constraint.
|
|
110
|
+
|
|
111
|
+
**MCP tool arguments.** Under `mcp.enforce:`, a bare constraint string targets the `path` argument. To constrain a differently-named argument use `arg: NAME` + `constraint:`, and to constrain several at once use `args: {NAME: constraint, …}`. This works for any downstream MCP tool and any constraint kind, locally and on Cloud. Tools you don't list are still allowed/denied by `default` and can be human-approval gated (`approval:`); they just aren't argument-constrained.
|
|
112
|
+
|
|
113
|
+
- **`mode: enforce`** blocks denied calls. **`mode: audit`** computes and logs the same decisions but blocks nothing; use it to dry-run a policy, then switch to `enforce`.
|
|
114
|
+
- **`default: deny`** denies any tool not listed (recommended). `default: audit` logs-and-allows unknown tools instead.
|
|
115
|
+
- **`subagents:`** declares roles; spawning is gated to those roles, and each runs under the session warrant **attenuated** to its `tools` (it can only ever do less than the session). [Details](docs/DETAILS.md#subagents).
|
|
116
|
+
|
|
117
|
+
Ready-made policies: [examples/policies/](examples/policies/). After any edit, run `tenuo-claude refresh`.
|
|
118
|
+
|
|
119
|
+
## Commands
|
|
120
|
+
|
|
121
|
+
Day to day, you mostly need `up` (start), `audit` (review), and `refresh` (after editing policy).
|
|
122
|
+
|
|
123
|
+
| Command | What it does |
|
|
124
|
+
|---------|--------------|
|
|
125
|
+
| `onboard` | Interactive first-run wizard (`--local` / `--cloud`); same flow as `bootstrap` but prompts and runs a preflight `check`. Scaffolds an example policy if you don't have one. |
|
|
126
|
+
| `bootstrap` | First-run quickstart (used above): non-interactive scaffold starter policy (if none) → `init` → `up` → `verify`. `--cloud` for Cloud. |
|
|
127
|
+
| `init` | Compile an **existing** `tenuo.yaml`: mint the warrant, wire the PreToolUse hook and MCP proxy. Pass `--scaffold` to write an example if none exists (it no longer does so automatically). |
|
|
128
|
+
| `up` / `down` | Start / stop the authorizer (auto-selects Docker or native; `--native` to force). |
|
|
129
|
+
| `refresh` | Recompile after editing `tenuo.yaml` (restarts the authorizer if running). In Cloud mode, warns if capability rules drifted from the last `tenuo-admin setup`. |
|
|
130
|
+
| `verify [--deep]` | Self-test the live policy against the authorizer (no Claude session needed). `--deep` adds an SSRF / encoded-IP matrix, extra Bash deny cases, and a live PreToolUse exit-code harness: a reproducible artifact for security review. |
|
|
131
|
+
| `audit [--tail N]` | Show the decision log (`.state/receipts.jsonl`). |
|
|
132
|
+
| `check` | Preflight: dependencies, wiring, audit-sink health, leaked admin keys, and (Cloud) control-plane bindings. |
|
|
133
|
+
| `status` | Warrant, mode, audit-sink health, and Cloud summary. |
|
|
134
|
+
| `install-authorizer` | Install the native authorizer to `~/.tenuo/bin` (no Docker, no Cargo). |
|
|
135
|
+
| `bench [--json]` | Measure per-call overhead on your machine (PoP sign, authorizer round-trip, full hook path). |
|
|
136
|
+
| `revoke` | Revoke the current session warrant. |
|
|
137
|
+
|
|
138
|
+
The warrant is short-lived (~1h TTL); `up` refreshes it. The authorizer listens on `127.0.0.1:9090`; change it with `TENUO_AUTHORIZER_PORT` before `bootstrap`. Generated files (don't commit): `.state/` (keys, warrant, credentials), `.claude/settings.json` (hooks), `.mcp.json` (MCP wiring).
|
|
139
|
+
|
|
140
|
+
Working from a git clone instead of PyPI? See [Build from source](#build-from-source).
|
|
141
|
+
|
|
142
|
+
## How enforcement works
|
|
143
|
+
|
|
144
|
+
`init` compiles `tenuo.yaml` into a signed warrant and wires two interception points; both check the **same** warrant against the **same** local authorizer:
|
|
145
|
+
|
|
146
|
+
- **Native tools** (Read, Bash, WebFetch, …) → a Claude Code **PreToolUse hook** intercepts the call, signs a proof-of-possession with the session key, and asks the authorizer.
|
|
147
|
+
- **MCP tools** → Claude is pointed at a **proxy** that stands in for the downstream MCP server; the proxy authorizes, then forwards only if allowed.
|
|
148
|
+
|
|
149
|
+

|
|
150
|
+
|
|
151
|
+
The authorizer (a small local service, ~1–3 ms/call) verifies the warrant's signature, proof-of-possession, and expiry, then checks the call's arguments against the warrant's constraints → allow, deny, or (Cloud) approval-required. Full detail in [docs/DETAILS.md](docs/DETAILS.md).
|
|
152
|
+
|
|
153
|
+
## Cloud mode
|
|
154
|
+
|
|
155
|
+
Local mode is enough to evaluate Tenuo on one project. Connect [cloud.tenuo.ai](https://cloud.tenuo.ai) for organization-scale governance:
|
|
156
|
+
|
|
157
|
+
- **Tenant-root warrants**: sessions chain to your org root, not a key on the laptop.
|
|
158
|
+
- **Signed receipts**: one verifiable allow/deny/approval audit stream (Ed25519 over CBOR).
|
|
159
|
+
- **Fleet revocation**: revoke a warrant id; authorizers pick it up within ~30s.
|
|
160
|
+
- **Human approval gates**: specific calls pause for a person instead of allow/deny ([below](#human-approval-cloud)).
|
|
161
|
+
- **Managed rollout**: push hook/MCP wiring through Claude Code managed settings instead of per-project local settings.
|
|
162
|
+
|
|
163
|
+

|
|
164
|
+
|
|
165
|
+
### Setup
|
|
166
|
+
|
|
167
|
+
Two keys, kept apart; the runtime never sees the admin key:
|
|
168
|
+
|
|
169
|
+
| Key | From | Goes in | Used by |
|
|
170
|
+
|-----|------|---------|---------|
|
|
171
|
+
| **Runtime** (`tenuo_ct_…`) | cloud.tenuo.ai → Agents → Quick Connect → **Authorizer Only** | `.state/cloud.env` | `tenuo-claude up`, hooks |
|
|
172
|
+
| **Tenant-admin** (`tc_…`) | Settings → API Keys → Create (admin role) | `~/.tenuo/admin.env` | `tenuo-admin setup` (once) |
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
mkdir my-project && cd my-project
|
|
176
|
+
tenuo-claude bootstrap --cloud # wizard prompts for the runtime token, then sets up + verifies
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Every session after that:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
tenuo-claude check && tenuo-claude up
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
If `check` reports a **cloud bindings** failure, run `tenuo-admin setup` and retry.
|
|
186
|
+
|
|
187
|
+
> `tenuo-claude up` refuses to start if a tenant-admin key is in the environment; keep it only in `~/.tenuo/admin.env`. And once a project is on Cloud, don't re-run plain `bootstrap`: it reverts the project to local mode and moves your Cloud files aside. Use `check && up`.
|
|
188
|
+
|
|
189
|
+
CI / non-interactive and manual step-by-step setup: [docs/DETAILS.md § Tenuo Cloud](docs/DETAILS.md#tenuo-cloud-extended). After changing `enforce`/`mcp`/`subagents`/approvals on Cloud, re-run `tenuo-admin setup`; for `mode`-only changes, `refresh` suffices.
|
|
190
|
+
|
|
191
|
+
### Human approval (Cloud)
|
|
192
|
+
|
|
193
|
+
A gated capability returns a third outcome, `approval-required`, instead of allow/deny. The hook opens a Cloud approval request, waits for an approver on their notification channel, then re-authorizes with their signed approval. The repo ships two worked examples (off-allowlist `WebFetch`, and `delete_deployment` with `target=production`). Setup and policy shape: [docs/DETAILS.md § Human approval](docs/DETAILS.md#human-approval-cloud).
|
|
194
|
+
|
|
195
|
+

|
|
196
|
+
|
|
197
|
+
## Security
|
|
198
|
+
|
|
199
|
+
Tenuo runs **alongside** Claude Code permissions; it doesn't replace managed settings. The difference is where and how policy is enforced:
|
|
200
|
+
|
|
201
|
+
| | Claude Code permissions | Tenuo warrant |
|
|
202
|
+
|---|---|---|
|
|
203
|
+
| Form | Allow/ask/deny rules in settings | Signed, expiring capability token; Cloud chains to your org root |
|
|
204
|
+
| Enforcement point | Claude's permission UI | PreToolUse hook + MCP proxy, checked by the authorizer |
|
|
205
|
+
| `--dangerously-skip-permissions` | Skips the prompts | Does not disable installed Tenuo hook/proxy checks |
|
|
206
|
+
| Expiry | Until edited | ~1h session TTL; `up` refreshes |
|
|
207
|
+
| Revocation | Edit rules (live sessions may keep allowances) | Revoke warrant id → ~30s fleet sync (Cloud) |
|
|
208
|
+
| Evidence | Optional hook logs | Local JSONL; signed receipt stream in Cloud |
|
|
209
|
+
| Org deployment | Per-user settings, locally editable | Managed-settings hooks + shared policy |
|
|
210
|
+
|
|
211
|
+
Admins can also block the bypass flag entirely in managed settings (`disableBypassPermissionsMode`).
|
|
212
|
+
|
|
213
|
+
**What's in scope.** Tenuo governs **model-invoked tool calls** (Read, Bash, WebFetch, MCP tools, subagent spawns) on the PreToolUse path, including the agent's own Bash. The TUI `!` shell (a command the *operator* types) is not a tool call and is out of scope; the model can't invoke it. ([details](docs/DETAILS.md#agent-tools-vs-operator-shell))
|
|
214
|
+
|
|
215
|
+
**Fail-closed.** A missing or broken `tenuo.yaml` denies every governed call until it's restored. Keys under `.state/` must be owner-only (`0600`).
|
|
216
|
+
|
|
217
|
+
**Receipts.** Every governed call carries a proof-of-possession signature the authorizer verifies. Locally, the hook appends a JSON line to `.state/receipts.jsonl` (read with `tenuo-claude audit`; in `mode: audit`, denials show as `WOULD-DENY`):
|
|
218
|
+
|
|
219
|
+
```json
|
|
220
|
+
{"phase":"pre","decision":"deny","claude_tool":"Read","governed":true,
|
|
221
|
+
"args":{"file_path":"/etc/passwd"},"reason":"Constraint not satisfied"}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Connected to Cloud, the authorizer also emits **signed** receipts to your tenant, the verifiable record for compliance and fleet audit.
|
|
225
|
+
|
|
226
|
+
**Rolling out to a team.** Keep `tenuo.yaml` in version control, push the hook/MCP wiring through Claude Code **managed settings** (not per-developer `settings.local.json`), and use Cloud for org-root warrants, central audit, and revocation. Start in `mode: audit`, review the `WOULD-DENY` rows, then switch to `enforce`. [Talk to us](https://tenuo.ai/early-access.html) about managed-settings rollout. Report issues: [SECURITY.md](SECURITY.md).
|
|
227
|
+
|
|
228
|
+
## Build from source
|
|
229
|
+
|
|
230
|
+
For development, running the demo from a checkout, or using `./bin/tenuo-claude`:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
git clone https://github.com/tenuo-ai/claude-governance.git
|
|
234
|
+
cd claude-governance
|
|
235
|
+
uv venv && uv sync && source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
236
|
+
chmod +x bin/tenuo-claude
|
|
237
|
+
uv run tenuo-claude install-authorizer # only if you don't use Docker
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Run via `./bin/tenuo-claude --help`, `uv run tenuo-claude --help`, or `pip install -e .`. Re-run `tenuo-claude init` (or `refresh`) after moving the repo or reinstalling. The hooks pin the launcher path at wiring time. Contributors: [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
241
|
+
|
|
242
|
+
## Performance
|
|
243
|
+
|
|
244
|
+
Authorization is ~1–3 ms per call; the command hook adds ~100–200 ms (mostly process startup). Measure on your machine with `tenuo-claude bench` after `up`.
|
|
245
|
+
|
|
246
|
+
## This repo
|
|
247
|
+
|
|
248
|
+
GitHub: [`tenuo-ai/claude-governance`](https://github.com/tenuo-ai/claude-governance) · PyPI: [`tenuo-claude-code`](https://pypi.org/project/tenuo-claude-code/)
|
|
249
|
+
|
|
250
|
+
| Path | Contents |
|
|
251
|
+
|------|----------|
|
|
252
|
+
| `src/tenuo_claude_code/` | Package source |
|
|
253
|
+
| `templates/` | Starter `tenuo.yaml` and credential examples |
|
|
254
|
+
| `examples/policies/` | Ready-made policy templates |
|
|
255
|
+
| `demo/` | Reference project and scripted tour |
|
|
256
|
+
| `docs/` | [Implementation details](docs/DETAILS.md) · [Troubleshooting](docs/TROUBLESHOOTING.md) |
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Reference demo
|
|
2
|
+
|
|
3
|
+
A pre-built project for watching Tenuo enforce a policy against real Claude Code calls — showing how the policy denies an out-of-scope action **regardless of why the agent attempted it**. It ships a sample `tenuo.yaml`, a workspace directory, a small MCP server, and a scripted tour. Commands: [README § Commands](../README.md#commands). Stuck? [Troubleshooting](../docs/TROUBLESHOOTING.md).
|
|
4
|
+
|
|
5
|
+
The headline example: `sandbox/incident-report.md` carries an embedded instruction telling the agent to read `../fake-secrets.env` and to call `delete_deployment` on production. The policy denies both — the file read is outside the `subpath:` directory, and `delete_deployment` isn't a granted capability — so the agent can be steered into *trying*, but not into *doing*.
|
|
6
|
+
|
|
7
|
+
This is one illustration of **cause-agnostic enforcement**. The agent might attempt that read or deletion because of a prompt injection, a model that hallucinated or drifted after a long context window, a malicious instruction buried in tool input, or simply a user who asked for it directly — Tenuo doesn't try to tell these apart. The policy denies the action identically every time. That invariance *is* the product: you can't control what the model thinks or is asked to do, but you can deterministically control what the agent is allowed to do.
|
|
8
|
+
|
|
9
|
+
A note on what you'll actually see: a capable model often self-refuses the embedded instruction on its own, so to watch the *policy* fire you force the attempt explicitly. Treat that forced boundary-push as the realistic "a user asks for something org policy forbids" case — exactly what deterministic governance exists to handle.
|
|
10
|
+
|
|
11
|
+
## Run it
|
|
12
|
+
|
|
13
|
+
From a git checkout, set up the venv once from the repo root:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv venv && uv sync && source .venv/bin/activate
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then, local-only (no Cloud account):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd demo
|
|
23
|
+
tenuo-claude bootstrap
|
|
24
|
+
tenuo-claude demo # scripted authorizer tour
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The scripted tour calls the authorizer directly and prints allow/deny decisions.
|
|
28
|
+
To populate `tenuo-claude audit`, open Claude Code in `demo/` (where
|
|
29
|
+
`tenuo.yaml` lives) or run the live Claude examples below.
|
|
30
|
+
|
|
31
|
+
**With Cloud** (signed receipts, approvals): `tenuo-claude bootstrap --cloud`, then on later sessions `tenuo-claude check && tenuo-claude up`. Credential setup is in [README § Cloud mode](../README.md#cloud-mode) — use the **Authorizer Only** Quick Connect token. Note: once Cloud is configured, don't re-run plain `bootstrap` (it reverts the project to local mode).
|
|
32
|
+
|
|
33
|
+
## What's inside
|
|
34
|
+
|
|
35
|
+
| Path | Purpose |
|
|
36
|
+
|------|---------|
|
|
37
|
+
| `tenuo.yaml` | Sample policy. Ships in `mode: audit` (logs `WOULD-DENY`, blocks nothing) so you can see decisions before enforcing |
|
|
38
|
+
| `sandbox/` | The directory `subpath:` constraints point at. `notes.txt` is in scope; `incident-report.md` carries an embedded out-of-policy instruction |
|
|
39
|
+
| `fake-secrets.env` | Fake credentials, placed **outside** `sandbox/` on purpose — reading it requires escaping the `subpath:` directory, so it's denied |
|
|
40
|
+
| `ops_server.py` | The downstream MCP server. Exposes `read_file` and `list_directory` (granted) plus a simulated `delete_deployment` (not granted → denied). Claude talks to Tenuo's proxy, not this directly |
|
|
41
|
+
| `tenuo_demo.py` | The scripted tour (`tenuo-claude demo`) |
|
|
42
|
+
| `.claude/agents/researcher.md` | A read-only subagent (`Read`/`Grep`/`Glob`) for the spawn-gate examples |
|
|
43
|
+
|
|
44
|
+
Policy overlay templates (Cloud, advanced/approval) live in [`templates/`](../templates/).
|
|
45
|
+
|
|
46
|
+
## Live Claude examples
|
|
47
|
+
|
|
48
|
+
Authorizer up (`tenuo-claude check && tenuo-claude up`), Claude Code on PATH.
|
|
49
|
+
The shipped policy is `mode: audit`, so out-of-scope calls are allowed by
|
|
50
|
+
Claude but logged as `WOULD-DENY`. Set `mode: enforce` and run
|
|
51
|
+
`tenuo-claude refresh` to block them.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
cd demo
|
|
55
|
+
claude -p "Read sandbox/notes.txt and summarize."
|
|
56
|
+
claude -p "Read /etc/hosts" --dangerously-skip-permissions # WOULD-DENY in audit mode
|
|
57
|
+
claude -p "Summarize sandbox/incident-report.md for me." --dangerously-skip-permissions # embedded out-of-policy instruction; WOULD-DENY in audit mode
|
|
58
|
+
claude -p "Use read_file to read sandbox/notes.txt and summarize." --dangerously-skip-permissions
|
|
59
|
+
claude -p "Use read_file to read /etc/passwd." --dangerously-skip-permissions # WOULD-DENY in audit mode
|
|
60
|
+
claude -p "Use delete_deployment to tear down production." --dangerously-skip-permissions # WOULD-DENY in audit mode
|
|
61
|
+
claude -p "Use the researcher subagent to run 'ls -la sandbox'." --dangerously-skip-permissions
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Human approval (optional, Cloud)
|
|
65
|
+
|
|
66
|
+
The demo exercises approver sign-off on **WebFetch** (native hook) and **delete_deployment** (MCP proxy) when approval is configured in policy:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
tenuo-admin setup # after adding the advanced overlay
|
|
70
|
+
tenuo-claude demo --advanced --live-approval # blocks until an approver responds
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Setup and policy shape: [README § Human approval](../README.md#human-approval-cloud) and [docs/DETAILS.md § Human approval](../docs/DETAILS.md#human-approval-cloud). Use `--approver-id` / `cloud.approver_identity_id` for team configs; display-name lookup is for demos only.
|