step.29.txt -- b3ubot U5.7: THE MULTI-HOME DISCIPLINE + the second real account -- scripts/b3ubot_profile.sh, the per-account profile wrapper that binds a data home (~/.b3ubot.N via $B3UBOT_HOME) to the credentials living INSIDE that home (~/.b3ubot.N/profile.env, 0600-enforced), so two staging accounts coexist on one dev box with NO invocation surface for mis-pairing; plus the operator ceremony that creates the plan's A3 actor (actor@example.com, real web signup + 4242 checkout) and proves each profile serves its OWN account. Closes the lifecycle plan's TC-3 / TC-14 / TC-15 / TC-16. (p1.1.4) of the user's (p1.1) plan. Step: 29 -- promote UoW **U5.7** (added BY AMENDMENT via the engine's own `ledger add-row`, the U5.4a/U5.6 precedent) to execution. Parent: design.md §2 P5 (the CCS trinity); b3u docs/test_plan_account_lifecycle.md §6 (TC-14..16, pre-named at UB.5 for exactly this step) + TC-3. UoW: U5.7 -- "Multi-home profiles + the A3 account". Date: 2026-07-24 Origin: User directive 2026-07-24: "proceed with (p1.1.4)" -- per the (p1.1) plan of 2026-07-23: sign up actor@example.com with the same 4242 card; resolve multi-account-on-one-host via ~/.b3ubot.1 (alex) and ~/.b3ubot.2 (actor). ## (0) Status DRAFTED for same-session execution. Phase A (wrapper + hermetic tests + gates) is machine work; Phase B (the A3 browser signup + checkout, the profile.env files -- they hold the operator's real passwords -- and the two-profile witness) is an OPERATOR ceremony. The step stays open until the operator declares it and the gates agree (C-27-B/C-53-C: a machine may not declare a human-witnessed gate green). ## (1) Ground truth probed at drafting (outputs -> named writers) EXISTS ($B3UBOT_HOME): the engine-wide data-home override -- app/egress.py:31 `Path(os.environ.get("B3UBOT_HOME", "~/.b3ubot")).expanduser()`; persistence.py names the same convention for per-project engine state; every engine artifact (orchestrator/, runs/, *.jsonl) lives under it. ~/.b3ubot.1 EXISTS (the U5.6 remote-ceremony home, UC.1 closed inside); ~/.b3ubot.2 ABSENT. EXISTS (account identity): B3uDevClient reads $B3U_EMAIL / $B3U_PASSWORD (env-only, B-4) and the CF Access pair $CF_ACCESS_CLIENT_ID/_SECRET (C-28-B) at __init__; the plane serves WHOEVER the creds name -- identity follows the environment, NOT the home. That is exactly TC-16's hazard: A1's home + A3's creds would silently serve A3. EXISTS (creds-file conventions): ~/.b3u_staging_creds.env + ~/.b3u_cf_smoke.env (0600, outside all repos, sourced by the operator; remote_gates.sh REFUSES naming missing vars). The CF service token is PER-HOST wall admission, not per-account identity -- shareable across profiles. EXISTS (the ceremony surface): `b3ubot ccs status` -- plane health + THIS account's entitlement row, no args -- the exact per-profile witness op. POST /billing/checkout (b3u main.py:522, outside the frozen contract v1) is the F-2 shell checkout path used at the UB.4 ceremony: login -> checkout -> {url} -> browser -> 4242. EXISTS (the plane, per the lifecycle plan's log): 2 accounts -- operator@example.com (A1, sub active) + actor-clock@example.com (A2, canceled/expired); actor@example.com (A3) ABSENT. Same-card facts asserted by plan §3 (email is the PK; one card backs many customers). ABSENT: any profile wrapper; any per-home creds file; any test touching $B3UBOT_HOME-vs-identity pairing. ## (2) What U5.7 delivers A. scripts/b3ubot_profile.sh [command...] -- the wrapper: - resolves HOME_DIR = ${B3UBOT_PROFILE_BASE:-$HOME}/.b3ubot.N (the PROFILE_BASE override exists for hermetic tests ONLY); N is digits-only -- there are NO flags, NO creds arguments (C-29-B: mis-pairing gets no invocation surface); - REFUSES (exit 2, values never echoed): missing profile.env; permissions other than 0600; profile.env that sets no B3U_EMAIL or no B3U_PASSWORD (names the KEY); - sources ${B3UBOT_CF_ENV:-~/.b3u_cf_smoke.env} if present (wall admission, per-host) THEN $HOME_DIR/profile.env (identity, per-account -- sourced LAST so the profile wins every overlap, including a polluted outer environment); - exports B3UBOT_HOME=$HOME_DIR and execs the command; with no command prints a non-secret summary (home, email, base URL, set-flags -- the password value NEVER). B. tests/test_profile_wrapper.py -- hermetic subprocess tests (scratch B3UBOT_PROFILE_BASE + B3UBOT_CF_ENV, fake creds): refusals (missing file / loose perms / missing key / flag-like or non-numeric N), home export, profile-overrides-outer-env, two-profile isolation, summary-never-prints-password, CF pair sourced when present / absent-file OK. C. scripts/multihome_gates.sh -- Phase A: legs U (the refusal battery, live), ISO (two scratch profiles -> distinct homes + each child sees its OWN identity, outer-env pollution overridden), PY (the new test file, hermetic), B4 (no scratch secret appears in any gate artifact). Boots nothing, touches no real plane (C-28-C); green prints the Phase B ceremony. D. Phase B -- the OPERATOR ceremony (TC-3 + TC-15 + TC-16 live): 1. ~/.b3ubot.1/profile.env (alex creds + staging base), 0600; 2. browser signup actor@example.com on staging -> gmail verify link -> shell checkout THROUGH THE WRAPPER (profile 2's env supplies creds + CF; F-2 path) -> 4242; 3. ~/.b3ubot.2/profile.env (actor creds), 0600 -- written BEFORE the checkout so the wrapper itself runs it; 4. `b3ubot_profile.sh 1 ... ccs status` -> alex's account; `b3ubot_profile.sh 2 ... ccs status` -> actor's; the polluted leg: alex creds SOURCED in the outer shell, profile 2 still serves actor (TC-16 witnessed live). E. TC-14 census (machine, ONE ssh -- the step.53 law): droplet accounts + modeled clients + Stripe customers/subs + default-pm card FINGERPRINTS -- A1/A3 fingerprints EQUAL (same card), customers/subs distinct, A1+A2 state untouched by A3's arrival. Ad-hoc heredoc this step; promotion candidate named (Q-29-C). F. Ledger: U5.7 by amendment at SKELETON (engine add-row), done at close (engine ledger close). b3u lifecycle plan §6 flips ride b3u commits as UB.5 plan execution (the LIVE-doc charter), NOT a new b3u step. Zero engine-code changes (C-29-C). ## (3) Verification gates G-A (Phase A, machine): multihome_gates.sh ALL PASS; full suite green, measured AFTER the last edit; [T1:pytest]. G-B (Phase B, ceremony): both profiles serve their OWN accounts via ccs status (distinct client/email identity), including the polluted-outer-env leg; closed by legs + the operator's explicit word (C-27-B/C-53-C), never by the machine alone. G-C (the plane): TC-14 census -- 3 accounts / 3 customers, A1 vs A3 same card fingerprint, distinct subscriptions, no cross-talk; alex + clockco rows byte-stable vs the UB.5 log. G-D (the plan): TC-3/TC-14/TC-15/TC-16 flipped with dated execution-log entries in the b3u lifecycle plan, same-commit as the runs, citing this step as evidence. ## (4) LOCKs C-29-A profile.env is a CREDENTIAL file: 0600 enforced by the wrapper, values never echoed by wrapper/gates/tests; real files live under ~/.b3ubot.N/ outside all repos; tests use fake creds under scratch dirs only. C-29-B identity comes from INSIDE the home: the wrapper takes no creds argument, no env-file argument, no flags -- the home IS the account selector; profile.env is sourced LAST so it beats any outer environment. C-29-C zero engine-code changes: the wrapper CONSUMES the existing $B3UBOT_HOME + env-only-creds contracts (egress.py, client.py); nothing under app/ changes. (inherited) B-4 / C-28-B env-only credentials; C-28-C gates boot nothing; C-27-B/C-53-C operator-closed ceremony; C-45-B never-push; one-ssh-call law (b3u step.53). ## (5) Open questions Q-29-A A3's client name: the operator picks it at signup (the plan's "TBD at (p1.1.4)"). LEAN: uafco. Recorded in the plan §2 at ceremony close. Q-29-B Where does B3U_BASE_URL live? LEAN: in profile.env, per-profile and explicit -- the wrapper defaults NOTHING (absent base = the client's own loopback default), so the same wrapper drives local-mode profiles unchanged. Q-29-C The TC-14 census: ad-hoc one-ssh heredoc this step; promotion candidate `b3u scripts/account_census.sh` (purpose: population snapshot as the plane grows; rationale: fresh_start_check's one-account claim is already superseded) -- deferred to the UB.6-era when account management becomes product work. ## (6) Acceptance [x] Wrapper + tests + gates landed; suite green measured after the last edit (G-A) [x] Phase A ALL PASS, recorded in §(7) (G-A) [x] Phase B ceremony: A3 live (signup/verify/4242); two profiles serve two accounts; polluted-env leg; operator's word (G-B) [x] TC-14 census: same card, no cross-talk, A1/A2 untouched (G-C) [x] Plan flips TC-3/14/15/16 committed b3u-side (G-D) [x] Ledger U5.7 active at SKELETON, done at close; zero engine-code changes (F, C-29-C) ## (7) Hash backfill SKELETON commit: 66f8d6b WORK commit: 4e92036 -- suite 660+28=688 measured after the last edit (644 baseline + 12 wrapper tests + 4 corpus pickups of this file). Two live-fire findings AT BUILD: (1) GNU env's -u flags must precede NAME=VALUE assignments or env execs "-u" as the command; (2) the corpus round-trip test caught this skeleton's INTERNAL banner wrapped off the ledger emitter's canonical form -- the suite guards even the step files. Phase A run: 2026-07-24, ALL PASS first run (commit b8c3797) -- U refusal battery 5/5 (no-arg, flag-like, missing profile.env, 0644 perms, keyless), ISO 2/2 (distinct homes/identities; polluted outer env overridden -- TC-15/TC-16 mechanics), PY 12/12 hermetic, B4 clean. Phase B close: 2026-07-24, operator-run and operator-reported (the pasted legs ARE the operator's word, C-27-B satisfied). A3 born live: real signup actor@example.com / client uafco + gmail verify; checkout run THROUGH profile 2's own wrapper env (~/a3_checkout.sh one-shot, the Q-28-E no-paste-hazard pattern -- deleted after); Stripe Link intercepted with the test-mode 000000 code; sub_1Two8X.. active. Witness legs: profile 1 -> alex_urakhchin/ betthreetesting active; profile 2 -> actor/ uafco active; polluted leg (alex creds sourced in the outer shell) STILL served uafco -- TC-16 live. TWO ceremony live-fire findings: (a) the chat paste channel injected a newline inside a curl -H header (Q-28-E third edition -- the one-shot script file is now the DEFAULT ceremony vehicle, not the fallback); (b) the helper's f-string \" escapes broke the SYSTEM python3 (pre-3.12 rules) while the venv's 3.12 accepted them -- ceremony helpers must target the oldest python on the box ("%s" formatting). Census run: 2026-07-24 (TC-14, one-ssh x2 + cleanup): 3 accounts / 3 modeled clients; alex active (period_end 2026-08-23), clockco canceled (2026-09-23), uafco active (2026-08-24); Stripe customers distinct, subs isolated; A1/A2 byte-stable vs the UB.5 log. Nuances: clock-backed customers are EXCLUDED from default Customer.list (A2 visible only via its clock -- census must query per-clock); newer Stripe API has no invoice.charge (use Charge.list(customer=..)); Stripe LINK hides last4/fingerprint on A1+A3 charges -> the same-card claim rests on the operator-witnessed 4242 entries, with A2's raw-card charges (4242, one fingerprint) as the control (plan F-6); one ORPHAN 0-customer lifecycle-plan clock (step.54's failed-run leftover -- its recorded deletion evidently hit the wrong twin) found and deleted. b3u plan flips: commit f740edb. RETRO commit: (retro pair -- see step.29.diff.txt)