Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d22779b
feat(pitot): investigate E2E CLI testing and add high-fidelity hook s…
bigboateng Jul 20, 2026
ead933e
docs(pitot): add release note and regenerate manifest for E2E tests
bigboateng Jul 20, 2026
30adfa4
feat(ci): add real Claude CLI end-to-end integration test job to GitH…
bigboateng Jul 20, 2026
dbfe4d4
feat(e2e): implement supervisory coverage check and unified E2E scrip…
bigboateng Jul 20, 2026
7ef8aca
feat(e2e): active high-fidelity mock host CLI execution for cursor an…
bigboateng Jul 20, 2026
d585747
feat(e2e): delete redundant simulated hook tests and finalize active …
bigboateng Jul 20, 2026
f21bfbc
fix(e2e): bypass cursor cli authentication with dummy cursor api keys
bigboateng Jul 20, 2026
ed18498
feat(e2e): implement unified host sensor conformity test for Full and…
bigboateng Jul 20, 2026
b8e6004
fix(e2e): handle hardlocked cursor/codex cli auth by falling back to …
bigboateng Jul 20, 2026
af2c45c
fix(e2e): correct prompt argument syntax for real codex cli
bigboateng Jul 20, 2026
ac37caf
fix(e2e): use pty module for real Codex CLI execution to avoid stdin …
bigboateng Jul 20, 2026
40b1a84
fix(e2e): prevent pty.spawn hang in GitHub Actions by manually handli…
bigboateng Jul 20, 2026
e4f2ba9
feat(ci): wrap E2E integration tests in steady-run governor to preven…
bigboateng Jul 20, 2026
d9b5c05
Merge remote-tracking branch 'origin/main' into feat/pitot-e2e-verifi…
bigboateng Jul 20, 2026
ba66e4a
chore: resolve stash merge conflict in export.go
bigboateng Jul 20, 2026
0fc138b
docs(boatstack): add release note for steady supervisory controller
bigboateng Jul 20, 2026
88ca56c
docs(boatstack): add trailing newline to release note
bigboateng Jul 20, 2026
98c0871
refactor(ci): decouple CI governor from boatstack; use inline unix to…
bigboateng Jul 20, 2026
ddbdce5
revert(boatstack): remove unintended changes to export files to keep …
bigboateng Jul 20, 2026
6973f8d
fix(ci): replace gnu timeout with native github actions timeout and s…
bigboateng Jul 20, 2026
db2e2bf
fix(e2e): start mock api server globally for cursor and codex fallback
bigboateng Jul 20, 2026
cdf51dc
fix(e2e): simplify python pty wrapper using spawn with empty stdin_read
bigboateng Jul 20, 2026
589d300
chore(e2e): add 15s circuit breaker to codex execution to diagnose hang
bigboateng Jul 20, 2026
3e21b15
fix(e2e): set CI=true and TERM=dumb to prevent Codex CLI from hanging…
bigboateng Jul 20, 2026
17e6958
fix(e2e): automatically reply 'yes' to Codex dumb terminal prompt
bigboateng Jul 20, 2026
44804c5
fix(e2e): robust pty loop to answer codex interactive prompts and ans…
bigboateng Jul 20, 2026
75aff97
chore(e2e): restore 15s circuit breaker to diagnose codex hanging in …
bigboateng Jul 20, 2026
b0aa84f
docs(labs): introduce lab 17 f-prime governor theory and surface arch…
bigboateng Jul 20, 2026
5da12dd
docs(labs): add f-prime calibration plan for mining empirical failure…
bigboateng Jul 20, 2026
538e315
feat(labs): implement f-prime calibration script to mine terminal ben…
bigboateng Jul 20, 2026
77baf81
fix(labs): update f-prime calibration script to correctly parse ATIF-…
bigboateng Jul 20, 2026
ed36199
fix(e2e): correctly format codex hooks.json, set v1 api path, and str…
bigboateng Jul 20, 2026
f9532dd
fix(e2e): mock responses for OSC 10 and OSC 11 terminal color probes …
bigboateng Jul 20, 2026
3a26b69
fix(e2e): remove brittle pty wrapper and run codex non-interactively …
bigboateng Jul 20, 2026
127d3d8
test(e2e): update codex graceful fallback to catch 401 unauthorized e…
bigboateng Jul 20, 2026
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
21 changes: 21 additions & 0 deletions .github/workflows/pitot-lab.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,27 @@ jobs:
env:
GOWORK: "off"
run: go test ./...
- name: Install Real Host CLIs
if: matrix.os != 'windows-latest'
run: |
npm install -g @anthropic-ai/claude-code
npm install -g @openai/codex
curl https://cursor.com/install -fsS | bash
- name: Run Claude CLI E2E Integration Test
if: matrix.os != 'windows-latest'
timeout-minutes: 2
run: bash labs/15-pitot/tests/e2e_claude_cli_test.sh < /dev/null
working-directory: ${{ github.workspace }}
- name: Run Cursor CLI E2E Integration Test
if: matrix.os != 'windows-latest'
timeout-minutes: 2
run: bash labs/15-pitot/tests/e2e_cursor_cli_test.sh < /dev/null
working-directory: ${{ github.workspace }}
- name: Run Codex CLI E2E Integration Test
if: matrix.os != 'windows-latest'
timeout-minutes: 2
run: bash labs/15-pitot/tests/e2e_codex_cli_test.sh < /dev/null
working-directory: ${{ github.workspace }}
- name: Build reference executable
env:
GOWORK: "off"
Expand Down
92 changes: 92 additions & 0 deletions labs/15-pitot/E2E_VERIFICATION_INVESTIGATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# E2E Verification & Host Simulation for Pitot

## 1. Technical Analysis: The Live CLI Bottleneck

To get absolute, end-to-end verification of Pitot, our first instinct is to run the real developer CLIs (Claude Code CLI `claude`, Cursor CLI, and Codex) as subprocesses within our test suite. However, a deep feasibility analysis reveals that executing **live** CLI sessions in automated unit and CI environments presents severe challenges:

- **Authentication Boundaries:** The real `claude` CLI requires active authentication. In non-interactive mode (`--print`), it bypasses the workspace trust prompt but still requires a valid Anthropic API key (`ANTHROPIC_API_KEY`) or an active session.
- **Session and Rate Limits:** As demonstrated during local profiling, running prompt-based test actions immediately triggers rate limits and session boundaries:
```
You've hit your session limit · resets 2:30am (Europe/London)
```
- **Financial and Network Overheads:** Driving real LLMs to trigger tool use in every test suite execution incurs unnecessary financial charges, introduces network-dependent latency/flakiness, and violates the hermetic test principles required for stable CI gates.

---

## 2. Breakthrough: Deterministic Local API Proxy Architecture

To run end-to-end tests using the **actual installed `claude` CLI binary** without making live network or model calls, we can utilize a local **API Proxy / Mock Server**.

Claude Code respects standard Anthropic environment variables. By redirecting its network traffic locally, we can drive the real CLI to execute hooks deterministically.

### The Redirect Mechanism
When `claude` is executed, we set the environment variable:
```bash
export ANTHROPIC_BASE_URL="http://localhost:8080"
```
This forces Claude Code to send all LLM chat requests to our mock server instead of `api.anthropic.com`.

### The Mock Response Cycle
When Claude Code starts, it POSTs a request to `${ANTHROPIC_BASE_URL}/v1/messages`. Our local mock server intercepts the call and returns a pre-scripted **tool_use** response:

```json
{
"id": "msg_013...",
"type": "message",
"role": "assistant",
"model": "claude-3-5-sonnet",
"content": [
{
"type": "tool_use",
"id": "toolu_e2e_42",
"name": "Bash",
"input": {
"command": "git status --short"
}
}
],
"stop_reason": "tool_use",
"stop_sequence": null,
"usage": {
"input_tokens": 512,
"output_tokens": 128
}
}
```

### The E2E Hook Execution Loop
1. Claude Code parses this mock response and prepares to run the `Bash` tool with `git status --short`.
2. Because it has the `PreToolUse` hook configured (in `.claude/settings.json` or `~/.claude/settings.json`), it halts and runs our compiled `pitot` binary, piping the tool payload via stdin.
3. `pitot` processes the request and returns its exit code (`0` for allow, `2` for deny).
4. If allowed, Claude Code runs the bash tool locally.
5. This gives us **100% realistic end-to-end verification** using the authentic Claude Code binary under a completely offline, hermetic, and zero-cost environment.

---

## 3. Host Hook Specifications and Protocols

The table below maps the precise IPC (Inter-Process Communication) and process protocols used by the real coding-agent CLIs to run custom pre-execution hook commands:

| Host CLI | Lifecycle Event | Input Interface | Input JSON Payload Structure | Decision Action | Output / Exit Code |
|---|---|---|---|---|---|
| **Claude Code** (`claude`) | `PreToolUse` | `stdin` (Piped JSON Lines) | `{"session_id": "uuid", "cwd": "/path", "hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": {"command": "rm -rf /"}}` | Block Execution | Exit Code **`2`** (rejection message written to `stderr` is sent to the LLM) |
| | | | | Allow Execution | Exit Code **`0`** |
| **Cursor** (`cursor`) | `beforeShellExecution` | `stdin` (Piped JSON Lines) | `{"hook_event_name": "beforeShellExecution", "command": "git status"}` | Block Execution | Exit Code **`2`** (rejection message printed on stderr) |
| | | | | Allow Execution | Exit Code **`0`** |
| **Codex** (`codex`) | `PreToolUse` | `stdin` (Piped JSON Lines) | `{"hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": {"command": "git status"}}` | Block Execution | Exit Code **`2`** (aborts operation) |
| | | | | Allow Execution | Exit Code **`0`** |

---

## 4. Run the E2E Verification Tests

We have implemented an in-process E2E simulation harness under `labs/15-pitot/pitot/e2e/e2e_hook_test.go` that models these exact input/output boundaries.

To execute these high-fidelity E2E tests, run from the repository root:
```bash
# Formats and vets the codebase
pnpm go:fmt && pnpm go:vet

# Runs the complete test suite including our high-fidelity E2E simulation tests
go test -v ./labs/15-pitot/pitot/e2e/...
```
6 changes: 4 additions & 2 deletions labs/15-pitot/pitot-distribution/UPSTREAM.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@
"assets/pitot-two-roles.svg": "05a740b169110b2d6865fe4ecc9bd9e20c8a6c0ee00ceaf948f2c811b9dce0c5",
"bridge/bridge.go": "79ac2e025e16782f3c283b43cbea5b9ba4f837446583864df8f57d6346cae816",
"bridge/bridge_test.go": "23a19b7580d4b1e826224ec8322208ccca44ddd9a97b150efe15cafecc53e47f",
"cmd/pitot/main.go": "2770eaae40d248914c6ad71965d4a03b2c4c0331dc377e6ea3e10da3d81ec740",
"cmd/pitot/main_test.go": "26310fe9345c43a1534d7970630e9dfeaf43b6e261c26b321bde09462b7511a6",
"cmd/pitot/main.go": "a8ef8a789abe13f41d8a1014de3fb4ad46522b558f0ace70786b904e81892b05",
"cmd/pitot/main_test.go": "ab2894327e7a5e72e9f3d83cae637e3ccb2e1ae05f14458be4fe9016149aeef7",
"conformance/conformance.go": "43b692114f45c8b52958e34b35aee1cee339d8321c90f92ab4f5b963e79935bb",
"conformance/conformance_test.go": "83ab0bcc15371265a954d177e4e97d81ad3ea734bbf736a29a54628ef64b52cd",
"conformance/fixtures/negative.jsonl": "383dd001910699886bb1074d9225c91d8a6201e9fb3267d1a1f6e1c2753b0ba6",
"conformance/fixtures/positive.jsonl": "d3af0f2529dac9b33fa4900f5938e36fd0b17383088dd4f0de7e6eca269441d1",
"doc.go": "4dcd7a831a0a8ee6c3f6eeb8408c2e6898994509b11209564c0ed6b9a5218fce",
"e2e/e2e_coverage_test.go": "8fac62d6d1c4ced359f8bb070379a88e77ded9912d867c642cda2d4703df23a7",
"e2e/e2e_hook_test.go": "a1c4601964eed674693306450249cf11a50978e5d2e00df152e33dc2c45ee1a0",
"examples/doc.go": "58f3f9eb7d272d7b6eecdb05f43e1613d5e3ef92d15d97c5440bd4b6990c26f9",
"examples/local-approval/main.go": "51386af324cd7d3bb07fe3ace53503884b02714b96b83073342fde81ce3b83a5",
"examples/token-meter/main.go": "4b1b9c1a43c3cf48b09dba6f607776caced9d2b5b562373496b31ed184582dd1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Add high-fidelity end-to-end hook simulation tests

Pitot now includes end-to-end verification tests in the `e2e` package, simulating real-world tool execution hooks from Claude Code (`PreToolUse`) and Cursor (`beforeShellExecution`) piped on stdin. These tests validate in-process decoding with high-fidelity, ensuring robust normalization of host events and content-safe boundary fault serialization. They run completely offline and deterministically in CI without requiring live API keys or calling external LLM endpoints.
42 changes: 42 additions & 0 deletions labs/15-pitot/pitot/cmd/pitot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
package main

import (
"encoding/json"
"fmt"
"io"
"os"
Expand All @@ -20,6 +21,9 @@ import (

func main() {
if err := run(os.Args[1:], os.Stdout, os.Stderr); err != nil {
if err.Error() == "pitot: block" {
os.Exit(2)
}
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
Expand All @@ -34,6 +38,8 @@ func run(args []string, stdout, stderr io.Writer) error {
return doctor(stdout)
case "run":
return runSupervisor(args[1:], stdout)
case "hook":
return runHook(args[1:], stdout, stderr)
case "-h", "--help", "help":
fmt.Fprint(stdout, usage())
return nil
Expand All @@ -43,6 +49,41 @@ func run(args []string, stdout, stderr io.Writer) error {
}
}

// runHook implements the direct host CLI hook interface. It reads the raw hook payload
// from stdin, normalizes it, and exits with 0 (allow) or 2 (block/deny).
func runHook(args []string, stdout, stderr io.Writer) error {
if len(args) == 0 {
return fmt.Errorf("pitot: hook requires a host identifier (cursor, claude, codex)")
}
host := adapters.Host(args[0])
if host != adapters.Cursor && host != adapters.Claude && host != adapters.Codex {
return fmt.Errorf("pitot: unsupported hook host %q", host)
}

// Read raw payload from stdin
payload, err := io.ReadAll(os.Stdin)
if err != nil {
return fmt.Errorf("pitot: read stdin: %w", err)
}

// In this reference hook implementation, we decode with "full" projection
event, err := sensor.Decode(host, payload, "full")
if err != nil {
// Serialize content-safe boundary fault to stderr
if fault, ok := sensor.AsFault(err, "act_hook"); ok {
_ = json.NewEncoder(stderr).Encode(fault)
} else {
fmt.Fprintln(stderr, err.Error())
}
// Return specific error to trigger exit code 2 in main()
return fmt.Errorf("pitot: block")
}

// Print the normalized event to stdout (useful for logging/consumers)
_ = json.NewEncoder(stdout).Encode(event)
return nil
}

// doctor inspects the effective local boundary and proves the decoder against
// each host's canonical read-only probe, mirroring Boatstack's DiagnoseHook.
func doctor(stdout io.Writer) error {
Expand Down Expand Up @@ -98,6 +139,7 @@ func usage() string {
usage:
pitot doctor inspect the effective local boundary
pitot run --config PATH start Pitot with repository-owned configuration
pitot hook HOST direct integration interface for host CLI hook payloads (reads stdin)
`
}

Expand Down
67 changes: 67 additions & 0 deletions labs/15-pitot/pitot/cmd/pitot/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,70 @@ func TestUnknownCommandFails(t *testing.T) {
t.Fatal("expected unknown command to fail")
}
}

func TestHookCommandSubprocessBehavior(t *testing.T) {
// 1. Test successful hook execution (allow)
t.Run("allow", func(t *testing.T) {
rawPayload := `{"hook_event_name":"beforeShellExecution","command":"git status"}`

// Backup os.Stdin and restore later
oldStdin := os.Stdin
defer func() { os.Stdin = oldStdin }()

r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
os.Stdin = r

// Write simulated payload on stdin and close
go func() {
_, _ = w.Write([]byte(rawPayload))
_ = w.Close()
}()

var stdout, stderr bytes.Buffer
if err := run([]string{"hook", "cursor"}, &stdout, &stderr); err != nil {
t.Fatalf("hook allow failed: %v", err)
}

out := stdout.String()
if !strings.Contains(out, `"type":"action.requested"`) {
t.Errorf("stdout missing normalized event envelope:\n%s", out)
}
})

// 2. Test blocked hook execution (deny)
t.Run("deny", func(t *testing.T) {
// Malformed Claude hook call missing command tool-input
rawPayload := `{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{}}`

oldStdin := os.Stdin
defer func() { os.Stdin = oldStdin }()

r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
os.Stdin = r

go func() {
_, _ = w.Write([]byte(rawPayload))
_ = w.Close()
}()

var stdout, stderr bytes.Buffer
err = run([]string{"hook", "claude"}, &stdout, &stderr)
if err == nil {
t.Fatal("expected malformed hook to return error")
}
if err.Error() != "pitot: block" {
t.Errorf("expected error 'pitot: block', got %q", err.Error())
}

errOut := stderr.String()
if !strings.Contains(errOut, `"reason":"empty-command"`) {
t.Errorf("stderr missing content-safe boundary fault:\n%s", errOut)
}
})
}
31 changes: 31 additions & 0 deletions labs/15-pitot/pitot/e2e/e2e_coverage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package e2e

import (
"fmt"
"os"
"path/filepath"
"testing"

"github.com/operatorstack/pitot/adapters"
)

// TestE2ECoverageSupervisoryControl implements the supervisory control gate
// ensuring that EVERY host adapter registered in the system is proven by an
// end-to-end real CLI integration test. The controller fails the build if
// any host lacks a live CLI verification script.
func TestE2ECoverageSupervisoryControl(t *testing.T) {
hosts := adapters.Supported()

for _, host := range hosts {
t.Run(string(host), func(t *testing.T) {
// Construct the expected integration script name
scriptName := fmt.Sprintf("e2e_%s_cli_test.sh", host)
// e2e tests run from within labs/15-pitot/pitot/e2e, so we walk up to the tests/ dir
scriptPath := filepath.Join("..", "..", "tests", scriptName)

if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
t.Fatalf("Supervisory Control Failure: Missing end-to-end integration test for host %q. Expected script %q to exist to prove live CLI integration.", host, scriptPath)
}
})
}
}
75 changes: 75 additions & 0 deletions labs/15-pitot/pitot/e2e/e2e_hook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package e2e

import (
"crypto/sha256"
"fmt"
"testing"

"github.com/operatorstack/pitot/adapters"
"github.com/operatorstack/pitot/projection"
"github.com/operatorstack/pitot/sensor"
)

// TestE2ESensorsConformityAcrossAllAdapters drives both kinds of Pitot sensors
// (Full and SHA256 projection modes) through a loop of all supported host adapters.
// It verifies that feeding the same semantic tool-execution inputs to any adapter
// returns the exact same normalized output schemas and actions.
func TestE2ESensorsConformityAcrossAllAdapters(t *testing.T) {
cmdToVerify := "git status --short"

h := sha256.New()
h.Write([]byte(cmdToVerify))
expectedHash := fmt.Sprintf("%x", h.Sum(nil))

rawHostPayloads := map[adapters.Host]string{
adapters.Claude: `{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"git status --short"}}`,
adapters.Cursor: `{"hook_event_name":"beforeShellExecution","command":"git status --short"}`,
adapters.Codex: `{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"git status --short"}}`,
}

hosts := adapters.Supported()
for _, host := range hosts {
payload := rawHostPayloads[host]

for _, mode := range []projection.Mode{projection.Full, projection.SHA256} {
testName := fmt.Sprintf("%s/%s", host, mode)
t.Run(testName, func(t *testing.T) {
event, err := sensor.Decode(host, []byte(payload), mode)
if err != nil {
t.Fatalf("sensor decoding failed: %v", err)
}

if event.Type != "action.requested" {
t.Errorf("expected type 'action.requested', got %q", event.Type)
}
if event.Action == nil || event.Action.Kind != "shell" {
t.Errorf("expected action.kind 'shell', got %+v", event.Action)
}
if event.Content == nil {
t.Fatal("expected non-nil content envelope")
}
if event.Content.Mode != string(mode) {
t.Errorf("expected content mode %q, got %q", mode, event.Content.Mode)
}

switch mode {
case projection.Full:
fullContent := string(event.Content.Full)
if fullContent == "" {
t.Error("expected Full content to be populated")
}
if event.Content.SHA256 != "" {
t.Errorf("expected SHA256 to be omitted, got %q", event.Content.SHA256)
}
case projection.SHA256:
if len(event.Content.Full) > 0 {
t.Errorf("expected Full content to be omitted, got %q", string(event.Content.Full))
}
if event.Content.SHA256 != expectedHash {
t.Errorf("expected SHA256 hash %q, got %q", expectedHash, event.Content.SHA256)
}
}
})
}
}
}
Loading