feat(runtime-host): add managed Host retirement - #3557

Merged
M4n5ter merged 4 commits into
mainfrom
feat/runtime-host-managed-retirement
Aug 23, 2026
Merged

feat(runtime-host): add managed Host retirement#3557
M4n5ter merged 4 commits into
mainfrom
feat/runtime-host-managed-retirement

Conversation

@M4n5ter

@M4n5terM4n5ter commented Aug 23, 2026

Copy link
Copy Markdown
Member
English

Summary

Adds the local-operator retirement boundary required before a managed Runtime Host service can be replaced. The CLI now binds the exact live Host to the managed service process, reports when active work blocks retirement unless interruption was explicitly authorized, stops every non-stable service state, prevents intentional retirement from being restarted by the service manager, and verifies that the State Root writer was released. Standalone service mutations are serialized with setup and deployment lifecycle operations.

This reuses the existing exact-Epoch Host prepare contract. Package staging, version switching, replacement startup, and update policy remain outside Runtime Host authority.

Refs #3231
Refs #3228

Verification

  • npm --workspace @maka/runtime-host test — 1,086 tests passed
  • npm --workspace maka-agent test — 393 tests passed
  • Desktop Runtime Host management tests — 4 tests passed
  • npm run lint
  • npm run format:check
  • npm run typecheck
  • Disposable Linux systemd user service: install, target-mismatch rejection, retire, idempotent retry, restart, status, uninstall, and cleanup all passed

AI use

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

Tool(s) and scope: OpenAI Codex contributed substantively to the implementation and tests under the contributor's direction. The human contributor owns review and submission.

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
简体中文

概要

增加 managed Runtime Host service 在替换前所需的 local-operator 退场边界。CLI 现在会将精确的 live Host 与 managed service 进程绑定;若未明确授权中断,则在存在 active work 时阻止退场;停止所有非稳定状态的 service,阻止 service manager 重启主动退场的进程,并验证 State Root writer 已释放。独立执行的 service mutation 也会与 setup 和 deployment lifecycle 操作串行化。

该实现复用现有的 exact-Epoch Host prepare contract。Package staging、版本切换、replacement 启动和更新策略仍不属于 Runtime Host authority。

关联 #3231
关联 #3228

验证

  • npm --workspace @maka/runtime-host test — 1,086 项测试通过
  • npm --workspace maka-agent test — 393 项测试通过
  • Desktop Runtime Host management — 4 项测试通过
  • npm run lint
  • npm run format:check
  • npm run typecheck
  • 一次性 Linux systemd user service:install、target mismatch 拒绝、retire、幂等重试、restart、status、uninstall 与清理均通过

AI 使用

  • 无生成式工具实质参与
  • 生成式工具有实质参与

工具与范围:OpenAI Codex 在贡献者指导下实质参与实现与测试。人工贡献者负责审查与提交。

检查清单

  • 测试覆盖该变更,且缺少该变更时会失败
  • lint、format、typecheck 与受影响测试套件均已在本地通过

本 PR 是否改变行为?

  • 是 — 已在概要中说明

@M4n5ter
M4n5terforce-pushed the feat/runtime-host-managed-retirement branch 2 times, most recently from 3c62651 to d7fb7bdCompareAugust 23, 2026 07:28
Managed updates need a local operator path that fences admission before the service releases its State Root. Reuse the existing exact-Epoch Host prepare contract, then let the CLI service manager stop the unit and verify writer release.
The new retire action remains outside ordinary remote Client authority and returns bounded active-work or completed-retirement facts for later update orchestration.
Generated-by: Codex
Bind retirement to the managed service PID, stop every non-stable service state, and serialize standalone mutations with setup lifecycle operations. Keep the operator result contract explicit without projecting diagnostic snapshots as authoritative blockers.
Generated-by: Codex
Give intentional Host retirement a process outcome that the service manager treats as successful without restarting. Require every reported service PID to pass exact-Host preparation, and make active-work refusal unsuccessful to shell and JSON callers.
Generated-by: Codex
Hold the exact State Root writer while stopping a transitional managed service that has not published a PID. This prevents a systemd restart from admitting durable work between the status snapshot and stop, while failing closed if another Host wins the writer first.
Generated-by: Codex

@Astro-HanAstro-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.

Independent review of 0768ef9bbe526ce09e0d949de1c346dfb65f24d6, from two lines that did not share a starting point or read each other's findings.

No P0–P2. One [P3]. Exact-head checks: test is completed / success on that SHA. audit and package did not run on this head — recorded as not-run, not as green.

The retirement chain holds

Identity binding is triple-checked on the pid-known path: service status pid, then queryHostDiagnostics pid, then the pid returned by host.upgrade.prepare, with expectedHostEpoch anchoring the operation to one exact Host. Blocking is decided inside the Host by #hasUpgradeBlockingActivity() rather than projected from the CLI's diagnostics snapshot, and active_tasks is returned to the caller rather than acted on. After the drain gate is set, both the handshake admission read and begin-operation refuse new work, so a task starting just after the check cannot slip past the fence.

--allow-interrupt-active-tasks is a per-call boolean. It is not persisted to service config, does not change later defaults, and the operation is absent from REMOTE_OWNER_OPERATION_GRANTS — only local-owner IPC can invoke it. Its scope is broad (all blocking work on that exact Host) but explicit, which matches forced-retirement semantics.

Restart suppression is closed rather than assumed: shutdownReason='retirement' drives both exit paths to code 3, and the unit sets SuccessExitStatus=3 with RestartPreventExitStatus=3 against Restart=always. Note that a manual systemctl start remains possible; that is an operator action, not a service-manager auto-restart, and nothing here claims otherwise.

[P3] The pid-known path does not re-verify identity across stop

packages/cli/src/runtime-host-service-manager.ts acquires rootFence only in the starting/pid-less branch. The pid-known path calls backend.stop() with no fence held, and afterwards checks only that the service reached stopped with pid === null, then that the root was released.

If the old Host crashes during drain with a code other than 3, Restart=always can bring up a successor that takes the State Root writer and admits durable work before our stop lands. That stop then terminates the successor, the stopped-state assertion passes, the root is genuinely released — and the call still reports {kind: 'retired', hostEpoch, pid} carrying the original identity. A generation change happened inside the window that was supposed to end in retirement, the successor's writes remain, and the success result names a Host that is not the one that was stopped.

No data is lost — writer exclusivity is never violated — and the window requires a crash plus a restart to land between prepare and stop, so this is a narrow timing path rather than anything an attacker steers. Hence P3.

The minimal fix is to hold the root fence across stop on the pid-known path as well, or to re-read identity evidence after stop (restart counter, or reconnect and compare epoch) before reporting retired.

Boundaries

The service-management authority is split cleanly — Host owns admission and drain, the CLI owns managed PID, stop, and writer observation, systemd owns restart policy — and package staging, replacement startup, and update policy stay outside Runtime Host, as the description states. No duplicated retirement authority was found across the 14 files.

Limitations

Both reviewers were on Linux and could not exercise real systemd unit behaviour; the SuccessExitStatus / RestartPreventExitStatus link rests on source reading plus unit-rendering assertions, not on observed systemd action. Local runs: service-manager 21/21 and setup green; host-kernel 57/61, where the four failures reproduce identically on pristine main (missing Electron libatk in this environment) and are not attributed to this PR. The author's own full runtime/agent/desktop suites and the disposable systemd end-to-end run were not independently reproduced.


AI-assisted review, two independent lines. Under CONTRIBUTING.md §Review this does not replace the required independent human review.

@M4n5ter
M4n5ter merged commit 7f68af7 into mainAug 23, 2026
1 of 2 checks passed
@M4n5ter
M4n5ter deleted the feat/runtime-host-managed-retirement branch August 23, 2026 08:58

@Astro-HanAstro-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.

Two additions from a third reviewer, after the approval above. Neither changes the P3 severity; both make it more actionable.

A better fix direction than the one I suggested. I proposed either holding the fence across stopor re-reading identity after stop. The second option is weak: by the time you re-read, the successor has already been stopped and its writes cannot be undone, so you learn about the generation change without being able to prevent it. The right shape is to acquire the root fence afterprepare has released the old Host and hold it throughbackend.stop() until the service is stably stopped — failing closed if a successor wins the lock first, rather than stopping it as though it were the original Host. The starting/pid-less branch already has exactly this shape; the pid-known branch should converge on the same authority.

There is also a narrow semantic consequence worth noting: work admitted by the successor inside that window is invisible to the old Host's active-work check, so allowInterruptActiveTasks=false can be satisfied while durable work is nonetheless interrupted.

The retired wording overstates what the unit does.runtime-host-systemd-service.ts:218 sets RestartPreventExitStatus=3, which suppresses Restart=always for that exit only. The unit remains installed and enabled with WantedBy=default.target at :227. So besides a manual systemctl start, a future user-manager or default-target activation will also start it. The current human-facing "service is retired" reads as durable disablement.

Suggested wording: stopped for replacement; immediate restart suppressed; unit remains installed and enabled, and may be started explicitly or by a future target activation.

For the record on what verifyRuntimeHostRootReleased() proves: tryAcquireInteractiveRootOwner() takes an OS-level non-blocking exclusive lock on the exact root's owner.lock and revalidates the lock artifact against root identity, so a successful acquisition is real observation — at that instant, the old Host holds no writer and neither does anyone else. It is a snapshot of lock vacancy, not evidence of an identity-contiguous handoff, and it stops proving anything once owner.close() runs.

Sign up for freeto 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.

2 participants

@M4n5ter@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(runtime-host): add managed Host retirement - #3557

Merged
M4n5ter merged 4 commits into
mainfrom
feat/runtime-host-managed-retirement
Aug 23, 2026
Merged

feat(runtime-host): add managed Host retirement#3557
M4n5ter merged 4 commits into
mainfrom
feat/runtime-host-managed-retirement

Conversation

@M4n5ter

@M4n5terM4n5ter commented Aug 23, 2026

Copy link
Copy Markdown
Member
English

Summary

Adds the local-operator retirement boundary required before a managed Runtime Host service can be replaced. The CLI now binds the exact live Host to the managed service process, reports when active work blocks retirement unless interruption was explicitly authorized, stops every non-stable service state, prevents intentional retirement from being restarted by the service manager, and verifies that the State Root writer was released. Standalone service mutations are serialized with setup and deployment lifecycle operations.

This reuses the existing exact-Epoch Host prepare contract. Package staging, version switching, replacement startup, and update policy remain outside Runtime Host authority.

Refs #3231
Refs #3228

Verification

  • npm --workspace @maka/runtime-host test — 1,086 tests passed
  • npm --workspace maka-agent test — 393 tests passed
  • Desktop Runtime Host management tests — 4 tests passed
  • npm run lint
  • npm run format:check
  • npm run typecheck
  • Disposable Linux systemd user service: install, target-mismatch rejection, retire, idempotent retry, restart, status, uninstall, and cleanup all passed

AI use

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

Tool(s) and scope: OpenAI Codex contributed substantively to the implementation and tests under the contributor's direction. The human contributor owns review and submission.

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
简体中文

概要

增加 managed Runtime Host service 在替换前所需的 local-operator 退场边界。CLI 现在会将精确的 live Host 与 managed service 进程绑定;若未明确授权中断,则在存在 active work 时阻止退场;停止所有非稳定状态的 service,阻止 service manager 重启主动退场的进程,并验证 State Root writer 已释放。独立执行的 service mutation 也会与 setup 和 deployment lifecycle 操作串行化。

该实现复用现有的 exact-Epoch Host prepare contract。Package staging、版本切换、replacement 启动和更新策略仍不属于 Runtime Host authority。

关联 #3231
关联 #3228

验证

  • npm --workspace @maka/runtime-host test — 1,086 项测试通过
  • npm --workspace maka-agent test — 393 项测试通过
  • Desktop Runtime Host management — 4 项测试通过
  • npm run lint
  • npm run format:check
  • npm run typecheck
  • 一次性 Linux systemd user service:install、target mismatch 拒绝、retire、幂等重试、restart、status、uninstall 与清理均通过

AI 使用

  • 无生成式工具实质参与
  • 生成式工具有实质参与

工具与范围:OpenAI Codex 在贡献者指导下实质参与实现与测试。人工贡献者负责审查与提交。

检查清单

  • 测试覆盖该变更,且缺少该变更时会失败
  • lint、format、typecheck 与受影响测试套件均已在本地通过

本 PR 是否改变行为?

  • 是 — 已在概要中说明

@M4n5ter
M4n5terforce-pushed the feat/runtime-host-managed-retirement branch 2 times, most recently from 3c62651 to d7fb7bdCompareAugust 23, 2026 07:28
Managed updates need a local operator path that fences admission before the service releases its State Root. Reuse the existing exact-Epoch Host prepare contract, then let the CLI service manager stop the unit and verify writer release.
The new retire action remains outside ordinary remote Client authority and returns bounded active-work or completed-retirement facts for later update orchestration.
Generated-by: Codex
Bind retirement to the managed service PID, stop every non-stable service state, and serialize standalone mutations with setup lifecycle operations. Keep the operator result contract explicit without projecting diagnostic snapshots as authoritative blockers.
Generated-by: Codex
Give intentional Host retirement a process outcome that the service manager treats as successful without restarting. Require every reported service PID to pass exact-Host preparation, and make active-work refusal unsuccessful to shell and JSON callers.
Generated-by: Codex
Hold the exact State Root writer while stopping a transitional managed service that has not published a PID. This prevents a systemd restart from admitting durable work between the status snapshot and stop, while failing closed if another Host wins the writer first.
Generated-by: Codex

@Astro-HanAstro-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.

Independent review of 0768ef9bbe526ce09e0d949de1c346dfb65f24d6, from two lines that did not share a starting point or read each other's findings.

No P0–P2. One [P3]. Exact-head checks: test is completed / success on that SHA. audit and package did not run on this head — recorded as not-run, not as green.

The retirement chain holds

Identity binding is triple-checked on the pid-known path: service status pid, then queryHostDiagnostics pid, then the pid returned by host.upgrade.prepare, with expectedHostEpoch anchoring the operation to one exact Host. Blocking is decided inside the Host by #hasUpgradeBlockingActivity() rather than projected from the CLI's diagnostics snapshot, and active_tasks is returned to the caller rather than acted on. After the drain gate is set, both the handshake admission read and begin-operation refuse new work, so a task starting just after the check cannot slip past the fence.

--allow-interrupt-active-tasks is a per-call boolean. It is not persisted to service config, does not change later defaults, and the operation is absent from REMOTE_OWNER_OPERATION_GRANTS — only local-owner IPC can invoke it. Its scope is broad (all blocking work on that exact Host) but explicit, which matches forced-retirement semantics.

Restart suppression is closed rather than assumed: shutdownReason='retirement' drives both exit paths to code 3, and the unit sets SuccessExitStatus=3 with RestartPreventExitStatus=3 against Restart=always. Note that a manual systemctl start remains possible; that is an operator action, not a service-manager auto-restart, and nothing here claims otherwise.

[P3] The pid-known path does not re-verify identity across stop

packages/cli/src/runtime-host-service-manager.ts acquires rootFence only in the starting/pid-less branch. The pid-known path calls backend.stop() with no fence held, and afterwards checks only that the service reached stopped with pid === null, then that the root was released.

If the old Host crashes during drain with a code other than 3, Restart=always can bring up a successor that takes the State Root writer and admits durable work before our stop lands. That stop then terminates the successor, the stopped-state assertion passes, the root is genuinely released — and the call still reports {kind: 'retired', hostEpoch, pid} carrying the original identity. A generation change happened inside the window that was supposed to end in retirement, the successor's writes remain, and the success result names a Host that is not the one that was stopped.

No data is lost — writer exclusivity is never violated — and the window requires a crash plus a restart to land between prepare and stop, so this is a narrow timing path rather than anything an attacker steers. Hence P3.

The minimal fix is to hold the root fence across stop on the pid-known path as well, or to re-read identity evidence after stop (restart counter, or reconnect and compare epoch) before reporting retired.

Boundaries

The service-management authority is split cleanly — Host owns admission and drain, the CLI owns managed PID, stop, and writer observation, systemd owns restart policy — and package staging, replacement startup, and update policy stay outside Runtime Host, as the description states. No duplicated retirement authority was found across the 14 files.

Limitations

Both reviewers were on Linux and could not exercise real systemd unit behaviour; the SuccessExitStatus / RestartPreventExitStatus link rests on source reading plus unit-rendering assertions, not on observed systemd action. Local runs: service-manager 21/21 and setup green; host-kernel 57/61, where the four failures reproduce identically on pristine main (missing Electron libatk in this environment) and are not attributed to this PR. The author's own full runtime/agent/desktop suites and the disposable systemd end-to-end run were not independently reproduced.


AI-assisted review, two independent lines. Under CONTRIBUTING.md §Review this does not replace the required independent human review.

@M4n5ter
M4n5ter merged commit 7f68af7 into mainAug 23, 2026
1 of 2 checks passed
@M4n5ter
M4n5ter deleted the feat/runtime-host-managed-retirement branch August 23, 2026 08:58

@Astro-HanAstro-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.

Two additions from a third reviewer, after the approval above. Neither changes the P3 severity; both make it more actionable.

A better fix direction than the one I suggested. I proposed either holding the fence across stopor re-reading identity after stop. The second option is weak: by the time you re-read, the successor has already been stopped and its writes cannot be undone, so you learn about the generation change without being able to prevent it. The right shape is to acquire the root fence afterprepare has released the old Host and hold it throughbackend.stop() until the service is stably stopped — failing closed if a successor wins the lock first, rather than stopping it as though it were the original Host. The starting/pid-less branch already has exactly this shape; the pid-known branch should converge on the same authority.

There is also a narrow semantic consequence worth noting: work admitted by the successor inside that window is invisible to the old Host's active-work check, so allowInterruptActiveTasks=false can be satisfied while durable work is nonetheless interrupted.

The retired wording overstates what the unit does.runtime-host-systemd-service.ts:218 sets RestartPreventExitStatus=3, which suppresses Restart=always for that exit only. The unit remains installed and enabled with WantedBy=default.target at :227. So besides a manual systemctl start, a future user-manager or default-target activation will also start it. The current human-facing "service is retired" reads as durable disablement.

Suggested wording: stopped for replacement; immediate restart suppressed; unit remains installed and enabled, and may be started explicitly or by a future target activation.

For the record on what verifyRuntimeHostRootReleased() proves: tryAcquireInteractiveRootOwner() takes an OS-level non-blocking exclusive lock on the exact root's owner.lock and revalidates the lock artifact against root identity, so a successful acquisition is real observation — at that instant, the old Host holds no writer and neither does anyone else. It is a snapshot of lock vacancy, not evidence of an identity-contiguous handoff, and it stops proving anything once owner.close() runs.

Sign up for freeto 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.

2 participants

@M4n5ter@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(runtime-host): add managed Host retirement - #3557

Merged
M4n5ter merged 4 commits into
mainfrom
feat/runtime-host-managed-retirement
Aug 23, 2026
Merged

feat(runtime-host): add managed Host retirement#3557
M4n5ter merged 4 commits into
mainfrom
feat/runtime-host-managed-retirement

Conversation

@M4n5ter

@M4n5terM4n5ter commented Aug 23, 2026

Copy link
Copy Markdown
Member
English

Summary

Adds the local-operator retirement boundary required before a managed Runtime Host service can be replaced. The CLI now binds the exact live Host to the managed service process, reports when active work blocks retirement unless interruption was explicitly authorized, stops every non-stable service state, prevents intentional retirement from being restarted by the service manager, and verifies that the State Root writer was released. Standalone service mutations are serialized with setup and deployment lifecycle operations.

This reuses the existing exact-Epoch Host prepare contract. Package staging, version switching, replacement startup, and update policy remain outside Runtime Host authority.

Refs #3231
Refs #3228

Verification

  • npm --workspace @maka/runtime-host test — 1,086 tests passed
  • npm --workspace maka-agent test — 393 tests passed
  • Desktop Runtime Host management tests — 4 tests passed
  • npm run lint
  • npm run format:check
  • npm run typecheck
  • Disposable Linux systemd user service: install, target-mismatch rejection, retire, idempotent retry, restart, status, uninstall, and cleanup all passed

AI use

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

Tool(s) and scope: OpenAI Codex contributed substantively to the implementation and tests under the contributor's direction. The human contributor owns review and submission.

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
简体中文

概要

增加 managed Runtime Host service 在替换前所需的 local-operator 退场边界。CLI 现在会将精确的 live Host 与 managed service 进程绑定;若未明确授权中断,则在存在 active work 时阻止退场;停止所有非稳定状态的 service,阻止 service manager 重启主动退场的进程,并验证 State Root writer 已释放。独立执行的 service mutation 也会与 setup 和 deployment lifecycle 操作串行化。

该实现复用现有的 exact-Epoch Host prepare contract。Package staging、版本切换、replacement 启动和更新策略仍不属于 Runtime Host authority。

关联 #3231
关联 #3228

验证

  • npm --workspace @maka/runtime-host test — 1,086 项测试通过
  • npm --workspace maka-agent test — 393 项测试通过
  • Desktop Runtime Host management — 4 项测试通过
  • npm run lint
  • npm run format:check
  • npm run typecheck
  • 一次性 Linux systemd user service:install、target mismatch 拒绝、retire、幂等重试、restart、status、uninstall 与清理均通过

AI 使用

  • 无生成式工具实质参与
  • 生成式工具有实质参与

工具与范围:OpenAI Codex 在贡献者指导下实质参与实现与测试。人工贡献者负责审查与提交。

检查清单

  • 测试覆盖该变更,且缺少该变更时会失败
  • lint、format、typecheck 与受影响测试套件均已在本地通过

本 PR 是否改变行为?

  • 是 — 已在概要中说明

@M4n5ter
M4n5terforce-pushed the feat/runtime-host-managed-retirement branch 2 times, most recently from 3c62651 to d7fb7bdCompareAugust 23, 2026 07:28
Managed updates need a local operator path that fences admission before the service releases its State Root. Reuse the existing exact-Epoch Host prepare contract, then let the CLI service manager stop the unit and verify writer release.
The new retire action remains outside ordinary remote Client authority and returns bounded active-work or completed-retirement facts for later update orchestration.
Generated-by: Codex
Bind retirement to the managed service PID, stop every non-stable service state, and serialize standalone mutations with setup lifecycle operations. Keep the operator result contract explicit without projecting diagnostic snapshots as authoritative blockers.
Generated-by: Codex
Give intentional Host retirement a process outcome that the service manager treats as successful without restarting. Require every reported service PID to pass exact-Host preparation, and make active-work refusal unsuccessful to shell and JSON callers.
Generated-by: Codex
Hold the exact State Root writer while stopping a transitional managed service that has not published a PID. This prevents a systemd restart from admitting durable work between the status snapshot and stop, while failing closed if another Host wins the writer first.
Generated-by: Codex

@Astro-HanAstro-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.

Independent review of 0768ef9bbe526ce09e0d949de1c346dfb65f24d6, from two lines that did not share a starting point or read each other's findings.

No P0–P2. One [P3]. Exact-head checks: test is completed / success on that SHA. audit and package did not run on this head — recorded as not-run, not as green.

The retirement chain holds

Identity binding is triple-checked on the pid-known path: service status pid, then queryHostDiagnostics pid, then the pid returned by host.upgrade.prepare, with expectedHostEpoch anchoring the operation to one exact Host. Blocking is decided inside the Host by #hasUpgradeBlockingActivity() rather than projected from the CLI's diagnostics snapshot, and active_tasks is returned to the caller rather than acted on. After the drain gate is set, both the handshake admission read and begin-operation refuse new work, so a task starting just after the check cannot slip past the fence.

--allow-interrupt-active-tasks is a per-call boolean. It is not persisted to service config, does not change later defaults, and the operation is absent from REMOTE_OWNER_OPERATION_GRANTS — only local-owner IPC can invoke it. Its scope is broad (all blocking work on that exact Host) but explicit, which matches forced-retirement semantics.

Restart suppression is closed rather than assumed: shutdownReason='retirement' drives both exit paths to code 3, and the unit sets SuccessExitStatus=3 with RestartPreventExitStatus=3 against Restart=always. Note that a manual systemctl start remains possible; that is an operator action, not a service-manager auto-restart, and nothing here claims otherwise.

[P3] The pid-known path does not re-verify identity across stop

packages/cli/src/runtime-host-service-manager.ts acquires rootFence only in the starting/pid-less branch. The pid-known path calls backend.stop() with no fence held, and afterwards checks only that the service reached stopped with pid === null, then that the root was released.

If the old Host crashes during drain with a code other than 3, Restart=always can bring up a successor that takes the State Root writer and admits durable work before our stop lands. That stop then terminates the successor, the stopped-state assertion passes, the root is genuinely released — and the call still reports {kind: 'retired', hostEpoch, pid} carrying the original identity. A generation change happened inside the window that was supposed to end in retirement, the successor's writes remain, and the success result names a Host that is not the one that was stopped.

No data is lost — writer exclusivity is never violated — and the window requires a crash plus a restart to land between prepare and stop, so this is a narrow timing path rather than anything an attacker steers. Hence P3.

The minimal fix is to hold the root fence across stop on the pid-known path as well, or to re-read identity evidence after stop (restart counter, or reconnect and compare epoch) before reporting retired.

Boundaries

The service-management authority is split cleanly — Host owns admission and drain, the CLI owns managed PID, stop, and writer observation, systemd owns restart policy — and package staging, replacement startup, and update policy stay outside Runtime Host, as the description states. No duplicated retirement authority was found across the 14 files.

Limitations

Both reviewers were on Linux and could not exercise real systemd unit behaviour; the SuccessExitStatus / RestartPreventExitStatus link rests on source reading plus unit-rendering assertions, not on observed systemd action. Local runs: service-manager 21/21 and setup green; host-kernel 57/61, where the four failures reproduce identically on pristine main (missing Electron libatk in this environment) and are not attributed to this PR. The author's own full runtime/agent/desktop suites and the disposable systemd end-to-end run were not independently reproduced.


AI-assisted review, two independent lines. Under CONTRIBUTING.md §Review this does not replace the required independent human review.

@M4n5ter
M4n5ter merged commit 7f68af7 into mainAug 23, 2026
1 of 2 checks passed
@M4n5ter
M4n5ter deleted the feat/runtime-host-managed-retirement branch August 23, 2026 08:58

@Astro-HanAstro-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.

Two additions from a third reviewer, after the approval above. Neither changes the P3 severity; both make it more actionable.

A better fix direction than the one I suggested. I proposed either holding the fence across stopor re-reading identity after stop. The second option is weak: by the time you re-read, the successor has already been stopped and its writes cannot be undone, so you learn about the generation change without being able to prevent it. The right shape is to acquire the root fence afterprepare has released the old Host and hold it throughbackend.stop() until the service is stably stopped — failing closed if a successor wins the lock first, rather than stopping it as though it were the original Host. The starting/pid-less branch already has exactly this shape; the pid-known branch should converge on the same authority.

There is also a narrow semantic consequence worth noting: work admitted by the successor inside that window is invisible to the old Host's active-work check, so allowInterruptActiveTasks=false can be satisfied while durable work is nonetheless interrupted.

The retired wording overstates what the unit does.runtime-host-systemd-service.ts:218 sets RestartPreventExitStatus=3, which suppresses Restart=always for that exit only. The unit remains installed and enabled with WantedBy=default.target at :227. So besides a manual systemctl start, a future user-manager or default-target activation will also start it. The current human-facing "service is retired" reads as durable disablement.

Suggested wording: stopped for replacement; immediate restart suppressed; unit remains installed and enabled, and may be started explicitly or by a future target activation.

For the record on what verifyRuntimeHostRootReleased() proves: tryAcquireInteractiveRootOwner() takes an OS-level non-blocking exclusive lock on the exact root's owner.lock and revalidates the lock artifact against root identity, so a successful acquisition is real observation — at that instant, the old Host holds no writer and neither does anyone else. It is a snapshot of lock vacancy, not evidence of an identity-contiguous handoff, and it stops proving anything once owner.close() runs.

Sign up for freeto 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.

2 participants

@M4n5ter@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(runtime-host): add managed Host retirement - #3557

Merged
M4n5ter merged 4 commits into
mainfrom
feat/runtime-host-managed-retirement
Aug 23, 2026
Merged

feat(runtime-host): add managed Host retirement#3557
M4n5ter merged 4 commits into
mainfrom
feat/runtime-host-managed-retirement

Conversation

@M4n5ter

@M4n5terM4n5ter commented Aug 23, 2026

Copy link
Copy Markdown
Member
English

Summary

Adds the local-operator retirement boundary required before a managed Runtime Host service can be replaced. The CLI now binds the exact live Host to the managed service process, reports when active work blocks retirement unless interruption was explicitly authorized, stops every non-stable service state, prevents intentional retirement from being restarted by the service manager, and verifies that the State Root writer was released. Standalone service mutations are serialized with setup and deployment lifecycle operations.

This reuses the existing exact-Epoch Host prepare contract. Package staging, version switching, replacement startup, and update policy remain outside Runtime Host authority.

Refs #3231
Refs #3228

Verification

  • npm --workspace @maka/runtime-host test — 1,086 tests passed
  • npm --workspace maka-agent test — 393 tests passed
  • Desktop Runtime Host management tests — 4 tests passed
  • npm run lint
  • npm run format:check
  • npm run typecheck
  • Disposable Linux systemd user service: install, target-mismatch rejection, retire, idempotent retry, restart, status, uninstall, and cleanup all passed

AI use

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

Tool(s) and scope: OpenAI Codex contributed substantively to the implementation and tests under the contributor's direction. The human contributor owns review and submission.

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
简体中文

概要

增加 managed Runtime Host service 在替换前所需的 local-operator 退场边界。CLI 现在会将精确的 live Host 与 managed service 进程绑定;若未明确授权中断,则在存在 active work 时阻止退场;停止所有非稳定状态的 service,阻止 service manager 重启主动退场的进程,并验证 State Root writer 已释放。独立执行的 service mutation 也会与 setup 和 deployment lifecycle 操作串行化。

该实现复用现有的 exact-Epoch Host prepare contract。Package staging、版本切换、replacement 启动和更新策略仍不属于 Runtime Host authority。

关联 #3231
关联 #3228

验证

  • npm --workspace @maka/runtime-host test — 1,086 项测试通过
  • npm --workspace maka-agent test — 393 项测试通过
  • Desktop Runtime Host management — 4 项测试通过
  • npm run lint
  • npm run format:check
  • npm run typecheck
  • 一次性 Linux systemd user service:install、target mismatch 拒绝、retire、幂等重试、restart、status、uninstall 与清理均通过

AI 使用

  • 无生成式工具实质参与
  • 生成式工具有实质参与

工具与范围:OpenAI Codex 在贡献者指导下实质参与实现与测试。人工贡献者负责审查与提交。

检查清单

  • 测试覆盖该变更,且缺少该变更时会失败
  • lint、format、typecheck 与受影响测试套件均已在本地通过

本 PR 是否改变行为?

  • 是 — 已在概要中说明

@M4n5ter
M4n5terforce-pushed the feat/runtime-host-managed-retirement branch 2 times, most recently from 3c62651 to d7fb7bdCompareAugust 23, 2026 07:28
Managed updates need a local operator path that fences admission before the service releases its State Root. Reuse the existing exact-Epoch Host prepare contract, then let the CLI service manager stop the unit and verify writer release.
The new retire action remains outside ordinary remote Client authority and returns bounded active-work or completed-retirement facts for later update orchestration.
Generated-by: Codex
Bind retirement to the managed service PID, stop every non-stable service state, and serialize standalone mutations with setup lifecycle operations. Keep the operator result contract explicit without projecting diagnostic snapshots as authoritative blockers.
Generated-by: Codex
Give intentional Host retirement a process outcome that the service manager treats as successful without restarting. Require every reported service PID to pass exact-Host preparation, and make active-work refusal unsuccessful to shell and JSON callers.
Generated-by: Codex
Hold the exact State Root writer while stopping a transitional managed service that has not published a PID. This prevents a systemd restart from admitting durable work between the status snapshot and stop, while failing closed if another Host wins the writer first.
Generated-by: Codex

@Astro-HanAstro-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.

Independent review of 0768ef9bbe526ce09e0d949de1c346dfb65f24d6, from two lines that did not share a starting point or read each other's findings.

No P0–P2. One [P3]. Exact-head checks: test is completed / success on that SHA. audit and package did not run on this head — recorded as not-run, not as green.

The retirement chain holds

Identity binding is triple-checked on the pid-known path: service status pid, then queryHostDiagnostics pid, then the pid returned by host.upgrade.prepare, with expectedHostEpoch anchoring the operation to one exact Host. Blocking is decided inside the Host by #hasUpgradeBlockingActivity() rather than projected from the CLI's diagnostics snapshot, and active_tasks is returned to the caller rather than acted on. After the drain gate is set, both the handshake admission read and begin-operation refuse new work, so a task starting just after the check cannot slip past the fence.

--allow-interrupt-active-tasks is a per-call boolean. It is not persisted to service config, does not change later defaults, and the operation is absent from REMOTE_OWNER_OPERATION_GRANTS — only local-owner IPC can invoke it. Its scope is broad (all blocking work on that exact Host) but explicit, which matches forced-retirement semantics.

Restart suppression is closed rather than assumed: shutdownReason='retirement' drives both exit paths to code 3, and the unit sets SuccessExitStatus=3 with RestartPreventExitStatus=3 against Restart=always. Note that a manual systemctl start remains possible; that is an operator action, not a service-manager auto-restart, and nothing here claims otherwise.

[P3] The pid-known path does not re-verify identity across stop

packages/cli/src/runtime-host-service-manager.ts acquires rootFence only in the starting/pid-less branch. The pid-known path calls backend.stop() with no fence held, and afterwards checks only that the service reached stopped with pid === null, then that the root was released.

If the old Host crashes during drain with a code other than 3, Restart=always can bring up a successor that takes the State Root writer and admits durable work before our stop lands. That stop then terminates the successor, the stopped-state assertion passes, the root is genuinely released — and the call still reports {kind: 'retired', hostEpoch, pid} carrying the original identity. A generation change happened inside the window that was supposed to end in retirement, the successor's writes remain, and the success result names a Host that is not the one that was stopped.

No data is lost — writer exclusivity is never violated — and the window requires a crash plus a restart to land between prepare and stop, so this is a narrow timing path rather than anything an attacker steers. Hence P3.

The minimal fix is to hold the root fence across stop on the pid-known path as well, or to re-read identity evidence after stop (restart counter, or reconnect and compare epoch) before reporting retired.

Boundaries

The service-management authority is split cleanly — Host owns admission and drain, the CLI owns managed PID, stop, and writer observation, systemd owns restart policy — and package staging, replacement startup, and update policy stay outside Runtime Host, as the description states. No duplicated retirement authority was found across the 14 files.

Limitations

Both reviewers were on Linux and could not exercise real systemd unit behaviour; the SuccessExitStatus / RestartPreventExitStatus link rests on source reading plus unit-rendering assertions, not on observed systemd action. Local runs: service-manager 21/21 and setup green; host-kernel 57/61, where the four failures reproduce identically on pristine main (missing Electron libatk in this environment) and are not attributed to this PR. The author's own full runtime/agent/desktop suites and the disposable systemd end-to-end run were not independently reproduced.


AI-assisted review, two independent lines. Under CONTRIBUTING.md §Review this does not replace the required independent human review.

@M4n5ter
M4n5ter merged commit 7f68af7 into mainAug 23, 2026
1 of 2 checks passed
@M4n5ter
M4n5ter deleted the feat/runtime-host-managed-retirement branch August 23, 2026 08:58

@Astro-HanAstro-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.

Two additions from a third reviewer, after the approval above. Neither changes the P3 severity; both make it more actionable.

A better fix direction than the one I suggested. I proposed either holding the fence across stopor re-reading identity after stop. The second option is weak: by the time you re-read, the successor has already been stopped and its writes cannot be undone, so you learn about the generation change without being able to prevent it. The right shape is to acquire the root fence afterprepare has released the old Host and hold it throughbackend.stop() until the service is stably stopped — failing closed if a successor wins the lock first, rather than stopping it as though it were the original Host. The starting/pid-less branch already has exactly this shape; the pid-known branch should converge on the same authority.

There is also a narrow semantic consequence worth noting: work admitted by the successor inside that window is invisible to the old Host's active-work check, so allowInterruptActiveTasks=false can be satisfied while durable work is nonetheless interrupted.

The retired wording overstates what the unit does.runtime-host-systemd-service.ts:218 sets RestartPreventExitStatus=3, which suppresses Restart=always for that exit only. The unit remains installed and enabled with WantedBy=default.target at :227. So besides a manual systemctl start, a future user-manager or default-target activation will also start it. The current human-facing "service is retired" reads as durable disablement.

Suggested wording: stopped for replacement; immediate restart suppressed; unit remains installed and enabled, and may be started explicitly or by a future target activation.

For the record on what verifyRuntimeHostRootReleased() proves: tryAcquireInteractiveRootOwner() takes an OS-level non-blocking exclusive lock on the exact root's owner.lock and revalidates the lock artifact against root identity, so a successful acquisition is real observation — at that instant, the old Host holds no writer and neither does anyone else. It is a snapshot of lock vacancy, not evidence of an identity-contiguous handoff, and it stops proving anything once owner.close() runs.

Sign up for freeto 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.

2 participants

@M4n5ter@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(runtime-host): add managed Host retirement - #3557

Merged
M4n5ter merged 4 commits into
mainfrom
feat/runtime-host-managed-retirement
Aug 23, 2026
Merged

feat(runtime-host): add managed Host retirement#3557
M4n5ter merged 4 commits into
mainfrom
feat/runtime-host-managed-retirement

Conversation

@M4n5ter

@M4n5terM4n5ter commented Aug 23, 2026

Copy link
Copy Markdown
Member
English

Summary

Adds the local-operator retirement boundary required before a managed Runtime Host service can be replaced. The CLI now binds the exact live Host to the managed service process, reports when active work blocks retirement unless interruption was explicitly authorized, stops every non-stable service state, prevents intentional retirement from being restarted by the service manager, and verifies that the State Root writer was released. Standalone service mutations are serialized with setup and deployment lifecycle operations.

This reuses the existing exact-Epoch Host prepare contract. Package staging, version switching, replacement startup, and update policy remain outside Runtime Host authority.

Refs #3231
Refs #3228

Verification

  • npm --workspace @maka/runtime-host test — 1,086 tests passed
  • npm --workspace maka-agent test — 393 tests passed
  • Desktop Runtime Host management tests — 4 tests passed
  • npm run lint
  • npm run format:check
  • npm run typecheck
  • Disposable Linux systemd user service: install, target-mismatch rejection, retire, idempotent retry, restart, status, uninstall, and cleanup all passed

AI use

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

Tool(s) and scope: OpenAI Codex contributed substantively to the implementation and tests under the contributor's direction. The human contributor owns review and submission.

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
简体中文

概要

增加 managed Runtime Host service 在替换前所需的 local-operator 退场边界。CLI 现在会将精确的 live Host 与 managed service 进程绑定;若未明确授权中断,则在存在 active work 时阻止退场;停止所有非稳定状态的 service,阻止 service manager 重启主动退场的进程,并验证 State Root writer 已释放。独立执行的 service mutation 也会与 setup 和 deployment lifecycle 操作串行化。

该实现复用现有的 exact-Epoch Host prepare contract。Package staging、版本切换、replacement 启动和更新策略仍不属于 Runtime Host authority。

关联 #3231
关联 #3228

验证

  • npm --workspace @maka/runtime-host test — 1,086 项测试通过
  • npm --workspace maka-agent test — 393 项测试通过
  • Desktop Runtime Host management — 4 项测试通过
  • npm run lint
  • npm run format:check
  • npm run typecheck
  • 一次性 Linux systemd user service:install、target mismatch 拒绝、retire、幂等重试、restart、status、uninstall 与清理均通过

AI 使用

  • 无生成式工具实质参与
  • 生成式工具有实质参与

工具与范围:OpenAI Codex 在贡献者指导下实质参与实现与测试。人工贡献者负责审查与提交。

检查清单

  • 测试覆盖该变更,且缺少该变更时会失败
  • lint、format、typecheck 与受影响测试套件均已在本地通过

本 PR 是否改变行为?

  • 是 — 已在概要中说明

@M4n5ter
M4n5terforce-pushed the feat/runtime-host-managed-retirement branch 2 times, most recently from 3c62651 to d7fb7bdCompareAugust 23, 2026 07:28
Managed updates need a local operator path that fences admission before the service releases its State Root. Reuse the existing exact-Epoch Host prepare contract, then let the CLI service manager stop the unit and verify writer release.
The new retire action remains outside ordinary remote Client authority and returns bounded active-work or completed-retirement facts for later update orchestration.
Generated-by: Codex
Bind retirement to the managed service PID, stop every non-stable service state, and serialize standalone mutations with setup lifecycle operations. Keep the operator result contract explicit without projecting diagnostic snapshots as authoritative blockers.
Generated-by: Codex
Give intentional Host retirement a process outcome that the service manager treats as successful without restarting. Require every reported service PID to pass exact-Host preparation, and make active-work refusal unsuccessful to shell and JSON callers.
Generated-by: Codex
Hold the exact State Root writer while stopping a transitional managed service that has not published a PID. This prevents a systemd restart from admitting durable work between the status snapshot and stop, while failing closed if another Host wins the writer first.
Generated-by: Codex

@Astro-HanAstro-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.

Independent review of 0768ef9bbe526ce09e0d949de1c346dfb65f24d6, from two lines that did not share a starting point or read each other's findings.

No P0–P2. One [P3]. Exact-head checks: test is completed / success on that SHA. audit and package did not run on this head — recorded as not-run, not as green.

The retirement chain holds

Identity binding is triple-checked on the pid-known path: service status pid, then queryHostDiagnostics pid, then the pid returned by host.upgrade.prepare, with expectedHostEpoch anchoring the operation to one exact Host. Blocking is decided inside the Host by #hasUpgradeBlockingActivity() rather than projected from the CLI's diagnostics snapshot, and active_tasks is returned to the caller rather than acted on. After the drain gate is set, both the handshake admission read and begin-operation refuse new work, so a task starting just after the check cannot slip past the fence.

--allow-interrupt-active-tasks is a per-call boolean. It is not persisted to service config, does not change later defaults, and the operation is absent from REMOTE_OWNER_OPERATION_GRANTS — only local-owner IPC can invoke it. Its scope is broad (all blocking work on that exact Host) but explicit, which matches forced-retirement semantics.

Restart suppression is closed rather than assumed: shutdownReason='retirement' drives both exit paths to code 3, and the unit sets SuccessExitStatus=3 with RestartPreventExitStatus=3 against Restart=always. Note that a manual systemctl start remains possible; that is an operator action, not a service-manager auto-restart, and nothing here claims otherwise.

[P3] The pid-known path does not re-verify identity across stop

packages/cli/src/runtime-host-service-manager.ts acquires rootFence only in the starting/pid-less branch. The pid-known path calls backend.stop() with no fence held, and afterwards checks only that the service reached stopped with pid === null, then that the root was released.

If the old Host crashes during drain with a code other than 3, Restart=always can bring up a successor that takes the State Root writer and admits durable work before our stop lands. That stop then terminates the successor, the stopped-state assertion passes, the root is genuinely released — and the call still reports {kind: 'retired', hostEpoch, pid} carrying the original identity. A generation change happened inside the window that was supposed to end in retirement, the successor's writes remain, and the success result names a Host that is not the one that was stopped.

No data is lost — writer exclusivity is never violated — and the window requires a crash plus a restart to land between prepare and stop, so this is a narrow timing path rather than anything an attacker steers. Hence P3.

The minimal fix is to hold the root fence across stop on the pid-known path as well, or to re-read identity evidence after stop (restart counter, or reconnect and compare epoch) before reporting retired.

Boundaries

The service-management authority is split cleanly — Host owns admission and drain, the CLI owns managed PID, stop, and writer observation, systemd owns restart policy — and package staging, replacement startup, and update policy stay outside Runtime Host, as the description states. No duplicated retirement authority was found across the 14 files.

Limitations

Both reviewers were on Linux and could not exercise real systemd unit behaviour; the SuccessExitStatus / RestartPreventExitStatus link rests on source reading plus unit-rendering assertions, not on observed systemd action. Local runs: service-manager 21/21 and setup green; host-kernel 57/61, where the four failures reproduce identically on pristine main (missing Electron libatk in this environment) and are not attributed to this PR. The author's own full runtime/agent/desktop suites and the disposable systemd end-to-end run were not independently reproduced.


AI-assisted review, two independent lines. Under CONTRIBUTING.md §Review this does not replace the required independent human review.

@M4n5ter
M4n5ter merged commit 7f68af7 into mainAug 23, 2026
1 of 2 checks passed
@M4n5ter
M4n5ter deleted the feat/runtime-host-managed-retirement branch August 23, 2026 08:58

@Astro-HanAstro-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.

Two additions from a third reviewer, after the approval above. Neither changes the P3 severity; both make it more actionable.

A better fix direction than the one I suggested. I proposed either holding the fence across stopor re-reading identity after stop. The second option is weak: by the time you re-read, the successor has already been stopped and its writes cannot be undone, so you learn about the generation change without being able to prevent it. The right shape is to acquire the root fence afterprepare has released the old Host and hold it throughbackend.stop() until the service is stably stopped — failing closed if a successor wins the lock first, rather than stopping it as though it were the original Host. The starting/pid-less branch already has exactly this shape; the pid-known branch should converge on the same authority.

There is also a narrow semantic consequence worth noting: work admitted by the successor inside that window is invisible to the old Host's active-work check, so allowInterruptActiveTasks=false can be satisfied while durable work is nonetheless interrupted.

The retired wording overstates what the unit does.runtime-host-systemd-service.ts:218 sets RestartPreventExitStatus=3, which suppresses Restart=always for that exit only. The unit remains installed and enabled with WantedBy=default.target at :227. So besides a manual systemctl start, a future user-manager or default-target activation will also start it. The current human-facing "service is retired" reads as durable disablement.

Suggested wording: stopped for replacement; immediate restart suppressed; unit remains installed and enabled, and may be started explicitly or by a future target activation.

For the record on what verifyRuntimeHostRootReleased() proves: tryAcquireInteractiveRootOwner() takes an OS-level non-blocking exclusive lock on the exact root's owner.lock and revalidates the lock artifact against root identity, so a successful acquisition is real observation — at that instant, the old Host holds no writer and neither does anyone else. It is a snapshot of lock vacancy, not evidence of an identity-contiguous handoff, and it stops proving anything once owner.close() runs.

Sign up for freeto 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.

2 participants

@M4n5ter@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(runtime-host): add managed Host retirement - #3557

Merged
M4n5ter merged 4 commits into
mainfrom
feat/runtime-host-managed-retirement
Aug 23, 2026
Merged

feat(runtime-host): add managed Host retirement#3557
M4n5ter merged 4 commits into
mainfrom
feat/runtime-host-managed-retirement

Conversation

@M4n5ter

@M4n5terM4n5ter commented Aug 23, 2026

Copy link
Copy Markdown
Member
English

Summary

Adds the local-operator retirement boundary required before a managed Runtime Host service can be replaced. The CLI now binds the exact live Host to the managed service process, reports when active work blocks retirement unless interruption was explicitly authorized, stops every non-stable service state, prevents intentional retirement from being restarted by the service manager, and verifies that the State Root writer was released. Standalone service mutations are serialized with setup and deployment lifecycle operations.

This reuses the existing exact-Epoch Host prepare contract. Package staging, version switching, replacement startup, and update policy remain outside Runtime Host authority.

Refs #3231
Refs #3228

Verification

  • npm --workspace @maka/runtime-host test — 1,086 tests passed
  • npm --workspace maka-agent test — 393 tests passed
  • Desktop Runtime Host management tests — 4 tests passed
  • npm run lint
  • npm run format:check
  • npm run typecheck
  • Disposable Linux systemd user service: install, target-mismatch rejection, retire, idempotent retry, restart, status, uninstall, and cleanup all passed

AI use

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

Tool(s) and scope: OpenAI Codex contributed substantively to the implementation and tests under the contributor's direction. The human contributor owns review and submission.

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
简体中文

概要

增加 managed Runtime Host service 在替换前所需的 local-operator 退场边界。CLI 现在会将精确的 live Host 与 managed service 进程绑定;若未明确授权中断,则在存在 active work 时阻止退场;停止所有非稳定状态的 service,阻止 service manager 重启主动退场的进程,并验证 State Root writer 已释放。独立执行的 service mutation 也会与 setup 和 deployment lifecycle 操作串行化。

该实现复用现有的 exact-Epoch Host prepare contract。Package staging、版本切换、replacement 启动和更新策略仍不属于 Runtime Host authority。

关联 #3231
关联 #3228

验证

  • npm --workspace @maka/runtime-host test — 1,086 项测试通过
  • npm --workspace maka-agent test — 393 项测试通过
  • Desktop Runtime Host management — 4 项测试通过
  • npm run lint
  • npm run format:check
  • npm run typecheck
  • 一次性 Linux systemd user service:install、target mismatch 拒绝、retire、幂等重试、restart、status、uninstall 与清理均通过

AI 使用

  • 无生成式工具实质参与
  • 生成式工具有实质参与

工具与范围:OpenAI Codex 在贡献者指导下实质参与实现与测试。人工贡献者负责审查与提交。

检查清单

  • 测试覆盖该变更,且缺少该变更时会失败
  • lint、format、typecheck 与受影响测试套件均已在本地通过

本 PR 是否改变行为?

  • 是 — 已在概要中说明

@M4n5ter
M4n5terforce-pushed the feat/runtime-host-managed-retirement branch 2 times, most recently from 3c62651 to d7fb7bdCompareAugust 23, 2026 07:28
Managed updates need a local operator path that fences admission before the service releases its State Root. Reuse the existing exact-Epoch Host prepare contract, then let the CLI service manager stop the unit and verify writer release.
The new retire action remains outside ordinary remote Client authority and returns bounded active-work or completed-retirement facts for later update orchestration.
Generated-by: Codex
Bind retirement to the managed service PID, stop every non-stable service state, and serialize standalone mutations with setup lifecycle operations. Keep the operator result contract explicit without projecting diagnostic snapshots as authoritative blockers.
Generated-by: Codex
Give intentional Host retirement a process outcome that the service manager treats as successful without restarting. Require every reported service PID to pass exact-Host preparation, and make active-work refusal unsuccessful to shell and JSON callers.
Generated-by: Codex
Hold the exact State Root writer while stopping a transitional managed service that has not published a PID. This prevents a systemd restart from admitting durable work between the status snapshot and stop, while failing closed if another Host wins the writer first.
Generated-by: Codex

@Astro-HanAstro-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.

Independent review of 0768ef9bbe526ce09e0d949de1c346dfb65f24d6, from two lines that did not share a starting point or read each other's findings.

No P0–P2. One [P3]. Exact-head checks: test is completed / success on that SHA. audit and package did not run on this head — recorded as not-run, not as green.

The retirement chain holds

Identity binding is triple-checked on the pid-known path: service status pid, then queryHostDiagnostics pid, then the pid returned by host.upgrade.prepare, with expectedHostEpoch anchoring the operation to one exact Host. Blocking is decided inside the Host by #hasUpgradeBlockingActivity() rather than projected from the CLI's diagnostics snapshot, and active_tasks is returned to the caller rather than acted on. After the drain gate is set, both the handshake admission read and begin-operation refuse new work, so a task starting just after the check cannot slip past the fence.

--allow-interrupt-active-tasks is a per-call boolean. It is not persisted to service config, does not change later defaults, and the operation is absent from REMOTE_OWNER_OPERATION_GRANTS — only local-owner IPC can invoke it. Its scope is broad (all blocking work on that exact Host) but explicit, which matches forced-retirement semantics.

Restart suppression is closed rather than assumed: shutdownReason='retirement' drives both exit paths to code 3, and the unit sets SuccessExitStatus=3 with RestartPreventExitStatus=3 against Restart=always. Note that a manual systemctl start remains possible; that is an operator action, not a service-manager auto-restart, and nothing here claims otherwise.

[P3] The pid-known path does not re-verify identity across stop

packages/cli/src/runtime-host-service-manager.ts acquires rootFence only in the starting/pid-less branch. The pid-known path calls backend.stop() with no fence held, and afterwards checks only that the service reached stopped with pid === null, then that the root was released.

If the old Host crashes during drain with a code other than 3, Restart=always can bring up a successor that takes the State Root writer and admits durable work before our stop lands. That stop then terminates the successor, the stopped-state assertion passes, the root is genuinely released — and the call still reports {kind: 'retired', hostEpoch, pid} carrying the original identity. A generation change happened inside the window that was supposed to end in retirement, the successor's writes remain, and the success result names a Host that is not the one that was stopped.

No data is lost — writer exclusivity is never violated — and the window requires a crash plus a restart to land between prepare and stop, so this is a narrow timing path rather than anything an attacker steers. Hence P3.

The minimal fix is to hold the root fence across stop on the pid-known path as well, or to re-read identity evidence after stop (restart counter, or reconnect and compare epoch) before reporting retired.

Boundaries

The service-management authority is split cleanly — Host owns admission and drain, the CLI owns managed PID, stop, and writer observation, systemd owns restart policy — and package staging, replacement startup, and update policy stay outside Runtime Host, as the description states. No duplicated retirement authority was found across the 14 files.

Limitations

Both reviewers were on Linux and could not exercise real systemd unit behaviour; the SuccessExitStatus / RestartPreventExitStatus link rests on source reading plus unit-rendering assertions, not on observed systemd action. Local runs: service-manager 21/21 and setup green; host-kernel 57/61, where the four failures reproduce identically on pristine main (missing Electron libatk in this environment) and are not attributed to this PR. The author's own full runtime/agent/desktop suites and the disposable systemd end-to-end run were not independently reproduced.


AI-assisted review, two independent lines. Under CONTRIBUTING.md §Review this does not replace the required independent human review.

@M4n5ter
M4n5ter merged commit 7f68af7 into mainAug 23, 2026
1 of 2 checks passed
@M4n5ter
M4n5ter deleted the feat/runtime-host-managed-retirement branch August 23, 2026 08:58

@Astro-HanAstro-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.

Two additions from a third reviewer, after the approval above. Neither changes the P3 severity; both make it more actionable.

A better fix direction than the one I suggested. I proposed either holding the fence across stopor re-reading identity after stop. The second option is weak: by the time you re-read, the successor has already been stopped and its writes cannot be undone, so you learn about the generation change without being able to prevent it. The right shape is to acquire the root fence afterprepare has released the old Host and hold it throughbackend.stop() until the service is stably stopped — failing closed if a successor wins the lock first, rather than stopping it as though it were the original Host. The starting/pid-less branch already has exactly this shape; the pid-known branch should converge on the same authority.

There is also a narrow semantic consequence worth noting: work admitted by the successor inside that window is invisible to the old Host's active-work check, so allowInterruptActiveTasks=false can be satisfied while durable work is nonetheless interrupted.

The retired wording overstates what the unit does.runtime-host-systemd-service.ts:218 sets RestartPreventExitStatus=3, which suppresses Restart=always for that exit only. The unit remains installed and enabled with WantedBy=default.target at :227. So besides a manual systemctl start, a future user-manager or default-target activation will also start it. The current human-facing "service is retired" reads as durable disablement.

Suggested wording: stopped for replacement; immediate restart suppressed; unit remains installed and enabled, and may be started explicitly or by a future target activation.

For the record on what verifyRuntimeHostRootReleased() proves: tryAcquireInteractiveRootOwner() takes an OS-level non-blocking exclusive lock on the exact root's owner.lock and revalidates the lock artifact against root identity, so a successful acquisition is real observation — at that instant, the old Host holds no writer and neither does anyone else. It is a snapshot of lock vacancy, not evidence of an identity-contiguous handoff, and it stops proving anything once owner.close() runs.

Sign up for freeto 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.

2 participants

@M4n5ter@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(runtime-host): add managed Host retirement - #3557

Merged
M4n5ter merged 4 commits into
mainfrom
feat/runtime-host-managed-retirement
Aug 23, 2026
Merged

feat(runtime-host): add managed Host retirement#3557
M4n5ter merged 4 commits into
mainfrom
feat/runtime-host-managed-retirement

Conversation

@M4n5ter

@M4n5terM4n5ter commented Aug 23, 2026

Copy link
Copy Markdown
Member
English

Summary

Adds the local-operator retirement boundary required before a managed Runtime Host service can be replaced. The CLI now binds the exact live Host to the managed service process, reports when active work blocks retirement unless interruption was explicitly authorized, stops every non-stable service state, prevents intentional retirement from being restarted by the service manager, and verifies that the State Root writer was released. Standalone service mutations are serialized with setup and deployment lifecycle operations.

This reuses the existing exact-Epoch Host prepare contract. Package staging, version switching, replacement startup, and update policy remain outside Runtime Host authority.

Refs #3231
Refs #3228

Verification

  • npm --workspace @maka/runtime-host test — 1,086 tests passed
  • npm --workspace maka-agent test — 393 tests passed
  • Desktop Runtime Host management tests — 4 tests passed
  • npm run lint
  • npm run format:check
  • npm run typecheck
  • Disposable Linux systemd user service: install, target-mismatch rejection, retire, idempotent retry, restart, status, uninstall, and cleanup all passed

AI use

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

Tool(s) and scope: OpenAI Codex contributed substantively to the implementation and tests under the contributor's direction. The human contributor owns review and submission.

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
简体中文

概要

增加 managed Runtime Host service 在替换前所需的 local-operator 退场边界。CLI 现在会将精确的 live Host 与 managed service 进程绑定;若未明确授权中断,则在存在 active work 时阻止退场;停止所有非稳定状态的 service,阻止 service manager 重启主动退场的进程,并验证 State Root writer 已释放。独立执行的 service mutation 也会与 setup 和 deployment lifecycle 操作串行化。

该实现复用现有的 exact-Epoch Host prepare contract。Package staging、版本切换、replacement 启动和更新策略仍不属于 Runtime Host authority。

关联 #3231
关联 #3228

验证

  • npm --workspace @maka/runtime-host test — 1,086 项测试通过
  • npm --workspace maka-agent test — 393 项测试通过
  • Desktop Runtime Host management — 4 项测试通过
  • npm run lint
  • npm run format:check
  • npm run typecheck
  • 一次性 Linux systemd user service:install、target mismatch 拒绝、retire、幂等重试、restart、status、uninstall 与清理均通过

AI 使用

  • 无生成式工具实质参与
  • 生成式工具有实质参与

工具与范围:OpenAI Codex 在贡献者指导下实质参与实现与测试。人工贡献者负责审查与提交。

检查清单

  • 测试覆盖该变更,且缺少该变更时会失败
  • lint、format、typecheck 与受影响测试套件均已在本地通过

本 PR 是否改变行为?

  • 是 — 已在概要中说明

@M4n5ter
M4n5terforce-pushed the feat/runtime-host-managed-retirement branch 2 times, most recently from 3c62651 to d7fb7bdCompareAugust 23, 2026 07:28
Managed updates need a local operator path that fences admission before the service releases its State Root. Reuse the existing exact-Epoch Host prepare contract, then let the CLI service manager stop the unit and verify writer release.
The new retire action remains outside ordinary remote Client authority and returns bounded active-work or completed-retirement facts for later update orchestration.
Generated-by: Codex
Bind retirement to the managed service PID, stop every non-stable service state, and serialize standalone mutations with setup lifecycle operations. Keep the operator result contract explicit without projecting diagnostic snapshots as authoritative blockers.
Generated-by: Codex
Give intentional Host retirement a process outcome that the service manager treats as successful without restarting. Require every reported service PID to pass exact-Host preparation, and make active-work refusal unsuccessful to shell and JSON callers.
Generated-by: Codex
Hold the exact State Root writer while stopping a transitional managed service that has not published a PID. This prevents a systemd restart from admitting durable work between the status snapshot and stop, while failing closed if another Host wins the writer first.
Generated-by: Codex

@Astro-HanAstro-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.

Independent review of 0768ef9bbe526ce09e0d949de1c346dfb65f24d6, from two lines that did not share a starting point or read each other's findings.

No P0–P2. One [P3]. Exact-head checks: test is completed / success on that SHA. audit and package did not run on this head — recorded as not-run, not as green.

The retirement chain holds

Identity binding is triple-checked on the pid-known path: service status pid, then queryHostDiagnostics pid, then the pid returned by host.upgrade.prepare, with expectedHostEpoch anchoring the operation to one exact Host. Blocking is decided inside the Host by #hasUpgradeBlockingActivity() rather than projected from the CLI's diagnostics snapshot, and active_tasks is returned to the caller rather than acted on. After the drain gate is set, both the handshake admission read and begin-operation refuse new work, so a task starting just after the check cannot slip past the fence.

--allow-interrupt-active-tasks is a per-call boolean. It is not persisted to service config, does not change later defaults, and the operation is absent from REMOTE_OWNER_OPERATION_GRANTS — only local-owner IPC can invoke it. Its scope is broad (all blocking work on that exact Host) but explicit, which matches forced-retirement semantics.

Restart suppression is closed rather than assumed: shutdownReason='retirement' drives both exit paths to code 3, and the unit sets SuccessExitStatus=3 with RestartPreventExitStatus=3 against Restart=always. Note that a manual systemctl start remains possible; that is an operator action, not a service-manager auto-restart, and nothing here claims otherwise.

[P3] The pid-known path does not re-verify identity across stop

packages/cli/src/runtime-host-service-manager.ts acquires rootFence only in the starting/pid-less branch. The pid-known path calls backend.stop() with no fence held, and afterwards checks only that the service reached stopped with pid === null, then that the root was released.

If the old Host crashes during drain with a code other than 3, Restart=always can bring up a successor that takes the State Root writer and admits durable work before our stop lands. That stop then terminates the successor, the stopped-state assertion passes, the root is genuinely released — and the call still reports {kind: 'retired', hostEpoch, pid} carrying the original identity. A generation change happened inside the window that was supposed to end in retirement, the successor's writes remain, and the success result names a Host that is not the one that was stopped.

No data is lost — writer exclusivity is never violated — and the window requires a crash plus a restart to land between prepare and stop, so this is a narrow timing path rather than anything an attacker steers. Hence P3.

The minimal fix is to hold the root fence across stop on the pid-known path as well, or to re-read identity evidence after stop (restart counter, or reconnect and compare epoch) before reporting retired.

Boundaries

The service-management authority is split cleanly — Host owns admission and drain, the CLI owns managed PID, stop, and writer observation, systemd owns restart policy — and package staging, replacement startup, and update policy stay outside Runtime Host, as the description states. No duplicated retirement authority was found across the 14 files.

Limitations

Both reviewers were on Linux and could not exercise real systemd unit behaviour; the SuccessExitStatus / RestartPreventExitStatus link rests on source reading plus unit-rendering assertions, not on observed systemd action. Local runs: service-manager 21/21 and setup green; host-kernel 57/61, where the four failures reproduce identically on pristine main (missing Electron libatk in this environment) and are not attributed to this PR. The author's own full runtime/agent/desktop suites and the disposable systemd end-to-end run were not independently reproduced.


AI-assisted review, two independent lines. Under CONTRIBUTING.md §Review this does not replace the required independent human review.

@M4n5ter
M4n5ter merged commit 7f68af7 into mainAug 23, 2026
1 of 2 checks passed
@M4n5ter
M4n5ter deleted the feat/runtime-host-managed-retirement branch August 23, 2026 08:58

@Astro-HanAstro-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.

Two additions from a third reviewer, after the approval above. Neither changes the P3 severity; both make it more actionable.

A better fix direction than the one I suggested. I proposed either holding the fence across stopor re-reading identity after stop. The second option is weak: by the time you re-read, the successor has already been stopped and its writes cannot be undone, so you learn about the generation change without being able to prevent it. The right shape is to acquire the root fence afterprepare has released the old Host and hold it throughbackend.stop() until the service is stably stopped — failing closed if a successor wins the lock first, rather than stopping it as though it were the original Host. The starting/pid-less branch already has exactly this shape; the pid-known branch should converge on the same authority.

There is also a narrow semantic consequence worth noting: work admitted by the successor inside that window is invisible to the old Host's active-work check, so allowInterruptActiveTasks=false can be satisfied while durable work is nonetheless interrupted.

The retired wording overstates what the unit does.runtime-host-systemd-service.ts:218 sets RestartPreventExitStatus=3, which suppresses Restart=always for that exit only. The unit remains installed and enabled with WantedBy=default.target at :227. So besides a manual systemctl start, a future user-manager or default-target activation will also start it. The current human-facing "service is retired" reads as durable disablement.

Suggested wording: stopped for replacement; immediate restart suppressed; unit remains installed and enabled, and may be started explicitly or by a future target activation.

For the record on what verifyRuntimeHostRootReleased() proves: tryAcquireInteractiveRootOwner() takes an OS-level non-blocking exclusive lock on the exact root's owner.lock and revalidates the lock artifact against root identity, so a successful acquisition is real observation — at that instant, the old Host holds no writer and neither does anyone else. It is a snapshot of lock vacancy, not evidence of an identity-contiguous handoff, and it stops proving anything once owner.close() runs.

Sign up for freeto 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.

2 participants

@M4n5ter@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(runtime-host): add managed Host retirement - #3557

Merged
M4n5ter merged 4 commits into
mainfrom
feat/runtime-host-managed-retirement
Aug 23, 2026
Merged

feat(runtime-host): add managed Host retirement#3557
M4n5ter merged 4 commits into
mainfrom
feat/runtime-host-managed-retirement

Conversation

@M4n5ter

@M4n5terM4n5ter commented Aug 23, 2026

Copy link
Copy Markdown
Member
English

Summary

Adds the local-operator retirement boundary required before a managed Runtime Host service can be replaced. The CLI now binds the exact live Host to the managed service process, reports when active work blocks retirement unless interruption was explicitly authorized, stops every non-stable service state, prevents intentional retirement from being restarted by the service manager, and verifies that the State Root writer was released. Standalone service mutations are serialized with setup and deployment lifecycle operations.

This reuses the existing exact-Epoch Host prepare contract. Package staging, version switching, replacement startup, and update policy remain outside Runtime Host authority.

Refs #3231
Refs #3228

Verification

  • npm --workspace @maka/runtime-host test — 1,086 tests passed
  • npm --workspace maka-agent test — 393 tests passed
  • Desktop Runtime Host management tests — 4 tests passed
  • npm run lint
  • npm run format:check
  • npm run typecheck
  • Disposable Linux systemd user service: install, target-mismatch rejection, retire, idempotent retry, restart, status, uninstall, and cleanup all passed

AI use

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

Tool(s) and scope: OpenAI Codex contributed substantively to the implementation and tests under the contributor's direction. The human contributor owns review and submission.

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
简体中文

概要

增加 managed Runtime Host service 在替换前所需的 local-operator 退场边界。CLI 现在会将精确的 live Host 与 managed service 进程绑定;若未明确授权中断,则在存在 active work 时阻止退场;停止所有非稳定状态的 service,阻止 service manager 重启主动退场的进程,并验证 State Root writer 已释放。独立执行的 service mutation 也会与 setup 和 deployment lifecycle 操作串行化。

该实现复用现有的 exact-Epoch Host prepare contract。Package staging、版本切换、replacement 启动和更新策略仍不属于 Runtime Host authority。

关联 #3231
关联 #3228

验证

  • npm --workspace @maka/runtime-host test — 1,086 项测试通过
  • npm --workspace maka-agent test — 393 项测试通过
  • Desktop Runtime Host management — 4 项测试通过
  • npm run lint
  • npm run format:check
  • npm run typecheck
  • 一次性 Linux systemd user service:install、target mismatch 拒绝、retire、幂等重试、restart、status、uninstall 与清理均通过

AI 使用

  • 无生成式工具实质参与
  • 生成式工具有实质参与

工具与范围:OpenAI Codex 在贡献者指导下实质参与实现与测试。人工贡献者负责审查与提交。

检查清单

  • 测试覆盖该变更,且缺少该变更时会失败
  • lint、format、typecheck 与受影响测试套件均已在本地通过

本 PR 是否改变行为?

  • 是 — 已在概要中说明

@M4n5ter
M4n5terforce-pushed the feat/runtime-host-managed-retirement branch 2 times, most recently from 3c62651 to d7fb7bdCompareAugust 23, 2026 07:28
Managed updates need a local operator path that fences admission before the service releases its State Root. Reuse the existing exact-Epoch Host prepare contract, then let the CLI service manager stop the unit and verify writer release.
The new retire action remains outside ordinary remote Client authority and returns bounded active-work or completed-retirement facts for later update orchestration.
Generated-by: Codex
Bind retirement to the managed service PID, stop every non-stable service state, and serialize standalone mutations with setup lifecycle operations. Keep the operator result contract explicit without projecting diagnostic snapshots as authoritative blockers.
Generated-by: Codex
Give intentional Host retirement a process outcome that the service manager treats as successful without restarting. Require every reported service PID to pass exact-Host preparation, and make active-work refusal unsuccessful to shell and JSON callers.
Generated-by: Codex
Hold the exact State Root writer while stopping a transitional managed service that has not published a PID. This prevents a systemd restart from admitting durable work between the status snapshot and stop, while failing closed if another Host wins the writer first.
Generated-by: Codex

@Astro-HanAstro-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.

Independent review of 0768ef9bbe526ce09e0d949de1c346dfb65f24d6, from two lines that did not share a starting point or read each other's findings.

No P0–P2. One [P3]. Exact-head checks: test is completed / success on that SHA. audit and package did not run on this head — recorded as not-run, not as green.

The retirement chain holds

Identity binding is triple-checked on the pid-known path: service status pid, then queryHostDiagnostics pid, then the pid returned by host.upgrade.prepare, with expectedHostEpoch anchoring the operation to one exact Host. Blocking is decided inside the Host by #hasUpgradeBlockingActivity() rather than projected from the CLI's diagnostics snapshot, and active_tasks is returned to the caller rather than acted on. After the drain gate is set, both the handshake admission read and begin-operation refuse new work, so a task starting just after the check cannot slip past the fence.

--allow-interrupt-active-tasks is a per-call boolean. It is not persisted to service config, does not change later defaults, and the operation is absent from REMOTE_OWNER_OPERATION_GRANTS — only local-owner IPC can invoke it. Its scope is broad (all blocking work on that exact Host) but explicit, which matches forced-retirement semantics.

Restart suppression is closed rather than assumed: shutdownReason='retirement' drives both exit paths to code 3, and the unit sets SuccessExitStatus=3 with RestartPreventExitStatus=3 against Restart=always. Note that a manual systemctl start remains possible; that is an operator action, not a service-manager auto-restart, and nothing here claims otherwise.

[P3] The pid-known path does not re-verify identity across stop

packages/cli/src/runtime-host-service-manager.ts acquires rootFence only in the starting/pid-less branch. The pid-known path calls backend.stop() with no fence held, and afterwards checks only that the service reached stopped with pid === null, then that the root was released.

If the old Host crashes during drain with a code other than 3, Restart=always can bring up a successor that takes the State Root writer and admits durable work before our stop lands. That stop then terminates the successor, the stopped-state assertion passes, the root is genuinely released — and the call still reports {kind: 'retired', hostEpoch, pid} carrying the original identity. A generation change happened inside the window that was supposed to end in retirement, the successor's writes remain, and the success result names a Host that is not the one that was stopped.

No data is lost — writer exclusivity is never violated — and the window requires a crash plus a restart to land between prepare and stop, so this is a narrow timing path rather than anything an attacker steers. Hence P3.

The minimal fix is to hold the root fence across stop on the pid-known path as well, or to re-read identity evidence after stop (restart counter, or reconnect and compare epoch) before reporting retired.

Boundaries

The service-management authority is split cleanly — Host owns admission and drain, the CLI owns managed PID, stop, and writer observation, systemd owns restart policy — and package staging, replacement startup, and update policy stay outside Runtime Host, as the description states. No duplicated retirement authority was found across the 14 files.

Limitations

Both reviewers were on Linux and could not exercise real systemd unit behaviour; the SuccessExitStatus / RestartPreventExitStatus link rests on source reading plus unit-rendering assertions, not on observed systemd action. Local runs: service-manager 21/21 and setup green; host-kernel 57/61, where the four failures reproduce identically on pristine main (missing Electron libatk in this environment) and are not attributed to this PR. The author's own full runtime/agent/desktop suites and the disposable systemd end-to-end run were not independently reproduced.


AI-assisted review, two independent lines. Under CONTRIBUTING.md §Review this does not replace the required independent human review.

@M4n5ter
M4n5ter merged commit 7f68af7 into mainAug 23, 2026
1 of 2 checks passed
@M4n5ter
M4n5ter deleted the feat/runtime-host-managed-retirement branch August 23, 2026 08:58

@Astro-HanAstro-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.

Two additions from a third reviewer, after the approval above. Neither changes the P3 severity; both make it more actionable.

A better fix direction than the one I suggested. I proposed either holding the fence across stopor re-reading identity after stop. The second option is weak: by the time you re-read, the successor has already been stopped and its writes cannot be undone, so you learn about the generation change without being able to prevent it. The right shape is to acquire the root fence afterprepare has released the old Host and hold it throughbackend.stop() until the service is stably stopped — failing closed if a successor wins the lock first, rather than stopping it as though it were the original Host. The starting/pid-less branch already has exactly this shape; the pid-known branch should converge on the same authority.

There is also a narrow semantic consequence worth noting: work admitted by the successor inside that window is invisible to the old Host's active-work check, so allowInterruptActiveTasks=false can be satisfied while durable work is nonetheless interrupted.

The retired wording overstates what the unit does.runtime-host-systemd-service.ts:218 sets RestartPreventExitStatus=3, which suppresses Restart=always for that exit only. The unit remains installed and enabled with WantedBy=default.target at :227. So besides a manual systemctl start, a future user-manager or default-target activation will also start it. The current human-facing "service is retired" reads as durable disablement.

Suggested wording: stopped for replacement; immediate restart suppressed; unit remains installed and enabled, and may be started explicitly or by a future target activation.

For the record on what verifyRuntimeHostRootReleased() proves: tryAcquireInteractiveRootOwner() takes an OS-level non-blocking exclusive lock on the exact root's owner.lock and revalidates the lock artifact against root identity, so a successful acquisition is real observation — at that instant, the old Host holds no writer and neither does anyone else. It is a snapshot of lock vacancy, not evidence of an identity-contiguous handoff, and it stops proving anything once owner.close() runs.

Sign up for freeto 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.

2 participants

@M4n5ter@Astro-Han