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.
@@ -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
+ ![Tenuo + Claude Code — every tool call is checked against policy before it runs](tenuo_claude_code_architecture.svg)
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
+ ![Authorization receipts in Tenuo Cloud](docs/images/cloud-audit-stream.png)
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
+ ![Receipt detail with human approval](docs/images/cloud-receipt-approval-detail.png)
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).