-
Notifications
You must be signed in to change notification settings - Fork 4
fix: refuse agent delivery to exited panes #389
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9338,16 +9338,39 @@ export function createServer(opts?: CreateServerOptions): McpServer { | |
| } | ||
| route = reresolved; | ||
| } | ||
| // Agent-path delivery requires a live agent TUI. A crashed CLI leaves its | ||
| // terminal surface alive at a bare shell; typing a routed message there | ||
| // executes fleet text as shell input. Target-scoped discovery validates | ||
| // only this route's stable UUID/ref binding around read-screen, so | ||
| // unrelated pane churn cannot block a healthy relay. Raw | ||
| // surface/command/key modes bypass this helper and remain available for | ||
| // deliberate recovery. | ||
| const assertAgentRouteHasTui = async (candidateRoute: typeof route) => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 High The final 🚀 Reply "fix it for me" or copy this AI Prompt for your agent: |
||
| const freshOccupant = await discovery.scanTarget(candidateRoute); | ||
| if ( | ||
| freshOccupant && | ||
| !freshOccupant.read_error && | ||
| freshOccupant.control_state === "shell" | ||
|
Comment on lines
+9350
to
+9353
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an exited CLI leaves recognizable TUI text in the last 30 screen lines—for example, Useful? React with 👍 / 👎. |
||
| ) { | ||
| throw new Error( | ||
| `Agent "${args.agent_id}" exited / no agent currently initiated on ` + | ||
| `surface ${candidateRoute.surface_id} (control_state=${freshOccupant.control_state}, ` + | ||
| `agent_type=${freshOccupant.cli}); refusing routed agent delivery. ` + | ||
| `Use send_to mode=surface, command, or key for deliberate raw terminal input.`, | ||
| ); | ||
| } | ||
| return freshOccupant; | ||
| }; | ||
| const freshOccupant = await assertAgentRouteHasTui(route); | ||
|
|
||
| // Identity guard: a live surface ref may have been RECYCLED — a crashed | ||
| // agent's pane reused by a different agent. If the live surface now hosts | ||
| // a known CLI that differs from this agent's recorded CLI, refuse rather | ||
| // than delivering to the new occupant. Fails OPEN when the live CLI is | ||
| // unknown/unreadable so a parse miss never blocks a healthy relay. | ||
| // than delivering to the new occupant. Fresh shell evidence was already | ||
| // refused above; other unknown/unreadable evidence remains inconclusive. | ||
| const expectedCli = engine.getAgentState(args.agent_id)?.cli; | ||
| if (requiresMutableRefGuards && expectedCli) { | ||
| const cachedOccupant = (await discovery.scan(false)).find( | ||
| (entry) => entry.surface_id === route.surface_id, | ||
| ); | ||
| const cachedOccupant = freshOccupant; | ||
| const isForeign = (occ: typeof cachedOccupant): boolean => | ||
| Boolean( | ||
| occ && | ||
|
|
@@ -9357,13 +9380,10 @@ export function createServer(opts?: CreateServerOptions): McpServer { | |
| occ.cli !== expectedCli, | ||
| ); | ||
| if (isForeign(cachedOccupant)) { | ||
| // Confirm against a FRESH scan before refusing. discovery.scan(false) | ||
| // serves a 2s cache that can predate the current occupant; refusing | ||
| // on it alone would false-refuse a healthy relay. | ||
| discovery.invalidate(); | ||
| const freshOccupant = (await discovery.scan(true)).find( | ||
| (entry) => entry.surface_id === route.surface_id, | ||
| ); | ||
| // Confirm against another target-scoped fresh read before refusing; | ||
| // one parse alone can be transient, while a fleet-wide scan would | ||
| // couple this route to unrelated pane churn. | ||
| const freshOccupant = await discovery.scanTarget(route); | ||
| if (isForeign(freshOccupant)) { | ||
| throw new Error( | ||
| `Agent "${args.agent_id}" (${expectedCli}) no longer occupies ` + | ||
|
|
@@ -9407,6 +9427,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { | |
| // landed, following a moved UUID would split one logical message across | ||
| // terminals, so route changes fail closed instead. | ||
| route = await engine.resolveAgentIoRoute(args.agent_id); | ||
| await assertAgentRouteHasTui(route); | ||
|
Comment on lines
9429
to
+9430
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This final shell check still occurs before Useful? React with 👍 / 👎. |
||
| const deliveryRoute = route; | ||
| const assertDeliveryRouteCurrent = async (): Promise<void> => { | ||
| const current = await engine.resolveAgentIoRoute(args.agent_id); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟠 High
src/agent-discovery.ts:178scanTargetvalidates surface identity bysurface.refwhentarget.surface_uuidis absent, butrefis mutable and can be recycled to a different UUID while keeping the same ref and workspace. If the surface is rebound duringscanSurface, the validation passes and returns stale screen evidence for the old occupant — which can route keystrokes to the new occupant. Compare the initial and completed stablesurface.idvalues whenever either side provides a UUID, not just whentarget.surface_uuidis present.🚀 Reply "fix it for me" or copy this AI Prompt for your agent: