pitot

package module
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 29, 2026 License: Apache-2.0 Imports: 0 Imported by: 0

README

Keep your coding agent. Add the behavior it is missing.

Pitot agent E2E

Every supervised adapter must pass a binary-observed prompt → real hook → projected Consumer → Controller allow/deny → tool-result loop on Ubuntu, macOS, and Windows.

Supervised adapters: Claude · Cursor · Codex · GitHub Copilot CLI · Gemini · Kimi Code · OpenCode · Pi · Qwen Code

Supervised runtime capabilities: hook control · Consumer delivery · explicit request

One language-neutral boundary for the coding agents your team already uses.

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:

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):

pitot doctor --host kimi

If the PreToolUse hook is missing, add it to ~/.kimi-code/config.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:

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

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

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:

  1. Maintain separate hooks, payload decoders, response formats, version quirks, and diagnostics for every host.
  2. Own or fork an entire coding-agent runtime just to gain a dependable event boundary.

Pitot provides a third option: keep the coding agents your users already chose, integrate with their host boundary once, and build only the product that differentiates you.

Coding-agent hosts connect through Pitot to passive consumers and one controller per action kind

Pitot separates two capabilities that are easy to blur:

Role Receives Can reply? Typical use
Consumer Projected events No Usage, memory, analytics, reports
Controller Registered requests Yes Approval, verification, policy, workflow

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.

Two small programs

An action auditor consumes events while an approval controller receives a request and returns a response

1. Audit shell requests without recording commands

Configure a Consumer with an omit content projection:

consumers:
  - id: action-audit
    command: ["python3", "./examples/action-audit.py"]
    events: ["action.requested"]
    projection:
      content: omit

Pitot writes newline-delimited JSON to the program's standard input:

{"pitot_version":"1","type":"action.requested","host":{"name":"claude"},"action":{"id":"act_7f2","kind":"shell"},"content":{"mode":"omit"},"observation":{"source":"host_hook","fidelity":"direct"}}

The Consumer is ordinary Python—no Pitot SDK required:

import json
import sys

for line in sys.stdin:
    event = json.loads(line)
    print(json.dumps({
        "host": event["host"]["name"],
        "action_id": event["action"]["id"],
        "kind": event["action"]["kind"],
    }), file=sys.stderr)

The command is projected out before bytes enter the Consumer pipe. Consumer failure cannot allow or deny the waiting host action.

2. Let a skill request approval

A coding-agent skill can make a synchronous request:

pitot request release.approval --data '{"release":"v1.4.0"}' --runtime "$PITOT_RUNTIME"

Register one Controller for that request kind:

controllers:
  release.approval:
    id: local-approval
    command: ["./examples/local-approval"]
    deadline_ms: 2000
    on_timeout: deny
    on_unavailable: deny

The Controller receives:

{"pitot_version":"1","type":"control.requested","kind":"release.approval","action_id":"act_7f2","data":{"release":"v1.4.0"}}

It checks its own source of truth and returns one correlated response:

{"pitot_version":"1","type":"control.response","controller_id":"local-approval","action_id":"act_7f2","outcome":"allow","message":"v1.4.0 is approved for publication."}

Pitot validates the controller identity, action ID, deadline, schema, and single-response rule before carrying the answer back. Pitot does not know what “approved” means; the Controller owns that definition.

Install

Nobody installs Pitot per machine — the repository pins it. pitot init writes two substrate files alongside your tenant fragments:

  • .pitot/version — one committed semver line, the only version authority
  • .pitot/bin/pitot (+ pitot.ps1) — a committed shim that reads the pin, hydrates exactly that release into a per-user cache (~/.cache/pitot/<version>/, sha256-verified against the published checksums.txt), and execs it

Fresh clones, CI, and cloud agents run .pitot/bin/pitot with zero setup — the first invocation hydrates, every later one is a cache hit. There is no fallback to whatever binary happens to be on PATH; a missing release with no network fails closed with a named error, and PITOT_NO_HYDRATE=1 makes hydration cache-only.

Upgrades are a reviewed diff. pitot upgrade verifies the new release, re-checks every tenant fragment against it, and rewrites the one pin line — nothing else. Commit that diff and every clone hydrates the new version on its next invocation; roll back by reverting it.

pitot upgrade --check   # report pinned vs latest
pitot upgrade           # hydrate, validate tenants, rewrite .pitot/version

For a global CLI convenience (running pitot init in new repos), grab a release binary or build from source:

go install github.com/operatorstack/pitot/cmd/pitot@latest

Typed SDKs install from our own registry through the distribution front door — never public npm/PyPI, pinned to the CLI's version:

pitot install typescript   # scoped .npmrc + @operatorstack/pitot@<version>
pitot install python       # .pitot/registry + operatorstack-pitot==<version>

Inspect the effective local boundary, pin, and cache state at any time:

pitot doctor

Quickstart

1. Scaffold a Controller. pitot init writes a runnable project — source and a package manifest — and registers it as one tenant fragment under .pitot/conf.d/. It never overwrites existing files unless you pass --force. Pick a starting template with --template:

pitot init --template shell-policy --language go --dir kimi-policy
Initialized go controller (shell-policy) in kimi-policy
Files written: .pitot/conf.d/kimi-policy.yaml, kimi-policy/go.mod, kimi-policy/main.go
Next:
  1. Configure a supported host hook (see: pitot doctor --host HOST).
  2. Run: pitot dev --host HOST -- AGENT [ARGS...]
     example: pitot dev --host kimi -- kimi -p "<prompt>"

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.

2. Run your agent behind it. pitot dev discovers every fragment under .pitot/conf.d/, starts the runtime and the declared Controllers, 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:

pitot dev --host kimi -- kimi -p "Run: PITOT_DENY_ME=1 echo nope"
Starting Pitot dev environment for host kimi...
Runtime ready. Starting agent: kimi -p Run: PITOT_DENY_ME=1 echo nope
Decisions:
  [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), 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 the same fragment — works with any other supported host whose hook is wired. Change only --host and the agent command:

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.

Multiple tools, one Pitot

Configuration is tenant-partitioned: every tool or person that registers processes with Pitot owns exactly one fragment in .pitot/conf.d/, and no tenant ever edits another tenant's file. The effective configuration is the deterministic merge of all fragments in filename order:

.pitot/
  conf.d/
    boatstack.yaml    # a tool's controller, written by its installer
    interlock.yaml    # another tool's controller, different request kind
    my-policy.yaml    # your own, scaffolded by `pitot init`

Each fragment is a complete, strictly parsed mini-config declaring controllers: and/or consumers:. Merge rules:

  • Consumers always compose. Any number of tenants can observe action.requested events.
  • A request kind has one owner. Two fragments claiming the same kind (for example shell) fail discovery with an error naming both files — a loud, attributable conflict instead of two tools silently fighting over one blocking hook. Controller and consumer ids must also be unique across fragments.
  • requires_protocol: "1" optionally pins the protocol version a fragment was written against; a fragment this binary cannot honor fails discovery.
  • dir: sets a process's working directory (relative to the repository root), so each tenant's command stays project-relative:
controllers:
  shell:
    id: local-shell-policy
    command: ["go", "run", "main.go"]
    dir: "kimi-policy"
    deadline_ms: 2000
    on_timeout: deny
    on_unavailable: deny

Installing a second Pitot-based tool is therefore additive by construction: it drops its own fragment next to yours, pitot run/pitot dev merge them, and uninstalling it is deleting its fragment.

Advanced: manual runtime

pitot dev is the recommended path. If you need to manage the runtime yourself (for example, sharing one runtime across several long-lived agent sessions), start it with repository-owned configuration and an owner-only runtime descriptor:

export PITOT_RUNTIME="${XDG_RUNTIME_DIR:-$TMPDIR}/pitot/project.json"
pitot run --runtime "$PITOT_RUNTIME"

With no --config, pitot run discovers and merges the repository's .pitot/conf.d/ fragments. Pass --config PATH to override discovery with one explicit file (useful for tests and ad-hoc runtimes).

Start coding-agent CLIs from the same environment. Their pitot hook HOST commands discover the authenticated runtime through PITOT_RUNTIME. Without that variable or --runtime PATH, hooks remain observation-only for backwards compatibility. Once a runtime is explicitly selected, transport or authentication failure blocks the controllable action.

On Windows, set the descriptor in the launching PowerShell session:

$env:PITOT_RUNTIME = Join-Path $env:LOCALAPPDATA "Pitot\project.json"
pitot run --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. 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

Install Kimi Code on macOS or Linux using its official installer:

curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash
kimi --version

On Windows, use the official PowerShell installer:

irm https://code.kimi.com/kimi-code/install.ps1 | iex
kimi --version

Connect Kimi Code's blocking shell boundary to Pitot in ~/.kimi-code/config.toml:

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

Kimi sends the hook payload to Pitot on standard input. Pitot exits 0 when the request is accepted and 2 when input is malformed or the configured Controller denies the action. Check the non-interactive Kimi CLI after configuration with:

kimi -p "Show the repository status"

See the Kimi Code documentation for CLI authentication, configuration, and hook behavior.

GitHub Copilot CLI

Copy integrations/copilot/PreToolUse to a stable executable path (or use PreToolUse.ps1 on Windows), then add the following Claude-compatible hook to ~/.copilot/settings.json:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{"type": "command", "command": "/path/to/PreToolUse"}]
    }]
  }
}

The bridge keeps the blocking payload on Pitot's standard hook_event_name and tool_input.command boundary and returns Copilot's structured native deny reason when a Controller rejects the command. See the official Copilot CLI hooks reference.

Cursor

Copy integrations/cursor/beforeShellExecution into the repository and point .cursor/hooks.json at it with failClosed: true. The bridge returns Cursor's native permission: "deny" envelope, including the Controller message, while the runtime remains available through PITOT_RUNTIME. See Cursor's hooks documentation.

Gemini

Copy integrations/gemini/BeforeTool to an executable path (or use BeforeTool.ps1 on Windows) and register it as a BeforeTool command hook for run_shell_command. The bridge translates Pitot rejection into Gemini's structured decision: "deny" and reason response so the model receives the blocked tool result. See the Gemini CLI hooks reference.

Qwen Code

Copy integrations/qwen/PreToolUse to a stable executable path (or use the Node-based PreToolUse.cjs bridge on Windows), then add this command hook to ~/.qwen/settings.json:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "^Bash$",
      "hooks": [{"type": "command", "command": "/path/to/PreToolUse"}]
    }]
  }
}

Qwen sends the native JSON payload on standard input. The bridge returns its native structured allow or deny decision and preserves the Controller reason. See the official Qwen Code hooks guide.

Pi

Copy integrations/pi/pitot.ts into ~/.pi/agent/extensions/pitot.ts (or the repository-local .pi/extensions/ directory). The extension converts Pi's blocking tool_call event into Pitot's stable envelope and returns Pi's native block response when Pitot rejects the request. See the official Pi extensions documentation.

Pitot uses supervised local processes in v1. It starts declared Consumers and Controllers itself, applies each projection before bytes enter the child pipe, and exposes only a loopback endpoint authenticated by the owner-only runtime descriptor. The capability token is never passed to child processes or logs.

Language-neutral by design

Pitot's reference implementation is Go. Its public contract is not.

Compatibility is defined by:

  • versioned JSON Schemas;
  • newline-delimited JSON framing;
  • explicit request/response state machines;
  • capability and projection declarations; and
  • language-neutral conformance fixtures.

If a program can read JSON Lines from standard input, it can be a Consumer. If it can return one schema-valid response on its dedicated output channel, it can be a Controller.

Official client libraries are optional conveniences—not a prerequisite and not the source of protocol truth.

Event envelope

Every event identifies its schema, source, session, and observation quality:

{
  "pitot_version": "1",
  "id": "evt_01J...",
  "type": "action.requested",
  "time": "2026-07-19T16:05:00Z",
  "host": {
    "name": "cursor",
    "adapter_version": "1.0.0"
  },
  "session_id": "sess_42",
  "action": {
    "id": "act_7f2",
    "kind": "shell"
  },
  "content": {
    "mode": "sha256",
    "sha256": "9f2..."
  },
  "observation": {
    "source": "host_hook",
    "fidelity": "direct"
  }
}

Adapters preserve host capability differences. A normalized field is never presented as directly observed when the host omitted it or Pitot inferred it.

Controller guarantees

Synchronous hooks are control channels: the host is blocked waiting for an answer. Pitot makes that privilege explicit.

  • Exactly one Controller may register for a request kind.
  • Registration is static, auditable configuration.
  • The registration declares a deadline and unavailable/timeout defaults.
  • Every response is bound to its pending action and Controller identity.
  • Late, stale, duplicate, mismatched, and malformed responses are rejected.
  • A pending action receives exactly one terminal resolution.
  • Registration and its configuration fingerprint appear in diagnostics.

The bridge mechanically enforces the declared default. It contains no approval, safety, completion, or shipping policy engine.

Boundary faults

Pitot distinguishes a broken measurement boundary from a judgment about the work:

{
  "pitot_version": "1",
  "type": "boundary.fault",
  "host": "cursor",
  "action_id": "act_7f2",
  "reason": "empty-command"
}

Reason codes never contain prompts, commands, tool inputs, or outputs. A Controller may choose to deny, retry, report, or escalate the fault according to its own policy.

Privacy model

Pitot is local and storage-free by default.

  • Raw host payloads terminate inside the adapter.
  • Content projection is full, sha256, or omit per Consumer.
  • Projection happens before delivery, not inside downstream applications.
  • No network exporter is enabled implicitly.
  • Pitot does not retain an event history unless a configured Consumer does.
  • Passive Consumers receive no Controller capability.
  • Diagnostics and fault codes are content-safe.

What can you build?

  • selective cross-agent memory;
  • session and decision reports;
  • token and cost attribution;
  • reliability and host-compatibility diagnostics;
  • OpenTelemetry exporters;
  • human approval routers;
  • security or compliance Controllers;
  • delivery verifiers;
  • custom agent interfaces over existing runtimes; and
  • new Pitot-compatible coding-agent runtimes.

What Pitot does not decide

Pitot does not define whether:

  • work is correct or complete;
  • an action is safe;
  • a person granted approval;
  • evidence satisfies a requirement;
  • a claim is valid; or
  • something may be shipped.

Those meanings belong to your Controller. Pitot reports. Your controller decides.

Project layout

pitot/
├── schema/          public event and response schemas
├── protocol/        framing and state-machine specifications
├── adapters/        built-in coding-agent hook boundaries
├── sensor/          normalization and observation pipeline
├── bridge/          controller routing and response transport
├── projection/      full, sha256, and omit policies
├── config/          strict Consumer and Controller declarations
├── runtime/         authenticated ingress and local process delivery
├── conformance/     language-neutral fixtures and negative controls
├── examples/        local approval and Consumer examples
└── cmd/pitot/        reference Go executable

Design principles

  1. Report before interpretation. Preserve what the host actually supplied.
  2. Make capability structural. Consumers cannot reply; Controllers can.
  3. Keep host churn together. Decoders and encoders share one compatibility boundary.
  4. Project before delivery. Privacy is enforced before content crosses the process boundary.
  5. Correlate every answer. A response can resolve only its pending action.
  6. Prefer one maintained implementation. Use a language-neutral protocol instead of rewriting the host boundary in every ecosystem.
  7. Extend standards where they fit. Map to OpenTelemetry GenAI conventions without erasing Pitot-specific provenance or semantic events.

Name

A pitot probe measures pressure difference so another system can determine airspeed. It does not fly the aircraft.

Pitot applies the same separation to coding-agent tooling: measurement belongs at the host boundary; interpretation and control belong downstream.

Contributing

Start with the protocol and conformance fixtures. A new adapter should declare its host capabilities, normalize supported events, classify boundary faults without exposing content, encode Controller responses, and pass the shared positive and negative fixture suite.

See CONTRIBUTING.md for development setup and compatibility requirements.

License

Apache-2.0

Documentation

Overview

Package pitot is the module root for the Pitot sensor and control transport.

Pitot converts host-specific coding-agent activity into a stable, language neutral event stream and carries correlated responses from a statically registered controller when a host is waiting synchronously. It reports what happened; the controller decides what it means.

Module layout (see the public README for the full contract):

schema/       public event and response types + versioned constants
protocol/     newline-delimited JSON framing and state-machine helpers
adapters/     supervised coding-agent host boundaries and payload normalization
sensor/       normalization and observation pipeline (decoder)
bridge/       controller routing and single-response transport
projection/   full, sha256, and omit content policies
conformance/  language-neutral fixtures and negative controls
examples/     token-meter and local-approval reference programs
cmd/pitot/    reference Go executable

Architectural invariant: sensor/ must never import bridge/. Measurement is innocent of control. The invariant is enforced at build time by scripts/check_import_boundary.sh.

Directories

Path Synopsis
Package adapters declares the coding-agent host boundaries Pitot supports and the host-specific hook shapes it understands.
Package adapters declares the coding-agent host boundaries Pitot supports and the host-specific hook shapes it understands.
Package bridge routes synchronous control requests to a statically registered Controller and carries back exactly one correlated response.
Package bridge routes synchronous control requests to a statically registered Controller and carries back exactly one correlated response.
cmd
generate-schema command
pitot command
Install wires a project to the typed Pitot SDKs from our own registry, through the distribution front door — never public npm/PyPI (packages-come-from-our-registry-or-nowhere).
Install wires a project to the typed Pitot SDKs from our own registry, through the distribution front door — never public npm/PyPI (packages-come-from-our-registry-or-nowhere).
Package config defines and validates Pitot's repository-owned runtime configuration.
Package config defines and validates Pitot's repository-owned runtime configuration.
Package conformance holds Pitot's language-neutral fixture corpus and the loader that drives it.
Package conformance holds Pitot's language-neutral fixture corpus and the loader that drives it.
Package examples documents the two reference programs that demonstrate the Consumer/Controller distinction, mirroring the public README:
Package examples documents the two reference programs that demonstrate the Consumer/Controller distinction, mirroring the public README:
local-approval command
Command local-approval is a Controller: it receives a correlated control.requested record and returns exactly one control.response on standard output.
Command local-approval is a Controller: it receives a correlated control.requested record and returns exactly one control.response on standard output.
token-meter command
Command token-meter is a passive Consumer: it sums token usage from the Pitot event stream without ever recording prompts.
Command token-meter is a passive Consumer: it sums token usage from the Pitot event stream without ever recording prompts.
Package hydrate makes the Pitot binary a repository property: a committed pin (.pitot/version) names the exact version, and hydration materializes it into a write-once user cache from the distribution front door.
Package hydrate makes the Pitot binary a repository property: a committed pin (.pitot/version) names the exact version, and hydration materializes it into a write-once user cache from the distribution front door.
internal
testrole command
Command testrole is a language-neutral-process fixture for Pitot runtime tests.
Command testrole is a language-neutral-process fixture for Pitot runtime tests.
Package projection applies a Consumer's content policy before bytes cross the process boundary.
Package projection applies a Consumer's content policy before bytes cross the process boundary.
Package protocol defines Pitot's wire framing and request/response state machine helpers.
Package protocol defines Pitot's wire framing and request/response state machine helpers.
Package runtime owns Pitot's local child-process delivery boundary.
Package runtime owns Pitot's local child-process delivery boundary.
Package schema defines Pitot's public event, control, and fault envelopes.
Package schema defines Pitot's public event, control, and fault envelopes.
Package sensor normalizes raw host hook payloads into Pitot's event envelope.
Package sensor normalizes raw host hook payloads into Pitot's event envelope.
tests
witness command
Command pitot-witness is a transparent executable wrapper used only by the real-agent E2E. It proves that the host invoked Pitot without changing the bytes or exit status observed by either process.
Command pitot-witness is a transparent executable wrapper used only by the real-agent E2E. It proves that the host invoked Pitot without changing the bytes or exit status observed by either process.
Package windtunnel holds Pitot's integrated HEAD-to-HEAD check, which drives a host hook event through the sensor and a control request through the bridge to prove the two planes agree at workspace HEAD.
Package windtunnel holds Pitot's integrated HEAD-to-HEAD check, which drives a host hook event through the sensor and a control request through the bridge to prove the two planes agree at workspace HEAD.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL