rainskills 0.1.19 → 0.1.20
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 +30 -29
- package/SKILL.md +12 -10
- package/bin/rainskills-tools.js +114 -240
- package/bin/rainskills.js +134 -591
- package/install.sh +68 -94
- 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 +13 -11
- package/package.json +2 -2
- package/rainbond-app-assistant/SKILL.md +112 -141
- package/rainbond-app-version-assistant/SKILL.md +98 -98
- package/rainbond-delivery-verifier/SKILL.md +98 -68
- package/rainbond-env-sync/SKILL.md +98 -69
- package/rainbond-fullstack-bootstrap/SKILL.md +97 -79
- package/rainbond-fullstack-bootstrap/modules/10-context-loading.md +1 -1
- package/rainbond-fullstack-troubleshooter/SKILL.md +98 -68
- package/rainbond-opensource-app-deploy/SKILL.md +98 -71
- package/rainbond-platform-installer/SKILL.md +2 -2
- package/rainbond-platform-installer/scripts/auto-update.js +0 -9
- package/rainbond-platform-installer/scripts/installed-version.js +1 -1
- package/rainbond-platform-installer/scripts/platform-installer.js +3 -89
- package/rainbond-platform-installer/scripts/runtime-state.js +14 -251
- package/rainbond-platform-installer/scripts/single-runtime.js +114 -0
- package/rainbond-platform-installer/scripts/user-message.js +0 -48
- package/rainbond-platform-installer/scripts/windows-client-config.js +0 -51
- package/rainbond-platform-installer/scripts/windows-onboarding.js +0 -3
- package/rainbond-platform-query/SKILL.md +103 -61
- package/rainbond-project-init/SKILL.md +98 -69
- package/rainbond-template-installer/SKILL.md +98 -70
- package/rainbond-platform-installer/scripts/environment-credentials.js +0 -225
- package/rainbond-platform-installer/scripts/environment-registry.js +0 -349
- package/rainbond-platform-installer/scripts/local-runtime-commands.js +0 -180
- package/rainbond-platform-installer/scripts/local-runtime.js +0 -58
- package/rainbond-platform-installer/scripts/runtime-context-resolver.js +0 -189
- package/rainbond-platform-installer/scripts/runtime-credentials.js +0 -163
- package/rainbond-platform-installer/scripts/runtime-intents.js +0 -386
- package/rainbond-platform-installer/scripts/runtime-operations.js +0 -597
- package/rainbond-platform-installer/scripts/windows-client-config.ps1 +0 -9
- package/rainbond-platform-installer/scripts/windows-read-user-environment.ps1 +0 -16
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ Rainskills 安装完成,下一条消息即可直接使用。
|
|
|
67
67
|
|
|
68
68
|
每个业务 Skill 都必须先通过相同门禁:
|
|
69
69
|
|
|
70
|
-
0. 第一步检查 Node.js 是否存在且主版本不低于 18。缺失或低于 18 时,只说明 Rainskills 执行组件需要 Node.js 18
|
|
70
|
+
0. 第一步检查 Node.js 是否存在且主版本不低于 18。缺失或低于 18 时,只说明 Rainskills 执行组件需要 Node.js 18 或更高版本并停止;不连接运行环境、不调用 CLI、不猜测替代命令。
|
|
71
71
|
|
|
72
72
|
固定 launcher 来自当前 `package.json` 版本:
|
|
73
73
|
|
|
@@ -77,12 +77,11 @@ Rainskills 安装完成,下一条消息即可直接使用。
|
|
|
77
77
|
|
|
78
78
|
所有调用都把 launcher 与参数合并成 argv 数组后直接执行;不得使用 `rainskills@latest`,不得把参数拼成 shell 字符串。
|
|
79
79
|
|
|
80
|
-
1.
|
|
81
|
-
2.
|
|
82
|
-
3.
|
|
83
|
-
4.
|
|
84
|
-
5. 安装私有环境时,只消费完成 schema、action、onboarding id 和参数边界校验的 `rainskills.next-action.v1.argv
|
|
85
|
-
6. 探针失败进入 reconnect。连接或安装完成后执行 launcher + `["intent", "resume", "--onboarding-id", "<同一 onboarding-id>"]`,恢复受保护的原始 intent 和 `resume_step`,不重新猜测用户动作。
|
|
80
|
+
1. 本会话首次使用时执行 launcher + `["runtime", "status", "--json"]`。只有返回 `connected` 且 `usable=true` 才进入业务流程。
|
|
81
|
+
2. 本机只保存一个 Rainbond 运行环境。没有环境时使用 `runtime connect` 连接 Cloud、已有私有 Rainbond,或安装一个私有 Rainbond;重新授权使用 `runtime reconnect <target>`。
|
|
82
|
+
3. 所有 Rainbond 查询和变更都直接通过 `~/.rainbond/bin/rainskills-tools.js`。不配置客户端 MCP,不创建业务 runtime operation,不传环境 ID、operation ID 或 intent JSON。
|
|
83
|
+
4. 写操作仍由 CLI 生成 confirmation ID;只有完全相同的输入追加 `--confirm` 后才执行一次。
|
|
84
|
+
5. 安装私有环境时,只消费完成 schema、action、onboarding id 和参数边界校验的 `rainskills.next-action.v1.argv`;onboarding id 只属于平台安装断点。
|
|
86
85
|
|
|
87
86
|
`deploy`/`create` 允许先只保存动作类型。选择私有环境后,先完整完成本地单机、服务器单机、主机集群或已有 Kubernetes 的平台安装和验收;此阶段不得询问本地项目路径、Git 仓库 URL、镜像地址或安装包路径。恢复到 `project-analysis` 后才识别当前项目或询问缺失的应用来源。
|
|
88
87
|
|
|
@@ -132,31 +131,35 @@ Rainskills 安装完成,下一条消息即可直接使用。
|
|
|
132
131
|
5. 指纹和一次服务器密码均由系统 SSH 读取;两种命令都只准备公钥连接,不安装 Rainbond。全部节点处理完后,用户统一回复一次“已完成”。
|
|
133
132
|
6. Agent 使用同一版本、同一 `onboarding-id` 和原安装参数继续。不得重新询问环境、安装模式、节点或应用来源。
|
|
134
133
|
7. 后续传输、安装和验收全部使用免密 SSH;系统终端不会继续安装或授权。
|
|
135
|
-
8. Rainbond
|
|
134
|
+
8. Rainbond 验收通过后只进行一次浏览器授权;授权成功后由当前 Agent 会话继续应用任务。
|
|
136
135
|
|
|
137
136
|
主机密钥发生变化时直接阻断并要求人工核对,不自动替换 `known_hosts`。聊天中永远不接收密码、私钥或 Token。
|
|
138
137
|
|
|
139
|
-
##
|
|
138
|
+
## 单运行环境
|
|
140
139
|
|
|
141
|
-
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
-
|
|
146
|
-
- 新增环境与首次部署复用同一套固定选择:先选“云端环境(免费体验)/私有环境(去对接)”;选择私有环境后直接选“部署到本机/部署到独立服务器/部署到已有 Rainbond”,不再出现旧的接入方式中间步骤。
|
|
147
|
-
- Agent 始终执行同一个受保护的本地 Rainskills CLI。CLI 按 `operation_id` 从隔离凭据存储中选择目标 Rainbond;Rainskills 不提供本地 MCP 服务,Agent 也不得绕过 CLI 直接调用 Rainbond MCP。
|
|
148
|
-
- 明确说“团队”表示默认或指定运行环境中的 Rainbond 团队;明确说“运行环境/平台”表示环境。裸名称同时匹配两者时必须询问。
|
|
140
|
+
- 本机只保存一个 Rainbond Console origin 和对应凭据。
|
|
141
|
+
- 更换环境时,新凭据通过 live probe 后才覆盖当前运行环境。
|
|
142
|
+
- 连接和重新授权始终进入浏览器 Device Flow,不复用 Shell 中缓存的 JWT。
|
|
143
|
+
- Agent 始终执行受保护的本地 Rainskills CLI,不配置或直接调用客户端 MCP。
|
|
144
|
+
- 项目的 preview/production 配置仍属于应用配置投影,与本机连接的 Rainbond 运行环境不是同一概念。
|
|
149
145
|
|
|
150
|
-
|
|
146
|
+
业务 CLI 默认忽略 Shell 中遗留的 `RAINBOND_URL` / `RAINBOND_JWT`。CI 需要使用环境变量凭据时,显式设置:
|
|
151
147
|
|
|
152
148
|
```bash
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
node
|
|
149
|
+
RAINSKILLS_CREDENTIAL_SOURCE=environment \
|
|
150
|
+
RAINBOND_URL=https://rainbond.example.com \
|
|
151
|
+
RAINBOND_JWT="$RAINBOND_JWT" \
|
|
152
|
+
node ~/.rainbond/bin/rainskills-tools.js status --skill-id rainbond-platform-query
|
|
157
153
|
```
|
|
158
154
|
|
|
159
|
-
|
|
155
|
+
常用命令:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
node ~/.rainbond/lib/rainskills/bin/rainskills.js runtime status --json
|
|
159
|
+
node ~/.rainbond/lib/rainskills/bin/rainskills.js runtime reconnect codex
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`runtime status` 会通过当前唯一环境执行 live probe。业务 Tool 继续通过 `~/.rainbond/bin/rainskills-tools.js` 调用。
|
|
160
163
|
|
|
161
164
|
### 已有应用环境选择
|
|
162
165
|
|
|
@@ -164,15 +167,13 @@ node <已安装的 Skills 根目录>/rainbond-platform-installer/scripts/local-r
|
|
|
164
167
|
|
|
165
168
|
### 认证与权限恢复
|
|
166
169
|
|
|
167
|
-
Rainbond
|
|
170
|
+
Rainbond CLI 返回 401 时执行:
|
|
168
171
|
|
|
169
172
|
```json
|
|
170
|
-
["runtime", "
|
|
171
|
-
["runtime", "reconnect", "--onboarding-id", "<同一 onboarding-id>"]
|
|
172
|
-
["intent", "resume", "--onboarding-id", "<同一 onboarding-id>"]
|
|
173
|
+
["runtime", "reconnect", "<target>"]
|
|
173
174
|
```
|
|
174
175
|
|
|
175
|
-
|
|
176
|
+
只读调用可在重新授权后重试一次。写调用不得自动重放,必须先查询平台真实状态。403 立即停止,不 reconnect。
|
|
176
177
|
|
|
177
178
|
凭据只由受保护的本地状态和客户端配置处理。不要在聊天、intent、日志或命令行参数中发送密码、JWT、Token 或私钥。
|
|
178
179
|
|
|
@@ -228,7 +229,7 @@ node ~/.rainbond/lib/rainskills/bin/rainskills.js platform install --onboarding-
|
|
|
228
229
|
|
|
229
230
|
Rainskills 会在用户下一次发起业务动作时,由本地运行时立即返回环境查询结果,并另行启动后台任务静默检查更新。更新只跟随 npm `latest` 指向的正式版。当前版本是 RC 或其他预发布版本时不会查询、不会自动升级;npm 上的新 RC 版本也不参与正式版自动升级。
|
|
230
231
|
|
|
231
|
-
发现更高的正式版后,后台任务只委托到经过校验的精确版本,例如 `rainskills@0.1.
|
|
232
|
+
发现更高的正式版后,后台任务只委托到经过校验的精确版本,例如 `rainskills@0.1.20`,不会执行浮动的 `@latest` 业务代码。新版本原子刷新已经安装的 Rainskills Skills;当前业务继续使用启动时已经加载的版本,最迟从下一条新任务开始使用新版。npm 超时、版本检查失败、安装位置不安全或文件迁移失败时会保留旧版本,且不会阻塞或改变当前操作。
|
|
232
233
|
|
|
233
234
|
升级只更新 Rainskills 自身,不触发 Rainbond 安装、运行环境选择、登录授权或重新对接,也不会新增 Agent MCP 配置。更新内容仅包括 Skills 和本地 CLI。原始业务操作会继续执行;只有该业务操作本身需要运行环境时,才按既有门禁检查当前连接。可用连接直接复用,401 只重新授权一次,403 立即停止,从未连接过运行环境时才进入环境选择。
|
|
234
235
|
|
package/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: rainskills
|
|
3
|
-
description: Use when a user asks to install, set up, initialize, update, repair, start using Rainskills, or
|
|
3
|
+
description: Use when a user asks to install, set up, initialize, update, repair, start using Rainskills, or connect, reconnect, inspect, or replace the single Rainskills runtime.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Rainskills
|
|
@@ -18,7 +18,7 @@ If the adjacent installer is missing, check the local Node.js version before cho
|
|
|
18
18
|
|
|
19
19
|
Skills-only 安装不需要 Node.js;CDN fallback 只负责安装 Skill 文件,不代表运行环境连接、应用部署或平台安装已经可执行。用户首次提出需要运行环境的动作时,对应业务 Skill 才检查 Node.js;固定 Rainskills launcher 需要 Node.js 18 或更高版本。缺失或版本过低时保留原始 intent 并停止,等待用户或 agent 明确同意安装或升级 Node.js,安装完成消息不得提前提示 Node.js。
|
|
20
20
|
|
|
21
|
-
执行优化:同一会话内只检查一次 Node.js
|
|
21
|
+
执行优化:同一会话内只检查一次 Node.js 和运行环境状态;仅在 Node.js、Rainskills 或唯一运行环境发生变化后失效。固定 launcher 和 argv 已在本 Skill 中,禁止读取、搜索或探测 `rainskills.js`,也禁止执行 `npm root -g`。
|
|
22
22
|
|
|
23
23
|
## Completion Message
|
|
24
24
|
|
|
@@ -38,18 +38,20 @@ Rainskills 安装完成,下一条消息即可直接使用。
|
|
|
38
38
|
也可以直接告诉我你想部署什么应用。
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
## Manage Runtime
|
|
41
|
+
## Manage the Single Runtime
|
|
42
42
|
|
|
43
43
|
受限沙箱(包括 Codex)执行本地状态命令时,必须申请用户级受保护目录访问权限;在 Codex 中使用 `require_escalated`。不得修改 `~/.rainbond` 权限、复制受保护状态到工作区,或因沙箱权限错误建议重装。
|
|
44
44
|
|
|
45
45
|
涉及浏览器或设备授权的 `runtime connect`,以及恢复/安装场景中的 `rainskills <target> --self-hosted`,必须在附加交互终端(TTY)中运行;在 Codex 中设置 `tty: true` 并保持进程附着直到授权完成。禁止通过非交互命令要求用户粘贴 JWT;非交互模式只可复用已存在的受保护凭据。
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
Rainskills 只保存一个全局运行环境,不维护环境列表、名称、默认值或环境 ID。使用固定 launcher `node <home>/.rainbond/lib/rainskills/bin/rainskills.js`(运行包版本 `rainskills@0.1.20`):
|
|
48
48
|
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
49
|
+
- 状态:执行 `runtime status --json`。
|
|
50
|
+
- 首次连接:执行 `runtime connect <target> --saas` 或 `runtime connect <target> --rainbond-url <Console origin>`。
|
|
51
|
+
- 安装私有环境:执行 `runtime connect <target> --install-private --location local|server`。
|
|
52
|
+
- 重新授权:执行 `runtime reconnect <target>`;必须进入浏览器 Device Flow,不复用 Shell 中缓存的 JWT。
|
|
53
|
+
- 更换环境:明确告知用户会替换当前唯一环境;新授权和 live probe 成功后再覆盖凭据。
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
所有 Rainbond 查询和变更继续通过 `~/.rainbond/bin/rainskills-tools.js`。不得配置客户端 MCP,也不得恢复环境枚举、业务 operation 生命周期、环境 ID 或 intent 恢复协议。
|
|
56
|
+
|
|
57
|
+
业务 CLI 默认只读取受保护的唯一运行环境文件,忽略 Shell 中遗留的 `RAINBOND_URL` / `RAINBOND_JWT`。CI 若要显式使用环境变量凭据,必须同时设置 `RAINSKILLS_CREDENTIAL_SOURCE=environment`。
|
package/bin/rainskills-tools.js
CHANGED
|
@@ -17,8 +17,6 @@ const PROTOCOL_VERSION = "2025-03-26";
|
|
|
17
17
|
const ENDPOINT_PATH = "/console/mcp/rainskills/api/query";
|
|
18
18
|
const CLI_VERSION = "2.1.0";
|
|
19
19
|
const CONFIG_DIRECTORY = ".rainbond";
|
|
20
|
-
const CREDENTIALS_FILENAME = "credentials.env";
|
|
21
|
-
const LEGACY_CREDENTIALS_FILENAME = "mcp.env";
|
|
22
20
|
const CATALOG_FILENAME = "capabilities.json";
|
|
23
21
|
const OPERATIONS_DIRECTORY = "operations";
|
|
24
22
|
const SKILL_MANIFEST_FILENAME = "rainskills-skill-manifest.json";
|
|
@@ -76,62 +74,6 @@ class BridgeError extends Error {
|
|
|
76
74
|
}
|
|
77
75
|
}
|
|
78
76
|
|
|
79
|
-
function parseShellValue(value) {
|
|
80
|
-
let offset = 0;
|
|
81
|
-
let result = "";
|
|
82
|
-
while (offset < value.length) {
|
|
83
|
-
const quote = value[offset];
|
|
84
|
-
if (quote === "'") {
|
|
85
|
-
const end = value.indexOf("'", offset + 1);
|
|
86
|
-
if (end === -1) return null;
|
|
87
|
-
result += value.slice(offset + 1, end);
|
|
88
|
-
offset = end + 1;
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (quote === '"') {
|
|
92
|
-
const end = value.indexOf('"', offset + 1);
|
|
93
|
-
if (end === -1) return null;
|
|
94
|
-
result += value.slice(offset + 1, end).replace(/\\([\\"$`])/g, "$1");
|
|
95
|
-
offset = end + 1;
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
const match = value.slice(offset).match(/^[^\s'"#]+/);
|
|
99
|
-
if (!match) return null;
|
|
100
|
-
result += match[0];
|
|
101
|
-
offset += match[0].length;
|
|
102
|
-
}
|
|
103
|
-
return result;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function parseEnvFile(contents) {
|
|
107
|
-
const values = {};
|
|
108
|
-
for (const line of contents.split(/\r?\n/)) {
|
|
109
|
-
const match = line.match(/^\s*(?:export\s+)?(RAINBOND_URL|RAINBOND_JWT|RAINBOND_ALLOW_INSECURE_HTTP)\s*=\s*(.*?)\s*$/);
|
|
110
|
-
if (!match) continue;
|
|
111
|
-
const parsed = parseShellValue(match[2]);
|
|
112
|
-
if (parsed !== null) values[match[1]] = parsed;
|
|
113
|
-
}
|
|
114
|
-
return values;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function readPrivateEnvFile(configPath) {
|
|
118
|
-
let stat;
|
|
119
|
-
try {
|
|
120
|
-
stat = fs.lstatSync(configPath);
|
|
121
|
-
} catch (error) {
|
|
122
|
-
if (error.code === "ENOENT") return {};
|
|
123
|
-
throw new BridgeError("unable to inspect Rainbond configuration", EXIT.CONFIG);
|
|
124
|
-
}
|
|
125
|
-
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0) {
|
|
126
|
-
throw new BridgeError("Rainbond configuration must be a private regular file", EXIT.CONFIG);
|
|
127
|
-
}
|
|
128
|
-
try {
|
|
129
|
-
return parseEnvFile(fs.readFileSync(configPath, "utf8"));
|
|
130
|
-
} catch (_error) {
|
|
131
|
-
throw new BridgeError("unable to read Rainbond configuration", EXIT.CONFIG);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
77
|
function requireRuntimeModule(filename) {
|
|
136
78
|
const candidates = [
|
|
137
79
|
path.resolve(__dirname, "..", "rainbond-platform-installer", "scripts", filename),
|
|
@@ -166,69 +108,25 @@ function requireRuntimeFile(...segments) {
|
|
|
166
108
|
throw new BridgeError("installed Rainskills package upload helper is incomplete", EXIT.CONFIG);
|
|
167
109
|
}
|
|
168
110
|
|
|
169
|
-
function loadOperationConfig({ homeDir, operationId, includeRuntime }) {
|
|
170
|
-
let operationStore;
|
|
171
|
-
const { createEnvironmentRegistry } = requireRuntimeModule("environment-registry.js");
|
|
172
|
-
const { createEnvironmentCredentialStore } = requireRuntimeModule("environment-credentials.js");
|
|
173
|
-
const { createRuntimeOperationStore } = requireRuntimeModule("runtime-operations.js");
|
|
174
|
-
const registry = createEnvironmentRegistry({
|
|
175
|
-
home: homeDir,
|
|
176
|
-
activeOperationIds: (environmentId) => operationStore?.activeOperationIds(environmentId) || [],
|
|
177
|
-
});
|
|
178
|
-
operationStore = createRuntimeOperationStore({ home: homeDir, registry });
|
|
179
|
-
const operation = operationStore.read(operationId);
|
|
180
|
-
if (!operation || !["active", "interrupted"].includes(operation.stage)) {
|
|
181
|
-
throw new BridgeError("protected runtime operation is missing or inactive", EXIT.CONFIG);
|
|
182
|
-
}
|
|
183
|
-
const environment = registry.get(operation.environment_id);
|
|
184
|
-
if (!environment || environment.connection_state !== "connected") {
|
|
185
|
-
throw new BridgeError("the operation environment is unavailable", EXIT.CONFIG);
|
|
186
|
-
}
|
|
187
|
-
const credential = createEnvironmentCredentialStore({ home: homeDir }).read({
|
|
188
|
-
environmentId: environment.id,
|
|
189
|
-
expectedOrigin: environment.console_origin,
|
|
190
|
-
});
|
|
191
|
-
const parsed = new URL(environment.console_origin);
|
|
192
|
-
const { INTENT_DEFINITIONS } = requireRuntimeModule("runtime-intents.js");
|
|
193
|
-
const requiredSkillId = INTENT_DEFINITIONS[operation.intent.type]?.skillId;
|
|
194
|
-
if (!requiredSkillId) {
|
|
195
|
-
throw new BridgeError("the protected runtime intent has no Skill binding", EXIT.CONFIG);
|
|
196
|
-
}
|
|
197
|
-
return {
|
|
198
|
-
baseUrl: environment.console_origin,
|
|
199
|
-
jwt: credential.token,
|
|
200
|
-
operationId: operation.operation_id,
|
|
201
|
-
environmentId: environment.id,
|
|
202
|
-
intent: operation.intent,
|
|
203
|
-
requiredSkillId,
|
|
204
|
-
operationStore,
|
|
205
|
-
...(includeRuntime ? {
|
|
206
|
-
homeDir,
|
|
207
|
-
allowInsecureHttp: parsed.protocol === "http:",
|
|
208
|
-
isInsecureHttp: parsed.protocol === "http:",
|
|
209
|
-
} : {}),
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
111
|
function loadConfig({
|
|
214
112
|
env = process.env,
|
|
215
113
|
homeDir = os.homedir(),
|
|
216
114
|
includeRuntime = false,
|
|
217
|
-
operationId,
|
|
218
115
|
} = {}) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
116
|
+
const hasEnvironmentOverride = env.RAINSKILLS_CREDENTIAL_SOURCE === "environment";
|
|
117
|
+
if (hasEnvironmentOverride && !(env.RAINBOND_URL && env.RAINBOND_JWT)) {
|
|
118
|
+
throw new BridgeError(
|
|
119
|
+
"RAINBOND_URL and RAINBOND_JWT must be provided together",
|
|
120
|
+
EXIT.CONFIG
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const stored = hasEnvironmentOverride
|
|
124
|
+
? null
|
|
125
|
+
: requireRuntimeModule("single-runtime.js").createSingleRuntimeStore({ home: homeDir }).read();
|
|
126
|
+
const baseUrl = hasEnvironmentOverride ? env.RAINBOND_URL : stored?.console_origin;
|
|
127
|
+
const jwt = hasEnvironmentOverride ? env.RAINBOND_JWT : stored?.token;
|
|
230
128
|
if (!baseUrl || !jwt) {
|
|
231
|
-
throw new BridgeError("
|
|
129
|
+
throw new BridgeError("no Rainbond runtime is configured", EXIT.CONFIG);
|
|
232
130
|
}
|
|
233
131
|
|
|
234
132
|
let parsed;
|
|
@@ -241,9 +139,9 @@ function loadConfig({
|
|
|
241
139
|
parsed.password || parsed.search || parsed.hash) {
|
|
242
140
|
throw new BridgeError("RAINBOND_URL must be a safe HTTP(S) URL", EXIT.CONFIG);
|
|
243
141
|
}
|
|
244
|
-
const allowInsecureHttp =
|
|
245
|
-
|
|
246
|
-
|
|
142
|
+
const allowInsecureHttp = hasEnvironmentOverride
|
|
143
|
+
? env.RAINBOND_ALLOW_INSECURE_HTTP === "true"
|
|
144
|
+
: stored.allow_insecure_http;
|
|
247
145
|
if (parsed.protocol === "http:" && !allowInsecureHttp) {
|
|
248
146
|
throw new BridgeError(
|
|
249
147
|
"RAINBOND_URL must use HTTPS; explicitly authorize insecure HTTP during installation",
|
|
@@ -262,101 +160,31 @@ function loadConfig({
|
|
|
262
160
|
return config;
|
|
263
161
|
}
|
|
264
162
|
|
|
265
|
-
function loadEnvironmentConfig({
|
|
266
|
-
homeDir = os.homedir(),
|
|
267
|
-
environmentId,
|
|
268
|
-
} = {}) {
|
|
269
|
-
const { createEnvironmentRegistry } = requireRuntimeModule("environment-registry.js");
|
|
270
|
-
const { createEnvironmentCredentialStore } = requireRuntimeModule("environment-credentials.js");
|
|
271
|
-
const registry = createEnvironmentRegistry({ home: homeDir });
|
|
272
|
-
const snapshot = registry.read();
|
|
273
|
-
const selectedId = environmentId || snapshot.default_environment_id;
|
|
274
|
-
if (!selectedId) {
|
|
275
|
-
throw new BridgeError("no default Rainbond environment is configured", EXIT.CONFIG);
|
|
276
|
-
}
|
|
277
|
-
const environment = snapshot.environments.find((entry) => entry.id === selectedId);
|
|
278
|
-
if (!environment || environment.connection_state !== "connected") {
|
|
279
|
-
throw new BridgeError("the selected Rainbond environment is unavailable", EXIT.CONFIG);
|
|
280
|
-
}
|
|
281
|
-
let credential;
|
|
282
|
-
try {
|
|
283
|
-
credential = createEnvironmentCredentialStore({ home: homeDir }).read({
|
|
284
|
-
environmentId: environment.id,
|
|
285
|
-
expectedOrigin: environment.console_origin,
|
|
286
|
-
});
|
|
287
|
-
} catch (_error) {
|
|
288
|
-
throw new BridgeError("the selected Rainbond environment credential is unavailable", EXIT.CONFIG);
|
|
289
|
-
}
|
|
290
|
-
const parsed = new URL(environment.console_origin);
|
|
291
|
-
return {
|
|
292
|
-
baseUrl: environment.console_origin,
|
|
293
|
-
jwt: credential.token,
|
|
294
|
-
environmentId: environment.id,
|
|
295
|
-
homeDir,
|
|
296
|
-
allowInsecureHttp: parsed.protocol === "http:",
|
|
297
|
-
isInsecureHttp: parsed.protocol === "http:",
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
|
|
301
163
|
function parseCommand(args) {
|
|
302
|
-
if (args
|
|
303
|
-
|
|
304
|
-
if (!Object.hasOwn(PLATFORM_QUERY_TO_RESOURCE, toolName || "")) {
|
|
305
|
-
throw new BridgeError("platform query tool is not allowed", EXIT.USAGE);
|
|
306
|
-
}
|
|
307
|
-
let environmentId;
|
|
308
|
-
let input;
|
|
309
|
-
for (let index = 2; index < args.length; index += 2) {
|
|
310
|
-
const option = args[index];
|
|
311
|
-
const value = args[index + 1];
|
|
312
|
-
if (!value || value.startsWith("--")) {
|
|
313
|
-
throw new BridgeError("platform query option requires a value", EXIT.USAGE);
|
|
314
|
-
}
|
|
315
|
-
if (option === "--environment-id" && !environmentId && OPERATION_ID_PATTERN.test(value)) {
|
|
316
|
-
environmentId = value;
|
|
317
|
-
} else if (option === "--input" && input === undefined && value === "-") {
|
|
318
|
-
input = value;
|
|
319
|
-
} else {
|
|
320
|
-
throw new BridgeError("invalid platform query command", EXIT.USAGE);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
if (input !== "-") throw new BridgeError("platform query requires --input -", EXIT.USAGE);
|
|
324
|
-
return { command: "query", toolName, input, environmentId };
|
|
164
|
+
if (args.includes("--operation-id") || args.includes("--environment-id")) {
|
|
165
|
+
throw new BridgeError("--operation-id and --environment-id are no longer supported", EXIT.USAGE);
|
|
325
166
|
}
|
|
326
|
-
const
|
|
327
|
-
if (
|
|
328
|
-
operationIndex < 0
|
|
329
|
-
|| !OPERATION_ID_PATTERN.test(args[operationIndex + 1] || "")
|
|
330
|
-
|| args.indexOf("--operation-id", operationIndex + 1) !== -1
|
|
331
|
-
) {
|
|
332
|
-
throw new BridgeError("every tools command requires --operation-id <uuid>", EXIT.USAGE);
|
|
333
|
-
}
|
|
334
|
-
const operationId = args[operationIndex + 1];
|
|
335
|
-
const operationArgs = [
|
|
336
|
-
...args.slice(0, operationIndex),
|
|
337
|
-
...args.slice(operationIndex + 2),
|
|
338
|
-
];
|
|
339
|
-
const skillIndex = operationArgs.indexOf("--skill-id");
|
|
167
|
+
const skillIndex = args.indexOf("--skill-id");
|
|
340
168
|
if (
|
|
341
169
|
skillIndex < 0
|
|
342
|
-
|| !SKILL_ID_PATTERN.test(
|
|
343
|
-
||
|
|
170
|
+
|| !SKILL_ID_PATTERN.test(args[skillIndex + 1] || "")
|
|
171
|
+
|| args.indexOf("--skill-id", skillIndex + 1) !== -1
|
|
344
172
|
) {
|
|
345
173
|
throw new BridgeError(
|
|
346
174
|
"every tools command requires --skill-id <active-skill-id>",
|
|
347
175
|
EXIT.USAGE
|
|
348
176
|
);
|
|
349
177
|
}
|
|
350
|
-
const skillId =
|
|
178
|
+
const skillId = args[skillIndex + 1];
|
|
351
179
|
const remaining = [
|
|
352
|
-
...
|
|
353
|
-
...
|
|
180
|
+
...args.slice(0, skillIndex),
|
|
181
|
+
...args.slice(skillIndex + 2),
|
|
354
182
|
];
|
|
355
183
|
const command = remaining[0];
|
|
356
|
-
const context = {
|
|
184
|
+
const context = { skillId };
|
|
357
185
|
if (
|
|
358
186
|
command === "context"
|
|
359
|
-
&& ["resolve"
|
|
187
|
+
&& remaining[1] === "resolve"
|
|
360
188
|
&& remaining.length === 4
|
|
361
189
|
&& remaining[2] === "--input"
|
|
362
190
|
&& remaining[3] === "-"
|
|
@@ -368,6 +196,18 @@ function parseCommand(args) {
|
|
|
368
196
|
...context,
|
|
369
197
|
};
|
|
370
198
|
}
|
|
199
|
+
if (
|
|
200
|
+
command === "query"
|
|
201
|
+
&& Object.hasOwn(PLATFORM_QUERY_TO_RESOURCE, remaining[1] || "")
|
|
202
|
+
&& remaining.length === 4
|
|
203
|
+
&& remaining[2] === "--input"
|
|
204
|
+
&& remaining[3] === "-"
|
|
205
|
+
) {
|
|
206
|
+
return { command, toolName: remaining[1], input: remaining[3], ...context };
|
|
207
|
+
}
|
|
208
|
+
if (command === "query") {
|
|
209
|
+
throw new BridgeError("platform query tool is not allowed", EXIT.USAGE);
|
|
210
|
+
}
|
|
371
211
|
if (command === "status" && remaining.length === 1) return { command, ...context };
|
|
372
212
|
if (command === "list") {
|
|
373
213
|
if (remaining.length === 1) return { command, ...context };
|
|
@@ -434,11 +274,7 @@ function platformQueryIntent(toolName, argumentsValue) {
|
|
|
434
274
|
}
|
|
435
275
|
intent.app_id = String(value);
|
|
436
276
|
}
|
|
437
|
-
|
|
438
|
-
return requireRuntimeModule("runtime-intents.js").validateIntent(intent);
|
|
439
|
-
} catch (_error) {
|
|
440
|
-
throw new BridgeError("platform query intent is invalid", EXIT.USAGE);
|
|
441
|
-
}
|
|
277
|
+
return intent;
|
|
442
278
|
}
|
|
443
279
|
|
|
444
280
|
async function resolvePlatformQueryArguments(toolName, argumentsValue, config) {
|
|
@@ -602,9 +438,6 @@ function skillManifestPath(config) {
|
|
|
602
438
|
}
|
|
603
439
|
|
|
604
440
|
function loadSkillBinding(config, skillId, rootSkillId) {
|
|
605
|
-
if (skillId !== config.requiredSkillId) {
|
|
606
|
-
throw new BridgeError("Skill does not match the protected runtime operation intent", EXIT.CONFIG);
|
|
607
|
-
}
|
|
608
441
|
const target = skillManifestPath(config);
|
|
609
442
|
let manifest;
|
|
610
443
|
try {
|
|
@@ -725,7 +558,6 @@ function prepareOperation(config, toolName, operationClass, argumentsValue, skil
|
|
|
725
558
|
tool_name: toolName,
|
|
726
559
|
operation_class: operationClass,
|
|
727
560
|
arguments_digest: argumentsDigest(argumentsValue),
|
|
728
|
-
runtime_operation_id: config.operationId,
|
|
729
561
|
skill_id: skillBinding.skillId,
|
|
730
562
|
root_skill_id: skillBinding.rootSkillId,
|
|
731
563
|
skill_package_version: skillBinding.packageVersion,
|
|
@@ -752,7 +584,6 @@ function confirmOperation(
|
|
|
752
584
|
if (!record || record.status !== "awaiting_confirmation" || record.tool_name !== toolName ||
|
|
753
585
|
record.operation_class !== operationClass ||
|
|
754
586
|
record.arguments_digest !== argumentsDigest(argumentsValue) ||
|
|
755
|
-
record.runtime_operation_id !== config.operationId ||
|
|
756
587
|
record.skill_id !== skillBinding.skillId ||
|
|
757
588
|
record.root_skill_id !== skillBinding.rootSkillId ||
|
|
758
589
|
record.skill_package_version !== skillBinding.packageVersion ||
|
|
@@ -1182,28 +1013,82 @@ async function validateToolArguments(config, toolName, argumentsValue) {
|
|
|
1182
1013
|
if (validationError) throw new BridgeError(validationError, EXIT.USAGE);
|
|
1183
1014
|
}
|
|
1184
1015
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1016
|
+
function boundedContextString(value) {
|
|
1017
|
+
return typeof value === "string"
|
|
1018
|
+
&& value.length > 0
|
|
1019
|
+
&& value.length <= 128
|
|
1020
|
+
&& !/[\u0000-\u001f\u007f-\u009f]/u.test(value)
|
|
1021
|
+
? value
|
|
1022
|
+
: null;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
async function resolveStatelessContext(input, config) {
|
|
1026
|
+
const required = input?.required;
|
|
1027
|
+
if (!Array.isArray(required)
|
|
1028
|
+
|| required.some((item) => !["enterprise", "workspace"].includes(item))) {
|
|
1029
|
+
throw new BridgeError("context required dimensions are invalid", EXIT.USAGE);
|
|
1030
|
+
}
|
|
1031
|
+
const identity = await execute({
|
|
1032
|
+
command: "read",
|
|
1033
|
+
toolName: "rainbond_get_current_user",
|
|
1034
|
+
argumentsValue: {},
|
|
1035
|
+
}, config);
|
|
1036
|
+
const enterpriseId = boundedContextString(identity?.enterprise_id);
|
|
1037
|
+
if (required.includes("enterprise") && !enterpriseId) {
|
|
1038
|
+
return { state: "blocked", reason: "no-current-enterprise" };
|
|
1039
|
+
}
|
|
1040
|
+
if (!required.includes("workspace")) {
|
|
1041
|
+
return { state: "resolved", context: { enterprise_id: enterpriseId } };
|
|
1042
|
+
}
|
|
1043
|
+
const teams = await execute({
|
|
1044
|
+
command: "read",
|
|
1045
|
+
toolName: "rainbond_query_teams",
|
|
1046
|
+
argumentsValue: enterpriseId ? { enterprise_id: enterpriseId } : {},
|
|
1047
|
+
}, config);
|
|
1048
|
+
const options = [];
|
|
1049
|
+
for (const item of Array.isArray(teams?.items) ? teams.items : []) {
|
|
1050
|
+
const teamId = boundedContextString(item?.team_id || item?.tenant_id);
|
|
1051
|
+
const teamName = boundedContextString(item?.team_name || item?.tenant_name);
|
|
1052
|
+
const teamAlias = boundedContextString(item?.team_alias || item?.tenant_alias);
|
|
1053
|
+
if (!teamId || !teamName || !Array.isArray(item.region_list)) continue;
|
|
1054
|
+
for (const region of item.region_list) {
|
|
1055
|
+
const regionName = boundedContextString(region?.region_name);
|
|
1056
|
+
if (!regionName) continue;
|
|
1057
|
+
const regionLabel = boundedContextString(region.region_alias) || regionName;
|
|
1058
|
+
const workspaceLabel = teamAlias && teamAlias !== teamName
|
|
1059
|
+
? `${teamAlias}(${teamName})`
|
|
1060
|
+
: teamName;
|
|
1061
|
+
options.push({
|
|
1062
|
+
id: `${teamId}:${regionName}`,
|
|
1063
|
+
label: `${workspaceLabel} / ${regionLabel}`,
|
|
1064
|
+
team_id: teamId,
|
|
1065
|
+
team_name: teamName,
|
|
1066
|
+
region_name: regionName,
|
|
1200
1067
|
});
|
|
1068
|
+
if (options.length > 256) {
|
|
1069
|
+
throw new BridgeError("workspace candidate set is too large", EXIT.TOOL);
|
|
1070
|
+
}
|
|
1201
1071
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1072
|
+
}
|
|
1073
|
+
if (options.length === 0) return { state: "blocked", reason: "no-accessible-workspace" };
|
|
1074
|
+
if (options.length === 1) {
|
|
1075
|
+
const option = options[0];
|
|
1076
|
+
return {
|
|
1077
|
+
state: "resolved",
|
|
1078
|
+
context: {
|
|
1079
|
+
enterprise_id: enterpriseId,
|
|
1080
|
+
team_id: option.team_id,
|
|
1081
|
+
team_name: option.team_name,
|
|
1082
|
+
region_name: option.region_name,
|
|
1083
|
+
},
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
return { state: "needs-selection", dimension: "workspace-region", options };
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
async function execute(command, config) {
|
|
1090
|
+
if (command.command === "context") {
|
|
1091
|
+
return resolveStatelessContext(command.argumentsValue, config);
|
|
1207
1092
|
}
|
|
1208
1093
|
if (command.command === "package-upload") {
|
|
1209
1094
|
return executePackageUpload(command, config);
|
|
@@ -1214,8 +1099,6 @@ async function execute(command, config) {
|
|
|
1214
1099
|
return {
|
|
1215
1100
|
status: "ok",
|
|
1216
1101
|
cli_version: CLI_VERSION,
|
|
1217
|
-
operation_id: config.operationId,
|
|
1218
|
-
environment_id: config.environmentId,
|
|
1219
1102
|
tool_count: tools.length,
|
|
1220
1103
|
catalog_age_ms: cached ? Math.max(0, Math.round(cached.age)) : 0,
|
|
1221
1104
|
};
|
|
@@ -1327,7 +1210,7 @@ async function main(args = process.argv.slice(2)) {
|
|
|
1327
1210
|
argumentRedactions = collectArgumentRedactions(argumentsValue);
|
|
1328
1211
|
}
|
|
1329
1212
|
if (command.command === "query") {
|
|
1330
|
-
config =
|
|
1213
|
+
config = loadConfig({ includeRuntime: true });
|
|
1331
1214
|
command.argumentsValue = await resolvePlatformQueryArguments(
|
|
1332
1215
|
command.toolName,
|
|
1333
1216
|
command.argumentsValue,
|
|
@@ -1348,14 +1231,7 @@ async function main(args = process.argv.slice(2)) {
|
|
|
1348
1231
|
}
|
|
1349
1232
|
config = loadConfig({
|
|
1350
1233
|
includeRuntime: true,
|
|
1351
|
-
operationId: command.operationId,
|
|
1352
1234
|
});
|
|
1353
|
-
if (command.skillId !== config.requiredSkillId) {
|
|
1354
|
-
throw new BridgeError(
|
|
1355
|
-
"Skill does not match the protected runtime operation intent",
|
|
1356
|
-
EXIT.CONFIG
|
|
1357
|
-
);
|
|
1358
|
-
}
|
|
1359
1235
|
if (config.isInsecureHttp) {
|
|
1360
1236
|
process.stderr.write('{"warning":"using insecure HTTP transport"}\n');
|
|
1361
1237
|
}
|
|
@@ -1382,10 +1258,8 @@ module.exports = {
|
|
|
1382
1258
|
loadConfig,
|
|
1383
1259
|
executePackageUpload,
|
|
1384
1260
|
packageUploadEnvironment,
|
|
1385
|
-
parseEnvFile,
|
|
1386
1261
|
parseCommand,
|
|
1387
1262
|
platformQueryIntent,
|
|
1388
|
-
loadEnvironmentConfig,
|
|
1389
1263
|
rpcRequest,
|
|
1390
1264
|
validateSchemaValue,
|
|
1391
1265
|
validateToolArguments,
|