Skip to content

Commit 06c18aa

Browse files
author
Su Fu
committed
docs(codex): default to daemon-backed enforcement
1 parent 175d798 commit 06c18aa

8 files changed

Lines changed: 168 additions & 94 deletions

File tree

clients/gmail-mcp/README.md

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,30 @@
11
# permit0-gmail-mcp
22

3-
MCP server that exposes Gmail tools (Gmail API v1) to Claude Code, **each
4-
call gated by permit0**. Mirrors `clients/outlook-mcp` — same 13 tools, same
5-
norm actions, same risk rules.
3+
MCP server that exposes Gmail tools (Gmail API v1) to agent hosts. With
4+
Codex, permit0 enforcement happens at the Codex `PreToolUse` hook layer:
5+
Codex proposes `mcp__permit0-gmail__gmail_send`, the permit0 hook sends it
6+
to the daemon, and the Gmail API call only runs when the hook allows it.
7+
Mirrors `clients/outlook-mcp` — same 13 tools, same norm actions, same
8+
risk rules.
69

710
## Architecture
811

12+
For Codex:
13+
914
```
10-
Claude Code ──MCP/stdio──▶ permit0-gmail-mcp
11-
12-
├─▶ @permit0.guard("email.X")
13-
│ └─ POST /api/v1/check_action ──▶ permit0 daemon
14-
│ (allow/deny/human)
15-
16-
└─▶ Gmail API (gmail.googleapis.com/gmail/v1)
17-
(only on allow)
15+
Codex ──PreToolUse hook──▶ permit0 hook --client codex --remote :9090
16+
│ │
17+
│ └─▶ permit0 daemon/dashboard
18+
│ (allow/deny/human)
19+
20+
└─MCP/stdio, only if allowed──▶ permit0-gmail-mcp
21+
└─▶ Gmail API
1822
```
1923

24+
For hosts without a hook layer, add an equivalent pre-tool gate before
25+
starting this MCP server. The server itself is plain MCP and does not
26+
call permit0 internally.
27+
2028
## Tools exposed
2129

2230
Same 13 tools as outlook-mcp — same parameters, same norm actions:
@@ -72,28 +80,49 @@ and silent-refreshes after that.
7280
python -c "from permit0_gmail_mcp.auth import get_token; print(bool(get_token()))"
7381
```
7482

75-
### 4. Wire into Claude Code
83+
### 4. Wire into Codex
84+
85+
Start the permit0 daemon and install the Codex hook in remote mode:
86+
87+
```bash
88+
cd /path/to/permit0
89+
cargo run -p permit0-cli -- serve --ui --port 9090
90+
PERMIT0_URL=http://127.0.0.1:9090 bash integrations/permit0-codex/examples/install-managed-prefs.sh
91+
```
92+
93+
Add the Gmail MCP server to your Codex config:
94+
95+
```toml
96+
[mcp_servers.permit0-gmail]
97+
command = "/absolute/path/to/permit0-gmail-mcp"
98+
```
99+
100+
Restart Codex. The 13 `gmail_*` tools appear, and every Gmail MCP tool
101+
call is enforced by the daemon-backed Codex hook before the MCP server
102+
is called.
103+
104+
### Claude Code
76105

77-
Add to `~/.claude.json` under top-level `mcpServers`:
106+
If you are using Claude Code, wire the server under top-level
107+
`mcpServers`:
78108

79109
```json
80110
{
81111
"mcpServers": {
82112
"permit0-gmail": {
83-
"command": "permit0-gmail-mcp",
84-
"env": {
85-
"PERMIT0_URL": "http://localhost:9090"
86-
}
113+
"command": "permit0-gmail-mcp"
87114
}
88115
}
89116
}
90117
```
91118

92-
Restart Claude Code. The 13 `gmail_*` tools appear.
119+
Then configure a pre-tool permit0 hook for Claude Code so the MCP call is
120+
gated before it reaches Gmail.
93121

94122
You can have **both** `permit0-outlook` and `permit0-gmail` configured at
95-
the same time — both gate through the same permit0 daemon, both lower to
96-
the same `email.*` norm actions, same risk rules.
123+
the same time. With the host's permit0 hook configured, both lower to the
124+
same `email.*` norm actions and are evaluated by the same daemon, using
125+
the same risk rules.
97126

98127
## Try it
99128

@@ -102,7 +131,8 @@ In Claude Code:
102131
> Search my Gmail for "newsletter" emails from the past 7 days and archive all results.
103132
104133
Claude Code will call `gmail_search(query="newsletter newer_than:7d")`
105-
permit0 evaluates each result's `gmail_archive(message_id=...)`.
134+
the host hook evaluates each `gmail_archive(message_id=...)` before it
135+
reaches the MCP server.
106136

107137
> Read that entire thread.
108138
@@ -123,12 +153,12 @@ conversationId stitching needed).
123153

124154
| Variable | Default | Purpose |
125155
|----------|---------|---------|
126-
| `PERMIT0_URL` | `http://localhost:9090` | permit0 daemon URL |
127156
| `GMAIL_CREDENTIALS` | `~/.permit0/gmail_credentials.json` | OAuth app credentials |
128157

129158
## Cross-backend invariant
130159

131-
If you have both Gmail and Outlook MCP servers configured, **both** lower
132-
to the same `email.*` norm actions. permit0 sees one unified IR. The
133-
dashboard's audit log shows `channel=gmail` vs `channel=outlook`, but the
134-
risk rule and the human reviewer's decision applies regardless.
160+
If you have both Gmail and Outlook MCP servers configured behind a
161+
permit0 hook, **both** lower to the same `email.*` norm actions. permit0
162+
sees one unified IR. The dashboard's audit log shows `channel=gmail` vs
163+
`channel=outlook`, but the risk rule and the human reviewer's decision
164+
applies regardless.

clients/outlook-mcp/README.md

Lines changed: 50 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
# permit0-outlook-mcp
22

3-
MCP server that exposes Outlook tools (Microsoft Graph) to Claude Code, **each
4-
call gated by permit0** so policy is enforced before any email is sent, moved,
5-
or deleted.
3+
MCP server that exposes Outlook tools (Microsoft Graph) to agent hosts.
4+
With Codex and Claude Code, permit0 enforcement happens at the host
5+
`PreToolUse` hook layer: the host proposes
6+
`mcp__permit0-outlook__outlook_send`, the permit0 hook sends it to the
7+
daemon, and the Graph API call only runs when the hook allows it.
68

79
## Architecture
810

11+
```text
12+
Agent host ──PreToolUse hook──▶ permit0 hook --client <host> --remote :9090
13+
│ │
14+
│ └─▶ permit0 daemon/dashboard
15+
│ (allow/deny/human)
16+
17+
└─MCP/stdio, only if allowed──────▶ permit0-outlook-mcp
18+
└─▶ Microsoft Graph API
919
```
10-
Claude Code ──MCP/stdio──▶ permit0-outlook-mcp
11-
12-
├─▶ @permit0.guard("email.X")
13-
│ └─ POST /api/v1/check_action ──▶ permit0 daemon
14-
│ (allow/deny/human)
15-
16-
└─▶ Microsoft Graph API
17-
(only on allow)
18-
```
20+
21+
For hosts without a hook layer, add an equivalent pre-tool gate before
22+
starting this MCP server. The server itself is plain MCP and does not
23+
call permit0 internally.
1924

2025
## Tools exposed
2126

@@ -33,13 +38,19 @@ Claude Code ──MCP/stdio──▶ permit0-outlook-mcp
3338

3439
## Setup
3540

36-
### 1. Make sure the permit0 daemon is running
41+
### 1. Make sure the permit0 daemon and host hook are running
3742

3843
```bash
3944
cd /path/to/permit0
4045
cargo run -p permit0-cli -- serve --ui --port 9090
4146
```
4247

48+
For Codex, install the daemon-backed hook:
49+
50+
```bash
51+
PERMIT0_URL=http://127.0.0.1:9090 bash integrations/permit0-codex/examples/install-managed-prefs.sh
52+
```
53+
4354
### 2. Install this MCP server + the Python SDK
4455

4556
```bash
@@ -61,43 +72,49 @@ Visit the Microsoft device-login URL it prints, sign in with your personal
6172
Outlook account, approve `Mail.ReadWrite` + `Mail.Send`. Token caches to
6273
`~/.permit0/outlook_token.json` and is reused by this MCP server.
6374

64-
### 4. Wire into Claude Code
75+
### 4. Wire into your host
76+
77+
For Codex, add the Outlook MCP server to your Codex config:
78+
79+
```toml
80+
[mcp_servers.permit0-outlook]
81+
command = "/absolute/path/to/permit0-outlook-mcp"
82+
```
6583

66-
Add to your Claude Code config (typically `~/.claude.json` or a project-local
67-
`.mcp.json`):
84+
For Claude Code, add it to `~/.claude.json` or project-local `.mcp.json`:
6885

6986
```json
7087
{
7188
"mcpServers": {
7289
"permit0-outlook": {
73-
"command": "permit0-outlook-mcp",
74-
"env": {
75-
"PERMIT0_URL": "http://localhost:9090"
76-
}
90+
"command": "permit0-outlook-mcp"
7791
}
7892
}
7993
}
8094
```
8195

82-
Restart Claude Code. The 9 tools above appear in its tool list.
96+
Restart the host. The 9 tools above appear in its tool list, and each
97+
mutating Outlook MCP call is enforced by the daemon-backed host hook
98+
before the MCP server is called.
8399

84100
## Try it
85101

86-
In Claude Code:
102+
In your agent host:
87103

88104
> List the 5 most recent emails in my inbox and archive all promotional emails from last week.
89105
90-
Claude Code will call `outlook_list` (no permit0 check), then for each
91-
candidate message, call `outlook_archive(message_id=...)`. Each archive call
92-
hits permit0 — you'll see the decisions live in the dashboard at
93-
http://localhost:9090/ui/ under the **Audit** tab.
106+
The host will call `outlook_search` or `outlook_list`, then for each
107+
candidate message, call `outlook_archive(message_id=...)`. Each archive
108+
call is evaluated by the hook before it reaches the MCP server; you'll
109+
see the decisions live in the dashboard at <http://localhost:9090/ui/>.
94110

95111
> Send an email to bob@example.com with subject "test" and body "hi".
96112
97-
Claude Code calls `outlook_send(to=..., subject=..., body=...)`. permit0
98-
evaluates `email.send` against its risk rule. A clean send → ALLOW. Embedding
99-
something like `password is hunter2` in the body → DENY (the SDK raises
100-
`permit0.Denied`, the MCP layer reports the block to Claude Code).
113+
The host calls `outlook_send(to=..., subject=..., body=...)`. permit0
114+
evaluates `email.send` against its risk rule. A clean send can run.
115+
Sensitive content or risky recipients return a deny envelope from the
116+
hook, so the host blocks the tool call before the MCP layer reaches
117+
Microsoft Graph.
101118

102119
## Combining with shadow mode
103120

@@ -107,16 +124,14 @@ If you want to **observe** without enforcement while you tune the risk rules:
107124
export PERMIT0_SHADOW=1 # for `permit0 hook` use
108125
```
109126

110-
Note: this env var is read by the **`permit0 hook`** subcommand
111-
(Claude Code PreToolUse). The MCP server itself enforces directly via
112-
`@permit0.guard` — there's no built-in shadow flag yet. If you want shadow
113-
mode for MCP tools too, ask and we'll add it.
127+
Note: this env var is read by the **`permit0 hook`** subcommand. The MCP
128+
server itself is plain MCP, so shadow/enforcement behavior is controlled
129+
by the host hook command.
114130

115131
## Configuration
116132

117133
| Variable | Default | Purpose |
118134
|----------|---------|---------|
119-
| `PERMIT0_URL` | `http://localhost:9090` | permit0 daemon URL |
120135
| `MSGRAPH_CLIENT_ID` | Microsoft Graph PowerShell public client | Override with your own Azure App reg |
121136

122137
## What this is NOT
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
"""permit0-outlook-mcp: MCP server exposing Outlook tools, each gated by permit0."""
1+
"""permit0-outlook-mcp: plain MCP server exposing Outlook tools."""
22

33
__version__ = "0.1.0"

clients/outlook-mcp/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
55
[project]
66
name = "permit0-outlook-mcp"
77
version = "0.1.0"
8-
description = "MCP server exposing Outlook (Microsoft Graph) tools, each gated by permit0"
8+
description = "Plain MCP server exposing Outlook (Microsoft Graph) tools for host-level permit0 hooks"
99
readme = "README.md"
1010
requires-python = ">=3.10"
1111
license = {text = "MIT"}

integrations/permit0-codex/README.md

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ Model proposes a tool call (Bash, apply_patch, mcp__<server>__<tool>)
1515
1616
Codex fires PreToolUse hook → spawns `permit0 hook --client codex`
1717
18-
permit0 normalizes the tool call through packs, scores it, returns
19-
either empty stdout (no objection) or a deny envelope
18+
permit0 forwards the tool call to the daemon, which normalizes it
19+
through packs, scores it, records the decision for the dashboard, and
20+
returns either empty stdout (no objection) or a deny envelope
2021
2122
Codex either runs the tool or blocks it with permit0's reason
2223
```
@@ -34,9 +35,20 @@ Codex either runs the tool or blocks it with permit0's reason
3435
# produces ./target/release/permit0
3536
```
3637

37-
## Setup (production-ish)
38+
## Setup (dashboard-visible enforcement)
3839

39-
Add the hook to your Codex config and trust it once via the TUI.
40+
Start the permit0 daemon, then add the hook to your Codex config and
41+
trust it once via the TUI. Remote daemon mode is the recommended setup:
42+
it gives one enforcement point, one approval flow, and one dashboard
43+
record for every Codex tool decision.
44+
45+
0. Start the daemon:
46+
47+
```bash
48+
cargo run -p permit0-cli -- serve --ui --port 9090
49+
```
50+
51+
Open <http://127.0.0.1:9090/ui/> to watch decisions and approvals.
4052

4153
1. Append this block to `~/.codex/config.toml`:
4254

@@ -49,7 +61,7 @@ Add the hook to your Codex config and trust it once via the TUI.
4961

5062
[[hooks.PreToolUse.hooks]]
5163
type = "command"
52-
command = "/absolute/path/to/permit0/target/release/permit0 hook --client codex --packs-dir /absolute/path/to/permit0/packs --unknown defer"
64+
command = "/absolute/path/to/permit0/target/release/permit0 hook --client codex --remote http://127.0.0.1:9090 --unknown deny"
5365
timeout = 30
5466
statusMessage = "permit0 safety check"
5567
```
@@ -70,24 +82,33 @@ Add the hook to your Codex config and trust it once via the TUI.
7082
Exit Codex.
7183

7284
3. From now on every `codex` and `codex exec` session fires permit0 on
73-
every tool call. Re-trust is required if you edit the hook command
85+
every tool call. If the daemon is down, remote mode fails closed and
86+
blocks the tool. Re-trust is required if you edit the hook command
7487
(Codex tracks a content hash).
7588

89+
Local hook mode (`--packs-dir ...`) still exists for offline development
90+
and synthetic smoke tests, but it evaluates in the hook process and does
91+
not write decisions to the daemon dashboard. Do not use local mode when
92+
you expect dashboard-visible enforcement.
93+
7694
## Setup (unattended — macOS auto-trust)
7795

7896
For CI, scripts, or anywhere you can't open the TUI: install the hook
7997
via macOS managed preferences. Codex treats these as MDM-sourced =
8098
always trusted, no review needed.
8199

82100
```bash
101+
cargo run -p permit0-cli -- serve --ui --port 9090
83102
bash integrations/permit0-codex/examples/install-managed-prefs.sh
84103
```
85104

86105
The script reads the same TOML config layered into macOS user defaults
87-
under `com.openai.codex/requirements_toml_base64`. To uninstall:
106+
under `com.openai.codex/requirements_toml_base64`. By default it installs
107+
remote daemon mode against `http://127.0.0.1:9090`; override with
108+
`PERMIT0_URL=http://host:port` if your daemon runs elsewhere. To uninstall:
88109

89110
```bash
90-
defaults delete com.openai.codex requirements_toml_base64
111+
bash integrations/permit0-codex/examples/install-managed-prefs.sh --uninstall
91112
```
92113

93114
This is the path the live-demo launcher in `dev-test-rig/codex-demo`
@@ -114,8 +135,8 @@ enforcement boundary." permit0 inherits them.
114135
|---|---|---|
115136
| `Allow` / `Defer` | Zero stdout bytes | Tool runs |
116137
| `Deny` | `permissionDecision: "deny"` envelope with reason | Tool blocked, model sees the reason |
117-
| `HumanInTheLoop` (Medium/High tier) | `permissionDecision: "deny"` envelope with `" — requires human review"` marker appended to the reason | Tool blocked; the marker tells users this was a HITL action under Claude Code, not a hard Critical block |
118-
| Internal error in permit0 | `permissionDecision: "deny"` envelope with `"permit0 internal error: <msg>"` | Tool blocked (fail-closed) |
138+
| `HumanInTheLoop` (Medium/High tier) | `permissionDecision: "deny"` envelope with `" — requires human review"` marker appended to the reason | Tool blocked; the marker tells users this was a HITL action, not a hard Critical block |
139+
| Daemon unavailable / internal error | `permissionDecision: "deny"` envelope with the failure reason | Tool blocked (fail-closed) |
119140

120141
Codex `PreToolUse` does **not** support `permissionDecision: "allow"` or
121142
`"ask"`; both are explicitly rejected. permit0 never emits either.

0 commit comments

Comments
 (0)