Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
d05dd49
fix: point OSS desktop entry at package launcher (#9424)
princepal9120 May 4, 2026
14980d7
Fix arity of request_ambient_agent_task_id_for_hidden_child test call…
jefflloyd May 21, 2026
2fea28b
Add warpctrl product and tech specs
zachlloyd May 22, 2026
ac892c9
Address warpctrl spec review feedback
zachlloyd May 22, 2026
86612f3
Add warpctrl security architecture documentation
zachlloyd May 22, 2026
4dc7cc1
Update warpctrl security enablement model
zachlloyd May 22, 2026
e4bb06b
Update warpctrl permission model
zachlloyd May 22, 2026
2151b02
Add warpctrl scripting enablement settings
zachlloyd May 22, 2026
6b9f0db
Update warpctrl permission taxonomy specs
zachlloyd May 22, 2026
2464e44
Add warpctrl CLI verification plan
zachlloyd May 23, 2026
e8d2e84
Specify warpctrl target selectors
zachlloyd May 23, 2026
270bd71
Document authoritative warpctrl specs branch
zachlloyd May 23, 2026
2ff71ac
Update warpctrl specs for targeting and security scope
zachlloyd May 23, 2026
6286aa1
Add WarpCtrlBehavior review gate to warpctrl tech spec
zachlloyd May 23, 2026
c4fde66
Update warpctrl tech plan for reviewable branch stack
zachlloyd May 23, 2026
cc48d49
Create warpctrl core foundation branch
zachlloyd May 23, 2026
e05f824
Tighten warpctrl foundation gates
zachlloyd May 23, 2026
338856e
Document combined warpctrl stack base
zachlloyd May 23, 2026
7d997e6
Document warpctrl core surfaces
zachlloyd May 24, 2026
bed8ee0
Address warpctrl foundation review feedback
zachlloyd May 24, 2026
a5be127
Clarify warpctrl outside-Warp foundation
zachlloyd May 24, 2026
a8a0957
Expand warpctrl authenticated scripting spec
zachlloyd May 25, 2026
97c82b2
Refine warpctrl product scope
zachlloyd May 25, 2026
f5e8795
Sync warpctrl contract stance
zachlloyd May 26, 2026
64985cf
Document warpctrl spec ownership invariant
zachlloyd May 26, 2026
5dcf32c
Restore canonical warpctrl specs
zachlloyd May 26, 2026
4d6d80c
Update warpctrl v2 branch architecture
zachlloyd May 26, 2026
21b07e5
Update warpctrl screenshot verification guidance
zachlloyd May 26, 2026
d26ed03
Clarify warpctrl visual validation requirements
zachlloyd May 26, 2026
138ada9
Clarify warpctrl catalog parser parity
zachlloyd May 27, 2026
8f7bbd1
Support explicit warpctrl window targets for tab create
zachlloyd May 27, 2026
cf9c747
Update Warp Control CLI contract foundation
zachlloyd May 27, 2026
ed28489
Align warpctrl TECH protocol envelope
zachlloyd May 28, 2026
88b0bd7
Document warpctrl active window fallback
zachlloyd May 28, 2026
67f1cd7
Ignore generated local publish artifacts
zachlloyd May 28, 2026
a312e79
Update warpctrl specs for wrapper architecture
zachlloyd May 29, 2026
b97aa90
Route warpctrl through Warp binary
zachlloyd May 29, 2026
ff18b06
Harden warpctrl local-control security
zachlloyd May 30, 2026
775601c
Update common skills lock
zachlloyd May 30, 2026
7761166
Align warpctrl specs with first slice implementation
zachlloyd May 30, 2026
2c62160
Polish warpctrl CLI settings row
zachlloyd May 31, 2026
6bc74d3
Address warpctrl review feedback
zachlloyd Jun 3, 2026
becf4db
Align Warp Control CLI implementation with specs
zachlloyd Jun 5, 2026
f480b10
Close warpctrl contract validation gaps
zachlloyd Jun 5, 2026
7542919
Fix warpctrl broker startup context
zachlloyd Jun 5, 2026
8a3de20
Merge master into zach/warp-cli-v2/contract-spec-sync
zachlloyd Jun 5, 2026
a02831a
Refine warpctrl authorization and discovery security
zachlloyd Jun 5, 2026
8d9da23
Fix local control cross-platform compilation
zachlloyd Jun 6, 2026
c6185d2
Fix Windows local-control CI failures
zachlloyd Jun 6, 2026
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
20 changes: 20 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ integration = { path = "crates/integration" }
ipc = { path = "crates/ipc" }
jsonrpc = { path = "crates/jsonrpc" }
languages = { path = "crates/languages" }
local_control = { path = "crates/local_control" }
lsp = { path = "crates/lsp" }
markdown_parser = { path = "crates/markdown_parser" }
mcp = { path = "crates/mcp" }
Expand Down
2 changes: 2 additions & 0 deletions app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ rmcp.workspace = true
warp_isolation_platform.workspace = true
warp_ripgrep.workspace = true
warp_managed_secrets.workspace = true
local_control.workspace = true

[target.'cfg(target_os = "macos")'.dependencies]
block.workspace = true
Expand Down Expand Up @@ -972,6 +973,7 @@ vertical_tabs = []
vertical_tabs_summary_mode = []
tab_configs = []
grouped_tabs = []
warp_control_cli = []
agent_harness = []
oz_handoff = []
handoff_local_cloud = []
Expand Down
2 changes: 2 additions & 0 deletions app/src/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,8 @@ fn enabled_features() -> HashSet<FeatureFlag> {
FeatureFlag::TabConfigs,
#[cfg(feature = "grouped_tabs")]
FeatureFlag::GroupedTabs,
#[cfg(feature = "warp_control_cli")]
FeatureFlag::WarpControlCli,
#[cfg(feature = "agent_harness")]
FeatureFlag::AgentHarness,
#[cfg(feature = "oz_handoff")]
Expand Down
16 changes: 16 additions & 0 deletions app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ mod gpu_state;
mod input_classifier;
mod interval_timer;
mod linear;
#[cfg(not(target_family = "wasm"))]
mod local_control;
#[cfg(any(target_os = "macos", target_os = "windows"))]
mod login_item;
mod menu;
Expand Down Expand Up @@ -585,6 +587,11 @@ pub fn run() -> Result<()> {

// Ensure feature flags are initialized before parsing command-line arguments.
features::init_feature_flags();
if let Some(args) = warp_cli::local_control::ControlArgs::from_control_mode_env() {
#[cfg(windows)]
warp_util::windows::attach_to_parent_console();
warp_cli::local_control::run_and_exit(args);
}

// Parse command-line arguments.
let args = warp_cli::Args::from_env();
Expand Down Expand Up @@ -2058,6 +2065,15 @@ pub(crate) fn initialize_app(
];
http_server::HttpServer::new(routers, ctx)
});
#[cfg(not(target_family = "wasm"))]
if matches!(
launch_mode,
LaunchMode::App { .. } | LaunchMode::Test { .. }
) && FeatureFlag::WarpControlCli.is_enabled()
{
ctx.add_singleton_model(local_control::LocalControlBridge::new);
ctx.add_singleton_model(local_control::LocalControlServer::new);
}

app_state
}
Expand Down
115 changes: 115 additions & 0 deletions app/src/local_control/bridge.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//! Bridge between protocol-level control requests and Warp application models.
//!
//! The bridge validates protocol version, selectors, credentials, and settings
//! before routing each supported action to an app-side handler.
use ::local_control::auth::CredentialGrant;
use ::local_control::{
Action, ActionKind, ControlError, ErrorCode, InstanceId, RequestEnvelope, ResponseEnvelope,
};
use warpui::{Entity, ModelContext, SingletonEntity};

use crate::local_control::handlers::{layout, metadata};
use crate::local_control::permissions::{
ensure_action_allowed, ensure_feature_enabled, ensure_protocol_version,
};
use crate::local_control::resolver::validate_action_params;

/// WarpUI model that executes already-authenticated local-control actions.
pub struct LocalControlBridge {
instance_id: Option<InstanceId>,
}

impl Entity for LocalControlBridge {
type Event = ();
}

impl SingletonEntity for LocalControlBridge {}

impl LocalControlBridge {
pub fn new(_ctx: &mut ModelContext<Self>) -> Self {
Self { instance_id: None }
}

pub(super) fn set_instance_id(&mut self, instance_id: InstanceId) {
self.instance_id = Some(instance_id);
}

pub(super) fn handle_request(
&mut self,
request: RequestEnvelope,
grant: CredentialGrant,
ctx: &mut ModelContext<Self>,
) -> ResponseEnvelope {
if let Err(error) = ensure_feature_enabled() {
return ResponseEnvelope::error(request.request_id, error);
}
if let Err(error) = ensure_protocol_version(request.protocol_version) {
return ResponseEnvelope::error(request.request_id, error);
}
let Some(instance_id) = &self.instance_id else {
return ResponseEnvelope::error(
request.request_id,
ControlError::new(
ErrorCode::BridgeUnavailable,
"local-control bridge has no active instance identity",
),
);
};
if let Err(error) = validate_request_authority(instance_id, &request.action, &grant) {
return ResponseEnvelope::error(request.request_id, error);
}
if let Err(error) =
ensure_action_allowed(grant.invocation_context, request.action.kind, ctx)
{
return ResponseEnvelope::error(request.request_id, error);
}
match request.action.kind {
ActionKind::InstanceList => match metadata::instance(&self.instance_id) {
Ok(data) => ResponseEnvelope::ok(request.request_id, data),
Err(error) => ResponseEnvelope::error(request.request_id, error),
},
ActionKind::AppPing => match metadata::ping(&self.instance_id) {
Ok(data) => ResponseEnvelope::ok(request.request_id, data),
Err(error) => ResponseEnvelope::error(request.request_id, error),
},
ActionKind::AppVersion => match metadata::version(&self.instance_id) {
Ok(data) => ResponseEnvelope::ok(request.request_id, data),
Err(error) => ResponseEnvelope::error(request.request_id, error),
},
ActionKind::TabCreate => {
match layout::create_terminal_tab(&self.instance_id, &request.target, ctx) {
Ok(data) => ResponseEnvelope::ok(request.request_id, data),
Err(error) => ResponseEnvelope::error(request.request_id, error),
}
}
action => ResponseEnvelope::error(
request.request_id,
ControlError::new(
ErrorCode::UnsupportedAction,
format!(
"{} is not implemented by this local-control bridge",
action.as_str()
),
),
),
}
}
}

pub(crate) fn validate_request_authority(
instance_id: &InstanceId,
action: &Action,
grant: &CredentialGrant,
) -> Result<(), ControlError> {
grant.verify_for_action(instance_id, action.kind)?;
if !action.kind.is_implemented() {
return Err(ControlError::new(
ErrorCode::UnsupportedAction,
format!(
"{} is not implemented by this local-control bridge",
action.kind.as_str()
),
));
}
validate_action_params(action)
}
3 changes: 3 additions & 0 deletions app/src/local_control/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
//! App-side action handlers invoked by the local-control bridge.
pub(super) mod layout;
pub(super) mod metadata;
99 changes: 99 additions & 0 deletions app/src/local_control/handlers/layout.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//! Layout mutation handlers for local-control actions.
#[cfg(test)]
#[path = "layout_tests.rs"]
mod tests;
use ::local_control::protocol::TargetSelector;
use ::local_control::{ActionKind, ControlError, ErrorCode, InstanceId};
use serde::Serialize;
use warpui::{ModelContext, TypedActionView};

use crate::local_control::resolver::{target_window_id_for_target, validate_tab_create_target};
use crate::local_control::LocalControlBridge;
use crate::workspace::{Workspace, WorkspaceAction};
#[derive(Serialize)]
struct TabCreateResponse<'a> {
action: &'static str,
created: bool,
instance_id: Option<&'a str>,
window: TargetWindowResponse,
tab: TabCountsResponse,
}

#[derive(Serialize)]
struct TargetWindowResponse {
selector: &'static str,
id: String,
}

#[derive(Serialize)]
struct TabCountsResponse {
id: String,
previous_count: usize,
count: usize,
active_index: usize,
}

pub(crate) fn create_terminal_tab(
instance_id: &Option<InstanceId>,
target: &TargetSelector,
ctx: &mut ModelContext<LocalControlBridge>,
) -> Result<serde_json::Value, ControlError> {
validate_tab_create_target(target)?;
let window_id = target_window_id_for_target(ctx, target, ActionKind::TabCreate)?;
let workspace = ctx
.views_of_type::<Workspace>(window_id)
.and_then(|workspaces| workspaces.into_iter().next())
.ok_or_else(|| {
ControlError::new(
ErrorCode::MissingTarget,
"tab.create requires a workspace in the target window",
)
})?;
let (tab_id, previous_tab_count, tab_count, active_tab_index) =
workspace.update(ctx, |workspace, ctx| {
let previous_tab_count = workspace.tab_count();
workspace.handle_action(
&WorkspaceAction::AddTerminalTab {
hide_homepage: false,
},
ctx,
);
let tab_id = workspace
.get_pane_group_view(workspace.active_tab_index())
.map(|tab| tab.id().to_string())
.ok_or_else(|| {
ControlError::new(
ErrorCode::Internal,
"tab.create did not produce an active tab identifier",
)
})?;
Ok((
tab_id,
previous_tab_count,
workspace.tab_count(),
workspace.active_tab_index(),
))
})?;
serde_json::to_value(TabCreateResponse {
action: ActionKind::TabCreate.as_str(),
created: true,
instance_id: instance_id.as_ref().map(|id| id.0.as_str()),
window: TargetWindowResponse {
selector: "target",
id: window_id.to_string(),
},
tab: TabCountsResponse {
id: tab_id,
previous_count: previous_tab_count,
count: tab_count,
active_index: active_tab_index,
},
})
.map_err(|err| {
ControlError::with_details(
ErrorCode::Internal,
"failed to serialize local-control tab.create response",
err.to_string(),
)
})
}
36 changes: 36 additions & 0 deletions app/src/local_control/handlers/layout_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use ::local_control::protocol::TargetSelector;
use ::local_control::InstanceId;
use warpui::App;

use super::create_terminal_tab;
use crate::local_control::LocalControlBridge;
use crate::workspace::view::tests::{initialize_app, mock_workspace};

#[test]
fn tab_create_handler_adds_and_activates_terminal_tab() {
App::test((), |mut app| async move {
initialize_app(&mut app);
let workspace = mock_workspace(&mut app);
let previous_count = workspace.read(&app, |workspace, _| workspace.tab_count());
let bridge = app.add_singleton_model(LocalControlBridge::new);
let instance_id = InstanceId("inst_test".to_owned());

let response = bridge.update(&mut app, |bridge, ctx| {
bridge.set_instance_id(instance_id.clone());
create_terminal_tab(&Some(instance_id.clone()), &TargetSelector::default(), ctx)
.expect("tab.create handler succeeds")
});

workspace.read(&app, |workspace, _| {
assert_eq!(workspace.tab_count(), previous_count + 1);
assert_eq!(workspace.active_tab_index(), previous_count);
});
assert_eq!(response["action"], "tab.create");
assert_eq!(response["created"], true);
assert_eq!(response["instance_id"], "inst_test");
assert_eq!(response["tab"]["previous_count"], previous_count);
assert_eq!(response["tab"]["count"], previous_count + 1);
assert_eq!(response["tab"]["active_index"], previous_count);
assert!(response["tab"]["id"].is_string());
});
}
Loading
Loading