16 KiB
TECH.md — Linear deeplink must not silently auto-submit prompts into the agent
GitHub Issue: warpdotdev/warp-external#703
Product Spec: specs/GH703/product.md
Context
warp://linear/work?prompt=<PROMPT> is a trusted warp:// URI. The handler decodes the prompt query parameter and feeds it into a new agent conversation. A shortcut in try_enter_agent_view today promotes any origin with an initial_prompt to auto-submit when the focused terminal was already in fullscreen agent view, which causes the Linear-supplied prompt to be sent to the LLM without any user interaction. This is contrary to the intent encoded in AgentViewEntryOrigin::should_autotrigger_request, which deliberately excludes LinearDeepLink.
The relevant code paths today:
app/src/uri/mod.rs:429-443—UriHost::Linearmatch arm parsesLinearAction::WorkOnIssue, buildsLinearIssueWork, and dispatches into the new-or-existing window action.app/src/linear.rs:30-38—LinearIssueWork::from_urlreadspromptfrom the URL with no validation beyond "non-empty".app/src/root_view.rs:1107-1119,2875-2891—open_linear_issue_work_in_{new,existing}_windowforwardsLinearIssueWorktoworkspace.open_linear_issue_work(...).app/src/workspace/view.rs:15888-15937—open_linear_issue_workopens a new tab and callsterminal_view.enter_agent_view_for_new_conversation(prompt, AgentViewEntryOrigin::LinearDeepLink, ctx).app/src/terminal/view/agent_view.rs:48-83—enter_agent_view_for_new_conversationdelegates totry_enter_agent_view.app/src/terminal/view/agent_view.rs:160-290—try_enter_agent_viewcaptureswas_in_agent_view_alreadyand runsif origin.should_autotrigger_request() || was_in_agent_view_already { send_user_query_in_conversation(...) }. This is where the auto-submit occurs forLinearDeepLink.app/src/ai/blocklist/agent_view/controller.rs:172-205—AgentViewEntryOrigin::LinearDeepLinkis defined andshould_autotrigger_requestintentionally returnsfalsefor it.app/src/uri/uri_test.rs:293-347— existing unit tests covering the Linear URI parsing surface.
The ephemeral "enter again to send" path already exists in the else arm of try_enter_agent_view (app/src/terminal/view/agent_view.rs:240-266): it populates the agent input buffer via self.input.update(...).replace_buffer_content(&initial_prompt, ctx) and shows an ephemeral message tagged ENTER_AGAIN_TO_SEND_MESSAGE_ID. That is the desired user experience for Linear-originated prompts.
Product invariants 1–13 in specs/GH703/product.md define the required behavior.
Proposed changes
1. Remove the implicit was_in_agent_view_already shortcut (committed decision)
try_enter_agent_view's auto-submit condition is the root cause. We make the auto-submit decision a function of the origin alone — the was_in_agent_view_already shortcut is deleted outright. This was evaluated against two options during spec review; we commit to option (a):
- (a) chosen: Only
origin.should_autotrigger_request()governs auto-submit. The prior shortcut existed to preserve "typed a new prompt in agent view and pressed Cmd+Enter" ergonomics, but that flow already enters viaAgentViewEntryOrigin::Input { was_prompt_autodetected: true }(in the allowlist) orAgentViewEntryOrigin::Keybinding/SlashCommandpaths. Any origin that legitimately needs to auto-submit when the user is already in agent view must be added toshould_autotrigger_requestexplicitly. - (b) rejected: gating the shortcut on a new
is_trusted_user_originatedpredicate keeps the footgun. If a futureAgentViewEntryOrigingets added and forgets to opt out of the predicate, the URI-injection bug reopens. Option (a) removes the whole class of problem. The concrete change replaces:
if origin.should_autotrigger_request() || was_in_agent_view_already {
// ...send to LLM
}
with a single check whose result depends only on the origin and the caller's explicit AutoSubmitPolicy (see step 3).
Audit of other origins (in-scope, committed deliverable). Every AgentViewEntryOrigin variant was enumerated against should_autotrigger_request's allowlist. The allowlist intentionally contains only: Input { was_prompt_autodetected: true }, SlashCommand { trigger: !is_keybinding() }, Cli, AcceptedPromptSuggestion. Every other variant — including CodexModal, CloudAgent, ChildAgent, ProjectEntry, OnboardingCallout, LinearDeepLink, AgentViewBlock, AgentRequestedNewConversation, SharedSessionSelection, RestoreExistingConversation, InlineCodeReview, ConversationSelector, AgentModeHomepage, AIDocument, AutoFollowUp, AcceptedUnitTestSuggestion, AcceptedPassiveCodeDiff, ImageAdded, SlashInit, CreateEnvironment, Keybinding, CodeReviewContext, InlineHistoryMenu, InlineConversationMenu, ConversationListView, DefaultSessionMode, LongRunningCommand, Onboarding, ClearBuffer, PromptChip, plus the soon-to-be-removed ContinueConversationButton/ViewPassiveCodeDiffDetails/ResumeConversationButton — now receives the explicit "populate input buffer + show ENTER_AGAIN_TO_SEND_MESSAGE_ID" behavior for initial_prompt. None of them carry URI- or network-sourced prompt strings except LinearDeepLink today, but the shortcut removal future-proofs the decision.
2. Keep the "populate buffer + enter again to send" path as the default for Linear
With the shortcut removed, LinearDeepLink naturally falls into the existing else branch of try_enter_agent_view (input buffer replacement + ephemeral ENTER_AGAIN_TO_SEND_MESSAGE_ID message). No new UI surface is needed; this is already the behavior for non-auto-triggering origins that arrive with an initial_prompt when the user is not in fullscreen agent view.
3. Defense-in-depth: opt open_linear_issue_work out of auto-submit at the call site (committed shape)
We commit to the "explicit parameter on try_enter_agent_view + thin wrapper method at the public API" shape:
- Add a private
AutoSubmitPolicyenum inapp/src/terminal/view/agent_view.rswith two variants:FromOrigin(today's behavior after step 1) andNeverAutoSubmit(forced draft). - Thread
auto_submit: AutoSubmitPolicyas a new parameter ontry_enter_agent_view. Insidetry_enter_agent_view, the auto-submit decision becomesmatch auto_submit { FromOrigin => origin.should_autotrigger_request(), NeverAutoSubmit => false }. - Expose two public entrypoints from
TerminalView:enter_agent_view_for_new_conversation(prompt, origin, ctx)— unchanged signature for existing callers; usesAutoSubmitPolicy::FromOrigin.enter_agent_view_for_new_conversation_with_prompt_draft(prompt, origin, ctx)— new wrapper for call sites that handle prompts from URI-sourced, IPC, or otherwise untrusted input; usesAutoSubmitPolicy::NeverAutoSubmit.
open_linear_issue_workinapp/src/workspace/view.rscallsenter_agent_view_for_new_conversation_with_prompt_draftinstead ofenter_agent_view_for_new_conversation. The parameter-plus-wrapper shape was chosen over a pure wrapper because the wrapper alone cannot prevent a future internal refactor from re-introducing an implicit fullscreen promotion insidetry_enter_agent_view; the explicit policy parameter makes the decision visible at every call site oftry_enter_agent_viewand forces a typed match. Any future origin that should behave the same way can call the draft entrypoint at its call site without needing to re-audittry_enter_agent_view. This is defense-in-depth: step 1 is sufficient on its own for correctness; step 3 ensures that even ifshould_autotrigger_requestever grows a buggy allowlist entry, Linear deeplinks remain safe.
4. Leave LinearAction, LinearIssueWork::from_url, and the URI validation layer unchanged
No parsing, validation, or sanitization of the prompt query parameter is added. The mitigation depends on user gesture, not on string filtering — content-based filters are trivially bypassable for prompt injection. LinearIssueWork::from_url continues to decode prompt verbatim (still filtering empty strings, matching product invariant 6).
5. Telemetry and logging redaction
No telemetry schema changes are required. did_auto_trigger_request in TelemetryEvent::AgentViewEntered naturally reports false for Linear deeplinks after the fix; no extra code is needed.
To honor product invariant 12 (logging / telemetry redaction), the implementation commits to the following, and reviewers should verify:
TelemetryEvent::AgentViewEnteredandTelemetryEvent::LinearIssueLinkOpenedcarry only origin classifiers and fixed schema fields. Theinitial_promptvalue is never attached to a telemetry payload.- The
log::error!call inenter_agent_view_for_new_conversation_with_policyinterpolates onlyorigin(a compile-time enum discriminant) ande(a structuredEnterAgentViewErrorwhoseDisplaydoes not include the prompt body). Theinitial_prompt: Option<String>is explicitly not passed to any formatting macro in this file. self.show_error_toast(e.to_string(), ctx)relies onEnterAgentViewError: Display; the error types enumerated inEnterAgentViewErrordo not carry user-prompt strings. New error variants added in the future must not include the prompt body.- Conversation title fallback remains
"Linear Issue"(product invariant 9) so the prompt never becomes a title.
End-to-end flow (after the fix)
- OS/dispatch primitive hands
warp://linear/work?prompt=<ATTACKER>to Warp. validate_custom_uri→UriHost::Linear→LinearAction::WorkOnIssue→LinearIssueWork::from_url→open_linear_issue_work_in_{new,existing}_window.workspace.open_linear_issue_workopens a new tab and invokesenter_agent_view_for_new_conversation_with_prompt_draft(prompt, AgentViewEntryOrigin::LinearDeepLink, ctx)(new entrypoint from step 3).try_enter_agent_viewenters agent view for the new conversation. Becauseshould_autotrigger_request()returnsfalseforLinearDeepLinkand thewas_in_agent_view_alreadyshortcut has been removed (step 1) — or, belt-and-suspenders, because the caller passedNeverAutoSubmit(step 3) — the code takes the "replace buffer + show ephemeral message" branch.- The user sees the prompt in the input buffer and the "press enter again to send to agent" message. No LLM request has been made.
- The user either sends (explicit Enter inside the confirmation window) or edits/clears the prompt.
Testing and validation
Tests map back to the numbered product invariants in specs/GH703/product.md.
- Unit tests in
app/src/terminal/view_test.rs(invariants 1, 2, 3, 4, 6). Three tests are implemented:linear_deeplink_populates_input_as_draft_when_not_in_agent_view— enters viaenter_agent_view_for_new_conversation_with_prompt_draftwhen agent view is inactive; asserts the input buffer contains the attacker prompt and the ephemeral message id isENTER_AGAIN_TO_SEND_MESSAGE_ID.linear_deeplink_does_not_auto_submit_when_already_in_agent_view— first enters fullscreen agent view viaInput { was_prompt_autodetected: false }, then dispatches the Linear deeplink. Asserts a new conversation id is allocated, the prompt lands in the input buffer instead of being auto-submitted, and the ephemeral affordance is shown.linear_deeplink_via_default_entrypoint_does_not_auto_submit_in_fullscreen— verifies the defense-in-depth layer: even if a caller forgets to use the draft entrypoint and goes throughenter_agent_view_for_new_conversationdirectly,LinearDeepLinkstill does not auto-submit because thewas_in_agent_view_alreadyshortcut is gone.
- Regression guard for other origins (invariant 10). The existing
clear_buffer_action_in_fullscreen_agent_view_starts_new_conversationtest and the broader agent-view test suite coverAgentViewEntryOrigin::Input { was_prompt_autodetected: true }and keyboard-driven flows. The allowlist inshould_autotrigger_requestis unchanged, so origins that auto-submit today (Cli,Input { was_prompt_autodetected: true },SlashCommand { trigger: !is_keybinding() },AcceptedPromptSuggestion) continue to do so. - Unit test for URL parsing (already present, keep).
app/src/uri/uri_test.rscoversvalidate_custom_uri_linear,test_linear_action_parse_*, andtest_linear_issue_work_*. No changes required; they continue to exercise the decoding path to ensure we don't regress the URI schema. - Manual verification (invariants 1–5, 7, 11).
- On macOS, Linux, and Windows, open
warp://linear/work?prompt=<attacker+payload>while the focused terminal is already in fullscreen agent view. Confirm the prompt shows up in the input, the ephemeral "enter again to send" message is visible, and no LLM request is made until the user presses Enter. - Repeat with the focused terminal not in agent view.
- Repeat with the app closed (cold start) to cover the dispatch path used by
open_linear_issue_work_in_new_window. - Repeat with Warp backgrounded at dispatch time (invariant 11): foreground Warp afterward and confirm the prompt stays as a draft and the affordance is still shown.
- Confirm that opening the URL does not cause any of
read_files,StartAgent,SendMessageToAgent,FetchConversation, orUseComputertool calls to be issued.
- On macOS, Linux, and Windows, open
- Telemetry and redaction spot check (invariants 8, 12). Inspect the emitted
AgentViewEnteredevent and confirmdid_auto_trigger_request = false. ConfirmLinearIssueLinkOpenedstill fires once per dispatch. Greplog::output from the agent-view entry path to confirm the verbatim prompt never appears in a log line. ./script/presubmitpasses (fmt, clippy, tests).
Risks and mitigations
-
Removing
was_in_agent_view_alreadycould regress other flows. Some origins may rely on this shortcut today (e.g. entering a second conversation from agent view with a typed prompt). Mitigation: the agent-view unit tests inapp/src/terminal/view_test.rscontinue to exerciseInput { was_prompt_autodetected: false },Input { was_prompt_autodetected: true }, and keyboard-driven flows. Because any origin that legitimately needs to auto-submit is already inshould_autotrigger_request's allowlist, removing the fullscreen promotion does not change their behavior. If a regression does surface, the correct fix is to add the specific origin to the allowlist — not to re-introduce a generic fullscreen shortcut. -
Other URI-originated origins could have the same bug.
CodexModal,ProjectEntry,OnboardingCallout, and future URI-sourced origins were audited in step 1 above and are not in the auto-submit allowlist; the shortcut removal already protects them. New URI- or IPC-sourced call sites should additionally useenter_agent_view_for_new_conversation_with_prompt_draftfor defense-in-depth. -
User confusion from unexpected input content. A user who did not intend to open a Linear deeplink will still see the URI-supplied prompt in their agent input. This is a strict improvement over silent submission: they can inspect, edit, or clear it. No mitigation needed; the ephemeral "enter again to send" message already signals that the prompt is pending.
-
Attacker-supplied very long prompts. Existing input buffer handling already supports arbitrarily long content. No change needed.
Follow-ups
- The audit of other
AgentViewEntryOriginvariants (previously a follow-up) is now promoted into step 1 above and is a committed deliverable of this change. No follow-up issue is required; the grep-based audit is covered by the shortcut removal. - Coordinate with the owners of #655 (Windows named pipe) and #666 (Linux D-Bus) to gate
warp://dispatch on a platform-trusted source. This spec is orthogonal defense; with that work landed the prompt-injection blast radius shrinks further. Tracked separately in those issues — this spec does not block on them. - Consider adding a dedicated UI indicator (banner or toast) identifying a prompt as "from a Linear deeplink" so the user knows its provenance at a glance. Not required for the fix but would improve trust; can be added iteratively once the safety invariant is in place.