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
33 changes: 20 additions & 13 deletions labs/15-pitot/pitot-distribution/UPSTREAM.json
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
{
"files": {
".goreleaser.yml": "d375f187624219e9420acd2a40cfae95772ef1fee3abda3e729d6a16ba612032",
"CONTRIBUTING.md": "02c89a5790f2943e5e2466b0d260210238cae306922f0f031adb5f7028b48066",
"CONTRIBUTING.md": "a983d968cf228cc60d3f80cecd9a52e5ba8f3235595886f858fc384ed3dd114b",
"LICENSE": "bd17d47aefaa7406616179288058001b6532881a2517254fe668d7e9c4965cfb",
"README.md": "27df9b71b14ac474de3d03f2049eabb7d1c2c35514b7d351940683b0651d1984",
"README.md": "dce6eccbb3638c04ea0310a3f6efe103f0181c22e254e79c2bdd91e923656e19",
"adapter-verification.json": "fedd1624e01d19739114203674281296de779097e45fbb9e497047805b7d9250",
"adapters/adapters.go": "13cc5d94ea7c4ae50eccfd27d63e0d91b38cf76ee20aa9737a7edc9bd47d13b2",
"adapters/adapters.go": "25ebd930b4e3aa92a02043e75f2a4a6a818c546b479c978f4b75b8c3962c56de",
"adapters/guard_test.go": "cd1c09e2fb3a2ecc7a73a1714d9b5bf976d67ab206ed845027ed34699c8e123d",
"assets/pitot-boundary.png": "8a0ddb7d81831d94e14813f50ea4ca8670d77417f339ed2f91f0c653bf52f41d",
"assets/pitot-boundary.svg": "0c3871d70c84748573f231842091deb38a6def2862403ca34e8cc4493b9c9ebf",
"assets/pitot-hero.png": "a73532252b1e66c06273abbf5a4fe6261e98de3133b09e8d550edacfeeab92f8",
"assets/pitot-hero.svg": "226206e254252e2e7111a49d650d12cf5b32ceedd9db2adf06d0598be44809d6",
"assets/pitot-mark.svg": "cacd728b4d4da45000ccde15d905314f2d92eb47b5e40a5f86a9e24ad671a003",
"assets/pitot-two-roles.png": "9093368b30b7a0b704358343e78df2f937af21f2263b50e8da21a2946306255a",
"assets/pitot-two-roles.svg": "528edf57c3eddb4432e119b9aa80a47452d47eb428e0e77a6f8623f244334475",
"bridge/bridge.go": "5adfcd3f743cae46e4446a6e030d53464ada97de0261a8588fa2a9fcd62136b8",
"bridge/bridge.go": "104d515b1cd66ef1e9dac57e5ef25d80b847b7512c44fc6703ad43efa99e59be",
"bridge/bridge_test.go": "6dcc6d05f2b39c25955fc0b2d21d3d148dd9d77600fb12799941f86bdb1acb61",
"bridge/window_test.go": "3d9f3f6af95af61a26d99da39f84842db8b71f33c0788f746782e8ec2f216c60",
"cmd/generate-schema/main.go": "6e9d0030290d99e36967433f96e38385a122974f899ad9421aac1ef7e50d8fcb",
"cmd/pitot/devin_acp.go": "b10962cc21239d0b3955d2521f1f4d940c5de34624d14ce89ef0e40e1fbc1138",
"cmd/pitot/devin_acp_test.go": "158a9ee019e8368b9f9434000eea8cbeee98201203213ef83797102549e1dd0a",
"cmd/pitot/doctor_host.go": "fa7819448218c3dd92d3393df9a0ac6e5aa70803348898eec10220bc7b3867a4",
"cmd/pitot/doctor_host.go": "f9d91ae1f0e8cba344f8a6e742467e98e2cf7c82afdb9da3043fba1bc13bbee5",
"cmd/pitot/doctor_host_test.go": "4e6e327f6cf27cf94a0a608e10eb6790d6c11fcd53e6dfd7370007190749952f",
"cmd/pitot/hook_notice_test.go": "b6a0c3689a97beecf32e9aced68585715cc125139f1646f8ba90c4e65e6b2fa8",
"cmd/pitot/install.go": "46cab87809566f8da47c4d71b8a87618d06cb0829bdabe1c8f734faf53a9f46c",
"cmd/pitot/kimi_control_test.go": "27b38867d4799636a664e3b1726ef55568f5dfd6a9be11f0ce5eca9931d759a4",
"cmd/pitot/kimi_smoke_test.go": "01cbf18312902cac42ec1f2547d35362c2bc920938acea508ba7c7f0638a9473",
"cmd/pitot/main.go": "9b7851b4e91c3eef08d6dac82939c1842f8e1a5237bf603e5a6ccc51a50561ec",
"cmd/pitot/main.go": "230d3ccc99a244150644602692a5efc0f40cf8e82de840f43cb830db29fd6bd9",
"cmd/pitot/main_test.go": "35c17c8de7caf13d0ed399c0b67903236cd3c64faa1ea134c5445d00887a32a5",
"cmd/pitot/shim.go": "3a7a4be5bd769010ad947de78188dd17c729388dbf74153d4dbd51fba2121ab1",
"cmd/pitot/substrate_test.go": "b4e7d83aa592f7b5bf4ec059ec552abd62473e51a6634a26428448168898d0a3",
Expand All @@ -36,7 +39,7 @@
"cmd/pitot/workbench_contract_test.go": "5e465f3d3f8b93ffaacfb4738279d369b13514bf7613b8611952f3cd26896586",
"cmd/pitot/workbench_dev_test.go": "abffe81e1a25f086d7f3c2f1c32986ee93618bbfc87220bdfda840ec6c6466bf",
"cmd/pitot/workbench_test.go": "3c561498dfee4aacf6935fe4b0bfe4449c3961c169c04f1f13e81a38b22d0914",
"config/config.go": "84476c7525833b07222b3b06834ff528f9c496f164bb065d006afad29bbca1fb",
"config/config.go": "d49d274ec7fe16aebb0991b2422095f2f68a28ba818c6a2b2e9826ee30377645",
"config/config_test.go": "87d3e5ddc4a3b43c736070de671d03e03ffe29cdd759771526ad27fd9bc0034c",
"config/findroot_test.go": "89d8b31f188325a02bde2dcd77e683c06e9dca4f59c872366c027111147ea8a5",
"config/merge_test.go": "595d2c96ac879cca7c57b77b99504f34629b8aa55b38d515f35ad76c006ef532",
Expand Down Expand Up @@ -67,8 +70,9 @@
"integrations/pi/pitot.ts": "ed2d60d5ab6e33e115cfa058e4f96095100e93a061567d0af31249aa756bab3e",
"integrations/qwen/PreToolUse": "95c358620f2f882bb8680e6aa9639f3b36a8567a3fd60cf14a6cdf3b3fbcf78b",
"integrations/qwen/PreToolUse.cjs": "e9bf00bbbee5c15f01ea203d0992b34f8754b75a9c9889eecb16baeb10fcce6c",
"internal/devinacp/client.go": "e2e70755d5888a2bd34e44b160d1dbf955fe147adf020050aa3fbc68cdbe709d",
"internal/devinacp/client.go": "531bf28ea26c5c4b417c7b6ce2f4b97a41ca760226ca06509412c353aae29131",
"internal/devinacp/client_test.go": "5bfff9b7f29d253450e4424ab44dab97877ddb39b3941607a4e264dd66da32cb",
"internal/devinacp/meta_test.go": "0b56f3beb24d4afeff0a6066cb4600bb773976cfa558fea7b963ccda9752c939",
"internal/testrole/main.go": "6d657eb85d8ddaff0ae5a3da281aa7aa0a4bdda860d179e99508110033765787",
"internal/testrole/main_test.go": "c7fbc4905bcef7d662c9e162a4c32c8a86d5c1a97820e9f4ee48f643c4e47a38",
"projection/projection.go": "4d3c823fd72a3ca5387dba3683838a1d7e455e9b18309acc839763a39b7bb35f",
Expand All @@ -79,9 +83,11 @@
"runtime/descriptor_windows.go": "2d9ffefe3af0154fa8042de6b67460d4e86dd3f4cdd9e986f180f7d0c535c9a5",
"runtime/request.go": "198c44fd6c547022a15b6d0d48e4d0130fa8afb687994365115576e4d874550d",
"runtime/request_test.go": "86d8a2feb4ec72e8ed675b9567da2d1f5d628950eeec907c10b9cc1675aa1904",
"runtime/runtime.go": "b90072bf119c9121e3d185fa27e8ac372ec9dcb33c9f38fea8050c314b9dd5e4",
"runtime/runtime.go": "36fdf4643c04d058fef380333a0a3942913d1dd60e910b893dc134da4b65d6bf",
"runtime/runtime_test.go": "afd78d122af20148bf30d0db873ff002544189df0dfec0f6167b8cf5cd0d42b1",
"runtime/transport.go": "83e2218fb28474e875dafa6943bc5b665acef0565aaf5955fa88b1b4fd21614e",
"runtime/strict_test.go": "d2eb2b78563733e3279289d8b6d17a4276e831ed3c78f8b3ec9d6706992ffaec",
"runtime/transport.go": "671d67be8b8ea0c5c3132cc355054b36b0ecf8001dcc79b4e04be7a584f4bf02",
"runtime/transport_fault_test.go": "1eb58f09a2412ed5411f5d07c4ac62bb8f0fca1ce41201f670f5fe9e98b1abc7",
"runtime/transport_test.go": "9b69f590f1e258470adea249b3ac6d4a00f1001f10bb08dfa7b56c6e2d6709ae",
"schema/schema.go": "fd5c3b76979c88aeed75fadb7e3c94abcad62e067d77595f021fb422a60f2211",
"sdk/csharp/Pitot/Types.cs": "3fecc1eabca6e3ad218c73741d185f815d868a618d1e7ed3b9320eeb6942977d",
Expand Down Expand Up @@ -112,10 +118,11 @@
"sdk/typescript/src/runner.ts": "c58babd3ec3996a05988f3cb7dec061cf7c48f41cd5125625be3408cb9b207f0",
"sdk/typescript/tsconfig.json": "de0065da9acf19a93ba8646c90b7101dfd5a8667552d3d3c59e3effd813218aa",
"sensor/decode_fuzz_test.go": "d27f2fbbc069eded26a73c9cd9bace98dd8a9e34949576790b81a08d130fbaf2",
"sensor/sensor.go": "498d4f69c9243a409b25704beab9d441bb312c7813d168138b85a69cff08d8e9",
"sensor/guard_test.go": "bbd112398c9bb61eeb889f5cb5468bd0d4581537b4292edfcb3b6bc0864b7347",
"sensor/sensor.go": "3832eeb1267513f74eefe910394983df847a4c5152029df0e19d170bd05fa059",
"sensor/sensor_test.go": "9e0ec50ec3930c37b0e4cc56419be451fb56432f0f22efa145b55ddf6440cd4b",
"tests/cursor_control_proxy.mjs": "ab532aa56a9299f497f3ceeedb4b6a0beb26089b7c022ed774385dd149ea56e6",
"tests/devin_control_proxy.py": "0be724db777bb790d18c49fad6280ddf77c261f7ae3c947323cc0cc20aab0422",
"tests/devin_control_proxy.py": "8306a80af890b040bfa7bda43573cd2fec7df207aa653f0b1a970f625a263566",
"tests/e2e_claude_cli_test.sh": "b28c4d1963e326b4b3f158a7cfc1b92771e768ac9665dd43e8a339013cc11568",
"tests/e2e_codex_cli_test.sh": "dbae5224c87410a5a5d67023d7e23406453bc5493d981af9347993b4e1f562f2",
"tests/e2e_copilot_cli_test.sh": "61b1e44dcd598d2d33e7f04dec26bec74405a9e2456ecda46e94dc8d43ad4315",
Expand All @@ -132,7 +139,7 @@
"tests/install_real_agent.py": "a8dc79bf0914ed5ab2a82c4f2471e805f24eef2adde2261fee22dbd5aabeb985",
"tests/mock_anthropic_server.js": "ecebea62f9e93791a79f1ae3dd3c67b8fa42490e9805b23b662b877edfdb0f0e",
"tests/model_control_proxy.py": "cd4733ab20c16770fe3bf6ac477935f7dc285cd89be020848ae4798cb509e5f2",
"tests/real_agent_driver.py": "789e72797137fedcc7afc61f14a943df3f57e02f0a2329d125beeb8ce9311394",
"tests/real_agent_driver.py": "33955c673caabcff33f7884a60b87a84e60880cce658e5747a9c939f9f20f4ee",
"tests/run_e2e_report.py": "a72cbc486c100b75fccf6ac787da1736e13426b44a15d9d5dddcb8177bbaf596",
"tests/runtime_capability_driver.py": "54a485fc4f16981f2542d0dbd903a12b80f31f071908a378607f26678aeea07f",
"tests/witness/main.go": "cd56bbd00aa44cc5baf6426c8461a8ebca4a8391518f6acfa2301ac36add7c5f",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
### Adapter boundary hardening

Pitot hardens the supervised boundary without removing any adapter, transport,
or observation mode. An event-delivery fault now returns a distinct conflict
response instead of one that a host could read as an allow, and the ACP
transport emits a one-shot diagnostic when a `tool_call` update omits the
vendor inference-tool metadata (it stays fail-closed either way).

An additive strict mode — the `require_controller` config key and the
`pitot run --strict` flag — makes a declared action kind with no registered
controller fault instead of allowing. The default remains permissive, now with
an explicit observation-only notice so a downgraded guarantee is never silent.
`pitot hook` prints a notice when no runtime is selected, and `pitot doctor`
gains a binary-hash drift check for hosts wired outside the repository.

Decode-time tool-name guards were added per host, unknown boundary events no
longer default to `shell`, and the router's resolved-action set is now bounded.
Coverage grows with Go guard tests, Devin control-proxy HTTP tests, receipt
negative-branch tests, the two E2E apparatus binaries, and npm/Kimi/Cursor
installer tests.
81 changes: 39 additions & 42 deletions labs/15-pitot/pitot/adapters/adapters.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ var (
MainEventName: "tool_call",
Parser: ParserConfig{
CanonicalEvent: []byte(`{"hook_event_name":"tool_call","tool_name":"bash","tool_input":{"command":"git status --short"}}`),
CommandFor: toolInputCommand,
CommandFor: shellCommandForTools("bash"),
ActionKinds: map[string]string{"tool_call": "shell"},
},
Partition: ControlPartition{Controllable: []string{"tool_call"}},
Expand All @@ -142,13 +142,7 @@ var (
MainEventName: "PreToolUse",
Parser: ParserConfig{
CanonicalEvent: []byte(`{"hook_event_name":"PreToolUse","tool_name":"Bash","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 != ""
},
CommandFor: shellCommandForTools("Bash"),
ActionKinds: map[string]string{
"PreToolUse": "shell",
},
Expand All @@ -162,13 +156,7 @@ var (
MainEventName: "PreToolUse",
Parser: ParserConfig{
CanonicalEvent: []byte(`{"hook_event_name":"PreToolUse","tool_name":"Bash","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 != ""
},
CommandFor: shellCommandForTools("Bash"),
ActionKinds: map[string]string{
"PreToolUse": "shell",
},
Expand All @@ -182,13 +170,7 @@ var (
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 != ""
},
CommandFor: shellCommandForTools("run_shell_command"),
ActionKinds: map[string]string{
"BeforeTool": "shell",
},
Expand All @@ -202,13 +184,7 @@ var (
MainEventName: "PreToolUse",
Parser: ParserConfig{
CanonicalEvent: []byte(`{"hook_event_name":"PreToolUse","tool_name":"Bash","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 != ""
},
CommandFor: shellCommandForTools("Bash"),
ActionKinds: map[string]string{
"PreToolUse": "shell",
},
Expand All @@ -222,13 +198,7 @@ var (
MainEventName: "PreToolUse",
Parser: ParserConfig{
CanonicalEvent: []byte(`{"hook_event_name":"PreToolUse","tool_name":"Bash","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 != ""
},
CommandFor: shellCommandForTools("Bash"),
ActionKinds: map[string]string{
"PreToolUse": "shell",
},
Expand All @@ -246,6 +216,24 @@ func toolInputCommand(raw RawHookEvent) (string, bool) {
return value, present && value != ""
}

// shellCommandForTools returns a CommandFor that accepts tool_input.command
// only when the event's tool_name is one of names. Hosts scope their hook to
// the shell tool in their own config; this guard holds the same line inside
// the decoder, so a widened or drifted host matcher cannot promote another
// tool's input to a supervised shell action.
func shellCommandForTools(names ...string) func(RawBoundaryEvent) (string, bool) {
allowed := make(map[string]struct{}, len(names))
for _, name := range names {
allowed[name] = struct{}{}
}
return func(raw RawBoundaryEvent) (string, bool) {
if _, ok := allowed[raw.ToolName]; !ok {
return "", false
}
return toolInputCommand(raw)
}
}

func preToolUseHost() HostConfig {
return HostConfig{
MainEventName: "PreToolUse",
Expand Down Expand Up @@ -463,15 +451,24 @@ func (h Host) HasBoundaryEvent(name string) bool {
// Deprecated: use HasBoundaryEvent.
func (h Host) HasHookEvent(name string) bool { return h.HasBoundaryEvent(name) }

// ActionKind returns the normalized action kind for a boundary event.
// ActionKind returns the normalized action kind for a boundary event. An
// empty boundaryEventName resolves through the host's main boundary event
// (hosts whose payloads omit the discriminator mean their primary boundary).
// An unknown non-empty event returns "" so callers fault instead of silently
// acquiring the shell kind.
func (h Host) ActionKind(boundaryEventName string) string {
registryMu.RLock()
defer registryMu.RUnlock()

if config, exists := registry[h]; exists {
if kind, ok := config.Parser.ActionKinds[boundaryEventName]; ok {
return kind
}
config, exists := registry[h]
if !exists {
return ""
}
if boundaryEventName == "" {
boundaryEventName = config.MainEventName
}
if kind, ok := config.Parser.ActionKinds[boundaryEventName]; ok {
return kind
}
return "shell" // fallback default
return ""
}
48 changes: 48 additions & 0 deletions labs/15-pitot/pitot/adapters/guard_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package adapters

import "testing"

// The decoder holds the same tool-scope line the host matcher promises, so a
// widened or drifted host config cannot promote another tool's input to a
// supervised shell action.
func TestCommandForRejectsNonShellTools(t *testing.T) {
cases := map[Host]struct {
accepted []string
}{
Claude: {accepted: []string{"Bash"}},
Codex: {accepted: []string{"Bash"}},
Kimi: {accepted: []string{"Bash"}},
Opencode: {accepted: []string{"Bash"}},
Gemini: {accepted: []string{"run_shell_command"}},
Pi: {accepted: []string{"bash"}},
Qwen: {accepted: []string{"Bash", "run_shell_command"}},
}
for host, tc := range cases {
for _, tool := range tc.accepted {
raw := RawBoundaryEvent{ToolName: tool, ToolInput: map[string]any{"command": "git status"}}
if command, ok := host.CommandFor(raw); !ok || command != "git status" {
t.Errorf("%s: expected tool %q accepted, got ok=%v", host, tool, ok)
}
}
raw := RawBoundaryEvent{ToolName: "Write", ToolInput: map[string]any{"command": "rm -rf /"}}
if _, ok := host.CommandFor(raw); ok {
t.Errorf("%s: non-shell tool %q must not yield a supervised shell command", host, "Write")
}
}
}

func TestActionKindNeverSilentlyDefaults(t *testing.T) {
if kind := Claude.ActionKind("SomeFutureEvent"); kind != "" {
t.Fatalf("unknown boundary event must have no kind, got %q", kind)
}
// An omitted discriminator means the host's main boundary event.
if kind := Claude.ActionKind(""); kind != "shell" {
t.Fatalf("empty event name should resolve via the main boundary event, got %q", kind)
}
if kind := Cursor.ActionKind("beforeMCPExecution"); kind != "mcp" {
t.Fatalf("registered kinds must be preserved, got %q", kind)
}
if kind := Host("unregistered").ActionKind("PreToolUse"); kind != "" {
t.Fatalf("unregistered host must have no kind, got %q", kind)
}
}
24 changes: 22 additions & 2 deletions labs/15-pitot/pitot/bridge/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,18 @@ func (r Registration) validate() error {
return nil
}

// resolvedWindow bounds the duplicate-detection memory: the Router remembers
// the most recent resolvedWindow resolved action IDs. Action IDs are 16
// crypto-random bytes minted per action, so a duplicate arriving after 4096
// newer actions is not a realistic correlation hazard, and the bound keeps
// long-lived runtimes at constant memory.
const resolvedWindow = 4096

// Router holds at most one Controller registration per request kind.
type Router struct {
registrations map[string]Registration
resolved map[string]struct{}
resolvedOrder []string
mu sync.Mutex
}

Expand All @@ -59,6 +67,18 @@ func NewRouter() *Router {
return &Router{registrations: map[string]Registration{}, resolved: map[string]struct{}{}}
}

// markResolved records actionID in the bounded duplicate-detection window.
// Callers must hold r.mu.
func (r *Router) markResolved(actionID string) {
r.resolved[actionID] = struct{}{}
r.resolvedOrder = append(r.resolvedOrder, actionID)
if len(r.resolvedOrder) > resolvedWindow {
evict := r.resolvedOrder[0]
r.resolvedOrder = r.resolvedOrder[1:]
delete(r.resolved, evict)
}
}

// Register records reg, enforcing the exactly-one-Controller-per-kind rule.
func (r *Router) Register(reg Registration) error {
r.mu.Lock()
Expand Down Expand Up @@ -119,7 +139,7 @@ func (r *Router) Resolve(req schema.ControlRequested, candidate *schema.ControlR
if !ok {
return schema.ControlResponse{}, ErrNoController
}
r.resolved[req.ActionID] = struct{}{}
r.markResolved(req.ActionID)
if candidate == nil {
return r.defaultResponse(reg, req, reg.OnUnavailable), nil
}
Expand Down Expand Up @@ -153,7 +173,7 @@ func (r *Router) TimeoutResponse(req schema.ControlRequested) (schema.ControlRes
if !ok {
return schema.ControlResponse{}, ErrNoController
}
r.resolved[req.ActionID] = struct{}{}
r.markResolved(req.ActionID)
return r.defaultResponse(reg, req, reg.OnTimeout), nil
}

Expand Down
Loading
Loading