diff --git a/core/crates/solstone-core-sol-client-cli/src/lib.rs b/core/crates/solstone-core-sol-client-cli/src/lib.rs index afbe28bf0..26757140e 100644 --- a/core/crates/solstone-core-sol-client-cli/src/lib.rs +++ b/core/crates/solstone-core-sol-client-cli/src/lib.rs @@ -20,7 +20,6 @@ pub enum Outcome { Migrated { path: Vec }, Chat { args: Vec }, Import { args: Vec }, - Link { args: Vec }, Notify { args: Vec }, MovedStub { name: OsString }, Unsupported { args: Vec }, @@ -77,20 +76,6 @@ pub fn evaluate_args(args: &[OsString]) -> Outcome { }, ) } - [command, rest @ ..] if command == OsStr::new("link") => { - let path = [String::from("link"), String::from("serve")]; - if rest.first().is_some_and(|verb| verb == OsStr::new("serve")) - && match_generated_resident_surface_path("sol-link", &path).is_some() - { - Outcome::Link { - args: rest.to_vec(), - } - } else { - Outcome::Unsupported { - args: args.to_vec(), - } - } - } [command, rest @ ..] if command == OsStr::new("notify") => { match_generated_surface_path("sol-notify", &[String::from("notify")]).map_or_else( || Outcome::Unsupported { diff --git a/core/crates/solstone-core-sol-client/src/seam.rs b/core/crates/solstone-core-sol-client/src/seam.rs index 752522cbd..d8bce54b9 100644 --- a/core/crates/solstone-core-sol-client/src/seam.rs +++ b/core/crates/solstone-core-sol-client/src/seam.rs @@ -265,12 +265,10 @@ pub enum LinkServeTransportErrorKind { #[derive(Debug, Clone, PartialEq, Eq)] pub enum LinkServeErrorKind { InvalidBundle, - InvalidRelayUrl, Bind { port: u16, addr_in_use: bool }, RuntimeUnavailable, BridgeCapability, Transport(LinkServeTransportErrorKind), - Shutdown, } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/core/crates/solstone-core-sol-link/src/serve.rs b/core/crates/solstone-core-sol-link/src/serve.rs index 3a2c0d681..7718e97fc 100644 --- a/core/crates/solstone-core-sol-link/src/serve.rs +++ b/core/crates/solstone-core-sol-link/src/serve.rs @@ -446,54 +446,7 @@ fn map_bridge_start_error(error: BridgeStartError, port: u16) -> LinkServeError } fn map_transport_error(error: TransportError) -> LinkServeTransportErrorKind { - match error { - TransportError::Io(error) => { - drop(error); - LinkServeTransportErrorKind::Io - } - TransportError::Tls(message) => { - drop(message); - LinkServeTransportErrorKind::Tls - } - TransportError::Crypto(message) => { - drop(message); - LinkServeTransportErrorKind::Crypto - } - TransportError::Mux(error) => { - drop(error); - LinkServeTransportErrorKind::Mux - } - TransportError::Http(error) => { - drop(error); - LinkServeTransportErrorKind::Http - } - TransportError::Json(error) => { - drop(error); - LinkServeTransportErrorKind::Json - } - TransportError::PairLink(message) => { - drop(message); - LinkServeTransportErrorKind::PairLink - } - TransportError::Pairing(message) => { - drop(message); - LinkServeTransportErrorKind::Pairing - } - TransportError::Rejected { status, body } => { - drop(body); - LinkServeTransportErrorKind::Rejected { status } - } - TransportError::Relay(error) => LinkServeTransportErrorKind::Relay(map_relay_error(error)), - TransportError::RelayControlRejected { endpoint, status } => { - LinkServeTransportErrorKind::RelayControlRejected { - endpoint: map_relay_control_endpoint(endpoint), - status, - } - } - TransportError::NoEndpoint => LinkServeTransportErrorKind::NoEndpoint, - TransportError::NotPaired => LinkServeTransportErrorKind::NotPaired, - TransportError::LocalOffset => LinkServeTransportErrorKind::LocalOffset, - } + map_transport_error_ref(&error) } fn map_transport_error_ref(error: &TransportError) -> LinkServeTransportErrorKind { diff --git a/core/crates/solstone-core-sol/src/lib.rs b/core/crates/solstone-core-sol/src/lib.rs index 98cbcdda0..072f22990 100644 --- a/core/crates/solstone-core-sol/src/lib.rs +++ b/core/crates/solstone-core-sol/src/lib.rs @@ -26,17 +26,12 @@ use solstone_core_sol_client::seam::{ BuildIdentityProvider, ChatEventSource, ChatInput, ClientItemIdProvider, Clock, FileProvider, HttpTransport, NotificationSink, NotificationSinkError, ProcessOutput, ProcessSpawner, }; -#[cfg(target_os = "ios")] -use solstone_core_sol_client::seam::{LinkServeError, LinkServeErrorKind, LinkServeRunner}; use solstone_core_sol_client::sse::SseDecoder; use solstone_core_sol_client::transport::UreqHttpTransport; use solstone_core_sol_client_cli::{ - DispatchSeams, LinkDispatch, LinkDispatchSeams, Outcome, dispatch_sol_call_with_seams, - dispatch_sol_chat_with_seams, dispatch_sol_import_with_seams, dispatch_sol_link_with_seams, - dispatch_sol_notify_with_seams, evaluate_args, help, + DispatchSeams, Outcome, dispatch_sol_call_with_seams, dispatch_sol_chat_with_seams, + dispatch_sol_import_with_seams, dispatch_sol_notify_with_seams, evaluate_args, help, }; -#[cfg(not(target_os = "ios"))] -use solstone_core_sol_link::SplLinkServeRunner; mod generated; mod skills; @@ -97,12 +92,6 @@ fn run_with_stdin_provider( [command, rest @ ..] if command == OsStr::new("import") => { run_top_level_native(public_argv0, &args, "import", rest, stdin_provider) } - [command, rest @ ..] - if command == OsStr::new("link") - && rest.first().is_some_and(|verb| verb == OsStr::new("serve")) => - { - run_top_level_native(public_argv0, &args, "link", rest, stdin_provider) - } [command, rest @ ..] if command == OsStr::new("notify") => { run_top_level_native(public_argv0, &args, "notify", rest, stdin_provider) } @@ -500,52 +489,6 @@ fn run_dispatched( let chat_events = ChannelChatEventSource::default(); let notification_sink = UnixNotificationSink::new(journal.path.join("health/callosum.sock")); - if let Outcome::Link { .. } = outcome { - #[cfg(not(target_os = "ios"))] - let link_serve_runner = SplLinkServeRunner; - #[cfg(target_os = "ios")] - let link_serve_runner = UnavailableLinkServeRunner; - let dispatch = dispatch_sol_link_with_seams( - &args, - &env, - &stdin, - &today, - LinkDispatchSeams { - transport: &transport, - clock: Some(&clock), - files: Some(&files), - link_pairing: None, - link_serve: Some(&link_serve_runner), - journal_root: Some(&journal.path), - }, - ); - return match dispatch { - LinkDispatch::Buffered(output) => render_output(output), - LinkDispatch::Resident { - handler, - args: resident_args, - } => { - let context = CommandContext { - args: &resident_args, - env: &env, - stdin: &stdin, - today: &today, - transport: &transport, - clock: Some(&clock), - chat_events: None, - files: Some(&files), - build_identity: None, - client_item_ids: None, - notification_sink: None, - link_pairing: None, - link_serve: Some(&link_serve_runner), - journal_root: Some(&journal.path), - }; - run_resident_command(handler, context) - } - }; - } - let output = match outcome { Outcome::Migrated { .. } | Outcome::MovedStub { .. } => dispatch_sol_call_with_seams( &args, @@ -607,26 +550,11 @@ fn run_dispatched( notification_sink: Some(¬ification_sink), }, ), - Outcome::Link { .. } => unreachable!("link outcome handled before buffered dispatch"), Outcome::Unsupported { .. } => unsupported_output(), }; render_output(output) } -#[cfg(target_os = "ios")] -#[derive(Debug, Default)] -struct UnavailableLinkServeRunner; - -#[cfg(target_os = "ios")] -impl LinkServeRunner for UnavailableLinkServeRunner { - fn start( - &self, - _request: solstone_core_sol_client::seam::LinkServeRequest, - ) -> Result, LinkServeError> { - Err(LinkServeError::new(LinkServeErrorKind::RuntimeUnavailable)) - } -} - fn delegate_to_compat(public_argv0: &str, all_args: &[OsString]) -> ExitCode { let existing_sentinel = env::var_os(COMPAT_SENTINEL); if let Err(output) = compat_env_preflight(existing_sentinel.as_deref()) { diff --git a/docs/design/native-sol-client/11-link-serve-design.md b/docs/design/native-sol-client/11-link-serve-design.md index 7d8d08e2f..8b557b89a 100644 --- a/docs/design/native-sol-client/11-link-serve-design.md +++ b/docs/design/native-sol-client/11-link-serve-design.md @@ -19,7 +19,10 @@ No implementation lands in this record. ## D0. Recommendation -Decision: **ship native `sol link serve` against SPL v0.3.0**. +Decision: **ship the native `link.serve` authority entry against SPL v0.3.0**. +It is declared, compiled, inventoried, and parity-covered in this lode, but +public `sol link` remains the Python compatibility-dispatched path until the +separate cutover lode flips the command. All four blockers from `09-link-serve-design.md` D9 are closed: @@ -126,8 +129,7 @@ the real resident handler and return buffered output before the serve loop can exist. A second buffered entry point would duplicate command parsing and create a cutover landmine where one path must refuse real invocations. -`dispatch_sol_link_with_seams` currently hardcodes `["link", "join"]` at -`core/crates/solstone-core-sol-client-cli/src/lib.rs:180-181`. Change it to +`dispatch_sol_link_with_seams` must not hardcode `["link", "join"]`. It should derive `["link", ""]` from argv: - Accept full argv `["link", "", ...]`. @@ -139,23 +141,17 @@ derive `["link", ""]` from argv: Its return type changes from `CommandOutput` to `LinkDispatch { Buffered(CommandOutput), Resident { handler: ResidentHandler, args: Vec } }`. The dispatcher resolves the path and returns the -trimmed argv for resident commands; `solstone-core-sol` builds the -`CommandContext` and calls `run_resident_command`. The `-cli` crate may resolve -and return a resident outcome, but it must not run the resident loop and must -not invoke the resident handler in production. `solstone-core-sol` owns the -runner so it can install the signal mask before the handler runs. - -`evaluate_args` and `run_dispatched` in `core/crates/solstone-core-sol` need a -top-level link outcome. `run_dispatched` should build the `CommandContext` from -the returned resident argv and return immediately through `run_resident_command` -when link dispatch returns a resident arm. The `link.serve` refusal cases are -rendered by `run_resident_command`'s `Err(CommandOutput)` arm, not by the -`-cli` dispatcher. +trimmed argv for resident commands. The `-cli` crate may resolve and return a +resident outcome for parity and seam callers, but it must not run the resident +loop or invoke the resident handler in production. Public `sol link` production +dispatch stays on the Python compatibility path in this lode, matching +`link.join`; the cutover lode owns wiring `solstone-core-sol` to build the +resident `CommandContext` and call `run_resident_command`. `core/crates/solstone-core-sol-client-cli/src/bin/resolve_parity_leaves.rs:47` must stop hardcoding `"sol-link" => ["link", "join"]`. It should use the same -argv-derived link path as production dispatch so `link.join` and `link.serve` -resolve independently. +argv-derived link-path helper as the link dispatcher so `link.join` and +`link.serve` resolve independently. `FINAL_TOP_LEVEL_LINK_TOTAL` moves from `1` to `2` in `scripts/build_native_sol_inventory.py:55`; the coverage recheck at @@ -221,9 +217,9 @@ handle/runtime in the returned session, and block in `serve` until `ShutdownSignal::wait()` fires. `CommandContext` gains `link_serve: Option<&dyn LinkServeRunner>`, mirroring -`link_pairing`. `LinkDispatchSeams` gains the same field. Production -`solstone-core-sol` passes `SplLinkServeRunner`; parity/unit tests pass -`ScriptedLinkServeRunner`. +`link_pairing`. `LinkDispatchSeams` gains the same field. Parity/unit tests pass +`ScriptedLinkServeRunner`; the cutover lode will wire production +`solstone-core-sol` to pass `SplLinkServeRunner`. Scripted test surface: @@ -286,10 +282,10 @@ Honor `10-resident-command-lane-design.md` D4: the runtime must be built inside the resident handler, never while constructing global process seams. Threads inherit the creator thread's signal mask, and `run_resident_command` blocks SIGINT/SIGTERM before calling the handler at -`core/crates/solstone-core-sol/src/lib.rs:692-705`. If the Tokio runtime or its -worker threads are created earlier in `run_dispatched`, they inherit the -unblocked mask and can observe process signals outside the resident shutdown -contract. +`core/crates/solstone-core-sol/src/lib.rs:692-705`. If a future cutover creates +the Tokio runtime or its worker threads before the resident handler runs, they +inherit the unblocked mask and can observe process signals outside the resident +shutdown contract. Port handling: @@ -569,7 +565,11 @@ AC 3, native implementation and no Python subprocess: - `make check-native-sol-no-python-spawn`. - `core/crates/solstone-core-sol-client-cli/src/lib.rs` unit test `sol_link_dispatch_resolves_join_and_serve_from_full_or_trimmed_argv`, proving - production dispatch uses generated native inventory for `link serve`. + generated native inventory resolves `link serve` for native parity/seam + callers without invoking Python. +- Production `sol link` remains the Python compatibility-dispatched path until + cutover; this lode does not claim no-Python production dispatch for public + `sol link serve`. AC 4, compat boundary untouched: @@ -703,8 +703,9 @@ Sequence: `core/deny.toml` source-policy adjustment required by the lock update. 2. Extend the generated inventory model for `resident = true`, add `RESIDENT_HANDLERS`, aggregate accessors, and resident static-slice tests. -3. Add link-dispatch return enum, top-level link outcome, argv-derived link - resolution, and parity resolver changes. +3. Add link-dispatch return enum, argv-derived link resolution, and parity + resolver changes; do not route public `sol link serve` to native production + dispatch in this lode. 4. Add client seam types, `CommandContext.link_serve`, and scripted runner. 5. Add `link_serve` resident handler: argparse-compatible help/errors, port validation, bundle loading, relay resolution, direct enforcement, startup diff --git a/solstone/think/native/link/command.rs b/solstone/think/native/link/command.rs index 2f22314fb..216971c49 100644 --- a/solstone/think/native/link/command.rs +++ b/solstone/think/native/link/command.rs @@ -820,21 +820,7 @@ fn load_peer_sender_instance_id(journal_root: &Path) -> Option { } fn observer_bundle_dir(label: &str, env: &BTreeMap) -> Result { - let base = env - .get("XDG_CONFIG_HOME") - .filter(|value| !value.is_empty()) - .map_or_else( - || { - env.get("HOME") - .filter(|value| !value.is_empty()) - .map(|home| PathBuf::from(home).join(".config")) - }, - |xdg| Some(PathBuf::from(xdg)), - ); - let Some(base) = base else { - return Err("Could not resolve home directory for observer credentials.".to_string()); - }; - Ok(base.join("solstone-observer").join("spl").join(label)) + Ok(observer_spl_root(env)?.join(label)) } fn validate_instance_id(value: &str) -> Option { @@ -990,9 +976,6 @@ fn serve_error_text(error: LinkServeError) -> String { LinkServeErrorKind::InvalidBundle => { "Link credentials are invalid. Run sol link join before sol link serve.".to_string() } - LinkServeErrorKind::InvalidRelayUrl => { - "Relay URL is invalid. Check --relay-url and retry.".to_string() - } LinkServeErrorKind::Bind { port, addr_in_use } => { if addr_in_use { format!( @@ -1009,9 +992,6 @@ fn serve_error_text(error: LinkServeError) -> String { "Native link bridge setup failed before serving. Retry after reinstalling solstone-core." .to_string() } - LinkServeErrorKind::Shutdown => { - "Native link serve shutdown failed.".to_string() - } LinkServeErrorKind::Transport(kind) => serve_transport_error_text(kind), } }