Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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