Skip to content
Merged
Show file tree
Hide file tree
Changes from 148 commits
Commits
Show all changes
155 commits
Select commit Hold shift + click to select a range
91f1444
chore(deps): add tinybox as a git submodule
senamakel Aug 21, 2026
9794573
chore(deps): update vendor/tinybox subproject commit
senamakel Aug 21, 2026
084d52f
fix(gateway): handle missing fields in gateway types
senamakel Aug 21, 2026
5dfd459
fix(store): handle missing store file gracefully
senamakel Aug 21, 2026
5d526b7
fix(ops): handle missing gateway response gracefully
senamakel Aug 21, 2026
458c558
fix(gateway): handle missing gateway ops gracefully
senamakel Aug 21, 2026
f6fa6f5
fix(gateway): handle missing gateway ops gracefully
senamakel Aug 21, 2026
7f8d934
fix(gateway): handle missing registry file on startup
senamakel Aug 21, 2026
6de0e47
fix(gateway): handle missing command arguments gracefully
senamakel Aug 21, 2026
0113750
chore(deps): update tauri dependency to 2.0.0-rc.0
senamakel Aug 21, 2026
a242b97
fix(core_rpc): handle missing optional fields in RPC response
senamakel Aug 21, 2026
bb3399b
fix(tauri): restore missing window menu on Linux
senamakel Aug 21, 2026
fe91b5b
feat(gateways): add gateway management commands and dependencies
senamakel Aug 21, 2026
414b548
test(gateway): add tests for type definitions
senamakel Aug 21, 2026
916a4e6
test(store): add tests for gateway store operations
senamakel Aug 21, 2026
2ca4602
fix(ops_tests): remove unused import to resolve compiler warning
senamakel Aug 21, 2026
12bd3e0
fix(gateway): handle missing store entries in registry lookup
senamakel Aug 21, 2026
a3605bd
fix(ops): handle missing gateway response gracefully
senamakel Aug 21, 2026
263dd8e
fix(ops_tests): remove unused import of `GatewayError`
senamakel Aug 21, 2026
a1293fa
fix(registry_tests): update test to match new registry behavior
senamakel Aug 21, 2026
b467969
fix(gateway): handle missing ops module file
senamakel Aug 21, 2026
8ee4b58
feat(store): add gateway core mode to coreModeSlice
senamakel Aug 21, 2026
9db4332
feat(utils): add gateway id persistence to configPersistence
senamakel Aug 21, 2026
48b343c
fix(gateway): handle missing gateway config gracefully
senamakel Aug 21, 2026
97d32ad
feat(settings): add GatewaySection component for core settings panel
senamakel Aug 21, 2026
48a0fb2
test(core): update tests for placement-to-workspace rename and core_r…
senamakel Aug 21, 2026
07ac13b
feat(settings): add gateway section to core connection panel
senamakel Aug 21, 2026
52ca445
feat(i18n): add missing English translations for new settings page
senamakel Aug 21, 2026
d08a4f5
fix(i18n): correct typo in English locale string
senamakel Aug 21, 2026
f8a3ddd
feat(i18n): add missing translations for multiple languages
senamakel Aug 21, 2026
66cb359
test(gateway): add test file for gateway service
senamakel Aug 21, 2026
41484ae
test(gateway-section): add test file for GatewaySection component
senamakel Aug 21, 2026
ccc3be5
fix: update useT call and replace template literals in GatewaySection
senamakel Aug 21, 2026
ed3c240
fix(gateway): correct argument order in PortMapping::fixed call
senamakel Aug 21, 2026
b38bb78
feat(bootCheck): skip core version checks in gateway mode
senamakel Aug 21, 2026
2dd76f8
feat(developer-options): show gateway core mode badge
senamakel Aug 21, 2026
48860f6
feat(i18n): add translations for new dev options gateway keys
senamakel Aug 21, 2026
dc52021
fix(settings): suppress false positive lint rule in gateway effect
senamakel Aug 21, 2026
92f9977
feat(architecture): document gateway architecture for remote core con…
senamakel Aug 21, 2026
e9dd565
fix(gatewayService): wrap capability probe in try/catch
senamakel Aug 21, 2026
9232026
chore: reformat long lines and tidy trailing whitespace
senamakel Aug 21, 2026
4e2defb
test(gateway): add end-to-end provisioning test for local box gateways
senamakel Aug 21, 2026
b180e71
fix(gateway): shut down provisioned gateways on teardown and improve …
senamakel Aug 21, 2026
a61bcde
fix(gateways): await shutdown instead of blocking on it
senamakel Aug 21, 2026
f9cee91
fix(gateway): derive Default on Reach instead of manual impl
senamakel Aug 21, 2026
a6469d8
fix(settings): use local image tag for gateway draft
senamakel Aug 21, 2026
4253ac4
feat(about_app): add core connection capability entry
senamakel Aug 21, 2026
3993682
fix(gateway): handle missing provision fields gracefully
senamakel Aug 21, 2026
b4478e9
fix(gateway): handle missing provision data in ops
senamakel Aug 21, 2026
1eaa167
fix(gateway): handle missing provision file gracefully
senamakel Aug 21, 2026
fa507bf
feat(gateway): extract provisioning into its own module
senamakel Aug 21, 2026
eac40af
fix(gateway): correct import paths for moved functions
senamakel Aug 21, 2026
931203d
fix(about_app): change SSH capability privacy data kind to Raw
senamakel Aug 21, 2026
40b8c4c
feat(bootCheck): re-activate gateway tunnel on relaunch
senamakel Aug 21, 2026
bdcc766
test(bootCheck): add gateway mode test suite
senamakel Aug 21, 2026
22cc453
fix(gateway): reorder import and format assertion in provisioning test
senamakel Aug 21, 2026
e0fcea4
fix(gateway): handle missing gateway ops gracefully
senamakel Aug 21, 2026
affff0b
fix(gateway): use unqualified BoxInfo in create_box return type
senamakel Aug 21, 2026
a21c96b
refactor(core-rpc): extract active endpoint resolution into a shared …
senamakel Aug 21, 2026
d5b69dc
docs(TEST-COVERAGE-MATRIX): add row for core gateways coverage
senamakel Aug 21, 2026
768f93b
docs(TEST-COVERAGE-MATRIX): realign table columns for consistent form…
senamakel Aug 21, 2026
36c4b40
chore(deps): update vendor/tinybox subproject commit
senamakel Aug 21, 2026
7e570fe
chore(deps): update vendor/tinybox subproject commit
senamakel Aug 21, 2026
068830b
docs(release-manual): add smoke-test row for Docker core gateway
senamakel Aug 21, 2026
7b10405
chore(deps): update vendor/tinybox subproject commit
senamakel Aug 21, 2026
f16067d
chore(deps): update vendor/tinybox subproject commit
senamakel Aug 22, 2026
0463f41
test(gateway): add render test for GatewaySection component
senamakel Aug 22, 2026
ab7798f
fix(test): correct import paths in GatewaySection render test
senamakel Aug 22, 2026
7169603
test(gatewayService): add tests for gateway mutation and activation
senamakel Aug 22, 2026
895af5d
test(gatewayService): add missing imports for activateGateway, delete…
senamakel Aug 22, 2026
0039955
test(configPersistence): add tests for gateway id storage
senamakel Aug 22, 2026
6e76d6b
fix(utils): recognise 'gateway' in stored core mode
senamakel Aug 22, 2026
dfcbda8
fix(utils): treat gateway core mode like cloud when skipping local us…
senamakel Aug 22, 2026
264c2ca
test(bootstrapActiveUser): add tests for gateway core mode
senamakel Aug 22, 2026
97b5257
test(core-mode): add gateway-mode tests for synchronous recovery and …
senamakel Aug 22, 2026
aa308e8
test(developer-options-panel): add test for gateway core mode display
senamakel Aug 22, 2026
ab94d1b
test: reformat test files for consistent object formatting
senamakel Aug 22, 2026
2a7fccd
Merge remote-tracking branch 'refs/remotes/upstream/main' into pr/5670
senamakel Aug 22, 2026
1cd9430
test(raw_coverage): add tool_specs field to test prompt dumps
senamakel Aug 22, 2026
c15c801
chore: files changed app/src-tauri/src/gateway/registry.rs
senamakel Aug 22, 2026
fc9b24c
feat(gateway): write gateway store atomically with restricted permiss…
senamakel Aug 22, 2026
6b0f2d7
fix(gateway): import Write trait and clean up unix permission import
senamakel Aug 22, 2026
5f77bfd
fix(gateway): stop core process on forward and health errors
senamakel Aug 22, 2026
f9ae2cb
fix(i18n): update Spanish, Italian, and Portuguese translations for g…
senamakel Aug 22, 2026
bba00eb
fix(i18n): update Portuguese gateway translations for clarity and con…
senamakel Aug 22, 2026
f315717
feat(i18n): add error message for gateway session timeout
senamakel Aug 22, 2026
c06b226
feat(utils): add i18n key constant for gateway session failure
senamakel Aug 22, 2026
2fff007
fix(desktop-deeplink): handle gateway session failure with translated…
senamakel Aug 22, 2026
6619cfb
feat(i18n): add gateway session error fallback translations
senamakel Aug 22, 2026
0032fce
feat(gateway): add remote transport validation for bearer tokens
senamakel Aug 22, 2026
aad8fd9
feat(store): validate remote transport before saving gateway
senamakel Aug 22, 2026
67a2f05
feat(core_rpc): reject bearer tokens over insecure transports
senamakel Aug 22, 2026
6b3f485
fix(core_rpc): guard insecure-transport check behind gateways feature
senamakel Aug 22, 2026
39da8d8
feat(gateway): return empty acknowledgment from activate command
senamakel Aug 22, 2026
393f3fd
fix(gateway): remove unused ActiveGateway import
senamakel Aug 22, 2026
4b7de9f
fix(api): remove ActiveGateway type and simplify activateGateway
senamakel Aug 22, 2026
af85fe7
test(gatewayService): tighten activation test to not leak credentials
senamakel Aug 22, 2026
9b8f072
feat(core): register core_rpc_endpoint in invoke handler
senamakel Aug 22, 2026
e7bb82a
feat(core): add atomic core_rpc_endpoint command
senamakel Aug 22, 2026
71bc42f
feat(coreRpcClient): add atomic shell endpoint cache
senamakel Aug 22, 2026
27f493d
fix(core-rpc): clear shell endpoint on cache reset
senamakel Aug 22, 2026
ce82e92
feat(core-rpc): add atomic shell endpoint resolver
senamakel Aug 22, 2026
025b48d
fix(coreRpcClient): use resolved shell endpoint for core RPC URL
senamakel Aug 22, 2026
19b327d
fix(core-rpc): use shell endpoint for token resolution
senamakel Aug 22, 2026
da9f2a9
test(coreRpcClient): update test mocks to use single core_rpc_endpoin…
senamakel Aug 22, 2026
e7cd13a
test(coreRpcClient): update mock to return endpoint object instead of…
senamakel Aug 22, 2026
e7256de
test(coreRpcClient): update mock to use combined endpoint
senamakel Aug 22, 2026
df245f3
test(coreRpcClient): update test to use new Tauri command name
senamakel Aug 22, 2026
0045d84
test(coreRpcClient): update mock invoke commands to use combined endp…
senamakel Aug 22, 2026
749a479
test(coreRpcClient): add test for atomic endpoint snapshot to prevent…
senamakel Aug 22, 2026
9e138c2
fix(store): add unix OpenOptionsExt import
senamakel Aug 22, 2026
297ea57
fix(core_rpc): redact URL path from log output
senamakel Aug 22, 2026
e825458
test(core_rpc): add test for URL redaction in logs
senamakel Aug 22, 2026
49ef278
fix: add missing import of redact_url_for_log in tests
senamakel Aug 22, 2026
302003b
fix(test): remove userinfo from redacted log URL
senamakel Aug 22, 2026
29980ac
feat(i18n): add missing translations for core RPC client tests
senamakel Aug 22, 2026
083735e
fix(coreRpcClient): correct error handling for empty response body
senamakel Aug 22, 2026
fbb777b
fix(coreRpcClient): correct error handling for RPC connection failures
senamakel Aug 22, 2026
2fc3c36
fix(coreRpcClient): correct error handling for empty response body
senamakel Aug 22, 2026
b246cb1
fix(coreRpcClient): correct error handling for empty response body
senamakel Aug 22, 2026
aa908b2
fix(core-rpc): deduplicate concurrent shell endpoint resolution
senamakel Aug 22, 2026
4faa7a6
fix(gateway): use `is_some_and` to check bearer token presence
senamakel Aug 22, 2026
6932b7f
chore(gateway): remove must_use attribute from validate_remote_transport
senamakel Aug 22, 2026
b468735
chore: reformat long lines and reorder imports
senamakel Aug 22, 2026
9de3f6b
feat(gateway): guard activateGateway with availability check
senamakel Aug 22, 2026
b57fad2
fix(gateway/store): prevent silent data loss on corrupt gateway file
senamakel Aug 22, 2026
94371cc
feat(gateway): add generation counter to activation state
senamakel Aug 22, 2026
e38382a
fix(gateway): prevent stale activation progress from overwriting term…
senamakel Aug 22, 2026
255f6da
fix(core_rpc): disable HTTP redirects for authenticated relay requests
senamakel Aug 22, 2026
b29676d
refactor(store): use constant for store file name in error messages
senamakel Aug 22, 2026
988c20a
fix(i18n): improve Polish translations for gateway settings
senamakel Aug 22, 2026
8db1a64
fix(gateway): reformat condition for readability
senamakel Aug 22, 2026
b47df95
test(store): add test that corrupt file blocks save instead of being …
senamakel Aug 22, 2026
0c0279e
Merge branch 'main' into tinybox-gateways
senamakel Aug 22, 2026
7e51628
Merge remote-tracking branch 'upstream/main' into pr/5670
senamakel Aug 22, 2026
502be1b
chore: files changed app/src-tauri/src/gateway/types.rs
senamakel Aug 22, 2026
b4dea79
fix(gateway): return GatewaySummary instead of full Gateway from list…
senamakel Aug 22, 2026
d0ae3e9
feat(gateway): add credential-free GatewaySummary type
senamakel Aug 22, 2026
1a765ae
fix(settings): use GatewaySummary type for gateway list state
senamakel Aug 22, 2026
9121386
test(gateway-section): update mock data to match new gateway shape
senamakel Aug 22, 2026
836df0b
test(gatewayService): update test data to match new gateway shape
senamakel Aug 22, 2026
9e4851d
Merge remote-tracking branch 'upstream/main' into pr/5670
senamakel Aug 22, 2026
51e8542
style(gateway): apply prettier formatting
senamakel Aug 22, 2026
b080e2b
Merge remote-tracking branch 'refs/remotes/upstream/main' into pr/5670
senamakel Aug 22, 2026
98a36a9
feat(i18n): add Arabic locale support
senamakel Aug 22, 2026
53a35c3
fix(core_rpc): handle missing optional fields in RPC response parsing
senamakel Aug 22, 2026
896b88f
fix(core_rpc): handle missing RPC response gracefully
senamakel Aug 22, 2026
7a5bf62
fix(core_rpc): handle missing optional fields in RPC response deseria…
senamakel Aug 22, 2026
c9e2748
fix(gateway): handle missing gateway config gracefully
senamakel Aug 22, 2026
1aa6de6
fix(gateway): handle missing gateway config gracefully
senamakel Aug 22, 2026
5c1bc01
fix(gateway): handle missing gateway config gracefully
senamakel Aug 22, 2026
ec7806b
fix(gateway): correct test for gateway section rendering
senamakel Aug 22, 2026
6d0ff19
feat(i18n): add missing English translations for new settings panel
senamakel Aug 22, 2026
a764505
feat(i18n): add new translation files for multiple languages
senamakel Aug 22, 2026
36aaf91
fix(scripts): handle missing `i18n` key in `i18n-find-english.ts`
senamakel Aug 22, 2026
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
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
path = vendor/tinyhosts
url = https://github.com/tinyhumansai/tinyhosts.git
branch = main
[submodule "vendor/tinybox"]
path = vendor/tinybox
url = https://github.com/tinyhumansai/tinybox
branch = main
[submodule "vendor/tinyruntime"]
path = vendor/tinyruntime
url = https://github.com/tinyhumansai/tinyruntime.git
39 changes: 39 additions & 0 deletions app/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 23 additions & 1 deletion app/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ futures-util = { version = "0.3", default-features = false, features = ["sink",

reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
rand = "0.9"
# tinybox: reach (local/ssh) and confinement (passthrough/docker) as
# independent axes, which is what lets one `GatewaySpec::Box` variant cover a
# container here, a machine over there, and a container over there. Vendored as
# a submodule and taken by path, the same shape as the other tiny* crates.
tinybox-core = { path = "../../vendor/tinybox/crates/tinybox-core", optional = true }
tinybox-host = { path = "../../vendor/tinybox/crates/tinybox-host", optional = true }
tinybox-ssh = { path = "../../vendor/tinybox/crates/tinybox-ssh", optional = true }
tinybox-docker = { path = "../../vendor/tinybox/crates/tinybox-docker", optional = true }
hex = "0.4"
# Tauri's vendored dev-server proxy (see `vendor/tauri-cef/.../protocol/tauri.rs`)
# builds a reqwest 0.13 client that requires a process-wide rustls
Expand Down Expand Up @@ -244,7 +252,21 @@ windows-sys = { version = "0.59", features = [
] }

[features]
default = []
default = ["gateways"]
# Routing the frontend to a core somewhere other than this process: a
# container, a machine over SSH, or a container on a machine over SSH. Pulls in
# the tinybox crates that provision and reach those boxes.
#
# This is a **shell-local** gate, unrelated to the feature-forwarding rules in
# AGENTS.md: those govern which `openhuman_core` gates the shell forwards, and
# `scripts/ci/check-feature-forwarding.mjs` reads the core dependency's feature
# list, not this table. Nothing here belongs in `scripts/ci/product-features.txt`.
gateways = [
"dep:tinybox-core",
"dep:tinybox-docker",
"dep:tinybox-host",
"dep:tinybox-ssh",
]
# `custom-protocol` switches Tauri from `devUrl` (vite dev server) to the
# bundled `frontendDist` served via `tauri://localhost`. `cargo tauri build`
# turns this on automatically for release; do not put it in `default` or
Expand Down
93 changes: 83 additions & 10 deletions app/src-tauri/src/core_rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,16 @@ fn relay_bearer_header(token: Option<&str>) -> Option<String> {
}

/// Redact a relay URL before it lands in a log line or error string: drop the
/// query, fragment, and any userinfo (which can carry tokens/credentials),
/// keeping just `scheme://host[:port]/path` so transport diagnostics stay
/// useful without persisting secrets. Falls back to a coarse sentinel when the
/// URL can't be parsed.
fn redact_url_for_log(url: &str) -> String {
/// query, fragment, path, and any userinfo (which can carry tokens/credentials
/// or PII in the path itself), keeping just `scheme://host[:port]` so transport
/// diagnostics stay useful without persisting secrets. Falls back to a coarse
/// sentinel when the URL can't be parsed.
pub(crate) fn redact_url_for_log(url: &str) -> String {
url.parse::<url::Url>()
.map(|mut parsed| {
parsed.set_query(None);
parsed.set_fragment(None);
parsed.set_path("");
let _ = parsed.set_username("");
let _ = parsed.set_password(None);
parsed.to_string()
Expand Down Expand Up @@ -94,8 +95,30 @@ pub(crate) async fn post_json_rpc(
token: Option<&str>,
body: String,
) -> Result<RelayHttpResponse, String> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
// Defense in depth behind `store::save`'s validation: never attach a
// bearer over plain HTTP to a non-loopback host, whatever the caller is.
// The local core (loopback) and any `https` endpoint keep working; a
// Remote gateway that slipped past persistence is still rejected here.
let relay_token = relay_bearer_header(token);
#[cfg(feature = "gateways")]
if relay_token.is_some()
&& crate::gateway::types::validate_remote_transport(url, token).is_err()
{
return Err(format!(
"refusing to send a bearer to {} over an insecure transport",
redact_url_for_log(url)
));
}

let mut client_builder = reqwest::Client::builder().timeout(Duration::from_secs(30));
if relay_token.is_some() {
// A bearer must never follow a redirect off the endpoint the user
// configured. reqwest strips Authorization on cross-origin redirects
// by default, but disabling redirects entirely for authenticated
// requests is the deterministic, fail-closed choice.
client_builder = client_builder.redirect(reqwest::redirect::Policy::none());
}
let client = client_builder
.build()
.map_err(|e| format!("failed to build HTTP client: {e}"))?;

Expand All @@ -104,15 +127,14 @@ pub(crate) async fn post_json_rpc(
.header("Content-Type", "application/json")
.body(body);

let bearer = relay_bearer_header(token);
if let Some(value) = bearer.as_deref() {
if let Some(value) = relay_token.as_deref() {
builder = builder.header("Authorization", value);
}

let safe_url = redact_url_for_log(url);
log::debug!(
"[core_rpc][relay] POST {safe_url} (auth={})",
bearer.is_some()
relay_token.is_some()
);

let resp = builder
Expand All @@ -133,6 +155,7 @@ pub(crate) async fn post_json_rpc(

#[cfg(test)]
mod tests {
use super::redact_url_for_log;
use super::relay_bearer_header;

#[test]
Expand All @@ -154,4 +177,54 @@ mod tests {
assert_eq!(relay_bearer_header(Some("")), None);
assert_eq!(relay_bearer_header(Some(" ")), None);
}

#[test]
fn redact_strips_credentials_query_and_path() {
// Userinfo, query, fragment, and path must not survive into logs; only
// the scheme://host[:port] surface is kept for transport diagnostics.
assert_eq!(
redact_url_for_log("http://user:pass@192.168.1.74:7788/rpc/secret?token=t0k#frag"),
"http://192.168.1.74:7788/"
);
assert_eq!(
redact_url_for_log("https://core.example.com/rpc"),
"https://core.example.com/"
);
// An unparseable URL degrades to the coarse sentinel.
assert_eq!(redact_url_for_log("not a url"), "<invalid relay url>");
}

/// A non-loopback `http` URL carrying a bearer must be refused, and the
/// surfaced error must carry the redacted `scheme://host[:port]` form —
/// never the raw secret-bearing userinfo, path, or query (CWE-532).
#[cfg(feature = "gateways")]
#[tokio::test]
async fn insecure_transport_refusal_redacts_url() {
let err = match super::post_json_rpc(
"http://user:pass@192.168.1.74:7788/rpc/secret?token=t0k",
Some("bearer-tok"),
"body".to_string(),
)
.await
{
Err(e) => e,
Ok(_) => panic!("insecure non-loopback + bearer must be refused"),
};
assert!(
!err.contains("pass"),
"raw userinfo leaked into refusal error: {err}"
);
assert!(
!err.contains("t0k"),
"raw query token leaked into refusal error: {err}"
);
assert!(
!err.contains("/secret"),
"raw path leaked into refusal error: {err}"
);
assert!(
err.contains("http://192.168.1.74:7788/"),
"redacted host should remain for diagnostics: {err}"
);
}
}
76 changes: 76 additions & 0 deletions app/src-tauri/src/gateway/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//! The Tauri surface for gateways.
//!
//! Thin by design: every command resolves arguments, delegates to [`store`] or
//! [`registry`], and maps the result. The rules live there.

use super::registry;
use super::store;
use super::types::{Gateway, GatewayStatus, GatewaySummary};

/// Every configured gateway, the built-in desktop one first.
///
/// Returns a credential-free [`GatewaySummary`], not the full [`Gateway`]: a
/// `Remote` bearer and a `Box`/SSH destination or identity path belong to the
/// shell store, and the renderer only needs the id, label and kind badge.
#[tauri::command]
pub(crate) fn gateway_list() -> Vec<GatewaySummary> {
let gateways = store::list();
log::debug!("[gateway][cmd] list -> {} gateway(s)", gateways.len());
gateways.iter().map(GatewaySummary::from).collect()
}

/// Add or replace a gateway.
///
/// Saving does not activate: a user editing an SSH destination should not have
/// their session torn down and re-provisioned on every keystroke that lands in
/// a save.
#[tauri::command]
pub(crate) fn gateway_save(gateway: Gateway) -> Result<(), String> {
log::info!(
"[gateway][cmd] save id={} kind={}",
gateway.id,
gateway.spec.kind()
);
store::save(gateway)
}

/// Forget a gateway.
///
/// Removing the active one does not switch away from it — the running session
/// keeps working, and the next launch falls back to the desktop core. Tearing a
/// working connection down as a side effect of tidying a list would be a
/// surprise.
#[tauri::command]
pub(crate) fn gateway_delete(id: String) -> Result<(), String> {
log::info!("[gateway][cmd] delete id={id}");
store::delete(&id)
}

/// Make a gateway the one RPC goes to.
///
/// Returns an empty acknowledgment, not the [`ActiveGateway`]: the endpoint and
/// bearer stay in the shell registry, where `core_rpc_url` / `core_rpc_token`
/// answer from. Handing the bearer to the renderer would let an XSS exfiltrate
/// a credential the renderer never needs — it only needs to know the switch
/// happened.
#[tauri::command]
pub(crate) async fn gateway_activate(
id: String,
desktop: tauri::State<'_, crate::core_process::CoreProcessHandle>,
) -> Result<(), String> {
let gateway = store::get(&id).ok_or_else(|| format!("no gateway named {id}"))?;
registry::activate(&gateway, desktop.inner()).await?;
Ok(())
}

/// Which gateway is active.
#[tauri::command]
pub(crate) async fn gateway_active() -> String {
registry::active_id().await
}

/// What a gateway is doing right now.
#[tauri::command]
pub(crate) async fn gateway_status(id: String) -> GatewayStatus {
registry::status_of(&id).await
}
51 changes: 51 additions & 0 deletions app/src-tauri/src/gateway/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//! Routing the frontend to a core, wherever that core runs.
//!
//! The app has always been able to reach two cores: the one in this process,
//! and one at a URL somebody else is running. This module adds the ones this
//! app provisions itself — a container here, a machine over SSH, or a container
//! on a machine over SSH — by driving [tinybox], whose model is exactly the
//! distinction that matters: *reach* (which machine) and *confinement* (what
//! contains it) are independent axes, so the third case needs no code of its
//! own.
//!
//! # The seam
//!
//! **A gateway resolves to a URL and a bearer, and nothing else changes.**
//! `core_rpc_url` and `core_rpc_token` answer from the active gateway, so every
//! call site in the renderer — `coreRpcClient`, `relay_http_rpc`, every screen —
//! reaches a remote core through the code that already reached the local one.
//! That is the whole reason this is tractable: the alternative, a transport
//! abstraction threaded through the frontend, would touch every caller.
//!
//! # Layout
//!
//! - [`types`] — what a gateway is
//! - [`store`] — where records live (shell-side, not renderer storage)
//! - [`ops`] — which of those a gateway needs, and what holds it open
//! - [`provision`] — create a box, start a core in it, open a tunnel to it
//! - [`registry`] — which gateway is active, and what is being held open for it
//! - [`commands`] — the Tauri surface
//!
//! [tinybox]: https://github.com/tinyhumansai/tinybox

pub mod commands;
pub mod ops;
pub mod provision;
pub mod registry;
pub mod store;
pub mod types;

// No re-exports: every consumer of this module is either `lib.rs` reaching
// `registry`/`commands`, or one of the submodules reaching a sibling. A
// convenience `pub use` here would be surface nothing asks for.

// Test modules are declared here rather than beside each file so they resolve
// as siblings in this directory. Each is named for the module it covers.
#[cfg(test)]
mod ops_tests;
#[cfg(test)]
mod registry_tests;
#[cfg(test)]
mod store_tests;
#[cfg(test)]
mod types_tests;
Loading
Loading