mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-02 05:01:04 -07:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 287c167eab | |||
| ae123c547c | |||
| c2705e8332 | |||
| 9574855435 |
@@ -1,6 +1,6 @@
|
||||
# Gemini 3 Pro and Gemini 3 Flash on Gemini CLI
|
||||
|
||||
Gemini 3 Pro and Gemini 3 Flash are available on Gemini CLI for all users!
|
||||
Learn about how you can use Gemini 3 Pro and Gemini 3 Flash on Gemini CLI.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
|
||||
@@ -178,9 +178,10 @@ describe('GeminiAgent', () => {
|
||||
isPlanEnabled: vi.fn().mockReturnValue(true),
|
||||
getGemini31LaunchedSync: vi.fn().mockReturnValue(false),
|
||||
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
|
||||
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
|
||||
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
|
||||
validatePathAccess: vi.fn().mockReturnValue(null),
|
||||
getExtensions: vi.fn().mockReturnValue([]),
|
||||
isTrustedFolder: vi.fn().mockReturnValue(true),
|
||||
getWorkspaceContext: vi.fn().mockReturnValue({
|
||||
addReadOnlyPath: vi.fn(),
|
||||
}),
|
||||
@@ -198,6 +199,9 @@ describe('GeminiAgent', () => {
|
||||
setClientName: vi.fn(),
|
||||
},
|
||||
setClientName: vi.fn(),
|
||||
getSkillManager: vi.fn().mockReturnValue({
|
||||
discoverSkills: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
get config() {
|
||||
return this;
|
||||
},
|
||||
@@ -510,6 +514,27 @@ describe('GeminiAgent', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should create a new session with additional roots', async () => {
|
||||
const _meta = { additionalRoots: ['/extra/path'] };
|
||||
|
||||
await agent.newSession({
|
||||
cwd: '/tmp',
|
||||
mcpServers: [],
|
||||
_meta,
|
||||
} as acp.NewSessionRequest & { _meta?: Record<string, unknown> });
|
||||
|
||||
expect(loadCliConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
context: expect.objectContaining({
|
||||
includeDirectories: expect.arrayContaining(['/extra/path']),
|
||||
}),
|
||||
}),
|
||||
'test-session-id',
|
||||
mockArgv,
|
||||
{ cwd: '/tmp' },
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle authentication failure gracefully', async () => {
|
||||
mockConfig.refreshAuth.mockRejectedValue(new Error('Auth failed'));
|
||||
const debugSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
@@ -579,6 +604,51 @@ describe('GeminiAgent', () => {
|
||||
expect(result).toMatchObject({ stopReason: 'end_turn' });
|
||||
});
|
||||
|
||||
it('should support additional roots mid-session in prompt', async () => {
|
||||
const _meta = { additionalRoots: ['/extra/path'] };
|
||||
|
||||
// Mock chat.sendMessageStream to avoid crash
|
||||
const mockChatInTest = {
|
||||
sendMessageStream: vi
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
createMockStream([
|
||||
{
|
||||
type: StreamEventType.CHUNK,
|
||||
value: { candidates: [{ content: { parts: [{ text: 'Hi' }] } }] },
|
||||
},
|
||||
]),
|
||||
),
|
||||
};
|
||||
(
|
||||
mockConfig.getGeminiClient().startChat as unknown as import('vitest').Mock
|
||||
).mockResolvedValue(mockChatInTest);
|
||||
|
||||
await agent.newSession({ cwd: '/tmp', mcpServers: [] });
|
||||
const session = (
|
||||
agent as unknown as { sessions: Map<string, Session> }
|
||||
).sessions.get('test-session-id');
|
||||
if (!session) throw new Error('Session not found');
|
||||
|
||||
// Stub addDirectories and getDirectories on workspaceContext
|
||||
const mockWorkspaceContext = mockConfig.getWorkspaceContext();
|
||||
mockWorkspaceContext.addDirectories = vi.fn();
|
||||
mockWorkspaceContext.getDirectories = vi
|
||||
.fn()
|
||||
.mockReturnValue(['/tmp', '/extra/path']);
|
||||
|
||||
await agent.prompt({
|
||||
sessionId: 'test-session-id',
|
||||
prompt: [],
|
||||
_meta,
|
||||
} as acp.PromptRequest & { _meta?: Record<string, unknown> });
|
||||
|
||||
expect(mockWorkspaceContext.addDirectories).toHaveBeenCalledWith([
|
||||
'/extra/path',
|
||||
]);
|
||||
expect(mockConfig.getSkillManager().discoverSkills).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delegate setMode to session', async () => {
|
||||
await agent.newSession({ cwd: '/tmp', mcpServers: [] });
|
||||
const session = (
|
||||
|
||||
@@ -259,10 +259,21 @@ export class GeminiAgent {
|
||||
);
|
||||
}
|
||||
|
||||
async newSession({
|
||||
cwd,
|
||||
mcpServers,
|
||||
}: acp.NewSessionRequest): Promise<acp.NewSessionResponse> {
|
||||
async newSession(
|
||||
req: acp.NewSessionRequest,
|
||||
): Promise<acp.NewSessionResponse> {
|
||||
const { cwd, mcpServers } = req;
|
||||
const meta = hasMeta(req) ? req._meta : undefined;
|
||||
const additionalRootsRaw = meta?.['additionalRoots'];
|
||||
const additionalRoots: string[] = [];
|
||||
if (Array.isArray(additionalRootsRaw)) {
|
||||
for (const root of additionalRootsRaw) {
|
||||
if (typeof root === 'string') {
|
||||
additionalRoots.push(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sessionId = randomUUID();
|
||||
const loadedSettings = loadSettings(cwd);
|
||||
const config = await this.newSessionConfig(
|
||||
@@ -270,6 +281,7 @@ export class GeminiAgent {
|
||||
cwd,
|
||||
mcpServers,
|
||||
loadedSettings,
|
||||
additionalRoots,
|
||||
);
|
||||
|
||||
const authType =
|
||||
@@ -473,6 +485,7 @@ export class GeminiAgent {
|
||||
cwd: string,
|
||||
mcpServers: acp.McpServer[],
|
||||
loadedSettings?: LoadedSettings,
|
||||
additionalRoots: string[] = [],
|
||||
): Promise<Config> {
|
||||
const currentSettings = loadedSettings || this.settings;
|
||||
const mergedMcpServers = { ...currentSettings.merged.mcpServers };
|
||||
@@ -513,6 +526,13 @@ export class GeminiAgent {
|
||||
const settings = {
|
||||
...currentSettings.merged,
|
||||
mcpServers: mergedMcpServers,
|
||||
context: {
|
||||
...currentSettings.merged.context,
|
||||
includeDirectories: [
|
||||
...(currentSettings.merged.context?.includeDirectories || []),
|
||||
...additionalRoots,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const config = await loadCliConfig(settings, sessionId, this.argv, { cwd });
|
||||
@@ -693,6 +713,29 @@ export class Session {
|
||||
const pendingSend = new AbortController();
|
||||
this.pendingPrompt = pendingSend;
|
||||
|
||||
const meta = hasMeta(params) ? params._meta : undefined;
|
||||
const additionalRootsRaw = meta?.['additionalRoots'];
|
||||
const additionalRoots: string[] = [];
|
||||
if (Array.isArray(additionalRootsRaw)) {
|
||||
for (const root of additionalRootsRaw) {
|
||||
if (typeof root === 'string') {
|
||||
additionalRoots.push(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (additionalRoots.length > 0) {
|
||||
this.context.config.getWorkspaceContext().addDirectories(additionalRoots);
|
||||
await this.context.config
|
||||
.getSkillManager()
|
||||
.discoverSkills(
|
||||
this.context.config.storage,
|
||||
this.context.config.getExtensions(),
|
||||
this.context.config.isTrustedFolder(),
|
||||
[...this.context.config.getWorkspaceContext().getDirectories()],
|
||||
);
|
||||
}
|
||||
|
||||
await this.context.config.waitForMcpInit();
|
||||
|
||||
const promptId = Math.random().toString(16).slice(2);
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { StatusRow } from './StatusRow.js';
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import { useComposerStatus } from '../hooks/useComposerStatus.js';
|
||||
import { type UIState } from '../contexts/UIStateContext.js';
|
||||
import { type TextBuffer } from '../components/shared/text-buffer.js';
|
||||
import { type SessionStatsState } from '../contexts/SessionContext.js';
|
||||
import { type ThoughtSummary } from '../types.js';
|
||||
import { ApprovalMode } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('../hooks/useComposerStatus.js', () => ({
|
||||
useComposerStatus: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('<StatusRow />', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const defaultUiState: Partial<UIState> = {
|
||||
currentTip: undefined,
|
||||
thought: null,
|
||||
elapsedTime: 0,
|
||||
currentWittyPhrase: undefined,
|
||||
activeHooks: [],
|
||||
buffer: { text: '' } as unknown as TextBuffer,
|
||||
sessionStats: { lastPromptTokenCount: 0 } as unknown as SessionStatsState,
|
||||
shortcutsHelpVisible: false,
|
||||
contextFileNames: [],
|
||||
showApprovalModeIndicator: ApprovalMode.DEFAULT,
|
||||
allowPlanMode: false,
|
||||
shellModeActive: false,
|
||||
renderMarkdown: true,
|
||||
currentModel: 'gemini-3',
|
||||
};
|
||||
|
||||
it('renders status and tip correctly when they both fit', async () => {
|
||||
(useComposerStatus as Mock).mockReturnValue({
|
||||
isInteractiveShellWaiting: false,
|
||||
showLoadingIndicator: true,
|
||||
showTips: true,
|
||||
showWit: true,
|
||||
modeContentObj: null,
|
||||
showMinimalContext: false,
|
||||
});
|
||||
|
||||
const uiState: Partial<UIState> = {
|
||||
...defaultUiState,
|
||||
currentTip: 'Test Tip',
|
||||
thought: { subject: 'Thinking...' } as unknown as ThoughtSummary,
|
||||
elapsedTime: 5,
|
||||
currentWittyPhrase: 'I am witty',
|
||||
};
|
||||
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
<StatusRow
|
||||
showUiDetails={false}
|
||||
isNarrow={false}
|
||||
terminalWidth={100}
|
||||
hideContextSummary={false}
|
||||
hideUiDetailsForSuggestions={false}
|
||||
hasPendingActionRequired={false}
|
||||
/>,
|
||||
{
|
||||
width: 100,
|
||||
uiState,
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Thinking...');
|
||||
expect(output).toContain('I am witty');
|
||||
expect(output).toContain('Tip: Test Tip');
|
||||
});
|
||||
|
||||
it('renders correctly when interactive shell is waiting', async () => {
|
||||
(useComposerStatus as Mock).mockReturnValue({
|
||||
isInteractiveShellWaiting: true,
|
||||
showLoadingIndicator: false,
|
||||
showTips: false,
|
||||
showWit: false,
|
||||
modeContentObj: null,
|
||||
showMinimalContext: false,
|
||||
});
|
||||
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
<StatusRow
|
||||
showUiDetails={true}
|
||||
isNarrow={false}
|
||||
terminalWidth={100}
|
||||
hideContextSummary={false}
|
||||
hideUiDetailsForSuggestions={false}
|
||||
hasPendingActionRequired={false}
|
||||
/>,
|
||||
{
|
||||
width: 100,
|
||||
uiState: defaultUiState,
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('! Shell awaiting input (Tab to focus)');
|
||||
});
|
||||
|
||||
it('renders tip with absolute positioning when it fits but might collide (verification of container logic)', async () => {
|
||||
(useComposerStatus as Mock).mockReturnValue({
|
||||
isInteractiveShellWaiting: false,
|
||||
showLoadingIndicator: true,
|
||||
showTips: true,
|
||||
showWit: true,
|
||||
modeContentObj: null,
|
||||
showMinimalContext: false,
|
||||
});
|
||||
|
||||
const uiState: Partial<UIState> = {
|
||||
...defaultUiState,
|
||||
currentTip: 'Test Tip',
|
||||
};
|
||||
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
<StatusRow
|
||||
showUiDetails={false}
|
||||
isNarrow={false}
|
||||
terminalWidth={100}
|
||||
hideContextSummary={false}
|
||||
hideUiDetailsForSuggestions={false}
|
||||
hasPendingActionRequired={false}
|
||||
/>,
|
||||
{
|
||||
width: 100,
|
||||
uiState,
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Tip: Test Tip');
|
||||
});
|
||||
});
|
||||
@@ -179,7 +179,13 @@ export const StatusRow: React.FC<StatusRowProps> = ({
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
if (entry) {
|
||||
setTipWidth(Math.round(entry.contentRect.width));
|
||||
const width = Math.round(entry.contentRect.width);
|
||||
// Only update if width > 0 to prevent layout feedback loops
|
||||
// when the tip is hidden. This ensures we always use the
|
||||
// intrinsic width for collision detection.
|
||||
if (width > 0) {
|
||||
setTipWidth(width);
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(node);
|
||||
@@ -230,6 +236,10 @@ export const StatusRow: React.FC<StatusRowProps> = ({
|
||||
const showRow1 = showUiDetails || showRow1Minimal;
|
||||
const showRow2 = showUiDetails || showRow2Minimal;
|
||||
|
||||
const onStatusResize = useCallback((width: number) => {
|
||||
if (width > 0) setStatusWidth(width);
|
||||
}, []);
|
||||
|
||||
const statusNode = (
|
||||
<StatusNode
|
||||
showTips={showTips}
|
||||
@@ -242,7 +252,7 @@ export const StatusRow: React.FC<StatusRowProps> = ({
|
||||
errorVerbosity={
|
||||
settings.merged.ui.errorVerbosity as 'low' | 'full' | undefined
|
||||
}
|
||||
onResize={setStatusWidth}
|
||||
onResize={onStatusResize}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -322,20 +332,23 @@ export const StatusRow: React.FC<StatusRowProps> = ({
|
||||
|
||||
<Box
|
||||
flexShrink={0}
|
||||
marginLeft={LAYOUT.TIP_LEFT_MARGIN}
|
||||
marginLeft={showTipLine ? LAYOUT.TIP_LEFT_MARGIN : 0}
|
||||
marginRight={
|
||||
isNarrow
|
||||
? LAYOUT.TIP_RIGHT_MARGIN_NARROW
|
||||
: LAYOUT.TIP_RIGHT_MARGIN_WIDE
|
||||
showTipLine
|
||||
? isNarrow
|
||||
? LAYOUT.TIP_RIGHT_MARGIN_NARROW
|
||||
: LAYOUT.TIP_RIGHT_MARGIN_WIDE
|
||||
: 0
|
||||
}
|
||||
position={showTipLine ? 'relative' : 'absolute'}
|
||||
{...(showTipLine ? {} : { top: -100, left: -100 })}
|
||||
>
|
||||
{/*
|
||||
We always render the tip node so it can be measured by ResizeObserver,
|
||||
but we control its visibility based on the collision detection.
|
||||
We always render the tip node so it can be measured by ResizeObserver.
|
||||
When hidden, we use absolute positioning so it can still be measured
|
||||
but doesn't affect the layout of Row 1. This prevents layout loops.
|
||||
*/}
|
||||
<Box display={showTipLine ? 'flex' : 'none'}>
|
||||
{!isNarrow && tipContentStr && renderTipNode()}
|
||||
</Box>
|
||||
{!isNarrow && tipContentStr && renderTipNode()}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -1413,6 +1413,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.storage,
|
||||
this.getExtensions(),
|
||||
this.isTrustedFolder(),
|
||||
[...this.workspaceContext.getDirectories()],
|
||||
);
|
||||
this.getSkillManager().setDisabledSkills(this.disabledSkills);
|
||||
|
||||
@@ -3111,6 +3112,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.storage,
|
||||
this.getExtensions(),
|
||||
this.isTrustedFolder(),
|
||||
[...this.workspaceContext.getDirectories()],
|
||||
);
|
||||
this.getSkillManager().setDisabledSkills(this.disabledSkills);
|
||||
|
||||
|
||||
@@ -7,13 +7,14 @@ allowOverrides = false
|
||||
[modes.default]
|
||||
network = false
|
||||
readonly = true
|
||||
approvedTools = []
|
||||
approvedTools = ['cat', 'ls', 'grep', 'head', 'tail', 'less', 'Get-Content', 'dir', 'type', 'findstr', 'Get-ChildItem', 'echo']
|
||||
allowOverrides = true
|
||||
|
||||
[modes.accepting_edits]
|
||||
network = false
|
||||
readonly = false
|
||||
approvedTools = ['sed', 'grep', 'awk', 'perl', 'cat', 'echo']
|
||||
approvedTools = ['sed', 'grep', 'awk', 'perl', 'cat', 'echo', 'Add-Content', 'Set-Content']
|
||||
allowOverrides = true
|
||||
|
||||
[commands]
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ export class LinuxSandboxManager implements SandboxManager {
|
||||
: false;
|
||||
const workspaceWrite = !isReadonlyMode || isApproved;
|
||||
const networkAccess =
|
||||
this.options.modeConfig?.network ?? req.policy?.networkAccess ?? false;
|
||||
this.options.modeConfig?.network || req.policy?.networkAccess || false;
|
||||
|
||||
const persistentPermissions = allowOverrides
|
||||
? this.options.policyManager?.getCommandPermissions(commandName)
|
||||
|
||||
@@ -78,7 +78,7 @@ export class MacOsSandboxManager implements SandboxManager {
|
||||
|
||||
const workspaceWrite = !isReadonlyMode || isApproved;
|
||||
const defaultNetwork =
|
||||
this.options.modeConfig?.network ?? req.policy?.networkAccess ?? false;
|
||||
this.options.modeConfig?.network || req.policy?.networkAccess || false;
|
||||
|
||||
// Fetch persistent approvals for this command
|
||||
const commandName = await getCommandName(req.command, req.args);
|
||||
|
||||
@@ -58,6 +58,13 @@ public class GeminiSandbox {
|
||||
public ulong OtherTransferCount;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
struct JOBOBJECT_NET_RATE_CONTROL_INFORMATION {
|
||||
public ulong MaxBandwidth;
|
||||
public uint ControlFlags;
|
||||
public byte DscpTag;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string lpName);
|
||||
|
||||
@@ -70,6 +77,9 @@ public class GeminiSandbox {
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
static extern bool DuplicateTokenEx(IntPtr hExistingToken, uint dwDesiredAccess, IntPtr lpTokenAttributes, uint ImpersonationLevel, uint TokenType, out IntPtr phNewToken);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
static extern bool CreateRestrictedToken(IntPtr ExistingTokenHandle, uint Flags, uint DisableSidCount, IntPtr SidsToDisable, uint DeletePrivilegeCount, IntPtr PrivilegesToDelete, uint RestrictedSidCount, IntPtr SidsToRestrict, out IntPtr NewTokenHandle);
|
||||
|
||||
@@ -143,6 +153,8 @@ public class GeminiSandbox {
|
||||
|
||||
private const int TokenIntegrityLevel = 25;
|
||||
private const uint SE_GROUP_INTEGRITY = 0x00000020;
|
||||
private const uint TOKEN_ALL_ACCESS = 0xF01FF;
|
||||
private const uint DISABLE_MAX_PRIVILEGE = 0x1;
|
||||
|
||||
static int Main(string[] args) {
|
||||
if (args.Length < 3) {
|
||||
@@ -182,14 +194,14 @@ public class GeminiSandbox {
|
||||
IntPtr lowIntegritySid = IntPtr.Zero;
|
||||
|
||||
try {
|
||||
// 1. Create Restricted Token
|
||||
if (!OpenProcessToken(GetCurrentProcess(), 0x0002 /* TOKEN_DUPLICATE */ | 0x0008 /* TOKEN_QUERY */ | 0x0080 /* TOKEN_ADJUST_DEFAULT */, out hToken)) {
|
||||
// 1. Duplicate Primary Token
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, out hToken)) {
|
||||
Console.WriteLine("Error: OpenProcessToken failed (" + Marshal.GetLastWin32Error() + ")");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Flags: 0x1 (DISABLE_MAX_PRIVILEGE)
|
||||
if (!CreateRestrictedToken(hToken, 1, 0, IntPtr.Zero, 0, IntPtr.Zero, 0, IntPtr.Zero, out hRestrictedToken)) {
|
||||
// Create a restricted token to strip administrative privileges
|
||||
if (!CreateRestrictedToken(hToken, DISABLE_MAX_PRIVILEGE, 0, IntPtr.Zero, 0, IntPtr.Zero, 0, IntPtr.Zero, out hRestrictedToken)) {
|
||||
Console.WriteLine("Error: CreateRestrictedToken failed (" + Marshal.GetLastWin32Error() + ")");
|
||||
return 1;
|
||||
}
|
||||
@@ -223,6 +235,18 @@ public class GeminiSandbox {
|
||||
SetInformationJobObject(hJob, 9 /* JobObjectExtendedLimitInformation */, lpJobLimits, (uint)Marshal.SizeOf(jobLimits));
|
||||
Marshal.FreeHGlobal(lpJobLimits);
|
||||
|
||||
if (!networkAccess) {
|
||||
JOBOBJECT_NET_RATE_CONTROL_INFORMATION netLimits = new JOBOBJECT_NET_RATE_CONTROL_INFORMATION();
|
||||
netLimits.MaxBandwidth = 1;
|
||||
netLimits.ControlFlags = 0x1 | 0x2; // ENABLE | MAX_BANDWIDTH
|
||||
netLimits.DscpTag = 0;
|
||||
|
||||
IntPtr lpNetLimits = Marshal.AllocHGlobal(Marshal.SizeOf(netLimits));
|
||||
Marshal.StructureToPtr(netLimits, lpNetLimits, false);
|
||||
SetInformationJobObject(hJob, 32 /* JobObjectNetRateControlInformation */, lpNetLimits, (uint)Marshal.SizeOf(netLimits));
|
||||
Marshal.FreeHGlobal(lpNetLimits);
|
||||
}
|
||||
|
||||
// 4. Handle Internal Commands or External Process
|
||||
if (command == "__read") {
|
||||
if (argIndex + 1 >= args.Length) {
|
||||
|
||||
@@ -158,7 +158,7 @@ describe('WindowsSandboxManager', () => {
|
||||
expect(icaclsArgs).toContainEqual([
|
||||
persistentPath,
|
||||
'/setintegritylevel',
|
||||
'Low',
|
||||
'(OI)(CI)Low',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -227,13 +227,13 @@ describe('WindowsSandboxManager', () => {
|
||||
expect(icaclsArgs).toContainEqual([
|
||||
path.resolve(testCwd),
|
||||
'/setintegritylevel',
|
||||
'Low',
|
||||
'(OI)(CI)Low',
|
||||
]);
|
||||
|
||||
expect(icaclsArgs).toContainEqual([
|
||||
path.resolve(allowedPath),
|
||||
'/setintegritylevel',
|
||||
'Low',
|
||||
'(OI)(CI)Low',
|
||||
]);
|
||||
} finally {
|
||||
fs.rmSync(allowedPath, { recursive: true, force: true });
|
||||
@@ -273,7 +273,7 @@ describe('WindowsSandboxManager', () => {
|
||||
expect(icaclsArgs).toContainEqual([
|
||||
path.resolve(extraWritePath),
|
||||
'/setintegritylevel',
|
||||
'Low',
|
||||
'(OI)(CI)Low',
|
||||
]);
|
||||
} finally {
|
||||
fs.rmSync(extraWritePath, { recursive: true, force: true });
|
||||
@@ -308,7 +308,7 @@ describe('WindowsSandboxManager', () => {
|
||||
expect(icaclsArgs).not.toContainEqual([
|
||||
uncPath,
|
||||
'/setintegritylevel',
|
||||
'Low',
|
||||
'(OI)(CI)Low',
|
||||
]);
|
||||
},
|
||||
);
|
||||
@@ -343,12 +343,12 @@ describe('WindowsSandboxManager', () => {
|
||||
expect(icaclsArgs).toContainEqual([
|
||||
longPath,
|
||||
'/setintegritylevel',
|
||||
'Low',
|
||||
'(OI)(CI)Low',
|
||||
]);
|
||||
expect(icaclsArgs).toContainEqual([
|
||||
devicePath,
|
||||
'/setintegritylevel',
|
||||
'Low',
|
||||
'(OI)(CI)Low',
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -236,6 +236,10 @@ export class WindowsSandboxManager implements SandboxManager {
|
||||
false,
|
||||
};
|
||||
|
||||
const defaultNetwork =
|
||||
this.options.modeConfig?.network || req.policy?.networkAccess || false;
|
||||
const networkAccess = defaultNetwork || mergedAdditional.network;
|
||||
|
||||
// 1. Handle filesystem permissions for Low Integrity
|
||||
// Grant "Low Mandatory Level" write access to the workspace.
|
||||
// If not in readonly mode OR it's a strictly approved pipeline, allow workspace writes
|
||||
@@ -251,7 +255,7 @@ export class WindowsSandboxManager implements SandboxManager {
|
||||
await this.grantLowIntegrityAccess(this.options.workspace);
|
||||
}
|
||||
|
||||
// Grant "Low Mandatory Level" read access to allowedPaths.
|
||||
// Grant "Low Mandatory Level" read/write access to allowedPaths.
|
||||
const allowedPaths = sanitizePaths(req.policy?.allowedPaths) || [];
|
||||
for (const allowedPath of allowedPaths) {
|
||||
await this.grantLowIntegrityAccess(allowedPath);
|
||||
@@ -342,10 +346,6 @@ export class WindowsSandboxManager implements SandboxManager {
|
||||
// GeminiSandbox.exe <network:0|1> <cwd> --forbidden-manifest <path> <command> [args...]
|
||||
const program = this.helperPath;
|
||||
|
||||
const defaultNetwork =
|
||||
this.options.modeConfig?.network ?? req.policy?.networkAccess ?? false;
|
||||
const networkAccess = defaultNetwork || mergedAdditional.network;
|
||||
|
||||
const args = [
|
||||
networkAccess ? '1' : '0',
|
||||
req.cwd,
|
||||
@@ -395,7 +395,11 @@ export class WindowsSandboxManager implements SandboxManager {
|
||||
}
|
||||
|
||||
try {
|
||||
await spawnAsync('icacls', [resolvedPath, '/setintegritylevel', 'Low']);
|
||||
await spawnAsync('icacls', [
|
||||
resolvedPath,
|
||||
'/setintegritylevel',
|
||||
'(OI)(CI)Low',
|
||||
]);
|
||||
this.allowedCache.add(resolvedPath);
|
||||
} catch (e) {
|
||||
debugLogger.log(
|
||||
|
||||
@@ -385,5 +385,14 @@ describe('SandboxManager', () => {
|
||||
expect(manager).toBeInstanceOf(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it('should return WindowsSandboxManager if sandboxing is enabled on win32', () => {
|
||||
vi.spyOn(os, 'platform').mockReturnValue('win32');
|
||||
const manager = createSandboxManager(
|
||||
{ enabled: true },
|
||||
{ workspace: '/workspace' },
|
||||
);
|
||||
expect(manager).toBeInstanceOf(WindowsSandboxManager);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,6 +48,7 @@ export class SkillManager {
|
||||
storage: Storage,
|
||||
extensions: GeminiCLIExtension[] = [],
|
||||
isTrusted: boolean = false,
|
||||
additionalRoots: string[] = [],
|
||||
): Promise<void> {
|
||||
this.clearSkills();
|
||||
|
||||
@@ -89,6 +90,26 @@ export class SkillManager {
|
||||
storage.getProjectAgentSkillsDir(),
|
||||
);
|
||||
this.addSkillsWithPrecedence(projectAgentSkills);
|
||||
|
||||
// 5. Additional roots (e.g. from includeDirectories)
|
||||
if (additionalRoots && additionalRoots.length > 0) {
|
||||
for (const root of additionalRoots) {
|
||||
// Direct search (looks for SKILL.md and */SKILL.md)
|
||||
const rootSkills = await loadSkillsFromDir(root);
|
||||
this.addSkillsWithPrecedence(rootSkills);
|
||||
|
||||
// Search in standard subdirectories inside the root
|
||||
const agentSkills = await loadSkillsFromDir(
|
||||
path.join(root, '.agents', 'skills'),
|
||||
);
|
||||
this.addSkillsWithPrecedence(agentSkills);
|
||||
|
||||
const geminiSkills = await loadSkillsFromDir(
|
||||
path.join(root, '.gemini', 'skills'),
|
||||
);
|
||||
this.addSkillsWithPrecedence(geminiSkills);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -408,7 +408,9 @@ function hasPromptCommandTransform(root: Node): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
function parseBashCommandDetails(command: string): CommandParseResult | null {
|
||||
export function parseBashCommandDetails(
|
||||
command: string,
|
||||
): CommandParseResult | null {
|
||||
if (treeSitterInitializationError) {
|
||||
debugLogger.debug(
|
||||
'Bash parser not initialized:',
|
||||
@@ -557,7 +559,19 @@ export function parseCommandDetails(
|
||||
const configuration = getShellConfiguration();
|
||||
|
||||
if (configuration.shell === 'powershell') {
|
||||
return parsePowerShellCommandDetails(command, configuration.executable);
|
||||
const result = parsePowerShellCommandDetails(
|
||||
command,
|
||||
configuration.executable,
|
||||
);
|
||||
if (!result || result.hasError) {
|
||||
// Fallback to bash parser which is usually good enough for simple commands
|
||||
// and doesn't rely on the host PowerShell environment restrictions (e.g., ConstrainedLanguage)
|
||||
const bashResult = parseBashCommandDetails(command);
|
||||
if (bashResult && !bashResult.hasError) {
|
||||
return bashResult;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (configuration.shell === 'bash') {
|
||||
|
||||
Reference in New Issue
Block a user