rainskills 0.1.6 → 0.1.8-rc.2

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.
Files changed (31) hide show
  1. package/README.md +14 -14
  2. package/SKILL.md +2 -2
  3. package/bin/rainskills-tools.js +229 -25
  4. package/bin/rainskills.js +4 -45
  5. package/install.sh +43 -332
  6. package/marketplace/rainskills/.claude-plugin/plugin.json +1 -1
  7. package/marketplace/rainskills/.codex-plugin/plugin.json +1 -1
  8. package/marketplace/rainskills/skills/rainskills/SKILL.md +3 -3
  9. package/package.json +11 -16
  10. package/rainbond-app-assistant/SKILL.md +7 -5
  11. package/rainbond-app-version-assistant/SKILL.md +6 -4
  12. package/rainbond-delivery-verifier/SKILL.md +6 -4
  13. package/rainbond-env-sync/SKILL.md +6 -4
  14. package/rainbond-fullstack-bootstrap/SKILL.md +7 -5
  15. package/rainbond-fullstack-troubleshooter/SKILL.md +6 -4
  16. package/rainbond-opensource-app-deploy/SKILL.md +249 -0
  17. package/rainbond-opensource-app-deploy/agents/openai.yaml +4 -0
  18. package/rainbond-opensource-app-deploy/references/failure-mode-playbook.md +78 -0
  19. package/rainbond-platform-installer/SKILL.md +1 -1
  20. package/rainbond-platform-installer/scripts/installed-version.js +1 -1
  21. package/rainbond-platform-installer/scripts/runtime-intents.js +7 -0
  22. package/rainbond-platform-installer/scripts/windows-client-config.js +0 -96
  23. package/rainbond-platform-installer/scripts/windows-onboarding.js +35 -70
  24. package/rainbond-platform-query/SKILL.md +6 -4
  25. package/rainbond-project-init/SKILL.md +7 -5
  26. package/rainbond-template-installer/SKILL.md +7 -5
  27. package/scripts/build-skill-manifest.mjs +180 -0
  28. package/scripts/install-local-cli.mjs +174 -0
  29. package/pi/rainskills-mcp.ts +0 -15745
  30. package/rainbond-platform-installer/scripts/mcp-router.js +0 -212
  31. package/rainbond-platform-installer/scripts/mcp-server.js +0 -79
package/README.md CHANGED
@@ -41,11 +41,11 @@ Claude Code:
41
41
  /reload-plugins
42
42
  ```
43
43
 
44
- 安装流程支持 Codex 和 Claude Code;macOS、Linux 和 WSL 不支持 OpenClaw 或 Pi Agent 安装。不要手工复制 Skill、拼接凭据或修改 MCP 配置。
44
+ 安装流程支持 Codex 和 Claude Code;macOS、Linux 和 WSL 不支持 OpenClaw 安装。不要手工复制 Skill、拼接凭据或修改 Agent 配置。
45
45
 
46
46
  ## 安装完成时用户会看到什么
47
47
 
48
- 安装完成后只完成 Skills 安装并展示能力列表,不选择运行环境、不登录 Rainbond,也不配置业务 MCP:
48
+ 安装完成后只完成 Skills 和受保护本地 CLI 的安装并展示能力列表,不选择运行环境、不登录 Rainbond,也不配置 Agent MCP:
49
49
 
50
50
  ```text
51
51
  Rainskills 安装完成,下一条消息即可直接使用。
@@ -72,13 +72,13 @@ Rainskills 安装完成,下一条消息即可直接使用。
72
72
  固定 launcher 来自当前 `package.json` 版本:
73
73
 
74
74
  ```json
75
- ["npx", "--yes", "rainskills@0.1.6"]
75
+ ["npx", "--yes", "rainskills@0.1.8-rc.2"]
76
76
  ```
77
77
 
78
78
  所有调用都把 launcher 与参数合并成 argv 数组后直接执行;不得使用 `rainskills@latest`,不得把参数拼成 shell 字符串。
79
79
 
80
- 1. 执行 launcher + `["environment", "list", "--json"]`,先于项目扫描和任何业务 MCP。未指定环境时只使用全局默认环境,默认不可用时停止,不自动回退。
81
- 2. 每个请求生成独立 operation UUID,并执行 `operation begin`;显式环境只写入这次 operation。每个 Rainbond MCP 调用都携带返回的 `rainskills_operation_id`。项目不保存环境、团队或应用绑定,同一项目可以部署到多个目标。
80
+ 1. 执行 launcher + `["environment", "list", "--json"]`,先于项目扫描和任何 Rainbond 业务调用。未指定环境时只使用全局默认环境,默认不可用时停止,不自动回退。
81
+ 2. 每个请求生成独立 operation UUID,并执行 `operation begin`;显式环境只写入这次 operation。所有 Rainbond 查询和变更都通过 `~/.rainbond/bin/rainskills-tools.js`,并绑定返回的 operation ID。项目不保存环境、团队或应用绑定,同一项目可以部署到多个目标。
82
82
  3. 将动作转换为 `runtime-intents.js` 中对应的受限 intent 并完成字段校验。target 只允许 `codex`、`claude`、`all`;按用户选择构造 launcher + `["runtime", "connect", "<target>", ...环境参数, "--intent-json", "<JSON.stringify(已校验 intent)>"]`。
83
83
  4. 环境参数必须恰好选择一组且互斥:Cloud 用 `["--saas"]`,已有私有环境用 `["--rainbond-url", "<已验证 Console origin>"]`,新建私有环境用 `["--install-private", "--location", "local"]` 或 `["--install-private", "--location", "server"]`。
84
84
  5. 安装私有环境时,只消费完成 schema、action、onboarding id 和参数边界校验的 `rainskills.next-action.v1.argv`;拒绝字符串命令和其他输出字段。
@@ -141,10 +141,10 @@ Rainskills 安装完成,下一条消息即可直接使用。
141
141
  - 第一个连接成功的环境自动成为全局默认环境;以后新增环境不修改默认值。
142
142
  - 用户未指定环境时使用默认环境;用户明确指定时只覆盖本次 operation。
143
143
  - 环境名称自动生成,可重命名;内部不可变 ID 不随重命名改变。
144
- - 添加、重命名、重新授权或删除环境不改项目配置,也不需要为每个环境重新配置 Agent MCP。
144
+ - 添加、重命名、重新授权或删除环境不改项目配置,也不需要为每个 Agent 单独配置 MCP。
145
145
  - 新增环境直接建立独立连接,不读取或恢复旧版单环境状态;HTTPS 直接进入授权,明文 HTTP 只增加一次可信网络确认。
146
146
  - 新增环境与首次部署复用同一套固定选择:先选“云端环境(免费体验)/私有环境(去对接)”;选择私有环境后直接选“部署到本机/部署到独立服务器/部署到已有 Rainbond”,不再出现旧的接入方式中间步骤。
147
- - Agent 始终连接一个本地 Rainskills MCP。Rainskills 按 `operation_id` 从隔离凭据存储中选择目标 Rainbond。
147
+ - Agent 始终执行同一个受保护的本地 Rainskills CLI。CLI 按 `operation_id` 从隔离凭据存储中选择目标 Rainbond;Rainskills 不提供本地 MCP 服务,Agent 也不得绕过 CLI 直接调用 Rainbond MCP。
148
148
  - 明确说“团队”表示默认或指定运行环境中的 Rainbond 团队;明确说“运行环境/平台”表示环境。裸名称同时匹配两者时必须询问。
149
149
 
150
150
  常用管理命令:
@@ -164,7 +164,7 @@ node <已安装的 Skills 根目录>/rainbond-platform-installer/scripts/local-r
164
164
 
165
165
  ### 认证与权限恢复
166
166
 
167
- 业务 MCP 返回 401 时,依次执行以下 argv 参数:
167
+ Rainbond 能力端点经 CLI 返回 401 时,依次执行以下 argv 参数:
168
168
 
169
169
  ```json
170
170
  ["runtime", "record-failure", "--onboarding-id", "<同一 onboarding-id>", "--step", "<当前固定步骤>", "--reason", "credential-expired"]
@@ -192,21 +192,21 @@ Windows 本地安装是预览能力,也可以改选 Linux 服务器。它要
192
192
 
193
193
  ```bash
194
194
  # 本地单机
195
- npx --yes rainskills@0.1.6 platform install --onboarding-id <id> \
195
+ npx --yes rainskills@0.1.8-rc.2 platform install --onboarding-id <id> \
196
196
  --location local --mode single-node
197
197
 
198
198
  # Linux 服务器单机
199
- npx --yes rainskills@0.1.6 platform install --onboarding-id <id> \
199
+ npx --yes rainskills@0.1.8-rc.2 platform install --onboarding-id <id> \
200
200
  --location server --mode single-node --ssh <user@host>
201
201
 
202
202
  # 服务器主机集群(自动生成受保护示例文件)
203
- npx --yes rainskills@0.1.6 platform install --onboarding-id <id> \
203
+ npx --yes rainskills@0.1.8-rc.2 platform install --onboarding-id <id> \
204
204
  --location server --mode host-cluster
205
205
 
206
206
  # 已有高级 ROI cluster.yaml 才追加:--cluster-config <path>
207
207
 
208
208
  # 已有 Kubernetes
209
- npx --yes rainskills@0.1.6 platform install --onboarding-id <id> \
209
+ npx --yes rainskills@0.1.8-rc.2 platform install --onboarding-id <id> \
210
210
  --location server --mode existing-kubernetes \
211
211
  --kubeconfig <path> --kube-context <name> --chart-version <version>
212
212
  ```
@@ -228,9 +228,9 @@ npx --yes rainskills@0.1.6 platform install --onboarding-id <id> \
228
228
 
229
229
  Rainskills 会在用户下一次发起业务动作时,由本地运行时立即返回环境查询结果,并另行启动后台任务静默检查更新。更新只跟随 npm `latest` 指向的正式版。当前版本是 RC 或其他预发布版本时不会查询、不会自动升级;npm 上的新 RC 版本也不参与正式版自动升级。
230
230
 
231
- 发现更高的正式版后,后台任务只委托到经过校验的精确版本,例如 `rainskills@1.2.3`,不会执行浮动的 `@latest` 业务代码。新版本原子刷新已经安装的 Rainskills Skills;当前业务继续使用启动时已经加载的版本,最迟从下一条新任务开始使用新版。npm 超时、版本检查失败、安装位置不安全或文件迁移失败时会保留旧版本,且不会阻塞或改变当前操作。
231
+ 发现更高的正式版后,后台任务只委托到经过校验的精确版本,例如 `rainskills@0.1.8-rc.2`,不会执行浮动的 `@latest` 业务代码。新版本原子刷新已经安装的 Rainskills Skills;当前业务继续使用启动时已经加载的版本,最迟从下一条新任务开始使用新版。npm 超时、版本检查失败、安装位置不安全或文件迁移失败时会保留旧版本,且不会阻塞或改变当前操作。
232
232
 
233
- 升级只更新 Rainskills 自身,不触发 Rainbond 安装、运行环境选择、登录授权、MCP 配置或重新对接。原始业务操作会继续执行;只有该业务操作本身需要运行环境时,才按既有门禁检查当前连接。可用连接直接复用,401 只重新授权一次,403 立即停止,从未连接过运行环境时才进入环境选择。
233
+ 升级只更新 Rainskills 自身,不触发 Rainbond 安装、运行环境选择、登录授权或重新对接,也不会新增 Agent MCP 配置。更新内容仅包括 Skills 和本地 CLI。原始业务操作会继续执行;只有该业务操作本身需要运行环境时,才按既有门禁检查当前连接。可用连接直接复用,401 只重新授权一次,403 立即停止,从未连接过运行环境时才进入环境选择。
234
234
 
235
235
  正在执行的部署、私有平台安装和授权流程继续使用启动时锁定的旧版本,自动升级不会插入这些流程。操作完成后会再次静默调度更新检查;检查进程有固定超时且不向用户输出远程 npx 命令。
236
236
 
package/SKILL.md CHANGED
@@ -9,7 +9,7 @@ This is the single marketplace entry for the complete Rainskills product. The in
9
9
 
10
10
  ## Initialize
11
11
 
12
- 1. Detect the current host client and map it to exactly one supported installer target: Codex=`codex` or Claude Code=`claude`. The macOS, Linux, and WSL installer intentionally does not support OpenClaw or Pi Agent; report that limitation instead of invoking an unsupported target. Do not ask the user which supported client they are currently using. If the host cannot be determined reliably, omit the target and let the installer ask.
12
+ 1. Detect the current host client and map it to exactly one supported installer target: Codex=`codex` or Claude Code=`claude`. The macOS, Linux, and WSL installer intentionally does not support OpenClaw; report that limitation instead of invoking an unsupported target. Do not ask the user which supported client they are currently using. If the host cannot be determined reliably, omit the target and let the installer ask.
13
13
  2. Resolve the directory containing this `SKILL.md`. On native Windows, if the adjacent `bin/rainskills.js` exists, run it by absolute path with the detected target in an attached interactive terminal: `node <skill-directory>/bin/rainskills.js <target>`. On macOS, Linux, or WSL, if the adjacent `install.sh` exists, run `bash <skill-directory>/install.sh <target>` the same way. Do not replace the installer with manual file copies.
14
14
  3. Keep stdin, stdout, and stderr attached. When `RAINSKILLS_USER_INPUT_REQUIRED` appears, pause for that installer choice. If the installer emits `rainskills.next-action.v1`, execute only its fixed `argv` through the same launcher; never evaluate output as a shell command. If the adjacent `bin/rainskills.js` exists, use it for fixed next actions; otherwise use the same versioned npm package fallback described below.
15
15
  4. Stay attached until every independent Skill is installed. Do not select, connect, or configure an application runtime during installation. In the user-facing response, output only the fixed completion message below.
@@ -38,7 +38,7 @@ Rainskills 安装完成,下一条消息即可直接使用。
38
38
 
39
39
  ## Manage Runtime Environments
40
40
 
41
- 环境是全局列表,不是项目绑定。本地状态命令必须定位已安装的同级 `rainbond-platform-installer/scripts/local-runtime.js`,解析为绝对路径后通过 `node` 以 argv 数组执行;不得为了查询本地环境访问 npm。只有连接或安装环境时才使用与当前技能包一致的固定 launcher `npx --yes rainskills@0.1.6`。
41
+ 环境是全局列表,不是项目绑定。本地状态命令必须定位已安装的同级 `rainbond-platform-installer/scripts/local-runtime.js`,解析为绝对路径后通过 `node` 以 argv 数组执行;不得为了查询本地环境访问 npm。只有连接或安装环境时才使用与当前技能包一致的固定 launcher `npx --yes rainskills@0.1.8-rc.2`。
42
42
 
43
43
  - 列表:用本地 launcher 执行 `environment list --json`,只展示名称、类型、状态、是否默认和最近验证时间。
44
44
  - 重命名:用本地 launcher 执行 `environment rename --environment-id <uuid> --name <name>`。
@@ -20,7 +20,12 @@ const CREDENTIALS_FILENAME = "credentials.env";
20
20
  const LEGACY_CREDENTIALS_FILENAME = "mcp.env";
21
21
  const CATALOG_FILENAME = "capabilities.json";
22
22
  const OPERATIONS_DIRECTORY = "operations";
23
+ const SKILL_MANIFEST_FILENAME = "rainskills-skill-manifest.json";
23
24
  const OPERATION_ID_PATTERN = /^[a-f0-9]{8}-[a-f0-9-]{27,}$/;
25
+ const SKILL_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,127}$/;
26
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
27
+ const SKILL_CONTENT_MAX_BYTES = 128 * 1024;
28
+ const SKILL_MANIFEST_MAX_BYTES = 4 * 1024 * 1024;
24
29
  const SENSITIVE_RESPONSE_KEY_PATTERN = /(?:authorization|jwt|token|password|secret|credential|private[_-]?key|key[_-]?file|certificate|cert[_-]?file|ssl[_-]?ca[_-]?cert)/i;
25
30
  const RISK_POLICY = Object.freeze({
26
31
  version: "1",
@@ -102,7 +107,72 @@ function readPrivateEnvFile(configPath) {
102
107
  }
103
108
  }
104
109
 
105
- function loadConfig({ env = process.env, homeDir = os.homedir(), includeRuntime = false } = {}) {
110
+ function requireRuntimeModule(filename) {
111
+ const candidates = [
112
+ path.resolve(__dirname, "..", "rainbond-platform-installer", "scripts", filename),
113
+ path.resolve(__dirname, "..", "lib", "rainbond-platform-installer", "scripts", filename),
114
+ ];
115
+ for (const candidate of candidates) {
116
+ try {
117
+ const info = fs.lstatSync(candidate);
118
+ if (info.isFile() && !info.isSymbolicLink()) return require(candidate);
119
+ } catch (error) {
120
+ if (error.code !== "ENOENT") throw error;
121
+ }
122
+ }
123
+ throw new BridgeError("installed Rainskills CLI runtime is incomplete", EXIT.CONFIG);
124
+ }
125
+
126
+ function loadOperationConfig({ homeDir, operationId, includeRuntime }) {
127
+ let operationStore;
128
+ const { createEnvironmentRegistry } = requireRuntimeModule("environment-registry.js");
129
+ const { createEnvironmentCredentialStore } = requireRuntimeModule("environment-credentials.js");
130
+ const { createRuntimeOperationStore } = requireRuntimeModule("runtime-operations.js");
131
+ const registry = createEnvironmentRegistry({
132
+ home: homeDir,
133
+ activeOperationIds: (environmentId) => operationStore?.activeOperationIds(environmentId) || [],
134
+ });
135
+ operationStore = createRuntimeOperationStore({ home: homeDir, registry });
136
+ const operation = operationStore.read(operationId);
137
+ if (!operation || !["active", "interrupted"].includes(operation.stage)) {
138
+ throw new BridgeError("protected runtime operation is missing or inactive", EXIT.CONFIG);
139
+ }
140
+ const environment = registry.get(operation.environment_id);
141
+ if (!environment || environment.connection_state !== "connected") {
142
+ throw new BridgeError("the operation environment is unavailable", EXIT.CONFIG);
143
+ }
144
+ const credential = createEnvironmentCredentialStore({ home: homeDir }).read({
145
+ environmentId: environment.id,
146
+ expectedOrigin: environment.console_origin,
147
+ });
148
+ const parsed = new URL(environment.console_origin);
149
+ const { INTENT_DEFINITIONS } = requireRuntimeModule("runtime-intents.js");
150
+ const requiredSkillId = INTENT_DEFINITIONS[operation.intent.type]?.skillId;
151
+ if (!requiredSkillId) {
152
+ throw new BridgeError("the protected runtime intent has no Skill binding", EXIT.CONFIG);
153
+ }
154
+ return {
155
+ baseUrl: environment.console_origin,
156
+ jwt: credential.token,
157
+ operationId: operation.operation_id,
158
+ environmentId: environment.id,
159
+ intent: operation.intent,
160
+ requiredSkillId,
161
+ ...(includeRuntime ? {
162
+ homeDir,
163
+ allowInsecureHttp: parsed.protocol === "http:",
164
+ isInsecureHttp: parsed.protocol === "http:",
165
+ } : {}),
166
+ };
167
+ }
168
+
169
+ function loadConfig({
170
+ env = process.env,
171
+ homeDir = os.homedir(),
172
+ includeRuntime = false,
173
+ operationId,
174
+ } = {}) {
175
+ if (operationId) return loadOperationConfig({ homeDir, operationId, includeRuntime });
106
176
  const configDirectory = path.join(homeDir, CONFIG_DIRECTORY);
107
177
  const credentialsPath = path.join(configDirectory, CREDENTIALS_FILENAME);
108
178
  const legacyCredentialsPath = path.join(configDirectory, LEGACY_CREDENTIALS_FILENAME);
@@ -149,24 +219,50 @@ function loadConfig({ env = process.env, homeDir = os.homedir(), includeRuntime
149
219
  }
150
220
 
151
221
  function parseCommand(args) {
152
- const command = args[0];
153
- if (command === "status" && args.length === 1) return { command };
222
+ const operationIndex = args.indexOf("--operation-id");
223
+ if (
224
+ operationIndex < 0
225
+ || !OPERATION_ID_PATTERN.test(args[operationIndex + 1] || "")
226
+ || args.indexOf("--operation-id", operationIndex + 1) !== -1
227
+ ) {
228
+ throw new BridgeError("every tools command requires --operation-id <uuid>", EXIT.USAGE);
229
+ }
230
+ const operationId = args[operationIndex + 1];
231
+ const remaining = [
232
+ ...args.slice(0, operationIndex),
233
+ ...args.slice(operationIndex + 2),
234
+ ];
235
+ const command = remaining[0];
236
+ if (command === "status" && remaining.length === 1) return { command, operationId };
154
237
  if (command === "list") {
155
- if (args.length === 1) return { command };
156
- if (args.length === 3 && args[1] === "--prefix" && args[2]) {
157
- return { command, prefix: args[2] };
238
+ if (remaining.length === 1) return { command, operationId };
239
+ if (remaining.length === 3 && remaining[1] === "--prefix" && remaining[2]) {
240
+ return { command, prefix: remaining[2], operationId };
158
241
  }
159
242
  }
160
- if (command === "describe" && args.length === 2 && args[1]) {
161
- return { command, toolName: args[1] };
162
- }
163
- if (command === "read" && args.length === 4 && args[1] && args[2] === "--input" && args[3] === "-") {
164
- return { command, toolName: args[1], input: args[3] };
165
- }
166
- if (command === "call" && args.length >= 4 && args[1] && args[2] === "--input" && args[3] === "-") {
167
- if (args.length === 4) return { command, toolName: args[1], input: args[3] };
168
- if (args.length === 6 && args[4] === "--confirm" && OPERATION_ID_PATTERN.test(args[5])) {
169
- return { command, toolName: args[1], input: args[3], confirmation: args[5] };
243
+ if (command === "describe" && remaining.length === 2 && remaining[1]) {
244
+ return { command, toolName: remaining[1], operationId };
245
+ }
246
+ if (command === "read" && remaining.length === 4 && remaining[1] && remaining[2] === "--input" && remaining[3] === "-") {
247
+ return { command, toolName: remaining[1], input: remaining[3], operationId };
248
+ }
249
+ if (command === "call" && remaining.length >= 4 && remaining[1] && remaining[2] === "--input" && remaining[3] === "-") {
250
+ const parsed = { command, toolName: remaining[1], input: remaining[3], operationId };
251
+ const allowed = new Set(["--confirm", "--skill-id", "--root-skill-id"]);
252
+ for (let index = 4; index < remaining.length; index += 2) {
253
+ const option = remaining[index];
254
+ const value = remaining[index + 1];
255
+ if (!allowed.has(option) || !value || value.startsWith("--")) break;
256
+ if (option === "--confirm" && !parsed.confirmation && OPERATION_ID_PATTERN.test(value)) {
257
+ parsed.confirmation = value;
258
+ } else if (option === "--skill-id" && !parsed.skillId && SKILL_ID_PATTERN.test(value)) {
259
+ parsed.skillId = value;
260
+ } else if (option === "--root-skill-id" && !parsed.rootSkillId && SKILL_ID_PATTERN.test(value)) {
261
+ parsed.rootSkillId = value;
262
+ } else {
263
+ break;
264
+ }
265
+ if (index + 2 === remaining.length) return parsed;
170
266
  }
171
267
  }
172
268
  throw new BridgeError("invalid command; use status, list, describe, read, or call", EXIT.USAGE);
@@ -233,6 +329,62 @@ function operationsPath(config) {
233
329
  return path.join(config.homeDir || os.homedir(), CONFIG_DIRECTORY, OPERATIONS_DIRECTORY);
234
330
  }
235
331
 
332
+ function skillManifestPath(config) {
333
+ return path.join(config.homeDir || os.homedir(), CONFIG_DIRECTORY, "bin", SKILL_MANIFEST_FILENAME);
334
+ }
335
+
336
+ function loadSkillBinding(config, skillId, rootSkillId) {
337
+ if (skillId !== config.requiredSkillId) {
338
+ throw new BridgeError("Skill does not match the protected runtime operation intent", EXIT.CONFIG);
339
+ }
340
+ const target = skillManifestPath(config);
341
+ let manifest;
342
+ try {
343
+ const info = fs.lstatSync(target);
344
+ if (
345
+ !info.isFile()
346
+ || info.isSymbolicLink()
347
+ || (process.platform !== "win32" && (info.mode & 0o077) !== 0)
348
+ || info.size > SKILL_MANIFEST_MAX_BYTES
349
+ ) throw new Error("unsafe manifest");
350
+ manifest = JSON.parse(fs.readFileSync(target, "utf8"));
351
+ } catch (_error) {
352
+ throw new BridgeError("RainSkills Skill manifest is missing or unsafe", EXIT.CONFIG);
353
+ }
354
+ if (
355
+ !manifest
356
+ || manifest.schema !== "rainskills.skill-manifest.v1"
357
+ || manifest.profile !== "cli"
358
+ || typeof manifest.package_version !== "string"
359
+ || !manifest.package_version
360
+ || !Array.isArray(manifest.skills)
361
+ ) {
362
+ throw new BridgeError("RainSkills Skill manifest schema is incompatible", EXIT.CONFIG);
363
+ }
364
+ const entry = manifest.skills.find((candidate) => candidate && candidate.id === skillId);
365
+ if (
366
+ !entry
367
+ || entry.profile !== "cli"
368
+ || entry.package_version !== manifest.package_version
369
+ || typeof entry.content !== "string"
370
+ || Buffer.byteLength(entry.content, "utf8") > SKILL_CONTENT_MAX_BYTES
371
+ || !SHA256_PATTERN.test(entry.content_sha256)
372
+ || !SHA256_PATTERN.test(entry.bundle_sha256)
373
+ || createHash("sha256").update(entry.content, "utf8").digest("hex") !== entry.content_sha256
374
+ ) {
375
+ throw new BridgeError("RainSkills Skill manifest entry is invalid", EXIT.CONFIG);
376
+ }
377
+ return {
378
+ skillId,
379
+ rootSkillId: rootSkillId || skillId,
380
+ packageVersion: entry.package_version,
381
+ sourceRevision: entry.source_revision || null,
382
+ contentSha256: entry.content_sha256,
383
+ bundleSha256: entry.bundle_sha256,
384
+ content: entry.content,
385
+ };
386
+ }
387
+
236
388
  function classifyTool(toolName) {
237
389
  const normalized = String(toolName || "").toLowerCase();
238
390
  if (RISK_POLICY.destructiveFragments.some((fragment) => normalized.includes(fragment))) {
@@ -298,13 +450,20 @@ function readOperation(config, operationId) {
298
450
  }
299
451
  }
300
452
 
301
- function prepareOperation(config, toolName, operationClass, argumentsValue) {
453
+ function prepareOperation(config, toolName, operationClass, argumentsValue, skillBinding) {
302
454
  const operationId = randomUUID();
303
455
  const record = {
304
456
  operation_id: operationId,
305
457
  tool_name: toolName,
306
458
  operation_class: operationClass,
307
459
  arguments_digest: argumentsDigest(argumentsValue),
460
+ runtime_operation_id: config.operationId,
461
+ skill_id: skillBinding.skillId,
462
+ root_skill_id: skillBinding.rootSkillId,
463
+ skill_package_version: skillBinding.packageVersion,
464
+ skill_source_revision: skillBinding.sourceRevision,
465
+ skill_content_sha256: skillBinding.contentSha256,
466
+ skill_bundle_sha256: skillBinding.bundleSha256,
308
467
  policy_version: RISK_POLICY.version,
309
468
  created_at: new Date().toISOString(),
310
469
  status: "awaiting_confirmation",
@@ -318,12 +477,19 @@ function confirmOperation(
318
477
  operationId,
319
478
  toolName,
320
479
  operationClass,
321
- argumentsValue
480
+ argumentsValue,
481
+ skillBinding
322
482
  ) {
323
483
  const record = readOperation(config, operationId);
324
484
  if (!record || record.status !== "awaiting_confirmation" || record.tool_name !== toolName ||
325
485
  record.operation_class !== operationClass ||
326
- record.arguments_digest !== argumentsDigest(argumentsValue)) {
486
+ record.arguments_digest !== argumentsDigest(argumentsValue) ||
487
+ record.runtime_operation_id !== config.operationId ||
488
+ record.skill_id !== skillBinding.skillId ||
489
+ record.root_skill_id !== skillBinding.rootSkillId ||
490
+ record.skill_package_version !== skillBinding.packageVersion ||
491
+ record.skill_content_sha256 !== skillBinding.contentSha256 ||
492
+ record.skill_bundle_sha256 !== skillBinding.bundleSha256) {
327
493
  throw new BridgeError("operation confirmation is missing or does not match this tool", EXIT.USAGE);
328
494
  }
329
495
  let claim;
@@ -339,6 +505,26 @@ function confirmOperation(
339
505
  return record;
340
506
  }
341
507
 
508
+ function buildAuditMetadata(operation, skillBinding) {
509
+ return {
510
+ schema: "rainskills.operation-meta.v1",
511
+ operation_id: operation.operation_id,
512
+ runtime_operation_id: operation.runtime_operation_id,
513
+ cli_version: CLI_VERSION,
514
+ confirmation_type: "rainskills_cli",
515
+ root_skill_id: skillBinding.rootSkillId,
516
+ skill: {
517
+ id: skillBinding.skillId,
518
+ profile: "cli",
519
+ package_version: skillBinding.packageVersion,
520
+ source_revision: skillBinding.sourceRevision,
521
+ content_sha256: skillBinding.contentSha256,
522
+ bundle_sha256: skillBinding.bundleSha256,
523
+ content: skillBinding.content,
524
+ },
525
+ };
526
+ }
527
+
342
528
  function updateOperation(config, record, status) {
343
529
  writeOperation(config, { ...record, status, updated_at: new Date().toISOString() });
344
530
  }
@@ -581,6 +767,8 @@ async function execute(command, config) {
581
767
  return {
582
768
  status: "ok",
583
769
  cli_version: CLI_VERSION,
770
+ operation_id: config.operationId,
771
+ environment_id: config.environmentId,
584
772
  tool_count: tools.length,
585
773
  catalog_age_ms: cached ? Math.max(0, Math.round(cached.age)) : 0,
586
774
  };
@@ -611,17 +799,23 @@ async function execute(command, config) {
611
799
  }
612
800
  const argumentsValue = command.argumentsValue || readArguments(command.input);
613
801
  let operation = null;
802
+ let skillBinding = null;
614
803
  if (operationClass !== "read") {
804
+ if (!command.skillId) {
805
+ throw new BridgeError("mutable calls require --skill-id <active-skill-id>", EXIT.USAGE);
806
+ }
807
+ skillBinding = loadSkillBinding(config, command.skillId, command.rootSkillId);
615
808
  if (!command.confirmation) {
616
809
  operation = prepareOperation(
617
810
  config,
618
811
  command.toolName,
619
812
  operationClass,
620
- argumentsValue
813
+ argumentsValue,
814
+ skillBinding
621
815
  );
622
816
  return {
623
817
  requires_confirmation: true,
624
- operation_id: operation.operation_id,
818
+ confirmation_id: operation.operation_id,
625
819
  operation_class: operation.operation_class,
626
820
  summary: "Review the target and repeat this exact call with --confirm <operation_id> to execute it once.",
627
821
  };
@@ -631,15 +825,22 @@ async function execute(command, config) {
631
825
  command.confirmation,
632
826
  command.toolName,
633
827
  operationClass,
634
- argumentsValue
828
+ argumentsValue,
829
+ skillBinding
635
830
  );
636
831
  }
637
832
 
638
833
  try {
639
- const result = await rpcRequest(config, "tools/call", {
834
+ const params = {
640
835
  name: command.toolName,
641
836
  arguments: argumentsValue,
642
- });
837
+ };
838
+ if (operation) {
839
+ params._meta = {
840
+ "com.rainbond/rainskills": buildAuditMetadata(operation, skillBinding),
841
+ };
842
+ }
843
+ const result = await rpcRequest(config, "tools/call", params);
643
844
  if (!result || typeof result !== "object") {
644
845
  throw new BridgeError("Rainbond tool result is invalid", EXIT.TRANSPORT);
645
846
  }
@@ -677,7 +878,10 @@ async function main(args = process.argv.slice(2)) {
677
878
  command.argumentsValue = argumentsValue;
678
879
  argumentRedactions = collectArgumentRedactions(argumentsValue);
679
880
  }
680
- config = loadConfig({ includeRuntime: true });
881
+ config = loadConfig({
882
+ includeRuntime: true,
883
+ operationId: command.operationId,
884
+ });
681
885
  if (config.isInsecureHttp) {
682
886
  process.stderr.write('{"warning":"using insecure HTTP transport"}\n');
683
887
  }
package/bin/rainskills.js CHANGED
@@ -332,39 +332,6 @@ function parseOperationBeginArgs(args) {
332
332
  return { operationId, environmentId: environmentId || undefined, intent };
333
333
  }
334
334
 
335
- function parseMcpServeArgs(args) {
336
- if (
337
- args.length !== 4
338
- || args[0] !== "mcp"
339
- || args[1] !== "serve"
340
- || args[2] !== "--client"
341
- || !["codex", "claude", "pi", "generic"].includes(args[3])
342
- ) {
343
- throw new Error("mcp serve 参数无效");
344
- }
345
- return { client: args[3] };
346
- }
347
-
348
- async function defaultMcpServerRunner({
349
- environmentRegistry,
350
- environmentCredentialStore,
351
- operationStore,
352
- }) {
353
- const { createMcpRouter } = require(
354
- "../rainbond-platform-installer/scripts/mcp-router.js"
355
- );
356
- const { serveStdio } = require(
357
- "../rainbond-platform-installer/scripts/mcp-server.js"
358
- );
359
- return serveStdio({
360
- router: createMcpRouter({
361
- registry: environmentRegistry,
362
- credentialStore: environmentCredentialStore,
363
- operationStore,
364
- }),
365
- });
366
- }
367
-
368
335
  function runAttached(executable, args, { env = process.env } = {}) {
369
336
  return new Promise((resolve, reject) => {
370
337
  const child = spawn(executable, args, { env, stdio: "inherit" });
@@ -434,7 +401,6 @@ async function runBuiltin(args, {
434
401
  credentialEnvironment = process.env,
435
402
  credentialPersister,
436
403
  connectedCredentialReader,
437
- mcpServerRunner = defaultMcpServerRunner,
438
404
  } = {}) {
439
405
  let environmentServices;
440
406
  const getEnvironmentServices = () => {
@@ -457,17 +423,6 @@ async function runBuiltin(args, {
457
423
  ).createRuntimeStateManager(operationScoped ? { operationId } : {});
458
424
  };
459
425
 
460
- if (args[0] === "mcp" && args[1] === "serve") {
461
- const { client } = parseMcpServeArgs(args);
462
- await mcpServerRunner({
463
- client,
464
- environmentRegistry: getEnvironmentRegistry(),
465
- environmentCredentialStore: getEnvironmentCredentialStore(),
466
- operationStore: getOperationStore(),
467
- });
468
- return true;
469
- }
470
-
471
426
  if (args[0] === "environment" && args[1] === "list") {
472
427
  if (args.length !== 3 || args[2] !== "--json") {
473
428
  throw new Error("environment list 只支持固定参数 --json");
@@ -998,6 +953,10 @@ function resolveInvocation(args, {
998
953
  "windows-onboarding.js"
999
954
  );
1000
955
 
956
+ if (args[0] === "mcp") {
957
+ throw new Error("Rainskills 不再提供本地 MCP 服务,请使用本地 CLI");
958
+ }
959
+
1001
960
  if (args[0] === "tools") {
1002
961
  return {
1003
962
  executable: execPath,