I'm ready to analyze Github project changes and create an exciting update summary! However, I don't see any input provided after "INPUT:" in your message.

Could you please share the Github project changes, commit history, pull requests, or release notes that you'd like me to analyze? This could include:

- Git commit logs
- Pull request descriptions
- Changelog entries
- Diff summaries
- Release notes

Once you provide the input, I'll create a clean CHANGES section with exciting 10-word bullets and relevant emojis! 🚀
This commit is contained in:
Pedram Amini
2025-12-18 00:01:17 -06:00
parent 08fd1c2732
commit ddb827ce75
3 changed files with 115 additions and 148 deletions

94
SECURITY.md Normal file
View File

@@ -0,0 +1,94 @@
# Security Policy
Thank you for your interest in the security of Maestro. We welcome contributions from security researchers and the broader community to help keep this project safe.
## Reporting a Vulnerability
### For Most Issues
Please open a [GitHub issue](https://github.com/pedramamini/Maestro/issues) with the `security` label. Include:
- A clear description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if you have one)
### For Serious/Critical Issues
If you discover a vulnerability that could cause significant harm if disclosed publicly before a fix is available, please contact us directly:
**Email:** pedram@runmaestro.ai
This allows us to develop and release a patch before public disclosure.
## Scope
### In Scope
- Maestro application code (Electron main process, renderer, preload scripts)
- IPC handler security
- Process spawning and command execution
- Local file system access
- Web server and tunnel functionality
- Authentication and session management
### Out of Scope
The following are **not** in scope for Maestro security reports:
- **AI agent vulnerabilities** - Security issues within Claude Code, OpenAI Codex, Gemini CLI, Qwen3 Coder, or other integrated agents are the responsibility of their respective maintainers
- **Upstream dependencies** - Vulnerabilities in Electron, Node.js, or npm packages should be reported to those projects (though please let us know if Maestro is using a vulnerable version)
- **Social engineering attacks**
- **Denial of service on local application**
- **Issues requiring physical access to the user's machine**
## Response Timeline
We aim to respond to security reports as soon as possible. However, please understand that Maestro is an open source side project maintained by volunteers. Until there is a larger developer community behind it, we cannot commit to specific response timelines.
What you can expect:
- Acknowledgment of your report
- Assessment of severity and impact
- A fix prioritized based on severity
- Credit in our release notes (unless you prefer to remain anonymous)
## Recognition
We appreciate security researchers who help improve Maestro. Contributors who report valid security issues will be:
- Credited in release notes and this document (with permission)
- Thanked publicly (unless anonymity is preferred)
**Security Contributors:**
- *Your name could be here!*
## Bug Bounty
There is no bug bounty program at this time. Maestro is an open source project without funding for monetary rewards. We hope the satisfaction of contributing to open source security and public recognition is sufficient motivation.
We also welcome pull requests! If you find a vulnerability and know how to fix it, PRs are greatly appreciated.
## Known Security Considerations
The following are known aspects of Maestro's design that users should be aware of:
### Process Execution
Maestro spawns AI agents and terminal processes with the same privileges as the user running the application. This is by design—the agents need filesystem and command access to function. Users should:
- Only run Maestro on projects they trust
- Be aware that AI agents can execute commands on your system
- Review agent actions, especially on sensitive repositories
### Local Web Server
When the web/mobile interface is enabled, Maestro runs a local web server. The Cloudflare tunnel feature can expose this externally. Users should:
- Only enable tunnels when needed
- Be aware of who has access to tunnel URLs
### IPC Security
Maestro uses Electron's IPC for communication between the main process and renderer. We follow Electron security best practices including context isolation and a minimal preload API surface.
### Sentry DSN
The Sentry DSN in the codebase is a **public secret by design**. This is standard practice for client-side error reporting—the DSN is intentionally exposed to allow error telemetry. Reporting this as a vulnerability is not necessary. We monitor for abuse and will rotate keys if needed.
## Security Best Practices in Codebase
For contributors, Maestro enforces these security patterns:
- **Always use `execFileNoThrow`** for external commands—never shell-based execution
- **Validate all IPC inputs** in main process handlers
- **Minimize preload API surface**—only expose what's necessary
- **Sanitize file paths** before filesystem operations
See [CLAUDE.md](CLAUDE.md) and [CONTRIBUTING.md](CONTRIBUTING.md) for more details.

View File

@@ -253,8 +253,8 @@ describe('SettingsModal', () => {
await vi.advanceTimersByTimeAsync(50); await vi.advanceTimersByTimeAsync(50);
}); });
// General tab content should show the Default AI Agent label // General tab content should show the Font Size label
expect(screen.getByText('Default AI Agent')).toBeInTheDocument(); expect(screen.getByText('Font Size')).toBeInTheDocument();
}); });
it('should respect initialTab prop', async () => { it('should respect initialTab prop', async () => {
@@ -326,7 +326,7 @@ describe('SettingsModal', () => {
}); });
// Start on general tab // Start on general tab
expect(screen.getByText('Default AI Agent')).toBeInTheDocument(); expect(screen.getByText('Font Size')).toBeInTheDocument();
// Press Cmd+Shift+] to go to shortcuts // Press Cmd+Shift+] to go to shortcuts
fireEvent.keyDown(window, { key: ']', metaKey: true, shiftKey: true }); fireEvent.keyDown(window, { key: ']', metaKey: true, shiftKey: true });
@@ -355,7 +355,7 @@ describe('SettingsModal', () => {
await vi.advanceTimersByTimeAsync(100); await vi.advanceTimersByTimeAsync(100);
}); });
expect(screen.getByText('Default AI Agent')).toBeInTheDocument(); expect(screen.getByText('Font Size')).toBeInTheDocument();
}); });
it('should wrap around when navigating past last tab', async () => { it('should wrap around when navigating past last tab', async () => {
@@ -375,7 +375,7 @@ describe('SettingsModal', () => {
await vi.advanceTimersByTimeAsync(100); await vi.advanceTimersByTimeAsync(100);
}); });
expect(screen.getByText('Default AI Agent')).toBeInTheDocument(); expect(screen.getByText('Font Size')).toBeInTheDocument();
}); });
it('should wrap around when navigating before first tab', async () => { it('should wrap around when navigating before first tab', async () => {
@@ -386,7 +386,7 @@ describe('SettingsModal', () => {
}); });
// Start on general tab (first tab) // Start on general tab (first tab)
expect(screen.getByText('Default AI Agent')).toBeInTheDocument(); expect(screen.getByText('Font Size')).toBeInTheDocument();
// Press Cmd+Shift+[ to wrap to AI Commands // Press Cmd+Shift+[ to wrap to AI Commands
fireEvent.keyDown(window, { key: '[', metaKey: true, shiftKey: true }); fireEvent.keyDown(window, { key: '[', metaKey: true, shiftKey: true });
@@ -418,64 +418,6 @@ describe('SettingsModal', () => {
}); });
}); });
describe('General tab - Agent settings', () => {
it('should load agents on modal open', async () => {
render(<SettingsModal {...createDefaultProps()} />);
await act(async () => {
await vi.advanceTimersByTimeAsync(100);
});
expect(window.maestro.agents.detect).toHaveBeenCalled();
});
it('should display available agents', async () => {
render(<SettingsModal {...createDefaultProps()} />);
await act(async () => {
await vi.advanceTimersByTimeAsync(100);
});
expect(screen.getByText('Claude Code')).toBeInTheDocument();
expect(screen.getByText('OpenAI Codex')).toBeInTheDocument();
});
it('should show Available badge for available agents', async () => {
render(<SettingsModal {...createDefaultProps()} />);
await act(async () => {
await vi.advanceTimersByTimeAsync(100);
});
expect(screen.getByText('Available')).toBeInTheDocument();
});
it('should call setDefaultAgent when agent is selected', async () => {
const setDefaultAgent = vi.fn();
render(<SettingsModal {...createDefaultProps({ setDefaultAgent })} />);
await act(async () => {
await vi.advanceTimersByTimeAsync(100);
});
const agentButton = screen.getByText('Claude Code').closest('button');
fireEvent.click(agentButton!);
expect(setDefaultAgent).toHaveBeenCalledWith('claude-code');
});
it('should disable agents that are not claude-code or not available', async () => {
render(<SettingsModal {...createDefaultProps()} />);
await act(async () => {
await vi.advanceTimersByTimeAsync(100);
});
const codexButton = screen.getByText('OpenAI Codex').closest('button');
expect(codexButton).toBeDisabled();
});
});
describe('General tab - Font settings', () => { describe('General tab - Font settings', () => {
it('should show font loading message initially', async () => { it('should show font loading message initially', async () => {
render(<SettingsModal {...createDefaultProps()} />); render(<SettingsModal {...createDefaultProps()} />);
@@ -1265,52 +1207,7 @@ describe('SettingsModal', () => {
}); });
}); });
describe('agent custom paths', () => {
it('should display custom path input for claude-code', async () => {
render(<SettingsModal {...createDefaultProps()} />);
await act(async () => {
await vi.advanceTimersByTimeAsync(100);
});
expect(screen.getByPlaceholderText('/path/to/claude')).toBeInTheDocument();
});
it('should save custom path on blur', async () => {
render(<SettingsModal {...createDefaultProps()} />);
await act(async () => {
await vi.advanceTimersByTimeAsync(100);
});
const customPathInput = screen.getByPlaceholderText('/path/to/claude');
fireEvent.change(customPathInput, { target: { value: '/custom/path/claude' } });
fireEvent.blur(customPathInput);
await act(async () => {
await vi.advanceTimersByTimeAsync(100);
});
expect((window.maestro as any).agents.setCustomPath).toHaveBeenCalledWith('claude-code', '/custom/path/claude');
});
});
describe('edge cases', () => { describe('edge cases', () => {
it('should handle agent detection failure gracefully', async () => {
vi.mocked(window.maestro.agents.detect).mockRejectedValue(new Error('Detection failed'));
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
render(<SettingsModal {...createDefaultProps()} />);
await act(async () => {
await vi.advanceTimersByTimeAsync(100);
});
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
it('should handle font detection failure gracefully', async () => { it('should handle font detection failure gracefully', async () => {
(window.maestro as any).fonts.detect.mockRejectedValue(new Error('Font detection failed')); (window.maestro as any).fonts.detect.mockRejectedValue(new Error('Font detection failed'));
@@ -1844,28 +1741,4 @@ describe('SettingsModal', () => {
expect(window.maestro.shells.detect).toHaveBeenCalledTimes(1); expect(window.maestro.shells.detect).toHaveBeenCalledTimes(1);
}); });
}); });
describe('Custom agent path clear button', () => {
it('should clear custom agent path when clear button is clicked', async () => {
(window.maestro as any).agents.getAllCustomPaths.mockResolvedValue({ 'claude-code': '/custom/path/to/claude' });
render(<SettingsModal {...createDefaultProps()} />);
await act(async () => {
await vi.advanceTimersByTimeAsync(100);
});
// Find the Clear button in the agent path section
const clearButtons = screen.getAllByText('Clear');
const agentPathClearButton = clearButtons[0]; // First Clear button is for agent path
fireEvent.click(agentPathClearButton);
await act(async () => {
await vi.advanceTimersByTimeAsync(100);
});
expect((window.maestro as any).agents.setCustomPath).toHaveBeenCalledWith('claude-code', null);
});
});
}); });

View File

@@ -85,7 +85,7 @@ describe('TabSwitcherModal', () => {
Element.prototype.scrollIntoView = vi.fn(); Element.prototype.scrollIntoView = vi.fn();
// Reset the mocks for each test // Reset the mocks for each test
vi.mocked(window.maestro.claude.getAllNamedSessions).mockResolvedValue([]); vi.mocked(window.maestro.agentSessions.getAllNamedSessions).mockResolvedValue([]);
vi.mocked(window.maestro.agentSessions.updateSessionName).mockResolvedValue(undefined); vi.mocked(window.maestro.agentSessions.updateSessionName).mockResolvedValue(undefined);
}); });
@@ -868,7 +868,7 @@ describe('TabSwitcherModal', () => {
it('switches to All Named mode on pill click', async () => { it('switches to All Named mode on pill click', async () => {
const tabs = [createTestTab({ name: 'Open Tab' })]; const tabs = [createTestTab({ name: 'Open Tab' })];
vi.mocked(window.maestro.claude.getAllNamedSessions).mockResolvedValue([ vi.mocked(window.maestro.agentSessions.getAllNamedSessions).mockResolvedValue([
{ {
agentSessionId: 'closed-123-abc-def-789', agentSessionId: 'closed-123-abc-def-789',
projectPath: '/test', projectPath: '/test',
@@ -891,7 +891,7 @@ describe('TabSwitcherModal', () => {
// Wait for named sessions to load // Wait for named sessions to load
await waitFor(() => { await waitFor(() => {
expect(window.maestro.claude.getAllNamedSessions).toHaveBeenCalled(); expect(window.maestro.agentSessions.getAllNamedSessions).toHaveBeenCalled();
}); });
// Click All Named pill // Click All Named pill
@@ -927,7 +927,7 @@ describe('TabSwitcherModal', () => {
}); });
it('shows "Closed" badge for closed named sessions', async () => { it('shows "Closed" badge for closed named sessions', async () => {
vi.mocked(window.maestro.claude.getAllNamedSessions).mockResolvedValue([ vi.mocked(window.maestro.agentSessions.getAllNamedSessions).mockResolvedValue([
{ {
agentSessionId: 'closed-session-id', agentSessionId: 'closed-session-id',
projectPath: '/test', projectPath: '/test',
@@ -949,7 +949,7 @@ describe('TabSwitcherModal', () => {
// Wait for load // Wait for load
await waitFor(() => { await waitFor(() => {
expect(window.maestro.claude.getAllNamedSessions).toHaveBeenCalled(); expect(window.maestro.agentSessions.getAllNamedSessions).toHaveBeenCalled();
}); });
fireEvent.click(screen.getByText(/All Named/)); fireEvent.click(screen.getByText(/All Named/));
@@ -960,7 +960,7 @@ describe('TabSwitcherModal', () => {
}); });
it('filters named sessions by current project', async () => { it('filters named sessions by current project', async () => {
vi.mocked(window.maestro.claude.getAllNamedSessions).mockResolvedValue([ vi.mocked(window.maestro.agentSessions.getAllNamedSessions).mockResolvedValue([
{ {
agentSessionId: 'same-project-id', agentSessionId: 'same-project-id',
projectPath: '/test', projectPath: '/test',
@@ -986,7 +986,7 @@ describe('TabSwitcherModal', () => {
); );
await waitFor(() => { await waitFor(() => {
expect(window.maestro.claude.getAllNamedSessions).toHaveBeenCalled(); expect(window.maestro.agentSessions.getAllNamedSessions).toHaveBeenCalled();
}); });
fireEvent.click(screen.getByText(/All Named/)); fireEvent.click(screen.getByText(/All Named/));
@@ -1001,7 +1001,7 @@ describe('TabSwitcherModal', () => {
const starredTab = createTestTab({ name: 'Starred Tab', starred: true }); const starredTab = createTestTab({ name: 'Starred Tab', starred: true });
const unstarredTab = createTestTab({ name: 'Unstarred Tab', starred: false }); const unstarredTab = createTestTab({ name: 'Unstarred Tab', starred: false });
vi.mocked(window.maestro.claude.getAllNamedSessions).mockResolvedValue([ vi.mocked(window.maestro.agentSessions.getAllNamedSessions).mockResolvedValue([
{ {
agentSessionId: 'starred-closed-123', agentSessionId: 'starred-closed-123',
projectPath: '/test', projectPath: '/test',
@@ -1029,7 +1029,7 @@ describe('TabSwitcherModal', () => {
); );
await waitFor(() => { await waitFor(() => {
expect(window.maestro.claude.getAllNamedSessions).toHaveBeenCalled(); expect(window.maestro.agentSessions.getAllNamedSessions).toHaveBeenCalled();
}); });
// Click Starred pill (use exact pattern to avoid matching list items) // Click Starred pill (use exact pattern to avoid matching list items)
@@ -1049,7 +1049,7 @@ describe('TabSwitcherModal', () => {
}); });
it('shows "No starred sessions" when there are no starred items', async () => { it('shows "No starred sessions" when there are no starred items', async () => {
vi.mocked(window.maestro.claude.getAllNamedSessions).mockResolvedValue([]); vi.mocked(window.maestro.agentSessions.getAllNamedSessions).mockResolvedValue([]);
renderWithLayerStack( renderWithLayerStack(
<TabSwitcherModal <TabSwitcherModal
@@ -1064,7 +1064,7 @@ describe('TabSwitcherModal', () => {
); );
await waitFor(() => { await waitFor(() => {
expect(window.maestro.claude.getAllNamedSessions).toHaveBeenCalled(); expect(window.maestro.agentSessions.getAllNamedSessions).toHaveBeenCalled();
}); });
fireEvent.click(screen.getByRole('button', { name: /Starred \(\d+\)/ })); fireEvent.click(screen.getByRole('button', { name: /Starred \(\d+\)/ }));
@@ -1079,7 +1079,7 @@ describe('TabSwitcherModal', () => {
const starredTab2 = createTestTab({ name: 'Starred 2', starred: true }); const starredTab2 = createTestTab({ name: 'Starred 2', starred: true });
const unstarredTab = createTestTab({ name: 'Unstarred', starred: false }); const unstarredTab = createTestTab({ name: 'Unstarred', starred: false });
vi.mocked(window.maestro.claude.getAllNamedSessions).mockResolvedValue([ vi.mocked(window.maestro.agentSessions.getAllNamedSessions).mockResolvedValue([
{ {
agentSessionId: 'starred-closed-abc', agentSessionId: 'starred-closed-abc',
projectPath: '/test', projectPath: '/test',
@@ -1101,7 +1101,7 @@ describe('TabSwitcherModal', () => {
); );
await waitFor(() => { await waitFor(() => {
expect(window.maestro.claude.getAllNamedSessions).toHaveBeenCalled(); expect(window.maestro.agentSessions.getAllNamedSessions).toHaveBeenCalled();
}); });
// Should show count of 3: 2 open starred + 1 closed starred // Should show count of 3: 2 open starred + 1 closed starred
@@ -1555,7 +1555,7 @@ describe('TabSwitcherModal', () => {
}); });
it('calls onNamedSessionSelect when clicking a closed named session', async () => { it('calls onNamedSessionSelect when clicking a closed named session', async () => {
vi.mocked(window.maestro.claude.getAllNamedSessions).mockResolvedValue([ vi.mocked(window.maestro.agentSessions.getAllNamedSessions).mockResolvedValue([
{ {
agentSessionId: 'closed-abc-123', agentSessionId: 'closed-abc-123',
projectPath: '/test', projectPath: '/test',
@@ -1580,7 +1580,7 @@ describe('TabSwitcherModal', () => {
); );
await waitFor(() => { await waitFor(() => {
expect(window.maestro.claude.getAllNamedSessions).toHaveBeenCalled(); expect(window.maestro.agentSessions.getAllNamedSessions).toHaveBeenCalled();
}); });
fireEvent.click(screen.getByText(/All Named/)); fireEvent.click(screen.getByText(/All Named/));