rainskills 0.1.42 → 0.1.44

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 (171) hide show
  1. package/README.md +140 -24
  2. package/SKILL.md +3 -3
  3. package/bin/delivery-probe.js +306 -0
  4. package/bin/handoff-context.js +151 -0
  5. package/bin/protected-poll.js +116 -0
  6. package/bin/rainskills-tools.js +132 -8
  7. package/bin/read-snapshot.js +82 -0
  8. package/contracts/handoff-context.schema.yaml +62 -0
  9. package/contracts/runtime/cli-base.md +43 -0
  10. package/contracts/runtime/embedded-base.md +20 -0
  11. package/contracts/runtime/overlay.schema.json +24 -0
  12. package/contracts/runtime/routing-modes.yaml +30 -0
  13. package/contracts/runtime/skills/rainbond-ai-assistant.yaml +13 -0
  14. package/contracts/runtime/skills/rainbond-app-assistant.yaml +12 -0
  15. package/contracts/runtime/skills/rainbond-app-version-assistant.yaml +11 -0
  16. package/contracts/runtime/skills/rainbond-delivery-verifier.yaml +12 -0
  17. package/contracts/runtime/skills/rainbond-env-sync.yaml +12 -0
  18. package/contracts/runtime/skills/rainbond-fullstack-bootstrap.yaml +12 -0
  19. package/contracts/runtime/skills/rainbond-fullstack-troubleshooter.yaml +12 -0
  20. package/contracts/runtime/skills/rainbond-opensource-app-deploy.yaml +12 -0
  21. package/contracts/runtime/skills/rainbond-platform-plugin-manager.yaml +12 -0
  22. package/contracts/runtime/skills/rainbond-platform-query.yaml +12 -0
  23. package/contracts/runtime/skills/rainbond-project-init.yaml +12 -0
  24. package/contracts/runtime/skills/rainbond-template-installer.yaml +12 -0
  25. package/contracts/shared/user-result.md +14 -0
  26. package/dsh/cordis.patch.yml +6 -0
  27. package/dsh/index.mjs +73 -0
  28. package/install.sh +5 -6
  29. package/marketplace/rainskills/.claude-plugin/plugin.json +1 -1
  30. package/marketplace/rainskills/.codex-plugin/plugin.json +1 -1
  31. package/marketplace/rainskills/skills/rainskills/SKILL.md +4 -4
  32. package/package.json +41 -9
  33. package/rainbond-ai-assistant/SKILL.md +3 -3
  34. package/rainbond-ai-assistant/references/generated/runtime-gate.md +189 -0
  35. package/rainbond-ai-assistant/references/generated/runtime-routing.md +15 -0
  36. package/rainbond-ai-assistant/references/generated/user-result.md +16 -0
  37. package/rainbond-ai-assistant/references/output-contract.md +1 -17
  38. package/rainbond-app-assistant/SKILL.md +9 -5
  39. package/rainbond-app-assistant/evals/04-delivered-stop-without-promotion.expected.yaml +0 -5
  40. package/rainbond-app-assistant/evals/04-delivered-stop-without-promotion.response.md +4 -25
  41. package/rainbond-app-assistant/evals/11-delivered-with-proxy-and-local-binding.expected.yaml +0 -9
  42. package/rainbond-app-assistant/evals/11-delivered-with-proxy-and-local-binding.response.md +4 -28
  43. package/rainbond-app-assistant/evals/16-source-delivered-manual-validation.expected.yaml +0 -6
  44. package/rainbond-app-assistant/evals/16-source-delivered-manual-validation.response.md +4 -23
  45. package/rainbond-app-assistant/references/generated/runtime-gate.md +227 -0
  46. package/rainbond-app-assistant/references/generated/runtime-routing.md +29 -0
  47. package/rainbond-app-assistant/references/generated/user-result.md +16 -0
  48. package/rainbond-app-assistant/references/local-project-analysis.md +46 -0
  49. package/rainbond-app-assistant/references/output-contract.md +21 -64
  50. package/rainbond-app-assistant/references/product-object-model.md +6 -26
  51. package/rainbond-app-assistant/references/workflow-rules.md +4 -13
  52. package/rainbond-app-assistant/schemas/generated/handoff-context.schema.yaml +64 -0
  53. package/rainbond-app-assistant/scripts/validate_cross_skill_routing.py +50 -58
  54. package/rainbond-app-assistant/scripts/validate_progressive_loading.py +25 -9
  55. package/rainbond-app-version-assistant/SKILL.md +43 -579
  56. package/rainbond-app-version-assistant/references/generated/runtime-gate.md +174 -0
  57. package/rainbond-app-version-assistant/references/generated/runtime-routing.md +15 -0
  58. package/rainbond-app-version-assistant/references/generated/user-result.md +16 -0
  59. package/rainbond-app-version-assistant/references/output-contract.md +156 -0
  60. package/rainbond-app-version-assistant/references/version-operations.md +50 -0
  61. package/rainbond-app-version-assistant/references/version-workflow.md +168 -0
  62. package/rainbond-delivery-verifier/SKILL.md +45 -541
  63. package/rainbond-delivery-verifier/evals/01-delivered-verified.expected.yaml +24 -0
  64. package/rainbond-delivery-verifier/evals/01-delivered-verified.response.md +21 -1
  65. package/rainbond-delivery-verifier/evals/02-delivered-manual-validation.response.md +1 -0
  66. package/rainbond-delivery-verifier/evals/03-partially-delivered-building.response.md +1 -0
  67. package/rainbond-delivery-verifier/evals/04-blocked-capacity.response.md +1 -0
  68. package/rainbond-delivery-verifier/evals/05-blocked-no-usable-url.response.md +1 -0
  69. package/rainbond-delivery-verifier/evals/06-blocked-runtime-unhealthy.response.md +19 -0
  70. package/rainbond-delivery-verifier/references/delivery-workflow.md +89 -0
  71. package/rainbond-delivery-verifier/references/generated/runtime-gate.md +242 -0
  72. package/rainbond-delivery-verifier/references/generated/runtime-routing.md +15 -0
  73. package/rainbond-delivery-verifier/references/generated/user-result.md +16 -0
  74. package/rainbond-delivery-verifier/references/output-contract.md +154 -0
  75. package/rainbond-delivery-verifier/references/verification-rules.md +103 -0
  76. package/rainbond-delivery-verifier/schemas/delivery-probe-input.schema.yaml +20 -0
  77. package/rainbond-delivery-verifier/schemas/delivery-probe-policy.schema.yaml +40 -0
  78. package/rainbond-delivery-verifier/schemas/delivery-probe-result.schema.yaml +82 -0
  79. package/rainbond-delivery-verifier/schemas/delivery-verification-result.schema.yaml +65 -0
  80. package/rainbond-delivery-verifier/schemas/generated/handoff-context.schema.yaml +64 -0
  81. package/rainbond-delivery-verifier/scripts/validate_delivery_verifier_output.py +27 -0
  82. package/rainbond-env-sync/SKILL.md +42 -636
  83. package/rainbond-env-sync/evals/db-name-source-cases.yaml +25 -0
  84. package/rainbond-env-sync/references/generated/runtime-gate.md +175 -0
  85. package/rainbond-env-sync/references/generated/runtime-routing.md +15 -0
  86. package/rainbond-env-sync/references/generated/user-result.md +16 -0
  87. package/rainbond-env-sync/references/output-contract.md +212 -0
  88. package/rainbond-env-sync/references/sync-policy.md +148 -0
  89. package/rainbond-env-sync/references/sync-workflow.md +94 -0
  90. package/rainbond-fullstack-bootstrap/SKILL.md +55 -461
  91. package/rainbond-fullstack-bootstrap/evals/07-provider-connection-contract.expected.yaml +9 -0
  92. package/rainbond-fullstack-bootstrap/evals/07-provider-connection-contract.response.md +1 -1
  93. package/rainbond-fullstack-bootstrap/modules/30-creation-rules.md +4 -232
  94. package/rainbond-fullstack-bootstrap/modules/35-runtime-topology-rules.md +194 -0
  95. package/rainbond-fullstack-bootstrap/modules/40-source-rules.md +94 -0
  96. package/rainbond-fullstack-bootstrap/modules/42-source-topology.md +70 -0
  97. package/rainbond-fullstack-bootstrap/modules/44-source-build-rules.md +55 -0
  98. package/rainbond-fullstack-bootstrap/modules/45-package-rules.md +83 -0
  99. package/rainbond-fullstack-bootstrap/modules/50-workflow.md +135 -0
  100. package/rainbond-fullstack-bootstrap/modules/55-convergence-rules.md +93 -0
  101. package/rainbond-fullstack-bootstrap/modules/60-verification-and-handoffs.md +1 -1
  102. package/rainbond-fullstack-bootstrap/modules/70-output-contract.md +2 -0
  103. package/rainbond-fullstack-bootstrap/references/generated/runtime-gate.md +262 -0
  104. package/rainbond-fullstack-bootstrap/references/generated/runtime-routing.md +24 -0
  105. package/rainbond-fullstack-bootstrap/references/generated/user-result.md +16 -0
  106. package/rainbond-fullstack-bootstrap/references/manifest-v1-reference.md +1 -1
  107. package/rainbond-fullstack-bootstrap/references/sql-init-recipe.md +1 -1
  108. package/rainbond-fullstack-bootstrap/schemas/generated/handoff-context.schema.yaml +64 -0
  109. package/rainbond-fullstack-bootstrap/scripts/validate_bootstrap_output.py +10 -0
  110. package/rainbond-fullstack-troubleshooter/SKILL.md +47 -1049
  111. package/rainbond-fullstack-troubleshooter/evals/10-config-file-configmap-missing.expected.yaml +6 -0
  112. package/rainbond-fullstack-troubleshooter/evals/10-config-file-configmap-missing.response.md +8 -7
  113. package/rainbond-fullstack-troubleshooter/references/diagnosis-context.md +211 -0
  114. package/rainbond-fullstack-troubleshooter/references/diagnosis-workflow.md +118 -0
  115. package/rainbond-fullstack-troubleshooter/references/generated/runtime-gate.md +227 -0
  116. package/rainbond-fullstack-troubleshooter/references/generated/runtime-routing.md +15 -0
  117. package/rainbond-fullstack-troubleshooter/references/generated/troubleshoot-contract.md +127 -0
  118. package/rainbond-fullstack-troubleshooter/references/generated/user-result.md +16 -0
  119. package/rainbond-fullstack-troubleshooter/references/output-contract.md +3 -67
  120. package/rainbond-fullstack-troubleshooter/references/root-cause-rules.md +3 -2
  121. package/rainbond-fullstack-troubleshooter/schemas/generated/handoff-context.schema.yaml +64 -0
  122. package/rainbond-fullstack-troubleshooter/scripts/generate_troubleshoot_contract.mjs +100 -0
  123. package/rainbond-fullstack-troubleshooter/scripts/validate_troubleshoot_output.py +0 -17
  124. package/rainbond-opensource-app-deploy/SKILL.md +6 -20
  125. package/rainbond-opensource-app-deploy/references/deployment-workflow.md +2 -2
  126. package/rainbond-opensource-app-deploy/references/generated/runtime-gate.md +188 -0
  127. package/rainbond-opensource-app-deploy/references/generated/runtime-routing.md +24 -0
  128. package/rainbond-opensource-app-deploy/references/generated/user-result.md +16 -0
  129. package/rainbond-platform-installer/SKILL.md +2 -2
  130. package/rainbond-platform-installer/scripts/auto-update.js +1 -0
  131. package/rainbond-platform-installer/scripts/host-targets.js +1 -1
  132. package/rainbond-platform-installer/scripts/installed-version.js +1 -1
  133. package/rainbond-platform-installer/scripts/single-runtime.js +9 -1
  134. package/rainbond-platform-installer/scripts/user-message.js +7 -9
  135. package/rainbond-platform-installer/scripts/windows-onboarding.js +12 -1
  136. package/rainbond-platform-plugin-manager/SKILL.md +4 -4
  137. package/rainbond-platform-plugin-manager/references/generated/runtime-gate.md +188 -0
  138. package/rainbond-platform-plugin-manager/references/generated/runtime-routing.md +15 -0
  139. package/rainbond-platform-plugin-manager/references/generated/user-result.md +16 -0
  140. package/rainbond-platform-plugin-manager/references/output-contract.md +1 -17
  141. package/rainbond-platform-plugin-manager/scripts/validate_progressive_loading.py +8 -2
  142. package/rainbond-platform-query/SKILL.md +49 -178
  143. package/rainbond-platform-query/references/generated/runtime-gate.md +109 -0
  144. package/rainbond-platform-query/references/generated/runtime-routing.md +15 -0
  145. package/rainbond-project-init/SKILL.md +49 -1163
  146. package/rainbond-project-init/references/generated/runtime-gate.md +201 -0
  147. package/rainbond-project-init/references/generated/runtime-routing.md +24 -0
  148. package/rainbond-project-init/references/generated/user-result.md +16 -0
  149. package/rainbond-project-init/references/manifest-inference.md +274 -0
  150. package/rainbond-project-init/references/manifest-rules.md +2 -271
  151. package/rainbond-project-init/references/operational-reference.md +1 -1
  152. package/rainbond-project-init/references/output-contract.md +6 -23
  153. package/rainbond-project-init/references/workflow-and-verification.md +8 -2
  154. package/rainbond-project-init/schemas/generated/handoff-context.schema.yaml +64 -0
  155. package/rainbond-project-init/schemas/project-init-result.schema.yaml +122 -0
  156. package/rainbond-template-installer/SKILL.md +43 -563
  157. package/rainbond-template-installer/references/generated/runtime-gate.md +201 -0
  158. package/rainbond-template-installer/references/generated/runtime-routing.md +24 -0
  159. package/rainbond-template-installer/references/generated/user-result.md +16 -0
  160. package/rainbond-template-installer/references/output-contract.md +168 -0
  161. package/rainbond-template-installer/references/template-resolution.md +55 -0
  162. package/rainbond-template-installer/references/template-workflow.md +129 -0
  163. package/rainbond-template-installer/schemas/generated/handoff-context.schema.yaml +64 -0
  164. package/scripts/install-local-cli.mjs +2 -0
  165. package/scripts/validate_customer_output.py +21 -14
  166. package/rainbond-ai-assistant/references/runtime-gate.md +0 -61
  167. package/rainbond-app-assistant/references/runtime-gate.md +0 -178
  168. package/rainbond-fullstack-bootstrap/modules/40-source-and-package-rules.md +0 -302
  169. package/rainbond-fullstack-bootstrap/modules/50-workflow-and-convergence.md +0 -226
  170. package/rainbond-opensource-app-deploy/references/runtime-gate.md +0 -158
  171. package/rainbond-platform-plugin-manager/references/runtime-gate.md +0 -62
package/README.md CHANGED
@@ -1,49 +1,165 @@
1
- # Rainskills
1
+ # RainSkills
2
2
 
3
- > 让你的 AI Agent 把应用真正跑起来。
3
+ > 让你的 AI Agent 把当前项目部署上线,并验证页面和 API。
4
4
 
5
- Rainskills 是一组开源 Agent Skills,让 Codex、Claude Code、Pi Agent、
6
- DeepSeek Harness、WorkBuddy 和 Hermes Agent 能够完成应用识别、部署、
7
- 排错、交付验证、版本管理和回滚。
5
+ RainSkills 是一组开源 Agent Skills,让 Codex、Claude Code、Pi Agent、
6
+ DeepSeek Harness、WorkBuddy 和 Hermes Agent 能够继续完成应用部署、
7
+ 故障排查、交付验证、版本管理和回滚。
8
8
 
9
- 安装后,你只需要告诉 Agent:
9
+ 它从 Agent 已经理解的当前项目出发,把前端、后端、数据库、缓存、
10
+ 端口和存储组织成 Rainbond 应用。应用可以运行在 Rainbond Cloud、
11
+ 你自己的服务器或 Kubernetes 中。
10
12
 
11
- > 帮我把当前项目部署上线,并验证页面和 API。
13
+ **AI 负责生成,RainSkills 负责交付,Rainbond 负责持续运行。**
12
14
 
13
- Rainskills 会分析项目结构、组件和依赖,连接 Rainbond Cloud、已有 Rainbond,
14
- 或者在本机和服务器上安装 Rainbond,然后继续完成部署与验证。
15
+ [产品介绍](https://www.rainbond.com/rainskills) ·
16
+ [安装与使用](https://www.rainbond.com/docs/ai/rainskills) ·
17
+ [视频教程](https://www.rainbond.com/videos/rainskills-ai-deploy) ·
18
+ [Rainbond Cloud](https://run.rainbond.com)
15
19
 
16
- **AI 负责生成,Rainskills 负责部署,Rainbond 负责持续运行。**
20
+ ## 为什么使用 RainSkills
17
21
 
18
- [快速安装](https://www.rainbond.com) ·
19
- [使用文档](https://www.rainbond.com/docs/ai/rainskills) · [Rainbond Cloud](https://run.rainbond.com)
22
+ AI Agent 已经理解了代码、依赖和项目结构。进入部署阶段后,如果改用临时脚本或直接 SSH,
23
+ 这些上下文通常需要重新整理,部署结果也很难沉淀为可以持续维护的应用。
20
24
 
21
- ## 快速安装
25
+ | 对比维度 | Agent 直接 SSH / 临时脚本 | RainSkills + Rainbond |
26
+ | --- | --- | --- |
27
+ | 工作起点 | 重新整理服务器、端口、依赖和命令 | 直接复用当前项目上下文 |
28
+ | 应用结构 | 人工拼装进程、数据库、网络和存储 | 用应用拓扑管理组件与依赖 |
29
+ | 失败排查 | 手工寻找构建、容器和网关日志 | Agent 读取构建日志、运行日志、Pod 和平台事件 |
30
+ | 完成标准 | 命令成功或进程已经启动 | 页面和 API 完成实际访问验证 |
31
+ | 权限边界 | Agent 通常直接持有 SSH 或生产凭据 | 浏览器授权、受控工具调用和高风险操作确认 |
32
+ | 后续维护 | 继续维护脚本和服务器差异 | 在同一应用上升级、快照、发布和回滚 |
22
33
 
23
- 从 Skill 市场安装:
34
+ RainSkills 更适合多组件应用、自托管环境、私有化交付,以及需要持续排障和版本管理的项目。
35
+
36
+ ## RainSkills 如何工作
37
+
38
+ ```text
39
+ 当前项目或部署目标
40
+ → Codex、Claude Code 等 AI Agent
41
+ → RainSkills 选择对应的部署、排障或验证 Skill
42
+ → 首次需要时选择并连接一个 Rainbond 运行环境
43
+ → Rainbond 构建并运行应用
44
+ → RainSkills 验证页面、API 和最终访问地址
45
+ ```
46
+
47
+ RainSkills 安装与运行环境连接是两个阶段。安装完成后可以直接提出部署任务;
48
+ 第一次需要 Rainbond 时,Agent 再引导你选择 Cloud、已有私有环境、本机或服务器。
49
+
50
+ ![RainSkills 在 AI Agent 中完成应用部署并返回访问地址](https://grstatic.tos-cn-beijing.volces.com/wechat/rainskills/rainskills-deploy.png)
51
+
52
+ ## 快速开始
53
+
54
+ 使用 Skill 市场安装:
24
55
 
25
56
  ```bash
26
57
  npx skills add goodrain/rainskills
27
58
  ```
28
59
 
29
- 或者直接运行:
60
+ 或者直接运行官方安装器:
30
61
 
31
62
  ```bash
32
63
  npx --yes rainskills
33
64
  ```
34
65
 
35
- 安装后,在项目目录告诉 Agent:
66
+ DeepSeek Harness 用户也可以把 RainSkills bootstrap 作为 profile bundle 安装:
67
+
68
+ ```bash
69
+ dsh plugin --profile web add rainskills
70
+ ```
71
+
72
+ 安装后调用 `/rainskills`,或直接告诉 Agent“安装 RainSkills”。该 bundle 只注册
73
+ bootstrap Skill;完整的 `rainbond-*` Skills 仍由官方安装器统一安装和更新。
74
+
75
+ `npx` 安装方式需要 Node.js 18 或更高版本。建议使用仍在维护的 Node.js 22 或 24。
76
+
77
+ 安装完成后,在项目目录告诉 Agent:
78
+
79
+ ```text
80
+ 帮我部署当前项目,并验证页面和 API。
81
+ ```
82
+
83
+ 如果还没有可用的 Rainbond,Agent 会在第一次需要运行环境时让你选择 Cloud、
84
+ 已有私有环境、本机或服务器,不会在安装 Skill 时替你决定目标环境。
85
+
86
+ ## 可以直接告诉 Agent 什么
87
+
88
+ - `帮我部署当前项目`
89
+ - `帮我部署这个 Git 仓库`
90
+ - `帮我通过镜像或安装包部署应用`
91
+ - `帮我安装一个应用模板`
92
+ - `帮我部署 Dify、n8n 或 Harbor`
93
+ - `帮我检查 backend 为什么构建失败`
94
+ - `确认当前应用是否已经交付成功,并给我访问地址`
95
+ - `给当前应用创建快照`
96
+ - `把当前应用回滚到上一个快照`
97
+
98
+ ## 支持范围
99
+
100
+ ### AI Agent
101
+
102
+ - Codex
103
+ - Claude Code
104
+ - Pi Agent
105
+ - DeepSeek Harness
106
+ - WorkBuddy
107
+ - Hermes Agent
108
+
109
+ ### 项目来源
110
+
111
+ - 当前本地项目
112
+ - 普通 Git 仓库
113
+ - 本地软件包
114
+ - 私有容器镜像或单镜像组件
115
+ - Compose、Helm 或多镜像描述
116
+ - Dify、n8n、Harbor 等第三方开源应用
117
+ - Rainbond 应用模板
118
+
119
+ Compose、Helm 和第三方应用中的特权容器、Operator、CRD、Job、主机网络或主机目录等语义,
120
+ 需要根据当前 Rainbond 能力单独判断,不承诺原样转换所有 Kubernetes 配置。
121
+
122
+ ### 运行环境
123
+
124
+ - Rainbond Cloud
125
+ - 已经运行的私有 Rainbond
126
+ - 本机 Rainbond
127
+ - 独立服务器上的 Rainbond
128
+ - 已有 Kubernetes 集群中的 Rainbond
129
+
130
+ ## 安全和权限边界
131
+
132
+ - 通过浏览器完成 Rainbond 授权,不要求在聊天中粘贴 JWT、密码或私钥。
133
+ - 创建、更新和删除资源前核对目标企业、团队、应用和组件。
134
+ - 删除资源、平台安装、数据变更和其他高风险操作需要明确确认。
135
+ - Rainbond 授权凭据保存在受保护的本地状态中,不写入项目文件。
136
+ - 部署完成后继续验证组件、页面和 API,不把命令成功当作交付完成。
137
+
138
+ 用户明确选择在独立服务器安装 Rainbond 时,受控安装流程可能使用 SSH。
139
+ 应用部署和后续管理仍通过 Rainbond 应用模型完成,而不是依赖散落的服务器命令。
140
+
141
+ ## 适合哪些场景
142
+
143
+ - AI Agent 已经写完或修改了项目,需要继续部署。
144
+ - 项目包含前端、后端、数据库、缓存或异步任务。
145
+ - 应用需要运行在自己的服务器、私有云或 Kubernetes 中。
146
+ - 部署完成后仍需日志、升级、快照、发布和回滚。
147
+ - 团队需要复用私有化或客户现场的交付流程。
148
+
149
+ ## 哪些场景可能不需要 RainSkills
36
150
 
37
- > 帮我部署当前项目,并验证页面和 API。
151
+ - 只发布一个接受完全托管的纯静态页面。
152
+ - 团队已经有成熟的内部开发平台和 GitOps 流程。
153
+ - 只是一次性的本地实验,不需要持续运行、排障或回滚。
38
154
 
39
- ## Rainskills 可以做什么
155
+ ## 文档与支持
40
156
 
41
- - 识别本地项目、Git 仓库、镜像和开源软件
42
- - 分析前端、后端、数据库及组件依赖
43
- - 自动完成应用构建和部署
44
- - 根据事件和日志排查部署故障
45
- - 验证组件状态、访问地址、页面和 API
46
- - 管理应用版本、升级、快照和回滚
157
+ - [RainSkills 产品介绍](https://www.rainbond.com/rainskills)
158
+ - [安装与使用文档](https://www.rainbond.com/docs/ai/rainskills)
159
+ - [安装与连接视频教程](https://www.rainbond.com/videos/rainskills-ai-deploy)
160
+ - [市场上架 Runbook](https://github.com/goodrain/rainskills/blob/main/docs/marketplace-publishing.md)
161
+ - [Rainbond 安装文档](https://www.rainbond.com/docs/installation)
162
+ - [提交问题](https://github.com/goodrain/rainskills/issues)
47
163
 
48
164
  ## License
49
165
 
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 connect, reconnect, inspect, or replace the single Rainskills runtime.
3
+ description: Install, update, repair, connect, reconnect, inspect, or replace the single Rainskills runtime. Use for Rainskills setup and onboarding, not application deployment.
4
4
  ---
5
5
 
6
6
  # Rainskills
@@ -9,7 +9,7 @@ This is the single marketplace entry for the complete Rainskills product. The in
9
9
 
10
10
  ## Initialize
11
11
 
12
- 1. Detect the current host client and map it to exactly one supported installer target: Codex=`codex`, Claude Code=`claude`, Pi Agent=`pi`, DeepSeek Harness=`dsh`, WorkBuddy=`workbuddy`, or Hermes Agent=`hermes`. All six use the same CLI profile, installed Skills, and protected local Rainskills CLI; none requires a separate client MCP adapter. DeepSeek Harness installs to `$DSH_HOME/skills` (default `~/.dsh/skills`). WorkBuddy installs to `$WORKBUDDY_CONFIG_DIR/skills` (default `~/.workbuddy-ai/skills`). Hermes Agent installs to `$HERMES_HOME/skills` (default `~/.hermes/skills`); `AI_AGENT=hermes-agent` or `HERMES_AGENT=true` is a reliable fallback when the host must be detected from a terminal subprocess. The macOS, Linux, and WSL installer intentionally does not support OpenClaw; report that limitation instead of invoking an unsupported target. Do not ask the user which supported client they are currently using. If the host cannot be determined reliably, omit the target and let the installer ask.
12
+ 1. Detect the current host client and map it to exactly one supported installer target: Codex=`codex`, Claude Code=`claude`, Pi Agent=`pi`, DeepSeek Harness=`dsh`, WorkBuddy=`workbuddy`, or Hermes Agent=`hermes`. All six use the same CLI profile, installed Skills, and protected local Rainskills CLI; none requires a separate client MCP adapter. DeepSeek Harness installs to `$DSH_HOME/skills` (default `~/.dsh/skills`). WorkBuddy installs to `$WORKBUDDY_CONFIG_DIR/skills` (default `~/.workbuddy/skills`). Hermes Agent installs to `$HERMES_HOME/skills` (default `~/.hermes/skills`); `AI_AGENT=hermes-agent` or `HERMES_AGENT=true` is a reliable fallback when the host must be detected from a terminal subprocess. The macOS, Linux, and WSL installer intentionally does not support OpenClaw; report that limitation instead of invoking an unsupported target. Do not ask the user which supported client they are currently using. If the host cannot be determined reliably, omit the target and let the installer ask.
13
13
  2. Resolve the directory containing this `SKILL.md`. On native Windows, if the adjacent `bin/rainskills.js` exists, run it by absolute path with the detected target in an attached interactive terminal: `node <skill-directory>/bin/rainskills.js <target>`. On macOS, Linux, or WSL, if the adjacent `install.sh` exists, run `bash <skill-directory>/install.sh <target>` the same way. Do not replace the installer with manual file copies.
14
14
  3. Keep stdin, stdout, and stderr attached. When `RAINSKILLS_USER_INPUT_REQUIRED` appears, pause for that installer choice. If the installer emits `rainskills.next-action.v1`, execute only its fixed `argv` through the same launcher; never evaluate output as a shell command. If the adjacent `bin/rainskills.js` exists, use it for fixed next actions; otherwise use the same versioned npm package fallback described below.
15
15
  4. Stay attached until every independent Skill is installed. Do not select, connect, or configure an application runtime during installation. In the user-facing response, output only the fixed completion message below.
@@ -54,7 +54,7 @@ Hermes Agent 中必须使用 `terminal` 以 `background=true` 启动授权命令
54
54
 
55
55
  DeepSeek Harness 和 WorkBuddy 也必须保持原命令附着直到退出;如果宿主把长命令转成持久终端或后台任务,只能轮询该原始命令句柄,不得另起 `runtime status` 推测授权结果。WorkBuddy 安装后若当前任务的 Skill 清单尚未刷新,在下一条业务动作前先刷新 Skill 列表或新建任务,不得退回内置 Sites 代替 Rainbond 部署。
56
56
 
57
- Rainskills 只保存一个全局运行环境,不维护环境列表、名称、默认值或环境 ID。使用固定 launcher `node <home>/.rainbond/lib/rainskills/bin/rainskills.js`(运行包版本 `rainskills@0.1.42`):
57
+ Rainskills 只保存一个全局运行环境,不维护环境列表、名称、默认值或环境 ID。使用固定 launcher `node <home>/.rainbond/lib/rainskills/bin/rainskills.js`(运行包版本 `rainskills@0.1.44`):
58
58
 
59
59
  - 状态:执行 `runtime status --json`。
60
60
  - 首次连接:执行 `runtime connect <target> --saas` 或 `runtime connect <target> --rainbond-url <Console origin>`。
@@ -0,0 +1,306 @@
1
+ "use strict";
2
+
3
+ const dns = require("node:dns").promises;
4
+ const http = require("node:http");
5
+ const https = require("node:https");
6
+ const net = require("node:net");
7
+
8
+ const MAX_REDIRECTS = 3;
9
+ const CONNECT_TIMEOUT_MS = 5_000;
10
+ const TOTAL_TIMEOUT_MS = 15_000;
11
+ const MAX_BODY_BYTES = 256 * 1024;
12
+ const MAX_URL_LENGTH = 4_096;
13
+ const POLICY_VERSION = "rainskills.delivery-probe-policy.v1";
14
+
15
+ function isPublicIpv4(address) {
16
+ const parts = address.split(".").map(Number);
17
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
18
+ return false;
19
+ }
20
+ const [a, b] = parts;
21
+ if (a === 0 || a === 10 || a === 127 || a >= 224) return false;
22
+ if (a === 100 && b >= 64 && b <= 127) return false;
23
+ if (a === 169 && b === 254) return false;
24
+ if (a === 172 && b >= 16 && b <= 31) return false;
25
+ if (a === 192 && (b === 0 || b === 168)) return false;
26
+ if (a === 192 && b === 0 && parts[2] === 2) return false;
27
+ if (a === 198 && (b === 18 || b === 19 || b === 51)) return false;
28
+ if (a === 203 && b === 0 && parts[2] === 113) return false;
29
+ return true;
30
+ }
31
+
32
+ function ipv6Parts(address) {
33
+ let normalized = address.toLowerCase().split("%", 1)[0];
34
+ const dotted = normalized.match(/(\d+\.\d+\.\d+\.\d+)$/);
35
+ if (dotted) {
36
+ if (!net.isIPv4(dotted[1])) return null;
37
+ const octets = dotted[1].split(".").map(Number);
38
+ normalized = `${normalized.slice(0, -dotted[1].length)}${((octets[0] << 8) | octets[1]).toString(16)}:${((octets[2] << 8) | octets[3]).toString(16)}`;
39
+ }
40
+ const halves = normalized.split("::");
41
+ if (halves.length > 2) return null;
42
+ const left = halves[0] ? halves[0].split(":") : [];
43
+ const right = halves.length === 2 && halves[1] ? halves[1].split(":") : [];
44
+ const missing = 8 - left.length - right.length;
45
+ if ((halves.length === 1 && missing !== 0) || missing < 0) return null;
46
+ const tokens = [...left, ...Array(missing).fill("0"), ...right];
47
+ if (tokens.length !== 8 || tokens.some((token) => !/^[0-9a-f]{1,4}$/.test(token))) return null;
48
+ return tokens.map((token) => Number.parseInt(token, 16));
49
+ }
50
+
51
+ function embeddedIpv4(parts, offset) {
52
+ return [
53
+ parts[offset] >> 8,
54
+ parts[offset] & 0xff,
55
+ parts[offset + 1] >> 8,
56
+ parts[offset + 1] & 0xff,
57
+ ].join(".");
58
+ }
59
+
60
+ function isPublicIpv6(address) {
61
+ const parts = ipv6Parts(address);
62
+ if (!parts) return false;
63
+ if (parts.every((part) => part === 0)) return false;
64
+ if (parts.slice(0, 7).every((part) => part === 0) && parts[7] === 1) return false;
65
+ if ((parts[0] & 0xfe00) === 0xfc00) return false;
66
+ if ((parts[0] & 0xffc0) === 0xfe80) return false;
67
+ if ((parts[0] & 0xff00) === 0xff00) return false;
68
+ if (parts[0] === 0x0100 && parts.slice(1, 4).every((part) => part === 0)) return false;
69
+ if (parts[0] === 0x2001 && parts[1] === 0x0000) return false;
70
+ if (parts[0] === 0x2001 && (parts[1] & 0xfff0) === 0x0010) return false;
71
+ if (parts[0] === 0x2001 && (parts[1] & 0xfff0) === 0x0020) return false;
72
+ if (parts[0] === 0x2001 && parts[1] === 0x0002) return false;
73
+ if (parts[0] === 0x2001 && parts[1] === 0x0db8) return false;
74
+ if ((parts[0] & 0xfff0) === 0x3ff0) return false;
75
+ if (parts.slice(0, 5).every((part) => part === 0) && parts[5] === 0xffff) {
76
+ return isPublicIpv4(embeddedIpv4(parts, 6));
77
+ }
78
+ if (parts.slice(0, 6).every((part) => part === 0)) {
79
+ return isPublicIpv4(embeddedIpv4(parts, 6));
80
+ }
81
+ if (parts[0] === 0x0064 && parts[1] === 0xff9b && parts.slice(2, 6).every((part) => part === 0)) {
82
+ return isPublicIpv4(embeddedIpv4(parts, 6));
83
+ }
84
+ if (parts[0] === 0x2002) return isPublicIpv4(embeddedIpv4(parts, 1));
85
+ return true;
86
+ }
87
+
88
+ function isPublicAddress(address) {
89
+ const family = net.isIP(address);
90
+ if (family === 4) return isPublicIpv4(address);
91
+ if (family === 6) return isPublicIpv6(address);
92
+ return false;
93
+ }
94
+
95
+ function normalizedHostname(url) {
96
+ return url.hostname.replace(/^\[|\]$/g, "");
97
+ }
98
+
99
+ async function defaultResolveHost(hostname) {
100
+ const records = await dns.lookup(hostname, { all: true, verbatim: true });
101
+ return records.map((record) => record.address);
102
+ }
103
+
104
+ function defaultRequestUrl(url, {
105
+ method,
106
+ address,
107
+ headers,
108
+ connectTimeoutMs = CONNECT_TIMEOUT_MS,
109
+ totalTimeoutMs = TOTAL_TIMEOUT_MS,
110
+ maxBodyBytes = MAX_BODY_BYTES,
111
+ }) {
112
+ const client = url.protocol === "https:" ? https : http;
113
+ return new Promise((resolve) => {
114
+ let settled = false;
115
+ let connectTimer;
116
+ let totalTimer;
117
+ const finish = (result, request) => {
118
+ if (settled) return;
119
+ settled = true;
120
+ clearTimeout(connectTimer);
121
+ clearTimeout(totalTimer);
122
+ if (request && !request.destroyed) request.destroy();
123
+ resolve(result);
124
+ };
125
+ const request = client.request(url, {
126
+ method,
127
+ headers,
128
+ redirect: "manual",
129
+ lookup: (_hostname, options, callback) => {
130
+ const family = net.isIP(address);
131
+ if (options && options.all) {
132
+ callback(null, [{ address, family }]);
133
+ return;
134
+ }
135
+ callback(null, address, family);
136
+ },
137
+ }, (response) => {
138
+ const statusCode = response.statusCode || 0;
139
+ const location = response.headers.location;
140
+ const contentType = String(response.headers["content-type"] || "");
141
+ const contentLength = Number(response.headers["content-length"] || 0);
142
+ if (Number.isFinite(contentLength) && contentLength > maxBodyBytes) {
143
+ response.resume();
144
+ finish({ error: "body_too_large" }, request);
145
+ return;
146
+ }
147
+ if (method === "HEAD" || (statusCode >= 300 && statusCode < 400)) {
148
+ response.resume();
149
+ finish({ statusCode, location, contentType, bodyBytes: 0 }, request);
150
+ return;
151
+ }
152
+ let bodyBytes = 0;
153
+ response.on("data", (chunk) => {
154
+ bodyBytes += chunk.length;
155
+ if (bodyBytes > maxBodyBytes) finish({ error: "body_too_large" }, request);
156
+ });
157
+ response.on("end", () => finish({ statusCode, location, contentType, bodyBytes }, request));
158
+ response.on("error", () => finish({ error: "response_error" }, request));
159
+ });
160
+ request.on("socket", (socket) => {
161
+ if (!socket.connecting) return;
162
+ connectTimer = setTimeout(() => finish({ error: "timeout" }, request), connectTimeoutMs);
163
+ socket.once(url.protocol === "https:" ? "secureConnect" : "connect", () => clearTimeout(connectTimer));
164
+ });
165
+ request.on("error", (error) => finish({ error: error.code === "ETIMEDOUT" ? "timeout" : "request_error" }, request));
166
+ totalTimer = setTimeout(() => finish({ error: "timeout" }, request), totalTimeoutMs);
167
+ request.end();
168
+ });
169
+ }
170
+
171
+ function parseTarget(value) {
172
+ if (typeof value !== "string" || !value || value.length > MAX_URL_LENGTH) return null;
173
+ let url;
174
+ try {
175
+ url = new URL(value);
176
+ } catch (_error) {
177
+ return null;
178
+ }
179
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) return null;
180
+ return url;
181
+ }
182
+
183
+ function createDeliveryProbe({
184
+ resolveHost = defaultResolveHost,
185
+ requestUrl = defaultRequestUrl,
186
+ } = {}) {
187
+ return async function probe(input) {
188
+ const allowedInputKeys = new Set(["policy_version", "url", "method"]);
189
+ const validShape = input && typeof input === "object" && !Array.isArray(input)
190
+ && Object.keys(input).every((key) => allowedInputKeys.has(key));
191
+ const method = input?.method;
192
+ let current = parseTarget(input?.url);
193
+ const candidateUrl = current ? current.href : String(input?.url || "");
194
+ if (!validShape || !current || !["HEAD", "GET"].includes(method) || input?.policy_version !== POLICY_VERSION) {
195
+ return { policy_version: POLICY_VERSION, status: "rejected", reason: "unsafe_url_or_method", candidate_url: candidateUrl };
196
+ }
197
+ const redirects = [];
198
+ let prefetchedResolution = null;
199
+
200
+ const resolveSafeAddresses = async (url) => {
201
+ const hostname = normalizedHostname(url);
202
+ try {
203
+ const addresses = net.isIP(hostname) ? [hostname] : await resolveHost(hostname);
204
+ if (!Array.isArray(addresses) || !addresses.length || addresses.some((address) => !isPublicAddress(address))) {
205
+ return { error: "non_public_address" };
206
+ }
207
+ return { addresses };
208
+ } catch (_error) {
209
+ return { error: "dns_error" };
210
+ }
211
+ };
212
+
213
+ for (let redirectCount = 0; redirectCount <= MAX_REDIRECTS; redirectCount += 1) {
214
+ const resolved = prefetchedResolution || await resolveSafeAddresses(current);
215
+ prefetchedResolution = null;
216
+ if (resolved.error) {
217
+ return {
218
+ policy_version: POLICY_VERSION,
219
+ status: resolved.error === "dns_error" ? "failed" : "rejected",
220
+ reason: resolved.error,
221
+ candidate_url: candidateUrl,
222
+ };
223
+ }
224
+
225
+ const headers = Object.freeze({ Accept: "*/*", "User-Agent": "rainskills-delivery-probe/1" });
226
+ const response = await requestUrl(current, {
227
+ method,
228
+ address: resolved.addresses[0],
229
+ headers,
230
+ connectTimeoutMs: CONNECT_TIMEOUT_MS,
231
+ totalTimeoutMs: TOTAL_TIMEOUT_MS,
232
+ maxBodyBytes: MAX_BODY_BYTES,
233
+ });
234
+ if (response?.error) {
235
+ return { policy_version: POLICY_VERSION, status: "failed", reason: response.error, candidate_url: candidateUrl };
236
+ }
237
+
238
+ const statusCode = Number(response?.statusCode || 0);
239
+ if (statusCode >= 300 && statusCode < 400) {
240
+ if (!response.location) {
241
+ return { policy_version: POLICY_VERSION, status: "failed", reason: "redirect_without_location", candidate_url: candidateUrl };
242
+ }
243
+ if (redirectCount === MAX_REDIRECTS) {
244
+ return { policy_version: POLICY_VERSION, status: "failed", reason: "too_many_redirects", candidate_url: candidateUrl };
245
+ }
246
+ let next;
247
+ try {
248
+ next = parseTarget(new URL(response.location, current).href);
249
+ } catch (_error) {
250
+ next = null;
251
+ }
252
+ if (!next) return { policy_version: POLICY_VERSION, status: "rejected", reason: "unsafe_redirect", candidate_url: candidateUrl };
253
+ const nextResolved = await resolveSafeAddresses(next);
254
+ if (nextResolved.error) {
255
+ return { policy_version: POLICY_VERSION, status: "rejected", reason: "unsafe_redirect", candidate_url: candidateUrl };
256
+ }
257
+ if (current.protocol === "https:" && next.protocol !== "https:") {
258
+ return { policy_version: POLICY_VERSION, status: "rejected", reason: "redirect_downgrade", candidate_url: candidateUrl };
259
+ }
260
+ const sameEndpoint = next.host === current.host && next.protocol === current.protocol;
261
+ const httpUpgrade = current.protocol === "http:"
262
+ && next.protocol === "https:"
263
+ && normalizedHostname(next) === normalizedHostname(current)
264
+ && (current.port || "80") === "80"
265
+ && (next.port || "443") === "443";
266
+ if (!sameEndpoint && !httpUpgrade) {
267
+ return {
268
+ policy_version: POLICY_VERSION,
269
+ status: "manual_validation_required",
270
+ reason: "cross_host_redirect",
271
+ candidate_url: candidateUrl,
272
+ redirect_target: next.href,
273
+ };
274
+ }
275
+ redirects.push(next.href);
276
+ current = next;
277
+ prefetchedResolution = nextResolved;
278
+ continue;
279
+ }
280
+
281
+ return {
282
+ policy_version: POLICY_VERSION,
283
+ status: statusCode >= 200 && statusCode < 400 ? "verified" : "failed",
284
+ reason: statusCode >= 200 && statusCode < 400 ? null : "http_status",
285
+ candidate_url: candidateUrl,
286
+ final_url: current.href,
287
+ status_code: statusCode,
288
+ content_type: String(response.contentType || ""),
289
+ body_bytes: Number(response.bodyBytes || 0),
290
+ redirects,
291
+ };
292
+ }
293
+ return { policy_version: POLICY_VERSION, status: "failed", reason: "too_many_redirects", candidate_url: candidateUrl };
294
+ };
295
+ }
296
+
297
+ module.exports = {
298
+ CONNECT_TIMEOUT_MS,
299
+ MAX_BODY_BYTES,
300
+ MAX_REDIRECTS,
301
+ TOTAL_TIMEOUT_MS,
302
+ POLICY_VERSION,
303
+ createDeliveryProbe,
304
+ defaultRequestUrl,
305
+ isPublicAddress,
306
+ };
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+
3
+ const { createHash } = require("node:crypto");
4
+
5
+ const SCHEMA = "rainskills.handoff-context.v1";
6
+ const PROFILE = new Set(["cli", "embedded"]);
7
+ const SOURCE_KIND = new Set(["source", "image", "package", "template", null]);
8
+ const RUNTIME_STATE = new Set([
9
+ "topology_missing", "topology_building", "runtime_unhealthy", "runtime_healthy",
10
+ "capacity_blocked", "code_or_build_handoff_needed",
11
+ ]);
12
+ const NEXT_PHASE = new Set(["init", "bootstrap", "troubleshoot", "verify", "stop"]);
13
+ const CONTEXT_KEYS = Object.freeze([
14
+ "schema", "producer", "profile", "observed_at", "enterprise_id", "team_id",
15
+ "team_name", "region_name", "app_id", "app_name", "runtime_fingerprint",
16
+ "identity_fingerprint", "snapshot_revision", "source_kind", "runtime_state",
17
+ "next_phase", "invalidation_reason",
18
+ ]);
19
+ const CREATE_KEYS = new Set([
20
+ "producer", "profile", "observed_at", "enterprise_id", "team_id", "team_name",
21
+ "region_name", "app_id", "app_name", "endpoint", "runtime_revision",
22
+ "snapshot_revision", "source_kind", "runtime_state", "next_phase", "invalidation_reason",
23
+ ]);
24
+ const SENSITIVE_KEY = /(?:jwt|token|password|secret|credential|confirmation|raw.?log|tool.?output)/i;
25
+ const SHA256 = /^[a-f0-9]{64}$/;
26
+ const SKILL_ID = /^rainbond-[a-z0-9-]+$/;
27
+
28
+ function canonical(value) {
29
+ if (Array.isArray(value)) return value.map(canonical);
30
+ if (value && typeof value === "object") {
31
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
32
+ }
33
+ return value;
34
+ }
35
+
36
+ function fingerprint(value) {
37
+ return createHash("sha256").update(JSON.stringify(canonical(value))).digest("hex");
38
+ }
39
+
40
+ function assertString(value, label, nullable = false) {
41
+ if (nullable && value === null) return;
42
+ if (typeof value !== "string" || value.length === 0) throw new Error(`${label} must be a non-empty string`);
43
+ }
44
+
45
+ function runtimeIdentity(value) {
46
+ return {
47
+ profile: value.profile,
48
+ endpoint: value.endpoint,
49
+ runtime_revision: value.runtime_revision,
50
+ };
51
+ }
52
+
53
+ function workspaceIdentity(value) {
54
+ return {
55
+ enterprise_id: value.enterprise_id,
56
+ team_id: value.team_id,
57
+ region_name: value.region_name,
58
+ app_id: value.app_id,
59
+ };
60
+ }
61
+
62
+ function validateCreationInput(value) {
63
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("handoff input must be an object");
64
+ for (const key of Object.keys(value)) {
65
+ if (SENSITIVE_KEY.test(key)) throw new Error("sensitive handoff fields are forbidden");
66
+ if (!CREATE_KEYS.has(key)) throw new Error(`unknown handoff field: ${key}`);
67
+ }
68
+ if (!SKILL_ID.test(value.producer || "")) throw new Error("producer is invalid");
69
+ if (!PROFILE.has(value.profile)) throw new Error("profile is invalid");
70
+ if (!Number.isFinite(Date.parse(value.observed_at))) throw new Error("observed_at is invalid");
71
+ for (const key of ["enterprise_id", "team_id", "team_name", "region_name", "endpoint", "runtime_revision"]) assertString(value[key], key);
72
+ if (!(value.app_id === null || (Number.isInteger(value.app_id) && value.app_id > 0))) throw new Error("app_id is invalid");
73
+ assertString(value.app_name, "app_name", true);
74
+ assertString(value.snapshot_revision, "snapshot_revision", true);
75
+ if (!SOURCE_KIND.has(value.source_kind)) throw new Error("source_kind is invalid");
76
+ if (!RUNTIME_STATE.has(value.runtime_state)) throw new Error("runtime_state is invalid");
77
+ if (!NEXT_PHASE.has(value.next_phase)) throw new Error("next_phase is invalid");
78
+ assertString(value.invalidation_reason, "invalidation_reason", true);
79
+ }
80
+
81
+ function validateContextShape(context) {
82
+ if (!context || typeof context !== "object" || Array.isArray(context)) throw new Error("context must be an object");
83
+ const keys = Object.keys(context).sort();
84
+ if (JSON.stringify(keys) !== JSON.stringify([...CONTEXT_KEYS].sort())) throw new Error("context fields are invalid");
85
+ if (context.schema !== SCHEMA || !SKILL_ID.test(context.producer || "") || !PROFILE.has(context.profile)) throw new Error("context identity is invalid");
86
+ if (!Number.isFinite(Date.parse(context.observed_at))) throw new Error("context observed_at is invalid");
87
+ for (const key of ["enterprise_id", "team_id", "team_name", "region_name"]) assertString(context[key], key);
88
+ if (!(context.app_id === null || (Number.isInteger(context.app_id) && context.app_id > 0))) throw new Error("context app_id is invalid");
89
+ assertString(context.app_name, "app_name", true);
90
+ if (!SHA256.test(context.runtime_fingerprint) || !SHA256.test(context.identity_fingerprint)) throw new Error("context fingerprint is invalid");
91
+ assertString(context.snapshot_revision, "snapshot_revision", true);
92
+ if (!SOURCE_KIND.has(context.source_kind) || !RUNTIME_STATE.has(context.runtime_state) || !NEXT_PHASE.has(context.next_phase)) throw new Error("context state is invalid");
93
+ assertString(context.invalidation_reason, "invalidation_reason", true);
94
+ }
95
+
96
+ function createHandoffContext(value) {
97
+ validateCreationInput(value);
98
+ return {
99
+ schema: SCHEMA,
100
+ producer: value.producer,
101
+ profile: value.profile,
102
+ observed_at: new Date(value.observed_at).toISOString(),
103
+ enterprise_id: value.enterprise_id,
104
+ team_id: value.team_id,
105
+ team_name: value.team_name,
106
+ region_name: value.region_name,
107
+ app_id: value.app_id,
108
+ app_name: value.app_name,
109
+ runtime_fingerprint: fingerprint(runtimeIdentity(value)),
110
+ identity_fingerprint: fingerprint(workspaceIdentity(value)),
111
+ snapshot_revision: value.snapshot_revision,
112
+ source_kind: value.source_kind,
113
+ runtime_state: value.runtime_state,
114
+ next_phase: value.next_phase,
115
+ invalidation_reason: value.invalidation_reason,
116
+ };
117
+ }
118
+
119
+ function validateHandoffContext(context, { current, signal = null, now = Date.now(), maxAgeMs = 30_000 } = {}) {
120
+ try {
121
+ validateContextShape(context);
122
+ validateCreationInput({
123
+ producer: context.producer,
124
+ observed_at: context.observed_at,
125
+ team_name: context.team_name,
126
+ app_name: context.app_name,
127
+ snapshot_revision: context.snapshot_revision,
128
+ source_kind: context.source_kind,
129
+ runtime_state: context.runtime_state,
130
+ next_phase: context.next_phase,
131
+ invalidation_reason: context.invalidation_reason,
132
+ ...current,
133
+ });
134
+ } catch (_error) {
135
+ return { valid: false, reason: "invalid-context" };
136
+ }
137
+ const signalReasons = new Set(["runtime-reconnect", "unauthorized-401", "forbidden-403", "not-found", "revision-conflict"]);
138
+ if (signal && signalReasons.has(signal)) return { valid: false, reason: signal };
139
+ if (fingerprint(runtimeIdentity(current)) !== context.runtime_fingerprint) return { valid: false, reason: "runtime-identity-changed" };
140
+ if (fingerprint(workspaceIdentity(current)) !== context.identity_fingerprint) return { valid: false, reason: "workspace-identity-changed" };
141
+ if (current.source_kind !== context.source_kind) return { valid: false, reason: "source-strategy-changed" };
142
+ if (current.snapshot_revision !== context.snapshot_revision) return { valid: false, reason: "snapshot-revision-changed" };
143
+ if (now - Date.parse(context.observed_at) > maxAgeMs) return { valid: false, reason: "mutable-state-stale" };
144
+ if (context.invalidation_reason) return { valid: false, reason: context.invalidation_reason };
145
+ return { valid: true, reason: null };
146
+ }
147
+
148
+ module.exports = {
149
+ createHandoffContext,
150
+ validateHandoffContext,
151
+ };