huaweicloud-devkit 1.1.9 → 1.1.10-next.1
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/integrations/opencode/package-lock.json +24 -0
- package/integrations/opencode/package.json +10 -0
- package/integrations/opencode/plugins/huaweicloud-devkit.js +194 -0
- package/package.json +1 -1
- package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +7 -1
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md +53 -0
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/hc-activity.md +16 -0
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/troubleshooting.md +51 -0
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/windows-ssh.md +24 -0
- package/plugins/huaweicloud-core/skills/huawei-iac/SKILL.md +38 -36
- package/plugins/huaweicloud-core/skills/huawei-iam/SKILL.md +35 -16
- package/plugins/huaweicloud-core/skills/huawei-iam/references/quick-ref.md +35 -0
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +8 -1
- package/plugins/huaweicloud-core/src/account-type.mjs +135 -0
- package/plugins/huaweicloud-core/src/auth/credentials.mjs +81 -0
- package/plugins/huaweicloud-core/src/auth/token-exchange.mjs +100 -0
- package/plugins/huaweicloud-core/src/hcloud-cli.mjs +58 -5
- package/plugins/huaweicloud-core/src/mcp-config-merge.mjs +1 -1
- package/plugins/huaweicloud-core/src/risk-rule-engine.mjs +1 -1
- package/plugins/huaweicloud-core/src/safety-policy.mjs +37 -6
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +172 -21
- package/plugins/huaweicloud-core/src/setup-cli.mjs +62 -4
- package/plugins/huaweicloud-core/src/tools.mjs +55 -5
|
@@ -21,16 +21,16 @@ Always run `hcloud <Service> <Operation> --help` before constructing commands to
|
|
|
21
21
|
|
|
22
22
|
**Proactive by design**: any deployment intent ("部署一个博客", "搭一套环境", "买个服务器") automatically triggers stages 1-5 BEFORE anything is created. The user NEVER needs to ask for an architecture proposal, cost estimate, or risk warnings - they are built into the flow. Only stages 6-8 (create / verify / manage) wait for explicit user confirmation at the approval gates.
|
|
23
23
|
|
|
24
|
-
| Stage | What happens | Hard rule
|
|
25
|
-
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
26
|
-
| 1. Scale assessment | **FIRST inspect the project code** (clone remote repos to a temp dir, or read the local path): tech stack, data services (Redis/MySQL/SQLite...), build artifacts, existing Dockerfile/k8s manifests → then classify or ask scale | **NEVER present deployment options (sandbox/ECS/OBS...) before the codebase has been inspected** - options without code analysis are guesses. If scale is not stated or clearly inferable, ASK - present the 3 tiers as options with what each includes and its monthly cost band (see `references/architectures.md`), and let the user choose. Never assume a tier. The scale question and the hosting-target question are SEPARATE questions on separate axes. FunctionGraph filesystem is ephemeral - never recommend SQLite/local files for persistent data
|
|
27
|
-
| 2. Frontend hosting choice | Present BOTH options: (a) OBS+CDN+DNS custom domain, (b) ECS+EIP | For SPA/static frontends (Vue/React dist), **OBS hosting must always appear as an option** with cost comparison against same-host Nginx - do not silently merge the frontend into a compute node. For option (a) three prerequisites: domain ownership verification (CDN requires a DNS record or file upload first - `CDN.00010185` otherwise), ICP filing check, DNS hosting check; domain registration link: https://www.huaweicloud.com/product/domain.html
|
|
28
|
-
| 3. Architecture proposal | ASCII topology tree with dependencies | User confirms before continuing. **Architecture decision points must be surfaced as explicit choices, never silently made**: (a) app depends on Redis/MySQL/Kafka etc. → managed service (DCS/RDS/DMS, billed) vs same-host install (cheap, ops burden) - state the trade-off and ask; (b) repo ships a Dockerfile / SWR image / k8s manifests → offer container path (Docker on ECS or CCE) alongside source-build path; (c) frontend hosting layer (Nginx same-host vs OBS+CDN). Silent defaults here are a correctness failure
|
|
29
|
-
| 4. Parameter discovery | `ListFlavors` / `ListImages` / `huaweicloud_list_regions` | Never hardcode flavor/image names
|
|
30
|
-
| 5. Cost + balance gate | Price every resource via BSS `ListOnDemandResourceRatings`, then query balance | **前置拦截(实测 #922):估算成本 > 余额(含余额为 0)时 STOP — 不要带着一份注定失败的下单继续 stage 6**。第一条消息必须说明:"您的余额不足以覆盖本次部署的按时长计费资源(预计首月 <估算>,当前余额 <余额>),下单将失败(Ecs.7000 Insufficient account balance)——已暂停,请先充值或领券,完成后回复"已充值/继续"即可从本阶段续跑原配置"。充值入口:https://account.huaweicloud.com/usercenter/?region=cn-north-4#/userindex/balanceRecharge 。Remedy priority: (1) FIRST claim the one-time incentive voucher (huaweicloud_voucher_status → huaweicloud_voucher_claim) to offset usage fees; (2) then instruct the user to top up balance (via the link above) — pause the flow and resume from stage 5 with the SAME configuration after the top-up, never walk a known-doomed order; (3) free-tier resources (FunctionGraph, small OBS) proceed without any balance. Balance >= 估算成本 → report how long it can sustain (balance ÷ monthly estimate). **权限降级(实测)**:IAM 用户无 `billing:balance:view` 时余额查询报 `CBC.0151` → 不阻塞流程,明确告知"余额无法程序化查询,请自行核对;若下单报 Ecs.7000 即余额不足",领券后继续。定价编码逐服务查证,见 `references/resource-catalog.md` § Pricing |
|
|
31
|
-
| 6. Provision | deployment_id, session state, batch approval, ordered creation | See "Provisioning Rules" below
|
|
32
|
-
| 7. Verify | `curl -I` the site / API; expect 200 | DNS CNAME may take minutes to propagate - not an error
|
|
33
|
-
| 8. Manage | Session-scoped status query and destroy | See "Destroy Rules" below
|
|
24
|
+
| Stage | What happens | Hard rule |
|
|
25
|
+
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
26
|
+
| 1. Scale assessment | **FIRST inspect the project code** (clone remote repos to a temp dir, or read the local path): tech stack, data services (Redis/MySQL/SQLite...), build artifacts, existing Dockerfile/k8s manifests → then classify or ask scale | **NEVER present deployment options (sandbox/ECS/OBS...) before the codebase has been inspected** - options without code analysis are guesses. If scale is not stated or clearly inferable, ASK - present the 3 tiers as options with what each includes and its monthly cost band (see `references/architectures.md`), and let the user choose. Never assume a tier. The scale question and the hosting-target question are SEPARATE questions on separate axes. FunctionGraph filesystem is ephemeral - never recommend SQLite/local files for persistent data |
|
|
27
|
+
| 2. Frontend hosting choice | Present BOTH options: (a) OBS+CDN+DNS custom domain, (b) ECS+EIP | For SPA/static frontends (Vue/React dist), **OBS hosting must always appear as an option** with cost comparison against same-host Nginx - do not silently merge the frontend into a compute node. For option (a) three prerequisites: domain ownership verification (CDN requires a DNS record or file upload first - `CDN.00010185` otherwise), ICP filing check, DNS hosting check; domain registration link: https://www.huaweicloud.com/product/domain.html |
|
|
28
|
+
| 3. Architecture proposal | ASCII topology tree with dependencies | User confirms before continuing. **Architecture decision points must be surfaced as explicit choices, never silently made**: (a) app depends on Redis/MySQL/Kafka etc. → managed service (DCS/RDS/DMS, billed) vs same-host install (cheap, ops burden) - state the trade-off and ask; (b) repo ships a Dockerfile / SWR image / k8s manifests → offer container path (Docker on ECS or CCE) alongside source-build path; (c) frontend hosting layer (Nginx same-host vs OBS+CDN). Silent defaults here are a correctness failure |
|
|
29
|
+
| 4. Parameter discovery | `ListFlavors` / `ListImages` / `huaweicloud_list_regions` | Never hardcode flavor/image names |
|
|
30
|
+
| 5. Cost + balance gate | Price every resource via BSS `ListOnDemandResourceRatings`, then query balance | **前置拦截(实测 #922):估算成本 > 余额(含余额为 0)时 STOP — 不要带着一份注定失败的下单继续 stage 6**。第一条消息必须说明:"您的余额不足以覆盖本次部署的按时长计费资源(预计首月 <估算>,当前余额 <余额>),下单将失败(Ecs.7000 Insufficient account balance)——已暂停,请先充值或领券,完成后回复"已充值/继续"即可从本阶段续跑原配置"。充值入口:https://account.huaweicloud.com/usercenter/?region=cn-north-4#/userindex/balanceRecharge 。Remedy priority: (1) FIRST claim the one-time incentive voucher (huaweicloud_voucher_status → huaweicloud_voucher_claim) to offset usage fees; (2) then instruct the user to top up balance (via the link above) — pause the flow and resume from stage 5 with the SAME configuration after the top-up, never walk a known-doomed order; (3) free-tier resources (FunctionGraph, small OBS) proceed without any balance. Balance >= 估算成本 → report how long it can sustain (balance ÷ monthly estimate). **权限降级(实测)**:IAM 用户无 `billing:balance:view` 时余额查询报 `CBC.0151` → 不阻塞流程,明确告知"余额无法程序化查询,请自行核对;若下单报 Ecs.7000 即余额不足",领券后继续。定价编码逐服务查证,见 `references/resource-catalog.md` § Pricing。**余额风控语义(实测 #935)**:按量付费下单校验的是**现金余额**而非代金券(券只在账单结算时抵扣,不参与下单风控);历史欠费(即使金额很小)同样触发 `KPS.9004`/`Ecs.0067` 拒单 —— 门禁检查项必须包含:现金余额充足 且 欠费已结清(查询:`hcloud BSS ShowCustomerAccountBalances --cli-region=cn-north-1`)。**子网 DNS 前置修复(实测 #935 坑4)**:模板/预置子网 `dnsList` 可能为空,会导致 cloud-init 域名解析与 yum/dnf 静默失败 —— 创建 ECS 前先执行 `hcloud VPC UpdateSubnet` 补充华为云内网 DNS `100.125.1.250`、`100.125.129.250`:`hcloud VPC UpdateSubnet --subnet_id=<subnet-id> --subnet.dnsList.1=100.125.1.250 --subnet.dnsList.2=100.125.129.250` |
|
|
31
|
+
| 6. Provision | deployment_id, session state, batch approval, ordered creation | See "Provisioning Rules" below |
|
|
32
|
+
| 7. Verify | `curl -I` the site / API; expect 200 | DNS CNAME may take minutes to propagate - not an error |
|
|
33
|
+
| 8. Manage | Session-scoped status query and destroy | See "Destroy Rules" below |
|
|
34
34
|
|
|
35
35
|
## Provisioning Rules (Stage 6)
|
|
36
36
|
|
|
@@ -52,32 +52,34 @@ Execution: show the to-be-deleted list for final confirmation → delete in **re
|
|
|
52
52
|
|
|
53
53
|
## Critical Warnings
|
|
54
54
|
|
|
55
|
-
| Trap
|
|
56
|
-
|
|
|
57
|
-
| Stale `securityToken` poisons every hcloud call
|
|
58
|
-
| OBS writes bypass the REST write-prefix policy
|
|
59
|
-
| `OBS mb` flag rules differ from cp/rm
|
|
60
|
-
| Duplicate `OBS mb` is idempotent success
|
|
61
|
-
| Bucket ACL does not cascade to objects
|
|
62
|
-
| OBS static website bucket name must equal the domain
|
|
63
|
-
| VPC v3 API differs from older docs
|
|
64
|
-
| Public exposure is auto-blocked
|
|
65
|
-
| ECS CreateServers hidden required params
|
|
66
|
-
| FunctionGraph runtime/code enums are strict
|
|
67
|
-
| Keypairs: `NovaCreateKeypair`, not CreateKeypair
|
|
68
|
-
| Long inline args break MCP JSON
|
|
69
|
-
| EIP quota fails at RUN stage, not at plan
|
|
70
|
-
| Zero balance blocks order submission
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
|
|
|
77
|
-
| EIP
|
|
78
|
-
|
|
|
79
|
-
|
|
|
80
|
-
|
|
|
55
|
+
| Trap | Why / Rule |
|
|
56
|
+
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
57
|
+
| Stale `securityToken` poisons every hcloud call | Symptom: every API returns `APIGW.0301 Incorrect IAM authentication information` while OBS/obsutil still works. `configure set` refuses empty values and auth-sync never clears the field. Fix: `hcloud configure delete --cli-profile=default`, then re-run auth init with AK/SK only (no token) |
|
|
58
|
+
| OBS writes bypass the REST write-prefix policy | `mb` / `cp` / `rm` / `chattri` are obsutil-style commands. ALWAYS route them through `huaweicloud_plan_cli_command` → approval → `huaweicloud_run_approved_command`. Never treat them as read-only |
|
|
59
|
+
| `OBS mb` flag rules differ from cp/rm | `mb` does NOT accept `-f` (flag parse error) and REQUIRES explicit `-location=<region>` even with a regional endpoint (else `IllegalLocationConstraintException`) |
|
|
60
|
+
| Duplicate `OBS mb` is idempotent success | Same-account same-name bucket creation returns success, not an error - never treat a second create as failure. OBS deletes are also idempotent (deleting a nonexistent object succeeds) |
|
|
61
|
+
| Bucket ACL does not cascade to objects | Every uploaded object needs `-acl=public-read` at upload time (`-f` IS valid on cp/rm) |
|
|
62
|
+
| OBS static website bucket name must equal the domain | e.g. bucket `www.example.com` for domain `www.example.com` |
|
|
63
|
+
| VPC v3 API differs from older docs | v3 `CreateSecurityGroup` has NO `vpc_id` (account-scoped); `CreateSecurityGroupRule` uses `multiport=80` not `port_range_min/max`; `DeleteSubnet` requires `vpc_id` too. Always `--help` first |
|
|
64
|
+
| Public exposure is auto-blocked | Risk rules deny `0.0.0.0/0` port rules at plan time. For test/dev use a narrower `remote_ip_prefix` (e.g. the VPC CIDR) or get user consent for a documented exception |
|
|
65
|
+
| ECS CreateServers hidden required params | Besides flavor/image/nics/az it also needs `--server.vpcid` and `--server.root_volume.volumetype` - order submission fails without them |
|
|
66
|
+
| FunctionGraph runtime/code enums are strict | runtime must be exact (`Node.js18.15`, `Python3.9`, `Go1.x`, ...); inline code is the PAIR `--package=default --code_type=inline` (`--package=inline` → `FSS.1006`); TIMER triggers use `--event_data.schedule_type=Rate --event_data.schedule=1m` (`@every` style → `FSS.1109`); DeleteFunction takes `--function_urn` WITHOUT `:latest` |
|
|
67
|
+
| Keypairs: `NovaCreateKeypair`, not CreateKeypair | `ECS CreateKeypair` does NOT exist - use `hcloud ECS NovaCreateKeypair`; delete takes `--keypair_name` (not `--keypair.name`). The API-generated `private_key` is MASKED in tool output - for SSH access create the keypair in the console or import the user's own public key, then bind with `--server.key_name=<n>` |
|
|
68
|
+
| Long inline args break MCP JSON | Values >~2KB inline (e.g. `--server.user_data=<base64 cloud-init>`) fail JSON parsing silently. Write a local JSON file and pass `--cli-jsonInput=<file>` (see `references/resource-catalog.md` header note) |
|
|
69
|
+
| EIP quota fails at RUN stage, not at plan | `EIP.7905 Quota exceeded` appears only after plan+approval - pre-check quota during stage 5 (and count precisely: `ListPublicips --limit=50` truncates, a "50/50" read on a 59-EIP account is wrong). Released quotas may lag minutes before `EIP.7905` clears |
|
|
70
|
+
| Zero balance blocks order submission | ECS creation fails with `Ecs.7000 Insufficient account balance` when cash + voucher accounts are 0. This is the stage-5 gate materialized — and since #922 the gate INTERCEPTS before the order: when the estimate exceeds the balance, STOP at stage 5 (recharge entry + resume after top-up) instead of walking a known-doomed `CreateServers`. Free-tier resources (FunctionGraph, small OBS) DO work at 0 balance - the gate applies to order-submission resources only |
|
|
71
|
+
| Pay-per-use orders check CASH balance, not vouchers (#935) | 按量付费下单风控只校验**现金余额**:代金券不参与按量付费下单风控(券只在账单结算时抵扣);历史欠费(即使金额很小)同样触发 `KPS.9004`/`Ecs.0067` 拒单。stage-5 门禁检查项必须包含:现金余额充足 且 欠费已结清(`hcloud BSS ShowCustomerAccountBalances --cli-region=cn-north-1`) |
|
|
72
|
+
| Subnet `dnsList` may be empty on template/preset subnets (#935) | cloud-init 域名解析与 yum/dnf 静默失败。创建 ECS 前先 `hcloud VPC UpdateSubnet --subnet_id=<id> --subnet.dnsList.1=100.125.1.250 --subnet.dnsList.2=100.125.129.250` 补充华为云内网 DNS(详见 `huawei-ecs/references/create-instance.md` §4) |
|
|
73
|
+
| CDN mainland acceleration requires ICP filing | Unregistered domains cannot go live on CDN - check before stage 3. CDN create ALSO requires domain ownership verification first (`CDN.00010185 Verify domain ownership failed` - user must add a DNS record or upload a verification file) |
|
|
74
|
+
| CDN is a global service in KooCLI | CDN operations only accept `--cli-region=cn-north-1` or `ap-southeast-1` - never the resource region |
|
|
75
|
+
| OBS static website hosting has NO CLI path | `chattri` only sets ACL/storage-class; obsutil has no website command. Options: console setup, or CDN OBS-origin with `--domain.sources.1.enable_obs_web_hosting=1`. Without website hosting, verify via direct object URLs (still public HTTP 200) |
|
|
76
|
+
| DNS hosting location | `hcloud DNS ListPublicZones` decides: hosted in Huawei Cloud → auto-create CNAME record set; hosted elsewhere → output the CNAME value for manual setup at the user's DNS provider |
|
|
77
|
+
| EIP bills when idle; stopped ECS still bills | Include in cost estimate; destroy unattached EIPs |
|
|
78
|
+
| ECS delete defaults leak resources | `--delete_publicip` and `--delete_volume` are false by default - set `true` when tearing down |
|
|
79
|
+
| EIP binding type is non-obvious | `--publicip.associate_instance_type=PORT` (not INSTANCE) |
|
|
80
|
+
| State is session-only | Temp-dir file, dies with the session. Never promise cross-session deploy management |
|
|
81
|
+
| State file must be UTF-8 WITHOUT BOM | PowerShell `Set-Content -Encoding utf8` adds a BOM that breaks JSON parsers - write state with Node or another BOM-less writer |
|
|
82
|
+
| HTTPS needs a certificate | CDN serves plain HTTP by default; SSL cert (upload or SCM) required for HTTPS |
|
|
81
83
|
|
|
82
84
|
## MCP Tools
|
|
83
85
|
|
|
@@ -16,14 +16,15 @@ Domain expertise for Huawei Cloud Identity and Access Management (IAM). Covers u
|
|
|
16
16
|
|
|
17
17
|
## Critical Warnings
|
|
18
18
|
|
|
19
|
-
| Trap
|
|
20
|
-
|
|
|
21
|
-
| NEVER create IAM users for humans
|
|
22
|
-
| NEVER create long-term AK/SK
|
|
23
|
-
| Wildcard policies dangerous
|
|
24
|
-
| Agency trust is powerful
|
|
25
|
-
| Root account must have MFA
|
|
26
|
-
| `缺少必填参数 cli-domain-id` = invalid creds
|
|
19
|
+
| Trap | Why |
|
|
20
|
+
| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
21
|
+
| NEVER create IAM users for humans | Use OneAccess (IAM Identity Center) or federated SSO |
|
|
22
|
+
| NEVER create long-term AK/SK | Use temporary STS tokens via agencies |
|
|
23
|
+
| Wildcard policies dangerous | Effect:Allow + Resource:* = full access. Always scope resources |
|
|
24
|
+
| Agency trust is powerful | Agencies let services assume roles. Always add conditions |
|
|
25
|
+
| Root account must have MFA | Root AK/SK is all-powerful. Enable MFA immediately |
|
|
26
|
+
| `缺少必填参数 cli-domain-id` = invalid creds | KooCLI's account-id lookup failed (APIGW.0301). Re-run `auth init`. Domain-level params (`--domain_id`, `--agency.domain_id`) are separate — get the account-id via `hcloud STS GetCallerIdentity --cli-region=<region>` when needed |
|
|
27
|
+
| Primary-account check = `username == domainName` | Use `huaweicloud_check_account_type` (one call). NOT `ShowDomainPasswordPolicy` (permission-gated, sub-users can succeed) and NOT `principal_id == account_id` (false for primary accounts too). Full procedure: references/quick-ref.md |
|
|
27
28
|
|
|
28
29
|
## Policy Structure
|
|
29
30
|
|
|
@@ -43,14 +44,15 @@ Domain expertise for Huawei Cloud Identity and Access Management (IAM). Covers u
|
|
|
43
44
|
|
|
44
45
|
## Common Workflows
|
|
45
46
|
|
|
46
|
-
| Task
|
|
47
|
-
|
|
|
48
|
-
| List users
|
|
49
|
-
| Create group
|
|
50
|
-
| Create custom policy
|
|
51
|
-
| Create agency
|
|
52
|
-
| Get temporary credential
|
|
53
|
-
|
|
|
47
|
+
| Task | Command | Steps |
|
|
48
|
+
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
|
|
49
|
+
| List users | hcloud IAM KeystoneListUsers --cli-region=<r> | references/quick-ref.md |
|
|
50
|
+
| Create group | hcloud IAM CreateGroupV5 --group_name=<name> | references/quick-ref.md |
|
|
51
|
+
| Create custom policy | hcloud IAM CreateCloudServiceCustomPolicy --role.display_name=<n> --role.type=<type> --role.policy.Statement.1.Effect=Allow | references/policy-examples.md |
|
|
52
|
+
| Create agency | hcloud IAM CreateAgency --agency.name=<n> --agency.domain_id=<id> --agency.trust_domain_name=<domain> | references/quick-ref.md |
|
|
53
|
+
| Get temporary credential | hcloud STS AssumeAgency --agency_urn=<urn> --agency_session_name=<n> --duration_seconds=3600 | references/quick-ref.md |
|
|
54
|
+
| Check primary vs sub-account | huaweicloud_check_account_type (or 2-call CLI fallback) | references/quick-ref.md |
|
|
55
|
+
| Attach policy to group | hcloud IAM AttachGroupPolicyV5 --group_id=<id> --policy_id=<id> | references/quick-ref.md |
|
|
54
56
|
|
|
55
57
|
## Policy Examples (Least Privilege)
|
|
56
58
|
|
|
@@ -83,6 +85,22 @@ Domain expertise for Huawei Cloud Identity and Access Management (IAM). Covers u
|
|
|
83
85
|
|
|
84
86
|
Agencies allow one service to act on behalf of another. Common use cases:
|
|
85
87
|
|
|
88
|
+
### STS 自委托需预创建(实测 #935 报告4 问题6)
|
|
89
|
+
|
|
90
|
+
生成 STS 临时凭证时报**委托不存在**时(如 `agency SELF_VERIFY not found`),需先预创建名为 `SELF_VERIFY` 的 IAM 自委托(agency)—— 活动/新环境默认**不预置**该委托:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# 1. 预创建自委托(信任本账号自身)
|
|
94
|
+
hcloud IAM CreateAgency --agency.name=SELF_VERIFY --agency.trust_domain_name=<本账号domain_name>
|
|
95
|
+
|
|
96
|
+
# 2. 授予基础权限(按需最小化)
|
|
97
|
+
hcloud IAM AssociateAgencyWithDomainPermission --agency_id=<agency-id> --role_id=<role-id>
|
|
98
|
+
|
|
99
|
+
# 3. 之后重试生成 STS 临时凭证
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
排查顺序:先 `hcloud IAM ListAgencies` 确认 `SELF_VERIFY` 是否存在,不存在再按上面步骤预创建,创建后重试 STS 调用。
|
|
103
|
+
|
|
86
104
|
### FunctionGraph Agency (VPC Access)
|
|
87
105
|
|
|
88
106
|
FunctionGraph needs an agency to access VPC resources (RDS, DCS, etc.):
|
|
@@ -104,6 +122,7 @@ Common roles for FunctionGraph: VPC Administrator, RDS Administrator, DCS Admini
|
|
|
104
122
|
|
|
105
123
|
- huaweicloud_list_operations service=IAM
|
|
106
124
|
- huaweicloud_run_readonly_command for user/policy discovery
|
|
125
|
+
- huaweicloud_check_account_type for primary/sub-account judgment
|
|
107
126
|
|
|
108
127
|
## References
|
|
109
128
|
|
|
@@ -55,6 +55,41 @@ hcloud IAM CreateLoginToken # create login token
|
|
|
55
55
|
hcloud IAM GetAccountSummaryV5 # account summary
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
## Primary vs Sub-Account Judgment
|
|
59
|
+
|
|
60
|
+
**Rule (validated against 6 live credentials, zero misjudgment): `username == domainName` → primary account; otherwise IAM sub-user.**
|
|
61
|
+
|
|
62
|
+
The primary account's operator user is named after the domain, and an IAM user cannot occupy that name (account-unique), so the rule has no false positives.
|
|
63
|
+
|
|
64
|
+
Preferred path — one MCP call:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
huaweicloud_check_account_type # optional region param, defaults to configured cli-region
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
CLI fallback (two read-only calls, no identity-policy permission required; works for both primary and sub-user credentials):
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
hcloud STS GetCallerIdentity --cli-region=<region>
|
|
74
|
+
# -> principal_urn "iam::<account_id>:user:<username>" gives username; account_id gives domainId
|
|
75
|
+
hcloud IAM KeystoneListAuthDomains
|
|
76
|
+
# -> domains[0].name gives domainName (domains[0].id must equal account_id — consistency check)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Example: `principal_urn ...:user:dingchunlei` + domain name `es_hwcddbdt_OpenCapability_team` → names differ → IAM sub-user. `...:user:keco_1919aa` + domain name `keco_1919aa` → equal → primary account.
|
|
80
|
+
|
|
81
|
+
**Do NOT use these two tempting-but-wrong rules** (both disproved in live testing):
|
|
82
|
+
|
|
83
|
+
| Wrong rule | Why it fails |
|
|
84
|
+
| --------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
85
|
+
| `ShowDomainPasswordPolicy` succeeds → primary | Permission-gated, not account-type-gated: a sub-user with IAM permissions also succeeds |
|
|
86
|
+
| `principal_id == account_id` → primary | False for primary accounts too (observed on three separate primary samples; IDs only share a prefix) |
|
|
87
|
+
|
|
88
|
+
Edge cases:
|
|
89
|
+
|
|
90
|
+
- Agency-session credentials (`principal_urn` starts with `sts::` or contains `assumed-agency`): the rule does not apply — report the identity only.
|
|
91
|
+
- `KeystoneValidateToken` looks like a one-call shortcut (its token payload has both names) but requires an existing token as `--X-Subject-Token`, which AK/SK signing does not provide.
|
|
92
|
+
|
|
58
93
|
## Security Best Practices
|
|
59
94
|
|
|
60
95
|
1. **NEVER create IAM users** — use IAM Identity Center or temporary STS tokens
|
|
@@ -294,7 +294,7 @@ fi
|
|
|
294
294
|
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:
|
|
295
295
|
|
|
296
296
|
- **Tier 1 — keyring API Key** (preferred): recent sandbox images (hd-space-ai-shell line) get `HW_DEVBRIDGE_API_KEY` auto-injected by the platform into the sandbox keyring — **zero user action, long-lived key**. Read it via `hwcloud keyring get HW_DEVBRIDGE_API_KEY 2>&1` (⚠ the value goes to **stderr** — `2>&1` is mandatory or you silently capture an empty string) and accept only values starting with `devbridge_` (`(not found)` and error text are rejected). If the first read fails, a light revive (restart the D-Bus session bus at `unix:path=/run/dbus-session`, replace a locked keyring daemon with `printf '\n' | gnome-keyring-daemon --unlock`) retries once — no package installs.
|
|
297
|
-
- **Tier 2 — AK/SK** (image builds, legacy sandboxes): image builds keep `--access-key/--secret-key`. STS credentials **must** include `--security-token "$HW_SECURITY_TOKEN"` or the gateway rejects the request with 401 `APIGW.0301`. 0.1.x binaries additionally
|
|
297
|
+
- **Tier 2 — AK/SK** (image builds, legacy sandboxes): image builds keep `--access-key/--secret-key`. STS credentials **must** include `--security-token "$HW_SECURITY_TOKEN"` or the gateway rejects the request with 401 `APIGW.0301`. **The `--huaweicloud` SSO login flag does NOT exist in sandbox devbridge 0.2.x binaries (e.g. 0.2.2)** — 文档超前于二进制:不要尝试 `--huaweicloud`,直接使用 `devbridge auth login --access-key/--secret-key`(上方脚本以 `auth login --help` 探测为准)。0.1.x binaries additionally advertised `--huaweicloud` (use it only when their help advertises it).
|
|
298
298
|
- **Tier 3 — `/tmp/hw_api_key`** (release builds, user-provided one-time): the file is written by `huaweicloud_sandbox_credentials` from the local `HW_API_KEY` env (preferred) or the `api_key` param.
|
|
299
299
|
|
|
300
300
|
`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:
|
|
@@ -324,6 +324,11 @@ devbridge auth status # separate step: a status failure must not mask the logi
|
|
|
324
324
|
- **`AKSK_SUPPORTED`** → done. The temporary AK/SK injected by `huaweicloud_sandbox_credentials` is used directly (validated against IAM before injection). If login fails, surface the CLI error — expired STS token → re-run `huaweicloud_sandbox_credentials`.
|
|
325
325
|
- **`NO_CREDENTIAL`** → first re-run `huaweicloud_sandbox_credentials` and retry (refreshes STS, most common quick fix). Still failing means an older sandbox whose image cannot auto-acquire (images are frozen at sandbox creation and cannot be upgraded in place) — guide the developer through a one-time API Key creation (wait for the key before continuing):
|
|
326
326
|
|
|
327
|
+
**沙箱凭证与 devbridge 版本差异陷阱(实测 #935 坑2/坑3)**:
|
|
328
|
+
|
|
329
|
+
- 沙箱内 devbridge 0.2.2 无技能文档所述 `--huaweicloud` 登录标志(文档超前于二进制)—— 应直接使用 `devbridge auth login --access-key/--secret-key`(`auth login --help` 探测到 `--access-key` 支持时走该分支,见上方手动等价脚本)。
|
|
330
|
+
- 沙箱内 MCP 注入的 STS 凭证为 obsutil 加密态存储:devbridge 读到密文凭证会报 `APIGW.0301`。需先上传本地 DevKit 明文凭证库(AK 20 位 / SK 40 位,通过 `huaweicloud_sandbox_credentials` 注入),再创建隧道。
|
|
331
|
+
|
|
327
332
|
1. **Why (one sentence)**: "沙箱的地址生成服务发布版构建已移除 AK/SK 登录,仅支持 API Key(镜像内置构建仍支持 AK/SK,Agent 已自动探测)。API Key 按账号管理,创建一次长期可用、所有沙箱通用。"
|
|
328
333
|
2. **Where (exact steps)**: open https://devstation.connect.huaweicloud.com/space/devbridge/apikey → 登录控制台 → 选择 DevBridge 场景 → 点击"创建"。**完整值仅在创建时展示一次,立即复制**(`devbridge_` 开头)。
|
|
329
334
|
3. **How to hand it over** (present both, recommend the first):
|
|
@@ -403,6 +408,8 @@ This eliminates the most common deployment failure — historical tunnels from p
|
|
|
403
408
|
|
|
404
409
|
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.
|
|
405
410
|
|
|
411
|
+
**沙箱无 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
|
+
|
|
406
413
|
### Step 1: Detect Framework Locally
|
|
407
414
|
|
|
408
415
|
**Always call `huaweicloud_detect_framework` first** before connecting to the sandbox. It scans the local project and returns:
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { runHcloud } from './hcloud-cli.mjs';
|
|
2
|
+
|
|
3
|
+
// Parse `hcloud STS GetCallerIdentity` stdout into structured identity fields.
|
|
4
|
+
// URN forms (see https://support.huaweicloud.com/usermanual-iam5/iam_01_1275.html):
|
|
5
|
+
// iam::<account_id>:user:<username> — user credentials (primary or IAM sub-user)
|
|
6
|
+
// sts::<account_id>::assumed-agency:<name>/… — agency session credentials (rule does not apply)
|
|
7
|
+
export function parseCallerIdentity(stdout) {
|
|
8
|
+
let parsed;
|
|
9
|
+
try {
|
|
10
|
+
parsed = typeof stdout === 'string' ? JSON.parse(stdout) : stdout;
|
|
11
|
+
} catch {
|
|
12
|
+
return { ok: false, error: 'GetCallerIdentity returned non-JSON output.' };
|
|
13
|
+
}
|
|
14
|
+
if (!parsed || typeof parsed !== 'object' || !parsed.principal_urn || !parsed.account_id) {
|
|
15
|
+
return { ok: false, error: 'GetCallerIdentity response missing principal_urn/account_id.' };
|
|
16
|
+
}
|
|
17
|
+
const principalUrn = String(parsed.principal_urn);
|
|
18
|
+
let urnType = 'unknown';
|
|
19
|
+
let username;
|
|
20
|
+
const userMatch = principalUrn.match(/^iam::[^:]+:user:(.+)$/);
|
|
21
|
+
if (userMatch) {
|
|
22
|
+
urnType = 'user';
|
|
23
|
+
username = userMatch[1];
|
|
24
|
+
} else if (/^sts::/.test(principalUrn) || /assumed-agency/.test(principalUrn)) {
|
|
25
|
+
urnType = 'agency';
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
ok: true,
|
|
29
|
+
accountId: String(parsed.account_id),
|
|
30
|
+
principalUrn,
|
|
31
|
+
principalId: parsed.principal_id ? String(parsed.principal_id) : undefined,
|
|
32
|
+
urnType,
|
|
33
|
+
username,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Parse `hcloud IAM KeystoneListAuthDomains` stdout into the domain list.
|
|
38
|
+
export function parseAuthDomains(stdout) {
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = typeof stdout === 'string' ? JSON.parse(stdout) : stdout;
|
|
42
|
+
} catch {
|
|
43
|
+
return { ok: false, error: 'KeystoneListAuthDomains returned non-JSON output.' };
|
|
44
|
+
}
|
|
45
|
+
const domains = Array.isArray(parsed?.domains) ? parsed.domains : [];
|
|
46
|
+
if (domains.length === 0) {
|
|
47
|
+
return { ok: false, error: 'IAM returned no domain information. Verify the AK/SK is valid.' };
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
ok: true,
|
|
51
|
+
domains: domains.map((d) => ({ id: String(d.id), name: String(d.name || '') })),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Judge primary vs sub-account. Rule (validated against 6 real credentials + 1 report case):
|
|
56
|
+
// username == domainName -> primary account (its operator user is named after the domain)
|
|
57
|
+
// username != domainName -> IAM sub-user (a sub-user cannot occupy the domain name)
|
|
58
|
+
// Two tempting-but-wrong rules, kept here so they are not "rediscovered":
|
|
59
|
+
// - ShowDomainPasswordPolicy is NOT primary-only (it is permission-gated; a sub-user with IAM
|
|
60
|
+
// permissions can call it) — observed live.
|
|
61
|
+
// - principal_id == account_id does NOT hold for primary accounts either — observed on three
|
|
62
|
+
// separate primary-account samples.
|
|
63
|
+
export function judgeAccountType(identity, domains) {
|
|
64
|
+
if (identity.urnType !== 'user') {
|
|
65
|
+
return {
|
|
66
|
+
ok: true,
|
|
67
|
+
applicable: false,
|
|
68
|
+
reason:
|
|
69
|
+
'Credentials are an agency session (principal_urn is not iam::…:user:…). ' +
|
|
70
|
+
'The primary/sub-account judgment applies to user credentials only.',
|
|
71
|
+
principalUrn: identity.principalUrn,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const matched = domains.domains.find((d) => d.id === identity.accountId);
|
|
75
|
+
if (!matched) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
error:
|
|
79
|
+
'Inconsistent identity response: GetCallerIdentity account_id does not match any domain ' +
|
|
80
|
+
'returned by KeystoneListAuthDomains. Retry, and re-run "npx huaweicloud-devkit auth init" if it persists.',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const isPrimaryAccount = identity.username === matched.name;
|
|
84
|
+
return {
|
|
85
|
+
ok: true,
|
|
86
|
+
applicable: true,
|
|
87
|
+
isPrimaryAccount,
|
|
88
|
+
domainId: matched.id,
|
|
89
|
+
domainName: matched.name,
|
|
90
|
+
username: identity.username,
|
|
91
|
+
userId: identity.principalId,
|
|
92
|
+
principalUrn: identity.principalUrn,
|
|
93
|
+
evidence: isPrimaryAccount
|
|
94
|
+
? `username == domainName ("${identity.username}") — the primary account's operator user is named after the domain.`
|
|
95
|
+
: `username ("${identity.username}") != domainName ("${matched.name}") — the caller is an IAM user under the domain.`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Orchestrate the two read-only hcloud calls and judge the account type.
|
|
100
|
+
export async function checkAccountType({ region } = {}) {
|
|
101
|
+
const regionArgs = region ? [`--cli-region=${region}`] : [];
|
|
102
|
+
const identityResult = await runHcloud(['STS', 'GetCallerIdentity', ...regionArgs], {
|
|
103
|
+
timeoutMs: 30000,
|
|
104
|
+
maxRetries: 0,
|
|
105
|
+
}).catch((error) => ({ ok: false, error: error.message }));
|
|
106
|
+
if (!identityResult.ok) {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
error: identityResult.error || 'STS GetCallerIdentity failed.',
|
|
110
|
+
hint: 'Re-run "npx huaweicloud-devkit auth init" (invalid/expired AK/SK is the most common cause, e.g. APIGW.0301).',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const identity = parseCallerIdentity(identityResult.stdout);
|
|
114
|
+
if (!identity.ok) {
|
|
115
|
+
return { ok: false, error: identity.error };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const domainsResult = await runHcloud(['IAM', 'KeystoneListAuthDomains'], {
|
|
119
|
+
timeoutMs: 30000,
|
|
120
|
+
maxRetries: 0,
|
|
121
|
+
}).catch((error) => ({ ok: false, error: error.message }));
|
|
122
|
+
if (!domainsResult.ok) {
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
error: domainsResult.error || 'IAM KeystoneListAuthDomains failed.',
|
|
126
|
+
hint: 'Re-run "npx huaweicloud-devkit auth init" if credentials are suspected expired.',
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const domains = parseAuthDomains(domainsResult.stdout);
|
|
130
|
+
if (!domains.ok) {
|
|
131
|
+
return { ok: false, error: domains.error };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return judgeAccountType(identity, domains);
|
|
135
|
+
}
|
|
@@ -393,6 +393,87 @@ export function resolveCredentialsWithRuntime(options = {}) {
|
|
|
393
393
|
return resolveCredentials(options);
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
+
export function resolveXAuthToken() {
|
|
397
|
+
const envToken = process.env.HW_X_AUTH_TOKEN;
|
|
398
|
+
if (present(envToken)) return envToken;
|
|
399
|
+
|
|
400
|
+
const parentCwd = getParentCwd();
|
|
401
|
+
const searchDirs = [process.env.CODEARTS_PROJECT_DIR, parentCwd, process.cwd(), homedir()];
|
|
402
|
+
for (const dir of searchDirs) {
|
|
403
|
+
if (!dir) continue;
|
|
404
|
+
const path = join(dir, '.codeartsdoer', 'mcp', 'mcp_settings.json');
|
|
405
|
+
try {
|
|
406
|
+
if (!existsSync(path)) continue;
|
|
407
|
+
const config = JSON.parse(readFileSync(path, 'utf8'));
|
|
408
|
+
const server = pickDevkitMcpServer(config?.mcpServers);
|
|
409
|
+
const token = server?.env?.HW_X_AUTH_TOKEN;
|
|
410
|
+
if (present(token)) return token;
|
|
411
|
+
} catch {}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const workPaths = [
|
|
415
|
+
join(homedir(), '.codearts', 'mcp', 'mcp_settings.json'),
|
|
416
|
+
join(homedir(), '.codeartswork', 'mcp', 'mcp_settings.json'),
|
|
417
|
+
];
|
|
418
|
+
for (const path of workPaths) {
|
|
419
|
+
try {
|
|
420
|
+
if (!existsSync(path)) continue;
|
|
421
|
+
const config = JSON.parse(readFileSync(path, 'utf8'));
|
|
422
|
+
const server = pickDevkitMcpServer(config?.mcp);
|
|
423
|
+
const token = server?.environment?.HW_X_AUTH_TOKEN;
|
|
424
|
+
if (present(token)) return token;
|
|
425
|
+
} catch {}
|
|
426
|
+
}
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function resolveXAuthTokenExpiry() {
|
|
431
|
+
const v = process.env.HW_X_AUTH_TOKEN_EXPIRES_AT;
|
|
432
|
+
if (v && typeof v === 'string' && v.trim()) {
|
|
433
|
+
const asNum = Number(v.trim());
|
|
434
|
+
if (Number.isFinite(asNum) && asNum > 0) return asNum > 1e12 ? asNum : asNum * 1000;
|
|
435
|
+
const t = Date.parse(v.trim());
|
|
436
|
+
if (Number.isFinite(t)) return t;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Fallback: 未注入 HW_X_AUTH_TOKEN_EXPIRES_AT 时,从 token 字符串自解析过期时间。
|
|
440
|
+
// X-Auth-Token 是 CMS/PKCS#7 SignedData(base64 DER),其 eContent 内嵌明文 JSON
|
|
441
|
+
// (含 token.expires_at);base64 解码 + 提取 {"token" JSON 即可读出,无需依赖注入方显式给过期时间。
|
|
442
|
+
const token = resolveXAuthToken();
|
|
443
|
+
if (!token) return null;
|
|
444
|
+
return parseXAuthTokenExpiry(token);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function parseXAuthTokenExpiry(token) {
|
|
448
|
+
try {
|
|
449
|
+
const pad = token + '='.repeat((4 - (token.length % 4)) % 4);
|
|
450
|
+
const text = Buffer.from(pad.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8');
|
|
451
|
+
const idx = text.indexOf('{"token"');
|
|
452
|
+
if (idx < 0) return null;
|
|
453
|
+
const tail = text.slice(idx);
|
|
454
|
+
let depth = 0;
|
|
455
|
+
let end = -1;
|
|
456
|
+
for (let i = 0; i < tail.length; i += 1) {
|
|
457
|
+
if (tail[i] === '{') depth += 1;
|
|
458
|
+
else if (tail[i] === '}') {
|
|
459
|
+
depth -= 1;
|
|
460
|
+
if (depth === 0) {
|
|
461
|
+
end = i + 1;
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (end < 0) return null;
|
|
467
|
+
const obj = JSON.parse(tail.slice(0, end));
|
|
468
|
+
const expiresAt = obj?.token?.expires_at;
|
|
469
|
+
if (!expiresAt || typeof expiresAt !== 'string') return null;
|
|
470
|
+
const t = Date.parse(expiresAt);
|
|
471
|
+
return Number.isFinite(t) ? t : null;
|
|
472
|
+
} catch {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
396
477
|
export function lastSyncPath() {
|
|
397
478
|
return join(baseHome(), '.config', 'huaweicloud', '.last_sync');
|
|
398
479
|
}
|