fix(contracts): add isWorkspaceEvent guard + close routeLiveInteractionEvent exhaustiveness gap (#2878)

Fixes two contract violations found in audit (closes #2875):

1. `isWorkspaceEvent()` type guard added next to WorkspaceEvent type definition.
   Applied at stream.onmessage JSON.parse boundary — replaces unsafe `as WorkspaceEvent`
   cast with validated parse + explicit error path for malformed payloads.

2. `routeLiveInteractionEvent()` switch extended with explicit cases for all three
   previously unhandled WorkspaceEvent variants:
   - bridge_status: handled upstream with early return, never reaches router
   - live_state_invalidation: handled upstream via handleLiveStateInvalidation
   - extension_error: terminal line produced by summarizeEvent, no live state update needed
This commit is contained in:
Jeremy McSpadden 2026-03-27 15:29:38 -05:00 committed by GitHub
parent 666731f56d
commit 2175f59522

View file

@ -470,6 +470,10 @@ export type WorkspaceEvent =
| TurnEndEvent
| ({ type: Exclude<string, "bridge_status" | "live_state_invalidation" | "extension_ui_request" | "extension_error" | "message_update" | "tool_execution_start" | "tool_execution_end" | "agent_end" | "turn_end">; [key: string]: unknown } & Record<string, unknown>)
export function isWorkspaceEvent(value: unknown): value is WorkspaceEvent {
return value !== null && typeof value === "object" && typeof (value as Record<string, unknown>).type === "string"
}
export interface WorkspaceCommandResponse {
type: "response"
command: string
@ -4866,8 +4870,15 @@ export class GSDWorkspaceStore {
stream.onmessage = (message) => {
try {
const payload = JSON.parse(message.data) as WorkspaceEvent
this.handleEvent(payload)
const parsed: unknown = JSON.parse(message.data)
if (!isWorkspaceEvent(parsed)) {
this.patchState({
lastClientError: "Malformed event received from stream",
terminalLines: withTerminalLine(this.state.terminalLines, createTerminalLine("error", "Malformed event received from stream")),
})
return
}
this.handleEvent(parsed)
} catch (error) {
const text = normalizeClientError(error)
this.patchState({
@ -4945,6 +4956,15 @@ export class GSDWorkspaceStore {
case "tool_execution_end":
this.handleToolExecutionEnd(event as ToolExecutionEndEvent)
break
case "bridge_status":
// Handled upstream in handleEvent with early return — never reaches here
break
case "live_state_invalidation":
// Handled upstream in handleEvent via handleLiveStateInvalidation — no live interaction state update needed
break
case "extension_error":
// Terminal line produced by summarizeEvent — no live interaction state update needed
break
}
}