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 .env edit, 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 own DOT_CUTTER_*_DB_PASSWORD key and assembling the connection in-process from components keeps the secret off every surface. No DOT_CUTTER_*_DB_DSN env key is recommended.
  • Blocker B-A1 / B-D1: adding the 4 connection keys to .env is a prerequisite for code and PG-backed dry-run; it is itself a small, separate, GPT-reviewable change (not done here — .env edit 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).

Back to Knowledge Hub 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