tauri-agent-tools 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/tauri-agent-tools/SKILL.md +195 -13
- package/.agents/skills/tauri-bridge-setup/SKILL.md +82 -14
- package/.agents/skills/tauri-debug-quickstart/SKILL.md +80 -0
- package/AGENTS.md +9 -7
- package/README.md +119 -11
- package/dist/bridge/client.d.ts +21 -2
- package/dist/bridge/client.js +119 -3
- package/dist/bridge/client.js.map +1 -1
- package/dist/cli.js +47 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/appPaths.d.ts +2 -0
- package/dist/commands/appPaths.js +97 -0
- package/dist/commands/appPaths.js.map +1 -0
- package/dist/commands/capabilitiesAudit.d.ts +2 -0
- package/dist/commands/capabilitiesAudit.js +105 -0
- package/dist/commands/capabilitiesAudit.js.map +1 -0
- package/dist/commands/capture.d.ts +3 -0
- package/dist/commands/capture.js +218 -0
- package/dist/commands/capture.js.map +1 -0
- package/dist/commands/check.d.ts +5 -0
- package/dist/commands/check.js +174 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/configInspect.d.ts +2 -0
- package/dist/commands/configInspect.js +223 -0
- package/dist/commands/configInspect.js.map +1 -0
- package/dist/commands/diagnose.d.ts +2 -0
- package/dist/commands/diagnose.js +311 -0
- package/dist/commands/diagnose.js.map +1 -0
- package/dist/commands/eval.js +16 -3
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/forensics.d.ts +2 -0
- package/dist/commands/forensics.js +331 -0
- package/dist/commands/forensics.js.map +1 -0
- package/dist/commands/health.d.ts +2 -0
- package/dist/commands/health.js +39 -0
- package/dist/commands/health.js.map +1 -0
- package/dist/commands/interact/click.d.ts +6 -0
- package/dist/commands/interact/click.js +102 -0
- package/dist/commands/interact/click.js.map +1 -0
- package/dist/commands/interact/focus.d.ts +3 -0
- package/dist/commands/interact/focus.js +40 -0
- package/dist/commands/interact/focus.js.map +1 -0
- package/dist/commands/interact/navigate.d.ts +3 -0
- package/dist/commands/interact/navigate.js +49 -0
- package/dist/commands/interact/navigate.js.map +1 -0
- package/dist/commands/interact/scroll.d.ts +11 -0
- package/dist/commands/interact/scroll.js +110 -0
- package/dist/commands/interact/scroll.js.map +1 -0
- package/dist/commands/interact/select.d.ts +3 -0
- package/dist/commands/interact/select.js +59 -0
- package/dist/commands/interact/select.js.map +1 -0
- package/dist/commands/interact/shared.d.ts +23 -0
- package/dist/commands/interact/shared.js +62 -0
- package/dist/commands/interact/shared.js.map +1 -0
- package/dist/commands/interact/type.d.ts +6 -0
- package/dist/commands/interact/type.js +59 -0
- package/dist/commands/interact/type.js.map +1 -0
- package/dist/commands/invoke.d.ts +3 -0
- package/dist/commands/invoke.js +53 -0
- package/dist/commands/invoke.js.map +1 -0
- package/dist/commands/osLogs.d.ts +2 -0
- package/dist/commands/osLogs.js +130 -0
- package/dist/commands/osLogs.js.map +1 -0
- package/dist/commands/probe.d.ts +2 -0
- package/dist/commands/probe.js +117 -0
- package/dist/commands/probe.js.map +1 -0
- package/dist/commands/processTree.d.ts +2 -0
- package/dist/commands/processTree.js +45 -0
- package/dist/commands/processTree.js.map +1 -0
- package/dist/commands/shared.d.ts +10 -4
- package/dist/commands/shared.js +23 -3
- package/dist/commands/shared.js.map +1 -1
- package/dist/commands/sidecarReplay.d.ts +7 -0
- package/dist/commands/sidecarReplay.js +93 -0
- package/dist/commands/sidecarReplay.js.map +1 -0
- package/dist/commands/sidecarTap.d.ts +2 -0
- package/dist/commands/sidecarTap.js +118 -0
- package/dist/commands/sidecarTap.js.map +1 -0
- package/dist/commands/storeInspect.d.ts +13 -0
- package/dist/commands/storeInspect.js +156 -0
- package/dist/commands/storeInspect.js.map +1 -0
- package/dist/commands/webviewAttach.d.ts +2 -0
- package/dist/commands/webviewAttach.js +64 -0
- package/dist/commands/webviewAttach.js.map +1 -0
- package/dist/platform/oslog/darwin.d.ts +21 -0
- package/dist/platform/oslog/darwin.js +72 -0
- package/dist/platform/oslog/darwin.js.map +1 -0
- package/dist/platform/oslog/linux.d.ts +16 -0
- package/dist/platform/oslog/linux.js +47 -0
- package/dist/platform/oslog/linux.js.map +1 -0
- package/dist/platform/oslog/windows.d.ts +15 -0
- package/dist/platform/oslog/windows.js +16 -0
- package/dist/platform/oslog/windows.js.map +1 -0
- package/dist/schemas/bridge.d.ts +256 -0
- package/dist/schemas/bridge.js +57 -0
- package/dist/schemas/bridge.js.map +1 -1
- package/dist/schemas/commands.d.ts +126 -0
- package/dist/schemas/commands.js +28 -0
- package/dist/schemas/commands.js.map +1 -1
- package/dist/schemas/index.d.ts +3 -2
- package/dist/schemas/index.js +3 -2
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/interact.d.ts +118 -0
- package/dist/schemas/interact.js +31 -0
- package/dist/schemas/interact.js.map +1 -0
- package/dist/schemas/osLog.d.ts +34 -0
- package/dist/schemas/osLog.js +18 -0
- package/dist/schemas/osLog.js.map +1 -0
- package/dist/schemas/sidecar.d.ts +33 -0
- package/dist/schemas/sidecar.js +17 -0
- package/dist/schemas/sidecar.js.map +1 -0
- package/dist/schemas/tauriConfig.d.ts +825 -0
- package/dist/schemas/tauriConfig.js +102 -0
- package/dist/schemas/tauriConfig.js.map +1 -0
- package/dist/util/ndjson.d.ts +37 -0
- package/dist/util/ndjson.js +82 -0
- package/dist/util/ndjson.js.map +1 -0
- package/dist/util/tauriConfig.d.ts +63 -0
- package/dist/util/tauriConfig.js +235 -0
- package/dist/util/tauriConfig.js.map +1 -0
- package/examples/frontend-stub/index.html +1 -0
- package/examples/tauri-bridge/Cargo.toml +6 -0
- package/examples/tauri-bridge/build.rs +3 -0
- package/examples/tauri-bridge/icons/icon.png +0 -0
- package/examples/tauri-bridge/src/dev_bridge.rs +509 -10
- package/examples/tauri-bridge/tauri.conf.json +25 -0
- package/package.json +3 -1
- package/rust-bridge/README.md +7 -5
|
@@ -2,19 +2,26 @@ use rand::Rng;
|
|
|
2
2
|
use serde::{Deserialize, Serialize};
|
|
3
3
|
use std::collections::{HashMap, VecDeque};
|
|
4
4
|
use std::fs;
|
|
5
|
-
use std::io::{BufRead, BufReader
|
|
5
|
+
use std::io::{BufRead, BufReader};
|
|
6
6
|
use std::process::{Command, Stdio};
|
|
7
7
|
use std::sync::{Arc, Condvar, Mutex};
|
|
8
8
|
use std::thread;
|
|
9
|
+
use std::time::Instant;
|
|
9
10
|
use tauri::{AppHandle, Manager};
|
|
10
11
|
use tiny_http::{Header, Response, Server};
|
|
11
12
|
use tracing_subscriber::layer::SubscriberExt;
|
|
12
13
|
use tracing_subscriber::util::SubscriberInitExt;
|
|
13
14
|
|
|
15
|
+
/// Bridge protocol version exposed via `GET /version`. Bumped whenever the
|
|
16
|
+
/// HTTP surface changes shape so CLI clients can feature-detect.
|
|
17
|
+
pub const BRIDGE_VERSION: &str = "0.7.0";
|
|
18
|
+
|
|
14
19
|
#[derive(Deserialize)]
|
|
15
20
|
struct EvalRequest {
|
|
16
21
|
js: String,
|
|
17
22
|
token: String,
|
|
23
|
+
#[serde(default)]
|
|
24
|
+
window: Option<String>,
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
#[derive(Deserialize)]
|
|
@@ -41,6 +48,97 @@ struct LogResponse {
|
|
|
41
48
|
entries: Vec<LogEntry>,
|
|
42
49
|
}
|
|
43
50
|
|
|
51
|
+
#[derive(Deserialize)]
|
|
52
|
+
struct DescribeRequest {
|
|
53
|
+
token: String,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#[derive(Serialize, Default)]
|
|
57
|
+
struct DescribeResponse {
|
|
58
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
59
|
+
app: Option<String>,
|
|
60
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
61
|
+
pid: Option<u32>,
|
|
62
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
63
|
+
windows: Option<Vec<String>>,
|
|
64
|
+
capabilities: Vec<String>,
|
|
65
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
66
|
+
surfaces: Option<HashMap<String, String>>,
|
|
67
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
68
|
+
exports: Option<HashMap<String, String>>,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#[derive(Serialize)]
|
|
72
|
+
struct VersionResponse {
|
|
73
|
+
version: String,
|
|
74
|
+
endpoints: Vec<String>,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#[derive(Deserialize)]
|
|
78
|
+
struct AuthedRequest {
|
|
79
|
+
token: String,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[derive(Serialize, Clone)]
|
|
83
|
+
struct SidecarSummary {
|
|
84
|
+
name: String,
|
|
85
|
+
pid: u32,
|
|
86
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
87
|
+
exe: Option<String>,
|
|
88
|
+
args: Vec<String>,
|
|
89
|
+
alive: Option<bool>,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#[derive(Serialize)]
|
|
93
|
+
struct ProcessResponse {
|
|
94
|
+
tauri: TauriProcessInfo,
|
|
95
|
+
sidecars: Vec<SidecarSummary>,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#[derive(Serialize)]
|
|
99
|
+
struct TauriProcessInfo {
|
|
100
|
+
pid: u32,
|
|
101
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
102
|
+
exe: Option<String>,
|
|
103
|
+
args: Vec<String>,
|
|
104
|
+
uptime_ms: u64,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#[derive(Serialize, Clone)]
|
|
108
|
+
struct CapabilityEntry {
|
|
109
|
+
identifier: String,
|
|
110
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
111
|
+
description: Option<String>,
|
|
112
|
+
windows: Vec<String>,
|
|
113
|
+
permissions: Vec<String>,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#[derive(Serialize)]
|
|
117
|
+
struct CapabilitiesResponse {
|
|
118
|
+
/// Capabilities as declared in tauri.conf.json (best-effort: tauri 2 stores
|
|
119
|
+
/// these as either bare strings or inline objects; we surface both shapes).
|
|
120
|
+
declared: Vec<CapabilityEntry>,
|
|
121
|
+
/// Window labels currently registered with Tauri.
|
|
122
|
+
windows: Vec<String>,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#[derive(Serialize)]
|
|
126
|
+
struct DevtoolsResponse {
|
|
127
|
+
platform: String,
|
|
128
|
+
inspectable: bool,
|
|
129
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
130
|
+
url: Option<String>,
|
|
131
|
+
hint: String,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#[derive(Serialize)]
|
|
135
|
+
struct HealthResponse {
|
|
136
|
+
uptime_ms: u64,
|
|
137
|
+
webview_ready: bool,
|
|
138
|
+
sidecars_alive: bool,
|
|
139
|
+
sidecars: Vec<SidecarSummary>,
|
|
140
|
+
}
|
|
141
|
+
|
|
44
142
|
#[derive(Serialize)]
|
|
45
143
|
struct TokenFile {
|
|
46
144
|
port: u16,
|
|
@@ -48,6 +146,92 @@ struct TokenFile {
|
|
|
48
146
|
pid: u32,
|
|
49
147
|
}
|
|
50
148
|
|
|
149
|
+
/// Per-process sidecar metadata captured at spawn time. Used by `/process`
|
|
150
|
+
/// and `/health` so an external diagnostic tool can see the process tree
|
|
151
|
+
/// without scraping `ps`.
|
|
152
|
+
struct SidecarRecord {
|
|
153
|
+
name: String,
|
|
154
|
+
pid: u32,
|
|
155
|
+
exe: Option<String>,
|
|
156
|
+
args: Vec<String>,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// Thread-safe registry of sidecars known to this bridge. Populated by
|
|
160
|
+
/// `spawn_sidecar_monitored` automatically; users with their own spawn flow
|
|
161
|
+
/// can call `register_sidecar` after spawning. Aliveness is computed at
|
|
162
|
+
/// request time via a cheap signal-0 check.
|
|
163
|
+
pub struct SidecarRegistry {
|
|
164
|
+
records: Mutex<Vec<SidecarRecord>>,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
impl SidecarRegistry {
|
|
168
|
+
pub fn new() -> Self {
|
|
169
|
+
Self {
|
|
170
|
+
records: Mutex::new(Vec::new()),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fn add(&self, record: SidecarRecord) {
|
|
175
|
+
let mut recs = self.records.lock().unwrap();
|
|
176
|
+
recs.push(record);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fn snapshot(&self) -> Vec<SidecarSummary> {
|
|
180
|
+
let recs = self.records.lock().unwrap();
|
|
181
|
+
recs.iter()
|
|
182
|
+
.map(|r| SidecarSummary {
|
|
183
|
+
name: r.name.clone(),
|
|
184
|
+
pid: r.pid,
|
|
185
|
+
exe: r.exe.clone(),
|
|
186
|
+
args: r.args.clone(),
|
|
187
|
+
alive: pid_alive(r.pid),
|
|
188
|
+
})
|
|
189
|
+
.collect()
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
impl Default for SidecarRegistry {
|
|
194
|
+
fn default() -> Self {
|
|
195
|
+
Self::new()
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// Best-effort liveness probe for a sidecar PID. Returns `Some(true)` if the
|
|
200
|
+
/// process is running, `Some(false)` if it has exited, and `None` when we
|
|
201
|
+
/// can't determine (e.g., on Windows where we don't ship a probe in v1).
|
|
202
|
+
#[cfg(unix)]
|
|
203
|
+
fn pid_alive(pid: u32) -> Option<bool> {
|
|
204
|
+
// SAFETY: libc::kill with signal 0 just checks process existence and never
|
|
205
|
+
// delivers a signal. Returns 0 on success, -1 on error (e.g., ESRCH).
|
|
206
|
+
let rc = unsafe { libc::kill(pid as libc::pid_t, 0) };
|
|
207
|
+
Some(rc == 0)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#[cfg(not(unix))]
|
|
211
|
+
fn pid_alive(_pid: u32) -> Option<bool> {
|
|
212
|
+
None
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/// Register a sidecar process with the bridge so it shows up in `/process`
|
|
216
|
+
/// and `/health` responses. Callers that use `spawn_sidecar_monitored` get
|
|
217
|
+
/// this for free; callers who spawn their own children can register them
|
|
218
|
+
/// here. Idempotent in the sense that re-registering a name is allowed
|
|
219
|
+
/// (both entries will be reported).
|
|
220
|
+
pub fn register_sidecar(
|
|
221
|
+
registry: &Arc<SidecarRegistry>,
|
|
222
|
+
name: &str,
|
|
223
|
+
pid: u32,
|
|
224
|
+
exe: Option<String>,
|
|
225
|
+
args: Vec<String>,
|
|
226
|
+
) {
|
|
227
|
+
registry.add(SidecarRecord {
|
|
228
|
+
name: name.to_string(),
|
|
229
|
+
pid,
|
|
230
|
+
exe,
|
|
231
|
+
args,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
51
235
|
/// Ring buffer for log entries. Thread-safe, capped at 1000 entries.
|
|
52
236
|
pub struct LogBuffer {
|
|
53
237
|
entries: Mutex<VecDeque<LogEntry>>,
|
|
@@ -151,12 +335,15 @@ pub fn create_log_layer(
|
|
|
151
335
|
|
|
152
336
|
/// Spawn a sidecar process with monitored stdout/stderr.
|
|
153
337
|
/// Lines from stdout are logged as "info", lines from stderr as "warn".
|
|
154
|
-
/// Returns the `std::process::Child` handle.
|
|
338
|
+
/// Returns the `std::process::Child` handle. If a `SidecarRegistry` is
|
|
339
|
+
/// supplied (recommended), the child is also recorded for `/process` and
|
|
340
|
+
/// `/health` responses; pass `None` to opt out of registry tracking.
|
|
155
341
|
pub fn spawn_sidecar_monitored(
|
|
156
342
|
name: &str,
|
|
157
343
|
command: &str,
|
|
158
344
|
args: &[&str],
|
|
159
345
|
log_buffer: &Arc<LogBuffer>,
|
|
346
|
+
registry: Option<&Arc<SidecarRegistry>>,
|
|
160
347
|
) -> Result<std::process::Child, String> {
|
|
161
348
|
let mut child = Command::new(command)
|
|
162
349
|
.args(args)
|
|
@@ -165,6 +352,15 @@ pub fn spawn_sidecar_monitored(
|
|
|
165
352
|
.spawn()
|
|
166
353
|
.map_err(|e| format!("Failed to spawn sidecar {name}: {e}"))?;
|
|
167
354
|
|
|
355
|
+
if let Some(reg) = registry {
|
|
356
|
+
reg.add(SidecarRecord {
|
|
357
|
+
name: name.to_string(),
|
|
358
|
+
pid: child.id(),
|
|
359
|
+
exe: Some(command.to_string()),
|
|
360
|
+
args: args.iter().map(|s| s.to_string()).collect(),
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
168
364
|
let source = format!("sidecar:{name}");
|
|
169
365
|
|
|
170
366
|
// Monitor stdout
|
|
@@ -235,9 +431,16 @@ pub fn __dev_bridge_result(
|
|
|
235
431
|
}
|
|
236
432
|
|
|
237
433
|
/// Start the development bridge HTTP server.
|
|
238
|
-
///
|
|
239
|
-
///
|
|
240
|
-
|
|
434
|
+
///
|
|
435
|
+
/// Returns the bound port, a shared log buffer, and a sidecar registry. Both
|
|
436
|
+
/// the buffer and registry are intended to be passed back to
|
|
437
|
+
/// `spawn_sidecar_monitored` for any sidecar processes you launch; the
|
|
438
|
+
/// registry is what powers the `/process` and `/health` endpoints' visibility
|
|
439
|
+
/// into the process tree. Callers that don't spawn sidecars can ignore the
|
|
440
|
+
/// registry handle.
|
|
441
|
+
pub fn start_bridge(
|
|
442
|
+
app: &AppHandle,
|
|
443
|
+
) -> Result<(u16, Arc<LogBuffer>, Arc<SidecarRegistry>), String> {
|
|
241
444
|
let server =
|
|
242
445
|
Server::http("127.0.0.1:0").map_err(|e| format!("Failed to start bridge: {e}"))?;
|
|
243
446
|
let port = server
|
|
@@ -283,19 +486,60 @@ pub fn start_bridge(app: &AppHandle) -> Result<(u16, Arc<LogBuffer>), String> {
|
|
|
283
486
|
});
|
|
284
487
|
app.manage(pending.clone());
|
|
285
488
|
|
|
489
|
+
// Sidecar registry — exposed to integrators via the return tuple and
|
|
490
|
+
// consulted by /process and /health.
|
|
491
|
+
let sidecar_registry = Arc::new(SidecarRegistry::new());
|
|
492
|
+
app.manage(sidecar_registry.clone());
|
|
493
|
+
|
|
494
|
+
// Capture process start metadata once so /process and /health don't pay
|
|
495
|
+
// for the lookup on every request.
|
|
496
|
+
let start_instant = Instant::now();
|
|
497
|
+
let tauri_pid = std::process::id();
|
|
498
|
+
let tauri_exe = std::env::current_exe()
|
|
499
|
+
.ok()
|
|
500
|
+
.and_then(|p| p.to_str().map(|s| s.to_string()));
|
|
501
|
+
let tauri_args: Vec<String> = std::env::args().collect();
|
|
502
|
+
|
|
286
503
|
let app_handle = app.clone();
|
|
287
504
|
let expected_token = token.clone();
|
|
288
505
|
let server_log_buffer = log_buffer.clone();
|
|
506
|
+
let server_registry = sidecar_registry.clone();
|
|
289
507
|
|
|
290
508
|
thread::spawn(move || {
|
|
291
509
|
// Keep _guard alive for the lifetime of the server thread
|
|
292
510
|
let _cleanup = _guard;
|
|
293
511
|
|
|
294
|
-
for request in server.incoming_requests() {
|
|
512
|
+
for mut request in server.incoming_requests() {
|
|
295
513
|
let is_post = request.method().as_str() == "POST";
|
|
296
514
|
let url = request.url().to_string();
|
|
297
515
|
|
|
298
|
-
|
|
516
|
+
// Handle GET /version (no auth needed). Clients feature-detect
|
|
517
|
+
// newer endpoints by checking the `endpoints` array.
|
|
518
|
+
if url == "/version" && request.method().as_str() == "GET" {
|
|
519
|
+
let resp = VersionResponse {
|
|
520
|
+
version: BRIDGE_VERSION.to_string(),
|
|
521
|
+
endpoints: vec![
|
|
522
|
+
"/eval".to_string(),
|
|
523
|
+
"/logs".to_string(),
|
|
524
|
+
"/describe".to_string(),
|
|
525
|
+
"/version".to_string(),
|
|
526
|
+
"/process".to_string(),
|
|
527
|
+
"/capabilities".to_string(),
|
|
528
|
+
"/devtools".to_string(),
|
|
529
|
+
"/health".to_string(),
|
|
530
|
+
],
|
|
531
|
+
};
|
|
532
|
+
let json = serde_json::to_string(&resp).unwrap();
|
|
533
|
+
let header = Header::from_bytes("Content-Type", "application/json").unwrap();
|
|
534
|
+
let _ = request.respond(Response::from_string(json).with_header(header));
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
let known_post = matches!(
|
|
539
|
+
url.as_str(),
|
|
540
|
+
"/eval" | "/logs" | "/describe" | "/process" | "/capabilities" | "/devtools" | "/health"
|
|
541
|
+
);
|
|
542
|
+
if !is_post || !known_post {
|
|
299
543
|
let _ = request.respond(Response::from_string("Not found").with_status_code(404));
|
|
300
544
|
continue;
|
|
301
545
|
}
|
|
@@ -333,6 +577,153 @@ pub fn start_bridge(app: &AppHandle) -> Result<(u16, Arc<LogBuffer>), String> {
|
|
|
333
577
|
continue;
|
|
334
578
|
}
|
|
335
579
|
|
|
580
|
+
// Handle /process endpoint — Tauri PID + sidecar registry snapshot.
|
|
581
|
+
if url == "/process" {
|
|
582
|
+
let req: AuthedRequest = match serde_json::from_str(&body) {
|
|
583
|
+
Ok(r) => r,
|
|
584
|
+
Err(_) => {
|
|
585
|
+
let _ = request
|
|
586
|
+
.respond(Response::from_string("Invalid JSON").with_status_code(400));
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
if req.token != expected_token {
|
|
591
|
+
let _ = request
|
|
592
|
+
.respond(Response::from_string("Unauthorized").with_status_code(401));
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
let resp = ProcessResponse {
|
|
596
|
+
tauri: TauriProcessInfo {
|
|
597
|
+
pid: tauri_pid,
|
|
598
|
+
exe: tauri_exe.clone(),
|
|
599
|
+
args: tauri_args.clone(),
|
|
600
|
+
uptime_ms: start_instant.elapsed().as_millis() as u64,
|
|
601
|
+
},
|
|
602
|
+
sidecars: server_registry.snapshot(),
|
|
603
|
+
};
|
|
604
|
+
let json = serde_json::to_string(&resp).unwrap();
|
|
605
|
+
let header = Header::from_bytes("Content-Type", "application/json").unwrap();
|
|
606
|
+
let _ = request.respond(Response::from_string(json).with_header(header));
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Handle /capabilities endpoint — declared Tauri capability set per window.
|
|
611
|
+
if url == "/capabilities" {
|
|
612
|
+
let req: AuthedRequest = match serde_json::from_str(&body) {
|
|
613
|
+
Ok(r) => r,
|
|
614
|
+
Err(_) => {
|
|
615
|
+
let _ = request
|
|
616
|
+
.respond(Response::from_string("Invalid JSON").with_status_code(400));
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
if req.token != expected_token {
|
|
621
|
+
let _ = request
|
|
622
|
+
.respond(Response::from_string("Unauthorized").with_status_code(401));
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
let windows: Vec<String> = app_handle.webview_windows().keys().cloned().collect();
|
|
627
|
+
let declared = collect_declared_capabilities(&app_handle);
|
|
628
|
+
let resp = CapabilitiesResponse { declared, windows };
|
|
629
|
+
let json = serde_json::to_string(&resp).unwrap();
|
|
630
|
+
let header = Header::from_bytes("Content-Type", "application/json").unwrap();
|
|
631
|
+
let _ = request.respond(Response::from_string(json).with_header(header));
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Handle /devtools endpoint — inspector URL or platform hint.
|
|
636
|
+
if url == "/devtools" {
|
|
637
|
+
let req: AuthedRequest = match serde_json::from_str(&body) {
|
|
638
|
+
Ok(r) => r,
|
|
639
|
+
Err(_) => {
|
|
640
|
+
let _ = request
|
|
641
|
+
.respond(Response::from_string("Invalid JSON").with_status_code(400));
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
if req.token != expected_token {
|
|
646
|
+
let _ = request
|
|
647
|
+
.respond(Response::from_string("Unauthorized").with_status_code(401));
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
let resp = devtools_response();
|
|
651
|
+
let json = serde_json::to_string(&resp).unwrap();
|
|
652
|
+
let header = Header::from_bytes("Content-Type", "application/json").unwrap();
|
|
653
|
+
let _ = request.respond(Response::from_string(json).with_header(header));
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Handle /health endpoint — quick "is this app sick" check.
|
|
658
|
+
if url == "/health" {
|
|
659
|
+
let req: AuthedRequest = match serde_json::from_str(&body) {
|
|
660
|
+
Ok(r) => r,
|
|
661
|
+
Err(_) => {
|
|
662
|
+
let _ = request
|
|
663
|
+
.respond(Response::from_string("Invalid JSON").with_status_code(400));
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
if req.token != expected_token {
|
|
668
|
+
let _ = request
|
|
669
|
+
.respond(Response::from_string("Unauthorized").with_status_code(401));
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
let sidecars = server_registry.snapshot();
|
|
673
|
+
let sidecars_alive = sidecars.iter().all(|s| matches!(s.alive, Some(true) | None));
|
|
674
|
+
let webview_ready = !app_handle.webview_windows().is_empty();
|
|
675
|
+
let resp = HealthResponse {
|
|
676
|
+
uptime_ms: start_instant.elapsed().as_millis() as u64,
|
|
677
|
+
webview_ready,
|
|
678
|
+
sidecars_alive,
|
|
679
|
+
sidecars,
|
|
680
|
+
};
|
|
681
|
+
let json = serde_json::to_string(&resp).unwrap();
|
|
682
|
+
let header = Header::from_bytes("Content-Type", "application/json").unwrap();
|
|
683
|
+
let _ = request.respond(Response::from_string(json).with_header(header));
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Handle /describe endpoint
|
|
688
|
+
if url == "/describe" {
|
|
689
|
+
let desc_req: DescribeRequest = match serde_json::from_str(&body) {
|
|
690
|
+
Ok(r) => r,
|
|
691
|
+
Err(_) => {
|
|
692
|
+
let _ = request
|
|
693
|
+
.respond(Response::from_string("Invalid JSON").with_status_code(400));
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
if desc_req.token != expected_token {
|
|
699
|
+
let _ = request
|
|
700
|
+
.respond(Response::from_string("Unauthorized").with_status_code(401));
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
let windows: Vec<String> = app_handle
|
|
705
|
+
.webview_windows()
|
|
706
|
+
.keys()
|
|
707
|
+
.cloned()
|
|
708
|
+
.collect();
|
|
709
|
+
|
|
710
|
+
let resp = DescribeResponse {
|
|
711
|
+
pid: Some(std::process::id()),
|
|
712
|
+
windows: Some(windows),
|
|
713
|
+
capabilities: vec![
|
|
714
|
+
"eval".to_string(),
|
|
715
|
+
"logs".to_string(),
|
|
716
|
+
"describe".to_string(),
|
|
717
|
+
],
|
|
718
|
+
..Default::default()
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
let json = serde_json::to_string(&resp).unwrap();
|
|
722
|
+
let header = Header::from_bytes("Content-Type", "application/json").unwrap();
|
|
723
|
+
let _ = request.respond(Response::from_string(json).with_header(header));
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
|
|
336
727
|
// Handle /eval endpoint
|
|
337
728
|
let eval_req: EvalRequest = match serde_json::from_str(&body) {
|
|
338
729
|
Ok(r) => r,
|
|
@@ -353,7 +744,8 @@ pub fn start_bridge(app: &AppHandle) -> Result<(u16, Arc<LogBuffer>), String> {
|
|
|
353
744
|
// Evaluate JS in webview via callback pattern
|
|
354
745
|
let request_id = uuid::Uuid::new_v4().to_string();
|
|
355
746
|
|
|
356
|
-
|
|
747
|
+
let window_label = eval_req.window.as_deref().unwrap_or("main");
|
|
748
|
+
if let Some(window) = app_handle.get_webview_window(window_label) {
|
|
357
749
|
// Build JS that evaluates the expression, then calls back into Rust
|
|
358
750
|
// via __TAURI__.core.invoke() to deliver the result.
|
|
359
751
|
let callback_js = format!(
|
|
@@ -436,8 +828,115 @@ pub fn start_bridge(app: &AppHandle) -> Result<(u16, Arc<LogBuffer>), String> {
|
|
|
436
828
|
}
|
|
437
829
|
});
|
|
438
830
|
|
|
439
|
-
eprintln!("Dev bridge started on port {port}");
|
|
831
|
+
eprintln!("Dev bridge {BRIDGE_VERSION} started on port {port}");
|
|
440
832
|
eprintln!("Token file: {token_path}");
|
|
441
833
|
|
|
442
|
-
Ok((port, log_buffer))
|
|
834
|
+
Ok((port, log_buffer, sidecar_registry))
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/// Read declared capabilities from tauri.conf.json via `app.config()`. Returns
|
|
838
|
+
/// a flat list of capability entries. Tauri 2 lets capabilities be either bare
|
|
839
|
+
/// permission identifiers (strings) or full inline definitions; we surface
|
|
840
|
+
/// both as `CapabilityEntry` rows with `permissions` populated where possible.
|
|
841
|
+
fn collect_declared_capabilities(app: &AppHandle) -> Vec<CapabilityEntry> {
|
|
842
|
+
let config = app.config();
|
|
843
|
+
let security = &config.app.security;
|
|
844
|
+
let mut out = Vec::new();
|
|
845
|
+
for cap in &security.capabilities {
|
|
846
|
+
let raw = serde_json::to_value(cap).unwrap_or(serde_json::Value::Null);
|
|
847
|
+
match &raw {
|
|
848
|
+
serde_json::Value::String(s) => {
|
|
849
|
+
// Capability declared by reference to a JSON file. We don't have
|
|
850
|
+
// the resolved contents at runtime here, but we surface the
|
|
851
|
+
// identifier so callers know what was requested.
|
|
852
|
+
out.push(CapabilityEntry {
|
|
853
|
+
identifier: s.clone(),
|
|
854
|
+
description: None,
|
|
855
|
+
windows: Vec::new(),
|
|
856
|
+
permissions: Vec::new(),
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
serde_json::Value::Object(map) => {
|
|
860
|
+
let identifier = map
|
|
861
|
+
.get("identifier")
|
|
862
|
+
.and_then(|v| v.as_str())
|
|
863
|
+
.unwrap_or("<inline>")
|
|
864
|
+
.to_string();
|
|
865
|
+
let description = map
|
|
866
|
+
.get("description")
|
|
867
|
+
.and_then(|v| v.as_str())
|
|
868
|
+
.map(|s| s.to_string());
|
|
869
|
+
let windows: Vec<String> = map
|
|
870
|
+
.get("windows")
|
|
871
|
+
.and_then(|v| v.as_array())
|
|
872
|
+
.map(|arr| {
|
|
873
|
+
arr.iter()
|
|
874
|
+
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
|
875
|
+
.collect()
|
|
876
|
+
})
|
|
877
|
+
.unwrap_or_default();
|
|
878
|
+
let permissions: Vec<String> = map
|
|
879
|
+
.get("permissions")
|
|
880
|
+
.and_then(|v| v.as_array())
|
|
881
|
+
.map(|arr| {
|
|
882
|
+
arr.iter()
|
|
883
|
+
.filter_map(|v| match v {
|
|
884
|
+
serde_json::Value::String(s) => Some(s.clone()),
|
|
885
|
+
serde_json::Value::Object(o) => o
|
|
886
|
+
.get("identifier")
|
|
887
|
+
.and_then(|i| i.as_str())
|
|
888
|
+
.map(|s| s.to_string()),
|
|
889
|
+
_ => None,
|
|
890
|
+
})
|
|
891
|
+
.collect()
|
|
892
|
+
})
|
|
893
|
+
.unwrap_or_default();
|
|
894
|
+
out.push(CapabilityEntry {
|
|
895
|
+
identifier,
|
|
896
|
+
description,
|
|
897
|
+
windows,
|
|
898
|
+
permissions,
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
_ => {}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
out
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/// Build a `/devtools` response for the current platform. v1 emits useful
|
|
908
|
+
/// hints rather than always producing a hot inspector URL — Safari attach on
|
|
909
|
+
/// macOS requires UI activation, Windows WebView2 needs a launch-time arg.
|
|
910
|
+
fn devtools_response() -> DevtoolsResponse {
|
|
911
|
+
if cfg!(target_os = "macos") {
|
|
912
|
+
DevtoolsResponse {
|
|
913
|
+
platform: "wkwebview".to_string(),
|
|
914
|
+
inspectable: cfg!(debug_assertions),
|
|
915
|
+
url: None,
|
|
916
|
+
hint: "Open Safari > Develop > <Mac name> > <App name> to attach. \
|
|
917
|
+
Requires the app to be built with debug_assertions (i.e., `tauri dev`)."
|
|
918
|
+
.to_string(),
|
|
919
|
+
}
|
|
920
|
+
} else if cfg!(target_os = "windows") {
|
|
921
|
+
let port = std::env::var("WEBVIEW2_REMOTE_DEBUGGING_PORT").ok();
|
|
922
|
+
let url = port.as_ref().map(|p| format!("http://127.0.0.1:{p}"));
|
|
923
|
+
DevtoolsResponse {
|
|
924
|
+
platform: "webview2".to_string(),
|
|
925
|
+
inspectable: url.is_some(),
|
|
926
|
+
url,
|
|
927
|
+
hint: "Set WEBVIEW2_REMOTE_DEBUGGING_PORT=9222 before launching, \
|
|
928
|
+
then open http://127.0.0.1:9222 in Chrome/Edge to inspect."
|
|
929
|
+
.to_string(),
|
|
930
|
+
}
|
|
931
|
+
} else {
|
|
932
|
+
let inspector = std::env::var("WEBKIT_INSPECTOR_SERVER").ok();
|
|
933
|
+
DevtoolsResponse {
|
|
934
|
+
platform: "webkitgtk".to_string(),
|
|
935
|
+
inspectable: inspector.is_some(),
|
|
936
|
+
url: inspector.as_ref().map(|s| format!("http://{s}")),
|
|
937
|
+
hint: "Export WEBKIT_INSPECTOR_SERVER=127.0.0.1:9222 before launching, \
|
|
938
|
+
then open http://127.0.0.1:9222 to inspect."
|
|
939
|
+
.to_string(),
|
|
940
|
+
}
|
|
941
|
+
}
|
|
443
942
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://schema.tauri.app/config/2",
|
|
3
|
+
"productName": "tauri-dev-bridge-example",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"identifier": "com.tauri-agent-tools.bridge-example",
|
|
6
|
+
"build": {
|
|
7
|
+
"frontendDist": "../frontend-stub"
|
|
8
|
+
},
|
|
9
|
+
"app": {
|
|
10
|
+
"windows": [
|
|
11
|
+
{
|
|
12
|
+
"label": "main",
|
|
13
|
+
"title": "Bridge Example",
|
|
14
|
+
"width": 800,
|
|
15
|
+
"height": 600
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"security": {
|
|
19
|
+
"csp": null
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"bundle": {
|
|
23
|
+
"active": false
|
|
24
|
+
}
|
|
25
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tauri-agent-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Agent-driven inspection toolkit for Tauri desktop apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
+
"ajv": "^8.20.0",
|
|
48
|
+
"ajv-formats": "^3.0.1",
|
|
47
49
|
"commander": "^14.0.0",
|
|
48
50
|
"zod": "^3.25.76"
|
|
49
51
|
},
|
package/rust-bridge/README.md
CHANGED
|
@@ -82,19 +82,21 @@ tauri-agent-tools eval "document.title"
|
|
|
82
82
|
1. Bridge starts an HTTP server on a random localhost port
|
|
83
83
|
2. A token file with `{ port, token, pid }` is written to `/tmp/`
|
|
84
84
|
3. `tauri-agent-tools` discovers the token file and authenticates via the token
|
|
85
|
-
4.
|
|
86
|
-
5.
|
|
85
|
+
4. The bridge exposes four endpoints: `POST /eval` (JS evaluation), `POST /logs` (Rust log retrieval), `POST /describe` (bridge metadata), and `GET /version` (unauthenticated health check)
|
|
86
|
+
5. `/eval` accepts an optional `window` field to target specific webview windows (defaults to `"main"`)
|
|
87
87
|
6. The injected JS evaluates the expression, then calls back into Rust via `window.__TAURI__.core.invoke("__dev_bridge_result", { id, value })` to deliver the result
|
|
88
88
|
7. The HTTP handler thread waits for the result (up to 5 seconds) and returns it as JSON
|
|
89
|
-
8.
|
|
90
|
-
9.
|
|
89
|
+
8. `/logs` drains the ring buffer of captured `tracing` events and returns them as JSON
|
|
90
|
+
9. `/describe` returns PID, window labels, and capabilities
|
|
91
|
+
10. The token file is cleaned up when the app exits
|
|
91
92
|
|
|
92
93
|
## Security
|
|
93
94
|
|
|
94
95
|
- **Localhost only** — the bridge binds to `127.0.0.1`
|
|
95
96
|
- **Token authenticated** — every request requires a random 32-char token
|
|
96
97
|
- **Development only** — wrapped in `cfg!(debug_assertions)`, stripped in release builds
|
|
97
|
-
- **
|
|
98
|
+
- **Inspection is read-only** — inspection commands only read DOM state
|
|
99
|
+
- **Interaction is debug-only** — interaction commands use eval-based DOM dispatch, sandboxed to the webview
|
|
98
100
|
|
|
99
101
|
## Agent-Assisted Setup
|
|
100
102
|
|