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
12 changes: 6 additions & 6 deletions labs/15-pitot/pitot-distribution/UPSTREAM.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"files": {
"CONTRIBUTING.md": "0613b71aa497f8ca7d7296bf34ade87bfc7237a664d2e812d9b77b3b6befb0ad",
"README.md": "e54193fee53061889b3d5e82df7c76e72e440f265377d260b4a793475c216a7a",
"adapters/adapters.go": "80110df7289ea69f830b48667326ef297770c596e4ddbf1bc66201639f8288ad",
"adapters/adapters.go": "1b09f188ed9d3f8a1b159055556bd2a68153ad6ee9d59810b23c18a96a4a83bd",
"assets/pitot-boundary.png": "8a0ddb7d81831d94e14813f50ea4ca8670d77417f339ed2f91f0c653bf52f41d",
"assets/pitot-boundary.svg": "0c3871d70c84748573f231842091deb38a6def2862403ca34e8cc4493b9c9ebf",
"assets/pitot-hero.png": "a73532252b1e66c06273abbf5a4fe6261e98de3133b09e8d550edacfeeab92f8",
Expand All @@ -13,15 +13,15 @@
"bridge/bridge.go": "79ac2e025e16782f3c283b43cbea5b9ba4f837446583864df8f57d6346cae816",
"bridge/bridge_test.go": "23a19b7580d4b1e826224ec8322208ccca44ddd9a97b150efe15cafecc53e47f",
"cmd/generate-schema/main.go": "6e9d0030290d99e36967433f96e38385a122974f899ad9421aac1ef7e50d8fcb",
"cmd/pitot/main.go": "a8ef8a789abe13f41d8a1014de3fb4ad46522b558f0ace70786b904e81892b05",
"cmd/pitot/main_test.go": "ab2894327e7a5e72e9f3d83cae637e3ccb2e1ae05f14458be4fe9016149aeef7",
"cmd/pitot/main.go": "266cfdd1b758f2842a3cc232346e4ada4360077b1aa18c3a635697f56cab78a8",
"cmd/pitot/main_test.go": "7281ac0029d5e92c63c9feb1d2095aaef924aacf4e8a3db9af52872174d609ac",
"conformance/conformance.go": "43b692114f45c8b52958e34b35aee1cee339d8321c90f92ab4f5b963e79935bb",
"conformance/conformance_test.go": "83ab0bcc15371265a954d177e4e97d81ad3ea734bbf736a29a54628ef64b52cd",
"conformance/fixtures/negative.jsonl": "383dd001910699886bb1074d9225c91d8a6201e9fb3267d1a1f6e1c2753b0ba6",
"conformance/fixtures/positive.jsonl": "d3af0f2529dac9b33fa4900f5938e36fd0b17383088dd4f0de7e6eca269441d1",
"doc.go": "4dcd7a831a0a8ee6c3f6eeb8408c2e6898994509b11209564c0ed6b9a5218fce",
"doc.go": "9b51db0301aa428db731edcf8aa6b5a4fd71ae84f6a0d6cb2fc27656b04b1428",
"e2e/e2e_coverage_test.go": "8fac62d6d1c4ced359f8bb070379a88e77ded9912d867c642cda2d4703df23a7",
"e2e/e2e_hook_test.go": "a1c4601964eed674693306450249cf11a50978e5d2e00df152e33dc2c45ee1a0",
"e2e/e2e_hook_test.go": "cb9f27f4471c81c3320ea11be039a19514f5399b5ec5a80ae3b375dad8f8e8b5",
"examples/doc.go": "58f3f9eb7d272d7b6eecdb05f43e1613d5e3ef92d15d97c5440bd4b6990c26f9",
"examples/local-approval/main.go": "51386af324cd7d3bb07fe3ace53503884b02714b96b83073342fde81ce3b83a5",
"examples/token-meter/main.go": "4b1b9c1a43c3cf48b09dba6f607776caced9d2b5b562373496b31ed184582dd1",
Expand All @@ -42,7 +42,7 @@
"sensor/sensor.go": "939e146eaa51906972f98cf4615747eb75811b0c54d467a938825750336abeae",
"sensor/sensor_test.go": "ea9a58d45ad56d29214a40fb2cfa935e769f85334afcb1856a20fb938d486245",
"windtunnel/doc.go": "44e0bcde632da73e1f8b98beade3a34ca8e0d0ea79cdfb91d131de290b164fc4",
"windtunnel/windtunnel_test.go": "1c8b6c5d56ff5c4e66403baeae0d98108255d153c1275572b8c68e333e091493"
"windtunnel/windtunnel_test.go": "7e70ddf99411278175d2d2eb8ed73733367c77b3d04d55b4bf00738518673200"
},
"schema_version": 1
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Add Gemini CLI as a supported host adapter

Pitot now supports the `gemini` host adapter. This integration natively parses Gemini CLI's `BeforeTool` synchronous hook events, allowing the Pitot sensor to safely monitor and project Gemini's shell command behavior along with Cursor, Claude, and Codex.
21 changes: 21 additions & 0 deletions labs/15-pitot/pitot/adapters/adapters.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const (
Cursor Host = "cursor"
Claude Host = "claude"
Codex Host = "codex"
Gemini Host = "gemini"
)

// AdapterVersion is the semantic version stamped onto normalized events so
Expand Down Expand Up @@ -122,6 +123,26 @@ var (
Controllable: []string{"PreToolUse"},
},
},
Gemini: {
MainEventName: "BeforeTool",
Parser: ParserConfig{
CanonicalEvent: []byte(`{"hook_event_name":"BeforeTool","tool_name":"run_shell_command","tool_input":{"command":"git status --short"}}`),
CommandFor: func(raw RawHookEvent) (string, bool) {
if raw.ToolInput == nil {
return "", false
}
value, present := raw.ToolInput["command"].(string)
return value, present && value != ""
},
ActionKinds: map[string]string{
"BeforeTool": "shell",
},
},
Partition: ControlPartition{
// In Gemini, the BeforeTool hook is synchronous (blocking).
Controllable: []string{"BeforeTool"},
},
},
}
)

Expand Down
4 changes: 2 additions & 2 deletions labs/15-pitot/pitot/cmd/pitot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ func run(args []string, stdout, stderr io.Writer) error {
// 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)")
return fmt.Errorf("pitot: hook requires a host identifier (cursor, claude, codex, gemini)")
}
host := adapters.Host(args[0])
if host != adapters.Cursor && host != adapters.Claude && host != adapters.Codex {
if host != adapters.Cursor && host != adapters.Claude && host != adapters.Codex && host != adapters.Gemini {
return fmt.Errorf("pitot: unsupported hook host %q", host)
}

Expand Down
2 changes: 1 addition & 1 deletion labs/15-pitot/pitot/cmd/pitot/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func TestDoctorReportsBoundary(t *testing.T) {
t.Fatalf("doctor: %v", err)
}
out := stdout.String()
for _, want := range []string{"local boundary", "cursor", "claude", "codex", "decoder=PASS", "unauthenticated local socket: none"} {
for _, want := range []string{"local boundary", "cursor", "claude", "codex", "gemini", "decoder=PASS", "unauthenticated local socket: none"} {
if !strings.Contains(out, want) {
t.Errorf("doctor output missing %q\n%s", want, out)
}
Expand Down
2 changes: 1 addition & 1 deletion labs/15-pitot/pitot/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
//
// schema/ public event and response types + versioned constants
// protocol/ newline-delimited JSON framing and state-machine helpers
// adapters/ Claude Code, Cursor, and Codex host boundaries
// adapters/ Claude Code, Cursor, Codex, and Gemini CLI host boundaries
// sensor/ normalization and observation pipeline (decoder)
// bridge/ controller routing and single-response transport
// projection/ full, sha256, and omit content policies
Expand Down
1 change: 1 addition & 0 deletions labs/15-pitot/pitot/e2e/e2e_hook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func TestE2ESensorsConformityAcrossAllAdapters(t *testing.T) {
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"}}`,
adapters.Gemini: `{"hook_event_name":"BeforeTool","tool_name":"run_shell_command","tool_input":{"command":"git status --short"}}`,
}

hosts := adapters.Supported()
Expand Down
1 change: 1 addition & 0 deletions labs/15-pitot/pitot/windtunnel/windtunnel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ var boatstackCanonicalEvents = map[adapters.Host]string{
adapters.Cursor: `{"hook_event_name":"beforeShellExecution","command":"git status --short"}`,
adapters.Claude: `{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"git status --short"}}`,
adapters.Codex: `{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"git status --short"}}`,
adapters.Gemini: `{"hook_event_name":"BeforeTool","tool_name":"run_shell_command","tool_input":{"command":"git status --short"}}`,
}

func TestSensorConsumesBoatstackCanonicalEvents(t *testing.T) {
Expand Down
2 changes: 2 additions & 0 deletions labs/15-pitot/tests/e2e_gemini_cli_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env bash
exec labs/15-pitot/tests/e2e_unified_runner.sh "gemini"
Loading