tenuo-claude-code 0.1.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.1.0/.gitignore +28 -0
- tenuo_claude_code-0.1.0/LICENSE +17 -0
- tenuo_claude_code-0.1.0/PKG-INFO +623 -0
- tenuo_claude_code-0.1.0/README.md +594 -0
- tenuo_claude_code-0.1.0/docs/images/README.md +9 -0
- tenuo_claude_code-0.1.0/pyproject.toml +54 -0
- tenuo_claude_code-0.1.0/src/tenuo_claude_code/__init__.py +3 -0
- tenuo_claude_code-0.1.0/src/tenuo_claude_code/admin.py +583 -0
- tenuo_claude_code-0.1.0/src/tenuo_claude_code/cli.py +2279 -0
- tenuo_claude_code-0.1.0/src/tenuo_claude_code/data/harness_tools.yaml +22 -0
- tenuo_claude_code-0.1.0/src/tenuo_claude_code/paths.py +84 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Generated state (keys, warrant, gateway, receipts, cloud creds) — NEVER commit
|
|
2
|
+
.state/
|
|
3
|
+
|
|
4
|
+
# Generated Claude wiring (hook timeout varies with approval overlay)
|
|
5
|
+
.claude/settings.json
|
|
6
|
+
.claude/settings.local.json
|
|
7
|
+
|
|
8
|
+
# .mcp.json is committed (portable ./bin/tenuo-claude launcher)
|
|
9
|
+
|
|
10
|
+
# Python
|
|
11
|
+
__pycache__/
|
|
12
|
+
*.py[cod]
|
|
13
|
+
*.egg-info/
|
|
14
|
+
.venv/
|
|
15
|
+
dist/
|
|
16
|
+
build/
|
|
17
|
+
|
|
18
|
+
# Local policy overrides (optional per-developer)
|
|
19
|
+
tenuo.yaml.local
|
|
20
|
+
tenuo.cloud.yaml
|
|
21
|
+
tenuo.advanced.yaml
|
|
22
|
+
|
|
23
|
+
# Scratch / agent output in the sandbox
|
|
24
|
+
sandbox/out.txt
|
|
25
|
+
sandbox/_probe.txt
|
|
26
|
+
|
|
27
|
+
# OS / editor
|
|
28
|
+
.DS_Store
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2025-2026 Tenuo Contributors
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tenuo-claude-code
|
|
3
|
+
Version: 0.1.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: mcp>=1.0
|
|
26
|
+
Requires-Dist: pyyaml>=6.0
|
|
27
|
+
Requires-Dist: tenuo==0.1.0b24
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# Tenuo for Claude Code
|
|
31
|
+
|
|
32
|
+
[Tenuo](https://tenuo.ai) governance for [Claude Code](https://code.claude.com/docs):
|
|
33
|
+
every agent tool call is checked against a signed warrant (hook → authorizer),
|
|
34
|
+
with a receipt on each decision, including under `--dangerously-skip-permissions`.
|
|
35
|
+
Policy is `tenuo.yaml` in your project directory; `tenuo-claude init` generates
|
|
36
|
+
the warrant, authorizer config, Claude hooks, and MCP proxy wiring.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
**PyPI** (your project anywhere on disk):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install tenuo-claude-code
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
PyPI: [pypi.org/project/tenuo-claude-code](https://pypi.org/project/tenuo-claude-code/)
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cd your-project # must contain tenuo.yaml
|
|
50
|
+
tenuo-claude init
|
|
51
|
+
tenuo-claude up
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Demo repo** (git clone — includes sample policy, sandbox, MCP demo server):
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://github.com/tenuo-ai/claude-governance.git
|
|
58
|
+
cd claude-governance
|
|
59
|
+
uv venv && uv sync && chmod +x bin/tenuo-claude
|
|
60
|
+
uv run tenuo-claude bootstrap --local
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`tenuo-claude` and `tenuo-admin` remain as CLI aliases for backward compatibility.
|
|
64
|
+
|
|
65
|
+
## Quick start (local, ~1 minute)
|
|
66
|
+
|
|
67
|
+
Requires Python ≥ 3.10, Docker, and [Claude Code](https://code.claude.com/docs).
|
|
68
|
+
|
|
69
|
+
After install (see above), bootstrap runs preflight → init → up → doctor. Open Claude
|
|
70
|
+
Code in the directory that contains `tenuo.yaml`.
|
|
71
|
+
|
|
72
|
+
Other entry points:
|
|
73
|
+
|
|
74
|
+
| Command | When |
|
|
75
|
+
|---------|------|
|
|
76
|
+
| `tenuo-claude check` | Diagnose deps, credentials, wiring drift |
|
|
77
|
+
| `tenuo-claude onboard` | Interactive wizard (local or Cloud) |
|
|
78
|
+
| `tenuo-claude onboard --cloud` | Cloud wizard (Quick Connect + optional admin setup) |
|
|
79
|
+
| `tenuo-claude init --cloud` | Write `tenuo.cloud.yaml` (Cloud URL only) |
|
|
80
|
+
| `tenuo-claude refresh` | Re-apply `tenuo.yaml` after policy edits |
|
|
81
|
+
|
|
82
|
+
Cloud credentials and platform setup: see [Cloud mode](#cloud-mode) below.
|
|
83
|
+
**Advanced demo** (optional WebFetch human approval): [docs/PRESENTATION.md](docs/PRESENTATION.md) —
|
|
84
|
+
separate `tenuo.advanced.yaml` overlay; not part of the default tour.
|
|
85
|
+
|
|
86
|
+
## Setup
|
|
87
|
+
|
|
88
|
+
**Python environment (recommended — [uv](https://docs.astral.sh/uv/)):**
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
uv venv && uv sync
|
|
92
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Or without uv: `python3 -m pip install -r requirements.txt` (same pins).
|
|
96
|
+
|
|
97
|
+
Re-run `init` after switching venvs — hooks pin `sys.executable` in `.claude/settings.json`.
|
|
98
|
+
|
|
99
|
+
### Local mode
|
|
100
|
+
|
|
101
|
+
No Tenuo Cloud account. Warrants are minted from a **local issuer key** in `.state/`.
|
|
102
|
+
Receipts stay in `.state/receipts.jsonl`. Off-allowlist `WebFetch` URLs are **denied**
|
|
103
|
+
(no human approval).
|
|
104
|
+
|
|
105
|
+
**1. Policy** — stock `tenuo.yaml` is ready. Do **not** add `cloud:` or
|
|
106
|
+
`WebFetch.approval` (those are Cloud-only).
|
|
107
|
+
|
|
108
|
+
**2. Ensure Cloud files are absent** (otherwise `up` stays in Cloud mode):
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# if you previously ran Cloud setup:
|
|
112
|
+
mv .state/cloud.env .state/cloud.env.bak 2>/dev/null
|
|
113
|
+
mv .state/cloud_state.json .state/cloud_state.json.bak 2>/dev/null
|
|
114
|
+
unset TENUO_ADMIN_KEY TENUO_CONNECT_TOKEN TENUO_API_KEY TENUO_CONTROL_PLANE_URL
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**3. Initialize and run:**
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
python3 tenuo_claude.py init # mint local warrant, wire hooks + MCP proxy
|
|
121
|
+
python3 tenuo_claude.py refresh # after editing tenuo.yaml (policy → warrant + gateway)
|
|
122
|
+
python3 tenuo_claude.py up # should print: Local mode (no Cloud).
|
|
123
|
+
python3 tenuo_claude.py doctor --no-live
|
|
124
|
+
python3 tenuo_demo.py
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**4. Verify** — `python3 tenuo_claude.py status` should show:
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
authorizer : up (http://127.0.0.1:9090) | cloud: disabled
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
No `web-approval:` line. Revoke with `python3 tenuo_claude.py revoke`.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
### Cloud mode
|
|
138
|
+
|
|
139
|
+
Root-signed session warrants, central receipt stream in
|
|
140
|
+
[cloud.tenuo.ai](https://cloud.tenuo.ai), fleet revocation (~30s SRL sync). Optional
|
|
141
|
+
**human approval** on off-allowlist `WebFetch` is an [advanced demo add-on](#advanced-demo-human-approval-optional), not default setup.
|
|
142
|
+
Presentation runbook: [docs/PRESENTATION.md](docs/PRESENTATION.md).
|
|
143
|
+
|
|
144
|
+
**1. Tenant + keys** — you need **two API keys** in **two files** (separation of duties):
|
|
145
|
+
|
|
146
|
+
| Key | Role | File | Used by |
|
|
147
|
+
|-----|------|------|---------|
|
|
148
|
+
| **Runtime** | Quick Connect authorizer service account | `.state/cloud.env` | `tenuo_claude.py up`, hooks, demo |
|
|
149
|
+
| **Admin** | Tenant admin (not in Quick Connect) | `~/.tenuo/admin.env` | `tenuo_admin.py setup` **once** |
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
mkdir -p .state ~/.tenuo
|
|
153
|
+
cp cloud.env.example .state/cloud.env
|
|
154
|
+
cp admin.env.example ~/.tenuo/admin.env
|
|
155
|
+
# Edit cloud.env — see Quick Connect steps below.
|
|
156
|
+
# Edit admin.env — tenant-admin key (separate from Quick Connect).
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### Runtime key via Quick Connect
|
|
160
|
+
|
|
161
|
+
Quick Connect copies a single **connect token** (`tenuo_ct_…`) that bundles the
|
|
162
|
+
control-plane URL and authorizer bearer key. Put that in `.state/cloud.env` as
|
|
163
|
+
`TENUO_CONNECT_TOKEN` — you do **not** need to set `TENUO_API_KEY` separately.
|
|
164
|
+
|
|
165
|
+
| What you copy | Env var | What it is |
|
|
166
|
+
|---------------|---------|------------|
|
|
167
|
+
| Connect token from dashboard | `TENUO_CONNECT_TOKEN` | One paste; preferred |
|
|
168
|
+
| Manual tab: URL + key | `TENUO_CONTROL_PLANE_URL` + `TENUO_API_KEY` | Fallback if you skip the token |
|
|
169
|
+
|
|
170
|
+
Internally, Cloud HTTP calls use the embedded `tc_…` bearer key (the `k` field
|
|
171
|
+
inside the token). The demo parses `TENUO_CONNECT_TOKEN` and passes that key to
|
|
172
|
+
the authorizer container as `TENUO_API_KEY`.
|
|
173
|
+
|
|
174
|
+
Quick Connect does **not** include the tenant-admin key.
|
|
175
|
+
|
|
176
|
+
1. Sign in at [cloud.tenuo.ai](https://cloud.tenuo.ai)
|
|
177
|
+
2. **Agents** → **Quick Connect**
|
|
178
|
+
3. Connection type: **Authorizer Only** (holder agent registration is done by
|
|
179
|
+
`tenuo-admin setup`, not Quick Connect)
|
|
180
|
+
4. Copy the connect token (`tenuo_ct_…`) into `.state/cloud.env`:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
export TENUO_CONNECT_TOKEN="tenuo_ct_..."
|
|
184
|
+
export TENUO_AUTHORIZER_NAME="claude-code-demo"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Or choose deployment **Manual** in the dialog and paste `TENUO_CONTROL_PLANE_URL`
|
|
188
|
+
+ `TENUO_API_KEY` instead (see `cloud.env.example`).
|
|
189
|
+
|
|
190
|
+
The token is shown **once**. Do not use `ak_…` values from the API Keys table — those
|
|
191
|
+
are key IDs, not bearer secrets. Quick Connect embeds the real `tc_…` runtime key.
|
|
192
|
+
|
|
193
|
+
#### Admin key (not in Quick Connect)
|
|
194
|
+
|
|
195
|
+
Create separately: **Settings → API Keys** → tenant-admin role — or use the key from
|
|
196
|
+
tenant onboarding. Save to `~/.tenuo/admin.env` only.
|
|
197
|
+
|
|
198
|
+
**Why Authorizer Only (not Agent + Authorizer)?** Quick Connect **Agent + Authorizer**
|
|
199
|
+
bundles an agent identity for embedded SDKs that auto-claim on startup. This demo uses
|
|
200
|
+
a **sidecar authorizer** plus a separate **holder agent**: PoP is signed by
|
|
201
|
+
`.state/holder_key.b64` in the Claude hook, while Quick Connect credentials only
|
|
202
|
+
authenticate the authorizer to Cloud (heartbeat, SRL, trigger fire). `tenuo-admin setup`
|
|
203
|
+
registers the holder agent and claims it with that local key; Cloud then issues
|
|
204
|
+
warrants bound to it. Agent + Authorizer Quick Connect would claim a different key
|
|
205
|
+
and break PoP verification.
|
|
206
|
+
|
|
207
|
+
**Important:** never put the admin key in `.state/cloud.env` or your shell when running
|
|
208
|
+
`tenuo-claude` / `tenuo_demo` — runtime refuses to start if an admin key is reachable.
|
|
209
|
+
|
|
210
|
+
**2. Cloud policy** — `tenuo-claude init --cloud` writes `tenuo.cloud.yaml` (control-plane
|
|
211
|
+
URL only). No yaml merge required.
|
|
212
|
+
|
|
213
|
+
**3. One-time Cloud registration** (platform / prep — not every session):
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
unset TENUO_ADMIN_KEY # only needed if exported in your shell
|
|
217
|
+
python3 tenuo_admin.py setup
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Creates the holder agent, Cloud trigger, and (if `tenuo.advanced.yaml` is present) approval
|
|
221
|
+
policy. Writes `.state/cloud_state.json`. Re-run after **policy changes** (`tenuo.yaml`,
|
|
222
|
+
`tenuo.cloud.yaml`, `tenuo.advanced.yaml` (if present), `subagents:`).
|
|
223
|
+
|
|
224
|
+
**4. Daily developer flow:**
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
unset TENUO_ADMIN_KEY
|
|
228
|
+
python3 tenuo_claude.py init # wire hooks; re-run after venv changes
|
|
229
|
+
python3 tenuo_claude.py refresh # after tenuo.yaml policy edits (warrant + gateway)
|
|
230
|
+
python3 tenuo_claude.py up # fires trigger → root-signed session warrant
|
|
231
|
+
python3 tenuo_claude.py doctor --no-live
|
|
232
|
+
python3 tenuo_demo.py
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**5. Verify** — `python3 tenuo_claude.py status` should show:
|
|
236
|
+
|
|
237
|
+
```text
|
|
238
|
+
authorizer : up (…) | cloud: registered authz_…
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
(`web-approval:` in `status` appears only with `tenuo.advanced.yaml` — see
|
|
242
|
+
[Advanced demo](#advanced-demo-human-approval-optional) below.)
|
|
243
|
+
|
|
244
|
+
Revoke from Cloud dashboard or `tenuo-claude status` warrant id (~30s SRL sync).
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
### Advanced demo: human approval (optional)
|
|
249
|
+
|
|
250
|
+
The **default** demo (`python3 tenuo_demo.py`) covers scope, deny, SSRF, and subagents —
|
|
251
|
+
off-allowlist `WebFetch` is **denied by the domain allowlist**, not sent for approval.
|
|
252
|
+
|
|
253
|
+
Human approval on off-allowlist URLs is an **advanced add-on** for customer presentations.
|
|
254
|
+
Use a separate overlay so it never mixes with the core tool or default tour:
|
|
255
|
+
|
|
256
|
+
**Prerequisite — approver in Tenuo Cloud (platform prep, not this repo):**
|
|
257
|
+
|
|
258
|
+
`tenuo-admin setup` **references** an existing approver; it does not create one. Before
|
|
259
|
+
`init --advanced`, someone with dashboard access must:
|
|
260
|
+
|
|
261
|
+
1. **Connect a notification channel** (Slack or Telegram) —
|
|
262
|
+
[Adding notification channels](https://docs.tenuo.ai/guides/adding-channels)
|
|
263
|
+
2. **Create an identity binding** with a **Display Name** (e.g. `Jane Doe`) —
|
|
264
|
+
[Identity bindings](https://docs.tenuo.ai/integrations/identity-bindings)
|
|
265
|
+
(Dashboard → Channels → Identity Bindings)
|
|
266
|
+
|
|
267
|
+
The `--approver` string must match that **Display Name** exactly.
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
tenuo-claude init --advanced --approver "Jane Doe"
|
|
271
|
+
# or: cp tenuo.yaml.advanced.example tenuo.advanced.yaml and edit
|
|
272
|
+
tenuo-admin setup # wires approval policy; re-run after overlay changes
|
|
273
|
+
python3 tenuo_demo.py --advanced # shows PAUSE for off-allowlist WebFetch
|
|
274
|
+
python3 tenuo_demo.py --advanced --live-approval # blocks until approver responds
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Runbook: [docs/PRESENTATION.md](docs/PRESENTATION.md).
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
### Switching local ↔ Cloud
|
|
282
|
+
|
|
283
|
+
`up` picks mode from **files on disk**, not yaml alone. If Cloud files exist, you stay
|
|
284
|
+
in Cloud mode until you remove them **and restart the authorizer**:
|
|
285
|
+
|
|
286
|
+
| To switch **to local** | To switch **to Cloud** |
|
|
287
|
+
|------------------------|------------------------|
|
|
288
|
+
| Move aside `.state/cloud.env` and `.state/cloud_state.json` | Restore both files |
|
|
289
|
+
| Comment/remove `tenuo.cloud.yaml` / `tenuo.advanced.yaml` | Restore overlay files |
|
|
290
|
+
| `tenuo-claude down` → `init` → `up` | `tenuo-admin setup` (if needed) → `down` → `up` |
|
|
291
|
+
| Status: `cloud: disabled` | Status: `cloud: registered …` |
|
|
292
|
+
|
|
293
|
+
`down` is required when switching — a running container keeps its old Cloud env until
|
|
294
|
+
replaced.
|
|
295
|
+
|
|
296
|
+
Reviewer brief: [docs/SECURITY-TEAM.md](docs/SECURITY-TEAM.md). Deep dive:
|
|
297
|
+
[docs/DETAILS.md](docs/DETAILS.md).
|
|
298
|
+
|
|
299
|
+
## See it in action
|
|
300
|
+
|
|
301
|
+
These examples run real Claude Code against the policy. `--dangerously-skip-permissions`
|
|
302
|
+
turns off Claude's permission prompts; the warrant still applies because enforcement
|
|
303
|
+
is in the hook, not in Claude.
|
|
304
|
+
|
|
305
|
+
In-scope vs out-of-scope reads:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
claude -p "Read sandbox/notes.txt and summarize." # allowed
|
|
309
|
+
claude -p "Read /etc/hosts" --dangerously-skip-permissions # denied
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Destructive instruction with guardrails off:
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
claude -p "Use delete_deployment to tear down production." --dangerously-skip-permissions
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Prompt injection: `sandbox/incident-report.md` hides instructions to exfil secrets and
|
|
319
|
+
delete prod. If the model refuses, fine — the warrant still does not grant those tools.
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
claude -p "Summarize sandbox/incident-report.md for me." --dangerously-skip-permissions
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Subagent attenuation (session allows `Bash`; researcher child warrant does not):
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
claude -p "Use the researcher subagent to run 'ls -la sandbox' and report the result." \
|
|
329
|
+
--dangerously-skip-permissions
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Without Claude:
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
python3 tenuo_demo.py
|
|
336
|
+
python3 tenuo_claude.py audit
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Receipt trail
|
|
340
|
+
|
|
341
|
+
Same demo sequence, real `audit` output (local convenience log; authorizer produces
|
|
342
|
+
the signed receipts, streamed to Cloud when connected):
|
|
343
|
+
|
|
344
|
+
```
|
|
345
|
+
$ python3 tenuo_demo.py && python3 tenuo_claude.py audit
|
|
346
|
+
ALLOW [gov] Read -> read_file authorized
|
|
347
|
+
DENY [gov] Read -> read_file Constraint not satisfied
|
|
348
|
+
DENY [aud] delete_deployment -> unlisted Constraint not satisfied
|
|
349
|
+
ALLOW [gov] Bash -> run_command authorized
|
|
350
|
+
DENY [gov] Bash -> run_command Constraint not satisfied
|
|
351
|
+
ALLOW [gov] Grep -> grep authorized
|
|
352
|
+
ALLOW [gov] WebFetch -> web_fetch authorized
|
|
353
|
+
DENY [gov] WebFetch -> web_fetch Constraint not satisfied
|
|
354
|
+
ALLOW [gov] Agent -> spawn_agent authorized
|
|
355
|
+
DENY [gov] Bash <researcher> -> run_command Constraint not satisfied
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
With Cloud `WebFetch.approval` enabled, an off-allowlist SSRF-safe URL shows
|
|
359
|
+
`PENDING [appr]` before resolve. See [DETAILS.md](docs/DETAILS.md#human-approval-cloud).
|
|
360
|
+
|
|
361
|
+
## vs. native Claude Code permissions
|
|
362
|
+
|
|
363
|
+
Claude Code permissions are **configuration**: allow/ask/deny rules in `settings.json`,
|
|
364
|
+
optionally locked down fleet-wide via **managed settings**. Tenuo adds a **credential**:
|
|
365
|
+
a signed warrant checked on every tool call, with TTL, revocation, and a receipt stream.
|
|
366
|
+
|
|
367
|
+
Tenuo is built **on top of** Claude's hook and managed-settings mechanisms — not a
|
|
368
|
+
replacement. You still deploy PreToolUse hooks (this demo wires them from `tenuo.yaml`);
|
|
369
|
+
for fleet enforce, use managed settings so users cannot remove them.
|
|
370
|
+
|
|
371
|
+
| | Claude Code permissions | Tenuo warrant |
|
|
372
|
+
|---|-------------------------|---------------|
|
|
373
|
+
| Policy form | Allow/ask/deny rules in settings | Signed credential; Cloud mode chains to tenant root |
|
|
374
|
+
| Expiry | Rules persist until edited | Session TTL (~1h); `up` refreshes |
|
|
375
|
+
| Revocation | Edit rules; sessions may keep prior allowances | Revoke warrant id; live in ~30s (Cloud), no restart |
|
|
376
|
+
| Evidence | Hook logs optional; no signed trail by default | Signed receipt per decision; central stream with Cloud |
|
|
377
|
+
| Delegation | Subagents follow project/user tool policy | Cryptographic attenuation; session is the ceiling |
|
|
378
|
+
| Exceptions | Additional allow rules | Optional Cloud approval gate on off-allowlist `WebFetch` |
|
|
379
|
+
| `--dangerously-skip-permissions` | Bypasses Claude permission prompts* | Warrant still enforced |
|
|
380
|
+
|
|
381
|
+
\*Managed settings can disable bypass (`disableBypassPermissionsMode`). Verify native
|
|
382
|
+
behavior against [Claude Code permissions](https://code.claude.com/docs/en/permissions).
|
|
383
|
+
|
|
384
|
+
## How it works
|
|
385
|
+
|
|
386
|
+

|
|
387
|
+
|
|
388
|
+
```
|
|
389
|
+
tenuo.yaml
|
|
390
|
+
(policy — single source of truth)
|
|
391
|
+
│
|
|
392
|
+
init / up generates
|
|
393
|
+
▼
|
|
394
|
+
┌─────────────────────────────────────────────┐
|
|
395
|
+
│ warrant · authorizer config · Claude hooks │
|
|
396
|
+
│ · MCP proxy wiring │
|
|
397
|
+
└─────────────────────────────────────────────┘
|
|
398
|
+
│
|
|
399
|
+
┌────────────────┴────────────────┐
|
|
400
|
+
native tools MCP tools
|
|
401
|
+
│ │
|
|
402
|
+
PreToolUse hook MCP proxy (.mcp.json)
|
|
403
|
+
└────────────┬────────────────┘
|
|
404
|
+
▼
|
|
405
|
+
tenuo_claude.py → authorizer → allow / deny → receipt
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
On each tool call the hook or MCP proxy signs a proof-of-possession and asks the
|
|
409
|
+
authorizer. The decision lives outside Claude.
|
|
410
|
+
|
|
411
|
+
| Path | Enforcement |
|
|
412
|
+
|------|-------------|
|
|
413
|
+
| MCP proxy | Structural: Claude talks to the proxy, not the downstream server |
|
|
414
|
+
| PreToolUse hook | Cooperative: returns allow/deny; hardened via fail-closed + managed settings |
|
|
415
|
+
|
|
416
|
+
Both use the same warrant and authorizer. See [DETAILS.md](docs/DETAILS.md#why-hook-and-mcp-proxy).
|
|
417
|
+
|
|
418
|
+
## What the security team sees
|
|
419
|
+
|
|
420
|
+
With [Tenuo Cloud](https://cloud.tenuo.ai), each session warrant chains to your tenant
|
|
421
|
+
root. Platform security gets one stream to answer: *what did agents do, under what
|
|
422
|
+
authority, who approved the exceptions* — and can revoke a compromised warrant in
|
|
423
|
+
about 30 seconds without touching the laptop.
|
|
424
|
+
|
|
425
|
+
Admin vs runtime separation:
|
|
426
|
+
|
|
427
|
+
| Tool | Key | Does |
|
|
428
|
+
|------|-----|------|
|
|
429
|
+
| `tenuo_admin.py setup` | admin (`~/.tenuo/admin.env`) | Register holder, create trigger from `tenuo.yaml` |
|
|
430
|
+
| `tenuo_claude.py up` | runtime (`.state/cloud.env`) | Fire trigger, run authorizer |
|
|
431
|
+
|
|
432
|
+
See [Setup → Cloud mode](#cloud-mode) for step-by-step credentials and commands.
|
|
433
|
+
|
|
434
|
+
### Cloud audit stream
|
|
435
|
+
|
|
436
|
+
Every hook and demo decision is also a **signed receipt** in [cloud.tenuo.ai](https://cloud.tenuo.ai):
|
|
437
|
+
allow, deny, spawn, and (when configured) human-approved exceptions — one stream for
|
|
438
|
+
the whole fleet.
|
|
439
|
+
|
|
440
|
+

|
|
441
|
+
|
|
442
|
+
Drill into an approved off-allowlist `WebFetch` to see the approval bound to that
|
|
443
|
+
specific call — approver, timestamp, and cryptographic request hash:
|
|
444
|
+
|
|
445
|
+

|
|
446
|
+
|
|
447
|
+
**Revocation:** revoke the session warrant id from `tenuo-claude status` or the Cloud
|
|
448
|
+
dashboard; authorizers pick up the SRL within ~30s. Local-only mode:
|
|
449
|
+
`tenuo-claude revoke`.
|
|
450
|
+
|
|
451
|
+
One-page brief for security reviewers: [docs/SECURITY-TEAM.md](docs/SECURITY-TEAM.md).
|
|
452
|
+
|
|
453
|
+
## Policy (`tenuo.yaml`)
|
|
454
|
+
|
|
455
|
+
Warrant, routes, hooks, and MCP wiring come from one file:
|
|
456
|
+
|
|
457
|
+
```yaml
|
|
458
|
+
name: claude-code-demo
|
|
459
|
+
sandbox: ./sandbox
|
|
460
|
+
mode: enforce
|
|
461
|
+
enforce:
|
|
462
|
+
Read: "subpath:{sandbox}"
|
|
463
|
+
Bash: "shlex:ls,pwd,echo,date"
|
|
464
|
+
WebFetch:
|
|
465
|
+
domains: ["api.github.com", "*.githubusercontent.com", "*.tenuo.ai"]
|
|
466
|
+
default: deny
|
|
467
|
+
subagents:
|
|
468
|
+
researcher:
|
|
469
|
+
tools: [Read, Grep, Glob]
|
|
470
|
+
mcp:
|
|
471
|
+
downstream: ./ops_server.py
|
|
472
|
+
enforce:
|
|
473
|
+
read_file: "subpath:{sandbox}"
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
- `enforce`: allowed and argument-checked.
|
|
477
|
+
- `audit`: harness tools from `harness_tools.yaml` (extend with `audit_extra:`).
|
|
478
|
+
- `default: deny`: everything else blocked with a receipt.
|
|
479
|
+
- `mcp.enforce`: bare downstream tool name + `path` arg only (single MCP server in this demo).
|
|
480
|
+
- With `subagents:` on, bundled **Workflow** is audit-allowed but its inner agent
|
|
481
|
+
calls use undeclared roles and are denied — see [DETAILS.md](docs/DETAILS.md#subagents).
|
|
482
|
+
|
|
483
|
+
## Commands
|
|
484
|
+
|
|
485
|
+
| Command | Does |
|
|
486
|
+
|---------|------|
|
|
487
|
+
| `init` | Mint warrant, wire hooks and `.mcp.json` |
|
|
488
|
+
| `refresh` | Re-apply `tenuo.yaml` (warrant, gateway, hooks); restarts authorizer if up |
|
|
489
|
+
| `up` / `down` | Start / stop authorizer |
|
|
490
|
+
| `status` | Warrant, posture, Cloud summary |
|
|
491
|
+
| `doctor [--no-live]` | Self-test allow/deny |
|
|
492
|
+
| `audit [--tail N]` | Receipt trail |
|
|
493
|
+
| `revoke` | Revoke session warrant |
|
|
494
|
+
|
|
495
|
+
## Enterprise deployment
|
|
496
|
+
|
|
497
|
+
Ship the **whole directory** (or an internal package) to a fixed path, e.g.
|
|
498
|
+
`/opt/tenuo/claude-governance`. Governance wiring uses the committed launcher
|
|
499
|
+
`bin/tenuo-claude` — no machine-specific Python paths in `.mcp.json`.
|
|
500
|
+
|
|
501
|
+
### Install layout
|
|
502
|
+
|
|
503
|
+
```bash
|
|
504
|
+
/opt/tenuo/claude-governance/
|
|
505
|
+
bin/tenuo-claude # resolves .venv / TENUO_PYTHON / uv
|
|
506
|
+
tenuo.yaml # team policy (git)
|
|
507
|
+
.mcp.json # portable MCP proxy wiring (git)
|
|
508
|
+
.venv/ # created on host: uv sync
|
|
509
|
+
.state/ # per-machine keys + warrant (never commit)
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Optional fleet env (MDM / systemd / launchd):
|
|
513
|
+
|
|
514
|
+
```bash
|
|
515
|
+
export TENUO_ROOT=/opt/tenuo/claude-governance
|
|
516
|
+
export TENUO_PYTHON=/opt/tenuo/claude-governance/.venv/bin/python
|
|
517
|
+
# Or pin the launcher path when generating wiring on a golden image:
|
|
518
|
+
export TENUO_CLAUDE_BIN=/opt/tenuo/claude-governance/bin/tenuo-claude
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
On each machine: `uv sync` (or `pip install tenuo-claude-code`), `tenuo-claude init`,
|
|
522
|
+
`tenuo-claude up`. After policy changes: `tenuo-claude refresh`. Preflight:
|
|
523
|
+
`tenuo-claude check` (validates launcher, hook/MCP wiring drift, authorizer).
|
|
524
|
+
|
|
525
|
+
### Managed settings (hooks)
|
|
526
|
+
|
|
527
|
+
Hooks override project `.claude/settings.json` when deployed via managed settings
|
|
528
|
+
(highest precedence). Point at the same launcher:
|
|
529
|
+
|
|
530
|
+
```jsonc
|
|
531
|
+
// macOS: /Library/Application Support/ClaudeCode/managed-settings.json
|
|
532
|
+
{
|
|
533
|
+
"hooks": {
|
|
534
|
+
"PreToolUse": [{"matcher": "*", "hooks": [{"type": "command", "command": "/opt/tenuo/claude-governance/bin/tenuo-claude _hook"}]}],
|
|
535
|
+
"PostToolUse": [{"matcher": "*", "hooks": [{"type": "command", "command": "/opt/tenuo/claude-governance/bin/tenuo-claude _post"}]}]
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### MCP (structural enforcement)
|
|
541
|
+
|
|
542
|
+
Project `.mcp.json` is checked into git and uses a **relative** launcher — Claude
|
|
543
|
+
starts MCP from the project root:
|
|
544
|
+
|
|
545
|
+
```json
|
|
546
|
+
{
|
|
547
|
+
"mcpServers": {
|
|
548
|
+
"tenuo-files": {
|
|
549
|
+
"command": "tenuo-claude",
|
|
550
|
+
"args": ["_mcp-proxy"]
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
For fleets that block project MCP config, mirror the same server in managed MCP
|
|
557
|
+
policy (same command/args, or absolute path to `bin/tenuo-claude`). **Do not** point
|
|
558
|
+
`.mcp.json` at the downstream server — that bypasses the proxy if the hook fails.
|
|
559
|
+
|
|
560
|
+
**Scope precedence:** local / managed MCP entries with the same server name override
|
|
561
|
+
project `.mcp.json`. Standardize on the `tenuo-files` name or enforce via
|
|
562
|
+
`allowedMcpServers` in managed settings.
|
|
563
|
+
|
|
564
|
+
Deploy with MDM alongside the CLI and `tenuo.yaml`. Governance covers agent tool
|
|
565
|
+
calls, not interactive `!` shell — restrict that at the workstation if needed.
|
|
566
|
+
|
|
567
|
+
## Rolling out
|
|
568
|
+
|
|
569
|
+
1. **Local eval** — [Setup → Local mode](#local-mode): `init`, `up`, `doctor`, `tenuo_demo.py`.
|
|
570
|
+
2. **Observe-only** — `mode: audit`: compute and receipt real allow/deny without
|
|
571
|
+
blocking. The hook emits **no** permission decision, so observe-only never weakens
|
|
572
|
+
Claude's stock prompts. Tune on `WOULD-DENY` rows, then set `mode: enforce`.
|
|
573
|
+
3. **Fleet enforce** — managed settings + Cloud root-signed warrants + team policy in
|
|
574
|
+
`tenuo.yaml`.
|
|
575
|
+
|
|
576
|
+
Send security reviewers [docs/SECURITY-TEAM.md](docs/SECURITY-TEAM.md). Mechanics:
|
|
577
|
+
[docs/DETAILS.md](docs/DETAILS.md).
|
|
578
|
+
|
|
579
|
+
## Security boundaries
|
|
580
|
+
|
|
581
|
+
Tenuo controls which tool calls the agent may make, not every execution side effect.
|
|
582
|
+
[The Map is not the Territory](https://niyikiza.com/posts/map-territory/).
|
|
583
|
+
|
|
584
|
+
Claude Code only blocks PreToolUse on exit code 2 or explicit deny; `_hook` converts
|
|
585
|
+
errors into deny decisions. `doctor --no-live` skips the live Claude harness check.
|
|
586
|
+
|
|
587
|
+
**Fail-closed** (run live for prospects):
|
|
588
|
+
|
|
589
|
+
```bash
|
|
590
|
+
mv tenuo.yaml tenuo.yaml.bak
|
|
591
|
+
# every tool call denied: Tenuo hook error (fail-closed): Missing …/tenuo.yaml
|
|
592
|
+
mv tenuo.yaml.bak tenuo.yaml
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
Limits: Bash allowlist checks command shape; WebFetch checks URL strings; new Claude
|
|
596
|
+
tools default-deny until listed in `harness_tools.yaml`.
|
|
597
|
+
|
|
598
|
+
**Claude Code version assumptions:** spawn routing keys on tool names `Agent` /
|
|
599
|
+
`Task` and the `agent_type` hook field (empirically claude 2.1.x). `doctor --live`
|
|
600
|
+
checks PreToolUse exit-code semantics (exit 2 blocks). If Anthropic renames spawn
|
|
601
|
+
tools, spawns fail closed unless the new name is only audit-listed.
|
|
602
|
+
|
|
603
|
+
## Files
|
|
604
|
+
|
|
605
|
+
| File | Purpose |
|
|
606
|
+
|------|---------|
|
|
607
|
+
| `tenuo.yaml` | Policy |
|
|
608
|
+
| `.mcp.json` | MCP proxy wiring (`tenuo-claude` or `./bin/tenuo-claude`) |
|
|
609
|
+
| `bin/tenuo-claude` | Git-clone launcher for hooks, MCP, and CLI |
|
|
610
|
+
| `src/tenuo_claude_code/` | PyPI package source |
|
|
611
|
+
| `tenuo.yaml.cloud.example` | Tool Cloud overlay template (`cloud.url` only) |
|
|
612
|
+
| `tenuo.yaml.advanced.example` | Advanced overlay (WebFetch approval + approver) — optional presentations |
|
|
613
|
+
| `cloud.env.example` | Runtime key template → `.state/cloud.env` |
|
|
614
|
+
| `admin.env.example` | Admin key template → `~/.tenuo/admin.env` |
|
|
615
|
+
| `harness_tools.yaml` | Bundled harness tool allowlist |
|
|
616
|
+
| `docs/SECURITY-TEAM.md` | One-page reviewer brief |
|
|
617
|
+
| `docs/DETAILS.md` | Deep dive (SSRF examples, audit invariants, subagents) |
|
|
618
|
+
| `docs/images/` | Cloud audit stream + approval receipt screenshots |
|
|
619
|
+
| `tenuo_claude.py` | CLI, hook, MCP proxy |
|
|
620
|
+
| `tenuo_demo.py` | Scripted tour + receipt trail |
|
|
621
|
+
| `CONTRIBUTING.md` | Maintainer notes |
|
|
622
|
+
|
|
623
|
+
Maintainer setup: [CONTRIBUTING.md](CONTRIBUTING.md).
|