Compare commits
513 Commits
ci/cachix-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8f8e92aba | ||
|
|
0b25e889a5 | ||
|
|
26c58273e7 | ||
|
|
11d69d8a1c | ||
|
|
30002b7ded | ||
|
|
faaf1021c3 | ||
|
|
e7d60654b8 | ||
|
|
63ff54b656 | ||
|
|
a9494d1b8b | ||
|
|
44ae6995de | ||
|
|
a2ea92cce2 | ||
|
|
4ed579792e | ||
|
|
73e9320cac | ||
|
|
794cf476d9 | ||
|
|
2d0a67d190 | ||
|
|
ab4bbf5dba | ||
|
|
cbe10a8eb9 | ||
|
|
68b04bd0b3 | ||
|
|
694349643f | ||
|
|
54e09bce18 | ||
|
|
fd30aad492 | ||
|
|
7cbc79ce52 | ||
|
|
0dae522382 | ||
|
|
87f883a6c8 | ||
|
|
cb9ec56612 | ||
|
|
6a5352eba7 | ||
|
|
e4aad2be37 | ||
|
|
b46482a4df | ||
|
|
3a0c35bf68 | ||
|
|
7f55c0bd7b | ||
|
|
32f0772a59 | ||
|
|
505c50fa58 | ||
|
|
d56fa8a75c | ||
|
|
0a70262dda | ||
|
|
5e186c192f | ||
|
|
d213d242e7 | ||
|
|
879885aecd | ||
|
|
51c69b6181 | ||
|
|
0995d25074 | ||
|
|
ce0da31f81 | ||
|
|
e93384082a | ||
|
|
3abd2d14cb | ||
|
|
4a918c46ee | ||
|
|
8264853833 | ||
|
|
8c6267b2c3 | ||
|
|
eb9a52bf96 | ||
|
|
33f8f87571 | ||
|
|
c6a4cfc9d7 | ||
|
|
2149201a95 | ||
|
|
7471da32e5 | ||
|
|
bf7764385a | ||
|
|
e739f5888f | ||
|
|
8b24b5d515 | ||
|
|
216f835d24 | ||
|
|
3333bb831e | ||
|
|
e16f9743fd | ||
|
|
ecfb1dc936 | ||
|
|
ba7d1573a4 | ||
|
|
94fa2e2ef3 | ||
|
|
9c2b207501 | ||
|
|
c8782f7631 | ||
|
|
d9b42b0f77 | ||
|
|
123f0a7cc4 | ||
|
|
a468354c55 | ||
|
|
53aac0dce0 | ||
|
|
ed86924dd3 | ||
|
|
34e5f011b9 | ||
|
|
b023ed119f | ||
|
|
13deaaf73f | ||
|
|
a003810ddd | ||
|
|
c2e8301f51 | ||
|
|
4e93f4ac56 | ||
|
|
0d60673f02 | ||
|
|
659c9f5973 | ||
|
|
aa27809dc5 | ||
|
|
88a4647384 | ||
|
|
e97594cd50 | ||
|
|
64d4106668 | ||
|
|
b915c422f4 | ||
|
|
990b572c3c | ||
|
|
632bb133f6 | ||
|
|
d562894125 | ||
|
|
2605856f77 | ||
|
|
fd33ccd2bf | ||
|
|
f072f3714a | ||
|
|
6481ec521d | ||
|
|
8c1fd05c7a | ||
|
|
feb5895dcc | ||
|
|
c2aaf58d1d | ||
|
|
082d03efcc | ||
|
|
cf1486ed48 | ||
|
|
734d2ac859 | ||
|
|
f569001ded | ||
|
|
57df329849 | ||
|
|
874649d990 | ||
|
|
b918448180 | ||
|
|
062945f265 | ||
|
|
314c9d8e23 | ||
|
|
439b5a67fd | ||
|
|
730adebd1e | ||
|
|
87e7260d8e | ||
|
|
4c107be1f6 | ||
|
|
d2d16be2af | ||
|
|
4b1b9fb299 | ||
|
|
83fc5534a9 | ||
|
|
f17d8ba5b8 | ||
|
|
9f1084f76f | ||
|
|
8cdfd30e0c | ||
|
|
961ac0f06c | ||
|
|
eb76f589ff | ||
|
|
dc8ec6f766 | ||
|
|
e1dc63afd9 | ||
|
|
23978beac1 | ||
|
|
fd6a7306da | ||
|
|
373f89ba57 | ||
|
|
4136401fbf | ||
|
|
e5c180f176 | ||
|
|
c26b45793a | ||
|
|
681d984288 | ||
|
|
78c88f009c | ||
|
|
3446aa3e22 | ||
|
|
caf5755de9 | ||
|
|
3d784abe7f | ||
|
|
2d2dc144ad | ||
|
|
c984b3e393 | ||
|
|
488260e0a8 | ||
|
|
7850eac853 | ||
|
|
b16bde62d3 | ||
|
|
d6de9e5e67 | ||
|
|
4c40ee0ac1 | ||
|
|
846246e91e | ||
|
|
865c970afa | ||
|
|
99b5e15e5c | ||
|
|
69e2d42fe0 | ||
|
|
f40ededb24 | ||
|
|
606418c4d5 | ||
|
|
4ef7776eb3 | ||
|
|
f1efd50ca7 | ||
|
|
19724e370d | ||
|
|
7b621d7f29 | ||
|
|
042aa82642 | ||
|
|
9f98b82943 | ||
|
|
36a8842472 | ||
|
|
c7c1e5a9e7 | ||
|
|
e0120c3709 | ||
|
|
8aa60d5a38 | ||
|
|
df77061a7f | ||
|
|
376aa32db0 | ||
|
|
8ad43deaf8 | ||
|
|
a2bfe2a626 | ||
|
|
b49874ed1e | ||
|
|
bd37c77e0c | ||
|
|
e15b04e90f | ||
|
|
aa3390b570 | ||
|
|
4d218944d1 | ||
|
|
2e83020fd3 | ||
|
|
81d703a00a | ||
|
|
41835c3b42 | ||
|
|
f94eda6be7 | ||
|
|
36efa52624 | ||
|
|
c52847004e | ||
|
|
ff30440860 | ||
|
|
7f6e664dab | ||
|
|
46d9d14d65 | ||
|
|
269c5163df | ||
|
|
fd492c6933 | ||
|
|
50decbe2b4 | ||
|
|
725bc6e0cf | ||
|
|
27872342d5 | ||
|
|
e0d7ad6426 | ||
|
|
c52fc90c54 | ||
|
|
b862d654b1 | ||
|
|
3ebbc65c55 | ||
|
|
27a29df0a6 | ||
|
|
8fe456207b | ||
|
|
1b4bf09f8c | ||
|
|
f1704422d3 | ||
|
|
764b4f827d | ||
|
|
57253324d3 | ||
|
|
7dc847afc0 | ||
|
|
e65690c856 | ||
|
|
ae705b8880 | ||
|
|
a3b71dfacf | ||
|
|
8eeb832796 | ||
|
|
e701ee7e5f | ||
|
|
d35904e4d8 | ||
|
|
2dc793a274 | ||
|
|
93e9cfdc9a | ||
|
|
4ec03e75d5 | ||
|
|
cec4a53fc4 | ||
|
|
b48016df09 | ||
|
|
d27c6a1b3a | ||
|
|
60f4d78459 | ||
|
|
ad6400395b | ||
|
|
10e6a0715d | ||
|
|
71d409fe32 | ||
|
|
81b7e6811b | ||
|
|
9bc3b14186 | ||
|
|
97b539d4f4 | ||
|
|
dfafd6cc5b | ||
|
|
a29d1420c2 | ||
|
|
e56d012657 | ||
|
|
133c9060ff | ||
|
|
08338f0f69 | ||
|
|
7be64cd9c9 | ||
|
|
ebee5d2352 | ||
|
|
48e091da9e | ||
|
|
2e4b0529e3 | ||
|
|
34d4647871 | ||
|
|
75c9417ab8 | ||
|
|
0547b7c517 | ||
|
|
e12950b0f3 | ||
|
|
ad519160df | ||
|
|
43bd8eef38 | ||
|
|
b39fc7f507 | ||
|
|
6d6f93c179 | ||
|
|
7251d3f74a | ||
|
|
0d3ded9603 | ||
|
|
4dc61b2cb9 | ||
|
|
d1f651b30f | ||
|
|
22a66d212b | ||
|
|
2b813ad781 | ||
|
|
26d88d3d1c | ||
|
|
d68a479958 | ||
|
|
ab67e92005 | ||
|
|
4b92cd6103 | ||
|
|
cdde387df9 | ||
|
|
20e73c6312 | ||
|
|
05dde40234 | ||
|
|
9c9f76f958 | ||
|
|
8c42ac7102 | ||
|
|
ae8199dc7b | ||
|
|
de1e28f877 | ||
|
|
a4cdb6afd6 | ||
|
|
9249523f63 | ||
|
|
ec5ff4a24b | ||
|
|
d1d843badb | ||
|
|
e0d47bbb75 | ||
|
|
c0429e372a | ||
|
|
8395ff3097 | ||
|
|
6f172e3f9f | ||
|
|
045c07a14c | ||
|
|
885e0de37a | ||
|
|
0cab5f9ab5 | ||
|
|
a9468871d5 | ||
|
|
800cb6c690 | ||
|
|
4694b4e54b | ||
|
|
afc2a81019 | ||
|
|
7240815dab | ||
|
|
03bb947aff | ||
|
|
69916b2ebc | ||
|
|
f704240b5f | ||
|
|
aee4e0558d | ||
|
|
8141468385 | ||
|
|
baf7895238 | ||
|
|
2ad3fae24b | ||
|
|
f5d79f4905 | ||
|
|
e8a3ca7231 | ||
|
|
73d3a07f64 | ||
|
|
5ed5853798 | ||
|
|
b5a994be61 | ||
|
|
2b65c57f04 | ||
|
|
92c89ebe88 | ||
|
|
bd00261cbc | ||
|
|
b7e1fd7df4 | ||
|
|
c98d5f0925 | ||
|
|
cc3dd647e4 | ||
|
|
df7f1919c7 | ||
|
|
89b9b6895b | ||
|
|
58c4cae97c | ||
|
|
8946ea7007 | ||
|
|
1f8f49b275 | ||
|
|
4902f1a66f | ||
|
|
a4b2dba828 | ||
|
|
faa5c60c7f | ||
|
|
9f2e652177 | ||
|
|
d0438c203e | ||
|
|
a8bed29f98 | ||
|
|
97001f8789 | ||
|
|
a80fbbe1ac | ||
|
|
9d3e92376c | ||
|
|
b95a2c2bbf | ||
|
|
8b3b01f720 | ||
|
|
cb061e5346 | ||
|
|
10e4d73318 | ||
|
|
febe56e66b | ||
|
|
948085aee3 | ||
|
|
efb416770d | ||
|
|
e54b4a72eb | ||
|
|
17055a419a | ||
|
|
584ad1556a | ||
|
|
203ee30747 | ||
|
|
d6b17b3462 | ||
|
|
77f95b2d31 | ||
|
|
be03e9c1f1 | ||
|
|
dafc8ce959 | ||
|
|
6844be5659 | ||
|
|
91388017b5 | ||
|
|
2d5b49dffe | ||
|
|
146be916d6 | ||
|
|
c87ec7a5b7 | ||
|
|
c005cf946b | ||
|
|
76068fdf8e | ||
|
|
76c0804664 | ||
|
|
8acd74a46b | ||
|
|
85888390e6 | ||
|
|
c006693ce5 | ||
|
|
8c8bae02d1 | ||
|
|
8e25eef3f1 | ||
|
|
bf98cf6573 | ||
|
|
31efb28909 | ||
|
|
e1011d51af | ||
|
|
8efac106b7 | ||
|
|
b227ebbaf6 | ||
|
|
e9225f59cb | ||
|
|
3460fa878a | ||
|
|
c0e79a5d05 | ||
|
|
c07498701c | ||
|
|
5f038b79cb | ||
|
|
12fbd5361e | ||
|
|
1d142fc533 | ||
|
|
8571d76667 | ||
|
|
11c2839883 | ||
|
|
a091baebe8 | ||
|
|
9c1b2d4ad1 | ||
|
|
f2bbd0d960 | ||
|
|
10b2ed77d6 | ||
|
|
0d8e9e7d72 | ||
|
|
943b983c1f | ||
|
|
90b7a77c87 | ||
|
|
c884a16f00 | ||
|
|
7440567bd1 | ||
|
|
3027087818 | ||
|
|
527d46df34 | ||
|
|
d3a67150f7 | ||
|
|
abd7c5ff0e | ||
|
|
e807eb4b77 | ||
|
|
ce85b55569 | ||
|
|
eff22020f0 | ||
|
|
594f88ccfb | ||
|
|
4ba6319ebc | ||
|
|
4d9eb961cd | ||
|
|
ce56cc5881 | ||
|
|
c746965646 | ||
|
|
aa661f72a6 | ||
|
|
c2623871c8 | ||
|
|
9f564ea4b7 | ||
|
|
626e600679 | ||
|
|
80373ef96a | ||
|
|
2b1e71bd01 | ||
|
|
397cec55c1 | ||
|
|
74cdab9527 | ||
|
|
260393e73a | ||
|
|
16f1167066 | ||
|
|
00c4d73a26 | ||
|
|
69d42f0167 | ||
|
|
9d3474c8e0 | ||
|
|
f00513c30e | ||
|
|
a6cc274260 | ||
|
|
7283f62107 | ||
|
|
382fd80a66 | ||
|
|
e7756a7cd8 | ||
|
|
2e7328edcb | ||
|
|
55310808f3 | ||
|
|
edd7380c68 | ||
|
|
8ac6079fb7 | ||
|
|
5cfa93a076 | ||
|
|
d6c5bdc12e | ||
|
|
565507643b | ||
|
|
6e9bc40eec | ||
|
|
6e34f3f3ba | ||
|
|
018e1e8ace | ||
|
|
531151e34d | ||
|
|
f95429513f | ||
|
|
0dd419c2a9 | ||
|
|
16d12d14b6 | ||
|
|
5e5d28a695 | ||
|
|
f37a4e184d | ||
|
|
e9a99f60dc | ||
|
|
ceffcf45ee | ||
|
|
84a606a1c8 | ||
|
|
283cfaeaca | ||
|
|
9127e02698 | ||
|
|
addf0a03c4 | ||
|
|
ef468a7de2 | ||
|
|
b967152466 | ||
|
|
7b7dd0e001 | ||
|
|
849e8d4bf2 | ||
|
|
3e7b845582 | ||
|
|
92d380cb75 | ||
|
|
8b4f86bf36 | ||
|
|
475e6986a7 | ||
|
|
554e09563d | ||
|
|
7b3b7590dc | ||
|
|
15a35bc58d | ||
|
|
b1be22b2d0 | ||
|
|
fbef208719 | ||
|
|
494dcc360c | ||
|
|
8ff0e2582e | ||
|
|
820d0709fa | ||
|
|
ba0b84b482 | ||
|
|
d963ce954c | ||
|
|
79071558e2 | ||
|
|
eabd8deab9 | ||
|
|
dccd3cebf0 | ||
|
|
8ac857d730 | ||
|
|
b6dcb57f95 | ||
|
|
0e94d92dbd | ||
|
|
a01fe9acff | ||
|
|
ce4387e90a | ||
|
|
4680ec6cd1 | ||
|
|
0f2e923094 | ||
|
|
71fd6f4534 | ||
|
|
d685b699f3 | ||
|
|
41824bcf9d | ||
|
|
cd109e2b54 | ||
|
|
9165d0228d | ||
|
|
91bf78a135 | ||
|
|
c6cc749128 | ||
|
|
de2fef944e | ||
|
|
837ae2d0d7 | ||
|
|
fe4bd06b3a | ||
|
|
b682bf6c07 | ||
|
|
31ded5c1b3 | ||
|
|
e55458cde4 | ||
|
|
61c93964cd | ||
|
|
444013a9b3 | ||
|
|
ac653c1396 | ||
|
|
0807d61c10 | ||
|
|
fd20aa20df | ||
|
|
2dfbeee333 | ||
|
|
3fd87e299b | ||
|
|
d2f8af5880 | ||
|
|
eacc1a26a4 | ||
|
|
49958c1902 | ||
|
|
6d5d2c9b44 | ||
|
|
e299edf13d | ||
|
|
5a1d557c4b | ||
|
|
a58fe5501b | ||
|
|
4e82d13be5 | ||
|
|
0c569c2f13 | ||
|
|
2f4c4c34a9 | ||
|
|
ee3df04b40 | ||
|
|
e29a8cc562 | ||
|
|
e6a527cc26 | ||
|
|
9c02944dd1 | ||
|
|
38edf67292 | ||
|
|
d97f568377 | ||
|
|
c93f90ad8a | ||
|
|
2c7a1993b9 | ||
|
|
c58eea515b | ||
|
|
83ac049753 | ||
|
|
6662afb55f | ||
|
|
4ad8f1df67 | ||
|
|
d8407b7149 | ||
|
|
a6dca87556 | ||
|
|
e52747808e | ||
|
|
7425e6770e | ||
|
|
02a8e40d6d | ||
|
|
0eac419399 | ||
|
|
d5308d2a67 | ||
|
|
63046dd763 | ||
|
|
ba566add95 | ||
|
|
f1138375d2 | ||
|
|
e54a3afba4 | ||
|
|
2cb3f41347 | ||
|
|
ae799225b3 | ||
|
|
454e1549a0 | ||
|
|
1d0c76d84c | ||
|
|
9a62be32e8 | ||
|
|
669e836148 | ||
|
|
a4a4e3ba11 | ||
|
|
0d012a5022 | ||
|
|
720b57138e | ||
|
|
7551945933 | ||
|
|
a39b769ac5 | ||
|
|
62b43a822b | ||
|
|
f9151a988a | ||
|
|
8d7489b093 | ||
|
|
d1314e9c5c | ||
|
|
36a8b0fc6d | ||
|
|
e7c05cebf7 | ||
|
|
f0625a67b3 | ||
|
|
bfe94d5f1a | ||
|
|
2a9a3be47b | ||
|
|
0eac830c65 | ||
|
|
57bafcd598 | ||
|
|
e28e35b1fc | ||
|
|
4422e0b767 | ||
|
|
3feb491321 | ||
|
|
1a6753a1c6 | ||
|
|
e01b23c2a5 | ||
|
|
3cb21cd49e | ||
|
|
06aeac8fbc | ||
|
|
5e4c8c1e1a | ||
|
|
3993c50689 | ||
|
|
b469a3ce73 | ||
|
|
a05a1ed7d6 | ||
|
|
c3d3be60ac | ||
|
|
f8681411dd | ||
|
|
4d999080ca | ||
|
|
585ce70684 | ||
|
|
255e3a6c26 | ||
|
|
309fa7a19c | ||
|
|
807a6563ab | ||
|
|
772be3e113 | ||
|
|
b94a14db48 | ||
|
|
e98f3f9e74 | ||
|
|
c5036d67bc | ||
|
|
f105871a9c | ||
|
|
8c206a611d | ||
|
|
56792757b0 | ||
|
|
2b3c43d1d0 |
82
.github/workflows/cache-only.yml
vendored
82
.github/workflows/cache-only.yml
vendored
@ -1,82 +0,0 @@
|
||||
name: Cache Only
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Nix Build Cache" ]
|
||||
types: [ completed ]
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
cache-only:
|
||||
if: ${{ github.event_name != 'workflow_run' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main') }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TARGET_SHA: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}
|
||||
CACHIX_CACHE_NAME: ${{ vars.CACHIX_CACHE_NAME }}
|
||||
WAIT_MINUTES: 45
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ env.TARGET_SHA }}
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v13
|
||||
|
||||
- name: Setup Cachix (read-only)
|
||||
if: ${{ vars.CACHIX_CACHE_NAME != '' }}
|
||||
uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: ${{ vars.CACHIX_CACHE_NAME }}
|
||||
|
||||
- name: Verify Cachix has required outputs
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "${CACHIX_CACHE_NAME:-}" ]; then
|
||||
echo "vars.CACHIX_CACHE_NAME not set; skipping cache verification."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
STORE_URL="https://${CACHIX_CACHE_NAME}.cachix.org"
|
||||
echo "Checking cache: ${STORE_URL}"
|
||||
|
||||
targets=(
|
||||
packages.aarch64-darwin.openclaw
|
||||
packages.aarch64-darwin.openclaw-gateway
|
||||
packages.aarch64-darwin.openclaw-tools
|
||||
packages.aarch64-darwin.openclaw-app
|
||||
packages.x86_64-linux.openclaw
|
||||
packages.x86_64-linux.openclaw-gateway
|
||||
packages.x86_64-linux.openclaw-tools
|
||||
checks.aarch64-darwin.gateway
|
||||
checks.x86_64-linux.gateway
|
||||
checks.x86_64-linux.gateway-tests
|
||||
checks.x86_64-linux.config-options
|
||||
)
|
||||
|
||||
deadline=$(( $(date +%s) + WAIT_MINUTES * 60 ))
|
||||
|
||||
while true; do
|
||||
missing=()
|
||||
for target in "${targets[@]}"; do
|
||||
out_path=$(nix --extra-experimental-features 'nix-command flakes' eval --accept-flake-config --raw ".#${target}.outPath")
|
||||
if ! nix path-info --store "$STORE_URL" "$out_path" >/dev/null 2>&1; then
|
||||
missing+=("${target}:${out_path}")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#missing[@]} -eq 0 ]; then
|
||||
echo "Cache ready on ${STORE_URL}."
|
||||
break
|
||||
fi
|
||||
|
||||
if [ "$(date +%s)" -gt "$deadline" ]; then
|
||||
echo "Cache missing after ${WAIT_MINUTES} minutes:"
|
||||
printf ' - %s\n' "${missing[@]}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Cache still missing (${#missing[@]}). Retrying in 30s..."
|
||||
sleep 30
|
||||
done
|
||||
52
.github/workflows/ci.yml
vendored
Normal file
52
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ main ]
|
||||
workflow_dispatch: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Verify flake.lock owners
|
||||
run: scripts/check-flake-lock-owners.sh
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v31
|
||||
|
||||
- name: Run Linux CI aggregator
|
||||
run: timeout --foreground 50m nix build .#checks.x86_64-linux.ci --accept-flake-config
|
||||
|
||||
- name: Dump failing source check log
|
||||
if: failure()
|
||||
run: |
|
||||
drv="$(nix eval --raw .#checks.x86_64-linux.source-checks.drvPath --accept-flake-config)"
|
||||
nix log "$drv" | tail -n 400 || true
|
||||
|
||||
macos:
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 40
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v22
|
||||
|
||||
- name: Build Darwin CI aggregator
|
||||
timeout-minutes: 25
|
||||
run: nix build .#checks.aarch64-darwin.ci --accept-flake-config
|
||||
|
||||
- name: Run HM activation
|
||||
timeout-minutes: 10
|
||||
run: scripts/hm-activation-macos.sh
|
||||
30
.github/workflows/config-options-check.yml
vendored
30
.github/workflows/config-options-check.yml
vendored
@ -1,30 +0,0 @@
|
||||
name: Config Options Guard
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ main ]
|
||||
workflow_run:
|
||||
workflows: [ "Yolo Update Pins" ]
|
||||
types: [ completed ]
|
||||
|
||||
jobs:
|
||||
config-options:
|
||||
if: ${{ github.event_name != 'workflow_run' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main') }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TARGET_SHA: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ env.TARGET_SHA }}
|
||||
|
||||
- name: Verify flake.lock owners
|
||||
run: scripts/check-flake-lock-owners.sh
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v13
|
||||
|
||||
- name: Verify config options are up to date
|
||||
run: nix build .#checks.x86_64-linux.config-options --print-build-logs
|
||||
27
.github/workflows/hm-activation-linux.yml
vendored
27
.github/workflows/hm-activation-linux.yml
vendored
@ -1,27 +0,0 @@
|
||||
name: HM Activation (Linux)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ main ]
|
||||
workflow_run:
|
||||
workflows: [ "Yolo Update Pins" ]
|
||||
types: [ completed ]
|
||||
|
||||
jobs:
|
||||
hm-activation-linux:
|
||||
if: ${{ github.event_name != 'workflow_run' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main') }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TARGET_SHA: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ env.TARGET_SHA }}
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v13
|
||||
|
||||
- name: Run HM activation
|
||||
run: nix build .#checks.x86_64-linux.hm-activation --print-build-logs
|
||||
27
.github/workflows/hm-activation-macos.yml
vendored
27
.github/workflows/hm-activation-macos.yml
vendored
@ -1,27 +0,0 @@
|
||||
name: HM Activation (macOS)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ main ]
|
||||
workflow_run:
|
||||
workflows: [ "Yolo Update Pins" ]
|
||||
types: [ completed ]
|
||||
|
||||
jobs:
|
||||
hm-activation-macos:
|
||||
if: ${{ github.event_name != 'workflow_run' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main') }}
|
||||
runs-on: macos-14
|
||||
env:
|
||||
TARGET_SHA: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ env.TARGET_SHA }}
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v13
|
||||
|
||||
- name: Run HM activation
|
||||
run: scripts/hm-activation-macos.sh
|
||||
91
.github/workflows/nix-build-cache.yml
vendored
91
.github/workflows/nix-build-cache.yml
vendored
@ -1,91 +0,0 @@
|
||||
name: Nix Build Cache
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CACHIX_CACHE_NAME: ${{ vars.CACHIX_CACHE_NAME }}
|
||||
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v13
|
||||
|
||||
- name: Setup Cachix
|
||||
if: ${{ vars.CACHIX_CACHE_NAME != '' }}
|
||||
uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: ${{ vars.CACHIX_CACHE_NAME }}
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
|
||||
- name: Build (and push if configured)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
targets=(
|
||||
.#packages.x86_64-linux.openclaw
|
||||
.#packages.x86_64-linux.openclaw-gateway
|
||||
.#packages.x86_64-linux.openclaw-tools
|
||||
.#checks.x86_64-linux.gateway
|
||||
.#checks.x86_64-linux.gateway-tests
|
||||
.#checks.x86_64-linux.config-options
|
||||
.#checks.x86_64-linux.default-instance
|
||||
.#checks.x86_64-linux.config-validity
|
||||
.#checks.x86_64-linux.package-contents
|
||||
)
|
||||
|
||||
if [ -n "${CACHIX_CACHE_NAME:-}" ] && [ -n "${CACHIX_AUTH_TOKEN:-}" ]; then
|
||||
echo "Building + pushing to Cachix cache: ${CACHIX_CACHE_NAME}"
|
||||
cachix watch-exec "${CACHIX_CACHE_NAME}" -- \
|
||||
nix build --accept-flake-config --print-build-logs "${targets[@]}"
|
||||
else
|
||||
echo "Building without Cachix push (cache not configured)."
|
||||
nix build --accept-flake-config --print-build-logs "${targets[@]}"
|
||||
fi
|
||||
|
||||
build-macos:
|
||||
runs-on: macos-14
|
||||
env:
|
||||
CACHIX_CACHE_NAME: ${{ vars.CACHIX_CACHE_NAME }}
|
||||
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v13
|
||||
|
||||
- name: Setup Cachix
|
||||
if: ${{ vars.CACHIX_CACHE_NAME != '' }}
|
||||
uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: ${{ vars.CACHIX_CACHE_NAME }}
|
||||
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
|
||||
- name: Build (and push if configured)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
targets=(
|
||||
.#packages.aarch64-darwin.openclaw
|
||||
.#packages.aarch64-darwin.openclaw-gateway
|
||||
.#packages.aarch64-darwin.openclaw-tools
|
||||
.#packages.aarch64-darwin.openclaw-app
|
||||
.#checks.aarch64-darwin.gateway
|
||||
)
|
||||
|
||||
if [ -n "${CACHIX_CACHE_NAME:-}" ] && [ -n "${CACHIX_AUTH_TOKEN:-}" ]; then
|
||||
echo "Building + pushing to Cachix cache: ${CACHIX_CACHE_NAME}"
|
||||
cachix watch-exec "${CACHIX_CACHE_NAME}" -- \
|
||||
nix build --accept-flake-config --print-build-logs "${targets[@]}"
|
||||
else
|
||||
echo "Building without Cachix push (cache not configured)."
|
||||
nix build --accept-flake-config --print-build-logs "${targets[@]}"
|
||||
fi
|
||||
240
.github/workflows/yolo-update.yml
vendored
240
.github/workflows/yolo-update.yml
vendored
@ -5,25 +5,253 @@ on:
|
||||
- cron: "5 * * * *"
|
||||
workflow_dispatch: {}
|
||||
|
||||
concurrency:
|
||||
group: yolo-update-pins
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
update:
|
||||
select:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
outputs:
|
||||
has_update: ${{ steps.select.outputs.has_update }}
|
||||
source_tag: ${{ steps.select.outputs.source_tag }}
|
||||
source_sha: ${{ steps.select.outputs.source_sha }}
|
||||
source_version: ${{ steps.select.outputs.source_version }}
|
||||
app_tag: ${{ steps.select.outputs.app_tag }}
|
||||
app_url: ${{ steps.select.outputs.app_url }}
|
||||
app_version: ${{ steps.select.outputs.app_version }}
|
||||
latest_stable_tag: ${{ steps.select.outputs.latest_stable_tag }}
|
||||
app_lag_releases: ${{ steps.select.outputs.app_lag_releases }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Select release
|
||||
id: select
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
selection="$(scripts/update-pins.sh select)"
|
||||
|
||||
while IFS='=' read -r key value; do
|
||||
echo "${key}=${value}" >> "$GITHUB_OUTPUT"
|
||||
done <<<"$selection"
|
||||
|
||||
has_update="$(printf '%s\n' "$selection" | awk -F= '$1 == "has_update" { print $2; exit }')"
|
||||
source_tag="$(printf '%s\n' "$selection" | awk -F= '$1 == "source_tag" { print $2; exit }')"
|
||||
app_tag="$(printf '%s\n' "$selection" | awk -F= '$1 == "app_tag" { print $2; exit }')"
|
||||
latest_stable_tag="$(printf '%s\n' "$selection" | awk -F= '$1 == "latest_stable_tag" { print $2; exit }')"
|
||||
app_lag_releases="$(printf '%s\n' "$selection" | awk -F= '$1 == "app_lag_releases" { print $2; exit }')"
|
||||
|
||||
{
|
||||
echo "### OpenClaw release selection"
|
||||
echo
|
||||
echo "- Latest stable upstream release: \`${latest_stable_tag:-unknown}\`"
|
||||
echo "- Selected source release: \`${source_tag:-unknown}\`"
|
||||
echo "- Selected macOS app artifact: \`${app_tag:-preserve-current}\`"
|
||||
echo "- Update needed: \`${has_update:-unknown}\`"
|
||||
if [[ -n "${app_lag_releases:-}" ]]; then
|
||||
echo "- macOS app asset lagging source release(s): \`${app_lag_releases}\`"
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
validate-linux:
|
||||
needs: select
|
||||
if: needs.select.outputs.has_update == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
outputs:
|
||||
materialization_digest: ${{ steps.materialization.outputs.materialization_digest }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v31
|
||||
|
||||
- name: Materialize selected release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
scripts/update-pins.sh apply \
|
||||
"${{ needs.select.outputs.source_tag }}" \
|
||||
"${{ needs.select.outputs.source_sha }}" \
|
||||
"${{ needs.select.outputs.app_tag }}" \
|
||||
"${{ needs.select.outputs.app_url }}"
|
||||
|
||||
- name: Record materialized diff digest
|
||||
id: materialization
|
||||
run: |
|
||||
set -euo pipefail
|
||||
digest="$(
|
||||
git diff --binary -- \
|
||||
nix/sources/openclaw-source.nix \
|
||||
nix/packages/openclaw-app.nix \
|
||||
nix/generated/openclaw-config-options.nix \
|
||||
| shasum -a 256 \
|
||||
| awk '{ print $1 }'
|
||||
)"
|
||||
echo "materialization_digest=${digest}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Verify flake.lock owners
|
||||
run: scripts/check-flake-lock-owners.sh
|
||||
|
||||
- name: Run Linux CI aggregator
|
||||
run: timeout --foreground 50m nix build .#checks.x86_64-linux.ci --accept-flake-config
|
||||
|
||||
- name: Dump failing source check log
|
||||
if: failure()
|
||||
run: |
|
||||
drv="$(nix eval --raw .#checks.x86_64-linux.source-checks.drvPath --accept-flake-config)"
|
||||
nix log "$drv" | tail -n 400 || true
|
||||
|
||||
validate-macos:
|
||||
needs: select
|
||||
if: needs.select.outputs.has_update == 'true'
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 40
|
||||
outputs:
|
||||
materialization_digest: ${{ steps.materialization.outputs.materialization_digest }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v22
|
||||
|
||||
- name: Materialize selected release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
scripts/update-pins.sh apply \
|
||||
"${{ needs.select.outputs.source_tag }}" \
|
||||
"${{ needs.select.outputs.source_sha }}" \
|
||||
"${{ needs.select.outputs.app_tag }}" \
|
||||
"${{ needs.select.outputs.app_url }}"
|
||||
|
||||
- name: Record materialized diff digest
|
||||
id: materialization
|
||||
run: |
|
||||
set -euo pipefail
|
||||
digest="$(
|
||||
git diff --binary -- \
|
||||
nix/sources/openclaw-source.nix \
|
||||
nix/packages/openclaw-app.nix \
|
||||
nix/generated/openclaw-config-options.nix \
|
||||
| shasum -a 256 \
|
||||
| awk '{ print $1 }'
|
||||
)"
|
||||
echo "materialization_digest=${digest}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build Darwin CI aggregator
|
||||
timeout-minutes: 25
|
||||
run: nix build .#checks.aarch64-darwin.ci --accept-flake-config
|
||||
|
||||
- name: Run HM activation
|
||||
timeout-minutes: 10
|
||||
run: scripts/hm-activation-macos.sh
|
||||
|
||||
promote:
|
||||
needs:
|
||||
- select
|
||||
- validate-linux
|
||||
- validate-macos
|
||||
if: needs.select.outputs.has_update == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
actions: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v13
|
||||
uses: DeterminateSystems/nix-installer-action@v22
|
||||
|
||||
- name: Run updater
|
||||
- name: Promote selected release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
LINUX_MATERIALIZATION_DIGEST: ${{ needs.validate-linux.outputs.materialization_digest }}
|
||||
MACOS_MATERIALIZATION_DIGEST: ${{ needs.validate-macos.outputs.materialization_digest }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name "openclaw-ci"
|
||||
git config user.email "ci@openclaw.local"
|
||||
scripts/update-pins.sh
|
||||
|
||||
if [[ -z "$LINUX_MATERIALIZATION_DIGEST" || -z "$MACOS_MATERIALIZATION_DIGEST" ]]; then
|
||||
echo "Missing validation materialization digest." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$LINUX_MATERIALIZATION_DIGEST" != "$MACOS_MATERIALIZATION_DIGEST" ]]; then
|
||||
echo "Linux and macOS materialized different release diffs." >&2
|
||||
echo "Linux: $LINUX_MATERIALIZATION_DIGEST" >&2
|
||||
echo "macOS: $MACOS_MATERIALIZATION_DIGEST" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
scripts/update-pins.sh apply \
|
||||
"${{ needs.select.outputs.source_tag }}" \
|
||||
"${{ needs.select.outputs.source_sha }}" \
|
||||
"${{ needs.select.outputs.app_tag }}" \
|
||||
"${{ needs.select.outputs.app_url }}"
|
||||
|
||||
if git diff --quiet -- \
|
||||
nix/sources/openclaw-source.nix \
|
||||
nix/packages/openclaw-app.nix \
|
||||
nix/generated/openclaw-config-options.nix; then
|
||||
echo "No pin changes detected."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
promote_digest="$(
|
||||
git diff --binary -- \
|
||||
nix/sources/openclaw-source.nix \
|
||||
nix/packages/openclaw-app.nix \
|
||||
nix/generated/openclaw-config-options.nix \
|
||||
| shasum -a 256 \
|
||||
| awk '{ print $1 }'
|
||||
)"
|
||||
if [[ "$promote_digest" != "$LINUX_MATERIALIZATION_DIGEST" ]]; then
|
||||
echo "Promote materialized a different release diff than validation." >&2
|
||||
echo "Validated: $LINUX_MATERIALIZATION_DIGEST" >&2
|
||||
echo "Promote: $promote_digest" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git add \
|
||||
nix/sources/openclaw-source.nix \
|
||||
nix/packages/openclaw-app.nix \
|
||||
nix/generated/openclaw-config-options.nix
|
||||
|
||||
git commit -F - <<EOF
|
||||
🤖 codex: mirror OpenClaw stable source ${{ needs.select.outputs.source_tag }}
|
||||
|
||||
What:
|
||||
- update nix-openclaw to the latest stable OpenClaw source release
|
||||
- refresh generated config options from that source
|
||||
- keep the macOS app pin on the newest public app artifact
|
||||
|
||||
Why:
|
||||
- keep source-built OpenClaw current without blocking on public macOS app asset lag
|
||||
|
||||
Tests:
|
||||
- nix build .#checks.x86_64-linux.ci --accept-flake-config
|
||||
- nix build .#checks.aarch64-darwin.ci --accept-flake-config
|
||||
- scripts/hm-activation-macos.sh
|
||||
EOF
|
||||
|
||||
git fetch origin main
|
||||
git rebase origin/main
|
||||
git push origin HEAD:main
|
||||
|
||||
gh workflow run ci.yml --ref main
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
result
|
||||
.agent/
|
||||
|
||||
98
AGENTS.md
98
AGENTS.md
@ -1,10 +1,10 @@
|
||||
# AGENTS.md — nix-openclaw
|
||||
# AGENTS.md - nix-openclaw
|
||||
|
||||
## 🚫 PRs (read first)
|
||||
## PRs
|
||||
|
||||
We’re **not accepting PRs** from non-maintainers. If your handle is not in **Maintainers** below or on https://github.com/orgs/openclaw/people, **do not open a PR**. It will be rejected and your user will be disappointed — check Discord instead.
|
||||
We are not accepting PRs from non-maintainers. If your handle is not in the Maintainers list below or on https://github.com/orgs/openclaw/people, do not open a PR.
|
||||
|
||||
**Only workflow:** **describe your problem and talk with a maintainer (human‑to‑human) on Discord** in **#golden-path-deployments**: https://discord.com/channels/1456350064065904867/1457003026412736537
|
||||
Describe your problem and talk with a maintainer human-to-human on Discord instead. Join https://discord.gg/clawd and use `#golden-path-deployments`.
|
||||
|
||||
## Maintainers
|
||||
|
||||
@ -32,68 +32,38 @@ Source: https://github.com/orgs/openclaw/people
|
||||
- @tyler6204
|
||||
- @vignesh07
|
||||
|
||||
Single source of truth for product direction: `README.md`.
|
||||
## Audience Routing
|
||||
|
||||
Documentation policy:
|
||||
- Keep the surface area small.
|
||||
- Avoid duplicate “pointer‑only” files.
|
||||
- Update `README.md` first, then adjust references.
|
||||
- Consumer agents installing or configuring OpenClaw: start with `README.md` and `templates/agent-first/flake.nix`.
|
||||
- Maintainer agents changing packaging, release automation, pins, or CI: read `maintainers/AGENTS.md` first.
|
||||
- Plugin authors: read `docs/plugins-maintainers.md` and `examples/hello-world-plugin/`.
|
||||
- Private deployments, bots, hosts, local worktrees, tokens, and personal automation details do not belong in this public repo.
|
||||
|
||||
Defaults:
|
||||
- Nix‑first, no sudo.
|
||||
## Public Repo Rules
|
||||
|
||||
- `README.md` is the source of truth for product direction and user-facing behavior.
|
||||
- Keep documentation surface area small. Update `README.md` first, then adjust references.
|
||||
- Keep committed guidance about public `nix-openclaw` behavior, public upstream OpenClaw releases, public artifacts, and public CI.
|
||||
- Keep consumer setup docs in `README.md`, templates, and module docs.
|
||||
- Keep maintainer runbooks in `maintainers/`.
|
||||
- Never add internal ExecPlans or agent scratch history to this repo. `.agent/` is ignored for this reason.
|
||||
- If a private deployment exposes a public packaging bug, fix the public package here and keep deployment-specific repair elsewhere.
|
||||
- OpenClaw plugin loading belongs here: package curated runtime plugin roots as Nix artifacts, expose curated outputs through package/check outputs for Garnix, and let host repos only enable/configure them.
|
||||
- Do not make host config run npm/ClawHub installs at runtime for the batteries-included path. `customPlugins.source = "npm:..."` is allowed only when nix-openclaw turns it into an immutable, hash-backed store path and wires it through OpenClaw's normal `plugins.load.paths`.
|
||||
|
||||
## Packaging Defaults
|
||||
|
||||
- Nix-first, no sudo.
|
||||
- Declarative config only.
|
||||
- Batteries‑included install is the baseline.
|
||||
- Breaking changes are acceptable pre‑1.0.0 (move fast, keep docs accurate).
|
||||
- No deprecations; use breaking changes.
|
||||
- NO INLINE SCRIPTS EVER.
|
||||
- NEVER send any message (iMessage, email, SMS, etc.) without explicit user confirmation:
|
||||
- Always show the full message text and ask: “I’m going to send this: <message>. Send? (y/n)”
|
||||
- Batteries-included install is the baseline.
|
||||
- Breaking changes are acceptable pre-1.0.0; no deprecations.
|
||||
- No inline scripts or inline file contents in Nix code. Use repo scripts and explicit file paths.
|
||||
- The gateway package must include Control UI assets.
|
||||
- User-facing docs should lead with one package: `openclaw`. Treat `openclaw-gateway` and `openclaw-app` as component outputs for modules, checks, and debugging.
|
||||
- QMD is the Nix-supported batteries-included local memory backend. Keep `qmd` internal to the `openclaw` wrapper PATH; users opt in with upstream config.
|
||||
|
||||
OpenClaw packaging:
|
||||
- The gateway package must include Control UI assets (run `pnpm ui:build` in the Nix build).
|
||||
## Safety
|
||||
|
||||
Golden path for pins (yolo + manual bumps):
|
||||
- Hourly GitHub Action **Yolo Update Pins** runs `scripts/update-pins.sh`, which:
|
||||
- Picks latest upstream openclaw SHA with green non-Windows checks
|
||||
- Rebuilds gateway to refresh `pnpmDepsHash`
|
||||
- Regenerates `nix/generated/openclaw-config-options.nix` from upstream schema
|
||||
- Updates app pin/hash, commits, rebases, pushes to `main`
|
||||
- Manual bump (rare): `GH_TOKEN=... scripts/update-pins.sh` (same steps as above). Use only if yolo is blocked.
|
||||
- To verify freshness: `git pull --ff-only` and check `nix/sources/openclaw-source.nix` vs `git ls-remote https://github.com/openclaw/openclaw.git refs/heads/main`.
|
||||
- If upstream is moving fast and tighter freshness is needed, trigger yolo manually: `gh workflow run "Yolo Update Pins"`.
|
||||
|
||||
CI polling (hard rule):
|
||||
- Never say "I'll keep polling" unless you are **already** running a blocking loop.
|
||||
- If you must report status, confirm the loop is active (`tmux ls` / session name).
|
||||
- Use a blocking bash loop in tmux (preferred) or a sub-agent; do not fake it.
|
||||
- Example: `tmux new -s nix-openclaw-ci '/tmp/poll-nix-openclaw-ci.sh'`.
|
||||
|
||||
Philosophy:
|
||||
|
||||
The Zen of ~~Python~~ OpenClaw, ~~by~~ shamelessly stolen from Tim Peters
|
||||
|
||||
Beautiful is better than ugly.
|
||||
Explicit is better than implicit.
|
||||
Simple is better than complex.
|
||||
Complex is better than complicated.
|
||||
Flat is better than nested.
|
||||
Sparse is better than dense.
|
||||
Readability counts.
|
||||
Special cases aren't special enough to break the rules.
|
||||
Although practicality beats purity.
|
||||
Errors should never pass silently.
|
||||
Unless explicitly silenced.
|
||||
In the face of ambiguity, refuse the temptation to guess.
|
||||
There should be one-- and preferably only one --obvious way to do it.
|
||||
Although that way may not be obvious at first unless you're Dutch.
|
||||
Now is better than never.
|
||||
Although never is often better than *right* now.
|
||||
If the implementation is hard to explain, it's a bad idea.
|
||||
If the implementation is easy to explain, it may be a good idea.
|
||||
Namespaces are one honking great idea -- let's do more of those!
|
||||
|
||||
Nix file policy:
|
||||
- No inline file contents in Nix code, ever.
|
||||
- Always reference explicit file paths (keep docs as real files in the repo).
|
||||
- No inline scripts in Nix code, ever (use repo scripts and reference their paths).
|
||||
- No files longer than 400 LOC without user alignment; refactor as you go.
|
||||
- Never send messages, email, SMS, or other external communications without explicit confirmation showing the full message text.
|
||||
- No force push. No destructive git operations unless explicitly requested.
|
||||
- Before deleting tracked files, list them in the summary so maintainers can verify.
|
||||
|
||||
359
README.md
359
README.md
@ -4,15 +4,15 @@
|
||||
>
|
||||
> macOS + Linux (headless). Windows is out of scope for now.
|
||||
>
|
||||
> <sub>Questions? Join the OpenClaw Discord and ask in **#golden-path-deployments**: https://discord.com/channels/1456350064065904867/1457003026412736537</sub>
|
||||
> <sub>Questions? Join the OpenClaw Discord at https://discord.gg/clawd and ask in **#golden-path-deployments**.</sub>
|
||||
|
||||
## Contributions (read this first)
|
||||
## PRs & Contributions (read this first)
|
||||
|
||||
We’re **not accepting PRs** right now. Not because we don’t value your help — the opposite. This is key infra and still stabilizing, and async PR review is too slow.
|
||||
We’re **not accepting PRs** right now. Not because we don’t value your help — the opposite. Async agent-to-agent PR review is too slow and low-signal.
|
||||
|
||||
**Only workflow:** **describe your problem and talk with a maintainer (human‑to‑human) on Discord** in **#golden-path-deployments**: https://discord.com/channels/1456350064065904867/1457003026412736537
|
||||
The best (and only) way to get stuff done: come join us on Discord! Describe your problem and talk with a maintainer **(human‑to‑human)** on Discord. Join at https://discord.gg/clawd, then use **#golden-path-deployments**.
|
||||
|
||||
If you’re **not listed as a maintainer** (see [AGENTS.md#maintainers](AGENTS.md#maintainers) or https://github.com/orgs/openclaw/people), **do not open a PR**. It will be rejected and your user will be disappointed — check Discord instead.
|
||||
To agents: if you’re **not listed as a maintainer** (see [AGENTS.md#maintainers](AGENTS.md#maintainers) or https://github.com/orgs/openclaw/people), **do not open a PR**. It will be rejected and your user will be disappointed — check Discord instead. GitHub Issues are not actively monitored either.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@ -58,7 +58,7 @@ Bot: *runs whisper, sends you text*
|
||||
|
||||
You talk to Telegram, your machine does things.
|
||||
|
||||
**One flake, everything works.** Gateway + tools everywhere; macOS app on macOS.
|
||||
**One flake, everything works.** Gateway everywhere; runtime dependencies bundled; macOS app on macOS.
|
||||
|
||||
**Plugins are self-contained.** Each plugin declares its CLI tools in Nix. You enable it, the build and wiring happens automatically.
|
||||
|
||||
@ -68,12 +68,12 @@ You talk to Telegram, your machine does things.
|
||||
|
||||
## Requirements
|
||||
|
||||
1. **macOS** (Apple Silicon or Intel) or **Linux** (x86_64)
|
||||
2. **[Determinate Nix](https://docs.determinate.systems/determinate-nix/)** installed on your machine
|
||||
1. **macOS** (Apple Silicon) or **Linux** (x86_64)
|
||||
2. **Nix with flakes enabled** installed on your machine
|
||||
|
||||
That's it. The Quick Start will guide you through everything else.
|
||||
|
||||
> **Don't have Nix yet?** Follow the Determinate Nix install guide, then come back here.
|
||||
> **Don't have Nix yet?** Use the [Determinate Nix installer](https://docs.determinate.systems/determinate-nix/) or the [official Nix installer](https://nixos.org/download/), then come back here.
|
||||
|
||||
---
|
||||
|
||||
@ -119,40 +119,49 @@ Nix is a **declarative package manager**. Instead of running commands to install
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option 1: Let your agent set it up (recommended)
|
||||
### Option 1: Ask your coding agent (recommended)
|
||||
|
||||
Copy this entire block and paste it to Claude, Cursor, or your preferred AI assistant:
|
||||
Tell your coding agent you want OpenClaw set up with Nix. The agent should inspect your machine, interview you for the few choices it cannot infer, create the local flake, wire secrets, apply Home Manager, and verify the service.
|
||||
|
||||
Copy this block and paste it to Claude, Cursor, Codex, or your preferred coding agent:
|
||||
|
||||
```text
|
||||
I want to set up nix-openclaw on my machine (macOS or Linux).
|
||||
I want to set up nix-openclaw on my machine (Apple Silicon macOS or x86_64 Linux).
|
||||
|
||||
Repository: github:openclaw/nix-openclaw
|
||||
|
||||
What nix-openclaw is:
|
||||
- Batteries-included Nix package for OpenClaw (AI assistant gateway)
|
||||
- Installs gateway + tools everywhere; macOS app only on macOS
|
||||
- Installs the gateway everywhere; macOS app only on macOS
|
||||
- Runs as a launchd service on macOS, systemd user service on Linux
|
||||
|
||||
What I need you to do:
|
||||
1. Check if Determinate Nix is installed (if not, install it)
|
||||
2. Create a local flake at ~/code/openclaw-local using templates/agent-first/flake.nix
|
||||
3. Create a docs dir next to the config (e.g., ~/code/openclaw-local/documents) with AGENTS.md, SOUL.md, TOOLS.md (optional: IDENTITY.md, USER.md, LORE.md, HEARTBEAT.md, PROMPTING-EXAMPLES.md)
|
||||
1. Inspect my OS, CPU architecture, shell, Home Manager setup, and whether Nix with flakes is installed
|
||||
2. Ask me only for missing choices: channel, bot/account secrets, allowed users, provider keys, and documents/identity preferences
|
||||
3. Create a local flake at ~/code/openclaw-local using templates/agent-first/flake.nix
|
||||
4. Create a docs dir next to the config (e.g., ~/code/openclaw-local/documents) with AGENTS.md, SOUL.md, TOOLS.md (optional: IDENTITY.md, USER.md, LORE.md, HEARTBEAT.md, PROMPTING-EXAMPLES.md)
|
||||
- If ~/.openclaw/workspace already has these files, adopt them into the documents dir first (use copy/rsync that dereferences symlinks, e.g. `cp -L`)
|
||||
4. Help me create a Telegram bot (@BotFather) and get my chat ID (@userinfobot)
|
||||
5. Set up secrets (bot token, Anthropic key) - plain files at ~/.secrets/ is fine
|
||||
6. Fill in the template placeholders and run home-manager switch
|
||||
7. Verify: service running, bot responds to messages
|
||||
5. Help me create or connect the channel account I choose
|
||||
6. Set up secrets (bot token, provider key) - plain files at ~/.secrets/ are fine unless I already have a secret manager
|
||||
7. Ask whether I want local memory through QMD; if yes, set `memory.backend = "qmd"` in OpenClaw config
|
||||
8. Fill in the template placeholders and run home-manager switch
|
||||
9. Verify end-to-end: package builds, service is running, gateway health works, QMD works if enabled, and the bot/channel responds if configured
|
||||
|
||||
My setup:
|
||||
- OS: [macOS / Linux]
|
||||
- CPU: [arm64 / x86_64]
|
||||
- System: [aarch64-darwin / x86_64-darwin / x86_64-linux]
|
||||
- System: [aarch64-darwin / x86_64-linux]
|
||||
- Home Manager config name: [FILL IN or "I don't have Home Manager yet"]
|
||||
|
||||
Reference the README and templates/agent-first/flake.nix in the repo for the module options.
|
||||
```
|
||||
|
||||
Your agent will install Nix, create your config, and get OpenClaw running. You just answer its questions.
|
||||
Your agent should do the setup work. You answer its short questions and confirm before it sends messages or changes external services.
|
||||
|
||||
QMD packaging note for agents: Linux uses upstream `github:tobi/qmd`; Darwin
|
||||
uses the `nix-openclaw-tools` QMD repair package until upstream Darwin packaging
|
||||
is fixed. Keep both pinned to the same QMD release unless there is a tested
|
||||
reason to diverge.
|
||||
|
||||
**What happens next:**
|
||||
1. Your agent sets everything up and runs `home-manager switch`
|
||||
@ -165,16 +174,17 @@ Your agent will install Nix, create your config, and get OpenClaw running. You j
|
||||
|
||||
### macOS (Home Manager + launchd)
|
||||
|
||||
1. Install Determinate Nix.
|
||||
1. Install Nix with flakes enabled.
|
||||
2. Create a local config:
|
||||
```bash
|
||||
mkdir -p ~/code/openclaw-local && cd ~/code/openclaw-local
|
||||
nix flake init -t github:openclaw/nix-openclaw#agent-first
|
||||
```
|
||||
3. Edit `flake.nix` placeholders:
|
||||
- `system` = `aarch64-darwin` (Apple Silicon) or `x86_64-darwin` (Intel)
|
||||
- `system` = `aarch64-darwin`
|
||||
- `home.username` and `home.homeDirectory`
|
||||
- `programs.openclaw.documents` with `AGENTS.md`, `SOUL.md`, `TOOLS.md` (optional: `IDENTITY.md`, `USER.md`, `LORE.md`, `HEARTBEAT.md`, `PROMPTING-EXAMPLES.md`)
|
||||
- Keep this directory inside the flake, or make sure the Nix daemon can read it and traverse every parent directory.
|
||||
- Provider secrets (Telegram/Discord tokens, Anthropic API key)
|
||||
4. Apply:
|
||||
```bash
|
||||
@ -187,7 +197,7 @@ Your agent will install Nix, create your config, and get OpenClaw running. You j
|
||||
|
||||
### Linux (headless + systemd user service)
|
||||
|
||||
1. Install Determinate Nix.
|
||||
1. Install Nix with flakes enabled.
|
||||
2. Create a local config:
|
||||
```bash
|
||||
mkdir -p ~/code/openclaw-local && cd ~/code/openclaw-local
|
||||
@ -197,6 +207,7 @@ Your agent will install Nix, create your config, and get OpenClaw running. You j
|
||||
- `system` = `x86_64-linux`
|
||||
- `home.username` and `home.homeDirectory` (e.g., `/home/<user>`)
|
||||
- `programs.openclaw.documents` with `AGENTS.md`, `SOUL.md`, `TOOLS.md` (optional: `IDENTITY.md`, `USER.md`, `LORE.md`, `HEARTBEAT.md`, `PROMPTING-EXAMPLES.md`)
|
||||
- Keep this directory inside the flake, or make sure the Nix daemon can read it and traverse every parent directory.
|
||||
- Provider secrets (Telegram/Discord tokens, Anthropic API key)
|
||||
4. Apply:
|
||||
```bash
|
||||
@ -254,21 +265,22 @@ All state lives in `~/.openclaw/`. Logs at `/tmp/openclaw/openclaw-gateway.log`.
|
||||
|
||||
Plugins extend what OpenClaw can do. Each plugin bundles tools and teaches the AI how to use them.
|
||||
|
||||
### First-party plugins
|
||||
### Bundled plugins
|
||||
|
||||
These ship with nix-openclaw. Toggle them in your config:
|
||||
These ship with nix-openclaw. Catalog source of truth: `nix/modules/home-manager/openclaw/plugin-catalog.nix`.
|
||||
Toggle them in your config:
|
||||
|
||||
```nix
|
||||
programs.openclaw.bundledPlugins = {
|
||||
summarize.enable = true; # Summarize web pages, PDFs, videos
|
||||
discrawl.enable = false; # Discord archive/search
|
||||
wacrawl.enable = false; # WhatsApp archive/search
|
||||
peekaboo.enable = true; # Take screenshots
|
||||
oracle.enable = false; # Web search
|
||||
poltergeist.enable = false; # Control your macOS UI
|
||||
poltergeist.enable = false; # File watching and automation
|
||||
sag.enable = false; # Text-to-speech
|
||||
camsnap.enable = false; # Camera snapshots
|
||||
gogcli.enable = false; # Google Calendar
|
||||
goplaces.enable = true; # Google Places API
|
||||
bird.enable = false; # Twitter/X
|
||||
sonoscli.enable = false; # Sonos control
|
||||
imsg.enable = false; # iMessage
|
||||
};
|
||||
@ -283,31 +295,48 @@ programs.openclaw.bundledPlugins.goplaces = {
|
||||
| Plugin | What it does |
|
||||
|--------|--------------|
|
||||
| `summarize` | Summarize URLs, PDFs, YouTube videos |
|
||||
| `discrawl` | Archive and search Discord history |
|
||||
| `wacrawl` | Archive and search WhatsApp Desktop history |
|
||||
| `peekaboo` | Screenshot your screen |
|
||||
| `oracle` | Search the web |
|
||||
| `poltergeist` | Click, type, control macOS UI |
|
||||
| `poltergeist` | File watching and automation |
|
||||
| `sag` | Text-to-speech |
|
||||
| `camsnap` | Take photos from connected cameras |
|
||||
| `gogcli` | Google Calendar integration |
|
||||
| `goplaces` | Google Places API (New) CLI |
|
||||
| `bird` | Twitter/X integration |
|
||||
| `sonoscli` | Control Sonos speakers |
|
||||
| `imsg` | Send/read iMessages |
|
||||
|
||||
### Adding community plugins
|
||||
|
||||
Tell your agent: *"Add the plugin from github:owner/repo-name"*
|
||||
Tell your agent: *"Add the plugin from github:owner/repo-name and pin it."*
|
||||
|
||||
Or add it manually to your config:
|
||||
|
||||
```nix
|
||||
customPlugins = [
|
||||
{ source = "github:owner/repo-name"; }
|
||||
{ source = "github:owner/repo-name?rev=<commit>&narHash=<narHash>"; }
|
||||
];
|
||||
```
|
||||
|
||||
Then run `home-manager switch` to install.
|
||||
|
||||
For an OpenClaw native plugin published to npm, keep the source shape close to
|
||||
OpenClaw's own install command and let Nix build the immutable plugin root:
|
||||
|
||||
```nix
|
||||
customPlugins = [
|
||||
{
|
||||
source = "npm:@scope/openclaw-plugin@1.2.3";
|
||||
id = "openclaw-plugin";
|
||||
hash = lib.fakeHash; # replace with the sha256 Nix reports
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
Use this for OpenClaw runtime plugins with `openclaw.plugin.json` /
|
||||
`package.json.openclaw`. It does not run npm at gateway startup; Nix builds and
|
||||
caches the plugin root, then adds it to OpenClaw's `plugins.load.paths`.
|
||||
|
||||
### Plugins with configuration
|
||||
|
||||
Some plugins need settings (auth files, preferences). Here's a simplified example:
|
||||
@ -316,7 +345,7 @@ Some plugins need settings (auth files, preferences). Here's a simplified exampl
|
||||
# Example: a padel court booking plugin (simplified for illustration)
|
||||
customPlugins = [
|
||||
{
|
||||
source = "github:example/padel-cli";
|
||||
source = "github:example/padel-cli?rev=<commit>&narHash=<narHash>";
|
||||
config = {
|
||||
env = {
|
||||
PADEL_AUTH_FILE = "~/.secrets/padel-auth"; # where your login token lives
|
||||
@ -393,7 +422,7 @@ Contract to implement:
|
||||
1) Add openclawPlugin output in flake.nix:
|
||||
- name
|
||||
- skills (paths to SKILL.md dirs)
|
||||
- packages (CLI packages to put on PATH)
|
||||
- packages (CLI packages to put on the OpenClaw runtime PATH)
|
||||
- needs (stateDirs + requiredEnv)
|
||||
|
||||
Example:
|
||||
@ -420,9 +449,9 @@ openclawPlugin = {
|
||||
|
||||
Standard plugin config shape (Nix-native, no JSON strings):
|
||||
|
||||
plugins = [
|
||||
customPlugins = [
|
||||
{
|
||||
source = "github:owner/my-plugin";
|
||||
source = "github:owner/my-plugin?rev=<commit>&narHash=<narHash>";
|
||||
config = {
|
||||
env = {
|
||||
MYPLUGIN_AUTH_FILE = "/run/agenix/myplugin-auth";
|
||||
@ -444,7 +473,7 @@ Config flags the host will use:
|
||||
- `config.settings` for typed config keys (rendered to config.json in the first stateDir)
|
||||
|
||||
CI note:
|
||||
- Make sure the plugin build is covered by CI (e.g. included in the repo's Nix build target list / cache-warming workflow).
|
||||
- If the repo uses Garnix, add the plugin build to its `garnix.yaml` (or equivalent) so CI verifies it.
|
||||
|
||||
Why: explicit, minimal, fail-fast, no inline JSON strings.
|
||||
Deliverables: flake output, env overrides, AGENTS.md, skill update.
|
||||
@ -491,10 +520,7 @@ The simplest setup:
|
||||
};
|
||||
};
|
||||
|
||||
# Built-ins (tools + skills) shipped via nix-steipete-tools.
|
||||
plugins = [
|
||||
{ source = "github:openclaw/nix-steipete-tools?dir=tools/summarize"; }
|
||||
];
|
||||
bundledPlugins.summarize.enable = true;
|
||||
};
|
||||
}
|
||||
```
|
||||
@ -532,40 +558,37 @@ Uses `instances.default` to unlock per-group mention rules. If `instances` is se
|
||||
};
|
||||
};
|
||||
|
||||
bundledPlugins.peekaboo.enable = true;
|
||||
customPlugins = [
|
||||
{ source = "github:joshp123/xuezh?rev=<commit>&narHash=<narHash>"; }
|
||||
{
|
||||
source = "github:joshp123/padel-cli?rev=<commit>&narHash=<narHash>";
|
||||
config = {
|
||||
env = { PADEL_AUTH_FILE = "/run/agenix/padel-auth"; };
|
||||
settings = {
|
||||
default_location = "CITY_NAME";
|
||||
preferred_times = [ "18:00" "20:00" ];
|
||||
preferred_duration = 90;
|
||||
venues = [
|
||||
{
|
||||
id = "VENUE_ID";
|
||||
alias = "VENUE_ALIAS";
|
||||
name = "VENUE_NAME";
|
||||
indoor = true;
|
||||
timezone = "TIMEZONE";
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
instances.default = {
|
||||
enable = true;
|
||||
package = pkgs.openclaw; # batteries-included
|
||||
stateDir = "~/.openclaw";
|
||||
workspaceDir = "~/.openclaw/workspace";
|
||||
launchd.enable = true;
|
||||
|
||||
# Plugins (prod: pinned GitHub). Built-ins are via nix-steipete-tools.
|
||||
# MVP target: repo pointers resolve to tools + skills automatically.
|
||||
plugins = [
|
||||
{ source = "github:openclaw/nix-steipete-tools?dir=tools/oracle"; }
|
||||
{ source = "github:openclaw/nix-steipete-tools?dir=tools/peekaboo"; }
|
||||
{ source = "github:joshp123/xuezh"; }
|
||||
{
|
||||
source = "github:joshp123/padel-cli";
|
||||
config = {
|
||||
env = { PADEL_AUTH_FILE = "/run/agenix/padel-auth"; };
|
||||
settings = {
|
||||
default_location = "CITY_NAME";
|
||||
preferred_times = [ "18:00" "20:00" ];
|
||||
preferred_duration = 90;
|
||||
venues = [
|
||||
{
|
||||
id = "VENUE_ID";
|
||||
alias = "VENUE_ALIAS";
|
||||
name = "VENUE_NAME";
|
||||
indoor = true;
|
||||
timezone = "TIMEZONE";
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
@ -577,138 +600,88 @@ Uses `instances.default` to unlock per-group mention rules. If `instances` is se
|
||||
|
||||
### Dual-instance setup (prod + dev)
|
||||
|
||||
Use a shared base config and override only what's different. After changing local plugin or gateway code, re-run `home-manager switch` to rebuild.
|
||||
Use named instances when you need two local gateways. Keep the default package unless you are actively debugging a local gateway checkout.
|
||||
|
||||
```nix
|
||||
# flake inputs (pin prod + app)
|
||||
inputs = {
|
||||
nix-openclaw.url = "github:openclaw/nix-openclaw?ref=v0.1.0"; # pins macOS app + gateway bundle
|
||||
};
|
||||
programs.openclaw = {
|
||||
documents = ./documents;
|
||||
|
||||
let
|
||||
prodConfig = {
|
||||
channels.telegram = {
|
||||
tokenFile = "/run/agenix/telegram-prod";
|
||||
allowFrom = [ 12345678 ];
|
||||
instances = {
|
||||
prod = {
|
||||
enable = true;
|
||||
gatewayPort = 18789;
|
||||
config.channels.telegram = {
|
||||
tokenFile = "/run/agenix/telegram-prod";
|
||||
allowFrom = [ 12345678 ];
|
||||
};
|
||||
plugins = [
|
||||
{ source = "github:owner/your-plugin?rev=<commit>&narHash=<narHash>"; }
|
||||
];
|
||||
};
|
||||
};
|
||||
devConfig = {
|
||||
channels.telegram = {
|
||||
tokenFile = "/run/agenix/telegram-dev";
|
||||
allowFrom = [ 12345678 ];
|
||||
};
|
||||
};
|
||||
prod = {
|
||||
enable = true;
|
||||
# Prod gateway pin (comes from nix-openclaw input @ v0.1.0 above).
|
||||
package = inputs.nix-openclaw.packages.${pkgs.system}.openclaw-gateway;
|
||||
config = prodConfig;
|
||||
plugins = [ { source = "github:owner/your-plugin"; } ];
|
||||
};
|
||||
in {
|
||||
# Pinned macOS app (POC: no local app builds, uses nix-openclaw @ v0.1.0 above).
|
||||
programs.openclaw.appPackage =
|
||||
inputs.nix-openclaw.packages.${pkgs.system}.openclaw-app;
|
||||
programs.openclaw.documents = ./documents;
|
||||
programs.openclaw.instances = {
|
||||
prod = prod;
|
||||
dev = prod // {
|
||||
# Dev uses the same pinned macOS app (from nix-openclaw input),
|
||||
# but overrides the gateway package to a local checkout.
|
||||
config = devConfig;
|
||||
|
||||
dev = {
|
||||
enable = true;
|
||||
gatewayPort = 18790;
|
||||
# Local gateway checkout (path). App stays pinned.
|
||||
gatewayPath = "/Users/you/code/openclaw";
|
||||
# Local plugin overrides prod if names collide (last wins).
|
||||
plugins = prod.plugins ++ [
|
||||
config.channels.telegram = {
|
||||
tokenFile = "/run/agenix/telegram-dev";
|
||||
allowFrom = [ 12345678 ];
|
||||
};
|
||||
plugins = [
|
||||
{ source = "path:/Users/you/code/your-plugin"; }
|
||||
{
|
||||
source = "github:joshp123/padel-cli";
|
||||
config = {
|
||||
env = { PADEL_AUTH_FILE = "/run/agenix/padel-auth-dev"; };
|
||||
settings = {
|
||||
default_location = "CITY_NAME";
|
||||
preferred_times = [ "18:00" ];
|
||||
preferred_duration = 90;
|
||||
venues = [];
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Plugin collisions
|
||||
|
||||
Plugins are keyed by their declared `name`. If two plugins declare the same name, the **last entry wins** (use this to override a prod plugin with a local dev one).
|
||||
|
||||
### Tool overrides (avoid collisions)
|
||||
|
||||
Home Manager auto-excludes `git` when `programs.git.enable = true`.
|
||||
|
||||
Drop built-in tools that you already install elsewhere:
|
||||
|
||||
```nix
|
||||
programs.openclaw.excludeTools = [ "git" "jq" "ripgrep" ];
|
||||
```
|
||||
|
||||
Or provide a custom list:
|
||||
|
||||
```nix
|
||||
programs.openclaw.toolNames = [ "nodejs_22" "pnpm_10" "summarize" ];
|
||||
```
|
||||
|
||||
If you override `programs.openclaw.package`, use `pkgs.openclawPackages.withTools { ... }.openclaw` to apply these lists.
|
||||
|
||||
---
|
||||
|
||||
## Packaging & Updates
|
||||
|
||||
**Goal:** `nix-openclaw` is a great Nix package. Automation, promotion, and fleet rollout live elsewhere.
|
||||
|
||||
### Stable only (for now)
|
||||
### Stable release mirroring
|
||||
|
||||
We ship a single pinned upstream commit:
|
||||
- **Stable**: last known-good pin. This is the default.
|
||||
We ship one default package: `.#openclaw`.
|
||||
|
||||
The gateway tracks the newest upstream stable OpenClaw source release that satisfies the Nix package contract:
|
||||
- gateway builds on Linux and macOS
|
||||
- gateway starts and answers local health checks
|
||||
|
||||
The macOS app is pinned separately to the newest stable public `OpenClaw-*.zip` artifact. If upstream has not promoted desktop assets for the latest source release yet, `openclaw-app` may lag; that must not block Linux users or macOS gateway users from getting the latest source-built OpenClaw.
|
||||
|
||||
The Nix gate is deliberately package-focused. It does not make the full upstream Vitest suite a hard promotion gate; upstream owns source test health, while `nix-openclaw` verifies the source build, generated config options, package contents, smoke startup, module activation, and newest available macOS app artifact.
|
||||
|
||||
Outputs:
|
||||
```
|
||||
.#openclaw
|
||||
.#openclaw-gateway
|
||||
.#openclaw-app # Darwin only
|
||||
```
|
||||
|
||||
Pin lives in:
|
||||
`.#openclaw-gateway` and `.#openclaw-app` are component outputs for modules, CI, debugging, and advanced use. Start with `.#openclaw`.
|
||||
|
||||
Pins live in:
|
||||
- `nix/sources/openclaw-source.nix`
|
||||
- `nix/packages/openclaw-app.nix`
|
||||
|
||||
### Responsibilities (who owns what)
|
||||
|
||||
- **openclaw (upstream)**: source code, tests, releases.
|
||||
- **nix-openclaw**: Nix packaging, pins, CI builds.
|
||||
- **moltinators**: update cadence, smoke tests, promotion, rollout/rollback.
|
||||
- **release automation**: update cadence, smoke tests, promotion, rollout/rollback.
|
||||
|
||||
### Automated pipeline (no manual steps)
|
||||
### Automated pipeline
|
||||
|
||||
1) **moltinators updater** proposes a new stable pin.
|
||||
2) **GitHub Actions** builds the package on Linux + macOS and runs gateway tests on Linux.
|
||||
Build outputs are pushed to a binary cache (Cachix) so installs are fast and reproducible.
|
||||
It also validates the generated Nix config options against the upstream schema.
|
||||
3) **moltinators smoke test** runs against real Discord in `#moltinators-test`.
|
||||
4) If green → promote to stable.
|
||||
5) If red → keep current stable pin.
|
||||
|
||||
---
|
||||
|
||||
### CI & binary cache
|
||||
|
||||
This repo uses GitHub Actions for CI. For fast installs, we push build outputs to a **Cachix** binary cache.
|
||||
|
||||
Maintainers: configure these in GitHub repo settings (Actions):
|
||||
- Variable: `CACHIX_CACHE_NAME`
|
||||
- Secret: `CACHIX_AUTH_TOKEN`
|
||||
|
||||
Users: run `cachix use <cache-name>` (or configure `substituters` + public key manually) to get cache hits.
|
||||
1) Hourly **Yolo Update Pins** polls upstream stable OpenClaw releases.
|
||||
2) It selects the newest stable source release and newest stable public macOS app zip independently.
|
||||
3) Newer source releases that lack public macOS app assets are reported as app lag, not skipped.
|
||||
4) Yolo materializes the source pin from the newest source tag ref, updates the app asset pin from the newest public app zip, and regenerates config options from the selected source.
|
||||
5) Yolo validates that source/app pin set on the same Linux + macOS contract as repository `CI`.
|
||||
6) Only after both validations pass does yolo push one release-mirroring commit to `main`.
|
||||
|
||||
---
|
||||
|
||||
@ -744,10 +717,36 @@ home-manager switch --rollback # revert
|
||||
|
||||
| Package | Contents |
|
||||
| --- | --- |
|
||||
| `openclaw` (default) | macOS: gateway + app + tools · Linux: gateway + tools (headless) |
|
||||
| `openclaw-gateway` | Gateway CLI only |
|
||||
| `openclaw-tools` | Toolchain bundle (gateway helpers + CLIs) |
|
||||
| `openclaw-app` | macOS app only |
|
||||
| `openclaw` (default) | Canonical package. Exposes `openclaw`; keeps runtime tools internal. macOS also links the app. |
|
||||
| `openclaw-gateway` | Component output: gateway CLI/service only |
|
||||
| `openclaw-app` | Component output: macOS app only |
|
||||
|
||||
### Local memory
|
||||
|
||||
`openclaw` includes QMD internally as the supported local memory backend. It is not enabled automatically. Linux uses upstream `tobi/qmd`; Darwin uses the repaired `nix-openclaw-tools` package until upstream QMD is fixed there.
|
||||
|
||||
Opt in through normal OpenClaw config:
|
||||
|
||||
```nix
|
||||
programs.openclaw.config = {
|
||||
memory.backend = "qmd";
|
||||
};
|
||||
```
|
||||
|
||||
QMD stays inside the `openclaw` wrapper PATH, so users do not need to install a separate `qmd` command. The builtin `memorySearch.provider = "local"` path is an escape hatch for people who want to manage `node-llama-cpp` themselves; it is not the primary Nix-supported path.
|
||||
|
||||
Plugin CLIs are also kept on the OpenClaw runtime PATH by default, not on the user's login shell PATH. Set `programs.openclaw.exposePluginPackages = true` only when you explicitly want plugin CLIs in `home.packages`.
|
||||
|
||||
Optional model prewarming is also declarative:
|
||||
|
||||
```nix
|
||||
programs.openclaw.qmd.prewarmModels.enable = true;
|
||||
```
|
||||
|
||||
That runs a temporary QMD collection through `qmd update`, `qmd embed`, and
|
||||
`qmd query` during Home Manager activation, which warms the default embedding,
|
||||
expansion, and reranking models in the user's QMD cache. Expect about 2.25GB of
|
||||
cache use.
|
||||
|
||||
### What we manage vs what you manage
|
||||
|
||||
@ -756,26 +755,24 @@ home-manager switch --rollback # revert
|
||||
| Gateway binary | ✓ | |
|
||||
| macOS app | ✓ | |
|
||||
| Service (launchd/systemd) | ✓ | |
|
||||
| Tools (whisper, etc) | ✓ | |
|
||||
| Runtime tools and QMD | ✓ | |
|
||||
| Telegram bot token | | ✓ |
|
||||
| Anthropic API key | | ✓ |
|
||||
| Chat IDs | | ✓ |
|
||||
|
||||
### Included tools
|
||||
### Runtime tools
|
||||
|
||||
> **Platform note:** the toolchain is filtered per platform. macOS-only tools are skipped on Linux.
|
||||
|
||||
**Core**: nodejs, pnpm, git, curl, jq, python3, ffmpeg, ripgrep
|
||||
The default `openclaw` package uses these tools internally and does not expose them as separate user commands.
|
||||
|
||||
**First‑party tools** are sourced from `nix-steipete-tools` when available (currently aarch64‑darwin).
|
||||
**Core**: nodejs, pnpm, git, curl, jq, python3, ffmpeg, sox, ripgrep
|
||||
|
||||
**AI/ML**: openai-whisper, sag (TTS)
|
||||
**Local memory**: QMD (`memory.backend = "qmd"` opt-in)
|
||||
|
||||
**Media**: spotify-player, sox, camsnap
|
||||
**Default first-party tools** come from `nix-openclaw-tools`: gogcli (`gog`), goplaces, summarize, camsnap, sonoscli.
|
||||
|
||||
**macOS**: peekaboo, blucli
|
||||
|
||||
**Integrations**: gogcli, goplaces, wacli, bird, mcporter
|
||||
**Optional bundled plugins** add their own packages when enabled: discrawl, wacrawl, peekaboo, poltergeist, sag, imsg.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
# Agent‑First Guide
|
||||
|
||||
Single source of instructions: `templates/agent-first/steps.md`.
|
||||
|
||||
This guide intentionally contains **no code blocks**. All code lives in files under `templates/agent-first/`.
|
||||
@ -7,6 +7,24 @@ Purpose: extend OpenClaw capabilities without bloating core; ship tools + skills
|
||||
- **Not:** new transports/providers; model plumbing; secrets baked in; inline scripts or ad-hoc package-manager installs; a place for random config outside its scope.
|
||||
- Why not skills-only: skills without binaries can hallucinate capability. Plugins ground skills in real tools and deliver versioned, reproducible functionality.
|
||||
|
||||
## Two Plugin Classes
|
||||
|
||||
Nix capability plugins are the tool/skill/env bundles described below. They do not use OpenClaw's JavaScript plugin loader. They are the right shape for CLIs such as `goplaces`, `gog`, `qmd`, `xuezh`, `camsnap`, and `summarize`.
|
||||
|
||||
OpenClaw plugins are runtime plugin directories with `openclaw.plugin.json` plus built JavaScript loaded by the gateway. They include bundled upstream plugins, official external plugins from OpenClaw's catalog or ClawHub, and third-party plugins. In Nix-managed deployments, these should be immutable plugin roots, not runtime npm installs hidden in host config.
|
||||
|
||||
Current nix-openclaw `customPlugins` implements both sides of the contract: package binaries on the gateway PATH, materialize skills, create state dirs, validate env files, render optional tool settings, and wire declared OpenClaw plugin roots into `plugins.load.paths` with an explicit default `plugins.entries.<id>.enabled` value.
|
||||
|
||||
PR #81 (`fix: copy plugin manifests into dist/extensions`) was related but not the missing external-plugin feature. It fixed bundled upstream plugin manifests missing from the packaged gateway `dist/extensions/*/openclaw.plugin.json` tree. Current packaging already copies those manifests and checks them in `openclaw-package-contents`.
|
||||
|
||||
Package authors can bridge the existing Nix contract to OpenClaw plugins:
|
||||
|
||||
- Extend `openclawPlugin` with an optional plugin declaration, for example `plugins = [ { id = "openclaw-weixin"; path = "${pkg}/lib/openclaw/plugins/openclaw-weixin"; enabled = true; } ];`.
|
||||
- For each selected plugin artifact, append those paths to generated `plugins.load.paths`.
|
||||
- Add a default `plugins.entries.<id>.enabled` value. `enabled` defaults to true, but plugin authors can set `enabled = false` for roots that should be discoverable while disabled until the host supplies config. User config can still override either default.
|
||||
- Keep OpenClaw plugin config in `programs.openclaw.config` / `instances.<name>.config` so upstream schema validation remains the source of truth.
|
||||
- Add a fixture shaped like `openclaw-weixin` so `customPlugins = [{ source = ...; }]` proves both package/skill wiring and OpenClaw plugin load wiring.
|
||||
|
||||
## Interface Contract (reference implementation: nix-openclaw)
|
||||
Every plugin artifact exposes the same fields (flake output `openclawPlugin` today, but the shape is host-agnostic):
|
||||
|
||||
@ -14,7 +32,8 @@ Every plugin artifact exposes the same fields (flake output `openclawPlugin` tod
|
||||
openclawPlugin = {
|
||||
name = "summarize"; # unique; last-wins on collision
|
||||
skills = [ ./skills/summarize ]; # dirs containing SKILL.md
|
||||
packages = [ pkgs.summarize-cli ]; # binaries placed on PATH
|
||||
packages = [ pkgs.summarize-cli ]; # binaries placed on the OpenClaw runtime PATH
|
||||
plugins = [ ]; # optional OpenClaw plugin roots: { id, path, enabled ? true }
|
||||
needs = {
|
||||
stateDirs = [ ".config/summarize" ]; # created under $HOME
|
||||
requiredEnv = [ "SUMMARIZE_API_KEY" ]; # must point to files
|
||||
@ -27,18 +46,19 @@ Host responsibilities (what the runtime guarantees):
|
||||
- Install `packages`; prepend to PATH for the gateway wrapper.
|
||||
- Create `needs.stateDirs` under `$HOME`.
|
||||
- Fail fast if any `requiredEnv` is unset or points to a missing/empty file.
|
||||
- Copy/symlink `skills` into `workspace/skills/<name>/...`.
|
||||
- Copy/symlink each `skills` entry into `workspace/skills/<skill-dir-basename>/...`.
|
||||
- If host config provides `config.settings`, render it to `config.json` in the first `stateDir`.
|
||||
- Export `config.env` (plus required envs) into the gateway wrapper.
|
||||
- Add declared OpenClaw plugin roots to `plugins.load.paths`, and set `plugins.entries.<id>.enabled` from the plugin contract as a default.
|
||||
- Reject duplicate skill paths; duplicate plugin names: last entry wins.
|
||||
|
||||
### Host-side config shape
|
||||
When enabling a plugin, the host can supply:
|
||||
|
||||
```nix
|
||||
plugins = [
|
||||
programs.openclaw.customPlugins = [
|
||||
{
|
||||
source = "github:owner/repo";
|
||||
source = "github:owner/repo?rev=<commit>&narHash=<narHash>";
|
||||
config = {
|
||||
env = { KEY = "/run/agenix/key"; EXTRA = "/path/to/file"; };
|
||||
settings = { foo = "bar"; retries = 3; };
|
||||
@ -51,23 +71,43 @@ plugins = [
|
||||
- `config.settings`: JSON-rendered into `config.json` inside the first `stateDir`.
|
||||
- Invariant: providing `settings` requires at least one `stateDir`.
|
||||
|
||||
Do not add raw npm package names to host config for the batteries-included path. Curated plugins packaged by this repo or `nix-openclaw-tools` should be exposed through package/check outputs so Garnix caches them.
|
||||
|
||||
OpenClaw native npm plugins use the same host list with an OpenClaw-style source:
|
||||
|
||||
```nix
|
||||
programs.openclaw.customPlugins = [
|
||||
{
|
||||
source = "npm:@scope/openclaw-plugin@1.2.3";
|
||||
id = "openclaw-plugin";
|
||||
hash = lib.fakeHash; # replace with the sha256 Nix reports
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
- `source`: currently supports registry npm specs with an explicit `npm:` prefix.
|
||||
- `id`: required because the Home Manager module must enable the plugin at eval time without importing the built JavaScript package.
|
||||
- `hash`: recursive output hash for the immutable plugin root; leave as `lib.fakeHash` to have Nix report the expected hash, then commit that value.
|
||||
- Runtime plugin config belongs in `programs.openclaw.config.plugins.entries.<id>.config`, not in `customPlugins.config`.
|
||||
- The module adds the built root to `plugins.load.paths` and writes a default `plugins.entries.<id>.enabled` value. OpenClaw owns runtime loading after that.
|
||||
|
||||
Curated npm plugins can be added to this repo or `nix-openclaw-tools` so Garnix caches them. Arbitrary user npm specs are still deterministic Nix artifacts, but this repo's cache cannot cover every user's private plugin choice. The user's local store or configured binary cache reuses the artifact until the source or hash changes. OpenClaw must not reinstall it on every gateway start.
|
||||
|
||||
## Dev workflow (fast iteration)
|
||||
- Worktree: build and test plugins outside the core repo; point OpenClaw at a local path source (e.g., `source = "path:/Users/you/code/my-plugin"`).
|
||||
- Worktree: build and test plugins outside the core repo; point OpenClaw at a local path source during impure local dev (e.g., `source = "path:/Users/you/code/my-plugin"`). Committed config uses pinned refs.
|
||||
- Rebuild loop: change plugin → `home-manager switch` (or host-equivalent) → gateway restarts with new PATH/skills/config; no manual copying.
|
||||
- Name collisions: use the same plugin `name` to override a pinned version (last entry wins); keep unique names otherwise to avoid surprise overrides.
|
||||
- Skills placement: skills land under `~/.openclaw*/workspace/skills/<plugin>/...` so you can inspect quickly; delete the workspace to fully reset cached skills.
|
||||
- Skills placement: skills land under `~/.openclaw*/workspace/skills/<skill-dir-basename>/...` so you can inspect quickly; delete the workspace to fully reset cached skills.
|
||||
- Env guardrails: required env vars must point to files (non-empty) or the activation fails—supply temp files during dev to exercise the checks.
|
||||
- Settings JSON: inspect the rendered `config.json` in the first `stateDir` to confirm schema and defaults before committing.
|
||||
|
||||
## Examples
|
||||
|
||||
### Minimal capability plugin (first-party `summarize`)
|
||||
### Minimal capability plugin (bundled `summarize`)
|
||||
Enable (host side):
|
||||
|
||||
```nix
|
||||
programs.openclaw.instances.default.plugins = [
|
||||
{ source = "github:openclaw/nix-steipete-tools?dir=tools/summarize"; }
|
||||
];
|
||||
programs.openclaw.bundledPlugins.summarize.enable = true;
|
||||
```
|
||||
|
||||
Plugin contract (inside the plugin repo):
|
||||
@ -85,9 +125,9 @@ openclawPlugin = {
|
||||
Enable (host side):
|
||||
|
||||
```nix
|
||||
programs.openclaw.instances.default.plugins = [
|
||||
programs.openclaw.customPlugins = [
|
||||
{
|
||||
source = "github:joshp123/xuezh";
|
||||
source = "github:joshp123/xuezh?rev=<commit>&narHash=<narHash>";
|
||||
config = {
|
||||
env = {
|
||||
# Required envs (guarded as files):
|
||||
@ -131,8 +171,9 @@ openclawPlugin = {
|
||||
Host behavior: creates `~/.config/xuezh/config.json` from `settings`; exports both envs; fails if the pointed files are missing/empty.
|
||||
|
||||
## Bundled Plugin Set (current)
|
||||
- summarize, peekaboo, oracle, poltergeist, sag, camsnap, gogcli, goplaces, bird, sonoscli, imsg.
|
||||
- Each follows the same contract: packages + skills; env/state declared via `needs`; enabled via config toggle; sources pinned (see nix-openclaw bundledPlugins mapping).
|
||||
- summarize, discrawl, wacrawl, peekaboo, poltergeist, sag, camsnap, gogcli, goplaces, sonoscli, imsg.
|
||||
- Source of truth: `nix/modules/home-manager/openclaw/plugin-catalog.nix`.
|
||||
- Each follows the same contract: packages + skills; env/state declared via `needs`; enabled via config toggle; sources pinned via the bundled plugin catalog.
|
||||
|
||||
## Authoring Rules
|
||||
- Keep CLIs configurable via env; honor XDG paths; no inline scripts.
|
||||
|
||||
@ -26,7 +26,7 @@ This RFC is only about:
|
||||
- A generic, end‑user Nix setup that lives outside any personal config repo.
|
||||
|
||||
This RFC is explicitly **not** about:
|
||||
- Josh’s personal `nixos-config` or any private machine configuration.
|
||||
- Any personal system configuration repo or private machine configuration.
|
||||
- Editing or publishing personal settings, tokens, or machine‑specific modules.
|
||||
|
||||
## 2) Goals / Non‑goals
|
||||
|
||||
@ -50,7 +50,7 @@ plugin/
|
||||
That's it. No registry. No central authority. Point at a repo, get a plugin.
|
||||
|
||||
One install gives you:
|
||||
- **Binary** on PATH (built from source, pinned version)
|
||||
- **Binary** on the OpenClaw runtime PATH (built from source, pinned version)
|
||||
- **Skills** in workspace (agent knows how to use it)
|
||||
- **Config** validated (missing env = install fails, not runtime error)
|
||||
- **State dirs** created (plugin has a home)
|
||||
@ -203,7 +203,7 @@ voicecall status --call-id abc123 # Check for responses
|
||||
4. **Create state dirs** — from manifest
|
||||
5. **Add `openclaw plugins` CLI** — list, enable, disable, info
|
||||
|
||||
That's it. No dynamic code loading, no TypeBox registration, no RPC handlers. Just: find plugins, validate their needs, put binaries on PATH, copy skills to workspace.
|
||||
That's it. No dynamic code loading, no TypeBox registration, no RPC handlers. Just: find plugins, validate their needs, put binaries on the OpenClaw runtime PATH, copy skills to workspace.
|
||||
|
||||
### How nix-openclaw fits in
|
||||
|
||||
@ -217,7 +217,7 @@ programs.openclaw.customPlugins = [
|
||||
{ source = "github:joshp123/padel-cli"; }
|
||||
|
||||
# Local dev: point at directory
|
||||
{ source = "path:/Users/josh/code/my-plugin"; }
|
||||
{ source = "path:/home/user/code/my-plugin"; }
|
||||
];
|
||||
|
||||
# Or enable bundled plugins (pinned in nix-openclaw):
|
||||
@ -304,17 +304,18 @@ Install wires up Twilio creds. Binary handles webhook server. Skill teaches agen
|
||||
|
||||
## The Plugin Ecosystem Vision
|
||||
|
||||
**First-party plugins** already exist — see [nix-steipete-tools](https://github.com/openclaw/nix-steipete-tools/tree/main/tools):
|
||||
**First-party plugins** already exist — see [nix-openclaw-tools](https://github.com/openclaw/nix-openclaw-tools/tree/main/tools):
|
||||
- `summarize` — YouTube/article summarization
|
||||
- `oracle` — second-model review
|
||||
- `discrawl` — Discord archive/search
|
||||
- `wacrawl` — WhatsApp archive/search
|
||||
- `peekaboo` — screenshot capture
|
||||
- `camsnap` — webcam capture
|
||||
- `poltergeist` — browser automation
|
||||
- `sag` — web search
|
||||
- `bird` — Twitter/X integration
|
||||
- `poltergeist` — file watching and automation
|
||||
- `sag` — text-to-speech
|
||||
- `sonoscli` — Sonos control
|
||||
- `imsg` — iMessage integration
|
||||
- `gogcli` — Google Calendar
|
||||
- `goplaces` — Google Places
|
||||
|
||||
All follow the same contract. All pinned in nix-openclaw. Enable with one line:
|
||||
```nix
|
||||
|
||||
@ -6,11 +6,18 @@
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
in {
|
||||
in
|
||||
{
|
||||
packages.default = pkgs.buildGoModule {
|
||||
pname = "hello-world";
|
||||
version = "0.1.0";
|
||||
@ -27,8 +34,8 @@
|
||||
skills = [ ./skills/hello-world ];
|
||||
packages = [ self.packages.${system}.default ];
|
||||
needs = {
|
||||
stateDirs = [];
|
||||
requiredEnv = [];
|
||||
stateDirs = [ ];
|
||||
requiredEnv = [ ];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
41
flake.lock
generated
41
flake.lock
generated
@ -38,21 +38,21 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-steipete-tools": {
|
||||
"nix-openclaw-tools": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1770240566,
|
||||
"narHash": "sha256-fY8t41kMSHu2ovf89mIdvC7vkceroCwKxw/MKVn4rsE=",
|
||||
"lastModified": 1778060041,
|
||||
"narHash": "sha256-tXWkN1VnwFG8XlRqW/e7VwbKnUfyU9tB7YDm9QHJXTY=",
|
||||
"owner": "openclaw",
|
||||
"repo": "nix-steipete-tools",
|
||||
"rev": "983210e3b6e9285780e87f48ce9354b51a270e95",
|
||||
"repo": "nix-openclaw-tools",
|
||||
"rev": "4c1cee3c7eaf68f9de0f756be1484534f5bb5f34",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "openclaw",
|
||||
"repo": "nix-steipete-tools",
|
||||
"repo": "nix-openclaw-tools",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
@ -88,12 +88,37 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"qmd": {
|
||||
"inputs": {
|
||||
"flake-utils": [
|
||||
"flake-utils"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1775429264,
|
||||
"narHash": "sha256-bqIVaNRTa8H5vrw3RwsD7QdtTa0xNvRuEVzlzE1hIBQ=",
|
||||
"owner": "tobi",
|
||||
"repo": "qmd",
|
||||
"rev": "65cd1b3fd02891d1ee0eefa751620918664fa321",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "tobi",
|
||||
"ref": "v2.1.0",
|
||||
"repo": "qmd",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"home-manager": "home-manager",
|
||||
"nix-steipete-tools": "nix-steipete-tools",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
"nix-openclaw-tools": "nix-openclaw-tools",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"qmd": "qmd"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
|
||||
156
flake.nix
156
flake.nix
@ -13,70 +13,164 @@
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
home-manager.url = "github:nix-community/home-manager";
|
||||
home-manager.inputs.nixpkgs.follows = "nixpkgs";
|
||||
nix-steipete-tools.url = "github:openclaw/nix-steipete-tools";
|
||||
nix-openclaw-tools.url = "github:openclaw/nix-openclaw-tools";
|
||||
qmd.url = "github:tobi/qmd/v2.1.0";
|
||||
qmd.inputs.flake-utils.follows = "flake-utils";
|
||||
qmd.inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, home-manager, nix-steipete-tools }:
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
home-manager,
|
||||
nix-openclaw-tools,
|
||||
qmd,
|
||||
}:
|
||||
let
|
||||
overlay = import ./nix/overlay.nix;
|
||||
openclawToolPkgsFor =
|
||||
system:
|
||||
if nix-openclaw-tools ? packages && builtins.hasAttr system nix-openclaw-tools.packages then
|
||||
nix-openclaw-tools.packages.${system}
|
||||
else
|
||||
{ };
|
||||
qmdPkgsFor =
|
||||
system:
|
||||
if qmd ? packages && builtins.hasAttr system qmd.packages then qmd.packages.${system} else { };
|
||||
overlay =
|
||||
final: prev:
|
||||
import ./nix/overlay.nix {
|
||||
openclawToolPkgs = openclawToolPkgsFor prev.stdenv.hostPlatform.system;
|
||||
qmdPkgs = qmdPkgsFor prev.stdenv.hostPlatform.system;
|
||||
} final prev;
|
||||
sourceInfoStable = import ./nix/sources/openclaw-source.nix;
|
||||
systems = [ "x86_64-linux" "aarch64-darwin" ];
|
||||
sourceInfoDogfood = import ./nix/sources/openclaw-dogfood-source.nix;
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-darwin"
|
||||
];
|
||||
in
|
||||
flake-utils.lib.eachSystem systems (system:
|
||||
flake-utils.lib.eachSystem systems (
|
||||
system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ overlay ];
|
||||
};
|
||||
steipetePkgs = if nix-steipete-tools ? packages && builtins.hasAttr system nix-steipete-tools.packages
|
||||
then nix-steipete-tools.packages.${system}
|
||||
else {};
|
||||
openclawToolPkgs = openclawToolPkgsFor system;
|
||||
qmdPkgs = qmdPkgsFor system;
|
||||
qmdPackage =
|
||||
if pkgs.stdenv.hostPlatform.isDarwin then
|
||||
openclawToolPkgs.qmd or null
|
||||
else
|
||||
qmdPkgs.qmd or qmdPkgs.default or null;
|
||||
packageSetStable = import ./nix/packages {
|
||||
pkgs = pkgs;
|
||||
sourceInfo = sourceInfoStable;
|
||||
steipetePkgs = steipetePkgs;
|
||||
openclawToolPkgs = openclawToolPkgs;
|
||||
inherit qmdPackage;
|
||||
};
|
||||
packageSetDogfood = import ./nix/packages {
|
||||
pkgs = pkgs;
|
||||
sourceInfo = sourceInfoDogfood;
|
||||
openclawToolPkgs = openclawToolPkgs;
|
||||
inherit qmdPackage;
|
||||
};
|
||||
in
|
||||
{
|
||||
formatter = pkgs.nixfmt-tree.override {
|
||||
settings = {
|
||||
global.excludes = [ "nix/generated/openclaw-config-options.nix" ];
|
||||
};
|
||||
};
|
||||
|
||||
packages = packageSetStable // {
|
||||
default = packageSetStable.openclaw;
|
||||
openclaw-dogfood = packageSetDogfood.openclaw;
|
||||
openclaw-gateway-dogfood = packageSetDogfood.openclaw-gateway;
|
||||
};
|
||||
|
||||
apps = {
|
||||
openclaw = flake-utils.lib.mkApp { drv = packageSetStable.openclaw-gateway; };
|
||||
openclaw = flake-utils.lib.mkApp { drv = packageSetStable.openclaw; };
|
||||
};
|
||||
|
||||
checks = {
|
||||
gateway = packageSetStable.openclaw-gateway;
|
||||
package-contents = pkgs.callPackage ./nix/checks/openclaw-package-contents.nix {
|
||||
openclawGateway = packageSetStable.openclaw-gateway;
|
||||
checks =
|
||||
let
|
||||
baseChecks = {
|
||||
gateway = packageSetStable.openclaw-gateway;
|
||||
bin-surface = pkgs.callPackage ./nix/checks/openclaw-bin-surface.nix {
|
||||
openclawPackage = packageSetStable.openclaw;
|
||||
};
|
||||
package-contents = pkgs.callPackage ./nix/checks/openclaw-package-contents.nix {
|
||||
openclawGateway = packageSetStable.openclaw-gateway;
|
||||
};
|
||||
package-contents-dogfood = pkgs.callPackage ./nix/checks/openclaw-package-contents.nix {
|
||||
openclawGateway = packageSetDogfood.openclaw-gateway;
|
||||
};
|
||||
default-instance = pkgs.callPackage ./nix/checks/openclaw-default-instance.nix { };
|
||||
config-validity = pkgs.callPackage ./nix/checks/openclaw-config-validity.nix {
|
||||
openclawGateway = packageSetStable.openclaw-gateway;
|
||||
};
|
||||
gateway-smoke = pkgs.callPackage ./nix/checks/openclaw-gateway-smoke.nix {
|
||||
openclawGateway = packageSetStable.openclaw-gateway;
|
||||
};
|
||||
}
|
||||
// pkgs.lib.optionalAttrs (qmdPackage != null) {
|
||||
qmd-runtime = pkgs.callPackage ./nix/checks/openclaw-qmd-runtime.nix {
|
||||
openclawPackage = packageSetStable.openclaw;
|
||||
inherit qmdPackage;
|
||||
};
|
||||
}
|
||||
// (
|
||||
if pkgs.stdenv.hostPlatform.isLinux then
|
||||
let
|
||||
sourceChecks = pkgs.callPackage ./nix/checks/openclaw-source-checks.nix {
|
||||
sourceInfo = sourceInfoStable;
|
||||
openclawGateway = packageSetStable.openclaw-gateway;
|
||||
};
|
||||
in
|
||||
{
|
||||
config-options = sourceChecks;
|
||||
source-checks = sourceChecks;
|
||||
hm-activation = import ./nix/checks/openclaw-hm-activation.nix {
|
||||
inherit pkgs home-manager;
|
||||
};
|
||||
}
|
||||
else
|
||||
{ }
|
||||
);
|
||||
in
|
||||
baseChecks
|
||||
// {
|
||||
# CI aggregator: build the expensive gateway once, then run all checks in the
|
||||
# same build machine/store to avoid cache-miss races between parallel jobs.
|
||||
ci = pkgs.symlinkJoin {
|
||||
name = "nix-openclaw-ci";
|
||||
paths = [
|
||||
packageSetStable.openclaw
|
||||
packageSetStable.openclaw-gateway
|
||||
]
|
||||
++ (builtins.attrValues baseChecks);
|
||||
};
|
||||
};
|
||||
config-validity = pkgs.callPackage ./nix/checks/openclaw-config-validity.nix {
|
||||
openclawGateway = packageSetStable.openclaw-gateway;
|
||||
};
|
||||
} // (if pkgs.stdenv.hostPlatform.isLinux then {
|
||||
gateway-tests = pkgs.callPackage ./nix/checks/openclaw-gateway-tests.nix {
|
||||
sourceInfo = sourceInfoStable;
|
||||
};
|
||||
config-options = pkgs.callPackage ./nix/checks/openclaw-config-options.nix {
|
||||
sourceInfo = sourceInfoStable;
|
||||
};
|
||||
default-instance = pkgs.callPackage ./nix/checks/openclaw-default-instance.nix {};
|
||||
hm-activation = import ./nix/checks/openclaw-hm-activation.nix {
|
||||
inherit pkgs home-manager;
|
||||
};
|
||||
} else {});
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = [
|
||||
pkgs.git
|
||||
pkgs.nixfmt-rfc-style
|
||||
pkgs.nixfmt-tree
|
||||
pkgs.nil
|
||||
];
|
||||
};
|
||||
}
|
||||
) // {
|
||||
)
|
||||
// {
|
||||
overlays.default = overlay;
|
||||
templates.agent-first = {
|
||||
path = ./templates/agent-first;
|
||||
description = "Agent-first Home Manager setup for OpenClaw through Nix.";
|
||||
};
|
||||
nixosModules.openclaw-gateway = import ./nix/modules/nixos/openclaw-gateway.nix;
|
||||
homeManagerModules.openclaw = import ./nix/modules/home-manager/openclaw.nix;
|
||||
darwinModules.openclaw = import ./nix/modules/darwin/openclaw.nix;
|
||||
};
|
||||
|
||||
15
garnix.yaml
Normal file
15
garnix.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
builds:
|
||||
include:
|
||||
# CI aggregators prove package contracts.
|
||||
- "checks.aarch64-darwin.ci"
|
||||
- "checks.x86_64-linux.ci"
|
||||
# User-facing/component packages must also be top-level Garnix artifacts,
|
||||
# otherwise downstream machines can see green CI but miss the binary cache.
|
||||
- "packages.aarch64-darwin.openclaw"
|
||||
- "packages.aarch64-darwin.openclaw-dogfood"
|
||||
- "packages.aarch64-darwin.openclaw-gateway"
|
||||
- "packages.aarch64-darwin.openclaw-gateway-dogfood"
|
||||
- "packages.x86_64-linux.openclaw"
|
||||
- "packages.x86_64-linux.openclaw-dogfood"
|
||||
- "packages.x86_64-linux.openclaw-gateway"
|
||||
- "packages.x86_64-linux.openclaw-gateway-dogfood"
|
||||
27
maintainers/AGENTS.md
Normal file
27
maintainers/AGENTS.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Maintainer Agent Guide
|
||||
|
||||
This directory is public maintainer guidance for agents working on `nix-openclaw`.
|
||||
It is not consumer setup documentation and must not contain private deployment state.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Keep consumer onboarding in `README.md`, templates, and module docs.
|
||||
- Keep private deployments, bots, hosts, local worktrees, tokens, and personal automation details out of this repo.
|
||||
- If a private deployment exposes a public packaging bug, fix the public package here and keep deployment-specific repair elsewhere.
|
||||
- Treat `README.md` as the product direction source of truth.
|
||||
|
||||
## Read Order
|
||||
|
||||
1. `packaging.md` for Nix-owned package invariants.
|
||||
2. `release-policy.md` for the split-track publishing invariant.
|
||||
3. `automation.md` for the maintainer repair loop.
|
||||
4. `gates.md` for verification and CI expectations.
|
||||
5. Root `AGENTS.md` for repo-wide rules.
|
||||
|
||||
## Maintainer Workflow
|
||||
|
||||
- Work on `main` by default and push small, surgical commits directly to `main` when maintainer policy allows it.
|
||||
- Use branches only when a maintainer asks, direct push is blocked, or a disposable local experiment is needed.
|
||||
- For multi-issue work, commit and push one issue at a time, then verify GitHub Actions for that pushed commit before continuing.
|
||||
- Do not leave completed maintainer work parked on an agent branch.
|
||||
- No force push. No weakening package checks just to get green.
|
||||
47
maintainers/automation.md
Normal file
47
maintainers/automation.md
Normal file
@ -0,0 +1,47 @@
|
||||
# Maintainer Automation
|
||||
|
||||
Maintainer automation is an agentic repair loop for the public packaging pipeline. It is not a second release pipeline and not a private deployment monitor.
|
||||
|
||||
## Daily Objective
|
||||
|
||||
Answer first:
|
||||
|
||||
```text
|
||||
Does nix-openclaw publish the latest upstream version for both supported tracks?
|
||||
```
|
||||
|
||||
Answer `YES` only when:
|
||||
|
||||
- `openclaw-gateway` matches the newest stable upstream source release.
|
||||
- `openclaw-app` matches the newest stable upstream release with a published public `OpenClaw-*.zip`.
|
||||
|
||||
If both tracks are current and yolo/CI are healthy, stop with a short CTO-level report:
|
||||
|
||||
- current gateway
|
||||
- latest upstream gateway
|
||||
- current app
|
||||
- latest published app
|
||||
- whether action was needed
|
||||
|
||||
## Repair Loop
|
||||
|
||||
If the desired state is not true, keep working until it is true or until the exact blocker is proven.
|
||||
|
||||
Diagnose across:
|
||||
|
||||
- upstream release data
|
||||
- yolo selection
|
||||
- pin materialization
|
||||
- generated config options
|
||||
- package builds
|
||||
- smoke checks
|
||||
- module activation
|
||||
- workflow behavior
|
||||
- caches
|
||||
- CI runner failures
|
||||
|
||||
Do not ask for a repair strategy when the desired state is clear.
|
||||
|
||||
If the fix belongs in `nix-openclaw`, edit the repo, self-review the diff until there are no actionable findings, run the relevant targeted checks plus the full gate, commit directly to `main`, push directly to `main`, and verify GitHub Actions on the pushed commit.
|
||||
|
||||
If upstream has not published public macOS app assets, call that out directly, keep the app pin on the newest public zip, keep packaging the latest stable source-built gateway, and repair `nix-openclaw` only if it fails to do that.
|
||||
22
maintainers/gates.md
Normal file
22
maintainers/gates.md
Normal file
@ -0,0 +1,22 @@
|
||||
# Gates
|
||||
|
||||
Use targeted checks while debugging, then run the full relevant gate before handoff.
|
||||
|
||||
## Required Checks
|
||||
|
||||
- `scripts/check-flake-lock-owners.sh`
|
||||
- selector tests
|
||||
- updater shell syntax
|
||||
- workflow YAML parse
|
||||
- `nix flake show --accept-flake-config`
|
||||
- Linux CI aggregator
|
||||
- Darwin CI aggregator when available
|
||||
- `scripts/hm-activation-macos.sh` when a macOS runner is available
|
||||
|
||||
## CI Verification
|
||||
|
||||
After pushing maintainer fixes, verify the GitHub Actions run for the pushed commit.
|
||||
|
||||
Never say you will keep polling unless a blocking poll is already running. If reporting a poll, name the active run or local polling session.
|
||||
|
||||
If CI fails, inspect the failing run, classify the failure, fix what belongs to `nix-openclaw`, and rerun until green or until the exact external blocker is proven.
|
||||
46
maintainers/packaging.md
Normal file
46
maintainers/packaging.md
Normal file
@ -0,0 +1,46 @@
|
||||
# Packaging Invariants
|
||||
|
||||
This repo ships a working Nix package for OpenClaw users, not just a pin mirror.
|
||||
|
||||
## Product Surface
|
||||
|
||||
- The user-facing package is `openclaw`.
|
||||
- `openclaw-gateway` is the source-built runnable gateway for Linux and macOS.
|
||||
- `openclaw-app` is the Darwin-only desktop app from upstream's public app artifact.
|
||||
- Component outputs exist for modules, checks, and debugging. They are not separate product tracks.
|
||||
- `openclaw-dogfood` and `openclaw-gateway-dogfood` are temporary maintainer
|
||||
artifacts for testing a specific upstream commit before the next stable
|
||||
release. They must not become the documented consumer default.
|
||||
- Do not split the repo into separate desktop and server tracks.
|
||||
|
||||
## Nix Ownership
|
||||
|
||||
- OpenClaw owns product and runtime behavior.
|
||||
- `nix-openclaw` owns batteries-included Nix packaging, Home Manager/NixOS/Darwin modules, runtime PATH/env injection, launchd/systemd wiring, and package-contract checks.
|
||||
- `nix-openclaw-tools` owns packaging OpenClaw-adjacent CLI tools and plugin metadata. Consume it here; do not duplicate its package definitions here.
|
||||
- Downstream system repos should only choose hosts, secrets, accounts, and enabled plugins. If downstream needs bespoke scripts to make a plugin or harness work, prefer fixing this repo or `nix-openclaw-tools`.
|
||||
- Nix mode means Nix owns `openclaw.json`.
|
||||
- Runtime config mutation belongs upstream in OpenClaw. Downstream patches here must be small, temporary, and removed after the pinned upstream release contains the fix.
|
||||
- Generated config options come from the upstream core schema.
|
||||
- Plugin-owned extension surfaces, such as `channels.<plugin-id>`, must remain accepted by the Home Manager module even when core does not type every plugin key.
|
||||
- Runtime tool injection belongs here. If a plugin or battery is enabled, the active OpenClaw harness must see its CLI tools and required environment without asking downstream to expose those tools globally on the user PATH.
|
||||
- OpenClaw plugin roots belong here too. The Home Manager module consumes `openclawPlugin.plugins` declarations from plugin flakes and writes `plugins.load.paths` plus default `plugins.entries.<id>.enabled` values into the generated config.
|
||||
- Raw npm/ClawHub plugin names are not batteries-included deployment config. Curated plugins packaged here must be exposed through packages/checks so CI/Garnix caches them. Arbitrary user specs need a deterministic lock/hash-backed Nix builder so Nix reuses the user's store/cache and only rebuilds when the spec, lock, or hash changes.
|
||||
|
||||
## Build Contract
|
||||
|
||||
- The gateway package must include Control UI assets.
|
||||
- No inline scripts or inline file contents in Nix code. Use repo scripts and explicit file paths.
|
||||
- Keep runtime tools internal to the `openclaw` wrapper unless they are intentionally part of the public package surface.
|
||||
- QMD is the Nix-supported batteries-included local memory backend. Keep `qmd` internal to the `openclaw` wrapper PATH; users opt in with upstream config.
|
||||
- ACPX is the first bundled OpenClaw plugin proof. It is consumed from OpenClaw's built `dist-runtime/extensions/acpx` tree, not installed or repaired by npm at runtime.
|
||||
- Keep files under 400 lines unless a maintainer explicitly accepts the larger file.
|
||||
|
||||
## Investigations
|
||||
|
||||
### mcporter and QMD
|
||||
|
||||
- `mcporter` is an OpenClaw-owned optional MCP/CLI bridge, not a QMD requirement.
|
||||
- OpenClaw defaults to direct `qmd` CLI execution. Keep that as the Nix-supported baseline until measured startup or per-query overhead proves otherwise.
|
||||
- Package `mcporter` in `nix-openclaw-tools` as an optional tool when needed, but do not add it to the default `openclaw` runtime PATH just because QMD is bundled.
|
||||
- If `memory.qmd.mcporter.enabled = true`, nix-openclaw should make `mcporter` visible to that instance and require the matching mcporter server config for `qmd mcp`.
|
||||
26
maintainers/release-policy.md
Normal file
26
maintainers/release-policy.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Release Policy
|
||||
|
||||
`nix-openclaw` publishes one user-facing package, `openclaw`, with component outputs for maintainers and modules.
|
||||
|
||||
## Desired State
|
||||
|
||||
- `openclaw-gateway` tracks the newest stable upstream OpenClaw source release that satisfies the Nix package contract.
|
||||
- `openclaw-app` tracks the newest stable upstream release that has a published public `OpenClaw-*.zip` app artifact.
|
||||
- These tracks are independent. Source and app versions may differ.
|
||||
|
||||
## Non-Negotiables
|
||||
|
||||
- Do not hold back the source-built gateway because a newer source release lacks public macOS app assets.
|
||||
- Do not treat source/app version mismatch as a failure.
|
||||
- Do not make upstream's full Vitest suite a promotion gate; upstream owns source test health.
|
||||
- Do verify the Nix-owned package contract: source build, generated config options, package contents, gateway smoke startup, module activation, and newest available public macOS app artifact.
|
||||
- Do prefer the upstream `.zip` app artifact for `openclaw-app`, but verify the unpacked contents contain an `.app`.
|
||||
|
||||
## Freshness Check
|
||||
|
||||
The package is fresh only when both are true:
|
||||
|
||||
- `nix/sources/openclaw-source.nix` matches GitHub's newest stable OpenClaw source tag.
|
||||
- `nix/packages/openclaw-app.nix` matches the newest stable public `OpenClaw-*.zip` app artifact.
|
||||
|
||||
If newer stable source releases lack public app assets, report that as an upstream app publishing miss and keep the app pin on the newest public zip.
|
||||
22
nix/checks/openclaw-bin-surface.nix
Normal file
22
nix/checks/openclaw-bin-surface.nix
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
lib,
|
||||
stdenvNoCC,
|
||||
openclawPackage,
|
||||
}:
|
||||
|
||||
stdenvNoCC.mkDerivation {
|
||||
pname = "openclaw-bin-surface";
|
||||
version = lib.getVersion openclawPackage;
|
||||
|
||||
dontUnpack = true;
|
||||
dontConfigure = true;
|
||||
dontBuild = true;
|
||||
|
||||
env = {
|
||||
OPENCLAW_PACKAGE = openclawPackage;
|
||||
};
|
||||
|
||||
doCheck = true;
|
||||
checkPhase = "${../scripts/check-openclaw-bin-surface.sh}";
|
||||
installPhase = "${../scripts/empty-install.sh}";
|
||||
}
|
||||
@ -1,174 +0,0 @@
|
||||
{ lib
|
||||
, pkgs
|
||||
, stdenv
|
||||
, fetchFromGitHub
|
||||
, fetchurl
|
||||
, nodejs_22
|
||||
, pnpm_10
|
||||
, pkg-config
|
||||
, jq
|
||||
, python3
|
||||
, node-gyp
|
||||
, git
|
||||
, zstd
|
||||
, sourceInfo
|
||||
, pnpmDepsHash ? (sourceInfo.pnpmDepsHash or null)
|
||||
}:
|
||||
|
||||
let
|
||||
linuxFirstParty = [
|
||||
"summarize"
|
||||
"gogcli"
|
||||
"goplaces"
|
||||
"camsnap"
|
||||
"sonoscli"
|
||||
"sag"
|
||||
"oracle"
|
||||
];
|
||||
enableFirstParty = name: stdenv.hostPlatform.isDarwin || lib.elem name linuxFirstParty;
|
||||
|
||||
stubModule = { lib, ... }: {
|
||||
options = {
|
||||
assertions = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.attrs;
|
||||
default = [];
|
||||
};
|
||||
|
||||
home.homeDirectory = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "/tmp";
|
||||
};
|
||||
|
||||
home.packages = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.anything;
|
||||
default = [];
|
||||
};
|
||||
|
||||
home.file = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = {};
|
||||
};
|
||||
|
||||
home.activation = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = {};
|
||||
};
|
||||
|
||||
launchd.agents = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = {};
|
||||
};
|
||||
|
||||
systemd.user.services = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = {};
|
||||
};
|
||||
|
||||
programs.git.enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
};
|
||||
|
||||
lib = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = {};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
pluginEval = lib.evalModules {
|
||||
modules = [
|
||||
stubModule
|
||||
../modules/home-manager/openclaw.nix
|
||||
({ lib, options, ... }: {
|
||||
config = {
|
||||
home.homeDirectory = "/tmp";
|
||||
programs.git.enable = false;
|
||||
lib.file.mkOutOfStoreSymlink = path: path;
|
||||
programs.openclaw = {
|
||||
enable = true;
|
||||
launchd.enable = false;
|
||||
systemd.enable = false;
|
||||
instances.default = {};
|
||||
bundledPlugins = lib.mapAttrs (name: _: { enable = enableFirstParty name; }) options.programs.openclaw.bundledPlugins;
|
||||
};
|
||||
};
|
||||
})
|
||||
];
|
||||
specialArgs = { inherit pkgs; };
|
||||
};
|
||||
|
||||
pluginEvalKey = builtins.deepSeq pluginEval.config.assertions "ok";
|
||||
|
||||
sourceFetch = lib.removeAttrs sourceInfo [ "pnpmDepsHash" ];
|
||||
pnpmPlatform = if stdenv.hostPlatform.isDarwin then "darwin" else "linux";
|
||||
pnpmArch = if stdenv.hostPlatform.isAarch64 then "arm64" else "x64";
|
||||
nodeAddonApi = stdenv.mkDerivation {
|
||||
pname = "node-addon-api";
|
||||
version = "8.5.0";
|
||||
src = fetchurl {
|
||||
url = "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz";
|
||||
hash = "sha256-0S8HyBYig7YhNVGFXx2o2sFiMxN0YpgwteZA8TDweRA=";
|
||||
};
|
||||
dontConfigure = true;
|
||||
dontBuild = true;
|
||||
installPhase = "${../scripts/node-addon-api-install.sh}";
|
||||
};
|
||||
|
||||
in
|
||||
|
||||
stdenv.mkDerivation (finalAttrs: {
|
||||
pname = "openclaw-config-options";
|
||||
version = "2026.1.8-2";
|
||||
|
||||
src = fetchFromGitHub sourceFetch;
|
||||
|
||||
pnpmDeps = pnpm_10.fetchDeps {
|
||||
inherit (finalAttrs) pname version src;
|
||||
hash = if pnpmDepsHash != null
|
||||
then pnpmDepsHash
|
||||
else lib.fakeHash;
|
||||
fetcherVersion = 2;
|
||||
npm_config_arch = pnpmArch;
|
||||
npm_config_platform = pnpmPlatform;
|
||||
nativeBuildInputs = [ git ];
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
nodejs_22
|
||||
pnpm_10
|
||||
pkg-config
|
||||
jq
|
||||
python3
|
||||
node-gyp
|
||||
zstd
|
||||
];
|
||||
|
||||
env = {
|
||||
npm_config_arch = pnpmArch;
|
||||
npm_config_platform = pnpmPlatform;
|
||||
PNPM_CONFIG_MANAGE_PACKAGE_MANAGER_VERSIONS = "false";
|
||||
npm_config_nodedir = nodejs_22;
|
||||
npm_config_python = python3;
|
||||
NODE_PATH = "${nodeAddonApi}/lib/node_modules:${node-gyp}/lib/node_modules";
|
||||
PNPM_DEPS = finalAttrs.pnpmDeps;
|
||||
NODE_GYP_WRAPPER_SH = "${../scripts/node-gyp-wrapper.sh}";
|
||||
GATEWAY_PREBUILD_SH = "${../scripts/gateway-prebuild.sh}";
|
||||
PROMOTE_PNPM_INTEGRITY_SH = "${../scripts/promote-pnpm-integrity.sh}";
|
||||
REMOVE_PACKAGE_MANAGER_FIELD_SH = "${../scripts/remove-package-manager-field.sh}";
|
||||
STDENV_SETUP = "${stdenv}/setup";
|
||||
CONFIG_OPTIONS_GENERATOR = "${../scripts/generate-config-options.ts}";
|
||||
CONFIG_OPTIONS_GOLDEN = "${../generated/openclaw-config-options.nix}";
|
||||
NODE_ENGINE_CHECK = "${../scripts/check-node-engine.ts}";
|
||||
OPENCLAW_PLUGIN_EVAL = pluginEvalKey;
|
||||
};
|
||||
|
||||
buildPhase = "${../scripts/gateway-tests-build.sh}";
|
||||
postPatch = "${../scripts/gateway-postpatch.sh}";
|
||||
|
||||
doCheck = true;
|
||||
checkPhase = "${../scripts/config-options-check.sh}";
|
||||
|
||||
installPhase = "${../scripts/empty-install.sh}";
|
||||
dontPatchShebangs = true;
|
||||
})
|
||||
@ -1,86 +1,94 @@
|
||||
{ lib, pkgs, stdenv, nodejs_22, openclawGateway }:
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
stdenv,
|
||||
nodejs_22,
|
||||
openclawGateway,
|
||||
}:
|
||||
|
||||
let
|
||||
stubModule = { lib, ... }: {
|
||||
options = {
|
||||
assertions = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.attrs;
|
||||
default = [];
|
||||
};
|
||||
stubModule =
|
||||
{ lib, ... }:
|
||||
{
|
||||
options = {
|
||||
assertions = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.attrs;
|
||||
default = [ ];
|
||||
};
|
||||
|
||||
home.homeDirectory = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "/tmp";
|
||||
};
|
||||
home.homeDirectory = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "/tmp";
|
||||
};
|
||||
|
||||
home.packages = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.anything;
|
||||
default = [];
|
||||
};
|
||||
home.packages = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.anything;
|
||||
default = [ ];
|
||||
};
|
||||
|
||||
home.file = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = {};
|
||||
};
|
||||
home.file = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
};
|
||||
|
||||
home.activation = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = {};
|
||||
};
|
||||
home.activation = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
};
|
||||
|
||||
launchd.agents = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = {};
|
||||
};
|
||||
launchd.agents = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
};
|
||||
|
||||
systemd.user.services = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = {};
|
||||
};
|
||||
systemd.user.services = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
};
|
||||
|
||||
programs.git.enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
};
|
||||
programs.git.enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
};
|
||||
|
||||
lib = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = {};
|
||||
lib = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
moduleEval = lib.evalModules {
|
||||
modules = [
|
||||
stubModule
|
||||
../modules/home-manager/openclaw.nix
|
||||
({ lib, ... }: {
|
||||
config = {
|
||||
home.homeDirectory = "/tmp";
|
||||
programs.git.enable = false;
|
||||
lib.file.mkOutOfStoreSymlink = path: path;
|
||||
programs.openclaw = {
|
||||
enable = true;
|
||||
launchd.enable = false;
|
||||
systemd.enable = false;
|
||||
instances.default = {};
|
||||
config = {
|
||||
gateway = {
|
||||
bind = "tailnet";
|
||||
auth = {
|
||||
mode = "token";
|
||||
token = "test-token";
|
||||
};
|
||||
reload = {
|
||||
mode = "hot";
|
||||
debounceMs = 500;
|
||||
(
|
||||
{ lib, ... }:
|
||||
{
|
||||
config = {
|
||||
home.homeDirectory = "/tmp";
|
||||
programs.git.enable = false;
|
||||
lib.file.mkOutOfStoreSymlink = path: path;
|
||||
programs.openclaw = {
|
||||
enable = true;
|
||||
launchd.enable = false;
|
||||
systemd.enable = false;
|
||||
instances.default = {
|
||||
workspaceDir = expectedWorkspace;
|
||||
config = {
|
||||
channels.telegram = {
|
||||
enabled = true;
|
||||
botToken = "123456:test-token";
|
||||
dmPolicy = "open";
|
||||
groupPolicy = "disabled";
|
||||
allowFrom = [ "*" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
discovery.mdns.mode = "minimal";
|
||||
};
|
||||
};
|
||||
};
|
||||
})
|
||||
}
|
||||
)
|
||||
];
|
||||
specialArgs = { inherit pkgs; };
|
||||
};
|
||||
@ -88,6 +96,7 @@ let
|
||||
configPathKey = ".openclaw/openclaw.json";
|
||||
configJson = moduleEval.config.home.file."${configPathKey}".text;
|
||||
configFile = pkgs.writeText "openclaw-config.json" configJson;
|
||||
expectedWorkspace = "/tmp/openclaw-explicit-workspace";
|
||||
|
||||
in
|
||||
stdenv.mkDerivation {
|
||||
@ -102,7 +111,8 @@ stdenv.mkDerivation {
|
||||
|
||||
env = {
|
||||
OPENCLAW_CONFIG_PATH = configFile;
|
||||
OPENCLAW_SRC = "${openclawGateway}/lib/openclaw";
|
||||
OPENCLAW_GATEWAY = openclawGateway;
|
||||
OPENCLAW_EXPECTED_WORKSPACE = expectedWorkspace;
|
||||
};
|
||||
|
||||
doCheck = true;
|
||||
|
||||
@ -1,78 +1,368 @@
|
||||
{ lib, pkgs, stdenv }:
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
stdenv,
|
||||
}:
|
||||
|
||||
let
|
||||
stubModule = { lib, ... }: {
|
||||
options = {
|
||||
assertions = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.attrs;
|
||||
default = [];
|
||||
testLib = lib.extend (
|
||||
_final: _prev: {
|
||||
hm.dag = {
|
||||
entryAfter = after: data: {
|
||||
inherit after data;
|
||||
before = [ ];
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
home.homeDirectory = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "/tmp";
|
||||
lockedPathFlake =
|
||||
name: path: narHash:
|
||||
let
|
||||
# If a fixture changes, update with: nix hash path --sri nix/tests/plugins/<name>
|
||||
storePath = builtins.path {
|
||||
inherit name path;
|
||||
sha256 = narHash;
|
||||
};
|
||||
in
|
||||
"path:${builtins.unsafeDiscardStringContext (toString storePath)}?narHash=${narHash}";
|
||||
|
||||
home.packages = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.anything;
|
||||
default = [];
|
||||
};
|
||||
alphaPluginSource =
|
||||
lockedPathFlake "openclaw-test-plugin-alpha" ../tests/plugins/alpha
|
||||
"sha256-FV4UN38sPy2Yp/HhqUxd0HW5l2PcIBBmUz4JzxTAOXY=";
|
||||
betaPluginSource =
|
||||
lockedPathFlake "openclaw-test-plugin-beta" ../tests/plugins/beta
|
||||
"sha256-lDKtQKHZHqOkOprjLZzBEu8cFJhAdyEzsays9hdVeqE=";
|
||||
runtimePluginSource =
|
||||
lockedPathFlake "openclaw-test-plugin-runtime" ../tests/plugins/runtime
|
||||
"sha256-Ytei4j076EQ5rcpoiMt4BhSGUMtlU5kohQ+CCfKwxEE=";
|
||||
|
||||
home.file = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = {};
|
||||
};
|
||||
stubModule =
|
||||
{ lib, ... }:
|
||||
{
|
||||
options = {
|
||||
assertions = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.attrs;
|
||||
default = [ ];
|
||||
};
|
||||
|
||||
home.activation = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = {};
|
||||
};
|
||||
home.homeDirectory = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "/tmp";
|
||||
};
|
||||
|
||||
launchd.agents = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = {};
|
||||
};
|
||||
home.packages = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.anything;
|
||||
default = [ ];
|
||||
};
|
||||
|
||||
systemd.user.services = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = {};
|
||||
};
|
||||
home.file = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
};
|
||||
|
||||
programs.git.enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
};
|
||||
home.activation = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
};
|
||||
|
||||
lib = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = {};
|
||||
launchd.agents = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
};
|
||||
|
||||
systemd.user.services = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
};
|
||||
|
||||
programs.git.enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
};
|
||||
|
||||
lib = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
eval = lib.evalModules {
|
||||
modules = [
|
||||
stubModule
|
||||
../modules/home-manager/openclaw.nix
|
||||
({ lib, ... }: {
|
||||
config = {
|
||||
home.homeDirectory = "/tmp";
|
||||
programs.git.enable = false;
|
||||
lib.file.mkOutOfStoreSymlink = path: path;
|
||||
programs.openclaw = {
|
||||
enable = true;
|
||||
launchd.enable = false;
|
||||
systemd.enable = true;
|
||||
};
|
||||
};
|
||||
})
|
||||
moduleEval =
|
||||
openclawConfig:
|
||||
testLib.evalModules {
|
||||
modules = [
|
||||
stubModule
|
||||
../modules/home-manager/openclaw.nix
|
||||
(
|
||||
{ lib, ... }:
|
||||
{
|
||||
config = {
|
||||
home.homeDirectory = "/tmp";
|
||||
programs.git.enable = false;
|
||||
lib.file.mkOutOfStoreSymlink = path: path;
|
||||
programs.openclaw = {
|
||||
enable = true;
|
||||
launchd.enable = pkgs.stdenv.hostPlatform.isDarwin;
|
||||
systemd.enable = pkgs.stdenv.hostPlatform.isLinux;
|
||||
}
|
||||
// openclawConfig;
|
||||
};
|
||||
}
|
||||
)
|
||||
];
|
||||
specialArgs = { inherit pkgs; };
|
||||
};
|
||||
|
||||
failedAssertions =
|
||||
eval: lib.filter (assertion: !(assertion.assertion or false)) eval.config.assertions;
|
||||
|
||||
requireNoAssertionFailures =
|
||||
name: eval:
|
||||
let
|
||||
failures = failedAssertions eval;
|
||||
messages = map (assertion: assertion.message or "(no message)") failures;
|
||||
in
|
||||
if failures == [ ] then "ok" else throw "${name}: ${lib.concatStringsSep "; " messages}";
|
||||
|
||||
requireAssertionFailure =
|
||||
name: needle: eval:
|
||||
let
|
||||
failures = failedAssertions eval;
|
||||
matching = lib.filter (assertion: lib.hasInfix needle (assertion.message or "")) failures;
|
||||
in
|
||||
if matching != [ ] then "ok" else throw "${name}: expected assertion containing `${needle}`.";
|
||||
|
||||
defaultEval = moduleEval { };
|
||||
defaultConfig = builtins.fromJSON defaultEval.config.home.file.".openclaw/openclaw.json".text;
|
||||
hasLinuxUnit = builtins.hasAttr "openclaw-gateway" defaultEval.config.systemd.user.services;
|
||||
hasDarwinAgent = builtins.hasAttr "com.steipete.openclaw.gateway" defaultEval.config.launchd.agents;
|
||||
defaultCheck = builtins.deepSeq (requireNoAssertionFailures "default instance" defaultEval) (
|
||||
if pkgs.stdenv.hostPlatform.isLinux && !hasLinuxUnit then
|
||||
throw "Default OpenClaw instance missing systemd.unitName."
|
||||
else if pkgs.stdenv.hostPlatform.isDarwin && !hasDarwinAgent then
|
||||
throw "Default OpenClaw instance missing launchd.label."
|
||||
else if (((defaultConfig.gateway or { }).mode or null) != "local") then
|
||||
throw "Default OpenClaw instance missing gateway.mode."
|
||||
else
|
||||
"ok"
|
||||
);
|
||||
|
||||
customPluginEval = moduleEval {
|
||||
customPlugins = [
|
||||
{ source = alphaPluginSource; }
|
||||
];
|
||||
specialArgs = { inherit pkgs; };
|
||||
};
|
||||
customPluginSkill = ".openclaw/workspace/skills/skill";
|
||||
customPluginActivation = builtins.toJSON customPluginEval.config.home.activation.openclawWorkspaceFiles;
|
||||
hasCustomPluginMaterializer = lib.hasInfix "openclaw-materialize-workspace-files" customPluginActivation;
|
||||
customPluginCheck = builtins.deepSeq (requireNoAssertionFailures "customPlugins" customPluginEval) (
|
||||
if hasCustomPluginMaterializer then
|
||||
"ok"
|
||||
else
|
||||
throw "customPlugins did not wire workspace file materialization."
|
||||
);
|
||||
|
||||
hasUnit = builtins.hasAttr "openclaw-gateway" eval.config.systemd.user.services;
|
||||
check = if hasUnit then "ok" else throw "Default OpenClaw instance missing systemd.unitName.";
|
||||
checkKey = builtins.deepSeq check "ok";
|
||||
duplicateSkillEval = moduleEval {
|
||||
customPlugins = [
|
||||
{ source = alphaPluginSource; }
|
||||
{ source = betaPluginSource; }
|
||||
];
|
||||
};
|
||||
duplicateSkillCheck =
|
||||
requireAssertionFailure "duplicate plugin skills"
|
||||
"Duplicate skill paths detected: ${customPluginSkill}"
|
||||
duplicateSkillEval;
|
||||
|
||||
userPluginSkillCollisionEval = moduleEval {
|
||||
customPlugins = [
|
||||
{ source = alphaPluginSource; }
|
||||
];
|
||||
skills = [
|
||||
{
|
||||
name = "skill";
|
||||
mode = "inline";
|
||||
}
|
||||
];
|
||||
};
|
||||
userPluginSkillCollisionCheck =
|
||||
requireAssertionFailure "user/plugin skill collision"
|
||||
"Duplicate skill paths detected: ${customPluginSkill}"
|
||||
userPluginSkillCollisionEval;
|
||||
|
||||
secretProviderEval = moduleEval {
|
||||
config.secrets.providers.test-file = {
|
||||
source = "file";
|
||||
path = "/tmp/openclaw-secrets.json";
|
||||
mode = "json";
|
||||
};
|
||||
};
|
||||
secretProviderConfig =
|
||||
builtins.fromJSON
|
||||
secretProviderEval.config.home.file.".openclaw/openclaw.json".text;
|
||||
secretProviderCheck =
|
||||
builtins.deepSeq (requireNoAssertionFailures "secrets.providers" secretProviderEval)
|
||||
(
|
||||
if
|
||||
((((secretProviderConfig.secrets or { }).providers or { }).test-file or { }).source == "file")
|
||||
then
|
||||
"ok"
|
||||
else
|
||||
throw "secrets.providers file variant missing from generated config."
|
||||
);
|
||||
|
||||
qmdPrewarmEval = moduleEval {
|
||||
qmd.prewarmModels.enable = true;
|
||||
};
|
||||
qmdPrewarmActivation = builtins.toJSON qmdPrewarmEval.config.home.activation.openclawQmdPrewarm;
|
||||
qmdPrewarmCheck = builtins.deepSeq (requireNoAssertionFailures "qmd.prewarmModels" qmdPrewarmEval) (
|
||||
if
|
||||
lib.hasInfix "OPENCLAW_QMD_BIN=" qmdPrewarmActivation
|
||||
&& lib.hasInfix "openclaw-qmd-prewarm.sh" qmdPrewarmActivation
|
||||
then
|
||||
"ok"
|
||||
else
|
||||
throw "qmd.prewarmModels did not wire QMD model-cache prewarm activation."
|
||||
);
|
||||
|
||||
runtimeProfileEval = moduleEval {
|
||||
runtimePackages = [ pkgs.jq ];
|
||||
environment.OPENCLAW_TEST_SECRET = "/tmp/openclaw-secret";
|
||||
};
|
||||
runtimeProfileActivation = builtins.toJSON runtimeProfileEval.config.home.activation.openclawCodexRuntimeProfiles;
|
||||
runtimeProfileCheck =
|
||||
builtins.deepSeq (requireNoAssertionFailures "runtime profile" runtimeProfileEval)
|
||||
(
|
||||
if lib.hasInfix "openclaw-link-codex-runtime-profiles.sh" runtimeProfileActivation then
|
||||
"ok"
|
||||
else
|
||||
throw "runtimePackages did not wire the Codex runtime profile activation."
|
||||
);
|
||||
|
||||
openclawPluginEval = moduleEval {
|
||||
customPlugins = [
|
||||
{ source = runtimePluginSource; }
|
||||
];
|
||||
config.plugins.load.paths = [
|
||||
"/tmp/user-openclaw-plugin"
|
||||
];
|
||||
};
|
||||
openclawPluginConfig = builtins.fromJSON (
|
||||
builtins.unsafeDiscardStringContext
|
||||
openclawPluginEval.config.home.file.".openclaw/openclaw.json".text
|
||||
);
|
||||
openclawPluginLoadPaths = ((openclawPluginConfig.plugins or { }).load or { }).paths or [ ];
|
||||
openclawPluginEntry = ((openclawPluginConfig.plugins or { }).entries or { }).runtime-test or { };
|
||||
openclawPluginDisabledEntry =
|
||||
((openclawPluginConfig.plugins or { }).entries or { }).runtime-disabled or null;
|
||||
openclawPluginCheck =
|
||||
builtins.deepSeq (requireNoAssertionFailures "OpenClaw plugin load" openclawPluginEval)
|
||||
(
|
||||
if !(lib.any (path: lib.hasSuffix "/plugin" path) openclawPluginLoadPaths) then
|
||||
throw "OpenClaw plugin root was not added to plugins.load.paths."
|
||||
else if !(lib.any (path: lib.hasSuffix "/disabled-plugin" path) openclawPluginLoadPaths) then
|
||||
throw "OpenClaw plugin root with enabled=false was not added to plugins.load.paths."
|
||||
else if !(lib.elem "/tmp/user-openclaw-plugin" openclawPluginLoadPaths) then
|
||||
throw "User-defined plugins.load.paths entry was not preserved."
|
||||
else if (openclawPluginEntry.enabled or false) != true then
|
||||
throw "OpenClaw plugin entry default was not enabled."
|
||||
else if (openclawPluginDisabledEntry.enabled or null) != false then
|
||||
throw "OpenClaw plugin entry with enabled=false did not render a disabled default."
|
||||
else
|
||||
"ok"
|
||||
);
|
||||
|
||||
openclawPluginOverrideEval = moduleEval {
|
||||
customPlugins = [
|
||||
{ source = runtimePluginSource; }
|
||||
];
|
||||
config.plugins.entries.runtime-test.enabled = false;
|
||||
};
|
||||
openclawPluginOverrideConfig = builtins.fromJSON (
|
||||
builtins.unsafeDiscardStringContext
|
||||
openclawPluginOverrideEval.config.home.file.".openclaw/openclaw.json".text
|
||||
);
|
||||
openclawPluginOverrideEntry =
|
||||
((openclawPluginOverrideConfig.plugins or { }).entries or { }).runtime-test or { };
|
||||
openclawPluginOverrideDisabledEntry =
|
||||
((openclawPluginOverrideConfig.plugins or { }).entries or { }).runtime-disabled or { };
|
||||
openclawPluginOverrideCheck =
|
||||
builtins.deepSeq (requireNoAssertionFailures "OpenClaw plugin override" openclawPluginOverrideEval)
|
||||
(
|
||||
if (openclawPluginOverrideEntry.enabled or null) != false then
|
||||
throw "User config could not override OpenClaw plugin enabled default."
|
||||
else if (openclawPluginOverrideDisabledEntry.enabled or null) != false then
|
||||
throw "Plugin enabled=false default did not survive when not overridden."
|
||||
else
|
||||
"ok"
|
||||
);
|
||||
|
||||
openclawPluginEnableOverrideEval = moduleEval {
|
||||
customPlugins = [
|
||||
{ source = runtimePluginSource; }
|
||||
];
|
||||
config.plugins.entries.runtime-disabled.enabled = true;
|
||||
};
|
||||
openclawPluginEnableOverrideConfig = builtins.fromJSON (
|
||||
builtins.unsafeDiscardStringContext
|
||||
openclawPluginEnableOverrideEval.config.home.file.".openclaw/openclaw.json".text
|
||||
);
|
||||
openclawPluginEnableOverrideEntry =
|
||||
((openclawPluginEnableOverrideConfig.plugins or { }).entries or { }).runtime-disabled or { };
|
||||
openclawPluginEnableOverrideCheck =
|
||||
builtins.deepSeq
|
||||
(requireNoAssertionFailures "OpenClaw plugin enable override" openclawPluginEnableOverrideEval)
|
||||
(
|
||||
if (openclawPluginEnableOverrideEntry.enabled or null) == true then
|
||||
"ok"
|
||||
else
|
||||
throw "User config could not override OpenClaw plugin enabled=false default."
|
||||
);
|
||||
|
||||
npmRuntimePluginEval = moduleEval {
|
||||
customPlugins = [
|
||||
{
|
||||
source = "npm:@tencent-weixin/openclaw-weixin@2.4.2";
|
||||
id = "openclaw-weixin";
|
||||
hash = lib.fakeHash;
|
||||
}
|
||||
];
|
||||
};
|
||||
npmRuntimePluginConfig = builtins.fromJSON (
|
||||
builtins.unsafeDiscardStringContext
|
||||
npmRuntimePluginEval.config.home.file.".openclaw/openclaw.json".text
|
||||
);
|
||||
npmRuntimePluginLoadPaths = ((npmRuntimePluginConfig.plugins or { }).load or { }).paths or [ ];
|
||||
npmRuntimePluginEntry =
|
||||
((npmRuntimePluginConfig.plugins or { }).entries or { }).openclaw-weixin or { };
|
||||
npmRuntimePluginCheck =
|
||||
builtins.deepSeq (requireNoAssertionFailures "npm OpenClaw runtime plugin" npmRuntimePluginEval)
|
||||
(
|
||||
if
|
||||
!(lib.any (
|
||||
path: lib.hasInfix "openclaw-runtime-plugin-openclaw-weixin" path
|
||||
) npmRuntimePluginLoadPaths)
|
||||
then
|
||||
throw "npm OpenClaw runtime plugin root was not added to plugins.load.paths."
|
||||
else if (npmRuntimePluginEntry.enabled or false) != true then
|
||||
throw "npm OpenClaw runtime plugin entry default was not enabled."
|
||||
else
|
||||
"ok"
|
||||
);
|
||||
|
||||
checkKey = builtins.deepSeq [
|
||||
defaultCheck
|
||||
customPluginCheck
|
||||
duplicateSkillCheck
|
||||
userPluginSkillCollisionCheck
|
||||
secretProviderCheck
|
||||
qmdPrewarmCheck
|
||||
runtimeProfileCheck
|
||||
openclawPluginCheck
|
||||
openclawPluginOverrideCheck
|
||||
openclawPluginEnableOverrideCheck
|
||||
npmRuntimePluginCheck
|
||||
] "ok";
|
||||
|
||||
in
|
||||
stdenv.mkDerivation {
|
||||
|
||||
27
nix/checks/openclaw-gateway-smoke.nix
Normal file
27
nix/checks/openclaw-gateway-smoke.nix
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
nodejs_22,
|
||||
openclawGateway,
|
||||
}:
|
||||
|
||||
stdenv.mkDerivation {
|
||||
pname = "openclaw-gateway-smoke";
|
||||
version = lib.getVersion openclawGateway;
|
||||
|
||||
dontUnpack = true;
|
||||
dontConfigure = true;
|
||||
dontBuild = true;
|
||||
|
||||
nativeBuildInputs = [ nodejs_22 ];
|
||||
|
||||
env = {
|
||||
OPENCLAW_GATEWAY = openclawGateway;
|
||||
};
|
||||
|
||||
__darwinAllowLocalNetworking = true;
|
||||
|
||||
doCheck = true;
|
||||
checkPhase = "${nodejs_22}/bin/node ${../scripts/gateway-smoke.mjs}";
|
||||
installPhase = "${../scripts/empty-install.sh}";
|
||||
}
|
||||
@ -1,90 +0,0 @@
|
||||
{ lib
|
||||
, stdenv
|
||||
, fetchFromGitHub
|
||||
, fetchurl
|
||||
, nodejs_22
|
||||
, pnpm_10
|
||||
, bun
|
||||
, pkg-config
|
||||
, jq
|
||||
, python3
|
||||
, node-gyp
|
||||
, vips
|
||||
, git
|
||||
, zstd
|
||||
, sourceInfo
|
||||
, pnpmDepsHash ? (sourceInfo.pnpmDepsHash or null)
|
||||
}:
|
||||
|
||||
let
|
||||
sourceFetch = lib.removeAttrs sourceInfo [ "pnpmDepsHash" ];
|
||||
pnpmPlatform = if stdenv.hostPlatform.isDarwin then "darwin" else "linux";
|
||||
pnpmArch = if stdenv.hostPlatform.isAarch64 then "arm64" else "x64";
|
||||
nodeAddonApi = stdenv.mkDerivation {
|
||||
pname = "node-addon-api";
|
||||
version = "8.5.0";
|
||||
src = fetchurl {
|
||||
url = "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz";
|
||||
hash = "sha256-0S8HyBYig7YhNVGFXx2o2sFiMxN0YpgwteZA8TDweRA=";
|
||||
};
|
||||
dontConfigure = true;
|
||||
dontBuild = true;
|
||||
installPhase = "${../scripts/node-addon-api-install.sh}";
|
||||
};
|
||||
in
|
||||
|
||||
stdenv.mkDerivation (finalAttrs: {
|
||||
pname = "openclaw-gateway-tests";
|
||||
version = "2026.1.8-2";
|
||||
|
||||
src = fetchFromGitHub sourceFetch;
|
||||
|
||||
pnpmDeps = pnpm_10.fetchDeps {
|
||||
inherit (finalAttrs) pname version src;
|
||||
hash = if pnpmDepsHash != null
|
||||
then pnpmDepsHash
|
||||
else lib.fakeHash;
|
||||
fetcherVersion = 2;
|
||||
npm_config_arch = pnpmArch;
|
||||
npm_config_platform = pnpmPlatform;
|
||||
nativeBuildInputs = [ git ];
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
nodejs_22
|
||||
pnpm_10
|
||||
bun
|
||||
pkg-config
|
||||
jq
|
||||
python3
|
||||
node-gyp
|
||||
zstd
|
||||
];
|
||||
|
||||
buildInputs = [ vips ];
|
||||
|
||||
env = {
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS = "1";
|
||||
npm_config_arch = pnpmArch;
|
||||
npm_config_platform = pnpmPlatform;
|
||||
PNPM_CONFIG_MANAGE_PACKAGE_MANAGER_VERSIONS = "false";
|
||||
npm_config_nodedir = nodejs_22;
|
||||
npm_config_python = python3;
|
||||
NODE_PATH = "${nodeAddonApi}/lib/node_modules:${node-gyp}/lib/node_modules";
|
||||
PNPM_DEPS = finalAttrs.pnpmDeps;
|
||||
NODE_GYP_WRAPPER_SH = "${../scripts/node-gyp-wrapper.sh}";
|
||||
GATEWAY_PREBUILD_SH = "${../scripts/gateway-prebuild.sh}";
|
||||
PROMOTE_PNPM_INTEGRITY_SH = "${../scripts/promote-pnpm-integrity.sh}";
|
||||
REMOVE_PACKAGE_MANAGER_FIELD_SH = "${../scripts/remove-package-manager-field.sh}";
|
||||
STDENV_SETUP = "${stdenv}/setup";
|
||||
};
|
||||
|
||||
postPatch = "${../scripts/gateway-postpatch.sh}";
|
||||
buildPhase = "${../scripts/gateway-tests-build.sh}";
|
||||
|
||||
doCheck = true;
|
||||
checkPhase = "${../scripts/gateway-tests-check.sh}";
|
||||
|
||||
installPhase = "${../scripts/empty-install.sh}";
|
||||
dontPatchShebangs = true;
|
||||
})
|
||||
@ -3,12 +3,25 @@
|
||||
let
|
||||
openclawModule = ../modules/home-manager/openclaw.nix;
|
||||
testScript = builtins.readFile ../tests/hm-activation.py;
|
||||
lockedPathFlake =
|
||||
name: path: narHash:
|
||||
let
|
||||
storePath = builtins.path {
|
||||
inherit name path;
|
||||
sha256 = narHash;
|
||||
};
|
||||
in
|
||||
"path:${builtins.unsafeDiscardStringContext (toString storePath)}?narHash=${narHash}";
|
||||
alphaPluginSource =
|
||||
lockedPathFlake "openclaw-test-plugin-alpha" ../tests/plugins/alpha
|
||||
"sha256-FV4UN38sPy2Yp/HhqUxd0HW5l2PcIBBmUz4JzxTAOXY=";
|
||||
|
||||
in
|
||||
pkgs.testers.nixosTest {
|
||||
name = "openclaw-hm-activation";
|
||||
|
||||
nodes.machine = { ... }:
|
||||
nodes.machine =
|
||||
{ ... }:
|
||||
{
|
||||
imports = [ home-manager.nixosModules.home-manager ];
|
||||
|
||||
@ -23,7 +36,8 @@ pkgs.testers.nixosTest {
|
||||
home-manager = {
|
||||
useGlobalPkgs = true;
|
||||
useUserPackages = true;
|
||||
users.alice = { lib, ... }:
|
||||
users.alice =
|
||||
{ lib, ... }:
|
||||
{
|
||||
imports = [ openclawModule ];
|
||||
|
||||
@ -35,6 +49,10 @@ pkgs.testers.nixosTest {
|
||||
|
||||
programs.openclaw = {
|
||||
enable = true;
|
||||
documents = ../tests/documents;
|
||||
customPlugins = [
|
||||
{ source = alphaPluginSource; }
|
||||
];
|
||||
installApp = false;
|
||||
launchd.enable = false;
|
||||
instances.default = {
|
||||
@ -65,7 +83,7 @@ pkgs.testers.nixosTest {
|
||||
"OPENCLAW_SKIP_CRON=1"
|
||||
"OPENCLAW_SKIP_GMAIL_WATCHER=1"
|
||||
"OPENCLAW_DISABLE_BONJOUR=1"
|
||||
"NODE_OPTIONS=--report-on-fatalerror --report-on-signal --report-signal=SIGABRT"
|
||||
"NODE_OPTIONS=--report-on-fatalerror"
|
||||
"NODE_REPORT_DIRECTORY=/tmp/openclaw"
|
||||
"NODE_REPORT_FILENAME=node-report.%p.json"
|
||||
];
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
{ lib, stdenv, openclawGateway }:
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
nodejs_22,
|
||||
openclawGateway,
|
||||
}:
|
||||
|
||||
stdenv.mkDerivation {
|
||||
pname = "openclaw-package-contents";
|
||||
@ -13,6 +18,7 @@ stdenv.mkDerivation {
|
||||
};
|
||||
|
||||
doCheck = true;
|
||||
nativeCheckInputs = [ nodejs_22 ];
|
||||
checkPhase = "${../scripts/check-package-contents.sh}";
|
||||
installPhase = "${../scripts/empty-install.sh}";
|
||||
}
|
||||
|
||||
24
nix/checks/openclaw-qmd-runtime.nix
Normal file
24
nix/checks/openclaw-qmd-runtime.nix
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
lib,
|
||||
stdenvNoCC,
|
||||
openclawPackage,
|
||||
qmdPackage ? null,
|
||||
}:
|
||||
|
||||
stdenvNoCC.mkDerivation {
|
||||
pname = "openclaw-qmd-runtime";
|
||||
version = lib.getVersion openclawPackage;
|
||||
|
||||
dontUnpack = true;
|
||||
dontConfigure = true;
|
||||
dontBuild = true;
|
||||
|
||||
env = {
|
||||
OPENCLAW_PACKAGE = openclawPackage;
|
||||
QMD_PACKAGE = lib.optionalString (qmdPackage != null) "${qmdPackage}";
|
||||
};
|
||||
|
||||
doCheck = true;
|
||||
checkPhase = "${../scripts/check-openclaw-qmd-runtime.sh}";
|
||||
installPhase = "${../scripts/empty-install.sh}";
|
||||
}
|
||||
170
nix/checks/openclaw-source-checks.nix
Normal file
170
nix/checks/openclaw-source-checks.nix
Normal file
@ -0,0 +1,170 @@
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
stdenv,
|
||||
fetchFromGitHub,
|
||||
fetchurl,
|
||||
nodejs_22,
|
||||
pnpm_10,
|
||||
fetchPnpmDeps,
|
||||
pkg-config,
|
||||
jq,
|
||||
python3,
|
||||
node-gyp,
|
||||
vips,
|
||||
git,
|
||||
zstd,
|
||||
sourceInfo,
|
||||
openclawGateway,
|
||||
pnpmDepsHash ? (sourceInfo.pnpmDepsHash or null),
|
||||
}:
|
||||
|
||||
let
|
||||
pluginCatalog = import ../modules/home-manager/openclaw/plugin-catalog.nix;
|
||||
linuxBundledPlugins = builtins.attrNames (
|
||||
lib.filterAttrs (_: plugin: plugin.linux or false) pluginCatalog
|
||||
);
|
||||
enableBundledPlugin = name: stdenv.hostPlatform.isDarwin || lib.elem name linuxBundledPlugins;
|
||||
|
||||
stubModule =
|
||||
{ lib, ... }:
|
||||
{
|
||||
options = {
|
||||
assertions = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.attrs;
|
||||
default = [ ];
|
||||
};
|
||||
|
||||
home.homeDirectory = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "/tmp";
|
||||
};
|
||||
|
||||
home.packages = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.anything;
|
||||
default = [ ];
|
||||
};
|
||||
|
||||
home.file = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
};
|
||||
|
||||
home.activation = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
};
|
||||
|
||||
launchd.agents = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
};
|
||||
|
||||
systemd.user.services = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
};
|
||||
|
||||
programs.git.enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
};
|
||||
|
||||
lib = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
pluginEval = lib.evalModules {
|
||||
modules = [
|
||||
stubModule
|
||||
../modules/home-manager/openclaw.nix
|
||||
(
|
||||
{ lib, options, ... }:
|
||||
{
|
||||
config = {
|
||||
home.homeDirectory = "/tmp";
|
||||
programs.git.enable = false;
|
||||
lib.file.mkOutOfStoreSymlink = path: path;
|
||||
programs.openclaw = {
|
||||
enable = true;
|
||||
launchd.enable = false;
|
||||
systemd.enable = false;
|
||||
instances.default = { };
|
||||
bundledPlugins = lib.mapAttrs (name: _: {
|
||||
enable = enableBundledPlugin name;
|
||||
}) options.programs.openclaw.bundledPlugins;
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
];
|
||||
specialArgs = { inherit pkgs; };
|
||||
};
|
||||
|
||||
pluginEvalKey = builtins.deepSeq pluginEval.config.assertions "ok";
|
||||
|
||||
common =
|
||||
import ../lib/openclaw-gateway-common.nix
|
||||
{
|
||||
inherit
|
||||
lib
|
||||
stdenv
|
||||
fetchFromGitHub
|
||||
fetchurl
|
||||
nodejs_22
|
||||
pnpm_10
|
||||
fetchPnpmDeps
|
||||
pkg-config
|
||||
jq
|
||||
python3
|
||||
node-gyp
|
||||
git
|
||||
zstd
|
||||
;
|
||||
}
|
||||
{
|
||||
pname = "openclaw-source-checks";
|
||||
sourceInfo = sourceInfo;
|
||||
pnpmDepsHash = pnpmDepsHash;
|
||||
pnpmDepsPname = "openclaw-gateway";
|
||||
enableSharp = true;
|
||||
extraBuildInputs = [ vips ];
|
||||
};
|
||||
|
||||
in
|
||||
|
||||
stdenv.mkDerivation (finalAttrs: {
|
||||
pname = "openclaw-source-checks";
|
||||
inherit (common) version;
|
||||
|
||||
src = common.resolvedSrc;
|
||||
pnpmDeps = common.pnpmDeps;
|
||||
|
||||
nativeBuildInputs = common.nativeBuildInputs;
|
||||
buildInputs = common.buildInputs;
|
||||
|
||||
env = common.env // {
|
||||
PNPM_DEPS = finalAttrs.pnpmDeps;
|
||||
OPENCLAW_GATEWAY = openclawGateway;
|
||||
CONFIG_OPTIONS_GENERATOR = "${../scripts/generate-config-options.ts}";
|
||||
CONFIG_OPTIONS_GOLDEN = "${../generated/openclaw-config-options.nix}";
|
||||
CONFIG_OPTIONS_CHECK_SH = "${../scripts/config-options-check.sh}";
|
||||
NODE_ENGINE_CHECK = "${../scripts/check-node-engine.ts}";
|
||||
OPENCLAW_PLUGIN_EVAL = pluginEvalKey;
|
||||
OPENCLAW_SCHEMA_REV = sourceInfo.rev;
|
||||
};
|
||||
|
||||
passthru = common.passthru;
|
||||
|
||||
postPatch = "${../scripts/gateway-postpatch.sh}";
|
||||
buildPhase = "${../scripts/source-checks-build.sh}";
|
||||
|
||||
doCheck = true;
|
||||
checkPhase = "${../scripts/source-checks-check.sh}";
|
||||
|
||||
installPhase = "${../scripts/empty-install.sh}";
|
||||
dontPatchShebangs = true;
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
48
nix/lib/npm-runtime-plugin.nix
Normal file
48
nix/lib/npm-runtime-plugin.nix
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
lib,
|
||||
stdenvNoCC,
|
||||
nodejs_22,
|
||||
}:
|
||||
|
||||
{
|
||||
id,
|
||||
source,
|
||||
hash ? lib.fakeHash,
|
||||
}:
|
||||
|
||||
let
|
||||
npmSpec =
|
||||
if lib.hasPrefix "npm:" source then
|
||||
lib.removePrefix "npm:" source
|
||||
else
|
||||
throw "OpenClaw runtime npm plugin source must start with `npm:`: ${source}";
|
||||
safeName = lib.replaceStrings [ "@" "/" ":" ] [ "" "-" "-" ] id;
|
||||
in
|
||||
stdenvNoCC.mkDerivation {
|
||||
pname = "openclaw-runtime-plugin-${safeName}";
|
||||
version = "1";
|
||||
|
||||
nativeBuildInputs = [ nodejs_22 ];
|
||||
|
||||
dontUnpack = true;
|
||||
dontConfigure = true;
|
||||
dontBuild = true;
|
||||
|
||||
outputHashMode = "recursive";
|
||||
outputHashAlgo = "sha256";
|
||||
outputHash = hash;
|
||||
|
||||
env = {
|
||||
OPENCLAW_RUNTIME_PLUGIN_ID = id;
|
||||
OPENCLAW_RUNTIME_PLUGIN_NPM_SPEC = npmSpec;
|
||||
};
|
||||
|
||||
installPhase = "${../scripts/npm-runtime-plugin-install.sh}";
|
||||
|
||||
meta = with lib; {
|
||||
description = "Nix-packaged OpenClaw runtime plugin ${id} from ${source}";
|
||||
homepage = "https://github.com/openclaw/openclaw";
|
||||
license = licenses.mit;
|
||||
platforms = platforms.darwin ++ platforms.linux;
|
||||
};
|
||||
}
|
||||
140
nix/lib/openclaw-gateway-common.nix
Normal file
140
nix/lib/openclaw-gateway-common.nix
Normal file
@ -0,0 +1,140 @@
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
fetchFromGitHub,
|
||||
fetchurl,
|
||||
nodejs_22,
|
||||
pnpm_10,
|
||||
fetchPnpmDeps,
|
||||
pkg-config,
|
||||
jq,
|
||||
python3,
|
||||
node-gyp,
|
||||
git,
|
||||
zstd,
|
||||
}:
|
||||
|
||||
# Shared build plumbing for OpenClaw gateway-related derivations.
|
||||
#
|
||||
# Goals:
|
||||
# - one source of truth for pnpm deps fetch + common env
|
||||
# - keep the individual derivations small/boring
|
||||
|
||||
{
|
||||
pname,
|
||||
sourceInfo,
|
||||
pnpmDepsHash ? (sourceInfo.pnpmDepsHash or null),
|
||||
pnpmDepsPname ? "openclaw-gateway",
|
||||
gatewaySrc ? null,
|
||||
src ? null,
|
||||
enableSharp ? false,
|
||||
extraNativeBuildInputs ? [ ],
|
||||
extraBuildInputs ? [ ],
|
||||
extraEnv ? { },
|
||||
}:
|
||||
|
||||
let
|
||||
sourceFetch = lib.removeAttrs sourceInfo [
|
||||
"pnpmDepsHash"
|
||||
"releaseTag"
|
||||
"releaseVersion"
|
||||
"applyPublicSurfaceHardlinksPatch"
|
||||
"applySkipPluginAutoEnableNixModePatch"
|
||||
"publicSurfaceHardlinksPatch"
|
||||
"fsSafeSource"
|
||||
];
|
||||
|
||||
# Prefer nixpkgs' platform mapping instead of hand-rolled arch/platform.
|
||||
pnpmPlatform = stdenv.hostPlatform.node.platform;
|
||||
pnpmArch = stdenv.hostPlatform.node.arch;
|
||||
|
||||
revShort = lib.substring 0 8 sourceInfo.rev;
|
||||
version = "unstable-${revShort}";
|
||||
|
||||
resolvedSrc =
|
||||
if src != null then
|
||||
src
|
||||
else if gatewaySrc != null then
|
||||
gatewaySrc
|
||||
else
|
||||
fetchFromGitHub sourceFetch;
|
||||
|
||||
fsSafeSource = if sourceInfo ? fsSafeSource then fetchFromGitHub sourceInfo.fsSafeSource else null;
|
||||
publicSurfaceHardlinksPatch =
|
||||
sourceInfo.publicSurfaceHardlinksPatch or ../patches/allow-package-public-surface-hardlinks.patch;
|
||||
|
||||
nodeAddonApi = import ../packages/node-addon-api.nix { inherit stdenv fetchurl; };
|
||||
|
||||
pnpmDeps = fetchPnpmDeps {
|
||||
pname = pnpmDepsPname;
|
||||
inherit version;
|
||||
src = resolvedSrc;
|
||||
pnpm = pnpm_10;
|
||||
hash = if pnpmDepsHash != null then pnpmDepsHash else lib.fakeHash;
|
||||
fetcherVersion = 3;
|
||||
npm_config_arch = pnpmArch;
|
||||
npm_config_platform = pnpmPlatform;
|
||||
nativeBuildInputs = [ git ];
|
||||
};
|
||||
|
||||
envBase = {
|
||||
npm_config_arch = pnpmArch;
|
||||
npm_config_platform = pnpmPlatform;
|
||||
PNPM_CONFIG_MANAGE_PACKAGE_MANAGER_VERSIONS = "false";
|
||||
npm_config_nodedir = nodejs_22;
|
||||
npm_config_python = python3;
|
||||
NODE_PATH = "${nodeAddonApi}/lib/node_modules:${node-gyp}/lib/node_modules";
|
||||
PNPM_DEPS = pnpmDeps;
|
||||
OPENCLAW_BUILD_ROOT_SH = "${../scripts/build-root.sh}";
|
||||
NODE_GYP_WRAPPER_SH = "${../scripts/node-gyp-wrapper.sh}";
|
||||
GATEWAY_PREBUILD_SH = "${../scripts/gateway-prebuild.sh}";
|
||||
PATCH_BUNDLED_RUNTIME_DEPS_SCRIPT = "${../patches/stage-bundled-plugin-runtime-deps.mjs}";
|
||||
PATCH_PUBLIC_SURFACE_HARDLINKS =
|
||||
if sourceInfo.applyPublicSurfaceHardlinksPatch or true then
|
||||
"${publicSurfaceHardlinksPatch}"
|
||||
else
|
||||
"";
|
||||
PATCH_SKIP_PLUGIN_AUTO_ENABLE_NIX_MODE =
|
||||
if sourceInfo.applySkipPluginAutoEnableNixModePatch or true then
|
||||
"${../patches/skip-plugin-auto-enable-persist-in-nix-mode.patch}"
|
||||
else
|
||||
"";
|
||||
PROMOTE_PNPM_INTEGRITY_SH = "${../scripts/promote-pnpm-integrity.sh}";
|
||||
REMOVE_PACKAGE_MANAGER_FIELD_SH = "${../scripts/remove-package-manager-field.sh}";
|
||||
STDENV_SETUP = "${stdenv}/setup";
|
||||
}
|
||||
// lib.optionalAttrs (fsSafeSource != null) {
|
||||
OPENCLAW_FS_SAFE_SOURCE = fsSafeSource;
|
||||
};
|
||||
|
||||
in
|
||||
{
|
||||
inherit
|
||||
version
|
||||
pnpmDeps
|
||||
resolvedSrc
|
||||
pnpmPlatform
|
||||
pnpmArch
|
||||
nodeAddonApi
|
||||
;
|
||||
|
||||
nativeBuildInputs = [
|
||||
nodejs_22
|
||||
pnpm_10
|
||||
pkg-config
|
||||
jq
|
||||
python3
|
||||
node-gyp
|
||||
zstd
|
||||
]
|
||||
++ extraNativeBuildInputs;
|
||||
|
||||
buildInputs = extraBuildInputs;
|
||||
|
||||
env = envBase // (lib.optionalAttrs enableSharp { SHARP_IGNORE_GLOBAL_LIBVIPS = "1"; }) // extraEnv;
|
||||
|
||||
passthru = {
|
||||
inherit sourceInfo pnpmDeps;
|
||||
pinnedRev = sourceInfo.rev;
|
||||
};
|
||||
}
|
||||
@ -2,16 +2,14 @@
|
||||
set -euo pipefail
|
||||
|
||||
link_agent() {
|
||||
local target="$1"
|
||||
local label="$2"
|
||||
local label="$1"
|
||||
local target="$HOME/Library/LaunchAgents/${label}.plist"
|
||||
|
||||
local candidate
|
||||
local candidate=""
|
||||
local hm_gen
|
||||
hm_gen="$(realpath "$HOME/.local/state/nix/profiles/home-manager" 2>/dev/null || true)"
|
||||
if [ -n "$hm_gen" ] && [ -e "$hm_gen/LaunchAgents/${label}.plist" ]; then
|
||||
candidate="$hm_gen/LaunchAgents/${label}.plist"
|
||||
else
|
||||
candidate="$(/bin/ls -t /nix/store/*${label}.plist 2>/dev/null | /usr/bin/head -n 1 || true)"
|
||||
fi
|
||||
|
||||
if [ -z "$candidate" ]; then
|
||||
@ -22,6 +20,7 @@ link_agent() {
|
||||
current="$(/usr/bin/readlink "$target" 2>/dev/null || true)"
|
||||
|
||||
if [ "$current" != "$candidate" ]; then
|
||||
/bin/mkdir -p "${target%/*}"
|
||||
/bin/ln -sfn "$candidate" "$target"
|
||||
/bin/launchctl bootout "gui/$UID" "$target" 2>/dev/null || true
|
||||
/bin/launchctl bootstrap "gui/$UID" "$target" 2>/dev/null || true
|
||||
@ -30,14 +29,6 @@ link_agent() {
|
||||
/bin/launchctl kickstart -k "gui/$UID/$label" 2>/dev/null || true
|
||||
}
|
||||
|
||||
link_agent "$HOME/Library/LaunchAgents/com.steipete.openclaw.gateway.nix.plist" \
|
||||
"com.steipete.openclaw.gateway.nix"
|
||||
|
||||
link_agent "$HOME/Library/LaunchAgents/com.steipete.openclaw.gateway.nix-test.plist" \
|
||||
"com.steipete.openclaw.gateway.nix-test"
|
||||
|
||||
link_agent "$HOME/Library/LaunchAgents/com.steipete.openclaw.gateway.prod.plist" \
|
||||
"com.steipete.openclaw.gateway.prod"
|
||||
|
||||
link_agent "$HOME/Library/LaunchAgents/com.steipete.openclaw.gateway.test.plist" \
|
||||
"com.steipete.openclaw.gateway.test"
|
||||
for label in "$@"; do
|
||||
link_agent "$label"
|
||||
done
|
||||
|
||||
22
nix/modules/home-manager/openclaw-link-codex-runtime-profiles.sh
Executable file
22
nix/modules/home-manager/openclaw-link-codex-runtime-profiles.sh
Executable file
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
manifest=$1
|
||||
|
||||
while IFS=$'\t' read -r profile_dir bin_dir; do
|
||||
[ -n "$profile_dir" ] || continue
|
||||
|
||||
mkdir -p "$profile_dir"
|
||||
|
||||
link="$profile_dir/bin"
|
||||
if [ -L "$link" ]; then
|
||||
rm "$link"
|
||||
fi
|
||||
|
||||
if [ -e "$link" ]; then
|
||||
echo "Refusing to replace non-symlink Codex runtime bin: $link" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ln -s "$bin_dir" "$link"
|
||||
done < "$manifest"
|
||||
42
nix/modules/home-manager/openclaw-materialize-workspace-files.sh
Executable file
42
nix/modules/home-manager/openclaw-materialize-workspace-files.sh
Executable file
@ -0,0 +1,42 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "$#" -ne 2 ]; then
|
||||
echo "usage: openclaw-materialize-workspace-files <state-manifest> <source-target-manifest>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
manifest="$1"
|
||||
source_manifest="$2"
|
||||
|
||||
manifest_dir="$(dirname "$manifest")"
|
||||
mkdir -p "$manifest_dir"
|
||||
new_manifest="$(mktemp)"
|
||||
trap 'rm -f "$new_manifest"' EXIT
|
||||
|
||||
copy_path() {
|
||||
source="$1"
|
||||
target="$2"
|
||||
|
||||
if [ -e "$target" ] || [ -L "$target" ]; then
|
||||
chmod -R u+w "$target" 2>/dev/null || true
|
||||
rm -rf "$target"
|
||||
fi
|
||||
mkdir -p "$(dirname "$target")"
|
||||
|
||||
if [ -d "$source" ]; then
|
||||
cp -RL "$source" "$target"
|
||||
else
|
||||
cp -L "$source" "$target"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$target" >> "$new_manifest"
|
||||
}
|
||||
|
||||
while IFS="$(printf '\t')" read -r source target; do
|
||||
if [ -n "$source" ] && [ -n "$target" ]; then
|
||||
copy_path "$source" "$target"
|
||||
fi
|
||||
done < "$source_manifest"
|
||||
|
||||
sort -u "$new_manifest" > "$manifest"
|
||||
@ -1,10 +1,16 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
openclawLib = import ./lib.nix { inherit config lib pkgs; };
|
||||
cfg = openclawLib.cfg;
|
||||
homeDir = openclawLib.homeDir;
|
||||
appPackage = openclawLib.appPackage;
|
||||
qmdPackage = openclawLib.qmdPackage;
|
||||
|
||||
defaultInstance = {
|
||||
enable = cfg.enable;
|
||||
@ -16,10 +22,12 @@ let
|
||||
gatewayPort = 18789;
|
||||
gatewayPath = null;
|
||||
gatewayPnpmDepsHash = lib.fakeHash;
|
||||
runtimePackages = [ ];
|
||||
environment = { };
|
||||
launchd = cfg.launchd;
|
||||
systemd = cfg.systemd;
|
||||
plugins = openclawLib.effectivePlugins;
|
||||
config = {};
|
||||
config = { };
|
||||
appDefaults = {
|
||||
enable = true;
|
||||
attachExistingOnly = true;
|
||||
@ -32,20 +40,37 @@ let
|
||||
};
|
||||
};
|
||||
|
||||
instances = if cfg.instances != {}
|
||||
then cfg.instances
|
||||
else lib.optionalAttrs cfg.enable { default = defaultInstance; };
|
||||
instances =
|
||||
if cfg.instances != { } then
|
||||
cfg.instances
|
||||
else
|
||||
lib.optionalAttrs cfg.enable { default = defaultInstance; };
|
||||
|
||||
enabledInstances = lib.filterAttrs (_: inst: inst.enable) instances;
|
||||
|
||||
plugins = import ./plugins.nix { inherit lib pkgs openclawLib enabledInstances; };
|
||||
|
||||
files = import ./files.nix {
|
||||
inherit config lib pkgs openclawLib enabledInstances plugins;
|
||||
plugins = import ./plugins.nix {
|
||||
inherit
|
||||
lib
|
||||
pkgs
|
||||
openclawLib
|
||||
enabledInstances
|
||||
;
|
||||
};
|
||||
|
||||
stripNulls = value:
|
||||
if value == null then null
|
||||
files = import ./files.nix {
|
||||
inherit
|
||||
lib
|
||||
pkgs
|
||||
openclawLib
|
||||
enabledInstances
|
||||
plugins
|
||||
;
|
||||
};
|
||||
|
||||
stripNulls =
|
||||
value:
|
||||
if value == null then
|
||||
null
|
||||
else if builtins.isAttrs value then
|
||||
lib.filterAttrs (_: v: v != null) (builtins.mapAttrs (_: stripNulls) value)
|
||||
else if builtins.isList value then
|
||||
@ -59,154 +84,253 @@ let
|
||||
};
|
||||
};
|
||||
|
||||
mkInstanceConfig = name: inst: let
|
||||
gatewayPackage =
|
||||
if inst.gatewayPath != null then
|
||||
pkgs.callPackage ../../packages/openclaw-gateway.nix {
|
||||
gatewaySrc = builtins.path {
|
||||
path = inst.gatewayPath;
|
||||
name = "openclaw-gateway-src";
|
||||
};
|
||||
pnpmDepsHash = inst.gatewayPnpmDepsHash;
|
||||
}
|
||||
else
|
||||
inst.package;
|
||||
pluginPackages = plugins.pluginPackagesFor name;
|
||||
pluginEnvAll = plugins.pluginEnvAllFor name;
|
||||
mergedConfig0 = stripNulls (lib.recursiveUpdate (lib.recursiveUpdate baseConfig cfg.config) inst.config);
|
||||
existingWorkspace = (((mergedConfig0.agents or {}).defaults or {}).workspace or null);
|
||||
mergedConfig =
|
||||
if (cfg.workspace.pinAgentDefaults or true) && existingWorkspace == null then
|
||||
lib.recursiveUpdate mergedConfig0 { agents = { defaults = { workspace = inst.workspaceDir; }; }; }
|
||||
else
|
||||
mergedConfig0;
|
||||
configJson = builtins.toJSON mergedConfig;
|
||||
configFile = pkgs.writeText "openclaw-${name}.json" configJson;
|
||||
gatewayWrapper = pkgs.writeShellScriptBin "openclaw-gateway-${name}" ''
|
||||
set -euo pipefail
|
||||
|
||||
if [ -n "${lib.makeBinPath pluginPackages}" ]; then
|
||||
export PATH="${lib.makeBinPath pluginPackages}:$PATH"
|
||||
fi
|
||||
|
||||
${lib.concatStringsSep "\n" (map (entry:
|
||||
let
|
||||
isFile = lib.hasSuffix "_FILE" entry.key;
|
||||
in ''
|
||||
if [ -f "${entry.value}" ]; then
|
||||
if ${if isFile then "true" else "false"}; then
|
||||
export ${entry.key}="${entry.value}"
|
||||
else
|
||||
rawValue="$("${lib.getExe' pkgs.coreutils "cat"}" "${entry.value}")"
|
||||
if [ "''${rawValue#${entry.key}=}" != "$rawValue" ]; then
|
||||
export ${entry.key}="''${rawValue#${entry.key}=}"
|
||||
else
|
||||
export ${entry.key}="$rawValue"
|
||||
fi
|
||||
fi
|
||||
mkInstanceConfig =
|
||||
name: inst:
|
||||
let
|
||||
gatewayPackage =
|
||||
if inst.gatewayPath != null then
|
||||
pkgs.callPackage ../../packages/openclaw-gateway.nix {
|
||||
gatewaySrc = builtins.path {
|
||||
path = inst.gatewayPath;
|
||||
name = "openclaw-gateway-src";
|
||||
};
|
||||
pnpmDepsHash = inst.gatewayPnpmDepsHash;
|
||||
}
|
||||
else
|
||||
export ${entry.key}="${entry.value}"
|
||||
fi
|
||||
'') pluginEnvAll)}
|
||||
|
||||
exec "${gatewayPackage}/bin/openclaw" "$@"
|
||||
'';
|
||||
appDefaults = lib.optionalAttrs (pkgs.stdenv.hostPlatform.isDarwin && inst.appDefaults.enable) {
|
||||
attachExistingOnly = inst.appDefaults.attachExistingOnly;
|
||||
gatewayPort = inst.gatewayPort;
|
||||
nixMode = inst.appDefaults.nixMode;
|
||||
};
|
||||
|
||||
appInstall = if !(pkgs.stdenv.hostPlatform.isDarwin && inst.app.install.enable && appPackage != null) then
|
||||
null
|
||||
else {
|
||||
name = lib.removePrefix "${homeDir}/" inst.app.install.path;
|
||||
value = {
|
||||
source = "${appPackage}/Applications/OpenClaw.app";
|
||||
recursive = true;
|
||||
force = true;
|
||||
inst.package;
|
||||
pluginPackages = plugins.pluginPackagesFor name;
|
||||
runtimePackages = lib.unique (
|
||||
openclawLib.toolSets.tools
|
||||
++ (lib.optional (qmdPackage != null) qmdPackage)
|
||||
++ pluginPackages
|
||||
++ cfg.runtimePackages
|
||||
++ inst.runtimePackages
|
||||
);
|
||||
runtimeProfile = pkgs.symlinkJoin {
|
||||
name = "openclaw-runtime-${name}";
|
||||
paths = runtimePackages;
|
||||
};
|
||||
};
|
||||
runtimePath = lib.makeBinPath runtimePackages;
|
||||
runtimeEnvAll =
|
||||
(plugins.pluginEnvAllFor name)
|
||||
++ (lib.mapAttrsToList (key: value: {
|
||||
inherit key value;
|
||||
plugin = "runtime";
|
||||
}) (cfg.environment // inst.environment));
|
||||
userConfig = stripNulls (lib.recursiveUpdate (stripNulls cfg.config) (stripNulls inst.config));
|
||||
pluginEntryConfig = plugins.openclawPluginEntriesConfigFor name;
|
||||
openclawPluginLoadPaths = plugins.openclawPluginLoadPathsFor name;
|
||||
mergedConfigWithoutLoadPaths = stripNulls (
|
||||
lib.recursiveUpdate (lib.recursiveUpdate baseConfig pluginEntryConfig) userConfig
|
||||
);
|
||||
existingOpenClawPluginLoadPaths = (
|
||||
((mergedConfigWithoutLoadPaths.plugins or { }).load or { }).paths or [ ]
|
||||
);
|
||||
mergedConfig0 =
|
||||
if openclawPluginLoadPaths == [ ] then
|
||||
mergedConfigWithoutLoadPaths
|
||||
else
|
||||
lib.recursiveUpdate mergedConfigWithoutLoadPaths {
|
||||
plugins = {
|
||||
load = {
|
||||
paths = lib.unique (openclawPluginLoadPaths ++ existingOpenClawPluginLoadPaths);
|
||||
};
|
||||
};
|
||||
};
|
||||
existingWorkspace = (((mergedConfig0.agents or { }).defaults or { }).workspace or null);
|
||||
mergedConfig =
|
||||
if (cfg.workspace.pinAgentDefaults or true) && existingWorkspace == null then
|
||||
lib.recursiveUpdate mergedConfig0 {
|
||||
agents = {
|
||||
defaults = {
|
||||
workspace = inst.workspaceDir;
|
||||
};
|
||||
};
|
||||
}
|
||||
else
|
||||
mergedConfig0;
|
||||
configJson = builtins.toJSON mergedConfig;
|
||||
configFile = pkgs.writeText "openclaw-${name}.json" configJson;
|
||||
agentIds =
|
||||
let
|
||||
agents = ((mergedConfig.agents or { }).list or [ ]);
|
||||
configured = lib.filter (id: id != null) (map (agent: agent.id or null) agents);
|
||||
in
|
||||
lib.unique ([ "main" ] ++ configured);
|
||||
codexRuntimeProfiles = map (
|
||||
agentId: "${inst.stateDir}/agents/${agentId}/agent/codex-home/home/.nix-profile"
|
||||
) agentIds;
|
||||
gatewayWrapper = pkgs.writeShellScriptBin "openclaw-gateway-${name}" ''
|
||||
set -euo pipefail
|
||||
|
||||
package = gatewayPackage;
|
||||
in {
|
||||
homeFile = {
|
||||
name = openclawLib.toRelative inst.configPath;
|
||||
value = { text = configJson; };
|
||||
};
|
||||
configFile = configFile;
|
||||
configPath = inst.configPath;
|
||||
if [ -n "${runtimePath}" ]; then
|
||||
export PATH="${runtimePath}:$PATH"
|
||||
fi
|
||||
|
||||
dirs = [ inst.stateDir inst.workspaceDir (builtins.dirOf inst.logPath) ];
|
||||
${lib.concatStringsSep "\n" (
|
||||
map (
|
||||
entry:
|
||||
let
|
||||
isFile = lib.hasSuffix "_FILE" entry.key;
|
||||
in
|
||||
''
|
||||
if [ -f "${entry.value}" ]; then
|
||||
if ${if isFile then "true" else "false"}; then
|
||||
export ${entry.key}="${entry.value}"
|
||||
else
|
||||
rawValue="$("${lib.getExe' pkgs.coreutils "cat"}" "${entry.value}")"
|
||||
if [ "''${rawValue#${entry.key}=}" != "$rawValue" ]; then
|
||||
export ${entry.key}="''${rawValue#${entry.key}=}"
|
||||
else
|
||||
export ${entry.key}="$rawValue"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
export ${entry.key}="${entry.value}"
|
||||
fi
|
||||
''
|
||||
) runtimeEnvAll
|
||||
)}
|
||||
|
||||
launchdAgent = lib.optionalAttrs (pkgs.stdenv.hostPlatform.isDarwin && inst.launchd.enable) {
|
||||
"${inst.launchd.label}" = {
|
||||
enable = true;
|
||||
config = {
|
||||
Label = inst.launchd.label;
|
||||
ProgramArguments = [
|
||||
"${gatewayWrapper}/bin/openclaw-gateway-${name}"
|
||||
"gateway"
|
||||
"--port"
|
||||
"${toString inst.gatewayPort}"
|
||||
];
|
||||
RunAtLoad = true;
|
||||
KeepAlive = true;
|
||||
WorkingDirectory = inst.stateDir;
|
||||
StandardOutPath = inst.logPath;
|
||||
StandardErrorPath = inst.logPath;
|
||||
EnvironmentVariables = {
|
||||
HOME = homeDir;
|
||||
OPENCLAW_CONFIG_PATH = inst.configPath;
|
||||
OPENCLAW_STATE_DIR = inst.stateDir;
|
||||
OPENCLAW_IMAGE_BACKEND = "sips";
|
||||
OPENCLAW_NIX_MODE = "1";
|
||||
exec "${gatewayPackage}/bin/openclaw" "$@"
|
||||
'';
|
||||
appDefaults = lib.optionalAttrs (pkgs.stdenv.hostPlatform.isDarwin && inst.appDefaults.enable) {
|
||||
attachExistingOnly = inst.appDefaults.attachExistingOnly;
|
||||
gatewayPort = inst.gatewayPort;
|
||||
nixMode = inst.appDefaults.nixMode;
|
||||
};
|
||||
|
||||
appInstall =
|
||||
if !(pkgs.stdenv.hostPlatform.isDarwin && inst.app.install.enable && appPackage != null) then
|
||||
null
|
||||
else
|
||||
{
|
||||
name = lib.removePrefix "${homeDir}/" inst.app.install.path;
|
||||
value = {
|
||||
source = "${appPackage}/Applications/OpenClaw.app";
|
||||
recursive = true;
|
||||
force = true;
|
||||
};
|
||||
};
|
||||
|
||||
package = gatewayPackage;
|
||||
in
|
||||
{
|
||||
homeFile = {
|
||||
name = openclawLib.toRelative inst.configPath;
|
||||
value = {
|
||||
text = configJson;
|
||||
force = true;
|
||||
};
|
||||
};
|
||||
configFile = configFile;
|
||||
configPath = inst.configPath;
|
||||
codexRuntimeProfiles = codexRuntimeProfiles;
|
||||
runtimeProfile = runtimeProfile;
|
||||
|
||||
dirs = [
|
||||
inst.stateDir
|
||||
inst.workspaceDir
|
||||
(builtins.dirOf inst.logPath)
|
||||
];
|
||||
|
||||
launchdAgent = lib.optionalAttrs (pkgs.stdenv.hostPlatform.isDarwin && inst.launchd.enable) {
|
||||
"${inst.launchd.label}" = {
|
||||
enable = true;
|
||||
config = {
|
||||
Label = inst.launchd.label;
|
||||
ProgramArguments = [
|
||||
"${gatewayWrapper}/bin/openclaw-gateway-${name}"
|
||||
"gateway"
|
||||
"--port"
|
||||
"${toString inst.gatewayPort}"
|
||||
];
|
||||
RunAtLoad = true;
|
||||
KeepAlive = true;
|
||||
WorkingDirectory = inst.stateDir;
|
||||
StandardOutPath = inst.logPath;
|
||||
StandardErrorPath = inst.logPath;
|
||||
EnvironmentVariables = {
|
||||
HOME = homeDir;
|
||||
OPENCLAW_CONFIG_PATH = inst.configPath;
|
||||
OPENCLAW_STATE_DIR = inst.stateDir;
|
||||
OPENCLAW_IMAGE_BACKEND = "sips";
|
||||
OPENCLAW_NIX_MODE = "1";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemdService = lib.optionalAttrs (pkgs.stdenv.hostPlatform.isLinux && inst.systemd.enable) {
|
||||
"${inst.systemd.unitName}" = {
|
||||
Unit = {
|
||||
Description = "OpenClaw gateway (${name})";
|
||||
};
|
||||
Service = {
|
||||
ExecStart = "${gatewayWrapper}/bin/openclaw-gateway-${name} gateway --port ${toString inst.gatewayPort}";
|
||||
WorkingDirectory = inst.stateDir;
|
||||
Restart = "always";
|
||||
RestartSec = "1s";
|
||||
Environment = [
|
||||
"HOME=${homeDir}"
|
||||
"OPENCLAW_CONFIG_PATH=${inst.configPath}"
|
||||
"OPENCLAW_STATE_DIR=${inst.stateDir}"
|
||||
"OPENCLAW_NIX_MODE=1"
|
||||
];
|
||||
StandardOutput = "append:${inst.logPath}";
|
||||
StandardError = "append:${inst.logPath}";
|
||||
systemdService = lib.optionalAttrs (pkgs.stdenv.hostPlatform.isLinux && inst.systemd.enable) {
|
||||
"${inst.systemd.unitName}" = {
|
||||
Unit = {
|
||||
Description = "OpenClaw gateway (${name})";
|
||||
};
|
||||
Service = {
|
||||
ExecStart = "${gatewayWrapper}/bin/openclaw-gateway-${name} gateway --port ${toString inst.gatewayPort}";
|
||||
WorkingDirectory = inst.stateDir;
|
||||
Restart = "always";
|
||||
RestartSec = "1s";
|
||||
Environment = [
|
||||
"HOME=${homeDir}"
|
||||
"OPENCLAW_CONFIG_PATH=${inst.configPath}"
|
||||
"OPENCLAW_STATE_DIR=${inst.stateDir}"
|
||||
"OPENCLAW_NIX_MODE=1"
|
||||
];
|
||||
StandardOutput = "append:${inst.logPath}";
|
||||
StandardError = "append:${inst.logPath}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
appDefaults = appDefaults;
|
||||
appInstall = appInstall;
|
||||
package = package;
|
||||
};
|
||||
appDefaults = appDefaults;
|
||||
appInstall = appInstall;
|
||||
package = package;
|
||||
launchdLabel =
|
||||
if pkgs.stdenv.hostPlatform.isDarwin && inst.launchd.enable then inst.launchd.label else null;
|
||||
};
|
||||
|
||||
instanceConfigs = lib.mapAttrsToList mkInstanceConfig enabledInstances;
|
||||
codexRuntimeProfileEntries = lib.flatten (
|
||||
map (
|
||||
item:
|
||||
map (profileDir: {
|
||||
inherit profileDir;
|
||||
binDir = "${item.runtimeProfile}/bin";
|
||||
}) item.codexRuntimeProfiles
|
||||
) instanceConfigs
|
||||
);
|
||||
codexRuntimeProfilesManifest = pkgs.writeText "openclaw-codex-runtime-profiles.tsv" (
|
||||
(lib.concatStringsSep "\n" (
|
||||
map (entry: "${entry.profileDir}\t${entry.binDir}") codexRuntimeProfileEntries
|
||||
))
|
||||
+ "\n"
|
||||
);
|
||||
appInstalls = lib.filter (item: item != null) (map (item: item.appInstall) instanceConfigs);
|
||||
launchdLabels = lib.filter (label: label != null) (map (item: item.launchdLabel) instanceConfigs);
|
||||
launchdLabelArgs = lib.concatStringsSep " " (map lib.escapeShellArg launchdLabels);
|
||||
|
||||
appDefaults = lib.foldl' (acc: item: lib.recursiveUpdate acc item.appDefaults) {} instanceConfigs;
|
||||
appDefaults = lib.foldl' (acc: item: lib.recursiveUpdate acc item.appDefaults) { } instanceConfigs;
|
||||
appDefaultsEnabled = lib.filterAttrs (_: inst: inst.appDefaults.enable) enabledInstances;
|
||||
|
||||
in {
|
||||
config = lib.mkIf (cfg.enable || cfg.instances != {}) {
|
||||
in
|
||||
{
|
||||
config = lib.mkIf (cfg.enable || cfg.instances != { }) {
|
||||
assertions = [
|
||||
{
|
||||
assertion = lib.length (lib.attrNames appDefaultsEnabled) <= 1;
|
||||
message = "Only one OpenClaw instance may enable appDefaults.";
|
||||
}
|
||||
] ++ files.documentsAssertions ++ files.skillAssertions ++ plugins.pluginAssertions ++ plugins.pluginSkillAssertions;
|
||||
]
|
||||
++ files.documentsAssertions
|
||||
++ files.duplicateSkillAssertion
|
||||
++ plugins.pluginAssertions
|
||||
++ [
|
||||
{
|
||||
assertion = !cfg.qmd.prewarmModels.enable || qmdPackage != null;
|
||||
message = "programs.openclaw.qmd.prewarmModels.enable requires a qmd package in openclawPackages.";
|
||||
}
|
||||
];
|
||||
|
||||
home.packages = lib.unique (
|
||||
(map (item: item.package) instanceConfigs)
|
||||
@ -223,8 +347,6 @@ in {
|
||||
};
|
||||
})
|
||||
(lib.listToAttrs appInstalls)
|
||||
files.documentsFiles
|
||||
files.skillFiles
|
||||
plugins.pluginConfigFiles
|
||||
(lib.optionalAttrs cfg.reloadScript.enable {
|
||||
".local/bin/openclaw-reload" = {
|
||||
@ -234,39 +356,72 @@ in {
|
||||
})
|
||||
];
|
||||
|
||||
home.activation.openclawDocumentGuard = lib.mkIf files.documentsEnabled (
|
||||
lib.hm.dag.entryBefore [ "writeBoundary" ] ''
|
||||
set -euo pipefail
|
||||
${files.documentsGuard}
|
||||
home.activation.openclawDirs = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
||||
run --quiet ${lib.getExe' pkgs.coreutils "mkdir"} -p ${
|
||||
lib.concatStringsSep " " (lib.concatMap (item: item.dirs) instanceConfigs)
|
||||
}
|
||||
${lib.optionalString (plugins.pluginStateDirsAll != [ ])
|
||||
"run --quiet ${lib.getExe' pkgs.coreutils "mkdir"} -p ${lib.concatStringsSep " " plugins.pluginStateDirsAll}"
|
||||
}
|
||||
'';
|
||||
|
||||
home.activation.openclawWorkspaceFiles = lib.mkIf (files.materializedEntries != [ ]) (
|
||||
lib.hm.dag.entryAfter [ "openclawDirs" ] ''
|
||||
run --quiet ${../openclaw-materialize-workspace-files.sh} ${lib.escapeShellArg "${homeDir}/.local/state/nix-openclaw/managed-workspace-files"} ${files.materializedManifest}
|
||||
''
|
||||
);
|
||||
|
||||
home.activation.openclawDirs = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
||||
run --quiet ${lib.getExe' pkgs.coreutils "mkdir"} -p ${lib.concatStringsSep " " (lib.concatMap (item: item.dirs) instanceConfigs)}
|
||||
${lib.optionalString (plugins.pluginStateDirsAll != []) "run --quiet ${lib.getExe' pkgs.coreutils "mkdir"} -p ${lib.concatStringsSep " " plugins.pluginStateDirsAll}"}
|
||||
home.activation.openclawConfigFiles = lib.hm.dag.entryAfter [ "openclawDirs" ] ''
|
||||
${lib.concatStringsSep "\n" (
|
||||
map (
|
||||
item: "run --quiet ${lib.getExe' pkgs.coreutils "ln"} -sfn ${item.configFile} ${item.configPath}"
|
||||
) instanceConfigs
|
||||
)}
|
||||
'';
|
||||
|
||||
home.activation.openclawConfigFiles = lib.hm.dag.entryAfter [ "openclawDirs" ] ''
|
||||
${lib.concatStringsSep "\n" (map (item: "run --quiet ${lib.getExe' pkgs.coreutils "ln"} -sfn ${item.configFile} ${item.configPath}") instanceConfigs)}
|
||||
'';
|
||||
home.activation.openclawCodexRuntimeProfiles = lib.mkIf (codexRuntimeProfileEntries != [ ]) (
|
||||
lib.hm.dag.entryAfter [ "openclawDirs" ] ''
|
||||
run --quiet ${pkgs.bash}/bin/bash ${../openclaw-link-codex-runtime-profiles.sh} ${codexRuntimeProfilesManifest}
|
||||
''
|
||||
);
|
||||
|
||||
home.activation.openclawPluginGuard = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
||||
set -euo pipefail
|
||||
${plugins.pluginGuards}
|
||||
'';
|
||||
|
||||
home.activation.openclawAppDefaults = lib.mkIf (pkgs.stdenv.hostPlatform.isDarwin && appDefaults != {}) (
|
||||
lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
||||
# Nix mode + app defaults (OpenClaw.app)
|
||||
/usr/bin/defaults write ai.openclaw.mac openclaw.nixMode -bool ${lib.boolToString (appDefaults.nixMode or true)}
|
||||
/usr/bin/defaults write ai.openclaw.mac openclaw.gateway.attachExistingOnly -bool ${lib.boolToString (appDefaults.attachExistingOnly or true)}
|
||||
/usr/bin/defaults write ai.openclaw.mac gatewayPort -int ${toString (appDefaults.gatewayPort or 18789)}
|
||||
home.activation.openclawQmdPrewarm = lib.mkIf (cfg.qmd.prewarmModels.enable && qmdPackage != null) (
|
||||
lib.hm.dag.entryAfter [ "openclawDirs" ] ''
|
||||
run --quiet ${lib.getExe' pkgs.coreutils "env"} \
|
||||
HOME=${lib.escapeShellArg homeDir} \
|
||||
XDG_CACHE_HOME=${lib.escapeShellArg "${homeDir}/.cache"} \
|
||||
XDG_CONFIG_HOME=${lib.escapeShellArg "${homeDir}/.config"} \
|
||||
XDG_DATA_HOME=${lib.escapeShellArg "${homeDir}/.local/share"} \
|
||||
OPENCLAW_QMD_BIN=${lib.escapeShellArg "${qmdPackage}/bin/qmd"} \
|
||||
${pkgs.bash}/bin/bash ${../../../scripts/openclaw-qmd-prewarm.sh}
|
||||
''
|
||||
);
|
||||
|
||||
home.activation.openclawAppDefaults =
|
||||
lib.mkIf (pkgs.stdenv.hostPlatform.isDarwin && appDefaults != { })
|
||||
(
|
||||
lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
||||
# Nix mode + app defaults (OpenClaw.app)
|
||||
/usr/bin/defaults write ai.openclaw.mac openclaw.nixMode -bool ${
|
||||
lib.boolToString (appDefaults.nixMode or true)
|
||||
}
|
||||
/usr/bin/defaults write ai.openclaw.mac openclaw.gateway.attachExistingOnly -bool ${
|
||||
lib.boolToString (appDefaults.attachExistingOnly or true)
|
||||
}
|
||||
/usr/bin/defaults write ai.openclaw.mac gatewayPort -int ${
|
||||
toString (appDefaults.gatewayPort or 18789)
|
||||
}
|
||||
''
|
||||
);
|
||||
|
||||
home.activation.openclawLaunchdRelink = lib.mkIf pkgs.stdenv.hostPlatform.isDarwin (
|
||||
lib.hm.dag.entryAfter [ "linkGeneration" ] ''
|
||||
/usr/bin/env bash ${../openclaw-launchd-relink.sh}
|
||||
/usr/bin/env bash ${../openclaw-launchd-relink.sh} ${launchdLabelArgs}
|
||||
''
|
||||
);
|
||||
|
||||
|
||||
@ -1,8 +1,21 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
imports = [
|
||||
(lib.mkRenamedOptionModule [ "programs" "openclaw" "firstParty" ] [ "programs" "openclaw" "bundledPlugins" ])
|
||||
(lib.mkRenamedOptionModule [ "programs" "openclaw" "plugins" ] [ "programs" "openclaw" "customPlugins" ])
|
||||
(lib.mkRemovedOptionModule [
|
||||
"programs"
|
||||
"openclaw"
|
||||
"firstParty"
|
||||
] "Use programs.openclaw.bundledPlugins.<name>.enable/config.")
|
||||
(lib.mkRemovedOptionModule [
|
||||
"programs"
|
||||
"openclaw"
|
||||
"plugins"
|
||||
] "Use programs.openclaw.customPlugins.")
|
||||
./options.nix
|
||||
./config.nix
|
||||
];
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
{ config, lib, pkgs, openclawLib, enabledInstances, plugins }:
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
openclawLib,
|
||||
enabledInstances,
|
||||
plugins,
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = openclawLib.cfg;
|
||||
@ -8,7 +14,8 @@ let
|
||||
documentsEnabled = cfg.documents != null;
|
||||
instanceWorkspaceDirs = map (inst: resolvePath inst.workspaceDir) (lib.attrValues enabledInstances);
|
||||
|
||||
renderSkill = skill:
|
||||
renderSkill =
|
||||
skill:
|
||||
let
|
||||
frontmatterLines = [
|
||||
"---"
|
||||
@ -24,65 +31,72 @@ let
|
||||
frontmatter = lib.concatStringsSep "\n" frontmatterLines;
|
||||
body = if skill ? body then skill.body else "";
|
||||
in
|
||||
"${frontmatter}\n\n${body}\n";
|
||||
"${frontmatter}\n\n${body}\n";
|
||||
|
||||
skillAssertions =
|
||||
duplicateSkillAssertion =
|
||||
let
|
||||
names = map (skill: skill.name) cfg.skills;
|
||||
nameCounts = lib.foldl' (acc: name: acc // { "${name}" = (acc.${name} or 0) + 1; }) {} names;
|
||||
duplicateNames = lib.attrNames (lib.filterAttrs (_: v: v > 1) nameCounts);
|
||||
targetsForInstance =
|
||||
instName: inst:
|
||||
let
|
||||
base = "${toRelative (resolvePath inst.workspaceDir)}/skills";
|
||||
userTargets = map (skill: "${base}/${skill.name}") cfg.skills;
|
||||
pluginsForInstance = plugins.resolvedPluginsByInstance.${instName} or [ ];
|
||||
pluginTargets = lib.flatten (
|
||||
map (p: map (skillPath: "${base}/${builtins.baseNameOf skillPath}") p.skills) pluginsForInstance
|
||||
);
|
||||
in
|
||||
userTargets ++ pluginTargets;
|
||||
skillTargets = lib.flatten (lib.mapAttrsToList targetsForInstance enabledInstances);
|
||||
counts = lib.foldl' (acc: path: acc // { "${path}" = (acc.${path} or 0) + 1; }) { } skillTargets;
|
||||
duplicates = lib.attrNames (lib.filterAttrs (_: v: v > 1) counts);
|
||||
in
|
||||
if duplicateNames == [] then [] else [
|
||||
if duplicates == [ ] then
|
||||
[ ]
|
||||
else
|
||||
[
|
||||
{
|
||||
assertion = false;
|
||||
message = "programs.openclaw.skills has duplicate names: ${lib.concatStringsSep ", " duplicateNames}";
|
||||
message = "Duplicate skill paths detected: ${lib.concatStringsSep ", " duplicates}";
|
||||
}
|
||||
];
|
||||
|
||||
skillFiles =
|
||||
skillEntries =
|
||||
let
|
||||
entriesForInstance = instName: inst:
|
||||
entriesForInstance =
|
||||
instName: inst:
|
||||
let
|
||||
base = "${toRelative (resolvePath inst.workspaceDir)}/skills";
|
||||
entryFor = skill:
|
||||
entryFor =
|
||||
skill:
|
||||
let
|
||||
mode = skill.mode or "symlink";
|
||||
source = if skill ? source && skill.source != null then resolvePath skill.source else null;
|
||||
in
|
||||
if mode == "inline" then
|
||||
{
|
||||
name = "${base}/${skill.name}/SKILL.md";
|
||||
value = { text = renderSkill skill; };
|
||||
}
|
||||
else if mode == "copy" then
|
||||
{
|
||||
name = "${base}/${skill.name}";
|
||||
value = {
|
||||
source = builtins.path {
|
||||
name = "openclaw-skill-${skill.name}";
|
||||
path = source;
|
||||
};
|
||||
recursive = true;
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
name = "${base}/${skill.name}";
|
||||
value = {
|
||||
source = config.lib.file.mkOutOfStoreSymlink source;
|
||||
recursive = true;
|
||||
};
|
||||
if mode == "inline" then
|
||||
{
|
||||
source = pkgs.writeText "openclaw-skill-${skill.name}.md" (renderSkill skill);
|
||||
target = "${resolvePath inst.workspaceDir}/skills/${skill.name}/SKILL.md";
|
||||
}
|
||||
else if mode == "copy" || mode == "symlink" then
|
||||
{
|
||||
source = builtins.path {
|
||||
name = "openclaw-skill-${skill.name}";
|
||||
path = source;
|
||||
};
|
||||
pluginEntriesFor = p:
|
||||
target = "${resolvePath inst.workspaceDir}/skills/${skill.name}";
|
||||
}
|
||||
else
|
||||
throw "Unsupported OpenClaw skill mode: ${mode}";
|
||||
pluginEntriesFor =
|
||||
p:
|
||||
map (skillPath: {
|
||||
name = "${base}/${builtins.baseNameOf skillPath}";
|
||||
value = { source = skillPath; recursive = true; };
|
||||
source = skillPath;
|
||||
target = "${resolvePath inst.workspaceDir}/skills/${builtins.baseNameOf skillPath}";
|
||||
}) p.skills;
|
||||
pluginsForInstance = plugins.resolvedPluginsByInstance.${instName} or [];
|
||||
pluginsForInstance = plugins.resolvedPluginsByInstance.${instName} or [ ];
|
||||
in
|
||||
(map entryFor cfg.skills) ++ (lib.flatten (map pluginEntriesFor pluginsForInstance));
|
||||
(map entryFor cfg.skills) ++ (lib.flatten (map pluginEntriesFor pluginsForInstance));
|
||||
in
|
||||
lib.listToAttrs (lib.flatten (lib.mapAttrsToList entriesForInstance enabledInstances));
|
||||
lib.flatten (lib.mapAttrsToList entriesForInstance enabledInstances);
|
||||
|
||||
documentsRequiredFiles = [
|
||||
"AGENTS.md"
|
||||
@ -103,9 +117,9 @@ let
|
||||
let
|
||||
extra = lib.filter (file: builtins.pathExists (cfg.documents + "/${file}")) documentsOptionalFiles;
|
||||
in
|
||||
documentsRequiredFiles ++ extra
|
||||
documentsRequiredFiles ++ extra
|
||||
else
|
||||
[];
|
||||
[ ];
|
||||
|
||||
documentsAssertions = lib.optionals documentsEnabled [
|
||||
{
|
||||
@ -126,77 +140,92 @@ let
|
||||
}
|
||||
];
|
||||
|
||||
documentsGuard =
|
||||
lib.optionalString documentsEnabled (
|
||||
let
|
||||
guardLine = file: ''
|
||||
if [ -e "${file}" ] && [ ! -L "${file}" ]; then
|
||||
echo "OpenClaw documents are managed by Nix. Please adopt ${file} into your documents directory and re-run." >&2
|
||||
exit 1
|
||||
fi
|
||||
'';
|
||||
guardForDir = dir: ''
|
||||
${lib.concatStringsSep "\n" (map (name: guardLine "${dir}/${name}") documentsFileNames)}
|
||||
'';
|
||||
in
|
||||
lib.concatStringsSep "\n" (map guardForDir instanceWorkspaceDirs)
|
||||
);
|
||||
|
||||
toolsReport =
|
||||
if documentsEnabled then
|
||||
let
|
||||
toolNames = toolSets.toolNames or [];
|
||||
renderPkgName = pkg:
|
||||
if pkg ? pname then pkg.pname else lib.getName pkg;
|
||||
renderPlugin = plugin:
|
||||
let
|
||||
pkgNames = map renderPkgName (lib.filter (p: p != null) plugin.packages);
|
||||
pkgSuffix =
|
||||
if pkgNames == []
|
||||
then ""
|
||||
else " — " + (lib.concatStringsSep ", " pkgNames);
|
||||
in
|
||||
"- " + plugin.name + pkgSuffix + " (" + plugin.source + ")";
|
||||
pluginLinesFor = instName: inst:
|
||||
let
|
||||
pluginsForInstance = plugins.resolvedPluginsByInstance.${instName} or [];
|
||||
lines = if pluginsForInstance == [] then [ "- (none)" ] else map renderPlugin pluginsForInstance;
|
||||
in
|
||||
[
|
||||
""
|
||||
"### Instance: ${instName}"
|
||||
] ++ lines;
|
||||
reportLines =
|
||||
renderPkgName = pkg: if pkg ? pname then pkg.pname else lib.getName pkg;
|
||||
renderPkgCommand =
|
||||
pkg:
|
||||
let
|
||||
pkgName = renderPkgName pkg;
|
||||
commandName = pkg.meta.mainProgram or pkgName;
|
||||
in
|
||||
if commandName == pkgName then commandName else "${commandName} (${pkgName})";
|
||||
toolPackages = lib.filter (p: p != null) (toolSets.tools or [ ]);
|
||||
renderPlugin =
|
||||
plugin:
|
||||
let
|
||||
pkgNames = map renderPkgCommand (lib.filter (p: p != null) plugin.packages);
|
||||
pkgSuffix = if pkgNames == [ ] then "" else " — " + (lib.concatStringsSep ", " pkgNames);
|
||||
in
|
||||
"- " + plugin.name + pkgSuffix + " (" + plugin.source + ")";
|
||||
renderPkgList =
|
||||
packages:
|
||||
let
|
||||
actualPackages = lib.filter (p: p != null) packages;
|
||||
in
|
||||
if actualPackages == [ ] then
|
||||
[ "- (none)" ]
|
||||
else
|
||||
map (pkg: "- " + renderPkgCommand pkg) actualPackages;
|
||||
pluginLinesFor =
|
||||
instName: inst:
|
||||
let
|
||||
pluginsForInstance = plugins.resolvedPluginsByInstance.${instName} or [ ];
|
||||
pluginLines =
|
||||
if pluginsForInstance == [ ] then [ "- (none)" ] else map renderPlugin pluginsForInstance;
|
||||
runtimePackages = lib.unique (
|
||||
(lib.optional (openclawLib.qmdPackage != null) openclawLib.qmdPackage)
|
||||
++ (cfg.runtimePackages or [ ])
|
||||
++ (inst.runtimePackages or [ ])
|
||||
);
|
||||
in
|
||||
[
|
||||
"<!-- BEGIN NIX-REPORT -->"
|
||||
""
|
||||
"## Nix-managed tools"
|
||||
""
|
||||
"### Built-in toolchain"
|
||||
"### Instance: ${instName}"
|
||||
]
|
||||
++ (if toolNames == [] then [ "- (none)" ] else map (name: "- " + name) toolNames)
|
||||
++ [
|
||||
""
|
||||
"## Nix-managed plugin report"
|
||||
""
|
||||
"Plugins enabled per instance (last-wins on name collisions):"
|
||||
"Plugins:"
|
||||
]
|
||||
++ lib.concatLists (lib.mapAttrsToList pluginLinesFor enabledInstances)
|
||||
++ pluginLines
|
||||
++ [
|
||||
""
|
||||
"Tools: batteries-included toolchain + plugin-provided CLIs."
|
||||
""
|
||||
"<!-- END NIX-REPORT -->"
|
||||
];
|
||||
"Runtime packages:"
|
||||
]
|
||||
++ renderPkgList runtimePackages;
|
||||
reportLines = [
|
||||
"<!-- BEGIN NIX-REPORT -->"
|
||||
""
|
||||
"## Nix-managed tools"
|
||||
""
|
||||
"### Built-in toolchain"
|
||||
]
|
||||
++ (
|
||||
if toolPackages == [ ] then [ "- (none)" ] else map (pkg: "- " + renderPkgCommand pkg) toolPackages
|
||||
)
|
||||
++ [
|
||||
""
|
||||
"## Nix-managed plugin report"
|
||||
""
|
||||
"Plugins enabled per instance (last-wins on name collisions):"
|
||||
]
|
||||
++ lib.concatLists (lib.mapAttrsToList pluginLinesFor enabledInstances)
|
||||
++ [
|
||||
""
|
||||
"Tools: batteries-included toolchain + runtime packages + plugin-provided CLIs."
|
||||
""
|
||||
"<!-- END NIX-REPORT -->"
|
||||
];
|
||||
reportText = lib.concatStringsSep "\n" reportLines;
|
||||
in
|
||||
pkgs.writeText "openclaw-tools-report.md" reportText
|
||||
pkgs.writeText "openclaw-tools-report.md" reportText
|
||||
else
|
||||
null;
|
||||
|
||||
toolsWithReport =
|
||||
if documentsEnabled then
|
||||
pkgs.runCommand "openclaw-tools-with-report.md" {} ''
|
||||
pkgs.runCommand "openclaw-tools-with-report.md" { } ''
|
||||
cat ${cfg.documents + "/TOOLS.md"} > $out
|
||||
echo "" >> $out
|
||||
cat ${toolsReport} >> $out
|
||||
@ -204,30 +233,39 @@ let
|
||||
else
|
||||
null;
|
||||
|
||||
documentsFiles =
|
||||
documentEntries =
|
||||
if documentsEnabled then
|
||||
let
|
||||
mkDocFiles = dir:
|
||||
mkDocFiles =
|
||||
dir:
|
||||
let
|
||||
mkDoc = name: {
|
||||
name = toRelative (dir + "/${name}");
|
||||
value = {
|
||||
source = if name == "TOOLS.md" then toolsWithReport else cfg.documents + "/${name}";
|
||||
};
|
||||
source = if name == "TOOLS.md" then toolsWithReport else cfg.documents + "/${name}";
|
||||
target = dir + "/${name}";
|
||||
};
|
||||
in
|
||||
lib.listToAttrs (map mkDoc documentsFileNames);
|
||||
map mkDoc documentsFileNames;
|
||||
in
|
||||
lib.mkMerge (map mkDocFiles instanceWorkspaceDirs)
|
||||
lib.flatten (map mkDocFiles instanceWorkspaceDirs)
|
||||
else
|
||||
{};
|
||||
[ ];
|
||||
|
||||
in {
|
||||
materializedEntries = documentEntries ++ skillEntries;
|
||||
materializedManifest =
|
||||
let
|
||||
renderEntry = entry: "${entry.source}\t${entry.target}";
|
||||
in
|
||||
pkgs.writeText "openclaw-workspace-files.tsv" (
|
||||
(lib.concatStringsSep "\n" (map renderEntry materializedEntries)) + "\n"
|
||||
);
|
||||
|
||||
in
|
||||
{
|
||||
inherit
|
||||
documentsEnabled
|
||||
documentsAssertions
|
||||
documentsGuard
|
||||
documentsFiles
|
||||
skillAssertions
|
||||
skillFiles;
|
||||
materializedManifest
|
||||
materializedEntries
|
||||
duplicateSkillAssertion
|
||||
;
|
||||
}
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
{ config, lib, pkgs }:
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.programs.openclaw;
|
||||
@ -9,59 +13,53 @@ let
|
||||
toolNamesOverride = cfg.toolNames;
|
||||
excludeToolNames = effectiveExcludeTools;
|
||||
};
|
||||
toolOverridesEnabled = cfg.toolNames != null || effectiveExcludeTools != [];
|
||||
toolOverridesEnabled = cfg.toolNames != null || effectiveExcludeTools != [ ];
|
||||
overlayPackage = pkgs.openclaw or null;
|
||||
toolSets = import ../../../tools/extended.nix ({ inherit pkgs; } // toolOverrides);
|
||||
defaultPackage =
|
||||
if toolOverridesEnabled && cfg.package == pkgs.openclaw
|
||||
then (pkgs.openclawPackages.withTools toolOverrides).openclaw
|
||||
else cfg.package;
|
||||
if toolOverridesEnabled && overlayPackage != null && cfg.package == overlayPackage then
|
||||
(pkgs.openclawPackages.withTools toolOverrides).openclaw
|
||||
else
|
||||
cfg.package;
|
||||
appPackage = if cfg.appPackage != null then cfg.appPackage else defaultPackage;
|
||||
qmdPackage = pkgs.openclawPackages.qmd or null;
|
||||
generatedConfigOptions = import ../../../generated/openclaw-config-options.nix { lib = lib; };
|
||||
pluginCatalog = import ./plugin-catalog.nix;
|
||||
|
||||
bundledPluginSources = let
|
||||
stepieteRev = "983210e3b6e9285780e87f48ce9354b51a270e95";
|
||||
stepieteNarHash = "sha256-fY8t41kMSHu2ovf89mIdvC7vkceroCwKxw/MKVn4rsE=";
|
||||
stepiete = tool:
|
||||
"github:openclaw/nix-steipete-tools?dir=tools/${tool}&rev=${stepieteRev}&narHash=${stepieteNarHash}";
|
||||
in {
|
||||
summarize = stepiete "summarize";
|
||||
peekaboo = stepiete "peekaboo";
|
||||
oracle = stepiete "oracle";
|
||||
poltergeist = stepiete "poltergeist";
|
||||
sag = stepiete "sag";
|
||||
camsnap = stepiete "camsnap";
|
||||
gogcli = stepiete "gogcli";
|
||||
goplaces = stepiete "goplaces";
|
||||
bird = stepiete "bird";
|
||||
sonoscli = stepiete "sonoscli";
|
||||
imsg = stepiete "imsg";
|
||||
};
|
||||
|
||||
bundledPlugins = lib.filter (p: p != null) (lib.mapAttrsToList (name: source:
|
||||
bundledPluginSources =
|
||||
let
|
||||
pluginCfg = cfg.bundledPlugins.${name};
|
||||
openclawToolsRev = "4c1cee3c7eaf68f9de0f756be1484534f5bb5f34";
|
||||
openclawToolsNarHash = "sha256-tXWkN1VnwFG8XlRqW/e7VwbKnUfyU9tB7YDm9QHJXTY=";
|
||||
openclawTools =
|
||||
tool:
|
||||
"github:openclaw/nix-openclaw-tools?dir=tools/${tool}&rev=${openclawToolsRev}&narHash=${openclawToolsNarHash}";
|
||||
in
|
||||
if (pluginCfg.enable or false) then {
|
||||
inherit source;
|
||||
config = pluginCfg.config or {};
|
||||
} else null
|
||||
) bundledPluginSources);
|
||||
lib.mapAttrs (_name: plugin: plugin.source or (openclawTools plugin.tool)) pluginCatalog;
|
||||
|
||||
bundledPlugins = lib.filter (p: p != null) (
|
||||
lib.mapAttrsToList (
|
||||
name: source:
|
||||
let
|
||||
pluginCfg = cfg.bundledPlugins.${name};
|
||||
in
|
||||
if (pluginCfg.enable or false) then
|
||||
{
|
||||
inherit source;
|
||||
config = pluginCfg.config or { };
|
||||
}
|
||||
else
|
||||
null
|
||||
) bundledPluginSources
|
||||
);
|
||||
|
||||
effectivePlugins = cfg.customPlugins ++ bundledPlugins;
|
||||
|
||||
resolvePath = p:
|
||||
if lib.hasPrefix "~/" p then
|
||||
"${homeDir}/${lib.removePrefix "~/" p}"
|
||||
else
|
||||
p;
|
||||
resolvePath = p: if lib.hasPrefix "~/" p then "${homeDir}/${lib.removePrefix "~/" p}" else p;
|
||||
|
||||
toRelative = p:
|
||||
if lib.hasPrefix "${homeDir}/" p then
|
||||
lib.removePrefix "${homeDir}/" p
|
||||
else
|
||||
p;
|
||||
toRelative = p: if lib.hasPrefix "${homeDir}/" p then lib.removePrefix "${homeDir}/" p else p;
|
||||
|
||||
in {
|
||||
in
|
||||
{
|
||||
inherit
|
||||
cfg
|
||||
homeDir
|
||||
@ -70,10 +68,12 @@ in {
|
||||
toolSets
|
||||
defaultPackage
|
||||
appPackage
|
||||
qmdPackage
|
||||
generatedConfigOptions
|
||||
bundledPluginSources
|
||||
bundledPlugins
|
||||
effectivePlugins
|
||||
resolvePath
|
||||
toRelative;
|
||||
toRelative
|
||||
;
|
||||
}
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
{ lib, openclawLib }:
|
||||
{
|
||||
lib,
|
||||
openclawLib,
|
||||
pluginOptionType,
|
||||
}:
|
||||
|
||||
{ name, config, ... }:
|
||||
{
|
||||
@ -17,9 +21,11 @@
|
||||
|
||||
stateDir = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = if name == "default"
|
||||
then "${openclawLib.homeDir}/.openclaw"
|
||||
else "${openclawLib.homeDir}/.openclaw-${name}";
|
||||
default =
|
||||
if name == "default" then
|
||||
"${openclawLib.homeDir}/.openclaw"
|
||||
else
|
||||
"${openclawLib.homeDir}/.openclaw-${name}";
|
||||
description = "State directory for this OpenClaw instance (logs, sessions, config).";
|
||||
};
|
||||
|
||||
@ -37,9 +43,11 @@
|
||||
|
||||
logPath = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = if name == "default"
|
||||
then "/tmp/openclaw/openclaw-gateway.log"
|
||||
else "/tmp/openclaw/openclaw-gateway-${name}.log";
|
||||
default =
|
||||
if name == "default" then
|
||||
"/tmp/openclaw/openclaw-gateway.log"
|
||||
else
|
||||
"/tmp/openclaw/openclaw-gateway-${name}.log";
|
||||
description = "Log path for this OpenClaw gateway instance.";
|
||||
};
|
||||
|
||||
@ -61,27 +69,27 @@
|
||||
description = "pnpmDeps hash for local gateway builds (omit to let Nix suggest the correct hash).";
|
||||
};
|
||||
|
||||
runtimePackages = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.package;
|
||||
default = [ ];
|
||||
description = "Extra packages visible to this OpenClaw instance and its isolated Codex harness only. These are not added to the user's PATH.";
|
||||
};
|
||||
|
||||
environment = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.str;
|
||||
default = { };
|
||||
description = "Extra runtime environment for this OpenClaw gateway wrapper. Values that point to files are read at runtime unless the variable name ends in _FILE.";
|
||||
};
|
||||
|
||||
plugins = lib.mkOption {
|
||||
type = lib.types.listOf (lib.types.submodule {
|
||||
options = {
|
||||
source = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Plugin source pointer (e.g., github:owner/repo or path:/...).";
|
||||
};
|
||||
config = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = {};
|
||||
description = "Plugin-specific configuration (env/files/etc).";
|
||||
};
|
||||
};
|
||||
});
|
||||
type = lib.types.listOf pluginOptionType;
|
||||
default = openclawLib.effectivePlugins;
|
||||
description = "Plugins enabled for this instance (includes first-party toggles).";
|
||||
description = "Plugins enabled for this instance (includes bundled plugin toggles).";
|
||||
};
|
||||
|
||||
config = lib.mkOption {
|
||||
type = lib.types.submodule { options = openclawLib.generatedConfigOptions; };
|
||||
default = {};
|
||||
default = { };
|
||||
description = "OpenClaw config (schema-typed).";
|
||||
};
|
||||
|
||||
@ -93,9 +101,11 @@
|
||||
|
||||
launchd.label = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = if name == "default"
|
||||
then "com.steipete.openclaw.gateway"
|
||||
else "com.steipete.openclaw.gateway.${name}";
|
||||
default =
|
||||
if name == "default" then
|
||||
"com.steipete.openclaw.gateway"
|
||||
else
|
||||
"com.steipete.openclaw.gateway.${name}";
|
||||
description = "launchd label for this instance.";
|
||||
};
|
||||
|
||||
@ -107,9 +117,7 @@
|
||||
|
||||
systemd.unitName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = if name == "default"
|
||||
then "openclaw-gateway"
|
||||
else "openclaw-gateway-${name}";
|
||||
default = if name == "default" then "openclaw-gateway" else "openclaw-gateway-${name}";
|
||||
description = "systemd user service unit name for this instance.";
|
||||
};
|
||||
|
||||
|
||||
@ -1,8 +1,42 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
openclawLib = import ./lib.nix { inherit config lib pkgs; };
|
||||
instanceModule = import ./options-instance.nix { inherit lib openclawLib; };
|
||||
pluginOptionType = lib.types.submodule {
|
||||
options = {
|
||||
source = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Plugin source. Use a plugin flake source (github:/path:) or an OpenClaw npm install source (npm:@scope/package@version).";
|
||||
};
|
||||
config = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
description = "Nix capability plugin configuration (env/files/etc). Runtime OpenClaw plugin config belongs under programs.openclaw.config.plugins.entries.<id>.config.";
|
||||
};
|
||||
id = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
description = "OpenClaw runtime plugin id. Required for npm: sources so Nix can enable the plugin without build-time introspection.";
|
||||
};
|
||||
enabled = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Default enabled state for an OpenClaw runtime plugin entry.";
|
||||
};
|
||||
hash = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = lib.fakeHash;
|
||||
description = "Recursive output hash for npm: runtime plugin sources. Use the hash Nix reports when this is left as lib.fakeHash.";
|
||||
};
|
||||
};
|
||||
};
|
||||
instanceModule = import ./options-instance.nix { inherit lib openclawLib pluginOptionType; };
|
||||
pluginCatalog = import ./plugin-catalog.nix;
|
||||
mkSkillOption = lib.types.submodule {
|
||||
options = {
|
||||
name = lib.mkOption {
|
||||
@ -30,7 +64,11 @@ let
|
||||
description = "Optional openclaw metadata for the skill frontmatter.";
|
||||
};
|
||||
mode = lib.mkOption {
|
||||
type = lib.types.enum [ "symlink" "copy" "inline" ];
|
||||
type = lib.types.enum [
|
||||
"symlink"
|
||||
"copy"
|
||||
"inline"
|
||||
];
|
||||
default = "symlink";
|
||||
description = "Install mode for the skill (symlink/copy/inline).";
|
||||
};
|
||||
@ -42,7 +80,8 @@ let
|
||||
};
|
||||
};
|
||||
|
||||
in {
|
||||
in
|
||||
{
|
||||
options.programs.openclaw = {
|
||||
enable = lib.mkEnableOption "OpenClaw (batteries-included)";
|
||||
|
||||
@ -60,7 +99,7 @@ in {
|
||||
|
||||
excludeTools = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [];
|
||||
default = [ ];
|
||||
description = "Tool names to remove from the built-in toolchain.";
|
||||
};
|
||||
|
||||
@ -96,6 +135,18 @@ in {
|
||||
};
|
||||
};
|
||||
|
||||
runtimePackages = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.package;
|
||||
default = [ ];
|
||||
description = "Extra packages visible to the OpenClaw gateway and isolated Codex harness only. These are not added to the user's PATH.";
|
||||
};
|
||||
|
||||
environment = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.str;
|
||||
default = { };
|
||||
description = "Extra runtime environment for OpenClaw gateway wrappers. Values that point to files are read at runtime unless the variable name ends in _FILE.";
|
||||
};
|
||||
|
||||
documents = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
@ -104,54 +155,28 @@ in {
|
||||
|
||||
skills = lib.mkOption {
|
||||
type = lib.types.listOf mkSkillOption;
|
||||
default = [];
|
||||
default = [ ];
|
||||
description = "Declarative skills installed into each instance workspace.";
|
||||
};
|
||||
|
||||
customPlugins = lib.mkOption {
|
||||
type = lib.types.listOf (lib.types.submodule {
|
||||
options = {
|
||||
source = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Plugin source pointer (e.g., github:owner/repo or path:/...).";
|
||||
};
|
||||
config = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = {};
|
||||
description = "Plugin-specific configuration (env/files/etc).";
|
||||
};
|
||||
};
|
||||
});
|
||||
default = [];
|
||||
description = "Custom/community plugins (merged with bundled plugin toggles).";
|
||||
type = lib.types.listOf pluginOptionType;
|
||||
default = [ ];
|
||||
description = "Custom/community plugins (merged with bundled plugin toggles). Flake sources provide Nix capability plugins; npm: sources provide OpenClaw runtime plugins.";
|
||||
};
|
||||
|
||||
bundledPlugins = let
|
||||
mkPlugin = { name, defaultEnable ? false }: {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = defaultEnable;
|
||||
description = "Enable the ${name} plugin (bundled).";
|
||||
};
|
||||
config = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = {};
|
||||
description = "Bundled plugin configuration passed through to ${name} (env/settings).";
|
||||
};
|
||||
bundledPlugins = lib.mapAttrs (name: plugin: {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = plugin.defaultEnable or false;
|
||||
description = "Enable the ${name} plugin (bundled).";
|
||||
};
|
||||
in {
|
||||
summarize = mkPlugin { name = "summarize"; };
|
||||
peekaboo = mkPlugin { name = "peekaboo"; };
|
||||
oracle = mkPlugin { name = "oracle"; };
|
||||
poltergeist = mkPlugin { name = "poltergeist"; };
|
||||
sag = mkPlugin { name = "sag"; };
|
||||
camsnap = mkPlugin { name = "camsnap"; };
|
||||
gogcli = mkPlugin { name = "gogcli"; };
|
||||
goplaces = mkPlugin { name = "goplaces"; defaultEnable = true; };
|
||||
bird = mkPlugin { name = "bird"; };
|
||||
sonoscli = mkPlugin { name = "sonoscli"; };
|
||||
imsg = mkPlugin { name = "imsg"; };
|
||||
};
|
||||
config = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
description = "Bundled plugin configuration passed through to ${name} (env/settings).";
|
||||
};
|
||||
}) pluginCatalog;
|
||||
|
||||
launchd.enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
@ -179,16 +204,22 @@ in {
|
||||
|
||||
instances = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.submodule instanceModule);
|
||||
default = {};
|
||||
default = { };
|
||||
description = "Named OpenClaw instances (prod/test).";
|
||||
};
|
||||
|
||||
exposePluginPackages = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
default = false;
|
||||
description = "Add plugin packages to home.packages so CLIs are on PATH.";
|
||||
};
|
||||
|
||||
qmd.prewarmModels.enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = "Download/check QMD's default GGUF models during Home Manager activation. This uses about 2.25GB under the user's QMD cache.";
|
||||
};
|
||||
|
||||
reloadScript = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
@ -199,7 +230,7 @@ in {
|
||||
|
||||
config = lib.mkOption {
|
||||
type = lib.types.submodule { options = openclawLib.generatedConfigOptions; };
|
||||
default = {};
|
||||
default = { };
|
||||
description = "OpenClaw config (schema-typed).";
|
||||
};
|
||||
};
|
||||
|
||||
74
nix/modules/home-manager/openclaw/plugin-catalog.nix
Normal file
74
nix/modules/home-manager/openclaw/plugin-catalog.nix
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
summarize = {
|
||||
tool = "summarize";
|
||||
description = "Summarize URLs, PDFs, YouTube videos";
|
||||
linux = true;
|
||||
};
|
||||
|
||||
discrawl = {
|
||||
tool = "discrawl";
|
||||
description = "Archive and search Discord history";
|
||||
linux = true;
|
||||
};
|
||||
|
||||
wacrawl = {
|
||||
tool = "wacrawl";
|
||||
description = "Archive and search WhatsApp Desktop history";
|
||||
linux = true;
|
||||
};
|
||||
|
||||
peekaboo = {
|
||||
tool = "peekaboo";
|
||||
description = "Screenshot your screen";
|
||||
linux = false;
|
||||
};
|
||||
|
||||
poltergeist = {
|
||||
tool = "poltergeist";
|
||||
description = "File watching and automation";
|
||||
linux = false;
|
||||
};
|
||||
|
||||
sag = {
|
||||
tool = "sag";
|
||||
description = "Text-to-speech";
|
||||
linux = true;
|
||||
};
|
||||
|
||||
camsnap = {
|
||||
tool = "camsnap";
|
||||
description = "Take photos from connected cameras";
|
||||
linux = true;
|
||||
};
|
||||
|
||||
gogcli = {
|
||||
tool = "gogcli";
|
||||
description = "Google Calendar integration";
|
||||
linux = true;
|
||||
};
|
||||
|
||||
goplaces = {
|
||||
tool = "goplaces";
|
||||
description = "Google Places API (New) CLI";
|
||||
defaultEnable = true;
|
||||
linux = true;
|
||||
};
|
||||
|
||||
qmd = {
|
||||
tool = "qmd";
|
||||
description = "Search local markdown knowledge bases";
|
||||
linux = true;
|
||||
};
|
||||
|
||||
sonoscli = {
|
||||
tool = "sonoscli";
|
||||
description = "Control Sonos speakers";
|
||||
linux = true;
|
||||
};
|
||||
|
||||
imsg = {
|
||||
tool = "imsg";
|
||||
description = "Send/read iMessages";
|
||||
linux = false;
|
||||
};
|
||||
}
|
||||
@ -1,185 +1,280 @@
|
||||
{ lib, pkgs, openclawLib, enabledInstances }:
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
openclawLib,
|
||||
enabledInstances,
|
||||
}:
|
||||
|
||||
let
|
||||
resolvePath = openclawLib.resolvePath;
|
||||
toRelative = openclawLib.toRelative;
|
||||
mkNpmRuntimePlugin = pkgs.callPackage ../../../lib/npm-runtime-plugin.nix { };
|
||||
|
||||
resolvePlugin = plugin: let
|
||||
flake = builtins.getFlake plugin.source;
|
||||
system = pkgs.stdenv.hostPlatform.system;
|
||||
openclawPluginRaw =
|
||||
if flake ? openclawPlugin then flake.openclawPlugin
|
||||
else throw "openclawPlugin missing in ${plugin.source}";
|
||||
openclawPlugin =
|
||||
if builtins.isFunction openclawPluginRaw
|
||||
then openclawPluginRaw system
|
||||
else openclawPluginRaw;
|
||||
resolvedPlugin =
|
||||
if openclawPlugin == null
|
||||
then throw "openclawPlugin is null in ${plugin.source} for ${system}"
|
||||
else openclawPlugin;
|
||||
needs = resolvedPlugin.needs or {};
|
||||
in {
|
||||
source = plugin.source;
|
||||
name = resolvedPlugin.name or (throw "openclawPlugin.name missing in ${plugin.source}");
|
||||
skills = resolvedPlugin.skills or [];
|
||||
packages = resolvedPlugin.packages or [];
|
||||
needs = {
|
||||
stateDirs = needs.stateDirs or [];
|
||||
requiredEnv = needs.requiredEnv or [];
|
||||
};
|
||||
config = plugin.config or {};
|
||||
};
|
||||
|
||||
resolvedPluginsByInstance =
|
||||
lib.mapAttrs (instName: inst:
|
||||
let
|
||||
resolved = map resolvePlugin inst.plugins;
|
||||
counts = lib.foldl' (acc: p:
|
||||
acc // { "${p.name}" = (acc.${p.name} or 0) + 1; }
|
||||
) {} resolved;
|
||||
duplicates = lib.attrNames (lib.filterAttrs (_: v: v > 1) counts);
|
||||
byName = lib.foldl' (acc: p: acc // { "${p.name}" = p; }) {} resolved;
|
||||
ordered = lib.attrValues byName;
|
||||
in
|
||||
if duplicates == []
|
||||
then ordered
|
||||
else lib.warn
|
||||
"programs.openclaw.instances.${instName}: duplicate plugin names detected (${lib.concatStringsSep ", " duplicates}); last entry wins."
|
||||
ordered
|
||||
) enabledInstances;
|
||||
|
||||
pluginPackagesFor = instName:
|
||||
lib.flatten (map (p: p.packages) (resolvedPluginsByInstance.${instName} or []));
|
||||
|
||||
pluginPackagesAll =
|
||||
lib.flatten (map pluginPackagesFor (lib.attrNames enabledInstances));
|
||||
|
||||
pluginStateDirsFor = instName:
|
||||
normalizeOpenClawPlugin =
|
||||
pluginSource: name: entry:
|
||||
let
|
||||
dirs = lib.flatten (map (p: p.needs.stateDirs) (resolvedPluginsByInstance.${instName} or []));
|
||||
id = entry.id or (throw "openclawPlugin ${name}: plugins entry missing id");
|
||||
path = entry.path or (throw "openclawPlugin ${name}: plugins.${id} missing path");
|
||||
enabled =
|
||||
if entry ? enable && !(entry ? enabled) then
|
||||
throw "openclawPlugin ${name}: plugins.${id}.enable is not supported; use enabled"
|
||||
else if entry ? enabled then
|
||||
if builtins.isBool entry.enabled then
|
||||
entry.enabled
|
||||
else
|
||||
throw "openclawPlugin ${name}: plugins.${id}.enabled must be a boolean"
|
||||
else
|
||||
true;
|
||||
in
|
||||
map (dir: resolvePath ("~/" + dir)) dirs;
|
||||
{
|
||||
inherit id path enabled;
|
||||
source = pluginSource;
|
||||
plugin = name;
|
||||
};
|
||||
|
||||
pluginStateDirsAll =
|
||||
lib.flatten (map pluginStateDirsFor (lib.attrNames enabledInstances));
|
||||
|
||||
pluginEnvFor = instName:
|
||||
resolveNpmRuntimePlugin =
|
||||
plugin:
|
||||
let
|
||||
entries = resolvedPluginsByInstance.${instName} or [];
|
||||
toPairs = p:
|
||||
id = plugin.id or (throw "OpenClaw npm runtime plugin ${plugin.source} requires id");
|
||||
path = mkNpmRuntimePlugin {
|
||||
inherit id;
|
||||
source = plugin.source;
|
||||
hash = plugin.hash or lib.fakeHash;
|
||||
};
|
||||
in
|
||||
if (plugin.config or { }) != { } then
|
||||
throw "OpenClaw npm runtime plugin ${plugin.source} must put runtime config under programs.openclaw.config.plugins.entries.${id}.config, not customPlugins.config"
|
||||
else
|
||||
{
|
||||
source = plugin.source;
|
||||
name = id;
|
||||
skills = [ ];
|
||||
packages = [ ];
|
||||
plugins = [
|
||||
{
|
||||
inherit id path;
|
||||
enabled = plugin.enabled or true;
|
||||
source = plugin.source;
|
||||
plugin = id;
|
||||
}
|
||||
];
|
||||
needs = {
|
||||
stateDirs = [ ];
|
||||
requiredEnv = [ ];
|
||||
};
|
||||
config = { };
|
||||
};
|
||||
|
||||
resolveFlakePlugin =
|
||||
plugin:
|
||||
let
|
||||
_ =
|
||||
if (plugin.id or null) != null then
|
||||
throw "Plugin ${plugin.source}: id is only valid for npm: OpenClaw runtime plugin sources"
|
||||
else if (plugin.hash or lib.fakeHash) != lib.fakeHash then
|
||||
throw "Plugin ${plugin.source}: hash is only valid for npm: OpenClaw runtime plugin sources"
|
||||
else if (plugin.enabled or true) != true then
|
||||
throw "Plugin ${plugin.source}: enabled is only valid for npm: OpenClaw runtime plugin sources"
|
||||
else
|
||||
null;
|
||||
system = pkgs.stdenv.hostPlatform.system;
|
||||
flake = builtins.getFlake plugin.source;
|
||||
openclawPluginRaw =
|
||||
if flake ? openclawPlugin then
|
||||
flake.openclawPlugin
|
||||
else
|
||||
throw "openclawPlugin missing in ${plugin.source}";
|
||||
openclawPlugin =
|
||||
if builtins.isFunction openclawPluginRaw then openclawPluginRaw system else openclawPluginRaw;
|
||||
resolvedPlugin =
|
||||
if openclawPlugin == null then
|
||||
throw "openclawPlugin is null in ${plugin.source} for ${system}"
|
||||
else
|
||||
openclawPlugin;
|
||||
name = resolvedPlugin.name or (throw "openclawPlugin.name missing in ${plugin.source}");
|
||||
needs = resolvedPlugin.needs or { };
|
||||
in
|
||||
builtins.seq _ {
|
||||
source = plugin.source;
|
||||
inherit name;
|
||||
skills = resolvedPlugin.skills or [ ];
|
||||
packages = resolvedPlugin.packages or [ ];
|
||||
plugins = map (normalizeOpenClawPlugin plugin.source name) (resolvedPlugin.plugins or [ ]);
|
||||
needs = {
|
||||
stateDirs = needs.stateDirs or [ ];
|
||||
requiredEnv = needs.requiredEnv or [ ];
|
||||
};
|
||||
config = plugin.config or { };
|
||||
};
|
||||
|
||||
resolvePlugin =
|
||||
plugin:
|
||||
if lib.hasPrefix "npm:" plugin.source then
|
||||
resolveNpmRuntimePlugin plugin
|
||||
else
|
||||
resolveFlakePlugin plugin;
|
||||
|
||||
resolvedPluginsByInstance = lib.mapAttrs (
|
||||
instName: inst:
|
||||
let
|
||||
resolved = map resolvePlugin inst.plugins;
|
||||
counts = lib.foldl' (acc: p: acc // { "${p.name}" = (acc.${p.name} or 0) + 1; }) { } resolved;
|
||||
duplicates = lib.attrNames (lib.filterAttrs (_: v: v > 1) counts);
|
||||
byName = lib.foldl' (acc: p: acc // { "${p.name}" = p; }) { } resolved;
|
||||
ordered = lib.attrValues byName;
|
||||
in
|
||||
if duplicates == [ ] then
|
||||
ordered
|
||||
else
|
||||
lib.warn "programs.openclaw.instances.${instName}: duplicate plugin names detected (${lib.concatStringsSep ", " duplicates}); last entry wins." ordered
|
||||
) enabledInstances;
|
||||
|
||||
pluginPackagesFor =
|
||||
instName: lib.flatten (map (p: p.packages) (resolvedPluginsByInstance.${instName} or [ ]));
|
||||
|
||||
pluginPackagesAll = lib.flatten (map pluginPackagesFor (lib.attrNames enabledInstances));
|
||||
|
||||
pluginStateDirsFor =
|
||||
instName:
|
||||
let
|
||||
dirs = lib.flatten (map (p: p.needs.stateDirs) (resolvedPluginsByInstance.${instName} or [ ]));
|
||||
in
|
||||
map (dir: resolvePath ("~/" + dir)) dirs;
|
||||
|
||||
pluginStateDirsAll = lib.flatten (map pluginStateDirsFor (lib.attrNames enabledInstances));
|
||||
|
||||
pluginEnvFor =
|
||||
instName:
|
||||
let
|
||||
entries = resolvedPluginsByInstance.${instName} or [ ];
|
||||
toPairs =
|
||||
p:
|
||||
let
|
||||
env = (p.config.env or {});
|
||||
env = (p.config.env or { });
|
||||
required = p.needs.requiredEnv;
|
||||
in
|
||||
map (k: { key = k; value = env.${k} or ""; plugin = p.name; }) required;
|
||||
map (k: {
|
||||
key = k;
|
||||
value = env.${k} or "";
|
||||
plugin = p.name;
|
||||
}) required;
|
||||
in
|
||||
lib.flatten (map toPairs entries);
|
||||
lib.flatten (map toPairs entries);
|
||||
|
||||
pluginEnvAllFor = instName:
|
||||
pluginEnvAllFor =
|
||||
instName:
|
||||
let
|
||||
entries = resolvedPluginsByInstance.${instName} or [];
|
||||
toPairs = p:
|
||||
let env = (p.config.env or {});
|
||||
in map (k: { key = k; value = env.${k}; plugin = p.name; }) (lib.attrNames env);
|
||||
entries = resolvedPluginsByInstance.${instName} or [ ];
|
||||
toPairs =
|
||||
p:
|
||||
let
|
||||
env = (p.config.env or { });
|
||||
in
|
||||
map (k: {
|
||||
key = k;
|
||||
value = env.${k};
|
||||
plugin = p.name;
|
||||
}) (lib.attrNames env);
|
||||
in
|
||||
lib.flatten (map toPairs entries);
|
||||
lib.flatten (map toPairs entries);
|
||||
|
||||
openclawPluginsFor =
|
||||
instName: lib.flatten (map (p: p.plugins) (resolvedPluginsByInstance.${instName} or [ ]));
|
||||
|
||||
openclawPluginLoadPathsFor = instName: map (p: toString p.path) (openclawPluginsFor instName);
|
||||
|
||||
openclawPluginEntriesConfigFor =
|
||||
instName:
|
||||
let
|
||||
entries = openclawPluginsFor instName;
|
||||
in
|
||||
lib.optionalAttrs (entries != [ ]) {
|
||||
plugins = {
|
||||
entries = lib.listToAttrs (
|
||||
map (p: {
|
||||
name = p.id;
|
||||
value = {
|
||||
enabled = p.enabled;
|
||||
};
|
||||
}) entries
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
openclawPluginIdAssertions = lib.mapAttrsToList (
|
||||
instName: _inst:
|
||||
let
|
||||
ids = map (p: p.id) (openclawPluginsFor instName);
|
||||
counts = lib.foldl' (acc: id: acc // { "${id}" = (acc.${id} or 0) + 1; }) { } ids;
|
||||
duplicates = lib.attrNames (lib.filterAttrs (_: v: v > 1) counts);
|
||||
in
|
||||
{
|
||||
assertion = duplicates == [ ];
|
||||
message = "programs.openclaw.instances.${instName}: duplicate OpenClaw plugin ids detected: ${lib.concatStringsSep ", " duplicates}";
|
||||
}
|
||||
) enabledInstances;
|
||||
|
||||
pluginAssertions =
|
||||
lib.flatten (lib.mapAttrsToList (instName: inst:
|
||||
let
|
||||
plugins = resolvedPluginsByInstance.${instName} or [];
|
||||
envFor = p: (p.config.env or {});
|
||||
missingFor = p:
|
||||
lib.filter (req: !(builtins.hasAttr req (envFor p))) p.needs.requiredEnv;
|
||||
configMissingStateDir = p:
|
||||
(p.config.settings or {}) != {} && (p.needs.stateDirs or []) == [];
|
||||
mkAssertion = p:
|
||||
let
|
||||
missing = missingFor p;
|
||||
in {
|
||||
assertion = missing == [];
|
||||
message = "programs.openclaw.instances.${instName}: plugin ${p.name} missing required env: ${lib.concatStringsSep ", " missing}";
|
||||
};
|
||||
mkConfigAssertion = p: {
|
||||
assertion = !(configMissingStateDir p);
|
||||
message = "programs.openclaw.instances.${instName}: plugin ${p.name} provides settings but declares no stateDirs (needed for config.json).";
|
||||
};
|
||||
in
|
||||
(map mkAssertion plugins) ++ (map mkConfigAssertion plugins)
|
||||
) enabledInstances);
|
||||
|
||||
pluginSkillsFiles =
|
||||
let
|
||||
entriesForInstance = instName: inst:
|
||||
openclawPluginIdAssertions
|
||||
++ lib.flatten (
|
||||
lib.mapAttrsToList (
|
||||
instName: inst:
|
||||
let
|
||||
base = "${toRelative (resolvePath inst.workspaceDir)}/skills";
|
||||
skillEntriesFor = p:
|
||||
map (skillPath: {
|
||||
name = "${base}/${builtins.baseNameOf skillPath}";
|
||||
value = { source = skillPath; recursive = true; };
|
||||
}) p.skills;
|
||||
plugins = resolvedPluginsByInstance.${instName} or [];
|
||||
plugins = resolvedPluginsByInstance.${instName} or [ ];
|
||||
envFor = p: (p.config.env or { });
|
||||
missingFor = p: lib.filter (req: !(builtins.hasAttr req (envFor p))) p.needs.requiredEnv;
|
||||
configMissingStateDir = p: (p.config.settings or { }) != { } && (p.needs.stateDirs or [ ]) == [ ];
|
||||
mkAssertion =
|
||||
p:
|
||||
let
|
||||
missing = missingFor p;
|
||||
in
|
||||
{
|
||||
assertion = missing == [ ];
|
||||
message = "programs.openclaw.instances.${instName}: plugin ${p.name} missing required env: ${lib.concatStringsSep ", " missing}";
|
||||
};
|
||||
mkConfigAssertion = p: {
|
||||
assertion = !(configMissingStateDir p);
|
||||
message = "programs.openclaw.instances.${instName}: plugin ${p.name} provides settings but declares no stateDirs (needed for config.json).";
|
||||
};
|
||||
in
|
||||
lib.flatten (map skillEntriesFor plugins);
|
||||
in
|
||||
lib.listToAttrs (lib.flatten (lib.mapAttrsToList entriesForInstance enabledInstances));
|
||||
(map mkAssertion plugins) ++ (map mkConfigAssertion plugins)
|
||||
) enabledInstances
|
||||
);
|
||||
|
||||
pluginConfigFiles =
|
||||
let
|
||||
entryFor = instName: inst:
|
||||
entryFor =
|
||||
instName: inst:
|
||||
let
|
||||
plugins = resolvedPluginsByInstance.${instName} or [];
|
||||
mkEntries = p:
|
||||
plugins = resolvedPluginsByInstance.${instName} or [ ];
|
||||
mkEntries =
|
||||
p:
|
||||
let
|
||||
cfg = p.config.settings or {};
|
||||
dir =
|
||||
if (p.needs.stateDirs or []) == []
|
||||
then null
|
||||
else lib.head (p.needs.stateDirs or []);
|
||||
cfg = p.config.settings or { };
|
||||
dir = if (p.needs.stateDirs or [ ]) == [ ] then null else lib.head (p.needs.stateDirs or [ ]);
|
||||
in
|
||||
if cfg == {} then
|
||||
[]
|
||||
else
|
||||
(if dir == null then
|
||||
if cfg == { } then
|
||||
[ ]
|
||||
else
|
||||
(
|
||||
if dir == null then
|
||||
throw "plugin ${p.name} provides settings but no stateDirs are defined"
|
||||
else [
|
||||
{
|
||||
name = toRelative (resolvePath ("~/" + dir + "/config.json"));
|
||||
value = { text = builtins.toJSON cfg; };
|
||||
}
|
||||
]);
|
||||
else
|
||||
[
|
||||
{
|
||||
name = toRelative (resolvePath ("~/" + dir + "/config.json"));
|
||||
value = {
|
||||
text = builtins.toJSON cfg;
|
||||
};
|
||||
}
|
||||
]
|
||||
);
|
||||
in
|
||||
lib.flatten (map mkEntries plugins);
|
||||
lib.flatten (map mkEntries plugins);
|
||||
entries = lib.flatten (lib.mapAttrsToList entryFor enabledInstances);
|
||||
in
|
||||
lib.listToAttrs entries;
|
||||
|
||||
pluginSkillAssertions =
|
||||
let
|
||||
skillTargets =
|
||||
lib.flatten (lib.concatLists (lib.mapAttrsToList (instName: inst:
|
||||
let
|
||||
base = "${toRelative (resolvePath inst.workspaceDir)}/skills";
|
||||
plugins = resolvedPluginsByInstance.${instName} or [];
|
||||
in
|
||||
map (p:
|
||||
map (skillPath:
|
||||
"${base}/${p.name}/${builtins.baseNameOf skillPath}"
|
||||
) p.skills
|
||||
) plugins
|
||||
) enabledInstances));
|
||||
counts = lib.foldl' (acc: path:
|
||||
acc // { "${path}" = (acc.${path} or 0) + 1; }
|
||||
) {} skillTargets;
|
||||
duplicates = lib.attrNames (lib.filterAttrs (_: v: v > 1) counts);
|
||||
in
|
||||
if duplicates == [] then [] else [
|
||||
{
|
||||
assertion = false;
|
||||
message = "Duplicate skill paths detected: ${lib.concatStringsSep ", " duplicates}";
|
||||
}
|
||||
];
|
||||
lib.listToAttrs entries;
|
||||
|
||||
pluginGuards =
|
||||
let
|
||||
@ -193,13 +288,14 @@ let
|
||||
exit 1
|
||||
fi
|
||||
'';
|
||||
entriesForInstance = instName:
|
||||
map (entry: entry // { instance = instName; }) (pluginEnvFor instName);
|
||||
entriesForInstance =
|
||||
instName: map (entry: entry // { instance = instName; }) (pluginEnvFor instName);
|
||||
entries = lib.flatten (map entriesForInstance (lib.attrNames enabledInstances));
|
||||
in
|
||||
lib.concatStringsSep "\n" (map renderCheck entries);
|
||||
lib.concatStringsSep "\n" (map renderCheck entries);
|
||||
|
||||
in {
|
||||
in
|
||||
{
|
||||
inherit
|
||||
resolvedPluginsByInstance
|
||||
pluginPackagesFor
|
||||
@ -208,9 +304,11 @@ in {
|
||||
pluginStateDirsAll
|
||||
pluginEnvFor
|
||||
pluginEnvAllFor
|
||||
openclawPluginsFor
|
||||
openclawPluginLoadPathsFor
|
||||
openclawPluginEntriesConfigFor
|
||||
pluginAssertions
|
||||
pluginSkillsFiles
|
||||
pluginConfigFiles
|
||||
pluginSkillAssertions
|
||||
pluginGuards;
|
||||
pluginGuards
|
||||
;
|
||||
}
|
||||
|
||||
217
nix/modules/nixos/openclaw-gateway.nix
Normal file
217
nix/modules/nixos/openclaw-gateway.nix
Normal file
@ -0,0 +1,217 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.services.openclaw-gateway;
|
||||
|
||||
deepConfigType = lib.types.mkOptionType {
|
||||
name = "openclaw-config-attrs";
|
||||
description = "OpenClaw JSON config (attrset), merged deeply via lib.recursiveUpdate.";
|
||||
check = builtins.isAttrs;
|
||||
merge = _loc: defs: lib.foldl' lib.recursiveUpdate { } (map (d: d.value) defs);
|
||||
};
|
||||
|
||||
configJson = builtins.toJSON cfg.config;
|
||||
generatedConfigFile = pkgs.writeText "openclaw.json" configJson;
|
||||
configFile = if cfg.configFile != null then cfg.configFile else generatedConfigFile;
|
||||
|
||||
# `environment.etc` takes a relative path.
|
||||
etcRelPath = lib.removePrefix "/etc/" cfg.configPath;
|
||||
|
||||
execStartCmd =
|
||||
if cfg.execStart != null then
|
||||
cfg.execStart
|
||||
else
|
||||
"${cfg.package}/bin/openclaw gateway --port ${toString cfg.port}";
|
||||
|
||||
in
|
||||
{
|
||||
options.services.openclaw-gateway = with lib; {
|
||||
enable = mkEnableOption "OpenClaw gateway (openclaw gateway as a systemd service)";
|
||||
|
||||
unitName = mkOption {
|
||||
type = types.str;
|
||||
default = "openclaw-gateway";
|
||||
description = "systemd unit name (service will be <unitName>.service).";
|
||||
};
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = if pkgs ? openclaw then pkgs.openclaw else pkgs.openclaw-gateway;
|
||||
description = "OpenClaw gateway package.";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 18789;
|
||||
description = "Gateway listen port.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "openclaw";
|
||||
description = "System user running the gateway.";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "openclaw";
|
||||
description = "System group running the gateway.";
|
||||
};
|
||||
|
||||
createUser = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Create the user/group automatically.";
|
||||
};
|
||||
|
||||
stateDir = mkOption {
|
||||
type = types.str;
|
||||
default = "/var/lib/openclaw";
|
||||
description = "State dir (OPENCLAW_STATE_DIR).";
|
||||
};
|
||||
|
||||
workingDirectory = mkOption {
|
||||
type = types.str;
|
||||
default = cfg.stateDir;
|
||||
description = "Working directory for the systemd service.";
|
||||
};
|
||||
|
||||
configPath = mkOption {
|
||||
type = types.str;
|
||||
default = "/etc/openclaw/openclaw.json";
|
||||
description = "Path to the OpenClaw JSON config file (OPENCLAW_CONFIG_PATH). Must be under /etc.";
|
||||
};
|
||||
|
||||
configFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description = "Optional path to an existing config file. If set, it is copied to configPath (under /etc).";
|
||||
};
|
||||
|
||||
config = mkOption {
|
||||
type = deepConfigType;
|
||||
default = { };
|
||||
description = "OpenClaw JSON config (attrset), deep-merged across definitions.";
|
||||
};
|
||||
|
||||
logPath = mkOption {
|
||||
type = types.str;
|
||||
default = "${cfg.stateDir}/logs/gateway.log";
|
||||
description = "Log file path (systemd StandardOutput/StandardError append).";
|
||||
};
|
||||
|
||||
environment = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = { };
|
||||
description = "Additional environment variables for the gateway process.";
|
||||
};
|
||||
|
||||
environmentFiles = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = "systemd EnvironmentFile= entries (use leading '-' to ignore missing).";
|
||||
};
|
||||
|
||||
execStart = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Override ExecStart command. If unset, runs: openclaw gateway --port <port>.";
|
||||
};
|
||||
|
||||
execStartPre = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = "List of ExecStartPre= commands.";
|
||||
};
|
||||
|
||||
servicePath = mkOption {
|
||||
type = types.listOf types.package;
|
||||
default = [ ];
|
||||
description = "Extra packages added to systemd service PATH.";
|
||||
};
|
||||
|
||||
restart = mkOption {
|
||||
type = types.str;
|
||||
default = "always";
|
||||
description = "systemd Restart=.";
|
||||
};
|
||||
|
||||
restartSec = mkOption {
|
||||
type = types.int;
|
||||
default = 2;
|
||||
description = "systemd RestartSec=.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = lib.hasPrefix "/etc/" cfg.configPath;
|
||||
message = "services.openclaw-gateway.configPath must be under /etc (got: ${cfg.configPath}).";
|
||||
}
|
||||
];
|
||||
|
||||
users.groups.${cfg.group} = lib.mkIf cfg.createUser { };
|
||||
users.users.${cfg.user} = lib.mkIf cfg.createUser {
|
||||
isSystemUser = true;
|
||||
group = cfg.group;
|
||||
home = cfg.stateDir;
|
||||
createHome = true;
|
||||
shell = pkgs.bashInteractive;
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d ${cfg.stateDir} 0750 ${cfg.user} ${cfg.group} - -"
|
||||
"d ${builtins.dirOf cfg.logPath} 0750 ${cfg.user} ${cfg.group} - -"
|
||||
"d ${builtins.dirOf cfg.configPath} 0755 root root - -"
|
||||
];
|
||||
|
||||
environment.etc.${etcRelPath} = {
|
||||
mode = "0644";
|
||||
source = configFile;
|
||||
};
|
||||
|
||||
systemd.services.${cfg.unitName} = {
|
||||
description = "OpenClaw gateway";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network.target" ];
|
||||
|
||||
environment = {
|
||||
OPENCLAW_CONFIG_PATH = cfg.configPath;
|
||||
OPENCLAW_STATE_DIR = cfg.stateDir;
|
||||
|
||||
# Backward-compatible env names.
|
||||
CLAWDBOT_CONFIG_PATH = cfg.configPath;
|
||||
CLAWDBOT_STATE_DIR = cfg.stateDir;
|
||||
}
|
||||
// cfg.environment;
|
||||
|
||||
serviceConfig = {
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
WorkingDirectory = cfg.workingDirectory;
|
||||
|
||||
EnvironmentFile = cfg.environmentFiles;
|
||||
ExecStartPre = cfg.execStartPre;
|
||||
ExecStart = execStartCmd;
|
||||
|
||||
Restart = cfg.restart;
|
||||
RestartSec = cfg.restartSec;
|
||||
|
||||
StandardOutput = "append:${cfg.logPath}";
|
||||
StandardError = "append:${cfg.logPath}";
|
||||
};
|
||||
|
||||
path = [
|
||||
pkgs.bash
|
||||
pkgs.coreutils
|
||||
]
|
||||
++ cfg.servicePath;
|
||||
};
|
||||
};
|
||||
}
|
||||
@ -1,14 +1,38 @@
|
||||
{
|
||||
openclawToolPkgs ? { },
|
||||
qmdPkgs ? { },
|
||||
}:
|
||||
final: prev:
|
||||
let
|
||||
packages = import ./packages { pkgs = prev; };
|
||||
toolNames = (import ./tools/extended.nix { pkgs = prev; }).toolNames;
|
||||
withTools = { toolNamesOverride ? null, excludeToolNames ? [] }:
|
||||
qmdPackage =
|
||||
if prev.stdenv.hostPlatform.isDarwin then
|
||||
openclawToolPkgs.qmd or null
|
||||
else
|
||||
qmdPkgs.qmd or qmdPkgs.default or null;
|
||||
packages = import ./packages {
|
||||
pkgs = prev;
|
||||
openclawToolPkgs = openclawToolPkgs;
|
||||
inherit qmdPackage;
|
||||
};
|
||||
toolNames =
|
||||
(import ./tools/extended.nix {
|
||||
pkgs = prev;
|
||||
openclawToolPkgs = openclawToolPkgs;
|
||||
}).toolNames;
|
||||
withTools =
|
||||
{
|
||||
toolNamesOverride ? null,
|
||||
excludeToolNames ? [ ],
|
||||
}:
|
||||
import ./packages {
|
||||
pkgs = prev;
|
||||
openclawToolPkgs = openclawToolPkgs;
|
||||
inherit qmdPackage;
|
||||
inherit toolNamesOverride excludeToolNames;
|
||||
};
|
||||
in
|
||||
packages // {
|
||||
packages
|
||||
// {
|
||||
openclawPackages = packages // {
|
||||
inherit toolNames withTools;
|
||||
};
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
{ pkgs
|
||||
, sourceInfo ? import ../sources/openclaw-source.nix
|
||||
, steipetePkgs ? {}
|
||||
, toolNamesOverride ? null
|
||||
, excludeToolNames ? []
|
||||
{
|
||||
pkgs,
|
||||
sourceInfo ? import ../sources/openclaw-source.nix,
|
||||
openclawToolPkgs ? { },
|
||||
qmdPackage ? null,
|
||||
toolNamesOverride ? null,
|
||||
excludeToolNames ? [ ],
|
||||
}:
|
||||
let
|
||||
isDarwin = pkgs.stdenv.hostPlatform.isDarwin;
|
||||
toolSets = import ../tools/extended.nix {
|
||||
pkgs = pkgs;
|
||||
steipetePkgs = steipetePkgs;
|
||||
openclawToolPkgs = openclawToolPkgs;
|
||||
inherit toolNamesOverride excludeToolNames;
|
||||
};
|
||||
openclawGateway = pkgs.callPackage ./openclaw-gateway.nix {
|
||||
@ -16,18 +18,17 @@ let
|
||||
pnpmDepsHash = sourceInfo.pnpmDepsHash or null;
|
||||
};
|
||||
openclawApp = if isDarwin then pkgs.callPackage ./openclaw-app.nix { } else null;
|
||||
openclawTools = pkgs.buildEnv {
|
||||
name = "openclaw-tools";
|
||||
paths = toolSets.tools;
|
||||
pathsToLink = [ "/bin" ];
|
||||
};
|
||||
openclawBundle = pkgs.callPackage ./openclaw-batteries.nix {
|
||||
openclaw-gateway = openclawGateway;
|
||||
openclaw-app = openclawApp;
|
||||
extendedTools = toolSets.tools;
|
||||
inherit qmdPackage;
|
||||
version = sourceInfo.releaseVersion or null;
|
||||
};
|
||||
in {
|
||||
in
|
||||
{
|
||||
openclaw-gateway = openclawGateway;
|
||||
openclaw = openclawBundle;
|
||||
openclaw-tools = openclawTools;
|
||||
} // (if isDarwin then { openclaw-app = openclawApp; } else {})
|
||||
}
|
||||
// (if qmdPackage != null then { qmd = qmdPackage; } else { })
|
||||
// (if isDarwin then { openclaw-app = openclawApp; } else { })
|
||||
|
||||
16
nix/packages/node-addon-api.nix
Normal file
16
nix/packages/node-addon-api.nix
Normal file
@ -0,0 +1,16 @@
|
||||
{ stdenv, fetchurl }:
|
||||
|
||||
stdenv.mkDerivation {
|
||||
pname = "node-addon-api";
|
||||
version = "8.5.0";
|
||||
|
||||
src = fetchurl {
|
||||
url = "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz";
|
||||
hash = "sha256-0S8HyBYig7YhNVGFXx2o2sFiMxN0YpgwteZA8TDweRA=";
|
||||
};
|
||||
|
||||
dontConfigure = true;
|
||||
dontBuild = true;
|
||||
|
||||
installPhase = "${../scripts/node-addon-api-install.sh}";
|
||||
}
|
||||
@ -1,15 +1,16 @@
|
||||
{ lib
|
||||
, stdenvNoCC
|
||||
, fetchzip
|
||||
{
|
||||
lib,
|
||||
stdenvNoCC,
|
||||
fetchzip,
|
||||
}:
|
||||
|
||||
stdenvNoCC.mkDerivation {
|
||||
pname = "openclaw-app";
|
||||
version = "2026.2.13";
|
||||
version = "2026.5.7";
|
||||
|
||||
src = fetchzip {
|
||||
url = "https://github.com/openclaw/openclaw/releases/download/v2026.2.13/OpenClaw-2026.2.13.zip";
|
||||
hash = "sha256-ewIZCQ3mg9dus3tD3BdUmESwsk1CFpClbJbLRT1g9Bc=";
|
||||
url = "https://github.com/openclaw/openclaw/releases/download/v2026.5.7/OpenClaw-2026.5.7.zip";
|
||||
hash = "sha256-64O1dzadr5R1HiS4DlpbC7En3qyEaibDZS8kKbH7GOo=";
|
||||
stripRoot = false;
|
||||
};
|
||||
|
||||
|
||||
@ -1,23 +1,46 @@
|
||||
{ lib
|
||||
, buildEnv
|
||||
, openclaw-gateway
|
||||
, openclaw-app ? null
|
||||
, extendedTools ? []
|
||||
{
|
||||
lib,
|
||||
stdenvNoCC,
|
||||
makeWrapper,
|
||||
python3Minimal,
|
||||
openclaw-gateway,
|
||||
openclaw-app ? null,
|
||||
extendedTools ? [ ],
|
||||
qmdPackage ? null,
|
||||
version ? null,
|
||||
}:
|
||||
|
||||
let
|
||||
appPaths = lib.optional (openclaw-app != null) openclaw-app;
|
||||
appLinks = lib.optional (openclaw-app != null) "/Applications";
|
||||
bundleVersion =
|
||||
if version != null && version != "" then version else lib.getVersion openclaw-gateway;
|
||||
runtimeTools = extendedTools ++ lib.optional (qmdPackage != null) qmdPackage;
|
||||
toolsPath = lib.makeBinPath runtimeTools;
|
||||
in
|
||||
buildEnv {
|
||||
name = "openclaw-2.0.0-beta5";
|
||||
paths = [ openclaw-gateway ] ++ appPaths ++ extendedTools;
|
||||
pathsToLink = [ "/bin" ] ++ appLinks;
|
||||
stdenvNoCC.mkDerivation {
|
||||
pname = "openclaw";
|
||||
version = bundleVersion;
|
||||
|
||||
dontUnpack = true;
|
||||
dontConfigure = true;
|
||||
dontBuild = true;
|
||||
|
||||
nativeBuildInputs = [ makeWrapper ];
|
||||
|
||||
env = {
|
||||
OPENCLAW_APP_PACKAGE = lib.optionalString (openclaw-app != null) "${openclaw-app}";
|
||||
OPENCLAW_GATEWAY_BIN = "${openclaw-gateway}/bin/openclaw";
|
||||
OPENCLAW_PINNED_WRITE_PYTHON = "${python3Minimal}/bin/python3";
|
||||
OPENCLAW_TOOLS_PATH = toolsPath;
|
||||
STDENV_SETUP = "${stdenvNoCC}/setup";
|
||||
};
|
||||
|
||||
installPhase = "${../scripts/openclaw-batteries-install.sh}";
|
||||
|
||||
meta = with lib; {
|
||||
description = "OpenClaw batteries-included bundle (gateway + app + tools)";
|
||||
homepage = "https://github.com/openclaw/openclaw";
|
||||
license = licenses.mit;
|
||||
platforms = platforms.darwin ++ platforms.linux;
|
||||
mainProgram = "openclaw";
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,95 +1,89 @@
|
||||
{ lib
|
||||
, stdenv
|
||||
, fetchFromGitHub
|
||||
, fetchurl
|
||||
, nodejs_22
|
||||
, pnpm_10
|
||||
, pkg-config
|
||||
, jq
|
||||
, python3
|
||||
, perl
|
||||
, node-gyp
|
||||
, makeWrapper
|
||||
, vips
|
||||
, git
|
||||
, zstd
|
||||
, sourceInfo
|
||||
, gatewaySrc ? null
|
||||
, pnpmDepsHash ? (sourceInfo.pnpmDepsHash or null)
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
fetchFromGitHub,
|
||||
fetchurl,
|
||||
nodejs_22,
|
||||
pnpm_10,
|
||||
fetchPnpmDeps,
|
||||
pkg-config,
|
||||
jq,
|
||||
python3,
|
||||
perl,
|
||||
node-gyp,
|
||||
makeWrapper,
|
||||
vips,
|
||||
git,
|
||||
zstd,
|
||||
sourceInfo,
|
||||
gatewaySrc ? null,
|
||||
pnpmDepsHash ? (sourceInfo.pnpmDepsHash or null),
|
||||
}:
|
||||
|
||||
assert gatewaySrc == null || pnpmDepsHash != null;
|
||||
|
||||
let
|
||||
sourceFetch = lib.removeAttrs sourceInfo [ "pnpmDepsHash" ];
|
||||
pnpmPlatform = if stdenv.hostPlatform.isDarwin then "darwin" else "linux";
|
||||
pnpmArch = if stdenv.hostPlatform.isAarch64 then "arm64" else "x64";
|
||||
nodeAddonApi = stdenv.mkDerivation {
|
||||
pname = "node-addon-api";
|
||||
version = "8.5.0";
|
||||
src = fetchurl {
|
||||
url = "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz";
|
||||
hash = "sha256-0S8HyBYig7YhNVGFXx2o2sFiMxN0YpgwteZA8TDweRA=";
|
||||
};
|
||||
dontConfigure = true;
|
||||
dontBuild = true;
|
||||
installPhase = "${../scripts/node-addon-api-install.sh}";
|
||||
};
|
||||
common =
|
||||
import ../lib/openclaw-gateway-common.nix
|
||||
{
|
||||
inherit
|
||||
lib
|
||||
stdenv
|
||||
fetchFromGitHub
|
||||
fetchurl
|
||||
nodejs_22
|
||||
pnpm_10
|
||||
fetchPnpmDeps
|
||||
pkg-config
|
||||
jq
|
||||
python3
|
||||
node-gyp
|
||||
git
|
||||
zstd
|
||||
;
|
||||
}
|
||||
{
|
||||
pname = "openclaw-gateway";
|
||||
sourceInfo = sourceInfo;
|
||||
pnpmDepsHash = pnpmDepsHash;
|
||||
pnpmDepsPname = "openclaw-gateway";
|
||||
gatewaySrc = gatewaySrc;
|
||||
enableSharp = true;
|
||||
extraNativeBuildInputs = [
|
||||
perl
|
||||
makeWrapper
|
||||
];
|
||||
extraBuildInputs = [ vips ];
|
||||
extraEnv = {
|
||||
NODE_BIN = "${nodejs_22}/bin/node";
|
||||
PATCH_CLIPBOARD_SH = "${../scripts/patch-clipboard.sh}";
|
||||
PATCH_CLIPBOARD_WRAPPER = "${../scripts/clipboard-wrapper.cjs}";
|
||||
};
|
||||
};
|
||||
|
||||
in
|
||||
|
||||
stdenv.mkDerivation (finalAttrs: {
|
||||
pname = "openclaw-gateway";
|
||||
version = "2026.1.8-2";
|
||||
inherit (common) version;
|
||||
|
||||
src = if gatewaySrc != null then gatewaySrc else fetchFromGitHub sourceFetch;
|
||||
src = common.resolvedSrc;
|
||||
pnpmDeps = common.pnpmDeps;
|
||||
|
||||
pnpmDeps = pnpm_10.fetchDeps {
|
||||
inherit (finalAttrs) pname version src;
|
||||
hash = if pnpmDepsHash != null
|
||||
then pnpmDepsHash
|
||||
else lib.fakeHash;
|
||||
fetcherVersion = 2;
|
||||
npm_config_arch = pnpmArch;
|
||||
npm_config_platform = pnpmPlatform;
|
||||
nativeBuildInputs = [ git ];
|
||||
};
|
||||
nativeBuildInputs = common.nativeBuildInputs;
|
||||
buildInputs = common.buildInputs;
|
||||
|
||||
nativeBuildInputs = [
|
||||
nodejs_22
|
||||
pnpm_10
|
||||
pkg-config
|
||||
jq
|
||||
python3
|
||||
perl
|
||||
node-gyp
|
||||
makeWrapper
|
||||
zstd
|
||||
];
|
||||
|
||||
buildInputs = [ vips ];
|
||||
|
||||
env = {
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS = "1";
|
||||
npm_config_arch = pnpmArch;
|
||||
npm_config_platform = pnpmPlatform;
|
||||
PNPM_CONFIG_MANAGE_PACKAGE_MANAGER_VERSIONS = "false";
|
||||
npm_config_nodedir = nodejs_22;
|
||||
npm_config_python = python3;
|
||||
NODE_PATH = "${nodeAddonApi}/lib/node_modules:${node-gyp}/lib/node_modules";
|
||||
NODE_BIN = "${nodejs_22}/bin/node";
|
||||
env = common.env // {
|
||||
# Nix doesn't automatically substitute finalAttrs into env.
|
||||
PNPM_DEPS = finalAttrs.pnpmDeps;
|
||||
NODE_GYP_WRAPPER_SH = "${../scripts/node-gyp-wrapper.sh}";
|
||||
GATEWAY_PREBUILD_SH = "${../scripts/gateway-prebuild.sh}";
|
||||
PROMOTE_PNPM_INTEGRITY_SH = "${../scripts/promote-pnpm-integrity.sh}";
|
||||
REMOVE_PACKAGE_MANAGER_FIELD_SH = "${../scripts/remove-package-manager-field.sh}";
|
||||
PATCH_CLIPBOARD_SH = "${../scripts/patch-clipboard.sh}";
|
||||
PATCH_CLIPBOARD_WRAPPER = "${../scripts/clipboard-wrapper.cjs}";
|
||||
STDENV_SETUP = "${stdenv}/setup";
|
||||
};
|
||||
|
||||
passthru = common.passthru;
|
||||
|
||||
postPatch = "${../scripts/gateway-postpatch.sh}";
|
||||
buildPhase = "${../scripts/gateway-build.sh}";
|
||||
installPhase = "${../scripts/gateway-install.sh}";
|
||||
dontFixup = true;
|
||||
dontStrip = true;
|
||||
dontPatchShebangs = true;
|
||||
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
diff --git a/src/plugins/public-surface-loader.ts b/src/plugins/public-surface-loader.ts
|
||||
index 5f6f939..b8d27c8 100644
|
||||
--- a/src/plugins/public-surface-loader.ts
|
||||
+++ b/src/plugins/public-surface-loader.ts
|
||||
@@ -133,8 +133,12 @@ export function loadBundledPluginPublicArtifactModuleSync<T extends object>(para
|
||||
- const opened = openRootFileSync({
|
||||
+ const packageRoot = path.resolve(OPENCLAW_PACKAGE_ROOT);
|
||||
+ const resolvedModulePath = path.resolve(location.modulePath);
|
||||
+ const isPackagePublicSurface = resolvedModulePath.startsWith(`${packageRoot}${path.sep}`);
|
||||
+
|
||||
+ const opened = openRootFileSync({
|
||||
absolutePath: location.modulePath,
|
||||
rootPath: location.boundaryRoot,
|
||||
boundaryLabel:
|
||||
location.boundaryRoot === OPENCLAW_PACKAGE_ROOT ? "OpenClaw package root" : "plugin root",
|
||||
- rejectHardlinks: true,
|
||||
+ rejectHardlinks: !isPackagePublicSurface,
|
||||
});
|
||||
if (!opened.ok) {
|
||||
throw new Error(
|
||||
20
nix/patches/allow-package-public-surface-hardlinks.patch
Normal file
20
nix/patches/allow-package-public-surface-hardlinks.patch
Normal file
@ -0,0 +1,20 @@
|
||||
diff --git a/src/plugins/public-surface-loader.ts b/src/plugins/public-surface-loader.ts
|
||||
index 1f5b5ab..a08ef8a 100644
|
||||
--- a/src/plugins/public-surface-loader.ts
|
||||
+++ b/src/plugins/public-surface-loader.ts
|
||||
@@ -124,8 +124,12 @@ export function loadBundledPluginPublicArtifactModuleSync<T extends object>(para
|
||||
- const opened = openBoundaryFileSync({
|
||||
+ const packageRoot = path.resolve(OPENCLAW_PACKAGE_ROOT);
|
||||
+ const resolvedModulePath = path.resolve(location.modulePath);
|
||||
+ const isPackagePublicSurface = resolvedModulePath.startsWith(`${packageRoot}${path.sep}`);
|
||||
+
|
||||
+ const opened = openBoundaryFileSync({
|
||||
absolutePath: location.modulePath,
|
||||
rootPath: location.boundaryRoot,
|
||||
boundaryLabel:
|
||||
location.boundaryRoot === OPENCLAW_PACKAGE_ROOT ? "OpenClaw package root" : "plugin root",
|
||||
- rejectHardlinks: true,
|
||||
+ rejectHardlinks: !isPackagePublicSurface,
|
||||
});
|
||||
if (!opened.ok) {
|
||||
throw new Error(
|
||||
@ -0,0 +1,26 @@
|
||||
diff --git a/src/gateway/server-startup-config.ts b/src/gateway/server-startup-config.ts
|
||||
index e3f1565a00..97feaf2e8c 100644
|
||||
--- a/src/gateway/server-startup-config.ts
|
||||
+++ b/src/gateway/server-startup-config.ts
|
||||
@@ -99,6 +99,21 @@ export async function loadGatewayStartupConfigSnapshot(params: {
|
||||
};
|
||||
}
|
||||
|
||||
+ if (isNixMode) {
|
||||
+ params.log.info(
|
||||
+ `gateway: auto-enabled plugins for this runtime without writing config in Nix mode:\n${autoEnable.changes.map((entry) => `- ${entry}`).join("\n")}`,
|
||||
+ );
|
||||
+ return {
|
||||
+ snapshot: {
|
||||
+ ...configSnapshot,
|
||||
+ runtimeConfig: autoEnable.config,
|
||||
+ config: autoEnable.config,
|
||||
+ },
|
||||
+ wroteConfig,
|
||||
+ ...(pluginMetadataSnapshot ? { pluginMetadataSnapshot } : {}),
|
||||
+ };
|
||||
+ }
|
||||
+
|
||||
try {
|
||||
const { replaceConfigFile } = await import("../config/mutate.js");
|
||||
await replaceConfigFile({
|
||||
426
nix/patches/stage-bundled-plugin-runtime-deps.mjs
Normal file
426
nix/patches/stage-bundled-plugin-runtime-deps.mjs
Normal file
@ -0,0 +1,426 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { resolveNpmRunner } from "./npm-runner.mjs";
|
||||
|
||||
function readJson(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
}
|
||||
|
||||
function writeJson(filePath, value) {
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
function removePathIfExists(targetPath) {
|
||||
fs.rmSync(targetPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function makeTempDir(parentDir, prefix) {
|
||||
return fs.mkdtempSync(path.join(parentDir, prefix));
|
||||
}
|
||||
|
||||
function sanitizeTempPrefixSegment(value) {
|
||||
const normalized = value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/-+/g, "-");
|
||||
return normalized.length > 0 ? normalized : "plugin";
|
||||
}
|
||||
|
||||
function replaceDir(targetPath, sourcePath) {
|
||||
removePathIfExists(targetPath);
|
||||
try {
|
||||
fs.renameSync(sourcePath, targetPath);
|
||||
return;
|
||||
} catch (error) {
|
||||
if (error?.code !== "EXDEV") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
fs.cpSync(sourcePath, targetPath, { recursive: true, force: true });
|
||||
removePathIfExists(sourcePath);
|
||||
}
|
||||
|
||||
function dependencyNodeModulesPath(nodeModulesDir, depName) {
|
||||
return path.join(nodeModulesDir, ...depName.split("/"));
|
||||
}
|
||||
|
||||
function createResolver(fromDir) {
|
||||
return createRequire(path.join(fromDir, "__openclaw-runtime-deps-resolver__.cjs"));
|
||||
}
|
||||
|
||||
function findPackageRoot(startPath, depName) {
|
||||
let currentDir = fs.statSync(startPath).isDirectory() ? startPath : path.dirname(startPath);
|
||||
while (true) {
|
||||
const packageJsonPath = path.join(currentDir, "package.json");
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
const packageJson = readJson(packageJsonPath);
|
||||
if (packageJson.name === depName) {
|
||||
return {
|
||||
dir: currentDir,
|
||||
packageJsonPath,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const parentDir = path.dirname(currentDir);
|
||||
if (parentDir === currentDir) {
|
||||
return null;
|
||||
}
|
||||
currentDir = parentDir;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveDependencyFromNodeModulesPath(fromDir, depName) {
|
||||
let currentDir = fromDir;
|
||||
while (true) {
|
||||
const nodeModulesDir =
|
||||
path.basename(currentDir) === "node_modules" ? currentDir : path.join(currentDir, "node_modules");
|
||||
const directPath = dependencyNodeModulesPath(nodeModulesDir, depName);
|
||||
const packageJsonPath = path.join(directPath, "package.json");
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
return {
|
||||
dir: directPath,
|
||||
packageJsonPath,
|
||||
};
|
||||
}
|
||||
|
||||
const parentDir = path.dirname(currentDir);
|
||||
if (parentDir === currentDir) {
|
||||
return null;
|
||||
}
|
||||
currentDir = parentDir;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveInstalledDependency(fromDir, depName) {
|
||||
const directResolution = resolveDependencyFromNodeModulesPath(fromDir, depName);
|
||||
if (directResolution !== null) {
|
||||
return directResolution;
|
||||
}
|
||||
|
||||
const resolver = createResolver(fromDir);
|
||||
try {
|
||||
return findPackageRoot(resolver.resolve(`${depName}/package.json`), depName);
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
return findPackageRoot(resolver.resolve(depName), depName);
|
||||
} catch {}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function stageInstalledRuntimeTree(rootNodeModulesDir, packageJson, stagedNodeModulesDir) {
|
||||
const packageCache = new Map();
|
||||
const stagedTargets = new Set();
|
||||
const queue = [
|
||||
...Object.entries(packageJson.dependencies ?? {}).map(([depName, spec]) => ({
|
||||
depName,
|
||||
spec,
|
||||
fromDir: rootNodeModulesDir,
|
||||
isOptional: false,
|
||||
targetNodeModulesDir: stagedNodeModulesDir,
|
||||
})),
|
||||
...Object.entries(packageJson.optionalDependencies ?? {}).map(([depName, spec]) => ({
|
||||
depName,
|
||||
spec,
|
||||
fromDir: rootNodeModulesDir,
|
||||
isOptional: true,
|
||||
targetNodeModulesDir: stagedNodeModulesDir,
|
||||
})),
|
||||
];
|
||||
stageInstalledRuntimeTree.lastFailure = null;
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { depName, fromDir, isOptional, spec, targetNodeModulesDir } = queue.shift();
|
||||
const resolvedDep = resolveInstalledDependency(fromDir, depName);
|
||||
if (resolvedDep === null) {
|
||||
if (isOptional) {
|
||||
continue;
|
||||
}
|
||||
stageInstalledRuntimeTree.lastFailure =
|
||||
fromDir === rootNodeModulesDir
|
||||
? `missing ${depName} (${spec}) from root`
|
||||
: `missing ${depName} (${spec}) from ${fromDir}`;
|
||||
return false;
|
||||
}
|
||||
|
||||
const packageJson =
|
||||
packageCache.get(resolvedDep.packageJsonPath) ?? readJson(resolvedDep.packageJsonPath);
|
||||
packageCache.set(resolvedDep.packageJsonPath, packageJson);
|
||||
|
||||
const targetPath = dependencyNodeModulesPath(targetNodeModulesDir, depName);
|
||||
if (!stagedTargets.has(targetPath)) {
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||
fs.cpSync(resolvedDep.dir, targetPath, { recursive: true, force: true, dereference: true });
|
||||
stagedTargets.add(targetPath);
|
||||
}
|
||||
|
||||
const childTargetNodeModulesDir = path.join(targetPath, "node_modules");
|
||||
for (const [childName, childSpec] of Object.entries(packageJson.dependencies ?? {})) {
|
||||
queue.push({
|
||||
depName: childName,
|
||||
spec: childSpec,
|
||||
fromDir: resolvedDep.dir,
|
||||
isOptional: false,
|
||||
targetNodeModulesDir: childTargetNodeModulesDir,
|
||||
});
|
||||
}
|
||||
for (const [childName, childSpec] of Object.entries(packageJson.optionalDependencies ?? {})) {
|
||||
queue.push({
|
||||
depName: childName,
|
||||
spec: childSpec,
|
||||
fromDir: resolvedDep.dir,
|
||||
isOptional: true,
|
||||
targetNodeModulesDir: childTargetNodeModulesDir,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function listBundledPluginRuntimeDirs(repoRoot) {
|
||||
const extensionsRoot = path.join(repoRoot, "dist", "extensions");
|
||||
if (!fs.existsSync(extensionsRoot)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fs
|
||||
.readdirSync(extensionsRoot, { withFileTypes: true })
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => path.join(extensionsRoot, dirent.name))
|
||||
.filter((pluginDir) => fs.existsSync(path.join(pluginDir, "package.json")));
|
||||
}
|
||||
|
||||
function hasRuntimeDeps(packageJson) {
|
||||
return (
|
||||
Object.keys(packageJson.dependencies ?? {}).length > 0 ||
|
||||
Object.keys(packageJson.optionalDependencies ?? {}).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
function shouldStageRuntimeDeps(packageJson) {
|
||||
return packageJson.openclaw?.bundle?.stageRuntimeDependencies === true;
|
||||
}
|
||||
|
||||
function sanitizeBundledManifestForRuntimeInstall(pluginDir) {
|
||||
const manifestPath = path.join(pluginDir, "package.json");
|
||||
const packageJson = readJson(manifestPath);
|
||||
let changed = false;
|
||||
|
||||
if (packageJson.peerDependencies?.openclaw) {
|
||||
const nextPeerDependencies = { ...packageJson.peerDependencies };
|
||||
delete nextPeerDependencies.openclaw;
|
||||
if (Object.keys(nextPeerDependencies).length === 0) {
|
||||
delete packageJson.peerDependencies;
|
||||
} else {
|
||||
packageJson.peerDependencies = nextPeerDependencies;
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (packageJson.peerDependenciesMeta?.openclaw) {
|
||||
const nextPeerDependenciesMeta = { ...packageJson.peerDependenciesMeta };
|
||||
delete nextPeerDependenciesMeta.openclaw;
|
||||
if (Object.keys(nextPeerDependenciesMeta).length === 0) {
|
||||
delete packageJson.peerDependenciesMeta;
|
||||
} else {
|
||||
packageJson.peerDependenciesMeta = nextPeerDependenciesMeta;
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (packageJson.devDependencies?.openclaw) {
|
||||
const nextDevDependencies = { ...packageJson.devDependencies };
|
||||
delete nextDevDependencies.openclaw;
|
||||
if (Object.keys(nextDevDependencies).length === 0) {
|
||||
delete packageJson.devDependencies;
|
||||
} else {
|
||||
packageJson.devDependencies = nextDevDependencies;
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
writeJson(manifestPath, packageJson);
|
||||
}
|
||||
|
||||
return packageJson;
|
||||
}
|
||||
|
||||
function resolveRuntimeDepsStampPath(pluginDir) {
|
||||
return path.join(pluginDir, ".openclaw-runtime-deps-stamp.json");
|
||||
}
|
||||
|
||||
function createRuntimeDepsFingerprint(packageJson) {
|
||||
return createHash("sha256").update(JSON.stringify(packageJson)).digest("hex");
|
||||
}
|
||||
|
||||
function readRuntimeDepsStamp(stampPath) {
|
||||
if (!fs.existsSync(stampPath)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return readJson(stampPath);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function stageInstalledRootRuntimeDeps(params) {
|
||||
const { fingerprint, packageJson, pluginDir, repoRoot } = params;
|
||||
const rootNodeModulesDir = path.join(repoRoot, "node_modules");
|
||||
const hasDeps =
|
||||
Object.keys(packageJson.dependencies ?? {}).length > 0 ||
|
||||
Object.keys(packageJson.optionalDependencies ?? {}).length > 0;
|
||||
if (!hasDeps || !fs.existsSync(rootNodeModulesDir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nodeModulesDir = path.join(pluginDir, "node_modules");
|
||||
const stampPath = resolveRuntimeDepsStampPath(pluginDir);
|
||||
const stagedNodeModulesDir = path.join(
|
||||
makeTempDir(
|
||||
os.tmpdir(),
|
||||
`openclaw-runtime-deps-${sanitizeTempPrefixSegment(path.basename(pluginDir))}-`,
|
||||
),
|
||||
"node_modules",
|
||||
);
|
||||
|
||||
if (!stageInstalledRuntimeTree(rootNodeModulesDir, packageJson, stagedNodeModulesDir)) {
|
||||
console.error(
|
||||
`[nix-openclaw] root runtime staging unavailable for ${path.basename(pluginDir)}: ${
|
||||
stageInstalledRuntimeTree.lastFailure ?? "unknown reason"
|
||||
}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
replaceDir(nodeModulesDir, stagedNodeModulesDir);
|
||||
writeJson(stampPath, {
|
||||
fingerprint,
|
||||
generatedAt: new Date().toISOString(),
|
||||
});
|
||||
return true;
|
||||
} finally {
|
||||
removePathIfExists(path.dirname(stagedNodeModulesDir));
|
||||
}
|
||||
}
|
||||
|
||||
function installPluginRuntimeDeps(params) {
|
||||
const { fingerprint, packageJson, pluginDir, pluginId, repoRoot } = params;
|
||||
if (
|
||||
repoRoot &&
|
||||
stageInstalledRootRuntimeDeps({ fingerprint, packageJson, pluginDir, repoRoot })
|
||||
) {
|
||||
return;
|
||||
}
|
||||
console.error(`[nix-openclaw] falling back to npm install for ${pluginId}`);
|
||||
const nodeModulesDir = path.join(pluginDir, "node_modules");
|
||||
const stampPath = resolveRuntimeDepsStampPath(pluginDir);
|
||||
const tempInstallDir = makeTempDir(
|
||||
os.tmpdir(),
|
||||
`openclaw-runtime-deps-${sanitizeTempPrefixSegment(pluginId)}-`,
|
||||
);
|
||||
const npmRunner = resolveNpmRunner({
|
||||
npmArgs: [
|
||||
"install",
|
||||
"--omit=dev",
|
||||
"--silent",
|
||||
"--ignore-scripts",
|
||||
"--legacy-peer-deps",
|
||||
"--package-lock=false",
|
||||
],
|
||||
});
|
||||
try {
|
||||
writeJson(path.join(tempInstallDir, "package.json"), packageJson);
|
||||
const result = spawnSync(npmRunner.command, npmRunner.args, {
|
||||
cwd: tempInstallDir,
|
||||
encoding: "utf8",
|
||||
env: npmRunner.env,
|
||||
stdio: "pipe",
|
||||
shell: npmRunner.shell,
|
||||
windowsVerbatimArguments: npmRunner.windowsVerbatimArguments,
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
const output = [result.stderr, result.stdout].filter(Boolean).join("\n").trim();
|
||||
throw new Error(
|
||||
`failed to stage bundled runtime deps for ${pluginId}: ${output || "npm install failed"}`,
|
||||
);
|
||||
}
|
||||
|
||||
const stagedNodeModulesDir = path.join(tempInstallDir, "node_modules");
|
||||
if (!fs.existsSync(stagedNodeModulesDir)) {
|
||||
throw new Error(
|
||||
`failed to stage bundled runtime deps for ${pluginId}: npm install produced no node_modules directory`,
|
||||
);
|
||||
}
|
||||
|
||||
replaceDir(nodeModulesDir, stagedNodeModulesDir);
|
||||
writeJson(stampPath, {
|
||||
fingerprint,
|
||||
generatedAt: new Date().toISOString(),
|
||||
});
|
||||
} finally {
|
||||
removePathIfExists(tempInstallDir);
|
||||
}
|
||||
}
|
||||
|
||||
function installPluginRuntimeDepsWithRetries(params) {
|
||||
const { attempts = 3 } = params;
|
||||
let lastError;
|
||||
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
||||
try {
|
||||
params.install({ ...params.installParams, attempt });
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (attempt === attempts) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
export function stageBundledPluginRuntimeDeps(params = {}) {
|
||||
const repoRoot = params.cwd ?? params.repoRoot ?? process.cwd();
|
||||
const installPluginRuntimeDepsImpl =
|
||||
params.installPluginRuntimeDepsImpl ?? installPluginRuntimeDeps;
|
||||
const installAttempts = params.installAttempts ?? 3;
|
||||
for (const pluginDir of listBundledPluginRuntimeDirs(repoRoot)) {
|
||||
const pluginId = path.basename(pluginDir);
|
||||
const packageJson = sanitizeBundledManifestForRuntimeInstall(pluginDir);
|
||||
const nodeModulesDir = path.join(pluginDir, "node_modules");
|
||||
const stampPath = resolveRuntimeDepsStampPath(pluginDir);
|
||||
if (!hasRuntimeDeps(packageJson) || !shouldStageRuntimeDeps(packageJson)) {
|
||||
removePathIfExists(nodeModulesDir);
|
||||
removePathIfExists(stampPath);
|
||||
continue;
|
||||
}
|
||||
const fingerprint = createRuntimeDepsFingerprint(packageJson);
|
||||
const stamp = readRuntimeDepsStamp(stampPath);
|
||||
if (fs.existsSync(nodeModulesDir) && stamp?.fingerprint === fingerprint) {
|
||||
continue;
|
||||
}
|
||||
installPluginRuntimeDepsWithRetries({
|
||||
attempts: installAttempts,
|
||||
install: installPluginRuntimeDepsImpl,
|
||||
installParams: {
|
||||
fingerprint,
|
||||
packageJson,
|
||||
pluginDir,
|
||||
pluginId,
|
||||
repoRoot,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
stageBundledPluginRuntimeDeps();
|
||||
}
|
||||
78
nix/scripts/build-root.sh
Normal file
78
nix/scripts/build-root.sh
Normal file
@ -0,0 +1,78 @@
|
||||
#!/bin/sh
|
||||
|
||||
openclaw_build_root_file() {
|
||||
if [ -n "${OPENCLAW_BUILD_ROOT_FILE:-}" ]; then
|
||||
printf '%s\n' "$OPENCLAW_BUILD_ROOT_FILE"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ -n "${NIX_BUILD_TOP:-}" ]; then
|
||||
printf '%s\n' "$NIX_BUILD_TOP/.openclaw-build-root"
|
||||
return
|
||||
fi
|
||||
|
||||
printf '%s\n' "$PWD/.openclaw-build-root"
|
||||
}
|
||||
|
||||
openclaw_init_output_build_root() {
|
||||
if [ -z "${out:-}" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
build_root="${NIX_BUILD_TOP:-${TMPDIR:-/tmp}}/.openclaw-build"
|
||||
build_root_file="$(openclaw_build_root_file)"
|
||||
|
||||
rm -rf "$build_root"
|
||||
mkdir -p "$build_root"
|
||||
( tar -cf - . ) | ( cd "$build_root" && tar -xf - )
|
||||
chmod -R u+w "$build_root"
|
||||
printf '%s\n' "$build_root" > "$build_root_file"
|
||||
cd "$build_root"
|
||||
}
|
||||
|
||||
openclaw_enter_build_root() {
|
||||
build_root_file="$(openclaw_build_root_file)"
|
||||
if [ ! -f "$build_root_file" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
build_root="$(cat "$build_root_file")"
|
||||
if [ -n "$build_root" ] && [ -d "$build_root" ]; then
|
||||
cd "$build_root"
|
||||
fi
|
||||
}
|
||||
|
||||
openclaw_cleanup_output_pnpm_store() {
|
||||
build_root_file="$(openclaw_build_root_file)"
|
||||
build_root=""
|
||||
store_path=""
|
||||
|
||||
if [ -f "$build_root_file" ]; then
|
||||
build_root="$(cat "$build_root_file")"
|
||||
fi
|
||||
|
||||
if [ -n "$build_root" ] && [ -f "$build_root/${PNPM_STORE_PATH_FILE:-.pnpm-store-path}" ]; then
|
||||
store_path="$(cat "$build_root/${PNPM_STORE_PATH_FILE:-.pnpm-store-path}")"
|
||||
fi
|
||||
|
||||
cd "${NIX_BUILD_TOP:-/tmp}" 2>/dev/null || cd / || true
|
||||
|
||||
case "$store_path" in
|
||||
"$out"/*) rm -rf "$store_path" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
openclaw_cleanup_output_build_root() {
|
||||
build_root_file="$(openclaw_build_root_file)"
|
||||
build_root=""
|
||||
|
||||
if [ -f "$build_root_file" ]; then
|
||||
build_root="$(cat "$build_root_file")"
|
||||
fi
|
||||
|
||||
openclaw_cleanup_output_pnpm_store
|
||||
|
||||
case "$build_root" in
|
||||
"$out"/*) rm -rf "$build_root" ;;
|
||||
esac
|
||||
}
|
||||
152
nix/scripts/check-config-validity.mjs
Executable file → Normal file
152
nix/scripts/check-config-validity.mjs
Executable file → Normal file
@ -1,93 +1,103 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const configPath = process.env.OPENCLAW_CONFIG_PATH;
|
||||
const srcRoot = process.env.OPENCLAW_SRC;
|
||||
const gatewayPackage = process.env.OPENCLAW_GATEWAY;
|
||||
const expectedWorkspace = process.env.OPENCLAW_EXPECTED_WORKSPACE;
|
||||
|
||||
if (!configPath) {
|
||||
console.error("OPENCLAW_CONFIG_PATH is not set");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!srcRoot) {
|
||||
console.error("OPENCLAW_SRC is not set");
|
||||
if (!gatewayPackage) {
|
||||
console.error("OPENCLAW_GATEWAY is not set");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const legacyValidationPath = path.join(srcRoot, "dist", "config", "validation.js");
|
||||
const distDir = path.join(srcRoot, "dist");
|
||||
if (!expectedWorkspace) {
|
||||
console.error("OPENCLAW_EXPECTED_WORKSPACE is not set");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let validateConfigObject = null;
|
||||
const openclaw = path.join(gatewayPackage, "bin", "openclaw");
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-config-validity-"));
|
||||
|
||||
if (fs.existsSync(legacyValidationPath)) {
|
||||
const moduleUrl = pathToFileURL(legacyValidationPath).href;
|
||||
const legacyModule = await import(moduleUrl);
|
||||
validateConfigObject = legacyModule.validateConfigObject;
|
||||
} else if (fs.existsSync(distDir)) {
|
||||
const candidates = fs.readdirSync(distDir)
|
||||
.filter((name) => name.startsWith("config-") && name.endsWith(".js"));
|
||||
try {
|
||||
const env = {
|
||||
...process.env,
|
||||
HOME: path.join(tmpDir, "home"),
|
||||
XDG_CONFIG_HOME: path.join(tmpDir, "config"),
|
||||
XDG_CACHE_HOME: path.join(tmpDir, "cache"),
|
||||
XDG_DATA_HOME: path.join(tmpDir, "data"),
|
||||
OPENCLAW_CONFIG_PATH: configPath,
|
||||
OPENCLAW_STATE_DIR: path.join(tmpDir, "state"),
|
||||
OPENCLAW_LOG_DIR: path.join(tmpDir, "logs"),
|
||||
OPENCLAW_NIX_MODE: "1",
|
||||
NO_COLOR: "1",
|
||||
};
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const candidatePath = path.join(distDir, candidate);
|
||||
const contents = fs.readFileSync(candidatePath, "utf8");
|
||||
|
||||
// Newer gateway bundles often only export validateConfigObjectWithPlugins (aliased),
|
||||
// while still containing an internal validateConfigObject function.
|
||||
if (!contents.includes("validateConfigObject") && !contents.includes("validateConfigObjectWithPlugins")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (contents.includes("./entry.js")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidateModule = await import(pathToFileURL(candidatePath).href);
|
||||
|
||||
// Prefer the plain validator when exported.
|
||||
if (typeof candidateModule.validateConfigObject === "function") {
|
||||
validateConfigObject = candidateModule.validateConfigObject;
|
||||
break;
|
||||
}
|
||||
|
||||
// Fall back to the plugin-aware validator (what most bundles export today).
|
||||
if (typeof candidateModule.validateConfigObjectWithPlugins === "function") {
|
||||
validateConfigObject = candidateModule.validateConfigObjectWithPlugins;
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle minified alias exports.
|
||||
let match = contents.match(/validateConfigObject as ([A-Za-z0-9_$]+)/);
|
||||
if (match && typeof candidateModule[match[1]] === "function") {
|
||||
validateConfigObject = candidateModule[match[1]];
|
||||
break;
|
||||
}
|
||||
|
||||
match = contents.match(/validateConfigObjectWithPlugins as ([A-Za-z0-9_$]+)/);
|
||||
if (match && typeof candidateModule[match[1]] === "function") {
|
||||
validateConfigObject = candidateModule[match[1]];
|
||||
break;
|
||||
}
|
||||
for (const key of [
|
||||
"HOME",
|
||||
"XDG_CONFIG_HOME",
|
||||
"XDG_CACHE_HOME",
|
||||
"XDG_DATA_HOME",
|
||||
"OPENCLAW_STATE_DIR",
|
||||
"OPENCLAW_LOG_DIR",
|
||||
]) {
|
||||
fs.mkdirSync(env[key], { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof validateConfigObject !== "function") {
|
||||
console.error(`Missing validation module: ${legacyValidationPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const validate = spawnSync(openclaw, ["config", "validate", "--json"], {
|
||||
env,
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
const raw = fs.readFileSync(configPath, "utf8");
|
||||
const parsed = JSON.parse(raw);
|
||||
|
||||
const result = validateConfigObject(parsed);
|
||||
if (!result.ok) {
|
||||
console.error("OpenClaw config validation failed:");
|
||||
for (const issue of result.issues ?? []) {
|
||||
const pathLabel = issue.path ? ` ${issue.path}` : "";
|
||||
console.error(`- ${pathLabel}: ${issue.message}`);
|
||||
if (validate.status !== 0) {
|
||||
if (validate.stdout) {
|
||||
process.stdout.write(validate.stdout);
|
||||
}
|
||||
if (validate.stderr) {
|
||||
process.stderr.write(validate.stderr);
|
||||
}
|
||||
console.error(`openclaw config validation failed with exit code ${validate.status ?? "unknown"}`);
|
||||
process.exit(validate.status ?? 1);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("openclaw config validation: ok");
|
||||
const validation = JSON.parse(validate.stdout);
|
||||
if (!validation || validation.valid !== true) {
|
||||
console.error("openclaw config validation did not report valid=true");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const workspace = spawnSync(openclaw, ["config", "get", "agents.defaults.workspace", "--json"], {
|
||||
env,
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
if (workspace.status !== 0) {
|
||||
if (workspace.stdout) {
|
||||
process.stdout.write(workspace.stdout);
|
||||
}
|
||||
if (workspace.stderr) {
|
||||
process.stderr.write(workspace.stderr);
|
||||
}
|
||||
console.error(`openclaw config get failed with exit code ${workspace.status ?? "unknown"}`);
|
||||
process.exit(workspace.status ?? 1);
|
||||
}
|
||||
|
||||
const actualWorkspace = JSON.parse(workspace.stdout);
|
||||
if (actualWorkspace !== expectedWorkspace) {
|
||||
console.error(
|
||||
`openclaw config returned unexpected workspace: ${JSON.stringify(actualWorkspace)} != ${JSON.stringify(expectedWorkspace)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("openclaw config validation: ok");
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
35
nix/scripts/check-openclaw-bin-surface.sh
Executable file
35
nix/scripts/check-openclaw-bin-surface.sh
Executable file
@ -0,0 +1,35 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ -z "${OPENCLAW_PACKAGE:-}" ]; then
|
||||
echo "OPENCLAW_PACKAGE is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bin_dir="${OPENCLAW_PACKAGE}/bin"
|
||||
openclaw_bin="${bin_dir}/openclaw"
|
||||
|
||||
if [ ! -x "$openclaw_bin" ]; then
|
||||
echo "Missing executable: $openclaw_bin" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
extra_bins="$(find "$bin_dir" -mindepth 1 -maxdepth 1 -print | while IFS= read -r entry; do
|
||||
name="$(basename "$entry")"
|
||||
if [ "$name" != "openclaw" ]; then
|
||||
printf '%s\n' "$name"
|
||||
fi
|
||||
done)"
|
||||
|
||||
if [ -n "$extra_bins" ]; then
|
||||
echo "openclaw package exposes internal runtime tools in bin:" >&2
|
||||
printf '%s\n' "$extra_bins" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -q 'PATH' "$openclaw_bin"; then
|
||||
echo "openclaw wrapper does not set the internal runtime tool PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "openclaw bin surface: ok"
|
||||
85
nix/scripts/check-openclaw-qmd-runtime.sh
Executable file
85
nix/scripts/check-openclaw-qmd-runtime.sh
Executable file
@ -0,0 +1,85 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ -z "${OPENCLAW_PACKAGE:-}" ]; then
|
||||
echo "OPENCLAW_PACKAGE is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${QMD_PACKAGE:-}" ]; then
|
||||
echo "QMD_PACKAGE is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
openclaw_bin="${OPENCLAW_PACKAGE}/bin/openclaw"
|
||||
qmd_bin="${QMD_PACKAGE}/bin/qmd"
|
||||
|
||||
if [ ! -x "$openclaw_bin" ]; then
|
||||
echo "Missing executable: $openclaw_bin" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -x "$qmd_bin" ]; then
|
||||
echo "Missing executable: $qmd_bin" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! "$qmd_bin" --version >/dev/null; then
|
||||
echo "qmd --version failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -q "${QMD_PACKAGE}/bin" "$openclaw_bin"; then
|
||||
echo "openclaw wrapper does not include qmd on the internal runtime PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q "OPENCLAW_PINNED_WRITE_PYTHON" "$openclaw_bin"; then
|
||||
echo "openclaw wrapper does not pin a Nix Python for safe writes" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmp_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp_dir"' EXIT
|
||||
|
||||
mkdir -p "$tmp_dir/home" "$tmp_dir/state" "$tmp_dir/config" "$tmp_dir/cache" "$tmp_dir/data" "$tmp_dir/logs"
|
||||
cat > "$tmp_dir/state/openclaw.json" <<'JSON'
|
||||
{
|
||||
"gateway": {
|
||||
"mode": "local"
|
||||
},
|
||||
"memory": {
|
||||
"backend": "qmd"
|
||||
}
|
||||
}
|
||||
JSON
|
||||
|
||||
env \
|
||||
HOME="$tmp_dir/home" \
|
||||
XDG_CONFIG_HOME="$tmp_dir/config" \
|
||||
XDG_CACHE_HOME="$tmp_dir/cache" \
|
||||
XDG_DATA_HOME="$tmp_dir/data" \
|
||||
OPENCLAW_CONFIG_PATH="$tmp_dir/state/openclaw.json" \
|
||||
OPENCLAW_STATE_DIR="$tmp_dir/state" \
|
||||
OPENCLAW_LOG_DIR="$tmp_dir/logs" \
|
||||
OPENCLAW_NIX_MODE=1 \
|
||||
NO_COLOR=1 \
|
||||
"$openclaw_bin" config validate --json >/dev/null
|
||||
|
||||
backend="$(
|
||||
env \
|
||||
HOME="$tmp_dir/home" \
|
||||
XDG_CONFIG_HOME="$tmp_dir/config" \
|
||||
XDG_CACHE_HOME="$tmp_dir/cache" \
|
||||
XDG_DATA_HOME="$tmp_dir/data" \
|
||||
OPENCLAW_CONFIG_PATH="$tmp_dir/state/openclaw.json" \
|
||||
OPENCLAW_STATE_DIR="$tmp_dir/state" \
|
||||
OPENCLAW_LOG_DIR="$tmp_dir/logs" \
|
||||
OPENCLAW_NIX_MODE=1 \
|
||||
NO_COLOR=1 \
|
||||
"$openclaw_bin" config get memory.backend --json
|
||||
)"
|
||||
|
||||
if [ "$backend" != '"qmd"' ]; then
|
||||
echo "OpenClaw did not read opt-in QMD memory config: $backend" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "openclaw qmd runtime: ok"
|
||||
@ -17,11 +17,76 @@ require_path() {
|
||||
|
||||
require_path "${root}/extensions"
|
||||
require_path "${root}/extensions/memory-core"
|
||||
require_path "${root}/extensions/memory-core/openclaw.plugin.json"
|
||||
require_path "${root}/dist/extensions/memory-core/openclaw.plugin.json"
|
||||
require_path "${root}/dist-runtime/extensions"
|
||||
require_path "${root}/dist-runtime/extensions/memory-core/openclaw.plugin.json"
|
||||
require_path "${root}/dist-runtime/extensions/acpx/openclaw.plugin.json"
|
||||
require_path "${root}/dist-runtime/extensions/acpx/package.json"
|
||||
require_path "${root}/dist-runtime/extensions/acpx/index.js"
|
||||
require_path "${root}/dist-runtime/extensions/acpx/error-format.mjs"
|
||||
require_path "${root}/dist-runtime/extensions/acpx/mcp-command-line.mjs"
|
||||
require_path "${root}/dist-runtime/extensions/acpx/mcp-proxy.mjs"
|
||||
require_path "${root}/docs/reference/templates"
|
||||
require_path "${root}/docs/reference/templates/AGENTS.md"
|
||||
require_path "${root}/docs/reference/templates/SOUL.md"
|
||||
require_path "${root}/docs/reference/templates/TOOLS.md"
|
||||
require_path "${root}/skills"
|
||||
require_path "${root}/node_modules/hasown"
|
||||
require_path "${root}/node_modules/combined-stream"
|
||||
|
||||
public_surface_loader="$(
|
||||
find "${root}/dist" -name "*.js" -type f -exec grep -sl "function loadBundledPluginPublicArtifactModuleSync" {} + | head -1
|
||||
)"
|
||||
if [ -z "$public_surface_loader" ]; then
|
||||
echo "Missing bundled plugin public surface loader" >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -q "rejectHardlinks: true" "$public_surface_loader"; then
|
||||
echo "Bundled plugin public surface loader still rejects hardlinked package files" >&2
|
||||
exit 1
|
||||
fi
|
||||
export PUBLIC_SURFACE_LOADER="$public_surface_loader"
|
||||
node --input-type=module <<'NODE'
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const loaderPath = process.env.PUBLIC_SURFACE_LOADER;
|
||||
if (!loaderPath) {
|
||||
throw new Error("PUBLIC_SURFACE_LOADER is not set");
|
||||
}
|
||||
|
||||
const loader = await import(pathToFileURL(loaderPath).href);
|
||||
const loadBundledPluginPublicArtifactModuleSync =
|
||||
loader.loadBundledPluginPublicArtifactModuleSync ?? loader.t;
|
||||
|
||||
if (typeof loadBundledPluginPublicArtifactModuleSync !== "function") {
|
||||
throw new Error("Bundled plugin public surface loader export not found");
|
||||
}
|
||||
|
||||
loadBundledPluginPublicArtifactModuleSync({
|
||||
dirName: "openai",
|
||||
artifactBasename: "provider-policy-api.js",
|
||||
});
|
||||
NODE
|
||||
|
||||
require_js_alias_target() {
|
||||
alias="$1"
|
||||
alias_path="${root}/dist/${alias}"
|
||||
require_path "$alias_path"
|
||||
|
||||
target="$(sed -n 's/^export \* from "\.\/\(.*\)";$/\1/p' "$alias_path" | head -1)"
|
||||
if [ -z "$target" ]; then
|
||||
echo "Alias has no export target: $alias_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
require_path "${root}/dist/${target}"
|
||||
}
|
||||
|
||||
require_js_alias_target "runtime-model-auth.runtime.js"
|
||||
|
||||
if ! find "${root}/skills" -name SKILL.md -type f | grep -q .; then
|
||||
echo "Missing bundled SKILL.md files under ${root}/skills" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "openclaw package contents: ok"
|
||||
|
||||
@ -42,15 +42,21 @@ fi
|
||||
cp "$CONFIG_OPTIONS_GENERATOR" ./generate-config-options.ts
|
||||
cp "$NODE_ENGINE_CHECK" ./check-node-engine.ts
|
||||
|
||||
if [ ! -x "./node_modules/.bin/tsx" ]; then
|
||||
echo "tsx not found at ./node_modules/.bin/tsx (run gateway-tests-build.sh first)" >&2
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
echo "node not found in PATH (run source-checks-build.sh first)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
./node_modules/.bin/tsx ./check-node-engine.ts --repo .
|
||||
tsx_cli="./node_modules/tsx/dist/cli.mjs"
|
||||
if [ ! -f "$tsx_cli" ]; then
|
||||
echo "tsx CLI not found at $tsx_cli (run source-checks-build.sh first)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
node "$tsx_cli" ./check-node-engine.ts --repo .
|
||||
|
||||
output_path="./generated-config-options.nix"
|
||||
|
||||
./node_modules/.bin/tsx ./generate-config-options.ts --repo . --out "$output_path"
|
||||
node "$tsx_cli" ./generate-config-options.ts --repo . --out "$output_path"
|
||||
|
||||
diff -u "$CONFIG_OPTIONS_GOLDEN" "$output_path"
|
||||
|
||||
@ -1,6 +1,22 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
log_step() {
|
||||
if [ "${OPENCLAW_NIX_TIMINGS:-1}" != "1" ]; then
|
||||
"$@"
|
||||
return
|
||||
fi
|
||||
|
||||
name="$1"
|
||||
shift
|
||||
|
||||
start=$(date +%s)
|
||||
printf '>> [timing] %s...\n' "$name" >&2
|
||||
"$@"
|
||||
end=$(date +%s)
|
||||
printf '>> [timing] %s: %ss\n' "$name" "$((end - start))" >&2
|
||||
}
|
||||
|
||||
if [ -z "${GATEWAY_PREBUILD_SH:-}" ]; then
|
||||
echo "GATEWAY_PREBUILD_SH is not set" >&2
|
||||
exit 1
|
||||
@ -27,13 +43,87 @@ export NPM_CONFIG_STORE_DIR="$store_path"
|
||||
export NPM_CONFIG_STORE_PATH="$store_path"
|
||||
export HOME="$(mktemp -d)"
|
||||
|
||||
pnpm install --offline --frozen-lockfile --ignore-scripts --store-dir "$store_path"
|
||||
chmod -R u+w node_modules
|
||||
log_step "pnpm install (offline, frozen, ignore-scripts)" pnpm install --offline --frozen-lockfile --ignore-scripts --store-dir "$store_path"
|
||||
|
||||
log_step "chmod node_modules writable" chmod -R u+w node_modules
|
||||
|
||||
# sharp may leave build artifacts around; remove to keep output smaller + avoid stale builds.
|
||||
rm -rf node_modules/.pnpm/sharp@*/node_modules/sharp/src/build
|
||||
|
||||
# Rebuild only native deps (avoid `pnpm rebuild` over the entire workspace).
|
||||
# node-llama-cpp postinstall attempts to download/compile llama.cpp (network blocked in Nix).
|
||||
NODE_LLAMA_CPP_SKIP_DOWNLOAD=1 pnpm rebuild
|
||||
bash -e -c ". \"$STDENV_SETUP\"; patchShebangs node_modules/.bin"
|
||||
pnpm build
|
||||
pnpm ui:build
|
||||
CI=true pnpm prune --prod
|
||||
# Also defensively disable other common downloaders.
|
||||
rebuild_list="$(jq -r '.pnpm.onlyBuiltDependencies // [] | .[]' package.json 2>/dev/null || true)"
|
||||
if [ -n "$rebuild_list" ]; then
|
||||
log_step "pnpm rebuild (onlyBuiltDependencies)" env \
|
||||
NODE_LLAMA_CPP_SKIP_DOWNLOAD=1 \
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \
|
||||
PUPPETEER_SKIP_DOWNLOAD=1 \
|
||||
ELECTRON_SKIP_BINARY_DOWNLOAD=1 \
|
||||
pnpm rebuild $rebuild_list
|
||||
else
|
||||
log_step "pnpm rebuild (all)" env \
|
||||
NODE_LLAMA_CPP_SKIP_DOWNLOAD=1 \
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \
|
||||
PUPPETEER_SKIP_DOWNLOAD=1 \
|
||||
ELECTRON_SKIP_BINARY_DOWNLOAD=1 \
|
||||
pnpm rebuild
|
||||
fi
|
||||
|
||||
log_step "patchShebangs node_modules/.bin" bash -e -c ". \"$STDENV_SETUP\"; patchShebangs node_modules/.bin"
|
||||
|
||||
# Git tarball dependencies do not get their npm prepack output in offline Nix
|
||||
# builds. OpenClaw currently depends on @openclaw/fs-safe this way.
|
||||
if [ -n "${OPENCLAW_FS_SAFE_SOURCE:-}" ] && [ ! -d "node_modules/@openclaw/fs-safe/dist" ]; then
|
||||
rm -rf node_modules/@openclaw/fs-safe
|
||||
mkdir -p node_modules/@openclaw
|
||||
cp -R "$OPENCLAW_FS_SAFE_SOURCE" node_modules/@openclaw/fs-safe
|
||||
chmod -R u+w node_modules/@openclaw/fs-safe
|
||||
log_step "build dependency: @openclaw/fs-safe" pnpm exec tsc -p node_modules/@openclaw/fs-safe/tsconfig.json
|
||||
fi
|
||||
|
||||
# Ensure rolldown is found from workspace bins in offline/sandbox builds.
|
||||
if [ -d "node_modules/.pnpm/node_modules/.bin" ]; then
|
||||
export PATH="$PWD/node_modules/.pnpm/node_modules/.bin:$PATH"
|
||||
fi
|
||||
|
||||
# Break down `pnpm build` (upstream package.json) so we can profile it while
|
||||
# still using upstream's asset hooks. v2026.5.7 has the older canvas-only helper;
|
||||
# newer OpenClaw has the generic bundled-plugin asset runner.
|
||||
if [ -f "scripts/bundled-plugin-assets.mjs" ]; then
|
||||
log_step "build: plugins:assets:build" node scripts/bundled-plugin-assets.mjs --phase build
|
||||
else
|
||||
log_step "build: canvas:a2ui:bundle" node scripts/bundle-a2ui.mjs
|
||||
fi
|
||||
log_step "build: tsdown" pnpm exec tsdown
|
||||
log_step "build: runtime-postbuild" node scripts/runtime-postbuild.mjs
|
||||
if [ -f "scripts/stage-bundled-plugin-runtime.mjs" ]; then
|
||||
log_step "build: stage bundled plugin runtime" node scripts/stage-bundled-plugin-runtime.mjs
|
||||
fi
|
||||
log_step "build: plugin-sdk dts" pnpm build:plugin-sdk:dts
|
||||
log_step "build: write-plugin-sdk-entry-dts" node --import tsx scripts/write-plugin-sdk-entry-dts.ts
|
||||
if [ -f "scripts/copy-plugin-sdk-root-alias.mjs" ]; then
|
||||
log_step "build: copy-plugin-sdk-root-alias" node scripts/copy-plugin-sdk-root-alias.mjs
|
||||
fi
|
||||
if [ -f "scripts/copy-bundled-plugin-metadata.mjs" ]; then
|
||||
log_step "build: copy-bundled-plugin-metadata" node scripts/copy-bundled-plugin-metadata.mjs
|
||||
fi
|
||||
if [ -f "scripts/bundled-plugin-assets.mjs" ]; then
|
||||
log_step "build: plugins:assets:copy" node scripts/bundled-plugin-assets.mjs --phase copy
|
||||
else
|
||||
log_step "build: canvas-a2ui-copy" node --import tsx scripts/canvas-a2ui-copy.ts
|
||||
fi
|
||||
log_step "build: copy-hook-metadata" node --import tsx scripts/copy-hook-metadata.ts
|
||||
log_step "build: write-build-info" node --import tsx scripts/write-build-info.ts
|
||||
log_step "build: write-cli-compat" node --import tsx scripts/write-cli-compat.ts
|
||||
|
||||
log_step "ui:build" pnpm ui:build
|
||||
|
||||
log_step "pnpm prune --prod" env CI=true pnpm prune --prod
|
||||
|
||||
# Reduce output size (pnpm implementation detail; safe to remove)
|
||||
rm -rf node_modules/.pnpm/node_modules
|
||||
|
||||
# pnpm prune can leave orphaned .bin links behind for removed prod deps.
|
||||
# Keep install-phase symlink validation strict by dropping only broken links here.
|
||||
find node_modules -xtype l -delete
|
||||
|
||||
@ -1,15 +1,90 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
log_step() {
|
||||
if [ "${OPENCLAW_NIX_TIMINGS:-1}" != "1" ]; then
|
||||
"$@"
|
||||
return
|
||||
fi
|
||||
|
||||
name="$1"
|
||||
shift
|
||||
|
||||
start=$(date +%s)
|
||||
printf '>> [timing] %s...\n' "$name" >&2
|
||||
"$@"
|
||||
end=$(date +%s)
|
||||
printf '>> [timing] %s: %ss\n' "$name" "$((end - start))" >&2
|
||||
}
|
||||
|
||||
if [ -n "${OPENCLAW_BUILD_ROOT_SH:-}" ]; then
|
||||
. "$OPENCLAW_BUILD_ROOT_SH"
|
||||
openclaw_enter_build_root
|
||||
fi
|
||||
|
||||
check_no_broken_symlinks() {
|
||||
root="$1"
|
||||
if [ ! -d "$root" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
broken_tmp="$(mktemp)"
|
||||
# Portable and faster than `find ... -exec test -e {} \;` on large trees.
|
||||
find "$root" -type l -print | while IFS= read -r link; do
|
||||
[ -e "$link" ] || printf '%s\n' "$link"
|
||||
done > "$broken_tmp"
|
||||
if [ -s "$broken_tmp" ]; then
|
||||
echo "dangling symlinks found under $root" >&2
|
||||
cat "$broken_tmp" >&2
|
||||
rm -f "$broken_tmp"
|
||||
return 1
|
||||
fi
|
||||
rm -f "$broken_tmp"
|
||||
}
|
||||
|
||||
copy_extension_manifests() {
|
||||
if [ ! -d extensions ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$out/lib/openclaw/extensions"
|
||||
find extensions -mindepth 2 -maxdepth 2 -name openclaw.plugin.json -type f -print | while IFS= read -r manifest; do
|
||||
name="$(basename "$(dirname "$manifest")")"
|
||||
mkdir -p "$out/lib/openclaw/extensions/$name"
|
||||
cp "$manifest" "$out/lib/openclaw/extensions/$name/openclaw.plugin.json"
|
||||
done
|
||||
}
|
||||
|
||||
mkdir -p "$out/lib/openclaw" "$out/bin"
|
||||
|
||||
cp -r dist node_modules package.json ui "$out/lib/openclaw/"
|
||||
set -- dist node_modules package.json
|
||||
if [ -d dist-runtime ]; then
|
||||
set -- "$@" dist-runtime
|
||||
fi
|
||||
log_step "copy build outputs" cp -R "$@" "$out/lib/openclaw/"
|
||||
if [ -d extensions ]; then
|
||||
cp -r extensions "$out/lib/openclaw/"
|
||||
log_step "copy extension manifests" copy_extension_manifests
|
||||
fi
|
||||
if [ -d skills ]; then
|
||||
log_step "copy bundled skills" cp -r skills "$out/lib/openclaw/"
|
||||
fi
|
||||
|
||||
# Gateway plugin discovery looks under dist/extensions/*/openclaw.plugin.json.
|
||||
# Upstream's build emits JS into dist/extensions but leaves manifests in extensions/.
|
||||
if [ -d "$out/lib/openclaw/extensions" ] && [ -d "$out/lib/openclaw/dist/extensions" ]; then
|
||||
for manifest in "$out/lib/openclaw/extensions"/*/openclaw.plugin.json; do
|
||||
[ -f "$manifest" ] || continue
|
||||
name="$(basename "$(dirname "$manifest")")"
|
||||
dist_ext="$out/lib/openclaw/dist/extensions/$name"
|
||||
if [ -d "$dist_ext" ] && [ ! -f "$dist_ext/openclaw.plugin.json" ]; then
|
||||
cp "$manifest" "$dist_ext/openclaw.plugin.json"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -d docs/reference/templates ]; then
|
||||
mkdir -p "$out/lib/openclaw/docs/reference"
|
||||
cp -r docs/reference/templates "$out/lib/openclaw/docs/reference/"
|
||||
log_step "copy reference templates" cp -r docs/reference/templates "$out/lib/openclaw/docs/reference/"
|
||||
fi
|
||||
|
||||
if [ -z "${STDENV_SETUP:-}" ]; then
|
||||
@ -21,11 +96,6 @@ if [ ! -f "$STDENV_SETUP" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bash -e -c '. "$STDENV_SETUP"; patchShebangs "$out/lib/openclaw/node_modules/.bin"'
|
||||
if [ -d "$out/lib/openclaw/ui/node_modules/.bin" ]; then
|
||||
bash -e -c '. "$STDENV_SETUP"; patchShebangs "$out/lib/openclaw/ui/node_modules/.bin"'
|
||||
fi
|
||||
|
||||
# Work around missing dependency declaration in pi-coding-agent (strip-ansi).
|
||||
# Ensure it is resolvable at runtime without changing upstream.
|
||||
pi_pkg="$(find "$out/lib/openclaw/node_modules/.pnpm" -path "*/node_modules/@mariozechner/pi-coding-agent" -print | head -n 1)"
|
||||
@ -80,4 +150,13 @@ if [ -n "$hasown_src" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
bash -e -c '. "$STDENV_SETUP"; makeWrapper "$NODE_BIN" "$out/bin/openclaw" --add-flags "$out/lib/openclaw/dist/index.js" --set-default OPENCLAW_NIX_MODE "1"'
|
||||
if [ -n "${OPENCLAW_BUILD_ROOT_SH:-}" ]; then
|
||||
openclaw_cleanup_output_pnpm_store
|
||||
fi
|
||||
|
||||
log_step "validate node_modules symlinks" check_no_broken_symlinks "$out/lib/openclaw/node_modules"
|
||||
if [ -d "$out/lib/openclaw/dist-runtime" ]; then
|
||||
log_step "validate dist-runtime symlinks" check_no_broken_symlinks "$out/lib/openclaw/dist-runtime"
|
||||
fi
|
||||
|
||||
log_step "wrap openclaw" bash -e -c '. "$STDENV_SETUP"; makeWrapper "$NODE_BIN" "$out/bin/openclaw" --add-flags "$out/lib/openclaw/dist/index.js" --set-default OPENCLAW_NIX_MODE "1"'
|
||||
|
||||
@ -4,6 +4,19 @@ if [ -f package.json ]; then
|
||||
"$REMOVE_PACKAGE_MANAGER_FIELD_SH" package.json
|
||||
fi
|
||||
|
||||
if [ -n "${PATCH_BUNDLED_RUNTIME_DEPS_SCRIPT:-}" ] && [ -f scripts/stage-bundled-plugin-runtime-deps.mjs ]; then
|
||||
cp "$PATCH_BUNDLED_RUNTIME_DEPS_SCRIPT" scripts/stage-bundled-plugin-runtime-deps.mjs
|
||||
chmod u+w scripts/stage-bundled-plugin-runtime-deps.mjs
|
||||
fi
|
||||
|
||||
if [ -n "${PATCH_PUBLIC_SURFACE_HARDLINKS:-}" ]; then
|
||||
patch -p1 < "$PATCH_PUBLIC_SURFACE_HARDLINKS"
|
||||
fi
|
||||
|
||||
if [ -n "${PATCH_SKIP_PLUGIN_AUTO_ENABLE_NIX_MODE:-}" ]; then
|
||||
patch -p1 < "$PATCH_SKIP_PLUGIN_AUTO_ENABLE_NIX_MODE"
|
||||
fi
|
||||
|
||||
if [ -f src/logging/logger.ts ]; then
|
||||
if ! grep -q "OPENCLAW_LOG_DIR" src/logging/logger.ts; then
|
||||
sed -i 's/export const DEFAULT_LOG_DIR = "\/tmp\/openclaw";/export const DEFAULT_LOG_DIR = process.env.OPENCLAW_LOG_DIR ?? "\/tmp\/openclaw";/' src/logging/logger.ts
|
||||
@ -41,3 +54,61 @@ if [ -f src/docker-setup.test.ts ]; then
|
||||
sed -i 's|if \[\[ "${1:-}" == "compose" \]\]; then|if [ "${1:-}" = "compose" ]; then|' src/docker-setup.test.ts
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f src/gateway/test-helpers.mocks.ts ]; then
|
||||
if ! grep -q 'augmentModelCatalogWithProviderPlugins: async () => \[\]' src/gateway/test-helpers.mocks.ts; then
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
path = Path("src/gateway/test-helpers.mocks.ts")
|
||||
text = path.read_text()
|
||||
needle = '''vi.mock("../plugins/loader.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("../plugins/loader.js")>("../plugins/loader.js");
|
||||
return {
|
||||
...actual,
|
||||
loadOpenClawPlugins: () => getTestPluginRegistry(),
|
||||
};
|
||||
});
|
||||
'''
|
||||
replacement = needle + '''
|
||||
vi.mock("../plugins/provider-runtime.runtime.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../plugins/provider-runtime.runtime.js")>(
|
||||
"../plugins/provider-runtime.runtime.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
augmentModelCatalogWithProviderPlugins: async () => [],
|
||||
};
|
||||
});
|
||||
vi.mock("../plugins/web-search-providers.runtime.js", () => ({
|
||||
resolvePluginWebSearchProviders: () => [],
|
||||
resolveRuntimeWebSearchProviders: () => [],
|
||||
__testing: {
|
||||
resetWebSearchProviderSnapshotCacheForTests: () => {},
|
||||
},
|
||||
}));
|
||||
vi.mock("../plugins/web-fetch-providers.runtime.js", () => ({
|
||||
resolvePluginWebFetchProviders: () => [],
|
||||
resolveRuntimeWebFetchProviders: () => [],
|
||||
__testing: {
|
||||
resetWebFetchProviderSnapshotCacheForTests: () => {},
|
||||
},
|
||||
}));
|
||||
vi.mock("../plugins/web-provider-public-artifacts.explicit.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("../plugins/web-provider-public-artifacts.explicit.js")>(
|
||||
"../plugins/web-provider-public-artifacts.explicit.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
resolveBundledExplicitWebSearchProvidersFromPublicArtifacts: () => [],
|
||||
resolveBundledExplicitWebFetchProvidersFromPublicArtifacts: () => [],
|
||||
};
|
||||
});
|
||||
'''
|
||||
if needle not in text:
|
||||
raise SystemExit("gateway test mocks loader marker not found")
|
||||
path.write_text(text.replace(needle, replacement, 1))
|
||||
PY
|
||||
fi
|
||||
fi
|
||||
|
||||
@ -1,23 +1,59 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
log_step() {
|
||||
if [ "${OPENCLAW_NIX_TIMINGS:-1}" != "1" ]; then
|
||||
"$@"
|
||||
return
|
||||
fi
|
||||
|
||||
name="$1"
|
||||
shift
|
||||
|
||||
start=$(date +%s)
|
||||
printf '>> [timing] %s...\n' "$name" >&2
|
||||
"$@"
|
||||
end=$(date +%s)
|
||||
printf '>> [timing] %s: %ss\n' "$name" "$((end - start))" >&2
|
||||
}
|
||||
|
||||
store_path_file="${PNPM_STORE_PATH_FILE:-.pnpm-store-path}"
|
||||
store_path="$(mktemp -d)"
|
||||
|
||||
if [ -n "${OPENCLAW_BUILD_ROOT_SH:-}" ]; then
|
||||
. "$OPENCLAW_BUILD_ROOT_SH"
|
||||
openclaw_init_output_build_root
|
||||
fi
|
||||
|
||||
if [ -n "${out:-}" ]; then
|
||||
store_path="$out/.pnpm-store"
|
||||
rm -rf "$store_path"
|
||||
mkdir -p "$store_path"
|
||||
else
|
||||
store_path="$(mktemp -d)"
|
||||
fi
|
||||
|
||||
printf "%s" "$store_path" > "$store_path_file"
|
||||
|
||||
fetcherVersion=$(cat "$PNPM_DEPS/.fetcher-version" 2>/dev/null || echo 1)
|
||||
if [ "$fetcherVersion" -ge 3 ]; then
|
||||
tar --zstd -xf "$PNPM_DEPS/pnpm-store.tar.zst" -C "$store_path"
|
||||
# tar --zstd uses libzstd; on some platforms it ends up single-threaded.
|
||||
# Use zstd directly, bounded by Nix's build-core budget.
|
||||
zstd_threads="${NIX_BUILD_CORES:-2}"
|
||||
case "$zstd_threads" in
|
||||
''|*[!0-9]*) zstd_threads=2 ;;
|
||||
esac
|
||||
log_step "extract pnpm store (fetcherVersion=${fetcherVersion})" sh -c '
|
||||
zstd -d --threads="$3" < "$1" | tar -xf - -C "$2"
|
||||
' sh "$PNPM_DEPS/pnpm-store.tar.zst" "$store_path" "$zstd_threads"
|
||||
else
|
||||
cp -Tr "$PNPM_DEPS" "$store_path"
|
||||
log_step "copy pnpm store (fetcherVersion=${fetcherVersion})" cp -Tr "$PNPM_DEPS" "$store_path"
|
||||
fi
|
||||
|
||||
chmod -R +w "$store_path"
|
||||
log_step "chmod pnpm store writable" chmod -R +w "$store_path"
|
||||
|
||||
# pnpm --ignore-scripts marks tarball deps as "not built" and offline install
|
||||
# later refuses to use them; if a dep doesn't require build, promote it.
|
||||
"$PROMOTE_PNPM_INTEGRITY_SH" "$store_path"
|
||||
log_step "promote pnpm integrity" "$PROMOTE_PNPM_INTEGRITY_SH" "$store_path"
|
||||
|
||||
export REAL_NODE_GYP="$(command -v node-gyp)"
|
||||
wrapper_dir="$(mktemp -d)"
|
||||
|
||||
202
nix/scripts/gateway-smoke.mjs
Normal file
202
nix/scripts/gateway-smoke.mjs
Normal file
@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env node
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import net from "node:net";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { once } from "node:events";
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
|
||||
const gatewayPackage = process.env.OPENCLAW_GATEWAY;
|
||||
|
||||
if (!gatewayPackage) {
|
||||
console.error("OPENCLAW_GATEWAY is not set");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const openclaw = path.join(gatewayPackage, "bin", "openclaw");
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-gateway-smoke-"));
|
||||
const token = `smoke-${crypto.randomUUID()}`;
|
||||
const logs = { stdout: "", stderr: "" };
|
||||
|
||||
function appendLog(name, chunk) {
|
||||
logs[name] += chunk.toString();
|
||||
if (logs[name].length > 12000) {
|
||||
logs[name] = logs[name].slice(-12000);
|
||||
}
|
||||
}
|
||||
|
||||
async function freePort() {
|
||||
const server = net.createServer();
|
||||
server.listen(0, "127.0.0.1");
|
||||
await once(server, "listening");
|
||||
const address = server.address();
|
||||
const port = typeof address === "object" && address ? address.port : null;
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
if (!port) {
|
||||
throw new Error("failed to allocate a local port");
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function stopProcess(child) {
|
||||
if (child.exitCode !== null || child.signalCode !== null) {
|
||||
return;
|
||||
}
|
||||
child.kill("SIGTERM");
|
||||
const stopped = await Promise.race([
|
||||
once(child, "exit").then(() => true),
|
||||
sleep(3000).then(() => false),
|
||||
]);
|
||||
if (!stopped) {
|
||||
child.kill("SIGKILL");
|
||||
await once(child, "exit").catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function isolatedEnv() {
|
||||
const env = {
|
||||
...process.env,
|
||||
HOME: path.join(tmpDir, "home"),
|
||||
XDG_CONFIG_HOME: path.join(tmpDir, "config"),
|
||||
XDG_CACHE_HOME: path.join(tmpDir, "cache"),
|
||||
XDG_DATA_HOME: path.join(tmpDir, "data"),
|
||||
OPENCLAW_CONFIG_PATH: path.join(tmpDir, "state", "openclaw.json"),
|
||||
OPENCLAW_STATE_DIR: path.join(tmpDir, "state"),
|
||||
OPENCLAW_LOG_DIR: path.join(tmpDir, "logs"),
|
||||
OPENCLAW_NIX_MODE: "1",
|
||||
NO_COLOR: "1",
|
||||
};
|
||||
|
||||
for (const key of [
|
||||
"HOME",
|
||||
"XDG_CONFIG_HOME",
|
||||
"XDG_CACHE_HOME",
|
||||
"XDG_DATA_HOME",
|
||||
"OPENCLAW_STATE_DIR",
|
||||
"OPENCLAW_LOG_DIR",
|
||||
]) {
|
||||
fs.mkdirSync(env[key], { recursive: true });
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
const env = isolatedEnv();
|
||||
let gateway = null;
|
||||
let gatewayHealthy = false;
|
||||
|
||||
try {
|
||||
const version = spawnSync(openclaw, ["--version"], {
|
||||
env,
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (version.status !== 0 || !version.stdout.trim()) {
|
||||
process.stdout.write(version.stdout ?? "");
|
||||
process.stderr.write(version.stderr ?? "");
|
||||
throw new Error("openclaw --version failed");
|
||||
}
|
||||
|
||||
const port = await freePort();
|
||||
gateway = spawn(
|
||||
openclaw,
|
||||
[
|
||||
"gateway",
|
||||
"run",
|
||||
"--allow-unconfigured",
|
||||
"--bind",
|
||||
"loopback",
|
||||
"--port",
|
||||
String(port),
|
||||
"--auth",
|
||||
"token",
|
||||
"--token",
|
||||
token,
|
||||
"--ws-log",
|
||||
"compact",
|
||||
],
|
||||
{
|
||||
cwd: tmpDir,
|
||||
env,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
},
|
||||
);
|
||||
|
||||
gateway.stdout.on("data", (chunk) => appendLog("stdout", chunk));
|
||||
gateway.stderr.on("data", (chunk) => appendLog("stderr", chunk));
|
||||
|
||||
const deadline = Date.now() + 30000;
|
||||
let lastError = "";
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
if (gateway.exitCode !== null || gateway.signalCode !== null) {
|
||||
throw new Error(`gateway exited before health check: ${gateway.exitCode ?? gateway.signalCode}`);
|
||||
}
|
||||
|
||||
const health = spawnSync(
|
||||
openclaw,
|
||||
[
|
||||
"gateway",
|
||||
"health",
|
||||
"--url",
|
||||
`ws://127.0.0.1:${port}`,
|
||||
"--token",
|
||||
token,
|
||||
"--json",
|
||||
"--timeout",
|
||||
"3000",
|
||||
],
|
||||
{
|
||||
cwd: tmpDir,
|
||||
env,
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
|
||||
if (health.status === 0) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(health.stdout);
|
||||
} catch (err) {
|
||||
lastError = `health returned invalid JSON: ${health.stdout}${health.stderr}`;
|
||||
await sleep(500);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed?.ok === true) {
|
||||
console.log(`openclaw gateway smoke: ok (${version.stdout.trim()})`);
|
||||
gatewayHealthy = true;
|
||||
break;
|
||||
}
|
||||
lastError = `health JSON did not contain ok=true: ${health.stdout}`;
|
||||
} else {
|
||||
lastError = `${health.stdout}${health.stderr}`;
|
||||
}
|
||||
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
if (!gatewayHealthy) {
|
||||
throw new Error(`gateway health did not become ready: ${lastError.trim()}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(String(err));
|
||||
if (logs.stdout.trim()) {
|
||||
console.error("--- gateway stdout ---");
|
||||
console.error(logs.stdout.trim());
|
||||
}
|
||||
if (logs.stderr.trim()) {
|
||||
console.error("--- gateway stderr ---");
|
||||
console.error(logs.stderr.trim());
|
||||
}
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
if (gateway) {
|
||||
await stopProcess(gateway);
|
||||
}
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
if [ -z "${GATEWAY_PREBUILD_SH:-}" ]; then
|
||||
echo "GATEWAY_PREBUILD_SH is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
. "$GATEWAY_PREBUILD_SH"
|
||||
|
||||
store_path_file="${PNPM_STORE_PATH_FILE:-.pnpm-store-path}"
|
||||
if [ ! -f "$store_path_file" ]; then
|
||||
echo "pnpm store path file missing: $store_path_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
store_path="$(cat "$store_path_file")"
|
||||
export PNPM_STORE_DIR="$store_path"
|
||||
export PNPM_STORE_PATH="$store_path"
|
||||
export NPM_CONFIG_STORE_DIR="$store_path"
|
||||
export NPM_CONFIG_STORE_PATH="$store_path"
|
||||
export HOME="$(mktemp -d)"
|
||||
|
||||
pnpm install --offline --frozen-lockfile --ignore-scripts --store-dir "$store_path"
|
||||
if [ -z "${STDENV_SETUP:-}" ]; then
|
||||
echo "STDENV_SETUP is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$STDENV_SETUP" ]; then
|
||||
echo "STDENV_SETUP not found: $STDENV_SETUP" >&2
|
||||
exit 1
|
||||
fi
|
||||
bash -e -c ". \"$STDENV_SETUP\"; patchShebangs node_modules/.bin"
|
||||
@ -1,25 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
store_path_file="${PNPM_STORE_PATH_FILE:-.pnpm-store-path}"
|
||||
if [ -f "$store_path_file" ]; then
|
||||
store_path="$(cat "$store_path_file")"
|
||||
export PNPM_STORE_DIR="$store_path"
|
||||
export PNPM_STORE_PATH="$store_path"
|
||||
export NPM_CONFIG_STORE_DIR="$store_path"
|
||||
export NPM_CONFIG_STORE_PATH="$store_path"
|
||||
fi
|
||||
export HOME="$(mktemp -d)"
|
||||
export TMPDIR="${HOME}/tmp"
|
||||
mkdir -p "$TMPDIR"
|
||||
export OPENCLAW_LOG_DIR="${TMPDIR}/openclaw-logs"
|
||||
mkdir -p "$OPENCLAW_LOG_DIR"
|
||||
mkdir -p /tmp/openclaw || true
|
||||
chmod 700 /tmp/openclaw || true
|
||||
export OPENCLAW_BUNDLED_PLUGINS_DIR="${TMPDIR}/openclaw-empty-extensions"
|
||||
mkdir -p "$OPENCLAW_BUNDLED_PLUGINS_DIR"
|
||||
export VITEST_POOL="forks"
|
||||
export VITEST_MIN_WORKERS="2"
|
||||
export VITEST_MAX_WORKERS="2"
|
||||
|
||||
pnpm vitest run --config vitest.gateway.config.ts --testTimeout=20000
|
||||
@ -11,6 +11,7 @@ const argValue = (flag: string): string | null => {
|
||||
|
||||
const repo = argValue("--repo") ?? process.cwd();
|
||||
const outPath = argValue("--out") ?? path.join(process.cwd(), "nix/generated/openclaw-config-options.nix");
|
||||
const schemaRev = argValue("--rev") ?? process.env.OPENCLAW_SCHEMA_REV ?? null;
|
||||
|
||||
const schemaPath = path.join(repo, "src/config/zod-schema.ts");
|
||||
const schemaUrl = pathToFileURL(schemaPath).href;
|
||||
@ -129,16 +130,16 @@ const stripNullable = (schemaObj: JsonSchema): { schema: JsonSchema; nullable: b
|
||||
return { schema, nullable: false };
|
||||
};
|
||||
|
||||
const typeForSchema = (schemaObj: JsonSchema, indent: string): string => {
|
||||
const typeForSchema = (schemaObj: JsonSchema, indent: string, pathSegments: string[] = []): string => {
|
||||
const { schema, nullable } = stripNullable(schemaObj);
|
||||
const typeExpr = baseTypeForSchema(schema, indent);
|
||||
const typeExpr = baseTypeForSchema(schema, indent, pathSegments);
|
||||
if (nullable) {
|
||||
return `t.nullOr (${typeExpr})`;
|
||||
}
|
||||
return typeExpr;
|
||||
};
|
||||
|
||||
const baseTypeForSchema = (schemaObj: JsonSchema, indent: string): string => {
|
||||
const baseTypeForSchema = (schemaObj: JsonSchema, indent: string, pathSegments: string[]): string => {
|
||||
const schema = deref(schemaObj, new Set());
|
||||
if (schema.const !== undefined) {
|
||||
return `t.enum [ ${nixLiteral(schema.const)} ]`;
|
||||
@ -150,13 +151,17 @@ const baseTypeForSchema = (schemaObj: JsonSchema, indent: string): string => {
|
||||
|
||||
if (schema.anyOf && Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
|
||||
const entries = schema.anyOf as JsonSchema[];
|
||||
const parts = entries.map((entry) => `(${typeForSchema(entry, indent)})`).join(" ");
|
||||
const objectUnion = objectUnionTypeForSchemas(entries, indent);
|
||||
if (objectUnion) return objectUnion;
|
||||
const parts = entries.map((entry) => `(${typeForSchema(entry, indent, pathSegments)})`).join(" ");
|
||||
return `t.oneOf [ ${parts} ]`;
|
||||
}
|
||||
|
||||
if (schema.oneOf && Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
|
||||
const entries = schema.oneOf as JsonSchema[];
|
||||
const parts = entries.map((entry) => `(${typeForSchema(entry, indent)})`).join(" ");
|
||||
const objectUnion = objectUnionTypeForSchemas(entries, indent);
|
||||
if (objectUnion) return objectUnion;
|
||||
const parts = entries.map((entry) => `(${typeForSchema(entry, indent, pathSegments)})`).join(" ");
|
||||
return `t.oneOf [ ${parts} ]`;
|
||||
}
|
||||
|
||||
@ -167,7 +172,7 @@ const baseTypeForSchema = (schemaObj: JsonSchema, indent: string): string => {
|
||||
const schemaType = schema.type;
|
||||
if (Array.isArray(schemaType) && schemaType.length > 0) {
|
||||
const parts = schemaType
|
||||
.map((entry) => `(${typeForSchema({ type: entry }, indent)})`)
|
||||
.map((entry) => `(${typeForSchema({ type: entry }, indent, pathSegments)})`)
|
||||
.join(" ");
|
||||
return `t.oneOf [ ${parts} ]`;
|
||||
}
|
||||
@ -183,13 +188,13 @@ const baseTypeForSchema = (schemaObj: JsonSchema, indent: string): string => {
|
||||
return "t.bool";
|
||||
case "array": {
|
||||
const items = (schema.items as JsonSchema) || {};
|
||||
return `t.listOf (${typeForSchema(items, indent)})`;
|
||||
return `t.listOf (${typeForSchema(items, indent, pathSegments)})`;
|
||||
}
|
||||
case "object":
|
||||
return objectTypeForSchema(schema, indent);
|
||||
return objectTypeForSchema(schema, indent, pathSegments);
|
||||
case undefined:
|
||||
if (schema.properties || schema.additionalProperties) {
|
||||
return objectTypeForSchema(schema, indent);
|
||||
return objectTypeForSchema(schema, indent, pathSegments);
|
||||
}
|
||||
return "t.anything";
|
||||
default:
|
||||
@ -197,7 +202,72 @@ const baseTypeForSchema = (schemaObj: JsonSchema, indent: string): string => {
|
||||
}
|
||||
};
|
||||
|
||||
const objectTypeForSchema = (schema: JsonSchema, indent: string): string => {
|
||||
const objectUnionTypeForSchemas = (entries: JsonSchema[], indent: string): string | null => {
|
||||
const discriminator = "source";
|
||||
const variants = entries.map((entry) => deref(entry, new Set()));
|
||||
const propsByVariant = variants.map((entry) => (entry.properties as Record<string, JsonSchema>) || null);
|
||||
if (propsByVariant.some((props) => props === null)) return null;
|
||||
const requiredByVariant = variants.map((entry) => new Set((entry.required as string[]) || []));
|
||||
|
||||
const sourceValues = propsByVariant.map((props) => {
|
||||
const source = deref((props as Record<string, JsonSchema>)[discriminator] || {}, new Set());
|
||||
if (typeof source.const === "string") return source.const;
|
||||
if (Array.isArray(source.enum) && source.enum.length === 1 && typeof source.enum[0] === "string") {
|
||||
return source.enum[0] as string;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
if (sourceValues.some((value) => value === null)) return null;
|
||||
|
||||
const uniqueSourceValues = Array.from(new Set(sourceValues as string[]));
|
||||
if (uniqueSourceValues.length !== sourceValues.length) return null;
|
||||
const keySets = propsByVariant.map((props) =>
|
||||
Object.keys(props as Record<string, JsonSchema>).sort().join("\n")
|
||||
);
|
||||
if (new Set(keySets).size === 1) return null;
|
||||
|
||||
const merged: Record<string, JsonSchema[]> = {};
|
||||
for (const props of propsByVariant as Record<string, JsonSchema>[]) {
|
||||
for (const [key, value] of Object.entries(props)) {
|
||||
if (!merged[key]) merged[key] = [];
|
||||
merged[key].push(value);
|
||||
}
|
||||
}
|
||||
const dedupeSchemas = (schemas: JsonSchema[]): JsonSchema[] => {
|
||||
const byKey: Record<string, JsonSchema> = {};
|
||||
for (const schema of schemas) {
|
||||
byKey[JSON.stringify(deref(schema, new Set()))] = schema;
|
||||
}
|
||||
return Object.values(byKey);
|
||||
};
|
||||
|
||||
const nextIndent = `${indent} `;
|
||||
const keys = Object.keys(merged).sort((a, b) => {
|
||||
if (a === discriminator) return -1;
|
||||
if (b === discriminator) return 1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
const inner = keys
|
||||
.map((key) => {
|
||||
if (key === discriminator) {
|
||||
return renderOption(key, { enum: uniqueSourceValues }, true, nextIndent);
|
||||
}
|
||||
const schemas = dedupeSchemas(merged[key]);
|
||||
const schema = schemas.length === 1 ? schemas[0] : { anyOf: schemas };
|
||||
const required =
|
||||
propsByVariant.every((props) => key in (props as Record<string, JsonSchema>)) &&
|
||||
requiredByVariant.every((requiredKeys) => requiredKeys.has(key));
|
||||
return renderOption(key, schema, required, nextIndent);
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
return `t.submodule { options = {\n${inner}\n${indent}}; }`;
|
||||
};
|
||||
|
||||
const allowsPluginChannelConfigs = (pathSegments: string[]): boolean =>
|
||||
pathSegments.length === 1 && pathSegments[0] === "channels";
|
||||
|
||||
const objectTypeForSchema = (schema: JsonSchema, indent: string, pathSegments: string[]): string => {
|
||||
const properties = (schema.properties as Record<string, JsonSchema>) || {};
|
||||
const requiredList = new Set((schema.required as string[]) || []);
|
||||
const keys = Object.keys(properties);
|
||||
@ -216,18 +286,29 @@ const objectTypeForSchema = (schema: JsonSchema, indent: string): string => {
|
||||
const nextIndent = `${indent} `;
|
||||
const inner = keys
|
||||
.sort()
|
||||
.map((key) => renderOption(key, properties[key], requiredList.has(key), nextIndent))
|
||||
.map((key) =>
|
||||
renderOption(key, properties[key], requiredList.has(key), nextIndent, [...pathSegments, key])
|
||||
)
|
||||
.join("\n");
|
||||
const freeform = allowsPluginChannelConfigs(pathSegments)
|
||||
? " freeformType = t.attrsOf t.anything;"
|
||||
: "";
|
||||
|
||||
return `t.submodule { options = {\n${inner}\n${indent}}; }`;
|
||||
return `t.submodule {${freeform} options = {\n${inner}\n${indent}}; }`;
|
||||
};
|
||||
|
||||
const renderOption = (key: string, schemaObj: JsonSchema, required: boolean, indent: string): string => {
|
||||
const renderOption = (
|
||||
key: string,
|
||||
schemaObj: JsonSchema,
|
||||
required: boolean,
|
||||
indent: string,
|
||||
pathSegments: string[] = [key]
|
||||
): string => {
|
||||
const schema = deref(schemaObj, new Set());
|
||||
const description = typeof schema.description === "string" ? schema.description : null;
|
||||
const hasSchemaDefault = schema.default !== undefined;
|
||||
const effectiveRequired = required && !hasSchemaDefault;
|
||||
const baseTypeExpr = typeForSchema(schema, indent);
|
||||
const baseTypeExpr = typeForSchema(schema, indent, pathSegments);
|
||||
const typeExpr =
|
||||
!effectiveRequired && !baseTypeExpr.startsWith("t.nullOr")
|
||||
? `t.nullOr (${baseTypeExpr})`
|
||||
@ -255,7 +336,11 @@ const renderOption = (key: string, schemaObj: JsonSchema, required: boolean, ind
|
||||
.map((key) => renderOption(key, rootProps[key], requiredRoot.has(key), " "))
|
||||
.join("\n\n");
|
||||
|
||||
const output = `# Generated from upstream OpenClaw schema. DO NOT EDIT.\n{ lib }:\nlet\n t = lib.types;\nin\n{\n${body}\n}\n`;
|
||||
const header = schemaRev
|
||||
? `# Generated from upstream OpenClaw schema at rev ${schemaRev}. DO NOT EDIT.`
|
||||
: "# Generated from upstream OpenClaw schema. DO NOT EDIT.";
|
||||
|
||||
const output = `${header}\n# Generator: nix/scripts/generate-config-options.ts\n{ lib }:\nlet\n t = lib.types;\nin\n{\n${body}\n}\n`;
|
||||
|
||||
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
||||
fs.writeFileSync(outPath, output, "utf8");
|
||||
|
||||
72
nix/scripts/npm-runtime-plugin-install.sh
Executable file
72
nix/scripts/npm-runtime-plugin-install.sh
Executable file
@ -0,0 +1,72 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
spec="${OPENCLAW_RUNTIME_PLUGIN_NPM_SPEC:?OPENCLAW_RUNTIME_PLUGIN_NPM_SPEC is required}"
|
||||
id="${OPENCLAW_RUNTIME_PLUGIN_ID:?OPENCLAW_RUNTIME_PLUGIN_ID is required}"
|
||||
|
||||
package_name="$(
|
||||
node -e '
|
||||
const spec = process.env.OPENCLAW_RUNTIME_PLUGIN_NPM_SPEC || "";
|
||||
const withoutProtocol = spec.startsWith("npm:") ? spec.slice(4) : spec;
|
||||
const at = withoutProtocol.startsWith("@")
|
||||
? withoutProtocol.indexOf("@", 1)
|
||||
: withoutProtocol.indexOf("@");
|
||||
const name = at === -1 ? withoutProtocol : withoutProtocol.slice(0, at);
|
||||
if (!name || name.startsWith("git+") || name.includes("://")) {
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write(name);
|
||||
'
|
||||
)" || {
|
||||
echo "Only registry npm package specs are supported for OpenClaw runtime plugins: $spec" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
export HOME="$TMPDIR/home"
|
||||
export npm_config_cache="$TMPDIR/npm-cache"
|
||||
export npm_config_ignore_scripts=true
|
||||
export npm_config_audit=false
|
||||
export npm_config_fund=false
|
||||
export npm_config_update_notifier=false
|
||||
|
||||
project="$TMPDIR/openclaw-runtime-plugin"
|
||||
mkdir -p "$HOME" "$npm_config_cache" "$project"
|
||||
cd "$project"
|
||||
|
||||
npm init -y >/dev/null
|
||||
npm install --ignore-scripts --omit=dev --no-audit --no-fund --package-lock=false "$spec"
|
||||
|
||||
package_dir="node_modules/$package_name"
|
||||
if [ ! -d "$package_dir" ]; then
|
||||
echo "npm install did not produce $package_dir for $spec" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$package_dir/openclaw.plugin.json" ] && [ ! -f "$package_dir/package.json" ]; then
|
||||
echo "npm package $spec does not look like an OpenClaw runtime plugin" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$out"
|
||||
cp -R "$package_dir/." "$out/"
|
||||
|
||||
if [ -d node_modules ]; then
|
||||
mkdir -p "$out/node_modules"
|
||||
cp -R node_modules/. "$out/node_modules/"
|
||||
rm -rf "$out/node_modules/$package_name"
|
||||
fi
|
||||
|
||||
find "$out" -name .package-lock.json -type f -delete
|
||||
|
||||
if [ ! -f "$out/openclaw.plugin.json" ]; then
|
||||
node -e '
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(process.env.out, "package.json"), "utf8"));
|
||||
const entries = pkg.openclaw?.runtimeExtensions || pkg.openclaw?.extensions || [];
|
||||
if (!Array.isArray(entries) || entries.length === 0) process.exit(1);
|
||||
'
|
||||
fi
|
||||
|
||||
printf '%s\n' "$spec" > "$out/.nix-openclaw-npm-spec"
|
||||
printf '%s\n' "$id" > "$out/.nix-openclaw-plugin-id"
|
||||
56
nix/scripts/openclaw-batteries-install.sh
Executable file
56
nix/scripts/openclaw-batteries-install.sh
Executable file
@ -0,0 +1,56 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ -z "${OPENCLAW_GATEWAY_BIN:-}" ]; then
|
||||
echo "OPENCLAW_GATEWAY_BIN is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -x "$OPENCLAW_GATEWAY_BIN" ]; then
|
||||
echo "OPENCLAW_GATEWAY_BIN is not executable: $OPENCLAW_GATEWAY_BIN" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${OPENCLAW_PINNED_WRITE_PYTHON:-}" ]; then
|
||||
echo "OPENCLAW_PINNED_WRITE_PYTHON is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -x "$OPENCLAW_PINNED_WRITE_PYTHON" ]; then
|
||||
echo "OPENCLAW_PINNED_WRITE_PYTHON is not executable: $OPENCLAW_PINNED_WRITE_PYTHON" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${STDENV_SETUP:-}" ]; then
|
||||
echo "STDENV_SETUP is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$STDENV_SETUP" ]; then
|
||||
echo "STDENV_SETUP not found: $STDENV_SETUP" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$out/bin"
|
||||
|
||||
if [ -n "${OPENCLAW_TOOLS_PATH:-}" ]; then
|
||||
bash -e -c '. "$STDENV_SETUP"; makeWrapper "$OPENCLAW_GATEWAY_BIN" "$out/bin/openclaw" --set OPENCLAW_PINNED_WRITE_PYTHON "$OPENCLAW_PINNED_WRITE_PYTHON" --prefix PATH : "$OPENCLAW_TOOLS_PATH"'
|
||||
else
|
||||
bash -e -c '. "$STDENV_SETUP"; makeWrapper "$OPENCLAW_GATEWAY_BIN" "$out/bin/openclaw" --set OPENCLAW_PINNED_WRITE_PYTHON "$OPENCLAW_PINNED_WRITE_PYTHON"'
|
||||
fi
|
||||
|
||||
if [ -n "${OPENCLAW_APP_PACKAGE:-}" ]; then
|
||||
app_dir="${OPENCLAW_APP_PACKAGE}/Applications"
|
||||
if [ ! -d "$app_dir" ]; then
|
||||
echo "OpenClaw app package has no Applications directory: $OPENCLAW_APP_PACKAGE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$out/Applications"
|
||||
found_app=0
|
||||
for app in "$app_dir"/*.app; do
|
||||
[ -e "$app" ] || continue
|
||||
ln -s "$app" "$out/Applications/$(basename "$app")"
|
||||
found_app=1
|
||||
done
|
||||
|
||||
if [ "$found_app" -ne 1 ]; then
|
||||
echo "OpenClaw app package has no .app under: $app_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
22
nix/scripts/openclaw-qmd-prewarm.sh
Normal file
22
nix/scripts/openclaw-qmd-prewarm.sh
Normal file
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
qmd="${OPENCLAW_QMD_BIN:?OPENCLAW_QMD_BIN is required}"
|
||||
tmp_dir="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$tmp_dir"
|
||||
"$qmd" collection remove openclaw-prewarm >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
printf "%s\n\n%s\n" \
|
||||
"# OpenClaw QMD prewarm" \
|
||||
"This temporary document warms QMD model caches." \
|
||||
> "$tmp_dir/prewarm.md"
|
||||
|
||||
"$qmd" collection remove openclaw-prewarm >/dev/null 2>&1 || true
|
||||
"$qmd" collection add "$tmp_dir" --name openclaw-prewarm >/dev/null
|
||||
"$qmd" update >/dev/null
|
||||
"$qmd" embed >/dev/null
|
||||
"$qmd" query "OpenClaw QMD prewarm" -n 1 --json >/dev/null
|
||||
113
nix/scripts/source-checks-build.sh
Executable file
113
nix/scripts/source-checks-build.sh
Executable file
@ -0,0 +1,113 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
log_step() {
|
||||
if [ "${OPENCLAW_NIX_TIMINGS:-1}" != "1" ]; then
|
||||
"$@"
|
||||
return
|
||||
fi
|
||||
|
||||
name="$1"
|
||||
shift
|
||||
|
||||
start=$(date +%s)
|
||||
printf '>> [timing] %s...\n' "$name" >&2
|
||||
"$@"
|
||||
end=$(date +%s)
|
||||
printf '>> [timing] %s: %ss\n' "$name" "$((end - start))" >&2
|
||||
}
|
||||
|
||||
if [ -z "${GATEWAY_PREBUILD_SH:-}" ]; then
|
||||
echo "GATEWAY_PREBUILD_SH is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
. "$GATEWAY_PREBUILD_SH"
|
||||
|
||||
store_path_file="${PNPM_STORE_PATH_FILE:-.pnpm-store-path}"
|
||||
if [ ! -f "$store_path_file" ]; then
|
||||
echo "pnpm store path file missing: $store_path_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
store_path="$(cat "$store_path_file")"
|
||||
export PNPM_STORE_DIR="$store_path"
|
||||
export PNPM_STORE_PATH="$store_path"
|
||||
export NPM_CONFIG_STORE_DIR="$store_path"
|
||||
export NPM_CONFIG_STORE_PATH="$store_path"
|
||||
export HOME="$(mktemp -d)"
|
||||
|
||||
log_step "pnpm install (source checks)" pnpm install --offline --frozen-lockfile --ignore-scripts --prod=false --store-dir "$store_path"
|
||||
|
||||
ensure_root_package_link() {
|
||||
pkg="$1"
|
||||
root_path="node_modules/$pkg"
|
||||
|
||||
if [ -e "$root_path" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
pkg_dir="$(find node_modules/.pnpm -path "*/node_modules/$pkg" -type d | head -n 1)"
|
||||
if [ -z "$pkg_dir" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$root_path")"
|
||||
ln -s "$pkg_dir" "$root_path"
|
||||
}
|
||||
|
||||
ensure_root_bin_link() {
|
||||
bin_name="$1"
|
||||
target_rel="$2"
|
||||
bin_path="node_modules/.bin/$bin_name"
|
||||
|
||||
mkdir -p "$(dirname "$bin_path")"
|
||||
rm -f "$bin_path"
|
||||
ln -s "$target_rel" "$bin_path"
|
||||
}
|
||||
|
||||
ensure_root_package_link "tsdown"
|
||||
ensure_root_package_link "tsx"
|
||||
ensure_root_bin_link "tsdown" "../tsdown/dist/run.mjs"
|
||||
ensure_root_bin_link "tsx" "../tsx/dist/cli.mjs"
|
||||
|
||||
tsdown_cli="node_modules/tsdown/dist/run.mjs"
|
||||
if [ ! -f "$tsdown_cli" ]; then
|
||||
tsdown_cli="$(find node_modules -path '*/tsdown/dist/run.mjs' -type f | head -n 1)"
|
||||
fi
|
||||
|
||||
if [ -z "${tsdown_cli:-}" ] || [ ! -f "$tsdown_cli" ]; then
|
||||
echo "tsdown CLI not found under ./node_modules" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tsc_cli="node_modules/typescript/bin/tsc"
|
||||
if [ ! -f "$tsc_cli" ]; then
|
||||
tsc_cli="$(find node_modules -path '*/typescript/bin/tsc' -type f | head -n 1)"
|
||||
fi
|
||||
|
||||
if [ -z "${tsc_cli:-}" ] || [ ! -f "$tsc_cli" ]; then
|
||||
echo "TypeScript CLI not found under ./node_modules" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${STDENV_SETUP:-}" ]; then
|
||||
echo "STDENV_SETUP is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$STDENV_SETUP" ]; then
|
||||
echo "STDENV_SETUP not found: $STDENV_SETUP" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_step "patchShebangs node_modules/.bin" bash -e -c ". \"$STDENV_SETUP\"; patchShebangs node_modules/.bin"
|
||||
|
||||
log_step "node $tsdown_cli" node "$tsdown_cli" --config-loader unrun --logLevel warn
|
||||
log_step "node scripts/build-stamp.mjs" node scripts/build-stamp.mjs
|
||||
log_step "node $tsc_cli" node "$tsc_cli" -p tsconfig.plugin-sdk.dts.json
|
||||
log_step "node --import tsx scripts/write-plugin-sdk-entry-dts.ts" node --import tsx scripts/write-plugin-sdk-entry-dts.ts
|
||||
if [ -f "scripts/copy-plugin-sdk-root-alias.mjs" ]; then
|
||||
log_step "node scripts/copy-plugin-sdk-root-alias.mjs" node scripts/copy-plugin-sdk-root-alias.mjs
|
||||
fi
|
||||
if [ -f "scripts/copy-bundled-plugin-metadata.mjs" ]; then
|
||||
log_step "node scripts/copy-bundled-plugin-metadata.mjs" node scripts/copy-bundled-plugin-metadata.mjs
|
||||
fi
|
||||
log_step "node scripts/check-plugin-sdk-exports.mjs" node scripts/check-plugin-sdk-exports.mjs
|
||||
19
nix/scripts/source-checks-check.sh
Executable file
19
nix/scripts/source-checks-check.sh
Executable file
@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
if [ -z "${CONFIG_OPTIONS_CHECK_SH:-}" ]; then
|
||||
echo "CONFIG_OPTIONS_CHECK_SH is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$CONFIG_OPTIONS_CHECK_SH" ]; then
|
||||
echo "CONFIG_OPTIONS_CHECK_SH not found: $CONFIG_OPTIONS_CHECK_SH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "${OPENCLAW_BUILD_ROOT_SH:-}" ]; then
|
||||
. "$OPENCLAW_BUILD_ROOT_SH"
|
||||
openclaw_enter_build_root
|
||||
trap openclaw_cleanup_output_build_root EXIT
|
||||
fi
|
||||
|
||||
"$CONFIG_OPTIONS_CHECK_SH"
|
||||
17
nix/sources/openclaw-dogfood-source.nix
Normal file
17
nix/sources/openclaw-dogfood-source.nix
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
owner = "openclaw";
|
||||
repo = "openclaw";
|
||||
releaseVersion = "2026.5.7-dogfood.20260508";
|
||||
rev = "954d20ece2de0fba3688f7800613183fbeb9685c";
|
||||
hash = "sha256-6CZWsH8dV6XZ4JeG5ItKLqGAOFqbzWosyCmMXVc+c/g=";
|
||||
pnpmDepsHash = "sha256-hNZA1OEuJgtoLz2hWLPk8Hm+7heLvhiZpDdBBQ1UXpc=";
|
||||
fsSafeSource = {
|
||||
owner = "openclaw";
|
||||
repo = "fs-safe";
|
||||
rev = "c7ccb99d3058f2acf2ad2758ad2470c7e113a53c";
|
||||
hash = "sha256-jndOOSSFROyrK4RiwAsJfUuCJTj7qbmmm4Qz8BqtJ/c=";
|
||||
};
|
||||
|
||||
publicSurfaceHardlinksPatch = ../patches/allow-package-public-surface-hardlinks-open-root.patch;
|
||||
applySkipPluginAutoEnableNixModePatch = false;
|
||||
}
|
||||
@ -2,7 +2,9 @@
|
||||
{
|
||||
owner = "openclaw";
|
||||
repo = "openclaw";
|
||||
rev = "222b2d7c3c6174ee31a17fbc0668acf5f1dc5e08";
|
||||
hash = "sha256-UTTL/JbsEsV5ErOvDQPwika55bPwpRXQ6coO9PSWgAQ=";
|
||||
pnpmDepsHash = "sha256-oYQ8hPXhwPav5vQ9VL0mfEcGPA/MyXqVisL95c3nLbc=";
|
||||
releaseTag = "v2026.5.7";
|
||||
releaseVersion = "2026.5.7";
|
||||
rev = "eeef4864494f859838fec1586bedbab1f8fa5702";
|
||||
hash = "sha256-ICkq6YfMJVvRC93sM+7/q2JI82wUhjaYAI3pRzmTHYc=";
|
||||
pnpmDepsHash = "sha256-LXaRfZ0WY8VDpDc2zFr+Oel6AuYo6SiTrp37yokT1VU=";
|
||||
}
|
||||
|
||||
3
nix/tests/documents/AGENTS.md
Normal file
3
nix/tests/documents/AGENTS.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Test Agent
|
||||
|
||||
Home Manager activation fixture.
|
||||
3
nix/tests/documents/SOUL.md
Normal file
3
nix/tests/documents/SOUL.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Test Soul
|
||||
|
||||
Home Manager activation fixture.
|
||||
3
nix/tests/documents/TOOLS.md
Normal file
3
nix/tests/documents/TOOLS.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Test Tools
|
||||
|
||||
Home Manager activation fixture.
|
||||
@ -2,51 +2,64 @@
|
||||
description = "nix-openclaw macOS Home Manager activation test";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
home-manager.url = "github:nix-community/home-manager";
|
||||
home-manager.inputs.nixpkgs.follows = "nixpkgs";
|
||||
nix-openclaw.url = "github:openclaw/nix-openclaw";
|
||||
nixpkgs.follows = "nix-openclaw/nixpkgs";
|
||||
home-manager.follows = "nix-openclaw/home-manager";
|
||||
};
|
||||
|
||||
outputs = { nixpkgs, home-manager, nix-openclaw, ... }:
|
||||
outputs =
|
||||
{
|
||||
nixpkgs,
|
||||
home-manager,
|
||||
nix-openclaw,
|
||||
...
|
||||
}:
|
||||
let
|
||||
system = "aarch64-darwin";
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ nix-openclaw.overlays.default ];
|
||||
};
|
||||
in {
|
||||
in
|
||||
{
|
||||
homeConfigurations.hm-test = home-manager.lib.homeManagerConfiguration {
|
||||
inherit pkgs;
|
||||
modules = [
|
||||
nix-openclaw.homeManagerModules.openclaw
|
||||
({ ... }: {
|
||||
home = {
|
||||
username = "runner";
|
||||
homeDirectory = "/tmp/hm-activation-home";
|
||||
stateVersion = "23.11";
|
||||
};
|
||||
(
|
||||
{ ... }:
|
||||
{
|
||||
home = {
|
||||
username = "runner";
|
||||
homeDirectory = "/tmp/hm-activation-home";
|
||||
stateVersion = "23.11";
|
||||
};
|
||||
|
||||
programs.openclaw = {
|
||||
enable = true;
|
||||
installApp = false;
|
||||
instances.default = {
|
||||
gatewayPort = 18999;
|
||||
config = {
|
||||
logging = {
|
||||
level = "debug";
|
||||
file = "/tmp/openclaw/openclaw-gateway.log";
|
||||
};
|
||||
gateway = {
|
||||
mode = "local";
|
||||
auth = {
|
||||
token = "hm-activation-test-token";
|
||||
programs.openclaw = {
|
||||
enable = true;
|
||||
installApp = false;
|
||||
runtimePackages = [ pkgs.jq ];
|
||||
environment.OPENCLAW_TEST_SECRET = "/tmp/openclaw-secret";
|
||||
instances.default = {
|
||||
gatewayPort = 18999;
|
||||
logPath = "/tmp/hm-activation-home/.openclaw/openclaw-gateway.log";
|
||||
launchd.label = "com.steipete.openclaw.gateway.hm-test";
|
||||
config = {
|
||||
logging = {
|
||||
level = "debug";
|
||||
file = "/tmp/hm-activation-home/.openclaw/openclaw-gateway.log";
|
||||
};
|
||||
gateway = {
|
||||
mode = "local";
|
||||
auth = {
|
||||
token = "hm-activation-test-token";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
})
|
||||
}
|
||||
)
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
@ -5,6 +5,13 @@ machine.wait_until_succeeds(
|
||||
)
|
||||
|
||||
machine.wait_until_succeeds("test -f /home/alice/.openclaw/openclaw.json")
|
||||
machine.wait_until_succeeds("test -f /home/alice/.openclaw/workspace/AGENTS.md")
|
||||
machine.succeed("test ! -L /home/alice/.openclaw/workspace/AGENTS.md")
|
||||
machine.wait_until_succeeds("test -f /home/alice/.openclaw/workspace/skills/skill/SKILL.md")
|
||||
machine.succeed("test ! -L /home/alice/.openclaw/workspace/skills/skill")
|
||||
machine.wait_until_succeeds(
|
||||
"test -x /home/alice/.openclaw/agents/main/agent/codex-home/home/.nix-profile/bin/jq"
|
||||
)
|
||||
|
||||
uid = machine.succeed("id -u alice").strip()
|
||||
machine.succeed("loginctl enable-linger alice")
|
||||
|
||||
12
nix/tests/plugins/alpha/flake.nix
Normal file
12
nix/tests/plugins/alpha/flake.nix
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
outputs =
|
||||
{ self }:
|
||||
{
|
||||
openclawPlugin = {
|
||||
name = "alpha";
|
||||
skills = [ ./skill ];
|
||||
packages = [ ];
|
||||
needs = { };
|
||||
};
|
||||
};
|
||||
}
|
||||
6
nix/tests/plugins/alpha/skill/SKILL.md
Normal file
6
nix/tests/plugins/alpha/skill/SKILL.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
name: skill
|
||||
description: Test OpenClaw plugin skill.
|
||||
---
|
||||
|
||||
Test skill fixture.
|
||||
12
nix/tests/plugins/beta/flake.nix
Normal file
12
nix/tests/plugins/beta/flake.nix
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
outputs =
|
||||
{ self }:
|
||||
{
|
||||
openclawPlugin = {
|
||||
name = "beta";
|
||||
skills = [ ./skill ];
|
||||
packages = [ ];
|
||||
needs = { };
|
||||
};
|
||||
};
|
||||
}
|
||||
6
nix/tests/plugins/beta/skill/SKILL.md
Normal file
6
nix/tests/plugins/beta/skill/SKILL.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
name: skill
|
||||
description: Test OpenClaw plugin skill.
|
||||
---
|
||||
|
||||
Test skill fixture.
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"id": "runtime-disabled",
|
||||
"name": "Runtime Disabled"
|
||||
}
|
||||
23
nix/tests/plugins/runtime/flake.nix
Normal file
23
nix/tests/plugins/runtime/flake.nix
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
outputs =
|
||||
{ self }:
|
||||
{
|
||||
openclawPlugin = {
|
||||
name = "runtime";
|
||||
skills = [ ];
|
||||
packages = [ ];
|
||||
needs = { };
|
||||
plugins = [
|
||||
{
|
||||
id = "runtime-test";
|
||||
path = "${self.outPath}/plugin";
|
||||
}
|
||||
{
|
||||
id = "runtime-disabled";
|
||||
path = "${self.outPath}/disabled-plugin";
|
||||
enabled = false;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
3
nix/tests/plugins/runtime/plugin/index.js
Normal file
3
nix/tests/plugins/runtime/plugin/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
export default {
|
||||
activate() {}
|
||||
};
|
||||
7
nix/tests/plugins/runtime/plugin/openclaw.plugin.json
Normal file
7
nix/tests/plugins/runtime/plugin/openclaw.plugin.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "runtime-test",
|
||||
"name": "Runtime Test",
|
||||
"activation": {
|
||||
"onStartup": true
|
||||
}
|
||||
}
|
||||
@ -1,20 +1,27 @@
|
||||
{ pkgs
|
||||
, steipetePkgs ? {}
|
||||
, toolNamesOverride ? null
|
||||
, excludeToolNames ? []
|
||||
{
|
||||
pkgs,
|
||||
openclawToolPkgs ? { },
|
||||
toolNamesOverride ? null,
|
||||
excludeToolNames ? [ ],
|
||||
}:
|
||||
let
|
||||
lib = pkgs.lib;
|
||||
safe = list: builtins.filter (p: p != null) list;
|
||||
pickFrom = scope: name:
|
||||
pickFrom =
|
||||
scope: name:
|
||||
if builtins.hasAttr name scope then
|
||||
let pkg = scope.${name}; in
|
||||
let
|
||||
pkg = scope.${name};
|
||||
in
|
||||
if lib.meta.availableOn pkgs.stdenv.hostPlatform pkg then pkg else null
|
||||
else
|
||||
null;
|
||||
pick = name:
|
||||
let fromSteipete = pickFrom steipetePkgs name; in
|
||||
if fromSteipete != null then fromSteipete else pickFrom pkgs name;
|
||||
pick =
|
||||
name:
|
||||
let
|
||||
fromOpenClawTools = pickFrom openclawToolPkgs name;
|
||||
in
|
||||
if fromOpenClawTools != null then fromOpenClawTools else pickFrom pkgs name;
|
||||
ensure = names: safe (map pick names);
|
||||
|
||||
baseNames = [
|
||||
@ -30,32 +37,17 @@ let
|
||||
];
|
||||
|
||||
extraNames = [
|
||||
"go"
|
||||
"uv"
|
||||
"openai-whisper"
|
||||
"spotify-player"
|
||||
"gogcli"
|
||||
"peekaboo"
|
||||
"camsnap"
|
||||
"bird"
|
||||
"sag"
|
||||
"summarize"
|
||||
"openhue-cli"
|
||||
"wacli"
|
||||
"sonoscli"
|
||||
"ordercli"
|
||||
"blucli"
|
||||
"eightctl"
|
||||
"mcporter"
|
||||
"oracle"
|
||||
"qmd"
|
||||
"nano-pdf"
|
||||
"goplaces"
|
||||
"summarize"
|
||||
"camsnap"
|
||||
"sonoscli"
|
||||
];
|
||||
toolNamesBase = if toolNamesOverride != null then toolNamesOverride else baseNames ++ extraNames;
|
||||
toolNames = builtins.filter (name: !builtins.elem name excludeToolNames) toolNamesBase;
|
||||
|
||||
in {
|
||||
in
|
||||
{
|
||||
tools = ensure toolNames;
|
||||
toolNames = toolNames;
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
# Allowed GitHub inputs in flake.lock (owner/repo)
|
||||
NixOS/nixpkgs
|
||||
openclaw/nix-steipete-tools
|
||||
openclaw/nix-openclaw-tools
|
||||
nix-community/home-manager
|
||||
nix-systems/default
|
||||
numtide/flake-utils
|
||||
tobi/qmd
|
||||
|
||||
@ -4,12 +4,25 @@ set -euo pipefail
|
||||
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
test_dir="$repo_root/nix/tests/hm-activation-macos"
|
||||
home_dir="/tmp/hm-activation-home"
|
||||
label="com.steipete.openclaw.gateway.hm-test"
|
||||
plist="$home_dir/Library/LaunchAgents/$label.plist"
|
||||
|
||||
cleanup() {
|
||||
if command -v launchctl >/dev/null 2>&1; then
|
||||
launchctl bootout "gui/$UID/$label" >/dev/null 2>&1 || true
|
||||
if [ -e "$plist" ]; then
|
||||
launchctl bootout "gui/$UID" "$plist" >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
rm -rf "$home_dir"
|
||||
mkdir -p "$home_dir"
|
||||
cleanup
|
||||
|
||||
export HOME="$home_dir"
|
||||
export USER="${USER:-runner}"
|
||||
export USER="runner"
|
||||
export LOGNAME="$USER"
|
||||
|
||||
cd "$test_dir"
|
||||
@ -21,7 +34,43 @@ nix build --accept-flake-config --impure \
|
||||
./result/activate
|
||||
|
||||
test -f "$HOME/.openclaw/openclaw.json"
|
||||
test -f "$plist"
|
||||
test -L "$HOME/.openclaw/agents/main/agent/codex-home/home/.nix-profile/bin"
|
||||
test -x "$HOME/.openclaw/agents/main/agent/codex-home/home/.nix-profile/bin/jq"
|
||||
|
||||
if command -v launchctl >/dev/null 2>&1; then
|
||||
launchctl print "gui/$UID/com.steipete.openclaw.gateway" >/dev/null 2>&1
|
||||
state_file="$home_dir/launchd-state.txt"
|
||||
running=false
|
||||
for _ in {1..20}; do
|
||||
if launchctl print "gui/$UID/$label" >"$state_file" 2>&1 && grep -q "state = running" "$state_file"; then
|
||||
running=true
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
if [ "$running" != true ]; then
|
||||
cat "$state_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
openclaw_bin=$(/usr/libexec/PlistBuddy -c "Print :ProgramArguments:0" "$plist")
|
||||
grep -q OPENCLAW_TEST_SECRET "$openclaw_bin"
|
||||
health_file="$home_dir/gateway-health.json"
|
||||
healthy=false
|
||||
for _ in {1..30}; do
|
||||
if "$openclaw_bin" gateway health \
|
||||
--url "ws://127.0.0.1:18999" \
|
||||
--token "hm-activation-test-token" \
|
||||
--json \
|
||||
--timeout 3000 >"$health_file" 2>&1 \
|
||||
&& grep -q '"ok"[[:space:]]*:[[:space:]]*true' "$health_file"; then
|
||||
healthy=true
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
if [ "$healthy" != true ]; then
|
||||
cat "$health_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
100
scripts/select-openclaw-release.mjs
Normal file
100
scripts/select-openclaw-release.mjs
Normal file
@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env node
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
export function selectOpenClawRelease(releases) {
|
||||
if (!Array.isArray(releases)) {
|
||||
throw new Error("Expected a GitHub releases JSON array");
|
||||
}
|
||||
|
||||
const stableReleases = releases.filter((release) => {
|
||||
return release && release.draft !== true && release.prerelease !== true;
|
||||
});
|
||||
const latestStable = stableReleases[0] ?? null;
|
||||
const latestStableSource = latestStable
|
||||
? stableSourceSelection(latestStable)
|
||||
: null;
|
||||
const appLagStableReleases = [];
|
||||
|
||||
for (const release of stableReleases) {
|
||||
const tagName = release.tag_name ?? release.tagName;
|
||||
if (!tagName) {
|
||||
appLagStableReleases.push({
|
||||
tagName: null,
|
||||
reason: "missing-tag",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const appAsset = (release.assets ?? []).find((asset) => {
|
||||
const name = asset?.name;
|
||||
return (
|
||||
typeof name === "string" &&
|
||||
/^OpenClaw-.*\.zip$/.test(name) &&
|
||||
!/dSYM/i.test(name) &&
|
||||
Boolean(asset?.browser_download_url)
|
||||
);
|
||||
});
|
||||
|
||||
if (!appAsset) {
|
||||
appLagStableReleases.push({
|
||||
tagName,
|
||||
reason: "missing-macos-zip",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
return {
|
||||
latestStable: latestStableSource
|
||||
? { tagName: latestStableSource.tagName }
|
||||
: null,
|
||||
latestStableSource,
|
||||
latestMacAppStable: {
|
||||
tagName,
|
||||
releaseVersion: tagName.replace(/^v/, ""),
|
||||
appAssetName: appAsset.name,
|
||||
appUrl: appAsset.browser_download_url,
|
||||
},
|
||||
appLagStableReleases,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
latestStable: latestStableSource
|
||||
? { tagName: latestStableSource.tagName }
|
||||
: null,
|
||||
latestStableSource,
|
||||
latestMacAppStable: null,
|
||||
appLagStableReleases,
|
||||
};
|
||||
}
|
||||
|
||||
function stableSourceSelection(release) {
|
||||
const tagName = release.tag_name ?? release.tagName;
|
||||
if (!tagName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
tagName,
|
||||
releaseVersion: tagName.replace(/^v/, ""),
|
||||
};
|
||||
}
|
||||
|
||||
function readStdin() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let data = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
process.stdin.on("end", () => resolve(data));
|
||||
process.stdin.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
const input = await readStdin();
|
||||
const releases = JSON.parse(input);
|
||||
const selection = selectOpenClawRelease(releases);
|
||||
process.stdout.write(`${JSON.stringify(selection, null, 2)}\n`);
|
||||
}
|
||||
87
scripts/select-openclaw-release.test.mjs
Normal file
87
scripts/select-openclaw-release.test.mjs
Normal file
@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import { selectOpenClawRelease } from "./select-openclaw-release.mjs";
|
||||
|
||||
const releases = [
|
||||
{
|
||||
tag_name: "v2026.5.3-1",
|
||||
draft: false,
|
||||
prerelease: false,
|
||||
assets: [],
|
||||
},
|
||||
{
|
||||
tag_name: "v2026.5.3",
|
||||
draft: false,
|
||||
prerelease: false,
|
||||
assets: [],
|
||||
},
|
||||
{
|
||||
tag_name: "v2026.5.2-beta.1",
|
||||
draft: false,
|
||||
prerelease: true,
|
||||
assets: [
|
||||
{
|
||||
name: "OpenClaw-2026.5.2-beta.1.zip",
|
||||
browser_download_url:
|
||||
"https://github.com/openclaw/openclaw/releases/download/v2026.5.2-beta.1/OpenClaw-2026.5.2-beta.1.zip",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tag_name: "v2026.5.2",
|
||||
draft: false,
|
||||
prerelease: false,
|
||||
assets: [
|
||||
{
|
||||
name: "OpenClaw-2026.5.2.dmg",
|
||||
browser_download_url:
|
||||
"https://github.com/openclaw/openclaw/releases/download/v2026.5.2/OpenClaw-2026.5.2.dmg",
|
||||
},
|
||||
{
|
||||
name: "OpenClaw-2026.5.2.dSYM.zip",
|
||||
browser_download_url:
|
||||
"https://github.com/openclaw/openclaw/releases/download/v2026.5.2/OpenClaw-2026.5.2.dSYM.zip",
|
||||
},
|
||||
{
|
||||
name: "OpenClaw-2026.5.2.zip",
|
||||
browser_download_url:
|
||||
"https://github.com/openclaw/openclaw/releases/download/v2026.5.2/OpenClaw-2026.5.2.zip",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const selection = selectOpenClawRelease(releases);
|
||||
|
||||
assert.equal(selection.latestStable.tagName, "v2026.5.3-1");
|
||||
assert.equal(selection.latestStableSource.tagName, "v2026.5.3-1");
|
||||
assert.equal(selection.latestStableSource.releaseVersion, "2026.5.3-1");
|
||||
assert.equal(selection.latestMacAppStable.tagName, "v2026.5.2");
|
||||
assert.equal(selection.latestMacAppStable.releaseVersion, "2026.5.2");
|
||||
assert.equal(
|
||||
selection.latestMacAppStable.appUrl,
|
||||
"https://github.com/openclaw/openclaw/releases/download/v2026.5.2/OpenClaw-2026.5.2.zip",
|
||||
);
|
||||
assert.deepEqual(
|
||||
selection.appLagStableReleases.map((release) => release.tagName),
|
||||
["v2026.5.3-1", "v2026.5.3"],
|
||||
);
|
||||
|
||||
const none = selectOpenClawRelease([
|
||||
{
|
||||
tag_name: "v2026.5.3",
|
||||
draft: false,
|
||||
prerelease: false,
|
||||
assets: [],
|
||||
},
|
||||
]);
|
||||
|
||||
assert.equal(none.latestStable.tagName, "v2026.5.3");
|
||||
assert.equal(none.latestStableSource.tagName, "v2026.5.3");
|
||||
assert.equal(none.latestStableSource.releaseVersion, "2026.5.3");
|
||||
assert.equal(none.latestMacAppStable, null);
|
||||
assert.deepEqual(none.appLagStableReleases, [
|
||||
{ tagName: "v2026.5.3", reason: "missing-macos-zip" },
|
||||
]);
|
||||
|
||||
console.log("release selection: ok");
|
||||
@ -9,260 +9,305 @@ fi
|
||||
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
source_file="$repo_root/nix/sources/openclaw-source.nix"
|
||||
app_file="$repo_root/nix/packages/openclaw-app.nix"
|
||||
config_options_file="$repo_root/nix/generated/openclaw-config-options.nix"
|
||||
|
||||
log() {
|
||||
printf '>> %s\n' "$*"
|
||||
printf '>> %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
upstream_checks_green() {
|
||||
local sha="$1"
|
||||
local checks_json
|
||||
checks_json=$(gh api "/repos/openclaw/openclaw/commits/${sha}/check-runs?per_page=100" 2>/dev/null || true)
|
||||
if [[ -z "$checks_json" ]]; then
|
||||
log "No check runs found for $sha"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local relevant_count
|
||||
relevant_count=$(printf '%s' "$checks_json" | jq '[.check_runs[] | select(.name | test("windows"; "i") | not)] | length')
|
||||
if [[ "$relevant_count" -eq 0 ]]; then
|
||||
log "No non-windows check runs found for $sha"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local failing_count
|
||||
failing_count=$(
|
||||
printf '%s' "$checks_json" | jq '[.check_runs[]
|
||||
| select(.name | test("windows"; "i") | not)
|
||||
| select(.status != "completed" or (.conclusion != "success" and .conclusion != "skipped"))
|
||||
] | length'
|
||||
)
|
||||
if [[ "$failing_count" -ne 0 ]]; then
|
||||
log "Non-windows checks not green for $sha"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
Usage:
|
||||
scripts/update-pins.sh select
|
||||
scripts/update-pins.sh apply <source_tag> <source_sha> <app_tag> <app_url>
|
||||
EOF
|
||||
}
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "jq is required but not installed." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Updating nix-steipete-tools input"
|
||||
nix flake update --update-input nix-steipete-tools --accept-flake-config
|
||||
|
||||
log "Resolving openclaw main SHAs"
|
||||
mapfile -t candidate_shas < <(gh api /repos/openclaw/openclaw/commits?per_page=10 | jq -r '.[].sha' || true)
|
||||
if [[ ${#candidate_shas[@]} -eq 0 ]]; then
|
||||
latest_sha=$(git ls-remote https://github.com/openclaw/openclaw.git refs/heads/main | awk '{print $1}' || true)
|
||||
if [[ -z "$latest_sha" ]]; then
|
||||
echo "Failed to resolve openclaw main SHA" >&2
|
||||
require_cmd() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "$1 is required but not installed." >&2
|
||||
exit 1
|
||||
fi
|
||||
candidate_shas=("$latest_sha")
|
||||
fi
|
||||
}
|
||||
|
||||
selected_sha=""
|
||||
selected_hash=""
|
||||
selected_source_store_path=""
|
||||
selected_source_url=""
|
||||
current_field() {
|
||||
local file="$1"
|
||||
local key="$2"
|
||||
awk -F'"' -v key="$key" '$0 ~ key" =" { print $2; exit }' "$file"
|
||||
}
|
||||
|
||||
for sha in "${candidate_shas[@]}"; do
|
||||
if ! upstream_checks_green "$sha"; then
|
||||
continue
|
||||
resolve_release_tag_sha() {
|
||||
local tag="$1"
|
||||
local tag_refs
|
||||
tag_refs=$(git ls-remote https://github.com/openclaw/openclaw.git "refs/tags/${tag}" "refs/tags/${tag}^{}" || true)
|
||||
if [[ -z "$tag_refs" ]]; then
|
||||
echo ""
|
||||
return 0
|
||||
fi
|
||||
log "Testing upstream SHA: $sha"
|
||||
source_url="https://github.com/openclaw/openclaw/archive/${sha}.tar.gz"
|
||||
log "Prefetching source tarball"
|
||||
source_prefetch=$(
|
||||
nix --extra-experimental-features "nix-command flakes" store prefetch-file --unpack --json "$source_url" 2>"/tmp/nix-prefetch-source.err" \
|
||||
|| true
|
||||
)
|
||||
if [[ -z "$source_prefetch" ]]; then
|
||||
cat "/tmp/nix-prefetch-source.err" >&2 || true
|
||||
rm -f "/tmp/nix-prefetch-source.err"
|
||||
echo "Failed to resolve source hash for $sha" >&2
|
||||
continue
|
||||
fi
|
||||
rm -f "/tmp/nix-prefetch-source.err"
|
||||
source_hash=$(printf '%s' "$source_prefetch" | jq -r '.hash // empty')
|
||||
if [[ -z "$source_hash" ]]; then
|
||||
printf '%s\n' "$source_prefetch" >&2
|
||||
echo "Failed to parse source hash for $sha" >&2
|
||||
continue
|
||||
fi
|
||||
source_store_path=$(printf '%s' "$source_prefetch" | jq -r '.path // .storePath // empty')
|
||||
if [[ -z "$source_store_path" ]]; then
|
||||
echo "Failed to parse source store path for $sha" >&2
|
||||
continue
|
||||
fi
|
||||
log "Source hash: $source_hash"
|
||||
|
||||
perl -0pi -e "s|rev = \"[^\"]+\";|rev = \"${sha}\";|" "$source_file"
|
||||
perl -0pi -e "s|hash = \"[^\"]+\";|hash = \"${source_hash}\";|" "$source_file"
|
||||
# Force a fresh pnpmDepsHash recalculation for the candidate rev.
|
||||
perl -0pi -e "s|pnpmDepsHash = \"[^\"]*\";|pnpmDepsHash = \"\";|" "$source_file"
|
||||
local deref_sha plain_sha
|
||||
deref_sha=$(printf '%s\n' "$tag_refs" | awk '/\^\{\}$/ { print $1; exit }')
|
||||
if [[ -n "$deref_sha" ]]; then
|
||||
printf '%s\n' "$deref_sha"
|
||||
return 0
|
||||
fi
|
||||
|
||||
plain_sha=$(printf '%s\n' "$tag_refs" | awk '!/\^\{\}$/ { print $1; exit }')
|
||||
printf '%s\n' "$plain_sha"
|
||||
}
|
||||
|
||||
prefetch_json() {
|
||||
local url="$1"
|
||||
nix --extra-experimental-features "nix-command flakes" store prefetch-file --unpack --json "$url"
|
||||
}
|
||||
|
||||
prefetch_file_json() {
|
||||
local url="$1"
|
||||
nix --extra-experimental-features "nix-command flakes" store prefetch-file --json "$url"
|
||||
}
|
||||
|
||||
unpacked_zip_hash() {
|
||||
local url="$1"
|
||||
local archive_prefetch archive_path unpack_dir app_list app_count app_path app_hash
|
||||
|
||||
archive_prefetch=$(prefetch_file_json "$url")
|
||||
archive_path=$(printf '%s' "$archive_prefetch" | jq -r '.path // .storePath // empty')
|
||||
if [[ -z "$archive_path" || ! -f "$archive_path" ]]; then
|
||||
echo "Failed to prefetch app archive for $url" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
unpack_dir=$(mktemp -d)
|
||||
if ! unzip -q "$archive_path" -d "$unpack_dir"; then
|
||||
rm -rf "$unpack_dir"
|
||||
echo "Failed to unzip app archive: $archive_path" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
app_list=$(find "$unpack_dir" -maxdepth 3 -type d -name '*.app' -print)
|
||||
app_count=$(printf '%s\n' "$app_list" | sed '/^$/d' | wc -l | tr -d ' ')
|
||||
if [[ "$app_count" != "1" ]]; then
|
||||
rm -rf "$unpack_dir"
|
||||
echo "Expected exactly one .app in app archive; found $app_count" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
app_path=$(printf '%s\n' "$app_list" | sed -n '1p')
|
||||
if [[ ! -d "$app_path/Contents" ]]; then
|
||||
rm -rf "$unpack_dir"
|
||||
echo "App archive contains an invalid app bundle: $app_path" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! app_hash=$(nix --extra-experimental-features "nix-command flakes" hash path "$unpack_dir"); then
|
||||
rm -rf "$unpack_dir"
|
||||
echo "Failed to hash unpacked app archive: $archive_path" >&2
|
||||
return 1
|
||||
fi
|
||||
rm -rf "$unpack_dir"
|
||||
printf '%s\n' "$app_hash"
|
||||
}
|
||||
|
||||
refresh_pnpm_hash() {
|
||||
local build_log pnpm_hash
|
||||
build_log=$(mktemp)
|
||||
log "Building gateway to validate pnpmDepsHash"
|
||||
if ! nix build .#openclaw-gateway --accept-flake-config >"$build_log" 2>&1; then
|
||||
pnpm_hash=$(grep -Eo 'got: *sha256-[A-Za-z0-9+/=]+' "$build_log" | head -n 1 | sed 's/.*got: *//' || true)
|
||||
if [[ -n "$pnpm_hash" ]]; then
|
||||
log "pnpmDepsHash mismatch detected: $pnpm_hash"
|
||||
perl -0pi -e "s|pnpmDepsHash = \"[^\"]*\";|pnpmDepsHash = \"${pnpm_hash}\";|" "$source_file"
|
||||
if ! nix build .#openclaw-gateway --accept-flake-config >"$build_log" 2>&1; then
|
||||
tail -n 200 "$build_log" >&2 || true
|
||||
rm -f "$build_log"
|
||||
continue
|
||||
fi
|
||||
else
|
||||
if [[ -z "$pnpm_hash" ]]; then
|
||||
tail -n 200 "$build_log" >&2 || true
|
||||
rm -f "$build_log"
|
||||
continue
|
||||
return 1
|
||||
fi
|
||||
log "pnpmDepsHash mismatch detected: $pnpm_hash"
|
||||
perl -0pi -e "s|pnpmDepsHash = \"[^\"]*\";|pnpmDepsHash = \"${pnpm_hash}\";|" "$source_file"
|
||||
nix build .#openclaw-gateway --accept-flake-config >"$build_log" 2>&1 || {
|
||||
tail -n 200 "$build_log" >&2 || true
|
||||
rm -f "$build_log"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
rm -f "$build_log"
|
||||
selected_sha="$sha"
|
||||
selected_hash="$source_hash"
|
||||
selected_source_store_path="$source_store_path"
|
||||
selected_source_url="$source_url"
|
||||
break
|
||||
done
|
||||
}
|
||||
|
||||
if [[ -z "$selected_sha" ]]; then
|
||||
echo "Failed to find a buildable upstream revision in the last ${#candidate_shas[@]} commits." >&2
|
||||
exit 1
|
||||
fi
|
||||
log "Selected upstream SHA: $selected_sha"
|
||||
regenerate_config_options() {
|
||||
local selected_sha="$1"
|
||||
local source_store_path="$2"
|
||||
local tmp_src
|
||||
tmp_src=$(mktemp -d)
|
||||
|
||||
log "Fetching latest release metadata"
|
||||
release_json=$(gh api /repos/openclaw/openclaw/releases?per_page=20 || true)
|
||||
if [[ -z "$release_json" ]]; then
|
||||
echo "Failed to fetch release metadata" >&2
|
||||
exit 1
|
||||
fi
|
||||
release_tag=$(printf '%s' "$release_json" | jq -r '[.[] | select([.assets[]?.name | (test("^OpenClaw-.*\\.zip$") and (test("dSYM") | not))] | any)][0].tag_name // empty')
|
||||
if [[ -z "$release_tag" ]]; then
|
||||
echo "Failed to resolve a release tag with an OpenClaw app asset" >&2
|
||||
exit 1
|
||||
fi
|
||||
log "Latest app release tag with asset: $release_tag"
|
||||
if [[ -d "$source_store_path" ]]; then
|
||||
cp -R "$source_store_path" "$tmp_src/src"
|
||||
elif [[ -f "$source_store_path" ]]; then
|
||||
mkdir -p "$tmp_src/src"
|
||||
tar -xf "$source_store_path" -C "$tmp_src/src" --strip-components=1
|
||||
else
|
||||
echo "Source path not found: $source_store_path" >&2
|
||||
rm -rf "$tmp_src"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
app_url=$(printf '%s' "$release_json" | jq -r '[.[] | select([.assets[]?.name | (test("^OpenClaw-.*\\.zip$") and (test("dSYM") | not))] | any)][0].assets[] | select(.name | (test("^OpenClaw-.*\\.zip$") and (test("dSYM") | not))) | .browser_download_url' | head -n 1 || true)
|
||||
if [[ -z "$app_url" ]]; then
|
||||
echo "Failed to resolve OpenClaw app asset URL from latest release" >&2
|
||||
exit 1
|
||||
fi
|
||||
log "App asset URL: $app_url"
|
||||
chmod -R u+w "$tmp_src/src"
|
||||
|
||||
app_prefetch=$(
|
||||
nix --extra-experimental-features "nix-command flakes" store prefetch-file --unpack --json "$app_url" 2>"/tmp/nix-prefetch-app.err" \
|
||||
|| true
|
||||
)
|
||||
if [[ -z "$app_prefetch" ]]; then
|
||||
cat "/tmp/nix-prefetch-app.err" >&2 || true
|
||||
rm -f "/tmp/nix-prefetch-app.err"
|
||||
echo "Failed to resolve app hash" >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -f "/tmp/nix-prefetch-app.err"
|
||||
app_hash=$(printf '%s' "$app_prefetch" | jq -r '.hash // empty')
|
||||
if [[ -z "$app_hash" ]]; then
|
||||
printf '%s\n' "$app_prefetch" >&2
|
||||
echo "Failed to parse app hash" >&2
|
||||
exit 1
|
||||
fi
|
||||
log "App hash: $app_hash"
|
||||
nix shell --extra-experimental-features "nix-command flakes" nixpkgs#nodejs_22 nixpkgs#pnpm_10 -c \
|
||||
bash -c "cd '$tmp_src/src' && pnpm install --frozen-lockfile --ignore-scripts"
|
||||
|
||||
app_version="${release_tag#v}"
|
||||
perl -0pi -e "s|version = \"[^\"]+\";|version = \"${app_version}\";|" "$app_file"
|
||||
perl -0pi -e "s|url = \"[^\"]+\";|url = \"${app_url}\";|" "$app_file"
|
||||
perl -0pi -e "s|hash = \"[^\"]+\";|hash = \"${app_hash}\";|" "$app_file"
|
||||
nix shell --extra-experimental-features "nix-command flakes" nixpkgs#nodejs_22 nixpkgs#pnpm_10 -c \
|
||||
bash -c "cd '$tmp_src/src' && OPENCLAW_SCHEMA_REV='${selected_sha}' pnpm exec tsx '$repo_root/nix/scripts/generate-config-options.ts' --repo . --out '$config_options_file'"
|
||||
|
||||
if [[ -z "$selected_source_store_path" ]]; then
|
||||
echo "Missing source path for selected upstream revision" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Regenerating openclaw config options from upstream schema"
|
||||
tmp_src=$(mktemp -d)
|
||||
cleanup_tmp() {
|
||||
rm -rf "$tmp_src"
|
||||
}
|
||||
trap cleanup_tmp EXIT
|
||||
if [[ -d "$selected_source_store_path" ]]; then
|
||||
cp -R "$selected_source_store_path" "$tmp_src/src"
|
||||
elif [[ -f "$selected_source_store_path" ]]; then
|
||||
mkdir -p "$tmp_src/src"
|
||||
tar -xf "$selected_source_store_path" -C "$tmp_src/src" --strip-components=1
|
||||
else
|
||||
echo "Source path not found: $selected_source_store_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
chmod -R u+w "$tmp_src/src"
|
||||
|
||||
nix shell --extra-experimental-features "nix-command flakes" nixpkgs#nodejs_22 nixpkgs#pnpm_10 -c \
|
||||
bash -c "cd '$tmp_src/src' && pnpm install --frozen-lockfile --ignore-scripts"
|
||||
select_release() {
|
||||
local release_json selection_json current_rev current_app_version source_tag source_version selected_sha
|
||||
local app_tag app_version app_url latest_stable_tag app_lag_releases has_update
|
||||
current_rev=$(current_field "$source_file" "rev")
|
||||
current_app_version=$(current_field "$app_file" "version")
|
||||
|
||||
nix shell --extra-experimental-features "nix-command flakes" nixpkgs#nodejs_22 nixpkgs#pnpm_10 -c \
|
||||
bash -c "cd '$tmp_src/src' && pnpm exec tsx '$repo_root/nix/scripts/generate-config-options.ts' --repo . --out '$repo_root/nix/generated/openclaw-config-options.nix'"
|
||||
log "Fetching OpenClaw stable release metadata"
|
||||
release_json=$(gh api '/repos/openclaw/openclaw/releases?per_page=100')
|
||||
selection_json=$(printf '%s' "$release_json" | node "$repo_root/scripts/select-openclaw-release.mjs")
|
||||
|
||||
cleanup_tmp
|
||||
trap - EXIT
|
||||
latest_stable_tag=$(printf '%s' "$selection_json" | jq -r '.latestStableSource.tagName // empty')
|
||||
source_tag=$(printf '%s' "$selection_json" | jq -r '.latestStableSource.tagName // empty')
|
||||
source_version=$(printf '%s' "$selection_json" | jq -r '.latestStableSource.releaseVersion // empty')
|
||||
app_tag=$(printf '%s' "$selection_json" | jq -r '.latestMacAppStable.tagName // empty')
|
||||
app_version=$(printf '%s' "$selection_json" | jq -r '.latestMacAppStable.releaseVersion // empty')
|
||||
app_url=$(printf '%s' "$selection_json" | jq -r '.latestMacAppStable.appUrl // empty')
|
||||
app_lag_releases=$(printf '%s' "$selection_json" | jq -r '[.appLagStableReleases[]?.tagName | select(. != null)] | join(",")')
|
||||
|
||||
log "Building app to validate fetchzip hash"
|
||||
current_system=$(nix eval --impure --raw --expr 'builtins.currentSystem' 2>/dev/null || true)
|
||||
if [[ "$current_system" == *darwin* ]]; then
|
||||
app_build_log=$(mktemp)
|
||||
if ! nix build .#openclaw-app --accept-flake-config >"$app_build_log" 2>&1; then
|
||||
app_hash_mismatch=$(grep -Eo 'got: *sha256-[A-Za-z0-9+/=]+' "$app_build_log" | head -n 1 | sed 's/.*got: *//' || true)
|
||||
if [[ -n "$app_hash_mismatch" ]]; then
|
||||
log "App hash mismatch detected: $app_hash_mismatch"
|
||||
perl -0pi -e "s|hash = \"[^\"]+\";|hash = \"${app_hash_mismatch}\";|" "$app_file"
|
||||
if ! nix build .#openclaw-app --accept-flake-config >"$app_build_log" 2>&1; then
|
||||
tail -n 200 "$app_build_log" >&2 || true
|
||||
rm -f "$app_build_log"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
tail -n 200 "$app_build_log" >&2 || true
|
||||
rm -f "$app_build_log"
|
||||
if [[ -z "$source_tag" || -z "$source_version" ]]; then
|
||||
echo "Failed to resolve an OpenClaw stable source release" >&2
|
||||
if [[ -n "$latest_stable_tag" ]]; then
|
||||
echo "Latest stable release: $latest_stable_tag" >&2
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
selected_sha=$(resolve_release_tag_sha "$source_tag")
|
||||
if [[ -z "$selected_sha" ]]; then
|
||||
echo "Failed to resolve tag SHA for $source_tag" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Selected latest stable source release: $source_tag ($selected_sha)"
|
||||
if [[ -n "$app_tag" ]]; then
|
||||
log "Selected latest public macOS app release: $app_tag"
|
||||
else
|
||||
log "No public macOS app asset found; preserving existing app pin"
|
||||
fi
|
||||
if [[ -n "$app_lag_releases" ]]; then
|
||||
log "macOS app asset lags source release(s): $app_lag_releases"
|
||||
fi
|
||||
|
||||
if [[ "$current_rev" == "$selected_sha" && ( -z "$app_version" || "$current_app_version" == "$app_version" ) ]]; then
|
||||
has_update=false
|
||||
else
|
||||
has_update=true
|
||||
fi
|
||||
|
||||
printf 'has_update=%s\n' "$has_update"
|
||||
printf 'source_tag=%s\n' "$source_tag"
|
||||
printf 'source_sha=%s\n' "$selected_sha"
|
||||
printf 'source_version=%s\n' "$source_version"
|
||||
printf 'app_tag=%s\n' "$app_tag"
|
||||
printf 'app_url=%s\n' "$app_url"
|
||||
printf 'app_version=%s\n' "$app_version"
|
||||
printf 'latest_stable_tag=%s\n' "$latest_stable_tag"
|
||||
printf 'app_lag_releases=%s\n' "$app_lag_releases"
|
||||
}
|
||||
|
||||
apply_release() {
|
||||
local source_tag="$1"
|
||||
local selected_sha="$2"
|
||||
local app_tag="$3"
|
||||
local app_url="$4"
|
||||
local source_version source_url source_prefetch source_hash source_store_path app_version app_hash
|
||||
local backup_dir success
|
||||
|
||||
source_version="${source_tag#v}"
|
||||
source_url="https://github.com/openclaw/openclaw/archive/${selected_sha}.tar.gz"
|
||||
|
||||
source_prefetch=$(prefetch_json "$source_url")
|
||||
source_hash=$(printf '%s' "$source_prefetch" | jq -r '.hash // empty')
|
||||
source_store_path=$(printf '%s' "$source_prefetch" | jq -r '.path // .storePath // empty')
|
||||
if [[ -z "$source_hash" || -z "$source_store_path" ]]; then
|
||||
echo "Failed to resolve source hash/path for $selected_sha" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$app_tag" || -n "$app_url" ]]; then
|
||||
if [[ -z "$app_tag" || -z "$app_url" ]]; then
|
||||
echo "app_tag and app_url must either both be set or both be empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
app_version="${app_tag#v}"
|
||||
app_hash=$(unpacked_zip_hash "$app_url")
|
||||
if [[ -z "$app_hash" ]]; then
|
||||
echo "Failed to resolve app hash for $app_tag" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
rm -f "$app_build_log"
|
||||
else
|
||||
log "Skipping app build on non-darwin system (${current_system:-unknown})"
|
||||
fi
|
||||
|
||||
if git diff --quiet; then
|
||||
echo "No pin changes detected."
|
||||
exit 0
|
||||
fi
|
||||
backup_dir=$(mktemp -d)
|
||||
success=0
|
||||
cp "$source_file" "$backup_dir/source.nix"
|
||||
cp "$app_file" "$backup_dir/app.nix"
|
||||
cp "$config_options_file" "$backup_dir/config-options.nix"
|
||||
|
||||
log "Committing updated pins"
|
||||
git add "$source_file" "$app_file" "$repo_root/nix/generated/openclaw-config-options.nix" "$repo_root/flake.lock"
|
||||
git commit -F - <<'EOF'
|
||||
🤖 codex: bump openclaw pins (no-issue)
|
||||
cleanup_apply() {
|
||||
if [[ "$success" -ne 1 ]]; then
|
||||
cp "$backup_dir/source.nix" "$source_file"
|
||||
cp "$backup_dir/app.nix" "$app_file"
|
||||
cp "$backup_dir/config-options.nix" "$config_options_file"
|
||||
fi
|
||||
rm -rf "$backup_dir"
|
||||
}
|
||||
trap cleanup_apply RETURN
|
||||
|
||||
What:
|
||||
- pin openclaw source to latest upstream main
|
||||
- refresh macOS app pin to latest release asset
|
||||
- update source and app hashes
|
||||
- regenerate config options from upstream schema
|
||||
perl -0pi -e 's| releaseTag = "[^"]+";\n||g; s| releaseVersion = "[^"]+";\n||g;' "$source_file"
|
||||
perl -0pi -e "s|rev = \"[^\"]+\";|releaseTag = \"${source_tag}\";\n releaseVersion = \"${source_version}\";\n rev = \"${selected_sha}\";|" "$source_file"
|
||||
perl -0pi -e "s|hash = \"[^\"]+\";|hash = \"${source_hash}\";|" "$source_file"
|
||||
perl -0pi -e 's|pnpmDepsHash = "[^"]*";|pnpmDepsHash = "";|' "$source_file"
|
||||
|
||||
Why:
|
||||
- keep nix-openclaw on latest upstream for yolo mode
|
||||
if [[ -n "${app_version:-}" ]]; then
|
||||
perl -0pi -e "s|version = \"[^\"]+\";|version = \"${app_version}\";|" "$app_file"
|
||||
perl -0pi -e "s|url = \"[^\"]+\";|url = \"${app_url}\";|" "$app_file"
|
||||
perl -0pi -e "s|hash = \"[^\"]+\";|hash = \"${app_hash}\";|" "$app_file"
|
||||
fi
|
||||
|
||||
Tests:
|
||||
- nix build .#openclaw-gateway --accept-flake-config
|
||||
- nix build .#openclaw-app --accept-flake-config
|
||||
EOF
|
||||
refresh_pnpm_hash
|
||||
regenerate_config_options "$selected_sha" "$source_store_path"
|
||||
|
||||
log "Rebasing on latest main"
|
||||
git fetch origin main
|
||||
git rebase origin/main
|
||||
success=1
|
||||
}
|
||||
|
||||
git push origin HEAD:main
|
||||
mode="${1:-}"
|
||||
case "$mode" in
|
||||
select)
|
||||
if [[ $# -ne 1 ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
require_cmd jq
|
||||
require_cmd gh
|
||||
require_cmd node
|
||||
select_release
|
||||
;;
|
||||
apply)
|
||||
if [[ $# -ne 5 ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
require_cmd jq
|
||||
require_cmd nix
|
||||
require_cmd perl
|
||||
require_cmd unzip
|
||||
require_cmd find
|
||||
apply_release "$2" "$3" "$4" "$5"
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
@ -2,18 +2,27 @@
|
||||
description = "OpenClaw local";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
home-manager.url = "github:nix-community/home-manager";
|
||||
home-manager.inputs.nixpkgs.follows = "nixpkgs";
|
||||
nix-openclaw.url = "github:openclaw/nix-openclaw";
|
||||
nixpkgs.follows = "nix-openclaw/nixpkgs";
|
||||
home-manager.follows = "nix-openclaw/home-manager";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, home-manager, nix-openclaw }:
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
home-manager,
|
||||
nix-openclaw,
|
||||
}:
|
||||
let
|
||||
# REPLACE: aarch64-darwin (Apple Silicon), x86_64-darwin (Intel), or x86_64-linux
|
||||
# REPLACE: aarch64-darwin (Apple Silicon) or x86_64-linux
|
||||
system = "<system>";
|
||||
pkgs = import nixpkgs { inherit system; overlays = [ nix-openclaw.overlays.default ]; };
|
||||
in {
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ nix-openclaw.overlays.default ];
|
||||
};
|
||||
in
|
||||
{
|
||||
# REPLACE: <user> with your username (run `whoami`)
|
||||
homeConfigurations."<user>" = home-manager.lib.homeManagerConfiguration {
|
||||
inherit pkgs;
|
||||
@ -47,18 +56,14 @@
|
||||
# REPLACE: your Telegram user ID (get from @userinfobot)
|
||||
allowFrom = [ <allowFrom> ];
|
||||
groups = {
|
||||
"*" = { requireMention = true; };
|
||||
"*" = {
|
||||
requireMention = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
instances.default = {
|
||||
enable = true;
|
||||
plugins = [
|
||||
# Example plugin without config:
|
||||
{ source = "github:acme/hello-world"; }
|
||||
];
|
||||
};
|
||||
enable = true;
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user