KB-6B14
dot-iu-cutter v0.4 — Connection & Secret-Loading Design (design only)
9 min read Revision 1
dot-iu-cutterdieu44v0.4db-adapterdesign-onlysecretsconnection
dot-iu-cutter v0.4 — Connection & Secret-Loading Design
document_path: knowledge/dev/laws/dieu44-trien-khai/v0.4-db-adapter-design/dot-iu-cutter-v0.4-connection-and-secret-loading-design-2026-05-17.md
revision: r1
date: 2026-05-17
author: Agent (Claude Code CLI, Opus 4.7 1M)
phase: v0.4 — REAL DB ADAPTER DESIGN (companion to design-master)
status: design_only_pending_gpt_review
⛔ DESIGN ONLY. No secret read, no
.envedit, no connection, no code. Env keys named below are referenced as targets; their values are NEVER read, printed, or echoed by this design.
§1 — Secret Substrate (fixed, already live)
substrate: /opt/incomex/docker/.env (perms 600 root:root, NOT in DB dumps)
live_keys (written by the PASSed credential cycle — values never shown):
DOT_CUTTER_EXEC_DB_USER = cutter_exec
DOT_CUTTER_EXEC_DB_PASSWORD = <secret, 40-char, SCRAM-backed>
DOT_CUTTER_VERIFY_DB_USER = cutter_verify
DOT_CUTTER_VERIFY_DB_PASSWORD = <secret>
rule: the adapter design consumes EXACTLY these four keys for identity +
password. It MUST NOT introduce password material anywhere else.
§2 — Connection Parameters Not Yet in .env (gap → blocker)
The four live keys carry identity + password only. A real connection
also needs host / port / dbname / sslmode. These are non-secret but must
still be env-driven (no hardcode). Recommended additional keys (to be
added by a SEPARATE authorized .env cycle — NOT this phase, NOT now):
DOT_CUTTER_DB_HOST # e.g. the postgres container reachable name/host
DOT_CUTTER_DB_PORT # e.g. 5432
DOT_CUTTER_DB_NAME # directus (the cutter_governance schema lives here)
DOT_CUTTER_DB_SSLMODE # recommend: require | verify-full (DA-2)
# shared across both principals; carry NO password
- Rationale for component keys over a single
…_DSN: a DSN string tends to embed the password, which then risks landing in logs/argv/history. Keeping password isolated in its ownDOT_CUTTER_*_DB_PASSWORDkey and assembling the connection in-process from components keeps the secret off every surface. NoDOT_CUTTER_*_DB_DSNenv key is recommended. - Blocker B-A1 / B-D1: adding the 4 connection keys to
.envis a prerequisite for code and PG-backed dry-run; it is itself a small, separate, GPT-reviewable change (not done here —.envedit is forbidden this phase).
§3 — Loading Discipline (fail-closed)
L-1 read-only-env: the adapter obtains values via the process environment
(the existing VPS/docker pattern that already injects /opt/incomex/docker/.env
into the container/process). The adapter NEVER opens, parses, or stat()s
the .env file itself, and NEVER shells out to read it.
L-2 required-set: { DOT_CUTTER_<P>_DB_USER, DOT_CUTTER_<P>_DB_PASSWORD,
DOT_CUTTER_DB_HOST, DOT_CUTTER_DB_PORT, DOT_CUTTER_DB_NAME,
DOT_CUTTER_DB_SSLMODE } for the bound principal <P>.
L-3 fail-closed: if ANY required key is missing/empty → raise a typed
ConfigMissing error BEFORE any socket/connect attempt. No default host,
no default password, no localhost fallback, no "try without ssl".
L-4 no-leak surface (hard): the password value MUST NOT appear in:
log lines, exception messages/stack frames, repr()/str() of the adapter
or connection, argv, shell history, KB, git, metrics, traces. Errors
reference the KEY NAME ("DOT_CUTTER_EXEC_DB_PASSWORD missing"), never the
VALUE. Connection-string assembly happens in locals that are not logged;
prefer passing connect params as discrete keyword args (psycopg3
`connect(user=…, password=…, host=…)`) rather than building a URL string.
L-5 principal-scoped load: an adapter bound to cutter_exec loads ONLY the
EXEC user/password keys; cutter_verify loads ONLY the VERIFY keys. No
code path can read the other principal's password. No single object holds
both passwords.
L-6 no-cutter_ro / no-privileged: the adapter has NO path that loads or uses
workflow_admin / directus / postgres / cutter_ro credentials. cutter_ro is
read-only and is NOT a connection identity for the write path (the writer
reads with its OWN exec/verify principal — doc master §4 find()).
L-7 rotatable-without-code-change: because identity+password come purely
from env keys, password rotation = ALTER ROLE … PASSWORD + update the
matching .env key + process restart; the adapter code/design is unchanged.
No secret is compiled in, so rotation never touches code. (DA-9)
§4 — Connection Construction (target shape, not code)
driver_recommendation: psycopg3 (sync) — mature, parameterised, supports
discrete connect kwargs (no URL needed), explicit transaction control,
SQLSTATE on errors (psycopg.errors.lookup). asyncpg = OPEN (DA-3): the
PASSed skeleton is sync; sync keeps parity and avoids an event-loop
dependency in the adapter. Recommend sync psycopg3.
construction_flow (per phase txn):
1. resolve principal (cutter_exec | cutter_verify) from phase routing (doc 4)
2. load principal-scoped + shared env keys (L-2..L-6); fail closed (L-3)
3. acquire a connection from that principal's small pool (§5), authenticated
AS the principal via SCRAM (PG verified SCRAM-SHA-256 storage)
4. set autocommit = OFF; set isolation per phase (doc 3)
5. run the single phase txn (doc 3); COMMIT/ROLLBACK; return connection to pool
no_url_string: pass user/password/host/port/dbname/sslmode as discrete
parameters; never assemble "postgresql://user:pass@host/db" (avoids the
password ever existing as a single loggable token).
identity_assertion (defence in depth): immediately after connect, the
adapter MAY issue `SELECT current_user` and assert it equals the bound
principal; mismatch → STOP (ConfigMismatch), no writes. (read-only probe,
no secret involved) (DA-4)
§5 — Pooling & Connection-Limit Compliance
hard_constraint: each role has CONNECTION_LIMIT 2 (verified live). Exceeding
it yields SQLSTATE 53300 (too_many_connections) and refuses the session.
pool_design:
- one small pool PER PRINCIPAL (exec pool, verify pool); pools are
independent and never share connections (P-2, separation of duty).
- max_size configurable; DEFAULT max_size = 1, hard ceiling = 2 (reserve
headroom so an ops/diagnostic session is still possible without tripping
the limit). Pool max MUST be clamped to ≤ CONNECTION_LIMIT-derived value
and is NOT hardcoded — it is a config knob (DA-5).
- min_size = 0 (lazy); idle connections closed after a configurable TTL so
rotation/restart reclaims them.
- acquire timeout configurable; on pool exhaustion the phase waits then
fails closed (no unbounded blocking, no opening a 3rd connection).
- the design does NOT assume a single worker: pool + status CAS + advisory
lock (doc 3) make multiple workers safe, but total live connections per
principal across ALL workers must still respect the limit → a future
multi-worker deployment needs an external pooler or a coordinated
max_size budget (DA-6, future-pooler compatibility).
future_pooler: a transaction-mode pooler (e.g. pgbouncer) is COMPATIBLE with
this design (one txn per phase, no session state, no prepared-statement
reliance if statement caching disabled). Recommended but not required;
flagged DA-6. If used, server-side CONNECTION_LIMIT still applies to the
pooler's own backend connections.
§6 — Non-Hardcode / Scaling Rules
- no hardcoded host/port/db/password/DSN anywhere (all env, §2/§3).
- schema/table/column names: the ONLY hardcoded constants permitted are the
inventory-checked governance identifiers (schema cutter_governance + the
12 table names + the status/superseded_by column names) — these are
structural, frozen, and verified against the v0.2/v0.3 inventories. Any
name not in the inventory MUST NOT be hardcoded.
- pool size, acquire timeout, retry bounds, isolation level, sslmode:
all configurable (env or config), with safe fail-closed defaults.
- no single-worker assumption (§5).
- secrets rotatable without code change (L-7).
§7 — Open Decisions (this doc)
DA-2 sslmode target: require vs verify-full (verify-full needs a CA path env
key + cert distribution) — recommend require minimum, verify-full if the
container TLS chain is available. GPT to set.
DA-3 driver: psycopg3 sync (recommended, parity with skeleton) vs asyncpg.
DA-4 post-connect current_user assertion: include (recommended) or omit.
DA-5 pool max_size default/ceiling within CONNECTION_LIMIT 2.
DA-6 future multi-worker / external pooler strategy.
DA-9 rotation runbook ownership (separate cycle).
End of connection & secret-loading design (design only; no secret read; no .env edit; no connection).