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
34 changes: 34 additions & 0 deletions .github/workflows/pitot-lab.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,37 @@ jobs:
run: python labs/15-pitot/scripts/pitot_adapter_supervisor.py check
- name: Verify projection and public surface
run: python -m unittest discover -s labs/15-pitot/tests -p 'test_*.py'

sdk-conformance:
name: SDK cross-language conformance
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# The shared conformance suite drives Python, TypeScript, Go, and Rust
# runners through the SAME fixture cases so their wire behavior stays
# identical. It lives under pitot-distribution/sdk/tests/ (beside the SDKs
# it exercises), OUTSIDE the labs/15-pitot/tests/ discovery root — so until
# this job existed the parity guarantee was written but never enforced. Each
# language skips cleanly when its toolchain is absent; here we install all
# four so nothing skips silently in CI.
- uses: ./.github/actions/go-setup
with:
go-version-file: labs/15-pitot/pitot/go.mod
cache-dependency-path: labs/15-pitot/pitot/go.sum
- uses: actions/setup-python@v6
with:
python-version: "3.11"
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install tsx (TypeScript runner)
run: npm install -g tsx
- uses: dtolnay/rust-toolchain@stable
- name: Rust SDK builds and tests in isolation
run: cargo test --manifest-path labs/15-pitot/pitot-distribution/sdk/rust/Cargo.toml
- name: Cross-language runner conformance (Python, TypeScript, Go, Rust)
env:
GOWORK: "off"
run: >-
python -m unittest discover
-s labs/15-pitot/pitot-distribution/sdk/tests -p 'test_*.py' -v
10 changes: 8 additions & 2 deletions labs/15-pitot/pitot-distribution/UPSTREAM.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
"runtime/descriptor_unix.go": "df41b6867e9840933f186c93b6bc61861e7ea5252a365455686ea0414bfa0044",
"runtime/descriptor_windows.go": "2d9ffefe3af0154fa8042de6b67460d4e86dd3f4cdd9e986f180f7d0c535c9a5",
"runtime/request.go": "198c44fd6c547022a15b6d0d48e4d0130fa8afb687994365115576e4d874550d",
"runtime/request_test.go": "86d8a2feb4ec72e8ed675b9567da2d1f5d628950eeec907c10b9cc1675aa1904",
"runtime/request_test.go": "7e1cafab3de6432af4ab60999c83e8e81c1a7e7c985e256e56cec2f24ce154cb",
"runtime/runtime.go": "36fdf4643c04d058fef380333a0a3942913d1dd60e910b893dc134da4b65d6bf",
"runtime/runtime_test.go": "afd78d122af20148bf30d0db873ff002544189df0dfec0f6167b8cf5cd0d42b1",
"runtime/strict_test.go": "d2eb2b78563733e3279289d8b6d17a4276e831ed3c78f8b3ec9d6706992ffaec",
Expand All @@ -91,6 +91,8 @@
"runtime/transport_test.go": "9b69f590f1e258470adea249b3ac6d4a00f1001f10bb08dfa7b56c6e2d6709ae",
"schema/schema.go": "fd5c3b76979c88aeed75fadb7e3c94abcad62e067d77595f021fb422a60f2211",
"sdk/csharp/Pitot/Types.cs": "3fecc1eabca6e3ad218c73741d185f815d868a618d1e7ed3b9320eeb6942977d",
"sdk/csharp/README.md": "dce102e6176e1a0ee8858fcd784bd9420a9603fb7f2079445e88f4b1e78df45d",
"sdk/java/README.md": "a5c3a6b350098473d7513126db2accd5b790128f6dcf5454d0547cc8284621ce",
"sdk/java/src/main/java/com/operatorstack/pitot/Action.java": "2d7974afc2f687921c5e8009077f7b812a8973cdb174de5020cb73f41e3c6570",
"sdk/java/src/main/java/com/operatorstack/pitot/BoundaryFault.java": "926c93d9a77b8ef1d94f4674e73e1b8c28aa19082e8f0261077be95ea975abb8",
"sdk/java/src/main/java/com/operatorstack/pitot/Content.java": "a430934623b0edfdc3a96f8b17da3deb1502b29e534c18fb580b4cd672b60f71",
Expand All @@ -102,16 +104,20 @@
"sdk/java/src/main/java/com/operatorstack/pitot/Observation.java": "179982beed89b31cc0a8218c46d8f1bfe6d9c7b1ecb7da82fbc26924d5051df7",
"sdk/java/src/main/java/com/operatorstack/pitot/Types.java": "26f8722e6b61faaa093e5a8b92388d3f93a2228026dc04b4b68d10340874a135",
"sdk/java/src/main/java/com/operatorstack/pitot/Usage.java": "bbddf4cb0fb7550ac14817600d9f233e68bfd84b72f73d5fbbf9a6cce6e3253d",
"sdk/python/README.md": "c49c15c48bbc064a4596815a94710c9f5270b38216c6b796cbd6fb38cf90fa34",
"sdk/python/pitot/__init__.py": "9cab11b333536f167d4e7bb6089ef00488690ec7b5cd5701f08e8bbb13cef0f7",
"sdk/python/pitot/runner.py": "8d4537ffc3aee2ea22b1d691559ac1eb1e95521df9ad5bf241ee6ac54c9925b1",
"sdk/python/pitot/types.py": "c9a7221f1ad6627f152f26148155d3c74f249c52262c73a515251f469e8eb4ad",
"sdk/python/pyproject.toml": "b7990a7a4d2a3db874a5f8c6f9e09673bf928936b64cf0bd4d4a1727aae3305d",
"sdk/runner.go": "e9a8db96d3cf6df7ea7e661e6755650f97e580970995dfe881b4268db0c3f832",
"sdk/rust/.gitignore": "6c135f35993002b30f7bdae3d88e923bbf33e4a25faa35dde452f6ab6fd4a6b2",
"sdk/rust/Cargo.toml": "1c42ed7ff351ad54ab87c29bbbef96a7bd574b4109ca12eb84d3f8daf42dcc77",
"sdk/rust/README.md": "a548821ca40287e4d17ffb12cd844f68ffc5d0f4358d3459f7bdafc8a55a0b2e",
"sdk/rust/src/lib.rs": "fd2dcb9bf9df47fb58e52e4e94136f95867616d941ba66b4b324413d1c5b1777",
"sdk/rust/src/runner.rs": "168eb3f926aed4b86aa27ad66ab605326974b6a345b07a35126d4b66e0968b33",
"sdk/rust/src/types.rs": "82cd5a5ec12effa6688e8b9b6028bbef67ec43f24d9fd9d19b4235934061ffb8",
"sdk/tests/test_runners.py": "9e86a03b53b108b70204e797b4c65b3d157c6765543a9bb9d7bb892daaf4d8c4",
"sdk/tests/test_runners.py": "28af8d9c235afe9c61e1d6e5e9b70ffe7054e36ec04310daaba4fd7488bda0e0",
"sdk/typescript/README.md": "5fc46f917ef30e944e1b20efae04d0a53ccc60fb868d143e44b04bcad684a51c",
"sdk/typescript/package.json": "747b5bab8df073d39b9ef92f28b6b7b43eb00bc127c0647cda4e4003b3db7295",
"sdk/typescript/src/index.ts": "bddb336bc120ecd979de0288e432ed2b41749d39f11c697f019af5e54c2e9fba",
"sdk/typescript/src/pitot.ts": "9c243824cbb7edc54b1e125abfd828bf2ed77e7151a0bd5c5d4f63e81ca9b00c",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
### The SDKs are ready for outside builders to start with

Building on Pitot means writing a Controller or Consumer in one of the language
bindings, so the bindings had to become something an outsider can pick up and
trust — not just code that happens to compile. This release closes the gaps
between "the SDK exists" and "the SDK is safe to build on".

The cross-language conformance suite is now CI-enforced. It drove Python,
TypeScript, Go, and Rust runners through the same fixture cases (controller
stream order, malformed-line skip, deny, consumer stdout-cleanliness) but ran
outside the discovered test path, so nothing gated on it. It is now a dedicated
`sdk-conformance` job in the Pitot workflow, and the Rust SDK is built and
tested in isolation there for the first time. Enabling it immediately caught a
real defect the gap had hidden: the Rust consumer fixture referenced a struct
field (`type_field`) that does not exist — the wire field is `event_type`
(`#[serde(rename = "type")]`). An unenforced guarantee is a silent-degradation
risk; this one is now enforced.

Java and C# are honestly labeled. They are generated types only — no runner, no
build manifest, no conformance entry — and each directory now carries a README
saying so and pointing builders at the four first-class SDKs (Python,
TypeScript, Go, Rust). A new lockstep-gate test fails the moment either preview
gains a build manifest without also being version-locked and conformance-tested,
so a preview can never quietly start shipping.

The version-lockstep gate now documents and covers the Go binding (its version is
the scaffold constant `pitotPackageVersion`, the one Go-side string that can
drift). Each shipping SDK gained a concise quickstart README — build your first
Controller and Consumer, install through the CLI front door, run it behind an
agent. Build-artifact churn is closed too: the projection builder skips Rust
`target/` and `Cargo.lock`, with a matching `.gitignore`, so a local `cargo
build` can never perturb the projected public surface.
21 changes: 21 additions & 0 deletions labs/15-pitot/pitot-distribution/sdk/csharp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Pitot C# SDK — preview (not yet runnable)

**Status: types-only preview. Not a shipping SDK.** `Pitot/Types.cs` holds the
generated C# data types for the Pitot wire protocol, emitted by
[`scripts/generate_types.sh`](../../../scripts/generate_types.sh) from the same
schema as every other binding. They let you *read the shapes* — they do **not** yet
give you a runnable Controller or Consumer.

What is **not** here yet, and what a first-class SDK requires:

- no build manifest (`.csproj` / solution), so the package does not build;
- no `runner` (the stdio JSON-Lines Controller/Consumer loop the other SDKs ship);
- no entry in the cross-language conformance suite
([`../tests/test_runners.py`](../tests/test_runners.py)), which is what proves a
binding's wire behavior is identical to the others;
- not covered by the version-lockstep gate — it carries no release version.

The first-class, runnable SDKs are **Python**, **TypeScript**, **Go**, and
**Rust**. Use one of those to build against Pitot today; see each SDK's `README.md`
for a quickstart. This preview will graduate only once it clears the same bar: a
runner, a conformance entry, a build, and version lockstep.
21 changes: 21 additions & 0 deletions labs/15-pitot/pitot-distribution/sdk/java/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Pitot Java SDK — preview (not yet runnable)

**Status: types-only preview. Not a shipping SDK.** These files are the generated
Java data types for the Pitot wire protocol (`com.operatorstack.pitot.*`), emitted
by [`scripts/generate_types.sh`](../../../scripts/generate_types.sh) from the same
schema as every other binding. They let you *read the shapes* — they do **not** yet
give you a runnable Controller or Consumer.

What is **not** here yet, and what a first-class SDK requires:

- no build manifest (`pom.xml` / `build.gradle`), so the package does not build;
- no `runner` (the stdio JSON-Lines Controller/Consumer loop the other SDKs ship);
- no entry in the cross-language conformance suite
([`../tests/test_runners.py`](../tests/test_runners.py)), which is what proves a
binding's wire behavior is identical to the others;
- not covered by the version-lockstep gate — it carries no release version.

The first-class, runnable SDKs are **Python**, **TypeScript**, **Go**, and
**Rust**. Use one of those to build against Pitot today; see each SDK's `README.md`
for a quickstart. This preview will graduate only once it clears the same bar: a
runner, a conformance entry, a build, and version lockstep.
65 changes: 65 additions & 0 deletions labs/15-pitot/pitot-distribution/sdk/python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Pitot Python SDK

The Python binding for Pitot: typed wire types plus a tiny stdio runner for
building a **Controller** (decides `control.requested` → allow/deny) or a passive
**Consumer** (reads the observed action/event stream). It speaks the same
JSON-Lines protocol as every other Pitot SDK — proven identical by the
cross-language conformance suite ([`../tests/test_runners.py`](../tests/test_runners.py)).

## Install

Pitot SDKs install from the project's own registry through the CLI front door,
pinned to the CLI's version — never public PyPI:

```bash
pitot install python # writes .pitot/registry + operatorstack-pitot==<version>
```

`pitot init --language python` scaffolds a complete, runnable project and registers
it as a tenant fragment; the snippets below are the core of what it writes.

## Build a Controller

A Controller receives a typed `ControlRequested` and returns an `Outcome`. Return
`allow(...)` or `deny(...)`; the runner serializes one `control.response` per line,
in request order.

```python
from pitot.runner import run_controller, allow, deny
from pitot.types import ControlRequested

def handler(req: ControlRequested):
# decide from req.data, req.kind, etc.
if req.kind == "shell" and "rm -rf" in str(req.data):
return deny("blocked a destructive shell command")
return allow("approved")

if __name__ == "__main__":
run_controller("my-controller", handler)
```

## Build a Consumer

A Consumer passively reads the event stream. Keep stdout clean — a Consumer must
never write to stdout (the conformance suite enforces this); log to stderr.

```python
import sys
from pitot.runner import run_consumer
from pitot.types import Event

def handler(event: Event):
print("observed", event.type, file=sys.stderr)

if __name__ == "__main__":
run_consumer(handler)
```

## Run it behind an agent

```bash
pitot dev --host claude -- claude -p "…" # discovers your fragment, starts the controller
```

See the repository [README](../../../public-readme-preview/README.md) for the full
runtime, host-hook wiring, and the enforcement boundary.
5 changes: 5 additions & 0 deletions labs/15-pitot/pitot-distribution/sdk/rust/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Rust build artifacts — the crate is a library; a resolved lockfile and the
# compiled target/ tree are produced by `cargo build`/`cargo test` (locally and
# in the conformance CI job) and are never part of the published SDK.
/target
Cargo.lock
60 changes: 60 additions & 0 deletions labs/15-pitot/pitot-distribution/sdk/rust/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Pitot Rust SDK

The Rust binding for Pitot: typed wire types plus a tiny stdio runner for building a
**Controller** (decides `control.requested` → allow/deny) or a passive **Consumer**
(reads the observed action/event stream). It speaks the same JSON-Lines protocol as
every other Pitot SDK — proven identical by the cross-language conformance suite
([`../tests/test_runners.py`](../tests/test_runners.py)).

## Add the dependency

The crate is `pitot`. During local development, depend on it by path; a released
version installs from the project registry pinned to the CLI's version.

```toml
[dependencies]
pitot = { path = "path/to/sdk/rust" } # or the pinned registry version
```

## Build a Controller

A Controller receives a typed `ControlRequested` and returns an `Outcome`. Return
`allow(...)` or `deny(...)`; the runner serializes one `control.response` per line,
in request order.

```rust
use pitot::{run_controller, allow, deny, ControlRequested, Outcome};

fn handler(req: ControlRequested) -> Outcome {
if req.kind == "shell" {
return deny(Some("blocked a shell command".to_string()));
}
allow(Some("approved".to_string()))
}

fn main() {
run_controller("my-controller", Box::new(handler));
}
```

## Build a Consumer

A Consumer passively reads the event stream. Keep stdout clean — a Consumer must
never write to stdout (the conformance suite enforces this); log to stderr. Note the
event's protocol `type` field is `event_type` in Rust (it is `#[serde(rename =
"type")]`, since `type` is a reserved word).

```rust
use pitot::{run_consumer, Event};

fn handler(event: Event) {
eprintln!("observed {}", event.event_type);
}

fn main() {
run_consumer(Box::new(handler));
}
```

See the repository [README](../../../public-readme-preview/README.md) for the full
runtime, host-hook wiring, and the enforcement boundary.
2 changes: 1 addition & 1 deletion labs/15-pitot/pitot-distribution/sdk/tests/test_runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def handler(event: Event):
RUST_CONSUMER = """use pitot::{run_consumer, Event};

fn handler(event: Event) {
eprintln!("consumed {}", event.type_field);
eprintln!("consumed {}", event.event_type);
}

fn main() {
Expand Down
59 changes: 59 additions & 0 deletions labs/15-pitot/pitot-distribution/sdk/typescript/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Pitot TypeScript SDK

The TypeScript binding for Pitot: typed wire types plus a tiny stdio runner for
building a **Controller** (decides `control.requested` → allow/deny) or a passive
**Consumer** (reads the observed action/event stream). It speaks the same
JSON-Lines protocol as every other Pitot SDK — proven identical by the
cross-language conformance suite ([`../tests/test_runners.py`](../tests/test_runners.py)).

## Install

Pitot SDKs install from the project's own registry through the CLI front door,
pinned to the CLI's version — never public npm:

```bash
pitot install typescript # scoped .npmrc + @operatorstack/pitot@<version>
```

`pitot init --language typescript` scaffolds a complete, runnable project and
registers it as a tenant fragment; the snippets below are the core of what it
writes.

## Build a Controller

A Controller receives a typed `ControlRequested` and returns an `Outcome`. Return
`allow(...)` or `deny(...)`; the runner serializes one `control.response` per line,
in request order.

```typescript
import { runController, allow, deny, ControlRequested } from "@operatorstack/pitot";

runController("my-controller", async (req: ControlRequested) => {
if (req.kind === "shell" && String(req.data).includes("rm -rf")) {
return deny("blocked a destructive shell command");
}
return allow("approved");
});
```

## Build a Consumer

A Consumer passively reads the event stream. Keep stdout clean — a Consumer must
never write to stdout (the conformance suite enforces this); log to stderr.

```typescript
import { runConsumer, Event } from "@operatorstack/pitot";

runConsumer(async (event: Event) => {
console.error("observed", event.type);
});
```

## Run it behind an agent

```bash
pitot dev --host claude -- claude -p "…" # discovers your fragment, starts the controller
```

See the repository [README](../../../public-readme-preview/README.md) for the full
runtime, host-hook wiring, and the enforcement boundary.
6 changes: 3 additions & 3 deletions labs/15-pitot/pitot/runtime/request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func TestNewControlRequestFillsEnvelopeAndMarshalsPayload(t *testing.T) {
Actor string `json:"actor"`
Operation string `json:"operation"`
}
req, err := NewControlRequest("interlock.decide", body{Actor: "publisher", Operation: "artifact.publish"})
req, err := NewControlRequest("interlock.effect", body{Actor: "publisher", Operation: "artifact.publish"})
if err != nil {
t.Fatalf("NewControlRequest: %v", err)
}
Expand All @@ -22,8 +22,8 @@ func TestNewControlRequestFillsEnvelopeAndMarshalsPayload(t *testing.T) {
if req.Type != schema.TypeControlRequested {
t.Errorf("type = %q, want %q", req.Type, schema.TypeControlRequested)
}
if req.Kind != "interlock.decide" {
t.Errorf("kind = %q, want interlock.decide", req.Kind)
if req.Kind != "interlock.effect" {
t.Errorf("kind = %q, want interlock.effect", req.Kind)
}
if req.ActionID == "" {
t.Error("ActionID was not minted")
Expand Down
6 changes: 6 additions & 0 deletions labs/15-pitot/scripts/build_pitot.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,14 @@ def _iter_sdk_files(repo: Path) -> list[tuple[str, Path]]:
if sdk_dir.is_dir():
for path in sorted(sdk_dir.rglob("*")):
if path.is_file() and not path.is_symlink():
# Never project a build artifact: Python bytecode caches, or a
# Rust `target/` tree and its lockfile left by a local/CI cargo
# build of the crate. These are not part of the published SDK and
# would otherwise churn the manifest on any machine that built it.
if "__pycache__" in path.parts or path.suffix == ".pyc":
continue
if "target" in path.parts or path.name == "Cargo.lock":
continue
projected = path.relative_to(repo / LAB / "pitot-distribution").as_posix()
entries.append((projected, path))
return entries
Expand Down
Loading
Loading