From 545ac2c341be3f70127f08d8d7267c009b6ed5de Mon Sep 17 00:00:00 2001 From: bigboateng Date: Fri, 31 Jul 2026 17:29:14 +0100 Subject: [PATCH] feat(pitot): make the SDKs ready for outside builders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Building on Pitot means writing a Controller or Consumer in a language binding, so the bindings had to become something an outsider can pick up and trust. This closes the gaps between "the SDK exists" and "the SDK is safe to build on" — all feature-preserving. - CI-enforce the cross-language conformance suite (Python/TS/Go/Rust) as a dedicated sdk-conformance job; it lived outside the discovered test path, so the parity guarantee was written but never gated. Enabling it caught a real defect it had hidden: the Rust consumer fixture used a field (type_field) that does not exist — the wire field is event_type. - Build and test the Rust SDK in isolation in that job (first time in CI). - Label Java and C# as types-only previews (README in each dir) and add a lockstep-gate test that fails the moment a preview gains a build manifest without being version-locked and conformance-tested. - Document and cover the Go binding version (scaffold constant pitotPackageVersion) in the version-lockstep gate. - Add per-SDK quickstart READMEs (TypeScript, Python, Rust): build your first Controller and Consumer, install via the CLI front door. - Stop Rust build-artifact churn: build_pitot skips target/ and Cargo.lock, with a matching sdk/rust/.gitignore, so a local cargo build can never perturb the projected public surface. - Align a stale sample kind in request_test.go (interlock.decide -> interlock.effect) with the now-canonical request kind; assertions unchanged. Disclosure-Reviewed: reviewed — public-safe only, private facet kept out of this commit --- .github/workflows/pitot-lab.yml | 34 ++++++++++ .../15-pitot/pitot-distribution/UPSTREAM.json | 10 ++- ...-31-sdk-readiness-for-external-builders.md | 32 +++++++++ .../pitot-distribution/sdk/csharp/README.md | 21 ++++++ .../pitot-distribution/sdk/java/README.md | 21 ++++++ .../pitot-distribution/sdk/python/README.md | 65 +++++++++++++++++++ .../pitot-distribution/sdk/rust/.gitignore | 5 ++ .../pitot-distribution/sdk/rust/README.md | 60 +++++++++++++++++ .../sdk/tests/test_runners.py | 2 +- .../sdk/typescript/README.md | 59 +++++++++++++++++ labs/15-pitot/pitot/runtime/request_test.go | 6 +- labs/15-pitot/scripts/build_pitot.py | 6 ++ labs/15-pitot/tests/test_sdk_versions.py | 43 +++++++++++- 13 files changed, 357 insertions(+), 7 deletions(-) create mode 100644 labs/15-pitot/pitot-distribution/release-notes/2026-07-31-sdk-readiness-for-external-builders.md create mode 100644 labs/15-pitot/pitot-distribution/sdk/csharp/README.md create mode 100644 labs/15-pitot/pitot-distribution/sdk/java/README.md create mode 100644 labs/15-pitot/pitot-distribution/sdk/python/README.md create mode 100644 labs/15-pitot/pitot-distribution/sdk/rust/.gitignore create mode 100644 labs/15-pitot/pitot-distribution/sdk/rust/README.md create mode 100644 labs/15-pitot/pitot-distribution/sdk/typescript/README.md diff --git a/.github/workflows/pitot-lab.yml b/.github/workflows/pitot-lab.yml index 0523372da..4179648c1 100644 --- a/.github/workflows/pitot-lab.yml +++ b/.github/workflows/pitot-lab.yml @@ -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 diff --git a/labs/15-pitot/pitot-distribution/UPSTREAM.json b/labs/15-pitot/pitot-distribution/UPSTREAM.json index 23de2e092..0d5e45f81 100644 --- a/labs/15-pitot/pitot-distribution/UPSTREAM.json +++ b/labs/15-pitot/pitot-distribution/UPSTREAM.json @@ -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", @@ -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", @@ -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", diff --git a/labs/15-pitot/pitot-distribution/release-notes/2026-07-31-sdk-readiness-for-external-builders.md b/labs/15-pitot/pitot-distribution/release-notes/2026-07-31-sdk-readiness-for-external-builders.md new file mode 100644 index 000000000..d0ce9ccfe --- /dev/null +++ b/labs/15-pitot/pitot-distribution/release-notes/2026-07-31-sdk-readiness-for-external-builders.md @@ -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. diff --git a/labs/15-pitot/pitot-distribution/sdk/csharp/README.md b/labs/15-pitot/pitot-distribution/sdk/csharp/README.md new file mode 100644 index 000000000..bfd0e8d07 --- /dev/null +++ b/labs/15-pitot/pitot-distribution/sdk/csharp/README.md @@ -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. diff --git a/labs/15-pitot/pitot-distribution/sdk/java/README.md b/labs/15-pitot/pitot-distribution/sdk/java/README.md new file mode 100644 index 000000000..e6ce63faa --- /dev/null +++ b/labs/15-pitot/pitot-distribution/sdk/java/README.md @@ -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. diff --git a/labs/15-pitot/pitot-distribution/sdk/python/README.md b/labs/15-pitot/pitot-distribution/sdk/python/README.md new file mode 100644 index 000000000..7ab1d1bcd --- /dev/null +++ b/labs/15-pitot/pitot-distribution/sdk/python/README.md @@ -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== +``` + +`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. diff --git a/labs/15-pitot/pitot-distribution/sdk/rust/.gitignore b/labs/15-pitot/pitot-distribution/sdk/rust/.gitignore new file mode 100644 index 000000000..6850c8b1a --- /dev/null +++ b/labs/15-pitot/pitot-distribution/sdk/rust/.gitignore @@ -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 diff --git a/labs/15-pitot/pitot-distribution/sdk/rust/README.md b/labs/15-pitot/pitot-distribution/sdk/rust/README.md new file mode 100644 index 000000000..bcc89f72a --- /dev/null +++ b/labs/15-pitot/pitot-distribution/sdk/rust/README.md @@ -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. diff --git a/labs/15-pitot/pitot-distribution/sdk/tests/test_runners.py b/labs/15-pitot/pitot-distribution/sdk/tests/test_runners.py index eb82fafdf..be1e02ca2 100644 --- a/labs/15-pitot/pitot-distribution/sdk/tests/test_runners.py +++ b/labs/15-pitot/pitot-distribution/sdk/tests/test_runners.py @@ -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() { diff --git a/labs/15-pitot/pitot-distribution/sdk/typescript/README.md b/labs/15-pitot/pitot-distribution/sdk/typescript/README.md new file mode 100644 index 000000000..10b2cde34 --- /dev/null +++ b/labs/15-pitot/pitot-distribution/sdk/typescript/README.md @@ -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@ +``` + +`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. diff --git a/labs/15-pitot/pitot/runtime/request_test.go b/labs/15-pitot/pitot/runtime/request_test.go index 608868a8c..3c9da5729 100644 --- a/labs/15-pitot/pitot/runtime/request_test.go +++ b/labs/15-pitot/pitot/runtime/request_test.go @@ -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) } @@ -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") diff --git a/labs/15-pitot/scripts/build_pitot.py b/labs/15-pitot/scripts/build_pitot.py index 39a042fba..35e32d3fe 100644 --- a/labs/15-pitot/scripts/build_pitot.py +++ b/labs/15-pitot/scripts/build_pitot.py @@ -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 diff --git a/labs/15-pitot/tests/test_sdk_versions.py b/labs/15-pitot/tests/test_sdk_versions.py index ee2c56417..0a65a5639 100644 --- a/labs/15-pitot/tests/test_sdk_versions.py +++ b/labs/15-pitot/tests/test_sdk_versions.py @@ -30,11 +30,16 @@ def test_sdk_manifests_and_scaffold_constant_agree(self): r'pitotPackageVersion = "([^"]+)"', workbench ).group(1) + # The Go binding ships from the pitot module itself (installed with + # `go install ...@`), so its version IS the release tag rather than a + # separate manifest. It is represented here by the scaffold constant + # `pitotPackageVersion` in workbench.go, which `pitot init` writes into the + # Go projects it generates — the one Go-side version string that can drift. versions = { "sdk/python": python_version, "sdk/typescript": package_json["version"], "sdk/rust": rust_version, - "scaffold constant (workbench.go pitotPackageVersion)": scaffold_version, + "sdk/go (scaffold constant workbench.go pitotPackageVersion)": scaffold_version, } self.assertEqual( len(set(versions.values())), @@ -42,6 +47,42 @@ def test_sdk_manifests_and_scaffold_constant_agree(self): f"SDK versions drifted out of lockstep: {versions}", ) + def test_preview_sdks_stay_unshippable_or_join_the_gate(self): + """Java and C# are types-only previews (see each dir's README): no runner, + no conformance entry, and no build manifest — so they carry no release + version and are deliberately excluded from the lockstep gate above. + + This guard makes that exclusion honest instead of a silent gap: the moment + someone adds a build manifest (turning a preview into something that + installs), this test fails and forces them to give it a real version and + fold it into the lockstep set — so a shipped-but-ungated SDK can never slip + through. + """ + preview_manifests = { + "sdk/java": [ + "pitot-distribution/sdk/java/pom.xml", + "pitot-distribution/sdk/java/build.gradle", + "pitot-distribution/sdk/java/build.gradle.kts", + ], + "sdk/csharp": [ + "pitot-distribution/sdk/csharp/Pitot/Pitot.csproj", + "pitot-distribution/sdk/csharp/Pitot.sln", + ], + } + shipped = { + name: rel + for name, rels in preview_manifests.items() + for rel in rels + if (LAB / rel).exists() + } + self.assertEqual( + shipped, + {}, + "a preview SDK gained a build manifest and now installs but is not " + f"version-locked: {shipped}. Add its version to the lockstep gate " + "above (and a conformance entry) before shipping it.", + ) + if __name__ == "__main__": unittest.main()