create-ekka-desktop-app 0.3.13 → 0.4.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/package.json +1 -1
- package/template/src-tauri/Cargo.toml +19 -1
- package/template/src-tauri/resources/ekka-engine-bootstrap +0 -0
- package/template/src-tauri/src/commands.rs +150 -35
- package/template/src-tauri/src/config.rs +33 -0
- package/template/src-tauri/src/engine_process.rs +123 -125
- package/template/src-tauri/src/main.rs +270 -369
- package/template/src-tauri/src/node_runner.rs +283 -140
- package/template/src-tauri/src/state.rs +0 -15
- package/template/src-tauri/src/well_known.rs +6 -1
package/package.json
CHANGED
|
@@ -18,8 +18,26 @@ tauri-plugin-dialog = "2"
|
|
|
18
18
|
serde = { version = "1", features = ["derive"] }
|
|
19
19
|
serde_json = "1"
|
|
20
20
|
chrono = { version = "0.4", features = ["serde"] }
|
|
21
|
+
ekka-sdk-core = { path = "../../ekka-execution-node-sdk-rust/crates/framework/ekka-sdk-core" }
|
|
22
|
+
ekka-runner-core = { path = "../../ekka-execution-node-sdk-rust/crates/framework/ekka-runner-core" }
|
|
23
|
+
ekka-runner-local = { path = "../../ekka-execution-node-sdk-rust/crates/apps/ekka-runner-local" }
|
|
24
|
+
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
|
21
25
|
uuid = { version = "1.0", features = ["v4"] }
|
|
22
|
-
|
|
26
|
+
tokio = { version = "1", features = ["sync"] }
|
|
27
|
+
tracing = "0.1"
|
|
28
|
+
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|
29
|
+
ed25519-dalek = { version = "2.1", features = ["rand_core"] }
|
|
30
|
+
rand = "0.8"
|
|
31
|
+
base64 = "0.22"
|
|
32
|
+
zeroize = { version = "1.7", features = ["derive"] }
|
|
33
|
+
dotenvy = "0.15"
|
|
34
|
+
hex = "0.4"
|
|
35
|
+
anyhow = "1.0"
|
|
36
|
+
regex = "1"
|
|
37
|
+
lazy_static = "1.4"
|
|
38
|
+
|
|
39
|
+
[dev-dependencies]
|
|
40
|
+
tempfile = "3"
|
|
23
41
|
|
|
24
42
|
[features]
|
|
25
43
|
default = ["custom-protocol"]
|
|
Binary file
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
use crate::bootstrap::initialize_home;
|
|
6
6
|
use crate::config;
|
|
7
|
-
use crate::engine_process;
|
|
8
7
|
use crate::grants::require_home_granted;
|
|
9
8
|
use crate::handlers;
|
|
10
9
|
use crate::node_auth;
|
|
@@ -64,39 +63,133 @@ pub fn engine_disconnect(state: State<EngineState>) {
|
|
|
64
63
|
}
|
|
65
64
|
}
|
|
66
65
|
|
|
67
|
-
/// Main RPC dispatcher - routes all operations to handlers
|
|
66
|
+
/// Main RPC dispatcher - routes all operations to local handlers
|
|
68
67
|
///
|
|
69
68
|
/// ═══════════════════════════════════════════════════════════════════════════════════════════
|
|
70
|
-
///
|
|
69
|
+
/// EXECUTION MODES
|
|
71
70
|
/// ═══════════════════════════════════════════════════════════════════════════════════════════
|
|
72
|
-
///
|
|
71
|
+
/// Cloud Mode (config::REMOTE_ONLY = true):
|
|
72
|
+
/// - Studio features return placeholder or explicit error
|
|
73
|
+
/// - API calls go directly to api.ekka.ai
|
|
73
74
|
///
|
|
74
|
-
///
|
|
75
|
-
///
|
|
76
|
-
///
|
|
77
|
-
///
|
|
78
|
-
/// Any changes to routing logic require explicit architecture review.
|
|
75
|
+
/// Studio Mode (config::REMOTE_ONLY = false):
|
|
76
|
+
/// - Full local + cloud execution
|
|
77
|
+
/// - All commands go directly to local handlers + cloud API
|
|
78
|
+
/// - The spawned engine is a runner runtime (for task execution), NOT a request router
|
|
79
79
|
/// ═══════════════════════════════════════════════════════════════════════════════════════════
|
|
80
80
|
#[tauri::command]
|
|
81
81
|
pub fn engine_request(req: EngineRequest, state: State<EngineState>) -> EngineResponse {
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
req.op.as_str()
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
// Cloud Mode: Handle Studio-only operations specially
|
|
83
|
+
if config::REMOTE_ONLY {
|
|
84
|
+
// CATEGORY 1: Operations that return placeholders (UI calls these but doesn't need real data in Cloud Mode)
|
|
85
|
+
match req.op.as_str() {
|
|
86
|
+
// Home status - return Cloud Mode placeholder (UI expects state field)
|
|
87
|
+
"home.status" => {
|
|
88
|
+
tracing::info!(op = "mode.cloud.placeholder", operation = "home.status", "Returning Cloud Mode placeholder");
|
|
89
|
+
return EngineResponse::ok(serde_json::json!({
|
|
90
|
+
"state": "HOME_GRANTED", // Pretend home is granted so UI proceeds
|
|
91
|
+
"mode": "cloud",
|
|
92
|
+
"home_path": null,
|
|
93
|
+
"marker_exists": false
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
// Node session bootstrap - no-op success (runner not needed in Cloud Mode)
|
|
97
|
+
"nodeSession.bootstrap" => {
|
|
98
|
+
tracing::info!(op = "mode.cloud.placeholder", operation = "nodeSession.bootstrap", "Returning Cloud Mode no-op");
|
|
99
|
+
return EngineResponse::ok(serde_json::json!({
|
|
100
|
+
"ok": true,
|
|
101
|
+
"mode": "cloud",
|
|
102
|
+
"runner_started": false
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
// Node session status - return Cloud Mode placeholder
|
|
106
|
+
"nodeSession.status" => {
|
|
107
|
+
tracing::info!(op = "mode.cloud.placeholder", operation = "nodeSession.status", "Returning Cloud Mode placeholder");
|
|
108
|
+
return EngineResponse::ok(serde_json::json!({
|
|
109
|
+
"hasIdentity": false,
|
|
110
|
+
"hasSession": false,
|
|
111
|
+
"sessionValid": false,
|
|
112
|
+
"mode": "cloud"
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
// Node session ensure identity - return Cloud Mode placeholder
|
|
116
|
+
"nodeSession.ensureIdentity" => {
|
|
117
|
+
tracing::info!(op = "mode.cloud.placeholder", operation = "nodeSession.ensureIdentity", "Returning Cloud Mode placeholder");
|
|
118
|
+
return EngineResponse::ok(serde_json::json!({
|
|
119
|
+
"ok": true,
|
|
120
|
+
"mode": "cloud",
|
|
121
|
+
"node_id": null
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
// Node credentials status - return not configured (no keychain in Cloud Mode)
|
|
125
|
+
"nodeCredentials.status" => {
|
|
126
|
+
tracing::info!(op = "mode.cloud.placeholder", operation = "nodeCredentials.status", "Returning Cloud Mode placeholder");
|
|
127
|
+
return EngineResponse::ok(serde_json::json!({
|
|
128
|
+
"hasCredentials": false,
|
|
129
|
+
"nodeId": null,
|
|
130
|
+
"isAuthenticated": false,
|
|
131
|
+
"mode": "cloud"
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
// Runner status - return stopped (no local runner in Cloud Mode)
|
|
135
|
+
"runner.status" => {
|
|
136
|
+
tracing::info!(op = "mode.cloud.placeholder", operation = "runner.status", "Returning Cloud Mode placeholder");
|
|
137
|
+
return EngineResponse::ok(serde_json::json!({
|
|
138
|
+
"enabled": false,
|
|
139
|
+
"state": "stopped",
|
|
140
|
+
"mode": "cloud"
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
_ => {}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// CATEGORY 2: Studio features unavailable in Cloud Mode
|
|
147
|
+
let is_studio_only = matches!(
|
|
148
|
+
req.op.as_str(),
|
|
149
|
+
// Node credentials mutations
|
|
150
|
+
"nodeCredentials.set" | "nodeCredentials.clear" |
|
|
151
|
+
// Home grant (requires local folder)
|
|
152
|
+
"home.grant" |
|
|
153
|
+
// Paths operations (local grants)
|
|
154
|
+
"paths.check" | "paths.list" | "paths.get" | "paths.request" | "paths.remove" |
|
|
155
|
+
// Vault operations (local encrypted storage)
|
|
156
|
+
"vault.status" | "vault.capabilities" |
|
|
157
|
+
"vault.secrets.list" | "vault.secrets.get" | "vault.secrets.create" |
|
|
158
|
+
"vault.secrets.update" | "vault.secrets.delete" | "vault.secrets.upsert" |
|
|
159
|
+
"vault.bundles.list" | "vault.bundles.get" | "vault.bundles.create" |
|
|
160
|
+
"vault.bundles.rename" | "vault.bundles.delete" | "vault.bundles.listSecrets" |
|
|
161
|
+
"vault.bundles.addSecret" | "vault.bundles.removeSecret" |
|
|
162
|
+
"vault.files.writeText" | "vault.files.writeBytes" | "vault.files.readText" |
|
|
163
|
+
"vault.files.readBytes" | "vault.files.list" | "vault.files.exists" |
|
|
164
|
+
"vault.files.delete" | "vault.files.mkdir" | "vault.files.move" |
|
|
165
|
+
"vault.attachSecretsToConnector" | "vault.injectSecretsIntoRun" | "vault.audit.list" |
|
|
166
|
+
// Runner task stats (requires node auth)
|
|
167
|
+
"runner.taskStats" |
|
|
168
|
+
// Database/Queue/Pipeline (Studio-only)
|
|
169
|
+
"db.get" | "db.put" | "db.delete" |
|
|
170
|
+
"queue.enqueue" | "queue.claim" | "queue.ack" | "queue.nack" | "queue.heartbeat" |
|
|
171
|
+
"pipeline.submit" | "pipeline.events" |
|
|
172
|
+
// Debug utilities
|
|
173
|
+
"debug.openFolder" | "debug.resolveVaultPath"
|
|
174
|
+
);
|
|
88
175
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
176
|
+
if is_studio_only {
|
|
177
|
+
tracing::info!(
|
|
178
|
+
op = "mode.cloud.studio_feature_unavailable",
|
|
179
|
+
operation = %req.op,
|
|
180
|
+
"Studio feature unavailable in Cloud Mode"
|
|
181
|
+
);
|
|
182
|
+
return EngineResponse::err("CLOUD_MODE", config::CLOUD_MODE_FEATURE_ERROR);
|
|
93
183
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
tracing::warn!(op = "engine.disabled.session", "Engine routing disabled for session, using stub");
|
|
184
|
+
|
|
185
|
+
// Cloud Mode: Never route to local engine, fall through to handler dispatch
|
|
97
186
|
}
|
|
98
187
|
|
|
99
|
-
//
|
|
188
|
+
// Studio Mode: All commands go directly to local handlers + cloud API.
|
|
189
|
+
// The spawned engine is a runner runtime (for task execution), not a request router.
|
|
190
|
+
// No engine routing is performed - this eliminates 404s and fallback spam.
|
|
191
|
+
|
|
192
|
+
// HANDLER DISPATCH: Handle operations locally or proxy to remote API
|
|
100
193
|
|
|
101
194
|
// Check connected (except for status operations and setup)
|
|
102
195
|
if !matches!(req.op.as_str(), "runtime.info" | "home.status" | "vault.status" | "setup.status" | "nodeCredentials.set" | "nodeCredentials.status" | "nodeCredentials.clear") {
|
|
@@ -116,7 +209,19 @@ pub fn engine_request(req: EngineRequest, state: State<EngineState>) -> EngineRe
|
|
|
116
209
|
// Dispatch based on operation
|
|
117
210
|
match req.op.as_str() {
|
|
118
211
|
// Setup Status (pre-login, no connection required)
|
|
119
|
-
|
|
212
|
+
// In Cloud Mode, skip node identity requirement
|
|
213
|
+
"setup.status" => {
|
|
214
|
+
if config::REMOTE_ONLY {
|
|
215
|
+
tracing::info!(op = "mode.cloud.setup_status", "Setup status in Cloud Mode");
|
|
216
|
+
return EngineResponse::ok(serde_json::json!({
|
|
217
|
+
"nodeIdentity": "cloud",
|
|
218
|
+
"setupComplete": true,
|
|
219
|
+
"mode": "cloud",
|
|
220
|
+
"apiBase": config::REMOTE_API_BASE
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
handle_setup_status(&state)
|
|
224
|
+
}
|
|
120
225
|
|
|
121
226
|
// Auth
|
|
122
227
|
"auth.set" => auth::handle_set(&req.payload, &state),
|
|
@@ -450,8 +555,8 @@ fn handle_node_credentials_clear() -> EngineResponse {
|
|
|
450
555
|
// Runner Task Stats (Proxied HTTP)
|
|
451
556
|
// =============================================================================
|
|
452
557
|
|
|
453
|
-
/// Fetch runner task stats from engine API.
|
|
454
|
-
/// Proxies GET /engine/runner-tasks/stats through Rust to avoid CORS.
|
|
558
|
+
/// Fetch runner task stats from engine API (V2).
|
|
559
|
+
/// Proxies GET /engine/runner-tasks-v2/stats through Rust to avoid CORS.
|
|
455
560
|
/// Auto-authenticates from keychain if token is missing (single-flight, no retry on failure).
|
|
456
561
|
fn handle_runner_task_stats(state: &EngineState) -> EngineResponse {
|
|
457
562
|
use crate::state::NodeAuthState;
|
|
@@ -532,15 +637,15 @@ fn handle_runner_task_stats(state: &EngineState) -> EngineResponse {
|
|
|
532
637
|
|
|
533
638
|
let request_id = uuid::Uuid::new_v4().to_string();
|
|
534
639
|
|
|
535
|
-
// Make request with security envelope headers
|
|
640
|
+
// Make request with security envelope headers (V2 endpoint)
|
|
536
641
|
let response = client
|
|
537
|
-
.get(format!("{}/engine/runner-tasks/stats", engine_url))
|
|
642
|
+
.get(format!("{}/engine/runner-tasks-v2/stats", engine_url))
|
|
538
643
|
.header("Content-Type", "application/json")
|
|
539
644
|
.header("Authorization", format!("Bearer {}", node_token.token))
|
|
540
645
|
.header("X-EKKA-PROOF-TYPE", "jwt")
|
|
541
646
|
.header("X-REQUEST-ID", &request_id)
|
|
542
647
|
.header("X-EKKA-CORRELATION-ID", &request_id)
|
|
543
|
-
.header("X-EKKA-MODULE", "engine.
|
|
648
|
+
.header("X-EKKA-MODULE", "engine.runner_tasks_v2")
|
|
544
649
|
.header("X-EKKA-ACTION", "stats")
|
|
545
650
|
.header("X-EKKA-CLIENT", config::app_slug())
|
|
546
651
|
.header("X-EKKA-CLIENT-VERSION", "0.2.0")
|
|
@@ -947,9 +1052,14 @@ fn build_security_headers(jwt: Option<&str>, module: &str, action: &str) -> Vec<
|
|
|
947
1052
|
|
|
948
1053
|
/// Create a workflow run (POST /engine/workflow-runs)
|
|
949
1054
|
fn handle_workflow_runs_create(payload: &Value) -> EngineResponse {
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
.to_string()
|
|
1055
|
+
// Cloud Mode: Use remote API base
|
|
1056
|
+
let engine_url = if config::REMOTE_ONLY {
|
|
1057
|
+
config::REMOTE_API_BASE.to_string()
|
|
1058
|
+
} else {
|
|
1059
|
+
option_env!("EKKA_ENGINE_URL")
|
|
1060
|
+
.unwrap_or("http://localhost:3200")
|
|
1061
|
+
.to_string()
|
|
1062
|
+
};
|
|
953
1063
|
|
|
954
1064
|
// Extract request body
|
|
955
1065
|
let request = match payload.get("request") {
|
|
@@ -1009,9 +1119,14 @@ fn handle_workflow_runs_create(payload: &Value) -> EngineResponse {
|
|
|
1009
1119
|
|
|
1010
1120
|
/// Get a workflow run (GET /engine/workflow-runs/{id})
|
|
1011
1121
|
fn handle_workflow_runs_get(payload: &Value) -> EngineResponse {
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
.to_string()
|
|
1122
|
+
// Cloud Mode: Use remote API base
|
|
1123
|
+
let engine_url = if config::REMOTE_ONLY {
|
|
1124
|
+
config::REMOTE_API_BASE.to_string()
|
|
1125
|
+
} else {
|
|
1126
|
+
option_env!("EKKA_ENGINE_URL")
|
|
1127
|
+
.unwrap_or("http://localhost:3200")
|
|
1128
|
+
.to_string()
|
|
1129
|
+
};
|
|
1015
1130
|
|
|
1016
1131
|
// Extract workflow run ID
|
|
1017
1132
|
let id = match payload.get("id").and_then(|v| v.as_str()) {
|
|
@@ -33,3 +33,36 @@ baked_config!(keychain_service, "EKKA_KEYCHAIN_SERVICE");
|
|
|
33
33
|
|
|
34
34
|
// EKKA Engine URL (e.g., "https://api.ekka.ai")
|
|
35
35
|
baked_config!(engine_url, "EKKA_ENGINE_URL");
|
|
36
|
+
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// EXECUTION MODES
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// Cloud Mode (REMOTE_ONLY=true):
|
|
41
|
+
// - Cloud-connected, no local execution
|
|
42
|
+
// - All API calls go directly to api.ekka.ai
|
|
43
|
+
// - Studio features (vault, paths, local runner) unavailable
|
|
44
|
+
//
|
|
45
|
+
// Studio Mode (REMOTE_ONLY=false):
|
|
46
|
+
// - Cloud + local execution (full power)
|
|
47
|
+
// - Local engine/runner spawning enabled
|
|
48
|
+
// - All features available
|
|
49
|
+
//
|
|
50
|
+
// Offline Mode (future):
|
|
51
|
+
// - Local-only, no cloud calls
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
/// Cloud Mode flag - when true, only cloud features are available
|
|
55
|
+
/// Internal constant name kept for compatibility
|
|
56
|
+
pub const REMOTE_ONLY: bool = false;
|
|
57
|
+
|
|
58
|
+
/// API base URL for Cloud Mode
|
|
59
|
+
pub const REMOTE_API_BASE: &str = "https://api.ekka.ai";
|
|
60
|
+
|
|
61
|
+
/// User-visible mode name
|
|
62
|
+
pub const MODE_NAME: &str = if REMOTE_ONLY { "Cloud Mode" } else { "Studio Mode" };
|
|
63
|
+
|
|
64
|
+
/// Error message for Studio features unavailable in Cloud Mode
|
|
65
|
+
pub const CLOUD_MODE_FEATURE_ERROR: &str = "This feature requires EKKA Studio. Cloud Mode is enabled — local execution is not available.";
|
|
66
|
+
|
|
67
|
+
/// Legacy alias (internal use only)
|
|
68
|
+
pub const REMOTE_ONLY_ERROR: &str = CLOUD_MODE_FEATURE_ERROR;
|