direxio-deployer 0.1.3 → 0.1.5

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
@@ -19,14 +19,14 @@
19
19
 
20
20
  ## Skill Installation And Updates
21
21
 
22
- Install the deployer skill from npm, then place it into the current agent runtime's skill directory. Project-local installs are preferred because they keep the deployment skill scoped to the workspace that uses it.
22
+ Install the deployer skill from npm, then place it into the current agent runtime's skill directory. The default install is global for the selected agent runtime. Use a project-local install only when you explicitly want the skill copied into a specific repository or workspace.
23
23
 
24
24
  The GitHub repository keeps tests for maintainers and CI, but the published npm package and installed skill copy exclude `tests/` to keep user installs small.
25
25
 
26
- If you want Codex to install and deploy in one instruction, do not say "install skills <GitHub URL>". That triggers GitHub skill installation and can place the skill in the global `~/.codex` directory. Use a short instruction that tells the agent to read this README's npm/project-local install rule:
26
+ If you want Codex to install and deploy in one instruction, do not say "install skills <GitHub URL>". That triggers GitHub skill installation instead of the npm-managed installer. Use a short instruction that gives the repository address for reading only and tells the agent to follow the README's npm install rule:
27
27
 
28
28
  ```text
29
- Follow the direxio-deployer README npm/project-local install rule, then deploy Direxio with domain __DOMAIN__.
29
+ Read https://github.com/YingSuiAI/direxio-deployer README and follow its npm install rule, then deploy Direxio with domain __DOMAIN__.
30
30
  ```
31
31
 
32
32
  After reading this instruction, the agent should run the npm install commands below; it should not switch to a GitHub skill installer.
@@ -35,34 +35,34 @@ POSIX shells:
35
35
 
36
36
  ```bash
37
37
  npm install -g direxio-deployer@latest
38
- direxio-deployer skill install --agent codex --scope project --project .
38
+ direxio-deployer skill install --agent codex
39
39
  ```
40
40
 
41
41
  Windows PowerShell:
42
42
 
43
43
  ```powershell
44
44
  npm install -g direxio-deployer@latest
45
- direxio-deployer skill install --agent codex --scope project --project .
45
+ direxio-deployer skill install --agent codex
46
46
  ```
47
47
 
48
48
  Update the installed skill with the same host runtime:
49
49
 
50
50
  ```bash
51
51
  npm install -g direxio-deployer@latest
52
- direxio-deployer skill update --agent codex --scope project --project .
52
+ direxio-deployer skill update --agent codex
53
53
  ```
54
54
 
55
- Use the matching agent name for your runtime: `codex`, `claudecode`, `gemini`, `cursor`, `copilot`, `openclaw`, `hermes`, `opencode`, `qoder`, `reasonix`, or another target listed in `references/agent-targets.md`. Use `--scope global` only when you intentionally want a host-level skill install:
55
+ Use the matching agent name for your runtime: `codex`, `claudecode`, `gemini`, `cursor`, `copilot`, `openclaw`, `hermes`, `opencode`, `qoder`, `reasonix`, or another target listed in `references/agent-targets.md`. Add `--scope project --project <path>` only when you intentionally want a repository-local skill install:
56
56
 
57
57
  ```bash
58
- direxio-deployer skill install --agent codex --scope global
58
+ direxio-deployer skill install --agent codex --scope project --project .
59
59
  ```
60
60
 
61
61
  The installer writes `.direxio-skill-install.json` into the target directory and refuses to overwrite unmanaged existing content unless `--force` is provided. To pin a version, install that package version first:
62
62
 
63
63
  ```bash
64
- npm install -g direxio-deployer@0.1.3
65
- direxio-deployer skill update --agent codex --scope project --project .
64
+ npm install -g direxio-deployer@0.1.4
65
+ direxio-deployer skill update --agent codex
66
66
  ```
67
67
 
68
68
  The CLI is implemented in Node and uses native paths for the host it runs on. On Windows it writes Windows-compatible paths; on Linux, macOS, Git Bash, or WSL it writes paths for that runtime.
package/README_zh.md CHANGED
@@ -17,14 +17,14 @@
17
17
 
18
18
  ## Skill 安装和更新
19
19
 
20
- 通过 npm 安装 deployer skill,再把它写入当前智能体运行时的 skill 目录。默认推荐 project-local 安装,让部署 skill 跟随当前 workspace。
20
+ 通过 npm 安装 deployer skill,再把它写入当前智能体运行时的 skill 目录。默认安装到所选智能体运行时的全局 skill 目录;只有在明确希望跟随某个仓库或 workspace 时,才指定 project-local 安装。
21
21
 
22
22
  GitHub 仓库保留测试用于维护和 CI,但发布到 npm 的包以及安装到智能体 skill 目录的副本不包含 `tests/`,以减小用户安装体积。
23
23
 
24
- 如果你想让 Codex 一句话安装并开始部署,不要说“安装 skills <GitHub 链接>”。那会触发 GitHub skill 安装器,容易安装到全局 `~/.codex`。推荐只说短句,让 agent 先读本 README 中的 npm/project-local 安装规则:
24
+ 如果你想让 Codex 一句话安装并开始部署,不要说“安装 skills <GitHub 链接>”。那会触发 GitHub skill 安装器,而不是 npm 管理的安装器。推荐只说短句,把仓库地址作为读取 README 的位置,并让 agent README 中的 npm 安装规则执行:
25
25
 
26
26
  ```text
27
- 请按 direxio-deployer README npm/project-local 规则安装 skill,然后部署 Direxio,域名 __DOMAIN__。
27
+ 请阅读 https://github.com/YingSuiAI/direxio-deployer README,并按其中 npm 安装规则安装 skill,然后部署 Direxio,域名 __DOMAIN__。
28
28
  ```
29
29
 
30
30
  Agent 读到这句后应执行下方 npm 安装命令;不要改用 GitHub skill installer。
@@ -33,34 +33,34 @@ POSIX shell:
33
33
 
34
34
  ```bash
35
35
  npm install -g direxio-deployer@latest
36
- direxio-deployer skill install --agent codex --scope project --project .
36
+ direxio-deployer skill install --agent codex
37
37
  ```
38
38
 
39
39
  Windows PowerShell:
40
40
 
41
41
  ```powershell
42
42
  npm install -g direxio-deployer@latest
43
- direxio-deployer skill install --agent codex --scope project --project .
43
+ direxio-deployer skill install --agent codex
44
44
  ```
45
45
 
46
46
  在同一个宿主运行时中更新已安装 skill:
47
47
 
48
48
  ```bash
49
49
  npm install -g direxio-deployer@latest
50
- direxio-deployer skill update --agent codex --scope project --project .
50
+ direxio-deployer skill update --agent codex
51
51
  ```
52
52
 
53
- 根据当前运行时替换 agent 名称:`codex`、`claudecode`、`gemini`、`cursor`、`copilot`、`openclaw`、`hermes`、`opencode`、`qoder`、`reasonix`,或使用 `references/agent-targets.md` 中列出的其他目标。只有明确想安装到宿主级目录时才使用 `--scope global`:
53
+ 根据当前运行时替换 agent 名称:`codex`、`claudecode`、`gemini`、`cursor`、`copilot`、`openclaw`、`hermes`、`opencode`、`qoder`、`reasonix`,或使用 `references/agent-targets.md` 中列出的其他目标。只有明确想安装到某个项目目录时才使用 `--scope project --project <path>`:
54
54
 
55
55
  ```bash
56
- direxio-deployer skill install --agent codex --scope global
56
+ direxio-deployer skill install --agent codex --scope project --project .
57
57
  ```
58
58
 
59
59
  安装器会在目标目录写入 `.direxio-skill-install.json`,并拒绝覆盖没有该 manifest 的既有目录,除非显式传入 `--force`。如需固定版本,先安装指定 npm 版本:
60
60
 
61
61
  ```bash
62
- npm install -g direxio-deployer@0.1.3
63
- direxio-deployer skill update --agent codex --scope project --project .
62
+ npm install -g direxio-deployer@0.1.4
63
+ direxio-deployer skill update --agent codex
64
64
  ```
65
65
 
66
66
  这个 CLI 由 Node 实现,并使用当前宿主的原生路径。Windows 下写入 Windows 路径;Linux、macOS、Git Bash 或 WSL 下写入对应运行时能读取的路径。
package/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: direxio-deployer
3
- description: Deploy, resume, verify, destroy, and locally wire a production P2P-IM Matrix server on AWS for any connent/connect-supported local agent runtime. Use when installing or updating this skill itself; install the versioned npm package `direxio-deployer` and use its CLI to place the skill in the runtime-specific project-local path from references/agent-targets.md unless the user explicitly asks for a global installation.
3
+ description: Deploy, resume, verify, destroy, and locally wire a production P2P-IM Matrix server on AWS for any connent/connect-supported local agent runtime. Use when installing or updating this skill itself; install the versioned npm package `direxio-deployer` and use its CLI to place the skill in the runtime-specific global path from references/agent-targets.md unless the user explicitly asks for a project-local installation.
4
4
  ---
5
5
 
6
6
  # Direxio Deployer
@@ -24,13 +24,17 @@ against the versioned npm package:
24
24
 
25
25
  ```bash
26
26
  npm install -g direxio-deployer@latest
27
- direxio-deployer skill refresh --agent <runtime> --scope project --project <project-root>
27
+ direxio-deployer skill refresh --agent <runtime>
28
28
  ```
29
29
 
30
30
  Use the current runtime name for `<runtime>` when known, such as `codex`,
31
- `claudecode`, `gemini`, `cursor`, `openclaw`, or `hermes`. Prefer project scope
32
- when a project or workspace root exists. Use `--scope global` only when the
33
- user explicitly asks for a global installation or no project target exists.
31
+ `claudecode`, `gemini`, `cursor`, `openclaw`, or `hermes`. Use project scope
32
+ only when the user explicitly asks for a repository-local install or provides
33
+ a project root for that purpose:
34
+
35
+ ```bash
36
+ direxio-deployer skill refresh --agent <runtime> --scope project --project <project-root>
37
+ ```
34
38
 
35
39
  `direxio-deployer skill refresh` checks the latest npm version, updates the
36
40
  global CLI when npm reports a newer package, and refreshes the managed skill
@@ -261,6 +265,19 @@ Step-by-step onboarding flow:
261
265
  credits only apply when the account, plan, region, and service usage are
262
266
  eligible. Recommend setting an AWS Budget or billing alert before leaving
263
267
  the node running.
268
+ - **Before asking for the final deployment confirmation**, run the S1
269
+ preflight or equivalent AWS CLI checks for the selected region so hard
270
+ capacity blockers are known up front. In particular, check EC2-VPC Elastic IP quota because each deployment needs one fixed public IP:
271
+
272
+ ```bash
273
+ aws service-quotas get-service-quota --service-code ec2 --quota-code L-0263D0A3 --query 'Quota.Value' --output text
274
+ aws ec2 describe-addresses --query 'length(Addresses[?Domain==`vpc`])' --output text
275
+ ```
276
+
277
+ If allocated Elastic IPs are already greater than or equal to quota, stop
278
+ before confirmation and tell the user to release an unused Elastic IP,
279
+ request quota, or choose another region. Do not wait until S3 allocation
280
+ fails on the live deployment path.
264
281
  - If the user asks what is billed, mention EC2/server, fixed public IPv4 or
265
282
  Elastic IP, storage, DNS, network traffic, and call relay traffic.
266
283
 
@@ -300,24 +317,25 @@ When the user asks to install or update this skill itself, or asks to wire Direx
300
317
  If the user says "install skills" and includes the GitHub repository URL
301
318
  `YingSuiAI/direxio-deployer`, do not use a generic GitHub skill installer for
302
319
  normal deployment use. First install the npm package and then run
303
- `direxio-deployer skill install --agent <runtime> --scope project --project
304
- <project-root>`. Use a Git clone only when the user explicitly asks for
320
+ `direxio-deployer skill install --agent <runtime>`. Use a Git clone only when the user explicitly asks for
305
321
  deployer development or local patching.
306
322
 
307
- For this skill repository itself, first determine whether the current working directory belongs to a project or workspace. Treat an explicit workspace root, project files, or an existing agent-specific directory such as `.codex/`, `.claude/`, `.gemini/`, `.cursor/`, `.github/copilot/`, `.devin/`, `.opencode/`, `.qoder/`, `.pi/`, `.openclaw/`, or `.hermes/` as a project target.
308
-
309
- If a project target exists, install or update this skill with the versioned npm CLI at the runtime-specific project-local path from `references/agent-targets.md`:
323
+ Install or update this skill with the versioned npm CLI at the runtime-specific
324
+ global path from `references/agent-targets.md`:
310
325
 
311
326
  ```bash
312
327
  npm install -g direxio-deployer@latest
313
- direxio-deployer skill install --agent <runtime> --scope project --project <project-root>
314
- direxio-deployer skill update --agent <runtime> --scope project --project <project-root>
328
+ direxio-deployer skill install --agent <runtime>
329
+ direxio-deployer skill update --agent <runtime>
315
330
  ```
316
331
 
317
- The installer writes `.direxio-skill-install.json` into the target directory and refuses to overwrite unmanaged existing content unless the operator explicitly uses `--force`. Use global runtime skill directories only when the user explicitly asks for a global install or no project target exists:
332
+ The installer writes `.direxio-skill-install.json` into the target directory
333
+ and refuses to overwrite unmanaged existing content unless the operator
334
+ explicitly uses `--force`. Use project-local runtime skill directories only
335
+ when the user explicitly asks to install into a repository or workspace:
318
336
 
319
337
  ```bash
320
- direxio-deployer skill install --agent <runtime> --scope global
338
+ direxio-deployer skill install --agent <runtime> --scope project --project <project-root>
321
339
  ```
322
340
 
323
341
  Use a Git clone only for development or local patching of this deployer, not as the normal end-user installation path.
@@ -355,7 +373,7 @@ The local MCP tool surface is `direxio-mcp`, installed from `direxio-mcp@latest`
355
373
 
356
374
  `DIREXIO_AGENT_PLATFORM` describes the host runtime following the skill, while `DIREXIO_CC_CONNECT_AGENT` describes the local agent backend that `direxio-connect` should launch. Host runtimes such as Hermes or OpenClaw are not native cc-connect backend types; S6 maps them to the generic ACP backend by default and records `cc_connect_agent=acp`. Override `DIREXIO_CC_CONNECT_AGENT` only when the operator intentionally wants a different local backend.
357
375
 
358
- `DIREXIO_AGENT_INSTALL` may be `skip`, `recommend`, or `auto`. Only `auto` attempts to run `npm install -g direxio-connent@latest` and `direxio-connect daemon install --config ~/.direxio/nodes/<service_id>/cc-connect/config.toml --service-name <service_id> --force`; the default `recommend` records and prints the command without mutating local daemon state. An automatic install is reported as installed only when `direxio-connect daemon status --service-name <service_id>` returns `Status: Running` and recent daemon logs do not show ACP session initialization failure; otherwise S6 records `agent_install_status=install_failed`. S6 calls `agent.matrix_session.create` with `agent_token` and retries transient HTTP 000/5xx responses before failing, because the Matrix action can become reachable a few seconds after `/healthz`.
376
+ `DIREXIO_AGENT_INSTALL` may be `skip`, `recommend`, or `auto`. Only `auto` attempts to run `npm install -g direxio-connent@latest` and `direxio-connect daemon install --config ~/.direxio/nodes/<service_id>/cc-connect/config.toml --service-name <service_id> --force`; the default `recommend` records and prints the command without mutating local daemon state. An automatic install is reported as installed only when `direxio-connect daemon status --service-name <service_id>` returns `Status: Running` and recent daemon logs do not show ACP session initialization failure; otherwise S6 records `agent_install_status=install_failed`. S6 calls `agent.matrix_session.create` with `agent_token` and retries transient HTTP 000/404/408/409/425/429/5xx responses before failing, because the Matrix action can become reachable after `/healthz`; defaults are 12 attempts with exponential backoff capped by `DIREXIO_MATRIX_SESSION_RETRY_MAX_INTERVAL`.
359
377
 
360
378
  Voice input is supported through `direxio-connect` speech-to-text. When `DIREXIO_SPEECH_API_KEY` or a provider-specific key such as `DIREXIO_SPEECH_QWEN_API_KEY`, `OPENAI_API_KEY`, `GROQ_API_KEY`, `DASHSCOPE_API_KEY`, `GEMINI_API_KEY`, or `GOOGLE_API_KEY` is present, S6 writes `[speech] enabled = true` into the generated config. Without an STT key, do not claim voice input is enabled.
361
379
 
@@ -94,14 +94,14 @@ function main() {
94
94
 
95
95
  function usage(exitCode) {
96
96
  const output = `Usage:
97
- direxio-deployer skill install --agent <runtime> [--scope project|global] [--project <path>]
98
- direxio-deployer skill update --agent <runtime> [--scope project|global] [--project <path>]
99
- direxio-deployer skill refresh --agent <runtime> [--scope project|global] [--project <path>]
97
+ direxio-deployer skill install --agent <runtime> [--scope global|project] [--project <path>]
98
+ direxio-deployer skill update --agent <runtime> [--scope global|project] [--project <path>]
99
+ direxio-deployer skill refresh --agent <runtime> [--scope global|project] [--project <path>]
100
100
 
101
101
  Options:
102
102
  --agent <runtime> Target agent runtime. Default: codex
103
- --scope <scope> project or global. Default: project
104
- --project <path> Project root for project installs. Default: current directory
103
+ --scope <scope> global or project. Default: global
104
+ --project <path> Project root for explicit project installs. Default: current directory
105
105
  --target <path> Explicit install target override
106
106
  --home <path> Home directory override for global installs
107
107
  --dry-run Resolve and print without writing
@@ -115,7 +115,7 @@ Options:
115
115
  function parseArgs(args) {
116
116
  const options = {
117
117
  agent: "codex",
118
- scope: "project",
118
+ scope: "global",
119
119
  project: process.cwd(),
120
120
  home: homedir(),
121
121
  target: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "direxio-deployer",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Versioned Direxio deployer agent skill and portable deployment orchestration tools.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,7 @@
18
18
  "scripts/"
19
19
  ],
20
20
  "scripts": {
21
- "test": "bash tests/npm_skill_distribution_test.sh && bash tests/skill_structure_test.sh && bash tests/private_file_permissions_test.sh && bash tests/s6_wire_local_test.sh && bash tests/render_userdata_remote_nodes_test.sh"
21
+ "test": "bash tests/npm_skill_distribution_test.sh && bash tests/skill_structure_test.sh && bash tests/private_file_permissions_test.sh && bash tests/eip_preflight_test.sh && bash tests/mcp_tools_runtime_check_test.sh && bash tests/runtime_summary_check_test.sh && bash tests/s6_wire_local_test.sh && bash tests/render_userdata_remote_nodes_test.sh"
22
22
  },
23
23
  "engines": {
24
24
  "node": ">=18"
@@ -4,52 +4,52 @@ Use this file when installing or updating this skill and when reviewing S6 local
4
4
 
5
5
  ## Npm Skill Installation
6
6
 
7
- Prefer a project-local npm-managed install when a project or workspace exists. Install the versioned package, then let the CLI copy the skill bundle into the runtime-specific target:
7
+ Prefer the npm-managed global install for normal users. Install the versioned package, then let the CLI copy the skill bundle into the selected runtime's host-level skill directory:
8
8
 
9
- Do not use a generic "install skills <GitHub URL>" instruction for normal users. That can invoke a host's GitHub skill installer and place this repository under the global runtime directory before the npm-managed installer runs. A short user prompt should point the agent back to this npm/project-local rule:
9
+ Do not use a generic "install skills <GitHub URL>" instruction for normal users. That can invoke a host's GitHub skill installer instead of the npm-managed installer. A short user prompt should give the repository URL for reading only and point the agent back to this npm install rule:
10
10
 
11
11
  ```text
12
- Follow the direxio-deployer README npm/project-local install rule, then deploy Direxio with domain __DOMAIN__.
12
+ Read https://github.com/YingSuiAI/direxio-deployer README and follow its npm install rule, then deploy Direxio with domain __DOMAIN__.
13
13
  ```
14
14
 
15
15
  POSIX shells:
16
16
 
17
17
  ```bash
18
18
  npm install -g direxio-deployer@latest
19
- direxio-deployer skill install --agent codex --scope project --project PROJECT_ROOT
20
- direxio-deployer skill update --agent codex --scope project --project PROJECT_ROOT
19
+ direxio-deployer skill install --agent codex
20
+ direxio-deployer skill update --agent codex
21
21
  ```
22
22
 
23
23
  Windows PowerShell:
24
24
 
25
25
  ```powershell
26
26
  npm install -g direxio-deployer@latest
27
- direxio-deployer skill install --agent codex --scope project --project PROJECT_ROOT
28
- direxio-deployer skill update --agent codex --scope project --project PROJECT_ROOT
27
+ direxio-deployer skill install --agent codex
28
+ direxio-deployer skill update --agent codex
29
29
  ```
30
30
 
31
- Use `--scope global` only when the user explicitly asks for a global install or no project target exists. Use a Git clone only for deployer development or local patching, not as the normal end-user installation path. The npm installer writes `.direxio-skill-install.json` and refuses to overwrite unmanaged existing target directories unless `--force` is passed.
31
+ Use `--scope project --project PROJECT_ROOT` only when the user explicitly asks for a repository-local install. Use a Git clone only for deployer development or local patching, not as the normal end-user installation path. The npm installer writes `.direxio-skill-install.json` and refuses to overwrite unmanaged existing target directories unless `--force` is passed.
32
32
 
33
- | Runtime | Project-local skill target | Global fallback only when explicitly requested or no project exists |
33
+ | Runtime | Default global skill target | Explicit project-local skill target |
34
34
  | --- | --- | --- |
35
- | Codex | `PROJECT_ROOT/.codex/skills/direxio-deployer` | `${CODEX_HOME:-$HOME/.codex}/skills/direxio-deployer` |
36
- | Claude Code | `PROJECT_ROOT/.claude/skills/direxio-deployer` | `${CLAUDE_HOME:-$HOME/.claude}/skills/direxio-deployer` |
37
- | Gemini | `PROJECT_ROOT/.gemini/skills/direxio-deployer` | `${GEMINI_HOME:-$HOME/.gemini}/skills/direxio-deployer` |
38
- | Cursor | `PROJECT_ROOT/.cursor/skills/direxio-deployer` | `${CURSOR_HOME:-$HOME/.cursor}/skills/direxio-deployer` |
39
- | GitHub Copilot | `PROJECT_ROOT/.github/copilot/skills/direxio-deployer` | `$HOME/.github/copilot/skills/direxio-deployer` |
40
- | OpenClaw | `PROJECT_ROOT/.openclaw/skills/direxio-deployer` | `${OPENCLAW_HOME:-$HOME/.openclaw}/skills/direxio-deployer` |
41
- | Hermes | `PROJECT_ROOT/.hermes/skills/direxio-deployer` | `${HERMES_HOME:-$HOME/.hermes}/skills/direxio-deployer` |
42
- | ACP-compatible | `PROJECT_ROOT/.agents/skills/direxio-deployer` | `$HOME/.agents/skills/direxio-deployer` |
43
- | Antigravity | `PROJECT_ROOT/.antigravity/skills/direxio-deployer` | `${ANTIGRAVITY_HOME:-$HOME/.antigravity}/skills/direxio-deployer` |
44
- | Devin | `PROJECT_ROOT/.devin/skills/direxio-deployer` | `${DEVIN_HOME:-$HOME/.devin}/skills/direxio-deployer` |
45
- | iFlow | `PROJECT_ROOT/.iflow/skills/direxio-deployer` | `${IFLOW_HOME:-$HOME/.iflow}/skills/direxio-deployer` |
46
- | Kimi | `PROJECT_ROOT/.kimi/skills/direxio-deployer` | `${KIMI_HOME:-$HOME/.kimi}/skills/direxio-deployer` |
47
- | OpenCode | `PROJECT_ROOT/.opencode/skills/direxio-deployer` | `${OPENCODE_HOME:-$HOME/.opencode}/skills/direxio-deployer` |
48
- | Pi | `PROJECT_ROOT/.pi/agent/skills/direxio-deployer` | `${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}/skills/direxio-deployer` |
49
- | Qoder | `PROJECT_ROOT/.qoder/skills/direxio-deployer` | `${QODER_HOME:-$HOME/.qoder}/skills/direxio-deployer` |
50
- | Reasonix | `PROJECT_ROOT/.reasonix/skills/direxio-deployer` | `${REASONIX_HOME:-$HOME/.reasonix}/skills/direxio-deployer` |
51
- | tmux | `PROJECT_ROOT/.agent/skills/direxio-deployer` | `$HOME/.agent/skills/direxio-deployer` |
52
- | Generic or unknown | `PROJECT_ROOT/.agent/skills/direxio-deployer` | `$HOME/.agent/skills/direxio-deployer` |
35
+ | Codex | `${CODEX_HOME:-$HOME/.codex}/skills/direxio-deployer` | `PROJECT_ROOT/.codex/skills/direxio-deployer` |
36
+ | Claude Code | `${CLAUDE_HOME:-$HOME/.claude}/skills/direxio-deployer` | `PROJECT_ROOT/.claude/skills/direxio-deployer` |
37
+ | Gemini | `${GEMINI_HOME:-$HOME/.gemini}/skills/direxio-deployer` | `PROJECT_ROOT/.gemini/skills/direxio-deployer` |
38
+ | Cursor | `${CURSOR_HOME:-$HOME/.cursor}/skills/direxio-deployer` | `PROJECT_ROOT/.cursor/skills/direxio-deployer` |
39
+ | GitHub Copilot | `$HOME/.github/copilot/skills/direxio-deployer` | `PROJECT_ROOT/.github/copilot/skills/direxio-deployer` |
40
+ | OpenClaw | `${OPENCLAW_HOME:-$HOME/.openclaw}/skills/direxio-deployer` | `PROJECT_ROOT/.openclaw/skills/direxio-deployer` |
41
+ | Hermes | `${HERMES_HOME:-$HOME/.hermes}/skills/direxio-deployer` | `PROJECT_ROOT/.hermes/skills/direxio-deployer` |
42
+ | ACP-compatible | `$HOME/.agents/skills/direxio-deployer` | `PROJECT_ROOT/.agents/skills/direxio-deployer` |
43
+ | Antigravity | `${ANTIGRAVITY_HOME:-$HOME/.antigravity}/skills/direxio-deployer` | `PROJECT_ROOT/.antigravity/skills/direxio-deployer` |
44
+ | Devin | `${DEVIN_HOME:-$HOME/.devin}/skills/direxio-deployer` | `PROJECT_ROOT/.devin/skills/direxio-deployer` |
45
+ | iFlow | `${IFLOW_HOME:-$HOME/.iflow}/skills/direxio-deployer` | `PROJECT_ROOT/.iflow/skills/direxio-deployer` |
46
+ | Kimi | `${KIMI_HOME:-$HOME/.kimi}/skills/direxio-deployer` | `PROJECT_ROOT/.kimi/skills/direxio-deployer` |
47
+ | OpenCode | `${OPENCODE_HOME:-$HOME/.opencode}/skills/direxio-deployer` | `PROJECT_ROOT/.opencode/skills/direxio-deployer` |
48
+ | Pi | `${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}/skills/direxio-deployer` | `PROJECT_ROOT/.pi/agent/skills/direxio-deployer` |
49
+ | Qoder | `${QODER_HOME:-$HOME/.qoder}/skills/direxio-deployer` | `PROJECT_ROOT/.qoder/skills/direxio-deployer` |
50
+ | Reasonix | `${REASONIX_HOME:-$HOME/.reasonix}/skills/direxio-deployer` | `PROJECT_ROOT/.reasonix/skills/direxio-deployer` |
51
+ | tmux | `$HOME/.agent/skills/direxio-deployer` | `PROJECT_ROOT/.agent/skills/direxio-deployer` |
52
+ | Generic or unknown | `$HOME/.agent/skills/direxio-deployer` | `PROJECT_ROOT/.agent/skills/direxio-deployer` |
53
53
 
54
54
  ## Direxio Connect Target
55
55
 
@@ -4,11 +4,24 @@
4
4
 
5
5
  1. Confirm `DOMAIN`, `DOMAIN_MODE`, and `CONFIRM_DOMAIN_BINDING=1`.
6
6
  2. Confirm AWS region, credentials, billing, instance type, and costs.
7
- 3. Check dependencies with the OS-specific commands in `tooling.md`.
8
- 4. Check current DNS provider. Prefer `DOMAIN_MODE=route53` when the user
7
+ 3. Before asking for final deployment confirmation, check regional hard blockers
8
+ that can be read without creating resources: default VPC, EC2 vCPU quota,
9
+ Elastic IP quota/current allocation, and Ubuntu AMI availability. The S1
10
+ preflight performs these checks; for a manual EIP check, compare:
11
+
12
+ ```bash
13
+ aws service-quotas get-service-quota --service-code ec2 --quota-code L-0263D0A3 --query 'Quota.Value' --output text
14
+ aws ec2 describe-addresses --query 'length(Addresses[?Domain==`vpc`])' --output text
15
+ ```
16
+
17
+ If the selected region has no available Elastic IP capacity, stop before
18
+ confirmation and ask the user to release an unused Elastic IP, request a
19
+ higher EC2-VPC Elastic IP quota, or choose another region.
20
+ 4. Check dependencies with the OS-specific commands in `tooling.md`.
21
+ 5. Check current DNS provider. Prefer `DOMAIN_MODE=route53` when the user
9
22
  confirms AWS may manage the hosted zone and A record. Use `DOMAIN_MODE=user`
10
23
  only as a fallback when no DNS provider automation is available.
11
- 5. Check state:
24
+ 6. Check state:
12
25
 
13
26
  ```bash
14
27
  bash scripts/orchestrate.sh status
@@ -74,7 +74,7 @@ DIREXIO_CREDENTIALS_FILE=~/.direxio/nodes/<service_id>/credentials.json direxio-
74
74
 
75
75
  ## cc-connect Matrix Bridge
76
76
 
77
- S6 calls `agent.matrix_session.create` with the backend `agent_token`, not the owner `access_token`. Current message-server builds must return a Matrix session for `@agent:<server>`, not for `@owner:<server>`. S6 retries transient HTTP 000/5xx responses before failing, because the Matrix action can become reachable shortly after `/healthz`. The resulting session is stored at:
77
+ S6 calls `agent.matrix_session.create` with the backend `agent_token`, not the owner `access_token`. Current message-server builds must return a Matrix session for `@agent:<server>`, not for `@owner:<server>`. S6 retries transient HTTP 000/404/408/409/425/429/5xx responses before failing, because the Matrix action can become reachable after `/healthz`; defaults are 12 attempts with exponential backoff capped by `DIREXIO_MATRIX_SESSION_RETRY_MAX_INTERVAL`. The resulting session is stored at:
78
78
 
79
79
  ```text
80
80
  ~/.direxio/nodes/<service_id>/cc-connect/matrix-session.json
@@ -5,7 +5,7 @@
5
5
  ## 阶段
6
6
 
7
7
  - **S0_PREREQ_AWS**: 校验 AWS CLI、凭据和账号身份。
8
- - **S1_PREFLIGHT**: 校验 region、默认 VPC、vCPU 配额、Ubuntu amd64 AMI。
8
+ - **S1_PREFLIGHT**: 校验 region、默认 VPC、vCPU 配额、Elastic IP 可用配额、Ubuntu amd64 AMI。
9
9
  - **S2_DOMAIN**: 确认正式长期域名和 Matrix `server_name` 不可逆绑定。
10
10
  - **S3_PROVISION**: 创建 EC2、密钥对、安全组、Elastic IP,按 DNS 模式处理 Route53 hosted zone/A 记录或等待外部 DNS,渲染 cloud-init。默认镜像 `MESSAGE_SERVER_IMAGE=direxio/message-server:latest`。
11
11
  - **S4_BOOTSTRAP_STACK**: 等 cloud-init 安装 Docker 并启动 `postgres:18 + message-server + caddy + coturn`,轮询 `https://<domain>/healthz`。
@@ -109,7 +109,7 @@ Use the Git Bash `$HOME` path for files generated by the deployer. If running `d
109
109
 
110
110
  ## EC2 SSH Key Paths
111
111
 
112
- SSH key files are written with Windows-compatible paths such as `C:/Users/.../.direxio/deploy/p2p-*.pem`. The deployer removes inherited Windows ACLs where possible so OpenSSH does not reject the private key as too open. The SSH command printed in the delivery summary works in Git Bash. If using PowerShell or cmd, convert forward slashes to backslashes.
112
+ SSH key files are written with Windows-compatible paths such as `C:/Users/.../.direxio/deploy/p2p-*.pem`. The deployer removes broad Windows ACL entries such as `Users`, `Authenticated Users`, `Everyone`, and Codex sandbox groups where possible, while keeping the current Windows user, `SYSTEM`, and `Administrators` on the file so OpenSSH does not reject the private key as too open. The SSH command printed in the delivery summary works in Git Bash. If using PowerShell or cmd, convert forward slashes to backslashes.
113
113
 
114
114
  ## Verifying Deployment
115
115
 
@@ -53,6 +53,9 @@ aws_redact_arn() {
53
53
  # EC2 vCPU quota code: Running On-Demand Standard instances.
54
54
  EC2_STD_QUOTA_CODE="L-1216C47A"
55
55
 
56
+ # EC2-VPC Elastic IP addresses per region.
57
+ EC2_VPC_EIP_QUOTA_CODE="L-0263D0A3"
58
+
56
59
  # Dynamically resolve the latest Ubuntu 22.04 amd64 AMI; never hard-code AMI IDs.
57
60
  aws_lookup_ubuntu_ami() {
58
61
  local ami
@@ -57,17 +57,35 @@ restrict_private_file() {
57
57
  if command -v cygpath >/dev/null 2>&1; then
58
58
  win_file=$(cygpath -w "$file")
59
59
  fi
60
- user=$(cmd.exe /c whoami 2>/dev/null | tr -d '\r' | tail -n 1 || true)
60
+ user=$(_windows_current_user)
61
61
  user_domain=${USERDOMAIN:-}
62
- icacls "$win_file" /inheritance:r >/dev/null 2>&1 || true
63
- icacls "$win_file" /remove:g \
62
+ MSYS2_ARG_CONV_EXCL='*' icacls "$win_file" /inheritance:r >/dev/null 2>&1 || true
63
+ MSYS2_ARG_CONV_EXCL='*' icacls "$win_file" /remove:g \
64
64
  "Users" "Authenticated Users" "Everyone" "CodexSandboxUsers" \
65
65
  "${user_domain}\\CodexSandboxUsers" >/dev/null 2>&1 || true
66
- [ -n "$user" ] && icacls "$win_file" /grant:r "$user:R" >/dev/null 2>&1 || true
66
+ MSYS2_ARG_CONV_EXCL='*' icacls "$win_file" /grant:r "NT AUTHORITY\\SYSTEM:F" "BUILTIN\\Administrators:F" >/dev/null 2>&1 || true
67
+ [ -n "$user" ] && MSYS2_ARG_CONV_EXCL='*' icacls "$win_file" /grant:r "$user:F" >/dev/null 2>&1 || true
67
68
  ;;
68
69
  esac
69
70
  }
70
71
 
72
+ _windows_current_user() {
73
+ if [ -n "${USERDOMAIN:-}" ] && [ -n "${USERNAME:-}" ]; then
74
+ printf '%s\\%s\n' "$USERDOMAIN" "$USERNAME"
75
+ return 0
76
+ fi
77
+ if command -v powershell.exe >/dev/null 2>&1; then
78
+ powershell.exe -NoProfile -NonInteractive -Command '[System.Security.Principal.WindowsIdentity]::GetCurrent().Name' 2>/dev/null \
79
+ | tr -d '\r' | tail -n 1
80
+ return 0
81
+ fi
82
+ if command -v whoami.exe >/dev/null 2>&1; then
83
+ whoami.exe 2>/dev/null | tr -d '\r' | tail -n 1
84
+ return 0
85
+ fi
86
+ return 0
87
+ }
88
+
71
89
  # Initialize state.json for a new deployment.
72
90
  state_init() {
73
91
  mkdir -p "$P2P_WORKDIR"
@@ -1,156 +1,92 @@
1
1
  #!/usr/bin/env node
2
- import { spawn } from "node:child_process";
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync, realpathSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
3
6
 
4
7
  const command = process.argv[2] || "direxio-mcp";
5
8
  const timeoutMs = Number.parseInt(process.env.DIREXIO_MCP_TOOLS_TIMEOUT_MS || "8000", 10);
6
9
 
7
- const child = spawn(command, {
8
- env: process.env,
9
- shell: true,
10
- stdio: ["pipe", "pipe", "pipe"]
11
- });
12
-
13
- let stdout = Buffer.alloc(0);
14
- let stderr = "";
15
- let completed = false;
16
- const responses = new Map();
17
-
18
10
  const timer = setTimeout(() => {
19
11
  finishWithError(`timed out waiting for MCP tools/list after ${timeoutMs}ms`);
20
12
  }, timeoutMs);
21
13
 
22
- child.stderr.on("data", (chunk) => {
23
- stderr += chunk.toString("utf8");
24
- });
25
-
26
- child.stdout.on("data", (chunk) => {
27
- stdout = Buffer.concat([stdout, chunk]);
28
- readFrames();
29
- if (completed) return;
30
- if (responses.has(2)) {
31
- const response = responses.get(2);
32
- const tools = Array.isArray(response?.result?.tools) ? response.result.tools : [];
33
- const names = tools
34
- .map((tool) => tool?.name)
35
- .filter((name) => typeof name === "string" && name.length > 0);
36
- finish({ tools: names, tool_count: names.length });
37
- }
38
- });
39
-
40
- child.on("error", (error) => {
41
- finishWithError(error.message);
42
- });
43
-
44
- child.on("exit", (code) => {
45
- if (!completed && code !== 0) {
46
- finishWithError(`MCP server exited with code ${code}${stderr ? `: ${stderr.trim()}` : ""}`);
47
- }
48
- });
49
-
50
- send({
51
- jsonrpc: "2.0",
52
- id: 1,
53
- method: "initialize",
54
- params: {
55
- protocolVersion: "2024-11-05",
56
- capabilities: {},
57
- clientInfo: { name: "direxio-deployer", version: "0.0.0" }
58
- }
59
- });
60
- send({ jsonrpc: "2.0", method: "notifications/initialized", params: {} });
61
- send({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
62
-
63
- function send(message) {
64
- const body = JSON.stringify(message);
65
- child.stdin.write(`Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`);
14
+ try {
15
+ const packageRoot = resolveDirexioMcpPackageRoot(command);
16
+ const sdkRoot = path.join(packageRoot, "node_modules", "@modelcontextprotocol", "sdk", "dist", "esm");
17
+ const { Client } = await import(pathToFileURL(path.join(sdkRoot, "client", "index.js")).href);
18
+ const { StdioClientTransport } = await import(pathToFileURL(path.join(sdkRoot, "client", "stdio.js")).href);
19
+
20
+ const transport = new StdioClientTransport({
21
+ command: process.execPath,
22
+ args: [path.join(packageRoot, "dist", "index.js")],
23
+ env: process.env
24
+ });
25
+ const client = new Client({ name: "direxio-deployer", version: "0.0.0" }, { capabilities: {} });
26
+ await client.connect(transport);
27
+ const response = await client.listTools();
28
+ await client.close();
29
+
30
+ const names = (Array.isArray(response?.tools) ? response.tools : [])
31
+ .map((tool) => tool?.name)
32
+ .filter((name) => typeof name === "string" && name.length > 0);
33
+ finish({ tools: names, tool_count: names.length });
34
+ } catch (error) {
35
+ finishWithError(error instanceof Error ? error.message : String(error));
66
36
  }
67
37
 
68
- function readFrames() {
69
- while (true) {
70
- if (stdout.length === 0) return;
71
- if (stdout[0] === 10 || stdout[0] === 13) {
72
- stdout = stdout.subarray(1);
73
- continue;
74
- }
75
- if (startsWithHeader(stdout, "Content-Length:")) {
76
- const header = readHeader(stdout);
77
- if (!header) return;
78
- const contentLength = parseContentLength(header.text);
79
- if (!Number.isSafeInteger(contentLength) || contentLength < 0) {
80
- finishWithError("MCP response frame is missing a valid Content-Length header");
81
- return;
82
- }
83
- const messageEnd = header.bodyStart + contentLength;
84
- if (stdout.length < messageEnd) return;
85
- const body = stdout.subarray(header.bodyStart, messageEnd).toString("utf8");
86
- stdout = stdout.subarray(messageEnd);
87
- handleMessage(body);
88
- if (completed) return;
89
- continue;
38
+ function resolveDirexioMcpPackageRoot(commandName) {
39
+ const executable = resolveExecutable(commandName);
40
+ const basedir = path.dirname(realpathSync(executable));
41
+ const candidates = [
42
+ path.join(basedir, "node_modules", "direxio-mcp"),
43
+ path.join(basedir, "..", "node_modules", "direxio-mcp"),
44
+ path.join(basedir, "..")
45
+ ];
46
+ for (const candidate of candidates) {
47
+ if (existsSync(path.join(candidate, "package.json")) && existsSync(path.join(candidate, "dist", "index.js"))) {
48
+ return realpathSync(candidate);
90
49
  }
91
-
92
- const lineEnd = stdout.indexOf("\n");
93
- if (lineEnd < 0) return;
94
- const line = stdout.subarray(0, lineEnd).toString("utf8").replace(/\r$/, "");
95
- stdout = stdout.subarray(lineEnd + 1);
96
- if (line.length === 0) continue;
97
- handleMessage(line);
98
- if (completed) return;
99
50
  }
51
+ throw new Error(`unable to locate direxio-mcp package root from ${executable}`);
100
52
  }
101
53
 
102
- function startsWithHeader(buffer, header) {
103
- return buffer.subarray(0, header.length).toString("utf8").toLowerCase() === header.toLowerCase();
104
- }
105
-
106
- function readHeader(buffer) {
107
- let marker = "\r\n\r\n";
108
- let headerEnd = buffer.indexOf(marker);
109
- if (headerEnd < 0) {
110
- marker = "\n\n";
111
- headerEnd = buffer.indexOf(marker);
54
+ function resolveExecutable(commandName) {
55
+ const nativeCommand = nativePath(commandName);
56
+ if (path.isAbsolute(nativeCommand) && existsSync(nativeCommand)) {
57
+ return nativeCommand;
112
58
  }
113
- if (headerEnd < 0) return null;
114
- return {
115
- text: buffer.subarray(0, headerEnd).toString("utf8"),
116
- bodyStart: headerEnd + marker.length
117
- };
118
- }
119
-
120
- function parseContentLength(headerText) {
121
- for (const line of headerText.split(/\r?\n/)) {
122
- const match = /^content-length:\s*(\d+)\s*$/i.exec(line);
123
- if (match) return Number.parseInt(match[1], 10);
59
+ const lookup = process.platform === "win32"
60
+ ? spawnSync("where.exe", [nativeCommand], { encoding: "utf8" })
61
+ : spawnSync("sh", ["-lc", `command -v ${shellQuote(nativeCommand)}`], { encoding: "utf8" });
62
+ if (lookup.status !== 0 || !lookup.stdout.trim()) {
63
+ throw new Error(`unable to find ${commandName} on PATH`);
124
64
  }
125
- return NaN;
65
+ return lookup.stdout.trim().split(/\r?\n/)[0];
126
66
  }
127
67
 
128
- function handleMessage(raw) {
129
- let message;
130
- try {
131
- message = JSON.parse(raw);
132
- } catch (error) {
133
- finishWithError(`invalid MCP JSON response: ${error.message}`);
134
- return;
68
+ function nativePath(value) {
69
+ if (process.platform !== "win32" || !String(value).startsWith("/")) {
70
+ return value;
135
71
  }
136
- if (typeof message.id !== "undefined") {
137
- responses.set(message.id, message);
72
+ const converted = spawnSync("cygpath", ["-w", value], { encoding: "utf8" });
73
+ if (converted.status === 0 && converted.stdout.trim()) {
74
+ return converted.stdout.trim();
138
75
  }
76
+ return value;
77
+ }
78
+
79
+ function shellQuote(value) {
80
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
139
81
  }
140
82
 
141
83
  function finish(value) {
142
- if (completed) return;
143
- completed = true;
144
84
  clearTimeout(timer);
145
85
  console.log(JSON.stringify(value));
146
- child.kill();
147
86
  }
148
87
 
149
88
  function finishWithError(message) {
150
- if (completed) return;
151
- completed = true;
152
89
  clearTimeout(timer);
153
90
  console.error(message);
154
- child.kill();
155
91
  process.exitCode = 1;
156
92
  }
@@ -122,7 +122,7 @@ cmd_status_inventory() {
122
122
  phase_user_meaning() {
123
123
  case "$1" in
124
124
  S0_PREREQ_AWS) echo "AWS credentials, CLI tooling, or account identity are not ready." ;;
125
- S1_PREFLIGHT) echo "AWS region, default VPC, quota, or Ubuntu AMI checks are not ready." ;;
125
+ S1_PREFLIGHT) echo "AWS region, default VPC, EC2/EIP quota, or Ubuntu AMI checks are not ready." ;;
126
126
  S2_DOMAIN) echo "The long-lived domain, DNS authority, or irreversible Matrix server_name binding is not confirmed." ;;
127
127
  S3_PROVISION) echo "AWS infrastructure provisioning, fixed public IP, security group, or DNS record setup is not complete." ;;
128
128
  S4_BOOTSTRAP_STACK) echo "The EC2 instance exists, but cloud-init, Docker, Caddy/TLS, or message-server has not reached healthy state." ;;
@@ -212,7 +212,7 @@ status_next_action() {
212
212
 
213
213
  case "$1" in
214
214
  S0_PREREQ_AWS) echo "configure AWS CLI credentials for the selected deployment identity and rerun status" ;;
215
- S1_PREFLIGHT) echo "fix AWS region, default VPC, EC2 quota, or AMI availability before creating resources" ;;
215
+ S1_PREFLIGHT) echo "fix AWS region, default VPC, EC2/EIP quota, or AMI availability before creating resources" ;;
216
216
  S2_DOMAIN) echo "confirm the long-lived domain, DNS authority, and irreversible Matrix server_name binding" ;;
217
217
  S3_PROVISION) echo "inspect EC2 provisioning, Elastic IP allocation, security group creation, and DNS record setup" ;;
218
218
  S4_BOOTSTRAP_STACK) echo "inspect cloud-init, Docker, Caddy/TLS, and message-server logs over SSH" ;;
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bash
2
- # S1 PREFLIGHT - default VPC, EC2 vCPU quota, and AMI checks.
2
+ # S1 PREFLIGHT - default VPC, EC2 vCPU quota, Elastic IP, and AMI checks.
3
3
  #
4
- # New AWS accounts often start with low or zero EC2 quota. Report the blocker
5
- # and keep polling instead of losing deployment state.
4
+ # New AWS accounts often start with low or exhausted EC2/EIP quota. Report the
5
+ # blocker before S3 creates resources.
6
6
 
7
7
  run_phase() {
8
8
  aws_env_prep
@@ -37,7 +37,10 @@ run_phase() {
37
37
  || { phase_set S1_PREFLIGHT failed "quota polling interrupted"; return 1; }
38
38
  fi
39
39
 
40
- # 3) AMI (amd64/x86).
40
+ # 3) Elastic IP quota and current regional usage. Unknown quota is warned but not blocked.
41
+ _check_eip_capacity || return $?
42
+
43
+ # 4) AMI (amd64/x86).
41
44
  local ami
42
45
  ami=$(aws_lookup_ubuntu_ami)
43
46
  if [ "$ami" = "None" ] || [ -z "$ami" ]; then
@@ -70,3 +73,33 @@ _quota_ge_2() {
70
73
  _is_unknown_quota "$q" && return 1
71
74
  _num_ge "$q" 2
72
75
  }
76
+
77
+ _check_eip_capacity() {
78
+ local quota allocated available
79
+ quota=$(aws service-quotas get-service-quota --service-code ec2 --quota-code "$EC2_VPC_EIP_QUOTA_CODE" \
80
+ --query 'Quota.Value' --output text 2>/dev/null || echo "unknown")
81
+ quota=${quota:-unknown}
82
+ allocated=$(aws ec2 describe-addresses \
83
+ --query 'length(Addresses[?Domain==`vpc`])' --output text 2>/dev/null || echo "unknown")
84
+ allocated=${allocated:-unknown}
85
+
86
+ res_set eip_quota "$quota"
87
+ res_set eip_allocated "$allocated"
88
+
89
+ if _is_unknown_quota "$quota" || _is_unknown_quota "$allocated"; then
90
+ warn "Could not read Elastic IP quota or current allocation; continuing. If allocate-address fails, check regional EIP quota."
91
+ return 0
92
+ fi
93
+
94
+ available=$(awk -v q="$quota" -v a="$allocated" 'BEGIN { v=int(q+0)-int(a+0); if (v < 0) v=0; print v }')
95
+ res_set eip_available "$available"
96
+ log "Elastic IP quota = $quota, allocated = $allocated, available = $available (need 1)"
97
+
98
+ if ! _num_ge "$available" 1; then
99
+ phase_set S1_PREFLIGHT waiting_user "Elastic IP quota exhausted: allocated=$allocated quota=$quota"
100
+ warn "This region has no available Elastic IP quota: allocated=$allocated quota=$quota."
101
+ warn "Release an unused Elastic IP, request a higher EC2-VPC Elastic IP quota, or choose another AWS region, then rerun."
102
+ return 2
103
+ fi
104
+ return 0
105
+ }
@@ -886,11 +886,13 @@ EOF
886
886
 
887
887
  _create_cc_connect_matrix_session() {
888
888
  local asurl=$1 agent_auth_token=$2 device_id=$3 out=$4 body code http_body
889
- local max_attempts interval attempt preview
889
+ local max_attempts interval max_interval attempt preview sleep_for
890
890
  body=$(json_build matrix-session-create "$device_id")
891
- max_attempts=${DIREXIO_MATRIX_SESSION_CREATE_MAX:-4}
891
+ max_attempts=${DIREXIO_MATRIX_SESSION_CREATE_MAX:-12}
892
892
  interval=${DIREXIO_MATRIX_SESSION_RETRY_INTERVAL:-2}
893
+ max_interval=${DIREXIO_MATRIX_SESSION_RETRY_MAX_INTERVAL:-10}
893
894
  attempt=1
895
+ sleep_for=$interval
894
896
  while [ "$attempt" -le "$max_attempts" ]; do
895
897
  http_body=$(mktemp)
896
898
  code=$(curl -sk \
@@ -913,10 +915,14 @@ _create_cc_connect_matrix_session() {
913
915
  preview=$(head -c 200 "$http_body" 2>/dev/null || true)
914
916
  rm -f "$http_body"
915
917
  case "${code:-000}" in
916
- 000|5*)
918
+ 000|404|408|409|425|429|5*)
917
919
  if [ "$attempt" -lt "$max_attempts" ]; then
918
- warn "agent.matrix_session.create returned HTTP ${code:-000} on attempt $attempt/$max_attempts; retrying."
919
- sleep "$interval"
920
+ warn "agent.matrix_session.create returned HTTP ${code:-000} on attempt $attempt/$max_attempts; retrying in ${sleep_for}s."
921
+ sleep "$sleep_for"
922
+ if _is_non_negative_integer "$sleep_for" && _is_non_negative_integer "$max_interval"; then
923
+ sleep_for=$((sleep_for * 2))
924
+ [ "$sleep_for" -gt "$max_interval" ] && sleep_for=$max_interval
925
+ fi
920
926
  attempt=$((attempt + 1))
921
927
  continue
922
928
  fi
@@ -932,6 +938,13 @@ _create_cc_connect_matrix_session() {
932
938
  return 1
933
939
  }
934
940
 
941
+ _is_non_negative_integer() {
942
+ case "$1" in
943
+ ''|*[!0-9]*) return 1 ;;
944
+ *) return 0 ;;
945
+ esac
946
+ }
947
+
935
948
  _write_cc_connect_config() {
936
949
  local config_path=$1 data_dir=$2 project=$3 agent=$4 workspace=$5 homeserver=$6 matrix_token=$7 matrix_user=$8 room_id=$9 admin_from=${10:-} agent_cmd=${11:-} agent_options_toml=${12:-}
937
950
  local q_data q_project q_agent q_workspace q_homeserver q_token q_user q_room q_admin_from q_agent_cmd speech_toml default_agent_options_toml