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
6 changes: 6 additions & 0 deletions .changeset/fix-windows-token-permission.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 5 additions & 1 deletion packages/server/src/services/auth/privateFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 4 additions & 1 deletion packages/server/src/services/auth/tokenStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
17 changes: 11 additions & 6 deletions packages/server/test/services-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,22 @@ 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);
const buf = await readPrivateFile(p);
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);
Expand All @@ -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();
Expand Down Expand Up @@ -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();
Expand All @@ -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);
});

Expand Down
Loading