Skip to content

Commit b79a894

Browse files
author
Federico Liva
committed
feat(server): WORKSPACE_STATE_DIR override for mutable state (token, master key, logs)
The server resolves its encrypted token, master key, and log directory against PROJECT_ROOT (the extension checkout). When the package is installed in a read-only location (e.g. a system-wide install owned by root while the server runs as an unprivileged user), those writes fail with EACCES. WORKSPACE_STATE_DIR points mutable state at a writable directory; the default (PROJECT_ROOT) is unchanged.
1 parent d78a4ef commit b79a894

6 files changed

Lines changed: 148 additions & 93 deletions

File tree

workspace-server/dist/headless-login.js

Lines changed: 42 additions & 41 deletions
Large diffs are not rendered by default.

workspace-server/dist/index.js

Lines changed: 49 additions & 48 deletions
Large diffs are not rendered by default.

workspace-server/src/__tests__/services/DriveService.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ jest.mock('node:path', () => {
4444
});
4545
jest.mock('../../utils/paths', () => ({
4646
PROJECT_ROOT: '/mock/project/root',
47+
STATE_DIR: '/mock/project/root',
4748
ENCRYPTED_TOKEN_PATH: '/mock/project/root/token.json',
4849
ENCRYPTION_MASTER_KEY_PATH: '/mock/project/root/key',
4950
}));

workspace-server/src/__tests__/utils/paths.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,44 @@ describe('paths utils', () => {
2626
expect(PROJECT_ROOT.endsWith('workspace-server')).toBe(false);
2727
});
2828
});
29+
30+
describe('STATE_DIR', () => {
31+
const reload = () => {
32+
jest.resetModules();
33+
// eslint-disable-next-line @typescript-eslint/no-require-imports
34+
return require('../../utils/paths') as typeof import('../../utils/paths');
35+
};
36+
37+
afterEach(() => {
38+
delete process.env['WORKSPACE_STATE_DIR'];
39+
jest.resetModules();
40+
});
41+
42+
it('defaults to PROJECT_ROOT when WORKSPACE_STATE_DIR is unset', () => {
43+
delete process.env['WORKSPACE_STATE_DIR'];
44+
const m = reload();
45+
expect(m.STATE_DIR).toBe(m.PROJECT_ROOT);
46+
expect(m.ENCRYPTED_TOKEN_PATH).toBe(
47+
path.join(m.PROJECT_ROOT, 'gemini-cli-workspace-token.json'),
48+
);
49+
expect(m.ENCRYPTION_MASTER_KEY_PATH).toBe(
50+
path.join(m.PROJECT_ROOT, '.gemini-cli-workspace-master-key'),
51+
);
52+
});
53+
54+
it('honors WORKSPACE_STATE_DIR for token and master-key paths', () => {
55+
process.env['WORKSPACE_STATE_DIR'] = '/var/lib/workspace-state';
56+
const m = reload();
57+
expect(m.STATE_DIR).toBe('/var/lib/workspace-state');
58+
expect(m.ENCRYPTED_TOKEN_PATH).toBe(
59+
path.join('/var/lib/workspace-state', 'gemini-cli-workspace-token.json'),
60+
);
61+
expect(m.ENCRYPTION_MASTER_KEY_PATH).toBe(
62+
path.join(
63+
'/var/lib/workspace-state',
64+
'.gemini-cli-workspace-master-key',
65+
),
66+
);
67+
});
68+
});
2969
});

workspace-server/src/utils/logger.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
import * as fs from 'node:fs/promises';
88
import * as path from 'node:path';
9-
import { PROJECT_ROOT } from './paths';
9+
import { STATE_DIR } from './paths';
1010

11-
const logFilePath = path.join(PROJECT_ROOT, 'logs', 'server.log');
11+
const logFilePath = path.join(STATE_DIR, 'logs', 'server.log');
1212

1313
async function ensureLogDirectoryExists() {
1414
try {

workspace-server/src/utils/paths.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,23 @@ function findProjectRoot(): string {
2222

2323
// Construct an absolute path to the project root.
2424
export const PROJECT_ROOT = findProjectRoot();
25+
26+
/**
27+
* Directory for mutable server state (encrypted token, master key, logs).
28+
*
29+
* Defaults to PROJECT_ROOT (the historical behavior, suitable when the
30+
* extension runs from a user-writable checkout). Deployments that install
31+
* the package in a read-only location (e.g. a system-wide install owned by
32+
* root while the server runs as an unprivileged user) can point state at a
33+
* writable directory via the WORKSPACE_STATE_DIR environment variable.
34+
*/
35+
export const STATE_DIR = process.env['WORKSPACE_STATE_DIR'] || PROJECT_ROOT;
36+
2537
export const ENCRYPTED_TOKEN_PATH = path.join(
26-
PROJECT_ROOT,
38+
STATE_DIR,
2739
'gemini-cli-workspace-token.json',
2840
);
2941
export const ENCRYPTION_MASTER_KEY_PATH = path.join(
30-
PROJECT_ROOT,
42+
STATE_DIR,
3143
'.gemini-cli-workspace-master-key',
3244
);

0 commit comments

Comments
 (0)