Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/local-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ LocalApp 只有一个 Server 实现。它可以作为开发机上的本地 Serve

用户只安装一个 `@patodo/localapp` npm 包,安装后的可执行命令仍为 `localapp`。个人电脑上运行的是当前操作系统用户的常驻 daemon;容器、NAS、局域网主机或公开服务器使用同一包的前台 Server 模式。项目不再提供 Tauri、托盘、Desktop 窗口或单独的 Rust CLI。

Windows 上的 daemon 是当前用户的计划任务:登录时启动,并以该用户自身的普通权限运行,不继承提权令牌。因此它需要有交互登录会话——只通过 SSH 使用、没有桌面会话的机器请改用 `localapp server run` 前台模式。首次注册计划任务可能需要管理员终端,之后 `status`、`stop`、`dev` 等命令在普通终端即可操作 daemon。

```bash
npm install --global @patodo/localapp
localapp server # 等同于 localapp server start,注册并启动用户 daemon
Expand Down
16 changes: 10 additions & 6 deletions packages/localapp/src/service/windows-user-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,15 @@ function isNotRunningTask(stderr: string): boolean {
}

/**
* The daemon must also run while the owning user has no interactive logon
* session (SSH-only or headless Windows), so the task registers with an S4U
* logon type instead of the schtasks /TR default, which is interactive-only
* and silently refuses to start. ExecutionTimeLimit PT0S removes the default
* 72-hour kill.
* InteractiveToken reuses the token from the owning user's own logon session,
* so the daemon runs at the user's integrity level. S4U would also allow a
* start with no interactive logon session, but it grants an administrator
* account an unfiltered (elevated) token, and the control pipe created by an
* elevated daemon carries a high mandatory label: Windows then lets a
* non-elevated client read the pipe but never write to it, so every CLI command
* that talks to the daemon fails with an access-denied transport error.
* LeastPrivilege is what keeps the reused token un-elevated and must stay with
* the principal. ExecutionTimeLimit PT0S removes the default 72-hour kill.
*/
function taskDefinition(nodePath: string, launcherPath: string): Buffer {
// Omitting UserId registers the principal as the creating user, so the
Expand All @@ -92,7 +96,7 @@ function taskDefinition(nodePath: string, launcherPath: string): Buffer {
</Triggers>
<Principals>
<Principal id="Author">
<LogonType>S4U</LogonType>
<LogonType>InteractiveToken</LogonType>
<RunLevel>LeastPrivilege</RunLevel>
</Principal>
</Principals>
Expand Down
8 changes: 6 additions & 2 deletions packages/localapp/tests/service-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ describe("per-user service manager", () => {
await expect(fs.stat(manager.registrationPath)).resolves.toMatchObject({ isFile: expect.any(Function) });
});

it("creates an S4U current-user Windows task that survives a logged-off console", async () => {
it("creates a least-privilege interactive current-user Windows task", async () => {
const fixture = await serviceFixture("windows & task (one)!");
const commands: ServiceCommandInvocation[] = [];
const manager = createServiceManager({
Expand All @@ -117,7 +117,11 @@ describe("per-user service manager", () => {
expect(create?.args).toContain("/XML");
expect(create?.args).not.toContain("SYSTEM");
const definition = await fs.readFile(create?.args[create.args.indexOf("/XML") + 1] ?? "", "utf16le");
expect(definition).toContain("<LogonType>S4U</LogonType>");
// Break caught: S4U hands an administrator account an elevated token, whose
// control pipe a non-elevated client can read but never write, so every
// daemon command failed with an access-denied transport error.
expect(definition).toContain("<LogonType>InteractiveToken</LogonType>");
expect(definition).not.toContain("S4U");
expect(definition).toContain("<RunLevel>LeastPrivilege</RunLevel>");
expect(definition).toContain("<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>");
expect(definition).toContain("<Command>C:\\Program Files\\Node &amp; Runtime\\node.exe</Command>");
Expand Down
Loading