huaweicloud-devkit 1.1.10 → 1.1.11

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
@@ -4,7 +4,7 @@
4
4
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
5
5
  [![CI](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml/badge.svg)](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
6
6
  [![npm version](https://img.shields.io/npm/v/huaweicloud-devkit)](https://www.npmjs.com/package/huaweicloud-devkit)
7
- [![Beta](https://img.shields.io/badge/beta-v1.1.10-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
7
+ [![Beta](https://img.shields.io/badge/beta-v1.1.11-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
8
8
 
9
9
  **[中文](README.zh-CN.md) | English**
10
10
 
package/README.zh-CN.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
5
5
  [![CI](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml/badge.svg)](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
6
6
  [![npm version](https://img.shields.io/npm/v/huaweicloud-devkit)](https://www.npmjs.com/package/huaweicloud-devkit)
7
- [![Beta](https://img.shields.io/badge/beta-v1.1.10-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
7
+ [![Beta](https://img.shields.io/badge/beta-v1.1.11-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
8
8
 
9
9
  **中文 | [English](README.md)**
10
10
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "huaweicloud-devkit",
3
3
  "mcpName": "io.github.huaweicloud/huaweicloud-devkit",
4
- "version": "1.1.10",
4
+ "version": "1.1.11",
5
5
  "kooCliVersion": "7.3.12",
6
6
  "description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
7
7
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  "mcpServers": "./.mcp.json",
18
18
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
19
19
  "skills": "./skills/",
20
- "version": "1.1.10",
20
+ "version": "1.1.11",
21
21
  "author": {
22
22
  "name": "HuaweiCloud Mate",
23
23
  "url": "https://github.com/huaweicloud"
@@ -20,7 +20,7 @@
20
20
  "mcpServers": "./.mcp.json",
21
21
  "description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
22
22
  "skills": "./skills/",
23
- "version": "1.1.10",
23
+ "version": "1.1.11",
24
24
  "author": {
25
25
  "name": "HuaweiCloud Mate",
26
26
  "url": "https://github.com/huaweicloud"
@@ -17,7 +17,7 @@
17
17
  "mcpServers": "./.mcp.json",
18
18
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
19
19
  "skills": "./skills/",
20
- "version": "1.1.10",
20
+ "version": "1.1.11",
21
21
  "author": {
22
22
  "name": "HuaweiCloud Mate",
23
23
  "url": "https://github.com/huaweicloud"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "huaweicloud-devkit",
3
- "version": "1.1.10",
3
+ "version": "1.1.11",
4
4
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
5
5
  "author": {
6
6
  "name": "HuaweiCloud Mate",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "huaweicloud-devkit",
3
- "version": "1.1.10",
3
+ "version": "1.1.11",
4
4
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
5
5
  "author": {
6
6
  "name": "HuaweiCloud Mate",
@@ -2,7 +2,7 @@
2
2
  "name": "huaweicloud-devkit",
3
3
  "id": "huaweicloud-devkit",
4
4
  "displayName": "HuaweiCloud DevKit",
5
- "version": "1.1.10",
5
+ "version": "1.1.11",
6
6
  "family": "bundle-plugin",
7
7
  "bundleFormat": "codex",
8
8
  "description": "Guide coding agents to use Huawei Cloud safely — KooCLI, APIs, SDKs, 28 MCP tools, skills, and safety guardrails.",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: huawei-obs
3
- description: 'Use when creating, configuring, or managing OBS buckets and objects on Huawei Cloud. Covers bucket creation, lifecycle policies, versioning, static website hosting, CORS, access control (IAM/bucket policy/ACL), cross-region replication, event notifications, and presigned URLs. Triggers on: OBS, bucket, object storage, lifecycle, versioning, static website, CORS, presigned, replication. NOT for: EVS block storage (use huawei-ecs), SFS file storage, CBR backup (use huawei-cbr).'
3
+ description: 'Use when creating, configuring, or managing OBS buckets and objects on Huawei Cloud. Covers bucket creation, lifecycle policies, versioning, static website hosting, CORS, access control (IAM/bucket policy/ACL), cross-region replication, event notifications, and presigned URLs. Triggers on: OBS, bucket, object storage, lifecycle, versioning, static website, CORS, presigned, replication, 浏览器直传, browser direct upload, 预签名上传, 直传 OBS. NOT for: EVS block storage (use huawei-ecs), SFS file storage, CBR backup (use huawei-cbr).'
4
4
  version: 1
5
5
  ---
6
6
 
@@ -62,21 +62,21 @@ hcloud OBS config -e=<endpoint> -i=<AK> -k=<SK> -t=token
62
62
 
63
63
  ## Common Workflows
64
64
 
65
- | Task | Command |
66
- | ---------------------------- | ------------------------------------------------------------------------- |
67
- | Create bucket | `hcloud OBS mb obs://<bucket> -location=<region>` |
68
- | List buckets/objects | `hcloud OBS ls [obs://<bucket>]` |
69
- | Upload file | `hcloud OBS cp <file> obs://<bucket>/<key>` |
70
- | Upload directory (recursive) | `hcloud OBS cp <dir>/ obs://<bucket>/ -r -f -flat` |
71
- | Download object | `hcloud OBS cp obs://<bucket>/<key> <local-path>` |
72
- | Set bucket ACL | `hcloud OBS chattri obs://<bucket> -acl=public-read` |
73
- | Set object ACL | `hcloud OBS chattri obs://<bucket>/<key> -acl=public-read` | Bucket ACL does NOT cascade — anonymous reads need both |
74
- | Set lifecycle | `hcloud OBS lifecycle obs://<bucket> -method=put -localfile=<json>` |
75
- | Set bucket policy | `hcloud OBS bucketpolicy obs://<bucket> -method=put -localfile=<json>` |
76
- | Set CORS | Not available via `hcloud OBS` — use API/SDK (`SetBucketCors`) or console |
77
- | Delete bucket | `hcloud OBS rm obs://<bucket> -r` (must be empty) |
78
- | Presigned URL | `hcloud OBS sign obs://<bucket>/<key> -e=<seconds>` |
79
- | Object metadata | `hcloud OBS stat obs://<bucket>/<key>` |
65
+ | Task | Command |
66
+ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
67
+ | Create bucket | `hcloud OBS mb obs://<bucket> -location=<region>` |
68
+ | List buckets/objects | `hcloud OBS ls [obs://<bucket>]` |
69
+ | Upload file | `hcloud OBS cp <file> obs://<bucket>/<key>` |
70
+ | Upload directory (recursive) | `hcloud OBS cp <dir>/ obs://<bucket>/ -r -f -flat` |
71
+ | Download object | `hcloud OBS cp obs://<bucket>/<key> <local-path>` |
72
+ | Set bucket ACL | `hcloud OBS chattri obs://<bucket> -acl=public-read` |
73
+ | Set object ACL | `hcloud OBS chattri obs://<bucket>/<key> -acl=public-read` | Bucket ACL does NOT cascade — anonymous reads need both |
74
+ | Set lifecycle | `hcloud OBS lifecycle obs://<bucket> -method=put -localfile=<json>` |
75
+ | Set bucket policy | `hcloud OBS bucketpolicy obs://<bucket> -method=put -localfile=<json>` |
76
+ | Set CORS | `huaweicloud_obs_set_cors` (set/get/delete) — KooCLI OBS has no cors subcommand; the tool does AWS4-signed `PUT /?cors` internally. allowedMethod MUST include HEAD |
77
+ | Delete bucket | `hcloud OBS rm obs://<bucket> -r` (must be empty) |
78
+ | Presigned URL | `hcloud OBS sign obs://<bucket>/<key> -e=<seconds>` |
79
+ | Object metadata | `hcloud OBS stat obs://<bucket>/<key>` |
80
80
 
81
81
  ## Static Website Deployment Workflow
82
82
 
@@ -131,3 +131,4 @@ See `references/single-file-share.md` for the full workflow to host one file and
131
131
  - Static website: references/static-website.md
132
132
  - Single-file share: references/single-file-share.md
133
133
  - Lifecycle: references/bucket-lifecycle.md
134
+ - Browser direct upload & CORS: references/common-pitfalls.md
@@ -69,7 +69,7 @@ KooCLI OBS **不支持** `SetBucketWebsite` 命令。配置静态网站托管需
69
69
 
70
70
  **避坑方法**:
71
71
 
72
- 1. **验证网页行为必须用 GET 直连**(`curl -s -D - -o NUL <url>` 或浏览器)——HEAD 和代理层会给出假象
72
+ 1. **验证网页行为必须用 GET 直连**(`curl -s -D - -o /dev/null <url>`,Windows 下为 `-o NUL`;或浏览器)——HEAD 和代理层会给出假象
73
73
  2. **官方解法**:绑定自定义域名接入 **CDN**,在"CDN 控制台 > 域名管理 > 高级设置 > HTTP header 配置"设置 `Content-Disposition: inline`(参见华为云 CDN 文档《如何在浏览器中在线预览 OBS 中的对象?》)
74
74
  3. 跨云对照:阿里云 OSS 同样策略(Bucket 域名访问 HTML 强制下载,绑定自定义域名即可在线预览)
75
75
 
@@ -126,3 +126,170 @@ hcloud OBS chattri obs://<bucket>/index.html -acl=public-read
126
126
  - SHOULD 开启访问日志
127
127
  - 预签名 URL 最大 7 天有效期
128
128
  - AK/SK 绝对不能写入 Bucket Policy
129
+
130
+ ## 浏览器直传(BrowserJS SDK)
131
+
132
+ 使用 OBS BrowserJS SDK(`esdk-obs-browserjs` 3.25.6)从浏览器直传 OBS 时的已知陷阱。**共享 AK/SK 的完整客户端代码严禁出现在浏览器端**——浏览器直传只允许「服务端生成预签名 URL / 临时凭证 + 浏览器携带凭证直传」模式。
133
+
134
+ ### p01 · CORS AllowedMethod 缺 HEAD → 探测请求被吞
135
+
136
+ **现象**:浏览器端 `listObjects` 一律失败,DevTools 只显示 `Network Error`,无任何 OBS 错误体——因为没有 CORS 头,浏览器直接吞掉了响应。
137
+
138
+ **根因**:BrowserJS SDK 3.25.6 默认开启签名协商(`isSignatureNegotiation=!0`),在发起 listObjects 前会先向桶发一个 `HEAD ?apiversion` 探测请求。若 CORS 规则的 AllowedMethod 不含 `HEAD`,这个预检/探测请求返回 403 `AccessForbidden` 且**响应不带 CORS 头**,浏览器按同源策略丢弃整个响应,最终只呈现 `Network Error`——真实错误(AccessForbidden)被完全掩盖。
139
+
140
+ **修复**:CORS 规则的 AllowedMethod 必须包含 `HEAD`(连同 GET/PUT/POST/DELETE,完整五方法见下文「CORS 配置」章节的 JSON/XML 模板)。
141
+
142
+ ### p02 · `signature:'v4'` 被静默忽略
143
+
144
+ **现象**:构造客户端时传 `signature:'v4'`,但生成的请求/预签名仍是 V2 签名,且无任何警告。
145
+
146
+ **根因**:3.25.6 的 `initFactory` 未消费 `signature` 配置项,静默降级为默认 V2。想要 V4 预签名,必须手动注入 `signatureContext`:
147
+
148
+ ```js
149
+ const client = new ObsClient({
150
+ access_key_id: '<AK>', // 临时凭证场景用 STS 临时 AK
151
+ secret_access_key: '<SK>',
152
+ server: 'https://obs.<region>.myhuaweicloud.com',
153
+ // signature: 'v4' 在 3.25.6 被静默忽略,不起作用 —— 见 p02
154
+ signatureContext: {
155
+ signature: 'v4',
156
+ headerPrefix: 'X-Amz-',
157
+ headerMetaPrefix: 'X-Amz-Meta-',
158
+ authPrefix: 'AWS4-HMAC-SHA256',
159
+ },
160
+ });
161
+ ```
162
+
163
+ ### p03 · 预签名 URL 的 token 参数大小写陷阱
164
+
165
+ **现象**:用临时凭证(STS token)生成 V4 预签名 URL,浏览器访问返回 403 `AccessDenied`。
166
+
167
+ **根因**:SDK 3.25.6 拼预签名 query 时把安全令牌参数拼成混合大小写 `X-Amz-security-token`,而 OBS 服务端对 query 参数名**大小写敏感**,只认小写 `x-amz-security-token`,大小写不匹配 → 403 `AccessDenied`。
168
+
169
+ **修复**:不要依赖 SDK 自动拼接 token,改为在生成预签名 URL 时以**小写**参数名手动传入 QueryParams:
170
+
171
+ ```js
172
+ const { signedUrl } = await client.createSignedUrl({
173
+ Method: 'PUT',
174
+ Bucket: '<bucket>',
175
+ Key: '<object-key>',
176
+ Expires: 3600,
177
+ Headers: { 'x-amz-acl': 'public-read' },
178
+ QueryParams: {
179
+ 'x-amz-security-token': '<security-token>', // 必须小写!SDK 自动拼的是 X-Amz-security-token(见 p03)
180
+ },
181
+ });
182
+ // 浏览器侧直接 fetch(signedUrl, { method: 'PUT', body: file })
183
+ ```
184
+
185
+ ### p04 · V4 头部签名在浏览器端不可用
186
+
187
+ **现象**:给浏览器端客户端配了 V4 signatureContext 后,listObjects / deleteObject 等头部鉴权请求全部 403。
188
+
189
+ **根因**:V4 头部签名依赖的派生/规范化逻辑在浏览器环境不完整(BrowserJS SDK 仅对**预签名 URL** 路径支持 V4)。
190
+
191
+ **修复**——双客户端分工:
192
+
193
+ - **列表/删除等 HTTP 头鉴权操作**:用 V2 头部签名客户端,并显式关闭签名协商(否则又踩 p01 的 HEAD 探测问题):
194
+
195
+ ```js
196
+ const v2Client = new ObsClient({
197
+ access_key_id: '<AK>',
198
+ secret_access_key: '<SK>',
199
+ server: 'https://obs.<region>.myhuaweicloud.com',
200
+ is_signature_negotiation: false, // 关闭协商,跳过 HEAD ?apiversion 探测(见 p01/p04)
201
+ });
202
+ await v2Client.listObjects({ Bucket: '<bucket>' });
203
+ ```
204
+
205
+ - **展示/上传**:走 p02+p03 修正后的 V4 预签名 URL(`createSignedUrl`),浏览器侧用 `fetch` 直接 PUT/GET。
206
+
207
+ ### p05 · OBS 错误响应缺 CORS 头 → 浏览器吞错
208
+
209
+ **现象**:任何 OBS 侧错误(AccessDenied、InvalidAccessKeyId、CORS 拒绝等)在浏览器里都只剩 `Network Error`,无法定位真实原因。
210
+
211
+ **根因**:预检失败时 OBS 的错误响应本身不带 CORS 头,浏览器同源策略直接丢弃响应体。这是浏览器行为,不是 OBS 不可用。
212
+
213
+ **诊断**(二选一,均可拿到真实状态码与错误体):
214
+
215
+ ```bash
216
+ # 方法①:curl 实测 OPTIONS 预检 —— 看 OBS 真实返回的状态码与错误体
217
+ curl -i -X OPTIONS 'https://<bucket>.obs.<region>.myhuaweicloud.com/<object-key>' \
218
+ -H 'Origin: https://<your-app-origin>' \
219
+ -H 'Access-Control-Request-Method: PUT' \
220
+ -H 'Access-Control-Request-Headers: content-type,x-amz-acl'
221
+
222
+ # 方法②:Node 环境(无 CORS 约束)复现同一请求 —— 用 node fetch 重放浏览器的完整请求
223
+ node -e "fetch('<same-url>', {method:'PUT', headers:{'Origin':'https://<your-app-origin>'}}).then(r=>r.text().then(t=>console.log(r.status,t)))"
224
+ ```
225
+
226
+ 预检返回 403 `AccessForbidden` → CORS 规则缺方法/头部;返回 200 但带 `Access-Control-Allow-Origin: *` → CORS 已通,错误在别处(签名/ACL/策略)。
227
+
228
+ ## CORS 配置(浏览器直传前置条件)
229
+
230
+ 浏览器直传 OBS 前,桶必须先配置 CORS 规则,否则所有请求都会被预检拦截。
231
+
232
+ **KooCLI 缺口**:KooCLI 的 OBS 包装层未暴露 obsutil 的 `cors` 子命令——执行 `hcloud OBS cors` 报 `No such command`(`hcloud OBS help` 列表中也无 cors)。
233
+
234
+ **首选路径:MCP 工具 `huaweicloud_obs_set_cors`(#1003)**——内部 AWS4 签名调 OBS REST API(`PUT/GET/DELETE /?cors`),屏蔽签名细节。`action=set` 传 `rules` 数组(`allowedOrigin` / `allowedMethod[]` 必须含 HEAD / `allowedHeader[]` / `maxAgeSeconds`),工具在入参边界就拒绝缺 HEAD 的规则(p01 前置防线);set/get 成功返回体自带 `verify` 命令(OPTIONS 预检)。SDK/控制台仅作降级路径。
235
+
236
+ **降级路径 ①:REST API `PUT /?cors` + CORS XML**
237
+
238
+ 向桶 endpoint 发 `PUT /?cors`,body 为 CORSConfiguration XML;请求需带 AWS4 签名(Authorization 头,可用 SDK/工具生成,或使用插件内 `huaweicloud_obs_set_website_config` 同款的 AWS4 签名方式)。`PUT /?cors` 使用的 AllowedMethod 必须覆盖浏览器直传全部五个方法:
239
+
240
+ ```xml
241
+ <CORSConfiguration>
242
+ <CORSRule>
243
+ <AllowedOrigin>https://<your-app-origin></AllowedOrigin>
244
+ <AllowedMethod>GET</AllowedMethod>
245
+ <AllowedMethod>PUT</AllowedMethod>
246
+ <AllowedMethod>POST</AllowedMethod>
247
+ <AllowedMethod>DELETE</AllowedMethod>
248
+ <AllowedMethod>HEAD</AllowedMethod> <!-- 缺 HEAD 会触发 p01:SDK 的 HEAD ?apiversion 探测被拒 -->
249
+ <AllowedHeader>*</AllowedHeader>
250
+ <MaxAgeSeconds>3000</MaxAgeSeconds>
251
+ </CORSRule>
252
+ </CORSConfiguration>
253
+ ```
254
+
255
+ **降级路径 ②:esdk-obs-python `setBucketCors`**
256
+
257
+ 服务端 Python 环境最小示例(凭证从环境变量读取,勿硬编码):
258
+
259
+ ```python
260
+ import os
261
+ from obs import ObsClient
262
+
263
+ client = ObsClient(
264
+ access_key_id=os.environ['HUAWEICLOUD_SDK_AK'],
265
+ secret_access_key=os.environ['HUAWEICLOUD_SDK_SK'],
266
+ server='https://obs.<region>.myhuaweicloud.com',
267
+ )
268
+
269
+ client.setBucketCors(
270
+ '<bucket>',
271
+ [
272
+ {
273
+ 'allowedOrigin': 'https://<your-app-origin>',
274
+ 'allowedMethod': ['GET', 'PUT', 'POST', 'DELETE', 'HEAD'], # HEAD 必含,见 p01
275
+ 'allowedHeader': ['*'],
276
+ 'maxAgeSeconds': 3000,
277
+ }
278
+ ],
279
+ )
280
+ ```
281
+
282
+ JSON 形态(供 MCP 工具/自研封装消费):
283
+
284
+ ```json
285
+ [
286
+ {
287
+ "allowedOrigin": "https://<your-app-origin>",
288
+ "allowedMethod": ["GET", "PUT", "POST", "DELETE", "HEAD"],
289
+ "allowedHeader": ["*"],
290
+ "maxAgeSeconds": 3000
291
+ }
292
+ ]
293
+ ```
294
+
295
+ 配置完成后用 p05 的 curl OPTIONS 预检命令验证:预检应返回 200 且响应头含 `Access-Control-Allow-Origin`。走 `huaweicloud_obs_set_cors` 工具时,set/get 返回体的 `verify` 字段就是这条预检命令(含 `--noproxy '*'`,#1000 的注入代理陷阱同样适用于 OPTIONS 预检)。
@@ -63,6 +63,14 @@ Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace t
63
63
  | `huaweicloud_sandbox_connect` | Connect to sandbox (one user one instance, reuses existing if available) |
64
64
  | `huaweicloud_sandbox_credentials` | Inject temporary AK/SK into a running sandbox |
65
65
 
66
+ ### Tunnel Persistence (public URL lifetime)
67
+
68
+ | Tool | Purpose |
69
+ | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
70
+ | `huaweicloud_sandbox_expose_tunnel` | Expose a port → public tunnel URL (auth, pre-clean, health check; `expiry_hours` 1-720, def 72). **AUTO-enrolls into the keepalive watchdog** (merged into existing coverage; `keepalive: false` opts out) |
71
+ | `huaweicloud_sandbox_renew_tunnel` | Renew a tunnel lifetime (`update -e`, rolling window; returns absolute expiresAt) |
72
+ | `huaweicloud_sandbox_keepalive_watchdog` | Install/replace the 3-layer watchdog: host re-attach + daily 720h renewal + 5-min health check |
73
+
66
74
  ### Terminal Execution
67
75
 
68
76
  | Tool | Purpose |
@@ -86,6 +94,8 @@ Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace t
86
94
  | Deployment scripts | `exec_one_shot+shot` | Long script, fresh connection avoids session timeouts |
87
95
  | nginx configuration | `deploy_nginx` | Auto-generates correct template + permissions + reload |
88
96
  | Deployment completeness check | `deploy_check` | Verifies nginx, DevBridge, URL, QR before reporting success |
97
+ | Tunnel lifetime renewal | `renew_tunnel` | Rolling-window renewal, same URL stays alive |
98
+ | URL must outlive the session | `keepalive_watchdog` | Unattended host re-attach + daily renewal + health check |
89
99
  | Single file upload (<1MB) | `upload_file` | Base64 chunked, reliable for small files |
90
100
  | Project directory upload (>1MB) | `upload_project` | HTTP tunnel, much faster than base64 for multi-file projects |
91
101
 
@@ -235,7 +245,7 @@ Expose a deployed web app to a public URL and return that URL to the developer.
235
245
  **Check the version first.** Sandboxes created before Sep 2026 ship devbridge 0.1.13, whose hardcoded gateway (`cn-north-4-bridge.myhuaweicloud.com`) was migrated and now serves a 「服务已迁移」 placeholder page — 0.1.x can no longer connect:
236
246
 
237
247
  ```bash
238
- export PATH="$HOME/.huawei/bin:$PATH" # PREPEND — appending lets a stale image binary in /usr/local/bin win
248
+ export PATH="/workspace/.local/bin:$HOME/.huawei/bin:$PATH" # PREPEND — appending lets a stale image binary in /usr/local/bin win
239
249
  devbridge version 2>/dev/null || echo "not installed"
240
250
  ```
241
251
 
@@ -256,39 +266,61 @@ DB_TARGET=$(grep -m1 'DEFAULT_VERSION=' /tmp/devbridge-install.sh | cut -d'"' -f
256
266
  if ! bash /tmp/devbridge-install.sh -s -u "https://gitcode.com/CloudDeveloperDepartment/devbrige/releases/download/${DB_TARGET}" -v "${DB_TARGET}"; then
257
267
  bash /tmp/devbridge-install.sh -s # fallback: GitHub (baked-in URL)
258
268
  fi
259
- export PATH="$HOME/.huawei/bin:$PATH" # PREPEND (session shells do not re-source ~/.bashrc)
269
+ export PATH="/workspace/.local/bin:$HOME/.huawei/bin:$PATH" # PREPEND (session shells do not re-source ~/.bashrc)
260
270
 
261
271
  # Layer 3 (both installer paths failed): direct GitHub release tar.gz.
262
272
  # Asset naming is fixed: devbridge_linux_arm64_<version>-release.tar.gz (the sandbox
263
273
  # is Linux aarch64). If DB_TARGET is empty the installer download itself failed —
264
274
  # resolve the latest tag from the GitHub releases API first.
275
+ # Install to /workspace/.local/bin — /workspace is the sandbox's DATA disk and
276
+ # survives reboots; ~/.huawei/bin lives on the SYSTEM disk and a reboot wipes it
277
+ # back to the image state (measured 2026-10-22: an overnight reboot reverted
278
+ # ~/.huawei/bin/devbridge 0.2.2 to the image's 0.1.13 — yesterday's success
279
+ # replayed as today's unexplained failure). (#999)
265
280
  if ! command -v devbridge >/dev/null 2>&1; then
266
281
  [ -z "$DB_TARGET" ] && DB_TARGET=$(curl -fsSL --http1.1 --retry 2 --max-time 60 \
267
282
  https://api.github.com/repos/huaweicloud/devspace-devbridge/releases/latest | grep -m1 '"tag_name"' | cut -d'"' -f4)
268
- mkdir -p "$HOME/.huawei/bin"
283
+ mkdir -p /workspace/.local/bin
269
284
  curl -fsSL --http1.1 --retry 3 --retry-all-errors --max-time 180 \
270
285
  "https://github.com/huaweicloud/devspace-devbridge/releases/download/${DB_TARGET}/devbridge_linux_arm64_${DB_TARGET}.tar.gz" \
271
286
  -o /tmp/devbridge.tar.gz \
272
287
  && rm -rf /tmp/devbridge-pkg && mkdir -p /tmp/devbridge-pkg \
273
288
  && tar -xzf /tmp/devbridge.tar.gz -C /tmp/devbridge-pkg \
274
- && install -m 0755 /tmp/devbridge-pkg/devbridge "$HOME/.huawei/bin/devbridge"
275
- export PATH="$HOME/.huawei/bin:$PATH"
289
+ && install -m 0755 /tmp/devbridge-pkg/devbridge /workspace/.local/bin/devbridge
290
+ export PATH="/workspace/.local/bin:$HOME/.huawei/bin:$PATH"
276
291
  fi
277
292
  devbridge version # must print 0.2.x-release
278
293
 
279
294
  # Remove the stale image-installed binary so only ONE version remains.
280
- # (Image sandboxes ship 0.1.x at /usr/local/bin/devbridge; the installer writes ~/.huawei/bin.
281
- # Compare against the explicit ~/.huawei/bin path — never `command -v`, which can resolve
282
- # to the stale binary itself when PATH order differs.)
283
- if [ -x "$HOME/.huawei/bin/devbridge" ] && [ -f /usr/local/bin/devbridge ] && [ "$(readlink -f /usr/local/bin/devbridge)" != "$(readlink -f "$HOME/.huawei/bin/devbridge")" ]; then
295
+ # (Image sandboxes ship 0.1.x at /usr/local/bin/devbridge; the installer writes
296
+ # /workspace/.local/bin. Compare against the explicit install path — never
297
+ # `command -v`, which can resolve to the stale binary itself when PATH order differs.)
298
+ if [ -f /usr/local/bin/devbridge ] && [ -x /workspace/.local/bin/devbridge ] && [ "$(readlink -f /usr/local/bin/devbridge)" != "$(readlink -f /workspace/.local/bin/devbridge)" ]; then
284
299
  rm -f /usr/local/bin/devbridge && echo "stale /usr/local/bin/devbridge removed"
285
300
  fi
301
+ # A leftover 0.2.x in ~/.huawei/bin (installed by older docs) is valid until the
302
+ # next reboot — deduplicate it against the /workspace copy so the version check
303
+ # above can never report the image's 0.1.x.
304
+ if [ -x "$HOME/.huawei/bin/devbridge" ] && [ -x /workspace/.local/bin/devbridge ] && [ "$(readlink -f "$HOME/.huawei/bin/devbridge")" != "$(readlink -f /workspace/.local/bin/devbridge)" ]; then
305
+ rm -f "$HOME/.huawei/bin/devbridge" && echo "stale ~/.huawei/bin/devbridge removed"
306
+ fi
286
307
  ```
287
308
 
288
309
  - `-s` (silent) is **required** in the sandbox — without it the installer blocks reading `/dev/tty`.
289
310
  - **Old tunnels do not survive the upgrade**: 0.1.x-created tunnels are not registered on the s2 gateway — always rebuild the tunnel after upgrading (the expose flow below does this via pre-cleanup).
290
311
  - If `10006: quota exceeded` appears after upgrading, stale pre-upgrade tunnels still count against the quota — `devbridge delete-all` and retry.
291
312
 
313
+ ### Sandbox filesystem persistence: what survives a reboot (#999)
314
+
315
+ | Path | Type | After a sandbox reboot |
316
+ | -------------------------------------------- | -------------- | ------------------------------------------------------------------------------------ |
317
+ | `/workspace` (incl. `/workspace/.local/bin`) | Data disk | **Survives** — install project code and the devbridge 0.2.x binary here |
318
+ | `$HOME` (incl. `~/.huawei/bin`) | System disk | **Rolled back to the image state** — an installed binary is lost, 0.1.13 returns |
319
+ | `/usr/local/bin` | System disk | Rolled back — the image-preinstalled 0.1.x returns |
320
+ | `/tmp` | Session-scoped | Cleared — `/tmp/hw_creds.sh`, `/tmp/hw_api_key`, `/tmp/tunnel-keepalive.sh` are gone |
321
+
322
+ Measured (2026-10-22): an overnight reboot reverted `~/.huawei/bin/devbridge` 0.2.2 to the image's 0.1.13 — **yesterday's successful operation replayed as today's unexplained failure**. Recovery after a reboot: re-run the Step 0 install (or just re-export the PATH to the surviving `/workspace/.local/bin`), re-run `huaweicloud_sandbox_credentials` to re-inject `/tmp` credentials, and re-run `huaweicloud_sandbox_keepalive_watchdog` to restore the keepalive loop.
323
+
292
324
  ### Step 1: Authenticate — verified waterfall, keyring first
293
325
 
294
326
  Authentication tries three credential sources in order, each verified with a real login + `auth status` before use; a failure falls through to the next tier:
@@ -300,7 +332,7 @@ Authentication tries three credential sources in order, each verified with a rea
300
332
  `huaweicloud_sandbox_expose_tunnel` runs this waterfall automatically and reports the winning mode (`API_KEY_KEYRING` / `AKSK_SUPPORTED` / `API_KEY_FILE`) or `NO_CREDENTIAL` with a non-sensitive per-tier `attempts` diagnostic. Manual equivalent:
301
333
 
302
334
  ```bash
303
- export PATH="$HOME/.huawei/bin:$PATH"
335
+ export PATH="/workspace/.local/bin:$HOME/.huawei/bin:$PATH"
304
336
  export HOME="${HOME:-/root}"
305
337
  # Tier 1: platform-injected keyring key (stderr! prefix filter!)
306
338
  export DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-unix:path=/run/dbus-session}"
@@ -361,7 +393,10 @@ sleep 10 && cat /tmp/host.log
361
393
 
362
394
  # 2. Extract tunnel URL and health-check before returning
363
395
  TUNNEL_URL=$(grep -oP 'Tunnel URL: \K.*' /tmp/host.log)
364
- HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$TUNNEL_URL" 2>/dev/null || echo "000")
396
+ # --noproxy is MANDATORY (#1000): the sandbox injects an HTTP proxy that does NOT
397
+ # route the tunnel domain — without it a working tunnel verifies as 502/404 and
398
+ # sends you chasing credentials/version/port.
399
+ HTTP_CODE=$(curl -s --noproxy '*' -o /dev/null -w "%{http_code}" --max-time 10 "$TUNNEL_URL" 2>/dev/null || echo "000")
365
400
  if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "304" ]; then
366
401
  echo "Tunnel verified: $TUNNEL_URL (HTTP $HTTP_CODE)"
367
402
  else
@@ -372,13 +407,15 @@ else
372
407
  disown 2>/dev/null || true
373
408
  sleep 10
374
409
  TUNNEL_URL=$(grep -oP 'Tunnel URL: \K.*' /tmp/host.log)
375
- HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$TUNNEL_URL" 2>/dev/null || echo "000")
410
+ HTTP_CODE=$(curl -s --noproxy '*' -o /dev/null -w "%{http_code}" --max-time 10 "$TUNNEL_URL" 2>/dev/null || echo "000")
376
411
  echo "Retry tunnel: $TUNNEL_URL (HTTP $HTTP_CODE)"
377
412
  fi
378
413
  ```
379
414
 
380
415
  **Never return a tunnel URL without verifying it first** — a stale URL (from a killed tunnel process) will silently fail. Always curl-check before giving the URL to the developer. Note: a migrated gateway serves its placeholder page with **HTTP 200** — a bare status check is not enough when diagnosing "200 but wrong content".
381
416
 
417
+ **Proxy trap on verify commands (#1000)**: every curl against `*.hwtunnel.com` MUST pass `--noproxy '*'` — the sandbox injects an HTTP/HTTPS proxy that does not route the tunnel domain, so a proxied verify returns 502/404 fakes while the tunnel is actually serving. If a tunnel "fails" verification, re-test with `--noproxy '*'` BEFORE checking versions/credentials/ports (same trap on the SSH path: huawei-ecs skill references/windows-ssh.md §0.1).
418
+
382
419
  **Quota recovery**: if the tunnel creation fails with `10006: quota exceeded`:
383
420
 
384
421
  ```bash
@@ -404,10 +441,35 @@ This eliminates the most common deployment failure — historical tunnels from p
404
441
 
405
442
  **No local downgrade**: if the tunnel tooling cannot be installed in the sandbox, STOP and report a generic error ("无法生成访问地址") without technical detail. Never install it on the developer's local machine — a local install would defeat the purpose of sandbox deployment.
406
443
 
444
+ ### Step 3: Keep the URL alive (persistence & renewal)
445
+
446
+ **AUTO-ENROLLMENT**: `huaweicloud_sandbox_expose_tunnel` automatically enrolls every exposed tunnel into the keepalive watchdog (merged into existing coverage — exposing a second tunnel never drops the first one's protection; `keepalive: false` opts out). The developer's requirement — "every tunnel deployed to the sandbox must be keepalive-protected" — is satisfied by the tool itself, not by remembering to call the watchdog.
447
+
448
+ The tunnel URL is bound to the tunnel ID, not the host process — persistence is about **preventing expiry, never URL recovery**. `-e` accepts 1..720 hours (720 = the 30-day maximum); the expiry is a **rolling window** (renewal sets `expiration = now + hours` regardless of remaining time), so renewing before expiry keeps the same URL alive indefinitely.
449
+
450
+ - **Renew** (same URL, extends lifetime): `huaweicloud_sandbox_renew_tunnel` (or `devbridge update <id> -e 720`). A renewal counts as ok only when the update succeeds AND `show <id> -v` reports the absolute `tunnelExpiration` — the management plane can lose a tunnel record (`10002 tunnel not found`) while the data plane keeps serving.
451
+ - **Host process dies** → the watchdog re-attaches with `devbridge host <id>`; the URL does NOT change.
452
+ - **Manual install / replacement / repair** → `huaweicloud_sandbox_keepalive_watchdog`: installs a setsid-detached loop (host watchdog + daily 720h renewal + 5-min health check) at `/tmp/tunnel-keepalive.sh`.
453
+
454
+ Complete measured procedure and traps (OSC-escape port list, health-check URL, watchdog pgrep): see `references/tunnel-persistence.md`.
455
+
456
+ ## Full-Stack: REST API over PostgreSQL (PostgREST)
457
+
458
+ When deploying a **full-stack workbench project** (前后端工作台) or asking for a database-backed REST API inside the sandbox (postgres, postgrest, 数据库, RESTful 接口, 工作台后端):
459
+
460
+ 1. **PostgreSQL 16 from PGDG** — the OS repo only ships PG 13, and PostgREST 16.x requires PG ≥ 14. HCE is NOT el8-lib-compatible: hand-write the repo file (the el8 path is `rhel-8-aarch64` WITH a hyphen), install the RPMs one-per-call with `--nodeps`, and shim the sonames — including the REAL `libicu-60` package (ICU symlinks load but crash on versioned symbols).
461
+ 2. **PostgREST `linux-static-aarch64`** — dynamic Ubuntu builds fail on HCE glibc. Create the schema and a read-only `api_user` role (the anon role gates all writes; POST/PATCH → 401).
462
+ 3. **Nginx single-origin** — `/` serves the frontend, `/api/` reverse-proxies to PostgREST (no CORS, same origin). The frontend dir needs `chmod o+x` on the whole path chain or nginx returns 403.
463
+ 4. **Expose + keep alive (automatic)** — `huaweicloud_sandbox_expose_tunnel` with the nginx port; it AUTO-enrolls the tunnel into the keepalive watchdog (merged into existing coverage). Prefer `expiry_hours: 720` for long-lived workbench deployments.
464
+
465
+ Complete measured procedure (with the full command chain): see `references/fullstack-postgrest.md`.
466
+
407
467
  ## Web Application Deployment
408
468
 
409
469
  When deploying a web application to the sandbox, build the app inside the sandbox before exposing it. Source code is uploaded, dependencies installed, and the framework built in the sandbox environment.
410
470
 
471
+ **Backend present?** If the project includes a PostgreSQL + PostgREST backend (or the developer asks for one), set up the backend FIRST by following [Full-Stack: REST API over PostgreSQL](#full-stack-rest-api-over-postgresql-postgrest) — install PostgreSQL/PostgREST, create the schema, and point the frontend's nginx `/api/` location at PostgREST — then continue with the steps below. Every tunnel exposed afterwards is auto-enrolled into the keepalive watchdog.
472
+
411
473
  **沙箱无 systemd(实测 #935 报告3 问题3)**:沙箱为无 systemd 的容器环境,`systemctl` 全部失效(`System has not been booted with systemd`)—— 不要用 `systemctl start/reload/restart nginx`。Nginx 配置变更直接使用 `nginx -t` 验证 + `nginx -s reload` 热加载,或直接执行 nginx 二进制(`sudo nginx` 启动 / `nginx -s stop` 停止)。`huaweicloud_sandbox_deploy_nginx` 工具返回的 nextStep 文案与该语义一致(配置写入后自动回验存在性与 `nginx -t`,通过后即生效,无需 systemctl)。
412
474
 
413
475
  ### Step 1: Detect Framework Locally
@@ -980,7 +1042,7 @@ Returns `complete: true/false`, `score`, and `nextStep` to fix missing items.
980
1042
  | Never expose tunnel details | Do not mention "DevBridge"/"tunnel"/"devbridge" to the developer — say "正在生成访问地址..." and hand over only the URL |
981
1043
  | devbridge auth: probe the build | `0.2.2-release` has two builds: image builds retain AK/SK login + env auto-read (fully automatic via `/tmp/hw_creds.sh`); release builds accept only API Key (`/tmp/hw_api_key`). Never assume — probe with `devbridge auth login --help 2>&1 \| grep -q -- '--access-key'` (Step 1 of "Expose the deployed app") |
982
1044
  | devbridge 0.1.x is dead | Sandboxes created before Sep 2026 ship 0.1.13, which connects to a migrated gateway serving a 「服务已迁移」 placeholder with HTTP 200. Check `devbridge version` first and upgrade in place (Step 0) — old tunnels never survive the upgrade |
983
- | CLI PATH | The installer only writes `~/.bashrc`; run `export PATH="$HOME/.huawei/bin:$PATH"` (prepend) in the session before using `devbridge` — appending lets a stale image binary in `/usr/local/bin` win |
1045
+ | CLI PATH | The installer only writes `~/.bashrc`; run `export PATH="/workspace/.local/bin:$HOME/.huawei/bin:$PATH"` (prepend) in the session before using `devbridge` — appending lets a stale image binary in `/usr/local/bin` win. `/workspace/.local/bin` survives sandbox reboots; `~/.huawei/bin` is on the system disk and is wiped on reboot (#999) |
984
1046
  | Never install tunnel tooling locally | If the sandbox cannot install it, report a generic error and stop — installing on the developer's machine defeats sandbox deployment |
985
1047
  | Return the deployment URL | Always hand the public URL from the host log to the developer as the final result |
986
1048
  | Deploy is not just nginx | Configuring nginx does NOT complete the deployment. Steps 7 (DevBridge expose) and deploy_check are REQUIRED — `deploy_nginx` returns `nextStep: expose_via_devbridge` as a reminder. Do not stop after nginx. |