Guide 03 / Configuration & SDKs
Tenant config & typed SDKs
Several tools sharing one blocking hook is where agent tooling usually turns hostile: last writer wins, and nobody knows who broke it. Pitot makes configuration tenant-partitioned — one fragment per tool, deterministic merge, loud attributable conflicts — and delivers its typed SDKs from your own registry, pinned to the CLI version.
01One fragment per tenant
Every tool or person that registers processes with Pitot owns exactly one file in .pitot/conf.d/, and no tenant ever edits another tenant's file:
.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`
Installing a second Pitot-based tool is additive by construction: it drops its own fragment next to yours, pitot run and pitot dev merge them, and uninstalling it is deleting its fragment.
02Merge rules: deterministic, and loud on conflict
The effective configuration is the deterministic merge of all fragments in filename order. Each fragment is a complete, strictly parsed mini-config declaring controllers: and/or consumers:.
Any number of tenants can observe action.requested events.
Two fragments claiming the same request kind fail discovery with an error naming both files — instead of two tools silently fighting over one blocking hook.
Controller and consumer ids must be unique across fragments; collisions are discovery errors, not overwrites.
03Fragment anatomy
# .pitot/conf.d/my-policy.yaml controllers: shell: id: local-shell-policy command: ["go", "run", "main.go"] dir: "kimi-policy" # per-process cwd, repo-relative deadline_ms: 2000 on_timeout: deny on_unavailable: deny
dir:sets a process's working directory relative to the repository root, so each tenant's command stays project-relative.requires_protocol: "1"optionally pins the protocol version a fragment was written against; a fragment the running binary cannot honor fails discovery — andpitot upgradere-checks this for every fragment before moving the pin.- Deadlines and defaults are declared, not implied. A Controller registration states its
deadline_msand what happens on timeout or unavailability.
04TypeScript SDK
pitot install typescript
writes a scoped .npmrc and installs @operatorstack/pitot@<pinned version>
The package resolves through the distribution front door to our registry — it is never published to public npm. The installed version is locked to the repository's CLI pin, so the SDK and the binary can never drift apart.
npm honors a project registry config only inside a project. When the directory has no package.json, pitot install typescript creates a minimal private one first — without it, npm would silently ignore the scoped .npmrc and fail against public npm with a confusing 404.
05Python SDK
pitot install python
writes .pitot/registry and installs operatorstack-pitot==<pinned version>
The distribution name is operatorstack-pitot (the bare PyPI name was taken), but the import is what you would expect:
import pitot
06No SDK required — the protocol is the contract
The SDKs are optional conveniences, not the source of protocol truth. Compatibility is defined by versioned JSON Schemas, newline-delimited JSON framing, and explicit request/response state machines. If a program can read JSON Lines from standard input, it can be a Consumer:
# ordinary Python — no Pitot SDK import json, 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)
Content projection (full, sha256, or omit) is applied before bytes enter your process, and a Consumer failure can never allow or deny the waiting host action.