@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.
@@ -0,0 +1,154 @@
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 { spawnSync } from 'node:child_process';
7
+ import { createRequire } from 'node:module';
8
+
9
+ const require = createRequire(import.meta.url);
10
+ const { readCredentials } = require('../src/credentials.js');
11
+ const { issueApproval } = require('../src/deploy-approval.js');
12
+ const cls = require('../data/tools-classification.json');
13
+ const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'cloudru-deploy-guard-'));
14
+ after(() => fs.rmSync(scratch, { recursive: true, force: true }));
15
+ const projectId = '11111111-2222-3333-4444-555555555555';
16
+ const vmId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee';
17
+ const otherVM = 'bbbbbbbb-cccc-dddd-eeee-ffffffffffff';
18
+ const planId = 'plan-123456abcdef';
19
+ const file = path.join(scratch, 'credentials.json');
20
+ const planFile = path.join(scratch, '.cloudru-vm', 'plans', `${planId}.json`);
21
+ const log = path.join(scratch, 'forwarded.jsonl');
22
+ const fake = path.join(scratch, 'fake-engine');
23
+ const env = { ...process.env, CLOUDRU_HUB_CREDENTIALS: file, CLOUDRU_HUB_APPROVALS: path.join(scratch, 'approvals'), CLOUDRU_VM_HOME: scratch, FAKE_LOG: log };
24
+ fs.writeFileSync(file, JSON.stringify({ key_id: 'test-key', secret: 'test-secret', project_id: projectId, region: 'ru.AZ-2' }), { mode: 0o600 });
25
+ fs.mkdirSync(path.dirname(planFile), { recursive: true });
26
+ const plan = { plan_id: planId, project_id: projectId, expires_at: new Date(Date.now() + 1800000).toISOString(), vm: { name: 'snake-pilot' }, ssh: { login: 'user1' }, cost: { currency: 'RUB', total_monthly_rub: 42 } };
27
+ const savePlan = () => fs.writeFileSync(planFile, JSON.stringify(plan));
28
+ savePlan();
29
+ fs.writeFileSync(fake, `#!/usr/bin/env node
30
+ 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==='tools/list')result={tools:[{name:'deploy_apply'},{name:'vm_exec'},{name:'destroy'}]};else if(m.method==='tools/call'){const deployment=process.env.FAKE_DEPLOY_OUTCOME?JSON.parse(process.env.FAKE_DEPLOY_OUTCOME):{vm_id:'${vmId}',status:'running',public_ip:'203.0.113.10',password:'generated-secret'};result={isError:m.params.name==='deploy_apply'&&!!process.env.FAKE_DEPLOY_ERROR,content:[{type:'text',text:m.params.name==='deploy_apply'?JSON.stringify(deployment):JSON.stringify({status:'ok'})}]};}process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:m.id,result})+'\\n');}});
31
+ `);
32
+ fs.chmodSync(fake, 0o755);
33
+ const rpc = (id, name, args = {}) => JSON.stringify({ jsonrpc: '2.0', id, method: 'tools/call', params: { name, arguments: args } });
34
+ function run(lines, overrides = {}) {
35
+ fs.writeFileSync(log, '');
36
+ const script = `const {runSafeMcp}=require(${JSON.stringify(path.resolve(import.meta.dirname, '../src/safe-mcp.js'))});runSafeMcp({toolsets:'vm-deploy',resolved:{path:${JSON.stringify(fake)},verified:true,sha256:${JSON.stringify(cls.engineSha256)}}});`;
37
+ const r = spawnSync(process.execPath, ['-e', script], { input: lines.join('\n') + '\n', encoding: 'utf8', timeout: 5000, env: { ...env, ...overrides } });
38
+ assert.equal(r.status, 0, r.stderr || r.error?.message);
39
+ return { responses: r.stdout.trim().split('\n').map(JSON.parse), forwarded: fs.readFileSync(log, 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse) };
40
+ }
41
+
42
+ test('credentials require owner-only regular file and do not interpolate shell text', () => {
43
+ assert.equal(readCredentials(env).projectId, projectId);
44
+ assert.equal(readCredentials(env).env.CLOUDRU_KEY_ID, 'test-key');
45
+ fs.chmodSync(file, 0o644);
46
+ assert.throws(() => readCredentials(env), /0600/);
47
+ fs.chmodSync(file, 0o600);
48
+ fs.writeFileSync(file, '{"secret":THIS_IS_A_FAKE_SECRET_FOR_QE}', { mode: 0o600 });
49
+ assert.throws(() => readCredentials(env), (error) => error.message === 'credentials file is not valid JSON');
50
+ fs.writeFileSync(file, JSON.stringify({ key_id: 'test-key', secret: 'test-secret', project_id: projectId, region: 'ru.AZ-2' }), { mode: 0o600 });
51
+ });
52
+
53
+ test('cost cap and project must match the persisted plan before approval', () => {
54
+ assert.throws(() => issueApproval(planId, 41, env), /cost exceeds/);
55
+ plan.project_id = otherVM;
56
+ savePlan();
57
+ assert.throws(() => issueApproval(planId, 100, env), /project differs/);
58
+ plan.project_id = projectId;
59
+ savePlan();
60
+ });
61
+
62
+ test('approved plan is single-use; remote commands are confined to its VM', () => {
63
+ issueApproval(planId, 50, env);
64
+ const out = run([
65
+ JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }),
66
+ rpc(2, 'deploy_apply', { plan_id: planId, confirmed: false }),
67
+ rpc(3, 'deploy_apply', { plan_id: planId, confirmed: true }),
68
+ ]);
69
+ const followup = run([
70
+ rpc(4, 'vm_exec', { vm: otherVM, command: 'id' }),
71
+ rpc(5, 'vm_exec', { vm: vmId, user: 'root', command: 'whoami' }),
72
+ rpc(6, 'destroy', { confirmed: true }),
73
+ ]);
74
+ const by = Object.fromEntries([...out.responses, ...followup.responses].map((x) => [x.id, x]));
75
+ assert.deepEqual(by[1].result.tools.map((x) => x.name), ['deploy_apply', 'vm_exec']);
76
+ assert.equal(by[2].result.isError, false);
77
+ assert.doesNotMatch(JSON.stringify(by[2]), /generated-secret/);
78
+ for (const id of [3, 4, 6]) assert.ok(by[id].error, `request ${id} blocked`);
79
+ assert.equal(by[5].result.content[0].text, JSON.stringify({ status: 'ok' }));
80
+ const calls = [...out.forwarded, ...followup.forwarded].filter((x) => x.method === 'tools/call');
81
+ assert.deepEqual(calls.map((x) => x.params), [
82
+ { name: 'deploy_apply', arguments: { plan_id: planId, confirmed: true } },
83
+ { name: 'vm_exec', arguments: { vm: vmId, user: 'user1', command: 'whoami' } },
84
+ ]);
85
+ });
86
+
87
+ test('a changed plan invalidates the exact approval', () => {
88
+ const second = 'plan-abcdef123456';
89
+ const secondFile = path.join(path.dirname(planFile), `${second}.json`);
90
+ fs.writeFileSync(secondFile, JSON.stringify({ ...plan, plan_id: second }));
91
+ issueApproval(second, 50, env);
92
+ fs.writeFileSync(secondFile, JSON.stringify({ ...plan, plan_id: second, cost: { currency: 'RUB', total_monthly_rub: 49 } }));
93
+ const out = run([rpc(1, 'deploy_apply', { plan_id: second, confirmed: true })]);
94
+ assert.match(out.responses[0].error.message, /approval does not match/);
95
+ assert.equal(out.forwarded.length, 0);
96
+ });
97
+
98
+ test('partial deploy preserves its VM UUID for recovery and redacts nested secrets', () => {
99
+ const third = 'plan-123456fedcba';
100
+ fs.writeFileSync(path.join(path.dirname(planFile), `${third}.json`), JSON.stringify({ ...plan, plan_id: third }));
101
+ issueApproval(third, 50, env);
102
+ const outcome = { status: 'state_persistence_failed', result: { vm_id: otherVM, status: 'running', password: 'generated-secret' }, error: 'local state write failed' };
103
+ const out = run([rpc(1, 'deploy_apply', { plan_id: third })], { FAKE_DEPLOY_OUTCOME: JSON.stringify(outcome), FAKE_DEPLOY_ERROR: '1' });
104
+ assert.equal(out.responses[0].result.isError, true);
105
+ const returned = JSON.parse(out.responses[0].result.content[0].text);
106
+ assert.equal(returned.result.vm_id, otherVM);
107
+ assert.equal(returned.result.password, undefined);
108
+ assert.match(returned.recovery, /Inspect/);
109
+ const denied = run([rpc(2, 'vm_exec', { vm: otherVM, command: 'id' })]);
110
+ assert.ok(denied.responses[0].error);
111
+ });
112
+
113
+ test('static site requires the reviewed digest and never grants an SSH command session', () => {
114
+ const id = 'plan-aabbccddeeff';
115
+ const digest = 'a'.repeat(64);
116
+ const sitePlan = { ...plan, plan_id: id, ports: [80], network: { floating_ip: true, subnet_id: 'subnet-a', attach_fip_id: otherVM },
117
+ static_site: { source: 'fixtures/snake/index.html', sha256: digest, size_bytes: 12, source_base64: Buffer.from('hello snake!').toString('base64'),
118
+ files: [{ path: 'index.html', sha256: digest, size_bytes: 12, content_base64: Buffer.from('hello snake!').toString('base64') }] } };
119
+ const siteFile = path.join(path.dirname(planFile), `${id}.json`);
120
+ fs.writeFileSync(siteFile, JSON.stringify(sitePlan));
121
+ assert.throws(() => issueApproval(id, 50, env), /site-sha256/);
122
+ assert.throws(() => issueApproval(id, 50, env, Date.now(), 'b'.repeat(64)), /site-sha256/);
123
+ const approved = issueApproval(id, 50, env, Date.now(), digest);
124
+ assert.equal(approved.staticSite.sha256, digest);
125
+ assert.match(approved.staticSite.ingress, /dedicated security group/);
126
+ assert.match(approved.staticSite.estimateScope, /public IP charges excluded/);
127
+ const outcome = { vm_id: otherVM, status: 'running', public_ip: '203.0.113.10', http_verification: { verified: true } };
128
+ const out = run([rpc(1, 'deploy_apply', { plan_id: id })], { FAKE_DEPLOY_OUTCOME: JSON.stringify(outcome) });
129
+ assert.equal(out.responses[0].result.isError, false);
130
+ const denied = run([rpc(2, 'vm_exec', { vm: otherVM, command: 'id' })]);
131
+ assert.ok(denied.responses[0].error);
132
+ });
133
+
134
+ test('static-site artifact and same-price network changes invalidate approval', () => {
135
+ const site = { source: 'snake.zip', sha256: 'c'.repeat(64), size_bytes: 20, source_base64: Buffer.from('test').toString('base64'),
136
+ files: [{ path: 'index.html', sha256: 'd'.repeat(64), size_bytes: 10, content_base64: 'aGVsbG8=' }] };
137
+ for (const [suffix, mutation] of [
138
+ ['010101010101', (p) => { p.static_site.sha256 = 'e'.repeat(64); }],
139
+ ['020202020202', (p) => { p.network.subnet_id = 'subnet-b'; }],
140
+ ['030303030303', (p) => { p.vm.zone = { id: 'zone-b', name: 'AZ-2' }; }],
141
+ ]) {
142
+ const id = `plan-${suffix}`;
143
+ const original = { ...plan, plan_id: id, ports: [80], network: { floating_ip: true, subnet_id: 'subnet-a', attach_fip_id: otherVM },
144
+ vm: { ...plan.vm, zone: { id: 'zone-a', name: 'AZ-1' } }, static_site: structuredClone(site) };
145
+ const filename = path.join(path.dirname(planFile), `${id}.json`);
146
+ fs.writeFileSync(filename, JSON.stringify(original));
147
+ issueApproval(id, 50, env, Date.now(), site.sha256);
148
+ mutation(original);
149
+ fs.writeFileSync(filename, JSON.stringify(original));
150
+ const out = run([rpc(1, 'deploy_apply', { plan_id: id })]);
151
+ assert.match(out.responses[0].error.message, /approval does not match/);
152
+ assert.equal(out.forwarded.length, 0);
153
+ }
154
+ });
@@ -1,7 +1,7 @@
1
1
  // Skill dialect compiler tests — ADR-006 Confirmation, all layer 1: size, links, dialect
2
2
  // grep, determinism. Uses a SYNTHETIC fixture (our own text carrying the Hermes-dialect
3
3
  // tokens) — the real canonical corpus is upstream content and never ships in this package.
4
- import { test } from 'node:test';
4
+ import { after, test } from 'node:test';
5
5
  import assert from 'node:assert/strict';
6
6
  import fs from 'node:fs';
7
7
  import os from 'node:os';
@@ -11,8 +11,18 @@ import { createRequire } from 'node:module';
11
11
  const require = createRequire(import.meta.url);
12
12
  const { compileSkill, loadDialects, filterFrontmatter } = require('../src/dialects.js');
13
13
 
14
+ // Every scratch dir is removed at the end (after() + process exit) unless DZ_KEEP_TMP=1 —
15
+ // MEASURED 2026-09-20: 81 cloudru-dialects-* dirs per full sweep (backlog 7cb6f9b7).
16
+ const created = [];
17
+ const removeCreated = () => {
18
+ if (process.env.DZ_KEEP_TMP === '1') return;
19
+ for (const d of created.splice(0)) fs.rmSync(d, { recursive: true, force: true });
20
+ };
21
+ after(removeCreated);
22
+ process.on('exit', removeCreated);
14
23
  function fixture() {
15
24
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cloudru-dialects-'));
25
+ created.push(dir);
16
26
  fs.writeFileSync(path.join(dir, 'SKILL.md'), `---
17
27
  name: cloudru-hub
18
28
  description: Управление Cloud.ru Evolution.
@@ -1,5 +1,5 @@
1
1
  // Installer tests — ADR-005 tiering + ADR-004 brake emission with EXECUTED veto.
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';
@@ -7,10 +7,21 @@ import path from 'node:path';
7
7
  import { createRequire } from 'node:module';
8
8
 
9
9
  const require = createRequire(import.meta.url);
10
+ const YAML = require('yaml');
10
11
  const { install, mergeSettings, claudeSettingsFragment, probeBrake, HOOK_TEMPLATE, TIERS } = require('../src/install.js');
11
12
  const { permissionRules } = require('../src/classification.js');
13
+ const pinnedEngine = { path: '/verified/cloudru-vm', verified: true, sha256: require('../data/tools-classification.json').engineSha256 };
12
14
 
13
- const tmp = () => fs.mkdtempSync(path.join(os.tmpdir(), 'cloudru-hub-test-'));
15
+ // Every scratch dir is removed at the end (after() + process exit) unless DZ_KEEP_TMP=1 —
16
+ // MEASURED 2026-09-20: 42 cloudru-hub-test-* dirs per full sweep (backlog 7cb6f9b7).
17
+ const created = [];
18
+ const removeCreated = () => {
19
+ if (process.env.DZ_KEEP_TMP === '1') return;
20
+ for (const d of created.splice(0)) fs.rmSync(d, { recursive: true, force: true });
21
+ };
22
+ after(removeCreated);
23
+ process.on('exit', removeCreated);
24
+ const tmp = () => { const d = fs.mkdtempSync(path.join(os.tmpdir(), 'cloudru-hub-test-')); created.push(d); return d; };
14
25
 
15
26
  test('claude-code install emits .mcp.json + permission brake + hook, and the veto is EXECUTED', () => {
16
27
  const dir = tmp();
@@ -80,7 +91,7 @@ test('install is idempotent — second run does not duplicate rules or hooks', (
80
91
  });
81
92
 
82
93
  test('DEGRADED targets are refused with the ADR-005 reason — never a fake install', () => {
83
- for (const target of ['openclaude', 'opencode', 'cursor', 'windsurf', 'copilot']) {
94
+ for (const target of ['openclaude', 'cursor', 'windsurf', 'copilot']) {
84
95
  const dir = tmp();
85
96
  const res = install(target, dir);
86
97
  assert.equal(res.code, 2, target);
@@ -104,22 +115,235 @@ test('POINTER-ONLY targets get the ≤2048-char pointer with the mandatory no-ve
104
115
  }
105
116
  });
106
117
 
107
- test('codex is plan-only: prints the user-global steps + mandatory live probe, applies nothing', () => {
118
+ test('codex installs a project-scoped safe server and preserves unrelated TOML', () => {
108
119
  const dir = tmp();
120
+ fs.mkdirSync(path.join(dir, '.codex'));
121
+ fs.writeFileSync(path.join(dir, '.codex', 'config.toml'), 'model = "other"\n');
109
122
  const res = install('codex', dir);
110
- assert.equal(res.code, 3);
111
- const out = res.lines.join('\n');
112
- assert.ok(out.includes('codex mcp add'));
113
- assert.ok(out.includes('~/.codex/hooks.json'));
114
- assert.ok(out.includes('MANDATORY probe'));
115
- assert.equal(fs.readdirSync(dir).length, 0, 'nothing written');
123
+ assert.equal(res.code, 0, res.lines.join('\n'));
124
+ const out = fs.readFileSync(path.join(dir, '.codex', 'config.toml'), 'utf8');
125
+ assert.match(out, /model = "other"/);
126
+ assert.match(out, /\[mcp_servers\.cloudru_hub\]/);
127
+ assert.match(out, /mcp-safe/);
128
+ assert.match(out, /enabled_tools = .*"vm_list"/);
129
+ assert.match(out, /--toolsets","inventory"/);
130
+ assert.doesNotMatch(out, /"deploy"/);
131
+ assert.match(out, /default_tools_approval_mode = "prompt"/);
132
+ assert.equal(install('codex', dir).code, 0, 'same install is idempotent');
133
+ });
134
+
135
+ test('OpenCode and Pi install a safe MCP server without replacing unrelated JSON', () => {
136
+ for (const target of ['opencode', 'pi']) {
137
+ const dir = tmp();
138
+ const file = path.join(dir, target === 'pi' ? '.pi/mcp.json' : 'opencode.json');
139
+ fs.mkdirSync(path.dirname(file), { recursive: true });
140
+ fs.writeFileSync(file, JSON.stringify(target === 'pi' ? { mcpServers: { other: { command: 'test' } }, theme: 'x' } : { mcp: { other: { type: 'remote', url: 'https://example.com/mcp' } }, theme: 'x' }));
141
+ assert.equal(install(target, dir).code, 0);
142
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
143
+ const servers = target === 'pi' ? data.mcpServers : data.mcp;
144
+ assert.ok(servers.other);
145
+ assert.equal(data.theme, 'x');
146
+ assert.match(JSON.stringify(servers.cloudru_hub), /mcp-safe/);
147
+ assert.equal(install(target, dir).code, 0);
148
+ }
149
+ });
150
+
151
+ test('installers refuse a competing raw Cloud.ru server', () => {
152
+ for (const target of ['codex', 'opencode', 'pi']) {
153
+ const dir = tmp();
154
+ if (target === 'codex') {
155
+ fs.mkdirSync(path.join(dir, '.codex'));
156
+ fs.writeFileSync(path.join(dir, '.codex/config.toml'), '[mcp_servers.cloudru-vm]\ncommand = "raw"\n');
157
+ } else {
158
+ const file = path.join(dir, target === 'pi' ? '.pi/mcp.json' : 'opencode.json');
159
+ fs.mkdirSync(path.dirname(file), { recursive: true });
160
+ fs.writeFileSync(file, JSON.stringify(target === 'pi' ? { mcpServers: { 'cloudru-vm': {} } } : { mcp: { 'cloudru-vm': {} } }));
161
+ }
162
+ assert.equal(install(target, dir).code, 2, target);
163
+ }
164
+ });
165
+
166
+ test('Codex refuses quoted and inline TOML MCP declarations before changing the file', () => {
167
+ const variants = [
168
+ '[mcp_servers."cloudru-vm"]\ncommand = "raw"\n',
169
+ '[mcp_servers."cloudru_hub"]\ncommand = "raw"\n',
170
+ 'mcp_servers = { cloudru_hub = { command = "raw" } }\n',
171
+ '["\\u006dcp_servers"."cloudru-vm"]\ncommand = "raw"\n',
172
+ ];
173
+ for (const source of variants) {
174
+ const dir = tmp();
175
+ const file = path.join(dir, '.codex/config.toml');
176
+ fs.mkdirSync(path.dirname(file));
177
+ fs.writeFileSync(file, source);
178
+ const res = install('codex', dir);
179
+ assert.equal(res.code, 2, source);
180
+ assert.equal(fs.readFileSync(file, 'utf8'), source);
181
+ }
182
+ });
183
+
184
+ test('Hermes installs a guarded nonempty include list without changing unrelated YAML', () => {
185
+ const dir = tmp();
186
+ const file = path.join(dir, 'config.yaml');
187
+ const source = 'plugins:\n enabled: true\nmcp_servers:\n github:\n command: "other"\nagent:\n model: "test"\n';
188
+ fs.writeFileSync(file, source);
189
+ const res = install('hermes', dir, { hermesHome: dir, resolvedEngine: pinnedEngine });
190
+ assert.equal(res.code, 0, res.lines.join('\n'));
191
+ const out = fs.readFileSync(file, 'utf8');
192
+ assert.deepEqual(Object.keys(YAML.parse(out).mcp_servers).sort(), ['cloudru_hub', 'github']);
193
+ assert.match(out, /mcp-safe/);
194
+ assert.match(out, /vm_list/);
195
+ assert.match(out, /--toolsets/);
196
+ assert.match(out, /include: \[/);
197
+ assert.equal(YAML.parse(out).mcp_servers.cloudru_hub.env.CLOUDRU_VM_BIN, pinnedEngine.path);
198
+ assert.doesNotMatch(out, /"deploy"/);
199
+ assert.match(out, /plugins:\n enabled: true/);
200
+ assert.match(out, / github:\n command: "other"/);
201
+ assert.match(out, /agent:\n model: "test"/);
202
+ assert.equal(install('hermes', dir, { hermesHome: dir, resolvedEngine: pinnedEngine }).code, 0);
203
+ assert.equal(fs.readFileSync(file, 'utf8'), out, 'second install is idempotent');
204
+ });
205
+
206
+ test('Hermes keeps valid comments and refuses a flow-root document', () => {
207
+ const commented = tmp();
208
+ const file = path.join(commented, 'config.yaml');
209
+ fs.writeFileSync(file, 'mcp_servers:\n other:\n command: harmless\n # explanatory comment\n');
210
+ assert.equal(install('hermes', commented, { hermesHome: commented, resolvedEngine: pinnedEngine }).code, 0);
211
+ assert.equal(YAML.parse(fs.readFileSync(file, 'utf8')).mcp_servers.other.command, 'harmless');
212
+ assert.equal(YAML.parse(fs.readFileSync(file, 'utf8')).mcp_servers.cloudru_hub.tools.include.length, 11);
213
+
214
+ const flow = tmp();
215
+ const flowFile = path.join(flow, 'config.yaml');
216
+ fs.writeFileSync(flowFile, '{ model: harmless }\n');
217
+ assert.equal(install('hermes', flow, { hermesHome: flow }).code, 2);
218
+ assert.equal(fs.readFileSync(flowFile, 'utf8'), '{ model: harmless }\n');
219
+ });
220
+
221
+ test('Hermes refuses missing or hash-mismatched engine without writing config', () => {
222
+ for (const resolvedEngine of [
223
+ { error: 'engine absent' },
224
+ { ...pinnedEngine, sha256: '0'.repeat(64) },
225
+ { ...pinnedEngine, verified: false },
226
+ ]) {
227
+ const dir = tmp();
228
+ const file = path.join(dir, 'config.yaml');
229
+ fs.writeFileSync(file, 'agent:\n model: test\n');
230
+ const res = install('hermes', dir, { hermesHome: dir, resolvedEngine });
231
+ assert.equal(res.code, 2);
232
+ assert.equal(fs.readFileSync(file, 'utf8'), 'agent:\n model: test\n');
233
+ }
234
+ });
235
+
236
+ test('Hermes refuses raw Cloud.ru entries and complex YAML without writing', () => {
237
+ for (const source of [
238
+ 'mcp_servers:\n cloudru-vm:\n command: raw\n',
239
+ 'mcp_servers: {cloudru_hub: {command: raw}}\n',
240
+ 'mcp_servers: &servers\n github: {}\n',
241
+ 'mcp_servers:\n other:\n command: harmless\n# comment\n cloudru-vm:\n command: raw\n',
242
+ 'mcp_servers:\n other:\n command: harmless\n"mcp_servers":\n cloudru-vm:\n command: raw\n',
243
+ ]) {
244
+ const dir = tmp();
245
+ const file = path.join(dir, 'config.yaml');
246
+ fs.writeFileSync(file, source);
247
+ assert.equal(install('hermes', dir, { hermesHome: dir }).code, 2, source);
248
+ assert.equal(fs.readFileSync(file, 'utf8'), source);
249
+ }
250
+ });
251
+
252
+ test('toolset selection validates before install and limits Codex enabled_tools', () => {
253
+ const dir = tmp();
254
+ const invalid = install('codex', dir, { toolsets: 'inventory,all' });
255
+ assert.equal(invalid.code, 1);
256
+ assert.deepEqual(fs.readdirSync(dir), []);
257
+ const ok = install('codex', dir, { toolsets: 'billing' });
258
+ assert.equal(ok.code, 0);
259
+ const config = fs.readFileSync(path.join(dir, '.codex/config.toml'), 'utf8');
260
+ assert.match(config, /billing_balance/);
261
+ assert.doesNotMatch(config, /vm_list/);
262
+ assert.match(config, /CLOUDRU_HUB_CREDENTIALS/);
263
+ assert.match(config, /CLOUDRU_AGREEMENT_ID/);
264
+ assert.match(config, /CLOUDRU_KEY_ID/);
265
+ });
266
+
267
+ test('vm-deploy installs guarded tools and long-call timeouts for Codex, Hermes, OpenCode and Pi', () => {
268
+ const codexDir = tmp();
269
+ assert.equal(install('codex', codexDir, { toolsets: 'vm-deploy' }).code, 0);
270
+ const config = fs.readFileSync(path.join(codexDir, '.codex/config.toml'), 'utf8');
271
+ for (const name of ['deploy_plan', 'deploy_apply', 'vm_exec']) assert.match(config, new RegExp(`"${name}"`));
272
+ for (const name of ['destroy', 'secret_value', 'stack_apply']) assert.doesNotMatch(config, new RegExp(`"${name}"`));
273
+ assert.match(config, /--toolsets","vm-deploy"/);
274
+ assert.match(config, /tool_timeout_sec = 1200/);
275
+ assert.match(config, /env_vars = \["CLOUDRU_VM_BIN","CLOUDRU_HUB_CREDENTIALS","CLOUDRU_VM_HOME","CLOUDRU_HUB_APPROVALS"\]/);
276
+ assert.match(config, /default_tools_approval_mode = "auto"/);
277
+
278
+ const hermesDir = tmp();
279
+ assert.equal(install('hermes', hermesDir, { hermesHome: hermesDir, toolsets: 'vm-deploy', resolvedEngine: pinnedEngine }).code, 0);
280
+ const yaml = YAML.parse(fs.readFileSync(path.join(hermesDir, 'config.yaml'), 'utf8'));
281
+ assert.ok(yaml.mcp_servers.cloudru_hub.tools.include.includes('deploy_apply'));
282
+ assert.ok(!yaml.mcp_servers.cloudru_hub.tools.include.includes('destroy'));
283
+ assert.equal(yaml.mcp_servers.cloudru_hub.timeout, 1200);
284
+
285
+ const opencodeDir = tmp();
286
+ assert.equal(install('opencode', opencodeDir, { toolsets: 'vm-deploy' }).code, 0);
287
+ const opencode = JSON.parse(fs.readFileSync(path.join(opencodeDir, 'opencode.json'), 'utf8'));
288
+ assert.equal(opencode.mcp.cloudru_hub.timeout, 1_200_000, 'OpenCode uses milliseconds');
289
+ assert.equal(install('opencode', opencodeDir, { toolsets: 'vm-deploy' }).code, 0);
290
+
291
+ const piDir = tmp();
292
+ assert.equal(install('pi', piDir, { toolsets: 'vm-deploy' }).code, 0);
293
+ const pi = JSON.parse(fs.readFileSync(path.join(piDir, '.pi/mcp.json'), 'utf8'));
294
+ assert.equal(pi.mcpServers.cloudru_hub.timeout, 1200, 'Pi uses seconds');
295
+ assert.equal(install('pi', piDir, { toolsets: 'vm-deploy' }).code, 0);
296
+ });
297
+
298
+ test('k8s-deploy installs the bounded profile across five hosts and refuses a raw Claude server', () => {
299
+ const codexDir = tmp();
300
+ assert.equal(install('codex', codexDir, { toolsets: 'k8s-deploy' }).code, 0);
301
+ const codex = fs.readFileSync(path.join(codexDir, '.codex/config.toml'), 'utf8');
302
+ assert.match(codex, /k8s-deploy/);
303
+ assert.match(codex, /tool_timeout_sec = 1200/);
304
+ assert.doesNotMatch(codex, /k8s_create/);
305
+
306
+ const hermesDir = tmp();
307
+ assert.equal(install('hermes', hermesDir, { toolsets: 'k8s-deploy', hermesHome: hermesDir, resolvedEngine: pinnedEngine }).code, 0);
308
+ const hermes = YAML.parse(fs.readFileSync(path.join(hermesDir, 'config.yaml'), 'utf8'));
309
+ assert.deepEqual(hermes.mcp_servers.cloudru_hub.tools.include, ['k8s_apply', 'k8s_get', 'k8s_kubectl', 'k8s_list', 'k8s_nodepools', 'k8s_options']);
310
+ assert.equal(hermes.mcp_servers.cloudru_hub.timeout, 1200);
311
+
312
+ const opencodeDir = tmp();
313
+ assert.equal(install('opencode', opencodeDir, { toolsets: 'k8s-deploy' }).code, 0);
314
+ assert.equal(JSON.parse(fs.readFileSync(path.join(opencodeDir, 'opencode.json'), 'utf8')).mcp.cloudru_hub.timeout, 1_200_000);
315
+
316
+ const piDir = tmp();
317
+ assert.equal(install('pi', piDir, { toolsets: 'k8s-deploy' }).code, 0);
318
+ assert.equal(JSON.parse(fs.readFileSync(path.join(piDir, '.pi/mcp.json'), 'utf8')).mcpServers.cloudru_hub.timeout, 1200);
319
+
320
+ const claudeDir = tmp();
321
+ assert.equal(install('claude-code', claudeDir, { toolsets: 'k8s-deploy' }).code, 0);
322
+ const mcp = JSON.parse(fs.readFileSync(path.join(claudeDir, '.mcp.json'), 'utf8'));
323
+ assert.ok(mcp.mcpServers.cloudru_hub.args.includes('k8s-deploy'));
324
+ assert.equal(mcp.mcpServers['cloudru-vm'], undefined);
325
+ assert.deepEqual(JSON.parse(fs.readFileSync(path.join(claudeDir, '.claude/settings.json'), 'utf8')).enabledMcpjsonServers, ['cloudru_hub']);
326
+ assert.equal(install('claude-code', claudeDir, { toolsets: 'k8s-deploy' }).code, 0);
327
+ assert.equal(install('claude-code', claudeDir, { toolsets: 'k8s-deploy', npx: true }).code, 2);
328
+ const rawDir = tmp();
329
+ fs.writeFileSync(path.join(rawDir, '.mcp.json'), JSON.stringify({ mcpServers: { 'cloudru-vm': { command: 'unsafe' } } }));
330
+ assert.equal(install('claude-code', rawDir, { toolsets: 'k8s-deploy' }).code, 2);
331
+ const invalidMcp = tmp();
332
+ fs.writeFileSync(path.join(invalidMcp, '.mcp.json'), '[]\n');
333
+ assert.equal(install('claude-code', invalidMcp, { toolsets: 'k8s-deploy' }).code, 2);
334
+ assert.equal(fs.readFileSync(path.join(invalidMcp, '.mcp.json'), 'utf8'), '[]\n');
335
+ const invalidSettings = tmp();
336
+ fs.mkdirSync(path.join(invalidSettings, '.claude'));
337
+ fs.writeFileSync(path.join(invalidSettings, '.claude/settings.json'), '[]\n');
338
+ assert.equal(install('claude-code', invalidSettings, { toolsets: 'k8s-deploy' }).code, 2);
339
+ assert.equal(fs.readFileSync(path.join(invalidSettings, '.claude/settings.json'), 'utf8'), '[]\n');
116
340
  });
117
341
 
118
342
  test('unknown target is an error listing the known ones', () => {
119
343
  const res = install('emacs', tmp());
120
344
  assert.equal(res.code, 1);
121
345
  assert.ok(res.lines[0].includes('unknown target'));
122
- assert.equal(Object.keys(TIERS).length, 9);
346
+ assert.equal(Object.keys(TIERS).length, 11);
123
347
  });
124
348
 
125
349
  test('probeBrake discriminates: a hook that stops vetoing FAILS the probe (broken-brake control)', () => {
@@ -0,0 +1,125 @@
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 { spawnSync } from 'node:child_process';
7
+ import { createRequire } from 'node:module';
8
+
9
+ const require = createRequire(import.meta.url);
10
+ const YAML = require('yaml');
11
+ const cls = require('../data/tools-classification.json');
12
+ const { selectedToolNames } = require('../src/toolsets.js');
13
+ const { PILOT_IMAGE, validateManifest, issueK8sApproval, consumeK8sApproval, k8sSession } = require('../src/k8s-approval.js');
14
+ const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'cloudru-k8s-approval-'));
15
+ after(() => fs.rmSync(scratch, { recursive: true, force: true }));
16
+ const clusterId = '11111111-2222-4333-8444-555555555555';
17
+ const projectId = 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee';
18
+ const credentials = path.join(scratch, 'credentials.json');
19
+ fs.writeFileSync(credentials, JSON.stringify({ key_id: 'test-only', secret: 'test-only', project_id: projectId }), { mode: 0o600 });
20
+ const env = { ...process.env, CLOUDRU_HUB_CREDENTIALS: credentials, CLOUDRU_HUB_APPROVALS: path.join(scratch, 'approvals') };
21
+
22
+ function docs() {
23
+ const metadata = (name) => ({ name, namespace: 'snake-pilot' });
24
+ const app = 'snake-game';
25
+ return [
26
+ { apiVersion: 'v1', kind: 'ConfigMap', metadata: metadata('snake-game-html'), data: { 'index.html': '<!doctype html><title>Snake</title>' } },
27
+ { apiVersion: 'apps/v1', kind: 'Deployment', metadata: metadata(app), spec: {
28
+ replicas: 1, selector: { matchLabels: { app } }, template: { metadata: { labels: { app } }, spec: {
29
+ automountServiceAccountToken: false, securityContext: { runAsNonRoot: true },
30
+ containers: [{ name: app, image: PILOT_IMAGE, command: ['httpd'], args: ['-f', '-p', '8080', '-h', '/www'],
31
+ ports: [{ containerPort: 8080 }], readinessProbe: { httpGet: { path: '/', port: 8080 } },
32
+ resources: { requests: { cpu: '50m', memory: '32Mi' }, limits: { cpu: '100m', memory: '64Mi' } },
33
+ securityContext: { allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, runAsUser: 10001, runAsGroup: 10001, capabilities: { drop: ['ALL'] } },
34
+ volumeMounts: [{ name: 'html', mountPath: '/www', readOnly: true }] }],
35
+ volumes: [{ name: 'html', configMap: { name: 'snake-game-html' } }],
36
+ } } } },
37
+ { apiVersion: 'v1', kind: 'Service', metadata: metadata(app), spec: { type: 'ClusterIP', selector: { app }, ports: [{ port: 80, targetPort: 8080 }] } },
38
+ ];
39
+ }
40
+ const serialize = (resources) => resources.map((r) => YAML.stringify(r)).join('---\n');
41
+ const manifest = serialize(docs());
42
+ const digest = require('crypto').createHash('sha256').update(manifest).digest('hex');
43
+ const manifestFile = path.join(scratch, 'snake.yaml');
44
+ fs.writeFileSync(manifestFile, manifest);
45
+
46
+ test('bounded Kubernetes manifest accepts exactly the reviewed pod-native resources', () => {
47
+ assert.deepEqual(validateManifest(manifest, 'snake-pilot').resources.map((r) => r.kind), ['ConfigMap', 'Deployment', 'Service']);
48
+ assert.deepEqual(selectedToolNames('k8s-deploy'), ['k8s_apply', 'k8s_get', 'k8s_kubectl', 'k8s_list', 'k8s_nodepools', 'k8s_options']);
49
+ for (const name of selectedToolNames('k8s-deploy')) {
50
+ assert.equal(cls.tools[name].permission, ['k8s_apply', 'k8s_kubectl'].includes(name) ? 'ask' : 'allow');
51
+ }
52
+ });
53
+
54
+ test('unexpected mutators, exposure, privilege, image and namespace are rejected', () => {
55
+ const mutations = [
56
+ (r) => { r[1].spec.template.spec.volumes.push({ name: 'host', hostPath: { path: '/etc' } }); },
57
+ (r) => { r[1].spec.template.spec.containers[0].securityContext.privileged = true; },
58
+ (r) => { r[1].spec.template.spec.containers[0].command = ['sh']; },
59
+ (r) => { r[1].spec.template.spec.containers[0].image = 'docker.io/library/busybox@sha256:' + 'f'.repeat(64); },
60
+ (r) => { r[2].spec.type = 'LoadBalancer'; },
61
+ (r) => { r[0].metadata.namespace = 'default'; },
62
+ (r) => { r.push({ apiVersion: 'v1', kind: 'Secret', metadata: { name: 's', namespace: 'snake-pilot' } }); },
63
+ ];
64
+ for (const mutate of mutations) {
65
+ const r = docs(); mutate(r);
66
+ assert.throws(() => validateManifest(serialize(r), 'snake-pilot'));
67
+ }
68
+ });
69
+
70
+ test('digest, project, expiry and single-use approval bind the exact manifest', () => {
71
+ assert.throws(() => issueK8sApproval(clusterId, 'snake-pilot', manifestFile, '0'.repeat(64), env), /digest differs/);
72
+ const a = issueK8sApproval(clusterId, 'snake-pilot', manifestFile, digest, env, 1000);
73
+ assert.equal(a.manifestSha256, digest);
74
+ const used = consumeK8sApproval(a.approvalId, env, 1100);
75
+ assert.equal(used.manifest, manifest);
76
+ assert.equal(k8sSession(a.approvalId, env, 1200).namespace, 'snake-pilot');
77
+ assert.throws(() => consumeK8sApproval(a.approvalId, env, 1200));
78
+ const b = issueK8sApproval(clusterId, 'snake-pilot', manifestFile, digest, env, 2000);
79
+ const otherCredentials = path.join(scratch, 'other.json');
80
+ fs.writeFileSync(otherCredentials, JSON.stringify({ key_id: 'test-other', secret: 'test-other', project_id: 'ffffffff-eeee-4ddd-8ccc-bbbbbbbbbbbb' }), { mode: 0o600 });
81
+ assert.throws(() => consumeK8sApproval(b.approvalId, { ...env, CLOUDRU_HUB_CREDENTIALS: otherCredentials }, 2100), /does not match/);
82
+ assert.throws(() => consumeK8sApproval(b.approvalId, env, 2000 + 10 * 60_000));
83
+ });
84
+
85
+ test('MCP proxy forwards only locally approved apply and four fixed verification queries', () => {
86
+ const a = issueK8sApproval(clusterId, 'snake-pilot', manifestFile, digest, env);
87
+ const engine = path.join(scratch, 'fake-engine');
88
+ const log = path.join(scratch, 'forwarded.jsonl');
89
+ fs.writeFileSync(engine, `#!/usr/bin/env node
90
+ 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==='tools/list')result={tools:[{name:'k8s_apply'},{name:'k8s_kubectl'},{name:'k8s_create'}]};else result={content:[{type:'text',text:'{"status":"applied"}'}]};process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:m.id,result})+'\\n');}});
91
+ `);
92
+ fs.chmodSync(engine, 0o755);
93
+ const script = `const {runSafeMcp}=require(${JSON.stringify(path.resolve(import.meta.dirname, '../src/safe-mcp.js'))});runSafeMcp({toolsets:'k8s-deploy',resolved:{path:${JSON.stringify(engine)},verified:true,sha256:${JSON.stringify(cls.engineSha256)}}});`;
94
+ const msg = (id, name, args) => JSON.stringify({ jsonrpc: '2.0', id, method: 'tools/call', params: { name, arguments: args } });
95
+ const lines = [
96
+ JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }),
97
+ msg(2, 'k8s_apply', { approval_id: a.approvalId, cluster_id: 'wrong', confirmed: true }),
98
+ msg(3, 'k8s_apply', { approval_id: a.approvalId }),
99
+ msg(4, 'k8s_apply', { approval_id: a.approvalId }),
100
+ msg(5, 'k8s_kubectl', { approval_id: a.approvalId, query: 'delete' }),
101
+ msg(6, 'k8s_kubectl', { approval_id: a.approvalId, query: 'pods', args: 'delete pods -A' }),
102
+ msg(7, 'k8s_kubectl', { approval_id: a.approvalId, query: 'pods' }),
103
+ msg(8, 'k8s_kubectl', { approval_id: a.approvalId, query: 'deployment' }),
104
+ msg(9, 'k8s_kubectl', { approval_id: a.approvalId, query: 'service' }),
105
+ msg(10, 'k8s_kubectl', { approval_id: a.approvalId, query: 'rollout' }),
106
+ msg(11, 'k8s_create', { confirmed: true }),
107
+ msg(12, 'k8s_apply', { approval_id: a.approvalId, create_only: false }),
108
+ ];
109
+ fs.writeFileSync(log, '');
110
+ const r = spawnSync(process.execPath, ['-e', script], { input: lines.join('\n') + '\n', encoding: 'utf8', timeout: 5000,
111
+ env: { ...env, FAKE_LOG: log } });
112
+ assert.equal(r.status, 0, r.stderr);
113
+ const responses = r.stdout.trim().split('\n').map(JSON.parse);
114
+ const byId = Object.fromEntries(responses.map((x) => [x.id, x]));
115
+ assert.deepEqual(byId[1].result.tools.map((x) => x.name), ['k8s_apply', 'k8s_kubectl']);
116
+ assert.deepEqual(Object.keys(byId[1].result.tools[0].inputSchema.properties), ['approval_id']);
117
+ for (const id of [2, 4, 5, 6, 11, 12]) assert.ok(byId[id].error, `request ${id} rejected`);
118
+ const forwarded = fs.readFileSync(log, 'utf8').trim().split('\n').map(JSON.parse).filter((x) => x.method === 'tools/call');
119
+ assert.deepEqual(forwarded.map((x) => x.params.name), ['k8s_apply', 'k8s_kubectl', 'k8s_kubectl', 'k8s_kubectl', 'k8s_kubectl']);
120
+ assert.deepEqual(forwarded[0].params.arguments, { cluster_id: clusterId, namespace: 'snake-pilot', manifest, create_only: true, confirmed: true });
121
+ assert.deepEqual(forwarded[1].params.arguments, { cluster_id: clusterId, args: '-n snake-pilot get pods -l app=snake-game -o json' });
122
+ assert.deepEqual(forwarded[2].params.arguments, { cluster_id: clusterId, args: '-n snake-pilot get deployment snake-game -o json' });
123
+ assert.deepEqual(forwarded[3].params.arguments, { cluster_id: clusterId, args: '-n snake-pilot get service snake-game -o json' });
124
+ assert.deepEqual(forwarded[4].params.arguments, { cluster_id: clusterId, args: '-n snake-pilot rollout status deployment/snake-game --timeout=90s' });
125
+ });
@@ -0,0 +1,39 @@
1
+ import assert from 'node:assert/strict';
2
+ import { readFileSync } from 'node:fs';
3
+ import test from 'node:test';
4
+
5
+ const packageRoot = new URL('../', import.meta.url);
6
+ const registry = JSON.parse(
7
+ readFileSync(new URL('./mutation-registry.json', import.meta.url), 'utf8'),
8
+ );
9
+
10
+ test('mutation registry entries are complete', () => {
11
+ for (const entry of registry.entries) {
12
+ assert.ok(typeof entry.id === 'string' && entry.id.length > 0, 'entry has no id');
13
+ assert.ok(typeof entry.file === 'string' && entry.file.length > 0, `${entry.id}: no file`);
14
+ if (entry.uncoverable === true) {
15
+ assert.ok(typeof entry.reason === 'string' && entry.reason.trim().length > 0,
16
+ `${entry.id}: uncoverable entry has no reason`);
17
+ continue;
18
+ }
19
+ assert.ok(typeof entry.mutation?.find === 'string' && entry.mutation.find.length > 0,
20
+ `${entry.id}: ${entry.file}: no mutation.find`);
21
+ assert.equal(typeof entry.mutation?.replace, 'string',
22
+ `${entry.id}: ${entry.file}: mutation.replace is not a string`);
23
+ assert.notEqual(entry.mutation.replace, entry.mutation.find,
24
+ `${entry.id}: ${entry.file}: mutation.replace equals mutation.find`);
25
+ }
26
+ });
27
+
28
+ test('every mutation.find occurs exactly once in its source file', () => {
29
+ const stale = [];
30
+ for (const entry of registry.entries) {
31
+ if (entry.uncoverable === true) continue;
32
+ const source = readFileSync(new URL(entry.file, packageRoot), 'utf8');
33
+ const occurrences = source.split(entry.mutation.find).length - 1;
34
+ if (occurrences !== 1) {
35
+ stale.push(`${entry.id}: ${entry.file}: mutation.find occurs ${occurrences} time(s)`);
36
+ }
37
+ }
38
+ assert.deepEqual(stale, [], `stale or ambiguous mutation entries:\n${stale.join('\n')}`);
39
+ });