carlos / platform Public

harness-build: -tags harness endpoint file fallback and test sums key, no-op hooks in the release build

One tagged file (internal/s3compat/harness.go) is the deployment
harness's only binary deviation (review B1). Without the tag the hooks
are nil and harness_default_test.go pins the release behaviour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 6b905c1154a337404d8b514cc3aae2a8c4145226 parent 85bd6c0
9 files changed, +326 −5
  • harness/RESOURCES.md +7 −0
  • harness/RUNLOG.md +47 −0
  • internal/host/claimproduction_marker.go +3 −1
  • internal/hostfence/writer.go +11 −2
  • internal/publicrelease/feed.go +12 −1
  • internal/s3compat/harness.go +81 −0
  • internal/s3compat/harness_default_test.go +54 −0
  • internal/s3compat/harness_test.go +71 −0
  • internal/s3compat/s3compat.go +40 −1
diff --git a/harness/RESOURCES.md b/harness/RESOURCES.md
new file mode 100644
index 000000000..d36d7161b
--- /dev/null
+++ b/harness/RESOURCES.md
@@ -0,0 +1,7 @@
+# Harness AWS resources (dev account 640012953448, eu-west-1, profile kass)
+
+Every resource is tagged `purpose=carlos-harness` and `ttl=<UTC expiry>`.
+Appended as each is created. Teardown commands: harness/TEARDOWN.md.
+
+| Created (UTC) | Type | ID / name | Notes |
+|---|---|---|---|
diff --git a/harness/RUNLOG.md b/harness/RUNLOG.md
new file mode 100644
index 000000000..cc3609b8d
--- /dev/null
+++ b/harness/RUNLOG.md
@@ -0,0 +1,47 @@
+# Harness phase 1 run log (steps 1 to 6)
+
+Build order: Fable's "Revised phase 1" in harness-spec-fable-review.md, steps 1 to 6.
+Each step's check passes before the next starts. Times are UTC. Commands run
+from DOCS-T1 unless they say "on host" (sent through real SSM) or "in <container>".
+
+## Step 1: the harness build
+
+Started 2026-10-20T00:39:52Z, worktree base 85bd6c0b (85bd6c0b).
+
+Change: one tagged file, `internal/s3compat/harness.go` (`//go:build harness`),
+plus no-op hooks in release code:
+
+- `s3compat.Endpoint()` falls back to `/etc/carlos/harness-endpoint` only when
+ `harnessEndpoint` is set, which only the tagged file does.
+- `s3compat.HarnessS3Options()` / `HarnessSTSEndpoint()`: called by the claim marker
+ client (`internal/host/claimproduction_marker.go`) and the fence writer
+ (`internal/hostfence/writer.go`, S3 and STS). No-ops without the tag.
+- `publicrelease.HarnessSumsKey`: nil without the tag; the tagged file reads
+ `/etc/carlos/harness-sums-pubkey`.
+- Both files fail closed (missing, group/world-writable or >4 KiB reads as "").
+- `CARLOS_HARNESS_INFO=1` makes a harness binary print its resolution and exit
+ (the review's "`carlos harness-info` or equivalent"; no new verb, so
+ `docs/site/cli.md` is untouched).
+
+Check (all on DOCS-T1):
+
+```
+$ go test ./internal/s3compat ./internal/publicrelease -count=1
+ok internal/s3compat 0.006s ; ok internal/publicrelease 0.008s
+$ go test -tags harness ./internal/s3compat ./internal/publicrelease -count=1
+ok internal/s3compat 0.006s ; ok internal/publicrelease 0.008s
+$ go test -count=1 -run 'SumsKey|ClaimsSums' . # default build
+ok github.com/carlosframework/platform 0.066s
+$ go test -tags harness -count=1 -run 'SumsKey|ClaimsSums' . # expected FAIL: the key is the deviation
+--- FAIL: TestClaimsSumsKeyIsTheCLIUpdateKey: publicrelease key "", cliUpdatePubKey "R+EO3q..."
+$ go vet (both builds), GOOS=darwin/windows build of internal/host + internal/hostfence: clean
+$ CARLOS_HARNESS_INFO=1 ./carlos-amd64-harness
+harness build: yes / endpoint: (empty) / sums key: (none)... sha256:e3b0c44298fc
+$ CARLOS_HARNESS_INFO=1 ./carlos-amd64-default version
+carlos dev # the default build ignores the variable
+```
+
+linux/arm64 binaries: `GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -tags harness
+-ldflags "-s -w -X .../internal/version.Build=harness-<sha>"` for `.` and `./cmd/console`.
+
+**Step 1: GREEN** (2026-10-20T00:39:52Z).
diff --git a/internal/host/claimproduction_marker.go b/internal/host/claimproduction_marker.go
index 76e34f185..a68887b10 100644
--- a/internal/host/claimproduction_marker.go
+++ b/internal/host/claimproduction_marker.go
@@ -11,6 +11,7 @@ import (
"github.com/carlosframework/platform/internal/hostbox"
"github.com/carlosframework/platform/internal/hostwrite"
"github.com/carlosframework/platform/internal/rootaws"
+ "github.com/carlosframework/platform/internal/s3compat"
)
type productionClaimMarkers struct {
@@ -23,7 +24,8 @@ func newProductionClaimMarkers(ctx context.Context, bucket, region string) (Clai
if err != nil {
return nil, err
}
- return productionClaimMarkers{Client: s3.NewFromConfig(cfg), Bucket: bucket}, nil
+ // HarnessS3Options is a no-op outside the deployment harness's build.
+ return productionClaimMarkers{Client: s3.NewFromConfig(cfg, s3compat.HarnessS3Options()), Bucket: bucket}, nil
}
func (m productionClaimMarkers) PutMarker(ctx context.Context, bucket, key string, body []byte, expected string, check func(context.Context) error) (string, error) {
if m.Client == nil || bucket != m.Bucket || check == nil || !strings.HasPrefix(key, "instances/") || !strings.HasSuffix(key, "/generation.json") {
diff --git a/internal/hostfence/writer.go b/internal/hostfence/writer.go
index 1b2372998..f98d66e23 100644
--- a/internal/hostfence/writer.go
+++ b/internal/hostfence/writer.go
@@ -15,6 +15,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/carlosframework/platform/internal/hostfloor"
"github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/s3compat"
)
type fenceCredentials struct {
@@ -58,10 +59,18 @@ func OpenRootFenceStore(ctx context.Context, bucket string) (release.Store, erro
return nil, err
}
cfg := aws.Config{Region: c.Region, Credentials: aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(c.AccessKey, c.SecretKey, ""))}
- provider := stscreds.NewAssumeRoleProvider(sts.NewFromConfig(cfg), c.Role, func(o *stscreds.AssumeRoleOptions) { o.RoleSessionName = "carlos-root-fence" })
+ // The two s3compat.Harness* hooks are no-ops outside the deployment
+ // harness's build: this writer talks only to AWS in every release build.
+ provider := stscreds.NewAssumeRoleProvider(sts.NewFromConfig(cfg, harnessSTS), c.Role, func(o *stscreds.AssumeRoleOptions) { o.RoleSessionName = "carlos-root-fence" })
cfg.Credentials = aws.NewCredentialsCache(provider)
- return &release.S3Store{Client: s3.NewFromConfig(cfg), Bucket: c.Bucket}, nil
+ return &release.S3Store{Client: s3.NewFromConfig(cfg, s3compat.HarnessS3Options()), Bucket: c.Bucket}, nil
}
+func harnessSTS(o *sts.Options) {
+ if ep := s3compat.HarnessSTSEndpoint(); ep != "" {
+ o.BaseEndpoint = &ep
+ }
+}
+
func decodeFenceCredentials(r io.Reader, bucket string) (fenceCredentials, error) {
var c fenceCredentials
d := json.NewDecoder(io.LimitReader(r, 16385))
diff --git a/internal/publicrelease/feed.go b/internal/publicrelease/feed.go
index 65f1e5270..5df6adcf4 100644
--- a/internal/publicrelease/feed.go
+++ b/internal/publicrelease/feed.go
@@ -29,13 +29,24 @@ type Feed struct {
// Default is the vendor feed. The strings are duplicated in update.go's
// package vars so update_test.go can repoint them; keep the two in step.
func Default() Feed {
- return Feed{
+ f := Feed{
LatestURL: "https://apt.carlosframework.com/latest.txt",
DownloadBase: "https://github.com/carlosframework/releases/releases/download",
PubKey: "R+EO3qQ47VsuKl9Qq/pkDXBucPYvzp5lU2ut4zpnJPc=",
}
+ if HarnessSumsKey != nil {
+ f.PubKey = HarnessSumsKey()
+ }
+ return f
}
+// HarnessSumsKey is set only by the deployment harness's build
+// (internal/s3compat/harness.go, -tags harness), which replaces the vendor
+// key with the harness's test sums key. It is nil in every release build;
+// internal/s3compat/harness_default_test.go and claims_sums_key_test.go
+// assert the vendor key.
+var HarnessSumsKey func() string
+
var TagRe = regexp.MustCompile(`^v[0-9]+\.[0-9]+\.[0-9]+$`)
func AssetName(platform string) string { return "carlos-" + platform }
diff --git a/internal/s3compat/harness.go b/internal/s3compat/harness.go
new file mode 100644
index 000000000..ef5c36c86
--- /dev/null
+++ b/internal/s3compat/harness.go
@@ -0,0 +1,81 @@
+//go:build harness
+
+// This file is compiled only into the deployment harness's build
+// (`go build -tags harness`), never into a release. It is the harness's one
+// binary deviation, and every harness run report lists it as "endpoint
+// resolution and sums key: harness build" (docs/superpowers/specs/
+// 2026-10-20-deployment-harness-design.md, review B1).
+//
+// It does two things:
+//
+// 1. Endpoint() falls back to /etc/carlos/harness-endpoint when
+// CARLOS_S3_ENDPOINT is empty. Root processes scrub AWS_ENDPOINT_URL_*
+// (rootaws.ScrubEnvironment) and systemd-run, incus exec and units carry
+// no harness variable, so a file every process can read is the one
+// mechanism that reaches all of them. HarnessS3Options and
+// HarnessSTSEndpoint then point the claim marker client and the fence
+// writer, which ignore CARLOS_S3_ENDPOINT in a release build, at the
+// same store.
+// 2. publicrelease.Default().PubKey becomes the harness's test sums key,
+// read from /etc/carlos/harness-sums-pubkey, so publish-identity can
+// verify a harness release's SHA256SUMS.txt.
+//
+// Both files fail closed: a missing, unreadable, or group- or world-writable
+// file reads as "", which leaves the client on AWS (unreachable from a
+// harness box) and makes every sums signature fail to verify.
+//
+// CARLOS_HARNESS_INFO=1 makes the binary print what it resolved and exit 0,
+// so a person can check a binary is a harness build without running a verb.
+package s3compat
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/carlosframework/platform/internal/publicrelease"
+)
+
+var (
+ harnessEndpointFile = "/etc/carlos/harness-endpoint"
+ harnessSumsKeyFile = "/etc/carlos/harness-sums-pubkey"
+)
+
+func init() {
+ harnessEndpoint = func() string { return readHarnessFile(harnessEndpointFile) }
+ publicrelease.HarnessSumsKey = func() string { return readHarnessFile(harnessSumsKeyFile) }
+ if os.Getenv("CARLOS_HARNESS_INFO") == "1" {
+ fmt.Fprint(os.Stdout, HarnessInfo())
+ os.Exit(0)
+ }
+}
+
+// HarnessInfo describes the harness build's resolution: the endpoint and a
+// prefix of the sums key with its sha256, never more of the key than that.
+func HarnessInfo() string {
+ key := publicrelease.Default().PubKey
+ sum := sha256.Sum256([]byte(key))
+ prefix := key
+ if len(prefix) > 8 {
+ prefix = prefix[:8]
+ }
+ if key == "" {
+ prefix = "(none)"
+ }
+ return fmt.Sprintf("harness build: yes\nendpoint: %s\nsums key: %s... sha256:%s\n",
+ Endpoint(), prefix, hex.EncodeToString(sum[:])[:12])
+}
+
+func readHarnessFile(path string) string {
+ st, err := os.Stat(path)
+ if err != nil || !st.Mode().IsRegular() || st.Mode().Perm()&0o022 != 0 || st.Size() > 4096 {
+ return ""
+ }
+ b, err := os.ReadFile(path)
+ if err != nil {
+ return ""
+ }
+ return strings.TrimSpace(string(b))
+}
diff --git a/internal/s3compat/harness_default_test.go b/internal/s3compat/harness_default_test.go
new file mode 100644
index 000000000..4428ebb5c
--- /dev/null
+++ b/internal/s3compat/harness_default_test.go
@@ -0,0 +1,54 @@
+//go:build !harness
+
+package s3compat
+
+import (
+ "testing"
+
+ awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/carlosframework/platform/internal/publicrelease"
+)
+
+// TestHarnessDefaultBuildHasNoFallback pins the release build: the
+// deployment harness's endpoint file and test sums key exist only under
+// -tags harness (harness.go). Without the tag there is no fallback, the
+// root clients' harness hooks change nothing, and the sums key is the
+// vendor's.
+func TestHarnessDefaultBuildHasNoFallback(t *testing.T) {
+ t.Setenv("CARLOS_S3_ENDPOINT", "")
+ if harnessEndpoint != nil {
+ t.Fatal("release build has a harness endpoint fallback")
+ }
+ if got := Endpoint(); got != "" {
+ t.Fatalf("Endpoint() = %q with the variable empty, want \"\"", got)
+ }
+ if got := HarnessSTSEndpoint(); got != "" {
+ t.Fatalf("HarnessSTSEndpoint() = %q, want \"\"", got)
+ }
+ var o awss3.Options
+ HarnessS3Options()(&o)
+ if o.BaseEndpoint != nil || o.UsePathStyle {
+ t.Fatal("HarnessS3Options changed a client in the release build")
+ }
+ if publicrelease.HarnessSumsKey != nil {
+ t.Fatal("release build has a harness sums key hook")
+ }
+ if got := publicrelease.Default().PubKey; got != "R+EO3qQ47VsuKl9Qq/pkDXBucPYvzp5lU2ut4zpnJPc=" {
+ t.Fatalf("sums key = %q, want the vendor key", got)
+ }
+}
+
+// TestHarnessDefaultBuildRootClientsIgnoreTheVariable: the root hooks stay
+// no-ops even when CARLOS_S3_ENDPOINT is set, so the marker client and the
+// fence writer keep their release behaviour.
+func TestHarnessDefaultBuildRootClientsIgnoreTheVariable(t *testing.T) {
+ t.Setenv("CARLOS_S3_ENDPOINT", "http://203.0.113.10:9000")
+ var o awss3.Options
+ HarnessS3Options()(&o)
+ if o.BaseEndpoint != nil {
+ t.Fatal("HarnessS3Options honoured CARLOS_S3_ENDPOINT in the release build")
+ }
+ if HarnessSTSEndpoint() != "" {
+ t.Fatal("HarnessSTSEndpoint honoured CARLOS_S3_ENDPOINT in the release build")
+ }
+}
diff --git a/internal/s3compat/harness_test.go b/internal/s3compat/harness_test.go
new file mode 100644
index 000000000..a42f39cae
--- /dev/null
+++ b/internal/s3compat/harness_test.go
@@ -0,0 +1,71 @@
+//go:build harness
+
+package s3compat
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/carlosframework/platform/internal/publicrelease"
+)
+
+func harnessFiles(t *testing.T, endpoint, key string, mode os.FileMode) {
+ t.Helper()
+ dir := t.TempDir()
+ ep, sk := filepath.Join(dir, "endpoint"), filepath.Join(dir, "key")
+ for p, v := range map[string]string{ep: endpoint, sk: key} {
+ if err := os.WriteFile(p, []byte(v+"\n"), mode); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Chmod(p, mode); err != nil {
+ t.Fatal(err)
+ }
+ }
+ oe, ok := harnessEndpointFile, harnessSumsKeyFile
+ harnessEndpointFile, harnessSumsKeyFile = ep, sk
+ t.Cleanup(func() { harnessEndpointFile, harnessSumsKeyFile = oe, ok })
+}
+
+func TestHarnessBuildFallsBackToFile(t *testing.T) {
+ t.Setenv("CARLOS_S3_ENDPOINT", "")
+ harnessFiles(t, "http://203.0.113.10:9000", "dGVzdC1zdW1zLWtleQ==", 0o644)
+ if got := Endpoint(); got != "http://203.0.113.10:9000" {
+ t.Fatalf("Endpoint() = %q, want the file's endpoint", got)
+ }
+ if got := HarnessSTSEndpoint(); got != "http://203.0.113.10:9000" {
+ t.Fatalf("HarnessSTSEndpoint() = %q", got)
+ }
+ var o awss3.Options
+ HarnessS3Options()(&o)
+ if o.BaseEndpoint == nil || *o.BaseEndpoint != "http://203.0.113.10:9000" || !o.UsePathStyle {
+ t.Fatalf("HarnessS3Options did not point the client at the harness store: %+v", o.BaseEndpoint)
+ }
+ if got := publicrelease.Default().PubKey; got != "dGVzdC1zdW1zLWtleQ==" {
+ t.Fatalf("sums key = %q, want the harness test key", got)
+ }
+ if info := HarnessInfo(); !strings.Contains(info, "sums key: dGVzdC1z...") {
+ t.Fatalf("HarnessInfo = %q", info)
+ }
+}
+
+func TestHarnessBuildEnvironmentStillWins(t *testing.T) {
+ harnessFiles(t, "http://203.0.113.10:9000", "k", 0o644)
+ t.Setenv("CARLOS_S3_ENDPOINT", "http://other.example:9000")
+ if got := Endpoint(); got != "http://other.example:9000" {
+ t.Fatalf("Endpoint() = %q, want the variable", got)
+ }
+}
+
+func TestHarnessBuildRefusesWritableFiles(t *testing.T) {
+ t.Setenv("CARLOS_S3_ENDPOINT", "")
+ harnessFiles(t, "http://203.0.113.10:9000", "k", 0o666)
+ if got := Endpoint(); got != "" {
+ t.Fatalf("Endpoint() = %q from a world-writable file, want \"\"", got)
+ }
+ if got := publicrelease.Default().PubKey; got != "" {
+ t.Fatalf("sums key = %q from a world-writable file, want \"\"", got)
+ }
+}
diff --git a/internal/s3compat/s3compat.go b/internal/s3compat/s3compat.go
index 68995019f..e86c19872 100644
--- a/internal/s3compat/s3compat.go
+++ b/internal/s3compat/s3compat.go
@@ -37,8 +37,47 @@ import (
// Endpoint reports the CARLOS_S3_ENDPOINT override, or "" for stock AWS. Read
// per call rather than cached at init so a test — and a process that re-reads
// its config — sees the value it just set.
+//
+// In the harness build (-tags harness, harness.go) an empty variable falls back
+// to the endpoint the deployment harness writes on each of its boxes. Every
+// other build has no fallback: harnessEndpoint stays nil.
func Endpoint() string {
- return strings.TrimSpace(os.Getenv("CARLOS_S3_ENDPOINT"))
+ if ep := strings.TrimSpace(os.Getenv("CARLOS_S3_ENDPOINT")); ep != "" {
+ return ep
+ }
+ if harnessEndpoint != nil {
+ return harnessEndpoint()
+ }
+ return ""
+}
+
+// harnessEndpoint is set only by harness.go, which is compiled only with
+// -tags harness. It is nil in every release build; harness_default_test.go
+// asserts that.
+var harnessEndpoint func() string
+
+// HarnessS3Options is for the two root S3 clients that deliberately ignore
+// CARLOS_S3_ENDPOINT (the claim generation-marker client and the fence
+// writer). In every build but the harness build it is a NO-OP, so those
+// clients keep talking to AWS exactly as before. In the harness build it
+// applies Options, pointing them at the harness's store like every other
+// client.
+func HarnessS3Options() func(*awss3.Options) {
+ return func(o *awss3.Options) {
+ if harnessEndpoint == nil {
+ return
+ }
+ Options()(o)
+ }
+}
+
+// HarnessSTSEndpoint is the STS base endpoint for the fence writer's
+// AssumeRole in the harness build, and "" (use AWS) in every other build.
+func HarnessSTSEndpoint() string {
+ if harnessEndpoint == nil {
+ return ""
+ }
+ return Endpoint()
}
// EndpointHost is the bare host of the override ("" for stock AWS), which is