From c20493b7b41f1324b60af96b92893b838fcda7cc Mon Sep 17 00:00:00 2001 From: JasonBuildAI Date: Fri, 16 Oct 2026 21:16:15 +0800 Subject: [PATCH] fix(task-lease): discover machine identity on native Windows Verified worktree leases fail closed on native Windows: the host identity source only covered macOS (IOPlatformUUID) and Linux (/etc/machine-id), so the product-recommended `--write-worktree` path always rejected with "host identity unavailable for worktree isolation". Read this OS installation's MachineGuid through `reg query` on win32 and keep unknown hosts fail-closed at a null identity. Unskip the TypeScript observation test on win32, correct the two Python skip reasons to their remaining Windows symlink constraint, and update the lease guide's supported-host sentence. Signed-off-by: JasonBuildAI --- docs/reference/canonical-lease-renew.md | 7 ++++--- .../work_items/task_lease_workspace.ts | 19 ++++++++++++++++--- tests/control_plane/test_task_lease_cli.py | 2 +- .../task_lease_workspace.test.ts | 4 +++- tests/test_task_lease_worktree.py | 2 +- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/docs/reference/canonical-lease-renew.md b/docs/reference/canonical-lease-renew.md index 7661d13d19..f0d0901c1c 100644 --- a/docs/reference/canonical-lease-renew.md +++ b/docs/reference/canonical-lease-renew.md @@ -95,9 +95,10 @@ if retained repository metadata differs for that physical checkout. Requests without a verified caller workspace retain ordinary exclusive scope checks, including against unknown legacy grants. Repository mismatch, redirected paths and a non-worktree root fail closed. Verified machine discovery -currently supports macOS and Linux; other hosts retain ordinary leases. No -workspace path or machine identifier is stored directly: only opaque digests -and the existing public repository identity enter the private authority record. +currently supports macOS, Linux and Windows; other hosts retain ordinary +leases. No workspace path or machine identifier is stored directly: only +opaque digests and the existing public repository identity enter the private +authority record. Renewal preserves this identity. Acquire retries must use the same worktree, scopes and execution key; an alias resolving to the same worktree is valid. diff --git a/loopx/control_plane/work_items/task_lease_workspace.ts b/loopx/control_plane/work_items/task_lease_workspace.ts index bc19d15304..17331884d3 100644 --- a/loopx/control_plane/work_items/task_lease_workspace.ts +++ b/loopx/control_plane/work_items/task_lease_workspace.ts @@ -84,6 +84,21 @@ export function sameLeaseCheckout(left: unknown, right: unknown): boolean { a.worktree === b.worktree; } +/** The platform's OS-installation identity for worktree isolation; unsupported hosts stay fail-closed. */ +async function machineIdentity(): Promise { + if (platform() === "darwin") { + return /"IOPlatformUUID"\s*=\s*"([^"]+)"/u.exec((await run("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {timeout: 5000})).stdout)?.[1]; + } + if (platform() === "linux") return (await readFile("/etc/machine-id", "utf8")).trim(); + if (platform() === "win32") { + // MachineGuid is the Windows analog of /etc/machine-id: this OS + // installation's identity, not a user-changeable computer name. + return /MachineGuid\s+REG_SZ\s+([0-9a-fA-F-]{36})/u.exec((await run("reg", + ["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"], {timeout: 5000})).stdout)?.[1]; + } + return undefined; +} + export async function observeLeaseWorktree(path: string, overlaps: (path: string) => boolean): Promise { if (!isAbsolute(path)) throw new LeaseWorktreeObservationError("worktree observation requires an absolute caller path"); const cwd = await realpath(path); @@ -132,9 +147,7 @@ export async function observeLeaseWorktree(path: string, overlaps: (path: string // Private local paths are not persisted. Filesystem inode identity collapses // symlink/case aliases; a recreated directory gets a new identity. const key = async (p: string) => {const s = await stat(p); return digest(`${s.dev}:${s.ino}`);}; - const machine = platform() === "darwin" - ? /"IOPlatformUUID"\s*=\s*"([^"]+)"/u.exec((await run("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {timeout: 5000})).stdout)?.[1] - : platform() === "linux" ? (await readFile("/etc/machine-id", "utf8")).trim() : null; + const machine = await machineIdentity(); if (!machine) throw new LeaseWorktreeObservationError("host identity unavailable for worktree isolation"); return {host: digest(machine), common_directory: await key(common), worktree: await key(root), repository}; } diff --git a/tests/control_plane/test_task_lease_cli.py b/tests/control_plane/test_task_lease_cli.py index 90d73b2805..ad44546c78 100644 --- a/tests/control_plane/test_task_lease_cli.py +++ b/tests/control_plane/test_task_lease_cli.py @@ -14,7 +14,7 @@ ROOT = Path(__file__).resolve().parents[2] -@pytest.mark.skipif(sys.platform not in {"darwin", "linux"}, reason="Native worktree host identity requires macOS or Linux") +@pytest.mark.skipif(sys.platform not in {"darwin", "linux"}, reason="worktree lease fixture requires symlinks; Windows needs Developer Mode") @pytest.mark.parametrize("provider", ["file", "sqlite"]) def test_dependency_links_replay_and_recovery_preserve_real_lease_authority(tmp_path, monkeypatch, provider): isolate_sqlite_runtime(tmp_path, monkeypatch) diff --git a/tests/control_plane_ts/task_lease_workspace.test.ts b/tests/control_plane_ts/task_lease_workspace.test.ts index 9b798d9b00..62f033aaa8 100644 --- a/tests/control_plane_ts/task_lease_workspace.test.ts +++ b/tests/control_plane_ts/task_lease_workspace.test.ts @@ -14,7 +14,7 @@ test("native observation never interprets a caller path relative to the worker", }); test("observed Git origins use the Todo repository identity contract", { - skip: !["darwin", "linux"].includes(platform()), + skip: !["darwin", "linux", "win32"].includes(platform()), }, async t => { const temporary = await mkdtemp(join(tmpdir(), "loopx-lease-origin-")); t.after(() => rm(temporary, {recursive: true, force: true})); @@ -58,6 +58,8 @@ test("observed Git origins use the Todo repository identity contract", { } }); +// Creating file symlinks needs Developer Mode or administrator rights on +// Windows; the machine-identity path is covered by the observation test above. test("normal executable file links preserve the declared independent checkout", { skip: !["darwin", "linux"].includes(platform()), }, async t => { diff --git a/tests/test_task_lease_worktree.py b/tests/test_task_lease_worktree.py index 37c652ad7b..9e5ee21fff 100644 --- a/tests/test_task_lease_worktree.py +++ b/tests/test_task_lease_worktree.py @@ -10,7 +10,7 @@ from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection -@pytest.mark.skipif(sys.platform not in {"darwin", "linux"}, reason="verified host identity currently supports macOS/Linux") +@pytest.mark.skipif(sys.platform not in {"darwin", "linux"}, reason="worktree lease fixture requires symlinks; Windows needs Developer Mode") @pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("origin,repository", [ ("https://github.com/example/project.git", "git:github.com/example/project"),