direxio-deployer 0.1.0

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 (77) hide show
  1. package/AGENTS.md +92 -0
  2. package/LICENSE +21 -0
  3. package/README.md +221 -0
  4. package/README_zh.md +218 -0
  5. package/SKILL.md +722 -0
  6. package/agents/README.md +25 -0
  7. package/agents/openai.yaml +12 -0
  8. package/bin/direxio-deployer.mjs +375 -0
  9. package/package.json +28 -0
  10. package/references/agent-targets.md +128 -0
  11. package/references/architecture.md +44 -0
  12. package/references/bug-history.md +78 -0
  13. package/references/deployment-lessons.md +218 -0
  14. package/references/deployment-optimization-audit.md +317 -0
  15. package/references/deployment-workflow.md +341 -0
  16. package/references/iam-policy.json +52 -0
  17. package/references/runtime-wiring.md +209 -0
  18. package/references/state-machine.md +46 -0
  19. package/references/token-refresh.md +81 -0
  20. package/references/tooling.md +106 -0
  21. package/references/troubleshooting.md +26 -0
  22. package/references/user-journey.md +75 -0
  23. package/references/verification-recovery.md +84 -0
  24. package/references/voip-turn-runbook.md +154 -0
  25. package/references/windows-deployment-notes.md +119 -0
  26. package/scripts/aws-credentials.sh +195 -0
  27. package/scripts/cloud-init/Caddyfile +48 -0
  28. package/scripts/cloud-init/docker-compose.yml +125 -0
  29. package/scripts/cloud-init/init-tokens.sh +238 -0
  30. package/scripts/cloud-init/user-data.yaml +40 -0
  31. package/scripts/destroy.ps1 +77 -0
  32. package/scripts/destroy.sh +589 -0
  33. package/scripts/lib/aws.sh +73 -0
  34. package/scripts/lib/domain.sh +175 -0
  35. package/scripts/lib/operation_report.sh +240 -0
  36. package/scripts/lib/ops.sh +230 -0
  37. package/scripts/lib/paths.sh +35 -0
  38. package/scripts/lib/state.sh +137 -0
  39. package/scripts/mcp-tools-list.mjs +95 -0
  40. package/scripts/orchestrate.ps1 +112 -0
  41. package/scripts/orchestrate.sh +1126 -0
  42. package/scripts/phases/s0_prereq_aws.sh +39 -0
  43. package/scripts/phases/s1_preflight.sh +72 -0
  44. package/scripts/phases/s2_domain.sh +103 -0
  45. package/scripts/phases/s3_provision.sh +421 -0
  46. package/scripts/phases/s4_bootstrap_stack.sh +38 -0
  47. package/scripts/phases/s5_init_tokens.sh +118 -0
  48. package/scripts/phases/s6_wire_local.sh +1435 -0
  49. package/scripts/phases/s7_verify_e2e.sh +136 -0
  50. package/scripts/pricing-estimate.sh +256 -0
  51. package/scripts/render/render-userdata.sh +86 -0
  52. package/scripts/reset-app-data.sh +40 -0
  53. package/scripts/update.sh +30 -0
  54. package/tests/aws_credentials_test.sh +139 -0
  55. package/tests/connect_daemon_runtime_check_test.sh +120 -0
  56. package/tests/default_paths_test.sh +58 -0
  57. package/tests/destroy_local_bridge_test.sh +154 -0
  58. package/tests/destroy_root_identity_test.sh +91 -0
  59. package/tests/destroy_route53_zone_test.sh +80 -0
  60. package/tests/domain_authoritative_dns_test.sh +49 -0
  61. package/tests/mcp_doctor_runtime_check_test.sh +86 -0
  62. package/tests/mcp_smoke_runtime_check_test.sh +121 -0
  63. package/tests/mcp_tools_runtime_check_test.sh +123 -0
  64. package/tests/npm_skill_distribution_test.sh +95 -0
  65. package/tests/operation_report_test.sh +258 -0
  66. package/tests/orchestrate_status_recovery_test.sh +91 -0
  67. package/tests/phase_timeout_test.sh +88 -0
  68. package/tests/pricing_estimate_test.sh +159 -0
  69. package/tests/render_userdata_remote_nodes_test.sh +40 -0
  70. package/tests/root_volume_tracking_test.sh +41 -0
  71. package/tests/route53_overwrite_guard_test.sh +86 -0
  72. package/tests/route53_zone_auto_create_test.sh +66 -0
  73. package/tests/runtime_summary_check_test.sh +203 -0
  74. package/tests/s6_wire_local_test.sh +405 -0
  75. package/tests/skill_structure_test.sh +298 -0
  76. package/tests/update_reset_ops_test.sh +230 -0
  77. package/tests/user_confirmation_gates_test.sh +152 -0
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env bash
2
+ # lib/aws.sh - shared AWS setup sourced by phases.
3
+ #
4
+ # Some local proxy setups truncate AWS API TLS (UNEXPECTED_EOF). Bypass proxies
5
+ # for AWS endpoints in every phase that calls aws.
6
+
7
+ aws_env_prep() {
8
+ local region=${AWS_DEFAULT_REGION:-${AWS_REGION:-}}
9
+ if [ -n "${STATE_JSON:-}" ] && [ -f "$STATE_JSON" ]; then
10
+ local state_region
11
+ state_region=$(jq -r '.region // empty' "$STATE_JSON" 2>/dev/null || true)
12
+ [ -n "$state_region" ] && region="$state_region"
13
+ fi
14
+ if [ -z "$region" ]; then
15
+ region=$(aws_configured_region)
16
+ fi
17
+ [ -n "$region" ] && export AWS_DEFAULT_REGION="$region"
18
+ export NO_PROXY="*"; export no_proxy="*"
19
+ unset HTTP_PROXY HTTPS_PROXY http_proxy https_proxy 2>/dev/null || true
20
+ }
21
+
22
+ aws_configured_region() {
23
+ if [ -n "${AWS_PROFILE:-}" ]; then
24
+ aws configure get region --profile "$AWS_PROFILE" 2>/dev/null || true
25
+ else
26
+ aws configure get region 2>/dev/null || true
27
+ fi
28
+ }
29
+
30
+ aws_identity_arn() {
31
+ aws sts get-caller-identity --query Arn --output text 2>/dev/null || true
32
+ }
33
+
34
+ aws_identity_account() {
35
+ aws sts get-caller-identity --query Account --output text 2>/dev/null || true
36
+ }
37
+
38
+ aws_arn_is_root() {
39
+ case "$1" in
40
+ arn:aws*:iam::*:root) return 0 ;;
41
+ *) return 1 ;;
42
+ esac
43
+ }
44
+
45
+ aws_redact_arn() {
46
+ printf '%s\n' "$1" | sed -E 's/::[0-9]{12}:/::<account>:/'
47
+ }
48
+
49
+ # EC2 vCPU quota code: Running On-Demand Standard instances.
50
+ EC2_STD_QUOTA_CODE="L-1216C47A"
51
+
52
+ # Dynamically resolve the latest Ubuntu 22.04 amd64 AMI; never hard-code AMI IDs.
53
+ aws_lookup_ubuntu_ami() {
54
+ local ami
55
+ ami=$(aws ssm get-parameters \
56
+ --names /aws/service/canonical/ubuntu/server/22.04/stable/current/amd64/hvm/ebs-gp2/ami-id \
57
+ --query 'Parameters[0].Value' --output text 2>/dev/null || true)
58
+ if [ -n "$ami" ] && [ "$ami" != "None" ]; then
59
+ echo "$ami"
60
+ return 0
61
+ fi
62
+
63
+ # Some AWS CLI environments return an empty SSM public parameter result.
64
+ # Fall back to Canonical's official owner and select the newest Jammy image.
65
+ aws ec2 describe-images \
66
+ --owners 099720109477 \
67
+ --filters \
68
+ 'Name=name,Values=ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*' \
69
+ 'Name=architecture,Values=x86_64' \
70
+ 'Name=virtualization-type,Values=hvm' \
71
+ 'Name=root-device-type,Values=ebs' \
72
+ --query 'Images | sort_by(@, &CreationDate)[-1].ImageId' --output text 2>/dev/null || echo "None"
73
+ }
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env bash
2
+ # lib/domain.sh - domain/DNS helpers for the production deployment path.
3
+
4
+ domain_normalize() {
5
+ printf '%s' "$1" | tr '[:upper:]' '[:lower:]'
6
+ }
7
+
8
+ domain_is_formal_name() {
9
+ local domain=$1 label
10
+ domain=$(domain_normalize "$domain")
11
+ [ -n "$domain" ] || return 1
12
+ [ "${#domain}" -le 253 ] || return 1
13
+ [[ "$domain" =~ ^[0-9]+(\.[0-9]+){3}$ ]] && return 1
14
+ case "$domain" in
15
+ "."*|*"."|*..*|*"_"*|*"/"*|*":"*|*"*"*|localhost|*.localhost|*sslip.io|*.sslip.io|*nip.io|*.nip.io|*xip.io|*.xip.io|*localtest.me|*.localtest.me|*lvh.me|*.lvh.me)
16
+ return 1
17
+ ;;
18
+ *.*) ;;
19
+ *) return 1 ;;
20
+ esac
21
+ IFS=. read -r -a labels <<< "$domain"
22
+ for label in "${labels[@]}"; do
23
+ [ -n "$label" ] || return 1
24
+ [ "${#label}" -le 63 ] || return 1
25
+ case "$label" in
26
+ -*|*-|*[!A-Za-z0-9-]*)
27
+ return 1
28
+ ;;
29
+ esac
30
+ done
31
+ return 0
32
+ }
33
+
34
+ domain_has_dns_record() {
35
+ local domain=$1
36
+ [ -n "$domain" ] || return 1
37
+
38
+ if command -v dig >/dev/null 2>&1; then
39
+ dig +short A "$domain" 2>/dev/null | grep -qE '^[0-9.]+$' && return 0
40
+ dig +short AAAA "$domain" 2>/dev/null | grep -q ':' && return 0
41
+ dig +short CNAME "$domain" 2>/dev/null | grep -q '.' && return 0
42
+ fi
43
+
44
+ if command -v nslookup >/dev/null 2>&1; then
45
+ nslookup "$domain" 2>/dev/null | grep -qE 'Address: [0-9a-fA-F:.]+' && return 0
46
+ fi
47
+
48
+ if command -v powershell.exe >/dev/null 2>&1; then
49
+ powershell.exe -NoProfile -Command "Resolve-DnsName -Name '$domain' -Type A -ErrorAction SilentlyContinue | Where-Object { \$_.IPAddress } | Select-Object -First 1" >/dev/null 2>&1 && return 0
50
+ fi
51
+
52
+ if command -v getent >/dev/null 2>&1; then
53
+ getent hosts "$domain" 2>/dev/null | grep -qE '^[0-9a-fA-F:.]+' && return 0
54
+ fi
55
+
56
+ return 1
57
+ }
58
+
59
+ domain_authoritative_servers() {
60
+ local domain=$1 query labels i zone servers
61
+ [ -n "$domain" ] || return 1
62
+
63
+ if command -v powershell.exe >/dev/null 2>&1; then
64
+ query=$(printf '%s' "$domain" | sed "s/'/''/g")
65
+ powershell.exe -NoProfile -Command "
66
+ \$labels = '$query'.Split('.');
67
+ for (\$i = 0; \$i -lt \$labels.Length - 1; \$i++) {
68
+ \$zone = (\$labels[\$i..\$(\$labels.Length - 1)] -join '.');
69
+ \$servers = Resolve-DnsName -Name \$zone -Type NS -ErrorAction SilentlyContinue |
70
+ Where-Object { \$_.NameHost } |
71
+ Select-Object -ExpandProperty NameHost;
72
+ if (\$servers) {
73
+ \$servers | ForEach-Object { \$_.TrimEnd('.') };
74
+ exit 0;
75
+ }
76
+ }
77
+ exit 1
78
+ " 2>/dev/null && return 0
79
+ fi
80
+
81
+ if command -v dig >/dev/null 2>&1; then
82
+ IFS=. read -r -a labels <<< "$domain"
83
+ for ((i=0; i<${#labels[@]}-1; i++)); do
84
+ zone=$(IFS=.; echo "${labels[*]:$i}")
85
+ servers=$(dig +short NS "$zone" 2>/dev/null | sed 's/\.$//' | sed '/^$/d')
86
+ [ -n "$servers" ] && { printf '%s\n' "$servers"; return 0; }
87
+ done
88
+ fi
89
+
90
+ if command -v nslookup >/dev/null 2>&1; then
91
+ IFS=. read -r -a labels <<< "$domain"
92
+ for ((i=0; i<${#labels[@]}-1; i++)); do
93
+ zone=$(IFS=.; echo "${labels[*]:$i}")
94
+ servers=$(nslookup -type=NS "$zone" 2>/dev/null | awk '/nameserver =/ { sub(/\.$/, "", $4); print $4 }')
95
+ [ -n "$servers" ] && { printf '%s\n' "$servers"; return 0; }
96
+ done
97
+ fi
98
+
99
+ return 1
100
+ }
101
+
102
+ domain_authoritative_resolves_to_ip() {
103
+ local domain=$1 ip=$2 server found=0
104
+ [ -n "$domain" ] && [ -n "$ip" ] || return 2
105
+
106
+ while IFS= read -r server; do
107
+ server=${server%$'\r'}
108
+ server=${server%.}
109
+ [ -n "$server" ] || continue
110
+ found=1
111
+
112
+ if command -v dig >/dev/null 2>&1; then
113
+ dig +short "@$server" A "$domain" 2>/dev/null | grep -qFx "$ip" && return 0
114
+ fi
115
+
116
+ if command -v nslookup >/dev/null 2>&1; then
117
+ nslookup "$domain" "$server" 2>/dev/null | awk -v want="$ip" '
118
+ /^Name:/ { in_answer = 1; next }
119
+ in_answer && /^Address:[[:space:]]*/ {
120
+ addr = $2
121
+ sub(/#.*$/, "", addr)
122
+ if (addr == want) found = 1
123
+ }
124
+ END { exit(found ? 0 : 1) }
125
+ ' && return 0
126
+ fi
127
+
128
+ if command -v powershell.exe >/dev/null 2>&1; then
129
+ powershell.exe -NoProfile -Command "\$r = Resolve-DnsName -Name '$domain' -Type A -Server '$server' -ErrorAction SilentlyContinue | Where-Object { \$_.IPAddress -eq '$ip' }; if (\$r) { exit 0 } else { exit 1 }" >/dev/null 2>&1 && return 0
130
+ fi
131
+ done < <(domain_authoritative_servers "$domain")
132
+
133
+ [ "$found" -eq 1 ] && return 1
134
+ return 2
135
+ }
136
+
137
+ domain_resolves_to_ip() {
138
+ local domain=$1 ip=$2 auth_rc
139
+ [ -n "$domain" ] && [ -n "$ip" ] || return 1
140
+
141
+ domain_authoritative_resolves_to_ip "$domain" "$ip"
142
+ auth_rc=$?
143
+ case "$auth_rc" in
144
+ 0) return 0 ;;
145
+ 1) return 1 ;;
146
+ esac
147
+
148
+ if command -v dig >/dev/null 2>&1; then
149
+ dig +short A "$domain" 2>/dev/null | grep -qFx "$ip" && return 0
150
+ fi
151
+
152
+ if command -v nslookup >/dev/null 2>&1; then
153
+ nslookup "$domain" 2>/dev/null | awk -v want="$ip" '
154
+ /^Name:/ { in_answer = 1; next }
155
+ in_answer && /^Address:[[:space:]]*/ {
156
+ addr = $2
157
+ sub(/#.*$/, "", addr)
158
+ if (addr == want) found = 1
159
+ }
160
+ END { exit(found ? 0 : 1) }
161
+ ' && return 0
162
+ fi
163
+
164
+ if command -v powershell.exe >/dev/null 2>&1; then
165
+ powershell.exe -NoProfile -Command "\$r = Resolve-DnsName -Name '$domain' -Type A -ErrorAction SilentlyContinue | Where-Object { \$_.IPAddress -eq '$ip' }; if (\$r) { exit 0 } else { exit 1 }" >/dev/null 2>&1 && return 0
166
+ local server
167
+ while IFS= read -r server; do
168
+ server=${server%$'\r'}
169
+ [ -n "$server" ] || continue
170
+ powershell.exe -NoProfile -Command "\$r = Resolve-DnsName -Name '$domain' -Type A -Server '$server' -ErrorAction SilentlyContinue | Where-Object { \$_.IPAddress -eq '$ip' }; if (\$r) { exit 0 } else { exit 1 }" >/dev/null 2>&1 && return 0
171
+ done < <(domain_authoritative_servers "$domain")
172
+ fi
173
+
174
+ return 1
175
+ }
@@ -0,0 +1,240 @@
1
+ #!/usr/bin/env bash
2
+ # lib/operation_report.sh - redacted operation reports for deploy/destroy flows.
3
+
4
+ operation_report_now() {
5
+ date -u +%Y-%m-%dT%H:%M:%SZ
6
+ }
7
+
8
+ operation_report_service_id() {
9
+ local state=$1 service_id
10
+ service_id=$(jq -r '.agent_service_id // .domain // empty' "$state")
11
+ printf '%s\n' "${service_id:-unknown-service}"
12
+ }
13
+
14
+ operation_report_default_path() {
15
+ local operation=$1 state=$2 service_id service_dir root
16
+ service_id=$(operation_report_service_id "$state")
17
+ service_dir=$(jq -r '.agent_service_dir // empty' "$state")
18
+ [ -n "$service_dir" ] || service_dir=$(dirname "$state")
19
+ case "$operation" in
20
+ destroy)
21
+ root=${DIREXIO_HOME:-$HOME/.direxio}
22
+ printf '%s/reports/%s/operation-report.json\n' "$root" "$service_id"
23
+ ;;
24
+ *)
25
+ printf '%s/operation-report.json\n' "$service_dir"
26
+ ;;
27
+ esac
28
+ }
29
+
30
+ operation_report_json() {
31
+ local operation=$1 status=$2 state=$3 generated_at=$4
32
+ jq -n \
33
+ --arg operation_type "$operation" \
34
+ --arg status "$status" \
35
+ --arg generated_at "$generated_at" \
36
+ --arg state_json "$state" \
37
+ --slurpfile state "$state" '
38
+ $state[0] as $st |
39
+ def redacted_status:
40
+ if (($st.password // "") | tostring | length) > 0
41
+ then "available_in_state_password_field_redacted"
42
+ else "missing"
43
+ end;
44
+ def phase_statuses:
45
+ ($st.phases // {} | with_entries(.value = (.value.status // "unknown")));
46
+ def user_gate($gate; $default):
47
+ ($st.user_confirmations[$gate].status // $default);
48
+ def local_refresh_status:
49
+ if ($st.agent_install_status // "") == "refresh_pending"
50
+ then "refresh_pending"
51
+ else "current_or_not_recorded"
52
+ end;
53
+ def redact_text($value):
54
+ def redact_one($secret):
55
+ if (($secret // "") | tostring | length) > 0
56
+ then split($secret | tostring) | join("<redacted>")
57
+ else .
58
+ end;
59
+ ($value // "" | tostring)
60
+ | redact_one($st.password)
61
+ | redact_one($st.access_token)
62
+ | redact_one($st.agent_token)
63
+ | redact_one($st.matrix_access_token)
64
+ | redact_one($st.owner_access_token)
65
+ | redact_one($st.aws_secret_access_key)
66
+ | redact_one($st.aws_session_token)
67
+ | gsub("[0-9]{8,}"; "<redacted>");
68
+ def user_gate_detail($gate; $default):
69
+ ($st.user_confirmations[$gate] // {}) as $gate_state |
70
+ {
71
+ status: ($gate_state.status // $default),
72
+ ts: ($gate_state.ts // ""),
73
+ evidence: redact_text($gate_state.evidence // ""),
74
+ evidence_redacted: ((redact_text($gate_state.evidence // "")) != (($gate_state.evidence // "") | tostring))
75
+ }
76
+ + (if $gate == "agent_mcp_runtime" then {
77
+ runtime_summary_status: ($gate_state.runtime_summary_status // ""),
78
+ runtime_probe_confirmed: ($gate_state.runtime_probe_confirmed // false)
79
+ } else {} end);
80
+ def billable:
81
+ [
82
+ (if (($st.resources.instance_id // "") | tostring | length) > 0 then "EC2 \($st.resources.instance_id)" else empty end),
83
+ (if (($st.resources.root_volume_id // "") | tostring | length) > 0 then "EBS root volume \($st.resources.root_volume_id)" else empty end),
84
+ (if (($st.resources.public_ip // "") | tostring | length) > 0 then "public IPv4 \($st.resources.public_ip)" else empty end),
85
+ (if (($st.resources.eip_id // "") | tostring | length) > 0 then "Elastic IP \($st.resources.eip_id)" else empty end),
86
+ (if (($st.resources.route53_zone_id // "") | tostring | length) > 0 then "Route53 hosted zone \($st.resources.route53_zone_id)" else empty end)
87
+ ];
88
+ def destroy_status($key):
89
+ ($st.destroy_evidence[$key].status // "not_checked");
90
+ def status_not_in($status; $safe):
91
+ (($safe | index($status)) == null);
92
+ def destroy_billable_residue:
93
+ [
94
+ (if (($st.resources.instance_id // "") | tostring | length) > 0
95
+ and status_not_in(destroy_status("ec2_instance"); ["terminated", "not_found", "skipped"])
96
+ then "EC2 \($st.resources.instance_id) status=\(destroy_status("ec2_instance"))"
97
+ else empty end),
98
+ (if (($st.resources.root_volume_id // "") | tostring | length) > 0
99
+ and status_not_in(destroy_status("ebs_root_volume"); ["deleted", "skipped"])
100
+ then "EBS root volume \($st.resources.root_volume_id) status=\(destroy_status("ebs_root_volume"))"
101
+ else empty end),
102
+ (if (($st.resources.eip_id // "") | tostring | length) > 0
103
+ and status_not_in(destroy_status("elastic_ip"); ["released", "skipped"])
104
+ then "Elastic IP \($st.resources.eip_id) status=\(destroy_status("elastic_ip"))"
105
+ else empty end),
106
+ (if (($st.resources.route53_zone_id // "") | tostring | length) > 0
107
+ and status_not_in(destroy_status("route53_hosted_zone"); ["deleted", "skipped"])
108
+ then "Route53 hosted zone \($st.resources.route53_zone_id) status=\(destroy_status("route53_hosted_zone"))"
109
+ else empty end)
110
+ ];
111
+ {
112
+ operation_type: $operation_type,
113
+ status: $status,
114
+ generated_at: $generated_at,
115
+ domain: ($st.domain // ""),
116
+ service_id: ($st.agent_service_id // $st.domain // ""),
117
+ service_dir: ($st.agent_service_dir // ""),
118
+ state_json: $state_json,
119
+ delivery: {
120
+ app_domain: ($st.domain // ""),
121
+ product_completion_status: $status,
122
+ init_code_status: redacted_status,
123
+ init_code_secret_redacted: true,
124
+ user_path: "enter app_domain and the eight-digit initialization code in the App"
125
+ },
126
+ agent: {
127
+ node_id: ($st.agent_node_id // ""),
128
+ room_id: ($st.agent_room_id // ""),
129
+ runtime: ($st.agent_runtime // "unknown"),
130
+ service_id: ($st.agent_service_id // $st.domain // ""),
131
+ credentials_file: ($st.agent_credentials_file // "")
132
+ },
133
+ gates: {
134
+ automated: phase_statuses,
135
+ user_confirmation: {
136
+ app_initialization: user_gate("app_initialization"; "pending_user_confirmation"),
137
+ real_chat: user_gate("real_chat"; "pending_user_confirmation"),
138
+ agent_mcp_runtime: user_gate("agent_mcp_runtime"; "pending_runtime_confirmation")
139
+ },
140
+ user_confirmation_details: {
141
+ app_initialization: user_gate_detail("app_initialization"; "pending_user_confirmation"),
142
+ real_chat: user_gate_detail("real_chat"; "pending_user_confirmation"),
143
+ agent_mcp_runtime: user_gate_detail("agent_mcp_runtime"; "pending_runtime_confirmation")
144
+ }
145
+ },
146
+ runtime_checks: {
147
+ summary: ($st.runtime_checks.summary // {status: "not_run"}),
148
+ connect_daemon: ($st.runtime_checks.connect_daemon // {status: "not_run"}),
149
+ mcp_doctor: ($st.runtime_checks.mcp_doctor // {status: "not_run"}),
150
+ mcp_smoke: ($st.runtime_checks.mcp_smoke // {status: "not_run"}),
151
+ mcp_tools: ($st.runtime_checks.mcp_tools // {status: "not_run"})
152
+ },
153
+ credentials: {
154
+ status: local_refresh_status,
155
+ credentials_file: ($st.agent_credentials_file // ""),
156
+ contains_secrets: true,
157
+ values_redacted: true
158
+ },
159
+ connect: {
160
+ package: ($st.cc_connect_npm_package // "direxio-connent@latest"),
161
+ agent: ($st.cc_connect_agent // ""),
162
+ config: ($st.cc_connect_config // ""),
163
+ install_status: ($st.agent_install_status // "")
164
+ },
165
+ mcp: {
166
+ status: local_refresh_status,
167
+ package: ($st.mcp_npm_package // "direxio-mcp@latest"),
168
+ server_name: ($st.mcp_server_name // ""),
169
+ config_dir: ($st.mcp_config_dir // ""),
170
+ codex: ($st.mcp_codex_config // ""),
171
+ openclaw: ($st.mcp_openclaw_config // ""),
172
+ hermes: ($st.mcp_hermes_config // ""),
173
+ doctor: ($st.mcp_doctor_command // "")
174
+ },
175
+ resources: {
176
+ region: ($st.region // ""),
177
+ domain_mode: ($st.domain_mode // ""),
178
+ instance_type: ($st.instance_type // ""),
179
+ instance_id: ($st.resources.instance_id // ""),
180
+ root_volume_id: ($st.resources.root_volume_id // ""),
181
+ public_ip: ($st.resources.public_ip // ""),
182
+ eip_id: ($st.resources.eip_id // ""),
183
+ route53_zone_id: ($st.resources.route53_zone_id // ""),
184
+ route53_zone_name: ($st.resources.route53_zone_name // ""),
185
+ route53_zone_created_by_deployer: ($st.resources.route53_zone_created_by_deployer // ""),
186
+ route53_name_servers: ($st.resources.route53_name_servers // ""),
187
+ route53_existing_a_value: ($st.resources.route53_existing_a_value // ""),
188
+ route53_pending_a_value: ($st.resources.route53_pending_a_value // ""),
189
+ route53_overwrite_confirmed: ($st.resources.route53_overwrite_confirmed // ""),
190
+ sg_id: ($st.resources.sg_id // ""),
191
+ key_name: ($st.resources.key_name // "")
192
+ },
193
+ billing: {
194
+ keeps_billing_until_destroy: ($operation_type != "destroy"),
195
+ recorded_billable_resources: billable,
196
+ cost_estimate: ($st.cost_estimate // null),
197
+ destroy_cleanup_status: (
198
+ if $operation_type != "destroy" then "not_destroy"
199
+ elif (destroy_billable_residue | length) == 0 then "no_recorded_billable_resource_residue"
200
+ else "possible_billable_resource_residue"
201
+ end
202
+ ),
203
+ possible_remaining_billable_resources: (
204
+ if $operation_type == "destroy" then destroy_billable_residue else [] end
205
+ )
206
+ },
207
+ security: {
208
+ secrets_included: false,
209
+ values_redacted: true,
210
+ root_access_key_allowed: true,
211
+ temporary_iam_cleanup_required: true,
212
+ temporary_iam_cleanup_action: "if a temporary DirexioDeployer access key was used, delete or disable it after deployment, or reduce it to a maintenance-only policy"
213
+ }
214
+ }
215
+ + (if $operation_type == "destroy" then {
216
+ destroy: {
217
+ resources_processed_from_state: true,
218
+ user_managed_dns_not_removed: true,
219
+ purchased_domain_not_removed: true,
220
+ local_service_dir: ($st.agent_service_dir // ""),
221
+ evidence: ($st.destroy_evidence // {})
222
+ }
223
+ } else {} end)
224
+ '
225
+ }
226
+
227
+ operation_report_write() {
228
+ local operation=$1 status=$2 state=$3 output=${4:-} generated_at tmp
229
+ [ -f "$state" ] || {
230
+ echo "state.json not found for operation report: $state" >&2
231
+ return 1
232
+ }
233
+ [ -n "$output" ] || output=$(operation_report_default_path "$operation" "$state")
234
+ mkdir -p "$(dirname "$output")"
235
+ generated_at=$(operation_report_now)
236
+ tmp="$output.tmp.$$"
237
+ operation_report_json "$operation" "$status" "$state" "$generated_at" > "$tmp"
238
+ mv "$tmp" "$output"
239
+ printf '%s\n' "$output"
240
+ }