Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 116 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
</p>

<p align="center">
<strong>The open sensor and control transport for coding-agent tooling.</strong>
<strong>Keep your coding agent. Add the behavior it is missing.</strong>
</p>

<!-- pitot-adapter-supervisor:start -->
Expand All @@ -22,13 +22,62 @@
One language-neutral boundary for the coding agents your team already uses.
</p>

Pitot lets you build above coding agents without rebuilding every host
integration or forking an agent runtime. It converts host-specific activity
into a stable local event stream and carries correlated responses from your
controller when a host is waiting synchronously.
Your coding agent runs shell commands, edits files, and calls tools. Pitot lets
you put your own code in the loop at that boundary — to allow, deny, or record
each action — without forking the agent or rewriting a host integration for
every tool.

Pitot reports what happened. Your code decides what it means.

## See it work with Kimi

The fastest way to understand Pitot is to watch one real command get allowed and
another get denied. This walkthrough is exactly what Pitot's automated test suite
exercises on every commit, so the behavior below is verified, not aspirational.

**1. Scaffold a sample shell policy.** This writes a runnable Controller that
allows shell commands by default and denies any command containing the canary
string `PITOT_DENY_ME`:

```bash
pitot init --template shell-policy --language go --dir ./kimi-policy
cd ./kimi-policy
```

**2. Check your Kimi host wiring** (Pitot does not edit your Kimi config for you):

```bash
pitot doctor --host kimi
```

If the `PreToolUse` hook is missing, add it to `~/.kimi-code/config.toml`:

```toml
[[hooks]]
event = "PreToolUse"
matcher = "Bash"
command = "pitot hook kimi"
```

**3. Run Kimi behind the Controller.** `pitot dev` starts the runtime, launches
the agent you name after `--`, and prints each decision:

```bash
pitot dev --host kimi -- kimi -p "Run: echo hello"
```

An ordinary command is allowed and runs. Now ask for the canary:

```bash
pitot dev --host kimi -- kimi -p "Run: PITOT_DENY_ME=1 echo nope"
```

The Controller denies it. The denied command never executes, and the denial
reason — `Pitot sample policy blocked the PITOT_DENY_ME canary.` — is returned to
Kimi as the blocked tool result. The `shell-policy` sample is a demonstration
tripwire, not a general shell-security control; the point is that *your* code
made the decision.

## Why Pitot?

Building above coding agents usually forces one of two expensive choices:
Expand Down Expand Up @@ -57,20 +106,6 @@ A passive Consumer cannot reach the response channel. A Controller is
statically registered for one request kind and returns at most one response for
the pending action.

## Use-case gallery

### Operational patterns (grid)

If you want to see concrete integration ideas, start with the **Use-Cases Grid**:

- [04 Use-Cases Editorial Grid](./brand-exploration/design-demos/04-use-cases-editorial-grid.html)

This gallery shows practical ways teams can compose Consumers and Controllers
without forcing each workflow into the host or into a single monolithic runtime.
It includes both engineering patterns (action auditing, approvals, audit hooks) and
non-coding workflows (email triage, file movement, and local automation),
so you can quickly evaluate where Pitot helps before building.

## Two small programs

<p align="center">
Expand Down Expand Up @@ -166,62 +201,61 @@ pitot doctor

## Quickstart

From a clean repository to one real allow/deny decision in two commands.

**1. Scaffold a project.** `pitot init` detects the language from the files
already in the directory, or prompts you to choose when it cannot. It writes a
runnable project — source, package manifest, and `.pitot.yaml` — and never
overwrites existing files unless you pass `--force`:
**1. Scaffold a Controller.** `pitot init` writes a runnable project — source, a
package manifest, and `.pitot.yaml` — and never overwrites existing files unless
you pass `--force`. Pick a starting template with `--template`:

```bash
pitot init
pitot init --template shell-policy --language go --dir ./kimi-policy
```

```
Detected python project in .
Initialized python controller in .
Files written: .pitot.yaml, main.py, pyproject.toml, requirements.txt
Next: cd . && pitot dev --host claude --exec "python3 main.py"
Initialized go controller (shell-policy) in ./kimi-policy
Files written: .pitot.yaml, go.mod, main.go
Next:
1. cd ./kimi-policy
2. Configure a supported host hook (see: pitot doctor --host HOST).
3. Run: pitot dev --host HOST -- AGENT [ARGS...]
example: pitot dev --host kimi -- kimi -p "<prompt>"
```

You can skip detection and prompts with flags — handy for CI:
Available templates are `shell-policy` (allow/deny shell commands),
`release-approval` and `blank-controller` (request/response controllers), and
`blank-consumer` (a passive event reader). Without `--template`, `pitot init`
detects the language from the current directory or prompts you to choose. The
four first-class languages (`python`, `typescript`, `go`, `rust`) each generate a
complete project that builds after installing dependencies.

```bash
pitot init --language python --role controller --dir ./approval
```

The four first-class languages (`python`, `typescript`, `go`, `rust`) each
generate a complete project: `python3 main.py`, `npx tsx main.ts`,
`go run main.go`, and `cargo run` all work after installing dependencies.

**2. Run it against an agent.** `pitot dev` starts the runtime on a private
loopback endpoint, waits until it is ready, launches your Controller, and prints
each decision as the agent makes it. `--exec` takes the full command line (or use
`-- CMD ARGS`):
**2. Run your agent behind it.** `pitot dev` starts the runtime and the
Controllers declared in `.pitot.yaml`, waits until the runtime is ready, then
launches the agent you name after `--` with `PITOT_RUNTIME` set so its host hook
finds the runtime. It prints each decision as the agent makes it:

```bash
pitot dev --host claude --exec "python3 main.py"
pitot dev --host kimi -- kimi -p "Run: PITOT_DENY_ME=1 echo nope"
```

```
Starting Pitot dev environment for host claude...
Runtime ready. Starting agent: python3 main.py
Starting Pitot dev environment for host kimi...
Runtime ready. Starting agent: kimi -p Run: PITOT_DENY_ME=1 echo nope
Decisions:
[ALLOW] release.approval (act_7f2) — v1.4.0 is approved for publication.
[DENY] shell.exec (act_1a9) — destructive command blocked
[DENY] shell (act_1a9) — Pitot sample policy blocked the PITOT_DENY_ME canary.
Agent finished. Runtime stopped.
```

`--host` must name a supported agent (`claude`, `codex`, `copilot`, `cursor`,
`gemini`, `kimi`, `opencode`, `pi`, `qwen`). The runtime descriptor lives in a
per-invocation temporary path and is removed on exit, so concurrent `pitot dev`
sessions never collide.
`gemini`, `kimi`, `opencode`, `pi`, `qwen`), and that agent's host hook must
already be wired to `pitot hook HOST` (see **Connect your agent** and
`pitot doctor --host HOST`). The runtime descriptor lives in a per-invocation
temporary path and is removed on exit, so concurrent `pitot dev` sessions never
collide.

**3. Swap the agent.** The same project — the same Controller and `.pitot.yaml` —
works with any other supported host. Change only `--host`:
works with any other supported host whose hook is wired. Change only `--host` and
the agent command:

```bash
pitot dev --host cursor --exec "python3 main.py"
pitot dev --host cursor -- cursor-agent -p "Run: PITOT_DENY_ME=1 echo nope"
```

The boundary is language- and agent-neutral: one Controller, every agent.
Expand Down Expand Up @@ -251,10 +285,37 @@ $env:PITOT_RUNTIME = Join-Path $env:LOCALAPPDATA "Pitot\project.json"
pitot run --config .pitot.yaml --runtime $env:PITOT_RUNTIME
```

## Supported hosts

Every host below normalizes its native blocking boundary to a `shell` action and
passes Pitot's language-neutral decoder conformance suite. The E2E column marks
adapters exercised by the cross-platform agent supervisor (the badge at the top)
on Ubuntu, macOS, and Windows. Kimi additionally has an in-repo, no-model test
that asserts the full allow **and** deny control path end to end.

| Host | Blocking boundary | Hook wiring | Verified in this repo |
|---|---|---|---|
| Kimi Code | `PreToolUse` / Bash | native `config.toml` | decoder + E2E + allow/deny control test |
| Claude | `PreToolUse` | native settings hook | decoder + E2E |
| Cursor | `beforeShellExecution` | bridge (`integrations/cursor`) | decoder + E2E |
| Codex | `PreToolUse` | bridge (`integrations/codex`) | decoder + E2E |
| GitHub Copilot CLI | `PreToolUse` | bridge (`integrations/copilot`) | decoder + E2E |
| Gemini | `BeforeTool` | bridge (`integrations/gemini`) | decoder + E2E |
| OpenCode | `PreToolUse` | bridge (`integrations/opencode`) | decoder + E2E |
| Pi | `tool_call` | extension (`integrations/pi`) | decoder + E2E |
| Qwen Code | `PreToolUse` | bridge (`integrations/qwen`) | decoder + E2E |

"Decoder" means Pitot correctly normalizes that host's payload into the stable
event envelope. It does not claim Pitot judges whether any command is safe — that
decision belongs to your Controller.

## Connect your agent

The per-host hooks below wire each agent's native blocking boundary to Pitot for
the manual runtime flow. `pitot dev` configures the selected `--host` for you.
The per-host hooks below wire each agent's native blocking boundary to Pitot.
This wiring is a one-time edit to each host's own configuration; Pitot does not
edit your host config for you. Run `pitot doctor --host HOST` to check whether a
host's hook is correctly configured. Once wired, both `pitot dev` and the manual
runtime flow use the same hook.

### Kimi Code

Expand Down
17 changes: 12 additions & 5 deletions UPSTREAM.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"files": {
"CONTRIBUTING.md": "23728d8a132d62b8adfb2e5c3eb9d9bfcf8a4d04543765b1e22ad8d55424af8f",
"README.md": "77995d36de1a6ac5f5c687fad4a152b8824ea3ad25abe65325a7c4ee9a2ef52d",
"README.md": "bd662302b629066b630dbb4c274174a4df61e98ab699417b481d7a48590de40b",
"adapter-verification.json": "f8ad4e206571650f698826a8b66d8c00822be425e8d2de8ae98d98239e575eb4",
"adapters/adapters.go": "1b46ba131fa3b2c93eed23526330275a3506451ba4bbd4f497e5378dfab2b6a8",
"assets/pitot-boundary.png": "8a0ddb7d81831d94e14813f50ea4ca8670d77417f339ed2f91f0c653bf52f41d",
Expand All @@ -14,16 +14,23 @@
"bridge/bridge.go": "5adfcd3f743cae46e4446a6e030d53464ada97de0261a8588fa2a9fcd62136b8",
"bridge/bridge_test.go": "6dcc6d05f2b39c25955fc0b2d21d3d148dd9d77600fb12799941f86bdb1acb61",
"cmd/generate-schema/main.go": "6e9d0030290d99e36967433f96e38385a122974f899ad9421aac1ef7e50d8fcb",
"cmd/pitot/main.go": "14de1d6bf7ef7a75172ffd015c7cab1b66c38706b879de8d354137b34596c7f3",
"cmd/pitot/doctor_host.go": "7ecade40618bfb3510ae8e55fa802361371b4f7fbafedcd61233d19ef46cb219",
"cmd/pitot/doctor_host_test.go": "4e6e327f6cf27cf94a0a608e10eb6790d6c11fcd53e6dfd7370007190749952f",
"cmd/pitot/kimi_control_test.go": "b3803a9bbdecf5f7e9a3dca90e317bdb94b4ba9bfa21d369d5a152d6742eac63",
"cmd/pitot/kimi_smoke_test.go": "6c2b92a8d3257955617d1387bc3f788846e0be091742c074a762f2b5cd04fbdf",
"cmd/pitot/main.go": "27d00919d7cc687e2b58c930024aba2ae7009a768af0abd9e73abfe123f1c8c3",
"cmd/pitot/main_test.go": "544997295e0c4b75ef8f3d698b3de0883153f671b6b8f62057cc6e3452d6dc93",
"cmd/pitot/workbench.go": "2e2522491437c624b241fd594d678be2dab6824fc5aa3235db1ec82e00a669c7",
"cmd/pitot/workbench.go": "70497ca0fd5579d8c1df5350e037cb46096038449293814f995b8c209a99b215",
"cmd/pitot/workbench_build_test.go": "8e9ae497a03c9f1ca0a1fc9ebf821df3a1869993cbb2cd568da3996d437551ec",
"cmd/pitot/workbench_contract_test.go": "f88ba5a34d16d1a18fd2a4fed54c7b4cbb62fb3ffbd0eb6091e2143ba89234c5",
"cmd/pitot/workbench_dev_test.go": "9693e84f24facd7d97cefcc92d95c0e6950d9422a7c23f8b07487bbd35df2eac",
"cmd/pitot/workbench_test.go": "457caa11cd4b73c1fb4e0dad806b3050b196ddac690a8125f1615a4c695cc073",
"config/config.go": "e6666567d0c0cca41de69361e8f1243adda1ec0a54a9300b39a84d2290bff319",
"config/config_test.go": "87d3e5ddc4a3b43c736070de671d03e03ffe29cdd759771526ad27fd9bc0034c",
"conformance/conformance.go": "43b692114f45c8b52958e34b35aee1cee339d8321c90f92ab4f5b963e79935bb",
"conformance/conformance_test.go": "83ab0bcc15371265a954d177e4e97d81ad3ea734bbf736a29a54628ef64b52cd",
"conformance/fixtures/negative.jsonl": "503ea76988df595d96ebf695f991b8ea6c892be4a578522dff4ddb0d39b647e4",
"conformance/fixtures/positive.jsonl": "23010bb2306f90fec40dc870cd922089550dbdfc977f778561c81032f4912a4c",
"conformance/fixtures/positive.jsonl": "881efdf58b66ee7d03171c6b4410bf1bce9e1b8db5c8bf969d0e8ec467420c3f",
"doc.go": "a8abdafac969b1bf4372c8bb023aa51125dc073f03218f4ab9913dfc5ffa877d",
"e2e/e2e_coverage_test.go": "6235bd1df7212e4e229be324ac50f592aef70dce8855519d02e6b843656ea109",
"e2e/e2e_hook_test.go": "5e184dc8907b6e36daeab90bbbb1654fa5336312866412031805a13ba535d1f8",
Expand Down Expand Up @@ -70,7 +77,7 @@
"sdk/python/pitot/__init__.py": "9cab11b333536f167d4e7bb6089ef00488690ec7b5cd5701f08e8bbb13cef0f7",
"sdk/python/pitot/runner.py": "8d4537ffc3aee2ea22b1d691559ac1eb1e95521df9ad5bf241ee6ac54c9925b1",
"sdk/python/pitot/types.py": "c9a7221f1ad6627f152f26148155d3c74f249c52262c73a515251f469e8eb4ad",
"sdk/python/pyproject.toml": "4b43380a1ebc12350ee3c30633695160ba0fc0bb64501b1e16f990a6fbe56d02",
"sdk/python/pyproject.toml": "ecd3d54f2e31ae4a95ab168d6a6674d218ae5d4cf1e91bf8c64a8cc1ce3e74cb",
"sdk/runner.go": "e9a8db96d3cf6df7ea7e661e6755650f97e580970995dfe881b4268db0c3f832",
"sdk/rust/Cargo.toml": "b8435f6c600ad0791bd29ada4bc396b14e54b96ee3804896c8e610583cc70c1a",
"sdk/rust/src/lib.rs": "fd2dcb9bf9df47fb58e52e4e94136f95867616d941ba66b4b324413d1c5b1777",
Expand Down
Loading
Loading