Skip to content
Draft
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
- 统一菜单自定义图片的公开契约:新上传会落盘为正式用户主题,只有旧版 `custom-upload` 保留为本地兼容槽。
- 恢复 `llms-full.txt` 同步、确定性 `.skill` 制品与发布哈希一致性。
- 以 `package.json` 作为版本真相源,README 改为不绑定 patch 版本的最新 Release 链接。
- 修复 Windows 缺少 `Set-Acl` 所需权限时,ACL 回退把裸 SID 传给 `icacls /setowner` 并触发 1332,最终误报 `LOCK_PERMISSIONS`、导致皮肤启动失败。
- 修复 Windows Store 版把首次注入完全交给 detached 临时控制器时的启动竞态:前台在已验证 CDP 后同步完成首次注入,再启动临时控制器保活,避免连续 `160/160` 后误报“未确认皮肤已应用”。
- 修复 Windows 前后台控制器每次抢锁都重复重写已合规状态目录 ACL,导致 `controller:start` 长时间持锁、后台 ACK 超时并补偿关闭:现对已有私有目录先做只读精确校验,仅在不合规时执行 ACL 迁移。

### 测试

Expand Down
38 changes: 33 additions & 5 deletions src/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1922,22 +1922,50 @@ export async function waitForAppliedSkin({
throw new Error("ephemeral controller 未确认皮肤已应用");
}

async function productionRegisterEphemeral({ deps, paths, port, preflight, themeId }) {
await ensureProductionState({
export async function productionRegisterEphemeral({
deps,
paths,
port,
preflight,
themeId,
loadedTheme,
themes,
preferStored,
}, {
ensureState = ensureProductionState,
spawnController = spawn,
controllerEntry = fileURLToPath(import.meta.url),
confirmApplied = waitForAppliedSkin,
} = {}) {
await ensureState({
paths,
themeId,
process: preflight.process,
keepUntilProcessExit: true,
});
const child = spawn(process.execPath, [
fileURLToPath(import.meta.url),

// 首次注入必须由仍附着在交互会话中的前台进程同步完成。Store 版 Codex
// 在关闭/激活之间会重建进程树;若把首次注入完全交给 detached 子进程,
// 子进程可能尚未启动就随旧进程退出,前台只能无信息地轮询到超时。
await deps.applySkin({
loadedTheme,
themes,
activeId: themeId,
port,
currentVersion: await deps.readCurrentPackageVersion(),
preferStored,
control: null,
});

const child = spawnController(process.execPath, [
controllerEntry,
"controller",
"--ephemeral",
"--port",
String(port),
], { detached: true, stdio: "ignore" });
child.unref();
await waitForAppliedSkin({ deps, port, themeId });
await confirmApplied({ deps, port, themeId });
return { mode: "active" };
}

Expand Down
20 changes: 16 additions & 4 deletions src/operation-lock.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2475,11 +2475,23 @@ async function prepareWindowsStateRoot(stateRoot, security) {
throw lockError("LOCK_PATH_INVALID", `Windows state root must be a real directory: ${stateRoot}`);
}
try {
if (created) {
await windowsSecurityBatch(security, [
{ action: "protect-directory", path: stateRoot },
{ action: "verify-directory", path: stateRoot },
]);
return;
}
try {
// 正常运行时状态根已经是精确私有 ACL。先只读验证,避免每次抢锁都
// Set-Acl/icacls 重写同一目录,造成前后台控制器互相阻塞。
await security.verifyDirectory(stateRoot);
return;
} catch {
// 旧版本或手工改动留下的非精确 ACL 仍走带所有者/写权限前置检查的迁移。
}
await windowsSecurityBatch(security, [
{
action: created ? "protect-directory" : "migrate-directory",
path: stateRoot,
},
{ action: "migrate-directory", path: stateRoot },
{ action: "verify-directory", path: stateRoot },
]);
} catch (cause) {
Expand Down
6 changes: 4 additions & 2 deletions src/windows-secure-fs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,17 @@ foreach ($entry in $operations) {
Microsoft.PowerShell.Security\Set-Acl -LiteralPath $TargetPath -AclObject $acl -ErrorAction Stop
} catch {
# 部分账户/会话没有 SeSecurityPrivilege;与 Windows 安装脚本一致,回退 icacls。
# fallback 必须与 Set-Acl 路径保持同一精确契约:setowner + 重置 grant + 随后 exact verify。
# migrate 已在上方证明 owner 是当前用户;protect 的目标则由当前进程刚创建。
# 不要把裸 SID 交给 icacls /setowner:它会按账户名解析并以 1332 失败。
# 与安装脚本保持同一契约:移除继承、重置当前 SID grant,再做 exact verify。
$detail = [string]$_.Exception.Message
$icacls = Join-Path $env:SystemRoot 'System32\icacls.exe'
if (-not (Test-Path -LiteralPath $icacls -PathType Leaf)) {
throw "Set-Acl failed and icacls is unavailable: $detail"
}
$sidText = [string]$currentSid.Value
$grant = if ($isDirectory) { '*{0}:(OI)(CI)F' -f $sidText } else { '*{0}:F' -f $sidText }
$output = & $icacls $TargetPath /inheritance:r /setowner $sidText /grant:r $grant 2>&1
$output = & $icacls $TargetPath /inheritance:r /grant:r $grant 2>&1
if ($LASTEXITCODE -ne 0) {
throw ("Set-Acl failed and icacls fallback failed: {0}; icacls: {1}" -f $detail, (($output | ForEach-Object { [string]$_ }) -join ' '))
}
Expand Down
109 changes: 109 additions & 0 deletions test/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
probeWindowsNativeProcessFromSnapshot,
productionLockOptions,
productionPreflight,
productionRegisterEphemeral,
runCli,
runControllerProcess,
spawnWindowsRestartIntoCdp,
Expand Down Expand Up @@ -694,6 +695,114 @@ test("waitForAppliedSkin emits progress while confirmation is pending", async ()
assert.ok(messages.some((message) => /无需点击/.test(message)));
});

test("production ephemeral registration injects in the foreground before spawning its keeper", async () => {
const events = [];
const loadedTheme = { manifest: { id: "miku-488137" } };
const themes = [loadedTheme];
const preflight = {
process: {
pid: 4242,
executablePath: "C:\\Program Files\\Codex\\Codex.exe",
startedAt: "2026-07-31T01:00:00.0000000Z",
},
};
const child = {
unref() { events.push(["unref"]); },
};

const result = await productionRegisterEphemeral({
deps: {
readCurrentPackageVersion: async () => "5.4.11",
applySkin: async (input) => {
events.push(["apply", structuredClone(input)]);
return { applied: 1 };
},
skinStatus: async () => {
throw new Error("confirmation is injected for this unit test");
},
},
paths: { stateRoot: "C:\\state" },
port: 9341,
preflight,
themeId: "miku-488137",
loadedTheme,
themes,
preferStored: true,
}, {
ensureState: async (input) => {
events.push(["state", structuredClone(input)]);
},
spawnController: (file, args, options) => {
events.push(["spawn", file, [...args], { ...options }]);
return child;
},
controllerEntry: "C:\\repo\\src\\cli.mjs",
confirmApplied: async (input) => {
events.push(["confirm", input.port, input.themeId]);
},
});

assert.deepEqual(result, { mode: "active" });
assert.deepEqual(events.map(([event]) => event), [
"state",
"apply",
"spawn",
"unref",
"confirm",
]);
assert.deepEqual(events[0][1], {
paths: { stateRoot: "C:\\state" },
themeId: "miku-488137",
process: preflight.process,
keepUntilProcessExit: true,
});
assert.deepEqual(events[1][1], {
loadedTheme,
themes,
activeId: "miku-488137",
port: 9341,
currentVersion: "5.4.11",
preferStored: true,
control: null,
});
assert.deepEqual(events[2].slice(2), [[
"C:\\repo\\src\\cli.mjs",
"controller",
"--ephemeral",
"--port",
"9341",
], {
detached: true,
stdio: "ignore",
}]);
});

test("production ephemeral registration does not spawn a keeper when foreground injection fails", async () => {
let spawned = false;
await assert.rejects(productionRegisterEphemeral({
deps: {
readCurrentPackageVersion: async () => "5.4.11",
applySkin: async () => {
throw new Error("foreground injection failed");
},
},
paths: { stateRoot: "C:\\state" },
port: 9341,
preflight: { process: { pid: 4242 } },
themeId: "miku-488137",
loadedTheme: { manifest: { id: "miku-488137" } },
themes: [],
preferStored: false,
}, {
ensureState: async () => {},
spawnController: () => {
spawned = true;
return { unref() {} };
},
}), /foreground injection failed/);
assert.equal(spawned, false);
});

test("apply on a native Codex queues one detached CDP restart and applies only after restart", async () => {
const fx = lifecycleDeps({
preflightLifecycle: async (input) => {
Expand Down
39 changes: 37 additions & 2 deletions test/operation-lock.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -667,20 +667,55 @@ test("Windows operation lease safely migrates a current-user-owned legacy state
const lockPath = join(stateRoot, "operation.lock");
await mkdir(stateRoot);
const securityEvents = [];
const windowsSecurity = fakeWindowsSecurity(securityEvents);
const verifyDirectory = windowsSecurity.verifyDirectory;
let firstVerification = true;
windowsSecurity.verifyDirectory = async (path) => {
if (path === stateRoot && firstVerification) {
firstVerification = false;
securityEvents.push(["verify-directory", path]);
throw new Error("legacy ACL is not exact");
}
return verifyDirectory(path);
};
const lease = await acquireOperationLock(acquisitionOptions(lockPath, {
platform: "win32",
stateRoot,
windowsSecurity: fakeWindowsSecurity(securityEvents),
windowsSecurity,
}));
t.after(() => lease.release());

assert.deepEqual(securityEvents.slice(0, 2), [
assert.deepEqual(securityEvents.slice(0, 3), [
["verify-directory", stateRoot],
["migrate-directory", stateRoot],
["verify-directory", stateRoot],
]);
assert.equal(await lease.assertOwned(), true);
});

test("Windows operation lease does not rewrite an already private state root ACL", async (t) => {
const { root } = await fixture(t);
const stateRoot = join(root, "private-windows-state");
const lockPath = join(stateRoot, "operation.lock");
await mkdir(stateRoot);
const securityEvents = [];
const lease = await acquireOperationLock(acquisitionOptions(lockPath, {
platform: "win32",
stateRoot,
windowsSecurity: fakeWindowsSecurity(securityEvents),
}));
t.after(() => lease.release());

assert.deepEqual(securityEvents.slice(0, 1), [
["verify-directory", stateRoot],
]);
assert.equal(
securityEvents.some(([action, path]) => action === "migrate-directory" && path === stateRoot),
false,
);
assert.equal(await lease.assertOwned(), true);
});

test("Windows operation lease has one concurrent winner and recovers a proven-dead owner", async (t) => {
const { root } = await fixture(t);
const stateRoot = join(root, "windows-state");
Expand Down
6 changes: 6 additions & 0 deletions test/windows-runtime.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,12 @@ test("Windows ACL adapter isolates Windows PowerShell modules from the parent ru
);
assert.match(windowsAclPowerShellScript, /Microsoft\.PowerShell\.Security\\Get-Acl/);
assert.match(windowsAclPowerShellScript, /Microsoft\.PowerShell\.Security\\Set-Acl/);
assert.match(windowsAclPowerShellScript, /'\*\{0\}:\(OI\)\(CI\)F'/);
assert.match(
windowsAclPowerShellScript,
/\$output = & \$icacls \$TargetPath \/inheritance:r \/grant:r \$grant/,
);
assert.doesNotMatch(windowsAclPowerShellScript, /\/setowner \$sidText/);
});

test("Windows ACL adapter preserves the canonical request path instead of rewriting 8.3 aliases", async () => {
Expand Down