Skip to content

feat(runtime): separate enrollment and native service lifecycle - #1128

Open
akazwz wants to merge 12 commits into
felinics:mainfrom
akazwz:feat/issue-1014-runtime-service
Open

feat(runtime): separate enrollment and native service lifecycle#1128
akazwz wants to merge 12 commits into
felinics:mainfrom
akazwz:feat/issue-1014-runtime-service

Conversation

@akazwz

@akazwz akazwz commented Sep 1, 2026

Copy link
Copy Markdown
Member

改动内容

#1014 提供 CLI enrollment 持久化及当前用户的后台服务管理。保存连接、安装程序、启动进程分别由独立命令负责,安装、重启或故障恢复都不会隐式改写凭证或启动其他实例。

memoh-runtime enroll --server https://memoh.example --key "$MEMOH_RUNTIME_KEY"
memoh-runtime service install
memoh-runtime service start
  • enroll 将连接配置原子写入 ~/.memoh/runtime.json;更换连接或修复损坏配置必须显式传 --replace,读不出已有文件时报错会提示这一点。运行中的进程只在下次启动或重启时读取新配置。服务端只靠 key 识别 runtime,配置里不再有 runtimeId
  • run 和原有前台连接参数保持只读,不隐式保存连接;--config 只指定输入文件。run --log <file> 让进程自己追加状态与错误日志,供无法捕获输出的服务管理器使用。
  • service install 将 CLI 和 bridge 协议文件复制到独立安装目录,固定 Node 路径,校验服务定义后注册服务并保持停止;成功后删除被替换的旧程序目录。启动不依赖 npm/npx 缓存;重新安装后也需要显式 start
  • start / restart 先校验安装和连接配置;restart 校验失败不会停止原进程。固定的 Node 或安装程序被删除(例如包管理器升级)时,报错直接给出路径和重装提示。stop / uninstall 不解析连接配置或安装记录,文件损坏时仍可清理服务。stop 只结束当前进程,注册仍在,下次登录会再次启动;uninstall --purge 额外移除安装程序、服务定义和日志,但保留 enrollment。
  • 使用 launchd、systemd user service 和 Windows Task Scheduler;Windows 任务名按当前用户 SID 隔离,使用交互用户身份,并通过 run --log 写入 ~/.memoh/runtime/logs/runtime.logstatus 报告原生服务状态及后端细节,不代表服务端连接状态。
  • ~/.memoh 下的目录链拒绝其他账户可写、用户自有符号链接和 reparse point;POSIX 使用私有权限,Windows 使用受保护 ACL,只持久化修改过的 Owner/DACL。固定的 Node 可执行文件只要求本身是其他账户不能改写的普通文件,Homebrew 的 admin 可写 Cellar、他人安装的 /usr/local 都能通过;macOS ACL 检查忽略用户授予自己的条目。凭证不进入原生服务定义。
  • 内部 Windows PowerShell 子进程移除继承的 PSModulePath,避免从 PowerShell 7 经 Node 启动时系统模块加载失败。

新增 runtime README 和架构说明,覆盖安装失败后的重试、固定目录、旧 PR 安装的迁移、各平台自启动范围和日志位置。本 PR 不包含 Web 安装命令生成和 Reconnect 界面改造。

验证

  • runtime 测试 101 通过、1 个 Windows 专用测试在 macOS 跳过;build、typecheck、ESLint 通过。
  • 此前提交 38e126aab 在 macOS launchd、Ubuntu 24.04 ARM64 systemd、Windows 11 ARM64 Task Scheduler 上完成过原生生命周期与真实 Bot 工具调用的人工验证。最新提交改动了可执行文件信任检查、Windows 任务参数和安装清理,三平台主路径需要重新人工验证。

⚠️ No human QA — this PR has not been verified by a human yet. Remove this line once a human confirms the happy path.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UV3sQkb4txRRj54H7VuB84

@akazwz
akazwz requested a review from a team as a code owner September 1, 2026 12:59
@chen-ran

chen-ran commented Sep 4, 2026

Copy link
Copy Markdown
Member

实测发现以下问题,建议合并前修复:

  1. 目录完整性保护不足:已有 .memoh 目录若允许其他用户写入,即使凭证文件为 0600,仍可被替换;已跨 UID 复现替换启动程序并以原用户身份执行。应检查关键目录的所有者和写权限。
  2. Linux 服务无法启动:WorkingDirectory="/home/victim" 在 systemd 255 上被判为非法路径,但 CLI 仍报告安装成功。需要修正字段编码并检查实际启动状态。
  3. 凭证替换与恢复不完整:前台命令会直接覆盖后台配置;新 --config 路径可绕过 --replace;安装失败后也不恢复旧凭证。
  4. Linux 重装不切换进程:修正测试 unit 后,install --replace 仍保留旧 PID、旧连接,直到显式 restart 才生效。

launchctl bootout returns before launchd finishes tearing the job down,
so an immediate bootstrap intermittently fails with
"Bootstrap failed: 5: Input/output error" (reproduced ~1 in 20 on
macOS 27). Retry bootstrap up to five times with a 300ms gap.
~/.memoh is shared with other Memoh components and --config may point
anywhere, so enforce 0700 only on the runtime-owned home directory and
rely on the 0600 file mode for the enrollment itself.
Create runtime directories private and keep the enrollment file 0600,
but never refuse to run because an existing directory is not 0700.
launchd's ProcessType=Background makes node hang inside dyld on macOS 27
(getpwuid_r never returns), so the agent runs as a standard process.

Login Items names a legacy agent after its executable file, so POSIX
staging adds a "Memoh Runtime" symlink to cli.mjs and the service
launches through it instead of showing "cli.mjs".
launchctl bootout returns before the job is gone, so a status check right
after stop or uninstall still reported the service as running.
@akazwz
akazwz force-pushed the feat/issue-1014-runtime-service branch from 6bdecfb to 10727a3 Compare September 7, 2026 13:01
@akazwz akazwz changed the title feat(runtime): persist CLI enrollment and manage service feat(runtime): separate enrollment and native service lifecycle Sep 7, 2026
akazwz and others added 3 commits September 7, 2026 06:09
… log

The pinned Node executable no longer has to sit in a directory chain that
only the user or root can write: Homebrew keeps its Cellar admin-writable
and a shared /usr/local belongs to whoever installed it, so service install
failed on the most common macOS setup. Only the file itself must be immune
to rewriting by other accounts. macOS ACL checks now ignore entries the
user granted to themselves.

Task Scheduler captures no output, so the Windows definition passes
run --log and the process appends its own status lines and errors to
logs/runtime.log. A missing pinned Node or program now names the path and
the remedy instead of a bare ENOENT, install removes the generations it
replaced, uninstall --purge also removes the service definitions, status
prints the backend detail, enroll names --replace when the saved file
cannot be read, and stop says the service returns at the next login.

The enrollment no longer carries runtimeId: the server identifies a
runtime by its key alone and the value was never sent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UV3sQkb4txRRj54H7VuB84
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants