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.
- package/README.md +14 -14
- package/SKILL.md +2 -2
- package/bin/rainskills-tools.js +229 -25
- package/bin/rainskills.js +4 -45
- package/install.sh +43 -332
- package/marketplace/rainskills/.claude-plugin/plugin.json +1 -1
- package/marketplace/rainskills/.codex-plugin/plugin.json +1 -1
- package/marketplace/rainskills/skills/rainskills/SKILL.md +3 -3
- package/package.json +11 -16
- package/rainbond-app-assistant/SKILL.md +7 -5
- package/rainbond-app-version-assistant/SKILL.md +6 -4
- package/rainbond-delivery-verifier/SKILL.md +6 -4
- package/rainbond-env-sync/SKILL.md +6 -4
- package/rainbond-fullstack-bootstrap/SKILL.md +7 -5
- package/rainbond-fullstack-troubleshooter/SKILL.md +6 -4
- package/rainbond-opensource-app-deploy/SKILL.md +249 -0
- package/rainbond-opensource-app-deploy/agents/openai.yaml +4 -0
- package/rainbond-opensource-app-deploy/references/failure-mode-playbook.md +78 -0
- package/rainbond-platform-installer/SKILL.md +1 -1
- package/rainbond-platform-installer/scripts/installed-version.js +1 -1
- package/rainbond-platform-installer/scripts/runtime-intents.js +7 -0
- package/rainbond-platform-installer/scripts/windows-client-config.js +0 -96
- package/rainbond-platform-installer/scripts/windows-onboarding.js +35 -70
- package/rainbond-platform-query/SKILL.md +6 -4
- package/rainbond-project-init/SKILL.md +7 -5
- package/rainbond-template-installer/SKILL.md +7 -5
- package/scripts/build-skill-manifest.mjs +180 -0
- package/scripts/install-local-cli.mjs +174 -0
- package/pi/rainskills-mcp.ts +0 -15745
- package/rainbond-platform-installer/scripts/mcp-router.js +0 -212
- 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
|
|
44
|
+
安装流程支持 Codex 和 Claude Code;macOS、Linux 和 WSL 不支持 OpenClaw 安装。不要手工复制 Skill、拼接凭据或修改 Agent 配置。
|
|
45
45
|
|
|
46
46
|
## 安装完成时用户会看到什么
|
|
47
47
|
|
|
48
|
-
安装完成后只完成 Skills
|
|
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.
|
|
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"]
|
|
81
|
-
2. 每个请求生成独立 operation UUID,并执行 `operation begin`;显式环境只写入这次 operation
|
|
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
|
-
-
|
|
144
|
+
- 添加、重命名、重新授权或删除环境不改项目配置,也不需要为每个 Agent 单独配置 MCP。
|
|
145
145
|
- 新增环境直接建立独立连接,不读取或恢复旧版单环境状态;HTTPS 直接进入授权,明文 HTTP 只增加一次可信网络确认。
|
|
146
146
|
- 新增环境与首次部署复用同一套固定选择:先选“云端环境(免费体验)/私有环境(去对接)”;选择私有环境后直接选“部署到本机/部署到独立服务器/部署到已有 Rainbond”,不再出现旧的接入方式中间步骤。
|
|
147
|
-
- Agent
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
231
|
+
发现更高的正式版后,后台任务只委托到经过校验的精确版本,例如 `rainskills@0.1.8-rc.2`,不会执行浮动的 `@latest` 业务代码。新版本原子刷新已经安装的 Rainskills Skills;当前业务继续使用启动时已经加载的版本,最迟从下一条新任务开始使用新版。npm 超时、版本检查失败、安装位置不安全或文件迁移失败时会保留旧版本,且不会阻塞或改变当前操作。
|
|
232
232
|
|
|
233
|
-
升级只更新 Rainskills 自身,不触发 Rainbond
|
|
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
|
|
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.
|
|
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>`。
|
package/bin/rainskills-tools.js
CHANGED
|
@@ -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
|
|
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
|
|
153
|
-
if (
|
|
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 (
|
|
156
|
-
if (
|
|
157
|
-
return { command, prefix:
|
|
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" &&
|
|
161
|
-
return { command, toolName:
|
|
162
|
-
}
|
|
163
|
-
if (command === "read" &&
|
|
164
|
-
return { command, toolName:
|
|
165
|
-
}
|
|
166
|
-
if (command === "call" &&
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
|
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({
|
|
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,
|