tenuo-claude-code 0.6.4__tar.gz → 0.7.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.7.0/.claude-plugin/marketplace.json +26 -0
- tenuo_claude_code-0.7.0/.gitattributes +2 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/CONTRIBUTING.md +12 -3
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/PKG-INFO +143 -61
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/README.md +134 -54
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/demo/README.md +1 -1
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/docs/DETAILS.md +5 -5
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/docs/ENTERPRISE.md +1 -1
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/docs/TROUBLESHOOTING.md +7 -7
- tenuo_claude_code-0.7.0/docs/releases/0.7.0.md +37 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/examples/managed/README.md +10 -6
- tenuo_claude_code-0.7.0/plugins/tenuo/.claude-plugin/plugin.json +15 -0
- tenuo_claude_code-0.7.0/plugins/tenuo/hooks/hooks.json +41 -0
- tenuo_claude_code-0.7.0/plugins/tenuo/scripts/tenuo-hook +88 -0
- tenuo_claude_code-0.7.0/plugins/tenuo/scripts/tenuo_plugin.py +447 -0
- tenuo_claude_code-0.7.0/plugins/tenuo/skills/audit/SKILL.md +16 -0
- tenuo_claude_code-0.7.0/plugins/tenuo/skills/status/SKILL.md +21 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/pyproject.toml +14 -7
- tenuo_claude_code-0.7.0/requirements.txt +3 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/__init__.py +1 -1
- tenuo_claude_code-0.7.0/src/tenuo_claude_code/action_identity.py +61 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/admin.py +6 -244
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/authorizer_runtime.py +33 -3
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/cli.py +594 -718
- tenuo_claude_code-0.7.0/src/tenuo_claude_code/cloud_policy.py +253 -0
- tenuo_claude_code-0.7.0/src/tenuo_claude_code/constraints.py +141 -0
- tenuo_claude_code-0.7.0/src/tenuo_claude_code/managed_runtime.py +63 -0
- tenuo_claude_code-0.7.0/src/tenuo_claude_code/mcp_proxy.py +111 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/packs/filesystem-dev/pack.yaml +11 -5
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/packs/filesystem-dev/tenuo.yaml.tmpl +6 -2
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/packs/github-mcp/pack.yaml +14 -10
- tenuo_claude_code-0.7.0/src/tenuo_claude_code/packs/github-mcp/tenuo.yaml.tmpl +117 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/paths.py +43 -1
- tenuo_claude_code-0.7.0/src/tenuo_claude_code/policy.py +375 -0
- tenuo_claude_code-0.7.0/src/tenuo_claude_code/routing.py +107 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/conftest.py +9 -0
- tenuo_claude_code-0.7.0/tests/test_action_identity.py +127 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_admin.py +15 -9
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_authorizer_runtime.py +50 -2
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_check.py +1 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_dx_improvements.py +21 -0
- tenuo_claude_code-0.7.0/tests/test_grep_exclusions.py +123 -0
- tenuo_claude_code-0.7.0/tests/test_managed_runtime.py +76 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_managed_template.py +3 -2
- tenuo_claude_code-0.7.0/tests/test_mcp_proxy.py +163 -0
- tenuo_claude_code-0.7.0/tests/test_packs.py +275 -0
- tenuo_claude_code-0.7.0/tests/test_plugin_launcher.py +520 -0
- tenuo_claude_code-0.7.0/tests/test_policy_validation.py +62 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_posture.py +19 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_routing.py +45 -20
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_scaffold.py +35 -1
- tenuo_claude_code-0.7.0/tests/test_state_dir.py +178 -0
- tenuo_claude_code-0.7.0/tests/test_tenuo_files.py +66 -0
- tenuo_claude_code-0.6.4/src/tenuo_claude_code/packs/github-mcp/tenuo.yaml.tmpl +0 -86
- tenuo_claude_code-0.6.4/tests/test_mcp_proxy.py +0 -53
- tenuo_claude_code-0.6.4/tests/test_packs.py +0 -160
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/.gitignore +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/LICENSE +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/demo/.claude/agents/researcher.md +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/demo/.mcp.json +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/demo/docs/README.md +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/demo/fake-secrets.env +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/demo/ops_server.py +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/demo/sandbox/checkout-evidence.log +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/demo/sandbox/incident-report.md +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/demo/sandbox/notes.txt +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/demo/tenuo.yaml +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/demo/tenuo_demo.py +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/docs/images/cloud-audit-stream.png +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/docs/images/cloud-receipt-approval-detail.png +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/docs/images/tenuo_claude_code_architecture.png +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/docs/images/tenuo_claude_code_architecture.svg +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/examples/policies/README.md +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/examples/policies/dry-run-rollout.yaml +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/examples/policies/enforce-with-mcp.yaml +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/examples/policies/read-only-research.yaml +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/data/harness_tools.yaml +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/packs/http-api-safe/pack.yaml +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/packs/http-api-safe/tenuo.yaml.tmpl +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/packs.py +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/templates/tenuo.yaml.example +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/src/tenuo_claude_code/verify.py +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/templates/README.md +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/templates/admin.env.example +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/templates/cloud.env.example +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/templates/tenuo.yaml.advanced.example +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/templates/tenuo.yaml.cloud.example +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/templates/tenuo.yaml.example +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_approvals.py +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_authorize_call.py +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_cloud_bindings.py +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_constraints.py +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_ipc_transport.py +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_permissions.py +0 -0
- {tenuo_claude_code-0.6.4 → tenuo_claude_code-0.7.0}/tests/test_subagents.py +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tenuo",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Tenuo",
|
|
5
|
+
"email": "daniel@tenuo.ai",
|
|
6
|
+
"url": "https://tenuo.ai"
|
|
7
|
+
},
|
|
8
|
+
"description": "Tenuo plugins for Claude Code.",
|
|
9
|
+
"plugins": [
|
|
10
|
+
{
|
|
11
|
+
"name": "tenuo",
|
|
12
|
+
"displayName": "Tenuo",
|
|
13
|
+
"source": "./plugins/tenuo",
|
|
14
|
+
"description": "Task-scoped authorization for Claude Code: warrants constrain tools and arguments before each call, with signed receipts. Inactive until a project opts in with tenuo.yaml.",
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "Tenuo",
|
|
17
|
+
"url": "https://tenuo.ai"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://tenuo.ai",
|
|
20
|
+
"repository": "https://github.com/tenuo-ai/tenuo-claude-code",
|
|
21
|
+
"license": "Apache-2.0",
|
|
22
|
+
"category": "security",
|
|
23
|
+
"tags": ["security", "authorization", "governance", "audit"]
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -20,7 +20,7 @@ Re-run `init` or `refresh` after switching Python venvs (hooks pin `sys.executab
|
|
|
20
20
|
2. Keep changes focused; match existing style in `src/tenuo_claude_code/`.
|
|
21
21
|
3. Run `uv run --with pytest pytest` (fast unit tests, no Docker). If you touch enforcement
|
|
22
22
|
or policy wiring, also run `cd demo && tenuo-claude verify` (live authorizer).
|
|
23
|
-
4. Do not commit
|
|
23
|
+
4. Do not commit the state directory, real API keys, or home-directory paths.
|
|
24
24
|
|
|
25
25
|
Security issues: see [SECURITY.md](SECURITY.md). No public issues for vulnerabilities.
|
|
26
26
|
|
|
@@ -28,7 +28,7 @@ Security issues: see [SECURITY.md](SECURITY.md). No public issues for vulnerabil
|
|
|
28
28
|
|
|
29
29
|
| Path | Contains |
|
|
30
30
|
|------|----------|
|
|
31
|
-
|
|
|
31
|
+
| state directory (`tenuo-claude state-dir`) | Holder/issuer keys, warrant, receipts, `cloud.env` |
|
|
32
32
|
| `.claude/settings.json` | Generated hook wiring |
|
|
33
33
|
| `~/.tenuo/admin.env` | Admin key for `tenuo-admin setup` only |
|
|
34
34
|
|
|
@@ -42,10 +42,16 @@ under `/Users/` or `C:\`.
|
|
|
42
42
|
|
|
43
43
|
| Module | Role |
|
|
44
44
|
|--------|------|
|
|
45
|
-
| `src/tenuo_claude_code/cli.py` |
|
|
45
|
+
| `src/tenuo_claude_code/cli.py` | Project state, hooks, and lifecycle orchestration |
|
|
46
|
+
| `src/tenuo_claude_code/policy.py`, `constraints.py`, `routing.py`, `action_identity.py` | Policy parsing, constraints, routing, and complete request identity without CLI state |
|
|
47
|
+
| `src/tenuo_claude_code/cloud_policy.py` | Cloud warrant configuration compiler |
|
|
48
|
+
| `src/tenuo_claude_code/mcp_proxy.py` | MCP transport adapter with explicit runtime callbacks |
|
|
49
|
+
| `src/tenuo_claude_code/managed_runtime.py` | Administrator-owned Python runtime validation |
|
|
46
50
|
| `src/tenuo_claude_code/admin.py` | One-time Cloud setup (`tenuo-admin`) |
|
|
47
51
|
| `src/tenuo_claude_code/verify.py` | Policy-driven authorizer self-test |
|
|
48
52
|
| `src/tenuo_claude_code/data/harness_tools.yaml` | Bundled audit-allow tool list |
|
|
53
|
+
| `plugins/tenuo/` | Claude Code plugin (hooks, launcher, `/tenuo:*` skills); decision table tested in `tests/test_plugin_launcher.py` |
|
|
54
|
+
| `.claude-plugin/marketplace.json` | Marketplace entry so `/plugin marketplace add` works on this repo |
|
|
49
55
|
|
|
50
56
|
Root `scripts/tenuo_claude.py` / `scripts/tenuo_admin.py` are thin shims; prefer `tenuo-claude` on PATH.
|
|
51
57
|
|
|
@@ -61,6 +67,9 @@ or `PYPI_TOKEN` secret).
|
|
|
61
67
|
Release steps:
|
|
62
68
|
|
|
63
69
|
1. Bump `version` in `pyproject.toml`, `src/tenuo_claude_code/__init__.py`, and `uv.lock`.
|
|
70
|
+
If anything under `plugins/tenuo/` changed, also bump `version` in
|
|
71
|
+
`plugins/tenuo/.claude-plugin/plugin.json` (installed plugins only update when it changes)
|
|
72
|
+
and run `claude plugin validate . --strict` and `claude plugin validate plugins/tenuo --strict`.
|
|
64
73
|
2. Commit (`Release X.Y.Z`) and push the `vX.Y.Z` tag (this triggers the PyPI publish).
|
|
65
74
|
3. Always publish brief release notes:
|
|
66
75
|
`gh release create vX.Y.Z --verify-tag --title vX.Y.Z --notes "<one short paragraph or a few bullets>"`.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tenuo-claude-code
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Tenuo governance for Claude Code — warrants, hooks, MCP proxy, and Cloud lifecycle
|
|
5
5
|
Project-URL: Homepage, https://tenuo.ai
|
|
6
|
-
Project-URL: Repository, https://github.com/tenuo-ai/claude-
|
|
7
|
-
Project-URL: Documentation, https://github.com/tenuo-ai/claude-
|
|
8
|
-
Project-URL: Issues, https://github.com/tenuo-ai/claude-
|
|
6
|
+
Project-URL: Repository, https://github.com/tenuo-ai/tenuo-claude-code
|
|
7
|
+
Project-URL: Documentation, https://github.com/tenuo-ai/tenuo-claude-code#readme
|
|
8
|
+
Project-URL: Issues, https://github.com/tenuo-ai/tenuo-claude-code/issues
|
|
9
9
|
Author: Tenuo Contributors
|
|
10
10
|
License-Expression: Apache-2.0
|
|
11
11
|
License-File: LICENSE
|
|
@@ -23,67 +23,59 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
23
23
|
Classifier: Topic :: Security
|
|
24
24
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
25
|
Requires-Python: >=3.10
|
|
26
|
-
Requires-Dist:
|
|
26
|
+
Requires-Dist: anyio>=4.14.2
|
|
27
|
+
Requires-Dist: certifi>=2026.10.18
|
|
27
28
|
Requires-Dist: cryptography>=42.0
|
|
28
|
-
Requires-Dist: mcp<2,>=1.
|
|
29
|
+
Requires-Dist: mcp<2,>=1.30
|
|
30
|
+
Requires-Dist: pyjwt>=2.15.0
|
|
29
31
|
Requires-Dist: pyyaml>=6.0
|
|
30
|
-
Requires-Dist: tenuo<0.4,>=0.3.
|
|
32
|
+
Requires-Dist: tenuo<0.4,>=0.3.6
|
|
31
33
|
Description-Content-Type: text/markdown
|
|
32
34
|
|
|
33
35
|
# Tenuo for Claude Code
|
|
34
36
|
|
|
35
37
|
[](https://pypi.org/project/tenuo-claude-code/)
|
|
36
38
|
[](https://pypi.org/project/tenuo-claude-code/)
|
|
37
|
-
[](https://github.com/tenuo-ai/tenuo-claude-code/actions/workflows/ci.yml)
|
|
39
|
+
[](https://github.com/tenuo-ai/tenuo-claude-code/actions/workflows/ci.yml?query=branch%3Amain)
|
|
38
40
|
[](LICENSE)
|
|
39
41
|
|
|
40
|
-
**
|
|
42
|
+
**Claude Code's permissions decide which tools Claude can call. Tenuo decides what it can call them with.**
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
Tenuo checks the arguments of every tool call against your policy before the call runs: which repo, which directory, which commands, which domains. That includes MCP tools, with no change to the MCP server. Each decision is written as a signed receipt.
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
Here Claude Code's own permissions allowed the GitHub MCP server's `get_file_contents`. The Tenuo policy allows one repository:
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
| **Revocation** | Edit rules (active sessions may keep access) | Instant fleet-wide revocation (Cloud) |
|
|
57
|
-
| **Enforcement** | Claude Code settings UI | PreToolUse hook + MCP proxy + authorizer |
|
|
58
|
-
| **Span** | Single session | Persists across MCP calls and subagents |
|
|
59
|
-
|
|
60
|
-
Tenuo doesn't replace Claude Code permissions—it adds a cryptographic gate that works regardless of why the model tried the call: prompt injection, hallucination, poisoned tool output, or direct user request.
|
|
61
|
-
|
|
62
|
-
## What It Looks Like
|
|
63
|
-
|
|
64
|
-
After setup, this is what you see:
|
|
48
|
+
```yaml
|
|
49
|
+
mcp:
|
|
50
|
+
server: github # must match the client MCP server name
|
|
51
|
+
enforce:
|
|
52
|
+
get_file_contents:
|
|
53
|
+
args:
|
|
54
|
+
owner: "exact:tenuo-ai"
|
|
55
|
+
repo: "pattern:tenuo"
|
|
56
|
+
default: deny
|
|
57
|
+
```
|
|
65
58
|
|
|
66
59
|
```
|
|
67
|
-
tenuo-claude audit
|
|
60
|
+
$ tenuo-claude audit
|
|
61
|
+
ALLOW [gov] mcp__github__get_file_contents -> mcp__github__get_file_contents authorized owner=tenuo-ai path=README.md repo=tenuo
|
|
62
|
+
DENY [gov] mcp__github__get_file_contents -> mcp__github__get_file_contents Constraint not satisfied owner=tenuo-ai path=README.md repo=tenuo-cloud
|
|
63
|
+
DENY [gov] mcp__github__get_file_contents -> mcp__github__get_file_contents Constraint not satisfied owner=torvalds path=README repo=linux
|
|
64
|
+
```
|
|
68
65
|
|
|
69
|
-
|
|
70
|
-
path outside workspace (subpath:/home/user/project)
|
|
66
|
+
The same check covers Claude's own tools: reads and edits stay inside the project, the shell runs only the commands you list, and WebFetch reaches only the domains you name.
|
|
71
67
|
|
|
72
|
-
|
|
73
|
-
DENY Bash rm -rf / command not permitted
|
|
74
|
-
'rm' not in allowlist [ls,pwd,cat,grep]
|
|
68
|
+
## Why Tenuo?
|
|
75
69
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
70
|
+
| | Claude Code permissions | Tenuo |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| **Matches** | Tool name; command and path patterns for built-in tools | Any argument of any tool, including MCP tools |
|
|
73
|
+
| **Decides** | Allow, deny or ask the user | Allow or deny before the call, against a signed, expiring warrant |
|
|
74
|
+
| **Evidence** | Optional hook logs | Signed, hash-chained receipts (`tenuo-claude audit --verify`) |
|
|
75
|
+
| **Subagents** | Same rules as the session | Each declared subagent gets a narrower warrant |
|
|
76
|
+
| **Teams** | Managed settings | Managed settings plus an org-signed root, revocation and approvals (Cloud) |
|
|
81
77
|
|
|
82
|
-
|
|
83
|
-
- **Signed** with Ed25519 (cryptographic proof)
|
|
84
|
-
- **Hash-chained** (tampering is detected)
|
|
85
|
-
- **Constraint-replayed** (warrant terms verified at audit time)
|
|
86
|
-
- **Cloud-synced** (if configured, sent to tenant for fleet audit)
|
|
78
|
+
Tenuo runs alongside Claude Code permissions. It checks the call whatever led the model to make it: prompt injection, a poisoned tool result, or a plain mistake.
|
|
87
79
|
|
|
88
80
|
## Quickstart
|
|
89
81
|
|
|
@@ -95,19 +87,20 @@ tenuo-claude bootstrap --native --pack filesystem-dev
|
|
|
95
87
|
claude
|
|
96
88
|
```
|
|
97
89
|
|
|
98
|
-
That's it. `bootstrap` with `--pack filesystem-dev` generates a
|
|
90
|
+
That's it. `bootstrap` with `--pack filesystem-dev` generates a starter policy (read and edit your project, keep Tenuo's keys and hook wiring out of reach, a minimal shell), starts the authorizer, and runs a self-test. The first run also downloads the pinned authorizer binary to `~/.tenuo/bin`. The pack starts in `mode: dry-run`, so nothing is blocked yet: open Claude Code, work normally, ask for something out of scope (say, reading a file outside the project), then:
|
|
99
91
|
|
|
100
92
|
```bash
|
|
101
93
|
tenuo-claude audit --verify
|
|
102
94
|
```
|
|
103
95
|
|
|
104
|
-
to see the signed decision log.
|
|
96
|
+
to see the signed decision log, including what would have been denied.
|
|
105
97
|
|
|
106
98
|
### What the quickstart does
|
|
107
99
|
|
|
108
100
|
1. **`--pack filesystem-dev`** generates a preconfigured policy:
|
|
109
|
-
- Read
|
|
110
|
-
-
|
|
101
|
+
- Read, edit and search files in your project directory (paths resolved, symlinks followed)
|
|
102
|
+
- Never read or change Tenuo's own files: its keys and state, `tenuo.yaml`, `.mcp.json`, `.claude/settings*.json`
|
|
103
|
+
- Run only `ls` and `pwd` in the shell; add your test and build commands to `Bash:`. The shell check covers the command name, not its paths, so list only commands you'd allow on any path
|
|
111
104
|
- Deny everything else by default
|
|
112
105
|
- Start in `mode: dry-run` (log, don't block)
|
|
113
106
|
|
|
@@ -124,12 +117,29 @@ tenuo-claude refresh
|
|
|
124
117
|
```
|
|
125
118
|
|
|
126
119
|
**Available policy packs:**
|
|
127
|
-
- `filesystem-dev` — Read/write project files,
|
|
120
|
+
- `filesystem-dev` — Read/write project files, minimal shell (default)
|
|
128
121
|
- `github-mcp` — GitHub MCP server, constrained API queries
|
|
129
122
|
- `http-api-safe` — WebFetch with SSRF protection and domain allowlists
|
|
130
123
|
|
|
131
124
|
Browse more: [examples/policies/](examples/policies/).
|
|
132
125
|
|
|
126
|
+
### Limit an MCP server
|
|
127
|
+
|
|
128
|
+
Add the server to Claude Code as usual, then scope it. For the GitHub MCP server:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
tenuo-claude init --pack github-mcp --param owner=myorg --param repos='service-*' --param approver=any-human
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The pack starts in `mode: dry-run`, which logs decisions but does not block calls. Review the generated `tenuo.yaml`, change it to `mode: enforce`, then apply it and start the authorizer:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
tenuo-claude refresh
|
|
138
|
+
tenuo-claude up
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
In enforce mode, reads and ordinary writes are limited to `myorg/service-*`; merges, pushes and file writes need a human approval (Tenuo Cloud; locally they are denied); everything else is denied. For another server, list its tools under `mcp.enforce` with a constraint per argument; see [Policy](#policy).
|
|
142
|
+
|
|
133
143
|
**For Cloud mode** (org root, revocation, human approval):
|
|
134
144
|
|
|
135
145
|
```bash
|
|
@@ -138,6 +148,37 @@ tenuo-claude bootstrap --native --pack filesystem-dev --cloud
|
|
|
138
148
|
|
|
139
149
|
Then follow the Cloud setup prompts. [Details: Cloud mode](#cloud-mode).
|
|
140
150
|
|
|
151
|
+
## Install as a Claude Code plugin
|
|
152
|
+
|
|
153
|
+
The repo is also a Claude Code plugin marketplace. Inside Claude Code:
|
|
154
|
+
|
|
155
|
+
```text
|
|
156
|
+
/plugin marketplace add tenuo-ai/tenuo-claude-code
|
|
157
|
+
/plugin install tenuo@tenuo
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The plugin still needs the CLI (`uv tool install tenuo-claude-code`) and a policy (`tenuo-claude bootstrap --native --pack filesystem-dev`). What it adds:
|
|
161
|
+
|
|
162
|
+
- **Hooks that stay out of the way.** PreToolUse, PostToolUse, and SessionStart hooks run in every project, but they do nothing unless the project has opted in. Repos without Tenuo keep Claude Code's normal permissions.
|
|
163
|
+
- **A fail-closed safety net.** If a project is governed but its `.claude/settings.json` wiring is missing (a fresh clone, a regenerated settings file), the plugin routes each call to `tenuo-claude _hook` itself. If the CLI can't be found or crashes, the call is denied with a message saying how to fix it.
|
|
164
|
+
- **A one-line status at session start**, plus `/tenuo:status` (`tenuo-claude check`) and `/tenuo:audit` (`tenuo-claude audit --verify`).
|
|
165
|
+
|
|
166
|
+
How the plugin decides, per tool call:
|
|
167
|
+
|
|
168
|
+
| Project state | PreToolUse result |
|
|
169
|
+
|---|---|
|
|
170
|
+
| No `tenuo.yaml` in the session directory or any parent | Nothing (Claude Code permissions apply) |
|
|
171
|
+
| `tenuo.yaml` but no state directory (never initialized) | Nothing; session start says to run `bootstrap` |
|
|
172
|
+
| `<state>/disabled` present (`tenuo-claude disable`) | Nothing; `tenuo-claude up` or `init` re-arms it |
|
|
173
|
+
| Tenuo hooks already wired by `init`/`bootstrap` (project, user, or managed settings) | Nothing; the wired hook enforces, so no double receipts |
|
|
174
|
+
| Initialized, not wired, `tenuo-claude` found | Delegates to `tenuo-claude _hook` |
|
|
175
|
+
| Initialized, not wired, `tenuo-claude` missing, crashing, or timing out | Deny (fail-closed) |
|
|
176
|
+
| `TENUO_PROJECT_DIR` set to a directory without `tenuo.yaml` | Deny (fail-closed) |
|
|
177
|
+
|
|
178
|
+
The CLI is found via `TENUO_CLAUDE_BIN`, the project `.venv`, `PATH`, then the uv tool and pipx install locations. The launcher is stdlib Python ([`plugins/tenuo/scripts/tenuo_plugin.py`](plugins/tenuo/scripts/tenuo_plugin.py)); it makes no network calls itself. macOS and Linux only, like the CLI.
|
|
179
|
+
|
|
180
|
+
To try a local checkout: `claude --plugin-dir ./plugins/tenuo`.
|
|
181
|
+
|
|
141
182
|
## How It Works
|
|
142
183
|
|
|
143
184
|
You define what Claude can do in `tenuo.yaml`:
|
|
@@ -219,7 +260,8 @@ It does **not** govern:
|
|
|
219
260
|
## Fail-Closed Design
|
|
220
261
|
|
|
221
262
|
- Missing or broken `tenuo.yaml` denies every governed call until restored
|
|
222
|
-
- Private keys
|
|
263
|
+
- Private keys are owner-only (`0600`) and kept outside the project
|
|
264
|
+
- In enforce mode a recursive `Grep` runs with the policy and hook wiring files (`tenuo.yaml`, `.mcp.json`, `.claude/settings*.json`) excluded, and is denied if they can't be excluded. Keep secrets out of those files anyway (`${VAR}` references), since `dry-run` blocks nothing
|
|
223
265
|
- A broken receipt sink surfaces loudly (not silent data loss)
|
|
224
266
|
- Cloud-disconnected nodes fall back to local policy (no open bypass)
|
|
225
267
|
|
|
@@ -252,14 +294,51 @@ subagents: # optional: each role runs under a narrower warrant
|
|
|
252
294
|
tools: [Read, Grep, Glob]
|
|
253
295
|
|
|
254
296
|
mcp: # optional: govern a downstream MCP server's tools
|
|
255
|
-
|
|
297
|
+
server: tenuo-files # client MCP server name (default: tenuo-files)
|
|
298
|
+
downstream: ./your_mcp_server.py # or {command: npx, args: [...], env: {...}}
|
|
256
299
|
enforce:
|
|
257
300
|
read_file: "subpath:{sandbox}"
|
|
258
301
|
run_query:
|
|
259
|
-
arg:
|
|
260
|
-
constraint: "
|
|
302
|
+
arg: database
|
|
303
|
+
constraint: "oneof:analytics,staging"
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
MCP capabilities include the server name: `mcp__<server>__<tool>`. A different server with the same tool name does not inherit these grants. For upgrades, see [0.7.0 migration notes](docs/releases/0.7.0.md).
|
|
307
|
+
|
|
308
|
+
### Any MCP client
|
|
309
|
+
|
|
310
|
+
`tenuo-claude mcp-proxy` serves `mcp.downstream` over stdio and checks each call against the policy before forwarding it, so the same rules apply in Cursor, Claude Desktop or any other MCP client. The downstream can be any stdio server. Env values can reference the proxy's environment, which keeps tokens out of `tenuo.yaml`:
|
|
311
|
+
|
|
312
|
+
```yaml
|
|
313
|
+
mcp:
|
|
314
|
+
server: github
|
|
315
|
+
downstream:
|
|
316
|
+
command: docker
|
|
317
|
+
args: [run, -i, --rm, -e, GITHUB_PERSONAL_ACCESS_TOKEN, ghcr.io/github/github-mcp-server]
|
|
318
|
+
env:
|
|
319
|
+
GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_TOKEN}"
|
|
320
|
+
enforce:
|
|
321
|
+
get_file_contents:
|
|
322
|
+
args: {owner: "exact:myorg", repo: "pattern:service-*"}
|
|
323
|
+
default: deny
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Then point the client at the proxy instead of the server, with `tenuo-claude up` running in the project:
|
|
327
|
+
|
|
328
|
+
```json
|
|
329
|
+
{
|
|
330
|
+
"mcpServers": {
|
|
331
|
+
"github": {
|
|
332
|
+
"command": "tenuo-claude",
|
|
333
|
+
"args": ["mcp-proxy"],
|
|
334
|
+
"env": {"TENUO_PROJECT_DIR": "/path/to/project", "GITHUB_TOKEN": "..."}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
261
338
|
```
|
|
262
339
|
|
|
340
|
+
A denied call returns an MCP error result (`isError: true`) naming the refused constraint, and is recorded in `tenuo-claude audit`.
|
|
341
|
+
|
|
263
342
|
### Constraints
|
|
264
343
|
|
|
265
344
|
| Constraint | Applies to | Checks |
|
|
@@ -312,14 +391,17 @@ Day to day: `up` (start), `audit` (review), `refresh` (after editing policy).
|
|
|
312
391
|
| `up` / `down` | Start / stop the authorizer (auto-selects Docker or native; `--native` forces native). |
|
|
313
392
|
| `refresh` | Recompile after editing `tenuo.yaml` (restarts authorizer if running). |
|
|
314
393
|
| `verify [--deep]` | Self-test the policy against the authorizer (no Claude session needed). `--deep` adds SSRF/encoded-IP matrix and Bash deny cases. |
|
|
315
|
-
| `audit [--tail N] [--verify]` | Show the decision log (
|
|
394
|
+
| `audit [--tail N] [--verify]` | Show the decision log (`<state>/receipts.jsonl`). `--verify` checks receipt signatures, hash-chain links, and embedded evidence. |
|
|
316
395
|
| `check` | Preflight: dependencies, wiring, audit-sink health, leaked keys, Cloud bindings. |
|
|
317
396
|
| `status` | Warrant, mode, audit-sink health, Cloud summary. |
|
|
318
397
|
| `install-authorizer` | Install the pinned native authorizer to `~/.tenuo/bin`. |
|
|
398
|
+
| `state-dir` | Print this project's state directory. |
|
|
319
399
|
| `bench [--json]` | Measure per-call overhead on your machine. |
|
|
320
400
|
| `revoke` | Revoke the current session warrant. |
|
|
321
401
|
|
|
322
|
-
The warrant is short-lived (~1h TTL); `up` refreshes it before expiry. Generated files (don't commit): `.
|
|
402
|
+
The warrant is short-lived (~1h TTL); `up` refreshes it before expiry. Generated files (don't commit): `.claude/settings.json` (hooks), `.mcp.json` (MCP proxy).
|
|
403
|
+
|
|
404
|
+
**State.** Keys, warrant, gateway config, receipts and Cloud credentials live outside the project, in `~/.tenuo/projects/<project>-<hash>/`, so nothing scoped to the project tree can reach them. `tenuo-claude state-dir` prints the path; `<state>/…` in these docs means a file in it. A project set up by an older release keeps using `<project>/.state` until the next `tenuo-claude refresh`, which moves it.
|
|
323
405
|
|
|
324
406
|
## Cloud Mode
|
|
325
407
|
|
|
@@ -330,7 +412,7 @@ For organizations: org root, fleet revocation, human approval, central audit.
|
|
|
330
412
|
1. **Create a Tenuo tenant** at [cloud.tenuo.ai](https://cloud.tenuo.ai). Complete the **Infrastructure** onboarding (provisions KMS-backed signing keys).
|
|
331
413
|
|
|
332
414
|
2. **Get credentials:**
|
|
333
|
-
- **Runtime key** (`tenuo_ct_…`) from Agents → Quick Connect → **Authorizer Only** →
|
|
415
|
+
- **Runtime key** (`tenuo_ct_…`) from Agents → Quick Connect → **Authorizer Only** → `<state>/cloud.env`
|
|
334
416
|
- **Tenant-admin key** (`tc_…`) from Settings → API Keys → Create (admin role) → `~/.tenuo/admin.env`
|
|
335
417
|
|
|
336
418
|
3. **Bootstrap with Cloud:**
|
|
@@ -391,7 +473,7 @@ When Claude tries a gated action:
|
|
|
391
473
|
|
|
392
474
|
**Setup:** See [docs/DETAILS.md § Approval setup runbook](docs/DETAILS.md#approval-setup-runbook).
|
|
393
475
|
|
|
394
|
-
For Cloud, receipts are sent to your tenant (central audit). Locally, receipts are always written to
|
|
476
|
+
For Cloud, receipts are sent to your tenant (central audit). Locally, receipts are always written to `<state>/receipts.jsonl`.
|
|
395
477
|
|
|
396
478
|
## Security Model
|
|
397
479
|
|
|
@@ -404,11 +486,11 @@ The warrant is a capability token: it lists what Claude can do and for how long.
|
|
|
404
486
|
|
|
405
487
|
**Receipts:** Every decision is signed with Ed25519 and hash-chained for tamper-evidence. Local `audit --verify` checks signatures, hash-chain links, warrant chains, and deterministic constraint replay. Cloud receipts are independently verified by the tenant.
|
|
406
488
|
|
|
407
|
-
**Fail-closed:** Missing or broken `tenuo.yaml` denies every governed call. Keys
|
|
489
|
+
**Fail-closed:** Missing or broken `tenuo.yaml` denies every governed call. Keys are owner-only and kept outside the project. A broken receipt sink surfaces loudly (not silent data loss).
|
|
408
490
|
|
|
409
491
|
**Rolling out to a team:** Keep `tenuo.yaml` in version control. Push hook/MCP wiring through Claude Code managed settings. Use Cloud for org-root warrants, central audit, and revocation. See [docs/DETAILS.md § Managed deployment](docs/DETAILS.md#managed-deployment) for the rollout checklist and hardened socket setup.
|
|
410
492
|
|
|
411
|
-
**Turning it off:** `tenuo-claude disable` unwires the hook (Claude stops calling the authorizer) and stops the authorizer, leaving policy and warrant in place. `tenuo-claude uninstall` also deletes
|
|
493
|
+
**Turning it off:** `tenuo-claude disable` unwires the hook (Claude stops calling the authorizer) and stops the authorizer, leaving policy and warrant in place. `tenuo-claude uninstall` also deletes the state directory (warrant, keys, credentials). `tenuo.yaml` is never touched.
|
|
412
494
|
|
|
413
495
|
---
|
|
414
496
|
|