KB-127B
dot-iu-cutter v0.4 — Principal Routing Design (design only)
8 min read Revision 1
dot-iu-cutterdieu44v0.4db-adapterdesign-onlyprincipal-routingseparation-of-duty
dot-iu-cutter v0.4 — Principal Routing Design
document_path: knowledge/dev/laws/dieu44-trien-khai/v0.4-db-adapter-design/dot-iu-cutter-v0.4-principal-routing-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 connection, no auth, no write. Routing rules below are a target that mirrors the LIVE least-privilege matrix; they neither widen nor exercise it.
§1 — Two Lanes, Bound at Construction
cutter_exec (lane DOT-991): the AUTHORING writer — MARK, REVIEW, CUT.
cutter_verify (lane DOT-992): the INDEPENDENT writer — VERIFY (incl. the
fail-path compensating set + escalation entry).
binding: a RealPostgresAdapter instance is constructed FOR ONE principal and
cannot change principal for its lifetime (P-2). CutterRuntime already wires
two principal-scoped adapters over the contract (skeleton: exec_adapter /
verify_adapter) — this design keeps that 1:1 with the live roles.
cutter_ro: NOT a routing target. It is NOLOGIN, read-only, views-only. No
phase routes to it. The writer reads with its OWN principal (master §4
find()), never via cutter_ro and never via a separate read role.
forbidden principals: workflow_admin, directus, postgres are in the
skeleton FORBIDDEN_WRITE_PRINCIPALS set and have NO adapter construction
path. They are never a runtime identity.
§2 — Phase → Principal Routing Table (authoritative)
phase principal writes (per transaction-mapping doc) granted?
MARK cutter_exec entry,history,dependency,sweep_log INSERT YES (matrix)
+ UPDATE(status) decision_backlog_entry YES (col)
REVIEW cutter_exec manifest_envelope,manifest_unit_block, YES
review_decision INSERT YES
+ UPDATE(status) decision_backlog_entry YES (col)
+ UPDATE(superseded_by_review_decision_id) YES (col,
on review_decision [re-review only] exec only)
CUT cutter_exec dot_pair_signature,cut_change_set, YES
cut_change_set_affected_row INSERT YES
+ UPDATE(status) decision_backlog_entry YES (col)
VERIFY cutter_verify verify_result,dot_pair_signature INSERT YES
+ (fail) cut_change_set, YES
cut_change_set_affected_row, YES
decision_backlog_entry(escalation), YES
decision_backlog_history INSERT YES
+ UPDATE(status) decision_backlog_entry YES (col)
every "granted?" cell == a tuple in the LIVE 33+3 matrix (credential PASS
2026-05-17). The adapter design adds NO write the matrix does not already
permit; conversely it uses NOTHING the matrix grants beyond these.
§3 — Phase-Principal Mismatch Is Refused (before any SQL)
R-1 the runtime selects the adapter by phase via a FIXED, table-driven map
(the §2 table). There is no code path where MARK/REVIEW/CUT run on
cutter_verify or VERIFY runs on cutter_exec.
R-2 defence-in-depth ladder for a mismatch (any one is sufficient; all
present):
(a) routing map: phase → fixed principal (compile-time/table constant).
(b) skeleton _assert_writer(): principal must be in {cutter_exec,
cutter_verify}; FORBIDDEN set rejects cutter_ro/workflow_admin/
directus/postgres. (already PASSed)
(c) per-adapter capability assertion: a cutter_verify-bound adapter MUST
refuse an insert() targeting review_decision/manifest_*/ (and
cutter_exec refuse verify_result) BEFORE issuing SQL — a typed
PrincipalCapabilityError, not a caught 42501.
(d) the LIVE PG grant matrix: even if (a)-(c) failed, PG itself returns
42501 and the txn rolls back (server-side backstop, independent of
the agent).
R-3 a 42501 actually reaching the client is therefore a STOP-class defect
(the agent attempted an ungranted write — a routing/design bug), handled
per the error doc (STOP + signal, never retry, never privilege-escalate).
R-4 NO cross-phase credential reuse: a connection authenticated as
cutter_exec is never reused to perform VERIFY work and vice-versa; pools
are per-principal and isolated (doc 2 §5). One phase txn = one principal =
one connection from that principal's pool.
§4 — Separation of Duty (SoD) Mapping
SoD-1 the executor (cutter_exec) authors the change; the verifier
(cutter_verify) independently attests it. They are DISTINCT PG roles with
DISJOINT write surfaces:
cutter_exec CANNOT INSERT verify_result (no grant) → cannot self-verify.
cutter_verify CANNOT INSERT review_decision/manifest_* (no grant) →
cannot author the plan it verifies.
This is enforced by the LIVE matrix, not merely by code — code review
alone is not the only line of defence.
SoD-2 G-VERIFY-SOD (flow doc) is satisfied structurally: VERIFY runs on a
different role than CUT by routing construction, so the verifier signature
(DOT-992) is necessarily produced by a different principal than the
executor signature (DOT-991).
SoD-3 the fail-path exception: VERIFY (cutter_verify) DOES write a
compensating cut_change_set + affected_row + an escalation
decision_backlog_entry. This is intentional and matrix-granted: the
verifier may record a FORWARD correction + escalate, but it still CANNOT
author a review_decision/manifest — it cannot "approve its own redo".
The escalation entry re-enters the backlog for an independent MARK→REVIEW
by cutter_exec under a fresh decision. The loop's integrity holds.
SoD-4 column-scoped asymmetry: only cutter_exec holds
UPDATE(superseded_by_review_decision_id) — review-lineage stamping is an
authoring act, never a verifier act. cutter_verify has no such grant.
§5 — Why This Cannot Bypass Least-Privilege
- the adapter's allowed write set is the INTERSECTION of (the routing table)
and (the live grant matrix); it can only ever be ≤ the matrix.
- there is no "admin fallback", no "retry as superuser", no "GRANT then
write" path — the adapter has no DDL/GRANT surface at all (append-only,
skeleton already raises AppendOnlyViolation on delete/truncate; no grant
method exists).
- secrets are principal-scoped (doc 2 L-5): a cutter_exec adapter physically
cannot load the cutter_verify password and vice-versa, so it cannot
"become" the other lane to widen its reach.
- the only way to change what a principal may do is ALTER/GRANT in a
separate, GPT-reviewed credential cycle — never the adapter at runtime.
§6 — Open Decisions (this doc)
DA-4 (shared w/ doc 2) post-connect `SELECT current_user` == bound principal
assertion: recommend INCLUDE (turns a mis-provisioned env into an early
STOP instead of a late 42501).
DA-12 per-adapter capability assertion (R-2c) granularity: hardcode the
per-principal allowed-table set from the inventory (recommended, frozen)
vs derive by querying information_schema.role_table_grants at startup
(self-checking but adds a startup read). Recommend the frozen inventory
constant + an OPTIONAL startup cross-check logged as advisory.
End of principal routing design (design only; mirrors live matrix; no connection; no write).