fix(pi): fail closed on oversized post-tool hook errors#1037
Conversation
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
Greptile SummaryThis PR hardens the Pi harness managed extension to fail closed when the Guard subprocess errors or times out, and adds payload bounding before Guard is invoked so oversized post-tool output cannot cause the hook to stall or be bypassed.
Confidence Score: 4/5The fail-closed and payload bounding changes are sound and correct; the remaining concern is the Pi hook model using a patched tool result rather than a native abort, which is an API-level limitation that neither this PR nor previous comments have resolved. The core fail-closed logic in runGuard is correctly implemented: serialization errors, subprocess timeouts (ETIMEDOUT and TimeoutError name are both checked), and non-zero exits all now deny instead of allow. The bounding helpers (boundValue, boundedOutputText) are well-structured — bigint conversion, cycle detection via WeakSet with RAII delete, undefined treated as non-truncated, and unknown types treated as truncated are all handled correctly. The _coalesce_string → _optional_string fix removes a subtle bug where a missing stdout field would cause the literal string 'unknown-artifact' to be appended to the text analyzed by classify_secret_content. The blockedToolResults map correctly uses toolCallIdKey validation and deletes entries in message_end after rewriting. The one open structural gap is that tool_call (PreToolUse) and input (UserPromptSubmit) handlers still send raw unbounded payloads to runGuard — oversized pre-tool arguments will time out and deny (fail closed via the new error handler), but there is no proactive bounding before serialization as there is for tool_result. src/codex_plugin_scanner/guard/adapters/pi_support.py — the generated TypeScript extension is the main change and warrants the most scrutiny; the tool_call handler has no bounding symmetry with the new tool_result handler. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Pi as Pi Runtime
participant Ext as Managed Extension (hol-guard.ts)
participant Guard as hol-guard subprocess
Pi->>Ext: tool_result event (event.content, event.toolCallId)
Ext->>Ext: boundValue(toolInput) → boundedToolInput
Ext->>Ext: boundValue(event.content) → boundedContent
Ext->>Ext: boundedOutputText(event.content) → boundedStdout
alt "any .truncated === true"
Ext->>Ext: store reason in blockedToolResults[toolCallId]
Ext-->>Pi: "blockedToolResult(reason) isError=true"
else all within limits
Ext->>Ext: build PostToolUse payload (tool_response, stdout)
Ext->>Ext: JSON.stringify(payload)
alt serialization fails
Ext-->>Pi: deny
else "payload > 24K and stdout removable"
Ext->>Ext: drop stdout, re-serialize
end
alt "still > 24K"
Ext-->>Pi: deny (size cap)
else
Ext->>Guard: "spawnSync(hol-guard, payload, timeout=10s)"
alt result.error (ETIMEDOUT / other)
Guard-->>Ext: error
Ext-->>Pi: deny (fail closed)
else non-zero exit
Guard-->>Ext: stderr / status
Ext-->>Pi: deny
else allow
Guard-->>Ext: allow
Ext-->>Pi: undefined (continue)
else deny
Guard-->>Ext: deny + reason
Ext->>Ext: store reason in blockedToolResults[toolCallId]
Ext-->>Pi: "blockedToolResult(reason) isError=true"
end
end
end
Pi->>Ext: "message_end (role=toolResult, toolCallId)"
alt toolCallId in blockedToolResults
Ext->>Ext: blockedToolResults.delete(toolCallId)
Ext-->>Pi: "rewrite message with block reason, isError=true"
else
Ext-->>Pi: undefined (no change)
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Pi as Pi Runtime
participant Ext as Managed Extension (hol-guard.ts)
participant Guard as hol-guard subprocess
Pi->>Ext: tool_result event (event.content, event.toolCallId)
Ext->>Ext: boundValue(toolInput) → boundedToolInput
Ext->>Ext: boundValue(event.content) → boundedContent
Ext->>Ext: boundedOutputText(event.content) → boundedStdout
alt "any .truncated === true"
Ext->>Ext: store reason in blockedToolResults[toolCallId]
Ext-->>Pi: "blockedToolResult(reason) isError=true"
else all within limits
Ext->>Ext: build PostToolUse payload (tool_response, stdout)
Ext->>Ext: JSON.stringify(payload)
alt serialization fails
Ext-->>Pi: deny
else "payload > 24K and stdout removable"
Ext->>Ext: drop stdout, re-serialize
end
alt "still > 24K"
Ext-->>Pi: deny (size cap)
else
Ext->>Guard: "spawnSync(hol-guard, payload, timeout=10s)"
alt result.error (ETIMEDOUT / other)
Guard-->>Ext: error
Ext-->>Pi: deny (fail closed)
else non-zero exit
Guard-->>Ext: stderr / status
Ext-->>Pi: deny
else allow
Guard-->>Ext: allow
Ext-->>Pi: undefined (continue)
else deny
Guard-->>Ext: deny + reason
Ext->>Ext: store reason in blockedToolResults[toolCallId]
Ext-->>Pi: "blockedToolResult(reason) isError=true"
end
end
end
Pi->>Ext: "message_end (role=toolResult, toolCallId)"
alt toolCallId in blockedToolResults
Ext->>Ext: blockedToolResults.delete(toolCallId)
Ext-->>Pi: "rewrite message with block reason, isError=true"
else
Ext-->>Pi: undefined (no change)
end
Reviews (7): Last reviewed commit: "docs(guard): sync harness support surfac..." | Re-trigger Greptile |
e6f2f38 to
524f5f0
Compare
Signed-off-by: internet-dot <207546839+internet-dot@users.noreply.github.com>
524f5f0 to
9357c4d
Compare
Signed-off-by: internet-dot <207546839+internet-dot@users.noreply.github.com>
Signed-off-by: internet-dot <207546839+internet-dot@users.noreply.github.com>
Signed-off-by: internet-dot <207546839+internet-dot@users.noreply.github.com>
Signed-off-by: internet-dot <207546839+internet-dot@users.noreply.github.com>
Signed-off-by: internet-dot <207546839+internet-dot@users.noreply.github.com>
Summary
Testing
python3 -m pytest tests/test_pi_adapter.py -qpython3 -m ruff check src/codex_plugin_scanner/guard/adapters/pi_support.py tests/test_pi_adapter.py