From a92778aa4a04fde764dce3d7e3f3942fa40d2ad1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 06:29:05 +0000 Subject: [PATCH 1/7] docs(agents): add verify skill with local monolith run recipe Record how to boot server.main:app in a cloud session without a Docker daemon: local PostgreSQL 16 and Redis, the role layout from scripts/init-postgres.sh, the get_tenant_context() seed that Alembic needs, the secrets the lifespan requires, the dev bypass key, and how to drive tenant-bound routes. Un-ignore .claude/skills/verify/ so the recipe ships. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GyhDYG4XxNF78HdSmgBVPA --- .claude/skills/verify/SKILL.md | 92 ++++++++++++++++++++++++++++++++++ .gitignore | 1 + 2 files changed, 93 insertions(+) create mode 100644 .claude/skills/verify/SKILL.md diff --git a/.claude/skills/verify/SKILL.md b/.claude/skills/verify/SKILL.md new file mode 100644 index 000000000..0a91271a5 --- /dev/null +++ b/.claude/skills/verify/SKILL.md @@ -0,0 +1,92 @@ +--- +name: verify +description: > + Build, run and drive the RegEngine monolith locally so a change can be + observed at its HTTP surface (not via tests). Recipe for a cloud session + with no Docker daemon: local PostgreSQL 16 + Redis + uvicorn server.main:app. +--- + +# Verify a change by running the app + +## Services (no Docker daemon in cloud sessions) + +PostgreSQL 16 binaries are at `/usr/lib/postgresql/16/bin`; `redis-server` is +on PATH. Postgres refuses to run as root, and the `postgres` user cannot +traverse the scratchpad, so put the data dir under `/var/lib/postgresql`. + +```bash +PGBIN=/usr/lib/postgresql/16/bin; D=/var/lib/postgresql/verify-data +su postgres -s /bin/bash -c "$PGBIN/initdb -D $D -U postgres --auth=trust -E UTF8 >/dev/null; $PGBIN/pg_ctl -D $D -l /var/lib/postgresql/pg.log -o '-p 5432 -k /var/lib/postgresql -c listen_addresses=127.0.0.1' start" +redis-server --port 6379 --bind 127.0.0.1 --daemonize yes +``` + +Roles must mirror `scripts/init-postgres.sh`: migrations run as a superuser +owner, the app as NOBYPASSRLS roles. `get_tenant_context()` must exist before +`alembic upgrade head`, or the V043 migration fails with +"app.tenant_id not set". + +```bash +psql -h 127.0.0.1 -U postgres -v ON_ERROR_STOP=1 \ + -c "CREATE ROLE regengine_owner LOGIN PASSWORD 'pw' SUPERUSER;" \ + -c "CREATE DATABASE regengine OWNER regengine_owner;" \ + -c "CREATE ROLE regengine LOGIN PASSWORD 'pw' NOSUPERUSER NOBYPASSRLS;" \ + -c "CREATE ROLE regengine_sysadmin LOGIN PASSWORD 'pw' NOSUPERUSER NOBYPASSRLS;" +psql -h 127.0.0.1 -U regengine_owner -d regengine -c "CREATE OR REPLACE FUNCTION get_tenant_context() RETURNS UUID AS \$f\$ DECLARE tid TEXT; BEGIN tid := NULLIF(current_setting('app.tenant_id', TRUE), ''); IF tid IS NULL THEN RAISE EXCEPTION 'app.tenant_id not set - tenant context required for RLS'; END IF; RETURN tid::UUID; END; \$f\$ LANGUAGE plpgsql STABLE;" +``` + +## Env and boot + +Do not `source .env`: it holds `` values that break the shell. +Write a self-contained env file. Every secret below is required at startup +(missing `AUDIT_INTEGRITY_KEY` or `MFA_ENCRYPTION_KEY` aborts the lifespan). + +```bash +S= +{ + grep -E "^(ENABLED_ROUTERS|REGENGINE_INFLOW_WORKBENCH_PATH|WEBAUTHN_EXPECTED_ORIGIN)=" .env + echo REGENGINE_ENV=development + echo REGENGINE_AUTH_STRICT= + echo DATABASE_URL=postgresql://regengine:pw@127.0.0.1:5432/regengine + echo ADMIN_DATABASE_URL=postgresql+psycopg://regengine_sysadmin:pw@127.0.0.1:5432/regengine + echo MIGRATIONS_DATABASE_URL=postgresql://regengine_owner:pw@127.0.0.1:5432/regengine + echo REDIS_URL=redis://127.0.0.1:6379/0 + echo AUTH_TEST_BYPASS_TOKEN=verify-bypass-token-local + echo AUTH_TEST_BYPASS_TENANT_ID=11111111-1111-1111-1111-111111111111 + for v in AUTH_SECRET_KEY JWT_SECRET AUDIT_INTEGRITY_KEY REGENGINE_INTERNAL_SECRET AUDIT_PII_HASH_SALT TOOL_ACCESS_SECRET WEBHOOK_HMAC_SECRET; do echo "$v=$(openssl rand -hex 32)"; done + echo "MFA_ENCRYPTION_KEY=$(venv/bin/python -c 'from cryptography.fernet import Fernet;print(Fernet.generate_key().decode())')" +} > "$S/verify.env" +set -a; source "$S/verify.env"; set +a +venv/bin/alembic upgrade head # ~107 revisions, under a minute +nohup venv/bin/uvicorn server.main:app --host 127.0.0.1 --port 8000 > "$S/uvicorn.log" 2>&1 & +curl -s --retry 45 --retry-delay 2 --retry-connrefused --retry-all-errors http://127.0.0.1:8000/health +``` + +Startup takes about 15 s. `/openapi.json` lists mounted routes; confirm the +route under test is there (`ENABLED_ROUTERS` gates routers). + +## Driving tenant routes + +- Header `X-RegEngine-API-Key: verify-bypass-token-local` gives a key bound to + tenant `11111111-1111-1111-1111-111111111111` with `webhooks.ingest`, read, + write and admin scopes. This works only because `REGENGINE_ENV=development`. +- Pass the tenant as `X-Tenant-ID` or form/query `tenant_id`. A different + tenant returns 403; a conflicting header/form pair returns 400. +- With no key at all, dev mode falls back to an open principal (200, with a + `dev_open_auth_activated` warning in the log). That path does not exist in + production, so a "no auth" probe must send an invalid key to see the 401. +- Fixtures: `services/ingestion/tests/fixtures/*.csv`. Build XLSX from a CSV + with `openpyxl`; to test understated-dimension handling, rewrite the + `` tag in `xl/worksheets/sheet1.xml` and re-zip. +- No-persistence check: count rows in `supplier_cte_events`, `audit_logs` and + `funnel_events` before and after via `psql -U regengine_owner`. + +## Gotchas + +- Jev is live when `TYPESAFE_API_KEY` is in the session env; options like + `semantic_review=true` then make real provider calls. The `jev_configured` + boot log line says `key_present=`. +- The strict Jev review validator rejects some provider answers + non-deterministically; re-run a semantic-review probe before calling it a + regression. +- Stop: `pkill -f "uvicorn server.main"`, `redis-cli shutdown nosave`, + `su postgres -s /bin/bash -c "$PGBIN/pg_ctl -D $D stop"`. diff --git a/.gitignore b/.gitignore index 94e3db919..bf5cc1230 100644 --- a/.gitignore +++ b/.gitignore @@ -136,6 +136,7 @@ audit-findings-*.md .claude/skills/* !.claude/skills/typesafe-ai/ !.claude/skills/jev-sidecar/ +!.claude/skills/verify/ .gemini/ .swarm_memory/ From 1f06ee6ec4a363f35ba93add5824398614a77533 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 06:55:41 +0000 Subject: [PATCH 2/7] docs(agents): allowlist throwaway local DB URLs in verify skill detect-secrets flagged the postgresql://user:pw@127.0.0.1 example lines as Basic Auth Credentials. They are placeholder passwords for a disposable local cluster, so mark them with the repo's inline allowlist pragma. PR Gate: Secrets reproduced locally before and passes after. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GyhDYG4XxNF78HdSmgBVPA --- .claude/skills/verify/SKILL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude/skills/verify/SKILL.md b/.claude/skills/verify/SKILL.md index 0a91271a5..7927114fe 100644 --- a/.claude/skills/verify/SKILL.md +++ b/.claude/skills/verify/SKILL.md @@ -46,9 +46,9 @@ S= grep -E "^(ENABLED_ROUTERS|REGENGINE_INFLOW_WORKBENCH_PATH|WEBAUTHN_EXPECTED_ORIGIN)=" .env echo REGENGINE_ENV=development echo REGENGINE_AUTH_STRICT= - echo DATABASE_URL=postgresql://regengine:pw@127.0.0.1:5432/regengine - echo ADMIN_DATABASE_URL=postgresql+psycopg://regengine_sysadmin:pw@127.0.0.1:5432/regengine - echo MIGRATIONS_DATABASE_URL=postgresql://regengine_owner:pw@127.0.0.1:5432/regengine + echo DATABASE_URL=postgresql://regengine:pw@127.0.0.1:5432/regengine # pragma: allowlist secret + echo ADMIN_DATABASE_URL=postgresql+psycopg://regengine_sysadmin:pw@127.0.0.1:5432/regengine # pragma: allowlist secret + echo MIGRATIONS_DATABASE_URL=postgresql://regengine_owner:pw@127.0.0.1:5432/regengine # pragma: allowlist secret echo REDIS_URL=redis://127.0.0.1:6379/0 echo AUTH_TEST_BYPASS_TOKEN=verify-bypass-token-local echo AUTH_TEST_BYPASS_TENANT_ID=11111111-1111-1111-1111-111111111111 From eb4657b156f128b7e5dd225c51a49bf4de5270b0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 07:40:27 +0000 Subject: [PATCH 3/7] docs(agents): fail closed and scope shutdown in verify skill Address CodeRabbit review on the local run recipe: chain initdb, pg_ctl, pg_isready and redis-server with && so a leftover data dir or occupied port stops the recipe instead of falling through to another store; make the health probe fail on HTTP errors with curl --fail; name the database in the persistence-check psql command; capture the uvicorn PID and a Redis pidfile so shutdown only stops what the recipe started. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GyhDYG4XxNF78HdSmgBVPA --- .claude/skills/verify/SKILL.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/.claude/skills/verify/SKILL.md b/.claude/skills/verify/SKILL.md index 7927114fe..4a07e05d5 100644 --- a/.claude/skills/verify/SKILL.md +++ b/.claude/skills/verify/SKILL.md @@ -16,8 +16,13 @@ traverse the scratchpad, so put the data dir under `/var/lib/postgresql`. ```bash PGBIN=/usr/lib/postgresql/16/bin; D=/var/lib/postgresql/verify-data -su postgres -s /bin/bash -c "$PGBIN/initdb -D $D -U postgres --auth=trust -E UTF8 >/dev/null; $PGBIN/pg_ctl -D $D -l /var/lib/postgresql/pg.log -o '-p 5432 -k /var/lib/postgresql -c listen_addresses=127.0.0.1' start" -redis-server --port 6379 --bind 127.0.0.1 --daemonize yes +# Fail closed: a leftover data dir or an occupied port 5432 must stop the +# recipe here, never fall through to some other PostgreSQL. +su postgres -s /bin/bash -c "$PGBIN/initdb -D $D -U postgres --auth=trust -E UTF8 >/dev/null && $PGBIN/pg_ctl -D $D -l /var/lib/postgresql/pg.log -o '-p 5432 -k /var/lib/postgresql -c listen_addresses=127.0.0.1' start" \ + && pg_isready -h 127.0.0.1 -p 5432 \ + && redis-server --port 6379 --bind 127.0.0.1 --daemonize yes --pidfile /var/lib/postgresql/verify-redis.pid \ + && redis-cli -p 6379 ping \ + || { echo "local service setup failed; stop here"; false; } ``` Roles must mirror `scripts/init-postgres.sh`: migrations run as a superuser @@ -58,7 +63,8 @@ S= set -a; source "$S/verify.env"; set +a venv/bin/alembic upgrade head # ~107 revisions, under a minute nohup venv/bin/uvicorn server.main:app --host 127.0.0.1 --port 8000 > "$S/uvicorn.log" 2>&1 & -curl -s --retry 45 --retry-delay 2 --retry-connrefused --retry-all-errors http://127.0.0.1:8000/health +UVICORN_PID=$! +curl -sS --fail --retry 45 --retry-delay 2 --retry-connrefused --retry-all-errors http://127.0.0.1:8000/health ``` Startup takes about 15 s. `/openapi.json` lists mounted routes; confirm the @@ -78,7 +84,8 @@ route under test is there (`ENABLED_ROUTERS` gates routers). with `openpyxl`; to test understated-dimension handling, rewrite the `` tag in `xl/worksheets/sheet1.xml` and re-zip. - No-persistence check: count rows in `supplier_cte_events`, `audit_logs` and - `funnel_events` before and after via `psql -U regengine_owner`. + `funnel_events` before and after via + `psql -h 127.0.0.1 -U regengine_owner -d regengine`. ## Gotchas @@ -88,5 +95,6 @@ route under test is there (`ENABLED_ROUTERS` gates routers). - The strict Jev review validator rejects some provider answers non-deterministically; re-run a semantic-review probe before calling it a regression. -- Stop: `pkill -f "uvicorn server.main"`, `redis-cli shutdown nosave`, +- Stop only what this recipe started: `kill "$UVICORN_PID"`, + `kill "$(cat /var/lib/postgresql/verify-redis.pid)"`, `su postgres -s /bin/bash -c "$PGBIN/pg_ctl -D $D stop"`. From 727263c9c9e46e7ba691dd5687bf98f5a25a878d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 07:48:23 +0000 Subject: [PATCH 4/7] docs(agents): require the recipe's own uvicorn PID to be alive around the health probe If port 8000 is busy, uvicorn exits on bind and curl would probe someone else's server. Check kill -0 on the captured PID before and after the health probe and fail the boot block otherwise. Addresses CodeRabbit's follow-up on the verify skill. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GyhDYG4XxNF78HdSmgBVPA --- .claude/skills/verify/SKILL.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.claude/skills/verify/SKILL.md b/.claude/skills/verify/SKILL.md index 4a07e05d5..06ac05df7 100644 --- a/.claude/skills/verify/SKILL.md +++ b/.claude/skills/verify/SKILL.md @@ -64,7 +64,12 @@ set -a; source "$S/verify.env"; set +a venv/bin/alembic upgrade head # ~107 revisions, under a minute nohup venv/bin/uvicorn server.main:app --host 127.0.0.1 --port 8000 > "$S/uvicorn.log" 2>&1 & UVICORN_PID=$! -curl -sS --fail --retry 45 --retry-delay 2 --retry-connrefused --retry-all-errors http://127.0.0.1:8000/health +# A busy port 8000 makes uvicorn exit on bind; curl would then be probing +# someone else's server. Require our process to be alive before and after. +sleep 2 && kill -0 "$UVICORN_PID" \ + && curl -sS --fail --retry 45 --retry-delay 2 --retry-connrefused --retry-all-errors http://127.0.0.1:8000/health \ + && kill -0 "$UVICORN_PID" \ + || { echo "uvicorn did not come up as PID $UVICORN_PID; see $S/uvicorn.log"; false; } ``` Startup takes about 15 s. `/openapi.json` lists mounted routes; confirm the From 50813f98bad79430171bcdb6374a90401ff4e29d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 07:52:22 +0000 Subject: [PATCH 5/7] docs(agents): require a free port 8000 and prove the listener is ours kill -0 passes while uvicorn is still importing, so a leftover server on port 8000 could answer the health probe first. Refuse to start when the port is busy, and after the probe require ss to show UVICORN_PID as the process bound to port 8000. Addresses Cursor Bugbot on the verify skill. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GyhDYG4XxNF78HdSmgBVPA --- .claude/skills/verify/SKILL.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.claude/skills/verify/SKILL.md b/.claude/skills/verify/SKILL.md index 06ac05df7..95f586c55 100644 --- a/.claude/skills/verify/SKILL.md +++ b/.claude/skills/verify/SKILL.md @@ -62,14 +62,15 @@ S= } > "$S/verify.env" set -a; source "$S/verify.env"; set +a venv/bin/alembic upgrade head # ~107 revisions, under a minute +# Port 8000 must be free before we start, or curl below would be probing +# someone else's server while ours is still importing. +[ -z "$(ss -ltnH 'sport = :8000')" ] || { echo "port 8000 is already in use; stop here"; false; } nohup venv/bin/uvicorn server.main:app --host 127.0.0.1 --port 8000 > "$S/uvicorn.log" 2>&1 & UVICORN_PID=$! -# A busy port 8000 makes uvicorn exit on bind; curl would then be probing -# someone else's server. Require our process to be alive before and after. -sleep 2 && kill -0 "$UVICORN_PID" \ - && curl -sS --fail --retry 45 --retry-delay 2 --retry-connrefused --retry-all-errors http://127.0.0.1:8000/health \ +curl -sS --fail --retry 45 --retry-delay 2 --retry-connrefused --retry-all-errors http://127.0.0.1:8000/health \ && kill -0 "$UVICORN_PID" \ - || { echo "uvicorn did not come up as PID $UVICORN_PID; see $S/uvicorn.log"; false; } + && ss -ltnpH 'sport = :8000' | grep -q "pid=$UVICORN_PID," \ + || { echo "uvicorn PID $UVICORN_PID is not the server on port 8000; see $S/uvicorn.log"; false; } ``` Startup takes about 15 s. `/openapi.json` lists mounted routes; confirm the From 8093d3c0ac71c35c4d7e021e2c3164fe828209e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 07:53:07 +0000 Subject: [PATCH 6/7] docs(agents): use fuser for the port 8000 guards in verify skill ss is not installed in the cloud session image, so the previous guard passed silently when the port was busy and always failed afterwards. fuser is present: refuse to start when 8000/tcp has a holder, and after the health probe require that holder to be UVICORN_PID. Checked against a throwaway listener in this environment. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GyhDYG4XxNF78HdSmgBVPA --- .claude/skills/verify/SKILL.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.claude/skills/verify/SKILL.md b/.claude/skills/verify/SKILL.md index 95f586c55..5654d2f5b 100644 --- a/.claude/skills/verify/SKILL.md +++ b/.claude/skills/verify/SKILL.md @@ -64,12 +64,11 @@ set -a; source "$S/verify.env"; set +a venv/bin/alembic upgrade head # ~107 revisions, under a minute # Port 8000 must be free before we start, or curl below would be probing # someone else's server while ours is still importing. -[ -z "$(ss -ltnH 'sport = :8000')" ] || { echo "port 8000 is already in use; stop here"; false; } +! fuser 8000/tcp >/dev/null 2>&1 || { echo "port 8000 is already in use; stop here"; false; } nohup venv/bin/uvicorn server.main:app --host 127.0.0.1 --port 8000 > "$S/uvicorn.log" 2>&1 & UVICORN_PID=$! curl -sS --fail --retry 45 --retry-delay 2 --retry-connrefused --retry-all-errors http://127.0.0.1:8000/health \ - && kill -0 "$UVICORN_PID" \ - && ss -ltnpH 'sport = :8000' | grep -q "pid=$UVICORN_PID," \ + && [ "$(fuser 8000/tcp 2>/dev/null | tr -d ' ')" = "$UVICORN_PID" ] \ || { echo "uvicorn PID $UVICORN_PID is not the server on port 8000; see $S/uvicorn.log"; false; } ``` From 8adcf4a6d6a79916865fd822642dbb38d2fe303f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 04:58:48 +0000 Subject: [PATCH 7/7] docs(agents): chain the boot block, bound the probe, prove Redis ownership Address the open review threads on the verify skill. The uvicorn block is now one chained command, so a busy port 8000 stops it before uvicorn starts instead of leaving later pasted lines to run. The health probe carries --connect-timeout, --max-time and --retry-max-time. The service block requires ports 5432 and 6379 to be free, removes any stale Redis pidfile first, and checks that the process holding 6379 afterwards is the pidfile's PID. Shutdown signals the Redis PID only if it is still a redis-server. Each guard was exercised against throwaway listeners. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GyhDYG4XxNF78HdSmgBVPA --- .claude/skills/verify/SKILL.md | 36 ++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/.claude/skills/verify/SKILL.md b/.claude/skills/verify/SKILL.md index 5654d2f5b..480029988 100644 --- a/.claude/skills/verify/SKILL.md +++ b/.claude/skills/verify/SKILL.md @@ -16,13 +16,17 @@ traverse the scratchpad, so put the data dir under `/var/lib/postgresql`. ```bash PGBIN=/usr/lib/postgresql/16/bin; D=/var/lib/postgresql/verify-data -# Fail closed: a leftover data dir or an occupied port 5432 must stop the -# recipe here, never fall through to some other PostgreSQL. -su postgres -s /bin/bash -c "$PGBIN/initdb -D $D -U postgres --auth=trust -E UTF8 >/dev/null && $PGBIN/pg_ctl -D $D -l /var/lib/postgresql/pg.log -o '-p 5432 -k /var/lib/postgresql -c listen_addresses=127.0.0.1' start" \ +# Fail closed, as one chained command: ports 5432 and 6379 must be free before +# anything starts, a leftover data dir stops initdb, and the Redis holding +# port 6379 afterwards must be the one this recipe launched (its pidfile). +RPID=/var/lib/postgresql/verify-redis.pid; rm -f "$RPID" +! fuser 5432/tcp >/dev/null 2>&1 && ! fuser 6379/tcp >/dev/null 2>&1 \ + && su postgres -s /bin/bash -c "$PGBIN/initdb -D $D -U postgres --auth=trust -E UTF8 >/dev/null && $PGBIN/pg_ctl -D $D -l /var/lib/postgresql/pg.log -o '-p 5432 -k /var/lib/postgresql -c listen_addresses=127.0.0.1' start" \ && pg_isready -h 127.0.0.1 -p 5432 \ - && redis-server --port 6379 --bind 127.0.0.1 --daemonize yes --pidfile /var/lib/postgresql/verify-redis.pid \ + && redis-server --port 6379 --bind 127.0.0.1 --daemonize yes --pidfile "$RPID" \ && redis-cli -p 6379 ping \ - || { echo "local service setup failed; stop here"; false; } + && [ "$(fuser 6379/tcp 2>/dev/null | tr -d ' ')" = "$(cat "$RPID" 2>/dev/null)" ] \ + || { echo "local service setup failed, or port 5432/6379 is held by something else; stop here"; false; } ``` Roles must mirror `scripts/init-postgres.sh`: migrations run as a superuser @@ -62,14 +66,14 @@ S= } > "$S/verify.env" set -a; source "$S/verify.env"; set +a venv/bin/alembic upgrade head # ~107 revisions, under a minute -# Port 8000 must be free before we start, or curl below would be probing -# someone else's server while ours is still importing. -! fuser 8000/tcp >/dev/null 2>&1 || { echo "port 8000 is already in use; stop here"; false; } -nohup venv/bin/uvicorn server.main:app --host 127.0.0.1 --port 8000 > "$S/uvicorn.log" 2>&1 & -UVICORN_PID=$! -curl -sS --fail --retry 45 --retry-delay 2 --retry-connrefused --retry-all-errors http://127.0.0.1:8000/health \ +# One chained command: port 8000 must be free before uvicorn starts (or curl +# would probe someone else's server while ours is still importing), and the +# process holding the port after the probe must be the one we launched. +! fuser 8000/tcp >/dev/null 2>&1 \ + && { nohup venv/bin/uvicorn server.main:app --host 127.0.0.1 --port 8000 > "$S/uvicorn.log" 2>&1 & UVICORN_PID=$!; } \ + && curl -sS --fail --connect-timeout 5 --max-time 20 --retry 45 --retry-delay 2 --retry-max-time 180 --retry-connrefused --retry-all-errors http://127.0.0.1:8000/health \ && [ "$(fuser 8000/tcp 2>/dev/null | tr -d ' ')" = "$UVICORN_PID" ] \ - || { echo "uvicorn PID $UVICORN_PID is not the server on port 8000; see $S/uvicorn.log"; false; } + || { echo "port 8000 was busy, or PID ${UVICORN_PID:-none} is not the server on it; see $S/uvicorn.log"; false; } ``` Startup takes about 15 s. `/openapi.json` lists mounted routes; confirm the @@ -100,6 +104,8 @@ route under test is there (`ENABLED_ROUTERS` gates routers). - The strict Jev review validator rejects some provider answers non-deterministically; re-run a semantic-review probe before calling it a regression. -- Stop only what this recipe started: `kill "$UVICORN_PID"`, - `kill "$(cat /var/lib/postgresql/verify-redis.pid)"`, - `su postgres -s /bin/bash -c "$PGBIN/pg_ctl -D $D stop"`. +- Stop only what this recipe started: `kill "$UVICORN_PID"`, then Redis only + if the recorded PID is still a redis-server (a stale pidfile must never + signal a reused PID): + `R=$(cat "$RPID" 2>/dev/null); [ "$(ps -o comm= -p "$R" 2>/dev/null)" = redis-server ] && kill "$R"`, + then `su postgres -s /bin/bash -c "$PGBIN/pg_ctl -D $D stop"`.