huaweicloud-devkit 1.1.1-next.14 → 1.1.1-next.16
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/integrations/atomcode/hooks/huaweicloud-telemetry.js +181 -0
- package/integrations/atomcode/hooks.json +9 -0
- package/integrations/dsh/hook-plugin.mjs +148 -0
- package/integrations/hermes/hooks/huaweicloud-telemetry.py +238 -0
- package/integrations/hermes/manifest.yaml +43 -0
- package/integrations/opencode/hooks/skill-tracker.js +115 -0
- package/integrations/workbuddy/hooks/telemetry-tracker.py +226 -0
- package/package.json +7 -2
- 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/hooks/huaweicloud-safety.py +29 -1
- package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +2 -2
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md +42 -2
- package/plugins/huaweicloud-core/skills/huawei-iac/SKILL.md +10 -10
- package/plugins/huaweicloud-core/skills/huawei-iac/references/resource-catalog.md +11 -10
- package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +11 -11
- package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/SKILL.md +11 -9
- package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +11 -2
- package/plugins/huaweicloud-core/src/auth/credentials.mjs +82 -2
- package/plugins/huaweicloud-core/src/auth/reconcile.mjs +174 -0
- package/plugins/huaweicloud-core/src/auth/service.mjs +53 -32
- package/plugins/huaweicloud-core/src/hcloud-cli.mjs +22 -1
- package/plugins/huaweicloud-core/src/mcp-server.mjs +37 -3
- package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +14 -4
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +5 -0
- package/plugins/huaweicloud-core/src/setup-cli.mjs +395 -25
- package/plugins/huaweicloud-core/src/telemetry/agent-detect.mjs +40 -0
- package/plugins/huaweicloud-core/src/telemetry/agent-registry.mjs +328 -0
- package/plugins/huaweicloud-core/src/telemetry/telemetry.mjs +378 -0
- package/plugins/huaweicloud-core/src/tools.mjs +193 -2
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* huaweicloud-telemetry.js — AtomCode PreToolUse Hook
|
|
3
|
+
*
|
|
4
|
+
* Captures Skill loading, hcloud CLI commands, and MCP tool invocations,
|
|
5
|
+
* writing structured events to hook-events.jsonl for the devkit telemetry
|
|
6
|
+
* module to ingest and report.
|
|
7
|
+
*
|
|
8
|
+
* Classification logic mirrors the existing four platform hooks exactly.
|
|
9
|
+
* This hook NEVER blocks execution — it always exits 0.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { appendFileSync, mkdirSync, existsSync, readFileSync } from 'node:fs';
|
|
13
|
+
import { join, dirname } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
// ══════════════════════════════════════════════════════════════════
|
|
17
|
+
// Debug
|
|
18
|
+
// ══════════════════════════════════════════════════════════════════
|
|
19
|
+
|
|
20
|
+
const DEBUG = process.env.HUAWEICLOUD_DEVKIT_DEBUG === 'true';
|
|
21
|
+
|
|
22
|
+
function debugLog(msg) {
|
|
23
|
+
if (!DEBUG) return;
|
|
24
|
+
try {
|
|
25
|
+
const logPath = join(TELEMETRY_DIR, 'atomcode-debug.log');
|
|
26
|
+
appendFileSync(logPath, `${new Date().toISOString()} ${msg}\n`);
|
|
27
|
+
} catch (_) {
|
|
28
|
+
/* best-effort */
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ══════════════════════════════════════════════════════════════════
|
|
33
|
+
// Paths — hook is installed at <plugin_dir>/hooks/
|
|
34
|
+
// ══════════════════════════════════════════════════════════════════
|
|
35
|
+
|
|
36
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
37
|
+
const HOOK_DIR = dirname(__filename);
|
|
38
|
+
const PLUGIN_DIR = dirname(HOOK_DIR);
|
|
39
|
+
const TELEMETRY_DIR = join(PLUGIN_DIR, 'telemetry');
|
|
40
|
+
const HOOK_EVENTS_PATH = join(TELEMETRY_DIR, 'hook-events.jsonl');
|
|
41
|
+
|
|
42
|
+
// ══════════════════════════════════════════════════════════════════
|
|
43
|
+
// Helpers
|
|
44
|
+
// ══════════════════════════════════════════════════════════════════
|
|
45
|
+
|
|
46
|
+
function ensureDir(dirPath) {
|
|
47
|
+
if (!existsSync(dirPath)) mkdirSync(dirPath, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function writeEvent(key, value, extra = {}) {
|
|
51
|
+
try {
|
|
52
|
+
ensureDir(TELEMETRY_DIR);
|
|
53
|
+
const line = JSON.stringify({ key, value, ...extra }) + '\n';
|
|
54
|
+
appendFileSync(HOOK_EVENTS_PATH, line, 'utf8');
|
|
55
|
+
debugLog(`writeEvent key=${key} value=${value}`);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
debugLog(`writeEvent FAILED: ${error.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ══════════════════════════════════════════════════════════════════
|
|
62
|
+
// Skill identification
|
|
63
|
+
// ══════════════════════════════════════════════════════════════════
|
|
64
|
+
|
|
65
|
+
function isHuaweiCloudSkill(name) {
|
|
66
|
+
return typeof name === 'string' && /^huawei/i.test(name);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ══════════════════════════════════════════════════════════════════
|
|
70
|
+
// CLI command classification (matches skill-tracker.js exactly)
|
|
71
|
+
// ══════════════════════════════════════════════════════════════════
|
|
72
|
+
|
|
73
|
+
const HCLOUD_RE = /(?:^|[;&|]\s*)hcloud(?:\.exe)?\s+(.+)/i;
|
|
74
|
+
const READ_VERBS = /\b(List|Show|Get|Describe|NovaList|NovaShow)\w*/i;
|
|
75
|
+
const WRITE_VERBS =
|
|
76
|
+
/\b(Create|Delete|Update|Modify|Remove|Revoke|Grant|Attach|Detach|Enable|Disable|Set|Add|Bind|Unbind|Reset|Change|Activate|Deactivate|Register|Unregister|Import|Export|Download|Upload|Copy|Move|Convert|Migrate|Run|Execute|Invoke|Trigger|Deploy|Push|Start|Stop|Restart|Reboot|Suspend|Resume|Terminate|Release|Allocate)\w*/i;
|
|
77
|
+
|
|
78
|
+
function classifyHcloud(text) {
|
|
79
|
+
const m = HCLOUD_RE.exec(text);
|
|
80
|
+
if (!m) return null;
|
|
81
|
+
const rest = m[1].trim();
|
|
82
|
+
const parts = rest.split(/\s+/).filter((p) => !p.startsWith('--'));
|
|
83
|
+
const cmd = parts.join(' ');
|
|
84
|
+
if (!cmd) return null;
|
|
85
|
+
if (READ_VERBS.test(cmd)) return { key: 'cli:read', value: `hcloud ${cmd}` };
|
|
86
|
+
if (WRITE_VERBS.test(cmd)) return { key: 'cli:write', value: `hcloud ${cmd}` };
|
|
87
|
+
return { key: 'cli:invoke', value: `hcloud ${cmd}` };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ══════════════════════════════════════════════════════════════════
|
|
91
|
+
// Input parsing — stdin JSON, with env-var fallback
|
|
92
|
+
// ══════════════════════════════════════════════════════════════════
|
|
93
|
+
|
|
94
|
+
function parseInput() {
|
|
95
|
+
// Primary: AtomCode passes data via env vars
|
|
96
|
+
const toolName = process.env.ATOMCODE_TOOL_NAME || '';
|
|
97
|
+
const context = process.env.ATOMCODE_HOOK_CONTEXT || '';
|
|
98
|
+
if (toolName || context) {
|
|
99
|
+
const input = { tool_name: toolName };
|
|
100
|
+
if (context) {
|
|
101
|
+
try {
|
|
102
|
+
input.tool_input = JSON.parse(context);
|
|
103
|
+
} catch (_) {
|
|
104
|
+
input.tool_input = { command: context };
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
input.tool_input = {};
|
|
108
|
+
}
|
|
109
|
+
return input;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Fallback: stdin JSON (piped for manual testing)
|
|
113
|
+
try {
|
|
114
|
+
const data = readFileSync(0, 'utf8').trim();
|
|
115
|
+
if (data) return JSON.parse(data);
|
|
116
|
+
} catch (_) {
|
|
117
|
+
/* stdin unavailable */
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { tool_name: '', tool_input: {} };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function extractCommand(toolInput) {
|
|
124
|
+
if (typeof toolInput === 'string') return toolInput;
|
|
125
|
+
if (toolInput && typeof toolInput === 'object') {
|
|
126
|
+
return toolInput.command || toolInput.arguments?.command || '';
|
|
127
|
+
}
|
|
128
|
+
return '';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function extractSkillName(toolInput) {
|
|
132
|
+
if (typeof toolInput === 'string') return toolInput;
|
|
133
|
+
if (toolInput && typeof toolInput === 'object') {
|
|
134
|
+
return toolInput.name || toolInput.command || '';
|
|
135
|
+
}
|
|
136
|
+
return '';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ══════════════════════════════════════════════════════════════════
|
|
140
|
+
// Main
|
|
141
|
+
// ══════════════════════════════════════════════════════════════════
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const input = parseInput();
|
|
145
|
+
const toolName = input.tool_name || input.tool || '';
|
|
146
|
+
const toolInput = input.tool_input || input.arguments || {};
|
|
147
|
+
|
|
148
|
+
debugLog(`tool=${toolName}`);
|
|
149
|
+
|
|
150
|
+
// ── Category 1: Skill tool calls ──
|
|
151
|
+
if (toolName === 'skill' || toolName === 'use_skill') {
|
|
152
|
+
const skillName = extractSkillName(toolInput);
|
|
153
|
+
debugLog(`skill name=${skillName}`);
|
|
154
|
+
if (isHuaweiCloudSkill(skillName)) {
|
|
155
|
+
writeEvent('skill:retrieve', skillName);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Category 2: Bash/pwsh with hcloud commands ──
|
|
160
|
+
if (toolName === 'bash' || toolName === 'pwsh') {
|
|
161
|
+
const command = extractCommand(toolInput);
|
|
162
|
+
if (command && HCLOUD_RE.test(command)) {
|
|
163
|
+
debugLog(`hcloud command: ${command.slice(0, 120)}`);
|
|
164
|
+
const classified = classifyHcloud(command);
|
|
165
|
+
if (classified) {
|
|
166
|
+
writeEvent(classified.key, classified.value, { capability: 'cli' });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Category 3: MCP Huawei Cloud tools ──
|
|
172
|
+
if (toolName.startsWith('mcp__huaweicloud')) {
|
|
173
|
+
writeEvent(`tool:${toolName}`, '1', { capability: 'mcp' });
|
|
174
|
+
debugLog(`MCP tool: ${toolName}`);
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
debugLog(`FATAL: ${error?.message || error}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Always allow — telemetry hook never blocks; the process exits 0 naturally
|
|
181
|
+
// since all I/O is synchronous and nothing holds the event loop open.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// hook-plugin.mjs — DSH Cordis Hook Plugin
|
|
2
|
+
// Intercepts Huawei Cloud skill calls and hcloud CLI commands,
|
|
3
|
+
// writes events to hook-events.jsonl for the telemetry module to consume.
|
|
4
|
+
|
|
5
|
+
import { appendFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
6
|
+
import { join, dirname } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
// ══════════════════════════════════════════════════════════════════
|
|
10
|
+
// Cordis plugin identity (required by DSH loader)
|
|
11
|
+
// ══════════════════════════════════════════════════════════════════
|
|
12
|
+
export const name = 'huaweicloud-devkit-hook';
|
|
13
|
+
export const inject = ['tools'];
|
|
14
|
+
|
|
15
|
+
// ══════════════════════════════════════════════════════════════════
|
|
16
|
+
// Paths — this file lives in $DSH_HOME/huaweicloud-plugins/
|
|
17
|
+
// ══════════════════════════════════════════════════════════════════
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const PLUGIN_DIR = dirname(__filename);
|
|
20
|
+
// telemetry/ dir is the single shared location for event files
|
|
21
|
+
const TELEMETRY_DIR = join(PLUGIN_DIR, 'telemetry');
|
|
22
|
+
const HOOK_EVENTS_PATH = join(TELEMETRY_DIR, 'hook-events.jsonl');
|
|
23
|
+
|
|
24
|
+
const DEBUG = process.env.HUAWEICLOUD_DEVKIT_DEBUG === 'true';
|
|
25
|
+
|
|
26
|
+
// ══════════════════════════════════════════════════════════════════
|
|
27
|
+
// Helpers
|
|
28
|
+
// ══════════════════════════════════════════════════════════════════
|
|
29
|
+
|
|
30
|
+
function ensureDir(dirPath) {
|
|
31
|
+
if (!existsSync(dirPath)) mkdirSync(dirPath, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function debugLog(msg) {
|
|
35
|
+
if (!DEBUG) return;
|
|
36
|
+
try {
|
|
37
|
+
const logPath = join(TELEMETRY_DIR, 'telemetry-debug.log');
|
|
38
|
+
ensureDir(TELEMETRY_DIR);
|
|
39
|
+
appendFileSync(logPath, `${new Date().toISOString()} [hook] ${msg}\n`, 'utf8');
|
|
40
|
+
} catch {
|
|
41
|
+
// debug log is best-effort; never fail the plugin
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function writeEvent(key, value, extra = {}) {
|
|
46
|
+
try {
|
|
47
|
+
ensureDir(TELEMETRY_DIR);
|
|
48
|
+
const line = JSON.stringify({ key, value, ...extra }) + '\n';
|
|
49
|
+
appendFileSync(HOOK_EVENTS_PATH, line, 'utf8');
|
|
50
|
+
debugLog(`writeEvent key=${key} value=${value}`);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
debugLog(`writeEvent FAILED: ${error.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ══════════════════════════════════════════════════════════════════
|
|
57
|
+
// Huawei Cloud identification
|
|
58
|
+
// ══════════════════════════════════════════════════════════════════
|
|
59
|
+
|
|
60
|
+
function isHuaweiCloudSkill(name) {
|
|
61
|
+
return typeof name === 'string' && /^huawei/i.test(name);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ══════════════════════════════════════════════════════════════════
|
|
65
|
+
// hcloud command classification (mirrors skill-tracker.js)
|
|
66
|
+
// ══════════════════════════════════════════════════════════════════
|
|
67
|
+
|
|
68
|
+
const HCLOUD_RE = /(?:^|[;&|]\s*)hcloud(?:\.exe)?\s+(.+)/i;
|
|
69
|
+
const READ_VERBS = /\b(List|Show|Get|Describe|NovaList|NovaShow)\w*/i;
|
|
70
|
+
const WRITE_VERBS =
|
|
71
|
+
/\b(Create|Delete|Update|Modify|Remove|Revoke|Grant|Attach|Detach|Enable|Disable|Set|Add|Bind|Unbind|Reset|Change|Activate|Deactivate|Register|Unregister|Import|Export|Download|Upload|Copy|Move|Convert|Migrate|Run|Execute|Invoke|Trigger|Deploy|Push|Start|Stop|Restart|Reboot|Suspend|Resume|Terminate|Release|Allocate)\w*/i;
|
|
72
|
+
|
|
73
|
+
function classifyHcloud(text) {
|
|
74
|
+
const m = HCLOUD_RE.exec(text);
|
|
75
|
+
if (!m) return null;
|
|
76
|
+
const rest = m[1].trim();
|
|
77
|
+
const parts = rest.split(/\s+/).filter((p) => !p.startsWith('--'));
|
|
78
|
+
const cmd = parts.join(' ');
|
|
79
|
+
if (!cmd) return null;
|
|
80
|
+
if (READ_VERBS.test(cmd)) return { key: 'cli:read', value: `hcloud ${cmd}`, capability: 'cli' };
|
|
81
|
+
if (WRITE_VERBS.test(cmd)) return { key: 'cli:write', value: `hcloud ${cmd}`, capability: 'cli' };
|
|
82
|
+
return { key: 'cli:invoke', value: `hcloud ${cmd}`, capability: 'cli' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ══════════════════════════════════════════════════════════════════
|
|
86
|
+
// apply() — Cordis plugin entry point
|
|
87
|
+
// ══════════════════════════════════════════════════════════════════
|
|
88
|
+
|
|
89
|
+
export function apply(ctx) {
|
|
90
|
+
debugLog('=== PLUGIN LOADED ===');
|
|
91
|
+
|
|
92
|
+
// ── Layer 1: tools/pre-execute waterfall ──────────────────────
|
|
93
|
+
ctx.on('tools/pre-execute', async (exec, next) => {
|
|
94
|
+
try {
|
|
95
|
+
debugLog(`pre-execute tool=${exec.name}`);
|
|
96
|
+
|
|
97
|
+
// ── Intercept 1: skill tool calls ──
|
|
98
|
+
if (exec.name === 'skill') {
|
|
99
|
+
const skillName = exec.arguments?.name || '';
|
|
100
|
+
debugLog(`skill name=${skillName}`);
|
|
101
|
+
if (isHuaweiCloudSkill(skillName)) {
|
|
102
|
+
writeEvent('skill:retrieve', skillName);
|
|
103
|
+
ctx.logger.info(`[hw-hook] skill called: ${skillName}`);
|
|
104
|
+
debugLog(`SKILL TRACKED: ${skillName}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Intercept 2: pwsh/bash with hcloud commands ──
|
|
109
|
+
if (exec.name === 'pwsh' || exec.name === 'bash') {
|
|
110
|
+
const command = exec.arguments?.command || '';
|
|
111
|
+
if (!command) return next();
|
|
112
|
+
|
|
113
|
+
if (HCLOUD_RE.test(command)) {
|
|
114
|
+
debugLog(`hcloud command: ${command.slice(0, 120)}`);
|
|
115
|
+
|
|
116
|
+
// Classify and write event
|
|
117
|
+
const classified = classifyHcloud(command);
|
|
118
|
+
if (classified) {
|
|
119
|
+
writeEvent(classified.key, classified.value, { capability: 'cli' });
|
|
120
|
+
debugLog(`CLI TRACKED: ${classified.key} ${classified.value}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Intercept 3: MCP Huawei Cloud tools (record only) ──
|
|
126
|
+
if (exec.name.startsWith('mcp__huaweicloud__')) {
|
|
127
|
+
writeEvent(`tool:${exec.name}`, '1', { capability: 'mcp' });
|
|
128
|
+
debugLog(`MCP tool: ${exec.name}`);
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
debugLog(`pre-execute ERROR: ${error?.message || error}`);
|
|
132
|
+
ctx.logger.warn(`[hw-hook] pre-execute error: ${error.message}`);
|
|
133
|
+
}
|
|
134
|
+
return next();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ── Layer 2: tools/result observe ─────────────────────────────
|
|
138
|
+
ctx.on('tools/result', (exec, result) => {
|
|
139
|
+
if (exec.name.startsWith('mcp__huaweicloud__') && result.isError) {
|
|
140
|
+
const msg = result.error?.message || 'unknown error';
|
|
141
|
+
debugLog(`tool error: ${exec.name}: ${msg}`);
|
|
142
|
+
ctx.logger.info(`[hw-hook] tool error: ${exec.name}: ${msg}`);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
ctx.logger.info('[hw-hook] DSH Huawei Cloud hook plugin activated');
|
|
147
|
+
debugLog('=== PLUGIN INIT DONE ===');
|
|
148
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Hermes pre_tool_call hook — Huawei Cloud telemetry data collection.
|
|
3
|
+
|
|
4
|
+
Captures Skill loading and hcloud CLI commands, writing structured events
|
|
5
|
+
to hook-events.jsonl for the devkit telemetry module to ingest and report.
|
|
6
|
+
|
|
7
|
+
This hook NEVER blocks execution — it always returns exit code 0.
|
|
8
|
+
Safety enforcement is handled by huaweicloud-safety.py.
|
|
9
|
+
|
|
10
|
+
Data flow:
|
|
11
|
+
stdin JSON (pre_tool_call event) → classify → hook-events.jsonl
|
|
12
|
+
→ MCP Server telemetry.mjs :: ingestHookEvents() → eventQueue → HTTP POST
|
|
13
|
+
|
|
14
|
+
Classification logic mirrors the OpenCode skill-tracker.js exactly.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ── Path resolution ─────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
def _get_plugin_dir():
|
|
27
|
+
"""Resolve the huaweicloud-core plugin directory.
|
|
28
|
+
|
|
29
|
+
Search order:
|
|
30
|
+
1. HUAWEICLOUD_PLUGIN_DIR env var (explicit override)
|
|
31
|
+
2. Dev repo: hooks/ → ../plugins/... (local dev)
|
|
32
|
+
3. ~/.hermes/huaweicloud-plugins/ (Hermes installed)
|
|
33
|
+
"""
|
|
34
|
+
env_dir = os.environ.get("HUAWEICLOUD_PLUGIN_DIR")
|
|
35
|
+
if env_dir:
|
|
36
|
+
return Path(env_dir)
|
|
37
|
+
|
|
38
|
+
here = Path(__file__).resolve()
|
|
39
|
+
|
|
40
|
+
# Local dev: plugins/huaweicloud-core/hooks/huaweicloud-telemetry.py
|
|
41
|
+
# parents[1] = plugins/huaweicloud-core/
|
|
42
|
+
if (here.parents[1] / "skills").is_dir():
|
|
43
|
+
return here.parents[1]
|
|
44
|
+
|
|
45
|
+
# Local dev (from repo root): hooks/ → ../plugins/huaweicloud-core/
|
|
46
|
+
relative = here.parents[1] / "plugins" / "huaweicloud-core"
|
|
47
|
+
if relative.is_dir():
|
|
48
|
+
return relative
|
|
49
|
+
|
|
50
|
+
# Hermes installed: ~/AppData/Local/hermes/huaweicloud-plugins/
|
|
51
|
+
# hooks/ is directly under the plugin dir, so parents[1] IS the plugin dir.
|
|
52
|
+
if (here.parents[1] / "telemetry").is_dir() or (here.parents[1] / "src").is_dir():
|
|
53
|
+
return here.parents[1]
|
|
54
|
+
|
|
55
|
+
home = Path.home()
|
|
56
|
+
|
|
57
|
+
# Hermes installed plugin dir (legacy ~/.hermes/ path)
|
|
58
|
+
hermes_dir = home / ".hermes" / "huaweicloud-plugins"
|
|
59
|
+
if hermes_dir.is_dir():
|
|
60
|
+
return hermes_dir
|
|
61
|
+
|
|
62
|
+
raise FileNotFoundError("Cannot resolve huaweicloud-core plugin directory")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _get_telemetry_dir():
|
|
66
|
+
"""Return the telemetry directory shared with all devkit platforms.
|
|
67
|
+
|
|
68
|
+
Always at <plugin_dir>/telemetry/, matching AGENT_TELEMETRY_DIR
|
|
69
|
+
in src/telemetry/telemetry.mjs.
|
|
70
|
+
"""
|
|
71
|
+
plugin_dir = _get_plugin_dir()
|
|
72
|
+
telemetry_dir = plugin_dir / "telemetry"
|
|
73
|
+
telemetry_dir.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
return telemetry_dir
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _get_hook_events_path():
|
|
78
|
+
"""Lazily resolve hook-events.jsonl path. Returns None if unresolvable."""
|
|
79
|
+
try:
|
|
80
|
+
return str(_get_telemetry_dir() / "hook-events.jsonl")
|
|
81
|
+
except Exception:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ── Classifiers (match skill-tracker.js exactly) ────────────────────
|
|
86
|
+
|
|
87
|
+
# hcloud command detection
|
|
88
|
+
HCLOUD_RE = re.compile(r'(?:^|[;&|]\s*)hcloud(?:\.exe)?\s+(.+)', re.IGNORECASE)
|
|
89
|
+
|
|
90
|
+
# Read operation verbs
|
|
91
|
+
READ_VERBS = re.compile(
|
|
92
|
+
r'\b(List|Show|Get|Describe|NovaList|NovaShow)\w*', re.IGNORECASE
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Write operation verbs
|
|
96
|
+
WRITE_VERBS = re.compile(
|
|
97
|
+
r'\b(Create|Delete|Update|Modify|Remove|Revoke|Grant|Attach|Detach|'
|
|
98
|
+
r'Enable|Disable|Set|Add|Bind|Unbind|Reset|Change|Activate|Deactivate|'
|
|
99
|
+
r'Register|Unregister|Import|Export|Download|Upload|Copy|Move|Convert|'
|
|
100
|
+
r'Migrate|Run|Execute|Invoke|Trigger|Deploy|Push|Start|Stop|Restart|'
|
|
101
|
+
r'Reboot|Suspend|Resume|Terminate|Release|Allocate)\w*', re.IGNORECASE
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Huawei Cloud skill name pattern
|
|
105
|
+
HUAWEI_SKILL_RE = re.compile(r'^huawei', re.IGNORECASE)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def classify_hcloud(text):
|
|
109
|
+
"""Classify an hcloud CLI command as read / write / invoke.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
dict with 'key' and 'value', or None if not an hcloud command.
|
|
113
|
+
Matches the classifyHcloud() logic in skill-tracker.js.
|
|
114
|
+
"""
|
|
115
|
+
m = HCLOUD_RE.search(text)
|
|
116
|
+
if not m:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
rest = m[1].strip()
|
|
120
|
+
# Extract command part, excluding flags/options
|
|
121
|
+
parts = rest.split()
|
|
122
|
+
cmd_parts = [p for p in parts if not p.startswith('--')]
|
|
123
|
+
cmd = ' '.join(cmd_parts).strip()
|
|
124
|
+
|
|
125
|
+
if not cmd:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
if READ_VERBS.search(cmd):
|
|
129
|
+
return {'key': 'cli:read', 'value': f'hcloud {cmd}'}
|
|
130
|
+
if WRITE_VERBS.search(cmd):
|
|
131
|
+
return {'key': 'cli:write', 'value': f'hcloud {cmd}'}
|
|
132
|
+
return {'key': 'cli:invoke', 'value': f'hcloud {cmd}'}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def is_huawei_skill(name):
|
|
136
|
+
"""Check if a skill name belongs to Huawei Cloud."""
|
|
137
|
+
return name and HUAWEI_SKILL_RE.search(name)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ── Event writer ────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
def write_event(key, value, extra=None):
|
|
143
|
+
"""Append a telemetry event to hook-events.jsonl.
|
|
144
|
+
|
|
145
|
+
Format matches OpenCode/DSH/WorkBuddy output exactly:
|
|
146
|
+
{"key": "...", "value": "...", "capability": "..."}
|
|
147
|
+
"""
|
|
148
|
+
path = _get_hook_events_path()
|
|
149
|
+
if not path:
|
|
150
|
+
return # Best-effort: silently skip if path is unresolvable
|
|
151
|
+
|
|
152
|
+
event = {"key": key, "value": value}
|
|
153
|
+
if extra:
|
|
154
|
+
event.update(extra)
|
|
155
|
+
try:
|
|
156
|
+
with open(path, "a", encoding="utf-8") as f:
|
|
157
|
+
f.write(json.dumps(event, ensure_ascii=False) + "\n")
|
|
158
|
+
except Exception:
|
|
159
|
+
# Best-effort; never block the main tool execution
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ── Command text extraction ─────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
def command_text(tool_input):
|
|
166
|
+
"""Extract command text from various tool_input shapes."""
|
|
167
|
+
if isinstance(tool_input, str):
|
|
168
|
+
return tool_input
|
|
169
|
+
if isinstance(tool_input, dict):
|
|
170
|
+
for key in ("command", "cmd", "script", "args", "arguments"):
|
|
171
|
+
value = tool_input.get(key)
|
|
172
|
+
if isinstance(value, list):
|
|
173
|
+
return " ".join(str(item) for item in value)
|
|
174
|
+
if value is not None:
|
|
175
|
+
return str(value)
|
|
176
|
+
return json.dumps(tool_input)
|
|
177
|
+
return json.dumps(tool_input)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ── Skill name extraction ───────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
def extract_skill_name(tool_name, tool_input):
|
|
183
|
+
"""Extract skill name from tool_input for skill-related tools.
|
|
184
|
+
|
|
185
|
+
Hermes skill tools: skill_view, skills_list, skill_manage.
|
|
186
|
+
The skill name may be in 'name', 'skill', or 'args' field.
|
|
187
|
+
"""
|
|
188
|
+
if not isinstance(tool_input, dict):
|
|
189
|
+
if isinstance(tool_input, str):
|
|
190
|
+
return tool_input
|
|
191
|
+
return ""
|
|
192
|
+
|
|
193
|
+
# Common field names across agent platforms
|
|
194
|
+
return (
|
|
195
|
+
tool_input.get("name", "")
|
|
196
|
+
or tool_input.get("skill", "")
|
|
197
|
+
or tool_input.get("command", "")
|
|
198
|
+
or ""
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# ── Main hook logic ─────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
def main():
|
|
205
|
+
try:
|
|
206
|
+
data = json.load(sys.stdin)
|
|
207
|
+
except Exception:
|
|
208
|
+
# Cannot parse input → not our concern, allow execution
|
|
209
|
+
sys.exit(0)
|
|
210
|
+
|
|
211
|
+
tool_name = data.get("tool_name", "")
|
|
212
|
+
tool_input = data.get("tool_input", {})
|
|
213
|
+
|
|
214
|
+
# ── Category 1: Skill tool loading ──
|
|
215
|
+
# Hermes skill tools: skill_view, skills_list, skill_manage
|
|
216
|
+
if tool_name in ("skill_view", "skills_list", "skill_manage", "skill"):
|
|
217
|
+
skill_name = extract_skill_name(tool_name, tool_input)
|
|
218
|
+
if is_huawei_skill(skill_name):
|
|
219
|
+
write_event('skill:retrieve', skill_name)
|
|
220
|
+
|
|
221
|
+
# ── Category 2: Bash/terminal tool with hcloud commands ──
|
|
222
|
+
elif tool_name in ("Bash", "terminal", "bash", "pwsh"):
|
|
223
|
+
command = command_text(tool_input)
|
|
224
|
+
if command and HCLOUD_RE.search(command):
|
|
225
|
+
result = classify_hcloud(command)
|
|
226
|
+
if result:
|
|
227
|
+
write_event(result['key'], result['value'], {'capability': 'cli'})
|
|
228
|
+
|
|
229
|
+
# ── Category 3: MCP Huawei Cloud tool invocations ──
|
|
230
|
+
elif tool_name.startswith("mcp__huaweicloud") or tool_name.startswith("huaweicloud_"):
|
|
231
|
+
write_event(f"tool:{tool_name}", "1", {'capability': 'mcp'})
|
|
232
|
+
|
|
233
|
+
# Always allow — telemetry hook never blocks execution
|
|
234
|
+
sys.exit(0)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
if __name__ == "__main__":
|
|
238
|
+
main()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
manifest_version: 1
|
|
2
|
+
name: huaweicloud-devkit
|
|
3
|
+
description: >
|
|
4
|
+
Huawei Cloud DevKit provides Skills, MCP tools, KooCLI guidance,
|
|
5
|
+
and safety guardrails for Huawei Cloud services within Hermes Agent.
|
|
6
|
+
Includes 27 skills covering ECS, OBS, VPC, RDS, CCE, ModelArts, and more.
|
|
7
|
+
|
|
8
|
+
source: https://github.com/huaweicloud/huaweicloud-devkit
|
|
9
|
+
|
|
10
|
+
transport:
|
|
11
|
+
type: stdio
|
|
12
|
+
command: 'node'
|
|
13
|
+
args: ['plugins/huaweicloud-core/src/mcp-server.mjs']
|
|
14
|
+
env:
|
|
15
|
+
HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local'
|
|
16
|
+
|
|
17
|
+
install:
|
|
18
|
+
type: git
|
|
19
|
+
url: https://github.com/huaweicloud/huaweicloud-devkit
|
|
20
|
+
bootstrap:
|
|
21
|
+
- 'npm ci --omit=dev'
|
|
22
|
+
- 'node plugins/huaweicloud-core/src/setup-cli.mjs install --target hermes --skip-mcp-server'
|
|
23
|
+
|
|
24
|
+
auth:
|
|
25
|
+
type: none
|
|
26
|
+
|
|
27
|
+
tools:
|
|
28
|
+
default_enabled: []
|
|
29
|
+
|
|
30
|
+
post_install: |
|
|
31
|
+
Huawei Cloud DevKit installed successfully!
|
|
32
|
+
|
|
33
|
+
What was set up:
|
|
34
|
+
• 27 skills (ECS, OBS, VPC, RDS, CCE, ModelArts, ...) → ~/.hermes/skills/
|
|
35
|
+
• MCP tools (33 tools for CLI, auth, sandbox, safety) → config.yaml
|
|
36
|
+
• Safety hooks (pre_tool_call terminal guard) → config.yaml
|
|
37
|
+
• Safety policy → ~/.hermes/huaweicloud-plugins/safety/
|
|
38
|
+
|
|
39
|
+
Next steps:
|
|
40
|
+
1. Configure credentials: npx huaweicloud-devkit auth init
|
|
41
|
+
2. Restart Hermes Agent session (MCP tools load on restart)
|
|
42
|
+
3. Verify: hermes doctor
|
|
43
|
+
4. Try: "Help me check my ECS instances" or "Deploy this website to OBS"
|