Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions src/apps/desktop/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ is `node tests/e2e/scripts/run-subscription-pool.mjs` (see its E2E guide).

For desktop card stacking, native parenting rollback, and legacy record/request compatibility,
run `cargo test --locked -p openbitfun-desktop --no-default-features --lib desktop_cards`.
This also checks host-stamped Peer turn metadata for legacy controllers. Tool
admission is turn-scoped: attached Peer controllers are observers and must not
block local card creation or display. Run the Core guide's `desktop_card` tests
for request-origin propagation; these fixtures do not establish live remote behavior.
On a graphical Windows session with Explorer running, also run
`cargo test --locked -p openbitfun-desktop --no-default-features --lib live_explorer_attachment_round_trip -- --ignored`.
The native fixtures use hidden disposable windows; they do not establish WebView rendering,
Expand All @@ -154,6 +158,12 @@ start it again) a pinned card must reattach within about 30 s, or come back unpi
pin-failure notice; it must never stay "Shown" without a window. UI, hidden-card pinning, and old-host negotiation coverage:
`pnpm --dir src/web-ui run test:run src/app/components/DesktopCard/DesktopCardWindow.test.tsx src/infrastructure/api/service-api/DesktopCardAPI.test.ts src/app/scenes/personal-overview/OverviewDesktopCardsSection.test.tsx src/app/scenes/settings/pages/application/DesktopCardsSettingsSection.test.tsx`.

For frontend customization with background Peer subscribers, direct Peer tool
refusal, revision recovery, and legacy state compatibility, run
`cargo test --locked -p openbitfun-desktop --no-default-features --lib frontend_workbench`.
The Core guide's `frontend_workbench` tests cover per-turn origin admission.
These fixtures do not launch the native confirmation controls or a live Peer connection.

For session storage directory access, run
`cargo test -p openbitfun-desktop --lib api::session_storage_directory::tests`
for session storage directory resolution (these tests do not launch a file manager),
Expand Down
9 changes: 9 additions & 0 deletions src/apps/desktop/src/api/miniapp_agent_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ pub struct MiniAppAgentContextFile {
#[serde(rename_all = "camelCase")]
pub struct MiniAppAgentRunRequest {
pub app_id: String,
/// Stamped by this host's Peer bridge, independently of client version.
/// Legacy and controller-local calls keep the local presentation context.
#[serde(default)]
pub peer_device_control: bool,
/// Full user prompt for the agent turn. The MiniApp owns its own task
/// protocol; the host only wraps it into a hidden agent session.
pub prompt: String,
Expand Down Expand Up @@ -551,6 +555,11 @@ pub async fn miniapp_agent_run(
request.enable_tools,
market_strict,
);
if request.peer_device_control {
submission_plan.metadata[
openbitfun_agent_runtime::remote_file_delivery::TOOL_CONTEXT_PEER_DEVICE_CONTROL_KEY
] = serde_json::Value::Bool(true);
}

let validated_existing_session =
if let Some(existing_session_id) = submission_plan.requested_session_id.clone() {
Expand Down
162 changes: 162 additions & 0 deletions src/apps/desktop/src/api/peer_host_invoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,22 @@ pub async fn dispatch(command: &str, args: Value) -> HostInvokeBridgeResult {
PeerHostVerdict::Execute => {}
}

// Direct tool calls have no dialog turn whose origin can reach
// ToolUseContext. Refuse tools that require the visible local desktop here.
let direct_tool = args
.get("request")
.unwrap_or(&args)
.get("toolName")
.and_then(Value::as_str);
if command == "execute_tool" && matches!(direct_tool, Some("DesktopCard" | "FrontendWorkbench"))
{
return HostInvokeBridgeResult {
ok: false,
value: None,
error: Some(format!("{} is unavailable through Peer Device Mode because it requires the visible local desktop.", direct_tool.unwrap())),
};
}

if command == "product_control_invoke" {
use openbitfun_product_domains::{
product_control::{ProductControlAction, ProductControlRequest},
Expand Down Expand Up @@ -409,6 +425,7 @@ pub async fn dispatch(command: &str, args: Value) -> HostInvokeBridgeResult {
}
}
};
let args = with_peer_turn_origin(command, args);
let response_scope =
serde_json::json!({"path": args.get("request").unwrap_or(&args).get("path")});
match bridge_via_webview(&app, command, args).await {
Expand Down Expand Up @@ -437,6 +454,38 @@ pub async fn dispatch(command: &str, args: Value) -> HostInvokeBridgeResult {
}
}

/// Preserve the caller's payload while recording where this turn was
/// submitted. This also covers older controllers that send no metadata.
fn with_peer_turn_origin(command: &str, mut args: Value) -> Value {
use openbitfun_agent_runtime::remote_file_delivery::TOOL_CONTEXT_PEER_DEVICE_CONTROL_KEY;

if !matches!(
command,
"start_dialog_turn" | "btw_ask_stream" | "miniapp_agent_run"
) {
return args;
}
let request = if args.get("request").is_some() {
args.get_mut("request").expect("request exists")
} else {
&mut args
};
if let Some(request) = request.as_object_mut() {
if command == "miniapp_agent_run" {
request.insert("peerDeviceControl".to_string(), Value::Bool(true));
return args;
}
let metadata = request
.entry("userMessageMetadata")
.or_insert_with(|| serde_json::json!({}));
if !metadata.is_object() {
*metadata = serde_json::json!({ "raw_metadata": metadata.take() });
}
metadata[TOOL_CONTEXT_PEER_DEVICE_CONTROL_KEY] = Value::Bool(true);
}
args
}

async fn bridge_via_webview(
app: &AppHandle,
command: &str,
Expand Down Expand Up @@ -478,6 +527,119 @@ async fn bridge_via_webview(
mod tests {
use super::*;

#[tokio::test]
async fn desktop_cards_direct_peer_calls_fail_before_the_native_bridge() {
let result = dispatch(
"execute_tool",
serde_json::json!({"request": {
"toolName": "DesktopCard", "input": {"action": "create"}
}}),
)
.await;
assert!(!result.ok);
assert!(result.error.unwrap().contains("Peer Device Mode"));
}

#[tokio::test]
async fn frontend_workbench_direct_peer_calls_fail_before_the_native_bridge() {
let request = serde_json::json!({
"toolName": "FrontendWorkbench", "input": {"action": "prepare"}
});
for args in [request.clone(), serde_json::json!({"request": request})] {
let result = dispatch("execute_tool", args).await;
assert!(!result.ok);
assert!(result.error.unwrap().contains("Peer Device Mode"));
}
}

#[test]
fn desktop_cards_miniapp_peer_origin_is_host_stamped() {
let original = serde_json::json!({"request": {
"appId": "builtin-app", "prompt": "Create a card"
}});
let local: super::super::miniapp_agent_api::MiniAppAgentRunRequest =
serde_json::from_value(original["request"].clone()).unwrap();
assert!(!local.peer_device_control);
let marked = with_peer_turn_origin("miniapp_agent_run", original);
let peer: super::super::miniapp_agent_api::MiniAppAgentRunRequest =
serde_json::from_value(marked["request"].clone()).unwrap();
assert!(peer.peer_device_control);
assert_eq!(peer.app_id, local.app_id);
assert_eq!(peer.prompt, local.prompt);
}

#[test]
fn desktop_cards_peer_origin_preserves_legacy_turn_payloads() {
use openbitfun_agent_runtime::remote_file_delivery::TOOL_CONTEXT_PEER_DEVICE_CONTROL_KEY;
use serde_json::json;

for command in ["start_dialog_turn", "btw_ask_stream"] {
for metadata in [
None,
Some(Value::Null),
Some(json!("legacy metadata")),
Some(json!({
"surface": "flow_chat",
"requestId": "request-1",
"peer_device_control": false
})),
] {
let mut original = json!({"request": {
"sessionId": "local-or-peer-session",
"turnId": "turn-1",
"userInput": "Create a card",
"agentType": "Standard"
}});
if let Some(metadata) = metadata.clone() {
original["request"]["userMessageMetadata"] = metadata;
}
let marked = with_peer_turn_origin(command, original.clone());
let marked_metadata = &marked["request"]["userMessageMetadata"];
assert_eq!(marked_metadata[TOOL_CONTEXT_PEER_DEVICE_CONTROL_KEY], true);
if command == "start_dialog_turn" {
let decoded: super::super::agentic_api::StartDialogTurnRequest =
serde_json::from_value(marked["request"].clone()).unwrap();
assert_eq!(decoded.session_id, "local-or-peer-session");
assert_eq!(
decoded.user_message_metadata.as_ref(),
Some(marked_metadata)
);
}
match metadata {
Some(Value::Object(_)) => {
assert_eq!(marked_metadata["surface"], "flow_chat");
assert_eq!(marked_metadata["requestId"], "request-1");
}
Some(Value::String(raw)) => assert_eq!(marked_metadata["raw_metadata"], raw),
_ => {}
}
let mut restored = marked["request"].clone();
restored
.as_object_mut()
.unwrap()
.remove("userMessageMetadata");
let mut original_fields = original["request"].clone();
original_fields
.as_object_mut()
.unwrap()
.remove("userMessageMetadata");
assert_eq!(restored, original_fields);
// The marker is ordinary additive metadata and survives queue /
// transcript serialization without changing the request schema.
let round_trip: Value = serde_json::from_str(&marked.to_string()).unwrap();
assert_eq!(round_trip, marked);
assert_eq!(with_peer_turn_origin(command, marked.clone()), marked);
}
}

let read = json!({"request": {"sessionId": "session-1"}});
assert_eq!(with_peer_turn_origin("get_session", read.clone()), read);
assert_eq!(
with_peer_turn_origin("start_dialog_turn", Value::Null),
Value::Null
);
}

fn control_state(controllers: &[&str], requests: &[&str]) -> PeerControlState {
PeerControlState {
controllers: controllers
Expand Down
13 changes: 3 additions & 10 deletions src/apps/desktop/src/desktop_cards/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,6 @@ fn pending_countdown(card: &DesktopCardRecord) -> Option<i64> {
}
}

fn ensure_local_display_surface() -> Result<(), String> {
if crate::api::peer_host_invoke::attached_controllers().is_empty() {
Ok(())
} else {
Err("This OpenBitFun host is being driven from another device through Peer Device Mode. Desktop cards would appear only on this host's screen, which that user cannot see.".to_string())
}
}

async fn due_notification_body(message: Option<&str>) -> String {
if let Some(message) = message.filter(|message| !message.trim().is_empty()) {
return message.to_string();
Expand Down Expand Up @@ -942,10 +934,12 @@ impl DesktopCards {
}

async fn handle_tool_request(&self, request: DesktopCardHostRequest) -> Result<Value, String> {
// DesktopCardTool checks the submitting turn's control surface before
// invoking this host. Attached peers are event subscribers, not proof
// that this particular request came from a remote user.
let action = request.action();
let result = match request {
DesktopCardHostRequest::Create { draft, origin } => {
ensure_local_display_surface()?;
json!({ "card": self.create(draft, origin).await? })
}
DesktopCardHostRequest::Update { card_id, patch } => {
Expand All @@ -956,7 +950,6 @@ impl DesktopCards {
json!({ "card": self.get(&card_id).await?, "enabled": self.is_enabled() })
}
DesktopCardHostRequest::Show { card_id } => {
ensure_local_display_surface()?;
self.require_enabled()?;
json!({ "card": self.set_visible(&card_id, true).await? })
}
Expand Down
47 changes: 28 additions & 19 deletions src/apps/desktop/src/frontend_workbench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,9 +348,8 @@ impl FrontendWorkbenchManager {
self: &Arc<Self>,
request: FrontendWorkbenchHostRequest,
) -> Result<Value, String> {
ensure_local_confirmation_surface(
crate::api::peer_host_invoke::attached_controllers().len(),
)?;
// Core admits the invoking turn's origin. Peer event subscribers may
// remain attached while the user customizes the visible local desktop.
match request.action.as_str() {
"prepare" => self.prepare(),
"status" => self.status(),
Expand Down Expand Up @@ -1231,16 +1230,6 @@ impl FrontendWorkbenchManager {
}
}

fn ensure_local_confirmation_surface(attached_peer_controllers: usize) -> Result<(), String> {
if attached_peer_controllers > 0 {
return Err(
"FrontendWorkbench is unavailable while this OpenBitFun host is controlled through Peer Device Mode; run Creative mode on the visible local desktop instead"
.to_string(),
);
}
Ok(())
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FrontendUpdateDecisionRequest {
Expand Down Expand Up @@ -2025,12 +2014,32 @@ mod tests {
assert!(validate_revision_id("creative-38e14f63-30ad-4ad7-9e4e-5ad556450ba3").is_ok());
}

#[test]
fn peer_control_requires_a_visible_local_confirmation_surface() {
assert!(ensure_local_confirmation_surface(0).is_ok());
let error = ensure_local_confirmation_surface(1)
.expect_err("peer-controlled frontend updates must fail loudly");
assert!(error.contains("Peer Device Mode"));
#[tokio::test]
async fn local_frontend_workbench_requests_ignore_background_peer_subscribers() {
struct AttachedController(String);
impl Drop for AttachedController {
fn drop(&mut self) {
crate::api::peer_host_invoke::detach_controller(&self.0);
}
}
let controller = AttachedController(format!("workbench-test-{}", Uuid::new_v4()));
crate::api::peer_host_invoke::attach_controller(controller.0.clone());
assert!(crate::api::peer_host_invoke::attached_controllers().contains(&controller.0));

let root = tempfile::tempdir().unwrap();
let manager = overlay_manager(root.path(), "installed product");
let result = manager
.handle_tool_request(FrontendWorkbenchHostRequest {
action: "prepare".into(),
draft_id: None,
command_id: None,
arguments: None,
})
.await
.expect("local customization remains available with Peer subscribers");
let draft = PathBuf::from(result["draftPath"].as_str().unwrap());
assert!(draft.join(overrides::CSS).is_file());
assert!(draft.join(overrides::JS).is_file());
}

#[test]
Expand Down
20 changes: 20 additions & 0 deletions src/crates/assembly/core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,26 @@ Narrower local guides already exist for some subtrees:

## Verification

For desktop card admission, per-turn Peer origin, recovery context and subagent
propagation:

```bash
cargo test --locked -p openbitfun-core --no-default-features --features agent-runtime,git --lib -- desktop_card
```

These tests use a stub card host and simulated remote contexts; they do not
exercise native card windows or live Peer/mobile/SSH connections. The Desktop
guide owns the HostInvoke compatibility and native host checks.

For Creative frontend customization origin admission and fresh approval policy:

```bash
cargo test --locked -p openbitfun-core --no-default-features --features agent-runtime,git,tools-creation --lib frontend_workbench
```

These tests simulate Peer/mobile and remote-workspace origins; they do not
exercise native recovery controls or live remote connections.

For opt-in category/system-prompt previews and session-storage isolation:

```bash
Expand Down
Loading
Loading