@dzhechkov/cloudru-hub 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dz-manifest.json +62 -14
- package/CHANGELOG.md +78 -0
- package/README.md +43 -22
- package/bin/cloudru-hub.js +0 -0
- package/data/tools-classification.json +9 -9
- package/data/tools-list-snapshot.json +6 -6
- package/docs/HERMES-CODEX.md +34 -0
- package/docs/LOCAL-TESTING.md +14 -22
- package/package.json +12 -9
- package/sbom.json +133 -13
- package/src/cli.js +64 -15
- package/src/credentials.js +61 -0
- package/src/deploy-approval.js +120 -0
- package/src/install.js +246 -27
- package/src/k8s-approval.js +208 -0
- package/src/probe-child.js +38 -0
- package/src/safe-mcp.js +268 -0
- package/src/toolsets.js +58 -0
- package/templates/.claude/hooks/cloudru-ssh-guard.cjs +0 -0
- package/test/billing-credentials.test.mjs +113 -0
- package/test/classification.test.mjs +23 -32
- package/test/deploy-approval.test.mjs +154 -0
- package/test/dialects.test.mjs +11 -1
- package/test/install.test.mjs +235 -11
- package/test/k8s-approval.test.mjs +125 -0
- package/test/mutation-registry-freshness.test.mjs +39 -0
- package/test/mutation-registry.json +11 -9
- package/test/resolve.test.mjs +46 -4
- package/test/safe-mcp.test.mjs +162 -0
|
@@ -46,10 +46,12 @@
|
|
|
46
46
|
"property": "Post-grant (ADR-001 AM-2) the pack must REMAIN publishable — a stray private:true would silently kill every future publish while CI stays green, so the publishable-state assertion must go red on it. (The Grant-Confirmation URL protection itself lives in LICENSE, which the gate cannot mutate — extensionless file, no parser; its layer-1 coverage is the pack-holds LICENSE assertions plus the dz guard licence-hold rule, both reading LICENSE directly on every run/publish.)",
|
|
47
47
|
"file": "package.json",
|
|
48
48
|
"mutation": {
|
|
49
|
-
"find": " \"
|
|
50
|
-
"replace": " \"
|
|
49
|
+
"find": " \"publishConfig\": {",
|
|
50
|
+
"replace": " \"private\": true,\n \"publishConfig\": {"
|
|
51
51
|
},
|
|
52
|
-
"minFailing": 1
|
|
52
|
+
"minFailing": 1,
|
|
53
|
+
"observed": 1,
|
|
54
|
+
"maxFailing": 1
|
|
53
55
|
},
|
|
54
56
|
{
|
|
55
57
|
"id": "licence-block-guard-trigger-declared",
|
|
@@ -72,18 +74,18 @@
|
|
|
72
74
|
"minFailing": 1
|
|
73
75
|
},
|
|
74
76
|
{
|
|
75
|
-
"id": "
|
|
76
|
-
"property": "
|
|
77
|
+
"id": "stack-status-remains-observational",
|
|
78
|
+
"property": "After the engine fix, stack_status reads persisted job state without advancing steps; stack_apply remains the explicitly gated mutation. Reclassifying status as ask loses the read-only contract and must fail the golden test.",
|
|
77
79
|
"file": "data/tools-classification.json",
|
|
78
80
|
"mutation": {
|
|
79
|
-
"find": "\"stack_status\": {\n \"effect\": \"
|
|
80
|
-
"replace": "\"stack_status\": {\n \"effect\": \"
|
|
81
|
+
"find": "\"stack_status\": {\n \"effect\": \"read-only\",\n \"permission\": \"allow\",",
|
|
82
|
+
"replace": "\"stack_status\": {\n \"effect\": \"read-only\",\n \"permission\": \"ask\","
|
|
81
83
|
},
|
|
82
|
-
"minFailing":
|
|
84
|
+
"minFailing": 1
|
|
83
85
|
},
|
|
84
86
|
{
|
|
85
87
|
"id": "brake-logs-stays-ask",
|
|
86
|
-
"property": "
|
|
88
|
+
"property": "The pinned engine quotes logs.service before remote execution, but logs still reaches SSH/compose and can disclose sensitive output. Flipping its classification from ask to allow violates the reviewed shell-reach boundary.",
|
|
87
89
|
"file": "data/tools-classification.json",
|
|
88
90
|
"mutation": {
|
|
89
91
|
"find": "\"logs\": {\n \"effect\": \"shell-reach\",\n \"permission\": \"ask\",",
|
package/test/resolve.test.mjs
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
// Engine resolution tests — ADR-002: runtime resolution, sha256 pin, loud esbuild-style error.
|
|
2
|
-
import { test } from 'node:test';
|
|
2
|
+
import { after, test } from 'node:test';
|
|
3
3
|
import assert from 'node:assert/strict';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import crypto from 'node:crypto';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
8
9
|
import { createRequire } from 'node:module';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
9
11
|
|
|
10
12
|
const require = createRequire(import.meta.url);
|
|
11
13
|
const { resolveEngine, sha256File, platformKey } = require('../src/resolve.js');
|
|
12
14
|
const PKG = require('../package.json');
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
// Every scratch dir is removed at the end (after() + process exit) unless DZ_KEEP_TMP=1 —
|
|
17
|
+
// MEASURED 2026-09-20: 18 cloudru-resolve-* dirs per full sweep (backlog 7cb6f9b7).
|
|
18
|
+
const created = [];
|
|
19
|
+
const removeCreated = () => {
|
|
20
|
+
if (process.env.DZ_KEEP_TMP === '1') return;
|
|
21
|
+
for (const d of created.splice(0)) fs.rmSync(d, { recursive: true, force: true });
|
|
22
|
+
};
|
|
23
|
+
after(removeCreated);
|
|
24
|
+
process.on('exit', removeCreated);
|
|
25
|
+
const tmp = () => { const d = fs.mkdtempSync(path.join(os.tmpdir(), 'cloudru-resolve-')); created.push(d); return d; };
|
|
15
26
|
|
|
16
27
|
test('env CLOUDRU_VM_BIN wins and is hashed; a non-pinned file reports verified:false but resolves', () => {
|
|
17
28
|
const dir = tmp();
|
|
@@ -24,6 +35,37 @@ test('env CLOUDRU_VM_BIN wins and is hashed; a non-pinned file reports verified:
|
|
|
24
35
|
assert.equal(r.verified, false);
|
|
25
36
|
});
|
|
26
37
|
|
|
38
|
+
test('mcp refuses a non-pinned engine before executing it', () => {
|
|
39
|
+
const dir = tmp();
|
|
40
|
+
const marker = path.join(dir, 'executed');
|
|
41
|
+
const fake = path.join(dir, 'cloudru-vm');
|
|
42
|
+
fs.writeFileSync(fake, `#!/bin/sh\ntouch "${marker}"\n`);
|
|
43
|
+
fs.chmodSync(fake, 0o755);
|
|
44
|
+
const result = spawnSync(process.execPath, [path.join(path.dirname(fileURLToPath(import.meta.url)), '../bin/cloudru-hub.js'), 'mcp'], {
|
|
45
|
+
encoding: 'utf8', timeout: 5000,
|
|
46
|
+
env: { ...process.env, CLOUDRU_VM_BIN: fake, CLOUDRU_HUB_CONFIG: path.join(dir, 'none.json') },
|
|
47
|
+
});
|
|
48
|
+
assert.equal(result.status, 1);
|
|
49
|
+
assert.match(result.stderr, /does not match pinned .*refusing to launch/);
|
|
50
|
+
assert.equal(fs.existsSync(marker), false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('self-test stops on a non-pinned engine before version or MCP probe', () => {
|
|
54
|
+
const dir = tmp();
|
|
55
|
+
const marker = path.join(dir, 'executed');
|
|
56
|
+
const fake = path.join(dir, 'cloudru-vm');
|
|
57
|
+
fs.writeFileSync(fake, `#!/bin/sh\ntouch "${marker}"\n`);
|
|
58
|
+
fs.chmodSync(fake, 0o755);
|
|
59
|
+
const result = spawnSync(process.execPath, [path.join(path.dirname(fileURLToPath(import.meta.url)), '../bin/cloudru-hub.js'), 'self-test', '--json'], {
|
|
60
|
+
encoding: 'utf8', timeout: 5000,
|
|
61
|
+
env: { ...process.env, CLOUDRU_VM_BIN: fake, CLOUDRU_HUB_CONFIG: path.join(dir, 'none.json') },
|
|
62
|
+
});
|
|
63
|
+
assert.equal(result.status, 1);
|
|
64
|
+
const report = JSON.parse(result.stdout);
|
|
65
|
+
assert.deepEqual(report.steps.map((step) => [step.name, step.ok]), [['resolve', true], ['sha256', false]]);
|
|
66
|
+
assert.equal(fs.existsSync(marker), false);
|
|
67
|
+
});
|
|
68
|
+
|
|
27
69
|
test('a file matching the pinned baseline hash reports verified:true', () => {
|
|
28
70
|
// Only meaningful on linux-x64 (the single pinned platform, ADR-002 point 4).
|
|
29
71
|
const key = platformKey();
|
|
@@ -73,9 +115,9 @@ test('sha256File matches node crypto', () => {
|
|
|
73
115
|
assert.equal(sha256File(f), crypto.createHash('sha256').update('abc').digest('hex'));
|
|
74
116
|
});
|
|
75
117
|
|
|
76
|
-
test('the pinned linux-x64 hash is the
|
|
118
|
+
test('the pinned linux-x64 hash is the rebuilt private engine sha256', () => {
|
|
77
119
|
assert.equal(
|
|
78
120
|
PKG.cloudruHub.binaryHashes['linux-x64'],
|
|
79
|
-
'
|
|
121
|
+
'89c5073418927ee71ac1de381eaba5ab69d599d615edc51b868d876e712dbf10',
|
|
80
122
|
);
|
|
81
123
|
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { after, test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const cls = require('../data/tools-classification.json');
|
|
11
|
+
const { GROUPS, selectedToolNames } = require('../src/toolsets.js');
|
|
12
|
+
const pkg = path.resolve(import.meta.dirname, '..');
|
|
13
|
+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'cloudru-safe-mcp-'));
|
|
14
|
+
after(() => fs.rmSync(scratch, { recursive: true, force: true }));
|
|
15
|
+
|
|
16
|
+
const fake = path.join(scratch, 'fake-engine');
|
|
17
|
+
const log = path.join(scratch, 'forwarded.jsonl');
|
|
18
|
+
fs.writeFileSync(fake, `#!/usr/bin/env node
|
|
19
|
+
const fs=require('fs');let b='';process.stdin.setEncoding('utf8');process.stdin.on('data',x=>{b+=x;let i;while((i=b.indexOf('\\n'))>=0){const line=b.slice(0,i);b=b.slice(i+1);if(!line)continue;const m=JSON.parse(line);fs.appendFileSync(process.env.FAKE_LOG,JSON.stringify(m)+'\\n');if(!Object.hasOwn(m,'id'))continue;let result={};if(m.method==='initialize')result={protocolVersion:'2025-06-18',capabilities:{tools:{},resources:{}},serverInfo:{name:'fake',version:'1'}};else if(m.method==='tools/list')result={tools:[{name:'vm_list'},{name:'deploy'},{name:'secret_value'},{name:'unknown_new'}]};else if(m.method==='tools/call')result={content:[{type:'text',text:'executed '+m.params.name}]};process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:m.id,result})+'\\n');}});
|
|
20
|
+
`);
|
|
21
|
+
fs.chmodSync(fake, 0o755);
|
|
22
|
+
|
|
23
|
+
const msg = (id, method, params) => JSON.stringify({ jsonrpc: '2.0', id, method, ...(params ? { params } : {}) });
|
|
24
|
+
function run(lines, toolsets = 'all') {
|
|
25
|
+
fs.writeFileSync(log, '');
|
|
26
|
+
const script = `const {runSafeMcp}=require(${JSON.stringify(path.join(pkg, 'src/safe-mcp.js'))});runSafeMcp({toolsets:${JSON.stringify(toolsets)},resolved:{path:${JSON.stringify(fake)},verified:true,sha256:${JSON.stringify(cls.engineSha256)}}});`;
|
|
27
|
+
const r = spawnSync(process.execPath, ['-e', script], { input: lines.join('\n') + '\n', encoding: 'utf8', timeout: 5000, env: { ...process.env, FAKE_LOG: log } });
|
|
28
|
+
assert.equal(r.status, 0, r.stderr || r.error?.message);
|
|
29
|
+
return { responses: r.stdout.trim().split('\n').map(JSON.parse), forwarded: fs.readFileSync(log, 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test('safe proxy filters listed tools and blocks ask, deny, unknown and other methods before engine', () => {
|
|
33
|
+
const out = run([
|
|
34
|
+
msg(1, 'initialize', { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'test', version: '1' } }),
|
|
35
|
+
msg(2, 'tools/list'),
|
|
36
|
+
msg(3, 'tools/call', { name: 'deploy', arguments: { confirmed: true } }),
|
|
37
|
+
msg(4, 'tools/call', { name: 'secret_value', arguments: {} }),
|
|
38
|
+
msg(5, 'tools/call', { name: 'unknown_new', arguments: {} }),
|
|
39
|
+
msg(6, 'resources/read', { uri: 'file:///etc/passwd' }),
|
|
40
|
+
msg(7, 'tools/call', { name: 'vm_list', arguments: {} }),
|
|
41
|
+
]);
|
|
42
|
+
const byId = Object.fromEntries(out.responses.map((r) => [r.id, r]));
|
|
43
|
+
assert.deepEqual(byId[2].result.tools.map((t) => t.name), ['vm_list']);
|
|
44
|
+
assert.deepEqual(Object.keys(byId[1].result.capabilities), ['tools']);
|
|
45
|
+
for (const id of [3, 4, 5, 6]) assert.ok(byId[id].error, `request ${id} blocked`);
|
|
46
|
+
assert.match(byId[7].result.content[0].text, /executed vm_list/);
|
|
47
|
+
assert.deepEqual(out.forwarded.filter((m) => m.method === 'tools/call').map((m) => m.params.name), ['vm_list']);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('Hermes keepalive gets the filtered tool list while the serial engine is busy', async () => {
|
|
51
|
+
const serial = path.join(scratch, 'serial-engine');
|
|
52
|
+
const serialLog = path.join(scratch, 'serial-forwarded.jsonl');
|
|
53
|
+
fs.writeFileSync(serial, `#!/usr/bin/env node
|
|
54
|
+
const fs=require('fs');let buffer='',busy=false;const queue=[];
|
|
55
|
+
function next(){if(busy||!queue.length)return;busy=true;const m=queue.shift();fs.appendFileSync(process.env.FAKE_LOG,JSON.stringify(m)+'\\n');
|
|
56
|
+
const result=m.method==='tools/list'?{tools:[{name:'vm_list'},{name:'deploy'}]}:{content:[{type:'text',text:'done'}]};
|
|
57
|
+
const done=()=>{process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:m.id,result})+'\\n');busy=false;next();};
|
|
58
|
+
if(m.method==='tools/call')setTimeout(done,1500);else done();}
|
|
59
|
+
process.stdin.setEncoding('utf8');process.stdin.on('data',chunk=>{buffer+=chunk;let end;while((end=buffer.indexOf('\\n'))>=0){const line=buffer.slice(0,end);buffer=buffer.slice(end+1);if(line)queue.push(JSON.parse(line));}next();});
|
|
60
|
+
`);
|
|
61
|
+
fs.chmodSync(serial, 0o755);
|
|
62
|
+
fs.writeFileSync(serialLog, '');
|
|
63
|
+
const script = `const {runSafeMcp}=require(${JSON.stringify(path.join(pkg, 'src/safe-mcp.js'))});runSafeMcp({resolved:{path:${JSON.stringify(serial)},verified:true,sha256:${JSON.stringify(cls.engineSha256)}}});`;
|
|
64
|
+
const child = spawn(process.execPath, ['-e', script], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, FAKE_LOG: serialLog } });
|
|
65
|
+
const responses = new Map();
|
|
66
|
+
const listeners = new Map();
|
|
67
|
+
let buffer = '';
|
|
68
|
+
child.stdout.on('data', (chunk) => {
|
|
69
|
+
buffer += chunk;
|
|
70
|
+
let end;
|
|
71
|
+
while ((end = buffer.indexOf('\n')) >= 0) {
|
|
72
|
+
const line = buffer.slice(0, end);
|
|
73
|
+
buffer = buffer.slice(end + 1);
|
|
74
|
+
if (!line) continue;
|
|
75
|
+
const response = JSON.parse(line);
|
|
76
|
+
responses.set(response.id, response);
|
|
77
|
+
listeners.get(response.id)?.(response);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
const waitFor = (id, timeoutMs) => new Promise((resolve, reject) => {
|
|
81
|
+
if (responses.has(id)) return resolve(responses.get(id));
|
|
82
|
+
const timer = setTimeout(() => reject(new Error(`MCP response ${id} timed out`)), timeoutMs);
|
|
83
|
+
listeners.set(id, (response) => { clearTimeout(timer); listeners.delete(id); resolve(response); });
|
|
84
|
+
});
|
|
85
|
+
try {
|
|
86
|
+
child.stdin.write(msg(1, 'tools/list') + '\n');
|
|
87
|
+
assert.deepEqual((await waitFor(1, 2000)).result.tools.map((t) => t.name), ['vm_list']);
|
|
88
|
+
child.stdin.write(msg(2, 'tools/call', { name: 'vm_list', arguments: {} }) + '\n');
|
|
89
|
+
child.stdin.write(msg(3, 'tools/list') + '\n');
|
|
90
|
+
assert.deepEqual((await waitFor(3, 1000)).result.tools.map((t) => t.name), ['vm_list']);
|
|
91
|
+
assert.equal((await waitFor(2, 2500)).result.content[0].text, 'done');
|
|
92
|
+
child.stdin.end();
|
|
93
|
+
assert.equal(await new Promise((resolve) => child.on('exit', resolve)), 0);
|
|
94
|
+
const forwarded = fs.readFileSync(serialLog, 'utf8').trim().split('\n').map(JSON.parse);
|
|
95
|
+
assert.deepEqual(forwarded.map((m) => m.method), ['tools/list', 'tools/call']);
|
|
96
|
+
} finally {
|
|
97
|
+
child.kill();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('Go case-insensitive JSON aliases cannot change the validated method or tool name', () => {
|
|
102
|
+
const out = run([
|
|
103
|
+
JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', Method: 'tools/call', params: { name: 'vm_list', Name: 'deploy', arguments: {} }, Params: { name: 'deploy', arguments: { confirmed: true } } }),
|
|
104
|
+
JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'ping', Method: 'tools/call', Params: { name: 'deploy', arguments: { confirmed: true } } }),
|
|
105
|
+
JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'deploy' }, Params: { name: 'vm_list' } }),
|
|
106
|
+
JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized', Method: 'tools/call', Params: { name: 'deploy' } }),
|
|
107
|
+
'{"jsonrpc":"2.0","id":4,"method":"tools/call","method":"resources/read","params":{"name":"vm_list"}}',
|
|
108
|
+
]);
|
|
109
|
+
assert.equal(out.responses.find((r) => r.id === 1).result.content[0].text, 'executed vm_list');
|
|
110
|
+
assert.ok(out.responses.find((r) => r.id === 3).error);
|
|
111
|
+
assert.ok(out.responses.find((r) => r.id === 4).error);
|
|
112
|
+
assert.deepEqual(out.forwarded, [
|
|
113
|
+
{ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'vm_list', arguments: {} } },
|
|
114
|
+
{ jsonrpc: '2.0', id: 2, method: 'ping' },
|
|
115
|
+
{ jsonrpc: '2.0', method: 'notifications/initialized' },
|
|
116
|
+
]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('a failed backend closes the proxy and resolves pending requests without host EOF', async () => {
|
|
120
|
+
const crashing = path.join(scratch, 'crashing-engine');
|
|
121
|
+
fs.writeFileSync(crashing, '#!/usr/bin/env node\nprocess.stdin.once("data", () => process.exit(2));\n');
|
|
122
|
+
fs.chmodSync(crashing, 0o755);
|
|
123
|
+
const script = `const {runSafeMcp}=require(${JSON.stringify(path.join(pkg, 'src/safe-mcp.js'))});runSafeMcp({resolved:{path:${JSON.stringify(crashing)},verified:true,sha256:${JSON.stringify(cls.engineSha256)}}});`;
|
|
124
|
+
const child = spawn(process.execPath, ['-e', script], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
125
|
+
let output = '';
|
|
126
|
+
child.stdout.on('data', (chunk) => { output += chunk; });
|
|
127
|
+
child.stdin.write(msg(1, 'tools/list') + '\n');
|
|
128
|
+
const timer = setTimeout(() => child.kill(), 2000);
|
|
129
|
+
const code = await new Promise((resolve) => child.on('exit', resolve));
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
assert.equal(code, 1);
|
|
132
|
+
assert.match(output, /Cloud.ru engine connection closed/);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('safe proxy rejects malformed JSON and batch requests', () => {
|
|
136
|
+
const out = run(['not-json', JSON.stringify([{ jsonrpc: '2.0', id: 1, method: 'tools/list' }])]);
|
|
137
|
+
assert.equal(out.responses.length, 2);
|
|
138
|
+
assert.ok(out.responses.every((r) => r.error));
|
|
139
|
+
assert.equal(out.forwarded.length, 0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('production command refuses an engine with a different hash', () => {
|
|
143
|
+
const r = spawnSync(process.execPath, [path.join(pkg, 'bin/cloudru-hub.js'), 'mcp-safe'], {
|
|
144
|
+
input: msg(1, 'tools/list') + '\n', encoding: 'utf8', timeout: 5000,
|
|
145
|
+
env: { ...process.env, CLOUDRU_VM_BIN: fake },
|
|
146
|
+
});
|
|
147
|
+
assert.equal(r.status, 1);
|
|
148
|
+
assert.match(r.stderr, /pinned engine SHA-256 required/);
|
|
149
|
+
assert.equal(r.stdout, '');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('named toolsets reduce the visible surface and each member is allow-classified', () => {
|
|
153
|
+
for (const group of Object.keys(GROUPS)) {
|
|
154
|
+
const names = selectedToolNames(group);
|
|
155
|
+
assert.ok(names.length > 0);
|
|
156
|
+
for (const name of names) assert.equal(cls.tools[name].permission, 'allow');
|
|
157
|
+
}
|
|
158
|
+
const out = run([msg(1, 'tools/list'), msg(2, 'tools/call', { name: 'deploy', arguments: { confirmed: true } })], 'inventory');
|
|
159
|
+
assert.deepEqual(out.responses.find((r) => r.id === 1).result.tools.map((x) => x.name), ['vm_list']);
|
|
160
|
+
assert.equal(out.forwarded.filter((m) => m.method === 'tools/call').length, 0);
|
|
161
|
+
assert.throws(() => selectedToolNames('inventory,all'), /invalid toolsets/);
|
|
162
|
+
});
|