Skip to content

Commit 9632561

Browse files
thrashr888claude
andcommitted
feat: multi-server support in desktop app
- Add ServerEntry type and servers list to Settings (Rust + TS) - Sidebar server switcher dropdown (visible with 2+ servers) - Settings page with per-server cards: add, remove, rename, switch - Server process management: start/stop agentkernel from the app - SetupWizard "Start Server" button when not connected - Tray status shows active server name - Auto-migration from single-server settings format - Remove legacy receipt compatibility section from Receipts page - Add new pages: Benchmark, Images, Jobs, Permissions, Sessions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e173ec1 commit 9632561

32 files changed

Lines changed: 2467 additions & 122 deletions

app/src-tauri/Cargo.lock

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/src-tauri/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ thiserror = "2"
2626
base64 = "0.22"
2727
dirs = "6"
2828
toml = "0.8"
29+
url = "2"
30+
which = "7"
2931
tauri-plugin-updater = "2"
3032
tauri-plugin-debug-bridge = { version = "0.4", optional = true }
3133

app/src-tauri/capabilities/default.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"permissions": [
66
"core:default",
77
"shell:allow-open",
8-
"updater:default"
8+
"updater:default",
9+
"debug-bridge:default"
910
]
1011
}

app/src-tauri/gen/schemas/acl-manifests.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"default":{"identifier":"default","description":"Default capabilities for AgentKernel","local":true,"windows":["main"],"permissions":["core:default","shell:allow-open","updater:default"]}}
1+
{"default":{"identifier":"default","description":"Default capabilities for AgentKernel","local":true,"windows":["main"],"permissions":["core:default","shell:allow-open","updater:default","debug-bridge:default"]}}

app/src-tauri/gen/schemas/desktop-schema.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2354,6 +2354,36 @@
23542354
"const": "core:window:deny-unminimize",
23552355
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
23562356
},
2357+
{
2358+
"description": "Default permissions for the debug bridge plugin. Allows the eval callback command.\n#### This default permission set includes:\n\n- `allow-eval-callback`\n- `allow-console-callback`",
2359+
"type": "string",
2360+
"const": "debug-bridge:default",
2361+
"markdownDescription": "Default permissions for the debug bridge plugin. Allows the eval callback command.\n#### This default permission set includes:\n\n- `allow-eval-callback`\n- `allow-console-callback`"
2362+
},
2363+
{
2364+
"description": "Enables the console_callback command without any pre-configured scope.",
2365+
"type": "string",
2366+
"const": "debug-bridge:allow-console-callback",
2367+
"markdownDescription": "Enables the console_callback command without any pre-configured scope."
2368+
},
2369+
{
2370+
"description": "Enables the eval_callback command without any pre-configured scope.",
2371+
"type": "string",
2372+
"const": "debug-bridge:allow-eval-callback",
2373+
"markdownDescription": "Enables the eval_callback command without any pre-configured scope."
2374+
},
2375+
{
2376+
"description": "Denies the console_callback command without any pre-configured scope.",
2377+
"type": "string",
2378+
"const": "debug-bridge:deny-console-callback",
2379+
"markdownDescription": "Denies the console_callback command without any pre-configured scope."
2380+
},
2381+
{
2382+
"description": "Denies the eval_callback command without any pre-configured scope.",
2383+
"type": "string",
2384+
"const": "debug-bridge:deny-eval-callback",
2385+
"markdownDescription": "Denies the eval_callback command without any pre-configured scope."
2386+
},
23572387
{
23582388
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
23592389
"type": "string",

app/src-tauri/gen/schemas/macOS-schema.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2354,6 +2354,36 @@
23542354
"const": "core:window:deny-unminimize",
23552355
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
23562356
},
2357+
{
2358+
"description": "Default permissions for the debug bridge plugin. Allows the eval callback command.\n#### This default permission set includes:\n\n- `allow-eval-callback`\n- `allow-console-callback`",
2359+
"type": "string",
2360+
"const": "debug-bridge:default",
2361+
"markdownDescription": "Default permissions for the debug bridge plugin. Allows the eval callback command.\n#### This default permission set includes:\n\n- `allow-eval-callback`\n- `allow-console-callback`"
2362+
},
2363+
{
2364+
"description": "Enables the console_callback command without any pre-configured scope.",
2365+
"type": "string",
2366+
"const": "debug-bridge:allow-console-callback",
2367+
"markdownDescription": "Enables the console_callback command without any pre-configured scope."
2368+
},
2369+
{
2370+
"description": "Enables the eval_callback command without any pre-configured scope.",
2371+
"type": "string",
2372+
"const": "debug-bridge:allow-eval-callback",
2373+
"markdownDescription": "Enables the eval_callback command without any pre-configured scope."
2374+
},
2375+
{
2376+
"description": "Denies the console_callback command without any pre-configured scope.",
2377+
"type": "string",
2378+
"const": "debug-bridge:deny-console-callback",
2379+
"markdownDescription": "Denies the console_callback command without any pre-configured scope."
2380+
},
2381+
{
2382+
"description": "Denies the eval_callback command without any pre-configured scope.",
2383+
"type": "string",
2384+
"const": "debug-bridge:deny-eval-callback",
2385+
"markdownDescription": "Denies the eval_callback command without any pre-configured scope."
2386+
},
23572387
{
23582388
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
23592389
"type": "string",

app/src-tauri/src/api_client.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,144 @@ impl ApiClient {
721721
.await
722722
}
723723

724+
// -----------------------------------------------------------------
725+
// Docker Images
726+
// -----------------------------------------------------------------
727+
728+
/// List cached Docker images.
729+
pub async fn list_images(&self) -> anyhow::Result<Vec<crate::types::DockerImage>> {
730+
self.request(reqwest::Method::GET, "/images", None::<&()>)
731+
.await
732+
}
733+
734+
/// Remove a Docker image by ID.
735+
pub async fn remove_image(&self, id: &str) -> anyhow::Result<()> {
736+
let _: String = self
737+
.request(
738+
reqwest::Method::DELETE,
739+
&format!("/images/{id}"),
740+
None::<&()>,
741+
)
742+
.await?;
743+
Ok(())
744+
}
745+
746+
// -----------------------------------------------------------------
747+
// Benchmark
748+
// -----------------------------------------------------------------
749+
750+
/// Run a hardware benchmark (create, exec, destroy cycle).
751+
pub async fn run_benchmark(&self) -> anyhow::Result<crate::types::BenchmarkResult> {
752+
self.request(reqwest::Method::POST, "/benchmark", None::<&()>)
753+
.await
754+
}
755+
756+
// -----------------------------------------------------------------
757+
// Sessions
758+
// -----------------------------------------------------------------
759+
760+
/// List all sandbox sessions (recorded exec history).
761+
pub async fn list_sessions(&self) -> anyhow::Result<Vec<crate::types::SandboxSession>> {
762+
self.request(reqwest::Method::GET, "/sessions", None::<&()>)
763+
.await
764+
}
765+
766+
/// Get session recording for a specific sandbox.
767+
pub async fn get_sandbox_session(
768+
&self,
769+
name: &str,
770+
) -> anyhow::Result<crate::types::SandboxSession> {
771+
self.request(
772+
reqwest::Method::GET,
773+
&format!("/sandboxes/{name}/session"),
774+
None::<&()>,
775+
)
776+
.await
777+
}
778+
779+
// -----------------------------------------------------------------
780+
// Export/Import Config
781+
// -----------------------------------------------------------------
782+
783+
/// Export sandbox configuration as TOML.
784+
pub async fn export_sandbox_config(&self, name: &str) -> anyhow::Result<String> {
785+
self.request(
786+
reqwest::Method::GET,
787+
&format!("/sandboxes/{name}/config"),
788+
None::<&()>,
789+
)
790+
.await
791+
}
792+
793+
/// Import sandbox configuration from TOML.
794+
pub async fn import_sandbox_config(
795+
&self,
796+
name: &str,
797+
config: &str,
798+
) -> anyhow::Result<SandboxInfo> {
799+
#[derive(serde::Serialize)]
800+
struct Body<'a> {
801+
config: &'a str,
802+
}
803+
self.request(
804+
reqwest::Method::POST,
805+
&format!("/sandboxes/{name}/config"),
806+
Some(&Body { config }),
807+
)
808+
.await
809+
}
810+
811+
// -----------------------------------------------------------------
812+
// Interactive Permissions
813+
// -----------------------------------------------------------------
814+
815+
/// List active permission grants.
816+
pub async fn list_permissions(&self) -> anyhow::Result<Vec<crate::types::PermissionGrant>> {
817+
self.request(reqwest::Method::GET, "/permissions", None::<&()>)
818+
.await
819+
}
820+
821+
/// Grant a permission.
822+
pub async fn grant_permission(
823+
&self,
824+
req: &crate::types::GrantPermissionRequest,
825+
) -> anyhow::Result<serde_json::Value> {
826+
self.request(reqwest::Method::POST, "/permissions/grant", Some(req))
827+
.await
828+
}
829+
830+
/// Revoke a permission grant.
831+
pub async fn revoke_permission(&self, id: &str) -> anyhow::Result<()> {
832+
let _: String = self
833+
.request(
834+
reqwest::Method::DELETE,
835+
&format!("/permissions/{id}"),
836+
None::<&()>,
837+
)
838+
.await?;
839+
Ok(())
840+
}
841+
842+
/// Check if a permission is granted.
843+
pub async fn check_permission(
844+
&self,
845+
kind: &str,
846+
sandbox: Option<&str>,
847+
) -> anyhow::Result<crate::types::PermissionCheckResult> {
848+
#[derive(serde::Serialize)]
849+
struct Body<'a> {
850+
kind: &'a str,
851+
#[serde(skip_serializing_if = "Option::is_none")]
852+
sandbox: Option<&'a str>,
853+
}
854+
self.request(
855+
reqwest::Method::POST,
856+
"/permissions/check",
857+
Some(&Body { kind, sandbox }),
858+
)
859+
.await
860+
}
861+
724862
// -----------------------------------------------------------------
725863
// Internal helpers
726864
// -----------------------------------------------------------------
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use tauri::State;
2+
3+
use crate::state::AppState;
4+
use crate::types::BenchmarkResult;
5+
6+
/// Run a hardware benchmark (timed create/exec/destroy cycle).
7+
#[tauri::command(rename_all = "snake_case")]
8+
pub async fn run_benchmark(state: State<'_, AppState>) -> Result<BenchmarkResult, String> {
9+
let client = state.client.lock().map_err(|e| e.to_string())?.clone();
10+
client.run_benchmark().await.map_err(|e| e.to_string())
11+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use tauri::State;
2+
3+
use crate::state::AppState;
4+
use crate::types::SandboxInfo;
5+
6+
/// Export sandbox configuration as TOML.
7+
#[tauri::command(rename_all = "snake_case")]
8+
pub async fn export_sandbox_config(
9+
name: String,
10+
state: State<'_, AppState>,
11+
) -> Result<String, String> {
12+
let client = state.client.lock().map_err(|e| e.to_string())?.clone();
13+
client
14+
.export_sandbox_config(&name)
15+
.await
16+
.map_err(|e| e.to_string())
17+
}
18+
19+
/// Import sandbox configuration from TOML.
20+
#[tauri::command(rename_all = "snake_case")]
21+
pub async fn import_sandbox_config(
22+
name: String,
23+
config: String,
24+
state: State<'_, AppState>,
25+
) -> Result<SandboxInfo, String> {
26+
let client = state.client.lock().map_err(|e| e.to_string())?.clone();
27+
client
28+
.import_sandbox_config(&name, &config)
29+
.await
30+
.map_err(|e| e.to_string())
31+
}

0 commit comments

Comments
 (0)