mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-02 21:21:09 -07:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 38c706dbb9 | |||
| 5e18b14c10 | |||
| 8e2629759d | |||
| 146442f2a2 | |||
| a3a3e66922 | |||
| d4555a473e | |||
| cdac58fe82 | |||
| 4a95ab62d4 | |||
| 0223181cf4 |
@@ -1540,7 +1540,7 @@ their corresponding top-level category object in your `settings.json` file.
|
|||||||
|
|
||||||
- **`experimental.enableAgents`** (boolean):
|
- **`experimental.enableAgents`** (boolean):
|
||||||
- **Description:** Enable local and remote subagents.
|
- **Description:** Enable local and remote subagents.
|
||||||
- **Default:** `false`
|
- **Default:** `true`
|
||||||
- **Requires restart:** Yes
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
- **`experimental.worktrees`** (boolean):
|
- **`experimental.worktrees`** (boolean):
|
||||||
|
|||||||
@@ -63,9 +63,6 @@ describe.skipIf(!chromeAvailable)('browser-policy', () => {
|
|||||||
rig.setup('browser-policy-skip-confirmation', {
|
rig.setup('browser-policy-skip-confirmation', {
|
||||||
fakeResponsesPath: join(__dirname, 'browser-policy.responses'),
|
fakeResponsesPath: join(__dirname, 'browser-policy.responses'),
|
||||||
settings: {
|
settings: {
|
||||||
experimental: {
|
|
||||||
enableAgents: true,
|
|
||||||
},
|
|
||||||
agents: {
|
agents: {
|
||||||
overrides: {
|
overrides: {
|
||||||
browser_agent: {
|
browser_agent: {
|
||||||
@@ -183,9 +180,6 @@ priority = 200
|
|||||||
rig.setup('browser-session-warning', {
|
rig.setup('browser-session-warning', {
|
||||||
fakeResponsesPath: join(__dirname, 'browser-agent.cleanup.responses'),
|
fakeResponsesPath: join(__dirname, 'browser-agent.cleanup.responses'),
|
||||||
settings: {
|
settings: {
|
||||||
experimental: {
|
|
||||||
enableAgents: true,
|
|
||||||
},
|
|
||||||
general: {
|
general: {
|
||||||
enableAutoUpdateNotification: false,
|
enableAutoUpdateNotification: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
|
|
||||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
import * as os from 'node:os';
|
import * as os from 'node:os';
|
||||||
import { TestRig } from './test-helper.js';
|
import { TestRig, skipFlaky } from './test-helper.js';
|
||||||
|
|
||||||
describe('Ctrl+C exit', () => {
|
describe.skipIf(skipFlaky)('Ctrl+C exit', () => {
|
||||||
let rig: TestRig;
|
let rig: TestRig;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -4,10 +4,8 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { writeFileSync } from 'node:fs';
|
|
||||||
import { join } from 'node:path';
|
|
||||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
import { TestRig, checkModelOutputContent, GEMINI_DIR } from './test-helper.js';
|
import { TestRig, checkModelOutputContent } from './test-helper.js';
|
||||||
|
|
||||||
describe('Plan Mode', () => {
|
describe('Plan Mode', () => {
|
||||||
let rig: TestRig;
|
let rig: TestRig;
|
||||||
@@ -36,27 +34,23 @@ describe('Plan Mode', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// We use a prompt that asks for both a read-only action and a write action.
|
|
||||||
// "List files" (read-only) followed by "touch denied.txt" (write).
|
|
||||||
const result = await rig.run({
|
const result = await rig.run({
|
||||||
approvalMode: 'plan',
|
approvalMode: 'plan',
|
||||||
stdin:
|
args: 'Please list the files in the current directory, and then attempt to create a new file named "denied.txt" using a shell command.',
|
||||||
'Please list the files in the current directory, and then attempt to create a new file named "denied.txt" using a shell command.',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const lsCallFound = await rig.waitForToolCall('list_directory');
|
|
||||||
expect(lsCallFound, 'Expected list_directory to be called').toBe(true);
|
|
||||||
|
|
||||||
const shellCallFound = await rig.waitForToolCall('run_shell_command');
|
|
||||||
expect(shellCallFound, 'Expected run_shell_command to fail').toBe(false);
|
|
||||||
|
|
||||||
const toolLogs = rig.readToolLogs();
|
const toolLogs = rig.readToolLogs();
|
||||||
const lsLog = toolLogs.find((l) => l.toolRequest.name === 'list_directory');
|
const lsLog = toolLogs.find((l) => l.toolRequest.name === 'list_directory');
|
||||||
expect(
|
const shellLog = toolLogs.find(
|
||||||
toolLogs.find((l) => l.toolRequest.name === 'run_shell_command'),
|
(l) => l.toolRequest.name === 'run_shell_command',
|
||||||
).toBeUndefined();
|
);
|
||||||
|
|
||||||
|
expect(lsLog, 'Expected list_directory to be called').toBeDefined();
|
||||||
expect(lsLog?.toolRequest.success).toBe(true);
|
expect(lsLog?.toolRequest.success).toBe(true);
|
||||||
|
expect(
|
||||||
|
shellLog,
|
||||||
|
'Expected run_shell_command to be blocked (not even called)',
|
||||||
|
).toBeUndefined();
|
||||||
|
|
||||||
checkModelOutputContent(result, {
|
checkModelOutputContent(result, {
|
||||||
expectedContent: ['Plan Mode', 'read-only'],
|
expectedContent: ['Plan Mode', 'read-only'],
|
||||||
@@ -84,23 +78,11 @@ describe('Plan Mode', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Disable the interactive terminal setup prompt in tests
|
await rig.run({
|
||||||
writeFileSync(
|
|
||||||
join(rig.homeDir!, GEMINI_DIR, 'state.json'),
|
|
||||||
JSON.stringify({ terminalSetupPromptShown: true }, null, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
const run = await rig.runInteractive({
|
|
||||||
approvalMode: 'plan',
|
approvalMode: 'plan',
|
||||||
|
args: 'Create a file called plan.md in the plans directory.',
|
||||||
});
|
});
|
||||||
|
|
||||||
await run.type('Create a file called plan.md in the plans directory.');
|
|
||||||
await run.type('\r');
|
|
||||||
|
|
||||||
await rig.expectToolCallSuccess(['write_file'], 30000, (args) =>
|
|
||||||
args.includes('plan.md'),
|
|
||||||
);
|
|
||||||
|
|
||||||
const toolLogs = rig.readToolLogs();
|
const toolLogs = rig.readToolLogs();
|
||||||
const planWrite = toolLogs.find(
|
const planWrite = toolLogs.find(
|
||||||
(l) =>
|
(l) =>
|
||||||
@@ -108,7 +90,25 @@ describe('Plan Mode', () => {
|
|||||||
l.toolRequest.args.includes('plans') &&
|
l.toolRequest.args.includes('plans') &&
|
||||||
l.toolRequest.args.includes('plan.md'),
|
l.toolRequest.args.includes('plan.md'),
|
||||||
);
|
);
|
||||||
expect(planWrite?.toolRequest.success).toBe(true);
|
|
||||||
|
if (!planWrite) {
|
||||||
|
console.error(
|
||||||
|
'All tool calls found:',
|
||||||
|
toolLogs.map((l) => ({
|
||||||
|
name: l.toolRequest.name,
|
||||||
|
args: l.toolRequest.args,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
planWrite,
|
||||||
|
'Expected write_file to be called for plan.md',
|
||||||
|
).toBeDefined();
|
||||||
|
expect(
|
||||||
|
planWrite?.toolRequest.success,
|
||||||
|
`Expected write_file to succeed, but it failed with error: ${planWrite?.toolRequest.error}`,
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should deny write_file to non-plans directory in plan mode', async () => {
|
it('should deny write_file to non-plans directory in plan mode', async () => {
|
||||||
@@ -131,19 +131,11 @@ describe('Plan Mode', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Disable the interactive terminal setup prompt in tests
|
await rig.run({
|
||||||
writeFileSync(
|
|
||||||
join(rig.homeDir!, GEMINI_DIR, 'state.json'),
|
|
||||||
JSON.stringify({ terminalSetupPromptShown: true }, null, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
const run = await rig.runInteractive({
|
|
||||||
approvalMode: 'plan',
|
approvalMode: 'plan',
|
||||||
|
args: 'Create a file called hello.txt in the current directory.',
|
||||||
});
|
});
|
||||||
|
|
||||||
await run.type('Create a file called hello.txt in the current directory.');
|
|
||||||
await run.type('\r');
|
|
||||||
|
|
||||||
const toolLogs = rig.readToolLogs();
|
const toolLogs = rig.readToolLogs();
|
||||||
const writeLog = toolLogs.find(
|
const writeLog = toolLogs.find(
|
||||||
(l) =>
|
(l) =>
|
||||||
@@ -151,10 +143,11 @@ describe('Plan Mode', () => {
|
|||||||
l.toolRequest.args.includes('hello.txt'),
|
l.toolRequest.args.includes('hello.txt'),
|
||||||
);
|
);
|
||||||
|
|
||||||
// In Plan Mode, writes outside the plans directory should be blocked.
|
|
||||||
// Model is undeterministic, sometimes it doesn't even try, but if it does, it must fail.
|
|
||||||
if (writeLog) {
|
if (writeLog) {
|
||||||
expect(writeLog.toolRequest.success).toBe(false);
|
expect(
|
||||||
|
writeLog.toolRequest.success,
|
||||||
|
'Expected write_file to non-plans dir to fail',
|
||||||
|
).toBe(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -169,28 +162,69 @@ describe('Plan Mode', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Disable the interactive terminal setup prompt in tests
|
|
||||||
writeFileSync(
|
|
||||||
join(rig.homeDir!, GEMINI_DIR, 'state.json'),
|
|
||||||
JSON.stringify({ terminalSetupPromptShown: true }, null, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Start in default mode and ask to enter plan mode.
|
|
||||||
await rig.run({
|
await rig.run({
|
||||||
approvalMode: 'default',
|
approvalMode: 'default',
|
||||||
stdin:
|
args: 'I want to perform a complex refactoring. Please enter plan mode so we can design it first.',
|
||||||
'I want to perform a complex refactoring. Please enter plan mode so we can design it first.',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const enterPlanCallFound = await rig.waitForToolCall('enter_plan_mode');
|
|
||||||
expect(enterPlanCallFound, 'Expected enter_plan_mode to be called').toBe(
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
const toolLogs = rig.readToolLogs();
|
const toolLogs = rig.readToolLogs();
|
||||||
const enterLog = toolLogs.find(
|
const enterLog = toolLogs.find(
|
||||||
(l) => l.toolRequest.name === 'enter_plan_mode',
|
(l) => l.toolRequest.name === 'enter_plan_mode',
|
||||||
);
|
);
|
||||||
|
expect(enterLog, 'Expected enter_plan_mode to be called').toBeDefined();
|
||||||
expect(enterLog?.toolRequest.success).toBe(true);
|
expect(enterLog?.toolRequest.success).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should allow write_file to the plans directory in plan mode even without a session ID', async () => {
|
||||||
|
const plansDir = '.gemini/tmp/foo/plans';
|
||||||
|
const testName =
|
||||||
|
'should allow write_file to the plans directory in plan mode even without a session ID';
|
||||||
|
|
||||||
|
await rig.setup(testName, {
|
||||||
|
settings: {
|
||||||
|
experimental: { plan: true },
|
||||||
|
tools: {
|
||||||
|
core: ['write_file', 'read_file', 'list_directory'],
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
defaultApprovalMode: 'plan',
|
||||||
|
plan: {
|
||||||
|
directory: plansDir,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await rig.run({
|
||||||
|
approvalMode: 'plan',
|
||||||
|
args: 'Create a file called plan-no-session.md in the plans directory.',
|
||||||
|
});
|
||||||
|
|
||||||
|
const toolLogs = rig.readToolLogs();
|
||||||
|
const planWrite = toolLogs.find(
|
||||||
|
(l) =>
|
||||||
|
l.toolRequest.name === 'write_file' &&
|
||||||
|
l.toolRequest.args.includes('plans') &&
|
||||||
|
l.toolRequest.args.includes('plan-no-session.md'),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!planWrite) {
|
||||||
|
console.error(
|
||||||
|
'All tool calls found:',
|
||||||
|
toolLogs.map((l) => ({
|
||||||
|
name: l.toolRequest.name,
|
||||||
|
args: l.toolRequest.args,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
planWrite,
|
||||||
|
'Expected write_file to be called for plan-no-session.md',
|
||||||
|
).toBeDefined();
|
||||||
|
expect(
|
||||||
|
planWrite?.toolRequest.success,
|
||||||
|
`Expected write_file to succeed, but it failed with error: ${planWrite?.toolRequest.error}`,
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Generated
+9
-9
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli",
|
"name": "@google/gemini-cli",
|
||||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
"version": "0.36.0-preview.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@google/gemini-cli",
|
"name": "@google/gemini-cli",
|
||||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
"version": "0.36.0-preview.3",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
@@ -17413,7 +17413,7 @@
|
|||||||
},
|
},
|
||||||
"packages/a2a-server": {
|
"packages/a2a-server": {
|
||||||
"name": "@google/gemini-cli-a2a-server",
|
"name": "@google/gemini-cli-a2a-server",
|
||||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
"version": "0.36.0-preview.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@a2a-js/sdk": "0.3.11",
|
"@a2a-js/sdk": "0.3.11",
|
||||||
"@google-cloud/storage": "^7.16.0",
|
"@google-cloud/storage": "^7.16.0",
|
||||||
@@ -17528,7 +17528,7 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@google/gemini-cli",
|
"name": "@google/gemini-cli",
|
||||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
"version": "0.36.0-preview.3",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@agentclientprotocol/sdk": "^0.16.1",
|
"@agentclientprotocol/sdk": "^0.16.1",
|
||||||
@@ -17700,7 +17700,7 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@google/gemini-cli-core",
|
"name": "@google/gemini-cli-core",
|
||||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
"version": "0.36.0-preview.3",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@a2a-js/sdk": "0.3.11",
|
"@a2a-js/sdk": "0.3.11",
|
||||||
@@ -17966,7 +17966,7 @@
|
|||||||
},
|
},
|
||||||
"packages/devtools": {
|
"packages/devtools": {
|
||||||
"name": "@google/gemini-cli-devtools",
|
"name": "@google/gemini-cli-devtools",
|
||||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
"version": "0.36.0-preview.3",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ws": "^8.16.0"
|
"ws": "^8.16.0"
|
||||||
@@ -17981,7 +17981,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk": {
|
"packages/sdk": {
|
||||||
"name": "@google/gemini-cli-sdk",
|
"name": "@google/gemini-cli-sdk",
|
||||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
"version": "0.36.0-preview.3",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/gemini-cli-core": "file:../core",
|
"@google/gemini-cli-core": "file:../core",
|
||||||
@@ -17998,7 +17998,7 @@
|
|||||||
},
|
},
|
||||||
"packages/test-utils": {
|
"packages/test-utils": {
|
||||||
"name": "@google/gemini-cli-test-utils",
|
"name": "@google/gemini-cli-test-utils",
|
||||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
"version": "0.36.0-preview.3",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/gemini-cli-core": "file:../core",
|
"@google/gemini-cli-core": "file:../core",
|
||||||
@@ -18015,7 +18015,7 @@
|
|||||||
},
|
},
|
||||||
"packages/vscode-ide-companion": {
|
"packages/vscode-ide-companion": {
|
||||||
"name": "gemini-cli-vscode-ide-companion",
|
"name": "gemini-cli-vscode-ide-companion",
|
||||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
"version": "0.36.0-preview.3",
|
||||||
"license": "LICENSE",
|
"license": "LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli",
|
"name": "@google/gemini-cli",
|
||||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
"version": "0.36.0-preview.3",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0-nightly.20260317.2f90b4653"
|
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0-preview.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli-a2a-server",
|
"name": "@google/gemini-cli-a2a-server",
|
||||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
"version": "0.36.0-preview.3",
|
||||||
"description": "Gemini CLI A2A Server",
|
"description": "Gemini CLI A2A Server",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
|||||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
|
PRIORITY_YOLO_ALLOW_ALL: 998,
|
||||||
Config: vi.fn().mockImplementation((params) => {
|
Config: vi.fn().mockImplementation((params) => {
|
||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
...params,
|
...params,
|
||||||
@@ -341,11 +342,11 @@ describe('loadConfig', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should default enableAgents to false when not provided', async () => {
|
it('should default enableAgents to true when not provided', async () => {
|
||||||
await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
||||||
expect(Config).toHaveBeenCalledWith(
|
expect(Config).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
enableAgents: false,
|
enableAgents: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export async function loadConfig(
|
|||||||
interactive: !isHeadlessMode(),
|
interactive: !isHeadlessMode(),
|
||||||
enableInteractiveShell: !isHeadlessMode(),
|
enableInteractiveShell: !isHeadlessMode(),
|
||||||
ptyInfo: 'auto',
|
ptyInfo: 'auto',
|
||||||
enableAgents: settings.experimental?.enableAgents ?? false,
|
enableAgents: settings.experimental?.enableAgents ?? true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const fileService = new FileDiscoveryService(workspaceDir, {
|
const fileService = new FileDiscoveryService(workspaceDir, {
|
||||||
|
|||||||
+5
-21
@@ -6,19 +6,12 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// --- Fast Path for Version ---
|
import { main } from './src/gemini.js';
|
||||||
// We check for version flags at the very top to avoid loading any heavy dependencies.
|
import { FatalError, writeToStderr } from '@google/gemini-cli-core';
|
||||||
// process.env.CLI_VERSION is defined during the build process by esbuild.
|
import { runExitCleanup } from './src/utils/cleanup.js';
|
||||||
if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
||||||
console.log(process.env['CLI_VERSION'] || 'unknown');
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Global Entry Point ---
|
// --- Global Entry Point ---
|
||||||
|
|
||||||
let writeToStderrFn: (message: string) => void = (msg) =>
|
|
||||||
process.stderr.write(msg);
|
|
||||||
|
|
||||||
// Suppress known race condition error in node-pty on Windows
|
// Suppress known race condition error in node-pty on Windows
|
||||||
// Tracking bug: https://github.com/microsoft/node-pty/issues/827
|
// Tracking bug: https://github.com/microsoft/node-pty/issues/827
|
||||||
process.on('uncaughtException', (error) => {
|
process.on('uncaughtException', (error) => {
|
||||||
@@ -35,22 +28,13 @@ process.on('uncaughtException', (error) => {
|
|||||||
// For other errors, we rely on the default behavior, but since we attached a listener,
|
// For other errors, we rely on the default behavior, but since we attached a listener,
|
||||||
// we must manually replicate it.
|
// we must manually replicate it.
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
writeToStderrFn(error.stack + '\n');
|
writeToStderr(error.stack + '\n');
|
||||||
} else {
|
} else {
|
||||||
writeToStderrFn(String(error) + '\n');
|
writeToStderr(String(error) + '\n');
|
||||||
}
|
}
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
const [{ main }, { FatalError, writeToStderr }, { runExitCleanup }] =
|
|
||||||
await Promise.all([
|
|
||||||
import('./src/gemini.js'),
|
|
||||||
import('@google/gemini-cli-core'),
|
|
||||||
import('./src/utils/cleanup.js'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
writeToStderrFn = writeToStderr;
|
|
||||||
|
|
||||||
main().catch(async (error) => {
|
main().catch(async (error) => {
|
||||||
// Set a timeout to force exit if cleanup hangs
|
// Set a timeout to force exit if cleanup hangs
|
||||||
const cleanupTimeout = setTimeout(() => {
|
const cleanupTimeout = setTimeout(() => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli",
|
"name": "@google/gemini-cli",
|
||||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
"version": "0.36.0-preview.3",
|
||||||
"description": "Gemini CLI",
|
"description": "Gemini CLI",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"config": {
|
"config": {
|
||||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0-nightly.20260317.2f90b4653"
|
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0-preview.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@agentclientprotocol/sdk": "^0.16.1",
|
"@agentclientprotocol/sdk": "^0.16.1",
|
||||||
|
|||||||
@@ -400,7 +400,7 @@ describe('SettingsSchema', () => {
|
|||||||
expect(setting).toBeDefined();
|
expect(setting).toBeDefined();
|
||||||
expect(setting.type).toBe('boolean');
|
expect(setting.type).toBe('boolean');
|
||||||
expect(setting.category).toBe('Experimental');
|
expect(setting.category).toBe('Experimental');
|
||||||
expect(setting.default).toBe(false);
|
expect(setting.default).toBe(true);
|
||||||
expect(setting.requiresRestart).toBe(true);
|
expect(setting.requiresRestart).toBe(true);
|
||||||
expect(setting.showInDialog).toBe(false);
|
expect(setting.showInDialog).toBe(false);
|
||||||
expect(setting.description).toBe('Enable local and remote subagents.');
|
expect(setting.description).toBe('Enable local and remote subagents.');
|
||||||
|
|||||||
@@ -1932,7 +1932,7 @@ const SETTINGS_SCHEMA = {
|
|||||||
label: 'Enable Agents',
|
label: 'Enable Agents',
|
||||||
category: 'Experimental',
|
category: 'Experimental',
|
||||||
requiresRestart: true,
|
requiresRestart: true,
|
||||||
default: false,
|
default: true,
|
||||||
description: 'Enable local and remote subagents.',
|
description: 'Enable local and remote subagents.',
|
||||||
showInDialog: false,
|
showInDialog: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli-core",
|
"name": "@google/gemini-cli-core",
|
||||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
"version": "0.36.0-preview.3",
|
||||||
"description": "Gemini CLI Core",
|
"description": "Gemini CLI Core",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -128,7 +128,10 @@ describe('A2AClientManager', () => {
|
|||||||
|
|
||||||
describe('getInstance / dispatcher initialization', () => {
|
describe('getInstance / dispatcher initialization', () => {
|
||||||
it('should use UndiciAgent when no proxy is configured', async () => {
|
it('should use UndiciAgent when no proxy is configured', async () => {
|
||||||
await manager.loadAgent('TestAgent', 'http://test.agent/card');
|
await manager.loadAgent('TestAgent', {
|
||||||
|
type: 'url',
|
||||||
|
url: 'http://test.agent/card',
|
||||||
|
});
|
||||||
|
|
||||||
const resolverOptions = vi.mocked(DefaultAgentCardResolver).mock
|
const resolverOptions = vi.mocked(DefaultAgentCardResolver).mock
|
||||||
.calls[0][0];
|
.calls[0][0];
|
||||||
@@ -153,7 +156,10 @@ describe('A2AClientManager', () => {
|
|||||||
} as Config;
|
} as Config;
|
||||||
|
|
||||||
manager = new A2AClientManager(mockConfigWithProxy);
|
manager = new A2AClientManager(mockConfigWithProxy);
|
||||||
await manager.loadAgent('TestProxyAgent', 'http://test.proxy.agent/card');
|
await manager.loadAgent('TestProxyAgent', {
|
||||||
|
type: 'url',
|
||||||
|
url: 'http://test.proxy.agent/card',
|
||||||
|
});
|
||||||
|
|
||||||
const resolverOptions = vi.mocked(DefaultAgentCardResolver).mock
|
const resolverOptions = vi.mocked(DefaultAgentCardResolver).mock
|
||||||
.calls[0][0];
|
.calls[0][0];
|
||||||
@@ -172,28 +178,40 @@ describe('A2AClientManager', () => {
|
|||||||
|
|
||||||
describe('loadAgent', () => {
|
describe('loadAgent', () => {
|
||||||
it('should create and cache an A2AClient', async () => {
|
it('should create and cache an A2AClient', async () => {
|
||||||
const agentCard = await manager.loadAgent(
|
const agentCard = await manager.loadAgent('TestAgent', {
|
||||||
'TestAgent',
|
type: 'url',
|
||||||
'http://test.agent/card',
|
url: 'http://test.agent/card',
|
||||||
);
|
});
|
||||||
expect(manager.getAgentCard('TestAgent')).toBe(agentCard);
|
expect(manager.getAgentCard('TestAgent')).toBe(agentCard);
|
||||||
expect(manager.getClient('TestAgent')).toBeDefined();
|
expect(manager.getClient('TestAgent')).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should configure ClientFactory with REST, JSON-RPC, and gRPC transports', async () => {
|
it('should configure ClientFactory with REST, JSON-RPC, and gRPC transports', async () => {
|
||||||
await manager.loadAgent('TestAgent', 'http://test.agent/card');
|
await manager.loadAgent('TestAgent', {
|
||||||
|
type: 'url',
|
||||||
|
url: 'http://test.agent/card',
|
||||||
|
});
|
||||||
expect(ClientFactoryOptions.createFrom).toHaveBeenCalled();
|
expect(ClientFactoryOptions.createFrom).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if an agent with the same name is already loaded', async () => {
|
it('should throw an error if an agent with the same name is already loaded', async () => {
|
||||||
await manager.loadAgent('TestAgent', 'http://test.agent/card');
|
await manager.loadAgent('TestAgent', {
|
||||||
|
type: 'url',
|
||||||
|
url: 'http://test.agent/card',
|
||||||
|
});
|
||||||
await expect(
|
await expect(
|
||||||
manager.loadAgent('TestAgent', 'http://test.agent/card'),
|
manager.loadAgent('TestAgent', {
|
||||||
|
type: 'url',
|
||||||
|
url: 'http://test.agent/card',
|
||||||
|
}),
|
||||||
).rejects.toThrow("Agent with name 'TestAgent' is already loaded.");
|
).rejects.toThrow("Agent with name 'TestAgent' is already loaded.");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use native fetch by default', async () => {
|
it('should use native fetch by default', async () => {
|
||||||
await manager.loadAgent('TestAgent', 'http://test.agent/card');
|
await manager.loadAgent('TestAgent', {
|
||||||
|
type: 'url',
|
||||||
|
url: 'http://test.agent/card',
|
||||||
|
});
|
||||||
expect(createAuthenticatingFetchWithRetry).not.toHaveBeenCalled();
|
expect(createAuthenticatingFetchWithRetry).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -204,7 +222,7 @@ describe('A2AClientManager', () => {
|
|||||||
};
|
};
|
||||||
await manager.loadAgent(
|
await manager.loadAgent(
|
||||||
'TestAgent',
|
'TestAgent',
|
||||||
'http://test.agent/card',
|
{ type: 'url', url: 'http://test.agent/card' },
|
||||||
customAuthHandler as unknown as AuthenticationHandler,
|
customAuthHandler as unknown as AuthenticationHandler,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -221,7 +239,7 @@ describe('A2AClientManager', () => {
|
|||||||
};
|
};
|
||||||
await manager.loadAgent(
|
await manager.loadAgent(
|
||||||
'AuthCardAgent',
|
'AuthCardAgent',
|
||||||
'http://authcard.agent/card',
|
{ type: 'url', url: 'http://authcard.agent/card' },
|
||||||
customAuthHandler as unknown as AuthenticationHandler,
|
customAuthHandler as unknown as AuthenticationHandler,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -252,7 +270,7 @@ describe('A2AClientManager', () => {
|
|||||||
|
|
||||||
await manager.loadAgent(
|
await manager.loadAgent(
|
||||||
'AuthCardAgent401',
|
'AuthCardAgent401',
|
||||||
'http://authcard.agent/card',
|
{ type: 'url', url: 'http://authcard.agent/card' },
|
||||||
customAuthHandler as unknown as AuthenticationHandler,
|
customAuthHandler as unknown as AuthenticationHandler,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -267,19 +285,65 @@ describe('A2AClientManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should log a debug message upon loading an agent', async () => {
|
it('should log a debug message upon loading an agent', async () => {
|
||||||
await manager.loadAgent('TestAgent', 'http://test.agent/card');
|
await manager.loadAgent('TestAgent', {
|
||||||
|
type: 'url',
|
||||||
|
url: 'http://test.agent/card',
|
||||||
|
});
|
||||||
expect(debugLogger.debug).toHaveBeenCalledWith(
|
expect(debugLogger.debug).toHaveBeenCalledWith(
|
||||||
expect.stringContaining("Loaded agent 'TestAgent'"),
|
expect.stringContaining("Loaded agent 'TestAgent'"),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should clear the cache', async () => {
|
it('should clear the cache', async () => {
|
||||||
await manager.loadAgent('TestAgent', 'http://test.agent/card');
|
await manager.loadAgent('TestAgent', {
|
||||||
|
type: 'url',
|
||||||
|
url: 'http://test.agent/card',
|
||||||
|
});
|
||||||
manager.clearCache();
|
manager.clearCache();
|
||||||
expect(manager.getAgentCard('TestAgent')).toBeUndefined();
|
expect(manager.getAgentCard('TestAgent')).toBeUndefined();
|
||||||
expect(manager.getClient('TestAgent')).toBeUndefined();
|
expect(manager.getClient('TestAgent')).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should load an agent from inline JSON without calling resolver', async () => {
|
||||||
|
const inlineJson = JSON.stringify(mockAgentCard);
|
||||||
|
const agentCard = await manager.loadAgent('JsonAgent', {
|
||||||
|
type: 'json',
|
||||||
|
json: inlineJson,
|
||||||
|
});
|
||||||
|
expect(agentCard).toBeDefined();
|
||||||
|
expect(agentCard.name).toBe('test-agent');
|
||||||
|
expect(manager.getAgentCard('JsonAgent')).toBe(agentCard);
|
||||||
|
expect(manager.getClient('JsonAgent')).toBeDefined();
|
||||||
|
// Resolver should not have been called for inline JSON
|
||||||
|
const resolverInstance = vi.mocked(DefaultAgentCardResolver).mock
|
||||||
|
.results[0]?.value;
|
||||||
|
if (resolverInstance) {
|
||||||
|
expect(resolverInstance.resolve).not.toHaveBeenCalled();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw a descriptive error for invalid inline JSON', async () => {
|
||||||
|
await expect(
|
||||||
|
manager.loadAgent('BadJsonAgent', {
|
||||||
|
type: 'json',
|
||||||
|
json: 'not valid json {{',
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(
|
||||||
|
/Failed to parse inline agent card JSON for agent 'BadJsonAgent'/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log "inline JSON" for JSON-loaded agents', async () => {
|
||||||
|
const inlineJson = JSON.stringify(mockAgentCard);
|
||||||
|
await manager.loadAgent('JsonLogAgent', {
|
||||||
|
type: 'json',
|
||||||
|
json: inlineJson,
|
||||||
|
});
|
||||||
|
expect(debugLogger.debug).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('inline JSON'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should throw if resolveAgentCard fails', async () => {
|
it('should throw if resolveAgentCard fails', async () => {
|
||||||
const resolverInstance = {
|
const resolverInstance = {
|
||||||
resolve: vi.fn().mockRejectedValue(new Error('Resolution failed')),
|
resolve: vi.fn().mockRejectedValue(new Error('Resolution failed')),
|
||||||
@@ -289,7 +353,10 @@ describe('A2AClientManager', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
manager.loadAgent('FailAgent', 'http://fail.agent'),
|
manager.loadAgent('FailAgent', {
|
||||||
|
type: 'url',
|
||||||
|
url: 'http://fail.agent',
|
||||||
|
}),
|
||||||
).rejects.toThrow('Resolution failed');
|
).rejects.toThrow('Resolution failed');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -304,7 +371,10 @@ describe('A2AClientManager', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
manager.loadAgent('FailAgent', 'http://fail.agent'),
|
manager.loadAgent('FailAgent', {
|
||||||
|
type: 'url',
|
||||||
|
url: 'http://fail.agent',
|
||||||
|
}),
|
||||||
).rejects.toThrow('Factory failed');
|
).rejects.toThrow('Factory failed');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -318,7 +388,10 @@ describe('A2AClientManager', () => {
|
|||||||
|
|
||||||
describe('sendMessageStream', () => {
|
describe('sendMessageStream', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await manager.loadAgent('TestAgent', 'http://test.agent/card');
|
await manager.loadAgent('TestAgent', {
|
||||||
|
type: 'url',
|
||||||
|
url: 'http://test.agent/card',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send a message and return a stream', async () => {
|
it('should send a message and return a stream', async () => {
|
||||||
@@ -433,7 +506,10 @@ describe('A2AClientManager', () => {
|
|||||||
|
|
||||||
describe('getTask', () => {
|
describe('getTask', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await manager.loadAgent('TestAgent', 'http://test.agent/card');
|
await manager.loadAgent('TestAgent', {
|
||||||
|
type: 'url',
|
||||||
|
url: 'http://test.agent/card',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get a task from the correct agent', async () => {
|
it('should get a task from the correct agent', async () => {
|
||||||
@@ -462,7 +538,10 @@ describe('A2AClientManager', () => {
|
|||||||
|
|
||||||
describe('cancelTask', () => {
|
describe('cancelTask', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await manager.loadAgent('TestAgent', 'http://test.agent/card');
|
await manager.loadAgent('TestAgent', {
|
||||||
|
type: 'url',
|
||||||
|
url: 'http://test.agent/card',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should cancel a task on the correct agent', async () => {
|
it('should cancel a task on the correct agent', async () => {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import * as grpc from '@grpc/grpc-js';
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { Agent as UndiciAgent, ProxyAgent } from 'undici';
|
import { Agent as UndiciAgent, ProxyAgent } from 'undici';
|
||||||
import { normalizeAgentCard } from './a2aUtils.js';
|
import { normalizeAgentCard } from './a2aUtils.js';
|
||||||
|
import type { AgentCardLoadOptions } from './types.js';
|
||||||
import type { Config } from '../config/config.js';
|
import type { Config } from '../config/config.js';
|
||||||
import { debugLogger } from '../utils/debugLogger.js';
|
import { debugLogger } from '../utils/debugLogger.js';
|
||||||
import { classifyAgentError } from './a2a-errors.js';
|
import { classifyAgentError } from './a2a-errors.js';
|
||||||
@@ -85,7 +86,7 @@ export class A2AClientManager {
|
|||||||
*/
|
*/
|
||||||
async loadAgent(
|
async loadAgent(
|
||||||
name: string,
|
name: string,
|
||||||
agentCardUrl: string,
|
options: AgentCardLoadOptions,
|
||||||
authHandler?: AuthenticationHandler,
|
authHandler?: AuthenticationHandler,
|
||||||
): Promise<AgentCard> {
|
): Promise<AgentCard> {
|
||||||
if (this.clients.has(name) && this.agentCards.has(name)) {
|
if (this.clients.has(name) && this.agentCards.has(name)) {
|
||||||
@@ -119,7 +120,24 @@ export class A2AClientManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resolver = new DefaultAgentCardResolver({ fetchImpl: cardFetch });
|
const resolver = new DefaultAgentCardResolver({ fetchImpl: cardFetch });
|
||||||
const rawCard = await resolver.resolve(agentCardUrl, '');
|
|
||||||
|
let rawCard: unknown;
|
||||||
|
let urlIdentifier = 'inline JSON';
|
||||||
|
|
||||||
|
if (options.type === 'json') {
|
||||||
|
try {
|
||||||
|
rawCard = JSON.parse(options.json);
|
||||||
|
} catch (error) {
|
||||||
|
const msg = error instanceof Error ? error.message : String(error);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to parse inline agent card JSON for agent '${name}': ${msg}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
urlIdentifier = options.url;
|
||||||
|
rawCard = await resolver.resolve(options.url, '');
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Remove normalizeAgentCard once @a2a-js/sdk handles
|
// TODO: Remove normalizeAgentCard once @a2a-js/sdk handles
|
||||||
// proto field name aliases (supportedInterfaces → additionalInterfaces,
|
// proto field name aliases (supportedInterfaces → additionalInterfaces,
|
||||||
// protocolBinding → transport).
|
// protocolBinding → transport).
|
||||||
@@ -153,12 +171,12 @@ export class A2AClientManager {
|
|||||||
this.agentCards.set(name, agentCard);
|
this.agentCards.set(name, agentCard);
|
||||||
|
|
||||||
debugLogger.debug(
|
debugLogger.debug(
|
||||||
`[A2AClientManager] Loaded agent '${name}' from ${agentCardUrl}`,
|
`[A2AClientManager] Loaded agent '${name}' from ${urlIdentifier}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return agentCard;
|
return agentCard;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
throw classifyAgentError(name, agentCardUrl, error);
|
throw classifyAgentError(name, urlIdentifier, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ import {
|
|||||||
DEFAULT_MAX_TIME_MINUTES,
|
DEFAULT_MAX_TIME_MINUTES,
|
||||||
DEFAULT_MAX_TURNS,
|
DEFAULT_MAX_TURNS,
|
||||||
type LocalAgentDefinition,
|
type LocalAgentDefinition,
|
||||||
|
type RemoteAgentDefinition,
|
||||||
|
getAgentCardLoadOptions,
|
||||||
|
getRemoteAgentTargetUrl,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
describe('loader', () => {
|
describe('loader', () => {
|
||||||
@@ -232,6 +235,75 @@ agent_card_url: https://example.com/card
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should parse a remote agent with agent_card_json', async () => {
|
||||||
|
const cardJson = JSON.stringify({
|
||||||
|
name: 'json-agent',
|
||||||
|
url: 'https://example.com/agent',
|
||||||
|
version: '1.0',
|
||||||
|
});
|
||||||
|
const filePath = await writeAgentMarkdown(`---
|
||||||
|
kind: remote
|
||||||
|
name: json-remote
|
||||||
|
description: A JSON-based remote agent
|
||||||
|
agent_card_json: '${cardJson}'
|
||||||
|
---
|
||||||
|
`);
|
||||||
|
const result = await parseAgentMarkdown(filePath);
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]).toMatchObject({
|
||||||
|
kind: 'remote',
|
||||||
|
name: 'json-remote',
|
||||||
|
description: 'A JSON-based remote agent',
|
||||||
|
agent_card_json: cardJson,
|
||||||
|
});
|
||||||
|
// Should NOT have agent_card_url
|
||||||
|
expect(result[0]).not.toHaveProperty('agent_card_url');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject agent_card_json that is not valid JSON', async () => {
|
||||||
|
const filePath = await writeAgentMarkdown(`---
|
||||||
|
kind: remote
|
||||||
|
name: invalid-json-remote
|
||||||
|
agent_card_json: "not valid json {{"
|
||||||
|
---
|
||||||
|
`);
|
||||||
|
await expect(parseAgentMarkdown(filePath)).rejects.toThrow(
|
||||||
|
/agent_card_json must be valid JSON/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject a remote agent with both agent_card_url and agent_card_json', async () => {
|
||||||
|
const filePath = await writeAgentMarkdown(`---
|
||||||
|
kind: remote
|
||||||
|
name: both-fields
|
||||||
|
agent_card_url: https://example.com/card
|
||||||
|
agent_card_json: '{"name":"test"}'
|
||||||
|
---
|
||||||
|
`);
|
||||||
|
await expect(parseAgentMarkdown(filePath)).rejects.toThrow(
|
||||||
|
/Validation failed/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should infer remote kind from agent_card_json', async () => {
|
||||||
|
const cardJson = JSON.stringify({
|
||||||
|
name: 'test',
|
||||||
|
url: 'https://example.com',
|
||||||
|
});
|
||||||
|
const filePath = await writeAgentMarkdown(`---
|
||||||
|
name: inferred-json-remote
|
||||||
|
agent_card_json: '${cardJson}'
|
||||||
|
---
|
||||||
|
`);
|
||||||
|
const result = await parseAgentMarkdown(filePath);
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]).toMatchObject({
|
||||||
|
kind: 'remote',
|
||||||
|
name: 'inferred-json-remote',
|
||||||
|
agent_card_json: cardJson,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should throw AgentLoadError if agent name is not a valid slug', async () => {
|
it('should throw AgentLoadError if agent name is not a valid slug', async () => {
|
||||||
const filePath = await writeAgentMarkdown(`---
|
const filePath = await writeAgentMarkdown(`---
|
||||||
name: Invalid Name With Spaces
|
name: Invalid Name With Spaces
|
||||||
@@ -242,6 +314,99 @@ Body`);
|
|||||||
/Name must be a valid slug/,
|
/Name must be a valid slug/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('error formatting and kind inference', () => {
|
||||||
|
it('should only show local agent errors when kind is inferred as local (via kind field)', async () => {
|
||||||
|
const filePath = await writeAgentMarkdown(`---
|
||||||
|
kind: local
|
||||||
|
name: invalid-local
|
||||||
|
# missing description
|
||||||
|
---
|
||||||
|
Body`);
|
||||||
|
const error = await parseAgentMarkdown(filePath).catch((e) => e);
|
||||||
|
expect(error).toBeInstanceOf(AgentLoadError);
|
||||||
|
expect(error.message).toContain('Validation failed');
|
||||||
|
expect(error.message).toContain('description: Required');
|
||||||
|
expect(error.message).not.toContain('Remote Agent');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only show local agent errors when kind is inferred as local (via local-specific keys)', async () => {
|
||||||
|
const filePath = await writeAgentMarkdown(`---
|
||||||
|
name: invalid-local
|
||||||
|
# missing description
|
||||||
|
tools:
|
||||||
|
- run_shell_command
|
||||||
|
---
|
||||||
|
Body`);
|
||||||
|
const error = await parseAgentMarkdown(filePath).catch((e) => e);
|
||||||
|
expect(error).toBeInstanceOf(AgentLoadError);
|
||||||
|
expect(error.message).toContain('Validation failed');
|
||||||
|
expect(error.message).toContain('description: Required');
|
||||||
|
expect(error.message).not.toContain('Remote Agent');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only show remote agent errors when kind is inferred as remote (via kind field)', async () => {
|
||||||
|
const filePath = await writeAgentMarkdown(`---
|
||||||
|
kind: remote
|
||||||
|
name: invalid-remote
|
||||||
|
# missing agent_card_url
|
||||||
|
---
|
||||||
|
Body`);
|
||||||
|
const error = await parseAgentMarkdown(filePath).catch((e) => e);
|
||||||
|
expect(error).toBeInstanceOf(AgentLoadError);
|
||||||
|
expect(error.message).toContain('Validation failed');
|
||||||
|
expect(error.message).toContain('agent_card_url: Required');
|
||||||
|
expect(error.message).not.toContain('Local Agent');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only show remote agent errors when kind is inferred as remote (via remote-specific keys)', async () => {
|
||||||
|
const filePath = await writeAgentMarkdown(`---
|
||||||
|
name: invalid-remote
|
||||||
|
auth:
|
||||||
|
type: apiKey
|
||||||
|
key: my_key
|
||||||
|
# missing agent_card_url
|
||||||
|
---
|
||||||
|
Body`);
|
||||||
|
const error = await parseAgentMarkdown(filePath).catch((e) => e);
|
||||||
|
expect(error).toBeInstanceOf(AgentLoadError);
|
||||||
|
expect(error.message).toContain('Validation failed');
|
||||||
|
expect(error.message).toContain('agent_card_url: Required');
|
||||||
|
expect(error.message).not.toContain('Local Agent');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show errors for both types when kind cannot be inferred', async () => {
|
||||||
|
const filePath = await writeAgentMarkdown(`---
|
||||||
|
name: invalid-unknown
|
||||||
|
# missing description and missing agent_card_url, no specific keys
|
||||||
|
---
|
||||||
|
Body`);
|
||||||
|
const error = await parseAgentMarkdown(filePath).catch((e) => e);
|
||||||
|
expect(error).toBeInstanceOf(AgentLoadError);
|
||||||
|
expect(error.message).toContain('Validation failed');
|
||||||
|
expect(error.message).toContain('(Local Agent)');
|
||||||
|
expect(error.message).toContain('(Remote Agent)');
|
||||||
|
expect(error.message).toContain('description: Required');
|
||||||
|
expect(error.message).toContain('agent_card_url: Required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format errors without a stray colon when the path is empty (e.g. strict object with unknown keys)', async () => {
|
||||||
|
const filePath = await writeAgentMarkdown(`---
|
||||||
|
kind: local
|
||||||
|
name: my-agent
|
||||||
|
description: test
|
||||||
|
unknown_field: true
|
||||||
|
---
|
||||||
|
Body`);
|
||||||
|
const error = await parseAgentMarkdown(filePath).catch((e) => e);
|
||||||
|
expect(error).toBeInstanceOf(AgentLoadError);
|
||||||
|
expect(error.message).toContain(
|
||||||
|
"Unrecognized key(s) in object: 'unknown_field'",
|
||||||
|
);
|
||||||
|
expect(error.message).not.toContain(': Unrecognized key(s)');
|
||||||
|
expect(error.message).not.toContain('Required');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('markdownToAgentDefinition', () => {
|
describe('markdownToAgentDefinition', () => {
|
||||||
@@ -372,6 +537,40 @@ Body`);
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should convert remote agent definition with agent_card_json', () => {
|
||||||
|
const cardJson = JSON.stringify({
|
||||||
|
name: 'json-agent',
|
||||||
|
url: 'https://example.com/agent',
|
||||||
|
});
|
||||||
|
const markdown = {
|
||||||
|
kind: 'remote' as const,
|
||||||
|
name: 'json-remote',
|
||||||
|
description: 'A JSON remote agent',
|
||||||
|
agent_card_json: cardJson,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = markdownToAgentDefinition(
|
||||||
|
markdown,
|
||||||
|
) as RemoteAgentDefinition;
|
||||||
|
expect(result.kind).toBe('remote');
|
||||||
|
expect(result.name).toBe('json-remote');
|
||||||
|
expect(result.agentCardJson).toBe(cardJson);
|
||||||
|
expect(result.agentCardUrl).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw for remote agent with neither agent_card_url nor agent_card_json', () => {
|
||||||
|
// Cast to bypass compile-time check — this tests the runtime guard
|
||||||
|
const markdown = {
|
||||||
|
kind: 'remote' as const,
|
||||||
|
name: 'no-card-agent',
|
||||||
|
description: 'Missing card info',
|
||||||
|
} as Parameters<typeof markdownToAgentDefinition>[0];
|
||||||
|
|
||||||
|
expect(() => markdownToAgentDefinition(markdown)).toThrow(
|
||||||
|
/neither agent_card_json nor agent_card_url/,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('loadAgentsFromDirectory', () => {
|
describe('loadAgentsFromDirectory', () => {
|
||||||
@@ -744,5 +943,103 @@ auth:
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw an error for an unknown auth type in markdownToAgentDefinition', () => {
|
||||||
|
const markdown = {
|
||||||
|
kind: 'remote' as const,
|
||||||
|
name: 'unknown-auth-agent',
|
||||||
|
agent_card_url: 'https://example.com/card',
|
||||||
|
auth: {
|
||||||
|
type: 'apiKey' as const,
|
||||||
|
key: 'some-key',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mutate the object at runtime to bypass TypeScript compile-time checks cleanly
|
||||||
|
Object.assign(markdown.auth, { type: 'some-unknown-type' });
|
||||||
|
|
||||||
|
expect(() => markdownToAgentDefinition(markdown)).toThrow(
|
||||||
|
/Unknown auth type: some-unknown-type/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAgentCardLoadOptions', () => {
|
||||||
|
it('should return json options when agentCardJson is present', () => {
|
||||||
|
const def = {
|
||||||
|
name: 'test',
|
||||||
|
agentCardJson: '{"url":"http://x"}',
|
||||||
|
} as RemoteAgentDefinition;
|
||||||
|
const opts = getAgentCardLoadOptions(def);
|
||||||
|
expect(opts).toEqual({ type: 'json', json: '{"url":"http://x"}' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return url options when agentCardUrl is present', () => {
|
||||||
|
const def = {
|
||||||
|
name: 'test',
|
||||||
|
agentCardUrl: 'http://x/card',
|
||||||
|
} as RemoteAgentDefinition;
|
||||||
|
const opts = getAgentCardLoadOptions(def);
|
||||||
|
expect(opts).toEqual({ type: 'url', url: 'http://x/card' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prefer agentCardJson over agentCardUrl when both present', () => {
|
||||||
|
const def = {
|
||||||
|
name: 'test',
|
||||||
|
agentCardJson: '{"url":"http://x"}',
|
||||||
|
agentCardUrl: 'http://x/card',
|
||||||
|
} as RemoteAgentDefinition;
|
||||||
|
const opts = getAgentCardLoadOptions(def);
|
||||||
|
expect(opts.type).toBe('json');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when neither is present', () => {
|
||||||
|
const def = { name: 'orphan' } as RemoteAgentDefinition;
|
||||||
|
expect(() => getAgentCardLoadOptions(def)).toThrow(
|
||||||
|
/Remote agent 'orphan' has neither agentCardUrl nor agentCardJson/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getRemoteAgentTargetUrl', () => {
|
||||||
|
it('should return agentCardUrl when present', () => {
|
||||||
|
const def = {
|
||||||
|
name: 'test',
|
||||||
|
agentCardUrl: 'http://x/card',
|
||||||
|
} as RemoteAgentDefinition;
|
||||||
|
expect(getRemoteAgentTargetUrl(def)).toBe('http://x/card');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract url from agentCardJson when agentCardUrl is absent', () => {
|
||||||
|
const def = {
|
||||||
|
name: 'test',
|
||||||
|
agentCardJson: JSON.stringify({
|
||||||
|
name: 'agent',
|
||||||
|
url: 'https://example.com/agent',
|
||||||
|
}),
|
||||||
|
} as RemoteAgentDefinition;
|
||||||
|
expect(getRemoteAgentTargetUrl(def)).toBe('https://example.com/agent');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined when JSON has no url field', () => {
|
||||||
|
const def = {
|
||||||
|
name: 'test',
|
||||||
|
agentCardJson: JSON.stringify({ name: 'agent' }),
|
||||||
|
} as RemoteAgentDefinition;
|
||||||
|
expect(getRemoteAgentTargetUrl(def)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined when agentCardJson is invalid JSON', () => {
|
||||||
|
const def = {
|
||||||
|
name: 'test',
|
||||||
|
agentCardJson: 'not json',
|
||||||
|
} as RemoteAgentDefinition;
|
||||||
|
expect(getRemoteAgentTargetUrl(def)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined when neither field is present', () => {
|
||||||
|
const def = { name: 'test' } as RemoteAgentDefinition;
|
||||||
|
expect(getRemoteAgentTargetUrl(def)).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import * as crypto from 'node:crypto';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import {
|
import {
|
||||||
type AgentDefinition,
|
type AgentDefinition,
|
||||||
|
type RemoteAgentDefinition,
|
||||||
DEFAULT_MAX_TURNS,
|
DEFAULT_MAX_TURNS,
|
||||||
DEFAULT_MAX_TIME_MINUTES,
|
DEFAULT_MAX_TIME_MINUTES,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
@@ -21,79 +22,6 @@ import { isValidToolName } from '../tools/tool-names.js';
|
|||||||
import { FRONTMATTER_REGEX } from '../skills/skillLoader.js';
|
import { FRONTMATTER_REGEX } from '../skills/skillLoader.js';
|
||||||
import { getErrorMessage } from '../utils/errors.js';
|
import { getErrorMessage } from '../utils/errors.js';
|
||||||
|
|
||||||
/**
|
|
||||||
* DTO for Markdown parsing - represents the structure from frontmatter.
|
|
||||||
*/
|
|
||||||
interface FrontmatterBaseAgentDefinition {
|
|
||||||
name: string;
|
|
||||||
display_name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FrontmatterMCPServerConfig {
|
|
||||||
command?: string;
|
|
||||||
args?: string[];
|
|
||||||
env?: Record<string, string>;
|
|
||||||
cwd?: string;
|
|
||||||
url?: string;
|
|
||||||
http_url?: string;
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
tcp?: string;
|
|
||||||
type?: 'sse' | 'http';
|
|
||||||
timeout?: number;
|
|
||||||
trust?: boolean;
|
|
||||||
description?: string;
|
|
||||||
include_tools?: string[];
|
|
||||||
exclude_tools?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FrontmatterLocalAgentDefinition
|
|
||||||
extends FrontmatterBaseAgentDefinition {
|
|
||||||
kind: 'local';
|
|
||||||
description: string;
|
|
||||||
tools?: string[];
|
|
||||||
mcp_servers?: Record<string, FrontmatterMCPServerConfig>;
|
|
||||||
system_prompt: string;
|
|
||||||
model?: string;
|
|
||||||
temperature?: number;
|
|
||||||
max_turns?: number;
|
|
||||||
timeout_mins?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Authentication configuration for remote agents in frontmatter format.
|
|
||||||
*/
|
|
||||||
interface FrontmatterAuthConfig {
|
|
||||||
type: 'apiKey' | 'http' | 'google-credentials' | 'oauth';
|
|
||||||
// API Key
|
|
||||||
key?: string;
|
|
||||||
name?: string;
|
|
||||||
// HTTP
|
|
||||||
scheme?: string;
|
|
||||||
token?: string;
|
|
||||||
username?: string;
|
|
||||||
password?: string;
|
|
||||||
value?: string;
|
|
||||||
// Google Credentials
|
|
||||||
scopes?: string[];
|
|
||||||
// OAuth2
|
|
||||||
client_id?: string;
|
|
||||||
client_secret?: string;
|
|
||||||
authorization_url?: string;
|
|
||||||
token_url?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FrontmatterRemoteAgentDefinition
|
|
||||||
extends FrontmatterBaseAgentDefinition {
|
|
||||||
kind: 'remote';
|
|
||||||
description?: string;
|
|
||||||
agent_card_url: string;
|
|
||||||
auth?: FrontmatterAuthConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
type FrontmatterAgentDefinition =
|
|
||||||
| FrontmatterLocalAgentDefinition
|
|
||||||
| FrontmatterRemoteAgentDefinition;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error thrown when an agent definition is invalid or cannot be loaded.
|
* Error thrown when an agent definition is invalid or cannot be loaded.
|
||||||
*/
|
*/
|
||||||
@@ -159,15 +87,13 @@ const localAgentSchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
/**
|
type FrontmatterLocalAgentDefinition = z.infer<typeof localAgentSchema> & {
|
||||||
* Base fields shared by all auth configs.
|
system_prompt: string;
|
||||||
*/
|
};
|
||||||
|
|
||||||
|
// Base fields shared by all auth configs.
|
||||||
const baseAuthFields = {};
|
const baseAuthFields = {};
|
||||||
|
|
||||||
/**
|
|
||||||
* API Key auth schema.
|
|
||||||
* Supports sending key in header, query parameter, or cookie.
|
|
||||||
*/
|
|
||||||
const apiKeyAuthSchema = z.object({
|
const apiKeyAuthSchema = z.object({
|
||||||
...baseAuthFields,
|
...baseAuthFields,
|
||||||
type: z.literal('apiKey'),
|
type: z.literal('apiKey'),
|
||||||
@@ -175,11 +101,6 @@ const apiKeyAuthSchema = z.object({
|
|||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP auth schema (Bearer or Basic).
|
|
||||||
* Note: Validation for scheme-specific fields is applied in authConfigSchema
|
|
||||||
* since discriminatedUnion doesn't support refined schemas directly.
|
|
||||||
*/
|
|
||||||
const httpAuthSchema = z.object({
|
const httpAuthSchema = z.object({
|
||||||
...baseAuthFields,
|
...baseAuthFields,
|
||||||
type: z.literal('http'),
|
type: z.literal('http'),
|
||||||
@@ -190,19 +111,12 @@ const httpAuthSchema = z.object({
|
|||||||
value: z.string().min(1).optional(),
|
value: z.string().min(1).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Google Credentials auth schema.
|
|
||||||
*/
|
|
||||||
const googleCredentialsAuthSchema = z.object({
|
const googleCredentialsAuthSchema = z.object({
|
||||||
...baseAuthFields,
|
...baseAuthFields,
|
||||||
type: z.literal('google-credentials'),
|
type: z.literal('google-credentials'),
|
||||||
scopes: z.array(z.string()).optional(),
|
scopes: z.array(z.string()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* OAuth2 auth schema.
|
|
||||||
* authorization_url and token_url can be discovered from the agent card if omitted.
|
|
||||||
*/
|
|
||||||
const oauth2AuthSchema = z.object({
|
const oauth2AuthSchema = z.object({
|
||||||
...baseAuthFields,
|
...baseAuthFields,
|
||||||
type: z.literal('oauth'),
|
type: z.literal('oauth'),
|
||||||
@@ -222,18 +136,16 @@ const authConfigSchema = z
|
|||||||
])
|
])
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
if (data.type === 'http') {
|
if (data.type === 'http') {
|
||||||
if (data.value) {
|
if (data.value) return;
|
||||||
// Raw mode - only scheme and value are needed
|
if (data.scheme === 'Bearer') {
|
||||||
return;
|
if (!data.token) {
|
||||||
}
|
ctx.addIssue({
|
||||||
if (data.scheme === 'Bearer' && !data.token) {
|
code: z.ZodIssueCode.custom,
|
||||||
ctx.addIssue({
|
message: 'Bearer scheme requires "token"',
|
||||||
code: z.ZodIssueCode.custom,
|
path: ['token'],
|
||||||
message: 'Bearer scheme requires "token"',
|
});
|
||||||
path: ['token'],
|
}
|
||||||
});
|
} else if (data.scheme === 'Basic') {
|
||||||
}
|
|
||||||
if (data.scheme === 'Basic') {
|
|
||||||
if (!data.username) {
|
if (!data.username) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
@@ -248,55 +160,127 @@ const authConfigSchema = z
|
|||||||
path: ['password'],
|
path: ['password'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: `HTTP scheme "${data.scheme}" requires "value"`,
|
||||||
|
path: ['value'],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const remoteAgentSchema = z
|
type FrontmatterAuthConfig = z.infer<typeof authConfigSchema>;
|
||||||
.object({
|
|
||||||
kind: z.literal('remote').optional().default('remote'),
|
const baseRemoteAgentSchema = z.object({
|
||||||
name: nameSchema,
|
kind: z.literal('remote').optional().default('remote'),
|
||||||
description: z.string().optional(),
|
name: nameSchema,
|
||||||
display_name: z.string().optional(),
|
description: z.string().optional(),
|
||||||
|
display_name: z.string().optional(),
|
||||||
|
auth: authConfigSchema.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const remoteAgentUrlSchema = baseRemoteAgentSchema
|
||||||
|
.extend({
|
||||||
agent_card_url: z.string().url(),
|
agent_card_url: z.string().url(),
|
||||||
auth: authConfigSchema.optional(),
|
agent_card_json: z.undefined().optional(),
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
// Use a Zod union to automatically discriminate between local and remote
|
const remoteAgentJsonSchema = baseRemoteAgentSchema
|
||||||
// agent types.
|
.extend({
|
||||||
|
agent_card_url: z.undefined().optional(),
|
||||||
|
agent_card_json: z.string().refine(
|
||||||
|
(val) => {
|
||||||
|
try {
|
||||||
|
JSON.parse(val);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ message: 'agent_card_json must be valid JSON' },
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const remoteAgentSchema = z.union([
|
||||||
|
remoteAgentUrlSchema,
|
||||||
|
remoteAgentJsonSchema,
|
||||||
|
]);
|
||||||
|
type FrontmatterRemoteAgentDefinition = z.infer<typeof remoteAgentSchema>;
|
||||||
|
|
||||||
|
type FrontmatterAgentDefinition =
|
||||||
|
| FrontmatterLocalAgentDefinition
|
||||||
|
| FrontmatterRemoteAgentDefinition;
|
||||||
|
|
||||||
const agentUnionOptions = [
|
const agentUnionOptions = [
|
||||||
{ schema: localAgentSchema, label: 'Local Agent' },
|
{ label: 'Local Agent' },
|
||||||
{ schema: remoteAgentSchema, label: 'Remote Agent' },
|
{ label: 'Remote Agent' },
|
||||||
] as const;
|
{ label: 'Remote Agent' },
|
||||||
|
];
|
||||||
|
|
||||||
const remoteAgentsListSchema = z.array(remoteAgentSchema);
|
const remoteAgentsListSchema = z.array(remoteAgentSchema);
|
||||||
|
|
||||||
const markdownFrontmatterSchema = z.union([
|
const markdownFrontmatterSchema = z.union([
|
||||||
agentUnionOptions[0].schema,
|
localAgentSchema,
|
||||||
agentUnionOptions[1].schema,
|
remoteAgentUrlSchema,
|
||||||
|
remoteAgentJsonSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function formatZodError(error: z.ZodError, context: string): string {
|
function guessIntendedKind(rawInput: unknown): 'local' | 'remote' | undefined {
|
||||||
const issues = error.issues
|
if (typeof rawInput !== 'object' || rawInput === null) return undefined;
|
||||||
.map((i) => {
|
const input = rawInput as Partial<FrontmatterLocalAgentDefinition> &
|
||||||
// Handle union errors specifically to give better context
|
Partial<FrontmatterRemoteAgentDefinition>;
|
||||||
|
|
||||||
|
if (input.kind === 'local') return 'local';
|
||||||
|
if (input.kind === 'remote') return 'remote';
|
||||||
|
|
||||||
|
const hasLocalKeys =
|
||||||
|
'tools' in input ||
|
||||||
|
'mcp_servers' in input ||
|
||||||
|
'model' in input ||
|
||||||
|
'temperature' in input ||
|
||||||
|
'max_turns' in input ||
|
||||||
|
'timeout_mins' in input;
|
||||||
|
const hasRemoteKeys =
|
||||||
|
'agent_card_url' in input || 'auth' in input || 'agent_card_json' in input;
|
||||||
|
|
||||||
|
if (hasLocalKeys && !hasRemoteKeys) return 'local';
|
||||||
|
if (hasRemoteKeys && !hasLocalKeys) return 'remote';
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatZodError(
|
||||||
|
error: z.ZodError,
|
||||||
|
context: string,
|
||||||
|
rawInput?: unknown,
|
||||||
|
): string {
|
||||||
|
const intendedKind = rawInput ? guessIntendedKind(rawInput) : undefined;
|
||||||
|
|
||||||
|
const formatIssues = (issues: z.ZodIssue[], unionPrefix?: string): string[] =>
|
||||||
|
issues.flatMap((i) => {
|
||||||
if (i.code === z.ZodIssueCode.invalid_union) {
|
if (i.code === z.ZodIssueCode.invalid_union) {
|
||||||
return i.unionErrors
|
return i.unionErrors.flatMap((unionError, index) => {
|
||||||
.map((unionError, index) => {
|
const label = unionPrefix
|
||||||
const label =
|
? unionPrefix
|
||||||
agentUnionOptions[index]?.label ?? `Agent type #${index + 1}`;
|
: ((agentUnionOptions[index] as { label?: string })?.label ??
|
||||||
const unionIssues = unionError.issues
|
`Branch #${index + 1}`);
|
||||||
.map((u) => `${u.path.join('.')}: ${u.message}`)
|
|
||||||
.join(', ');
|
if (intendedKind === 'local' && label === 'Remote Agent') return [];
|
||||||
return `(${label}) ${unionIssues}`;
|
if (intendedKind === 'remote' && label === 'Local Agent') return [];
|
||||||
})
|
|
||||||
.join('\n');
|
return formatIssues(unionError.issues, label);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return `${i.path.join('.')}: ${i.message}`;
|
const prefix = unionPrefix ? `(${unionPrefix}) ` : '';
|
||||||
})
|
const path = i.path.length > 0 ? `${i.path.join('.')}: ` : '';
|
||||||
.join('\n');
|
return `${prefix}${path}${i.message}`;
|
||||||
return `${context}:\n${issues}`;
|
});
|
||||||
|
|
||||||
|
const formatted = Array.from(new Set(formatIssues(error.issues))).join('\n');
|
||||||
|
return `${context}:\n${formatted}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -343,8 +327,7 @@ export async function parseAgentMarkdown(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new AgentLoadError(
|
throw new AgentLoadError(
|
||||||
filePath,
|
filePath,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
`YAML frontmatter parsing failed: ${getErrorMessage(error)}`,
|
||||||
`YAML frontmatter parsing failed: ${(error as Error).message}`,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,7 +351,7 @@ export async function parseAgentMarkdown(
|
|||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new AgentLoadError(
|
throw new AgentLoadError(
|
||||||
filePath,
|
filePath,
|
||||||
`Validation failed: ${formatZodError(result.error, 'Agent Definition')}`,
|
`Validation failed: ${formatZodError(result.error, 'Agent Definition', rawFrontmatter)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,17 +366,14 @@ export async function parseAgentMarkdown(
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Local agent validation
|
|
||||||
// Validate tools
|
|
||||||
|
|
||||||
// Construct the local agent definition
|
// Construct the local agent definition
|
||||||
const agentDef: FrontmatterLocalAgentDefinition = {
|
return [
|
||||||
...frontmatter,
|
{
|
||||||
kind: 'local',
|
...frontmatter,
|
||||||
system_prompt: body.trim(),
|
kind: 'local',
|
||||||
};
|
system_prompt: body.trim(),
|
||||||
|
},
|
||||||
return [agentDef];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -403,15 +383,9 @@ export async function parseAgentMarkdown(
|
|||||||
function convertFrontmatterAuthToConfig(
|
function convertFrontmatterAuthToConfig(
|
||||||
frontmatter: FrontmatterAuthConfig,
|
frontmatter: FrontmatterAuthConfig,
|
||||||
): A2AAuthConfig {
|
): A2AAuthConfig {
|
||||||
const base = {};
|
|
||||||
|
|
||||||
switch (frontmatter.type) {
|
switch (frontmatter.type) {
|
||||||
case 'apiKey':
|
case 'apiKey':
|
||||||
if (!frontmatter.key) {
|
|
||||||
throw new Error('Internal error: API key missing after validation.');
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
...base,
|
|
||||||
type: 'apiKey',
|
type: 'apiKey',
|
||||||
key: frontmatter.key,
|
key: frontmatter.key,
|
||||||
name: frontmatter.name,
|
name: frontmatter.name,
|
||||||
@@ -419,20 +393,13 @@ function convertFrontmatterAuthToConfig(
|
|||||||
|
|
||||||
case 'google-credentials':
|
case 'google-credentials':
|
||||||
return {
|
return {
|
||||||
...base,
|
|
||||||
type: 'google-credentials',
|
type: 'google-credentials',
|
||||||
scopes: frontmatter.scopes,
|
scopes: frontmatter.scopes,
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'http': {
|
case 'http':
|
||||||
if (!frontmatter.scheme) {
|
|
||||||
throw new Error(
|
|
||||||
'Internal error: HTTP scheme missing after validation.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (frontmatter.value) {
|
if (frontmatter.value) {
|
||||||
return {
|
return {
|
||||||
...base,
|
|
||||||
type: 'http',
|
type: 'http',
|
||||||
scheme: frontmatter.scheme,
|
scheme: frontmatter.scheme,
|
||||||
value: frontmatter.value,
|
value: frontmatter.value,
|
||||||
@@ -440,40 +407,27 @@ function convertFrontmatterAuthToConfig(
|
|||||||
}
|
}
|
||||||
switch (frontmatter.scheme) {
|
switch (frontmatter.scheme) {
|
||||||
case 'Bearer':
|
case 'Bearer':
|
||||||
if (!frontmatter.token) {
|
// Token is required by schema validation
|
||||||
throw new Error(
|
|
||||||
'Internal error: Bearer token missing after validation.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
...base,
|
|
||||||
type: 'http',
|
type: 'http',
|
||||||
scheme: 'Bearer',
|
scheme: 'Bearer',
|
||||||
token: frontmatter.token,
|
|
||||||
|
token: frontmatter.token!,
|
||||||
};
|
};
|
||||||
case 'Basic':
|
case 'Basic':
|
||||||
if (!frontmatter.username || !frontmatter.password) {
|
// Username/password are required by schema validation
|
||||||
throw new Error(
|
|
||||||
'Internal error: Basic auth credentials missing after validation.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
...base,
|
|
||||||
type: 'http',
|
type: 'http',
|
||||||
scheme: 'Basic',
|
scheme: 'Basic',
|
||||||
username: frontmatter.username,
|
username: frontmatter.username!,
|
||||||
password: frontmatter.password,
|
password: frontmatter.password!,
|
||||||
};
|
};
|
||||||
default: {
|
default:
|
||||||
// Other IANA schemes without a value should not reach here after validation
|
|
||||||
throw new Error(`Unknown HTTP scheme: ${frontmatter.scheme}`);
|
throw new Error(`Unknown HTTP scheme: ${frontmatter.scheme}`);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
case 'oauth':
|
case 'oauth':
|
||||||
return {
|
return {
|
||||||
...base,
|
|
||||||
type: 'oauth2',
|
type: 'oauth2',
|
||||||
client_id: frontmatter.client_id,
|
client_id: frontmatter.client_id,
|
||||||
client_secret: frontmatter.client_secret,
|
client_secret: frontmatter.client_secret,
|
||||||
@@ -483,8 +437,12 @@ function convertFrontmatterAuthToConfig(
|
|||||||
};
|
};
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
const exhaustive: never = frontmatter.type;
|
const exhaustive: never = frontmatter;
|
||||||
throw new Error(`Unknown auth type: ${exhaustive}`);
|
const raw: unknown = exhaustive;
|
||||||
|
if (typeof raw === 'object' && raw !== null && 'type' in raw) {
|
||||||
|
throw new Error(`Unknown auth type: ${String(raw['type'])}`);
|
||||||
|
}
|
||||||
|
throw new Error('Unknown auth type');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -515,25 +473,41 @@ export function markdownToAgentDefinition(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (markdown.kind === 'remote') {
|
if (markdown.kind === 'remote') {
|
||||||
return {
|
const base: RemoteAgentDefinition = {
|
||||||
kind: 'remote',
|
kind: 'remote',
|
||||||
name: markdown.name,
|
name: markdown.name,
|
||||||
description: markdown.description || '',
|
description: markdown.description || '',
|
||||||
displayName: markdown.display_name,
|
displayName: markdown.display_name,
|
||||||
agentCardUrl: markdown.agent_card_url,
|
|
||||||
auth: markdown.auth
|
auth: markdown.auth
|
||||||
? convertFrontmatterAuthToConfig(markdown.auth)
|
? convertFrontmatterAuthToConfig(markdown.auth)
|
||||||
: undefined,
|
: undefined,
|
||||||
inputConfig,
|
inputConfig,
|
||||||
metadata,
|
metadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
'agent_card_json' in markdown &&
|
||||||
|
markdown.agent_card_json !== undefined
|
||||||
|
) {
|
||||||
|
base.agentCardJson = markdown.agent_card_json;
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
if ('agent_card_url' in markdown && markdown.agent_card_url !== undefined) {
|
||||||
|
base.agentCardUrl = markdown.agent_card_url;
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AgentLoadError(
|
||||||
|
metadata?.filePath || 'unknown',
|
||||||
|
'Unexpected state: neither agent_card_json nor agent_card_url present on remote agent',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a model is specified, use it. Otherwise, inherit
|
// If a model is specified, use it. Otherwise, inherit
|
||||||
const modelName = markdown.model || 'inherit';
|
const modelName = markdown.model || 'inherit';
|
||||||
|
|
||||||
const mcpServers: Record<string, MCPServerConfig> = {};
|
const mcpServers: Record<string, MCPServerConfig> = {};
|
||||||
if (markdown.kind === 'local' && markdown.mcp_servers) {
|
if (markdown.mcp_servers) {
|
||||||
for (const [name, config] of Object.entries(markdown.mcp_servers)) {
|
for (const [name, config] of Object.entries(markdown.mcp_servers)) {
|
||||||
mcpServers[name] = new MCPServerConfig(
|
mcpServers[name] = new MCPServerConfig(
|
||||||
config.command,
|
config.command,
|
||||||
@@ -606,15 +580,13 @@ export async function loadAgentsFromDirectory(
|
|||||||
dirEntries = await fs.readdir(dir, { withFileTypes: true });
|
dirEntries = await fs.readdir(dir, { withFileTypes: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If directory doesn't exist, just return empty
|
// If directory doesn't exist, just return empty
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
||||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
result.errors.push(
|
result.errors.push(
|
||||||
new AgentLoadError(
|
new AgentLoadError(
|
||||||
dir,
|
dir,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
`Could not list directory: ${getErrorMessage(error)}`,
|
||||||
`Could not list directory: ${(error as Error).message}`,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
@@ -644,8 +616,7 @@ export async function loadAgentsFromDirectory(
|
|||||||
result.errors.push(
|
result.errors.push(
|
||||||
new AgentLoadError(
|
new AgentLoadError(
|
||||||
filePath,
|
filePath,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
`Unexpected error: ${getErrorMessage(error)}`,
|
||||||
`Unexpected error: ${(error as Error).message}`,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -596,7 +596,7 @@ describe('AgentRegistry', () => {
|
|||||||
});
|
});
|
||||||
expect(loadAgentSpy).toHaveBeenCalledWith(
|
expect(loadAgentSpy).toHaveBeenCalledWith(
|
||||||
'RemoteAgentWithAuth',
|
'RemoteAgentWithAuth',
|
||||||
'https://example.com/card',
|
{ type: 'url', url: 'https://example.com/card' },
|
||||||
mockHandler,
|
mockHandler,
|
||||||
);
|
);
|
||||||
expect(registry.getDefinition('RemoteAgentWithAuth')).toEqual(
|
expect(registry.getDefinition('RemoteAgentWithAuth')).toEqual(
|
||||||
|
|||||||
@@ -4,10 +4,12 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as crypto from 'node:crypto';
|
||||||
import { Storage } from '../config/storage.js';
|
import { Storage } from '../config/storage.js';
|
||||||
import { CoreEvent, coreEvents } from '../utils/events.js';
|
import { CoreEvent, coreEvents } from '../utils/events.js';
|
||||||
import type { AgentOverride, Config } from '../config/config.js';
|
import type { AgentOverride, Config } from '../config/config.js';
|
||||||
import type { AgentDefinition, LocalAgentDefinition } from './types.js';
|
import type { AgentDefinition, LocalAgentDefinition } from './types.js';
|
||||||
|
import { getAgentCardLoadOptions, getRemoteAgentTargetUrl } from './types.js';
|
||||||
import { loadAgentsFromDirectory } from './agentLoader.js';
|
import { loadAgentsFromDirectory } from './agentLoader.js';
|
||||||
import { CodebaseInvestigatorAgent } from './codebase-investigator.js';
|
import { CodebaseInvestigatorAgent } from './codebase-investigator.js';
|
||||||
import { CliHelpAgent } from './cli-help-agent.js';
|
import { CliHelpAgent } from './cli-help-agent.js';
|
||||||
@@ -162,7 +164,14 @@ export class AgentRegistry {
|
|||||||
if (!agent.metadata) {
|
if (!agent.metadata) {
|
||||||
agent.metadata = {};
|
agent.metadata = {};
|
||||||
}
|
}
|
||||||
agent.metadata.hash = agent.agentCardUrl;
|
agent.metadata.hash =
|
||||||
|
agent.agentCardUrl ??
|
||||||
|
(agent.agentCardJson
|
||||||
|
? crypto
|
||||||
|
.createHash('sha256')
|
||||||
|
.update(agent.agentCardJson)
|
||||||
|
.digest('hex')
|
||||||
|
: undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!agent.metadata?.hash) {
|
if (!agent.metadata?.hash) {
|
||||||
@@ -443,12 +452,13 @@ export class AgentRegistry {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const targetUrl = getRemoteAgentTargetUrl(remoteDef);
|
||||||
let authHandler: AuthenticationHandler | undefined;
|
let authHandler: AuthenticationHandler | undefined;
|
||||||
if (definition.auth) {
|
if (definition.auth) {
|
||||||
const provider = await A2AAuthProviderFactory.create({
|
const provider = await A2AAuthProviderFactory.create({
|
||||||
authConfig: definition.auth,
|
authConfig: definition.auth,
|
||||||
agentName: definition.name,
|
agentName: definition.name,
|
||||||
targetUrl: definition.agentCardUrl,
|
targetUrl,
|
||||||
agentCardUrl: remoteDef.agentCardUrl,
|
agentCardUrl: remoteDef.agentCardUrl,
|
||||||
});
|
});
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
@@ -461,7 +471,7 @@ export class AgentRegistry {
|
|||||||
|
|
||||||
const agentCard = await clientManager.loadAgent(
|
const agentCard = await clientManager.loadAgent(
|
||||||
remoteDef.name,
|
remoteDef.name,
|
||||||
remoteDef.agentCardUrl,
|
getAgentCardLoadOptions(remoteDef),
|
||||||
authHandler,
|
authHandler,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -515,7 +525,7 @@ export class AgentRegistry {
|
|||||||
|
|
||||||
if (this.config.getDebugMode()) {
|
if (this.config.getDebugMode()) {
|
||||||
debugLogger.log(
|
debugLogger.log(
|
||||||
`[AgentRegistry] Registered remote agent '${definition.name}' with card: ${definition.agentCardUrl}`,
|
`[AgentRegistry] Registered remote agent '${definition.name}' with card: ${definition.agentCardUrl ?? 'inline JSON'}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.agents.set(definition.name, definition);
|
this.agents.set(definition.name, definition);
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ describe('RemoteAgentInvocation', () => {
|
|||||||
|
|
||||||
expect(mockClientManager.loadAgent).toHaveBeenCalledWith(
|
expect(mockClientManager.loadAgent).toHaveBeenCalledWith(
|
||||||
'test-agent',
|
'test-agent',
|
||||||
'http://test-agent/card',
|
{ type: 'url', url: 'http://test-agent/card' },
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -240,7 +240,7 @@ describe('RemoteAgentInvocation', () => {
|
|||||||
});
|
});
|
||||||
expect(mockClientManager.loadAgent).toHaveBeenCalledWith(
|
expect(mockClientManager.loadAgent).toHaveBeenCalledWith(
|
||||||
'test-agent',
|
'test-agent',
|
||||||
'http://test-agent/card',
|
{ type: 'url', url: 'http://test-agent/card' },
|
||||||
mockHandler,
|
mockHandler,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -266,11 +266,10 @@ describe('RemoteAgentInvocation', () => {
|
|||||||
);
|
);
|
||||||
const result = await invocation.execute(new AbortController().signal);
|
const result = await invocation.execute(new AbortController().signal);
|
||||||
|
|
||||||
expect(result.returnDisplay).toMatchObject({
|
expect(result.returnDisplay).toMatchObject({ state: 'error' });
|
||||||
result: expect.stringContaining(
|
expect((result.returnDisplay as SubagentProgress).result).toContain(
|
||||||
"Failed to create auth provider for agent 'test-agent'",
|
"Failed to create auth provider for agent 'test-agent'",
|
||||||
),
|
);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not load the agent if already present', async () => {
|
it('should not load the agent if already present', async () => {
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import {
|
|||||||
type RemoteAgentDefinition,
|
type RemoteAgentDefinition,
|
||||||
type AgentInputs,
|
type AgentInputs,
|
||||||
type SubagentProgress,
|
type SubagentProgress,
|
||||||
|
getAgentCardLoadOptions,
|
||||||
|
getRemoteAgentTargetUrl,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { type AgentLoopContext } from '../config/agent-loop-context.js';
|
import { type AgentLoopContext } from '../config/agent-loop-context.js';
|
||||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||||
@@ -92,10 +94,11 @@ export class RemoteAgentInvocation extends BaseToolInvocation<
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.definition.auth) {
|
if (this.definition.auth) {
|
||||||
|
const targetUrl = getRemoteAgentTargetUrl(this.definition);
|
||||||
const provider = await A2AAuthProviderFactory.create({
|
const provider = await A2AAuthProviderFactory.create({
|
||||||
authConfig: this.definition.auth,
|
authConfig: this.definition.auth,
|
||||||
agentName: this.definition.name,
|
agentName: this.definition.name,
|
||||||
targetUrl: this.definition.agentCardUrl,
|
targetUrl,
|
||||||
agentCardUrl: this.definition.agentCardUrl,
|
agentCardUrl: this.definition.agentCardUrl,
|
||||||
});
|
});
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
@@ -162,7 +165,7 @@ export class RemoteAgentInvocation extends BaseToolInvocation<
|
|||||||
if (!this.clientManager.getClient(this.definition.name)) {
|
if (!this.clientManager.getClient(this.definition.name)) {
|
||||||
await this.clientManager.loadAgent(
|
await this.clientManager.loadAgent(
|
||||||
this.definition.name,
|
this.definition.name,
|
||||||
this.definition.agentCardUrl,
|
getAgentCardLoadOptions(this.definition),
|
||||||
authHandler,
|
authHandler,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import type { AnyDeclarativeTool } from '../tools/tools.js';
|
|||||||
import { type z } from 'zod';
|
import { type z } from 'zod';
|
||||||
import type { ModelConfig } from '../services/modelConfigService.js';
|
import type { ModelConfig } from '../services/modelConfigService.js';
|
||||||
import type { AnySchema } from 'ajv';
|
import type { AnySchema } from 'ajv';
|
||||||
|
import type { AgentCard } from '@a2a-js/sdk';
|
||||||
import type { A2AAuthConfig } from './auth-provider/types.js';
|
import type { A2AAuthConfig } from './auth-provider/types.js';
|
||||||
import type { MCPServerConfig } from '../config/config.js';
|
import type { MCPServerConfig } from '../config/config.js';
|
||||||
|
|
||||||
@@ -128,6 +129,62 @@ export function isToolActivityError(data: unknown): boolean {
|
|||||||
* The base definition for an agent.
|
* The base definition for an agent.
|
||||||
* @template TOutput The specific Zod schema for the agent's final output object.
|
* @template TOutput The specific Zod schema for the agent's final output object.
|
||||||
*/
|
*/
|
||||||
|
export type AgentCardLoadOptions =
|
||||||
|
| { type: 'url'; url: string }
|
||||||
|
| { type: 'json'; json: string };
|
||||||
|
|
||||||
|
/** Minimal shape needed by helper functions, avoids generic TOutput constraints. */
|
||||||
|
interface RemoteAgentRef {
|
||||||
|
name: string;
|
||||||
|
agentCardUrl?: string;
|
||||||
|
agentCardJson?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives the AgentCardLoadOptions from a RemoteAgentDefinition.
|
||||||
|
* Throws if neither agentCardUrl nor agentCardJson is present.
|
||||||
|
*/
|
||||||
|
export function getAgentCardLoadOptions(
|
||||||
|
def: RemoteAgentRef,
|
||||||
|
): AgentCardLoadOptions {
|
||||||
|
if (def.agentCardJson) {
|
||||||
|
return { type: 'json', json: def.agentCardJson };
|
||||||
|
}
|
||||||
|
if (def.agentCardUrl) {
|
||||||
|
return { type: 'url', url: def.agentCardUrl };
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Remote agent '${def.name}' has neither agentCardUrl nor agentCardJson`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts a target URL for auth providers from a RemoteAgentDefinition.
|
||||||
|
* For URL-based agents, returns the agentCardUrl.
|
||||||
|
* For JSON-based agents, attempts to parse the URL from the inline card JSON.
|
||||||
|
* Returns undefined if no URL can be determined.
|
||||||
|
*/
|
||||||
|
export function getRemoteAgentTargetUrl(
|
||||||
|
def: RemoteAgentRef,
|
||||||
|
): string | undefined {
|
||||||
|
if (def.agentCardUrl) {
|
||||||
|
return def.agentCardUrl;
|
||||||
|
}
|
||||||
|
if (def.agentCardJson) {
|
||||||
|
try {
|
||||||
|
const parsed: unknown = JSON.parse(def.agentCardJson);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
|
const card = parsed as AgentCard;
|
||||||
|
if (card.url) {
|
||||||
|
return card.url;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// JSON parse will fail properly later in loadAgent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BaseAgentDefinition<
|
export interface BaseAgentDefinition<
|
||||||
TOutput extends z.ZodTypeAny = z.ZodUnknown,
|
TOutput extends z.ZodTypeAny = z.ZodUnknown,
|
||||||
> {
|
> {
|
||||||
@@ -172,11 +229,10 @@ export interface LocalAgentDefinition<
|
|||||||
processOutput?: (output: z.infer<TOutput>) => string;
|
processOutput?: (output: z.infer<TOutput>) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RemoteAgentDefinition<
|
export interface BaseRemoteAgentDefinition<
|
||||||
TOutput extends z.ZodTypeAny = z.ZodUnknown,
|
TOutput extends z.ZodTypeAny = z.ZodUnknown,
|
||||||
> extends BaseAgentDefinition<TOutput> {
|
> extends BaseAgentDefinition<TOutput> {
|
||||||
kind: 'remote';
|
kind: 'remote';
|
||||||
agentCardUrl: string;
|
|
||||||
/** The user-provided description, before any remote card merging. */
|
/** The user-provided description, before any remote card merging. */
|
||||||
originalDescription?: string;
|
originalDescription?: string;
|
||||||
/**
|
/**
|
||||||
@@ -187,6 +243,13 @@ export interface RemoteAgentDefinition<
|
|||||||
auth?: A2AAuthConfig;
|
auth?: A2AAuthConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RemoteAgentDefinition<
|
||||||
|
TOutput extends z.ZodTypeAny = z.ZodUnknown,
|
||||||
|
> extends BaseRemoteAgentDefinition<TOutput> {
|
||||||
|
agentCardUrl?: string;
|
||||||
|
agentCardJson?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type AgentDefinition<TOutput extends z.ZodTypeAny = z.ZodUnknown> =
|
export type AgentDefinition<TOutput extends z.ZodTypeAny = z.ZodUnknown> =
|
||||||
| LocalAgentDefinition<TOutput>
|
| LocalAgentDefinition<TOutput>
|
||||||
| RemoteAgentDefinition<TOutput>;
|
| RemoteAgentDefinition<TOutput>;
|
||||||
|
|||||||
@@ -1027,7 +1027,7 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
this.model = params.model;
|
this.model = params.model;
|
||||||
this.disableLoopDetection = params.disableLoopDetection ?? false;
|
this.disableLoopDetection = params.disableLoopDetection ?? false;
|
||||||
this._activeModel = params.model;
|
this._activeModel = params.model;
|
||||||
this.enableAgents = params.enableAgents ?? false;
|
this.enableAgents = params.enableAgents ?? true;
|
||||||
this.agents = params.agents ?? {};
|
this.agents = params.agents ?? {};
|
||||||
this.disableLLMCorrection = params.disableLLMCorrection ?? true;
|
this.disableLLMCorrection = params.disableLLMCorrection ?? true;
|
||||||
this.planEnabled = params.plan ?? true;
|
this.planEnabled = params.plan ?? true;
|
||||||
|
|||||||
@@ -88,7 +88,11 @@ export * from './utils/approvalModeUtils.js';
|
|||||||
export * from './utils/fileDiffUtils.js';
|
export * from './utils/fileDiffUtils.js';
|
||||||
export * from './utils/retry.js';
|
export * from './utils/retry.js';
|
||||||
export * from './utils/shell-utils.js';
|
export * from './utils/shell-utils.js';
|
||||||
export { PolicyDecision, ApprovalMode } from './policy/types.js';
|
export {
|
||||||
|
PolicyDecision,
|
||||||
|
ApprovalMode,
|
||||||
|
PRIORITY_YOLO_ALLOW_ALL,
|
||||||
|
} from './policy/types.js';
|
||||||
export * from './utils/tool-utils.js';
|
export * from './utils/tool-utils.js';
|
||||||
export * from './utils/terminalSerializer.js';
|
export * from './utils/terminalSerializer.js';
|
||||||
export * from './utils/systemEncoding.js';
|
export * from './utils/systemEncoding.js';
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ priority = 70
|
|||||||
modes = ["plan"]
|
modes = ["plan"]
|
||||||
|
|
||||||
# Allow write_file and replace for .md files in the plans directory (cross-platform)
|
# Allow write_file and replace for .md files in the plans directory (cross-platform)
|
||||||
|
# We split this into two rules to avoid ReDoS checker issues with nested optional segments.
|
||||||
|
# This rule handles the case where there is a session ID in the plan file path
|
||||||
[[rule]]
|
[[rule]]
|
||||||
toolName = ["write_file", "replace"]
|
toolName = ["write_file", "replace"]
|
||||||
decision = "allow"
|
decision = "allow"
|
||||||
@@ -117,6 +119,14 @@ priority = 70
|
|||||||
modes = ["plan"]
|
modes = ["plan"]
|
||||||
argsPattern = "\\x00\"file_path\":\"[^\"]+[\\\\/]+\\.gemini[\\\\/]+tmp[\\\\/]+[\\w-]+[\\\\/]+[\\w-]+[\\\\/]+plans[\\\\/]+[\\w-]+\\.md\"\\x00"
|
argsPattern = "\\x00\"file_path\":\"[^\"]+[\\\\/]+\\.gemini[\\\\/]+tmp[\\\\/]+[\\w-]+[\\\\/]+[\\w-]+[\\\\/]+plans[\\\\/]+[\\w-]+\\.md\"\\x00"
|
||||||
|
|
||||||
|
# This rule handles the case where there isn't a session ID in the plan file path
|
||||||
|
[[rule]]
|
||||||
|
toolName = ["write_file", "replace"]
|
||||||
|
decision = "allow"
|
||||||
|
priority = 70
|
||||||
|
modes = ["plan"]
|
||||||
|
argsPattern = "\\x00\"file_path\":\"[^\"]+[\\\\/]+\\.gemini[\\\\/]+tmp[\\\\/]+[\\w-]+[\\\\/]+plans[\\\\/]+[\\w-]+\\.md\"\\x00"
|
||||||
|
|
||||||
# Explicitly Deny other write operations in Plan mode with a clear message.
|
# Explicitly Deny other write operations in Plan mode with a clear message.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
toolName = ["write_file", "replace"]
|
toolName = ["write_file", "replace"]
|
||||||
|
|||||||
@@ -702,15 +702,6 @@ export class PolicyEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sandbox Expansion requests MUST always be confirmed by the user,
|
|
||||||
// even if the base command is otherwise ALLOWED by the policy engine.
|
|
||||||
if (
|
|
||||||
decision === PolicyDecision.ALLOW &&
|
|
||||||
toolCall.args?.['additional_permissions']
|
|
||||||
) {
|
|
||||||
decision = PolicyDecision.ASK_USER;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
decision: this.applyNonInteractiveMode(decision),
|
decision: this.applyNonInteractiveMode(decision),
|
||||||
rule: matchedRule,
|
rule: matchedRule,
|
||||||
|
|||||||
@@ -233,13 +233,19 @@ export {
|
|||||||
export function getShellDefinition(
|
export function getShellDefinition(
|
||||||
enableInteractiveShell: boolean,
|
enableInteractiveShell: boolean,
|
||||||
enableEfficiency: boolean,
|
enableEfficiency: boolean,
|
||||||
|
enableToolSandboxing: boolean = false,
|
||||||
): ToolDefinition {
|
): ToolDefinition {
|
||||||
return {
|
return {
|
||||||
base: getShellDeclaration(enableInteractiveShell, enableEfficiency),
|
base: getShellDeclaration(
|
||||||
|
enableInteractiveShell,
|
||||||
|
enableEfficiency,
|
||||||
|
enableToolSandboxing,
|
||||||
|
),
|
||||||
overrides: (modelId) =>
|
overrides: (modelId) =>
|
||||||
getToolSet(modelId).run_shell_command(
|
getToolSet(modelId).run_shell_command(
|
||||||
enableInteractiveShell,
|
enableInteractiveShell,
|
||||||
enableEfficiency,
|
enableEfficiency,
|
||||||
|
enableToolSandboxing,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ describe('coreTools snapshots for specific models', () => {
|
|||||||
{ name: 'list_directory', definition: LS_DEFINITION },
|
{ name: 'list_directory', definition: LS_DEFINITION },
|
||||||
{
|
{
|
||||||
name: 'run_shell_command',
|
name: 'run_shell_command',
|
||||||
definition: getShellDefinition(true, true),
|
definition: getShellDefinition(true, true, true),
|
||||||
},
|
},
|
||||||
{ name: 'replace', definition: EDIT_DEFINITION },
|
{ name: 'replace', definition: EDIT_DEFINITION },
|
||||||
{ name: 'google_web_search', definition: WEB_SEARCH_DEFINITION },
|
{ name: 'google_web_search', definition: WEB_SEARCH_DEFINITION },
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export function getCommandDescription(): string {
|
|||||||
export function getShellDeclaration(
|
export function getShellDeclaration(
|
||||||
enableInteractiveShell: boolean,
|
enableInteractiveShell: boolean,
|
||||||
enableEfficiency: boolean,
|
enableEfficiency: boolean,
|
||||||
|
enableToolSandboxing: boolean = false,
|
||||||
): FunctionDeclaration {
|
): FunctionDeclaration {
|
||||||
return {
|
return {
|
||||||
name: SHELL_TOOL_NAME,
|
name: SHELL_TOOL_NAME,
|
||||||
@@ -110,35 +111,39 @@ export function getShellDeclaration(
|
|||||||
description:
|
description:
|
||||||
'Set to true if this command should be run in the background (e.g. for long-running servers or watchers). The command will be started, allowed to run for a brief moment to check for immediate errors, and then moved to the background.',
|
'Set to true if this command should be run in the background (e.g. for long-running servers or watchers). The command will be started, allowed to run for a brief moment to check for immediate errors, and then moved to the background.',
|
||||||
},
|
},
|
||||||
[PARAM_ADDITIONAL_PERMISSIONS]: {
|
...(enableToolSandboxing
|
||||||
type: 'object',
|
? {
|
||||||
description:
|
[PARAM_ADDITIONAL_PERMISSIONS]: {
|
||||||
'Sandbox permissions for the command. Use this to request additional sandboxed filesystem or network permissions if a previous command failed with "Operation not permitted".',
|
type: 'object',
|
||||||
properties: {
|
description:
|
||||||
network: {
|
'Sandbox permissions for the command. Use this to request additional sandboxed filesystem or network permissions if a previous command failed with "Operation not permitted".',
|
||||||
type: 'boolean',
|
properties: {
|
||||||
description:
|
network: {
|
||||||
'Set to true to enable network access for this command.',
|
type: 'boolean',
|
||||||
},
|
description:
|
||||||
fileSystem: {
|
'Set to true to enable network access for this command.',
|
||||||
type: 'object',
|
},
|
||||||
properties: {
|
fileSystem: {
|
||||||
read: {
|
type: 'object',
|
||||||
type: 'array',
|
properties: {
|
||||||
items: { type: 'string' },
|
read: {
|
||||||
description:
|
type: 'array',
|
||||||
'List of additional absolute paths to allow reading.',
|
items: { type: 'string' },
|
||||||
},
|
description:
|
||||||
write: {
|
'List of additional absolute paths to allow reading.',
|
||||||
type: 'array',
|
},
|
||||||
items: { type: 'string' },
|
write: {
|
||||||
description:
|
type: 'array',
|
||||||
'List of additional absolute paths to allow writing.',
|
items: { type: 'string' },
|
||||||
|
description:
|
||||||
|
'List of additional absolute paths to allow writing.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
},
|
: {}),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
required: [SHELL_PARAM_COMMAND],
|
required: [SHELL_PARAM_COMMAND],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -332,8 +332,16 @@ export const DEFAULT_LEGACY_SET: CoreToolSet = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
run_shell_command: (enableInteractiveShell, enableEfficiency) =>
|
run_shell_command: (
|
||||||
getShellDeclaration(enableInteractiveShell, enableEfficiency),
|
enableInteractiveShell,
|
||||||
|
enableEfficiency,
|
||||||
|
enableToolSandboxing,
|
||||||
|
) =>
|
||||||
|
getShellDeclaration(
|
||||||
|
enableInteractiveShell,
|
||||||
|
enableEfficiency,
|
||||||
|
enableToolSandboxing,
|
||||||
|
),
|
||||||
|
|
||||||
replace: {
|
replace: {
|
||||||
name: EDIT_TOOL_NAME,
|
name: EDIT_TOOL_NAME,
|
||||||
|
|||||||
@@ -338,8 +338,16 @@ export const GEMINI_3_SET: CoreToolSet = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
run_shell_command: (enableInteractiveShell, enableEfficiency) =>
|
run_shell_command: (
|
||||||
getShellDeclaration(enableInteractiveShell, enableEfficiency),
|
enableInteractiveShell,
|
||||||
|
enableEfficiency,
|
||||||
|
enableToolSandboxing,
|
||||||
|
) =>
|
||||||
|
getShellDeclaration(
|
||||||
|
enableInteractiveShell,
|
||||||
|
enableEfficiency,
|
||||||
|
enableToolSandboxing,
|
||||||
|
),
|
||||||
|
|
||||||
replace: {
|
replace: {
|
||||||
name: EDIT_TOOL_NAME,
|
name: EDIT_TOOL_NAME,
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export interface CoreToolSet {
|
|||||||
run_shell_command: (
|
run_shell_command: (
|
||||||
enableInteractiveShell: boolean,
|
enableInteractiveShell: boolean,
|
||||||
enableEfficiency: boolean,
|
enableEfficiency: boolean,
|
||||||
|
enableToolSandboxing: boolean,
|
||||||
) => FunctionDeclaration;
|
) => FunctionDeclaration;
|
||||||
replace: FunctionDeclaration;
|
replace: FunctionDeclaration;
|
||||||
google_web_search: FunctionDeclaration;
|
google_web_search: FunctionDeclaration;
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ describe('ShellTool', () => {
|
|||||||
getShellToolInactivityTimeout: vi.fn().mockReturnValue(1000),
|
getShellToolInactivityTimeout: vi.fn().mockReturnValue(1000),
|
||||||
getEnableInteractiveShell: vi.fn().mockReturnValue(false),
|
getEnableInteractiveShell: vi.fn().mockReturnValue(false),
|
||||||
getEnableShellOutputEfficiency: vi.fn().mockReturnValue(true),
|
getEnableShellOutputEfficiency: vi.fn().mockReturnValue(true),
|
||||||
|
getSandboxEnabled: vi.fn().mockReturnValue(false),
|
||||||
sanitizationConfig: {},
|
sanitizationConfig: {},
|
||||||
sandboxManager: new NoopSandboxManager(),
|
sandboxManager: new NoopSandboxManager(),
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|||||||
@@ -696,6 +696,7 @@ export class ShellTool extends BaseDeclarativeTool<
|
|||||||
const definition = getShellDefinition(
|
const definition = getShellDefinition(
|
||||||
context.config.getEnableInteractiveShell(),
|
context.config.getEnableInteractiveShell(),
|
||||||
context.config.getEnableShellOutputEfficiency(),
|
context.config.getEnableShellOutputEfficiency(),
|
||||||
|
context.config.getSandboxEnabled(),
|
||||||
);
|
);
|
||||||
super(
|
super(
|
||||||
ShellTool.Name,
|
ShellTool.Name,
|
||||||
@@ -745,6 +746,7 @@ export class ShellTool extends BaseDeclarativeTool<
|
|||||||
const definition = getShellDefinition(
|
const definition = getShellDefinition(
|
||||||
this.context.config.getEnableInteractiveShell(),
|
this.context.config.getEnableInteractiveShell(),
|
||||||
this.context.config.getEnableShellOutputEfficiency(),
|
this.context.config.getEnableShellOutputEfficiency(),
|
||||||
|
this.context.config.getSandboxEnabled(),
|
||||||
);
|
);
|
||||||
return resolveToolDeclaration(definition, modelId);
|
return resolveToolDeclaration(definition, modelId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli-devtools",
|
"name": "@google/gemini-cli-devtools",
|
||||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
"version": "0.36.0-preview.3",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/src/index.js",
|
"main": "dist/src/index.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli-sdk",
|
"name": "@google/gemini-cli-sdk",
|
||||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
"version": "0.36.0-preview.3",
|
||||||
"description": "Gemini CLI SDK",
|
"description": "Gemini CLI SDK",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@google/gemini-cli-test-utils",
|
"name": "@google/gemini-cli-test-utils",
|
||||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
"version": "0.36.0-preview.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "gemini-cli-vscode-ide-companion",
|
"name": "gemini-cli-vscode-ide-companion",
|
||||||
"displayName": "Gemini CLI Companion",
|
"displayName": "Gemini CLI Companion",
|
||||||
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
|
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
|
||||||
"version": "0.36.0-nightly.20260317.2f90b4653",
|
"version": "0.36.0-preview.3",
|
||||||
"publisher": "google",
|
"publisher": "google",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -2680,8 +2680,8 @@
|
|||||||
"enableAgents": {
|
"enableAgents": {
|
||||||
"title": "Enable Agents",
|
"title": "Enable Agents",
|
||||||
"description": "Enable local and remote subagents.",
|
"description": "Enable local and remote subagents.",
|
||||||
"markdownDescription": "Enable local and remote subagents.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `false`",
|
"markdownDescription": "Enable local and remote subagents.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `true`",
|
||||||
"default": false,
|
"default": true,
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"worktrees": {
|
"worktrees": {
|
||||||
|
|||||||
Reference in New Issue
Block a user