-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathTaskfile.yaml
More file actions
712 lines (649 loc) · 32.8 KB
/
Copy pathTaskfile.yaml
File metadata and controls
712 lines (649 loc) · 32.8 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
version: "3"
vars:
PROJECT: thump
REGISTRY: '{{.REGISTRY | default "ghcr.io/ianeff"}}'
BEATS: "clank rattle hiss thump bootstrap"
# The four long-running services that actually run as cluster pods.
SERVICE_BEATS: "clank rattle hiss thump"
# bootstrap (R6c) is neither: it's a one-shot Kubernetes Job, not a
# long-running service, but unlike calipers it runs in-cluster and needs a
# container image. IMAGE_BEATS is the union that actually gets built/
# pushed/signed; SERVICE_BEATS keeps its own meaning of "has a Deployment."
JOB_BEATS: "bootstrap"
IMAGE_BEATS: "{{.SERVICE_BEATS}} {{.JOB_BEATS}}"
# calipers is the operator CLI (incidents/approve/force/unseal/corpus/rca/
# tune/replay/harvest/probe/transcript/scorecard/validate) — no internal/beat runtime, no otelc compile-time
# instrumentation, no Helm Deployment, ships only as a binary
# (build-calipers/goreleaser), never as a container image. Kept out of
# BEATS because it isn't otelc-instrumented like the other five.
CALIPERS: "calipers"
VERSION:
sh: git describe --tags --always --dirty 2>/dev/null || echo dev
COMMIT:
sh: git rev-parse HEAD 2>/dev/null || echo none
# Pinned to the commit's own timestamp, not wall-clock — two builds of the
# same commit must produce the same binary (and therefore the same image
# digest), or images/sign-images silently drift the :{{.COMMIT}} tag out
# from under whatever was already signed. Falls back to wall-clock only
# outside a git checkout (e.g. a source tarball with no .git).
DATE:
sh: git show -s --format=%cd --date=format:'%Y-%m-%dT%H:%M:%SZ' HEAD 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%SZ
LDFLAGS: "-s -w -X main.version={{.VERSION}} -X main.commit={{.COMMIT}} -X main.date={{.DATE}}"
tasks:
default:
desc: Alias for ci
cmds:
- task: ci
doctor:
desc: Check local development prerequisites, CLI tools, and runtime allocations
cmds:
- bash scripts/doctor.sh
ci:
desc: Full local CI — fmt-check, vet, lint, vulncheck, chart-lint, promql, validate:config, race, build
cmds:
- task: otelc:check-gomod
- task: fmt-check
- task: vet
- task: lint
- task: vulncheck
- task: chart-lint
- task: test:promql
- task: validate:config
- task: race
- task: integration
- task: build
validate:config:
desc: "Validate profile configs (actions, policy, whir, rattle): task validate:config [PROFILE=dev|thump-test|acme|all]"
cmds:
- go run ./cmd/calipers validate --profile={{.PROFILE | default "all"}}
fmt:
desc: gofmt the tree
cmds:
- go fmt ./...
fmt-check:
desc: Fail if any file is not gofmt-clean
cmds:
- |
out=$(gofmt -l .)
if [ -n "$out" ]; then
echo "unformatted files:"
echo "$out"
echo "run: task fmt"
exit 1
fi
vet:
desc: go vet ./...
cmds:
- go vet ./...
lint:
desc: golangci-lint run
cmds:
- golangci-lint run
vulncheck:
desc: govulncheck over deps
cmds:
- go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
chart-lint:
desc: helm template | kubeconform, strict
cmds:
- |
helm template deploy/chart/thump | go run github.com/yannh/kubeconform/cmd/kubeconform@v0.8.0 -strict -summary \
-schema-location default \
-schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{"{{"}}.Group{{"}}"}}/{{"{{"}}.ResourceKind{{"}}"}}_{{"{{"}}.ResourceAPIVersion{{"}}"}}.json'
# helm template never renders crds/ (same as helm upgrade — Helm's own
# documented behavior), so the CRD is linted directly here. -ignore-missing-schemas
# because yannh/kubernetes-json-schema's default catalog has no schema
# for the bare CustomResourceDefinition kind itself (verified: it carries
# customresourcedefinitioncondition/-names/-status but not the top-level
# object) — this still catches real YAML/JSON errors, just skips the
# kind it has nothing to check against.
- |
go run github.com/yannh/kubeconform/cmd/kubeconform@v0.8.0 -strict -summary -ignore-missing-schemas \
-schema-location default \
deploy/chart/thump/crds/approvalrequest.yaml
# Every value-gated template renders to nothing under the defaults, so
# the pass above validates none of them. The admission policies are the
# authority boundary on the ApprovalRequest surface — a typo in one
# fails open — so the on-state is rendered and checked too.
# -ignore-missing-schemas: MutatingAdmissionPolicy is newer than the
# schema catalog, and the ValidatingAdmissionPolicy pair is covered by
# internal/hiss/chart_test.go's assertions on the rendered output.
- |
helm template deploy/chart/thump \
--set approvalRequests.enabled=true \
--set 'approvalRequests.approvers[0].kind=User' \
--set 'approvalRequests.approvers[0].name=someone@example.com' \
--set 'approvalRequests.approvers[0].apiGroup=rbac.authorization.k8s.io' \
| go run github.com/yannh/kubeconform/cmd/kubeconform@v0.8.0 -strict -summary -ignore-missing-schemas \
-schema-location default \
-schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{"{{"}}.Group{{"}}"}}/{{"{{"}}.ResourceKind{{"}}"}}_{{"{{"}}.ResourceAPIVersion{{"}}"}}.json'
# rbac-harvest-{argocd,chaosmesh,oteldemo}.yaml all gate on
# .Values.harvest.enabled and .Values.harvest.operators, so — same
# reasoning as the approvalRequests.enabled=true render above — every
# prior pass validated none of them. argocd.enabled defaults to true,
# so this one render exercises all three.
- |
helm template deploy/chart/thump \
--set harvest.enabled=true \
--set 'harvest.operators[0].kind=User' \
--set 'harvest.operators[0].name=someone@example.com' \
--set 'harvest.operators[0].apiGroup=rbac.authorization.k8s.io' \
| go run github.com/yannh/kubeconform/cmd/kubeconform@v0.8.0 -strict -summary -ignore-missing-schemas \
-schema-location default \
-schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{"{{"}}.Group{{"}}"}}/{{"{{"}}.ResourceKind{{"}}"}}_{{"{{"}}.ResourceAPIVersion{{"}}"}}.json'
# deploy/tilt-values-dev.yaml (docs/dev-environment.md) exercises a
# values shape none of the passes above do: configProfile: dev pulls
# config/dev/ through configmap-{whir,rattle}.yaml's .Files.Get, and
# domains.otelDemo.enabled=true + argocd.enabled=false renders RBAC
# combinations the chart's plain default never touches.
- |
helm template deploy/chart/thump --values deploy/tilt-values-dev.yaml --set image.tag=dev \
| go run github.com/yannh/kubeconform/cmd/kubeconform@v0.8.0 -strict -summary -ignore-missing-schemas \
-schema-location default \
-schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{"{{"}}.Group{{"}}"}}/{{"{{"}}.ResourceKind{{"}}"}}_{{"{{"}}.ResourceAPIVersion{{"}}"}}.json'
test:promql:
desc: promtool unit tests against the SLO recording rules (offline — needs promtool and yq on PATH)
cmds:
- mkdir -p bin
# slo-rules.yaml is a PrometheusRule CRD manifest; promtool only
# understands the plain `groups:` shape underneath the CRD envelope.
- yq eval '.spec' deploy/dev/manifests/slo-rules.yaml > bin/slo-rules.rules.yaml
- promtool test rules deploy/dev/manifests/slo-rules_test.yaml
test:
desc: go test -shuffle=on ./...
cmds:
- go test -shuffle=on ./...
race:
desc: go test -race ./...
cmds:
- go test -race ./...
coverage:
desc: coverage profile + total
cmds:
- go test -coverprofile=coverage.out ./...
- go tool cover -func=coverage.out | tail -1
bench:
desc: Run all benchmarks with allocation profiling and benchstat count
cmds:
- go test -run=^$ -bench=. -benchmem -count=6 ./...
integration:
desc: Run hermetic integration test suite (nothin' live)
cmds:
- go test -count=1 ./test/integration
# eval is key-gated (ANTHROPIC_API_KEY) and NEVER part of ci — a real-model
# assertion is exactly the flakiness `task ci` exists to keep out. Missing
# key = a clean skip, not a failure, so this is always safe to run.
eval:
desc: Run the reasoner eval against the production catalog (key-gated, not part of ci)
cmds:
- go test -tags eval ./internal/clank -run TestEval_ReasonerAgainstProductionCatalog -v
rca:
desc: "Run the graded RCA suite — did the reasoner cite the right evidence, not just reach the right verdict? task rca [RIG=thump-test] [RUNS=1] [FLOOR=0.75] [MODEL=haiku|sonnet|gemini-low] [ROW=substring] (key-gated, not part of ci)"
vars:
RIG: '{{.RIG | default "thump-test"}}'
RUNS: '{{.RUNS | default "1"}}'
FLOOR: '{{.FLOOR | default "0.75"}}'
MODEL: '{{.MODEL | default "haiku"}}'
cmds:
- go run ./cmd/calipers rca -rig {{.RIG}} -runs {{.RUNS}} -floor {{.FLOOR}} -model {{.MODEL}} {{if .ROW}}-row "{{.ROW}}"{{end}}
corpus:
desc: Mine the sealed WAL into testdata/corpus/ — every (Confidence, FailureClass, Result) triple this engine has produced (needs S3_*/THUMP_SEAL_KEY; rig-free, object-store creds only)
cmds:
- go run ./cmd/calipers corpus
capture-detection:
desc: "Farm a live detection into a fixture: task capture-detection SRC=<path to .yaml> NAME=<fixture-name>"
requires:
vars: [SRC, NAME]
cmds:
- cp "{{.SRC}}" internal/clank/testdata/detections/{{.NAME}}.yaml
- echo "captured internal/clank/testdata/detections/{{.NAME}}.yaml"
- echo "-> add a row to evalTable() in internal/clank/eval_test.go if it belongs in the eval score"
probe:
desc: "Fire N real, read-only reasoning runs against a captured detection: task probe DETECTION=<path> [RUNS=N] (key-gated, not part of ci)"
requires:
vars: [DETECTION]
vars:
RUNS: '{{.RUNS | default "1"}}'
cmds:
- go run ./cmd/calipers probe -detection {{.DETECTION}} -runs {{.RUNS}}
transcript:
desc: "Export one live run's sealed transcript + proposal.Set into a replay pair: task transcript RUN_ID=<fingerprint/unixnano> OUT=<dir> (needs S3_*/THUMP_SEAL_KEY; rig-free, object-store creds only)"
requires:
vars: [RUN_ID, OUT]
cmds:
- go run ./cmd/calipers transcript -run-id {{.RUN_ID}} -out {{.OUT}}
calibrate:
desc: "Chain transcript export -> corpus mine -> tune sweep, so nobody re-derives the sequence: task calibrate OUT=<dir> (needs S3_*/THUMP_SEAL_KEY)"
requires:
vars: [OUT]
cmds:
- go run ./cmd/calipers transcript -all -out {{.OUT}}
- go run ./cmd/calipers corpus
- go run ./cmd/calipers tune -transcripts {{.OUT}}
# .git/hooks/ isn't version-controlled, so a fresh clone has no protection
# until this runs once. Re-run any time after cloning or after a hook changes.
hooks:install:
desc: Install repo git hooks (currently just the otelc go.mod guard)
cmds:
- cp .githooks/pre-commit .git/hooks/pre-commit
- chmod +x .git/hooks/pre-commit
# otelc go.mod/go.sum/otelc.runtime.go persistence is upstream-unsupported —
# see https://github.com/open-telemetry/opentelemetry-go-compile-instrumentation/issues/585.
# Only the fully-ephemeral path works: no otel.instrumentation.go, no otelc
# requires/replaces in go.mod, ever. Never run `otelc pin` or `otelc setup`
# standalone in this repo, and never commit anything otelc writes — `task
# build` (plain `go tool otelc go build`) is the only supported entry point.
# This task clears every leftover artifact those forbidden paths leave
# behind: cmd/*/otelc.runtime.go (stale generated imports survive a bare
# `rm -rf .otelc-build` and feed wrong package lists into the next build)
# plus `otelc cleanup`'s own state. Safe any time — .gitignore keeps all of
# it out of git, so there's nothing here to lose.
otelc:reset:
desc: Clear otelc build state left behind by an interrupted or forbidden-path build
cmds:
- rm -f cmd/*/otelc.runtime.go otel.instrumentation.go
- go tool otelc cleanup
# Mechanical fix for otelc:check-gomod's failure: strips every otelc
# submodule require ("go.opentelemetry.io/otelc/...") and every otelc
# replace directive from go.mod, leaving only the one permanent line
# (the bare "go.opentelemetry.io/otelc" tool require) plus the "tool"
# directive. Verified byte-for-byte against the last known-good go.mod
# on 2026-08-10 — this is a real fix, not a description of one.
otelc:fix-gomod:
desc: Strip committed otelc build-transient lines from go.mod back to the clean baseline
cmds:
- |
awk '
/^replace go\.opentelemetry\.io\/otelc\// { next }
/^\tgo\.opentelemetry\.io\/otelc\// { next }
{ print }
' go.mod | cat -s | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}' > go.mod.fixed
mv go.mod.fixed go.mod
- task: otelc:check-gomod
- echo "go.mod stripped clean. Run 'task build' once to confirm it round-trips."
# go.mod must never carry otelc's build-transient replace directives — otelc
# adds them per build and reverts them (see .gitignore). If a build gets
# killed mid-commit (or a commit races a running build) they can end up
# checked in, pointing at this machine's absolute .otelc-build/ path; a fresh
# checkout then fails deep inside otelc's own internal `go mod tidy`, since
# that path never existed there. Catch it here, fast and legibly, instead of
# 15 minutes into a CI run.
otelc:check-gomod:
desc: Fail fast if go.mod carries committed otelc build-transient replace directives
cmds:
- |
if grep -q '\.otelc-build' go.mod; then
echo "go.mod carries otelc replace directives pointing into .otelc-build/ — these must never be committed (see .gitignore)."
echo "An interrupted (or forbidden-path — see otelc:reset) 'otelc go build' left go.mod dirty."
echo "Fix: run 'task otelc:fix-gomod', then 'task build' once to confirm it round-trips clean."
exit 1
fi
build:
desc: Build the four beats and bootstrap (otelc-instrumented) plus calipers (plain) to bin/
cmds:
- mkdir -p bin
- for beat in {{.BEATS}}; do go tool otelc go build -ldflags "{{.LDFLAGS}}" -o bin/$beat ./cmd/$beat || exit 1; done
- go build -ldflags "{{.LDFLAGS}}" -o bin/calipers ./cmd/calipers
# images builds one container per beat, tagged with the git SHA (never
# `latest` — mutable tags break GitOps drift detection), with an SBOM and
# SLSA provenance attestation attached to the manifest list via buildx's
# own scanner — no separate syft invocation needed for the image path.
# Requires a buildx builder using the docker-container driver (the default
# docker driver can't emit multi-platform manifests or attestations).
# --metadata-file records the pushed manifest-list digest per beat — the
# thing sign-images actually signs, never the mutable tag (Liz Rice's rule:
# pin and verify by digest, a tag is a pointer, not an identity).
# Override the destination with `task images REGISTRY=ghcr.io/whoever`.
images:
desc: Multi-arch (linux/amd64,linux/arm64) container per service, SBOM + provenance, pushed to REGISTRY
cmds:
- mkdir -p bin/metadata
- |
for beat in {{.IMAGE_BEATS}}; do
echo "building {{.REGISTRY}}/thump-$beat:{{.COMMIT}} (linux/amd64,linux/arm64)"
docker buildx build \
--platform linux/amd64,linux/arm64 \
--sbom=true \
--provenance=true \
--build-arg BEAT=$beat \
--build-arg VERSION={{.VERSION}} \
--build-arg COMMIT={{.COMMIT}} \
--build-arg DATE={{.DATE}} \
-t {{.REGISTRY}}/thump-$beat:{{.COMMIT}} \
--metadata-file bin/metadata/$beat.json \
--push \
. || exit 1
done
push-images:
desc: "Alias: multi-arch manifests are already pushed by images (buildx --push)"
deps: [images]
cmds:
- echo "multi-arch manifests already pushed by 'images' (buildx --push) — nothing more to do"
# sign-images keyless-signs (Sigstore/Fulcio OIDC, no long-lived key) the
# exact digest images just pushed, read back from its --metadata-file —
# never the :{{.COMMIT}} tag, which is only a pointer to that digest, not
# the digest itself. Doesn't touch the source tree or the pushed manifest;
# purely additive on the registry side. By default also submits a public
# Rekor transparency-log entry — decide on that deliberately, don't just
# discover it later. Fulcio is a shared public service outside this repo's
# control; 120s bounds the wait (OIDC login + cert issuance) so a stall
# there fails loud instead of hanging the terminal forever.
sign-images:
desc: Keyless (Sigstore/Fulcio) cosign signature over each image images just pushed
deps: [images]
cmds:
- |
for beat in {{.IMAGE_BEATS}}; do
digest=$(jq -r '."containerimage.digest"' bin/metadata/$beat.json)
echo "signing {{.REGISTRY}}/thump-$beat@$digest"
timeout 120 go run github.com/sigstore/cosign/v2/cmd/cosign@latest sign --yes \
{{.REGISTRY}}/thump-$beat@$digest || exit 1
done
# sbom-binaries/sign-binaries cover the non-container path: bin/ output
# isn't shipped anywhere today (images is the real delivery surface), kept
# here so the tooling exists once thump grows a bare-binary release channel.
sbom-binaries:
desc: SBOM for the bin/ outputs
deps: [build]
cmds:
- mkdir -p bin/sbom
- |
for beat in {{.BEATS}} {{.CALIPERS}}; do
go run github.com/anchore/syft/cmd/syft@latest bin/$beat -o spdx-json=bin/sbom/$beat.sbom.json
done
sign-binaries:
desc: Keyless blob-signature for the bin/ outputs
deps: [sbom-binaries]
cmds:
- |
for beat in {{.BEATS}} {{.CALIPERS}}; do
go run github.com/sigstore/cosign/v2/cmd/cosign@latest sign-blob --yes \
--output-signature=bin/sbom/$beat.sig \
--output-certificate=bin/sbom/$beat.pem \
bin/$beat
done
run:clank:
desc: go run ./cmd/clank
cmds:
- go run ./cmd/clank
run:rattle:
desc: go run ./cmd/rattle
cmds:
- go run ./cmd/rattle
run:hiss:
desc: go run ./cmd/hiss
cmds:
- go run ./cmd/hiss
run:thump:
desc: go run ./cmd/thump
cmds:
- go run ./cmd/thump
run:calipers:
desc: go run ./cmd/calipers
cmds:
- go run ./cmd/calipers
# docs/dev-environment.md — the fully-local dev environment. dev:cluster is
# the one piece Tilt itself can't do: `allow_k8s_contexts()` and `helm()`
# both evaluate at Tiltfile load, so the k3d context has to exist before
# `tilt up` runs at all. dev:up does both steps in the order Ian's other
# profiles get for free from an out-of-band rig repo.
#
# deploy/dev/k3d.yaml sets switchCurrentContext: false (so creating this
# cluster never clobbers whatever context the Mac's other sessions are on)
# — but Tilt's own k8s_resource() apply path has no --context equivalent,
# it resolves the cluster purely from kubeconfig's ambient current-context.
# Every local_resource in the Tiltfile pins --context explicitly and
# doesn't care, but k8s_resource()-managed things (nats, clank, hiss,
# rattle, thump, ...) sat on "waiting-for-cluster" forever the one time
# current-context drifted to an unrelated stub. Switch it explicitly here,
# every run, so tilt up always has a live connection to attach to.
dev:cluster:
desc: Create (idempotently) the local k3d cluster deploy/dev/k3d.yaml describes
preconditions:
- sh: command -v k3d >/dev/null
msg: "k3d not found on PATH — install it before running dev:cluster"
- sh: command -v helm >/dev/null
msg: "helm not found on PATH — install it before running dev:cluster"
- sh: command -v tilt >/dev/null
msg: "tilt not found on PATH — install it before running dev:cluster"
- sh: command -v kubectl >/dev/null
msg: "kubectl not found on PATH — install it before running dev:cluster"
- sh: docker info >/dev/null 2>&1
msg: "Docker is not running — start Docker before running dev:cluster"
cmds:
- |
# k3d cluster list's existence check says nothing about whether the
# cluster is actually running — after a laptop reboot it exists and
# is stopped, and everything downstream (tilt up's guarded() preflight)
# burns its full 60s budget against a dead API server before printing
# advice to run the task that just succeeded. serversRunning==0 with
# the cluster present means "start it", not "create it".
running=$(k3d cluster list thump-dev -o json 2>/dev/null | \
python3 -c 'import json,sys; d=json.load(sys.stdin); print(d[0]["serversRunning"] if d else -1)' 2>/dev/null || echo -1)
if [ "$running" = "-1" ]; then
k3d cluster create --config deploy/dev/k3d.yaml
elif [ "$running" = "0" ]; then
echo "thump-dev cluster exists but is stopped — starting it" >&2
k3d cluster start thump-dev
fi
- kubectl config use-context k3d-thump-dev
dev:up:
desc: Bring up the full local thump dev environment (cluster + substrate + all five beats)
cmds:
- task: dev:cluster
- tilt up -- --cluster=dev
dev:down:
desc: Tear down the local k3d dev cluster entirely (irreversible — see docs/dev-environment.md before running)
cmds:
- k3d cluster delete thump-dev
dev:gate-on:
desc: Freeze the dev cluster so every detection escalates to a human
cmds:
- |
# Reads from config/dev/hiss/policy.yaml, not the live ConfigMap: a
# second run used to read back its own already-mutated ConfigMap and
# append a SECOND freezeWindows: key to the same YAML — a duplicate
# mapping key, not idempotent. Starting from the checked-in file makes
# gate-on itself idempotent regardless of how many times it's run.
end_ts=$(date -u -v+4H +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '+4 hours' +%Y-%m-%dT%H:%M:%SZ)
cp config/dev/hiss/policy.yaml /tmp/policy.yaml
printf 'freezeWindows:\n - name: dev-demo\n start: %s\n end: %s\n' \
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
"$end_ts" >> /tmp/policy.yaml
kubectl --context k3d-thump-dev -n thump create cm thump-hiss \
--from-file=policy.yaml=/tmp/policy.yaml --dry-run=client -o yaml | kubectl apply -f -
kubectl --context k3d-thump-dev -n thump rollout restart deploy/hiss
dev:gate-off:
desc: Un-freeze the dev cluster so detections resume auto-approving
cmds:
- |
kubectl --context k3d-thump-dev -n thump create cm thump-hiss \
--from-file=policy.yaml=config/dev/hiss/policy.yaml --dry-run=client -o yaml | kubectl apply -f -
kubectl --context k3d-thump-dev -n thump rollout restart deploy/hiss
dev:certs:
desc: Extract NATS client/CA TLS certificates from dev cluster into bin/certs/
cmds:
- mkdir -p bin/certs
- kubectl --context k3d-thump-dev -n thump get secret nats-tls -o jsonpath='{.data.ca\.crt}' | base64 -d > bin/certs/ca.crt
- kubectl --context k3d-thump-dev -n thump get secret calipers-tls -o jsonpath='{.data.tls\.crt}' | base64 -d > bin/certs/tls.crt
- kubectl --context k3d-thump-dev -n thump get secret calipers-tls -o jsonpath='{.data.tls\.key}' | base64 -d > bin/certs/tls.key
- echo "NATS TLS certs written to bin/certs/"
dev:incidents:
desc: List incidents from dev cluster over NATS port-forward (requires task dev:certs)
cmds:
- go run ./cmd/calipers incidents --nats-url tls://localhost:4222 --tls-cert bin/certs/tls.crt --tls-key bin/certs/tls.key --tls-ca bin/certs/ca.crt --server-name nats.thump.svc
dev:approve:
desc: "Approve a held incident by fingerprint: task dev:approve FP=<fp> (requires task dev:certs)"
requires:
vars: [FP]
cmds:
- go run ./cmd/calipers approve {{.FP}} --nats-url tls://localhost:4222 --tls-cert bin/certs/tls.crt --tls-key bin/certs/tls.key --tls-ca bin/certs/ca.crt --server-name nats.thump.svc
dev:govern:
desc: >-
Run one proposal.Set through hiss (locally, offline) then thump (a
one-shot in-cluster Job, real RBAC) and print the verdict + outcome:
task dev:govern SET=<path to a proposal.Set yaml>. Never touches
thump.proposals/thump.decisions/thump.outcomes or any WAL — hiss's leg
runs against scratch local dirs, thump's leg runs in its own scratch
emptyDirs inside the cluster. Only for the dev cluster's offline
dir-poll beats; never point this at a broker-mode deployment.
requires:
vars: [SET]
vars:
RUN_ID:
sh: date +%s
cmds:
- mkdir -p bin/govern/{{.RUN_ID}}/hiss-inbox bin/govern/{{.RUN_ID}}/hiss-outbox
- cp {{.SET}} bin/govern/{{.RUN_ID}}/hiss-inbox/set.yaml
- HISS_POLICY=config/dev/hiss/policy.yaml
HISS_INBOX=bin/govern/{{.RUN_ID}}/hiss-inbox
HISS_OUTBOX=bin/govern/{{.RUN_ID}}/hiss-outbox
go run ./cmd/hiss --once
- echo "--- hiss verdict ---"
- cat bin/govern/{{.RUN_ID}}/hiss-outbox/*.yaml
- |
GOVERNED=$(ls bin/govern/{{.RUN_ID}}/hiss-outbox/*.yaml | head -1)
kubectl --context k3d-thump-dev -n thump create configmap dev-govern-input-{{.RUN_ID}} --from-file=governed.yaml="$GOVERNED"
- |
THUMP_IMAGE=$(kubectl --context k3d-thump-dev -n thump get deployment thump -o jsonpath='{.spec.template.spec.containers[0].image}')
PROM_URL=$(kubectl --context k3d-thump-dev -n thump get deployment thump -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="PROM_URL")].value}')
RUN_ID={{.RUN_ID}} THUMP_IMAGE="$THUMP_IMAGE" THUMP_EXECUTOR="{{.THUMP_EXECUTOR | default "live"}}" PROM_URL="$PROM_URL" \
INPUT_CONFIGMAP=dev-govern-input-{{.RUN_ID}} \
envsubst < deploy/dev/manifests/dev-govern-job.yaml | kubectl --context k3d-thump-dev apply -f -
- kubectl --context k3d-thump-dev -n thump wait --for=condition=complete --for=condition=failed --timeout=180s job/dev-govern-{{.RUN_ID}}
- echo "--- thump outcome ---"
- kubectl --context k3d-thump-dev -n thump logs job/dev-govern-{{.RUN_ID}}
- kubectl --context k3d-thump-dev -n thump delete job dev-govern-{{.RUN_ID}} configmap dev-govern-input-{{.RUN_ID}} --ignore-not-found
- rm -rf bin/govern/{{.RUN_ID}}
dev:probe:
desc: >-
Fire N real clank reasoning runs against a captured detection, straight
from a laptop: task dev:probe DETECTION=<path> [RUNS=5] [FLOOR=0.75]
[MODEL=haiku|sonnet|gemini-low]. Read-only against the dev cluster by
construction — clank.ProbeEngine never wires a Pub or a Journal, so
nothing here reaches thump.proposals or the corpus. Requires Tilt's
prometheus-ui and loki-port-forward resources up (PROM_URL/LOKI_URL
below dial their local ports), and ANTHROPIC_API_KEY (haiku/sonnet) or
GEMINI_API_KEY (gemini-low) set in the shell for whichever MODEL is
selected.
requires:
vars: [DETECTION]
cmds:
- |
PROM_URL=http://localhost:9090 \
LOKI_URL=http://localhost:3100 \
EVIDENCE_QUERIES=config/dev/whir/evidence-queries.yaml \
WHIR_CATALOG=config/dev/whir/catalog-info.yaml \
WHIR_STATE_QUERIES=config/dev/whir/state-queries.yaml \
ACTION_CATALOG=config/dev/actions/catalog.yaml \
FAILURE_CLASSES=config/dev/actions/failure-classes.yaml \
CLANK_WEIGHTS=config/clank/weights.yaml \
CLANK_LIMITS=config/clank/limits.yaml \
go run ./cmd/calipers probe -detection {{.DETECTION}} -runs {{.RUNS | default 1}} -floor {{.FLOOR | default 0.75}} -model {{.MODEL | default "haiku"}}
harvest:dev:
desc: "Run a harvest scenario against dev: task harvest:dev ROW=cart-failure [REPEAT=1] (requires task dev:certs, Tilt's 4222 forward up)"
requires:
vars: [ROW]
cmds:
- mkdir -p bin/harvest
- |
go run ./cmd/calipers harvest \
--scenarios chaos/scenarios-dev.yaml \
--kube-context k3d-thump-dev \
--nats-url tls://localhost:4222 \
--tls-cert bin/certs/tls.crt --tls-key bin/certs/tls.key --tls-ca bin/certs/ca.crt \
--server-name nats.thump.svc \
--row "{{.ROW}}" --repeat "{{.REPEAT | default 1}}" \
--json | tee bin/harvest/$(date +%s).jsonl
# dev's S3 is in-cluster s3mock, unreachable from the laptop at the
# endpoint its own secret advertises — port-forward and override. Local
# 9091, not 9090: Tilt already forwards Prometheus there.
dev:transcript:
desc: "Export and unseal one run's transcript: task dev:transcript RUN=<run_id>"
requires:
vars: [RUN]
cmds:
- |
set -euo pipefail
kubectl --context k3d-thump-dev -n thump port-forward svc/s3mock 9091:9090 >/dev/null 2>&1 &
PF_PID=$!
trap 'kill $PF_PID 2>/dev/null' EXIT
sleep 1
export THUMP_SEAL_KEY=$(kubectl --context k3d-thump-dev -n thump get secret thump-seal -o jsonpath='{.data.key}' | base64 -d)
export S3_ENDPOINT=http://localhost:9091
export S3_BUCKET=thump-wal
export S3_ACCESS_KEY=$(kubectl --context k3d-thump-dev -n thump get secret thump-s3 -o jsonpath='{.data.access-key}' | base64 -d)
export S3_SECRET_KEY=$(kubectl --context k3d-thump-dev -n thump get secret thump-s3 -o jsonpath='{.data.secret-key}' | base64 -d)
go run ./cmd/calipers transcript -run-id "{{.RUN}}" -out "bin/transcripts/{{.RUN}}"
dev:transcripts:
desc: "Export and unseal every run's transcript: task dev:transcripts"
cmds:
- |
set -euo pipefail
kubectl --context k3d-thump-dev -n thump port-forward svc/s3mock 9091:9090 >/dev/null 2>&1 &
PF_PID=$!
trap 'kill $PF_PID 2>/dev/null' EXIT
sleep 1
export THUMP_SEAL_KEY=$(kubectl --context k3d-thump-dev -n thump get secret thump-seal -o jsonpath='{.data.key}' | base64 -d)
export S3_ENDPOINT=http://localhost:9091
export S3_BUCKET=thump-wal
export S3_ACCESS_KEY=$(kubectl --context k3d-thump-dev -n thump get secret thump-s3 -o jsonpath='{.data.access-key}' | base64 -d)
export S3_SECRET_KEY=$(kubectl --context k3d-thump-dev -n thump get secret thump-s3 -o jsonpath='{.data.secret-key}' | base64 -d)
go run ./cmd/calipers transcript -all -out bin/transcripts
# The bucket pull stays the operator's job: which object store, which
# credentials, and which segments are worth reading are all site facts, and
# a tool that fetched for you would need cluster reach it has no business
# having.
unseal:
desc: "Decrypt sealed WAL segments pulled from the bucket: task unseal SEG=<path>... (needs THUMP_SEAL_KEY)"
requires:
vars: [SEG]
cmds:
- go run ./cmd/calipers unseal {{.SEG}}
seal-key:
desc: "Print THUMP_SEAL_KEY from the cluster, both base64 layers handled — export it before task unseal"
cmds:
- kubectl get secret thump-seal -n thump -o jsonpath='{.data.key}' | base64 -d
tidy:
desc: go mod tidy
cmds:
- go mod tidy
clean:
desc: Remove bin/ and coverage.out
cmds:
- rm -rf bin/ coverage.out
stats:
desc: Display repo statistics.
cmds:
- |
prod_lines=$(git ls-files '*.go' | grep -v '_test.go$' | xargs cloc --quiet --csv 2>/dev/null | awk -F, 'END {print $5}')
prod_files=$(git ls-files '*.go' | grep -v '_test.go$' | wc -l | xargs)
test_lines=$(git ls-files '*_test.go' | xargs cloc --quiet --csv 2>/dev/null | awk -F, 'END {print $5}')
test_files=$(git ls-files '*_test.go' | wc -l | xargs)
ratio=$(awk -v t="$test_lines" -v p="$prod_lines" 'BEGIN {printf "%.2f", t/p}')
echo "📊 thump Code Pulse"
echo "=========================================="
echo "📦 Production Go : ${prod_lines} lines across ${prod_files} files"
echo "🧪 Test Suite : ${test_lines} lines across ${test_files} files"
echo "⚡ Test/Code Ratio: ${ratio}x test lines per prod line"
echo "=========================================="
chaos:preflight:
desc: Run preflight checks before launching live chaos scenarios
cmds:
- ./chaos/preflight.sh
chaos:cart-failure:
desc: Inject cartFailure fault into otel-demo flagd
cmds:
- ./chaos/flagd-cart-failure.sh inject
chaos:cart-restore:
desc: Restore cartFailure fault in otel-demo flagd
cmds:
- ./chaos/flagd-cart-failure.sh restore
chaos:crashloop:
desc: Patch product-catalog memory limit below need to force OOMKilled/CrashLoopBackOff
cmds:
- ./chaos/crashloop.sh inject
chaos:crashloop-restore:
desc: Restore product-catalog memory limit
cmds:
- ./chaos/crashloop.sh restore