forked from agentscope-ai/AgentTeams
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstart-manager-agent.sh
More file actions
executable file
·1207 lines (1116 loc) · 63.7 KB
/
Copy pathstart-manager-agent.sh
File metadata and controls
executable file
·1207 lines (1116 loc) · 63.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/bash
# start-manager-agent.sh - Initialize and start the Manager Agent
# Supports local (supervisord), cloud (SAE), and K8s (Helm) deployments.
# In local mode this is the last supervisord component to start (priority 800).
# In cloud/k8s mode (AGENTTEAMS_RUNTIME=aliyun|k8s) this is the container entrypoint.
#
# Runtime selection:
# AGENTTEAMS_MANAGER_RUNTIME=qwenpaw - QwenPaw 2.0 workspace mode (default)
# AGENTTEAMS_MANAGER_RUNTIME=copaw - Legacy alias, auto-routed to QwenPaw
# AGENTTEAMS_MANAGER_RUNTIME=openclaw - OpenClaw gateway mode
source /opt/agentteams/scripts/lib/agentteams-env.sh
# ============================================================
# Runtime selection
# ============================================================
MANAGER_RUNTIME="${AGENTTEAMS_MANAGER_RUNTIME:-qwenpaw}"
case "${MANAGER_RUNTIME}" in
copaw|qwenpaw)
log "Manager runtime: QwenPaw (Python workspace, runtime=${MANAGER_RUNTIME})"
;;
*)
log "Manager runtime: OpenClaw (Node.js gateway)"
MANAGER_RUNTIME="openclaw"
;;
esac
# ============================================================
# Set timezone from TZ env var
# ============================================================
if [ -n "${TZ}" ] && [ -f "/usr/share/zoneinfo/${TZ}" ]; then
ln -sf "/usr/share/zoneinfo/${TZ}" /etc/localtime
echo "${TZ}" > /etc/timezone
log "Timezone set to ${TZ}"
fi
export MATRIX_DOMAIN="${AGENTTEAMS_MATRIX_DOMAIN:-matrix-local.agentteams.io:8080}"
AI_GATEWAY_DOMAIN="${AGENTTEAMS_AI_GATEWAY_DOMAIN:-aigw-local.agentteams.io}"
# ============================================================
# YOLO mode promotion
# ============================================================
# In embedded mode the controller does not propagate AGENTTEAMS_YOLO to the
# manager container, but installer / test scripts touch a marker file at
# `${WORKSPACE}/yolo-mode` instead. Promote that marker to the env var so the
# agent's documented YOLO check (`AGENTTEAMS_YOLO=1`) reliably detects it without
# depending on filesystem lookups during a turn.
if [ -z "${AGENTTEAMS_YOLO:-}" ] && [ -f /root/manager-workspace/yolo-mode ]; then
export AGENTTEAMS_YOLO=1
log "YOLO mode marker detected at /root/manager-workspace/yolo-mode; AGENTTEAMS_YOLO=1 exported"
fi
# ============================================================
# Cloud/K8s mode: validate required environment variables + initial credentials
# ============================================================
if [ "${AGENTTEAMS_RUNTIME}" = "aliyun" ] || [ "${AGENTTEAMS_RUNTIME}" = "k8s" ]; then
: "${AGENTTEAMS_MATRIX_URL:?AGENTTEAMS_MATRIX_URL is required}"
: "${AGENTTEAMS_MATRIX_DOMAIN:?AGENTTEAMS_MATRIX_DOMAIN is required}"
: "${AGENTTEAMS_AI_GATEWAY_URL:?AGENTTEAMS_AI_GATEWAY_URL is required}"
if [ "${AGENTTEAMS_RUNTIME}" = "aliyun" ]; then
: "${AGENTTEAMS_MANAGER_GATEWAY_KEY:?AGENTTEAMS_MANAGER_GATEWAY_KEY is required}"
: "${AGENTTEAMS_MANAGER_PASSWORD:?AGENTTEAMS_MANAGER_PASSWORD is required (cloud containers are stateless, password must be injected)}"
fi
if [ "${AGENTTEAMS_RUNTIME}" = "k8s" ]; then
# K8s mode: controller handles initialization (admin registration, Higress setup).
# Manager only needs credentials injected by the ManagerReconciler.
: "${AGENTTEAMS_MANAGER_GATEWAY_KEY:?AGENTTEAMS_MANAGER_GATEWAY_KEY is required (injected by controller)}"
# AGENTTEAMS_MANAGER_PASSWORD is optional: not needed in AppService mode
# (token obtained via AS login), only required in legacy password mode.
else
# Cloud (aliyun) mode: Manager still does its own initialization
: "${AGENTTEAMS_REGISTRATION_TOKEN:?AGENTTEAMS_REGISTRATION_TOKEN is required}"
: "${AGENTTEAMS_ADMIN_USER:?AGENTTEAMS_ADMIN_USER is required}"
: "${AGENTTEAMS_ADMIN_PASSWORD:?AGENTTEAMS_ADMIN_PASSWORD is required}"
fi
log "${AGENTTEAMS_RUNTIME} mode: validating environment... OK"
log " Matrix: ${AGENTTEAMS_MATRIX_URL}, AI Gateway: ${AGENTTEAMS_AI_GATEWAY_URL}, Storage: ${AGENTTEAMS_FS_BUCKET}"
if [ "${AGENTTEAMS_RUNTIME}" = "aliyun" ]; then
ensure_mc_credentials || { log "FATAL: Initial STS credential fetch failed"; exit 1; }
fi
fi
# ============================================================
# Local mode: host symlinks, /etc/hosts, wait for local services
# ============================================================
if [ "${AGENTTEAMS_RUNTIME}" != "aliyun" ] && [ "${AGENTTEAMS_RUNTIME}" != "k8s" ]; then
# Create symlink for host directory access
if [ -d "/host-share" ]; then
ORIGINAL_HOST_HOME="${HOST_ORIGINAL_HOME:-$HOME}"
if [ ! -e "${ORIGINAL_HOST_HOME}" ] && [ "${ORIGINAL_HOST_HOME}" != "/" ] && [ "${ORIGINAL_HOST_HOME}" != "/root" ] && [ "${ORIGINAL_HOST_HOME}" != "/data" ] && [ "${ORIGINAL_HOST_HOME}" != "/host-share" ]; then
mkdir -p "$(dirname "${ORIGINAL_HOST_HOME}")"
ln -sfn /host-share "${ORIGINAL_HOST_HOME}"
log "Created symlink: ${ORIGINAL_HOST_HOME} -> /host-share"
else
ln -sfn /host-share /root/host-home
log "Created fallback symlink: /root/host-home -> /host-share"
fi
fi
# Add local domains to /etc/hosts
HOSTS_DOMAINS="${MATRIX_DOMAIN%%:*} ${AGENTTEAMS_MATRIX_CLIENT_DOMAIN:-matrix-client-local.agentteams.io} ${AI_GATEWAY_DOMAIN} ${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}"
if ! grep -q "${AI_GATEWAY_DOMAIN}" /etc/hosts 2>/dev/null; then
echo "127.0.0.1 ${HOSTS_DOMAINS}" >> /etc/hosts
log "Added local domains to /etc/hosts"
fi
# Wait for local infrastructure
waitForService "Higress Gateway" "127.0.0.1" 8080 180
waitForService "Higress Console" "127.0.0.1" 8001 180
waitForService "Tuwunel" "127.0.0.1" 6167 120
waitForHTTP "Tuwunel Matrix API" "${AGENTTEAMS_MATRIX_URL}/_tuwunel/server_version" 120
waitForService "MinIO" "127.0.0.1" 9000 120
else
# Cloud/K8s mode: wait for external Tuwunel
log "Waiting for Tuwunel Matrix server at ${AGENTTEAMS_MATRIX_URL}..."
_retry=0
while [ "${_retry}" -lt 30 ]; do
if curl -sf "${AGENTTEAMS_MATRIX_URL}/_matrix/client/versions" > /dev/null 2>&1; then
log "Tuwunel is ready"
break
fi
_retry=$((_retry + 1))
log " Waiting for Tuwunel (attempt ${_retry}/30)..."
sleep 5
done
if [ "${_retry}" -ge 30 ]; then
log "ERROR: Tuwunel not reachable at ${AGENTTEAMS_MATRIX_URL}"
exit 1
fi
fi
# ============================================================
# Auto-generate secrets if not provided via environment
# Persisted to /data so they survive container restart
# ============================================================
SECRETS_FILE="/data/agentteams-secrets.env"
if [ -f "${SECRETS_FILE}" ]; then
source "${SECRETS_FILE}"
log "Loaded persisted secrets from ${SECRETS_FILE}"
fi
if [ -z "${AGENTTEAMS_MANAGER_GATEWAY_KEY}" ]; then
export AGENTTEAMS_MANAGER_GATEWAY_KEY="$(generateKey 32)"
log "Auto-generated AGENTTEAMS_MANAGER_GATEWAY_KEY"
fi
if [ -z "${AGENTTEAMS_MANAGER_PASSWORD}" ]; then
export AGENTTEAMS_MANAGER_PASSWORD="$(generateKey 16)"
log "Auto-generated AGENTTEAMS_MANAGER_PASSWORD"
fi
# Persist secrets so they survive supervisord restart
mkdir -p /data
cat > "${SECRETS_FILE}" <<EOF
export AGENTTEAMS_MANAGER_GATEWAY_KEY="${AGENTTEAMS_MANAGER_GATEWAY_KEY}"
export AGENTTEAMS_MANAGER_PASSWORD="${AGENTTEAMS_MANAGER_PASSWORD}"
EOF
chmod 600 "${SECRETS_FILE}"
# Cloud mode: pull workspace from OSS before initialization
if [ "${AGENTTEAMS_RUNTIME}" = "aliyun" ]; then
AGENTTEAMS_FS="/root/agentteams-fs"
mkdir -p "${AGENTTEAMS_FS}/shared" "${AGENTTEAMS_FS}/agents"
log "Pulling workspace from OSS..."
ensure_mc_credentials
mc mirror "${AGENTTEAMS_STORAGE_PREFIX}/manager/" /root/manager-workspace/ --overwrite 2>/dev/null || true
mc mirror "${AGENTTEAMS_STORAGE_PREFIX}/shared/" "${AGENTTEAMS_FS}/shared/" --overwrite 2>/dev/null || true
mc mirror "${AGENTTEAMS_STORAGE_PREFIX}/agents/" "${AGENTTEAMS_FS}/agents/" --overwrite 2>/dev/null || true
# Keep the canonical filesystem link in the Manager workspace.
ln -sfn "${AGENTTEAMS_FS}" /root/manager-workspace/agentteams-fs
ln -sfn "${AGENTTEAMS_FS}" /root/manager-workspace/agentteams-fs
fi
# K8s mode: sync workspace from cluster-internal MinIO
if [ "${AGENTTEAMS_RUNTIME}" = "k8s" ]; then
AGENTTEAMS_FS="/root/agentteams-fs"
mkdir -p "${AGENTTEAMS_FS}/shared" "${AGENTTEAMS_FS}/agents" "${AGENTTEAMS_FS}/agentteams-config"
log "Configuring mc alias for cluster MinIO..."
mc alias set agentteams "${AGENTTEAMS_FS_ENDPOINT}" "${AGENTTEAMS_FS_ACCESS_KEY}" "${AGENTTEAMS_FS_SECRET_KEY}"
log "Syncing workspace from MinIO..."
mc mirror "${AGENTTEAMS_STORAGE_PREFIX}/manager/" /root/manager-workspace/ --overwrite 2>/dev/null || true
mc mirror "${AGENTTEAMS_STORAGE_PREFIX}/" "${AGENTTEAMS_FS}/" --overwrite 2>/dev/null || true
ln -sfn "${AGENTTEAMS_FS}" /root/manager-workspace/agentteams-fs
ln -sfn "${AGENTTEAMS_FS}" /root/manager-workspace/agentteams-fs
touch "${AGENTTEAMS_FS}/.initialized"
fi
# ============================================================
# Initialize / upgrade Manager workspace
# First boot: full init via upgrade-builtins.sh
# Subsequent boots: compare image version; upgrade only if changed
# ============================================================
mkdir -p /root/manager-workspace
IMAGE_VERSION=$(cat /opt/agentteams/agent/.builtin-version 2>/dev/null || echo "unknown")
INSTALLED_VERSION=$(cat /root/manager-workspace/.builtin-version 2>/dev/null || echo "")
if [ ! -f /root/manager-workspace/.initialized ]; then
log "First boot: initializing manager workspace..."
bash /opt/agentteams/scripts/init/upgrade-builtins.sh
touch /root/manager-workspace/.initialized
log "Manager workspace initialized (version: ${IMAGE_VERSION})"
elif [ "${IMAGE_VERSION}" != "${INSTALLED_VERSION}" ] || [ "${IMAGE_VERSION}" = "latest" ]; then
log "Upgrade detected: ${INSTALLED_VERSION} -> ${IMAGE_VERSION}${IMAGE_VERSION:+ (latest: always upgrade)}"
bash /opt/agentteams/scripts/init/upgrade-builtins.sh
log "Manager workspace upgraded to version: ${IMAGE_VERSION}"
else
log "Workspace up to date (version: ${IMAGE_VERSION})"
fi
# Local mode: wait for mc mirror initialization (shared + worker data in /root/agentteams-fs/)
if [ "${AGENTTEAMS_RUNTIME}" != "aliyun" ] && [ "${AGENTTEAMS_RUNTIME}" != "k8s" ]; then
log "Waiting for MinIO storage initialization..."
_minio_wait=0
while [ ! -f /root/agentteams-fs/.initialized ]; do
sleep 2
_minio_wait=$(( _minio_wait + 1 ))
if [ "${_minio_wait}" -ge 60 ]; then
log "ERROR: MinIO storage initialization timed out after 120s"
exit 1
fi
done
log "MinIO storage initialized"
fi
# ============================================================
# Obtain Manager Matrix access token.
#
# Priority:
# 1. Pre-injected token (AGENTTEAMS_MANAGER_MATRIX_TOKEN) — set by the
# controller in both AppService mode (where no password exists) and
# legacy mode (to avoid a redundant login round-trip).
# 2. Password-based login — fallback for containers started without
# controller involvement (manual docker run, legacy installs).
# ============================================================
if [ -n "${AGENTTEAMS_MANAGER_MATRIX_TOKEN:-}" ]; then
MANAGER_TOKEN="${AGENTTEAMS_MANAGER_MATRIX_TOKEN}"
log "Manager Matrix token pre-injected by controller (token prefix: ${MANAGER_TOKEN:0:10}...)"
elif [ "${AGENTTEAMS_RUNTIME}" = "k8s" ]; then
log "K8s mode: obtaining Manager Matrix token via password login..."
_LOGIN_RESPONSE=$(curl -s -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login \
-H 'Content-Type: application/json' \
-d '{
"type": "m.login.password",
"identifier": {"type": "m.id.user", "user": "manager"},
"password": "'"${AGENTTEAMS_MANAGER_PASSWORD}"'"
}' 2>&1)
MANAGER_TOKEN=$(echo "${_LOGIN_RESPONSE}" | jq -r '.access_token' 2>/dev/null)
if [ -z "${MANAGER_TOKEN}" ] || [ "${MANAGER_TOKEN}" = "null" ]; then
log "ERROR: Failed to obtain Manager Matrix token"
log "ERROR: Login response was: ${_LOGIN_RESPONSE}"
exit 1
fi
log "Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)"
else
log "Registering human admin Matrix account..."
curl -sf -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/register \
-H 'Content-Type: application/json' \
-d '{
"username": "'"${AGENTTEAMS_ADMIN_USER}"'",
"password": "'"${AGENTTEAMS_ADMIN_PASSWORD}"'",
"auth": {
"type": "m.login.registration_token",
"token": "'"${AGENTTEAMS_REGISTRATION_TOKEN}"'"
}
}' > /dev/null 2>&1 || log "Admin account may already exist"
log "Registering Manager Agent Matrix account..."
curl -sf -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/register \
-H 'Content-Type: application/json' \
-d '{
"username": "manager",
"password": "'"${AGENTTEAMS_MANAGER_PASSWORD}"'",
"auth": {
"type": "m.login.registration_token",
"token": "'"${AGENTTEAMS_REGISTRATION_TOKEN}"'"
}
}' > /dev/null 2>&1 || log "Manager account may already exist"
# Get Manager Agent's Matrix access token
log "Obtaining Manager Matrix access token..."
_LOGIN_RESPONSE=$(curl -s -X POST ${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login \
-H 'Content-Type: application/json' \
-d '{
"type": "m.login.password",
"identifier": {"type": "m.id.user", "user": "manager"},
"password": "'"${AGENTTEAMS_MANAGER_PASSWORD}"'"
}' 2>&1)
_LOGIN_EXIT=$?
log "Matrix login HTTP exit code: ${_LOGIN_EXIT}"
log "Matrix login response: ${_LOGIN_RESPONSE}"
MANAGER_TOKEN=$(echo "${_LOGIN_RESPONSE}" | jq -r '.access_token' 2>/dev/null)
if [ -z "${MANAGER_TOKEN}" ] || [ "${MANAGER_TOKEN}" = "null" ]; then
log "ERROR: Failed to obtain Manager Matrix token (exit=${_LOGIN_EXIT})"
log "ERROR: Login response was: ${_LOGIN_RESPONSE}"
exit 1
fi
log "Manager Matrix token obtained (token prefix: ${MANAGER_TOKEN:0:10}...)"
fi
# ============================================================
# Higress Console initialization
# Docker mode: full setup-higress.sh (internal Higress at localhost:8001)
# K8s mode: skip — controller Initializer handles Higress setup
# Cloud (aliyun) mode: skip entirely (Higress managed externally)
# ============================================================
_HIGRESS_CONSOLE_URL=""
_HIGRESS_USER="${AGENTTEAMS_ADMIN_USER}"
_HIGRESS_PASS="${AGENTTEAMS_ADMIN_PASSWORD}"
if [ "${AGENTTEAMS_RUNTIME}" = "k8s" ]; then
log "K8s mode: skipping Higress initialization (handled by controller)"
elif [ "${AGENTTEAMS_RUNTIME}" != "aliyun" ]; then
_HIGRESS_CONSOLE_URL="http://127.0.0.1:8001"
fi
if [ -n "${_HIGRESS_CONSOLE_URL}" ]; then
COOKIE_FILE="/tmp/higress-session-cookie"
log "Waiting for Higress Console (${_HIGRESS_CONSOLE_URL}) to be fully ready and initializing admin..."
INIT_DONE=false
for i in $(seq 1 90); do
INIT_RESULT=$(curl -s -X POST "${_HIGRESS_CONSOLE_URL}/system/init" \
-H 'Content-Type: application/json' \
-d '{"adminUser":{"name":"'"${_HIGRESS_USER}"'","password":"'"${_HIGRESS_PASS}"'","displayName":"'"${_HIGRESS_USER}"'"}}' 2>/dev/null) || true
if echo "${INIT_RESULT}" | grep -qE '"success":true|already.?init' 2>/dev/null; then
INIT_DONE=true
break
fi
if echo "${INIT_RESULT}" | grep -q '"name"' 2>/dev/null; then
INIT_DONE=true
break
fi
sleep 2
done
if [ "${INIT_DONE}" != "true" ]; then
log "ERROR: Higress Console did not become ready within 180s"
exit 1
fi
log "Higress Console init done"
log "Logging into Higress Console..."
LOGIN_OK=false
for i in $(seq 1 10); do
HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -X POST "${_HIGRESS_CONSOLE_URL}/session/login" \
-H 'Content-Type: application/json' \
-c "${COOKIE_FILE}" \
-d '{"username":"'"${_HIGRESS_USER}"'","password":"'"${_HIGRESS_PASS}"'"}' 2>/dev/null) || true
if { [ "${HTTP_CODE}" = "200" ] || [ "${HTTP_CODE}" = "201" ]; } && [ -f "${COOKIE_FILE}" ] && [ -s "${COOKIE_FILE}" ]; then
LOGIN_OK=true
break
fi
log "Login attempt $i (HTTP ${HTTP_CODE}), retrying in 3s..."
sleep 3
done
if [ "${LOGIN_OK}" != "true" ]; then
log "ERROR: Could not login to Higress Console after retries"
exit 1
fi
log "Higress Console login successful"
VERIFY_CODE=$(curl -s -o /dev/null -w '%{http_code}' "${_HIGRESS_CONSOLE_URL}/v1/consumers" -b "${COOKIE_FILE}" 2>/dev/null) || true
if [ "${VERIFY_CODE}" = "200" ]; then
log "Console session verified (cookie valid)"
else
log "WARNING: Console session may be invalid (verify returned HTTP ${VERIFY_CODE})"
rm -f "${COOKIE_FILE}"
for i in $(seq 1 5); do
curl -s -o /dev/null -w '%{http_code}' -X POST "${_HIGRESS_CONSOLE_URL}/session/login" \
-H 'Content-Type: application/json' \
-c "${COOKIE_FILE}" \
-d '{"username":"'"${_HIGRESS_USER}"'","password":"'"${_HIGRESS_PASS}"'"}' 2>/dev/null
VERIFY2=$(curl -s -o /dev/null -w '%{http_code}' "${_HIGRESS_CONSOLE_URL}/v1/consumers" -b "${COOKIE_FILE}" 2>/dev/null) || true
if [ "${VERIFY2}" = "200" ]; then
log "Re-login successful, session verified"
break
fi
sleep 2
done
fi
export HIGRESS_COOKIE_FILE="${COOKIE_FILE}"
export HIGRESS_CONSOLE_URL="${_HIGRESS_CONSOLE_URL}"
if [ "${AGENTTEAMS_RUNTIME}" = "k8s" ]; then
# K8s mode: lightweight Higress config — only what's needed for LLM access
source /opt/agentteams/scripts/lib/base.sh
_k8s_higress_api() {
local method="$1" path="$2" desc="$3"; shift 3; local body="$*"
local tmpfile; tmpfile=$(mktemp)
local http_code
http_code=$(curl -s -o "${tmpfile}" -w '%{http_code}' -X "${method}" "${_HIGRESS_CONSOLE_URL}${path}" \
-b "${COOKIE_FILE}" -H 'Content-Type: application/json' -d "${body}" 2>/dev/null) || true
local response; response=$(cat "${tmpfile}" 2>/dev/null); rm -f "${tmpfile}"
if echo "${response}" | grep -q '"success":true' 2>/dev/null; then
log "${desc} ... OK"
elif [ "${http_code}" = "409" ]; then
log "${desc} ... already exists, skipping"
elif [ "${http_code}" = "200" ] || [ "${http_code}" = "201" ] || [ "${http_code}" = "204" ]; then
log "${desc} ... OK (HTTP ${http_code})"
else
log "WARNING: ${desc} ... (HTTP ${http_code}): ${response}"
fi
}
# 1. Service Sources (DNS type → K8s Service FQDN)
# Extract host:port from URLs for Higress service source registration
_TUWUNEL_HOST=$(echo "${AGENTTEAMS_MATRIX_URL}" | sed 's|^http[s]*://||')
_TUWUNEL_DOMAIN=$(echo "${_TUWUNEL_HOST}" | cut -d: -f1)
_TUWUNEL_PORT=$(echo "${_TUWUNEL_HOST}" | cut -d: -f2)
_k8s_higress_api POST /v1/service-sources "Registering Tuwunel service source" \
'{"type":"dns","name":"tuwunel","domain":"'"${_TUWUNEL_DOMAIN}"'","port":'"${_TUWUNEL_PORT}"'}'
if [ -n "${AGENTTEAMS_ELEMENT_WEB_URL:-}" ]; then
_ELEMENT_HOST=$(echo "${AGENTTEAMS_ELEMENT_WEB_URL}" | sed 's|^http[s]*://||')
_ELEMENT_DOMAIN=$(echo "${_ELEMENT_HOST}" | cut -d: -f1)
_ELEMENT_PORT=$(echo "${_ELEMENT_HOST}" | cut -d: -f2)
_k8s_higress_api POST /v1/service-sources "Registering Element Web service source" \
'{"type":"dns","name":"element-web","domain":"'"${_ELEMENT_DOMAIN}"'","port":'"${_ELEMENT_PORT}"'}'
fi
# 2. Manager Consumer (key-auth)
_k8s_higress_api POST /v1/consumers "Creating Manager consumer" \
'{"name":"manager","credentials":[{"type":"key-auth","source":"BEARER","values":["'"${AGENTTEAMS_MANAGER_GATEWAY_KEY}"'"]}]}'
# 3. LLM Provider
_LLM_PROVIDER="${AGENTTEAMS_LLM_PROVIDER:-qwen}"
_LLM_API_URL="${AGENTTEAMS_LLM_API_URL:-}"
if [ -z "${_LLM_API_URL}" ]; then
case "${_LLM_PROVIDER}" in
qwen) _LLM_API_URL="https://dashscope.aliyuncs.com/compatible-mode/v1" ;;
esac
fi
# 4. LLM Provider type-specific config
case "${_LLM_PROVIDER}" in
qwen)
_k8s_higress_api POST /v1/ai/providers "Creating LLM provider (qwen)" \
'{"type":"qwen","name":"qwen","tokens":["'"${AGENTTEAMS_LLM_API_KEY}"'"],"protocol":"openai/v1","tokenFailoverConfig":{"enabled":false},"rawConfigs":{"qwenEnableSearch":false,"qwenEnableCompatible":true,"qwenFileIds":[],"agentteamsMode":true}}'
;;
*)
_BODY='{"name":"'"${_LLM_PROVIDER}"'","type":"openai","tokens":["'"${AGENTTEAMS_LLM_API_KEY}"'"],"modelMapping":{},"protocol":"openai/v1","rawConfigs":{"agentteamsMode":true}}'
_k8s_higress_api POST /v1/ai/providers "Creating LLM provider (${_LLM_PROVIDER})" "${_BODY}"
;;
esac
# 5. AI Route (bind provider + consumer auth, /v1 prefix to avoid clash with Element Web catch-all)
_k8s_higress_api POST /v1/ai/routes "Creating AI Gateway route" \
'{"name":"default-ai-route","domains":[],"pathPredicate":{"matchType":"PRE","matchValue":"/v1","caseSensitive":false},"upstreams":[{"provider":"'"${_LLM_PROVIDER}"'","weight":100,"modelMapping":{}}],"authConfig":{"enabled":true,"allowedCredentialTypes":["key-auth"],"allowedConsumers":["manager"]}}'
# 6. Matrix Homeserver Route (/_matrix/* → Tuwunel, no auth)
_k8s_higress_api POST /v1/routes "Creating Matrix Homeserver route" \
'{"name":"matrix-homeserver","domains":[],"path":{"matchType":"PRE","matchValue":"/_matrix"},"services":[{"name":"tuwunel.dns","port":'"${_TUWUNEL_PORT}"',"weight":100}]}'
# 7. Element Web Route (/ catch-all → Element Web, no auth)
if [ -n "${AGENTTEAMS_ELEMENT_WEB_URL:-}" ]; then
_k8s_higress_api POST /v1/routes "Creating Element Web route" \
'{"name":"element-web","domains":[],"path":{"matchType":"PRE","matchValue":"/"},"services":[{"name":"element-web.dns","port":'"${_ELEMENT_PORT}"',"weight":100}]}'
fi
# 8. Remove Higress default landing page (Exact match on / takes precedence over Element Web catch-all)
_k8s_higress_api DELETE /v1/routes/default "Removing Higress default landing route"
log "K8s Higress lightweight setup complete"
# Wait for AI plugin activation (~45s for first config)
log "Waiting for AI Gateway plugin activation (45s)..."
sleep 45
else
# Docker mode: full setup with all routes, domains, MCP servers
/opt/agentteams/scripts/init/setup-higress.sh
fi
fi
# ============================================================
# Create admin DM room, persist to state.json, send welcome message
# K8s mode: skip — controller ProvisionManager creates the Admin DM
# room (Step 4 in service/provisioner.go) AND reconcileManagerWelcome
# delivers the first-boot onboarding prompt once OpenClaw inside this
# container has joined the room. In k8s mode the manager intentionally
# does NOT have the admin password (only AGENTTEAMS_ADMIN_USER), so it
# could not log in as admin to send the welcome itself anyway. The
# Manager Agent discovers its admin DM room on first heartbeat via
# state.json / `manage-state.sh --action set-admin-dm` (see HEARTBEAT.md
# Step 1) — it does not need it to be pre-injected by this script.
# Runs in both local and cloud modes (idempotent)
# ============================================================
if [ "${AGENTTEAMS_RUNTIME}" = "k8s" ]; then
log "K8s mode: skipping admin DM room creation and welcome message (both handled by agentteams-controller)"
else
MANAGER_FULL_ID="@manager:${MATRIX_DOMAIN}"
ADMIN_FULL_ID="@${AGENTTEAMS_ADMIN_USER}:${MATRIX_DOMAIN}"
log "Logging in as admin to create DM room..."
_ADMIN_LOGIN=$(curl -sf -X POST "${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/login" \
-H 'Content-Type: application/json' \
-d '{
"type": "m.login.password",
"identifier": {"type": "m.id.user", "user": "'"${AGENTTEAMS_ADMIN_USER}"'"},
"password": "'"${AGENTTEAMS_ADMIN_PASSWORD}"'"
}' 2>&1) || true
ADMIN_MATRIX_TOKEN=$(echo "${_ADMIN_LOGIN}" | jq -r '.access_token // empty' 2>/dev/null)
if [ -z "${ADMIN_MATRIX_TOKEN}" ]; then
log "WARNING: Failed to login as admin, skipping DM room creation"
else
# Search for existing DM room with Manager (idempotent)
DM_ROOM_ID=""
_JOINED_ROOMS=$(curl -sf "${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/joined_rooms" \
-H "Authorization: Bearer ${ADMIN_MATRIX_TOKEN}" 2>/dev/null \
| jq -r '.joined_rooms[]' 2>/dev/null) || true
for _rid in ${_JOINED_ROOMS}; do
_members=$(curl -sf "${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${_rid}/members" \
-H "Authorization: Bearer ${ADMIN_MATRIX_TOKEN}" 2>/dev/null \
| jq -r '.chunk[].state_key' 2>/dev/null) || continue
_count=$(echo "${_members}" | wc -l | xargs)
if [ "${_count}" = "2" ] && echo "${_members}" | grep -q "@manager:"; then
DM_ROOM_ID="${_rid}"
break
fi
done
if [ -n "${DM_ROOM_ID}" ]; then
log "Existing DM room found: ${DM_ROOM_ID}"
else
log "Creating DM room with Manager..."
_RAW=$(curl -s -w '\nHTTP_CODE:%{http_code}' -X POST "${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/createRoom" \
-H "Authorization: Bearer ${ADMIN_MATRIX_TOKEN}" \
-H 'Content-Type: application/json' \
-d "{\"is_direct\":true,\"invite\":[\"${MANAGER_FULL_ID}\"],\"preset\":\"trusted_private_chat\"}" 2>&1) || true
_HTTP_CODE=$(echo "${_RAW}" | tail -1 | sed 's/HTTP_CODE://')
_CREATE_RESP=$(echo "${_RAW}" | sed '$d')
DM_ROOM_ID=$(echo "${_CREATE_RESP}" | jq -r '.room_id // empty' 2>/dev/null)
if [ -n "${DM_ROOM_ID}" ]; then
log "DM room created: ${DM_ROOM_ID}"
else
log "WARNING: Failed to create DM room (HTTP ${_HTTP_CODE}): ${_CREATE_RESP}"
fi
fi
# Persist admin DM room ID to state.json
if [ -n "${DM_ROOM_ID}" ]; then
STATE_SCRIPT="/opt/agentteams/agent/skills/task-management/scripts/manage-state.sh"
if [ -f "${STATE_SCRIPT}" ]; then
bash "${STATE_SCRIPT}" --action init 2>/dev/null || true
bash "${STATE_SCRIPT}" --action set-admin-dm --room-id "${DM_ROOM_ID}" 2>/dev/null || true
log "Admin DM room persisted to state.json: ${DM_ROOM_ID}"
fi
fi
# Schedule welcome message in background (only on first boot)
if [ -n "${DM_ROOM_ID}" ] && [ ! -f "/root/manager-workspace/soul-configured" ]; then
log "Scheduling welcome message (background, waiting for OpenClaw to start)..."
(
_AGENTTEAMS_LANGUAGE="${AGENTTEAMS_LANGUAGE:-zh}"
_AGENTTEAMS_TIMEZONE="${TZ:-Asia/Shanghai}"
_wait=0
_ready=false
while [ "${_wait}" -lt 300 ]; do
if curl -sf http://127.0.0.1:18799/ > /dev/null 2>&1; then
_ready=true
break
fi
sleep 3
_wait=$((_wait + 3))
done
if [ "${_ready}" != "true" ]; then
echo "[manager] WARNING: OpenClaw gateway not ready within 300s, skipping welcome message"
exit 0
fi
# Ensure Manager has joined the DM room before sending the welcome
# message. Without this, there is a race between OpenClaw's Matrix
# auto-join and the message send — the message may land before Manager
# joins, so OpenClaw's /sync never picks it up.
_join_ok=false
for _join_attempt in 1 2 3; do
if curl -sf -X POST "${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/join" \
-H "Authorization: Bearer ${MANAGER_TOKEN}" \
-H 'Content-Type: application/json' \
-d '{}' > /dev/null 2>&1; then
echo "[manager] Manager joined DM room before welcome message"
_join_ok=true
break
fi
sleep 2
done
if [ "${_join_ok}" != "true" ]; then
echo "[manager] WARNING: Manager join request failed after 3 attempts (may already be joined)"
fi
_welcome_msg="This is an automated message from the AgentTeams setup. This is a fresh installation.
--- Installation Context ---
User Language: ${_AGENTTEAMS_LANGUAGE} (zh = Chinese, en = English)
User Timezone: ${_AGENTTEAMS_TIMEZONE} (IANA timezone identifier)
---
You are an AI agent that manages a team of worker agents. Your identity and personality have not been configured yet — the human admin is about to meet you for the first time.
Please begin the onboarding conversation:
1. Greet the admin warmly and briefly describe what you can do (coordinate workers, manage tasks, run multi-agent projects)
2. The user has selected \"${_AGENTTEAMS_LANGUAGE}\" as their preferred language during installation. Use this language for your greeting and all subsequent communication.
3. The user's timezone is ${_AGENTTEAMS_TIMEZONE}. Based on this timezone, you may infer their likely region and suggest additional language options.
4. Ask them: a) What would they like to call you? b) Communication style preference? c) Any behavior guidelines? d) Confirm default language
5. After they reply, write their preferences to ~/SOUL.md
6. Confirm what you wrote, and ask if they would like to adjust anything
7. Once confirmed, run: touch ~/soul-configured
The human admin will start chatting shortly."
_txn_id="welcome-$(date +%s)"
_payload=$(jq -nc --arg body "${_welcome_msg}" '{"msgtype":"m.text","body":$body}')
_raw=$(curl -s -w '\nHTTP_CODE:%{http_code}' -X PUT "${AGENTTEAMS_MATRIX_URL}/_matrix/client/v3/rooms/${DM_ROOM_ID}/send/m.room.message/${_txn_id}" \
-H "Authorization: Bearer ${ADMIN_MATRIX_TOKEN}" \
-H 'Content-Type: application/json' \
-d "${_payload}" 2>&1) || true
_http_code=$(echo "${_raw}" | tail -1 | sed 's/HTTP_CODE://')
_send_resp=$(echo "${_raw}" | sed '$d')
if echo "${_send_resp}" | jq -e '.event_id' > /dev/null 2>&1; then
echo "[manager] Welcome message sent to DM room"
else
echo "[manager] WARNING: Failed to send welcome message (HTTP ${_http_code}): ${_send_resp}"
fi
) &
log "Welcome message background process started (PID: $!)"
fi
fi
fi # end K8s mode skip for admin DM room
# ============================================================
# Generate Manager Agent openclaw.json from template
# ============================================================
log "Generating Manager openclaw.json..."
export MANAGER_MATRIX_TOKEN="${MANAGER_TOKEN}"
export MANAGER_GATEWAY_KEY="${AGENTTEAMS_MANAGER_GATEWAY_KEY}"
# Resolve model parameters based on model name
MODEL_NAME="${AGENTTEAMS_DEFAULT_MODEL:-qwen3.6-plus}"
case "${MODEL_NAME}" in
gpt-5.3-codex|gpt-5-mini|gpt-5-nano)
export MODEL_CONTEXT_WINDOW=400000 MODEL_MAX_TOKENS=128000 ;;
claude-opus-4-6)
export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=128000 ;;
claude-sonnet-4-6)
export MODEL_CONTEXT_WINDOW=1000000 MODEL_MAX_TOKENS=64000 ;;
claude-haiku-4-5)
export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;
qwen3.6-plus|qwen3.5-plus)
export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=64000 ;;
deepseek-chat|deepseek-reasoner|kimi-k2.5)
export MODEL_CONTEXT_WINDOW=256000 MODEL_MAX_TOKENS=128000 ;;
glm-5|MiniMax-M2.7|MiniMax-M2.7-highspeed|MiniMax-M2.5)
export MODEL_CONTEXT_WINDOW=200000 MODEL_MAX_TOKENS=128000 ;;
*)
export MODEL_CONTEXT_WINDOW=150000 MODEL_MAX_TOKENS=128000 ;;
esac
export MODEL_REASONING=true
# Override with user-supplied custom model parameters from env (set during install)
[ -n "${AGENTTEAMS_MODEL_CONTEXT_WINDOW:-}" ] && export MODEL_CONTEXT_WINDOW="${AGENTTEAMS_MODEL_CONTEXT_WINDOW}"
[ -n "${AGENTTEAMS_MODEL_MAX_TOKENS:-}" ] && export MODEL_MAX_TOKENS="${AGENTTEAMS_MODEL_MAX_TOKENS}"
[ -n "${AGENTTEAMS_MODEL_REASONING:-}" ] && export MODEL_REASONING="${AGENTTEAMS_MODEL_REASONING}"
# E2EE: convert AGENTTEAMS_MATRIX_E2EE to JSON boolean for template substitution
if [ "${AGENTTEAMS_MATRIX_E2EE:-0}" = "1" ] || [ "${AGENTTEAMS_MATRIX_E2EE:-}" = "true" ]; then
export MATRIX_E2EE_ENABLED=true
else
export MATRIX_E2EE_ENABLED=false
fi
log "Matrix E2EE: ${MATRIX_E2EE_ENABLED}"
# Resolve input modalities: only vision-capable models get "image"
case "${MODEL_NAME}" in
gpt-5.4|gpt-5.3-codex|gpt-5-mini|gpt-5-nano|claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5|qwen3.6-plus|qwen3.5-plus|kimi-k2.5)
export MODEL_INPUT='["text", "image"]' ;;
*)
export MODEL_INPUT='["text"]' ;;
esac
# Override with user-supplied vision setting from env
if [ "${AGENTTEAMS_MODEL_VISION:-}" = "true" ]; then
export MODEL_INPUT='["text", "image"]'
elif [ "${AGENTTEAMS_MODEL_VISION:-}" = "false" ]; then
export MODEL_INPUT='["text"]'
fi
log "Model: ${MODEL_NAME} (context=${MODEL_CONTEXT_WINDOW}, maxTokens=${MODEL_MAX_TOKENS}, reasoning=${MODEL_REASONING}, input=${MODEL_INPUT})"
if [ -f /root/manager-workspace/openclaw.json ]; then
log "Manager openclaw.json already exists, updating dynamic fields only (preserving user customizations)..."
# Merge known models into existing config (add missing, preserve user-added)
# Use known-models.json (valid JSON) instead of template (contains ${VAR} placeholders)
KNOWN_MODELS=$(cat /opt/agentteams/configs/known-models.json 2>/dev/null || echo '[]')
jq --arg token "${MANAGER_TOKEN}" \
--arg key "${AGENTTEAMS_MANAGER_GATEWAY_KEY}" \
--arg model "${MODEL_NAME}" \
--arg emb_model "${AGENTTEAMS_EMBEDDING_MODEL}" \
--arg aigw_domain "${AI_GATEWAY_DOMAIN}" \
--arg matrix_user_id "@manager:${MATRIX_DOMAIN}" \
--argjson e2ee "${MATRIX_E2EE_ENABLED}" \
--argjson known_models "${KNOWN_MODELS}" \
--argjson ctx "${MODEL_CONTEXT_WINDOW}" \
--argjson max "${MODEL_MAX_TOKENS}" \
--argjson reasoning "${MODEL_REASONING}" \
--argjson input "${MODEL_INPUT}" \
'
# Merge known models: add any model id not already present
.models.providers["agentteams-gateway"].models as $existing
| ($existing | map(.id)) as $existing_ids
| ($known_models | map(select(.id as $id | $existing_ids | index($id) | not))) as $new
| .models.providers["agentteams-gateway"].models = ($existing + $new)
# Ensure the user-chosen default model is in the list (custom model support)
| if (.models.providers["agentteams-gateway"].models | map(.id) | index($model) | not) then
.models.providers["agentteams-gateway"].models += [{"id": $model, "name": $model, "reasoning": $reasoning, "contextWindow": $ctx, "maxTokens": $max, "input": $input}]
else . end
# Rebuild model aliases from the full models list
| (.models.providers["agentteams-gateway"].models | map({ ("agentteams-gateway/" + .id): { "alias": .id } }) | add // {}) as $aliases
| .agents.defaults.models = ((.agents.defaults.models // {}) + $aliases)
| .channels.matrix.accessToken = $token | .channels.matrix.userId = $matrix_user_id | .models.providers["agentteams-gateway"].apiKey = $key
| ((.hooks.token // "") as $ht | if $ht == $key or $ht == ($key + "-hooks" | @base64) then del(.hooks) else . end)
| .agents.defaults.model.primary = ("agentteams-gateway/" + $model)
| .commands.restart = true
| .gateway.port = 18799
| .gateway.bind = "lan"
| .gateway.controlUi = ((.gateway.controlUi // {}) + {"dangerouslyDisableDeviceAuth": true, "allowInsecureAuth": true, "allowedOrigins": ["*"]})
| .channels.matrix.encryption = $e2ee
| .channels.matrix.network = ((.channels.matrix.network // {}) + {"dangerouslyAllowPrivateNetwork": true})
| .channels.matrix.autoJoin = "always"
# OpenClaw YOLO defaults: host exec without approval prompts (see openclaw docs tools/exec-approvals)
| .tools = (.tools // {})
| .tools.exec = ((.tools.exec // {}) + {"host":"gateway","security":"full","ask":"off"})
| .tools.elevated = (.tools.elevated // {})
| .tools.elevated.enabled = true
| .tools.elevated.allowFrom |= ((. // {}) | .matrix = ["*"])
| .agents.defaults.elevatedDefault = "full"
# Ensure memorySearch config exists (embedding model for memory) — skip if embedding model is empty
| if $emb_model != "" then .agents.defaults.memorySearch //= {"provider":"openai","model":$emb_model,"remote":{"baseUrl":("http://" + $aigw_domain + ":8080/v1"),"apiKey":$key}} else . end
' \
/root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \
mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json
# Disable openclaw's observe-recovery mechanism which compares config against
# a lastKnownGood baseline in config-health.json. When meta is missing from the
# current file but present in the baseline, observe-recovery restores from .bak,
# undoing user customizations (plugins, channels, etc).
# Clearing config-health.json removes the baseline so observe-recovery won't
# interfere, while preserving .bak as a backup.
rm -f /root/manager-workspace/.openclaw/logs/config-health.json
# Verify the token was written correctly
_written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)
if [ -z "${_written_token}" ] || [ "${_written_token}" = "null" ]; then
log "ERROR: Matrix token was not written correctly to openclaw.json (got: ${_written_token})"
else
log "Matrix token written to openclaw.json (prefix: ${_written_token:0:10}...)"
fi
else
log "Manager openclaw.json not found, generating from template..."
envsubst < /opt/agentteams/configs/manager-openclaw.json.tmpl > /root/manager-workspace/openclaw.json
# Post-envsubst injection: memorySearch + custom model (single jq pass when possible)
if ! jq -e --arg model "${MODEL_NAME}" '.models.providers["agentteams-gateway"].models | map(.id) | index($model)' /root/manager-workspace/openclaw.json > /dev/null 2>&1; then
log "Custom model '${MODEL_NAME}' not in built-in list, injecting into config..."
jq --arg emb_model "${AGENTTEAMS_EMBEDDING_MODEL}" \
--arg aigw_domain "${AI_GATEWAY_DOMAIN}" \
--arg key "${AGENTTEAMS_MANAGER_GATEWAY_KEY}" \
--arg model "${MODEL_NAME}" \
--argjson ctx "${MODEL_CONTEXT_WINDOW}" \
--argjson max "${MODEL_MAX_TOKENS}" \
--argjson reasoning "${MODEL_REASONING}" \
--argjson input "${MODEL_INPUT}" \
'
(if $emb_model != "" then .agents.defaults.memorySearch = {"provider":"openai","model":$emb_model,"remote":{"baseUrl":("http://" + $aigw_domain + ":8080/v1"),"apiKey":$key}} else . end)
| .models.providers["agentteams-gateway"].models += [{"id": $model, "name": $model, "reasoning": $reasoning, "contextWindow": $ctx, "maxTokens": $max, "input": $input}]
| .agents.defaults.models += {("agentteams-gateway/" + $model): {"alias": $model}}
' /root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \
mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json
elif [ -n "${AGENTTEAMS_EMBEDDING_MODEL}" ]; then
jq --arg emb_model "${AGENTTEAMS_EMBEDDING_MODEL}" \
--arg aigw_domain "${AI_GATEWAY_DOMAIN}" \
--arg key "${AGENTTEAMS_MANAGER_GATEWAY_KEY}" \
'.agents.defaults.memorySearch = {"provider":"openai","model":$emb_model,"remote":{"baseUrl":("http://" + $aigw_domain + ":8080/v1"),"apiKey":$key}}' \
/root/manager-workspace/openclaw.json > /tmp/openclaw.json.tmp && \
mv /tmp/openclaw.json.tmp /root/manager-workspace/openclaw.json
fi
_written_token=$(jq -r '.channels.matrix.accessToken' /root/manager-workspace/openclaw.json 2>/dev/null)
log "Matrix token written from template (prefix: ${_written_token:0:10}...)"
fi
# Cloud/K8s mode: overlay cloud-specific settings onto generated config
if [ "${AGENTTEAMS_RUNTIME}" = "aliyun" ] || [ "${AGENTTEAMS_RUNTIME}" = "k8s" ]; then
log "Applying cloud/k8s overlay to openclaw.json..."
jq --arg homeserver "${AGENTTEAMS_MATRIX_URL}" \
--arg gateway "${AGENTTEAMS_AI_GATEWAY_URL}/v1" \
--arg key "${AGENTTEAMS_MANAGER_GATEWAY_KEY}" \
'.channels.matrix.homeserver = $homeserver
| .models.providers["agentteams-gateway"].baseUrl = $gateway
| .models.providers["agentteams-gateway"].apiKey = $key
| ((.hooks.token // "") as $ht | if $ht == $key or $ht == ($key + "-hooks" | @base64) then del(.hooks) else . end)
| .commands.restart = false
| if .agents.defaults.memorySearch then .agents.defaults.memorySearch.remote.baseUrl = $gateway | .agents.defaults.memorySearch.remote.apiKey = $key else . end' \
/root/manager-workspace/openclaw.json > /tmp/openclaw-cloud.json && \
mv /tmp/openclaw-cloud.json /root/manager-workspace/openclaw.json
log "Cloud/K8s overlay applied"
fi
# ============================================================
# Optional: enable openclaw-cms-plugin observability
# Config is applied at runtime so secrets stay out of image layers.
# ============================================================
CMS_TRACES_ENABLED="$(echo "${AGENTTEAMS_CMS_TRACES_ENABLED:-false}" | tr '[:upper:]' '[:lower:]')"
if [ "${CMS_TRACES_ENABLED}" = "true" ]; then
CMS_PLUGIN_NAME="openclaw-cms-plugin"
CMS_PLUGIN_DIR="${OPENCLAW_CMS_PLUGIN_DIR:-/opt/openclaw/extensions/openclaw-cms-plugin}"
CMS_PLUGIN_MANIFEST="${CMS_PLUGIN_DIR}/openclaw.plugin.json"
DIAG_PLUGIN_NAME="diagnostics-otel"
DIAG_PLUGIN_DIR="/opt/openclaw/extensions/diagnostics-otel"
CMS_LICENSE_KEY="${AGENTTEAMS_CMS_LICENSE_KEY:-}"
CMS_PROJECT="${AGENTTEAMS_CMS_PROJECT:-}"
CMS_METRICS_ENABLED="${AGENTTEAMS_CMS_METRICS_ENABLED:-false}"
if [ ! -f "${CMS_PLUGIN_MANIFEST}" ]; then
log "WARNING: ${CMS_PLUGIN_NAME} manifest not found at ${CMS_PLUGIN_MANIFEST}, skipping plugin config"
else
_missing=0
[ -z "${AGENTTEAMS_CMS_ENDPOINT:-}" ] && log "WARNING: AGENTTEAMS_CMS_ENDPOINT is required when AGENTTEAMS_CMS_TRACES_ENABLED=true" && _missing=1
[ -z "${CMS_LICENSE_KEY:-}" ] && log "WARNING: AGENTTEAMS_CMS_LICENSE_KEY is required when AGENTTEAMS_CMS_TRACES_ENABLED=true" && _missing=1
[ -z "${AGENTTEAMS_CMS_WORKSPACE:-}" ] && log "WARNING: AGENTTEAMS_CMS_WORKSPACE is required when AGENTTEAMS_CMS_TRACES_ENABLED=true" && _missing=1
if [ "${_missing}" = "0" ]; then
CMS_SERVICE_NAME="${AGENTTEAMS_CMS_SERVICE_NAME:-agentteams-manager}"
CMS_ENABLE_METRICS="${CMS_METRICS_ENABLED}"
DIAG_AVAILABLE="0"
_metrics_lc="$(echo "${CMS_ENABLE_METRICS}" | tr '[:upper:]' '[:lower:]')"
if [ "${_metrics_lc}" = "true" ]; then
if [ -f "${DIAG_PLUGIN_DIR}/package.json" ]; then
DIAG_AVAILABLE="1"
if [ ! -d "${DIAG_PLUGIN_DIR}/node_modules" ]; then
log "diagnostics-otel dependencies missing, installing..."
if (cd "${DIAG_PLUGIN_DIR}" && npm install --omit=dev --ignore-scripts >/tmp/agentteams-diag-install.log 2>&1); then
log "diagnostics-otel dependencies installed"
else
log "WARNING: diagnostics-otel npm install failed, metrics plugin may not load"
fi
else
log "diagnostics-otel dependencies already present"
fi
else
log "WARNING: diagnostics-otel package.json not found at ${DIAG_PLUGIN_DIR}, metrics plugin may not load"
fi
fi
log "Applying ${CMS_PLUGIN_NAME} config to openclaw.json..."
jq --arg pluginName "${CMS_PLUGIN_NAME}" \
--arg pluginDir "${CMS_PLUGIN_DIR}" \
--arg endpoint "${AGENTTEAMS_CMS_ENDPOINT}" \
--arg licenseKey "${CMS_LICENSE_KEY}" \
--arg armsProject "${CMS_PROJECT}" \
--arg cmsWorkspace "${AGENTTEAMS_CMS_WORKSPACE}" \
--arg serviceName "${CMS_SERVICE_NAME}" \
--arg diagPluginName "${DIAG_PLUGIN_NAME}" \
--arg diagPluginDir "${DIAG_PLUGIN_DIR}" \
--arg metricsRaw "${CMS_ENABLE_METRICS}" \
--arg diagAvailableRaw "${DIAG_AVAILABLE}" \
'
.plugins = (.plugins // {})
| .plugins.load = (.plugins.load // {})
| .plugins.entries = (.plugins.entries // {})
| if (.plugins.allow | type) != "array" then .plugins.allow = [] else . end
| if (.plugins.allow | index($pluginName)) == null then .plugins.allow += [$pluginName] else . end
| if (.plugins.load.paths | type) != "array" then .plugins.load.paths = [] else . end
| if (.plugins.load.paths | index($pluginDir)) == null then .plugins.load.paths += [$pluginDir] else . end
| .plugins.entries[$pluginName] = {
"enabled": true,
"config": {
"endpoint": $endpoint,
"headers": {
"x-arms-license-key": $licenseKey,
"x-arms-project": $armsProject,
"x-cms-workspace": $cmsWorkspace
},
"serviceName": $serviceName
}
}
# diagnostics-otel metrics (optional)
| ($metricsRaw | ascii_downcase) as $m
| ($diagAvailableRaw == "1") as $diagAvailable
| (($m == "true") and $diagAvailable) as $metricsEnabled
| if $metricsEnabled then
(if (.plugins.allow | index($diagPluginName)) == null then .plugins.allow += [$diagPluginName] else . end)
| (if (.plugins.load.paths | index($diagPluginDir)) == null then .plugins.load.paths += [$diagPluginDir] else . end)
| .plugins.entries[$diagPluginName].enabled = true
| .diagnostics = (.diagnostics // {})
| .diagnostics.otel = (.diagnostics.otel // {})
| .diagnostics.enabled = true
| .diagnostics.otel.enabled = true
| .diagnostics.otel.endpoint = $endpoint
| .diagnostics.otel.protocol = (.diagnostics.otel.protocol // "http/protobuf")
| .diagnostics.otel.headers = {
"x-arms-license-key": $licenseKey,
"x-arms-project": $armsProject,
"x-cms-workspace": $cmsWorkspace
}
| .diagnostics.otel.serviceName = $serviceName
| .diagnostics.otel.metrics = true
| .diagnostics.otel.traces = (.diagnostics.otel.traces // false)
| .diagnostics.otel.logs = (.diagnostics.otel.logs // false)
else
.
end
' /root/manager-workspace/openclaw.json > /tmp/openclaw-cms.json && \
mv /tmp/openclaw-cms.json /root/manager-workspace/openclaw.json
log "${CMS_PLUGIN_NAME} config applied (metrics=${CMS_ENABLE_METRICS}, service=${CMS_SERVICE_NAME})"
else
log "Skipping ${CMS_PLUGIN_NAME} config due to missing required env vars"
fi
fi
fi
# ============================================================
# Detect container runtime (for Worker creation)
# ============================================================
source /opt/agentteams/scripts/lib/container-api.sh
if container_api_available; then
log "Container runtime socket detected at ${CONTAINER_SOCKET} — direct Worker creation enabled"
export AGENTTEAMS_CONTAINER_RUNTIME="socket"
elif [ "${AGENTTEAMS_RUNTIME}" = "aliyun" ] || [ "${AGENTTEAMS_RUNTIME}" = "k8s" ]; then
log "Cloud/K8s mode — Workers created via controller API"
export AGENTTEAMS_CONTAINER_RUNTIME="cloud"
else
log "No container runtime found — Worker creation will output install commands"
export AGENTTEAMS_CONTAINER_RUNTIME="none"
fi
# ============================================================
# Recreate Worker containers as needed after Manager restart.
# Workers are on agentteams-net; Docker DNS resolves *-local.agentteams.io via
# the Manager's network aliases, so IP changes don't require worker recreation.
# Only recreate stopped/missing workers.
# ============================================================
if container_api_available; then
_workers_json=$(agt get workers -o json 2>/dev/null || echo '{"workers":[]}')
for _worker_name in $(echo "${_workers_json}" | jq -r '.workers[].name'); do
[ -z "${_worker_name}" ] && continue
_status=$(container_status_worker "${_worker_name}")
if [ "${_status}" = "running" ]; then
log "Worker running: ${_worker_name}, skipping"
continue
fi
# Container missing or stopped — recreate.
log "Worker container ${_status}: ${_worker_name}, recreating..."
_creds_file="/data/worker-creds/${_worker_name}.env"
if [ -f "${_creds_file}" ]; then
source "${_creds_file}"
_runtime=$(echo "${_workers_json}" | jq -r --arg w "${_worker_name}" '.workers[] | select(.name == $w) | .runtime // "openclaw"')
_recreated=false
for _attempt in 1 2 3; do
_env_map=""
_create_body=""
_env_map=$(jq -cn \
--arg name "${_worker_name}" \
--arg fak "${_worker_name}" \
--arg fsk "${WORKER_MINIO_PASSWORD:-}" \
--arg fs_domain "${AGENTTEAMS_FS_DOMAIN:-fs-local.agentteams.io}" \
--arg controller_url "${AGENTTEAMS_CONTROLLER_URL:-}" \
'{
"AGENTTEAMS_WORKER_NAME": $name,
"AGENTTEAMS_FS_ENDPOINT": ("http://" + ($fs_domain | split(":")[0]) + ":9000"),
"AGENTTEAMS_FS_ACCESS_KEY": $fak,
"AGENTTEAMS_FS_SECRET_KEY": $fsk
}
| if $controller_url != "" then . + {"AGENTTEAMS_CONTROLLER_URL": $controller_url} else . end')
_create_body=$(jq -cn --arg name "${_worker_name}" --arg runtime "${_runtime}" --argjson env "${_env_map}" '{name: $name, runtime: $runtime, env: $env}')
worker_backend_create "${_create_body}" > /dev/null 2>&1 && _recreated=true && break
log " Attempt ${_attempt}/3 failed for ${_worker_name}, retrying in $((5 * _attempt))s..."
sleep $((5 * _attempt))
done
if [ "${_recreated}" = true ]; then
log " Recreated ${_runtime} worker: ${_worker_name}"
else
log " ERROR: Failed to recreate ${_worker_name} after 3 attempts"
fi
else
log " WARNING: No credentials found for ${_worker_name} (${_creds_file} missing), skipping"
fi
done
fi
# ============================================================
# Notify workers of builtin updates if upgrade happened
# Builtin files (AGENTS.md, skills) are already synced by upgrade-builtins.sh
#
# Cooldown: skip notification if the last successful notify was within
# NOTIFY_COOLDOWN_SECS (default 3600s / 1 hour). This prevents repeated
# notifications when the Manager crash-loops and re-runs upgrade-builtins
# on every restart (e.g. IMAGE_VERSION=latest always triggers upgrade).
# ============================================================
NOTIFY_COOLDOWN_SECS="${AGENTTEAMS_NOTIFY_COOLDOWN_SECS:-3600}"
NOTIFY_TS_FILE="/root/manager-workspace/.last-worker-notify-ts"
if [ -f /root/manager-workspace/.upgrade-pending-worker-notify ]; then
_now=$(date +%s)
_last_notify=$(cat "${NOTIFY_TS_FILE}" 2>/dev/null || echo "0")