Skip to content

Commit ded5873

Browse files
shrutip90jacob314
andauthored
feat: Show untrusted status in the Footer (google-gemini#6210)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
1 parent 04abc24 commit ded5873

10 files changed

Lines changed: 219 additions & 58 deletions

File tree

packages/cli/src/config/config.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1809,7 +1809,14 @@ describe('loadCliConfig trustedFolder', () => {
18091809
description,
18101810
} of testCases) {
18111811
it(`should be correct for: ${description}`, async () => {
1812-
(isWorkspaceTrusted as vi.Mock).mockReturnValue(mockTrustValue);
1812+
(isWorkspaceTrusted as vi.Mock).mockImplementation(
1813+
(settings: Settings) => {
1814+
const featureIsEnabled =
1815+
(settings.folderTrustFeature ?? false) &&
1816+
(settings.folderTrust ?? true);
1817+
return featureIsEnabled ? mockTrustValue : true;
1818+
},
1819+
);
18131820
const argv = await parseArguments();
18141821
const settings: Settings = { folderTrustFeature, folderTrust };
18151822
const config = await loadCliConfig(settings, [], 'test-session', argv);

packages/cli/src/config/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ export async function loadCliConfig(
321321
const folderTrustFeature = settings.folderTrustFeature ?? false;
322322
const folderTrustSetting = settings.folderTrust ?? true;
323323
const folderTrust = folderTrustFeature && folderTrustSetting;
324-
const trustedFolder = folderTrust ? isWorkspaceTrusted() : true;
324+
const trustedFolder = isWorkspaceTrusted(settings);
325325

326326
const allExtensions = annotateActiveExtensions(
327327
extensions,

packages/cli/src/config/trustedFolders.test.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
TrustLevel,
3636
isWorkspaceTrusted,
3737
} from './trustedFolders.js';
38+
import { Settings } from './settings.js';
3839

3940
vi.mock('fs', async (importOriginal) => {
4041
const actualFs = await importOriginal<typeof fs>();
@@ -130,6 +131,10 @@ describe('Trusted Folders Loading', () => {
130131
describe('isWorkspaceTrusted', () => {
131132
let mockCwd: string;
132133
const mockRules: Record<string, TrustLevel> = {};
134+
const mockSettings: Settings = {
135+
folderTrustFeature: true,
136+
folderTrust: true,
137+
};
133138

134139
beforeEach(() => {
135140
vi.spyOn(process, 'cwd').mockImplementation(() => mockCwd);
@@ -153,51 +158,51 @@ describe('isWorkspaceTrusted', () => {
153158
it('should return true for a directly trusted folder', () => {
154159
mockCwd = '/home/user/projectA';
155160
mockRules['/home/user/projectA'] = TrustLevel.TRUST_FOLDER;
156-
expect(isWorkspaceTrusted()).toBe(true);
161+
expect(isWorkspaceTrusted(mockSettings)).toBe(true);
157162
});
158163

159164
it('should return true for a child of a trusted folder', () => {
160165
mockCwd = '/home/user/projectA/src';
161166
mockRules['/home/user/projectA'] = TrustLevel.TRUST_FOLDER;
162-
expect(isWorkspaceTrusted()).toBe(true);
167+
expect(isWorkspaceTrusted(mockSettings)).toBe(true);
163168
});
164169

165170
it('should return true for a child of a trusted parent folder', () => {
166171
mockCwd = '/home/user/projectB';
167172
mockRules['/home/user/projectB/somefile.txt'] = TrustLevel.TRUST_PARENT;
168-
expect(isWorkspaceTrusted()).toBe(true);
173+
expect(isWorkspaceTrusted(mockSettings)).toBe(true);
169174
});
170175

171176
it('should return false for a directly untrusted folder', () => {
172177
mockCwd = '/home/user/untrusted';
173178
mockRules['/home/user/untrusted'] = TrustLevel.DO_NOT_TRUST;
174-
expect(isWorkspaceTrusted()).toBe(false);
179+
expect(isWorkspaceTrusted(mockSettings)).toBe(false);
175180
});
176181

177182
it('should return undefined for a child of an untrusted folder', () => {
178183
mockCwd = '/home/user/untrusted/src';
179184
mockRules['/home/user/untrusted'] = TrustLevel.DO_NOT_TRUST;
180-
expect(isWorkspaceTrusted()).toBeUndefined();
185+
expect(isWorkspaceTrusted(mockSettings)).toBeUndefined();
181186
});
182187

183188
it('should return undefined when no rules match', () => {
184189
mockCwd = '/home/user/other';
185190
mockRules['/home/user/projectA'] = TrustLevel.TRUST_FOLDER;
186191
mockRules['/home/user/untrusted'] = TrustLevel.DO_NOT_TRUST;
187-
expect(isWorkspaceTrusted()).toBeUndefined();
192+
expect(isWorkspaceTrusted(mockSettings)).toBeUndefined();
188193
});
189194

190195
it('should prioritize trust over distrust', () => {
191196
mockCwd = '/home/user/projectA/untrusted';
192197
mockRules['/home/user/projectA'] = TrustLevel.TRUST_FOLDER;
193198
mockRules['/home/user/projectA/untrusted'] = TrustLevel.DO_NOT_TRUST;
194-
expect(isWorkspaceTrusted()).toBe(true);
199+
expect(isWorkspaceTrusted(mockSettings)).toBe(true);
195200
});
196201

197202
it('should handle path normalization', () => {
198203
mockCwd = '/home/user/projectA';
199204
mockRules[`/home/user/../user/${path.basename('/home/user/projectA')}`] =
200205
TrustLevel.TRUST_FOLDER;
201-
expect(isWorkspaceTrusted()).toBe(true);
206+
expect(isWorkspaceTrusted(mockSettings)).toBe(true);
202207
});
203208
});

packages/cli/src/config/trustedFolders.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import * as fs from 'fs';
88
import * as path from 'path';
99
import { homedir } from 'os';
1010
import { getErrorMessage, isWithinRoot } from '@google/gemini-cli-core';
11+
import { Settings } from './settings.js';
1112
import stripJsonComments from 'strip-json-comments';
1213

1314
export const TRUSTED_FOLDERS_FILENAME = 'trustedFolders.json';
@@ -109,7 +110,15 @@ export function saveTrustedFolders(
109110
}
110111
}
111112

112-
export function isWorkspaceTrusted(): boolean | undefined {
113+
export function isWorkspaceTrusted(settings: Settings): boolean | undefined {
114+
const folderTrustFeature = settings.folderTrustFeature ?? false;
115+
const folderTrustSetting = settings.folderTrust ?? true;
116+
const folderTrustEnabled = folderTrustFeature && folderTrustSetting;
117+
118+
if (!folderTrustEnabled) {
119+
return true;
120+
}
121+
113122
const { rules, errors } = loadTrustedFolders();
114123

115124
if (errors.length > 0) {

packages/cli/src/ui/App.test.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
163163
getCurrentIde: vi.fn(() => 'vscode'),
164164
getDetectedIdeDisplayName: vi.fn(() => 'VSCode'),
165165
})),
166+
isTrustedFolder: vi.fn(() => true),
166167
};
167168
});
168169

@@ -1118,5 +1119,45 @@ describe('App UI', () => {
11181119
await Promise.resolve();
11191120
expect(lastFrame()).toContain('Do you trust this folder?');
11201121
});
1122+
1123+
it('should display the folder trust dialog when the feature is enabled but the folder is not trusted', async () => {
1124+
const { useFolderTrust } = await import('./hooks/useFolderTrust.js');
1125+
vi.mocked(useFolderTrust).mockReturnValue({
1126+
isFolderTrustDialogOpen: true,
1127+
handleFolderTrustSelect: vi.fn(),
1128+
});
1129+
mockConfig.isTrustedFolder.mockReturnValue(false);
1130+
1131+
const { lastFrame, unmount } = render(
1132+
<App
1133+
config={mockConfig as unknown as ServerConfig}
1134+
settings={mockSettings}
1135+
version={mockVersion}
1136+
/>,
1137+
);
1138+
currentUnmount = unmount;
1139+
await Promise.resolve();
1140+
expect(lastFrame()).toContain('Do you trust this folder?');
1141+
});
1142+
1143+
it('should not display the folder trust dialog when the feature is disabled', async () => {
1144+
const { useFolderTrust } = await import('./hooks/useFolderTrust.js');
1145+
vi.mocked(useFolderTrust).mockReturnValue({
1146+
isFolderTrustDialogOpen: false,
1147+
handleFolderTrustSelect: vi.fn(),
1148+
});
1149+
mockConfig.isTrustedFolder.mockReturnValue(false);
1150+
1151+
const { lastFrame, unmount } = render(
1152+
<App
1153+
config={mockConfig as unknown as ServerConfig}
1154+
settings={mockSettings}
1155+
version={mockVersion}
1156+
/>,
1157+
);
1158+
currentUnmount = unmount;
1159+
await Promise.resolve();
1160+
expect(lastFrame()).not.toContain('Do you trust this folder?');
1161+
});
11211162
});
11221163
});

packages/cli/src/ui/App.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
171171
const [editorError, setEditorError] = useState<string | null>(null);
172172
const [footerHeight, setFooterHeight] = useState<number>(0);
173173
const [corgiMode, setCorgiMode] = useState(false);
174+
const [isTrustedFolderState, setIsTrustedFolder] = useState(
175+
config.isTrustedFolder(),
176+
);
174177
const [currentModel, setCurrentModel] = useState(config.getModel());
175178
const [shellModeActive, setShellModeActive] = useState(false);
176179
const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false);
@@ -254,7 +257,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
254257

255258
const { isFolderTrustDialogOpen, handleFolderTrustSelect } = useFolderTrust(
256259
settings,
257-
config,
260+
setIsTrustedFolder,
258261
);
259262

260263
const {
@@ -1198,6 +1201,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
11981201
promptTokenCount={sessionStats.lastPromptTokenCount}
11991202
nightly={nightly}
12001203
vimMode={vimModeEnabled ? vimMode : undefined}
1204+
isTrustedFolder={isTrustedFolderState}
12011205
/>
12021206
</Box>
12031207
</Box>

packages/cli/src/ui/components/Footer.test.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,57 @@ describe('<Footer />', () => {
103103
expect(lastFrame()).toContain(defaultProps.model);
104104
expect(lastFrame()).toMatch(/\(\d+% context[\s\S]*left\)/);
105105
});
106+
107+
describe('sandbox and trust info', () => {
108+
it('should display untrusted when isTrustedFolder is false', () => {
109+
const { lastFrame } = renderWithWidth(120, {
110+
...defaultProps,
111+
isTrustedFolder: false,
112+
});
113+
expect(lastFrame()).toContain('untrusted');
114+
});
115+
116+
it('should display custom sandbox info when SANDBOX env is set', () => {
117+
vi.stubEnv('SANDBOX', 'gemini-cli-test-sandbox');
118+
const { lastFrame } = renderWithWidth(120, {
119+
...defaultProps,
120+
isTrustedFolder: undefined,
121+
});
122+
expect(lastFrame()).toContain('test');
123+
vi.unstubAllEnvs();
124+
});
125+
126+
it('should display macOS Seatbelt info when SANDBOX is sandbox-exec', () => {
127+
vi.stubEnv('SANDBOX', 'sandbox-exec');
128+
vi.stubEnv('SEATBELT_PROFILE', 'test-profile');
129+
const { lastFrame } = renderWithWidth(120, {
130+
...defaultProps,
131+
isTrustedFolder: true,
132+
});
133+
expect(lastFrame()).toMatch(/macOS Seatbelt.*\(test-profile\)/s);
134+
vi.unstubAllEnvs();
135+
});
136+
137+
it('should display "no sandbox" when SANDBOX is not set and folder is trusted', () => {
138+
// Clear any SANDBOX env var that might be set.
139+
vi.stubEnv('SANDBOX', '');
140+
const { lastFrame } = renderWithWidth(120, {
141+
...defaultProps,
142+
isTrustedFolder: true,
143+
});
144+
expect(lastFrame()).toContain('no sandbox');
145+
vi.unstubAllEnvs();
146+
});
147+
148+
it('should prioritize untrusted message over sandbox info', () => {
149+
vi.stubEnv('SANDBOX', 'gemini-cli-test-sandbox');
150+
const { lastFrame } = renderWithWidth(120, {
151+
...defaultProps,
152+
isTrustedFolder: false,
153+
});
154+
expect(lastFrame()).toContain('untrusted');
155+
expect(lastFrame()).not.toMatch(/test-sandbox/s);
156+
vi.unstubAllEnvs();
157+
});
158+
});
106159
});

packages/cli/src/ui/components/Footer.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ interface FooterProps {
3232
promptTokenCount: number;
3333
nightly: boolean;
3434
vimMode?: string;
35+
isTrustedFolder?: boolean;
3536
}
3637

3738
export const Footer: React.FC<FooterProps> = ({
@@ -47,6 +48,7 @@ export const Footer: React.FC<FooterProps> = ({
4748
promptTokenCount,
4849
nightly,
4950
vimMode,
51+
isTrustedFolder,
5052
}) => {
5153
const { columns: terminalWidth } = useTerminalSize();
5254

@@ -90,7 +92,7 @@ export const Footer: React.FC<FooterProps> = ({
9092
)}
9193
</Box>
9294

93-
{/* Middle Section: Centered Sandbox Info */}
95+
{/* Middle Section: Centered Trust/Sandbox Info */}
9496
<Box
9597
flexGrow={isNarrow ? 0 : 1}
9698
alignItems="center"
@@ -99,7 +101,9 @@ export const Footer: React.FC<FooterProps> = ({
99101
paddingX={isNarrow ? 0 : 1}
100102
paddingTop={isNarrow ? 1 : 0}
101103
>
102-
{process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec' ? (
104+
{isTrustedFolder === false ? (
105+
<Text color={theme.status.warning}>untrusted</Text>
106+
) : process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec' ? (
103107
<Text color="green">
104108
{process.env.SANDBOX.replace(/^gemini-(?:cli-)?/, '')}
105109
</Text>

0 commit comments

Comments
 (0)