Repository navigation
Expand file tree
/
Copy pathTaskfile.yml
More file actions
950 lines (877 loc) · 44.5 KB
/
Copy pathTaskfile.yml
File metadata and controls
950 lines (877 loc) · 44.5 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
version: "3"
vars:
COMPOSE_FILE: deploy/docker-compose.yml
# Single source of truth for Go test/vet package set, shared by local
# (test:go, vet:go) and CI (test:go:ci) targets.
GO_TEST_PACKAGES: ./internal/... ./cmd/aileron/... ./cmd/aileron-mcp/... ./sdk/go/... ./test/system/lib/...
tasks:
default:
desc: List available tasks
cmds:
- task --list
setup:
desc: One-time local setup after clone (activates committed git hooks)
cmds:
# Point git at the committed hooks so they survive re-clone and are shared
# across all worktrees. The pre-commit hook refuses commits in the primary
# checkout, keeping the default branch a pure fast-forward mirror of origin.
- git config core.hooksPath .githooks
- 'echo "git hooks activated (core.hooksPath=.githooks)"'
generate:
desc: Run all code generators
cmds:
- task: generate:api
- task: generate:tokens
generate:api:
desc: Generate Go types and server interface from OpenAPI spec
dir: internal
cmds:
- go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config api/oapi-codegen.yaml api/openapi.yaml
sources:
- api/openapi.yaml
- api/oapi-codegen.yaml
generates:
- api/gen/server.gen.go
generate:tokens:
desc: Generate design token CSS for docs and webapp
cmds:
- node design/build/generate-tokens.mjs
sources:
- design/tokens.json
- design/build/generate-tokens.mjs
generates:
- design/build/output/tokens.generated.css
- docs/src/styles/tokens.generated.css
- webapp/src/tokens.generated.css
generate:agent-digests:
desc: >-
Regenerate the sandbox image digest lockfile at release prep. Resolves each
published per-agent image's immutable digest from the registry and writes
internal/sandbox/composition/agent-images.lock.json so a release pins a
fixed image by @sha256 instead of the floating `latest` tag (#1233). Run
AFTER the release's images are published, BEFORE tagging the binary.
Requires docker buildx + registry read access. Override the resolved tag
with TAG=<tag> (default: latest).
dir: internal
vars:
TAG: '{{.TAG | default "latest"}}'
cmds:
- go run ./tools/agentdigests -tag {{.TAG}} -out sandbox/composition/agent-images.lock.json
generate:tools-lock:
desc: >-
Regenerate the managed-toolchain lockfile that pins Node.js for reproducible
sandbox devcontainer builds (#1525). Resolves the per-platform sha256 of
each supported Node distribution archive from Node's published
SHASUMS256.txt and writes internal/sandbox/container/tools.lock.json so a
build verifies a fetched archive against the pin and rejects a tampered or
drifted distribution. Run when bumping the pinned Node version. Pass the
version with VERSION=<x.y.z> (default: the version already in the lock).
Requires network read access to nodejs.org.
dir: internal
vars:
VERSION: '{{.VERSION | default "22.14.0"}}'
cmds:
- go run ./tools/toolslock -version {{.VERSION}} -out sandbox/container/tools.lock.json
build:
desc: Build everything (containers, server, MCP, CLI, webapp, docs)
cmds:
- task: build:docker
- task: build:webapp
- task: build:server
- task: build:mcp
- task: build:cli
- task: build:docs
build:docker:
desc: Build all Docker containers
cmds:
- docker compose -f {{.COMPOSE_FILE}} build
build:server:
desc: Build the Go server binary locally (embeds webapp/build via internal/app/webapp_dist)
deps:
- build:webapp
vars:
VERSION: dev
COMMIT:
sh: git rev-parse --short HEAD
# Host executable suffix from the canonical Go primitive: empty on
# Unix/macOS, ".exe" on Windows. An explicit `-o build/<name>` makes the
# toolchain write that exact name and skip the platform `.exe` append it
# only performs when `-o` is absent; appending {{.HOST_EXE}} restores it.
# Scoped per-task (not global) so Go-free targets like build:docs/webapp
# don't eagerly evaluate `go env`, matching build:mcp's HOST_GOOS/GOARCH.
HOST_EXE:
sh: go env GOEXE
cmds:
# Force a pure-Go build (CGO_ENABLED=0) to match .goreleaser.yml. The
# binaries import no cgo, so an ambient CGO_ENABLED=1 only invites
# toolchain failures (e.g. runtime/cgo's -fPIC under clang/msvc on
# Windows). The inline assignment is required: go-task's task-level env:
# does NOT override a CGO_ENABLED already set in the OS environment, and a
# cmd-level env: is silently ignored — a shell prefix always wins.
- CGO_ENABLED=0 go build -ldflags "-X github.com/ALRubinger/aileron/internal/version.Version={{.VERSION}} -X github.com/ALRubinger/aileron/internal/version.Commit={{.COMMIT}}" -o build/aileron-server{{.HOST_EXE}} ./internal/server
build:mcp:
desc: Build the Aileron MCP server binary locally (plus a Linux variant on macOS/Windows hosts so `aileron launch --sandbox=docker` can bind-mount it into the container)
vars:
VERSION: dev
COMMIT:
sh: git rev-parse --short HEAD
HOST_GOOS:
sh: go env GOOS
HOST_GOARCH:
sh: go env GOARCH
# Empty on Unix/macOS, ".exe" on Windows. Appended only to the host-arch
# output below; the forced GOOS=linux sandbox sibling stays extensionless
# (it is bind-mounted into a Linux container).
HOST_EXE:
sh: go env GOEXE
cmds:
# Force a pure-Go build (CGO_ENABLED=0) to match .goreleaser.yml. The
# inline assignment is required: go-task's task-level env: does NOT
# override a CGO_ENABLED already set in the OS environment, and a cmd-level
# env: is silently ignored — a shell prefix always wins.
- CGO_ENABLED=0 go build -ldflags "-X github.com/ALRubinger/aileron/internal/version.Version={{.VERSION}} -X github.com/ALRubinger/aileron/internal/version.Commit={{.COMMIT}}" -o build/aileron-mcp{{.HOST_EXE}} ./cmd/aileron-mcp
# Sandbox launch bind-mounts aileron-mcp into a Linux container. On
# non-Linux hosts the host-arch Mach-O / PE binary above fails with
# exec format error; this second build produces a sibling the
# launcher's resolveSandboxMCPBinary prefers for the sandbox path.
- |
if [ "{{.HOST_GOOS}}" != "linux" ]; then
CGO_ENABLED=0 GOOS=linux GOARCH={{.HOST_GOARCH}} go build -ldflags "-X github.com/ALRubinger/aileron/internal/version.Version={{.VERSION}} -X github.com/ALRubinger/aileron/internal/version.Commit={{.COMMIT}}" -o build/aileron-mcp-linux-{{.HOST_GOARCH}} ./cmd/aileron-mcp
fi
build:cli:
desc: Build the aileron CLI binary locally
vars:
VERSION: dev
COMMIT:
sh: git rev-parse --short HEAD
# Empty on Unix/macOS, ".exe" on Windows. Restores the suffix the toolchain
# skips when `-o` is given explicitly (see build:server for the full why).
HOST_EXE:
sh: go env GOEXE
cmds:
# Force a pure-Go build (CGO_ENABLED=0) to match .goreleaser.yml. The
# inline assignment is required: go-task's task-level env: does NOT
# override a CGO_ENABLED already set in the OS environment, and a cmd-level
# env: is silently ignored — a shell prefix always wins.
- CGO_ENABLED=0 go build -ldflags "-X github.com/ALRubinger/aileron/internal/version.Version={{.VERSION}} -X github.com/ALRubinger/aileron/internal/version.Commit={{.COMMIT}}" -o build/aileron{{.HOST_EXE}} ./cmd/aileron
build:webapp:
desc: Install webapp dependencies, build static output, copy into internal/app/webapp_dist for Go embed
deps: [generate:tokens]
dir: webapp
cmds:
- pnpm install
- pnpm run build
# Replace the embed dir with the fresh build output. Everything
# under webapp_dist/ is gitignored except `.gitkeep`, which we
# restore after the copy so the embed pattern still matches the
# tracked file and `git status` stays clean.
- rm -rf ../internal/app/webapp_dist
- cp -r build ../internal/app/webapp_dist
- touch ../internal/app/webapp_dist/.gitkeep
build:ui:
desc: (Frozen) Install ui dependencies and build. The cloud-tier UI; not embedded into the daemon. New work belongs in webapp/.
dir: ui
cmds:
- pnpm install
- pnpm run build
build:docs:
desc: Install docs dependencies and build
deps: [generate:tokens]
dir: docs
cmds:
- pnpm install
- mkdir -p public
- cp ../internal/api/openapi.yaml public/openapi.yaml
- pnpm run build
mcp:setup:
desc: Build and register the Aileron MCP server with Claude Code
vars:
# Match the path build:mcp wrote: ".exe" on Windows, empty elsewhere, so
# the registered MCP binary points at the file that actually exists.
HOST_EXE:
sh: go env GOEXE
cmds:
- task: build:mcp
- claude mcp add --scope project aileron -e AILERON_URL=http://localhost:8080 -- ./build/aileron-mcp{{.HOST_EXE}}
up:
desc: Start all services via Docker Compose
cmds:
- test -f deploy/.env || cp deploy/.env.example deploy/.env
- docker compose -f {{.COMPOSE_FILE}} up {{.CLI_ARGS}}
down:
desc: Stop all services
cmds:
- docker compose -f {{.COMPOSE_FILE}} down {{.CLI_ARGS}}
clean:
desc: Stop services and remove volumes
cmds:
- docker compose -f {{.COMPOSE_FILE}} down -v --remove-orphans
- rm -rf build
- rm -rf webapp/.svelte-kit webapp/build webapp/node_modules
- rm -rf ui/.svelte-kit ui/build ui/node_modules
- rm -rf docs/.astro docs/dist docs/node_modules
- rm -f docs/public/openapi.yaml
# Reset the embed dir to just the committed `.gitkeep` so a
# subsequent `go build` still succeeds without running
# `task build:webapp`. The handler's `webappNotBuiltStub`
# fallback covers the page render.
- rm -rf internal/app/webapp_dist
- mkdir -p internal/app/webapp_dist
- touch internal/app/webapp_dist/.gitkeep
logs:
desc: Tail service logs
cmds:
- docker compose -f {{.COMPOSE_FILE}} logs -f {{.CLI_ARGS}}
ps:
desc: Show running services
cmds:
- docker compose -f {{.COMPOSE_FILE}} ps
dev:webapp:
desc: Run the local webapp dev server. Iterate without rebuilding the daemon — `aileron launch` still serves the embedded build, but the dev server at localhost:5173 hot-reloads on edits.
deps: [generate:tokens]
dir: webapp
cmds:
- pnpm dev
dev:ui:
desc: (Frozen) Run the cloud-tier UI dev server. The local webapp lives in webapp/; use `dev:webapp` instead unless you're working on the (currently dormant) cloud-hosted UI.
dir: ui
cmds:
- pnpm dev
dev:docs:
desc: Run the docs dev server locally
deps: [generate:tokens]
dir: docs
cmds:
- mkdir -p public
- cp ../internal/api/openapi.yaml public/openapi.yaml
- pnpm dev
lint:
desc: Run linters
cmds:
- task: lint:go
- task: lint:webapp
- task: lint:ui
- task: lint:docs
lint:go:
desc: Vet Go code
cmds:
- go vet {{.GO_TEST_PACKAGES}}
lint:webapp:
desc: Check the local webapp for type errors
dir: webapp
cmds:
- pnpm check
lint:ui:
desc: (Frozen) Check the cloud-tier UI for type errors. Local webapp is `lint:webapp`.
dir: ui
cmds:
- pnpm check
lint:docs:
desc: Check the docs site for type errors
dir: docs
cmds:
- pnpm check
test:
desc: Run all tests
cmds:
- task: test:go
- task: test:webapp
- task: test:ui
- task: test:docs
test:webapp:
desc: Run local webapp unit tests (Vitest)
dir: webapp
cmds:
- pnpm install
- pnpm test
test:webapp:e2e:
desc: Run mocked-API Playwright E2E tests against the local webapp
deps:
- build:webapp
dir: webapp
cmds:
- pnpm install
- pnpm exec playwright install --with-deps chromium
- pnpm test:e2e
test:webapp:e2e:integration:
desc: Run Playwright E2E tests against a real daemon seeded with pending approvals
deps:
- build:server
- build:cli
cmds:
- cd webapp && pnpm install
- cd webapp && pnpm exec playwright install --with-deps chromium
- webapp/scripts/run-integration-e2e.sh
test:ui:
desc: (Frozen) Run cloud-tier UI unit tests. Local webapp tests live in `test:webapp`.
dir: ui
cmds:
- pnpm install
- pnpm test
test:docs:
desc: Run docs unit tests (rehype plugins, etc.)
dir: docs
cmds:
- pnpm install
- pnpm test
test:go:
desc: Run Go tests
cmds:
- go test {{.GO_TEST_PACKAGES}}
test:go:cover:
desc: Run Go tests with coverage; print summary excluding generated and test-helper code
cmds:
- go test -coverprofile=coverage-raw.txt {{.GO_TEST_PACKAGES}}
- grep -v -E '/(api/gen|sessions/sessionstest|tools)/' coverage-raw.txt > coverage.txt
- rm coverage-raw.txt
- go tool cover -func=coverage.txt | tail -1
test:go:ci:
desc: Run Go tests with race, coverage, and JUnit output (single-shot; local equivalent of the CI shards combined)
cmds:
- gotestsum --junitfile junit.xml -- -race -coverprofile=coverage-raw.txt {{.GO_TEST_PACKAGES}}
# Strip generated code (oapi-codegen), test-helper packages
# (e.g. sessions/sessionstest), and release-prep dev tooling
# (internal/tools, which shells out to docker) from the coverage
# profile. Test helpers' err-check branches only fire when the SUT
# misbehaves and dev tools are not production code; measuring either
# as production code is a category mismatch. Belt-and-suspenders
# alongside codecov.yml's ignore directive so local `go tool cover`
# summaries agree with Codecov reports.
- grep -v -E '/(api/gen|sessions/sessionstest|tools)/' coverage-raw.txt > coverage.txt
- rm coverage-raw.txt
test:go:ci:sandbox:
# Sandbox shard of the CI Go test suite. Paired with
# test:go:ci:rest; together they cover the same package set as
# test:go:ci. Split because internal/sandbox/... accounts for ~44%
# of total Go test wall time, so isolating it gives a roughly
# balanced 2-shard matrix.
desc: Run CI Go tests for the internal/sandbox/... shard
cmds:
- gotestsum --junitfile junit.xml -- -race -coverprofile=coverage-raw.txt ./internal/sandbox/...
- grep -v -E '/(api/gen|sessions/sessionstest|tools)/' coverage-raw.txt > coverage.txt
- rm coverage-raw.txt
test:go:ci:rest:
# Non-sandbox shard. Computes the package list at runtime as
# "everything in GO_TEST_PACKAGES minus internal/sandbox/...";
# using `go list | grep` keeps the source-of-truth in
# GO_TEST_PACKAGES so newly added packages flow into the right
# shard automatically.
desc: Run CI Go tests for everything except internal/sandbox/...
cmds:
- |
set -euo pipefail
pkgs=$(go list {{.GO_TEST_PACKAGES}} | grep -vE '/sandbox(/|$)' | tr '\n' ' ')
gotestsum --junitfile junit.xml -- -race -coverprofile=coverage-raw.txt $pkgs
- grep -v -E '/(api/gen|sessions/sessionstest|tools)/' coverage-raw.txt > coverage.txt
- rm coverage-raw.txt
test:go:publish-e2e:
# Build-tagged e2e for `skill publish` (#1901) AND `skill install <oci-ref>`
# (#1902) against a REAL registry + docker daemon (umbrella #1898). Not part
# of GO_TEST_PACKAGES: the tag keeps it out of the unit shards, and it needs
# AILERON_TEST_REGISTRY (e.g. localhost:5000) plus a working docker daemon
# (the composed path pushes via docker tag/push). The integration-publish CI
# job provisions the registry. Fails fast (no t.Skip) when the registry env
# is unset.
desc: Run the skill publish + install e2es against a real OCI registry (needs AILERON_TEST_REGISTRY + docker)
dir: internal
cmds:
# -coverprofile + -coverpkg so the registry read/write boundary the e2es
# exercise (publish's docker-CLI boundary, pull's resolve+fetch, and the
# shared ociremote helper) is uploaded to codecov under the integration
# flag — genuinely tested here, not unit-testable.
- go test -tags=integration_publish -run 'TestPublishE2E|TestPullE2E' -coverprofile=coverage-publish-e2e.txt -coverpkg=./flightplan/publish/...,./flightplan/pull/...,./flightplan/ociremote/... ./flightplan/publish/... ./flightplan/pull/...
vet:go:
desc: Run go vet over all Go packages
cmds:
- go vet {{.GO_TEST_PACKAGES}}
test:integration:
desc: Run integration tests against running services
cmds:
- go test -tags=integration -race ./test/integration/... ./internal/store/postgres/...
env:
AILERON_API_URL: http://localhost:8080
AILERON_UI_BASE_URL: http://localhost:3000
test:integration:sandbox:
desc: Run the sandbox MCP parity integration test (in-process daemon + aileron-mcp host subprocess; see internal/app/sandbox_mcp_test.go for the contract)
cmds:
- go test -tags=integration_sandbox -race -run TestSandboxMCP ./internal/app/...
test:integration:sandbox-proxy-clients:
desc: "Run the real-client sandbox-proxy subprocess integration tests: curl (credential injection + proxied audit) and aws (SigV4 re-sign end-to-end + seal). In-process daemon + intercepting CONNECT/TLS proxy + fake HTTPS upstream; the CLIs run as host subprocesses. Fail-fast — requires `curl` AND `aws` on PATH (no self-skip)."
cmds:
- go test -tags=integration_sandbox -race -run 'TestSandboxProxyCurlIntegration|TestSandboxProxyAWSSigV4Integration' ./internal/app/...
test:integration:sandbox-proxy-tool-container:
desc: "Run the real-container in-container step-egress e2e suite (#1769/#1829): (1) a real `docker run amazon/aws-cli` reaches an in-process sigv4-resign proxy through host.docker.internal, trusting a mounted session CA via AWS_CA_BUNDLE, and the proxy re-signs with the vault secret — no credential bytes in the container image/env/mounts/args; (2) a real `docker run curlimages/curl` egressing under a daemon-minted STEP-SCOPED proxy credential reaches its declared sealed host and is refused 403 at CONNECT for an undeclared one. Fail-fast — requires `docker` on PATH (no self-skip)."
cmds:
- go test -tags=integration_sandbox -race -run 'TestSandboxProxyToolContainerIntegration|TestSandboxProxyToolStepScopeContainerIntegration' ./internal/app/...
test:integration:launch-anthropic-probe:
desc: "Run the composed launch->gateway->Anthropic-probe regression guard (#1699): renders the real api-key AuthSpec (ANTHROPIC_API_KEY + key-aware onboarding doc), stands up the real daemon mux order against a spec-compliant Anthropic upstream (401 without x-api-key, 200+org with it), and asserts Claude Code's GET /api/oauth/profile probe authenticates through the daemon with the rendered key. In-process, no Docker/subprocess prereqs (no self-skip). Fails before #1698, passes after."
dir: internal
cmds:
- go test -tags=integration_sandbox -race -run TestLaunchAnthropicProbe ./app/...
test:integration:auth-github-route:
desc: "Run the auth github route-drift guard (#1157): stands up the real daemon handler over a MemVault and asserts the CLI's hardcoded vault path still resolves to the spec-registered route. In-process, no external prereqs (no self-skip)."
dir: cmd/aileron
cmds:
- go test -tags=integration -race -run TestRunAuthGitHub_PUTReachesRealDaemonRoute .
test:integration:authspec-bindmount:
desc: "Run the AuthSpec bind-mount writability integration test on a Linux Docker host (#988); self-skips on non-Linux or when no Docker runtime is on PATH"
dir: internal
cmds:
- go test -tags=integration_sandbox -run TestAuthSpecBindMountWritable ./launch/...
env:
AILERON_SANDBOX_BASE_CONTEXT:
sh: cd "{{.ROOT_DIR}}/images/sandbox-base" && pwd
test:integration:workspace-uid-remap:
desc: "Run the workspace uid-remap integration test on a Linux Docker host (#1461): proves the agent uid mismatch blocks the writability probe and that the aileron-remap-agent-uid entrypoint fixes it; self-skips on non-Linux or when no Docker runtime is on PATH"
dir: internal
cmds:
- go test -v -tags=integration_sandbox -run TestWorkspaceUIDRemap ./launch/...
env:
AILERON_SANDBOX_BASE_CONTEXT:
sh: cd "{{.ROOT_DIR}}/images/sandbox-base" && pwd
test:integration:authspec-container-read:
desc: "Run the in-container credential read + capture round-trip integration test (#1025): renders a Claude-shaped AuthSpec, launches a real container, asserts the agent reads the credential at ContainerPath and that an in-container rotation round-trips to the vault on clean exit. Fail-fast — requires Docker (no self-skip)."
dir: internal
cmds:
- go test -tags=integration_sandbox -run TestAuthSpecInContainerCredentialReadCapture ./launch/...
env:
AILERON_SANDBOX_BASE_CONTEXT:
sh: cd "{{.ROOT_DIR}}/images/sandbox-base" && pwd
test:integration:sandbox-features:
desc: "Build the per-agent sandbox Features (claude/codex) and assert each agent CLI is on PATH in the composed image: directly via @devcontainers/cli (#1082) and through Aileron's own composition.Discover -> Builder.Build path with a second Feature (#1083); plus the credential-metadata end-to-end assertion (#1857) that composes the aws-cli + gh Features and asserts the composed image's devcontainer.metadata parses into both credential conventions (sigv4-resign + bearer). Fail-fast — requires Docker + Node + @devcontainers/cli (no self-skip). Excludes the managed-toolchain build (TestSandboxFeaturesManaged*), which has its own target/job."
dir: internal
cmds:
- go test -tags=integration_sandbox -run 'TestSandboxFeatures$|TestSandboxFeaturesComposeViaAileron$|TestGHFeatureComposesViaAileron$|TestComposedImageCredentialMetadata$' ./launch/...
env:
AILERON_SANDBOX_BASE_CONTEXT:
sh: cd "{{.ROOT_DIR}}/images/sandbox-base" && pwd
AILERON_SANDBOX_FEATURES_CONTEXT:
sh: cd "{{.ROOT_DIR}}/images/sandbox-features" && pwd
test:integration:sandbox-managed:
desc: "Build a sandbox Feature (claude) through Aileron's composition.Discover -> Builder.Build path with the MANAGED toolchain wired (real provisioner: fetch + verify + cache Node and the pinned @devcontainers/cli), then assert the agent CLI is on PATH — the #1530 acceptance gate; fail-fast — requires Docker plus network/registry access for the managed Node fetch (no self-skip). Host Node/npx is NOT required."
dir: internal
cmds:
- go test -tags=integration_sandbox -run TestSandboxFeaturesManaged ./launch/...
env:
AILERON_SANDBOX_BASE_CONTEXT:
sh: cd "{{.ROOT_DIR}}/images/sandbox-base" && pwd
AILERON_SANDBOX_FEATURES_CONTEXT:
sh: cd "{{.ROOT_DIR}}/images/sandbox-features" && pwd
test:integration:sandbox-agent-image:
desc: "Smoke-test a published per-agent sandbox image (claude/codex): assert the agent CLI is on PATH and Builder.Validate succeeds (launchability), against the image ref in AILERON_SANDBOX_AGENT_IMAGE (#1086); fail-fast — requires Docker and the env set (no self-skip)"
dir: internal
cmds:
- go test -tags=integration_sandbox -run TestSandboxAgentImageSmoke ./launch/...
test:integration:multiarch:
desc: >-
Run the cross-arch multi-arch freeze->publish->launch e2e plus the
host-arch composed-tools boot guard, both under the
integration_sandbox_multiarch tag (#2038). FAIL-FAST — requires: Docker; a
docker-container buildx builder (docker buildx create --driver
docker-container --use); the QEMU binfmt emulators (docker run --privileged
tonistiigi/binfmt --install all) for the real linux/amd64+linux/arm64 build;
@devcontainers/cli for the composer; a reachable OCI registry in
AILERON_TEST_REGISTRY (e.g. localhost:5000); the multi-arch runner base on
that registry/ghcr; and, for the boot guard, a live host daemon via
AILERON_API_URL/AILERON_TOKEN. The e2e runs HOST-NATIVE: buildx+QEMU emulate
the arm64 leg of the multi-arch BUILD, but the test process stays native so it
can exec the native-arch host build tooling (@devcontainers/cli, docker,
buildx). It proves the pin carries both linux/amd64+linux/arm64, publishes the
manifest list, and verifies the host's own child. The genuinely foreign-arch
(#2025) SELECTION is covered by the freeze/ociremote hostGOARCH seam unit
tests; a full foreign-arch consumer boot needs real arm64 hardware (documented
limitation). No self-skip: an absent prerequisite is a hard failure.
dir: cmd/aileron
cmds:
- go test -tags=integration_sandbox_multiarch -run 'TestFlightPlanComposedToolsBootGuard|TestFlightPlanCrossArchMultiArchE2E' .
env:
# The production composer builds the composed image onto the sandbox base
# from this context, exactly as the sandbox-features target does.
AILERON_SANDBOX_BASE_CONTEXT:
sh: cd "{{.ROOT_DIR}}/images/sandbox-base" && pwd
AILERON_SANDBOX_FEATURES_CONTEXT:
sh: cd "{{.ROOT_DIR}}/images/sandbox-features" && pwd
build:launch:
desc: >-
Build the binaries `aileron launch --sandbox=docker` needs: the CLI
(build/aileron) plus the MCP server. On non-Linux hosts build:mcp also
emits build/aileron-mcp-linux-<arch>, the sibling the launcher
bind-mounts into the Linux sandbox. Reusable build seam for the
system-test scenarios so each one tests a freshly built launch path.
deps:
- build:cli
- build:mcp
_test:system:precond:docker:
internal: true
desc: >-
Fail-fast precondition: the Docker daemon must be reachable. Shared by
the smoke target (no agent) and the agent scenarios. On miss, exits
non-zero with the exact remediation — never a silent skip.
cmds:
- |
if ! docker info >/dev/null 2>&1; then
echo "Docker daemon is not reachable. Start Docker Desktop / dockerd, then re-run: task {{.TARGET | default "test:system:smoke"}}" >&2
exit 1
fi
_test:system:precond:
internal: true
desc: >-
Fail-fast preconditions for an agent system-test scenario: Docker must be
reachable AND the agent's credential must be resolvable. Pass
AGENT=codex|claude. The credential is accepted from EITHER source: the
host-side auth file the launcher's AuthSpec mounts, OR the vault/daemon
entry agents/<agent>/oauth (which the launcher's AuthSpec renders into the
sandbox at run time, so the host file may legitimately be absent on a
machine where the credential lives only in the vault). On a missing
credential, exits non-zero naming the remediation — never a silent skip.
vars:
AGENT: '{{.AGENT}}'
cmds:
- task: _test:system:precond:docker
vars:
TARGET: 'test:system:launch:{{.AGENT}}'
- |
case "{{.AGENT}}" in
codex)
authfile="$HOME/.codex/auth.json"
logincmd="codex login"
;;
claude)
authfile="$HOME/.claude/.credentials.json"
logincmd="claude /login"
;;
*)
echo "Unknown AGENT '{{.AGENT}}' (expected codex or claude)" >&2
exit 1
;;
esac
# Normalize any Windows backslashes in $HOME so the path renders with a
# single separator style (e.g. C:/Users/alr/.codex/auth.json) instead of
# the mixed C:\Users\alr/.codex/auth.json that git-bash would print.
# Use go-task's embedded shell (mvdan.cc/sh) pattern-substitution builtin
# rather than `tr`: that Unix coreutil is not on PATH on a stock Windows
# host, where it failed the whole run with exit 127 (#1653).
authfile="${authfile//\\//}"
vaultentry="agents/{{.AGENT}}/oauth"
# The credential is accepted from EITHER the host file OR the vault
# entry. The launcher's AuthSpec renders the vault credential into the
# sandbox at run time, so the host file may be absent on a machine where
# the credential lives only in the vault (agents/<agent>/oauth).
if [ -f "$authfile" ]; then
:
else
# The vault-entry check below touches the vault with stderr
# suppressed. When the daemon vault is locked, that touch would
# prompt for the passphrase on /dev/tty with the prompt text hidden,
# hanging the run with no visible prompt. Touch the vault VISIBLY
# first (stdout discarded, the passphrase prompt left on the
# terminal) so the operator sees and can answer it; this is a no-op
# when the vault is already unlocked.
aileron vault list >/dev/null || true
# Whole-line membership test for $vaultentry in the vault listing,
# done with shell builtins (`while read` + `case`) instead of
# `grep -qx`: like `tr` above, grep is a Unix external not guaranteed
# on a stock Windows host (#1653). A here-string feeds the loop in the
# current shell (no pipe subshell), so $found survives; `case` matches
# the whole line, preserving the exact-line semantics of `grep -qx`.
entries="$(aileron vault list --scope agent 2>/dev/null)"
found=
while IFS= read -r line; do
case "$line" in
"$vaultentry") found=1; break ;;
esac
done <<< "$entries"
if [ -n "$found" ]; then
:
else
echo "Missing {{.AGENT}} credential: neither host file $authfile nor a vault entry ($vaultentry) is present. Authenticate first with: $logincmd" >&2
exit 1
fi
fi
_test:system:cleanup:
internal: true
desc: >-
Teardown for a system-test scenario, invoked via `defer:` so it runs even
when an earlier assertion fails. Removes the sandbox container
aileron-sbx-<SESSION> (tolerating "no such container") and the scenario's
temp WORKSPACE dir, guarded so an unset/empty path never `rm -rf /`.
vars:
SESSION: '{{.SESSION}}'
WORKSPACE: '{{.WORKSPACE}}'
cmds:
- |
if [ -n "{{.SESSION}}" ]; then
docker rm -f "aileron-sbx-{{.SESSION}}" >/dev/null 2>&1 || true
fi
case "{{.WORKSPACE}}" in
""|/) ;;
*) rm -rf "{{.WORKSPACE}}" ;;
esac
test:system:
desc: >-
Run the `aileron launch` system-test scenarios (run-by-hand, not wired
into CI — v1). Runs the harness smoke self-test and the shared
scenario-library contract tests, then the per-agent scenarios codex
(#1476) and claude (#1477) end to end. Requires a reachable Docker daemon
and, per scenario, an agent credential resolvable from EITHER the host
auth file OR the vault entry agents/<agent>/oauth — each scenario
fail-fasts with the exact remediation if its auth is missing from both
sources, never a silent skip. To run
a single agent without the other, invoke its leaf directly
(e.g. `task test:system:launch:claude`).
cmds:
- task: test:system:lib
- task: test:system:smoke
- task: test:system:launch:codex
- task: test:system:launch:claude
test:system:lib:
desc: >-
Contract tests for the shared system-test scenario library
(test/system/lib/assert.go + probes.go) used by the codex (#1476) and
claude (#1477) scenarios. A shell-free Go contract suite that needs only
the Go toolchain. It needs no Docker daemon, no `aileron launch`, and no
Unix shell, so it is safe to run in CI, unattended, and on Windows. It
validates the assert helpers, the discover_container match-count
arbitration, and the launch-target taskenv wiring regression against
their contracts.
cmds:
- go test ./test/system/lib/...
test:system:smoke:
desc: >-
Harness self-test (no `aileron launch`, no agent, no LLM tokens): proves
the system-test mechanics end to end — build fires, the Docker
precondition gates, and `defer` cleanup runs even when an inner step
fails. Builds aileron, checks Docker, creates a temp workspace + sentinel,
then deliberately fails an inner step inside a subshell; the deferred
cleanup removes the workspace, and the target verifies it is gone (so the
target itself exits 0). Requires a reachable Docker daemon.
deps:
- build:launch
vars:
SESSION: smoke-test
# Pure-template temp path: go-task exposes env vars (TMPDIR/TEMP/TMP) as
# template vars, so this resolves a writable dir on Windows (TEMP/TMP set)
# and Unix (TMPDIR or /tmp) without `mktemp`, whose bundled-coreutils form
# can't resolve /tmp under go-task's embedded shell on Windows. `unixEpoch`
# + `randInt` replace `date +%s` and `$$` (no shell binary on PATH needed).
WORKSPACE: '{{.TMPDIR | default .TEMP | default .TMP | default "/tmp"}}/aileron-smoke-{{now | unixEpoch}}-{{randInt 10000 99999}}'
# ".exe" on Windows, empty elsewhere, so the build:launch artifact check
# below tests the actual file the toolchain wrote (build/aileron.exe).
HOST_EXE:
sh: go env GOEXE
cmds:
- task: _test:system:precond:docker
vars:
TARGET: test:system:smoke
- defer: { task: _test:system:cleanup, vars: { SESSION: '{{.SESSION}}', WORKSPACE: '{{.WORKSPACE}}' } }
# The template no longer creates the dir the way `mktemp -d` did; create it
# explicitly (mkdir is bundled in Task's coreutils, works cross-platform).
- mkdir -p "{{.WORKSPACE}}"
- |
set -u
# Prove build:launch produced the launch binary.
test -f build/aileron{{.HOST_EXE}} || { echo "build:launch did not produce build/aileron{{.HOST_EXE}}" >&2; exit 1; }
# Seed a sentinel the deferred cleanup must remove.
touch "{{.WORKSPACE}}/sentinel"
test -f "{{.WORKSPACE}}/sentinel" || { echo "failed to seed sentinel" >&2; exit 1; }
# Deliberately fail an inner step in a subshell to prove `defer` still
# fires; absorb the non-zero so the outer target can assert cleanup ran
# and still return 0.
( echo "smoke: simulating a scenario assertion failure" >&2; exit 1 ) || true
- |
set -u
# Run the SAME teardown logic the deferred _test:system:cleanup runs,
# inline, so we can assert removal within this run (a `defer` fires only
# after the target returns, so its effect is not observable here). The
# registered `defer` then re-runs this idempotently as the genuine
# belt-and-suspenders teardown on exit. This mirrors _test:system:cleanup
# exactly; keep the two in sync.
if [ -n "{{.SESSION}}" ]; then
docker rm -f "aileron-sbx-{{.SESSION}}" >/dev/null 2>&1 || true
fi
case "{{.WORKSPACE}}" in
""|/) ;;
*) rm -rf "{{.WORKSPACE}}" ;;
esac
if [ -e "{{.WORKSPACE}}" ]; then
echo "cleanup did not remove workspace {{.WORKSPACE}}" >&2
exit 1
fi
echo "smoke: harness OK — build fired, Docker precondition passed, cleanup removed the workspace"
test:system:launch:codex:
desc: >-
System-test scenario for the codex agent under `aileron launch` (#1476).
Wires the reusable build seam (build:launch), fail-fast preconditions
(_test:system:precond AGENT=codex), and `defer` cleanup (#1475), then runs
the shell-free Go scenario driver (`go run ./test/system/scenario codex`):
it drives `aileron launch codex -- exec "…"` once (never `docker run`
directly), runs the shared R8 wiring-invariant probes against the live
container, asserts R7a arg-forwarding, the R9 sentinel, and the R10 audit
round-trip. STATIC GATE — a live run needs a real codex
login and LLM tokens, so it is run BY HAND on a real host; the headless
path validates wiring with `task --dry test:system:launch:codex`, which
compiles the target without executing the scenario. See test/system/README.md.
deps:
- build:launch
vars:
# Pure-template, no shell binary on PATH needed: `unixEpoch` + `randInt`
# replace `date +%s` and `$$` so SESSION/WORKSPACE resolve under go-task's
# embedded shell on Windows (no `date`, no usable `mktemp`). go-task exposes
# env vars (TMPDIR/TEMP/TMP) as template vars for a writable temp root.
SESSION: 'codex-{{now | unixEpoch}}-{{randInt 10000 99999}}'
WORKSPACE: '{{.TMPDIR | default .TEMP | default .TMP | default "/tmp"}}/aileron-codex-{{now | unixEpoch}}-{{randInt 10000 99999}}'
# ".exe" on Windows, empty elsewhere. AILERON_BIN below must point at the
# file build:launch wrote; the Go driver reads AILERON_BIN from the env and
# invokes that binary's `launch` directly.
HOST_EXE:
sh: go env GOEXE
# Task-scoped env: go-task only honors `env:` at the task level, not on an
# individual `cmds:` entry (a command-level `env:` is silently ignored), so
# the Go scenario driver must receive AILERON_BIN et al. from here. Keeping
# it task-scoped also exports the vars to the `defer`/precond cmds uniformly.
env:
AILERON_BIN: '{{.ROOT_DIR}}/build/aileron{{.HOST_EXE}}'
AILERON_SYSTEST_LIB: '{{.ROOT_DIR}}/test/system/lib'
WORKSPACE: '{{.WORKSPACE}}'
AILERON_STATE_DIR:
sh: echo "${AILERON_STATE_DIR:-$HOME/.aileron}"
cmds:
# Register cleanup FIRST so a failed precondition (Docker down or codex
# auth absent) still removes the WORKSPACE — a `defer` only registers once
# its line is reached.
- defer: { task: _test:system:cleanup, vars: { SESSION: '{{.SESSION}}', WORKSPACE: '{{.WORKSPACE}}' } }
# The WORKSPACE template no longer creates the dir the way `mktemp -d` did;
# create it explicitly (mkdir is bundled in Task's coreutils, cross-platform)
# before the scenario bind-mounts it and reads back its sentinel.
- mkdir -p "{{.WORKSPACE}}"
- task: _test:system:precond
vars:
AGENT: codex
- go run ./test/system/scenario codex
test:system:launch:claude:
desc: >-
System-test scenario for the claude agent under `aileron launch` (#1477).
Wires the reusable build seam (build:launch), fail-fast preconditions
(_test:system:precond AGENT=claude), and `defer` cleanup (#1475), then
runs the shell-free Go scenario driver (`go run ./test/system/scenario
claude`): it drives `aileron launch claude -- -p "…"` once (never `docker
run` directly), runs the shared R8 wiring-invariant probes against the
live container, asserts R7a arg-forwarding, the R9 sentinel, and the R10
audit round-trip. STATIC GATE — a live run needs a
real Claude login (vault-captured, ADR-0025) and LLM tokens, so it is run
BY HAND on a real host; the headless path validates wiring with
`task --dry test:system:launch:claude`, which compiles the target without
executing the scenario. See test/system/README.md.
deps:
- build:launch
vars:
# Pure-template, no shell binary on PATH needed: `unixEpoch` + `randInt`
# replace `date +%s` and `$$` so SESSION/WORKSPACE resolve under go-task's
# embedded shell on Windows (no `date`, no usable `mktemp`). go-task exposes
# env vars (TMPDIR/TEMP/TMP) as template vars for a writable temp root.
SESSION: 'claude-{{now | unixEpoch}}-{{randInt 10000 99999}}'
WORKSPACE: '{{.TMPDIR | default .TEMP | default .TMP | default "/tmp"}}/aileron-claude-{{now | unixEpoch}}-{{randInt 10000 99999}}'
# ".exe" on Windows, empty elsewhere. AILERON_BIN below must point at the
# file build:launch wrote; the Go driver reads AILERON_BIN from the env and
# invokes that binary's `launch` directly.
HOST_EXE:
sh: go env GOEXE
# Task-scoped env: go-task only honors `env:` at the task level, not on an
# individual `cmds:` entry (a command-level `env:` is silently ignored), so
# the Go scenario driver must receive AILERON_BIN et al. from here. Keeping
# it task-scoped also exports the vars to the `defer`/precond cmds uniformly.
env:
AILERON_BIN: '{{.ROOT_DIR}}/build/aileron{{.HOST_EXE}}'
AILERON_SYSTEST_LIB: '{{.ROOT_DIR}}/test/system/lib'
WORKSPACE: '{{.WORKSPACE}}'
AILERON_STATE_DIR:
sh: echo "${AILERON_STATE_DIR:-$HOME/.aileron}"
cmds:
# Register cleanup FIRST so a failed precondition (Docker down or claude
# auth absent) still removes the WORKSPACE — a `defer` only registers once
# its line is reached.
- defer: { task: _test:system:cleanup, vars: { SESSION: '{{.SESSION}}', WORKSPACE: '{{.WORKSPACE}}' } }
# The WORKSPACE template no longer creates the dir the way `mktemp -d` did;
# create it explicitly (mkdir is bundled in Task's coreutils, cross-platform)
# before the scenario bind-mounts it and reads back its sentinel.
- mkdir -p "{{.WORKSPACE}}"
- task: _test:system:precond
vars:
AGENT: claude
- go run ./test/system/scenario claude
test:e2e:integration:
desc: Run Playwright E2E tests against running services
dir: ui
cmds:
- pnpm install
- pnpm exec playwright install --with-deps chromium
- pnpm test:e2e:integration
env:
AILERON_API_URL: http://localhost:8080
AILERON_UI_BASE_URL: http://localhost:3000
test:integration:coverage:
desc: Start a coverage-instrumented stack, run integration tests, and emit coverage-integration.txt
cmds:
- mkdir -p gocoverdir && chmod 777 gocoverdir
- docker compose -f {{.COMPOSE_FILE}} -f deploy/docker-compose.coverage.yml up -d --build --wait
- go test -tags=integration -race ./test/integration/... ./internal/store/postgres/...
- docker compose -f {{.COMPOSE_FILE}} -f deploy/docker-compose.coverage.yml stop server
- go tool covdata textfmt -i=gocoverdir -o=coverage-integration.txt
- docker compose -f {{.COMPOSE_FILE}} -f deploy/docker-compose.coverage.yml down -v
env:
AILERON_API_URL: http://localhost:8080
AILERON_UI_BASE_URL: http://localhost:3000
test:integration:local:
desc: One command for the local Go integration loop — build + start the stack, wait for health, run the integration suite, then tear the stack down (even on failure)
cmds:
# Register teardown FIRST so the stack comes down even if `up`, the health
# poll, or the tests fail — a `defer` only registers once its line runs,
# mirroring the test:e2e:scenario cleanup-first pattern in this file.
- defer: { task: down, vars: { CLI_ARGS: '-v' } }
# Match the `up` task: seed deploy/.env from the example so a fresh clone's
# `docker compose up` resolves its env interpolation.
- test -f deploy/.env || cp deploy/.env.example deploy/.env
- docker compose -f {{.COMPOSE_FILE}} up -d --build --wait
# Belt-and-suspenders over compose `--wait`: poll the health endpoint the
# same way the CI integration-go job does before running the suite.
- for i in $(seq 1 60); do curl -sf http://localhost:8080/v1/health && break; sleep 3; done
- go test -tags=integration -race ./test/integration/... ./internal/store/postgres/...
env:
AILERON_API_URL: http://localhost:8080
AILERON_UI_BASE_URL: http://localhost:3000
ci:
desc: Run the full CI pipeline locally
cmds:
- task: generate
- task: lint
- task: test
- task: build:docker
- task: up -- -d --build --wait
- task: test:integration
- task: down
release:check:
desc: Validate the GoReleaser config (includes the scoops and homebrew_casks blocks)
cmds:
- goreleaser check
release:snapshot:
desc: Test GoReleaser locally (snapshot, no publish). Pass extra flags after --, e.g. `task release:snapshot -- --skip=sign`.
cmds:
- goreleaser release --snapshot --clean {{.CLI_ARGS}}
health:
desc: Check the server health endpoint
cmds:
- curl -s http://localhost:8080/v1/health | head -c 500 && echo
check:oauth-callback:
desc: Verify stable OAuth callback domain configuration
cmds:
- |
if [ -z "${AILERON_OAUTH_CALLBACK_BASE_URL}" ]; then
echo "AILERON_OAUTH_CALLBACK_BASE_URL is not set — using dynamic callback URLs (default)"
else
echo "Stable callback URL: ${AILERON_OAUTH_CALLBACK_BASE_URL}"
echo "Trusted origins: ${AILERON_TRUSTED_ORIGINS:-<not set>}"
echo ""
echo "Register these URIs with your OAuth providers:"
echo " Google: ${AILERON_OAUTH_CALLBACK_BASE_URL}/auth/google/callback"
echo " GitHub: ${AILERON_OAUTH_CALLBACK_BASE_URL}/auth/github/callback"
fi