chore: include uncommitted workspace files from orions-belt

This commit is contained in:
Sehoon Shon
2026-03-10 13:36:46 -04:00
parent 7658e47eaf
commit 1add82278f
6 changed files with 260 additions and 20 deletions

View File

@@ -1,10 +1,12 @@
{
"experimental": {
"plan": true,
"extensionReloading": true,
"modelSteering": true
"extensionReloading": true
},
"general": {
"devtools": true
},
"tools": {
"allowed": ["run_shell_command"]
}
}

2
.vscode/launch.json vendored
View File

@@ -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": ["<node_internals>/**"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",

View File

@@ -361,4 +361,4 @@
}
]
}
]
]

View File

@@ -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 <file-path>',
});
});
});
});

View File

@@ -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"

View File

@@ -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.