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.
- package/AGENTS.md +92 -0
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/README_zh.md +218 -0
- package/SKILL.md +722 -0
- package/agents/README.md +25 -0
- package/agents/openai.yaml +12 -0
- package/bin/direxio-deployer.mjs +375 -0
- package/package.json +28 -0
- package/references/agent-targets.md +128 -0
- package/references/architecture.md +44 -0
- package/references/bug-history.md +78 -0
- package/references/deployment-lessons.md +218 -0
- package/references/deployment-optimization-audit.md +317 -0
- package/references/deployment-workflow.md +341 -0
- package/references/iam-policy.json +52 -0
- package/references/runtime-wiring.md +209 -0
- package/references/state-machine.md +46 -0
- package/references/token-refresh.md +81 -0
- package/references/tooling.md +106 -0
- package/references/troubleshooting.md +26 -0
- package/references/user-journey.md +75 -0
- package/references/verification-recovery.md +84 -0
- package/references/voip-turn-runbook.md +154 -0
- package/references/windows-deployment-notes.md +119 -0
- package/scripts/aws-credentials.sh +195 -0
- package/scripts/cloud-init/Caddyfile +48 -0
- package/scripts/cloud-init/docker-compose.yml +125 -0
- package/scripts/cloud-init/init-tokens.sh +238 -0
- package/scripts/cloud-init/user-data.yaml +40 -0
- package/scripts/destroy.ps1 +77 -0
- package/scripts/destroy.sh +589 -0
- package/scripts/lib/aws.sh +73 -0
- package/scripts/lib/domain.sh +175 -0
- package/scripts/lib/operation_report.sh +240 -0
- package/scripts/lib/ops.sh +230 -0
- package/scripts/lib/paths.sh +35 -0
- package/scripts/lib/state.sh +137 -0
- package/scripts/mcp-tools-list.mjs +95 -0
- package/scripts/orchestrate.ps1 +112 -0
- package/scripts/orchestrate.sh +1126 -0
- package/scripts/phases/s0_prereq_aws.sh +39 -0
- package/scripts/phases/s1_preflight.sh +72 -0
- package/scripts/phases/s2_domain.sh +103 -0
- package/scripts/phases/s3_provision.sh +421 -0
- package/scripts/phases/s4_bootstrap_stack.sh +38 -0
- package/scripts/phases/s5_init_tokens.sh +118 -0
- package/scripts/phases/s6_wire_local.sh +1435 -0
- package/scripts/phases/s7_verify_e2e.sh +136 -0
- package/scripts/pricing-estimate.sh +256 -0
- package/scripts/render/render-userdata.sh +86 -0
- package/scripts/reset-app-data.sh +40 -0
- package/scripts/update.sh +30 -0
- package/tests/aws_credentials_test.sh +139 -0
- package/tests/connect_daemon_runtime_check_test.sh +120 -0
- package/tests/default_paths_test.sh +58 -0
- package/tests/destroy_local_bridge_test.sh +154 -0
- package/tests/destroy_root_identity_test.sh +91 -0
- package/tests/destroy_route53_zone_test.sh +80 -0
- package/tests/domain_authoritative_dns_test.sh +49 -0
- package/tests/mcp_doctor_runtime_check_test.sh +86 -0
- package/tests/mcp_smoke_runtime_check_test.sh +121 -0
- package/tests/mcp_tools_runtime_check_test.sh +123 -0
- package/tests/npm_skill_distribution_test.sh +95 -0
- package/tests/operation_report_test.sh +258 -0
- package/tests/orchestrate_status_recovery_test.sh +91 -0
- package/tests/phase_timeout_test.sh +88 -0
- package/tests/pricing_estimate_test.sh +159 -0
- package/tests/render_userdata_remote_nodes_test.sh +40 -0
- package/tests/root_volume_tracking_test.sh +41 -0
- package/tests/route53_overwrite_guard_test.sh +86 -0
- package/tests/route53_zone_auto_create_test.sh +66 -0
- package/tests/runtime_summary_check_test.sh +203 -0
- package/tests/s6_wire_local_test.sh +405 -0
- package/tests/skill_structure_test.sh +298 -0
- package/tests/update_reset_ops_test.sh +230 -0
- 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
|
+
}
|