oficina / sheets Public

Review isolated canary session bootstrap and teardown open

On branch test/canary-session-fixture

Bootstrap design review at1aca44c: REQUEST CHANGES, actual model claude-fable-5-1. No bootstrap code or deployment exists. Creating isolated test/canary-session-fixture for the amended design and later independently reviewed bootstrap, preserving approved watermark candidate unchanged.

Verdict: REQUEST CHANGES on the design at 1aca44c. The bootstrap shape is sound and every framework claim in the spec checks out, but two gaps would make the deployment sequence either fail at step 4 or pass vacuously.

Verified against source (spec claims that hold)

  • Sessions schema is exactly token_hash, subject, method, auth_time, created_at, expires_at; lookup only checks hash and expires_at, never method (rastrillo@v0.23.0/sessions/sessions.go:162-176). identityFrom casts method without validation, and Sheets never reads .Method or uses freshness, so method=fixture rides the unchanged path.
  • Cookie name and attributes match the spec: __Host-rastrillo_session, Path=/, HttpOnly, SameSite=Lax, Secure on https (sessions.go:112-125).
  • Platform ExecBackend.Start execs <live> --socket <rt.Socket> --db <rt.DBPath or DataDir/host.db> (platform/internal/activator/backend_exec.go:557-561, 691); Seed appends --db <path> to manifest argv (:357-370). childEnv never sets STATE_DIRECTORY, and Resolve only joins it for relative paths (rastrillo run.go:90), so "absent is not fatal" is correct. DataDir defaults to /data via CARLOS_DATA_DIR in the edge; root still confirms the route's actual value.
  • memberFor promotes an address on SHEETS_OWNERS to owner at first sight (internal/sheets/members.go:44-50); auth.Signout revokes the row after a same-origin check (auth/handlers.go:118-125).

Blocking gaps

  1. The candidate will not boot on the canary without SMTP env. LoadAuthEnv hard-exits when CARLOS_SMTP_HOST or CARLOS_SMTP_FROM is unset and the origin is not loopback (internal/sheets/auth.go:107-115, cmd/sheets/main.go:73-77). The spec says no SMTP sandbox exists but never says what those two variables hold in the named environment. Step 4's poll would never converge. The spec must state the SMTP values the canary carries, require the bootstrap to run the same LoadAuthEnv so "bootstrap boots" implies "candidate boots", and forbid any fixture check from POSTing /signin, which would send real mail to example.test.
  2. Bootstrap and candidate must carry distinguishable version stamps. make release stamps only ./cmd/sheets with the git short SHA (Makefile:19-25). A bootstrap built from the same commit reports the same /api/version, so step 4's "poll until it equals the candidate SHA" has no failing case, which AGENTS.md names as proving nothing. Add a separate stamped target for the bootstrap, stamp it <sha>-fixture-<runID>, and require step 2 and step 4 to assert the two stamps differ.

Minimal missing guards and tests

  • Empty-precondition table list derived from sqlite_master minus the migration ledger, with a test asserting it covers every table BootSchema creates (currently 15: the 12 Sheets tables plus sessions, auth_links, auth_sessions). Naming tables by hand rots on the next migration.
  • Import-graph test: go list -deps ./cmd/sheets must not include the bootstrap package. This is the mechanical form of "candidate contains no fixture code".
  • Local acceptance test through the real sessions.New and auth.RequireSession: seeded row with cookie is admitted, random cookie is refused, expired fixture row is refused.
  • Path guard tests: refuse a relative --db before any join, a different directory with matching basename, a symlink target, and a --socket other than the compiled one.
  • Idempotency test: identical row is accepted, same hash with a different expiry is refused, and the stored expires_at is unchanged after a repeat.
  • Teardown assertion: after POST /signout, the old cookie on GET /sheets redirects to /signin and the row is gone.

One expected behaviour to record, not change. Removing fixture env at step 3 means a hibernate re-wake of the still-selected bootstrap refuses to boot until the candidate replaces it. That is the fail-closed outcome the design wants, but the runbook should say so, or the wake failure will read as a platform fault.

This review was read-only. Per AGENTS.md §Review, root should record it in the branch discussion with this model attributed. Approval here does not waive the later code, test, and deployment gates.

Fable design APPROVE atc5ebba3, actual model claude-fable-5-1. This closes SMTP/stamp blockers only; runtime path is not yet verified. Root reports actual resolved DB path is not exposed by control-plane route API, so a further narrow preflight evidence amendment follows before any writer implementation. No bootstrap deployment.

Verdict: APPROVE the design at c5ebba3, for design only. Both blockers from the 1aca44c review are closed, and every new claim in revision 2 checks out against pinned source. Code, test and deployment gates remain separate.

Blocker 1 (candidate boot without SMTP): closed.

  • LoadAuthEnv only requires non-empty host and from at a non-loopback origin (internal/sheets/auth.go:107-115). mail.FromEnv builds an smtpSender without dialing, so smtp-fixture.invalid boots cleanly (rastrillo@v0.23.0 mail/mail.go:96-110). Port 587 is also the default. Empty user skips AUTH entirely (mail.go:60).
  • The sign-in mailer is the only mail.Sender consumer in the app. There is no invite or share mail path, so forbidding POST /signin is sufficient to guarantee no dial.
  • Named-environment semantics are as the spec assumes: the named bundle overlays the default bundle per key (platform/internal/appconfig/environments.go:196-201), and carlos email enable delivers CARLOSSMTP into the default bundle (internal/mail/walk.go:818-825), so the canary override wins. An explicit empty value survives the whole chain: parseKV accepts KEY=, ValidVarValue accepts "", RenderEnv emits KEY=, and parseEnvBytes reads it back as "" (internal/activator/admintoken.go:209). childEnv strips every CARLOS_ from the agent's own process env (backend_exec.go:802-814), so nothing inherits from the agent.
  • "Explicitly empty" is the right requirement. Deleting the key from the named bundle would inherit real credentials.

Blocker 2 (indistinguishable stamps): closed. rastrillo.BuildVersion is the ldflags target and /api/version serves it as plain text (rastrillo@v0.23.0 serve.go:63,480). The release-name regex ^[a-zA-Z0-9._-]+$ accepts <sha>-fixture-<runID> as a version name too, and no live platform code compares the served build to the release name, so the suffix cannot break convergence.

Other revision-2 claims verified. Ledger table is rastrillo_migrations (migrate/apply.go:24), and the sqlite_master minus ledger shape is what rastrillo's own adopt check already uses (adopt.go:107). The twelve Sheets tables plus sessions, auth_links, auth_sessions is exactly what BootSchema creates. Resolve joins only relative --db values (run.go:90), so "reject relative before Resolve" is testable as written.

Non-blocking notes for root's pre-implementation verification

  1. The box-local /data/<host>.env overlays <host>.config.env and wins every conflict (backend_exec.go:854-863). "Resolved child environment" must mean reading that file on the box, not only the CLI bundle.
  2. All checks above are against rastrillo v0.23.0 as pinned in go.mod. If the fixture branch bumps it, re-verify sessions schema and mail semantics before trusting this approval.
  3. The fixture-release target should refuse a dirty tree exactly as make release does, or its stamp names a commit that is not in the binary.

This review was read-only. Per AGENTS.md §Review, root should record it in Amadan discussion 21 with this model attributed.

Fable design APPROVE runtime-path preflight amendment at b340a3d, actual model claude-fable-5-1. No code/deployment approval yet. Implementation will use distinct guard codes and a fresh writer runID; root must confirm empty instance seed argv and actual supported log/activation contract. Platform may restore a replica before preflight; only the preflight itself is guaranteed not to materialize a DB. Empty-table writer guard remains mandatory regardless.

Sheets declares no seed argv or schedule in-repo, so the platform only spawns the preflight in the plain --socket --db shape. I have everything needed for the verdict.

Verdict: APPROVE the revision 3 preflight amendment at b340a3d, for design only. Every claim about the platform's exec-child path checks out against the platform checkout at 97eb53f, and the preflight is fail-closed at each step. Code, test and deployment gates remain separate.

Verified against source

  • Argv shape is exactly two flags. Start execs the live symlink with --socket <rt.Socket> --db <rt.DBPath or DataDir/host.db> and nothing else (backend_exec.go:691). No exec child ever receives -addr or LISTEN_FDS. Those belong to the unit-tenant shape only, so refusing them is correct.
  • The DB path really is runtime state. rt.DBPath is a registry column with a narrow SetDBPath writer, so a source default is not evidence. The preflight closes a real gap.
  • The preflight's line reaches root. Child stdout is teed line-wise into the app log sink when the agent has one, and the production agent wires it (agent.go:295). ParseAppLine does no redaction. RedactPaths applies only to platform event text, not app lines, so the absolute DB path survives into carlos logs.
  • A non-serving child is harmless. The health wait fails after 30s, the child is stopped, the lease is released, and the entry is deleted so the next request retries from scratch (activator.go:641). No backoff state, no rollback, no channel change. Deploy itself does not require a healthy boot: WaitHealthyVersion has no production caller.
  • Stdlib-only validation is feasible. DEK is std base64 decoding to 32 bytes; instance key is non-empty; SMTP names are CARLOS_SMTP_HOST/PORT/USER/PASS/FROM; owners and origin are exact string compares. Nothing needs the app or rastrillo.
  • No file or socket open is required. Cleaning, absolute-ness and basename are pure string operations. Reading env and writing to an inherited stdout opens nothing.

Non-blocking notes for the implementation and deployment gates

  1. The platform, not the preflight, may still touch the DB file. Before Start, Restore runs and will pull an S3 replica to the DB path if the local file is absent and a replica exists. The spec's "no database is materialized" is true of the preflight. Say so precisely, so a restored file is not later misread as the writer's work.
  2. Seed shape. If the instance record carried a seed argv and the DB were fresh, the platform would invoke the preflight binary as <seedArgs> --db <path> first. Sheets declares no seed and the console API exposes the field, so root can confirm it is empty. The preflight must exit non-zero on that shape, or an exit 0 would be read as "seeded".
  3. Distinct guard codes. A basename mismatch is the one refusal that would mean the assumption is wrong, yet by design it reports no path. Give each guard its own fixed code so root can tell that case from an SMTP or owner mismatch.
  4. Run ID wording. "One-use" run ID is asserted for both the preflight and the writer. State whether the writer takes a fresh ID or shares the preflight's. Either is safe, since it is a scope guard, but the spec should not say one-use for two builds.
  5. Confirm the log route works before relying on it. Check that carlos logs already shows this canary's app output. If the deployed agent lacks a log sink, the preflight is still harmless but yields no evidence.
  6. Empty vs absent SMTP credentials. childEnv strips every CARLOS_* from the agent's own process env, so an absent CARLOS_SMTP_USER in the child is as safe as an explicitly empty one. Requiring explicit emptiness is stricter than necessary but fail-closed. Keep it if the CLI reliably renders KEY=, as the previous review found.

Platform findings are against the /home/paulca/amadan.net/carlos/platform checkout at 97eb53f, dated 2026-08-31. If the box runs a newer agent, re-check the Start argv line and the log tee before trusting note 3. This review was read-only. Per AGENTS.md §Review, root should record it in Amadan discussion 21 with this model attributed.

Preflight implementation pushed at0b3cfae after approved designb340a3d. Full local make test passed including race and migration checks. Focused tests pin strict environment/activation/build guards, absent STATE_DIRECTORY support, stdlib-only preflight dependencies, candidate importgraph exclusion and no created directories/files. Dirty-tree fixture-preflight-release refuses; clean stamped arm64 build succeeds with a local-only test runID. Exact-head CI is running and code review is queued behind atomic review. No observer deployment, no writer implementation and no candidate deployment.

Fable APPROVE preflight code at0b3cfaefddf4819ef60a327ac046062cf20feabc, actual claude-fable-5-1. Full local make test and exact-head CI green. Native-process smoke also proves no files created, unsupported seed refuses, SMTP mismatch refuses without values: /tmp/sheets-preflight-native-check.json. No writer/candidate/deployment approval implied. Root must verify seedargv/log/socket/current runtime contract and review clean git status including untracked files before build (inherited make release guard ignores untracked files; strengthen before writer target).

Verdict: APPROVE the preflight code at 0b3cfae (diff b340a3d..HEAD). Preflight code only; no writer, candidate or deployment approval implied.

Verified

  • No-secret guarantee holds. The only outputs are fixed fixture-preflight:<code> strings on stderr and a JSON line with version, dbpath, socketpath. Env values are compared but never formatted. No panic path exists: args[0]/args[2] are only indexed after the len(args) != 4 check short-circuits. The db path is rejected on any control character, so the logged line cannot carry injected content. Test covers every guarded key with a sentinel value and asserts it never appears in the error.
  • No-write guarantee holds. main.go makes no file, socket or network call (only LookupEnv, Args, Stdout, Stderr, Exit). Import graph is stdlib-only and the test enforces it; candidate go list -deps contains no fixture package. TempDir test proves no directory or file is created. Confirmed locally: vet clean, all four tests pass; CI record at exact HEAD matches.
  • Shape matches the platform. backend_exec.go:691 at platform 97eb53f execs exactly --socket <rt.Socket> --db <path>, and provision.DefaultSocket is /run/carlos/<host>.sock, which equals the compiled fixtureSocket. Seed shape (seed --db …), addr shape, and LISTEN_FDS all refuse.
  • Spec notes correctly absorb the four design-review notes (restore vs empty state, seed argv, distinct guard codes, fresh writer run ID).

Actionable gaps (non-blocking for this binary, fix before the writer target reuses the pattern)

  1. Dirty-tree check ignores untracked files. git diff --quiet && git diff --cached --quiet passes with an untracked extra.go in the package, which would be compiled into a binary stamped as HEAD. Demonstrated in a scratch repo. Use test -z "$$(git status --porcelain)". The existing release target has the same inherited gap.
  2. Socket is a registry value, not a constant. If the canary was provisioned with a non-default rt.Socket, preflight refuses with code socket. Fail-closed, but root should read that code as "check the registry", not "platform changed argv".
  3. DEK strictness differs from candidate. Candidate trims whitespace before base64 decoding; preflight does not. A DEK stored with a trailing newline passes candidate boot but fails preflight with dek. Stricter, fail-closed; note it so a dek refusal is not misread as a wrong key.
  4. No explicit absent-DEK test. Absence does fail ("" decodes to 0 bytes), but only the mismatch case is tested. Add missing-SHEETS_DEK to the loop for symmetry.

Deployment remains gated on root's seed-argv/log-route/socket checks per spec. Production candidate 1aca44c is unaffected by this diff.

Writer implementation pushed ataca6668 with the exact root-observed paths. Full local make test, focused bootstrap tests and package race pass. Real sessions.New plus auth.RequireSession admits the seeded synthetic session, rejects random/expired tokens; normal auth.Signout deletes it and old cookie redirects. Idempotency never extends expiry; differing hash/expiry or any populated user table refuses. Simultaneous identical starts produce one session.

A mutation removing nonempty-table refusal makes TestEmptyCheckCoversBootSchemaAndFutureTables fail by accepting populated sheets; tracked source unchanged. All three release targets refuse an untracked extra Go file, and clean fixture-release builds with a distinct local-only test stamp. The support package is tools/canaryfixture because adding a second internal app package makes the framework migration tool refuse discovery; no gate was disabled. Candidate dependency check remains green. Fable writer code review now running; CI and no deployment gates remain.

Fable APPROVE writer code at aca6668fa2765eba5c6b70b27f37d1071fbec973, actual claude-fable-5-1. Full local make test, specific fixture race and exact-head CI green. Root deployment still must use a fresh writer scope ID/hash/expiry, observe writer process version/health and non-platform404, remove fixture env before replacing with separately reviewed d35cf94 candidate, then execute authenticated canary checks and normal signout teardown. This certifies session validation, not email delivery. No writer deployment by this agent.

Verification is complete. Writing the verdict.

Verdict: APPROVE the writer code at aca6668 (diff 0b3cfae..HEAD). Code only. No deployment, candidate or email-flow approval implied. Review was read-only; the one probe I ran used a go test -overlay file in $TMPDIR, and the tree is still clean.

Verified

  • Scope isolation holds. Every expected value is a compiled constant in tools/canaryfixture/config.go. Validate checks env, argv shape, socket, absolute cleaned DB path and STATE_DIRECTORY before Resolve runs. Main then compares Resolve's own output against the same constants, so a Resolve join could not slip a different path through. The preflight stamp is refused by the suffix test. Candidate import graph is clean: go list -deps ./cmd/sheets contains no fixture package, and the gate test in the preflight package runs under make test and rejects any dependency containing "fixture".
  • Emptiness guard is real. GLOB prefix exclusion covers the sqliteX case. Coverage is pinned to the 15 BootSchema tables, so any schema change breaks the test and forces re-review. Pre-migration check runs in its own transaction and leaves an old database untouched. Recheck runs inside the insert transaction on the single-connection writer pool, so two in-process starts serialize. Mutation log confirms the count check is load-bearing.
  • Rollback and idempotency. Injected insert failure leaves zero rows. Same hash with a one-second expiry shift is refused with the row unchanged. Concurrent identical starts leave one row.
  • Real session path. The test drives sessions.New, auth.RequireSession and auth.Signout from rastrillo v0.23.0. In the candidate, Authorize is consulted only at magic-link verification (auth/handlers.go:131), never at session resolution, and memberFor promotes the SHEETS_OWNERS address. No Method filter exists anywhere in internal/sheets, so method "fixture" is admitted as an owner.
  • Attack surface. Options.Locales is empty, so buildHandler registers no locale switch route. The process serves only GET /healthz, GET /api/version and an empty mux that 404s. Wrap is nil.
  • Leakage. All refusals are fixed codes. Gorm and framework logs go to io.Discard. Child stderr is teed into the app log at platform backend_exec.go:135, so a guard code reaches carlos logs.
  • Fresh path works end to end. Probe: absent path passes CheckPaths, rdb.Open creates the file, AfterOpen passes, Initialize inserts. No wasted deployment cycle from a false db-absent-after-open.
  • Build gates. All three targets share the porcelain check, untracked source refused per the JSON record, and no .gitignore pattern can hide a .go file. Vet clean, all fixture tests pass at HEAD.

Non-blocking notes for root's writer run

  1. Expiry must end in Z. An RFC3339 value with +00:00 is refused with session-expiry (verified). Generate it with date -u +%Y-%m-%dT%H:%M:%SZ.
  2. /data and /run/carlos must be real directories. EvalSymlinks on the parent must return the parent unchanged. The preflight never probed the filesystem, so this is unverified until the first writer boot. A path-parent refusal means box layout, not tampering.
  3. After the two-hour window, restart refuses. A hibernation re-wake past expiry fails with session-expiry rather than idempotent success. Fail-closed and consistent with spec step 3, but do not read it as a platform fault.
  4. Success is silent. The quiet logger swallows Serve's "serving" line. The only success evidence is the /api/version stamp plus the health route; that matches spec step 2.
  5. Restored replica with empty tables. Emptiness would pass on a replica whose tables exist but hold no rows, and migrate.Apply's adopt path then decides. No content risk, but the ledger state after such a run is worth a glance.

Per AGENTS.md §Review, root should record this in Amadan discussion 21 with this model attributed.

Isolated deployment and authenticated browser gate completed 2026-09-07 ~21:26 UTC. Approved writer aca6668fa2765eba5c6b70b27f37d1071fbec973 was clean-built with fresh run ID r20260907-f0d51843ba611261, deployed only canary/overnight-batch at overnight-sheets.bds.oncarlos.com, environment overnight-batch. Actual GET /api/version returned aca6668-fixture-r20260907-f0d51843ba611261; /healthz 200, /signin and /sheets 404. Exact observed /data/overnight-sheets.bds.oncarlos.com.db and /run/carlos/overnight-sheets.bds.oncarlos.com.sock guards remained intact. Writer could serve only after guarded empty-database initialization succeeded.

Unset all three SHEETSFIXTURERUNID/TOKENHASH/EXPIRES_AT variables, then replaced channel with exact reviewed ordinary d35cf9494fe45bd729722ccb5ca0a52b44f7eb91, built via make release. Actual /api/version d35cf94 verified before successful committed tools/drive/canary-batch.mjs run. Real deployed browser evidence: native 500-cell paste one batch request, 442ms; second-page live update; clear then paste visible; invalid late formula rolls back whole batch; reload persistence; scriptless table; deliberately failed first cut chunk stops remaining queue (one request). No POST /signin or email delivery attempted.

Honest convergence caveat: both CLI 45s deploy watches timed out during adoption/wake. Scoped restart selected only the canary environment/host. Writer root deliberately returns 404; direct /healthz and process-version receipts established success. First browser invocation failed its version guard with transient 'instance failed to wake' and made no editor assertions. Retried only after direct /api/version returned d35cf94; full committed gate then passed. No guard or code changed.

Teardown: normal POST /signout returned 303 /signin; replaying original saved cookie on /sheets also returned 303 /signin. Fixture environment variables verified absent, private raw-token and browser-state files removed. Canary remains ordinary d35cf94; production process remains 7212864 and edge channel unchanged. Synthetic workbook remains isolated in canary. This certifies ordinary session validation and batch behavior, not email delivery.

Evidence: /tmp/sheets-canary-writer-receipt.json; /tmp/sheets-canary-batch-result.json; /tmp/sheets-canary-batch-browser.log; /tmp/sheets-canary-teardown-result.json. Existing independent Claude Fable 5.1 approvals and exact-head green CI remain unchanged. Production deployment remains blocked by source reconciliation and pending user copy review; no production merge/deploy performed.

Coordinated follow-up: isolated shared-theme draft from frozen reconciliation a75c7a9 is tracked in discussion 24, branch feat/estilo-theme. This changes only theme source, dependency transport and asset tests; no bootstrap, canary, session or original candidate changes. Existing #21 canary proof is not represented as proof of this new candidate. Root retains integration and canary ownership.