Skip to content

fix(windows): run the daemon with the user's own privileges - #3

Merged
Patodo merged 1 commit into
mainfrom
fix/windows-daemon-interactive-token
Sep 17, 2026
Merged

Patodo merged 1 commit into
mainfrom
fix/windows-daemon-interactive-token

Conversation

@Patodo

@Patodo Patodo commented Sep 17, 2026

Copy link
Copy Markdown
Owner

修复 Windows 上 daemon 无法被普通权限 CLI 控制的问题(上一轮排查的结论)。

问题

Windows 上 daemon 由当前用户的计划任务拉起,任务 principal 用的是 S4U 登录类型。对属于 Administrators 的账户,S4U 会产生不经 UAC 过滤的令牌,于是 daemon(以及它拉起的 Server)以高完整性运行,它创建的命名管道也带 High 强制标签。Windows 允许中完整性客户端读这个管道、但不能写(NO_WRITE_UP),而 libuv 固定以读写方式连接,因此所有走控制通道的命令都失败:

$ localapp server status
{"error":{"code":"ipc_transport_failed","message":"The LocalApp daemon control transport failed"}}

判别证据(普通权限下对管道按方向连接):In 成功、Out/InOut 被拒——这是 NO_WRITE_UP 的特征,而非 DACL 拒绝(那会连读一起拒)。

修法

任务 principal 改为 InteractiveToken,复用用户自身登录会话的令牌,daemon 就以该用户的完整性级别运行;RunLevel=LeastPrivilege 保留(它决定令牌不被提权)。

顺带记录一个被证伪的方向:先试过「给管道设 DACL + 完整性标签」(SetSecurityInfo + SeSecurityPrivilege,调用返回成功),但无效——命名管道每个实例的安全描述符在 CreateNamedPipe 时按创建进程的令牌确定,事后修改不影响后续实例。所以只能从令牌这一侧解决。

实测(Windows 11 26200,管理员账户)

按任务定义测得的令牌完整性:

任务 principal 得到的令牌
S4U(原实现) High
S4U + LeastPrivilege High(RunLevel 对 S4U 无效)
InteractiveToken(本 PR) Medium
InteractiveToken + HighestAvailable High

端到端:重新注册并让任务拉起 daemon 后,普通终端下

$ localapp server status
{"action":"status","status":{"bootId":"...","server":{"status":"ready","listenUrl":"http://127.0.0.1:52812",...}}}
$ localapp server logs
{"action":"logs","logs":""}

/health 返回 200 {"status":"ok"};管道按方向探测 In/Out/InOut 全部连接成功。

pnpm -C packages/localapp test 中 creates a least-privilege interactive current-user Windows task 通过,并新增断言禁止回到 S4U。tsc --noEmit 通过。

这是一处能力取舍

S4U 是 1e4d48c 为「用户未登录时也能启动 daemon」(SSH-only / headless Windows)引入的,这两个目标对管理员账户直接冲突,本 PR 选择保住「daemon 可被普通 CLI 控制」。无桌面会话的 Windows 机器请改用前台 localapp server run,文档已注明。

未包含

  • 未修改 native adapter。
  • 未改 npm 发布流程。

The Windows user task registered with an S4U logon type. For an administrator
account S4U issues an unfiltered (elevated) token, so the daemon — and the
Server it spawns — ran elevated, and the control pipe it created carried a high
mandatory label. Windows then lets a non-elevated client read that pipe but
never write to it, so every command that talks to the daemon failed:

  $ localapp server status
  {"error":{"code":"ipc_transport_failed","message":"..."}}

Register the task with InteractiveToken instead, which reuses the token from
the owner's own logon session and runs the daemon at that user's integrity
level. RunLevel LeastPrivilege stays on the principal: measured on Windows 11,
this definition with HighestAvailable yields a High token, and S4U yields High
whether or not RunLevel is present.

Verified on Windows 11 26200 with an administrator account: after the task
re-registered and started the daemon, the control pipe accepts read+write from a
non-elevated client, and `localapp server status`, `server logs` and the Server
/health endpoint all succeed from a normal terminal.

Trade-off: the task can no longer start the daemon without an interactive logon
session, which is what S4U was introduced for. Session-less Windows machines
should use the foreground `localapp server run` instead, which docs/local-runtime.md
now states.
@Patodo
Patodo merged commit 2ee770c into main Sep 17, 2026
4 of 5 checks passed
Patodo added a commit that referenced this pull request Sep 17, 2026
Ships the Windows daemon privilege fix (#3): the current-user task now uses an
InteractiveToken principal, so the daemon runs at the owner's integrity level
and its control pipe accepts non-elevated clients again.

Version-bound test assertions move to 0.2.6 and the reviewed public source
baseline digests are re-recorded for the two files this touches. The flagged
content in both files is unchanged: the same three canonical package markers
and the same apiKey fixture as before.
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.

1 participant