@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.
@@ -0,0 +1,34 @@
1
+ # Cloud.ru tools in Codex, Hermes, OpenCode and Pi
2
+
3
+ `cloudru-hub mcp-safe` is the common process boundary. It starts only the binary whose SHA-256 matches the package pin and the classification baseline. Its default profile advertises the 78 golden `allow` tools and rejects `ask`, `deny`, unknown tools and unsupported MCP methods before forwarding a call. Some planning tools may still write local plan files. Filesystem and shell access granted separately to an agent remain outside this MCP boundary.
4
+
5
+ Installers default to the smaller `inventory` toolset (11 tools). Select comma-separated groups with `--toolsets inventory,billing`; the available groups are `inventory`, `compute`, `kubernetes`, `data`, `network`, `storage`, and `billing`. `--toolsets all` exposes all 78 allow-classified tools. The proxy checks the chosen set on every `tools/call`, so a tool hidden from discovery cannot be invoked directly.
6
+
7
+ For the read-only billing tools, put `agreement_id` in the same owner-only credential file as `key_id`, `secret` and `project_id`, then select `--toolsets billing` or `inventory,billing`. The safe proxy passes `agreement_id` to the Go engine as `CLOUDRU_AGREEMENT_ID`; Codex installation inherits the credential-file path and billing environment variables. If the file is absent, an existing environment-only setup still works. An explicitly configured but missing or invalid file is an error. `billing_consumption` currently returns an agreement-wide aggregate. The Cloud.ru API documents project filtering, but this adapter has not implemented or live-verified that filter; do not label its total as a project-only charge.
8
+
9
+ The separate `--toolsets vm-deploy` profile allows one reviewed VM plan and remote commands on the VM returned by that plan. Put temporary credentials in `~/.config/cloudru-hub/credentials.json` (owner-only mode `0600`) as `{"key_id":"…","secret":"…","project_id":"UUID","region":"ru.AZ-2"}`. The proxy reads JSON directly and passes values to the pinned Go child as environment variables; it never runs shell `eval`. The region may also be a zone UUID. After `deploy_plan` returns a persisted plan, inspect its VM shape and RUB/month estimate, then run `cloudru-hub approve --plan-id <id> --max-monthly-rub <cap>`. This creates a ten-minute, one-use approval for the exact plan bytes and project. `deploy_apply` ignores the model's `confirmed` value and supplies it only after consuming that approval. Successful deployment opens a two-hour `vm_exec` session for that VM UUID and the plan's SSH login. A generated login password is removed from the MCP result because the engine uses its own SSH key. The deployment profile gives Codex and Hermes a 1200-second per-tool timeout, covering VM boot, SSH and API retries. A same-name VM blocks creation; inspect the returned UUID and live inventory before retrying with a different name. The profile does not expose `destroy`; inspect the VM ID and remove the test resource through a separate supervised cleanup step.
10
+
11
+ This local approval is an operator workflow, not a hard security boundary against an agent with unrestricted shell access as the same Unix user: such an agent could invoke `approve` or edit the local files itself. Use process/user isolation and cloud IAM limits if the deployment agent is untrusted. The cost number is an estimate, and the operator cap controls the reviewed plan, not Cloud.ru's final bill.
12
+
13
+ The launcher archive does **not bundle** the engine. A Linux/x64 npm install pulls `@dzhechkov/cloudru-vm-linux-x64@0.2.6` as an exact optional dependency. On other platforms, or when optional dependencies are disabled, set `CLOUDRU_VM_BIN` to a trusted copy whose hash matches `package.json.cloudruHub.binaryHashes`. Run `cloudru-hub self-test --json` first. A missing or changed binary makes `mcp-safe` refuse startup.
14
+
15
+ | Host | Setup | Activation check |
16
+ |---|---|---|
17
+ | Codex | `cloudru-hub install --target codex --dir /trusted/project --toolsets vm-deploy` writes `.codex/config.toml` with `required=true`, 12 enabled tools, an explicit 1200-second timeout and inherited paths for the pinned engine and credentials file | Trust the project; in a fresh session call local-only `status` before planning. The profile requests host `auto`, but `codex exec` with approval policy `never` blocked even `status` in the local probe; `--approve-for-me` allowed it. The proxy still requires a separate plan-bound approval for `deploy_apply` |
18
+ | OpenCode | `cloudru-hub install --target opencode --dir /trusted/project` writes `opencode.json` with a local server | `opencode mcp list`, then inspect tools in a session; installer refuses an existing `opencode.jsonc` to avoid conflicting configs |
19
+ | Pi | `cloudru-hub install --target pi --dir /trusted/project` writes `.pi/mcp.json` | Use a Pi release with built-in MCP, trust the project, run `pi mcp list`, then `/mcp` in a session; an older Pi may require an adapter |
20
+ | Hermes | With a hash-verified engine available, `cloudru-hub install --target hermes` adds a guarded entry to `$HERMES_HOME/config.yaml` with an explicit nonempty include list and `CLOUDRU_VM_BIN` path; `--hermes-home` selects a test directory | Run `hermes mcp test cloudru_hub`; complex YAML and existing Cloud.ru entries are refused for manual review |
21
+
22
+ Hermes needs model authentication separately from the Cloud.ru service-account file. On this server, `hermes model` → **OpenAI Codex** → import existing Codex CLI credentials configured the default Hermes profile without changing `~/.codex/auth.json`. Choose a model, then check `hermes auth status openai-codex` and make a short inference probe before attempting a cloud operation. Hermes keeps an independent copy of the imported OAuth session in its own auth store; subsequent token refreshes are not shared with Codex CLI. A successful model probe alone does not establish that the Cloud.ru MCP server has credentials or that a VM deployment passed.
23
+
24
+ Hermes sends periodic MCP tool-list keepalives. The pinned proxy answers repeat listings from its filtered in-process cache while the Go engine handles a long deployment. This prevents the keepalive from waiting behind a serial `deploy_apply` and losing that call's result on reconnect. The first listing still comes from the engine; restart the proxy after changing the pinned engine or classification.
25
+
26
+ Project configs contain an absolute path to this installed package. Regenerate them after moving the package or copying the project to another machine. Hermes filters inherited environment variables when starting an MCP server, so its installer first checks the local engine hash and writes the engine's absolute path into the server-specific `env`. The proxy hashes that file again on every start. Hermes installation refuses a missing or mismatched engine without changing YAML; regenerate the entry after moving the binary. The installers preserve unrelated simple configuration and refuse an existing Cloud.ru server instead of silently overwriting it. Codex refuses existing MCP declarations whose TOML semantics need review. Hermes inserts only into a plain mapping, keeps unrelated YAML text, and refuses anchors or unusual structure. A Hermes plugin's `pre_tool_call` hook is supplementary: plugin failures and some plugin dispatch paths do not provide a reliable enforcement boundary. The proxy enforces its policy even when plugins are disabled.
27
+
28
+ Host-side registration and read checks alone do not prove a live VM deployment. The package tests use a fake engine to show that an unapproved call never reaches the backend and a VM command cannot target an unrelated VM. Do not switch these host entries to raw `mcp` to recover write access. Codex and Hermes each completed the two live Snake scenarios; their HTTP checks and cleanup evidence are in the feature acceptance record.
29
+
30
+ This integration follows the current [Codex MCP configuration](https://developers.openai.com/codex/extend/mcp), [OpenCode MCP](https://opencode.ai/docs/mcp-servers), [Pi MCP](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/mcp.md) and locally inspected Hermes `mcp_servers` contract. Host versions and effective configuration should be recorded when testing a real installation.
31
+
32
+ ## Ideas worth carrying forward from EVO
33
+
34
+ The [EVO CLI](https://github.com/shatim-ops/evo-cli) currently has a broader standalone Go CLI, more MCP operations, HTTP transport, selectable `--toolsets`, `--allow-project`, `doctor`, JSON output and dry-run/preview flows. Those are concrete advantages over this thin adapter. The most useful next work in our own Go engine is a shared operation registry for CLI and MCP, a measured toolset/profile switch to reduce context use, and an explanation of resource cost before changes. A `confirmed=true` argument supplied by a model is not evidence that a human approved a mutation; any write mode in our hub must enforce approval independently of that argument.
@@ -1,7 +1,6 @@
1
- # Local testing — @dzhechkov/cloudru-hub (no publish, no licence needed)
1
+ # Local testing — @dzhechkov/cloudru-hub
2
2
 
3
- The launcher and everything around it run TODAY against a locally present engine build.
4
- Nothing here publishes anything; the ADR-001 hold stays untouched.
3
+ The launcher runs against the pinned Go build. Its archive contains no engine bytes; the Linux/x64 optional package contains the static engine and its dependency-license inventory.
5
4
 
6
5
  ## 1. Point the launcher at an engine binary
7
6
 
@@ -9,23 +8,19 @@ Any of the three, in this precedence order:
9
8
 
10
9
  ```bash
11
10
  # a) environment variable (simplest — what the self-test proof below uses)
12
- export CLOUDRU_VM_BIN=/path/to/hermes-engine-main/engine-build/cloudru-vm
11
+ export CLOUDRU_VM_BIN=/path/to/dz-harness-hub/packages/@dzhechkov/cloudru-vm-linux-x64/native/cloudru-vm
13
12
 
14
13
  # b) persistent config
15
14
  mkdir -p ~/.cloudru-hub
16
15
  cat > ~/.cloudru-hub/config.json <<'EOF'
17
- { "enginePath": "/path/to/hermes-engine-main/engine-build/cloudru-vm" }
16
+ { "enginePath": "/path/to/dz-harness-hub/packages/@dzhechkov/cloudru-vm-linux-x64/native/cloudru-vm" }
18
17
  EOF
19
18
 
20
- # c) (post-grant only) the platform package @dzhechkov/cloudru-vm-linux-x64
19
+ # c) no override: the installed Linux/x64 optional platform package
21
20
  ```
22
21
 
23
- The pinned baseline archive is
24
- `features/hermes-claude-adaptation/evidence/hermes-engine-src-20260718.tgz` (sha256 in
25
- `evidence/ENGINE-BASELINE.json`); the matching prebuilt binary identifies itself as
26
- `cloudru-vm 0.2.3-20260718`, sha256
27
- `59f83fc0678b95146ca539764d0c805bd2514b8588f198c1e835b26e54c05124` — the launcher
28
- verifies the resolved binary against exactly this pin and says so.
22
+ The pinned binary identifies itself as `cloudru-vm 0.2.6-20261020` and has SHA-256
23
+ `4463b09323c9dfe0c32d618d335734a947e7fab0a75d07a7bea089b97a97c9b6`.
29
24
 
30
25
  ## 2. Self-test (end-to-end, no credentials required)
31
26
 
@@ -34,19 +29,18 @@ cd packages/@dzhechkov/cloudru-hub
34
29
  node bin/cloudru-hub.js self-test
35
30
  ```
36
31
 
37
- Expected output (MEASURED — live run on this server, 2026-08-10, baseline binary):
32
+ Expected output for the current private build:
38
33
 
39
34
  ```
40
- ✓ resolve: …/engine-build/cloudru-vm (source: env)
41
- ✓ sha256: 59f83fc0678b95146ca539764d0c805bd2514b8588f198c1e835b26e54c05124 — MATCHES pinned baseline
42
- ✓ engine-version: cloudru-vm 0.2.3-20260718
35
+ ✓ resolve: …/cloudru-vm-linux-x64/native/cloudru-vm (source: env)
36
+ ✓ sha256: 4463b09323c9dfe0c32d618d335734a947e7fab0a75d07a7bea089b97a97c9b6 — MATCHES pinned baseline
37
+ ✓ engine-version: cloudru-vm 0.2.6-20261020
43
38
  ✓ tools-list: 144 tools live (snapshot: 144)
44
39
  ✓ classification-coverage: every live tool is classified (144 golden entries)
45
40
  self-test: ALL GREEN
46
41
  ```
47
42
 
48
- `tools/list` needs no Cloud.ru credentials; real tool calls do (env
49
- `CLOUDRU_KEY_ID`/`CLOUDRU_SECRET`/`CLOUDRU_PROJECT_ID`, see the engine's own docs).
43
+ `tools/list` needs no Cloud.ru credentials. For `--toolsets vm-deploy`, place temporary keys in the owner-only JSON file described in `HERMES-CODEX.md`; the proxy maps them to the Go child's environment without shell evaluation.
50
44
 
51
45
  ## 3. Optional: `npm link` for a global `cloudru-hub`
52
46
 
@@ -70,8 +64,6 @@ success unless the veto executes. Finish with the manual probe it prints: in a f
70
64
  Claude Code session, call `mcp__cloudru-vm__deploy` and confirm a permission prompt
71
65
  appears; note which prefix spelling the runtime used.
72
66
 
73
- ## 5. Testing from another server AFTER the grant (`npm install` path)
67
+ ## 5. Testing from another server (`npm install` path)
74
68
 
75
- Blocked until ADR-001 clears — see README "Post-grant publish checklist". Until then,
76
- another server can test by copying this package directory (or `npm pack` + install the
77
- tarball) plus an engine binary, then steps 1–4 exactly as above.
69
+ On Linux/x64, install `@dzhechkov/cloudru-hub@0.1.14` normally and run `cloudru-hub self-test` without setting `CLOUDRU_VM_BIN`. npm installs the exact optional engine package. If optional dependencies were disabled or the platform is unsupported, provide a matching trusted binary explicitly.
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@dzhechkov/cloudru-hub",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "licenseHold": {
5
5
  "adr": "features/hermes-claude-adaptation/03_adr/001-licence-and-provenance-precondition.md",
6
- "note": "SATISFIED hold (ADR-001 AM-2, grant recorded 2026-08-12 — see grant-record-2026-08-12.md). The field deliberately STAYS: the dz guard `licence-hold` rule passes a satisfied hold with the trigger in place and keeps verifying LICENSE (no PENDING placeholder, Grant-Confirmation URL), THIRD_PARTY_NOTICES and the SPDX license field on every publish. test/pack-holds.test.mjs goes RED if this field is dropped or the LICENSE grant record is degraded."
6
+ "note": "SATISFIED hold. Timur is credited for substantial Cloud.ru API testing. The 2026-10-20 correction is in the historical grant record. This field keeps the dz guard licence-hold rule checking LICENSE, THIRD_PARTY_NOTICES and SPDX on every publish."
7
7
  },
8
- "description": "Unofficial Cloud.ru Evolution launcher for Claude Code and harness-cli targets: thin JS launcher for the cloudru-vm MCP engine (144 tools), per-target installers with a deterministic paid-step brake (permission ask/deny + PreToolUse veto hooks), and skill dialect compiler. The engine binary is resolved at RUNTIME, never bundled. NOT affiliated with Cloud.ru.",
8
+ "description": "Unofficial Cloud.ru Evolution launcher with a pinned cloud-write-blocking MCP proxy for Codex, Hermes, OpenCode and Pi, Claude Code permissions, and skill dialect compiler. Engine binary resolved at runtime, never bundled. NOT affiliated with Cloud.ru.",
9
9
  "main": "src/cli.js",
10
10
  "bin": {
11
11
  "cloudru-hub": "./bin/cloudru-hub.js"
@@ -26,9 +26,9 @@
26
26
  ],
27
27
  "cloudruHub": {
28
28
  "binaryHashes": {
29
- "linux-x64": "59f83fc0678b95146ca539764d0c805bd2514b8588f198c1e835b26e54c05124"
29
+ "linux-x64": "4463b09323c9dfe0c32d618d335734a947e7fab0a75d07a7bea089b97a97c9b6"
30
30
  },
31
- "engineVersion": "cloudru-vm 0.2.3-20260718",
31
+ "engineVersion": "cloudru-vm 0.2.6-20261020",
32
32
  "platformPackages": {
33
33
  "linux-x64": "@dzhechkov/cloudru-vm-linux-x64"
34
34
  }
@@ -49,19 +49,25 @@
49
49
  "engines": {
50
50
  "node": ">=18.0.0"
51
51
  },
52
+ "dependencies": {
53
+ "yaml": "^2.9.0"
54
+ },
55
+ "optionalDependencies": {
56
+ "@dzhechkov/cloudru-vm-linux-x64": "0.2.6"
57
+ },
58
+ "scripts": {
59
+ "test": "node --test 'test/*.test.mjs'"
60
+ },
52
61
  "repository": {
53
62
  "type": "git",
54
63
  "url": "git+https://github.com/djd1m/dz-harness.git",
55
64
  "directory": "packages/@dzhechkov/cloudru-hub"
56
65
  },
57
- "homepage": "https://github.com/djd1m/dz-harness/tree/main/packages/@dzhechkov/cloudru-hub#readme",
66
+ "homepage": "https://aicoding.space",
58
67
  "bugs": {
59
68
  "url": "https://github.com/djd1m/dz-harness/issues"
60
69
  },
61
70
  "publishConfig": {
62
71
  "access": "public"
63
- },
64
- "scripts": {
65
- "test": "node --test 'test/*.test.mjs'"
66
72
  }
67
- }
73
+ }
package/sbom.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "hashes": [
16
16
  {
17
17
  "alg": "SHA-256",
18
- "content": "edc0f1b076a22dc14b863bef8b287c1b9d8618a12b7ecf9f6eb0c497f74361f3"
18
+ "content": "4d6d0e6f47b09b017a32e4f3287c0669821e61569835c397245e5077435f5b15"
19
19
  }
20
20
  ]
21
21
  },
@@ -25,7 +25,7 @@
25
25
  "hashes": [
26
26
  {
27
27
  "alg": "SHA-256",
28
- "content": "68da42b10dcc095cdfd7249557cee30285664e5dea00dc85475ff047d975616a"
28
+ "content": "78a14ae697528fa79451ea10725c5ca356f6658378bfb39849b51d05207ab3f3"
29
29
  }
30
30
  ]
31
31
  },
@@ -35,7 +35,7 @@
35
35
  "hashes": [
36
36
  {
37
37
  "alg": "SHA-256",
38
- "content": "504ce5cd5b6293b3606f861f31406ae89aa6fee45a03c9099d793aa5a456ced1"
38
+ "content": "2c25bbd9659f1a02e2a7a5413de8673e8735d67e583458fdede75dfd032b6106"
39
39
  }
40
40
  ]
41
41
  },
@@ -45,7 +45,7 @@
45
45
  "hashes": [
46
46
  {
47
47
  "alg": "SHA-256",
48
- "content": "109cffdaaf9402e88f6cc35a0497ba49db015b329fce87ba20762f34dc95c051"
48
+ "content": "1986a75d5f90631adea8fb40c9fc182371cb875c9d62d65d07fd479b32c876df"
49
49
  }
50
50
  ]
51
51
  },
@@ -85,7 +85,7 @@
85
85
  "hashes": [
86
86
  {
87
87
  "alg": "SHA-256",
88
- "content": "13d6a3e87d767d0015f6b2af0223633fa99e49c69c9a8adcec8f6fce6e46d36e"
88
+ "content": "9abb3ca6347b0cd6cd51b23649c59e0a0c816e6c12a325f573b392affa350d0b"
89
89
  }
90
90
  ]
91
91
  },
@@ -95,7 +95,17 @@
95
95
  "hashes": [
96
96
  {
97
97
  "alg": "SHA-256",
98
- "content": "56221fa8170f2b32ab6d3a907261badc07695f47ae9ac4adca4e43d2ea5ee06f"
98
+ "content": "dff98bb859dfbbf8ed2fb6f16f17b767444cc2172ecc7684ca3df8188c95707c"
99
+ }
100
+ ]
101
+ },
102
+ {
103
+ "type": "file",
104
+ "name": "docs/HERMES-CODEX.md",
105
+ "hashes": [
106
+ {
107
+ "alg": "SHA-256",
108
+ "content": "3605797972f9a7c79c13fbcce0cd9ac8b8fb3a8249abbab938f2b1a45f515420"
99
109
  }
100
110
  ]
101
111
  },
@@ -105,7 +115,7 @@
105
115
  "hashes": [
106
116
  {
107
117
  "alg": "SHA-256",
108
- "content": "092142c00295c54866330f2159a13284d04e2f35a749b32dff0ca4f2ab28ea54"
118
+ "content": "de6f709e820fbb831f390e274238b28ba938644670a8db04fd50d1fa0d86da75"
109
119
  }
110
120
  ]
111
121
  },
@@ -119,7 +129,7 @@
119
129
  },
120
130
  {
121
131
  "name": "dz:canonical-json-sha256-v2",
122
- "value": "c40ea35a6a994034e4643310bdd990367abcb033f49279f5349e95fe489b4e1a"
132
+ "value": "d906ed1871af834274d77dbd3a345b168604456b15360f009a9474778273b11f"
123
133
  }
124
134
  ]
125
135
  },
@@ -139,7 +149,27 @@
139
149
  "hashes": [
140
150
  {
141
151
  "alg": "SHA-256",
142
- "content": "db981ad0078c80ba7a859057638a2576d7f02d87ca2891f8083a92a8147742dd"
152
+ "content": "a22f3fcced6e9970e7bed6b935fc03637b6d4b8d3cd6cc65a2685595a6549c1c"
153
+ }
154
+ ]
155
+ },
156
+ {
157
+ "type": "file",
158
+ "name": "src/credentials.js",
159
+ "hashes": [
160
+ {
161
+ "alg": "SHA-256",
162
+ "content": "21f074b4bf5b5ee9e893a2c1919cf4bb62f9fa05a0d7a1a8c04afc051ccb0e78"
163
+ }
164
+ ]
165
+ },
166
+ {
167
+ "type": "file",
168
+ "name": "src/deploy-approval.js",
169
+ "hashes": [
170
+ {
171
+ "alg": "SHA-256",
172
+ "content": "5c1fba3d298df73ff82f28c72bc5199d23acc6dfc64022438874b0e3381c9394"
143
173
  }
144
174
  ]
145
175
  },
@@ -159,7 +189,27 @@
159
189
  "hashes": [
160
190
  {
161
191
  "alg": "SHA-256",
162
- "content": "5d087b289aaf8cd2e1b4cffbd2227eb3bdc7264cddc0877561e08bc0ea6d6fd2"
192
+ "content": "c928db48febfb4a79884ce1830365a8302122a55fb5d6ada8e771c049396ca89"
193
+ }
194
+ ]
195
+ },
196
+ {
197
+ "type": "file",
198
+ "name": "src/k8s-approval.js",
199
+ "hashes": [
200
+ {
201
+ "alg": "SHA-256",
202
+ "content": "014c2effd1e843d870ae1af4f93f5c82faff9972e9f6ca028ed39d47ec33ee3a"
203
+ }
204
+ ]
205
+ },
206
+ {
207
+ "type": "file",
208
+ "name": "src/probe-child.js",
209
+ "hashes": [
210
+ {
211
+ "alg": "SHA-256",
212
+ "content": "0f7a0c1f6d0d033006ca816df49c24e69eb82aa6b70ec03491bce9e4d674bdd4"
163
213
  }
164
214
  ]
165
215
  },
@@ -169,7 +219,27 @@
169
219
  "hashes": [
170
220
  {
171
221
  "alg": "SHA-256",
172
- "content": "5cbd0861c2973fab62271269b10f748b9638e74f40e8ea034f78ac63bed58001"
222
+ "content": "d3eba14c23be5bfb7e73952e6ae27908667d535f91be9c43e11dbf7b7c4de49d"
223
+ }
224
+ ]
225
+ },
226
+ {
227
+ "type": "file",
228
+ "name": "src/safe-mcp.js",
229
+ "hashes": [
230
+ {
231
+ "alg": "SHA-256",
232
+ "content": "4f95cfb9576c411232503b26053ece6d8d186e4b337beff9bd4afd249f74a014"
233
+ }
234
+ ]
235
+ },
236
+ {
237
+ "type": "file",
238
+ "name": "src/toolsets.js",
239
+ "hashes": [
240
+ {
241
+ "alg": "SHA-256",
242
+ "content": "89229c882b6a3f5bb9ee542b31d1a40374b91879747582a3e82e8aadc72decb3"
173
243
  }
174
244
  ]
175
245
  },
@@ -183,13 +253,33 @@
183
253
  }
184
254
  ]
185
255
  },
256
+ {
257
+ "type": "file",
258
+ "name": "test/billing-credentials.test.mjs",
259
+ "hashes": [
260
+ {
261
+ "alg": "SHA-256",
262
+ "content": "f2e1b05bdb6a6e811844ee19b0142daa02647b387f5d96b4a5d29bd34e3c985d"
263
+ }
264
+ ]
265
+ },
186
266
  {
187
267
  "type": "file",
188
268
  "name": "test/classification.test.mjs",
189
269
  "hashes": [
190
270
  {
191
271
  "alg": "SHA-256",
192
- "content": "25290e9a8715d13670d617e23cb47c4c2d71b1506a8d98e5b682733b626a18b8"
272
+ "content": "026e3031180ce9227b4d31775b9c886b10ad0cb418ed0754aed75b12ded84e31"
273
+ }
274
+ ]
275
+ },
276
+ {
277
+ "type": "file",
278
+ "name": "test/deploy-approval.test.mjs",
279
+ "hashes": [
280
+ {
281
+ "alg": "SHA-256",
282
+ "content": "d72e4363fd50d0fcc65fd23b40601516bd73368c3659f2a55a394c4d1ed3f85c"
193
283
  }
194
284
  ]
195
285
  },
@@ -199,7 +289,7 @@
199
289
  "hashes": [
200
290
  {
201
291
  "alg": "SHA-256",
202
- "content": "9b83b2e2cbbee4ead33af61db840833ce4aa2dd0cf5a4448d72d5886098639c6"
292
+ "content": "3e6236f0cfb159dd73477b6243f6af536925968308085fd248df93932a2bebbf"
203
293
  }
204
294
  ]
205
295
  },
@@ -219,7 +309,17 @@
219
309
  "hashes": [
220
310
  {
221
311
  "alg": "SHA-256",
222
- "content": "e986b29346672bf75b384ab6d8657265b8ecb8f62df6563dca317c1fc63fc8fc"
312
+ "content": "07fe729b0dc6955329d0a3121ce97d575582f8c6f2895e72d0c38175f82b52d2"
313
+ }
314
+ ]
315
+ },
316
+ {
317
+ "type": "file",
318
+ "name": "test/k8s-approval.test.mjs",
319
+ "hashes": [
320
+ {
321
+ "alg": "SHA-256",
322
+ "content": "af04d1941fb7aed2cc51b05f65ffed1c440d4d23a7fb26e13261d45e07b0a1f8"
223
323
  }
224
324
  ]
225
325
  },
@@ -239,7 +339,7 @@
239
339
  "hashes": [
240
340
  {
241
341
  "alg": "SHA-256",
242
- "content": "e56ee7b4ba160f28687423ca113d1bfbcbfdd351fe9d31ad5a19b4d0069dbe3a"
342
+ "content": "09b8fdc28bbe5df9b4cfd7d563c1acb479532157bbe1a99e909b3b370dd96630"
243
343
  }
244
344
  ]
245
345
  },
@@ -249,7 +349,7 @@
249
349
  "hashes": [
250
350
  {
251
351
  "alg": "SHA-256",
252
- "content": "a083ff619f1d97b3e0e1b95dec6e98b56c4296da5171e2eb41c713dd983c8d69"
352
+ "content": "f5e66cce7344efb22c40bba91b422ce84979948b66bb0305377f8c12eb078002"
253
353
  }
254
354
  ]
255
355
  },
@@ -259,7 +359,17 @@
259
359
  "hashes": [
260
360
  {
261
361
  "alg": "SHA-256",
262
- "content": "6b524f69c5ed03aaafab67e734e58092033423b86feade28ad03f516feba2380"
362
+ "content": "9ad83ba418dee86ef680575a0cc7da4af4be3ae995c00f23f84b336838fbda47"
363
+ }
364
+ ]
365
+ },
366
+ {
367
+ "type": "file",
368
+ "name": "test/safe-mcp.test.mjs",
369
+ "hashes": [
370
+ {
371
+ "alg": "SHA-256",
372
+ "content": "45406dabeee0635ede5cda3d2f4144c5c13886d624287fd223b01252d2859b87"
263
373
  }
264
374
  ]
265
375
  }
package/src/cli.js CHANGED
@@ -6,6 +6,9 @@ const { spawn, spawnSync } = require('child_process');
6
6
  const { resolveEngine } = require('./resolve');
7
7
  const { loadClassification, loadSnapshot, coverageGaps } = require('./classification');
8
8
  const { install } = require('./install');
9
+ const { runSafeMcp } = require('./safe-mcp');
10
+ const { issueApproval } = require('./deploy-approval');
11
+ const { issueK8sApproval } = require('./k8s-approval');
9
12
  const { compileSkill, writeCompiled } = require('./dialects');
10
13
  const PKG = require('../package.json');
11
14
 
@@ -13,22 +16,30 @@ function showHelp(write) {
13
16
  write(`cloudru-hub ${PKG.version} — unofficial Cloud.ru Evolution launcher (NOT affiliated with Cloud.ru)
14
17
 
15
18
  The cloudru-vm engine binary is resolved at RUNTIME, never bundled in this package.
16
- Engine additions authored by Тимур (publication approved — grant record in LICENSE Part 2);
17
- launcher, adapters and CLI foundation by Dmitry Zhechkov (MIT).
19
+ Тимур contributed extensive Cloud.ru API testing and identified API defects.
18
20
 
19
21
  Usage: cloudru-hub <command> [options]
20
22
 
21
23
  Commands:
22
24
  resolve [--json] Locate the engine binary (env CLOUDRU_VM_BIN → ~/.cloudru-hub/config.json
23
25
  enginePath → optional platform package) and verify its sha256 pin.
24
- self-test [--json] End-to-end local check: resolve → sha256 → engine version → live
25
- tools/list probe → golden-classification coverage. Exit 0 = all green.
26
+ self-test [--json] Local check: resolve → sha256 → engine version → live tools/list
27
+ → golden-classification coverage. Exit 0 = all green.
26
28
  mcp Run the resolved engine as an MCP stdio server (what .mcp.json points at).
27
- install --target <t> Emit the per-target integration per ADR-005 tiering:
28
- claude-code (full: .mcp.json + permission brake + PreToolUse veto, with an
29
- EXECUTED veto probe), codex (plan-only), agents-md|gemini (pointer-only),
30
- openclaude|opencode|cursor|windsurf|copilot (refused: DEGRADED until live probe).
31
- [--dir <path>] Project directory (default: cwd). [--npx] use the npx form in .mcp.json.
29
+ mcp-safe [--toolsets <groups>] Run a pinned MCP proxy; vm-deploy and k8s-deploy enable bounded deployment.
30
+ approve --plan-id <id> --max-monthly-rub <amount> [--site-sha256 <digest>]
31
+ Authorize one exact VM plan for ten minutes within a RUB/month cap.
32
+ Static sites require the artifact digest shown by deploy_plan.
33
+ k8s-approve --cluster-id <uuid> --namespace <name> --manifest <file> --manifest-sha256 <digest>
34
+ Authorize one exact three-resource Snake manifest on an existing cluster.
35
+ install --target <t> claude-code: permissions + executed hook probe;
36
+ codex|opencode|pi: project MCP config using mcp-safe;
37
+ hermes: guarded $HERMES_HOME/config.yaml entry using mcp-safe;
38
+ openclaude|cursor|windsurf|copilot: refused pending live probe.
39
+ [--dir <path>] Project directory (default: cwd). [--toolsets <groups>] defaults to
40
+ inventory for safe hosts; all|compute|kubernetes|data|network|storage|billing|vm-deploy|k8s-deploy.
41
+ [--hermes-home <dir>] overrides $HERMES_HOME for Hermes.
42
+ [--npx] use the npx form in Claude .mcp.json.
32
43
  compile-skill --canonical <dir> --target <t> --out <dir>
33
44
  Generate a target dialect of the canonical skill tree (ADR-006). The
34
45
  canonical corpus is NOT shipped here; point --canonical at the skill/
@@ -53,8 +64,7 @@ function parseArgs(argv) {
53
64
  }
54
65
 
55
66
  function jsonRpcProbe(binPath, timeoutMs = 20_000) {
56
- const req = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }) + '\n';
57
- const r = spawnSync(binPath, ['mcp'], { input: req, encoding: 'utf8', timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 });
67
+ const r = spawnSync(process.execPath, [require('path').join(__dirname, 'probe-child.js'), binPath], { encoding: 'utf8', timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 });
58
68
  if (r.error) return { error: String(r.error.message || r.error) };
59
69
  const line = String(r.stdout || '').split('\n').find((l) => l.trim().startsWith('{'));
60
70
  if (!line) return { error: 'engine produced no JSON-RPC response line' };
@@ -76,7 +86,7 @@ function cmdResolve(flags, write) {
76
86
  write(`source ${r.source}`);
77
87
  write(`sha256 ${r.sha256}`);
78
88
  write(`pinned ${r.pinned || '<no pin for ' + r.key + '>'}`);
79
- write(`verified ${r.verified ? 'yes — matches the pinned baseline' : 'NO — not the pinned baseline (tolerated for env/config paths)'}`);
89
+ write(`verified ${r.verified ? 'yes — matches the pinned baseline' : 'NO — not the pinned baseline (resolution only; MCP launch refused)'}`);
80
90
  return 0;
81
91
  }
82
92
 
@@ -87,7 +97,11 @@ function cmdSelfTest(flags, write) {
87
97
  const r = resolveEngine();
88
98
  if (r.error) { step('resolve', false, r.error.split('\n')[0]); if (flags.has('json')) write(JSON.stringify(out, null, 2)); return 1; }
89
99
  step('resolve', true, `${r.path} (source: ${r.source})`);
90
- step('sha256', true, `${r.sha256}${r.verified ? ' — MATCHES pinned baseline' : ' — no pin match (dev build?)'}`);
100
+ step('sha256', r.verified, `${r.sha256}${r.verified ? ' — MATCHES pinned baseline' : ' — DOES NOT MATCH pinned baseline'}`);
101
+ if (!r.verified) {
102
+ if (flags.has('json')) write(JSON.stringify(out, null, 2));
103
+ return 1;
104
+ }
91
105
 
92
106
  const ver = spawnSync(r.path, ['version'], { encoding: 'utf8', timeout: 10_000 });
93
107
  const verOk = ver.status === 0;
@@ -97,13 +111,13 @@ function cmdSelfTest(flags, write) {
97
111
  if (probe.error) { step('tools-list', false, probe.error); if (flags.has('json')) write(JSON.stringify(out, null, 2)); return 1; }
98
112
  const names = probe.tools.map((t) => t.name);
99
113
  const snap = loadSnapshot();
100
- step('tools-list', true, `${names.length} tools live (snapshot: ${snap.toolCount})`);
114
+ step('tools-list', names.length === snap.toolCount, `${names.length} tools live (snapshot: ${snap.toolCount})`);
101
115
 
102
116
  const gaps = coverageGaps(names);
103
- const gapsOk = gaps.unknown.length === 0;
117
+ const gapsOk = gaps.unknown.length === 0 && gaps.missing.length === 0;
104
118
  step('classification-coverage', gapsOk, gapsOk
105
119
  ? `every live tool is classified (${Object.keys(loadClassification().tools).length} golden entries)${gaps.missing.length ? `; ${gaps.missing.length} classified tool(s) absent live: ${gaps.missing.join(', ')}` : ''}`
106
- : `UNCLASSIFIED live tools (deny-by-default — update data/tools-classification.json): ${gaps.unknown.join(', ')}`);
120
+ : `tool drift: unclassified=${gaps.unknown.join(', ') || '<none>'}; missing=${gaps.missing.join(', ') || '<none>'}`);
107
121
 
108
122
  out.ok = out.steps.every((s) => s.ok);
109
123
  if (flags.has('json')) write(JSON.stringify(out, null, 2));
@@ -114,6 +128,10 @@ function cmdSelfTest(flags, write) {
114
128
  function cmdMcp() {
115
129
  const r = resolveEngine();
116
130
  if (r.error) { process.stderr.write(r.error + '\n'); return 1; }
131
+ if (!r.verified) {
132
+ process.stderr.write(`cloudru-hub mcp: engine sha256 ${r.sha256} does not match pinned ${r.pinned || '<none>'}; refusing to launch\n`);
133
+ return 1;
134
+ }
117
135
  const child = spawn(r.path, ['mcp'], { stdio: 'inherit' });
118
136
  child.on('exit', (code, signal) => process.exit(signal ? 1 : code === null ? 1 : code));
119
137
  child.on('error', (e) => { process.stderr.write('cloudru-hub mcp: ' + e.message + '\n'); process.exit(1); });
@@ -139,10 +157,40 @@ function main(argv, write = (s) => process.stdout.write(s + '\n')) {
139
157
  const rc = cmdMcp();
140
158
  return rc === null ? null : rc;
141
159
  }
160
+ case 'mcp-safe':
161
+ return runSafeMcp({ toolsets: opts.get('toolsets') || 'all' });
162
+ case 'approve': {
163
+ const amount = Number(opts.get('max-monthly-rub'));
164
+ if (!opts.get('plan-id') || !opts.has('max-monthly-rub') || !Number.isFinite(amount) || amount < 0) {
165
+ write('cloudru-hub approve: --plan-id and non-negative --max-monthly-rub are required');
166
+ return 1;
167
+ }
168
+ try {
169
+ const approval = issueApproval(opts.get('plan-id'), amount, process.env, Date.now(), opts.get('site-sha256') || '');
170
+ write(flags.has('json') ? JSON.stringify(approval, null, 2) :
171
+ `approved ${approval.planId}: ${approval.vmName}, estimated ${approval.estimatedMonthlyRub} RUB/month (cap ${approval.maxMonthlyRub}); expires ${new Date(approval.expiresAt).toISOString()}${approval.staticSite ? `; static source ${approval.staticSite.source}, SHA-256 ${approval.staticSite.sha256} (${approval.staticSite.sizeBytes} bytes, ${approval.staticSite.files.length} files), ${approval.staticSite.ingress}; ${approval.staticSite.estimateScope}` : ''}`);
172
+ return 0;
173
+ } catch (error) {
174
+ write(`cloudru-hub approve: ${error.message}`);
175
+ return 1;
176
+ }
177
+ }
178
+ case 'k8s-approve': {
179
+ try {
180
+ const approval = issueK8sApproval(opts.get('cluster-id'), opts.get('namespace'),
181
+ opts.get('manifest'), opts.get('manifest-sha256'));
182
+ write(flags.has('json') ? JSON.stringify(approval, null, 2) :
183
+ `approved ${approval.approvalId}: cluster ${approval.clusterId}, namespace ${approval.namespace}, SHA-256 ${approval.manifestSha256}; expires ${new Date(approval.expiresAt).toISOString()}`);
184
+ return 0;
185
+ } catch (error) {
186
+ write(`cloudru-hub k8s-approve: ${error.message}`);
187
+ return 1;
188
+ }
189
+ }
142
190
  case 'install': {
143
191
  const target = opts.get('target');
144
192
  if (!target) { write('cloudru-hub install: --target is required (claude-code|codex|agents-md|gemini|…)'); return 1; }
145
- const res = install(target, opts.get('dir') || process.cwd(), { npx: flags.has('npx') });
193
+ const res = install(target, opts.get('dir') || process.cwd(), { npx: flags.has('npx'), toolsets: opts.get('toolsets') || 'inventory', hermesHome: opts.get('hermes-home') });
146
194
  for (const line of res.lines) write(line);
147
195
  return res.code;
148
196
  }