fix(server): skip Unix-only permission check on Windows for server token (#1135)

This commit is contained in:
qer 2026-06-26 18:38:24 +08:00 committed by GitHub
parent b0b2aee8c5
commit bf51fb7a10
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 26 additions and 8 deletions

View file

@ -0,0 +1,6 @@
---
"@moonshot-ai/server": patch
"@moonshot-ai/kimi-code": patch
---
Fix the local server failing to start on Windows after the first run because the persistent token file's synthesized mode was rejected as too permissive.

View file

@ -54,7 +54,11 @@ export async function writePrivateFile(
export async function readPrivateFile(filePath: string): Promise<Buffer> {
const info = await stat(filePath);
if ((info.mode & 0o077) !== 0) {
// Windows does not have Unix-style permission bits; libuv synthesises the
// mode from the read-only attribute, so a private writable file is reported
// as 0o666 and a read-only one as 0o444. The ACL-based security model is
// different, so this check only makes sense on POSIX systems.
if (process.platform !== 'win32' && (info.mode & 0o077) !== 0) {
throw new PrivateFileTooPermissiveError(filePath, info.mode & 0o777);
}
return readFile(filePath);

View file

@ -48,7 +48,10 @@ export async function createTokenStore(homeDir: string): Promise<TokenStore> {
}
// Changed: re-read, but refuse a too-permissive file and never let an
// empty/partial read clobber the last good token.
if ((st.mode & 0o077) !== 0) {
// Skip the check on Windows: fs.stat mode is synthesised from the
// read-only attribute and does not reflect real ACLs, so it would always
// appear too permissive and prevent legitimate token reloads.
if (process.platform !== 'win32' && (st.mode & 0o077) !== 0) {
return cache.token;
}
try {

View file

@ -44,14 +44,14 @@ describe('privateFiles', () => {
expect(statSync(join(tmpDir, 'nested', 'dir')).mode & 0o777).toBe(0o700);
});
it.skipIf(process.platform === 'win32')('round-trips string content through readPrivateFile', async () => {
it('round-trips string content through readPrivateFile', async () => {
const p = join(tmpDir, 'secret');
await writePrivateFile(p, 's3cr3t-value');
const buf = await readPrivateFile(p);
expect(buf.toString('utf8')).toBe('s3cr3t-value');
});
it.skipIf(process.platform === 'win32')('round-trips Buffer content through readPrivateFile', async () => {
it('round-trips Buffer content through readPrivateFile', async () => {
const p = join(tmpDir, 'bin');
const data = Buffer.from([0, 1, 2, 254, 255]);
await writePrivateFile(p, data);
@ -59,7 +59,7 @@ describe('privateFiles', () => {
expect(buf.equals(data)).toBe(true);
});
it('readPrivateFile throws on a 0644 file', async () => {
it.skipIf(process.platform === 'win32')('readPrivateFile throws on a 0644 file', async () => {
const p = join(tmpDir, 'leaky');
writeFileSync(p, 'x', { mode: 0o644 });
chmodSync(p, 0o644);
@ -84,7 +84,7 @@ describe('tokenStore', () => {
await b.dispose();
});
it.skipIf(process.platform === 'win32')('reuses the same persistent token across stores in one home dir', async () => {
it('reuses the same persistent token across stores in one home dir', async () => {
const home = join(tmpDir, 'home');
const a = await createTokenStore(home);
const token = a.getToken();
@ -123,7 +123,7 @@ describe('tokenStore', () => {
expect(existsSync(store.tokenPath)).toBe(true);
});
it.skipIf(process.platform === 'win32')('re-reads the token after the file is rewritten (live rotation)', async () => {
it('re-reads the token after the file is rewritten (live rotation)', async () => {
const home = join(tmpDir, 'home');
const store = await createTokenStore(home);
const original = store.getToken();
@ -142,11 +142,16 @@ describe('tokenStore', () => {
});
describe('persistentToken', () => {
it.skipIf(process.platform === 'win32')('loadOrCreateServerToken generates once and reuses thereafter', async () => {
it('loadOrCreateServerToken generates once and reuses thereafter', async () => {
const home = join(tmpDir, 'home');
const a = await loadOrCreateServerToken(home);
const b = await loadOrCreateServerToken(home);
expect(a).toBe(b);
});
it.skipIf(process.platform === 'win32')('writes server.token with mode 0600', async () => {
const home = join(tmpDir, 'home');
await loadOrCreateServerToken(home);
expect(statSync(join(home, 'server.token')).mode & 0o777).toBe(0o600);
});