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.
Files changed (39) hide show
  1. package/README.md +30 -29
  2. package/SKILL.md +12 -10
  3. package/bin/rainskills-tools.js +114 -240
  4. package/bin/rainskills.js +134 -591
  5. package/install.sh +68 -94
  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 +13 -11
  9. package/package.json +2 -2
  10. package/rainbond-app-assistant/SKILL.md +112 -141
  11. package/rainbond-app-version-assistant/SKILL.md +98 -98
  12. package/rainbond-delivery-verifier/SKILL.md +98 -68
  13. package/rainbond-env-sync/SKILL.md +98 -69
  14. package/rainbond-fullstack-bootstrap/SKILL.md +97 -79
  15. package/rainbond-fullstack-bootstrap/modules/10-context-loading.md +1 -1
  16. package/rainbond-fullstack-troubleshooter/SKILL.md +98 -68
  17. package/rainbond-opensource-app-deploy/SKILL.md +98 -71
  18. package/rainbond-platform-installer/SKILL.md +2 -2
  19. package/rainbond-platform-installer/scripts/auto-update.js +0 -9
  20. package/rainbond-platform-installer/scripts/installed-version.js +1 -1
  21. package/rainbond-platform-installer/scripts/platform-installer.js +3 -89
  22. package/rainbond-platform-installer/scripts/runtime-state.js +14 -251
  23. package/rainbond-platform-installer/scripts/single-runtime.js +114 -0
  24. package/rainbond-platform-installer/scripts/user-message.js +0 -48
  25. package/rainbond-platform-installer/scripts/windows-client-config.js +0 -51
  26. package/rainbond-platform-installer/scripts/windows-onboarding.js +0 -3
  27. package/rainbond-platform-query/SKILL.md +103 -61
  28. package/rainbond-project-init/SKILL.md +98 -69
  29. package/rainbond-template-installer/SKILL.md +98 -70
  30. package/rainbond-platform-installer/scripts/environment-credentials.js +0 -225
  31. package/rainbond-platform-installer/scripts/environment-registry.js +0 -349
  32. package/rainbond-platform-installer/scripts/local-runtime-commands.js +0 -180
  33. package/rainbond-platform-installer/scripts/local-runtime.js +0 -58
  34. package/rainbond-platform-installer/scripts/runtime-context-resolver.js +0 -189
  35. package/rainbond-platform-installer/scripts/runtime-credentials.js +0 -163
  36. package/rainbond-platform-installer/scripts/runtime-intents.js +0 -386
  37. package/rainbond-platform-installer/scripts/runtime-operations.js +0 -597
  38. package/rainbond-platform-installer/scripts/windows-client-config.ps1 +0 -9
  39. 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 或更高版本并停止;不选择运行环境、不调用 MCP、不猜测替代命令。只有用户或 agent 明确同意后才安装或升级 Node.js,再从同一原始 intent 继续。
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. 执行 launcher + `["environment", "list", "--json"]`,先于项目扫描和任何 Rainbond 业务调用。未指定环境时只使用全局默认环境,默认不可用时停止,不自动回退。
81
- 2. 每个请求生成独立 operation UUID,并执行 `operation begin`;显式环境只写入这次 operation。所有 Rainbond 查询和变更都通过 `~/.rainbond/bin/rainskills-tools.js`,并绑定返回的 operation ID。项目不保存环境、团队或应用绑定,同一项目可以部署到多个目标。
82
- 3. 将动作转换为 `runtime-intents.js` 中对应的受限 intent 并完成字段校验。target 只允许 `codex`、`claude`、`pi`、`all`;按用户选择构造 launcher + `["runtime", "connect", "<target>", ...环境参数, "--intent-json", "<JSON.stringify(已校验 intent)>"]`。
83
- 4. 环境参数必须恰好选择一组且互斥:Cloud `["--saas"]`,已有私有环境用 `["--rainbond-url", "<已验证 Console origin>"]`,新建私有环境用 `["--install-private", "--location", "local"]` 或 `["--install-private", "--location", "server"]`。
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 验收通过后只进行一次浏览器授权,随后自动恢复最初的应用任务;不得回到 Agent 后再次授权。
134
+ 8. Rainbond 验收通过后只进行一次浏览器授权;授权成功后由当前 Agent 会话继续应用任务。
136
135
 
137
136
  主机密钥发生变化时直接阻断并要求人工核对,不自动替换 `known_hosts`。聊天中永远不接收密码、私钥或 Token。
138
137
 
139
- ## 多运行环境
138
+ ## 单运行环境
140
139
 
141
- - 第一个连接成功的环境自动成为全局默认环境;以后新增环境不修改默认值。
142
- - 用户未指定环境时使用默认环境;用户明确指定时只覆盖本次 operation。
143
- - 环境名称自动生成,可重命名;内部不可变 ID 不随重命名改变。
144
- - 添加、重命名、重新授权或删除环境不改项目配置,也不需要为每个 Agent 单独配置 MCP。
145
- - 新增环境直接建立独立连接,不读取或恢复旧版单环境状态;HTTPS 直接进入授权,明文 HTTP 只增加一次可信网络确认。
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
- node <已安装的 Skills 根目录>/rainbond-platform-installer/scripts/local-runtime.js environment list --json
154
- node <已安装的 Skills 根目录>/rainbond-platform-installer/scripts/local-runtime.js environment rename --environment-id <uuid> --name <name>
155
- node <已安装的 Skills 根目录>/rainbond-platform-installer/scripts/local-runtime.js environment set-default --environment-id <uuid>
156
- node <已安装的 Skills 根目录>/rainbond-platform-installer/scripts/local-runtime.js environment remove --environment-id <uuid>
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
- 这些本地状态命令不访问 npm 或网络。Agent 从当前已加载 Skill 的同级目录解析实际绝对路径;只有连接或安装运行环境时才调用固定版本的 npx launcher。
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 能力端点经 CLI 返回 401 时,依次执行以下 argv 参数:
170
+ Rainbond CLI 返回 401 时执行:
168
171
 
169
172
  ```json
170
- ["runtime", "record-failure", "--onboarding-id", "<同一 onboarding-id>", "--step", "<当前固定步骤>", "--reason", "credential-expired"]
171
- ["runtime", "reconnect", "--onboarding-id", "<同一 onboarding-id>"]
172
- ["intent", "resume", "--onboarding-id", "<同一 onboarding-id>"]
173
+ ["runtime", "reconnect", "<target>"]
173
174
  ```
174
175
 
175
- 只允许重新授权一次,并只重试记录的步骤;第二次 401 停止。403 时执行 `["runtime", "record-failure", "--onboarding-id", "<同一 onboarding-id>", "--step", "<当前固定步骤>", "--reason", "permission-denied"]`,随后立即停止,不 reconnect、不重新授权、不自动重试。
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.19`,不会执行浮动的 `@latest` 业务代码。新版本原子刷新已经安装的 Rainskills Skills;当前业务继续使用启动时已经加载的版本,最迟从下一条新任务开始使用新版。npm 超时、版本检查失败、安装位置不安全或文件迁移失败时会保留旧版本,且不会阻塞或改变当前操作。
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 list, add, rename, remove, reconnect, or change the default Rainskills runtime environment.
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(首次使用本地 CLI 前);仅在 Node.jsRainskills 安装、升级,或 PATH 变更后失效。固定 launcher 和 argv 已在本 Skill 中,禁止读取、搜索或探测 `rainskills.js`,也禁止执行 `npm root -g`。每个新的业务操作仍需要刷新一次环境列表;带已有 `operation_id` 或 `onboarding-id` 的续接复用已绑定的环境 ID,不重复枚举环境。
21
+ 执行优化:同一会话内只检查一次 Node.js 和运行环境状态;仅在 Node.jsRainskills 或唯一运行环境发生变化后失效。固定 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 Environments
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
- 环境是全局列表,不是项目绑定。本地状态命令必须定位已安装的同级 `rainbond-platform-installer/scripts/local-runtime.js`,解析为绝对路径后通过 `node` 以 argv 数组执行;不得为了查询本地环境访问 npm。只有连接或安装环境时才使用与当前技能包一致的固定 launcher `node <home>/.rainbond/lib/rainskills/bin/rainskills.js`(运行包版本 `rainskills@0.1.19`)。
47
+ Rainskills 只保存一个全局运行环境,不维护环境列表、名称、默认值或环境 ID。使用固定 launcher `node <home>/.rainbond/lib/rainskills/bin/rainskills.js`(运行包版本 `rainskills@0.1.20`):
48
48
 
49
- - 列表:用本地 launcher 执行 `environment list --json`,只展示名称、类型、状态、是否默认和最近验证时间。
50
- - 重命名:用本地 launcher 执行 `environment rename --environment-id <uuid> --name <name>`。
51
- - 设为默认:用本地 launcher 执行 `environment set-default --environment-id <uuid>`。只影响之后未指定环境的新操作。
52
- - 删除:用本地 launcher 执行 `environment remove --environment-id <uuid>`;默认环境或活动操作使用中的环境必须先阻断。
53
- - 添加:新增环境时直接生成 operation UUID,以 `{"type":"environment-add"}` 执行 runtime connect。先原样展示 `runtime message --id add-environment-location`。选择云端环境时直接执行 `--saas` route;选择私有环境时立即原样展示 `runtime message --id private-deployment-location`,并与首次部署保持完全相同的路径:选择 1(部署到本机)执行 `--install-private --location local`,选择 2(部署到独立服务器)执行 `--install-private --location server`,选择 3(部署到已有 Rainbond)原样展示 `runtime message --id private-console-origin`,收到地址后执行 `--rainbond-url <已验证 Console origin>`。不得展示任何额外的接入方式中间步骤。授权成功后必须把 launcher 返回的 `user_message` 中完整环境列表原样展示给用户,再按用户要求重命名并执行 `operation complete --operation-id <uuid>`。第二个环境不得自动改成默认环境。不得先执行 `runtime status`,不得检查、迁移、备份、删除或修复旧的 `runtime-connection-v1.json`;旧 runtime 状态不是新增环境的前置条件,连接失败时只执行 launcher 返回的固定恢复 argv,不得自行设计恢复流程。
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
- 用户在一次业务请求中明确指定运行环境时,只把该环境 ID 传给本次 `operation begin`;同一项目可以部署到任意多个环境和团队。明确“团队”表示环境内团队;明确“运行环境/平台”表示环境;裸名称同时匹配两者时必须询问,不能靠名称中是否含“环境”二字猜测。
55
+ 所有 Rainbond 查询和变更继续通过 `~/.rainbond/bin/rainskills-tools.js`。不得配置客户端 MCP,也不得恢复环境枚举、业务 operation 生命周期、环境 ID 或 intent 恢复协议。
56
+
57
+ 业务 CLI 默认只读取受保护的唯一运行环境文件,忽略 Shell 中遗留的 `RAINBOND_URL` / `RAINBOND_JWT`。CI 若要显式使用环境变量凭据,必须同时设置 `RAINSKILLS_CREDENTIAL_SOURCE=environment`。
@@ -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
- if (operationId) return loadOperationConfig({ homeDir, operationId, includeRuntime });
220
- const configDirectory = path.join(homeDir, CONFIG_DIRECTORY);
221
- const credentialsPath = path.join(configDirectory, CREDENTIALS_FILENAME);
222
- const legacyCredentialsPath = path.join(configDirectory, LEGACY_CREDENTIALS_FILENAME);
223
- const fileValues = readPrivateEnvFile(credentialsPath);
224
- const legacyValues = Object.keys(fileValues).length === 0
225
- ? readPrivateEnvFile(legacyCredentialsPath)
226
- : {};
227
-
228
- const baseUrl = env.RAINBOND_URL || fileValues.RAINBOND_URL || legacyValues.RAINBOND_URL;
229
- const jwt = env.RAINBOND_JWT || fileValues.RAINBOND_JWT || legacyValues.RAINBOND_JWT;
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("RAINBOND_URL and RAINBOND_JWT are required", EXIT.CONFIG);
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 = (env.RAINBOND_ALLOW_INSECURE_HTTP ||
245
- fileValues.RAINBOND_ALLOW_INSECURE_HTTP ||
246
- legacyValues.RAINBOND_ALLOW_INSECURE_HTTP) === "true";
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[0] === "query") {
303
- const toolName = args[1];
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 operationIndex = args.indexOf("--operation-id");
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(operationArgs[skillIndex + 1] || "")
343
- || operationArgs.indexOf("--skill-id", skillIndex + 1) !== -1
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 = operationArgs[skillIndex + 1];
178
+ const skillId = args[skillIndex + 1];
351
179
  const remaining = [
352
- ...operationArgs.slice(0, skillIndex),
353
- ...operationArgs.slice(skillIndex + 2),
180
+ ...args.slice(0, skillIndex),
181
+ ...args.slice(skillIndex + 2),
354
182
  ];
355
183
  const command = remaining[0];
356
- const context = { operationId, skillId };
184
+ const context = { skillId };
357
185
  if (
358
186
  command === "context"
359
- && ["resolve", "select"].includes(remaining[1])
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
- try {
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
- async function execute(command, config) {
1186
- if (command.command === "context") {
1187
- const { createRuntimeContextResolver } = requireRuntimeModule("runtime-context-resolver.js");
1188
- const resolver = createRuntimeContextResolver({
1189
- operationStore: config.operationStore,
1190
- queryTool: async (toolName, argumentsValue) => execute({
1191
- command: "read",
1192
- toolName,
1193
- argumentsValue,
1194
- }, config),
1195
- });
1196
- if (command.action === "resolve") {
1197
- return resolver.resolve({
1198
- operationId: config.operationId,
1199
- required: command.argumentsValue.required,
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
- return resolver.select({
1203
- operationId: config.operationId,
1204
- selectionId: command.argumentsValue.selection_id,
1205
- optionId: command.argumentsValue.option_id,
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 = loadEnvironmentConfig({ environmentId: command.environmentId });
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,