Skip to content

feat(desktop): manage remote runtime host services - #3282

Merged
M4n5ter merged 8 commits into
mainfrom
feat/desktop-runtime-host-management
Aug 22, 2026
Merged

feat(desktop): manage remote runtime host services#3282
M4n5ter merged 8 commits into
mainfrom
feat/desktop-runtime-host-management

Conversation

@M4n5ter

@M4n5ter M4n5ter commented Aug 20, 2026

Copy link
Copy Markdown
Member
English

Summary

Add a per-computer management surface for SSH-managed Runtime Hosts. Desktop can inspect service status and bounded recent logs, start or restart the service, repair its managed installation, and uninstall the Maka-managed service while preserving both the State Root and Desktop Profile.

Onboarding now binds each managed Profile to the exact managed service and State Root. Every repair or management action revalidates that binding before mutation, so manual connection Profiles cannot manage an unrelated service and SSH destination drift fails closed. The SSH management channel also closes admission before Desktop shutdown.

Direct connection Profiles remain usable without claiming a service-management channel. Runtime Host protocol clients still cannot upgrade or terminate the service; these explicit management actions run through the separately authenticated SSH operator channel.

Depends on #3236

Fixes #3281

Verification

  • npm run lint
  • npm run format:check
  • npm run typecheck
  • npx knip --workspace apps/desktop
  • Runtime Host tests: 1025 passed, 1 skipped
  • CLI tests: 319 passed
  • Desktop tests: 992 passed
  • Exercised status, recent logs, service action errors, and uninstall confirmation against a Linux systemd user service over SSH; the uninstall action itself was cancelled

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: OpenAI Codex implemented the change and assisted with tests, validation, and PR wording under maintainer direction and review.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No
中文

概要

为通过 SSH 管理的 Runtime Host 增加 per-computer 管理界面。Desktop 可以查看服务状态和有界的近期日志,启动或重启服务,修复其托管安装,以及在保留 State Root 和 Desktop Profile 的前提下卸载 Maka 管理的服务。

Onboarding 现在会把每个 managed Profile 绑定到精确的 managed service 与 State Root。每次修复或管理操作都会在执行变更前重新验证该绑定,因此手工 connection Profile 无法管理无关服务,SSH 目标漂移也会 fail closed。Desktop shutdown 前还会先关闭 SSH 管理操作的 admission。

Direct connection Profile 仍可正常使用,但不会被描述为具备服务管理通道。Runtime Host protocol Client 仍不能升级或终止服务;这些显式管理操作经由单独认证的 SSH operator channel 执行。

依赖 #3236

修复 #3281

验证

  • npm run lint
  • npm run format:check
  • npm run typecheck
  • npx knip --workspace apps/desktop
  • Runtime Host 测试:1025 项通过,1 项跳过
  • CLI 测试:319 项通过
  • Desktop 测试:992 项通过
  • 通过 SSH 在 Linux systemd user service 上验证状态、近期日志、服务操作错误和卸载确认;实际卸载操作已取消

AI 使用

仅选择一项:

  • 没有生成式工具作出实质贡献
  • 生成式工具作出了实质贡献

工具与范围:OpenAI Codex 在维护者指导与审核下实施变更,并协助测试、验证和 PR 措辞。

检查清单

  • 测试覆盖此变更,且在缺少变更时会失败
  • lint、format、typecheck 和相关测试套件已在本地通过

这个 PR 是否包含行为变更?

  • 是——已在上方概要中说明

Product walkthrough / 功能演示

▶ Watch the complete management flow / 查看完整管理流程(MP4,45 秒)

Managed computer entry / 托管电脑入口 Service overview and actions / 服务概览与操作
Latest bounded logs / 最新的有界日志 Explicit uninstall boundary / 明确的卸载边界

Astro-Han added a commit that referenced this pull request Aug 20, 2026
Branch protection landed in .asf.yaml (#3262) and now requires an approving review and a passing `test` check on every pull request to main, with stale approvals dismissed on each new commit. Every open pull request based on main reports reviewDecision REVIEW_REQUIRED; #3282, based on a feature branch, reports nothing.

That leaves the fast path with nothing to exempt. CONTRIBUTING defined it as merging without independent human review, which a committer can no longer do. Redefining it as "the baseline alone" does not rescue it: the extra scrutiny in this section applies only to protected areas, and not touching a protected area was already a fast-path precondition, so the two scopes never overlap. A named fast path would exempt nothing while adding a comment obligation, and the maintainer sign-off it claimed to skip was never written down anywhere.

State the rule directly instead. The section gives the baseline every pull request clears, says what material changes need beyond it, and closes with "for everything else the baseline is enough" — which answers which changes take the light route without keeping a concept that no longer carries content. The merge-time comment goes with it: it existed to leave a trace for merges that had no approval, and every merge now has one.

Two corrections in the same section. The baseline says branch protection enforces the mechanics and that independent human judgment is policy GitHub cannot verify, because an approval from someone other than the author is not by itself proof of an independent human. The Chinese text is realigned so 重大 distributes across the whole protected list, matching the English, instead of modifying only the first item.

The public-decision rule moves from future to present tense. dev@maka.apache.org exists and carries active threads, so project-level decisions belong there now rather than "once an ASF development list is available".

.coderabbit.yaml and .github/skills/code-review/SKILL.md told automated reviewers not to make a fast-path determination. With the concept gone, both now say only that automated review is not authorization to merge. Their neighbouring lines about independent human review remain accurate and are left alone.

Generated-by: Claude Code
Astro-Han added a commit that referenced this pull request Aug 20, 2026
Branch protection landed in .asf.yaml (#3262) and now requires an approving review and a passing `test` check on every pull request to main, with stale approvals dismissed on each new commit. Every open pull request based on main reports reviewDecision REVIEW_REQUIRED; #3282, based on a feature branch, reports nothing.

That leaves the fast path with nothing to exempt. CONTRIBUTING defined it as merging without independent human review, which a committer can no longer do. Redefining it as "the baseline alone" does not rescue it: the extra scrutiny in this section applies only to protected areas, and not touching a protected area was already a fast-path precondition, so the two scopes never overlap. A named fast path would exempt nothing while adding a comment obligation, and the maintainer sign-off it claimed to skip was never written down anywhere.

State the rule directly instead. The section gives the baseline every pull request clears, names a maintainer as the one who decides whether a change is material and whether the review it received is enough, and closes with "for everything else the baseline is enough" — which answers which changes take the light route without keeping a concept that no longer carries content. Naming the classifier keeps what the old "a maintainer makes the final determination" actually did: settle whether a change needs more than the mechanics. The merge-time comment does not survive; it existed to leave a trace for merges that had no approval, and every merge now has one.

Two corrections in the same section. The baseline says branch protection enforces the mechanics and that independent human judgment is policy GitHub cannot verify, because an approval from someone other than the author is not by itself proof of an independent human. The Chinese text is realigned so 重大 distributes across the whole protected list, matching the English, instead of modifying only the first item.

The public-decision rule moves from future to present tense. dev@maka.apache.org exists and carries active threads, so project-level decisions belong there now rather than "once an ASF development list is available".

.coderabbit.yaml and .github/skills/code-review/SKILL.md told automated reviewers not to make a fast-path determination. With the concept gone, both now say only that automated review is not authorization to merge. Their neighbouring lines about independent human review remain accurate and are left alone.

Generated-by: Claude Code
M4n5ter pushed a commit that referenced this pull request Aug 20, 2026
Branch protection landed in .asf.yaml (#3262) and now requires an approving review and a passing `test` check on every pull request to main, with stale approvals dismissed on each new commit. Every open pull request based on main reports reviewDecision REVIEW_REQUIRED; #3282, based on a feature branch, reports nothing.

That leaves the fast path with nothing to exempt. CONTRIBUTING defined it as merging without independent human review, which a committer can no longer do. Redefining it as "the baseline alone" does not rescue it: the extra scrutiny in this section applies only to protected areas, and not touching a protected area was already a fast-path precondition, so the two scopes never overlap. A named fast path would exempt nothing while adding a comment obligation, and the maintainer sign-off it claimed to skip was never written down anywhere.

State the rule directly instead. The section gives the baseline every pull request clears, names a maintainer as the one who decides whether a change is material and whether the review it received is enough, and closes with "for everything else the baseline is enough" — which answers which changes take the light route without keeping a concept that no longer carries content. Naming the classifier keeps what the old "a maintainer makes the final determination" actually did: settle whether a change needs more than the mechanics. The merge-time comment does not survive; it existed to leave a trace for merges that had no approval, and every merge now has one.

Two corrections in the same section. The baseline says branch protection enforces the mechanics and that independent human judgment is policy GitHub cannot verify, because an approval from someone other than the author is not by itself proof of an independent human. The Chinese text is realigned so 重大 distributes across the whole protected list, matching the English, instead of modifying only the first item.

The public-decision rule moves from future to present tense. dev@maka.apache.org exists and carries active threads, so project-level decisions belong there now rather than "once an ASF development list is available".

.coderabbit.yaml and .github/skills/code-review/SKILL.md told automated reviewers not to make a fast-path determination. With the concept gone, both now say only that automated review is not authorization to merge. Their neighbouring lines about independent human review remain accurate and are left alone.

Generated-by: Claude Code
@M4n5ter
M4n5ter force-pushed the feat/desktop-managed-remote-onboarding branch from 32fa336 to 3f403af Compare August 20, 2026 09:42
@M4n5ter
M4n5ter force-pushed the feat/desktop-managed-remote-onboarding branch 3 times, most recently from 6cc6cd1 to 25bea91 Compare August 21, 2026 02:23
Base automatically changed from feat/desktop-managed-remote-onboarding to main August 21, 2026 06:09
@M4n5ter
M4n5ter force-pushed the feat/desktop-runtime-host-management branch from 8c188ca to 50a4915 Compare August 21, 2026 08:06
@M4n5ter
M4n5ter marked this pull request as ready for review August 21, 2026 08:07
@M4n5ter

M4n5ter commented Aug 21, 2026

Copy link
Copy Markdown
Member Author

The failing Windows package check matches #3340: a Runtime Host candidate survived the update handoff, leaving the installed app at 0.1.11 instead of 0.1.12. This is being addressed by #3382 and is not caused by this PR; we can rebase and rerun the check after that fix lands.

@M4n5ter
M4n5ter force-pushed the feat/desktop-runtime-host-management branch 2 times, most recently from 1626713 to 9f4b20a Compare August 22, 2026 05:53
Add an SSH-backed management surface for installed remote Runtime Host services so Desktop can inspect status and logs, recover or restart the service, and uninstall it without conflating service lifecycle with Profile or State Root ownership.

Keep direct connection Profiles usable without claiming a management channel, and use a bounded framed CLI result so machine output cannot expose launch configuration.

Generated-by: Codex
Bind repair and management to the exact service and State Root identity established during onboarding, and reject requests when that identity drifts.

Close SSH admission during shutdown and remove duplicate management state, IPC, and version derivation.

Generated-by: Codex
Route ongoing management through the installed Host operator, keep management metadata outside the shared connection profile schema, and make repair and uninstall recoverable across version and process boundaries.

Generated-by: Codex
Bind the installed operator to its original Client Data Root and give it a self-contained cleanup phase so interrupted uninstalls remain recoverable. Correlate management responses with requested actions and remove duplicate recovery and action representations.

Generated-by: Codex
Record the POSIX-only managed operator integration test added by the service hardening change so the checked Windows test inventory matches the suite.\n\nGenerated-by: Codex
Position the Runtime Host log view at its newest entry after every successful refresh so operators see the current service state immediately.\n\nGenerated-by: Codex
Keep the generated inventory summary aligned with the surface path set after rebasing onto the latest main branch.
@M4n5ter
M4n5ter force-pushed the feat/desktop-runtime-host-management branch from a1ffaea to 3e22749 Compare August 22, 2026 07:45
@likun666661

Copy link
Copy Markdown
Member

Design concern: please model remote lifecycle mutations as desired-state reconciliation, similar to Kubernetes, rather than as an imperative cross-machine workflow.

Persist the intent before network I/O. A managed Runtime Host record should carry a desired spec such as presence, running state, and service configuration, together with a generation/finalizer and observed status or conditions. A controller should periodically observe the actual remote state and retry idempotent actions with backoff until the resource converges.

For uninstall, set desired presence to absent first, retain a cleanup finalizer, reconcile the service, unit, config, operator, and deployment root toward absence, and remove the finalizer and local binding only after observing that all remote resources are absent. This naturally handles lost SSH responses, Desktop restarts, and partial remote commits, and it gives repair, configuration changes, and future updates one reusable model instead of adding operation-specific workflow states.

Interactive SSH can trigger a foreground reconciliation. Periodic retries should use non-interactive BatchMode and report an AuthenticationRequired condition rather than repeatedly opening a terminal.

This is my only design objection to the PR; the target binding and separate SSH operator authority fit naturally underneath such a reconciler.

中文

设计意见:远端生命周期变更应当像 Kubernetes 一样建模为 desired-state reconciliation,而不是一个跨机器的命令式工作流。

应当先持久化用户意图,再执行网络操作。Managed Runtime Host 记录可以包含期望的 presence、running 状态和服务配置,以及 generation、finalizer 和 observed status/conditions。Controller 周期观察远端实际状态,并通过带退避的幂等操作不断重试,直到实际状态收敛。

对于卸载,应先把 desired presence 写成 absent,保留 cleanup finalizer,然后持续把 service、unit、config、operator 和 deployment root 收敛为不存在;只有观察到所有远端资源都不存在后,才移除 finalizer 和本地 binding。这样可以自然处理 SSH 响应丢失、Desktop 重启和远端部分提交,也能让 repair、配置变更和未来 update 复用同一个模型,而不是继续增加特定操作的流程状态。

交互式 SSH 可以触发一次前台 reconcile;后台周期重试应使用非交互 BatchMode,并在需要认证时记录 AuthenticationRequired condition,而不是反复弹出终端。

这是我对本 PR 唯一的设计异议;target binding 和独立 SSH operator authority 都可以自然地保留在 reconciler 之下。

export interface DesktopRuntimeHostManagedServiceBinding {
readonly profile: RemoteRuntimeHostProfile;
readonly service: DesktopRuntimeHostManagedService;
readonly state: "active" | "uninstalling";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This persists the last completed workflow step rather than the desired remote state. Please consider making this record resource-shaped: a spec for desired presence/running/configuration, metadata.generation plus a cleanup finalizer, and status.observedGeneration/conditions. The phase should be an observation, not the authority that decides the next mutation; a reconciler should recompute the next action from actual remote state on every retry.

中文

这里持久化的是上一次完成到哪个流程步骤,而不是远端资源的期望状态。建议把记录改成资源模型:用 spec 表达期望的 presence、running 和 configuration,用 metadata.generation 与 cleanup finalizer 管理生命周期,再用 status.observedGeneration 和 conditions 表达观察结果。phase 应当只是观测结果,不应成为决定下一步 mutation 的 authority;reconciler 每次重试都应该从远端实际状态重新计算下一步动作。

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

English

Thank you. I do not plan to make this record Kubernetes-shaped in this PR. uninstalling is not an observation of the last completed workflow step; it is the sole durable cleanup obligation after the idempotent service removal has been confirmed and before the operator removes itself. An active binding retries service removal, an uninstalling binding retries deployment cleanup, and the binding is removed only after cleanup succeeds.

There is no unattended controller authority in this slice, so adding spec, generation, finalizers, and observed status would encode the same two recovery states while introducing ownership and scheduling machinery that nothing currently executes. #3228 already reserves bounded automatic reconciliation for update policy; persistent desired versions and maintenance policy will provide concrete requirements for that model there.

中文

谢谢。我不打算在本 PR 中把这条记录改成 Kubernetes 式资源模型。uninstalling 并不是对“上一个 workflow step”的观测;它是幂等 service removal 已确认完成、operator 即将删除自身之前,唯一需要持久保留的 cleanup obligation。active binding 会重试 service removal,uninstalling binding 会重试 deployment cleanup,只有 cleanup 成功后才删除 binding。

本阶段没有无人值守的 controller authority,因此添加 spec、generation、finalizer 和 observed status 只会用更多机制重复表达相同的两个恢复状态,但没有任何现存 owner 去执行新增的调度语义。#3228 已经把 bounded automatic reconciliation 放在 update policy 阶段;届时 persistent desired version 和 maintenance policy 会为该模型提供真实需求。

return input.runServiceManagement(managementInput);
}

let pending = managed;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This IPC handler currently owns a synchronous cross-machine transaction. I suggest that uninstall first durably writes desired presence as absent and returns the reconciled/pending resource status; a controller should then probe and idempotently remove the remote service and deployment until convergence. Network loss or a Desktop crash should delay convergence, not require remembering which command response was received.

中文

当前 IPC handler 直接承担了一个同步的跨机器事务。建议卸载时先持久化 desired presence 为 absent,并返回当前的 reconciled/pending resource status;之后由 controller 观察远端并幂等地删除 service 和 deployment,直到收敛。网络中断或 Desktop 崩溃应该只会延迟收敛,而不应该要求系统记住上一条命令的响应是否已经收到。

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

English

The IPC call waits for a foreground operation, but it is not relying on an indivisible cross-machine transaction. Both remote stages are idempotent. If the service response is lost, the binding remains active and the same uninstall is safely replayed. Once absence is confirmed, uninstalling is persisted before deployment cleanup, so a restart or unknown cleanup outcome resumes from that durable boundary.

Persisting desired absence before the first network request would add value if an unattended controller were expected to resume the operation automatically. This slice instead uses interactive SSH and requires a user-owned foreground retry when authentication or connectivity is unavailable. I am therefore keeping the smaller two-phase recovery model rather than adding controller state without a controller owner.

中文

IPC 调用会等待一个前台操作,但它并不依赖不可分割的跨机器事务。两个远程阶段都是幂等的。如果 service 响应丢失,binding 仍保持 active,可以安全重放同一个 uninstall。确认 service 不存在后,系统会在 deployment cleanup 前持久化 uninstalling,因此重启或 cleanup 结果未知时都能从这个持久边界恢复。

如果存在一个无人值守 controller,需要它自动恢复操作,那么在第一次网络请求前持久化 desired absence 会有额外价值。本阶段使用交互式 SSH,认证或连接不可用时需要用户所有的前台重试。因此我会保留更小的两阶段恢复模型,而不是在没有 controller owner 时先引入 controller state。

function runtimeHostManagedDeploymentCleanupRemoteCommand(operatorPath: string): string {
const operator = quotePosix(operatorPath);
const invocation =
`if [ ! -e ${operator} ]; then exit 0; fi; exec ${operator} __cleanup-managed-deployment`;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a concrete case where reconciliation should be based on observed resource state: a missing operator is not equivalent to an absent managed deployment. During finalizer reconciliation, probe the service, unit, config, operator, and deployment root independently; keep retrying or report a blocked condition until every owned remote resource is observed absent, and only then remove the local binding.

中文

这里正好说明 reconciliation 必须基于对资源实际状态的观察:operator 不存在并不等价于整个 managed deployment 已不存在。在 finalizer reconcile 中,应分别探测 service、unit、config、operator 和 deployment root;在所有受管理的远端资源都被观察为不存在之前,应持续重试或记录 blocked condition,只有全部收敛后才能删除本地 binding。

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

English

I agree with the narrower invariant here, although I do not think it requires the broader resource model. The cleanup launcher removes every managed deployment entry before deleting the operator, so a missing operator proves that the managed payload was removed; however, the final rmdir may still have failed after the operator deleted itself.

I tightened this path so a missing operator succeeds only when the deployment root is also absent or rmdir proves it is empty. A non-empty, redirected, or otherwise undeletable root now produces a non-zero result, preserving the uninstalling binding for recovery. The service was already confirmed absent before entering this phase, so separately probing the service and unit here would duplicate the preceding authority rather than close another gap.

中文

我认同这里更窄的 invariant,但不认为它需要更广的 resource model。Cleanup launcher 会在删除 operator 之前先删除所有 managed deployment entry,因此 operator 缺失能够证明 managed payload 已经移除;但 operator 删除自身后,最后的 rmdir 仍可能失败。

我已经收紧了这条路径:operator 缺失时,只有 deployment root 也不存在,或者 rmdir 能证明它为空,cleanup 才会成功。非空、被重定向或其他无法删除的 root 现在会返回非零结果,从而保留 uninstalling binding 以便恢复。进入这个阶段前,service 已经确认不存在,因此在这里重复探测 service 和 unit 只会复制前一个 authority,不会弥补新的缺口。

Keep an uninstall binding recoverable when the remote operator is already absent but its deployment root still contains data. Treat cleanup as complete only when that root is absent or can be removed as an empty directory.

Generated-by: Codex
@M4n5ter

M4n5ter commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

@likun666661 Thank you for the thoughtful review.

English

I agree that desired-state reconciliation is the right model once Maka has continuous, unattended lifecycle management. After reconsidering this PR against that model, however, I do not think introducing a resource-shaped controller here would improve the current design.

This PR handles explicit foreground operations that may require interactive SSH authentication. It does not yet have a durable controller that can continue running independently of Desktop, and introducing spec, status, generations, finalizers, conditions, retry scheduling, and controller ownership would create that entire authority before we have a supported unattended execution model.

The current uninstall path is already a small recoverable state machine:

active
  → idempotently remove the remote service
  → persist uninstalling
  → idempotently remove the managed deployment
  → remove the local binding

If the service response is lost, the binding remains active and the same idempotent uninstall is retried. Once the service is confirmed absent, uninstalling durably records the remaining cleanup responsibility before the operator removes itself. A Desktop restart or SSH interruption therefore delays completion without losing the recovery path.

Persisting desired absence before the first network request would be necessary if the mutation were non-idempotent or an unattended controller were expected to resume it automatically. Neither is true in this slice. Background reconciliation would also need a defined owner and an authentication UX for password, passphrase, MFA, or host-key prompts; BatchMode plus an AuthenticationRequired condition would add persistent product state while still requiring the user to return for an interactive operation.

The tracking plan already reserves bounded automatic reconciliation for the later update-policy work. That is where persistent desired versions, maintenance policy, health observation, retry behavior, and Linux/macOS backend convergence will provide concrete requirements for a reconciler. I would prefer to design that authority from those requirements instead of introducing a Kubernetes-shaped model speculatively in this service-management PR.

Your concrete observation about a missing operator is still useful. The cleanup launcher deletes deployment contents before deleting itself, so a missing operator normally proves the package files are gone. However, the final deployment-root removal may have failed after the operator was deleted. I will tighten that boundary so an absent operator is accepted only when the deployment root is also absent or can be proven empty; otherwise the uninstalling binding remains for recovery.

中文

我认同,当 Maka 进入持续、无人值守的生命周期管理阶段后,desired-state reconciliation 会是合适的模型。不过结合当前 PR 的实际范围重新审视后,我认为现在引入 resource-shaped controller 并不会改善设计。

本 PR 处理的是用户显式触发、可能需要交互式 SSH 认证的前台操作。目前不存在一个能够脱离 Desktop 持续运行的持久 controller;如果现在引入 specstatus、generation、finalizer、condition、重试调度和 controller ownership,实际上是在无人值守执行模型尚未成立前,提前引入整套新的 authority。

当前卸载流程已经是一个小型、可恢复的状态机:

active
  → 幂等删除远程 service
  → 持久化 uninstalling
  → 幂等删除 managed deployment
  → 删除本地 binding

如果 service 响应丢失,binding 仍保持 active,重试同一个幂等 uninstall 即可。service 确认不存在后,系统会在 operator 删除自身之前持久化 uninstalling,记录剩余的 cleanup 责任。因此 Desktop 重启或 SSH 中断只会延迟完成,不会丢失恢复路径。

只有 mutation 不幂等,或者系统需要由无人值守 controller 自动恢复时,才必须在第一次网络请求前持久化 desired absence。本阶段两者都不成立。后台 reconciliation 还需要明确 controller owner,以及 password、passphrase、MFA 和 host-key prompt 的认证体验;BatchModeAuthenticationRequired condition 会增加新的持久产品状态,但最终仍需要用户回来完成交互式操作。

Tracking plan 已经把 bounded automatic reconciliation 放在后续 update-policy 阶段。届时会有持久的 desired version、维护策略、健康观测、重试行为,以及 Linux/macOS backend 收敛等具体需求。相比在当前 service-management PR 中预先套用 Kubernetes 模型,我更倾向于根据这些真实需求设计对应的 authority。

你关于 operator 缺失的具体观察仍然有价值。Cleanup launcher 会先删除 deployment 内容,最后删除自身,因此 operator 缺失通常能够证明 package 文件已经清除;但 operator 删除后,最终删除 deployment root 的步骤仍可能失败。我会收紧这个边界:只有 deployment root 同样不存在,或者能够证明它为空时,才把 operator 缺失视为 cleanup 完成;否则保留 uninstalling binding 以便继续恢复。

@M4n5ter
M4n5ter requested a review from likun666661 August 22, 2026 08:23

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @M4n5ter — reviewed at exact head 27a355a00849206c4b666b828dcf8ff8a733ca31. No P0–P2; approving. Two P3 notes at the end.

Given the size of this change (37 files across main, preload and renderer) it was looked at along two independent lines — reachable failure paths, and authority structure plus test coverage — and the load-bearing claims from each were then checked adversarially by a third pass.

Authority placement

The management authority stays on the remote operator; Desktop is a thin client that reaches it over SSH through the framed operator protocol. Generalising createSetupOutputFilter into createFramedOutputFilter reuses the existing SSH/terminal mechanism instead of introducing a second one. runtime-host-managed-services.ts holds the client-side profile-to-service binding deliberately outside the shared profile catalog — the test asserting exactly that makes the boundary explicit rather than incidental — with a file lock, a strict schema and atomic writes.

Trust boundary

Preload gains a single new entry point, runtimeHostManagement.run(profileId, action). Both gates are in the main process: action is checked against the six-entry MANAGEMENT_ACTIONS set on the first line, and profileId is length-validated and then resolved against stored bindings, throwing if it is not bound. Everything that ends up in a path or an SSH destination — destination, operatorPath, rootPath, serviceId, rootId — comes from that binding lookup rather than from the caller, so there is no renderer-controlled value reaching command or path construction.

Lifecycle and recovery

Local state advances only after the remote returns a framed result, and uninstall only clears a binding once the remote confirms not_installed; disconnects, timeouts, error frames and missing result frames all fail without clearing state. A failed cleanup keeps the uninstalling marker so a retry does not re-submit the service uninstall, and the operator-missing case requires the deployment root to be provably absent or empty rather than treating leftover data as success.

Single owner for the SSH terminal

active has one writer and startTerminalProcess throws when it is already set, so setup, tunnel, service management and deployment cleanup cannot preempt one another. It is cleared on all three completion paths, and waitForTerminalProcess has a timeout plus explicit termination, so an abnormal exit does not leave the slot permanently occupied and block later operations. Profile catalog, preferences and binding mutations are serialised on a single tail, and markUninstalling / clear re-verify both profile target and service identity before acting.

Two P3 notes, neither blocking

  • Binding cleanup in removeProfile is best-effort and only logs on failure, which can leave an orphan record whose profile is gone. It is inert as written: removeIfCurrent requires an exact service id and root path match, an orphan is no longer reachable through the catalog, and binding ids are UUIDs so a new profile cannot land on the same one. Worth a follow-up only to keep the store tidy.
  • runtime-host-managed-services.test.ts covers the concurrent-write, target-mismatch and markUninstalling / removeIfCurrent paths but has a single happy path for the store itself; decode failure on a malformed or oversized document, the binding-count limit, and the non-SSH save rejection are untested. Decode throws, so the untested direction is fail-closed.

Exact-head audit, package and test all SUCCESS; git diff --check clean.


This review was AI-assisted. It is not a substitute for independent human review by a committer.

@M4n5ter

M4n5ter commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

@Astro-Han Thank you for the exact-head review and approval.

English

I agree that both P3 observations are accurate, but I do not plan additional changes for them in this PR.

For Profile removal, the ordering is deliberate: the Profile remains the reachable authority, so it is removed before its now-inert managed-service metadata. Reversing the order would create the more harmful failure mode where a still-existing Profile permanently loses its management binding. A cleanup failure can leave an unreachable orphan record, but it cannot be resolved through the catalog or inherited by a new UUID-based Profile. I would prefer to revisit cleanup only if real failures show that these records accumulate, rather than introduce a cross-file transaction or background collector now.

For store validation, malformed or oversized documents, the binding limit, and non-SSH saves all fail closed through direct validation without recovery or state-transition semantics. The existing tests cover the load-bearing contracts: concurrent writes, target mismatch, markUninstalling, and guarded removal. Additional branch-by-branch decoder tests would mostly restate the implementation, so I am keeping the current test budget.

Thanks again for checking the authority boundaries, recovery behavior, and exact-head CI state.

中文

感谢你对精确 head 的审查和批准。

我认同这两个 P3 观察在事实上都成立,但不准备在本 PR 中继续修改。

对于 Profile removal,当前顺序是有意选择的:Profile 是仍然可达的 authority,因此先删除 Profile,再清理已经不可达的 managed-service metadata。反转顺序会产生更严重的失败模式——Profile 仍然存在,但永久失去 management binding。Cleanup 失败可能留下不可达的孤立记录,但它无法通过 catalog 使用,也不会被使用 UUID 的新 Profile 继承。除非真实故障证明这些记录会持续累积,否则我不希望现在为此引入跨文件事务或后台回收器。

对于 store validation,malformed 或 oversized document、binding limit 和 non-SSH save 都通过直接校验 fail closed,不涉及恢复或状态迁移语义。现有测试已经覆盖真正承重的契约:并发写入、target mismatch、markUninstalling 和 guarded removal。继续为每个 decoder 分支增加测试主要是在复述实现,因此这里保留当前测试预算。

再次感谢你核查 authority boundary、恢复行为和精确 head 的 CI 状态。

@M4n5ter
M4n5ter merged commit 401c7c9 into main Aug 22, 2026
3 checks passed
@M4n5ter
M4n5ter deleted the feat/desktop-runtime-host-management branch August 22, 2026 10:34
@M4n5ter
M4n5ter restored the feat/desktop-runtime-host-management branch August 22, 2026 10:40
@M4n5ter
M4n5ter deleted the feat/desktop-runtime-host-management branch August 22, 2026 10:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(desktop): manage installed remote Runtime Host services

3 participants