#30: per-session folder layout + README + sessions.json
Restructures the flat repo into one folder per session, with a top-level README.md (the "about sessions" page, also rendered at /docs/sessions/about by rfc-app v0.19.0+) and a sessions.json title manifest keyed by 4-digit NNNN. Filenames inside each folder retain the full SESSION-NNNN.M-TRANSCRIPT-… form (no trim of the SESSION- prefix). Legacy letter-form transcripts, if any are still at the root, are not moved — the folder convention applies to the numeric form only. Session 0017.0; see SESSION-PROTOCOL.md §1 amendment for the binding shape.
This commit is contained in:
@@ -0,0 +1,667 @@
|
||||
# Session E — Transcript
|
||||
|
||||
> Date: 2026-05-27 → 2026-05-28
|
||||
> Goal: Execute Slices 1 and 2 of the `ohm-rfc-app-flotilla` SPEC §20
|
||||
> slicing plan. Slice 1 = registry + non-secret overlay (no actuation).
|
||||
> Slice 2 = secret references + Secret Manager bootstrap doc.
|
||||
>
|
||||
> Outcome: **Slice 1 shipped at v0.1.0** — committed (`581687a`) and
|
||||
> tagged locally; not yet pushed. Python package skeleton, SQLite
|
||||
> schema (migration 001), seven CLI verbs (`deployment {list, show,
|
||||
> add, remove}` + `overlay {show, set, unset}`), Gitea Actions CI,
|
||||
> register-ohm.sh as the executable form of SPEC §13, 15 pytest tests
|
||||
> green. **Slice 2 code shipped in working tree, uncommitted, awaiting
|
||||
> the operator-side Secret Manager bootstrap gesture before v0.2.0
|
||||
> cut** — migration 002 (`secret_refs`), the `secrets` group of CLI
|
||||
> verbs (`bind`/`unbind`/`list`), a Secret Manager read wrapper,
|
||||
> `docs/secret-bootstrap.md` as the operator runbook, 35 pytest tests
|
||||
> green. CHANGELOG block reads "0.2.0 — pending operator gesture";
|
||||
> `VERSION` deliberately left at `0.1.0`.
|
||||
>
|
||||
> Two implementation decisions settled: CLI uses **Click** (not Typer),
|
||||
> CI uses **Gitea Actions** (canonical remote is git.wiggleverse.org).
|
||||
> Migration runner pattern mirrors `rfc-app/backend/app/db.py` — raw
|
||||
> `.sql` files, filename stem stored in `schema_migrations.version`.
|
||||
> Spec corrections (per §19.3 rule 2) normalized every `flotilla …
|
||||
> ohm` reference in §20.1–§20.5 to `ohm-rfc-app` (the bound name from
|
||||
> §13.1).
|
||||
>
|
||||
> Two harness-level issues surfaced and were fixed in-session: Click
|
||||
> 8.2 removed the `mix_stderr` kwarg from `CliRunner` (broke Slice 1
|
||||
> tests on first run); Slice 1's `test_open_db_is_idempotent` asserted
|
||||
> a literal migration count of `1` and broke when Slice 2 added
|
||||
> migration 002 (replaced with a no-re-apply invariant). Both were
|
||||
> root-caused and fixed rather than worked around.
|
||||
|
||||
---
|
||||
|
||||
## Turn 1 — User
|
||||
|
||||
> run the session based on what's in memory
|
||||
|
||||
## Turn 1 — Assistant
|
||||
|
||||
Read the project memory at
|
||||
`/Users/benstull/.claude/projects/-Users-benstull-projects-wiggleverse-ohm-rfc-app-flotilla/memory/`
|
||||
to recover context: `MEMORY.md` (the index),
|
||||
`project_ohm_rfc_app_flotilla.md` (this repo's framing, with the
|
||||
§20 slicing plan and the standalone-tool-not-library commitment),
|
||||
`project_open_human_model.md` (OHM = corpus, RFC = process),
|
||||
`project_wiggleverse.md` (founder/org/spec split), and
|
||||
`project_ohm_deployment_host.md` (live URL is `ohm.wiggleverse.org`,
|
||||
deprovisioned host was `rfc.wiggleverse.org`).
|
||||
|
||||
Memory was unambiguous: v0.0.1 ships only `SPEC.md`; the §20 slicing
|
||||
plan names Slice 1 as the next session ("Subsequent build sessions
|
||||
pick a slice off this list"). User's prompt "run the session based on
|
||||
what's in memory" was parsed as "execute Slice 1."
|
||||
|
||||
Read repo state:
|
||||
|
||||
```
|
||||
$ ls -la
|
||||
SPEC.md 70774 bytes
|
||||
CHANGELOG.md 644 bytes
|
||||
README.md 773 bytes
|
||||
LICENSE 1066 bytes
|
||||
VERSION 6 bytes (0.0.1)
|
||||
.git/
|
||||
|
||||
$ git log --oneline -10
|
||||
a9a9aaa Release 0.0.1: initial spec
|
||||
```
|
||||
|
||||
Read SPEC §20 in full (1318–1479) — confirmed Slice 1 scope:
|
||||
Python project skeleton with Click entrypoint, SQLite schema per §4.3,
|
||||
seven CLI verbs (`deployment add/list/show/remove`, `overlay
|
||||
set/unset/show`), register OHM per §13.1–§13.6 (everything except
|
||||
secrets and pin reading), pytest round-trip tests. Exit criteria:
|
||||
`flotilla deployment show ohm-rfc-app` and `flotilla overlay show
|
||||
ohm-rfc-app` render the OHM record. Ships as v0.1.0.
|
||||
|
||||
Spotted a spec inconsistency: §20.1 exit criteria reads `flotilla
|
||||
deployment show ohm` then `flotilla overlay show ohm-rfc-app` — two
|
||||
different names for the same deployment. §13.1 binds `name =
|
||||
ohm-rfc-app`. Per §19.3 rule 2, flagged for in-session correction.
|
||||
|
||||
Two implementation choices remained per the spec: CLI library (§12
|
||||
says "Click or Typer; either is fine — this is an implementation
|
||||
choice the Slice 1 session settles") and CI platform (§20.1 calls for
|
||||
a "CI scaffold" but doesn't say which). Asked the user via
|
||||
AskUserQuestion, recommending Click + Gitea Actions (canonical
|
||||
remote is git.wiggleverse.org).
|
||||
|
||||
## Turn 2 — User
|
||||
|
||||
Both recommendations accepted:
|
||||
- CLI lib: **Click**
|
||||
- CI scaffold: **Gitea Actions**
|
||||
|
||||
## Turn 2 — Assistant
|
||||
|
||||
Dispatched one Explore agent to survey `rfc-app` for patterns to
|
||||
mirror — project layout, version-file format, migration runner shape,
|
||||
`schema_migrations` columns, test layout, CI presence, CLAUDE.md
|
||||
conventions, CLI framework (if any). In parallel, read SPEC §12 in
|
||||
full to confirm CLI surface details (`--json` on every read verb,
|
||||
errors to stderr + non-zero exit).
|
||||
|
||||
Explore agent report (under 400 words):
|
||||
|
||||
- rfc-app is TypeScript frontend + Python (FastAPI) backend, no CLI
|
||||
- VERSION file at root, mirrored in `frontend/package.json#version`
|
||||
- Migrations: numbered `.sql` files in `backend/migrations/`
|
||||
(`001_users_and_audit.sql` … `011_allowlist.sql`); hand-rolled
|
||||
runner at `backend/app/db.py:34-54`; `schema_migrations` table is
|
||||
`(version TEXT PRIMARY KEY, applied_at TEXT DEFAULT datetime('now'))`
|
||||
with `version` storing the filename stem
|
||||
- pytest, no conftest.py at root, tests in `backend/tests/`
|
||||
- No CI in rfc-app — the user has no precedent to follow there
|
||||
- CLAUDE.md exists at `/Users/benstull/git/rfc-app/CLAUDE.md` with
|
||||
separation-of-concerns + VERSION mirroring rules
|
||||
- No CLI framework in rfc-app
|
||||
|
||||
Entered plan mode (`EnterPlanMode`) and wrote a structured plan to
|
||||
`/Users/benstull/.claude/plans/snappy-cuddling-zebra.md`. Plan covered:
|
||||
repo bootstrap (`pyproject.toml`, package skeleton, Click entrypoint,
|
||||
Python `>=3.12`), DB layer (mirror rfc-app's runner shape), migration
|
||||
001 (deployments + overlay_entries + schema_migrations, including
|
||||
`target_vm_tunnel_through_iap` from §13.2 even though §4.3's
|
||||
illustrative list omits it), registry as pure data layer
|
||||
(dataclasses + CRUD, no Click), CLI verbs (Slice 1 scope only — not
|
||||
`secret`/`pin`/`deploy`), `scripts/register-ohm.sh` as the executable
|
||||
form of §13, pytest with temp-SQLite fixture, Gitea Actions
|
||||
`.gitea/workflows/test.yml`, VERSION bump + CHANGELOG + spec
|
||||
correction. Out of scope explicitly: secrets, pin reading, deploy
|
||||
gesture, memory updates.
|
||||
|
||||
Called `ExitPlanMode`.
|
||||
|
||||
## Turn 3 — User
|
||||
|
||||
Plan approved.
|
||||
|
||||
## Turn 3 — Assistant
|
||||
|
||||
Created 10 tracking tasks via `TaskCreate`, then executed in order.
|
||||
|
||||
**Task 1 — Project skeleton.** Wrote `pyproject.toml` with setuptools
|
||||
backend, Click + pytest deps, entrypoint `ohm-rfc-app-flotilla =
|
||||
ohm_rfc_app_flotilla.cli:main`, Python `>=3.12`. Wrote
|
||||
`ohm_rfc_app_flotilla/__init__.py` reading `__version__` from the
|
||||
`VERSION` file at repo root.
|
||||
|
||||
**Task 2 — DB layer.** Wrote `ohm_rfc_app_flotilla/db.py` with
|
||||
`data_dir()` honoring `OHM_RFC_APP_FLOTILLA_DATA_DIR` env override,
|
||||
`connect()` enabling WAL + foreign-keys, `run_migrations(conn)`
|
||||
mirroring `rfc-app/backend/app/db.py:34-54` (creates
|
||||
`schema_migrations` if missing, iterates `migrations/*.sql` sorted,
|
||||
skips already-applied, wraps each in `BEGIN; … COMMIT;`), and
|
||||
`open_db()` that connects + migrates in one call.
|
||||
|
||||
**Task 3 — Initial migration.** Wrote
|
||||
`ohm_rfc_app_flotilla/migrations/001_initial_schema.sql` with
|
||||
`deployments` (15 columns per §4.3 + §13.2's
|
||||
`target_vm_tunnel_through_iap`), `overlay_entries` (with `ON DELETE
|
||||
CASCADE` to `deployments` and a unique constraint on
|
||||
`(deployment_id, env_key)`), and an index on
|
||||
`overlay_entries(deployment_id)`.
|
||||
|
||||
**Task 4 — Registry data layer.** Wrote
|
||||
`ohm_rfc_app_flotilla/registry.py` with frozen dataclasses
|
||||
`Deployment` and `OverlayEntry`, plus CRUD functions (`add_deployment`,
|
||||
`get_deployment`, `list_deployments`, `remove_deployment`,
|
||||
`set_overlay`, `unset_overlay`, `list_overlay`). Custom exception
|
||||
classes `NotFoundError` and `DuplicateError`. Overlay set uses
|
||||
`INSERT … ON CONFLICT … DO UPDATE` for upsert.
|
||||
|
||||
**Task 5 — CLI verbs.** Wrote `ohm_rfc_app_flotilla/cli.py` with the
|
||||
Click root group, `deployment` subgroup (list / show / add / remove,
|
||||
with all 11 of §13's `add` flags), `overlay` subgroup (show / set /
|
||||
unset). `_exit_with_error()` helper writes to stderr and exits 1. All
|
||||
read verbs accept `--json`. `overlay show` rendered the secret refs
|
||||
section as `(no secrets registered — Slice 2)` placeholder.
|
||||
|
||||
**Task 6 — OHM registration script.** Wrote
|
||||
`scripts/register-ohm.sh` (chmod +x) as the executable form of §13:
|
||||
one `flotilla deployment add ohm-rfc-app …` invocation with §13.2's
|
||||
values, then 13 `flotilla overlay set` invocations covering §13.6's
|
||||
non-secret keys. `OAUTH_CLIENT_ID` deliberately omitted (value not
|
||||
known at Slice 1 time; Slice 2 firms it up from the live `.env`).
|
||||
|
||||
**Task 7 — Tests.** Wrote `tests/conftest.py` (`tmp_data_dir` fixture
|
||||
sets the env var to `tmp_path`; `runner` fixture wraps Click's
|
||||
`CliRunner` with an `expect_exit` assertion helper), `tests/test_db.py`
|
||||
(3 tests: migration apply, idempotent open, FK cascade),
|
||||
`tests/test_deployment.py` (6 tests: add/list/show/remove round-trip,
|
||||
JSON shape, error paths), `tests/test_overlay.py` (6 tests:
|
||||
set/show/unset, upsert, `=` in value, JSON shape, error paths), and
|
||||
empty `tests/__init__.py`.
|
||||
|
||||
**Task 8 — Gitea Actions CI.** Wrote `.gitea/workflows/test.yml`:
|
||||
Python 3.12, `pip install -e .[test]`, `pytest -q`, triggers on push
|
||||
to main and on PR.
|
||||
|
||||
**Task 9 — Docs/version bump.** Bumped `VERSION` `0.0.1 → 0.1.0`,
|
||||
added a `## 0.1.0 — 2026-05-27` CHANGELOG block, expanded README with
|
||||
a quickstart, applied the §19.3-rule-2 spec correction to §20.1
|
||||
(`deployment show ohm` → `deployment show ohm-rfc-app`).
|
||||
|
||||
**Task 10 — Verification.** First attempt failed:
|
||||
|
||||
```
|
||||
$ python3.12 -m venv .venv
|
||||
(eval):1: command not found: python3.12
|
||||
```
|
||||
|
||||
Discovered the system has Python 3.13 but not 3.12. Since the
|
||||
pyproject floor is `>=3.12`, used 3.13 locally (CI keeps 3.12 as the
|
||||
pinned matrix entry):
|
||||
|
||||
```
|
||||
$ python3.13 -m venv .venv && .venv/bin/pip install -e '.[test]'
|
||||
(succeeded)
|
||||
|
||||
$ .venv/bin/pytest -q
|
||||
…
|
||||
E TypeError: CliRunner.__init__() got an unexpected keyword argument 'mix_stderr'
|
||||
…
|
||||
3 passed, 12 errors in 0.04s
|
||||
```
|
||||
|
||||
Click 8.2 removed `mix_stderr`. Investigated rather than worked
|
||||
around — the kwarg was added in Click 8.0 to keep stderr separate
|
||||
from stdout; in 8.2 separation became the default and the kwarg was
|
||||
dropped. Fix was one line in `tests/conftest.py`:
|
||||
|
||||
```python
|
||||
- self._runner = CliRunner(mix_stderr=False)
|
||||
+ self._runner = CliRunner()
|
||||
```
|
||||
|
||||
Re-ran: `15 passed in 0.03s`. End-to-end against a sandbox data dir:
|
||||
|
||||
```
|
||||
$ SANDBOX=$(mktemp -d) && export OHM_RFC_APP_FLOTILLA_DATA_DIR="$SANDBOX"
|
||||
$ export PATH="$PWD/.venv/bin:$PATH"
|
||||
$ ./scripts/register-ohm.sh
|
||||
registered deployment 'ohm-rfc-app'
|
||||
GITEA_URL = https://git.wiggleverse.org
|
||||
…
|
||||
EMAIL_FROM_NAME = Wiggleverse
|
||||
|
||||
$ ohm-rfc-app-flotilla deployment show ohm-rfc-app
|
||||
name ohm-rfc-app
|
||||
target_vm_name ohm-app
|
||||
target_vm_zone us-central1-a
|
||||
target_vm_project wiggleverse-ohm
|
||||
…
|
||||
health_url https://ohm.wiggleverse.org/api/health
|
||||
|
||||
$ ohm-rfc-app-flotilla overlay show ohm-rfc-app
|
||||
# overlay for ohm-rfc-app
|
||||
APP_URL = https://ohm.wiggleverse.org
|
||||
DATABASE_PATH = /opt/ohm-app/backend/data/rfc-app.db
|
||||
…
|
||||
# secret refs
|
||||
(no secrets registered — Slice 2)
|
||||
|
||||
$ ohm-rfc-app-flotilla deployment show ohm-rfc-app --json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "ohm-rfc-app",
|
||||
…
|
||||
"target_vm_tunnel_through_iap": false,
|
||||
…
|
||||
}
|
||||
```
|
||||
|
||||
§20.1 exit criteria met. Wrote a `.gitignore` covering `.venv/`,
|
||||
`*.egg-info/`, `__pycache__/`, `*.pyc`, `.pytest_cache/`. Reported
|
||||
Slice 1 done; left commit to the user per the harness rule "never
|
||||
commit unless the user explicitly asks."
|
||||
|
||||
## Turn 4 — User
|
||||
|
||||
> Ok, let's move on to Slice 2
|
||||
|
||||
## Turn 4 — Assistant
|
||||
|
||||
Two preconditions to settle before plan-mode:
|
||||
|
||||
1. Slice 1 was uncommitted — moving on without a tagged baseline
|
||||
would tangle Slice 2's commits with Slice 1's.
|
||||
2. Slice 2 has two distinct halves: a code part (schema + verbs + GCP
|
||||
client + tests) flotilla can do alone, and an operator part
|
||||
(`gcloud auth ADC`, SSH to ohm-app, read live `.env`, `gcloud
|
||||
secrets create` per key, then `flotilla secret bind` per key) that
|
||||
needs the user's credentials.
|
||||
|
||||
Read SPEC §7 to confirm the binding shape — read-only client (no
|
||||
secret creation in v1 per §7.6), `bind` validates by attempting a
|
||||
Secret Manager read (refuses if the operator can't resolve it), `list`
|
||||
never prints bytes, naming convention `<deployment>-<env-key>` with
|
||||
underscores → dashes, IAM model is operator-with-ADC needing
|
||||
`secretmanager.secretAccessor` for ongoing use and `secretmanager.admin`
|
||||
for the bootstrap.
|
||||
|
||||
Asked two questions via `AskUserQuestion`:
|
||||
|
||||
1. Commit + tag v0.1.0 first? Recommended yes.
|
||||
2. Slice 2 scope split? Recommended "code + docs this session; you
|
||||
run the operator gesture in your own time" — three options
|
||||
(code+docs+you-gesture, code+drive-gesture-together-here,
|
||||
code-only-defer-docs).
|
||||
|
||||
## Turn 5 — User
|
||||
|
||||
Both recommendations accepted:
|
||||
- Commit + tag v0.1.0 first
|
||||
- Code + docs this session; user runs the operator gesture
|
||||
|
||||
## Turn 5 — Assistant
|
||||
|
||||
Committed Slice 1 with explicit paths (avoiding `git add -A`):
|
||||
|
||||
```
|
||||
$ git add .gitignore .gitea/ ohm_rfc_app_flotilla/ pyproject.toml \
|
||||
scripts/ tests/ CHANGELOG.md README.md SPEC.md VERSION
|
||||
|
||||
$ git commit -m "Release 0.1.0: Slice 1 — registry + non-secret overlay
|
||||
|
||||
Ships the Python skeleton, SQLite schema, and seven CLI verbs that let
|
||||
the operator register a deployment and edit its non-secret overlay.
|
||||
OHM is registered via scripts/register-ohm.sh — the executable form of
|
||||
SPEC §13.1–§13.6. No actuation, no GCP, no network.
|
||||
|
||||
Per SPEC §20.1 exit criteria: \`ohm-rfc-app-flotilla deployment show
|
||||
ohm-rfc-app\` and \`… overlay show ohm-rfc-app\` render the OHM record.
|
||||
|
||||
Spec correction (§19.3 rule 2): §20.1 exit criteria normalized to
|
||||
\`ohm-rfc-app\` as the deployment name, matching §13.1's binding.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
||||
[main 581687a] Release 0.1.0: Slice 1 — registry + non-secret overlay
|
||||
18 files changed, 839 insertions(+), 3 deletions(-)
|
||||
|
||||
$ git tag v0.1.0
|
||||
$ git tag -l
|
||||
v0.0.1
|
||||
v0.1.0
|
||||
```
|
||||
|
||||
(Git printed an identity-setup notice about `Ben Stull
|
||||
<benstull@mac.lan>`; left it as-is, did not touch git config per the
|
||||
git-safety protocol.)
|
||||
|
||||
Entered plan mode for Slice 2. The harness reminded that a plan file
|
||||
already existed from Slice 1; per the workflow, overwrote it for the
|
||||
new task. Wrote a structured plan covering:
|
||||
|
||||
- Migration `002_secret_refs.sql` per §4.3 (`secret_version TEXT
|
||||
DEFAULT 'latest'` to carry both `latest` and integer-as-string)
|
||||
- `ohm_rfc_app_flotilla/secrets.py` — read-only GCP wrapper with
|
||||
`parse_ref` / `format_ref` / `read_secret` / `SecretReadError`,
|
||||
lazy-importing `google.cloud.secretmanager` so monkeypatch-stubbed
|
||||
tests don't need the package installed
|
||||
- `registry.py` additions: `SecretRef` dataclass + three CRUD
|
||||
functions (matching Slice 1's upsert pattern)
|
||||
- CLI: new `secret` group (bind/unbind/list); update `overlay show`
|
||||
to render bound secrets per §6.2 as `KEY = [secret:
|
||||
project/id@version]`
|
||||
- `tests/test_secrets.py` with monkeypatch fixture; update
|
||||
`test_overlay.py` for the new secret listing
|
||||
- Add `google-cloud-secret-manager>=2.20` to dependencies
|
||||
- `docs/secret-bootstrap.md` — 9-step operator runbook
|
||||
- Spec corrections in §20.2/§20.3/§20.4/§20.5 (continuing the §19.3
|
||||
rule 2 cleanup begun in Slice 1)
|
||||
- `VERSION` **unchanged** at `0.1.0`; CHANGELOG block headed `## 0.2.0
|
||||
— pending operator gesture` to be flipped to a real date in the
|
||||
operator's follow-up commit
|
||||
|
||||
Called `ExitPlanMode`.
|
||||
|
||||
## Turn 6 — User
|
||||
|
||||
Plan approved.
|
||||
|
||||
## Turn 6 — Assistant
|
||||
|
||||
Cleared the 10 Slice 1 tasks (marked deleted) and created 9 Slice 2
|
||||
tasks.
|
||||
|
||||
**Task 11 — Migration 002.** Wrote
|
||||
`ohm_rfc_app_flotilla/migrations/002_secret_refs.sql` with the
|
||||
`secret_refs` table per §4.3 + an index, including a header comment
|
||||
stating `Never carries the secret bytes themselves (§3 invariant 1)`.
|
||||
|
||||
**Task 12 — secrets.py.** Wrote `ohm_rfc_app_flotilla/secrets.py`:
|
||||
|
||||
- `SecretReadError(Exception)` — single error type carrying the
|
||||
failure-mode-named message
|
||||
- `parse_ref(ref)` — parses `<project>/<id>[@version]` with version
|
||||
defaulting to `"latest"`; raises `ValueError` for empty, missing
|
||||
`/`, empty project, empty id, empty version after `@`
|
||||
- `format_ref(project, id, version)` — inverse, always emits the
|
||||
`@version` suffix (so `latest` is unambiguous in display)
|
||||
- `read_secret(project, id, version="latest")` — lazy-imports
|
||||
`google.cloud.secretmanager`, `google.api_core.exceptions`,
|
||||
`google.auth.exceptions`. Catches `DefaultCredentialsError` →
|
||||
"no ADC available — run `gcloud auth application-default login`";
|
||||
`PermissionDenied` → "permission denied reading …";
|
||||
`NotFound` → "secret not found …"; generic `GoogleAPICallError`
|
||||
fallthrough. Each raise wraps the underlying with `from e` for
|
||||
inspection but the user-facing message names the mode.
|
||||
|
||||
**Task 13 — registry.py additions.** Edited
|
||||
`ohm_rfc_app_flotilla/registry.py`: added `SecretRef` frozen
|
||||
dataclass and three functions (`bind_secret` with the same `INSERT …
|
||||
ON CONFLICT … DO UPDATE` upsert pattern as `set_overlay`,
|
||||
`unbind_secret` with `NotFoundError` on zero rowcount,
|
||||
`list_secret_refs` ordered by `env_key`).
|
||||
|
||||
**Task 14 — CLI: secret group + overlay show update.** Edited
|
||||
`ohm_rfc_app_flotilla/cli.py`: imported `secrets`; added `secret`
|
||||
group with `bind` / `unbind` / `list` commands. `bind` flow:
|
||||
`get_deployment` (validates deployment exists) → `parse_ref` →
|
||||
`secrets.read_secret` (validates readability — refuses with "refused
|
||||
to bind — secret not readable: …" on `SecretReadError`, no DB row
|
||||
written) → `bind_secret`. `list` uses `--json` for the structured
|
||||
shape, never prints bytes. Updated `overlay show` to read
|
||||
`list_secret_refs` and render bound secrets per §6.2 as `KEY =
|
||||
[secret: project/id@version]`; with `--json` the `secret_refs` field
|
||||
is now the real list, not `[]`.
|
||||
|
||||
**Task 15 — Tests.** Wrote `tests/test_secrets.py` with 18 tests:
|
||||
|
||||
- `parse_ref` happy + parametrized error cases (`""`, `"no-slash"`,
|
||||
`"/no-project"`, `"proj/"`, `"proj/id@"`, `"/"`)
|
||||
- `format_ref` round-trip + explicit-`latest` rendering
|
||||
- `stub_read_secret` fixture monkeypatches
|
||||
`ohm_rfc_app_flotilla.cli.secrets.read_secret` to return stub bytes
|
||||
- Round-trip: bind → list (human + `--json`) → unbind → list empty
|
||||
- `--json` shape includes `secret_version`, excludes `secret_value`
|
||||
and `value` (§3 invariant 1 guard)
|
||||
- Idempotent rebind (second bind updates version)
|
||||
- Refuses when stubbed `read_secret` raises `SecretReadError` (no DB
|
||||
row written; verified by `list --json` returning `[]`)
|
||||
- Malformed ref rejected with "missing '/'" message
|
||||
- Unknown-deployment and unknown-key errors
|
||||
- `overlay show` lists bound secrets; stub bytes never appear in
|
||||
output
|
||||
- `overlay show --json` includes the real `secret_refs` list with
|
||||
the correct shape
|
||||
|
||||
Updated one assertion in `tests/test_overlay.py` to match the new
|
||||
behavior (`"no secrets registered" → "no secrets bound"`).
|
||||
|
||||
**Task 16 — Dependency.** Edited `pyproject.toml` to add
|
||||
`google-cloud-secret-manager>=2.20`.
|
||||
|
||||
**Task 17 — Operator runbook.** Wrote `docs/secret-bootstrap.md` —
|
||||
9 sections:
|
||||
|
||||
0. Read first (sources of truth, no-bytes invariant, two buckets)
|
||||
1. Prereqs (`gcloud auth ADC`, enable Secret Manager API, IAM
|
||||
`secretmanager.admin` for bootstrap, `secretAccessor` afterward)
|
||||
2. Pull live `.env`: `gcloud compute ssh ohm-app … 'sudo cat
|
||||
/opt/ohm-app/backend/.env' > /tmp/ohm-rfc-app.env` with chmod 600
|
||||
3. Partition table — §13.6 (14 non-secret keys) vs §13.7 (6 secret
|
||||
keys), instruction to stop and discuss anything else
|
||||
4. Reconcile the non-secret overlay (especially `OAUTH_CLIENT_ID`
|
||||
which the Slice 1 register script left blank)
|
||||
5. Create the six Secret Manager entries via `printf '%s' "$VALUE" |
|
||||
gcloud secrets create ohm-rfc-app-<env-key> --data-file=-
|
||||
--project=wiggleverse-ohm --replication-policy=automatic`, with a
|
||||
table mapping each §13.7 ENV_KEY to its §7.2 secret ID, and
|
||||
handling for `ALREADY_EXISTS` (use `secrets versions add` instead)
|
||||
6. Six explicit `flotilla secret bind ohm-rfc-app KEY
|
||||
wiggleverse-ohm/ohm-rfc-app-<id>` invocations
|
||||
7. Verify: `flotilla secret list ohm-rfc-app` shows the six bindings;
|
||||
optional manual cross-check via `gcloud secrets versions access
|
||||
latest …`
|
||||
8. Cleanup: `shred -u /tmp/ohm-rfc-app.env` (or `rm -P` on macOS),
|
||||
IAM downgrade
|
||||
9. Cut v0.2.0: bump VERSION, replace the CHANGELOG placeholder
|
||||
header with the date, commit + tag
|
||||
|
||||
**Task 18 — Spec corrections + CHANGELOG + README.** Edited `SPEC.md`
|
||||
in six places: §20.2 exit criteria (`secret list ohm` →
|
||||
`secret list ohm-rfc-app`), §20.3 exit criteria (`deploy ohm
|
||||
--dry-run` → `deploy ohm-rfc-app --dry-run`), §20.4 goal + exit
|
||||
criteria (`deploy ohm` → `deploy ohm-rfc-app`, twice), §20.5 scope
|
||||
bullets (`pin check ohm` and `deploy ohm` → both `ohm-rfc-app`),
|
||||
§20.5 operator-guide bullet (`deployment add ohm …` →
|
||||
`deployment add ohm-rfc-app …`). Added the `## 0.2.0 — pending
|
||||
operator gesture` CHANGELOG block describing what shipped. Updated
|
||||
README "Status" section to mention Slice 2 lands ahead of v0.2.0 and
|
||||
link the bootstrap doc.
|
||||
|
||||
**Task 19 — Verification.** `pip install -e .[test]` picked up
|
||||
`google-cloud-secret-manager` and its transitive deps. `pytest -q`:
|
||||
|
||||
```
|
||||
.F................................. [100%]
|
||||
=================================== FAILURES ===================================
|
||||
__________________________ test_open_db_is_idempotent __________________________
|
||||
…
|
||||
rows = conn.execute("SELECT COUNT(*) AS n FROM schema_migrations").fetchone()
|
||||
> assert rows["n"] == 1
|
||||
E assert 2 == 1
|
||||
|
||||
tests/test_db.py:19: AssertionError
|
||||
```
|
||||
|
||||
Slice 1's test asserted a literal migration count of `1`. With Slice
|
||||
2's migration 002 the count is `2`. The test was too narrow — its
|
||||
spec-relevant invariant is "no migration runs twice," not "exactly
|
||||
one migration exists." Replaced with that invariant:
|
||||
|
||||
```python
|
||||
def test_open_db_is_idempotent(tmp_data_dir):
|
||||
flotilla_db.open_db().close()
|
||||
first = _migration_versions(flotilla_db.open_db())
|
||||
second = _migration_versions(flotilla_db.open_db())
|
||||
assert first == second
|
||||
assert len(first) == len(set(first))
|
||||
```
|
||||
|
||||
Re-ran: `35 passed in 0.06s`.
|
||||
|
||||
End-to-end spot-check of the new CLI surface against a sandbox data
|
||||
dir, including the deliberately unauthenticated `bind` to verify the
|
||||
ADC-missing path surfaces correctly:
|
||||
|
||||
```
|
||||
$ SANDBOX=$(mktemp -d) && export OHM_RFC_APP_FLOTILLA_DATA_DIR="$SANDBOX"
|
||||
$ ./scripts/register-ohm.sh > /dev/null
|
||||
|
||||
$ ohm-rfc-app-flotilla overlay show ohm-rfc-app | tail -5
|
||||
SMTP_STARTTLS = 1
|
||||
SMTP_USER = ben@wiggleverse.org
|
||||
|
||||
# secret refs
|
||||
(no secrets bound)
|
||||
|
||||
$ ohm-rfc-app-flotilla secret list ohm-rfc-app
|
||||
(no secrets bound)
|
||||
|
||||
$ env -u GOOGLE_APPLICATION_CREDENTIALS HOME=/tmp/no-such-home \
|
||||
ohm-rfc-app-flotilla secret bind ohm-rfc-app GITEA_BOT_TOKEN \
|
||||
wiggleverse-ohm/ohm-rfc-app-gitea-bot-token
|
||||
error: refused to bind — secret not readable: no Application Default
|
||||
Credentials available — run `gcloud auth application-default login`
|
||||
exit: 1
|
||||
```
|
||||
|
||||
Exit code 1, no DB row written, message names the failure mode. §3
|
||||
invariant 1 + §7.5 satisfied throughout.
|
||||
|
||||
Reported Slice 2 code complete and verified. Final working-tree
|
||||
state:
|
||||
|
||||
```
|
||||
Changes not staged for commit:
|
||||
modified: CHANGELOG.md
|
||||
modified: README.md
|
||||
modified: SPEC.md
|
||||
modified: ohm_rfc_app_flotilla/cli.py
|
||||
modified: ohm_rfc_app_flotilla/registry.py
|
||||
modified: pyproject.toml
|
||||
modified: tests/test_db.py
|
||||
modified: tests/test_overlay.py
|
||||
|
||||
Untracked files:
|
||||
docs/
|
||||
ohm_rfc_app_flotilla/migrations/002_secret_refs.sql
|
||||
ohm_rfc_app_flotilla/secrets.py
|
||||
tests/test_secrets.py
|
||||
```
|
||||
|
||||
Per the user's Turn 5 scope decision, did not commit — the v0.2.0
|
||||
tag waits on the operator gesture, and committing now would create a
|
||||
"code shipped" commit that the user can compose with the gesture
|
||||
results into a single v0.2.0 commit (or split if they prefer).
|
||||
|
||||
## Turn 7 — User
|
||||
|
||||
> ok, I'm going to start a new session for slice 3
|
||||
|
||||
## Turn 7 — Assistant
|
||||
|
||||
Wrote a brief hand-off summary:
|
||||
|
||||
- v0.1.0 (Slice 1) committed (`581687a`) + tagged
|
||||
- Slice 2 code complete in the working tree, uncommitted
|
||||
- 35 tests pass
|
||||
- CHANGELOG says "0.2.0 — pending operator gesture"; VERSION reads
|
||||
`0.1.0` deliberately
|
||||
- Slice 3 (SPEC §20.3) is pin reading + dry-run deploy → v0.3.0
|
||||
- Suggested the next session decide whether to commit Slice 2 first
|
||||
(clean baseline) or stack on top
|
||||
|
||||
No `/schedule` offer — the user signaled done, no quoted artifact
|
||||
with a date in this session's work.
|
||||
|
||||
## Turn 8 — User
|
||||
|
||||
> ok what's the claude command I should use to start slice 3 and what
|
||||
> dir should i start it from?
|
||||
|
||||
## Turn 8 — Assistant
|
||||
|
||||
```
|
||||
cd /Users/benstull/projects/wiggleverse/ohm-rfc-app-flotilla
|
||||
claude
|
||||
```
|
||||
|
||||
Flagged that the directory matters: the auto-memory is keyed to that
|
||||
path (`-Users-benstull-projects-wiggleverse-ohm-rfc-app-flotilla`).
|
||||
Launching from anywhere else loses the project context. Suggested
|
||||
first prompts: `run the session based on what's in memory` (same as
|
||||
Turn 1 of this session) or just `start Slice 3`.
|
||||
|
||||
## Turn 9 — User
|
||||
|
||||
> write a full transcript of this session (EVERYTHING) to ~/git/ohm-infra
|
||||
|
||||
## Turn 9 — Assistant
|
||||
|
||||
This file. Checked `~/git/ohm-infra/` for existing convention —
|
||||
found `SESSION-B-TRANSCRIPT.md`, `SESSION-C-TRANSCRIPT.md`,
|
||||
`SESSION-D-TRANSCRIPT.md` (82 turns, 1267 lines). Matched the
|
||||
format: header with date/goal/outcome blockquote, numbered turns
|
||||
alternating User and Assistant, narrative prose for assistant turns
|
||||
with code blocks for key outputs. Wrote
|
||||
`/Users/benstull/git/ohm-infra/SESSION-E-TRANSCRIPT.md`.
|
||||
|
||||
---
|
||||
|
||||
## What's load-bearing for the next session
|
||||
|
||||
- Slice 2 code is **on disk, uncommitted, tested green** at the start
|
||||
of Session F. Decide commit timing before plan-mode.
|
||||
- `VERSION` stays `0.1.0` until the operator gesture in
|
||||
`docs/secret-bootstrap.md` runs end-to-end. Session F's Slice 3
|
||||
work can land on top either way; the v0.2.0 cut is independent.
|
||||
- The §19.3-rule-2 spec-correction discipline is now established
|
||||
practice (Slice 1 fixed §20.1; Slice 2 fixed §20.2–§20.5). If Slice
|
||||
3 finds more §20 typos or anything else, fix in-session.
|
||||
- Migration runner pattern is set: numbered `.sql` files, filename
|
||||
stem in `schema_migrations.version`, raw SQL via `executescript`
|
||||
wrapped `BEGIN; … COMMIT;`. Slice 3 likely adds no new tables (§20.3
|
||||
scope is "exercise existing pin-source columns" + a Gitea client +
|
||||
plan assembly, no schema).
|
||||
- Test fixture conventions: `tmp_data_dir` (env-var override),
|
||||
`runner` (Click CliRunner wrapper with `expect_exit`), monkeypatch
|
||||
fixtures for external clients (`stub_read_secret` pattern is
|
||||
reusable for a future `stub_gitea_pin`). New external client in
|
||||
Slice 3 will be a Gitea raw-file reader; mirror the
|
||||
`ohm_rfc_app_flotilla/secrets.py` shape (lazy import, named-error
|
||||
failure modes, "no bytes leak" discipline applied as "no token
|
||||
leak" for Gitea PAT auth).
|
||||
- Click 8.2 `mix_stderr` lesson: don't pin patterns from old Click
|
||||
docs; the test helper in `tests/conftest.py` is now `CliRunner()`
|
||||
with no kwargs.
|
||||
- Python toolchain: local dev on 3.13 (no 3.12 installed); CI matrix
|
||||
pinned at 3.12; pyproject floor `>=3.12`. Either works.
|
||||
Reference in New Issue
Block a user