rainskills 0.1.4 → 0.1.6

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 CHANGED
@@ -72,7 +72,7 @@ Rainskills 安装完成,下一条消息即可直接使用。
72
72
  固定 launcher 来自当前 `package.json` 版本:
73
73
 
74
74
  ```json
75
- ["npx", "--yes", "rainskills@0.1.4"]
75
+ ["npx", "--yes", "rainskills@0.1.6"]
76
76
  ```
77
77
 
78
78
  所有调用都把 launcher 与参数合并成 argv 数组后直接执行;不得使用 `rainskills@latest`,不得把参数拼成 shell 字符串。
@@ -192,21 +192,21 @@ Windows 本地安装是预览能力,也可以改选 Linux 服务器。它要
192
192
 
193
193
  ```bash
194
194
  # 本地单机
195
- npx --yes rainskills@0.1.4 platform install --onboarding-id <id> \
195
+ npx --yes rainskills@0.1.6 platform install --onboarding-id <id> \
196
196
  --location local --mode single-node
197
197
 
198
198
  # Linux 服务器单机
199
- npx --yes rainskills@0.1.4 platform install --onboarding-id <id> \
199
+ npx --yes rainskills@0.1.6 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.4 platform install --onboarding-id <id> \
203
+ npx --yes rainskills@0.1.6 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.4 platform install --onboarding-id <id> \
209
+ npx --yes rainskills@0.1.6 platform install --onboarding-id <id> \
210
210
  --location server --mode existing-kubernetes \
211
211
  --kubeconfig <path> --kube-context <name> --chart-version <version>
212
212
  ```
package/SKILL.md CHANGED
@@ -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.4`。
41
+ 环境是全局列表,不是项目绑定。本地状态命令必须定位已安装的同级 `rainbond-platform-installer/scripts/local-runtime.js`,解析为绝对路径后通过 `node` 以 argv 数组执行;不得为了查询本地环境访问 npm。只有连接或安装环境时才使用与当前技能包一致的固定 launcher `npx --yes rainskills@0.1.6`。
42
42
 
43
43
  - 列表:用本地 launcher 执行 `environment list --json`,只展示名称、类型、状态、是否默认和最近验证时间。
44
44
  - 重命名:用本地 launcher 执行 `environment rename --environment-id <uuid> --name <name>`。
package/bin/rainskills.js CHANGED
@@ -1046,6 +1046,7 @@ async function runAutoUpdatePhase(args, {
1046
1046
  platform = process.platform,
1047
1047
  packageRoot = path.resolve(__dirname, ".."),
1048
1048
  checkForUpdate,
1049
+ acquireArtifact,
1049
1050
  synchronizeSkills,
1050
1051
  updateState,
1051
1052
  delegate,
@@ -1087,6 +1088,7 @@ async function runAutoUpdatePhase(args, {
1087
1088
  }
1088
1089
  }
1089
1090
  let lease = null;
1091
+ let artifact = null;
1090
1092
  try {
1091
1093
  if (autoUpdate.isStableVersion(currentVersion) && autoUpdate.isSafeAutoUpdateEntry(args)) {
1092
1094
  lease = getState().acquireLease?.() || null;
@@ -1101,15 +1103,27 @@ async function runAutoUpdatePhase(args, {
1101
1103
  env,
1102
1104
  home,
1103
1105
  platform,
1106
+ ...(activeOperationDetector ? { activeOperationDetector } : {}),
1104
1107
  ...(state ? { updateState: state } : {}),
1105
1108
  });
1106
1109
  if (decision.action !== "delegate") {
1107
1110
  return { handled: false, reason: decision.reason };
1108
1111
  }
1109
- const invocation = autoUpdate.buildStableUpdateInvocation(decision.version, args, { platform });
1112
+ artifact = await (acquireArtifact || autoUpdate.acquireStableUpdateArtifact)(decision, {
1113
+ home,
1114
+ platform,
1115
+ });
1116
+ const detectActive = activeOperationDetector
1117
+ || (() => autoUpdate.hasActiveOperation({ home, platform }));
1118
+ if (detectActive()) return { handled: false, reason: "active-operation" };
1119
+ const invocation = autoUpdate.buildStableUpdateInvocation(decision, args, {
1120
+ platform,
1121
+ artifactPath: artifact.path,
1122
+ });
1110
1123
  const environment = autoUpdate.buildStableUpdateEnvironment(env, {
1111
1124
  fromVersion: currentVersion,
1112
1125
  targetVersion: decision.version,
1126
+ registry: decision.registry,
1113
1127
  });
1114
1128
  let result;
1115
1129
  try {
@@ -1132,6 +1146,7 @@ async function runAutoUpdatePhase(args, {
1132
1146
  signal: result.signal || null,
1133
1147
  };
1134
1148
  } finally {
1149
+ try { artifact?.cleanup(); } catch { /* protected cleanup is best effort */ }
1135
1150
  lease?.release();
1136
1151
  }
1137
1152
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rainskills",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Install the complete Rainskills AI deployment skill suite as one product.",
5
5
  "author": {
6
6
  "name": "Goodrain",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rainskills",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Install the complete Rainskills AI deployment skill suite as one product.",
5
5
  "author": {
6
6
  "name": "Goodrain",
@@ -14,7 +14,7 @@ This is the single marketplace entry for the complete Rainskills product. The in
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.
16
16
 
17
- If the adjacent installer is missing, check the local Node.js version before choosing the fallback. With `npx` and Node.js 18 or newer, use `npx --yes rainskills@0.1.4 <target>`. With no Node.js or a version below 18, use `bash <(curl -fsSL https://get.rainbond.com/rainskills/install.sh) <target>` instead. Omit `<target>` only when the host cannot be determined reliably. Keep either command attached to the interactive terminal. For an update or repair, refresh this marketplace Skill first, then run the installer again; it compares and updates every independent internal Skill.
17
+ If the adjacent installer is missing, check the local Node.js version before choosing the fallback. With `npx` and Node.js 18 or newer, use `npx --yes rainskills@0.1.6 <target>`. With no Node.js or a version below 18, use `bash <(curl -fsSL https://get.rainbond.com/rainskills/install.sh) <target>` instead. Omit `<target>` only when the host cannot be determined reliably. Keep either command attached to the interactive terminal. For an update or repair, refresh this marketplace Skill first, then run the installer again; it compares and updates every independent internal Skill.
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
 
@@ -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.4`。
41
+ 环境是全局列表,不是项目绑定。本地状态命令必须定位已安装的同级 `rainbond-platform-installer/scripts/local-runtime.js`,解析为绝对路径后通过 `node` 以 argv 数组执行;不得为了查询本地环境访问 npm。只有连接或安装环境时才使用与当前技能包一致的固定 launcher `npx --yes rainskills@0.1.6`。
42
42
 
43
43
  - 列表:用本地 launcher 执行 `environment list --json`,只展示名称、类型、状态、是否默认和最近验证时间。
44
44
  - 重命名:用本地 launcher 执行 `environment rename --environment-id <uuid> --name <name>`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rainskills",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Interactive Rainbond skill installer for Codex and Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/goodrain/rainskills#readme",
@@ -15500,7 +15500,7 @@ function serializeMessage(message) {
15500
15500
  // package.json
15501
15501
  var package_default = {
15502
15502
  name: "rainskills",
15503
- version: "0.1.4",
15503
+ version: "0.1.6",
15504
15504
  description: "Interactive Rainbond skill installer for Codex and Claude Code",
15505
15505
  license: "Apache-2.0",
15506
15506
  homepage: "https://github.com/goodrain/rainskills#readme",
@@ -98,7 +98,7 @@ description: "Use whenever a user asks to deploy, run, deliver, publish, inspect
98
98
 
99
99
  第一步检查 Node.js 是否存在且主版本不低于 18。Node.js 缺失或低于 18 时,只说明“Rainskills 执行组件需要 Node.js 18 或更高版本”并停止:不选择运行环境,不调用 MCP,不猜测替代命令。只有用户或 agent 明确同意后才安装或升级 Node.js,再从同一原始 intent 继续。
100
100
 
101
- 固定 launcher 是 `["npx", "--yes", "rainskills@0.1.4"]`;版本必须与本技能包 `package.json` 一致。把 launcher 与参数拼成 argv 数组直接执行,禁止使用 `rainskills@latest`,禁止拼接或执行 shell 字符串。
101
+ 固定 launcher 是 `["npx", "--yes", "rainskills@0.1.6"]`;版本必须与本技能包 `package.json` 一致。把 launcher 与参数拼成 argv 数组直接执行,禁止使用 `rainskills@latest`,禁止拼接或执行 shell 字符串。
102
102
 
103
103
  本地 launcher 必须从当前 Skill 所在目录的同级目录定位 `rainbond-platform-installer/scripts/local-runtime.js`,解析为绝对路径后使用 `["node", "<绝对路径>"]` 执行。`environment list`、`operation begin`、`operation complete` 和 `runtime message` 只能使用本地 launcher;本地 launcher 只读取已安装文件和本机受保护状态,不得访问 npm 或其它网络。只有用户选定连接或安装运行环境后,才使用上面的固定 npx launcher。
104
104
 
@@ -106,7 +106,7 @@ description: "Use whenever a user asks to deploy, run, deliver, publish, inspect
106
106
  ```json
107
107
  {
108
108
  "schema": "rainskills.skill-runtime-contract.v1",
109
- "launcher": ["npx", "--yes", "rainskills@0.1.4"],
109
+ "launcher": ["npx", "--yes", "rainskills@0.1.6"],
110
110
  "local_launcher": ["node", "<installed-skills-root>/rainbond-platform-installer/scripts/local-runtime.js"],
111
111
  "local_argv": {
112
112
  "environment-list": ["node", "<installed-skills-root>/rainbond-platform-installer/scripts/local-runtime.js", "environment", "list", "--json"],
@@ -126,9 +126,9 @@ description: "Use whenever a user asks to deploy, run, deliver, publish, inspect
126
126
  "existing": ["saas", "private-existing"]
127
127
  },
128
128
  "connect_argv": {
129
- "saas": ["npx", "--yes", "rainskills@0.1.4", "runtime", "connect", "<target>", "--saas", "--intent-json", "<intent-json>"],
130
- "private-existing": ["npx", "--yes", "rainskills@0.1.4", "runtime", "connect", "<target>", "--rainbond-url", "<rainbond-url>", "--intent-json", "<intent-json>"],
131
- "install-private": ["npx", "--yes", "rainskills@0.1.4", "runtime", "connect", "<target>", "--install-private", "--location", "<private-location>", "--intent-json", "<intent-json>"]
129
+ "saas": ["npx", "--yes", "rainskills@0.1.6", "runtime", "connect", "<target>", "--saas", "--intent-json", "<intent-json>"],
130
+ "private-existing": ["npx", "--yes", "rainskills@0.1.6", "runtime", "connect", "<target>", "--rainbond-url", "<rainbond-url>", "--intent-json", "<intent-json>"],
131
+ "install-private": ["npx", "--yes", "rainskills@0.1.6", "runtime", "connect", "<target>", "--install-private", "--location", "<private-location>", "--intent-json", "<intent-json>"]
132
132
  }
133
133
  }
134
134
  ```
@@ -14,7 +14,7 @@ description: "Use when a user explicitly asks for an existing Rainbond app versi
14
14
 
15
15
  第一步检查 Node.js 是否存在且主版本不低于 18。Node.js 缺失或低于 18 时,只说明“Rainskills 执行组件需要 Node.js 18 或更高版本”并停止:不选择运行环境,不调用 MCP,不猜测替代命令。只有用户或 agent 明确同意后才安装或升级 Node.js,再从同一原始 intent 继续。
16
16
 
17
- 固定 launcher 是 `["npx", "--yes", "rainskills@0.1.4"]`;版本必须与本技能包 `package.json` 一致。把 launcher 与参数拼成 argv 数组直接执行,禁止 `rainskills@latest` 或执行 shell 字符串。
17
+ 固定 launcher 是 `["npx", "--yes", "rainskills@0.1.6"]`;版本必须与本技能包 `package.json` 一致。把 launcher 与参数拼成 argv 数组直接执行,禁止 `rainskills@latest` 或执行 shell 字符串。
18
18
 
19
19
  本地 launcher 必须从当前 Skill 所在目录的同级目录定位 `rainbond-platform-installer/scripts/local-runtime.js`,解析为绝对路径后使用 `["node", "<绝对路径>"]` 执行。`environment list`、`operation begin`、`operation complete` 和 `runtime message` 只能使用本地 launcher;本地 launcher 只读取已安装文件和本机受保护状态,不得访问 npm 或其它网络。只有用户选定连接运行环境后,才使用上面的固定 npx launcher。
20
20
 
@@ -22,7 +22,7 @@ description: "Use when a user explicitly asks for an existing Rainbond app versi
22
22
  ```json
23
23
  {
24
24
  "schema": "rainskills.skill-runtime-contract.v1",
25
- "launcher": ["npx", "--yes", "rainskills@0.1.4"],
25
+ "launcher": ["npx", "--yes", "rainskills@0.1.6"],
26
26
  "local_launcher": ["node", "<installed-skills-root>/rainbond-platform-installer/scripts/local-runtime.js"],
27
27
  "local_argv": {
28
28
  "environment-list": ["node", "<installed-skills-root>/rainbond-platform-installer/scripts/local-runtime.js", "environment", "list", "--json"],
@@ -37,8 +37,8 @@ description: "Use when a user explicitly asks for an existing Rainbond app versi
37
37
  },
38
38
  "routes": {"existing": ["saas", "private-existing"]},
39
39
  "connect_argv": {
40
- "saas": ["npx", "--yes", "rainskills@0.1.4", "runtime", "connect", "<target>", "--saas", "--intent-json", "<intent-json>"],
41
- "private-existing": ["npx", "--yes", "rainskills@0.1.4", "runtime", "connect", "<target>", "--rainbond-url", "<rainbond-url>", "--intent-json", "<intent-json>"]
40
+ "saas": ["npx", "--yes", "rainskills@0.1.6", "runtime", "connect", "<target>", "--saas", "--intent-json", "<intent-json>"],
41
+ "private-existing": ["npx", "--yes", "rainskills@0.1.6", "runtime", "connect", "<target>", "--rainbond-url", "<rainbond-url>", "--intent-json", "<intent-json>"]
42
42
  }
43
43
  }
44
44
  ```
@@ -46,7 +46,7 @@ description: "Use only when the user explicitly asks for final delivery or acces
46
46
 
47
47
  第一步检查 Node.js 是否存在且主版本不低于 18。Node.js 缺失或低于 18 时,只说明“Rainskills 执行组件需要 Node.js 18 或更高版本”并停止:不选择运行环境,不调用 MCP,不猜测替代命令。只有用户或 agent 明确同意后才安装或升级 Node.js,再从同一原始 intent 继续。
48
48
 
49
- 固定 launcher 是 `["npx", "--yes", "rainskills@0.1.4"]`;版本必须与本技能包 `package.json` 一致。把 launcher 与参数拼成 argv 数组直接执行,禁止 `rainskills@latest` 或执行 shell 字符串。
49
+ 固定 launcher 是 `["npx", "--yes", "rainskills@0.1.6"]`;版本必须与本技能包 `package.json` 一致。把 launcher 与参数拼成 argv 数组直接执行,禁止 `rainskills@latest` 或执行 shell 字符串。
50
50
 
51
51
  本地 launcher 必须从当前 Skill 所在目录的同级目录定位 `rainbond-platform-installer/scripts/local-runtime.js`,解析为绝对路径后使用 `["node", "<绝对路径>"]` 执行。`environment list`、`operation begin`、`operation complete` 和 `runtime message` 只能使用本地 launcher;本地 launcher 只读取已安装文件和本机受保护状态,不得访问 npm 或其它网络。只有用户选定连接运行环境后,才使用上面的固定 npx launcher。
52
52
 
@@ -54,7 +54,7 @@ description: "Use only when the user explicitly asks for final delivery or acces
54
54
  ```json
55
55
  {
56
56
  "schema": "rainskills.skill-runtime-contract.v1",
57
- "launcher": ["npx", "--yes", "rainskills@0.1.4"],
57
+ "launcher": ["npx", "--yes", "rainskills@0.1.6"],
58
58
  "local_launcher": ["node", "<installed-skills-root>/rainbond-platform-installer/scripts/local-runtime.js"],
59
59
  "local_argv": {
60
60
  "environment-list": ["node", "<installed-skills-root>/rainbond-platform-installer/scripts/local-runtime.js", "environment", "list", "--json"],
@@ -67,8 +67,8 @@ description: "Use only when the user explicitly asks for final delivery or acces
67
67
  },
68
68
  "routes": {"existing": ["saas", "private-existing"]},
69
69
  "connect_argv": {
70
- "saas": ["npx", "--yes", "rainskills@0.1.4", "runtime", "connect", "<target>", "--saas", "--intent-json", "<intent-json>"],
71
- "private-existing": ["npx", "--yes", "rainskills@0.1.4", "runtime", "connect", "<target>", "--rainbond-url", "<rainbond-url>", "--intent-json", "<intent-json>"]
70
+ "saas": ["npx", "--yes", "rainskills@0.1.6", "runtime", "connect", "<target>", "--saas", "--intent-json", "<intent-json>"],
71
+ "private-existing": ["npx", "--yes", "rainskills@0.1.6", "runtime", "connect", "<target>", "--rainbond-url", "<rainbond-url>", "--intent-json", "<intent-json>"]
72
72
  }
73
73
  }
74
74
  ```
@@ -14,7 +14,7 @@ description: "Use when a user explicitly asks to sync non-sensitive preview or p
14
14
 
15
15
  第一步检查 Node.js 是否存在且主版本不低于 18。Node.js 缺失或低于 18 时,只说明“Rainskills 执行组件需要 Node.js 18 或更高版本”并停止:不选择运行环境,不调用 MCP,不猜测替代命令。只有用户或 agent 明确同意后才安装或升级 Node.js,再从同一原始 intent 继续。
16
16
 
17
- 固定 launcher 是 `["npx", "--yes", "rainskills@0.1.4"]`;版本必须与本技能包 `package.json` 一致。把 launcher 与参数拼成 argv 数组直接执行,禁止 `rainskills@latest` 或执行 shell 字符串。
17
+ 固定 launcher 是 `["npx", "--yes", "rainskills@0.1.6"]`;版本必须与本技能包 `package.json` 一致。把 launcher 与参数拼成 argv 数组直接执行,禁止 `rainskills@latest` 或执行 shell 字符串。
18
18
 
19
19
  本地 launcher 必须从当前 Skill 所在目录的同级目录定位 `rainbond-platform-installer/scripts/local-runtime.js`,解析为绝对路径后使用 `["node", "<绝对路径>"]` 执行。`environment list`、`operation begin`、`operation complete` 和 `runtime message` 只能使用本地 launcher;本地 launcher 只读取已安装文件和本机受保护状态,不得访问 npm 或其它网络。只有用户选定连接运行环境后,才使用上面的固定 npx launcher。
20
20
 
@@ -22,7 +22,7 @@ description: "Use when a user explicitly asks to sync non-sensitive preview or p
22
22
  ```json
23
23
  {
24
24
  "schema": "rainskills.skill-runtime-contract.v1",
25
- "launcher": ["npx", "--yes", "rainskills@0.1.4"],
25
+ "launcher": ["npx", "--yes", "rainskills@0.1.6"],
26
26
  "local_launcher": ["node", "<installed-skills-root>/rainbond-platform-installer/scripts/local-runtime.js"],
27
27
  "local_argv": {
28
28
  "environment-list": ["node", "<installed-skills-root>/rainbond-platform-installer/scripts/local-runtime.js", "environment", "list", "--json"],
@@ -35,8 +35,8 @@ description: "Use when a user explicitly asks to sync non-sensitive preview or p
35
35
  },
36
36
  "routes": {"existing": ["saas", "private-existing"]},
37
37
  "connect_argv": {
38
- "saas": ["npx", "--yes", "rainskills@0.1.4", "runtime", "connect", "<target>", "--saas", "--intent-json", "<intent-json>"],
39
- "private-existing": ["npx", "--yes", "rainskills@0.1.4", "runtime", "connect", "<target>", "--rainbond-url", "<rainbond-url>", "--intent-json", "<intent-json>"]
38
+ "saas": ["npx", "--yes", "rainskills@0.1.6", "runtime", "connect", "<target>", "--saas", "--intent-json", "<intent-json>"],
39
+ "private-existing": ["npx", "--yes", "rainskills@0.1.6", "runtime", "connect", "<target>", "--rainbond-url", "<rainbond-url>", "--intent-json", "<intent-json>"]
40
40
  }
41
41
  }
42
42
  ```
@@ -14,7 +14,7 @@ description: "Use only when the user explicitly asks to create the Rainbond app
14
14
 
15
15
  第一步检查 Node.js 是否存在且主版本不低于 18。Node.js 缺失或低于 18 时,只说明“Rainskills 执行组件需要 Node.js 18 或更高版本”并停止:不选择运行环境,不调用 MCP,不猜测替代命令。只有用户或 agent 明确同意后才安装或升级 Node.js,再从同一原始 intent 继续。
16
16
 
17
- 固定 launcher 是 `["npx", "--yes", "rainskills@0.1.4"]`;版本必须与本技能包 `package.json` 一致。把 launcher 与参数拼成 argv 数组直接执行,禁止 `rainskills@latest` 或执行 shell 字符串。
17
+ 固定 launcher 是 `["npx", "--yes", "rainskills@0.1.6"]`;版本必须与本技能包 `package.json` 一致。把 launcher 与参数拼成 argv 数组直接执行,禁止 `rainskills@latest` 或执行 shell 字符串。
18
18
 
19
19
  本地 launcher 必须从当前 Skill 所在目录的同级目录定位 `rainbond-platform-installer/scripts/local-runtime.js`,解析为绝对路径后使用 `["node", "<绝对路径>"]` 执行。`environment list`、`operation begin`、`operation complete` 和 `runtime message` 只能使用本地 launcher;本地 launcher 只读取已安装文件和本机受保护状态,不得访问 npm 或其它网络。只有用户选定连接或安装运行环境后,才使用上面的固定 npx launcher。
20
20
 
@@ -22,7 +22,7 @@ description: "Use only when the user explicitly asks to create the Rainbond app
22
22
  ```json
23
23
  {
24
24
  "schema": "rainskills.skill-runtime-contract.v1",
25
- "launcher": ["npx", "--yes", "rainskills@0.1.4"],
25
+ "launcher": ["npx", "--yes", "rainskills@0.1.6"],
26
26
  "local_launcher": ["node", "<installed-skills-root>/rainbond-platform-installer/scripts/local-runtime.js"],
27
27
  "local_argv": {
28
28
  "environment-list": ["node", "<installed-skills-root>/rainbond-platform-installer/scripts/local-runtime.js", "environment", "list", "--json"],
@@ -42,9 +42,9 @@ description: "Use only when the user explicitly asks to create the Rainbond app
42
42
  "existing": ["saas", "private-existing"]
43
43
  },
44
44
  "connect_argv": {
45
- "saas": ["npx", "--yes", "rainskills@0.1.4", "runtime", "connect", "<target>", "--saas", "--intent-json", "<intent-json>"],
46
- "private-existing": ["npx", "--yes", "rainskills@0.1.4", "runtime", "connect", "<target>", "--rainbond-url", "<rainbond-url>", "--intent-json", "<intent-json>"],
47
- "install-private": ["npx", "--yes", "rainskills@0.1.4", "runtime", "connect", "<target>", "--install-private", "--location", "<private-location>", "--intent-json", "<intent-json>"]
45
+ "saas": ["npx", "--yes", "rainskills@0.1.6", "runtime", "connect", "<target>", "--saas", "--intent-json", "<intent-json>"],
46
+ "private-existing": ["npx", "--yes", "rainskills@0.1.6", "runtime", "connect", "<target>", "--rainbond-url", "<rainbond-url>", "--intent-json", "<intent-json>"],
47
+ "install-private": ["npx", "--yes", "rainskills@0.1.6", "runtime", "connect", "<target>", "--install-private", "--location", "<private-location>", "--intent-json", "<intent-json>"]
48
48
  }
49
49
  }
50
50
  ```
@@ -46,7 +46,7 @@ description: "Use only when the user explicitly asks for a bounded build, runtim
46
46
 
47
47
  第一步检查 Node.js 是否存在且主版本不低于 18。Node.js 缺失或低于 18 时,只说明“Rainskills 执行组件需要 Node.js 18 或更高版本”并停止:不选择运行环境,不调用 MCP,不猜测替代命令。只有用户或 agent 明确同意后才安装或升级 Node.js,再从同一原始 intent 继续。
48
48
 
49
- 固定 launcher 是 `["npx", "--yes", "rainskills@0.1.4"]`;版本必须与本技能包 `package.json` 一致。把 launcher 与参数拼成 argv 数组直接执行,禁止 `rainskills@latest` 或执行 shell 字符串。
49
+ 固定 launcher 是 `["npx", "--yes", "rainskills@0.1.6"]`;版本必须与本技能包 `package.json` 一致。把 launcher 与参数拼成 argv 数组直接执行,禁止 `rainskills@latest` 或执行 shell 字符串。
50
50
 
51
51
  本地 launcher 必须从当前 Skill 所在目录的同级目录定位 `rainbond-platform-installer/scripts/local-runtime.js`,解析为绝对路径后使用 `["node", "<绝对路径>"]` 执行。`environment list`、`operation begin`、`operation complete` 和 `runtime message` 只能使用本地 launcher;本地 launcher 只读取已安装文件和本机受保护状态,不得访问 npm 或其它网络。只有用户选定连接运行环境后,才使用上面的固定 npx launcher。
52
52
 
@@ -54,7 +54,7 @@ description: "Use only when the user explicitly asks for a bounded build, runtim
54
54
  ```json
55
55
  {
56
56
  "schema": "rainskills.skill-runtime-contract.v1",
57
- "launcher": ["npx", "--yes", "rainskills@0.1.4"],
57
+ "launcher": ["npx", "--yes", "rainskills@0.1.6"],
58
58
  "local_launcher": ["node", "<installed-skills-root>/rainbond-platform-installer/scripts/local-runtime.js"],
59
59
  "local_argv": {
60
60
  "environment-list": ["node", "<installed-skills-root>/rainbond-platform-installer/scripts/local-runtime.js", "environment", "list", "--json"],
@@ -67,8 +67,8 @@ description: "Use only when the user explicitly asks for a bounded build, runtim
67
67
  },
68
68
  "routes": {"existing": ["saas", "private-existing"]},
69
69
  "connect_argv": {
70
- "saas": ["npx", "--yes", "rainskills@0.1.4", "runtime", "connect", "<target>", "--saas", "--intent-json", "<intent-json>"],
71
- "private-existing": ["npx", "--yes", "rainskills@0.1.4", "runtime", "connect", "<target>", "--rainbond-url", "<rainbond-url>", "--intent-json", "<intent-json>"]
70
+ "saas": ["npx", "--yes", "rainskills@0.1.6", "runtime", "connect", "<target>", "--saas", "--intent-json", "<intent-json>"],
71
+ "private-existing": ["npx", "--yes", "rainskills@0.1.6", "runtime", "connect", "<target>", "--rainbond-url", "<rainbond-url>", "--intent-json", "<intent-json>"]
72
72
  }
73
73
  }
74
74
  ```
@@ -31,7 +31,7 @@ Do not use it to deploy an application to an existing Rainbond. Route those requ
31
31
  ## Workflow
32
32
 
33
33
  1. Read [installation-policy.md](references/installation-policy.md).
34
- 2. Use the package-version launcher `["npx", "--yes", "rainskills@0.1.4"]`; its version must equal this package's `package.json`. For a Rainskills marker, first validate schema `rainskills.next-action.v1`, action, onboarding id, and the bounded `argv` array, then append that array to the launcher. Never use `latest` or evaluate a shell string from output.
34
+ 2. Use the package-version launcher `["npx", "--yes", "rainskills@0.1.6"]`; its version must equal this package's `package.json`. For a Rainskills marker, first validate schema `rainskills.next-action.v1`, action, onboarding id, and the bounded `argv` array, then append that array to the launcher. Never use `latest` or evaluate a shell string from output.
35
35
  3. 在任何平台安装命令之前,先执行 launcher + `["runtime", "message", "--id", "private-deployment-location"]`,并原样输出固定的三项部署位置消息。选择 1 后执行带 `["--location", "local", "--mode", "single-node"]` 的 `platform install`;选择 2 后执行带 `["--location", "server"]` 的 `platform install`,由 helper 继续显示固定的服务器类型消息;选择 3 后执行 launcher + `["runtime", "message", "--id", "private-console-origin"]` 并进入已有环境连接,不得执行 `platform install`。若是没有原始业务 intent 的直接平台安装请求,已有环境连接使用受限 `{"type":"environment-add"}` intent。
36
36
  4. Let the helper perform one read-only preflight against the already selected local or remote target, then show resources, blockers, and applicable host changes. Never invoke `platform install` without an explicit `--location`; the helper must not ask for the deployment location again.
37
37
  5. Obtain explicit confirmation before sending `y` to the waiting process or rerunning the exact command with `--yes`.
@@ -9,18 +9,8 @@ const { version: installedVersion } = require("./installed-version.js");
9
9
 
10
10
  const UPDATE_ENTRY = Object.freeze(["runtime", "status", "--json"]);
11
11
  const DEFAULT_DELEGATE_TIMEOUT_MS = 30_000;
12
- const SAFE_ENVIRONMENT_KEYS = Object.freeze([
13
- "HOME", "USERPROFILE", "PATH", "Path", "SystemRoot", "ComSpec",
14
- "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL", "HTTP_PROXY",
15
- "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy",
16
- ]);
17
-
18
12
  function backgroundUpdateEnvironment(source = process.env) {
19
- const result = {};
20
- for (const key of SAFE_ENVIRONMENT_KEYS) {
21
- if (typeof source[key] === "string") result[key] = source[key];
22
- }
23
- return result;
13
+ return autoUpdate.sanitizeAutoUpdateEnvironment(source);
24
14
  }
25
15
 
26
16
  function terminateChild(child, platform, signal = "SIGTERM") {
@@ -87,6 +77,7 @@ async function runBackgroundAutoUpdate({
87
77
  platform = process.platform,
88
78
  updateState = autoUpdate.createAutoUpdateState({ home, platform }),
89
79
  checkForUpdate = autoUpdate.checkForStableUpdate,
80
+ acquireArtifact = autoUpdate.acquireStableUpdateArtifact,
90
81
  activeOperationDetector = () => autoUpdate.hasActiveOperation({ home, platform }),
91
82
  runDelegated = (invocation, environment) => runBoundedChild(
92
83
  invocation,
@@ -95,6 +86,7 @@ async function runBackgroundAutoUpdate({
95
86
  ),
96
87
  } = {}) {
97
88
  let lease;
89
+ let artifact;
98
90
  try {
99
91
  lease = updateState.acquireLease?.() || null;
100
92
  } catch {
@@ -111,17 +103,17 @@ async function runBackgroundAutoUpdate({
111
103
  updateState,
112
104
  });
113
105
  if (decision.action !== "delegate") return decision;
114
- if (activeOperationDetector()) {
115
- return { action: "continue", reason: "active-operation" };
116
- }
106
+ artifact = await acquireArtifact(decision, { home, platform });
107
+ if (activeOperationDetector()) return { action: "continue", reason: "active-operation" };
117
108
  const invocation = autoUpdate.buildStableUpdateInvocation(
118
- decision.version,
109
+ decision,
119
110
  UPDATE_ENTRY,
120
- { platform }
111
+ { platform, artifactPath: artifact.path }
121
112
  );
122
113
  const environment = autoUpdate.buildStableUpdateEnvironment(env, {
123
114
  fromVersion: currentVersion,
124
115
  targetVersion: decision.version,
116
+ registry: decision.registry,
125
117
  });
126
118
  const result = await runDelegated(invocation, environment);
127
119
  if (result.code !== 0 || result.signal) {
@@ -133,6 +125,7 @@ async function runBackgroundAutoUpdate({
133
125
  try { updateState.recordFailure?.(); } catch { /* best effort */ }
134
126
  return { action: "continue", reason: "update-failed" };
135
127
  } finally {
128
+ try { artifact?.cleanup(); } catch { /* protected cleanup is best effort */ }
136
129
  lease?.release();
137
130
  }
138
131
  }
@@ -5,16 +5,41 @@ const fs = require("node:fs");
5
5
  const https = require("node:https");
6
6
  const os = require("node:os");
7
7
  const path = require("node:path");
8
+ const tls = require("node:tls");
8
9
 
9
10
  const { createSecureStateStore } = require("./secure-state.js");
10
11
 
11
12
  const OFFICIAL_LATEST_URL = "https://registry.npmjs.org/rainskills/latest";
12
13
  const OFFICIAL_REGISTRY = "https://registry.npmjs.org/";
14
+ const MIRROR_REGISTRY = "https://registry.npmmirror.com/";
15
+ const REGISTRY_SOURCES = Object.freeze({
16
+ official: Object.freeze({
17
+ name: "official",
18
+ registry: OFFICIAL_REGISTRY,
19
+ latestUrl: OFFICIAL_LATEST_URL,
20
+ tarballOrigin: OFFICIAL_REGISTRY,
21
+ }),
22
+ mirror: Object.freeze({
23
+ name: "mirror",
24
+ registry: MIRROR_REGISTRY,
25
+ latestUrl: `${MIRROR_REGISTRY}rainskills/latest`,
26
+ tarballOrigin: "https://cdn.npmmirror.com/",
27
+ }),
28
+ });
13
29
  const UPDATE_STATE_SCHEMA = "rainskills.auto-update.v1";
14
30
  const UPDATE_STATE_VERSION = 1;
15
31
  const UPDATE_OPERATION_ID = "b53d72d4-5e88-4aae-9a9d-8305142d0262";
16
32
  const DEFAULT_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
17
33
  const MAX_METADATA_BYTES = 64 * 1024;
34
+ const MAX_TARBALL_BYTES = 16 * 1024 * 1024;
35
+ const METADATA_TIMEOUT_MS = 3000;
36
+ const TARBALL_TIMEOUT_MS = 15_000;
37
+ const INTERNAL_TIMEOUT_CODE = "RAINSKILLS_REQUEST_TIMEOUT";
38
+ const PROTOCOL_ERROR_CODE = "RAINSKILLS_PROTOCOL_ERROR";
39
+ const METADATA_FALLBACK_ERROR_CODES = Object.freeze([
40
+ "ENOTFOUND", "EAI_AGAIN", "ECONNREFUSED", "ECONNRESET", "ETIMEDOUT",
41
+ "EHOSTUNREACH", "ENETUNREACH",
42
+ ]);
18
43
  const STABLE_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
19
44
  const SAFE_ENTRY = Object.freeze(["runtime", "status", "--json"]);
20
45
  const DEFAULT_AGENT_ROOTS = Object.freeze([
@@ -22,6 +47,23 @@ const DEFAULT_AGENT_ROOTS = Object.freeze([
22
47
  [".claude", "skills"],
23
48
  [".agents", "skills"],
24
49
  ]);
50
+ const AUTO_UPDATE_ENVIRONMENT_KEYS = Object.freeze([
51
+ "HOME", "USERPROFILE", "PATH", "Path", "SystemRoot", "ComSpec",
52
+ "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL",
53
+ "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy",
54
+ ]);
55
+
56
+ function protocolError(message) {
57
+ const error = new Error(message);
58
+ error.code = PROTOCOL_ERROR_CODE;
59
+ return error;
60
+ }
61
+
62
+ function timeoutError(label) {
63
+ const error = new Error(`${label}请求超时`);
64
+ error.code = INTERNAL_TIMEOUT_CODE;
65
+ return error;
66
+ }
25
67
 
26
68
  function isStableVersion(value) {
27
69
  return typeof value === "string" && STABLE_VERSION_PATTERN.test(value);
@@ -48,36 +90,85 @@ function isSafeAutoUpdateEntry(args) {
48
90
  && args.every((value, index) => value === SAFE_ENTRY[index]);
49
91
  }
50
92
 
51
- function validateOfficialLatestMetadata(metadata) {
93
+ function tarballUrlFor(source, version) {
94
+ if (source === REGISTRY_SOURCES.mirror) {
95
+ return `${source.tarballOrigin}packages/rainskills/${version}/rainskills-${version}.tgz`;
96
+ }
97
+ return `${source.tarballOrigin}rainskills/-/rainskills-${version}.tgz`;
98
+ }
99
+
100
+ function metadataTarballUrlFor(source, version) {
101
+ return `${source.registry}rainskills/-/rainskills-${version}.tgz`;
102
+ }
103
+
104
+ function sourceForRegistry(registry) {
105
+ return Object.values(REGISTRY_SOURCES).find((source) => source.registry === registry) || null;
106
+ }
107
+
108
+ function validateRegistryLatestMetadata(metadata, source) {
109
+ if (!Object.values(REGISTRY_SOURCES).includes(source)) {
110
+ throw protocolError("npm registry 来源无效");
111
+ }
52
112
  if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
53
- throw new Error("npm latest metadata 无效");
113
+ throw protocolError("npm latest metadata 无效");
54
114
  }
55
115
  if (metadata.name !== "rainskills" || !isStableVersion(metadata.version)) {
56
- throw new Error("npm latest 不是 Rainskills 正式版");
116
+ throw protocolError("npm latest 不是 Rainskills 正式版");
57
117
  }
58
- const expectedTarball = `${OFFICIAL_REGISTRY}rainskills/-/rainskills-${metadata.version}.tgz`;
118
+ const expectedMetadataTarball = metadataTarballUrlFor(source, metadata.version);
119
+ const downloadTarball = tarballUrlFor(source, metadata.version);
59
120
  if (
60
121
  !metadata.dist
61
122
  || typeof metadata.dist !== "object"
62
123
  || typeof metadata.dist.integrity !== "string"
63
124
  || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(metadata.dist.integrity)
64
- || metadata.dist.tarball !== expectedTarball
125
+ || metadata.dist.tarball !== expectedMetadataTarball
65
126
  ) {
66
- throw new Error("npm latest 制品来源或完整性信息无效");
127
+ throw protocolError("npm latest 制品来源或完整性信息无效");
67
128
  }
68
- return { version: metadata.version };
129
+ return {
130
+ version: metadata.version,
131
+ registry: source.registry,
132
+ tarball: downloadTarball,
133
+ integrity: metadata.dist.integrity,
134
+ };
69
135
  }
70
136
 
71
- function fetchOfficialLatest({
137
+ function validateOfficialLatestMetadata(metadata) {
138
+ return validateRegistryLatestMetadata(metadata, REGISTRY_SOURCES.official);
139
+ }
140
+
141
+ function validateContentLength(headers, maxBytes, { allowZero, label }) {
142
+ const raw = headers && headers["content-length"];
143
+ if (raw === undefined) return;
144
+ if (typeof raw !== "string" || !/^(0|[1-9]\d*)$/.test(raw)) {
145
+ throw protocolError(`${label} Content-Length 无效`);
146
+ }
147
+ const value = Number(raw);
148
+ if ((!allowZero && value === 0) || !Number.isSafeInteger(value) || value > maxBytes) {
149
+ throw protocolError(`${label} Content-Length 超出限制`);
150
+ }
151
+ }
152
+
153
+ function fixedHttpsOptions(headers) {
154
+ return {
155
+ headers,
156
+ rejectUnauthorized: true,
157
+ checkServerIdentity: tls.checkServerIdentity,
158
+ ca: tls.rootCertificates,
159
+ };
160
+ }
161
+
162
+ function fetchRegistryMetadata(source, {
72
163
  request = https.get,
73
- timeoutMs = 3000,
164
+ timeoutMs = METADATA_TIMEOUT_MS,
74
165
  maxBytes = MAX_METADATA_BYTES,
75
166
  } = {}) {
76
167
  return new Promise((resolve, reject) => {
77
168
  let settled = false;
78
169
  let req;
79
170
  const deadline = setTimeout(() => {
80
- req?.destroy(new Error("npm latest 请求超时"));
171
+ req?.destroy(timeoutError("npm latest "));
81
172
  }, timeoutMs);
82
173
  const finish = (error, value) => {
83
174
  if (settled) return;
@@ -86,12 +177,17 @@ function fetchOfficialLatest({
86
177
  if (error) reject(error);
87
178
  else resolve(value);
88
179
  };
89
- req = request(OFFICIAL_LATEST_URL, {
90
- headers: { accept: "application/json" },
91
- }, (response) => {
180
+ req = request(source.latestUrl, fixedHttpsOptions({ accept: "application/json" }), (response) => {
92
181
  if (response.statusCode !== 200 || response.headers.location) {
93
182
  response.resume();
94
- finish(new Error("npm latest 响应无效"));
183
+ finish(protocolError("npm latest 响应无效"));
184
+ return;
185
+ }
186
+ try {
187
+ validateContentLength(response.headers, maxBytes, { allowZero: true, label: "npm latest" });
188
+ } catch (error) {
189
+ response.resume();
190
+ finish(error);
95
191
  return;
96
192
  }
97
193
  let size = 0;
@@ -99,7 +195,7 @@ function fetchOfficialLatest({
99
195
  response.on("data", (chunk) => {
100
196
  size += chunk.length;
101
197
  if (size > maxBytes) {
102
- req.destroy(new Error("npm latest 响应过大"));
198
+ req.destroy(protocolError("npm latest 响应过大"));
103
199
  return;
104
200
  }
105
201
  chunks.push(chunk);
@@ -108,17 +204,290 @@ function fetchOfficialLatest({
108
204
  response.once("end", () => {
109
205
  try {
110
206
  const metadata = JSON.parse(Buffer.concat(chunks).toString("utf8"));
111
- finish(null, validateOfficialLatestMetadata(metadata));
207
+ finish(null, validateRegistryLatestMetadata(metadata, source));
208
+ } catch (error) {
209
+ finish(error.code ? error : protocolError("npm latest JSON 无效"));
210
+ }
211
+ });
212
+ });
213
+ req.setTimeout(timeoutMs, () => req.destroy(timeoutError("npm latest ")));
214
+ req.once("error", (error) => finish(error));
215
+ });
216
+ }
217
+
218
+ function fetchOfficialLatest(options = {}) {
219
+ return fetchRegistryMetadata(REGISTRY_SOURCES.official, options);
220
+ }
221
+
222
+ function isMetadataFallbackError(error) {
223
+ return error?.code === INTERNAL_TIMEOUT_CODE
224
+ || METADATA_FALLBACK_ERROR_CODES.includes(error?.code);
225
+ }
226
+
227
+ async function fetchLatestWithFallback({
228
+ fetchMetadata = fetchRegistryMetadata,
229
+ ...requestOptions
230
+ } = {}) {
231
+ try {
232
+ return await fetchMetadata(REGISTRY_SOURCES.official, requestOptions);
233
+ } catch (error) {
234
+ if (!isMetadataFallbackError(error)) throw error;
235
+ return fetchMetadata(REGISTRY_SOURCES.mirror, requestOptions);
236
+ }
237
+ }
238
+
239
+ function validateUpdateDescriptor(descriptor) {
240
+ const source = sourceForRegistry(descriptor?.registry);
241
+ if (
242
+ !source
243
+ || !isStableVersion(descriptor?.version)
244
+ || descriptor.tarball !== tarballUrlFor(source, descriptor.version)
245
+ || typeof descriptor.integrity !== "string"
246
+ || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(descriptor.integrity)
247
+ ) {
248
+ throw protocolError("自动升级制品描述无效");
249
+ }
250
+ return { ...descriptor };
251
+ }
252
+
253
+ function expectedIntegrityBytes(integrity) {
254
+ const bytes = Buffer.from(integrity.slice("sha512-".length), "base64");
255
+ if (bytes.length !== 64) throw protocolError("自动升级 SHA-512 integrity 无效");
256
+ return bytes;
257
+ }
258
+
259
+ function writeAllSync(fd, chunk) {
260
+ let offset = 0;
261
+ while (offset < chunk.length) {
262
+ offset += fs.writeSync(fd, chunk, offset, chunk.length - offset);
263
+ }
264
+ }
265
+
266
+ function hashOpenFile(fd, maxBytes = MAX_TARBALL_BYTES) {
267
+ const hash = crypto.createHash("sha512");
268
+ const buffer = Buffer.allocUnsafe(64 * 1024);
269
+ let size = 0;
270
+ let offset = 0;
271
+ for (;;) {
272
+ const count = fs.readSync(fd, buffer, 0, buffer.length, offset);
273
+ if (count === 0) break;
274
+ size += count;
275
+ if (size > maxBytes) throw protocolError("自动升级本地制品过大");
276
+ hash.update(buffer.subarray(0, count));
277
+ offset += count;
278
+ }
279
+ if (size === 0) throw protocolError("自动升级本地制品为空");
280
+ return { size, digest: hash.digest() };
281
+ }
282
+
283
+ function downloadTarballToDescriptor(descriptor, fd, {
284
+ request = https.get,
285
+ timeoutMs = TARBALL_TIMEOUT_MS,
286
+ maxBytes = MAX_TARBALL_BYTES,
287
+ } = {}) {
288
+ return new Promise((resolve, reject) => {
289
+ let settled = false;
290
+ let req;
291
+ let size = 0;
292
+ const hash = crypto.createHash("sha512");
293
+ const deadline = setTimeout(() => req?.destroy(timeoutError("npm tarball ")), timeoutMs);
294
+ const finish = (error, value) => {
295
+ if (settled) return;
296
+ settled = true;
297
+ clearTimeout(deadline);
298
+ if (error) reject(error);
299
+ else resolve(value);
300
+ };
301
+ req = request(descriptor.tarball, fixedHttpsOptions({ accept: "application/octet-stream" }), (response) => {
302
+ if (response.statusCode !== 200 || response.headers.location) {
303
+ response.resume();
304
+ finish(protocolError("npm tarball 响应无效"));
305
+ return;
306
+ }
307
+ try {
308
+ validateContentLength(response.headers, maxBytes, { allowZero: false, label: "npm tarball" });
309
+ } catch (error) {
310
+ response.resume();
311
+ finish(error);
312
+ return;
313
+ }
314
+ response.on("data", (chunk) => {
315
+ if (settled) return;
316
+ size += chunk.length;
317
+ if (size > maxBytes) {
318
+ req.destroy(protocolError("npm tarball 响应过大"));
319
+ return;
320
+ }
321
+ try {
322
+ writeAllSync(fd, chunk);
323
+ hash.update(chunk);
324
+ } catch (error) {
325
+ req.destroy(error);
326
+ }
327
+ });
328
+ response.once("error", (error) => finish(error));
329
+ response.once("end", () => {
330
+ if (size === 0) {
331
+ finish(protocolError("npm tarball 为空"));
332
+ return;
333
+ }
334
+ try {
335
+ const actual = hash.digest();
336
+ const expected = expectedIntegrityBytes(descriptor.integrity);
337
+ if (!crypto.timingSafeEqual(actual, expected)) {
338
+ throw protocolError("npm tarball SHA-512 integrity 不匹配");
339
+ }
340
+ finish(null, { size, digest: actual.toString("hex") });
112
341
  } catch (error) {
113
342
  finish(error);
114
343
  }
115
344
  });
116
345
  });
117
- req.setTimeout(timeoutMs, () => req.destroy(new Error("npm latest 请求超时")));
346
+ req.setTimeout(timeoutMs, () => req.destroy(timeoutError("npm tarball ")));
118
347
  req.once("error", (error) => finish(error));
119
348
  });
120
349
  }
121
350
 
351
+ async function acquireStableUpdateArtifact(descriptor, {
352
+ home = os.homedir(),
353
+ platform = process.platform,
354
+ stateStore = createDefaultStateStore(platform, home),
355
+ request = https.get,
356
+ randomBytes = crypto.randomBytes,
357
+ } = {}) {
358
+ const validated = validateUpdateDescriptor(descriptor);
359
+ const directory = stateStore.ensurePrivateDirectory(
360
+ path.join(home, ".rainbond", "rainskills", "update-artifacts")
361
+ );
362
+ const digestPrefix = expectedIntegrityBytes(validated.integrity).toString("hex").slice(0, 16);
363
+ const finalPath = path.join(directory, `rainskills-${validated.version}-${digestPrefix}.tgz`);
364
+ if (fs.existsSync(finalPath)) {
365
+ stateStore.assertProtectedRegularFile(finalPath);
366
+ const noFollow = fs.constants.O_NOFOLLOW || 0;
367
+ const existingFd = fs.openSync(finalPath, fs.constants.O_RDONLY | noFollow);
368
+ let identity;
369
+ try {
370
+ identity = fs.fstatSync(existingFd);
371
+ const pathIdentity = fs.lstatSync(finalPath);
372
+ if (
373
+ pathIdentity.isSymbolicLink()
374
+ || !pathIdentity.isFile()
375
+ || pathIdentity.dev !== identity.dev
376
+ || pathIdentity.ino !== identity.ino
377
+ ) throw protocolError("自动升级既有制品身份发生变化");
378
+ const actual = hashOpenFile(existingFd);
379
+ if (!crypto.timingSafeEqual(actual.digest, expectedIntegrityBytes(validated.integrity))) {
380
+ throw protocolError("自动升级既有制品 SHA-512 integrity 不匹配");
381
+ }
382
+ } finally {
383
+ fs.closeSync(existingFd);
384
+ }
385
+ let cleaned = false;
386
+ return {
387
+ path: finalPath,
388
+ cleanup() {
389
+ if (cleaned) return;
390
+ cleaned = true;
391
+ try {
392
+ const current = fs.lstatSync(finalPath);
393
+ if (
394
+ !current.isSymbolicLink()
395
+ && current.isFile()
396
+ && current.dev === identity.dev
397
+ && current.ino === identity.ino
398
+ ) fs.unlinkSync(finalPath);
399
+ } catch (error) {
400
+ if (error.code !== "ENOENT") throw error;
401
+ }
402
+ },
403
+ };
404
+ }
405
+ const candidatePath = path.join(
406
+ directory,
407
+ `.rainskills-${validated.version}.${process.pid}.${randomBytes(6).toString("hex")}.candidate`
408
+ );
409
+ const noFollow = fs.constants.O_NOFOLLOW || 0;
410
+ let fd;
411
+ let published = false;
412
+ let identity;
413
+ try {
414
+ fd = fs.openSync(
415
+ candidatePath,
416
+ fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow,
417
+ 0o600
418
+ );
419
+ await downloadTarballToDescriptor(validated, fd, { request });
420
+ fs.fsyncSync(fd);
421
+ stateStore.protectRegularFile(candidatePath);
422
+ identity = fs.fstatSync(fd);
423
+ const candidateIdentity = fs.lstatSync(candidatePath);
424
+ if (
425
+ candidateIdentity.isSymbolicLink()
426
+ || !candidateIdentity.isFile()
427
+ || candidateIdentity.dev !== identity.dev
428
+ || candidateIdentity.ino !== identity.ino
429
+ ) {
430
+ throw protocolError("自动升级候选制品身份发生变化");
431
+ }
432
+ fs.linkSync(candidatePath, finalPath);
433
+ published = true;
434
+ stateStore.protectRegularFile(finalPath);
435
+ const finalIdentity = fs.lstatSync(finalPath);
436
+ if (
437
+ finalIdentity.isSymbolicLink()
438
+ || !finalIdentity.isFile()
439
+ || finalIdentity.dev !== identity.dev
440
+ || finalIdentity.ino !== identity.ino
441
+ ) {
442
+ throw protocolError("自动升级最终制品身份发生变化");
443
+ }
444
+ fs.unlinkSync(candidatePath);
445
+ if (platform !== "win32") {
446
+ const directoryFd = fs.openSync(directory, "r");
447
+ try { fs.fsyncSync(directoryFd); } finally { fs.closeSync(directoryFd); }
448
+ }
449
+ fs.closeSync(fd);
450
+ fd = undefined;
451
+ let cleaned = false;
452
+ return {
453
+ path: finalPath,
454
+ cleanup() {
455
+ if (cleaned) return;
456
+ cleaned = true;
457
+ try {
458
+ const current = fs.lstatSync(finalPath);
459
+ if (
460
+ !current.isSymbolicLink()
461
+ && current.isFile()
462
+ && current.dev === identity.dev
463
+ && current.ino === identity.ino
464
+ ) fs.unlinkSync(finalPath);
465
+ } catch (error) {
466
+ if (error.code !== "ENOENT") throw error;
467
+ }
468
+ },
469
+ };
470
+ } catch (error) {
471
+ if (fd !== undefined) fs.closeSync(fd);
472
+ for (const target of [candidatePath, ...(published ? [finalPath] : [])]) {
473
+ try { fs.unlinkSync(target); } catch (cleanupError) {
474
+ if (cleanupError.code !== "ENOENT") throw cleanupError;
475
+ }
476
+ }
477
+ throw error;
478
+ }
479
+ }
480
+
481
+ async function prepareStableUpdate({
482
+ fetchLatest = fetchLatestWithFallback,
483
+ acquireArtifact = acquireStableUpdateArtifact,
484
+ ...options
485
+ } = {}) {
486
+ const descriptor = validateUpdateDescriptor(await fetchLatest(options));
487
+ const artifact = await acquireArtifact(descriptor, options);
488
+ return { descriptor, artifact };
489
+ }
490
+
122
491
  function createDefaultStateStore(platform, home) {
123
492
  if (platform === "win32") {
124
493
  const { createWindowsSecureStateStore } = require("./windows-platform.js");
@@ -276,7 +645,7 @@ async function checkForStableUpdate({
276
645
  platform = process.platform,
277
646
  now = () => Date.now(),
278
647
  ttlMs = DEFAULT_CHECK_TTL_MS,
279
- fetchLatest = fetchOfficialLatest,
648
+ fetchLatest = fetchLatestWithFallback,
280
649
  activeOperationDetector,
281
650
  updateState,
282
651
  } = {}) {
@@ -312,7 +681,9 @@ async function checkForStableUpdate({
312
681
  try { state.recordFailure?.(); } catch { /* best-effort only */ }
313
682
  return { action: "continue", reason: "check-failed" };
314
683
  }
315
- if (!latest || !isStableVersion(latest.version)) {
684
+ try {
685
+ latest = validateUpdateDescriptor(latest);
686
+ } catch {
316
687
  try { state.recordCheck(null); } catch { /* best-effort only */ }
317
688
  return { action: "continue", reason: "invalid-latest" };
318
689
  }
@@ -320,28 +691,55 @@ async function checkForStableUpdate({
320
691
  try { state.recordCheck(latest.version); } catch { /* best-effort only */ }
321
692
  return { action: "continue", reason: "up-to-date" };
322
693
  }
323
- return { action: "delegate", version: latest.version };
694
+ return { action: "delegate", ...latest };
324
695
  }
325
696
 
326
- function buildStableUpdateInvocation(version, args, { platform = process.platform } = {}) {
327
- if (!isStableVersion(version)) throw new Error("自动升级只能委托到正式版");
697
+ function buildStableUpdateInvocation(descriptor, args, {
698
+ platform = process.platform,
699
+ artifactPath,
700
+ } = {}) {
701
+ const validated = validateUpdateDescriptor(descriptor);
328
702
  if (!isSafeAutoUpdateEntry(args)) throw new Error("自动升级入口无效");
703
+ const isAbsoluteArtifact = platform === "win32"
704
+ ? path.win32.isAbsolute(artifactPath || "")
705
+ : path.isAbsolute(artifactPath || "");
706
+ if (typeof artifactPath !== "string" || !isAbsoluteArtifact) {
707
+ throw new Error("自动升级本地制品路径无效");
708
+ }
329
709
  return {
330
- executable: platform === "win32" ? "npx.cmd" : "npx",
331
- args: ["--yes", "--ignore-scripts", `rainskills@${version}`, ...args],
710
+ executable: platform === "win32" ? "npm.cmd" : "npm",
711
+ args: [
712
+ "exec",
713
+ "--yes",
714
+ "--ignore-scripts",
715
+ `--registry=${validated.registry}`,
716
+ `--package=${artifactPath}`,
717
+ "--",
718
+ "rainskills",
719
+ ...args,
720
+ ],
332
721
  };
333
722
  }
334
723
 
335
- function buildStableUpdateEnvironment(source, { fromVersion, targetVersion }) {
724
+ function sanitizeAutoUpdateEnvironment(source = process.env) {
725
+ const environment = {};
726
+ for (const key of AUTO_UPDATE_ENVIRONMENT_KEYS) {
727
+ if (typeof source[key] === "string") environment[key] = source[key];
728
+ }
729
+ return environment;
730
+ }
731
+
732
+ function buildStableUpdateEnvironment(source, { fromVersion, targetVersion, registry }) {
336
733
  if (!isStableVersion(fromVersion) || !isStableVersion(targetVersion)) {
337
734
  throw new Error("自动升级环境只能绑定正式版");
338
735
  }
736
+ if (!sourceForRegistry(registry)) throw new Error("自动升级 registry 无效");
339
737
  return {
340
- ...source,
738
+ ...sanitizeAutoUpdateEnvironment(source),
341
739
  RAINSKILLS_AUTO_UPDATE_HOP: "1",
342
740
  RAINSKILLS_AUTO_UPDATE_FROM: fromVersion,
343
741
  RAINSKILLS_AUTO_UPDATE_TARGET: targetVersion,
344
- npm_config_registry: OFFICIAL_REGISTRY,
742
+ npm_config_registry: registry,
345
743
  npm_config_ignore_scripts: "true",
346
744
  };
347
745
  }
@@ -492,19 +890,35 @@ function synchronizeInstalledSkills({
492
890
  }
493
891
 
494
892
  module.exports = {
893
+ AUTO_UPDATE_ENVIRONMENT_KEYS,
495
894
  DEFAULT_CHECK_TTL_MS,
895
+ INTERNAL_TIMEOUT_CODE,
896
+ MAX_METADATA_BYTES,
897
+ MAX_TARBALL_BYTES,
898
+ METADATA_FALLBACK_ERROR_CODES,
899
+ METADATA_TIMEOUT_MS,
496
900
  OFFICIAL_LATEST_URL,
901
+ OFFICIAL_REGISTRY,
902
+ REGISTRY_SOURCES,
903
+ TARBALL_TIMEOUT_MS,
904
+ acquireStableUpdateArtifact,
497
905
  buildStableUpdateEnvironment,
498
906
  buildStableUpdateInvocation,
499
907
  checkForStableUpdate,
500
908
  compareStableVersions,
501
909
  createAutoUpdateState,
910
+ fetchLatestWithFallback,
502
911
  fetchOfficialLatest,
912
+ fetchRegistryMetadata,
503
913
  hasActiveOperation,
504
914
  isSafeAutoUpdateEntry,
505
915
  isStableVersion,
916
+ prepareStableUpdate,
506
917
  recordSkillInstallDestinations,
507
918
  resolveInstallDestinations,
919
+ sanitizeAutoUpdateEnvironment,
508
920
  synchronizeInstalledSkills,
921
+ validateRegistryLatestMetadata,
922
+ validateUpdateDescriptor,
509
923
  validateOfficialLatestMetadata,
510
924
  };
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
 
3
3
  module.exports = Object.freeze({
4
- version: "0.1.4",
4
+ version: "0.1.6",
5
5
  });
@@ -14,7 +14,7 @@ Node.js 前置检查通过后,每次请求先执行本地 launcher + `["enviro
14
14
 
15
15
  第一步检查 Node.js 是否存在且主版本不低于 18。Node.js 缺失或低于 18 时,只说明“Rainskills 执行组件需要 Node.js 18 或更高版本”并停止:不选择运行环境,不调用 MCP,不猜测替代命令。只有用户或 agent 明确同意后才安装或升级 Node.js,再从同一原始 intent 继续。
16
16
 
17
- 固定 launcher 是 `["npx", "--yes", "rainskills@0.1.4"]`;版本必须与本技能包 `package.json` 一致。把 launcher 与参数拼成 argv 数组直接执行,禁止 `rainskills@latest` 或执行 shell 字符串。
17
+ 固定 launcher 是 `["npx", "--yes", "rainskills@0.1.6"]`;版本必须与本技能包 `package.json` 一致。把 launcher 与参数拼成 argv 数组直接执行,禁止 `rainskills@latest` 或执行 shell 字符串。
18
18
 
19
19
  本地 launcher 必须从当前 Skill 所在目录的同级目录定位 `rainbond-platform-installer/scripts/local-runtime.js`,解析为绝对路径后使用 `["node", "<绝对路径>"]` 执行。`environment list`、`operation begin`、`operation complete` 和 `runtime message` 只能使用本地 launcher;本地 launcher 只读取已安装文件和本机受保护状态,不得访问 npm 或其它网络。只有用户选定连接运行环境后,才使用上面的固定 npx launcher。
20
20
 
@@ -22,7 +22,7 @@ Node.js 前置检查通过后,每次请求先执行本地 launcher + `["enviro
22
22
  ```json
23
23
  {
24
24
  "schema": "rainskills.skill-runtime-contract.v1",
25
- "launcher": ["npx", "--yes", "rainskills@0.1.4"],
25
+ "launcher": ["npx", "--yes", "rainskills@0.1.6"],
26
26
  "local_launcher": ["node", "<installed-skills-root>/rainbond-platform-installer/scripts/local-runtime.js"],
27
27
  "local_argv": {
28
28
  "environment-list": ["node", "<installed-skills-root>/rainbond-platform-installer/scripts/local-runtime.js", "environment", "list", "--json"],
@@ -35,8 +35,8 @@ Node.js 前置检查通过后,每次请求先执行本地 launcher + `["enviro
35
35
  },
36
36
  "routes": {"existing": ["saas", "private-existing"]},
37
37
  "connect_argv": {
38
- "saas": ["npx", "--yes", "rainskills@0.1.4", "runtime", "connect", "<target>", "--saas", "--intent-json", "<intent-json>"],
39
- "private-existing": ["npx", "--yes", "rainskills@0.1.4", "runtime", "connect", "<target>", "--rainbond-url", "<rainbond-url>", "--intent-json", "<intent-json>"]
38
+ "saas": ["npx", "--yes", "rainskills@0.1.6", "runtime", "connect", "<target>", "--saas", "--intent-json", "<intent-json>"],
39
+ "private-existing": ["npx", "--yes", "rainskills@0.1.6", "runtime", "connect", "<target>", "--rainbond-url", "<rainbond-url>", "--intent-json", "<intent-json>"]
40
40
  }
41
41
  }
42
42
  ```
@@ -14,7 +14,7 @@ description: "Use only when the user explicitly asks to initialize or link a loc
14
14
 
15
15
  第一步检查 Node.js 是否存在且主版本不低于 18。Node.js 缺失或低于 18 时,只说明“Rainskills 执行组件需要 Node.js 18 或更高版本”并停止:不选择运行环境,不调用 MCP,不猜测替代命令。只有用户或 agent 明确同意后才安装或升级 Node.js,再从同一原始 intent 继续。
16
16
 
17
- 固定 launcher 是 `["npx", "--yes", "rainskills@0.1.4"]`;版本必须与本技能包 `package.json` 一致。把 launcher 与参数拼成 argv 数组直接执行,禁止 `rainskills@latest` 或执行 shell 字符串。
17
+ 固定 launcher 是 `["npx", "--yes", "rainskills@0.1.6"]`;版本必须与本技能包 `package.json` 一致。把 launcher 与参数拼成 argv 数组直接执行,禁止 `rainskills@latest` 或执行 shell 字符串。
18
18
 
19
19
  本地 launcher 必须从当前 Skill 所在目录的同级目录定位 `rainbond-platform-installer/scripts/local-runtime.js`,解析为绝对路径后使用 `["node", "<绝对路径>"]` 执行。`environment list`、`operation begin`、`operation complete` 和 `runtime message` 只能使用本地 launcher;本地 launcher 只读取已安装文件和本机受保护状态,不得访问 npm 或其它网络。只有用户选定连接或安装运行环境后,才使用上面的固定 npx launcher。
20
20
 
@@ -22,7 +22,7 @@ description: "Use only when the user explicitly asks to initialize or link a loc
22
22
  ```json
23
23
  {
24
24
  "schema": "rainskills.skill-runtime-contract.v1",
25
- "launcher": ["npx", "--yes", "rainskills@0.1.4"],
25
+ "launcher": ["npx", "--yes", "rainskills@0.1.6"],
26
26
  "local_launcher": ["node", "<installed-skills-root>/rainbond-platform-installer/scripts/local-runtime.js"],
27
27
  "local_argv": {
28
28
  "environment-list": ["node", "<installed-skills-root>/rainbond-platform-installer/scripts/local-runtime.js", "environment", "list", "--json"],
@@ -35,9 +35,9 @@ description: "Use only when the user explicitly asks to initialize or link a loc
35
35
  },
36
36
  "routes": {"new": ["saas", "private-existing", "install-private"]},
37
37
  "connect_argv": {
38
- "saas": ["npx", "--yes", "rainskills@0.1.4", "runtime", "connect", "<target>", "--saas", "--intent-json", "<intent-json>"],
39
- "private-existing": ["npx", "--yes", "rainskills@0.1.4", "runtime", "connect", "<target>", "--rainbond-url", "<rainbond-url>", "--intent-json", "<intent-json>"],
40
- "install-private": ["npx", "--yes", "rainskills@0.1.4", "runtime", "connect", "<target>", "--install-private", "--location", "<private-location>", "--intent-json", "<intent-json>"]
38
+ "saas": ["npx", "--yes", "rainskills@0.1.6", "runtime", "connect", "<target>", "--saas", "--intent-json", "<intent-json>"],
39
+ "private-existing": ["npx", "--yes", "rainskills@0.1.6", "runtime", "connect", "<target>", "--rainbond-url", "<rainbond-url>", "--intent-json", "<intent-json>"],
40
+ "install-private": ["npx", "--yes", "rainskills@0.1.6", "runtime", "connect", "<target>", "--install-private", "--location", "<private-location>", "--intent-json", "<intent-json>"]
41
41
  }
42
42
  }
43
43
  ```
@@ -14,7 +14,7 @@ description: "Use when the user explicitly asks to install a local or cloud Rain
14
14
 
15
15
  第一步检查 Node.js 是否存在且主版本不低于 18。Node.js 缺失或低于 18 时,只说明“Rainskills 执行组件需要 Node.js 18 或更高版本”并停止:不选择运行环境,不调用 MCP,不猜测替代命令。只有用户或 agent 明确同意后才安装或升级 Node.js,再从同一原始 intent 继续。
16
16
 
17
- 固定 launcher 是 `["npx", "--yes", "rainskills@0.1.4"]`;版本必须与本技能包 `package.json` 一致。把 launcher 与参数拼成 argv 数组直接执行,禁止 `rainskills@latest` 或执行 shell 字符串。
17
+ 固定 launcher 是 `["npx", "--yes", "rainskills@0.1.6"]`;版本必须与本技能包 `package.json` 一致。把 launcher 与参数拼成 argv 数组直接执行,禁止 `rainskills@latest` 或执行 shell 字符串。
18
18
 
19
19
  本地 launcher 必须从当前 Skill 所在目录的同级目录定位 `rainbond-platform-installer/scripts/local-runtime.js`,解析为绝对路径后使用 `["node", "<绝对路径>"]` 执行。`environment list`、`operation begin`、`operation complete` 和 `runtime message` 只能使用本地 launcher;本地 launcher 只读取已安装文件和本机受保护状态,不得访问 npm 或其它网络。只有用户选定连接或安装运行环境后,才使用上面的固定 npx launcher。
20
20
 
@@ -22,7 +22,7 @@ description: "Use when the user explicitly asks to install a local or cloud Rain
22
22
  ```json
23
23
  {
24
24
  "schema": "rainskills.skill-runtime-contract.v1",
25
- "launcher": ["npx", "--yes", "rainskills@0.1.4"],
25
+ "launcher": ["npx", "--yes", "rainskills@0.1.6"],
26
26
  "local_launcher": ["node", "<installed-skills-root>/rainbond-platform-installer/scripts/local-runtime.js"],
27
27
  "local_argv": {
28
28
  "environment-list": ["node", "<installed-skills-root>/rainbond-platform-installer/scripts/local-runtime.js", "environment", "list", "--json"],
@@ -38,9 +38,9 @@ description: "Use when the user explicitly asks to install a local or cloud Rain
38
38
  "existing": ["saas", "private-existing"]
39
39
  },
40
40
  "connect_argv": {
41
- "saas": ["npx", "--yes", "rainskills@0.1.4", "runtime", "connect", "<target>", "--saas", "--intent-json", "<intent-json>"],
42
- "private-existing": ["npx", "--yes", "rainskills@0.1.4", "runtime", "connect", "<target>", "--rainbond-url", "<rainbond-url>", "--intent-json", "<intent-json>"],
43
- "install-private": ["npx", "--yes", "rainskills@0.1.4", "runtime", "connect", "<target>", "--install-private", "--location", "<private-location>", "--intent-json", "<intent-json>"]
41
+ "saas": ["npx", "--yes", "rainskills@0.1.6", "runtime", "connect", "<target>", "--saas", "--intent-json", "<intent-json>"],
42
+ "private-existing": ["npx", "--yes", "rainskills@0.1.6", "runtime", "connect", "<target>", "--rainbond-url", "<rainbond-url>", "--intent-json", "<intent-json>"],
43
+ "install-private": ["npx", "--yes", "rainskills@0.1.6", "runtime", "connect", "<target>", "--install-private", "--location", "<private-location>", "--intent-json", "<intent-json>"]
44
44
  }
45
45
  }
46
46
  ```