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
10 changes: 9 additions & 1 deletion UPSTREAM.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
"runtime/capabilities.go": "76c27bdd7ddfa11a1d639915ac2eb2d573b7ccdd687a5f548b3731b7c1e1828f",
"runtime/descriptor_unix.go": "df41b6867e9840933f186c93b6bc61861e7ea5252a365455686ea0414bfa0044",
"runtime/descriptor_windows.go": "2d9ffefe3af0154fa8042de6b67460d4e86dd3f4cdd9e986f180f7d0c535c9a5",
"runtime/request.go": "198c44fd6c547022a15b6d0d48e4d0130fa8afb687994365115576e4d874550d",
"runtime/request_test.go": "86d8a2feb4ec72e8ed675b9567da2d1f5d628950eeec907c10b9cc1675aa1904",
"runtime/runtime.go": "e0624a16ac9f79e8080246042912c73b1588f1b9aeb4f9f95b3711c7acef7e81",
"runtime/runtime_test.go": "afd78d122af20148bf30d0db873ff002544189df0dfec0f6167b8cf5cd0d42b1",
"runtime/transport.go": "83e2218fb28474e875dafa6943bc5b665acef0565aaf5955fa88b1b4fd21614e",
Expand Down Expand Up @@ -115,5 +117,11 @@
"windtunnel/doc.go": "44e0bcde632da73e1f8b98beade3a34ca8e0d0ea79cdfb91d131de290b164fc4",
"windtunnel/windtunnel_test.go": "d34929ffdb1927b2ee27cc79640b333b0571659d231977b91c3a8898cf79bc42"
},
"schema_version": 1
"generator": "operatorstack/pitot:project",
"schema_version": 1,
"source": {
"commit": "f3e44076ff395a608f025c7162627e44e795f55f",
"path": "labs/15-pitot",
"repository": "operatorstack/intelligence-flow"
}
}
24 changes: 24 additions & 0 deletions adapter-verification.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"schema_version": 6,
"capabilities": [
{"id": "hook_control", "matrix": "agent_platform"},
{"id": "consumer_delivery", "matrix": "agent_platform"},
{"id": "explicit_request", "matrix": "platform"}
],
"platforms": [
{"id": "ubuntu", "runner": "ubuntu-latest"},
{"id": "macos", "runner": "macos-latest"},
{"id": "windows", "runner": "windows-latest"}
],
"agents": [
{"id": "claude", "label": "Claude", "version": "2.1.217", "executable": "claude", "installer": {"kind": "npm", "package": "@anthropic-ai/claude-code"}, "integration": "native_command_hook", "artifacts": [], "runtime": {"ubuntu": "native", "macos": "native", "windows": "native"}, "driver": "real_agent_driver.py", "required_mode": "real_cli"},
{"id": "cursor", "label": "Cursor", "version": "2026.07.20-8cc9c0b", "executable": "agent", "installer": {"kind": "cursor_release", "package": "https://downloads.cursor.com/lab/2026.07.20-8cc9c0b"}, "integration": "native_command_hook", "artifacts": ["integrations/cursor/beforeShellExecution"], "runtime": {"ubuntu": "native", "macos": "native", "windows": "wsl"}, "driver": "real_agent_driver.py", "required_mode": "real_cli"},
{"id": "codex", "label": "Codex", "version": "0.145.0", "executable": "codex", "installer": {"kind": "npm", "package": "@openai/codex"}, "integration": "native_command_hook", "artifacts": ["integrations/codex/PreToolUse.ps1"], "runtime": {"ubuntu": "native", "macos": "native", "windows": "native"}, "driver": "real_agent_driver.py", "required_mode": "real_cli"},
{"id": "copilot", "label": "GitHub Copilot CLI", "version": "1.0.73", "executable": "copilot", "installer": {"kind": "npm", "package": "@github/copilot"}, "integration": "native_command_hook", "artifacts": ["integrations/copilot/PreToolUse", "integrations/copilot/PreToolUse.ps1"], "runtime": {"ubuntu": "native", "macos": "native", "windows": "native"}, "driver": "real_agent_driver.py", "required_mode": "real_cli"},
{"id": "gemini", "label": "Gemini", "version": "0.51.0", "executable": "gemini", "installer": {"kind": "npm", "package": "@google/gemini-cli"}, "integration": "native_command_hook", "artifacts": ["integrations/gemini/BeforeTool", "integrations/gemini/BeforeTool.ps1"], "runtime": {"ubuntu": "native", "macos": "native", "windows": "native"}, "driver": "real_agent_driver.py", "required_mode": "real_cli"},
{"id": "kimi", "label": "Kimi Code", "version": "0.29.0", "executable": "kimi", "installer": {"kind": "kimi_release", "package": "https://code.kimi.com/kimi-code"}, "integration": "native_command_hook", "artifacts": [], "runtime": {"ubuntu": "native", "macos": "native", "windows": "native"}, "driver": "real_agent_driver.py", "required_mode": "real_cli"},
{"id": "opencode", "label": "OpenCode", "version": "1.18.4", "executable": "opencode", "installer": {"kind": "npm", "package": "opencode-ai"}, "integration": "opencode_plugin", "artifacts": ["integrations/opencode/pitot.ts"], "runtime": {"ubuntu": "native", "macos": "native", "windows": "native"}, "driver": "real_agent_driver.py", "required_mode": "real_cli"},
{"id": "pi", "label": "Pi", "version": "0.81.1", "executable": "pi", "installer": {"kind": "npm", "package": "@earendil-works/pi-coding-agent"}, "integration": "pi_extension", "artifacts": ["integrations/pi/pitot.ts"], "runtime": {"ubuntu": "native", "macos": "native", "windows": "native"}, "driver": "real_agent_driver.py", "required_mode": "real_cli"},
{"id": "qwen", "label": "Qwen Code", "version": "0.20.1", "executable": "qwen", "installer": {"kind": "npm", "package": "@qwen-code/qwen-code"}, "integration": "native_command_hook", "artifacts": ["integrations/qwen/PreToolUse", "integrations/qwen/PreToolUse.cjs"], "runtime": {"ubuntu": "native", "macos": "native", "windows": "native"}, "driver": "real_agent_driver.py", "required_mode": "real_cli"}
]
}
39 changes: 39 additions & 0 deletions integrations/codex/PreToolUse.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
param(
[Parameter(Mandatory = $true)][string]$Pitot,
[Parameter(Mandatory = $true)][string]$RealBin,
[Parameter(Mandatory = $true)][string]$Receipt,
[Parameter(Mandatory = $true)][string]$Nonce,
[Parameter(Mandatory = $true)][string]$Runtime
)

$payload = [Console]::In.ReadToEnd()
$OutputEncoding = [Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)
$arguments = @("--real-bin", $RealBin, "--receipt", $Receipt, "--nonce", $Nonce, "hook", "codex", "--runtime", $Runtime)
$startInfo = New-Object System.Diagnostics.ProcessStartInfo
$startInfo.FileName = $Pitot
$startInfo.Arguments = (($arguments | ForEach-Object { '"' + $_.Replace('"', '\"') + '"' }) -join ' ')
$startInfo.UseShellExecute = $false
$startInfo.CreateNoWindow = $true
$startInfo.RedirectStandardInput = $true
$startInfo.StandardInputEncoding = [Text.UTF8Encoding]::new($false)
$startInfo.RedirectStandardOutput = $true
$startInfo.RedirectStandardError = $true
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $startInfo
[void]$process.Start()
$payloadBytes = [Text.UTF8Encoding]::new($false).GetBytes($payload)
$stdin = $process.StandardInput.BaseStream
$stdin.Write($payloadBytes, 0, $payloadBytes.Length)
$stdin.Close()
$stdout = $process.StandardOutput.ReadToEnd()
$stderr = $process.StandardError.ReadToEnd()
$process.WaitForExit()
$pitotOutput = ($stdout + $stderr).Trim()
if ($process.ExitCode -eq 0) {
exit 0
}

if (-not $pitotOutput) { $pitotOutput = "Pitot rejected the shell request" }
if ($pitotOutput.Length -gt 1024) { $pitotOutput = $pitotOutput.Substring(0, 1024) }
@{ decision = "block"; reason = $pitotOutput } | ConvertTo-Json -Compress
exit 0
12 changes: 12 additions & 0 deletions integrations/copilot/PreToolUse
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -uo pipefail

PITOT_COMMAND="${PITOT_BIN:-pitot}"
PAYLOAD=$(cat)
if PITOT_ERROR=$(printf '%s' "$PAYLOAD" | "$PITOT_COMMAND" hook copilot 2>&1 >/dev/null); then
printf '%s\n' '{"permissionDecision":"allow","permissionDecisionReason":"Pitot accepted the shell action"}'
exit 0
fi

python3 -c 'import json,sys; print(json.dumps({"permissionDecision":"deny","permissionDecisionReason":(sys.argv[1] or "Pitot rejected the shell request")[:1024]},separators=(",",":")))' "$PITOT_ERROR"
exit 0
31 changes: 31 additions & 0 deletions integrations/copilot/PreToolUse.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
$payload = [Console]::In.ReadToEnd()
$OutputEncoding = [Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)
$pitot = if ($env:PITOT_BIN) { $env:PITOT_BIN } else { "pitot" }
$startInfo = New-Object System.Diagnostics.ProcessStartInfo
$startInfo.FileName = $pitot
$startInfo.Arguments = '"hook" "copilot"'
$startInfo.UseShellExecute = $false
$startInfo.CreateNoWindow = $true
$startInfo.RedirectStandardInput = $true
$startInfo.StandardInputEncoding = [Text.UTF8Encoding]::new($false)
$startInfo.RedirectStandardOutput = $true
$startInfo.RedirectStandardError = $true
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $startInfo
[void]$process.Start()
$payloadBytes = [Text.UTF8Encoding]::new($false).GetBytes($payload)
$stdin = $process.StandardInput.BaseStream
$stdin.Write($payloadBytes, 0, $payloadBytes.Length)
$stdin.Close()
$stdout = $process.StandardOutput.ReadToEnd()
$stderr = $process.StandardError.ReadToEnd()
$process.WaitForExit()
$pitotOutput = ($stdout + $stderr).Trim()
if ($process.ExitCode -eq 0) {
@{ permissionDecision = "allow"; permissionDecisionReason = "Pitot accepted the shell action" } | ConvertTo-Json -Compress
} else {
if (-not $pitotOutput) { $pitotOutput = "Pitot rejected the shell request" }
if ($pitotOutput.Length -gt 1024) { $pitotOutput = $pitotOutput.Substring(0, 1024) }
@{ permissionDecision = "deny"; permissionDecisionReason = $pitotOutput } | ConvertTo-Json -Compress
}
exit 0
19 changes: 19 additions & 0 deletions integrations/cursor/beforeShellExecution
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -uo pipefail

PITOT_COMMAND="${1:-${PITOT_BIN:-pitot}}"
PITOT_ARGUMENTS=()
if [ "$#" -ge 5 ]; then
PITOT_ARGUMENTS=(--real-bin "$2" --receipt "$3" --nonce "$4")
RUNTIME_ARGUMENTS=(--runtime "$5")
else
RUNTIME_ARGUMENTS=()
fi
PAYLOAD=$(cat)
if PITOT_ERROR=$(printf '%s' "$PAYLOAD" | "$PITOT_COMMAND" "${PITOT_ARGUMENTS[@]}" hook cursor "${RUNTIME_ARGUMENTS[@]}" 2>&1 >/dev/null); then
printf '%s\n' '{"continue":true,"permission":"allow"}'
exit 0
fi

python3 -c 'import json,sys; reason=(sys.argv[1] or "Pitot rejected the shell request")[:1024]; print(json.dumps({"continue":True,"permission":"deny","user_message":reason,"agent_message":reason},separators=(",",":")))' "$PITOT_ERROR"
exit 0
19 changes: 19 additions & 0 deletions integrations/gemini/BeforeTool
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -uo pipefail

PITOT_COMMAND="${1:-${PITOT_BIN:-pitot}}"
PITOT_ARGUMENTS=()
if [ "$#" -ge 5 ]; then
PITOT_ARGUMENTS=(--real-bin "$2" --receipt "$3" --nonce "$4")
RUNTIME_ARGUMENTS=(--runtime "$5")
else
RUNTIME_ARGUMENTS=()
fi
PAYLOAD=$(cat)
if PITOT_ERROR=$(printf '%s' "$PAYLOAD" | "$PITOT_COMMAND" "${PITOT_ARGUMENTS[@]}" hook gemini "${RUNTIME_ARGUMENTS[@]}" 2>&1 >/dev/null); then
printf '%s\n' '{"decision":"allow"}'
exit 0
fi

python3 -c 'import json,sys; print(json.dumps({"decision":"deny","reason":(sys.argv[1] or "Pitot rejected the shell request")[:1024]},separators=(",",":")))' "$PITOT_ERROR"
exit 0
41 changes: 41 additions & 0 deletions integrations/gemini/BeforeTool.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
param(
[string]$Pitot = $(if ($env:PITOT_BIN) { $env:PITOT_BIN } else { "pitot" }),
[string]$RealBin = "",
[string]$Receipt = "",
[string]$Nonce = "",
[string]$Runtime = ""
)
$payload = [Console]::In.ReadToEnd()
$OutputEncoding = [Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)
$arguments = @()
if ($RealBin) { $arguments += @("--real-bin", $RealBin, "--receipt", $Receipt, "--nonce", $Nonce) }
$arguments += @("hook", "gemini")
if ($Runtime) { $arguments += @("--runtime", $Runtime) }
$startInfo = New-Object System.Diagnostics.ProcessStartInfo
$startInfo.FileName = $Pitot
$startInfo.Arguments = (($arguments | ForEach-Object { '"' + $_.Replace('"', '\"') + '"' }) -join ' ')
$startInfo.UseShellExecute = $false
$startInfo.CreateNoWindow = $true
$startInfo.RedirectStandardInput = $true
$startInfo.StandardInputEncoding = [Text.UTF8Encoding]::new($false)
$startInfo.RedirectStandardOutput = $true
$startInfo.RedirectStandardError = $true
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $startInfo
[void]$process.Start()
$payloadBytes = [Text.UTF8Encoding]::new($false).GetBytes($payload)
$stdin = $process.StandardInput.BaseStream
$stdin.Write($payloadBytes, 0, $payloadBytes.Length)
$stdin.Close()
$stdout = $process.StandardOutput.ReadToEnd()
$stderr = $process.StandardError.ReadToEnd()
$process.WaitForExit()
$pitotOutput = ($stdout + $stderr).Trim()
if ($process.ExitCode -eq 0) {
@{ decision = "allow" } | ConvertTo-Json -Compress
} else {
if (-not $pitotOutput) { $pitotOutput = "Pitot rejected the shell request" }
if ($pitotOutput.Length -gt 1024) { $pitotOutput = $pitotOutput.Substring(0, 1024) }
@{ decision = "deny"; reason = $pitotOutput } | ConvertTo-Json -Compress
}
exit 0
25 changes: 25 additions & 0 deletions integrations/opencode/pitot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { spawnSync } from "node:child_process";

// OpenCode runs plugins in-process. This is the genuine synchronous
// tool.execute.before boundary; it is not a Claude PreToolUse simulation.
export const PitotPlugin = async () => ({
"tool.execute.before": async (input, output) => {
if (input.tool !== "bash") return;
const command = output.args?.command;
const payload = JSON.stringify({
hook_event_name: "PreToolUse",
tool_name: "Bash",
tool_input: { command: typeof command === "string" ? command : "" },
});
const result = spawnSync(process.env.PITOT_BIN || "pitot", ["hook", "opencode"], {
input: payload,
encoding: "utf8",
maxBuffer: 1024 * 1024,
});
if (result.status !== 0) {
throw new Error((result.stderr || "Pitot rejected the shell request").trim());
}
},
});

export default PitotPlugin;
25 changes: 25 additions & 0 deletions integrations/pi/pitot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { spawnSync } from "node:child_process";

// handleToolCall is exported so the shipped boundary can be tested without a
// live Pi session. Pi itself calls the default extension registration below.
export function handleToolCall(event, run = spawnSync) {
if (event.toolName !== "bash") return undefined;

const payload = JSON.stringify({
hook_event_name: "tool_call",
tool_name: "bash",
tool_input: { command: event.input?.command ?? "" },
});
const result = run(process.env.PITOT_BIN || "pitot", ["hook", "pi"], {
input: payload,
encoding: "utf8",
maxBuffer: 1024 * 1024,
});
if (result.status === 0) return undefined;
const reason = (result.stderr || "Pitot rejected the shell request").trim();
return { block: true, reason: reason.slice(0, 1024) };
}

export default function pitotExtension(pi) {
pi.on("tool_call", async (event) => handleToolCall(event));
}
19 changes: 19 additions & 0 deletions integrations/qwen/PreToolUse
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -uo pipefail

PITOT_COMMAND="${1:-${PITOT_BIN:-pitot}}"
PITOT_ARGUMENTS=()
if [ "$#" -ge 5 ]; then
PITOT_ARGUMENTS=(--real-bin "$2" --receipt "$3" --nonce "$4")
RUNTIME_ARGUMENTS=(--runtime "$5")
else
RUNTIME_ARGUMENTS=()
fi
PAYLOAD=$(cat)
if PITOT_ERROR=$(printf '%s' "$PAYLOAD" | "$PITOT_COMMAND" "${PITOT_ARGUMENTS[@]}" hook qwen "${RUNTIME_ARGUMENTS[@]}" 2>&1 >/dev/null); then
printf '%s\n' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"Pitot accepted the shell action"}}'
exit 0
fi

python3 -c 'import json,sys; print(json.dumps({"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":(sys.argv[1] or "Pitot rejected the shell request")[:1024]}},separators=(",",":")))' "$PITOT_ERROR"
exit 0
27 changes: 27 additions & 0 deletions integrations/qwen/PreToolUse.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env node
"use strict";

const { spawnSync } = require("node:child_process");
const fs = require("node:fs");

const supplied = process.argv.slice(2);
const pitot = supplied[0] || process.env.PITOT_BIN || "pitot";
const pitotArgs = supplied.length >= 5
? ["--real-bin", supplied[1], "--receipt", supplied[2], "--nonce", supplied[3], "hook", "qwen", "--runtime", supplied[4]]
: ["hook", "qwen"];
const payload = fs.readFileSync(0);
const result = spawnSync(pitot, pitotArgs, { input: payload, encoding: "utf8", windowsHide: true });
const detail = `${result.stdout || ""}${result.stderr || ""}`.trim();
const allowed = result.status === 0 && !result.error;
const reason = allowed
? "Pitot accepted the shell action"
: (detail || result.error?.message || "Pitot rejected the shell request").slice(0, 1024);

process.stdout.write(JSON.stringify({
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: allowed ? "allow" : "deny",
permissionDecisionReason: reason,
},
}) + "\n");

60 changes: 60 additions & 0 deletions runtime/request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package runtime

// Typed request construction. A caller issuing an explicit control request would
// otherwise hand-build the control.requested envelope: set the version and type
// discriminators, mint a correlation id, and marshal the structured body into the
// raw `data` field itself. NewControlRequest and RequestTyped remove that JSON
// boundary — the caller passes a typed payload and gets a correlated response.

import (
"context"
"encoding/json"
"errors"
"fmt"

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

// NewControlRequest builds a control.requested envelope for the given request
// kind, marshaling payload into the data field. It stamps the current envelope
// version and control.requested type and mints an unpredictable correlation
// ActionID, so a caller never hand-writes the wire JSON or the boilerplate. A nil
// payload produces an empty data field (a request that carries no structured
// body); a payload that cannot be marshaled is an error.
func NewControlRequest(kind string, payload any) (schema.ControlRequested, error) {
if kind == "" {
return schema.ControlRequested{}, errors.New("pitot: control request kind is required")
}
var data json.RawMessage
if payload != nil {
encoded, err := json.Marshal(payload)
if err != nil {
return schema.ControlRequested{}, fmt.Errorf("pitot: marshal control request data: %w", err)
}
data = encoded
}
actionID, err := NewActionID()
if err != nil {
return schema.ControlRequested{}, err
}
return schema.ControlRequested{
PitotVersion: schema.Version,
Type: schema.TypeControlRequested,
Kind: kind,
ActionID: actionID,
Data: data,
}, nil
}

// RequestTyped builds a typed control request for kind and issues it, returning
// the correlated response. It is the typed convenience over Request: the caller
// supplies a structured payload rather than a pre-framed envelope with raw JSON.
// All of Request's guarantees still apply — correlation, the client deadline, and
// the runtime's fail-closed unavailable/timeout defaults.
func (c *Client) RequestTyped(ctx context.Context, kind string, payload any) (schema.ControlResponse, error) {
req, err := NewControlRequest(kind, payload)
if err != nil {
return schema.ControlResponse{}, err
}
return c.Request(ctx, req)
}
Loading
Loading