test: add read-only canary activation observer
.gitignore+2 −0Makefile+9 −0cmd/sheets-fixture-preflight/main.go+99 −0cmd/sheets-fixture-preflight/main_test.go+120 −0docs/superpowers/specs/2026-09-07-canary-fixture-bootstrap.md+3 −3
CI passed — run details
Intent
READ-ONLY focused Fable DESIGN amendment review; no edits/deploys/secret reads. Repo /home/paulca/amadan.net/oficina/sheets-core/.claude/worktrees/overnight-fixture. Read HEAD and git diff c5ebba3..HEAD plus prior APPROVE /tmp/sheets-bootstrap-fable-review2.json. Root verified CLI explicit empty SMTP overrides and configured synthetic transport. Runtime DB path cannot be exposed by route API, so new preflight-only standard-library command validates exact origin/owner/runID/socket/SMTP/keys and absolute cleaned canary DB basename, logs only its unique linker stamp and canary DB/socket args, then exits BEFORE any file/socket open, migration or path probing. Root uses actual argv evidence to compile a separate writer with exact DB guard. No toggle turns observer into writer. Writer keeps all approved path/symlink/emptyDB/session/teardown guards and rastrillo.Resolve+LoadAuthEnv. This preflight catches actual child .env overrides without reading/logging any secret. Assess this narrow evidence phase; explicit APPROVE or REQUEST CHANGES. Later code and deployment gates remain. No application code implemented yet.
Attributed to this commit: 6.54 g CO₂e (360300 tokens). This is an attribution, not a measurement — usage counted from this commit's own marker forward to the next one on test/canary-session-fixture, never backward from it. A turn that produced more than one commit books all of its tokens to the last one, and a concurrent session on this branch can add to the same figure. Some of it was priced using the shared default rate, because no model-specific factor row exists yet for at least one model involved. The Carbon view has the repo total, which is never derived from this attribution.
diff --git a/.gitignore b/.gitignore| index d30a32e..60c268b 100644 |
| --- a/.gitignore |
| +++ b/.gitignore |
| @@ -23,3 +23,5 @@ |
| .claude/worktrees |
| .claude/HANDOFF.md |
| /sheets-arm64 |
| + |
| +/sheets-fixture-preflight-arm64 |
diff --git a/Makefile b/Makefile| index a142a80..1b3084e 100644 |
| --- a/Makefile |
| +++ b/Makefile |
| @@ -90,3 +90,12 @@ test-go: |
| # gets deployed. |
| test-race: |
| go test -race -count=1 ./internal/sheets/store/... ./internal/sheets/live/... |
| + |
| +.PHONY: fixture-preflight-release |
| +fixture-preflight-release: |
| + @git diff --quiet && git diff --cached --quiet || exit 1 |
| + @case "$${FIXTURE_RUN_ID}" in ''|*[!a-z0-9-]*) exit 1;; esac |
| + @test "$${#FIXTURE_RUN_ID}" -ge 8 && test "$${#FIXTURE_RUN_ID}" -le 64 |
| + CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath \ |
| + -ldflags "-s -w -X main.buildVersion=$$(git rev-parse --short HEAD)-fixture-preflight-$${FIXTURE_RUN_ID} -X main.runID=$${FIXTURE_RUN_ID}" \ |
| + -o ./sheets-fixture-preflight-arm64 ./cmd/sheets-fixture-preflight |
diff --git a/cmd/sheets-fixture-preflight/main.go b/cmd/sheets-fixture-preflight/main.go| new file mode 100644 |
| index 0000000..6d8f58d |
| --- /dev/null |
| +++ b/cmd/sheets-fixture-preflight/main.go |
| @@ -0,0 +1,99 @@ |
| +package main |
| + |
| +import ( |
| + "encoding/base64" |
| + "encoding/json" |
| + "errors" |
| + "fmt" |
| + "os" |
| + "path/filepath" |
| + "strings" |
| + "unicode" |
| +) |
| + |
| +const fixtureHost = "overnight-sheets.bds.oncarlos.com" |
| +const fixtureSocket = "/run/carlos/" + fixtureHost + ".sock" |
| +const fixtureOwner = "oficina-fixture@example.test" |
| + |
| +var buildVersion = "dev" |
| +var runID = "" |
| + |
| +type observation struct { |
| + Kind string `json:"kind"` |
| + Version string `json:"version"` |
| + DB string `json:"db_path"` |
| + Socket string `json:"socket_path"` |
| +} |
| + |
| +func main() { |
| + result, err := observe(os.Args[1:], os.LookupEnv, buildVersion, runID) |
| + if err != nil { |
| + fmt.Fprintln(os.Stderr, err) |
| + os.Exit(1) |
| + } |
| + if err = json.NewEncoder(os.Stdout).Encode(result); err != nil { |
| + os.Exit(1) |
| + } |
| +} |
| + |
| +func observe(args []string, env func(string) (string, bool), version, id string) (observation, error) { |
| + fail := func(code string) (observation, error) { return observation{}, errors.New("fixture-preflight:" + code) } |
| + if !validRunID(id) { |
| + return fail("run-id-build") |
| + } |
| + suffix := "-fixture-preflight-" + id |
| + sha, ok := strings.CutSuffix(version, suffix) |
| + if !ok || len(sha) < 7 || len(sha) > 40 || strings.IndexFunc(sha, func(c rune) bool { return !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f') }) >= 0 { |
| + return fail("version-build") |
| + } |
| + for _, check := range []struct{ key, want, code string }{ |
| + {"SHEETS_ORIGIN", "https://" + fixtureHost, "origin"}, |
| + {"SHEETS_OWNERS", fixtureOwner, "owner"}, |
| + {"SHEETS_FIXTURE_RUN_ID", id, "run-id-env"}, |
| + {"CARLOS_SMTP_HOST", "smtp-fixture.invalid", "smtp-host"}, |
| + {"CARLOS_SMTP_FROM", fixtureOwner, "smtp-from"}, |
| + {"CARLOS_SMTP_PORT", "587", "smtp-port"}, |
| + {"CARLOS_SMTP_USER", "", "smtp-user"}, |
| + {"CARLOS_SMTP_PASS", "", "smtp-pass"}, |
| + } { |
| + value, present := env(check.key) |
| + if !present || value != check.want { |
| + return fail(check.code) |
| + } |
| + } |
| + key, _ := env("SHEETS_INSTANCE_KEY") |
| + if strings.TrimSpace(key) == "" { |
| + return fail("instance-key") |
| + } |
| + dek, _ := env("SHEETS_DEK") |
| + decoded, err := base64.StdEncoding.DecodeString(dek) |
| + if err != nil || len(decoded) != 32 { |
| + return fail("dek") |
| + } |
| + if value, _ := env("LISTEN_FDS"); value != "" { |
| + return fail("listen-fds") |
| + } |
| + if len(args) != 4 || args[0] != "--socket" || args[2] != "--db" { |
| + return fail("argv-shape") |
| + } |
| + if args[1] != fixtureSocket { |
| + return fail("socket") |
| + } |
| + db := args[3] |
| + if !filepath.IsAbs(db) { |
| + return fail("db-relative") |
| + } |
| + if filepath.Clean(db) != db || strings.IndexFunc(db, unicode.IsControl) >= 0 { |
| + return fail("db-unclean") |
| + } |
| + if filepath.Base(db) != fixtureHost+".db" { |
| + return fail("db-basename") |
| + } |
| + return observation{Kind: "fixture-preflight", Version: version, DB: db, Socket: args[1]}, nil |
| +} |
| + |
| +func validRunID(id string) bool { |
| + return len(id) >= 8 && len(id) <= 64 && strings.IndexFunc(id, func(c rune) bool { |
| + return !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '-') |
| + }) < 0 |
| +} |
diff --git a/cmd/sheets-fixture-preflight/main_test.go b/cmd/sheets-fixture-preflight/main_test.go| new file mode 100644 |
| index 0000000..5b12118 |
| --- /dev/null |
| +++ b/cmd/sheets-fixture-preflight/main_test.go |
| @@ -0,0 +1,120 @@ |
| +package main |
| + |
| +import ( |
| + "encoding/base64" |
| + "os" |
| + "os/exec" |
| + "path/filepath" |
| + "strings" |
| + "testing" |
| +) |
| + |
| +const testRunID = "preflight-test-1234" |
| +const testVersion = "abcdef1-fixture-preflight-" + testRunID |
| + |
| +func fixtureEnv() map[string]string { |
| + return map[string]string{ |
| + "SHEETS_ORIGIN": "https://" + fixtureHost, "SHEETS_OWNERS": fixtureOwner, "SHEETS_FIXTURE_RUN_ID": testRunID, |
| + "CARLOS_SMTP_HOST": "smtp-fixture.invalid", "CARLOS_SMTP_FROM": fixtureOwner, "CARLOS_SMTP_PORT": "587", |
| + "CARLOS_SMTP_USER": "", "CARLOS_SMTP_PASS": "", "SHEETS_INSTANCE_KEY": "synthetic-instance-key", |
| + "SHEETS_DEK": base64.StdEncoding.EncodeToString(make([]byte, 32)), |
| + } |
| +} |
| +func lookup(values map[string]string) func(string) (string, bool) { |
| + return func(key string) (string, bool) { value, ok := values[key]; return value, ok } |
| +} |
| +func argsFor(db string) []string { return []string{"--socket", fixtureSocket, "--db", db} } |
| + |
| +func TestPreflightReportsOnlyGuardedActivationWithoutCreatingFiles(t *testing.T) { |
| + root := t.TempDir() |
| + db := filepath.Join(root, "does-not-exist", fixtureHost+".db") |
| + env := fixtureEnv() |
| + env["STATE_DIRECTORY"] = "/unrelated/runtime/value" |
| + result, err := observe(argsFor(db), lookup(env), testVersion, testRunID) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if result.DB != db || result.Socket != fixtureSocket || result.Version != testVersion { |
| + t.Fatal("wrong observation") |
| + } |
| + entries, err := os.ReadDir(root) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if len(entries) != 0 { |
| + t.Fatal("preflight created a file or directory") |
| + } |
| + delete(env, "STATE_DIRECTORY") |
| + if _, err = observe(argsFor(db), lookup(env), testVersion, testRunID); err != nil { |
| + t.Fatal(err) |
| + } |
| +} |
| + |
| +func TestPreflightRefusesGuardMismatchesWithoutLeakingValues(t *testing.T) { |
| + for _, key := range []string{"SHEETS_ORIGIN", "SHEETS_OWNERS", "SHEETS_FIXTURE_RUN_ID", "CARLOS_SMTP_HOST", "CARLOS_SMTP_FROM", "CARLOS_SMTP_PORT", "CARLOS_SMTP_USER", "CARLOS_SMTP_PASS", "SHEETS_DEK"} { |
| + t.Run(key, func(t *testing.T) { |
| + env := fixtureEnv() |
| + env[key] = "sensitive-value-must-not-appear" |
| + _, err := observe(argsFor("/data/"+fixtureHost+".db"), lookup(env), testVersion, testRunID) |
| + if err == nil || strings.Contains(err.Error(), env[key]) { |
| + t.Fatal("guard accepted mismatch or exposed its value") |
| + } |
| + }) |
| + } |
| + for _, key := range []string{"SHEETS_INSTANCE_KEY", "CARLOS_SMTP_USER", "CARLOS_SMTP_PASS"} { |
| + t.Run("missing-"+key, func(t *testing.T) { |
| + env := fixtureEnv() |
| + delete(env, key) |
| + if _, err := observe(argsFor("/data/"+fixtureHost+".db"), lookup(env), testVersion, testRunID); err == nil { |
| + t.Fatal("missing guard accepted") |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestPreflightRefusesUnsupportedActivationAndBuild(t *testing.T) { |
| + env := fixtureEnv() |
| + for _, args := range [][]string{ |
| + {"seed", "--db", "/data/" + fixtureHost + ".db"}, |
| + {"--db", "/data/" + fixtureHost + ".db"}, |
| + {"--socket", "/run/carlos/other.sock", "--db", "/data/" + fixtureHost + ".db"}, |
| + {"--socket", fixtureSocket, "--addr", ":8080"}, |
| + argsFor(fixtureHost + ".db"), argsFor("/data/../data/" + fixtureHost + ".db"), argsFor("/data/other.db"), |
| + argsFor("/data/line\nbreak/" + fixtureHost + ".db"), |
| + } { |
| + if _, err := observe(args, lookup(env), testVersion, testRunID); err == nil { |
| + t.Fatal("unsupported activation accepted") |
| + } |
| + } |
| + for _, version := range []string{"dev", "abcdef1", "abcdef1-fixture-other-" + testRunID, testVersion + "-different"} { |
| + if _, err := observe(argsFor("/data/"+fixtureHost+".db"), lookup(env), version, testRunID); err == nil { |
| + t.Fatal("unstamped build accepted") |
| + } |
| + } |
| + if _, err := observe(argsFor("/data/"+fixtureHost+".db"), lookup(env), testVersion, ""); err == nil { |
| + t.Fatal("unscoped build accepted") |
| + } |
| + env["LISTEN_FDS"] = "1" |
| + if _, err := observe(argsFor("/data/"+fixtureHost+".db"), lookup(env), testVersion, testRunID); err == nil { |
| + t.Fatal("unit activation accepted") |
| + } |
| +} |
| + |
| +func TestPreflightAndCandidateImportGraphsRemainSeparate(t *testing.T) { |
| + output, err := exec.Command("go", "list", "-deps", "-f", "{{if not .Standard}}{{.ImportPath}}{{end}}", ".").CombinedOutput() |
| + if err != nil { |
| + t.Fatalf("preflight dependencies: %v", err) |
| + } |
| + for _, name := range strings.Fields(string(output)) { |
| + if name != "amadan.net/oficina/sheets/cmd/sheets-fixture-preflight" { |
| + t.Fatalf("preflight imports nonstdlib package %s", name) |
| + } |
| + } |
| + output, err = exec.Command("go", "list", "-deps", "../sheets").CombinedOutput() |
| + if err != nil { |
| + t.Fatalf("candidate dependencies: %v", err) |
| + } |
| + if strings.Contains(string(output), "fixture") { |
| + t.Fatal("candidate depends on fixture code") |
| + } |
| +} |
diff --git a/docs/superpowers/specs/2026-09-07-canary-fixture-bootstrap.md b/docs/superpowers/specs/2026-09-07-canary-fixture-bootstrap.md| index 3b87faf..63ae6f3 100644 |
| --- a/docs/superpowers/specs/2026-09-07-canary-fixture-bootstrap.md |
| +++ b/docs/superpowers/specs/2026-09-07-canary-fixture-bootstrap.md |
| @@ -1,6 +1,6 @@ |
| # Isolated Sheets canary session fixture |
| -Status: revision 3; Fable 5.1 (claude-fable-5-1) approved revision 2 at c5ebba3. The runtime-path preflight evidence amendment below requires focused review before implementation. Isolated branch test/canary-session-fixture, Amadan discussion 21. Do not deploy yet. Normal SMTP-based sign-in remains preferred and root has asked the user about a controlled mailbox. This design is only for testing unchanged session validation and editor behavior if that mailbox is unavailable. |
| +Status: revision 3 design approved at b340a3d by Fable 5.1 (claude-fable-5-1), following revision 2 approval at c5ebba3. Preflight code requires separate review; no deployment approval yet. Isolated branch test/canary-session-fixture, Amadan discussion 21. Do not deploy yet. Normal SMTP-based sign-in remains preferred and root has asked the user about a controlled mailbox. This design is only for testing unchanged session validation and editor behavior if that mailbox is unavailable. |
| ## Target and boundary |
| @@ -25,9 +25,9 @@ Root has confirmed the named environment supports explicit empty values and conf |
| Build cmd/sheets-fixture-preflight with a clean-tree fixture-preflight-release target and a unique <sha>-fixture-preflight-<runID> stamp. It uses only standard-library dependencies and its own linker-stamped buildVersion, since importing the app/framework would pull database code into the preflight import graph. It parses the documented two-flag exec-child shape itself; the writer still uses rastrillo.Resolve. It contains no database-open, migration, seed, socket-listen or filesystem-probing path. It validates the compiled exact origin, synthetic owner, one-use run ID, SMTP settings and key formats against the actual child environment. This catches an unexpected box-local .env override without opening or printing that file or any secret value. A mismatch refuses with a fixed guard code. It parses the activation args, rejects relative/unclean DB paths before Resolve could join them, requires the exact known canary socket and exact canary DB basename, and refuses addr/LISTEN_FDS or other unsupported shapes. |
| -After these guards pass, report only the preflight stamp, validated canary DB argument and socket argument to the operator log, then exit. No file or socket is opened, no database is materialized, no environment changes, no HTTP route is served, and no app assertion runs. Root reads only this instance's supported logs and records that one observed path. The activation may report unsuccessful readiness because preflight deliberately exits without serving; that expected result is not a failed candidate test. Repeated activation only repeats the same guarded read-only observation. |
| +After these guards pass, report only the preflight stamp, validated canary DB argument and socket argument to the operator log, then exit. The preflight opens no file or socket, materializes no database, changes no environment, serves no HTTP route and runs no app assertion. The platform may restore an existing replica before starting it; that does not establish an empty database and never waives the writer empty-state guard. Root confirms the instance seed argv is empty and the supported canary log route works, then reads only this instance's logs and records that one observed path. Unsupported seed invocation must exit nonzero. The activation may report unsuccessful readiness because preflight deliberately exits without serving; that expected result is not a failed candidate test. Repeated activation only repeats the same guarded read-only observation. |
| -Root reviews the observed absolute path and confirms it is this canary's path. Compile that exact DB path into a different startup-only writer binary. It retains all symlink/parent/empty-database/transaction/session guards below. There is no env flag that turns the permissive path-observer into a writer. Candidate and writer never learn their expected DB paths from caller-controlled fixture environment. Remove the preflight channel reference when promoting the writer. Test that preflight cannot create a DB or socket and that its package has no database/migration/seed dependencies; the candidate import-graph gate excludes both fixture commands/packages. No generic shell, directory listing or path probing across tenants is part of this sequence. |
| +Root reviews the observed absolute path and confirms it is this canary's path. The writer receives a fresh run ID distinct from the preflight scope ID. Compile that exact DB path into a different startup-only writer binary. It retains all symlink/parent/empty-database/transaction/session guards below. There is no env flag that turns the permissive path-observer into a writer. Candidate and writer never learn their expected DB paths from caller-controlled fixture environment. Remove the preflight channel reference when promoting the writer. Test that preflight cannot create a DB or socket and that its package has no database/migration/seed dependencies; the candidate import-graph gate excludes both fixture commands/packages. No generic shell, directory listing or path probing across tenants is part of this sequence. |
| ## Candidate boot and transport isolation |