direxio-deployer 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +10 -2
  2. package/README_zh.md +10 -2
  3. package/SKILL.md +32 -8
  4. package/bin/direxio-deployer.mjs +1 -2
  5. package/package.json +2 -3
  6. package/references/agent-targets.md +7 -1
  7. package/references/deployment-lessons.md +5 -7
  8. package/references/deployment-workflow.md +8 -4
  9. package/references/runtime-wiring.md +5 -5
  10. package/references/tooling.md +11 -12
  11. package/references/user-journey.md +2 -2
  12. package/references/voip-turn-runbook.md +2 -2
  13. package/references/windows-deployment-notes.md +2 -1
  14. package/scripts/destroy.sh +24 -43
  15. package/scripts/json.mjs +841 -0
  16. package/scripts/lib/aws.sh +5 -1
  17. package/scripts/lib/json.sh +114 -0
  18. package/scripts/lib/operation_report.sh +8 -195
  19. package/scripts/lib/ops.sh +8 -21
  20. package/scripts/lib/state.sh +18 -44
  21. package/scripts/mcp-tools-list.mjs +66 -5
  22. package/scripts/orchestrate.sh +166 -249
  23. package/scripts/phases/s3_provision.sh +5 -10
  24. package/scripts/phases/s5_init_tokens.sh +7 -17
  25. package/scripts/phases/s6_wire_local.sh +22 -42
  26. package/scripts/phases/s7_verify_e2e.sh +5 -5
  27. package/scripts/pricing-estimate.sh +36 -80
  28. package/tests/aws_credentials_test.sh +0 -139
  29. package/tests/connect_daemon_runtime_check_test.sh +0 -120
  30. package/tests/default_paths_test.sh +0 -58
  31. package/tests/destroy_local_bridge_test.sh +0 -154
  32. package/tests/destroy_root_identity_test.sh +0 -91
  33. package/tests/destroy_route53_zone_test.sh +0 -80
  34. package/tests/domain_authoritative_dns_test.sh +0 -49
  35. package/tests/mcp_doctor_runtime_check_test.sh +0 -86
  36. package/tests/mcp_smoke_runtime_check_test.sh +0 -121
  37. package/tests/mcp_tools_runtime_check_test.sh +0 -123
  38. package/tests/npm_skill_distribution_test.sh +0 -95
  39. package/tests/operation_report_test.sh +0 -258
  40. package/tests/orchestrate_status_recovery_test.sh +0 -91
  41. package/tests/phase_timeout_test.sh +0 -88
  42. package/tests/pricing_estimate_test.sh +0 -159
  43. package/tests/render_userdata_remote_nodes_test.sh +0 -40
  44. package/tests/root_volume_tracking_test.sh +0 -41
  45. package/tests/route53_overwrite_guard_test.sh +0 -86
  46. package/tests/route53_zone_auto_create_test.sh +0 -66
  47. package/tests/runtime_summary_check_test.sh +0 -203
  48. package/tests/s6_wire_local_test.sh +0 -405
  49. package/tests/skill_structure_test.sh +0 -298
  50. package/tests/update_reset_ops_test.sh +0 -230
  51. package/tests/user_confirmation_gates_test.sh +0 -152
@@ -253,12 +253,7 @@ _route53_existing_a_value() {
253
253
  local zone_id=$1 domain=$2 records name
254
254
  name="${domain}."
255
255
  records=$(aws route53 list-resource-record-sets --hosted-zone-id "$zone_id" --output json 2>/dev/null) || return 0
256
- printf '%s\n' "$records" | jq -r --arg name "$name" '
257
- .ResourceRecordSets[]?
258
- | select(.Name == $name and .Type == "A")
259
- | [.ResourceRecords[]?.Value]
260
- | join(",")
261
- ' | sed -n '1p'
256
+ printf '%s\n' "$records" | json_stdin_route53_a_values "$name" | sed -n '1p'
262
257
  }
263
258
 
264
259
  _guard_route53_a_overwrite() {
@@ -342,7 +337,7 @@ _find_route53_zone() {
342
337
  fi
343
338
  ;;
344
339
  esac
345
- done < <(printf '%s\n' "$zones_json" | jq -r '.HostedZones[] | [.Id, .Name] | @tsv')
340
+ done < <(printf '%s\n' "$zones_json" | json_stdin_tsv HostedZones Id Name)
346
341
  [ -n "$best_id" ] || return 1
347
342
  printf '%s\t%s\n' "$best_id" "$best_name"
348
343
  }
@@ -355,9 +350,9 @@ _create_route53_zone() {
355
350
  --name "$zone_name" \
356
351
  --caller-reference "$caller" \
357
352
  --output json) || return 1
358
- zone_id=$(printf '%s\n' "$created" | jq -r '.HostedZone.Id // empty' | sed 's#^/hostedzone/##')
359
- returned_name=$(printf '%s\n' "$created" | jq -r '.HostedZone.Name // empty')
360
- name_servers=$(printf '%s\n' "$created" | jq -r '(.DelegationSet.NameServers // []) | join(",")')
353
+ zone_id=$(printf '%s\n' "$created" | json_stdin_get HostedZone.Id | sed 's#^/hostedzone/##')
354
+ returned_name=$(printf '%s\n' "$created" | json_stdin_get HostedZone.Name)
355
+ name_servers=$(printf '%s\n' "$created" | json_stdin_join DelegationSet.NameServers ",")
361
356
  [ -n "$zone_id" ] && [ -n "$returned_name" ] || return 1
362
357
 
363
358
  _record_route53_zone "$zone_id" "${returned_name%.}" true "$name_servers"
@@ -38,8 +38,8 @@ run_phase() {
38
38
  phase_set S5_INIT_TOKENS failed "bootstrap.json missing password/access/agent credentials"
39
39
  fail "bootstrap.json must contain password as an eight-digit initialization-code string plus access_token and agent_token."
40
40
  fi
41
- asurl=$(jq -r --arg domain "$domain" '.as_url // ("https://" + $domain)' "$out")
42
- agent_room_id=$(jq -r '.agent_room_id // empty' "$out")
41
+ asurl=$(json_get "$out" as_url "https://$domain")
42
+ agent_room_id=$(json_get "$out" agent_room_id)
43
43
  if [ -z "$agent_room_id" ] || [[ "$agent_room_id" == \!agent:* ]]; then
44
44
  phase_set S5_INIT_TOKENS failed "bootstrap.json missing real agent_room_id"
45
45
  fail "bootstrap.json must contain a real Matrix agent_room_id; legacy !agent:<domain> ids are not supported."
@@ -59,9 +59,10 @@ run_phase() {
59
59
 
60
60
  _extract_output_tokens() {
61
61
  local out=$1 password token access_token
62
- password=$(jq -r 'if (.password | type) == "string" then .password else empty end' "$out")
63
- token=$(jq -r '.agent_token // empty' "$out")
64
- access_token=$(jq -r '.access_token // empty' "$out")
62
+ [ "$(json_type "$out" password)" = "string" ] || return 1
63
+ password=$(json_get "$out" password)
64
+ token=$(json_get "$out" agent_token)
65
+ access_token=$(json_get "$out" access_token)
65
66
  [ -n "$password" ] && [ -n "$token" ] && [ -n "$access_token" ] || return 1
66
67
  printf '%s' "$password" | grep -Eq '^[0-9]{8}$' || return 1
67
68
  printf '%s\t%s\t%s\n' "$password" "$token" "$access_token"
@@ -91,18 +92,7 @@ _normalize_bootstrap_output() {
91
92
  local domain=$1 src=$2 out=$3
92
93
  local tmp
93
94
  tmp=$(mktemp)
94
- if ! jq --arg domain "$domain" --arg asurl "https://$domain" '
95
- . + {
96
- domain: (.domain // $domain),
97
- as_url: (.as_url // $asurl),
98
- p2p_url: (.p2p_url // $asurl),
99
- user_id: (.user_id // .owner_user_id // ""),
100
- bot_mxid: (.bot_mxid // .owner_user_id // .user_id // ("@owner:" + $domain)),
101
- access_token: (.access_token // ""),
102
- agent_token: (.agent_token // ""),
103
- agent_room_id: (.agent_room_id // "")
104
- }
105
- ' "$src" > "$tmp"; then
95
+ if ! json_build bootstrap-normalized "$src" "$domain" > "$tmp"; then
106
96
  rm -f "$tmp"
107
97
  return 1
108
98
  fi
@@ -572,15 +572,21 @@ _openclaw_acp_args_toml() {
572
572
  url=${DIREXIO_OPENCLAW_ACP_URL:-}
573
573
  token_file=${DIREXIO_OPENCLAW_ACP_TOKEN_FILE:-}
574
574
  session=${DIREXIO_OPENCLAW_ACP_SESSION:-}
575
- [ -n "$url" ] || missing="${missing} DIREXIO_OPENCLAW_ACP_URL"
576
- [ -n "$token_file" ] || missing="${missing} DIREXIO_OPENCLAW_ACP_TOKEN_FILE"
577
- [ -n "$session" ] || missing="${missing} DIREXIO_OPENCLAW_ACP_SESSION"
578
- if [ -n "$missing" ]; then
579
- fail "OpenClaw ACP requires real Gateway settings:${missing}. Set them from the current OpenClaw runtime, or provide DIREXIO_OPENCLAW_ACP_ARGS_TOML with the complete args array."
575
+ if [ -n "$url" ] && [ -n "$token_file" ] && [ -n "$session" ]; then
576
+ token_file=$(_local_connect_path "$token_file")
577
+ _toml_array acp --url "$url" --token-file "$token_file" --session "$session"
578
+ return 0
579
+ fi
580
+ if [ -n "$url" ] || [ -n "$token_file" ]; then
581
+ [ -n "$url" ] || missing="${missing} DIREXIO_OPENCLAW_ACP_URL"
582
+ [ -n "$token_file" ] || missing="${missing} DIREXIO_OPENCLAW_ACP_TOKEN_FILE"
583
+ [ -n "$session" ] || missing="${missing} DIREXIO_OPENCLAW_ACP_SESSION"
584
+ fail "OpenClaw ACP explicit Gateway settings are incomplete:${missing}. Set all of DIREXIO_OPENCLAW_ACP_URL, DIREXIO_OPENCLAW_ACP_TOKEN_FILE, and DIREXIO_OPENCLAW_ACP_SESSION; otherwise leave URL/token-file unset so openclaw acp can auto-detect from its config."
580
585
  return 1
581
586
  fi
582
- token_file=$(_local_connect_path "$token_file")
583
- _toml_array acp --url "$url" --token-file "$token_file" --session "$session"
587
+ # Fallback: OpenClaw acp auto-discovers gateway from ~/.openclaw/openclaw.json.
588
+ warn "OpenClaw ACP: Gateway URL/token-file not set; using session '${session:-agent:main:main}' and letting openclaw acp auto-detect the Gateway from its config."
589
+ _toml_array acp --session "${session:-agent:main:main}"
584
590
  }
585
591
 
586
592
  _hermes_acp_args_toml() {
@@ -768,22 +774,7 @@ _write_mcp_json_config() {
768
774
  local path=$1 server_name=$2 command=$3 credentials_file=$4 node_id=${5:-}
769
775
  mkdir -p "$(dirname "$path")"
770
776
  umask 077
771
- jq -n \
772
- --arg server_name "$server_name" \
773
- --arg command "$command" \
774
- --arg credentials_file "$credentials_file" \
775
- --arg node_id "$node_id" \
776
- '{
777
- mcpServers: {
778
- ($server_name): {
779
- command: $command,
780
- env: {
781
- DIREXIO_CREDENTIALS_FILE: $credentials_file,
782
- DIREXIO_AGENT_NODE_ID: $node_id
783
- }
784
- }
785
- }
786
- }' > "$path"
777
+ json_build mcp-json-config "$server_name" "$command" "$credentials_file" "$node_id" > "$path"
787
778
  chmod 600 "$path" 2>/dev/null || true
788
779
  }
789
780
 
@@ -791,17 +782,7 @@ _write_mcp_openclaw_server_config() {
791
782
  local path=$1 command=$2 credentials_file=$3 node_id=${4:-}
792
783
  mkdir -p "$(dirname "$path")"
793
784
  umask 077
794
- jq -n \
795
- --arg command "$command" \
796
- --arg credentials_file "$credentials_file" \
797
- --arg node_id "$node_id" \
798
- '{
799
- command: $command,
800
- env: {
801
- DIREXIO_CREDENTIALS_FILE: $credentials_file,
802
- DIREXIO_AGENT_NODE_ID: $node_id
803
- }
804
- }' > "$path"
785
+ json_build mcp-openclaw-server-config "$command" "$credentials_file" "$node_id" > "$path"
805
786
  chmod 600 "$path" 2>/dev/null || true
806
787
  }
807
788
 
@@ -905,7 +886,7 @@ EOF
905
886
 
906
887
  _create_cc_connect_matrix_session() {
907
888
  local asurl=$1 access_token=$2 device_id=$3 out=$4 body code http_body
908
- body=$(jq -n --arg device_id "$device_id" '{action:"agent.matrix_session.create",params:{device_id:$device_id}}')
889
+ body=$(json_build matrix-session-create "$device_id")
909
890
  http_body=$(mktemp)
910
891
  code=$(curl -sk -o "$http_body" -w '%{http_code}' -X POST "$asurl/_p2p/command" \
911
892
  -H 'Content-Type: application/json' \
@@ -916,7 +897,7 @@ _create_cc_connect_matrix_session() {
916
897
  rm -f "$http_body"
917
898
  return 1
918
899
  fi
919
- if ! jq -e '.access_token and .device_id and .user_id and .homeserver' "$http_body" >/dev/null; then
900
+ if ! json_assert "$http_body" matrix-session >/dev/null; then
920
901
  warn "agent.matrix_session.create response is missing Matrix session fields: $(head -c 200 "$http_body" 2>/dev/null)"
921
902
  rm -f "$http_body"
922
903
  return 1
@@ -1221,8 +1202,7 @@ _agent_node_id_matches_host() {
1221
1202
  _write_credentials_file() {
1222
1203
  local cred=$1 domain=$2 asurl=$3 token=$4 password=$5 access_token=$6 agent_room_id=$7 node_id=$8
1223
1204
  mkdir -p "$(dirname "$cred")"
1224
- jq -n --arg domain "$domain" --arg url "$asurl" --arg tok "$token" --arg password "$password" --arg access "$access_token" --arg room "$agent_room_id" --arg node_id "$node_id" \
1225
- '{profiles:{default:{domain:$domain,password:$password,access_token:$access,agent_room_id:$room,direxio_domain:$url,direxio_agent_token:$tok,direxio_agent_room_id:$room,direxio_agent_node_id:$node_id}}}' > "$cred"
1205
+ json_build credentials-profile "$domain" "$asurl" "$token" "$password" "$access_token" "$agent_room_id" "$node_id" > "$cred"
1226
1206
  chmod 600 "$cred"
1227
1207
  }
1228
1208
 
@@ -1358,10 +1338,10 @@ run_phase() {
1358
1338
  phase_set S6_WIRE_LOCAL failed "agent Matrix session creation failed"
1359
1339
  fail "failed to create cc-connect Matrix session via agent.matrix_session.create."
1360
1340
  fi
1361
- matrix_token=$(jq -r '.access_token' "$cc_session")
1362
- matrix_user=$(jq -r '.user_id' "$cc_session")
1363
- matrix_device=$(jq -r '.device_id' "$cc_session")
1364
- matrix_homeserver=$(jq -r '.homeserver' "$cc_session")
1341
+ matrix_token=$(json_get "$cc_session" access_token)
1342
+ matrix_user=$(json_get "$cc_session" user_id)
1343
+ matrix_device=$(json_get "$cc_session" device_id)
1344
+ matrix_homeserver=$(json_get "$cc_session" homeserver)
1365
1345
  if [ "$matrix_user" = "@owner:$domain" ]; then
1366
1346
  phase_set S6_WIRE_LOCAL failed "agent Matrix session returned owner user"
1367
1347
  fail "agent.matrix_session.create returned owner Matrix user; deploy a message-server build with agent Matrix session support."
@@ -45,8 +45,8 @@ _check_p2p_agent_auth() {
45
45
  -X POST "https://$domain/_p2p/query" \
46
46
  -H 'Content-Type: application/json' \
47
47
  -H "Authorization: Bearer $token" \
48
- -d "{\"action\":\"mcp.messages.list\",\"params\":{\"room_id\":\"$room_id\",\"limit\":1}}" 2>/dev/null)
49
- if [ "$code" = "200" ] && jq -e '(.messages | type == "array") and (.room_id | type == "string")' "$body" >/dev/null 2>&1; then
48
+ -d "$(json_build mcp-messages-list "$room_id")" 2>/dev/null)
49
+ if [ "$code" = "200" ] && json_assert "$body" messages-response >/dev/null 2>&1; then
50
50
  rm -f "$body"
51
51
  ok " ✓ _p2p/query mcp.messages.list (agent token)"
52
52
  return 0
@@ -61,7 +61,7 @@ _p2p_access_token() {
61
61
  local args=()
62
62
  while IFS= read -r arg; do args+=("$arg"); done < <(curl_resolve_args "$domain")
63
63
  at=$(curl -sk "${args[@]}" -X POST "https://$domain/_p2p/command" -H 'Content-Type: application/json' \
64
- -d "{\"action\":\"portal.auth\",\"params\":{\"password\":\"$password\"}}" 2>/dev/null | jq -r '.access_token // empty')
64
+ -d "{\"action\":\"portal.auth\",\"params\":{\"password\":\"$password\"}}" 2>/dev/null | json_stdin_get access_token)
65
65
  printf '%s' "$at"
66
66
  }
67
67
 
@@ -97,7 +97,7 @@ _check_matrix_server_wellknown() {
97
97
  local args=()
98
98
  while IFS= read -r arg; do args+=("$arg"); done < <(curl_resolve_args "$domain")
99
99
  body=$(curl -sk "${args[@]}" "https://$domain/.well-known/matrix/server" 2>/dev/null)
100
- if printf '%s' "$body" | jq -e --arg want "$domain:443" '.["m.server"] == $want' >/dev/null 2>&1; then
100
+ if printf '%s' "$body" | json_stdin_assert well-known-server "$domain:443" >/dev/null 2>&1; then
101
101
  ok " ✓ matrix federation well-known ($domain:443)"; return 0
102
102
  fi
103
103
  warn " x matrix federation well-known invalid:$(printf '%s' "$body" | head -c 120)"; return 1
@@ -128,7 +128,7 @@ _check_turn() {
128
128
  if [ -z "$at" ]; then warn " x TURN (failed to exchange access_token; cannot verify turnServer)"; return 1; fi
129
129
  turn=$(curl -sk "${args[@]}" "https://$domain/_matrix/client/v3/voip/turnServer" \
130
130
  -H "Authorization: Bearer $at" 2>/dev/null)
131
- if printf '%s' "$turn" | jq -e '(.uris|type=="array" and length>0) and (any(.uris[]; test("^turns?:"))) and (.username|tostring|length>0) and (.password|tostring|length>0) and (.ttl>0)' >/dev/null 2>&1; then
131
+ if printf '%s' "$turn" | json_stdin_assert turn-credentials >/dev/null 2>&1; then
132
132
  ok " ✓ TURN turnServer non-empty and valid"; return 0
133
133
  else
134
134
  warn " x TURN turnServer invalid/empty:$(printf '%s' "$turn" | head -c 120)"; return 1
@@ -2,6 +2,10 @@
2
2
  # pricing-estimate.sh - estimate monthly AWS costs for a Direxio EC2 node.
3
3
  set -euo pipefail
4
4
 
5
+ HERE=$(cd "$(dirname "$0")" && pwd)
6
+ # shellcheck disable=SC1090
7
+ source "$HERE/lib/json.sh"
8
+
5
9
  usage() {
6
10
  cat >&2 <<'EOF'
7
11
  Usage:
@@ -38,13 +42,7 @@ price_from_get_products() {
38
42
  --filters "Type=TERM_MATCH,Field=location,Value=$location" "$@" \
39
43
  --max-results 1 \
40
44
  --output json 2>/dev/null) || return 1
41
- printf '%s\n' "$json" | jq -r '
42
- .PriceList[0]
43
- | fromjson
44
- | .terms.OnDemand
45
- | to_entries[0].value.priceDimensions
46
- | to_entries[0].value.pricePerUnit.USD
47
- ' 2>/dev/null
45
+ printf '%s\n' "$json" | json_stdin_price_usd 2>/dev/null
48
46
  }
49
47
 
50
48
  numeric_or_empty() {
@@ -98,7 +96,7 @@ build_estimate() {
98
96
 
99
97
  if [ -z "$location" ]; then
100
98
  status=fallback
101
- warnings_json=$(jq -cn --arg w "Region is not mapped to an AWS Pricing location; using conservative fallback estimates" '$ARGS.named | [.w]')
99
+ warnings_json='["Region is not mapped to an AWS Pricing location; using conservative fallback estimates"]'
102
100
  fi
103
101
 
104
102
  if [ "$status" = "queried" ] && ec2_hourly=$(lookup_ec2_hourly "$location" "$instance_type"); then
@@ -126,7 +124,11 @@ build_estimate() {
126
124
  fi
127
125
 
128
126
  if [ "$status" = "fallback" ]; then
129
- warnings_json=$(printf '%s\n' "$warnings_json" | jq '. + ["AWS Pricing API unavailable; using conservative fallback estimates"] | unique')
127
+ case "$warnings_json" in
128
+ *"AWS Pricing API unavailable; using conservative fallback estimates"*) ;;
129
+ "[]") warnings_json='["AWS Pricing API unavailable; using conservative fallback estimates"]' ;;
130
+ *) warnings_json=${warnings_json%]}; warnings_json="$warnings_json,\"AWS Pricing API unavailable; using conservative fallback estimates\"]" ;;
131
+ esac
130
132
  fi
131
133
 
132
134
  if [ "$domain_mode" = "route53" ]; then
@@ -135,71 +137,25 @@ build_estimate() {
135
137
  route53_monthly=0
136
138
  fi
137
139
 
138
- jq -n \
139
- --arg pricing_status "$status" \
140
- --arg region "$region" \
141
- --arg location "$location" \
142
- --arg instance_type "$instance_type" \
143
- --arg domain_mode "$domain_mode" \
144
- --arg ec2_source "$ec2_source" \
145
- --arg gp3_source "$gp3_source" \
146
- --arg ipv4_source "$ipv4_source" \
147
- --argjson warnings "$warnings_json" \
148
- --argjson hours "$hours" \
149
- --argjson disk_gb "$disk_gb" \
150
- --argjson ec2_hourly "$ec2_hourly" \
151
- --argjson ec2_monthly "$(round2 "$(awk -v h="$ec2_hourly" -v m="$hours" 'BEGIN { print h*m }')")" \
152
- --argjson gp3_rate "$gp3_rate" \
153
- --argjson gp3_monthly "$(round2 "$(awk -v r="$gp3_rate" -v gb="$disk_gb" 'BEGIN { print r*gb }')")" \
154
- --argjson ipv4_hourly "$public_ipv4_hourly" \
155
- --argjson ipv4_monthly "$(round2 "$(awk -v h="$public_ipv4_hourly" -v m="$hours" 'BEGIN { print h*m }')")" \
156
- --argjson route53_monthly "$route53_monthly" '
157
- def total:
158
- (.components.ec2_instance.monthly_usd
159
- + .components.ebs_gp3.monthly_usd
160
- + .components.public_ipv4.monthly_usd
161
- + .components.route53_hosted_zone.monthly_usd);
162
- {
163
- pricing_status: $pricing_status,
164
- region: $region,
165
- location: $location,
166
- hours_per_month: $hours,
167
- warnings: $warnings,
168
- components: {
169
- ec2_instance: {
170
- instance_type: $instance_type,
171
- hourly_usd: $ec2_hourly,
172
- monthly_usd: $ec2_monthly,
173
- source: $ec2_source
174
- },
175
- ebs_gp3: {
176
- storage_gb: $disk_gb,
177
- gb_month_usd: $gp3_rate,
178
- monthly_usd: $gp3_monthly,
179
- source: $gp3_source
180
- },
181
- public_ipv4: {
182
- hourly_usd: $ipv4_hourly,
183
- monthly_usd: $ipv4_monthly,
184
- billed_even_when_attached: true,
185
- source: $ipv4_source
186
- },
187
- route53_hosted_zone: {
188
- monthly_usd: $route53_monthly,
189
- included: ($domain_mode == "route53")
190
- }
191
- },
192
- notes: [
193
- "Estimate excludes data transfer, TURN relay traffic, domain registration, taxes, and AWS credit eligibility.",
194
- "Public IPv4 is billed hourly by AWS even when attached to a running instance.",
195
- "AWS credits may reduce charges only when the account, plan, region, and service usage are eligible; verify in AWS Billing Console."
196
- ],
197
- recommendations: [
198
- "Set an AWS Budget or billing alert before leaving the node running.",
199
- "Review AWS Billing Console after deployment and after destroy to confirm actual charges and remaining credits."
200
- ]
201
- } | .total_monthly_usd = ((total * 100 | round) / 100)
202
- '
140
+ json_build pricing-estimate \
141
+ "$status" \
142
+ "$region" \
143
+ "$location" \
144
+ "$instance_type" \
145
+ "$domain_mode" \
146
+ "$ec2_source" \
147
+ "$gp3_source" \
148
+ "$ipv4_source" \
149
+ "$warnings_json" \
150
+ "$hours" \
151
+ "$disk_gb" \
152
+ "$ec2_hourly" \
153
+ "$(round2 "$(awk -v h="$ec2_hourly" -v m="$hours" 'BEGIN { print h*m }')")" \
154
+ "$gp3_rate" \
155
+ "$(round2 "$(awk -v r="$gp3_rate" -v gb="$disk_gb" 'BEGIN { print r*gb }')")" \
156
+ "$public_ipv4_hourly" \
157
+ "$(round2 "$(awk -v h="$public_ipv4_hourly" -v m="$hours" 'BEGIN { print h*m }')")" \
158
+ "$route53_monthly"
203
159
  }
204
160
 
205
161
  state=""
@@ -227,10 +183,11 @@ if [ -n "$state" ]; then
227
183
  echo "state.json not found: $state" >&2
228
184
  exit 1
229
185
  }
230
- region=${region:-$(jq -r '.region // empty' "$state")}
231
- instance_type=${instance_type:-$(jq -r '.instance_type // empty' "$state")}
232
- domain_mode=${domain_mode:-$(jq -r '.domain_mode // "user"' "$state")}
233
- disk_gb=${disk_gb:-$(jq -r '.resources.root_volume_gb // .root_volume_gb // "8"' "$state")}
186
+ region=${region:-$(json_get "$state" region)}
187
+ instance_type=${instance_type:-$(json_get "$state" instance_type)}
188
+ domain_mode=${domain_mode:-$(json_get "$state" domain_mode user)}
189
+ disk_gb=${disk_gb:-$(json_get "$state" resources.root_volume_gb)}
190
+ disk_gb=${disk_gb:-$(json_get "$state" root_volume_gb 8)}
234
191
  fi
235
192
 
236
193
  region=${region:-${AWS_DEFAULT_REGION:-${AWS_REGION:-}}}
@@ -249,8 +206,7 @@ if [ "$write_state" = "1" ]; then
249
206
  echo "--write-state requires --state" >&2
250
207
  exit 1
251
208
  }
252
- tmp="$state.tmp.$$"
253
- jq --argjson estimate "$estimate" '.cost_estimate = $estimate' "$state" > "$tmp" && mv "$tmp" "$state"
209
+ json_mutate "$state" set-json cost_estimate "$estimate"
254
210
  fi
255
211
 
256
212
  printf '%s\n' "$estimate"
@@ -1,139 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- ROOT=$(cd "$(dirname "$0")/.." && pwd)
5
- tmp=$(mktemp -d)
6
- trap 'rm -rf "$tmp"' EXIT
7
-
8
- export HOME="$tmp/home"
9
- mkdir -p "$HOME"
10
-
11
- fakebin="$tmp/bin"
12
- mkdir -p "$fakebin"
13
-
14
- cat > "$fakebin/aws" <<'EOF'
15
- #!/usr/bin/env bash
16
- set -euo pipefail
17
- printf 'aws' >> "$CALLS"
18
- printf ' %q' "$@" >> "$CALLS"
19
- printf '\n' >> "$CALLS"
20
-
21
- case "${1:-} ${2:-}" in
22
- "configure get")
23
- exit 1
24
- ;;
25
- "sts get-caller-identity")
26
- profile=${AWS_PROFILE:-}
27
- key=${AWS_ACCESS_KEY_ID:-}
28
- if [ "$profile" = "root-profile" ] || [ "$key" = "AKIAROOTTEST" ]; then
29
- arn="arn:aws:iam::123456789012:root"
30
- account="123456789012"
31
- else
32
- arn="arn:aws:iam::123456789012:user/DirexioDeployer-20260628"
33
- account="123456789012"
34
- fi
35
- case "$*" in
36
- *"--query Arn"*) printf '%s\n' "$arn" ;;
37
- *"--query Account"*) printf '%s\n' "$account" ;;
38
- *) printf '{"Account":"%s","Arn":"%s"}\n' "$account" "$arn" ;;
39
- esac
40
- ;;
41
- *)
42
- echo "unexpected aws call: $*" >&2
43
- exit 2
44
- ;;
45
- esac
46
- EOF
47
- chmod 700 "$fakebin/aws"
48
-
49
- CALLS="$tmp/aws.calls"
50
- export CALLS
51
- export PATH="$fakebin:$PATH"
52
- export AWS_SHARED_CREDENTIALS_FILE="$tmp/aws/credentials"
53
- export AWS_CONFIG_FILE="$tmp/aws/config"
54
-
55
- file_mode() {
56
- if stat -c '%a' "$1" >/dev/null 2>&1; then
57
- stat -c '%a' "$1"
58
- else
59
- stat -f '%Lp' "$1"
60
- fi
61
- }
62
-
63
- cat > "$tmp/direxio.csv" <<'CSV'
64
- User name,Access key ID,Secret access key
65
- DirexioDeployer-20260628,AKIADIREXIOTEST,SECRET_DIREXIO_VALUE
66
- CSV
67
-
68
- out=$(bash "$ROOT/scripts/aws-credentials.sh" import-csv "$tmp/direxio.csv" direxio-deployer ap-southeast-1)
69
-
70
- [[ "$out" == *"profile=direxio-deployer"* ]]
71
- [[ "$out" == *"arn:aws:iam::<account>:user/DirexioDeployer-20260628"* ]]
72
- if [[ "$out" == *"AKIADIREXIOTEST"* || "$out" == *"SECRET_DIREXIO_VALUE"* ]]; then
73
- echo "aws-credentials output leaked credential values" >&2
74
- printf '%s\n' "$out" >&2
75
- exit 1
76
- fi
77
-
78
- grep -q '^\[direxio-deployer\]$' "$AWS_SHARED_CREDENTIALS_FILE"
79
- grep -q '^aws_access_key_id = AKIADIREXIOTEST$' "$AWS_SHARED_CREDENTIALS_FILE"
80
- grep -q '^aws_secret_access_key = SECRET_DIREXIO_VALUE$' "$AWS_SHARED_CREDENTIALS_FILE"
81
- grep -q '^\[profile direxio-deployer\]$' "$AWS_CONFIG_FILE"
82
- grep -q '^region = ap-southeast-1$' "$AWS_CONFIG_FILE"
83
-
84
- credential_perm=$(file_mode "$AWS_SHARED_CREDENTIALS_FILE")
85
- config_perm=$(file_mode "$AWS_CONFIG_FILE")
86
- case "$(uname -s)" in
87
- MINGW*|MSYS*|CYGWIN*)
88
- [[ "$credential_perm" == "600" || "$credential_perm" == "644" ]]
89
- [[ "$config_perm" == "600" || "$config_perm" == "644" ]]
90
- ;;
91
- *)
92
- [ "$credential_perm" = "600" ]
93
- [ "$config_perm" = "600" ]
94
- ;;
95
- esac
96
-
97
- verify_out=$(AWS_PROFILE=direxio-deployer bash "$ROOT/scripts/aws-credentials.sh" verify direxio-deployer)
98
- [[ "$verify_out" == *"profile=direxio-deployer"* ]]
99
- [[ "$verify_out" == *"root=false"* ]]
100
-
101
- cat > "$tmp/root.csv" <<'CSV'
102
- Access key ID,Secret access key
103
- AKIAROOTTEST,SECRET_ROOT_VALUE
104
- CSV
105
-
106
- root_out=$(bash "$ROOT/scripts/aws-credentials.sh" import-csv "$tmp/root.csv" root-profile us-east-1)
107
- [[ "$root_out" == *"profile=root-profile"* ]]
108
- [[ "$root_out" == *"root=true"* ]]
109
- if [[ "$root_out" == *"AKIAROOTTEST"* || "$root_out" == *"SECRET_ROOT_VALUE"* ]]; then
110
- echo "aws-credentials root output leaked credential values" >&2
111
- printf '%s\n' "$root_out" >&2
112
- exit 1
113
- fi
114
- grep -q '^\[root-profile\]$' "$AWS_SHARED_CREDENTIALS_FILE"
115
- grep -q '^aws_access_key_id = AKIAROOTTEST$' "$AWS_SHARED_CREDENTIALS_FILE"
116
- grep -q '^aws_secret_access_key = SECRET_ROOT_VALUE$' "$AWS_SHARED_CREDENTIALS_FILE"
117
-
118
- root_verify_out=$(AWS_PROFILE=root-profile bash "$ROOT/scripts/aws-credentials.sh" verify root-profile)
119
- [[ "$root_verify_out" == *"profile=root-profile"* ]]
120
- [[ "$root_verify_out" == *"root=true"* ]]
121
-
122
- set +e
123
- s0_output=$(
124
- P2P_WORKDIR="$tmp/state-root" AWS_PROFILE=root-profile bash -c '
125
- set -uo pipefail
126
- cd "$1"
127
- source scripts/lib/state.sh
128
- state_init >/dev/null 2>&1
129
- source scripts/lib/aws.sh
130
- source scripts/phases/s0_prereq_aws.sh
131
- run_phase
132
- ' _ "$ROOT" 2>&1
133
- )
134
- s0_rc=$?
135
- set -e
136
- [ "$s0_rc" -eq 0 ]
137
- [[ "$s0_output" == *"AWS credentials are valid"* ]]
138
-
139
- echo "aws credentials ok"
@@ -1,120 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- ROOT=$(cd "$(dirname "$0")/.." && pwd)
5
- tmp=$(mktemp -d)
6
- trap 'rm -rf "$tmp"' EXIT
7
-
8
- export HOME="$tmp/home"
9
- mkdir -p "$HOME"
10
-
11
- fakebin="$tmp/bin"
12
- mkdir -p "$fakebin"
13
- cat > "$fakebin/direxio-connect" <<'EOF'
14
- #!/usr/bin/env bash
15
- set -euo pipefail
16
-
17
- if [ "${1:-}" = "daemon" ] && [ "${2:-}" = "logs" ]; then
18
- [ "${3:-}" = "--service-name" ]
19
- [ "${4:-}" = "connect-check.example.test" ]
20
- printf '%s\n' "${CONNECT_LOG_OUTPUT:-}"
21
- exit 0
22
- fi
23
-
24
- [ "${1:-}" = "daemon" ]
25
- [ "${2:-}" = "status" ]
26
- [ "${3:-}" = "--service-name" ]
27
- [ "${4:-}" = "connect-check.example.test" ]
28
-
29
- cat <<STATUS
30
- cc-connect daemon status
31
-
32
- Status: ${CONNECT_STATUS:-Running}
33
- Platform: test
34
- WorkDir: ${CONNECT_WORK_DIR:-}
35
- STATUS
36
- EOF
37
- chmod 700 "$fakebin/direxio-connect"
38
-
39
- service_dir="$HOME/.direxio/nodes/connect-check.example.test"
40
- mkdir -p "$service_dir/cc-connect"
41
- config="$service_dir/cc-connect/config.toml"
42
- : > "$config"
43
- state="$service_dir/state.json"
44
- jq -n \
45
- --arg service_dir "$service_dir" \
46
- --arg config "$config" \
47
- '{
48
- run_id: "connect-daemon-test",
49
- region: "ap-northeast-1",
50
- domain_mode: "user",
51
- domain: "connect-check.example.test",
52
- agent_service_id: "connect-check.example.test",
53
- agent_service_dir: $service_dir,
54
- cc_connect_config: $config,
55
- cc_connect_binary: "direxio-connect",
56
- phase: "S7_VERIFY_E2E",
57
- phases: {
58
- S0_PREREQ_AWS: {status: "done"},
59
- S1_PREFLIGHT: {status: "done"},
60
- S2_DOMAIN: {status: "done"},
61
- S3_PROVISION: {status: "done"},
62
- S4_BOOTSTRAP_STACK: {status: "done"},
63
- S5_INIT_TOKENS: {status: "done"},
64
- S6_WIRE_LOCAL: {status: "done"},
65
- S7_VERIFY_E2E: {status: "done"}
66
- },
67
- resources: {}
68
- }' > "$state"
69
-
70
- verify_output=$(P2P_WORKDIR="$service_dir" PATH="$fakebin:$PATH" CONNECT_WORK_DIR="$service_dir/cc-connect" bash "$ROOT/scripts/orchestrate.sh" verify connect_daemon)
71
- printf '%s\n' "$verify_output" | grep -q 'verified runtime check: connect_daemon'
72
-
73
- expected_work_dir="$service_dir/cc-connect"
74
- if command -v cygpath >/dev/null 2>&1; then
75
- expected_work_dir=$(cygpath -m "$expected_work_dir")
76
- fi
77
-
78
- jq -e '
79
- .runtime_checks.connect_daemon.status == "passed"
80
- and .runtime_checks.connect_daemon.service_name == "connect-check.example.test"
81
- and .runtime_checks.connect_daemon.daemon_status == "Running"
82
- and .runtime_checks.connect_daemon.work_dir == "'"$expected_work_dir"'"
83
- and (.user_confirmations.agent_mcp_runtime | not)
84
- ' "$state" >/dev/null
85
-
86
- set +e
87
- P2P_WORKDIR="$service_dir" PATH="$fakebin:$PATH" CONNECT_WORK_DIR="$service_dir/cc-connect" CONNECT_LOG_OUTPUT='ACP error (ACP_SESSION_INIT_FAILED): ACP metadata is missing for agent:main:acp:a18569b4-1f24-4f8a-aec6-f6a54530d50e. Recreate this ACP session with /acp spawn and rebind the thread.' bash "$ROOT/scripts/orchestrate.sh" verify connect_daemon > "$tmp/acp-error.out" 2>&1
88
- acp_rc=$?
89
- set -e
90
- [ "$acp_rc" -ne 0 ] || {
91
- echo "connect daemon check must fail when daemon logs show ACP session init failure" >&2
92
- exit 1
93
- }
94
- jq -e '
95
- .runtime_checks.connect_daemon.status == "failed"
96
- and (.runtime_checks.connect_daemon.evidence | contains("ACP session initialization failure"))
97
- and (.runtime_checks.connect_daemon.agent_error | contains("ACP_SESSION_INIT_FAILED"))
98
- ' "$state" >/dev/null
99
-
100
- report_output=$(P2P_WORKDIR="$service_dir" bash "$ROOT/scripts/orchestrate.sh" report new_deploy)
101
- report_path=$(printf '%s\n' "$report_output" | sed -nE 's/^operation report: //p' | tail -n 1)
102
- jq -e '
103
- .runtime_checks.connect_daemon.status == "failed"
104
- and .gates.user_confirmation.agent_mcp_runtime == "pending_runtime_confirmation"
105
- ' "$report_path" >/dev/null
106
-
107
- set +e
108
- P2P_WORKDIR="$service_dir" PATH="$fakebin:$PATH" CONNECT_WORK_DIR="$HOME/.direxio/nodes/other.example.test/cc-connect" bash "$ROOT/scripts/orchestrate.sh" verify connect_daemon > "$tmp/wrong.out" 2>&1
109
- wrong_rc=$?
110
- set -e
111
- [ "$wrong_rc" -ne 0 ] || {
112
- echo "connect daemon check must fail when daemon WorkDir belongs to another service" >&2
113
- exit 1
114
- }
115
- jq -e '
116
- .runtime_checks.connect_daemon.status == "failed"
117
- and (.runtime_checks.connect_daemon.evidence | contains("different service"))
118
- ' "$state" >/dev/null
119
-
120
- echo "connect daemon runtime check ok"