Repository navigation
Expand file tree
/
Copy pathflows-rig.sh
More file actions
executable file
·1721 lines (1644 loc) · 70.4 KB
/
Copy pathflows-rig.sh
File metadata and controls
executable file
·1721 lines (1644 loc) · 70.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/sh
# flows-rig.sh: build, place and run basal's isolated test rig, ckdev-flows.
#
# The rig is a private subc daemon with its own port and XDG homes under the
# default root ~/Work/Projects/CortexKit-scratch/ckdev-flows (override with
# BASAL_RIG_ROOT), and its own ckdev-* binaries, each built here from a named
# revision. It runs the credentials vault, Fusiform, Broca, entorhinal,
# cingulate, prefrontal-core, prefrontal-routing, ck-basal and a rig-only
# callosum stub and run-scope probe. It never reads, writes or starts anything
# belonging to the production daemon or another rig.
#
# Usage:
# script/flows-rig.sh build [--prefrontal-rev <rev>] [--subc-rev <rev>]
# [--broca-rev <rev>] [--credentials-rev <rev>]
# [--commons-rev <rev>] [--entorhinal-rev <rev>]
# [--fusiform-rev <rev>] [--cingulate-rev <rev>]
# [--sibling-lock <repo>]... [--dry-run]
# script/flows-rig.sh copy-stage <stage dir> [--dry-run]
# script/flows-rig.sh place [--from-stage <dir>] [--dry-run]
# script/flows-rig.sh config [--dry-run]
# script/flows-rig.sh start [--dry-run]
# script/flows-rig.sh status [--dry-run]
# script/flows-rig.sh stop [--dry-run]
# script/flows-rig.sh manifest [--dry-run]
# script/flows-rig.sh credential --key-file <path> [--dry-run]
# script/flows-rig.sh test [--models | --packages] [--dry-run]
#
# --dry-run prints every command the subcommand would run and every file it
# would write, with the file's content, and changes nothing.
#
# Subcommands:
# build clone each repository at the named revision and build it.
# prefrontal defaults to defe4a72a (the codemode run-scope build);
# cingulate defaults to c650e9db (the deployed consent/v1 build),
# and other repositories default to HEAD. basal is always built from this checkout's
# HEAD, without uncommitted changes. The source checkouts are
# only read. A build that changes a tracked file fails, except
# that --sibling-lock <repo> lets that repository's Cargo.lock
# change the versions of path dependencies on a sibling clone
# (claustrum builds against ../subconscious by path); the
# manifest then records its cargo_lock as sibling-refreshed.
# place sign every binary by script/signing.sh under a ckdev-*
# identifier (basal keeps its production identifiers) and place it
# in bin/. The rig's ck-basal is built
# with the rig-kill-hook feature, which the contract suite's
# crash case needs.
# config write subc.jsonc, bootstrap the key-file vault and grant only
# Broca's read of apikey:openai and routing's llm-provider list.
# Temporarily start the rig to read its served Fusiform catalog,
# then pin alfonso-routing.jsonc to openai/gpt-6-luna. Refuse a
# catalog without that model. No production config is copied.
# Luna's only score is a labeled rig fixture (elo 1, eq/speed 0),
# not measured quality; the stack manifest records that fact.
# credential ingest the isolated API key with provider_ids: ["openai"],
# delete the input file after deposit and confirm exactly one
# active credential. The operator saves it at
# $RIG_HOME/.secrets/openai.key.
# start, status, stop
# run, inspect (pids, identifiers, open stores) and stop the
# daemon. status fails if any open store lies outside the rig.
# manifest write results/<timestamp>/stack.json: every repository's
# commit and each binary's sha256 and signing identifier.
# test start the rig if needed, write a manifest and run the live
# contract suite (basal-rig-contract) against the real
# prefrontal-core, with contract.json and contract.log beside
# the manifest. It fails if any check failed.
# A full run without --models reports model cases as not run by name.
# --packages runs only setup, the package group and its cleanup;
# the report marks the other groups as not run, not as a full pass.
# Full runs include codemode, using a real core-owned agent scope and a
# rig-only agent-run-scopes/v1 probe. It checks identity, catalog,
# console output, return value, CPU exhaustion and scope closure.
# The ungranted-worker case is reported as not run: registering a
# real worker needs a core task, which this rig does not launch.
# It does not test AFT's allow/ask/deny round trip;
# that needs AFT itself to declare agent-run-scopes/v1.
# Package recovery waits for the stored generation to advance once
# and its version to be confirmed. Further-pass call-count checks
# are not run: no completed-pass or received-ensure counter exists.
# --models requires that credential and agent-owned flows through
# core's relay (prefrontal 73c66ff1f or later): minimal call first,
# routing/journal, result/usage settlement, classify, token-cap
# refusal, crash recovery and no tools. Three tiny calls at most
# in normal execution; each manifest caps all its scheduled runs
# in a day, for an aggregate allowance of 4,112 tokens per suite
# (including the unscoped-send flow described below, if Broca's
# enforcement regresses).
# A flow scope is the subc daemon's record of one flow's
# authority: core registers one per flow, and basal opens every
# route it uses for that flow under it. A scope's carrier list
# names the only principals the daemon lets open routes under
# that scope. Each send's Broca checkpoint must match core's
# flow_scope selector, attest reserved:basal as the first sender
# and freeze the flow_id. A direct client opening the same selector
# must be refused as a non-carrier. That proves an outsider can't
# use the scope; it doesn't read the carrier list, which no store
# exposes. Tests in the prefrontal repository prove that the
# carrier list core registers for every flow scope, agent-owned
# or global, is exactly [reserved:basal]. For reference, they are
# at commit cba528a11, in
# crates/prefrontal-core-module/src/scope_owner.rs:
# flow_scope_registration_is_basal_only,
# registered_flow_scope_wire_vector_pins_attributes_and_the_entire_carrier_list,
# registered_global_flow_scope_wire_vector_pins_no_agent_and_the_entire_carrier_list.
# The rig's own ck-basal build (with the unscoped-send switch
# described below) also sends once without a scope and requires
# Broca's flow_scope_required refusal with no RunStarted or
# run_index row.
# Any first-call refusal is reported verbatim and stops the model
# cases; it is never retried with another provider or model.
#
# copy-stage <dir> copies the stage's binaries, SHA-256 sidecars, card.md and
# revision into stages/<stage-name>/ so a worker can use the copy without
# reading production staging. The head runs this before handing the path to a
# worker. place --from-stage <dir> places ck-basal and ck-basal-worker from a
# stage directory script/stage.sh wrote, byte for byte and under their
# production identifiers, instead of the rig's own build of them; every other
# binary is the rig's build. The stage must be of the commit the rig built basal at.
# The rig's own ck-basal is built with two rig-only switches: a one-shot kill
# switch, armed by a file, that kills ck-basal at a named point in a run so the
# suite can check crash recovery; and an unscoped-send switch, armed by
# another file, that sends one model call to Broca without the flow's scope so
# the suite can check that Broca refuses it. A staged ck-basal has neither
# switch, so test reports the sink crash, pre-send route checkpoint, accepted
# model crash and unscoped-send cases as not run; a plain place puts the rig's
# build back.
#
# What the rig isolates:
# - Everything lives under $RIG: src/ (one clone per repository at the
# commit built), build/ (cargo output and the build record), bin/ (the
# placed, signed binaries), config/, data/, runtime/ (the rig's XDG
# homes), home/ (HOME and working directory of every rig process), logs/,
# results/<timestamp>/ and stages/.
# - The daemon listens on port 8791; production's listens on 8757. config
# and start refuse if 8791 is taken.
# - Every rig process starts from an empty environment: the login name,
# HOME set to home/, the rig's XDG homes, SUBC_CONNECTION_FILE naming the
# rig's connection file, and a PATH with every CortexKit directory
# removed, so no production variable or file under the real home
# reaches a rig module.
# - The rig root must resolve outside ~/.local/share/cortexkit/ and /Volumes/.
# Every rig path is also refused if its written or resolved form reaches
# the production CortexKit store.
# - The vault starts empty with its own key file, so the macOS keychain is
# never touched. Every auth command names the rig data, key and connection
# paths explicitly. Fusiform's store and HTTPS catalog polling are isolated
# too; no production auth-methods.json is ever copied into the rig.
#
# Model cases need core's agent-owned flow scope registration at approval:
# prefrontal 73c66ff1f or later, plus a basal build that opens scoped Broca
# routes. The scope checks require Broca's flow_scope_required enforcement;
# every refusal is checked, never assumed. Any scope_owner_mismatch must not
# be bypassed.
# Flow install and package cards use cingulate's consent/v1 API. Flow cards
# are matched to core's durable install intent before the operator answers.
# Cingulate's default matches the deployed consent/v1 provider. Explicit
# revision overrides let a rehearsal test the same bytes a deployment places.
# script/flows-rig.sh build --prefrontal-rev defe4a72a3ee4bac8ce81645df801c6907a8ce4c --cingulate-rev c650e9db6e87dbb28fe20dd5fc2815db5deb41e9 --sibling-lock claustrum
set -eu
ROOT=$(cd "$(dirname "$0")/.." && pwd -P)
# The signing policy stage.sh signs production binaries with.
# shellcheck source=script/signing.sh
. "$ROOT/script/signing.sh"
WORKSPACE=${CORTEXKIT_WORKSPACE:-$HOME/Work/Projects/CortexKit}
CK_SHARE="$HOME/.local/share/cortexkit"
CK_CONFIG="$HOME/.config/cortexkit"
RIG=${BASAL_RIG_ROOT:-$HOME/Work/Projects/CortexKit-scratch/ckdev-flows}
BIN="$RIG/bin"
SRC="$RIG/src"
TARGETS="$RIG/build/target"
STACK="$RIG/build/stack.tsv"
# Where the placed ck-basal and ck-basal-worker came from: the rig's build
# (with the kill switch) or a stage. `place` writes it; `test`, `status` and
# the manifest read it.
BASAL_SOURCE="$RIG/build/basal-source.tsv"
LOGS="$RIG/logs"
RESULTS="$RIG/results"
STAGES="$RIG/stages"
CONFIG_HOME="$RIG/config"
DATA_HOME="$RIG/data"
RUNTIME_DIR="$RIG/runtime"
# HOME for every rig process. A module that falls back to $HOME for any path
# (OpenCode's auth.json, a ~/.config file, a dot-directory) then lands inside
# the rig instead of reaching the user's real files.
RIG_HOME="$RIG/home"
CONN="$RUNTIME_DIR/subc-connection.json"
PIDFILE="$RUNTIME_DIR/ckdev-subc.pid"
SUBC_CONFIG="$CONFIG_HOME/cortexkit/subc.jsonc"
ROUTING_CONFIG="$CONFIG_HOME/cortexkit/alfonso-routing.jsonc"
BROCA_INDEX="$DATA_HOME/cortexkit/broca/run-index.db"
# The vault's data directory is where the daemon's storage convention puts
# module `claustrum`. Its master key must live outside that directory (the
# vault refuses a key beside its own store), so it sits in the config home.
VAULT_DIR="$DATA_HOME/cortexkit/claustrum"
VAULT_KEY="$CONFIG_HOME/claustrum/master.key"
# The stores the contract suite reads (read-only), and the file whose content
# names the runtime boundary at which the rig build of ck-basal kills itself
# once (crates/basal-module/src/rig_kill.rs); the suite writes it.
CORE_STORE="$DATA_HOME/cortexkit/prefrontal-core/store.db"
BASAL_STORE="$DATA_HOME/cortexkit/basal/store.db"
MACHINE_ID="$DATA_HOME/cortexkit/machine-id"
KILL_FILE="$RUNTIME_DIR/basal-kill-at"
UNSCOPED_FILE="$RUNTIME_DIR/basal-unscoped-send"
# The contract suite: a client of the rig, not a module, so it is run from
# the build output rather than placed in bin/.
CONTRACT="$TARGETS/basal/release/basal-rig-contract"
# Where the contract suite's projects live: one git repository per run, which
# the rig creates under its own home and registers in its entorhinal (see
# ensure_project).
PROJECTS="$RIG_HOME/projects"
# Clear of production's daemon (8757) and of the older isolation rig under
# ckdev-rig/ (8799, plus 8377 and 8378 for one of its modules).
PORT=8791
DRY=0
say() { printf '%s\n' "$*"; }
die() { printf 'flows-rig: %s\n' "$*" >&2; exit 1; }
usage() {
awk '/^# Usage:/ { on = 1 } /^set -eu/ { exit } on' "$0" | sed 's/^# \{0,1\}//' >&2
exit 2
}
# ---------------------------------------------------------------- guards
# The physical form of a path, following symlinks in every existing parent,
# so a symlink inside the rig cannot point a write at production.
physical() {
# Resolve even a dangling final link: a later write could create its target.
# Python also follows chains and refuses loops instead of silently walking
# back to a lexical parent as the shell's -e test does for a dangling link.
python3 -c 'from pathlib import Path; import sys; print(Path(sys.argv[1]).resolve())' "$1" \
|| die "cannot resolve $1"
}
# Refuse any path under ~/.local/share/cortexkit/, both as written and after
# resolving symlinks. The rig root is checked separately and is never exempt.
guard_path() {
resolved=$(physical "$1") || exit 1
for form in "$1" "$resolved"; do
case "$form" in
"$CK_SHARE" | "$CK_SHARE"/* | "$CK_SHARE_PHYSICAL" | "$CK_SHARE_PHYSICAL"/*)
die "refusing: $1 resolves to $form under $CK_SHARE (outside the rig)" ;;
esac
done
}
guard_all_paths() {
case "$RIG" in
/*) ;;
*) die "refusing: BASAL_RIG_ROOT must be an absolute path: $RIG" ;;
esac
CK_SHARE_PHYSICAL=$(physical "$CK_SHARE")
RIG_PHYSICAL=$(physical "$RIG")
for root_form in "$RIG" "$RIG_PHYSICAL"; do
case "$root_form" in
"$CK_SHARE"|"$CK_SHARE"/*|"$CK_SHARE_PHYSICAL"|"$CK_SHARE_PHYSICAL"/*)
die "refusing: the rig root $RIG resolves under production store $CK_SHARE" ;;
/Volumes|/Volumes/*)
die "refusing: the rig root $RIG resolves under /Volumes; external disks are not supported" ;;
esac
done
for path in "$BIN" "$SRC" "$TARGETS" "$STACK" "$BASAL_SOURCE" "$LOGS" "$RESULTS" "$STAGES" \
"$CONFIG_HOME" "$DATA_HOME" "$RUNTIME_DIR" "$CONN" "$PIDFILE" \
"$SUBC_CONFIG" "$ROUTING_CONFIG" "$BROCA_INDEX" "$VAULT_DIR" "$VAULT_KEY" "$RIG_HOME" "$CORE_STORE" \
"$BASAL_STORE" "$MACHINE_ID" "$KILL_FILE" "$UNSCOPED_FILE" "$CONTRACT" "$PROJECTS"; do
guard_path "$path"
done
}
# The pid listening on the rig port, if any.
port_listener() {
lsof -nP -iTCP:"$PORT" -sTCP:LISTEN -t 2>/dev/null | head -n 1 || true
}
# For subcommands that would start a daemon: the port must be free.
guard_port_free() {
# Dry runs start no daemon, so host port occupancy is irrelevant to them.
[ "$DRY" = 1 ] && return 0
listener=$(port_listener)
[ -z "$listener" ] || die "refusing: port $PORT is already taken by pid $listener"
}
# For subcommands that talk to a running rig: the port is free or held by the
# rig's own daemon, never by anything else.
guard_port_ours() {
listener=$(port_listener)
[ -z "$listener" ] || [ "$listener" = "${1:-}" ] \
|| die "refusing: port $PORT is held by pid $listener, which is not the rig daemon"
}
# ---------------------------------------------------------------- helpers
run() {
if [ "$DRY" = 1 ]; then
say "+ $*"
else
"$@"
fi
}
# Run a command in a directory (cargo picks its toolchain file from there).
run_in() {
dir=$1
shift
if [ "$DRY" = 1 ]; then
say "+ (cd $dir) $*"
else
(cd "$dir" && "$@")
fi
}
# Write stdin to a file through a temp name and a rename.
write_file() {
dest=$1
guard_path "$dest"
if [ "$DRY" = 1 ]; then
say "+ write $dest:"
sed 's/^/| /'
return
fi
mkdir -p "$(dirname "$dest")"
tmp=$(mktemp "$(dirname "$dest")/.write.XXXXXX")
cat > "$tmp"
mv -f "$tmp" "$dest"
}
# PATH with every entry under the CortexKit data or config tree removed, so a
# rig process that runs a program by name can never reach a production binary.
rig_path() {
# The trailing newline matters: read drops a final line that lacks one.
printf '%s\n' "$PATH" | tr ':' '\n' | while IFS= read -r entry; do
case "$entry" in
"$CK_SHARE" | "$CK_SHARE"/* | "$CK_CONFIG" | "$CK_CONFIG"/* | "") ;;
*) printf '%s\n' "$entry" ;;
esac
done | paste -s -d: -
}
# Run a command in the rig's environment: an empty environment plus the rig's
# own HOME (also the working directory), its three XDG homes, its connection
# file, a temp dir inside its runtime dir, and the user's name. Nothing
# inherited from the caller can redirect a module at production state
# (CK_MASTER_KEY_PATH, BROCA_STATE_ROOT, SUBC_PORT, the real HOME, ...).
# rig_env_exec replaces the calling (sub)shell, so `(rig_env_exec cmd) &`
# leaves $! naming cmd itself.
rig_env() {
(rig_env_exec "$@")
}
rig_env_exec() {
cd "$RIG_HOME" || die "no rig home at $RIG_HOME; run start first"
# Close every descriptor above stderr first. Whatever launched this script
# (an agent's shell tool, a terminal multiplexer) may hold files open, and a
# daemon inheriting them would keep files outside the rig open for its whole
# life and pass them on to every module it spawns.
exec python3 -c 'import os, sys
os.closerange(3, min(os.sysconf("SC_OPEN_MAX"), 1 << 16))
os.execvp(sys.argv[1], sys.argv[1:])' \
env -i \
HOME="$RIG_HOME" USER="${USER:-}" LOGNAME="${LOGNAME:-}" LANG="${LANG:-en_US.UTF-8}" \
BASAL_RIG_ROOT="$RIG" \
PATH="$(rig_path)" \
TMPDIR="$RUNTIME_DIR/tmp" \
XDG_CONFIG_HOME="$CONFIG_HOME" \
XDG_DATA_HOME="$DATA_HOME" \
XDG_RUNTIME_DIR="$RUNTIME_DIR" \
SUBC_CONNECTION_FILE="$CONN" \
"$@"
}
# `ck` against the rig. SUBC_CONNECTION_FILE is exclusive in ck's discovery
# (it never falls back to another daemon), and any `daemon: <path>` line ck
# prints must name the rig's connection file. ck prints that line only after
# a module mutation; for read verbs, check_daemon_identity compares pids.
rig_ck() {
out=$(rig_env "$BIN/ckdev-ck" "$@" 2>&1) || {
printf '%s\n' "$out"
die "ck $* failed"
}
printf '%s\n' "$out"
named=$(printf '%s\n' "$out" | sed -n 's/^daemon: \(\/.*\)$/\1/p')
if [ -n "$named" ] && [ "$named" != "$CONN" ]; then
die "ck $* answered from $named, not the rig's $CONN"
fi
}
# ck's auth face is linked only inside the rig. All three paths are explicit
# even for offline bootstrap, so neither CLI discovery nor a keychain fallback
# can ever select the operator's real vault.
rig_auth() {
# Follow final symlinks too: a key-file or connection-file symlink must not
# make explicit rig flags a disguised reference to production.
python3 - "$RIG_PHYSICAL" "$VAULT_DIR" "$VAULT_KEY" "$CONN" <<'PY' || die "auth paths escape the rig"
import os, sys
root = sys.argv[1] + os.sep
for path in sys.argv[2:]:
if not os.path.realpath(path).startswith(root):
sys.exit("flows-rig: auth path resolves outside the rig: " + path)
PY
run rig_env PATH="$BIN:$(rig_path)" "$BIN/ckdev-ck" auth "$@" \
--data-dir "$VAULT_DIR" --key-path "$VAULT_KEY" --subc "$CONN"
}
conn_pid() {
python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["pid"])' "$CONN" 2>/dev/null || true
}
identifier_of() {
codesign_identifier "$1"
}
# The placement mode `place` recorded: "rig-build" or "staged".
basal_mode() {
if [ -f "$BASAL_SOURCE" ]; then
cut -f1 "$BASAL_SOURCE"
else
printf 'rig-build\n'
fi
}
# The signing identifier a placed basal binary must carry: always its
# production one (ck-basal, ck-basal-worker). Only the rig's file names change
# to ckdev-, so the processes can't be mistaken for the production module;
# macOS and basal's own checks identify code by its signing identifier, which
# the file name doesn't affect.
basal_identifier() {
printf '%s\n' "$1"
}
# The repositories the rig builds from: name, source checkout, Cargo-built.
# The clones under $SRC keep these names because the repositories reach each
# other by relative path (../subconscious, ../commons, ../claustrum).
repos() {
cat <<EOF
subconscious $WORKSPACE/subconscious
commons $WORKSPACE/commons
broca $WORKSPACE/broca
claustrum $WORKSPACE/claustrum
entorhinal $WORKSPACE/entorhinal
fusiform $WORKSPACE/fusiform
cingulate $WORKSPACE/cingulate
prefrontal $WORKSPACE/prefrontal
basal $ROOT
EOF
}
# Every placed binary: rig name, repository, cargo binary, placed file name.
# Every placed file is named ckdev-*, including basal's two even when their
# bytes come from a production stage. That works because ck-basal looks for
# its worker beside itself as "<its own file name>-worker", so ckdev-basal
# finds ckdev-basal-worker.
binaries() {
cat <<'EOF'
subc subconscious ck-subc ckdev-subc
ck subconscious ck ckdev-ck
broca broca ck-broca ckdev-broca
claustrum claustrum ck-claustrum ckdev-claustrum
auth claustrum ck-auth ckdev-auth
entorhinal entorhinal ck-entorhinal ckdev-entorhinal
fusiform fusiform ck-fusiform ckdev-fusiform
models fusiform ck-models ckdev-models
cingulate cingulate ck-cingulate ckdev-cingulate
prefrontal-core prefrontal ck-prefrontal-core ckdev-prefrontal-core
prefrontal-routing prefrontal ck-prefrontal-routing ckdev-prefrontal-routing
basal basal ck-basal ckdev-basal
basal-worker basal ck-basal-worker ckdev-basal-worker
callosum basal ck-callosum-stub ckdev-callosum
runscope-probe basal ckdev-runscope-probe ckdev-runscope-probe
EOF
}
# The supervised modules: subc module id and placed file name.
modules() {
cat <<'EOF'
claustrum ckdev-claustrum
broca ckdev-broca
fusiform ckdev-fusiform
entorhinal ckdev-entorhinal
cingulate ckdev-cingulate
prefrontal-core ckdev-prefrontal-core
prefrontal-routing ckdev-prefrontal-routing
basal ckdev-basal
callosum ckdev-callosum
ckdev-runscope-probe ckdev-runscope-probe
EOF
}
all_placed() {
for file in $(binaries | cut -f4); do
[ -f "$BIN/$file" ] || return 1
done
}
daemon_pid() {
[ -f "$PIDFILE" ] || return 0
pid=$(cat "$PIDFILE")
if kill -0 "$pid" 2>/dev/null; then
printf '%s\n' "$pid"
fi
}
# ---------------------------------------------------------------- build
cmd_build() {
prefrontal_rev=defe4a72a3ee4bac8ce81645df801c6907a8ce4c
subc_rev=HEAD
broca_rev=HEAD
credentials_rev=HEAD
commons_rev=HEAD
entorhinal_rev=HEAD
fusiform_rev=HEAD
cingulate_rev=c650e9db6e87dbb28fe20dd5fc2815db5deb41e9
sibling_lock=""
while [ $# -gt 0 ]; do
case "$1" in
--sibling-lock) [ $# -ge 2 ] || usage; sibling_lock="$sibling_lock $2"; shift 2 ;;
--prefrontal-rev) [ $# -ge 2 ] || usage; prefrontal_rev=$2; shift 2 ;;
--subc-rev) [ $# -ge 2 ] || usage; subc_rev=$2; shift 2 ;;
--broca-rev) [ $# -ge 2 ] || usage; broca_rev=$2; shift 2 ;;
--credentials-rev) [ $# -ge 2 ] || usage; credentials_rev=$2; shift 2 ;;
--commons-rev) [ $# -ge 2 ] || usage; commons_rev=$2; shift 2 ;;
--entorhinal-rev) [ $# -ge 2 ] || usage; entorhinal_rev=$2; shift 2 ;;
--fusiform-rev) [ $# -ge 2 ] || usage; fusiform_rev=$2; shift 2 ;;
--cingulate-rev) [ $# -ge 2 ] || usage; cingulate_rev=$2; shift 2 ;;
*) usage ;;
esac
done
# Pin core's codemode run-scope API instead of following a moving HEAD.
# Record overrides in the manifest so results identify the revision tested.
if [ -n "$(git -C "$ROOT" status --porcelain --untracked-files=no)" ]; then
say "note: $ROOT has uncommitted changes; the rig builds its HEAD commit only"
fi
# Forget the previous build before starting this one. The record is written
# again only when every crate has built, so after a failed build `place`
# refuses instead of placing a mix of new and old binaries.
run rm -f "$STACK"
stack=""
while IFS=' ' read -r name source; do
case "$name" in
subconscious) rev=$subc_rev ;;
commons) rev=$commons_rev ;;
broca) rev=$broca_rev ;;
claustrum) rev=$credentials_rev ;;
entorhinal) rev=$entorhinal_rev ;;
fusiform) rev=$fusiform_rev ;;
cingulate) rev=$cingulate_rev ;;
prefrontal) rev=$prefrontal_rev ;;
basal) rev=HEAD ;;
esac
[ -d "$source" ] || die "no checkout of $name at $source"
sha=$(git -C "$source" rev-parse --verify --quiet "$rev^{commit}") \
|| die "$name: cannot resolve $rev in $source"
say "$name: $rev -> $sha ($source)"
clone_at "$name" "$source" "$sha"
stack="$stack$name $source $rev $sha
"
done <<EOF_REPOS
$(repos)
EOF_REPOS
cargo_build subconscious "" -p subc-core --bin ck-subc --bin ck
cargo_build broca "" -p broca-module-serve --bin ck-broca
cargo_build claustrum "" -p credentials-module --bin ck-claustrum --bin ck-auth
cargo_build entorhinal "" -p entorhinal-module --bin ck-entorhinal
cargo_build fusiform "" -p fusiform-module --bin ck-fusiform
cargo_build fusiform "" -p fusiform-cli --bin ck-models
cargo_build cingulate "" -p ck-cingulate --bin ck-cingulate
prefrontal_sha=$(printf '%s' "$stack" | awk -F'\t' '$1=="prefrontal"{print $4}')
# prefrontal's build scripts embed the revision they were told; the clone
# is at that exact commit with no local changes, so it is not dirty.
cargo_build prefrontal "CK_BUILD_GIT_SHA=$prefrontal_sha CK_BUILD_GIT_DIRTY=false" \
-p prefrontal-core-module --bin ck-prefrontal-core
cargo_build prefrontal "CK_BUILD_GIT_SHA=$prefrontal_sha CK_BUILD_GIT_DIRTY=false" \
-p prefrontal-routing-module --bin ck-prefrontal-routing
# The rig's ck-basal carries the one-shot kill switch the contract suite's
# crash case arms; a production build never enables this feature.
# basal's binaries embed their revision too, as script/stage.sh builds them.
basal_sha=$(printf '%s' "$stack" | awk -F'\t' '$1=="basal"{print $4}')
basal_env="CK_BUILD_GIT_SHA=$basal_sha CK_BUILD_GIT_DIRTY=false"
cargo_build basal "$basal_env" -p basal-module --features rig-kill-hook --bin ck-basal
cargo_build basal "$basal_env" -p basal-worker --bin ck-basal-worker
cargo_build basal "" -p basal-rig --bin ck-callosum-stub --bin ckdev-runscope-probe --bin basal-rig-contract
# A build that rewrote a tracked file (a Cargo.lock refreshed against a
# sibling at another revision) did not build exactly the named commit.
# The one exception is a repository named with --sibling-lock: its
# Cargo.lock may change, but only the versions of path dependencies on a
# sibling clone. That happens when the repository was built in production
# against a sibling checkout at an earlier revision than the rig's. The
# change is saved beside the clone, recorded in the stack, and undone.
locks=""
while IFS=' ' read -r name source; do
[ "$DRY" = 0 ] || continue
changed=$(git -C "$SRC/$name" status --porcelain --untracked-files=no)
[ -n "$changed" ] || continue
case " $sibling_lock " in
*" $name "*)
if [ "$changed" = " M Cargo.lock" ] && sibling_versions_only "$SRC/$name"; then
git -C "$SRC/$name" diff Cargo.lock > "$SRC/$name.sibling-lock.diff"
git -C "$SRC/$name" checkout --quiet -- Cargo.lock
say "$name: Cargo.lock refreshed against sibling path dependencies; diff in $SRC/$name.sibling-lock.diff"
locks="$locks $name"
continue
fi
;;
esac
git -C "$SRC/$name" status --short --untracked-files=no >&2
die "$name: the build changed tracked files in $SRC/$name"
done <<EOF_REPOS
$(repos)
EOF_REPOS
stack=$(printf '%s' "$stack" | while IFS=' ' read -r n s r c; do
# Leading-paren patterns: /bin/sh's bash 3.2 misparses a bare pattern's
# closing paren inside a command substitution.
case " $locks " in (*" $n "*) l=sibling-refreshed ;; (*) l=committed ;; esac
printf '%s\t%s\t%s\t%s\t%s\n' "$n" "$s" "$r" "$c" "$l"
done)
printf 'repo\tsource\trequested\tcommit\tcargo_lock\n%s\n' "$stack" | write_file "$STACK"
say "built; next: $0 place"
}
# sibling_versions_only <clone>: succeed if the clone's Cargo.lock differs from
# its commit only in the version of packages that have no `source`, which are
# path dependencies, and adds or removes no package.
sibling_versions_only() {
git -C "$1" show HEAD:Cargo.lock > "$1.committed-lock"
status=0
python3 - "$1.committed-lock" "$1/Cargo.lock" <<'PY' || status=$?
import sys
def blocks(path):
out = {}
for chunk in open(path).read().split("[[package]]")[1:]:
fields = dict(
line.split(" = ", 1) for line in chunk.strip().splitlines()
if " = " in line and not line.startswith(" ")
)
out[(fields["name"], fields.get("source"))] = (fields, chunk)
return out
old = blocks(sys.argv[1])
new = blocks(sys.argv[2])
if old.keys() != new.keys():
sys.exit(1)
for key, (fields, chunk) in new.items():
if chunk == old[key][1]:
continue
if key[1] is not None:
sys.exit(1)
if chunk.replace(fields["version"], old[key][0]["version"], 1) != old[key][1]:
sys.exit(1)
PY
rm -f "$1.committed-lock"
return "$status"
}
# Bring $SRC/<name> to exactly <sha> without touching the source checkout:
# a separate clone, detached at the commit, with no local changes.
clone_at() {
name=$1
source=$2
sha=$3
dest="$SRC/$name"
guard_path "$dest"
if [ -d "$dest/.git" ]; then
# The clone is the rig's own; origin only says where to fetch from, and
# basal's source moves between worktrees.
run git -C "$dest" remote set-url origin "$source"
run git -C "$dest" fetch --quiet origin
else
run mkdir -p "$SRC"
run git clone --quiet --no-checkout "$source" "$dest"
fi
if [ "$DRY" = 0 ] && ! git -C "$dest" cat-file -e "$sha^{commit}" 2>/dev/null; then
git -C "$dest" fetch --quiet origin "$sha"
fi
run git -C "$dest" checkout --quiet --force --detach "$sha"
if [ "$DRY" = 0 ] && [ -n "$(git -C "$dest" status --porcelain --untracked-files=no)" ]; then
die "$dest has local changes after checkout"
fi
}
# cargo_build <repo> "<VAR=value ...>" <cargo args>: a release build in the
# clone, into a target directory of the rig's own.
cargo_build() {
repo=$1
extra_env=$2
shift 2
# shellcheck disable=SC2086 # extra_env is a list of VAR=value words
run_in "$SRC/$repo" env CARGO_TARGET_DIR="$TARGETS/$repo" $extra_env \
cargo build --release "$@"
}
# copy-stage <stage dir> checks both binary sidecars and copies the complete
# stage into the rig without changing the source in production staging.
verify_stage_sha() {
file=$1
sidecar=$2
want=$(awk 'NF { print $1; exit }' "$sidecar" | tr '[:upper:]' '[:lower:]')
case "$want" in
''|*[!0123456789abcdef]*) return 1 ;;
esac
[ "${#want}" -eq 64 ] || return 1
got=$(shasum -a 256 "$file" | awk '{ print tolower($1) }') || return 1
[ "$got" = "$want" ]
}
cmd_copy_stage() {
[ $# -eq 1 ] || usage
stage=$(cd "$1" 2>/dev/null && pwd -P) || die "no stage directory at $1"
name=${stage##*/}
case "$name" in
''|.|..|*[!A-Za-z0-9._-]*) die "refusing: invalid stage directory name $name" ;;
esac
for file in ck-basal ck-basal.sha256 ck-basal-worker ck-basal-worker.sha256 card.md revision; do
[ -f "$stage/$file" ] && [ ! -L "$stage/$file" ] \
|| die "refusing: stage is missing a regular $file file"
done
for binary in ck-basal ck-basal-worker; do
verify_stage_sha "$stage/$binary" "$stage/$binary.sha256" \
|| die "refusing: $stage/$binary fails its SHA-256 sidecar"
done
destination="$STAGES/$name"
guard_path "$destination"
[ ! -e "$destination" ] && [ ! -L "$destination" ] \
|| die "refusing: copied stage already exists at $destination"
if [ "$DRY" = 1 ]; then
say "+ mkdir -p $STAGES"
say "+ copy ck-basal ck-basal.sha256 ck-basal-worker ck-basal-worker.sha256 card.md revision from $stage to $destination"
say "+ verify both copied binaries against their SHA-256 sidecars"
return
fi
mkdir -p "$STAGES"
tmp=$(mktemp -d "$STAGES/.copy-stage.XXXXXX") || die "cannot create a temporary stage copy"
for file in ck-basal ck-basal.sha256 ck-basal-worker ck-basal-worker.sha256 card.md revision; do
if ! cp "$stage/$file" "$tmp/$file"; then
rm -rf "$tmp"
die "cannot copy $stage/$file into the rig"
fi
done
for binary in ck-basal ck-basal-worker; do
if ! verify_stage_sha "$tmp/$binary" "$tmp/$binary.sha256"; then
rm -rf "$tmp"
die "copied $binary fails its SHA-256 sidecar"
fi
done
mv "$tmp" "$destination"
say "copied and verified stage $name at $destination"
}
# ---------------------------------------------------------------- place
cmd_place() {
stage=""
while [ $# -gt 0 ]; do
case "$1" in
--from-stage) [ $# -ge 2 ] || usage; stage=$2; shift 2 ;;
*) usage ;;
esac
done
[ -z "$(daemon_pid)" ] || die "refusing: the rig daemon is running; stop it first"
[ "$DRY" = 1 ] || [ -f "$STACK" ] || die "nothing built yet; run build first"
revision=""
if [ -n "$stage" ]; then
stage=$(cd "$stage" 2>/dev/null && pwd -P) || die "no stage directory at $stage"
# stage.sh's own check of a stage: both sidecars and both signatures,
# under the production identifiers the fleet's placement would use.
verified=$(sh "$ROOT/script/stage.sh" --verify "$stage") \
|| die "the stage at $stage fails stage.sh --verify"
revision=$(printf '%s\n' "$verified" | sed -n 's/^revision //p')
if [ "$DRY" = 0 ]; then
built=$(awk -F'\t' '$1=="basal"{print $4}' "$STACK")
# The callosum stub, the contract suite and the worker's gate script
# come from the rig's build, so it must be of the staged commit.
[ "$revision" = "$built" ] \
|| die "the stage is of $revision but the rig built basal at $built; build the rig at the staged commit first"
fi
fi
run mkdir -p "$BIN"
while IFS=' ' read -r name repo cargo_bin file; do
case "$name" in
basal | basal-worker)
if [ -n "$stage" ]; then
place_staged "$name" "$stage/$cargo_bin" "$BIN/$file" "$cargo_bin"
continue
fi ;;
esac
place_one "$name" "$TARGETS/$repo/release/$cargo_bin" "$BIN/$file"
done <<EOF_BINARIES
$(binaries)
EOF_BINARIES
run ln -sf ckdev-auth "$BIN/ck-auth"
if [ -n "$stage" ]; then
printf 'staged\t%s\t%s\n' "$stage" "$revision" | write_file "$BASAL_SOURCE"
else
printf 'rig-build\n' | write_file "$BASAL_SOURCE"
fi
worker_identifier=ck-basal-worker
# The worker runs flow code, so it must also pass basal's own gate at its
# final path: hardened runtime, no entitlements, the checked-in Seatbelt
# profile embedded, and a live probe that confinement denies a file read,
# a socket connect and a program launch. The gate script comes from the
# basal clone, so it checks against the profile of the commit that was built.
worker="$BIN/ckdev-basal-worker"
if [ "$DRY" = 1 ]; then
say "+ BASAL_WORKER_IDENTIFIER=$worker_identifier sh $SRC/basal/script/sign-worker.sh verify $worker"
elif ! BASAL_WORKER_IDENTIFIER=$worker_identifier sh "$SRC/basal/script/sign-worker.sh" verify "$worker"; then
rm -f "$worker"
die "the placed worker failed basal's confinement gate and was removed"
fi
say "placed; next: $0 config"
}
# Sign a copy under a temp name with an explicit identifier (codesign would
# otherwise derive one from the temp name), check it, rename it into place,
# and check the final file again.
place_one() {
name=$1
built=$2
dest=$3
identifier="ckdev-$name"
case "$name" in
basal | basal-worker) identifier="ck-$name" ;;
esac
guard_path "$dest"
if [ "$DRY" = 1 ]; then
say "+ cp $built $BIN/.place.XXXXXX; chmod 0755 $BIN/.place.XXXXXX"
say "+ sign_hardened $BIN/.place.XXXXXX $identifier (script/signing.sh)"
say "+ verify_hardened $BIN/.place.XXXXXX $identifier"
say "+ mv -f $BIN/.place.XXXXXX $dest"
say "+ verify_hardened $dest $identifier"
return
fi
[ -f "$built" ] || die "$name: no built binary at $built"
tmp=$(mktemp "$BIN/.place.XXXXXX")
cp "$built" "$tmp"
# mktemp creates the file 0600 and cp keeps that mode on an existing file.
chmod 0755 "$tmp"
# The production signing policy: production modules run with the hardened
# runtime, and basal's worker gate refuses a worker without it, so the rig
# matches both.
sign_hardened "$tmp" "$identifier" 2>/dev/null
if ! verify_hardened "$tmp" "$identifier"; then
rm -f "$tmp"
die "$name: the signed copy fails the signing policy"
fi
mv -f "$tmp" "$dest"
verify_hardened "$dest" "$identifier" || die "$name: $dest fails the signing policy"
say "placed $dest ($identifier)"
}
# place_staged <name> <staged file> <dest> <identifier>: copy a staged binary
# into the rig unchanged (never re-signed: the rig runs the bytes production
# placement would install), and check at the final path that the bytes match the stage's
# sidecar and that the signature passes under its production identifier.
place_staged() {
name=$1
staged=$2
dest=$3
identifier=$4
guard_path "$dest"
if [ "$DRY" = 1 ]; then
say "+ cp $staged $BIN/.place.XXXXXX; chmod 0755 $BIN/.place.XXXXXX; mv -f $BIN/.place.XXXXXX $dest"
say "+ sha256 of $dest must equal $staged.sha256; verify_hardened $dest $identifier"
return
fi
tmp=$(mktemp "$BIN/.place.XXXXXX")
cp "$staged" "$tmp"
chmod 0755 "$tmp"
want=$(awk '{ print $1 }' "$staged.sha256")
got=$(shasum -a 256 "$tmp" | awk '{ print $1 }')
if [ "$got" != "$want" ] || ! verify_hardened "$tmp" "$identifier"; then
rm -f "$tmp"
die "$name: staged copy fails its digest or signing policy"
fi
mv -f "$tmp" "$dest"
say "placed $dest from the stage ($identifier, sha256 $got)"
}
# ---------------------------------------------------------------- config
cmd_config() (
[ $# -eq 0 ] || usage
guard_port_free
[ -z "$(daemon_pid)" ] || die "refusing: the rig daemon is running; stop it first"
# From scratch: the whole file is generated here, never merged with a
# previous one. ck-bus and nats-server are left out (the bus needs its own
# NATS server, operator JWT and vault signing); prefrontal-core then
# reports "bus: connecting" in its health, which is expected on this rig.
write_file "$SUBC_CONFIG" <<EOF
{
// ckdev-flows: basal's isolated rig, written by script/flows-rig.sh config.
// Regenerate it with that command rather than editing it by hand.
"version": 1,
"port": $PORT,
"storage": {
"backend": "sqlite",
// Every module's store is <data_home>/cortexkit/<module id>/store.db.
"data_home": "$DATA_HOME"
},
"modules": {
// The credentials vault, keyed by a master key file of the rig's
// own (never the macOS keychain, never a production key).
"claustrum": {
"program": "$BIN/ckdev-claustrum",
"args": [],
"env": { "CK_MASTER_KEY_PATH": "$VAULT_KEY" },
"enabled": true,
"reserved": true,
"launch_nonce_env": false
},
"broca": {
"program": "$BIN/ckdev-broca",
"args": [],
"env": { "BROCA_STATE_ROOT": "$DATA_HOME/cortexkit/broca" },
"enabled": true,
"reserved": true,
"launch_nonce_env": false
},
// The daemon supplies data/cortexkit/fusiform/store.db to this module.
"fusiform": {
"program": "$BIN/ckdev-fusiform",
"args": [],
"env": {},
"enabled": true
},
// The project-identity/v1 provider. prefrontal-core declares that
// capability required, and the daemon refuses every route to core until
// a provider has registered. Its store is data/cortexkit/entorhinal/.
"entorhinal": {
"program": "$BIN/ckdev-entorhinal",
"args": [],
"env": {},
"enabled": true
},
// Core discovers this real consent/v1 provider for flow and package cards.
"cingulate": {
"program": "$BIN/ckdev-cingulate",
"args": [],
"env": {},
"enabled": true,
"reserved": true
},
// Core runs its projects registry consumer against the rig's entorhinal,
// as in production.
// Never set PREFRONTAL_CORE_DIAGNOSTICS here: it opens core's test seams,
// which exist for prefrontal's end-to-end harness only.
"prefrontal-core": {
"program": "$BIN/ckdev-prefrontal-core",
"args": [],
"env": {},
"enabled": true,
"reserved": true,
"launch_nonce_env": false
},
"prefrontal-routing": {
"program": "$BIN/ckdev-prefrontal-routing",
"args": [],
"env": {},
"enabled": true,
"reserved": true,
"launch_nonce_env": false
},
// Reserved, so its routes carry the principal reserved:basal. The rig's
// build reads the two arming files for crash and unscoped-send checks;
// production ck-basal has neither switch and ignores both variables.
"basal": {
"program": "$BIN/ckdev-basal",
"args": [],
"env": { "BASAL_RIG_KILL_FILE": "$KILL_FILE", "BASAL_RIG_UNSCOPED_FILE": "$UNSCOPED_FILE" },
"enabled": true,
"reserved": true
},
// Rig only, never in a production config: a stub under the reserved id
// callosum, which core and basal both treat as the operator. It answers
// the contract suite's consent cards (crates/basal-rig).
"callosum": {
"program": "$BIN/ckdev-callosum",
"args": [],
"env": {},
"enabled": true,
"reserved": true
},
// Rig only: advertises agent-run-scopes/v1 and echoes daemon-stamped identity.
"ckdev-runscope-probe": {
"program": "$BIN/ckdev-runscope-probe",
"args": [],
"env": {},
"enabled": true
}
}
}
EOF
run mkdir -p -m 700 "$RIG_HOME" "$RUNTIME_DIR/tmp" "$(dirname "$VAULT_KEY")"
rig_auth bootstrap
if [ "$DRY" = 1 ]; then
say "+ start the temporary rig for authenticated vault grants and its served catalog"
else