feat(littleblue): blue tool registry (list/propose action via local API) + run_turn extraEnv
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ const STDIO_PATH = fileURLToPath(new URL('../../mcp/companion-stdio.js', import.
|
||||
*/
|
||||
export async function runAgentTurn({
|
||||
agent, persona, registryName, toolNames, spaceId = null, view = null,
|
||||
sessionId, resume = false, userText, claudeExe = 'claude', home, onEvent
|
||||
sessionId, resume = false, userText, claudeExe = 'claude', home, onEvent, extraEnv = {}
|
||||
}) {
|
||||
const agentActor = { kind: 'agent', id: agent.id, capabilities: agent.capabilities, scopes: agent.scopes };
|
||||
const mcpConfigPath = join(tmpdir(), `void-mcp-${randomUUID()}.json`);
|
||||
@@ -30,7 +30,8 @@ export async function runAgentTurn({
|
||||
VOID_AGENT_JSON: JSON.stringify(agentActor),
|
||||
VOID_VIEW_JSON: view ? JSON.stringify(view) : '',
|
||||
DATABASE_URL: process.env.DATABASE_URL || '',
|
||||
OLLAMA_URL: process.env.OLLAMA_URL || ''
|
||||
OLLAMA_URL: process.env.OLLAMA_URL || '',
|
||||
...extraEnv
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
29
lib/ai/agent/tools/blue/actions.js
Normal file
29
lib/ai/agent/tools/blue/actions.js
Normal file
@@ -0,0 +1,29 @@
|
||||
// Little Blue's action tools. They run inside the MCP child, which holds NO infra
|
||||
// creds — only a scoped little-blue bearer + the local API URL. The main server
|
||||
// (which has the Proxmox/SSH creds) does the actual work behind /api/actions.
|
||||
function api(env = process.env) { return { base: env.VOID_API_URL, token: env.VOID_AGENT_TOKEN }; }
|
||||
|
||||
export const listActionsTool = {
|
||||
name: 'list_actions',
|
||||
description: 'List the whitelisted fix-it actions you may take (id, label, tier).',
|
||||
input_schema: { type: 'object', properties: {} },
|
||||
async handler(_args, _ctx, { fetchImpl = fetch } = {}) {
|
||||
const { base, token } = api();
|
||||
const res = await fetchImpl(`${base}/api/actions`, { headers: { Authorization: `Bearer ${token}` } });
|
||||
if (!res.ok) return { error: `list_actions ${res.status}` };
|
||||
return res.json();
|
||||
}
|
||||
};
|
||||
|
||||
export const proposeActionTool = {
|
||||
name: 'propose_action',
|
||||
description: 'Take a whitelisted action by id. SAFE actions run immediately; RISKY ones queue for the owner to approve. You can only name an id from list_actions — never a command.',
|
||||
input_schema: { type: 'object', properties: { action_id: { type: 'string' } }, required: ['action_id'] },
|
||||
async handler({ action_id }, _ctx, { fetchImpl = fetch } = {}) {
|
||||
const { base, token } = api();
|
||||
const res = await fetchImpl(`${base}/api/actions/${encodeURIComponent(action_id)}/run`,
|
||||
{ method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' } });
|
||||
if (!res.ok) return { error: `propose_action ${res.status}` };
|
||||
return res.json();
|
||||
}
|
||||
};
|
||||
9
lib/ai/agent/tools/blue/index.js
Normal file
9
lib/ai/agent/tools/blue/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createRegistry } from '../../registry.js';
|
||||
import { searchTool } from '../search.js';
|
||||
import { listActionsTool, proposeActionTool } from './actions.js';
|
||||
|
||||
// read (search) + her action tools. No propose_change (she fixes infra, not content).
|
||||
export const blueRegistry = createRegistry();
|
||||
blueRegistry.registerTool(searchTool);
|
||||
blueRegistry.registerTool(listActionsTool);
|
||||
blueRegistry.registerTool(proposeActionTool);
|
||||
@@ -21,11 +21,13 @@ import {
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { companionRegistry } from '../ai/agent/tools/index.js';
|
||||
import { securityRegistry } from '../ai/agent/tools/security/index.js';
|
||||
import { blueRegistry } from '../ai/agent/tools/blue/index.js';
|
||||
import { buildCtxFromEnv } from './context.js';
|
||||
|
||||
// Which toolset this stdio server exposes, selected at launch via
|
||||
// VOID_TOOL_REGISTRY (default: Dross's companion tools; 'security' = Yerin's).
|
||||
const REGISTRIES = { companion: companionRegistry, security: securityRegistry };
|
||||
// VOID_TOOL_REGISTRY (default: Dross's companion tools; 'security' = Yerin's;
|
||||
// 'blue' = Little Blue's action tools).
|
||||
const REGISTRIES = { companion: companionRegistry, security: securityRegistry, blue: blueRegistry };
|
||||
function resolveRegistry(env = process.env) {
|
||||
return REGISTRIES[env.VOID_TOOL_REGISTRY] || companionRegistry;
|
||||
}
|
||||
|
||||
22
tests/ai/agent/tools/blue.test.js
Normal file
22
tests/ai/agent/tools/blue.test.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { listActionsTool, proposeActionTool } from '../../../../lib/ai/agent/tools/blue/actions.js';
|
||||
|
||||
beforeEach(() => { process.env.VOID_API_URL = 'http://127.0.0.1:3000'; process.env.VOID_AGENT_TOKEN = 'blue-tok'; });
|
||||
|
||||
describe('blue action tools', () => {
|
||||
it('list_actions GETs the whitelist with the agent bearer', async () => {
|
||||
const fetchMock = vi.fn(async () => ({ ok: true, json: async () => ({ actions: [{ id: 'restart-caddy-ct100', tier: 'safe' }] }) }));
|
||||
const out = await listActionsTool.handler({}, {}, { fetchImpl: fetchMock });
|
||||
expect(out.actions[0].id).toBe('restart-caddy-ct100');
|
||||
const [url, opts] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('http://127.0.0.1:3000/api/actions');
|
||||
expect(opts.headers.Authorization).toBe('Bearer blue-tok');
|
||||
});
|
||||
it('propose_action POSTs run and returns the queued/executed result', async () => {
|
||||
const fetchMock = vi.fn(async () => ({ ok: true, json: async () => ({ queued: true, action_row_id: 'r1' }) }));
|
||||
const out = await proposeActionTool.handler({ action_id: 'stop-ct107' }, {}, { fetchImpl: fetchMock });
|
||||
expect(out.queued).toBe(true);
|
||||
expect(fetchMock.mock.calls[0][0]).toBe('http://127.0.0.1:3000/api/actions/stop-ct107/run');
|
||||
expect(fetchMock.mock.calls[0][1].method).toBe('POST');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user