diff --git a/.gemini/settings.json b/.gemini/settings.json index 1a4c889066..47dd055181 100644 --- a/.gemini/settings.json +++ b/.gemini/settings.json @@ -1,10 +1,12 @@ { "experimental": { "plan": true, - "extensionReloading": true, - "modelSteering": true + "extensionReloading": true }, "general": { "devtools": true + }, + "tools": { + "allowed": ["run_shell_command"] } } diff --git a/.vscode/launch.json b/.vscode/launch.json index 0294e27ed4..123e12d68e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,7 @@ "request": "launch", "name": "Build & Launch CLI", "runtimeExecutable": "npm", - "runtimeArgs": ["run", "build-and-start"], + "runtimeArgs": ["run", "build-and-start", "--", "-m", "abc"], "skipFiles": ["/**"], "cwd": "${workspaceFolder}", "console": "integratedTerminal", diff --git a/docs/sidebar.json b/docs/sidebar.json index 5d0d21d14f..a8c9f29f3e 100644 --- a/docs/sidebar.json +++ b/docs/sidebar.json @@ -361,4 +361,4 @@ } ] } -] \ No newline at end of file +] diff --git a/packages/cli/src/ui/commands/agentsCommand.test.ts b/packages/cli/src/ui/commands/agentsCommand.test.ts index 5e6cc36efa..decd24235e 100644 --- a/packages/cli/src/ui/commands/agentsCommand.test.ts +++ b/packages/cli/src/ui/commands/agentsCommand.test.ts @@ -8,10 +8,14 @@ import { vi, describe, it, expect, beforeEach } from 'vitest'; import { agentsCommand } from './agentsCommand.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; import type { Config } from '@google/gemini-cli-core'; +import { Storage } from '@google/gemini-cli-core'; import type { LoadedSettings } from '../../config/settings.js'; import { MessageType } from '../types.js'; import { enableAgent, disableAgent } from '../../utils/agentSettings.js'; import { renderAgentActionFeedback } from '../../utils/agentUtils.js'; +import * as fs from 'node:fs/promises'; + +vi.mock('node:fs/promises'); vi.mock('../../utils/agentSettings.js', () => ({ enableAgent: vi.fn(), @@ -105,40 +109,34 @@ describe('agentsCommand', () => { ); }); - it('should reload the agent registry when reload subcommand is called', async () => { + it('should reload the agent registry when refresh subcommand is called', async () => { const reloadSpy = vi.fn().mockResolvedValue(undefined); mockConfig.getAgentRegistry = vi.fn().mockReturnValue({ reload: reloadSpy, }); - const reloadCommand = agentsCommand.subCommands?.find( - (cmd) => cmd.name === 'reload', + const refreshCommand = agentsCommand.subCommands?.find( + (cmd) => cmd.name === 'refresh', ); - expect(reloadCommand).toBeDefined(); + expect(refreshCommand).toBeDefined(); - const result = await reloadCommand!.action!(mockContext, ''); + const result = await refreshCommand!.action!(mockContext, ''); expect(reloadSpy).toHaveBeenCalled(); - expect(mockContext.ui.addItem).toHaveBeenCalledWith( - expect.objectContaining({ - type: MessageType.INFO, - text: 'Reloading agent registry...', - }), - ); expect(result).toEqual({ type: 'message', messageType: 'info', - content: 'Agents reloaded successfully', + content: 'Agents refreshed successfully.', }); }); - it('should show an error if agent registry is not available during reload', async () => { + it('should show an error if agent registry is not available during refresh', async () => { mockConfig.getAgentRegistry = vi.fn().mockReturnValue(undefined); - const reloadCommand = agentsCommand.subCommands?.find( - (cmd) => cmd.name === 'reload', + const refreshCommand = agentsCommand.subCommands?.find( + (cmd) => cmd.name === 'refresh', ); - const result = await reloadCommand!.action!(mockContext, ''); + const result = await refreshCommand!.action!(mockContext, ''); expect(result).toEqual({ type: 'message', @@ -462,4 +460,56 @@ describe('agentsCommand', () => { expect(completions).toEqual(['agent1', 'agent2']); }); }); + + describe('import sub-command', () => { + it('should import an agent with correct tool mapping', async () => { + vi.mocked(fs.readFile).mockResolvedValue(`--- +name: test-claude-agent +tools: Read, Glob, Grep, Bash +model: sonnet +--- +System prompt content`); + + vi.mocked(fs.mkdir).mockResolvedValue(undefined); + vi.mocked(fs.writeFile).mockResolvedValue(undefined); + vi.spyOn(Storage, 'getUserAgentsDir').mockReturnValue('/mock/agents'); + + const importCommand = agentsCommand.subCommands?.find( + (cmd) => cmd.name === 'import', + ); + expect(importCommand).toBeDefined(); + + const result = await importCommand!.action!( + mockContext, + '/path/to/claude.md', + ); + + expect(fs.readFile).toHaveBeenCalledWith('/path/to/claude.md', 'utf-8'); + expect(fs.writeFile).toHaveBeenCalledWith( + expect.stringMatching(/test-claude-agent\.md$/), + expect.stringContaining('tools:\n - read_file\n - run_command\n'), + 'utf-8', + ); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining( + "Successfully imported agent 'test-claude-agent'", + ), + }); + }); + + it('should show error if no file path provided', async () => { + const importCommand = agentsCommand.subCommands?.find( + (cmd) => cmd.name === 'import', + ); + const result = await importCommand!.action!(mockContext, ' '); + + expect(result).toEqual({ + type: 'message', + messageType: 'error', + content: 'Usage: /agents import ', + }); + }); + }); }); diff --git a/packages/core/scripts/build-teleporter.sh b/packages/core/scripts/build-teleporter.sh new file mode 100755 index 0000000000..1683b3ba36 --- /dev/null +++ b/packages/core/scripts/build-teleporter.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -e + +# Define paths +CLI_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +EXA_ROOT="$(cd "$CLI_ROOT/../jetski/Exafunction" && pwd)" +TELEPORTER_TS="$CLI_ROOT/packages/core/src/teleportation/trajectory_teleporter.ts" +TELEPORTER_MIN_JS="$CLI_ROOT/packages/core/src/teleportation/trajectory_teleporter.min.js" + +if [ ! -d "$EXA_ROOT" ]; then + echo "Error: Exafunction directory not found at $EXA_ROOT" + exit 1 +fi + +echo "Building Protobuf JS definitions in Exafunction..." +cd "$EXA_ROOT" +pnpm --dir exa/proto_ts build + +echo "Bundling and minifying trajectory_teleporter.ts..." +# Because esbuild resolves relative imports from the source file's directory, +# and trajectory_teleporter.ts playfully imports './exa/...', we copy it to EXA_ROOT +# temporarily for the build step to succeed. +cp "$TELEPORTER_TS" "$EXA_ROOT/trajectory_teleporter_tmp.ts" + +cd "$EXA_ROOT" +pnpm dlx esbuild "./trajectory_teleporter_tmp.ts" \ + --bundle \ + --format=esm \ + --platform=node \ + --outfile="$TELEPORTER_MIN_JS" + +rm "$EXA_ROOT/trajectory_teleporter_tmp.ts" + +echo "Done! Wrote bundle to $TELEPORTER_MIN_JS" diff --git a/packages/core/src/teleportation/remote_teleportation_proposal.md b/packages/core/src/teleportation/remote_teleportation_proposal.md new file mode 100644 index 0000000000..c89e41ae55 --- /dev/null +++ b/packages/core/src/teleportation/remote_teleportation_proposal.md @@ -0,0 +1,154 @@ +# Remote Teleportation Architecture Proposal + +## Objective + +Prevent leaking proprietary JetSki Protobuf schemas and decryption keys directly +within the public Gemini CLI bundle. When a user requests to resume a JetSki +trajectory, the CLI will interact with an external, secure, or isolated +converter to fetch the standard `ConversationRecord` JSON. + +## Guarantees & Constraints + +- No Protobuf definitions are packaged or visible in the Gemini CLI + distribution. +- Telemetry/Decryption keys do not need to be hardcoded in the CLI. +- Conversion remains effortless and instantaneous for the end-user. +- **Strict Logic Confidentiality**: Proprietary conversion logic and Protobuf + schemas cannot be readable or reverse-engineered by the user via local caches + or easily accessible client-side code. + +--- + +## Option 1: Local MCP Server (JetSki Daemon Extension) + +Since Gemini CLI already natively supports the **Model Context Protocol (MCP)**, +we can utilize JetSki directly as an MCP provider. + +### How it works + +The JetSki extension (or its already running background daemon) can expose an +MCP tool, for example, `get_trajectory_history`. When the user runs `/resume`, +Gemini CLI invokes this tool via the standard MCP pipeline. + +1. **User runs `/resume`**: Gemini CLI lists available trajectories (it can + list .pb files from the local directory or query the MCP server for a + history list). +2. **Conversation Selection**: The user selects a session. +3. **MCP Tool Call**: Gemini calls the MCP tool + `get_trajectory(filePath: string)`. +4. **Local Conversion**: JetSki decrypts, parses, and returns the strictly + CLI-compliant `HistoryItem` JSON payload over stdio/SSE. +5. **No Overhead**: The CLI injects the payload directly into its state without + ever touching a Protobuf. + +### Effort & Trade-offs + +- **Effort**: Very Low. Gemini CLI already supports MCP out of the box. No new + endpoints, infrastructure, or routing is needed. We only need to write a small + MCP server module using `@modelcontextprotocol/sdk` inside JetSki. +- **Pros**: Zero remote network latency; highly secure (stays on local machine); + incredibly seamless. +- **Cons**: Requires the JetSki service to be installed and running in the + background. + +--- + +## Option 2: Stateless Cloud Function (API/Microservice) + +Host the trajectory parsing logic completely server-side. + +### How it works + +1. **User runs `/resume`**: The CLI scans local `.pb` trajectories. +2. **Cloud Request**: The CLI sends a + `POST https://api.exafunction.com/v1/teleport` request. The body contains + the binary payload (and maybe an authorization token/key). +3. **Cloud Conversion**: The cloud function decrypts, parses, and formats the + trajectory. +4. **Response**: The API responds with the `ConversationRecord` JSON. + +### Effort & Trade-offs + +- **Effort**: Medium. Requires setting up a secured API endpoint, likely using + Google Cloud Functions or AWS Lambda. +- **Pros**: Completely decouples schema and keys from the end-user's device + entirely. The conversion environment is completely under Exafunction's + control. +- **Cons**: Network roundtrip latency whenever the user converts a session. + Involves sending binary trajectory files (potentially containing source code + snippets) over the network, which could be a privacy concern for some users. + +--- + +## Option 3: Remote "Plugin" Loader (WASM / Transient Javascript) + +The CLI downloads the interpreter on-demand, or runs it inside an isolated +Sandbox (like WebAssembly). + +### How it works + +1. **On-Demand Download**: When `/resume` is first used, the CLI downloads a + private, versioned conversion payload (e.g., `teleporter_v1.2.3.wasm` or an + obfuscated JS bundle) from a secure URL and caches it locally. +2. **Local Execution**: It runs this script locally to perform decryption and + conversion. +3. It keeps the schema out of the open-source CLI bundle, though a determined + user could still reverse engineer the WASM/JS if they inspect their local + caches. + +### Effort & Trade-offs + +- **Effort**: Medium-High. Requires robust WebAssembly compilation from the + Protobuf definitions, or a dynamic code loading execution chain in Node.js + that doesn't trigger security flags. +- **Pros**: Speed of local processing, decoupled from the main CLI installation. +- **Cons**: Adds complexity to the bundle distribution system. +- **Cons (Security)**: A determined user could reverse engineer the cached + WebAssembly or obfuscated JS bundle to reconstruct the JetSki Protobuf schema, + breaking the logic confidentiality requirement. + +--- + +## Option 4: Remote MCP Server (via SSE) + +A remote MCP server would bridge the user's local trajectory files with +Exafunction's parser inside a totally separate host. + +### How it works + +- We run an SSE MCP service on the Exafunction side. +- Instead of using stdio transport from a local background process (Option 1), + the Gemini CLI establishes an SSE connection to + `https://mcp.exafunction.com/sse`. +- The local CLI still queries the filesystem for the `.pb` trajectories. +- It invokes a remote tool: `parse_trajectory(trajectoryPb: string)` and passes + the local protobuf string as the request argument. +- The remote server unmarshalls, decrypts, and maps the proprietary protobufs + into the standard JSON response, which the CLI renders natively. + +### Effort & Trade-offs + +- **Effort**: Medium. Gemini CLI already supports SSE network transports for + MCP. You would need to host an MCP SSE server remotely. +- **Pros**: Proprietary mapping logic, credentials, and Protobuf schemas are + hosted totally remote and are decoupled from JetSki OR the CLI. Since it uses + standard MCP, it requires absolutely no specialized HTTP routing or bespoke + protocol headers in the CLI because Gemini naturally maps arguments + dynamically already. +- **Cons**: High network latency (sending large Protobuf strings back and + forth), privacy concerns because user code trajectories are being transmitted + remotely. + +--- + +## Recommendation + +**Option 4 (Remote MCP)** or **Option 2 (Cloud Function Isolation)** is the +recommended production approach to ensure strict confidentiality. + +By keeping the proprietary deserialization binary completely remote behind an +authentication layer, Gemini CLI ensures that end users cannot observe execution +state, trace unmarshalled arrays, or scrape proprietary JetSki Protobuf +primitives natively. If removing heavy network latency is the highest priority, +**Option 1 (JetSki Local MCP)** remains the most effortless and robust path +forward without modifying the open-source CLI distribution.