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
203 changes: 203 additions & 0 deletions labs/15-pitot/KIMI_CONTROLLED_ACTION_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# Plan: Pitot's first truthful, reproducible Kimi controlled action

**Branch:** `worktree-pitot-kimi-controlled-action` (off `origin/main`)
**PR title:** Make Pitot's first controlled action truthful and reproducible
**Public promise:** *Keep your coding agent. Add the behavior it is missing.*

Goal: prove `clone → start one shell Controller → launch Kimi → allow one action → deny one
action → prove denied command never ran → show denial reached Kimi`, then rewrite the README
around that tested outcome. Everything below is grounded in the current code.

---

## Ground truth (verified against the tree)

All paths under `labs/15-pitot/`.

| Claim in the brief | Verified reality | File |
|---|---|---|
| Controller registered for `test.approval` | Confirmed, hardcoded regardless of language | `pitot/cmd/pitot/workbench.go:198-205` |
| Kimi normalizes `PreToolUse` → kind `shell` | Confirmed | `pitot/adapters/adapters.go:196-215` |
| Dispatch keyed by `event.action.kind` | Confirmed; controllers map keyed by kind | `pitot/runtime/runtime.go:118-144` |
| Generated Controller can't control Kimi | Confirmed — `test.approval` ≠ `shell`, never matches | — |
| Next-step hint points `--exec` at the Controller | Confirmed: `Next: ... pitot dev --host claude --exec "go run main.go"` | `pitot/cmd/pitot/workbench.go:135-137` |
| `--host` validates only, never configures a host | Confirmed — only `adapters.IsSupported` + banner | `pitot/cmd/pitot/workbench.go:503-508,540` |
| README claims `pitot dev` configures the host | Confirmed, line 257 of the public README | `public-readme-preview/README.md:257` |
| README use-case gallery link is unprojected | Confirmed — points into excluded `brand-exploration/` | `public-readme-preview/README.md:66` |
| Generated manifests assume `0.1.0` | Confirmed via `pitotPackageVersion = "0.1.0"` | `pitot/cmd/pitot/workbench.go:22,154,348,373,430,465` |
| Python dist name `pitot` occupied on PyPI | Confirmed — SDK **and** generated manifest both use bare `pitot` | `pitot-distribution/sdk/python/pyproject.toml:6`; `workbench.go:154,348` |

**Corrections to the brief (things that differ from its assumptions):**

1. **No `--template` flag exists.** `pitot init` supports only `--language`, `--role`, `--dir`,
`--force`. Templates must be added from scratch (`workbench.go:37-62`).
2. **`pitot setup` does not exist. `pitot doctor` does** but has **no `--host` flag** — it loops
every host and prints a decoder PASS/FAIL only (`cmd/pitot/main.go:199-217`). Part 2's check
is an *extension of `doctor`* to accept `--host` and add PATH/config/hook checks. Recommend
`pitot doctor --host kimi` (smaller surface than a new `setup` verb).
3. **Python rename is bigger than the brief implies.** The published SDK `pyproject.toml` itself
uses `name = "pitot"`, not just the generated manifest. Both must change to
`operatorstack-pitot` while keeping `import pitot`.
4. **Command extraction shape:** in Full mode `Content.Full` is a **JSON-encoded string** of the
command (`projection/projection.go:40-46`). The shell-policy controller must
`json.Unmarshal(event.Content.Full, &command)` after confirming `event.Content.Mode == "full"`.
5. **README is `public-readme-preview/README.md`** (the only projected README). Any surface change
requires regenerating `pitot-distribution/UPSTREAM.json` via `scripts/build_pitot.py --write`,
or CI's `--check` fails (`scripts/build_pitot.py:153-176`).
6. **No `docs/public-claims.json` exists in this lab.** The support-matrix truthfulness lives in
the README + `adapter-verification.json`. Do not invent a claims file unless we choose to.
7. **Kimi already has an allow-path unit test** (`cmd/pitot/main_test.go:172-196`) but **no
deny-path test**, and live-CLI E2E is intentionally deferred
(`pitot-distribution/release-notes/2026-07-22-kimi-code-adapter.md`).

---

## Work breakdown (single PR, ordered so each step is independently green)

### Step 1 — `shell-policy` template + template selection (`workbench.go`)

- Add `--template` flag to `runInit` (`workbench.go:37-62`); allowed values:
`shell-policy`, `release-approval`, `blank-controller`, `blank-consumer`. Default preserves
today's behavior (map `blank-controller`/`blank-consumer` to the existing templates).
- New `shell-policy` controller template per language. Go example (the reference language, since
`main.go` is the sample Controller):
```go
func main() {
sdk.RunController("local-shell-policy", func(req schema.ControlRequested) sdk.Outcome {
var event schema.Event
if err := json.Unmarshal(req.Data, &event); err != nil {
return sdk.Deny("Pitot sample policy could not decode the event.")
}
if event.Content == nil || event.Content.Mode != schema.ContentFull {
return sdk.Deny("Pitot sample policy requires full content mode.")
}
var command string
if err := json.Unmarshal(event.Content.Full, &command); err != nil {
return sdk.Deny("Pitot sample policy could not decode the command.")
}
if strings.Contains(command, "PITOT_DENY_ME") {
return sdk.Deny("Pitot sample policy blocked the PITOT_DENY_ME canary.")
}
return sdk.Allow("Pitot sample policy allowed the shell request.")
})
}
```
Include a code comment stating the substring check is a **sample canary, not production shell
security** (brief requirement; no general security claim).
- `pitotConfig` (`workbench.go:187-206`): when template is `shell-policy`, emit the controller
keyed under **`shell`** (not `test.approval`) with `id: local-shell-policy`, `deadline_ms: 2000`,
`on_timeout: deny`, `on_unavailable: deny`. This is the change that actually lets the Controller
govern Kimi (dispatch keys on `shell`).
- Keep this PR to those 4 templates only.

### Step 2 — Fix generated guidance (`workbench.go:135-137`)

- Stop pointing `--exec` at the Controller. The runtime already launches `main.go` from
`.pitot.yaml`; `--exec`/`-- CMD` is for the **agent**.
- If host known: `pitot dev --host kimi -- kimi` (or `-- kimi -p "<prompt>"`).
- If host unknown, print the two-line "configure a host, then `pitot dev --host HOST -- AGENT`"
hint from the brief.

### Step 3 — Truthful host setup: `pitot doctor --host kimi` (`cmd/pitot/main.go:199-217`)

- Extend `runDoctor` to accept `--host`. With a host, verify and report:
`kimi` on `PATH`; Kimi config path resolvable; a `PreToolUse`/`Bash` hook exists; hook command
invokes `pitot hook kimi`; config parses (use `kimi doctor` when available).
- **Do not** have `pitot dev` edit global Kimi config in this PR. Document the canonical TOML:
```toml
[[hooks]]
event = "PreToolUse"
matcher = "Bash"
command = "pitot hook kimi"
timeout = 5
```
- Document Kimi hooks are **fail-open** on crash/timeout per host semantics; do not market as a
sandbox.

### Step 4 — Two levels of Kimi testing

- **Test A (deterministic, no model)** — new Go test (extend `cmd/pitot/main_test.go`, or a new
`e2e` test). Flow: start runtime + sample shell Controller → submit canonical Kimi allow payload
→ assert hook exit 0 → harness executes canary → assert `/tmp/pitot-allowed-canary` exists →
submit deny payload → assert hook exit 2 → harness does **not** execute canary → assert
`/tmp/pitot-denied-canary` absent → assert deny stderr carries the Controller reason → assert
decision receipt has `kind=shell`, `outcome=deny`. Uses the exact allow/deny payloads from the
brief. This is the gate for the whole PR and for rewriting the README.
- **Test B (opt-in real Kimi smoke)** — manual/opt-in (needs Kimi auth). Preflight
(`kimi --version`, `kimi doctor`, `pitot doctor --host kimi`), clean canaries, run the brief's
`pitot dev --host kimi -- kimi -p '...'` prompt, assert postconditions, and emit a **bounded JSON
evidence artifact** (Kimi version, hook config hash w/ secrets omitted, pitot commit + binary
SHA-256, runtime descriptor identity, allow/deny action IDs, Controller outcomes, canary states,
final Kimi text, exit status). Prose alone is not proof.

### Step 5 — Workbench test hardening (`cmd/pitot/workbench_test.go`)

Replace "files exist" with contract + build tests:
- **Init contract**, per language: expected files exist; `.pitot.yaml` parses; role+template map to
the correct request kind (**`shell` for shell-policy**); generated source references the right SDK
API; non-destructive; next-step guidance launches an **agent**, not the Controller.
- **Build tests** (gated on available toolchains, no remote registries): Go `go test/build` with
local module `replace`; Python import; TS local package smoke; Rust `cargo check` with path dep.
- **Dev tests**: unsupported host rejected; args preserved; unique runtime paths; readiness before
child; child gets `PITOT_RUNTIME`; runtime dir removed on exit; decision timeline renders
allow+deny; `--exec` vs `-- CMD ARGS` semantics distinct.

### Step 6 — Package naming truthfulness

- **Python:** rename distribution to `operatorstack-pitot` in
`pitot-distribution/sdk/python/pyproject.toml` **and** generated `requirements.txt`/`pyproject.toml`
(`workbench.go:154,340-349`), keeping import package `pitot`. Generated dep:
`operatorstack-pitot>=0.1.0`.
- **TypeScript:** keep `@operatorstack/pitot`; README install claim only if actually published.
- **Rust:** use `operatorstack-pitot` if `pitot` crate name isn't controlled; lib name may stay `pitot`.
- **Go:** README must label SDK setup source-based/unreleased until `github.com/operatorstack/pitot v0.1.0`
is tagged.
- Until registries/tags exist, README labels every unpublished install as source-based.

### Step 7 — README value translation (`public-readme-preview/README.md`) — **only after Test A passes**

- New opening: `# Pitot` / `## Keep your coding agent. Add the behavior it is missing.` + the
Kimi-first framing. Keep "Pitot reports. Your controller decides." as *supporting* vocabulary.
- First visible section **"See it work with Kimi"** = the tested canary path, before protocol,
adapter matrix, envelope reference, manual runtime, privacy.
- Reader sequence: problem → outcome → two-command path → small Controller → proof → hosts/languages
→ mechanism → guarantees → advanced.
- Remove the `brand-exploration/` gallery link (line 66). Remove the "`pitot dev` configures the
host" claim (line 257). Remove `--exec "<controller cmd>"` examples. Move the big E2E evidence
block below the quickstart.
- Add the truthful **support matrix** (Surface / Implemented / Tested / Published); every "Yes"
backed by CI or a release artifact.

### Step 8 — Projection sync (mandatory, or CI fails)

- After README/SDK/surface edits: `python3 scripts/build_pitot.py --write` to regenerate
`pitot-distribution/UPSTREAM.json`; verify with `--check`. Confirm the gallery link removal and
README rewrite are reflected. Keep public projection and `UPSTREAM.json` synchronized.

---

## Definition of done (from the brief, mapped to steps)

Clean checkout builds the CLI (existing); sample shell Controller runs without a published SDK dep
(Step 1, Go template uses local module) ; config registers Controller for **`shell`** (Step 1);
allow payload → exit 0, deny payload → exit 2, denied side effect never occurs (Step 4A);
`pitot dev --host kimi -- kimi -p ...` runs against a manually configured hook, one allow + one
deny, reason returns to Kimi, canaries correct (Step 4B); README first quickstart is the tested
path, no auto-host-config claim, no `--exec`→Controller, gallery link gone, install claims match
artifacts (Steps 2,6,7); existing adapter/runtime E2E stay green (Step 5); public projection +
`UPSTREAM.json` synchronized (Step 8).

**Excluded (do not build):** browser UI, automatic global host config, more SDK languages,
LLM-based policy generation, marketplace, cloud runtime, general shell-security claims.

## Suggested commit sequence

1. init: add `--template`, shell-policy template, register controller under `shell`
2. init: fix next-step guidance (agent, not controller)
3. doctor: add `--host` checks (PATH/config/hook)
4. test: deterministic Kimi allow/deny adapter+control test (Test A)
5. test: opt-in real Kimi smoke + bounded JSON evidence (Test B)
6. test: workbench init-contract / build / dev hardening
7. packaging: Python `operatorstack-pitot` rename + honest version labels
8. docs: README value translation + support matrix
9. chore: regenerate UPSTREAM.json / projection sync
17 changes: 12 additions & 5 deletions labs/15-pitot/pitot-distribution/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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
### Prove one truthful Kimi controlled-action path

Pitot now ships an end-to-end, locally reproducible proof that a coding agent's shell action can be allowed or denied through a Pitot Controller, and the README leads with it.

- **`pitot init --template shell-policy`**: scaffolds a sample shell Controller in Go, Python, TypeScript, or Rust that requires full content mode and denies only the `PITOT_DENY_ME` canary (a demo tripwire, not a general shell-security control). The generated Rust manifest now declares `serde_json`, so the Rust project compiles offline like the other three.
- **`pitot doctor --host HOST`**: reports whether the host's PreToolUse hook is wired to `pitot hook`, prints the resolved config path, and flags fail-open gaps — the one-time host wiring the README describes.
- **`pitot dev --host kimi -- kimi -p ...`**: launches the real agent behind the runtime and prints an allow/deny decision timeline carrying the Controller's reason.
- **Tests**: a deterministic control-path test drives canonical Kimi allow/deny payloads through the built controller (asserting exit 0/2, canary side effects, and that the deny reason reaches the caller), plus dev end-to-end, multi-language build, and `doctor --host` coverage. An opt-in real-Kimi smoke test (`PITOT_KIMI_SMOKE`) emits a bounded JSON evidence artifact.

The README now opens with "Keep your coding agent. Add the behavior it is missing.", walks through the tested Kimi allow/deny canary, replaces the false "`pitot dev` configures the host for you" claim with truthful one-time hook wiring, and adds a supported-hosts matrix keyed to what is verified in this repo.
8 changes: 7 additions & 1 deletion labs/15-pitot/pitot-distribution/sdk/python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

# The bare name "pitot" is already taken on PyPI by an unrelated aeronautics
# project, so the distribution is published as operatorstack-pitot. The importable
# package stays "pitot" (packages below), so `import pitot` is unchanged.
[project]
name = "pitot"
name = "operatorstack-pitot"
version = "0.1.0"
description = "Pitot: The passive, protocol-first measurement boundary for coding agents."
authors = [
Expand All @@ -13,6 +16,9 @@ license = { text = "MIT" }
requires-python = ">=3.10"
dependencies = []

[tool.setuptools]
packages = ["pitot"]

[project.urls]
Homepage = "https://github.com/operatorstack/pitot"
Repository = "https://github.com/operatorstack/pitot"
Loading
Loading