You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/09/16 02:25:13 UTC

(maka) branch feat/runtime-host-rust updated: feat(desktop): manage native hosts over SSH and WSL

This is an automated email from the ASF dual-hosted git repository.

M4n5ter pushed a commit to branch feat/runtime-host-rust
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/feat/runtime-host-rust by this push:
     new 17c1d8ca9c feat(desktop): manage native hosts over SSH and WSL
17c1d8ca9c is described below

commit 17c1d8ca9ceb27030d1a88abce2045a154e5f6a6
Author: Wang <m4...@gmail.com>
AuthorDate: Wed Sep 16 10:24:58 2026 +0800

    feat(desktop): manage native hosts over SSH and WSL
    
    Coordinate remote retirement through trusted operators with exact connection identities while retaining per-profile reconnect fences.
    
    Generated-by: OpenAI Codex
---
 .../__tests__/runtime-host-desktop-manager.test.ts |  61 +++++++++--
 .../src/main/native-runtime-host-command.ts        | 104 +++++++++++++++++++
 .../src/main/native-runtime-host-management.ts     |  68 ++++++------
 apps/desktop/src/main/runtime-host-boot.ts         |  36 ++++++-
 apps/desktop/src/main/runtime-host-client.ts       |   4 +
 .../src/main/runtime-host-desktop-candidate.ts     |   1 +
 .../src/main/runtime-host-desktop-manager.ts       |  72 +++++++++----
 apps/desktop/src/preload/bridge-contract.d.ts      |   1 +
 apps/desktop/src/preload/preload.ts                |   4 +-
 .../native-runtime-host-management-dialog.tsx      |  17 +--
 .../renderer/settings/projects-settings-page.tsx   |   2 +-
 .../settings/runtime-host-profiles-section.tsx     |  17 ++-
 .../src/renderer/settings/settings-surface.tsx     |  11 +-
 crates/cli/src/args.rs                             |  10 +-
 crates/cli/src/deployment/update.rs                |   2 +-
 crates/cli/src/host_client.rs                      |   2 +
 crates/protocol/src/host.rs                        |   9 +-
 crates/runtime-host/src/server.rs                  |  14 ++-
 crates/runtime-host/src/server/diagnostics.rs      |  10 +-
 crates/runtime-host/src/server/dispatch.rs         |   5 +-
 crates/runtime-host/src/server/handshake.rs        |  18 ++--
 crates/runtime-host/src/server/retirement.rs       |  33 +++++-
 .../runtime-host/tests/integration/retirement.rs   |  57 ++++++++++-
 docs/rust-runtime.md                               |   3 +
 docs/rust-runtime.zh-CN.md                         |   3 +
 tests/fixtures/client-native-managed-entry.mjs     | 114 ++++++++++++++++-----
 tests/fixtures/client.mjs                          |  29 +++---
 27 files changed, 562 insertions(+), 145 deletions(-)

diff --git a/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts b/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts
index b3b099ee82..1af20325f9 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts
@@ -33,6 +33,7 @@ import {
   type HostHandoffAction,
   type OpenHostHandoffSurface,
   HostHandoffRequiredError,
+  LOCAL_RUNTIME_HOST_PROFILE,
 } from '@maka/runtime-host/client';
 import {
   INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID,
@@ -208,17 +209,17 @@ test('native management rejects stale retirement and retains stop intent until c
     waitForHostExit: async () => {},
   });
   try {
-    await assert.rejects(owner.runNativeLocalHostChange(async (scope) => {
+    await assert.rejects(owner.runNativeHostChange({ profile: LOCAL_RUNTIME_HOST_PROFILE }, async (scope) => {
       await scope.retire({ ...deployment, configRevision: 1 });
     }), /no longer matches/);
     assert.equal(first.prepareRetirementCalls, 0);
     assert.equal(resumes, 1);
-    await owner.runNativeLocalHostChange(async (scope) => {
+    await owner.runNativeHostChange({ profile: LOCAL_RUNTIME_HOST_PROFILE }, async (scope) => {
       assert.equal(await scope.retire(deployment), true);
       scope.hold();
     });
     assert.equal(first.prepareRetirementCalls, 1);
-    await assert.rejects(owner.runNativeLocalHostChange(async (scope) => {
+    await assert.rejects(owner.runNativeHostChange({ profile: LOCAL_RUNTIME_HOST_PROFILE }, async (scope) => {
       scope.hold();
       throw new Error('activation receipt lost');
     }), /receipt lost/);
@@ -226,7 +227,7 @@ test('native management rejects stale retirement and retains stop intent until c
     assert.equal(starts, 1);
     assert.equal(pauses, 2); // Failed start reused the retained suspension.
     assert.equal(resumes, 1);
-    await owner.runNativeLocalHostChange(async (scope) => {
+    await owner.runNativeHostChange({ profile: LOCAL_RUNTIME_HOST_PROFILE }, async (scope) => {
       scope.hold();
       scope.resumeOnSuccess();
     });
@@ -238,6 +239,53 @@ test('native management rejects stale retirement and retains stop intent until c
   }
 });
 
+test('native remote management pins its epoch and uses only its operator without affecting local ownership', async () => {
+  const target = { ...remoteTarget('native-remote'), profileIncarnationId: 'persisted-incarnation' };
+  const local = candidateHarness();
+  const remote = candidateHarness({ ownership: 'external', hostId: target.profile.rootId });
+  const deployment = {
+    deploymentId: 'f7672ac5-42f0-4456-8a46-0e36f6d6137b', configRevision: 2,
+    rootId: target.profile.rootId, rootPath: '/remote/root', executable: '/remote/maka', sha256: 'a'.repeat(64),
+    mode: 'on_demand' as const, websocket: '127.0.0.1:0',
+  };
+  const owner = await startRuntimeHostDesktopManager({
+    candidateLaunchBarrier: {
+      pause() { assert.fail('remote mutation paused local launches'); },
+      resume() { assert.fail('remote mutation resumed local launches'); }, async retireExcept() {},
+    },
+  } as unknown as DesktopRuntimeHostCandidateStartInput, {
+    startCandidate: async (input) => ready(input.profileTarget?.profile.kind === 'remote' ? remote.candidate : local.candidate),
+    waitForHostExit: async () => { assert.fail('remote PID is not a local process'); },
+  });
+  try {
+    await owner.enable(target);
+    await owner.waitUntilReady(target.profile.id);
+    await assert.rejects(owner.runNativeHostChange({ ...target, profileIncarnationId: 'replaced' }, async () => {
+      assert.fail('replaced profile entered mutation');
+    }), /target changed/);
+    await assert.rejects(owner.runNativeHostChange(target, async (scope) => {
+      await scope.retire(deployment, 'stale-epoch', async () => { assert.fail('stale operator invoked'); });
+    }), /no longer matches/);
+    await owner.runNativeHostChange(target, async (scope) => {
+      assert.equal(await scope.retire(deployment, 'test-host-epoch', async (id) => {
+        assert.equal(id, 'desktop-connection');
+        assert.equal(remote.closeCalls, 0);
+        return false;
+      }), false);
+    });
+    await owner.runNativeHostChange(target, async (scope) => {
+      assert.equal(await scope.retire(deployment, 'test-host-epoch', async () => true), true);
+      scope.hold();
+    });
+    assert.equal(remote.prepareRetirementCalls, 0);
+    assert.equal(remote.closeCalls, 1);
+    assert.equal(local.closeCalls, 0);
+    await assert.rejects(owner.runNativeHostChange(remoteTarget('guest', 'default', 'session_guest'), async () => {}), /Guest/);
+  } finally {
+    await owner.close();
+  }
+});
+
 test('native retirement refusal releases a new pause, but ambiguous retirement retains it', async () => {
   let uncertain = false;
   const first = candidateHarness({
@@ -253,10 +301,10 @@ test('native retirement refusal releases a new pause, but ambiguous retirement r
     startCandidate: async () => ready(first.candidate),
   });
   try {
-    await owner.runNativeLocalHostChange(async (scope) => assert.equal(await scope.retire(null), false));
+    await owner.runNativeHostChange({ profile: LOCAL_RUNTIME_HOST_PROFILE }, async (scope) => assert.equal(await scope.retire(null), false));
     assert.equal(resumes, 1);
     uncertain = true;
-    await assert.rejects(owner.runNativeLocalHostChange(async (scope) => {
+    await assert.rejects(owner.runNativeHostChange({ profile: LOCAL_RUNTIME_HOST_PROFILE }, async (scope) => {
       await scope.retire(null);
     }), /reply lost/);
     assert.equal(resumes, 1);
@@ -1987,6 +2035,7 @@ function candidateHarness(
     hostPid: 42,
     ...(options.ownedProcess ? { ownedProcess: options.ownedProcess } : {}),
     client: {
+      connectionId: 'desktop-connection',
       hostId: options.hostId ?? 'test-host',
       hostEpoch: options.hostEpoch ?? 'test-host-epoch',
       get lifecycleState() {
diff --git a/apps/desktop/src/main/native-runtime-host-command.ts b/apps/desktop/src/main/native-runtime-host-command.ts
new file mode 100644
index 0000000000..803b32cf2f
--- /dev/null
+++ b/apps/desktop/src/main/native-runtime-host-command.ts
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+
+import { spawn } from 'node:child_process';
+import {
+  normalizeRuntimeHostSshDestination,
+  normalizeRuntimeHostWslDistribution,
+  resolveSystemRuntimeHostWslExecutable,
+  runtimeHostSshOperatorRemoteCommand,
+} from '@maka/runtime-host/client';
+import { runtimeHostOperatorInvocation, type RuntimeHostNativeOperatorCommand } from '@maka/runtime-host/operator';
+
+export type NativeRuntimeHostOperator =
+  | { readonly kind: 'local'; readonly executable: string }
+  | { readonly kind: 'ssh'; readonly destination: string; readonly sshPort?: number;
+      readonly operator: RuntimeHostNativeOperatorCommand }
+  | { readonly kind: 'wsl'; readonly distribution: string;
+      readonly operator: RuntimeHostNativeOperatorCommand<'posix'> };
+
+/** Drain and reap even on excess output: never kill an in-flight durable mutation. */
+export function runNativeRuntimeHostCommand(
+  target: NativeRuntimeHostOperator,
+  args: readonly string[],
+  readOnly = false,
+): Promise<string> {
+  const command = invocation(target, args);
+  return new Promise((resolve, reject) => {
+    const child = spawn(command.executable, command.args, {
+      windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'],
+    });
+    const output: Buffer[] = [];
+    let bytes = 0;
+    let errorText = '';
+    // A readonly query can be abandoned. Mutations must settle or explicitly
+    // report an unknown outcome when the underlying connection breaks.
+    const timer = readOnly ? setTimeout(() => child.kill(), 15_000) : undefined;
+    child.stdout.on('data', (chunk: Buffer) => {
+      bytes += chunk.length;
+      if (bytes <= 256 * 1024) output.push(chunk);
+    });
+    child.stderr.setEncoding('utf8');
+    child.stderr.on('data', (chunk: string) => { errorText = (errorText + chunk).slice(-8192); });
+    child.once('error', (error) => {
+      clearTimeout(timer);
+      reject(error);
+    });
+    child.once('close', (code) => {
+      clearTimeout(timer);
+      if (code !== 0 || bytes > 256 * 1024) {
+        reject(new Error(`Native Host operation was not confirmed; refresh status before retrying. ${errorText.trim()}`));
+      } else resolve(Buffer.concat(output).toString('utf8'));
+    });
+  });
+}
+
+function invocation(target: NativeRuntimeHostOperator, args: readonly string[]) {
+  switch (target.kind) {
+    case 'local':
+      return { executable: target.executable, args: ['host', ...args] };
+    case 'wsl': {
+      const command = runtimeHostOperatorInvocation(target.operator, args);
+      return {
+        executable: resolveSystemRuntimeHostWslExecutable(),
+        args: ['--distribution', normalizeRuntimeHostWslDistribution(target.distribution),
+          '--exec', command.executable, ...command.args],
+      };
+    }
+    case 'ssh': {
+      if (target.sshPort !== undefined &&
+        (!Number.isSafeInteger(target.sshPort) || target.sshPort < 1 || target.sshPort > 65_535)) {
+        throw new Error('Invalid SSH port');
+      }
+      return {
+        executable: 'ssh',
+        args: [
+          '-T', '-o', 'BatchMode=yes', '-o', 'ControlMaster=no', '-o', 'ControlPath=none',
+          '-o', 'ClearAllForwardings=yes', '-o', 'RemoteCommand=none', '-o', 'ConnectTimeout=15',
+          '-o', 'ServerAliveInterval=15', '-o', 'ServerAliveCountMax=5',
+          ...(target.sshPort === undefined ? [] : ['-p', String(target.sshPort)]),
+          normalizeRuntimeHostSshDestination(target.destination),
+          runtimeHostSshOperatorRemoteCommand(target.operator, args),
+        ],
+      };
+    }
+  }
+}
+
diff --git a/apps/desktop/src/main/native-runtime-host-management.ts b/apps/desktop/src/main/native-runtime-host-management.ts
index e65a1e312a..049366a299 100644
--- a/apps/desktop/src/main/native-runtime-host-management.ts
+++ b/apps/desktop/src/main/native-runtime-host-management.ts
@@ -18,10 +18,11 @@
  */
 
 
-import { spawn } from 'node:child_process';
 import { decodeRuntimeHostActivationFrame } from '@maka/runtime-host/operator';
+import { z } from 'zod';
 import {
   nativeRuntimeHostDeploymentSchema,
+  decodeNativeRuntimeHostDeploymentStatus,
   type NativeRuntimeHostDeployment,
 } from '../shared/native-runtime-host-deployment.js';
 import {
@@ -34,31 +35,34 @@ import {
   type NativeRuntimeHostMutation,
   type NativeRuntimeHostSettings,
 } from '../shared/native-runtime-host-management.js';
-import { readNativeRuntimeHostDeployment } from './native-runtime-host-deployment.js';
+import { runNativeRuntimeHostCommand, type NativeRuntimeHostOperator } from './native-runtime-host-command.js';
 
 export interface NativeRuntimeHostChangeScope {
   /** Once a mutation may have run, reconnect stays paused until confirmed ready. */
   hold(): void;
-  retire(deployment: NativeRuntimeHostDeployment | null): Promise<boolean>;
+  retire(deployment: NativeRuntimeHostDeployment | null, hostEpoch?: string,
+    prepareRemote?: (connectionId: string) => Promise<boolean>): Promise<boolean>;
   resumeOnSuccess(): void;
 }
 
 export function createNativeRuntimeHostManagement(input: {
-  readonly executable: string;
+  readonly operator: NativeRuntimeHostOperator;
   readonly rootId: string;
-  readonly rootPath: string;
+  readonly rootPath?: string;
   readonly change: <T>(run: (scope: NativeRuntimeHostChangeScope) => Promise<T>) => Promise<T>;
 }) {
-  const read = () => readNativeRuntimeHostDeployment(input.executable, input.rootId);
-  const execute = (args: string[]) => runNativeRuntimeHostCommand(input.executable, ['host', ...args]);
+  const execute = (args: string[], readOnly = false) => runNativeRuntimeHostCommand(input.operator, args, readOnly);
   const rooted = (action: string) => [action, '--root-id', input.rootId];
+  const read = async () => decodeNativeRuntimeHostDeploymentStatus(
+    JSON.parse(await execute(rooted('status'), true)), input.rootId,
+  );
   const requireCurrent = async (expected: NativeRuntimeHostExpected) => {
     const status = await read();
     if (status.kind !== 'installed' || status.deployment.deploymentId !== expected.deploymentId ||
       status.deployment.configRevision !== expected.configRevision) {
       throw new Error('Native Host deployment changed; refresh before applying this operation');
     }
-    return status.deployment;
+    return status;
   };
   const activate = async (deployment: NativeRuntimeHostDeployment): Promise<NativeRuntimeHostMutation> => {
     const receipt = decodeRuntimeHostActivationFrame((await execute([...rooted('activate'), '--framed'])).trim());
@@ -71,6 +75,18 @@ export function createNativeRuntimeHostManagement(input: {
       hostEpoch: receipt.hostEpoch, pid: receipt.pid, port: receipt.endpoint.port,
     } };
   };
+  const retire = (scope: NativeRuntimeHostChangeScope, deployment: NativeRuntimeHostDeployment, hostEpoch?: string) =>
+    scope.retire(deployment, hostEpoch, async (connectionId) => {
+      if (!hostEpoch) throw new Error('Native Host epoch is unavailable; refresh before retiring it');
+      const result = z.discriminatedUnion('kind', [
+        z.object({ kind: z.literal('active_tasks') }).strict(),
+        z.object({ kind: z.literal('prepared'), pid: z.number().int().positive().max(0xffff_ffff) }).strict(),
+      ]).parse(JSON.parse(await execute([
+        'retire', '--root', deployment.rootPath, '--expected-host-epoch', hostEpoch,
+        '--handoff-connection-id', connectionId,
+      ])));
+      return result.kind === 'prepared';
+    });
   const mutate = async (
     action: 'stop' | 'restart' | 'uninstall' | 'update' | 'reconcile',
     current: NativeRuntimeHostDeployment,
@@ -97,10 +113,12 @@ export function createNativeRuntimeHostManagement(input: {
           (status.kind === 'installed' && status.deployment.admission !== 'revoked')) {
           throw new Error('Native Host is already installed or incomplete; refresh deployment status');
         }
+        const rootPath = status.kind === 'installed' ? status.deployment.rootPath : input.rootPath;
+        if (!rootPath) throw new Error('Native Host installation requires a known Root path');
         if (!await scope.retire(null)) return { kind: 'active_tasks' };
         scope.hold();
         const installed = nativeRuntimeHostDeploymentSchema.parse(JSON.parse(await execute([
-          'install', '--root', input.rootPath, ...settingsArguments(request.settings),
+          'install', '--root', rootPath, ...settingsArguments(request.settings),
         ])));
         if (installed.rootId !== input.rootId || installed.admission !== undefined) {
           throw new Error('Native Host install returned another Root or revoked deployment');
@@ -121,7 +139,9 @@ export function createNativeRuntimeHostManagement(input: {
       return result;
     }
 
-    const current = await requireCurrent(request.expected);
+    const observed = await requireCurrent(request.expected);
+    const current = observed.deployment;
+    const hostEpoch = observed.host.kind === 'connected' ? observed.host.identity.hostEpoch : undefined;
     if (request.action === 'update' || request.action === 'reconcile') {
       // Stage under native authority while the old Host is still serving.
       scope.hold();
@@ -134,13 +154,13 @@ export function createNativeRuntimeHostManagement(input: {
       if (staged.kind !== 'active_tasks' || !staged.target) {
         throw new Error('Native Host did not confirm a pending update');
       }
-      if (!await scope.retire(current)) return staged;
+      if (!await retire(scope, current, hostEpoch)) return staged;
       const result = await mutate('reconcile', current);
       if (result.kind === 'ready') scope.resumeOnSuccess();
       return result;
     }
 
-    if (!await scope.retire(current)) return { kind: 'active_tasks', deployment: current };
+    if (!await retire(scope, current, hostEpoch)) return { kind: 'active_tasks', deployment: current };
     scope.hold();
     const result = await mutate(request.action, current);
     if (result.kind === 'ready') scope.resumeOnSuccess();
@@ -151,7 +171,7 @@ export function createNativeRuntimeHostManagement(input: {
       const request = nativeRuntimeHostManagementRequestSchema.parse(value);
       if (request.action === 'status') return { status: await read() };
       if (request.action === 'logs') {
-        const logs = nativeRuntimeHostLogsSchema.parse(JSON.parse(await execute(rooted('logs'))));
+        const logs = nativeRuntimeHostLogsSchema.parse(JSON.parse(await execute(rooted('logs'), true)));
         return { status: await read(), logs };
       }
       return input.change(async (scope) => {
@@ -211,25 +231,3 @@ function validateMutation(
   }
   throw new Error('Native Host operation returned an unexpected revision or outcome');
 }
-
-/** Drain and reap even on excess output: never kill an in-flight durable mutation. */
-function runNativeRuntimeHostCommand(executable: string, args: string[]): Promise<string> {
-  return new Promise((resolve, reject) => {
-    const child = spawn(executable, args, { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
-    const output: Buffer[] = [];
-    let bytes = 0;
-    let errorText = '';
-    child.stdout.on('data', (chunk: Buffer) => {
-      bytes += chunk.length;
-      if (bytes <= 256 * 1024) output.push(chunk);
-    });
-    child.stderr.setEncoding('utf8');
-    child.stderr.on('data', (chunk: string) => { errorText = (errorText + chunk).slice(-8192); });
-    child.once('error', reject);
-    child.once('close', (code) => {
-      if (code !== 0 || bytes > 256 * 1024) {
-        reject(new Error(`Native Host operation was not confirmed; refresh status before retrying. ${errorText.trim()}`));
-      } else resolve(Buffer.concat(output).toString('utf8'));
-    });
-  });
-}
diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts
index 9f87146522..68dd53e301 100644
--- a/apps/desktop/src/main/runtime-host-boot.ts
+++ b/apps/desktop/src/main/runtime-host-boot.ts
@@ -242,6 +242,7 @@ import { createDesktopLocalRuntimeHostRemoteAccess } from './runtime-host-local-
 import { createDesktopRuntimeHostOnboarding } from "./runtime-host-onboarding.js";
 import { createDesktopRuntimeHostManagement } from "./runtime-host-management.js";
 import { createNativeRuntimeHostManagement } from './native-runtime-host-management.js';
+import type { NativeRuntimeHostOperator } from './native-runtime-host-command.js';
 import { createDesktopRuntimeHostLocalManagement } from './runtime-host-local-management.js';
 import { createDesktopRuntimeHostPeerMeshManagement } from './runtime-host-peer-mesh-management.js';
 import { registerExternalAgentSetupIpc } from "./external-agent-setup-ipc-main.js";
@@ -762,16 +763,43 @@ const runtimeHostOnboarding = createDesktopRuntimeHostOnboarding({
     mainWindowController.send("runtime-host-onboarding:changed", snapshot),
 });
 const nativeRuntimeHostManagement = isE2e ? undefined : createNativeRuntimeHostManagement({
-  executable: nativeHostExecutable,
+  operator: { kind: 'local', executable: nativeHostExecutable },
   rootId: startupLocalStorageRoot.rootId,
   rootPath: startupLocalStorageRoot.canonicalPath,
   change: (run) => {
     if (!runtimeHostManager) throw new Error('Runtime Host manager is unavailable');
-    return runtimeHostManager.runNativeLocalHostChange(run);
+    return runtimeHostManager.runNativeHostChange({ profile: LOCAL_RUNTIME_HOST_PROFILE }, run);
   },
 });
-ipcMain.handle('runtime-host-management:native', (_event, request: unknown) =>
-  nativeRuntimeHostManagement?.run(request) ?? null);
+ipcMain.handle('runtime-host-management:native', async (_event, request: unknown, profileId: unknown = 'local') => {
+  if (typeof profileId !== 'string' || !profileId || profileId.length > 128) {
+    throw new Error('Invalid native Host profile ID');
+  }
+  if (profileId === 'local') return nativeRuntimeHostManagement?.run(request) ?? null;
+  const target = await runtimeHostProfileCatalog.resolve(profileId);
+  const profile = target.profile;
+  let operator: NativeRuntimeHostOperator;
+  if (profile.kind === 'environment' && profile.operator.kind === 'native') {
+    operator = { kind: 'wsl', distribution: profile.provider.distribution, operator: profile.operator };
+  } else if (profile.kind === 'remote' && profile.access !== 'session_guest' &&
+    profile.transport.kind === 'ssh' && profile.transport.activation?.operator.kind === 'native') {
+    operator = {
+      kind: 'ssh', destination: profile.transport.destination,
+      ...(profile.transport.sshPort === undefined ? {} : { sshPort: profile.transport.sshPort }),
+      operator: profile.transport.activation.operator,
+    };
+  } else {
+    throw new Error('This profile has no native Host operator');
+  }
+  return createNativeRuntimeHostManagement({
+    operator,
+    rootId: profile.rootId,
+    change: (run) => {
+      if (!runtimeHostManager) throw new Error('Runtime Host manager is unavailable');
+      return runtimeHostManager.runNativeHostChange(target, run);
+    },
+  }).run(request);
+});
 
 const localRuntimeHostManagement = createDesktopRuntimeHostLocalManagement({
   remoteAccess: localRuntimeHostRemoteAccess,
diff --git a/apps/desktop/src/main/runtime-host-client.ts b/apps/desktop/src/main/runtime-host-client.ts
index ab21f66cd7..bc7cd54a98 100644
--- a/apps/desktop/src/main/runtime-host-client.ts
+++ b/apps/desktop/src/main/runtime-host-client.ts
@@ -290,6 +290,10 @@ export class DesktopRuntimeHostClient {
     return this.connection.hostEpoch;
   }
 
+  get connectionId(): string {
+    return this.connection.connectionId;
+  }
+
   get hostId(): string {
     return this.rootId;
   }
diff --git a/apps/desktop/src/main/runtime-host-desktop-candidate.ts b/apps/desktop/src/main/runtime-host-desktop-candidate.ts
index de3cca6e2b..366d0f62e2 100644
--- a/apps/desktop/src/main/runtime-host-desktop-candidate.ts
+++ b/apps/desktop/src/main/runtime-host-desktop-candidate.ts
@@ -235,6 +235,7 @@ export interface DesktopRuntimeHostCandidateStartInput
   readonly profileTarget?: {
     readonly profile: PersistedRuntimeHostProfile;
     readonly credential?: string;
+    readonly profileIncarnationId?: string;
     readonly sshInteraction?: RuntimeHostSshInteraction;
   };
 }
diff --git a/apps/desktop/src/main/runtime-host-desktop-manager.ts b/apps/desktop/src/main/runtime-host-desktop-manager.ts
index 18d9646ec2..0e5ee924ed 100644
--- a/apps/desktop/src/main/runtime-host-desktop-manager.ts
+++ b/apps/desktop/src/main/runtime-host-desktop-manager.ts
@@ -109,7 +109,7 @@ export interface RuntimeHostDesktopManager {
     signal?: AbortSignal,
   ): Promise<void>;
   runManagedLocalHostChange<T>(change: () => Promise<T>): Promise<T>;
-  runNativeLocalHostChange<T>(change: (scope: NativeRuntimeHostChangeScope) => Promise<T>): Promise<T>;
+  runNativeHostChange<T>(target: ResolvedRuntimeHostProfile, change: (scope: NativeRuntimeHostChangeScope) => Promise<T>): Promise<T>;
   setDefaultProfile(profileId: string): void;
   retireOwnedLocalHost(mode: RuntimeHostRetirementMode): Promise<DesktopLocalHostRetirement>;
   prepareOwnedLocalHostQuit(mode: RuntimeHostRetirementMode): Promise<'ready' | 'active_tasks'>;
@@ -235,6 +235,7 @@ interface DesktopRuntimeHostTargetGeneration {
   state: RuntimeHostDesktopTargetState;
   hostId?: string;
   lifecycle?: RuntimeHostReconnectLifecycle<DesktopRuntimeHostCandidate>;
+  nativeSuspension?: { resume(): void };
   unsubscribeLifecycle?: () => void;
   unsubscribeRoutes?: () => void;
   skipPeerRouteRefreshOnce?: boolean;
@@ -312,7 +313,6 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager {
   #defaultProfileId: string = LOCAL_RUNTIME_HOST_PROFILE.id;
   #localHostRetirement: Extract<PreparedLocalHostRetirement, { kind: 'retired' }> | undefined;
   #localHostRetirementTask: DesktopLocalHostRetirementTask | undefined;
-  #nativeLocalSuspension: { resume(): void } | undefined;
   #closed = false;
   #closeTask: Promise<void> | undefined;
 
@@ -765,20 +765,36 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager {
     });
   }
 
-  runNativeLocalHostChange<T>(change: (scope: NativeRuntimeHostChangeScope) => Promise<T>): Promise<T> {
-    return this.#mutateTarget(LOCAL_RUNTIME_HOST_PROFILE.id, async () => {
-      const lifecycle = this.#requireLifecycle(this.#requireTarget(LOCAL_RUNTIME_HOST_PROFILE.id));
-      const alreadyPaused = this.#nativeLocalSuspension !== undefined;
-      if (!this.#nativeLocalSuspension) {
+  runNativeHostChange<T>(expectedTarget: ResolvedRuntimeHostProfile, change: (scope: NativeRuntimeHostChangeScope) => Promise<T>): Promise<T> {
+    return this.#mutateTarget(expectedTarget.profile.id, async () => {
+      if (expectedTarget.profile.kind === 'remote' && expectedTarget.profile.access === 'session_guest') {
+        throw new Error('Session Guest profiles cannot manage native Hosts');
+      }
+      const target = this.#targets.get(expectedTarget.profile.id);
+      if (!target) {
+        if (expectedTarget.profile.kind === 'local') throw new Error('Local Host manager is unavailable');
+        // Disabled profiles have no reconnect or client lifetime to suspend.
+        // Their native operator still acquires the exact deployment and Root.
+        return change({ hold() {}, async retire() { return true; }, resumeOnSuccess() {} });
+      }
+      if (!sameResolvedRuntimeHostProfileTarget(target.target, expectedTarget) ||
+        target.target.profileIncarnationId !== expectedTarget.profileIncarnationId) {
+        throw new Error('Native Host management target changed or is not an owner');
+      }
+      const local = expectedTarget.profile.kind === 'local';
+      const barrier = local ? this.#baseInput.candidateLaunchBarrier : undefined;
+      const lifecycle = this.#requireLifecycle(target);
+      const alreadyPaused = target.nativeSuspension !== undefined;
+      if (!target.nativeSuspension) {
         const suspension = await lifecycle.suspend();
         try {
-          this.#baseInput.candidateLaunchBarrier?.pause();
+          barrier?.pause();
         } catch (error) {
           suspension.resume();
           throw error;
         }
-        this.#nativeLocalSuspension = { resume: () => {
-          this.#baseInput.candidateLaunchBarrier?.resume();
+        target.nativeSuspension = { resume: () => {
+          barrier?.resume();
           suspension.resume();
         } };
       }
@@ -787,27 +803,36 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager {
       let succeeded = false;
       try {
         // Finish pre-existing launches before a management command can acquire Root.
-        await this.#baseInput.candidateLaunchBarrier?.retireExcept(lifecycle.current?.hostPid ?? -1);
+        await barrier?.retireExcept(lifecycle.current?.hostPid ?? -1);
         const result = await change({
           hold: () => { keepPaused = true; },
           resumeOnSuccess: () => { resumeOnSuccess = true; },
-          retire: async (deployment) => {
+          retire: async (deployment, hostEpoch, prepareRemote) => {
             const current = lifecycle.current;
             if (!current) return true; // Native RootOwner still proves exclusivity.
-            if (deployment
-              ? current.hostOwnership !== 'managed' || current.client.hostId !== deployment.rootId ||
-                current.managedDeployment?.deploymentId !== deployment.deploymentId ||
-                current.managedDeployment.configRevision !== deployment.configRevision
-              : current.hostOwnership !== 'owned_ephemeral') {
+            const matches = deployment
+              ? current.client.hostId === deployment.rootId && (local
+                ? current.hostOwnership === 'managed' &&
+                  current.managedDeployment?.deploymentId === deployment.deploymentId &&
+                  current.managedDeployment.configRevision === deployment.configRevision
+                : hostEpoch !== undefined && current.client.hostEpoch === hostEpoch)
+              : local && current.hostOwnership === 'owned_ephemeral';
+            if (!matches) {
               throw new Error('Connected native Host no longer matches the management target');
             }
             keepPaused = true; // RPC failure may follow a successful retirement.
-            const retirement = await current.client.prepareHostRetirement('refuse_active_work');
-            if (retirement.kind === 'active_tasks') {
+            const retirement = local
+              ? await current.client.prepareHostRetirement('refuse_active_work')
+              : undefined;
+            if (!local && !prepareRemote) throw new Error('Native remote retirement requires its trusted operator');
+            const prepared = retirement
+              ? retirement.kind === 'prepared'
+              : await prepareRemote!(current.client.connectionId);
+            if (!prepared) {
               keepPaused = alreadyPaused;
               return false;
             }
-            await this.waitForHostExit(retirement.pid);
+            if (retirement?.kind === 'prepared') await this.waitForHostExit(retirement.pid);
             await current.close();
             return true;
           },
@@ -816,8 +841,8 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager {
         return result;
       } finally {
         if ((!keepPaused || (succeeded && resumeOnSuccess)) && !this.#closed) {
-          const suspension = this.#nativeLocalSuspension;
-          this.#nativeLocalSuspension = undefined;
+          const suspension = target.nativeSuspension;
+          target.nativeSuspension = undefined;
           suspension?.resume();
         }
       }
@@ -1444,6 +1469,9 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager {
           ...(input.profileTarget.credential === undefined
             ? {}
             : { credential: input.profileTarget.credential }),
+          ...(input.profileTarget.profileIncarnationId === undefined
+            ? {}
+            : { profileIncarnationId: input.profileTarget.profileIncarnationId }),
         }
       : { profile: LOCAL_RUNTIME_HOST_PROFILE };
     const epoch = randomUUID();
diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts
index af098a6d30..be5225710e 100644
--- a/apps/desktop/src/preload/bridge-contract.d.ts
+++ b/apps/desktop/src/preload/bridge-contract.d.ts
@@ -882,6 +882,7 @@ export interface MakaBridge {
   runtimeHostManagement: {
     runNative(
       request: import('../shared/native-runtime-host-management.js').NativeRuntimeHostManagementRequest,
+      profileId?: string,
     ): Promise<import('../shared/native-runtime-host-management.js').NativeRuntimeHostManagementResult | null>;
     run(
       profileId: string,
diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts
index b03dc91cca..80bbe9c371 100644
--- a/apps/desktop/src/preload/preload.ts
+++ b/apps/desktop/src/preload/preload.ts
@@ -1680,8 +1680,8 @@ const makaBridge = {
     },
   },
   runtimeHostManagement: {
-    runNative(request) {
-      return ipcRenderer.invoke('runtime-host-management:native', request);
+    runNative(request, profileId = 'local') {
+      return ipcRenderer.invoke('runtime-host-management:native', request, profileId);
     },
     run(
       profileId: string,
diff --git a/apps/desktop/src/renderer/settings/native-runtime-host-management-dialog.tsx b/apps/desktop/src/renderer/settings/native-runtime-host-management-dialog.tsx
index 9fe4e46936..7a36951a70 100644
--- a/apps/desktop/src/renderer/settings/native-runtime-host-management-dialog.tsx
+++ b/apps/desktop/src/renderer/settings/native-runtime-host-management-dialog.tsx
@@ -31,11 +31,14 @@ import type {
 import { getSettingsProjectsCopy } from '../locales/settings-projects-copy.js';
 import { RuntimeHostProjectDirectoryEditor } from './runtime-host-project-directory-editor.js';
 
-export function NativeRuntimeHostManagementDialog(props: { readonly onClose: () => void }) {
+export function NativeRuntimeHostManagementDialog(props: {
+  readonly target: { readonly id: string; readonly name: string };
+  readonly onClose: () => void;
+}) {
   const locale = useUiLocale();
   const zh = locale === 'zh-CN';
   const copy = {
-    title: zh ? '本机 Host' : 'Local Host',
+    title: props.target.name,
     refresh: zh ? '刷新' : 'Refresh',
     install: zh ? '安装并启动' : 'Install and start',
     start: zh ? '启动' : 'Start',
@@ -44,7 +47,9 @@ export function NativeRuntimeHostManagementDialog(props: { readonly onClose: ()
     uninstall: zh ? '卸载(保留数据)' : 'Uninstall (retain data)',
     confirm: zh ? '确认卸载' : 'Confirm uninstall',
     cancel: zh ? '取消' : 'Cancel',
-    update: zh ? '应用此 Desktop 的 Host 版本' : 'Apply this Desktop’s Host version',
+    update: props.target.id === 'local'
+      ? (zh ? '应用此 Desktop 的 Host 版本' : 'Apply this Desktop’s Host version')
+      : (zh ? '应用所选 Host 版本' : 'Apply selected Host version'),
     edit: zh ? '编辑配置' : 'Edit configuration',
     save: zh ? '应用配置与当前版本' : 'Apply settings and current version',
     reconcile: zh ? '继续待处理更新' : 'Continue pending update',
@@ -80,17 +85,17 @@ export function NativeRuntimeHostManagementDialog(props: { readonly onClose: ()
   const alive = useRef(true);
   useEffect(() => {
     alive.current = true;
-    void window.maka.runtimeHostManagement.runNative({ action: 'status' }).then((value) => {
+    void window.maka.runtimeHostManagement.runNative({ action: 'status' }, props.target.id).then((value) => {
       if (alive.current && value) setResult(value);
     }, (cause: unknown) => { if (alive.current) setError(String(cause)); });
     return () => { alive.current = false; };
-  }, []);
+  }, [props.target.id]);
 
   async function run(request: NativeRuntimeHostManagementRequest) {
     setBusy(true);
     setError(undefined);
     try {
-      const value = await window.maka.runtimeHostManagement.runNative(request);
+      const value = await window.maka.runtimeHostManagement.runNative(request, props.target.id);
       if (!alive.current) return;
       if (!value) throw new Error('Native Host management is unavailable');
       setResult(value);
diff --git a/apps/desktop/src/renderer/settings/projects-settings-page.tsx b/apps/desktop/src/renderer/settings/projects-settings-page.tsx
index 0a5fa0a080..716e40ce89 100644
--- a/apps/desktop/src/renderer/settings/projects-settings-page.tsx
+++ b/apps/desktop/src/renderer/settings/projects-settings-page.tsx
@@ -77,7 +77,7 @@ export function ProjectsSettingsPage(props: {
   ): Promise<UpdateAppSettingsResult>;
   onRetryRuntimeHost(): Promise<void>;
   onRemoteHostAdded(profileId: string): void;
-  onManageNativeHost(): void;
+  onManageNativeHost(target: { readonly id: string; readonly name: string }): void;
 }) {
   const host = useOptionalRuntimeHostSettingsTarget();
   const locale = useUiLocale();
diff --git a/apps/desktop/src/renderer/settings/runtime-host-profiles-section.tsx b/apps/desktop/src/renderer/settings/runtime-host-profiles-section.tsx
index eeeb006e64..9585bcdcd7 100644
--- a/apps/desktop/src/renderer/settings/runtime-host-profiles-section.tsx
+++ b/apps/desktop/src/renderer/settings/runtime-host-profiles-section.tsx
@@ -92,7 +92,7 @@ function createRemoteHostDraft() {
 
 export function RuntimeHostProfilesSection(props: {
   readonly onRemoteHostAdded: (profileId: string) => void;
-  readonly onManageNativeHost: () => void;
+  readonly onManageNativeHost: (target: { readonly id: string; readonly name: string }) => void;
 }) {
   const locale = useUiLocale();
   const copy = getSettingsProjectsCopy(locale).runtimeHost;
@@ -418,7 +418,9 @@ export function RuntimeHostProfilesSection(props: {
         {nativeManagementAvailable ? <SettingsRow
           label={locale === 'zh-CN' ? '本机 Host' : 'Local Host'}
           end={<Button variant="secondary" size="sm" label={copy.manage}
-            isDisabled={switching} onClick={props.onManageNativeHost} />}
+            isDisabled={switching} onClick={() => props.onManageNativeHost({
+              id: 'local', name: locale === 'zh-CN' ? '本机 Host' : 'Local Host',
+            })} />}
         /> : nativeManagementAvailable === false ? <SettingsRow
           label={copy.thisComputerRemoteAccess}
           description={
@@ -653,6 +655,10 @@ export function RuntimeHostProfilesSection(props: {
             {connectedEntries.map((entry) => {
               const profile = entry.profile;
               if (profile.kind === 'local') return null;
+              const nativeManagement = profile.kind === 'environment'
+                ? profile.operator.kind === 'native'
+                : profile.access !== 'session_guest' && profile.transport.kind === 'ssh' &&
+                  profile.transport.activation?.operator.kind === 'native';
               const managementTarget: RuntimeHostManagementTarget | undefined =
                 entry.managedService
                   ? profile.kind === 'environment'
@@ -745,7 +751,12 @@ export function RuntimeHostProfilesSection(props: {
                         onChanged={() => void reload()}
                         onWorkingChange={setSwitching}
                         items={[
-                          ...(managementTarget && !entry.pairingPending
+                          ...(nativeManagement && !entry.pairingPending ? [{
+                            label: copy.manage,
+                            isDisabled: switching,
+                            onClick: () => props.onManageNativeHost({ id: profile.id, name: profile.name }),
+                          }] : []),
+                          ...(managementTarget && !nativeManagement && !entry.pairingPending
                             ? [{
                                 label: copy.manage,
                                 isDisabled: switching,
diff --git a/apps/desktop/src/renderer/settings/settings-surface.tsx b/apps/desktop/src/renderer/settings/settings-surface.tsx
index bb0af08a9a..5fc8e32889 100644
--- a/apps/desktop/src/renderer/settings/settings-surface.tsx
+++ b/apps/desktop/src/renderer/settings/settings-surface.tsx
@@ -295,7 +295,7 @@ function SettingsSurfaceContent(
   const [clientSettings, setClientSettings] = useState(
     initialClientSettings ?? defaultSettings,
   );
-  const [showNativeManagement, setShowNativeManagement] = useState(false);
+  const [nativeManagementTarget, setNativeManagementTarget] = useState<{ readonly id: string; readonly name: string }>();
   const [runtimeHostSettings, setRuntimeHostSettings] = useState<
     SettingsResourceState<RuntimeHostAppSettings>
   >(() => createSettingsResourceState(
@@ -1045,7 +1045,7 @@ function SettingsSurfaceContent(
                             archivedTasks={props.archivedTasks}
                             onTaskImported={props.onTaskImported}
                             onRemoteHostAdded={props.onRemoteHostAdded}
-                            onManageNativeHost={() => setShowNativeManagement(true)}
+                            onManageNativeHost={setNativeManagementTarget}
                             openProviderCatalog={providerCatalogRequested}
                             initialConnectionSlug={props.initialConnectionSlug}
                             initialCreateProviderType={createProviderRequest}
@@ -1067,14 +1067,15 @@ function SettingsSurfaceContent(
           </section>
         )}
       />
-      {showNativeManagement ? <NativeRuntimeHostManagementDialog
-        onClose={() => setShowNativeManagement(false)} /> : null}
+      {nativeManagementTarget ? <NativeRuntimeHostManagementDialog
+        key={nativeManagementTarget.id} target={nativeManagementTarget}
+        onClose={() => setNativeManagementTarget(undefined)} /> : null}
     </div>
   );
 }
 
 function SettingsPageBody(props: {
-  onManageNativeHost(): void;
+  onManageNativeHost(target: { readonly id: string; readonly name: string }): void;
   section: SettingsSection;
   isLocalRuntimeHost: boolean;
   settings: AppSettings;
diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs
index d018dae960..b1166f3c7f 100644
--- a/crates/cli/src/args.rs
+++ b/crates/cli/src/args.rs
@@ -78,6 +78,9 @@ enum HostCommand {
         root: Root,
         #[arg(long)]
         expected_host_epoch: Option<String>,
+        /// Coordinate with this exact connected client without interrupting other clients.
+        #[arg(long, requires = "expected_host_epoch")]
+        handoff_connection_id: Option<uuid::Uuid>,
         /// Permit interrupting other clients and non-cooperative resources.
         #[arg(long)]
         allow_interrupt_active_tasks: bool,
@@ -151,11 +154,16 @@ impl Cli {
             Command::Host(HostCommand::Retire {
                 root,
                 expected_host_epoch,
+                handoff_connection_id,
                 allow_interrupt_active_tasks,
             }) => {
                 let mut client = crate::host_client::HostClient::connect(&root.root, None).await?;
                 let result = client
-                    .retire(expected_host_epoch.as_deref(), allow_interrupt_active_tasks)
+                    .retire(
+                        expected_host_epoch.as_deref(),
+                        allow_interrupt_active_tasks,
+                        handoff_connection_id,
+                    )
                     .await?;
                 drop(client);
                 println!("{}", serde_json::to_string(&result)?);
diff --git a/crates/cli/src/deployment/update.rs b/crates/cli/src/deployment/update.rs
index ac2cadd084..b0b4ca62a4 100644
--- a/crates/cli/src/deployment/update.rs
+++ b/crates/cli/src/deployment/update.rs
@@ -212,7 +212,7 @@ pub(super) async fn retire(deployment: &Deployment) -> Result<Option<RootOwner>,
             if let Ok(mut client) =
                 HostClient::connect(&deployment.root_path, Some(&deployment.generation())).await
             {
-                match client.retire(None, false).await? {
+                match client.retire(None, false, None).await? {
                     RetirementResult::ActiveTasks => return Ok(None),
                     RetirementResult::Prepared { .. } => {}
                 }
diff --git a/crates/cli/src/host_client.rs b/crates/cli/src/host_client.rs
index 66a6e064a9..396c6db286 100644
--- a/crates/cli/src/host_client.rs
+++ b/crates/cli/src/host_client.rs
@@ -185,6 +185,7 @@ impl HostClient {
         &mut self,
         expected_epoch: Option<&str>,
         interrupt: bool,
+        handoff_connection_id: Option<uuid::Uuid>,
     ) -> Result<RetirementResult, HostError> {
         if expected_epoch.is_some_and(|epoch| epoch != self.discovery.host_epoch) {
             return Err("Host epoch does not match the retirement target".into());
@@ -193,6 +194,7 @@ impl HostClient {
             expected_host_epoch: self.discovery.host_epoch.clone(),
             allow_interrupt_active_tasks: interrupt,
             allow_cooperative_handoff: Some(true),
+            handoff_connection_id: handoff_connection_id.map(|id| id.to_string()),
         };
         let result = decode_retirement_result(
             &self
diff --git a/crates/protocol/src/host.rs b/crates/protocol/src/host.rs
index 4563c9834f..7e5e5b3de2 100644
--- a/crates/protocol/src/host.rs
+++ b/crates/protocol/src/host.rs
@@ -32,6 +32,9 @@ pub struct RetirementInput {
     pub allow_interrupt_active_tasks: bool,
     #[serde(skip_serializing_if = "Option::is_none")]
     pub allow_cooperative_handoff: Option<bool>,
+    /// A trusted operator may coordinate retirement with this still-connected client.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub handoff_connection_id: Option<String>,
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -45,7 +48,7 @@ pub fn decode_retirement_input(value: &Value) -> Result<RetirementInput> {
     codec::shaped(
         codec::record(value, "Host retirement")?,
         &["expectedHostEpoch", "allowInterruptActiveTasks"],
-        &["allowCooperativeHandoff"],
+        &["allowCooperativeHandoff", "handoffConnectionId"],
     )?;
     let boolean = |value: &Value| {
         value
@@ -59,6 +62,10 @@ pub fn decode_retirement_input(value: &Value) -> Result<RetirementInput> {
             .get("allowCooperativeHandoff")
             .map(boolean)
             .transpose()?,
+        handoff_connection_id: value
+            .get("handoffConnectionId")
+            .map(|value| codec::string(value, "Handoff connection ID", 128))
+            .transpose()?,
     })
 }
 
diff --git a/crates/runtime-host/src/server.rs b/crates/runtime-host/src/server.rs
index fc96afb63b..f17ef2bee0 100644
--- a/crates/runtime-host/src/server.rs
+++ b/crates/runtime-host/src/server.rs
@@ -104,7 +104,7 @@ impl Default for HostOptions {
 pub struct Host {
     options: HostOptions,
     retirement: Arc<Mutex<retirement::Phase>>,
-    accepted_connections: AtomicUsize,
+    accepted_connections: Mutex<std::collections::HashSet<Uuid>>,
     started: std::time::Instant,
     accepted_connection_revision: AtomicU64,
     executions: Arc<crate::execution::Executions>,
@@ -227,7 +227,7 @@ impl Host {
         let host = Arc::new(Self {
             options,
             retirement: interactions.retirement.clone(),
-            accepted_connections: AtomicUsize::new(0),
+            accepted_connections: Mutex::default(),
             started: std::time::Instant::now(),
             accepted_connection_revision: AtomicU64::new(0),
             shells: executions.shells.clone(),
@@ -349,3 +349,13 @@ impl Drop for ConnectionCount<'_> {
         self.0.fetch_sub(1, Ordering::SeqCst);
     }
 }
+
+struct AcceptedConnection<'a> {
+    connections: &'a Mutex<std::collections::HashSet<Uuid>>,
+    id: Uuid,
+}
+impl Drop for AcceptedConnection<'_> {
+    fn drop(&mut self) {
+        self.connections.lock().unwrap().remove(&self.id);
+    }
+}
diff --git a/crates/runtime-host/src/server/diagnostics.rs b/crates/runtime-host/src/server/diagnostics.rs
index 05c5215f15..0c6a261e8c 100644
--- a/crates/runtime-host/src/server/diagnostics.rs
+++ b/crates/runtime-host/src/server/diagnostics.rs
@@ -19,7 +19,7 @@
 
 use super::Host;
 use maka_protocol::host::{Diagnostics, Platform, Residency, Status};
-use std::{collections::VecDeque, sync::atomic::Ordering};
+use std::collections::VecDeque;
 
 pub(super) struct Activity {
     connections: usize,
@@ -56,8 +56,14 @@ impl Activity {
 
 impl Host {
     pub(super) fn activity(&self) -> Activity {
+        self.activity_except(None)
+    }
+
+    pub(super) fn activity_except(&self, handoff_connection: Option<uuid::Uuid>) -> Activity {
+        let connections = self.accepted_connections.lock().unwrap();
         Activity {
-            connections: self.accepted_connections.load(Ordering::SeqCst),
+            connections: connections.len()
+                - usize::from(handoff_connection.is_some_and(|id| connections.contains(&id))),
             commands: self.commands.len(),
             executions: self.executions.active_count(),
             shells: self.shells.active_count(),
diff --git a/crates/runtime-host/src/server/dispatch.rs b/crates/runtime-host/src/server/dispatch.rs
index 75e754c8be..93a576ae17 100644
--- a/crates/runtime-host/src/server/dispatch.rs
+++ b/crates/runtime-host/src/server/dispatch.rs
@@ -42,7 +42,10 @@ impl Host {
     ) -> Result<Outcome, HostError> {
         if operation == Operation::HostUpgradePrepare {
             return match self
-                .prepare_retirement(maka_protocol::host::decode_retirement_input(&input)?)
+                .prepare_retirement(
+                    maka_protocol::host::decode_retirement_input(&input)?,
+                    connection_id,
+                )
                 .await
             {
                 Ok(result) => Ok(Outcome::success(serde_json::to_value(result)?)),
diff --git a/crates/runtime-host/src/server/handshake.rs b/crates/runtime-host/src/server/handshake.rs
index cfb650fc71..5506a88ba0 100644
--- a/crates/runtime-host/src/server/handshake.rs
+++ b/crates/runtime-host/src/server/handshake.rs
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-use super::{ConnectionCount, Host, HostError, LifecycleMode, authority::Authority};
+use super::{AcceptedConnection, Host, HostError, LifecycleMode, authority::Authority};
 use maka_protocol::{
     COMPATIBILITY_EPOCH, COMPOSITION_ID,
     handshake::{ClientHello, HostHandshake, Lifecycle, ProtocolRange, Replacement},
@@ -29,7 +29,7 @@ use uuid::Uuid;
 
 type Admission<'a> = (
     HostHandshake,
-    Option<ConnectionCount<'a>>,
+    Option<AcceptedConnection<'a>>,
     Option<DropGuard>,
 );
 
@@ -54,7 +54,7 @@ impl Host {
         let generation_mismatch =
             ephemeral && hello.generation.is_some() && hello.generation != self.options.generation;
         let local_owner = matches!(authority, Authority::LocalOwner);
-        let settled = self.accepted_connections.load(Ordering::SeqCst) == 0
+        let settled = self.accepted_connections.lock().unwrap().is_empty()
             && self.requests.is_empty()
             && self.executions.active_count() == 0
             && self.shells.active_count() == 0;
@@ -103,7 +103,10 @@ impl Host {
                 None,
             ));
         };
-        self.accepted_connections.fetch_add(1, Ordering::SeqCst);
+        self.accepted_connections
+            .lock()
+            .unwrap()
+            .insert(connection_id);
         Ok((
             HostHandshake::Accepted {
                 root_id: self.root_id().into(),
@@ -116,7 +119,10 @@ impl Host {
                 state: Lifecycle::Ready,
                 cooperative_handoff: Some(true),
             },
-            Some(ConnectionCount(&self.accepted_connections)),
+            Some(AcceptedConnection {
+                connections: &self.accepted_connections,
+                id: connection_id,
+            }),
             None,
         ))
     }
@@ -124,7 +130,7 @@ impl Host {
     fn activity_snapshot(&self, version: Option<u8>) -> Value {
         let activity = self.activity();
         let mut result = json!({
-            "connections": self.accepted_connections.load(Ordering::SeqCst),
+            "connections": self.accepted_connections.lock().unwrap().len(),
             "activeOperations": self.commands.len(),
             "processUptimeSeconds": self.started.elapsed().as_secs(),
             "residencies": activity.residencies(),
diff --git a/crates/runtime-host/src/server/retirement.rs b/crates/runtime-host/src/server/retirement.rs
index 8c4dbdccd6..1be2d494d6 100644
--- a/crates/runtime-host/src/server/retirement.rs
+++ b/crates/runtime-host/src/server/retirement.rs
@@ -61,7 +61,14 @@ impl Host {
     pub(super) async fn prepare_retirement(
         &self,
         input: RetirementInput,
+        requesting_connection: uuid::Uuid,
     ) -> Result<RetirementResult, OperationError> {
+        let handoff_connection = input
+            .handoff_connection_id
+            .as_deref()
+            .map(str::parse::<uuid::Uuid>)
+            .transpose()
+            .map_err(|_| failure(Code::OperationConflict, "Invalid handoff connection"))?;
         let admission = self.executions.lock_admission().await;
         let preparation = {
             let mut phase = self.retirement.lock().unwrap_or_else(|e| e.into_inner());
@@ -74,7 +81,16 @@ impl Host {
                     "Host lifetime changed or retirement already began",
                 ));
             }
-            let activity = self.activity();
+            if handoff_connection.is_some_and(|id| {
+                id == requesting_connection
+                    || !self.accepted_connections.lock().unwrap().contains(&id)
+            }) {
+                return Err(failure(
+                    Code::OperationConflict,
+                    "Handoff connection is no longer attached",
+                ));
+            }
+            let activity = self.activity_except(handoff_connection);
             if input.allow_interrupt_active_tasks || !activity.blocks_retirement(1) {
                 *phase = Phase::Retiring;
                 self.draining.cancel();
@@ -90,12 +106,13 @@ impl Host {
             }
         };
         drop(admission);
-        self.cooperate(preparation).await
+        self.cooperate(preparation, handoff_connection).await
     }
 
     async fn cooperate(
         &self,
         mut preparation: Preparation<'_>,
+        handoff_connection: Option<uuid::Uuid>,
     ) -> Result<RetirementResult, OperationError> {
         let handoff_id = uuid::Uuid::new_v4().to_string();
         let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
@@ -103,7 +120,11 @@ impl Host {
         let mut reserved = BTreeSet::new();
         loop {
             let admission = self.executions.lock_admission().await;
-            if self.draining.is_cancelled() || self.activity().blocks_cooperation(1) {
+            if self.draining.is_cancelled()
+                || self
+                    .activity_except(handoff_connection)
+                    .blocks_cooperation(1)
+            {
                 return Ok(RetirementResult::ActiveTasks);
             }
             let Some(runs) = self.executions.cooperative_runs() else {
@@ -142,7 +163,11 @@ impl Host {
                     // fence. A callback either retains command residency here or
                     // observes Retiring before it can enter the dispatcher.
                     let mut phase = self.retirement.lock().unwrap_or_else(|e| e.into_inner());
-                    if self.draining.is_cancelled() || self.activity().blocks_cooperation(1) {
+                    if self.draining.is_cancelled()
+                        || self
+                            .activity_except(handoff_connection)
+                            .blocks_cooperation(1)
+                    {
                         return Ok(RetirementResult::ActiveTasks);
                     }
                     // All admitted owners are held or have completed. Commit is
diff --git a/crates/runtime-host/tests/integration/retirement.rs b/crates/runtime-host/tests/integration/retirement.rs
index 39d46c3f59..072018f6fb 100644
--- a/crates/runtime-host/tests/integration/retirement.rs
+++ b/crates/runtime-host/tests/integration/retirement.rs
@@ -70,6 +70,31 @@ async fn receive(frames: &mut mpsc::UnboundedReceiver<Value>) -> Value {
         .unwrap()
 }
 
+async fn attach(
+    host: &std::sync::Arc<Host>,
+) -> (
+    mpsc::UnboundedSender<Value>,
+    Value,
+    tokio::task::JoinHandle<Result<(), maka_runtime_host::server::HostError>>,
+) {
+    let (requests, reader) = mpsc::unbounded_channel();
+    let (frames, mut responses) = mpsc::unbounded_channel();
+    let task = tokio::spawn(host.clone().local_owner_connection(
+        Reader(reader),
+        Writer {
+            frames,
+            gate: None,
+            fail: false,
+        },
+    ));
+    requests.send(hello()).unwrap();
+    let handshake = receive(&mut responses).await;
+    assert_eq!(handshake["kind"], "accepted");
+    // Keep the writer open until the request side leaves.
+    tokio::spawn(async move { while responses.recv().await.is_some() {} });
+    (requests, handshake, task)
+}
+
 async fn short_connections_reset_idle_expiry(host: &std::sync::Arc<Host>) {
     let connect = || async {
         let (requests, reader) = mpsc::unbounded_channel();
@@ -164,8 +189,34 @@ async fn retirement_fences_admission_and_keeps_root_until_receipt_flushed_or_aba
         requests.send(hello()).unwrap();
         let handshake = receive(&mut responses).await;
         assert_eq!(handshake["state"], "ready");
-        requests.send(json!({"requestId":"retire", "operation":"host.upgrade.prepare",
-            "input":{"expectedHostEpoch":handshake["hostEpoch"], "allowInterruptActiveTasks":false}}))
+        let (desktop, desktop_identity, desktop_task) = attach(&host).await;
+        let (unrelated, _, unrelated_task) = attach(&host).await;
+        let mut input = json!({"expectedHostEpoch":handshake["hostEpoch"],
+            "allowInterruptActiveTasks":false, "allowCooperativeHandoff":true,
+            "handoffConnectionId":desktop_identity["connectionId"]});
+        requests
+            .send(json!({"requestId":"blocked", "operation":"host.upgrade.prepare", "input":input}))
+            .unwrap();
+        assert_eq!(
+            receive(&mut responses).await["result"]["kind"],
+            "active_tasks"
+        );
+        drop(unrelated);
+        unrelated_task.await.unwrap().unwrap();
+        input["handoffConnectionId"] = uuid::Uuid::new_v4().to_string().into();
+        requests
+            .send(json!({"requestId":"stale", "operation":"host.upgrade.prepare", "input":input}))
+            .unwrap();
+        assert_eq!(
+            receive(&mut responses).await["error"]["code"],
+            "operation_conflict"
+        );
+        input["handoffConnectionId"] = desktop_identity["connectionId"].clone();
+        requests
+            .send(
+                json!({"requestId":"retire", "operation":"host.upgrade.prepare",
+            "input":input}),
+            )
             .unwrap();
         tokio::time::timeout(Duration::from_secs(5), entered.cancelled())
             .await
@@ -202,6 +253,8 @@ async fn retirement_fences_admission_and_keeps_root_until_receipt_flushed_or_aba
         }
         drop(requests);
         assert_eq!(connection.await.unwrap().is_err(), fail);
+        drop(desktop);
+        desktop_task.await.unwrap().unwrap();
         drop(host);
         tokio::time::timeout(Duration::from_secs(5), server)
             .await
diff --git a/docs/rust-runtime.md b/docs/rust-runtime.md
index ff35d5753c..a3e90d4bd6 100644
--- a/docs/rust-runtime.md
+++ b/docs/rust-runtime.md
@@ -82,6 +82,9 @@ start a service or change account policy.
 Desktop reuses managed native Hosts and activates pinned code when needed;
 its own generation and exit do not govern their lifetime. Pausing local launches
 waits for outstanding activations before handing off the Root.
+Desktop can manage local native deployments and existing SSH/WSL native-operator
+profiles. Remote lifecycle commands use SSH/WSL OS authority, not WebSocket
+credentials. Stopped or uncertain targets stay paused until startup is confirmed.
 Deployment authority lives in account-level
 SQLite, outside the State Root; startup checks it before database migrations.
 On Windows, supervised installations use the sibling `maka-service.exe`, a
diff --git a/docs/rust-runtime.zh-CN.md b/docs/rust-runtime.zh-CN.md
index d50a28d87a..5d416e0be6 100644
--- a/docs/rust-runtime.zh-CN.md
+++ b/docs/rust-runtime.zh-CN.md
@@ -70,6 +70,9 @@ Linux inode 未变并保留 Root ID。不要用它接管复制的根或旧版数
 Windows 要求交互式用户会话。单独安装不会启动服务或修改账户策略。
 Desktop 复用原生托管 Host,需要时激活固定版本;自身版本变化和退出不控制托管 Host 的寿命。
 暂停本地启动后,会等待已发出的激活收尾,再交接 Root。
+Desktop 可管理本机原生部署及已有的 SSH/WSL 原生 operator 配置。远程生命周期命令使用
+SSH/WSL 的操作系统权限,不扩大 WebSocket 凭据权限;停止或结果未确认的目标保持暂停,
+直到启动得到确认。
 部署权威保存在 State Root 之外的账户级 SQLite 中,启动时先校验再执行数据库迁移。
 Windows 托管服务使用同目录的 `maka-service.exe`,它是同一 Host 的无窗口入口,须与 `maka.exe` 一起分发。
 按需激活要求启动环境允许脱离 Windows Job;应直接运行已构建的程序,不要通过 `cargo run` 激活。
diff --git a/tests/fixtures/client-native-managed-entry.mjs b/tests/fixtures/client-native-managed-entry.mjs
index 7006b822ff..6dbc0e1b7b 100644
--- a/tests/fixtures/client-native-managed-entry.mjs
+++ b/tests/fixtures/client-native-managed-entry.mjs
@@ -18,16 +18,15 @@
  */
 
 import assert from 'node:assert/strict';
-import { execFile } from 'node:child_process';
-import { parseArgs, promisify } from 'node:util';
+import { parseArgs } from 'node:util';
 import { createNativeRuntimeHostCandidateLaunchBarrier } from '../../apps/desktop/src/main/native-runtime-host.js';
 import { readNativeRuntimeHostDeployment } from '../../apps/desktop/src/main/native-runtime-host-deployment.js';
 import { createNativeRuntimeHostManagement } from '../../apps/desktop/src/main/native-runtime-host-management.js';
+import { runNativeRuntimeHostCommand } from '../../apps/desktop/src/main/native-runtime-host-command.js';
 import { decodeNativeRuntimeHostDeploymentStatus } from '../../apps/desktop/src/shared/native-runtime-host-deployment.js';
 import { connectExistingRuntimeHost } from '../../packages/runtime-host/src/client/connection.js';
-import { prepareConnectedRuntimeHostRetirement } from '../../packages/runtime-host/src/client/host-retirement.js';
+import { connectRuntimeHostProfile } from '../../packages/runtime-host/src/client/host-profile.js';
 
-const run = promisify(execFile);
 const { values } = parseArgs({
   options: {
     'native-managed': { type: 'string' },
@@ -35,11 +34,27 @@ const { values } = parseArgs({
     'root-id': { type: 'string' },
     revoked: { type: 'boolean', default: false },
     management: { type: 'boolean', default: false },
+    ssh: { type: 'string' },
   },
 });
 const executable = values['native-managed'];
 assert(executable && values.root && values['root-id']);
-const observed = await readNativeRuntimeHostDeployment(executable, values['root-id']);
+const operator = values.ssh
+  ? {
+      kind: 'ssh',
+      destination: values.ssh,
+      operator: { kind: 'native', platform: 'posix', executablePath: executable },
+    }
+  : { kind: 'local', executable };
+const execute = (args, readOnly = false) => runNativeRuntimeHostCommand(operator, args, readOnly);
+const read = async () =>
+  values.ssh
+    ? decodeNativeRuntimeHostDeploymentStatus(
+        JSON.parse(await execute(['status', '--root-id', values['root-id']], true)),
+        values['root-id'],
+      )
+    : readNativeRuntimeHostDeployment(executable, values['root-id']);
+const observed = await read();
 assert.equal(observed.kind, 'installed');
 assert.throws(() => decodeNativeRuntimeHostDeploymentStatus(observed, 'f'.repeat(64)));
 assert.throws(() =>
@@ -65,23 +80,19 @@ const request = {
   closeOnLauncherExit: true,
 };
 const connections = [];
+let pairing;
 async function stop() {
   const deadline = Date.now() + 5000;
   for (;;) {
-    const { stdout } = await run(
-      executable,
-      [
-        'host',
-        'stop',
-        '--root-id',
-        values['root-id'],
-        '--expected-deployment-id',
-        expected.deploymentId,
-        '--expected-revision',
-        String(expected.configRevision),
-      ],
-      { windowsHide: true },
-    );
+    const stdout = await execute([
+      'stop',
+      '--root-id',
+      values['root-id'],
+      '--expected-deployment-id',
+      expected.deploymentId,
+      '--expected-revision',
+      String(expected.configRevision),
+    ]);
     const outcome = JSON.parse(stdout);
     if (outcome.kind === 'stopped') return;
     assert.equal(outcome.kind, 'active_tasks');
@@ -141,8 +152,17 @@ try {
 } finally {
   barrier.release();
   for (const connection of connections) await connection.close();
+  if (pairing)
+    await execute([
+      'access',
+      'revoke',
+      '--root',
+      values.root,
+      '--credential-id',
+      pairing.credentialId,
+    ]);
   if (!values.revoked) {
-    const current = await readNativeRuntimeHostDeployment(executable, values['root-id']);
+    const current = await read();
     if (current.kind === 'installed' && current.deployment.admission === undefined) {
       expected = current.deployment;
       await stop();
@@ -154,7 +174,7 @@ async function verifyManagement() {
   let owned;
   let resumes = 0;
   const management = createNativeRuntimeHostManagement({
-    executable: observed.deployment.executable,
+    operator: values.ssh ? operator : { kind: 'local', executable: observed.deployment.executable },
     rootId: values['root-id'],
     rootPath: values.root,
     change: async (run) =>
@@ -163,15 +183,11 @@ async function verifyManagement() {
         resumeOnSuccess() {
           resumes++;
         },
-        async retire(deployment) {
+        async retire(deployment, hostEpoch, prepareRemote) {
           if (!owned) return true;
           assert.equal(deployment.deploymentId, expected.deploymentId);
-          const retired = await prepareConnectedRuntimeHostRetirement(
-            owned,
-            'refuse_active_work',
-            5000,
-          );
-          if (retired.kind === 'active_tasks') return false;
+          assert.equal(hostEpoch, owned.hostEpoch);
+          if (!(await prepareRemote(owned.connectionId))) return false;
           await owned.close();
           owned = undefined;
           return true;
@@ -181,7 +197,37 @@ async function verifyManagement() {
   const started = await management.run({ action: 'start' });
   assert.equal(started.outcome.kind, 'ready');
   assert.equal(resumes, 1);
+  if (values.ssh)
+    pairing = JSON.parse(
+      await execute([
+        'access',
+        'prepare',
+        '--root',
+        values.root,
+        '--principal',
+        'native-management-test',
+      ]),
+    );
   const connect = async () => {
+    if (pairing) {
+      const connection = await connectRuntimeHostProfile({
+        profile: {
+          id: 'native-test',
+          name: 'Native test',
+          kind: 'remote',
+          rootId: values['root-id'],
+          transport: {
+            kind: 'ssh',
+            destination: values.ssh,
+            activation: { kind: 'ssh_operator', operator: operator.operator },
+          },
+        },
+        credential: pairing.credential,
+        clientInstanceId: 'native-management-test',
+      });
+      connections.push(connection);
+      return connection;
+    }
     const result = await connectExistingRuntimeHost({
       rootPath: values.root,
       protocol: request.protocol,
@@ -192,6 +238,18 @@ async function verifyManagement() {
     return result.connection;
   };
   owned = await connect();
+  if (pairing) {
+    await owned.request('access.credential.finalize', {});
+    await owned.close();
+    owned = await connect();
+    await assert.rejects(
+      owned.request('host.upgrade.prepare', {
+        expectedHostEpoch: owned.hostEpoch,
+        allowInterruptActiveTasks: false,
+      }),
+      { code: 'unauthorized' },
+    );
+  }
   const other = await connect();
   const epoch = owned.hostEpoch;
   await assert.rejects(
diff --git a/tests/fixtures/client.mjs b/tests/fixtures/client.mjs
index 794a30487a..642ba7a832 100644
--- a/tests/fixtures/client.mjs
+++ b/tests/fixtures/client.mjs
@@ -54,19 +54,22 @@ await withSourceBundle(fileURLToPath(new URL(entry, import.meta.url)), (bundle,
   const child = spawnSync(process.execPath, [bundle, ...process.argv.slice(2)], {
     stdio: 'inherit',
     // The parent owns cleanup even if a failed fixture retains handles.
-    timeout: process.argv.includes('--live-provider-workspace')
-      ? 330000
-      : process.argv.includes('--oauth-execution-workspace')
-        ? 150000
-        : process.argv.includes('--message-queue-workspace')
-          ? 55000
-          : process.argv.includes('--large-output-workspace')
-            ? 120000
-            : process.argv.includes('--bash-workspace') ||
-                process.argv.includes('--onboarding-workspace') ||
-                (process.argv.includes('--native-managed') && process.argv.includes('--management'))
-              ? 45000
-              : 15000,
+    timeout: process.argv.includes('--ssh')
+      ? 180000
+      : process.argv.includes('--live-provider-workspace')
+        ? 330000
+        : process.argv.includes('--oauth-execution-workspace')
+          ? 150000
+          : process.argv.includes('--message-queue-workspace')
+            ? 55000
+            : process.argv.includes('--large-output-workspace')
+              ? 120000
+              : process.argv.includes('--bash-workspace') ||
+                  process.argv.includes('--onboarding-workspace') ||
+                  (process.argv.includes('--native-managed') &&
+                    process.argv.includes('--management'))
+                ? 45000
+                : 15000,
   });
   if (child.error) throw child.error;
   if (child.signal) throw new Error(`Original-client subprocess terminated by ${child.signal}`);


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org