Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Unlike the AI CLIs above, the following network activity is initiated by Paralle
- **Over Tailscale.** Traffic is carried by your tailnet — typically a direct WireGuard connection between your devices, but Tailscale's coordination service and (when direct connection is not possible) DERP relays may be involved per [Tailscale's network architecture](https://tailscale.com/kb/1257/connection-types). How Tailscale handles that traffic is governed by Tailscale's own policies, not this one.
- **Sub-task coordinator (MCP)** — when sub-tasks run under a coordinator agent, Parallel Code starts a local token-protected HTTP/WebSocket server so sub-task agents can call back into the app (e.g. to signal completion). No traffic from this feature passes through infrastructure operated by the Parallel Code project.
- **Bind address.** When MCP starts its own listener, it binds to `127.0.0.1` except on macOS Docker setups, where it binds to `0.0.0.0` so containers can reach it via `host.docker.internal` — this also makes the port reachable from other hosts on your LAN, though access still requires the token. If a Remote Access server is already running when a coordinator starts, the coordinator reuses that listener; because Remote Access binds to `0.0.0.0`, MCP routes inherit that LAN reach on any platform (including Linux), though access still requires the MCP token.
- **Where the token can land.** Token-bearing MCP data is written or passed in several places: a worktree `.mcp.json` when a worktree path is available, or a project-root `.mcp.json` otherwise, so the coordinator agent can auto-discover the server (Parallel Code also adds `.mcp.json` to your `.git/info/exclude` so it is not committed); a non-Docker coordinator config in your OS temp directory named `parallel-code-mcp-<coordinatorTaskId>.json`; per-sub-task configs in your OS temp directory for host-mode sub-tasks (`parallel-code-subtask-<taskId>.json`) or under the coordinator's `.parallel-code/` directory for Docker sub-tasks (`subtask-<taskId>.json`); and short-lived `.parallel-code-atomic-<uuid>.tmp` files written next to these configs during atomic-rename steps. These files are written with `0600` permissions where the platform supports it.
- **Where the token can land.** Token-bearing MCP data is written or passed in several places: a worktree `.mcp.json` when a worktree path is available, or a project-root `.mcp.json` otherwise, so the coordinator agent can auto-discover the server (Parallel Code also adds `.mcp.json` to your `.git/info/exclude` so it is not committed); an auto-discovered `.kimi-code/mcp.json` (or fallback `.mcp.json`) inside each Kimi sub-task worktree, with both that config and its adjacent `.parallel-code-atomic-*.tmp` files added to `.git/info/exclude` before credentials are written; a non-Docker coordinator config in your OS temp directory named `parallel-code-mcp-<coordinatorTaskId>.json`; per-sub-task configs in your OS temp directory for host-mode sub-tasks (`parallel-code-subtask-<taskId>.json`) or under the coordinator's `.parallel-code/` directory for Docker sub-tasks (`subtask-<taskId>.json`); and short-lived `.parallel-code-atomic-<uuid>.tmp` files written next to these configs during atomic-rename steps. These files are written with `0600` permissions where the platform supports it.
- **Codex token in the command line.** For Codex specifically, the MCP token is passed as a literal command-line argument (`--config mcp_servers.parallel-code={... env = { PARALLEL_CODE_MCP_TOKEN = "..." }}`). Process command lines are visible to other processes — via `/proc/<pid>/cmdline` on Linux or `ps` on macOS — so any local process that runs concurrently with a Codex sub-task can read that token and call back into the coordinator under its authority until the coordinator exits. Other agents receive the token through a token-protected file or env var instead.
- **Docker task isolation** — when you enable Docker mode for a task, or when you opt coordinator sub-tasks into Docker-isolated mode, the agent launches in a container via `docker run --network host`. **Docker mode is not a security boundary.** It isolates the filesystem against the worktree, but does not isolate the network or credentials from the agent.
- **Network.** `--network host` means the container shares the host's network namespace; its outbound reachability is the same as your host's, including loopback services and any LAN address your host can reach.
Expand Down
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
</p>

<p align="center">
Works with Claude Code, Codex, and Gemini · Every change isolated in its own git worktree · Free, open source, no extra platform fee
Works with Claude Code, Codex, Gemini, and Docker-pinned Kimi Code · Every change isolated in its own git worktree · Free, open source, no extra platform fee
</p>

<p align="center">
Expand Down Expand Up @@ -45,7 +45,7 @@

## Why Parallel Code?

- **Use the AI coding tools you already trust** — [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex CLI](https://github.com/openai/codex), [Gemini CLI](https://github.com/google-gemini/gemini-cli), and [Copilot CLI](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli) — all from one interface.
- **Use the AI coding tools you already trust** — [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex CLI](https://github.com/openai/codex), [Gemini CLI](https://github.com/google-gemini/gemini-cli), Docker-pinned [Kimi Code CLI](https://github.com/MoonshotAI/kimi-code), and [Copilot CLI](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli) — all from one interface.
- **Free and open source** — no extra subscription required. MIT licensed.
- **Keep every change isolated and reviewable** — each task gets its own git branch and worktree automatically.
- **Run agents in parallel, not in sequence** — five agents on five features at the same time, zero conflicts.
Expand Down Expand Up @@ -115,10 +115,23 @@ When you're happy with the result, merge the branch back to main from the sideba
- **macOS** — `.dmg` (universal)
- **Linux** — `.AppImage` or `.deb`

2. **Install at least one AI coding CLI:** [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex CLI](https://github.com/openai/codex), [Gemini CLI](https://github.com/google-gemini/gemini-cli), [Antigravity CLI](https://antigravity.google/), or [Copilot CLI](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli)
2. **Install at least one AI coding CLI:** [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex CLI](https://github.com/openai/codex), [Gemini CLI](https://github.com/google-gemini/gemini-cli), [Antigravity CLI](https://antigravity.google/), or [Copilot CLI](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli). Kimi Code is currently supported through the bundled Docker image instead of a native installation.

3. **Open Parallel Code**, point it at a git repo, and start dispatching tasks.

<details>
<summary><strong>Kimi Code: use Docker mode</strong></summary>

Parallel Code's Kimi integration writes an auto-discovered project MCP config into each fresh
task worktree. Kimi Code 0.33 added a workspace-trust prompt, and 0.36 defaults to declining
project MCP launch targets in that prompt. A current native Kimi installation can therefore
pause on every new worktree instead of starting the task unattended.

Use Docker-isolated Kimi tasks for now. The bundled image pins Kimi Code 0.32, before the
workspace-trust gate. Native Kimi support is not currently claimed.

</details>

<details>
<summary><strong>Antigravity CLI: run natively, not in Docker isolation</strong></summary>

Expand Down
3 changes: 2 additions & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
RUN ln -sf "$(command -v fdfind)" /usr/local/bin/fd 2>/dev/null || true

# AI agent CLIs — must be present so Docker-mode tasks can execute them
RUN npm install -g @anthropic-ai/claude-code @openai/codex @google/gemini-cli opencode-ai
# Keep Kimi below 0.33: newer releases block fresh worktrees on workspace trust.
RUN npm install -g @anthropic-ai/claude-code @openai/codex @google/gemini-cli opencode-ai @moonshot-ai/kimi-code@0.32.0

# Antigravity CLI (agy) — distributed as a Go binary via the official installer
# (not on npm). The installer's `--dir` flag drops the binary straight into a
Expand Down
4 changes: 4 additions & 0 deletions electron/ipc/agents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ describe('getSkipPermissionsArgs', () => {

expect(getSkipPermissionsArgs('claude')).toEqual(['--dangerously-skip-permissions']);
});

it('returns Kimi Code skip-permission args', () => {
expect(getSkipPermissionsArgs('kimi')).toEqual(['--yolo']);
});
});
9 changes: 9 additions & 0 deletions electron/ipc/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ const DEFAULT_AGENTS: AgentDef[] = [
skip_permissions_args: ['--yolo'],
description: "Google's Gemini CLI agent",
},
{
id: 'kimi',
name: 'Kimi Code CLI',
command: 'kimi',
args: [],
resume_args: ['--continue'],
skip_permissions_args: ['--yolo'],
description: "Moonshot AI's Kimi Code CLI agent",
},
{
id: 'opencode',
name: 'OpenCode',
Expand Down
1 change: 1 addition & 0 deletions electron/ipc/pty.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ describe('spawnAgent docker mode', () => {
['opencode', '.config/opencode'],
['copilot', '.config/github-copilot'],
['agy', '.gemini/antigravity-cli'],
['kimi', '.kimi-code'],
])(
'%s bind-mounts a user-owned host directory when shareDockerAgentAuth is enabled',
(command, relDir) => {
Expand Down
1 change: 1 addition & 0 deletions electron/ipc/pty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,7 @@ const AGENT_CONFIG_DIRS: Record<string, string[]> = {
opencode: ['.config/opencode'],
copilot: ['.config/github-copilot'],
agy: ['.gemini/antigravity-cli'],
kimi: ['.kimi-code'],
};

// Config files (not directories) each agent CLI uses for auth, relative to HOME.
Expand Down
2 changes: 2 additions & 0 deletions electron/ipc/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1561,6 +1561,7 @@ export function registerAllHandlers(win: BrowserWindow): void {
landingSummary?: string;
landedMetadata?: import('../mcp/types.js').LandedMetadata;
mcpConfigPath?: string;
autoDiscoveredMcpConfig?: import('../mcp/types.js').AutoDiscoveredMcpConfigState;
agentCommand?: string;
preambleFileExistedBefore?: boolean;
initialPrompt?: string;
Expand Down Expand Up @@ -1599,6 +1600,7 @@ export function registerAllHandlers(win: BrowserWindow): void {
landingSummary: args.landingSummary,
landedMetadata: args.landedMetadata,
mcpConfigPath: args.mcpConfigPath,
autoDiscoveredMcpConfig: args.autoDiscoveredMcpConfig,
agentCommand: args.agentCommand,
preambleFileExistedBefore: args.preambleFileExistedBefore,
initialPrompt: args.initialPrompt,
Expand Down
11 changes: 11 additions & 0 deletions electron/mcp/agent-args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
isAntigravityCommand,
isCodexCommand,
isCopilotCommand,
isKimiCommand,
} from './agent-args.js';

const config = {
Expand Down Expand Up @@ -67,6 +68,16 @@ describe('MCP agent launch args', () => {
expect(buildMcpLaunchArgs('agy', '/tmp/config.json', config)).toEqual([]);
});

it('detects Kimi commands by executable name', () => {
expect(isKimiCommand('kimi')).toBe(true);
expect(isKimiCommand('/home/agent/.local/bin/kimi')).toBe(true);
expect(isKimiCommand('claude')).toBe(false);
});

it('emits no --mcp-config for Kimi Code', () => {
expect(buildMcpLaunchArgs('kimi', '/tmp/config.json', config)).toEqual([]);
});

it('detects copilot commands by executable name', () => {
expect(isCopilotCommand('copilot')).toBe(true);
expect(isCopilotCommand('/opt/homebrew/bin/copilot')).toBe(true);
Expand Down
9 changes: 9 additions & 0 deletions electron/mcp/agent-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export function isAntigravityCommand(command: string): boolean {
return command.split('/').pop() === 'agy';
}

export function isKimiCommand(command: string): boolean {
return command.split('/').pop() === 'kimi';
}

export function isCopilotCommand(command: string): boolean {
return command.split('/').pop() === 'copilot';
}
Expand Down Expand Up @@ -53,6 +57,11 @@ export function buildMcpLaunchArgs(
if (isAntigravityCommand(command)) {
return [];
}
// Kimi Code auto-discovers user and project MCP config files and does not
// accept the generic `--mcp-config` flag.
if (isKimiCommand(command)) {
return [];
}
// Copilot has no `--mcp-config` flag — passing it makes Copilot exit immediately
// with "unknown option" before the prompt is ever sent (#146). It accepts
// `--additional-mcp-config <@file|json>` (and also auto-discovers a workspace
Expand Down
15 changes: 15 additions & 0 deletions electron/mcp/coordinator-test-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const enoent = () => Object.assign(new Error('ENOENT'), { code: 'ENOENT' });

const mocks = vi.hoisted(() => {
const mockExecFile = vi.fn();
const mockSpawnSync = vi.fn();
const mockWriteFileSync = vi.fn();
const mockReadFileSync = vi.fn();
const mockExistsSync = vi.fn();
Expand All @@ -37,6 +38,7 @@ const mocks = vi.hoisted(() => {
const mockFsMkdir = vi.fn();
const mockAtomicWriteFileSync = vi.fn();
const mockAtomicWriteFile = vi.fn();
const mockAppendGitInfoExcludeBlock = vi.fn();
const mockNotifyRenderer = vi.fn();
const mockLogInfo = vi.fn();
const mockLogWarn = vi.fn();
Expand All @@ -59,6 +61,7 @@ const mocks = vi.hoisted(() => {

return {
mockExecFile,
mockSpawnSync,
mockWriteFileSync,
mockReadFileSync,
mockExistsSync,
Expand All @@ -71,6 +74,7 @@ const mocks = vi.hoisted(() => {
mockFsMkdir,
mockAtomicWriteFileSync,
mockAtomicWriteFile,
mockAppendGitInfoExcludeBlock,
mockNotifyRenderer,
mockLogInfo,
mockLogWarn,
Expand All @@ -95,6 +99,7 @@ const mocks = vi.hoisted(() => {

vi.mock('child_process', () => ({
execFile: mocks.mockExecFile,
spawnSync: mocks.mockSpawnSync,
}));

vi.mock('fs', () => ({
Expand All @@ -118,6 +123,10 @@ vi.mock('./atomic.js', () => ({
atomicWriteFile: mocks.mockAtomicWriteFile,
}));

vi.mock('../ipc/git-exclude.js', () => ({
appendGitInfoExcludeBlock: mocks.mockAppendGitInfoExcludeBlock,
}));

vi.mock('../shared/prompt-detect.js', () => ({
stripAnsi: (s: string) =>
s.replace(
Expand Down Expand Up @@ -238,6 +247,7 @@ vi.mock('../log.js', () => ({

export const {
mockExecFile,
mockSpawnSync,
mockWriteFileSync,
mockReadFileSync,
mockExistsSync,
Expand All @@ -250,6 +260,7 @@ export const {
mockFsMkdir,
mockAtomicWriteFileSync,
mockAtomicWriteFile,
mockAppendGitInfoExcludeBlock,
mockNotifyRenderer,
mockLogInfo,
mockLogWarn,
Expand Down Expand Up @@ -303,6 +314,8 @@ export function resetCoordinatorMocks(): void {
return { on: vi.fn() };
},
);
mockSpawnSync.mockReset();
mockSpawnSync.mockReturnValue({ status: 1, error: undefined, stderr: Buffer.alloc(0) });

mockWriteFileSync.mockReset();
mockReadFileSync.mockReset();
Expand All @@ -326,6 +339,8 @@ export function resetCoordinatorMocks(): void {
mockAtomicWriteFileSync.mockReset();
mockAtomicWriteFile.mockReset();
mockAtomicWriteFile.mockResolvedValue(undefined);
mockAppendGitInfoExcludeBlock.mockReset();
mockAppendGitInfoExcludeBlock.mockReturnValue('appended');

mockNotifyRenderer.mockReset();
mockLogInfo.mockReset();
Expand Down
Loading
Loading