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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ekka-desktop-app",
3
- "version": "0.3.13",
3
+ "version": "0.4.0",
4
4
  "description": "Create an EKKA desktop app with built-in demo backend. No setup required.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- dirs = "5"
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"]
@@ -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
- /// DRIFT GUARD - ARCHITECTURE FREEZE
69
+ /// EXECUTION MODES
71
70
  /// ═══════════════════════════════════════════════════════════════════════════════════════════
72
- /// DO NOT extend this without revisiting the Desktop–Engine architecture decision.
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
- /// The routing switch below (engine vs stub) is FROZEN as of Phase 3G.
75
- /// Engine routing is one-way: once disabled for a session, it stays disabled.
76
- /// The stub path handles all operations locally via SDK handlers.
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
- // LOCAL-ONLY OPERATIONS: Never route to engine
83
- // These are desktop-specific operations that must be handled locally
84
- let local_only = matches!(
85
- req.op.as_str(),
86
- "setup.status" | "nodeCredentials.set" | "nodeCredentials.status" | "nodeCredentials.clear"
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
- // ROUTING SWITCH: If real engine is available and not local-only, route to it
90
- if !local_only && state.is_engine_available() {
91
- if let Some(response) = engine_process::route_to_engine(&req) {
92
- return response;
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
- // Engine failed - permanently disable for this session
95
- state.disable_engine();
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
- // STUB PATH: Handle locally via SDK handlers
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
- "setup.status" => handle_setup_status(&state),
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.runner_tasks")
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
- let engine_url = option_env!("EKKA_ENGINE_URL")
951
- .unwrap_or("http://localhost:3200")
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
- let engine_url = option_env!("EKKA_ENGINE_URL")
1013
- .unwrap_or("http://localhost:3200")
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;