huaweicloud-devkit 1.1.4 → 1.1.6-next.0
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +22 -1
- package/plugins/huaweicloud-core/src/mcp-protocol.mjs +7 -19
- package/plugins/huaweicloud-core/src/mcp-server-remote.mjs +1 -1
- package/plugins/huaweicloud-core/src/mcp-server.mjs +1 -1
- package/plugins/huaweicloud-core/src/officeace-paths.mjs +51 -0
- package/plugins/huaweicloud-core/src/risk-rule-engine.mjs +43 -1
- package/plugins/huaweicloud-core/src/safety-policy.mjs +86 -3
- package/plugins/huaweicloud-core/src/setup-cli.mjs +62 -24
- package/plugins/huaweicloud-core/src/tools.mjs +17 -2
- package/plugins/huaweicloud-core/src/update-check.mjs +19 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/huaweicloud-devkit)
|
|
7
|
-
[](https://github.com/huaweicloud/huaweicloud-devkit)
|
|
8
8
|
|
|
9
9
|
**[中文](README.zh-CN.md) | English**
|
|
10
10
|
|
package/README.zh-CN.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/huaweicloud-devkit)
|
|
7
|
-
[](https://github.com/huaweicloud/huaweicloud-devkit)
|
|
8
8
|
|
|
9
9
|
**中文 | [English](README.md)**
|
|
10
10
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
3
|
"mcpName": "io.github.huaweicloud/huaweicloud-devkit",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.6-next.0",
|
|
5
5
|
"kooCliVersion": "7.2.12",
|
|
6
6
|
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
7
7
|
"type": "module",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"mcpServers": "./.mcp.json",
|
|
18
18
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
19
19
|
"skills": "./skills/",
|
|
20
|
-
"version": "1.1.
|
|
20
|
+
"version": "1.1.6-next.0",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "HuaweiCloud Mate",
|
|
23
23
|
"url": "https://github.com/huaweicloud"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"mcpServers": "./.mcp.json",
|
|
21
21
|
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
22
22
|
"skills": "./skills/",
|
|
23
|
-
"version": "1.1.
|
|
23
|
+
"version": "1.1.6-next.0",
|
|
24
24
|
"author": {
|
|
25
25
|
"name": "HuaweiCloud Mate",
|
|
26
26
|
"url": "https://github.com/huaweicloud"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"mcpServers": "./.mcp.json",
|
|
18
18
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
19
19
|
"skills": "./skills/",
|
|
20
|
-
"version": "1.1.
|
|
20
|
+
"version": "1.1.6-next.0",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "HuaweiCloud Mate",
|
|
23
23
|
"url": "https://github.com/huaweicloud"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6-next.0",
|
|
4
4
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "HuaweiCloud Mate",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6-next.0",
|
|
4
4
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "HuaweiCloud Mate",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
3
|
"id": "huaweicloud-devkit",
|
|
4
4
|
"displayName": "HuaweiCloud DevKit",
|
|
5
|
-
"version": "1.1.
|
|
5
|
+
"version": "1.1.6-next.0",
|
|
6
6
|
"family": "bundle-plugin",
|
|
7
7
|
"bundleFormat": "codex",
|
|
8
8
|
"description": "Guide coding agents to use Huawei Cloud safely — KooCLI, APIs, SDKs, 28 MCP tools, skills, and safety guardrails.",
|
|
@@ -8,6 +8,24 @@ version: 1
|
|
|
8
8
|
|
|
9
9
|
**STOP - Do not answer from general knowledge.** Follow the procedure below.
|
|
10
10
|
|
|
11
|
+
## Target-Selection Gate (MANDATORY — do not skip)
|
|
12
|
+
|
|
13
|
+
When the developer asks to **deploy / host / publish / 上线 / 部署 / 发布 / 搭建** a website, web app, or static site WITHOUT naming a specific target, you MUST present the target options and obtain an explicit choice BEFORE calling any sandbox lifecycle tool (`huaweicloud_sandbox_check_user`, `huaweicloud_sandbox_connect`, `huaweicloud_sandbox_upload_project`, `huaweicloud_sandbox_deploy_nginx`, etc.).
|
|
14
|
+
|
|
15
|
+
Use the `question` tool and present, in this exact order:
|
|
16
|
+
|
|
17
|
+
1. **沙箱 (Sandbox)** — temporary runtime, instant preview URL (recommended for quick preview)
|
|
18
|
+
2. **OBS 静态托管** — long-term static hosting + CDN + custom domain
|
|
19
|
+
3. **ECS** — server-based hosting
|
|
20
|
+
4. **CCE** — container-based hosting
|
|
21
|
+
|
|
22
|
+
Hard rules:
|
|
23
|
+
|
|
24
|
+
- **NEVER infer or default the target** when it is unstated. Calling `huaweicloud_sandbox_connect` before this choice is made is a violation.
|
|
25
|
+
- Only skip the question when: (a) the developer already named a target (e.g. "部署到 ECS", "用 OBS"), or (b) the developer explicitly asked for a temporary / sandbox / preview runtime.
|
|
26
|
+
- "部署到华为云" / "deploy to Huawei Cloud" / "上线" alone is NOT a named target — still run the gate.
|
|
27
|
+
- After the choice: sandbox → continue with this skill; OBS → `huawei-obs`; ECS → `huawei-ecs` / `huawei-iac`; CCE → `huawei-cce`.
|
|
28
|
+
|
|
11
29
|
## Overview
|
|
12
30
|
|
|
13
31
|
Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace terminal execution. Covers sandbox lifecycle via hdkitservice API and remote terminal command execution via hwlink protocol.
|
|
@@ -15,7 +33,7 @@ Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace t
|
|
|
15
33
|
## Activation
|
|
16
34
|
|
|
17
35
|
- **Proactive offering**: when the developer's task needs a temporary runtime (e.g. "deploy this web app", "run this app and preview it"), offer the sandbox proactively — the developer never has to say "use sandbox". Prompt: "This task can be satisfied by a sandbox — use it?"
|
|
18
|
-
- **Static website hosting — present options, sandbox first**: when the developer asks to deploy/host/publish a static website or web app without naming a target, do NOT silently pick a service.
|
|
36
|
+
- **Static website hosting — present options, sandbox first (MANDATORY)**: when the developer asks to deploy/host/publish a static website or web app without naming a target, do NOT silently pick a service and do NOT default to the sandbox. Run the [Target-Selection Gate](#target-selection-gate-mandatory--do-not-skip) via the `question` tool first. Present, in order: ① 沙箱 (recommended — temporary runtime, instant preview URL) ② OBS 静态托管 (long-term static hosting/CDN) ③ ECS ④ CCE. Recommended prompt: "建议优先部署到沙箱(临时运行环境,可预览访问),也可选择 OBS 静态托管、ECS 或 CCE,你想部署到哪里?" Only if the developer, after being asked, expresses no preference may you proceed with the sandbox.
|
|
19
37
|
- **Detect web apps in deploy-a-repo prompts**: prompts like "部署 GitHub - <owner>/<repo>" or "deploy <github repo>" do not say "web app". Clone or inspect the repository first (package.json with dev/serve/build scripts, index.html, frontend framework dependencies such as vue/react/angular/vite, static sites, Dockerfile running a web server). If it is a web application, offer the sandbox first: "检测到该项目是 Web 应用,建议优先使用沙箱部署(临时运行环境,可预览访问),是否使用?"
|
|
20
38
|
- **Clone into the sandbox workspace directory**: always put project code under `/workspace/<repo-name>` (create the directory if missing) — `/workspace` is the sandbox's dedicated workspace mount at the filesystem root, not `$HOME/workspace`. Never use `/tmp` or other ephemeral locations. This keeps the project with the sandbox session, is easy to reference for serving/exposing, and survives session-level restarts of the shell.
|
|
21
39
|
- **Deployment must end with a public URL**: after deploying and exposing the app with DevBridge, always return the tunnel URL to the developer as the final result — a deployment without an accessible link is incomplete.
|
|
@@ -86,6 +104,8 @@ Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace t
|
|
|
86
104
|
|
|
87
105
|
## Workflow
|
|
88
106
|
|
|
107
|
+
**Step 0 — Target-Selection Gate (MANDATORY)**: before any sandbox lifecycle tool, confirm the target. If the developer asked to deploy/host/publish a site without naming a target, STOP and run the [Target-Selection Gate](#target-selection-gate-mandatory--do-not-skip) first; do NOT call `huaweicloud_sandbox_connect` until it is resolved. Proceed with this workflow only if the target is already confirmed as sandbox (or a temporary runtime).
|
|
108
|
+
|
|
89
109
|
Setup is a **plugin-side preflight** — the developer should be asked a question only once, when the agreement actually needs signing:
|
|
90
110
|
|
|
91
111
|
1. **Check user** (transparent): `huaweicloud_sandbox_check_user` — returns `realnameVerified`/`agreementSigned` (200) when all good, OR throws a 403 error with one of these codes:
|
|
@@ -836,6 +856,7 @@ Returns `complete: true/false`, `score`, and `nextStep` to fix missing items.
|
|
|
836
856
|
|
|
837
857
|
| Trap | Why |
|
|
838
858
|
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
859
|
+
| Target not confirmed | "部署到华为云" without a named target is NOT a go signal. You MUST run the Target-Selection Gate and get an explicit choice before calling any sandbox lifecycle tool. Skipping it and defaulting to the sandbox is a violation. |
|
|
839
860
|
| Agreement required first | `sandbox_connect` fails if the agreement isn't signed; the `sandbox_check_user` preflight detects this, so surface it to the developer only when signing is needed |
|
|
840
861
|
| Real-name required | `sandbox_connect` fails if `realnameVerified=false`; tell the developer once and stop, don't loop on connect |
|
|
841
862
|
| Never expose tunnel details | Do not mention "DevBridge"/"tunnel"/"devbridge" to the developer — say "正在生成访问地址..." and hand over only the URL |
|
|
@@ -1,25 +1,9 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { resolve, dirname, join } from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
|
|
5
1
|
import { TOOL_DEFINITIONS, callTool } from './tools.mjs';
|
|
6
|
-
import { peekCachedUpdateInfo, applyUpdateHint } from './update-check.mjs';
|
|
2
|
+
import { peekCachedUpdateInfo, applyUpdateHint, readInstalledVersion } from './update-check.mjs';
|
|
7
3
|
import { initTelemetry } from './telemetry/telemetry.mjs';
|
|
8
4
|
import { detectAgent } from './telemetry/agent-detect.mjs';
|
|
9
5
|
|
|
10
|
-
const
|
|
11
|
-
const pluginRoot = resolve(__dirname, '..');
|
|
12
|
-
const packageRoot = resolve(pluginRoot, '..', '..');
|
|
13
|
-
let pkgVersion = '0.0.0';
|
|
14
|
-
for (const base of [pluginRoot, packageRoot]) {
|
|
15
|
-
try {
|
|
16
|
-
const version = JSON.parse(readFileSync(join(base, 'package.json'), 'utf8')).version;
|
|
17
|
-
if (version) {
|
|
18
|
-
pkgVersion = version;
|
|
19
|
-
break;
|
|
20
|
-
}
|
|
21
|
-
} catch {}
|
|
22
|
-
}
|
|
6
|
+
const pkgVersion = readInstalledVersion() || '0.0.0';
|
|
23
7
|
|
|
24
8
|
// 会话内首个非 check/upgrade 工具调用附加 _updateInfo,只消费一次。
|
|
25
9
|
// 按会话隔离:同进程内不同会话(A/B)各自首次提示;stdio 用固定 'stdin'。
|
|
@@ -92,5 +76,9 @@ export async function dispatch(method, params, opts = {}) {
|
|
|
92
76
|
return { resources: [] };
|
|
93
77
|
}
|
|
94
78
|
|
|
95
|
-
|
|
79
|
+
// JSON-RPC 2.0: unknown methods must surface as -32601 (Method not found),
|
|
80
|
+
// not the generic -32603 internal error (#650 D9-2).
|
|
81
|
+
const methodError = new Error(`Method not found: ${method}`);
|
|
82
|
+
methodError.code = -32601;
|
|
83
|
+
throw methodError;
|
|
96
84
|
}
|
|
@@ -60,7 +60,7 @@ export async function startRemoteServer({ port = DEFAULT_PORT, host = DEFAULT_HO
|
|
|
60
60
|
response = {
|
|
61
61
|
jsonrpc: '2.0',
|
|
62
62
|
id: message.id,
|
|
63
|
-
error: { code: -32603, message: error.message },
|
|
63
|
+
error: { code: Number.isSafeInteger(error.code) ? error.code : -32603, message: error.message },
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Shared OfficeAce config-root resolution used by both the installer
|
|
2
|
+
// (setup-cli.mjs) and the MCP runtime (tools.mjs), so skills are installed
|
|
3
|
+
// to and read from the same location (#559).
|
|
4
|
+
//
|
|
5
|
+
// OfficeAce exposes no API for the plugin to query the current user or
|
|
6
|
+
// install directory, so the root is located by probing. The probe result is
|
|
7
|
+
// persisted in a marker file so a non-standard directory (entered via prompt,
|
|
8
|
+
// whose OFFICE_CLAW_CONFIG_ROOT env does not survive across processes) is
|
|
9
|
+
// still found by later uninstall/update runs.
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
12
|
+
import { join, resolve, dirname } from 'node:path';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
|
|
15
|
+
function markerPath() {
|
|
16
|
+
return join(process.env.HUAWEICLOUD_HOME || homedir(), '.config', 'huaweicloud', 'devkit-officeace-root.json');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sqliteSibling(dir) {
|
|
20
|
+
return join(resolve(dir, '..'), 'data', 'mcp-connectors.sqlite');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// A directory is a usable OfficeAce config root when it exists and carries
|
|
24
|
+
// OfficeAce evidence — capabilities.json, the connector DB, or DevKit's own
|
|
25
|
+
// installed artifacts. Deliberately more lenient than "capabilities.json must
|
|
26
|
+
// exist" so uninstall/update still find the root after it was removed.
|
|
27
|
+
export function isUsableOfficeaceRoot(dir) {
|
|
28
|
+
if (!dir || !existsSync(dir)) return false;
|
|
29
|
+
return (
|
|
30
|
+
existsSync(join(dir, 'capabilities.json')) ||
|
|
31
|
+
existsSync(sqliteSibling(dir)) ||
|
|
32
|
+
existsSync(join(dir, 'skills')) ||
|
|
33
|
+
existsSync(join(dir, 'huaweicloud-plugins'))
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function readOfficeaceRootMarker() {
|
|
38
|
+
try {
|
|
39
|
+
const marker = JSON.parse(readFileSync(markerPath(), 'utf8'));
|
|
40
|
+
return isUsableOfficeaceRoot(marker.root) ? marker.root : null;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function writeOfficeaceRootMarker(root) {
|
|
47
|
+
try {
|
|
48
|
+
mkdirSync(dirname(markerPath()), { recursive: true });
|
|
49
|
+
writeFileSync(markerPath(), JSON.stringify({ root, savedAt: new Date().toISOString() }, null, 2));
|
|
50
|
+
} catch {}
|
|
51
|
+
}
|
|
@@ -108,15 +108,57 @@ function evaluate(stage, inputs, options = {}) {
|
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
function isNonEmptyString(value) {
|
|
112
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isNonEmptyContainer(value) {
|
|
116
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
117
|
+
if (value && typeof value === 'object') return Object.keys(value).length > 0;
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// A risk check that could not inspect its input must never report "allow"
|
|
122
|
+
// (fail-open) — it reports deny with an `invalid` risk and a synthetic finding
|
|
123
|
+
// so the reason is visible through the tool response (#564).
|
|
124
|
+
function invalidRiskResult(stage, reason) {
|
|
125
|
+
return {
|
|
126
|
+
decision: 'deny',
|
|
127
|
+
risk: 'invalid',
|
|
128
|
+
findings: [
|
|
129
|
+
{
|
|
130
|
+
ruleId: 'invalid-input',
|
|
131
|
+
title: 'Invalid input for risk check',
|
|
132
|
+
category: 'invalid',
|
|
133
|
+
severity: 'deny',
|
|
134
|
+
message: reason,
|
|
135
|
+
remediation: 'Provide a valid, non-empty input before re-running the check.',
|
|
136
|
+
source: stage,
|
|
137
|
+
evidence: '',
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
111
143
|
export function evaluateCommandRisk(command, options = {}) {
|
|
144
|
+
if (!isNonEmptyString(command)) {
|
|
145
|
+
return invalidRiskResult('command', 'command must be a non-empty string.');
|
|
146
|
+
}
|
|
112
147
|
return evaluate('command', { command }, options);
|
|
113
148
|
}
|
|
114
149
|
|
|
115
150
|
export function evaluateArtifacts(artifacts, options = {}) {
|
|
116
|
-
|
|
151
|
+
if (!Array.isArray(artifacts) || artifacts.length === 0) {
|
|
152
|
+
return invalidRiskResult('artifact', 'artifacts must be a non-empty array.');
|
|
153
|
+
}
|
|
154
|
+
return evaluate('artifact', artifacts, options);
|
|
117
155
|
}
|
|
118
156
|
|
|
119
157
|
export function evaluateDeployPlan(plan, options = {}) {
|
|
158
|
+
const valid = isNonEmptyString(plan) || (plan != null && typeof plan === 'object' && isNonEmptyContainer(plan));
|
|
159
|
+
if (!valid) {
|
|
160
|
+
return invalidRiskResult('deploy_plan', 'plan must be a non-empty object, array, or string.');
|
|
161
|
+
}
|
|
120
162
|
return evaluate('deploy_plan', { plan }, options);
|
|
121
163
|
}
|
|
122
164
|
|
|
@@ -66,11 +66,38 @@ export function redactSecrets(value, policy = DEFAULT_POLICY) {
|
|
|
66
66
|
|
|
67
67
|
function stripExecutable(args) {
|
|
68
68
|
if (!args.length) return [];
|
|
69
|
-
|
|
69
|
+
let current = args;
|
|
70
|
+
// Unwrap shell wrappers (bash -c / sh -c 'hcloud ...', sudo hcloud ...) so
|
|
71
|
+
// wrapped write commands keep their deny classification (#650 D4-16).
|
|
72
|
+
for (let depth = 0; depth < 5; depth++) {
|
|
73
|
+
const first = String(current[0]).toLowerCase();
|
|
74
|
+
const isShell = [
|
|
75
|
+
'bash',
|
|
76
|
+
'sh',
|
|
77
|
+
'zsh',
|
|
78
|
+
'dash',
|
|
79
|
+
'bash.exe',
|
|
80
|
+
'sh.exe',
|
|
81
|
+
'/bin/bash',
|
|
82
|
+
'/bin/sh',
|
|
83
|
+
'/bin/zsh',
|
|
84
|
+
'/bin/dash',
|
|
85
|
+
].includes(first);
|
|
86
|
+
if (isShell && String(current[1]).toLowerCase() === '-c' && current[2]) {
|
|
87
|
+
current = splitSimpleCommand(current[2]);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (first === 'sudo' && current.length > 1) {
|
|
91
|
+
current = current.slice(1);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
const first = String(current[0]).toLowerCase();
|
|
70
97
|
if (first === 'hcloud' || first.endsWith('/hcloud') || first.endsWith('\\hcloud') || first === 'hcloud.exe') {
|
|
71
|
-
return
|
|
98
|
+
return current.slice(1);
|
|
72
99
|
}
|
|
73
|
-
return
|
|
100
|
+
return current;
|
|
74
101
|
}
|
|
75
102
|
|
|
76
103
|
function commandOperation(args) {
|
|
@@ -123,8 +150,44 @@ function applyRawCommandRiskRules(base, command, options = {}) {
|
|
|
123
150
|
return mergeRiskDecision(base, risk);
|
|
124
151
|
}
|
|
125
152
|
|
|
153
|
+
// Find hcloud command segments split by shell operators (; && || |) so a write
|
|
154
|
+
// command in the middle of a concatenated string keeps its deny classification
|
|
155
|
+
// (#650 review edge 1).
|
|
156
|
+
function findHcloudCommandSegments(text) {
|
|
157
|
+
return String(text)
|
|
158
|
+
.split(/(?:\|\||&&|;|\|)/)
|
|
159
|
+
.map((segment) => segment.trim())
|
|
160
|
+
.filter(Boolean)
|
|
161
|
+
.map((segment) => splitSimpleCommand(segment))
|
|
162
|
+
.filter((tokens) => {
|
|
163
|
+
const first = String(tokens[0] || '').toLowerCase();
|
|
164
|
+
return first === 'hcloud' || first.endsWith('/hcloud') || first.endsWith('\\hcloud') || first === 'hcloud.exe';
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
126
168
|
export function classifyHcloudArgs(args, options = {}) {
|
|
127
169
|
const policy = options.policy || DEFAULT_POLICY;
|
|
170
|
+
// Second pass for shell-wrapped input (#650 D4-16 review edge 1): when the
|
|
171
|
+
// leading command is not hcloud but a concatenated segment contains one,
|
|
172
|
+
// classify every hcloud segment and merge to the most severe decision.
|
|
173
|
+
const unwrappedTokens = stripExecutable(Array.isArray(args) ? args.map(String) : []);
|
|
174
|
+
const unwrappedFirst = String(unwrappedTokens[0] || '').toLowerCase();
|
|
175
|
+
const unwrappedIsHcloud =
|
|
176
|
+
unwrappedFirst === 'hcloud' ||
|
|
177
|
+
unwrappedFirst.endsWith('/hcloud') ||
|
|
178
|
+
unwrappedFirst.endsWith('\\hcloud') ||
|
|
179
|
+
unwrappedFirst === 'hcloud.exe';
|
|
180
|
+
if (!unwrappedIsHcloud && !options._segmentDepth) {
|
|
181
|
+
const hcloudSegments = findHcloudCommandSegments(unwrappedTokens.join(' '));
|
|
182
|
+
if (hcloudSegments.length > 0) {
|
|
183
|
+
const results = hcloudSegments.map((segment) => classifyHcloudArgs(segment, { ...options, _segmentDepth: 1 }));
|
|
184
|
+
return (
|
|
185
|
+
results.find((result) => result.decision === 'deny') ||
|
|
186
|
+
results.find((result) => ['write', 'execution', 'secret', 'credential'].includes(result.risk)) ||
|
|
187
|
+
results[0]
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
128
191
|
const { service, operation, args: normalizedArgs } = commandOperation(args);
|
|
129
192
|
const joined = normalizedArgs.join(' ');
|
|
130
193
|
|
|
@@ -342,6 +405,26 @@ export function classifyTextCommand(command, options = {}) {
|
|
|
342
405
|
};
|
|
343
406
|
}
|
|
344
407
|
|
|
408
|
+
// Credential variable references bypass the env-command gate above: HW_ is
|
|
409
|
+
// the plugin's own documented credential prefix (HW_ACCESS_KEY/HW_SECRET_KEY/
|
|
410
|
+
// HW_SECURITY_TOKEN), and `echo $HW_SECRET_KEY` / `printenv HW_ACCESS_KEY`
|
|
411
|
+
// previously fell through to allow (#650 D4-2).
|
|
412
|
+
//
|
|
413
|
+
// The negative lookbehind exempts literal-NAME references — backslash-escaped
|
|
414
|
+
// (`\$HW_*`) or single-quoted (`'$HW_*'`, which the shell never expands) —
|
|
415
|
+
// while unescaped `$HW_*` is a potential expansion/dump regardless of the
|
|
416
|
+
// command. No command-name whitelist, so no false negative (#650 review).
|
|
417
|
+
if (
|
|
418
|
+
/(?<!['\\])\$\{?(?:HUAWEICLOUD|HWC|HW|OS)_(?:ACCESS_KEY|SECRET_KEY|SECURITY_TOKEN)/i.test(text) ||
|
|
419
|
+
/(?:^|\s)printenv\s+(?:HUAWEICLOUD|HWC|HW|OS)_(?:ACCESS_KEY|SECRET_KEY|SECURITY_TOKEN)/i.test(text)
|
|
420
|
+
) {
|
|
421
|
+
return {
|
|
422
|
+
decision: 'deny',
|
|
423
|
+
risk: 'credential',
|
|
424
|
+
reason: 'Printing cloud credential environment variables is blocked.',
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
345
428
|
if (/(^|\s)hcloud(\.exe)?\s+/i.test(text)) {
|
|
346
429
|
return classifyHcloudArgs(splitSimpleCommand(text), { ...options, rawCommand: text });
|
|
347
430
|
}
|
|
@@ -37,7 +37,8 @@ import {
|
|
|
37
37
|
} from './proxy/proxy-config.mjs';
|
|
38
38
|
import { removeKooCli, removeObsConfig } from './sandbox/uninstall-cleanup.mjs';
|
|
39
39
|
import { mergeCommandStyle, mergeArgsStyle, extractUserDelta, applyUserDelta } from './mcp-config-merge.mjs';
|
|
40
|
-
import { saveAgentDelta, takeAgentDelta, purgeBackup } from './mcp-config-backup.mjs';
|
|
40
|
+
import { readAgentDelta, saveAgentDelta, takeAgentDelta, purgeBackup } from './mcp-config-backup.mjs';
|
|
41
|
+
import { isUsableOfficeaceRoot, readOfficeaceRootMarker, writeOfficeaceRootMarker } from './officeace-paths.mjs';
|
|
41
42
|
import { queryDistTagsFetch, determineTarget, semverCompare } from './update-check.mjs';
|
|
42
43
|
import { getKooCliVersion, compareVersion, kooCliDownloadBase, KOO_CLI_BASE } from './koocli-version.mjs';
|
|
43
44
|
import { findHcloudBin, hcloudProbeNextStep, probeHcloud } from './hcloud-probe.mjs';
|
|
@@ -185,11 +186,13 @@ function readOfficeaceRegistryInstallDir() {
|
|
|
185
186
|
|
|
186
187
|
function officeaceCapabilitiesDir() {
|
|
187
188
|
const configRoot = process.env.OFFICE_CLAW_CONFIG_ROOT;
|
|
188
|
-
if (
|
|
189
|
+
if (isUsableOfficeaceRoot(configRoot)) return configRoot;
|
|
190
|
+
const markerRoot = readOfficeaceRootMarker();
|
|
191
|
+
if (markerRoot) return markerRoot;
|
|
189
192
|
const regDir = readOfficeaceRegistryInstallDir();
|
|
190
193
|
if (regDir) {
|
|
191
194
|
const dir = join(regDir, '.office-claw');
|
|
192
|
-
if (
|
|
195
|
+
if (isUsableOfficeaceRoot(dir)) return dir;
|
|
193
196
|
}
|
|
194
197
|
if (platform() === 'win32') {
|
|
195
198
|
const bases = [process.env.ProgramFiles, 'C:\\Program Files', 'D:\\Program Files'];
|
|
@@ -197,7 +200,7 @@ function officeaceCapabilitiesDir() {
|
|
|
197
200
|
for (const base of bases) {
|
|
198
201
|
if (!base) continue;
|
|
199
202
|
const dir = join(base, 'OfficeAce', '.office-claw');
|
|
200
|
-
if (
|
|
203
|
+
if (isUsableOfficeaceRoot(dir)) return dir;
|
|
201
204
|
}
|
|
202
205
|
}
|
|
203
206
|
return null;
|
|
@@ -242,7 +245,7 @@ function ensureOfficeaceMcpInSqlite() {
|
|
|
242
245
|
if (!existsSync(dbPath)) {
|
|
243
246
|
console.log(` \x1b[31mOfficeAce database not found: ${dbPath}\x1b[0m`);
|
|
244
247
|
console.log(` \x1b[33mPlease ensure OfficeAce is installed and has been launched at least once.\x1b[0m`);
|
|
245
|
-
return
|
|
248
|
+
return 'db-missing';
|
|
246
249
|
}
|
|
247
250
|
|
|
248
251
|
const mcpPath = join(officeacePluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
|
|
@@ -281,21 +284,27 @@ function ensureOfficeaceMcpInSqlite() {
|
|
|
281
284
|
if (existing.command === 'node' && existing.args_json === nextArgsJson && existing.env_json === nextEnvJson) {
|
|
282
285
|
console.log(` MCP config unchanged: ${dbPath}`);
|
|
283
286
|
db.close();
|
|
284
|
-
return
|
|
287
|
+
return 'ok';
|
|
285
288
|
}
|
|
286
289
|
db.prepare(
|
|
287
290
|
'UPDATE mcp_connectors SET command = ?, args_json = ?, env_json = ?, updated_at = ?, status = ?, enabled = 1 WHERE id = ?',
|
|
288
291
|
).run('node', nextArgsJson, nextEnvJson, now, 'disconnected', existing.id);
|
|
289
292
|
console.log(` MCP config merged (user fields preserved): ${dbPath}`);
|
|
290
293
|
} else {
|
|
291
|
-
|
|
294
|
+
// Restore user identity + args saved by a previous uninstall (#559).
|
|
295
|
+
// owner_user_id has no queryable source: we copy it from an existing
|
|
296
|
+
// row, and fall back to the value backed up before a prior uninstall so
|
|
297
|
+
// an emptied table does not make registration permanently impossible.
|
|
298
|
+
const officeaceDelta = readAgentDelta('officeace');
|
|
299
|
+
const ownerUserId = officeaceGetOwnerUserId() || (officeaceDelta && officeaceDelta.ownerUserId);
|
|
292
300
|
if (!ownerUserId) {
|
|
293
|
-
console.log(` \x1b[31mCannot determine owner_user_id
|
|
301
|
+
console.log(` \x1b[31mCannot determine owner_user_id for the huaweicloud-devkit connector.\x1b[0m`);
|
|
302
|
+
console.log(
|
|
303
|
+
` \x1b[33mThe OfficeAce connector database has no rows to copy the user id from and no prior registration is backed up. Open OfficeAce, add/enable any connector once, then re-run the install.\x1b[0m`,
|
|
304
|
+
);
|
|
294
305
|
db.close();
|
|
295
|
-
return
|
|
306
|
+
return 'owner-missing';
|
|
296
307
|
}
|
|
297
|
-
// Restore user args saved by a previous uninstall (issue #615).
|
|
298
|
-
const officeaceDelta = takeAgentDelta('officeace');
|
|
299
308
|
const insertArgsJson =
|
|
300
309
|
officeaceDelta && Array.isArray(officeaceDelta.argsExtra)
|
|
301
310
|
? JSON.stringify([mcpPath, ...officeaceDelta.argsExtra])
|
|
@@ -304,10 +313,12 @@ function ensureOfficeaceMcpInSqlite() {
|
|
|
304
313
|
`INSERT INTO mcp_connectors (id, owner_user_id, type, name, normalized_name, transport, timeout_ms, command, args_json, env_json, enabled, status, created_at, updated_at, version, seeded)
|
|
305
314
|
VALUES (?, ?, 'custom', 'huaweicloud-devkit', 'huaweicloud-devkit', 'stdio', 60000, 'node', ?, ?, 1, 'disconnected', ?, ?, 1, 0)`,
|
|
306
315
|
).run(randomUUID(), ownerUserId, insertArgsJson, envJson, now, now);
|
|
316
|
+
// Consume the backup only after a successful insert so a DB failure keeps it for the next retry.
|
|
317
|
+
takeAgentDelta('officeace');
|
|
307
318
|
console.log(` MCP config created: ${dbPath}`);
|
|
308
319
|
}
|
|
309
320
|
db.close();
|
|
310
|
-
return
|
|
321
|
+
return 'ok';
|
|
311
322
|
} catch (error) {
|
|
312
323
|
if (db) {
|
|
313
324
|
try {
|
|
@@ -315,7 +326,7 @@ function ensureOfficeaceMcpInSqlite() {
|
|
|
315
326
|
} catch {}
|
|
316
327
|
}
|
|
317
328
|
console.log(` \x1b[31mFailed to write MCP config: ${error.message}\x1b[0m`);
|
|
318
|
-
return
|
|
329
|
+
return 'error';
|
|
319
330
|
}
|
|
320
331
|
}
|
|
321
332
|
|
|
@@ -325,17 +336,28 @@ function removeOfficeaceMcpFromSqlite() {
|
|
|
325
336
|
let db;
|
|
326
337
|
try {
|
|
327
338
|
db = openOfficeaceDb();
|
|
328
|
-
// Back up user args before deleting the connector row
|
|
339
|
+
// Back up user identity + args before deleting the connector row:
|
|
340
|
+
// owner_user_id is copied from existing rows on reinstall, so an empty
|
|
341
|
+
// table after this delete would make registration impossible (#559).
|
|
329
342
|
try {
|
|
330
|
-
const row = db
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
343
|
+
const row = db
|
|
344
|
+
.prepare("SELECT owner_user_id, args_json FROM mcp_connectors WHERE name = 'huaweicloud-devkit'")
|
|
345
|
+
.get();
|
|
346
|
+
const delta = {};
|
|
347
|
+
if (row?.owner_user_id) delta.ownerUserId = row.owner_user_id;
|
|
348
|
+
try {
|
|
349
|
+
const parsed = row ? JSON.parse(row.args_json || '[]') : [];
|
|
350
|
+
if (Array.isArray(parsed) && parsed.length > 1) delta.argsExtra = parsed.slice(1);
|
|
351
|
+
} catch {}
|
|
352
|
+
if (Object.keys(delta).length > 0) saveAgentDelta('officeace', delta);
|
|
353
|
+
} catch {}
|
|
354
|
+
// mcp_connector_tools may not exist on minimal installs — deleting it must
|
|
355
|
+
// not abort deleting the connector row itself.
|
|
356
|
+
try {
|
|
357
|
+
db.prepare(
|
|
358
|
+
"DELETE FROM mcp_connector_tools WHERE connector_id IN (SELECT id FROM mcp_connectors WHERE name = 'huaweicloud-devkit')",
|
|
359
|
+
).run();
|
|
335
360
|
} catch {}
|
|
336
|
-
db.prepare(
|
|
337
|
-
"DELETE FROM mcp_connector_tools WHERE connector_id IN (SELECT id FROM mcp_connectors WHERE name = 'huaweicloud-devkit')",
|
|
338
|
-
).run();
|
|
339
361
|
const result2 = db.prepare("DELETE FROM mcp_connectors WHERE name = 'huaweicloud-devkit'").run();
|
|
340
362
|
if (result2.changes > 0) {
|
|
341
363
|
console.log(` MCP config removed: ${dbPath}`);
|
|
@@ -2377,8 +2399,17 @@ async function installOfficeAce() {
|
|
|
2377
2399
|
console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
|
|
2378
2400
|
|
|
2379
2401
|
installRuntimeDeps(pluginDest);
|
|
2380
|
-
ensureOfficeaceMcpInSqlite()
|
|
2402
|
+
if (ensureOfficeaceMcpInSqlite() === 'owner-missing') {
|
|
2403
|
+
throw new Error(
|
|
2404
|
+
'OfficeAce MCP connector registration failed (owner_user_id unavailable). ' +
|
|
2405
|
+
'Open OfficeAce, add/enable any connector once, then re-run install.',
|
|
2406
|
+
);
|
|
2407
|
+
}
|
|
2381
2408
|
registerOfficeaceSkillEntries();
|
|
2409
|
+
// Persist the resolved root so a later uninstall/update (new process, no
|
|
2410
|
+
// OFFICE_CLAW_CONFIG_ROOT env) still targets the same directory (#559).
|
|
2411
|
+
const resolvedRoot = officeaceCapabilitiesDir();
|
|
2412
|
+
if (resolvedRoot) writeOfficeaceRootMarker(resolvedRoot);
|
|
2382
2413
|
}
|
|
2383
2414
|
|
|
2384
2415
|
async function updateOfficeAce() {
|
|
@@ -2395,8 +2426,15 @@ async function updateOfficeAce() {
|
|
|
2395
2426
|
copyDir(safetyDir, join(pluginDest, 'safety'));
|
|
2396
2427
|
console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
|
|
2397
2428
|
installRuntimeDeps(pluginDest);
|
|
2398
|
-
ensureOfficeaceMcpInSqlite()
|
|
2429
|
+
if (ensureOfficeaceMcpInSqlite() === 'owner-missing') {
|
|
2430
|
+
console.log(
|
|
2431
|
+
` \x1b[31m[WARN]\x1b[0m OfficeAce MCP connector registration failed (owner_user_id unavailable). ` +
|
|
2432
|
+
`The connector may stay disconnected until you add/enable any connector once in OfficeAce and re-run update.`,
|
|
2433
|
+
);
|
|
2434
|
+
}
|
|
2399
2435
|
registerOfficeaceSkillEntries();
|
|
2436
|
+
const resolvedRoot = officeaceCapabilitiesDir();
|
|
2437
|
+
if (resolvedRoot) writeOfficeaceRootMarker(resolvedRoot);
|
|
2400
2438
|
mkdirSync(pluginDest, { recursive: true });
|
|
2401
2439
|
writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
|
|
2402
2440
|
}
|
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
upgradePackage,
|
|
64
64
|
} from './update-check.mjs';
|
|
65
65
|
import { hcloudProbeNextStep, probeHcloud } from './hcloud-probe.mjs';
|
|
66
|
+
import { isUsableOfficeaceRoot, readOfficeaceRootMarker } from './officeace-paths.mjs';
|
|
66
67
|
|
|
67
68
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
68
69
|
const SKILLS_ROOT_DEV = join(__dirname, '..', 'skills');
|
|
@@ -104,7 +105,14 @@ function readOfficeaceRegistryInstallDir() {
|
|
|
104
105
|
|
|
105
106
|
function officeaceSkillsRoot() {
|
|
106
107
|
const configRoot = process.env.OFFICE_CLAW_CONFIG_ROOT;
|
|
107
|
-
|
|
108
|
+
// Same lenient probe as the installer (isUsableOfficeaceRoot) so env pointing
|
|
109
|
+
// at "skills present but capabilities.json absent" resolves identically at
|
|
110
|
+
// install and at runtime (#559 review).
|
|
111
|
+
if (configRoot && isUsableOfficeaceRoot(configRoot)) return join(configRoot, 'skills');
|
|
112
|
+
// Same persisted root used by the installer keeps install-time and
|
|
113
|
+
// runtime skill discovery consistent (#559).
|
|
114
|
+
const markerRoot = readOfficeaceRootMarker();
|
|
115
|
+
if (markerRoot) return join(markerRoot, 'skills');
|
|
108
116
|
const regDir = readOfficeaceRegistryInstallDir();
|
|
109
117
|
if (regDir) {
|
|
110
118
|
const dir = join(regDir, '.office-claw', 'skills');
|
|
@@ -2015,8 +2023,15 @@ function explainError({ service = 'unknown', errorCode = '', message = '', reque
|
|
|
2015
2023
|
}
|
|
2016
2024
|
if (!suggestions.length) {
|
|
2017
2025
|
suggestions.push(
|
|
2018
|
-
'
|
|
2026
|
+
'No known error-code pattern matched. Re-run the failing command with --debug to capture ' +
|
|
2027
|
+
'X-Request-Id from the response headers, then re-call explain_error with service, operation, ' +
|
|
2028
|
+
'region, the full redacted message, and requestId.',
|
|
2019
2029
|
);
|
|
2030
|
+
if (errorCode) {
|
|
2031
|
+
suggestions.push(
|
|
2032
|
+
`Search the error code "${errorCode}" in the Huawei Cloud error center (support.huaweicloud.com) for the authoritative cause.`,
|
|
2033
|
+
);
|
|
2034
|
+
}
|
|
2020
2035
|
}
|
|
2021
2036
|
|
|
2022
2037
|
if (requestId) {
|
|
@@ -135,6 +135,19 @@ function selfDir() {
|
|
|
135
135
|
return dirname(fileURLToPath(import.meta.url));
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
// Plugin manifests also carry the version and are kept in sync with package.json
|
|
139
|
+
// by `npm run validate`. Codex marketplace installs (`.codex/plugins/cache/...`)
|
|
140
|
+
// contain only the plugin directory — no package.json — so fall back to these
|
|
141
|
+
// manifests when neither candidate package.json exists (#576).
|
|
142
|
+
const VERSION_MANIFEST_RELS = [
|
|
143
|
+
'.codex-plugin/plugin.json',
|
|
144
|
+
'.claude-plugin/plugin.json',
|
|
145
|
+
'.cursor-plugin/plugin.json',
|
|
146
|
+
'.workbuddy-plugin/plugin.json',
|
|
147
|
+
'.hermes-plugin/plugin.json',
|
|
148
|
+
'openclaw.plugin.json',
|
|
149
|
+
];
|
|
150
|
+
|
|
138
151
|
export function readInstalledVersion() {
|
|
139
152
|
const pluginRoot = resolve(selfDir(), '..');
|
|
140
153
|
const packageRoot = resolve(pluginRoot, '..', '..');
|
|
@@ -144,6 +157,12 @@ export function readInstalledVersion() {
|
|
|
144
157
|
if (typeof version === 'string' && version) return version;
|
|
145
158
|
} catch {}
|
|
146
159
|
}
|
|
160
|
+
for (const rel of VERSION_MANIFEST_RELS) {
|
|
161
|
+
try {
|
|
162
|
+
const version = JSON.parse(readFileSync(join(pluginRoot, rel), 'utf8')).version;
|
|
163
|
+
if (typeof version === 'string' && version) return version;
|
|
164
|
+
} catch {}
|
|
165
|
+
}
|
|
147
166
|
return null;
|
|
148
167
|
}
|
|
149
168
|
|