Files
NetMesh/infrastructure/ai/vaultAgentBridgeClient.test.ts

2145 lines
71 KiB
TypeScript
Raw Permalink Normal View History

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import { fitLargeToolResultForModel } from './harness/toolResultFitting';
import type { GroupConfig, Host, ManagedSource, PortForwardingRule, ProxyProfile, Snippet, VaultNote } from '../../domain/models';
import { handleVaultAgentOp, runSerializedVaultAgentRequest, type VaultAgentApiDeps } from './vaultAgentBridgeClient';
type DepsSeed = {
hosts?: Host[];
notes?: VaultNote[];
snippets?: Snippet[];
customGroups?: string[];
groupConfigs?: GroupConfig[];
portForwardingRules?: PortForwardingRule[];
proxyProfiles?: ProxyProfile[];
managedSources?: ManagedSource[];
};
function applySnippetUpdate(
current: Snippet[],
update: Snippet[] | ((snippets: Snippet[]) => Snippet[]),
): Snippet[] {
return typeof update === 'function' ? update(current) : update;
}
function createDeps(
overrides: Partial<VaultAgentApiDeps> & DepsSeed = {},
): VaultAgentApiDeps {
let hosts = overrides.hosts ?? [];
let notes = overrides.notes ?? [];
let snippets = overrides.snippets ?? [];
let customGroups = overrides.customGroups ?? [];
let groupConfigs = overrides.groupConfigs ?? [];
let portForwardingRules = overrides.portForwardingRules ?? [];
let managedSources = overrides.managedSources ?? [];
const base: VaultAgentApiDeps = {
getHosts: () => hosts,
getNotes: () => notes,
getCustomGroups: () => customGroups,
snippets,
getGroupConfigs: () => groupConfigs,
getPortForwardingRules: () => portForwardingRules,
getManagedSources: () => managedSources,
proxyProfiles: overrides.proxyProfiles ?? [],
keys: [],
identities: [],
knownHosts: [],
resolveEffectiveHost: (host) => host,
updateHostNotes: () => {},
updateCustomGroups: (groups) => {
customGroups = groups;
},
updateGroupConfigs: (configs) => {
groupConfigs = configs;
},
updatePortForwardingRules: (rules) => {
portForwardingRules = rules;
},
updateManagedSources: (sources) => {
managedSources = sources;
},
updateHosts: (nextHosts) => {
hosts = nextHosts;
},
commitVaultGroupMutation: async (mutate) => {
const result = mutate({
groups: customGroups,
configs: groupConfigs,
hosts,
managedSources,
snippets,
});
if (result.ok) {
customGroups = result.state.groups;
groupConfigs = result.state.configs;
hosts = result.state.hosts;
managedSources = result.state.managedSources;
snippets = result.state.snippets;
}
return result;
},
saveKeyPassphrase: async () => {},
resolveKeyPassphraseAliases: async (keyPath) => [keyPath],
readKeyPassphrases: async () => ({ values: [], unreadable: false }),
removeKeyPassphrases: () => {},
updateNotes: (nextNotes) => {
notes = nextNotes;
},
updateSnippets: (snippetUpdate) => {
snippets = applySnippetUpdate(snippets, snippetUpdate);
},
startTunnel: async () => ({ success: true }),
stopTunnel: async () => ({ success: true }),
stopRuleTunnels: async () => ({ success: true }),
openHost: (host) => {
return { ok: true, sessionId: `session-${host.id}`, host };
},
};
return {
...base,
...overrides,
get snippets() {
return snippets;
},
getHosts: overrides.getHosts ?? base.getHosts,
getNotes: overrides.getNotes ?? base.getNotes,
getCustomGroups: overrides.getCustomGroups ?? base.getCustomGroups,
updateHosts: (nextHosts) => {
base.updateHosts(nextHosts);
overrides.updateHosts?.(nextHosts);
},
updateNotes: (nextNotes) => {
base.updateNotes(nextNotes);
overrides.updateNotes?.(nextNotes);
},
updateSnippets: (snippetUpdate) => {
base.updateSnippets(snippetUpdate);
overrides.updateSnippets?.(snippetUpdate);
},
updateCustomGroups: (groups) => {
base.updateCustomGroups(groups);
overrides.updateCustomGroups?.(groups);
},
updateGroupConfigs: (configs) => {
base.updateGroupConfigs(configs);
overrides.updateGroupConfigs?.(configs);
},
updatePortForwardingRules: (rules) => {
base.updatePortForwardingRules(rules);
overrides.updatePortForwardingRules?.(rules);
},
updateManagedSources: (sources) => {
base.updateManagedSources(sources);
overrides.updateManagedSources?.(sources);
},
};
}
describe('handleVaultAgentOp vault notes', () => {
it('bounds both Catty and MCP reads through the real service and forwards continuation/search parameters', async () => {
const require = createRequire(import.meta.url);
const { createVaultService } = require('../../electron/capabilities/services/vaultService.cjs');
const { registerMcpTools } = require('../../electron/capabilities/codegen/mcpToolRegistry.cjs');
const content = Array.from({ length: 10_000 }, (_, i) => `line ${i}: example text\n`).join('');
const deps = createDeps({ notes: [{ id: 'note-1', title: 'Long', content, createdAt: 1, updatedAt: 2 }] });
const service = createVaultService({ invokeVaultAgent: (op: string, params: Record<string, unknown>) => handleVaultAgentOp(op, params, deps) });
type Result = { note: { content: string }; nextOffset: number | null; matchOffset?: number };
const handlers = new Map<string, (params: Record<string, unknown>) => Promise<{ content: Array<{ text: string }> }>>();
registerMcpTools({ tool: (...args: unknown[]) => handlers.set(args[0] as string, args.at(-1) as NonNullable<ReturnType<typeof handlers.get>>) }, {
rpcCall: (_method: string, params: Record<string, unknown>) => service.getNote(params),
scopeParams: {}, guardWriteOperation: () => null, catalogDescription: (_name: string, description: string) => description,
});
const get = handlers.get('vault_notes_get')!;
let offset: number | null = 0;
let collected = '';
while (offset !== null) {
const params = { noteId: 'note-1', offset, maxChars: 9000, expectedUpdatedAt: 2 };
const raw = await service.getNote(params);
const catty = fitLargeToolResultForModel({ result: raw, capabilityId: 'vault.note.get' });
const mcp = JSON.parse((await get(params)).content[0].text) as Result;
assert.deepEqual(catty, mcp);
assert.ok(mcp.note.content.length <= 6000);
collected += mcp.note.content;
offset = mcp.nextOffset;
}
assert.equal(collected, content);
const found = JSON.parse((await get({ noteId: 'note-1', query: 'line 9999:', maxChars: 20 })).content[0].text) as Result;
assert.equal(found.matchOffset, content.indexOf('line 9999:'));
assert.equal(found.note.content.length, 20);
assert.equal((await service.getNote({ noteId: 'note-1', expectedUpdatedAt: 1 })).ok, false);
assert.equal((await service.getNote({ noteId: 'missing' })).ok, false);
const importedId = 'x'.repeat(201);
deps.updateNotes([{ id: importedId, title: 'Imported', content, createdAt: 1, updatedAt: 2 }]);
const imported = await service.getNote({ noteId: importedId });
assert.equal(imported.ok, true);
assert.equal(imported.note.id, importedId);
assert.equal(imported.note.content.length, 6000);
});
it('note.create persists to updateNotes and returns the new note', async () => {
const updated: VaultNote[][] = [];
const deps = createDeps({
notes: [],
updateNotes: (nextNotes) => {
updated.push(nextNotes);
},
});
const result = await handleVaultAgentOp(
'note.create',
{ title: 'Deploy runbook', content: '# Steps\n1. Connect' },
deps,
);
assert.equal(result.ok, true);
assert.equal(updated.length, 1);
assert.equal(updated[0]?.length, 1);
assert.equal(updated[0]?.[0]?.title, 'Deploy runbook');
assert.equal(updated[0]?.[0]?.content, '# Steps\n1. Connect');
assert.equal((result as { note?: VaultNote }).note?.id, updated[0]?.[0]?.id);
});
it('note.list returns summaries without full content', async () => {
const note: VaultNote = {
id: 'note-1',
title: 'Existing',
content: 'secret body',
createdAt: 1,
updatedAt: 2,
};
const result = await handleVaultAgentOp('note.list', {}, createDeps({ notes: [note] }));
assert.equal(result.ok, true);
const listed = (result as { notes?: Array<{ id: string; contentLength: number; title: string }> }).notes;
assert.equal(listed?.length, 1);
assert.equal(listed?.[0]?.title, 'Existing');
assert.equal(listed?.[0]?.contentLength, 'secret body'.length);
assert.equal('content' in (listed?.[0] ?? {}), false);
});
it('note.update replaces content and bumps updatedAt', async () => {
const existing: VaultNote = {
id: 'note-1',
title: 'Old title',
content: 'old',
createdAt: 100,
updatedAt: 100,
};
const updated: VaultNote[][] = [];
const deps = createDeps({
notes: [existing],
updateNotes: (nextNotes) => {
updated.push(nextNotes);
},
});
const result = await handleVaultAgentOp(
'note.update',
{ noteId: 'note-1', title: 'New title', content: 'new body' },
deps,
);
assert.equal(result.ok, true);
assert.equal(updated[0]?.[0]?.title, 'New title');
assert.equal(updated[0]?.[0]?.content, 'new body');
assert.ok((updated[0]?.[0]?.updatedAt ?? 0) >= 100);
});
it('note.delete removes the selected vault note', async () => {
const deps = createDeps({
notes: [
{ id: 'note-1', title: 'One', content: 'a', createdAt: 1, updatedAt: 1 },
{ id: 'note-2', title: 'Two', content: 'b', createdAt: 2, updatedAt: 2 },
],
});
const result = await handleVaultAgentOp('note.delete', { noteId: 'note-1' }, deps);
assert.equal(result.ok, true);
assert.deepEqual(deps.getNotes().map((note) => note.id), ['note-2']);
});
it('sequential note.create calls accumulate instead of overwriting prior notes', async () => {
const deps = createDeps({ notes: [] });
const first = await handleVaultAgentOp(
'note.create',
{ title: 'First', content: 'one' },
deps,
);
const second = await handleVaultAgentOp(
'note.create',
{ title: 'Second', content: 'two' },
deps,
);
assert.equal(first.ok, true);
assert.equal(second.ok, true);
assert.equal(deps.getNotes().length, 2);
assert.deepEqual(deps.getNotes().map((note) => note.title), ['First', 'Second']);
});
it('host.notes.set still updates host metadata separately from vault notes', async () => {
const host: Host = {
id: 'host-1',
label: 'prod',
hostname: '10.0.0.1',
username: 'root',
notes: '',
};
let hostNotes = '';
const deps = createDeps({
hosts: [host],
updateHostNotes: (hostId, notes) => {
assert.equal(hostId, 'host-1');
hostNotes = notes;
},
});
const result = await handleVaultAgentOp(
'host.notes.set',
{ hostId: 'host-1', notes: 'host detail memo' },
deps,
);
assert.equal(result.ok, true);
assert.equal(hostNotes, 'host detail memo');
});
});
describe('handleVaultAgentOp vault hosts', () => {
const csvText = [
'Label,Hostname,Port,Username,Groups',
'web-1,10.0.0.10,22,deploy,prod/web',
'db-1,10.0.0.20,22,root,prod/db',
].join('\n');
it('host.list returns metadata without passwords', async () => {
const host: Host = {
id: 'host-1',
label: 'prod',
hostname: '10.0.0.1',
username: 'root',
password: 'secret',
port: 22,
};
const result = await handleVaultAgentOp('host.list', {}, createDeps({ hosts: [host] }));
assert.equal(result.ok, true);
const hosts = (result as { hosts?: Array<Record<string, unknown>> }).hosts;
assert.equal(hosts?.length, 1);
assert.equal(hosts?.[0]?.hostname, '10.0.0.1');
assert.equal('password' in (hosts?.[0] ?? {}), false);
});
it('host.open creates a terminal session for a vault host', async () => {
const host: Host = {
id: 'host-open-1',
label: 'edge',
hostname: 'edge.example.com',
username: 'ops',
port: 22,
};
const result = await handleVaultAgentOp(
'host.open',
{ hostId: 'host-open-1', chatSessionId: 'chat-1' },
createDeps({ hosts: [host] }),
);
assert.equal(result.ok, true);
assert.equal((result as { sessionId?: string }).sessionId, 'session-host-open-1');
assert.equal((result as { hostId?: string }).hostId, 'host-open-1');
assert.equal((result as { status?: string }).status, 'connecting');
assert.equal((result as { host?: { hostname?: string } }).host?.hostname, 'edge.example.com');
});
it('host.open marks in-app chat opens (with chatSessionId) as not an external MCP call', async () => {
const host: Host = {
id: 'host-open-2',
label: 'edge',
hostname: 'edge.example.com',
username: 'ops',
port: 22,
};
let receivedIsExternalMcpCall: boolean | undefined;
const deps = createDeps({
hosts: [host],
openHost: (hostToOpen, isExternalMcpCall) => {
receivedIsExternalMcpCall = isExternalMcpCall;
return { ok: true, sessionId: `session-${hostToOpen.id}`, host: hostToOpen };
},
});
await handleVaultAgentOp('host.open', { hostId: 'host-open-2', chatSessionId: 'chat-1' }, deps);
assert.equal(receivedIsExternalMcpCall, false);
});
it('host.open marks calls under the reserved external-MCP scope as an external MCP call', async () => {
// The TCP bridge forces every authenticated external-MCP socket's
// chatSessionId to '__external_mcp__' (electron/bridges/mcpServerBridge.cjs),
// so that reserved value — not a missing chatSessionId — is what identifies
// a real external MCP client.
const host: Host = {
id: 'host-open-3',
label: 'edge',
hostname: 'edge.example.com',
username: 'ops',
port: 22,
};
let receivedIsExternalMcpCall: boolean | undefined;
const deps = createDeps({
hosts: [host],
openHost: (hostToOpen, isExternalMcpCall) => {
receivedIsExternalMcpCall = isExternalMcpCall;
return { ok: true, sessionId: `session-${hostToOpen.id}`, host: hostToOpen };
},
});
await handleVaultAgentOp('host.open', { hostId: 'host-open-3', chatSessionId: '__external_mcp__' }, deps);
assert.equal(receivedIsExternalMcpCall, true);
});
it('host.open does not treat a missing chatSessionId as an external MCP call', async () => {
const host: Host = {
id: 'host-open-4',
label: 'edge',
hostname: 'edge.example.com',
username: 'ops',
port: 22,
};
let receivedIsExternalMcpCall: boolean | undefined;
const deps = createDeps({
hosts: [host],
openHost: (hostToOpen, isExternalMcpCall) => {
receivedIsExternalMcpCall = isExternalMcpCall;
return { ok: true, sessionId: `session-${hostToOpen.id}`, host: hostToOpen };
},
});
await handleVaultAgentOp('host.open', { hostId: 'host-open-4' }, deps);
assert.equal(receivedIsExternalMcpCall, false);
});
it('session.close closes the matching terminal session', async () => {
const closed: string[] = [];
const result = await handleVaultAgentOp(
'session.close',
{ sessionId: 'session-host-open-1' },
createDeps({
closeSession: (sessionId) => {
closed.push(sessionId);
return { ok: true };
},
}),
);
assert.equal(result.ok, true);
assert.equal((result as { status?: string }).status, 'closed');
assert.deepEqual(closed, ['session-host-open-1']);
});
it('session.close reports unavailable sessions without claiming success', async () => {
const result = await handleVaultAgentOp(
'session.close',
{ sessionId: 'missing' },
createDeps({
closeSession: () => ({ ok: false, error: 'Session "missing" was not found.' }),
}),
);
assert.equal(result.ok, false);
assert.match(String((result as { error?: string }).error), /not found/i);
});
it('opens a host created by the immediately preceding request', async () => {
let openedHost: Host | undefined;
const deps = createDeps({
openHost: (hostToOpen: Host) => {
openedHost = hostToOpen;
return { ok: true, sessionId: `session-${hostToOpen.id}`, host: hostToOpen };
},
});
const created = await handleVaultAgentOp('hosts.create', {
hosts: JSON.stringify([{
label: 'New host', hostname: 'new.example.com', username: 'deploy',
}]),
}, deps);
const hostId = (created as { previewHosts?: Array<{ id?: string }> }).previewHosts?.[0]?.id ?? '';
const opened = await handleVaultAgentOp('host.open', { hostId }, deps);
assert.equal(opened.ok, true);
assert.equal(openedHost?.id, hostId);
assert.equal(openedHost?.hostname, 'new.example.com');
});
it('opens a host with group defaults changed by the immediately preceding request', async () => {
const groupedHost: Host = {
id: 'grouped-host', label: 'Grouped', hostname: 'grouped.example.com',
username: 'root', group: 'production', tags: [], os: 'linux',
};
let openedHost: Host | undefined;
let deps: VaultAgentApiDeps;
deps = createDeps({
hosts: [groupedHost],
customGroups: ['production'],
groupConfigs: [{ path: 'production', username: 'old-user' }],
resolveEffectiveHost: (hostToResolve) => ({
...hostToResolve,
username: deps.getGroupConfigs().find((config) => config.path === hostToResolve.group)?.username
?? hostToResolve.username,
}),
openHost: (hostToOpen) => {
openedHost = hostToOpen;
return { ok: true, sessionId: `session-${hostToOpen.id}`, host: hostToOpen };
},
});
assert.equal((await handleVaultAgentOp('group.update', {
path: 'production',
defaults: '{"username":"new-user"}',
}, deps)).ok, true);
assert.equal((await handleVaultAgentOp('host.open', { hostId: groupedHost.id }, deps)).ok, true);
assert.equal(openedHost?.username, 'new-user');
});
it('host.open fails for missing host ids', async () => {
const result = await handleVaultAgentOp(
'host.open',
{ hostId: 'missing' },
createDeps({ hosts: [] }),
);
assert.equal(result.ok, false);
assert.match(String((result as { error?: string }).error), /not found/i);
});
it('host.open requires hostId', async () => {
const result = await handleVaultAgentOp('host.open', {}, createDeps({ hosts: [] }));
assert.equal(result.ok, false);
assert.match(String((result as { error?: string }).error), /hostId/i);
});
it('hosts.create maps structured JSON from arbitrary text into vault hosts', async () => {
const updatedHosts: Host[][] = [];
const unstructuredMapped = JSON.stringify([
{ label: 'Prod API', hostname: 'api.example.com', username: 'deploy', port: 22, group: 'prod/api' },
{ label: 'Staging DB', hostname: '10.20.0.5', username: 'postgres', tags: ['db', 'staging'] },
]);
const result = await handleVaultAgentOp(
'hosts.create',
{ hosts: unstructuredMapped },
createDeps({
updateHosts: (hosts) => {
updatedHosts.push(hosts);
},
updateCustomGroups: () => {},
}),
);
assert.equal(result.ok, true);
assert.equal(updatedHosts[0]?.length, 2);
assert.equal((result as { addedCount?: number }).addedCount, 2);
});
it('hosts.create stores a referenced key path without private key content', async () => {
const deps = createDeps({ hosts: [] });
const result = await handleVaultAgentOp(
'hosts.create',
{
hosts: JSON.stringify([
{ hostname: 'key.example.com', username: 'deploy', keyPath: '~/.ssh/id_ed25519' },
]),
},
deps,
);
assert.equal(result.ok, true);
assert.deepEqual(deps.getHosts()[0]?.identityFilePaths, ['~/.ssh/id_ed25519']);
assert.equal(deps.getHosts()[0]?.authMethod, 'key');
const preview = (result as { previewHosts?: Array<Record<string, unknown>> }).previewHosts?.[0];
assert.equal('privateKey' in (preview ?? {}), false);
});
it('hosts.create saves a referenced key passphrase without returning it', async () => {
const savedPassphrases: Array<{ keyPath: string; passphrase: string }> = [];
const deps = createDeps({ hosts: [] });
Object.assign(deps, {
saveKeyPassphrase: async (keyPath: string, passphrase: string) => {
savedPassphrases.push({ keyPath, passphrase });
},
});
const result = await handleVaultAgentOp(
'hosts.create',
{
hosts: JSON.stringify([
{
hostname: 'encrypted-key.example.com',
username: 'deploy',
keyPath: '~/.ssh/id_encrypted',
passphrase: 'correct horse battery staple',
},
]),
},
deps,
);
assert.equal(result.ok, true);
assert.deepEqual(savedPassphrases, [{
keyPath: '~/.ssh/id_encrypted',
passphrase: 'correct horse battery staple',
}]);
assert.equal('passphrase' in (deps.getHosts()[0] ?? {}), false);
const preview = (result as { previewHosts?: Array<Record<string, unknown>> }).previewHosts?.[0];
assert.equal('passphrase' in (preview ?? {}), false);
});
it('hosts.create dry run never saves a referenced key passphrase', async () => {
const savedPassphrases: string[] = [];
const deps = createDeps({
hosts: [],
saveKeyPassphrase: async (_keyPath, passphrase) => {
savedPassphrases.push(passphrase);
},
});
const result = await handleVaultAgentOp(
'hosts.create',
{
dryRun: true,
hosts: JSON.stringify([{
hostname: 'encrypted-key.example.com',
keyPath: '~/.ssh/id_encrypted',
passphrase: 'must not be saved',
}]),
},
deps,
);
assert.equal(result.ok, true);
assert.equal((result as { dryRun?: boolean }).dryRun, true);
assert.deepEqual(savedPassphrases, []);
assert.equal(deps.getHosts().length, 0);
});
it('sequential hosts.create calls accumulate instead of dropping prior hosts', async () => {
const deps = createDeps({ hosts: [], customGroups: [] });
const first = await handleVaultAgentOp(
'hosts.create',
{ hosts: JSON.stringify([{ hostname: '10.0.0.1', username: 'root', label: 'first' }]) },
deps,
);
const second = await handleVaultAgentOp(
'hosts.create',
{ hosts: JSON.stringify([{ hostname: '10.0.0.2', username: 'root', label: 'second' }]) },
deps,
);
assert.equal(first.ok, true);
assert.equal(second.ok, true);
assert.equal(deps.getHosts().length, 2);
assert.deepEqual(deps.getHosts().map((host) => host.hostname), ['10.0.0.1', '10.0.0.2']);
});
it('host.update changes selected fields and preserves unrelated host data', async () => {
const deps = createDeps({
hosts: [{
id: 'host-1',
label: 'old',
hostname: '10.0.0.1',
username: 'root',
port: 22,
tags: ['keep'],
os: 'linux',
}],
customGroups: [],
});
const result = await handleVaultAgentOp(
'host.update',
{ hostId: 'host-1', name: 'new', ip: '10.0.0.2', group: 'prod' },
deps,
);
assert.equal(result.ok, true);
assert.equal(deps.getHosts()[0]?.label, 'new');
assert.equal(deps.getHosts()[0]?.hostname, '10.0.0.2');
assert.equal(deps.getHosts()[0]?.username, 'root');
assert.deepEqual(deps.getHosts()[0]?.tags, ['keep']);
assert.ok(deps.getCustomGroups().includes('prod'));
});
it('host.update can set the host operating system for AI-facing metadata', async () => {
const deps = createDeps({
hosts: [{
id: 'host-1',
label: 'windows box',
hostname: '10.0.0.5',
username: 'root',
port: 22,
tags: [],
os: 'linux',
}],
customGroups: [],
});
const result = await handleVaultAgentOp(
'host.update',
{ hostId: 'host-1', os: 'windows' },
deps,
);
assert.equal(result.ok, true);
assert.equal(deps.getHosts()[0]?.os, 'windows');
});
it('host.update saves a passphrase for the host key path without returning it', async () => {
const savedPassphrases: Array<{ keyPath: string; passphrase: string }> = [];
const deps = createDeps({
hosts: [{
id: 'host-1',
label: 'encrypted key',
hostname: 'key.example.com',
username: 'deploy',
port: 22,
tags: [],
os: 'linux',
identityFilePaths: ['~/.ssh/id_encrypted'],
authMethod: 'key',
}],
saveKeyPassphrase: async (keyPath, passphrase) => {
savedPassphrases.push({ keyPath, passphrase });
},
});
const result = await handleVaultAgentOp(
'host.update',
{ hostId: 'host-1', passphrase: 'updated secret' },
deps,
);
assert.equal(result.ok, true);
assert.deepEqual(savedPassphrases, [{
keyPath: '~/.ssh/id_encrypted',
passphrase: 'updated secret',
}]);
assert.equal('passphrase' in ((result as { host?: Record<string, unknown> }).host ?? {}), false);
});
it('host.update clears a saved passphrase for the host key path', async () => {
const removedKeyPaths: string[][] = [];
const deps = createDeps({
hosts: [{
id: 'host-1',
label: 'encrypted key',
hostname: 'key.example.com',
username: 'deploy',
port: 22,
tags: [],
os: 'linux',
identityFilePaths: ['~/.ssh/id_encrypted'],
authMethod: 'key',
}],
removeKeyPassphrases: (keyPaths) => {
removedKeyPaths.push(keyPaths);
},
});
const result = await handleVaultAgentOp(
'host.update',
{ hostId: 'host-1', passphrase: '' },
deps,
);
assert.equal(result.ok, true);
assert.deepEqual(removedKeyPaths, [['~/.ssh/id_encrypted']]);
});
it('host.update can clear the key path and its saved passphrase together', async () => {
const removedKeyPaths: string[][] = [];
const deps = createDeps({
hosts: [{
id: 'host-1',
label: 'encrypted key',
hostname: 'key.example.com',
username: 'deploy',
port: 22,
tags: [],
os: 'linux',
identityFilePaths: ['~/.ssh/id_encrypted'],
authMethod: 'key',
}],
removeKeyPassphrases: (keyPaths) => {
removedKeyPaths.push(keyPaths);
},
});
const result = await handleVaultAgentOp(
'host.update',
{ hostId: 'host-1', keyPath: '', passphrase: '' },
deps,
);
assert.equal(result.ok, true);
assert.deepEqual(removedKeyPaths, [['~/.ssh/id_encrypted']]);
assert.deepEqual(deps.getHosts()[0]?.identityFilePaths, []);
});
it('host.update clears the removed local key passphrase without touching a shared identity key', async () => {
const removedKeyPaths: string[][] = [];
const deps = createDeps({
hosts: [{
id: 'host-1',
label: 'identity with old local key',
hostname: 'key.example.com',
username: 'deploy',
port: 22,
tags: [],
os: 'linux',
identityId: 'identity-1',
identityFilePaths: ['~/.ssh/id_old_local'],
authMethod: 'key',
}],
keys: [{
id: 'shared-key',
label: 'shared key',
type: 'ED25519',
category: 'key',
source: 'reference',
filePath: '~/.ssh/id_shared',
privateKey: '',
created: 1,
}],
identities: [{
id: 'identity-1',
label: 'shared identity',
username: 'deploy',
authMethod: 'key',
keyId: 'shared-key',
created: 1,
}],
removeKeyPassphrases: (keyPaths) => {
removedKeyPaths.push(keyPaths);
},
});
const result = await handleVaultAgentOp(
'host.update',
{ hostId: 'host-1', keyPath: '', passphrase: '' },
deps,
);
assert.equal(result.ok, true);
assert.deepEqual(removedKeyPaths, [['~/.ssh/id_old_local']]);
assert.deepEqual(deps.getHosts()[0]?.identityFilePaths, []);
assert.equal(deps.getHosts()[0]?.identityId, 'identity-1');
});
it('host.update treats a whitespace-only key path as clearing the old local key', async () => {
const removedKeyPaths: string[][] = [];
const deps = createDeps({
hosts: [{
id: 'host-1', label: 'host', hostname: 'key.example.com', username: 'deploy',
tags: [], os: 'linux', identityFilePaths: ['~/.ssh/id_old'], authMethod: 'key',
}],
removeKeyPassphrases: (keyPaths) => removedKeyPaths.push(keyPaths),
});
const result = await handleVaultAgentOp(
'host.update',
{ hostId: 'host-1', keyPath: ' ', passphrase: '' },
deps,
);
assert.equal(result.ok, true);
assert.deepEqual(removedKeyPaths, [['~/.ssh/id_old']]);
assert.deepEqual(deps.getHosts()[0]?.identityFilePaths, []);
});
it('host.update gives keyPath priority over an empty keypath alias', async () => {
const removedKeyPaths: string[][] = [];
const deps = createDeps({
hosts: [{
id: 'host-1', label: 'host', hostname: 'key.example.com', username: 'deploy',
tags: [], os: 'linux', identityFilePaths: ['~/.ssh/id_old'], authMethod: 'key',
}],
removeKeyPassphrases: (keyPaths) => removedKeyPaths.push(keyPaths),
});
const result = await handleVaultAgentOp(
'host.update',
{ hostId: 'host-1', keyPath: '~/.ssh/id_new', keypath: '', passphrase: '' },
deps,
);
assert.equal(result.ok, true);
assert.deepEqual(removedKeyPaths, [['~/.ssh/id_new']]);
assert.deepEqual(deps.getHosts()[0]?.identityFilePaths, ['~/.ssh/id_new']);
});
it('host.update uses a newly selected key path for the saved passphrase', async () => {
const savedKeyPaths: string[] = [];
const deps = createDeps({
hosts: [{
id: 'host-1',
label: 'encrypted key',
hostname: 'key.example.com',
username: 'deploy',
port: 22,
tags: [],
os: 'linux',
identityFilePaths: ['~/.ssh/id_old'],
authMethod: 'key',
}],
saveKeyPassphrase: async (keyPath) => {
savedKeyPaths.push(keyPath);
},
});
const result = await handleVaultAgentOp(
'host.update',
{
hostId: 'host-1',
keyPath: '~/.ssh/id_new',
passphrase: 'new secret',
},
deps,
);
assert.equal(result.ok, true);
assert.deepEqual(savedKeyPaths, ['~/.ssh/id_new']);
assert.deepEqual(deps.getHosts()[0]?.identityFilePaths, ['~/.ssh/id_new']);
});
it('host.update rejects a passphrase without a key path before changing the host', async () => {
const deps = createDeps({
hosts: [{
id: 'host-1',
label: 'original',
hostname: 'password.example.com',
username: 'deploy',
port: 22,
tags: [],
os: 'linux',
}],
});
const result = await handleVaultAgentOp(
'host.update',
{ hostId: 'host-1', label: 'must not change', passphrase: 'orphan secret' },
deps,
);
assert.equal(result.ok, false);
assert.match(String((result as { error?: string }).error), /keyPath/i);
assert.equal(deps.getHosts()[0]?.label, 'original');
});
it('host.update applies effective inherited credential settings', async () => {
const host: Host = {
id: 'host-1',
label: 'inherited',
hostname: '10.0.0.1',
username: 'root',
port: 22,
tags: [],
os: 'linux',
};
const passwordDeps = createDeps({
hosts: [host],
resolveEffectiveHost: (current) => ({ ...current, savePassword: false }),
});
const identityDeps = createDeps({
hosts: [host],
resolveEffectiveHost: (current) => ({ ...current, identityId: 'identity-from-group' }),
});
const passwordResult = await handleVaultAgentOp(
'host.update',
{ hostId: host.id, password: 'do-not-persist' },
passwordDeps,
);
const usernameResult = await handleVaultAgentOp(
'host.update',
{ hostId: host.id, username: 'deploy' },
identityDeps,
);
assert.equal(passwordResult.ok, false);
assert.equal(passwordDeps.getHosts()[0]?.password, undefined);
assert.equal(usernameResult.ok, true);
assert.equal(identityDeps.getHosts()[0]?.username, 'deploy');
assert.equal(identityDeps.getHosts()[0]?.identityId, '');
});
it('host.update aligns managed-source ownership when moving groups', async () => {
const deps = createDeps({
hosts: [{
id: 'host-1',
label: 'managed host',
hostname: '10.0.0.1',
username: 'root',
group: 'managed',
tags: [],
os: 'linux',
managedSourceId: 'source-1',
}],
managedSources: [{
id: 'source-1',
type: 'ssh_config',
filePath: '~/.ssh/config',
groupName: 'managed',
lastSyncedAt: 1,
}],
});
const result = await handleVaultAgentOp(
'host.update',
{ hostId: 'host-1', group: '' },
deps,
);
assert.equal(result.ok, true);
assert.equal(deps.getHosts()[0]?.group, undefined);
assert.equal(deps.getHosts()[0]?.managedSourceId, undefined);
});
it('host.delete removes the requested host', async () => {
const deps = createDeps({
hosts: [
{ id: 'host-1', label: 'one', hostname: 'one', username: 'root', tags: [], os: 'linux' },
{ id: 'host-2', label: 'two', hostname: 'two', username: 'root', tags: [], os: 'linux' },
],
});
const result = await handleVaultAgentOp('host.delete', { hostId: 'host-1' }, deps);
assert.equal(result.ok, true);
assert.deepEqual(deps.getHosts().map((host) => host.id), ['host-2']);
assert.equal((result as { deletedHost?: { id?: string } }).deletedHost?.id, 'host-1');
});
it('host.delete keeps jump hosts referenced by empty group defaults', async () => {
const jump: Host = {
id: 'jump', label: 'jump', hostname: 'jump.example.com', username: 'root',
protocol: 'ssh', tags: [], os: 'linux',
};
const deps = createDeps({
hosts: [jump],
groupConfigs: [{ path: 'prod', hostChain: { hostIds: [jump.id] } }],
});
const result = await handleVaultAgentOp('host.delete', { hostId: jump.id }, deps);
assert.equal(result.ok, false);
assert.deepEqual(deps.getHosts().map((current) => current.id), [jump.id]);
});
it('host.update and host.delete never return nested proxy passwords', async () => {
const host: Host = {
id: 'host-1',
label: 'proxied',
hostname: '10.0.0.1',
username: 'root',
tags: [],
os: 'linux',
proxyConfig: {
type: 'http',
host: 'proxy.example.com',
port: 8080,
username: 'proxy-user',
password: 'proxy-secret',
},
};
const updateDeps = createDeps({ hosts: [host] });
const deleteDeps = createDeps({ hosts: [host] });
const updateResult = await handleVaultAgentOp(
'host.update',
{ hostId: host.id, notes: 'updated' },
updateDeps,
);
const deleteResult = await handleVaultAgentOp(
'host.delete',
{ hostId: host.id },
deleteDeps,
);
assert.equal(updateResult.ok, true);
assert.equal(deleteResult.ok, true);
const updatedProxy = (updateResult as {
host?: { proxyConfig?: Record<string, unknown> };
}).host?.proxyConfig;
const deletedProxy = (deleteResult as {
deletedHost?: { proxyConfig?: Record<string, unknown> };
}).deletedHost?.proxyConfig;
assert.equal(updatedProxy?.host, 'proxy.example.com');
assert.equal(deletedProxy?.host, 'proxy.example.com');
assert.equal('password' in (updatedProxy ?? {}), false);
assert.equal('password' in (deletedProxy ?? {}), false);
});
it('host.import dryRun previews parsed hosts without writing', async () => {
const updatedHosts: Host[][] = [];
const result = await handleVaultAgentOp(
'host.import',
{ format: 'csv', text: csvText, dryRun: 'true' },
createDeps({
updateHosts: (hosts) => {
updatedHosts.push(hosts);
},
}),
);
assert.equal(result.ok, true);
assert.equal((result as { dryRun?: boolean }).dryRun, true);
assert.equal(updatedHosts.length, 0);
assert.equal((result as { previewHosts?: unknown[] }).previewHosts?.length, 2);
});
it('host.import dryRun reports path-alias passphrase conflicts without writing', async () => {
const saved: Array<{ keyPath: string; passphrase: string }> = [];
const result = await handleVaultAgentOp(
'host.import',
{
format: 'csv',
dryRun: true,
text: [
'Label,Hostname,Username,KeyPath,Passphrase',
'first,first.example.com,root,~/.ssh/shared,first-secret',
'second,second.example.com,root,/Users/alice/.ssh/shared,second-secret',
].join('\n'),
},
createDeps({
resolveKeyPassphraseAliases: async (keyPath) => (
keyPath.startsWith('~/')
? [keyPath, `/Users/alice/${keyPath.slice(2)}`]
: [keyPath, `~/${keyPath.slice('/Users/alice/'.length)}`]
),
saveKeyPassphrase: async (keyPath, passphrase) => {
saved.push({ keyPath, passphrase });
},
}),
);
assert.equal(result.ok, true);
assert.equal((result as { dryRun?: boolean }).dryRun, true);
assert.deepEqual(saved, []);
assert.match(
(result as { issues?: Array<{ message: string }> }).issues?.[0]?.message ?? '',
/conflicting passphrases/u,
);
});
it('host.import blocks alias conflicts involving a skipped duplicate host', async () => {
const existing: Host = {
id: 'existing',
label: 'old',
hostname: 'old.example.com',
username: 'root',
port: 22,
};
const saved: Array<{ keyPath: string; passphrase: string }> = [];
const params = {
format: 'csv',
text: [
'Label,Hostname,Port,Username,KeyPath,Passphrase',
'old,old.example.com,22,root,~/.ssh/shared,old-secret',
'new,new.example.com,22,root,/Users/alice/.ssh/shared,new-secret',
].join('\n'),
};
const aliases = async (keyPath: string) => (
keyPath.startsWith('~/')
? [keyPath, `/Users/alice/${keyPath.slice(2)}`]
: [keyPath, `~/${keyPath.slice('/Users/alice/'.length)}`]
);
const preview = await handleVaultAgentOp(
'host.import',
{ ...params, dryRun: true },
createDeps({ hosts: [existing], resolveKeyPassphraseAliases: aliases }),
);
const imported = await handleVaultAgentOp(
'host.import',
params,
createDeps({
hosts: [existing],
resolveKeyPassphraseAliases: aliases,
saveKeyPassphrase: async (keyPath, passphrase) => {
saved.push({ keyPath, passphrase });
},
}),
);
assert.equal(preview.ok, true);
assert.equal(imported.ok, true);
assert.deepEqual(saved, []);
assert.match(
(preview as { issues?: Array<{ message: string }> }).issues?.[0]?.message ?? '',
/conflicting passphrases/u,
);
assert.match(
(imported as { issues?: Array<{ message: string }> }).issues?.[0]?.message ?? '',
/conflicting passphrases/u,
);
});
it('host.import keeps a new host credential when a skipped alias has the same passphrase', async () => {
const existing: Host = {
id: 'existing',
label: 'old',
hostname: 'old.example.com',
username: 'root',
port: 22,
};
const saved: Array<{ keyPath: string; passphrase: string }> = [];
const result = await handleVaultAgentOp(
'host.import',
{
format: 'csv',
text: [
'Label,Hostname,Port,Username,KeyPath,Passphrase',
'old,old.example.com,22,root,~/.ssh/shared,same-secret',
'new,new.example.com,22,root,/Users/alice/.ssh/shared,same-secret',
].join('\n'),
},
createDeps({
hosts: [existing],
resolveKeyPassphraseAliases: async (keyPath) => (
keyPath.startsWith('~/')
? [keyPath, `/Users/alice/${keyPath.slice(2)}`]
: [keyPath, `~/${keyPath.slice('/Users/alice/'.length)}`]
),
saveKeyPassphrase: async (keyPath, passphrase) => {
saved.push({ keyPath, passphrase });
},
}),
);
assert.equal(result.ok, true);
assert.deepEqual(saved, [{
keyPath: '/Users/alice/.ssh/shared',
passphrase: 'same-secret',
}]);
});
it('host.import blocks an alias after an exact-path conflict', async () => {
const saved: Array<{ keyPath: string; passphrase: string }> = [];
const result = await handleVaultAgentOp(
'host.import',
{
format: 'csv',
text: [
'Label,Hostname,Username,KeyPath,Passphrase',
'one,one.example.com,root,~/.ssh/shared,one',
'two,two.example.com,root,~/.ssh/shared,two',
'three,three.example.com,root,/Users/alice/.ssh/shared,three',
].join('\n'),
},
createDeps({
resolveKeyPassphraseAliases: async (keyPath) => (
keyPath.startsWith('~/')
? [keyPath, `/Users/alice/${keyPath.slice(2)}`]
: [keyPath, `~/${keyPath.slice('/Users/alice/'.length)}`]
),
saveKeyPassphrase: async (keyPath, passphrase) => {
saved.push({ keyPath, passphrase });
},
}),
);
assert.equal(result.ok, true);
assert.deepEqual(saved, []);
assert.ok(
(result as { issues?: Array<{ message: string }> }).issues
?.some((issue) => /conflicting passphrases/u.test(issue.message)),
);
});
it('host.import applies hosts to the vault', async () => {
const updatedHosts: Host[][] = [];
const updatedGroups: string[][] = [];
const result = await handleVaultAgentOp(
'host.import',
{ format: 'auto', text: csvText },
createDeps({
updateHosts: (hosts) => {
updatedHosts.push(hosts);
},
updateCustomGroups: (groups) => {
updatedGroups.push(groups);
},
}),
);
assert.equal(result.ok, true);
assert.equal(updatedHosts[0]?.length, 2);
assert.ok(updatedGroups[0]?.includes('prod/web'));
assert.equal((result as { addedCount?: number }).addedCount, 2);
});
it('host.import saves CSV key passphrases only for newly added hosts', async () => {
const saved: Array<{ keyPath: string; passphrase: string }> = [];
const result = await handleVaultAgentOp(
'host.import',
{
format: 'csv',
text: [
'Label,Hostname,Username,KeyPath,Passphrase',
'key-host,key.example.com,ubuntu,~/.ssh/id_ed25519,secret',
].join('\n'),
},
createDeps({
saveKeyPassphrase: async (keyPath, passphrase) => {
saved.push({ keyPath, passphrase });
},
}),
);
assert.equal(result.ok, true);
assert.deepEqual(saved, [{ keyPath: '~/.ssh/id_ed25519', passphrase: 'secret' }]);
saved.length = 0;
const duplicate = await handleVaultAgentOp(
'host.import',
{
format: 'csv',
text: [
'Label,Hostname,Username,KeyPath,Passphrase',
'key-host,key.example.com,ubuntu,~/.ssh/id_ed25519,replacement',
].join('\n'),
},
createDeps({
hosts: [{
id: 'existing-key-host',
label: 'key-host',
hostname: 'key.example.com',
username: 'ubuntu',
port: 22,
tags: [],
os: 'linux',
}],
saveKeyPassphrase: async (keyPath, passphrase) => {
saved.push({ keyPath, passphrase });
},
}),
);
assert.equal(duplicate.ok, true);
assert.deepEqual(saved, []);
});
it('host.import does not save conflicting passphrases for a shared key path', async () => {
const saved: Array<{ keyPath: string; passphrase: string }> = [];
const result = await handleVaultAgentOp(
'host.import',
{
format: 'csv',
text: [
'Label,Hostname,Username,KeyPath,Passphrase',
'first,first.example.com,root,~/.ssh/id_shared,first-secret',
'second,second.example.com,root,~/.ssh/id_shared,second-secret',
].join('\n'),
},
createDeps({
saveKeyPassphrase: async (keyPath, passphrase) => {
saved.push({ keyPath, passphrase });
},
}),
);
assert.equal(result.ok, true);
assert.deepEqual(saved, []);
assert.equal(
(result as { issues?: Array<{ message: string }> }).issues
?.filter((issue) => /conflicting passphrases/u.test(issue.message)).length,
1,
);
assert.match(
(result as { issues?: Array<{ message: string }> }).issues?.[0]?.message ?? '',
/conflicting passphrases/u,
);
});
it('host.import resolves path aliases before saving passphrases', async () => {
const saved: Array<{ keyPath: string; passphrase: string }> = [];
const result = await handleVaultAgentOp(
'host.import',
{
format: 'csv',
text: [
'Label,Hostname,Username,KeyPath,Passphrase',
'first,first.example.com,root,~/.ssh/shared,first-secret',
'second,second.example.com,root,/Users/alice/.ssh/shared,second-secret',
].join('\n'),
},
createDeps({
resolveKeyPassphraseAliases: async (keyPath) => (
keyPath.startsWith('~/')
? [keyPath, `/Users/alice/${keyPath.slice(2)}`]
: [keyPath, `~/${keyPath.slice('/Users/alice/'.length)}`]
),
saveKeyPassphrase: async (keyPath, passphrase) => {
saved.push({ keyPath, passphrase });
},
}),
);
assert.equal(result.ok, true);
assert.deepEqual(saved, []);
assert.match(
(result as { issues?: Array<{ message: string }> }).issues?.[0]?.message ?? '',
/conflicting passphrases/u,
);
});
it('host.import reports passphrase persistence failures as partial success', async () => {
const result = await handleVaultAgentOp(
'host.import',
{
format: 'csv',
text: [
'Label,Hostname,Username,KeyPath,Passphrase',
'key-host,key.example.com,root,~/.ssh/id_ed25519,secret',
].join('\n'),
},
createDeps({
saveKeyPassphrase: async () => {
throw new Error('storage unavailable');
},
}),
);
assert.equal(result.ok, true);
assert.match(
(result as { issues?: Array<{ message: string }> }).issues?.[0]?.message ?? '',
/Could not save the passphrase/u,
);
});
it('host.import keeps a different existing saved key passphrase', async () => {
const saved: Array<{ keyPath: string; passphrase: string }> = [];
const result = await handleVaultAgentOp(
'host.import',
{
format: 'csv',
text: [
'Label,Hostname,Username,KeyPath,Passphrase',
'new,new.example.com,root,~/.ssh/shared,stale-import',
].join('\n'),
},
createDeps({
readKeyPassphrases: async () => ({
values: ['current-saved'],
unreadable: false,
}),
saveKeyPassphrase: async (keyPath, passphrase) => {
saved.push({ keyPath, passphrase });
},
}),
);
assert.equal(result.ok, true);
assert.deepEqual(saved, []);
assert.ok(
(result as { issues?: Array<{ message: string }> }).issues
?.some((issue) => /existing saved passphrase/u.test(issue.message)),
);
});
it('host.import keeps a passphrase corrected after the initial check', async () => {
const saved: Array<{ keyPath: string; passphrase: string }> = [];
const result = await handleVaultAgentOp(
'host.import',
{
format: 'csv',
text: [
'Label,Hostname,Username,KeyPath,Passphrase',
'new,new.example.com,root,~/.ssh/shared,stale-import',
].join('\n'),
},
createDeps({
readKeyPassphrases: async () => ({ values: [], unreadable: false }),
saveImportedKeyPassphrase: async () => 'conflict',
saveKeyPassphrase: async (keyPath, passphrase) => {
saved.push({ keyPath, passphrase });
},
}),
);
assert.equal(result.ok, true);
assert.deepEqual(saved, []);
assert.ok(
(result as { issues?: Array<{ message: string }> }).issues
?.some((issue) => /existing saved passphrase/u.test(issue.message)),
);
});
});
describe('handleVaultAgentOp vault management gaps', () => {
const host: Host = {
id: 'host-1',
label: 'prod',
hostname: '10.0.0.1',
username: 'root',
tags: [],
os: 'linux',
};
it('lists safe identity metadata and binds an existing identity to a host', async () => {
const deps = createDeps({
hosts: [host],
identities: [{
id: 'identity-1',
label: 'Production deploy',
username: 'deploy',
authMethod: 'password',
password: 'must-not-leak',
created: 1,
}],
});
const listed = await handleVaultAgentOp('identity.list', {}, deps);
assert.equal(listed.ok, true);
assert.equal(JSON.stringify(listed).includes('must-not-leak'), false);
const updated = await handleVaultAgentOp(
'host.update',
{ hostId: 'host-1', identityId: 'identity-1' },
deps,
);
assert.equal(updated.ok, true);
assert.equal(deps.getHosts()[0]?.identityId, 'identity-1');
assert.equal(deps.getHosts()[0]?.username, 'deploy');
});
it('updates advanced connection settings on a host', async () => {
const jump: Host = { ...host, id: 'jump-1', label: 'jump', hostname: 'jump.test' };
const deps = createDeps({
hosts: [host, jump],
proxyProfiles: [{
id: 'proxy-1',
label: 'Office proxy',
config: { type: 'socks5', host: '127.0.0.1', port: 1080, password: 'hidden' },
createdAt: 1,
}],
});
const result = await handleVaultAgentOp('host.update', {
hostId: 'host-1',
jumpHostIds: '["jump-1"]',
proxyProfileId: 'proxy-1',
startupCommand: 'tmux attach || tmux',
environmentVariables: '{"APP_ENV":"production"}',
moshEnabled: 'true',
moshServerPath: '/usr/local/bin/mosh-server',
etEnabled: 'false',
etPort: 2022,
}, deps);
assert.equal(result.ok, true);
assert.deepEqual(deps.getHosts()[0]?.hostChain?.hostIds, ['jump-1']);
assert.equal(deps.getHosts()[0]?.proxyProfileId, 'proxy-1');
assert.deepEqual(deps.getHosts()[0]?.environmentVariables, [{ name: 'APP_ENV', value: 'production' }]);
assert.equal(JSON.stringify(result).includes('hidden'), false);
});
it('creates, updates, and deletes port forwarding rules', async () => {
const deps = createDeps({ hosts: [host] });
const created = await handleVaultAgentOp('portforward.rules.create', {
label: 'Web', type: 'local', localPort: 8080, bindAddress: '127.0.0.1',
remoteHost: '127.0.0.1', remotePort: 80, hostId: 'host-1',
}, deps);
assert.equal(created.ok, true);
const ruleId = (created as { rule?: { id?: string } }).rule?.id ?? '';
assert.equal((await handleVaultAgentOp('portforward.rules.update', { ruleId, localPort: 8081 }, deps)).ok, true);
assert.equal((await handleVaultAgentOp('portforward.rules.duplicate', { ruleId }, deps)).ok, true);
assert.equal(deps.getPortForwardingRules().length, 2);
assert.equal((await handleVaultAgentOp('portforward.rules.delete', { ruleId }, deps)).ok, true);
assert.equal(deps.getPortForwardingRules().length, 1);
});
it('reports an inactive rule after changing a running forwarding connection', async () => {
const rule: PortForwardingRule = {
id: 'rule-1', label: 'Web', type: 'local', localPort: 8080,
bindAddress: '127.0.0.1', remoteHost: '127.0.0.1', remotePort: 80,
hostId: host.id, status: 'active', createdAt: 1,
};
let stopCalls = 0;
const deps = createDeps({
hosts: [host],
portForwardingRules: [rule],
stopRuleTunnels: async () => {
stopCalls += 1;
return { success: true };
},
});
const result = await handleVaultAgentOp('portforward.rules.update', {
ruleId: rule.id,
localPort: 8081,
}, deps);
assert.equal(result.ok, true);
assert.equal(stopCalls, 1);
assert.equal((result as { rule?: { status?: string } }).rule?.status, 'inactive');
assert.equal(deps.getPortForwardingRules()[0]?.status, 'inactive');
});
it('preserves forwarding rule changes made while an old tunnel is stopping', async () => {
const rule: PortForwardingRule = {
id: 'rule-1', label: 'Web', type: 'local', localPort: 8080,
bindAddress: '127.0.0.1', remoteHost: '127.0.0.1', remotePort: 80,
hostId: host.id, status: 'active', createdAt: 1,
};
const concurrentRule: PortForwardingRule = {
id: 'rule-2', label: 'Database', type: 'local', localPort: 5432,
bindAddress: '127.0.0.1', remoteHost: '127.0.0.1', remotePort: 5432,
hostId: host.id, status: 'inactive', createdAt: 2,
};
let deps: VaultAgentApiDeps;
deps = createDeps({
hosts: [host],
portForwardingRules: [rule],
stopRuleTunnels: async () => {
deps.updatePortForwardingRules([...deps.getPortForwardingRules(), concurrentRule]);
return { success: true };
},
});
const result = await handleVaultAgentOp('portforward.rules.update', {
ruleId: rule.id,
localPort: 8081,
}, deps);
assert.equal(result.ok, true);
assert.equal(deps.getPortForwardingRules().find((entry) => entry.id === rule.id)?.localPort, 8081);
assert.equal(deps.getPortForwardingRules().find((entry) => entry.id === concurrentRule.id)?.label, 'Database');
});
it('reports a stopped rule as inactive when the same connection was saved concurrently', async () => {
const rule: PortForwardingRule = {
id: 'rule-1', label: 'Web', type: 'local', localPort: 8080,
bindAddress: '127.0.0.1', remoteHost: '127.0.0.1', remotePort: 80,
hostId: host.id, status: 'active', createdAt: 1,
};
let deps: VaultAgentApiDeps;
deps = createDeps({
hosts: [host],
portForwardingRules: [rule],
stopRuleTunnels: async () => {
deps.updatePortForwardingRules([{
...rule,
localPort: 8081,
status: 'active',
error: 'stale error',
}]);
return { success: true };
},
});
const result = await handleVaultAgentOp('portforward.rules.update', {
ruleId: rule.id,
localPort: 8081,
}, deps);
assert.equal(result.ok, true);
assert.equal((result as { rule?: { status?: string; error?: string } }).rule?.status, 'inactive');
assert.equal((result as { rule?: { status?: string; error?: string } }).rule?.error, undefined);
assert.equal(deps.getPortForwardingRules()[0]?.status, 'inactive');
assert.equal(deps.getPortForwardingRules()[0]?.error, undefined);
});
it('does not restore a forwarding rule deleted while its old tunnel is stopping', async () => {
const rule: PortForwardingRule = {
id: 'rule-1', label: 'Web', type: 'local', localPort: 8080,
bindAddress: '127.0.0.1', remoteHost: '127.0.0.1', remotePort: 80,
hostId: host.id, status: 'active', createdAt: 1,
};
let deps: VaultAgentApiDeps;
deps = createDeps({
hosts: [host],
portForwardingRules: [rule],
stopRuleTunnels: async () => {
deps.updatePortForwardingRules([]);
return { success: true };
},
});
const result = await handleVaultAgentOp('portforward.rules.update', {
ruleId: rule.id,
localPort: 8081,
}, deps);
assert.equal(result.ok, false);
assert.deepEqual(deps.getPortForwardingRules(), []);
});
it('cleans up backend tunnels before editing an inactive forwarding rule', async () => {
const rule: PortForwardingRule = {
id: 'rule-1', label: 'Web', type: 'local', localPort: 8080,
bindAddress: '127.0.0.1', remoteHost: '127.0.0.1', remotePort: 80,
hostId: host.id, status: 'inactive', createdAt: 1,
};
let cleanupCalls = 0;
const deps = createDeps({
hosts: [host],
portForwardingRules: [rule],
stopRuleTunnels: async () => {
cleanupCalls += 1;
return { success: true };
},
});
const result = await handleVaultAgentOp('portforward.rules.update', {
ruleId: rule.id,
localPort: 8081,
}, deps);
assert.equal(result.ok, true);
assert.equal(cleanupCalls, 1);
});
it('keeps a running forwarding rule unchanged when stopping its old tunnel fails', async () => {
const rule: PortForwardingRule = {
id: 'rule-1', label: 'Web', type: 'local', localPort: 8080,
bindAddress: '127.0.0.1', remoteHost: '127.0.0.1', remotePort: 80,
hostId: host.id, status: 'active', createdAt: 1,
};
const deps = createDeps({
hosts: [host],
portForwardingRules: [rule],
stopRuleTunnels: async () => ({ success: false, error: 'stop failed' }),
});
const result = await handleVaultAgentOp('portforward.rules.update', {
ruleId: rule.id,
localPort: 8081,
}, deps);
assert.equal(result.ok, false);
assert.equal(deps.getPortForwardingRules()[0]?.localPort, 8080);
assert.equal(deps.getPortForwardingRules()[0]?.status, 'active');
});
it('passes trusted host records when the agent starts port forwarding', async () => {
const rule: PortForwardingRule = {
id: 'rule-1', label: 'Web', type: 'local', localPort: 8080,
bindAddress: '127.0.0.1', remoteHost: '127.0.0.1', remotePort: 80,
hostId: host.id, status: 'inactive', createdAt: 1,
};
const knownHosts = [{
id: 'known-1',
hostname: host.hostname,
port: 22,
keyType: 'ssh-ed25519',
publicKey: 'AAAAC3NzaC1lZDI1NTE5AAAAITest',
discoveredAt: 1,
}];
let forwardedKnownHosts: VaultAgentApiDeps['knownHosts'] | undefined;
const deps = createDeps({
hosts: [host],
knownHosts,
portForwardingRules: [rule],
startTunnel: async (
_rule,
_host,
_hosts,
_keys,
_identities,
_onStatusChange,
_enableReconnect,
_terminalSettings,
receivedKnownHosts,
) => {
forwardedKnownHosts = receivedKnownHosts;
return { success: true };
},
});
const result = await handleVaultAgentOp('portforward.start', { ruleId: rule.id }, deps);
assert.equal(result.ok, true);
assert.equal(forwardedKnownHosts, knownHosts);
});
it('does not start a forwarding rule whose host was changed to serial', async () => {
const serialHost: Host = { ...host, protocol: 'serial' };
const rule: PortForwardingRule = {
id: 'rule-1', label: 'Web', type: 'local', localPort: 8080,
bindAddress: '127.0.0.1', remoteHost: '127.0.0.1', remotePort: 80,
hostId: host.id, status: 'inactive', createdAt: 1,
};
let startCalls = 0;
const deps = createDeps({
hosts: [serialHost],
portForwardingRules: [rule],
startTunnel: async () => {
startCalls += 1;
return { success: true };
},
});
const result = await handleVaultAgentOp('portforward.start', { ruleId: rule.id }, deps);
assert.equal(result.ok, false);
assert.equal(startCalls, 0);
});
it('rejects forwarding rules when the host inherits an unsupported protocol', async () => {
const rule: PortForwardingRule = {
id: 'rule-1', label: 'Web', type: 'local', localPort: 8080,
bindAddress: '127.0.0.1', remoteHost: '127.0.0.1', remotePort: 80,
hostId: host.id, status: 'inactive', createdAt: 1,
};
let startCalls = 0;
const deps = createDeps({
hosts: [host],
portForwardingRules: [rule],
resolveEffectiveHost: (current) => ({ ...current, protocol: 'telnet' }),
startTunnel: async () => {
startCalls += 1;
return { success: true };
},
});
const createResult = await handleVaultAgentOp('portforward.rules.create', {
label: 'Inherited', type: 'local', localPort: 8081,
remoteHost: '127.0.0.1', remotePort: 81, hostId: host.id,
}, deps);
const updateResult = await handleVaultAgentOp('portforward.rules.update', {
ruleId: rule.id, label: 'Renamed',
}, deps);
const duplicateResult = await handleVaultAgentOp('portforward.rules.duplicate', {
ruleId: rule.id,
}, deps);
const startResult = await handleVaultAgentOp('portforward.start', { ruleId: rule.id }, deps);
for (const result of [createResult, updateResult, duplicateResult, startResult]) {
assert.equal(result.ok, false);
if (!result.ok) assert.match(String(result.error), /does not support port forwarding/i);
}
assert.equal(startCalls, 0);
assert.deepEqual(deps.getPortForwardingRules(), [rule]);
});
it('persists an inactive status after stopping a forwarding rule', async () => {
const rule: PortForwardingRule = {
id: 'rule-1', label: 'Web', type: 'local', localPort: 8080,
bindAddress: '127.0.0.1', remoteHost: '127.0.0.1', remotePort: 80,
hostId: host.id, status: 'active', error: 'stale', createdAt: 1,
};
const deps = createDeps({ hosts: [host], portForwardingRules: [rule] });
const stopped = await handleVaultAgentOp('portforward.stop', { ruleId: rule.id }, deps);
const listed = await handleVaultAgentOp('portforward.rules.list', {}, deps);
assert.equal(stopped.ok, true);
assert.equal(deps.getPortForwardingRules()[0]?.status, 'inactive');
assert.equal(deps.getPortForwardingRules()[0]?.error, undefined);
assert.equal((listed as { rules?: Array<{ status?: string }> }).rules?.[0]?.status, 'inactive');
});
it('manages groups and applies reusable defaults without exposing secrets', async () => {
const deps = createDeps({
hosts: [{ ...host, group: 'prod' }],
identities: [{ id: 'identity-1', label: 'Deploy', username: 'deploy', authMethod: 'key', keyId: 'key-1', created: 1 }],
proxyProfiles: [{ id: 'proxy-1', label: 'Proxy', config: { type: 'http', host: 'proxy.test', port: 8080, password: 'hidden' }, createdAt: 1 }],
customGroups: ['prod'],
groupConfigs: [{
path: 'prod',
startupCommand: 'echo startup-secret',
environmentVariables: [{ name: 'TOKEN', value: 'environment-secret' }],
identityFileId: 'key-1',
identityFilePaths: ['/Users/alice/.ssh/id_prod'],
moshServerPath: '/Users/alice/bin/mosh-server',
proxyConfig: {
type: 'command', host: 'proxy.internal', port: 22, command: 'proxy-command-secret',
},
}],
});
const updated = await handleVaultAgentOp('group.update', {
path: 'prod',
defaults: '{"username":"deploy","identityId":"identity-1","proxyProfileId":"proxy-1"}',
}, deps);
assert.equal(updated.ok, true);
assert.equal(deps.getGroupConfigs()[0]?.username, 'deploy');
assert.equal(JSON.stringify(updated).includes('hidden'), false);
const listed = await handleVaultAgentOp('group.list', {}, deps);
const serializedList = JSON.stringify(listed);
assert.equal(serializedList.includes('startup-secret'), false);
assert.equal(serializedList.includes('environment-secret'), false);
assert.equal(serializedList.includes('proxy-command-secret'), false);
assert.equal(serializedList.includes('id_prod'), false);
assert.equal(serializedList.includes('/Users/alice/bin/mosh-server'), true);
assert.equal(serializedList.includes('key-1'), false);
assert.equal(serializedList.includes('deploy'), true);
const invalidDelete = await handleVaultAgentOp('group.delete', {
path: 'prod', deleteHosts: 'ture',
}, deps);
assert.equal(invalidDelete.ok, false);
assert.equal(deps.getCustomGroups().includes('prod'), true);
const removed = await handleVaultAgentOp('group.delete', { path: 'prod' }, deps);
assert.equal(removed.ok, true);
assert.equal(deps.getCustomGroups().includes('prod'), false);
assert.equal(deps.getHosts()[0]?.group, undefined);
});
it('group path updates transform the latest snippet snapshot', async () => {
const original: Snippet = {
id: 'deploy', label: 'Deploy', command: 'nct.log(1)', kind: 'script',
targetGroups: ['prod'],
};
let committedSnippets: Snippet[] = [];
const deps = createDeps({
snippets: [original],
customGroups: ['prod'],
commitVaultGroupMutation: async (mutate) => {
const result = mutate({
groups: ['prod'],
configs: [],
hosts: [],
managedSources: [],
snippets: [{ ...original, command: 'nct.log(2)' }],
});
if (result.ok) committedSnippets = result.state.snippets;
return result;
},
});
const result = await handleVaultAgentOp('group.update', {
path: 'prod',
newPath: 'production',
}, deps);
assert.equal(result.ok, true);
assert.equal(committedSnippets[0]?.command, 'nct.log(2)');
assert.deepEqual(committedSnippets[0]?.targetGroups, ['production']);
});
it('does not report group update success when the joint Vault commit fails', async () => {
const deps = createDeps({
customGroups: ['prod'],
commitVaultGroupMutation: async () => {
throw new Error('Vault quota exhausted');
},
});
const result = await handleVaultAgentOp('group.update', {
path: 'prod', newPath: 'production',
}, deps);
assert.equal(result.ok, false);
assert.match(String(result.error), /quota exhausted/i);
assert.deepEqual(deps.getCustomGroups(), ['prod']);
});
it('does not report group delete success when the joint Vault commit fails', async () => {
const deps = createDeps({
customGroups: ['prod'],
commitVaultGroupMutation: async () => {
throw new Error('Vault quota exhausted');
},
});
const result = await handleVaultAgentOp('group.delete', { path: 'prod' }, deps);
assert.equal(result.ok, false);
assert.match(String(result.error), /quota exhausted/i);
assert.deepEqual(deps.getCustomGroups(), ['prod']);
});
});
describe('handleVaultAgentOp snippets and scripts', () => {
it('snippets.create persists script metadata via updateSnippets', async () => {
const updated: Snippet[][] = [];
const deps = createDeps({
snippets: [],
updateSnippets: (nextSnippets) => {
updated.push(applySnippetUpdate([], nextSnippets));
},
});
const result = await handleVaultAgentOp(
'snippets.create',
{
label: 'Deploy',
content: 'await nct.screen.sendLine("echo ok");',
kind: 'script',
trigger: 'manual',
},
deps,
);
assert.equal(result.ok, true);
assert.equal(updated.length, 1);
assert.equal(updated[0]?.[0]?.kind, 'script');
assert.equal(updated[0]?.[0]?.label, 'Deploy');
});
it('snippets.create persists multi-line run mode', async () => {
const updated: Snippet[][] = [];
const deps = createDeps({
snippets: [],
updateSnippets: (nextSnippets) => {
updated.push(applySnippetUpdate([], nextSnippets));
},
});
const result = await handleVaultAgentOp(
'snippets.create',
{
label: 'Login',
content: 'admin\npassword',
multiLineRunMode: 'lineDelay',
},
deps,
);
assert.equal(result.ok, true);
assert.equal(updated[0]?.[0]?.multiLineRunMode, 'lineDelay');
});
it('snippets.update persists multi-line run mode', async () => {
const updated: Snippet[][] = [];
const deps = createDeps({
snippets: [{ id: 'snippet-1', label: 'Login', command: 'admin\npassword', kind: 'snippet' }],
updateSnippets: (nextSnippets) => {
updated.push(applySnippetUpdate([], nextSnippets));
},
});
const result = await handleVaultAgentOp(
'snippets.update',
{
snippetId: 'snippet-1',
multiLineRunMode: 'lineDelay',
},
deps,
);
assert.equal(result.ok, true);
assert.equal(updated[0]?.[0]?.multiLineRunMode, 'lineDelay');
});
it('snippets.list returns kind and trigger fields', async () => {
const deps = createDeps({
snippets: [{
id: 's1',
label: 'Auto',
command: 'nct.log(1)',
kind: 'script',
trigger: 'onConnect',
targetsAllHosts: true,
}],
});
const result = await handleVaultAgentOp('snippets.list', {}, deps);
assert.equal(result.ok, true);
const listed = (result as { snippets?: Array<{ kind?: string; trigger?: string; targetsAllHosts?: boolean }> }).snippets;
assert.equal(listed?.[0]?.kind, 'script');
assert.equal(listed?.[0]?.trigger, 'onConnect');
assert.equal(listed?.[0]?.targetsAllHosts, true);
});
it('scripts.reference returns nct syntax reference', async () => {
const result = await handleVaultAgentOp('scripts.reference', {}, createDeps());
assert.equal(result.ok, true);
const reference = (result as { reference?: string }).reference ?? '';
assert.match(reference, /nct\.screen\.waitForPrompt/);
});
it('host.connectScripts.set updates host queue', async () => {
const host: Host = {
id: 'host-a',
label: 'A',
hostname: 'a.example',
username: 'root',
os: 'linux',
protocol: 'ssh',
tags: [],
};
const snippets: Snippet[] = [{
id: 'run',
label: 'Run',
command: 'nct.log(1)',
kind: 'script',
trigger: 'onConnect',
targets: ['host-a'],
}];
const deps = createDeps({ hosts: [host], snippets });
const result = await handleVaultAgentOp(
'host.connectScripts.set',
{ hostId: 'host-a', scriptIds: JSON.stringify(['run']) },
deps,
);
assert.equal(result.ok, true);
assert.deepEqual(deps.getHosts()[0]?.connectScriptIds, ['run']);
});
it('scripts.delete removes script from vault', async () => {
const snippets: Snippet[] = [{
id: 'run',
label: 'Run',
command: 'nct.log(1)',
kind: 'script',
}];
const updated: Snippet[][] = [];
const deps = createDeps({
snippets,
updateSnippets: (next) => {
updated.push(applySnippetUpdate(snippets, next));
},
});
const result = await handleVaultAgentOp('scripts.delete', { scriptId: 'run' }, deps);
assert.equal(result.ok, true);
assert.equal(updated[0]?.length, 0);
});
it('scripts.delete rejects text snippets', async () => {
const deps = createDeps({
snippets: [{ id: 'cmd', label: 'Cmd', command: 'ls', kind: 'snippet' }],
});
const result = await handleVaultAgentOp('scripts.delete', { scriptId: 'cmd' }, deps);
assert.equal(result.ok, false);
});
});
describe('runSerializedVaultAgentRequest', () => {
it('runs concurrent tasks sequentially without losing updates', async () => {
let value = 0;
const results = await Promise.all([
runSerializedVaultAgentRequest(async () => {
const snapshot = value;
await new Promise((resolve) => setTimeout(resolve, 5));
value = snapshot + 1;
return value;
}),
runSerializedVaultAgentRequest(async () => {
const snapshot = value;
await new Promise((resolve) => setTimeout(resolve, 1));
value = snapshot + 1;
return value;
}),
]);
assert.deepEqual(results, [1, 2]);
assert.equal(value, 2);
});
});